1. 为什么 GMM 和 HMM 值得放在同一张笔记里
我最早接触这两个模型是在做一段时序信号分类的任务,当时的需求很朴素:一段连续采样的数据,既要在每个时刻判断它属于哪种"隐含状态",还要对这种状态下的观测值分布有个刻画。用 K-means 硬聚类跑了一遍,结果每次换随机种子就变一个样,边界样本被硬生生劈到某一类里,完全没法解释。回头翻资料才发现,GMM(高斯混合模型)和 HMM(隐马尔可夫模型)本来就是为解决这类问题而生的组合拳。
GMM 解决的是"一个观测值可能同时属于多个模式,且这些模式的分布形状各异"这类问题,它用若干个高斯分布加权叠加来逼近任意形状的数据分布,输出的是"属于每一类的概率"而不是非黑即白的标签。HMM 解决的是"背后有一串看不见的状态在随时间演化,我们只能看到这些状态吐出来的观测"这类问题,它刻画的是状态之间的转移规律。把两者接在一起,就是经典的 GMM-HMM 架构:HMM 负责描述状态怎么跳,GMM 负责描述每个状态下的观测长什么样。
这份笔记想讲清楚的正是这条路:从 GMM 的高斯混合和 EM 算法,到 HMM 的三要素、三大问题,再到把两者拼起来为什么能撑起一个时代。内容会覆盖公式背后的直觉、代码怎么落地、参数怎么选、以及在实操里我最容易栽跟头的地方。
适合谁看?如果你已经会一点概率论、能读懂简单的 Python,又不想只停留在"调个库就完事"的层面,那这份笔记基本够用。我不会一上来就堆符号,而是尽量把每个公式翻译成"人话",再配代码对拍验证。坦白说,这两个模型真正难的不是公式推导,而是理解它们在什么场景下该用、参数怎么定、结果不理想时该往哪个方向调。
还有一点值得先说明:GMM 和 HMM 未必是当下最热门的方案,但它们是理解更复杂序列模型的台阶。很多深度序列模型里"隐状态""发射概率""前向后向"的思想,都能在这里找到源头。把这层地基打牢,后面看更花哨的东西会顺很多。
2. GMM 的本质:用几个高斯分布拼出任意分布
2.1 从 K-means 的硬伤说起
K-means 的假设非常粗暴:每个簇是一个球形、大小相近的团,样本离哪个中心近就归谁。可现实数据经常不听话。举个我踩过的例子,一维数据里两个簇一个宽一个窄,K-means 会把宽阔的那一簇切成两半去平衡两个中心的距离,因为它只认"距离最近",不认"这个分布的方差有多大"。更麻烦的是硬分配——边界上的点被强行判给某一类,而它其实"两边都沾一点",这种信息丢失在后续建模里是要付代价的。
GMM 的第一层修正就是软分配。它的思路是:假设数据由 K 个高斯分布按不同比例混合生成,每个高斯有自己的均值 μ 和协方差 Σ,还有一个混合权重 π。那么对于一个观测点 x,它由第 k 个高斯"吐出来"的概率可以用贝叶斯公式算,这个概率就是软分配值。最终我们不去问"它属于谁",而是回答"它有多大概率属于每一个"。
从概率密度角度看,GMM 的公式是:
p(x) = Σ_k π_k · N(x | μ_k, Σ_k),其中所有 π_k 加起来等于 1
这就是"混合"二字的含义——把多个高斯概率密度按权重叠加。理论上高斯个数足够多时,它们能逼近任意连续分布,这是它比单个高斯强得多的地方。我第一次真正理解这句话,是在一维数据上画了两三个高斯的叠加曲线,看着一个双峰的分布被两个高斯拼出来,那种直观感比看公式强一百倍。
2.2 EM 算法的 E 步和 M 步到底在算什么
GMM 参数估计靠的是 EM 算法,也就是期望最大化。它之所以存在,是因为如果我们知道每个点属于哪个高斯,参数估计就变成简单的加权平均,闭式解随手就写;可我们不知道归属,而归属又依赖于参数。这是一个典型的"鸡生蛋"循环,EM 用迭代的方式破局:先猜参数,用参数推归属,再用归属更新参数,反复拉扯直到稳定。
E 步:固定当前的 μ、Σ、π,计算每个点属于每个高斯的后验概率,也就是软分配值。记作 γ_{ik},等于"该高斯的先验权重乘该高斯在 x_i 处的密度"再除以"所有高斯的同类项之和"。这一步本质是把当前的参数假设代入贝叶斯公式。
M 步:固定 γ_{ik},把它当作"每个点对每个高斯的贡献权重",去更新参数。新的 μ_k 是所有点以 γ 为权重的加权平均;新的 Σ_k 是加权协方差;新的 π_k 是这一类的总权重占比。把它和 K-means 对比会非常清晰:K-means 里权重是 0 或 1,而 GMM 里权重是连续的。
关键理解:EM 每一步都在抬高似然函数的下界,保证似然单调不降。这意味着你不可能越迭代越差,但也不保证收敛到全局最优——它只会收敛到一个局部最优,具体是哪个取决于初值。
2.3 协方差矩阵类型:全协方差、对角、球形怎么选
这是 GMM 实操里最容易被忽略却最影响结果的一环。协方差矩阵决定了每个高斯"长什么形状",而不同形状假设带来的自由度和过拟合风险差异巨大。sklearn 的GaussianMixture提供了四种:
| covariance_type | 每个高斯的形状 | 参数量 | 适用场景 |
|---|---|---|---|
| full | 任意椭球,各维度独立相关 | 最大 | 维度低、样本多、维度间强相关 |
| tied | 所有高斯共用同一个协方差 | 居中 | 各类形状相似、样本有限 |
| diag | 轴对齐椭球,维度间不相关 | 较小 | 维度中等、特征近似独立 |
| spherical | 球体,各方向方差相同 | 最小 | 高维、样本少、只关心中心 |
我个人的经验是:维数一超过二三十,先别急着上 full。full 的协方差参数量随维度平方增长,几十维就上千个参数,样本不够就直接过拟合,甚至出现协方差矩阵奇异。做文本、音频这类高维特征时,diag 往往是性价比最高的选择——它牺牲了维度间相关性,换来稳定和速度,效果通常只掉一点点。
2.4 手撸一个 GMM 和 sklearn 对拍
光看公式容易虚,写一遍才踏实。下面是一维 GMM 的 EM 迭代,重点看 E 步和 M 步的对应关系:
import numpy as np def gmm_em_1d(x, k, n_iter=100, tol=1e-6): n = len(x) # 初值:随机选 k 个点当均值 mu = x[np.random.choice(n, k, replace=False)] sigma = np.full(k, x.std()) pi = np.full(k, 1.0 / k) prev_ll = -np.inf for _ in range(n_iter): # E 步 resp = np.zeros((n, k)) for j in range(k): resp[:, j] = pi[j] * norm_pdf(x, mu[j], sigma[j]) resp /= resp.sum(axis=1, keepdims=True) # M 步 nk = resp.sum(axis=0) mu = (resp * x[:, None]).sum(axis=0) / nk sigma = np.sqrt((resp * (x[:, None] - mu) ** 2).sum(axis=0) / nk) pi = nk / n # 对数似然 ll = np.log(resp.dot(pi) + 1e-300).sum() if abs(ll - prev_ll) < tol: break prev_ll = ll return mu, sigma, pi def norm_pdf(x, mu, sigma): return np.exp(-0.5 * ((x - mu) / sigma) ** 2) / (sigma * np.sqrt(2 * np.pi))跑完之后和sklearn.mixture.GaussianMixture对比,你会发现均值基本一致,只是排列顺序可能不同——这是正常的,因为标签本身没有语义。对拍的意义在于确认你对 E/M 两步的理解没有跑偏,一旦你的实现和库的结果南辕北辙,多半是某个步骤的维度或归一化写错了。
3. HMM 的三要素与三大问题
3.1 状态、观测、转移概率:用天气来理解
HMM 的经典比喻是天气和穿衣。真实天气(晴、雨)是隐状态,你看不见;你看到的只是朋友每天穿了什么(观测)。天气本身按一定规律转移(今天晴,明天大概率还晴),而你观察到的穿着又依赖于当天天气(下雨天穿雨衣的概率高)。整个 HMM 就是由三部分拧成的:
- 初始状态概率 π:第一天是晴天的概率
- 状态转移矩阵 A:从一种天气转到另一种的概率
- 发射/观测概率 B:在某天某种天气下,观察到某件衣服的概率
用符号说,隐状态序列记作 q,观测序列记作 o,那么模型就是 λ = (A, B, π)。这里最容易混的是 A 和 B 的区别:A 描述状态到状态,B 描述状态到观测。我见过不少初学者把两者搞反,结果模型怎么训都不对。
HMM 还带两个核心假设:一是齐次马尔可夫假设,当前状态只依赖前一状态,跟更早的历史无关;二是观测独立性假设,当前观测只依赖当前状态,跟其他观测无关。这两个假设是它能把复杂问题拆解的根基,也是它表达能力受限的根源,后面会展开。
3.2 三大问题与对应算法
学 HMM 绕不开三个经典问题,它们各有一个对应算法,我整理成表:
| 问题 | 想解决什么 | 算法 |
|---|---|---|
| 评估问题 | 给定 λ 和观测序列,算它出现的概率 | 前向算法 |
| 学习问题 | 只知道观测,怎么反推最优的 λ | Baum-Welch(EM 变体) |
| 解码问题 | 给定 λ 和观测,找最可能的隐状态序列 | Viterbi |
你会发现这三个问题和 GMM 的 EM 有种呼应感:学习问题同样是隐变量导致的循环依赖,同样用 EM 破局。只不过 HMM 的隐变量不仅有"每个时刻属于哪个状态",还有"状态之间怎么转移",所以 Baum-Welch 的 E 步需要同时算单时刻状态概率和相邻两时刻的联合概率。
3.3 前向算法为什么能躲开指数爆炸
如果暴力枚举所有长度 T 的状态序列,总共有 N^T 种,T 稍微大一点就爆炸。前向算法的精髓是动态规划:定义一个前向变量 α_t(i),表示"到 t 时刻为止、t 时刻处于状态 i 并观测到 o_1..o_t 的概率"。它可以递推:
α_t(i) = [ Σ_j α_{t-1}(j) · a_{ji} ] · b_i(o_t)
翻译成人话:先把前一时刻所有状态的概率按转移概率汇总到状态 i,再乘以状态 i 发射出当前观测的概率。每一步只需要 N×N 的运算,总共 T·N²,从指数级直接降到多项式级。
第一次看懂这个递推时我很感慨:它和 GMM 的"用旧参数算新归属"其实共享同一种思想——把大问题拆成一串小步骤,每步复用上一步的结果。理解到这里,前向算法就不再是一堆符号,而是一个自洽的机制。
3.4 Viterbi:解码问题的手算过程
Viterbi 和前向算法长得很像,只是一个用求和,一个用取最大值。它要找的是整条最可能的状态路径,而不是单时刻最优(注意:单时刻各自最优拼起来的路径未必整体最优,这就是为什么需要 Viterbi 而不是简单 argmax)。
它的递推是这样:δ_t(i) 表示"在 t 时刻以状态 i 结尾的所有路径中,概率最大的那条",同时用一个回溯指针记下每一步是从哪个状态跳过来的。递推时把前向的求和换成取最大,最后从末尾回溯就能还原整条最优路径。
用一个三状态、观测为"红黄绿"的小例子手算一遍,你会发现 Viterbi 和 GMM 的软分配完全是两种哲学:GMM 是软的、概率叠加的,Viterbi 是硬的、只留一条最大路径。什么时候用哪个?如果你要的是每个时刻最可能的状态,可以用后验概率 argmax;如果你要的是全局一条最合理的解释,就用 Viterbi。这个选择我在做序列标注时纠结过很久,后来结论是:看下游任务关心的是局部判断还是整条序列的连贯性。
4. GMM-HMM 合体:为什么发射概率要用高斯混合
4.1 单高斯发射的局限
标准 HMM 里 b_i(o_t) 通常假设是离散的或者单个高斯。可现实中的观测常常是多峰的:同样是"某个语音状态",可能对应好几种不同的发音方式,每种方式下特征的分布中心并不一样。用一个高斯去套,均值被拉在中间,方差被撑得很大,刻画得非常粗糙。
GMM-HMM 的解法就是把每个状态的发射概率从单高斯换成 GMM。也就是说,HMM 的每个状态不再只对应一个高斯,而是对应一小撮高斯按权重混合。这样每个状态内部的多样性就被多个高斯捕捉了,同时又保留了 HMM 对时序转移的建模能力。
我第一次把这个组合跑通,是在一个简单的手势时序数据上。单高斯 HMM 的准确率卡在某条线上上不去,换成每状态三个混合分量后明显有改善。虽然增大了参数量,但对多峰数据来说,这点代价花得值。
4.2 GMM-HMM 的训练流程
把两者接起来,训练其实还是 EM,只是隐变量变成了一条链。简化地说,流程是这样:
- 初始化:给每个状态的 GMM 一个初始参数(常用 k-means 或全局聚类切分)
- 用前向后向算法算出每个时刻处于每个状态、以及每个时刻属于该状态内哪个混合分量的后验概率
- 用这些后验概率去更新每个状态内 GMM 的均值、协方差、权重
- 用更新后的发射概率去更新转移矩阵 A
- 反复迭代直到似然收敛
注意坑点:最开始的初始对齐非常关键。如果初始划分太离谱,EM 会稳稳地收敛到一个很差的局部最优,表现出的现象是"训练集都学不好"。我的做法是先用整段数据做一次全局 GMM 聚类,再按时间把聚类结果分配到各状态上,作为初始对齐。
4.3 齐次假设的代价与应对
前面提到的两个核心假设,在 GMM-HMM 里会体现为明显的局限。齐次马尔可夫假设意味着"当前状态只看前一状态",这没法建模长期依赖;观测独立性假设意味着"当前观测只看当前状态",这忽略了观测之间的相关性。这两条在语音、文本这类长程依赖强的数据上都是硬伤。
正因为如此,后来的架构开始加各种补丁:用高阶 HMM 放宽一阶假设,用多层结构处理更长的上下文,直到更灵活的序列模型出现。但我想强调的是,理解这些局限的边界,比记住"它过时了"更有价值。你能清楚说出一个方法在什么条件下失效,才说明你真懂它。
5. 实操踩坑记录:数值、初值、维度三座大山
5.1 数值下溢:为什么要在对数域里算
前向算法连乘 T 次概率,每个概率都小于 1,几十步之后就会小到接近零,浮点数直接下溢成 0,后面全废。我第一次实现前向算法时就被这个坑卡了很久,输出的概率全是 0,一度以为是公式错了。
解法有两个:一是每步做完归一化,把缩放因子记下来,最后再合并;二是干脆全程在对数域运算,把乘法变成加法,用 log-sum-exp 技巧处理求和。实际工程里我强烈建议走对数域,虽然代码稍复杂,但稳定得多。Baum-Welch 里的期望统计量同样要在对数域算,否则哪怕前向不溢出,后向一乘又崩了。
5.2 初值敏感:多试几次再下结论
GMM 和 HMM 都是非凸优化,初值不同结果不同,这是它们的固有性质。我踩过的典型坑是:跑一次结果不好就否定了整个模型,其实多跑几次取似然最高的那个解,效果往往能上一个台阶。sklearn 的GaussianMixture有n_init参数,默认只跑一次,我通常调到 5 到 10,用best_params_拿最优解。
判断"是不是初值问题"有个简单信号:如果多次运行的似然值波动很大,说明对初值敏感,需要多做重启;如果波动很小但还是差,那问题多半出在特征、维度或混合数选择上,重启救不了。
5.3 协方差奇异:正则化的必要性
当某个混合分量只分到很少的几个点,而数据维度又不低时,它的协方差矩阵很容易接近奇异,求逆时直接报错或给出天文数字。我遇到过最气人的情况是训练中途突然出现 NaN,排查半天才发现是某个分量"饿死"了,权重趋近于零但协方差已经退化。
几个应对手段:加一个reg_covar正则项,在协方差对角线上加一个很小的正数,sklearn 里这个参数默认是 1e-6,样本少或维度高时我会调到 1e-4 甚至更大;或者换 diag/spherical 减少参数量;再不行就减少混合分量数。别指望模型自己在数据稀疏时保持数值健康,该加的正则一定要加。
5.4 状态数与混合数:怎么定才不玄学
这是最没有标准答案的问题,但也不是完全拍脑袋。它们直接关系到模型容量:状态数决定能刻画多少种"隐阶段",混合数决定每个阶段内部能有多复杂。
我的经验做法分三步:先用领域知识给个粗略范围,比如一个动作通常分几个阶段;再用 BIC 或 AIC 这类信息准则做筛选,它们会在似然和复杂度之间做惩罚平衡;最后拿验证集的实际任务指标(分类准确率、识别率等)做最终确认,因为信息准则挑出来的是拟合最优,未必是任务最优。
一个容易忽视的点是:状态数往上加,收益往往先增后平再降。我在语音小任务上试过从 3 个状态一路加到 20 个,中间有个明显的平台区,超过之后就只是把参数量堆上去,验证集反而不升。找到那个拐点,比一味往上堆更重要。
6. 学完之后怎么用:定位与调试清单
6.1 它们在现代方案里的位置
GMM 和 HMM 在今天未必是你第一个该掏出来的工具,但它们远没有"过时"到可以无视。GMM 至今在异常检测、密度估计、软聚类里被大量使用,因为它给出的概率密度和不确定性估计非常实用;HMM 在序列切分、小样本时序建模、可解释性要求高的场景里依然稳当。更要紧的是,它们是理解一系列更复杂序列模型的跳板——前向后向、隐状态、发射分布这些概念,换个包装就会出现在别的地方。
我个人判断某个任务要不要用它们,看三点:数据量是不是不大、可解释性是不是重要、序列结构是不是相对规范。三个都满足,它们往往是最省心的选择;如果不满足,就考虑更复杂的模型。
6.2 一份可以贴在屏幕旁的调试清单
最后把这半年踩坑总结成一张检查表,遇到问题照着走:
- 结果每次都不一样:先加大
n_init做多次重启,取似然最优 - 训练出现 NaN:查协方差是否退化,调大
reg_covar,或降维度/减分量 - 似然不升反降:检查 E 步/M 步的归一化,或对数域是否有 bug
- 训练好、验证差:典型过拟合,减混合数、降维度、换 diag 协方差
- 模型学不动:多半是初始对齐太差,重新做初始划分
- 状态数加不动收益:说明到平台期了,别再堆参数
这套流程不玄乎,但确实能省下大量瞎试的时间。我用它排查过好几次"看起来毫无头绪"的问题,往往几分钟就能定位到大方向。
踩过这么多次坑之后,我越来越觉得 GMM 和 HMM 的价值不在公式有多美,而在于它们逼着你把"数据是怎么生成的""隐变量和观测什么关系""优化为什么可能陷入局部最优"这些问题想清楚。这三个问题想透了,再看别的模型,你会发现自己评判方案的眼光变刁了——这大概就是学经典模型最大的回报。