简介:面向模式识别与机器学习入门者的《模式识别》电子书配套讲义,聚焦马尔科夫模型这一经典章节,适合正在学习人工智能课程、准备课堂汇报或需要快速梳理概率图模型基础的高校学生与自学者。压缩包内共1个文件,为PPT格式讲义,整体约5.79MB,页面按知识点逐层展开,便于直接翻阅或投影讲解。内容从马尔科夫链、马尔科夫过程的定义与马尔可夫性讲起,给出转移概率矩阵的表示方法及齐次链的条件,并以晴、阴、雨三种天气的转移矩阵为例说明概率取值;随后引入隐马尔可夫模型,交代状态、观察值、转移概率与观察值概率四类要素,用摸球(Urn)实验串起观察值序列的生成过程,并指出缸间转移不可直接观察、球色与缸并非一一对应等约束;末尾落到语音识别、图像识别、自然语言处理与生物信息学等应用方向。已有1593人学习,适合作为隐马尔可夫模型入门的第一份梳理材料。
1. 状态看不见反而更有用:模式识别里那条被藏起来的链
模式识别这门课讲到隐马尔可夫模型时,最容易被跳过的一句话是“状态不可见”。多数人的第一反应是困惑:既然要建模状态,为什么不干脆把状态标注出来,非要绕一层概率去猜?这个反直觉的选择恰恰是整套方法能成立的根本。语音识别里我们拿到的是声波采样,手写识别里拿到的是笔画坐标,生物序列分析里拿到的是一串碱基字符——这些都是观测,而“此刻在发哪个音素”“这个笔画属于哪一类结构”“这段序列处在哪个功能区段”,从来没有谁会直接递到手上。
如果强行用观测去等同于状态,模型就退化成一个查表器:见到什么输入就吐什么标签,遇到没见过的组合立刻失效。HMM做的事情是承认这层缺口存在,然后用两层概率把缺口接上:一层描述看不见的状态怎么随时间流转,另一层描述某个状态有多大概率“吐”出某个观测。理解这条被藏起来的链为什么必须存在、又凭什么能被推断出来,基本就理解了模式识别从“模板匹配”走向“概率生成模型”的那一步跨越。
2. 马尔可夫性:将来只依赖现在,这个假设到底省掉了什么
整套模型的地基是一句听起来很朴素的话:如果一个过程的“将来”只依赖“现在”,而与“过去”无关,这个过程就具有马尔可夫性。写成条件概率就是 P(X_{t+1} | X_t, X_{t-1}, …, X_0) = P(X_{t+1} | X_t)。等号左边是“知道全部历史”,右边是“只知道当前一格”,两者被强行画上等号,这是整个理论里最大方的一次假设,也是收益最大的一次。
值得先想清楚的是:这个假设不是对世界的真实描述。天气明天的走向当然和前天、大前天都有关,语音里下一个音素也受整句话语义影响。马尔可夫性是一种工程上的主动让步——把“无限长的历史依赖”压缩成“一格的当前状态”。让步换来的东西非常具体:参数从不可数变成可数,计算从不可行变成可行。
2.1 从“全部历史”到“当前一格”:条件独立砍掉的参数量
算一笔参数账最直观。设状态空间有 N 个取值。如果过程真的依赖前 k 步历史,那么描述“下一步往哪走”需要给每一种历史组合都准备一套分布,参数量是 N^k × (N−1) 这个量级。N 取 100、k 取 3,就是接近一亿个数;而一阶马尔可夫只需要 N × (N−1),也就是 9900 个数。三个数量级的差距,直接决定了这个模型能不能在内存里放下、能不能从有限语料里估出来。
更关键的是统计效率。语料是有限的,参数量一旦超过数据能支撑的规模,大部分参数就永远估不准,只能靠先验硬填。一阶假设把参数压到语料能覆盖的范围,每个转移概率都能被反复观测到,估计出来的数才有意义。所以在实际项目里,宁可接受“历史信息丢失”带来的偏差,也不愿意背上估不准的方差——偏差可以靠特征工程和后处理补,方差没法补。
这里还有一层常被忽略的细节:马尔可夫性描述的是状态序列,不是观测序列。观测之间往往明显不独立,这一点模型并没有否认,它只是把相关性全部推给了那条隐藏的状态链去承载。所以“模型假设观测独立”这种说法是不准确的,准确的表述是“观测在给定状态的条件下独立”。
2.2 转移概率矩阵的行和为什么必须是 1
把转移概率按状态排成方阵,就得到转移概率矩阵。矩阵元素 P_ij(m, m+n) 表示时刻 m 处在状态 a_i 的条件下,到时刻 m+n 落到状态 a_j 的概率。这份讲义里给出的天气例子是三个状态(晴、阴、雨)的 3×3 矩阵:
| 从\到 | 晴 | 阴 | 雨 |
|---|---|---|---|
| 晴 | 0.50 | 0.25 | 0.25 |
| 阴 | 0.375 | 0.25 | 0.375 |
| 雨 | 0.25 | 0.125 | 0.625 |
注意每一行的和都精确等于 1。这不是为了好看,而是从“链在时刻 m 从任意状态 a_i 出发,到时刻 m+n 必然落到 a_1, a_2, … 中的某一个”这条事实直接推出来的归一化约束:Σ_j P_ij(m, m+n) = 1。这条约束在实现里就是一道硬性校验,任何一行加起来不等于 1 的矩阵,都不是合法的转移矩阵,用它算出来的概率会随着步数漂移,最后既不是 0 也不是 1。
行和等于 1 还带来一个实用推论:矩阵自乘可以得到多步转移概率。上面那张表自乘一次,从晴天出发两天后仍是晴天的概率变成 0.40625,比一天的 0.5 下降了;仍是雨天的概率则从 0.25 升到 0.375。多做几次幂运算,各行会逐渐收敛到同一个向量,也就是平稳分布。这个性质在工程上常被用来检查矩阵是否“健康”:如果幂运算几十次之后各行还散着,多半是数据太稀疏或者矩阵被填坏了。
2.3 齐次性:把时变矩阵钉成一张常表
严格来说,转移概率可以随时刻 m 变化,P_ij(m, m+n) 里 m 是自由的。但那样每个时刻都要一张矩阵,参数量又炸了。所以实际用的都是齐次马尔可夫链:转移概率与 m 无关,只与步长 n 有关,记作 P_ij(n)。通常说的马尔可夫链,默认指的就是齐次链。
齐次假设的代价是放弃了“系统行为随时间演变”的建模能力。比如一个语音识别系统在句首和句尾的发音习惯其实不同,齐次模型对此视而不见。回报是整条链可以用一张矩阵 A 描述,多步转移就是 A 的幂,前向递推里每一步复用同一个 A,代码写起来只有一行矩阵乘法。这个取舍在绝大多数场景下是划算的,因为非齐次带来的表达能力提升,往往抵不过参数估计误差的增加。
3. 双重随机过程:一条链藏起来,一条链露出来
讲义的缸球实验是理解 HMM 结构最快的一条路径。设有 N 个缸,每个缸里装有很多彩球,缸内各颜色的比例由一组概率分布描述;实验者被一块帘子挡住,看不见缸的切换。流程是:按初始概率分布随机选一个缸开始,按该缸的颜色分布抽一个球、记录颜色、放回,再按缸之间的转移概率分布随机选下一个缸,重复下去。最终得到的只有颜色序列 O_1, O_2, …,也就是观察值序列。
三个约束值得逐条咀嚼:缸之间的转移不能被直接观察;抽到的球颜色和缸并不是一一对应的;每次选哪个缸由一组转移概率决定。前两条道出了“隐”的本质,第三条道出了“随机”的层次。很多实现出错,就出在把第二条当成了一一映射去处理。
3.1 缸球实验里的两次掷骰子
把实验拆成每时刻发生的两个独立随机动作,结构就清楚了。第一个动作是状态转移:当前在哪个缸,按转移分布决定下一刻去哪个缸,这一步是隐的。第二个动作是观测发射:确定缸之后,按缸内的颜色分布抽球,这一步是显的。两个动作各自是一次随机抽样,串起来就是“双重随机过程”。
这个拆法解释了一个常见困惑:为什么观测序列看起来有规律,模型却说它是随机的?因为规律来自状态链的持续性(比如缸 1 大概率留在缸 1),随机性来自发射环节的抖动(缸 1 里也可能抽出非主色调的球)。观测序列的表面规律,是这两层结构叠加之后的结果,单独看任何一层都解释不了。
用概率语言写下来,一条状态路径 q_1, q_2, …, q_T 和对应观测 o_1, o_2, …, o_T 的联合概率是初始概率、转移概率、发射概率三者的连乘:P(O, Q | λ) = π_{q_1} b_{q_1}(o_1) × Π_{t=2}^{T} a_{q_{t-1} q_t} b_{q_t}(o_t)。这个连乘式是后面所有算法的出发点,暴力枚举、前向递推、维特比解码,全都是在它上面做变形。
3.2 五元组与它的尺寸账
完整描述一个 HMM 需要五个要素,通常记作 λ = (N, M, π, A, B)。N 是状态数,M 是每个状态可能输出的观测符号数,π 是初始状态分布向量,A 是 N×N 的状态转移矩阵,B 是 N×M 的观测概率矩阵(也叫发射矩阵)。前两个是结构参数,后三个是数值参数。
尺寸账要算清楚,因为它决定了训练数据的下限。独立参数个数是 N(N−1) + N(M−1) + (N−1),每一组都因为行和(或列和)为 1 而少一个自由度。N=5、M=20 时是 20 + 95 + 4 = 119 个参数;N 提到 20,参数就跳到 380 + 380 + 19 = 779。语料里如果只有几百条观测序列,N 取大了必然过拟合,发射矩阵里大量格子会停在初始值附近不动。
把三个矩阵分开看,职责也很清楚:π 决定“从哪儿开始”,A 决定“怎么流动”,B 决定“怎么说话”。调试时定位问题的思路就来自这个分工——输出全错但状态序列看着合理,先查 B;状态序列本身乱跳,先查 A 和 π。
3.3 观测与状态的多对一映射为什么是关键让步
如果观测和状态一一对应,看到红球就知道在缸 1,那状态根本不需要“隐”,直接查表就行。HMM 主动放弃了这种便利,允许同一个观测由多个状态以不同概率发射。这个让步换来两样东西。
一是表达能力。语音里同一个音素在不同上下文下的声学表现差异巨大,一一对应根本无法覆盖;多对一映射让每个状态拥有一个观测分布,把“同一个标签的不同表现”收进分布内部,而不是拆成无数个细碎状态。
二是抗噪能力。观测被噪声污染时,一一映射会直接把噪声传递成状态错误;而分布式的发射概率相当于做了一次软判决,个别观测异常只会降低某条路径的概率,不会立刻翻转结论。这也是为什么在信噪比不稳的场景里,生成式模型往往比判别式模型更“皮实”。
代价同样明显:给定观测序列,状态序列不是唯一的,必须用算法去搜最可能的那条。这就引出了后面要解决的三类问题——评估、解码、学习。
4. 前向算法:把指数级路径枚举压成 O(N²T) 的递推
给定模型 λ 和一个观测序列 O,最直接的想法是把所有可能的状态路径列出来,每条算一次联合概率,再加总得到 P(O|λ)。这个思路在数学上完全正确,在工程上完全不可行。T 个时刻、每时刻 N 个状态,路径总数是 N^T。T=100、N=5 时是 5^100,约 10^69 量级,任何机器都枚举不完。所谓“评估问题”,本质就是找一种办法绕开这个指数。
绕开的钥匙还是马尔可夫性。既然当前状态已经浓缩了全部历史信息,那么“前 t 步观测到、且第 t 步停在状态 i”的所有路径,可以合并成一个标量而不损失信息。这个标量就是前向变量 α_t(i) = P(o_1, …, o_t, q_t = i | λ)。路径被合并了,指数就变成了乘法。
4.1 前向递推的矩阵化实现
递推关系只有两步:先按转移矩阵把上一步所有状态的前向变量加权求和,再乘上当前状态发射出该观测的概率。写成公式是 α_t(j) = [Σ_i α_{t−1}(i) a_ij] b_j(o_t),初值是 α_1(j) = π_j b_j(o_1)。最终 P(O|λ) = Σ_j α_T(j)。中间那个求和正好是向量乘矩阵,落到代码里就是一行。
import numpy as np def forward(pi, A, B, obs): """ pi : 初始状态分布, shape (N,) A : 状态转移矩阵, shape (N, N),A[i][j] = P(q_{t+1}=j | q_t=i) B : 观测概率矩阵, shape (N, M),B[i][k] = P(o_t=k | q_t=i) obs: 观测序列(已整数编码), 长度 T 返回: (P(O|λ), alpha 矩阵),alpha 为 T×N 的数组 """ T = len(obs) N = A.shape[0] alpha = np.zeros((T, N)) # t=0:初始分布与首个观测的发射概率逐元素相乘 alpha[0] = pi * B[:, obs[0]] # t>=1:先做状态转移的加权求和(矩阵乘法),再乘发射概率 for t in range(1, T): alpha[t] = (alpha[t - 1] @ A) * B[:, obs[t]] return alpha[-1].sum(), alphaalpha[t-1] @ A这一句是整个算法的核心,它等价于对上一时刻所有状态做一次加权求和,权重就是转移概率。写法上把内层循环省掉,复杂度从 O(N²T) 的显式循环降到一次 BLAS 调用,长序列上的差距非常明显。B[:, obs[t]]是按当前观测符号取出所有状态对应的发射概率,得到长度 N 的列向量,与左边结果逐元素相乘。
函数返回的 alpha 矩阵不只是为算总概率,调试时很有用:某一列全部趋近于零,说明该时刻所有状态都解释不了这个观测,通常是编码错位或者发射矩阵里有硬零。
4.2 数值下溢:长序列上必然遇到的那道坎
上面这段代码在短序列上没问题,序列一长就会翻车。每个 α 都是若干概率的连乘,量级随 T 指数衰减,T 超过几百之后双精度浮点直接下溢成 0,最后总和也是 0,取对数变成负无穷,训练时梯度全废。这不是边界情况,是长序列上的常态。
标准解法是每一步做一次归一化,把缩放因子记下来,最后补回去。改写后每步的 α 之和为 1,缩放因子的累乘就是 P(O|λ)。因为缩放因子本身也在变很小,实际实现里通常直接对缩放因子取对数累加,或者全程在对数域里做递推,用 log-sum-exp 代替普通求和,避免下溢和上溢。
def forward_scaled(pi, A, B, obs): """带缩放的版本,避免长序列下溢""" T = len(obs) N = A.shape[0] alpha = np.zeros(N) log_likelihood = 0.0 alpha = pi * B[:, obs[0]] c = alpha.sum() # 缩放因子 alpha /= c log_likelihood += np.log(c) for t in range(1, T): alpha = (alpha @ A) * B[:, obs[t]] c = alpha.sum() # 每步归一化,累加对数因子 alpha /= c log_likelihood += np.log(c) return log_likelihood, alpha缩放因子的连乘在数学上严格等于原始总概率,只是把它分摊到每一步去算,避免中间结果被浮点数吃掉。log_likelihood返回的是对数似然,比原始概率更适合作为训练目标,也不会因为序列长度增加而出现比较时的精度损失。
4.3 三个基本问题其实是同一套积木
评估问题用前向算法解决,解码问题(给定观测求最可能的状态序列)用维特比算法解决,学习问题(给定观测估计 λ)用 Baum-Welch 解决。三者的代码骨架高度相似,都是“初始化—按时刻递推—回溯或求和”,区别只在递推里的运算是求和还是取最大。
维特比把前向里的求和换成取最大值,同时记录每条路径的来源,回溯就能得到最优状态序列。Baum-Welch 则是 EM 的特例:E 步跑前向和后向得到状态占用的期望,M 步用这些期望重新估计 π、A、B,反复迭代到似然不再上升。理解了前向算法的那一行矩阵乘法,另外两个算法基本就是把sum换成max、把一次前向换成前向后向各一次。所以真正值得吃透的,是这一个递推为什么能把指数问题压成线性。
5. 避坑清单:HMM 落地时最容易翻车的五处
理论和能跑的代码之间隔着一段距离,这段距离基本由踩坑填满。下面几条都是实际项目里反复出现的,按“现象—原因—解决”列出来,遇到时可以直接对照。
5.1 状态数 N 拍脑袋定死
现象:训练集上似然一直在涨,验证集上的识别率反而下降;或者无论怎么调,模型都学不出有区分度的状态。
原因:N 是结构参数,不是靠调参能救的。N 太小,模型没有足够的状态去承载数据里的区分性结构,欠拟合;N 太大,参数量超过语料支撑能力,发射矩阵大量格子停在初值附近,过拟合。
解决:从明显偏小的 N 起步,逐个加倍观察验证集指标,找到拐点就停。也可以用 BIC 这类带参数惩罚的准则做粗筛,但最终还是要看任务指标。经验上 N 每翻一倍,训练语料至少要多一个数量级才撑得住。
5.2 转移矩阵里出现硬零
现象:某条观测序列的概率恒为 0,取对数后是负无穷,训练直接崩;或者解码时输出一条明显荒谬的路径。
原因:训练语料里没出现过的状态转移被直接写成了 0。这相当于断言“这件事绝对不可能发生”,而真实世界只是“这次没观测到”。一旦推理时遇到训练集没覆盖的路径,整条链的概率被清零。
解决:加平滑。最简单的是给所有转移和发射概率加一个很小的常数再重新归一化,保证每行和仍为 1;更讲究的做法用带先验的平滑,对高频转移少加、对稀疏转移多加。平滑系数不需要精调,量级对就够了。
5.3 训练和推理用了两套符号表
现象:离线评估指标很好,上线后结果乱套;同一个输入在两次运行里给出不同答案。
原因:观测序列是被整数编码后喂进模型的,编码字典如果没有和模型参数一起固化保存,训练时红=0、蓝=1,推理时可能变成蓝=0、红=1,发射矩阵的列就对不上了。
解决:把编码字典当作模型的一部分序列化,加载参数时强制校验字典版本。更稳妥的做法是把原始符号串做哈希后写进模型元信息,加载时比对,不一致直接报错而不是静默算出一个错答案。
5.4 长序列上忘记做数值缩放
现象:序列长度超过几百后,似然输出恒为 0 或 nan,模型看起来像死了。
原因:前向变量是连乘,量级指数衰减,双精度浮点的下溢阈值在 T 几百的量级就会被击穿。前向算法本身没错,是浮点表示范围不够。
解决:每步归一化并累加对数因子,或者全程对数域用 log-sum-exp。注意缩放因子本身也可能很小,所以最终存的是对数似然而不是概率值,比较模型时也统一在对数域比。
5.5 初始概率和初值设成完全均匀
现象:同样的数据、同样的超参,跑两次结果差别明显;有时收敛到一个明显更差的局部最优。
原因:EM 类算法对初值敏感,π 和 B 全设成均匀分布,等于让优化从一个高度对称的点出发,对称性会让不同状态学到几乎一样的参数,模型退化。
解决:多次随机初始化取最优,或者用 k-means 对观测做一次粗聚类,用聚类结果初始化发射矩阵。初始化后先跑几轮只看似然变化,如果前几轮几乎不动,说明初值把模型卡住了,重来比硬跑划算。
6. 设计取舍:为什么宁愿“看不见”也不肯“看得见”
把整条链路走完之后,最值得回味的不是算法本身,而是那几个主动放弃的假设。马尔可夫性放弃了长程历史,齐次性放弃了时变行为,多对一映射放弃了状态的可辨识性。三个放弃叠在一起,换来的是参数量可控、计算可递推、模型可解释。这套交换在数据稀缺、算力有限的年代几乎是唯一解,放到今天也没有完全过时——小样本场景、需要给出中间状态解释的场景、需要增量更新的场景,HMM 仍然是性价比很高的选择。
它真正的边界在于假设强度。端到端模型用大量数据和强大非线性把马尔可夫假设直接绕过去了,让模型自己决定要看多长的历史。代价是可解释性、数据需求和训练成本。我一般会这么理解:HMM 是把“结构先验”写死进模型,深度学习是把“结构先验”交给数据去学。前者省数据、可解释、易调试,后者上限高、吃数据、黑匣子。选哪条路,取决于你手上是几百条标注还是一条都没有,取决于出错时你能不能接受“模型就是这么学的”这种解释。
验证一个 HMM 是否真的学到了东西,有个很朴素的办法:把训练好的转移矩阵画出来看。如果对角线明显占优,说明状态有持续性,符合大多数时序任务的直觉;如果矩阵接近均匀,说明状态之间几乎无差别,模型大概率没学到结构。再检查发射矩阵,每个状态应该有一两个概率明显偏高的观测符号,全都平摊就是欠拟合的信号。这两个矩阵比任何 loss 曲线都更能说明问题。
踩过的坑里,代价最大的一次是拿一个 N 设得过大的模型直接上线,离线指标漂亮,线上遇到稍微不同的输入就崩,回头查发现发射矩阵里一半的格子从训练第一天起就没动过。从那以后我每次定状态数,都强制走一遍“小 N 起步、逐级加倍、验证集看拐点”的流程,宁可多跑几轮训练,也不让模型带着一堆死参数出门。希望帮到你。
本文还有配套的精品资源,点击获取