1. 为什么我会选高斯混合模型做聚类
很多人第一次接触聚类,脑子里蹦出来的第一个算法基本都是K-Means。原因很简单,它足够直观,代码也就几行,跑起来还快。但我在实际项目里用K-Means踩过几次坑之后,慢慢开始把目光转向了高斯混合模型(Gaussian Mixture Model,简称GMM),而scikit-learn里正好提供了非常成熟的实现,也就是sklearn.mixture.GaussianMixture。
先说清楚这个内容是什么。GMM是一种概率式聚类方法,它假设整个数据集是由若干个高斯分布按不同权重“混合”生成的,每个高斯分布对应一个簇。它和K-Means最大的区别在于:K-Means是“硬聚类”,一个样本要么属于A簇,要么属于B簇,非黑即白;而GMM是“软聚类”,它给出的是每个样本属于每个簇的概率,比如某个点有0.7的概率属于簇1、0.3的概率属于簇2。这个概率信息在很多场景下比一个硬标签值钱得多。
那它能解决什么问题?举几个我实际遇到的场景。第一个是用户分群,一个用户可能既像“高价值低频用户”,又有点像“潜力成长用户”,硬要把他塞进某一类会丢失信息,而概率输出能保留这种模糊性。第二个是异常检测,GMM可以算出每个样本在整体分布下的概率密度,密度极低的点大概率就是异常点。第三个是数据生成和密度估计,GMM本身就是一个生成式模型,可以用来采样新数据。
这个内容适合谁来参考?我觉得有三类人。第一类是已经会用K-Means、想进一步了解概率聚类的朋友;第二类是做数据分析、用户画像、异常检测相关工作的从业者;第三类是想搞明白EM算法到底怎么落地的人。哪怕你之前没接触过GMM,只要会一点Python和numpy,跟着往下看基本都能跑通。
我写这篇东西的出发点,是因为网上很多教程只告诉你fit一下就完事了,但真正决定GMM效果好坏的那些细节——比如协方差类型怎么选、初始化怎么做、怎么选簇数量、怎么处理维度灾难——几乎没人讲透。下面我就按我自己做项目的思路,把这些东西一层层拆开。
2. 高斯混合模型的核心原理拆解
2.1 从“一个高斯”到“多个高斯混合”
先打个生活化的比方。假设你站在一个路口统计经过的人的身高,如果只统计男性,身高大概服从一个正态分布,均值175左右;如果只统计女性,均值大概162左右。现在你不区分性别,把所有人的身高混在一起统计,得到的直方图就会出现两个“鼓包”,这时候用单个高斯去拟合就非常别扭,拟合出来的曲线又扁又平,完全抓不住双峰的特征。
GMM的思路就是:既然一个高斯搞不定,那我就用K个高斯分布加权叠加去逼近整体分布。数学上,一个样本x的概率密度写成:
p(x) = Σ π_k · N(x | μ_k, Σ_k)
这里的π_k是第k个高斯的权重(所有π加起来等于1),μ_k是均值向量,Σ_k是协方差矩阵。每个高斯成分就是一个“簇”,π_k反映这个簇在整体里占多大比例。
理解这个公式有个关键点:GMM不是在给样本贴标签,而是在建模整个数据的生成过程。它假设数据是这样产生的——先按权重π随机挑一个高斯成分,再从那个高斯里采样出一个点。我们看到的观测数据,就是这么一个过程重复N次的结果。这个“生成式”的视角,是GMM和K-Means最本质的思维差异。
2.2 EM算法:GMM到底怎么“学”出来的
知道了模型长什么样,接下来的问题是:π、μ、Σ这些参数怎么求?这里就轮到**EM算法(期望最大化)**登场了。因为每个样本到底来自哪个高斯成分,我们是不知道的,这属于“隐变量”,所以没法直接用最大似然估计一步到位。
EM算法的做法是反复迭代两步:
- E步(期望步):固定当前所有参数,计算每个样本属于每个高斯成分的“责任度”(responsibility),也就是后验概率γ_ik。直观理解就是:在当前参数下,这个点有多大概率是第k个高斯生成的。
- M步(最大化步):固定E步算出的责任度,反过来更新π、μ、Σ,让数据的对数似然最大化。更新μ_k时,用的是所有样本的加权平均,权重就是γ_ik——责任度越高的点,对第k个高斯的均值影响越大。
这两步来回迭代,直到对数似然基本不再增长(收敛)为止。scikit-learn里有个参数max_iter控制最大迭代次数,tol控制收敛阈值,默认分别是100和1e-3。
我个人的理解是:EM算法本质上是一种“软版的K-Means”。K-Means在分配点时是“赢者通吃”,离哪个质心近就归谁;而EM是“按比例分配”,每个点按概率同时参与所有簇的参数更新。这也是为什么GMM对初始值没那么敏感、不容易像K-Means那样陷入奇怪的局部最优。
2.3 协方差类型:GMM最容易被忽视的关键参数
scikit-learn的GaussianMixture里有个非常重要的参数叫covariance_type,它决定了每个高斯成分的协方差矩阵长什么样。这个参数直接决定了模型的复杂度和适用场景,很多人调参时完全忽略它,结果效果差一大截。
| covariance_type | 含义 | 参数数量 | 适用场景 |
|---|---|---|---|
full | 每个成分有独立的完整协方差矩阵 | 最多 | 各簇形状、方向都不同,数据量充足 |
tied | 所有成分共享同一个协方差矩阵 | 中等 | 各簇形状相似,只是位置不同 |
diag | 每个成分有独立的对角协方差矩阵 | 较少 | 各维度不相关,或想降低参数量 |
spherical | 每个成分是球形,单一方差 | 最少 | 各簇近似圆形,数据量小 |
这里面的取舍逻辑很实在。full最灵活,但参数量是O(K·D²),D是特征维度。如果你有50维特征、10个簇,那光协方差参数就有10×50×50=25000个,数据量不够的话必然过拟合。我一般的原则是:维度低于10、样本量上万,可以放心用full;维度高或者样本少,优先考虑diag或spherical。
diag假设各维度独立,参数量降到O(K·D),在高维稀疏场景下非常实用。spherical最简单,每个簇就是一个球,适合做快速原型验证。tied介于中间,适合你事先知道各簇形状差不多的场景。
提示:
covariance_type不是越复杂越好。我见过有人一上来就用full,结果在小数据集上跑出各种奇形怪状的椭圆,反而diag效果更稳。选型要结合数据量和维度一起看。
3. scikit-learn实战:从数据到聚类结果
3.1 环境准备与数据构造
先把环境弄好。scikit-learn的安装没什么好说的,pip install scikit-learn就行,numpy和matplotlib一般会一起装上。我习惯用下面这套导入:
import numpy as np import matplotlib.pyplot as plt from sklearn.mixture import GaussianMixture from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler from sklearn.metrics import silhouette_score为了演示,我用make_blobs造一批带三个簇的二维数据。这里我故意把簇的方差设得不一样,因为GMM相比K-Means的一大优势就是能处理不同大小、不同形状的簇。
X, y_true = make_blobs( n_samples=800, centers=3, cluster_std=[1.0, 2.5, 0.6], random_state=42 ) X = StandardScaler().fit_transform(X)注意这里我做了标准化。GMM对特征的尺度是敏感的,因为协方差矩阵直接受各维度量纲影响。如果一维是身高(厘米)、另一维是收入(万元),不标准化的话,协方差会被大数值维度主导,聚类结果基本就废了。这一步千万别省。
3.2 拟合模型与查看软聚类结果
接下来拟合一个GMM。我先把簇数设成3,协方差类型用full:
gmm = GaussianMixture( n_components=3, covariance_type='full', random_state=42, n_init=5 ) gmm.fit(X) labels = gmm.predict(X) # 硬标签 proba = gmm.predict_proba(X) # 软标签(概率)predict给出的是硬标签,取的是概率最大的那个成分;predict_proba给出的是完整的概率矩阵,形状是(n_samples, n_components)。我强烈建议你至少看一眼predict_proba的输出,这是GMM区别于K-Means的核心价值所在。
比如你可以这样找出那些“模棱两可”的样本:
max_proba = proba.max(axis=1) uncertain = X[max_proba < 0.6]这些点就是模型自己都拿不准归属的样本。在实际业务里,这批点往往最值得人工复核,或者单独拿出来做进一步分析。
3.3 用BIC和AIC选簇数量
K-Means选K靠肘部法,主观性很强。GMM有个更“数学”的办法:BIC(贝叶斯信息准则)和AIC(赤池信息准则)。这两个指标都在衡量模型拟合优度和复杂度之间的平衡,数值越小越好。
bic_scores = [] aic_scores = [] k_range = range(1, 10) for k in k_range: gmm = GaussianMixture(n_components=k, covariance_type='full', random_state=42) gmm.fit(X) bic_scores.append(gmm.bic(X)) aic_scores.append(gmm.aic(X))跑完之后把曲线画出来,BIC通常会在某个K处出现明显的“拐点”或最低点,那个K往往就是比较合理的簇数。我的经验是:BIC偏保守,倾向选更少的簇;AIC偏激进,倾向选更多的簇。如果两者给出的最优K不一致,我会结合业务可解释性来定——毕竟聚类最终是要给人看的,簇太多业务方也消化不了。
注意:BIC/AIC的前提是模型假设大致成立。如果你的数据根本不是高斯混合生成的,这两个指标也可能误导你。所以它们只是参考,不是圣旨。
4. 实操中那些坑与排查技巧
4.1 初始化与局部最优
GMM虽然比K-Means稳,但EM算法本质上还是局部优化,初始值不好照样会掉坑。scikit-learn提供了init_params参数,可以选kmeans(默认,用K-Means初始化)或random。我实测下来,默认的kmeans初始化在大多数场景下都更靠谱,因为它至少保证初始的簇中心是分散的。
另一个关键参数是n_init,它表示用不同初始化跑多少次,取对数似然最高的那次。默认是1,我一般会调到5甚至10,尤其是数据复杂的时候。代价是训练时间成倍增加,但换来的是结果稳定性,我觉得值。
gmm = GaussianMixture( n_components=3, covariance_type='full', n_init=10, random_state=42 )4.2 常见问题速查表
下面这张表是我自己整理的高频问题清单,基本覆盖了新手会遇到的绝大多数情况:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 报错“ill-defined empirical covariance” | 某个簇样本太少或几乎共线 | 减少簇数、改用diag、或先做降维 |
| 结果每次跑都不一样 | 未固定random_state或n_init太小 | 设random_state,增大n_init |
| 某个簇只有一个点 | 初始化差或K设太大 | 减小n_components,检查BIC曲线 |
| 收敛警告“did not converge” | max_iter太小或数据难分 | 增大max_iter,检查是否标准化 |
| 高维数据效果差 | 维度灾难,协方差参数爆炸 | 先PCA降维,或改covariance_type='diag' |
| 概率输出全是0或1 | 数据可分性太强或方差过小 | 检查reg_covar参数,适当调大 |
这里重点说两个。第一个是reg_covar,它是加在协方差对角线上的一个小正则项,默认1e-6。当某个簇的样本几乎共线时,协方差矩阵会接近奇异,加上这个正则项能保证数值稳定。如果报协方差相关的错,把它调到1e-3甚至1e-2往往能救回来。
第二个是维度问题。GMM在低维(2-10维)表现最好,一旦超过20维,full协方差的参数量就非常恐怖了。我一般的做法是先用PCA把维度压到10维以内再上GMM,既降了参数量,又去掉了噪声维度,效果通常比直接硬上高维要好。
4.3 一个我踩过的真实坑
早些年做一个用户分群的项目,特征有三十多维多,我图省事直接用了full协方差,结果模型跑出来每个簇都是一个又细又长的“针状”椭圆,业务方看了完全没法解释。后来我把维度用PCA降到6维,协方差改成diag,簇的形状立刻变得规整,业务含义也清晰了。
这件事给我的教训是:GMM的协方差类型选择,本质上是在“表达能力”和“可解释性/稳定性”之间做权衡。不是越灵活越好,而是要匹配你的数据量和业务需求。数据量撑不起full的时候,硬用full就是给自己找麻烦。
5. 进阶玩法与场景延展
5.1 用GMM做异常检测
GMM的score_samples方法能返回每个样本的对数似然,数值越低说明这个点越“不合群”。这天然就是一个异常检测器:
log_likelihood = gmm.score_samples(X) threshold = np.percentile(log_likelihood, 2) # 取最低的2%作为异常 anomalies = X[log_likelihood < threshold]相比孤立森林这类方法,GMM的好处是它有明确的概率解释——你可以说“这个点出现的概率低于某个阈值”,而不是一个抽象的异常分数。在需要向业务方解释“为什么判定它是异常”的场景下,这个优势很明显。
5.2 生成新样本
因为GMM是生成式模型,它可以直接采样新数据:
new_samples, _ = gmm.sample(100)这个能力在数据增强、仿真模拟里很有用。比如你有一批稀缺的样本,可以用GMM学出分布,再采样出更多“像真的”的合成样本。当然,采样质量取决于模型拟合得好不好,别指望它能凭空造出训练集里完全没有的模式。
5.3 和K-Means的取舍
最后说说什么时候该用GMM、什么时候K-Means就够了。我的判断标准很简单:
- 如果簇是近似球形、大小相近、你只需要硬标签,K-Means又快又省事,没必要上GMM。
- 如果簇形状各异、大小悬殊、你需要概率输出、或者要做密度估计和异常检测,那GMM是更合适的选择。
- 如果数据量特别大(百万级以上)且维度高,K-Means的性价比通常更高,GMM的训练开销会明显上升。
我个人的习惯是:先用K-Means快速探个底,看看数据大概分几堆;如果发现簇形状不规则或者需要概率信息,再切到GMM。两者不是替代关系,而是不同阶段的工具。
另外提一句,GMM的predict_proba输出可以直接喂给下游模型当特征。比如在风控场景里,把“属于各风险簇的概率”作为新特征输入到分类模型,往往比直接用硬标签效果更好,因为它保留了不确定性信息。这个用法我在几个项目里都验证过,提升虽然不算巨大,但胜在稳定、好解释。
最后分享一个小技巧:调试GMM的时候,把predict_proba的最大值分布画个直方图,如果大部分样本的最大概率都接近1,说明簇分得很开;如果大量样本集中在0.5附近,说明簇之间重叠严重,这时候要么增加簇数,要么重新审视特征是否选得合适。这个图比看任何指标都直观。