
简介一份围绕 K-means 聚类算法 Matlab 实现的图文整理面向需要掌握无监督学习与聚类分析方法的 MATLAB 初学者及数据分析人员。资源以 docx 格式集中讲解了 K-means 的基本原理、迭代步骤与常用距离度量方式包括初始化簇中心、为样本分配最近簇、重新计算质心等核心环节在此基础上给出基于内置 kmeans 函数的完整可运行示例演示二维数据点的聚类结果与质心可视化绘图并解析了索引向量与质心矩阵的返回含义。文档还针对预先指定簇数困难、初始中心敏感、易受异常值干扰等典型局限介绍了 K-means、二分 K-means、核 K-means 等改进思路帮助读者在实际项目中依据数据特点选择合适方案。该压缩包共 1 个 docx 文件大小约 15KB内容紧凑但覆盖从基础概念到代码实践的常用路径已有 442 人学习浏览适合作为快速入门参考。1. 一个能跑的kmeans示例距离真正用在数据上还差几步“kmeans聚类算法matlab代码”这类标题搜出来一大堆教程示例几乎都是同一副面孔定义data矩阵、设定K、调用kmeans、画散点图。照抄一遍确实能跑但换成自己的业务数据后你很快会遇到“跑是能跑结果不对劲”的情况。K到底设几个特征量纲差得很远要不要先处理为什么同样的代码两次跑出来的分类结果都不一样一个最小示例能复现不等于你能把这段代码直接迁移到新数据集上。这篇文章就从那个7×2的示例矩阵起步拆开kmeans函数的真实输入输出、关键参数和数据预处理最后封装成一个可复用脚本顺带指出几个最常见的翻车细节。适合刚接触聚类、想把MATLAB里这套命令真正用起来的从业者。2. 拆开最小示例kmeans的输入输出到底返回了什么2.1 先跑通最小代码每一行的真实含义先把这个最简单版本完整跑一遍。示例数据是7个二维点肉眼已经能看出大概两堆所以K2的设定在这里不算拍脑袋而是数据分布本身给了提示。% 示例数据每行一个样本第一列 x 坐标第二列 y 坐标 data [1 1; 1.5 2; 3 4; 5 7; 3.5 5; 4.5 5; 3.5 4.5]; K 2; % 聚类簇数 % 执行 K-means 聚类 [idx, centroids] kmeans(data, K); % 结果可视化 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;这段代码可以拆成三层看。第一层是data这个7×2的数值矩阵每一行是一个样本点两列分别是x和y坐标第二层设置K2并调用kmeans函数返回idx和centroids两个变量第三层是可视化scatter用idx给不同簇分配颜色质心用黑色叉号标出。有几个参数不是示例代码里写出来的但对理解MATLAB行为很关键。kmeans(data, K)是最简调用K必填其余都有默认值。idx是n×1的整型向量表示每个样本分到哪个簇取值范围1到Kcentroids是K×2的质心矩阵每一行对应一个簇的均值中心。如果你只盯着图看会觉得“聚类嘛就是画个彩图”但真正对后续分析有用的是idx它不是颜色标签而是每个样本的簇编号后续统计、导出、建模都要靠它。另一个值得注意的细节是scatter的第四个参数idx在这里被当作颜色向量直接传入MATLAB会自动按当前colormap给不同整数分配不同颜色所以不需要你手动指定哪个点是什么颜色。这个写法在示例数据上没问题但如果样本量很大、簇数很多建议改用gscatter或先sortrows再画否则同类点会被遮挡得很严重。2.2 输出不止两个sumd和D比那张图更有工程价值大多数教程只讲idx和centroids实际做项目时更常用的反而是后面两个输出。我一般习惯把kmeans的返回值全部接出来[idx, centroids, sumd] kmeans(data, K); disp(sumd); % 每个簇内所有点到质心的距离平方和sumd是K×1向量第i个元素表示第i个簇的紧致程度。两次聚类画出来的散点图可能几乎一样但如果sumd有差距说明某一次找到了更“紧”的划分。做参数调优时我通常会把sumd的总和作为量化指标而不是靠肉眼判断哪次聚类更好。第四个输出D是n×K的距离矩阵每个元素是样本点到某个质心的距离拿它做离群点检测和异常分析非常顺手。这个矩阵看似费内存但n上万时仍然可以接受而且很多聚类质量指标都直接依赖它。顺带解释一个容易困惑的点MATLAB的kmeans虽然是个封装好的黑匣子但内部流程就是课本上那四步——初始化质心、把每个点分配到最近的簇、重新计算每个簇的均值、重复直到质心不再变化或达到最大迭代次数。你写不写这个过程函数都会迭代理解这一点对后面调参很有帮助。2.3 K值不是拍脑袋手肘法初筛再谈evalclusters示例里的K2是因为数据分布太明显。真实数据里簇数往往没有决定性的直观证据最常见做法是先跑一条手肘法曲线看总簇内距离和的拐点在哪里。% 简单手肘法对多个K运行kmeans记录总距离和 K_range 1:6; totd zeros(size(K_range)); for i 1:length(K_range) [~, ~, sumd] kmeans(data, K_range(i), Replicates, 10); totd(i) sum(sumd); end plot(K_range, totd, o-); xlabel(K); ylabel(Total sum of distances);这段代码里出现了两个值得注意的参数。Replicates10表示重复聚类10次并保留最优结果因为kmeans对初始中心敏感单次运行可能停在局部最优手肘法又依赖稳定的sumd数值所以这里必须加。K_range取1到6是因为原始数据只有7个点簇数不可能超过样本数真实情况下可以根据业务规模扩大范围。手肘法只是一条粗线索更严格的做法是用evalclusters那条路径比如silhouette或Davies-Bouldin指标。不过对大多数场景来说手肘法先把候选K缩到两三个再结合业务含义去选才是真正落地时的时间分配方式。K值这个东西统计指标只能给建议最终拍板的是你对数据和业务的理解。3. 把示例升级成可用脚本标准化、随机性与距离度量3.1 量纲不统一聚类结果会被大数字带偏示例数据是平面坐标x和y的量纲一致。真实数据里经常出现年龄、收入、最近一次消费间隔、年消费金额这种数值范围完全不同的列。如果直接把原始数据丢给kmeans欧氏距离的计算会被数值范围大的特征主导小量纲特征几乎起不到区分作用。构造一个能说明问题的扩展场景给示例数据加上第三列取值在10到70之间前两列还是原来的坐标% 给示例数据增加第三列量纲明显不同 data3 [1 1 10; 1.5 2 20; 3 4 30; 5 7 40; 3.5 5 50; 4.5 5 60; 3.5 4.5 70]; K 2; [idx, centroids] kmeans(data3, K); disp(idx);跑完你会发现聚类结果几乎完全由第三列决定前两列的区分能力被稀释了。原因在于距离计算时第三列的绝对差值远大于前两列的坐标差值平方之后更夸张。解决方式很简单聚类前先做标准化data3_z zscore(data3); % 每列减去均值、除以标准差 [idx_z, centroids_z] kmeans(data3_z, K); disp(idx_z);zscore是MATLAB对每列独立做零均值单位方差处理的标准操作。标准化之后第三列不再“以大欺小”前两列才能重新参与距离计算。这个操作对kmeans这类基于距离的算法几乎是必修课没有例外。我一般会在标准化之后顺手做一步检查看标准化后的数据有没有明显的离群值。kmeans对异常值非常敏感一个点特别远就可能把某个簇的质心拉过去如果数据里存在明显离群点建议先做截尾处理或用稳健标准化否则聚类边界会被带歪。这一步没有写在任何示例代码里但几乎每个真实数据集都会遇到。3.2 用Replicates和rng对抗局部最优kmeans的初始化本质是随机的。MATLAB的kmeans函数默认使用的已经是k-means初始化这比完全随机选初始点好很多但k-means自身也带随机性数据稍微复杂一点单次运行照样可能停在局部最优。表现就是同样的代码两次运行idx的划分结果对不上。要解决这个问题我的固定套路是设置较大的Replicates同时用rng固定随机数生成器的种子rng(2024); % 固定随机种子保证结果可复现 [idx_stable, centroids_stable] kmeans(data3_z, K, ... Replicates, 10, MaxIter, 200);Replicates10意味着MATLAB会从10组不同的初始质心出发分别聚类最终返回总距离和最小的一次结果。MaxIter200限制单次迭代的最大次数防止极端情况下循环不收敛。这两个参数组合起来基本能应对绝大多数中小规模数据。固定rng种子是工程上非常必要的习惯。否则哪怕用了Replicates不固定种子下次重跑脚本时随机序列变了结果又会对不上。做项目报告或给别人交付代码时rng(固定值)这行几乎是强制项它能保证任何人拿到你的脚本跑出来的分类结果完全一致而不是“大概一致”。还有一点容易被忽略Replicates越大运算时间线性增长。数据量几万行以上时10次重复会明显变慢。我一般会根据数据集规模在5到15之间调整先用3跑通流程确认没问题再加到10。3.3 距离度量sqeuclidean不是唯一选择MATLAB的kmeans默认使用sqeuclidean也就是平方欧氏距离最小化的是簇内距离平方和这跟算法的数学定义一致。但实际业务中默认并不总是最优解。下表是几个常用距离度量的真实适用场景距离度量适用场景注意点sqeuclidean默认选项数据接近球形分布时效果好对量纲非常敏感使用前必须标准化cityblock高维稀疏数据或对离群点不敏感的任务曼哈顿距离迭代收敛通常更慢cosine文本向量、用户画像等方向敏感数据对向量的模长不敏感只关注方向correlation时序数据或形状相似性分析用得最少需要数据本身有一定结构指定距离度量的方式很简单在参数里加Distance即可[idx_cos, centroids_cos] kmeans(data3_z, K, Distance, cosine);选距离度量时我建议先想清楚业务上“相似”到底意味着什么。比如做文本聚类cosine通常比sqeuclidean合理因为文本向量的模长往往代表文档长度而不是语义差异做地理位置聚类sqeuclidean或cityblock都说得通。这类选择没有标准答案但有一个经验先标准化再用sqeuclidean跑一版如果结果不合理再试cosine比一开始就纠结距离参数更高效。4. 避坑MATLAB kmeans实战里五个容易翻车的细节4.1 现象同样的代码两次运行分类结果对不上第一次跑完聚类没保存第二次重新执行脚本发现idx变了甚至簇的编号都换了这是最常见的困惑。原因kmeans的初始质心是随机选取的k-means也只保证“大概率好”不保证每次一样。不同初始点可能收敛到不同的局部最优解所以两次运行结果不同是正常现象不是代码写错了。解决在kmeans之前调用rng固定随机种子同时把Replicates设到5以上让MATLAB从多个初始点出发选取最优解。固定种子还能让其他人拿到你的脚本时复现出完全一致的结果这对项目交付尤其重要。4.2 现象数据没标准化聚类结果主要由数值范围最大的特征决定第三个特征取值范围0到10000前两个特征取值0到1跑完发现簇的边界几乎只沿着第三个特征方向切分前两个特征形同虚设。原因kmeans用欧氏距离衡量样本相似度取值大的特征在距离计算中占绝对权重小量纲特征的区分能力被直接淹没。这个现象在真实数据里非常隐蔽因为函数完全不会报错结果看起来也很“正常”。解决聚类前对数值型特征做zscore标准化。注意只对连续数值列做类别型特征要另做编码或直接排除避免把编号当作有序数值参与距离计算。4.3 现象设了K5结果却出现空簇或NaN质心数据分布比较稀疏或不均衡时把K调大一些kmeans返回的centroids里出现NaN行或者某个簇一个样本都没有。原因初始质心分布不佳迭代过程中某个簇没有任何样本被分配进去导致该簇无法计算均值。MATLAB对空簇的默认处理是在后续迭代中重新初始化该簇质心但结果往往不稳定尤其当数据存在大量重复点或分布极度偏斜时。解决第一步检查K是否明显超过数据内在结构手肘法的拐点会给出提示第二步增加Replicates让不同初始点下的空簇问题被覆盖第三步检查数据中是否有大量完全相同的重复样本有就先去重。如果K确实需要大考虑改用层次聚类或GMM这类对簇数不敏感的算法作为对照。4.4 现象函数没报错但聚类结果明显偏离直觉数据在散点图上能看出明显的非球形分布聚类结果却把几个相距很远的区域强行分到一起质心落在完全没有样本的空旷区域。原因kmeans假设簇是凸的、各向同性的用欧氏距离和均值更新质心当真实数据呈现环形、长条形或密度不均时它给出的划分就是会偏离直觉这是算法本身的边界不是MATLAB实现的问题。解决先用scatter简单看一眼数据形态确认簇形状和分布。如果数据是非球形的换个思路用谱聚类或DBSCAN这类基于密度或图结构的算法或者先对数据做核映射再聚类。另外异常值也要单独排查个别离群点会把质心拉偏导致正常样本被错分。离群点检测可以在聚类前单独做一步。4.5 现象把表格变量直接传给kmeans报错说数据必须为数值矩阵数据读进来后是table类型直接运行kmeans(data, K)报错信息提示输入必须是数值矩阵。原因kmeans只接受数值矩阵table、cell、逻辑数组都不行。很多人从读文件到聚类之间少了一步类型转换。解决先确认数据全为数值列然后使用table2array转换data_array table2array(data_table);如果表格里混有类别列需要单独提取数值列或者对类别列做编码后再拼回矩阵。聚类前顺手查看一下data_array的size和class能减少大量这类低级报错。5. 技巧把聚类结果导出成表封装一个可复用脚本5.1 把簇号写回原表聚类做完不能只看图要把idx写回原始数据才能做后续的按簇统计。最简单的方式是直接拼列result_table array2table(data); result_table.Cluster idx; head(result_table);这里array2table把数值矩阵转成table再加一列Cluster保存簇编号。导出到CSV用一条writetable即可后续在Excel或SQL里都能直接处理。5.2 封装一个最小可复用函数我习惯把标准化、固定种子、聚类、回写结果封装成一个函数避免每次在不同脚本里复制粘贴一长串参数function [idx, centroids, result_table] run_kmeans(data, K, varargin) % 参数解析支持 Normalize、Replicates p inputParser; addParameter(p, Normalize, true); addParameter(p, Replicates, 10); parse(p, varargin{:}); if p.Results.Normalize data zscore(data); end rng(2024); [idx, centroids] kmeans(data, K, Replicates, p.Results.Replicates); result_table array2table(data); result_table.Cluster idx; end使用起来很直接[idx, centroids, t] run_kmeans(data3, 2); writetable(t, clustering_result.csv);这个封装把三类容易出错的细节一次解决标准化默认为开启、随机种子固定、结果自动回写。从那以后我每次拿新数据做聚类都会强制走一遍这个流程先标准化再固定种子再用Replicates至少跑5次选最优排查问题省掉很多心力。希望这个流程也能帮到你。本文还有配套的精品资源点击获取