ESC
输入关键词搜索文章
目录

K-Means 聚类

无监督学习 · 迭代优化 · 球形假设
用均值与距离,把混沌的数据划分为有序的簇
2交替步骤
O(nk)每轮迭代
K簇数超参
初始敏感
动机
无标注数据的困境与聚类的直觉

监督学习有标签——每一份数据都告诉你"答案是什么"。但现实中,绝大多数数据是没有标注的:用户的浏览记录、传感器的时序信号、社交网络中的行为日志。面对一堆没有标签的点,我们能做什么?

聚类(Clustering)的回答是:物以类聚。如果某些数据点在特征空间中彼此靠近,它们很可能属于同一类。我们不需要标签,只需要一个"距离"的概念。

K-Means 是最经典、最直觉的聚类算法。它的核心思想只有一句话:把数据分成 K 组,让每组内的点尽可能靠近它们的中心

核心抽象:给定 $n$ 个数据点 $\{x_1, x_2, \ldots, x_n\}$,找到 $K$ 个中心 $\{\mu_1, \mu_2, \ldots, \mu_K\}$ 和一种分配方案,使得每个点到其所属簇中心的距离之和最小。

算法
K-Means 的两步迭代

K-Means 的优雅之处在于,它把一个 NP-hard 的联合优化问题,拆成了两个极其简单的交替步骤:

步骤名称操作
Step 1分配(Assignment)将每个点分配到距离最近的中心所在的簇
Step 2更新(Update)将每个簇的中心更新为该簇所有点的均值

用流程图来直观展示整个迭代过程:

graph TD
  A["随机初始化 K 个中心"] --> B["分配:每个点归入最近中心"]
  B --> C["更新:每个中心取簇内均值"]
  C --> D{"中心是否变化?"}
  D -->|"是"| B
  D -->|"否"| E["输出最终聚类结果"]

  style A fill:#3498db,color:#fff
  style B fill:#e74c3c,color:#fff
  style C fill:#27ae60,color:#fff
  style E fill:#f39c12,color:#fff

形式化地,设第 $t$ 轮迭代时,簇中心为 $\mu_1^{(t)}, \ldots, \mu_K^{(t)}$

分配步骤:对每个数据点 $x_i$,找到最近的中心:

$$c_i^{(t)} = \arg\min_{j \in \{1,\ldots,K\}} \|x_i - \mu_j^{(t)}\|^2$$

更新步骤:重新计算每个簇的均值:

$$\mu_j^{(t+1)} = \frac{1}{|C_j^{(t)}|} \sum_{i \in C_j^{(t)}} x_i$$

其中 $C_j^{(t)} = \{i : c_i^{(t)} = j\}$ 是第 $j$ 个簇的数据点集合。

import numpy as np

def kmeans(X, K, max_iters=100):
    n, d = X.shape

    # 随机初始化:从数据中随机选 K 个点作为初始中心
    idx = np.random.choice(n, K, replace=False)
    centroids = X[idx].copy()

    for _ in range(max_iters):
        # Step 1: 分配 — 每个点归入最近的中心
        # distances[i][j] = ||x_i - mu_j||^2
        distances = np.sum((X[:, None, :] - centroids[None, :, :]) ** 2, axis=2)
        labels = np.argmin(distances, axis=1)

        # Step 2: 更新 — 每个中心取簇内均值
        new_centroids = np.zeros_like(centroids)
        for j in range(K):
            members = X[labels == j]
            if len(members) > 0:
                new_centroids[j] = members.mean(axis=0)
            else:
                new_centroids[j] = centroids[j]  # 空簇保留原中心

        # 收敛判断
        if np.allclose(centroids, new_centroids):
            break
        centroids = new_centroids

    return labels, centroids

空簇处理

当某个簇在分配步骤后没有收到任何数据点时,就产生了空簇。常见处理策略:(1) 保留原中心;(2) 从最大簇中分裂一个点过来;(3) 随机重新选择一个中心。实际应用中空簇并不常见,但代码中必须处理。

收敛性
为什么 K-Means 一定会停?

K-Means 的收敛性可以从一个优雅的不等式链得出。定义失真函数(Distortion Function)

$$J(C, \mu) = \sum_{i=1}^{n} \|x_i - \mu_{c_i}\|^2$$

也就是所有数据点到其所属簇中心的距离平方和。K-Means 的目标就是最小化 $J$

每一轮迭代中,两个步骤分别做了什么?

步骤固定什么优化什么$J$ 的影响
分配中心 $\mu$分配 $c_i$$J$ 不增(每个点选最近中心)
更新分配 $c_i$中心 $\mu_j$$J$ 不增(均值最小化平方和)

更新步骤为什么能降低 $J$这是一个经典的结论:对于固定的簇 $C_j$,使得 $\sum_{i \in C_j} \|x_i - \mu\|^2$ 最小的 $\mu$ 恰好就是算术平均值。可以通过对 $\mu$ 求导令其为零直接验证。

由于每步都不增,且 $J$ 有下界($\geq 0$),所以 $J$ 必然收敛。又因为可能的分配方案只有有限种($K^n$ 种),所以经过有限步后分配不再变化,算法终止。

关键区分:K-Means 保证收敛到局部最优,而非全局最优。失真函数 $J$ 是非凸的,不同的初始中心可能导致不同的结果。这是 K-Means 最大的局限之一。
初始化
K-Means++:聪明的起跑线

既然 K-Means 对初始中心敏感,一个自然的想法是:能不能让初始中心"均匀铺开",而不是随机扎堆?

K-Means++(Arthur & Vassilvitskii, 2007)用一种贪心策略实现了这一点:

步骤操作
1从数据中随机选择第一个中心 $\mu_1$
2对每个数据点 $x$,计算它到已选中心的最短距离 $D(x) = \min_j \|x - \mu_j\|^2$
3以概率 $p(x) \propto D(x)^2$ 选择下一个中心(距离越远,概率越大)
4重复步骤 2-3 直到选出 $K$ 个中心

直觉上,这就像撒种子——第一颗随机撒,后续的种子倾向于落在离已有种子最远的地方,确保初始中心"覆盖"整个数据分布。

def kmeans_plus_plus_init(X, K):
    n, d = X.shape
    centroids = np.zeros((K, d))

    # 第一个中心随机选
    centroids[0] = X[np.random.randint(n)]

    for k in range(1, K):
        # 计算每个点到已有中心的最短距离平方
        dists = np.min(
            np.sum((X[:, None, :] - centroids[:k][None, :, :]) ** 2, axis=2),
            axis=1
        )
        # 按距离平方的概率分布采样
        probs = dists / dists.sum()
        centroids[k] = X[np.random.choice(n, p=probs)]

    return centroids

理论保证

K-Means++ 有一个漂亮的理论结果:它找到的初始中心对应的失真值 $J$,期望不超过最优解的 $O(\log K)$ 倍。具体来说,$\mathbb{E}[J] \leq 8(\ln K + 2) \cdot J^*$,其中 $J^*$ 是全局最优失真值。虽然这不是常数因子近似,但在实践中 K-Means++ 的表现远优于随机初始化。

评估
怎么知道 K 选对了没有?

K-Means 有一个无法回避的超参数:簇数 $K$。如何选择合适的 $K$

肘部法则(Elbow Method)

最直觉的方法:对不同的 $K$ 值分别运行 K-Means,画出失真值 $J$$K$ 变化的曲线。

随着 $K$ 增大,$J$ 必然单调递减(极端情况 $K = n$$J = 0$)。但在某个点之后,$J$ 的下降速度会明显变缓,形成一个"肘部"。这个拐点对应的 $K$ 就是较好的选择。

肘部法则的局限:实践中曲线往往很平滑,"肘部"不明显。这时候需要结合领域知识或其他指标来辅助判断。

轮廓系数(Silhouette Coefficient)

轮廓系数为每个数据点 $x_i$ 定义了一个质量分数,衡量它的"归属感"有多强:

$$s(i) = \frac{b(i) - a(i)}{\max(a(i), \, b(i))}$$

其中:

· $a(i)$$x_i$ 到同簇其他点的平均距离(内聚度,越小越好)

· $b(i)$$x_i$ 到最近其他簇的平均距离(分离度,越大越好)

轮廓系数的取值范围 $[-1, 1]$

范围含义
接近 1点与同簇非常紧密,与其他簇很远——完美分配
接近 0点处于两个簇的边界——模棱两可
接近 -1点可能被分配到了错误的簇

对不同的 $K$ 计算所有点的平均轮廓系数,选择使均值最大的 $K$

from sklearn.metrics import silhouette_score

# 对不同的 K 计算轮廓系数
silhouette_scores = []
K_range = range(2, 11)  # K 至少为 2 才有意义

for K in K_range:
    labels, _ = kmeans(X, K)
    score = silhouette_score(X, labels)
    silhouette_scores.append(score)

# 选择轮廓系数最高的 K
best_K = K_range[np.argmax(silhouette_scores)]

其他评估指标

指标类型适用场景
Calinski-Harabasz Index内部指标簇间方差 / 簇内方差,越大越好
Davies-Bouldin Index内部指标簇内散度 / 簇间距离,越小越好
Gap Statistic内部指标对比均匀分布的期望 $J$,找差距最大的 $K$
Adjusted Rand Index外部指标有真实标签时才能用
局限
K-Means 的假设与失效场景

K-Means 的简洁背后隐藏了几个强假设,了解它们才能避免误用:

假设含义失效场景
球形簇簇在欧氏空间中呈凸形环形、月牙形、不规则形状
等大小各簇大小大致相同一个巨大簇 + 几个微小簇
等密度各簇内部密度相近高密簇和稀疏簇并存
固定 K簇数需要预先指定完全未知数据结构
欧氏距离$L_2$ 距离衡量相似度高维数据(维度灾难)

高维灾难

在高维空间中,所有点对之间的距离趋于相同(距离的相对差异消失),K-Means 基于距离的分配失去意义。常见的应对方案是先降维(PCA、t-SNE、UMAP),再在低维空间中聚类。

一个经典的失效案例:两个同心圆环。K-Means 无论如何也无法正确分离它们,因为它只能切出"饼状"的区域。

变体
从经典到实用的进化

针对 K-Means 的各种局限,研究者提出了多种变体:

变体改进点核心思想
K-Means++初始化概率采样,距离越远越容易被选中
Mini-Batch K-Means大规模数据每轮只用一小批数据更新中心,牺牲精度换速度
K-Medoids (PAM)鲁棒性中心必须是真实数据点(medoid),对异常值更鲁棒
Kernel K-Means非线性在核空间中聚类,可以分离非凸簇
Fuzzy C-Means软分配每个点以不同"隶属度"属于多个簇

Mini-Batch K-Means

当数据量达到百万甚至亿级时,每轮遍历所有数据太慢。Mini-Batch K-Means 每次随机抽取一小批数据(如 100~1000 个点)来更新中心:

def mini_batch_kmeans(X, K, batch_size=100, max_iters=100):
    n, d = X.shape
    centroids = kmeans_plus_plus_init(X, K)
    counts = np.zeros(K)  # 每个簇累计分配的点数

    for _ in range(max_iters):
        # 随机采样一个小批
        batch_idx = np.random.choice(n, batch_size, replace=False)
        batch = X[batch_idx]

        # 分配:小批内每个点归入最近中心
        dists = np.sum((batch[:, None, :] - centroids[None, :, :]) ** 2, axis=2)
        labels = np.argmin(dists, axis=1)

        # 增量更新中心
        for i, label in enumerate(labels):
            counts[label] += 1
            eta = 1.0 / counts[label]  # 学习率递减
            centroids[label] = (1 - eta) * centroids[label] + eta * batch[i]

    return centroids

Mini-Batch 的关键技巧是增量更新:每来一个点,中心按学习率 $\eta = 1/n_j$$n_j$ 是该簇累计处理的点数)做一步梯度步。这使得算法只需 $O(b)$ 而非 $O(n)$ 的每轮开销,且在实践中效果损失很小。

深层联系
K-Means 与 EM 算法:硬分配与软分配

K-Means 看起来和概率模型没什么关系,但它实际上是高斯混合模型(GMM)的一个特例,对应 EM 算法的一种极端退化。

在高斯混合模型中,假设数据由 $K$ 个高斯分布混合生成:

$$p(x) = \sum_{j=1}^{K} \pi_j \cdot \mathcal{N}(x \mid \mu_j, \Sigma_j)$$

EM 算法同样有两步:

K-MeansEM (GMM)
E 步硬分配:$c_i = \arg\min_j \|x_i - \mu_j\|^2$软分配:$\gamma_{ij} = P(z_i = j \mid x_i)$(后验概率)
M 步更新均值 $\mu_j$更新 $\mu_j, \Sigma_j, \pi_j$(所有参数)
协方差隐含 $\Sigma_j = \sigma^2 I$(各向同性,等方差)
混合权重隐含 $\pi_j = 1/K$(等权重)

当 GMM 的协方差矩阵趋于 $\sigma^2 I$$\sigma^2 \to 0$ 时,软分配退化为硬分配——每个点以概率 1 属于最近的簇。这就是 K-Means。

一图胜千言:K-Means 和 EM 的关系就像"非黑即白"和"灰度渐变"。K-Means 说"你属于簇 A",EM 说"你有 70% 的概率属于簇 A,30% 属于簇 B"。后者更灵活,但代价是更高的计算复杂度和更多的参数。
graph TD
  GMM["高斯混合模型
(GMM)"] -->|"Σ→σ²I, σ→0"| KM["K-Means
(硬分配)"] GMM -->|"一般情况"| EM["EM 算法
(软分配)"] KM --> INIT["K-Means++
(智能初始化)"] KM --> MB["Mini-Batch
(大规模)"] KM --> MED["K-Medoids
(鲁棒)"] EM --> FB["Fuzzy C-Means
(模糊聚类)"] EM --> VAR["变分推断
(变分 EM)"] style GMM fill:#3498db,color:#fff style KM fill:#e74c3c,color:#fff style EM fill:#27ae60,color:#fff
实战
复杂度分析与工程实践

时间复杂度

步骤时间说明
每轮迭代$O(nKd)$$n$ 个点 × $K$ 个中心 × $d$ 维距离
总时间$O(nKd \cdot T)$$T$ 为迭代轮数,通常 $T \ll n$
K-Means++ 初始化$O(nKd)$逐个选中心,每轮算距离
Mini-Batch 每轮$O(bKd)$$b \ll n$,每轮极快

关键观察:K-Means 的复杂度与数据量 $n$线性关系,这使得它在大规模数据上非常实用。相比之下,层次聚类的复杂度是 $O(n^2)$$O(n^3)$

sklearn 实战模板

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import numpy as np

# 1. 标准化(K-Means 对量纲敏感!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 2. 用肘部法则 + 轮廓系数选 K
for K in range(2, 11):
    km = KMeans(n_clusters=K, init='k-means++', n_init=10, random_state=42)
    labels = km.fit_predict(X_scaled)
    print(f"K={K}: inertia={km.inertia_:.2f}, "
          f"silhouette={silhouette_score(X_scaled, labels):.4f}")

# 3. 用选定的 K 训练最终模型
best_K = 4
km = KMeans(n_clusters=best_K, init='k-means++', n_init=10)
labels = km.fit_predict(X_scaled)
centroids = scaler.inverse_transform(km.cluster_centers_)

# 4. 大规模数据用 Mini-Batch
from sklearn.cluster import MiniBatchKMeans
mbkm = MiniBatchKMeans(n_clusters=best_K, batch_size=1000, n_init=3)
labels = mbkm.fit_predict(X_scaled)

工程实践要点

1. 一定要标准化:K-Means 基于欧氏距离,特征量纲不同会导致结果偏向方差大的维度。

2. n_init 设为 10+:sklearn 默认用 K-Means++ 运行 n_init 次,取最优结果。这是应对局部最优的实用策略。

3. 高维先降维:超过 50 维时,先用 PCA 将维度降至 10~20 再聚类。

4. 类别特征需编码:K-Means 只能处理数值特征,类别特征需要 one-hot 或 embedding。

总结
一图总览:K-Means 的完整知识图谱
graph LR
  CORE["K-Means
核心算法"] --> A["两步迭代
分配 + 更新"] CORE --> B["失真函数 J
收敛到局部最优"] CORE --> C["K-Means++
O(log K) 近似"] CORE --> EVAL["评估"] EVAL --> E1["肘部法则"] EVAL --> E2["轮廓系数"] EVAL --> E3["CH / DB 指标"] CORE --> VAR["变体"] VAR --> V1["Mini-Batch
O(b) 每轮"] VAR --> V2["K-Medoids
鲁棒"] VAR --> V3["Kernel K-Means
非线性"] CORE --> CONN["深层联系"] CONN --> G1["GMM / EM
软分配推广"] CONN --> G2["向量量化
信号压缩"] style CORE fill:#e74c3c,color:#fff style EVAL fill:#27ae60,color:#fff style VAR fill:#3498db,color:#fff style CONN fill:#f39c12,color:#fff
一句话总结:K-Means 是一个"简单到令人怀疑它是否有效"的算法——但在数据满足球形假设时,它仍然是最高效、最实用的聚类工具。理解它的假设和局限,比理解它的算法本身更重要。

参考来源

  • Arthur, D. & Vassilvitskii, S. (2007). K-Means++: The Advantages of Careful Seeding. SODA 2007.
  • Bishop, C. M. Pattern Recognition and Machine Learning, Chapter 9: Mixture Models and EM
  • Sculley, D. (2010). Web-Scale K-Means Clustering. WWW 2010. (Mini-Batch K-Means)
  • scikit-learn: Clustering — K-Means