知识图谱(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}')预测结果:
