条件随机场(CRF)实战:从零构建PyTorch序列标注模型

📅 2026/7/14 2:18:55
条件随机场(CRF)实战:从零构建PyTorch序列标注模型
1. 条件随机场CRF基础概念条件随机场Conditional Random Field简称CRF是一种判别式概率图模型专门用于处理序列标注问题。与生成式模型如隐马尔可夫模型HMM不同CRF直接对条件概率$P(Y|X)$建模无需考虑输入$X$的分布。这种特性使其在自然语言处理任务如命名实体识别、词性标注中表现优异。1.1 CRF的核心思想CRF的核心是利用上下文信息进行序列决策。举个例子在词性标注中当前词的词性不仅取决于词本身还受前后词性的影响。CRF通过以下两种得分捕捉这种依赖关系发射得分Unigram Score当前标签与当前观测值的匹配程度如动词跑更可能对应标签V。转移得分Bigram Score相邻标签之间的转移概率如形容词→名词比形容词→动词更常见。1.2 CRF与LSTM/Transformer的结合现代序列标注模型常将CRF与深度学习结合典型结构如下特征提取层BERT/LSTM提取上下文相关的词特征。CRF层通过转移矩阵约束标签序列的合理性。例如BERT输出每个词的标签概率分布CRF层则避免出现B-PER→I-LOC这类非法转移。为什么需要CRF层即使BERT能学习上下文特征但独立预测每个标签仍可能导致序列矛盾。CRF通过全局归一化和转移约束确保输出标签序列的合法性。2. PyTorch实现CRF层2.1 定义CRF类import torch import torch.nn as nn class CRF(nn.Module): def __init__(self, num_tags): super().__init__() self.num_tags num_tags # 转移矩阵transition[i][j]表示从标签i转移到j的得分 self.transition nn.Parameter(torch.randn(num_tags, num_tags)) # 强制约束不可能转移如B-PER→I-LOC self.reset_parameters() def reset_parameters(self): nn.init.xavier_normal_(self.transition) # 禁止转移到START_TAG或从STOP_TAG转出 self.transition.data[:, START_TAG] -10000 self.transition.data[STOP_TAG, :] -100002.2 计算损失函数CRF的损失函数包含两部分真实路径得分根据真实标签计算发射得分和转移得分的总和。所有路径得分通过动态规划前向算法高效计算。def neg_log_likelihood(self, emissions, tags): # emissions: (seq_len, batch_size, num_tags) - BERT的输出 # tags: (batch_size, seq_len) - 真实标签 gold_score self._score_sentence(emissions, tags) forward_score self._forward_algorithm(emissions) return (forward_score - gold_score).mean()3. 动态规划与Viterbi解码3.1 前向算法计算所有路径得分def _forward_algorithm(self, emissions): init_alphas torch.full((batch_size, self.num_tags), -10000.) init_alphas[:, START_TAG] 0 # 初始化START_TAG得分 # 递推计算每个时间步的得分 for t in range(seq_len): emit_scores emissions[t] trans_scores self.transition.unsqueeze(0) # (1, num_tags, num_tags) # 动态规划公式alpha[t][j] logsumexp(alpha[t-1][i] trans[i][j] emit[j]) next_alphas torch.logsumexp( current_alphas.unsqueeze(2) trans_scores emit_scores.unsqueeze(1), dim1 ) return torch.logsumexp(final_alphas self.transition[STOP_TAG], dim1)3.2 Viterbi解码最优路径def viterbi_decode(self, emissions): backpointers [] # 初始化 viterbi torch.full((batch_size, self.num_tags), -10000.) viterbi[:, START_TAG] 0 for t in range(seq_len): # 计算所有可能转移的得分 scores viterbi.unsqueeze(2) self.transition # (batch, from, to) best_scores, best_tags torch.max(scores, dim1) viterbi best_scores emissions[t] backpointers.append(best_tags) # 回溯找到最优路径 best_path [] best_last_tag torch.argmax(viterbi self.transition[:, STOP_TAG], dim1) best_path.append(best_last_tag) for bp in reversed(backpointers): best_path.append(bp[range(batch_size), best_path[-1]]) return best_path[::-1]4. 实战BERT-CRF命名实体识别4.1 模型结构class BERT_CRF(nn.Module): def __init__(self, bert_model, num_tags): super().__init__() self.bert bert_model self.fc nn.Linear(bert_config.hidden_size, num_tags) self.crf CRF(num_tags) def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_ids, attention_mask) emissions self.fc(outputs.last_hidden_state) # (batch, seq_len, num_tags) if labels is not None: loss self.crf.neg_log_likelihood(emissions.transpose(0, 1), labels) return loss else: return self.crf.viterbi_decode(emissions.transpose(0, 1))4.2 关键训练细节Mask处理忽略padding部分的得分计算。标签约束通过转移矩阵限制非法转移如I-PER→B-LOC。学习率BERT层使用较小学习率如2e-5CRF层可适当调大。5. 常见问题与调优技巧5.1 CRF的优缺点优点显式建模标签间依赖提升序列一致性。适合小数据集不易过拟合。缺点解码速度慢于独立分类但实际影响不大。转移矩阵需要领域知识设计约束。5.2 效果调优转移矩阵初始化用标注数据的共现统计初始化。部分标注支持通过掩码处理缺失标签。对抗训练在BERT嵌入层添加对抗扰动。# 示例基于统计初始化转移矩阵 def init_transition_from_counts(counts): # counts[i][j]是标签i→j的共现次数 probs counts / counts.sum(axis1, keepdimsTrue) self.crf.transition.data torch.log(probs 1e-12)通过上述步骤我们实现了一个完整的PyTorch版BERT-CRF模型。实际使用时建议直接使用优化过的开源库如torchcrf但理解底层原理对调试模型至关重要。完整代码示例可在GitHub仓库中找到包含数据预处理和训练循环的实现。