
简介这份K-means聚类算法MATLAB实现文档面向机器学习初学者、数据分析人员以及需要快速上手聚类分析的MATLAB用户可作为课内实验参考或工程实践速查手册。文档从无监督学习基本概念切入系统讲解欧氏距离等度量方式并以可复现的步骤拆解初始化聚类中心、样本分配、质心更新及迭代收敛四个关键流程同时说明kmeans函数的调用格式与参数含义。示例代码包含7个二维数据点的完整聚类演示利用scatter绘制着色散点图与质心交叉标记读者可直观验证簇划分效果并给出idx与centroids等返回值的具体含义文档还深入讨论了K值选取、异常值干扰、初始化敏感等常见问题引入K-means、Bisecting K-means、Kernel K-means等改进方案帮助理解不同场景下的算法优化策略。资源为单个DOCX文件体积仅15KB目前已有442人学习下载内容精炼且可直接复用。1. K-means聚类为什么同一份数据、同一个K两次运行结果会不一样在MATLAB里做K-means聚类表面上看就是一行代码[idx, centroids] kmeans(data, K);。但很多人第一次跑完就懵了——同一份数据、同一个K连续跑两次聚类结果可能完全不同连质心位置都变了。这不是玄学而是K-means算法的本质它求解的是一个非凸优化问题最终落在哪个局部最优取决于初始质心的选择。理解这一点才能真正看懂kmeans函数的返回值、参数和那些时不时冒出来的警告信息。这篇把K-means从一行代码拆到迭代机制、参数设计再落到MATLAB的源码级行为。搞懂之后你会明白什么时候该信结果什么时候该换初始化方式。2. 迭代收敛机制分配与更新的循环为什么一定会停下来2.1 组内平方和K-means到底在优化什么K-means不是把点随便分几堆的直觉算法它背后有一个明确的优化目标——组内平方和Within-Cluster Sum of SquaresWCSS。给定数据集每个样本x_i被分到某个簇c_j该簇的质心是m_j那么整个聚类结果的代价函数是% WCSS 的数学定义MATLAB中由 kmeans 内部迭代最小化 WCSS sum_{j1}^{K} sum_{x_i in C_j} || x_i - m_j ||^2对每个簇内部的所有点算它们到质心的欧氏距离平方再全部加起来。聚类效果越好这个值越小。注意K-means用的是距离平方而不是距离本身所以它对离群点格外敏感——一个偏离很远的点平方之后对代价函数的贡献会被放大。理解WCSS是理解后面所有行为的前提。kmeans函数返回的idx和centroids本质上就是一组使WCSS尽可能小的簇分配和质量中心。问题是这个优化问题是非凸的它既有离散变量每个点属于哪个簇又有连续变量质心坐标两类变量耦合在一起没法一步求出全局最优解。于是MATLAB采用的策略是交替优化这就是Lloyd算法。2.2 Lloyd两步循环为什么单调收敛Lloyd算法的核心是两步交替执行。第一步叫分配Assignment固定质心把每个数据点分给离它最近的质心第二步叫更新Update固定分配结果重新计算每个簇的均值作为新质心。重复这两步直到质心不再移动。下面是手写版本的核心循环它和MATLAB内置kmeans的迭代逻辑基本一致% 手写 K-means 核心循环演示用非内置实现 function [idx, centroids] my_kmeans(data, K, maxIter) % 初始化随机挑 K 个数据点作为初始质心 centroids data(randperm(size(data,1), K), :); for iter 1:maxIter % 分配步骤算每个点到所有质心的距离取最近的 dist pdist2(data, centroids, euclidean); % n x K 距离矩阵 [~, idx] min(dist, [], 2); % 每个点最近的质心索引 % 更新步骤每个簇取均值作为新质心 for j 1:K centroids(j, :) mean(data(idx j, :), 1); end end end关键机制在于每轮迭代都保证WCSS不增。分配步骤中每个点都换到了距离更近的质心这一项的平方距离只会下降更新步骤中均值是簇内所有点平方距离和的最小解所以把质心换成均值WCSS只会更小。两步联手代价函数单调递减又因为WCSS有下界至少是0迭代必然收敛。这就是为什么kmeans函数不会无限循环下去。不过要留意一个细节算法收敛到的是WCSS的局部极小值不是全局最优。初始质心选得不好算法可能落进一个很差的局部极小结果看起来也合理但全局来看很糟糕。这就是同一份数据跑两次结果不同的根源。MATLAB内置函数的默认设置只跑一次初始化所以翻车概率并不低。2.3 收敛判据MaxIter与TolFun到底控制什么MATLAB的kmeans函数内部并不只运行到质心完全不动才停它还有两个收敛条件。第一个是最大迭代次数MaxIter默认100第二个是相对误差容限TolFun默认1e-4。观察两个相邻迭代轮的WCSS变化如果相对变化量小于TolFun就认为收敛了。% 指定收敛参数示例 [idx, centroids, sumd] kmeans(data, K, MaxIter, 200, TolFun, 1e-6);第三个返回值sumd是每个簇内的点到质心距离平方和把它加起来就是总WCSS。实际调试时sumd比idx更能反映聚类质量——不同初始化跑出来的结果对比总WCSS就能选出更优的那个。如果算法报出警告达到最大迭代次数但未收敛通常意味着MaxIter太小或者数据本身的簇结构很弱增加MaxIter能解决一部分问题。但真正的问题往往出在初始化上这会在后面参数那一章展开。3. 参数矩阵距离度量、初始化与空簇处理的设计逻辑3.1 距离度量sqeuclidean之外的选项怎么选kmeans函数的名字里没有欧氏字样但它默认用的就是平方欧氏距离sqeuclidean。这跟WCSS是配套的分配步骤中最小化平方距离等价于最小化欧氏距离更新步骤中均值恰好是平方距离和的最优解。这套设计让Lloyd算法的两步都名正言顺。但MATLAB还提供了其他距离选项比如cityblock曼哈顿距离和cosine余弦距离。换距离不是随便换的它改变了整个优化问题的几何结构。用cityblock时簇中心不再是最普通的均值而是各维度的中位数——这让算法对离群点的鲁棒性变好但MATLAB内部迭代时收敛性质会弱一些。用cosine时质心会被重新归一化到单位长度适合文本向量或方向敏感的数据。% 换距离度量示例 [idx_cos, cen_cos] kmeans(data, K, Distance, cosine);我一般建议默认sqeuclidean处理大多数数值特征特征方向有意义、量级无意义时用cosine数据里有明显的长尾离群值时考虑cityblock。注意改了距离之后sumd的含义也跟着变——它不再直接可比所以在对比不同距离的结果时不要只盯绝对值。3.2 初始化选项plus、sample与random的真实差异MATLAB的kmeans函数从R2014a版本开始默认初始化不再是简单随机抽样而是K-means对应Start参数的plus。K-means的初始化逻辑是第一个质心随机选后续每个质心按概率比例选——距离已有质心越远的点被选中做新质心的概率越大。这样做能让初始质心散布得更开WCSS的最终值显著优于纯随机抽样。% 显式指定不同的初始化方式 [idx1, c1] kmeans(data, K, Start, plus); % K-means [idx2, c2] kmeans(data, K, Start, sample); % 随机抽样 [idx3, c3] kmeans(data, K, Start, uniform); % 均匀随机范围外sample是纯随机从数据中取K个点uniform是在数据范围内均匀随机生成点后者的质心初始位置不一定是真实样本空簇概率更高。实际测试中plus通常比sample稳定得多但也会受随机数种子影响。所以R2014a之后的MATLAB在第一次运行kmeans时如果没设种子两次结果不同是常态——这不是bug是概率算法的特性。要消除随机性有两个办法第一运行前执行rng(固定值)让随机数生成器从同一起点出发第二用Replicates参数重复跑多次每次独立初始化最后返回WCSS最小的那组结果。第二种办法在工程上更实用因为它不只是让结果可复现而是真的能压掉一部分局部最优。3.3 空簇与异常值EmptyAction如何兜底迭代过程中可能出现一个极端情况某个簇在分配步骤后一个点都没分到。常见原因是初始质心选得太密集或者数据里存在大段空白区域某个质心成了孤岛。一旦出现空簇mean就无定义整个迭代直接崩掉。MATLAB的EmptyAction参数就是为这个兜底的默认值是singleton——空簇会重新选取数据中离该质心最远的点作为新质心保持K不收缩。% 空簇处理策略 [idx, c] kmeans(data, K, EmptyAction, singleton); % 重选最远点 [idx, c] kmeans(data, K, EmptyAction, drop); % 删除簇输出K个簇 [idx, c] kmeans(data, K, EmptyAction, error); % 报错停止drop会让最终簇数少于最初的K返回值里会出现空的簇索引error则直接把问题暴露出来适合写严谨代码时排查数据。如果你在用K-means时收到类似空簇在迭代中被重新初始化的警告不要直接忽略——它往往意味着K设得过大或者数据里有大量离群点。先画图看分布再决定是降K还是做数据清洗。4. 从代码到可视化idx、centroids与scatter背后发生了什么4.1 返回值的深层含义kmeans返回的第一个值idx不是聚类标签的语义名称它只是一个n×1的整数向量记录每个数据点最终属于哪个簇。索引从1到K数值本身没有大小含义——1簇不比2簇更靠前。第二个返回值centroids是一个K×p的矩阵每一行是对应簇的质心坐标。对二维数据它就是K个点的x、y坐标对p维数据每行是一个p维均值向量。% 示例读返回值的结构 data [1 1; 1.5 2; 3 4; 5 7; 3.5 5; 4.5 5; 3.5 4.5]; K 2; [idx, centroids] kmeans(data, K); % idx 是 7x1 的整数向量centroids 是 2x2 的矩阵 disp(idx); disp(centroids);这里有个容易踩的细节idx的顺序每次运行都可能变——第一次簇1包含点1、2第二次簇1可能包含点3、4。所以不要用簇1对应什么类别这样的固定思维去看聚类结果要看整体结构是否稳定。两组不同运行结果的idx做对比时必须做簇标签对齐否则直接算一致性会得到很低的分数造成结果不稳定的错觉。4.2 数据标准化与量纲陷阱K-means完全基于距离距离的计算又直接受特征量纲影响。假设一个二维数据集的第一个特征范围是0到1第二个特征范围是0到10000那第二个特征在距离计算里几乎一票否决——簇的形状会被拉成第二个特征的等距带第一个特征形同虚设。这不是算法的问题是距离本性的问题。所以跑kmeans之前对连续数值特征做标准化几乎是必须的% 标准化后再聚类 data_std zscore(data); % 每列均值0标准差1 [idx, centroids] kmeans(data_std, K);zscore把每个特征缩放到同一量级距离贡献才均衡。但也不是所有场景都该标准化如果特征本身有物理意义比如像素坐标、经纬度强行标准化反而会扭曲真实的近邻关系。判断标准就一句话——你是否希望所有特征在聚类时拥有同等话语权。图上的7个数据点如果直接跑量纲一致倒无所谓一旦换成真实业务数据这一步省了就大概率翻车。4.3 可视化验证K2时从图上能看出什么示例代码里的可视化部分其实是很好的诊断工具% 绘制聚类结果点按簇着色质心用黑叉标出 figure; hold on; scatter(data(:, 1), data(:, 2), [], idx, filled); scatter(centroids(:, 1), centroids(:, 2), 100, k, filled, Marker, x); legend(Cluster 1, Cluster 2, Centroids); hold off;scatter(data(:,1), data(:,2), [], idx, filled)中第三个参数[]表示点的尺寸用默认值第四个参数idx不是颜色值本身而是索引——MATLAB会按索引自动映射到当前颜色图的颜色上。第二个scatter用100指定质心标记尺寸k是黑色filled配合Marker, x画出一个黑叉质心。读图时重点看两点簇与簇之间有没有清晰分界质心是否落在各自簇的中心感位置。如果质心被挤到边缘或者两个簇纠缠在一起说明K选得不对或者数据本身不适合K-means。5. 避坑K-means实战中常见的五个翻车现场5.1 同样的代码两次结果不一样现象同一份数据、同一个K连跑两次kmeans结果idx和centroids不同。原因K-means初始化依赖随机数生成器不同的随机起点会得到不同的初始质心最终收敛到不同的局部最优。解决固定随机种子rng(42)实现可复现或者用Replicates参数跑多次取最优% 同时固定种子并多次运行 rng(0); [idx, centroids, sumd] kmeans(data, K, Replicates, 10);Replicates默认是1设成10意味着一共跑10次独立初始化返回总WCSS最小的一组结果。这个参数在数据量大一点后建议至少设5翻车概率能压到很低。5.2 质心出现NaN整个结果没法用现象运行完idx里有0值或centroids里有NaN绘图时某些点没有颜色。原因数据里混入了NaN或者某个簇在迭代中变成空簇。K-means的均值计算遇到NaN结果直接污染。解决聚类之前先清洗数据里任何一行含NaN要么填充分位数值要么直接删掉data_clean data(~any(isnan(data), 2), :);空簇的兜底在前一章说过用EmptyAction来接管但更聪明的做法是先画数据分布确认K值没有明显大于实际簇数一个已经分裂失败的空簇往往是K超标的信号。5.3 聚类结果对K值异常敏感K3和K4完全不像现象K从3换成4不仅多了一个簇原有3个簇的结构也全变了簇边界面目全非。原因这是K-means的本性增加K相当于改变优化空间的维度结果不连续是正常现象。解决不要拍脑袋定K用肘部法或轮廓系数做定量判断——对K1到K10分别跑聚类记录总WCSS画成折线后找拐点% 肘部法快速扫描 K 值 for k 1:10 [~, ~, sumd] kmeans(data, k, Replicates, 5); wcss(k) sum(sumd); end plot(1:10, wcss, -o);拐点处对应的K就是性价比最高的簇数。轮廓系数silhouette函数则给每个点一个0到1的凝聚度分数均值越高聚类越紧实。两者结合比单看某一个K的结果要靠谱得多。5.4 高维数据聚类效果奇差画出来完全重叠现象数据有几十个特征跑完聚类后画前两个主成分的散点发现各簇完全重叠感觉算法没用。原因特征维度高时欧氏距离的区分度会退化——所有点之间的距离趋于接近最近邻和次近邻之间的差距越来越小。解决先用PCA降维到5到20维或者在pdist2层面改用cosine距离后者在高维稀疏数据上经常表现远好于欧氏距离。% PCA降维后再聚类 [coeff, score] pca(data); data_pca score(:, 1:20); [idx, c] kmeans(data_pca, K);有个血泪教训先聚类再降维可视化跟先降维再聚类两者结果常常不一样。前者保留了全部特征的聚类结构后者可能丢掉关键信息。所以生产上通常先做降维压缩再聚类最后用前两个主成分画图做验证。5.5 图例永远显示Cluster 1和Cluster 2这样无意义的名称现象聚类画出来后图例永远是Cluster 1、Cluster 2看不出业务含义。原因kmeans返回的idx是数值索引并没有把哪个簇对应哪类用户映射出来。解决跑完聚类后手动对每个簇做业务特征画像——比如计算每个簇的特征均值、中位数再人工命名。这是聚类分析里最容易被忽略但价值最大的一步% 为每个簇打印特征均值辅助业务解读 for j 1:K fprintf(Cluster %d: , j); disp(mean(data(idx j, :), 1)); end没有这一步聚类结果只是数字游戏补上这一步才算真正完成了从算法输出到业务洞察的闭环。6. 设计取舍与进阶K-means为什么只改初始化不改进迭代K-means最妙的地方在于它只修改了初始化的概率规则没有动迭代过程一步。Lloyd算法的两步循环几十年没变过也没有引入额外计算负担K-means增加的复杂度只在初始化阶段总体时间复杂度依然是O(n·K·iter)。这个设计取舍说明一个问题K-means的主要痛点不在于迭代走法不对而在于初始质心的选择太粗糙。只要初始质心覆盖得够开后续的局部最优解质量就能逼近全局最优根本不需要重构迭代逻辑。理解了这个取舍你再看其他改进方案的思想就通透了。Bisecting K-means选择每次把最大的簇一分为二的策略本质上是把初始化问题转化为一个可以枚举的搜索过程Kernel K-means则干脆换了特征空间让原本线性不可分的簇在映射后变得可分。它们都不是在Lloyd迭代本身上做文章而是从数据表示或问题分解入手——这恰好印证了K-means框架的迭代机制本身设计得足够稳真正的不稳定因素在前置选择。我在实际项目里养成了一个习惯无论数据多简单跑kmeans时都强制走一遍rng固定种子Replicates至少5画分布图这三步。有时候看着图觉得这簇不是一眼就能看出来吗但真等到数据维度变高、噪声变大这个流程能救回很多次分析。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。