资讯动态

Matlab手写Kmeans聚类:从原理到代码实现与调参全攻略

发布时间:2026/9/11 23:40:21 来源:尧图企业网站定制
用Matlab做Kmeans聚类应该是很多工科生和数据方向同学绕不开的一道坎。不管是课程设计、数学建模还是老板临时丢过来一沓数据让你“分个类看看”Kmeans基本都是第一个跳出来的方案。Matlab作为矩阵运算起家的工具写Kmeans简直像量身定做矩阵化表达比Python循环不知道清爽到哪里去而且自带的可视化能力能让你一眼看到聚类效果调试起来特别顺手。这篇文章我把手写Kmeans的全过程拆开讲清楚从算法原理到Matlab代码从数据标准化到K值选择再附上各种踩坑实录。你不需要有很强的编程基础只要能跑通Matlab基础语法照着我的思路和代码走一遍就既能“调包”又会“造轮子”以后再遇到聚类任务不会心里没底。1. Kmeans为什么适合用Matlab实现——算法思路先理清1.1 聚类到底在解决什么问题聚类说白了就是“物以类聚”在不知道数据标签的情况下把相似的数据点自动归到同一组。注意这里的关键词是“不知道标签”——这是聚类和分类最本质的区别。分类像是拿着标准答案改卷子每个样本必须归到事先定好的类目里聚类则更像是看一堆照片自己分堆分得好不好取决于这些点天然的结构。举一个特别生活化的例子你手里有一堆球颜色和大小各不一样但你事先不知道有几种球只是觉得“这堆球看起来好像可以分成几组”。聚类做的事情就是根据球的颜色、大小这些特征自动把它们分成几堆每一堆内部尽量相似堆与堆之间尽量不同。Kmeans就是这类方法里最经典、最常用、也是最适合入门的一个。在Matlab里做这件事的优势非常明显Matlab的矩阵运算功底深厚Kmeans迭代过程中的距离计算、质心更新本质上都是矩阵运算而且Matlab自带的kmeans函数性能可靠可视化工具如scatter、gscatter、silhouette也都很好用写很少的代码就能看到完整的分析结果。1.2 Kmeans的迭代逻辑与Matlab矩阵化表达的契合Kmeans的算法流程用大白话说只有四步先在数据范围里随机挑K个点当作初始的“聚类中心”把每个数据点分给离它最近的聚类中心形成K个簇重新计算每个簇的平均值也就是质心用它来更新聚类中心重复第2和第3步直到聚类中心不再变化或者达到最大迭代次数。这个逻辑看起来简单但如果让你用嵌套循环去写数据量一大就会慢得怀疑人生。而Matlab的强项恰恰在这里计算每个点到所有聚类中心的距离完全可以一次性算成一个矩阵然后用min函数按行取最小值一次性分区。整个迭代过程不需要传统的for循环逐点处理几十行代码就能写得很优雅。这也是我一直建议“先手写一遍再用内置函数”的原因。手写能让你真正理解每一步在做什么怎么选初始中心、距离怎么算、质心怎么更新、空簇怎么处理。内置函数用起来虽然快但出了问题你往往不知道它内部到底卡在哪一步。自己写一遍之后再用kmeans你会看得懂Options结构体里那些参数到底在干嘛。1.3 为什么我更推荐先用Matlab手写一遍有个很常见的现象很多人毕业设计做聚类直接一行[idx, C] kmeans(X, K)就完事了导师问一句“算法原理是什么”只能背出教科书上的公式。倒不是说不能用内置函数而是完全依赖内置函数会让你缺少对算法的掌控感。我做这个项目的时候是先自己写了一遍纯Matlab的Kmeans大约60行代码把迭代过程全部可视化出来——每一步聚类中心怎么移动、每个点怎么变换颜色一目了然。然后再用内置kmeans做对比验证自己写出来的结果和官方实现是否一致。这个对比过程让我对算法参数的理解上了一个台阶也帮我排除了很多潜在问题。而且手写版本在理解“局部最优”这件事上特别有帮助你会看到同一种数据因为初始中心随机选得不好最后可能收敛到一个明显很差的划分。这是Kmeans算法的固有缺陷不是代码的bug理解了这一点你就明白为什么kmeans函数里要有Replicates参数来多次运行取最优。2. 手写Kmeans之前这些细节必须想清楚2.1 数据准备标准化比很多人想的更重要聚类算法里最容易被忽略的一步就是特征标准化。举个实际的例子如果数据里有“年龄”和“收入”两个特征年龄在20到60之间波动收入可能在5000到50000之间波动。如果不做处理欧氏距离计算出来的结果会被“收入”这个量纲大的特征主导年龄这个维度基本就失去意义了聚类出来的结果可能完全不是你想要的结构。所以数据标准化不是可选项而是聚类前必须认真做的一步。常用的方法有两种Z-score标准化$(x - \mu) / \sigma$让每个特征均值为0、方差为1Min-Max归一化$(x - \min) / (\max - \min)$把数据压缩到[0,1]区间。在Matlab里这两种都很好实现zscore(X)一行搞定Z-score标准化Min-Max则可以用(X - min(X))./range(X)来完成。具体用哪一种取决于你的数据分布。如果数据有比较严重的离群点Min-Max会把正常数据压得很窄这时Z-score更稳如果只是想统一量纲且数据范围比较规整Min-Max更直观。注意标准化时只能用训练数据的统计量不能在聚类之前先看全量数据的分布再决定怎么处理否则会在潜意识里“偷看答案”影响对聚类结果的客观判断。实际操作中我先用zscore(X)把数据标准化再进入聚类流程简单有效。2.2 距离度量欧氏距离是默认但不是唯一选项Kmeans最常用的距离是欧氏距离因为均值更新就是在最小化欧氏距离平方和。但如果你的数据特征性质不一样欧氏距离不一定是最佳选择这时候可以换其他距离度量距离度量适用场景Matlab表示欧氏距离数据各维度量纲统一且分布接近球形pdist2(X, C, euclidean)曼哈顿距离特征维度较多且存在异常值pdist2(X, C, cityblock)余弦相似度文本向量、用户行为向量等高维稀疏数据pdist2(X, C, cosine)相关系数距离表达谱、时间序列等形状相似性分析pdist2(X, C, correlation)需要特别强调的是换距离度量之后“质心”的更新方式也要跟着变。欧氏距离下质心就是每个簇的均值但曼哈顿距离下理论上最优中心是中位数用均值只能算近似余弦距离下更常用的做法是先把向量归一化再用欧氏距离。所以我个人的建议是刚入门就用欧氏距离把主流程跑通后再考虑其他距离。2.3 初始化策略随机初始化与k-means的差别Kmeans的初始化对结果影响非常大这不算什么秘密。纯随机初始化的问题在于如果初始中心靠得太近或者几个中心同时落在同一个大簇里迭代就很容易陷入糟糕的局部最优解最后聚类出来的簇可能严重不平衡。k-means的思路则是让初始中心彼此尽量远。具体做法是第一个中心随机从数据中选然后计算每个数据点到最近现有中心的距离距离越大的点被选为下一个中心的概率越高重复直到选够K个中心。这样选出来的初始中心不会挤在一起迭代时收敛更快结果也更稳定。在Matlab里内置的kmeans默认用的就是k-means策略这也是为什么建议大家优先使用内置函数做正式分析。手写版本里如果你也想实现k-means其实也不复杂第一轮用randperm随机选一个点作为中心后面每轮先算距离再用加权随机抽样挑下一个中心。实操建议确定性复现很重要。手写Kmeans时我习惯在代码开头设置rng(42)保证每次运行都得到相同结果这样调试和写报告都方便。内置kmeans里同样可以在调用前加rng也可以通过Options里的UseParallel控制随机性。2.4 K值怎么定肘部法则和轮廓系数的配合K值应该设多少是聚类任务里最经典的问题。如果你对数据一无所知最常见的做法是看“肘部法则”的图横轴是K的取值纵轴是每个样本到其所属簇中心距离的平方和也就是SSESum of Squared Errors。随着K增大SSE会一直下降但在某个K之后下降速度明显变缓这个“拐点”就是所谓的“肘部”。肘部法则的操作流程很机械对K从1到10分别运行Kmeans记录每次的SSE然后画折线图眼睛找拐点。但说实话很多数据集的肘部并不明显这时候需要辅助轮廓系数Silhouette Coefficient来判断。轮廓系数的思想是衡量一个点与自己所在簇的紧密程度和与最近其他簇的分离程度取值范围在-1到1之间越大越好。Matlab里可以直接用evalclusters函数一把梭它支持多种评价指标比如CalinskiHarabasz、DaviesBouldin、Silhouette自动帮你算不同K下的得分eva evalclusters(X, kmeans, CalinskiHarabasz, KList, 1:10); plot(eva);我实际使用中不会单独迷信某一个指标而是肘部法则、轮廓系数、业务经验三样一起看。比如用户分群任务即使指标说K6最好但业务上只需要3类策略那最后还是会折中到K3或K4。算法给出的是参考决策还是要结合场景。3. Matlab实现Kmeans的完整代码与逐步拆解3.1 生成测试数据并做可视化我先造一份能直观看出聚类效果的二维数据。这里用了三个高斯簇故意让它们有一部分重叠模拟真实场景中那种“边界模糊”的情况方便后面演示不同参数对结果的影响。rng(42); % 生成三个高斯簇 X1 mvnrnd([0, 0], [1.2, 0.3; 0.3, 0.8], 150); X2 mvnrnd([4, 3], [0.8, 0.2; 0.2, 0.6], 150); X3 mvnrnd([-3, 4], [1.0, -0.1; -0.1, 0.9], 100); X [X1; X2; X3]; figure; scatter(X(:,1), X(:,2), 20, filled, MarkerFaceAlpha, 0.6); title(原始数据分布); xlabel(特征1); ylabel(特征2); axis equal; grid on;运行这段代码你看到的散点图大致能看出三堆点但两两之间有轻微粘连。这正是合适的数据形态——如果分得太开Kmeans闭着眼睛都能分对如果完全重叠什么算法都不好使。生成数据后别忘了做标准化。二维演示数据我故意让两个维度的方差差不多但真实场景里量纲差异极大所以建议养成良好的习惯上来先标准化X_std zscore(X);大家可以看到zscore一步就能解决量纲问题非常省事。3.2 手写一个Kmeans主函数这是我个人比较推荐的一份手写Kmeans实现没有用任何机器学习工具箱只用基础Matlab函数。function [idx, C, sumD] myKmeans(X, K, maxIter) % 手写Kmeans聚类 % 输入 % X : n-by-d 数据矩阵每行一个样本 % K : 聚类数 % maxIter: 最大迭代次数默认100 % 输出 % idx : n-by-1 每个样本所属簇编号 % C : K-by-d 聚类中心 % sumD: K-by-1 每个簇内样本到中心的距离平方和 if nargin 3 maxIter 100; end n size(X, 1); % k-means 初始化 rng(42, twister); C zeros(K, size(X, 2)); C(1, :) X(randi(n), :); D pdist2(X, C(1, :), squaredeuclidean); for k 2:K minDist min(D, [], 2); prob minDist / sum(minDist); cumProb cumsum(prob); r rand(); idx find(cumProb r, 1, first); C(k, :) X(idx, :); Dk pdist2(X, C(k, :), squaredeuclidean); D [D, Dk]; end % 迭代更新 idx zeros(n, 1); for iter 1:maxIter % 计算每个点到所有中心的距离 D pdist2(X, C, squaredeuclidean); % 分配样本到最近的簇 [minDist, newIdx] min(D, [], 2); % 更新聚类中心 for k 1:K if any(newIdx k) C(k, :) mean(X(newIdx k, :), 1); else % 空簇处理重新随机指定一个样本作为中心 C(k, :) X(randi(n), :); end end % 收敛判断 if isequal(newIdx, idx) idx newIdx; break; else idx newIdx; end end % 计算每个簇的SSE sumD zeros(K, 1); for k 1:K if any(idx k) sumD(k) sum(sum((X(idx k, :) - C(k, :)).^2, 2)); end end end这段代码里有两个地方需要重点解释。第一是k-means初始化。用D pdist2(X, C(1, :), squaredeuclidean)先算第一个中心到所有点的距离然后每选一个新中心就把当前所有已选中心到各点的最小距离作为权重做加权随机采样。这样选出来的中心分布均匀能有效规避随机初始化带来的局部最优问题。第二是空簇处理如果某个簇在分配完之后一个样本都没有就把这个中心重设为随机样本点然后继续迭代。如果不处理mean函数会返回NaN整条计算链就崩了。3.3 调用Matlab自带的kmeans函数做对比手写版本跑通之后再来看Matlab自带kmeans的写法。K 3; [idx_ml, C_ml, sumD_ml] kmeans(X_std, K, ... Distance, sqeuclidean, ... Replicates, 10, ... MaxIter, 300, ... Display, final);Replicates参数是这个函数比大多数自定义实现更“稳”的地方它会把整个Kmeans算法从随机初始化开始重复跑10次最后返回误差最小的那一次结果。因为Kmeans每次迭代结果受到初始化影响单跑一次可能踩到局部最优多跑几次再择优效果要好得多在数据量大时我一般设到20甚至更多。Display参数建议设成final这样能实时看到每次复制的迭代次数对判断数据收敛情况很有帮助。如果不动这个参数默认不输出任何运行信息出了问题很难定位。3.4 结果可视化绘制聚类中心与决策边界聚类跑完可视化是重中之重。我通常分三步画图先画原始数据按类别着色再画聚类中心位置最后画决策边界。散点图部分figure; gscatter(X_std(:,1), X_std(:,2), idx_ml); hold on; plot(C_ml(:,1), C_ml(:,2), kx, MarkerSize, 15, LineWidth, 2); legend(簇1, 簇2, 簇3, 聚类中心); title(Kmeans聚类结果K3); hold off;gscatter是Matlab里做分组散点图的利器会自动按组别分配不同颜色和图例比手动scatter写循环省事得多。聚类中心我用黑色叉号叠加标记这样一眼就能看到每个簇的代表点在哪里。决策边界画起来也不难思路是在坐标范围内生成密集网格点对每个网格点做一次聚类分配判断它属于哪个簇然后画等值线[x1Grid, x2Grid] meshgrid(linspace(min(X_std(:,1))-0.5, max(X_std(:,1))0.5, 200), ... linspace(min(X_std(:,2))-0.5, max(X_std(:,2))0.5, 200)); XGrid [x1Grid(:), x2Grid(:)]; idxGrid kmeans_assign(XGrid, C_ml); % 用最近中心分配不再迭代 figure; gscatter(XGrid(:,1), XGrid(:,2), idxGrid, [0.8 0.2 0.2; 0.2 0.8 0.2; 0.2 0.2 0.8], .); hold on; gscatter(X_std(:,1), X_std(:,2), idx_ml); plot(C_ml(:,1), C_ml(:,2), kx, MarkerSize, 15, LineWidth, 2); hold off;这里的kmeans_assign是我自己写的一个小函数就一行min(pdist2(X, C, squaredeuclidean), [], 2)。因为决策边界只需要判断网格点离哪个中心最近即可不需要重新迭代。注意网格图层的透明度要低一点否则原始数据点会被盖住。4. 常见问题与排查技巧实录4.1 聚类结果每次跑都不一样怎么办这是Kmeans新手最容易踩的坑。同一个数据集今天跑出来簇1是红点明天跑出来簇1变成了蓝点甚至簇的划分整体都变了就以为代码写错了。实际上这是Kmeans的随机初始化导致的正常现象。Kmeans的优化目标是非凸的从不同初始点出发很可能收敛到不同局部最小值。解决思路有几个层级最基础的做法固定随机种子rng(0)保证结果可复现更专业的做法用Replicates参数多次运行取最优更高阶的做法改用k-means初始化并配合MaxIter调大。我实测过一个1500个样本、10个特征的数据集只跑一次和用20次Replicates跑出来的SSE相差15%左右而且前者有时候会明显出现一个“胖子簇”和一个“瘦子簇”的不均衡划分。如果项目中聚类结果要写进报告或者影响后续决策多次运行取最优几乎是必须的。4.2 迭代次数太多迟迟不收敛是怎么回事Kmeans的收敛性在理论上是有保证的每轮迭代SSE都下降但因为数据量、初始中心和距离度量的关系实际运行中有些数据要跑几百轮才能达到严格意义上的中心不变。遇到这种情况不要一味调大MaxIter要先判断是不是初始化的问题。我通常的做法是量化前后两轮质心变化量如果变化量已经小于一个很小的阈值比如1e-6就认为“质量上已经收敛了”没必要死等中心完全不变。在你的手写代码里可以把收敛判断从isequal(newIdx, idx)改成delta sum(sqrt(sum((C_new - C_old).^2, 2))); if delta 1e-6 break; end这样既保留了收敛判断逻辑又避免了迭代步数被“差一点点”拖住。另外还需要检查数据标准化有没有做如果某个特征方差特别大距离计算会被放大导致收敛路径特别曲折。4.3 出现空簇怎么处理空簇就是迭代过程中某个聚类中心身边一个样本都没有。新手往往在这里卡半天以为是代码逻辑问题其实这是Kmeans实现中必须处理的一个分支。产生空簇的原因很典型初始化中心选得不好或者数据分布极度不平衡——某个簇的样本数量特别少中心又被其他簇的样本“抢”走了。处理空簇的策略有三种简单粗暴把空簇中心重设为随机样本点继续迭代更有导向性把空簇中心设置为距离现有中心最远的样本点这样下一次迭代能“抢”到样本更平滑为每个簇引入一个最小样本数约束但这会破坏标准Kmeans的迭代框架。我手写的代码里用的是第一种因为它实现最简单而且配合Replicates多次运行通常就能筛掉差的结果。但如果你用内置kmeans它内部其实有更复杂的空簇处理策略这也体现在它的稳定性上。4.4 多特征数据聚类后如何衡量效果二维数据可以画图看效果但真实任务往往有十个、二十个特征这时候无法可视化必须靠量化指标评价聚类好坏。我最常用的三个指标SSE簇内误差平方和越小说明簇内越紧凑但这个指标会随K增加单调下降不能用来跨K比较轮廓系数Silhouette综合考虑簇内紧凑度和簇间分离度数值越接近1越好Calinski-Harabasz指数衡量簇间方差与簇内方差的比值越大越好。Matlab里用silhouette(X_std, idx_ml)一行就能画出每个样本的轮廓图特别直观。如果大部分样本的轮廓值接近1说明聚类效果好如果出现大量负值说明很多样本被分错了簇这时候要回头检查K选择或者数据预处理是否有问题。5. 从“能跑”到“能用”使用场景和扩展建议5.1 Kmeans在实际业务中的几个典型应用学完Kmeans很多人会问这玩意在公司里到底能干啥。我分享一下自己遇到过和使用过的几个真实场景。一是用户画像与分群。面向用户的产品几乎都会做分群Kmeans可以根据用户的活跃度、消费金额、使用时长等特征把用户分成高价值、中价值、低价值等不同群体然后针对性做运营策略。实际上很多大厂用户运营体系里都有用Kmeans或改造的Kmeans即便不用最终模型也用来做前期的探索性分析。二是图像颜色量化。一张彩色图片动辄几十万像素为每个像素都存储精确颜色需要很多空间。用Kmeans把像素颜色聚成16类或32类再用聚类中心颜色代表每个像素就能把图片压缩不小同时肉眼几乎看不出差别。Matlab里读入图像把像素RGB值整理成n-by-3矩阵跑一次Kmeans再映射回颜色几行代码就能演示。三是异常检测辅助手段。先把正常数据聚成几个簇再判断新样本离所有簇中心的距离距离特别大的就标为异常。这个方法虽然不如专业异常检测算法那么精细但在没有标注数据的情况下是一个简单好用的baseline。四是文档主题聚类。把每篇文章表示成词频向量做TF-IDF处理后用余弦距离聚类就能把相似主题的文章聚到一起。我在做舆情分析的时候试过这个方案虽然不能和LDA主题模型比语义挖掘深度但胜在简单直接、可解释性强。5.2 与层次聚类、DBSCAN对比什么时候别用KmeansKmeans虽然经典但不是所有聚类问题都适合它。理解它的局限选对算法是数据工作者成熟的表现。Kmeans最大的假设是簇的形状近似凸的、大小尽量均匀。如果数据是月牙形、环形或者各簇密度差异极大Kmeans的效果就很差。这时候可以考虑DBSCAN基于密度的聚类它能把任意形状的簇找出来还能自动识别离群点不需要预先指定K。代价是它对密度阈值参数敏感参数不好调。层次聚类则适合那些需要“聚类的层次结构”的场景比如生物分类学、基因表达谱分析。它可以画出树状图dendrogram让你看到哪些样本先合并、哪些后合并不需要事先指定K可以看树状图再决定切分位置。缺点是计算复杂度高数据量过万后速度感人不过Python里做层次聚类的库也挺成熟。我整理的对比表格方便你做选型算法是否要指定K是否适合非凸簇是否自带异常点识别大数据量表现Kmeans是否否很好O(n)线性层次聚类否是否较差O(n^2)以上DBSCAN否是是中等Matlab里clusterdata可以做最简单的层次聚类dbscan函数是R2019a之后引入的用起来也很方便。我这里就不展开代码了重点是要建立“选算法先看数据形态”的意识。5.3 后续可以往哪个方向扩展Kmeans这个项目做完之后扩展方向其实非常丰富选一两个做深就会收获很大。一个方向是做高维数据上的改进。标准Kmeans在高维空间里受“维度灾难”影响严重距离趋于均匀聚类困难。针对文本数据可以配合潜在语义分析LSA降维后再聚类针对图像数据可以先提取特征再做聚类这些都是可以深挖的路径。另一个方向是模糊聚类。FCM模糊C均值允许一个样本按隶属度归属于多个簇而不是硬性划分到某一类。这在很多实际场景中更有意义。比如用户可能既是内容消费者又是内容生产者硬分进某一个类就损失了信息。Matlab里可以用fcm函数模糊逻辑工具箱直接做也是Kmeans的上位扩展学起来不费劲。Kmeans本身还能和很多算法结合PCA降维Kmeans可视化、Kmeans做特征离散化、Kmeans做半监督学习的伪标签生成……这些都是“会跑一个聚类”之后的进阶路线。我个人觉得把Kmeans吃透是学聚类算法性价比最高的一步因为很多变体都是它的思想衍生出来的。最后再分享一个小技巧不管你是手写Kmeans还是调内置函数聚类之前一定要先把数据分布画一画。哪怕特征再多先做一次PCA降维到二维看看大致的形态再决定K怎么选、用什么距离、要不要标准化都比闷头跑代码靠谱得多。我见过太多人拿到数据就开跑Kmeans跑出来结果不好看又不知道问题出在哪其实就是跳过了“先看图”这一步。做数据分析尤其是聚类分析永远让眼睛和脑子走在代码前面。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价