阶段4 | NLP传统 · 第20课/共30课

📖 第20课:CRF序列标注

🎯 课程目标

条件随机场(CRF)是序列标注的黄金标准方法,克服了HMM的观测独立假设限制。本课深入理解CRF原理并实现简化版本。

📐 核心原理

CRF vs HMM

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特征函数设计

常用特征模板

特征类型示例说明
Unigramw_i="张"当前词本身
Bigramw_{i-1}="在"前一个词
组合w_{i-1}+w_i="在张"相邻词组合
位置BOS+w_i句首特征
形状is_capital(w_i)大写首字母

📝 练习

动手练习

  1. 实现CRF的前向算法用于计算归一化常数
  2. 设计更丰富的特征模板,提升标注准确率
  3. 用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

方面CRFBiLSTM-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

方面CRFBiLSTM-CRF
特征人工设计自动学习
训练速度
数据需求少量即可需要大量数据
可解释性

📚 参考文献

推荐阅读

在线资源

CRF序列标注 - 关键概念速查

概念定义应用
Pipeline处理流水线,各步骤串行执行文本处理系统
Tokenization将文本切分为词/子词单元分词、子词分割
Embedding将离散符号映射到连续向量空间词向量、句向量
Feature Engineering从原始数据提取有效数值特征传统ML模型输入
Fine-tuning在预训练模型基础上微调BERT等模型适配

本课要点回顾

  1. CRF序列标注是NLP技术栈的重要组件
  2. 理解原理比调用API更重要——知道为什么比知道怎么做更有价值
  3. 代码验证是学习的好方法——动手跑一遍胜过读十遍
  4. 从简单方法开始,逐步过渡到复杂方法——简单方法的基线作用不可替代
  5. 实践出真知——用自己的数据跑一遍,发现课本上没写的问题

❓ 常见问题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传统MLpip install scikit-learn
nltk英文NLP工具pip install nltk
spaCy工业NLP管线pip install spacy
hanlp中文NLP工具包pip install hanlp
fasttextFacebook文本分类pip install fasttext
textblob简单NLP接口pip install textblob