K-means 聚类算法详解:从原理到实战

K-means 聚类算法详解:从原理到实战

无监督学习中最经典、最常用的聚类算法之一,凭借思想简洁、实现高效,长期活跃在客户分群、图像压缩、异常检测等众多场景中。本文从直观理解出发,依次讲解算法原理、sklearn API 用法、完整实现流程、效果评估方法,以及如何借助 K-means 完成特征降维。


什么是 K-means 聚类?

含义概念

K-means 是一种无监督学习算法,它会在没有标签的数据集中,自动把相似的样本归到同一个簇,使得"同簇内样本尽可能相似,不同簇之间尽可能不同"。

这里有两个关键角色:

  • K:你想把数据分成多少个簇,需要人为指定。
  • means:每个簇用一个"均值点"(簇中心 / 质心,centroid)来代表,质心就是该簇内所有样本坐标的平均值。

其实K-means聚类算法和KNN算法思想很接近,详情可以跳转-- KNN算法详解:从原理到实践入门

举例说明:

假设你是一家连锁超市的店长,手里有 300 位会员的两项数据:年消费金额到店频次。你希望把顾客分成几类,以便做精准营销。

一开始这些点散落在坐标图上,看不出明显规律。你猜测大概可以分成 3 类,于是启动 K-means:

  1. 算法随机放上 3 个"质心点"(可以理解为 3 面旗帜)。
  2. 每位顾客看看自己离哪面旗帜最近,就站到那面旗下------这一步叫分配
  3. 每面旗下重新计算所有顾客的平均位置,把旗帜移到这个新位置------这一步叫更新
  4. 旗帜移动后,可能有些顾客发现另一面旗更近,于是换队。重复"分配→更新",直到旗帜不再移动,算法收敛。

最终结果如下图所示,顾客被清晰地分成了三个群体:

三个簇的现实含义非常直观:

  • 红色簇:消费低、到店少------可能是偶发顾客或流失边缘,适合用优惠券唤醒。
  • 蓝色簇:消费和频次中等------主力家庭客户,重点维护。
  • 绿色簇:消费高、频次高------VIP 高价值客户,提供专属服务。

这就是 K-means 的威力:你不需要事先告诉算法"谁是 VIP",它仅凭数据本身的分布结构,就能帮你发现群体。


算法实现流程

K-means 的标准流程可以归纳为以下四步,循环执行直至收敛:

步骤 1:选择 K 值并初始化

由业务需求或数据探索决定簇的数量 K。这是 K-means 唯一的核心超参数,后文会介绍如何用肘部法则、轮廓系数辅助选 K。然后从数据中随机挑选 K 个点作为初始质心。

步骤 2:分配样本

对每个样本,计算它到 K 个质心的欧氏距离,把它分配给最近的那个质心所在的簇。

步骤 3:更新质心

对每个簇,重新计算簇内所有样本的均值,作为新的质心。

步骤 4:判断收敛

如果质心几乎不再移动(变化量小于阈值 tol),或达到最大迭代次数 max_iter,算法停止;否则回到步骤 2 继续迭代。

整个过程的目标函数 是最小化所有样本到其所属质心的平方距离之和(称为 SSE,Sum of Squared Errors,也叫 inertia):

J = ∑ j = 1 K ∑ x ∈ C j ∥ x − μ j ∥ 2 J = \sum_{j=1}^{K} \sum_{x \in C_j} \left\| x - \mu_j \right\|^2 J=j=1∑Kx∈Cj∑∥x−μj∥2

需要说明的是,K-means 只能保证收敛到一个局部最优 ,而非全局最优。这也是为什么 sklearn 会用 n_init 参数多次运行取最优结果的原因。


API 详解

scikit-learn 提供了 sklearn.cluster.KMeans 类,是目前 Python 生态中最常用的实现。大数据集用 MiniBatchKMeans。 当样本量超过 1 万,标准 K-means 会明显变慢。sklearn.cluster.MiniBatchKMeans 用小批量样本更新质心,速度提升数倍,结果接近。

核心参数

python 复制代码
class sklearn.cluster.KMeans(
    n_clusters=8,
    init='k-means++',
    n_init='auto',
    max_iter=300,
    tol=1e-4,
    verbose=0,
    random_state=None,
    copy_x=True,
    algorithm='lloyd'
)
参数 默认值 说明
n_clusters 8 聚类簇数 K
init 'k-means++' 质心初始化方式,可选 'k-means++''random'、数组或可调用对象
n_init 'auto' 用不同随机种子运行的次数,取 inertia 最小的结果。'auto' 时若 init='k-means++' 则跑 1 次,若 init='random' 则跑 10 次
max_iter 300 单次运行的最大迭代次数
tol 1e-4 质心变化的收敛阈值,Frobenius 范数小于该值即停止
random_state None 随机种子,设为整数可复现结果
algorithm 'lloyd' 算法变体,'lloyd' 为经典 EM 风格,'elkan' 利用三角不等式在某些数据上更快但更耗内存

常用方法

  • fit(X):对数据 X 进行聚类。
  • fit_predict(X):聚类并直接返回每个样本的簇标签,等价于 fit(X).labels_
  • predict(X):用已拟合的模型预测新样本属于哪个簇。
  • transform(X):将 X 转换到"到各质心距离"的空间,形状 (n_samples, n_clusters),可用于特征降维。
  • fit_transform(X):等价于 fit(X).transform(X),但更高效。

案例实战:鸢尾花数据聚类

下面用经典的鸢尾花数据集演示完整流程。虽然鸢尾花本身有标签,但我们在聚类时假装不知道标签,纯粹用四个特征(花萼长宽、花瓣长宽)做无监督聚类,最后再和真实标签对比。

完整代码:

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 1. 加载数据
iris = load_iris()
X = iris.data          # 四个特征
y_true = iris.target   # 真实标签(仅用于最后对比,聚类时不使用)

# 2. 标准化(K-means 基于距离,对量纲敏感,务必标准化)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 3. 用肘部法则选 K
sse = []
sil = []
K_range = range(2, 11)
for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_scaled)
    sse.append(km.inertia_)
    sil.append(silhouette_score(X_scaled, km.labels_))

fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].plot(K_range, sse, 'o-')
ax[0].set_xlabel('K')
ax[0].set_ylabel('SSE (inertia)')
ax[0].set_title('肘部法则')

ax[1].plot(K_range, sil, 'o-')
ax[1].set_xlabel('K')
ax[1].set_ylabel('轮廓系数')
ax[1].set_title('轮廓系数法')
plt.tight_layout()
plt.savefig('kmeans_eval.png', dpi=120)
plt.show()

# 4. 选定 K=3 进行聚类
km = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = km.fit_predict(X_scaled)
centers = km.cluster_centers_

# 5. 可视化(取前两个特征画图)
plt.figure(figsize=(8, 6))
scatter = plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap='viridis', s=50, alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='*', s=300, label='质心')
plt.xlabel('花萼长度(标准化)')
plt.ylabel('花萼宽度(标准化)')
plt.title('K-means 鸢尾花聚类结果 (K=3)')
plt.legend()
plt.colorbar(scatter, label='簇编号')
plt.savefig('kmeans_result.png', dpi=120)
plt.show()

# 6. 输出评估指标
print(f'最终 SSE (inertia): {km.inertia_:.4f}')
print(f'轮廓系数: {silhouette_score(X_scaled, labels):.4f}')
print(f'迭代次数: {km.n_iter_}')

运行后你会观察到:

  • 肘部法则图在 K=3 处出现明显拐点,SSE 下降速度骤减。
  • 轮廓系数在 K=2 或 K=3 时达到峰值。
  • 聚类结果中,三类鸢尾花被较好地分开,其中一类(Setosa)几乎完全正确,另外两类(Versicolor 与 Virginica)有少量样本重叠------这与它们在特征空间中的真实分布一致。

这个案例体现了 K-means 的典型工作流:标准化 → 选 K → 聚类 → 评估 → 可视化


效果评估方法

聚类没有真实标签可用,评估比监督学习更依赖内部指标。以下是三种常用方法。

肘部法则(Elbow Method)

绘制不同 K 值对应的 SSE 曲线,寻找"肘部"拐点。SSE 随 K 增大单调下降,当 K 超过真实簇数后,下降幅度会明显变缓,这个转折点就是较优的 K。

肘部法则简单直观,但拐点有时不明显,需要结合其他方法交叉验证。

轮廓系数(Silhouette Score)

轮廓系数同时考虑簇内紧密度和簇间分离度,取值范围 − 1 , 1 -1, 1 −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 ) a(i) a(i):样本 i i i 到同簇其他样本的平均距离(越小越好)。
  • b ( i ) b(i) b(i):样本 i i i 到最近的其他簇中样本的平均距离(越大越好)。

解读标准:

  • 接近 1:聚类效果好,样本分到了正确的簇。
  • 接近 0:样本处于两个簇的边界。
  • 接近 -1:样本可能被分错了簇。

sklearn 中调用方式:

python 复制代码
from sklearn.metrics import silhouette_score
score = silhouette_score(X, labels)

CH 指数(Calinski-Harabasz Index)

又称方差比准则,计算簇间离散度与簇内离散度之比,值越大说明簇间越分散、簇内越紧凑,聚类效果越好。

python 复制代码
from sklearn.metrics import calinski_harabasz_score
score = calinski_harabasz_score(X, labels)

CH 指数计算速度快,适合在大数据集上做快速评估。

方法 优点 缺点 适用场景
肘部法则 直观易懂 拐点可能不明显,主观性强 初步探索 K
轮廓系数 同时考虑紧密度和分离度,有明确取值范围 计算复杂度高 O ( n 2 ) O(n^2) O(n2) 中小数据集精细评估
CH 指数 计算快,值越大越好 无固定取值范围,仅适合横向比较 大数据集快速评估

特征降维

K-means 除了聚类,还有一个常被忽略的用途------特征降维 ,在信号处理领域称为向量量化(Vector Quantization)

基本思想

假设你有 n n n 个样本,每个样本 d d d 维。K-means 聚类后得到 K 个质心,我们可以把每个样本替换成"它到这 K 个质心的距离",于是样本维度从 d d d 变成了 K K K。

当 K ≪ d K \ll d K≪d 时,就实现了降维。这正是 sklearn 中 transform 方法的用途:

python 复制代码
km = KMeans(n_clusters=50, random_state=42)
X_dist = km.fit_transform(X)   # 形状从 (n, d) 变为 (n, 50)

X_dist[i][j] 表示样本 i i i 到第 j j j 个质心的距离。这种距离特征往往比原始特征更具判别力,常作为下游分类器的输入。

经典应用

图像压缩是 K-means 降维最直观的案例。一张彩色图片每个像素由 RGB 三个值(0-255)表示,共有 1600 万种可能颜色。如果用 K-means 把所有像素颜色聚成 16 类,每个像素只需记录它属于 16 个质心中的哪一个(4 bit),颜色种类从 1600 万压缩到 16,文件体积大幅缩小,而肉眼几乎看不出差别。

python 复制代码
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
from matplotlib.image import imread

# 1. 读取图片,把像素拉平成 (n_pixels, 3)
img = imread('photo.jpg')
h, w, c = img.shape
pixels = img.reshape(-1, 3)

# 2. 用 K-means 把颜色聚成 16 类
kmeans = KMeans(n_clusters=16, random_state=42, n_init=10)
labels = kmeans.fit_predict(pixels)
compressed_palette = kmeans.cluster_centers_

# 3. 用质心颜色重建图片
compressed_img = compressed_palette[labels].reshape(h, w, c)

# 4. 对比展示
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
ax[0].imshow(img)
ax[0].set_title('原始图片')
ax[0].axis('off')
ax[1].imshow(compressed_img / 255)
ax[1].set_title('压缩后(16 色)')
ax[1].axis('off')
plt.tight_layout()
plt.savefig('image_compression.png', dpi=120)
plt.show()
维度 K-means 降维 PCA 降维
原理 用到质心的距离替换原特征 用主成分投影保留最大方差方向
类型 非线性(基于聚类) 线性
输出 到 K 个质心的距离(K 维) 投影到前 K 个主成分(K 维)
适用场景 簇结构明显、想做特征工程 一般降维、去噪、可视化

实际工程中,两者经常组合使用:先用 PCA 把高维数据降到几十维,再用 K-means 聚类,既能提升速度,又能改善效果。