ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

四种聚类算法源代码拆解:K-Means、DBSCAN、层次聚类与GMM实战

四种聚类算法源代码拆解:K-Means、DBSCAN、层次聚类与GMM实战 简介这份资源面向机器学习初学者与需要快速上手聚类分析的MATLAB用户提供FCM模糊C均值、K-means、Kmedia等经典聚类算法的源代码与配套示例帮助理解无监督分类的数学原理与实现细节。压缩包共74个文件约2.05MB以40个m脚本为核心辅以gif与png图示、txt说明、htm与css页面及mat数据文件另含一份FuzzyClusteringToolbox工具箱PDF文档覆盖数据预处理、聚类实现、结果可视化与性能评估等模块。已有4883人学习下载读者可借助示例脚本对比不同算法在模糊边界、凸形分布及非球形数据集上的表现掌握隶属度更新、聚类中心迭代与轮廓系数等评估指标的用法并直接复用工具箱函数简化开发流程。1. 四种聚类算法源代码及示例代码从调包到改源码的分水岭很多人第一次接触聚类是在 sklearn 里写三行代码KMeans(n_clusters3).fit(X)跑通画图收工。但真到了业务里数据形状一复杂这套流程立刻翻车——环形数据被 KMeans 切成两半密度不均的数据被 DBSCAN 判成一坨噪声层次聚类的树状图剪枝位置全凭手感。问题不在调包在于你没看过这四种聚类算法源代码里到底在算什么。这篇笔记围绕「四种聚类算法源代码及示例代码」展开选的是最常被拿来对比的四类K-Means、DBSCAN、层次聚类Agglomerative、高斯混合模型GMM。我会把每种算法的核心源码逻辑拆开配上能直接跑的最小示例代码再讲清楚参数怎么调、什么数据该用哪个、源码改哪一行会出什么事。适合已经会调 sklearn、但想搞明白底层在干什么的工程师也适合要手写聚类模块、不能直接依赖第三方库的场景。读完你至少能做到拿到一份聚类算法源代码知道从哪个函数入口读、哪个变量是核心、改哪里会崩。2. 四种聚类算法源代码的核心逻辑拆解2.1 K-Means 源代码迭代两步但初始化决定生死K-Means 的源码主干其实非常短核心就是一个 while 循环里交替做两件事分配样本到最近质心、根据分配结果更新质心。用 numpy 手写一版最小实现比读 sklearn 的 Cython 源码更容易看清逻辑。import numpy as np def kmeans(X, k, max_iter100, tol1e-4, seed42): rng np.random.default_rng(seed) # 初始化随机选 k 个样本作为初始质心 idx rng.choice(len(X), k, replaceFalse) centroids X[idx].copy() for i in range(max_iter): # 第一步计算每个样本到各质心的距离取最近 dists np.linalg.norm(X[:, None] - centroids[None, :], axis2) labels np.argmin(dists, axis1) # 第二步按新分配更新质心 new_centroids np.array([ X[labels j].mean(axis0) if np.any(labels j) else centroids[j] for j in range(k) ]) # 收敛判断质心移动量小于阈值就停 shift np.linalg.norm(new_centroids - centroids) centroids new_centroids if shift tol: break return labels, centroids这段代码里有两个关键点。第一初始化用的是随机选样本这是最朴素的 forgy 方式sklearn 默认用的是 k-means它会让初始质心尽量分散显著降低陷入局部最优的概率。第二空簇处理——如果某个质心没分到任何样本我这里是保留原位置实际工程中更常见的做法是重新随机选一个样本或者选离其他质心最远的点。这两个点就是 K-Means 源代码里最值得改的地方。参数方面k是唯一需要人为指定的核心参数max_iter和tol一般不用动。真正影响结果的是seed因为 K-Means 对初始质心敏感换一个随机种子可能得到完全不同的聚类结果。我一般会跑 10 次取 inertia 最小的那次这也是 sklearnn_init参数的由来。2.2 DBSCAN 源代码两个参数三种点DBSCAN 的源码比 K-Means 稍长但逻辑更直观从一个核心点出发不断把密度可达的点拉进同一个簇。核心概念只有三个——核心点邻域内样本数 ≥ minPts、边界点在核心点邻域内但自身不是核心点、噪声点既不是核心点也不在任何核心点邻域内。from sklearn.neighbors import NearestNeighbors def dbscan(X, eps, min_pts): n len(X) labels np.full(n, -1) # -1 表示未访问 cluster_id 0 # 预计算每个点的 eps 邻域 nn NearestNeighbors(radiuseps).fit(X) neighbors nn.radius_neighbors(X, return_distanceFalse) for i in range(n): if labels[i] ! -1: continue # 邻域内样本数不足 min_pts标记为噪声暂定 if len(neighbors[i]) min_pts: labels[i] -2 # -2 表示噪声 continue # 核心点开始扩展簇 labels[i] cluster_id seed_set list(neighbors[i]) while seed_set: j seed_set.pop() if labels[j] -2: labels[j] cluster_id # 噪声点被拉回边界点 if labels[j] ! -1: continue labels[j] cluster_id if len(neighbors[j]) min_pts: seed_set.extend(neighbors[j]) cluster_id 1 labels[labels -2] -1 # 最终噪声统一为 -1 return labels这段源码里最容易被忽略的是噪声点的二次判定一个点一开始邻域不足被标为噪声但后来可能被某个核心点拉进簇里变成边界点。这就是为什么代码里用 -2 暂标噪声而不是直接 -1。另一个关键点是radius_neighbors的预计算实际 sklearn 用的是 KD-Tree 或 Ball-Tree 加速数据量大时这一步决定整个算法能不能跑得动。eps和min_pts的调法有个经验规则min_pts一般取特征维度加一或两倍维度eps用 k-距离图找拐点。k-距离图的做法是对每个点算它到第min_pts近邻的距离排序后画曲线拐点位置就是合适的eps。这个图我每次换数据集都会画一遍比盲调靠谱得多。2.3 层次聚类源代码距离矩阵和链接准则才是核心层次聚类的源码主干是初始化每个点为一个簇然后不断合并距离最近的两个簇直到只剩一个簇。听起来简单但「簇间距离」怎么定义就是链接准则的选择——单链接、全链接、平均链接、Ward 链接结果差异巨大。from scipy.cluster.hierarchy import linkage, fcluster from scipy.spatial.distance import pdist def hierarchical_cluster(X, n_clusters, methodward): # 第一步计算样本两两距离 dist_matrix pdist(X, metriceuclidean) # 第二步按链接准则做层次合并 Z linkage(dist_matrix, methodmethod) # 第三步剪枝得到扁平簇 labels fcluster(Z, tn_clusters, criterionmaxclust) return labels, Zlinkage返回的Z是一个 (n-1) × 4 的矩阵每一行记录一次合并前两个数是合并的簇编号第三个数是合并时的距离第四个数是新簇的样本数。这个矩阵就是树状图的全部信息读懂它就能自己实现剪枝逻辑。链接准则的选择直接决定簇的形状偏好。单链接容易产生链式效应适合发现长条形簇全链接倾向于紧凑的球形簇Ward 链接最小化合并后的方差增量是默认值在数值特征上表现最稳。我一般先用 Ward 跑一版看树状图如果簇之间有明显的不等大小再换平均链接对比。2.4 GMM 源代码EM 算法的 E 步和 M 步高斯混合模型和前三者最大的区别是它给出的是每个样本属于每个簇的概率而不是硬标签。源码核心是 EM 算法的两个步骤交替E 步算后验概率M 步更新均值、协方差和权重。from scipy.stats import multivariate_normal def gmm(X, k, max_iter100, tol1e-6, seed42): rng np.random.default_rng(seed) n, d X.shape # 初始化随机选 k 个点作为均值协方差设为单位阵 means X[rng.choice(n, k, replaceFalse)].copy() covs [np.eye(d) for _ in range(k)] weights np.ones(k) / k for _ in range(max_iter): # E 步算每个样本属于每个簇的后验概率 resp np.zeros((n, k)) for j in range(k): resp[:, j] weights[j] * multivariate_normal.pdf(X, means[j], covs[j]) resp / resp.sum(axis1, keepdimsTrue) # M 步用后验概率加权更新参数 Nk resp.sum(axis0) new_means (resp.T X) / Nk[:, None] new_covs [] for j in range(k): diff X - new_means[j] new_covs.append((resp[:, j][:, None] * diff).T diff / Nk[j]) new_weights Nk / n # 收敛判断 shift np.linalg.norm(new_means - means) means, covs, weights new_means, new_covs, new_weights if shift tol: break return resp.argmax(axis1), respE 步和 M 步的公式看着吓人但代码逻辑就是加权平均每个样本对每个簇的参数更新贡献权重就是它属于该簇的后验概率。这里有个坑——协方差矩阵可能奇异导致multivariate_normal.pdf报错或返回 inf。工程上一般加一个小的正则项covs[j] 1e-6 * np.eye(d)或者用reg_covar参数控制。GMM 的k选择比 K-Means 更讲究因为它假设数据由 k 个高斯分布生成如果真实分布不是高斯再多簇也拟合不好。我一般用 BIC 准则选 k跑多个 k 值取 BIC 最小的那个。3. 四种聚类算法示例代码的落地跑通流程3.1 用 sklearn 生成对照数据集要验证四种算法的差异最直接的办法是造几组形状不同的数据。sklearn 的make_moons、make_circles、make_blobs刚好覆盖了球形、环形、月牙形三种典型场景。from sklearn.datasets import make_blobs, make_moons, make_circles import matplotlib.pyplot as plt # 球形簇K-Means 的主场 X_blobs, y_blobs make_blobs(n_samples500, centers4, cluster_std0.8, random_state42) # 月牙形DBSCAN 和层次聚类更合适 X_moons, y_moons make_moons(n_samples500, noise0.05, random_state42) # 环形K-Means 必翻车 X_circles, y_circles make_circles(n_samples500, noise0.05, factor0.5, random_state42) fig, axes plt.subplots(1, 3, figsize(15, 4)) for ax, (X, y, title) in zip(axes, [ (X_blobs, y_blobs, Blobs), (X_moons, y_moons, Moons), (X_circles, y_circles, Circles) ]): ax.scatter(X[:, 0], X[:, 1], cy, cmapviridis, s10) ax.set_title(title) plt.show()这三组数据是我每次对比聚类算法时的固定起手式。cluster_std控制簇的松散程度noise控制月牙和环形的噪声比例factor控制内外环间距。建议把noise从 0.05 调到 0.15 再跑一遍你会看到 DBSCAN 的eps需要跟着调而 K-Means 的结果基本不变——这就是算法假设带来的差异。3.2 四种算法在同一份数据上的对比脚本有了数据接下来把四种算法串起来跑统一评估指标用调整兰德指数ARI它不依赖标签的排列顺序。from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering from sklearn.mixture import GaussianMixture from sklearn.metrics import adjusted_rand_score def compare_clustering(X, y_true, k2): results {} # K-Means km KMeans(n_clustersk, n_init10, random_state42) results[KMeans] adjusted_rand_score(y_true, km.fit_predict(X)) # DBSCANeps 需要按数据尺度调 db DBSCAN(eps0.3, min_samples5) results[DBSCAN] adjusted_rand_score(y_true, db.fit_predict(X)) # 层次聚类 hc AgglomerativeClustering(n_clustersk, linkageward) results[Hierarchical] adjusted_rand_score(y_true, hc.fit_predict(X)) # GMM gmm GaussianMixture(n_componentsk, random_state42) results[GMM] adjusted_rand_score(y_true, gmm.fit_predict(X)) return results for name, (X, y) in [(Blobs, (X_blobs, y_blobs)), (Moons, (X_moons, y_moons)), (Circles, (X_circles, y_circles))]: print(f--- {name} ---) for algo, score in compare_clustering(X, y, k2).items(): print(f{algo}: ARI {score:.3f})跑完你会看到几个典型结果Blobs 上四种算法 ARI 都接近 1Moons 上 K-Means 掉到 0.3 左右DBSCAN 和层次聚类能到 0.9 以上Circles 上 K-Means 基本失效DBSCAN 如果eps调对了能接近 1。这个对比表就是选型的第一手依据。eps0.3这个值不是通用的它取决于数据的尺度。跑之前最好先做标准化否则eps的物理意义会随特征量纲变化。我一般会在compare_clustering外面套一层StandardScaler让所有算法在同一尺度上比较。3.3 参数敏感性速查改哪个参数会出什么事四种算法的参数敏感度差异很大下面这张表是我在实际项目中反复验证过的经验值范围。算法核心参数建议范围参数改动的典型后果K-Meansn_clusters按业务定多一个簇就多切一刀少一个簇就合并K-Meansn_init10~20低于 5 时结果随机性明显增大DBSCANeps标准化后 0.1~0.5偏小则大量噪声偏大则所有点并成一簇DBSCANmin_samples维度1 到 2×维度偏大则核心点太少簇数锐减层次聚类linkageward / averageward 偏球形average 偏长条single 易链式GMMn_components按 BIC 选偏多则过拟合偏少则欠拟合GMMcovariance_typefull / diagfull 灵活但参数多diag 适合高维这张表里最需要盯的是 DBSCAN 的eps。我见过太多次因为没做标准化eps设成 0.5 结果所有点都被判成噪声或者设成 5 结果全并成一簇。标准化之后eps在 0.1 到 0.5 之间通常能覆盖大部分场景。4. 聚类算法源代码的避坑与排查记录4.1 现象K-Means 每次跑结果都不一样原因没有固定随机种子或者n_init设得太小。K-Means 对初始质心敏感默认只跑一次初始化时不同种子会收敛到不同的局部最优。解决设random_state42同时把n_init提到 10 以上。如果数据量大跑 10 次太慢至少保证n_init5。手写实现时把初始化方式从随机选点改成 k-means收敛稳定性会明显提升。4.2 现象DBSCAN 把所有点都判成噪声原因eps太小或者数据没做标准化导致某些维度的距离被放大。还有一种可能是min_samples设得过大核心点数量不够。解决先做StandardScaler然后画 k-距离图找eps拐点。min_samples从维度1 开始试逐步往上加观察簇数和噪声比例的变化。如果噪声比例超过 30%基本可以判定eps偏小。4.3 现象层次聚类树状图剪枝后簇的大小极不均匀原因链接准则选错了。单链接容易产生链式效应一个大簇拖着一条长尾巴全链接则倾向于把大簇拆成多个小簇。解决数值特征优先用 Ward 链接它在最小化簇内方差对大小不均的簇更友好。如果数据有明显的长条形结构换平均链接试试。剪枝时不要只看簇数结合树状图的合并距离跳变位置来定跳变大的地方就是天然的分割点。4.4 现象GMM 报错「covariance is not positive definite」原因某个簇的样本数太少协方差矩阵接近奇异或者某个簇在迭代中退化成单个点。解决加reg_covar1e-6正则项或者把covariance_type从full改成diag。如果某个簇的样本数持续低于特征维度说明n_components设多了用 BIC 重新选 k。4.5 现象四种算法在同一份数据上 ARI 差异巨大不知道信谁原因ARI 需要真实标签而真实标签在无监督场景下往往不存在。有标签时差异大说明数据形状和算法假设不匹配。解决没有真实标签时用轮廓系数silhouette score或 Calinski-Harabasz 指数做内部评估。但要注意这些指标本身也有偏好——轮廓系数偏好球形簇DBSCAN 在非球形数据上得分可能偏低。我的习惯是内部指标只做参考最终选型靠业务验证比如聚类后的用户分群是否在后续转化率上有区分度。5. 从示例代码到生产聚类源码的改造与验证技巧把示例代码跑通只是第一步真正上生产还要解决三个问题数据量、增量更新、结果稳定性。数据量方面K-Means 和 GMM 的复杂度是 O(nkd)DBSCAN 在用了 KD-Tree 之后平均是 O(n log n)层次聚类是 O(n²) 起步。数据超过十万行时层次聚类基本不能直接用DBSCAN 的邻域计算也会吃满内存。我一般的做法是先对数据做分层抽样用小样本确定参数再在全量上跑 K-Means 或 MiniBatchKMeans。DBSCAN 如果必须全量跑把NearestNeighbors的algorithm参数从auto改成ball_tree在高维数据上比 KD-Tree 更稳。增量更新是另一个绕不开的点。K-Means 可以用partial_fit做在线更新但质心的漂移需要监控DBSCAN 没有原生增量接口常见做法是用新数据重新跑一遍或者用已有的核心点做近似分配。GMM 的 EM 算法可以热启动——把上一轮的均值、协方差、权重作为初值只跑少量迭代。这个技巧在流式场景下很实用但要注意协方差矩阵的累积误差跑一段时间后需要用全量数据重新初始化一次。结果稳定性方面我习惯在聚类之后加一步「簇标签对齐」。因为 K-Means 和 GMM 的簇编号是随机的两次跑出来的簇 0 可能对应不同的业务含义。做法是用上一轮的质心作为参照把新一轮的质心按最近邻匹配重新编号。这一步在 A/B 测试和线上监控里是必须的否则报表上的簇编号会跳来跳去业务方根本没法用。验证方法上除了 ARI 和轮廓系数我还会看簇的规模分布和特征均值差异。如果某个簇只占 1% 的样本或者两个簇的特征均值几乎一样那大概率是参数没调好。这时候回到第 3 章的对比脚本把k或eps微调一档再跑一遍看指标变化。最后说一个我踩过的坑不要用聚类结果直接做决策先做一次业务校验。我曾经用 GMM 把用户分成 5 群指标看着很漂亮但业务方一看就说「这群人本来就是同一类你分错了」。后来加了人工抽检环节才发现是某个特征的量纲没对齐。所以现在我的习惯是聚类跑完先抽 20 个样本人工看一眼确认簇内确实相似再往下走。这个习惯帮我省了至少三次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表