从零掌握NLP核心:Python案例教你用HMM实现词性标注(附完整代码)
📚 目录导读
- 为什么词性标注是NLP基石? —— 技术背景与价值
- HMM模型原理精讲 —— 用数学语言讲人话
- Python实战:手写HMM词性标注器 —— 从数据到预测
- 核心代码逐段解析 —— 训练、维特比、解码
- 结果评估与调优技巧 —— 准确率提升30%的秘诀
- 高频问答 —— 解决新手常见90%的困惑
为什么词性标注是NLP基石?
词性标注(POS Tagging)是对句子中每个单词分配词性标签(如名词、动词、形容词)的任务,它是句法分析、命名实体识别、情感分析等高级NLP任务的前置步骤。

- 句子:“我 打 篮球” → 标签:
我/代词 打/动词 篮球/名词
隐马尔可夫模型(HMM) 是解决词性标注的经典监督学习方法,它利用了单词的连续依赖关系和词性间的转移概率,相比简单规则方法,HMM能处理语言歧义(如“打”既可能是动词也可能是量词)。
HMM模型原理精讲
HMM包含两个核心随机过程:
- 隐藏状态序列(Z):词性序列(如
名词->动词->名词) - 观测序列(X):单词序列(如
我 打 篮球)
三大参数
| 参数 | 含义 | 数学表达 |
|---|---|---|
| 初始概率 π | 句子第一个词是某词性的概率 | P(Z₁=tag) |
| 转移概率 A | 前一词性→后一词性的概率 | P(Zₜ |
| 发射概率 B | 某词性下生成某单词的概率 | P(Xₜ |
求解任务:给定观测序列X,找出最可能的隐藏状态序列Z,即argmax P(Z|X)。
维特比算法通过动态规划高效求解,避免穷举所有路径。
Python实战:手写HMM词性标注器
本案例使用《人民日报》标注语料(约1万句子),实现完整的训练-预测流程。环境要求:Python 3.8+,仅需numpy库。
步骤概览
# 1. 数据预处理:加载并分割句子-词性对 # 2. 训练阶段:统计π, A, B参数(加平滑处理) # 3. 预测阶段:维特比算法解码 # 4. 评估:计算准确率
核心代码逐段解析
1 数据加载与参数统计
import numpy as np
from collections import defaultdict
def train_hmm(corpus):
# 统计词性列表,为每个标签分配ID
tags = list(set([tag for sent in corpus for tag in sent[1]]))
tag2id = {tag:i for i,tag in enumerate(tags)}
id2tag = {i:tag for i,tag in enumerate(tags)}
vocab = list(set([word for sent in corpus for word in sent[0]]))
word2id = {word:i for i,word in enumerate(vocab)}
# 初始化计数矩阵
pi = np.zeros(len(tags)) # 初始概率
A = np.zeros((len(tags), len(tags))) # 转移概率
B = np.zeros((len(tags), len(vocab))) # 发射概率
# 遍历语料统计
for words, tags_seq in corpus:
prev_tag_id = None
for i, (word, tag) in enumerate(zip(words, tags_seq)):
tag_id = tag2id[tag]
word_id = word2id[word]
if i == 0: # 初始位置
pi[tag_id] += 1
else:
A[prev_tag_id][tag_id] += 1
B[tag_id][word_id] += 1
prev_tag_id = tag_id
# 加1平滑并转log避免下溢
pi = np.log(pi + 1) - np.log(pi.sum() + len(tags))
A = np.log(A + 1) - np.log(A.sum(axis=1, keepdims=True) + len(tags))
B = np.log(B + 1) - np.log(B.sum(axis=1, keepdims=True) + len(vocab))
return pi, A, B, tag2id, id2tag, word2id
平滑处理:使用加1平滑(Laplace平滑)防止未登录词导致概率为0。
2 维特比解码算法
def viterbi_decode(sentence_words, pi, A, B, tag2id, id2tag, word2id):
T = len(sentence_words)
V = len(pi)
prob = np.zeros((V, T)) # 动态规划表
prev = np.zeros((V, T), dtype=int) # 回溯指针
# 初始化:第一个词
first_word_id = word2id.get(sentence_words[0], None)
if first_word_id is None: # 未登录词处理(用全局平均)
first_word_id = -1
B_col = np.mean(B, axis=1) # 所有词性下的平均发射概率
else:
B_col = B[:, first_word_id]
prob[:, 0] = pi + B_col
# 递推:t=1..T-1
for t in range(1, T):
word_id = word2id.get(sentence_words[t], None)
if word_id is None:
B_col = np.mean(B, axis=1)
else:
B_col = B[:, word_id]
for cur_state in range(V):
# 计算从所有前驱状态到当前状态的最大概率
trans_probs = prob[:, t-1] + A[:, cur_state]
best_prev = np.argmax(trans_probs)
prob[cur_state, t] = trans_probs[best_prev] + B_col[cur_state]
prev[cur_state, t] = best_prev
# 回溯最优路径
best_path = np.argmax(prob[:, -1])
tags_seq = [best_path]
for t in range(T-1, 0, -1):
best_path = prev[best_path, t]
tags_seq.append(best_path)
tags_seq.reverse()
return [id2tag[t] for t in tags_seq]
未登录词策略:当单词不在词表时,用该词性下所有单词的平均发射概率作为近似。
3 完整训练与测试流程
# 假设已加载corpus(training_data),test_data
pi, A, B, tag2id, id2tag, word2id = train_hmm(corpus)
correct = 0
total = 0
for words, true_tags in test_data:
pred_tags = viterbi_decode(words, pi, A, B, tag2id, id2tag, word2id)
for true, pred in zip(true_tags, pred_tags):
if true == pred: correct += 1
total += 1
print(f"准确率:{correct/total:.4f}")
结果评估与调优技巧
在标准测试集上,该实现通常达到85%-90% 准确率,调优方向:
- 数据量:使用更大语料(如CTB、UD),词性标签更丰富
- 特征工程:加入词首/尾特征(如-ing、-ed暗示动词)
- 平滑方法:使用Good-Turing或Kneser-Ney平滑
- 集成模型:HMM+CRF(条件随机场)叠加
高频问答
Q1:HMM与RNN相比,谁更适合词性标注?
A:HMM优点是可解释性强、训练快(O(TV²)),适合小数据集;RNN(LSTM)在大量数据下准确率更高(95%+),但需GPU和更长训练时间,生产环境常见HMM做基准模型。
Q2:代码中如何处理未登录词(OOV)?
A:案例中使用全局平均发射概率,更优做法是:将罕见词替换为<UNK> token,或在训练时加入形态特征(如“-ly”推测为副词)。
Q3:维特比算法为何用log概率而非原始概率?
A:防止浮点下溢,链式乘法T=50时,概率可能小到10^(-50),log可将乘法转为加法,稳定计算。
Q4:如何在自己的中文数据集上复用代码?
A:要求数据格式为每行 word/tag,句子间用空行分隔,参考PFR语料格式:
我/代词 打/动词 篮球/名词
北京/地名 是/动词 首都/名词
本文从理论到Python实现,完整展示了HMM在词性标注中的应用,掌握该案例,你便理解了监督学习在序列标注中的核心框架——同样的思路稍加改动即可用于命名实体识别、语音识别等领域,建议读者在github.com/your-repo下载完整代码,尝试修改参数观察准确率变化。
练习:扩展代码支持Bigram(二阶HMM),观察准确率提升幅度。