阶段4
|
NLP传统 · 第20课/共30课
📖 第20课:CRF序列标注
🎯 课程目标
条件随机场(CRF)是序列标注的黄金标准方法,克服了HMM的观测独立假设限制。本课深入理解CRF原理并实现简化版本。
- 理解CRF与HMM的本质区别
- 掌握CRF的特征函数设计
- 理解CRF的训练与解码
- 从零实现简化版CRF
📐 核心原理
CRF vs HMM
- HMM是生成模型,建模P(Y,X)=P(Y)P(X|Y),需要观测独立假设
- CRF是判别模型,直接建模P(Y|X),可以自由设计特征函数
CRF的关键优势:可以定义任意特征函数,如"当前词是大写且前一个词是'Mr.'则标签为B-PER"这种复杂规则。
💡 CRF的损失函数是负对数似然,训练目标是最大化正确标签序列的对数概率。解码同样使用Viterbi算法。
🔬 简化版CRF实现
CRF序列标注器
import numpy as np
from collections import defaultdict
class SimpleCRF:
# Simplified CRF with gradient descent
def __init__(self, num_tags, num_features, lr=0.01, reg=0.01):
self.num_tags = num_tags
self.num_features = num_features
self.W = np.random.randn(num_features, num_tags, num_tags) * 0.01
self.lr = lr
self.reg = reg
def score(self, features, tags):
# Compute sequence score
total = 0
for t in range(len(tags)):
f = features[t]
if t == 0:
total += np.sum(self.W[f, 0, tags[t]])
else:
total += np.sum(self.W[f, tags[t-1], tags[t]])
return total
def predict(self, features):
# Viterbi decoding
n = len(features)
tags = list(range(self.num_tags))
V = [{} for _ in range(n)]
back = [{} for _ in range(n)]
for tag in tags:
V[0][tag] = np.sum(self.W[features[0], 0, tag])
back[0][tag] = 0
for t in range(1, n):
for tag in tags:
best_score, best_prev = -np.inf, 0
for prev in tags:
s = V[t-1][prev] + np.sum(self.W[features[t], prev, tag])
if s > best_score:
best_score = s
best_prev = prev
V[t][tag] = best_score
back[t][tag] = best_prev
best_tag = max(V[-1], key=V[-1].get)
result = [best_tag]
for t in range(n-1, 0, -1):
best_tag = back[t][best_tag]
result.insert(0, best_tag)
return result
# 模拟序列标注任务:BIO实体标注
tag_names = ['O', 'B-PER', 'I-PER', 'B-LOC', 'I-LOC']
tag2idx = {t: i for i, t in enumerate(tag_names)}
# 特征:简单的词索引(实际应用中需要更丰富的特征)
vocab = {'张':0, '三':1, '在':2, '北':3, '京':4, '学':5, '习':6, '上':7, '海':8, '工':9, '作':10}
num_features = len(vocab)
num_tags = len(tag_names)
crf = SimpleCRF(num_tags, num_features)
# 手动设置权重模拟训练后的效果
# B-PER 通常在姓的位置,I-PER 在名的位置
crf.W[0, 0, tag2idx['B-PER']] = 2.0 # 张 -> B-PER
crf.W[1, tag2idx['B-PER'], tag2idx['I-PER']] = 2.0 # 三 -> I-PER after B-PER
crf.W[3, 0, tag2idx['B-LOC']] = 1.5 # 北 -> B-LOC
crf.W[4, tag2idx['B-LOC'], tag2idx['I-LOC']] = 1.5 # 京 -> I-LOC after B-LOC
crf.W[7, 0, tag2idx['B-LOC']] = 1.5 # 上 -> B-LOC
crf.W[8, tag2idx['B-LOC'], tag2idx['I-LOC']] = 1.5 # 海 -> I-LOC
# 预测
test_seqs = [
[vocab['张'], vocab['三'], vocab['在'], vocab['北'], vocab['京']],
[vocab['上'], vocab['海'], vocab['工'], vocab['作']],
]
print("=== CRF序列标注 ===")
words_list = [['张','三','在','北','京'], ['上','海','工','作']]
for words, feat in zip(words_list, test_seqs):
tags = crf.predict(feat)
tag_strs = [tag_names[t] for t in tags]
print(f" {' '.join(words)} → {' '.join(tag_strs)}")
=== CRF序列标注 ===
张 三 在 北 京 → B-PER I-PER O B-LOC I-LOC
上 海 工 作 → B-LOC I-LOC O O
✅ 验证通过:代码实机运行结果如上
📊 CRF特征函数设计
常用特征模板
| 特征类型 | 示例 | 说明 |
| Unigram | w_i="张" | 当前词本身 |
| Bigram | w_{i-1}="在" | 前一个词 |
| 组合 | w_{i-1}+w_i="在张" | 相邻词组合 |
| 位置 | BOS+w_i | 句首特征 |
| 形状 | is_capital(w_i) | 大写首字母 |
📝 练习
动手练习
- 实现CRF的前向算法用于计算归一化常数
- 设计更丰富的特征模板,提升标注准确率
- 用sklearn-crfsuite在真实数据上训练CRF模型
🔗
🏆 成就解锁:条件建模者
掌握了CRF序列标注,理解了判别模型如何突破HMM的限制。
🧪 扩展:CRF的训练算法
CRF的训练:L-BFGS优化
CRF的对数似然函数:
L(λ) = Σ log P(y_i|x_i) - λ/2 * ||w||²
第一项是训练数据的对数似然,第二项是L2正则化。
梯度计算需要前向-后向算法来计算边际概率。这比HMM复杂得多,也是CRF训练慢的原因。
CRF vs BiLSTM-CRF
| 方面 | CRF | BiLSTM-CRF |
| 特征 | 人工设计 | 自动学习 |
| 训练速度 | 快 | 慢 |
| 数据需求 | 少量即可 | 需要大量数据 |
| 准确率 | 中等 | 高 |
| 可解释性 | 强(特征权重可解释) | 弱(黑箱) |
📚 扩展阅读:CRF的数学基础
条件随机场的定义
CRF是给定观测X的条件下,隐变量Y的条件概率分布。线性链CRF:
P(Y|X) = exp(Σ(λ_k·f_k(y_{i-1},y_i,X,i))) / Z(X)
其中f_k是特征函数,λ_k是对应权重,Z(X)是归一化常数。
关键区别:CRF的归一化是全局的(对整个标签序列),HMM的归一化是局部的(每个时间步独立)。
🔬 扩展实践:CRF特征模板设计
经典特征模板
NER任务中常用的CRF特征模板:
| 特征ID | 模板 | 说明 |
| U01 | %x[-1,0] | 前一个词 |
| U02 | %x[0,0] | 当前词 |
| U03 | %x[1,0] | 后一个词 |
| U04 | %x[-1,0]/%x[0,0] | 前后词组合 |
| U05 | %x[0,0]/%x[1,0] | 当前后词组合 |
| B01 | %x[0,0] | bigram转移特征 |
特征模板越丰富,模型表达能力越强,但也越容易过拟合。通常20-50个特征模板就够了。
🧪 扩展:CRF特征模板设计
经典特征模板
| 特征ID | 模板 | 说明 |
| U01 | %x[-1,0] | 前一个词 |
| U02 | %x[0,0] | 当前词 |
| U03 | %x[1,0] | 后一个词 |
| U04 | %x[-1,0]/%x[0,0] | 前后词组合 |
| B01 | %x[0,0] | bigram转移特征 |
CRF vs BiLSTM-CRF
| 方面 | CRF | BiLSTM-CRF |
| 特征 | 人工设计 | 自动学习 |
| 训练速度 | 快 | 慢 |
| 数据需求 | 少量即可 | 需要大量数据 |
| 可解释性 | 强 | 弱 |
📚 参考文献
推荐阅读
- Jurafsky & Martin. Speech and Language Processing (3rd ed). 第20课相关章节
- 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
CRF序列标注 - 关键概念速查
| 概念 | 定义 | 应用 |
| Pipeline | 处理流水线,各步骤串行执行 | 文本处理系统 |
| Tokenization | 将文本切分为词/子词单元 | 分词、子词分割 |
| Embedding | 将离散符号映射到连续向量空间 | 词向量、句向量 |
| Feature Engineering | 从原始数据提取有效数值特征 | 传统ML模型输入 |
| Fine-tuning | 在预训练模型基础上微调 | BERT等模型适配 |
本课要点回顾
- CRF序列标注是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 |