写在前面
在上两篇的内容中(《3.数据预处理》《4.0机器学习模型编码》)我们已经了解了如何获取高质量的数据进行下游任务,那么从这一篇开始,我们就要进入到机器学习算法的领域。首先,第一个是聚类算法。本篇分享几种最常用的聚类算法,包括其原理及评价指标,还有对距离公式的计算。
- 1.python基础;
- 2.ai模型概念+基础;
- 3.数据预处理;
- 4.机器学习模型--1.编码(嵌入);2.聚类;3.降维;4.回归(预测);5.分类;
- 5.正则化技术;
- 6.神经网络模型--1.概念+基础;2.几种常见的神经网络模型;
- 7.对回归、分类模型的评价方式;
- 8.简单强化学习概念;
- 9.几种常见的启发式算法及应用场景;
- 10.机器学习延申应用-数据分析相关内容--1.A/B Test;2.辛普森悖论;3.蒙特卡洛模拟;
- 11.数据挖掘--关联规则挖掘
- 12.数学建模--决策分析方法,评价模型
- 13.主动学习(半监督学习)
- 以及其他的与人工智能相关的学习经历,如数据挖掘、计算机视觉-OCR光学字符识别、大模型等。
目录
曼哈顿距离 (Manhattan Distance) / L1 距离
BIRCH(Balanced Iterative Reducing and Clustering using Hierarchies)
DBSCAN (Density-Based Spatial Clustering of Applications with Noise)
OPTICS(Ordering Points To Identify the Clustering Structure)
CH 指数(Calinski-Harabasz Index):
调整兰德指数(Adjusted Rand Index, ARI)
归一化互信息(Normalized Mutual Information, NMI)
距离
欧式距离 (Euclidean Distance)
概念:计算两点之间的直线距离,即最短路径。它是最常用的距离度量之一。
适用场景: 当数据的各个维度之间的尺度相同且遵循直线距离概念时。
曼哈顿距离 (Manhattan Distance) / L1 距离
概念: 计算两点之间沿坐标轴方向的总路径距离。也叫做“城市街区距离”,因为其类似于沿着街道行走的路径。
适用场景: 当坐标之间没有对角距离的概念,或在高维空间中计算效率高。
明氏距离 (Minkowski Distance)
概念: 明氏距离是欧式距离和曼哈顿距离的广义形式。它取决于一个参数p来调节距离度量的性质。
当p=2时,等价于欧式距离;当p=1时,等价于曼哈顿距离。
适用场景: 根据 p 值调整距离度量方式,灵活性高。
切比雪夫距离 (Chebyshev Distance)
概念: 切比雪夫距离是任意两个点在各个坐标轴上距离的最大值。用于考虑所有方向的最大移动成本。
适用场景: 用于需要评估各方向的极端情况时,适合棋盘问题等。
马氏距离 (Mahalanobis Distance)
概念: 考虑了不同维度之间的相关性。它衡量的是数据点之间的标准化距离,适合有协方差矩阵的数据。
适用场景: 适合数据各维度有不同尺度且存在协方差的情况,常用于多元统计分析中。
余弦距离 (Cosine Distance)
概念: 余弦距离度量两个向量之间的夹角,而不是向量的大小,通常用于文本数据的相似性计算。
适用场景: 主要用于文本分析、向量数据的比较。
汉明距离 (Hamming Distance)
概念: 度量两个等长字符串之间相同位置上不同字符的个数。用于二进制或分类数据的比较。
适用场景: 适合比较二进制字符串或分类数据。
杰卡德距离 (Jaccard Distance)
概念: 计算两个集合的相似性,即两个集合的交集与并集之比,常用于集合相似度的计算。
适用场景: 主要用于集合数据或二元数据的相似度计算。
几个概念
球状簇
在接触具体的聚类算法前,还需搞懂一个概念:什么是球状簇数据。因为聚类算法按适用性分类的话球状簇数据与否是一个重要的条件。
球状簇数据指的是那些簇的形状接近于一个球体(即圆形或球形)的数据。
这种形状的簇通常有以下特点:
- 均匀分布:簇内部的点相对均匀地分布在中心周围。
- 距离中心较短:距离簇中心越近,点的密度越高,反之越低。
- 相对分离:不同簇之间有明显的分隔,通常可以通过欧几里得距离进行区分。
假设有两个簇,分布在二维空间中,形状接近圆形的分布,这就是典型的球状簇数据。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# 生成球状簇数据
X, y = make_blobs(n_samples=300, centers=2, cluster_std=1.0, random_state=42)
# 绘制散点图
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='viridis', marker='o')
plt.title("example")
plt.show()
肘部原则
肘部原则 (Elbow Method) 是一种常用于确定聚类算法(特别是 K-means)中最佳聚类数的方法。
概念
在K-means聚类中,我们通常需要指定聚类的数量 k。不同的 k 会影响聚类的结果。肘部原则通过计算不同 k 值下聚类后的 总误差平方和 (SSE, Sum of Squared Errors),来帮助选择最佳的 k。
步骤
- 计算 SSE:对于每个 k 值,计算簇内所有数据点到其簇中心的误差平方和。
- 绘制图形:以 k 值为横轴,SSE 为纵轴,绘制一条折线图。
- 寻找“肘部”:随着 k 值增加,SSE 会逐渐减小,但当 k 增加到一定程度时,SSE 的减小速度会放缓,形成一个类似“肘部”的拐点。
- 选择拐点:拐点处的 k 值即为最佳的聚类数,通常认为这个 k 可以平衡聚类效果和模型复杂度。
图形解释
- 图的前半段:随着 k 增加,SSE 显著减小,表明聚类效果显著提升。
- 图的后半段:SSE 减小变缓,说明增加聚类数的边际效益减少。
通过选择“肘部”位置的 k,我们能找到一个合理的聚类数,使得聚类效果较好,同时避免过度复杂的模型。
轮廓系数
概念
轮廓系数 (Silhouette Coefficient) 是一种用于评估聚类效果的指标。它通过衡量样本在其所在簇的紧密性及与其他簇的分离程度,来评价聚类的质量。通过计算所有数据点的轮廓系数的平均值,可以得到整个聚类结果的全局轮廓系数,用来评估聚类的整体效果。值越接近 1,说明聚类效果越好。也可用于选择聚类数。
计算
对于每一个数据点 i,轮廓系数 s(i) 通过以下步骤计算:
1. 计算点 i 到同簇内其他点的平均距离 a(i):
- a(i) 是点 i 到其所属簇内所有其他点的平均距离,表示该点在簇内的紧密度。数值越小,说明该点与簇内其他点越接近。
2. 计算点 i 到最近簇(不包括自己所在簇)内所有点的平均距离 b(i):
- b(i) 是点 i 到其最近的其他簇的平均距离,表示该点与其他簇的分离度。数值越大,说明该点离其他簇越远。
3. 计算点 i 的轮廓系数 s(i):
JS散度
这是一种适用于“软聚类”模型选择最佳聚类数的指标
# 计算两个高斯混合模型(GMM)之间的Jensen-Shannon散度(平方根形式,使其成为严格意义上的度量)
# 参考实现来自:https://stackoverflow.com/questions/26079881/kl-divergence-of-two-gmms
# 注意:此处取平方根,使其满足距离度量的性质(非负性、对称性、三角不等式)
def gmm_js(gmm_p, gmm_q, n_samples=10**5):
"""
参数:
gmm_p: 第一个GMM模型
gmm_q: 第二个GMM模型
n_samples: 采样点数(默认为10万)。n_samples越大,估计越准确,但计算成本越高
返回:
两个GMM之间的Jensen-Shannon距离(取平方根后的值)
"""
# 从gmm_p中采样数据点
X = gmm_p.sample(n_samples)[0] # sample()返回(样本, 类别标签),我们只需要样本数据
# 计算采样点在两个GMM下的对数概率密度
log_p_X = gmm_p.score_samples(X) # 在gmm_p下的对数概率
log_q_X = gmm_q.score_samples(X) # 在gmm_q下的对数概率
# 计算混合分布的对数概率(使用logsumexp技巧避免数值溢出)
log_mix_X = np.logaddexp(log_p_X, log_q_X) # 相当于log(exp(log_p_X) + exp(log_q_X))
# 从gmm_q中采样数据点
Y = gmm_q.sample(n_samples)[0]
# 重复上述计算过程
log_p_Y = gmm_p.score_samples(Y)
log_q_Y = gmm_q.score_samples(Y)
log_mix_Y = np.logaddexp(log_p_Y, log_q_Y)
# 计算Jensen-Shannon散度,并取平方根使其成为度量
js_divergence = (
(log_p_X.mean() - (log_mix_X.mean() - np.log(2))) + # KL(P||M)部分
(log_q_Y.mean() - (log_mix_Y.mean() - np.log(2))) # KL(Q||M)部分
) / 2
return np.sqrt(js_divergence) # 取平方根得到JS距离
# 定义要测试的聚类数量范围:从2到7
n_clusters = np.arange(2, 8)
# 设置每个聚类数的迭代次数
iterations = 20
# 初始化存储结果的列表
results = [] # 存储最优JS距离均值
res_sigs = [] # 存储JS距离的标准差
# 遍历每个聚类数
for n in n_clusters:
dist = [] # 临时存储当前聚类数的所有JS距离
# 多次迭代以减少随机性影响
for iteration in range(iterations):
# 将数据随机分为训练集和测试集(各50%)
train, test = train_test_split(X_principal, test_size=0.5)
# 在训练集上拟合GMM模型
gmm_train = GaussianMixture(n, n_init=2).fit(train)
# 在测试集上拟合GMM模型
gmm_test = GaussianMixture(n, n_init=2).fit(test)
# 计算两个GMM之间的JS距离并保存
dist.append(gmm_js(gmm_train, gmm_test))
# 选择表现最好的25%结果(iterations/5次)
selec = SelBest(np.array(dist), int(iterations/5))
# 计算最优结果的均值和标准差
result = np.mean(selec)
res_sig = np.std(selec)
# 保存结果
results.append(result)
res_sigs.append(res_sig)
plt.errorbar(n_clusters, results, yerr=res_sigs)
plt.title("Distance between Train and Test GMMs", fontsize=20)
plt.xticks(n_clusters)
plt.xlabel("N. of clusters")
plt.ylabel("Distance")
plt.show()
量化两个GMM分布之间的差异,适用于各种概率模型比较任务。
- - JS 距离越小 → 训练和测试的 GMM 越相似 → 模型稳定性高
- - JS 距离越大 → 训练和测试的 GMM 差异大 → 模型可能过拟合或欠拟合
JS 距离(Jensen-Shannon Divergence)用于衡量 两个概率分布的相似性。
JS 距离低(接近 0)
- 训练和测试的 GMM(高斯混合模型) 分布几乎相同
- 模型泛化能力强,在新数据上表现稳定
- 聚类数
n
选择合理,没有过拟合或欠拟合
JS 距离高(> 0.1)
- 训练和测试的 GMM 差异大
- 可能的问题:
- 过拟合(
n
太大,模型在训练数据上“死记硬背”) - 欠拟合(
n
太小,模型无法捕捉数据真实结构) - 数据分布不一致(训练/测试数据本身差异大)
- 过拟合(
聚类
聚类是一种无监督学习方法(可见第二篇《ai模型概念+基础》),用于将数据集划分为若干个簇,每个簇中的数据点彼此相似,而不同簇之间的数据点差异较大。一般用于数据探索、模式识别和数据分组,帮助识别数据的内在结构。
K-Means
是一种迭代的聚类算法,将数据分为 K 个簇,目标是最小化簇内数据点与簇中心的欧几里得距离平方和,最大化簇间数据点与簇中心的距离平方和。
- 特点: 简单、高效,适合球状簇的情况。
- 优点: 易于理解和实现,计算速度快。
- 缺点: 需要预先指定簇的数量 𝐾,对初始值敏感,不能处理非球状簇。
步骤:
- 初始化:选择 kkk 个初始的聚类中心(质心),可以随机选取数据点作为质心。
- 分配数据点:将每个数据点分配给最近的质心,形成 kkk 个簇。
- 更新质心:对每个簇计算新的质心,即该簇中所有数据点的平均值。
- 重复步骤 2 和 3:不断分配数据点并更新质心,直到质心不再发生变化或达到最大迭代次数。
- 结束:聚类完成,输出结果。
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 数据
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
# 创建并拟合模型
kmeans = KMeans(n_clusters=2, random_state=0).fit(X)
# 输出簇标签和簇中心
print(kmeans.labels_)
print(kmeans.cluster_centers_)
# 绘制散点图
plt.scatter(X[:, 0], X[:, 1], c=kmeans.labels_, cmap='viridis', marker='o')
# 绘制簇中心
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], c='red', marker='x', s=100, label='Centroids')
# 添加图例和标题
plt.legend()
plt.title('K-Means Clustering')
# 显示图像
plt.show()
输出:
[1 1 1 0 0 0]
[[10. 2.]
[ 1. 2.]]
-
簇标签(Cluster Labels):
- 每个数据点在聚类过程中会被分配到某个簇,K-means 算法根据数据点的距离将它们划分到不同的簇中。簇标签是这些簇的编号或类别,用整数表示(如 0, 1, 2 等)。
- 例如,6 个数据点,算法把这些数据点分成了 2 个簇,那么每个数据点都会得到一个簇标签,用来表示它属于哪个簇,在图中则表示为不同颜色。
- 簇标签是算法输出的结果之一,用于标记每个数据点对应的簇。例如,
kmeans.labels_
会返回每个数据点所属的簇编号。
-
簇中心(Cluster Centers):
- 每个簇都有一个中心点,称为簇中心,它是该簇中所有数据点在特征空间的质心(centroid),即所有数据点的平均值。簇中心可以看作是代表整个簇的特征。
- 簇中心用于描述该簇的中心位置,并且在算法的迭代过程中会不断更新,以便让簇更好地适应数据。
- 在代码中,
kmeans.cluster_centers_
返回的是每个簇的中心点坐标(即簇中心)。
自行实现:
def distEclud(vecA, vecB):
"""
函数说明:利用欧式距离来计算两个向量之间的距离
parameters:
vecA - A样本的特征向量(二维坐标值)
vecB - B样本的特征向量(二维坐标值)
return:
Dist - 两个样本点之间的欧式距离
"""
return sqrt(sum(power(vecA - vecB, 2)))
def randCent(DataMat, k):
"""
函数说明:从当前样本点中随机选取k个初始簇中心
parameters:
DataMat - 数据集
k - 聚类后簇的数量
return:
centroids - 簇中心列表,numpy 矩阵形式
"""
n = shape(DataMat)[1] # 特征数量
centroids = mat(zeros((k, n)))
for j in range(n):
minJ = min(DataMat[:, j])
rangeJ = float(max(DataMat[:, j]) - minJ)
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
return centroids
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent, plot_steps=True, show_step=False):
"""
函数说明:K-均值算法
parameters:
dataSet - 数据集
k - 簇个数
distMeas - 距离计算函数
createCent - 创建初始质心函数
return:
centroids - 质心列表
clusterAssment - 簇分配结果矩阵(每行前两列分别是簇索引和距离)
"""
m = shape(dataSet)[0]
clusterAssment = mat(zeros((m, 2)))
centroids = createCent(dataSet, k)
clusterChanged = True
iteration = 0
while clusterChanged:
if (show_step==True):
# 可视化聚类结果
drawDataSet(dataMat, centroids, clusterAssment, k, title=f'Iteration {iteration}')
print(centroids)
iteration += 1
clusterChanged = False
# 遍历所有样本点
for i in range(m):
minDist = inf
minIndex = -1
# 遍历所有簇中心,找到最近的簇
for j in range(k):
distJI = distMeas(centroids[j, :], dataSet[i, :])
if distJI < minDist:
minDist = distJI
minIndex = j
# 如果该样本点的簇发生改变,则记录下来
if clusterAssment[i, 0] != minIndex:
clusterChanged = True
clusterAssment[i, :] = minIndex, minDist**2
# 更新簇中心
for cent in range(k):
# 取出所有分配到该簇的样本点
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
if len(ptsInClust) > 0:
centroids[cent, :] = mean(ptsInClust, axis=0)
else:
# 如果某个簇没有点分配,重新随机初始化该簇中心
centroids[cent, :] = createCent(dataSet, 1)
return centroids, clusterAssment
def elbow_and_silhouette(dataMat, max_k=10):
"""
Compute and plot the Elbow Method and Silhouette Scores for different k values.
Parameters:
dataMat - Dataset
max_k - Maximum number of clusters to try
Returns:
None, displays the plots
"""
inertia = [] # Within-Cluster Sum of Squares
silhouette_scores = []
K = range(2, max_k+1)
for k in K:
print(f'Processing k={k}')
centroids, clusterAssment = kMeans(dataMat, k, plot_steps=False, show_step=False)
# Calculate inertia(惯性)
inertia_k = sum(clusterAssment[:, 1])
inertia.append(inertia_k)
# Calculate silhouette score(轮廓系数)
# Convert cluster assignments to labels
labels = clusterAssment[:, 0].A.flatten()
if len(set(labels)) > 1:
sil_score = silhouette_score(dataMat, labels)
silhouette_scores.append(sil_score)
print(f'k={k}, Inertia={inertia_k:.2f}, Silhouette Score={sil_score:.4f}')
else:
silhouette_scores.append(-1) # Invalid silhouette score
print(f'k={k}, Inertia={inertia_k:.2f}, Silhouette Score=Undefined (only one cluster)')
# Plot Elbow Method(找惯性下降趋势明显减缓的点)
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(K, inertia, 'bo-')
plt.xlabel('Number of clusters (k)')
plt.ylabel('Inertia (Within-Cluster Sum of Squares)')
plt.title('Elbow Method for Optimal k')
# Plot Silhouette Scores(找最大的点)
plt.subplot(1, 2, 2)
plt.plot(K, silhouette_scores, 'bo-')
plt.xlabel('Number of clusters (k)')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Scores for Various k')
plt.tight_layout()
plt.show()
def drawDataSet(dataMat, centList, clusterAssment, k, title='K-Means Clustering'):
"""
函数说明:将聚类结果可视化
parameters:
centList - 质心列表
clusterAssment - 簇分配结果矩阵
dataMat - 数据集
k - 簇个数
return:
无,显示一张图
"""
# 颜色列表
colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k']
if k > len(colors):
# 如果簇数超过预定义颜色数量,则重复使用颜色
colors = colors * (k // len(colors) + 1)
# 绘制所有数据点
for i in range(shape(dataMat)[0]):
mark = int(clusterAssment[i, 0])
plt.plot(dataMat[i, 0], dataMat[i, 1], marker='o', color=colors[mark])
# 绘制质心
plt.scatter(centList[:, 0].flatten().A[0], centList[:, 1].flatten().A[0],
marker='x', s=200, linewidths=3, color='black')
plt.xlabel('X1')
plt.ylabel('X2')
plt.title(title)
plt.show()
if __name__ == '__main__':
"""
按照Kmeans原理来调用上述函数
"""
# 设置随机种子以保证结果可重复
random.seed(1)
# 加载数据
data_file = '\1.txt'
dataMat = loadDataSet(data_file)
# Determine optimal k using Elbow Method and Silhouette Scores
elbow_and_silhouette(dataMat, max_k=10)
# 设置簇的数量
k = 4
# 运行K-Means算法
centroids, clusterAssment = kMeans(dataMat, k, plot_steps=True, show_step=True)
层次聚类(Hierarchical Clustering)
基于数据点之间的距离进行逐层聚合或分裂,形成一个树状的层次结构(树状图)。可以通过观察离群点的层次关系来检测异常数据点。这些异常点可能会在树状图的低层次部分表现为孤立。
- 特点: 可以生成不同层次的聚类结果,提供更全面的数据层次关系。
- 优点: 不需要预先指定簇的数量,能够生成不同层次的聚类。可以用来选择和分析重要特征。通过观察哪些特征在聚类过程中变得重要,可以帮助理解数据和选择合适的特征进行建模。
- 缺点: 计算复杂度较高,难以处理大规模数据集。
步骤:
可以分为凝聚层次聚类和分裂层次聚类,这里介绍常用的凝聚层次聚类(自下而上)步骤:
- 初始化:将每个数据点视为一个单独的簇,共有 nnn 个簇(每个簇包含一个数据点)。
- 计算相似度:计算所有簇之间的相似度或距离,常用距离如欧氏距离或余弦相似度。
- 合并簇:将最相似的两个簇合并为一个簇。
- 重复合并:不断重复步骤 2 和 3,每次合并两个最近的簇,直到所有数据点最终聚成一个簇,或者达到预设的聚类数。
- 形成聚类树:通过合并过程生成一棵树状的树状图(dendrogram),展示不同层次的聚类结构。
from scipy.cluster.hierarchy import dendrogram, linkage
import numpy as np
import matplotlib.pyplot as plt
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
# 进行层次聚类
Z = linkage(X, 'ward')
# 绘制树状图
dendrogram(Z)
plt.show()
linkage
函数使用 Ward 距离度量方法,它尝试在每次合并簇时最小化簇内的方差。
输出:
图的解读:
1. X轴(水平轴):
- X轴上的点代表每个数据点或簇。如
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
中有6个数据点,这些点按顺序展示在树状图的X轴上。
2. Y轴(垂直轴):
- Y轴表示的是距离度量,即簇之间的相似度或距离。树状图中,每当两簇被合并时,Y轴上的高度表示这两个簇在合并时的距离。距离越大,表示两个簇之间的相似度越低。
3. 簇的合并:
- 图中的线表示簇的合并过程。从下往上看,越早合并的簇表示它们之间的相似度越高(距离小),而越晚合并的簇表示它们的相似度较低(距离大)。
- 如果两个簇的连线靠近底部,说明它们在聚类的初期就被合并,这表明它们彼此非常相似。
4. 水平线:
- 树状图中的水平线连接了两个簇,这条线的高度表示合并时的距离。可以通过这条线的位置来判断簇的相似度。例如,在树状图中距离较低的水平线代表相似的簇,而较高的水平线表示这些簇相似度较低。
5. 截断线(Threshold):
- 可以人为设置一个截断线(如用
plt.axhline(y=some_value)
),根据想要的聚类数量选择一个Y轴的高度,将树状图“切断”。在这条线以下的簇会被视为一个整体,生成较少数量的簇。
层次聚类 v.s K-means
层次聚类提供了一种可视化和分析数据层次结构的方式,而K-means更适合于在已知的类数下进行快速分组。选择哪种方法取决于具体的应用需求和数据特性。
BIRCH(Balanced Iterative Reducing and Clustering using Hierarchies)
基于层次聚类,设计用于大规模数据集的聚类方法,使用聚类特征树(CF树)来压缩数据。
- 特点: 适用于大规模数据集,能够动态调整聚类簇数。
- 优点: 高效处理大规模数据,内存使用较低。
- 缺点: 适合球状簇,难以处理复杂簇结构。
步骤:
1. 构建 CF 树:
- 通过读取数据点并将其插入到CF树中,CF树由多个节点和簇特征向量组成。簇特征向量保存了聚类信息(如质心和数据点数),用于表示和紧凑存储簇的特征。
- CF 树会根据指定的阈值(threshold)控制每个叶节点中的簇规模,防止树的规模过大。
2. 聚类压缩:
- BIRCH 通过树的层次结构压缩数据,将类似的数据点分配到相同的簇中。
- 如果新的数据点与已有簇的距离较近,则会被合并到相应的簇中;否则,可能会创建新的簇。
3. 进一步聚类:
- 构建完 CF 树后,BIRCH 可以通过全局聚类(如 K-means 等算法)来进一步细化和处理簇。
from sklearn.cluster import Birch
import numpy as np
import matplotlib.pyplot as plt
# 示例数据
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
# 创建并拟合 BIRCH 模型
birch = Birch(n_clusters=2)
birch.fit(X)
# 输出簇标签和簇中心
labels = birch.labels_
centroids = birch.subcluster_centers_
print("簇标签:", labels)
print("簇中心:", centroids)
# 绘制散点图
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='rainbow')
plt.scatter(centroids[:, 0], centroids[:, 1], color='black', marker='x', s=100)
plt.show()
输出:
簇标签: [1 1 1 0 0 0]
簇中心: [[ 1. 2.]
[ 1. 4.]
[ 1. 0.]
[10. 2.]
[10. 4.]
[10. 0.]]
输出解读:
- BIRCH 的工作过程是逐步聚合数据点形成小簇(即子簇),这些子簇存储在 CF 树的叶节点中。即使最终的全局聚类数量是
n_clusters=2
,在树构建的过程中,可能形成多个子簇。这是因为在构建 CF 树的过程中,BIRCH会根据数据分布进行局部的聚类压缩。子簇是在数据分层压缩的过程中产生的局部结构。 n_clusters=2
表示 BIRCH 最终会将数据分成 2 个簇。但是,在初步构建 CF 树的过程中,BIRCH 创建了多个子簇(例如 6 个),每个子簇有它自己的质心。这些子簇是对数据的初步分割,用来帮助算法在数据上建立层次结构。- 最终,当设定
n_clusters=2
,BIRCH 会将这些子簇合并为 2 个全局簇,但子簇的信息仍然保留并可以输出,因此会看到 6 个子簇中心的坐标。
DBSCAN (Density-Based Spatial Clustering of Applications with Noise)
是一种基于密度的聚类算法,能够识别任意形状的簇,并且可以处理噪声数据。通过找到密度足够高的数据点形成簇,并将噪声点标记为离群点。
- 特点: 可以发现任意形状的簇,能够处理噪声和离群点。
- 优点: 不需要指定簇的数量,能够发现任意形状的簇,鲁棒性较强。
- 缺点: 对参数(如密度阈值)的选择敏感,难以处理密度变化较大的簇。
步骤:
- 对于每个点,计算其ε-邻域内的点的数量。
- 如果邻域内的点数大于某个阈值,将该点标记为核心点,并将其邻域内的点视为同一个簇。
- 扩展簇,直到没有新的核心点被加入。
- 将无法归入任何簇的点标记为噪声。
from sklearn.cluster import DBSCAN
import numpy as np
import matplotlib.pyplot as plt
# 数据集
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
# 创建并拟合模型
db = DBSCAN(eps=3, min_samples=2).fit(X)
# 输出簇标签
print(db.labels_)
# 可视化聚类结果
labels = db.labels_
# 为了方便可视化,区分噪声点和不同的簇
unique_labels = set(labels)
# 为每个簇分配颜色
colors = [plt.cm.Spectral(each)
for each in np.linspace(0, 1, len(unique_labels))]
plt.figure(figsize=(6, 6))
# 遍历每个簇,绘制其数据点
for k, col in zip(unique_labels, colors):
if k == -1:
# -1 表示噪声点
col = [0, 0, 0, 1] # 黑色
class_member_mask = (labels == k)
# 簇中的数据点
xy = X[class_member_mask]
plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=tuple(col),
markeredgecolor='k', markersize=14)
plt.title('DBSCAN')
plt.show()
输出:
[0 0 0 1 1 1]
OPTICS(Ordering Points To Identify the Clustering Structure)
是DBSCAN的扩展版本,能够更好地处理不同密度的簇。
- 特点: 不需要指定簇的数量,可以发现任意形状的簇。
- 优点: 适用于密度变化较大的数据集,能够处理噪声和离群点。
- 缺点: 计算复杂度高,参数选择较为困难。
聚类模型评价
评价一个聚类模型通常依赖于聚类的质量、稳定性和模型的适用性。
内部指标(无监督方法)
这些指标不需要真实标签,只基于数据和聚类结果来评价模型。
轮廓系数(Silhouette Coefficient)
衡量每个样本与其所在簇内点的相似度与它与最近其他簇的相似度之间的差异。值范围为 [-1, 1],值越大说明聚类效果越好。
from sklearn.metrics import silhouette_score
silhouette_avg = silhouette_score(X, labels)
CH 指数(Calinski-Harabasz Index):
衡量簇间距离(分离度)和簇内紧密程度。值越大,表明簇间分离越好,簇内点距离中心点越近。
from sklearn.metrics import calinski_harabasz_score
ch_score = calinski_harabasz_score(X, labels)
DB 指数(Davies-Bouldin Index)
衡量簇的紧密度与分离度。值越小表示聚类效果越好。
from sklearn.metrics import davies_bouldin_score
db_score = davies_bouldin_score(X, labels)
外部指标(有监督方法)
当有真实的标签时(如进行监督学习时),可以用以下指标来衡量聚类结果与真实标签的匹配程度。
调整兰德指数(Adjusted Rand Index, ARI)
衡量聚类结果和真实标签的一致性。值范围在 [-1, 1],1 表示完全一致,0 表示随机聚类。
from sklearn.metrics import adjusted_rand_score
ari_score = adjusted_rand_score(true_labels, predicted_labels)
归一化互信息(Normalized Mutual Information, NMI)
衡量聚类结果与真实标签的互信息。值范围在 [0, 1],值越大表示越一致。
from sklearn.metrics import normalized_mutual_info_score
nmi_score = normalized_mutual_info_score(true_labels, predicted_labels)
可视化方法
- 散点图:在2D或3D空间中可视化聚类结果,观察聚类的分布和簇之间的分离情况。对于高维数据,可以使用PCA或t-SNE降维。
- 树状图(层次聚类):查看层次聚类的聚合过程,判断簇的层次结构。
聚类数的选择
- 肘部法则(Elbow Method):绘制簇内平方误差(SSE)与簇数量的关系曲线(会在第七篇《对回归、分类模型的评价》中涉及),选择SSE开始平缓处的簇数量。
- 轮廓系数分析:通过观察不同簇数量时的轮廓系数来确定最佳的簇数量。
模型的适用性
- 对数据形态的适应性:一些模型(如 K-Means)更适合于球状簇,而像 DBSCAN 则适合发现任意形状的簇。
- 鲁棒性:模型对噪声的敏感度,比如 DBSCAN 对噪声点比较鲁棒,而 K-Means 容易受到离群点影响。
其他
我认为除了上述这几种所谓的“聚类”专用算法,还有一种聚类就是字面意义的“分类”,而这种分类的聚类操作在一些具有特别的实际意义时是应该首先被考虑的。
举个例子,要求是对灾难发生概率聚类为低中高风险三个类别,根据我们国家出台的相关文件,已经有现成的规定发生概率小于10%的为低风险,大于30%的为高风险,中间为中风险。那此时首先应该考虑的既符合题意又具有现实意义的聚类操作应是按百分位划分,而不是首先考虑灾难发生概率之间的距离或是相关性层面。
实战
问题:
顾客分类聚类分析问题,分析顾客的消费行为数据,根据顾客的年龄、收入和消费分数等特征,将顾客划分为多个类别,以便进行差异化营销。
思路:
- 数据读取与预处理:读取顾客数据集,并对“性别”变量进行数值编码(将“Female”转为0,“Male”转为1)。
- 确定聚类参数k:通过肘部法则(SSE误差折线图)和轮廓系数评估不同的K值,以找到最佳的簇数。
- K-means聚类:选择20个簇(n_clusters=20)进行K-means聚类,并使用
silhouette_score
评估聚类效果。 - 可视化聚类结果:使用三维散点图(顾客年龄、年收入和消费分数为三个维度)展示不同簇的分布情况。
- 统计各类别顾客数目:查看每个簇中的顾客数量。
数据:
数据是自己随机拟定的,不含任何实际意义。
CustomerID,Gender,Age,Annual Income (k$),Spending Score (1-100)
1,Male,22,89,92
2,Female,51,62,51
3,Female,54,101,94
4,Male,42,52,39
5,Female,21,87,75
6,Female,54,71,58
7,Male,37,81,56
8,Female,35,142,42
9,Male,52,95,71
10,Female,56,115,29
11,Male,19,61,63
12,Female,64,57,7
13,Female,43,30,40
14,Male,61,113,67
15,Male,38,41,38
16,Male,67,79,46
17,Female,54,114,38
18,Female,27,85,92
19,Male,68,61,90
20,Female,24,140,82
21,Female,54,94,88
22,Female,67,124,18
23,Female,45,43,36
24,Male,63,64,8
25,Male,18,72,32
26,Female,28,73,22
27,Male,24,138,64
28,Male,53,31,60
29,Female,56,34,66
30,Male,33,116,77
31,Male,29,124,97
32,Female,33,110,35
33,Female,52,107,4
34,Female,27,127,83
35,Female,54,75,87
36,Female,48,57,27
37,Female,57,93,89
38,Male,57,123,93
39,Male,38,124,66
40,Female,53,118,33
41,Female,68,73,68
42,Male,33,130,99
43,Male,21,85,37
44,Female,45,30,12
45,Female,43,134,88
46,Female,45,92,27
47,Female,57,107,12
48,Female,29,58,14
49,Female,56,110,11
50,Female,56,28,25
51,Female,21,108,12
52,Female,35,33,21
53,Female,37,92,96
54,Male,46,108,89
55,Female,49,53,17
56,Male,33,92,16
57,Female,25,124,44
58,Female,27,142,28
59,Male,35,123,7
60,Female,50,125,63
61,Male,67,124,8
62,Female,60,139,69
63,Female,65,63,34
64,Male,30,143,45
65,Male,39,70,57
66,Female,35,19,29
67,Male,32,127,75
68,Female,35,81,15
69,Female,25,138,80
70,Female,55,75,82
71,Female,52,34,15
72,Female,31,127,43
73,Female,29,18,14
74,Female,38,21,35
75,Female,46,133,38
76,Female,33,144,34
77,Female,28,86,19
78,Male,34,84,53
79,Female,61,102,75
80,Female,65,110,15
81,Male,40,31,55
82,Female,61,82,32
83,Female,25,81,40
84,Male,33,101,27
85,Male,40,57,50
86,Male,45,100,49
87,Female,26,126,69
88,Male,26,77,86
89,Female,21,95,70
90,Male,58,130,41
91,Female,64,28,3
92,Male,60,137,36
93,Female,65,111,78
94,Male,57,109,82
95,Female,30,80,37
96,Female,30,110,50
97,Male,49,115,62
98,Female,26,129,14
99,Female,28,47,97
100,Female,23,126,73
101,Male,46,64,45
102,Male,59,96,24
103,Female,21,86,13
104,Male,56,64,5
105,Female,54,97,48
106,Male,41,137,15
107,Male,20,101,23
108,Female,31,45,23
109,Female,20,45,10
110,Male,38,38,99
111,Female,22,129,50
112,Female,60,94,61
113,Female,68,106,63
114,Female,25,91,12
115,Male,41,137,21
116,Male,27,64,17
117,Male,19,134,3
118,Female,31,35,47
119,Female,53,92,18
120,Female,21,47,49
121,Female,20,63,5
122,Female,63,25,96
123,Male,47,120,31
124,Female,68,118,31
125,Female,21,70,35
126,Male,57,48,25
127,Female,25,60,34
128,Male,23,119,49
129,Male,61,80,98
130,Male,21,109,35
131,Female,59,80,29
132,Female,55,105,27
133,Female,26,104,85
134,Female,31,125,77
135,Male,53,81,22
136,Female,36,126,57
137,Female,30,53,36
138,Male,64,35,28
139,Female,32,65,3
140,Female,61,92,19
141,Female,47,135,89
142,Male,22,137,28
143,Female,33,125,73
144,Female,35,138,21
145,Male,38,19,5
146,Male,67,112,6
147,Female,68,22,3
148,Male,34,22,21
149,Female,21,127,27
150,Female,21,94,84
151,Male,24,107,71
152,Female,43,118,20
153,Male,50,54,16
154,Female,66,54,47
155,Female,67,86,32
156,Female,50,134,53
157,Female,57,78,80
158,Female,58,128,24
159,Female,32,84,7
160,Female,40,41,57
161,Male,47,70,91
162,Male,28,43,21
163,Female,60,121,9
164,Male,57,101,89
165,Female,37,32,37
166,Male,44,89,93
167,Female,58,106,33
168,Male,42,136,24
169,Female,21,97,36
170,Female,52,89,88
171,Male,23,97,6
172,Male,41,18,69
173,Female,23,37,26
174,Female,31,86,17
175,Female,57,83,80
176,Male,31,60,71
177,Female,22,123,78
178,Female,59,121,45
179,Male,28,23,55
180,Male,63,92,16
181,Female,32,91,5
182,Female,50,66,42
183,Female,39,135,92
184,Male,67,21,33
185,Female,66,41,30
186,Male,64,56,47
187,Female,41,44,92
188,Female,38,45,33
189,Female,57,142,19
190,Female,53,41,34
191,Female,34,113,11
192,Male,32,124,45
193,Male,67,60,92
194,Male,65,26,60
195,Female,48,77,56
196,Female,56,32,55
197,Male,61,41,64
198,Female,25,103,32
199,Female,24,80,97
200,Female,22,136,87
201,Female,30,97,29
202,Female,56,25,74
203,Female,67,128,27
204,Male,49,82,13
205,Female,26,72,91
206,Male,21,24,90
207,Female,39,93,84
208,Female,67,93,37
209,Female,36,21,9
210,Female,35,132,17
211,Male,60,79,16
212,Female,56,52,58
213,Female,29,60,94
214,Female,24,120,71
215,Male,39,28,77
216,Female,35,75,35
217,Male,29,112,69
218,Female,45,98,28
219,Male,35,109,18
220,Male,54,75,53
221,Female,23,131,39
222,Female,27,60,91
223,Female,49,61,93
224,Female,52,109,33
225,Female,65,37,26
226,Female,53,52,13
227,Male,30,130,63
228,Female,31,48,4
229,Female,40,35,99
230,Female,57,40,44
231,Female,26,108,78
232,Female,32,81,84
233,Female,45,140,50
234,Female,60,142,7
235,Male,22,136,98
236,Male,36,103,91
237,Male,27,80,42
238,Female,25,90,75
239,Female,58,44,17
240,Male,34,118,5
241,Female,52,18,4
242,Female,33,49,58
243,Female,64,104,45
244,Female,33,87,32
245,Male,25,115,99
246,Male,36,120,69
247,Female,24,118,32
248,Male,55,72,38
249,Female,52,142,13
250,Female,38,56,71
251,Female,49,37,36
252,Male,63,25,50
253,Male,61,141,15
254,Female,24,98,58
255,Male,52,121,12
256,Male,28,45,15
257,Female,25,118,94
258,Female,41,21,21
259,Female,50,101,34
260,Female,58,72,38
代码:
# k-means
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(font='SimHei')
import warnings
warnings.filterwarnings('ignore')
# 读取数据
data = pd.read_csv(r'数据文件地址')
print(data.head()) #显示DataFrame或Series的前几行,默认情况下显示前5行
print(data.shape)
data.info()
#数据预处理 将性别变量进行编码转化
data['Gender'].replace(to_replace={'Female':0,'Male':1},inplace=True)
print(data.head())
#确定聚类参数k
#1. 肘部法则
# 通过绘制不同K值对应的聚类误差(通常是SSE,Sum of Squared Errors)的折线图,
# 通常从较小的k值(1-10)开始,来寻找一个“肘点”(拐点),该点对应的K值即为较为合适的聚类数。
from sklearn.cluster import KMeans
new_df = data.drop('CustomerID',axis=1) #从DataFrame中删除名为'CustomerID'的列。
# 肘部法则
loss = []
for i in range(2,21):
model = KMeans(n_clusters=i).fit(new_df) #n_clusters=i 指定要分成的簇的数量
loss.append(model.inertia_) #model.inertia_ :KMeans中的一个属性,表示聚类结果的簇内的总平方距离,越小越好。
plt.plot(range(2,21),loss)
plt.xlabel('k')
plt.ylabel('loss')
plt.title('肘部法则')
plt.show()
#确定聚类参数k
#2. 轮廓系数
# 结合了聚类的紧密度(簇内样本距离平均值)和分离度(不同簇之间样本距离平均值,找最近的)
# 轮廓系数:s = (b - a) / max(a, b) 从而提供一个综合的聚类效果评估。
# 如果轮廓系数接近于1,则表示簇内样本紧密度高,簇间分离度较好,聚类效果较好。
# 如果轮廓系数接近于-1,则表示簇内样本紧密度较低,簇间分离度不好,聚类效果较差。
# 如果轮廓系数接近于0,则表示簇内外样本的距离相差不大,聚类效果一般。
from sklearn.metrics import silhouette_score
score = []
for i in range(2,21):
model = KMeans(n_clusters=i).fit(new_df)
score.append(silhouette_score(new_df, model.labels_, metric='euclidean'))
#model.labels_ 是聚类模型对数据new_df进行聚类后得到的簇标签。
#etric='euclidean'表示计算距离时采用的距离度量,这里采用欧氏距离(Euclidean Distance)。
plt.plot(range(2,21),score)
plt.xlabel('k')
plt.ylabel('silhouette_score')
plt.title('轮廓系数')
plt.show()
#Kmeans聚类
kmeans = KMeans(n_clusters=20, init='k-means++')#label从0-19 共20个簇
#n_jobs控制并行计算的数量。设置-1表示使用所有可用的CPU核心进行并行计算
#init指定了初始化簇中心的方法。'k-means++'是一种智能的初始化方法,
# 它会选择初始簇中心,使得它们之间的距离较远,有助于加速算法的收敛速度和提高聚类结果的质量。
kmeans.fit(new_df)
print(silhouette_score(new_df, kmeans.labels_, metric='euclidean'))
#聚类效果可视化
clusters = kmeans.fit_predict(data.iloc[:,1:])
new_df["label"] = clusters
print(new_df.head())
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(new_df.Age[new_df.label == 0], new_df["Annual Income (k$)"][new_df.label == 0], new_df["Spending Score (1-100)"][new_df.label == 0], c='blue', s=60)
#表示在 new_df.label == 0 的条件下,分别提取 Age、Annual Income (k$) 和 Spending Score (1-100) 列的值,然后绘制三维散点图。Age,income,spending分别作为xyz
ax.scatter(new_df.Age[new_df.label == 1], new_df["Annual Income (k$)"][new_df.label == 1], new_df["Spending Score (1-100)"][new_df.label == 1], c='red', s=60)
ax.scatter(new_df.Age[new_df.label == 2], new_df["Annual Income (k$)"][new_df.label == 2], new_df["Spending Score (1-100)"][new_df.label == 2], c='green', s=60)
ax.scatter(new_df.Age[new_df.label == 3], new_df["Annual Income (k$)"][new_df.label == 3], new_df["Spending Score (1-100)"][new_df.label == 3], c='orange', s=60)
ax.scatter(new_df.Age[new_df.label == 4], new_df["Annual Income (k$)"][new_df.label == 4], new_df["Spending Score (1-100)"][new_df.label == 4], c='black', s=60)
ax.scatter(new_df.Age[new_df.label == 5], new_df["Annual Income (k$)"][new_df.label == 5], new_df["Spending Score (1-100)"][new_df.label == 5], c='purple', s=60)
ax.view_init(30, 185)
plt.show()
#查看各聚类类别的个数
data['label'] = clusters
print(data['label'].value_counts())
print(data.head())
输出:
总结
聚类