☰
隐马尔可夫模型HMM:从马尔可夫链到序列建模与工程避坑
2026/10/28 5:30:27 网站建设 项目流程

简介:以PPT课件形式呈现的模式识别学习材料,面向修读模式识别、人工智能与机器学习课程的本科生、研究生及复习备考者,重点解决马尔科夫模型与隐马尔科夫模型(HMM)概念抽象、状态转移与观察序列关系不易理解的问题。包内共1个文件,为PPT类型,压缩包约5.79MB,按课件分页组织,便于跟随课堂节奏浏览、摘录和对照复习。已有1592人学习。内容从马尔可夫性、马尔科夫链和转移概率矩阵讲起,逐步延伸到HMM的状态、观察值、转移概率与观察值概率,并结合语音识别、图像识别、自然语言处理、生物信息学等场景说明应用方式;缸球实验实例及“缸间转移不可直接观察”等约束,有助于理解HMM的双重随机过程。适合课堂配套、考前梳理和模型入门时使用。

1. 看不见的状态:为什么模式识别里最难的从来不是分类

大多数人以为模式识别的难点在分类器:特征提对了、模型选对了、准确率上去了,任务就结束了。但真正处理过语音、手写、时序信号的人都知道,最难的从来不是“给一个特征向量贴上标签”,而是“标签本身从来没出现在数据里”。语音识别拿到的是每 10 毫秒一帧的频谱,音素状态没人标给你;手写识别拿到的是像素,笔画意图也没人标给你。可观测层和你想推的那一层之间,隔着一层概率映射。

这份模式识别电子书把主线压在马尔可夫链到隐马尔可夫模型上,讲的正是这一层映射:马尔可夫性怎么把“过去”砍掉、转移概率矩阵为什么每行必须归一、缸和球的实验怎么把“隐”这个字讲清楚、HMM 的三条约束到底约束了什么。把这条线走通,很多看起来像玄学的模型设定——齐次假设、观测独立假设、Baum-Welch 反复迭代——都会还原成可解释的工程取舍。

2. 马尔可夫性:一条“将来只依赖现在”的假设,凭什么敢用

教材里那句话写得很轻:“如果一个过程的将来仅依赖现在而不依赖过去,则此过程具有马尔可夫性”,公式只有一行:X(t+1) = f(X(t))。但这一行是整个序列建模里最激进的一次剪枝:它把无限长的历史压缩成一个当前状态,等于宣告“只要我知道你现在在哪,你怎么走到这儿的我不关心”。问题在于,现实里的过程几乎都有长记忆,音频有共振峰的惯性,文本有跨句指代,用户行为有周内节律。那凭什么敢用?

答案不在“假设成立”,而在“假设失效时损失可控”。一阶马尔可夫把参数从随历史长度指数增长,压到只跟状态数平方相关。这是拿表达能力换可计算性,而且换得非常划算——后面所有算法(前向、Viterbi、Baum-Welch)能写成 O(N²T),根子都在这一刀上。

2.1 历史依赖断在哪:从 N 的 k 次方回到 N 的平方

设状态空间大小是 N。如果要求模型显式记住前 k 步,转移参数的规模是 N 的 k 次方;只记一步,规模就是 N²。这个差距在小规模上看不出痛,规模一上来就是生死线。

状态数 N一阶参数 N²二阶 N³三阶 N⁴
101001,00010,000
502,500125,0006,250,000
20040,0008,000,0001,600,000,000

状态数 200 时,三阶模型光转移参数就十六亿个,每个参数还要有足够样本才能估准。这就是踩坑现场:理论上更准的高阶模型,实际训练出来往往比一阶还差,因为绝大多数参数只见过个位数样本,估出来的概率全是噪声。常见的实现思路是:宁可要一个估得准的一阶模型,也不要一个估不准的三阶模型,需要更长记忆时用状态设计去补(比如把“时长”编码进状态,或者用分层状态),而不是简单提高阶数。

顺带说一下,自然语言里的 n-gram 语言模型是同一个思路的另一个写法:三元组统计本质上就是二阶马尔可夫,词表几万,参数量立刻变成天文数字,所以工业界靠的是剪枝、回退和平滑,而不是硬估。血泪经验是:马尔可夫阶数是“参数预算”问题,不是“精度”问题。

2.2 转移概率矩阵:每行和为 1 既是归一化,也是约束

马尔可夫链在时刻 m 处于状态 aᵢ 的条件下,在时刻 m+n 转移到 aⱼ 的条件概率记作 Pᵢⱼ(m, m+n)。把这堆概率排成方阵,就是转移概率矩阵。讲义里给了一张天气表,正好可以用来验算:

import numpy as np states = ["晴", "阴", "雨"] # 行 = 当前状态,列 = 下一状态;A[i][j] 即 P(X_{t+1}=j | X_t=i) A = np.array([ [0.500, 0.250, 0.250], # 晴 -> 晴 / 阴 / 雨 [0.375, 0.250, 0.375], # 阴 -> 晴 / 阴 / 雨 [0.250, 0.125, 0.625], # 雨 -> 晴 / 阴 / 雨 ]) # 全概率:从任何状态出发,下一时刻必然落在某个状态上,所以每行和为 1 print(A.sum(axis=1)) # [1. 1. 1.] assert np.allclose(A.sum(axis=1), 1.0)

这段代码只有一件事值得盯住:检查的是axis=1,也就是按行求和,不是按列。这个方向搞反是新手最常见的翻车点之一。行代表“从哪来”,列代表“到哪去”,固定起点之后终点必然穷尽状态空间,所以行和为 1 是全概率公式的直接后果;列和没有这个约束,列和是“有多少概率会进入某个状态”,跟出发点怎么分布有关。如果你写成按列归一,矩阵照样能跑、算法照样不报错,但语义已经错了,训练出来的模型会莫名其妙地偏向某些状态。

还有一个容易被跳过的细节:矩阵元素的含义是条件概率,不是频率统计。用样本来估的时候,分母应该是“从状态 i 出发的总次数”,不是“所有转移的总次数”。这个分母错了,行和就不再是 1,前向算法每迭代一步都在丢概率质量,最后似然小得离谱,却查不出哪里写错了。

2.3 齐次性:时刻无关的假设,现实里经常不成立

讲义里那句话是:当 Pᵢⱼ(m, m+n) 与 m 无关时,称这条马尔可夫链为齐次马尔可夫链,通常说的马尔可夫链都指齐次马尔可夫链。翻译成工程语言就是:转移矩阵 A 不随时刻变化,同一个 A 从第一天用到第一万天。

这条假设带来的最大好处是“矩阵幂”这个性质:n 步转移概率就是把 A 自己乘 n 次。多步转移不用重新估参数,直接做矩阵乘法就行,这在实现上省掉了巨量状态,也让“预测未来第 k 步的分布”变成一行代码:

def sample_chain(A, n, rng, s0=0): """按转移矩阵采样一条状态链,验证齐次链的行为""" idx = [s0] for _ in range(n - 1): # p 必须是归一化向量,否则 np.random.choice 直接抛错 idx.append(int(rng.choice(len(A), p=A[idx[-1]]))) return [states[i] for i in idx] rng = np.random.default_rng(20240322) print(sample_chain(A, 12, rng, s0=0)) print(np.linalg.matrix_power(A, 3)) # 3 步转移矩阵 = A 的三次幂

参数说明:A是行归一的转移矩阵,s0是起始状态下标,rng.choice的p参数要求传入的向量和为 1,这也是为什么上一步的归一化检查不能省。matrix_power(A, 3)得到的三步转移矩阵,第 i 行第 j 列就是从 i 出发走三步到 j 的概率。

问题是齐次性在真实数据上经常被打破。天气矩阵本身就有季节性,语音里的发音速度会变,用户点击行为在工作日和周末完全是两个分布。判断标准很朴素:把时间轴切成若干段,分别估 A,如果各段的 A 差异远大于抽样误差,齐次假设就已经破了。处理方式不是硬拗,而是分段建模、按时段各训一个模型,或者引入随时间变化的 A_t(代价是参数变多、估准变难)。我一般会这么理解:齐次性是给“平稳段”准备的假设,遇到明显拐点就该切段,而不是指望一个矩阵吃掉所有情况。

3. 双重随机:缸和球之间那层概率映射怎么算

马尔可夫链解决的是“状态怎么跳”,但 HMM 要回答的问题更绕:状态你根本看不见。讲义用了缸和球的经典实验来搭这个直觉——N 个缸,每个缸里彩色球的比例不同,按初始分布选一个缸,按缸内颜色分布摸一个球记下颜色再放回,然后按缸间的转移分布选下一个缸,循环往复。最后你手上只有一串颜色,缸的顺序从头到尾都没露过面。

这个实验的价值在于它同时演示了两件互相纠缠的事:缸的顺序本身是一条马尔可夫链,而每一口缸吐出的颜色又是一个独立随机过程。两个随机源叠在一起,才是“双重随机过程”这个词的真实含义——不是一个随机过程加噪声,而是两条链通过一个概率表耦合。

3.1 三条约束拆开看,就是 HMM 的三根支柱

讲义专门列了三条注意点:不能被直接观察缸间的转移;从缸中选取的球的颜色和缸不是一一对应的;每次选哪个缸由一组转移概率决定。这三条正好对应 HMM 的三个设计决定。

第一条是“隐”:状态序列 Q 不可观测,只有观测序列 O 可见。这决定了推断的目标不是“还原真实状态”,而是“在给定观测下求状态的后验分布”——前者做不到,后者可以算。

第二条是“非确定性映射”:球色和缸不是一一对应。同一口缸里既有红球也有白球,这意味着你不能靠一次观测反推状态,必须靠整条序列的上下文。这条约束在实践中非常关键,它解释了为什么单帧分类永远比不过序列解码:单帧只用了 B 的一行,序列解码用了整条链的约束。

第三条是“转移由概率决定”:选哪个缸不是随机的均匀抽取,而是服从 A 的分布。这条让状态序列本身带上结构,于是“语音里辅音后面很少直接接辅音簇”这类先验才能被编码进模型。

3.2 参数 (π, A, B):一个 HMM 就是三张表

把缸球实验形式化,一个 HMM 由三组参数构成,通常记为 λ = (π, A, B):

参数维度含义在缸球实验里的对应物
πN初始状态分布,Σπᵢ = 1实验开始时选哪口缸的概率
AN × N状态转移矩阵,每行归一从一口缸换到另一口缸的概率
BN × M观测概率矩阵,每行归一每口缸里各种颜色球的比例

这里 M 是观测符号的个数,N 是隐藏状态数,两个数没有任何必然关系。维度搞混是后面排查章节里要重点讲的坑。给定一条状态路径 Q = (q₁, …, q_T) 和观测 O = (o₁, …, o_T),联合概率是连乘形式:

P(O, Q | λ) = π(q₁) · B(q₁, o₁) · ∏ₜ₌₂ A(qₜ₋₁, qₜ) · B(qₜ, oₜ)

这个式子里每一项都能在矩阵里查到,写起来很干净。注意它同时包含 A 和 B,也就是说状态路径和观测是被“绑”在同一个乘积里的,这正是双重随机的数学表达。

3.3 观测似然:把隐藏路径全部边缘化掉

真正要算的从来不是联合概率,而是 P(O | λ)——只给观测,模型给这条观测打多少分。路径 Q 是未知的,所以按全概率公式要把所有可能的路径加起来:

P(O | λ) = Σ_Q P(O, Q | λ)

这个求和是理解 HMM 全部算法复杂度的分水岭。状态数 N、序列长度 T,可能的路径有 N 的 T 次方条。N=5、T=100 时,这个数字是 5¹⁰⁰,比宇宙原子数量还大几个量级。直接枚举在物理上不可能完成,写代码验证一下规模就明白了:

import itertools def brute_force_likelihood(pi, A, B, obs): """朴素枚举:所有路径求和,只在小规模下可用,用来做正确性对照""" N, T = len(pi), len(obs) total = 0.0 for path in itertools.product(range(N), repeat=T): p = pi[path[0]] * B[path[0]][obs[0]] for t in range(1, T): p *= A[path[t - 1]][path[t]] * B[path[t]][obs[t]] total += p return total

参数说明:obs是观测符号的下标序列,pi是长度 N 的初始分布,A是 N×N,B是 N×M。这段代码只该出现在两个场合——单元测试里验证前向算法的结果,以及教学演示。它的复杂度是 O(T·Nᵀ),T 超过 15 基本就卡死。

黑匣子就在这里:隐藏路径被边缘化掉了,模型对外的接口只剩一个标量似然。这个设计带来的好处是接口极简,坏处是出了问题很难定位——似然低了,你分不清是 A 估歪了还是 B 估歪了,这个“没有后悔药”的特性会一直伴随到排查章节。

4. 三个基本问题:前向、Viterbi、Baum-Welch 各自的算法骨架

评估、解码、学习这三个问题,是 HMM 落地时绕不开的全部内容。它们的共同点是都在处理上一节那个“路径指数爆炸”的困境,区别在于用什么手段绕开:评估靠缓存子问题,解码靠最优子结构,学习靠期望补标注。三者的代码骨架都不长,但每一行背后的动机值得单独说。

4.1 前向算法:把重复子问题缓存起来

前向算法的核心观察是:所有路径在时刻 t 到达状态 i 的那部分乘积,可以被合并成一个量 αₜ(i),后面的计算只依赖这个量,不依赖具体是怎么走到 i 的。这一步合并不是近似,是精确的重新结合,把 Nᵀ 条路径按“时刻 t 落在哪个状态”分成 N 组,组内求和后只保留 N 个中间结果。

import numpy as np def forward(pi, A, B, obs): """前向算法:alpha[t][i] = P(o_1..o_t, q_t=i | lambda)""" T, N = len(obs), len(pi) alpha = np.zeros((T, N)) alpha[0] = pi * B[:, obs[0]] # 初始化:pi 与首帧发射概率逐元素相乘 for t in range(1, T): # 先做转移求和(列向量 alpha[t-1] 左乘 A),再乘当前帧的发射概率 alpha[t] = (alpha[t - 1] @ A) * B[:, obs[t]] return alpha, alpha[-1].sum() # 末尾求和即 P(O | lambda)

说明几个容易写错的点。B[:, obs[t]]取的是 B 的第 obs[t] 列,也就是“当前时刻所有状态产生这个观测”的概率向量,必须先转移再乘发射,顺序反了结果就对不上。alpha[-1].sum()是对最后时刻所有状态求和,因为路径可以在任意状态结束。复杂度 O(N²T),N=5、T=100 时是 2500 次乘加,跟枚举的 5¹⁰⁰ 不在一个量级。

还有一个必须知道的变体:直接乘会下溢。T=100、每步概率 0.1 量级,alpha 很快就掉到 1e-100 以下,浮点数直接变 0。标准做法是全程在 log 域算,把乘法换成加法,求和用 logsumexp:

from scipy.special import logsumexp def forward_log(pi, A, B, obs): log_pi, log_A, log_B = np.log(pi), np.log(A), np.log(B) T, N = len(obs), len(pi) la = np.zeros((T, N)) la[0] = log_pi + log_B[:, obs[0]] for t in range(1, T): # logsumexp 沿状态维做“log 域求和”,等价于原空间里的 alpha @ A la[t] = logsumexp(la[t - 1][:, None] + log_A, axis=0) + log_B[:, obs[t]] return logsumexp(la[-1])

la[t-1][:, None] + log_A是一个 N×N 的矩阵,第 i 行第 j 列是“上一时刻在 i 且转移到 j”的对数概率,沿 axis=0 做 logsumexp 就得到“到达 j”的对数总概率。这套写法是所有 HMM 实现的默认姿势,手写一遍比看十遍公式管用。

4.2 Viterbi:把求和换成取最大,为什么还能得到全局最优

解码要的不是似然,是最可能的那条状态路径。注意这里有个陷阱:最可能路径不等于“每个时刻取最可能状态”拼起来的路径。逐帧取 argmax 得到的是后验边缘最大的点集,这条路径本身可能根本走不通(某两个相邻状态之间的转移概率是 0)。Viterbi 解的是真正的 argmax over paths。

它的实现跟前向算法几乎一模一样,只把求和换成取最大,另外记一个回溯指针:

def viterbi(pi, A, B, obs): """返回最可能的状态路径(对数域计算)""" T, N = len(obs), len(pi) delta = np.log(pi) + np.log(B[:, obs[0]]) back = np.zeros((T, N), dtype=int) for t in range(1, T): trans = delta[:, None] + np.log(A) # i -> j 的候选得分 back[t] = trans.argmax(axis=0) # 记下每个 j 的最优前驱 i delta = trans.max(axis=0) + np.log(B[:, obs[t]]) path = [int(delta.argmax())] for t in range(T - 1, 0, -1): path.append(int(back[t][path[-1]])) # 反追溯,从终点一路倒回起点 return path[::-1]

为什么取最大还能保证全局最优?因为路径得分是连乘,取对数后变成链式求和,而链式求和具备最优子结构:如果全局最优路径在时刻 t 经过状态 j,那么它从起点到 (t, j) 的那一段,一定是所有到达 (t, j) 的路径里得分最高的那条;否则把这一段换掉就能得到一条更高分的全局路径,矛盾。这就是能用动态规划的理由。back[t][j]存的是“到达 (t, j) 的最优路径上,t-1 时刻是哪个状态”,回溯时从终点的 argmax 开始一路往前推。

要提醒一句:Viterbi 得到的是“整条路径联合概率最大”,不是“单点后验最大”。如果你的下游任务需要每个时刻的状态置信度,用前向-后向算 γ 更合适,别拿 Viterbi 路径当标注硬用。

4.3 Baum-Welch:没有标注时,用期望值当伪标注

前两个问题都假设 λ 已知。真实场景里 A 和 B 都是未知的,而你手上只有一堆没标注的观测序列——这就是学习问题,也是 HMM 最容易让人困惑的地方:既然状态看不见,怎么估转移概率?

思路是 EM 的老配方:先随便给一组参数,算出每个时刻处于每个状态的期望次数(E 步),拿这些期望次数当“软标注”去重新估参数(M 步),反复迭代。E 步要算两个量:γₜ(i) = P(qₜ = i | O, λ),即单点后验;ξₜ(i, j) = P(qₜ = i, qₜ₊₁ = j | O, λ),即相邻两时刻的联合后验。

def e_step(pi, A, B, obs): """前向-后向:算 gamma 与 xi""" alpha, _ = forward(pi, A, B, obs) T, N = len(obs), len(pi) beta = np.ones((T, N)) for t in range(T - 2, -1, -1): # 反向递推 beta[t] = A @ (B[:, obs[t + 1]] * beta[t + 1]) gamma = alpha * beta gamma /= gamma.sum(axis=1, keepdims=True) # 归一成后验 xi = np.zeros((T - 1, N, N)) for t in range(T - 1): xi[t] = alpha[t][:, None] * A * (B[:, obs[t + 1]] * beta[t + 1])[None, :] xi[t] /= xi[t].sum() return gamma, xi def m_step(obs, gamma, xi, n_states, n_obs, eps=1e-8): """用期望计数重估 (pi, A, B),eps 用来避免零概率死锁""" pi = gamma[0] / gamma[0].sum() A = xi.sum(axis=0) + eps # 对所有时刻的 xi 累加 A /= A.sum(axis=1, keepdims=True) B = np.full((n_states, n_obs), eps) for t, o in enumerate(obs): B[:, o] += gamma[t] # 按观测符号归集 gamma B /= B.sum(axis=1, keepdims=True) return pi, A, B

参数说明:alpha[t][i]是“前 t 帧观测且 t 时刻在 i”的概率,beta[t][i]是“给定 t 时刻在 i 的条件下,后面 T-t 帧观测的概率”,两者相乘再归一就是 γ。xi[t]里的alpha[t][:, None]是行向量、beta[t+1]是列向量,广播出来的 N×N 矩阵每一项对应一条转移边。

eps那一行是保命的:如果某个转移在整条训练集里从未被期望走到,M 步会给出 0,下一轮迭代 log(0) 直接变 -inf,整个模型报废。加一个极小值做下限,等价于给转移矩阵加了一层均匀先验。EM 的性质是每轮迭代似然单调不降,所以判断收敛看的就是似然曲线是否已经平了——但“单调不降”只保证收敛到局部极值,不保证是全局最优,这一点在排查章节还会再提。

5. 避坑与排查:HMM 真正跑起来会翻的五个车

公式推得再顺,代码一上手该翻的车一个都不会少。下面五条是实际调试里出现频率最高的,每条都按“现象 → 原因 → 解决”记,遇到问题时对着现象查比对着公式查快得多。

5.1 数值层的坑:下溢、死锁、归一化漂移

现象一:似然算出来是 0.0 或者 -inf,序列越长越必然。原因是概率连乘的下溢。T=100、每步平均 0.1,乘积量级是 1e-100,双精度浮点的下界大概在 1e-308 附近,再长一点的序列直接归零,之后所有除法、归一化全部变成 NaN。解决办法是全流程切到对数域,乘法变加法,求和用 logsumexp;如果只想快速验证,也可以每步对 alpha 做一次归一化并记录缩放因子,最后把 log 缩放因子加起来还原真值。前者更稳,后者改动小,我一般直接上对数域,省得后面到处补丁。

现象二:训练几轮之后某些转移概率永久变成 0,怎么迭代都回不来。这是零概率死锁。原因是 M 步用的是纯极大似然,某条转移在训练集里一次都没被期望走到,估计值就是 0,下一轮 log(0) = -inf,这个状态直接退出竞争。解决方式是给 A 和 B 加一个极小下限(上面代码里的eps),或者更规范地加狄利克雷先验做平滑。注意平滑系数不能太大,1e-8 到 1e-3 这个量级比较常见,加太多会把真实的结构抹平。

现象三:前向算出的似然和前向-后向算出的对不上。常见原因是 beta 的初始化或归一化不一致。beta 的初值在 t=T 时应该是全 1(末尾没有更多观测),如果误设成其他常数,或者 gamma 归一化时把keepdims写漏导致按列归一,结果就会系统性偏差。排查手段很直接:拿一段短序列跑一遍朴素枚举,把三个实现的似然摆一起比,对不上的那个一定是实现问题,不是模型问题。这种小规模对照测试值得写进单元测试,别嫌麻烦。

5.2 建模层的坑:维度错位与假设失效

现象四:代码不报错,但结果明显不对,状态序列几乎全挤在一两个状态上。先查维度:状态数 N 和观测符号数 M 是两个完全独立的量,B 是 N×M 不是 N×N。把 B 按方阵初始化,观测符号一多就索引越界;反过来把 N 设成 M,状态空间虚高,多余的状永远分不到概率质量。另一个同类问题是观测符号的编码——训练时用字符串建词表,推理时换了顺序,索引全错位,模型输出看起来还挺“合理”,这种错误最难查。解决办法是把词表序列化存进模型文件,加载时强制校验。

现象五:训练集上似然很漂亮,换一段数据就崩。这通常是齐次假设被打破,或者初始化踩进了局部极值。区分方法看两点:如果是数据本身有分段结构(场景切换、说话人变化),似然曲线在分段点附近会有明显台阶,处理方式是按段切开分别建模;如果是同一段数据上重复训练十次得到十个差别很大的结果,那是 Baum-Welch 对初值敏感,属于局部极值问题,处理方式是用分段 K-means 给一个靠谱初值,或者多组随机初值各跑一遍,取最终似然最高的那组。这里没有后悔药:Baum-Welch 只保证收敛,不保证收敛到你要的地方。

最后补一条容易被忽略的经验:状态数 N 不是越大越好。N 加大会让训练集似然单调上升,但验证集似然往往先升后降,跟过拟合是一个道理。选 N 的时候盯着验证集似然或者下游任务指标,别盯着训练集似然自我安慰。

6. 设计取舍:条件独立假设明明不成立,HMM 为什么还站得住

HMM 有两个假设在真实数据面前几乎必然被违反:一是齐次性,转移矩阵不随时间和上下文变化;二是观测独立性,给定状态后当前观测与历史观测无关。语音里相邻帧的频谱高度相关,文本里相邻词强依赖,这两条假设明摆着是错的。可这套模型在工程上用了很多年,至今仍是序列建模的入门基准,原因值得单独想一想。

我的理解是,这两个假设买到的不是精度,而是可解释性和可检验性。因为参数只有 π、A、B 三张表,你可以把 A 打印出来逐行看——“雨转晴 0.25、雨转雨 0.625”这种数字是能被领域知识直接审的;因为状态有明确语义,出问题时能定位到“第三个状态承担了本该属于第五个状态的观测”。换成端到端模型,这些全都变成了黑匣子,调参靠试,出错靠猜。这就是取舍的实质:牺牲一部分建模能力,换一个每一块都能被人读懂、被人改动的结构。

同一条逻辑也解释了为什么后来的序列模型并没有把 HMM 的思维淘汰掉。CTC 的对齐边缘化、注意力机制里的软对齐,本质上都在处理同一个问题——可观测序列和不可观测结构之间那层多对多的概率映射。缸和球实验里那三条约束,换到今天依然是判断一个序列模型能不能用的检查表:状态可见吗?观测和状态是一一对应吗?转移是学出来的还是拍脑袋定的?这三个问题问清楚,选型基本就不会跑偏。

验证一个 HMM 实现对不对,我一直用同一套流程:先用小规模数据跑朴素枚举对齐似然,再拿人工构造的、转移结构已知的序列去训,看收敛后的 A 能不能把真实结构还原出来,最后才上真实数据。中间任何一步对不上,都停下来查实现,不往下走。从那以后我每次动序列模型的参数估计代码,都强制走一遍这套三步验证,因为似然这种东西一旦在错误的实现上“看起来还行”,后面所有的调优都是在错误的地基上盖楼。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询