
做数据分析这行聚类几乎是最先接触的算法。前段时间有个项目要快速给一批客户样本分群我把Matlab里常见的聚类方法从头到尾过了一遍Kmeans聚类、模糊C均值聚类、层次聚类、高斯混合模型聚类、SOM神经网络聚类。现在把整套实验代码、调参思路和踩过的坑整理成一篇博文希望能给正在做聚类实验、写课程设计或者搞数据挖掘的同学一点参考。这篇不是教科书也不是单纯的代码堆叠更多是“我实际跑下来”的经验总结。五种方法各有各的脾气选型不对聚类效果直接拉胯。1. 五种聚类的本质区别先搞懂思路再写代码1.1 Kmeans与模糊C均值硬划分与软划分的一步之遥Kmeans聚类是最经典的划分式聚类。它的思路很直白给定K个初始质心重复两步——把每个样本分配到离它最近的质心然后重新计算每个簇的质心位置直到质心基本不再变化。这个“最近”通常指欧氏距离所以Kmeans天然倾向于寻找球形、大小相近的簇。代码写起来简单跑起来也快几千几万个样本一点压力没有。但Kmeans有个致命限制硬划分。一个样本要么属于A簇要么属于B簇没有中间态。实际数据里很多边界样本本来就模棱两可硬掰到某一侧会让结果显得不自然。模糊C均值聚类FCM就是冲这个问题来的。它把“硬归属”改成“隶属度”每个样本对每个簇都有一个0到1之间的隶属度所有簇的隶属度加起来等于1。迭代时不再直接分配样本而是用隶属度加权计算新的簇中心。你可以把FCM理解成Kmeans的“软版本”边界样本不再是二选一而是“A簇占六成、B簇占四成”。我在实际项目里用过一次FCM做客户分群业务方正好需要“这个客户同时有多个标签”的输出模糊隶属度直接就能当置信度用。而Kmeans给出的硬标签在这种场景下就不够灵活。所以选型的时候先想清楚一个关键问题你需要的输出是“非此即彼”的标签还是“带概率的归属”这个问题的答案基本就能决定你在Kmeans和FCM之间怎么选。1.2 层次聚类不用预设K值先把树建起来层次聚类和划分式聚类的思路完全不同。它不预设K而是先把样本之间的距离矩阵算出来然后不断合并最近的两个簇凝聚式最终形成一棵树。这棵树的每一层都对应一个可能的聚类方案从最底层的每个样本单独成簇到最顶层所有样本合成一个大簇中间任意一层都可以当做一次聚类结果。这给使用者带来的好处特别直观你不需要在跑算法前拍脑袋定K而是可以先看树状图找到距离跳变最明显的“断层”在那个位置切一刀K值就自然出来了。我经常用层次聚类做前期的探索性分析目的不是为了直接拿最终分群结果而是想通过树状图理解数据内部的结构关系看看哪些簇靠得近、哪些簇差异大。这种层级信息是Kmeans给不了的。不过层次聚类的代价也明显。它要维护样本间距离矩阵样本量大时内存和计算量都涨得很快。我一般只在几千样本以内用层次聚类再大的数据集就换Kmeans或者GMM了。另外一个容易踩的坑是连接方式也就是“簇与簇之间的距离怎么定义”。最常用的有ward、average、complete、single几种ward基于方差增量、average基于平均距离、single基于最近的距离。single对噪声特别敏感一点点异常值就会把两个本不该合并的簇连在一起我实际用下来最稳的是ward但它要求距离度量是欧氏距离这个限制在后面代码部分会细说。1.3 高斯混合模型给每个簇配一个概率分布高斯混合模型聚类走的是另一条路假设整份数据是由若干个高斯分布混合生成的每个高斯分布就是潜在的一个簇。算法拿到数据后用EM算法去估计每个高斯分布的均值、协方差以及每个分布所占的权重然后根据样本在每个分布下的概率密度来决定它属于哪个簇。GMM相比Kmeans的优势在于簇形状的宽容度。Kmeans假设每个簇是“圆形”更准确说是各方向等方差的球形GMM通过协方差矩阵可以描述椭圆形的簇不同簇的大小也可以不同。做图像分割时我经常用GMM因为图像里几个区域的颜色分布往往是椭球状Kmeans硬切成球形的效果总是差口气GMM的灵活度就能很好匹配这种数据。这里有个理解GMM的关键Kmeans其实是GMM的一个特例。如果每个高斯分布的协方差矩阵都固定为单位矩阵乘一个常数并且所有簇的先验权重相等那么EM迭代的结果和Kmeans的迭代在思路上非常接近。所以你会发现在同一个数据集上Kmeans和GMM的分簇结果经常大差不差。真正让GMM拉开差距的场景是簇的形状差异明显、大小差异明显或者你需要输出“样本属于每个簇的后验概率”的时候。1.4 SOM神经网络聚类把高维样本“摊”到二维网格上神经网络聚类的代表是SOM自组织映射它属于竞争学习算法。SOM会定义一个输出层通常是二维网格比如5乘5的网格就有25个神经元。训练时每个输入样本会激活一个最匹配的神经元同时激活这个神经元附近的邻居权重向量不断向输入数据靠拢。最终效果是高维数据被映射到低维网格上并且原本在特征空间里相近的样本在网格上的位置也相近。我第一次用SOM的感觉是“这东西更像可视化工具而不是聚类器”。它的确能聚类但输出的是每个样本映射到的神经元坐标而不是簇编号。要和Kmeans直接对比的话得先把25个神经元合并成几个簇。我常用一个偷懒但有效的策略训练完SOM后把25个神经元的权重向量拿出来再做一次Kmeans聚成3类这样每个样本通过它对应的神经元就能拿到最终簇标签。SOM还有一个很受用的特点它能保留拓扑结构。业务场景里如果希望分群结果带有可解释的空间关系比如“这两堆客户在特征上是相邻的”SOM的网格地图非常直观。Matlab里可以用plotsompos、plotsomplanes直接看权重距离和每个特征维度的响应面这对向非技术同事解释聚类结果很有帮助。2. 实验前的准备工作数据、预处理与评价指标2.1 数据选择用Fisher鸢尾花数据跑通全流程为了对比五种聚类方法我选了Matlab里自带的Fisher iris数据集。load fisheriris加载进来后meas是150行4列的数值矩阵对应三种鸢尾花各50个样本每个样本有花萼长、花萼宽、花瓣长、花瓣宽四个特征。species是150行1列的元胞数组记录了真实类别。选这个数据有三个原因。第一规模小五种方法都能在几秒内跑完适合反复调参数看效果。第二有真实标签虽然聚类本身不需要标签但可以做事后对比看看哪种方法更接近真实分类。第三四个特征维度不高不低既不需要降维又能看出特征量纲差异对聚类的影响。需要提醒的是真实标签在聚类实验里只用来事后验证不能拿来告诉算法“正确答案”。聚类的本质是在无监督条件下发现结构你要是把标签喂进去那就变成分类问题了。2.2 特征标准化看似小事影响却是全局的iris数据四个特征的量纲差异不算悬殊但依然要标准化。我见过太多人拿到数据直接丢进kmeans函数结果出来一个看起来合理但实际上被某些特征主导的聚类结果。Kmeans、FCM、GMM、层次聚类这四种方法都涉及距离计算或协方差估计特征量纲不同会让取值范围大的特征在距离计算中占据压倒性权重取值范围小的特征直接被忽略。我在脚本里统一用zscore做标准化这一步在Matlab里就一行load fisheriris X meas; X zscore(X); % 按列标准化均值为0方差为1 labels_true grp2idx(species); % 转成1、2、3仅供事后对比SOM虽然用的也是距离但神经网络训练时通常建议同样进行标准化不然竞争力强的特征会压过其他特征。这篇博文里五种方法的对比实验统一基于这份标准化后的X保证公平。有一个细节值得注意标准化后的聚类结果不一定和业务原始尺度吻合。比如做客户分群时消费金额这个特征量级天然比“活跃天数”大标准化后两者的权重被拉平了。如果业务上就想强调消费金额那可以人为调权重或者不做标准化而是给关键特征单独加权。要不要标准化本质上取决于你想让每个特征以什么权重参与聚类。2.3 聚类效果怎么量化轮廓系数、CH指标与DB指标聚类没有“正确答案”怎么评价效果好是门学问。我常用的三个内部指标轮廓系数Silhouette Coefficient最直观。它既考虑每个样本与同簇其他样本的紧密程度又考虑与最近邻簇的分离程度取值范围在-1到1之间。大于0.5说明结构明显0.25到0.5说明有结构但边界模糊小于0.25基本说明数据不分团或者参数选得不对。Matlab里直接mean(silhouette(X, idx))就能拿到均值。Calinski-Harabasz指数CH也叫方差比标准计算簇间离散度与簇内离散度的比值越大说明簇间越紧凑、簇与簇之间离得越开。Davies-Bouldin指数DB正好相反是簇内散度与簇间距离的比值越小越好。这三个指标在Matlab里evalclusters函数一键就能算。我用它做K值选择的辅助判断比较靠谱后面踩坑章节会展开讲。需要注意的一点是内部指标反应的只是“几何结构是否清晰”它和业务上“这个分群是否有意义”是两码事。有些分群虽然在指标上非常漂亮但落在业务上没法解释这种情况我会反过来优先照顾业务规则指标只作为参考。3. Matlab五种聚类方法的实现与调参记录3.1 Kmeans聚类Replicates和MaxIter决定稳定度Matlab里kmeans函数在统计和机器学习工具箱里基础调用特别简单rng(default); % 锁定随机种子保证结果可复现 [idx_km, C_km] kmeans(X, 3, Replicates, 10, MaxIter, 500); sil_km mean(silhouette(X, idx_km)); fprintf(Kmeans silhouette: %.4f\n, sil_km);这里的Replicates, 10非常关键。Kmeans的迭代结果严重依赖初始质心虽然Matlab默认的Start选项已经用k-means做了智能初始化但只能保证“大概率好”不能保证“每次都好”。Replicates参数让算法用不同初始质心从头跑10次最后返回代价最小的那次结果。我通常设10到20再大就有点浪费算力收益不明显了。MaxIter是单次迭代上限默认是100通常够用但数据量大或者K较大时可能还没收敛就到了上限。可以设成500顺带消除告警。我踩过一个坑某个数据集跑kmeans偶尔出现一个簇只有一个样本的诡异结果最初以为是数据问题后来发现是Replicates设成1撞上了糟糕的初始质心。改成Replicates10之后这种问题基本绝迹。3.2 模糊C均值聚类模糊指数怎么定FCM在用模糊逻辑工具箱函数名就叫fcm。基本调用options [2.0, 200, 1e-5, 1]; % [模糊指数m, 最大迭代数, 最小改善量, 是否显示] [centers_fcm, U] fcm(X, 3, options); [~, idx_fcm_raw] max(U, [], 1); idx_fcm idx_fcm_raw; sil_fcm mean(silhouette(X, idx_fcm));options四个参数第一个是模糊指数m常用默认值2.0。m越大隶属度分布越模糊所有样本对所有簇的隶属度都趋向均匀m越接近1结果越像硬聚类。我试过把m调到3分出来的簇边界完全糊掉很难用。实践中m在1.5到2.5之间调整即可除非有明确的业务理由别去碰极端值。U矩阵是核心输出维度是K行乘N列每列对应一个样本在所有簇上的隶属度。要对样本做硬标签max(U, [], 1)取出每列最大值所在的位置再转置成列向量。如果业务需要“模糊”U矩阵本身就能用。fcm的初始化也是随机的所以跑之前同样要rng(default)。另外提醒一句fcm函数属于模糊逻辑工具箱没有这个工具箱会直接报错折腾环境前先license(test,Fuzzy_Toolbox)确认一下是否装了。3.3 层次聚类pdist、linkage到dendrogram的完整链路层次聚类在Matlab里是三步走的套路D pdist(X, euclidean); % 1. 计算样本两两距离 Z linkage(D, ward); % 2. 按ward法生成聚类树 figure; dendrogram(Z, 30); % 3. 画树状图只画前30个叶子 idx_hc cluster(Z, maxclust, 3); % 在树上切一刀得到3个簇 sil_hc mean(silhouette(X, idx_hc));pdist默认是欧氏距离但也可以换成cityblock、correlation等。链接方式选择ward时距离度量最好保持欧氏因为ward的公式就是基于欧氏距离的方差增量推导的。如果你用了cityblock距离又配ward结果不一定错但理论上就不太严谨。dendrogram画图时样本量一大叶子标签会挤成一团。我习惯用dendrogram(Z, 30)只显示靠近末端的30个叶子看清局部结构就够了。切树的时候观察树状图里哪些分支的垂直高度很长说明这些分支之间的合并发生得比较晚也就是簇间差异大在这些长分支处切刀往往就是合理的K。还有个小技巧可以用inconsistent(Z)算不一致系数它衡量某次合并和相邻合并的相对高度变化值大幅跳高的位置通常对应“自然的”聚类数量。不过实践里我更多还是看图配合轮廓系数验证。3.4 高斯混合模型聚类协方差结构与正则化GMM在统计和机器学习工具箱里最常用的拟合函数是fitgmdistgm fitgmdist(X, 3, ... CovarianceType, full, ... RegularizationValue, 1e-5, ... Replicates, 5); idx_gmm cluster(gm, X); [P, ~] posterior(gm, X); % 后验概率矩阵想要软标签就取它 sil_gmm mean(silhouette(X, idx_gmm));CovarianceType默认是full也就是每个高斯分布有自己的完整协方差矩阵能刻画椭圆形的簇。如果你确定问题是圆簇可以改成diagonal参数少一半、训练更快但我实测在常见数据集上full的表现普遍更好优先用full。RegularizationValue这个参数很容易被忽略但特别重要。EM算法在某个簇样本太少或特征高度相关时协方差矩阵可能变成奇异矩阵拟合直接报错。给一个1e-5到1e-3之间的正则化系数相当于给协方差矩阵对角线加一点点扰动数值上就稳了。我一般先设1e-5如果报“ill-conditioned covariance”就逐步调大。还有一种做法是先降维减少特征数量也能从根源上缓解这个问题。Replicates和Kmeans里的意义一样EM算法同样对初始值敏感多跑几次取最优能明显提高稳定性。另外fitgmdist还支持把K设成一个向量比如fitgmdist(X, 1:6)它会自动拟合出1到6个分量的模型并推荐最优的这样写省去手撸循环。要注意返回的gm是一个gmdistribution数组取第一个元素是自动选出的最优模型调用方式稍显绕我一般还是自己写循环跑BIC。3.5 SOM神经网络聚类训练后用Kmeans合并神经元SOM需要在深度学习工具箱代码写起来和前面几个风格不一样因为神经网络工具箱约定样本按列存放输入矩阵是“特征维度乘样本数”rng(default); net selforgmap([5 5]); % 5x5的二维网格 net.trainParam.epochs 200; net train(net, X); y net(X); % 每个样本在25个神经元上的响应 winner vec2ind(y); % 每个样本对应的获胜神经元索引1到25 W net.IW{1,1}; % 25x4的权重矩阵每行对应一个神经元 neuIdx kmeans(W, 3, Replicates, 10); % 对神经元做二次聚类 idx_som neuIdx(winner); % 把神经元标签映射回样本 sil_som mean(silhouette(X, idx_som));这里用5乘5的网格是经验值。网格太大神经元数量多二次聚类时容易把同一个自然簇切碎网格太小拓扑结构不够丰富高维信息被过度压缩。我一般先按样本量的平方根估算输出神经元总数再排成接近正方形的网格。比如150个样本大概用16到25个神经元所以选5乘5比较合适。训练完SOM后net.IW{1,1}里存的是每个神经元的权重向量。把这25个权重向量当作“第二层数据”再做一次Kmeans聚到3类本质上是先用SOM做矢量量化压缩数据规模再用Kmeans做最终分簇。这种“先粗聚类再细聚类”的级联思路在工程里很实用也能让SOM的结果直接和其他四种方法对比。可视化方面plotsompos能看到25个神经元在网格上的权重距离关系颜色越深的地方说明两个神经元之间权重变化越大往往就是簇的边界。plotsomplanes能分别看每个特征在网格上的响应面对解释“为什么分成这些组”很有帮助。这些图画完直接能贴进实验报告里比干巴巴的聚类指标更有说服力。4. 踩坑实录聚类分析常见问题与排查方法4.1 K值不确定时怎么选择聚类前最头痛的问题就是K设多少。我常用的组合拳是先画肘部图再看轮廓系数最后结合业务判断。Matlab里evalclusters函数能一次性帮你算多个K下的指标eva evalclusters(X, kmeans, CalinskiHarabasz, KList, 1:8); figure; plot(eva); eva.OptimalK % 最优K值evalclusters的数据输入可以是原始矩阵也可以是自定义函数返回的聚类索引。指标选项支持CalinskiHarabasz、DaviesBouldin、Silhouette等。我一般把CH和DB都算一遍如果两个指标给出的最优K不一致再回去看树状图和业务约束。这类指标有个通病倾向于在高K值上给出“好结果”因为簇越多通常几何上越紧凑但分得太细的业务意义不大。我印象很深的一次K从2到8的CH指数一路走高在8达到峰值可业务上分成8组根本无法解释。最后我还是选了K等于4因为那个结果能对应上市场的四个自然档位。指标是工具决策还是要人来下。4.2 出现“空簇”和“吞并簇”怎么办跑Kmeans或FCM时偶尔会遇到某个簇一个样本都没有。这个问题的根源往往是初始质心扎堆或者离群点把质心拽偏了。排查思路先是在kmeans调用里把Replicates调到10以上观察是否缓解如果还出现就要检查数据里是否有极端离群点。我处理离群点时一般先做可视化二维数据直接画散点高维数据先用PCA降到前两个主成分看。确认离群点后可以用rmoutliers按阈值剔除但不要无脑删先看看离群点是不是代表一种稀有但真实的业务场景。还有一招是把离群点单独设为一类在聚类前不剔除而是要求算法多设一个K。这在某些异常检测场景里反而更合适。FCM出现空簇的概率比Kmeans低因为隶属度机制让每个簇至少能吸引到一部分边界样本但模糊指数m调得过大时所有簇基本“平分”样本簇之间没有了区分度。如果你发现FCM的簇中心高度重叠先检查m是否太大了。4.3 高维数据聚类前想清楚这三件事高维数据是聚类的重灾区。维度一高距离度量越来越“失效”所有样本之间的距离都趋近于差不多这在高维几何里叫“维数灾难”。我在高维场景下有三条固定原则第一先降维再聚类。PCA是最稳妥的选择把维度降到二维或三维先看能不能画出分团结构。如果PCA投影里数据重叠得厉害再试t-SNEMatlab里tsne函数虽然慢一点但在高维数据的可视化上效果好得多。第二降维后不能直接拿降维特征做业务解读。PCA的主成分含义可能很难解释t-SNE的坐标更没有任何物理意义。降维只是为了观察和调试最终的聚类如果用于业务尽量回到原始特征上看各簇的特征均值找到有业务含义的区分点。第三原始特征的相关性处理。高维数据里常存在高度相关的特征这在GMM里会直接造成协方差矩阵病态。先算相关矩阵把相关性超过0.95的特征只保留一个能省下后续大量调参时间。4.4 结果不可复现是随机种子没锁聚类算法大多有随机性。Kmeans的初始质心随机FCM的初始隶属度矩阵随机EM的初始参数随机SOM的权重初始化随机。同一份代码跑两遍结果不一样这在实验报告里是大忌。我的做法是在所有脚本最顶部加上rng(default)需要更稳定的复现环境时用rng(42)这类固定整数。这里有个容易漏的点每个算法内部可能多次调用随机数比如Replicates10会生成10组初始值如果你中途运行过其他随机数函数后续“随机序列”的走向会被打乱。所以实验时养成习惯所有聚类操作紧挨着rng调用或者干脆把整个脚本格式化成“随机种子开头、各聚类方法顺序执行”这样最省心。但有一个反直觉的事实有时候你需要刻意制造变化。比如你要评估某个数据集上聚类结果的稳定性锁死随机种子只看到一种结果反而不能暴露出算法对初值的敏感性。这种场景下我会跑几十次Replicates然后统计不同簇分配结果的频率看看哪些样本被判来判去这些“边界样本”往往是最有价值的分析点。5. 横向对比、选型建议与融合扩展5.1 一张表看五种聚类的适用边界五种方法跑完建议先看下面这张表它会告诉你大多数场景下该优先选谁。方法是否需要预设K硬/软聚类适合的簇形状Matlab工具箱一句话选型建议Kmeans聚类需要硬球形、凸簇Statistics and Machine Learning Toolbox数据量大、簇形状简单时的首选模糊C均值需要软球形、凸簇允许交叠Fuzzy Logic Toolbox需要隶属度或概率化归属时用层次聚类不需要硬任意依赖距离度量Statistics and Machine Learning Toolbox样本量小、想看层级结构和聚类过程高斯混合模型需要软椭圆形大小可以不同Statistics and Machine Learning Toolbox簇形状不规则或需要概率输出时用SOM神经网络不需要但需后处理确定簇数硬任意通过拓扑映射Deep Learning Toolbox高维可视化、展示拓扑关系时用这个表看着轻巧背后是我跑了整整一轮实验得出的结论。如果只想要一个干净利落的簇标签Kmeans八成够用如果想要“每个样本属于每个簇的概率”FCM和GMM可以做到但FCM倾向于给比较平均的模糊度GMM的概率有更强的统计意义如果想理解数据内部的组成层次层次聚类的树状图无可替代如果想给老板展示一个直观的二维分群地图SOM的网格图胜出。5.2 从单算法到多算法融合图像分割中的应用参考聚类方法不止能用来分客户、分样本还能做图像分割。图像分割本质上就是把每个像素当作一个样本颜色特征RGB或灰度加上像素坐标组成特征向量然后做聚类每个簇对应一个分割区域。我试过用FCM和GMM做彩色图像分割效果显著好于普通的阈值分割因为图像的颜色分布在RGB空间里经常是椭球状GMM的椭圆簇特性刚好匹配。更有意思的是多算法融合的思路。比如先用Kmeans快速做一个预分割得到大致的区域范围然后在每个大区域内再用GMM做精细分割这样的级联结构既能保证速度又能拿到GMM的精确边界。我还做过一个更简单的融合把SOM输出网格里每个神经元的权重向量作为新的数据表示再喂给层次聚类做最终聚合这种“特征变换加聚类”的组合往往比单一方法质量更稳。如果把第二层扩展成业务系统完全可以按多算法融合的思路设计成模块化流程数据标准化模块、聚类算法选择模块、内部指标评价模块、结果可视化模块每个模块独立封装。这样在更换算法或调整参数时只需要替换其中一个模块其他部分不受影响整体可控性比“一把梭跑到底”强得多。这也是为什么很多数字图像处理系统会采用多算法融合的架构设计五种聚类各有长处组合起来才能覆盖更复杂的场景。当然图像分割的完整设计还涉及像素采样、超像素预处理、后处理去噪声等环节这里就不展开了但聚类这一步是整个体系的底座。5.3 我的一点经验体会如果你正在选型我的经验是先把Kmeans和层次聚类跑一遍看轮廓系数和树状图这能让你对当前数据集的形状有个基本判断。如果簇边界明显模糊或者业务上需要概率输出就上FCM或GMM如果特征维度高且想向非技术同事展示结构SOM的拓扑图是最直观的表达。五种方法不是竞争关系更像是一套工具组配合着用效果才好。最后再分享一个小技巧跑聚类前先用t-SNE画一张二维散点图看数据是否真的分团。如果二维投影里样本糊成一团你就不用指望Kmeans能给出多漂亮的结果这时候换GMM或FCM去抓边界反而可能有意外收获。聚类不是万能的但五种方法配合着用绝大多数分群需求都能兜住。我后来把这段实验流程固化成了自己的标准模板遇到新数据先标准化再跑Kmeans和层次聚类做侦察根据结果决定要不要请出FCM、GMM和SOM整个过程又快又稳。