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
本篇技术文章基于 ML-For-Beginners 仓库的 5-Clustering 模块 展开,围绕“如何对无标签数据做聚类分析”这一核心主题,完整覆盖两节课程:先用 Seaborn 对 Spotify 抓取的尼日利亚音乐数据做探索性可视化,再用 Scikit-learn 的 K-Means 完成从特征选择、肘部法选 k 到轮廓系数与方差诊断的全流程。读完后,你将掌握一套可复制的聚类工作流:数据清洗与筛选、相关性分析、K-Means 建模与评估指标(轮廓系数、WCSS/肘部法、inertia)解读,以及识别“数据不适合 K-Means”的实战判断方法。
一、聚类是什么:模块的整体视角
5-Clustering/README.md 对聚类的定义是:聚类是一种寻找彼此相似的物体并将它们归入“簇(cluster)”的机器学习任务。它与其他机器学习方法的关键区别在于整个过程是自动的、无需标签的——模块原文甚至说,聚类在某种意义上是监督学习的“反面”。
模块选定的区域化主题是:面向尼日利亚听众的聚类分析 🎧。尼日利亚的听众构成多元,音乐品味也多元。课程使用的数据来自 Spotify 抓取,包含歌曲的danceability(可舞性)、acousticness(原声度)、loudness(响度)、speechiness(人声占比)、popularity(热度)与energy(能量)等字段。模块给出的定位非常明确:
- 当数据集缺少标签时,聚类是发现模式的首选手段;
- 若数据带有标签,前面课程学过的分类技术往往更合适;
- 聚类特别适合“先看数据、后建模型”的场景——对大型数据集先做分组压缩,再做更细粒度的分析。
在真实业务中,聚类的典型用途包括:市场细分(例如确定哪个年龄层购买哪类商品)、异常检测(如从信用卡交易中发现欺诈)、医学影像中的肿瘤定位、搜索结果分组(按商品链接、图片或评论聚类),以及用“簇 ID 代替敏感属性”来保留数据隐私——把数据点归入簇后,只用簇 ID 指代它,而不是用更具辨识度的字段。
模块还提到一个冷知识:聚类分析起源于 20 世纪 30 年代的民族学与心理学领域。
数据集:nigerian-songs.csv
本模块所有课程共享同一份数据:5-Clustering/data/nigerian-songs.csv。实际核对该文件后,数据集包含530 首歌曲、16 列,其中数值型特征列(popularity、danceability、acousticness、energy、instrumentalness、liveness、loudness、speechiness、tempo)无空值,文本列包括name、album、artist、artist_top_genre。按artist_top_genre统计,前三大流派为afro dancehall(328 首)、afropop(90 首)、nigerian pop(24 首),另有 53 首流派标记为Missing(即 Spotify 未分类)。后续课程会聚焦前三大流派并剔除popularity == 0的记录(视为噪声),最终筛选出286 首歌曲(afro dancehall206 首、afropop61 首、nigerian pop19 首)。
二、如何选择聚类算法:Scikit-learn 方法对照表
第一节课首先给出了 Scikit-learn 支持的聚类方法及其适用场景的简化对照表。这张表是本模块的方法论核心,完整继承如下:
| 方法名 | 适用场景 |
|---|---|
| K-Means | 通用、归纳式(inductive) |
| Affinity propagation | 数量多、大小不均的簇、归纳式 |
| Mean-shift | 数量多、大小不均的簇、归纳式 |
| Spectral clustering | 少量、均匀的簇、推论式(transductive) |
| Ward hierarchical clustering | 数量多、受限簇、推论式 |
| Agglomerative clustering | 数量多、受限、非欧氏距离、推论式 |
| DBSCAN | 非平坦几何、大小不均的簇、推论式 |
| OPTICS | 非平坦几何、大小不均且密度可变的簇、推论式 |
| Gaussian mixtures | 平坦几何、归纳式 |
| BIRCH | 带离群值的大数据集、归纳式 |
表中的术语需要展开理解,这也是原文档明确强调的词汇基础:
- 推论式(transductive) vs 归纳式(inductive):推论式是从“观察到的训练样本直接映射到具体测试样本”,归纳式是先从训练样本归纳出一般规则,再把规则应用到测试样本。原文给出的例子很生动:假设数据集中部分样本标了“唱片(records)”“CD”,其余空白。用归纳式,你训练一个能识别“唱片”和“CD”的模型去给空白打标,它很难识别出其实是“磁带(cassettes)”的东西;而推论式直接想办法把相似的东西聚成组再给组打标,可能得到“圆形音乐物”和“方形音乐物”这样的簇,对未知类型更鲁棒。
- 平坦(flat) vs 非平坦(non-flat)几何:源于数学术语。“平坦”指欧氏几何下的距离(两点间线段长度),“非平坦”指非欧氏几何下的距离(沿曲线测量)。如果数据可视化后看起来不落在平面上,就需要专门算法来处理。
- 距离与中心点:簇由点之间的“距离矩阵”定义。欧氏簇由均值定义,包含“质心(centroid)”即中心点,距离就是到质心的距离;非欧氏距离则使用“clustroid”(最接近其他各点的点),其定义方式更多样。
- 受限(constrained)聚类:把“半监督”引入无监督方法,通过给点之间标记
cannot-link/must-link关系来强制一些规则。例如给算法附加约束“物品必须是塑料做的”“物品必须能发出声音”,可以防止它在无标签数据上产生质量很差的分组。 - 密度(density):有噪声的数据可视为“稠密”的。各簇内部点间距的疏密不同,需要用合适的聚类方法(如 DBSCAN 一类的密度基方法)来分析。
五大类经典聚类算法
在同一节课中,文档把上百种聚类算法归纳为五大类:
- 层次聚类(Hierarchical clustering):对象依据与“近邻”而非“远处对象”的接近程度分类,簇由成员之间及与其他对象的距离形成。Scikit-learn 的 agglomerative(凝聚式)聚类即属此类。
- 质心聚类(Centroid clustering):需要先给定簇数
k,算法确定每个簇的中心点并把数据向其聚集。K-Means 是最流行的版本,中心由“最近均值”确定,算法最小化到簇的平方距离。 - 基于分布的聚类(Distribution-based):立足统计建模,核心是估计一个数据点属于某簇的概率并据此分配,高斯混合(Gaussian mixture)方法属于此类。
- 基于密度的聚类(Density-based):按密度(围绕彼此聚集的程度)给点分簇,离群体过远的点被视为离群值或噪声。DBSCAN、Mean-shift、OPTICS 属于此类。
- 基于网格的聚类(Grid-based):对多维数据建立网格,把数据划分到网格单元中从而形成簇。
三、第一节课实战:用可视化摸清数据“脾气”
1-Visualize 课程的核心命题是:聚类技术很大程度上依赖恰当的可视化,先通过可视化判断该数据适合用哪种聚类方法,再动手建模。对应练习在 notebook.ipynb 中完成,步骤如下。
1. 加载数据与基本探查
先安装 Seaborn,然后加载数据:
!pip install seaborn import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("../data/nigerian-songs.csv") df.head()前几行数据(对应 nigerian-songs.csv 的实际内容):
| name | album | artist | artist_top_genre | release_date | length | popularity | danceability | acousticness | energy | ... | tempo | time_signature |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Sparky | Mandy & The Jungle | Cruel Santino | alternative r&b | 2019 | 144000 | 48 | 0.666 | 0.851 | 0.42 | ... | 133.015 | 5 |
| shuga rush | EVERYTHING YOU HEARD IS TRUE | Odunsi (The Engine) | afropop | 2020 | 89488 | 30 | 0.71 | 0.0822 | 0.683 | ... | 129.993 | 3 |
| LITT! | LITT! | AYLØ | indie r&b | 2018 | 207758 | 40 | 0.836 | 0.272 | 0.564 | ... | 130.005 | 4 |
接着用df.info()确认结构(530 行、16 列,8 个 float64 列、4 个 int64 列、4 个 object 列,无空值),用df.isnull().sum()复核每列空值均为 0,再用df.describe()获取数值分布摘要,关键字段均值大致为:
| 字段 | mean | std | min | max |
|---|---|---|---|---|
| release_date | 2015.39 | 3.13 | 1998 | 2020 |
| length | 222298.17 | 39696.82 | 89488 | 511738 |
| popularity | 17.51 | 18.99 | 0 | 73 |
| danceability | 0.7416 | 0.1175 | 0.255 | 0.966 |
| acousticness | 0.2654 | 0.2083 | 0.000665 | 0.954 |
| energy | 0.7606 | 0.1485 | 0.111 | 0.995 |
| loudness | -4.953 | 2.464 | -19.362 | 0.582 |
| speechiness | 0.1307 | 0.0929 | 0.0278 | 0.514 |
| tempo | 116.49 | 23.52 | 61.695 | 206.007 |
这里有一个值得记住的思辨点:既然聚类是无监督方法、不依赖标签,为什么还要看带标签(流派名等)的数据?原文的解释是——在数据探索阶段标签很有帮助,但并非聚类算法所必需,你甚至可以去掉列头、只用列号引用数据。
2. 流派分布与数据清洗
用条形图看最流行的流派:
import seaborn as sns top = df['artist_top_genre'].value_counts() plt.figure(figsize=(10,7)) sns.barplot(x=top[:5].index, y=top[:5].values) plt.xticks(rotation=45) plt.title('Top genres', color='blue')若想看全部流派,把[:5]去掉即可。图中会出现Missing流派——意味着 Spotify 没有对这类歌曲分类,需要过滤掉:
df = df[df['artist_top_genre'] != 'Missing'] top = df['artist_top_genre'].value_counts() plt.figure(figsize=(10,7)) sns.barplot(x=top.index, y=top.values) plt.xticks(rotation=45) plt.title('Top genres', color='blue')清洗后重新统计,三大流派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)] top = df['artist_top_genre'].value_counts() plt.figure(figsize=(10,7)) sns.barplot(x=top.index, y=top.values) plt.xticks(rotation=45) plt.title('Top genres', color='blue')3. 相关性热力图
快速检验数值列之间是否存在强相关:
corrmat = df.corr(numeric_only=True) f, ax = plt.subplots(figsize=(12, 9)) sns.heatmap(corrmat, vmax=.8, square=True)结论只有一个强相关对:energy与loudness(响亮的歌通常也充满能量),其余相关性都相对较弱。原文特意强调:相关不等于因果,这里只证明了相关性。这个结论为下一节埋下伏笔——数据相关性弱,聚类可能并不容易。
4. 分布形态:KDE 联合分布与散点图
围绕“歌曲的热度与可舞性是否在某处收敛”这一问题,先看 KDE(核密度估计)联合分布图:
sns.set_theme(style="ticks") g = sns.jointplot( data=df, x="popularity", y="danceability", hue="artist_top_genre", kind="kde", )KDE 用连续概率密度曲线表示数据,便于在多个分布并存时解读。可以看到三个流派围绕某个“收敛点”形成大致同心圆,但整体只是松散对齐。再换成 FacetGrid 散点图交叉验证:
sns.FacetGrid(df, hue="artist_top_genre", height=5) \ .map(plt.scatter, "popularity", "danceability") \ .add_legend()散点图呈现相似的收敛模式。课程总结:对聚类而言,散点图是展示数据簇最直接的可视化手段,值得熟练。本节的课程作业要求你研究其他可用于聚类的可视化方式,在 notebook 中用散点图绘制至少五个有良好文档说明的图并解释发现(评分标准见该文件的 Rubric 表)。
四、第二节课实战:K-Means 全流程
2-K-Means 课程使用 Scikit-learn 对上一课清洗好的数据建立 K-Means 模型,涉及四个核心概念:轮廓系数(Silhouette scoring)、肘部法(Elbow method)、WCSS(inertia)、方差(Variance)。练习在 notebook.ipynb 中完成(其中已含上一课的数据导入与清洗),参考答案在 solution/notebook.ipynb。
1. K-Means 原理与 Voronoi 图
K-Means 源自信号处理领域,用于把数据划分为k个簇:每个观测样本被归入离它最近的“均值”(即簇中心点)所在簇。簇可以被可视化为 Voronoi 图——每个点(“种子”)加上其对应的区域:
K-Means 的执行是一个三步迭代过程(以 Scikit-learn 的 K-Means 实现为准):
- 算法从数据集中采样选出 k 个中心点,然后进入循环;
- 循环体内:把每个样本分配到最近的质心;用已分配样本的均值生成新质心;
- 计算新旧质心之差,反复迭代直到质心稳定。
其固有缺陷是必须事先指定k(质心数量)。好在“肘部法”能帮助估计一个好的起点值。
2. 箱线图排查异常值,选定特征列
先对每个数值列画箱线图,观察分布与离群点:
plt.figure(figsize=(20,20), dpi=200) plt.subplot(4,3,1) sns.boxplot(x = 'popularity', data = df) plt.subplot(4,3,2) sns.boxplot(x = 'acousticness', data = df) plt.subplot(4,3,3) sns.boxplot(x = 'energy', data = df) plt.subplot(4,3,4) sns.boxplot(x = 'instrumentalness', data = df) plt.subplot(4,3,5) sns.boxplot(x = 'liveness', data = df) plt.subplot(4,3,6) sns.boxplot(x = 'loudness', data = df) plt.subplot(4,3,7) sns.boxplot(x = 'speechiness', data = df) plt.subplot(4,3,8) sns.boxplot(x = 'tempo', data = df) plt.subplot(4,3,9) sns.boxplot(x = 'time_signature', data = df) plt.subplot(4,3,10) sns.boxplot(x = 'danceability', data = df) plt.subplot(4,3,11) sns.boxplot(x = 'length', data = df) plt.subplot(4,3,12) sns.boxplot(x = 'release_date', data = df)这份数据有些“吵”:逐列的箱线图都能看到离群点。逐个删除离群点会让数据变得太少,所以本课选择不删,而是挑选量纲相近的列用于聚类。选定popularity、danceability、acousticness、loudness、energy五列,并把artist_top_genre用LabelEncoder编码为数值(保留流派信息以便后续对照“模型分簇 vs 真实流派”):
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)3. 首次建模与轮廓系数
已知数据里筛出了 3 个流派,先尝试k = 3:
from sklearn.cluster import KMeans nclusters = 3 seed = 0 km = KMeans(n_clusters=nclusters, random_state=seed) km.fit(X) # Predict the cluster for each data point y_cluster_kmeans = km.predict(X) y_cluster_kmeans输出是一个数组,每行对应 0、1、2 中一个簇编号。用该数组计算轮廓系数:
from sklearn import metrics score = metrics.silhouette_score(X, y_cluster_kmeans) score轮廓系数的读法:取值范围 -1 到 1。越接近 1,说明簇内点越密集且与其他簇分离得越好;接近 0 表示簇彼此重叠、样本紧贴相邻簇的决策边界。本课的实际得分约为.53,居中——这直接说明该数据并不是特别适合这类聚类,但课程选择继续走完全流程,把“为什么不好”讲透。
4. WCSS、肘部法与 k-means++ 初始化
接下来构建用于选 k 的 WCSS 序列。这段代码中有几个值得逐词理解的概念:
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):对 1 到 10 个簇逐一迭代建模;
- random_state:决定质心初始化时的随机数生成,保证结果可复现;
- WCSS(within-cluster sums of squares,簇内平方和):衡量一个簇内所有点到该簇质心的平均平方距离;
- inertia:K-Means 的目标函数就是选择使 inertia(簇“内部一致性”的度量)最小的质心,每次迭代把
kmeans.inertia_追加进wcss; - k-means++:Scikit-learn 提供的初始化优化策略——让初始质心“通常彼此相距较远”,从而大概率优于随机初始化。
然后画肘部图,找曲线的“拐弯处”:
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()此前我们“拍脑袋”选了 3(因为数据里恰好有 3 个流派),肘部法给出的拐点确实指向 3 附近——但这一步的价值在于:选 k 的依据来自数据本身,而不是先验假设。
5. 展示簇并做“对照检查”
再次以 3 簇建模,把预测的簇编号画成散点图:
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)))模型准确率并不理想,而簇的形状给出了线索。参考答案 notebook 中对结果的表述是“不算差,但也不太好”。
五、结果诊断:方差、数据不平衡与缩放
这一节是本模块最有实战价值的部分——承认 K-Means 在此数据上表现不佳,并解释原因。
原文总结:这份数据太不平衡(286 首中afro dancehall占 206 首)、相关性太弱(前文热力图已证实)、且列与列之间的方差差异太大,导致簇形不成清晰分界。事实上形成的簇很可能被我们人为划定的三个流派标签所“带偏”。Scikit-learn 官方文档中的示例图也指出:这种簇边界不清晰的模型属于典型的“方差(variance)问题”。
方差(Variance)定义为“各数据点与均值之差的平方的平均”。放到本问题里,就是数据集的数值偏离均值偏多,各特征列的量纲与离散程度差异悬殊。
课程在 Challenge 部分给出了改进方向(原文以“提示”形式给出):
- 更彻底地清洗数据(例如移除离群值);
- 尝试使用不同的列;
- 使用不同的聚类算法(作业 assignment.md 正要求你用非 K-Means 的算法再做一遍,评分标准以文档完整度为准);
- 对数据做标准化缩放(scaling),使各列在量纲上更可比。
这一点在仓库源码层面有直接印证:solution/notebook.ipynb 中预先写好了from sklearn.preprocessing import LabelEncoder, StandardScaler,并保留了被注释的scaler = StandardScaler()与X = scaler.fit_transform(X)两行——正是供学习者打开后自行对比实验的代码。原文同时给出了一个重要观察:缩放后轮廓系数会下降,但肘部图的“拐弯”反而更平滑。原因是不缩放时,方差小的特征会携带更大的权重,扭曲了距离度量。
六、小结:一套可迁移的聚类工作流
把 5-Clustering 模块 的两节课串起来,得到的是一条完整的无监督分析流水线,可直接迁移到其他“无标签 + 需要发现分组”的场景:
- 探查:
head/info/describe/isnull四连击确认数据结构与缺失情况; - 清洗:过滤未分类(
Missing)记录与零值噪声样本,聚焦目标子集; - 可视化诊断:条形图看类别分布、热力图看相关结构、KDE 与 FacetGrid 散点图看分布形态与收敛趋势;
- 建模样板:
LabelEncoder编码分类特征 →KMeans(n_clusters, init='k-means++', random_state)固定随机种子 →predict得到簇标签; - 评估三件套:轮廓系数(簇质量)、WCSS 肘部图(选 k)、与先验标签的对照检查(诊断用);
- 失败分析:当结果不理想时,从类别不平衡、弱相关、量纲方差三个维度归因,并用
StandardScaler缩放后复测。
本模块的结论本身就是一个教学结论:并非所有数据都适合 K-Means。在相关性弱、方差差异大、类别严重不平衡的数据上,K-Means 会产生边界模糊的簇;正确做法是先用可视化读懂数据,选对算法与预处理,再谈建模。模块给出的延伸阅读方向包括:了解不同数据形状下各聚类算法的行为差异、使用 K-Means 模拟器手动调整簇数与质心、以及用 Azure Machine Learning Designer 一类的低代码工具练习聚类建模。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考