刚开始接触聚类分析的时候,很多人都会拿鸢尾花数据集练手,跑一遍K-均值、层次聚类和DBSCAN,然后对比三个聚类图,觉得“哦,都分得挺不错”。但等到自己真正做项目、写报告的时候才发现,同样的数据,换个预处理方式,聚类结果能差很多;不同的算法对参数又特别敏感,选不好就出诡异结果。这个标题看起来简单,真正把它拆开、讲透,其实涉及聚类算法的适用边界、距离度量、参数选择、外部评估指标等一系列问题。
本篇文章会围绕鸢尾花数据集中三种经典聚类算法(K-均值、层次聚类、DBSCAN)的完整对比过程,把数据准备、算法原理、参数调优、指标评判和常见坑全部过一遍。适合正在学多元统计分析或机器学习入门的学生、需要快速完成数据聚类对比报告的开发者,以及想理解“三种聚类算法到底各擅长什么”的项目实践者。
1. 项目理解与整体设计
1.1 看似经典的对比实验,背后的核心需求是什么
一眼看这个标题,很容易理解成“用同一个数据集跑三个算法,然后画图对比”。但如果只做到这一步,那这个项目基本没有含金量。真正有价值的是通过这个过程搞清楚三件事:这几种聚类算法的本质差异是什么?在同一个数据集上为什么会出现不同的聚类边界?当出现差异时,用什么指标来判断“哪种结果更好”?
鸢尾花数据集恰好是非常适合做这件事的样本。它有150条样本,4个特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度),3个类别(Setosa、Versicolour、Virginica),每一类50条数据。更妙的是,在这个数据集中,Setosa和其他两类在特征空间里是完全线性可分的,而Versicolour和Virginica却存在部分重叠。这个分布特性,让三种聚类算法面对的是一个“同一个预期分组,不同重叠程度”的问题,正好能体现出不同算法的行为和局限。
很多人用这个数据集做聚类却忽略了一个关键点:真实标签虽然存在,标准聚类过程是不知道标签的,我们对比的是纯无监督的聚类输出。真实标签只在评估时有用,用来计算外部指标(ARI、NMI、纯度等),评价聚类结果和真实分布的吻合程度。如果忽略这个区别,就会把聚类分析当成“用标签训练”,这是概念上的硬伤。
1.2 方案选型:为什么偏偏是这三种算法
选择K-均值、层次聚类、DBSCAN作为对比对象,并不是随便挑的。它们分别代表了聚类分析的三大主流思想:
- K-均值属于基于划分的聚类。预先指定K值,通过迭代优化每个样本到所属簇中心的距离,把数据划分为K个互不重叠的簇。优点是计算快,能处理较大的样本量;缺点是只适合凸形簇,对初始中心和异常值敏感。
- 层次聚类属于基于层次的聚类。通过不断合并(凝聚式)或分裂(分裂式)来构造一棵树状的聚类图,不需要事先指定K,可以从树状图中切出任意数量的簇。优点是聚类过程可视化,适用于任意形状簇;缺点是计算复杂度高,不适合大数据量。
- DBSCAN属于基于密度的聚类。通过样本的密度连通性来识别任意形状的簇,并且能自动筛出异常点。优点是无需指定K,能发现非线性簇、能识别噪声;缺点是对密度差异大的数据效果不好,两个关键参数(epsilon和min_samples)需要结合数据分布来调。
这三个算法放在一起,正好覆盖了“基于划分”、“基于层次”、“基于密度”三种思路。在鸢尾花数据集上的对比,本质上不是在比谁跑出来的图更漂亮,而是在看不同聚类思想在面对有重叠、有不同簇密度、有异常值的数据时,各自的优势和劣势在哪里。
1.3 评价体系:聚类结果好坏的判断标准
聚类和分类不一样,分类有真实的猫和狗标签,分类错了可以直接算准确率;聚类没有标准答案,同一个数据可以由不同角度划分出不同分组。那怎么比较三个算法?通常会用到两类指标:
内部指标——只利用聚类结果自身信息来评估,最典型的是轮廓系数。轮廓系数取值范围在-1到1之间,越接近1说明簇内紧凑、簇间分离,聚类质量越好。不同K值下比较轮廓系数可以辅助确定聚类数。对于K-均值和层次聚类,我一般会同时算SSE(簇内误差平方和)和轮廓系数;这两个指标一个看紧密度,一个看综合分离度,组合起来更可靠。
外部指标——利用已知的标签信息评判聚类结果与真实类别的匹配程度。这里最常用的三个是调整兰德指数(ARI)、标准化互信息(NMI)和纯度(Purity)。ARI的取值范围在-1到1之间,越接近1表示聚类结果和真实分组的吻合度越高,而且它对随机划分有惩罚,所以比单纯准确率更可靠。NMI也是0到1之间的标准化值,衡量两个分布之间的共享信息量。在项目报告中,我建议把这三类指标都算出来,再配一张表格对比,比单独贴一张散点图有说服力得多。
在鸢尾花数据集上,因为K=3是已知的,所以也可以在计算K-均值和层次聚类时固定K=3;但DBSCAN不需要K。最终评价时,外部指标通常比内部指标更直观,因为我们可以直接回答“聚类结果和真实分组到底有多像”。这个评价设计从一开始就要确定下来,后面所有代码围绕这个体系来写。
2. 数据准备与预处理要点
2.1 鸢尾花数据集的构成与分布检查
项目的第一步应该是读取数据,但不要急着跑算法,先做数据探查。鸢尾花数据集的150条样本中,Setosa在特征空间里和另外两类完全分开,而Versicolour和Virginica在花瓣宽度和花瓣长度上有明显重叠。这个先验认知会直接影响对聚类结果的判断。
具体做三件事:
- 查看数据的基本统计量(均值、方差、分位数),确认不同特征的量纲范围。鸢尾花四个特征的量纲差距不算特别大,但花萼长宽和花瓣长宽的单位都是厘米,数值范围都在0到8之间。这意味着完全不标准化直接聚类也可以,但一旦遇到量纲差异大的数据集,标准化就成了必选项。
- 做相关性检查和二维可视化。在只取两个特征的情况下,画出不同类别数据的散点分布,快速感受类间的重叠程度。
- 确认数据中是否存在缺失值或异常值。鸢尾花是“干净”数据,但真正项目实践里,缺失值和离群点会严重影响聚类结果,检查步骤不能省。
2.2 标准化:做还是不做,区别很大
这是一个非常容易被忽略但影响巨大的决定。K-均值和层次聚类都依赖距离计算,而欧氏距离对特征的量纲非常敏感。如果某个特征数值范围是另一个的10倍,它在距离计算中的影响力就会远远超过另一个特征。虽然鸢尾花数据的量纲相对接近,但在严谨对比三种算法时,做出可复现的标准化处理能让结论更干净。
我建议在项目里同时跑“原始数据”和“标准化数据”两套结果,然后观察差异。很多时候结论会不一样:比如某些特征在原始尺度下占主导,会让聚类结果产生偏差;标准化之后,每个特征权重被拉齐,聚类边界更符合真实结构。但这里要提醒一个反向经验:DBSCAN对尺度尤其敏感,因为它的密度判定完全基于距离阈值。标准化前后,同一个epsilon值效果可能完全不同,所以对于DBSCAN来说,数据标准化决策要格外慎重,必须和epsilon参数一起调试,不能只调算法不调数据。
光谱下来讲,标准的做法是用StandardScaler(标准化为均值为0、方差为1)或者MinMaxScaler(缩放到0到1之间)。对于K-均值和层次聚类,我都观察过在标准化后的数据上聚类稳定性更好,尤其是层次聚类用ward方法时,标准化的效果更稳定。为了让对比实验更具可复现性,正式代码示例中我统一用标准化后的数据进行三个算法的执行,但在结论部分会根据原始数据的对比结果做补充说明。
2.3 降维可视化:聚类结果不是“看图说话”,但有图更容易理解
上面说聚类好坏看指标,但可视化依然很重要。原因很简单:指标是数值结论,聚类图是直观感知。对聚类分析这种无监督任务来说,最终的工程师要能“看懂”聚类边界是怎么形成的,不然很难向项目相关方解释为什么这样划分。
因为鸢尾花是4维数据,二维散点图不能直接画,所以通常用PCA降到二维空间,再把聚类标签着色到散点上,直观展示聚类效果。这是一种通用的可视化方案,并不意味着DPCA就是对原数据降维后用于聚类;聚类本身是在4维原始特征(或标准化后的4维特征)上进行的,PCA降维只用于最终的可视化展示。如果直接在PCA后的二维空间重新跑聚类,那么指标和图形就脱节了,报告的结论会有偏差。
实操经验:PCA降到两个主成分之后,前两个主成分通常能解释90%以上的方差(鸢尾花数据集大约能解释95%左右),视觉上足够可靠。但如果在其他数据上,前两维只能解释50%到60%的方差,那散点图的可信度就需要打个问号,这时如果一定要可视化,可以尝试t-SNE或UMAP作为补充。
3. 三种聚类算法原理与参数选型
3.1 K-均值:K怎么定,初始中心怎么挑
K-均值的目标很直接:把样本分成K个簇,使得每个样本到它所属簇中心的距离平方和最小。这个目标函数叫簇内误差平方和(SSE),算法通过迭代更新聚类中心和样本分配来优化它。流程很简单:
- 随机选择K个初始中心;
- 每个样本分配到最近中心所在的簇;
- 重新计算每个簇的中心;
- 重复2和3直到中心不再变化或达到最大迭代次数。
但简单不等于容易用。两个实际问题立刻出现:K怎么选?随机初始化不稳定怎么解决?
关于K的选择,最常用的两个方法:
- 手肘法:计算不同K值下的SSE,画出折线图,找到“肘部”对应的K。K从1到8测试,在K=3附近,SSE下降速度明显放缓,这就是手肘位置。
- 轮廓系数法:计算不同K值下的平均轮廓系数,值最大的K往往是最合适的。在鸢尾花数据上,K=2的轮廓系数通常大于K=3,但K=3更接近真实类别数。所以如果项目目标是用无监督聚类的视角探索数据结构,选择轮廓系数最大的K=2没有错;但如果有领域先验知识(知道三类),那K=3更有解释性。
关于随机初始化问题,不能只用random_state=0跑一次就下结论。K-均值对初始中心敏感,不同随机种子可能得到不同的局部最优解。实操中我会把K-均值跑20次甚至50次,用k-means++初始化策略,这种方法能让初始中心互相远离,显著减少随机性影响。在scikit-learn中,设置n_init=10或更高、random_state固定,从而保证实验可复现。千万别用默认值不设置随机种子就输出结果,别人复现不出来。
3.2 层次聚类:树状图怎么看,linkage方法如何选择
层次聚类的核心不是直接确定K,而是递归地合并样本或簇,形成一个树状图。凝聚式层次聚类从每个样本单独成为一个簇开始,每次合并距离最近的两个簇,直到最后所有样本被合并为一个簇。这个过程中,簇间距离的定义方式(linkage)决定了合并顺序和最终形状。
常用的linkage方法:
- ward:合并后簇内方差增加最小的两个簇被合并。适合凸形簇,倾向于生成大小相近的簇,是很多场景下的默认选择。
- complete:取两个簇中相距最远的两个样本之间的距离作为簇间距离,对离群点敏感度低,容易生成紧凑的簇。
- average:取两个簇中所有样本对的平均距离,介于两者之间,相对稳定。
- single:取两个簇中最近的两个样本之间的距离,能找到很奇特的细长簇,但容易产生“链条效应”,把本应分开的簇连在一起。
在鸢尾花数据上,我实测下来ward和complete通常都能给出比较清晰的三簇结果,而single容易把数据拉成长条形状,对数据的过拟合倾向非常明显。这里有一个重要经验:不要照搬任何默认参数,至少要比较ward和average两种方法下的聚类结果,用外部指标来判断哪一种更适合真实标签分布。
树状图的作用也容易被低估。树的纵轴表示合并时簇间距离,横轴是样本;如果从某条横线切下去,下方的子树就对应不同聚类结果。观察树状图可以直观看到,在较高的距离阈值处,数据大致分成两个或三个明显的大分支。这比单纯看散点图更容易解释“为什么选择K=3或K=2”。
3.3 DBSCAN:epsilon和min_samples怎么调,密度不均怎么办
DBSCAN的思想是说一个簇是“密集区域相连通的样本集合”。它把样本分成三类:
- 核心点:在半径epsilon范围内至少有min_samples个样本(包括自身);
- 边界点:样本本身不是核心点,但在某个核心点的邻域内;
- 噪声点:既不是核心点也不是边界点。
这个算法最大的优点是:它不要求簇是凸形的,能发现“月牙形”、“环形”簇,而且能直接识别出异常样本;最大的缺点是:它只有epsilon和min_samples两个参数,但这两个参数对密度不均匀的数据非常敏感。
在鸢尾花数据上,最典型的现象是Setosa这个类别密度较高、分布紧凑,而Versicolour和Virginica之间有重叠、挤在中间。如果选择一个比较小的epsilon,Setosa会被分成一个簇,但另外两类会被切得零零碎碎;如果选一个比较大的epsilon,Setosa和部分Versicolour被合并在一起,分类边界变得模糊。换句话说,一个全局epsilon很难同时满足“中间密集区域”和“两侧稀疏区域”的密度要求,DBSCAN的整体表现通常不如K-均值和层次聚类。
min_samples的默认经验值是2乘以维度,即2×4=8左右起步进行调试。epsilon的选择可以用k-距离图辅助:计算每个样本到第k近邻居的距离,排序后画出折线图,在“斜率变化最剧烈”的位置取epsilon。这个方法和手肘法很像,本质上是在找密度分布的拐点。对于鸢尾花数据集,这个拐点区域通常在0.3左右附近(标准化后)。但一定注意,这只是初始值,最终还是要结合聚类结果反复微调。
3.4 距离度量:欧氏距离最常用,但不能无脑选
三种算法中都默认使用欧氏距离,这很常见,但不代表在所有场景中没有其他选择。欧氏距离适合连续型数值特征,在空间上的直觉是“直线距离”。如果特征之间存在量纲差异,必须先用标准化把尺度拉平,否则欧氏距离会被大数值特征主导。
曼哈顿距离适合高维稀疏数据,它对异常值相对不敏感,但和K-均值这一类的目标函数匹配度不如欧氏距离。余弦相似度常用于文本向量等高维场景,在聚类鸢尾花这类低维数值特征数据时意义不大。层次聚类和DBSCAN可以比较灵活地替换距离度量;K-均值本质上是基于欧氏平方距离推导的,换了其他距离度量算法就不那么稳定。实操中,最简单可靠的配置就是“标准化 + 欧氏距离”,除非有充足理由,不要为了花哨换度量。
4. Python实操:完整复现流程对比
4.1 环境准备与数据加载
这个项目的代码实现建议直接使用Python + scikit-learn。它是当前机器学习领域最主流的工具库,内置了鸢尾花数据集和三种聚类算法的标准实现,非常适合做这种对比实验。需要安装的库有scikit-learn、pandas、numpy、matplotlib和scipy,前四个用pip安装即可,scipy会在scikit-learn安装时作为依赖自动装上。
数据加载用scikit-learn自带的load_iris()接口,比从外部文件下载更方便,还能保证数据版本一致。加载后整理成DataFrame格式,方便后续查看和分析。
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.decomposition import PCA from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN 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 = iris.target feature_names = iris.feature_names # 转成DataFrame df_iris = pd.DataFrame(X, columns=feature_names) df_iris['label'] = y print(df_iris.head()) print(df_iris['label'].value_counts())这一段跑完之后,应该能看到150行×5列的数据,3个标签各50条。检查一下有没有缺失值,用df_iris.isnull().sum()看一眼,没有缺失再继续。
4.2 数据标准化与PCA降维
标准化和降维是两个容易被混淆的操作——标准化是对每个特征分别做零均值、单位方差变换,在聚类的距离计算之前完成;PCA是把多维特征压缩到少数主成分,用于可视化和理解数据,但不会直接参与聚类过程。
下面代码同时进行标准化和PCA降维:
# 标准化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # PCA降至2维,用于可视化 pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) # 查看主成分方差解释比例 print("每个主成分解释方差比例:", pca.explained_variance_ratio_) print("前两个主成分累计解释方差比例:", pca.explained_variance_ratio_.sum())如果你运行这段代码,会发现前两个主成分大约解释了95%以上的总方差。第一主成分基本对应花瓣长度和花瓣宽度的组合效应,第二主成分主要和花萼宽度相关。这个解释比例意味着,用二维散点图来可视化四种特征的综合分布,可信度很高。
然后画一张原始标签的PCA散点图:
plt.figure(figsize=(8, 5)) colors = ['#e74c3c', '#3498db', '#2ecc71'] for i in range(3): idx = y == i plt.scatter(X_pca[idx, 0], X_pca[idx, 1], c=colors[i], label=iris.target_names[i], alpha=0.7, edgecolors='w') plt.xlabel("PC1 (%.2f%%)" % (pca.explained_variance_ratio_[0] * 100)) plt.ylabel("PC2 (%.2f%%)" % (pca.explained_variance_ratio_[1] * 100)) plt.title("原始标签分布(PCA降维)") plt.legend() plt.grid(alpha=0.3) plt.show()从图里可以很清楚地看到红色标签的类别三与另外两类完全分离,蓝色和绿色两类存在局部重叠。这张图是整个对比分析“地面真相”的参照物。
4.3 K-均值聚类:手肘法确定K,跑完看指标
在K-均值部分,先做两步:用SSE手肘图辅助决定K值,再计算轮廓系数对比。然后选择K=3运行正式聚类。
# 计算不同K值下的SSE和轮廓系数 sse = [] silhouette_scores = [] K_range = range(2, 9) for k in K_range: km = KMeans(n_clusters=k, init='k-means++', n_init=20, max_iter=300, random_state=42) km.fit(X_scaled) sse.append(km.inertia_) sil = silhouette_score(X_scaled, km.labels_) silhouette_scores.append(sil) # 画手肘图和轮廓系数图 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(list(K_range), sse, marker='o') axes[0].set_xlabel("K") axes[0].set_ylabel("SSE") axes[0].set_title("手肘图") axes[0].grid(alpha=0.3) axes[1].plot(list(K_range), silhouette_scores, marker='o') axes[1].set_xlabel("K") axes[1].set_ylabel("Silhouette Score") axes[1].set_title("轮廓系数") axes[1].grid(alpha=0.3) plt.tight_layout() plt.show()一般跑完这个,在手肘图上你会看到K=3处SSE下降速度明显变缓,尽管K=2处也有一点拐弯趋势,但结合轮廓系数和领域知识,K=3是最符合数据结构的。轮廓系数在K=2时可能更高一点,但这不代表三分类是错的——轮廓系数天然偏爱分离度噪响清晰的紧凑簇,而K=2恰恰能把数据分成两个很分离的大块;但在实际应用中,如果目标是研究三种鸢尾花的区分,K=3更有解释价值。
正式训练K-均值:
kmeans = KMeans(n_clusters=3, init='k-means++', n_init=20, max_iter=300, random_state=42) kmeans_labels = kmeans.fit_predict(X_scaled)注意这里的n_init=20,意味着用20个不同的初始中心跑20次,选择所有运行中SSE最小的结果作为最终聚类。这个参数很关键,能显著提升结果稳定性。
4.4 层次聚类:树状图辅助判断,ward方法作为主方案
先画树状图,观察合并过程中的簇间距离跳跃:
# 用ward方法计算层次聚类 Z = linkage(X_scaled, method='ward') # 画树状图 plt.figure(figsize=(10, 6)) dendrogram(Z, truncate_mode='level', p=5) plt.xlabel("样本序号") plt.ylabel("簇间距离") plt.title("层次聚类树状图(ward)") plt.show()树状图一般会清楚显示出最上层的两个或三个大分支。在距离阈值大约5到7的位置(标准化数据上的数值),可以看见三个比较明显的聚类分组。这个视觉判断和K-均值手肘法的结论能够互相印证。
然后运行层次聚类正式训练。我这里建议把ward和complete都跑一遍做对比,最后用指标说话:
hier_ward = AgglomerativeClustering(n_clusters=3, metric='euclidean', linkage='ward') hier_ward_labels = hier_ward.fit_predict(X_scaled) hier_complete = AgglomerativeClustering(n_clusters=3, metric='euclidean', linkage='complete') hier_complete_labels = hier_complete.fit_predict(X_scaled)注意在scikit-learn的新版本中,AgglomerativeClustering的参数是metric而不是affinity,如果你还有老版本的习惯,要按自己的版本对应调整。用ward方法时,距离度量只能是欧氏距离,这是它的数学特性决定的;如果强行用其他度量,程序会直接报错。
4.5 DBSCAN聚类:用k-距离图锁定epsilon,再微调
先根据k-距离图确定一个初始epsilon:
from sklearn.neighbors import NearestNeighbors # 计算每个样本到其第4个最近邻居的距离(取min_samples为8,变化范围从2到20) # 实际调试中先固定min_samples=6或8,再根据排序距离曲线看拐点 nn = NearestNeighbors(n_neighbors=8) nn.fit(X_scaled) distances, indices = nn.kneighbors(X_scaled) k_distances = np.sort(distances[:, -1]) plt.figure(figsize=(8, 5)) plt.plot(k_distances) plt.xlabel("样本排序") plt.ylabel("第8近邻距离") plt.title("k-距离图(k=8)") plt.grid(alpha=0.3) plt.show()k-距离图的纵轴是距离,横轴是按距离排序后的样本编号。曲线中会有几个明显的“肘部”,在肘部对应的高度取epsilon值。鸢尾花数据标准化后的k-距离图,在y轴大约0.4到0.6之间往往会出现转折。
然后先给定一个初始参数,比如epsilon=0.5、min_samples=8,跑一版看效果:
dbscan = DBSCAN(eps=0.5, min_samples=8) dbscan_labels = dbscan.fit_predict(X_scaled)跑完之后第一步看dbscan_labels中包含哪些值。如果出现-1标签,说明存在噪声点;如果类别数不是3,再微调epsilon:聚类数过多就增大epsilon,聚类数太少就减小epsilon。鸢尾花这个数据上,如果你用较大的epsilon如0.8,Setosa很可能和部分Versicolour合到一起,整体效果不如K-均值。
4.6 指标计算与结果对比
我们把三种算法(K-均值取固定K=3,层次聚类取ward方法,DBSCAN取合理调参后的方案)的结果汇总到一个DataFrame中,计算外部指标(ARI、NMI)和内部指标(轮廓系数)。
def cluster_evaluation(labels, X_scaled, y_true, name): ari = adjusted_rand_score(y_true, labels) nmi = normalized_mutual_info_score(y_true, labels) sil = silhouette_score(X_scaled, labels) # 纯度:每个簇中最多数类别的占比之和 / 总样本数 from collections import Counter purity_sum = 0 for cluster in np.unique(labels): if cluster == -1: continue mask = labels == cluster true_in_cluster = y_true[mask] if len(true_in_cluster) > 0: purity_sum += max(Counter(true_in_cluster).values()) purity = purity_sum / len(labels) return pd.Series({'ARI': ari, 'NMI': nmi, 'Silhouette': sil, 'Purity': purity, '噪声数': np.sum(labels == -1)}, name=name) results = pd.DataFrame([ cluster_evaluation(kmeans_labels, X_scaled, y, 'K-Means'), cluster_evaluation(hier_ward_labels, X_scaled, y, 'Hierarchical-Ward'), cluster_evaluation(hier_complete_labels, X_scaled, y, 'Hierarchical-Complete'), cluster_evaluation(dbscan_labels, X_scaled, y, 'DBSCAN') ]) print(results.round(4))表格输出一般会显示类似这样的趋势:K-均值和层次聚类(ward)在ARI和NMI上都会非常接近,数值在0.7左右(具体数值可能因随机成分略有浮动);层次聚类complete可能稍低一点;DBSCAN的ARI往往低于它们,可能会在0.5左右甚至更低,而且伴随一定的噪声点。轮廓系数方面,如果DBSCAN的噪声点多,轮廓系数会受影响,但也不一定非常差。
有一点必须说明:指标输出只是一个代表性结果,因为K-均值有随机初始化的波动、DBSCAN参数变化后的结果差异也很大,同一份代码不同机器上跑出来的数值会有些许出入。项目报告或博客中重点不在逐分逐秒对比精确数值,而在于解释“为什么K-均值和层次聚类在这个数据上表现更优”,以及“DBSCAN的哪些参数导致了它的边界偏差”。
4.7 聚类结果可视化
最后把三种算法的聚类结果画在同一套PCA坐标轴里,和原始标签分布形成直观对照。这里的关键细节是:因为K-均值、层次聚类的聚类标签都是0、1、2,但不同算法之间标签的“第0类”不一定是同一个真实类别,所以随便用固定颜色画出来会出现“颜色对不上”的假象。实际项目中为了直观比较,我通常会把三个算法的簇标签按照与真实标签的最大重叠率进行重映射,然后再统一颜色。这样图形对照才有意义。
from scipy.stats import mode def remap_labels(true_labels, pred_labels): # 将预测标签按多数投票方式重映射到真实标签的编号,仅用于可视化 remapped = np.zeros_like(pred_labels) for i in np.unique(pred_labels): mask = pred_labels == i if np.sum(mask) == 0: continue # 找出当前簇中最多的真实标签编号 true_in_cluster = true_labels[mask] most_common = np.bincount(true_in_cluster).argmax() remapped[mask] = most_common return remapped # 可视化函数 def plot_clusters(ax, X_pca, labels, title): for i in np.unique(labels): idx = labels == i ax.scatter(X_pca[idx, 0], X_pca[idx, 1], label=f"类{i}", alpha=0.7, edgecolors='w') ax.set_title(title) ax.set_xlabel("PC1") ax.set_ylabel("PC2") ax.legend(fontsize=8) ax.grid(alpha=0.3) kmeans_viz = remap_labels(y, kmeans_labels) hier_viz = remap_labels(y, hier_ward_labels) dbscan_viz = remap_labels(y, dbscan_labels) fig, axes = plt.subplots(2, 2, figsize=(12, 10)) plot_clusters(axes[0, 0], X_pca, y, "原始标签") plot_clusters(axes[0, 1], X_pca, kmeans_viz, "K-Means") plot_clusters(axes[1, 0], X_pca, hier_viz, "Hierarchical-Ward") plot_clusters(axes[1, 1], X_pca, dbscan_viz, "DBSCAN") plt.tight_layout() plt.show()如果你跑完图形,大概率会看到这样的分布:K-均值在PCA图上把Setosa、Versicolour、Virginica分成了三个区域,边界清晰;层次聚类的结果和K-均值非常接近,但可能在重叠区域某些样本的归属上存在差异;DBSCAN要么在图里多了一些噪声点,要么把某些本应分开的样本划到了一起。
5. 常见问题与排查实录
5.1 K-均值聚类结果不稳定,怎么办
现象:同一份数据,只改了random_state,聚类标签就变了。
原因:K-均值通过随机初始化中心开始迭代,不同初始值可能收敛到不同的局部最优解。鸢尾花数据分布虽然相对规则,但在两个簇的交界区域,不同的初始中心会导致边界样本归属发生变化。
解决:设置n_init=20甚至更高,scikit-learn会多次随机初始化并保留SSE最小的一次。同时在项目代码中固定random_state,保证实验可复现。对比不同随机种子下的结果一致性时,可以用ARI评估两次聚类结果的相似度:如果两次输出的ARI接近1,说明聚类结果比较稳定;如果明显低于0.9,说明数据中存在较强的多解性,需要在报告中说明。
5.2 手肘图不明显,K值到底怎么选
现象:手肘图上没有明显的“肘部”,K=2、K=3、K=4的SSE下降曲线比较平滑。
原因:当簇之间的分离度不够强时,SSE曲线不会出现清晰的拐点。鸢尾花数据的Setosa分离度较好,但其他两类有重叠,这会让手肘的“陡度”减弱。
解决:不要只看SSE,结合轮廓系数一起判断。如果多组K值的轮廓系数差距不大,选择符合业务解释的K。集群分析的核心是解释数据结构,而不是在数学指标上较真到0.01的差异。
5.3 DBSCAN把数据分出很多小簇,噪声点特别多
现象:DBSCAN结果里出现大量-1(噪声),或者原本一个簇被拆成好几个小簇。
原因:epsilon设置太小。很多密度稍低一点的点没有被任意核心点的邻域覆盖,被误判为噪声。
解决:把epsilon逐步调大,观察聚类结果中噪声点的数量变化。结合k-距离图,找图上“肘部”附近的临界值。如果无论如何调,簇数都不是业务上合理的结果,说明数据密度不均匀,DBSCAN并不适合这个数据集,没必要硬调。
5.4 标准化后DBSCAN结果反而变差了
现象:原始数据下DBSCAN表现尚可,标准化后反而出现更多噪声点或簇数变化。
原因:标准化会改变样本之间的相对距离分布。原始数据中某些特征的数值范围本身就携带了密度信息,标准化反而把这种密度结构抹平了。DBSCAN的密度概念依赖绝对的距离阈值epsilon,所以它对输入特征尺度的敏感度比K-均值和层次聚类更高。
解决:如果你明确要用DBSCAN,可以同时尝试原始数据和标准化数据两套输入,分别调参;不要默认标准化对所有算法都好。在项目报告中明确说明“DBSCAN在标准化前后的参数选择差异”这一现象,本身就是有价值的分析结论。
5.5 聚类标签和真实类别编号对不上
现象:K-均值聚类出来的“0类”对应真实标签的“2类”,用混淆矩阵看时很混乱。
原因:这完全正常。聚类算法输出的簇标签只是标识符,不代表类别编号。真实标签是0、1、2,聚类标签也是0、1、2,但两者没有顺序上的对应关系。把K-均值输出的0号簇对应到真实标签的哪一类,需要靠多数投票或其他匹配方式来对齐。
解决:计算纯度指标时直接按多数投票法处理;画图时用重映射标签统一颜色。这样做完,才能让图形和真实标签分布有直观可比的对照。
5.6 比较三个算法时用了三个不同版本的预处理
现象:K-均值用标准化数据,层次聚类用原始数据,DBSCAN又用另一种缩放方式,最后比较指标时得出的结论毫无公信力。
原因:预处理方式不同,数据分布已经改变,同一个算法指标不具备可比性;更不要说三个算法分别用了不同的数据版本,这完全没法科学决策。
解决:做一个完整的统一实验设计。要么全部标准化,要么全部原始数据。如果非要讨论标准化影响,就单独列一个“预处理对比”子章节,每个算法都在同一套预处理下比较,而不是混着来。
6. 实操体验与选择建议
6.1 从鸢尾花这个经典例子里,能延伸到哪些项目场景
这个项目在课程作业里很常见,但它背后的方法思路可以直接迁移到很多实际分析任务中。举个例子,做用户分群时,如果手上有用户的消费金额、活跃天数、购买频次等数值特征,K-均值往往是最快出结果的方案,它能给出相对均衡的群体划分;如果想要理解用户群体之间的层级关系,比如“高端用户下面包含哪些细分人群”,层次聚类的树状图能直观展示这种嵌套结构;如果数据中包含大量离群点,比如异常登录行为、机器故障数据、传感器异常点,那么DBSCAN自动识别噪声的能力就派上用场了。
关键在于:选算法不是凭喜好,而是看数据的分布形态和分析目标。鸢尾花的对比实验,其实就是在训练这种“数据感知”能力。你会慢慢形成一个条件反射——看到凸形分布、数据量中等、预期有几类分区时,首选K-均值或ward层次聚类;看到分布形状奇特、存在大量噪声或离群点时,优先考虑DBSCAN。
6.2 调参策略上的几条实战心得
先说K-均值。项目里用K-均值时,我养成了一个习惯:先跑一遍K从2到10的SSE和轮廓系数,然后选定K;正式训练设置n_init=20甚至50,并固定random_state。如果最终结果对random_state仍敏感,那说明数据本身的聚类结构不清晰,草率选择K值只会得到虚假稳定的结论。
再说层次聚类。实际工作中树状图一定要画,不要只输出一个聚类标签。树状图能帮你看到数据分层结构的变化——在哪个距离上分出两大群,在哪个距离上变成三大群,这些信息对业务解释非常有用。ward和complete我都建议试,因为这两者在多数场景下的效果差异够大,能直观展现linkage选择的影响。
最后说DBSCAN。这个算法是最难调也是最容易让人迷惑的,因为它没有K的概念,输出是“自动”的,给人感觉似乎不需要调参。实际恰恰相反,epsilon和min_samples的小幅变化可能导致聚类数量的大幅波动。我的做法是:先用k-距离图估一个epsilon初值,然后做一个小网格搜索(epsilon取在初值周围的一组值,min_samples取4到12),选择噪声比例和簇数都相对稳定的参数组合,而不强求最优ARI。在鸢尾花数据上,每次手动调整epsilon,我都能看到聚类结果的剧烈变化,这个参数敏感性本身就是最值得写进分析报告中的经验。
6.3 报告怎么写才不像“跑代码交作业”
很多学生把聚类分析项目做成了“三个算法各跑一段代码,各画一张图,贴一段总结”的流水账。这种报告缺少分析链条,含金量不高。真正有参考价值的写法是:
第一,说明数据分布特征。你观察到哪些类的分离度较高,哪些类有重叠?这对后面理解聚类结果至关重要。第二,说明预处理决策及其理由。为什么标准化?为什么统一用欧氏距离?如果试过不同方案,差异在哪里?第三,说明参数选择依据。K=3是怎么定的?DBSCAN的epsilon为什么选了某个值?有没有做参数敏感性分析?第四,指标对比要回答“为什么”,而不只是“是什么”。为什么K-均值和层次聚类得分高?为什么DBSCAN会低估某些边界样本?第五,落点在一个决策建议上。对于当前数据形态,未来遇到类似场景时,首选哪种算法?为什么?
这样一来,你的项目报告就从“代码展示”上升到了“方法研究”,无论面试答辩还是博客分享,都会扎实得多。
6.4 扩展方向
如果想让对比更深入,可以考虑替换数据集验证算法迁移性,看看在类似规模的其他数据集上三种算法的性能排序是否会变化;也可以尝试给原始数据人为添加噪声点或异常值,观察DBSCAN在异常值影响下的鲁棒性是否真的比K-均值好;还可以比较不同标准化方法(StandardScaler与MinMaxScaler)对聚类结果的影响,这在实际项目中经常是决定性因素。
我个人在实际操作中比较深的体会是,聚类分析项目里七成的价值来自数据理解和预处理阶段,三成来自算法选择和调参。很多人把重心放在调参上,却忽略了对数据的观察,最后给出的结论经不起推敲。这个鸢尾花比较项目,恰恰能帮你把“先看数据、再选工具、最后评判”这一整套工作习惯建立起来,这套方法论放到任何聚类任务当中,都不会过时。