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_iter100, tol1e-6): n len(x) # 初值随机选 k 个点当均值 mu x[np.random.choice(n, k, replaceFalse)] 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(axis1, keepdimsTrue) # M 步 nk resp.sum(axis0) mu (resp * x[:, None]).sum(axis0) / nk sigma np.sqrt((resp * (x[:, None] - mu) ** 2).sum(axis0) / 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-WelchEM 变体解码问题给定 λ 和观测找最可能的隐状态序列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 的价值不在公式有多美而在于它们逼着你把数据是怎么生成的隐变量和观测什么关系优化为什么可能陷入局部最优这些问题想清楚。这三个问题想透了再看别的模型你会发现自己评判方案的眼光变刁了——这大概就是学经典模型最大的回报。