机器阅读理解——样本数据处理与Baseline模型搭建训练

  • 前言
  • 样本数据处理
    • 数据测试
  • 模型部分
    • 模型构建
    • 模型训练
    • 部分推理结果
  • 总结

前言

最近看到今年早些时候百度的“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))//2-1end = answer_start_k+len(answers_text_k)+xif answer_start_k-x <0:begain = 0idx = answer_start_kelse:begain = answer_start_k-xidx = xcontext_k = context_k[begain:end]else:idx = answer_start_kchars = np.zeros((len(context_k)))chars[idx:idx+len(answers_text_k)]=1'''计算答案与文本重合的区域,通过answer_start来定位答案,可以避免多答案的问题。'''enc_context = tokenizer.encode(context_k) enc_question = tokenizer.encode(question_k) offsets = [] idx=0for t in enc_context[1:]:w = tokenizer.decode([t])'''跳过 [cls] 对context_encode 进行反解码'''if '#' in w and len(w)>1:w = w.replace('#','')'''处理中发现,token反解码cm ml等单位计量时,会生成##cm的格式,这里做去除'''if w == '[UNK]':'''如果反解码出'[UNK]'则定以其长度为1,用‘。’代替'''w = '。'offsets.append((idx,idx+len(w)))idx += len(w)'''得到反解码出来的每一个元素在context中的位置 offsets'''toks = []for i,(a,b) in enumerate(offsets):'''如果该位置在context中属于答案重合部分,即之前标记的1,则标记为答案片段'''sm = np.sum(chars[a:b])if sm>0: toks.append(i) input_ids[k,:len(enc_question)+len(enc_context)-1] = enc_question + enc_context[1:]attention_mask[k,:len(enc_question)+len(enc_context)-1] = 1if len(toks)>0:start_tokens[k,toks[0]+len(enc_question)] = 1end_tokens[k,toks[-1]+len(enc_question)] = 1'''生成input_ids、attention_mask作为输入x取答案片段最开始的token_idex与最后的token_idex作为Y值start_tokens、end_tokens'''return input_ids,attention_mask,start_tokens,end_tokens

测试集数据处理同理,但不需要给Y值

def test_data_proceed(tokenizer,context_t,question_t,answers_text_t,answer_start_t,MAX_LEN):ct = len(context_t)input_ids_t = np.zeros((ct,MAX_LEN),dtype='int32')attention_mask_t = np.zeros((ct,MAX_LEN),dtype='int32')for k in range(len(context_t)):enc_context_t = tokenizer.encode(context_t[k]) enc_question_t = tokenizer.encode(question_t[k])if len(enc_question_t)+len(enc_context_t)-1 > MAX_LEN:x = enc_question_t + enc_context_t[1:]input_ids_t[k] = x[:MAX_LEN]attention_mask_t[k,:MAX_LEN] = 1else:input_ids_t[k,:len(enc_question_t)+len(enc_context_t)-1] = enc_question_t + enc_context_t[1:]attention_mask_t[k,:len(enc_question_t)+len(enc_context_t)-1] = 1return input_ids_t,attention_mask_t

数据测试

def random_int_list(start, stop, length):start, stop = (int(start), int(stop)) if start <= stop else (int(stop), int(start))length = int(abs(length)) if length else 0random_list = []for i in range(length):random_list.append(random.randint(start, stop))return random_listprint('#'*25)
print('### check train_data')
print('#'*25)for i in random_int_list(0,len(context),10):x_ = [tokenizer.decode([t]) for t in input_ids[i]]token_ans = ''.join(x_[np.argmax(start_tokens[i]):np.argmax(end_tokens[i])+1])print(token_ans+'  '+answers_text[i])'''1200字左右  1200字左右一天两次  一天两次龙岩市  龙岩市荆州地区  荆州地区深圳纽仕达电商之家  深圳纽仕达电商之家25度左右  25度左右第六期  第六期狼爪  狼爪298.0元  298.0元所有的肌肤类型  所有的肌肤类型'''print('#'*25)
print('### check test_data')
print('#'*25)for i in random_int_list(0,len(context_t),5):x_ = [tokenizer.decode([t]) for t in input_ids_t[i]]token_ans = ''.join(x_)print(token_ans)

模型部分

模型构建

Tensorflow:2.0.0
Transformers:3.1.0

模型1:

def build_model(pretrained_path,config,MAX_LEN):ids = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)att = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)   bert_model = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)x = bert_model(ids,attention_mask=att)'''取最后一层的hidden_layers,分别接两个liner+softmax,得到start和end'''x1 = tf.keras.layers.Dropout(0.1)(x[0]) x1 = tf.keras.layers.Conv1D(1,1)(x1)'''(None, 96, 768)(None, 96, 1)769个参数相当于做了一次加权+b如果不指定该函数,将不会使用任何激活函数(即使用线性激活函数:a(x)=x)flatten后得到展开的768接一个softmax'''x1 = tf.keras.layers.Flatten()(x1)x1 = tf.keras.layers.Activation('softmax')(x1)x2 = tf.keras.layers.Dropout(0.1)(x[0]) x2 = tf.keras.layers.Conv1D(1,1)(x2)x2 = tf.keras.layers.Flatten()(x2)x2 = tf.keras.layers.Activation('softmax')(x2)model = tf.keras.models.Model(inputs=[ids, att], outputs=[x1,x2])optimizer = tf.keras.optimizers.Adam(learning_rate=3e-5)model.compile(loss='categorical_crossentropy', optimizer=optimizer)return model

模型2:

def build_model_2(pretrained_path,config,MAX_LEN):ids = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)att = tf.keras.layers.Input((MAX_LEN,), dtype=tf.int32)config.output_hidden_states = Truebert_model = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)x, _, hidden_states = bert_model(ids,attention_mask=att)layer_1 = hidden_states[-1]layer_2 = hidden_states[-2]'''不同于模型1,模型2取了最后两层hidden_layers分别接了两个一维卷积后作softmax,因为start和end来源于不同的hidden_layer,模型学习的目的性更强,特征也更加丰富'''x1 = tf.keras.layers.Dropout(0.1)(layer_1)#(512,768)x1 = tf.keras.layers.Conv1D(128, 2, padding='same')(x1)#(512,128)x1 = tf.keras.layers.LeakyReLU()(x1)'''ReLU是将所有的负值都设为零,相反,Leaky ReLU是给所有负值赋予一个非零斜率。'''#x1 = tf.keras.layers.Conv1D(64, 2, padding='same')(x1)#(512,64)x1 = tf.keras.layers.Dense(1, dtype='float32')(x1)#(512,1)start_logits = tf.keras.layers.Flatten()(x1)#(512,)start_logits = tf.keras.layers.Activation('softmax')(start_logits)x2 = tf.keras.layers.Dropout(0.1)(layer_2)x2 = tf.keras.layers.Conv1D(128, 2, padding='same')(x2)x2 = tf.keras.layers.LeakyReLU()(x2)x2 = tf.keras.layers.Conv1D(64, 2, padding='same')(x2)x2 = tf.keras.layers.Dense(1, dtype='float32')(x2)end_logits = tf.keras.layers.Flatten()(x2)end_logits = tf.keras.layers.Activation('softmax')(end_logits)model = tf.keras.models.Model(inputs=[ids, att], outputs=[start_logits,end_logits])optimizer = tf.keras.optimizers.Adam(learning_rate=2e-5)model.compile(loss='categorical_crossentropy', optimizer=optimizer)return model

模型训练

这里采用的是K折训练,单模型stacking,将数据集随机划分成5份,取其中4份作为训练集,1份作为测试集,分别训练5个val_loss最低的模型,分别对验证集进行推理,将5个模型的softmax结果加权平均后,argmax得到预测的答案start和end位置。

def main():VER='v0'DISPLAY=2 # USE display=1 FOR INTERACTIVEoof_start = np.zeros((input_ids.shape[0],MAX_LEN))oof_end = np.zeros((input_ids.shape[0],MAX_LEN))preds_start = np.zeros((input_ids_t.shape[0],MAX_LEN))preds_end = np.zeros((input_ids_t.shape[0],MAX_LEN))skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=777)for fold,(idxT,idxV) in enumerate(skf.split(input_ids,answer_start)):'''fold:当前K折折次idxT:当前训练集indexidxV:当前测试集indexlen(idxV)*4=len(idxT)'''print('#'*25)print('### FOLD %i'%(fold+1))print('#'*25)K.clear_session()model = build_model_2(pretrained_path,config,MAX_LEN)sv = tf.keras.callbacks.ModelCheckpoint('./model_/%s-roberta-%i.h5'%(VER,fold), monitor='val_loss', verbose=2, save_best_only=True,save_weights_only=True, mode='auto', save_freq='epoch')model.fit([input_ids[idxT,], attention_mask[idxT,]],[start_tokens[idxT,],end_tokens[idxT,]], epochs=1, batch_size=8, verbose=DISPLAY, callbacks=[sv],validation_data=([input_ids[idxV,],attention_mask[idxV,]], [start_tokens[idxV,], end_tokens[idxV,]]))print('Loading model...')model.load_weights('./model_/%s-roberta-%i.h5'%(VER,fold))print('Predicting Test...')preds = model.predict([input_ids_t,attention_mask_t],verbose=DISPLAY)preds_start += preds[0]/skf.n_splitspreds_end += preds[1]/skf.n_splits'''5折验证结果进行投票,选出得分最高的点作为开始和结束点'''all_ = []for k in range(len(input_ids_t)):a = np.argmax(preds_start[k,])b = np.argmax(preds_end[k,])if a>b: st = context_t[k]else:x_ = [tokenizer.decode([t]) for t in input_ids_t[k]]st = ''.join(x_[a:b+1])all_.append(st)ans_data = pd.DataFrame(context_t,columns=['context'])ans_data['question'] = question_tans_data['answers_text'] = answers_text_tans_data['pred_answers'] = all_ans_data.to_csv('result.csv')

部分推理结果


抽取了一些验证集上的推理结果,可以看到模型基本都能准确给出正确答案。


总结

有关于其他的数据增强、训练技巧这里就不做过多介绍了,想要了解的朋友可以去看下大牛们的技术报告。

总的来说,阅读理解baseline模型还是非常简单的,大家可以采取不同的方法取标注指针,不一定是模型的最后两层隐藏层。

在实际训练中,所有子模型都只训练了一轮就达到了过拟合,有几个可能:1.数据太简单,2.模型太复杂了,3.学习速率太高,可以考虑使用warm_up。

接下来我会找时间去复现关系抽取、事件抽取这两个任务类型,涉及到NER,应该会更加有意思。


完整训练代码,可以参考我的github: https://github.com/zhengyanzhao1997/TF-NLP-model/blob/main/model/train/SQuAD_baseline.py

部分参考资料:
样本数据构造与模型训练参考思路:https://www.kaggle.com/cdeotte/tensorflow-roberta-0-705/comments

模型参考思路:https://zhuanlan.zhihu.com/p/139779541

信息抽取(一)机器阅读理解——样本数据处理与Baseline模型搭建训练(2020语言与智能技术竞赛)相关推荐

  1. 基于百度2020语言与智能技术竞赛:事件抽取任务

    关注微信公众号:NLP分享汇.[喜欢的扫波关注,每天都在更新自己之前的积累] 文章链接:https://mp.weixin.qq.com/s/4oGMn1eZehGCBrmKJSf1_A ​[前言] ...

  2. NLP-阅读理解:“阅读理解”综述(Machine Reading Comprehension, MRC)【完形填空、多项选择、抽取式(答案片段抽取)、生成式(自由作答)】《机器阅读理解数据集》

    <原始论文:Neural Machine Reading Comprehension: Methods and Trends> 一.阅读理解概述 所谓的机器阅读理解(Machine Rea ...

  3. 使用MRC(机器阅读理解)方式做事件抽取任务,基于2020百度事件抽取任务

    ​关注微信公众号:NLP分享汇.[喜欢的扫波关注,每天都在更新自己之前的积累] 文章链接:https://mp.weixin.qq.com/s/aKB6j42bC1MnWCFIEyjwQQ [前言] ...

  4. 神经机器阅读理解最新综述:方法和趋势

    作者丨刘姗姗 学校丨国防科技大学 研究方向丨自然语言处理 近年来,基于深度学习方法的机器阅读理解受到了广泛的关注.近日,来自国防科技大学的团队在arXiv上发布了预印版综述文章 Neural Mach ...

  5. NLP机器阅读理解:四大任务及相应数据集、比赛

    作者 | 周俊贤 整理 | NewBeeNLP 关于机器阅历理解应用,首先介绍大家一个真实的业务场景,从网购平台的退单工单中抽取实际退款金额,数据大概是这样: 你好,我3月份在网上买的洗衣服务,当时买 ...

  6. 科大讯飞刷新纪录,机器阅读理解如何超越人类平均水平? | 技术头条

    点击上方↑↑↑蓝字关注我们~ 「2019 Python开发者日」明日开启,扫码咨询 ↑↑↑ 记者 | 琥珀 出品 | AI科技大本营(公众号ID:rgznai100) 对于日常从事模型训练的研究人员来 ...

  7. 2018机器阅读理解技术竞赛,奇点机智获第一名

    长期以来,大家一直有这样的疑问:机器到底能不能真正理解人类?机器阅读理解的能力,能否超越人类? 2018年5月15日,由中国中文信息学会(CIPS).中国计算机学会(CCF)和百度联手举办的" ...

  8. 什么是机器阅读理解?跟自然语言处理有什么关系?

    导读:机器阅读理解(Machine Reading Comprehension,MRC)是一种利用算法使计算机理解文章语义并回答相关问题的技术.由于文章和问题均采用人类语言的形式,因此机器阅读理解属于 ...

  9. 【博文笔记】Attentive Reader\Impatient Reader:机器阅读理解之开山之作Teaching Machines to Read and Comprehend

    来源 参考博客: 机器阅读理解(看经典MRC模型与花式Attention) CNN&Dailymail:Teaching Machines to Read and Comprehend 论文: ...

最新文章

  1. Visual Studio 2017工程项目的几个重要文件解析
  2. nginx稳定版本_Nginx简介
  3. 网络营销专员浅析如何以低成本高获取实现网络营销?
  4. 工作以后如何有效学习
  5. Android 拍照是开启(调用)闪光灯(原创)
  6. VTK:可视化算法之HeadBone
  7. 《混合云计算》——2.2 结合服务创建混合云环境
  8. 背景图层和普通图层的区别_ps:图层有多少种类?我已经列出来了,学不学就看你自己了...
  9. android action bar 风格,自定义ActionBar的风格
  10. 编写一个Rubygem, 如何在gem 被Install之前运行一段程序?
  11. Data truncation: Out of range value for column ‘quanity‘ at row 问题解决方案
  12. python字典用法大全
  13. 东南大学成贤学院计算机报名,2019上半年东南大学成贤学院全国计算机等级考试预报名通知...
  14. pandas之map(), apply()和applymap()
  15. 02. Django基础:Django项目结构
  16. 前端实战项目:前端框架Vue3.0项目教程(一)Vue3.0环境的搭建
  17. JAVA垃圾回收机制
  18. 编译原理与编译构造 LR文法
  19. 【论文阅读笔记】《Simple, Accurate, and Robust Projector-Camera Calibration》
  20. 端元提取——逐次投影算法SPA与顶点成分分析VCA

热门文章

  1. strncmp用法说明
  2. 导出全部记录到excel
  3. 通过console口连接交换机
  4. 把数据导出Excel
  5. 另一个日历(根据农历网上的寿星万年历修改)最新修改适应FF
  6. 速读训练软件_记忆力训练:如何提高注意力呢?
  7. 主线程如何等待多线程完成 返回数据_多线程基础体系知识清单
  8. mysql完整字段包括_MySQL字段类型最全解析
  9. 鸿蒙系统已经推出,华为号召力太吓人!鸿蒙系统发布短短两天,主流应用已开始适配!...
  10. linux 32bit swt,无法在Windows 32位上加载SWT库