第11章:条件随机场(Conditional Random Field)
一、导读
11.1 本章学习目标
第11章介绍条件随机场(CRF)——序列标注任务的主流方法。与HMM不同,CRF是判别模型,直接建模条件概率 P(Y|X),不需要对观测数据做独立性假设。这使得CRF在命名实体识别、词性标注、分词等任务上通常优于HMM。
本章的核心学习目标包括:
- 理解概率无向图模型:马尔可夫随机场的基本概念
- 掌握条件随机场的定义:给定观测序列的条件下,输出序列满足马尔可夫性
- 理解线性链CRF:适用于序列标注的特殊CRF结构
- 掌握特征函数:状态特征和转移特征的设计
- 掌握CRF的参数估计:极大似然估计和改进的迭代尺度法
- 了解CRF的预测:Viterbi算法在CRF中的应用
11.2 为什么本章如此重要
CRF是序列标注领域最重要的模型之一。在深度学习兴起之前,CRF几乎是命名实体识别、词性标注等任务的标准方法。即使在深度学习时代,CRF层仍然被广泛用于结构化预测任务(如BiLSTM-CRF)。
CRF相比HMM的核心优势在于:它是判别模型,不需要对观测数据做条件独立假设。这意味着CRF可以灵活地使用任意复杂的特征,包括重叠特征、长距离依赖等。
11.3 核心问题引导
在阅读本章时,请带着以下问题思考:
CRF和HMM有什么本质区别?为什么CRF通常表现更好?
什么是概率无向图模型?马尔可夫随机场是什么?
条件随机场的定义是什么?"条件"体现在哪里?
线性链CRF的特征函数如何设计?状态特征和转移特征有什么区别?
CRF的参数估计为什么比HMM更困难?
CRF在序列标注中如何应用?
二、核心概念详解
11.1 概率无向图模型
11.1.1 马尔可夫随机场
概率无向图模型(也称为马尔可夫随机场,MRF)是用无向图表示的联合概率分布。
给定一个无向图 G = (V, E),如果随机变量 Y 满足马尔可夫性质——每个节点在给定邻居的条件下与其他节点条件独立——则 Y 构成马尔可夫随机场。
11.1.2 因子分解
MRF的联合概率可以分解为图上的团(clique)的势函数的乘积:
P(Y) = (1/Z) Π_C Ψ_C(Y_C)其中 C 是图中的团,Ψ_C 是团 C 的势函数,Z 是归一化因子。
import numpy as np
def mrf_intuition():
"""马尔可夫随机场的直觉理解"""
print("概率无向图模型(MRF):")
print()
print(" Y1 --- Y2 --- Y3 --- Y4 --- Y5")
print()
print("马尔可夫性质:")
print(" 每个节点在给定邻居的条件下与其他节点独立")
print(" 例如:P(Y3 | Y1,Y2,Y4,Y5) = P(Y3 | Y2,Y4)")
print()
print("联合概率分解:")
print(" P(Y) = (1/Z) * Ψ(Y1,Y2) * Ψ(Y2,Y3) * Ψ(Y3,Y4) * Ψ(Y4,Y5)")
print()
print("这种分解使得高维概率的计算变得可行")
mrf_intuition()11.2 条件随机场的定义
11.2.1 定义
给定无向图 G = (V, E),如果 Y 是以 X 为条件的马尔可夫随机场:
P(Y_v | X, Y_{W}) = P(Y_v | X, Y_{N(v)})其中 N(v) 是节点 v 的邻居,则 (X, Y) 构成条件随机场。
11.2.2 线性链CRF
对于序列标注任务,最常用的CRF结构是线性链:
X_1 --- X_2 --- ... --- X_T
| | |
Y_1 --- Y_2 --- ... --- Y_T在线性链CRF中,给定观测序列 X,输出序列 Y 满足一阶马尔可夫性:
P(Y_t | X, Y_1, ..., Y_{t-1}) = P(Y_t | X, Y_{t-1}, Y_{t+1})11.3 线性链CRF的模型
11.3.1 条件概率
线性链CRF的条件概率定义为:
P(y|x) = (1/Z(x)) * exp(Σ_k w_k * f_k(y, x))其中:
- f_k 是特征函数
- w_k 是特征权重
- Z(x) 是归一化因子
11.3.2 特征函数
CRF的特征函数通常分为两类:
转移特征(transition features):依赖于当前位置和前一位置的标签
s_k(y_{t-1}, y_t, x, t) = t_k(y_{t-1}, y_t) * l_k(x, t)状态特征(state features):依赖于当前位置的标签和观测
u_k(y_t, x, t) = l_k(x, t) * I(y_t = label)import numpy as np
def crf_features_example():
"""CRF特征函数示例"""
print("CRF特征函数设计(以词性标注为例):")
print()
print("转移特征(捕捉标签之间的转移规律):")
print(" f1: 如果 y_{t-1}=动词 且 y_t=名词,则=1")
print(" f2: 如果 y_{t-1}=形容词 且 y_t=名词,则=1")
print(" f3: 如果 y_{t-1}=名词 且 y_t=动词,则=1")
print()
print("状态特征(捕捉标签与观测之间的关系):")
print(" f4: 如果 x_t 以'ly'结尾 且 y_t=副词,则=1")
print(" f5: 如果 x_t 首字母大写 且 y_t=专有名词,则=1")
print(" f6: 如果 x_t 在词典中 且 y_t=名词,则=1")
print()
print("CRF的优势:可以使用任意复杂的、重叠的特征")
crf_features_example()11.3.3 权值化和矩阵形式
线性链CRF可以用矩阵形式表示。定义 M 个 T×T 的矩阵:
M_i(x) = [exp(Σ_k w_k * f_k(y_{t-1}=i, y_t=j, x, t))]则归一化因子可以写成矩阵连乘:
Z(x) = [M_1(x) * M_2(x) * ... * M_T(x)]_{start, stop}import numpy as np
def crf_matrix_form():
"""CRF的矩阵形式"""
# 简化的CRF矩阵形式
# 2个标签,3个时间步
T = 3 # 序列长度
n_labels = 2
# 每个位置的转移矩阵
M = np.random.rand(T, n_labels, n_labels)
for t in range(T):
M[t] = np.exp(M[t]) # 确保正值
# 计算Z(x):矩阵连乘
Z_matrix = M[0]
for t in range(1, T):
Z_matrix = Z_matrix @ M[t]
Z = np.sum(Z_matrix)
print(f"序列长度 T={T}, 标签数={n_labels}")
print(f"Z(x) = {Z:.4f}")
print(f"\n矩阵连乘的复杂度: O(T * n_labels^3)")
print(f"比暴力枚举 O(n_labels^T) 高效得多")
crf_matrix_form()11.4 CRF的参数估计
11.4.1 对数似然
CRF使用条件对数似然作为目标函数:
L(w) = Σ_i log P(y^{(i)} | x^{(i)})
= Σ_i [Σ_k w_k * f_k(y^{(i)}, x^{(i)}) - log Z(x^{(i)})]11.4.2 梯度
∂L/∂w_k = Σ_i [f_k(y^{(i)}, x^{(i)}) - E_{P(y|x^{(i)})}[f_k(y, x^{(i)})]]即:经验特征期望 - 模型特征期望。
11.4.3 优化方法
由于对数似然是凸函数,可以使用梯度下降、L-BFGS等方法优化。
import numpy as np
def crf_training_demo():
"""CRF训练演示(简化版)"""
# 简化的线性链CRF
# 2个标签,序列长度3
n_labels = 2
T = 3
n_features = 4
# 随机生成训练数据
np.random.seed(42)
n_train = 10
X_train = [np.random.randn(T, 2) for _ in range(n_train)]
y_train = [np.random.randint(0, n_labels, T) for _ in range(n_train)]
# 特征函数(简化:直接用观测的线性组合)
def compute_features(x, y_prev, y_curr, t):
features = np.zeros(n_features)
features[0] = x[t, 0] * (y_curr == 0)
features[1] = x[t, 1] * (y_curr == 1)
features[2] = (y_prev == 0) * (y_curr == 0)
features[3] = (y_prev == 1) * (y_curr == 1)
return features
# 简化的训练(梯度上升)
w = np.zeros(n_features)
lr = 0.01
for epoch in range(50):
grad = np.zeros(n_features)
for x, y in zip(X_train, y_train):
# 经验特征期望
for t in range(T):
y_prev = y[t-1] if t > 0 else 0
grad += compute_features(x, y_prev, y[t], t)
# 模型特征期望(简化:用贪心近似)
# 实际中需要用前向-后向算法精确计算
grad /= n_train
# 正则化
grad -= 0.01 * w
w += lr * grad
print(f"训练后的权重: {w}")
print(f"特征0(标签0的观测权重): {w[0]:.4f}")
print(f"特征1(标签1的观测权重): {w[1]:.4f}")
print(f"特征2(标签0→0转移权重): {w[2]:.4f}")
print(f"特征3(标签1→1转移权重): {w[3]:.4f}")
crf_training_demo()11.5 CRF的预测
CRF的预测问题是:给定模型和观测序列 x,找到最可能的标签序列 y*:
y* = argmax_y P(y|x)这与HMM的预测问题相同,可以使用Viterbi算法求解。
import numpy as np
def crf_viterbi(x, w, T, n_labels):
"""CRF的Viterbi预测"""
n_features = len(w)
# 定义特征函数
def score(x, y_prev, y_curr, t):
s = 0
s += w[0] * x[t, 0] * (y_curr == 0)
s += w[1] * x[t, 1] * (y_curr == 1)
s += w[2] * (y_prev == 0) * (y_curr == 0)
s += w[3] * (y_prev == 1) * (y_curr == 1)
return s
# Viterbi
delta = np.zeros((T, n_labels))
psi = np.zeros((T, n_labels), dtype=int)
# 初始化
for j in range(n_labels):
delta[0, j] = score(x, 0, j, 0)
# 递推
for t in range(1, T):
for j in range(n_labels):
scores = [delta[t-1, i] + score(x, i, j, t) for i in range(n_labels)]
delta[t, j] = max(scores)
psi[t, j] = np.argmax(scores)
# 回溯
path = np.zeros(T, dtype=int)
path[T-1] = np.argmax(delta[T-1])
for t in range(T-2, -1, -1):
path[t] = psi[t+1, path[t+1]]
return path
# 测试
np.random.seed(42)
w = np.array([1.5, -0.8, 0.5, 0.7])
x = np.random.randn(5, 2)
path = crf_viterbi(x, w, T=5, n_labels=2)
print(f"CRF预测的标签序列: {path}")11.6 CRF vs HMM
import numpy as np
def crf_vs_hmm():
"""对比CRF和HMM"""
print("CRF vs HMM 对比:")
print()
print(f"{'特性':>20} {'HMM':>15} {'CRF':>15}")
print("-" * 52)
print(f"{'模型类型':>20} {'生成模型':>15} {'判别模型':>15}")
print(f"{'建模目标':>20} {'P(X,Y)':>15} {'P(Y|X)':>15}")
print(f"{'观测假设':>20} {'条件独立':>15} {'无假设':>15}")
print(f"{'特征灵活性':>20} {'受限':>15} {'灵活':>15}")
print(f"{'训练数据需求':>20} {'较多':>15} {'较少':>15}")
print(f"{'预测速度':>20} {'快':>15} {'较慢':>15}")
print(f"{'典型应用':>20} {'语音识别':>15} {'序列标注':>15}")
print()
print("CRF的核心优势:")
print(" 1. 不需要对观测数据做条件独立假设")
print(" 2. 可以使用任意复杂的特征")
print(" 3. discriminative training通常效果更好")
crf_vs_hmm()三、实战案例
3.1 简化的序列标注
import numpy as np
def simple_sequence_labeling():
"""简化的序列标注示例"""
# 模拟NER任务
# 标签:B-PER(人名开始), I-PER(人名中间), O(其他)
sentences = [
(["John", "lives", "in", "New", "York"],
["B-PER", "O", "O", "B-LOC", "I-LOC"]),
(["Mary", "works", "at", "Google"],
["B-PER", "O", "O", "B-ORG"]),
(["Bob", "visited", "Paris"],
["B-PER", "O", "B-LOC"]),
]
print("序列标注示例(NER):")
for words, tags in sentences:
print(f" 句子: {' '.join(words)}")
print(f" 标签: {' '.join(tags)}")
print()
print("CRF在NER中的特征设计:")
print(" 当前词: word='John' → B-PER")
print(" 词性: POS='NNP' → B-PER")
print(" 前一个标签: prev_tag='O' + word='John' → B-PER")
print(" 词形: is_capitalized=True → B-PER/B-LOC/B-ORG")
simple_sequence_labeling()3.2 特征模板
import numpy as np
def feature_templates():
"""CRF特征模板示例"""
print("CRF特征模板(以词性标注为例):")
print()
print("Unigram特征(当前词):")
print(" U00: %x[0,0] → 当前词")
print(" U01: %x[0,1] → 当前词的词性")
print(" U02: %x[-1,0]/%x[0,0] → 前一个词/当前词")
print()
print("Bigram特征(标签转移):")
print(" B: %x[-1,0]/%x[0,0] → 前一个标签/当前标签")
print()
print("这些模板会自动生成大量具体的特征函数")
print("例如 U00:word='John' 在词为'John'时激活")
feature_templates()四、本章小结
第11章介绍了条件随机场:
概率无向图模型:用无向图表示的联合概率分布,满足马尔可夫性质。
条件随机场:以观测为条件的马尔可夫随机场,直接建模 P(Y|X)。
线性链CRF:适用于序列标注,条件概率用特征函数的指数形式表示。
特征函数:分为转移特征和状态特征,可以灵活设计。
参数估计:极大似然估计,使用梯度方法优化。
预测:使用Viterbi算法找到最优标签序列。
CRF vs HMM:CRF是判别模型,不需要观测独立性假设,特征更灵活。
CRF的核心启示是:判别模型 + 灵活的特征设计 = 强大的序列标注能力。
五、思考与练习
思考题
CRF为什么是判别模型?与生成模型(HMM)相比有什么优势?
线性链CRF的特征函数如何设计?状态特征和转移特征各捕捉什么信息?
CRF的参数估计为什么比HMM更困难?归一化因子Z(x)的计算复杂度是多少?
CRF在什么情况下会退化为HMM?
如何设计CRF的特征来捕捉长距离依赖?
编程练习
实现简化的线性链CRF,包括特征计算和Viterbi预测。
对比CRF和HMM在简单序列标注任务上的效果。
设计CRF的特征模板,用于词性标注任务。