尧图精选

K-Means聚类算法详解:从数学原理到MATLAB实现与调参实战

🕒 发布时间:2026/9/10 10:01:06 📁 来源:尧图网络
简介面向机器学习初学者和MATLAB用户的K均值聚类算法实现资源提供完整可运行的MATLAB代码与配套示例数据集帮助理解聚类核心概念、算法迭代步骤及聚类结果可视化。K均值算法是一种经典的无监督聚类方法通过随机选取初始聚类中心计算每个样本到各中心的距离并划分至最近簇随后重新计算各簇均值中心反复迭代直至收敛该资源完整展示了这一过程适用于算法教学、实验验证及入门级数据挖掘任务。压缩包内共两个文件包含一个MATLAB主程序和一个表格数据集整体体积不到一百KB轻量易用。已有七百余人学习作者承诺运行失败或报错可免费解决降低使用门槛。借助该资源学习者可快速掌握K均值算法的基本原理、距离度量方式与聚类中心更新逻辑并通过可视化输出直观观察不同聚类数下的分组效果同时还可根据自身数据替换示例数据集进行验证加深对参数选择和聚类过程的理解文件组织清晰便于按步骤调试运行适合作为课程作业、毕业设计或算法研究的参考模板。1. K-Means不是“调个包”就完事的聚类算法K均值聚类K-Means可能是工业界和课堂上出现频率最高的聚类算法但大多数人对它的理解停留在“给个K调个kmeans()画个散点图”这一步。真正用它解决过实际问题的人都清楚这个算法的坑远比它的代码量要多初始中心怎么选、K到底取几、特征量纲不统一怎么办、聚出来是一坨叠在一起的簇怎么解释——这些才是K-Means从“能跑”到“能用”的分水岭。这篇文用MATLAB把K-Means的完整链路走一遍从算法本身的数学原理讲到手写实现、可视化验证再到用内置函数处理高维数据和调参陷阱最后落在一个可以直接抄的聚类评估脚本上。适合正在做数据分析、图像分割或特征工程的人尤其是想在MATLAB里把聚类结果真正讲清楚而不只是交一张彩点图的工程师。2. K-Means聚类的数学原理与MATLAB实现路径2.1 从“找质心”到“收敛”K-Means到底在优化什么K-Means做的事情在数学上非常干净给定n个样本和预设的簇数K算法要把样本分成K个集合使得每个样本到它所属簇的中心质心的欧氏距离平方和最小。这个目标函数写作J Σ(i1..K) Σ(每个样本x∈C_i) ‖x - μ_i‖²其中μ_i是第i个簇的质心C_i是第i个簇的样本集合。整个聚类过程就是交替做两步E步把每个样本分配给距离最近的质心M步重新计算每个簇的质心为簇内样本的均值。重复这两步直到质心不再移动或移动量低于阈值。代价函数J是单调递减的所以K-Means必然收敛但它收敛到的是局部最小值不是全局最优。这意味着不同的初始质心选择会得到不同的聚类结果这就是为什么MATLAB里kmeans函数要反复跑多次、选代价最小的结果。理解这个原理对后面调参数非常关键——你加的Replicates和Start参数本质上都是在跟局部最优解对抗。2.2 为什么选MATLAB而不选Python用MATLAB做K-Means有三个明显的理由。第一MATLAB的kmeans函数是Statistics and Machine Learning Toolbox里的成熟实现内置了多种初始化策略和距离度量工程稳定性比手写循环强得多。第二MATLAB的可视化能力是交互式的gscatter、silhouette这些函数几行代码就能输出出版级质量的聚类图这在数据报告场景中很占优势。第三MATLAB的矩阵运算天然适合K-Means这种反复计算距离的操作当数据维度高、样本量大的时候写好的代码不需要额外调优就能跑得动。当然如果用Python的Scikit-learn做K-Means也很顺手但今天这篇文的场景定位在MATLAB生态里。% 用MATLAB内置kmeans跑一次基础聚类 rng(42); % 固定随机种子保证结果可复现 X [randn(100,2)*0.5 [2 2]; randn(100,2)*0.5 [-2 -2]; randn(100,2)*0.5 [0 -2]]; % 构造3个高斯簇 [idx, C] kmeans(X, 3); gscatter(X(:,1), X(:,2), idx, rgb, o, 8); hold on; plot(C(:,1), C(:,2), kx, MarkerSize, 15, LineWidth, 2); legend(Cluster 1, Cluster 2, Cluster 3, Centroids);这段代码先固定随机种子让结果可复现然后生成三个高斯分布的点簇。kmeans(X, 3)返回每个样本的簇标签idx和最终的质心坐标Cgscatter按簇着色画散点图质心用黑色叉号叠加。注意rng(42)这行不是可选项——K-Means的结果受随机性影响很大不固定种子你每次跑出来的图都可能不一样复现实验和写报告的时候会非常痛苦。2.3 手写一个K-Means循环理解MATLAB向量化写法虽然直接调kmeans很方便但我还是建议你至少手写一次K-Means不是为了重复造轮子而是为了看清每一行计算到底在做什么。手写版本能让你彻底理解距离计算的维度匹配逻辑也能在将来需要定制距离函数或加约束条件时知道从哪里下手。function [idx, C, J_history] my_kmeans(X, K, max_iters) % 手写K-Means欧氏距离随机初始化质心 [n, d] size(X); rng(1); % 随机选K个样本作为初始质心 init_idx randperm(n, K); C X(init_idx, :); J_history zeros(max_iters, 1); for iter 1:max_iters % 计算每个样本到每个质心的欧氏距离得到n×K矩阵 distances zeros(n, K); for k 1:K diff X - C(k, :); distances(:, k) sum(diff.^2, 2); end % 分配每个样本到最近质心的簇 [~, idx] min(distances, [], 2); % 更新质心为簇内样本均值 for k 1:K if any(idx k) C(k, :) mean(X(idx k, :), 1); end end % 记录当前代价函数值 J_history(iter) sum(min(distances, [], 2)); end endmin(distances, [], 2)的2表示按行取最小返回每行最小距离和对应的列索引idx这个索引就是样本的簇归属。质心更新时mean(X(idxk,:),1)按列求均值保证维度从1×d正确返回。代价函数记录的是每个样本到它所属质心距离的平方和这个曲线后期应该趋于平稳如果还在大幅下降说明迭代次数不够。手写版在理解上有一个很重要的细节初始质心我用的randperm随机采样这是最简单的初始化方式但很容易导致收敛到差的局部最优所以实际场景中除非你在教学否则应当用MATLAB内置的k-means策略即kmeans里默认启用的初始化它能显著提升聚类稳定性。3. 聚类可视化从二维散点到高维投影的MATLAB实践3.1 直接用gscatter画出聚类结果参数和细节拿到聚类标签后最直观的可视化手段就是gscatter。它比scatter更适合聚类场景因为可以自动按分组分配颜色和标记符号省去手动循环plot的麻烦。% 对真实数据集做聚类并可视化 load fisheriris; X meas(:, 3:4); % 取花瓣长度和宽度两个特征方便平面可视化 [idx, C] kmeans(X, 3, Replicates, 10); figure(Position, [100 100 800 500]); gscatter(X(:,1), X(:,2), idx, [0.2 0.6 0.9; 0.9 0.4 0.3; 0.5 0.7 0.2], .*x, 10); hold on; plot(C(:,1), C(:,2), kp, MarkerSize, 18, MarkerFaceColor, y); title(K-Means聚类结果Fisher Iris子集); xlabel(花瓣长度 (cm)); ylabel(花瓣宽度 (cm)); legend(簇1, 簇2, 簇3, 质心, Location, best); grid on;Replicates设为10意思是从10组不同的随机初始质心出发跑10次最后返回代价最小的结果。这是MATLAB里对抗局部最优最简单有效的方法代价只是多几倍计算时间对中小数据集可以放心开到10到20。颜色用了三组RGB 1×3向量来精确控制簇的配色比默认配色更适合打印和报告。.*x表示三组数据的标记形状分别用点、星号和叉号这样即使打印成黑白图也能区分簇。质心用黄色填充的五角星突出显示让图的视觉焦点锁定在簇中心。如果你发现图上有两个簇重叠得很厉害先别急着换算法认真检查一下是不是特征没做标准化——这是K-Means使用中排第一的错误来源。3.2 高维数据的可视化方案PCA降维与silhouette图当特征超过3维时直接把原始数据画出来是不可能的标准做法是先用主成分分析PCA降维到二维或三维把降维后的坐标画出来簇标签仍然用K-Means在原始高维空间里算出来的标签不要降维后再聚类否则会丢掉大量信息。% 高维数据先PCA投影再可视化 load ionosphere; % 34维数据集 X Z; % 或你自己的高维特征矩阵 [idx, C_high] kmeans(X, 4, Replicates, 15, MaxIter, 500); % PCA降维到前两个主成分 [coeff, score, ~, ~, explained] pca(X); X_pca score(:, 1:2); % 注意质心也要变换到PCA空间再画 C_pca (C_high - mean(X)) * coeff(:, 1:2); figure(Position, [100 100 900 400]); subplot(1, 2, 1); gscatter(X_pca(:,1), X_pca(:,2), idx, , o, 6); title(sprintf(PCA投影前两维贡献率 %.1f%%, sum(explained(1:2)))); xlabel(PC1); ylabel(PC2); subplot(1, 2, 2); silhouette(X, idx); title(轮廓系数图);PCA的投影矩阵coeff是原始空间到主成分空间的线性变换所以质心坐标必须减去数据均值后乘上coeff的前两列才能正确画在PCA图上。explained向量给出了每个主成分解释的方差比例这个数字很重要如果前两维加起来的贡献率不到60%那这张PCA图只能算参考不能完全代表真实聚类结构。silhouette图是K-Means聚类质量验证的标准工具每个样本的轮廓值越接近1说明它离自己所属簇中心近、离隔壁簇远聚类效果好出现大量负值或靠近0的值说明有样本被分错了簇或者K取大了。轮廓系数会在第5章进一步展开。3.3 可视化不同K值的聚类效果evalclusters一步到位选K是K-Means最让人头疼的问题。手动循环K2到8去画图、对比每次还要重新写一遍绘图逻辑效率很低。MATLAB的evalclusters函数把这件事压缩成了一个调用它会针对你给定的K范围分别聚类然后计算指定的评估指标输出一个可交互的图。% 用evalclusters自动评估K2到8的聚类质量 rng(42); load fisheriris; X meas; % 用全部4个特征 % Calinski-Harabasz指标簇间离散度与簇内离散度的比值越大越好 eva evalclusters(X, kmeans, CalinskiHarabasz, KList, 2:8); figure; plot(eva); title(Calinski-Harabasz 指标随K值变化); xlabel(K); ylabel(CH值);evalclusters的第二个参数传kmeans表示使用MATLAB内置K-Means也可以传一个函数句柄来自定义聚类方法。第三个参数是评估指标可选CalinskiHarabasz、Silhouette、DaviesBouldin和gap。CH值和轮廓系数越大越好DaviesBouldin越小越好Gap statistic适合和随机均匀分布做对比来判断最佳K计算最慢但理论上最严谨。实际项目中我一般组合看两个指标——Silhouette和CalinskiHarabasz如果它们给出的最佳K不一致取靠中间的那个值并回到业务上确认每个簇是否有解释意义这一点比任何统计指标都优先。4. 参数调优与常见陷阱K值、初始化、距离度量与数据预处理4.1Replicates、MaxIter和OptionsMATLAB K-Means的关键参数表MATLAB的kmeans函数参数不算多但每个都会对结果和性能产生实质影响值得做的做法是把常用参数整理成一张速查表贴在自己的脚本模板头部。参数作用推荐设置说明Replicates从多组初始质心出发跑多次选J最小的10~20数据量大时可以降到3~5避免计算时间爆炸MaxIter单次迭代的最大轮数默认100高维数据可调至500收敛前达到上限会输出告警需要调大Start初始质心选择策略plus默认k-means也可以传矩阵手动指定初始质心Distance距离度量方式sqeuclidean最常用文本数据可换cosine数值数据别用cityblockEmptyAction出现空簇时怎么办singleton默认error报错方便调试drop会减少输出簇数Start参数值得多说两句。k-means初始化的核心逻辑是第一个质心随机选后续每个质心选离已有质心远的样本点权重正比于距离平方。这样选出的初始点散布在数据空间的不同区域能显著降低收敛到坏局部最优的概率。如果你处理的数据有明显的时间顺序比如时间序列片段聚类可以考虑Start传一个自己设计的初始质心矩阵用等间隔采样的数据点这样能保证质心覆盖整个时间范围。4.2 特征标准化量纲不统一的后果比你想的严重K-Means依赖欧氏距离而欧氏距离对每个特征的尺度极其敏感。假设你有两个特征一个是身高范围150~190一个是年收入范围3万~100万那么K-Means计算距离时身高的差异会被收入的差异完全淹没——本质上聚类结果只由收入决定。这个问题在MATLAB中处理起来非常轻量但经常被忽略。% 标准化后再聚类 X_raw [身高数据, 收入数据]; % 示意两列量纲完全不同的特征 X_std zscore(X_raw); % zscore按列做零均值单位方差标准化 [idx, C] kmeans(X_std, 3, Replicates, 10);zscore逐列计算均值和标准差(x - μ) / σ把所有特征拉到同一个尺度。标准化后质心坐标的含义变了不再是原始特征空间的中心而是标准化空间里的中心。如果你想反算回原始尺度用C_raw C .* std(X_raw) mean(X_raw)逐列还原。还有一种情况需要留意如果你用的是cosine距离做文本向量聚类通常不需要标准化因为余弦距离本身已经对向量的模长做了归一再做zscore反而会破坏稀疏结构。4.3 手把手教你处理K-Means的4个典型失败场景场景一聚类结果每次跑都不一样。先检查有没有固定随机种子。MATLAB的rng在脚本开头设置一次即可注意kmeans内部还会用自己的随机数流。如果固定了种子仍然每次结果不同检查代码里是否有别的随机函数比如数据加载时用了randperm做洗牌。在工程脚本里我会在kmeans调用前显示执行rng(42)这样无论脚本被谁跑、在哪个版本里跑结果都一样。场景二出现空簇。当K设得比实际簇数多或者数据分布极度不均衡时某个簇可能没有任何样本被分配进来。默认的EmptyAction是singleton它会随机选一个离当前所有质心最远的样本作为新质心继续保持K个簇。空簇本身就是一个信号说明你的K太大了看轮廓图或者直接减少K是更合理的做法。场景三迭代次数达到上限但没有完全收敛。MATLAB会输出告警“Failed to converge”。优先调大MaxIter到500甚至1000如果调大后仍然收敛失败检查数据里是否包含异常值——一个离群点可能让某个质心反复震荡。预处理时用isoutlier函数检测一下或者改用cityblock距离配合中位数相关的方法增强鲁棒性。场景四所有样本几乎被分到一两个大簇里其他簇只有零星几个点。这是数据本身的问题大概率特征分布严重偏态或有大量离群点。先对特征做对数变换log1p、然后用zscore标准化再做K-Means。还有一个做法是把数据缩放到[0,1]区间X_norm (X - min(X)) ./ (max(X) - min(X))这种非线性变换对偏态数据的拉伸效果比zscore更好。4.4 距离度量不是摆设什么时候放弃欧氏距离欧氏距离假设每个维度独立且尺度可比这在很多现实中不成立。最典型的替代场景是文本数据。比如用TF-IDF向量表示文档时向量是高维稀疏的欧氏距离会被文档长度差异主导两个内容相似但长度差很远的文档会被分到不同簇而cosine距离只看方向不看模长更适合这类数据。% 余弦距离聚类的正确写法 [idx, C] kmeans(X_tfidf, 5, Distance, cosine, Replicates, 10);用余弦距离时kmeans内部会把所有向量按行做L2归一化再执行欧氏距离计算因为归一化后的欧氏距离和余弦距离是单调等价的。这带来一个数学上优雅的后果质心不再是原始空间的中心而是归一化空间里的方向中心你可能得到一些负坐标值。字符特征比如颜色名称的文本特征则应该考虑用hamming距离它计算的是两个向量对应位置不同的比例。时间序列数据如果用K-Means通常配合DTW距离效果更好但MATLAB内置的kmeans不接受自定义距离函数句柄这类场景要写自己的K-Means循环。5. 聚类评估与模型的业务落地轮廓系数、肘部法则与验证脚本5.1 用轮廓系数判断聚类是否“真的分开了”轮廓系数的数学定义是对每个样本i计算它到同簇所有其他样本的平均距离a(i)再计算它到最近的其他簇所有样本的平均距离b(i)轮廓值s(i) (b(i) - a(i)) / max(a(i), b(i))。s(i)的范围从-1到1越接近1越好。全局轮廓系数是全体样本s(i)的均值。这个指标的厉害之处在于它不依赖任何先验标签纯粹从几何结构上判断分离度。在实际项目中我会把平均轮廓值0.25作为“需要重新考虑聚类方案”的下限0.5以上说明有清晰的簇结构0.7以上说明分离非常干净。低于0.25时一般先检查数据预处理再调K如果还是不理想才会考虑换成DBSCAN或层次聚类AGNES来对比效果。在MATLAB里完整验证一个聚类结果至少要跑三步计算轮廓系数、画轮廓图、对比不同K的平均轮廓值。下面这组代码是一个可以直接复制使用的验证模块% 聚类验证脚本轮廓系数 对比不同K rng(42); load fisheriris; X zscore(meas); % 标准化 K_range 2:8; avg_sil zeros(size(K_range)); for i 1:length(K_range) K K_range(i); idx kmeans(X, K, Replicates, 15, MaxIter, 500); s silhouette(X, idx); avg_sil(i) mean(s); end % 打印对比表 fprintf(K\t平均轮廓系数\n); for i 1:length(K_range) fprintf(%d\t%.4f\n, K_range(i), avg_sil(i)); end % 自动选择最佳K [best_val, best_idx] max(avg_sil); best_K K_range(best_idx); fprintf(推荐K %d平均轮廓系数 %.4f\n, best_K, best_val);把所有K值的平均轮廓系数以表格式输出然后选最大的。这比肉眼在图画里找肘点可靠也比evalclusters的输出更适合嵌入批处理流程。注意silhouette函数虽然自带画图功能但如果你只需要数值加上缩放因子就可以只返回不画图。轮廓系数只能在K≥2时有定义K1没有意义。5.2 结合肘部法则WCSS曲线的拐点到底怎么读肘部法则是另一种常见的K值选择方法看的是簇内误差平方和Within-Cluster Sum of Squares简称WCSSMATLAB里对应sumd的输出随K变化的曲线。K增大时WCSS必然减小但减小速度会在某个K值后突然变慢这个“拐点”就是肘。原理很直观K小于真实簇数时多分一个簇能显著降低误差K超过真实簇数后多分一个簇只能把已有的簇切得更碎误差下降变缓。% 计算不同K的WCSS并画肘部曲线 rng(42); X zscore(meas); K_range 1:10; wcss zeros(size(K_range)); for i 1:length(K_range) [~, ~, sumd] kmeans(X, K_range(i), Replicates, 10); wcss(i) sum(sumd); % sumd每个簇的簇内点到质心距离平方和 end figure; plot(K_range, wcss, b-o, LineWidth, 2); grid on; title(肘部法则WCSS与K的关系); xlabel(K); ylabel(WCSS);kmeans的第三个返回值sumd是一个K×1的向量保存了每个簇内部所有样本到质心距离平方的和sum(sumd)就是全局WCSS。读图的时候有一个常见的误区把WCSS曲线整体看成一个向下的弧线机械地找“角度最大的点”。实际数据往往没有那个理想肘点曲线可能平滑下降这时候用轮廓系数来定K远比肉眼找拐点可靠。我的习惯是两种方法都跑出来——轮廓系数给一个确定推荐值肘部曲线用来给出候选范围然后回到业务场景里去验证簇的语义。聚类是探索性工具最终要回答的是“这些簇分别代表什么用户群体/故障模式/基因表达类型”统计指标只能帮你缩小范围不能替你下业务结论。5.3 验证聚类结果稳定性的重采样技巧K-Means的一个务实问题是给你的数据加上少量噪声或删掉几个样本聚类结果是否还能保持一致。这个问题在实际部署中非常关键比如用户分群模型今天跑出来3群明天变成4群运营就无法基于这个模型做事。稳定性验证的常见做法是Bootstrap重采样——从原始数据中有放回地抽取和原数据同样大小的样本集重复聚类然后比较每次结果的相似度。% 用Bootstrap检查聚类结果稳定性 rng(42); X zscore(meas); K 3; n_bootstrap 50; cluster_agreement zeros(n_bootstrap, 1); % 先用全量数据得到基准簇标签 [ref_idx, ~] kmeans(X, K, Replicates, 15); for b 1:n_bootstrap % 有放回抽样 sample_idx randsample(size(X, 1), size(X, 1), true); X_boot X(sample_idx, :); [boot_idx, ~] kmeans(X_boot, K, Replicates, 10); % 用兰德指数Adjusted Rand Index比较两次聚类的相似度 % 但这里样本是重采样的需要把簇标签映射回原始样本 cluster_agreement(b) mean(ref_idx(sample_idx) boot_idx); end histogram(cluster_agreement, 10); xlabel(簇标签一致率); ylabel(频次); title(sprintf(Bootstrap聚类稳定性平均一致率 %.2f, mean(cluster_agreement)));由于Bootstrap抽样是有放回的同一个原始样本可能被抽中多次也可能不被抽中。这段代码用ref_idx(sample_idx)把基准聚类标签按抽样索引重新映射再与bootstrap聚类标签比较mean(相等)就得到一致率。如果平均一致率低于0.8说明聚类结构脆弱——换一批数据结果就不一样了这时候要么调整预处理方法要么换更稳的聚类算法层次聚类或DBSCAN而不是急着接受当前模型。5.4 一个可以直接改的完整聚类评估模板把前面几节的技术点组合成一个模板脚本面对任何一个表格式数据集你只需要替换X的加载部分就能完成全套验证。这个模板我用了很久组里其他人接手项目也能看明白每一步在做什么。%% 通用K-Means聚类分析模板 % 输入X 为 n×d 数据矩阵 % 输出最佳K值、聚类标签、可视化结果、稳定性报告 rng(42); % 全局随机种子 K_range 2:8; % 备选K值范围 n_replicates 15; % 每组K重复次数 % 1. 数据预处理 X_std zscore(X); % 2. 用轮廓系数扫描K avg_sil zeros(size(K_range)); for i 1:length(K_range) K K_range(i); idx kmeans(X_std, K, Replicates, n_replicates, MaxIter, 500); avg_sil(i) mean(silhouette(X_std, idx)); end % 3. 选最佳K并输出结果 [best_val, best_idx] max(avg_sil); best_K K_range(best_idx); fprintf(最佳K%d轮廓系数%.4f\n, best_K, best_val); % 4. 最终聚类 [final_idx, C, sumd] kmeans(X_std, best_K, Replicates, n_replicates, MaxIter, 500); % 5. 可视化PCA投影 轮廓图PCA保留原始数据空间 [coeff, score] pca(X); % 注意这里pca在标准化前的数据或标准化后数据上计算 figure(Position,[100 100 900 400]); subplot(1,2,1); gscatter(score(:,1), score(:,2), final_idx, [], o, 6); title(PCA投影视角); subplot(1,2,2); silhouette(X_std, final_idx); title([轮廓图K num2str(best_K) ]);模板的输入输出边界很明确只需要把X替换成实际数据其他部分基本不用动。注意PCA投影这边我用的是原始X而非标准化后的X_std来降维原因是标准化会改变方差结构如果所有特征方差都被拉成1主成分的方向就和原始数据的方差贡献率无关了。但这并不是绝对的如果你更看重聚类视角的分离效果在X_std上做PCA也可以差别不大。真正重要的是PCA投影图只用于展示不用于验证验证靠的是轮廓系数和稳定性检验这两个定量指标。小技巧是如果你发现轮廓系数在某个K值接近但不出众同时业务上对簇的数量有硬性要求比如运营团队只要3个客群那就不必纠结统计指标直接用业务要求的K然后用轮廓系数审视是否还能接受——实践中的聚类很少是纯数据问题它往往是数据和业务约束的折中。最后记得聚类脚本的随机种子写在文件顶部注释里万一结果需要复现一行参数改动就能找回当时的结果。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →