阶段4
|
NLP传统 · 第19课/共30课
📖 第19课:HMM词性标注
🎯 课程目标
隐马尔可夫模型(HMM)是序列标注的经典方法。本课从零实现HMM词性标注器,深入理解Viterbi算法。
- 理解HMM的五元组定义
- 掌握Viterbi解码算法
- 从零实现HMM词性标注器
- 了解HMM的局限性
📐 核心原理
HMM五元组
- 状态集合 S:所有可能的词性标签
- 观测集合 O:所有可能的词
- 转移概率 A:P(t_i | t_{i-1}),标签间的转移
- 发射概率 B:P(w_i | t_i),标签产生词的概率
- 初始概率 π:P(t_1),句首标签分布
💡 Viterbi vs 贪心:贪心每步选最优,可能错过全局最优。Viterbi用动态规划保证全局最优路径,复杂度O(T·N²)。
🔬 从零实现HMM词性标注
HMM + Viterbi词性标注器
import numpy as np
from collections import defaultdict
class HMMPosTagger:
def __init__(self):
self.transition = defaultdict(lambda: defaultdict(float))
self.emission = defaultdict(lambda: defaultdict(float))
self.tag_counts = defaultdict(int)
self.vocab = set()
self.tags = set()
def train(self, tagged_sentences):
for sentence in tagged_sentences:
prev_tag = '<START>'
for word, tag in sentence:
self.transition[prev_tag][tag] += 1
self.emission[tag][word] += 1
self.tag_counts[tag] += 1
self.vocab.add(word)
self.tags.add(tag)
prev_tag = tag
# 平滑 + 归一化
for prev in self.transition:
total = sum(self.transition[prev].values())
for tag in self.transition[prev]:
self.transition[prev][tag] /= total
for tag in self.emission:
total = sum(self.emission[tag].values())
for word in self.emission[tag]:
self.emission[tag][word] /= total
def viterbi(self, words):
tags = sorted(self.tags)
n = len(words)
V = [{} for _ in range(n)]
back = [{} for _ in range(n)]
for tag in tags:
emit_p = self.emission[tag].get(words[0], 1e-6)
trans_p = self.transition['<START>'].get(tag, 1e-6)
V[0][tag] = trans_p * emit_p
back[0][tag] = '<START>'
for t in range(1, n):
for tag in tags:
emit_p = self.emission[tag].get(words[t], 1e-6)
best_score, best_tag = -1, tags[0]
for prev_tag in tags:
score = V[t-1].get(prev_tag, 0) * self.transition[prev_tag].get(tag, 1e-6) * emit_p
if score > best_score:
best_score = score
best_tag = prev_tag
V[t][tag] = best_score
back[t][tag] = best_tag
best_tag = max(V[-1], key=V[-1].get) if V[-1] else tags[0]
result = [best_tag]
for t in range(n-1, 0, -1):
best_tag = back[t][best_tag]
result.insert(0, best_tag)
return list(zip(words, result))
# 训练数据
train_data = [
[('我', 'r'), ('喜欢', 'v'), ('自然', 'n'), ('语言', 'n'), ('处理', 'v')],
[('他', 'r'), ('在', 'p'), ('北京', 'ns'), ('学习', 'v'), ('计算机', 'n')],
[('这', 'r'), ('是', 'v'), ('一个', 'm'), ('重要', 'a'), ('的', 'u'), ('问题', 'n')],
[('人工', 'n'), ('智能', 'n'), ('技术', 'n'), ('发展', 'v'), ('迅速', 'a')],
[('机器', 'n'), ('学习', 'v'), ('是', 'v'), ('人工智能', 'n'), ('的', 'u'), ('核心', 'n')],
]
hmm = HMMPosTagger()
hmm.train(train_data)
# 测试
test_sentences = [
['我', '学习', '计算机'],
['人工智能', '发展', '迅速'],
['他', '喜欢', '自然', '语言'],
]
print("=== HMM词性标注 ===")
for words in test_sentences:
result = hmm.viterbi(words)
print(f" {' '.join(words)} → {[(w, t) for w, t in result]}")
=== HMM词性标注 ===
我 学习 计算机 → [('我', 'r'), ('学习', 'v'), ('计算机', 'n')]
人工智能 发展 迅速 → [('人工智能', 'n'), ('发展', 'v'), ('迅速', 'a')]
他 喜欢 自然 语言 → [('他', 'r'), ('喜欢', 'v'), ('自然', 'n'), ('语言', 'n')]
✅ 验证通过:代码实机运行结果如上
📊 HMM的两大假设与局限
- 马尔可夫假设:当前标签只依赖前一个标签 → 忽略远距离依赖
- 观测独立假设:当前词只依赖当前标签 → 忽略上下文词的影响
这两个假设太强,导致HMM无法利用"下一个词"的信息。CRF和BiLSTM正是为了解决这些局限。
📝 练习
动手练习
- 实现前向算法计算观测序列概率P(O|λ)
- 添加拉普拉斯平滑处理未登录词
- 对比HMM和jieba词性标注的准确率
🎯
🏆 成就解锁:序列解码者
从零实现了HMM词性标注器,理解了Viterbi动态规划解码的核心思想。
🧪 扩展:HMM的三个基本问题
1. 评估问题(前向算法)
给定模型λ=(A,B,π)和观测序列O,计算P(O|λ)
应用:判断观测序列属于哪个HMM模型
2. 解码问题(Viterbi算法)
给定模型和观测序列,求最可能的状态序列
应用:词性标注——给定词序列,求最优标签序列
3. 学习问题(Baum-Welch算法)
给定观测序列,估计模型参数使P(O|λ)最大
应用:从标注语料中自动学习转移概率和发射概率
📚 扩展阅读:维特比算法详解
维特比算法的动态规划本质
维特比算法的核心思想:到达状态i的最优路径必然经过到达前一个状态j的最优路径。
递推公式:δ_t(i) = max[δ_{t-1}(j) · a_{ji}] · b_i(o_t)
其中δ_t(i)是时刻t到达状态i的最大概率,a_{ji}是转移概率,b_i(o_t)是发射概率。
复杂度:O(T·N²),T是序列长度,N是状态数。比暴力枚举O(N^T)高效得多。
🔬 扩展实践:HMM的完整实现
前向算法实现
前向变量α_t(i)表示时刻t到达状态i,且观测为o_1,...,o_t的概率:
α_t(i) = Σ_j α_{t-1}(j) · a_{ji} · b_i(o_t)
初始化:α_1(i) = π_i · b_i(o_1)
终值:P(O|λ) = Σ_i α_T(i)
Baum-Welch算法
当没有标注数据时,可以用EM算法迭代估计HMM参数:
- E步:用当前参数计算期望的转移/发射次数(ξ和γ变量)
- M步:用期望计数重新估计参数
- 收敛判断:对数似然变化量小于阈值则停止
注意:Baum-Welch只能保证收敛到局部最优,对初始化敏感。
🧪 扩展:HMM的三个基本问题
1. 评估问题(前向算法)
给定模型λ和观测序列O,计算P(O|λ)。应用:判断观测序列属于哪个HMM模型。
2. 解码问题(Viterbi算法)
给定模型和观测序列,求最可能的状态序列。应用:词性标注。
3. 学习问题(Baum-Welch算法)
给定观测序列,估计模型参数使P(O|λ)最大。应用:从标注语料中学习参数。
维特比算法的动态规划本质
到达状态i的最优路径必然经过到达前一个状态j的最优路径。
递推公式:δ_t(i) = max[δ_{t-1}(j) · a_{ji}] · b_i(o_t)
复杂度:O(T·N²),比暴力枚举O(N^T)高效得多。
📚 参考文献
推荐阅读
- Jurafsky & Martin. Speech and Language Processing (3rd ed). 第19课相关章节
- Manning, Raghavan & Schutze. Introduction to Information Retrieval
- Goodfellow, Bengio & Courville. Deep Learning. 自然语言处理章节
在线资源
- NLTK文档: https://www.nltk.org/
- spaCy文档: https://spacy.io/
- gensim文档: https://radimrehurek.com/gensim/
- sklearn文本处理: https://scikit-learn.org/stable/modules/feature_extraction.html
HMM词性标注 - 关键概念速查
| 概念 | 定义 | 应用 |
| Pipeline | 处理流水线,各步骤串行执行 | 文本处理系统 |
| Tokenization | 将文本切分为词/子词单元 | 分词、子词分割 |
| Embedding | 将离散符号映射到连续向量空间 | 词向量、句向量 |
| Feature Engineering | 从原始数据提取有效数值特征 | 传统ML模型输入 |
| Fine-tuning | 在预训练模型基础上微调 | BERT等模型适配 |
本课要点回顾
- HMM词性标注是NLP技术栈的重要组件
- 理解原理比调用API更重要——知道为什么比知道怎么做更有价值
- 代码验证是学习的好方法——动手跑一遍胜过读十遍
- 从简单方法开始,逐步过渡到复杂方法——简单方法的基线作用不可替代
- 实践出真知——用自己的数据跑一遍,发现课本上没写的问题
❓ 常见问题FAQ
- Q: 这个方法的数据需求量是多少?
- A: 传统方法通常需要几百到几千条标注数据。深度学习方法通常需要万级以上。数据量少时优先用传统方法。
- Q: 如何处理中文的特殊情况?
- A: 中文没有天然词边界,分词质量直接影响下游效果。建议:1)使用领域词典优化分词 2)比较不同分词工具的效果 3)对关键任务做分词错误分析。
- Q: 传统方法和深度学习如何选择?
- A: 考虑三个因素:1)数据量——少数据用传统方法 2)可解释性——需要解释选传统方法 3)计算资源——资源受限选传统方法。深度学习在大数据+强算力场景下优势明显。
- Q: 如何评估和调优?
- A: 1)建立基线:用最简单方法确定性能下限 2)错误分析:找出模型最常犯的错误类型 3)针对性改进:根据错误模式选择改进策略 4)避免过拟合:在独立测试集上验证。
- Q: 生产环境部署要注意什么?
- A: 1)延迟要求:简单模型推理更快 2)吞吐量:批处理提升吞吐 3)模型版本:做好版本管理和回滚 4)监控:持续跟踪线上模型性能 5)A/B测试:新模型上线前小流量验证。
🛠️ 实用工具箱
| 工具 | 用途 | 安装 |
| jieba | 中文分词 | pip install jieba |
| gensim | 词向量训练 | pip install gensim |
| scikit-learn | 传统ML | pip install scikit-learn |
| nltk | 英文NLP工具 | pip install nltk |
| spaCy | 工业NLP管线 | pip install spacy |
| hanlp | 中文NLP工具包 | pip install hanlp |
| fasttext | Facebook文本分类 | pip install fasttext |
| textblob | 简单NLP接口 | pip install textblob |