2021 CCF BDCI 千言-问题匹配鲁棒性评测Baseline
本案例介绍 NLP 最基本的任务类型之一 —— 文本语义匹配,并且基于 PaddleNLP 使用百度开源的预训练模型 ERNIE-Gram 搭建效果优异的语义匹配模型,来判断 2 段文本语义是否相同。
1. 背景介绍
文本语义匹配任务,简单来说就是给定两段文本,让模型来判断两段文本是不是语义相似。
以权威的语义匹配数据集 LCQMC 为例,LCQMC 数据集是基于百度知道相似问题推荐构造的通问句语义匹配数据集。训练集中的每两段文本都会被标记为 1(语义相似) 或者 0(语义不相似)。更多数据集可访问千言获取哦。
例如百度知道场景下,用户搜索一个问题,模型会计算这个问题与候选问题是否语义相似,语义匹配模型会找出与问题语义相似的候选问题返回给用户,加快用户提问-获取答案的效率。例如,当某用户在搜索引擎中搜索 “深度学习的教材有哪些?”,模型就自动找到了一些语义相似的问题展现给用户:
2.快速实践
介绍如何准备数据,基于 ERNIE-Gram 模型搭建匹配网络,然后快速进行语义匹配模型的训练、评估和预测。
2.1 数据加载
# 正式开始实验之前首先通过如下命令安装最新版本的 paddlenlp
!pip install --upgrade paddlenlp
# 检查数据集所在路径
!tree -L 3 /home/aistudio/data
!unzip -o data/data104940/train.zip -d data
# 将LCQMC、BQ、OPPO三个数据集的训练集和验证集合并
!cat ./data/train/LCQMC/train ./data/train/BQ/train ./data/train/OPPO/train > train.txt
!cat ./data/train/LCQMC/dev ./data/train/BQ/dev ./data/train/OPPO/dev > dev.txt
from functools import partial
import argparse
import os
import random
import time
import numpy as np
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
import paddlenlp as ppnlp
from paddlenlp.data import Stack, Tuple, Pad
from paddlenlp.datasets import load_dataset
from paddlenlp.transformers import LinearDecayWithWarmup
from work.data import create_dataloader, read_text_pair, convert_example
train_ds = load_dataset(read_text_pair, data_path="train.txt", is_test=False, lazy=False)
dev_ds = load_dataset(read_text_pair, data_path="dev.txt", is_test=False, lazy=False)
# 输出训练集的前 3 条样本
for idx, example in enumerate(train_ds):
if idx <= 2:
print(example)
2.2 数据预处理
通过 PaddleNLP 加载进来的数据集是原始的明文数据集,这部分我们来实现组 batch、tokenize 等预处理逻辑,将原始明文数据转换成网络训练的输入数据。
# 因为是基于预训练模型 ERNIE-Gram 来进行,所以需要首先加载 ERNIE-Gram 的 tokenizer,
# 后续样本转换函数基于 tokenizer 对文本进行切分
tokenizer = ppnlp.transformers.ErnieGramTokenizer.from_pretrained('ernie-gram-zh')
### 对训练集的第 1 条数据进行转换
input_ids, token_type_ids, label = convert_example(train_ds[0], tokenizer)
print(input_ids)
print(token_type_ids)
# 为了后续方便使用,我们使用python偏函数(partial)给 convert_example 赋予一些默认参数
# 训练集和验证集的样本转换函数
trans_func = partial(convert_example, tokenizer=tokenizer, max_seq_length=256)
# 我们的训练数据会返回 input_ids, token_type_ids, labels 3 个字段
# 因此针对这 3 个字段需要分别定义 3 个组 batch 操作
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # text_pair_input
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # text_pair_segment
Stack(dtype="int64") # label
): [data for data in fn(samples)]
定义 Dataloader
下面我们基于组 batchify_fn 函数和样本转换函数 trans_func 来构造训练集的 DataLoader, 支持多卡训练
train_data_loader = create_dataloader(
train_ds,
mode='train',
batch_size=32,
batchify_fn=batchify_fn,
trans_fn=trans_func)
dev_data_loader = create_dataloader(
dev_ds,
mode='dev',
batch_size=128,
batchify_fn=batchify_fn,
trans_fn=trans_func)
2.3 模型搭建
自从 2018 年 10 月以来,NLP 个领域的任务都通过 Pretrain + Finetune 的模式相比传统 DNN 方法在效果上取得了显著的提升,本节我们以百度开源的预训练模型 ERNIE-Gram 为基础模型,在此之上构建 Point-wise 语义匹配网络。
首先我们来定义网络结构:
- Point-wise 语义匹配网络
- ERNIE-Gram
- Rdrop
# 我们基于 ERNIE-Gram 模型结构搭建 Point-wise 语义匹配网络
# 所以此处先定义 ERNIE-Gram 的 pretrained_model
pretrained_model = ppnlp.transformers.ErnieGramModel.from_pretrained('ernie-gram-zh')
class QuestionMatching(nn.Layer):
def __init__(self, pretrained_model, dropout=None, rdrop_coef=0.0):
super().__init__()
self.ptm = pretrained_model
self.dropout = nn.Dropout(dropout if dropout is not None else 0.1)
# num_labels = 2 (similar or dissimilar)
self.classifier = nn.Linear(self.ptm.config["hidden_size"], 2)
self.rdrop_coef = rdrop_coef
self.rdrop_loss = ppnlp.losses.RDropLoss()
def forward(self,
input_ids,
token_type_ids=None,
position_ids=None,
attention_mask=None,
do_evaluate=False):
_, cls_embedding1 = self.ptm(input_ids, token_type_ids, position_ids, attention_mask)
cls_embedding1 = self.dropout(cls_embedding1)
logits1 = self.classifier(cls_embedding1)
# For more information about R-drop please refer to this paper: https://arxiv.org/abs/2106.14448
# Original implementation please refer to this code: https://github.com/dropreg/R-Drop
if self.rdrop_coef > 0 and not do_evaluate:
_, cls_embedding2 = self.ptm(input_ids, token_type_ids, position_ids, attention_mask)
cls_embedding2 = self.dropout(cls_embedding2)
logits2 = self.classifier(cls_embedding2)
kl_loss = self.rdrop_loss(logits1, logits2)
else:
kl_loss = 0.0
return logits1, kl_loss
model = QuestionMatching(pretrained_model, rdrop_coef=0.0)
2.4 模型训练 & 评估
epochs = 3
num_training_steps = len(train_data_loader) * epochs
lr_scheduler = LinearDecayWithWarmup(5e-5, num_training_steps, 0.0)
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=0.0,
apply_decay_param_fun=lambda x: x in decay_params)
criterion = paddle.nn.loss.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
@paddle.no_grad()
def evaluate(model, criterion, metric, data_loader):
model.eval()
metric.reset()
losses = []
total_num = 0
for batch in data_loader:
input_ids, token_type_ids, labels = batch
total_num += len(labels)
logits, _ = model(input_ids=input_ids, token_type_ids=token_type_ids, do_evaluate=True)
loss = criterion(logits, labels)
losses.append(loss.numpy())
correct = metric.compute(logits, labels)
metric.update(correct)
accu = metric.accumulate()
print("dev_loss: {:.5}, accuracy: {:.5}, total_num:{}".format(np.mean(losses), accu, total_num))
model.train()
metric.reset()
return accu
global_step = 0
best_accuracy = 0.0
tic_train = time.time()
for epoch in range(1, 3 + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, token_type_ids, labels = batch
logits1, kl_loss = model(input_ids=input_ids, token_type_ids=token_type_ids)
correct = metric.compute(logits1, labels)
metric.update(correct)
acc = metric.accumulate()
ce_loss = criterion(logits1, labels)
if kl_loss > 0:
loss = ce_loss + kl_loss * args.rdrop_coef
else:
loss = ce_loss
global_step += 1
if global_step % 10 == 0:
print(
"global step %d, epoch: %d, batch: %d, loss: %.4f, ce_loss: %.4f., kl_loss: %.4f, accu: %.4f, speed: %.2f step/s"
% (global_step, epoch, step, loss, ce_loss, kl_loss, acc,
10 / (time.time() - tic_train)))
tic_train = time.time()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.clear_grad()
if global_step % 200 == 0:
accuracy = evaluate(model, criterion, metric, dev_data_loader)
if accuracy > best_accuracy:
save_dir = os.path.join("./checkpoint", "model_%d" % global_step)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
save_param_path = os.path.join(save_dir, 'model_state.pdparams')
paddle.save(model.state_dict(), save_param_path)
tokenizer.save_pretrained(save_dir)
best_accuracy = accuracy
2.5 模型预测
接下来我们使用已经训练好的语义匹配模型对一些预测数据进行预测。
! wget https://paddlenlp.bj.bcebos.com/models/text_matching/question_matching_rdrop0p0_baseline_model.tar
! tar -xvf question_matching_rdrop0p0_baseline_model.tar
!head -3 "data/data104941/test_A"
!$ unset CUDA_VISIBLE_DEVICES
!python -u \
work/predict.py \
--device gpu \
--params_path "./ernie_gram_rdrop0p0/model_state.pdparams" \
--batch_size 128 \
--input_file "data/data104941/test_A" \
--result_file "ccf_qianyan_qm_result_A.csv"
提交预测结果2021 CCF BDCI 千言-问题匹配鲁棒性评测比赛👈
更多优化方案
- 对抗训练
- 数据增强
- 大模型
- 模型集成