☰
K-means、层次聚类与DBSCAN在鸢尾花数据集上的实战对比
2026/9/27 2:25:52 网站建设 项目流程

聚类算法大概是机器学习里最"看起来简单、做起来全是细节"的一类。很多人第一次跑通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优化的目标函数——簇内平方和(WCSS,Within-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_clusters=k, init='k-means++', n_init=10, random_state=42) km.fit(X_scaled) wcss.append(km.inertia_) plt.plot(range(1, 11), wcss, marker='o') plt.xlabel('K') plt.ylabel('WCSS') plt.show()

鸢尾花数据跑出来,K=3附近曲线明显变缓,这和真实类别数一致。但肘部法有个问题:拐点有时候不明显,尤其真实数据里。这时候用轮廓系数(Silhouette Score)辅助判断,它同时考虑簇内紧凑度和簇间分离度,取值-1到1,越接近1越好:

from sklearn.metrics import silhouette_score for k in range(2, 11): km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_scaled) score = silhouette_score(X_scaled, labels) print(f"K={k}, Silhouette={score:.3f}")

我的经验是:肘部法给个大致范围,轮廓系数在范围内挑最优。两个方法结论冲突时,优先信轮廓系数,因为它对簇的形状和分离度更敏感。但最终拍板还得结合业务——如果业务上明确知道应该分几类,那就别纠结指标了。

2.3 init参数:为什么k-means++比随机初始化靠谱

K-means对初始中心点很敏感,随机初始化可能收敛到局部最优。经典例子是三个真实簇,随机初始化把两个中心点都扔进同一个簇里,最后结果就废了。k-means++的做法是:第一个中心随机选,后续每个中心选离已选中心最远的点(按距离概率加权),这样初始点天然分散,收敛质量高很多。

sklearn从1.0版本开始,init默认就是'k-means++',但老代码里经常能看到init='random'。我建议除非你在做算法对比实验,否则一律用k-means++。另外n_init参数控制用不同初始点跑几次取最优,老版本默认10,新版本(1.4+)改成了'auto'。为了结果可复现,我习惯显式写n_init=10并固定random_state。

2.4 结果解读:混淆矩阵和簇标签对齐的坑

跑完K-means,怎么知道分得好不好?鸢尾花有真实标签,可以做个交叉表看看:

import pandas as pd km = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42) 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, method='ward') 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, t=3, criterion='maxclust') print(pd.crosstab(y, labels_hc))

criterion='maxclust'表示按最大簇数切,t=3就是要3个簇。也可以设criterion='distance'按距离阈值切,这时候t是距离值。

对比K-means和层次聚类在鸢尾花上的结果,两者对setosa的划分几乎一致,差异主要在versicolor和virginica的重叠区域。层次聚类的ward方法因为优化的是方差,和K-means的目标其实很像,所以结果接近不奇怪。但层次聚类有个K-means没有的好处:它一次性给出所有可能的簇数对应的结果,你不需要反复跑。代价是计算复杂度O(n²)甚至O(n³),样本上万就基本跑不动了。

3.4 层次聚类的适用边界和性能陷阱

层次聚类最大的优势是不需要预设簇数,而且树状图提供了完整的层次结构信息,这在做数据探索时非常有用——你能看到数据是怎么一层层聚合的。但它的短板也很明显:

第一,计算和内存开销大。需要计算并存储所有样本对的距离矩阵,150个样本是150×150,10000个样本就是10000×10000,内存直接爆掉。所以层次聚类基本只适合小数据集(几千以内)。

第二,一旦合并就不能撤销。凝聚式是贪心算法,早期合并错了,后面没法修正。这也是它不如K-means灵活的地方。

第三,对噪声和异常值敏感,尤其是single和complete链接。ward相对好一些,但也不是免疫。

我的建议是:层次聚类用来做探索和小数据集的最终聚类,大数据集先用它采样看看结构,再决定用K-means还是DBSCAN。

4. 密度聚类DBSCAN:能找任意形状,但参数是门玄学

4.1 DBSCAN的两个核心参数:eps和min_samples

DBSCAN(Density-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),把这些距离从小到大排序画出来,找曲线的"肘部",那个位置对应的距离就是合适的eps:

from sklearn.neighbors import NearestNeighbors import numpy as np k = 5 nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled) distances, indices = nbrs.kneighbors(X_scaled) distances = np.sort(distances[:, k-1], axis=0) plt.plot(distances) plt.xlabel('Points sorted by distance') plt.ylabel(f'{k}-th nearest neighbor distance') plt.show()

鸢尾花标准化后,k-距离图的肘部大概在0.5到0.8之间。我试过eps=0.5、min_samples=5,结果是把setosa单独分出来,另外两类合并,还有少量噪声点。eps调到0.7,噪声减少但两类还是分不太开。这其实反映了DBSCAN在鸢尾花上的一个现实:versicolor和virginica在特征空间里密度连成一片,DBSCAN没法靠密度把它们分开。

4.3 DBSCAN在鸢尾花上的实测结果与噪声处理

跑一下看看:

from sklearn.cluster import DBSCAN db = DBSCAN(eps=0.6, min_samples=5) 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到1,1表示完全一致:

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_components=2) 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], c=lbl, cmap='viridis', s=30) ax.set_title(name) plt.show()

看这三张图,你能直观感受到:K-means和层次聚类的划分很像,都是三块;DBSCAN是两块加一堆散落的噪声点。这个可视化对比比任何文字描述都有说服力,建议你自己跑一遍,亲眼看看差异。

5.4 从鸢尾花到真实项目:选型决策的思考路径

鸢尾花是教学数据,真实项目里情况复杂得多。我总结的选型路径是这样的:

第一步,先看数据规模和维度。样本上万,层次聚类直接排除;维度很高(几百上千),所有基于距离的算法都要先降维,否则距离度量失效。

第二步,看簇的形状和密度。画个散点图或者用t-SNE降维看看,如果簇明显非球形,K-means排除;如果密度差异大,DBSCAN要谨慎调参。

第三步,看有没有噪声。数据脏、离群点多,DBSCAN的噪声识别能力是加分项;数据干净,K-means更简单直接。

第四步,看业务对簇数的要求。如果业务明确要分3类,K-means直接设K=3最省事;如果不知道分几类,层次聚类或DBSCAN能帮你探索。

这套路径不是死的,但能帮你快速缩小选择范围,避免在错误的算法上浪费时间。

6. 实操中那些文档不会告诉你的坑

6.1 随机种子不固定,结果每次都不一样

K-means和k-means++都涉及随机初始化,不设random_state,每次跑结果都可能不同。这在调试阶段特别坑——你改了个参数,以为是参数起作用了,其实是随机种子变了。我吃过这个亏,后来养成习惯:所有涉及随机的步骤,一律固定random_state=42,等最终确定方案了再换几个种子验证稳定性。

层次聚类本身是确定性的(给定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、轮廓系数这些指标能说明一部分问题,但不能全信。我见过轮廓系数很高但业务上毫无意义的聚类——算法把数据按某个无关特征分得很开,指标好看,但没用。真正靠谱的做法是:指标给个参考,最终一定要拿业务逻辑去验证。比如分出来的簇,每个簇的样本在业务上有没有共同特征?能不能解释?解释不通的簇,指标再高也要打问号。

鸢尾花这个练习,价值不在于把三种算法跑通,而在于让你建立"算法假设要和数据特性匹配"这个意识。等你在真实项目里面对一份没有标签的数据,能快速判断该用哪种方法、该调哪些参数、结果该怎么验证,这个练习的目的就达到了。我个人在实际操作中的体会是,聚类最花时间的从来不是调包跑代码,而是理解数据、验证结果、和业务对齐这三件事,代码本身反而是最简单的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询