“Lecture 5 GMM DBSCAN”——如果你是按顺序追机器学习课程,这一讲多半排在K-means之后,专门解决硬聚类解决不了的两类问题。我在真实项目里踩过同样的坑:做用户分群时,K-means 跑出来的几个簇看上去轮廓分明,一落到业务人员手里就被质疑“分群边界为什么这么生硬”“中间这群人真的属于某个类别吗”。后来把 GMM 的概率输出引入,再用 DBSCAN 处理那些形状不规则的密度区域,才把结果做成可解释、可下钻的方案。这篇内容不是课程笔记的复读,我会把 GMM 为什么要靠 EM 算法迭代、DBSCAN 的 eps 和 min_samples 到底怎么调,结合我实际跑过的离线任务讲透,还会把项目里掉过的坑直接摆出来。
文章面向三类人:正在啃聚类算法的学习者、要做用户画像或异常检测的数据分析同学、以及需要在特征工程里做分桶和标签生产的算法工程师。看完你能直接上手调参,也能在下次被问到“为什么不直接K-means”的时候,把选择逻辑讲得明明白白。
1. 核心思路:为什么 K-means 不够用,才需要 GMM 和 DBSCAN
1.1 我先讲一个翻车现场
当时我接到的任务是对一批电商用户做分层。样本量不大,大约 8 万用户,特征选了客单价、近 30 天购买次数、平均浏览深度,做标准化之后直接扔进 K-means,K 用肘部法定了 4。结果出来很尴尬:其中一个大簇占掉了 68% 的用户,剩下三个簇边缘又特别模糊。我检查过数据分布,根本没有明显的球形结构,用户的特征在二维投影里呈现的是长条形和月牙形,K-means 用质心和等方差假设去切,等于硬拿直尺量曲线,量出来的边界当然不靠谱。
这个场景其实是聚类算法选型里最常见的误区:拿着最熟悉的算法去套所有问题。K-means 本质上是每个点硬分配到一个簇,簇的形状天然偏向凸集,对噪声点也完全没有缓冲能力。遇到非凸簇、密度不均匀、簇与簇之间有重叠的情况,K-means 的误差就容易集中爆发。也正是这种挫败感,让我把注意力转向 GMM 和 DBSCAN。
1.2 两个算法的分工逻辑
GMM 解决的是“软边界”问题。它假设数据由若干个高斯分布混合生成,每个样本不是被硬性切分,而是算出属于每个高斯成分的概率。比如用户 A 有 0.7 的概率属于高活跃簇、0.3 的概率属于中等活跃簇,这个概率信息对营销策略的下钻非常有价值。GMM 还可以通过学习协方差矩阵来适应不同簇的形状,也就是说它比 K-means 多了一点“变形”能力,能拟合椭圆形的簇,同时各个簇的尺度可以不同。
DBSCAN 则换了一条完全不同的路,不看质心,只看密度。它把空间里靠得足够近、密度足够高的点连成一片,剩下的零散点标记成噪声。这条思路对簇的形状没有任何预设——你给它一个月牙形,它给你聚出一个月牙形;给它一个环形,它也能识别出来。更重要的是,它天生带异常点检测能力,你不用先清洗噪声再聚类,DBSCAN 会在聚类过程中把噪声单拎出来。
把两者放在同一讲里,本质上是在讲聚类算法的两条延伸路线:一条在概率生成模型上做文章,把聚类变成密度估计;另一条在几何密度上做文章,把聚类变成连通区域搜索。两条路线解决问题的思路完全不同,但恰好互补。
2. GMM 的核心细节:概率、协方差与 EM 迭代
2.1 模型结构到底在表达什么
GMM 的公式很简单:p(x) = Σ πₖ · N(x | μₖ, Σₖ)。其中 πₖ 是第 k 个高斯成分的混合权重,满足所有 πₖ 之和为 1;μₖ 是该成分的均值向量;Σₖ 是该成分的协方差矩阵。直观地理解,就是“总体数据是由 K 个高斯分布按不同比例混合而成的”,我们要做的是反推每个分布的参数。
我在项目里最看重的是协方差矩阵 Σₖ。sklearn 里 GMM 通过 covariance_type 参数提供了四种设置:full 允许每个成分都有自己的完整协方差矩阵,能拟合任意椭圆形状;tied 让所有成分共享同一个协方差矩阵;diag 要求每个成分的协方差矩阵是对角阵,特征之间独立;spherical 则假设每个成分是球形,退化成类似 K-means 的分布。实际用下来,用户行为数据的各个特征之间往往有相关性,比如购买次数和客单价经常正相关,所以我会优先试 full;如果样本量不够,担心过拟合,再用 diag 降参数量。
举个直观例子:你对一批客户做分群,如果只用 K-means,你默认每个群是“圆形”,群的边界是以质心为圆心的圆。但真实世界里高价值客户可能同时集中在“下单频次高但客单价低”的区间和“下单频次中等但客单价高”的区间,两个区间形成两个方向不同的椭圆。GMM 用协方差矩阵正好能抓住这种特征间的相关结构,而 K-means 会在两个椭圆交界处产生大量错误分配。
2.2 EM 算法一次看懂
GMM 的求解不能直接套最大似然估计闭式解,因为每个样本来自哪个高斯成分是隐藏变量。EM 算法就是为了处理这种“有隐藏变量”的估计问题。
EM 的核心思想是两步交替:E 步和 M 步。
E 步,固定当前的参数,对每个样本计算它属于每个成分的后验概率,也就是责任度(responsibility)。假设样本 xᵢ,当前成分 k 的参数为 μₖ 和 Σₖ,那么责任度 rᵢₖ 正比于 πₖ · N(xᵢ | μₖ, Σₖ),再对所有成分做归一化。这一步相当于把每个样本按概率“软性分配”给各个成分。
M 步,用这份责任度作为加权系数,重新估计每个成分的参数。πₖ 更新为所有样本对成分 k 的责任度平均值;μₖ 更新为责任度加权的样本均值;Σₖ 更新为责任度加权的样本协方差。
这两步反复迭代,对数似然函数会单调上升,直到收敛。实际调包时,你只需要设置 n_components 和 max_iter,sklearn 在后台自动完成 E-M 循环。但要注意一点:EM 对初始化敏感,不同的初始点可能收敛到不同的局部最优。这个问题我会在后面的常见错误里详细说。
2.3 选多少个成分才合理
GMM 里 n_components 就是事先指定的混合成分数,可以把它理解为“簇数 K”。选 K 的方法和 K-means 类似,但更推荐用信息准则,比如 BIC 或 AIC。BIC = -2·ln(L) + k·ln(n),其中 L 是模型最大似然值,k 是模型自由参数个数,n 是样本数。BIC 在拟合优度和模型复杂度之间取得平衡,值越小越好。
我一般会画一条“不同成分数对应 BIC 值”的曲线,找拐点或最小值。有过一次经历让我印象很深:某数据真实有三个簇,但 BIC 曲线在 K=6 时才最低,原因是数据里存在大量离群的小群体,多出来的成分去拟合那些小群体,导致 BIC 持续下降。当时我的处理办法是“BIC 差不多的区间里选小 K”,比如 K=4 和 K=6 的 BIC 差距只有 1.5%,那我会取 4,避免模型过度拟合那些业务上无意义的细分群体。
3. DBSCAN 的关键机制与调参逻辑
3.1 密度连通是怎么一回事
DBSCAN 的定义需要先理解三个角色:核心点、边界点、噪声点。算法设定一个半径 eps,以及最小邻居数 min_samples。
- 核心点:在以该点为中心、半径为 eps 的圆形区域内,包含的样本数不少于 min_samples。
- 边界点:不是核心点,但落在某个核心点的 eps 邻域内。
- 噪声点:既不是核心点,也不落在任何核心点邻域内的点。
在此基础上定义“密度可达”:如果点 p 在核心点 q 的邻域内,就说 p 从 q 直接密度可达;如果存在一串点 p→...→q,且相邻点之间直接密度可达,那么 p 从 q 密度可达。DBSCAN 把所有相互密度可达的点归入同一个簇。
这个机制聪明的地方在于,它不关心簇的中心在哪,只关心点与点之间的连接。所以能发现任意形状的簇,同时在边界点处理上留了余地,不像 K-means 那样强制每个点归属到最近中心。
3.2 eps 和 min_samples 到底怎么定
这两个参数磨掉了多少人的耐心,我非常清楚。先说 min_samples,一般经验做法是取特征维度的 2 倍,或者更大的数。比如二维数据,min_samples 取 4~10 比较常见;如果你处理的数据有 20 个特征,min_samples 至少取 40,否则密度估计几乎没有统计意义。如果知道数据中噪声比例较高,可以适当增大 min_samples,让核心点定义更严格。
eps 是更难的参数。一个经典方法是 k 距离图:对每个样本计算它到第 k 个最近邻居的距离,将所有距离从小到大排列,然后画曲线。曲线斜率的拐点对应的距离就是候选 eps。实际做的时候,我通常会设置 k = min_samples,然后对这个距离数组排序,在上升最陡的那个位置找拐点。
但我想提醒的是,eps 的确定不应该完全依赖拐点,还得结合业务语义回归一下。例如我聚的是地理围栏里的配送点,距离单位是公里,我直接可以知道业务方需要半径 2 公里内的点在一起,那 eps 就直接设 2.0,不需要依赖拐点。算法参数服务于业务目标,这句话放在聚类任务里尤其重要。
3.3 DBSCAN 的适用边界
DBSCAN 不是万能药。第一,它对密度变化大的数据非常吃力。如果数据里一部分簇很密集、另一部分簇很稀疏,同一个 eps 无法同时满足两边需求,稀疏簇可能被拆散,或者密集簇被合并。
第二,高维空间里“邻域”这个概念越来越稀薄。维度一高,点到点之间的距离都趋于相近,eps 邻域要么包含全量点,要么空无一物,密度估计的判别力下降。我做过一个几十维特征的数据集,DBSCAN 跑完几乎把所有点都标成噪声,后来把维度降成 5 维再用它才正常。
第三,DBSCAN 不是确定性的。同一数据集,样本顺序变化可能导致边界点归属不同簇,因为算法对点的访问顺序有依赖。虽然内部点和簇结构相对稳定,但如果你特别在意结果完全可复现,记得固定样本顺序和随机种子。
4. 实操过程:拿同一份数据过一遍 GMM 和 DBSCAN
4.1 数据和预处理准备
为了讲清楚,我构造一份简化版的电商用户行为数据。字段包括:log_avg_amount(平均客单价的对数)、purchase_freq(近30天购买次数)、browse_depth(平均浏览深度)。对数和标准化处理后,我用 PCA 把数据降到二维用于可视化,但聚类算法本身在标准化后的原始特征上运行。
有一件事必须提醒:GMM 对量纲极其敏感,不标准化的结果根本无法解释。我用 StandardScaler 把所有特征缩放成均值为 0、方差为 1。DBSCAN 对距离敏感,同样需要标准化,否则量纲大的特征会主导 eps 邻域判断。这是所有基于距离和概率的聚类算法共用的前置条件,别偷懒跳过。
4.2 GMM 实操与成分数选择
我用 scikit-learn 的 GaussianMixture 按 BIC 曲线选成分数。代码大概长这样:
from sklearn.mixture import GaussianMixture from sklearn.preprocessing import StandardScaler import numpy as np X_scaled = StandardScaler().fit_transform(X) bic_scores = [] for k in range(1, 11): gmm = GaussianMixture(n_components=k, covariance_type='full', random_state=42, max_iter=200) gmm.fit(X_scaled) bic_scores.append(gmm.bic(X_scaled)) best_k = np.argmin(bic_scores) + 1我跑完的 BIC 曲线在 k=3 之后下降趋缓,取 k=3 作为最终成分数。当年让我长记性的细节是:GMM 的 n_init 参数默认只有 1,意味着 EM 只跑一组随机初始化,非常容易陷进局部最优。我在前面踩坑后养成了习惯,n_init=10,让算法跑多组初始化选似然最大的结果,计算量多一点但稳定得多。
选定 k=3 后,看每个样本的后验概率。落在某个簇后验概率高于 0.8 的样本,是“清晰分层”用户;概率在 0.4~0.8 之间的,属于“边缘用户”;没有任何簇概率超过 0.4 的,我直接视为“不确定群体”拉出来人工复核。这种处理方式在业务方那里特别好讲,因为他们能看到“概率”而不是冷冰冰的标签。
4.3 用 k 距离图定 DBSCAN 参数
DBSCAN 这边,我先取 min_samples = 2 * n_features = 2 * 3 = 6,然后用 k 距离图找 eps:
from sklearn.neighbors import NearestNeighbors from sklearn.cluster import DBSCAN k = 6 nn = NearestNeighbors(n_neighbors=k).fit(X_scaled) distances, _ = nn.kneighbors(X_scaled) k_dist = np.sort(distances[:, -1])画出来之后,曲线在 1.2 附近有一个明显爬升拐点,我设eps=1.2, min_samples=6跑 DBSCAN。跑完看簇标签分布:大约 74% 的样本聚成 4 个簇,剩余样本被标记为噪声。这里有一个我调整过的策略:第一版 eps=1.0 的时候,噪声比例飙到 22%,簇拆成 6 个,明显过碎了。我把 eps 调到 1.4,噪声降到 8%,但两个小簇又合并在一起,业务上没有区分度。最后折中取了 1.2,保持 4 个簇和 11% 的噪声,这 11% 的噪声点最终单独验证后判定为异常行为用户。
这个过程我想强调:DBSCAN 的参数不是一次性定死的,而要在“噪声比例”和“簇数量”之间来回权衡。如果你发现噪声比例过高,多半是 eps 设太小或 min_samples 设太大;如果簇少得离谱、所有点连成一片,多半是 eps 太大。
4.4 两组结果怎么对照使用
同一份数据,GMM 给出三个概率簇,DBSCAN 给出四个密度簇加噪声。很多初学者会纠结到底哪个“对”,我的看法是两者回答的问题不同。GMM 擅长把数据切分成可解释的“分层”,比如高、中、低活跃用户,适合给运营做用户分群;DBSCAN 擅长找出“聚集区域”和“离群点”,适合做行为模式发现和异常识别。
我在项目里的常规操作是先用 DBSCAN 过滤出噪声点,把这些点单独标记为异常,再把非噪声数据交给 GMM 做细粒度分层。这样既保留了密度聚类的异常识别能力,又利用了 GMM 的概率分层能力。如果只跑 DBSCAN,那 11% 噪声点可能被当成废弃数据;如果只跑 GMM,那些异常点会被强行塞进某个概率簇里,干扰簇的参数估计。两套模型串联使用的效果,明显优于单独任一方。
5. 常见问题与排查技巧实录
5.1 GMM 常见的三个坑
第一个坑,初始化不稳定。GMM 的目标函数非凸,不同初始化经常收敛到不同结果。解决办法是调高 n_init,用多次初始化取最佳结果。如果你发现每次都收敛到不同的标签数量分布,大概率是 n_init 太低、数据重叠程度太高。
第二个坑,协方差矩阵奇异。样本量比特征维度少、或者数据存在完全冗余特征时,协方差矩阵可能变成奇异矩阵,算法直接报错或出现 NaN。解决办法是检查特征是否有多重共线性,先做 PCA 降维,或者把 covariance_type 调成 diag。我在项目里遇到过因为不小心把一个非数值 ID 列放进特征,导致协方差爆炸的情况,排查到最后一身冷汗。
第三个坑,对异常值敏感。GMM 本质是概率密度拟合,极端离群点会把某个成分的均值拉偏、协方差撑大,甚至为离群点专门分配一个成分。处理办法是聚类前先过滤或截断极端值,也可以先跑一遍 DBSCAN 或孤立森林把噪声挑走。
5.2 DBSCAN 常见的三个坑
第一个坑,eps 选得毫无依据。拍脑袋设 eps=0.5 的结果通常是灾难:有的数据点距离很近,你以为是同一个簇,实际因为 eps 太小被拆散了;有的数据点密度均匀,eps 稍微大一点全连成一片。我建议任何时候都先画 k 距离图,不要跳过这一步。
第二个坑,高维密度失真。前面提过,高维里最近邻距离分布非常扁平,k 距离图拐点不明显,DBSCAN 就失去了判别力。我的经验是:特征超过 10 维就先用 PCA 或 UMAP 降到 5 维以内再跑 DBSCAN,降维虽然会丢失一点点细节,但换来的聚类稳定性非常值得。
第三个坑,把噪声点直接丢弃。这个坑更偏思维层面。DBSCAN 标记出的 noise,不一定是“错误”,反而可能是最值得业务关注的对象。我在反欺诈类项目里,DBSCAN 分出来的噪声点里搞出了大量人工标注出来的风险样本。噪声不一定是垃圾,只是密度上“不太合群”,它们往往是异常检测的信号源。
5.3 聚类效果评估与选型建议
聚类没有唯一正确标签,评估很头疼。轮廓系数是常用的内部指标,它可以度量簇内紧密度和簇间分离度,但要注意它假设簇是凸的,对 DBSCAN 找出的复杂形状不一定公平。我常用的做法是:同时看轮廓系数、DBI(Davies-Bouldin Index)和业务留存率。如果业务方明确说“这个分群可以被策略使用”,比任何数值指标都有效。
选型建议方面,我总结一条简单的判断流:如果你想给每个样本一个概率归属,或者需要协方差结构来拟合不同形状的簇,用 GMM;如果你的数据簇形状不规则、有大量噪声,或者你想顺手做异常检测,用 DBSCAN;如果你既想概率分层,又想识别异常,像我前面说的,把两者串联起来是最稳的方案。前提永远是一样的:理解你的数据、理解你的业务问题,而不是拿着工具书按图索骥。
最后再分享一个小技巧:无论跑 GMM 还是 DBSCAN,我都会把聚类结果输出成一张“特征分布 vs 簇标签”的箱线图,直接看每个簇在原始特征上的差异。有一次就是靠这张图发现某个簇在所有特征上几乎完全相同,只是规模比其他簇小,后来查出来是数据采集端的重复记录。算法越自动化,人工验证这步就越不能省。