• STI比赛任务一:【智能问答baseline】


    比赛简介

    百度搜索首届技术创新挑战赛:赛道一 答案抽取

    欢迎大家关注公众号ChallengeHub,获取更多比赛资料

    比赛链接:https://aistudio.baidu.com/aistudio/competition/detail/660/0/task-definition

    任务定义

    本赛题任务是:给定一个用户搜索问题集合Q,基于每个搜索问题q,给定搜索引擎检索得到的网页文档集合Dq,其中包括最多40个网页文档。针对每个q-d对,要求参评系统从d中抽取能够回答q的答案片段a。

    问题query:渝北区面积
    
    篇章doc_text:重庆主城九区面积 【导语】:重庆市主城九区面积为:4779平方千米。其中渝中区面积为23....	
    
    答案answer:[渝北区幅员面积1452平方公里, 2、渝北区面积: 全区幅员面积1452平方公里。] [53, 225]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    数据集

    训练集包含约900个query、30000个query-document对;验证集和测试集各包含约100个 query,3000个query-document对。数据的主要特点为:

    文档长度普遍较长,质量参差不齐,内部往往包含大量噪声 句子级别答案片段,通常由包含完整上下文的若干句子组成 标注数据只保证答案片段与搜索问题间的相关性,不保证正确性,且存在不包含答案的文档 数据样例,文档长度分布如下:
    [站外图片上传中…(image-4276fb-1668574875215)]

    问题q:备孕偶尔喝冰的可以吗
    
    篇章d:备孕能吃冷的食物吗 炎热的夏天让很多人都觉得闷热...,下面一起来看看吧! 备孕能吃冷的食物吗 在中医养生中,女性体质属阴,不可以贪凉。吃了过多寒凉、生冷的食物后,会消耗阳气,导致寒邪内生,侵害子宫。另外,宫寒是肾阳虚的表现,不会直接导致不孕。但宫寒会引起妇科疾病,所以也不可不防。因此处于备孕期的女性最好不要吃冷的食物。 备孕食谱有哪些 ...
    
    答案a:在中医养生中,女性体质属阴,不可以贪凉。吃了过多寒凉、生冷的食物后,会消耗阳气,导致寒邪内生,侵害子宫。另外,宫寒是肾阳虚的表现,不会直接导致不孕。但宫寒会引起妇科疾病,所以也不可不防。因此处于备孕期的女性最好不要吃冷的食物。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    评价指标

    计算基于每个query-document对模型预测答案与标注答案间字粒度的准确、召回、F1值,取所有测试数据的平均F1作为最终指标。对于不包含答案的文档,其答案可看做一个特殊token【无答案】,若模型预测出答案,其F1为0,若模型预测【无答案】,其F1

    智能问答Ernie3.0基线

    项目地址:https://aistudio.baidu.com/aistudio/projectdetail/5043272

    导入包

    本基线需要基于最新版本的paddlenlp运行,安装命令如下:

    !python -m pip install paddlepaddle-gpu==2.3.0.post101 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html
    !pip install --upgrade paddlenlp
    
    • 1
    • 2
    
    import pandas as pd
    import paddle
    from paddlenlp.data import Stack, Dict, Pad
    import paddlenlp
    from paddlenlp.datasets import load_dataset
    from functools import partial
    import collections
    import time
    import json
    import paddle
    from paddlenlp.metrics.squad import squad_evaluate, compute_prediction
    import pyarrow as pa
    import pyarrow.dataset as ds
    import pandas as pd
    from datasets import Datase
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    解压数据

    !ls /home/aistudio/data/data174963
    
    # 将比赛数据解压到work目录下
    !if [ -d "work/data_task1/" ];then rm -f -r work/data_task1/;fi
    !tar -zxvf /home/aistudio/data/data174963/data_task1.tar.gz -C /home/aistudio/work/
    !ls work/data_task1/
    !tree work/data_task1/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    加载自定义数据集

    paddlenlp底层load_dataset返回的是hf数据格式,我们可以直接基于DataFrame创建pyarrow数据:

    
    train_examples = Dataset(pa.Table.from_pandas(train))
    dev_examples = Dataset(pa.Table.from_pandas(dev))
    train_examples,dev_examples
    
    • 1
    • 2
    • 3
    • 4

    [站外图片上传中…(image-7d4f9b-1668574875215)]

    利用tokenizer处理数据

    由于文章加问题的文本长度可能大于max_seq_length,答案出现的位置有可能出现在文章最后,所以不能简单的对文章进行截断。
    
    那么对于过长的文章,则采用滑动窗口将文章分成多段,分别与问题组合。再用对应的tokenizer转化为模型可接受的feature。doc_stride参数就是每次滑动的距离。滑动窗口生成InputFeature的过程如下图:
    
    • 1
    • 2
    • 3

    [站外图片上传中…(image-10dce0-1668574875215)]

    from functools import partial
    
    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)
    
    
    dev_trans_func = partial(prepare_validation_features, 
                               max_seq_length=max_seq_length, 
                               doc_stride=doc_stride,
                               tokenizer=tokenizer)
        
    column_names = train_examples.column_names
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    调用map()方法批量处理数据

    由于我们传入了lazy=False,所以我们使用load_dataset()自定义的数据集是MapDataset对象。MapDataset是paddle.io.Dataset的功能增强版本。其内置的map()方法适合用来进行批量数据集处理。

    map()方法接受的主要参数是一个用于数据处理的function。正好可以与tokenizer相配合。

    train_ds = train_examples.map(train_trans_func, batched=True, num_proc=3, remove_columns=column_names)
    dev_ds = dev_examples.map(dev_trans_func, batched=True, num_proc=3, remove_columns=column_names)
    
    • 1
    • 2

    Batchify和数据读入

    使用paddle.io.BatchSampler和paddlenlp.data中提供的方法把数据组成batch。

    然后使用paddle.io.DataLoader接口多线程异步加载数据。

    batchify_fn详解:
    [站外图片上传中…(image-1106cc-1668574875215)]

    import paddle
    from paddlenlp.data import DataCollatorWithPadding
    
    batch_size = 64
    
    # 定义BatchSampler
    train_batch_sampler = paddle.io.DistributedBatchSampler(
            train_ds, batch_size=batch_size, shuffle=True)
    
    dev_batch_sampler = paddle.io.BatchSampler(
        dev_ds, batch_size=batch_size, shuffle=False)
    
    
    # 定义batchify_fn
    
    train_batchify_fn = DataCollatorWithPadding(tokenizer)
    
    dev_batchify_fn = DataCollatorWithPadding(tokenizer)
    
    # 构造DataLoader
    train_data_loader = paddle.io.DataLoader(
        dataset=train_ds,
        batch_sampler=train_batch_sampler,
        collate_fn=train_batchify_fn,
        return_list=True)
    
    dev_data_loader = paddle.io.DataLoader(
        dataset=dev_ds_for_model,
        batch_sampler=dev_batch_sampler,
        collate_fn=dev_batchify_fn,
        return_list=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    加载ernie3.0模型

    以下项目以ERNIE为例,介绍如何将预训练模型Fine-tune完成DuReaderrobust阅读理解任务。

    DuReaderrobust阅读理解任务的本质是答案抽取任务。根据输入的问题和文章,从预训练模型的sequence_output中预测答案在文章中的起始位置和结束位置。原理如下图所示:
    [站外图片上传中…(image-7d74f8-1668574875215)]

    
    from paddlenlp.transformers import AutoModelForQuestionAnswering
    
    model = AutoModelForQuestionAnswering.from_pretrained(MODEL_NAME)
    
    • 1
    • 2
    • 3
    • 4

    设计loss function

    模型的网络结构确定后我们就可以设计loss function了。

    AutoModelForQuestionAnswering模型对将ErnieModel的sequence_output拆开成start_logits和end_logits输出,所以DuReaderrobust的loss由start_loss和end_loss两部分组成,我们需要自己定义loss function。

    对于答案起始位置和结束位置的预测可以分别看成两个分类任务。所以设计的loss function如下:

    class CrossEntropyLossForRobust(paddle.nn.Layer):
        def __init__(self):
            super(CrossEntropyLossForRobust, self).__init__()
    
        def forward(self, y, label):
            start_logits, end_logits = y
            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.cross_entropy(
                input=start_logits, label=start_position)
            end_loss = paddle.nn.functional.cross_entropy(
                input=end_logits, label=end_position)
            loss = (start_loss + end_loss) / 2
            return loss
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    训练配置

    适用于ERNIE/BERT这类Transformer模型的学习率为warmup的动态学习率。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JK6w69yx-1668574808616)(https://upload-images.jianshu.io/upload_images/1531909-6da3d2343de5a2d4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    # 训练过程中的最大学习率
    learning_rate = 3e-5 
    
    # 训练轮次
    epochs = 1
    
    # 学习率预热比例
    warmup_proportion = 0.1
    
    # 权重衰减系数,类似模型正则项策略,避免模型过拟合
    weight_decay = 0.01
    
    num_training_steps = len(train_data_loader) * epochs
    
    # 学习率衰减策略
    lr_scheduler = paddlenlp.transformers.LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_proportion)
    
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    模型训练和评估

    模型训练的过程通常有以下步骤:

    从dataloader中取出一个batch data。
    将batch data喂给model,做前向计算。
    将前向计算结果传给损失函数,计算loss。
    loss反向回传,更新梯度。重复以上步骤。
    每训练一个epoch时,程序通过evaluate()调用paddlenlp.metric.squad中的squad_evaluate(), compute_predictions()评估当前模型训练的效果,其中:

    compute_predictions()用于生成可提交的答案;

    squad_evaluate()用于返回评价指标。

    二者适用于所有符合squad数据格式的答案抽取任务。这类任务使用F1和exact来评估预测的答案和真实答案的相似程度。

    
    criterion = CrossEntropyLossForRobust()
    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=batch["input_ids"], token_type_ids=batch["token_type_ids"])
            loss = criterion(logits, (batch["start_positions"], batch["end_positions"]))
    
            if global_step % 400 == 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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    结果预测与提交

    test=pd.read_json('work/data_task1/test_data/test.json',lines=True)
    test['id']=[f'test_{id}' for id in range(len(test))]
    test['answer_list']=[[] for i in range(len(test))]
    test['answer_start_list']=[[] for i in range(len(test))]
    test_examples = Dataset(pa.Table.from_pandas(test))
    
    test_trans_func = partial(prepare_validation_features, 
                               max_seq_length=max_seq_length, 
                               doc_stride=doc_stride,
                               tokenizer=tokenizer)
    test_ds = test_examples.map(test_trans_func, batched=True, num_proc=3, remove_columns=['answer_list',
     'title',
     'url',
     'answer_start_list',
     'doc_text',
     'query',
     'id'])
    test_ds_for_model = test_ds.remove_columns(["example_id", "offset_mapping"])
    
    
    batch_size = 16
    
    
    test_batch_sampler = paddle.io.BatchSampler(
        test_ds, batch_size=batch_size, shuffle=False)
    
    
    # 定义batchify_fn
    
    test_batchify_fn = DataCollatorWithPadding(tokenizer)
    
    
    test_data_loader = paddle.io.DataLoader(
        dataset=test_ds_for_model,
        batch_sampler=test_batch_sampler,
        collate_fn=test_batchify_fn,
        return_list=True)
    # 传入test_data_loader,并将is_test参数设为True,即可生成千言比赛可提交的结果。
    all_predictions, all_nbest_json, scores_diff_json=evaluate(model=model, raw_dataset=test_examples, dataset=test_ds, data_loader=test_data_loader, is_test=True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    制作提交结果

    sub_data=[]
    for i in range(len(test_examples["query"])):
        prob=all_nbest_json[test_examples['id'][i]][0]['probability']
        text=all_nbest_json[test_examples['id'][i]][0]['text']
        sub_data.append([prob,text])
    sub=pd.DataFrame(sub_data,columns=['prob','text'])
    sub['text']=sub['text'].apply(lambda  x:"NoAnswer" if len(x.strip())<2 else x)
    sub[['prob','text']].to_csv('subtask1_test_pred.txt',sep='\t',header=None,index=None)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    优化思路

    • 目前该baseline还不支持多片段答案抽取,不过可以从all_nbest_json候选答案中挑选答案
    • 可以参考NLP优化算法,使用fgm进行对抗训练
    • 对于NoAnswer的数据,answer_list可以默认填充‘无答案’字符,然后同时在doc_text拼接这个字符,然后进行训练和预测,目前基于预测答案长度修改答案,过于粗糙

    参考资料

  • 相关阅读:
    Python的输入输出(来自菜鸟教程)
    Bank Marketing预测一个客户购买理财产品的成功率
    Windows网络与通信程序设计实验四:基于WSAEventSelect模型的通信仿真
    P3378 【模板】堆
    C语言典范编程
    Element中按键修饰符需要加.native
    微信公众号如何通过迁移变更主体?
    基于开源模型搭建实时人脸识别系统(六):人脸识别(人脸特征提取)
    365天深度学习训练营-第5周:运动鞋品牌识别
    浅谈JVM(面试常考)
  • 原文地址:https://blog.csdn.net/yanqianglifei/article/details/127883167