← 全部技术笔记

机器学习 · 聚类

从质心移动开始理解 k-means

把每个点分给最近的质心,再把质心移动到组内平均位置。通过这两个动作,观察散点如何形成聚类。

一手资料核验日期:2026年8月31日

01

跟着质心移动到稳定

Step执行一次“分配→更新平均值”。背景颜色表示当前离哪个质心最近。

迭代
0
inertia
1,085,095
点数
30

点击画布可以增加点。切换到细长数据后,会更容易看到k-means不擅长的形状。

02

目标是缩小点到所属质心的平方距离之和

k-means先把每个点分给最近的质心,再用组内点的平均位置更新质心。这里显示的inertia,就是所有点到所属质心平方距离的总和。

反复执行后inertia会下降,质心最终停止移动。但结果只是局部最优,不同初始质心可能得到不同分组。

03

适合紧凑团块,不擅长细长或密度不均的数据

最近质心形成的边界更偏向凸形、大小接近的聚类。细长形、圆环、密度差异或离群点都可能让结果偏离肉眼看到的结构。

实际使用时应重复不同初始化、检查特征缩放,并与silhouette、DBSCAN或Gaussian mixture等方法比较。k的选择本身就是问题的一部分。

04

只写出算法核心循环

这段教学代码把二维点的分配和均值更新直接写成Python。

Python 3kmeans_from_scratch.py
def kmeans(points, k, iterations=20):
    """A compact teaching implementation for 2D points."""
    centroids = [list(point) for point in points[:k]]

    for _ in range(iterations):
        groups = [[] for _ in range(k)]

        for point in points:
            label = min(
                range(k),
                key=lambda i: squared_distance(point, centroids[i]),
            )
            groups[label].append(point)

        updated = []
        for i, group in enumerate(groups):
            if not group:
                updated.append(centroids[i])
                continue
            updated.append([
                sum(p[0] for p in group) / len(group),
                sum(p[1] for p in group) / len(group),
            ])

        if all(squared_distance(a, b) < 1e-10
               for a, b in zip(centroids, updated)):
            break
        centroids = updated

    return centroids, groups

def squared_distance(a, b):
    return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2

05

为什么不能直接用于生产

06

核验的一手资料

  1. scikit-learn User Guide — K-means

    核对目标函数、迭代步骤、初始化依赖与形状限制。

  2. scikit-learn API — sklearn.cluster.KMeans

    核对当前生产API的主要参数和输出。

  3. Lloyd (1982), Least Squares Quantization in PCM

    核对迭代量化步骤的原始论文。