MATLAB实战Kmeans聚类:从算法原理到调参与图像分割应用

📅 2026/8/27 12:09:26
MATLAB实战Kmeans聚类:从算法原理到调参与图像分割应用
1. 项目概述从数据分堆到模式发现如果你手头有一堆数据比如几百个客户的消费记录或者一堆图片的像素值想看看它们能不能自然地分成几个“小团体”这时候Kmeans算法就是你工具箱里最趁手的那把“瑞士军刀”。它不是什么高深莫测的黑科技核心思想简单到可以用一个生活场景来比喻假设你有一大袋混合的彩色玻璃珠红、绿、蓝你的任务是不用看颜色标签仅凭珠子本身的颜色把它们分成三堆。你会怎么做一个很自然的做法是先随便指定三个珠子作为“中心点”比如一个偏红的、一个偏绿的、一个偏蓝的然后把袋子里剩下的每个珠子都跟这三个中心点比一比颜色看它跟谁最像距离最近就把它归到那一堆里去。等所有珠子都分完堆你再重新计算每一堆珠子的平均颜色把这个新的平均颜色作为这一堆新的“中心点”。接着再用新的中心点去重新给所有珠子分堆……如此反复直到中心点不再明显移动分堆结果稳定下来。Kmeans干的就是这个“自动分堆”的活儿它在数据挖掘、图像分割、客户细分、异常检测等场景中应用极广。今天我就结合自己多次在数学建模竞赛和实际项目中应用Kmeans的经验不仅带你用MATLAB手把手实现这个算法更要把算法里那些容易踩坑的细节和调参心得掰开揉碎了讲清楚。2. Kmeans算法核心原理拆解不只是“算距离”很多人对Kmeans的理解停留在“计算距离、找中心、再分配”的循环上这没错但要想用好它必须深入理解其数学本质和几个关键假设。2.1 算法目标最小化“类内”差异Kmeans算法的终极目标是让同一个簇类内的数据点尽可能相似不同簇之间的数据点尽可能不同。用数学语言说就是要最小化所有数据点到其所属簇中心的距离平方和。这个指标叫做误差平方和Sum of Squared Errors, SSE也叫“畸变Distortion”。其公式为SSE Σ(i1 to k) Σ(x in Ci) ||x - μi||²其中k是簇的个数Ci是第i个簇的集合μi是第i个簇的中心点均值向量||x - μi||是数据点x到中心μi的距离通常是欧氏距离。为什么是距离的平方使用平方项有两个主要原因一是数学上便于求导和优化在推导中心点更新公式时对SSE求关于中心点的导数平方项会让导数形式更简洁直接得到均值二是它对远离中心点的“异常值”给予更大的惩罚使得算法对簇的形状更敏感倾向于形成紧凑的球形簇。这也是Kmeans一个重要的局限性它隐含地假设每个簇都是凸形的、各向同性的在各个方向上方差相近。如果你的数据是拉长的带状或嵌套的环形标准Kmeans可能就力不从心了。2.2 标准流程与两大核心步骤Kmeans是一个迭代优化算法其标准流程可以精炼为两个交替进行的步骤分配步骤Assignment Step固定簇中心{μ1, μ2, ..., μk}将每个数据点x分配到离它最近的簇中心所属的簇中。即为每个数据点i寻找簇标签c(i)c(i) argmin(j) ||x(i) - μj||²这一步决定了当前的簇划分。更新步骤Update Step固定簇划分{C1, C2, ..., Ck}重新计算每个簇的中心点即该簇内所有数据点的均值向量μj (1/|Cj|) * Σ(i in Cj) x(i)这一步根据新的成员关系优化簇中心的位置。这两个步骤循环进行直到满足停止条件。停止条件通常有两种一是簇中心的位置在连续两次迭代中变化小于一个很小的阈值如1e-5二是分配的簇成员不再发生变化三是达到预设的最大迭代次数防止不收敛或震荡。注意这个算法保证每次迭代后SSE都不会增加通常会减少最终会收敛到一个局部最优解。但它不能保证找到全局最优解最终结果严重依赖于初始簇中心的选取。这是实操中需要应对的第一个关键点。2.3 距离度量的选择欧氏距离是唯一答案吗在MATLAB的kmeans函数中默认使用平方欧氏距离。但在不同场景下距离度量需要慎重选择。欧氏距离‘sqeuclidean’最常用适用于连续数值型特征且各特征量纲和重要性相当时。它对坐标系旋转是不变的但受量纲影响大因此数据标准化如Z-score标准化通常是预处理必备步骤。城市街区距离‘cityblock’即曼哈顿距离对异常值不如欧氏距离敏感。在某些维度特征有明确物理意义且差异是线性叠加的场景下可能更合适。余弦距离‘cosine’衡量的是向量方向的差异而非绝对距离。在文本挖掘如文档词向量、高维稀疏数据如用户-物品评分矩阵中非常有效因为它只关注模式哪些维度有值而不关心数值的绝对大小。相关距离‘correlation’基于皮尔逊相关系数衡量两个向量变化趋势的相似性。在时间序列分析、基因表达数据分析中常用。在MATLAB中可以通过kmeans(data, k, ‘Distance‘, ‘cosine‘)这样的参数来指定。选择距离度量前一定要思考你的数据特征和业务目标。例如做客户画像如果特征包括“年收入万元”和“每周购物频率次”直接算欧氏距离年收入的微小波动如1万元会完全压倒购物频率的显著变化如5次。必须先标准化。3. MATLAB实战编程从函数调用到自己动手MATLAB提供了高度优化的kmeans函数但为了真正理解算法我们分两步走先学习如何正确使用官方函数并解读结果再自己动手实现一个简化版加深对每个环节的理解。3.1 使用内置kmeans函数参数详解与结果分析我们先在一个经典的鸢尾花Iris数据集上演示。这个数据集包含150个样本每个样本有4个特征花萼长宽、花瓣长宽真实类别有3种。我们用Kmeans尝试对其进行无监督聚类。% 1. 加载并准备数据这里用内置数据集为例实际中可能是你自己的数据矩阵 load fisheriris; % 加载鸢尾花数据集变量名为meas150x4数据和species150x1标签 data meas; % 我们的特征数据 % 2. 数据标准化强烈建议 data_zscore zscore(data); % Z-score标准化使每个特征均值为0标准差为1 % 3. 确定簇数量k这里我们先假设知道是3实际中需要方法确定后文会讲 k 3; % 4. 调用kmeans函数进行多次重复以降低初始值影响 opts statset(‘Display‘, ‘final‘); % 设置显示最终结果 [idx, C, sumd, D] kmeans(data_zscore, k, ‘Options‘, opts, ‘Replicates‘, 10, ‘MaxIter‘, 1000); % 参数解释 % - data_zscore: 输入数据矩阵每行一个样本每列一个特征。 % - k: 簇的个数。 % - ‘Options‘, opts: 算法选项这里设置显示最终迭代信息。 % - ‘Replicates‘, 10: 重复运行算法10次每次使用不同的随机初始中心返回SSE最小的那次结果。这是**避免糟糕局部最优的必备操作**。 % - ‘MaxIter‘, 1000: 最大迭代次数。 % - 输出: % idx: 一个150x1的向量每个元素是对应样本的簇索引1,2,3。 % C: 一个k x 4的矩阵每一行是一个簇的中心点坐标。 % sumd: 一个1 x k的向量每个元素是该簇内所有点到中心点的距离之和不是平方和。 % D: 一个150 x k的矩阵D(i,j)是第i个样本到第j个簇中心的距离。 % 5. 可视化结果以降维后的二维散点图为例 % 使用主成分分析(PCA)将四维数据降至二维以便可视化 [coeff, score] pca(data_zscore); figure; gscatter(score(:,1), score(:,2), idx); % 根据聚类结果idx着色 hold on; % 将簇中心也投影到PCA空间 C_pca C * coeff(:,1:2); % 注意中心点C是在标准化后的原始空间需要乘以PCA系数得到投影坐标 plot(C_pca(:,1), C_pca(:,2), ‘kx‘, ‘MarkerSize‘, 15, ‘LineWidth‘, 3); % 用黑色‘x‘标出中心 title(‘K-means聚类结果PCA降维可视化‘); xlabel(‘第一主成分‘); ylabel(‘第二主成分‘); legend(‘Cluster 1‘, ‘Cluster 2‘, ‘Cluster 3‘, ‘Cluster Centers‘); % 6. 评估聚类效果与真实标签对比仅用于有标签数据的验证 % 计算调整兰德指数(Adjusted Rand Index, ARI)或归一化互信息(NMI) % 这里使用简单的混淆矩阵和调整兰德指数需要下载外部函数或使用Statistics and Machine Learning Toolbox if exist(‘species‘, ‘var‘) % 将文本标签转为数字 [~, ~, true_labels] unique(species); % 计算调整兰德指数 (需要 stats toolbox) % ari adjustedrand(idx, true_labels); % fprintf(‘调整兰德指数(ARI)为%.4f\n‘, ari); % 简易计算混淆矩阵 confusion_mat confusionmat(true_labels, idx); disp(‘混淆矩阵行真实类别 列预测簇:‘); disp(confusion_mat); end运行这段代码你会在命令窗口看到最终的迭代信息并得到一张聚类结果图。从混淆矩阵可以直观看出聚类结果与真实类别的匹配程度。请注意Kmeans是无监督学习它发现的“簇”不一定对应真实的“类”顺序也是任意的。图中簇中心黑色X大致位于各簇的“重心”位置。3.2 手撕Kmeans实现一个简易版理解迭代过程自己实现一遍是理解算法细节的最佳途径。我们来实现一个基础版本包含随机初始化和基本的迭代循环。function [idx, centers, SSE_history] my_kmeans(data, k, max_iters, tol) % 简易版Kmeans实现 % 输入 % data - m x n 矩阵m个样本n个特征 % k - 簇数量 % max_iters - 最大迭代次数可选默认100 % tol - 中心点变化容忍度可选默认1e-5 % 输出 % idx - m x 1 向量每个样本的簇标签1到k % centers - k x n 矩阵最终簇中心 % SSE_history - 记录每次迭代的SSE用于观察收敛 if nargin 3 max_iters 100; end if nargin 4 tol 1e-5; end [m, n] size(data); SSE_history zeros(max_iters, 1); % 1. 初始化随机选择k个样本作为初始中心点这是一种简单策略 rng(‘default‘); % 设置随机种子使结果可复现 random_indices randperm(m, k); centers data(random_indices, :); % k x n old_centers centers; for iter 1:max_iters % 2. 分配步骤计算每个样本到所有中心的距离并分配标签 distances zeros(m, k); for i 1:k % 计算data中所有行与centers(i,:)的欧氏距离向量化操作高效 % 利用 (a-b)^2 a^2 b^2 - 2ab data_sq sum(data.^2, 2); % m x 1 center_sq sum(centers(i,:).^2); cross_term data * centers(i,:)‘; distances(:, i) data_sq center_sq - 2*cross_term; % 平方欧氏距离 end [~, idx] min(distances, [], 2); % 找到每个样本距离最小的中心索引 % 3. 更新步骤重新计算每个簇的中心均值 for i 1:k members (idx i); % 逻辑索引找出属于簇i的样本 if sum(members) 0 % 防止空簇 centers(i, :) mean(data(members, :), 1); else % 如果出现空簇处理策略随机选择一个样本作为新中心 warning(‘簇 %d 为空将随机重新初始化该中心.‘, i); centers(i, :) data(randi(m), :); end end % 4. 计算当前SSE并记录 current_SSE 0; for i 1:k members data(idx i, :); if ~isempty(members) diff members - centers(i, :); % 差值矩阵 current_SSE current_SSE sum(sum(diff.^2, 2)); % 累加平方和 end end SSE_history(iter) current_SSE; % 5. 检查收敛条件中心点变化是否小于容忍度 center_shift sqrt(sum((centers - old_centers).^2, 2)); % 每个中心移动的距离 if max(center_shift) tol fprintf(‘迭代在 %d 步后收敛。\n‘, iter); SSE_history SSE_history(1:iter); % 截断记录 break; end old_centers centers; % 更新旧中心 end if iter max_iters warning(‘达到最大迭代次数 %d可能未完全收敛。\n‘, max_iters); end end你可以用同样的鸢尾花数据测试这个函数并与内置函数的结果对比。这个简易实现揭示了几个关键点初始化的随机性我们用了最简单的“随机选点”法效果不稳定。空簇处理在更新步骤中如果某个簇失去了所有成员空簇我们必须有处理策略如代码中的随机重初始化否则算法会出错。距离计算的向量化我们使用了向量化运算data * centers(i,:)‘来计算点积这比用循环逐样本计算快得多。这是MATLAB编程的性能关键。收敛判断我们监控中心点的最大移动距离和SSE的变化。实操心得自己实现的版本有助于理解但在生产环境或严肃分析中务必使用MATLAB内置的kmeans函数。它不仅经过高度优化速度极快还内置了更鲁棒的初始化方法如‘kmeans‘和并行计算支持并且妥善处理了各种边界情况如空簇。自己写的版本更适合教学和调试思想。4. 关键问题与调参实战让Kmeans真正为你所用会用函数只是开始解决实际问题时以下几个问题才是真正的挑战。4.1 如何确定最佳的簇数量k这是Kmeans应用中最经典、最没有银弹的问题。我们不知道数据应该分成几堆。以下是几种常用方法肘部法则Elbow Method原理是随着k增大SSE会下降因为每个簇更精细。我们希望找到一个k使得再增加kSSE的下降幅度突然变缓这个拐点像“肘部”。在MATLAB中实现% 肘部法则示例 max_k 10; sse_values zeros(max_k, 1); for k 1:max_k [~, ~, sumd] kmeans(data_zscore, k, ‘Replicates‘, 5, ‘Display‘, ‘off‘); sse_values(k) sum(sumd); % 注意内置kmeans返回的sumd是距离和不是平方和。对于肘部法则趋势一致即可。 % 更精确的SSE需要自己计算sse sum(sumd.^2)? 不sumd已经是距离和。这里我们用 sum(sumd) 作为SSE的代理指标。 end figure; plot(1:max_k, sse_values, ‘bo-‘); xlabel(‘簇数量 k‘); ylabel(‘SSE (或距离和)‘); title(‘肘部法则‘); grid on;观察曲线寻找那个“肘点”。但很多时候曲线是平滑的没有明显的肘部这就需要结合其他方法。轮廓系数Silhouette Coefficient它结合了簇内的凝聚度和簇间的分离度。对于每个样本i计算a(i) i到同簇其他样本的平均距离凝聚度。b(i) i到其他所有簇中样本i到该簇所有样本平均距离的最小值分离度。样本i的轮廓系数s(i) (b(i) - a(i)) / max(a(i), b(i))。s(i)范围在[-1, 1]越接近1说明聚类越好。所有样本s(i)的均值即为整体轮廓系数。我们可以计算不同k下的平均轮廓系数取最大值对应的k。% 轮廓系数示例 (需要 Statistics and Machine Learning Toolbox) silhouette_values zeros(max_k-1, 1); % k从2开始 for k 2:max_k idx kmeans(data_zscore, k, ‘Replicates‘, 5, ‘Display‘, ‘off‘); silhouette_vals silhouette(data_zscore, idx); silhouette_values(k-1) mean(silhouette_vals); end figure; plot(2:max_k, silhouette_values, ‘rs-‘); xlabel(‘簇数量 k‘); ylabel(‘平均轮廓系数‘); title(‘轮廓系数法‘); grid on; [best_sil, best_k_idx] max(silhouette_values); best_k best_k_idx 1; fprintf(‘轮廓系数建议的最佳k值为%d (系数%.4f)\n‘, best_k, best_sil);间隙统计量Gap Statistic比较实际数据的SSE与随机参考数据如均匀分布的SSE的差异。当实际数据的对数SSE与随机期望的对数SSE之差间隙最大时对应的k较优。MATLAB没有内置函数需要自己实现或找第三方代码。业务理解与可视化有时数学指标不如业务直觉。将数据用PCA或t-SNE降维到2D/3D可视化观察不同k下的聚类结果结合你对数据背景的理解比如你知道客户大概分高、中、低三档价值做出最终决策。注意事项没有一种方法绝对可靠。我通常的做法是“三管齐下”先看肘部图和轮廓系数图得到一个候选k的范围比如2-5然后把这些k值的结果都做出来通过降维可视化观察簇的分离情况和合理性最后结合业务目标拍板。在数学建模中需要清晰阐述你选择k的方法和理由。4.2 初始化策略Kmeans 为什么更好随机初始化可能导致算法收敛到差的局部最优或者迭代次数增多。Kmeans是一种智能初始化方法基本思想是让初始中心点彼此尽可能远离。步骤随机选择第一个中心点。对于每个数据点计算它到已选中心点的最短距离D(x)。按照D(x)²的概率分布随机选择下一个中心点距离越远的点被选中的概率越大。重复2-3步直到选出k个中心点。MATLAB的kmeans函数默认使用的就是‘kmeans‘初始化从R2019a开始早期版本需指定‘Start‘, ‘plus‘。它能显著提高聚类质量和解的稳定性。在你自己的实现中强烈建议加入Kmeans初始化代码稍复杂但效果提升明显。4.3 处理不同尺度与异常值数据标准化是Kmeans的标配而非可选。如果特征量纲不同如年龄和收入量级大的特征会主导距离计算。常用标准化方法Z-score标准化(x - mean)/std。最常用适用于特征大致符合正态分布。Min-Max归一化(x - min)/(max - min)。将数据缩放到[0,1]区间但对异常值敏感。异常值会严重拉偏簇中心的位置。应对方法预处理时剔除或缩尾使用箱线图或3σ原则识别并处理异常值。使用更鲁棒的距离度量如曼哈顿距离。考虑其他聚类算法如DBSCAN它能自动识别噪声点。4.4 评估聚类结果没有真实标签怎么办在没有真实标签的无监督学习中评估本身就是个难题。除了前述用于确定k的内部指标如轮廓系数、Davies-Bouldin Index还可以簇内相似性/簇间分离性计算簇内平均距离要小簇间中心点距离要大。稳定性分析对数据做子采样多次运行聚类看样本的簇分配是否稳定。一致性高的聚类结果更可靠。业务逻辑校验将聚类结果每个簇的样本拿给业务专家看根据他们的经验判断分群是否合理、是否有解释性。在数学建模论文中对每个簇进行特征画像描述该簇客户的典型特征是必不可少的环节。5. 数学建模与进阶应用场景在数学建模竞赛中Kmeans很少单独作为最终模型它通常是特征工程、数据预处理或更大模型中的一个环节。5.1 特征工程与降维后聚类高维数据直接聚类会遭遇“维数灾难”且计算距离的意义会减弱。常见做法是先用PCA主成分分析或t-SNE进行降维保留主要信息在低维空间如2-3维进行聚类和可视化。将聚类得到的簇标签作为一个新的类别特征加入到后续的分类或回归模型中。这相当于让模型学习到数据内部的一种分组结构信息。% 示例PCA降维后聚类 [coeff, score, latent] pca(data_normalized); explained_variance cumsum(latent)./sum(latent); % 选择累积贡献率95%的主成分数量 n_components find(explained_variance 0.95, 1); data_pca score(:, 1:n_components); % 在降维后的数据上做Kmeans k 3; [idx, C] kmeans(data_pca, k, ‘Replicates‘, 10); % 此时可视化非常方便 figure; gscatter(data_pca(:,1), data_pca(:,2), idx); hold on; plot(C(:,1), C(:,2), ‘kx‘, ‘MarkerSize‘, 15, ‘LineWidth‘, 3); title(‘PCA降维后Kmeans聚类‘);5.2 图像分割与颜色量化Kmeans在图像处理中一个经典应用是颜色量化压缩颜色数量和图像分割。将每个像素的RGB或Lab颜色值作为三维特征数据进行聚类。聚类后每个像素被赋予其簇中心的颜色值从而实现用少数几种颜色代表整个图像。% 简易图像颜色量化示例 img imread(‘peppers.png‘); img_double im2double(img); % 转为双精度 [m, n, d] size(img_double); % 将图像重塑为 m*n 行3列RGB的矩阵 pixel_list reshape(img_double, m*n, d); k 16; % 希望压缩到16种颜色 [idx, color_table] kmeans(pixel_list, k, ‘Replicates‘, 3, ‘MaxIter‘, 200); % 用簇中心颜色替换每个像素的颜色 quantized_pixels color_table(idx, :); quantized_img reshape(quantized_pixels, m, n, d); figure; subplot(1,2,1); imshow(img); title(‘原始图像‘); subplot(1,2,2); imshow(quantized_img); title(sprintf(‘颜色量化 (k%d)‘, k));5.3 时序数据与轨迹聚类对于时间序列数据不能直接对原始序列聚类因为长度可能不同。需要先进行特征提取如计算序列的统计特征均值、方差、趋势、或使用动态时间规整DTW作为距离度量再应用Kmeans。对于轨迹数据如车辆GPS点常需要先进行轨迹预处理分段、采样、对齐提取方向、速度、曲率等特征再进行聚类以发现常见的移动模式。5.4 与层次聚类的结合二分Kmeans标准Kmeans需要指定k且对初始值敏感。一种改进是二分Kmeans开始时将所有数据视为一个簇。选择当前所有簇中SSE最大的那个簇对其用Kmeansk2进行二分裂。重复步骤2直到达到预设的簇数量k。 这种方法有时能产生更稳定的层次化聚类结果并且可以在分裂过程中通过SSE下降幅度来决定何时停止从而自动确定k。6. 常见陷阱、调试技巧与性能优化即使理解了原理实际编码和调试中还是会遇到各种问题。这里记录几个我踩过的坑和解决方法。6.1 空簇Empty Cluster问题在迭代过程中可能某个簇会失去所有成员。内置的kmeans函数有稳健的处理机制。如果你自己实现必须添加检查策略一简单但可能不稳定随机选择一个数据点作为该簇的新中心。策略二更优选择距离当前所有中心最远的那个数据点作为新中心或者选择SSE贡献最大的那个数据点所在的簇将其分裂。6.2 迭代不收敛或震荡如果设置了MaxIter但算法在达到最大次数前仍未收敛中心点还在较大幅度变化可能原因数据有异常值异常值会“吸引”中心点导致中心点不断追逐异常值。检查数据进行清洗。k值选择过大过于细分的簇可能导致边界点在不同簇间反复横跳。尝试减小k或使用轮廓系数检查。距离度量不合适尝试更换距离度量如从‘sqeuclidean‘换为‘cityblock‘。增加MaxIter对于复杂数据集可能需要更多迭代。但通常100-1000次足够再多可能意味着问题本身。6.3 性能优化处理大数据集当数据量很大数十万以上时Kmeans的计算尤其是距离计算会成为瓶颈。优化策略使用内置函数并开启并行MATLAB的kmeans支持并行计算。确保Parallel Computing Toolbox已安装并在调用时使用‘Options‘, statset(‘UseParallel‘, true)。对于多核CPU加速效果显著。数据采样如果数据量极大可以先进行随机采样在样本上确定k和初始中心然后再用全部数据进行一次精细聚类初始中心设为样本聚类得到的中心。使用更快的算法变种如Mini-Batch K-Means它每次迭代只使用一个数据子集来更新中心速度更快适用于海量数据。MATLAB未直接提供但可以自己实现或寻找工具箱。向量化与矩阵运算如我们手写代码示例所示避免在循环内对单个样本计算距离利用MATLAB的矩阵运算能力。6.4 结果的可复现性Kmeans的结果依赖于随机初始化。为了确保结果可复现设置随机数种子在运行kmeans前使用rng(seed)如rng(42)固定随机数生成器状态。使用‘Replicates‘参数即使设置了种子单次运行仍可能陷入局部最优。设置‘Replicates‘, 10或更多让算法运行多次并返回最佳结果这是获得稳定、高质量结果的黄金准则。虽然计算时间增加但结果可靠性大幅提升。最后记住Kmeans是一个工具它有明确的适用场景球形簇、簇大小密度相近和局限。当你的数据不符合这些假设时不要强行使用。不妨试试层次聚类、DBSCAN基于密度或高斯混合模型GMM。理解算法的“为什么”和“怎么样”远比记住函数调用更重要。在数学建模中清晰阐述你选择Kmeans的理由、参数确定的依据、以及对结果局限性的讨论往往比单纯跑出一个结果更能赢得评委的青睐。