MATLAB实现K-means聚类:从原理到可视化全流程解析 简介K均值聚类是数据分析和机器学习领域最常用的无监督学习算法之一这份以MATLAB为开发语言的代码包完整演示了多维矩阵聚类与可视化输出的实现过程。压缩包为rar格式共7个文件其中5个为.m脚本文件2个为.mat数据文件整体仅62KB轻盈小巧。脚本按K均值标准流程组织覆盖初始化质心、计算距离分配簇、更新类簇中心以及迭代运行等核心环节两个数据文件则提供了多维矩阵测试样本方便读者直接加载运行并观察聚类变化。目前已有2270人学习浏览特别适合刚接触聚类算法的MATLAB用户、数据挖掘课程学习者及需要快速产出聚类演示的项目开发者。通过研读这套代码不仅可以掌握K均值在多重特征数据上的聚类执行细节还能学会借助二维或三维散点图对类簇分布进行可视化评估为后续数据分析或科研实验积累了可直接复用的MATLAB脚本。1. K-means 聚类在 MATLAB 里的落地形态这套代码给你省掉了哪几步如果你手头有一份多维矩阵要分群第一反应多半是直接调kmeans()函数但真把数据喂进去之后你会发现结果不稳定、不知道 K 取几、画出来的图也说不清类簇边界在哪。这套以 MATLAB 为开发语言的 K-means 聚类资源把从数据加载、质心初始化、迭代分配到可视化展示的全流程拆成了一个个独立文件你既能一键跑通main.m看到完整链路也能单步调试findClosestCentroids.m和computeCentroids.m把聚类过程看透。对于正在做多维矩阵聚类、又想把聚类过程可视化展示出来的从业者来说这套代码比单纯调内置函数多走了一步——它把黑匣子打开了。2. 多维矩阵聚类的核心原理初始化、迭代与数据集怎么配合2.1 K 值选多少、质心怎么初始化先搞懂这两个黑匣子K-means 这个算法本身不难四步就能讲完初始化质心、分配数据点、更新质心、重复迭代直到收敛。但真正让新手翻车的往往是两个前置问题K 取多少质心初始位置怎么定。K 值决定了最终分成几个簇这不是算法自己算出来的得你根据业务场景去定。常见做法是先跑一遍肘部法则看 loss 曲线的拐点再结合业务判断。而质心初始化代码里用的是kMeansInitCentroids.m逻辑是从数据集中随机选取 K 个样本作为初始质心这比全零初始化或者随机生成坐标点要稳得多——因为初始质心如果落在数据分布之外迭代时会花很多轮次才能拉回数据密集区甚至可能直接收敛到一个空簇。function centroids kMeansInitCentroids(X, K) % 从数据集 X 中随机选取 K 个样本作为初始质心 % X: m x n 的多维矩阵m 个样本n 个特征 % K: 聚类数 % 随机打乱样本顺序 randidx randperm(size(X, 1)); % 取前 K 个作为初始质心 centroids X(randidx(1:K), :); end这段代码的巧妙之处在于它完全避开了随机数生成坐标点可能带来的边界问题。randperm返回一个从 1 到 m 的随机排列取前 K 个索引对应的样本就是 K 个实际存在的样本点。这样做的好处是质心一开始就落在数据空间内后续迭代的收敛速度会快很多。参数K换算到这里就是你要分的类簇数如果发现聚类结果里某个簇的样本数少得离谱优先怀疑 K 是不是给大了。2.2 拆开 runkMeans.m迭代主循环的每一步在做什么整个聚类的迭代过程被封装在runkMeans.m里这个文件是理解 K-means 从零实现的关键。它做的事情说起来很朴素循环里交替调用findClosestCentroids.m和computeCentroids.m每次循环做一次分配、一次更新直到质心不再变化或者达到最大迭代次数。function [centroids, idx] runkMeans(X, initial_centroids, max_iters) % X: m x n 数据矩阵 % initial_centroids: K x n 初始质心矩阵 % max_iters: 最大迭代次数 centroids initial_centroids; % 记录历史质心位置用于可视化收敛过程 history zeros(max_iters * K, n); for i 1:max_iters % 分配步骤每个样本找最近的质心 idx findClosestCentroids(X, centroids); % 更新步骤每个簇重新计算均值 centroids computeCentroids(X, idx, K); % 记录当前质心位置到历史数组 for j 1:K history((i-1)*K j, :) centroids(j, :); end end end这里的idx是一个 m 维列向量每个元素是 1 到 K 之间的整数表示对应样本归属的簇编号。history变量是个容易被忽略但很有用的设计——它把所有迭代轮次的质心都存了下来后面画质心移动轨迹全靠它。实际使用中max_iters我一般设 50 就够超过 100 轮还没收敛的数据集要么是 K 值有问题要么是数据归一化没做好再跑下去纯属浪费算力。2.3 football_test.mat 与 karate_test.mat两份数据集能验证什么这套资源里附带了两份测试数据football_test.mat和karate_test.mat。从命名就能猜到一份是足球相关数据一份是空手道俱乐部数据。这两份数据都是多维矩阵格式对应不同的聚类难度。用它们验证代码的优势在于数据规模适中跑起来快而且类簇结构相对清晰适合作为基准测试。你改完参数后先在这两份数据上跑一遍如果聚类结果和预期差别很大那问题大概率出在代码逻辑上而不是数据本身。提示拿到代码先不要急着换成自己的业务数据先在football_test.mat上跑通确认可视化输出符合预期再换数据能省下大量排查时间。3. 跑通整套流程从 main.m 一键运行到手动实现分配与更新3.1 用 main.m 一键跑通先看结果再抠细节main.m是整个资源的入口脚本它把数据加载、初始化、迭代和可视化串成了一条完整的链路。第一次上手时直接在 MATLAB 里打开main.m改一下数据文件名和 K 值然后运行整个脚本就能看到聚类结果的可视化输出。这种方式特别适合先建立整体感知——你知道最终输出长什么样再回头去看每个函数是怎么工作的理解成本会低很多。%% 加载数据 data load(football_test.mat); X data.football_test; % 假设 mat 文件里存的是 football_test 变量 %% 设置聚类参数 K 3; % 聚类数根据实际场景调整 max_iters 50; % 最大迭代次数 %% 初始化质心并运行 K-means initial_centroids kMeansInitCentroids(X, K); [centroids, idx] runkMeans(X, initial_centroids, max_iters); %% 可视化聚类结果 figure; scatter(X(:,1), X(:,2), 40, idx, filled); hold on; plot(centroids(:,1), centroids(:,2), kx, MarkerSize, 15, LineWidth, 3); title(K-means 聚类结果可视化);这一段里面有个容易踩的坑MATLAB 里load一个 mat 文件后变量名不一定是football_test你得用whos先看下工作区里加载进了什么变量名或者用fieldnames(data)查看结构体的字段。如果变量名对不上直接data.football_test会报错。scatter的第三个参数是点的大小第四个参数传idxMATLAB 会自动按不同值分配不同颜色filled表示填充实心点。质心用黑色叉号叠加在图上方便看最终质心落在什么位置。3.2 手写一次分配与更新findClosestCentroids.m 和 computeCentroids.m 的配合如果你不想只停留在调包层面这两个函数就是理解 K-means 内部机制的最佳入口。findClosestCentroids.m做的事情是对每个样本计算它到 K 个质心的欧氏距离找到最近的质心并记录索引。computeCentroids.m则是把每个簇内的样本取平均得到新的质心位置。function idx findClosestCentroids(X, centroids) % X: m x n 数据矩阵 % centroids: K x n 质心矩阵 % idx: m x 1 向量记录每个样本的簇归属 K size(centroids, 1); m size(X, 1); idx zeros(m, 1); for i 1:m % 计算第 i 个样本到所有质心的距离 distances sum((X(i, :) - centroids) .^ 2, 2); % 取距离最小的质心索引 [~, min_idx] min(distances); idx(i) min_idx; end end这里使用的距离是欧氏距离的平方因为开根号是单调运算不影响最小值所在的索引但省了一次逐元素的sqrt操作在大样本量时能省不少时间。sum(..., 2)表示对每一行求和结果是一个 K 维列向量表示当前样本到 K 个质心的距离。如果你想把距离度量改成曼哈顿距离或者余弦相似度只需要替换这一行的距离计算方式其余逻辑不用动。function centroids computeCentroids(X, idx, K) % X: m x n 数据矩阵 % idx: m x 1 簇归属向量 % K: 聚类数 % centroids: K x n 更新后的质心矩阵 m size(X, 1); n size(X, 2); centroids zeros(K, n); for k 1:K % 找到属于第 k 簇的所有样本 points X(idx k, :); if ~isempty(points) % 取均值作为新质心 centroids(k, :) mean(points, 1); end end endidx k生成一个逻辑向量X(idx k, :)取出所有属于第 k 簇的样本。有个细节如果某个簇一个样本都没有points会是空矩阵mean会返回 NaN所以在更新前加了一个非空判断。实际跑的时候如果发现某个质心是 NaN说明出现了空簇这是 K-means 的经典问题后面避坑章节会专门讲处理方法。4. 可视化展示的多维视角把聚类过程与结果都画出来4.1 二维和三维散点图颜色编码看类簇分布可视化是这套资源里另外一半核心价值。数据本身可能是高维的但可视化最终要落到二维或三维平面上。最简单的做法是只取前两列特征画散点图用颜色区分不同簇这在特征维度不多时够用。如果数据维度超过三维就需要先做降维——PCA 是最常用的选择把高维数据投影到二维平面再画散点图能看到更完整的类簇分布。%% 对高维数据先做 PCA 降维到二维再可视化 [coeff, score, ~] pca(X); X_pca score(:, 1:2); % 取前两个主成分 figure; scatter(X_pca(:,1), X_pca(:,2), 40, idx, filled); xlabel(第一主成分); ylabel(第二主成分); title(PCA 降维后 K-means 聚类结果);这里pca函数返回三个输出coeff是主成分系数矩阵score是原始数据在主成分方向上的投影坐标~用来忽略第三个输出即特征值。取score的前两列就是降维到二维的结果。值得注意的一个点是PCA 降维后再画聚类图类簇的视觉分离度不一定会比原始数据前两列更好因为主成分方向是方差最大的方向不一定是最能区分类簇的方向。如果发现降维后类簇重叠严重要多尝试几个主成分组合或者换用 t-SNE 这类非线性降维方法。4.2 质心移动轨迹怎么画观察收敛过程的两种做法静态的聚类结果图只能告诉你最终分成了几簇但看不出迭代过程中质心是怎么移动的。把质心移动轨迹画出来能直观确认算法是否收敛——如果迭代十几轮后质心还在大幅移动说明收敛条件设置有问题。这里用runkMeans.m里存的history变量画轨迹线。%% 画质心移动轨迹 figure; hold on; % 每个质心的迭代路径用不同颜色画线 colorOrder lines(K); for k 1:K % 从 history 中提取第 k 个质心的所有历史位置 track history(k:K:end, :); plot(track(:,1), track(:,2), o-, Color, colorOrder(k, :), LineWidth, 1.5); end scatter(X(:,1), X(:,2), 30, idx, filled, MarkerFaceAlpha, 0.3); plot(centroids(:,1), centroids(:,2), kx, MarkerSize, 15, LineWidth, 3); title(质心移动轨迹与最终聚类结果);画轨迹的核心思路是用history(k:K:end, :)按步长抽取——因为history的存储顺序是第 1 轮所有质心、第 2 轮所有质心所以第 k 个质心的所有位置就是每隔 K 行取一次。MarkerFaceAlpha参数控制散点的透明度设置成 0.3 可以让散点不遮挡质心轨迹线。用这个图能很直观地看到质心从初始位置一步步挪到最终位置的过程如果某个质心的轨迹绕了很大的弯才稳定下来说明初始质心选得不太理想。提示可视化不是终点而是手段。画图时优先保证信息的可读性——类簇颜色要能明显区分质心标记要醒目必要时加上图例说明否则图是画出来了但别人看不懂你想表达什么。5. K-means 实战避坑与常见问题排查5.1 聚类结果每次跑都不一样随机初始化导致的局部最优现象同一份数据、同一个 K 值连续跑两次main.m两次的聚类结果和可视化图形明显不同。原因K-means 对初始质心敏感而kMeansInitCentroids.m里的质心选取是随机的。不同初始质心会收敛到不同的局部最优解这在数据分布复杂时尤其明显。解决最常见的处理是跑多次聚类取最优结果。做法是设置一个循环跑 10 到 20 次 K-means每次记录最终的类内距离总和选最小的一次作为最终结果。不过也要注意如果不同运行结果差异特别大更要检查 K 值选得是否合理——K 值偏小时初始质心的影响会被放大。5.2 维度灾难多维矩阵聚类效果变差的前兆与处理方式现象数据维度从 5 维升到 50 维后聚类结果变得难以解释——每个簇看起来都差不多轮廓系数大幅下降。原因高维空间里样本间欧氏距离趋于均匀分布最近质心与次近质心的距离差变小导致分配结果不稳定。这是标准的维度灾难表现。解决先做特征选择剔除明显无关或高度相关的特征再做 PCA 降维把有效维度压缩到 10 维以内再聚类。我一般会先把相关系数矩阵画出来看一遍删除相关系数超过 0.9 的冗余特征再做 PCA降维后再跑 K-means 的结果通常明显改善。5.3 空簇问题某个质心一个点都没分到怎么办现象迭代过程中某个质心变成 NaN绘制散点图时那个簇没有数据点。原因随机初始化的质心可能落在数据稀疏区域或者 K 值设得比实际类簇数大导致某些簇在分配阶段没有吸引到任何样本computeCentroids.m里取均值时拿到空矩阵。解决最简单的办法是遇到空簇时把该质心重新初始化为数据集中随机的一个样本点然后继续迭代。另一个思路是检查 K 值是否过大——如果你认为数据最多只能分成 5 个自然簇但强行设 K10空簇几乎是必然的。5.4 K 值怎么定肘部法则的实现与误用现象不知道到底选几个簇或者 K 取多少都说得通无法判断。原因K-means 的 K 值需要外部确定算法本身不给出答案。肘部法则是一个常用参考思路画损失函数关于 K 的曲线找拐点。但血泪经验是很多数据集的损失曲线没有明显拐点是一条平滑下降的曲线这时候就需要结合业务语义来判断。解决先跑 K1 到 K10 的聚类记录每次的损失值画出来看趋势。如果拐点不明显我一般会做一次稳定性分析——同一个 K 值跑 20 次看聚类结果的一致性结果越稳定这个 K 越可信。%% 肘部法则实现画损失关于 K 的曲线 for K 1:10 % 多次运行取最优避免随机初始化影响判断 best_loss inf; for trial 1:10 initial_centroids kMeansInitCentroids(X, K); [centroids, idx] runkMeans(X, initial_centroids, 100); loss 0; for k 1:K points X(idx k, :); if ~isempty(points) loss loss sum(sum((points - centroids(k, :)).^2)); end end if loss best_loss best_loss loss; end end ssd(K) best_loss; end plot(1:10, ssd, o-); xlabel(K 值); ylabel(类内距离总和);这段代码里每个 K 值跑 10 次取最优损失是为了消除随机初始化对肘部法则判读的干扰。ssd(K)存的是第 K 个 K 值对应的最优类内距离总和。把曲线画出来之后看哪个位置斜率从陡变缓那个位置就是候选的 K 值。注意这条曲线不一定是单调下降的偶尔会有小波动属于正常现象。6. 把 K-means 用在自己的数据上数据预处理、参数调优与结果验证6.1 自己的数据怎么塞进这套代码矩阵格式、归一化与变量名检查把football_test.mat换成你自己的数据看起来是件小事但这里有个很容易被忽略的坑你自己的数据十有八九没有做归一化。如果各个特征的量纲差异很大比如一列是年龄0-100另一列是收入0-100000距离计算会被数值范围大的特征主导聚类结果基本等价于只用那一列在做分类。我一般会在跑 K-means 之前加一步标准化用zscore函数把每列数据转换成均值为 0、标准差为 1 的分布。另外导入数据后一定要用whos查看变量名然后把代码里的字段名改成你自己的变量名这一步不做好后面所有代码都跑不动。%% 数据预处理示例 % 假设你自己的数据存在 data.xlsx 或 data.csv 里 raw readmatrix(myData.csv); X zscore(raw); % 标准化消除量纲影响 %% 检查聚类结果质量计算轮廓系数 sil silhouette(X, idx); mean_sil mean(sil); disp([平均轮廓系数: , num2str(mean_sil)]);zscore按列做标准化每列变成均值为 0、标准差为 1这样所有特征在距离计算时权重相同。silhouette函数返回每个样本的轮廓系数范围在 -1 到 1 之间越接近 1 表示该样本的簇归属越合理。平均轮廓系数大于 0.3 算基本可用大于 0.5 就不错。如果算出来接近 0 甚至为负值要反思的特征选择和 K 值选取。6.2 聚类质量评估不只是看可视化图形顺不顺眼可视化图形只能给出直观印象真正的质量评估要看量化指标。除了轮廓系数还可以看每个簇的样本量分布——理想情况下各簇样本量应该相对均匀如果某个簇占了 90% 的样本而其他簇只有零星几个说明 K 值或初始化有问题。另外质心最终位置也可以当参考如果某个质心落在数据分布的明显边缘那个簇很可能是个边缘簇需要回头看看原始数据里那部分样本的特性。我自己有一个固定习惯每次跑完 K-means至少看三个输出——轮廓系数、各簇样本量分布、质心位置与数据分布的重叠情况三个都对得上才敢说这轮聚类结果可信。6.3 关于可视化展示的一个补充做法如果你的聚类结果最终要放进报告或者给非技术背景的同事看建议在散点图基础上加一个簇的凸包边界。做法是用convhull函数计算每个簇的凸包顶点然后画多边形轮廓。这个图比单纯散点更有信息量边界一眼可见。从那以后我每次跑完 K-means都会把原始数据的散点图、质心轨迹图、带凸包边界的聚类图各保存一份作为交付材料的附录。这样做的好处是后续回看结果时任一环节都能对上号不用重新跑一遍代码去回忆当时参数设了啥。希望帮到你。本文还有配套的精品资源点击获取