日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2025/3/8 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 信息抽取(一)机器阅读理解——样本数据处理与Baseline模型搭建训练(2020语言与智能技术竞赛) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

機器閱讀理解——樣本數據處理與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语言与智能技术竞赛)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。