知识图谱(BILSTM+CRF项目完整实现、训练结果优化方向(面试))【第八章】
一、训练、评估模型
训练函数基本步骤:
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}')
预测结果:

更多推荐


所有评论(0)