基于预训练模型 ERNIE-Gram 的科大讯飞中文问题相似度挑战赛
使用百度最新开源的ERNIE-Gram 预训练模型实现科大讯飞中文问题Point-wise语义匹配任务
赛题介绍
一、赛事背景
问答系统中包括三个主要的部分:问题理解,信息检索和答案抽取。而问题理解是问答系统的第一部分也是非常关键的一部分。问题理解有非常广泛的应用,如重复评论识别、相似问题识别等。
重复问题检测是一个常见的文本挖掘任务,在很多实际问答社区都有相应的应用。重复问题检测可以方便进行问题的答案聚合,以及问题答案推荐,自动QA等。由于中文词语的多样性和灵活性,本赛题需要选手构建一个重复问题识别算法。
二、赛事任务
本次赛题希望参赛选手对两个问题完成相似度打分。比赛链接
训练集:约5千条问题对和标签。若两个问题是相同的问题,标签为1;否则为0。
测试集:约5千条问题对,需要选手预测标签。
三、数据说明
训练集给定问题对和标签,使用\t进行分隔。测试集给定问题对,使用\t进行分隔。
eg:世界上什么东西最恐怖 世界上最恐怖的东西是什么? 1
解析:“世界上什么东西最恐怖”与”世界上最恐怖的东西是什么“问题相同,故是重复问题,标签为1。
四、评估指标
本次竞赛的评价标准采用准确率指标,最高分为1。计算方法参考https://scikit-learn.org/stable/modules/generated/sklearn.metrics.accuracy_score.html,
评估代码参考:
from sklearn.metrics import accuracy_score
y_pred = [0, 2, 1, 3]
y_true = [0, 1, 2, 3]
accuracy_score(y_true, y_pred)
ERNIE-Gram介绍
ERNIE模型是多层transformer结构的堆叠,transformer首先对输入的token进行位置编码,经过注意力机制与残差链接进行归一化,再经过多层前馈神经网络得到最终的embedding
ERNIE是对BERT的改进模型,ERNIE相比BERT模型最大的区别在于BERT是基于Token做的掩码,而ERNIE是基于词做的掩码,相当于提供了更多的先验知识,做了知识增强
模型构建
安装paddlenlp
# 安装最新版本的 paddlenlp
!pip install --upgrade paddlenlp -i https://pypi.org/simple
导入类库
import time
import os
import numpy as np
import paddle
import paddle.nn.functional as F
from paddlenlp.datasets import load_dataset
from paddlenlp.datasets import MapDataset
import paddlenlp
import pandas as pd
from paddle.io import Dataset
from sklearn.model_selection import train_test_split
定义数据类
由于训练数据量较大,通常会将数据分成小的batch喂给神经网络学习,paddle对应的api分别是paddle.io.Dataset与paddle.io.DataLoader。
构造自定义数据类步骤:
- 定义类函数,继承paddle.io.Dataset类
- 实现__init__函数
- 实现__getitem__方法
- 实现__len__方法
对于自定义的数据集,可以在数据集的构造函数中进行数据增强方法的定义,之后对 getitem 中返回的数据进行应用,就可以完成自定义数据增强。
class MyDataset(Dataset):
"""
构造自己的数据类,配合后面的Dataloader使用
"""
def __init__(self, data, mode='train'):
super(MyDataset, self).__init__()
self.mode = mode
self.data = self._load_data(data)
# 改造数据集 将dataframe格式数据转换为[{},{}]形式数据
def _load_data(self, data):
data_set = []
for index, row in data.iterrows():
data_dict = {}
data_dict['query'] = row['query']
data_dict['title'] = row['title']
if self.mode == 'train':
data_dict['label'] = row['label']
data_set.append(data_dict)
return data_set
def __getitem__(self, idx):
return self.data[idx]
def __len__(self):
return len(self.data)
数据导入与处理
train = pd.read_csv('/home/aistudio/data/data102337/train.csv',sep='\t', names=['query', 'title','label'])
x_train,x_dev = train_test_split(train,test_size=0.1, stratify=train['label'].iloc[:])
x_train.shape
train_ds = MapDataset(MyDataset(x_train))
dev_ds = MapDataset(MyDataset(x_dev))
# 输出训练集的前 3 条样本
for idx, example in enumerate(train_ds):
if idx <= 3:
print(example)
# 将 1 条明文数据的 query、title 拼接起来,根据预训练模型的 tokenizer 将明文转换为 ID 数据
# 返回 input_ids 和 token_type_ids
def convert_example(example, tokenizer, max_seq_length=512, is_test=False):
query, title = example["query"], example["title"]
encoded_inputs = tokenizer(
text=query, text_pair=title, max_seq_len=max_seq_length)
input_ids = encoded_inputs["input_ids"]
token_type_ids = encoded_inputs["token_type_ids"]
if not is_test:
label = np.array([example["label"]], dtype="int64")
return input_ids, token_type_ids, label
# 在预测或者评估阶段,不返回 label 字段
else:
return input_ids, token_type_ids
# 为了后续方便使用,我们使用python偏函数(partial)给 convert_example 赋予一些默认参数
from functools import partial
# 训练集和验证集的样本转换函数,后续直接使用map生成每一行的input_id和token_type_id
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=512)
# 我们的训练数据会返回 input_ids, token_type_ids, labels 3 个字段,对数据pad组合
# 因此针对这 3 个字段需要分别定义 3 个组 batch 操作
from paddlenlp.data import Stack, Pad, Tuple
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input_ids
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # token_type_ids
Stack(dtype="int64") # label
): [data for data in fn(samples)]
# 定义分布式 Sampler: 自动对训练数据进行切分,支持多卡并行训练
batch_sampler = paddle.io.DistributedBatchSampler(train_ds, batch_size=32, shuffle=True)
# 基于 train_ds 定义 train_data_loader
# 因为我们使用了分布式的 DistributedBatchSampler, train_data_loader 会自动对训练数据进行切分
train_data_loader = paddle.io.DataLoader(
dataset=train_ds.map(trans_func),
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
# 针对验证集数据加载,我们使用单卡进行评估,所以采用 paddle.io.BatchSampler 即可
# 定义 dev_data_loader
batch_sampler = paddle.io.BatchSampler(dev_ds, batch_size=32, shuffle=False)
dev_data_loader = paddle.io.DataLoader(
dataset=dev_ds.map(trans_func),
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
加载预训练模型tokenizer
# 因为是基于预训练模型 ERNIE-Gram 来进行,所以需要首先加载 ERNIE-Gram 的 tokenizer,其他预训练模型可以参考https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/model_zoo/transformers.rst
# 后续样本转换函数基于 tokenizer 对文本进行切分
tokenizer = paddlenlp.transformers.ErnieGramTokenizer.from_pretrained('ernie-gram-zh')
定义模型类
import paddle.nn as nn
# 我们基于 ERNIE-Gram 模型结构搭建 Point-wise 语义匹配网络
# 所以此处先定义 ERNIE-Gram 的 pretrained_model
pretrained_model = paddlenlp.transformers.ErnieGramModel.from_pretrained('ernie-gram-zh')
#pretrained_model = paddlenlp.transformers.ErnieModel.from_pretrained('ernie-1.0')
class PointwiseMatching(nn.Layer):
# 此处的 pretained_model 在本例中会被 ERNIE-Gram 预训练模型初始化
def __init__(self, pretrained_model, dropout=None):
super().__init__()
self.ptm = pretrained_model
self.dropout = nn.Dropout(dropout if dropout is not None else 0.1)
# 语义匹配任务: 相似、不相似 2 分类任务
self.classifier = nn.Linear(self.ptm.config["hidden_size"], 2)
def forward(self,
input_ids,
token_type_ids=None,
position_ids=None,
attention_mask=None):
# 此处的 Input_ids 由两条文本的 token ids 拼接而成
# token_type_ids 表示两段文本的类型编码
# 返回的 cls_embedding 就表示这两段文本经过模型的计算之后而得到的语义表示向量
_, cls_embedding = self.ptm(input_ids, token_type_ids, position_ids,
attention_mask)
cls_embedding = self.dropout(cls_embedding)
# 基于文本对的语义表示向量进行 2 分类任务
logits = self.classifier(cls_embedding)
probs = F.softmax(logits)
return probs
# 定义 Point-wise 语义匹配网络
model = PointwiseMatching(pretrained_model)
定义优化参数与评价函数
from paddlenlp.transformers import LinearDecayWithWarmup
epochs = 30
num_training_steps = len(train_data_loader) * epochs
# 定义 learning_rate_scheduler,负责在训练过程中对 lr 进行调度
lr_scheduler = LinearDecayWithWarmup(5E-5, num_training_steps, 0.0)
# 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
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, phase="dev"):
model.eval()
metric.reset()
losses = []
for batch in data_loader:
input_ids, token_type_ids, labels = batch
probs = model(input_ids=input_ids, token_type_ids=token_type_ids)
loss = criterion(probs, labels)
losses.append(loss.numpy())
correct = metric.compute(probs, labels)
metric.update(correct)
accu = metric.accumulate()
print("eval {} loss: {:.5}, accu: {:.5}".format(phase,
np.mean(losses), accu))
model.train()
metric.reset()
# 接下来,开始正式训练模型,训练时间较长,可注释掉这部分
global_step = 0
tic_train = time.time()
for epoch in range(1, epochs + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, token_type_ids, labels = batch
probs = model(input_ids=input_ids, token_type_ids=token_type_ids)
loss = criterion(probs, labels)
correct = metric.compute(probs, labels)
metric.update(correct)
acc = metric.accumulate()
global_step += 1
# 每间隔 10 step 输出训练指标
if global_step % 10 == 0:
print(
"global step %d, epoch: %d, batch: %d, loss: %.5f, accu: %.5f, speed: %.2f step/s"
% (global_step, epoch, step, loss, acc,
10 / (time.time() - tic_train)))
tic_train = time.time()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.clear_grad()
# 每间隔 100 step 在验证集和测试集上进行评估
if global_step % 100 == 0:
evaluate(model, criterion, metric, dev_data_loader, "dev")
# 训练结束后,存储模型参数
save_dir = os.path.join("checkpoint", "model_%d" % global_step)
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)
# 定义预测函数
def predict(model, data_loader):
batch_probs = []
# 预测阶段打开 eval 模式,模型中的 dropout 等操作会关掉
model.eval()
with paddle.no_grad():
for batch_data in data_loader:
input_ids, token_type_ids = batch_data
input_ids = paddle.to_tensor(input_ids)
token_type_ids = paddle.to_tensor(token_type_ids)
# 获取每个样本的预测概率: [batch_size, 2] 的矩阵
batch_prob = model(
input_ids=input_ids, token_type_ids=token_type_ids).numpy()
batch_probs.append(batch_prob)
batch_probs = np.concatenate(batch_probs, axis=0)
return batch_probs
## 定义数据dataloader
# 预测数据的转换函数
# predict 数据没有 label, 因此 convert_exmaple 的 is_test 参数设为 True
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=512,
is_test=True)
# 预测数据的组 batch 操作
# predict 数据只返回 input_ids 和 token_type_ids,因此只需要 2 个 Pad 对象作为 batchify_fn
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input_ids
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # segment_ids
): [data for data in fn(samples)]
# 加载预测数据
test = pd.read_csv('/home/aistudio/data/data102337/test.csv',sep='\t', names=['query', 'title'])
test_ds = MapDataset(MyDataset(test,mode='test'))
batch_sampler = paddle.io.BatchSampler(test_ds, batch_size=32, shuffle=False)
# 生成预测数据 data_loader
predict_data_loader =paddle.io.DataLoader(
dataset=test_ds.map(trans_func),
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
save_param_path
通过保存参数加载模型并预测
#定义模型
pretrained_model = paddlenlp.transformers.ErnieGramModel.from_pretrained('ernie-gram-zh')
model = PointwiseMatching(pretrained_model)
state_dict = paddle.load(save_param_path)
model.set_dict(state_dict)
# 执行预测函数
y_probs = predict(model, predict_data_loader)
# 根据预测概率获取预测 label
y_preds = np.argmax(y_probs, axis=1)
y_preds
# 保存结果文件
res = pd.DataFrame({'label':y_preds})
dle.load(save_param_path)
model.set_dict(state_dict)
# 执行预测函数
y_probs = predict(model, predict_data_loader)
# 根据预测概率获取预测 label
y_preds = np.argmax(y_probs, axis=1)
y_preds
# 保存结果文件
res = pd.DataFrame({'label':y_preds})
res.to_csv('./baseline.csv',index=False)
结果提交
后续改进
- 更换其他的预训练模型
- 模型融合
- 参数调优
更多推荐
所有评论(0)