【CLUE benchmark】基于CMRC2018的机器阅读理解

资源

⭐ ⭐ ⭐ 欢迎点个小小的Star支持!⭐ ⭐ ⭐

开源不易,希望大家多多支持~

一、背景介绍

这是Paddle版本的CLUE benchmark,旨在为用户提供Paddle版本的benchmark进行学习和交流,该版本提供了bert,ernie,roberta-wwm三个版本的基线。CLUE官网的链接为:https://www.cluebenchmarks.com/

二、数据预处理

在运行程序之前,首先导入需要的第三方库包

import paddlenlp as ppnlp
from utils import prepare_train_features, prepare_validation_features
from functools import partial
from paddlenlp.metrics.squad import squad_evaluate, compute_prediction
import paddle
from paddlenlp.data import Stack, Dict, Pad
import collections
import time
import json
from paddlenlp.datasets import DatasetBuilder
import inspect
import os

解压数据集

!unzip -o data/data116454/cmrc2018_public.zip -d data/
Archive:  data/data116454/cmrc2018_public.zip
  inflating: data/trial.json         
  inflating: data/train.json         
  inflating: data/test.json          
  inflating: data/dev.json           

CMRC是一个中文阅读理解数据集,由人类专家在Wikipedia段落上注释的近20,000个真实问题组成。训练集有13021条,验证集有3351条,测试集有4895条。下面我们来看其中的一条数据:

  "data": [
    {
      "paragraphs": [
        {
          "id": "TRAIN_186", 
          "context": "范廷颂枢机(,),圣名保禄·若瑟(),是越南罗马天主教枢机。1963年被任为主教;1990年被擢升为天主教河内总教区宗座署理;1994年被擢升为总主教,同年年底被擢升为枢机;2009年2月离世。范廷颂于1919年6月15日在越南宁平省天主教发艳教区出生;童年时接受良好教育后,被一位越南神父带到河内继续其学业。范廷颂于1940年在河内大修道院完成神学学业。范廷颂于1949年6月6日在河内的主教座堂晋铎;及后被派到圣女小德兰孤儿院服务。1950年代,范廷颂在河内堂区创建移民接待中心以收容到河内避战的难民。1954年,法越战争结束,越南民主共和国建都河内,当时很多天主教神职人员逃至越南的南方,但范廷颂仍然留在河内。翌年管理圣若望小修院;惟在1960年因捍卫修院的自由、自治及拒绝政府在修院设政治课的要求而被捕。1963年4月5日,教宗任命范廷颂为天主教北宁教区主教,同年8月15日就任;其牧铭为「我信天主的爱」。由于范廷颂被越南政府软禁差不多30年,因此他无法到所属堂区进行牧灵工作而专注研读等工作。范廷颂除了面对战争、贫困、被当局迫害天主教会等问题外,也秘密恢复修院、创建女修会团体等。1990年,教宗若望保禄二世在同年6月18日擢升范廷颂为天主教河内总教区宗座署理以填补该教区总主教的空缺。1994年3月23日,范廷颂被教宗若望保禄二世擢升为天主教河内总教区总主教并兼天主教谅山教区宗座署理;同年11月26日,若望保禄二世擢升范廷颂为枢机。范廷颂在1995年至2001年期间出任天主教越南主教团主席。2003年4月26日,教宗若望保禄二世任命天主教谅山教区兼天主教高平教区吴光杰主教为天主教河内总教区署理主教;及至2005年2月19日,范廷颂因获批辞去总主教职务而荣休;吴光杰同日真除天主教河内总教区总主教职务。范廷颂于2009年2月22日清晨在河内离世,享年89岁;其葬礼于同月26日上午在天主教河内总教区总主教座堂举行。", 
          "qas": [
            {
              "question": "范廷颂是什么时候被任为主教的?", 
              "id": "TRAIN_186_QUERY_0", 
              "answers": [
                {
                  "text": "1963年", 
                  "answer_start": 30
                }
              ]
            },

其中上下文context为:

"范廷颂枢机(,),圣名保禄·若瑟(),是越南罗马天主教枢机。1963年被任为主教;1990年被擢升为天主教河内总教区宗座署理;1994年被擢升为总主教,同年年底被擢升为枢机;2009年2月离世。范廷颂于1919年6月15日在越南宁平省天主教发艳教区出生;童年时接受良好教育后,被一位越南神父带到河内继续其学业。范廷颂于1940年在河内大修道院完成神学学业。范廷颂于1949年6月6日在河内的主教座堂晋铎;及后被派到圣女小德兰孤儿院服务。1950年代,范廷颂在河内堂区创建移民接待中心以收容到河内避战的难民。1954年,法越战争结束,越南民主共和国建都河内,当时很多天主教神职人员逃至越南的南方,但范廷颂仍然留在河内。翌年管理圣若望小修院;惟在1960年因捍卫修院的自由、自治及拒绝政府在修院设政治课的要求而被捕。1963年4月5日,教宗任命范廷颂为天主教北宁教区主教,同年8月15日就任;其牧铭为「我信天主的爱」。由于范廷颂被越南政府软禁差不多30年,因此他无法到所属堂区进行牧灵工作而专注研读等工作。范廷颂除了面对战争、贫困、被当局迫害天主教会等问题外,也秘密恢复修院、创建女修会团体等。1990年,教宗若望保禄二世在同年6月18日擢升范廷颂为天主教河内总教区宗座署理以填补该教区总主教的空缺。1994年3月23日,范廷颂被教宗若望保禄二世擢升为天主教河内总教区总主教并兼天主教谅山教区宗座署理;同年11月26日,若望保禄二世擢升范廷颂为枢机。范廷颂在1995年至2001年期间出任天主教越南主教团主席。2003年4月26日,教宗若望保禄二世任命天主教谅山教区兼天主教高平教区吴光杰主教为天主教河内总教区署理主教;及至2005年2月19日,范廷颂因获批辞去总主教职务而荣休;吴光杰同日真除天主教河内总教区总主教职务。范廷颂于2009年2月22日清晨在河内离世,享年89岁;其葬礼于同月26日上午在天主教河内总教区总主教座堂举行。"

对应的id为TRAIN_186,这条文本的问题是:

范廷颂是什么时候被任为主教的?

TRAIN_186_QUERY_0表示的是该问句的id

答案是:

1963年

answer_start表示的是答案在原文中的起始位置。

下面定义CMRC2018数据集的类,用于加载CRMR数据集。

class CMRC2018(DatasetBuilder):
    '''
    This dataset is a Span-Extraction dataset for Chinese machine reading 
    comprehension. The dataset is composed by near 20,000 real questions 
    annotated on Wikipedia paragraphs by human experts.
    '''

    SPLITS = {
        'train': 'train.json',
        'dev': 'dev.json',
        'test': 'test.json'
    }

    def _get_data(self, mode, **kwargs):
        default_root = 'data'
        filename = self.SPLITS[mode]
        fullname = os.path.join(default_root, filename)

        return fullname

    def _read(self, filename, *args):
        with open(filename, "r", encoding="utf8") as f:
            input_data = json.load(f)["data"]
        for entry in input_data:
            title = entry.get("title", "").strip()
            for paragraph in entry["paragraphs"]:
                context = paragraph["context"].strip()
                for qa in paragraph["qas"]:
                    qas_id = qa["id"]
                    question = qa["question"].strip()
                    answer_starts = [
                        answer["answer_start"]
                        for answer in qa.get("answers", [])
                    ]
                    answers = [
                        answer["text"].strip()
                        for answer in qa.get("answers", [])
                    ]

                    yield {
                        'id': qas_id,
                        'title': title,
                        'context': context,
                        'question': question,
                        'answers': answers,
                        'answer_starts': answer_starts
                    }

定义load_dataset函数,用于加载数据集

def load_dataset(path_or_read_func,
                 name=None,
                 data_files=None,
                 splits=None,
                 lazy=None,
                 **kwargs):
   
    reader_cls = CMRC2018
    print(reader_cls)
    if not name:
        reader_instance = reader_cls(lazy=lazy, **kwargs)
    else:
        reader_instance = reader_cls(lazy=lazy, name=name, **kwargs)

    datasets = reader_instance.read_datasets(data_files=data_files, splits=splits)
    return datasets

加载cmrc数据集

train_ds, dev_ds,test_ds = load_dataset('cmrc2018', splits=('train', 'dev','test'))
# 打印2条训练集
for idx in range(2):
    print(train_ds[idx]['question'])
    print(train_ds[idx]['context'])
    print(train_ds[idx]['answers'])
    print(train_ds[idx]['answer_starts'])
    print()
<class '__main__.CMRC2018'>
范廷颂是什么时候被任为主教的?
范廷颂枢机(,),圣名保禄·若瑟(),是越南罗马天主教枢机。1963年被任为主教;1990年被擢升为天主教河内总教区宗座署理;1994年被擢升为总主教,同年年底被擢升为枢机;2009年2月离世。范廷颂于1919年6月15日在越南宁平省天主教发艳教区出生;童年时接受良好教育后,被一位越南神父带到河内继续其学业。范廷颂于1940年在河内大修道院完成神学学业。范廷颂于1949年6月6日在河内的主教座堂晋铎;及后被派到圣女小德兰孤儿院服务。1950年代,范廷颂在河内堂区创建移民接待中心以收容到河内避战的难民。1954年,法越战争结束,越南民主共和国建都河内,当时很多天主教神职人员逃至越南的南方,但范廷颂仍然留在河内。翌年管理圣若望小修院;惟在1960年因捍卫修院的自由、自治及拒绝政府在修院设政治课的要求而被捕。1963年4月5日,教宗任命范廷颂为天主教北宁教区主教,同年8月15日就任;其牧铭为「我信天主的爱」。由于范廷颂被越南政府软禁差不多30年,因此他无法到所属堂区进行牧灵工作而专注研读等工作。范廷颂除了面对战争、贫困、被当局迫害天主教会等问题外,也秘密恢复修院、创建女修会团体等。1990年,教宗若望保禄二世在同年6月18日擢升范廷颂为天主教河内总教区宗座署理以填补该教区总主教的空缺。1994年3月23日,范廷颂被教宗若望保禄二世擢升为天主教河内总教区总主教并兼天主教谅山教区宗座署理;同年11月26日,若望保禄二世擢升范廷颂为枢机。范廷颂在1995年至2001年期间出任天主教越南主教团主席。2003年4月26日,教宗若望保禄二世任命天主教谅山教区兼天主教高平教区吴光杰主教为天主教河内总教区署理主教;及至2005年2月19日,范廷颂因获批辞去总主教职务而荣休;吴光杰同日真除天主教河内总教区总主教职务。范廷颂于2009年2月22日清晨在河内离世,享年89岁;其葬礼于同月26日上午在天主教河内总教区总主教座堂举行。
['1963年']
[30]

1990年,范廷颂担任什么职务?
范廷颂枢机(,),圣名保禄·若瑟(),是越南罗马天主教枢机。1963年被任为主教;1990年被擢升为天主教河内总教区宗座署理;1994年被擢升为总主教,同年年底被擢升为枢机;2009年2月离世。范廷颂于1919年6月15日在越南宁平省天主教发艳教区出生;童年时接受良好教育后,被一位越南神父带到河内继续其学业。范廷颂于1940年在河内大修道院完成神学学业。范廷颂于1949年6月6日在河内的主教座堂晋铎;及后被派到圣女小德兰孤儿院服务。1950年代,范廷颂在河内堂区创建移民接待中心以收容到河内避战的难民。1954年,法越战争结束,越南民主共和国建都河内,当时很多天主教神职人员逃至越南的南方,但范廷颂仍然留在河内。翌年管理圣若望小修院;惟在1960年因捍卫修院的自由、自治及拒绝政府在修院设政治课的要求而被捕。1963年4月5日,教宗任命范廷颂为天主教北宁教区主教,同年8月15日就任;其牧铭为「我信天主的爱」。由于范廷颂被越南政府软禁差不多30年,因此他无法到所属堂区进行牧灵工作而专注研读等工作。范廷颂除了面对战争、贫困、被当局迫害天主教会等问题外,也秘密恢复修院、创建女修会团体等。1990年,教宗若望保禄二世在同年6月18日擢升范廷颂为天主教河内总教区宗座署理以填补该教区总主教的空缺。1994年3月23日,范廷颂被教宗若望保禄二世擢升为天主教河内总教区总主教并兼天主教谅山教区宗座署理;同年11月26日,若望保禄二世擢升范廷颂为枢机。范廷颂在1995年至2001年期间出任天主教越南主教团主席。2003年4月26日,教宗若望保禄二世任命天主教谅山教区兼天主教高平教区吴光杰主教为天主教河内总教区署理主教;及至2005年2月19日,范廷颂因获批辞去总主教职务而荣休;吴光杰同日真除天主教河内总教区总主教职务。范廷颂于2009年2月22日清晨在河内离世,享年89岁;其葬礼于同月26日上午在天主教河内总教区总主教座堂举行。
['1990年被擢升为天主教河内总教区宗座署理']
[41]
# 打印2条测试集
for idx in range(2):
    print(test_ds[idx]['question'])
    print(test_ds[idx]['context'])
    print(test_ds[idx]['answers'])
    print(test_ds[idx]['answer_starts'])
    print()
罗亚尔港号是什么级别的导弹巡洋舰?
罗亚尔港号(USS Port Royal CG-73)是美国海军提康德罗加级导弹巡洋舰,是该级巡洋舰的第27艘也是最后一艘。它也是美国海军第二艘以皇家港为名字命名的军舰。第一艘是1862年下水、曾参与南北战争的。船名来自曾在美国独立战争和南北战争中均发生过海战的南卡罗来纳州(Port Royal Sound)。美国海军在1988年2月25日订购该船,1991年10月18日在密西西比州帕斯卡古拉河畔的英戈尔斯造船厂放置龙骨。1992年11月20日下水,1992年12月5日由苏珊·贝克(Susan G. Baker,老布什政府时期的白宫办公厅主任,也是前国务卿詹姆斯·贝克的夫人)为其命名,1994年7月9日正式服役。2009年2月5日,罗亚尔港号巡洋舰在位于檀香山国际机场以南0.5英里的一处珊瑚礁上发生搁浅,之前该舰刚完成在旱坞内的维护,正在进行维护后的第一次海试。2009年2月9日凌晨2点,罗亚尔港号被脱离珊瑚礁。无人在这次事故中受伤,也未发生船上燃料的泄漏。但由于这次搁浅,罗亚尔港号巡洋舰不得不回到旱坞重新进行维修。1995年12月加入尼米兹号为核心的航空母舰战斗群,参与了南方守望行动,这是罗亚尔港号巡洋舰首次参与的军事部署行动。1996年3月由于台湾海峡导弹危机的发生被部署到了南中国海,随着危机的结束,1997年9月至1998年3月回到尼米兹号航空母舰战斗群参与南方守望行动。后随约翰·C·斯坦尼斯号航空母舰战斗群继续参加南方守望行动。2000年1月由于多次追击涉嫌违反联合国禁运制裁走私偷运伊拉克原油的船只因而造成对船上动力设备的持续性机械磨损而撤离,回到夏威夷进行整修和升级。2001年11月7日加入约翰·C·斯坦尼斯号航空母舰战斗群参与旨在对基地组织和对它进行庇护的阿富汗塔利班政权进行打击的持久自由军事行动。
['FAKE_ANSWER_1', 'FAKE_ANSWER_2', 'FAKE_ANSWER_3']
[-1, -1, -1]

罗亚尔港号是美国第几艘以皇家港为名字命名的军舰?
罗亚尔港号(USS Port Royal CG-73)是美国海军提康德罗加级导弹巡洋舰,是该级巡洋舰的第27艘也是最后一艘。它也是美国海军第二艘以皇家港为名字命名的军舰。第一艘是1862年下水、曾参与南北战争的。船名来自曾在美国独立战争和南北战争中均发生过海战的南卡罗来纳州(Port Royal Sound)。美国海军在1988年2月25日订购该船,1991年10月18日在密西西比州帕斯卡古拉河畔的英戈尔斯造船厂放置龙骨。1992年11月20日下水,1992年12月5日由苏珊·贝克(Susan G. Baker,老布什政府时期的白宫办公厅主任,也是前国务卿詹姆斯·贝克的夫人)为其命名,1994年7月9日正式服役。2009年2月5日,罗亚尔港号巡洋舰在位于檀香山国际机场以南0.5英里的一处珊瑚礁上发生搁浅,之前该舰刚完成在旱坞内的维护,正在进行维护后的第一次海试。2009年2月9日凌晨2点,罗亚尔港号被脱离珊瑚礁。无人在这次事故中受伤,也未发生船上燃料的泄漏。但由于这次搁浅,罗亚尔港号巡洋舰不得不回到旱坞重新进行维修。1995年12月加入尼米兹号为核心的航空母舰战斗群,参与了南方守望行动,这是罗亚尔港号巡洋舰首次参与的军事部署行动。1996年3月由于台湾海峡导弹危机的发生被部署到了南中国海,随着危机的结束,1997年9月至1998年3月回到尼米兹号航空母舰战斗群参与南方守望行动。后随约翰·C·斯坦尼斯号航空母舰战斗群继续参加南方守望行动。2000年1月由于多次追击涉嫌违反联合国禁运制裁走私偷运伊拉克原油的船只因而造成对船上动力设备的持续性机械磨损而撤离,回到夏威夷进行整修和升级。2001年11月7日加入约翰·C·斯坦尼斯号航空母舰战斗群参与旨在对基地组织和对它进行庇护的阿富汗塔利班政权进行打击的持久自由军事行动。
['FAKE_ANSWER_1', 'FAKE_ANSWER_2', 'FAKE_ANSWER_3']
[-1, -1, -1]
# MODEL_NAME = "bert-base-chinese"
# 提供了三个模型的基线,可以根据情况进行选择
MODEL_NAME = "ernie-1.0"
if(MODEL_NAME=="bert-base-chinese"):
    tokenizer = ppnlp.transformers.BertTokenizer.from_pretrained(MODEL_NAME)
elif(MODEL_NAME=="roberta-wwm-ext"):
    tokenizer=ppnlp.transformers.RobertaTokenizer.from_pretrained(MODEL_NAME)
elif(MODEL_NAME=="ernie-1.0"):
    tokenizer=ppnlp.transformers.ErnieTokenizer.from_pretrained(MODEL_NAME)
[2022-01-24 20:11:12,587] [    INFO] - Downloading https://paddlenlp.bj.bcebos.com/models/transformers/ernie/vocab.txt and saved to /home/aistudio/.paddlenlp/models/ernie-1.0
[2022-01-24 20:11:12,589] [    INFO] - Downloading vocab.txt from https://paddlenlp.bj.bcebos.com/models/transformers/ernie/vocab.txt
100%|██████████| 90/90 [00:00<00:00, 2302.72it/s]

设置模型使用的超参数

max_seq_length = 512
doc_stride = 128

train_trans_func = partial(prepare_train_features, 
                           max_seq_length=max_seq_length, 
                           doc_stride=doc_stride,
                           tokenizer=tokenizer)

train_ds.map(train_trans_func, batched=True)

dev_trans_func = partial(prepare_validation_features, 
                           max_seq_length=max_seq_length, 
                           doc_stride=doc_stride,
                           tokenizer=tokenizer)
                           
dev_ds.map(dev_trans_func, batched=True)

test_ds.map(dev_trans_func, batched=True)
<paddlenlp.datasets.dataset.MapDataset at 0x7f60a7923c10>

构建数据的dataloader,用于模型获取源源不断的batch数据

batch_size = 32

train_batch_sampler = paddle.io.DistributedBatchSampler(
        train_ds, batch_size=batch_size, shuffle=True)

train_batchify_fn = lambda samples, fn=Dict({
    "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),
    "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
    "start_positions": Stack(dtype="int64"),
    "end_positions": Stack(dtype="int64")
}): fn(samples)

train_data_loader = paddle.io.DataLoader(
    dataset=train_ds,
    batch_sampler=train_batch_sampler,
    collate_fn=train_batchify_fn,
    return_list=True)

dev_batch_sampler = paddle.io.BatchSampler(
    dev_ds, batch_size=batch_size, shuffle=False)

dev_batchify_fn = lambda samples, fn=Dict({
    "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),
    "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id)
}): fn(samples)

test_batch_sampler = paddle.io.BatchSampler(
    test_ds, batch_size=batch_size, shuffle=False)

dev_data_loader = paddle.io.DataLoader(
    dataset=dev_ds,
    batch_sampler=dev_batch_sampler,
    collate_fn=dev_batchify_fn,
    return_list=True)

test_data_loader = paddle.io.DataLoader(
    dataset=test_ds,
    batch_sampler=test_batch_sampler,
    collate_fn=dev_batchify_fn,
    return_list=True)

三、模型构建

可以选择bert,roberta,ernie等模型,更多的预训练模型请参考链接:https://paddlenlp.readthedocs.io/zh/latest/model_zoo/transformers.html

抽取式阅读理解的原理示意图如下图,模型的输入是context和question两个,然后经过BERT进行特征提取,然后获得BERT的输出,连接一个全连接层,利用softmax函数得到开始位置(Output start)和结束位置(Output end)。具体地,BERT输出的序列中,每个token向量的维度是768维,序列的长度是N,经过全连接层之后,得到 O N × 2 = F C ( T N × 768 ) O^{N \times 2} = FC(T^{N \times 768}) ON×2=FC(TN×768) ,其中FC表示的是全连接层, O N × 2 O^{N\times 2} ON×2 为每一个token分别作为答案开头和结尾的logit值,再经过Softmax层之后就得到了相应的概率值。

# 设置想要使用模型的名称
if(MODEL_NAME=="bert-base-chinese"):
    model = ppnlp.transformers.BertForQuestionAnswering.from_pretrained(MODEL_NAME)
elif(MODEL_NAME=="roberta-wwm-ext"):
    model=ppnlp.transformers.RobertaForQuestionAnswering.from_pretrained(MODEL_NAME)
elif(MODEL_NAME=="ernie-1.0"):
    model=ppnlp.transformers.ErnieForQuestionAnswering.from_pretrained(MODEL_NAME)
[2022-01-24 20:13:15,116] [    INFO] - Downloading https://paddlenlp.bj.bcebos.com/models/transformers/ernie/ernie_v1_chn_base.pdparams and saved to /home/aistudio/.paddlenlp/models/ernie-1.0
[2022-01-24 20:13:15,120] [    INFO] - Downloading ernie_v1_chn_base.pdparams from https://paddlenlp.bj.bcebos.com/models/transformers/ernie/ernie_v1_chn_base.pdparams
100%|██████████| 392507/392507 [00:10<00:00, 38156.03it/s]
W0124 20:13:25.550086  5796 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0124 20:13:25.558058  5796 device_context.cc:465] device: 0, cuDNN Version: 7.6.

定义阅读理解的损失函数,该损失函数包含start和end_loss,然后取平均。

class CrossEntropyLossForSQuAD(paddle.nn.Layer):
    def __init__(self):
        super(CrossEntropyLossForSQuAD, self).__init__()

    def forward(self, y, label):
        start_logits, end_logits = y   # both shape are [batch_size, seq_len]
        start_position, end_position = label
        start_position = paddle.unsqueeze(start_position, axis=-1)
        end_position = paddle.unsqueeze(end_position, axis=-1)
        start_loss = paddle.nn.functional.softmax_with_cross_entropy(
            logits=start_logits, label=start_position, soft_label=False)
        start_loss = paddle.mean(start_loss)
        end_loss = paddle.nn.functional.softmax_with_cross_entropy(
            logits=end_logits, label=end_position, soft_label=False)
        end_loss = paddle.mean(end_loss)

        loss = (start_loss + end_loss) / 2
        return loss

四、模型配置

# 训练过程中的最大学习率
learning_rate = 3e-5 
# 训练轮次
epochs = 3
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.01

num_training_steps = len(train_data_loader) * epochs
lr_scheduler = ppnlp.transformers.LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_proportion)

# Generate parameter names needed to perform weight decay.
# All bias and LayerNorm parameters are excluded.
decay_params = [
    p.name for n, p in model.named_parameters()
    if not any(nd in n for nd in ["bias", "norm"])
]
optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=weight_decay,
    apply_decay_param_fun=lambda x: x in decay_params)

criterion = CrossEntropyLossForSQuAD()

五、模型训练

在模型训练的过程中需要进行交叉验证,所以首先实现evaluate函数。

@paddle.no_grad()
def evaluate(model, data_loader):
    model.eval()

    all_start_logits = []
    all_end_logits = []
    tic_eval = time.time()

    for batch in data_loader:
        input_ids, token_type_ids = batch
        start_logits_tensor, end_logits_tensor = model(input_ids,
                                                       token_type_ids)

        for idx in range(start_logits_tensor.shape[0]):
            if len(all_start_logits) % 1000 == 0 and len(all_start_logits):
                print("Processing example: %d" % len(all_start_logits))
                print('time per 1000:', time.time() - tic_eval)
                tic_eval = time.time()

            all_start_logits.append(start_logits_tensor.numpy()[idx])
            all_end_logits.append(end_logits_tensor.numpy()[idx])

    all_predictions, _, _ = compute_prediction(
        data_loader.dataset.data, data_loader.dataset.new_data,
        (all_start_logits, all_end_logits), False, 20, 30)
    squad_evaluate(
        examples=data_loader.dataset.data,
        preds=all_predictions,
        is_whitespace_splited=False)
    
    model.train()

下面是模型的训练过程


global_step = 0
for epoch in range(1, epochs + 1):
    for step, batch in enumerate(train_data_loader, start=1):
        global_step += 1
        input_ids, segment_ids, start_positions, end_positions = batch
        logits = model(input_ids=input_ids, token_type_ids=segment_ids)
        loss = criterion(logits, (start_positions, end_positions))

        if global_step % 100 == 0 :
            print("global step %d, epoch: %d, batch: %d, loss: %.5f" % (global_step, epoch, step, loss))
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.clear_grad()

    evaluate(model=model, data_loader=dev_data_loader) 

model.save_pretrained('./checkpoint')
tokenizer.save_pretrained('./checkpoint')
global step 100, epoch: 1, batch: 100, loss: 2.56097
global step 200, epoch: 1, batch: 200, loss: 2.41449
global step 300, epoch: 1, batch: 300, loss: 1.83983
global step 400, epoch: 1, batch: 400, loss: 1.65969
global step 500, epoch: 1, batch: 500, loss: 1.88513
Processing example: 1000
time per 1000: 11.05217695236206
Processing example: 2000
time per 1000: 10.466019630432129
Processing example: 3000
time per 1000: 10.451993942260742
Processing example: 4000
time per 1000: 10.789400815963745
Processing example: 5000
time per 1000: 10.473366975784302
{
  "exact": 60.11183597390494,
  "f1": 84.66431373532029,
  "total": 3219,
  "HasAns_exact": 60.11183597390494,
  "HasAns_f1": 84.66431373532029,
  "HasAns_total": 3219
}
global step 600, epoch: 2, batch: 68, loss: 0.89370
global step 700, epoch: 2, batch: 168, loss: 1.15915

六、模型预测

模型预测过程加载测试集合,然后预测输出保存到json文件中。

@paddle.no_grad()
def do_predict(model, data_loader):
    model.eval()

    all_start_logits = []
    all_end_logits = []
    tic_eval = time.time()

    for batch in data_loader:
        input_ids, token_type_ids = batch
        start_logits_tensor, end_logits_tensor = model(input_ids,
                                                       token_type_ids)

        for idx in range(start_logits_tensor.shape[0]):
            if len(all_start_logits) % 1000 == 0 and len(all_start_logits):
                print("Processing example: %d" % len(all_start_logits))
                print('time per 1000:', time.time() - tic_eval)
                tic_eval = time.time()

            all_start_logits.append(start_logits_tensor.numpy()[idx])
            all_end_logits.append(end_logits_tensor.numpy()[idx])

    all_predictions, _, _ = compute_prediction(
        data_loader.dataset.data, data_loader.dataset.new_data,
        (all_start_logits, all_end_logits), False, 20, 30)


    # Can also write all_nbest_json and scores_diff_json files if needed
    with open('cmrc2018_predict.json', "w", encoding='utf-8') as writer:
        writer.write(
            json.dumps(
                all_predictions, ensure_ascii=False, indent=4) + "\n")
    
    model.train()
do_predict(model, test_data_loader)

预测结束后,就可以把预测的cmrc2018_predict.json下载到本地,然后压缩成压缩包,上传到CLUE网站上就可以进行评测了。

七、更多PaddleEdu信息内容

1. PaddleEdu一站式深度学习在线百科awesome-DeepLearning中还有其他的能力,大家可以敬请期待:

  • 深度学习入门课
  • 深度学习百问
  • 特色课
  • 产业实践

PaddleEdu使用过程中有任何问题欢迎在awesome-DeepLearning提issue,同时更多深度学习资料请参阅飞桨深度学习平台

记得点个Star⭐收藏噢~~

2. 飞桨PaddleEdu技术交流群(QQ)

目前QQ群已有2000+同学一起学习,欢迎扫码加入

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐