☰
从K-means到GMM:EM算法手写实现与聚类避坑
2026/11/3 3:29:13 网站建设 项目流程

做用户分群时,很多人第一反应是拿 K-means 跑一遍,得到一个簇标签就直接交差。我早期也这么干过,直到有一次遇到一批用户行为数据:两个群体的中心几乎重合,但一个群体在消费频次上特别集中,另一个群体则极度分散,一头是偶尔来一次,一头是天天来。K-means 把这两拨人硬生生按距离切成两半,结果业务方拿到分群结果后一脸茫然,说这分群跟随机抽签没区别。问题不在于 K-means 实现得不对,而在于它假设每个簇是一个"球",且每个点只归属一个簇。混合高斯模型(GMM)换了个思路:它不假设簇是球,而是假设数据由若干个高斯分布按不同比例叠加生成,每个点以一定概率属于每个分量。这个转变带来的直接好处是,簇可以是椭圆的、可以倾斜、可以大小不一,而且输出的不是一个硬标签,而是一组归属概率。而要把这套模型真正算出来,就绕不开EM 算法那套"先猜归属、再更新参数、反复拉扯"的迭代逻辑。下面我按照自己从踩坑到跑通的顺序,把 GMM 的概率形式、EM 的推导脉络、手写实现的关键代码,以及落地时最容易翻车的几个地方,一次讲透。读者最好有概率论基础和一点 Python 经验,但即使没有,跟着推导和代码走一遍也能拿到可用的东西。

1. 从一次聚类失效说起:K-means 的三条硬约束

1.1 球状簇假设带来的系统性偏差

K-means 的优化目标是簇内平方误差之和最小,这个目标函数本身就隐含了"簇是一个各向同性的球"这个几何假设。换个说法:它衡量一个点到中心的距离时,用的是欧氏距离,所有方向权重一样。当真实数据的簇是沿某个方向拉长的椭圆时,K-means 的切分边界会垂直于两个中心的连线,根本不去管数据实际的延展方向。我处理过一批二维传感器读数,两个簇分别是"横向拉伸的长条"和"纵向排列的点阵",K-means 画出来的分界线直接把长条从中间截断,把纵向点阵的一部分划到了长条那边。这不是调参能救的,是模型假设错了。

1.2 硬划分丢掉了边界地带的信息

第二个约束更隐蔽:每个点只能属于一个簇。现实里存在大量"骑墙"样本,比如一个用户既有一线城市高频消费的特征,又有低线城市低频浏览的特征。硬划分会强行给出一个标签,而下游业务如果拿这个标签做精准营销,就会在边界样本上持续犯错。GMM 输出的是一组概率,比如"0.62 属于 A 簇,0.38 属于 B 簇",这种软信息在风控、推荐、用户分层里价值很高,因为你可以选择只对归属概率高于某个阈值的样本做确定性操作,把模糊地带单独拿出来人工处理。

1.3 对簇的形状、大小、方向一无所知

K-means 里每个簇的"大小"只体现在分到多少个点上,模型本身对簇的规模没有任何参数。GMM 里每个分量都有自己的协方差矩阵,这个矩阵直接编码了簇的形状(特征值比例)、方向(特征向量)和体积(行列式)。我在做设备故障检测时,正常状态的振动数据在某个维度上方差极小,在另一个维度上方差较大,呈现明显的细长椭圆。GMM 的一个满协方差分量就能精确描述这个形状,异常点落在椭圆外的概率自然就高。这是 K-means 完全表达不出来的信息。

把这三个约束放在一起看,就能明白 GMM 要解决的不是"聚类效果差一点"的问题,而是"模型表达能力上限"的问题。你需要先承认数据结构可能是椭圆、倾斜、大小不一的,再谈用什么算法去拟合。

2. GMM 的概率模型:几个高斯叠起来到底在拟合什么

2.1 单高斯的表达能力边界

一维情况下,一个高斯分布由均值 μ 和方差 σ² 决定,它的形状永远是对称的钟形,只有一个峰。二维情况下就是那个经典的椭圆形等高线。问题在于,很多真实分布是"双峰"甚至"多峰"的。举个例子,某电商平台上用户的客单价分布,往往是"低客单价高频"和"高客单价低频"两个峰,中间凹下去一块。你硬用一个高斯去拟合,会得到一个又宽又扁、均值落在两个峰之间的分布,它对任何一个峰的描述都不准。这就是单高斯的表达能力上限。

2.2 混合分布的核心思想:加权求和

混合高斯的思路非常朴素:既然一个高斯描述不了双峰,那就用两个高斯,按一定比例混在一起。数学上的写法是:把 K 个高斯密度函数各自乘以一个权重 π_k,再加总。这里的 π_k 满足两个条件,每个都大于等于 0,且所有 π_k 加起来等于 1。这样一来,整体概率密度函数仍然是合法的(积分为 1),但形状可以任意复杂——峰的数量最多可以到 K 个,每个峰的宽窄、方向、高低都能单独控制。

我常拿一个类比来解释:想象你在一个房间里撒了 K 堆沙子,每堆沙子自然堆成一个圆锥(高斯),π_k 就是这堆沙子占总沙量的比例,μ_k 是这堆沙子的堆顶位置,Σ_k 决定这堆沙子摊开得多广、朝哪个方向摊。你在远处看整个地面,看到的是这 K 堆沙子叠在一起形成的高低起伏,而不是单独的某一堆。

2.3 参数组:权重、均值、协方差各管什么

一个 K 分量的 d 维 GMM,待估参数一共分三组。第一组是混合权重 π_1 到 π_K,共 K 个,但受归一化约束,实际自由度为 K−1。第二组是每个分量的均值向量 μ_k,每个是 d 维,共 K·d 个参数。第三组是每个分量的协方差矩阵 Σ_k,每个是 d×d 的对称矩阵,共 K·d(d+1)/2 个参数。这三组参数加起来,参数量随维度平方增长,这是 GMM 在高维场景下容易过拟合、需要正则化的根本原因。

协方差矩阵的类型选择是个很实际的工程问题。满协方差最灵活但参数最多;对角协方差假设各维度独立,参数量降到 K·d,适合维度高但特征间相关性弱的场景;还有球形协方差(所有维度方差相同)和绑定协方差(所有分量共享一个协方差矩阵)等变体。我在做高维文本特征聚类时,基本都用对角或球形,因为满协方差在几百维上根本估不准。

2.4 对数似然函数里那个绕不开的求和

模型定义好后,最自然的想法是最大似然估计:找一组参数,让所有观测数据出现的联合概率最大。取对数之后,单个样本的对数似然是:

log p(x_i) = log( Σ_k π_k · N(x_i | μ_k, Σ_k) )

麻烦就出在这个 log 里面套了求和。如果 log 里面只有一项,求导后能直接得到闭式解,就像单个高斯那样,均值就是样本均值,方差就是样本方差。但现在是 K 项加起来再取 log,对 μ_k 求导时,分母会带上整个混合密度,导致方程里同时出现所有分量的参数,互相耦合,没有闭式解。

我第一次推导到这里的时候卡了很久,总想着能不能通过某种变换把它拆开。结论是:在只用观测数据的似然函数上,确实拆不开。EM 算法的价值就在于,它引入了一个"隐藏的归属变量",把这个问题转化成了一个可以交替求解的形式。这一点是整个方法的枢纽,下一节展开。

3. EM 的两步走:E 步算责任度,M 步更新三组参数

3.1 引入隐变量:每个点到底是谁生成的

假设我们不仅观测到了 x_i,还知道它是第 k 个分量生成的,那么似然函数就变成了 log(π_k · N(x_i | μ_k, Σ_k)),log 里面没有求和,求导后每个分量的参数只和属于它的样本有关,闭式解立刻就有了。可惜这个"归属信息"是观测不到的,所以它被称为隐变量(latent variable),记作 z_i。

EM 的思路是:既然不知道 z_i,那我就先根据当前参数估计一个"软归属",也就是样本 i 来自分量 k 的后验概率。这个后验概率在文献里叫 responsibility(责任度),我习惯叫它归属概率。有了它,就可以把它当成权重,去做加权版的参数估计。然后更新参数,再重新算归属概率,反复迭代。这是一个典型的鸡生蛋蛋生鸡问题,但 EM 保证了每一轮迭代都不会让似然函数变差。

3.2 E 步:用贝叶斯公式算后验

E 步的任务很明确:在参数固定的前提下,计算每个样本对每个分量的责任度。公式是贝叶斯公式的直接应用:

γ_ik = ( π_k · N(x_i | μ_k, Σ_k) ) / ( Σ_j π_j · N(x_i | μ_j, Σ_j) )

分子是"先验权重 × 该分量生成这个点的概率密度",分母是所有分量对应项的加总,本质上是归一化因子。算出来的 γ_ik 满足对 k 求和等于 1,可以理解成"这个点有 γ_ik 的份额属于第 k 个分量"。

这里有个容易误解的地方:N(x_i | μ_k, Σ_k) 是概率密度,不是概率,它可以大于 1(尤其在低维、小方差时)。但因为它只出现在分子和分母的比值里,量纲会自动约掉,所以不用担心。我在代码里第一次实现时,看到某个密度值算出 12.7 还愣了一下,后来确认这是正常的。

实际编码时,E 步要在对数域里算,否则高维下密度值会下溢到 0。做法是先算每个分量下样本的对数密度,加上 log π_k,然后用 log-sum-exp 技巧归一化。这个细节后面代码部分会展开。

3.3 M 步:三组参数的闭式解长什么样

M 步是在责任度固定的前提下,最大化期望完全数据对数似然。用人话说,就是把每个样本按责任度拆成若干份,分别喂给各个分量,然后各自做加权版的参数估计。三组参数的更新公式分别是:

权重更新:π_k = ( Σ_i γ_ik ) / N。也就是第 k 个分量分到的"有效样本数"占总样本数的比例。所有 π_k 加起来自然等于 1,归一化约束自动满足。

均值更新:μ_k = ( Σ_i γ_ik · x_i ) / ( Σ_i γ_ik )。这就是加权平均,权重是责任度。归属概率高的样本对均值的拉动更大,这符合直觉。

协方差更新:Σ_k = ( Σ_i γ_ik · (x_i − μ_k)(x_i − μ_k)^T ) / ( Σ_i γ_ik )。同样是加权版本的样本协方差。注意分母用的是 Σ_i γ_ik,而不是 N,因为这里只统计"有效属于第 k 个分量"的那部分样本量。

这三组公式都不复杂,但实现时有几个坑:协方差矩阵可能因为某个分量的有效样本数太少而变得接近奇异,需要加正则项;计算 (x_i − μ_k)(x_i − μ_k)^T 时要注意维度顺序;分母 Σ_i γ_ik 可能非常小,需要设一个下限保护。

3.4 为什么反复迭代不会让似然变差

EM 能收敛这件事,核心在于它优化的不是原始对数似然,而是一个下界。具体来说,对任意一组责任度分布 q(z),原始对数似然可以分解成"期望完全数据对数似然"加上"q 与后验的 KL 散度"两部分。E 步固定参数、把 q 设成真实后验,此时 KL 散度为 0,下界等于原似然;M 步固定 q、优化参数,下界只会上升,而原似然 ≥ 下界,所以原似然也不会下降。这个"下界单调不减"的性质保证了算法至少能走到一个局部最优。

我在实践中观察到,对数似然的增量在前期很大,后期会迅速变小,呈现出典型的"先陡后平"曲线。所以判断收敛不能只看固定的迭代次数,得看增量的相对变化。这一点在第五节会详细说。

4. 手写一版 EM:从数据生成到和现成库对齐

4.1 造一批有形状差异的二维数据

要验证实现对不对,最好用自己造的数据,因为真值已知。我一般构造三个分量,分别是横向拉长的椭圆、纵向拉长的椭圆和一个近似圆形的小簇,权重不等。这样能同时检验协方差的方向估计、权重估计和均值定位。

import numpy as np np.random.seed(42) def make_data(n=900): # 分量1:横向拉长 cov1 = np.array([[6.0, 1.5], [1.5, 0.6]]) x1 = np.random.multivariate_normal([0, 0], cov1, int(n * 0.5)) # 分量2:纵向拉长 cov2 = np.array([[0.5, -0.3], [-0.3, 4.0]]) x2 = np.random.multivariate_normal([5, 5], cov2, int(n * 0.3)) # 分量3:小而集中的簇 cov3 = np.array([[0.4, 0.1], [0.1, 0.4]]) x3 = np.random.multivariate_normal([2, 8], cov3, int(n * 0.2)) X = np.vstack([x1, x2, x3]) return X X = make_data() print(X.shape) # (900, 2)

这段代码的关键在于协方差矩阵的设置。第一个矩阵的[0,0]元素远大于[1,1],说明横向方差大;非对角元素为正,说明数据沿右上方向倾斜。第二个矩阵相反,纵向方差大,非对角为负,倾斜方向相反。如果你的实现能把这些方向和比例估出来,说明协方差的更新逻辑是对的。

4.2 E 步与 M 步的向量化实现

逐样本循环写起来直观,但慢。我习惯直接向量化。核心是把对数密度矩阵、对数权重、log-sum-exp 三件事拼起来。这里给出一个可运行的最小实现。

from scipy.special import logsumexp def log_gaussian_pdf(X, mean, cov): d = X.shape[1] diff = X - mean cov_reg = cov + 1e-6 * np.eye(d) # 加正则防止奇异 L = np.linalg.cholesky(cov_reg) sol = np.linalg.solve(L, diff.T) quad = np.sum(sol ** 2, axis=0) log_det = 2 * np.sum(np.log(np.diag(L))) return -0.5 * (d * np.log(2 * np.pi) + log_det + quad) def e_step(X, weights, means, covs): N, K = X.shape[0], len(weights) log_prob = np.zeros((N, K)) for k in range(K): log_prob[:, k] = log_gaussian_pdf(X, means[k], covs[k]) + np.log(weights[k] + 1e-12) log_norm = logsumexp(log_prob, axis=1, keepdims=True) gamma = np.exp(log_prob - log_norm) return gamma, log_norm.sum() def m_step(X, gamma): N, d = X.shape K = gamma.shape[1] nk = gamma.sum(axis=0) + 1e-10 weights = nk / N means = (gamma.T @ X) / nk[:, None] covs = np.zeros((K, d, d)) for k in range(K): diff = X - means[k] covs[k] = (gamma[:, k, None] * diff).T @ diff / nk[k] covs[k] += 1e-6 * np.eye(d) return weights, means, covs

这里有几个我踩过的细节。用 Cholesky 分解算二次型,比直接求逆数值稳定得多,而且顺带拿到了 log 行列式——对角线取对数再求和的两倍就是 log det。logsumexp 避免了先 exp 再归一化导致的下溢问题。协方差矩阵加的那个小正则项 1e-6·I,是防止某个分量塌缩到单个点上导致矩阵不可逆,这个正则在 sklearn 里对应 reg_covar 参数。

4.3 主循环与收敛判据

主循环要做的不只是转到不动,还要监控对数似然。我通常会记录前一轮的对数似然,算相对增量,低于阈值就停,同时设一个最大迭代次数兜底。

def fit_gmm(X, K=3, max_iter=200, tol=1e-5): N, d = X.shape idx = np.random.choice(N, K, replace=False) means = X[idx].copy() covs = np.array([np.cov(X.T) for _ in range(K)]) weights = np.ones(K) / K prev_ll = -np.inf history = [] for it in range(max_iter): gamma, ll = e_step(X, weights, means, covs) weights, means, covs = m_step(X, gamma) history.append(ll) if abs(ll - prev_ll) < tol * abs(prev_ll + 1e-10): break prev_ll = ll return weights, means, covs, gamma, history w, m, c, g, hist = fit_gmm(X) print("权重:", np.round(w, 3)) print("均值:\n", np.round(m, 2)) print("迭代轮数:", len(hist))

跑下来,权重会接近 0.5 / 0.3 / 0.2 这组真值,均值会落在 (0,0)、(5,5)、(2,8) 附近,协方差矩阵的方向性也能恢复。如果你的结果对不上,八成是收敛判据写错了,或者初始化的协方差用了单位阵导致早期责任度过于平均。我在第一次实现时把收敛条件写成绝对差小于 1e-5,结果数据量一大,对数似然的绝对值动辄几千,相对变化早就很小了但绝对差还不满足,白白跑了两百轮。

4.4 和现成库的结果对齐

自己写的版本跑通之后,一定要和成熟库对一下,否则你不知道自己是"碰巧对"还是"逻辑对"。sklearn 的 GaussianMixture 是个很好的参照。

from sklearn.mixture import GaussianMixture gm = GaussianMixture(n_components=3, covariance_type='full', random_state=42, reg_covar=1e-6).fit(X) print("sklearn 权重:", np.round(gm.weights_, 3)) print("sklearn 均值:\n", np.round(gm.means_, 2)) print("对数似然:", round(gm.score(X) * len(X), 3))

对齐时要注意两个差异。一是标签顺序:GMM 的分量编号是任意的,你手写版本的第 0 个分量可能对应 sklearn 的第 2 个,比较前要按均值排序配对。二是默认的 initialization 不同,sklearn 默认用 kmeans 初始化,而你手写版本用的是随机选点,如果数据本身有多个局部最优,两边可能收敛到不同的解,这属于正常现象。我一般会把 sklearn 的 init_params 设成 'random' 再对比,减少变量。

5. 真正落地时最容易翻车的六个地方

5.1 协方差矩阵塌缩与正则化

这是最高频的问题。当某个分量的责任度总和很小,或者所有归属它的样本几乎重合,协方差矩阵会接近奇异矩阵,行列式趋近于 0,似然函数里的 log det 项会趋向负无穷,表现为对数似然突然爆掉或者出现 NaN。表现是训练过程看起来正常,但某个分量的协方差矩阵条件数极大,预测时对所有点都给出极端概率。

解决办法有三层。第一层是加正则项,在协方差对角线上加一个小的 ε,这就是 reg_covar 的作用,一般取 1e-6 到 1e-3 之间,量级取决于你的特征尺度。第二层是给每个分量的有效样本数设下限,当 Σ_i γ_ik 小于某个阈值时,重置这个分量的参数(重新随机选一个点当均值,协方差设成全局协方差)。第三层是事后检查,训练完遍历所有协方差矩阵,算条件数,超过 1e10 的标记出来处理。我在实际项目里三层都上,宁可多写几行防御代码。

提示:如果你的特征没有标准化,1e-6 这个正则可能根本不够用。特征量级差得远的时候,协方差的条件数本身就很大,正则项应该按每个维度方差的比例来设,而不是取固定值。

5.2 初始化敏感性

EM 只能保证收敛到局部最优,初始值不同结果可能差很多。我遇到过最极端的一次,同一批数据跑十次,有三次把三个真实分量合成了两个、又从一个分量里劈出了两个,原因是初始均值选了两个靠得很近的点。这不是实现 bug,是 EM 的固有性质。

缓解手段有几种。最常用的是用 K-means 先跑一遍,拿它的中心作为初始均值,各簇的样本协方差作为初始协方差,权重按簇大小占比。sklearn 默认就是这么干的。另一个办法是多次随机初始化,选对数似然最高的那组。还有一个更稳但更慢的做法是分层初始化:先尝试 K=1,然后每次增加一个分量时,把新分量的均值放在当前模型密度最低的地方。我一般用 K-means 初始化加三到五次重启取最优,性价比最高。

5.3 分量数 K 怎么定

K 不是越多越好。K 太大,模型会把噪声也当成结构拟合进去,表现为某些分量的协方差极小、权重极低,实际上是在过拟合个别离群点。K 太小,又会把不同形状的簇强行合并。常用的判定指标是 BIC 和 AIC,两者都是"负对数似然 + 参数个数惩罚",BIC 的惩罚项更重,倾向选更简单的模型。

指标公式结构倾向适用场景
AIC2p − 2ln(L)惩罚轻,容易选大 K样本量大、以预测为主
BICp·ln(N) − 2ln(L)惩罚重,倾向小 K样本量中等、以解释结构为主
轮廓系数基于簇内外距离与似然无关需要硬标签时辅助参考

我一般的做法是把 K 从 1 扫到 10,每个 K 跑多次初始化取最高似然,画 BIC 曲线,找拐点。同时看一眼每个分量的权重,如果有分量的权重低于 0.02,基本可以认为这个分量是多余的。这里要提醒一句,BIC 在数据不满足高斯假设时并不可靠,如果残差分布明显偏态,指标只能当参考。

5.4 特征标准化:最容易被忽略的前置步骤

GMM 的协方差矩阵是相对于特征原始尺度的。如果某个特征的单位是"元"(范围几千),另一个是"比例"(范围 0 到 1),不标准化直接扔进模型,第一次算距离时,那个大尺度的特征会完全主导结果,小尺度特征相当于不存在。我见过一个案例,用户把"年龄"和"年收入"两个特征直接放进模型,结果所有簇的划分几乎只由收入决定,年龄带来的形状信息完全被淹没了。

标准化的方式要选对。用 Z-score 标准化(减均值除标准差)是常规操作,但如果数据里有明显的长尾,中位数和四分位距的鲁棒标准化会更稳。这里有个容易被忽略的点:如果你标准化之后再建模,输出的均值是在标准化空间里的,业务方拿到"均值 = 0.83"这样的数字会困惑,记得反变换回原始尺度再解释。协方差矩阵的反变换更麻烦一些,需要乘上尺度矩阵再乘积,如果只是做聚类不做解释,其实可以跳过反变换,但一定要在文档里写清楚。

5.5 收敛判据:相对增量比绝对差靠谱

前面提过,对数似然的绝对值跟样本量成正比,用绝对差当判据在不同数据规模上表现不一致。我现在的标准写法是算相对变化,也就是当前与上一轮的差的绝对值除以上一轮的绝对值,小于 1e-5 就停。同时一定会加最大迭代次数限制,一般 100 到 300 轮足够。另一个细节是,前几轮的对数似然跳变很大,如果一开始就去判断相对变化,可能因为上一轮是负无穷或者接近 0 而出错,所以要么从第二轮开始判,要么给分母加个保护项。

还有一个隐蔽的坑:单次 EM 收敛的对数似然可能比另一次低很多,但收敛曲线看起来都很平滑。所以调试时不要只看"有没有收敛",要把每次运行的最终对数似然打出来对比。我习惯在训练结束后打印一个表格,列出每次重启的初始似然、最终似然、迭代轮数,一眼就能看出哪次是坏的。

5.6 维度灾难与协方差类型的选择

满协方差在维度 d 下每个分量有 d(d+1)/2 个参数。d=10 时是 55 个,d=50 时是 1275 个,d=100 时是 5050 个。如果样本量不够大,协方差矩阵估计会非常不稳定,表现为某些方向上方差被估得极小,密度值异常尖锐。这就是高维下必须降维或者换协方差类型的现实原因。

我做过一个对比实验,同一批 60 维特征,样本量 2000,用满协方差跑出来 BIC 反而比对角协方差差,日志里能看到好几个分量的协方差矩阵条件数超过 1e12。换成对角协方差之后,结果稳定了,而且因为忽略了一些弱相关的特征组合,实际效果反而更好。经验上,当样本量除以参数量小于 10 时,就该考虑降维或者简化协方差结构了。另外提一句,协方差类型的选择不要靠猜,直接把几种类型都跑一遍,比 BIC,这比拍脑袋靠谱。

6. GMM 能用在哪些地方:几个我实际碰过的场景

6.1 软聚类:把"骑墙样本"变成可利用的信息

这是 GMM 最直接的用法,但价值不在聚类本身,而在于它输出的概率矩阵。我服务过的一个零售场景,用 GMM 对用户做分群,得到每个用户对每个群的归属概率。业务方的用法很有意思:归属概率高于 0.8 的按确定人群处理,走常规营销;0.4 到 0.8 之间的单独抽出来,用更谨慎的触达策略,比如先发小额券试探反应;低于 0.4 的基本不碰。这套分层比之前用 K-means 硬切出来的效果稳定得多,因为硬切会把这些模糊用户随机扔到某一侧,造成策略误伤。

要注意的是,软归属的概率值本身不具备"置信度"的绝对含义,它依赖模型假设。如果真实数据偏离高斯假设,概率可能整体偏平或偏尖,所以阈值要根据分布的实际形态来定,最好画一下最大归属概率的直方图,看看双峰结构在哪里。

6.2 密度估计做异常检测:椭圆外就是可疑

GMM 拟合的是整个数据的概率密度,这就给了异常检测一个天然的低密度判定规则。计算每个新样本在模型下的对数密度,低于某个分位数的标记为异常。相比基于距离的方法,GMM 的优势在于它考虑了簇的形状:同样的欧氏距离,落在椭圆长轴方向上的点其实很正常,落在短轴方向上的点才是异常。我在设备振动监测里就是这么用的,正常状态用一个多分量 GMM 拟合,线上数据流逐条算对数密度,低于训练集 0.5% 分位数的报警。

这里有个实操细节:异常检测场景下,K 的选择要偏保守,宁可 K 小一点,让每个分量覆盖的范围宽一些,否则模型会把训练集里的噪声也建模成独立簇,导致正常的新样本被判成异常。另外阈值分位数不要拍脑袋,要根据误报率要求反过来算,业务上能接受 1% 的误报就取 1% 分位数,接受 0.1% 就往极端走。

6.3 作为其他模型的组件:不止于独立使用

GMM 还经常作为更复杂系统的一个模块出现。比较典型的是在序列建模里,用 GMM 描述每个状态下的观测分布,再配合状态转移,这就是经典的声学建模思路。另一个常见用法是用 GMM 的输出概率作为特征,喂给下游的分类器。我做过一个小实验,把 GMM 的责任度矩阵当成新的特征表示,输入到梯度提升树里,效果比直接用原始特征略好,原因是 GMM 把原始的连续空间压缩成了"到各个分量的相对位置",捕获了一些非线性的区域结构。

还有一种用法是做生成。GMM 本质是一个生成模型,参数学好后可以从中采样,生成合成的数据点。这个在数据增强里偶尔会用到,但要小心:GMM 生成的数据只会落在它拟合出的那些椭圆区域内,如果原始数据的分布不是高斯的,生成的样本会有明显的"假"痕迹。我一般只在数据量严重不足、且确认分布接近高斯时才会考虑这种方式。

6.4 和 K-means 的取舍:一张表说清楚

很多人在选型时会纠结用哪个。我的判断标准很简单:如果下游要的是确定性标签,且数据分布本身就近似球状、各簇规模相当,K-means 够用而且快;如果数据结构复杂、需要软归属、或者要做密度估计,就上 GMM。

维度K-meansGMM
划分方式硬划分,每个点一个标签软划分,输出归属概率
簇形状各向同性球状任意椭圆,可倾斜可缩放
需要指定簇数 K簇数 K 加协方差类型
计算开销低,迭代快高,每轮要算稠密矩阵
对初始值敏感,但有成熟优化更敏感,需多次重启
输出信息量只有中心和标签均值、协方差、权重、密度

实际项目里我经常两个都跑,用 K-means 的结果做 GMM 的初始化,同时用 GMM 的对数似然来评估数据到底有多"不球"。如果对数似然对 K 的曲线在某个 K 之后基本走平,说明再加簇也解释不了什么了,这时候硬要加簇就是过拟合。

最后分享一个我个人踩坑比较多的点:不要在没搞清楚数据量、维度、特征尺度这三个数之前就直接调库。我见过太多次一上来就跑 GaussianMixture,结果因为没标准化、没设 reg_covar、K 取太大,跑出来的结果还不如随机分组的案例。先把这三个数摆出来,样本量除以参数量算一下,特征尺度检查一遍,再决定协方差类型和 K 的搜索范围,比盲目调参省时间得多。EM 的收敛曲线也要盯着看,它平滑上升是正常,锯齿状或者突然断崖,基本就是数值问题,得回头查正则和初始化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询