作者:jjyaoao, 欢迎各位佬来AiStudio和俺互关呀!
一、方案介绍
1.1 项目简介
中医文献阅读理解是一个将自然语言处理技术应用于中医药领域的项目。它的目标是开发一个能够读取、理解和回答中医药知识的模型,以便更好地普及和传播中医药知识。
为了实现这个目标,该项目需要建立一个大规模的中医药语料库,并使用自然语言处理技术对语料库进行处理,提取关键信息并建立模型。模型的输入可以是一个中医药相关的问题、模型的输出则是一个与问题相关的答案。
例如,如果输入的问题是”什么是中医证候学?“,则模型的输出可能是“中医证候学是中医药的一个重要理论,它旨在通过观察患者的症状和体征,推断患者所患疾病的特点和发展趋势,为临床治疗提供理论指导。
此外,基于该项目还可以开发更多关于中医药知识的小应用,如中医药问诊系统、中医药辨证论治辅助工具等,以帮助更多人了解和应用中医药知识。
1.2 数据集介绍
本次标注数据源来自中医药领域文本,包括【黄帝内经翻译版】、【名医百科中医篇】、【中成药用药卷】、【慢性病养生保健科普知识】四个主要来源,共标注 13000对(问题、文档、答案),来源于5000篇文档,每篇文档由人工标注产生1~4对(问题, 答案)对。
例子:
{
2 "id": 98,
3 "text": "黄帝道:什麽叫重实?岐伯说:所谓重实,如大热病人,邪气甚热,而脉象又盛满,内外俱实,便叫重实",
4 "annotations": [
5 {
6 "Q": "重实是指什么?",
7 "A": "所谓重实,如大热病人,邪气甚热,而脉象又盛满,内外俱实,便叫重实"
8 },
9 {
10 "Q": "重实之人的脉象是什么样?",
11 "A": "脉象又盛满"
12 }
13 ],
14 "source": "黄帝内经翻译版"
15 }
以Json格式提供,包括:
id: 段落id
text: 段落文本
annotations: 包含(问题、答案)对
Q:问题 A:答案
1.3 技术点介绍
1、构建中医MRC数据集;
2、使用PaddleNLP开发库搭建、训练并调优阅读理解模型;
3、动转静,完成静态图的推理,并用gradio实现可交互的部署。
1.3.1 PaddleNLP
PaddleNLP 是飞桨自然语言处理开发库,具备 易用的文本领域API,多场景的应用示例、和 高性能分布式训练 三大特点,旨在提升飞桨开发者文本领域建模效率,旨在提升开发者在文本领域的开发效率,并提供丰富的NLP应用示例。
-
易用的文本领域API
- 提供丰富的产业级预置任务能力 Taskflow 和全流程的文本领域API:支持丰富中文数据集加载的 Dataset API,可灵活高效地完成数据预处理的 Data API ,预置60+预训练词向量的 Embedding API ,提供100+预训练模型的 Transformer API 等,可大幅提升NLP任务建模的效率。
-
多场景的应用示例
- 覆盖从学术到产业级的NLP应用示例,涵盖NLP基础技术、NLP系统应用以及相关拓展应用。全面基于飞桨核心框架2.0全新API体系开发,为开发者提供飞桨文本领域的最佳实践。
-
高性能分布式训练
- 基于飞桨核心框架领先的自动混合精度优化策略,结合分布式Fleet API,支持4D混合并行策略,可高效地完成大规模预训练模型训练。
-
项目GitHub: github.com/PaddlePaddl…
1.3.2 Roberta阅读理解模型
阅读理解本质是一个答案抽取任务,PaddleNLP对于各种预训练模型已经内置了对于下游任务-答案抽取的Fine-tune网络。
以下项目以百度飞桨模型库中的Roberta模型为例,介绍如何将预训练模型Fine-tune完成答案抽取任务。
答案抽取任务的本质就是根据输入的问题和文章,预测答案在文章中的起始位置和结束位置。基于BERT的答案抽取原理如下图所示:
Roberta模型主要是在BERT基础上做了几点调整:
1)训练时间更长,batch size更大,训练数据更多;
2)移除了next predict loss;
3)训练序列更长;
4)动态调整Masking机制。
5) Byte level BPE RoBERTa is trained with dynamic masking
另外,使用别的类似模型,利用 PaddleNLP 开发库也能很快捷的实现。
paddlenlp.transformers.RobertaForQuestionAnswering()
一行代码即可加载预训练模型BERT用于答案抽取任务的Fine-tune网络。
paddlenlp.transformers.RobertaForQuestionAnswering.from_pretrained()
指定想要使用的模型名称和文本分类的类别数,一行代码完成网络构建。
1.3.3 Gradio算法可视化部署
如何将你的AI算法迅速分享给别人,让对方体验,一直是一件麻烦事儿。
首先大部分人都是在本地跑代码,让别人使用你的模型,以往有这三种方案:
- 上github
- 将代码打包或者封装成docker后,用QQ/百度云/U盘传输
- 学习前后端知识,写个前端界面,买个域名,用flask这样微服务框架快速部署,看情况结合一下内网穿透。 这些方案的问题在于——前两者需要对方会编程会配置环境(还得愿意),我们的分享对象满足这个条件的寥寥无几;后者则需要你这个算法工程师升级成全栈,学习前后端开发,学习成本太高。
总结起来:场景不匹配,需求不契合,费时又费力!
那么有没有更好的解决方案呢?有!它就是我今天要给大家安利的一个python开源库:Gradio。
Gradio是MIT的开源项目,GitHub 2k+ star。
使用gradio,只需在原有的代码中增加几行,就能自动化生成交互式web页面,并支持多种输入输出格式,比如图像分类中的图>>标签,超分辨率中的图>>图等。
同时还支持生成能外部网络访问的链接,能够迅速让你的朋友,同事体验你的算法。
总结起来,它的优势有:
自动生成页面且可交互 改动几行代码就能完成 支持自定义多种输入输出 支持生成可外部访问的链接进行分享
想要了解更多,请见官网gradio
1.4 最终完成效果呈现
接下来进行详解与代码展示
二、详细设计流程展示
2.0 环境配置
本项目基于PaddlePaddle 2.0.2 与 PaddleNLP 2.0.7版本, 关于如何下载此版本可以点击飞桨官网,查看下载方式,更加推荐在Aistudio上一键运行哦,项目链接
# 首先导入实验所需要用到的库包。
# base
import paddlenlp as ppnlp
from utils import prepare_train_features, prepare_validation_features
from functools import partial
from paddlenlp.metrics.squad import squad_evaluate, compute_prediction
import collections
import time
import json
# data preprocess:
from paddlenlp.datasets import load_dataset, MapDataset
from sklearn.model_selection import train_test_split
from paddle.io import Dataset
# Build the dataloader
import paddle
from paddlenlp.data import Stack, Dict, Pad
2.1 方案设计
阅读理解的方案如上图,首先是query表示的是问句,一般是用户的提问,passage表示的是文章,表示的是query的答案要从passage里面抽取出来,query和passage经过数据预处理,得到id形式的输入,然后把query,passage的id形式输入到Roberta模型里面,Roberta模型经过处理会输出答案的位置,输出位置以后就可以得到相应的answer了。
2.2 数据处理
具体的任务定义为:对于一个给定的问题q和一个篇章p,根据篇章内容,给出该问题的答案a。数据集中的每个样本,是一个三元组<q, p, a>,例如:
问题 q: 草菇有什么功效?
篇章 p: 草菇荠菜汤鲜嫩清香、色味搭配,具有清热和脾、益气平肝、降糖降压等功效,是夏季解暑祛热的良食佳品.......
参考答案 a: 草菇荠菜汤鲜嫩清香、色味搭配,具有清热和脾、益气平肝、降糖降压等功效,是夏季解暑祛热的良食佳品
我们本次的数据集是以Json格式提供,包括:
- id: 段落id
- text: 段落文本
- annotations: 包含(问题、答案)对,共有
- Q:问题
- A:答案
将上述数据进行简单地数据清洗以及格式(sqaud格式)转换操作,为了方便读取,具体格式如下:
{
'id': 'xx', 'title': 'xxx',
'context': 'xxxx',
'question': 'xxxxx',
'answers': ['xxxx'],
'answer_starts': [xxx]
}
2.2.1 数据集加载与处理
PaddleNLP已经内置SQuAD,CMRC等中英文阅读理解数据集,使用paddlenlp.datasets.load_dataset()API即可一键加载。本实例加载的是自行装配的中医阅读理解数据集,采用SQuAD数据格式读入,InputFeature使用滑动窗口的方法生成,即一个example可能对应多个InputFeature。
答案抽取任务即根据输入的问题和文章,预测答案在文章中的起始位置和结束位置。
由于文章加问题的文本长度可能大于max_seq_length,答案出现的位置有可能出现在文章最后,所以不能简单的对文章进行截断。
那么对于过长的文章,则采用滑动窗口将文章分成多段,分别与问题组合。再用对应的tokenizer转化为模型可接受的feature。doc_stride参数就是每次滑动的距离。滑动窗口生成InputFeature的过程如下图:
ppnlp.transformers.RobertaTokenizer
调用RobertaTokenizer进行数据处理。
预训练模型Roberta对中文数据的处理是以byte为单位的BPE编码。官方词表包含5w多的byte级别的token。merges.txt中存储了所有的token,而vocab.json则是一个byte到索引的映射,通常频率越高的byte索引越小。所以转换的过程是,先将输入的所有tokens转化为merges.txt中对应的byte,再通过vocab.json中的字典进行byte到索引的转化。
tokenizer的作用是将原始输入文本转化成模型可以接受的输入数据形式。对于Roberta,比如输入的文本是
What's up with the tokenizer?
首先使用merges.txt转化为对应的Byte(类似于标准化的过程)
['What', "'s", 'Ġup', 'Ġwith', 'Ġthe', 'Ġtoken', 'izer', '?']
再通过vocab.json文件存储的映射转化为对应的索引
[ 'What', "'s", 'Ġup', 'Ġwith', 'Ġthe', 'Ġtoken', 'izer', '?']
---- becomes ----
[ 2061, 338, 510, 351, 262, 11241, 7509, 30]
# 更多可选择模型:
# ['bert-base-uncased', 'bert-large-uncased', 'bert-base-multilingual-uncased', 'bert-base-cased', 'bert-base-chinese', 'bert-base-multilingual-cased'
# , 'bert-large-cased', 'bert-wwm-chinese', 'bert-wwm-ext-chinese', 'macbert-base-chinese', 'macbert-large-chinese', 'simbert-base-chinese']
# 定义使用paddleNLP内置的roberta中文预训练模型
MODEL_NAME = 'roberta-wwm-ext-large'
tokenizer = ppnlp.transformers.RobertaTokenizer.from_pretrained(MODEL_NAME)
2.2.2 数据转化
使用load_dataset()API默认读取到的数据集是MapDataset对象,MapDataset是paddle.io.Dataset的功能增强版本。其内置的map()方法适合用来进行批量数据集处理。map()方法传入的是一个用于数据处理的function。
以下是采取的数据转化的用法:
max_seq_length = 512
doc_stride = 128
train_trans_func = partial(prepare_train_features,
max_seq_length=max_seq_length,
doc_stride=doc_stride,
tokenizer=tokenizer)
train_ds.map(train_trans_func, batched=True)
dev_trans_func = partial(prepare_validation_features,
max_seq_length=max_seq_length,
doc_stride=doc_stride,
tokenizer=tokenizer)
dev_ds.map(dev_trans_func, batched=True)
2.2.3 构造Dataloader
使用paddle.io.DataLoader接口多线程异步加载数据。同时使用paddlenlp.data中提供的方法把feature组成batch
# Build the dataloader
batch_size = 8
train_batch_sampler = paddle.io.DistributedBatchSampler(
train_ds, batch_size=batch_size, shuffle=True)
train_batchify_fn = lambda samples, fn=Dict({
"input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),
"token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
"start_positions": Stack(dtype="int64"),
"end_positions": Stack(dtype="int64")
}): fn(samples)
train_data_loader = paddle.io.DataLoader(
dataset=train_ds,
batch_sampler=train_batch_sampler,
collate_fn=train_batchify_fn,
return_list=True)
dev_batch_sampler = paddle.io.BatchSampler(
dev_ds, batch_size=batch_size, shuffle=False)
dev_batchify_fn = lambda samples, fn=Dict({
"input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),
"token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id)
}): fn(samples)
dev_data_loader = paddle.io.DataLoader(
dataset=dev_ds,
batch_sampler=dev_batch_sampler,
collate_fn=dev_batchify_fn,
return_list=True)
2.3 模型训练与策略选择
2.3.1设置Fine-Tune优化策略
适用于ERNIE/BERT这类Transformer模型的学习率为warmup的动态学习率。
# 参数配置
# 训练过程中的最大学习率
learning_rate = 3e-5
# 训练轮次
epochs = 2
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.01
num_training_steps = len(train_data_loader) * epochs
lr_scheduler = ppnlp.transformers.LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_proportion)
# Generate parameter names needed to perform weight decay.
# All bias and LayerNorm parameters are excluded.
decay_params = [
p.name for n, p in model.named_parameters()
if not any(nd in n for nd in ["bias", "norm"])
]
optimizer = paddle.optimizer.AdamW(
learning_rate=lr_scheduler,
parameters=model.parameters(),
weight_decay=weight_decay,
apply_decay_param_fun=lambda x: x in decay_params)
2.3.2 设计loss function
由于BertForQuestionAnswering模型对将BertModel的sequence_output拆开成start_logits和end_logits进行输出,所以阅读理解任务的loss也由start_loss和end_loss组成,我们需要自己定义loss function。对于答案其实位置和结束位置的预测可以分别成两个分类任务。所以设计的loss function如下:
class CrossEntropyLossForSQuAD(paddle.nn.Layer):
def __init__(self):
super(CrossEntropyLossForSQuAD, self).__init__()
def forward(self, y, label):
start_logits, end_logits = y # both shape are [batch_size, seq_len]
start_position, end_position = label
start_position = paddle.unsqueeze(start_position, axis=-1)
end_position = paddle.unsqueeze(end_position, axis=-1)
start_loss = paddle.nn.functional.softmax_with_cross_entropy(
logits=start_logits, label=start_position, soft_label=False)
start_loss = paddle.mean(start_loss)
end_loss = paddle.nn.functional.softmax_with_cross_entropy(
logits=end_logits, label=end_position, soft_label=False)
end_loss = paddle.mean(end_loss)
loss = (start_loss + end_loss) / 2
return loss
2.3.3 模型训练
模型训练的过程通常有以下步骤:
- 从dataloader中取出一个batch data
- 将batch data喂给model,做前向计算
- 将前向计算结果传给损失函数,计算loss。
- loss反向回传,更新梯度。重复以上步骤。
每训练一个epoch时,程序通过evaluate()调用paddlenlp.metric.squad中的squad_evaluate(), compute_predictions()评估当前模型训练的效果,其中:
-
compute_predictions()用于生成可提交的答案;
-
squad_evaluate()用于返回评价指标。
二者适用于所有符合squad数据格式的答案抽取任务。这类任务使用Rouge-L和exact来评估预测的答案和真实答案的相似程度。
三、预测部署
模型训练完成之后接下来我们实现模型的预测部署。虽然训练阶段使用的动态图模式有诸多优点,包括Python风格的编程体验(使用RNN等包含控制流的网络时尤为明显)、友好的debug交互机制等。但Python动态图模式无法更好的满足预测部署阶段的性能要求,同时也限制了部署环境。
静态图是预测部署通常采用的方式。通过静态图中预先定义的网络结构,一方面无需像动态图那样执行开销较大的Python代码;另一方面,预先固定的图结构也为基于图的优化提供了可能,这些能够有效提升预测部署的性能。常用的基于图的优化策略有内存复用和算子融合,这需要预测引擎的支持。下面是算子融合的一个示例(将Transformer Block的FFN中的矩阵乘->加bias->relu激活替换为单个算子):
高性能预测部署需要静态图模型导出和预测引擎两方面的支持,这里分别介绍。
3.1 动转静导出模型
基于静态图的预测部署要求将动态图的模型转换为静态图形式的模型(网络结构和参数权重)。
Paddle静态图形式的模型(由变量和算子构成的网络结构)使用Program来存放,Program的构造可以通过Paddle的静态图模式说明,静态图模式下网络构建执行的各API会将输入输出变量和使用的算子添加到Program中。
3.2 使用推理库预测
获得静态图模型之后,我们使用Paddle Inference进行预测部署。Paddle Inference是飞桨的原生推理库,作用于服务器端和云端,提供高性能的推理能力。
Paddle Inference采用 Predictor 进行预测。Predictor 是一个高性能预测引擎,该引擎通过对计算图的分析,完成对计算图的一系列的优化(如OP的融合、内存/显存的优化、 MKLDNN,TensorRT 等底层加速库的支持等),能够大大提升预测性能。另外Paddle Inference提供了Python、C++、GO等多语言的API,可以根据实际环境需要进行选择,为了便于演示这里使用Python API来完成,其已在安装的Paddle包中集成,直接使用即可。使用 Paddle Inference 开发 Python 预测程序仅需以下步骤:
3.3 gradio进行交互项部署
gradio部署有以下步骤:
- 将模型拷贝到本地,并按照接口要求封装好方法
import gradio as gr
def question_answer(context, question):
pass # Implement your question-answering model here...
gr.Interface(fn=question_answer, inputs=["text", "text"], outputs=["textbox", "text"]).launch(share=True)
-
将用户输入的context, question加载,并利用模型返回answer
-
返回到gradio部署的框内, 进行页面展示
-
生成公开链接
进入public URL验证
四、总结与展望
4.1 模型效果对比
| ernie-2.0-large-en | bert-base-chinese | bert-wwm-ext-chinese | roberta-wwm-ext-large | |
|---|---|---|---|---|
| Epoch | ||||
| 1 | 34.64 | 56.62 | 59.04 | 62.56 |
| 2 | 35.68 | 57.84 | 59.94 | 63.29 |
| 3 | 34.98 | 57.12 | 59.67 | 62.97 |
-
上述表格得分为 F1 值
-
最开始使用ernie2.0-large-en模型,结果发现好像该模型并不是针对于中文的预训练模型,因此导致效果比较差。
-
bert基本模型较为良好,能够基本完成任务。
-
可以发现随着bert基线拓展而成的bert-wwm-ext-chinese模型与改良bert后的roberta-wwm-ext-large模型,能够取得较好的效果,随之付出的代价就是模型的体积变大,并且训练速度变迟缓。
-
由于采用预训练 + 微调的方法,处理数据,因此Epoch过大极易发生过拟合,这也是我们以后应该避免的,尽量做到手工微调。
4.2 项目展望
-
数据:
-
寻找更多优质中医语料数据集,进行简单增强
-
采用回译等数据增强方法,从无到有的构建文本相似数据集
-
-
项目部署:
-
gradio再优化
-
aistudio沙盒打包
-
flask生成api,使用docker打包flask制作小应用
-
-
模型
- 将模型微调的几步Epoch结果保存,进行模型平均操作
- 采用模型Bagging策略,将训练质量好的模型进行融合处理
- 探寻更多优质模型,针对任务,寻找使用更多医疗数据集训练好的与训练模型
-
更多:
- 学习RocketQA等端到端问答模型,加上检索条件,在机器阅读理解基础上,制作完整的基于检索的问答系统,并可为后续学习基于生成的问答模型打下基础(PS:励志做个类似chatgpt的通用问答模型(bushi))