越学越有趣:『手把手带你学NLP』系列项目02 ——语义相似度计算的那些事儿...
点击左上方蓝字关注我们
课程简介
“手把手带你学NLP”是基于飞桨PaddleNLP的系列实战项目。本系列由百度多位资深工程师精心打造,提供了从词向量、预训练语言模型,到信息抽取、情感分析、文本问答、结构化数据问答、文本翻译、机器同传、对话系统等实践项目的全流程讲解,旨在帮助开发者更全面清晰地掌握百度飞桨框架在NLP领域的用法,并能够举一反三、灵活使用飞桨框架和PaddleNLP进行NLP深度学习实践。
从6月7日起,百度飞桨 & 自然语言处理部携手推出了12节NLP精品课,课程中会介绍到这里的实践项目。
课程报名请戳:
https://aistudio.baidu.com/aistudio/course/introduce/24177
欢迎来课程QQ群(群号:758287592)交流吧~~
本案例介绍 NLP 最基本的任务类型之一 —— 文本语义匹配,并且基于 PaddleNLP 使用百度开源的预训练模型 ERNIE-Gram 搭建效果优异的语义匹配模型,来判断两段文本语义是否相同。
1. 背景介绍
文本语义匹配任务,简单来说就是给定两段文本,让模型来判断两段文本是不是语义相似。
在本案例中以权威的语义匹配数据集LCQMC为例,LCQMC数据集是基于百度知道相似问题推荐构造的通问句语义匹配数据集。训练集中的每两段文本都会被标记为 1(语义相似) 或者 0(语义不相似)。
例如百度知道场景下,用户搜索一个问题,模型会计算这个问题与候选问题是否语义相似,语义匹配模型会找出与问题语义相似的候选问题返回给用户,加快用户提问-获取答案的效率。例如,当某用户在搜索引擎中搜索 “深度学习的教材有哪些?”,模型就自动找到了一些语义相似的问题展现给用户:
2.快速实践
本节主要介绍如何准备数据,基于 ERNIE-Gram 模型搭建匹配网络,然后快速进行语义匹配模型的训练、评估和预测。
2.1 数据加载
为了训练匹配模型,一般需要准备三个数据集:训练集 train.tsv、验证集dev.tsv、测试集test.tsv。此案例我们使用 PaddleNLP 内置的语义数据集 LCQMC 来进行训练、评估、预测。
LCQMC 数据集是公开的语义匹配权威数据集。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
import paddlenlp
# 一键加载 Lcqmc 的训练集、验证集
train_ds, dev_ds = load_dataset("lcqmc", splits=["train", "dev"])
# 输出训练集的前 3 条样本
for idx, example in enumerate(train_ds):if idx <= 3:print(example)
{'query': '喜欢打篮球的男生喜欢什么样的女生', 'title': '爱打篮球的男生喜欢什么样的女生', 'label': 1}
{'query': '我手机丢了,我想换个手机', 'title': '我想买个新手机,求推荐', 'label': 1}
{'query': '大家觉得她好看吗', 'title': '大家觉得跑男好看吗?', 'label': 0}
{'query': '求秋色之空漫画全集', 'title': '求秋色之空全集漫画', 'label': 1}
2.2 数据预处理
通过 PaddleNLP 加载进来的 LCQMC 数据集是原始的明文数据集,这部分我们来实现组 batch、tokenize 等预处理逻辑,将原始明文数据转换成网络训练的输入数据。
定义样本转换函数
# 因为是基于预训练模型 ERNIE-Gram 来进行,所以需要首先加载 ERNIE-Gram 的 tokenizer,# 后续样本转换函数基于 tokenizer 对文本进行切分
tokenizer = paddlenlp.transformers.ErnieGramTokenizer.from_pretrained('ernie-gram-zh')
# 将 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
### 对训练集的第 1 条数据进行转换
input_ids, token_type_ids, label = convert_example(train_ds[0], tokenizer)
print(input_ids)
[1, 692, 811, 445, 2001, 497, 5, 654, 21, 692, 811, 614, 356, 314, 5, 291, 21, 2, 329, 445, 2001, 497, 5, 654, 21, 692, 811, 614, 356, 314, 5, 291, 21, 2]
print(token_type_ids)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
print(label)
[1]
# 为了后续方便使用,我们给 convert_example 赋予一些默认参数
from functools import partial
# 训练集和验证集的样本转换函数
trans_func = partial(convert_example,tokenizer=tokenizer,max_seq_length=512)
组装 Batch 数据 & Padding
上一小节,我们完成了对单条样本的转换,本节我们需要将样本组合成 Batch 数据,对于不等长的数据还需要进行 Padding 操作,便于 GPU 训练。
PaddleNLP 提供了许多关于 NLP 任务中构建有效的数据 pipeline 的常用 API:
更多数据处理操作详见:
https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/data.md
from paddlenlp.data import Stack, Pad, Tuple
# 我们的训练数据会返回 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), # input_idsPad(axis=0, pad_val=tokenizer.pad_token_type_id), # token_type_idsStack(dtype="int64") # label
): [data for data in fn(samples)]
定义 Dataloader
下面我们基于组 batchify_fn 函数和样本转换函数 trans_func 来构造训练集的 DataLoader, 支持多卡训练
# 定义分布式 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)
2.3 模型搭建
自从 2018 年 10 月以来,NLP 个领域的任务都通过 Pretrain + Finetune 的模式相比传统 DNN 方法在效果上取得了显著的提升,本节我们以百度开源的预训练模型 ERNIE-Gram 为基础模型,在此之上构建 Point-wise 语义匹配网络。
首先我们来定义网络结构
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_modelself.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)
2.4 模型训练 & 评估
from paddlenlp.transformers import LinearDecayWithWarmup
epochs = 3
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 = batchprobs = 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 = batchprobs = 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)
模型训练过程中会输出如下日志
global step 5310, epoch: 3, batch: 1578, loss: 0.31671, accu: 0.95000, speed: 0.63 step/s
global step 5320, epoch: 3, batch: 1588, loss: 0.36240, accu: 0.94063, speed: 6.98 step/s
global step 5330, epoch: 3, batch: 1598, loss: 0.41451, accu: 0.93854, speed: 7.40 step/s
···········
基于默认参数配置进行单卡训练大概要持续 4 个小时左右,会训练完成 3 个 Epoch, 模型最终的收敛指标结果如下:
可以看到: 我们基于 PaddleNLP ,利用 ERNIE-Gram 预训练模型使用非常简洁的代码,就在权威语义匹配数据集上取得了很不错的效果。
2.5 模型预测
接下来我们使用已经训练好的语义匹配模型对一些预测数据进行预测。待预测数据为每行都是文本对的.tsv文件,我们使用Lcqmc数据集的测试集作为我们的预测数据。
定义预测函数
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_datainput_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
定义预测数据的 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_idsPad(axis=0, pad_val=tokenizer.pad_token_type_id), # segment_ids
): [data for data in fn(samples)]
# 加载预测数据
test_ds = load_dataset("lcqmc", splits=["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)
定义预测模型
pretrained_model = paddlenlp.transformers.ErnieGramModel.from_pretrained('ernie-gram-zh')
model = PointwiseMatching(pretrained_model)
加载已训练好的模型参数
# 刚才下载的模型解压之后存储路径为 ./ernie_gram_zh_pointwise_matching_model/model_state.pdparams
state_dict = paddle.load("./ernie_gram_zh_pointwise_matching_model/model_state.pdparams")
model.set_dict(state_dict)
开始预测
for idx, batch in enumerate(predict_data_loader):if idx < 1:print(batch)
# 执行预测函数
y_probs = predict(model, predict_data_loader)
# 根据预测概率获取预测 label
y_preds = np.argmax(y_probs, axis=1)
输出预测结果
# 我们按照千言文本相似度竞赛的提交格式将预测结果存储在 lcqmc.tsv 中,用来后续提交# 同时将预测结果输出到终端,便于大家直观感受模型预测效果
test_ds = load_dataset("lcqmc", splits=["test"])
with open("lcqmc.tsv", 'w', encoding="utf-8") as f:f.write("index\tprediction\n") for idx, y_pred in enumerate(y_preds):f.write("{}\t{}\n".format(idx, y_pred))text_pair = test_ds[idx]text_pair["label"] = y_predprint(text_pair)
{'query': '女生青春期都会什么样?', 'title': '青春期女生应该穿什么样的内衣', 'label': 0}
{'query': '瞎子背瞎子打一谚语', 'title': '瞎子背瞎子(打一谚语)', 'label': 1}
{'query': '请问这个日语是什么意思?', 'title': '请问日语(啊嘛喋)是什么意思?', 'label': 0}
{'query': '我应该用什么面膜比较好', 'title': '现在用什么面膜比较好', 'label': 1}
{'query': '一动不动写词语', 'title': '关于一动不动的词语有那些', 'label': 1}
········
动手试一试
是不是觉得很有趣呀。小编强烈建议初学者参考上面的代码亲手敲一遍,因为只有这样,才能加深你对代码的理解呦。
本次项目对应的代码:
https://aistudio.baidu.com/aistudio/projectdetail/2029701
除此之外, PaddleNLP提供了多种预训练模型,可一键调用,来更换一下预训练试试吧:
https://paddlenlp.readthedocs.io/zh/latest/model_zoo/transformers.html
更多PaddleNLP信息,欢迎访问GitHub点star收藏后体验:
https://github.com/PaddlePaddle/PaddleNLP
加入交流群,一起学习吧
如果你在学习过程中遇到任何问题或疑问,欢迎加入PaddleNLP的QQ技术交流群!
回顾往期
越学越有趣:『手把手带你学NLP』系列项目01 ——词向量应用的那些事儿
飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个自主研发、功能完备、开源开放的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。
END
越学越有趣:『手把手带你学NLP』系列项目02 ——语义相似度计算的那些事儿...相关推荐
- 越学越有趣:『手把手带你学NLP』系列项目01 ——词向量应用的那些事儿
点击左上方蓝字关注我们 课程简介 "手把手带你学NLP"是基于飞桨PaddleNLP的系列实战项目.本系列由百度多位资深工程师精心打造,提供了从词向量.预训练语言模型,到信息抽取. ...
- 【手把手带你学Java EE】多线程那些事,你了解了吗?
[手把手带你学Java EE]多线程那些事,你了解了吗? 线程 概念 意义 进程和线程的区别 面试题:谈谈进程和线程的区别和联系 Java中的多线程编程 创建线程的方法 方法一 方法二 方法三 &am ...
- Yhen手把手带你使用百度智能云①----项目的创建
声明: 以下内容为本人原创,仅供用于参考学习 禁止用于商业及违法用途 ·作者:@Yhen ·原文网站:CSDN ·原文链接:https://blog.csdn.net/Yhen1/article/de ...
- 过节的快乐,在于Free | 手把手带你学Django
一开始入坑Django,你总是会遭遇诸如此类的迷茫和困惑,比如, 大神推荐,看官方文档就够了,这么多文档,都要看完吗? 如何规划自己的Python web成长路径? Django开发的未来在哪里? 学 ...
- python讲1020逆序输出_手把手带你学 Python3(九)| 快速实现数据处理的不二工具(文末有彩蛋)...
", line 1, in TypeError: 'int' object is not callable #当然实际代码绝对不能这么写,这里是为了说明函数名也是变量.要恢复abs函数,请重 ...
- 【手把手带你学nRF52832/nRF52840 · 开发环境搭建】
1.首先去Nordic官网下载SDK SDK下载链接 ▼点击选定下图中的17.0.2版本,可以看到目前最新的版本是17.1.0 ▼下拉网页到最后,确认要下载的文件,前面的一些例程可以去掉,保留最后一个 ...
- 手把手带你学python自动化测试(五)——鼠标键盘操作
在浏览器中,通常会用到鼠标来进行操作,比如右键菜单中选择一个操作,在 selenium 中提供了下列鼠标相关操作. ActionChains 类似提供了以下方法: context_click() 右击 ...
- 手把手带你学Docker:Docker容器日常管理(四)
运行Docker容器的命令:docker run == docker create + docker start 查看容器 docker ps:查看正在运行的容器 docker ps -a:查看已经创 ...
- 手把手带你学python—牛客网python 机器学习 信息熵的计算
牛客是一款不论是面试 还是刷题 都是非常有用的 还等什么,传送门- 牛客网python基础
最新文章
- rust挖油机怎么整_挖掘机发动机进气管断裂该怎么办?
- Mybatis中typeAliases的使用
- 工作391-选择Hbuilder x作为开发工具
- (32)SystemVerilog语言编写打两拍
- 让Python更加充分的使用Sqlite3
- web.xml中配置:通用的用户登录过滤器(SessionFilter)
- 红米note4android,红米Note4深度评测
- Spring5 系统架构
- 北航软件工程硕士_21考研择校 | 北京航空航天大学软件工程怎么样?
- 软件开发中什么是CI/CD
- matlab-微分方程求解方法汇总
- 屏的接口类型种类以及接口定义分析
- 在一张表里实现省市区镇村五级联动
- linux strace命令--跟踪系统调用
- 13. 利用 条件运算符 的嵌套来完成此题: 学习成绩>=90分的同学A表示, 60-89分之间的用B表示, 60分以下的用C表示
- 模取幂运算 计算a^b mod n
- 拆解报告:爱否开物1A2C 65W PD氮化镓充电器智融SW3516十分表现抢眼
- Android性能优化之网络优化
- Classification:Winnow
- java有丰富的库,【判断题】Java有丰富的库供我们调用
A. 正确
B. 错误
热门文章
- 利用EMCP物联网云平台通过网口远程监控S7-200 Smart PLC云组态
- 关于LuaGC算法的演化概述
- 【华为云-开发者专属集市】使用ECS搭建WordPress博客平台
- GitHub 详细教程
- 什么是TPM? Windows 11 是否会重蹈 Windows Vista 覆辙?
- MediaWiki的权限管理
- SimpleDateFormat 中的12小时制与24小时制
- (转)DataGrid资料
- c语言40h高四位送41h低四位,《单片机原理及接口技术》复习题
- 数学 ( 解高次同余方程 )——Discrete Logarithm Problem ( UVA 7457 )