
聚类算法大概是机器学习里最看起来简单、做起来全是细节的一类。很多人第一次跑通K-means看到三行代码就把鸢尾花数据分成三堆觉得不过如此等到真正拿真实数据上手才发现簇数怎么定、初始点怎么选、量纲要不要统一、密度聚类为什么把大半数据判成噪声全是坑。这篇就把K-means、层次聚类、密度聚类这三种最常用的聚类方法放在鸢尾花Iris数据集上从头到尾走一遍把每一步背后的逻辑、参数怎么调、结果怎么读讲清楚。不管你是刚学机器学习的学生还是需要拿聚类做数据探索的从业者都能照着复现并且知道每个选择为什么这么做。1. 为什么拿鸢尾花数据练聚类以及聚类到底在解决什么问题1.1 聚类和分类的本质区别别一上来就搞混先把最容易混淆的概念掰开。分类classification是有标签的监督学习模型见过这朵花是山鸢尾、那朵是变色鸢尾学的是从特征到标签的映射。聚类clustering完全相反它拿到的数据没有任何标签目标是根据样本之间的相似度把长得像的样本自动归到一堆里去。换句话说聚类是在数据里找结构而不是学一个已知的答案。这个区别决定了评估方式的不同。分类可以用准确率、召回率这些指标因为有标准答案对照。聚类没有标准答案你只能从簇内是否紧凑、簇间是否分离这种内部指标去判断好坏或者拿已知的真实标签做外部验证鸢尾花恰好有真实标签所以适合做教学演示。我在实际项目里见过太多人拿聚类结果当分类结果用然后困惑为什么准确率这么低——因为聚类本来就不保证簇的编号和真实类别一一对应甚至簇的数量都可能对不上。鸢尾花数据集之所以成为聚类入门的经典原因很实在它只有150个样本、4个特征、3个真实类别规模小到能在几秒内跑完同时又足够真实——其中两个类别versicolor和virginica在特征空间里有重叠不是那种随便分分就开的玩具数据。这让你能真实体会到聚类算法不是万能的这件事。1.2 鸢尾花数据的四个特征和它们的量纲问题鸢尾花数据集包含三个品种setosa、versicolor、virginica每个品种50个样本。四个特征分别是花萼长度sepal length、花萼宽度sepal width、花瓣长度petal length、花瓣宽度petal width单位都是厘米。这里有个新手极易忽略的点这四个特征虽然单位相同但数值范围不一样。花萼长度大概在4.3到7.9之间花瓣宽度只有0.1到2.5。K-means和层次聚类默认用欧氏距离距离计算对数值大的特征更敏感。如果你不做标准化花瓣宽度这个特征几乎会被花萼长度的波动淹没。我实测过不做标准化直接跑K-means在某些随机种子下会把setosa分得很好但另外两类混得一塌糊涂做了标准化之后结果稳定性明显提升。提示距离-based的聚类算法K-means、层次聚类、DBSCAN几乎都要先做标准化。树模型不需要但聚类基本都逃不掉。标准化的做法是把每个特征减均值除标准差Z-score标准化让每个特征均值0、方差1。这样四个特征在距离计算里权重就平等了。当然如果你有业务理由认为某个特征更重要可以手动加权但那是进阶操作入门阶段先老老实实标准化。1.3 三种聚类方法各自适合什么形状的数据这是选型时最该先想清楚的问题。K-means假设簇是球形的、大小差不多、用均值就能代表层次聚类不假设形状但计算量大、对噪声敏感密度聚类DBSCAN能找出任意形状的簇还能识别噪声点但对参数极其敏感。鸢尾花数据在二维可视化下setosa是一坨明显分离的点另外两类是两坨有重叠的点整体偏球形。所以三种方法都能跑出结果但表现会有差异。这个数据集正好能让你对比出K-means在球形簇上快而准层次聚类能给你一棵可解释的树DBSCAN则可能把重叠区域的点判成噪声。理解了数据形状和算法假设的匹配关系你换到真实数据上就不会抓瞎。2. K-means三行代码背后的五个关键决策2.1 K-means的迭代逻辑用找重心来理解K-means的核心思想可以用一句话概括先随便选K个中心点把每个样本分给最近的中心然后重新计算每堆的均值作为新中心反复直到中心不再移动。这个过程叫Lloyd算法是K-means最常用的实现。用生活化的类比假设你要在小区里设K个快递柜先随便放K个位置然后每个住户去最近的柜子取件取完之后你根据所有住户的位置重新调整柜子位置让总距离最短反复调整直到柜子不动了。这个总距离就是K-means优化的目标函数——簇内平方和WCSSWithin-Cluster Sum of Squares。理解这个目标函数很重要因为它是后面选K值的依据。WCSS越小说明簇内越紧凑但K越大WCSS必然越小极端情况每个点自成一簇WCSS为0所以不能单纯追求WCSS最小要找下降变缓的拐点。2.2 标准化和K值选择肘部法和轮廓系数怎么配合用先上标准化代码这是所有后续步骤的前提from sklearn.preprocessing import StandardScaler from sklearn.datasets import load_iris iris load_iris() X iris.data y iris.target scaler StandardScaler() X_scaled scaler.fit_transform(X)选K值我一般两个方法一起看。肘部法Elbow Method是画出不同K值对应的WCSS曲线找拐点from sklearn.cluster import KMeans import matplotlib.pyplot as plt wcss [] for k in range(1, 11): km KMeans(n_clustersk, initk-means, n_init10, random_state42) km.fit(X_scaled) wcss.append(km.inertia_) plt.plot(range(1, 11), wcss, markero) plt.xlabel(K) plt.ylabel(WCSS) plt.show()鸢尾花数据跑出来K3附近曲线明显变缓这和真实类别数一致。但肘部法有个问题拐点有时候不明显尤其真实数据里。这时候用轮廓系数Silhouette Score辅助判断它同时考虑簇内紧凑度和簇间分离度取值-1到1越接近1越好from sklearn.metrics import silhouette_score for k in range(2, 11): km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels) print(fK{k}, Silhouette{score:.3f})我的经验是肘部法给个大致范围轮廓系数在范围内挑最优。两个方法结论冲突时优先信轮廓系数因为它对簇的形状和分离度更敏感。但最终拍板还得结合业务——如果业务上明确知道应该分几类那就别纠结指标了。2.3 init参数为什么k-means比随机初始化靠谱K-means对初始中心点很敏感随机初始化可能收敛到局部最优。经典例子是三个真实簇随机初始化把两个中心点都扔进同一个簇里最后结果就废了。k-means的做法是第一个中心随机选后续每个中心选离已选中心最远的点按距离概率加权这样初始点天然分散收敛质量高很多。sklearn从1.0版本开始init默认就是k-means但老代码里经常能看到initrandom。我建议除非你在做算法对比实验否则一律用k-means。另外n_init参数控制用不同初始点跑几次取最优老版本默认10新版本1.4改成了auto。为了结果可复现我习惯显式写n_init10并固定random_state。2.4 结果解读混淆矩阵和簇标签对齐的坑跑完K-means怎么知道分得好不好鸢尾花有真实标签可以做个交叉表看看import pandas as pd km KMeans(n_clusters3, initk-means, n_init10, random_state42) labels km.fit_predict(X_scaled) print(pd.crosstab(y, labels))你会看到类似这样的结果setosa标签0几乎全部落在一个簇里versicolor和virginica有部分交叉。这里有个大坑簇的编号是随机的和真实类别编号没有任何对应关系。第一次跑可能簇0对应setosa换个随机种子可能簇0对应virginica。所以你不能直接拿labels y算准确率必须先做标签对齐用匈牙利算法或者手动映射。我一般用scipy.optimize.linear_sum_assignment做最优匹配或者简单点看交叉表手动映射。这个坑我在带新人的时候见过无数次有人兴冲冲报告准确率只有30%一看是把簇编号当类别编号了。2.5 K-means的硬伤它假设不了的那些事K-means有三个绕不过去的假设簇是球形的、簇大小相近、簇密度相近。鸢尾花数据基本满足所以效果好。但换成环形数据、月牙形数据K-means就彻底歇菜——它会把一个环形硬切成几块。另外K-means对异常值敏感因为均值会被极端值拉偏。一个远离所有簇的离群点能把整个簇的中心拽过去。实际项目里如果数据有噪声要么先清洗要么换用K-medoids用中位数代替均值。这些限制不是K-means的bug是它的设计前提用之前先确认数据符不符合。3. 层次聚类不预设簇数用一棵树看清数据的分层结构3.1 凝聚式层次聚类的合并逻辑层次聚类分两种凝聚式自底向上和分裂式自顶向下。实际用得最多的是凝聚式逻辑是一开始每个样本自成一簇然后每次找最近的两个簇合并直到所有样本合成一簇。整个过程形成一棵树状图dendrogram你在任意高度切一刀就得到对应数量的簇。这个最近怎么定义就是链接准则linkage的问题直接决定聚类结果single单链接两个簇中最近的两个点之间的距离。容易产生链式效应把一条长链上的点都归成一簇。complete全链接两个簇中最远的两个点之间的距离。倾向于产生紧凑的簇但对异常值敏感。average平均链接所有点对距离的平均。折中方案比较稳健。ward离差平方和合并后簇内方差增加最小的两个簇。这是sklearn的默认值也是实践中最常用的因为它倾向于产生大小相近的紧凑簇。我实测鸢尾花数据ward和average效果都不错single会把setosa和另外两类通过几个边界点连起来效果明显差。选linkage没有绝对标准但ward是安全的默认起点。3.2 树状图怎么读切在哪里有讲究画树状图是层次聚类最直观的部分from scipy.cluster.hierarchy import dendrogram, linkage import matplotlib.pyplot as plt Z linkage(X_scaled, methodward) plt.figure(figsize(12, 6)) dendrogram(Z) plt.xlabel(Sample Index) plt.ylabel(Distance) plt.show()读树状图的关键是看合并高度。两个簇在很高的位置才合并说明它们差异大在很低的位置合并说明很相似。你要找的是那种某次合并高度突然跳升的位置在跳升之前切一刀簇数就定下来了。鸢尾花的ward树状图在距离大概10左右有个明显跳升切在这里得到3个簇和真实类别吻合。但要注意树状图在样本量大时会糊成一团150个样本还能看上千个样本就得靠颜色标记或者只看前几层。3.3 用fcluster切树以及和K-means结果的对比切树用fcluster函数可以按距离阈值切也可以直接指定簇数from scipy.cluster.hierarchy import fcluster labels_hc fcluster(Z, t3, criterionmaxclust) print(pd.crosstab(y, labels_hc))criterionmaxclust表示按最大簇数切t3就是要3个簇。也可以设criteriondistance按距离阈值切这时候t是距离值。对比K-means和层次聚类在鸢尾花上的结果两者对setosa的划分几乎一致差异主要在versicolor和virginica的重叠区域。层次聚类的ward方法因为优化的是方差和K-means的目标其实很像所以结果接近不奇怪。但层次聚类有个K-means没有的好处它一次性给出所有可能的簇数对应的结果你不需要反复跑。代价是计算复杂度O(n²)甚至O(n³)样本上万就基本跑不动了。3.4 层次聚类的适用边界和性能陷阱层次聚类最大的优势是不需要预设簇数而且树状图提供了完整的层次结构信息这在做数据探索时非常有用——你能看到数据是怎么一层层聚合的。但它的短板也很明显第一计算和内存开销大。需要计算并存储所有样本对的距离矩阵150个样本是150×15010000个样本就是10000×10000内存直接爆掉。所以层次聚类基本只适合小数据集几千以内。第二一旦合并就不能撤销。凝聚式是贪心算法早期合并错了后面没法修正。这也是它不如K-means灵活的地方。第三对噪声和异常值敏感尤其是single和complete链接。ward相对好一些但也不是免疫。我的建议是层次聚类用来做探索和小数据集的最终聚类大数据集先用它采样看看结构再决定用K-means还是DBSCAN。4. 密度聚类DBSCAN能找任意形状但参数是门玄学4.1 DBSCAN的两个核心参数eps和min_samplesDBSCANDensity-Based Spatial Clustering of Applications with Noise的思路和前面两个完全不同它不看距离远近看密度。核心概念有三个核心点core point在半径eps内至少有min_samples个点包括自己的样本。边界点border point自己不是核心点但落在某个核心点的eps邻域内。噪声点noise point既不是核心点也不是边界点最终被标记为-1。算法从一个核心点出发把密度可达的所有点归成一簇然后换下一个未访问的核心点直到所有点处理完。这样能找出任意形状的簇因为它是沿着密度连通的区域扩展的不受球形限制。两个参数里eps是半径min_samples是密度阈值。eps太小大部分点都成噪声eps太大所有点挤成一簇。min_samples一般取特征数1起步鸢尾花4个特征可以从5开始试。4.2 用k-距离图确定eps比瞎试靠谱eps怎么定最常用的方法是k-距离图k-distance graph。对每个点算它到第k个最近邻的距离k一般取min_samples把这些距离从小到大排序画出来找曲线的肘部那个位置对应的距离就是合适的epsfrom sklearn.neighbors import NearestNeighbors import numpy as np k 5 nbrs NearestNeighbors(n_neighborsk).fit(X_scaled) distances, indices nbrs.kneighbors(X_scaled) distances np.sort(distances[:, k-1], axis0) plt.plot(distances) plt.xlabel(Points sorted by distance) plt.ylabel(f{k}-th nearest neighbor distance) plt.show()鸢尾花标准化后k-距离图的肘部大概在0.5到0.8之间。我试过eps0.5、min_samples5结果是把setosa单独分出来另外两类合并还有少量噪声点。eps调到0.7噪声减少但两类还是分不太开。这其实反映了DBSCAN在鸢尾花上的一个现实versicolor和virginica在特征空间里密度连成一片DBSCAN没法靠密度把它们分开。4.3 DBSCAN在鸢尾花上的实测结果与噪声处理跑一下看看from sklearn.cluster import DBSCAN db DBSCAN(eps0.6, min_samples5) labels_db db.fit_predict(X_scaled) print(pd.crosstab(y, labels_db)) print(f噪声点数量: {(labels_db -1).sum()})典型结果是setosa对应一个簇versicolor和virginica大部分落进另一个簇还有十几个点被标成-1噪声。这个结果不好看但它恰恰说明了DBSCAN的特性——它不会强行把密度连通的区域切开宁可判成噪声也不乱分。处理噪声点有几种策略一是直接剔除后续分析不考虑二是把噪声点分配给最近的核心点所在簇三是调整参数让噪声减少。我一般先看噪声比例如果超过10%就要警惕可能是eps太小或者数据本身不适合密度聚类。鸢尾花这个例子噪声比例大概8%左右属于可接受范围。4.4 密度聚类的真正用武之地非球形和含噪数据DBSCAN在鸢尾花上表现一般不代表它没用。它的主场是那些K-means搞不定的场景地理空间数据里的热点区域识别、异常检测、图像分割里的连通区域。比如你要从GPS轨迹里找出用户经常停留的区域这些区域形状不规则、大小不一K-means会切得乱七八糟DBSCAN就能沿着密度自然地把停留点聚起来还把路上的移动点判成噪声。用DBSCAN前先问自己两个问题数据里有没有明显的密度差异簇是不是非球形如果两个都是是DBSCAN值得一试。如果数据是均匀的球形簇那还是K-means更省心。另外DBSCAN对参数太敏感同一份数据eps差0.1结果可能天差地别所以k-距离图那步不能省。5. 三种方法横向对比同一份数据三种视角5.1 用统一指标对比聚类质量把三种方法的结果放一起对比用调整兰德指数ARI和轮廓系数两个指标。ARI衡量聚类结果和真实标签的一致性取值-1到11表示完全一致from sklearn.metrics import adjusted_rand_score, silhouette_score results { K-means: labels, Hierarchical: labels_hc, DBSCAN: labels_db } for name, lbl in results.items(): # DBSCAN有噪声点算轮廓系数时要排除 mask lbl ! -1 if len(set(lbl[mask])) 1: sil silhouette_score(X_scaled[mask], lbl[mask]) else: sil float(nan) ari adjusted_rand_score(y, lbl) print(f{name}: ARI{ari:.3f}, Silhouette{sil:.3f})鸢尾花上典型结果K-means和层次聚类的ARI都在0.6-0.7左右DBSCAN因为把两类合并了ARI会低一些0.5左右。轮廓系数K-means和层次聚类接近DBSCAN因为噪声点被排除剩下的点反而更紧凑轮廓系数可能不低但这有作弊嫌疑——把难分的点扔掉了。5.2 一张表看清三种方法的取舍维度K-means层次聚类DBSCAN是否需要预设簇数是否否簇形状假设球形无任意形状对噪声敏感度高中高低能识别噪声计算复杂度O(n·k·t)O(n²)~O(n³)O(n log n)有索引时适合数据规模大小中结果可解释性中高树状图中主要参数K、initlinkage、切树位置eps、min_samples这张表是我选型时的第一参考。实际项目里如果数据量大、簇偏球形直接K-means如果数据小、想探索层次结构用层次聚类如果有噪声、簇形状不规则上DBSCAN。没有哪个算法全面胜出只有匹不匹配。5.3 可视化对比二维投影下的三种划分鸢尾花有4个特征画图得先降维。用PCA降到2维from sklearn.decomposition import PCA pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) fig, axes plt.subplots(1, 3, figsize(15, 4)) for ax, (name, lbl) in zip(axes, results.items()): ax.scatter(X_pca[:, 0], X_pca[:, 1], clbl, cmapviridis, s30) ax.set_title(name) plt.show()看这三张图你能直观感受到K-means和层次聚类的划分很像都是三块DBSCAN是两块加一堆散落的噪声点。这个可视化对比比任何文字描述都有说服力建议你自己跑一遍亲眼看看差异。5.4 从鸢尾花到真实项目选型决策的思考路径鸢尾花是教学数据真实项目里情况复杂得多。我总结的选型路径是这样的第一步先看数据规模和维度。样本上万层次聚类直接排除维度很高几百上千所有基于距离的算法都要先降维否则距离度量失效。第二步看簇的形状和密度。画个散点图或者用t-SNE降维看看如果簇明显非球形K-means排除如果密度差异大DBSCAN要谨慎调参。第三步看有没有噪声。数据脏、离群点多DBSCAN的噪声识别能力是加分项数据干净K-means更简单直接。第四步看业务对簇数的要求。如果业务明确要分3类K-means直接设K3最省事如果不知道分几类层次聚类或DBSCAN能帮你探索。这套路径不是死的但能帮你快速缩小选择范围避免在错误的算法上浪费时间。6. 实操中那些文档不会告诉你的坑6.1 随机种子不固定结果每次都不一样K-means和k-means都涉及随机初始化不设random_state每次跑结果都可能不同。这在调试阶段特别坑——你改了个参数以为是参数起作用了其实是随机种子变了。我吃过这个亏后来养成习惯所有涉及随机的步骤一律固定random_state42等最终确定方案了再换几个种子验证稳定性。层次聚类本身是确定性的给定linkage和距离度量但如果你在它前面做了PCA降维PCA在某些实现里也有随机性比如用随机SVD同样要固定种子。DBSCAN是确定性的不受种子影响这点比较省心。6.2 标准化做在划分训练测试集之前还是之后聚类通常没有训练测试集之分但如果你要做半监督或者拿聚类结果喂给下游模型就会涉及数据划分。这时候标准化的fit只能在训练集上做然后transform到测试集否则会有数据泄露。这个坑在分类任务里被讲烂了但聚类场景下很多人觉得反正没标签泄露无所谓——其实一样有影响测试集的均值和方差信息泄露进了标准化参数里评估会偏乐观。6.3 高维数据下距离度量失效先降维再聚类鸢尾花只有4维距离度量还好用。维度一高比如文本TF-IDF动辄几千维欧氏距离会变得没有区分度——所有点对的距离都趋近于同一个值这叫维度灾难。这时候要么先PCA降维要么换余弦距离要么用专门的高维聚类方法。我处理文本聚类时一般先TF-IDF再TruncatedSVD降到100-300维然后才上K-means效果比直接在高维跑好很多。6.4 聚类结果不稳定时先检查这几件事结果忽好忽坏按这个顺序排查第一标准化做了没第二随机种子固定了没第三K值或eps是不是在临界点上稍微动一点结果剧变说明参数不稳第四数据里有没有极端异常值第五特征之间是不是高度相关相关特征相当于给某个方向加了权重会扭曲距离。这五条覆盖了我遇到的大部分玄学问题。6.5 别用聚类准确率骗自己内部指标和业务验证都要看最后说个心态问题。聚类没有标准答案拿ARI、轮廓系数这些指标能说明一部分问题但不能全信。我见过轮廓系数很高但业务上毫无意义的聚类——算法把数据按某个无关特征分得很开指标好看但没用。真正靠谱的做法是指标给个参考最终一定要拿业务逻辑去验证。比如分出来的簇每个簇的样本在业务上有没有共同特征能不能解释解释不通的簇指标再高也要打问号。鸢尾花这个练习价值不在于把三种算法跑通而在于让你建立算法假设要和数据特性匹配这个意识。等你在真实项目里面对一份没有标签的数据能快速判断该用哪种方法、该调哪些参数、结果该怎么验证这个练习的目的就达到了。我个人在实际操作中的体会是聚类最花时间的从来不是调包跑代码而是理解数据、验证结果、和业务对齐这三件事代码本身反而是最简单的部分。