一、k-means 是什么
一句话:把数据分成 k 组,让每组内的点尽量靠近自己的中心。
直觉理解
想象你桌上有 100 颗糖豆散落一地,你想把它们分成 3 堆。你的做法大概是:
- 随便放 3 个碗在桌上当"堆心"
- 每颗糖豆归到离它最近的那个碗
- 根据分好的糖豆,把每个碗移到这一堆的中心位置
- 糖豆重新按最近距离归碗
- 反复 3、4 步,直到碗不再移动
这就是 k-means。
算法步骤
输入:数据集 D,分组数 k 输出:k 个簇及其中心点 1. 从 D 中随机选 k 个点作为初始中心 2. 重复: a) 分配:把每个数据点归到距离最近的中心所在的簇 b) 更新:重新计算每个簇的中心(所有点的均值) 3. 直到中心不再变化(或变化小于阈值)用一个简单例子走一遍:
数据点: A(1,1) B(2,1) C(4,3) D(5,3) k=2 第1轮:随机选中心 C1=A(1,1), C2=D(5,3) 分配:A→簇1, B→簇1(离C1近), C→簇2(离C2近), D→簇2 更新:C1=(1.5,1), C2=(4.5,3) 第2轮: 分配:A→簇1, B→簇1, C→簇2, D→簇2 (没变) 更新:C1=(1.5,1), C2=(4.5,3) (没变) 结束,分两组:{A,B} 和 {C,D}数学形式
目标函数(惯性 / Inertia):
J=∑i=1k∑x∈Ci∥x−μi∥2J = \sum_{i=1}^{k} \sum_{x \in C_i} \|x - \mu_i\|^2J=i=1∑kx∈Ci∑∥x−μi∥2
其中μi\mu_iμi是第iii个簇的中心,CiC_iCi是第iii个簇的所有点。
k-means 本质上就是在最小化这个值——让每个点到自己簇中心的距离平方和尽量小。
时间复杂度:O(n · k · d · t),n=样本数,k=簇数,d=维度,t=迭代轮数。通常很快。
三个老问题
| 问题 | 说明 | 应对 |
|---|---|---|
| k 怎么选 | 需要事先指定分组数 | 肘部法则、轮廓系数、Gap Statistic |
| 初始中心敏感 | 不同起点可能收敛到不同结果 | k-means++(优选初始点)、多次运行取最优 |
| 只适合球形簇 | 只按距离划分,组不了月牙形、环形等异形簇 | 换 DBSCAN、谱聚类等 |
k-means++ 初始化:不随机选,而是让第一个中心随机选,之后每个新中心尽量离已有中心远一些。这样起步就更稳,收敛更快。
代码示例
fromsklearn.clusterimportKMeansfromsklearn.datasetsimportmake_blobsimportmatplotlib.pyplotasplt# 生成模拟数据X,_=make_blobs(n_samples=300,centers=4,random_state=42)# 聚类km=KMeans(n_clusters=4,init='k-means++',n_init=10,random_state=42)labels=km.fit_predict(X)# 可视化plt.scatter(X[:,0],X[:,1],c=labels,cmap='viridis',s=30)plt.scatter(km.cluster_centers_[:,0],km.cluster_centers_[:,1],c='red',marker='X',s=200,label='中心点')plt.legend()plt.show()二、如何评估聚类效果
聚类没有标准答案(无监督学习),所以评估比分类/回归要棘手。分两大类:
内部指标(不需要真实标签)
1. 惯性 / SSE(越小越好)
就是上面那个目标函数 J 本身——每个点到自己簇中心的距离平方和。
- 越小说明簇内越紧凑
- 但 k 越大必然越小,k=n 时为 0,所以不能直接比
2. 肘部法则(选 k 用)
k=1 SSE=1000 k=2 SSE=400 k=3 SSE=200 k=4 SSE=180 ← 下降变缓的拐点 k=5 SSE=170 k=6 SSE=165画一条 k-SSE 曲线,找"拐弯"的地方——拐点之前加 k 收益大,拐点之后加了也差不多。
SSE |\ | \ | \___ | \_______ ← 拐点(肘部)就是合适的 k +----------→ k局限:拐点有时候不明显,人为判断有主观性。
3. 轮廓系数(Silhouette Score)
取值[-1, 1],越大越好。
每个点算一个轮廓值:
s(i)=b(i)−a(i)max(a(i),b(i))s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}s(i)=max(a(i),b(i))b(i)−a(i)
- a(i):点 i 到同簇其他点的平均距离(越小越好,说明自己簇很紧)
- b(i):点 i 到最近其他簇的平均距离(越大越好,说明跟别的簇远)
直观理解:
| 轮廓值 | 含义 |
|---|---|
| 接近 1 | 这个点跟自己人很近,跟外人很远,分对了 |
| 接近 0 | 在两个簇的边界上,分得不明确 |
| 接近 -1 | 跟自己人远,跟外人近,可能分错了 |
fromsklearn.metricsimportsilhouette_scoreforkinrange(2,10):km=KMeans(n_clusters=k,n_init=10,random_state=42)labels=km.fit_predict(X)score=silhouette_score(X,labels)print(f"k={k}, 轮廓系数={score:.3f}")选轮廓系数最大的 k。
4. CH 指数(Calinski-Harabasz Index)
簇间分散度 / 簇内紧凑度,越大越好。计算比轮廓系数快。
5. Davies-Bouldin 指数
每个簇跟"最相似"的簇之间的相似度取平均,越小越好。
外部指标(有真实标签时用)
如果碰巧知道真实分类(比如用标注数据做实验),可以对比:
| 指标 | 思路 | 取值 |
|---|---|---|
| ARI(调整兰德指数) | 聚类结果和真实标签的一致程度,排除了随机猜的干扰 | [-1, 1],1=完全一致 |
| NMI(归一化互信息) | 两种分组共享了多少信息量 | [0, 1],1=完全一致 |
| V-measure | 均匀性和完整性的调和平均 | [0, 1] |
fromsklearn.metricsimportadjusted_rand_score,normalized_mutual_info_score ari=adjusted_rand_score(y_true,labels)# y_true 是真实标签nmi=normalized_mutual_info_score(y_true,labels)指标对比速查
| 指标 | 需要标签? | 衡量什么 | 越大/小越好 | 选 k 适用 |
|---|---|---|---|---|
| SSE / Inertia | 不需要 | 簇内紧凑度 | 越小越好 | 配合肘部法则 |
| 轮廓系数 | 不需要 | 紧凑 + 分离 | 越大越好 | 直接选最大 |
| CH 指数 | 不需要 | 簇间/簇内比 | 越大越好 | 直接选最大 |
| Davies-Bouldin | 不需要 | 簇间相似度 | 越小越好 | 直接选最小 |
| ARI | 需要 | 与真实标签一致度 | 越大越好 | - |
| NMI | 需要 | 与真实标签信息重叠 | 越大越好 | - |
实际建议
- 先用肘部法则粗选 k,再用轮廓系数精细确认
- 如果数据量大,轮廓系数计算慢,用 CH 指数替代
- 有真实标签时一定看 ARI/NMI,比内部指标可靠
- 指标只是参考——最终还是要看业务上分出来的簇是否有意义