ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

隐马尔可夫模型(HMM)原理与实战:从序列标注到中文分词

隐马尔可夫模型(HMM)原理与实战:从序列标注到中文分词 1. 从“猜词游戏”到序列标注为什么我们需要隐马尔可夫模型想象一下你正在玩一个“听音猜词”的游戏。你坐在一个屏风后面只能听到你的朋友在另一侧用键盘打字的声音。你听不到他具体在说什么但你能清晰地听到每一次按键发出的“咔哒”声。你的任务是仅凭这一连串“咔哒”声的序列去猜测他到底打出了什么单词或句子。这个看似不可能的任务恰恰是序列标注问题的一个绝佳比喻我们观察到的是表面现象观测序列即按键声我们需要推断的是背后隐藏的真实状态状态序列即打出的字符。在自然语言处理、生物信息学、语音识别等众多领域这类问题无处不在。比如在中文分词中我们看到的是一串连续的汉字观测序列需要给每个字打上标签状态序列如“B”词首、“M”词中、“E”词尾、“S”单字词从而将字序列切分成词序列。又比如在词性标注中看到的是一句话的单词序列观测序列需要推断每个单词对应的词性状态序列如名词、动词等。隐马尔可夫模型就是为解决这类“由表及里”的推理问题而设计的一种强大统计工具。它巧妙地刻画了这样一个双重随机过程一个是隐藏的、我们无法直接观测的状态序列比如打字员脑子里想的词这个序列本身按照一个马尔可夫链在演化另一个是我们可以观测到的观测序列比如按键声这个序列的每一个观测值其概率分布只依赖于当前时刻的隐藏状态。HMM之所以在序列标注任务中经久不衰甚至在深度学习时代依然有其独特的价值核心在于它的几个关键特性首先模型假设清晰且符合直觉。它认为当前状态只依赖于前一个状态马尔可夫性而当前观测只依赖于当前状态。这个假设虽然简化但在很多序列问题中是一个合理的近似。其次拥有高效且完备的算法工具箱。对于给定的模型参数我们可以用维特比算法高效地找出最可能的状态序列解码问题用前向-后向算法计算观测序列的概率评估问题用鲍姆-韦尔奇算法从无标签数据中学习模型参数学习问题。最后模型具有极强的可解释性。模型的参数——状态转移概率矩阵、观测概率矩阵和初始状态概率分布——都有明确的物理意义便于我们理解和调整。因此当我们需要处理那些观测与状态之间存在概率性关联的序列数据时HMM提供了一套严谨、高效且可解释的数学框架。它不仅是入门概率图模型的经典案例更是许多实际工业系统尤其是那些对可解释性和计算效率有要求的场景的基石组件。2. HMM的核心构件三组参数与两个假设要理解HMM如何工作我们必须先拆解它的数学模型。一个完整的HMM由以下五元组定义λ (N, M, A, B, π)。这看起来有些抽象我们结合中文分词的例子把它们一一具象化。N隐藏状态的集合。这是系统所有可能的内在状态。在中文分词任务中如果我们采用经典的4-tagB, M, E, S标注集那么N就是{B, M, E, S}N4。在词性标注中N可能就是{名词动词形容词...}等几十个词性标签。M观测值的集合。这是我们能直接看到的东西。在基于字的分词模型中观测值就是汉字本身M可以是整个汉字字符集规模很大。在实际建模中我们通常会处理成一个词汇表未登录词则用特殊符号如UNK表示。A状态转移概率矩阵。这是一个N x N的矩阵A [a_ij]其中a_ij P(下一时刻状态为q_j | 当前时刻状态为q_i)。它描述了隐藏状态之间的转换规律。例如在分词任务中a_{B-M}从词首转移到词中的概率应该很高而a_{B-S}从词首直接转移到单字词的概率应该为0因为一个词不可能只有一个“词首”就结束了。这个矩阵捕捉了状态序列的语法或结构约束。B观测概率矩阵。这是一个N x M的矩阵B [b_j(k)]其中b_j(k) P(在时刻t观测到v_k | 当前时刻状态为q_j)。它描述了在某个隐藏状态下产生某个观测值的可能性。例如b_{名词}(“苹果”)的概率可能很高因为“苹果”这个词经常作为名词出现而b_{动词}(“苹果”)的概率则很低。这个矩阵建立了隐藏状态与可观测现象之间的桥梁。π初始状态概率向量。这是一个长度为N的向量π [π_i]其中π_i P(初始时刻状态为q_i)。它描述了序列开始时系统处于各个隐藏状态的概率。例如一句话开头是“词首”B或“单字词”S的概率可能会比较高。有了这五元组HMM通过两个核心假设将复杂的联合概率分解为这些局部参数的乘积齐次马尔可夫性假设假设隐藏状态链在任意时刻t的状态只依赖于其前一时刻t-1的状态而与更早的历史状态和未来的状态无关。即P(q_t | q_1, q_2, ..., q_{t-1}) P(q_t | q_{t-1})。这个假设极大地简化了模型使得状态转移只需要一个矩阵A就能描述。观测独立性假设假设任意时刻t的观测值o_t只依赖于该时刻的隐藏状态q_t而与其他时刻的观测值和隐藏状态无关。即P(o_t | q_1, q_2, ..., q_T, o_1, ..., o_T) P(o_t | q_t)。这个假设使得观测概率可以简单地用矩阵B来表示。注意观测独立性假设是一个很强的简化。在实际语言中一个词的观测字或词本身很可能与其上下文观测相关。例如“苹果”后面出现“吃”的概率比出现“运行”的概率大。标准的HMM无法直接建模这种观测之间的依赖关系这是它的一个主要局限性也是后来条件随机场等模型试图改进的地方。正是基于这两个假设一个观测序列O (o_1, o_2, ..., o_T)和状态序列Q (q_1, q_2, ..., q_T)的联合概率可以优雅地表示为P(O, Q | λ) π_{q_1} * b_{q_1}(o_1) * Π_{t2}^{T} [ a_{q_{t-1} q_t} * b_{q_t}(o_t) ]这个公式是HMM所有算法推导的基石。3. 三大经典问题与算法评估、解码与学习定义了模型之后HMM要解决三个基本问题对应着三种核心算法。理解这三个问题就掌握了HMM应用的钥匙。3.1 问题一评估问题——序列有多“像”我的模型问题描述给定模型参数λ和观测序列O计算该观测序列由该模型生成的概率P(O | λ)。这有什么用比如在语音识别中我们有多个HMM模型分别对应“你好”、“谢谢”等词对于一个未知的语音观测序列我们分别计算它由每个模型生成的概率概率最大的那个模型对应的词就是识别结果。解决方案前向算法与后向算法。直接按联合概率公式对所有可能的状态序列Q求和来计算P(O|λ)计算量是O(TN^T)是指数级的不可行。前向算法通过动态规划巧妙地解决了这个问题。我们定义前向概率α_t(i) P(o_1, o_2, ..., o_t, q_t i | λ)表示在时刻t观测到前t个观测值且当前状态为i的概率。初始化α_1(i) π_i * b_i(o_1), for i 1 to N。这计算了第一个观测值由各个初始状态产生的概率。递推对t 1, 2, ..., T-1α_{t1}(j) [ Σ_{i1}^{N} α_t(i) * a_{ij} ] * b_j(o_{t1}), for j 1 to N。这个递推式的含义是要到达t1时刻的状态j必须从t时刻的某个状态i转移过来并对所有可能的i求和到达j后再由j产生观测o_{t1}。终止P(O|λ) Σ_{i1}^{N} α_T(i)。将最终时刻所有可能状态的概率相加即得到整个序列的概率。后向算法β_t(i)定义类似表示从t1到T的观测序列给定当前状态为i的概率。两者结合可以用于参数学习等更复杂的计算。前向算法将计算复杂度从指数级降低到了O(TN^2)变得非常高效。3.2 问题二解码问题——最可能的状态序列是什么问题描述给定模型λ和观测序列O求最可能最大概率对应的隐藏状态序列Q* argmax_Q P(Q | O, λ)。这正是序列标注的核心任务给定一句话观测序列找出每个词最可能的词性标签状态序列。解决方案维特比算法。这是一个应用动态规划求最优路径的经典算法与前向算法结构相似但把“求和”换成了“取最大值”。我们定义维特比变量δ_t(i) max_{q_1, ..., q_{t-1}} P(q_1, ..., q_{t-1}, q_ti, o_1, ..., o_t | λ)表示到时刻t为止所有产生前t个观测值且以状态i结尾的部分状态序列中概率最大的那条路径的概率。 同时我们需要一个回溯指针ψ_t(i)来记录这条最优路径是从哪个状态来的。初始化δ_1(i) π_i * b_i(o_1)ψ_1(i) 0递推对t 2, 3, ..., Tδ_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} ]这一步为每个时刻的每个状态j找到到达它的最优前驱状态i。终止P* max_{1≤i≤N} δ_T(i)最优路径的概率q_T* argmax_{1≤i≤N} δ_T(i)最优路径的终点路径回溯对t T-1, T-2, ..., 1q_t* ψ_{t1}(q_{t1}*)从终点开始根据回溯指针倒推出完整的最优状态序列。维特比算法是序列标注任务的实际执行者。在中文分词中我们就是用这个算法根据训练好的A、B、π矩阵对输入的句子字序列解码出最优的B/M/E/S标签序列再根据标签序列规则合并成词。3.3 问题三学习问题——如何从数据中学习模型参数问题描述给定观测序列O可能有很多条如何调整模型参数λ(A, B, π)使得P(O|λ)最大这是模型的训练过程。解决方案鲍姆-韦尔奇算法。这是一种特殊的期望最大化算法。当我们的训练数据只有观测序列而没有对应的状态序列标签时无监督或半监督场景这个算法可以大显身手。算法的核心是定义两个中间变量ξ_t(i, j)在给定模型λ和观测序列O的条件下时刻t处于状态i且时刻t1处于状态j的概率。γ_t(i)在给定模型λ和观测序列O的条件下时刻t处于状态i的概率。通过前向-后向算法我们可以计算出所有时刻的α_t(i)和β_t(i)进而推导出ξ_t(i, j)和γ_t(i)。然后利用这些“软计数”来重新估计模型参数初始状态概率π_i的估计等于在时刻1处于状态i的期望次数即γ_1(i)。状态转移概率a_{ij}的估计等于从状态i转移到状态j的期望次数除以从状态i转移出去的期望总次数。即Σ_{t1}^{T-1} ξ_t(i, j) / Σ_{t1}^{T-1} γ_t(i)。观测概率b_j(k)的估计等于在状态j下观测到符号v_k的期望次数除以处于状态j的期望总次数。即Σ_{t1, s.t. o_tv_k}^{T} γ_t(j) / Σ_{t1}^{T} γ_t(j)。鲍姆-韦尔奇算法通过迭代执行E步计算期望ξ和γ和M步用期望重新估计参数不断优化模型参数直到P(O|λ)收敛到一个局部最大值。这使我们能够利用大量未标注的文本数据来训练HMM提升模型对语言统计规律的捕捉能力。4. 实战用Python实现一个中文分词HMM理论说得再多不如动手实现一遍。下面我们抛开现成的库用纯Python和NumPy从头实现一个基于字符的HMM分词器你会对上述算法有更深刻的理解。4.1 数据准备与参数估计我们首先需要训练数据。假设我们有一个已经分好词的中文文本文件train.txt每行一句词语之间用空格隔开例如“今天 天气 很好”。我们的第一步是将词语序列转化为字符级别的状态标签序列B, M, E, S。规则很简单单字词标记为 S多字词第一个字标记为 B最后一个字标记为 E中间的字标记为 M。例如“今天/天气/很好”会被转化为字符序列“今 天 天 气 很 好”和对应的标签序列“B E B E B E”。import numpy as np from collections import defaultdict import math def load_data_and_create_label(file_path): 加载分词训练数据并生成字序列和对应的BMES标签序列。 char_seqs [] label_seqs [] with open(file_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue words line.split() char_seq [] label_seq [] for word in words: if len(word) 1: char_seq.append(word) label_seq.append(S) else: char_seq.append(word[0]) label_seq.append(B) for ch in word[1:-1]: char_seq.append(ch) label_seq.append(M) char_seq.append(word[-1]) label_seq.append(E) char_seqs.append(char_seq) label_seqs.append(label_seq) return char_seqs, label_seqs # 假设状态顺序为 [B, M, E, S] states [B, M, E, S] state2id {s: i for i, s in enumerate(states)} id2state {i: s for i, s in enumerate(states)} # 观测值汉字的ID映射会在统计过程中动态构建 obs2id {} id2obs {}接下来我们统计足够多的数据用极大似然估计来初始化HMM的参数π, A, B。这里我们采用有监督的统计方法因为我们的训练数据有标签。def train_supervised(char_seqs, label_seqs): 有监督训练统计频次计算初始概率、转移概率和发射概率。 # 初始化计数 pi_count np.zeros(len(states)) # 初始状态计数 A_count np.zeros((len(states), len(states))) # 转移计数 B_count defaultdict(lambda: np.zeros(len(states))) # 发射计数key为汉字 state_count np.zeros(len(states)) # 状态出现总次数用于归一化B # 构建观测值词汇表 vocab set() for seq in char_seqs: vocab.update(seq) for i, obs in enumerate(sorted(vocab)): obs2id[obs] i id2obs[i] obs vocab_size len(vocab) # 统计频次 for chars, labels in zip(char_seqs, label_seqs): if len(chars) ! len(labels): continue # 初始状态 first_label_id state2id[labels[0]] pi_count[first_label_id] 1 # 遍历序列 for t in range(len(chars)): cur_state_id state2id[labels[t]] cur_char chars[t] # 发射计数 if cur_char in obs2id: B_count[cur_char][cur_state_id] 1 state_count[cur_state_id] 1 # 转移计数如果不是最后一个时刻 if t len(chars) - 1: next_state_id state2id[labels[t1]] A_count[cur_state_id][next_state_id] 1 # 计算概率加1平滑防止零概率 # 初始概率 pi (pi_count 1) / (np.sum(pi_count) len(states)) # 转移概率 A (A_count 1) / (np.sum(A_count, axis1, keepdimsTrue) len(states)) # 发射概率 B np.zeros((len(states), vocab_size)) for obs, obs_id in obs2id.items(): for s_id in range(len(states)): B[s_id, obs_id] (B_count[obs][s_id] 1) / (state_count[s_id] vocab_size) # 取对数将后续的乘法变为加法防止下溢 log_pi np.log(pi) log_A np.log(A) log_B np.log(B) return log_pi, log_A, log_B, vocab_size实操心得对数概率与平滑技术对数空间计算直接使用原始概率进行连续乘法极小的概率值会导致浮点数下溢结果变为0。因此在实际实现中我们通常存储和使用对数概率。这样概率相乘变为对数概率相加更加数值稳定。维特比算法中的max和乘法也相应变为max和加法。拉普拉斯平滑加1平滑在估计概率时对于训练集中未出现的事件如某个字从未以‘M’状态出现其条件概率会被估计为0。这会导致在预测时一旦遇到这样的组合整个序列的概率就会变成0。为了避免这种情况我们给所有计数加上一个小的常数通常是1再进行归一化。这保证了任何事件都有非零的概率提高了模型的鲁棒性。4.2 维特比解码的实现有了训练好的对数概率参数我们就可以实现维特比算法来进行分词了。def viterbi_decode(obs_seq, log_pi, log_A, log_B, obs2id): 维特比算法解码。 :param obs_seq: 观测序列字列表如 [今, 天, 天, 气, 很, 好] :return: 最可能的状态序列标签列表如 [B, E, B, E, B, E] T len(obs_seq) # 序列长度 N log_pi.shape[0] # 状态数 # 初始化维特比矩阵和回溯指针 delta np.full((T, N), -np.inf) # 存储最大对数概率 psi np.zeros((T, N), dtypeint) # 存储回溯指针 # 第一步初始化 first_obs_id obs2id.get(obs_seq[0], -1) if first_obs_id -1: # 处理未登录词可以给一个均匀分布或回退概率这里简单设为均匀 delta[0, :] log_pi np.log(1.0 / N) else: delta[0, :] log_pi log_B[:, first_obs_id] # 第二步递推 for t in range(1, T): cur_obs_id obs2id.get(obs_seq[t], -1) if cur_obs_id -1: # 未登录词发射概率设为均匀分布的对数 log_b np.log(1.0 / log_B.shape[1]) # 假设词汇表大小 else: log_b log_B[:, cur_obs_id] for j in range(N): # 当前状态j # 计算从所有前一状态i转移到j的概率并加上发射概率 trans_probs delta[t-1, :] log_A[:, j] best_prev_state np.argmax(trans_probs) delta[t, j] trans_probs[best_prev_state] log_b[j] psi[t, j] best_prev_state # 第三步终止与回溯 best_last_state np.argmax(delta[T-1, :]) best_path [best_last_state] for t in range(T-1, 0, -1): best_last_state psi[t, best_last_state] best_path.insert(0, best_last_state) # 将状态ID转换回标签 best_path_labels [id2state[s] for s in best_path] return best_path_labels4.3 标签序列到分词结果的转换解码得到BMES标签序列后我们需要将其还原为分词结果。def labels_to_words(char_seq, label_seq): 将字序列和BMES标签序列转换为分词后的词语列表。 words [] current_word [] for char, label in zip(char_seq, label_seq): current_word.append(char) if label in [E, S]: # 遇到E(词尾)或S(单字词)当前词结束 words.append(.join(current_word)) current_word [] # 处理异常情况如果循环结束current_word还有内容说明标签序列可能不以E或S结尾强制作为一个词 if current_word: words.append(.join(current_word)) return words # 整合成一个完整的分词函数 def segment(sentence, log_pi, log_A, log_B, obs2id): 对输入句子进行分词 char_list list(sentence.strip()) if not char_list: return [] label_list viterbi_decode(char_list, log_pi, log_A, log_B, obs2id) words labels_to_words(char_list, label_list) return words # 示例使用 if __name__ __main__: # 假设我们已经用train_supervised函数训练得到了log_pi, log_A, log_B # log_pi, log_A, log_B, vocab_size train_supervised(train_char_seqs, train_label_seqs) # 模拟一个训练好的模型这里用随机值代替实际应从文件加载 N_STATES 4 VOCAB_SIZE 5000 # 假设词汇表大小 dummy_log_pi np.log(np.array([0.7, 0.0, 0.2, 0.1])) # 假设初始概率 dummy_log_A np.log(np.random.dirichlet(np.ones(N_STATES), sizeN_STATES)) # 随机转移矩阵 dummy_log_B np.log(np.random.dirichlet(np.ones(VOCAB_SIZE), sizeN_STATES)) # 随机发射矩阵 # 构建一个小的测试obs2id映射 test_obs2id {ch: i for i, ch in enumerate(今天天气很好我们一起出去玩)} test_sentence 今天天气很好 result segment(test_sentence, dummy_log_pi, dummy_log_A, dummy_log_B, test_obs2id) print(f输入: {test_sentence}) print(f分词结果: {result}) # 由于是随机参数结果可能不正确但流程是完整的。这个简单的实现揭示了HMM分词的核心流程训练阶段统计频次得到概率参数预测阶段用维特比算法解码最优标签序列最后将标签序列合并成词。在实际工业系统中会对未登录词、数字、英文等做更精细的处理并可能融入更复杂的特征。5. HMM在序列标注中的优势、局限与演进尽管HMM是序列标注的奠基性模型但在实际应用中我们必须清醒地认识到它的长处和短板。核心优势模型简单训练和预测速度快。维特比解码的时间复杂度是O(T * N^2)对于状态数N不大的任务如分词、词性标注效率极高能满足实时性要求。对数据稀疏问题有一定鲁棒性。通过平滑技术可以给未见过的事件分配一个小的概率避免零概率问题。可解释性强。参数A和B有明确的物理意义便于分析和调试。例如我们可以检查A矩阵看“B-E”的转移概率是否很低这符合词语结构如果很高则说明模型可能有问题。为无标注数据训练提供了途径。鲍姆-韦尔奇算法使得利用海量无标注文本提升模型成为可能这在深度学习数据饥渴之前是一个重要优势。主要局限性观测独立性假设过强。这是HMM最受诟病的一点。它假设当前观测只与当前状态有关而与上下文观测无关。但在自然语言中当前词的出现强烈依赖于前后的词。例如“苹果”后面出现“公司”的概率远高于出现“香蕉”的概率但标准HMM无法直接利用“公司”这个观测信息来帮助判断“苹果”的状态是水果还是公司名。特征表示能力弱。HMM的观测概率B是基于离散符号的多元分布。它无法直接利用词的形态特征如前缀、后缀、词向量表示等丰富的分布式特征。标注偏置问题。由于是生成式模型HMM致力于最大化联合概率P(X, Y)而不是条件概率P(Y|X)。在解码时它倾向于选择那些本身先验概率P(Y)高的状态序列有时会忽略观测序列X提供的强烈证据。演进与改进 为了克服这些局限性序列标注模型经历了多次演进最大熵马尔可夫模型在HMM的框架内允许观测依赖于一个时间窗口内的多个观测并采用最大熵模型来估计状态转移概率和观测概率引入了丰富的特征。条件随机场直接对条件概率P(Y|X)建模彻底摆脱了观测独立性假设可以任意定义全局特征函数同时避免了标注偏置问题成为HMM之后、深度学习之前的主流序列标注模型。基于深度学习的序列标注利用循环神经网络、长短时记忆网络、Transformer等模型自动学习观测序列的上下文表示并接一个CRF层如BiLSTM-CRF进行全局归一化标注兼具强大的特征学习能力和序列建模能力目前在多数任务上达到了state-of-the-art的性能。那么HMM过时了吗并非如此。在许多场景下HMM依然是首选或重要的组成部分计算资源受限的环境如嵌入式设备或实时性要求极高的场景HMM的轻量级和高效解码是巨大优势。小数据或冷启动场景当标注数据非常少时简单的HMM比复杂的深度学习模型更不容易过拟合结合无监督学习鲍姆-韦尔奇能更快产生可用模型。可解释性要求高的场景在医疗、金融等领域模型的决策过程需要被理解HMM清晰的概率图结构比深度神经网络的“黑箱”更有优势。作为复杂模型的组件或基准HMM常作为更复杂系统的一部分如语音识别中的声学模型或作为验证新模型效果的基准线。理解HMM不仅是掌握了一个经典的序列建模工具更是理解了一整类概率图模型的思想起点。它的简洁、优雅和高效使其在序列标注的历史和现实中始终占有一席之地。当你面临一个序列标注问题时从HMM开始思考往往能帮你建立起最清晰的问题框架和解决路径。
返回列表