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
聚类(Clustering)是机器学习中一类典型的无监督学习任务:它自动在无标签数据中寻找彼此相似的对象,并将其归入称为"簇"(cluster)的组别。本篇文章以 ML-For-Beginners 开源课程的第五模块(5-Clustering)为核心,完整讲解聚类的概念体系、Scikit-learn 支持的聚类算法选型,以及一个贯穿两节课的真实项目——用 Spotify 尼日利亚歌曲数据探索听众的音乐品味。读完本文,你将掌握:如何对无标签数据进行探索性可视化、如何用 K-Means 建立聚类模型、如何用轮廓系数(silhouette score)与肘部法(elbow method)评估和调优聚类效果。
聚类:无监督学习的核心任务
聚类是一种机器学习任务,它寻找彼此相似的对象,并将这些对象归入被称为"簇"的组。聚类与其他机器学习方法最大的区别在于:整个过程自动发生。事实上,可以公允地说,聚类是监督学习的对立面——它假设数据集没有标签,或输入没有与预定义的输出匹配,然后使用各种算法在无标签数据中筛选并按照识别出的模式输出分组。
聚类的应用场景非常广泛:
- 市场细分:判断哪些年龄群体购买哪些商品;
- 异常检测:在信用卡交易数据中发现欺诈行为;
- 医学影像:在一系列医学扫描中圈定肿瘤区域;
- 搜索结果分组:按购物链接、图片或评论聚合搜索结果;
- 大数据集降维分析:在构建其他模型之前先用聚类了解数据;
- 隐私保护:数据被组织成簇后,可分配一个"簇 ID",用簇 ID 而非更具识别性的原始数据来引用数据点。
有趣的是,聚类分析起源于 1930 年代的人类学与心理学领域。当你的数据已带有标签时,此前课程中学过的分类技术通常更合适;但当你要对无标签数据分组时,聚类是发现模式的绝佳途径。
区域主题:尼日利亚听众的音乐品味
本模块的区域主题是"为尼日利亚听众的音乐品味建立聚类模型"。尼日利亚多元的人口拥有同样多元的音乐品味。课程使用从 Spotify 抓取的数据(数据集 5-Clustering/data/nigerian-songs.csv,原始来源为 Kaggle,基于 Spotify 数据),观察在尼日利亚流行的音乐。该数据集包含每首歌曲的以下特征:
| 特征 | 含义 |
|---|---|
danceability | 舞蹈性评分(0~1) |
acousticness | 声学性(原声程度,0~1) |
loudness | 响度(dB) |
speechiness | 语音性(含歌词说话成分的多少,0~1) |
popularity | 流行度评分 |
energy | 能量(0~1) |
instrumentalness | 器乐性(0~1) |
liveness | 现场感(0~1) |
tempo | 速度(BPM) |
time_signature | 拍号 |
length | 时长(毫秒) |
release_date | 发行年份 |
这个数据集共 530 条记录、16 列。在本模块的课程中,我们将发现这些数据中隐藏的模式——它是否会按歌曲的"可舞性"水平聚拢出某种尼日利亚听众的品味倾向?
本模块共包含两节课,对应仓库中的两个子目录:
- 引入聚类(数据可视化与探索)——对应 Notebook 5-Clustering/1-Visualize/notebook.ipynb;
- K-Means 聚类——对应 Notebook 5-Clustering/2-K-Means/notebook.ipynb。
选择合适的聚类算法:Scikit-learn 方法总览
Scikit-learn 提供了大量执行聚类的方法,选择哪种取决于你的具体用例。根据官方文档,每种方法都有各自的优势。下表是课程中给出的、Scikit-learn 支持的聚类方法及其适用场景的简化对照表:
| 方法名 | 适用场景 |
|---|---|
| K-Means | 通用目的,归纳式(inductive) |
| Affinity propagation(亲和传播) | 多而形状不规则的簇,归纳式 |
| Mean-shift(均值漂移) | 多而形状不规则的簇,归纳式 |
| 谱聚类(Spectral clustering) | 少量、均匀的簇,转导式(transductive) |
| Ward 层次聚类 | 多而受限的簇,转导式 |
| 凝聚聚类(Agglomerative clustering) | 多而受限、非欧几里得距离的簇,转导式 |
| DBSCAN | 非平面几何、形状不规则的簇,转导式 |
| OPTICS | 非平面几何、密度变化的形状不规则簇,转导式 |
| 高斯混合(Gaussian mixtures) | 平面几何,归纳式 |
| BIRCH | 含离群点的大数据集,归纳式 |
要真正读懂这张表,需要理解几个关键术语:
- 转导式 vs 归纳式(transductive vs inductive):转导式推理由观测到的训练实例推导,直接映射到特定测试实例;归纳式推理则从训练实例推导出通用规则,再将这些规则应用于测试实例。课程中的例子:假设数据集中部分对象是"黑胶唱片"、部分是"CD",还有一部分是空白的。若采用归纳式方法,训练一个寻找"黑胶唱片"和"CD"的模型,再把标签套到无标签数据上——这种方法在遇到真正是"磁带"的对象时会分类困难。而转导式方法直接对相似对象分组,再给整组贴一个标签,此时簇可能反映的是"圆形音乐物件"与"方形音乐物件"。
- 非平面 vs 平面几何(non-flat vs flat geometry):源自数学术语,指用"平面"(欧几里得)还是"非平面"(非欧几里得)几何方法测量点间距离。欧几里得距离被量化为两点之间线段的长度;非欧几里得距离则沿曲线测量。如果你的数据可视化后看起来并不处于一个平面上,可能需要专门的算法处理。
- 距离(distances):簇由其距离矩阵定义。欧几里得簇由点值的平均值定义,包含一个"质心"(centroid)或中点,距离即到该质心的距离;非欧几里得距离则引用"簇中心点"(clustroid)——即离其他点最近的点,clustroid 可以有多种定义方式。
- 受限(constrained):受限聚类在无监督方法中引入了"半监督"学习——点之间的关系被标记为"cannot link"或"must-link",从而对数据集施加某些规则。例如:如果让算法自由处理一批无标签或半标签数据,它可能把"圆形音乐物件""方形音乐物件""三角形物件"和"蛋糕"聚在一起;如果给它一些约束("物件必须是塑料做的""物件必须能播放音乐"),就能"约束"算法做出更好的选择。
- 密度(density):被视为"嘈杂"的数据被认为是"稠密"的。其各簇内点与点之间的距离经考察可能或密或疏,因此需要用合适的聚类方法分析这类数据。课程提到,K-Means 与 HDBSCAN 算法在处理含不规则簇密度的嘈杂数据集时表现不同。
五类主流聚类算法
现存超过 100 种聚类算法,具体采用哪种取决于数据性质。课程着重讨论了以下几种主要类别:
- 层次聚类(Hierarchical clustering):如果一个对象依据其与邻近对象(而非较远对象)的接近程度被归类,那么簇将根据其成员到其他对象的距离形成。Scikit-learn 的凝聚聚类即属于层次聚类。
- 质心聚类(Centroid clustering):这种流行的算法需要选择"k",即要形成的簇的数量,然后算法确定簇的中心点并围绕该点收集数据。K-Means 聚类是质心聚类的流行版本。中心点由最近的均值决定(由此得名),簇的平方距离被最小化。
- 基于分布的聚类(Distribution-based clustering):基于统计建模,聚焦于确定一个数据点属于某个簇的概率并据此分配。高斯混合方法属于此类。
- 基于密度的聚类(Density-based clustering):数据点根据其密度或彼此聚集的程度被分配到簇中;远离群体的点被视为离群点或噪声。DBSCAN、Mean-shift 和 OPTICS 属于此类。
- 基于网格的聚类(Grid-based clustering):对于多维数据集,创建网格并将数据划分到网格的单元格之间,从而形成簇。
第一课实操:数据探索与可视化
聚类技术非常依赖正确的可视化,第一课的目标就是在动手聚类之前,先充分了解数据的性质,从而判断该用哪种聚类方法。课程对应的 Notebook 位于 5-Clustering/1-Visualize/notebook.ipynb。
安装依赖并加载数据
首先安装Seaborn包以获得良好的数据可视化能力,然后加载歌曲数据:
!pip install seaborn import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("../data/nigerian-songs.csv") df.head()数据加载后,前几行记录如下(部分):
| name | album | artist | artist_top_genre | release_date | length | popularity | danceability | acousticness | energy | instrumentalness | liveness | loudness | speechiness | tempo | time_signature |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Sparky | Mandy & The Jungle | Cruel Santino | alternative r&b | 2019 | 144000 | 48 | 0.666 | 0.851 | 0.42 | 0.534 | 0.11 | -6.699 | 0.0829 | 133.015 | 5 |
| shuga rush | EVERYTHING YOU HEARD IS TRUE | Odunsi (The Engine) | afropop | 2020 | 89488 | 30 | 0.71 | 0.0822 | 0.683 | 0.000169 | 0.101 | -5.64 | 0.36 | 129.993 | 3 |
| LITT! | LITT! | AYLØ | indie r&b | 2018 | 207758 | 40 | 0.836 | 0.272 | 0.564 | 0.000537 | 0.11 | -7.127 | 0.0424 | 130.005 | 4 |
| Confident / Feeling Cool | Enjoy Your Life | Lady Donli | nigerian pop | 2019 | 175135 | 14 | 0.894 | 0.798 | 0.611 | 0.000187 | 0.0964 | -4.961 | 0.113 | 111.087 | 4 |
| wanted you | rare. | Odunsi (The Engine) | afropop | 2018 | 152049 | 25 | 0.702 | 0.116 | 0.833 | 0.91 | 0.348 | -6.044 | 0.0447 | 105.115 | 4 |
检查数据质量
调用info()查看 DataFrame 的结构信息:
df.info()输出显示:RangeIndex: 530 entries, 0 to 529,共 16 列;其中name、album、artist、artist_top_genre为 object 类型,release_date、length、popularity、time_signature为 int64,其余 8 列为 float64,总内存占用约 66.4+ KB。
接着检查是否存在空值:
df.isnull().sum()输出显示所有 16 列的非空计数均为 0,数据没有缺失值。再用describe()查看统计摘要:
df.describe()关键统计信息包括:popularity的均值约 17.5、中位数 13、最小值 0(说明存在未获得评分的歌曲,后续需要剔除);danceability的均值约 0.74,范围 0.255~0.966;loudness的范围为 -19.362~0.582 dB;tempo范围为 61.695~206.007 BPM。
思考:既然聚类是不需要标签的无监督方法,为什么还要带着标签查看数据?因为在数据探索阶段,标签是有用的,但它们并不是聚类算法运行所必需的——你完全可以去掉列标题,只用列号引用数据。
分析最受欢迎的流派
用条形图找出最受欢迎的流派(取前 5 名):
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')注意图中若出现名为 "Missing" 的流派,表示 Spotify 尚未对其进行分类,应当剔除。同时,前三大流派(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')相关性分析
快速检验数据之间是否存在特别强的相关性:
corrmat = df.corr(numeric_only=True) f, ax = plt.subplots(figsize=(12, 9)) sns.heatmap(corrmat, vmax=.8, square=True)从相关性热图可以观察到:唯一较强的相关性存在于energy与loudness之间——这并不令人意外,因为响亮的音乐通常相当有能量。除此之外,各特征间的相关性相对较弱。这正是聚类算法可以发挥作用的场景:数据维度间没有强线性结构,但可能存在非线性或局部的聚集模式。
注意:相关性并不意味着因果性!我们拥有的是相关的证据,而不是因果的证据。
数据分布与散点图
用jointplot绘制三个流派在popularity与danceability两个轴上的分布:
sns.set_theme(style="ticks") g = sns.jointplot( data=df, x="popularity", y="danceability", hue="artist_top_genre", kind="kde", )该示例使用 KDE(核密度估计)图,用连续的概率密度曲线表示数据,便于在存在多个分布时解读数据。结果显示:三个流派在流行度和可舞性方面大致松散地聚合在一起,存在围绕某个一般收敛点的同心圆分布。要在这些松散对齐的数据中确定簇将是一项挑战。
再绘制普通散点图:
sns.FacetGrid(df, hue="artist_top_genre", height=5) \ .map(plt.scatter, "popularity", "danceability") \ .add_legend()散点图显示了类似的收敛模式。一般来说,在聚类中你会用散点图来展示数据的簇,所以掌握这种可视化类型非常有用。第一课的结论是:带着这些过滤后的数据进入下一课,用 K-Means 聚类去发现数据中那些以有趣方式重叠的组。
第一课的课后任务(5-Clustering/1-Visualize/assignment.md)要求:研究不同的散点图制作方法与库,在一个 Notebook 中记录至少五个有良好文档说明的散点图,并解释你的发现。
第二课实操:用 K-Means 建立聚类模型
第二课学习如何使用 Scikit-learn 和前面导入的尼日利亚音乐数据集创建簇,覆盖 K-Means 的基础知识,并引入四个核心概念:轮廓系数、肘部法、惯性(inertia)、方差(variance)。Notebook 位于 5-Clustering/2-K-Means/notebook.ipynb,其中包含了上一课完成的数据导入与初步清洗。
K-Means 的原理
K-Means 聚类是一种源自信号处理领域的方法,用于通过一系列观测将数据划分(partition)为"k"个簇。每个观测都致力于将给定数据点分组到离它最近的"均值",即簇的中心点。
簇可以被可视化为Voronoi 图:图中包含一个点(或"种子")及其对应的区域。K-Means 的聚类过程按照一个三步流程执行:
- 算法从数据集中抽样选择 k 个中心点,然后迭代以下步骤:
- 将每个样本分配给最近的中心点;
- 取所有被分配给先前中心点的样本的平均值,创建新的中心点;
- 计算新旧中心点之间的差异,并重复迭代,直到中心点稳定下来。
K-Means 的一个缺点是你必须事先确定"k"(中心点的数量)。幸运的是,"肘部法"可以帮助估算一个好的 k 起始值。
观察数据中的离群点
首先再次审视歌曲数据,用boxplot()为每一列绘制箱线图:
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)这些数据有点"嘈杂":通过箱线图观察每一列,可以看到离群点的存在。你也可以遍历数据集移除这些离群点,但那会让数据变得相当贫乏。课程选择暂时保留它们。
特征选择与标签编码
为聚类练习选择量纲相近的列,并将artist_top_genre列编码为数值数据:
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)这里X是输入特征矩阵,y是真实流派标签(用于事后对照评估聚类效果)。
训练第一个 K-Means 模型
已知数据集中剥离出 3 种歌曲流派,所以先尝试 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输出是一个数组,为 DataFrame 的每一行给出预测的簇编号(0、1 或 2)。
用轮廓系数评估聚类质量
使用该数组计算"轮廓系数":
from sklearn import metrics score = metrics.silhouette_score(X, y_cluster_kmeans) score轮廓系数在 -1 到 1 之间取值。接近 1 的分数意味着簇既紧密又与相邻簇分离良好;接近 0 的值表示簇之间重叠,样本非常接近相邻簇的决策边界。
本例中我们的得分是0.53,属于中等水平。这提示这些数据并非特别适合这种聚类方式,但课程选择继续推进。
肘部法与 WCSS/惯性
接下来导入KMeans并启动聚类过程,通过循环尝试 1~10 个簇,计算每个 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 到 10 个簇。
- random_state:决定用于初始化中心点的随机数生成(来源:Scikit-learn
KMeans文档),设定固定值可保证结果可复现。 - WCSS:即"within-cluster sums of squares"(簇内平方和),衡量一个簇内所有点到簇中心点的平方平均距离。
- 惯性(inertia):K-Means 算法试图选择使"惯性"最小化的中心点,惯性是"簇内部一致性的度量"。每次迭代的该值被追加到
wcss变量中。 - k-means++:Scikit-learn 中可用的
k-means++优化,它初始化中心点时让它们(总体上)彼此远离,可能比随机初始化产生更好的结果。
然后使用肘部法绘制 WCSS 曲线:
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()使用上一步构建的wcss变量绘制图表,观察"肘部"的拐点,它指示最优簇的数量。对于该数据,拐点确实出现在 3 处——之前假设的 3 个簇是正确的选择。
可视化簇并评估准确率
再次以 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)))这个模型的准确率并不理想,而簇的形状给出了原因:这些数据过于不平衡、相关性太弱,列值之间的方差过大,无法形成良好的簇。事实上,形成的簇很可能受到我们上面定义的三个流派类别的强烈影响或偏差。这是一次有教育意义的尝试!在 Scikit-learn 的文档中可以看到,像这样簇边界不清晰的模型存在"方差"问题。
方差问题与改进方向
方差被定义为"与均值之差的平方的平均值"。在本聚类问题的语境中,它指数据集中数字往往偏离均值过多的趋势。
这是一个思考如何修正问题的好时机:
- 对数据做更多的调整?
- 使用其他列?
- 换一种算法?
课程给出的关键提示是:尝试缩放(scale)数据以进行归一化,并测试其他列。Notebook 中有被注释掉的代码,可以通过添加标准缩放(standard scaling)让各数据列在取值范围上更接近。你会发现:当数据不缩放时,方差较小的数据会获得更大的权重。值得注意的权衡是——数据经缩放后,虽然轮廓分数会下降,但肘部图的"拐点"会变得更加平滑清晰。
第二课的课后挑战(5-Clustering/2-K-Means/assignment.md)建议:花时间调整 Notebook 中的参数,尝试进一步清洗数据(例如移除离群点)、使用权重给予特定数据点更多权重,或尝试不同的聚类方法,看看能否改善模型。
挑战:为生产环境梳理聚类算法
作为第一课的挑战任务,请整理一份你会在生产环境中遇到并使用的不同聚类算法清单,并思考:聚类试图解决哪些类型的问题?在应用聚类算法之前,理解数据集的本质是一个好习惯;不同聚类算法面对不同类型数据时的行为差异很大。第二课还提供了一个 K-Means 模拟器思路:可视化样本点并确定它们的中心点,通过调整数据的随机性、簇的数量和中心点的数量,直观感受数据如何被分组。
小结
通过本模块两节课的完整实战,我们完成了以下闭环:
- 理解聚类本质:自动化的无监督任务,与监督学习相对,适用于无标签数据;
- 算法选型:掌握 Scikit-learn 十种聚类方法的适用场景,理解转导式/归纳式、平面/非平面几何、受限聚类与密度等核心概念;
- 数据探索:用
info()、isnull()、describe()完成数据质量检查,用条形图、相关性热图、KDE 联合分布图与 FacetGrid 散点图完成可视化判断; - 建模与评估:用 LabelEncoder 编码类别特征、K-Means 聚类、轮廓系数定量评估、肘部法确定 k 值,并最终认识到"数据太不平衡、方差过大"这一真实问题的存在及其改进方向(数据缩放、换列、换算法)。
整个流程体现了机器学习工程中一条重要的经验法则:先理解数据,再选择算法;用可视化驱动决策,用指标验证假设。你也可以在仓库对应的两个 Notebook 与翻译文档(translations/da/5-Clustering/)中继续深入研读每一处代码细节。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考