
1. 聚类技术解决的本质问题无监督分组与相似度选择聚类技术是我工作中用得最多、也最容易被误会的一类算法。刚做数据分析那会儿我以为它就是把数据画个散点图然后让机器自动圈出几个团块后来读了MacQueen在1967年的论文又啃了DBSCAN、谱聚类等一系列工作才意识到聚类技术背后拖着一条很长的历史线索——从上世纪50年代的数值分类学到如今高维数据里的子空间聚类不同类别的方法解决的是完全不同的问题。这篇文章就按这条线索把聚类技术的发展历史、主要分类和值得精读的几篇重要论文一起梳理一下也穿插一些我做层次聚类和k-means时用Python、Matlab攒下来的实操经验。1.1 聚类不是分类无监督问题里的“答案”由谁定义首先要掰扯清楚一个常见误区聚类和分类不是一回事。分类是有监督问题训练集里每个样本都已经贴好标签算法要学的是“标签背后的规律”聚类是无监督问题数据集里只有样本本身没有任何先验标签算法要回答的是“这批样本天然能分成几堆、每一堆由什么特征定义”。这个差异看似简单影响却极其深远。有标签时你可以用准确率、召回率这些客观指标去评判模型好坏没有标签时“聚类结果好还是不好”就变成了一个开放命题。同样一批用户行为数据如果按消费金额聚类可能得到“高活跃、中活跃、低活跃”三段式结构如果按商品品类偏好聚类可能得到“数码控、美妆党、母婴人群”等完全不同的分组。两种结果都有道理取决于你想让“相似”这个词朝哪个方向倾斜。所以我会反复提醒刚入行的朋友启动聚类项目之前先回答三个问题——你希望哪些维度的相似性起主要作用你允许的簇形状大概什么样你准备怎么评估聚类结果这三个问题的答案会直接决定你该选哪种聚类算法、用哪个距离度量而不是上来就fit一个KMeans了事。1.2 距离和相似度看似简单却决定算法上限的基本功大多数聚类算法都需要一个“样本之间有多像”的量化标准。最常见的是闵可夫斯基距离族的欧氏距离当数据维度都是连续值、量纲相似时欧氏距离直观好用当特征数量级差异巨大比如“消费金额”动辄几千、“点击次数”只有个位数不标准化就直接算欧氏距离金额会把次数完全淹没当数据是文本向量或行为偏好向量时欧氏距离往往不如余弦相似度合适因为文本向量更关心方向而不是模长。你可以做一个简单的实验找一批二维点用欧氏距离聚类和用相关系数距离聚类大概率得到不同形状的簇。这提醒我们聚类结果不是算法的唯一产物而是“距离度量 算法假设 数据特征”三者的共同结果。1.3 从数值分类学到现代聚类一段简短的历史坐标聚类技术的历史可以追溯到分类学家对物种进行数量化归类的需求。20世纪50年代Sneath和Sokal等人推动了数值分类学他们把物种间的形态特征编码成数值再用系统聚类构建树状关系这就是现代层次聚类的重要源头。1963年Ward提出了最小方差层次聚类1967年MacQueen正式使用了“k-means”这个术语1967年Johnson也给层次聚类搭建了通用框架。再往后1996年的DBSCAN、1998年的CLIQUE、2000年前后的谱聚类分别从密度、子空间和图模型三个方向把聚类技术的版图扩展开来。这些发展脉络不是考古学。理解每一类算法诞生的动机你才能真正理解它们各自的边界——这也是后面几个章节我想展开讲的东西。2. 原型聚类四十年从Lloyd/MacQueen到k-means的工程启示如果只允许我向业务方推荐一种聚类算法我一定会选k-means。原因很简单它快、直观、有几十年的工程验证。但“直观”不等于“无脑”k-means里面埋着好几个足以毁掉项目的暗坑。2.1 原型聚类的起点不在1967Lloyd算法与MacQueen命名的错位很多资料说k-means诞生于1967年这个说法需要校正。实际上1957年Lloyd在贝尔实验室就提出了脉冲编码调制中的最小二乘量化算法这就是后来k-means的核心——交替更新样本归属和簇中心。只是因为当时属于内部技术报告直到1982年才正式发表。1967年MacQueen发表论文时首次使用了“k-means”这一术语并讨论了在线式逐个样本更新的算法版本这是“k-means”这个名字的由来。1979年Hartigan和Wong给出了更稳定的批量更新版本后来很长一段时间里软件包中实现的默认算法都沿用了这一思想。理解这段历史对实际工作有直接帮助当你用不同的库跑k-means时迭代方式、初始质心策略、终止条件可能各不相同所以同一份数据用Matlab的kmeans和Python的KMeans跑出略微不同的结果完全正常。关键不是纠结“哪个对”而是要理解算法收敛到局部最优是默认状态不是bug。2.2 找k值不是玄学肘部图、轮廓系数与Gap Statistick-means的第一个大问题是k值怎么定。这里我按实用程度排序常用三种方法。肘部图Elbow Method对一系列k值计算簇内误差平方和SSE画折线图找斜率由陡变缓的“拐点”。注意真实数据往往没有明显拐点所以这个方法更像“参考线”。轮廓系数Silhouette Coefficient对每个样本计算簇内平均距离a和最近邻簇平均距离b轮廓系数为(b-a)/max(a,b)。它对每个簇的形状和分离度都比较敏感通常取轮廓系数最大的k。但它在簇很大、形状不规则时会偏高或偏低不能盲信。Gap Statistic在不同k下比较当前数据SSE和均匀分布参考数据的SSE看看哪个k带来的“超出预期的紧致程度”最大。这个方法相对严谨但计算量偏大。我在实际项目中通常是三管齐下先跑肘部图和轮廓系数圈定候选范围再用业务口径去判断每个簇是否可解释。聚类最终是要给人用的一个轮廓系数很高但业务上完全无法解释的划分还不如一个略“不完美”却有明确业务含义的划分。2.3 初始质心敏感、距离度量和运行细节MATLAB与Python的工程经验k-means天然对初始质心敏感。烂的初始点会把算法带进很差的局部最优。2007年Arthur和Vassilvitskii提出了k-means初始化先从数据中随机取第一个质心之后每次以“距离当前最近质心越远的点越容易被选为下一质心”的概率抽样而不是纯随机。这能把初始质心铺得更开收敛质量显著提升。现代工具里这已经是基本功了。以Python为例sklearn.cluster.KMeans的initk-means就是默认值Matlab的kmeans默认用k-means类似的策略但可以通过Start参数调整。Matlab里跑k-means最容易被忽略的是Replicates参数[idx, C, sumd] kmeans(X, 3, Replicates, 10, MaxIter, 1000, Options, statset(UseParallel, true));Replicates表示从不同初始质心重复跑10次取SSE最小的一次。如果数据量不大强烈建议把Replicates设为10或20——这是性价比最高的“防呆”手段。Python里对应的做法是设置n_init10random_state0保证结果可复现。Python侧另一个常见坑是忘记标准化。k-means基于距离如果特征量纲不统一直接聚类会把注意力全放在数值大的维度上。我习惯用StandardScaler或MinMaxScaler先处理再送入算法。此外如果你处理的是大规模数据MiniBatchKMeans是很好的近似加速方案代价是结果稳定性稍有下降。最后提醒一点k-means假设簇是凸的、大小接近的球形结构。当你遇到细长簇、环形簇或严重不均衡的数据时k-means效果会很差。这时候就该考虑下面两章的方法。3. 层次聚类的树状图逻辑链接准则、Ward法与Python调包的取舍层次聚类是另一种历史悠久的聚类技术它的输出是一棵层次树不像k-means那样直接给你平面划分。这既是优点也是负担优点是你可以在不同粒度查看聚类结果负担是当数据量超过几千条时传统层次聚类的计算量相当可观。3.1 凝聚还是分裂树状图背后的两种构建方式层次聚类主要分两类。凝聚式层次聚类AGNES从每个样本各自成一个簇开始每次合并最相似的两个簇直到变成一个簇分裂式层次聚类DIANA反过来从整个数据集一个大簇开始每次挑一个簇拆开直到每个样本单独成簇。实际应用中凝聚式远比分裂式常见因为分裂式要决定“拆哪个簇、怎么拆”计算和判断都更复杂。这个过程的副产品就是树状图dendrogram。树状图不仅告诉你每个样本从哪一步开始被合并还传达了簇与簇之间的“亲缘”层次。我在做表达谱数据时特别喜欢用树状图因为可以直观看到大簇里面还有没有更细的结构这是k-means无法直接提供的。3.2 单链接、全链接、平均链接与Ward把链接准则选明白凝聚式层次聚类每一步都要算“两个簇之间的距离”而这个“簇间距离”有不同算法也就是链接准则。它们之间差异很大单链接single两个簇的最近样本距离作为簇间距离。容易形成“链式效应”把两个本不相关的簇通过一系列中间样本连在一起造成细长条状簇。全链接complete两个簇的最远样本距离作为簇间距离。倾向于生成紧凑的球形簇但对离群点和噪声非常敏感一个离群点就可能把两个簇拉远。平均链接average两个簇所有样本距离的平均值。折中方案也叫UPGMA在生物学分类里用得很多。Ward法Ward‘s method合并时选择使“合并后簇内SSE增幅最小”的两个簇结果趋近于紧凑、大小相近的凸簇。对数据分布的要求和k-means类似但通常聚类结构更稳定。这里有一个容易被忽略的“为什么”为什么Ward法不能配任意距离度量因为Ward法本质上要通过距离平方来计算方差增量通常配欧氏距离才是严密的。实际中你如果在Python的linkage里写methodward, metriccityblock很多版本会直接报错或行为不可预期。选链接准则前先想清楚你要的簇形状要扁平紧凑用Ward要能容忍链状自然延伸用单链接想稳定夹中间用平均链接。3.3 Python/scipy里的层次聚类实验从linkage到dendrogramPython里最常用的是scipy.cluster.hierarchy。一个典型流程如下import numpy as np from scipy.cluster.hierarchy import linkage, dendrogram, fcluster import matplotlib.pyplot as plt # 假设 X 是已经标准化后的特征矩阵 Z linkage(X, methodward, metriceuclidean) # 画树状图 plt.figure(figsize(10, 6)) dendrogram(Z) plt.show() # 在高度阈值处切出k个簇 labels fcluster(Z, t3, criterionmaxclust)这个流程看起来很简单但有几个细节值得说。第一linkage的输入是样本矩阵还是距离矩阵如果你传入的是n×n距离矩阵且condensed标志不对scipy会按样本矩阵去解析结果完全错乱。一个稳妥做法是先用pdist算距离再传入squareform处理后的向量from scipy.spatial.distance import pdist dist_mat pdist(X, metriceuclidean) Z linkage(dist_mat, methodward)第二树状图横轴的顺序是根据合并顺序排出来的不是样本原顺序。很多人以为dendrogram返回的叶子顺序是原始索引其实是dendrogram(Z)[leaves]里的映射。如果需要把聚类结果映射回原始样本一定用fcluster返回的标签而不是从图上去猜。第三计算复杂度。凝聚式层次聚类朴素的实现是O(n^3)即使优化过也至少是O(n^2)级别。数据量超过一万时跑起来非常酸爽。工程上的折中做法是先用k-means粗聚到比如200个中心再对这200个中心做层次聚类这样既能得到树状图的大结构又能在可接受时间内出结果。4. 密度聚类与欧氏距离的边界DBSCAN/OPTICS在真实数据里的参数逻辑k-means和层次聚类处理“团块”很顺手但一旦遇到环形簇、月牙形簇或者带大量噪声的数据它们往往就力不从心。这时候需要密度聚类登场。4.1 DBSCAN的三个点类型与密度可达的直观解释DBSCANDensity-Based Spatial Clustering of Applications with Noise是1996年Ester、Kriegel、Sander和Xu在KDD上提出的经典算法。它的核心是“物以类聚人以密度分”如果一个点周围足够密它就作为一个簇的种子向外扩展。DBSCAN把样本分成三类核心点半径eps邻域内样本数不少于minPts的点边界点位于某个核心点邻域内但自己邻域内样本数不足minPts的点噪声点既不满足核心点条件也不落在任何核心点邻域内的点。直观理解就是核心点是簇的“骨架”边界点是簇的“外沿”噪声点是孤立于所有簇之外的数据。算法通过密度直达、密度可达、密度相连等概念把核心点的邻域关系扩展出去最后形成一个个任意形状的簇。这个设计比原型聚类高明在哪它不需要预先指定簇数量也不假设簇是凸的。只要局部密度足够它可以顺着数据流找到环形簇、月牙形簇。同时它天然能识别噪声点这在带有离群点的真实场景里太重要了。4.2 eps和minPts的选择k距离图与高维失效问题DBSCAN最大的难点是参数eps和minPts。这两个参数决定了“密度”的定义实际调参时有几条经验minPts通常取数据的维度加1dim1如果数据量比较大、噪声多可以取2*dim甚至更大。它越小越容易把边界点算进簇里越大越容易把本来密集但较小的簇当成噪声。eps是个半径阈值变化非常敏感。经验做法是画k距离图对每个样本计算它到第minPts个近邻的距离按距离从大到小排序后画曲线曲线的“拐点”对应的距离就是合适的eps。但我要说句实话k距离图在真实数据里经常没有明显拐点。而且eps是全局阈值如果你的数据不同区域密度差异很大——比如城市POI点云里市中心密集、郊区稀疏——一个全局eps很难同时照顾两边。这时可以考虑用OPTICS。OPTICSAnkerst et al., 1999不要求全局统一的eps它通过计算每个样本的可达距离生成一个“可达距离图”人可以从图上识别出不同密度的簇。HDBSCAN则基于层次密度进一步自动化了簇提取过程工程上表现更省心。还有一个反直觉的点DBSCAN的复杂度对高维数据很不友好。它基于距离计算而高维空间中距离会趋于均匀化所谓“距离集中”现象导致eps变得极其难调。我一般在特征维度超过20时就不太敢直接裸跑DBSCAN要么先做PCA/UMAP降维要么切换到子空间聚类或谱聚类。4.3 欧氏聚类这个名字背后的点云场景KD树、邻域搜索与二次开发“欧氏聚类”这个词经常在点云处理领域出现典型代表是PCLPoint Cloud Library里的pcl::EuclideanClusterExtraction。它的做法沿用了“邻域生长”的思想设定一个空间距离阈值用KD树找每个点的近邻满足距离阈值的近邻点归入同一个簇然后像水波一样扩展直到没有新的近距离点加入。这种聚类和DBSCAN在直觉上很像但它不区分核心点和边界点也缺乏对噪声的正式定义实际中更依赖阈值选择和后处理。如果你在机器人或激光雷达场景里做过障碍物聚类你会深有体会欧氏聚类的“距离阈值”到底设多大直接取决于传感器噪声、目标物体尺寸和点云密度。太大会把两个紧邻物体融成一个簇太小会把同一物体切成好几段。工程上常见的处理是先对点云做体素降采样再设置一个基于经验的距离阈值最后用簇内点数做滤波去掉太小的噪声簇。这套流程虽然简单但在结构化场景中非常稳定。5. 高维困境催生的子空间聚类与谱聚类一张关键论文地图当你面对高维数据比如基因表达矩阵、图像特征向量时前面这些基于全局距离的方法会遇到共同麻烦维度越多所有样本之间的距离越趋于接近聚类结构被淹没在“噪声维度”里。这不是数据错了而是几何特性变了。5.1 高维空间里“距离失效”的直观图景可以这么想在二维平面上你很容易看到两个点是否挨得近但在100维空间里每个样本的坐标分布在几乎每个维度上都有变化两点之间“近”需要所有维度同时近概率极低。结果就是最大距离和最小距离的差距变得很小任何基于距离的划分都变得不稳定。这给聚类带来的影响是全局距离不再可靠簇可能只在某些特征子空间里体现。比如一批用户数据有100个特征但某个人群只在“价格敏感度”和“复购间隔”两个维度上表现出鲜明差异其它98个维度基本是噪声。全局聚类会把这种差异稀释掉但子空间聚类的思路就是“去不同维度找不同结构”。5.2 CLIQUE与子空间聚类按维度网格找簇的早期尝试1998年Agrawal等人发表的CLIQUE是子空间聚类的重要开创性工作之一。它把每个维度划分成网格单元统计每个网格单元里的数据密度然后利用类似Apriori的剪枝策略从低维空间开始搜索哪些维度组合能形成高密度连通区域最终输出既有维度信息又有聚类结果的结构。这个思路在当时的价值是聚类结果不再只是一个标签它同时告诉你“哪些簇是由哪些特征维度支撑的”。后续的SUBCLU、PROCLUS、ORCLUS等方法都在这个方向上演进有的调整搜索策略有的从样本投影的角度迭代优化。不过说实话子空间聚类在工程里用的没那么频繁因为它对网格粒度敏感、参数多而且高维搜索的计算量不可小觑。但如果你的项目核心就是“从几千个特征里找到能定义人群的那几十个特征组合”子空间聚类依然是值得试的路线。5.3 谱聚类从图分割到特征向量的关键建模谱聚类走的是一条完全不同的路。它不直接对样本做距离聚类而是把样本看成图上的节点把相似度看成边的权重然后想把整张图切成几块使得“切掉的总权重最小、块内权重最大”。这就是图割问题。直接做图割是NP难的所以研究者们做了松弛处理。Shi和Malik在2000年发表了Normalized CutNcut论文提出把图割问题转化为求解图拉普拉斯矩阵的特征向量问题然后用这些特征向量作为低维表示。Ng、Jordan和Weiss在2001年进一步给出了一个标准化的谱聚类流程构造相似度矩阵计算拉普拉斯矩阵取前k个特征向量并做行归一化最后对特征向量组成的矩阵跑一次k-means。这里有个非常有趣的数学事实谱聚类本质上是在对图的指示向量做低维松弛而后面那个k-means步骤是为了把松弛后的连续向量离散化成簇标签。所以谱聚类和k-means不是对立关系而是“特征变换 k-means”的配合关系。理解了这一点你就明白为什么谱聚类的参数尤其是相似度矩阵里的带宽gamma对结果影响巨大——gamma太大了每个点都只跟最近的邻居有边图会碎成很多小团gamma太小了所有点之间的边权重都差不多图几乎没法切。在实际项目中我遇到非球形分布、或者数据包含复杂拓扑结构时会优先考虑谱聚类。Sklearn里的SpectralClustering实现很方便但计算量受限于相似度矩阵的O(n^2)存储样本量超过几万就要靠近似方法或把计算放到更小规模数据上。6. 聚类结果落地热点图、趋势图与富集条目联动分析的完整套路最后这部分不说原理纯粹讲实战套路。因为在基因表达、时序观测、用户分群等场景里聚类不是终点聚类之后的可视化和业务解读才是真正交付物。“聚类热图 趋势图 富集条目”这个组合是生物信息学里的标准动作现在也被越来越多行业借鉴。6.1 聚类热图是怎么做的以生物信息学为例聚类热图本质上是一张二维矩阵图行是特征比如基因/蛋白/商品列是样本或时间点颜色深浅代表数值大小。为了让模式更清晰我们通常对行做聚类让表达模式相近的基因靠在一起对列也可以做聚类让样本出现分组。生成热图时我最常用的工具是seaborn.clustermap它内部整合了scipy的层次聚类和树状图一行代码就能输出带行/列树状图的热图import seaborn as sns g sns.clustermap( df_scaled, methodward, metriceuclidean, z_score0, # 按行做z-score标准化 col_clusterTrue, row_clusterTrue, cmapRdBu_r, figsize(8, 10) )注意几个坑一是z_score0表示按行做z-score也就是让每个基因在自己所有样本里的均值为0、标准差为1这样不同表达量的基因才能在同一张图上比较模式而不是比较绝对量。二是聚类用的是什么距离度量默认是欧氏距离但如果你的数据是表达趋势可能用皮尔逊相关系数更合适metriccorrelation。三是行/列聚类顺序由树状图决定不能靠肉眼读行列顺序来推断原始样本顺序。6.2 趋势图与聚类顺序的关联热图之后通常还需要画趋势图。做法是对每个聚类簇计算簇内所有样本/基因在每一个时间点或样本类别的均值并画出均值曲线和置信区间带。这样能清楚看到哪些簇是“随时间持续上升”哪些是“先升后降”哪些是“持续下降”。这一步的关键是热图的行是根据聚类树排列的趋势图用的簇编号必须和热图的簇对应上。很多人在这里翻车——在clustermap里看到簇结构但不知道哪个簇对应哪几行于是自己手动数行数结果完全对不上。更稳的做法是先显式跑linkage fcluster得到每个特征的簇标签再按标签分组计算均值最后单独画热图和趋势图保证了两边的标签映射一致。from scipy.cluster.hierarchy import linkage, fcluster import pandas as pd Z linkage(df_scaled.T.values, methodward) clusters fcluster(Z, t4, criterionmaxclust) df_trend df_scaled.T.groupby(clusters).mean().T # 行时间/样本列簇6.3 富集分析中的聚类条目如何提升可读性生物信息学里聚类后要对每个簇的基因做GO/KEGG富集分析看每个簇富集到了哪些生物学通路。这里最常见的痛点是富集结果列表太长、看不出重点。常规做法是取每个簇内显著富集的前几个条目按p值排序然后画气泡图或条形图并把富集条目横向排列在趋势图旁边。富集条目怎么跟聚类更好联动我的经验是不要只把富集结果贴在最后而是按簇呈现每个簇给出富集方向的关键词比如“簇1免疫应答、炎症信号”“簇2细胞周期、DNA复制”。这样读者先看懂每个簇的生物学身份再去看热图和趋势图很容易形成完整故事。6.4 一个完整的小型pipeline示例我通常是这么组织的数据预处理过滤低表达/低变化基因做log2标准化按行z-score计算距离与聚类用皮尔逊相关系数作为距离Ward法做层次聚类确定簇数结合树状图和业务预期用fcluster切成若干簇绘图clustermap画热图每个簇按时间点画均值趋势图用gseapy或clusterProfiler做富集取每个簇显著条目绘制富集气泡图并排放置热图在上/左趋势图在右富集条目在最右侧用共同簇编号串联。这套pipeline我跑了不止十次每次都会因为“聚类用欧氏距离还是相关系数”之类的问题纠结一阵。我的最终建议是先看数据形态再做两三个实验哪个聚类结果在业务上更可解释就采用哪个。算法是工具业务解读才是项目成败的终点。最后分享一点个人体会聚类技术五十多年发展下来没有哪个算法可以通吃所有问题。k-means适合大规模、球形簇层次聚类适合需要树状图层次结构的中小数据DBSCAN类方法适合任意形状和带噪声的数据子空间聚类和谱聚类则在高维复杂结构里发挥作用。项目开始前花点时间研究数据和需求再决定选用哪条路线远胜于把全部算法无脑跑一遍然后挑一个“看起来最好”的。聚类不仅仅是算法题更是一道需要结合业务理解、数据处理和结果解译的综合题。