
简介这份资源面向刚接触无监督学习、需要在 MATLAB 环境中完成数据聚类实验的学生与工程人员围绕 K-means 算法的原理讲解与代码落地展开帮助读者理解聚类中心初始化、样本分配、质心更新与迭代终止的完整流程。压缩包共 1 个 docx 文件约 15KB内容以文字讲解配合 MATLAB 示例代码为主涵盖内置 kmeans 函数的调用方式、idx 与 centroids 返回值的含义以及用 scatter 绘制分簇散点与质心的可视化写法。文中还讨论了 K 值选取、异常值敏感与初始化依赖等局限并简要介绍 K-means、Bisecting K-means 与 Kernel K-means 等改进思路便于读者在复现示例后继续扩展。目前已有 444 人学习适合作为课堂实验或自学聚类的入门参考材料。1. 拿到一份 kmeans 聚类算法 matlab 代码先别急着跑七行二维数据、一个kmeans(data, K)调用、一张散点图这份 kmeans 聚类算法 matlab 代码文档看起来简单到让人怀疑它值不值得单独存一份。但真正在 MATLAB 里做过聚类落地的人都知道能跑通和能交付之间隔着一段距离idx和centroids两个返回值怎么用、K 怎么定、重复跑三次结果为什么不一样、散点图上的颜色顺序为什么和簇编号对不上这些问题在示例代码里一行注释都不会提。这份资源的价值不在于算法本身有多深而在于它给了一个最小可复现的骨架让你能把 K-means 从公式落到 MATLAB 的变量空间里再往上叠评估、叠可视化、叠业务解释。适合两类人一类是刚接触无监督学习、需要在课程设计或小数据集上快速出结果的新手另一类是熟手想找一个干净的基线脚本用来对比自己写的初始化策略或距离度量改动。下面按「这份代码到底在做什么 → 怎么把它改成能用的脚本 → 哪里会翻车」的顺序拆开讲。2. 七行数据背后的 K-means 执行链路从 idx 到 centroids2.1 示例代码逐行拆解与变量语义先把原料里的代码原样摆出来逐行看清楚每个变量在 MATLAB 工作区里到底是什么形状、什么类型。很多人跑完只看到图却说不清idx是列向量还是行向量、centroids的行序和簇编号是不是一一对应后面接评估指标时就会对不上号。% K-means 聚类算法示例 % 初始化数据7 个二维样本每行一个样本 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: N×1 向量取值 1..K表示每个样本归属的簇 % centroids: K×2 矩阵第 i 行是第 i 个簇的质心 [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是 N×D 矩阵N7、D2这是 MATLAB 统计与机器学习工具箱里绝大多数函数的样本约定——行是样本、列是特征搞反了不会报错但结果全错。kmeans(data, K)返回两个东西idx是 N×1 的簇索引centroids是 K×D 的质心矩阵。绘图部分用idx直接作为scatter的颜色参数这是 MATLAB 里很顺手的一个技巧因为scatter的第四个参数接受数值向量并自动映射到当前色图。参数说明K是唯一需要人定的超参示例取 2 是因为数据肉眼可分两团。kmeans还有几个默认值藏在背后——距离度量默认是平方欧氏距离sqeuclidean最大迭代默认 100启动重复次数默认 1初始化默认用kmeans的采样策略。这几个默认值决定了同一份数据两次运行可能给出不同的簇编号后面避坑章节会专门说。2.2 为什么示例选内置 kmeans 而不是手写迭代有人会问既然是学算法为什么不自己写初始化、分配、更新质心那三步循环答案是分场景的。如果你要交的是算法原理作业手写一遍确实有必要能逼你看清「分配」和「更新」交替进行时目标函数单调不增这件事。但如果你要的是工程落地内置kmeans至少帮你处理了三件手写容易漏的事空簇处理、收敛判据、以及多次启动取最优。手写版本最常见的翻车是某个簇在迭代中变空然后质心变成NaN整个矩阵污染后面所有距离计算全废。内置函数遇到空簇会重新选一个离现有质心最远的点作为新质心这个策略不完美但至少不会让脚本崩掉。收敛判据上内置实现比较的是连续两次迭代的簇分配是否稳定而不是简单看质心位移小于某个阈值后者在数据尺度差异大时很难定一个通用阈值。所以我的建议是用内置kmeans做基线把手写版本作为对照实验。具体做法是把内置结果的簇内平方和within-cluster sum of squares算出来再和你手写版本的结果比如果手写版明显更差大概率是初始化或空簇处理出了问题。这个对照不需要额外工具箱几行代码就能搭起来。% 计算簇内平方和作为后续对比手写版本的基线 wcss 0; for k 1:K members data(idx k, :); if isempty(members) continue; end diff members - centroids(k, :); wcss wcss sum(diff(:).^2); end fprintf(K%d 时簇内平方和: %.4f\n, K, wcss);这段代码里idx k做逻辑索引取出第 k 簇的所有样本diff(:).^2把矩阵拉平后逐元素平方再求和得到该簇所有样本到质心的平方距离总和。wcss是后面选 K 的核心指标也是判断一次聚类是否「聚散了」的量化依据。注意空簇判断不能省虽然内置函数一般不会返回空簇但你换成手写版本或改初始化策略后这个分支就会派上用场。3. 把示例改成可复用脚本K 值选择、重复启动与结果固化3.1 用肘部法给 K 一个说得过去的理由示例里K 2是拍脑袋定的真实数据上拍脑袋的代价是返工。常见做法是跑一串 K 值记录每个 K 对应的簇内平方和画出来找拐点。这不是什么高深方法但它能把「我觉得分两类合适」变成「K 从 1 到 6 的 WCSS 曲线在 K3 之后下降明显变缓」后者在评审或答辩场合站得住脚。% 肘部法扫描 K1..6记录每个 K 的簇内平方和 K_range 1:6; wcss_list zeros(size(K_range)); for i 1:numel(K_range) k K_range(i); % Replicates5每次 K 跑 5 次启动取 WCSS 最小的那次 [idx_tmp, C_tmp, sumd] kmeans(data, k, Replicates, 5); wcss_list(i) sum(sumd); % sumd 是每个簇的簇内距离和 end figure; plot(K_range, wcss_list, -o); xlabel(聚类簇数 K); ylabel(簇内平方和 WCSS); title(肘部法选择 K); grid on;逻辑说明kmeans的第三个返回值sumd是 K×1 向量存每个簇内样本到质心的距离平方和sum(sumd)就是整体 WCSS比手动循环算更省事。Replicates, 5是关键参数它让每次 K 值跑 5 次不同初始化并保留 WCSS 最小的结果直接缓解了初始化敏感问题。参数说明Replicates取 5 是经验值数据量大或簇结构复杂时可以加到 10 到 20代价是运行时间线性增长。K_range的上界不要超过样本数的平方根太多7 个样本扫到 6 已经接近极限真实数据一般扫到 10 左右就够看拐点了。肘部法本身有主观性拐点不明显时别硬找换轮廓系数或直接上业务约束。3.2 固定随机种子让结果可复现同一份数据、同一个 K两次运行给出不同的簇编号这是 K-means 最容易被误判为「算法不稳定」的地方。根因是初始化带随机性而 MATLAB 的随机数流默认每次启动都不同。解决办法是在调用前固定随机种子让整个流程可复现。% 固定随机种子保证多次运行结果一致 rng(42); % 42 是任意选的整数换成别的也行关键是固定 % 用固定种子重新跑一次并显式指定距离度量和重复次数 [idx_fixed, C_fixed, sumd_fixed] kmeans(data, 2, ... Distance, sqeuclidean, ... Replicates, 10, ... MaxIter, 200, ... Display, final); % 把结果固化到结构体方便后续保存或传给其他函数 result.idx idx_fixed; result.centroids C_fixed; result.wcss sum(sumd_fixed); result.K 2; save(kmeans_result.mat, result);逻辑说明rng(42)设定全局随机数生成器的起点之后所有依赖随机数的函数都从同一序列取值所以只要代码和输入不变结果就稳定。Display, final会在命令窗口打印最终迭代次数和 WCSS调试时比闷头跑有用。最后把结果塞进结构体存成.mat是为了把「跑出结果」和「用结果」解耦——绘图、评估、导出可以分脚本做不用每次重跑聚类。参数说明Distance显式写出来是为了防止有人后来改了全局默认却不知道可选值还有cityblock曼哈顿距离、cosine余弦距离选哪个取决于你的特征语义欧氏距离适合连续且量纲一致的数值特征。MaxIter从默认 100 提到 200 是给复杂数据留余量正常收敛远用不到。注意rng只保证同一 MATLAB 版本内可复现跨版本不保证所以固化结果比固化种子更可靠。3.3 把 idx 和 centroids 接到下游导出与可视化增强聚类结果最终要被人看、被别的程序用。示例里的散点图只能看个大概实际交付时至少要做两件事把每个样本的簇归属导出成表以及在图上标出簇编号和质心坐标避免图例和实际簇号错位。% 导出样本归属表 T array2table(data, VariableNames, {x, y}); T.cluster idx_fixed; writetable(T, cluster_assignments.csv); % 增强可视化标注簇编号与质心坐标 figure; gscatter(data(:,1), data(:,2), idx_fixed, lines(K), o, 8); hold on; for k 1:K text(C_fixed(k,1), C_fixed(k,2), sprintf(C%d, k), ... FontSize, 12, FontWeight, bold, Color, k); plot(C_fixed(k,1), C_fixed(k,2), kx, MarkerSize, 14, LineWidth, 2); end xlabel(特征 1); ylabel(特征 2); title(sprintf(K-means 聚类结果K%d, K)); hold off;逻辑说明array2table把数值矩阵转成带列名的表追加cluster列后writetable导出 CSV这是把聚类结果交给非 MATLAB 环境比如表格软件或下游脚本最省事的方式。可视化部分换成gscatter它按分组变量自动着色并生成图例比手动scatter加legend更不容易出现「图例写 Cluster 1 但实际颜色对应簇 2」的错位。text在质心位置标C1、C2plot用黑色叉号叠一层保证打印成黑白也能分辨。参数说明lines(K)生成 K 种区分度较高的颜色K 超过 7 时颜色会开始接近这时候要考虑换标记形状或分面画图。MarkerSize, 14和LineWidth, 2是为了质心在图上足够醒目数值可按输出尺寸调。导出的 CSV 里簇编号是数值下游如果按字符串处理记得转换。4. 避坑与排查K-means 在 MATLAB 里最容易翻车的五处4.1 现象两次运行簇编号对调误以为算法不稳定原因K-means 的簇编号本身没有语义初始化不同会导致簇 1 和簇 2 互换但聚类结构其实一样。解决固定rng种子并在比较两次结果时不要直接比idx是否相等而是比 WCSS 或调整兰德指数如果只是展示固定种子后编号就稳定了。4.2 现象数据量纲差异大时某个特征完全主导距离原因平方欧氏距离对数值范围敏感一个取值在千级的特征会淹没取值在个级的特征。解决聚类前做标准化data_norm normalize(data)或手动(data - mean(data)) ./ std(data)再喂给kmeans。注意标准化参数要保存下来新样本预测时用同一套均值和标准差。4.3 现象K 设大了出现空簇或单样本簇原因数据本身簇结构少于 K或者初始化把两个质心扔进了同一团。解决先看肘部曲线确认合理 K 范围确实需要大 K 时提高Replicates次数并检查返回的sumd里是否有接近 0 的项那通常就是单样本簇业务上要判断是否该合并。4.4 现象idx拿去索引原矩阵时报维度不匹配原因idx是 N×1 列向量有人当成行向量用或者data本身是 D×N 转置过的。解决进函数前统一用size(data,1)断言样本数assert(size(data,1) numel(idx))把维度问题挡在聚类之后、下游之前。4.5 现象含缺失值的数据直接报错或结果异常原因kmeans不接受NaN部分版本会直接报错部分场景下静默丢弃导致样本数对不上。解决聚类前显式处理缺失要么删行data data(~any(isnan(data),2),:)要么用均值或中位数填补并在结果表里保留原始行号映射避免导出后对不上原始记录。5. 从二维示例到真实数据维度扩展与结果验证的一个习惯示例只有两个特征画个散点图就能肉眼验收真实数据动辄十几维图是画不出来的这时候验证手段要换。我一般会做三件事按顺序走一遍再决定这份聚类结果能不能用。第一件是看簇规模分布。用tabulate(idx)或histcounts(idx, 1:K1)看每个簇有多少样本如果出现一个簇占了 90% 以上基本说明 K 偏小或距离度量不合适别急着解释业务含义。第二件是看质心在原始特征上的取值把centroids反标准化回原始量纲逐维看哪个特征在簇间差异大这比任何降维图都直接。第三件是留出验证如果样本量允许随机抽 20% 不参与聚类用训练出的质心给它们做最近邻分配看分配结果的簇规模比例和训练集是否接近偏差过大说明簇结构不稳定。% 真实数据上的三步验证骨架 data_norm normalize(data); % 标准化 [mu, sigma] deal(mean(data), std(data)); % 保存反标准化参数 rng(7); [idx_v, C_v, sumd_v] kmeans(data_norm, K, Replicates, 20); % 1) 簇规模分布 fprintf(簇规模分布\n); tabulate(idx_v); % 2) 质心反标准化回到原始量纲看差异 C_original C_v .* sigma mu; disp(各簇质心原始量纲); disp(array2table(C_original, VariableNames, feature_names)); % 3) 留出验证用质心给新样本做最近邻分配 % 这里 new_data 需先按同一套 mu/sigma 标准化 new_norm (new_data - mu) ./ sigma; d pdist2(new_norm, C_v); % 新样本到各质心的距离 [~, new_idx] min(d, [], 2); % 取最近质心 fprintf(留出样本簇规模分布\n); tabulate(new_idx);这段骨架里pdist2算的是新样本到每个质心的距离矩阵min(d, [], 2)沿第二维取最小值得到最近质心编号这就是 K-means 分配步骤的复用。feature_names需要你按实际列名填没有就跳过表头。留出验证的簇规模比例和训练集差多少算「接近」没有硬标准我的经验是单个簇的占比偏差超过 10 个百分点就要回头查标准化是否一致、K 是否合理。最后说一个我踩过的坑早期做聚类交付时我把idx直接当业务标签用结果换一批数据重跑簇编号全变了下游报表对不上。从那以后我每次固化聚类结果都会同时存质心矩阵和标准化参数新数据一律走「标准化 → 算距离 → 取最近质心」这条固定路径不再依赖重跑聚类来对齐编号。这个习惯比任何调参技巧都省事希望帮到你。本文还有配套的精品资源点击获取