在 ML-For-Beginners 中让聚类不再依赖 K-Means:基于尼日利亚歌曲数据集实践层次、密度与分布型聚类
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
本篇技术指南围绕 translations/bn/5-Clustering/2-K-Means/assignment.md 这份课后作业展开:在学习了 K-Means 聚类之后,尝试使用一种不是 K-Means 的聚类算法,在同一份数据上建模并记录学习收获。文章会先完整还原 5-Clustering/2-K-Means/README.md 中的基线流程(数据清洗、特征选择、K-Means、轮廓系数、肘部法则),再结合 5-Clustering/1-Visualize/README.md 给出的 Scikit-learn 聚类方法地图,逐一手把手实现层次凝聚聚类、DBSCAN 与高斯混合模型三种替代方案。读完本文,你将能够独立完成该作业、对聚类结果做轮廓系数与标签一致性评估,并理解"什么时候 K-Means 不适用、该换哪种算法"。
作业任务解读:这门课后练习到底要求什么
该作业的原文指令翻译如下:
本课中你已经学习了 K-Means 聚类。有时 K-Means 并不适合你的数据。请创建一个 notebook,使用本课或其他来源的数据(请注明来源),展示一种不使用 K-Means的聚类方法。你学到了什么?
作业的评分标准(Rubric)为三档:
| 标准 | 优秀(Exemplary) | 合格(Adequate) | 需改进(Needs Improvement) |
|---|---|---|---|
| 整体交付 | 提交一个带有良好文档说明的聚类模型 notebook | 提交的 notebook文档不完整和/或内容不完整 | 提交了未完成的工作 |
由此可以看出,作业的完成度评判核心不在"模型精度多高",而在三件事:选择了非 K-Means 的算法、过程有据可查、对结果有自己的解读。这也与 5-Clustering/1-Visualize/README.md 强调的主旨一致:聚类是探索性分析,"你选用的方法取决于你的数据"。
要完成它,最好的切入点就是本仓库已经准备好的 nigerian-songs.csv 数据集、上一课的 1-Visualize/notebook.ipynb(完成数据导入与清洗),以及本课的 2-K-Means/notebook.ipynb(完成特征挑选与 K-Means 基线)。仓库在 solution 目录下还提供了答案参考(notebook.ipynb、tester.ipynb以及 R/Julia 实现),可作为提交前对照。
前置准备:数据从哪来、基线怎么打
加载并理解数据
作业允许"使用本课数据或其他来源数据(注明来源)"。最省事且最能和课程结论对话的选择,就是本课数据集:
import matplotlib.pyplot as plt import pandas as pd import seaborn as sns # 相对本课目录 5-Clustering/2-K-Means/ 的上级 data 目录 df = pd.read_csv("../data/nigerian-songs.csv") df.head()该数据集包含 530 行、16 列(曲名、专辑、艺人、艺人主流派、发行年份、时长、流行度以及 danceability、acousticness、energy、instrumentalness、liveness、loudness、speechiness、tempo、time_signature 等 Spotify 音频特征),这是 1-Visualize/README.md 中df.info()的输出内容,其中 8 列为 float64、4 列为 int64、4 列为 object,全表无空值。
K-Means 课延续上一课的数据清洗结论,只保留afro dancehall、afropop、nigerian pop三个占主导的流派,并剔除popularity == 0(无排名的噪声)的记录:
df = df[(df['artist_top_genre'] == 'afro dancehall') | (df['artist_top_genre'] == 'afropop') | (df['artist_top_genre'] == 'nigerian pop')] df = df[(df['popularity'] > 0)]上一课的探索还发现一个关键事实:数据整体相关性很弱,唯一较强相关的是energy与loudness(大声的音乐通常能量感强)。这意味着聚类算法能从这份数据中"读出"的结构有限,为后续 K-Means 表现不佳埋下伏笔——这也正是本作业选择替代算法的动机来源。
用箱线图观察离群值
在 K-Means 课程的准备环节,脚本对popularity、acousticness、energy、instrumentalness、liveness、loudness、speechiness、tempo、time_signature、danceability、length、release_date共 12 个特征逐列绘制sns.boxplot(参见 2-K-Means/notebook.ipynb):
plt.figure(figsize=(20,20), dpi=200) plt.subplot(4,3,1) sns.boxplot(x = 'popularity', data = df) # ... 依次对每个特征重复 subplot + boxplot ... plt.subplot(4,3,12) sns.boxplot(x = 'release_date', data = df)结论是"数据有些嘈杂":每个特征列都存在明显的离群点。课程给出的处理策略是不要彻底删除离群值——那会让数据量变得过小,而是选择量纲相近的列参与聚类。
构造特征矩阵 X 与编码标签
基线做法选择artist_top_genre、popularity、danceability、acousticness、loudness、energy六个特征,并用LabelEncoder将流派字符串转为数值:
from sklearn.preprocessing import LabelEncoder le = LabelEncoder() X = df.loc[:, ('artist_top_genre','popularity','danceability', 'acousticness','loudness','energy')] y = df['artist_top_genre'] X['artist_top_genre'] = le.fit_transform(X['artist_top_genre']) y = le.transform(y)注意:这里y虽被编码,但它只是用来事后核对聚类结果与真实流派的吻合度,聚类本身(无监督)并不消费标签。
K-Means 基线:3 个簇 + 轮廓系数
数据集只保留了 3 个主流流派,因此先试n_clusters = 3:
from sklearn.cluster import KMeans nclusters = 3 seed = 0 km = KMeans(n_clusters=nclusters, random_state=seed) km.fit(X) y_cluster_kmeans = km.predict(X) y_cluster_kmeans输出是一个数组,每行数据对应一个预测簇标签(0、1 或 2)。接着用轮廓系数(silhouette score)评估聚类质量:
from sklearn import metrics score = metrics.silhouette_score(X, y_cluster_kmeans) score轮廓系数取值范围为 -1 到 1:越接近 1 表示簇内稠密、簇间分离清晰;接近 0 表示簇与簇重叠,样本紧贴相邻簇的决策边界。课程中该模型得分约为0.53,"刚好在中间",说明数据并不特别适合这种(基于质心、球状假设的)聚类方式——但课程为了教学仍然继续了下去。
用肘部法则验证 k 的选择
"因为知道有 3 个流派就选 3 个簇"只是一种猜测,需要用肘部法则验证。思路是对 k 从 1 到 10 逐一运行 K-Means,记录每次的组内平方和(WCSS / inertia):
from sklearn.cluster import KMeans wcss = [] for i in range(1, 11): kmeans = KMeans(n_clusters=i, init='k-means++', random_state=42) kmeans.fit(X) wcss.append(kmeans.inertia_)对这段代码涉及的关键参数,课程有明确注释:
range(1, 11):聚类过程的迭代次数集合;random_state:决定质心初始化的随机数生成方式,固定后可复现;WCSS(within-cluster sums of squares):簇内所有点到簇质心的平方平均距离;inertia:K-Means 试图最小化的目标,即"簇内部一致程度"的度量,每次迭代后被追加进wcss变量;k-means++:Scikit-learn 提供的质心初始化优化,让初始质心彼此(大体上)远离,通常优于纯随机初始化。
绘制 k 与 WCSS 的折线图,弯折处("肘部")对应的 k 即最优簇数:
plt.figure(figsize=(10,5)) sns.lineplot(x=range(1, 11), y=wcss, marker='o', color='red') plt.title('Elbow') plt.xlabel('Number of clusters') plt.ylabel('WCSS') plt.show()该图与 images/elbow.png 一致地表明,3 或许确实是一个合理取值。
展示簇并评估"精度"
以 3 个簇重新拟合后,用popularity与danceability做散点并给不同簇着色:
from sklearn.cluster import KMeans kmeans = KMeans(n_clusters=3) kmeans.fit(X) labels = kmeans.predict(X) plt.scatter(df['popularity'], df['danceability'], c=labels) plt.xlabel('popularity') plt.ylabel('danceability') plt.show()再拿簇标签与真实流派编码对比,计算"正确率":
labels = kmeans.labels_ correct_labels = sum(y == labels) print("Result: %d out of %d samples were correctly labeled." % (correct_labels, y.size)) print('Accuracy score: {0:0.2f}'.format(correct_labels/float(y.size)))课程的结论是:模型准确率并不理想,散点图中簇的形状已经暗示了原因——这份数据过于不均衡、特征之间相关度太低、各列取值方差过大,因此难以聚出清晰边界;实际形成的簇很可能被我们事先划定的三个流派标签严重带偏(见 images/clusters.png 的效果图)。在 Scikit-learn 的文档语境里,这种簇边界不清晰的模型被称为存在variance(方差)问题:数据中各数值相对均值偏离过大。
为什么 K-Means 在这里表现不佳
K-Means 的硬伤可以总结为四点,这也是作业要求你另寻算法的全部理由:
- 必须预先指定 k:虽然肘部法则能给出参考,但"参考"不等于"正确答案";
- 簇形状假设为凸/球状:对非球形、长条形、嵌套形簇无能为力;
- 对量纲敏感:各特征取值范围差异大时,方差大的特征会主导距离计算;
- 不擅长处理噪声与密度不均:所有点都会被硬性划入某个簇,没有"噪声点"概念。
上一课 1-Visualize/README.md 将上述问题概括得更体系化:簇可以是"flat/non-flat geometry(欧氏/非欧氏几何)"、"transductive/inductive(转导/归纳)"、"constrained(带约束)"、"density(密度型)"等不同性质,K-Means 只覆盖其中很小一类。
替代算法路线图:Scikit-learn 给了你哪些选项
依据 1-Visualize/README.md 中列出的方法对照表,除去 K-Means,适合本作业"换一种方法"的候选及其适用场景是:
| 方法 | 适用场景(仓库文档原文含义) |
|---|---|
| Affinity propagation(亲和传播) | 簇多、簇大小不均,归纳式 |
| Mean-shift(均值漂移) | 簇多、簇大小不均,归纳式 |
| Spectral clustering(谱聚类) | 簇少、簇大小均匀,转导式 |
| Ward hierarchical clustering(Ward 层次聚类) | 簇多、存在约束,转导式 |
| Agglomerative clustering(凝聚层次聚类) | 簇多、存在约束、支持非欧氏距离,转导式 |
| DBSCAN | 非平坦几何、簇大小不均、存在噪声,转导式 |
| OPTICS | 非平坦几何、密度可变的簇,转导式 |
| Gaussian mixtures(高斯混合) | 平坦几何,归纳式 |
| BIRCH | 带离群点的大规模数据集,归纳式 |
按算法家族划分(同样源自该课):
- 层次聚类(hierarchical):对象根据与相邻对象的接近程度归类,Scikit-learn 的凝聚聚类属于此类;
- 质心聚类(centroid):K-Means 为代表,需指定 k;
- 分布型聚类(distribution-based):基于统计建模判断数据点属于某簇的概率,高斯混合方法属于此类;
- 密度型聚类(density-based):按点与点之间的密度聚集,远离群体的点被视为离群点/噪声,DBSCAN、Mean-shift、OPTICS 属于此类。
下方三种方案分别从层次、密度、分布三类中各取一个代表性算法,全部可直接照抄进你的作业 notebook。
方案 A:层次凝聚聚类(AgglomerativeClustering,Ward 连接)
层次聚类不需要预设"距离是到质心的欧氏距离"这种球状假设,而是从每个样本各自成簇开始,按相似度逐层合并。Scikit-learn 中 Ward 连接以最小化合并时簇内方差增量为目标,与 K-Means 的最小化 inertia 思路在数学上同源,但聚类方式完全不同——它是转导式的,直接对给定样本分组,不产出可推广到新样本的模型。
from sklearn.cluster import AgglomerativeClustering from sklearn import metrics # 先用与 K-Means 相同的 n_clusters=3 保持可比性 agg = AgglomerativeClustering(n_clusters=3, linkage='ward') labels_agg = agg.fit_predict(X) score_agg = metrics.silhouette_score(X, labels_agg) print('Agglomerative silhouette:', round(score_agg, 3)) # 与真实流派对照 correct_agg = sum(y == labels_agg) print('Matched samples: %d / %d' % (correct_agg, y.size))比 K-Means 更进一步,凝聚聚类还可以用**树状图(dendrogram)**直接观察合并过程,不需要提前拍脑袋定 k。Scikit-learn 不直接提供画树状图的 API,需要借助 SciPy:
from scipy.cluster.hierarchy import dendrogram, linkage from scipy.spatial.distance import pdist # 基于 Ward 连接的层次结构 Z = linkage(pdist(X), method='ward') plt.figure(figsize=(12, 6)) dendrogram(Z, truncate_mode='level', p=5) plt.title('Hierarchical Clustering Dendrogram (Ward)') plt.xlabel('Sample index / (cluster size)') plt.ylabel('Distance') plt.show()观察树状图中最大的纵向间隙,可以佐证"数据到底分几簇更自然",这正是作业"展示不同方法并说明学到了什么"的理想素材。
方案 B:密度型聚类(DBSCAN)
K-Means 会给每个点硬分配一个簇;DBSCAN 则完全不同:它把点划分为核心点、边界点与噪声点,噪声点会被标记为-1,而不是被硬塞进某个簇。这对本数据集的现实意义很大——上一课已指出数据存在大量离群点,而"离群点是否应该拥有一个簇标签"本身就是值得在作业里讨论的问题。
from sklearn.cluster import DBSCAN # eps 控制邻域半径,min_samples 控制成为核心点所需的最少邻居数 db = DBSCAN(eps=3, min_samples=10) labels_db = db.fit_predict(X) n_noise = list(labels_db).count(-1) n_clusters_db = len(set(labels_db)) - (1 if -1 in labels_db else 0) print('DBSCAN found %d clusters, %d noise points' % (n_clusters_db, n_noise)) # DBSCAN 结果包含 -1,直接计算 silhouette 需要剔除噪声点 mask = labels_db != -1 if mask.sum() > 1 and len(set(labels_db[mask])) > 1: score_db = metrics.silhouette_score(X[mask], labels_db[mask]) print('DBSCAN silhouette (excluding noise):', round(score_db, 3))eps与min_samples是 DBSCAN 的两个超参数:eps过小会导致几乎全是噪声,过大则把所有点并成一簇;min_samples越大,越容易把稀疏区域判为噪声。由于它们对结果影响显著,一个诚实的做法是在一定范围上网格搜索,并记录不同参数下的簇数、噪声点占比与轮廓系数,把"参数敏感性"写进作业的文档说明中。
import numpy as np for eps in [2, 3, 5, 8]: for min_samples in [5, 10, 20]: db = DBSCAN(eps=eps, min_samples=min_samples).fit(X) labels_tmp = db.labels_ n_clu = len(set(labels_tmp)) - (1 if -1 in labels_tmp else 0) n_noi = int((labels_tmp == -1).sum()) print('eps=%.1f min_samples=%2d -> clusters=%d, noise=%d' % (eps, min_samples, n_clu, n_noi))方案 C:分布型聚类(高斯混合模型,GaussianMixture)
高斯混合模型把数据看成若干个高斯分布的叠加,输出的是"每个点属于每个簇的概率"(软分配),而不是 K-Means 那样的硬 0/1 标签。它可以拟合椭圆状、大小不均的簇,并且在密度聚类认为"该点是噪声"的地方,仍能给出一个带概率的归属——这正好回应了作业开头的提示"K-Means 有时不合适"。
from sklearn.mixture import GaussianMixture # 与前面的 n_clusters 保持一致,取 3 个分量 gmm = GaussianMixture(n_components=3, random_state=0) gmm.fit(X) labels_gmm = gmm.predict(X) proba_gmm = gmm.predict_proba(X) # 每行的软分配概率 score_gmm = metrics.silhouette_score(X, labels_gmm) print('GaussianMixture silhouette:', round(score_gmm, 3)) print('Mean max membership probability:', round(proba_gmm.max(axis=1).mean(), 3)) # 与真实流派对照 correct_gmm = sum(y == labels_gmm) print('Matched samples: %d / %d' % (correct_gmm, y.size))predict_proba返回的是 n_samples × n_components 的概率矩阵;某簇的最大后验概率普遍偏低,说明大量样本在两个流派特征带之间模糊重叠——这一观察可以直接写进作业的"你学到了什么"。
共通教训:特征缩放是绕不开的一步
课程 Challenge 与 Review 部分都反复提示了特征缩放:课程指出,不做缩放的代码里留有被注释的标准化步骤;一旦启用标准缩放,"轮廓系数会下降,而肘部曲线的拐点会变平滑"。原因在于:不缩放时,方差更大的特征会在欧氏距离中携带更高权重,掩盖了其他特征的信息;缩放后各列回归到同一量纲,聚类反映的才是综合特征结构而非单一特征。
因此无论选方案 A/B/C,都建议在作业里做一组"缩放前 vs 缩放后"的对照实验:
from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # 特征缩放 + 任一聚类器组合为管道,避免数据泄漏 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 例:对缩放后的数据重跑层次聚类 agg_scaled = AgglomerativeClustering(n_clusters=3, linkage='ward') labels_agg_scaled = agg_scaled.fit_predict(X_scaled) print('Agglomerative silhouette (scaled):', round(metrics.silhouette_score(X_scaled, labels_agg_scaled), 3)) # 例:对缩放后的数据重跑高斯混合 gmm_scaled = GaussianMixture(n_components=3, random_state=0) labels_gmm_scaled = gmm_scaled.fit_predict(X_scaled) print('GaussianMixture silhouette (scaled):', round(metrics.silhouette_score(X_scaled, labels_gmm_scaled), 3))把四种结果(K-Means、Agglomerative、DBSCAN、GMM × 原始/缩放)整理成一张对比表,是让作业达到"优秀"档位的最直接方式。
对照评分细则自查与文档化建议
对照作业的 Rubric,可以从以下三个层面自查 notebook 是否达到"Exemplary":
- 选用了非 K-Means 的聚类方法:优先展示一个"从头到尾完整跑通"的算法(层次/密度/分布三选一即可),代码注释说明参数含义与选取依据;
- 文档完整:每个代码块前后用 Markdown 说明"在做什么、预期输出、为什么这么做";至少包含数据来源声明(如"数据取自本仓库 5-Clustering/data/nigerian-songs.csv,出自 ML-For-Beginners 聚类课程")、一次 silhouette 或其他内部指标评估、一次与已知流派的标签对照分析;
- 有"你学到了什么"的结论段:把课程的结论复述并验证——例如本数据"相关性弱、量纲差异大、三类流派特征带相互重叠",导致无论 K-Means 还是替代算法都难以得到高轮廓系数;此时聚类更适合用作探索性工具而非分类器,这一认知本身就是作业要的训练目标。
若想进一步提高聚类质量,可沿课程 Challenge 的方向继续:清理更多离群点、换用不同的特征组合(例如只取相关度更高的energy/loudness)、对样本加权等。仓库的 solution/notebook.ipynb 与 solution/tester.ipynb 提供了参考答案与自动评测脚本,R 语言学习者还可参考 solution/R/lesson_15-R.ipynb(其中还给出了 K-Means 的完整五步迭代描述:分配、重算质心、再分配,直到质心几乎不再移动)。
小结
本作业的真正价值不在于"找出一个比 K-Means 更准的模型",而在于体验"方法选择依赖数据性质"这一无监督学习的核心方法论。K-Means 教程用尼日利亚歌曲数据给出了 0.53 的轮廓系数与不理想的标签吻合度,恰恰为替代算法提供了绝佳的对照基线;而层次聚类让你看到合并层级、DBSCAN 让你直面噪声点、高斯混合让你获得软概率分配,三种视角互补,共同构成对同一份"难聚"数据的完整画像。按上文步骤完成建模、评估、缩放对照与文档化,即可交付一份符合仓库作业评分标准的高质量 notebook。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考