ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

混合高斯模型聚类实战:从特征工程到EM算法与Python实现

混合高斯模型聚类实战:从特征工程到EM算法与Python实现 最近在整理一批用户行为数据需要做无监督学习聚类的时候我把几个经典算法来回试了一圈最后真正解决我问题的反而是被很多人当成“K-Means高级版”的混合高斯模型GMM。这个名字听起来唬人实际用下来你会发现它的思路很朴素整体数据大概率是几个互相重叠的高斯分布叠在一起我们要做的就是把这一堆分布拆开顺便给每个样本算一个“属于某个簇”的概率而不是硬生生划出一条边界。这篇文章会从特征提取、特征融合、特征降维这些前置步骤讲起把GMM聚类的完整链路拆开揉碎最后给出一套可以直接跑的Python代码。适合正在做聚类、用户分群、异常识别或者刚接触无监督学习想系统理解GMM的读者。1. 为什么聚类任务会选到混合高斯模型1.1 从K-Means的“球状强迫症”说起很多搞机器学习的人第一个学的聚类算法就是K-Means我也一样。K-Means确实简单、快、还好解释但它有一个经常被忽略的硬假设它用欧氏距离来度量样本与聚类中心的相似度这意味着它天然会倾向于生成“球形”的簇而且这个球在各个方向上的半径都得差不多。你回想一下K-Means的迭代过程就知道它只更新簇中心不考虑每个簇的宽度、走向和密度差异。现实里的数据哪有这么听话。比如用户活跃时长和消费金额这两个维度高价值用户往往在“时长中等、金额很高”的区域形成一个斜着的长条形分布普通用户则聚成另一个区间两个分布在边界上还会交叉。你用K-Means去分十有八九会把这条斜着的长条从中间硬切成两半分出来的簇毫无业务解释。我最早做客户分群时就踩过这个坑簇内看起来方差很小但画出来一看一个簇吃掉了两个群体的头部非常尴尬。还有一点K-Means给每个样本的输出只有一个标签它不告诉你这个样本被分到这一簇的置信度是多少。边界上的样本到底属于A还是BK-Means的答案是“非A即B”没有任何中间地带。可实际业务中边界样本恰恰是最需要谨慎处理的比如风险用户、流失边缘用户它们往往是“既像A又像B”的模糊状态你直接给一个硬标签后续风控或运营动作就会变得特别武断。1.2 混合高斯模型到底在算什么GMM换了一个思路。它不再先找几个中心点而是假设整个数据集是由若干个高斯分布也就是钟形曲线混合在一起生成的。每个高斯分布代表一个潜在的簇混合权重表示这个簇在整体里占多大比例。数学上写成P(x) Σ πk · N(x | μk, Σk)其中πk是第k个高斯分布的权重μk是均值向量Σk是协方差矩阵。这个公式并不难理解我随便扔出一个样本点它可能来自第一个分布的“地盘”也可能来自第二个只是来源不同概率不同。聚类的目标就变成了估计这些πk、μk、Σk使得整体数据被这些高斯分布“解释”得最好。这就是GMM最核心的优势——软聚类。它给每个样本输出的是一个后验概率向量比如“这个用户有70%概率属于高价值群30%概率属于普通群”然后你可以取概率最大的那一类作为最终标签也可以直接把概率保留下来做下游任务。等于说GMM既给了你聚类结果又给了你置信度这在K-Means里是没有的。另外由于引入了协方差矩阵ΣkGMM允许每个簇是任意方向、任意粗细的椭圆形状。换成生活类比来说K-Means像是用圆规在纸上画圈GMM则是拿着一支可以随便旋转和伸缩的画笔。它能拟合出斜着的椭圆、细长的带状、甚至重叠的两个分布这也是它在实际数据上比K-Means更稳的根本原因。1.3 和层次聚类、DBSCAN放在一起怎么选每次有人问聚类算法选型我脑子里都会过一遍三五个候选层次聚类、DBSCAN、K-Means、GMM。它们的适用边界其实挺清晰。层次聚类的好处是能输出一棵完整的谱系树你可以从树状图里直观看到数据从“一个簇”逐渐分裂到“每个样本独立”的过程解释性极强。但它最大的缺点是复杂度偏高样本量到了几万级别计算代价就开始让人难受而且它对高维数据的距离度量也很敏感业务上一般用于小样本的探索性分析。DBSCAN则是靠密度定义簇能发现各种奇形怪状的簇还能顺便把稀疏区域的点标记为噪声这个特性在空间数据、地理坐标聚类里很好用。但它的参数邻域半径eps和最小样本数min_samples非常难调因为数据密度一旦分布不均一套参数只能适配局部区域。换个场景可能结果就完全变样了。GMM的特长恰恰是处理“分布重叠”和“椭圆簇”。如果两个群体在特征空间里有明显的大面积交叠DBSCAN很容易把它们连成一个簇层次聚类也未必能切开而GMM可以通过协方差矩阵把重叠部分“按概率切开”。所以我的选型经验是先看数据形状如果怀疑是多个高斯混合体、边界模糊、又需要输出概率直接上GMM如果追求非凸簇和噪声标记用DBSCAN如果只是想要一个快速基准结果先跑K-Means。下面这张表可以辅助决策算法簇形状假设分配方式输出主要短板K-Means各向同性的球形簇硬分配标签无法捕获椭圆簇和重叠分布层次聚类无固定形状硬分配树状图标签样本量大时计算慢DBSCAN密度连通区域硬分配噪声标签、噪声点eps参数难调密度不均时不稳定高斯混合模型任意方向的椭圆簇软分配标签概率对高维协方差估计敏感需要调分量数1.4 什么情况下别用GMMGMM也不是万能钥匙。我在实际项目里遇到过几次GMM表现反而更差的情况。第一种是数据形状是环形或者马蹄形比如传感器数据绕着一个圆环分布GMM用几个椭圆去拟合这个环需要叠加很多分量才能凑出形状不如DBSCAN一步到位。第二种是样本量很小但特征维度很高比如只有200条样本、特征却有300维此时每个簇的协方差矩阵需要估计很多参数模型很容易过拟合跑出来的聚类结果可能纯粹是噪声。第三种情况是业务只关心快速迭代。如果团队已经有一套K-Means的管线在跑而且分簇效果勉强能接受我不会劝你立刻换GMM。GMM的迭代毕竟要反复计算高斯密度和协方差逆矩阵耗时比K-Means高一个量级。先想清楚你要不要“概率输出”和“椭圆簇”再决定是否引入这个复杂度。2. 特征链路提取、融合与降维2.1 特征提取给GMM喂输入前的第一步不管用什么聚类算法最终形态都是“输入特征矩阵输出标签”。GMM也一样它不会替你决定用什么特征。特征提取这一步往往是整个项目里最影响结果、但又最容易被忽略的环节。对于结构化表格数据比如用户的年龄、收入、页面浏览次数特征提取相对简单把字段整理成数值矩阵处理掉缺失值和偏态分布再做标准化。对于图像数据常用做法是提取颜色直方图、纹理特征或者直接取深度学习网络中间层的Embedding向量这些向量比原始像素更适合高斯分布假设。对于文本数据早期多用TF-IDF向量现在则更多用Sentence Embedding把整句话压缩成一个固定维度的向量。这里有一个需要反复强调的细节GMM对输入特征的尺度非常敏感。因为GMM的协方差矩阵直接依赖数值的大小和单位如果“收入”字段的单位是元数值在几千到几万而“浏览次数”只有个位数那么收入和浏览次数在协方差矩阵里的“贡献”完全不在一个量级。很多人一开始不标准化跑出来的簇几乎等于只按收入字段切分这是典型的“特征量纲碾压”。所以无论用什么特征第一步我都会先做标准化z-score让每个特征有零均值和单位方差再进模型。2.2 特征降维为什么GMM之前总喜欢配PCAGMM每个簇要估计一个协方差矩阵当特征维度升高时协方差矩阵的参数数量是按维度平方增长的。一个d维的全协方差矩阵需要d(d1)/2个参数再加上d个均值和1个权重。这意味着在高维场景下如果样本量不够充足协方差矩阵的估计会非常不稳定动不动就出现奇异矩阵模型没法收敛。这也是GMM在高维数据上总是和PCA搭配出现的原因。PCA负责把原始特征做线性变换去掉特征之间的线性相关性并把最关键的方差集中到少数几个主成分上。降维之后GMM需要估计的参数大幅减少训练更稳定速度也会快很多。我在项目里一般的做法是先用PCA看累计解释方差曲线取累计方差贡献率在85%到95%之间的主成分数量再用降维后的数据去跑GMM。如果只是为了可视化则保留两个主成分画散点图配合聚类标签观察分布形态。特别提醒一句t-SNE和UMAP这类非线性降维方法虽然画图很漂亮但一般不建议拿它们的输出作为GMM的输入。原因很简单t-SNE追求的是局部结构近似它会在降维过程中扭曲簇的形状、尺寸和距离关系这些被扭曲后的坐标值很难满足高斯分布的假设。你用t-SNE降维再聚类聚出来的类很可能和原始特征空间里的真实簇并不对应。2.3 特征融合多源特征不是简单拼在一起做真实项目时特征往往不是单一来源。比如用户分群里常见的做法是把三类特征拼起来一是行为统计特征活跃天数、访问频次、平均停留时长二是内容偏好特征类别点击占比、收藏量三是时间规律特征凌晨活跃占比、工作日/周末活跃差异。这三类特征无论是含义、量纲还是分布形态差异都很大直接concat成一个矩阵喂进模型麻烦很快就来了。我踩过最明显的坑是量纲不一致行为统计特征动辄几十上百内容偏好特征可能是0到1的比例时间规律特征可能是百分比。把它们直接拼在一起GMM的协方差矩阵会被这些数值尺度带着走最后聚类结果完全由量纲大的特征主导内容偏好信息全部失效。所以做特征融合时我通常分两步走。第一步每个特征子块内部先单独标准化把频次、比例、角度等不同性质的特征拉到同一个尺度第二步再对融合后的整体特征矩阵做一次PCA降维生成一组去相关的主成分特征。这样做的好处是既保留了各来源特征的信息又避免了某一类特征凭借数值大小“带节奏”。如果某些特征本身是类别型字段比如设备类型、所在城市最好先用独热编码或目标编码转成数值再参与融合。3. 实操过程从数据到GMM聚类的完整项目3.1 准备一份可以立刻复现的数据为了讲清楚完整流程我不用那些动不动就需要授权的商业数据直接用scikit-learn自带的模拟数据来演示。make_blobs可以生成高斯分布簇恰好贴合GMM的模型假设代码跑完就能出结果。我先构造三个高斯簇每个簇的样本数量不同协方差也不同其中两个簇有部分重叠模拟真实业务里“边界模糊”的场景import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.mixture import GaussianMixture from sklearn.cluster import KMeans np.random.seed(42) X, y_true make_blobs( n_samples1200, centers[[1, 1], [3, 3], [6, 2]], cluster_std[0.4, 0.9, 0.6], random_state42 ) # 把第二个簇适当拉伸制造椭圆分布 X[:, 1] np.where(y_true 1, X[:, 1] * 1.8, X[:, 1])这里y_true是真实标签只在评估时用建模过程不会把它喂给GMM。先画出数据分布你会看到第二个簇明显是一个竖向拉长的椭圆而且和第一个簇有部分重叠。这种数据就是K-Means最不擅长、GMM相对擅长的情况。3.2 EM算法到底是怎么迭代的GMM的参数不能像K-Means那样直接解出来因为每个样本来自哪个高斯分布是个隐藏变量我们不知道。这个问题求不出解析解于是就用EM算法期望最大化来逼近。EM算法的迭代逻辑分两步很像“先猜后修正”的循环。第一步叫E步Expectation在参数固定的情况下根据当前的高斯分布参数计算每个样本属于每个簇的后验概率也就是“这个样本现在有多大可能来自第k个簇”。第二步叫M步Maximization用上一步算出的概率作为权重重新估计每个簇的权重πk、均值μk和协方差矩阵Σk。具体更新规则是πk等于所有样本对第k簇后验概率的平均值μk等于所有样本按后验概率加权后的平均向量Σk则是按后验概率加权的“样本到均值距离的外积平均”。这个循环一直进行直到对数似然的变化量小于某个阈值或者达到最大迭代次数。你可以把它理解成一群人在分组讨论先凭印象把每个人分到某个小组里然后根据各小组成员的共同点更新小组的“画像”再用新的画像重新调整人员归属反复几次分组越来越合理。EM算法就是这套流程的数学版。3.3 关键参数n_components与covariance_type用sklearn的GaussianMixture最核心的两个参数是n_components和covariance_type。n_components是混合分量的个数也就是你想聚成几类。这个参数需要你根据业务经验或者BIC/AIC曲线来定我后面的调参部分会专门展开。covariance_type决定每个簇的协方差矩阵长什么样它直接影响模型能表达什么形状的簇也决定参数数量。四种选择分别如下类型含义参数数量适用场景full每个簇有自己的完整协方差矩阵最多最常见适合各簇形状差异大的数据tied所有簇共享同一个协方差矩阵较少簇形状近似但均值不同的数据diag每个簇的特征之间互相独立中等特征相关性弱想降低过拟合风险时spherical每个簇是球形协方差为对角且各方向相等最少近似K-Means但保留了概率输出我的习惯是默认用full因为GMM的核心卖点就是椭圆簇。如果发现协方差矩阵不稳定或者样本量不足再退到diag或tied牺牲一些形状表达能力换取更稳定的训练。3.4 完整代码与结果解读下面是一份最小可用的完整代码覆盖标准化、PCA降维、GMM聚类、结果对比四个环节# 1. 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 2. PCA降维这里原始数据只有2维降维主要为了演示 pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) # 3. GMM聚类 gmm GaussianMixture(n_components3, covariance_typefull, random_state42) gmm.fit(X_pca) gmm_labels gmm.predict(X_pca) gmm_proba gmm.predict_proba(X_pca) # 4. K-Means作为对比基线 kmeans KMeans(n_clusters3, random_state42, n_init10) kmeans_labels kmeans.fit_predict(X_pca) # 5. 可视化对比 fig, axes plt.subplots(1, 3, figsize(15, 4)) axes[0].scatter(X_pca[:, 0], X_pca[:, 1], cy_true, cmapviridis, s20) axes[0].set_title(True Labels) axes[1].scatter(X_pca[:, 0], X_pca[:, 1], ckmeans_labels, cmapviridis, s20) axes[1].set_title(K-Means) axes[2].scatter(X_pca[:, 0], X_pca[:, 1], cgmm_labels, cmapviridis, s20) axes[2].set_title(GMM) plt.show() # 6. 边缘样本的后验概率 edge_idx np.argmax(np.min(gmm_proba, axis1)) print(最模糊样本的簇归属概率:, gmm_proba[edge_idx])跑完之后你会看到两个明显差异。第一在椭圆簇和重叠区域K-Means会沿着空间垂直中线把重叠部分一刀切开分出来的形状像是被“砍”过而GMM的边界是平滑的贴合了两个高斯分布的交叉地带。第二GMM会输出概率最模糊的样本在两个簇之间的概率可能接近五五开如果你用K-Means这个样本只会被强行塞进某一类。我在真实项目里还会多做一步把概率小于某个阈值的样本单独拎出来看。这些样本往往是异常值、脏数据或者新出现的用户群体值得人工分析而不是直接打标签进下游。3.5 调参技巧BIC/AIC曲线选分量数n_components不是越大越好因为分量越多模型复杂度越高过拟合风险越大。常用的做法是画BIC贝叶斯信息准则或AIC赤池信息准则曲线。BIC的公式大致是-2乘以对数似然加上参数数量乘以ln(n)它同时惩罚了“似然不够高”和“参数太多”两头。实际操作时把n_components从1试到10每个值跑一次GMM记录BIC值然后看曲线最低点。曲线最低点对应的分量数就是模型在“拟合能力”和“复杂度”之间的平衡点。AIC的逻辑类似但惩罚力度更小通常更偏向于选择稍多一些的分量数。有一个经验值得分享BIC曲线有时会一直下降没有明显最低点。这时候不能机械地选最低点而要结合业务解释。比如你分12类BIC最低但分出来有3个簇只包含十几条样本业务上完全解释不了我宁可放弃那个“最优”选一个BIC稍微高一点但簇规模合理、业务含义清晰的分量数。模型指标要为业务服务不是让业务去迁就指标。4. 常见问题与排查技巧实录4.1 协方差矩阵奇异、训练不收敛怎么办GMM训练时最常见的报错是协方差矩阵奇异singular或者直接给你一个ConvergenceWarning。这个问题的本质是某个高斯分布被“推”到了一个极端的数值范围里样本量不足以支撑它估计一个有效的协方差矩阵。我排查这个问题有固定顺序。先看一眼数据有没有做标准化这一步最常被漏掉。再检查特征中是否有重复列或高度线性相关的字段PCA降维往往能直接解决这个问题。然后看n_components是不是开得太大分量数接近或超过真实样本数时协方差矩阵必炸。最后还有一个常用兜底手段给GaussianMixture传一个reg_covar参数比如reg_covar1e-4。这个参数会在协方差矩阵的对角线加一个很小的常数防止矩阵在计算逆的时候变成奇异矩阵代价是稍微改变一点簇的形状但能保命。另外需要注意GMM对异常值很敏感。一个极端离群点会像磁铁一样把某个簇的均值拽偏还可能撑大协方差矩阵导致其他正常簇的边界变得异常宽松。数据里如果有明显的离群点先做一遍异常筛选或者使用RobustScaler这类对异常值更稳健的标准化方法会比直接在GMM里硬扛更有效。4.2 分量数怎么选才靠谱BIC曲线我已经在上文提过但实际项目里还有另一个常被忽略的信号完全空置的簇。当你设置n_components8但跑完之后发现其中两个簇的成员数量只有零星几个或者几乎没有样本以高概率归属到它们这说明分量数可能过头了。我会额外看一下每个簇的“最大后验概率”均值。如果某个簇的成员普遍只有0.3到0.4的归属概率说明这个簇并没有清晰地“圈住”任何一批样本它只是在给边界样本硬凑出一个糊状区域。这种簇对业务没有价值应该减少分量数。还有一种做法是逐步增加n_components每次增加后看新增簇是否带来了可解释的新群体。如果新簇只是把原有簇拦腰截断没有任何业务含义说明它只是在“过度拟合”数据的局部密度波动。4.3 随机种子和初始化陷阱GMM的EM算法对初始化很敏感不同的初始参数可能收敛到不同的局部最优解。sklearn默认使用K-Means的结果来初始化GMM的均值和协方差这是一个比较聪明的策略因为K-Means本身已经能提供一个不差的起点EM在这个基础上做精修。但即使如此我仍然建议在训练时固定random_state尤其在对比实验里。不然你跑了10次调参结果每次标签都在变根本无法判断是参数引起的波动还是初始化随机性引起的波动。我见过有人调了一个晚上参数最后发现只是随机种子没固定纯属白忙。如果要更稳妥可以把n_init设置成5或者10sklearn会从多个初始化里选对数似然最高的结果。代价只是多花一点训练时间但能显著降低“运气不好”的概率。4.4 从聚类升级到分类器的小技巧很多人以为GMM只能做无监督聚类其实训练好的GMM完全可以当分类器用因为它天生就能输出概率。区别在于聚类时我们不知道标签GMM靠数据自己找分组而在监督场景下你可以给GMM指定每一类对应的高斯分布然后对新样本算它属于每一类的后验概率。我最常用的一种落地方式是半监督结合先用少量带标签的数据确定簇的对应关系比如第1个高斯分布对应“高价值用户”第2个对应“普通用户”然后对大量无标签数据跑GMM输出概率把高概率的样本直接当成伪标签加入训练集。这种做法在冷启动阶段特别有用标注数据不够时能快速扩出一批相对可靠的数据。注意要持续用真实标注数据校验伪标签的准确率防止误差累积。如果把GMM当分类器需要清楚它的边界它的决策边界永远是二次曲线高斯分布对线性可分的数据反而可能过度复杂如果数据分布离高斯很远比如多模态、长尾严重GMM分类器的效果就明显不如集成树或者神经网络。我的建议是在需要概率输出、样本量又不太大的场景下GMM作为分类器是一个很轻量、可解释的选项但别指望它在所有数据上都碾压其他模型。我在实际项目里还有一个固定动作跑完GMM之后用外部标签算一下ARI调整兰德指数或者NMI标准化互信息看聚类结果和真实业务标签的重合度。如果聚类效果很好但业务标签对不上往往不是模型错了而是业务标签本身的定义需要重新审视。这个视角有时候比调参更重要。有一次做用户行为分群K-Means把一批高价值用户硬生生劈成了两个簇换用GMM之后分群不仅合理了那些概率极低的样本还帮我揪出了一批数据埋点异常的记录。从那以后遇到分布重叠明显的数据我都习惯先跑一遍GMM做预聚类再结合概率输出决定哪些样本需要人工介入。这套打法在我看来比单纯追求聚类指标更贴近真实业务也更能发挥无监督学习的价值。
返回列表