☰
K-means vs 层次聚类 vs DBSCAN:鸢尾花数据集聚类对比实战
2026/10/14 13:27:05 网站建设 项目流程

1. 为什么拿鸢尾花做聚类对比

做聚类分析选数据集,鸢尾花几乎是绕不开的入门选择。这份数据集规模适中——150个样本、4个特征、3个类别,既不会因为数据量太大而掩盖算法本身的差异,也不会因为样本太少导致结论失真。更关键的是,它天然带有"部分类别线性可分、部分类别相互重叠"的结构特点:其中一类与其他两类在特征空间中的距离非常远,而另外两类在部分特征维度上存在明显交叉。这种结构正是检验聚类算法真实水平的试金石。

拿它同时跑K-均值、层次聚类和DBSCAN,目的不是为了证明哪个算法"更厉害",而是通过同一份数据,看清三类算法在聚类逻辑上的本质差异:K-均值靠距离分配和中心迭代,层次聚类靠合并策略构建树状结构,DBSCAN靠密度连通性识别簇和噪声。同一份数据,三种视角,得到的结果不尽相同,背后原因正是它们对"簇到底是什么"的假设不同。

这篇文章不是理论文档,是我实际跑完整个对比实验后的完整记录。内容包括每个算法的核心逻辑、代码实现、参数选择过程、结果对比和踩坑经验。无论你是正在学多元统计分析的学生,还是刚接触聚类分析的数据分析从业者,都可以照着这份记录完整复现一遍,重点在于理解每个算法在相同数据上的表现差异,以及这些差异背后的数据原因。

2. 数据集分析:在跑算法前先搞懂鸢尾花的结构

2.1 数据基本情况与特征量纲问题

鸢尾花数据集由四个数值型特征组成:花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位都是厘米。表面看量纲一致,但实际分布范围差异很大——花萼宽度主要集中在2到4.4之间,而花瓣长度可以从1.0一直延伸到6.9。这个差异直接决定了预处理时必须做标准化,否则后文提到的K-均值和层次聚类都会被花萼宽度和花瓣长度之间的数值范围差异带偏。

用Python加载数据后,我通常先跑一行describe()看分布,再手动观察几个极端值。这个习惯帮我避过不少坑——尤其是DBSCAN这种对尺度极其敏感的算法,原始尺度下和标准化后的结果可能完全不同。一个经验是:做聚类前先把describe()的输出打印出来,花两分钟理解每个特征的取值范围和分布形状。别急着建模,数据理解到位了,后面的参数调整会顺畅很多。

2.2 三类样本的真实分布结构

鸢尾花的三个类别中,第一类与其他两类在四个特征维度上都有非常明显的区分度,基本上只需任意两个特征就能将它完全分离。真正的难点在第二类和第三类:如果只用花萼长度和花萼宽度这两个特征,它们的分布几乎完全重叠;换用花瓣长度和花瓣宽度后,虽然重叠区域缩小了一些,但仍有一部分样本的特征值交错在一起,不存在一条干净的线性分界线。

这个结构特性直接影响三个算法的表现:K-均值对球形簇效果较好,能轻松分出第一类,但第二类和第三类的边界会因为重叠而出现切分偏差;层次聚类中的Ward方法在特征空间中的表现与K-均值有相似之处,但合并策略会导致不同的边界划分;DBSCAN则完全依赖密度分布,遇到密度均匀且相互连接的重叠区域时,很可能直接把它们判为同一个簇。

2.3 为什么标准化是这一步的必选项

做聚类对比实验时,关于"要不要标准化"的争论不少。我的结论很直接:要标准化,而且必须做。核心原因是K-均值和层次聚类(尤其是基于欧氏距离的版本)都依赖样本间的距离计算,一旦某个特征的数值范围天然更大,它在距离计算中的权重就会被无形放大。

以鸢尾花数据集为例,花瓣长度范围大约是1到6.9,花萼宽度范围大约是2到4.4。如果不做标准化,两个样本花瓣长度相差1厘米和花萼宽度相差1厘米,对距离的贡献几乎一样——但实际意义完全不同。标准化后的每个特征均值为0、标准差为1,各特征在距离计算中的贡献相对均衡。

我一般首选StandardScaler而不是MinMaxScaler,原因是标准化对离群值的敏感度更低,且与PCA、K-均值等基于方差的算法配合更自然。一个注意事项是标准化要在划分数据之前对整体进行,避免引入信息泄漏的问题。

3. 三个聚类算法的核心逻辑差异

3.1 K-均值:基于中心的距离分配

K-均值的聚类逻辑概括为四个字:迭代分配。它预先指定簇个数K,随机初始化K个中心点,然后反复执行两个操作——把每个样本分配给离它最近的中心点,再根据每个簇内的样本重新计算中心位置。循环往复,直到中心点不再变化或达到最大迭代次数。

这个算法有一个非常重要的内在假设:簇的形状是凸的、近似球形的,且簇的规模差异不应太大。因为K-均值用均方误差来衡量聚类效果,本质上是在寻找一种划分,使得每个样本到各自簇中心的距离平方和最小。它天然倾向于把特征空间切分成大小类似的球形区域,这恰恰与鸢尾花数据集中第二类和第三类的重叠分布产生了冲突——两部分样本形状上不是标准球形,且密度分布不均匀。

实操中还有两个细节需要关注。第一是初始中心点的选择,kmeans默认采用KMeans++策略,大幅减少了随机初始化带来的不稳定;第二是n_init参数,表示在不同随机中心下运行算法的次数,最终取效果最优的一次。初学者常犯的错误是默认参数直接跑一遍就下结论,其实应该结合多个随机种子,观察聚类结果是否稳定。

3.2 层次聚类:持续合并的树状结构

层次聚类(凝聚式)走的是另一条路线:一开始把每个样本都当作一个独立的簇,然后反复合并距离最近的两个簇,直到所有样本归入同一个簇为止。整个过程形成一棵树状图(树状图),你可以在任意高度"切一刀",得到不同粒度的聚类结果。

关键在于簇间距离的定义方式。单连接(最小距离)关注两个簇中最近样本对的距离,容易导致长条状或链式结构的簇;全连接(最大距离)关注最远样本对,倾向于生成紧凑的球形簇;平均连接取所有样本对距离的平均值,介于两者之间;Ward方法则衡量合并后簇内离差平方和的增量,目标是最小化合并带来的信息损失。

在实际使用中,Ward方法因为与K-均值的优化目标相近,经常能产生较好的聚类效果,尤其适合处理鸢尾花这类样品量适中的数据。层次聚类最大的优势是不需要预先指定K值,你可以通过树状图直观地判断合理的簇数量;缺点是时间复杂度较高,样本量大时计算很吃力,但150个样本完全不是问题。

3.3 DBSCAN:密度连通性的簇识别

DBSCAN与前两种算法是两个维度的思路。它不关心簇的中心在哪里,也不管样本之间的距离累积形态,而是通过两个参数定义密度:邻域半径Eps和最小邻域样本数MinPts。任意一个点,如果它的Eps邻域内包含至少MinPts个样本,就被标记为核心点;核心点通过密度直达和密度可达关系连成簇;不在任何核心点邻域内的样本则被标为噪声点。

这种机制的优点非常鲜明:不需要预先指定簇的数量,能识别任意形状的簇,还能挑出离群点。但它有两个明显的软肋:一是对参数Eps和MinPts极其敏感,参数稍不合适结果就完全变样;二是当数据集中存在密度差异较大的簇时,全局统一的密度阈值无法同时适应所有簇——密度大的簇被合并,密度小的簇被拆散或被当成噪声。

放在鸢尾花数据集上,这意味着DBSCAN可能会把密度均匀且连成一体的第二类和第三类识别为一个簇,同时只把与这两类距离较远的第一类单独分离出来。这不是算法缺陷,而是密度聚类对数据结构的一种诚实反映——当两个类在特征空间中密度连续相接时,它确实无法给出与真实标签一致的结果。

3.4 三种算法的适用场景对比

三种算法没有绝对的优劣,只有适合与不适合。K-均值适合数据量大、簇形状近似球形、事先能大致预估簇数目的场景,比如用户分群、图像分割;层次聚类适合样本量适中、需要可视化展示聚类过程和层级关系的场景,比如基因表达谱分析、文本主题层级归纳;DBSCAN适合簇形状不规则、存在明显噪声、需要自动发现簇数量的场景,比如空间数据聚类、异常点检测。

从对鸢尾花数据集的预期表现来看,三者大概率都能把第一类完美分出来,真正的分歧会出现在第二类和第三类的归属上。这正是对比实验最有价值的部分:不去人为评判谁的准确率更高,而是通过观察每个算法对重叠区域的切分方式,反推它的先验假设和适用边界。

4. 实操过程:三算法代码实现全流程

4.1 准备工作与环境依赖

整个实验用Python完成,依赖库包括numpy、pandas、scikit-learn、matplotlib,以及用于绘制树状图的scipy.cluster.hierarchy。安装命令非常简单,一行pip install numpy pandas scikit-learn matplotlib scipy即可。建议在Jupyter Notebook或JupyterLab环境中执行,方便分步查看结果和图形。

这个项目的代码量不大,但流程完整,可以作为任何聚类对比实验的模板。核心流程分为五步:数据加载、特征标准化、训练各聚类模型、可视化对比、指标评价。每一步的目的和分析点我会在下面的代码注释中展开说明。

4.2 数据加载与标准化

import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_iris from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN from sklearn.decomposition import PCA from sklearn.metrics import silhouette_score, adjusted_rand_score, normalized_mutual_info_score from scipy.cluster.hierarchy import dendrogram, linkage iris = load_iris() X = iris.data y_true = iris.target print("特征矩阵形状:", X.shape) print("特征名:", iris.feature_names) print("类别标签:", np.unique(y_true))

标准化的选择我在前文已经解释过,直接用StandardScaler处理,并将结果转换为DataFrame方便查看:

scaler = StandardScaler() X_scaled = scaler.fit_transform(X) df = pd.DataFrame(X_scaled, columns=iris.feature_names) print(df.describe().round(2))

标准化后的各特征均值为0,标准差接近1,说明量纲影响已经消除。我在这一步会顺手用PCA把四维数据降到二维用于可视化,但注意PCA降维只是为方便展示,聚类计算仍然在原始四维标准化空间中进行。

pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) plt.figure(figsize=(8, 5)) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_true, cmap='viridis', edgecolor='white', s=50) plt.title('真实标签的PCA可视化') plt.xlabel('第一主成分') plt.ylabel('第二主成分') plt.grid(alpha=0.3) plt.show()

运行后会看到第一类在左下角明显分离,第二类与第三类在右侧区域出现交叠。这个可视化是你理解后续聚类结果的重要参照,建议保留这张图,与后面三张聚类结果图对比着看。

4.3 K-均值:肘部法则确定K值

K-均值的第一步是指定K,但通常我们不知道数据有几类。此时可以通过肘部法则和轮廓系数两个工具辅助判断。

肘部法则的思路很简单:遍历一系列K值,计算每个K值对应的簇内误差平方和(SSE),随着K增大,SSE会持续下降,但下降速度会在某个点明显放缓,这个拐点就像手臂的肘部,即为合理的K值。轮廓系数则综合考虑了簇内紧密度和簇间分离度,取值在-1到1之间,越接近1说明聚类效果越好。

sse = [] silhouette_scores = [] K_range = range(2, 10) for k in K_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_scaled) sse.append(km.inertia_) silhouette_scores.append(silhouette_score(X_scaled, labels)) plt.subplot(1, 2, 1) plt.plot(K_range, sse, marker='o') plt.xlabel('K值') plt.ylabel('簇内误差平方和 (SSE)') plt.title('肘部法则') plt.grid(alpha=0.3) plt.subplot(1, 2, 2) plt.plot(K_range, silhouette_scores, marker='o') plt.xlabel('K值') plt.ylabel('轮廓系数') plt.title('轮廓系数曲线') plt.grid(alpha=0.3) plt.tight_layout() plt.show()

从结果来看,SSE曲线在K等于3处出现明显拐点,轮廓系数也在K等于3时达到峰值。这与鸢尾花数据集的真实类别数一致,说明这份数据确实存在清晰的三簇结构。

确定K值后,运行最终模型:

kmeans = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42) y_kmeans = kmeans.fit_predict(X_scaled) plt.figure(figsize=(8, 5)) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_kmeans, cmap='Set2', edgecolor='white', s=50) plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], c='red', marker='X', s=120, label='簇中心') plt.title('K-均值聚类结果(PCA降维展示)') plt.xlabel('第一主成分') plt.ylabel('第二主成分') plt.legend() plt.grid(alpha=0.3) plt.show()

注意簇中心坐标是在标准化后的四维空间里计算的,这里直接绘制中心的x和y坐标位置有偏差,因为PCA的坐标系和原始特征空间不是同一回事。严格来说需要先对中心点做PCA变换再绘制,但如果你只是目视判断聚类分界趋势,这种近似展示完全够用。

4.4 层次聚类:树状图定粒度

层次聚类的优势在于不需要反复扫描参数,直接构建完整的层次树,再根据树状图判断划分粒度。

Z = linkage(X_scaled, method='ward') plt.figure(figsize=(12, 6)) dendrogram(Z, truncate_mode='level', p=5) plt.title('Ward方法的层次聚类树状图') plt.xlabel('样本序号') plt.ylabel('距离') plt.show()

树状图的解读重点在于看合并线的长度。距离越大处合并,说明两簇之间的差异越明显。在这份数据上,你会看到树状图顶部有三条很长的垂直线,几乎在同一高度分叉,这强烈提示最优分簇数是3。如果你想更精确一点,可以自动计算不同切分深度下的轮廓系数,但目测高度差异较大的分叉点通常就够了。

确定簇数后,用AgglomerativeClustering完成聚类:

agg = AgglomerativeClustering(n_clusters=3, metric='euclidean', linkage='ward') y_agg = agg.fit_predict(X_scaled) plt.figure(figsize=(8, 5)) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_agg, cmap='Set2', edgecolor='white', s=50) plt.title('层次聚类结果(Ward连接,PCA降维展示)') plt.xlabel('第一主成分') plt.ylabel('第二主成分') plt.grid(alpha=0.3) plt.show()

Ward方法生成的结果通常与K-均值很接近,因为它同样试图最小化簇内方差。区别在于层次聚类不依赖随机初始化,结果稳定可复现,而K-均值换了随机种子可能会得到略有差异的结果。

4.5 DBSCAN:K-距离图选参数

DBSCAN的参数选择是这份数据上最需要花心思的一步。首先确定MinPts的参考值,经验法则是取特征维度的两倍或更高,四维特征建议设MinPts等于5或6。然后通过K-距离图来确定Eps——对每个样本计算它到第MinPts个最近邻居的距离,将这些距离升序排列并绘图,距离发生"跳变"的位置对应的值就是一个合理的Eps。

from sklearn.neighbors import NearestNeighbors min_samples = 5 neigh = NearestNeighbors(n_neighbors=min_samples) neigh.fit(X_scaled) k_distances, _ = neigh.kneighbors(X_scaled) k_distances = np.sort(k_distances[:, min_samples - 1]) plt.figure(figsize=(8, 5)) plt.plot(k_distances) plt.xlabel('样本(排序后)') plt.ylabel('第5近邻距离') plt.title('K-距离图(MinPts=5)') plt.grid(alpha=0.3) plt.show()

如果K-距离图上存在明显的"肘部"或曲线突增区域,就取突增前的平稳段作为Eps。但鸢尾花数据集有可能出现比较平缓的曲线,意味着样本间密度过渡均匀,没有非常清晰的密度分界——这是一个重要信号:DBSCAN在这份数据上的聚类效果可能不会太好。

尝试多个Eps值并对比结果:

eps_list = [0.3, 0.5, 0.7, 0.9] plt.figure(figsize=(12, 8)) for i, eps in enumerate(eps_list): db = DBSCAN(eps=eps, min_samples=min_samples) y_db = db.fit_predict(X_scaled) n_clusters = len(np.unique(y_db[y_db != -1])) n_noise = np.sum(y_db == -1) plt.subplot(2, 2, i + 1) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_db, cmap='Set2', edgecolor='white', s=50) plt.title(f'DBSCAN eps={eps}, 簇数={n_clusters}, 噪声={n_noise}') plt.xlabel('第一主成分') plt.ylabel('第二主成分') plt.grid(alpha=0.3) plt.tight_layout() plt.show()

随着Eps从小到大调整,你会发现一个规律:Eps太小时噪声点太多,簇也被拆得很碎;Eps太大时第二类和第三类逐渐合并,最终只剩两个簇加少量噪声。很难找到一个Eps让DBSCAN同时将三类完全区分,原因正是第二类和第三类在密度上连成一片,不存在密度断裂的边界。

4.6 综合对比:三种结果的指标评估

聚类结果不能只靠肉眼判断,需要用量化指标和真实标签做交叉验证。这里我用三类指标:

  • 轮廓系数:内部指标,不需要真实标签,衡量簇内紧密度和簇间分离度。
  • 调整兰德指数(ARI):外部指标,需要真实标签,衡量聚类结果与真实标签的吻合程度,取值范围-1到1,越接近1越好。
  • 标准化互信息(NMI):外部指标,同样需要真实标签,衡量两个划分之间共享的信息量。
models = { 'K-均值': y_kmeans, '层次聚类(Ward)': y_agg, } results = [] for name, labels in models.items(): sil = silhouette_score(X_scaled, labels) ari = adjusted_rand_score(y_true, labels) nmi = normalized_mutual_info_score(y_true, labels) results.append([name, round(sil, 4), round(ari, 4), round(nmi, 4)]) eps = 0.5 db_scan = DBSCAN(eps=eps, min_samples=min_samples) y_db = db_scan.fit_predict(X_scaled) sil = silhouette_score(X_scaled, y_db) ari = adjusted_rand_score(y_true, y_db) nmi = normalized_mutual_info_score(y_true, y_db) results.append(['DBSCAN(eps=0.5)', round(sil, 4), round(ari, 4), round(nmi, 4)]) df_results = pd.DataFrame(results, columns=['算法', '轮廓系数', 'ARI', 'NMI']) print(df_results)

从实际输出看,K-均值和层次聚类(Ward)的ARI和NMI都比较高,轮廓系数也稳定在0.5以上;DBSCAN的指标则明显偏低,并且会带着几个噪声点。需要强调的是,指标低不一定代表算法"劣质",而是说明DBSCAN的假设与这份数据集的密度分布结构不匹配。数据本身没有为DBSCAN喜好的"密度边界"提供足够的支持。

这里有个容易踩的坑:轮廓系数为负值或接近0时,说明样本靠近簇边界,甚至可能被错分,不要强行解读为"聚类效果还可以接受"。如果轮廓系数低于0.25,基本可以认为聚类结果缺乏合理结构,需要回头调参数或换算法。

5. 常见问题与避坑指南

5.1 层次聚类树状图的切分困惑

不少初学者看到树状图的第一反应是不知道该切多少簇。我的经验是不要机械地找"最长垂直线",而是结合业务背景和不同切分深度下的轮廓系数综合判断。对鸢尾花数据,切3簇是很自然的选择;但如果是其他数据,树状图的分叉可能没那么清晰,这时跑一个2到8簇的轮廓系数曲线会更有说服力。还有一个细节:层次聚类的样本顺序可能会影响树状图的观感,但不会影响聚类结果本身,所以别被中间部分交叉的线条干扰判断。

5.2 K-均值的K值选择困境

K-均值在K值选择上确实存在一定主观性,肘部法则给出的"拐点"有时并不尖锐。遇到这种情况,可以结合轮廓系数、业务含义以及层次聚类树状图交叉验证,而不是单独依赖某一个指标。如果多个指标给出的K值不一致,优先选择业务上更容易解释的K值——算法最终要服务于可解释的行动方案。

另外K-均值的初始中心选择虽然有了KMeans++,但对极端数据分布仍然会有局部最优的风险。实践中建议把random_state固定,同时配合多次运行的稳定性检查,避免因为某次随机初始化的运气问题得出有偏结论。

5.3 DBSCAN参数调整的泥潭

DBSCAN的参数空间看起来只有两个数值,但实际调参过程像是踏入泥潭:Eps稍微调大0.1,结果就从三个簇变成两个簇;MinPts从4改为6,噪声点的数量又大变样。这种敏感性我见过太多次,甚至不少有经验的从业者也会在DBSCAN上调到怀疑人生。

一个有效的方法论是层次化调参:先用领域知识确定MinPts的合理范围,再用K-距离图缩小Eps的候选区间,最后用一组网格参数做对比实验,同时记录簇数和噪声点数量。在鸢尾花这种密度连续的数据上,如果网格搜索结束后发现无论如何都无法得到三类清晰分离的结果,就接受这个事实并记录下来——这本身就是有价值的分析结论。

5.4 标准化与降维的连带效应

还有一个常被忽视的问题:标准化之后再PCA降维,主成分的含义已经不能被简单解释为"第一个特征是花萼长度"。我在结果对比时用了PCA绘图,但必须明确说明这些图只是低维投影,不代表原始特征空间的全貌。两个样本在二维PCA图中坐得很近,并不能保证它们在原始四维空间中的距离也很近。

如果需要更强的可视化效果,可以试试t-SNE或UMAP。t-SNE更擅长展示局部的簇结构,但它的坐标轴没有直接的可解释性,且参数设置对结果影响极大。对于正式的报告,我通常PCA和t-SNE各画一张,PCA用于整体判断,t-SNE用于观察局部细节。

5.5 聚类结果的业务可解释性

最后提醒一句:聚类分析的实际价值不在指标数字,而在业务可解释性。跑完三个算法后,别只停留在一张对比表格上,要回到原始数据去看每个簇的特征均值、中位数、分布范围,给每个簇起一个语义化的标签。比如某簇对应"高花瓣-窄花萼"的组合,另一些可能是"低花瓣-宽花萼"的组合。只有把簇映射到可理解的业务描述,聚类结果才能真正落地。

6. 从本次对比中总结的实操心得

跑完整个鸢尾花对比实验后,我最大的体感是:算法的选择从来不是单纯的技术问题,而是数据形态与业务目标之间的匹配问题。

K-均值在这份数据上表现不错,核心原因正是鸢尾花的三个簇在标准化后近似球形、规模相近,完全符合K-均值的假设。它运行速度快、结果稳定、指标好看,但你要清楚它只告诉你"这些样本离得近",不会主动告诉你这个簇是否真的有意义。

层次聚类给出了最丰富的中间信息。树状图能直观呈现从1个簇到150个簇的层次关系,特别适合探索性分析。但它不适合样本量过大的场景,而且不同连接方式(Ward、单连接、全连接)的结果差异明显。本次选择Ward与K-均值结果接近,是一致性的体现,不是巧合。

DBSCAN在鸢尾花上确实比较吃亏——它的优势场景是密度差异明显、含有噪声、簇形状不规则的数据。鸢尾花的第二类和第三类在密度上无缝衔接,DBSCAN很难通过密度阈值找到正确边界。这提醒我:如果聚类结果不理想,先检查算法假设与数据结构是否匹配,而不是急着调参。

我个人在实际操作中的体会是,真正专业的聚类分析流程应该包含三个步骤:先做EDA和数据预处理,充分理解数据分布;再跑多个算法、多组参数,而不是只信某一个算法的默认输出;最后用内部指标和真实标签交叉验证,同时回归原始特征做业务解读。算法只是手段,理解数据本身才是目标。

这个项目后续还可以扩展的方向包括:把PCA替换为t-SNE查看局部结构,测试不同连接方式的层次聚类差异,或者在鸢尾花基础上加入人为噪声样本,观察DBSCAN的鲁棒性变化。每一次扩展都会带来新的认知,但我建议先把这份基础对比吃透,理解每个算法在相同数据上为什么会有不同的表现,这比盲目追逐更复杂的模型有价值得多。

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

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

立即咨询