首页
/ 在 ML-For-Beginners 中让聚类不再依赖 K-Means:基于尼日利亚歌曲数据集实践层次、密度与分布型聚类

在 ML-For-Beginners 中让聚类不再依赖 K-Means:基于尼日利亚歌曲数据集实践层次、密度与分布型聚类

2026-09-07 17:52:36作者:戚魁泉Nursing

本篇技术指南围绕 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.ipynbtester.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.mddf.info() 的输出内容,其中 8 列为 float64、4 列为 int64、4 列为 object,全表无空值。

K-Means 课延续上一课的数据清洗结论,只保留 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)]

上一课的探索还发现一个关键事实:数据整体相关性很弱,唯一较强相关的是 energyloudness(大声的音乐通常能量感强)。这意味着聚类算法能从这份数据中"读出"的结构有限,为后续 K-Means 表现不佳埋下伏笔——这也正是本作业选择替代算法的动机来源。

用箱线图观察离群值

在 K-Means 课程的准备环节,脚本对 popularityacousticnessenergyinstrumentalnesslivenessloudnessspeechinesstempotime_signaturedanceabilitylengthrelease_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_genrepopularitydanceabilityacousticnessloudnessenergy 六个特征,并用 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 个簇重新拟合后,用 popularitydanceability 做散点并给不同簇着色:

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 的硬伤可以总结为四点,这也是作业要求你另寻算法的全部理由:

  1. 必须预先指定 k:虽然肘部法则能给出参考,但"参考"不等于"正确答案";
  2. 簇形状假设为凸/球状:对非球形、长条形、嵌套形簇无能为力;
  3. 对量纲敏感:各特征取值范围差异大时,方差大的特征会主导距离计算;
  4. 不擅长处理噪声与密度不均:所有点都会被硬性划入某个簇,没有"噪声点"概念。

上一课 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))

epsmin_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":

  1. 选用了非 K-Means 的聚类方法:优先展示一个"从头到尾完整跑通"的算法(层次/密度/分布三选一即可),代码注释说明参数含义与选取依据;
  2. 文档完整:每个代码块前后用 Markdown 说明"在做什么、预期输出、为什么这么做";至少包含数据来源声明(如"数据取自本仓库 5-Clustering/data/nigerian-songs.csv,出自 ML-For-Beginners 聚类课程")、一次 silhouette 或其他内部指标评估、一次与已知流派的标签对照分析;
  3. 有"你学到了什么"的结论段:把课程的结论复述并验证——例如本数据"相关性弱、量纲差异大、三类流派特征带相互重叠",导致无论 K-Means 还是替代算法都难以得到高轮廓系数;此时聚类更适合用作探索性工具而非分类器,这一认知本身就是作业要的训练目标。

若想进一步提高聚类质量,可沿课程 Challenge 的方向继续:清理更多离群点、换用不同的特征组合(例如只取相关度更高的 energy/loudness)、对样本加权等。仓库的 solution/notebook.ipynbsolution/tester.ipynb 提供了参考答案与自动评测脚本,R 语言学习者还可参考 solution/R/lesson_15-R.ipynb(其中还给出了 K-Means 的完整五步迭代描述:分配、重算质心、再分配,直到质心几乎不再移动)。

小结

本作业的真正价值不在于"找出一个比 K-Means 更准的模型",而在于体验"方法选择依赖数据性质"这一无监督学习的核心方法论。K-Means 教程用尼日利亚歌曲数据给出了 0.53 的轮廓系数与不理想的标签吻合度,恰恰为替代算法提供了绝佳的对照基线;而层次聚类让你看到合并层级、DBSCAN 让你直面噪声点、高斯混合让你获得软概率分配,三种视角互补,共同构成对同一份"难聚"数据的完整画像。按上文步骤完成建模、评估、缩放对照与文档化,即可交付一份符合仓库作业评分标准的高质量 notebook。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388