深度学习项目5_基于BART模型的文本生成任务

深度学习项目5_基于BART模型的文本生成任务 一准备介绍1.1 项目背景介绍在现代医疗体系中医生每天需要撰写大量的电子病历EMR和诊断报告。然而这一过程面临着严峻挑战耗时费力医生需将复杂的检查数据、影像结果和患者主诉转化为规范的文字描述占据了大量诊疗时间。质量参差不齐受疲劳程度和经验差异影响人工撰写的诊断描述可能存在术语不规范、逻辑遗漏或风格不统一的问题。数据价值未释放海量的结构化医疗数据如检验指标、影像标签未能自动转化为高质量的文本知识难以直接用于科研分析或下级医院辅助。随着自然语言生成NLG技术的成熟利用AI辅助生成标准化、高质量的诊断描述已成为智慧医疗的关键突破口。BARTBidirectional and Auto-Regressive Transformers模型凭借其卓越的“理解生成”双重能力成为解决这一问题的理想核心引擎。本项目模块分为数据处理模型训练模型验证结果评估推理预测。2.1 BART介绍BARTBidirectional and Auto-Regressive Transformers由 Facebook AI Research (FAIR) 在 2019 年提出的一种强大的序列到序列 (Seq2Seq)预训练模型。将 BERT 的双向编码能力与 GPT 的自回归解码能力完美结合通过去噪自编码器 (Denoising Autoencoder) 的方式进行预训练。这使得 BART 在文本生成、理解以及两者结合的任务如摘要、翻译上表现卓越。详细知识以后会在另一文章介绍。二数据预处理当我们打开文件的时候里面的数据文件只有test.csv以及train.csv打开train.csv里面有三列第一列是id第二列是计算机断层扫描CT第三列是医生的描述。而打开test.csv发现里面只有两列少的那一列就是医生的描述该列就是给模型预测的。里面的数据全是数字而不是文字是由于这些数据已经进行过脱敏处理。脱敏处理是指移除或者替换病例数据中所有能直接或者间接识别患者个人身份信息的过程。图2-1 训练数据图2-2 测试数据2.1 处理数据集由于官方的数据集没有验证数据集所以要将训练数据集分割为训练数据集和验证数据集使用的方法是pandas数据包中sample采样数据函数将90%的数据作为训练数据集10%的数据作为验证数据集。代码实现#处理数据的文件 import pandas as pd #处理表格数据 pre_train_file data/train.csv train_df pd.read_csv(pre_train_file,headerNone,names[id,input,tgt]) #读入数据 print(train_df.head()) # 划分训练集和验证集 # 使用sample方法frac采样比例、random_state随机种子、axis轴 train_data train_df.sample(frac0.9, random_state0, axis0) #采样0.9的比例 val_data train_df[~train_df.index.isin(train_data.index)] #train_data.index是取到的数据的下标train_df.index是全部数据的下标isin包含~取反。可以看到结果有2000条数据 # 将数据集保存为csv文件 train_data.to_csv(data/pro_train_data.csv, indexFalse,headerFalse) val_data.to_csv(data/pro_val_data.csv, indexFalse,headerFalse)2.2 词表处理在处理数据集中的数字的时候要将数字转化为模型可以理解的输入ids但是官方的数据集中的数据可能与我们自己的词表vocab不对应即有些数字不在词表中。那么我们就需要处理一下自己的词表。词表对于未见过的词的处理思路有以下三种1直接数字当id将数据集中出现的每一个数字直接当成一个唯一的id。方法简单干脆但是无法保证数据集中的数据代表的含义与词表进行一一对应即同样的数字在词表中的表达和自己想要的表达不一致导致模型训练效果欠佳。2直接加字将数据集中出现的词表中没有的数字作为新的词汇加入词表增大词表。可以保证数据集中所有数字都被识别到但是若数据集中数字变化大、范围广、没见过的数字居多会导致词表过大引入噪声使模型胡言乱语。3重新制作词表将数据集中实际出现的数据加入到词表中重新建一个专属的词表。可以减少词表的大小确保数据集中的数字可以全被模型处理。这就需要对数据集进行分析确定哪些数字是实际需要的然后根据这些数字重新构建词表。综合上述情况选择第三种方法它只包含实际的数字避免不必要的复杂性更好平衡了词表大小和模型的泛化能力。代码实现args parse_args() #设置 字典 属性类 config {} # 1、读数据 def load_data(path): # 打开数据文件20000行数据 with open(path, r, encodingutf-8) as f: lines f.readlines() datas [] # 取出一行每行两个, for line in lines: line line.strip().split(,) #line.strip()去掉换行符.split(,)按逗号分隔 if len(line) 3: # 训练集 text, target line[1].split( ), line[2].split( ) #line[?].split( )进一步拆分逐字提取 datas.append(text target) #提取出来后加入datas中 else: text line[1].split( ) datas.append(text) return datas train_data load_data(./data/train.csv) # 2、统计所有出现过的数字 token2count Counter() #计数工具 哈希表 for i in train_data: token2count.update(i) #不需要知道原理(调试看见统计了不重复出现的数字个数以及每个数字出现了多少次) # 把数字从count中取出来变成列表 tail [] ct 0 #阈值 for k, v in token2count.items(): if v ct: #超过阈值就加入列表 tail.append(k) tail.sort() vocab tail # 3、处理词表建立自己的词表 vocab.insert(0,[PAD]) vocab.insert(100,[UNK]) vocab.insert(101,[CLS]) vocab.insert(102,[SEP]) vocab.insert(103,[MASK]) vocab.insert(104,[EOS]) new_vocabs vocab # 4、保存新的词表 with open(args.pre_model_path/vocab.txt, w, encodingutf-8) as f: #词表在mybart_base_chinese下面 for v in new_vocabs: f.write(f{v}\n) #保存 # 4、模型部分为什么词表变了模型就要变 model BartForConditionalGeneration.from_pretrained(args.pre_model_path) #原模型Embedding(1297, 768)表示词汇表大小为 1297lm_head(768, 1297) model.resize_token_embeddings(len(new_vocabs)) #新模型Embedding(51440, 768)表示词汇表大小为 51440lm_head(768, 51440) state_dict model.state_dict() torch.save(state_dict, args.pre_model_path/pytorch_model.bin) #保存新模型 bartconfig BartConfig.from_pretrained(args.pre_model_path) #保存config为什么也要因为有词表的长度设置 bartconfig.vocab_size len(new_vocabs) bartconfig.save_pretrained(args.pre_model_path) #新config的vocab_size从1297变成了51440三自监督预训练和微调3.1 预训练由于大型的预训练模型是通过大量网络数据训练而来的但该模型没有见过我们的数据集所以为了让模型可以为我所用在使用模型之前要用我们的数据集进行预训练。而预训练之前要考虑清楚两个问题1预训练的数据从哪里来2预训练的数据哪些可以用哪些不可以用1已保存的脱敏处理的医学数据集。2训练集的xy特征标签。验证集的x。测试集的x。不能用验证集的y若用了验证集的y在后续验证测试的时候相当于提前知道答案对模型的能力会产生影响。解决上述后就可以对模型进行预训练从三个模块进行代码实现1. 数据# 数据部分 all_data loadData(args.data_path) train_MLM_data MLM_Data(all_data, args) train_dataloader DataLoader(train_MLM_data, batch_sizeargs.batch_size, shuffleTrue,collate_fntrain_MLM_data.collate) #创建了训练数据集2. 模型model preModel(args) # 加载预训练模型 optimizer, scheduler build_optimizer(args, model) # 优化器设置学习率调整3. 训练# 开始训练了找前向传播和梯度回传在哪里 for epoch in range(args.max_epochs): for batch in train_dataloader: model.train() loss model(batch) #这里batch里面就是数据调试可以看到是长度为4的list loss loss.mean() #多卡训练取均值 loss.backward() #loss回传 optimizer.step() #优化器更新 optimizer.zero_grad() #优化器清零 scheduler.step() #学习率调整 step 1 if step % args.print_steps 0: time_per_step (time.time() - start_time) / max(1, step) remaining_time time_per_step * (num_total_steps - step) remaining_time time.strftime(%H:%M:%S, time.gmtime(remaining_time)) logging.info(fEpoch {epoch} step {step} eta {remaining_time}: loss {loss:.3f}) logging.info(fVAL_Epoch {epoch} step {step}: loss {loss:.3f}) # 不同2预训练不验证并且模型经过一些轮次就保存一次不是保存最优模型 if epoch % 5 0: torch.save({epoch: epoch, model_state_dict: model.module.state_dict()}, f{args.savedmodel_path}/lr{args.learning_rate}epoch{epoch}loss{loss:.3f}pre_model.bin)3.2 微调在预训练的基础上进行的利用特定任务的小规模标注数据对模型的参数进行进一步的调整和优化使其更好地适应具体的下游任务。1. 数据# 1. load data train_dataloader, val_dataloader create_dataloaders(args) #加载数据2. 模型model myModel(args) #是否使用预训练模型如果还没有预训练就设为false use_pre False #已经训练过了就设为True if use_pre: #加载预训练过的模型 print(use_pre) checkpoint torch.load(args.my_pre_model_path, map_locationcpu) new_KEY model.load_state_dict(checkpoint[model_state_dict],strictTrue) #不同1strictTrue表示在加载模型权重时要求模型的结构与预训练模型的结构完全一致 optimizer, scheduler build_optimizer(args, model) model model.to(args.device)3. 训练#进入训练 model.train() #------------------------------- # loss, results validate(model, val_dataloader) # 3. training step 0 best_score args.best_score #评估指标类似分类任务里面的准确率 # 开始训练了找前向传播和梯度回传在哪里 for epoch in range(args.max_epochs): for (source, targets) in tqdm(train_dataloader): #读数据 source source.cuda() #不同2将输入数据移动到 GPU targets targets.cuda() # 训练模式 model.train() pred model(source[:, :args. input_l], targets[:, :args.output_l]) #得到预测值source[:, :args. input_l]的第一个:是样本数第二个:是输入长度不能超过input_l loss CE(pred[:, :-1], targets[:, 1:]) #求losstargets里面去掉第一个(调试可以看到每个target第一个都是101这是之前补的所以要去掉)pred里面去掉最后一个(因为target和pred的长度要一致而且最后一个一般都是padding这种所以去掉最后一个) loss loss.mean() #多卡训练取均值 loss.backward() #loss回传 optimizer.step() model.zero_grad() scheduler.step() step 1 # 验证 if epoch % 1 0: #恒成立每一轮都要做验证 # cider_score cider_score validate(model, val_dataloader, args) logging.info(fEpoch {epoch} step {step}: loss {loss:.3f}, cider_score {cider_score}) if cider_score best_score: #不同3注重验证过程中的 cider_score并根据验证结果保存模型 best_score cider_score torch.save({epoch: epoch, model_state_dict: model.state_dict()}, f{args.savedmodel_path}/model_epoch_{epoch}_cider_score_{cider_score}.bin)四结果评估在文本生成的任务中有着许多的评估指标如BLUEROUCE等。一般文本生成都用 ROUGE 或 BLEU但是它们主要看 n-gram 的重合度容易受措辞差异影响。而由于医疗诊断描述和图像描述Image Captioning非常像都是根据输入信息检查单/图片生成一段描述性文字而不是开放式的对话或创作。故选择CiderD作为评估指标。4.1 CiderD简介CiderDConsensus-based Image Description Evaluation with Diversity作为一种图像描述生成质量的指标用来评价文本相似度。若真实文本和预测文本相似度越高说明模型性能越好。CiderD_scorer用来计算分数分数越高说明预测文本越接近真实文本。CiderD是在CiderConsensus-based Image Description Evaluation的基础上改进而来原始的 Cider 在某些情况下如生成极短或极长句子时可能不稳定而CiderD主要增加了长度惩罚Damping和更严格的标准化处理使其与人类评分的相关性更高。代码实现validate()函数比较预测文本和真实文本的相似度。# 1. 初始化 CIDEr-D 评分器对象 # 参数解释 # dfcorpus: 指定用于计算逆文档频率 (IDF) 的词频统计文件路径或预定义数据集名称。 # CIDEr 的核心在于使用 TF-IDF weighting这里的 corpus 通常指整个训练集的语料库 # 用来判断哪些词是常见的权重低哪些词是独特的权重高。 # sigma15: CIDEr-D 中的 D 代表 Deviation (标准差)。 # 这个参数用于高斯核函数用来惩罚那些长度与参考描述长度差异过大的预测描述。 # sigma15 是论文中的默认推荐值表示允许的长度偏差范围。 CiderD_scorer CiderD(dfcorpus, sigma15) # 2. 计算得分 # 参数解释 # gts (Ground Truths): 真实标签列表。格式通常为字典 {image_id: [ref_sent1, ref_sent2, ...]} # 即每张图片对应多个人类编写的参考描述。 # res (Results): 模型生成的预测结果列表。格式通常为字典 {image_id: [pred_sent]} # 即每张图片对应模型生成的一条描述。 # 返回值解释 # cider_score: 所有样本的平均 CIDEr 得分 (标量)用于衡量模型整体性能。 # cider_scores: 每个样本单独的 CIDEr 得分列表 (向量)可用于分析具体某张图的生成质量。 cider_score, cider_scores CiderD_scorer.compute_score(gts, res)train_and_validate()函数此处调用了validate()函数获取cider_score通过cider_score的值来判断是否保存当前模型。# cider_score 执行验证步骤 cider_score validate(model, val_dataloader, args) #记录训练日志 logging.info(fEpoch {epoch} step {step}: loss {loss:.3f}, cider_score {cider_score}) if cider_score best_score: #不同3注重验证过程中的 cider_score并根据验证结果保存模型 #更新历史最佳分数 best_score cider_score # 4. 保存最佳模型权重 torch.save({epoch: epoch, model_state_dict: model.state_dict()}, f{args.savedmodel_path}/model_epoch_{epoch}_cider_score_{cider_score}.bin)五模型推理预测主要利用已经预训练好的模型对测试集进行预测由此并不需要模型的训练以及参数的更新。相对而言任务会更加轻松点。过程中只需要加载模型加载测试集将测试集输入模型得到预测结果并保存下来即可。代码实现5.1 数据和模型加载# 导入必要的库和自定义模块 from tqdm import tqdm # 导入进度条工具用于在循环中显示处理进度 import csv # 导入 CSV 文件处理模块用于写入结果 from model_utils.utils import to_device, array2str # 从自定义工具包导入to_device(设备迁移工具), array2str(数组转字符串工具) from model_utils.models import myModel # 从自定义模型包导入定义好的模型结构 myModel from model_utils.data import create_dataloaders # 从自定义数据包导入数据加载器创建函数 import torch # 导入 PyTorch 深度学习框架 from model_utils.config import parse_args # 从自定义配置包导入参数解析函数 def inference(args): 执行模型推理的主函数 :param args: 包含所有配置参数的对象如模型路径、数据路径、输出路径等 # 创建测试集的数据加载器 (DataLoader) # testTrue 表示加载的是测试数据集通常意味着不进行数据增强或不打乱顺序 test_loader create_dataloaders(args, testTrue) # 实例化模型对象 # 根据 args 中的配置初始化网络结构此时模型权重是随机初始化的 model myModel(args) # 打印当前要加载的模型检查点文件路径方便调试确认 print(args.ckpt_file) # 加载模型权重文件 # map_locationcpu 确保即使在没有 GPU 的环境下也能加载模型后续会再移到 GPU checkpoint torch.load(args.ckpt_file, map_locationcpu) # 将保存的权重加载到模型中 # strictFalse 允许模型结构与保存的权重不完全匹配例如某些层被忽略或新增提高兼容性 model.load_state_dict(checkpoint[model_state_dict], strictFalse) # 将模型移动到指定的 GPU 设备 (cuda:0) 以加速计算 model.to(cuda:0) # 将模型设置为评估模式 (Evaluation Mode) # 这会关闭 Dropout 和 BatchNorm 的训练行为固定参数仅进行前向传播 model.eval()5.2 模型推理预测# 这里是在评估模式下运行不会计算梯度也不会更新模型参数纯粹是为了获取预测结果。 # 打开输出 CSV 文件准备写入结果 # w 表示写入模式覆盖原有内容newline 防止在 Windows 下出现空行 fp open(args.test_output_csv, w, newline) # 创建 CSV 写入对象 writer csv.writer(fp) # 初始化全局计数器用于给每一行预测结果编号 tot 0 # 遍历测试数据加载器 # tqdm 包裹后会在命令行显示一个带有进度条和预计剩余时间的循环 for source in tqdm(test_loader): # 将输入数据 (source) 移动到 GPU (cuda:0) 上 source to_device(source, cuda:0) # 执行模型前向传播推理 # 在 eval 模式下输入数据经过网络计算得到预测输出 pred # 此时不计算损失不反向传播 pred model(source) # 将预测结果从 GPU 张量转换回 CPU 上的 NumPy 数组以便后续处理或保存 pred pred.cpu().numpy() # 遍历当前批次 (batch) 中的每一个样本 for i in range(pred.shape[0]): # 写入一行结果到 CSV 文件 # 格式为[全局序号, 转换后的预测字符串] # pred[i][2:] 表示跳过前两个元素可能是坐标或其他辅助信息只取有效预测部分 # array2str 函数负责将数值数组格式化为带空格的字符串 writer.writerow([tot, array2str(pred[i][2:], args)]) # 全局计数器加 1 tot 1 # 关闭文件确保数据完全写入磁盘 fp.close() if __name__ __main__: # 解析命令行参数或配置文件获取运行所需的所有超参数和路径 args parse_args() # 调用推理函数开始执行测试/生成任务 inference(args)这部分代码做模型推理的是 pred model(source)其余都是些关于数据文件模型的处理。5.3 结果图5-1 预测结果