news 2026/9/8 23:08:07

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

作者头像

张小明

前端开发工程师

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

在 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.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.md 中df.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.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:07:04

Axure RP 9元件库加载与自制打包全攻略

简介:这是一套专为Axure RP原型设计打造的通用元件库合集,支持Axure RP 7/8/9,覆盖Bootstrap4、iOS、Android、Web流程图及扩展元件库等常见场景,适合产品经理、交互设计师和UI设计师在需求演示、交互原型搭建与高保真设计时直接调…

作者头像 李华
网站建设 2026/9/8 23:06:51

基于MATLAB的弧齿锥齿轮传动分析:从几何建模到强度校核

简介:基于Matlab的弧齿锥齿轮传动分析源码包,主要面向机械工程、车辆工程专业师生、从事齿轮设计与传动仿真的工程技术人员,适用于弧齿锥齿轮啮合特性分析、接触轨迹计算以及传动性能验证等场景。压缩包共收录11个文件,包含6个M脚…

作者头像 李华