base-llm 2.2.3 模型构建、训练与推理

一、模型结构设计

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 代码实现

  1. 开启双向: bidirectional=True
  2. 增加特征融合层: 双向GRU输出为hidden_size*2 -> 经过线性层转为hidden_size
  3. 继承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 实现模型组件

  1. 创建模型目录
  2. 定义模型基类BaseNerNetwork
  3. 实现具体NER模型

3.5 实现数据加载组件

  1. 创建NerDataset
  2. 创建DataLoader

3.6 实现分词器组件

  1. 定义分词器基类
  2. 实现字符级分词器
  3. 创建词汇表管理器
  4. 优化分词器: normalize_text等

3.7 实现评估指标组件

判断每个Token分类是否准确是不够的,需要关心能够准确地、完整地抽取出命名实体。 需要计算实体级别指标: 准确率、召回率和F1值

指标流程:

  1. 解码: 将模型预测出的标签转为实体片段列表
  2. 对比: 预测出的实体列表与真实实体列表比较
  3. 计算:
    • TP(True Positive: 预测和真实实体完全匹配
    • FP(False Positives): 预测出的、但实际上不存在的实体数量
    • FN(Flase Negatives): 真实存在、但模型未预测出的实体数量
    • Precision = TP/(TP+FP) (注:正确/正确+本来不存在的)
    • Recall = TP/(TP+FN) (注 正确/ 正确+没预测出来的)
    • F1 = 2(PrecisionRecall)/(Precision+Recall)

3.8 组装所有组件

总结:

架构设计:

  1. Vocabulary类: 用于将token转为id
  2. Tokenizer类: 基类, CharTokenizer: 用于实现分词,并调用vocabulary类,实现文本转token_ids
  3. NerDataSet: 处理单条数据,返回 token_ids和label_ids. 其中调用tokenizer实现数据转换。
  4. NerDataloader: 实现Collate_batch处理pad问题, 使用torch的dataloader处理批量数据。
  5. NerModel: 基于bigru实现,输出进行分类,类别为带标签的实体名。
  6. 评估组件:根据评估结果和实际label进行对比,计算TP / FP / FN,P / R / F1(含零保护)
  7. 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): 保存模型,包括最近的和最佳的模型
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容