ML-For-Beginners 聚类模块实战:从数据可视化到 K-Means 的尼日利亚音乐数据无监督分析
2026/9/6 20:25:07 网站建设 项目流程

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 列,其中数值型特征列(popularitydanceabilityacousticnessenergyinstrumentalnesslivenessloudnessspeechinesstempo)无空值,文本列包括namealbumartistartist_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 一类的密度基方法)来分析。

五大类经典聚类算法

在同一节课中,文档把上百种聚类算法归纳为五大类:

  1. 层次聚类(Hierarchical clustering):对象依据与“近邻”而非“远处对象”的接近程度分类,簇由成员之间及与其他对象的距离形成。Scikit-learn 的 agglomerative(凝聚式)聚类即属此类。
  2. 质心聚类(Centroid clustering):需要先给定簇数k,算法确定每个簇的中心点并把数据向其聚集。K-Means 是最流行的版本,中心由“最近均值”确定,算法最小化到簇的平方距离。
  3. 基于分布的聚类(Distribution-based):立足统计建模,核心是估计一个数据点属于某簇的概率并据此分配,高斯混合(Gaussian mixture)方法属于此类。
  4. 基于密度的聚类(Density-based):按密度(围绕彼此聚集的程度)给点分簇,离群体过远的点被视为离群值或噪声。DBSCAN、Mean-shift、OPTICS 属于此类。
  5. 基于网格的聚类(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 的实际内容):

namealbumartistartist_top_genrerelease_datelengthpopularitydanceabilityacousticnessenergy...tempotime_signature
SparkyMandy & The JungleCruel Santinoalternative r&b2019144000480.6660.8510.42...133.0155
shuga rushEVERYTHING YOU HEARD IS TRUEOdunsi (The Engine)afropop202089488300.710.08220.683...129.9933
LITT!LITT!AYLØindie r&b2018207758400.8360.2720.564...130.0054

接着用df.info()确认结构(530 行、16 列,8 个 float64 列、4 个 int64 列、4 个 object 列,无空值),用df.isnull().sum()复核每列空值均为 0,再用df.describe()获取数值分布摘要,关键字段均值大致为:

字段meanstdminmax
release_date2015.393.1319982020
length222298.1739696.8289488511738
popularity17.5118.99073
danceability0.74160.11750.2550.966
acousticness0.26540.20830.0006650.954
energy0.76060.14850.1110.995
loudness-4.9532.464-19.3620.582
speechiness0.13070.09290.02780.514
tempo116.4923.5261.695206.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 dancehallafropopnigerian 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)

结论只有一个强相关对:energyloudness(响亮的歌通常也充满能量),其余相关性都相对较弱。原文特意强调:相关不等于因果,这里只证明了相关性。这个结论为下一节埋下伏笔——数据相关性弱,聚类可能并不容易。

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 实现为准):

  1. 算法从数据集中采样选出 k 个中心点,然后进入循环;
  2. 循环体内:把每个样本分配到最近的质心;用已分配样本的均值生成新质心;
  3. 计算新旧质心之差,反复迭代直到质心稳定。

其固有缺陷是必须事先指定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)

这份数据有些“吵”:逐列的箱线图都能看到离群点。逐个删除离群点会让数据变得太少,所以本课选择不删,而是挑选量纲相近的列用于聚类。选定popularitydanceabilityacousticnessloudnessenergy五列,并把artist_top_genreLabelEncoder编码为数值(保留流派信息以便后续对照“模型分簇 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 模块 的两节课串起来,得到的是一条完整的无监督分析流水线,可直接迁移到其他“无标签 + 需要发现分组”的场景:

  1. 探查head/info/describe/isnull四连击确认数据结构与缺失情况;
  2. 清洗:过滤未分类(Missing)记录与零值噪声样本,聚焦目标子集;
  3. 可视化诊断:条形图看类别分布、热力图看相关结构、KDE 与 FacetGrid 散点图看分布形态与收敛趋势;
  4. 建模样板LabelEncoder编码分类特征 →KMeans(n_clusters, init='k-means++', random_state)固定随机种子 →predict得到簇标签;
  5. 评估三件套:轮廓系数(簇质量)、WCSS 肘部图(选 k)、与先验标签的对照检查(诊断用);
  6. 失败分析:当结果不理想时,从类别不平衡、弱相关、量纲方差三个维度归因,并用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),仅供参考

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

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

立即咨询