简介:面向机器学习初学者与聚类分析实践者的算法教学代码包,聚焦鸢尾花数据集上的无监督聚类任务。资源共6个文件,压缩包大小433KB,包含5个Python脚本和1个Word文档:脚本分别实现和演示K-Means、凝聚式层次聚类(Agglomerative Clustering)与DBSCAN三种算法在鸢尾花数据上的聚类流程,Word文档则对算法原理、参数选择和结果分析做系统梳理。已有2320人学习下载。通过对照代码可直观理解K-Means对初始化质心的敏感性、合并聚类的层级合并过程、DBSCAN中epsilon与minPts的调参影响,同时也能学习数据标准化、距离计算、聚类结果可视化的典型写法,适合课程实验、期末复习或入门机器学习时快速跑通三种主流聚类方法。
1. 鸢尾花数据集上k均值、合并聚类和DBSCAN聚类代码的取舍
下载“k均值、合并聚类和DBSCAN聚类算法对鸢尾花数据集聚类代码”这类压缩包的人,多数是想把三种算法放在同一份数据上跑一遍,好直观感受差异。跑完第一眼通常感到意外:k均值分成三类,合并聚类分成三类,DBSCAN却可能只分出一类,剩下样本全被标成噪声。这不是代码写错了,而是三种算法对“簇”的定义不同。k均值假设簇是凸的近似球状,合并聚类通过距离递归合并或切割层次,DBSCAN完全依赖局部密度,密度不连续处就被判为噪声。鸢尾花数据集共150个样本、4个特征、3个真实类别,其中setosa在特征空间和其他两类完全分开,versicolor与virginica却又存在特征重叠,正好能把三种算法的偏好暴露得清清楚楚。这篇博文从算法假设讲起,给出pandas读取鸢尾花数据集、特征标准化、三种聚类建模调参、到轮廓系数与ARI评估的完整聚类代码路径,适合刚接触无监督学习的读者,也适合要快速拿聚类结果做基线比对的工程师。
2. 三种聚类算法的核心逻辑与选型理由
2.1 距离度量:三种算法共享的底层标尺
k均值、合并聚类、DBSCAN在scikit-learn里默认都用欧氏距离。欧氏距离有一个容易被忽视的前提:参与运算的特征必须大致处于同一量纲,否则数值范围大的特征会在距离公式里占据支配地位,让聚类结果被那个特征牵着走。鸢尾花数据四个特征单位都是厘米,量级看起来一致,实际方差差异不小:
| 特征 | 最小值 | 最大值 | 标准差 |
|---|---|---|---|
| sepal length (cm) | 4.3 | 7.9 | 0.83 |
| sepal width (cm) | 2.0 | 4.4 | 0.43 |
| petal length (cm) | 1.0 | 6.9 | 1.76 |
| petal width (cm) | 0.1 | 2.5 | 0.76 |
petal length的标准差大约是sepal width的四倍,如果直接拿原始特征算距离,petal length就成了最大的权重维度。无论选哪种算法,我一般先把这步验证一遍,感受量级差距:
import numpy as np from sklearn.datasets import load_iris X = load_iris().data # 两个样本在原始特征空间里的欧氏距离 dist_raw = np.linalg.norm(X[0] - X[1]) print("原始特征欧氏距离:", dist_raw)这段代码用numpy的linalg.norm计算两个样本向量差的L2范数,也就是欧氏距离。样本量级固定后,后续标准化的必要性会更直观:如果特征标准差从0.43到1.76不等,距离计算已经隐含了特征加权。这就是为什么三种算法在同一个数据集上出现不同结果,第一步排查要先看输入数据而不是算法参数。
2.2 k均值:球状簇假设下的迭代优化
k均值的工作流程是:随机初始化k个质心,把每个样本分配到距离最近的质心,再计算每个簇的均值作为新质心,反复迭代直到质心移动量小于阈值。优化目标是簇内样本到质心的距离平方和,sklearn里记为inertia_。
这个目标隐含两个假设:簇近似球形、规模相近。如果真实簇是狭长形或嵌套结构,k均值会强行用中垂面把它们切开。在鸢尾花数据上,setosa和另外两类距离足够远,k均值能干净分离;versicolor和virginica存在特征重叠,k均值在重叠区用一个平面硬切,分界面和真实物种边界不一定重合。
这就是为什么k均值在鸢尾花上通常能达到90%上下的准确率,但很难再往上走:错分样本基本都落在versicolor和virginica的交界区域。如果业务数据是这种形态,优先考虑密度类算法或混合模型,而不是纠结k均值调参。
2.3 合并聚类:从叶子到根的层次合并
合并聚类(AgglomerativeClustering)初始把每个样本看成一簇,然后反复合并距离最近的两簇。簇间距离由linkage参数决定:
| linkage | 计算方式 | 适用形态 |
|---|---|---|
| ward | 合并后簇内方差增量最小 | 球形簇,结果接近k均值 |
| complete | 两簇间最远样本对的距离 | 受离群点影响较大 |
| average | 两簇间所有样本对平均距离 | 折中方案,计算量略大 |
| single | 两簇间最近样本对的距离 | 容易产生链式粘连 |
ward在鸢尾花数据上的结果和k均值重合度很高,因为两者都在做“簇内离差最小化”。complete和average的差异主要出现在重叠区域,重叠样本可能被分到不同簇。single则经常出现一条细长链把两个真实类串在一起,看到树状图上某两簇被一个样本逐个连接,就是典型的链式效应。遇到这种结构,不要用single做最终结论,可以先借树状图观察样本间的亲疏关系。
2.4 DBSCAN:当密度定义一切
DBSCAN用eps和min_samples两个参数定义密度:eps是邻域半径,min_samples是半径内需要达到的最少样本数。满足条件的是核心点,落在核心点半径内但自身不满足条件的是边界点,两者都不满足的是噪声点。
这个机制适合三类问题:簇形状不规则的数据、密度差异明显的数据、明确需要过滤离群点的业务数据。鸢尾花数据集样本只有150条,versicolor与virginica重叠区没有明显的密度凹陷,DBSCAN很容易把这两个类别看成同一个密度连通域。尤其在标准化后的空间里,重叠区的eps会像桥一样把两簇接起来。
因此DBSCAN在鸢尾花上往往表现不如k均值,这是数据形态与算法定位不匹配的结果。如果业务场景里存在簇形状复杂或离群点较多的需求,优先考虑DBSCAN;其余情况先用k均值和合并聚类做基线,再根据轮廓系数判断是否换算法。
3. 用pandas读取鸢尾花数据集iris并完成特征标准化
3.1 最小依赖:四个库就能跑完全部聚类代码
三种聚类算法完整跑下来,只需要scikit-learn、pandas、scipy、matplotlib四个库。scikit-learn提供KMeans、AgglomerativeClustering、DBSCAN和评估指标,pandas负责读取表格数据,scipy的cluster.hierarchy模块用来画树状图,matplotlib出图。
pip install scikit-learn pandas scipy matplotlib安装完成后先打印版本,确认依赖没有装到过老的版本:
import sklearn import pandas import scipy import matplotlib print("sklearn:", sklearn.__version__) print("pandas:", pandas.__version__) print("scipy:", scipy.__version__)如果四行版本号能被正常打印,环境就可以支撑下面的代码。需要留意的兼容点是sklearn的AgglomerativeClustering,在较新版本里参数名从affinity改成了metric,老教程里的代码直接复制会报unexpected keyword。这类差异在升级sklearn时经常遇到,先跑一段最小代码验证环境,比对着文档查配置更省时间。
3.2 用pandas读取鸢尾花数据集iris并检查数据
直接从sklearn的数据集模块加载,省去手动下载csv的步骤:
import pandas as pd from sklearn.datasets import load_iris iris = load_iris(as_frame=True) df = iris.frame df['species'] = iris.target_names[iris.target] print(df.head()) print(df.shape) print(df.isnull().sum())as_frame=True让sklearn返回带列名的DataFrame,省掉把numpy数组转DataFrame那行代码。head()查看前5行结构,shape确认行数和列数,isnull().sum()判断是否存在缺失值。鸢尾花数据集在pandas里的结构很干净:
| 检查项 | 结果 |
|---|---|
| 样本行数 | 150 |
| 特征列数 | 4 |
| 类别数 | 3(setosa / versicolor / virginica) |
| 每类样本数 | 50 / 50 / 50 |
| 缺失值 | 0 |
数据越干净,三种聚类算法的差异就越纯粹地来自算法本身。如果用真实的业务数据做同样对比,缺失值、量纲差异、类别不均衡都会干扰判断,所以这类示例数据适合用来建立算法选择的直觉。
3.3 z-score标准化:聚类前必做的预处理
检查数据后,把特征矩阵和标签分开。标签用来做外部评估,但聚类过程中不使用。
from sklearn.preprocessing import StandardScaler X = df[['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']].values y = df['target'].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) print("标准化前 petal width 标准差: {:.4f}".format(X[:, 3].std())) print("标准化后 petal width 标准差: {:.4f}".format(X_scaled[:, 3].std()))StandardScaler对每个特征列做z-score变换,输出均值为0、方差为1。标准化之后,四个特征在欧氏距离中的贡献一致,三种聚类算法的输入基准完全相同。k均值对标准化尤其敏感,因为inertia直接由距离平方和决定;DBSCAN的eps是一个绝对半径,特征缩放后eps的含义会彻底改变;合并聚类中的ward基于方差增量,同样受量纲影响。自己写聚类代码时,标准化这一步建议固定为流程的一部分,而不是临时看情况补上。
注意:fit_transform同时完成“估计均值和方差”以及“执行变换”两步。聚类场景中这不会引入信息泄露,但如果后续要用同样的scaler处理新样本,必须先保存scaler对象,再调用transform。
4. k均值、合并聚类和DBSCAN在鸢尾花数据集上的建模与调参
4.1 k均值:用肘部法则先确定k,再固定随机种子
k均值必须指定k值。鸢尾花数据集有真实标签三分类,但无监督场景下不应该直接使用这个外部信息。最基础的做法是画肘部图,观察inertia随k值的变化:
import matplotlib.pyplot as plt from sklearn.cluster import KMeans inertia = [] k_range = range(1, 9) for k in k_range: model = KMeans(n_clusters=k, random_state=42, n_init=10) model.fit(X_scaled) inertia.append(model.inertia_) plt.plot(list(k_range), inertia, marker='o') plt.xlabel('k') plt.ylabel('Inertia') plt.title('鸢尾花数据集KMeans肘部图') plt.show()inertia等于样本到质心的距离平方和,k越大,inertia必然单调下降。实际调试中看的是曲线的弯曲点:k从1增加到2、从2增加到3时下降幅度明显,k=3之后下降变得平缓,这个拐点就是合理的k。固定random_state=42保证结果可复现,n_init=10让每次初始化跑10轮取最优,避免单个坏质心把结果拖入局部最优。
确定k后建立正式模型:
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) kmeans_labels = kmeans.fit_predict(X_scaled)fit_predict在训练的同时返回每个样本的簇标签,和先fit再predict的效果一致。k均值调参的关键就三个:k的选择、n_init是否足够、random_state是否固定。很多代码里省略n_init,在sklearn 1.2之后默认行为会根据数据量自动选择,但对150样本的小数据,显式指定10次最稳妥。
4.2 合并聚类:linkage参数决定树形态
合并聚类把n_clusters当作树状图的切割层数,调参重点是linkage和metric:
from sklearn.cluster import AgglomerativeClustering hc = AgglomerativeClustering( n_clusters=3, linkage='ward', metric='euclidean' ) hc_labels = hc.fit_predict(X_scaled)linkage='ward'按合并后方差增量最小选择簇对,metric='euclidean'计算样本间距离。ward只支持欧氏距离,其他linkage可选manhattan、cosine等。想观察合并过程,用scipy画树状图:
from scipy.cluster.hierarchy import dendrogram, linkage Z = linkage(X_scaled, method='ward') plt.figure(figsize=(9, 5)) dendrogram(Z, truncate_mode='level', p=5) plt.title('鸢尾花数据集合并聚类树状图') plt.show()dendrogram的truncate_mode='level'表示只显示顶层5层,p=5控制树的纵向深度。小数据集可以直接展示完整树,数据量上万时截断是更实用的做法。对照4.1节,ward的结果和k均值接近,本来就是同样的方差最小化逻辑;想测试别的簇间距离定义,把linkage换成average或complete再跑一遍即可。
4.3 DBSCAN:先看k-distance曲线,再网格搜索
DBSCAN的两个参数直接决定簇的数量和噪声比例。最常用的参数起点是k-distance曲线:计算每个样本到第k个最近邻的距离,按升序排列后画折线,曲线上拐点对应的距离就是eps的参考值。
from sklearn.neighbors import NearestNeighbors neigh = NearestNeighbors(n_neighbors=5) neigh.fit(X_scaled) distances, _ = neigh.kneighbors(X_scaled) k_dist = np.sort(distances[:, -1]) plt.plot(k_dist) plt.title('k-distance曲线(第5近邻距离排序)') plt.show()n_neighbors=5对应min_samples=5,distances[:, -1]取的是“第5个最近邻”的距离,因为kneighbors返回的距离矩阵包括自己,第0列是自己到自己的0距离,最后一列才是有效值。排序后纵坐标快速抬升的位置就是拐点。
拿到eps参考值后,再做参数网格扫描:
from sklearn.cluster import DBSCAN for eps in [0.3, 0.4, 0.5, 0.6]: for min_samples in [3, 5, 8]: db = DBSCAN(eps=eps, min_samples=min_samples) labels_tmp = db.fit_predict(X_scaled) n_clusters_tmp = len(set(labels_tmp) - {-1}) n_noise_tmp = sum(labels_tmp == -1) print(f"eps={eps:.1f}, min_samples={min_samples}, " f"簇数={n_clusters_tmp}, 噪声={n_noise_tmp}")输出中如果某个组合得到3个簇、且噪声数量很少,这个参数就是相对合理的。注意set(labels_tmp)里会包含-1这个噪声标签,统计簇数前必须把它去掉。EPS和min_samples对DBSCAN的影响方向恰好相反:eps过大合并簇,过小制造噪声;min_samples过大时核心点减少,过小时一个孤立点也可能成为核心点。
4.4 聚类的参数对照:调参时先看结果往哪个方向偏
三种算法调参时面对的约束不同,整理成对照表方便记忆:
| 参数 | 算法 | 作用 | 调参方向 |
|---|---|---|---|
| n_clusters | k均值 / 合并聚类 | 期望簇数 | 肘部点或业务确定 |
| n_init | k均值 | 初始化次数 | 设为10以上减少局部最优 |
| random_state | k均值 | 固定随机种子 | 复现和分析稳定性时固定 |
| linkage | 合并聚类 | 簇间距离定义 | ward优先,观察链式结构用single |
| eps | DBSCAN | 邻域半径 | 从k-distance曲线的拐点起步 |
| min_samples | DBSCAN | 核心点最小邻域样本数 | 小数据集取3到5 |
如果网格搜索里没有一个DBSCAN参数组合能同时满足“3个簇”和“低噪点率”,不要为了凑数而继续上调eps。把eps调大到所有点都聚成一类,DBSCAN就退化成一种密度版的合并聚类,失去了噪声识别的意义。报告结果时,应该记录最优组合是什么,以及它在噪声率和簇数之间的取舍,而不是强行追求和真实标签一致。
5. 轮廓系数、ARI与PCA:三种聚类结果的评估与对比
5.1 内部评估:轮廓系数衡量紧致与分离
聚类没有标签时,最常用的内部评估指标是轮廓系数。对每个样本计算a(到自身簇内样本的平均距离)和b(到最近邻簇样本的平均距离),硅胶系数为(b - a) / max(a, b),最终取所有样本的平均值。
from sklearn.metrics import silhouette_score results = { 'k均值(k=3)': kmeans_labels, '合并聚类(ward)': hc_labels, 'DBSCAN(eps=0.5,min_samples=5)': dbscan_labels } for name, labels in results.items(): s = silhouette_score(X_scaled, labels) print(f"{name}: 轮廓系数 = {s:.4f}")轮廓系数的取值范围在-1到1之间,越接近1说明簇越紧致且分离度越高。在鸢尾花数据集上,k均值和合并聚类的轮廓系数通常能到0.5上下,说明这三类在标准化后的空间里确实存在可分离结构;DBSCAN的得分取决于参数选择,噪声点比例高时轮廓系数会明显下降。
但轮廓系数只回答“聚类内部结构是否自洽”,它不回答“聚类结果是否符合真实类别分布”。两个类别交错但被算法切成几个小圆球,轮廓系数反而可能很高。所以内部评估只能过滤明显差的结果,不能单独作为最终结论。
5.2 外部评估:用真实标签计算ARI和NMI
鸢尾花数据集的优势是拥有真实标签,可以做外部评估。调整兰德指数(ARI)对样本对的聚类一致程度进行计数,并校正随机分组的影响,取值1表示与真实分类完全一致,0表示随机水平。标准化互信息(NMI)衡量两个划分的互信息归一化值,对类别数量不均衡的数据更稳健。
from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score for name, labels in results.items(): ari = adjusted_rand_score(y, labels) nmi = normalized_mutual_info_score(y, labels) print(f"{name}: ARI = {ari:.4f}, NMI = {nmi:.4f}")在鸢尾花数据集中,常见参考范围如下:
| 算法 | 轮廓系数 | ARI | NMI |
|---|---|---|---|
| k均值(k=3) | 0.52 ~ 0.56 | 0.85 ~ 0.90 | 0.74 ~ 0.78 |
| 合并聚类(ward,k=3) | 0.48 ~ 0.52 | 0.80 ~ 0.85 | 0.70 ~ 0.75 |
| DBSCAN(良好参数下) | 0.35 ~ 0.55 | 0.60 ~ 0.90 | 0.60 ~ 0.85 |
以上是多次运行中常见的参考区间,具体值和sklearn版本、随机种子有关。k均值错分的样本集中在versicolor与virginica交界处,ARI到0.9附近就封顶了;DBSCAN参数好时能接近k均值,参数差时降到0.6以下。如果一次运行连0.8都到不了,先检查是否忘了标准化、k值是否选错、eps是否偏离了k-distance曲线的拐点。
5.3 PCA降维可视化:能看趋势,别当精确边界
把聚类标签画到二维平面,最常用的降维方式是PCA。PCA把数据投影到方差最大的方向上,保留的信息量能覆盖原始数据的大部分方差。
from sklearn.decomposition import PCA pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) fig, axes = plt.subplots(1, 3, figsize=(14, 4)) for ax, (name, labels) in zip(axes, results.items()): ax.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap='viridis', edgecolor='k', s=35) ax.set_title(f"{name} 聚类结果") ax.set_xlabel("PC1") ax.set_ylabel("PC2") plt.tight_layout() plt.show()在PC1和PC2的投影平面上,setosa位于左下方且与其他两类间距明显,versicolor和virginica在右上方有重叠区域。对比三张图时,重叠区域的着色差异就是算法分歧的直观来源。但要注意,PCA是线性投影,高维空间里两个点在这个投影下可能贴上,在其他方向上却相隔很远,可视化只能作为辅助验证,最终结论必须回到量化指标。
6. 聚类代码落地时的三个验证细节
6.1 用两个随机种子验证k均值的稳定性
k均值对初始质心敏感,哪怕n_init=10,不同random_state之间也可能出现完全不同的簇划分。验证方式是把两个种子的结果做一次ARI对比:
from sklearn.cluster import KMeans from sklearn.metrics import adjusted_rand_score labels_seed42 = KMeans(n_clusters=3, random_state=42, n_init=10).fit_predict(X_scaled) labels_seed0 = KMeans(n_clusters=3, random_state=0, n_init=10).fit_predict(X_scaled) print("两个种子的ARI:", adjusted_rand_score(labels_seed42, labels_seed0))ARI等于1.0表示两个种子给出的簇划分完全一致,说明数据结构的信号足够强;如果明显低于1.0,说明聚类结果依赖随机初始化,这时把n_init提升到50甚至100,让算法挑选inertia最小的解,稳定性会好很多。
6.2 从k-distance曲线读取eps,而不是拍脑袋
DBSCAN调参最常见的错误是直接从0.1开始试,要么噪声点一大堆,要么全并成一类。正确顺序是先确定min_samples(常用5),画出k-distance曲线找到拐点,再以拐点附近的值作为eps基准。如果拐点不明显,说明数据没有明显的密度断裂带,DBSCAN可能根本不是合适的选择。这个判断本身就是调参过程中最有价值的输出。
6.3 把簇中心反标准化回原始单位
k均值聚类得到的cluster_centers_位于标准化后的坐标空间,直接输出很难解读。用scaler的反变换还原到原始量纲,才能看出每个簇在各维度上的实际均值:
centers_original = scaler.inverse_transform(kmeans.cluster_centers_) center_df = pd.DataFrame(centers_original, columns=iris.feature_names) print(center_df.round(2))输出的是每个簇在sepal length、sepal width、petal length、petal width四个维度上的均值,比如簇0的花瓣长度均值是1.46,簇1是4.26,簇2是5.55。这组数值可以直接和业务方确认:簇0是“小花瓣”,簇1是“中等花瓣”,簇2是“大花瓣”。聚类结果的反向解释往往比图表更贴近实际决策,因为数值回到了原始单位,方便直接写入报告或对接后续流程。
本文还有配套的精品资源,点击获取