一、训练、评估模型

训练函数基本步骤:
1.构建数据迭代器Dataloader(包括数据处理与构建数据源Dataset)
2.实例化模型
3.实例化损失函数对象
4.实例化优化器对象
5.定义打印日志参数
6.开始训练
6.1 实现外层大循环epoch
    6.2 将模型设置为训练模式
    6.3 内部遍历数据迭代器dataloader
        1)将数据送入模型得到输出结果
        2)计算损失
        3)梯度清零: optimizer.zero_grad()
        4)反向传播(计算梯度): loss.backward()
        5)梯度更新(参数更新): optimizer.step()
        6)打印内部训练日志
    6.4 使用验证集进行模型评估【将模型设置为评估模式】
    6.5 保存模型: torch.save(model.state_dict(), "model_path")
6.6 打印外部训练日志


验证函数基本步骤:
1.定义打印日志参数
2.将模型设置为评估模式
3.内部遍历数据迭代器dataloader
  3.1 将数据送入模型得到输出结果
  3.2 计算损失
  3.3 处理结果
  3.4 统计批次内指标
4.统计整体指标

train.py

import time
import torch
import torch.nn as nn
from sklearn.metrics import precision_score, recall_score, f1_score, classification_report
from tqdm import tqdm

from P03_NER.LSTM_CRF.config import Config
from P03_NER.LSTM_CRF.model.BiLSTM import NERLSTM
from P03_NER.LSTM_CRF.model.BiLSTM_CRF import NERLSTM_CRF
from P03_NER.LSTM_CRF.utils.data_loader import get_data, word2id

conf = Config()


def model2dev(val_dataloader, model, criterion=None):
    '''
    使用验证集,评估模型的效果
    :param val_dataloader: 验证集
    :param model: 需要评估的模型对象
    :param criterion: 损失函数对象【只有BiLSTM模型会用到】
    :return: 评估指标
    '''
    # 1.定义打印日志参数
    avg_loss = 0
    preds = []  # 用来保存非padding位置的预测标签
    golds = []  # 用来保存非padding位置的真实标签
    # 2.将模型设置为评估模式
    model.eval()
    # 3.内部遍历数据迭代器dataloader
    for index, (input_ids, labels, attention_mask) in enumerate(tqdm(val_dataloader)):
        # 3.1 将数据送入模型得到输出结果
        # 将数据放到 GPU
        input_ids = input_ids.to(conf.device)
        labels = labels.to(conf.device)
        attention_mask = attention_mask.to(conf.device)

        # 对模型进行判断
        if conf.model == 'BiLSTM':
            # 将数据送入模型得到输出结果
            output = model(input_ids, attention_mask)
            # print(f'output-->{output.shape}')

            # 3.2 计算损失
            reshape_output = output.view(-1, len(conf.tag2id))
            reshape_labels = labels.view(-1)
            loss = criterion(reshape_output, reshape_labels)
            # 对损失进行累加操作
            avg_loss += loss.item()

            # 3.3 处理结果
            predict = output.argmax(dim=-1).tolist()
            # print(f'predict-->{predict}')
        else:
            # 将数据送入模型得到输出结果
            predict = model(input_ids, attention_mask)
            # print(f'predict-->{predict}')

            # 3.2 计算损失
            loss = model.log_likelihood(input_ids, labels, attention_mask).mean()
            # 对损失进行累加操作
            avg_loss += loss

            # 3.3 处理结果(略)

        # 目的:基于真实样本长度,去获取非padding位置的预测标签和真实标签
        # 1)先获取真实长度
        real_len = (labels != 11).sum(-1).tolist()
        # print(f'real_len-->{real_len}')
        # 2)基于真实长度去获取非padding位置的预测标签
        for index, label in enumerate(predict):
            real_len_label = label[0:real_len[index]]
            # print(f'real_len_label-->{real_len_label}')
            preds.extend(real_len_label)  # 注意:这里需要使用extend()方法,将每个标签添加到列表中
        # 3)基于真实长度去获取非padding位置的真实标签
        for index, label in enumerate(labels.tolist()):
            golds.extend(label[0:real_len[index]])
        # print(f'preds-->{preds}')
        # print(f'golds-->{golds}')

        # 3.4 统计批次内指标
        # break

    # 4.统计整体指标
    avg_loss /= len(val_dataloader)
    precision = precision_score(golds, preds, average='weighted')
    recall = recall_score(golds, preds, average='weighted')
    f1 = f1_score(golds, preds, average='weighted')
    report = classification_report(golds, preds)
    # print(f'precision-->{precision}')
    # print(f'recall-->{recall}')
    # print(f'f1-->{f1}')
    # print(f'report-->{report}')
    return avg_loss, precision, recall, f1, report


def model2train():
    # 1.构建数据迭代器Dataloader(包括数据处理与构建数据源Dataset)
    train_dataloader, val_dataloader = get_data()
    # 2.实例化模型
    models = {'BiLSTM': NERLSTM,
              'BiLSTM_CRF': NERLSTM_CRF}
    model = models[conf.model](conf.embedding_dim, conf.hidden_dim, conf.tag2id, word2id, conf.dropout).to(conf.device)
    print(f'model-->{model}')
    # 3.实例化损失函数对象
    # 忽略索引为11的标签,即[PAD]。这样做是因为,[PAD]标签不需要参与损失的计算
    criterion = nn.CrossEntropyLoss(ignore_index=11)
    # 4.实例化优化器对象
    optimizer = torch.optim.Adam(model.parameters(), lr=conf.lr)
    # 5.定义打印日志参数
    start_time = time.time()

    # 6.开始训练
    best_f1 = -100  # 用来保存最好的模型对应的f1值
    # 6.1 实现外层大循环epoch
    if conf.model == 'BiLSTM':
        for epoch in range(conf.epochs):
            # 6.2 将模型设置为训练模式
            model.train()
            # 6.3 内部遍历数据迭代器dataloader
            for index, (input_ids, labels, attention_mask) in enumerate(tqdm(train_dataloader)):
                # 1)将数据送入模型得到输出结果
                # 将数据放到 GPU
                input_ids = input_ids.to(conf.device)
                labels = labels.to(conf.device)
                attention_mask = attention_mask.to(conf.device)
                # 将数据送入模型得到输出结果
                output = model(input_ids, attention_mask)
                # print(f'output-->{output.shape}')

                # 2)计算损失
                # 在计算损失之前,需要将预测结果的形状转变成(batch_size*seq_len, tag_size),并将标签结果转成(batch_size*seq_len)
                reshape_output = output.view(-1, len(conf.tag2id))
                # print(f'reshape_output-->{reshape_output.shape}')
                reshape_labels = labels.view(-1)
                # print(f'reshape_labels-->{reshape_labels.shape}')
                loss = criterion(reshape_output, reshape_labels)
                # print(f'loss-->{loss}')

                # 3)梯度清零: optimizer.zero_grad()
                optimizer.zero_grad()
                # 4)反向传播(计算梯度): loss.backward()
                loss.backward()
                # 5)梯度更新(参数更新): optimizer.step()
                optimizer.step()

                # 6)打印内部训练日志
                if (index+1) % 50 == 0:
                    print('epoch:%04d,------------loss:%f' % (epoch, loss.item()))
                    # break
            # 6.4 使用验证集进行模型评估【将模型设置为评估模式】
            avg_loss, precision, recall, f1, report = model2dev(val_dataloader, model, criterion)
            # 6.5 保存模型: torch.save(model.state_dict(), "model_path")
            if f1 > best_f1:
                print(f'epoch:{epoch}, avg_loss:{avg_loss}, precision:{precision}, recall:{recall}, f1:{f1}')
                print(f'report-->{report}')
                torch.save(model.state_dict(), "save_model/bilstm_best_f1.pth")
                best_f1 = f1  # 注意:不忘忘记更新best_f1
            # break
    else:
        for epoch in range(conf.epochs):
            # 6.2 将模型设置为训练模式
            model.train()
            # 6.3 内部遍历数据迭代器dataloader
            for index, (input_ids, labels, attention_mask) in enumerate(tqdm(train_dataloader)):
                # 1)将数据送入模型得到输出结果
                # 将数据放到 GPU
                input_ids = input_ids.to(conf.device)
                labels = labels.to(conf.device)
                attention_mask = attention_mask.to(conf.device)

                # 2)计算损失
                # 需要使用mean()方法,将损失求平均
                loss = model.log_likelihood(input_ids, labels, attention_mask).mean()
                # print(f'loss-->{loss}')

                # 3)梯度清零: optimizer.zero_grad()
                optimizer.zero_grad()
                # 4)反向传播(计算梯度): loss.backward()
                loss.backward()
                # 梯度裁剪,作用是防止训练不稳定或梯度爆炸
                torch.nn.utils.clip_grad_norm_(parameters=model.parameters(), max_norm=10)
                # 5)梯度更新(参数更新): optimizer.step()
                optimizer.step()

                # 6)打印内部训练日志
                if (index + 1) % 50 == 0:
                    print('epoch:%04d,------------loss:%f' % (epoch, loss.item()))
                    # break
            # 6.4 使用验证集进行模型评估【将模型设置为评估模式】
            avg_loss, precision, recall, f1, report = model2dev(val_dataloader, model)
            # 6.5 保存模型: torch.save(model.state_dict(), "model_path")
            if f1 > best_f1:
                print(f'epoch:{epoch}, avg_loss:{avg_loss}, precision:{precision}, recall:{recall}, f1:{f1}')
                print(f'report-->{report}')
                torch.save(model.state_dict(), "save_model/bilstm_crf_best_f1.pth")
                best_f1 = f1  # 注意:不忘忘记更新best_f1
            # break
    # 6.6 打印外部训练日志
    print('训练结束,总耗时:%f' % (time.time() - start_time))


if __name__ == '__main__':
    model2train()

使用CRF之后,效果比之前稍微好一些,但是训练成本会提高很多。

二、模型优化

BiLSTM_CRF模型在训练完后,可以做哪些优化来改善模型性能?

1)模型优化

==预训练词向量==:使用预训练的词向量(如Word2Vec、GloVe、FastText)替代随机初始化的词嵌入,可以更好地捕捉词汇语义信息。

自注意力机制:在BiLSTM后加入自注意力层,增强模型对长距离依赖的捕捉能力。

==替换BiLSTM模型==:使用Bert或Bert变体来替换BiLSTM,一般来说是可以获取更好的语义表达。

调整随机失活层:可以在embedding层后添加随机失活层,也可以修改随机失活比例。

2)训练过程优化

  • ==shuffles设置==:注意真正训练时,需要将DataLoader中的shuffle设置为True

  • 梯度裁剪:在反向传播时对梯度进行裁剪,防止梯度爆炸(比如预设最大梯度值为10,然后反向传播计算梯度得到20,那么等比例缩放,得到梯度值为10除以20=0.5,这样可以有效防止梯度爆炸)。

  • 早停机制:监控验证集F1值,若连续多个epoch未提升则提前终止训练(存储多次得到的f1值,如果为提升那么提前终止训练)。

3)训练数据优化

  • 如果训练集和验证集数据分布不同,也就是说使用的是差距很大的样本,会使模型的效果较差,所以可以将==数据打散==后再送到dataloader中(shuffle=True,这样可以防止一种情况:数据存储在多个文件夹下面,不同文件夹之间的数据关联性不高,那么前几个文件夹的数据都用来训练大模型了,后面几个文件夹的数据作为验证集效果不好)

  • 除了这种方式之外,也可以使用==分类采样==的方式。这种方式可以绝对类型上,训练集和验证集的分布是一致的(在每个文件夹里面的数据全部采用8:2的方式划分训练集和验证集,这样训练效果可以更好)。

另外,还有以下方法:

更多数据:收集或标注更多数据,送到模型中进行训练。

实体替换:保留实体边界,随机替换实体内容(如疾病名称、药品名称),提升实体识别泛化能力。

三、模型预测

使用训练好的模型,随机抽取文本进行NER

3.1预测流程

基本步骤:

1.实例化模型
2.加载训练好的模型参数
3.处理数据
4.模型预测
5.结果处理

整体思路:

结果解析思路

提取实体和标签:

 获取标签类型:先获取标签类型,然后把这个类型添加到类型列表entity里面,最后把实体和标签类型添加到实体列表entities里面;

3.2预测代码

import torch

from P03_NER.LSTM_CRF.config import Config
from P03_NER.LSTM_CRF.model.BiLSTM import NERLSTM
from P03_NER.LSTM_CRF.model.BiLSTM_CRF import NERLSTM_CRF
from P03_NER.LSTM_CRF.utils.data_loader import word2id

conf = Config()
id2tag = {v: k for k, v in conf.tag2id.items()}
print(f'id2tag-->{id2tag}')

# 1.实例化模型
models = {'BiLSTM': NERLSTM,
          'BiLSTM_CRF': NERLSTM_CRF}
model = models[conf.model](conf.embedding_dim, conf.hidden_dim, conf.tag2id, word2id, conf.dropout).to(conf.device)
print(f'model-->{model}')

# 2.加载训练好的模型参数
if conf.model == 'BiLSTM':
    model.load_state_dict(torch.load("save_model/bilstm_best_f1.pth", weights_only=True))
else:
    model.load_state_dict(torch.load("save_model/bilstm_crf_best_f1.pth", weights_only=True))


def model2predict(text):
    # 3.处理数据
    ids = []
    for char in text:
        if char not in word2id:
            ids.append(word2id['UNK'])
        else:
            ids.append(word2id[char])
    # 需要给ids加上一个batch_size维度
    ids_tensor = torch.tensor([ids]).to(conf.device)
    # print(f'ids_tensor-->{ids_tensor}')
    attention_mask = (ids_tensor != 0).long()

    # 4.模型预测
    model.eval()
    with torch.no_grad():
        if conf.model == 'BiLSTM':
            # 获取预测结果
            result = model(ids_tensor, attention_mask)
            # print(f'result-->{result}')
            predict = result.argmax(dim=-1).tolist()[0]
            # print(f'predict-->{predict}')
        else:
            # 获取预测结果
            predict = model(ids_tensor, attention_mask)[0]
            # print(f'predict-->{predict}')

        # 5.结果处理
        tags = [id2tag[tag_id] for tag_id in predict]
        # print(f'tags-->{tags}')

        # 将实体从文本中抽取出来,返回一个字典
        chars = [char for char in text]
        result = extract_entities(chars, tags)
        # print(f'result-->{result}')
        return result

def extract_entities(tokens, labels):
    entities = []  # 用来保存所有 (实体类型, 实体)
    entity = []  # 用来保存单个实体
    entity_type = None  # 实体类型

    for token, label in zip(tokens, labels):
        if label.startswith("B-"):  # 实体的开始
            if entity:  # 如果已经有实体,先保存
                entities.append((entity_type, ''.join(entity)))
                entity = []
            entity_type = label.split('-')[1]
            entity.append(token)
        elif label.startswith("I-") and entity:  # 实体的中间或结尾
            entity.append(token)
        else:
            if entity:  # 保存上一个实体
                entities.append((entity_type, ''.join(entity)))
                entity = []

    # 如果最后一个实体没有保存,手动保存
    if entity:
        entities.append((entity_type, ''.join(entity)))

    # print(f'entities-->{entities}')
    return {entity: entity_type for entity_type, entity in entities}



if __name__ == '__main__':
    text = "小明的父亲患有冠心病及糖尿病,无手术外伤史及药物过敏史"
    result = model2predict(text)
    print(f'text-->{text}')
    print(f'result-->{result}')

预测结果:

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐