简介Matlab初学者与机器学习入门者可借助这份文档系统掌握K-means聚类算法的原理与实现。文档先介绍聚类核心思想再梳理初始化、分配、更新、迭代四个基本步骤并给出可直接运行的Matlab代码示例涵盖数据准备、kmeans函数调用、idx与centroids参数解读以及散点图着色、质心标记等可视化操作便于快速上手并理解输出结果。除基础用法外还专门讨论K值选择、异常值敏感、初始化随机等局限引入K-means、二分K-means、核K-means等改进思路适合课程设计、期末复习或实际数据分析前参考。资源为单个docx文档压缩包体积约15KB轻量便捷。已有442人学习下载可作为Matlab聚类工程实践的入门参考。1. 别急着搜“kmeans聚类算法matlab代码.docx”先想清楚你要的是什么网上流传着一份《kmeans聚类算法matlab代码.docx》搜到这个标题的人多半不是想啃原理而是手头有数据、要分群、要给老板交付一张聚类图。kmeans聚类算法matlab代码这个东西难点从来不在“用哪一行命令”而在“K选几、数据要不要标准化、结果怎么解释”。这篇笔记直接从这三个问题入手给出一份能直接跑的MATLAB实现并讲清内置函数与手写代码的界限。适合初学MATLAB的在校学生、做数据分群但被聚类结果反复困扰的工程师以及想快速验证聚类思路的算法测试人员。2. MATLAB跑k-means内置函数到底省了多少事代价是什么2.1 k-means的迭代逻辑三步走一个收敛条件k-means的数学表达很简洁给定N个样本每个样本有d维特征把样本分成K个簇使得每个样本到它所属簇中心的距离平方和最小。这个目标函数叫SSE误差平方和。迭代逻辑就三步初始化K个簇中心把每个样本分到离它最近的中心重新计算每个簇的均值作为新中心。然后重复第二、三步直到簇中心不再明显移动。为什么很多入门教程不敢把这一步讲透因为SSE函数是非凸的梯度下降基本帮不上忙实际工程里用的是交替迭代的Lloyd算法只能保证收敛到局部最优不能保证全局最优。这也解释了同一份数据换个初始中心分群结果可能完全不一样。这个特性是后续所有参数调节和踩坑的根源先记在心里。2.2 内置kmeans函数与手写实现的取舍MATLAB自带的kmeans函数封装了迭代、K-means初始化还支持并行计算一行代码就能调用。但它有个让工程师头疼的问题过程是个黑匣子你很难把中间迭代过程拿出来解释给业务方。当老板问你“这个客户为什么被分到这一组”你手上只有一张最终的散点图那就是个尴尬场面。我一般做两手准备先用内置函数快速验证聚类可行性再在手写代码里复现核心逻辑保留每一步的中心坐标和SSE记录。很多人说内置函数性能更好但实际上50万行数据以内手写版本的性能差距并不明显反而手写代码在debug时能直接看到每一次迭代的状态。对比维度内置kmeans手写实现代码量1行约40行迭代过程透明性低高初始化策略默认K-means自由控制大规模数据性能明显更优一般够用适合场景快速验证、海量数据学习、定制、业务解释2.3 为什么不直接复制文档里的代码就跑docx里贴的代码大概率来自某个版本的教学文档直接复制到新版MATLAB常会翻车。总结下来主要有三类问题一是全角字符doc排版时中文引号、括号被转成全角MATLAB编辑器不认报错信息指向莫名其妙的字符二是旧版本options写法老代码喜欢先定义一个庞大的option结构体新版本改成了字符串参数三是示例数据和跑聚类部分的代码耦合在一起换成自己的数据要动好几个地方。我写这篇笔记特意把代码做成了“函数调用脚本”的结构数据格式、参数、可视化全部分开。你想复用哪块就抽哪块不用复制整页代码回去改半小时。对旧代码报错的处理办法后面避坑章节会单独给一条经验。3. 从零写一版可用的k-means代码核心函数加调用脚本一次给全3.1 主函数迭代、中心更新与簇分配直接给一份完整的手写实现不依赖任何工具箱任何常见MATLAB版本都能跑function [label, mu, SSEtrace, iters] my_kmeans(X, K0, maxIter, tol) % 输入 % X : N×d 数据矩阵一行一个样本一列一个特征 % K0 : 簇数 % maxIter : 最大迭代次数 % tol : SSE相对变化阈值小于它判定收敛 % 输出 % label : N×1 的簇标签取值1..K0 % mu : K0×d 的最终簇中心 % SSEtrace: 每次迭代的SSE值向量用于画收敛曲线 % iters : 实际迭代次数 N size(X, 1); idx randi(K0, N, 1); % 先给每个样本一个随机初始簇标签 mu zeros(K0, size(X, 2)); SSEtrace zeros(1, maxIter); for iter 1:maxIter % —— 更新簇中心按当前划分取均值 —— for k 1:K0 inCluster (idx k); if sum(inCluster) 0 % 空簇保护随机选一个样本当新中心避免除零 mu(k, :) X(randi(N), :); else mu(k, :) mean(X(inCluster, :), 1); end end % —— 重新分配样本到最近中心 —— distAll zeros(N, K0); for k 1:K0 diffMat X - mu(k, :); % N×d 广播减法 distAll(:, k) sum(diffMat.^2, 2); % 欧氏距离平方 end [~, newIdx] min(distAll, [], 2); % —— 计算SSE样本到所属中心距离平方和 —— sse 0; for k 1:K0 if sum(newIdx k) 0 sub X(newIdx k, :); mu_k mean(sub, 1); sse sse sum(sum((sub - mu_k).^2, 2)); end end SSEtrace(iter) sse; % —— 收敛判断SSE几乎不下降就退出 —— if iter 1 abs(SSEtrace(iter) - SSEtrace(iter-1)) / SSEtrace(iter-1) tol iters iter; SSEtrace SSEtrace(1:iters); label newIdx; return; end idx newIdx; end % 达到最大迭代上限返回最后一次结果 iters maxIter; label idx; SSEtrace SSEtrace(1:iters); end逻辑说明代码故意不用任何工具箱函数只用基础语法。三个关键点在收敛判断、空簇保护和SSE计算。收敛判断用“相对变化量”而不是绝对变化量是因为不同数据集的SSE量级可能差几十倍绝对阈值没法通用。空簇保护是随机初始化下必须兜底的逻辑没有它会直接出现NaN并让整条结果被污染。SSE的计算放在重新分配之后用的是新标签和新均值这样收敛曲线是单调下降的排查问题时会很直观。参数说明tol设成1e-4二维仿真数据一般10到20次迭代就收敛。如果你的数据特征方差特别大建议把它放宽到1e-3否则迭代会在阈值附近来回震荡。maxIter设100就够如果到了100还没收敛多半是K选择不合适或数据本身不适合做k-means不是迭代次数不够。3.2 调用脚本数据生成、运行与可视化主函数写好之后还需要一个调用脚本把数据喂进去并把结果画出来rng(1); % 生成3个高斯簇的仿真数据每个簇200个点 X [randn(200, 2) [4, 4]; ... randn(200, 2) [0, 0]; ... randn(200, 2) [4, 0]]; K0 3; maxIter 100; tol 1e-4; [label, mu, SSEtrace, iters] my_kmeans(X, K0, maxIter, tol); figure; gscatter(X(:, 1), X(:, 2), label, rgb, ..); hold on; plot(mu(:, 1), mu(:, 2), kx, MarkerSize, 12, LineWidth, 2); title(sprintf(k-means clustering, iters%d, SSE%.2f, iters, SSEtrace(end))); xlabel(feature 1); ylabel(feature 2); figure; plot(1:length(SSEtrace), SSEtrace, -o); xlabel(迭代次数); ylabel(SSE); title(SSE收敛曲线);参数说明rng(1)固定随机种子保证结果可复现。randn生成标准正态分布数据叠加位移构造三个清晰可分的簇。gscatter按label画不同颜色..控制点样式plot里的kx是黑色叉号用来标簇中心MarkerSize设12在图上才醒目。第二条SSE曲线如果出现反弹说明中心更新和样本分配的顺序没对齐对照我的实现检查大多数问题都出在“用旧标签算新SSE”这个顺序上。3.3 代码的边界手写实现能跑什么不能跑什么手写代码在二维、三维数据上做演示和验证完全够用但超过5万行、特征维度上百时双重for循环会明显变慢。这时候两种改法向量化或用内置函数。向量化可以用pdist2计算整个距离矩阵用accumarray按标签聚合均值速度能快一个数量级。若没有统计工具箱pdist2会报错可以用bsxfun展开平方差计算。另一个边界是所有k-means实现共有的数据里的簇必须是凸形结构。如果数据呈长条、环形或交错形状k-means的SSE再低也没有实际意义这时候应该换DBSCAN或高斯混合模型。手写实现最值得保留的不是聚类结果本身而是SSE轨迹它是判断“数据到底适不适合做k-means”的第一手证据。4. K值选择与初始化策略聚类效果的分水岭4.1 肘部法则画一张SSE曲线图K就不是猜的了K值选多少是k-means最常被问的问题。最常用的判断方法是肘部法则从K1到K10分别跑一遍记录每个K的最终SSE画成曲线。曲线里会有一个下降速度突然变缓的点形状像手臂的肘部那个K就是推荐值。sseAll zeros(1, 10); for Ktmp 1:10 [~, ~, SSEtraceTmp, ~] my_kmeans(X, Ktmp, 100, 1e-4); sseAll(Ktmp) SSEtraceTmp(end); end figure; plot(1:10, sseAll, -o); xlabel(K); ylabel(SSE); title(Elbow Method for K selection);逻辑说明K每增加一个SSE必然下降但多分一个簇能带来的边际收益在递减。肘部选择的就是“边际收益明显变小”的K。这个方法的麻烦在于有些数据没有明显拐点SSE平滑下降此时不能硬找肘部要结合轮廓系数和业务约束做决定。业务约束通常指运营成本客户分群多一个簇后续话术、产品策略就多一套工程取舍优先级往往高于统计指标。4.2 轮廓系数评估聚类质量的量化指标SSE只衡量簇内紧密度但如果你只看SSE最“优”的方案是把每个点都单独分成一个簇。轮廓系数综合了簇内紧密度和簇间分离度取值接近1说明样本分得合理接近-1说明应该换到邻近簇。MATLAB统计工具箱有silhouette函数直接算没有工具箱时手写也不复杂s zeros(size(X, 1), 1); for i 1:size(X, 1) ci label(i); inSame X(label ci, :); a mean(sqrt(sum((inSame - X(i, :)).^2, 2))); bestB inf; for kk setdiff(1:K0, ci) inOther X(label kk, :); b mean(sqrt(sum((inOther - X(i, :)).^2, 2))); bestB min(bestB, b); end s(i) (bestB - a) / max(a, bestB); end meanS mean(s);逻辑说明a(i)是样本i到同簇其他样本的平均距离b(i)是到最近的其他簇所有样本的平均距离s(i) (b-a) / max(a,b)。全局轮廓系数是全部样本s(i)的均值。多个K都算一遍轮廓均值最高者胜出。注意这段代码是O(N²)复杂度上万样本会明显卡顿。实战中更快的做法是直接调silhouette函数它对大规模计算做了优化或者随机抽几百个样本算近似的轮廓均值趋势通常足以做判断。4.3 初始化策略K-means和它的“翻车”与兜底随机初始化在K较大或数据带噪声时容易陷入局部最优常见做法是用K-means初始化先随机取一个中心之后每个新中心按距离平方的概率被选中离已有中心越远越可能成为下一个中心。这个策略保证初始中心散布足够开大幅降低局部最优概率。但K-means也有翻车的时候最典型的是第一个中心恰好落在离群点上后续中心选择会被带偏。数据量小的时候可以用最大最小距离初始化替代第一个中心随机取后面每个中心都选“到已有中心最小距离最大”的样本点这种方式更稳定但复杂度高。数据量大时直接跑内置kmeans并设Replicates10让MATLAB从多个起点里挑SSE最低的一次作为输出。这两种兜底方案我都在实战里验证过能把不确定性压到可接受范围。参数建议值说明maxIter100到200手写代码够用大多数数据20次内收敛tol1e-4到1e-3特征方差大时放宽阈值Replicates10内置函数参数多起点挑最优SSEDistancesqeuclidean默认值适合连续特征5. k-means落地的避坑清单从数据到结果的5个血泪教训5.1 坑1没做标准化量纲把聚类结果带偏了现象收入和年龄放在一起跑聚类结果所有分群都按收入切开了年龄这个变量几乎没有区分度。原因k-means底层用欧氏距离数值范围大的特征在距离计算里占据绝对权重。收入是万元级年龄是十岁级年龄再怎么波动也影响不了距离排序。解决聚类前先做z-score标准化X_norm (X - mean(X, 1)) ./ std(X, 1);也可以做min-max归一化两者差别不大。标准化的代价是聚类结果的解释性变弱因为簇中心不在原始量纲下。我的处理方式是标准化跑聚类再把最终簇中心乘回标准差、加回均值用真实量纲做业务沟通。这一步做好分群结果才有人愿意信。5.2 坑2空簇直接报错代码要提前兜底现象K设成5跑到某次迭代时一个簇里没有任何样本mean()计算出NaN随后所有距离计算全变NaN聚类图出现大块空白。原因随机初始化时几个中心扎堆或者数据分布不均匀某个中心离所有样本都太远一个样本都没抢到。解决手写代码必须加空簇保护我的my_kmeans里用的是“随机取一个样本当新中心”。这个做法简单可靠迭代不会中断。使用内置kmeans时设置EmptyActionsingleton行为是遇到空簇就从离该中心最远的样本里选一个补进去。加上这个兜底之后空簇问题基本不会再来烦你。5.3 坑3K值拍脑袋选结果解释全靠脑补现象A同学把K定成4跑完分群对着图讲故事别人问为什么是4答“感觉比较合适”。图做得再花哨没有选择依据结论就没有说服力。原因K的选择没有客观指标支撑输出结果只能靠解释者自己圆场这在交付场景里是致命的。解决K选择至少做肘部法则加轮廓系数双重验证跑K2到K8留一张SSE曲线图和一张轮廓系数表。交付给别人的时候这个判断过程比聚类图本身更能证明方案的严谨性。如果统计指标和业务预期冲突明确把冲突点写出来而不是藏着。聚类本身就是探索性分析把不确定性讲清楚反而是加分项。5.4 坑4高维数据直接跑距离矩阵失真现象80维特征的高维数据直接跑k-means聚类结果跟随机划分差不多簇中心之间没有明显可解释差异。原因高维空间下欧氏距离趋同所有点之间的距离都差不多聚类结构被淹没在噪声里。这正是常说的“维度灾难”。解决先降维再聚类。用PCA保留90%方差或者用流形学习降到2到3维再跑k-means处理后分群通常立刻有结构。另外要意识到k-means本身假设簇是凸形高维数据经过非线性降维后簇形状可能变了这时反而更适合用DBSCAN。别在k-means上死磕换个算法不算丢人。5.5 坑5docx里的旧代码在新版MATLAB上直接挂现象从文档里复制代码到R2023b报错信息指向kmeans函数的参数不认或者提示invalid character看起来莫名其妙。原因文档里的代码来自旧版本用的是全角字符或旧式options结构体写法。doc排版时把中文引号、括号转成了全角MATLAB编辑器对这些字符的处理很严格直接抛错。解决先全文搜索替换把全角引号、括号统一换成半角再把option结构体写法改成字符串参数链[idx, C] kmeans(X, K, Distance, sqeuclidean, Replicates, 10);这种写法新旧版本都认参数短、直观排错也容易。如果只有旧文档可用另一个保险办法是先在命令窗口跑一段最小复现代码确认环境没问题再把完整脚本放进来别一次性粘贴几百行出了错根本定位不到一行。6. 进阶用稳定性指标给k-means结果“验算”再把它接进后续建模k-means结果多变怎么证明这次跑出来的分组不是随机运气我的习惯是多跑几次看分组结果的重复度跑两遍聚类把两次的簇标签对齐后算Fowlkes-Mallows指数。如果两次结果几乎一致说明数据里确实有稳定的分群结构如果错位严重先回检数据质量和K值选择别急着交付。function fm stableFowlkesMallows(X, K0) L1 my_kmeans(X, K0, 100, 1e-4); L2 my_kmeans(X, K0, 100, 1e-4); n numel(L1); tp 0; fp 0; fn_ 0; for i 1:n-1 for j i1:n same1 (L1(i) L1(j)); same2 (L2(i) L2(j)); if same1 same2 tp tp 1; elseif same1 ~same2 fp fp 1; elseif ~same1 same2 fn_ fn_ 1; end end end fm tp / sqrt((tp fp) * (tp fn_)); end逻辑说明Fowlkes-Mallows指数比较两次划分中所有样本对是否被分到同一簇值越接近1说明划分越稳定。0.9以上基本可以放心0.8到0.9算可用低于0.8就要想想初始化、标准化和K值是不是有问题。注意这个函数是O(N²)上千样本就卡得难受实战请先抽样到几百个点再验算。真正的业务接法不是把聚类结果当独立模块丢出去而是把簇标签作为一列新特征喂给后续监督模型。比如做客户流失预测时把聚类标签加进逻辑回归的特征矩阵经常能观察到模型AUC小幅提升因为聚类把用户高维行为模式压缩成了一维分群信息。训练模型时把聚类标签一起放进特征集然后观察重要度排序能直观看到分群信息对业务目标的贡献。最后说一个我自己的习惯跑分群之前先记录随机种子准备好数据说明聚类结束后把每个簇的特征均值拉出来对齐到业务维度逐个解释。早先有一次我做分群指标全是从脚本里蹦出来的结果业务方完全看不懂后来改成“每簇取特征均值用业务语言标注每个簇的人群特征”方案才真正落地。聚类算法本身并不难难的是让结果经得起推敲。希望帮到你。本文还有配套的精品资源点击获取