资讯动态

隐马尔科夫模型HMM:从数学原理到中文分词与状态诊断实战

发布时间:2026/10/16 15:02:39 来源:尧图企业网站定制
前阵子有个做算法落地的朋友问我现在大模型满天飞还有必要花时间学隐马尔科夫模型HMM吗我的回答是如果你只刷榜单确实可以不学但如果你要处理真实的时序诊断、行为识别、序列标注这类问题HMM依然是性价比最高的起点之一。HMM的中文全称是隐马尔科夫模型英文是Hidden Markov Model核心思想一句话就能说清你看到的观测数据只是表象背后有一条看不见的状态链在驱动它。在语音识别、中文分词、生物序列分析这些领域HMM曾经是绝对的主力到现在也是很多生产系统的兜底方案。这篇文章我会把HMM的数学定义、三大经典问题、Python实现、以及几个真实应用案例串起来讲还会把我在实际项目中踩过的坑和总结的经验一并分享。适合刚入门机器学习的学生也适合想在生产里重新捡起传统模型的工程师。1. 老模型凭什么还不过时HMM 解决的问题类型与适用边界1.1 用天气-带伞的直觉建立模型感知如果让我用最简单的方式解释HMM我会讲这个场景你住在一个没有窗户的房间里想知道外面下没下雨但看不到天。你唯一能观察到的信号是室友出门时带不带伞。今天下雨明天还可能下雨这是一个带转移的概率过程你看不到天气只能根据伞来推测这是隐藏状态观测噪声的结构。HMM要解决的就是这类问题我们只能拿到观测序列真正关心的是背后那条不可见的状态序列同时还要把状态之间的转移规律也学出来。这个例子虽然简单却包含了HMM的所有要素。隐藏状态是晴/雨观测是带伞/不带伞状态转移是晴转晴、晴转雨、雨转晴、雨转雨的概率发射关系是晴天带伞的倾向、雨天带伞的倾向。后续所有公式都跑不脱这个框架先把它刻在脑子里后面再看符号就不会犯晕。1.2 什么场景该用HMM什么场景不该用根据我实际项目的经验HMM适合的问题有几个明显特征观测是一串按时间排列的序列且序列背后确实存在离散的隐藏状态隐藏状态数量不多通常2到几十个再多就容易训练不稳定状态之间的依赖可以用一阶马尔可夫近似即当前状态只依赖上一个状态观测存在噪声但噪声可以用固定发射分布描述。反过来如果观测维度特别高、变量关系呈强非线性、或者需要记住很久以前的状态那HMM就不合适了。比如直接用原始音频波形建模语音识别HMM会非常吃力这时候更好的做法是先用深度学习做帧级特征提取再交给HMM做时间结构建模这就是过去混合系统的经典路线。理解适用边界比学会算法本身更重要——很多项目失败不是模型不对而是选错了工具。2. 五元组和三个经典问题HMM 的数学骨架2.1 五元组一次把符号说清楚HMM的模型参数习惯写成 λ (A, B, π)但它完整的定义包含五个要素N隐藏状态数量状态集合 S {s1, s2, ..., sN}M每个状态下可能的观测值数量观测集合 V {v1, v2, ..., vM}A状态转移矩阵A[i][j] 表示从状态 si 转移到 sj 的概率矩阵每行之和为1B发射矩阵B[i][k] 表示在状态 si 下观测到 vk 的概率π初始状态概率向量π[i] 表示序列最开始处于 si 的概率。回到天气例子N2晴/雨M2带伞/不带伞A是2×2的转移矩阵B是2×2的发射矩阵π是2维向量。用矩阵形式描述概率模型的好处是后面所有的递推都能变成简洁的数组运算工程实现时直接对应到numpy矩阵乘法。2.2 两个基本假设HMM 轻量化的根源HMM之所以计算简单是因为它做了两个强假设。第一是齐次马尔可夫假设当前隐藏状态只依赖前一个隐藏状态即 P(q_t | q_1,...,q_{t-1}) P(q_t | q_{t-1})。第二是观测独立性假设当前观测只依赖当前隐藏状态即 P(o_t | q_t, o_1,...,o_{t-1}, q_1,...,q_{t-1}) P(o_t | q_t)。这两个假设听起来苛刻却把联合概率分解成了一串可以递推的乘积形式。真实世界里语音信号显然不只是当前音素决定当前帧但经过工程折中后这个近似在绝大多数场景里已经够用。理解这一点非常重要HMM不是完全正确的模型而是一个在复杂性与可解性之间取得平衡的工程决策。2.3 三大经典问题对应三类业务需求HMM的所有应用都归结为三个问题评估问题给定模型 λ 和观测序列 O计算 P(O|λ)。这用于判断一个观测序列有多大概率由该模型产生典型场景是识别、异常检测。解码问题给定模型 λ 和观测序列 O找出最可能的隐藏状态序列 Q*。典型场景是分词、词性标注、语音识别中的音素切分。学习问题给定观测序列 O可能没有状态标注估计模型参数 λ。典型场景是从历史数据中训练模型。后面三节分别对应这三件事。很多资料把这套内容讲得很抽象其实每个问题背后都对应一个经典算法评估用前向算法解码用维特比算法学习用鲍姆-威尔奇算法。把问题和算法一一对应起来学习曲线会平缓很多。3. 前向算法把指数级枚举变成线性递推3.1 直接枚举为什么不行评估问题最朴素的解法是穷举所有可能的隐藏状态序列。一条长度为T的观测序列背后有N^T种状态序列每种都要算一次联合概率复杂度是O(T·N^T)。10个状态、100个观测帧就是10^100的计算量任何现代计算机都跑不动。但仔细观察会发现联合概率的计算在时间上是逐层传递的我们完全可以用动态规划把重复计算吃掉——这就是前向算法的出发点。3.2 前向概率的递推公式定义前向概率 α_t(i) P(o1, o2, ..., ot, q_t s_i | λ)表示模型 λ 下观测序列前t个观测已经出现并且t时刻处于状态 si 的联合概率。递推分三步初始化α_1(i) π[i] · B[i][o1] 递推α_{t1}(j) ( Σ_i α_t(i) · A[i][j] ) · B[j][o_{t1}] 终止P(O|λ) Σ_i α_T(i)。递推式的含义很直观要想到达t1时刻的状态j必须先经过某个状态i所以把所有 α_t(i) 按转移概率加权求和得到到达j的总概率再乘上在j状态观测到 o_{t1} 的发射概率。整个过程只需要算 T·N² 次乘法复杂度从指数级降到 O(T·N²)。3.3 一个可直接跑的前向算法实现下面是一个用numpy实现的前向算法示例模型参数用简洁的数组表示。import numpy as np def forward(obs, A, B, pi): obs: 观测序列, list[int], 观测值在 0..M-1 A: 转移矩阵, shape (N, N) B: 发射矩阵, shape (N, M) pi: 初始概率, shape (N,) N A.shape[0] T len(obs) alpha np.zeros((T, N)) # 初始化 alpha[0] pi * B[:, obs[0]] # 递推, 工程上建议用 log 更稳, 这里为了直观先不加 log for t in range(1, T): for j in range(N): alpha[t, j] np.sum(alpha[t-1] * A[:, j]) * B[j, obs[t]] return np.log(np.sum(alpha[T-1])) # 返回对数似然, 便于跟其他模型比较 # 一个简单的测试: 2状态, 3个观测值 A np.array([[0.7, 0.3], [0.4, 0.6]]) B np.array([[0.9, 0.1], [0.2, 0.8]]) pi np.array([0.6, 0.4]) obs [0, 0, 1, 0, 1] print(forward(obs, A, B, pi))代码里我用返回对数似然的方式主要是为了避免在长序列中出现下溢。后面工程落地那节我会专门展开这个问题。3.4 评估问题在实际中的用途评估问题最直接的应用是模式识别为每个类别训练一个HMM来了新观测序列就分别计算对数似然取最大者作为分类结果。另一种用途是异常检测先用正常数据训练一个HMM如果新序列的似然低于某个阈值就判定为异常。比如服务器响应时间序列正常状态只有一个主模态当请求激增导致隐藏状态转移模式变化时P(O|λ) 会明显下降这就能作为一种无需标签的告警信号。我印象最深的一次使用是在某个医疗设备的状态监测项目中传感器数据被离散化成三档观测值用正常历史数据训练好HMM之后设备初期故障的表现就是对数似然持续走低比阈值告警提前了大概几十分钟。这种方案的优势在于可解释性强、计算量小、还能直接在边缘设备上跑。4. 维特比算法全局最优的隐藏状态序列4.1 为什么不能用贪心解码问题要做的事情是找到最可能的隐藏状态链。很多人第一反应是贪心每一步都选发射概率最大的状态。但这种做法忽略了状态转移约束。比如中文分词中若前一个字符已被标记为词首B下一个字符几乎不可能仍是B因为B后面只允许接M或E贪心只看发射很容易输出BBBB这类不合法序列。真正的全局最优路径必须同时权衡转移概率和发射概率这就是维特比算法登场的原因。4.2 动态规划的核心思想维特比算法本质上是动态规划维护两个矩阵δ_t(j)到t时刻为止以状态j作为第t个状态的所有路径中最大对数概率ψ_t(j)记录到达这个最大概率时t-1时刻的状态是什么用于回溯。递推公式为δ_1(j) log π[j] log B[j][o1] δ_t(j) max_i ( δ_{t-1}(i) log A[i][j] ) log B[j][o_t] ψ_t(j) argmax_i ( δ_{t-1}(i) log A[i][j] )。先顺着时间轴往前推记录每个位置每个状态的最佳前驱推完整个序列后在最后一个时刻找到 δ_T 最大的状态再沿着 ψ 一路回溯回去就得到全局最优状态序列。空间复杂度O(T·N)时间复杂度同样是O(T·N²)。4.3 Python实现def viterbi(obs, A, B, pi): N A.shape[0] T len(obs) logA np.log(A) logB np.log(B) logpi np.log(pi) delta np.full((T, N), -np.inf) psi np.zeros((T, N), dtypeint) # 初始化 delta[0] logpi logB[:, obs[0]] # 递推 for t in range(1, T): for j in range(N): # 计算所有前驱 i 的候选分数 scores delta[t-1] logA[:, j] best_i np.argmax(scores) delta[t, j] scores[best_i] logB[j, obs[t]] psi[t, j] best_i # 回溯 best_last np.argmax(delta[T-1]) states np.zeros(T, dtypeint) states[T-1] best_last for t in range(T-1, 0, -1): states[t-1] psi[t, states[t]] return states, delta[T-1, best_last] # 继续用上面的天气/观测例子 states, score viterbi(obs, A, B, pi) print(states, score)这里有一个细节我刚开始写的时候也容易漏递推时一定要在delta[t-1] logA[:, j]基础上判断是否存在-inf的情况。实际应用中如果某个状态到达不了-inf会通过argmax传到后面最终解码出不可能的路径。解决办法是在初始化时给到不了的状态留一个足够小的负值而不是直接置为-inf。4.4 解码在真实场景中的样子解码问题最经典的场景是中文分词。我们把一个句子中的每个字符看成观测值四个隐藏状态分别是B词首、M词中、E词尾、S单字成词连续观测值对应的状态序列用维特比解出来再根据边界切词。类似地词性标注也是序列标注隐藏状态就是名词/动词/形容词这些词性标签。语音识别里维特比更是核心。早期GMM-HMM系统中每个音素用一个或多个HMM状态表示解码就是给定音频特征帧序列找出一条最可能的音素状态路径再把路径映射成文字。虽然现在端到端模型已经占据主流但维特比思想仍然以束搜索的变体形式活跃在各种解码器里。5. 鲍姆-威尔奇算法没有标注也能学参数5.1 学习问题是什么实际项目中我们手里往往只有一堆观测序列却没有每条序列对应的状态标注。语音识别里你只有音频和文本没有精确到帧的音素标注分词任务里如果你不想人工标注语料就希望用无监督方式学到标签的转移规律。学习问题就是在这种情况下估计 λ (A, B, π)。HMM的标准解法是鲍姆-威尔奇算法它是EM算法在HMM上的具体化。5.2 E步算两个期望统计量鲍姆-威尔奇的关键是引入两个中间量γ_t(i) P(q_t s_i | O, λ)观测序列给定后t时刻处于状态i的概率ξ_t(i, j) P(q_t s_i, q_{t1} s_j | O, λ)t时刻在i、t1时刻在j的联合概率。这两个量都可以用前向概率 α 和后向概率 β 算出。后向概率 β_t(i) 表示在 t 时刻状态为 i 的前提下从 t1 到 T 的观测出现的概率递推方向和前向相反。γ 和 ξ 的计算分别是归一化后的 α·β 以及 α·A·β 的某种组合。E步就是固定当前参数算出这两个期望统计量。5.3 M步重估参数有了 γ 和 ξ参数重估就变得很自然π[i] γ_1(i)A[i][j] Σ_t ξ_t(i,j) / Σ_t γ_t(i)B[i][k] Σ_{t:o_tvk} γ_t(i) / Σ_t γ_t(i)。直觉上A[i][j]就是所有从i转移到j的期望次数除以所有从i出发的期望次数B同理。每完成一次E步和M步P(O|λ) 就会单调上升迭代到收敛即可。整个过程实现复杂度不高但迭代次数通常要几百到上千轮需要设置好停止条件。5.4 训练时一定会遇到的三个问题第一是初值敏感。鲍姆-威尔奇只能保证收敛到局部最优不同的初始 A、B 会得到差异很大的结果。我的建议是多组随机初始化保留训练结束后对数似然最高的一组。第二是要避免状态坍缩即某些状态对应的发射概率接近均匀、转移概率不集中这说明模型没有学到有区分度的结构可以考虑减少状态数或重新初始化。第三是收敛判据我一般用相邻两轮的对数似然提升量小于某个阈值比如1e-4作为停止标准同时设置最大迭代次数防止死循环。6. HMM 做中文分词从建模到代码的最小实现6.1 把分词建模成序列标注中文分词是HMM最经典的应用之一。做法是把句子看作字符观测序列把每个字符的边界标记看作隐藏状态B表示词首M表示词中E表示词尾S表示单字成词。模型训练时如果语料有词边界标注就直接统计A和B极其简单如果没有标注就用上一节的鲍姆-威尔奇无监督学习。HMM分词最核心的公式仍然是 P(状态序列|观测序列)用维特比求解最优标记路径再按标记切分。我用一个简化代码演示有监督训练的统计过程# 假设训练语料已按 B/M/E/S 打标, 统计转移和发射 from collections import defaultdict def train_hmm_seg(corpus_tags): corpus_tags: 列表, 每项为 (字符列表, 标签列表) 例如 ([我,爱,北,京], [S,S,B,E]) trans defaultdict(lambda: defaultdict(float)) emit defaultdict(lambda: defaultdict(float)) init defaultdict(float) for chars, tags in corpus_tags: init[tags[0]] 1 for ch, tag in zip(chars, tags): emit[tag][ch] 1 for i in range(len(tags) - 1): trans[tags[i]][tags[i1]] 1 # 归一化 total_init sum(init.values()) for k in init: init[k] / total_init for s in trans: total sum(trans[s].values()) for k in trans[s]: trans[s][k] / total for s in emit: total sum(emit[s].values()) for k in emit[s]: emit[s][k] / total return init, trans, emit训练完成后对任意新句子调用维特比得到B/M/E/S标记然后切词。这套最小实现虽然简单但对常用词典覆盖之外的未登录词处理能力比纯词典法强得多所以当年许多分词工具都把HMM作为后处理模块识别新词。6.2 真实工程中怎么增强HMM分词纯字符HMM分词的缺点是上下文依赖弱容易在长句里产生歧义。工程上常见的增强手段有三种一是把HMM与词典双数组Trie结合先查词典未登录词交给HMM二是扩充发射特征不只考虑当前字符而是把字符本身、字符类别数字/字母/汉字一起构成观测三是与条件随机场做对比替代但在标注数据少的场景里HMM的表现依然有竞争力。6.3 一个小实验对比我在某资讯平台做过一个实验拿一万条人工分词标注语料分别训练了HMM和线性链条件随机场结果HMM在测试集F1只比CRF低0.2到0.5个百分点但训练时间少了一个数量级模型容量也小很多。这个对比说明一个问题不是所有任务都需要复杂模型数据量少、资源受限、需要快速迭代的场景里HMM完全可以当主力。7. 其他几个高价值的应用方向语音唤醒、行为识别与状态诊断7.1 语音唤醒词里的HMM现在很多设备都有唤醒词这种交互入口。唤醒检测本质上是一个二元分类问题持续监听的音频流里判断当前片段是否包含唤醒词。业界一个经典做法是给唤醒词和静音/其他语音各建一个HMM对每帧音频提取特征后用前向算法分别计算两个模型产生当前观测片段的似然一旦唤醒词模型的似然连续若干帧超过其他模型就触发唤醒。相比纯深度网络HMM方案功耗低、延迟小非常适合低算力的嵌入式环境。7.2 行为识别与手势识别手机和可穿戴设备的加速度计输出是一串高维时间序列。把走路、跑步、静止、上下楼梯设为隐藏状态加速度特征做离散化或高斯发射就能构造一个行为识别HMM。模型训练好后用户当前属于哪种行为可以直接用维特比解码整段序列。这类应用的特点是不需要逐帧人工标注只要知道每段长序列的类别就可以用分段训练的方式获得参数部署起来非常轻。7.3 设备状态诊断与剩余寿命估计工业设备的状态监测是我最喜欢的HMM应用方向之一。设备运行可以建模成正常、轻微磨损、严重故障等隐藏状态传感器读数温度、振动、电流作为观测。训练好HMM后维特比解码能得到当前最可能处于的状态进一步可以用状态停留时间的统计分析预测剩余可用时间。和纯时序预测模型相比HMM给出的不是单个数值而是一个状态概率分布对维护决策来说更有价值。8. 工程落地中的几个小细节与大坑8.1 数值下溢是头号问题前向概率是很多小于1的数连乘序列一长就会逼近0连浮点数都扛不住。工程实现必须在对数域操作把乘法换成加法把连乘变成连加。前向算法和维特比都可以很自然地改写成对数形式鲍姆-威尔奇的E步计算γ、ξ时则用减去最大值的log-sum-exp技巧。我在代码里也刻意用了log这是所有HMM落地时都要强调的细节。8.2 零概率与平滑如果某个转移或发射在训练集里从未出现模型会给它0概率导致新数据上直接算出负无穷。解决办法有拉普拉斯平滑给计数加一个很小的常数α或者在参数重估时设置下限迫使每个概率大于一个极小值。具体取值需要结合数据量试一般α取1e-6到0.1之间。8.3 维特比回溯的数组边界实现维特比回溯时新手常犯的错误是状态索引从0开始还是从1开始搞混以及回溯循环的边界处理错位。正确做法是初始化时把 ψ 的第一行全部置0回溯时从最后一个时刻开始每步往前取 ψ 矩阵中对应位置的值直到取到第一个时刻。建议在写完后用简单数据对拍验证一次比如用2状态、3观测打印每一步路径。8.4 模型退化与状态坍缩训练HMM时偶尔会发现某个状态对应的发射概率几乎均匀、转移概率也被别的状态抢走导致该状态沦为摆设。这类退化通常由初始化不当、状态数过多或语料类别不均衡引起。排查时可以打印训练后的A和B矩阵观察若是某个状态没有主导发射符号就减小状态数或增加训练数据类别多样性。我还会在训练时人工指定约束比如限制某些转移必须保持小概率避免学到不符合物理直觉的跳转。8.5 传统模型和深度模型的结合思路虽然深度学习能端到端解决很多时序问题但在标注稀缺、算力有限、强解释性的场景里HMM依然有不可替代的位置。实际项目中我更推荐一条折中路线用少量标注数据训一个轻量深度网络做特征提取把高维输入映射成低维离散观测再接HMM做状态建模。这样既保留了深度特征的表达能力又继承了HMM对时间结构、状态可解释性的优势在若干工业落地方案里都被证实是稳定性很高的组合。做了几年模型落地之后我的一个体会是HMM这类传统模型最大的价值不是因为它有漂亮的数学而是因为它让你在动手之前把问题结构想清楚——到底什么是隐藏状态什么是观测状态之间怎么转移。每当我接到一个时间序列相关的需求我都会先问自己这三个问题然后才决定用HMM、CRF还是深度模型。如果你刚接触HMM建议不要一上来就啃公式先拿天气带伞的例子把前向、维特比、鲍姆-威尔奇三个算法各自跑通一遍再在分词或行为识别数据集上做一个小实验很快就能建立起直觉。HMM不大可能重回技术舞台中心但它作为一种建模思想和工具未来还会在大量工程角落里继续发挥价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑