尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

隐马尔可夫模型(HMM)原理详解与Python实战:从评估、解码到参数学习

隐马尔可夫模型(HMM)原理详解与Python实战:从评估、解码到参数学习 1. 项目概述从理论到实践的隐马尔可夫模型之旅最近在整理自己的学习笔记翻到了当年啃隐马尔可夫模型Hidden Markov Model, HMM时留下的厚厚一沓草稿纸。这东西在语音识别、自然语言处理、生物信息学这些领域简直就是个“扫地僧”般的存在——原理听起来有点绕但用起来是真香。很多朋友一听到“马尔可夫”、“前向后向算法”、“维特比解码”这些词就头大感觉是数学系高材生的专属玩具。其实不然只要你把它的“隐”和“显”两层结构想明白了整个框架就清晰了。今天我就把自己从原理推导到代码实现的全过程学习记录整理出来目标就是让哪怕只有一点概率论基础的朋友也能跟着一步步把HMM搞懂并且能亲手用代码把它跑起来。我们会先掰开揉碎讲清楚HMM到底在描述一个什么样的世界然后手把手推导核心的评估、解码和学习算法最后用Python从零实现一个完整的HMM并用它来玩点有趣的例子比如根据观测序列预测天气或者分析股票走势的隐藏状态。相信我走完这一趟你会对序列建模有全新的认识。2. HMM核心思想与模型定义拆解2.1 什么是“隐”马尔可夫要理解HMM关键在于区分两样东西状态和观测。我们用一个经典的例子来说明假设你是一个被关在密室里的宅男只能通过房间里的一个管子感知外面的天气比如通过管子湿度判断是否下雨。你无法直接看到天气状态但你能感受到湿度观测。天气本身的变化晴天-雨天是一个马尔可夫过程即明天的天气只依赖于今天的天气。而你观测到的湿度则依赖于当天的天气。这里的“隐”指的就是你无法直接观测的天气状态序列而你能得到的只是一串依赖这些隐藏状态的观测序列湿度数据。HMM就是为这类场景建模的利器。它假设有一个我们看不见的、由马尔可夫链驱动的状态序列在背后运行而我们只能看到由这些状态“发射”出来的一系列观测值。模型的目标就是通过观测值去推断背后隐藏的状态序列或者学习这个隐藏的马尔可夫过程本身的参数。2.2 HMM的五大核心要素一个完整的HMM由以下五个部分严格定义这就像它的“身份证”状态集合 (Q): 所有可能的隐藏状态的集合。比如我们的天气系统Q {晴天 阴天 雨天}状态数量N3。观测集合 (V): 所有可能的观测值的集合。在天气例子中假设我们通过湿度计观测V {干燥 湿润 潮湿}观测数量M3。状态转移概率矩阵 (A): 一个N x N的矩阵其中 A[i][j] 表示在时刻t处于状态i的条件下在时刻t1转移到状态j的概率。例如A[晴天][雨天] 0.1 表示如果今天是晴天明天下雨的概率是10%。它刻画了隐藏状态之间的动态变化。观测概率矩阵 (B): 一个N x M的矩阵其中 B[j][k] 表示在时刻t处于状态j的条件下生成观测值k的概率。例如B[雨天][潮湿] 0.7 表示在雨天我们观测到“潮湿”这个读数的概率是70%。它建立了隐藏状态与可观测量之间的桥梁。初始状态概率分布 (π): 一个长度为N的向量π[i] 表示在初始时刻(t1)处于状态i的概率。例如π[晴天] 0.6 表示第一天是晴天的概率为60%。这五个要素 (N, M, A, B, π) 共同定义了一个HMM记作 λ (A, B, π)。所有后续的算法都围绕着它们展开。注意这里有一个非常重要的假设——齐次马尔可夫性和观测独立性。前者指状态转移概率只依赖于前一个状态与时间t无关后者指任意时刻的观测只依赖于该时刻的状态与其他观测和状态无关。这是HMM计算可行的基石虽然在实际中可能不完全成立但极大地简化了模型。2.3 HMM解决的三大经典问题理解模型定义后我们就要用它来干活了。HMM通常用来解决三类问题这构成了我们学习和应用的主线评估问题 (Evaluation): 给定模型λ和观测序列O计算该观测序列出现的概率 P(O|λ)。这有什么用比如我们有两个描述不同天气模式的HMM一个对应雨季一个对应旱季当拿到一段新的湿度观测数据时我们可以计算它更可能由哪个模型产生从而进行模式识别或分类。核心算法是前向算法或后向算法。解码问题 (Decoding): 给定模型λ和观测序列O找出最有可能产生该观测序列的隐藏状态序列。这是我们最感兴趣的问题之一即“透过现象看本质”。在天气例子中就是根据湿度记录反推出最可能的真实天气序列。核心算法是著名的维特比算法 (Viterbi Algorithm)。学习问题 (Learning): 仅给定观测序列O或者可能多个观测序列如何估计模型参数λ(A, B, π)使得该模型下观测序列的概率P(O|λ)最大这是最困难也最实用的问题因为现实中模型参数往往未知需要我们从数据中学习。核心算法是鲍姆-韦尔奇算法 (Baum-Welch Algorithm)它是期望最大化EM算法在HMM中的具体实现。接下来我们就沿着这三个问题深入算法的细节。3. 核心算法原理与手把手推导3.1 评估问题与前向-后向算法为什么需要前向算法最直接计算P(O|λ)的方法是列举所有可能的状态序列计算每个序列生成观测O的概率然后求和。但状态序列有N^T种可能T为序列长度这是无法承受的计算量。前向算法通过动态规划将复杂度降低到O(N^2 T)。我们定义前向概率α_t(i)在给定模型λ下到时刻t为止的观测序列为(o1, o2, ..., ot)且时刻t隐藏状态为i的概率。 即α_t(i) P(o1, o2, ..., ot, q_t i | λ)推导前向算法步骤初始化计算第一个观测值出现且第一个状态为i的概率。 α_1(i) π_i * b_i(o1), for i 1, 2, ..., N 这里π_i是初始状态概率b_i(o1)是状态i下观测到o1的概率。递推对于t1, 2, ..., T-1计算 α_{t1}(j) [ Σ_{i1}^{N} α_t(i) * a_{ij} ] * b_j(o_{t1}), for j 1, 2, ..., N 这个公式怎么理解时刻t1状态为j且能观测到o_{t1}的概率等于【所有在时刻t可能的状态i转移到状态j并生成观测o_{t1}的概率】之和。Σ_{i1}^{N} α_t(i) * a_{ij} 计算了在已知前t个观测的情况下t1时刻状态为j的概率不考虑新观测再乘以b_j(o_{t1})就加上了新观测的约束。终止观测序列的整体概率就是所有可能最终状态的概率之和。 P(O|λ) Σ_{i1}^{N} α_T(i)后向算法β_t(i)定义类似是给定模型λ和时刻t状态为i的条件下从t1到T的观测序列的概率。即β_t(i) P(o_{t1}, o_{t2}, ..., o_T | q_ti, λ)。其递推方向是从后向前。前后向概率结合在解决解码和学习问题时非常有用。实操心得在代码实现时α_t(i)的值可能会非常小多个概率连乘容易造成下溢Underflow。一个关键的技巧是使用对数概率进行计算将乘法变为加法。或者在每一步递推后对α_t进行缩放归一化使其和为1并记录缩放因子最后通过缩放因子还原总概率。这是实现中的第一个“坑”。3.2 解码问题与维特比算法维特比算法的目标是在所有可能的状态序列路径中找到一条最优路径使得P(状态序列 | 观测序列, 模型) 最大。它本质上也是一个动态规划算法与最短路径Dijkstra算法神似。我们定义两个变量δ_t(i): 在时刻t所有到达状态i的路径中概率最大的那条路径的概率值。 δ_t(i) max_{q1, q2, ..., q_{t-1}} P(q1, q2, ..., q_{t-1}, q_ti, o1, o2, ..., ot | λ)ψ_t(i): 记录使得δ_t(i)最大的那条路径中时刻t-1的状态是什么。用于最后回溯得到完整路径。维特比算法步骤初始化 δ_1(i) π_i * b_i(o1), for i 1, ..., N ψ_1(i) 0 (因为第一个状态没有前驱)递推对t2, 3, ..., T对每个状态j δ_t(j) max_{1≤i≤N} [ δ_{t-1}(i) * a_{ij} ] * b_j(o_t) ψ_t(j) argmax_{1≤i≤N} [ δ_{t-1}(i) * a_{ij} ] 这一步是关键要到达时刻t的状态j最优路径必然来自上一时刻t-1的某个状态i我们选择能使路径概率最大的那个i。终止与路径回溯最优路径的终点P* max_{1≤i≤N} δ_T(i)最优路径的终点状态q_T* argmax_{1≤i≤N} δ_T(i)回溯最优路径对t T-1, T-2, ..., 1 q_t* ψ_{t1}(q_{t1}*)注意事项维特比算法得到的是全局最优的单个路径状态序列。有时我们可能关心每个时刻最可能的状态即计算γ_t(i) P(q_ti | O, λ)这可以通过前后向算法求得但这N个局部最优状态拼起来不一定是一条整体上概率最大的连续路径。这是“最可能序列”与“每个时刻最可能状态”的区别初学者容易混淆。3.3 学习问题与鲍姆-韦尔奇算法这是HMM参数估计的“重头戏”。当没有标注的状态序列即我们不知道真实的天气只有观测序列湿度记录时如何学习出A, B, π鲍姆-韦尔奇算法通过迭代不断优化模型参数以更好地解释观测数据。算法基于两个中间变量由前后向概率计算得出γ_t(i): 给定模型和观测序列在时刻t处于状态i的概率。 γ_t(i) P(q_t i | O, λ) α_t(i)β_t(i) / P(O|λ)ξ_t(i, j): 给定模型和观测序列在时刻t处于状态i且在时刻t1处于状态j的概率。 ξ_t(i, j) P(q_t i, q_{t1} j | O, λ) [α_t(i) * a_{ij} * b_j(o_{t1}) * β_{t1}(j)] / P(O|λ)鲍姆-韦尔奇算法EM框架步骤初始化随机或根据先验知识设定模型参数λ⁰ (A⁰, B⁰, π⁰)。E步期望基于当前参数λ利用前向-后向算法计算所有时刻的γ_t(i)和ξ_t(i, j)。M步最大化利用E步计算出的期望值重新估计模型参数得到λ_new。初始状态概率π_i_new γ_1(i) 即第一天处于状态i的期望概率状态转移概率a_ij_new (从状态i转移到j的期望次数) / (从状态i转移出去的期望总次数) Σ_{t1}^{T-1} ξ_t(i, j) / Σ_{t1}^{T-1} γ_t(i)观测概率b_j(k)new (在状态j下观测到k的期望次数) / (处于状态j的期望总次数) Σ{t1, s.t. o_t k}^{T} γ_t(j) / Σ_{t1}^{T} γ_t(j)迭代将λ更新为λ_new重复E步和M步直到参数的变化小于某个阈值或P(O|λ)的增长不再显著。核心理解鲍姆-韦尔奇算法是一个典型的无监督学习过程。E步相当于在现有模型下对隐藏状态进行“软分配”计算每个状态的概率分布。M步则相当于用这个“软分配”的结果去统计并更新模型参数就像我们有了“模糊”的标注数据一样。反复迭代模型对观测数据的似然概率就会逐步提高。4. 从零开始Python代码实现与详解理论说再多不如跑一遍代码来得实在。我们将完全使用NumPy库不依赖任何专门的HMM包实现上述三个核心算法。4.1 模型定义与初始化首先我们定义一个HMM类并实现初始化方法。参数可以随机初始化也可以根据经验设定。import numpy as np class HMM: def __init__(self, n_states, n_observations): 初始化HMM模型结构 :param n_states: 隐藏状态数量 N :param n_observations: 观测值数量 M self.N n_states self.M n_observations # 初始化参数 A, B, pi # 使用随机值但确保满足概率分布的性质和为1 self.A np.random.rand(n_states, n_states) self.A self.A / self.A.sum(axis1, keepdimsTrue) # 行归一化 self.B np.random.rand(n_states, n_observations) self.B self.B / self.B.sum(axis1, keepdimsTrue) # 行归一化 self.pi np.random.rand(n_states) self.pi self.pi / self.pi.sum() # 归一化 def _validate_observations(self, O): 将观测序列转换为内部索引0-based # 这里假设观测序列O是整数列表每个整数代表观测集合V中的索引 # 如果输入是符号需要先建立映射字典 return np.array(O, dtypeint)4.2 前向算法实现带缩放防下溢这是评估问题的核心。我们实现一个带缩放Scaling的版本这是工程上的最佳实践。def forward(self, O): 带缩放的前向算法计算观测序列的概率P(O|λ)以及所有前向概率alpha :param O: 观测序列长度为T :return: log_prob (对数概率), alpha (缩放后的前向概率), scale_factors (缩放因子) O self._validate_observations(O) T len(O) alpha np.zeros((T, self.N)) # 缩放后的alpha scale np.zeros(T) # 缩放因子c_t # 1. 初始化 alpha[0] self.pi * self.B[:, O[0]] scale[0] 1.0 / alpha[0].sum() alpha[0] * scale[0] # 2. 递推 for t in range(1, T): for j in range(self.N): # 计算未缩放的alpha_t(j) alpha[t, j] np.dot(alpha[t-1], self.A[:, j]) * self.B[j, O[t]] # 缩放当前时刻的alpha防止下溢 scale[t] 1.0 / alpha[t].sum() alpha[t] * scale[t] # 3. 计算对数概率 # P(O|λ) Π_{t1}^T (1/c_t)因此 log P -Σ log(c_t) log_prob -np.sum(np.log(scale)) return log_prob, alpha, scale代码细节解析缩放因子scale[t]是当前时刻所有alpha[t, :]之和的倒数。缩放后alpha[t]变成了一个条件概率分布给定前t个观测时刻t处于各状态的概率。最终的概率通过对缩放因子取对数求和再取负得到。这个方法完美解决了概率值过小的问题。4.3 维特比算法实现接下来是实现解码找到最优隐藏状态序列。def viterbi(self, O): 维特比算法解码最优隐藏状态序列 :param O: 观测序列 :return: (最优路径概率的对数, 最优状态序列) O self._validate_observations(O) T len(O) delta np.zeros((T, self.N)) psi np.zeros((T, self.N), dtypeint) # 记录回溯指针 # 1. 初始化 delta[0] np.log(self.pi 1e-12) np.log(self.B[:, O[0]] 1e-12) # psi[0] 默认为0无需设置 # 2. 递推 for t in range(1, T): for j in range(self.N): # 计算转移到状态j的所有可能路径的概率对数空间 trans_probs delta[t-1] np.log(self.A[:, j] 1e-12) # 找到最大概率对应的前一状态 max_idx np.argmax(trans_probs) delta[t, j] trans_probs[max_idx] np.log(self.B[j, O[t]] 1e-12) psi[t, j] max_idx # 3. 终止 log_prob np.max(delta[T-1]) best_last_state np.argmax(delta[T-1]) # 4. 回溯 best_path np.zeros(T, dtypeint) best_path[T-1] best_last_state for t in range(T-2, -1, -1): best_path[t] psi[t1, best_path[t1]] return log_prob, best_path避坑技巧在计算对数概率时我们给概率值加了一个很小的数如1e-12再取对数。这是因为log(0)是负无穷会导致计算错误。这个技巧称为“拉普拉斯平滑”的一种简单形式在概率可能为0时非常有用。psi矩阵存储的是索引用于回溯重建路径。4.4 鲍姆-韦尔奇算法实现最后是挑战最大的参数学习算法。我们需要实现E步和M步。def baum_welch(self, O, max_iter100, tol1e-4): 鲍姆-韦尔奇算法无监督学习模型参数 :param O: 观测序列列表或数组 :param max_iter: 最大迭代次数 :param tol: 收敛阈值对数似然变化 :return: 训练后的模型自身 O self._validate_observations(O) T len(O) log_prob_old -np.inf for iteration in range(max_iter): # ---------- E步 ---------- # 计算前向概率、后向概率、缩放因子 log_prob, alpha, scale self.forward(O) beta self._backward(O, scale) # 需要实现带缩放的后向算法 # 计算gamma和xi gamma, xi self._compute_gamma_xi(alpha, beta, O) # 需要实现此辅助函数 # 检查似然是否增长如果下降则发出警告可能由于数值问题 if iteration 0 and log_prob log_prob_old: print(fIteration {iteration}: Log-likelihood decreased from {log_prob_old:.4f} to {log_prob:.4f}. Possible numerical issue.) # 检查收敛 if iteration 0 and abs(log_prob - log_prob_old) tol: print(fConverged after {iteration} iterations.) break log_prob_old log_prob # ---------- M步 ---------- # 1. 更新初始状态概率 pi self.pi gamma[0] # 2. 更新状态转移概率 A for i in range(self.N): denominator np.sum(gamma[:-1, i]) # 分母从状态i出发的期望次数 if denominator 0: for j in range(self.N): self.A[i, j] np.sum(xi[:, i, j]) / denominator else: self.A[i, :] 1.0 / self.N # 平滑处理 # 3. 更新观测概率 B for j in range(self.N): denominator np.sum(gamma[:, j]) # 分母处于状态j的期望总次数 if denominator 0: for k in range(self.M): # 找出所有观测值为k的时刻对gamma求和 mask (O k) self.B[j, k] np.sum(gamma[mask, j]) / denominator else: self.B[j, :] 1.0 / self.M # 平滑处理 # 行归一化确保仍然是概率分布 self.A self.A / self.A.sum(axis1, keepdimsTrue) self.B self.B / self.B.sum(axis1, keepdimsTrue) if iteration % 10 0: print(fIteration {iteration}, log-likelihood: {log_prob:.4f}) return self def _backward(self, O, scale): 带缩放的后向算法实现 T len(O) beta np.zeros((T, self.N)) # 初始化: beta_{T-1}(i) 1并应用缩放 beta[T-1] scale[T-1] # 递推 for t in range(T-2, -1, -1): for i in range(self.N): beta[t, i] 0 for j in range(self.N): beta[t, i] self.A[i, j] * self.B[j, O[t1]] * beta[t1, j] # 应用与forward中对应的缩放因子 beta[t] * scale[t] return beta def _compute_gamma_xi(self, alpha, beta, O): 利用alpha, beta计算gamma和xi T len(O) gamma alpha * beta # 根据定义缩放后的alpha*beta即为gamma # 需要重新归一化吗因为alpha和beta都缩放过它们的乘积gamma自动满足和为1对于每个t xi np.zeros((T-1, self.N, self.N)) for t in range(T-1): denominator np.sum(alpha[t] self.A * self.B[:, O[t1]].T * beta[t1]) for i in range(self.N): for j in range(self.N): numerator alpha[t, i] * self.A[i, j] * self.B[j, O[t1]] * beta[t1, j] xi[t, i, j] numerator / denominator if denominator 0 else 0 return gamma, xi实现难点与技巧数值稳定性前后向算法都必须使用相同的缩放因子否则gamma的计算会出错。我们的_backward函数接收scale参数确保了缩放一致性。平滑处理在M步更新A和B时分母可能为0例如某个状态在训练序列中从未出现。这时直接更新会导致除零错误。我们添加了一个简单的平滑策略如果分母为0则将该行参数设为均匀分布。更精细的方法可以使用拉普拉斯平滑或加一个很小的伪计数。迭代终止我们监控对数似然log_prob的变化。当变化小于阈值tol时认为模型已收敛。同时设置最大迭代次数防止无限循环。多序列训练上述实现是针对单条观测序列的。实际中我们通常有多个独立观测序列用于训练。此时E步需要对每个序列分别计算gamma和xi然后在M步中用所有序列的统计量之和来更新参数。这需要对代码进行一些扩展核心思想是将每个序列视为一个独立样本累加其充分统计量。5. 实战演练用HMM预测天气与分析股价现在让我们用实现的HMM类来模拟两个经典场景看看模型的实际表现。5.1 场景一基于海藻湿度的天气预测假设隐藏状态是天气0-晴天1-阴天2-雨天。 观测值是海藻的湿度0-干燥1-湿润2-潮湿。 我们设定一组“真实”的参数来生成模拟数据def simulate_weather_data(true_hmm, seq_length100): 使用真实HMM生成模拟的观测序列和状态序列 states [] observations [] # 根据初始概率pi选择第一个状态 current_state np.random.choice(range(true_hmm.N), ptrue_hmm.pi) states.append(current_state) # 根据第一个状态生成第一个观测 observations.append(np.random.choice(range(true_hmm.M), ptrue_hmm.B[current_state])) for _ in range(1, seq_length): # 根据转移矩阵A选择下一个状态 current_state np.random.choice(range(true_hmm.N), ptrue_hmm.A[current_state]) states.append(current_state) # 根据新状态生成观测 observations.append(np.random.choice(range(true_hmm.M), ptrue_hmm.B[current_state])) return np.array(states), np.array(observations) # 定义“真实”的天气HMM参数 true_hmm HMM(n_states3, n_observations3) # 手动设置合理的参数模拟一个天气系统 true_hmm.pi np.array([0.6, 0.3, 0.1]) # 初始大概率晴天 true_hmm.A np.array([ [0.7, 0.2, 0.1], # 晴天 - 晴天/阴天/雨天 [0.3, 0.5, 0.2], # 阴天 - ... [0.2, 0.3, 0.5] # 雨天 - ... ]) true_hmm.B np.array([ [0.8, 0.15, 0.05], # 晴天时干燥/湿润/潮湿的概率 [0.2, 0.6, 0.2], # 阴天时... [0.05, 0.25, 0.7] # 雨天时... ]) # 生成模拟数据 true_states, observations simulate_weather_data(true_hmm, seq_length200) print(fGenerated observation sequence (first 20): {observations[:20]}) print(fTrue state sequence (first 20): {true_states[:20]}) # 初始化一个待训练的模型参数随机 learn_hmm HMM(n_states3, n_observations3) # 使用鲍姆-韦尔奇算法仅基于观测序列进行训练 learn_hmm.baum_welch(observations, max_iter50, tol1e-6) print(\n--- Learned Parameters ---) print(Initial Prob (π):) print(learn_hmm.pi) print(\nTransition Matrix (A):) print(learn_hmm.A) print(\nEmission Matrix (B):) print(learn_hmm.B) # 使用训练好的模型进行解码预测状态序列 log_prob, predicted_states learn_hmm.viterbi(observations) print(f\nViterbi decoding log probability: {log_prob:.2f}) # 计算解码准确率与真实状态对比 accuracy np.mean(predicted_states true_states) print(fState prediction accuracy: {accuracy:.2%})运行这段代码你会看到模型从随机的初始参数开始通过迭代学习最终得到的A、B、π矩阵会非常接近我们预设的“真实”参数可能行列顺序不同因为状态标签是匿名的。解码准确率通常会很高这证明了HMM在状态推断上的有效性。5.2 场景二股票市场隐状态分析这个例子更具探索性。我们假设股票市场存在几种不同的“隐状态”比如“牛市”、“震荡市”、“熊市”。每天的股价涨跌观测受到当前市场状态的影响。我们无法直接看到市场状态但可以通过历史价格数据来推断。# 假设我们有一组简单的股价涨跌观测0-跌1-平2-涨 # 这里我们用随机序列模拟真实应用中应使用历史数据 np.random.seed(42) fake_price_data np.random.choice([0, 1, 2], size500, p[0.4, 0.2, 0.4]) # 假设有3种隐藏的市场状态 market_hmm HMM(n_states3, n_observations3) # 用鲍姆-韦尔奇算法学习市场状态 market_hmm.baum_welch(fake_price_data, max_iter30) # 解码看看模型认为的历史市场状态序列 _, market_states market_hmm.viterbi(fake_price_data) # 分析状态序列的统计特性 from collections import Counter state_counts Counter(market_states) print(Inferred market state distribution:) for state, count in state_counts.items(): print(f State {state}: {count} days ({count/len(market_states):.1%})) # 可以进一步分析每个状态下涨跌的概率 print(\nEmission probabilities per state (learned B matrix):) print(market_hmm.B) # 例如如果某个状态的B矩阵中‘涨’的概率很高我们可以将其解读为‘牛市状态’这个例子更侧重于展示HMM在无监督模式发现上的能力。模型从杂乱的价格变动中自动聚类出了几种不同的“模式”。我们可以通过分析学习到的B矩阵来尝试解释每种状态的含义。例如某个状态下“涨”的概率显著高于其他状态我们就可以将其标记为“上涨动力状态”。6. 常见问题、调试技巧与进阶思考在实际实现和应用HMM时你会遇到各种各样的问题。这里记录一些我踩过的坑和解决方法。6.1 数值下溢与稳定性处理这是实现HMM算法时头号敌人。概率连乘会迅速趋近于0超出浮点数精度范围。症状概率输出为0或NaN对数概率为-inf。解决方案对数空间计算如维特比算法所示全程在对数空间进行计算用加法代替乘法。这是最常用、最有效的方法。缩放技巧如前向-后向算法实现所示每一步都对中间概率进行缩放使其和为1。缩放因子需要被记录下来用于最终概率的计算。添加极小值在取对数前给概率矩阵A和B的元素加上一个极小值如1e-12防止log(0)。但这只是权宜之计不能根本解决连乘下溢。我的选择对于评估问题前向/后向我推荐缩放技巧因为它能同时得到缩放后的alpha和beta方便计算gamma和xi。对于解码问题维特比对数空间计算是标准做法。两者结合使用时需注意接口的一致性。6.2 鲍姆-韦尔奇算法不收敛或效果差可能原因1初始值太差。EM算法对初始值敏感容易陷入局部最优。对策尝试多次随机初始化选择最终似然概率最高的模型。或者如果对数据有一定先验知识如某些状态更可能产生某些观测可以用它来初始化B矩阵。可能原因2观测序列太短。数据量不足模型无法有效学习。对策收集更多数据。对于序列数据可以考虑使用多序列训练。修改baum_welch函数使其能接受一个观测序列列表在E步分别计算每个序列的统计量在M步合并更新。可能原因3状态数N设置不合理。N太小模型表达能力不足N太大模型过于复杂容易过拟合且需要更多数据。对策N是一个超参数。可以使用交叉验证在验证集上查看模型对新序列的似然概率或者结合信息准则如AIC, BIC来选择N。可能原因4数据不符合HMM假设。例如观测可能依赖于多个过去的状态或者状态转移不是一阶马尔科夫的。对策考虑使用更复杂的模型如二阶HMM或隐半马尔可夫模型HSMM。或者对数据进行预处理使其更符合假设。6.3 状态标签歧义与对齐问题HMM学习到的状态是匿名的整数索引0,1,2,...。模型可能会学习到一个有效的状态序列但其顺序与我们心目中的语义如“牛市”、“熊市”是打乱的或颠倒的。问题在天气例子中学习到的“状态0”可能对应真实的“雨天”而不是“晴天”。这并不影响序列的预测能力但影响可解释性。解决办法如果有少量带标签的数据可以通过比较模型解码出的状态序列与真实标签建立一个映射关系。如果没有标签通常通过分析学习到的发射矩阵B来人工解释每个状态的含义例如哪个状态更倾向于发射“上涨”观测。6.4 超越基础HMM模型变体与应用扩展当你掌握了标准HMM后可以探索其强大的变体以适应更复杂的场景连续观测HMM我们的例子中观测是离散的。但很多数据是连续的如语音信号中的MFCC特征。这时需要用连续概率密度函数通常是高斯混合模型GMM来代替离散的观测矩阵B。这就是GMM-HMM它是语音识别系统的核心。隐半马尔可夫模型标准HMM中每个状态的持续时间服从几何分布这通常不符合实际例如一个“牛市”可能持续很多天。HSMM显式地对状态持续时间建模允许更灵活的驻留时间分布。输入输出HMM用于序列到序列的映射问题在条件随机场CRF流行之前常用于自然语言处理的序列标注任务。用于序列分类为每个类别如单词训练一个HMM。识别时将未知序列输入每个HMM计算似然概率选择概率最高的类别。这是早期孤立词语音识别的基本框架。从原理推导到代码实现再通过实战案例加深理解最后梳理常见问题和进阶方向这套流程走下来HMM就不再是一个黑盒子了。它精妙的概率图模型思想以及评估、解码、学习三大问题的解决方案为你理解更复杂的序列模型如RNN、LSTM、Transformer打下了坚实的基础。下次当你看到语音识别、基因预测或者金融时间序列分析时你会知道背后可能就藏着这个优雅的隐马尔可夫模型。
返回列表