前言
最近看到今年早些时候百度的“2020语言与智能技术竞赛”比赛,里面有五个赛道,三个赛道与信息抽取有关,分别是机器阅读理解、关系抽取、事件抽取。最近正好对信息抽取任务比较感兴趣,所以拿来复现一下baseline模型,同时参考参考大佬们的想法,学习下思想和技巧。
参考比赛:Tweet Sentiment Extraction、2020语言与智能技术竞赛-机器阅读理解,这个两个赛题都涉及到了信息抽取
Tweet Sentiment Extraction:通过给定的tweet,以及其情感倾向,从该条tweet中抽取可以代表其情感倾向的词句。
2020语言与智能技术竞赛-机器阅读理解:给定背景内容context,根据所给问题,从context中抽取对应的答案。
其实Tweet Sentiment Extraction也可以看作阅读理解问题,只是把tweet和emotion作为context,而问题固定(抽取情感语句)。
这篇是信息抽取系列的第一篇:即机器阅读理解,比起其他两个信息抽取任务来说,可以算是小打小闹了,baseline模型也比较简单,通过两个softmax,找到答案的start和end即可。
但在数据集构建时真的不知道出了多少bug,最后自己总结一个模版,可以用在中文和英文并且对标注的数据有很大的噪声容忍度的SQuAD任务的数据集生成方法。
样本数据处理
数据样例:数据为json格式,每条样本包含context、question、id、answers:text、answer_start(答案的起始位置)。
注意这个answer_start很重要,后面有妙用,如果当文中出现重复的答案片段时,要保证你的y值和answer_start保持一致,而且answer_start可以用来处理过长(>512)的context,把每一个样本数据都利用起来。
'''{
"data": [
{
"title": "",
"paragraphs": [
{
"context": "第35集雪见缓缓张开眼睛,景天又惊又喜之际,长卿和紫萱的仙船驶至,见众人无恙,也十分高兴。\
众人登船,用尽合力把自身的真气和水分输给她。\
雪见终于醒过来了,但却一脸木然,全无反应。众人向常胤求助,却发现人世界竟没有雪见的身世纪录。\
长卿询问清微的身世,清微语带双关说一切上了天界便有答案。长卿驾驶仙船,众人决定立马动身,往天界而去。\
众人来到一荒山,长卿指出,魔界和天界相连。由魔界进入通过神魔之井,便可登天。众人至魔界入口,仿若一黑色的蝙蝠洞,但始终无法进入。\
后来花楹发现只要有翅膀便能飞入。于是景天等人打下许多乌鸦,模仿重楼的翅膀,制作数对翅膀状巨物。刚佩戴在身,便被吸入洞口。众人摔落在地,抬头发现魔界守卫。\
景天和众魔套交情,自称和魔尊重楼相熟,众魔不理,打了起来。",
"qas": [
{
"question": "仙剑奇侠传3第几集上天界",
"id": "0a25cb4bc1ab6f474c699884e04601e4",
"answers": [
{
"text": "第35集",
"answer_start": 0
}
]
}
]
},
'''
数据提取,这里就不多介绍怎么把样本以结构的形式取出,我的方法比较笨。
def data_load(path):
with open(path) as json_file:
data = json.load(json_file)
context = [x['context'] for x in data['data'][0]['paragraphs']]
question = [x['qas'][0]['question'] for x in data['data'][0]['paragraphs']]
answers_text = [x['qas'][0]['answers'][0]['text'] for x in data['data'][0]['paragraphs']]
answer_start = [x['qas'][0]['answers'][0]['answer_start'] for x in data['data'][0]['paragraphs']]
return context,question,answers_text,answer_start
训练样本处理:前方高能
主要思路:找到answer与context重合的区域,对context进行编码,再反解码后,如果反解码出来的文字在原生context中的位置处于答案重合区域,则该文字的token视为答案的一部分,找到连续答案token取第一个为start_index,最后一个为end_index。
def train_data_proceed(tokenizer,context,question,answers_text,answer_start,MAX_LEN==512):
ct = len(context)
input_ids = np.zeros((ct,MAX_LEN),dtype='int32')
attention_mask = np.zeros((ct,MAX_LEN),dtype='int32')
start_tokens = np.zeros((ct,MAX_LEN),dtype='int32')
end_tokens = np.zeros((ct,MAX_LEN),dtype='int32')
'''
这里我们不再用transfomers自带的tokenizer直接编码文本
而是自己定义input_ids和attention_mask,是为了方便计算start_tokens和end_tokens
'''
for k in range(ct):
context_k = context[k]
question_k = question[k]
answers_text_k = answers_text[k]
answer_start_k = answer_start[k]
'''
坑1:在处理context的时候发现tokenizer对文本中的' '空格是不编码的,因此会导致反解码时,会省略空格,使得context 与 context_encode_decode顺序错乱。
因此这里repalce掉答案出现之间的空格,相应的answer_start也向前移动x,x为取出空格的数量。
'''
answer_start_k = answer_start_k - len(re.findall(' ',context_k[:answer_start_k]))
context_k = context_k.replace(' ','')
'''
定义最后 input_ids 的形式
[cls] question [sep] context [sep] [pad] [pad] ......
'''
if len(question_k) + 3 + len(context_k)>= MAX_LEN:
'''
如果形成的input_ids长度大于MAX_LEN,则考虑对context进行截取,定义截取context中答案前后 长为X 字符的片段
则有公式:answer_start_k+len(answers_text_k)+x - (answer_start_k-x)+3+len(question_k) <= MAX_LEN
解得:2x = MAX_LEN-len(answers_text_k) - 3 -len(question_k)
'''
x = (MAX_LEN-len(answers_text_k) - 3 -len(question_k))