赞
踩
文本匹配任务在自然语言处理中是非常重要的基础任务之一,在问答系统、智能对话等诸多应用场景起到关键性的作用,但中文对话中的文本匹配仍然存在很多难点
根据问题识别出正确的待匹配文本,给定两个问题Q,判定该问题对语义是否匹配。
赛题数据由训练集和测试集组成,训练集数据集读取代码:
import pandas as pd
pd.read_csv('train.csv',sep='\t')
本次竞赛的评价标准采用准确率指标,最高分为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)
文件格式:预测结果文件按照csv格式提交
文件大小:无要求
提交次数限制:每支队伍每天最多3次
预测结果文件详细说明:
以csv格式提交,编码为UTF-8,第一行为表头;
标签顺序需要与测试集文本保持一致;
提交前请确保预测结果的格式与sample_submit.csv中的格式一致。具体格式如下:
0
0
0
赛题是一个典型的文本匹配的任务,因此基于 ERNIE-Gram 模型搭建匹配网络,然后快速进行语义匹配模型的训练、评估和预测。
流程大致如下:
在项目效果上,ERNIE模型 对比Bert 等模型有更好的效果
ERNIE 多粒度预训练语义理解技术
作为自然语言处理的基本语义单元,更充分的语言粒度学习能帮助模型实现更强的语义理解能力:
#解压数据文件
!unzip -d /home/aistudio/data/data177274/data /home/aistudio/data/data177274/中文对话文本匹配挑战赛数据集.zip
Archive: /home/aistudio/data/data177274/中文对话文本匹配挑战赛数据集.zip
creating: /home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/
inflating: /home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/sample_submit.csv
inflating: /home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/test.csv
inflating: /home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/train.csv
# 安装paddlenlp 相关框架,且需先更新pip组件
!pip install --upgrade pip
!pip install paddlenlp==2.4.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
!python -m pip install --upgrade matplotlib
from IPython.display import clear_output
clear_output()
print("环境安装成功!请重启内核!!")
环境安装成功!请重启内核!!
#查看样例数据
#查看训练数据
import pandas as pd
path='/home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/train.csv'
data=pd.read_csv(path,encoding='utf-8-sig',sep='\t',header=None,names=['text1', 'text2','label'])
data
text1 | text2 | label | |
---|---|---|---|
0 | 藏獒为什么这么贵 | 藏獒见人不咬为什么 | 0 |
1 | 人生应该怎么才算精彩? | 人生要怎么过才算精彩啊 | 1 |
2 | 为什么打牌老是输 | 为什么我枪神纪进不去了 | 0 |
3 | 现在网上卖什么最赚钱 | 网上卖什么最赚钱 | 1 |
4 | 如何提高气质 | 怎样提高自身气质? | 1 |
... | ... | ... | ... |
49995 | 剑灵拳师破力士不屈 | 剑灵力士能否当T | 0 |
49996 | 冰雪奇缘在哪看 | 冰雪奇缘在哪里看 | 1 |
49997 | 高分子材料与材料科学工程那个专业出路好 | 高分子材料科学与工程专业考研 | 0 |
49998 | 李世民的年号是什么? | 李世民是什么王? | 0 |
49999 | 什么软件可以挂机赚钱的 | 什么挂机软件赚钱 | 1 |
50000 rows × 3 columns
#计算最大的文本长度
def max_text_len(self):
for i, col in enumerate(self):
data[col] = data[col].astype(str)
n_max = data[col].str.len().max()
print(i, col, n_max)
#查看最大的内容和文本长度
print(max_text_len(data.columns.tolist()))
0 text1 49
1 text2 131
2 label 1
None
data['label'].value_counts(normalize=True)
1 0.58078
0 0.41922
Name: label, dtype: float64
#可视化label 为1 和0 的值的分布情况
import matplotlib.pyplot as plt
import numpy as np
label1= data.loc[data['label']=='1']
label0= data.loc[data['label']=='0']
x = np.array(["label-1", "label-0"])
y = np.array([len(label1), len(label0)])
plt.bar(x, y, color = ["blue","red"],width = 0.3)
plt.show()
能够看到在标签分布中58% 都是为1 的标签,42% 的标签为0
#计算标签数据间的差额
label_dif=len(label1)-len(label0)
print(label_dif)
8078
#数据增强
#随机抽取label为0的数据,进行数据增强
import sklearn
label0_df=sklearn.utils.shuffle(label0) #随机打乱
label0_data = label0_df.sample(n=label_dif, random_state=0, axis=0)
label0_data.head(5)
text1 | text2 | label | |
---|---|---|---|
19936 | 时空猎人27w战斗力花多少钱 | 时空猎人战斗力最高是多少 | 0 |
3646 | 欧洲移民哪个国家好 | 欧洲哪个国家好移民 | 0 |
34892 | 去腿毛的好方法 | 怎样去黑头?鼻贴用了没效果啊。 | 0 |
14249 | 许地山以?么作自己的笔名 | 用别人的名字买车 | 0 |
29883 | 你觉得我发的这个动漫头像好看吗? | 这两个头像分别是哪部动漫里面的?好看吗? | 0 |
# 将抽取的数据以文件的方式保存
label0_data.to_csv('label0_data.csv', index=False,encoding='utf-8',sep ='|',header =['text1', 'text2','label'])
#构造文件读取函数
def read_file(filename):
lines = []
with open(filename, 'r', encoding='utf-8') as f:
next(f)
for line in f:
lines.append({"text1":line.split("|")[0].strip(),"text2":line.split("|")[1].strip()})
return lines
data_list=read_file('label0_data.csv')
#通过同义词和上下文替换的方式来进行数据增强 from paddlenlp.dataaug import WordSubstitute aug = WordSubstitute(['synonym','mlm'], create_n=1, aug_n=1) Replenish_data=pd.DataFrame() for line in data_list: text1=aug.augment(line['text1']) text2=aug.augment(line['text2']) result={"text1":text1,"text2":text2} if len(text1)!=0 and len(text2)!=0: text1=text1[0] text2=text2[0] #对于增强失败的数据,直接调换text1和text2的位置 elif len(text1)!=0 and len(text2)==0: text1=text1[0] text2=line['text1'] elif len(text1)!=0 and len(text2)==0: text2=text2[0] text1=line['text2'] else: text1=line['text2'] text2=line['text1'] result={"text1":text1,"text2":text2} Replenish_data=Replenish_data.append(result,ignore_index=True)
#查看数据增强后的数据
Replenish_data
text1 | text2 | |
---|---|---|
0 | 日子猎人27w战斗力花多少钱 | 时空猎人27w战斗力花多少钱 |
1 | 欧洲哪个国家好移民 | 欧洲移民哪个国家好 |
2 | 怎样去黑头?鼻贴用了没效果啊。 | 去腿毛的好方法 |
3 | 用别人的名字买车 | 许地山以?么作自己的笔名 |
4 | 你觉得我发的这个动漫半身像好看吗? | 这两块头像分别是哪部动漫里面的?好看吗? |
... | ... | ... |
8073 | 牛的母爱读书答案 | 牛的母爱阅读答案 |
8074 | 吃湿核桃藉多了会发胖么? | 请问吃核桃能润肠通便吗?天长日久吃它会发胖吗? |
8075 | 昵图网如何查素材被谁下载了 | 求怎样用手机看种子 |
8076 | 花灰色裤子配什么颜色的袄? | 土灰色的裤子配什么颜色的短装呀? |
8077 | 女孩子说我宣你是什么意思? | 女孩子说我是你的汤是什么意思 |
8078 rows × 2 columns
#进行数据集合并
result = pd.merge(data, Replenish_data, how='outer', on=['text1', 'text2'])
result.fillna('0',inplace=True)
result.loc[(result['label']!='1') & (result['label']!='0')]
text1 | text2 | label |
---|
# 构造训练集和测试集
import sklearn
df=sklearn.utils.shuffle(result) #随机打乱
train_data = df.sample(frac=0.8, random_state=0, axis=0)
dev_data = df[~df.index.isin(train_data.index)]
# input_ids:字的编码
# token_type_ids:标识是第一个句子还是第二个句子
# attention_mask:标识是不是填充
#生成训练集和测试集的数据文件
train_data.to_csv('train_data.csv', index=False,encoding='utf-8',sep ='♬',header =['text1', 'text2','label'])
dev_data.to_csv('dev_data.csv', index=False,encoding='utf-8',sep ='♬',header =['text1', 'text2','label'])
from paddlenlp.datasets import load_dataset def read(data_path): with open(data_path, 'r', encoding='utf-8') as f: # 跳过列名 next(f) for line in f: text1, text2,label = line.strip('\t').split("♬")[0].strip(),line.strip('\t').split("♬")[1].strip(),line.strip('\t').split("♬")[2].strip() text1 = line.strip('\t').split("♬")[0].strip() text2 = line.strip('\t').split("♬")[1].strip() label=line.strip('\t').split("♬")[2].strip() yield ({'text1': text1, 'text2': text2,'label':label }) # data_path为read()方法的参数 train_dataset = load_dataset(read, data_path='train_data.csv',lazy=False, split="train") dev_dataset = load_dataset(read, data_path='dev_data.csv',lazy=False, split="dev")
#查看训练集和测试集的划分情况
print(len(train_dataset))
print(len(dev_dataset))
46462
11616
# 输出训练集的前 3 条样本
for idx, example in enumerate(train_dataset):
if idx <= 3:
print(example)
{'text1': '孕妇可以吃草莓吗?', 'text2': '孕妇能吃草莓么', 'label': '1'}
{'text1': '这句日语是什么意思啊?', 'text2': '日语这句话什么意思', 'label': '1'}
{'text1': '求杉杉来吃', 'text2': '武汉话吃饼子?', 'label': '0'}
{'text1': '有哪些好听的经典钢琴曲', 'text2': '好听的伤感钢琴曲有哪些', 'label': '1'}
# 模型加载
import paddlenlp
MODEL_NAME = "ernie-gram-zh"
tokenizer = paddlenlp.transformers.ErnieGramTokenizer.from_pretrained(MODEL_NAME)
[2022-12-29 00:51:20,152] [ INFO] - Already cached /home/aistudio/.paddlenlp/models/ernie-gram-zh/vocab.txt
[2022-12-29 00:51:20,165] [ INFO] - tokenizer config file saved in /home/aistudio/.paddlenlp/models/ernie-gram-zh/tokenizer_config.json
[2022-12-29 00:51:20,167] [ INFO] - Special tokens file saved in /home/aistudio/.paddlenlp/models/ernie-gram-zh/special_tokens_map.json
def convert_example(example, tokenizer, max_seq_length=256, is_test=False):
query, title = example["text1"], example["text2"]
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
# 对训练集的第 1 条数据进行转换
input_ids, token_type_ids, label = convert_example(train_dataset[0], tokenizer)
#查看转换成的结果
print(input_ids)
print(token_type_ids)
print(label)
[1, 1883, 1176, 48, 22, 943, 688, 3496, 1114, 12045, 2, 1883, 1176, 52, 943, 688, 3496, 356, 2]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
[1]
# 为了后续方便使用,我们使用python偏函数(partial)给 convert_example 赋予一些默认参数
# 预训练模型的最大文本长度为512,当前最大文本长度为131
from functools import partial
# 训练集和验证集的样本转换函数
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=256)
# 我们的训练数据会返回 input_ids, token_type_ids, labels 3 个字段
# 因此针对这 3 个字段需要分别定义 3 个组 batch 操作
from paddlenlp.data import Stack, Pad, Tuple
import paddle
import paddlenlp
import paddle.nn as nn
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_dataset, batch_size=32, shuffle=True) # 使用单卡进行评估,所以采用 paddle.io.BatchSampler 即可 batch_sampler = paddle.io.BatchSampler(dev_dataset, batch_size=32, shuffle=False) # 基于 train_dataset 定义 train_data_loader # train_data_loader 会自动对训练数据进行切分 train_data_loader = paddle.io.DataLoader( dataset=train_dataset.map(trans_func), batch_sampler=batch_sampler, collate_fn=batchify_fn, return_list=True) # 定义 dev_data_loader dev_data_loader = paddle.io.DataLoader( dataset=dev_dataset.map(trans_func), batch_sampler=batch_sampler, collate_fn=batchify_fn, return_list=True)
# 我们基于 ERNIE-Gram 模型结构搭建 Point-wise 语义匹配网络 # 所以此处先定义 ERNIE-Gram 的 pretrained_model pretrained_model = paddlenlp.transformers.ErnieGramModel.from_pretrained(MODEL_NAME) 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)
[2022-12-29 00:51:41,914] [ INFO] - Already cached /home/aistudio/.paddlenlp/models/ernie-gram-zh/ernie_gram_zh.pdparams
W1229 00:51:41.920701 26894 gpu_resources.cc:61] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 11.2
W1229 00:51:41.924329 26894 gpu_resources.cc:91] device: 0, cuDNN Version: 8.2.
e: 0, cuDNN Version: 8.2.
from paddlenlp.transformers import LinearDecayWithWarmup import paddle.nn.functional as F import time from visualdl import LogWriter import os #定义模型训练相关配置 #训练轮数 epochs = 20 #学习率 learning_rate=2e-5 # 每间隔 1000 step 在验证集和测试集上进行评估 eval_steps=1000 # 每间隔 10 step 输出训练指标 log_steps =100 # 训练模型保存路径 output_dir = 'checkpoints' #训练日志保存路径 log_writer = LogWriter('visualdl_log_dir') #学习率预热比例 warmup=0.01 #总训练步数 num_training_steps = len(train_data_loader) * epochs # AdamW优化器参数weight_decay weight_decay=0.01 # 定义 learning_rate_scheduler,负责在训练过程中对 lr 进行调度 lr_scheduler = LinearDecayWithWarmup(learning_rate, num_training_steps, warmup) # # LayerNorm参数不参与weight_decay 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=weight_decay, apply_decay_param_fun=lambda x: x in decay_params) # 采用交叉熵 损失函数 criterion = paddle.nn.loss.CrossEntropyLoss() # 评估的时候采用准确率指标 metric = paddle.metric.Accuracy()
#训练函数 def train(model, train_data_loader): global_step = 0 tic_train = time.time() for epoch in range(epochs): for step, batch in enumerate(train_data_loader): #正向传播 optimizer.clear_grad() global_step += 1 input_ids, token_type_ids, label = batch outputs=model(input_ids=input_ids,token_type_ids=token_type_ids) loss = criterion(outputs, label) correct = metric.compute(outputs, label) metric.update(correct) acc = metric.accumulate() ## 每间隔 100 step 输出训练指标 if global_step % log_steps == 0: print( "global step %d, epoch: %d, batch: %d, loss: %.5f, accu: %.5f, speed: %.2f " % (global_step, epoch, step, loss, acc, log_steps / (time.time() - tic_train))) log_writer.add_scalar("train_loss", loss, global_step) tic_train = time.time() ## 反向梯度回传,更新参数 loss.backward() optimizer.step() lr_scheduler.step() optimizer.clear_grad() # 每间隔 1000 step 在验证集和测试集上进行评估 if global_step % eval_steps == 0: evaluate(model, criterion, metric, dev_data_loader, "dev") # 训练结束后,存储模型参数 if not os.path.exists(output_dir): os.makedirs(output_dir) save_dir = os.path.join(output_dir) # 保存模型参数 save_param_path = os.path.join(save_dir, 'model_state.pdparams') paddle.save(model.state_dict(), save_param_path) # 保存tokenizer的词表等 tokenizer.save_pretrained(output_dir)
# 因为训练过程中同时要在验证集进行模型评估,因此我们先定义评估函数 @paddle.no_grad() def evaluate(model, criterion, metric, data_loader, phase="dev"): model.eval() total_eval_accuracy = 0 total_eval_loss = 0 losses = [] for batch in data_loader: #正常传播 input_ids, token_type_ids, label = batch outputs=model(input_ids=input_ids,token_type_ids=token_type_ids) loss = criterion(outputs, label) losses.append(loss.numpy()) correct = metric.compute(outputs, label) metric.update(correct) accu = metric.accumulate() print("eval {} loss: {:.5}, accu: {:.5}".format(phase, np.mean(losses), accu)) model.train() metric.reset()
# 调用模型训练
train(model, train_data_loader)
path='/home/aistudio/data/data177274/data/╓╨╬─╢╘╗░╬─▒╛╞е┼ф╠Ї╒╜╚№╩¤╛▌╝п/test.csv'
#validation_data=pd.read_csv(path,encoding='utf-8-sig',sep='\s+',header=None,names=['text1', 'text2'])
validation_data=pd.read_csv(path,encoding='utf-8-sig',sep='\t',header=None,names=['text1', 'text2'])
validation_data.head(5)
text1 | text2 | |
---|---|---|
0 | 成语中的历史人物 | 成语有关的历史人物 |
1 | 黄财神怎样供奉 | 怎样供奉财神 |
2 | 进门是餐厅好吗 | 进门见餐厅好吗 |
3 | 怎么提高理解力? | 怎样提高理解力 |
4 | 仓鼠用什么磨牙都可以吗 | 用什么可以代替仓鼠的磨牙棒 |
#定义预测的data_loader
# 预测数据的转换函数
# 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)]
# 因为测评集的数据没用label,因此需要做特殊调整
def read_file(data_path):
with open(data_path, 'r', encoding='utf-8') as f:
# 跳过列名
next(f)
for line in f:
text1, text2 = line.strip('\t').split("♬")[0].strip(),line.strip('\t').split("♬")[1].strip()
text1 = line.strip('\t').split("♬")[0].strip()
text2 = line.strip('\t').split("♬")[1].strip()
yield ({'text1': text1, 'text2': text2})
#将测评集的数据进行格式转换
validation_data.to_csv('validation_data.csv', index=False,encoding='utf-8',sep ='♬',header =['text1', 'text2'])
#加载测评集数据
validation_dataset = load_dataset(read_file, data_path='validation_data.csv',lazy=False, split="val")
batch_sampler = paddle.io.BatchSampler(validation_dataset, batch_size=32, shuffle=False)
# 生成预测数据 data_loader
validation_data_loader =paddle.io.DataLoader(
dataset=validation_dataset.map(trans_func),
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
# 加载训练好的模型
state_dict = paddle.load("./checkpoints/model_state.pdparams")
model.set_dict(state_dict)
#预测函数 import paddle.nn.functional as F def prediciton(model, data_loader): model.eval() prediction_list = [] label_map = {0: '0', 1: '1'} with paddle.no_grad(): for batch in data_loader: input_ids, token_type_ids = batch input_ids = paddle.to_tensor(input_ids) token_type_ids = paddle.to_tensor(token_type_ids) # 获取每个样本的预测概率: [batch_size, 2] 的矩阵 outputs=model(input_ids=input_ids,token_type_ids=token_type_ids) probs = F.softmax(outputs, axis=-1) idx = paddle.argmax(probs, axis=1).numpy() idx = idx.tolist() labels = [label_map[i] for i in idx] prediction_list.extend(labels) return prediction_list
# 执行预测函数
test_result=prediciton(model,validation_data_loader)
pd.DataFrame(test_result).to_csv('submit.csv', header=None, index=None)
这个项目还有很多值得优化的地方,如:
本人是AI达人特训营第二期项目中的一名学员,非常有幸能与大家分享自己的所思所想。
作者:范远展 指导导师:黄灿桦
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。