简介:一份帮助Matlab用户快速上手K-means聚类算法的代码详解文档,面向需要进行数据分组、模式识别或课程实验的初学者与科研人员。文档从欧氏距离、曼哈顿距离等常见度量方式切入,逐步展示初始化聚类中心、分配数据点、更新质心的完整迭代流程,并基于一个包含7个二维数据点的示例数据集,演示kmeans函数的具体调用方法,包括K值设定、idx与centroids的输出解读,以及利用scatter绘制的聚类着色与质心标记可视化。配套内容以单个docx文件提供,大小仅15KB,便于阅读和批注,代码部分可直接复制到Matlab中运行,注释和解析可作为工程实现的前置参考。资源还总结了K值难以预先确定、对异常值敏感、初始化随机影响结果等常见局限,并介绍了K-means++、二分K-means、核K-means等改进策略,帮助读者在掌握基础方法的同时拓宽进阶思路。目前已有442人学习该资源,适合Matlab环境下聚类分析任务和算法原理对照学习。
1. K-means 聚类 MATLAB 实现:从算法到可视化,这份代码直接能跑
如果你正在做数据分析、图像分割或者用户分群,K-means 很可能是你接触的第一个聚类算法,也大概率是你第一个想用 MATLAB 跑出可视化结果的对象。这份资源给的恰恰是一个可以零改动运行的 MATLAB 实现:给定二维数据点、指定簇数 K,调用kmeans函数完成聚类,然后用scatter把不同簇名的数据点用不同颜色展示出来,质心用黑色交叉标记标注。对于刚入门聚类算法的同学,这份代码最大的价值不是算法本身,而是一条完整的「数据 → 聚类 → 可视化 → 解读」链路。对于已经在做研究的开发者,它也是一个很好的 baseline:改数据、调 K、换距离度量、加初始化策略,都能在这份代码基础上快速迭代。接下来我会逐步拆解这段代码的每一行、每个参数以及背后的算法动机,并给出可直接替换到你自己数据上的改进版本。
2. 核心概念拆解:K-means 在 MATLAB 中到底做了什么
2.1 迭代式聚类:四步走的核心逻辑
K-means 算法做的事情一句话就能讲清:把 N 个数据点划分到 K 个簇中,让每个点离它所属簇的质心最近。但这个「最近」不是一次算完的,而是一个不断修正的过程。
算法核心步骤如下:
- 从数据中随机选 K 个点作为初始质心
- 计算每个数据点到 K 个质心的距离(默认欧氏距离),把点分配到距离最近的质心所在簇
- 对每个簇,重新计算簇内所有点的均值,将该均值作为新质心
- 重复第 2、3 步,直到质心位置不再变化,或达到最大迭代次数
MATLAB 的内置kmeans函数把这个迭代过程封装好了。你不需要手写循环,只需要传数据、告诉它 K 等于几,它会自己迭代并返回每个点的簇归属和最终质心。
2.2 距离度量:不仅仅是欧氏距离
MATLABkmeans默认使用欧氏距离(平方欧氏距离),这一点很多初学者会忽略。实际上kmeans函数支持多种距离度量,通过Distance参数控制:
sqeuclidean:平方欧氏距离,默认值,适用于大多数连续数值型数据cityblock:曼哈顿距离,对异常值更鲁棒一些,适合高维稀疏数据cosine:余弦距离,常用于文本向量或方向敏感的数据correlation:相关系数距离,适合基因表达谱这类需要形状相似性的数据hamming:汉明距离,针对二进制数据
选择哪种距离度量,直接决定聚类结果是否符合你的业务语义。比如做文本聚类用余弦距离就比欧氏距离合理得多,因为文本向量的模长差异往往不携带语义信息。
2.3 初始化策略与 K-means++
原生 K-means 的随机初始化是一把双刃剑 — 运气好时几轮迭代就收敛,运气差时会陷入局部最优解,聚类结果完全不可用。MATLAB 的kmeans函数在 R2014a 之后默认使用kmeans++初始化策略,这算是一个不用你操心的隐形福利。
kmeans++的核心思想是:初始化时尽量让质心彼此远离。具体做法是第一个质心随机选取,后续每个质心都以与已有质心距离成正比的概率从剩余点中选取。这样初始质心的分布比纯随机更均匀,收敛速度和聚类质量都有明显提升。
如果你想看随机初始化的效果对比,可以显式设置Replicates参数,比如kmeans(data, K, 'Replicates', 5)表示用不同随机种子跑 5 次,返回误差最小的一次结果。这个技巧在数据量不大时非常实用,几乎是免费的质量提升。
3. 循序渐进实现:从随机数据到完整聚类代码
3.1 生成模拟数据:二维各向异性数据与标准化预处理
原始代码里的data只有 7 个二维点,用来验证算法能跑通没问题,但很难看出聚类的真实效果。我一般会在本地先生成一批「有重叠、有噪声、各簇密度不同」的二维数据,这样聚类结果才有讨论价值。
在生成数据之后,绝大多数场景下需要先做标准化处理。K-means 对特征尺度极其敏感——如果特征 A 的取值范围是 0 到 100,特征 B 是 0 到 1,那么距离计算会被特征 A 完全支配,聚类结果几乎等同于只按特征 A 划分。标准化的常用做法是zscore:
% 生成三个簇的模拟数据,每个簇 50 个点 rng(42); % 固定随机种子,保证结果可复现 cluster1 = randn(50, 2) * 0.5 + [2, 2]; % 围绕 (2,2) 的高斯簇 cluster2 = randn(50, 2) * 0.8 + [6, 5]; % 围绕 (6,5) 的高斯簇,方差更大 cluster3 = randn(50, 2) * 0.3 + [8, 1]; % 围绕 (8,1) 的紧凑簇 data = [cluster1; cluster2; cluster3]; % 标准化:让每个特征的均值为 0,标准差为 1 data_norm = zscore(data); % 执行 K-means 聚类,K=3 K = 3; [idx, centroids] = kmeans(data_norm, K, 'Replicates', 5); % 绘制聚类结果 figure; hold on; gscatter(data(:, 1), data(:, 2), idx, 'rgb', 'o', 8); plot(centroids(:, 1), centroids(:, 2), 'kx', 'MarkerSize', 15, 'LineWidth', 3); legend('Cluster 1', 'Cluster 2', 'Cluster 3', 'Centroids'); hold off;这段代码里有几个值得注意的点:rng(42)固定随机种子,保证每次运行生成的数据一致,这在调试和比较不同参数时至关重要;zscore对每列做标准化处理;Replicates, 5让算法跑 5 次取最优。还有一个容易被忽略的细节——可视化时我用gscatter(data(:, 1), data(:, 2), idx)在原始数据坐标上绘图,而不是用标准化后的data_norm绘图,这样坐标轴尺度和簇的实际位置更直观。质心坐标同理,kmeans返回的质心是标准化空间中的坐标,直接画在原始空间里坐标会失真,但作为簇中心的相对位置展示,差异也在可接受范围内。
如果想让质心准确映射回原始空间,一个做法是在标准化之前保存原始数据的均值和标准差,然后把质心乘回去:
% 将质心从标准化空间映射回原始空间 mu = mean(data); sigma = std(data); centroids_orig = centroids .* sigma + mu;3.2 距离计算与质心更新:手写版 K-means 加深理解
MATLAB 的kmeans是黑匣子,调用方便,但如果你想深入了解算法内部机制,或者需要自定义距离度量、做算法对比实验,手写一版迭代循环反而更灵活。下面这个手写版本不依赖统计工具箱,也能跑通整个流程:
% 手写 K-means 核心迭代 function [idx, centroids] = my_kmeans(data, K, max_iter) % 输入: data - NxD 矩阵,K - 簇数,max_iter - 最大迭代次数 % 输出: idx - Nx1 簇索引,centroids - KxD 质心矩阵 [N, D] = size(data); % 1. 初始化:从数据中随机选 K 个点作为初始质心 rng(42); init_idx = randperm(N, K); centroids = data(init_idx, :); % 2. 迭代更新 for iter = 1:max_iter % 分配步骤:计算每个点到所有质心的欧氏距离 dist = zeros(N, K); for k = 1:K diff = data - centroids(k, :); dist(:, k) = sum(diff.^2, 2); % 平方欧氏距离 end [~, idx] = min(dist, [], 2); % 取最近质心索引 % 更新步骤:计算每个簇的均值作为新质心 new_centroids = zeros(K, D); for k = 1:K points_in_cluster = data(idx == k, :); if ~isempty(points_in_cluster) new_centroids(k, :) = mean(points_in_cluster); else % 空簇处理:保留原质心或重新随机初始化 new_centroids(k, :) = data(randi(N), :); end end % 检查收敛:质心不再变化则提前退出 if norm(new_centroids - centroids) < 1e-6 break; end centroids = new_centroids; end end这个手写版本包含三个细节值得展开。第一,距离计算我用sum(diff.^2, 2)而不是sqrt(sum(diff.^2, 2)),因为平方欧氏距离和欧氏距离的单调性一致,求最近质心时开根号是多余的计算。第二,空簇处理是个很容易翻车的地方——当某个簇没有分配到任何点时,mean会返回 NaN,导致后续迭代崩溃。这里的做法是重新随机选一个点作为质心。第三,收敛判断用质心变化的范数与阈值比较,阈值1e-6是一个比较合理的默认值,过小会导致算法在收敛附近多做无效迭代,过大会错失精度提升的机会。
3.3 可视化结果:散点图细节与质心标记
MATLAB 的kmeans函数在 2014a 之后默认采用 k-means++ 初始化策略,这一步省去了手动实现初始质心的麻烦。如果没有特殊需求,我建议直接使用内置函数而非手写版本。
可视化方面,用一个scatter函数展示所有数据点,用相同的颜色体系标注质心,是最直观的方式:
% 可视化聚类结果 figure; hold on; colors = lines(K); % 生成 K 种区分度高的颜色 for k = 1:K cluster_points = data(idx == k, :); scatter(cluster_points(:, 1), cluster_points(:, 2), 40, colors(k, :), 'filled'); end scatter(centroids(:, 1), centroids(:, 2), 120, 'kx', 'LineWidth', 2); title('K-means 聚类结果 (K=3)'); xlabel('X 坐标'); ylabel('Y 坐标'); grid on; hold off;这里比原始代码多了一个细节:用lines(K)生成颜色矩阵,循环绘制每个簇的点。这样做的优势是当一个簇的点数远多于另一个簇时,legend仍然能正确对应,而原始代码中的legend('Cluster 1', 'Cluster 2')在 K 较大时写起来非常冗长;此外,grid on能让散点图的刻度更清晰,方便从坐标上读聚类簇的分布范围。
质心标记用kx(黑色交叉)加大MarkerSize,在视觉上比圆点更有区分度。如果你觉得黑色交叉不够醒目,也可以用不同形状的标记(比如菱形d、星号*),这在簇数较多时更实用。
4. 避坑指南:K-means 实践中的高频踩坑记录
4.1 坑一:K 值选错导致聚类结果完全不可解释
现象:无论怎么调整初始化策略和迭代次数,聚类结果总是「看起来不对劲」——比如把明显连续分布的数据硬切成了几块,或者两个簇之间界限模糊,质心落在一片空白区域。
原因:K 值是在聚类之前就要指定的,但在拿到真实数据时,你通常并不知道数据到底该分成几类。这是 K-means 算法最核心的局限,不属于数学推导问题,而属于模型选择问题。如果 K 选得比真实簇数小,算法会强制合并不相关的簇;如果 K 选得比真实簇数大,数据集会被无意义地切碎。
解决:常用方法是肘部法则(Elbow Method)加上业务经验判断。肘部法则就是跑多个 K 值(比如 1 到 10),记录每个 K 对应的簇内误差平方和(SSE),画出折线图,找到 SSE 下降速度骤减的「肘点」作为 K 的候选值。在 MATLAB 中实现如下:
% 使用肘部法则选择 K 值 K_max = 10; sse = zeros(K_max, 1); for K = 1:K_max [~, ~, sumd] = kmeans(data_norm, K, 'Replicates', 5); sse(K) = sum(sumd); % sumd 是每个点到所属质心的距离之和 end figure; plot(1:K_max, sse, 'bo-', 'LineWidth', 2); xlabel('K 值'); ylabel('SSE 簇内误差平方和'); title('肘部法则:选择最优 K');需要注意的是,肘部法则在数据簇间界清晰时效果很好,簇有重叠或数据维度较高时折线的「拐点」往往并不明显。这时候我会结合silhouette函数看轮廓系数,或者直接根据业务需求定 K——比如做用户分群,业务上只需要 4 类运营策略,那就用 K=4,不必纠结统计上是否最优。
4.2 坑二:质心坐标与原始数据不在同一坐标系
现象:对数据做zscore标准化后聚类,画图时centroids坐标明显偏离真实数据点的聚集区域,离群甚至出现负坐标。
原因:kmeans返回的质心是在你传入数据所在的空间中计算的。如果你传入的是标准化后的数据,质心自然也是标准化空间中的坐标。直接把这些坐标画在原始数据散点图上,坐标系都对不上,视觉效果自然不对。
解决:绘图时要么把质心从标准化空间映射回原始空间(用之前保存的mu和sigma做线性变换),要么直接对原始数据聚类但不做标准化。我习惯的做法是:用标准化数据聚类,但在绘图时将质心反标准化,这样聚类质量不损失、可视化也准确。如果数据各特征量纲本来接近,也可以偷懒直接用原始数据聚类,但要注意这只是特例而非通则。
4.3 坑三:聚类结果每次运行都不一样
现象:同一个数据集,同一个 K 值,第一次运行和第二次运行得到的聚类结果不同,甚至簇的编号顺序都会变。
原因:kmeans默认的初始化带有随机性,即使使用kmeans++,第一步的质心也是随机选的。不同的初始质心可能导致最终收敛到不同的局部最优解,这是算法本身的特性,不是 bug。另外,rng随机种子没有固定时,每次运行的随机序列也不同。
解决:方案有两层。第一层是通过Replicates参数增加重复次数,让 MATLAB 跑多次、自动返回 SSE 最小的一次结果,这在数据量不大时几乎总能消除随机性对结果的影响。第二层是显式固定随机种子,在脚本开头调用rng(42),保证每次运行严格可复现。我以前做实验时也踩过这个坑,后来所有实验脚本都强制在开头设置随机种子和Replicates,再也没出现过结果对不上的问题。
5. 真实场景改造:从模拟数据到业务数据的完整路径
5.1 数据预处理的取舍:标准化、降维与缺失值
把上面这份代码迁移到真实业务数据上,最容易被忽视的环节是数据预处理。真实数据通常存在几个问题:特征量纲不统一、高维稀疏、含缺失值和异常值。这些问题每一条都会直接影响聚类质量。
先说量纲问题。K-means 的距离计算把每个特征都当作平等维度来对待,但当特征的数值范围差异很大时(比如年龄 0-100、收入 0-100000),距离会被数值范围大的特征支配。这时标准化的价值就体现出来了。zscore是用的最多的方法,但要注意对某些特征分布非常偏斜的数据,zscore并不能解决偏态问题,可以考虑先做log变换再标准化,把长尾特征拉回正常范围。
再说降维。数据维度超过几十维时,直接做 K-means 往往效果会很差,因为高维空间中距离度量变得不再可靠。这时候推荐先做主成分分析:
% 高维数据先降维再聚类 [coeff, score, ~] = pca(data); data_pca = score(:, 1:min(3, size(score, 2))); % 保留前 2-3 个主成分 [idx, centroids] = kmeans(data_pca, K);这里只保留前两个或三个主成分,一方面是为了可视化方便,另一方面是前几个主成分通常已能解释大部分方差。但要注意,降维必然会损失信息,如果 PCA 后累计方差贡献率不足 80%,建议保留更多主成分再做聚类,或者换用不适合降维场景的聚类算法。
5.2 聚类评估:轮廓系数怎么说
选完 K 值、跑完聚类之后,评估聚类质量不只是用肉眼看图。轮廓系数定义为:
对于每个数据点,计算a(该点到同簇其他点的平均距离)和b(该点到最近其他簇所有点的平均距离),轮廓系数等于(b - a) / max(a, b)。取值范围在 -1 到 1 之间,越接近 1 表示聚类越合理,接近 0 表示点在边界上,负值表示可能被分错簇。
MATLAB 的silhouette函数可以直接给出所有点的轮廓系数并画图:
% 计算轮廓系数并可视化 [~, s] = silhouette(data_norm, idx); mean_s = mean(s); fprintf('平均轮廓系数: %.4f\n', mean_s);平均轮廓系数大于 0.5 时,聚类结果通常被认为具有合理的一致性;低于 0.2 时基本没有可信度,需要重新审视 K 值、数据预处理甚至聚类算法本身。我把轮廓系数当作一个体检指标用——每次聚类跑完都先看一眼这个数字,再决定要不要继续往深里调。
5.3 从内置 kmeans 到自定义改进:初始化方法与距离驱动
如果需要跳出 MATLAB 内置kmeans的边界做二次开发,你至少有两条路可以走。
第一条路是直接在kmeans的选项上做文章。比如设置'OnlinePhase', 'on',让算法在批量迭代之后再做一轮逐点调整,有时能把 SSE 再压低一点。或者设置'Options', statset('Display', 'iter')打开迭代日志,观察收敛过程——这在排查「算法是不是已经收敛了但结果还是不理想」时很有用。
第二条路是改算法本身。比如实现 K-means++ 在自己的框架里,或者把距离计算换成马氏距离(Mahalanobis Distance):
% 使用马氏距离替代欧氏距离 [idx, centroids] = kmeans(data_norm, K, 'Distance', 'mahalanobis');马氏距离会减去均值并除以协方差,实际效果相当于先做数据白化再算欧氏距离。当数据各个维度的方差差异很大、且维度间存在相关性时,马氏距离往往能抓到欧氏距离漏掉的结构。不过代价是计算协方差矩阵的开销,数据量上万时就需要评估一下性能是否可接受。
6. 二次开发:把这段代码变成你自己的聚类工具
6.1 封装成函数:输入数据、输出带标签的散点图
你拿到的这段示例代码是脚本形式,直接改进的第一件事就是把它封装成一个可复用的函数。函数的好处很明显:同一套逻辑,换个数据集就能用,不用每次改动脚本里的变量名和数据赋值。
function [idx, centroids, fig] = run_kmeans_analysis(data, K, show_plot) % 功能: 对输入数据执行 K-means 聚类并可视化 % 输入: % data - NxD 数值矩阵,每行是一个样本 % K - 聚类簇数,正整数 % show_plot - 逻辑值,是否绘制聚类结果图 % 输出: % idx - Nx1 簇索引向量 % centroids - KxD 质心矩阵 % fig - 生成的图像句柄,可用于保存 if nargin < 3 show_plot = true; end % 标准化数据 data_norm = zscore(data); % 聚类的核心调用 [idx, centroids] = kmeans(data_norm, K, 'Replicates', 5); if show_plot % 绘图部分 fig = figure('Color', 'w', 'Position', [100, 100, 800, 600]); hold on; gscatter(data(:, 1), data(:, 2), idx); % 质心反标准化回原始坐标 mu = mean(data); sigma = std(data); centroids_orig = centroids .* sigma + mu; scatter(centroids_orig(:, 1), centroids_orig(:, 2), 150, 'k', 'x', 'LineWidth', 3); hold off; else fig = []; end end封装时有几个点值得注意。nargin < 3处理了用户不传show_plot的情况,默认绘制图形,这是 MATLAB 函数式编程里很常见的惯例。质心反标准化的代码在之前拆解可视化时提到过,现在把它变成了正式的功能——不回退到原始坐标,图形解读就容易出偏差。另一个小细节是把figure设置成白色背景'Color', 'w'而不是 MATLAB 默认的灰色,放到论文里会更顺眼。
6.2 批量扫描 K 值:自动输出评估指标表
封装好函数之后,下一步就是把「选 K 值」这个步骤自动化。我用arrayfun配合kmeans,把 K 从 1 到 8 扫一遍,同时记录 SSE、轮廓系数和运行时间,输出一张对比表。
% 批量扫描 K 值并输出评估指标 K_values = 1:8; results = table('Size', [length(K_values), 4], ... 'VariableTypes', {'double', 'double', 'double', 'double'}, ... 'VariableNames', {'K', 'SSE', 'Silhouette', 'Time_s'}); for i = 1:length(K_values) K = K_values(i); tic; [idx, ~, sumd] = kmeans(data_norm, K, 'Replicates', 5); elapsed = toc; s = silhouette(data_norm, idx); results.K(i) = K; results.SSE(i) = sum(sumd); results.Silhouette(i) = mean(s); results.Time_s(i) = elapsed; end % 打印表格查看哪几个 K 值轮廓系数较高 disp(results);跑完这张表,K 值选起来就有客观依据了。实际经验是:轮廓系数最高和 SSE 拐点往往不是同一个 K,这时候优先看重业务解释成本,统计指标作为参考即可。
6.3 把聚类标签接回业务逻辑
聚类本身只是第一步,真正产生价值的是后续的标签应用。现在很多数据团队的做法,是把 K-means 聚类的输出作为特征喂给后续模型,或者直接生成画像。在这个 MATLAB 代码的基础上,你可以把idx导出成 CSV,或者整合进更大的分析框架:
% 将聚类结果与原始数据一同写回文件 result_table = array2table([data, idx], 'VariableNames', ... {'Feature1', 'Feature2', 'Cluster'}); writetable(result_table, 'cluster_output.csv');这段代码执行后得到的是一个多了一列聚类标签的数据文件,你可以对它继续做分组统计、交叉验证或喂给分类模型。从这个角度回看整个项目,这份 MATLAB 代码的价值完全不在那十几行代码本身,而在于它给你提供了一条可复制的研究思路:数据进来,先标准化,再聚类,用轮廓系数评估,把标签吸入下一步分析。我也踩过不少次坑——当时也是直接拿着别人给的代码就跑,懒得理解每一行的含义,结果换了数据集之后结果完全不对。从那以后我每次做聚类实验都强制走一遍「数据了解 → 标准化 → 多次重复聚类 → 轮廓系数验证 → 标签落地」的过程,再也没有只靠一张散点图下过结论。希望这份拆解能帮你少走这些弯路,把算法真正跑成自己的工具。
本文还有配套的精品资源,点击获取