1. 百度NLP賽事實踐解讀!

        共 19615字,需瀏覽 40分鐘

         ·

        2022-11-22 11:10

        開放領域的搜索場景下得到的網頁數據會非常復雜,其中往往存在著網頁文檔質量參差不齊、長短不一,問題答案分布零散、長度較長等問題,給答案抽取答案置信度計算帶來了較大挑戰(zhàn)。

        本文基于百度搜索技術創(chuàng)新挑戰(zhàn)賽中的搜索問答賽題,對搜索問答類任務做詳細解讀。本文思路如圖:

        (本文實踐講解框架,其中改進思路見文末)

        賽題背景

        本賽題希望調研真實網絡環(huán)境下的文檔級機器閱讀理解技術,共分為兩個子任務,涉及基于復雜網頁文檔內容的答案抽取答案檢驗技術(詳細任務定義可參考賽事官網)。

        賽事官網:

        https://aistudio.baidu.com/aistudio/competition/detail/660/0/introduction

        難點分析

        如何在文檔長度不定答案長度較長的數據環(huán)境中取得良好且魯棒的答案抽取效果是子任務1關注的重點。

        方案介紹

        賽題可以視為基礎的信息抽取任務,也可以直接視為問答類型的信息抽取問題。我們需要構建一個模型,根據query從document中找到想要的答案。

        思路一:BERT或ERNIE

        如果我們使用BERT 或者 ERNIE 可以直接參考如下思路,模型的輸出可以為對應的兩個位置,分別是回答的開始位置和結束位置。

        這里需要深入一下模型的實現細節(jié):

        • query和documnet是一起輸入給模型,一般情況下query在前面。

        • 回答對應的輸出可以通過模型輸出后的全連接層完成分類,當然回歸也可以。


        思路二:QA

        如果采用QA的思路,則需要將比賽數據集轉換為QA的格式,特別是文本的處理:長文本需要進行截斷。

        方案代碼

        方案借助QA的思路,使用ERNIE快速完成模型訓練與預測。同時,文末給出了提分改進方案的思路。

        詳情可參考源Notebook:

        https://aistudio.baidu.com/aistudio/projectdetail/5013840(一鍵運行提交

        步驟1:解壓數據集

        !pip install paddle-ernie > log.log
        # !cp data/data174963/data_task1.tar /home/aistudio/
        !tar -xf /home/aistudio/data_task1.tar

        步驟2:讀取數據集

        # 導入常見的庫
        import numpy as np
        import pandas as pd
        import os, sys, json
        # 讀取訓練集、測試集和驗證集
        train_json = pd.read_json('data_task1/train_data/train.json', lines=True)
        test_json = pd.read_json('data_task1/test_data/test.json', lines=True)
        dev_json = pd.read_json('data_task1/dev_data/dev.json', lines=True)
        # 查看數據集樣例
        train_json.head(1)
        train_json.iloc[0]
        test_json.iloc[0]

        步驟3:加載ERNIE模型

        這里我們使用paddlenlp==2.0.7,當然你也可以選擇更高的版本。更高的版本會將損失計算也封裝進去,其他的部分區(qū)別不大。

        import paddle
        import paddlenlp
        print('paddle version', paddle.__version__)
        print('paddlenlp version', paddlenlp.__version__)

        from paddlenlp.transformers import ErnieForQuestionAnswering, ErnieTokenizer
        tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')
        model = ErnieForQuestionAnswering.from_pretrained('ernie-1.0')
        # 對文檔的文檔進行劃分、計算文檔的長度
        train_json['doc_sentence'] = train_json['doc_text'].str.split('。')
        train_json['doc_sentence_length'] = train_json['doc_sentence'].apply(lambda doc: [len(sentence) for sentence in doc])
        train_json['doc_sentence_length_max'] = train_json['doc_sentence_length'].apply(max)
        train_json = train_json[train_json['doc_sentence_length_max'] < 10000# 刪除了部分超長文檔

        # 對文檔的文檔進行劃分、計算文檔的長度
        dev_json['doc_sentence'] = dev_json['doc_text'].str.split('。')
        dev_json['doc_sentence_length'] = dev_json['doc_sentence'].apply(lambda doc: [len(sentence) for sentence in doc])
        dev_json['doc_sentence_length_max'] = dev_json['doc_sentence_length'].apply(max)
        dev_json = dev_json[dev_json['doc_sentence_length_max'] < 10000# 刪除了部分超長文檔

        # 對文檔的文檔進行劃分、計算文檔的長度
        test_json['doc_sentence'] = test_json['doc_text'].str.split('。')
        test_json['doc_sentence_length'] = test_json['doc_sentence'].apply(lambda doc: [len(sentence) for sentence in doc])
        test_json['doc_sentence_length_max'] = test_json['doc_sentence_length'].apply(max)
        train_json.iloc[10]
        test_json.iloc[10]

        步驟4:構建數據集

        接下來需要構建QA任務的數據集,這里的數據集需要處理為如下的格式:

        query [SEP] sentence of document
        • 訓練集數據集處理
        train_encoding = []

        # for idx in range(len(train_json)):
        for idx in range(10000):

            # 讀取原始數據的一條樣本
            title = train_json.iloc[idx]['title']
            answer_start_list = train_json.iloc[idx]['answer_start_list']
            answer_list = train_json.iloc[idx]['answer_list']
            doc_text = train_json.iloc[idx]['doc_text']
            query = train_json.iloc[idx]['query']
            doc_sentence = train_json.iloc[idx]['doc_sentence']
            
            #  對于文章中的每個句子
            for sentence in set(doc_sentence):

                # 如果存在答案
                for answer in answer_list:
                    answer = answer.strip("。")
                    
                    # 如果問題 + 答案 太長,跳過
                    if len(query + sentence) > 512:
                        continue
                    
                    # 對問題 + 答案進行編碼
                    encoding = tokenizer.encode(query, sentence, max_seq_len=512, return_length=True
                        return_position_ids=True, pad_to_max_seq_len=True, return_attention_mask=True)
                    
                    # 如果答案在這個句子中,找到start 和 end的 位置
                    if answer in sentence:            
                        encoding['start_positions'] = len(query) + 2 + sentence.index(answer)
                        encoding['end_positions'] = len(query) + 2 + sentence.index(answer) + len(answer)

                    # 如果不存在,則位置設置為0
                    else:
                        encoding['start_positions'] = 0
                        encoding['end_positions'] = 0
                    
                    # 存儲正樣本
                    if encoding['start_positions'] != 0:
                        train_encoding.append(encoding)
                    
                    # 對負樣本進行采樣,因為負樣本太多
                    # 正樣本:query + sentence -> answer 的情況
                    # 負樣本:query + sentence -> No answer 的情況
                    if encoding['start_positions'] == 0 and np.random.randint(0100) > 99:
                        train_encoding.append(encoding)
            
            if len(train_encoding) % 500 == 0:
                print(len(train_encoding))
        • 驗證集數據集處理
        val_encoding = []

        for idx in range(len(dev_json)):
        # for idx in range(200):
            title = dev_json.iloc[idx]['title']
            answer_start_list = dev_json.iloc[idx]['answer_start_list']
            answer_list = dev_json.iloc[idx]['answer_list']
            doc_text = dev_json.iloc[idx]['doc_text']
            query = dev_json.iloc[idx]['query']
            doc_sentence = dev_json.iloc[idx]['doc_sentence']

            for sentence in set(doc_sentence):
                for answer in answer_list:
                    answer = answer.strip("。")

                    if len(query + sentence) > 512:
                        continue
                    
                    encoding = tokenizer.encode(query, sentence, max_seq_len=512, return_length=True
                        return_position_ids=True, pad_to_max_seq_len=True, return_attention_mask=True)

                    if answer in sentence:            
                        encoding['start_positions'] = len(query) + 2 + sentence.index(answer)
                        encoding['end_positions'] = len(query) + 2 + sentence.index(answer) + len(answer)
                    else:
                        encoding['start_positions'] = 0
                        encoding['end_positions'] = 0
                    
                    if encoding['start_positions'] != 0:
                        val_encoding.append(encoding)

                    if encoding['start_positions'] == 0 and np.random.randint(0100) > 99:
                        val_encoding.append(encoding)
        • 測試集數據集處理
        test_encoding = []
        test_raw_txt = []
        for idx in range(len(test_json)):
            title = test_json.iloc[idx]['title']
            doc_text = test_json.iloc[idx]['doc_text']
            query = test_json.iloc[idx]['query']
            doc_sentence = test_json.iloc[idx]['doc_sentence']

            for sentence in set(doc_sentence):
                if len(query + sentence) > 512:
                    continue
                
                encoding = tokenizer.encode(query, sentence, max_seq_len=512, return_length=True
                    return_position_ids=True, pad_to_max_seq_len=True, return_attention_mask=True)

                test_encoding.append(encoding)
                test_raw_txt.append(
                    [idx, query, sentence]
                )

        步驟5:批量數據讀取

        # 手動將數據集進行批量打包
        def data_generator(data_encoding, batch_size = 6):
            for idx in range(len(data_encoding) // batch_size):
                batch_data = data_encoding[idx * batch_size : (idx+1) * batch_size]
                batch_encoding = {}
                for key in batch_data[0].keys():
                    if key == 'seq_len':
                        continue
                    batch_encoding[key] = paddle.to_tensor(np.array([x[key] for x in batch_data]))
                
                yield batch_encoding

        步驟6:模型訓練與驗證

        # 優(yōu)化器
        optimizer = paddle.optimizer.SGD(0.0005, parameters=model.parameters())

        # 損失函數
        loss_fct = paddle.nn.CrossEntropyLoss()
        best_val_start_acc = 0

        for epoch in range(10):
            # 每次打亂訓練集,防止過擬合
            np.random.shuffle(train_encoding)
            
            # 訓練部分
            train_loss = []
            for batch_encoding in data_generator(train_encoding, 10):

                # ERNIE正向傳播
                start_logits, end_logits = model(batch_encoding['input_ids'], batch_encoding['token_type_ids'])

                # 計算損失
                start_loss = loss_fct(start_logits, batch_encoding['start_positions'])
                end_loss = loss_fct(end_logits, batch_encoding['end_positions'])
                total_loss = (start_loss + end_loss) / 2
                
                # 參數更新
                total_loss.backward()
                train_loss.append(total_loss)
                optimizer.step()
                optimizer.clear_gradients()
            
            # 驗證部分
            val_start_acc = []
            val_end_acc = []
            with paddle.no_grad():
                for batch_encoding in data_generator(val_encoding, 10):

                    # ERNIE正向傳播
                    start_logits, end_logits = model(batch_encoding['input_ids'], batch_encoding['token_type_ids'])

                    # 計算識別精度
                    start_acc = paddle.mean((start_logits.argmax(1) == batch_encoding['start_positions']).astype(float))
                    end_acc = paddle.mean((end_logits.argmax(1) == batch_encoding['end_positions']).astype(float))

                    val_start_acc.append(start_acc)
                    val_end_acc.append(end_acc)

            # 轉換數據格式為float
            train_loss = paddle.to_tensor(train_loss).mean().item()
            val_start_acc = paddle.to_tensor(val_start_acc).mean().item()
            val_end_acc = paddle.to_tensor(val_end_acc).mean().item()
            
            # 存儲最優(yōu)模型
            if val_start_acc > best_val_start_acc:
                paddle.save(model.state_dict(), 'model.pkl')
                best_val_start_acc = val_start_acc
            
            # 每個epoch打印輸出結果
            print(f'Epoch {epoch}{train_loss:3f}{val_start_acc:3f}/{val_end_acc:3f}')
        # 關閉dropout
        model = model.eval()

        步驟7:模型預測

        test_start_idx = []
        test_end_idx = []

        # 對測試集中query 和 sentence的情況進行預測
        with paddle.no_grad():
            for batch_encoding in data_generator(test_encoding, 12):
                start_logits, end_logits = model(batch_encoding['input_ids'], batch_encoding['token_type_ids'])
                
                test_start_idx += start_logits.argmax(1).tolist()
                test_end_idx += end_logits.argmax(1).tolist()
                
                if len(test_start_idx) % 500 == 0:
                    print(len(test_start_idx), len(test_encoding))
        test_submit = [''] * len(test_json)

        # 對預測結果進行后處理
        for (idx, query, sentence), st_idx, end_idx in zip(test_raw_txt, test_start_idx, test_end_idx):

            # 如果start 或 end位置識別失敗,或 start位置 晚于 end位置
            if st_idx == 0 or end_idx == 0 or st_idx >= end_idx:
                continue
            
            # 如果start位置在query部分
            if st_idx - len(query) - 2 < 0:
                continue
            
            test_submit[idx] += sentence[st_idx - len(query) - 2: end_idx - len(query) - 2]
        # 生成提交結果
        with open('subtask1_test_pred.txt''w'as up:
            for x in test_submit:
                if x == '':
                    up.write('1\tNoAnswer\n')
                else:
                    up.write('1\t'+x+'\n')

        改進方向

        從精度改變大小,可以從以下幾個角度改進:訓練數據 > 數據處理 > 模型與預訓練 > 模型集成

        • 訓練數據:使用全量的訓練數據
        • 數據處理:對文檔進行切分,現在使用。進行切分,后續(xù)也可以嘗試其他。
        • 模型與預處理:嘗試ERNIE版本,或者進行預訓練。
        • 模型集成:
          • 嘗試不同的數據劃分得到不同的模型
          • 嘗試不同的文本處理方法得到不同的模型

        當然也可以考慮其他數據,如不同的網頁擁有答案的概率不同,以及從標題可以判斷是否包含答案。

        完整代碼也可以點擊左下角“原文鏈接”進行查看。

        整理不易,三連

        瀏覽 50
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 操逼挺好看看 | 天天操夜夜爽 | 欧美乱人伦 | 国产精品久久久久久久久午夜福利 | 黄色肉文视频 |