一、模型结构设计
NER本质是序列标注问题——为输入序列中的每一个Token预测一个对应的标签。
1. Token Embedding 层
token_ids 转成词向量。
使用nn.embedding层,属于静态词向量。
2. 动态特征提取层
作用: 让模型理解上下文,生成包含上下文特征的动态词向量。 因为静态词向量无法理解上下文。
实现: RNN机器变体(LSTM、GRU)处理序列数据经典选择。也可以使用更强大的模型如BERT来作为特征提取器。这里使用Bi-GRU(双向GRU)
3. 分类决策层
- 作用: 每个token预测最终实体标签
- 实现: 全连接层,encoder输出的动态词向量从hidden_size维度映射到num_classes,输出token所在变迁上的置信度得分。
二、构建Pytorch模型
2.1 变长序列处理
双向RNN,因为每条数据长度不一样,pad不应该参与反向传播。
Pytorch 提供了工具——torch.nn.utils.rnn.pack_padded_sequence。
pack_padded_sequence —— 封装的PackedSequence对象,记录了数据真实长度
pad_packed_sequecen —— 统一长度,解压成带有填充的、规整的Tensor.
2.2 代码实现
- 开启双向: bidirectional=True
- 增加特征融合层: 双向GRU输出为hidden_size*2 -> 经过线性层转为hidden_size
- 继承pack/pad
class BIGRUNerModel(nn.Module):
def __init__(self, vocab_size, hidden_size, num_tags, num_gru_layers):
super(BIGRUNerModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, hidden_size)
self.gru_layers = nn.ModuleList() # 模块组件
# 多层GRU层
for i in range(num_gru_layers):
self.gru_layers.append(
nn.GRU(input_size= hidden_size,
hidden_size=hidden_size,
batch_first=True,
bidirectional=True))
self.fc = nn.Linear(hidden_size*2, hidden_size)
self.classifier = nn.Linear(hidden_size, num_tags)
def forward(self, token_ids, attention_mask=None): # token_ids [batch_size, seq_len]
# 1. 计算真实长度
lengths = attention_mask.sum(dim=1).cpu() # 计算真实长度
# 获取词向量
# [batch_size, seq_len] -> [batch_size, seq_len, hidden_size]
embedded_text = self.embedding(token_ids)
# 3. 打包序列
current_packed_input = rnn.pack_padded_sequence(
embedded_text, lengths, batch_first=True, enforce_sorted=False)
# 4. 循环通过GRU层
for gru_layer in self.gru_layers:
# GRU 输出
packed_output, _ = gru_layer(current_packed_input) # output, h_n
# (经过gru之后的输出)解包以进行后续操作,并制定total_length
output, _ = rnn.pad_packed_sequence(
packed_output, batch_first=True, total_length=token_ids.shape[1])
features = self.fc(output)
# current也就是没经过gru的输出
input_padded, _ = rnn.pad_packed_sequence(
current_packed_input, batch_first=True, total_length=token_ids.shape[1])
# 残差连接
current_input = features + input_padded
# 重新打包作为下一层输入
current_packed_input = rnn.pack_padded_sequence(
current_input, lengths, batch_first=True, enforce_sorted=False)
final_output, _= rnn.pad_packed_sequence(
current_packed_input, batch_first=True, total_length=token_ids.shape[1])
logits = self.classifier(final_output)
return logits
三、 组件构建与训练封装
Trainer: 执行标准的训练和评估循环。不关心模型怎么构建的,也不关心数据怎么加载的
组件由外部创建并注入:模型、优化器、数据加载器等所有必要的组件都在外部呗创建好,然后像零件一样被注入到Trainer的构造函数中。
3.1 搭建Trainer骨架
# src/trainer/trainer.py
import torch
import os
class Trainer:
def __init__(self, model, optimizer, loss_fn, train_loader, dev_loader=None,
eval_metric_fn=None, output_dir=None, device='cpu'):
"""
初始化训练器。
Args:
model: PyTorch 模型。
optimizer: 优化器。
loss_fn: 损失函数。
train_loader: 训练数据加载器。
dev_loader: 验证数据加载器。
eval_metric_fn: 评估函数。
output_dir: 模型输出目录。
device: 训练设备。
"""
self.model = model.to(device)
self.optimizer = optimizer
self.loss_fn = loss_fn
self.train_loader = train_loader
self.dev_loader = dev_loader
self.eval_metric_fn = eval_metric_fn
self.output_dir = output_dir
self.device = torch.device(device)
if self.output_dir:
os.makedirs(self.output_dir, exist_ok=True)
def fit(self, epochs):
"""
训练的主入口,负责整个训练流程的调度。
"""
pass
def _train_one_epoch(self):
"""封装一个 epoch 的训练逻辑。"""
pass
def _train_step(self, batch):
"""封装一个训练步骤的逻辑(前向、损失、反向)。"""
pass
def _evaluate(self):
"""封装评估逻辑。"""
pass
def _evaluation_step(self, batch):
"""封装一个评估步骤的逻辑(前向、损失)。"""
pass
def _save_checkpoint(self, is_best=False):
"""封装模型保存逻辑。"""
pass
3.2 引入配置类管理参数
- 路径参数: 训练集验证集位置,词汇表位置,模型输出位置等
- 训练参数:batch_size, epochs, learning_rate等
3.3 完善trainer 类
3.4 实现模型组件
- 创建模型目录
- 定义模型基类BaseNerNetwork
- 实现具体NER模型
3.5 实现数据加载组件
- 创建NerDataset
- 创建DataLoader
3.6 实现分词器组件
- 定义分词器基类
- 实现字符级分词器
- 创建词汇表管理器
- 优化分词器: normalize_text等
3.7 实现评估指标组件
判断每个Token分类是否准确是不够的,需要关心能够准确地、完整地抽取出命名实体。 需要计算实体级别指标: 准确率、召回率和F1值
指标流程:
- 解码: 将模型预测出的标签转为实体片段列表
- 对比: 预测出的实体列表与真实实体列表比较
- 计算:
- TP(True Positive: 预测和真实实体完全匹配
- FP(False Positives): 预测出的、但实际上不存在的实体数量
- FN(Flase Negatives): 真实存在、但模型未预测出的实体数量
- Precision = TP/(TP+FP) (注:正确/正确+本来不存在的)
- Recall = TP/(TP+FN) (注 正确/ 正确+没预测出来的)
- F1 = 2(PrecisionRecall)/(Precision+Recall)
3.8 组装所有组件
总结:
架构设计:
- Vocabulary类: 用于将token转为id
- Tokenizer类: 基类, CharTokenizer: 用于实现分词,并调用vocabulary类,实现文本转token_ids
- NerDataSet: 处理单条数据,返回 token_ids和label_ids. 其中调用tokenizer实现数据转换。
- NerDataloader: 实现Collate_batch处理pad问题, 使用torch的dataloader处理批量数据。
- NerModel: 基于bigru实现,输出进行分类,类别为带标签的实体名。
- 评估组件:根据评估结果和实际label进行对比,计算TP / FP / FN,P / R / F1(含零保护)
- Trainer: 训练分为几部分:
fit(self, epochs)方法: 处理完整所有训练周期: 训练,评估,保存模型
_train_one_epoch(self): 单个训练周期,返回平均loss
_train_step(self, batch): 执行单个训练步骤, 包括数据,前向,损失,反向
_evaluate(self): 评估, 禁用梯度计算下,收集结果,并调用评估函数计算指标,返回metrics.
_evaluation_step(self, batch): 单个评估步骤(前向,损失)
_save_checkpoint(self, is_best): 保存模型,包括最近的和最佳的模型