基于 BERT 模型的机器阅读理解
1. 案例概述
机器阅读理解是自然语言处理中的一个重要的任务,最常见的有单篇章的抽取式阅读理
解。机器阅读理解的应用范围很广,比如客服机器人,通过文字或者语音与用户进行沟通交
流,然后获取相关的信息并提供准确可靠的回答。搜索引擎中精确返回用户所给定问题的答
案。在医疗领域中自动阅读病人的资料来找到相应的病因。在教育领域中,利用阅读理解模
型自动为学生的作文给出改进意见等等。
图 1.1 阅读理解任务的设计方案
阅读理解的方案如上图,首先是 query 表示的是问句,一般是用户的提问,passage 表
示的是文章,表示的是 query 的答案要从 passage 里面抽取出来,query 和 passage 经过数
据预处理,得到 id 形式的输入,然后把 query,passage 的 id 形式输入到 BERT 模型里面,
BERT 模型经过处理会输出答案的位置,输出位置以后就可以得到相应的答案了。
具体的任务定义为:对于一个给定的问题 q 和一个篇章 p,根据篇章内容,给出该问题
的答案 a。数据集中的每个样本,是一个三元组<q, p, a>,例如:
问题 q: 乔丹打了多少个赛季
篇章 p: 迈克尔.乔丹在 NBA 打了 15 个赛季。他在 84 年进入 nba,期间在 1993 年 10
月 6 日第一次退役改打棒球,95 年 3 月 18 日重新回归,在 99 年 1 月 13 日第二次退役,后
于 2001 年 10 月 31 日复出,在 03 年最终退役…
参考答案 a: [‘15 个’,‘15 个赛季’]
阅读理解模型的鲁棒性是衡量该技术能否在实际应用中大规模落地的重要指标之一。随
着当前技术的进步,模型虽然能够在一些阅读理解测试集上取得较好的性能,但在实际应用
中,这些模型所表现出的鲁棒性仍然难以令人满意。本示例使用的 DuReader-robust 数据
集作为首个关注阅读理解模型鲁棒性的中文数据集,旨在考察模型在真实应用场景中的过敏
感性、过稳定性以及泛化能力等问题。
2. 数据处理
首先导入实验所需要用到的库包。(utils、squad 中涉及到的函数请见附录)
1. import os
2. import torch
3. import torch.nn as nn
4. import torch.nn.functional as F
5. import torch.nn.init as I
6. from torch.nn.utils.rnn import pad_sequence
7. import torch.optim as optim
8. import torch.utils.data
9.
10. from torch.utils.data import RandomSampler,DataLoader,SequentialSampl
er
11.
12.
13. import matplotlib.pyplot as plt
14. import transformers
15. from transformers import BertTokenizerFast,BertForQuestionAnswering,g
et_linear_schedule_with_warmup
16.
17. from utils import prepare_train_features, prepare_validation_features
18. from squad import compute_prediction,squad_evaluate
19. from functools import partial
20.
21. import collections
22. import time
2.1. 数据集加载
对于 PaddleNLP,其中已经内置 SQuAD,CMRC 等中英文阅读理解数据集,可通过调
用函数完成加载。在本案例中我们使用 DuReaderRobust 中文阅读理解数据集,可通过公开
链接完成数据集下载
1
。
由于 DuReaderRobust 数据集采用 SQuAD 数据格式,InputFeature 使用滑动窗口的方
法生成,即一个 example 可能对应多个 InputFeature。
答案抽取任务即根据输入的问题和文章,预测答案在文章中的起始位置和结束位置。由
于文章加问题的文本长度可能大于 max_seq_length,答案出现的位置有可能出现在文章最
后,所以不能简单的对文章进行截断。所以对于过长的文章,则采用滑动窗口将文章分成多
段,分别与问题组合。再用对应的 tokenizer 转化为模型可接受的 feature。doc_stride 参数
就是每次滑动的距离。滑动窗口生成 InputFeature 的过程如下图:
图 2.1 滑动窗口生成 InputFeature 示意图
本次实验使用的数据集为 json 格式的数据集,数据文件均存储在 data 文件夹下,读取
数据集的代码示例如下:
1. def load_data(filepath):
2.
3. '''''
4. json 数据的处理
5. '''
6.
7. train_path = os.path.join(filepath, 'train.json')
8. dev_path = os.path.join(filepath, 'dev.json')
9.
10. train_file = open(train_path, 'r',encoding='utf-8')
11. tr_content = train_file.read()
12. tr_ds = json.loads(tr_content)
13. tr_ds = tr_ds['data'] # a list
14.
15. dev_path = open(dev_path, 'r',encoding='utf-8')
16. dev_content = dev_path.read()
17. dev_ds = json.loads(dev_content)
18. dev_ds = dev_ds['data'] # a list
19.
20.
1
dureader_robust - 飞桨 AI Studio (baidu.com)
21. return tr_ds,dev_ds
2.2. 数据格式处理
源数据集的结构比较复杂为方便后续的处理,首先将读取到的数据转化为列表,列表中
的每个元素为一个字典格式数据,代表着一个实例,对数据进行处理的代码示例如下:
1. def extract_data(data):
2. #源数据结构复杂,重新构建一个结构简单的字典列表,方便后续处理 train 和
dev 数据结构基本一致,可以直接调用相同函数
3.
4. #需要注意的是 dev 数据中存在一个 question 多个 answer 的情况
5. results = []
6. for article in data:
7. paragraphs = article['paragraphs']
8. for paragraph in paragraphs:
9. context = paragraph['context']
10. qas = paragraph['qas']
11. for q in qas:
12. question = q['question']
13. id = q['id']
14. answers = q['answers']
15. text_temp = []
16. answer_start_temp = []
17. for answer in answers:
18. text = answer['text']
19. text_temp.append(text)
20. answer_start = answer['answer_start']
21. answer_start_temp.append(answer_start)
22. results.append({'question':question, 'id':id,'context
':context,'answers':text_temp,'answer_starts':answer_start_temp})
23.
24. return results
2.3. 将数据转换为模型可以接收的形式
首先调用 BertTokenizerFast 进行数据处理。预训练模型 Bert 对中文数据的处理是以字
为单位。在本案例中,我们选择调用 transformers 库中已经内置的 tokenizer,指定想要使
用的模型名字即可加载对应的 tokenizer,将原始输入文本转化成模型可以接受的输入数据
形式。具体的调用方法如下(本案例中选择使用的是 bert-base-chinese):
1. #调用 BertTokenizer 进行数据处理
2. #tokenizer 的作用是将原始输入文本转化成模型可以接受的输入数据形式。
3. def getTokenizer(model_name):
4. tokenizer = BertTokenizerFast.from_pretrained(model_name)# 使用
'bert-base-chinese'
5. #这里使用 BertTokennizerFast 的原因是为了使用 return_offsets_mapping
参数 使用 BertTokennizer 会报错
6. return tokenizer
利用已经获取的 tokenizer 对数据进行进一步处理:
1. def trans_features(data,tokenizer,tr_or_dev):
2. '''''
3. 数据集中的 example 将会被转换成了模型可以接收的 feature,包括
input_ids、token_type_ids、答案的起始位置等信息。
4. 其中:
5. input_ids: 表示输入文本的 token ID。
6. token_type_ids: 表示对应的 token 属于输入的问题还是答案。(Transformer
类预训练模型支持单句以及句对输入)。
7. overflow_to_sample: feature 对应的 example 的编号。
8. offset_mapping: 每个 token 的起始字符和结束字符在原文中对应的 index(用
于生成答案文本)。
9. start_positions: 答案在这个 feature 中的开始位置。
10. end_positions: 答案在这个 feature 中的结束位置。
11. '''
12. max_seq_length = 512
13. doc_stride = 128
14.
15. train_trans_func = partial(prepare_train_features,
16. max_seq_length=max_seq_length,
17. doc_stride=doc_stride,
18. tokenizer=tokenizer)
19.
20. dev_trans_func = partial(prepare_validation_features,
21. max_seq_length=max_seq_length,
22. doc_stride=doc_stride,
23. tokenizer=tokenizer)
24.
25. if tr_or_dev == 'train':
26. data = map(data,train_trans_func)
27. elif tr_or_dev == 'dev':
28. data = map(data,dev_trans_func)
29.
30. return data