文章目录
1、介绍
MRC,让机器回答基于给定上下文的问题来测试机器理解自然语言的程度的任务,简单来说即给定一个或多个文档p,以及一个问题Q,输出问题Q的答案A
机器阅读理解形式多样,常见类型包括完形填空式,答案选择式,片段抽取式,自由生成式,本次课程讲解的内容为片段抽取式的机器阅读理解,即问题Q的答案A在文档P中,A是P中的一个连续片段。
如何为机器阅读理解任务进行建模?
答:定位答案在文档中的起始位置和结束位置
1.1、数据集
CMRC2018
1.2、评估指标
-
精准匹配度(EM):计算预测结果是否与答案完全匹配
-
模糊匹配度(F1):计算预测结果与标准答案之间字级别的匹配程度
1.3、数据处理格式
[CLS] Question [SEP] Context [SEP]
1.3.1、如何精准定位答案位置
- start_position/end_position
- offset_mapping
1.3.2、Context过长如何解决
- 策略1——直接截断:简单易实现,但是会损失答案靠后的数据,因为无法定位答案
- 策略2——滑动窗口,实现较为复杂,会丢失部分上下文,但综合来看损失较小
1.4、模型结构
- ModelForQuestionAnswering
2、代码实战
2.1、导包
from transformers import (AutoTokenizer,
AutoModelForQuestionAnswering,
TrainingArguments,
Trainer,
DefaultDataCollator,
DataCollatorForTokenClassification)
from datasets import load_dataset
import evaluate
2.2、数据集加载
datasets = load_dataset('hfl/cmrc2018',cache_dir='./data',trust_remote_code=True)
datasets['train'][0]
2.3、数据集预处理
tokenizer = AutoTokenizer.from_pretrained("../Model/chinese-macbert-base")
tokenizer
2.3.1、简单处理
对上下文根据最大长度进行截断,若答案再被截断的上下文中,则将左右起始点设置为0;否则,进行夹逼。
这样可能会造成答案上下文丢失,就是说可能存在多条数据不存在答案的情况。
def process_func(examples):
tokenized_examples = tokenizer(text=examples['question'],
text_pair=examples['context'],
max_length=128,
truncation='only_second',
padding='max_length',
return_offsets_mapping=True,
)
offset_mapping = tokenized_examples.pop('offset_mapping')
start_positions = []
end_positions = []
for idx,offset in enumerate(offset_mapping):
#找到答案在原始文本中的起始位置和结束位置
answer = examples['answers'][idx]
start_char = answer['answer_start'][0]
end_char = start_char + len(answer['text'][0])
#定位答案在token中的起始位置和结束位置
#方法:我们拿到context的起始与结束,然后从左右两侧向答案逼近
#文本起始位置
context_start = tokenized_examples.sequence_ids(idx).index(1)
##文本结束位置
context_end = tokenized_examples.sequence_ids(idx).index(None,context_start) -1
#判断是否在context中
if offset[context_end][1] < start_char or offset[context_start][0]>end_char:
start_token_pos = 0
end_token_pos = 0
else:
token_id = context_start
while token_id <= context_end and offset[token_id][0] < start_char:
token_id += 1
start_token_pos = token_id
token_id = context_end
while token_id >= context_start and offset[token_id][1] > end_char:
token_id -= 1
end_token_pos = token_id
start_positions.append(start_token_pos)
end_positions.append(end_token_pos)
# print(answer,start_char,end_char,context_start,context_end,start_token_pos,end_token_pos)
# print(tokenizer.decode(tokenized_examples['input_ids'][idx][start_token_pos:end_token_pos+1]))#切片左闭右开 要加+1
tokenized_examples['start_positions'] = start_positions
tokenized_examples['end_positions'] = end_positions
return tokenized_examples
2.3.2、滑动窗口
上下文根据根据滑动窗口划分多个片段,在每个片段中寻找答案。
def process_func(examples):
'''
examples:原始datasets
'''
tokenized_examples = tokenizer(text=examples['question'],
text_pair=examples['context'],
max_length=256,
truncation='only_second',
padding='max_length',
return_overflowing_tokens=True,##滑动窗口
return_offsets_mapping=True,#每个单词的起始和结束位置
stride=128,###重叠部分大小
)
# offset_mapping = tokenized_examples.pop('offset_mapping')
sample_mapping = tokenized_examples.pop('overflow_to_sample_mapping')
start_positions = []
end_positions = []
examples_ids = []
for idx,true_idx in enumerate(sample_mapping):
#找到答案在原始文本中的起始位置和结束位置
answer = examples['answers'][sample_mapping[idx]]# 将滑动窗口后的每个片段找到对应的真实answer
#拿到answer起始位置
start_char = answer['answer_start'][0]
#拿到answer结束位置
end_char = start_char + len(answer['text'][0])
#定位答案在token中的起始位置和结束位置
#方法:我们拿到context的起始与结束,然后从左右两侧向答案逼近
#文本起始位置
#滑动窗口后,每个sequence中[question Context],context的起始位置
context_start = tokenized_examples.sequence_ids(idx).index(1)
##文本结束位置
context_end = tokenized_examples.sequence_ids(idx).index(None,context_start) -1
#拿到第idx个sequence的offset_mapping
offset = tokenized_examples.get('offset_mapping')[idx]
#判断是否在context中
if offset[context_end][1] < start_char or offset[context_start][0]>end_char:
start_token_pos = 0
end_token_pos = 0
else:
token_id = context_start
while token_id <= context_end and offset[token_id][0] < start_char:
token_id += 1
start_token_pos = token_id
token_id = context_end
while token_id >= context_start and offset[token_id][1] > end_char:
token_id -= 1
end_token_pos = token_id
start_positions.append(start_token_pos)#起始位置
end_positions.append(end_token_pos)#结束位置
##example_ids,存放经滑动窗口处理后的每个片段的id
examples_ids.append(examples['id'][sample_mapping[idx]])
# print(answer,start_char,end_char,context_start,context_end,start_token_pos,end_token_pos)
# print(tokenizer.decode(tokenized_examples['input_ids'][idx][start_token_pos:end_token_pos+1]))#切片左闭右开 要加+1
##把question的offset转化为None,保存对应的context的offset
tokenized_examples['offset_mapping'][idx] = [
(o if tokenized_examples.sequence_ids(idx)[k] == 1 else None)
for k , o in enumerate(tokenized_examples['offset_mapping'][idx])
]
tokenized_examples['start_positions'] = start_positions
tokenized_examples['end_positions'] = end_positions
tokenized_examples['example_ids'] = examples_ids
return tokenized_examples
2.3、数据处理
tokenized_datasets = datasets.map(process_func,batched=True,remove_columns=datasets['train'].column_names)
tokenized_datasets
2.4、获取模型输出
import numpy as np
import collections
def get_result(start_logits, end_logits,examples,features):
'''
start_logits:预测的起始位置
end_logits:预测的结束位置
examples:原始数据
features: tokenized dataset
'''
predictions = {}
references = {}
#example 和feature的映射
#每个标签下对应的sequence id
example_to_feature = collections.defaultdict(list)
for idx,example_id in enumerate(features['example_ids']):
example_to_feature[example_id].append(idx)
#最终答案候选
n_best= 20
#最大答案长度
max_answer_length = 30
for example in examples:
#example 的 id
example_id = example['id']
#context
context = example['context']
answers = []
#对于每个id下的sequence list遍历
for feature_idx in example_to_feature[example_id]:
#拿到预测结果
start_logit = start_logits[feature_idx]
end_logit = end_logits[feature_idx]
#拿到每个单词的位置映射offset
offset = features[feature_idx]['offset_mapping']
#按概率进行排序,取最大的n_best个
start_indexes = np.argsort(start_logit)[::-1][:n_best].tolist()
end_indexes = np.argsort(end_logit)[::-1][:n_best].tolist()
#遍历所有的可能的位置
for start_idx in start_indexes:
for end_idx in end_indexes:
#如何取在question中
if offset[start_idx] is None or offset[end_idx] is None:
continue
#位置不合法
if end_idx<start_idx or end_idx -start_idx + 1 > max_answer_length:
continue
#拿到真实结果与对应得分
answers.append({
"text":context[offset[start_idx][0]:offset[end_idx][1]],
"score": start_logit[start_idx] + end_logit[end_idx]
})
if len(answers)>0:
best_answer = max(answers,key=lambda x:x['score'])
predictions[example_id] = best_answer['text']
else:
predictions[example_id] = ""
references[example_id] = example['answers']['text']
return predictions,references
2.5、评估函数
from cmrc_eval import evaluate_cmrc
def metric(pred):
start_logits,end_logits = pred[0]
if start_logits.shape[0] == len(tokenized_datasets['validation']):
p,r = get_result(start_logits,end_logits,datasets['validation'],tokenized_datasets['validation'])
else:
p,r = get_result(start_logits,end_logits,datasets['test'],tokenized_datasets['test'])
return evaluate_cmrc(p,r)
2.6、模型加载
model = AutoModelForQuestionAnswering.from_pretrained('../Model/chinese-macbert-base')
2.7、模型配置
args = TrainingArguments(
output_dir='model_for_qa',
per_device_train_batch_size = 8,
per_device_eval_batch_size = 8,
gradient_accumulation_steps=32, # 梯度累加 现存减少,训练时间延长
gradient_checkpointing= True, #优化
optim='adafactor', #指定优化器
eval_steps=10,
eval_strategy= 'steps',
save_strategy='epoch',
logging_steps=50,
num_train_epochs=1,
)
2.8、加载Trainer进行训练
for name,params in model.bert.named_parameters():
params.requires_grad = False
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=DefaultDataCollator(),
compute_metrics=metric
)
trainer.train()
2.9、模型预测
from transformers import pipeline
pipe = pipeline('question-answering',model=model,tokenizer = tokenizer,device=0)
pipe
pipe(question='小明在哪里上班?',context ='小明在北京上班。')