简介:面向Matlab初学者与数据分析入门者的K-means聚类算法学习资料,以一份docx文档形式系统讲解无监督聚类的基本原理与代码实现,帮助读者理解如何用Matlab内置kmeans函数完成数据分簇与结果可视化。压缩包内共1个docx文件,约15KB,内容涵盖算法简介、基本步骤、示例代码、逐行解析以及局限性与改进方向等模块,结构紧凑便于通读。文档以7个二维数据点、K=2的实例演示聚类流程,并说明idx与centroids的含义及scatter绘图方式,同时讨论K值选取、异常值敏感、初始化敏感等问题,引出K-means++、Bisecting K-means与Kernel K-means等改进思路。已有444人学习,适合希望快速上手聚类分析、对照示例理解算法细节的读者参考。
1. 一行 kmeans 调用背后,藏着多少没告诉你的决策
很多人第一次在 MATLAB 里跑聚类,写下的就是[idx, centroids] = kmeans(data, K)这一行。它太顺了,顺到让人误以为聚类就是把数据丢进去、簇数填一填、图一画就完事。可只要换一组数据、换一个 K、甚至只是重跑一次,颜色分布就可能完全变样——这不是玄学,而是 K-means 这个算法从骨子里带出来的不确定性。真正值得搞懂的问题是:这一行调用内部到底替你做了哪些决定?初始质心怎么选、距离怎么算、迭代什么时候停、空簇怎么处理?这些默认值一旦和你的数据特性错位,结果就会翻车。把 MATLAB 的kmeans当成黑匣子用,和知道它内部在做什么,得到的结论质量差一个量级。下面就从这行调用出发,把 K-means 的机制、MATLAB 的实现取舍和踩坑点一层层拆开。
2. K-means 的迭代机制:为什么“算均值”能收敛
2.1 目标函数:最小化的是簇内平方和
K-means 不是随便把点分堆,它有一个明确的目标函数——簇内平方误差和(Within-Cluster Sum of Squares,WCSS):
$$ J = \sum_{k=1}^{K} \sum_{x_i \in C_k} | x_i - \mu_k |^2 $$
其中 $C_k$ 是第 $k$ 个簇,$\mu_k$ 是该簇质心。算法要做的就是找一组划分,让 $J$ 尽量小。理解这一点很关键:后面所有的“分配—更新”两步,本质上都是在交替地降低 $J$,而不是在做某种启发式的猜测。
2.2 两步交替:分配步与更新步各自在优化什么
K-means 的每次迭代分两步,每一步都保证 $J$ 不增:
- 分配步:固定质心 $\mu_k$,把每个点分给距离最近的质心。对单个点来说,选最近质心就是让它的平方误差最小,所以这一步 $J$ 单调不增。
- 更新步:固定划分 $C_k$,把质心移到簇内点的均值。对平方误差求导并令其为零,解出来正好是均值 $\mu_k = \frac{1}{|C_k|}\sum_{x_i \in C_k} x_i$。所以这一步同样让 $J$ 不增。
两步交替,$J$ 单调下降且有下界(≥0),因此必然收敛。但要注意,收敛到的是局部最优,不保证全局最优——这正是初始化敏感的根本原因。常见的实现思路是:因为 $J$ 的下降是单调的,所以“质心不再移动”和“$J$ 变化小于阈值”这两种停止条件在数学上是等价的,MATLAB 默认用的是后者配合最大迭代次数兜底。
2.3 收敛判据与迭代上限
MATLAB 的kmeans默认MaxIter为 100,停止条件是质心移动量或目标函数变化低于容差。这里有个容易被忽略的点:如果数据尺度很大(比如坐标是几万量级),默认容差可能过早触发停止,看起来“收敛了”,其实还没稳定。反过来,如果MaxIter设得太小,算法会在质心还在明显移动时被强行截断,结果每次重跑都不一样。判断是否真收敛,可以看返回的sumd(簇内距离和)在多次运行间是否稳定,而不是只看它有没有报错。
3. 初始化:为什么同一个 K 跑两次结果不同
3.1 随机初始化的方差问题
最朴素的初始化是随机挑 K 个点当质心。问题在于,如果两个初始质心恰好落在同一个真实簇里,那个簇就永远分不开,另一个簇则被强行拆成两半。这种“坏初始化”的概率不低,尤其在 K 较大时。血泪经验是:在小数据集上反复跑,你会看到sumd在明显不同的值之间跳,这就是初始化方差在作祟。
3.2 kmeans++:用距离平方加权挑下一个质心
kmeans++ 的思路很直接:第一个质心随机选,之后每个新质心按“到已选质心最近距离的平方”加权随机选。距离现有质心越远的点,被选中的概率越大,从而让初始质心尽量分散。常见的实现思路是:
% kmeans++ 初始化示意(非 MATLAB 内置,仅说明机制) function centroids = kmeanspp_init(data, K) n = size(data, 1); centroids = zeros(K, size(data, 2)); % 第一个质心随机选 centroids(1, :) = data(randi(n), :); for k = 2:K % 计算每个点到最近已选质心的距离平方 d2 = min(pdist2(data, centroids(1:k-1, :)).^2, [], 2); % 按距离平方加权采样 probs = d2 / sum(d2); idx = randsample(n, 1, true, probs); centroids(k, :) = data(idx, :); end end这段代码的关键在d2和probs:pdist2算出每个点到所有已选质心的距离,取最小值再平方,得到权重;randsample按这个权重抽一个点。距离平方而不是距离本身,是为了让远点的优势更明显。MATLAB 的kmeans默认Start是'plus',也就是 kmeans++,但很多人不知道可以显式指定'Replicates'来多次重启取最优。
3.3 Replicates:用多次重启换稳定性
'Replicates', N会让算法用不同的初始质心跑 N 次,返回sumd最小的那次结果。代价是时间线性增长,收益是大幅降低坏初始化的概率。对于 K 较大或数据分布复杂的情况,把Replicates设到 5~10 是常见做法。这里有个取舍:Replicates治标不治本,它降低的是“运气差”的概率,不改变 K-means 只能找局部最优的本质。如果数据本身有重叠簇,再多重启也分不干净。
4. 距离度量与数据预处理:欧氏距离的隐含假设
4.1 欧氏距离默认各维等权
kmeans默认用平方欧氏距离,这等价于假设每个特征维度对聚类的贡献相同、尺度一致。如果一维是 0~1 的归一化值,另一维是 0~10000 的金额,后者会完全主导距离计算,前者几乎不起作用。这不是算法 bug,是度量选择和数据没对齐。常见的做法是先做 z-score 标准化:
% 标准化后再聚类,避免量纲主导距离 data_norm = zscore(data); % 每列减均值除标准差 [idx, centroids] = kmeans(data_norm, K, 'Replicates', 5); % 注意:centroids 是标准化空间的,要还原到原空间需乘回标准差加均值zscore按列处理,centroids落在标准化空间里,画图或解释时要记得反变换。如果某些维度是类别型或稀疏的,欧氏距离本身就不合适,这时应该考虑其他度量或先做特征工程,而不是硬套kmeans。
4.2 异常值为什么能拖垮质心
更新步用的是均值,均值对异常值极其敏感。一个远离所有簇的离群点,如果被分到某个簇,会把该簇质心往它那边拽,进而影响下一轮分配,形成连锁偏移。这就是摘要里提到的“对异常值敏感”的机制根源。排查方法是先看数据的箱线图或马氏距离,把明显离群的点剔除或单独处理,再跑聚类。如果业务上不能删,可以考虑用 K-medoids 这类基于中位数的变体,代价是计算更慢。
5. 避坑与排查:五个真实会翻车的点
5.1 现象:每次运行颜色分布都不一样
原因:默认Replicates为 1,初始质心随机,落到了不同的局部最优。解决:显式设'Replicates', 10,并固定随机种子rng(42)便于复现;同时检查sumd是否在多次运行间稳定。
5.2 现象:某个簇是空的,或者只有一个点
原因:初始质心太近,或数据本身在该区域没有点,分配步后某簇无成员。MATLAB 内部对空簇有处理(通常会重新分配一个远离现有质心的点),但结果可能不理想。解决:增大Replicates,或先用层次聚类看一眼大致簇结构再定 K;空簇频繁出现往往说明 K 选大了。
5.3 现象:K 调大后 sumd 一直降,但聚类没意义
原因:WCSS 随 K 单调下降,K 等于样本数时降为 0,所以不能只看 sumd 选 K。解决:用肘部法看 sumd 下降的拐点,或算轮廓系数(silhouette)评估簇的紧密度和分离度,两者结合判断。
5.4 现象:标准化后结果和没标准化差别巨大
原因:量纲主导了距离,标准化改变了各维权重。解决:这不是错误,是要明确“哪些维度应该等权”。如果业务上某维更重要,可以手动加权而不是简单 z-score;反变换质心时别忘了乘回标准差。
5.5 现象:大数据集上 kmeans 跑得极慢或内存爆掉
原因:pdist2式的全量距离计算是 O(nK) 每轮,n 很大时内存和时间都吃不消。解决:MATLAB 对大数据有基于抽样的加速选项,也可以先对数据做子采样估质心,再全量分配;或者改用 Mini-Batch K-means 思路分批更新质心。
6. 从内置函数到可解释实现:设计取舍在哪里
MATLAB 的kmeans把初始化、迭代、空簇处理、多重启都封装进一个调用,换来的是易用性,牺牲的是对中间过程的可见性。理解它的取舍,才能判断什么时候该信默认值、什么时候该自己接管。
| 设计点 | 内置默认 | 取舍 |
|---|---|---|
| 初始化 | kmeans++ | 比纯随机稳,但仍非全局最优 |
| 重启次数 | 1 | 快,但方差大;复杂数据需手动调高 |
| 距离 | 平方欧氏 | 快且可微,但对量纲和异常值敏感 |
| 停止条件 | 容差 + MaxIter | 大数据尺度下容差可能过早触发 |
| 空簇处理 | 内部重分配 | 避免崩溃,但可能引入偏差 |
如果想验证内置结果是否可信,一个实用习惯是:自己写一遍最朴素的“分配—更新”循环,和内置结果对比sumd。自己写的版本通常更慢、更容易卡在坏初始化,但能让你看清每一步质心怎么动、哪些点换了簇。常见的实现思路是用pdist2算距离矩阵、min取最近簇、accumarray算均值,几十行就能跑通。对比之后你会发现,内置函数真正值钱的地方不是算法本身,而是 kmeans++ 加多重启这套组合拳把“运气”的影响压到了可接受范围。
另一个进阶方向是当 K 本身不确定时,不要硬猜。肘部法和轮廓系数是两条独立证据,前者看 WCSS 拐点,后者看簇内紧密度与簇间分离度的比值,两者结论一致时 K 才比较可信。如果数据非线性可分,K-means 的球形簇假设会失效,这时该换核方法或密度聚类,而不是继续调 K。
我自己的习惯是:任何一次聚类结果要拿去做决策之前,先用至少三个随机种子各跑一遍,看idx的簇大小分布是否稳定;不稳定就加Replicates,还不行就回头查数据预处理。这个流程救过我好几次,避免把初始化噪声当成业务信号。希望帮到你。
本文还有配套的精品资源,点击获取