问答任务
抽取式问答任务:给定一个问题和一段文本,从这段文本中找出能回答该问题的文本片段(span)。
例如:

load数据集
数据预处理
依旧是tokenizer的套路。
下面是生成mask的方式:
tokenizer生成的token IDs也就是input_ids一般来说随着预训练模型名字的不同而有所不同。原因是不同的预训练模型在预训练的时候设定了不同的规则。但只要tokenizer和model的名字一致,那么tokenizer预处理的输入格式就会满足model需求的。关于预处理更多内容参考这个教程。
预训练机器问答模型们是如何处理非常长的文本:一般来说预训练模型输入有最大长度要求,所以我们通常将超长的输入进行截断。但是,如果我们将问答数据三元组中的超长context截断,那么我们可能丢掉答案(因为我们是从context中抽取出一个小片段作为答案)。 把超长的输入切片为多个较短的输入,每个输入都要满足模型最大长度输入要求。由于答案可能存在与切片的地方,因此我们需要允许相邻切片之间有交集,代码中通过doc_stride参数控制。
机器问答预训练模型通常将question和context拼接之后作为输入,然后让模型从context里寻找答案。
由于context是拼接在question后面的,对应着第2个文本,所以使用
only_second控制.tokenizer使用doc_stride控制切片之间的重合长度。由于我们对超长文本进行了切片,我们需要重新寻找答案所在位置(相对于每一片context开头的相对位置)。机器问答模型将使用答案的位置(答案的起始位置和结束位置,start和end)作为训练标签(而不是答案的token IDS)。所以切片需要和原始输入有一个对应关系,每个token在切片后context的位置和原始超长context里位置的对应关系。在tokenizer里可以使用
return_offsets_mapping参数得到这个对应关系的map:
我们还需要使用
sequence_ids参数来区分question和context。
注意,有时候question拼接context,而有时候是context拼接question,不同的模型有不同的要求,因此我们需要使用
padding_side参数来指定。
合并
Fine-tuning微调模型
由于我们要做的是机器问答任务,于是我们使用这个类AutoModelForQuestionAnswering
会提示
Some weights of the model checkpoint at distilbert-base-uncased were not used when initializing DistilBertForQuestionAnswering: ['vocab_projector.bias', 'vocab_transform.bias', 'vocab_transform.weight', 'vocab_layer_norm.weight', 'vocab_projector.weight', 'vocab_layer_norm.bias'] - This IS expected if you are initializing DistilBertForQuestionAnswering from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model). - This IS NOT expected if you are initializing DistilBertForQuestionAnswering from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model). Some weights of DistilBertForQuestionAnswering were not initialized from the model checkpoint at distilbert-base-uncased and are newly initialized: ['qa_outputs.weight', 'qa_outputs.bias'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
由于我们微调的任务是机器问答任务,而我们加载的是预训练的语言模型,那么上面会提示我们加载模型的时候扔掉了一些不匹配的神经网络参数(预训练语言模型的神经网络head被扔掉了,同时随机初始化了机器问答的神经网络head)。
正因为有这些随机初始化的参数,所以我们要在新的数据集上重新fine-tune我们的模型。
args
default_data_collator将预处理好的数据喂给模型。
把模型,训练参数,数据,之前使用的tokenizer,和数据投递工具default_data_collator传入Tranier即可。
Evaluation评估
我们需要将模型的输出后处理成我们需要的文本格式。
模型评估会稍微优点复杂,我们需要将模型的输出后处理成我们需要的文本格式。模型本身预测的是answer所在start/end位置的logits。如果我们评估时喂入模型的是一个batch,那么输出如下:
模型的输出是一个像dict的数据结构,包含了loss(因为提供了label,所有有loss),answer start和end的logits。我们在输出预测结果的时候不需要看loss,直接看logits就好了。
每个feature里的每个token都会有一个logit。预测answer最简单的方法就是选择start的logits里最大的下标最为answer其实位置,end的logits里最大下标作为answer的结束位置。
以上策略大部分情况下都是不错的。但是,如果我们的输入告诉我们找不到答案:比如start的位置比end的位置下标大,或者start和end的位置指向了question。
这个时候,简单的方法是我们继续需要选择第2好的预测作为我们的答案了,实在不行看第3好的预测,以此类推。
由于上面的方法不太容易找到可行的答案,我们需要思考更合理的方法。我们将start和end的logits相加得到新的打分,然后去看最好的n_best_size个start和end对。从n_best_size个start和end对里推出相应的答案,然后检查答案是否有效,最后将他们按照打分进行怕苦,选择得分最高的作为答案。
随后我们对根据score对valid_answers进行排序,找到最好的那一个。最后还剩一步是:检查start和end位置对应的文本是否在context里面而不是在question里面。
为了完成这件事情,我们需要添加以下两个信息到validation的features里面:
产生feature的example的ID。由于每个example可能会产生多个feature,所以每个feature/切片的feature需要知道他们对应的example。
offset mapping: 将每个切片的tokens的位置映射会原始文本基于character的下标位置。
所以我们又重新处理了以下validation验证集。和处理训练的时候的prepare_train_features稍有不同。
和之前一样将prepare_validation_features函数应用到每个验证集合的样本上。
使用Trainer.predict方法获得所有预测结果
这个 Trainer 隐藏了 一些模型训练时候没有使用的属性(这里是 example_id和offset_mapping,后处理的时候会用到),所以我们需要把这些设置回来:
当一个token位置对应着question部分时候,prepare_validation_features函数将offset mappings设定为None,所以我们根据offset mapping很容易可以鉴定token是否在context里面啦。我们同样也根绝扔掉了特别长的答案。
可以看到模型做对了!
如同上面的例子所言,由于第1个feature一定是来自于第1个example,所以相对容易。对于其他的fearures来说,我们需要一个features和examples的一个映射map。同样,由于一个example可能被切片成多个features,所以我们也需要将所有features里的答案全部手机起来。以下的代码就将exmaple的下标和features的下标进行map映射。
对于后处理过程基本上已经全部完成了。最后一点事情是如何解决无答案的情况(squad_v2=True的时候)。以上的代码都只考虑了context里面的asnwers,所以我们同样需要将无答案的预测得分进行搜集(无答案的预测对应的CLSt oken的start和end)。如果一个example样本又多个features,那么我们还需要在多个features里预测是不是都无答案。所以无答案的最终得分是所有features的无答案得分最小的那个。
只要无答案的最终得分高于其他所有答案的得分,那么该问题就是无答案。
把所有事情都合并起来:
将后处理函数应用到原始预测上:
然后我们加载评测指标:
同理,也可以使用我们提供的本地脚本来加载:
然后我们基于预测和标注对评测指标进行计算。为了合理的比较,我们需要将预测和标注的格式。对于squad2来说,评测指标还需要no_answer_probability参数(由于已经无答案直接设置成了空字符串,所以这里直接将这个参数设置为0.0)
Last updated
Was this helpful?