『行远见大』短文本相似度计算 baseline 78.416%

项目简介

文本相似度作业 baseline,各位同学可在此基础上调优。

向开源致敬!

大家好,我是行远见大。欢迎你与我一同建设飞桨开源社区,知识分享是一种美德,让我们向开源致敬!

2021年7日打卡营大作业

大家好,这里是2021年7日打卡营大作业,本次作业内容为实现文本相似度任务,通过课上所学知识,实现文本相似度任务的代码。目前已经给出了基于SimilarityNet的相似度任务的实现代码,同学们可以基于本代码进行修改或者重写,实现更高的准确率,其中数据集已经上传,如果有误操,可以联系qq群助教,获得数据集。

【作业内容】

按照本项目给出的模块完成代码并跑通。

【作业提交】

我们会给出一个数据集压缩包,包含baidu_train.tsv,baidu_dev.tsv,vocab.txttest_forstu.tsv。其中baidu_train.tsv,baidu_dev.tsv,vocab.txt用来训练模型。最后大家根据test_forstu.tsv测试数据输出预测结果。命名为result.tsv的文件并提交。

note:遇到问题请及时向qq群的助教反馈。

【评分标准】

优秀学员:

条件:

  1. 完成每天的课后作业;
  2. 大作业代码运行成功且有结果;
  3. 准确率排名前26名。

奖品设置:

等级奖品名额
一等奖小度在家1
二等奖耳机5
三等奖10
四等奖健身包10
鼓励奖招财熊170

1. 任务介绍

1.1 任务内容

文本语义匹配是自然语言处理中一个重要的基础问题,NLP领域的很多任务都可以抽象为文本匹配任务。例如,信息检索可以归结为查询项和文档的匹配,问答系统可以归结为问题和候选答案的匹配,对话系统可以归结为对话和回复的匹配。语义匹配在搜索优化、推荐系统、快速检索排序、智能客服上都有广泛的应用。如何提升文本匹配的准确度,是自然语言处理领域的一个重要挑战。

  • 信息检索:在信息检索领域的很多应用中,都需要根据原文本来检索与其相似的其他文本,使用场景非常普遍。
  • 新闻推荐:通过用户刚刚浏览过的新闻标题,自动检索出其他的相似新闻,个性化地为用户做推荐,从而增强用户粘性,提升产品体验。
  • 智能客服:用户输入一个问题后,自动为用户检索出相似的问题和答案,节约人工客服的成本,提高效率。

1.2 什么是文本匹配?

让我们来看一个简单的例子,比较各候选句子哪句和原句语义更相近

原句:“车头如何放置车牌”

  • 比较句1:“前牌照怎么装”
  • 比较句2:“如何办理北京车牌”
  • 比较句3:“后牌照怎么装”

(1)比较句1与原句,虽然句式和语序等存在较大差异,但是所表述的含义几乎相同

(2)比较句2与原句,虽然存在“如何” 、“车牌”等共现词,但是所表述的含义完全不同

(3)比较句3与原句,二者讨论的都是如何放置车牌的问题,只不过一个是前牌照,另一个是后牌照。二者间存在一定的语义相关性。

所以语义相关性,句1大于句3,句3大于句2.这就是语义匹配。

1.3 短文本语义匹配网络

短文本语义匹配(SimilarityNet, SimNet)是一个计算短文本相似度的框架,可以根据用户输入的两个文本,计算出相似度得分。主要包括BOW、CNN、RNN、MMDNN等核心网络结构形式,提供语义相似度计算训练和预测框架,适用于信息检索、新闻推荐、智能客服等多个应用场景,帮助企业解决语义匹配问题。

SimNet模型结构如图所示,包括输入层、表示层以及匹配层。



图 SimilarityNet 框架

1.4 实验设计

本任务给出一个文本相似度任务实现方法,模型框架结构图如下图所示,其中query和title是数据集经过处理后的待匹配的文本,然后经过分词处理,编码成id,经过SimilarityNet处理,得到输出,训练的损失函数使用的是交叉熵损失。



图 SimilarityNet实验流程

2. 实验详细实现

文本相似度实验流程包含如下6个步骤:

  1. 数据读取:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取;
  2. 模型构建:设计基于GRU的文本相似度模型,判断两句话是否是相似;
  3. 训练配置:实例化模型,选择模型计算资源(CPU或者GPU),指定模型迭代的优化算法;
  4. 模型训练与保存:执行多轮训练不断调整参数,以达到较好的效果,保存模型;
  5. 模型评估:训练好的模型在测试集合上测试;
  6. 模型预测:加载训练好的模型,并进行预测;

2.1 数据读取

本实验共计需要读取四份数据: 训练集 train.tsv、验证集 dev.tsv、测试集 test.tsv 和 词汇表 vocab.txt。加载数据的代码如下:

  • 加载第三方库,paddle和paddlenlp相关的库
import math
import numpy as np
import os
import collections
from functools import partial
import random
import time
import inspect
import importlib
from tqdm import tqdm

import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.io import IterableDataset
from paddle.utils.download import get_path_from_url
print("本项目基于Paddle的版本号为:"+ paddle.__version__)
本项目基于Paddle的版本号为:2.0.2
  • 本实验需要依赖与paddlenlp,aistudio上的paddlenlp版本过低,所以需要首先升级paddlenlp
!pip install paddlenlp --upgrade
  • 导入paddlenlp相关的包
import paddlenlp as ppnlp
from paddlenlp.data import JiebaTokenizer, Pad, Stack, Tuple, Vocab
# from utils import convert_example
from paddlenlp.datasets import MapDataset
from paddle.dataset.common import md5file
from paddlenlp.datasets import DatasetBuilder
def convert_example(example, tokenizer, is_test=False):
    """
    为序列分类任务从序列生成模型输入
    """
    
    query, title = example["query"], example["title"]
    # query id生成
    query_ids = np.array(tokenizer.encode(query), dtype="int64")
    query_seq_len = np.array(len(query_ids), dtype="int64")

    # title id 生成
    title_ids = np.array(tokenizer.encode(title), dtype="int64")
    title_seq_len = np.array(len(title_ids), dtype="int64")
    
    # 训练模式
    if not is_test:
        label = np.array(example["label"], dtype="int64")
        return query_ids, title_ids, query_seq_len, title_seq_len, label
    else:
        # 测试模式
        return query_ids, title_ids, query_seq_len, title_seq_len
class BAIDUData(DatasetBuilder):

    SPLITS = {
        'train':os.path.join('data', 'baidu_train.tsv'),
        'dev': os.path.join('data', 'baidu_dev.tsv'),
    }

    def _get_data(self, mode, **kwargs):
        filename = self.SPLITS[mode]
        return filename

    def _read(self, filename):
        """读取数据"""
        with open(filename, 'r', encoding='utf-8') as f:
            head = None
            for line in f:
                data = line.strip().split("\t")
                if not head:
                    head = data
                else:
                    query, title, label = data
                    yield {"query": query, "title": title, "label": label}

    def get_labels(self):
        return ["0", "1"]
def load_dataset(name=None,
                 data_files=None,
                 splits=None,
                 lazy=None,
                 **kwargs):
   
    reader_cls = BAIDUData
    print(reader_cls)
    if not name:
        reader_instance = reader_cls(lazy=lazy, **kwargs)
    else:
        reader_instance = reader_cls(lazy=lazy, name=name, **kwargs)

    datasets = reader_instance.read_datasets(data_files=data_files, splits=splits)
    return datasets
  • 加载词汇表
vocab_path="vocab.txt"

vocab = Vocab.load_vocabulary(vocab_path, unk_token='[UNK]', pad_token='[PAD]')
  • 加载训练集,验证集,测试集
# Loads dataset.
train_ds, dev_ds = load_dataset(splits=["train", "dev"])
<class '__main__.BAIDUData'>
  • 创建dataloader
def create_dataloader(dataset,
                      trans_fn=None,
                      mode='train',
                      batch_size=1,
                      batchify_fn=None):
    if trans_fn:
        dataset = dataset.map(trans_fn)

    shuffle = True if mode == 'train' else False
    if mode == "train":
        sampler = paddle.io.DistributedBatchSampler(
            dataset=dataset, batch_size=batch_size, shuffle=True)
    else:
        sampler = paddle.io.BatchSampler(
            dataset=dataset, batch_size=batch_size, shuffle=shuffle)
    dataloader = paddle.io.DataLoader(
        dataset,
        batch_sampler=sampler,
        return_list=True,
        collate_fn=batchify_fn)
    return dataloader
# Reads data and generates mini-batches.
batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=vocab.token_to_idx.get('[PAD]', 0)),  # query_ids
        Pad(axis=0, pad_val=vocab.token_to_idx.get('[PAD]', 0)),  # title_ids
        Stack(dtype="int64"),  # query_seq_lens
        Stack(dtype="int64"),  # title_seq_lens
        Stack(dtype="int64")   # label
    ): [data for data in fn(samples)]
tokenizer = ppnlp.data.JiebaTokenizer(vocab)

trans_fn = partial(convert_example, tokenizer=tokenizer, is_test=False)

2.2 模型构建

本节实验构建了基于GRU的SimilarityNet网络结构,如图所示。模型得输入是query和title,然后分别经过embedding层,GRU层,把GRU输出得向量拼接(concat操作),最后经过FC(全连接)层。


图 基于GRU的SimNet网络结构

class GRUEncoder(nn.Layer):

    def __init__(self,
                 input_size,
                 hidden_size,
                 num_layers=1,
                 direction="forward",
                 dropout=0.0,
                 pooling_type=None,
                 **kwargs):
        super().__init__()
        # 输入的大小
        self._input_size = input_size
        # 隐藏层的大小
        self._hidden_size = hidden_size
        self._direction = direction
        self._pooling_type = pooling_type
        # 实例化GRU层
        self.gru_layer = nn.GRU(input_size=input_size,
                                hidden_size=hidden_size,
                                num_layers=num_layers,
                                direction=direction,
                                dropout=dropout,
                                **kwargs)

    def get_input_dim(self):
        """
        Returns the dimension of the vector input for each element in the sequence input
        to a `GRUEncoder`. This is not the shape of the input tensor, but the
        last element of that shape.
        """
        return self._input_size

    def get_output_dim(self):
        """
        Returns the dimension of the final vector output by this `GRUEncoder`.  This is not
        the shape of the returned tensor, but the last element of that shape.
        """
        if self._direction == "bidirect":
            return self._hidden_size * 2
        else:
            return self._hidden_size

    def forward(self, inputs, sequence_length):
        
        encoded_text, last_hidden = self.gru_layer(
            inputs, sequence_length=sequence_length)
        if not self._pooling_type:
            # We exploit the `last_hidden` (the hidden state at the last time step for every layer)
            # to create a single vector.
            # If gru is not bidirection, then output is the hidden state of the last time step 
            # at last layer. Output is shape of `(batch_size, hidden_size)`.
            # If gru is bidirection, then output is concatenation of the forward and backward hidden state 
            # of the last time step at last layer. Output is shape of `(batch_size, hidden_size*2)`.
            if self._direction != 'bidirect':
                output = last_hidden[-1, :, :]
            else:
                output = paddle.concat(
                    (last_hidden[-2, :, :], last_hidden[-1, :, :]), axis=1)
        else:
            # We exploit the `encoded_text` (the hidden state at the every time step for last layer)
            # to create a single vector. We perform pooling on the encoded text.
            # The output shape is `(batch_size, hidden_size*2)` if use bidirectional GRU, 
            # otherwise the output shape is `(batch_size, hidden_size*2)`.
            if self._pooling_type == 'sum':
                output = paddle.sum(encoded_text, axis=1)
            elif self._pooling_type == 'max':
                output = paddle.max(encoded_text, axis=1)
            elif self._pooling_type == 'mean':
                output = paddle.mean(encoded_text, axis=1)
            else:
                raise RuntimeError(
                    "Unexpected pooling type %s ."
                    "Pooling type must be one of sum, max and mean." %
                    self._pooling_type)
        return output
class GRUModel(nn.Layer):
    def __init__(self,
                 vocab_size,
                 num_classes,
                 emb_dim=128,
                 padding_idx=0,
                 gru_hidden_size=128,
                 direction='forward',
                 gru_layers=1,
                 dropout_rate=0.0,
                 pooling_type=None,
                 fc_hidden_size=96):
        super().__init__()
        # 词嵌入层
        self.embedder = nn.Embedding(
            num_embeddings=vocab_size,
            embedding_dim=emb_dim,
            padding_idx=padding_idx)
        # GRUb编码层
        self.gru_encoder = GRUEncoder(
            emb_dim,
            gru_hidden_size,
            num_layers=gru_layers,
            direction=direction,
            dropout=dropout_rate)
        # 线性层
        self.fc = nn.Linear(self.gru_encoder.get_output_dim() * 2,fc_hidden_size)
        # 输出层
        self.output_layer = nn.Linear(fc_hidden_size, num_classes)

    def forward(self, query, title, query_seq_len, title_seq_len):
        # Shape: (batch_size, num_tokens, embedding_dim)
        embedded_query = self.embedder(query)
        embedded_title = self.embedder(title)
        # Shape: (batch_size, gru_hidden_size)
        query_repr = self.gru_encoder(
            embedded_query, sequence_length=query_seq_len)
        title_repr = self.gru_encoder(
            embedded_title, sequence_length=title_seq_len)
        # Shape: (batch_size, 2*gru_hidden_size)
        contacted = paddle.concat([query_repr, title_repr], axis=-1)
        # Shape: (batch_size, fc_hidden_size)
        fc_out = paddle.tanh(self.fc(contacted))
        # Shape: (batch_size, num_classes)
        logits = self.output_layer(fc_out)
        # probs = F.softmax(logits, axis=-1)

        return logits
class SimNet(nn.Layer):
    def __init__(self,
                 network,
                 vocab_size,
                 num_classes,
                 emb_dim=128,
                 pad_token_id=0):
        super().__init__()
        # 转换成小写
        network = network.lower()
        # gru网络
        if network == 'gru':
            self.model = GRUModel(
                vocab_size,
                num_classes,
                emb_dim,
                direction='forward',
                padding_idx=pad_token_id)

    def forward(self, query, title, query_seq_len=None, title_seq_len=None):
        logits = self.model(query, title, query_seq_len, title_seq_len)
        return logits

2.3 训练配置

# 检测是否可以使用GPU,如果可以优先使用GPU
use_gpu = True if paddle.get_device().startswith("gpu") else False
if use_gpu:
    paddle.set_device('gpu')

batch_size=64   # batch size 的大小
  • 实例化SimilarityNet网络
# 构建SimNet网络,网络的主体采用GRUb网络
network='gru'
model = SimNet(
        network=network,
        vocab_size=len(vocab),
        num_classes=len(train_ds.label_list))
model = paddle.Model(model)
  • 构建训练集,验证集和测试集的dataloader
# 构建训练集的dataloader
train_loader = create_dataloader(
        train_ds,
        trans_fn=trans_fn,
        batch_size=batch_size,
        mode='train',
        batchify_fn=batchify_fn)
# 构建验证集的dataloader
dev_loader = create_dataloader(
        dev_ds,
        trans_fn=trans_fn,
        batch_size=batch_size,
        mode='validation',
        batchify_fn=batchify_fn)
# # 构建测试集的dataloader
# test_loader = create_dataloader(
#         test_ds,
#         trans_fn=trans_fn,
#         batch_size=batch_size,
#         mode='test',
#         batchify_fn=batchify_fn)
  • 定义优化器
lr=4e-3
optimizer = paddle.optimizer.Adam(
        parameters=model.parameters(), learning_rate=lr)
  • 实例化损失函数
# Defines loss and metric.
criterion = paddle.nn.CrossEntropyLoss()  # 交叉熵损失函数
  • 实例化评估方式
metric = paddle.metric.Accuracy()  # accuracy 评估方式
  • 编译模型
model.prepare(optimizer, criterion, metric)

2.4 模型训练

  • 模型训练使用paddle的高级API,使用fit函数就可以实现训练和模型保存,模型保存的位置为save_dir
epochs=3
save_dir='./checkpoints'
# Starts training and evaluating.
model.fit(
        train_loader,
        dev_loader,
        epochs=epochs,
        save_dir=save_dir)

2.5 模型评估

  • 模型评估实用paddle高级API自带的evaluate函数就可以实现评估,评估的方式是accuracy
# Finally tests model.
results = model.evaluate(dev_loader)
print("Finally test acc: %.5f" % results['acc'])
Eval begin...
The loss value printed in the log is the current batch, and the metric is the average value of previous step.
step  10/122 - loss: 0.4328 - acc: 0.7578 - 17ms/step
step  20/122 - loss: 0.5026 - acc: 0.7758 - 15ms/step
step  30/122 - loss: 0.5133 - acc: 0.7797 - 14ms/step
step  40/122 - loss: 0.5651 - acc: 0.7895 - 14ms/step
step  50/122 - loss: 0.7246 - acc: 0.7803 - 13ms/step
step  60/122 - loss: 0.5053 - acc: 0.7771 - 13ms/step
step  70/122 - loss: 0.7614 - acc: 0.7799 - 13ms/step
step  80/122 - loss: 0.6370 - acc: 0.7805 - 13ms/step
step  90/122 - loss: 0.5377 - acc: 0.7783 - 13ms/step
step 100/122 - loss: 0.4521 - acc: 0.7805 - 13ms/step
step 110/122 - loss: 0.3851 - acc: 0.7820 - 13ms/step
step 120/122 - loss: 0.5549 - acc: 0.7835 - 12ms/step
step 122/122 - loss: 0.5174 - acc: 0.7842 - 12ms/step
Eval samples: 7802
Finally test acc: 0.78416

2.6 模型预测

def predict(model, data, label_map, batch_size=1, pad_token_id=0):
    """
    预测数据的标签
    """
    # 把数据分成若干个batch
    batches = [
        data[idx:idx + batch_size] for idx in range(0, len(data), batch_size)
    ]

    batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=pad_token_id),  # query_ids
        Pad(axis=0, pad_val=pad_token_id),  # title_ids
        Stack(dtype="int64"),  # query_seq_lens
        Stack(dtype="int64"),  # title_seq_lens
    ): [data for data in fn(samples)]

    results = []
    model.eval()
    for batch in batches:
        query_ids, title_ids, query_seq_lens, title_seq_lens = batchify_fn(
            batch)
        query_ids = paddle.to_tensor(query_ids)
        title_ids = paddle.to_tensor(title_ids)
        query_seq_lens = paddle.to_tensor(query_seq_lens)
        title_seq_lens = paddle.to_tensor(title_seq_lens)
        logits = model(query_ids, title_ids, query_seq_lens, title_seq_lens)
        probs = F.softmax(logits, axis=1)
        idx = paddle.argmax(probs, axis=1).numpy()
        idx = idx.tolist()
        labels = [label_map[i] for i in idx]
        results.extend(labels)
    return results
  • 建立id到类别的映射,本实验有两个类别,编码为0,1。0类为dissimilar,1类为similar
label_map = {0: 'dissimilar', 1: 'similar'}
  • 加载训练的模型的参数
# 实例化SimNet模型
model = SimNet(network=network, vocab_size=len(vocab), num_classes=len(label_map))
params_path='./checkpoints/final.pdparams'
#  加载模型参数
state_dict = paddle.load(params_path)
model.set_dict(state_dict)
print("Loaded parameters from %s" % params_path)
Loaded parameters from ./checkpoints/final.pdparams
def preprocess_prediction_data(data, tokenizer):
    examples = []
    for query, title in data:
        query_ids = tokenizer.encode(query)
        title_ids = tokenizer.encode(title)
        examples.append([query_ids, title_ids, len(query_ids), len(title_ids)])
    return examples
# Firstly pre-processing prediction data  and then do predict.
# 将data替换为我们给出的测试集test_forstu.tsv,输出预测结果result.tsv并提交。
data = [
        ['世界上什么东西最小', '世界上什么东西最小?'],
        ['光眼睛大就好看吗', '眼睛好看吗?'],
        ['小蝌蚪找妈妈怎么样', '小蝌蚪找妈妈是谁画的'],
    ]
examples = preprocess_prediction_data(data, tokenizer)
results = predict(
        model,
        examples,
        label_map=label_map,
        batch_size=batch_size,
        pad_token_id=vocab.token_to_idx.get('[PAD]', 0))

for idx, text in enumerate(data):
    print('Data: {} \t Label: {}'.format(text, results[idx]))
Data: ['世界上什么东西最小', '世界上什么东西最小?'] 	 Label: similar
Data: ['光眼睛大就好看吗', '眼睛好看吗?'] 	 Label: dissimilar
Data: ['小蝌蚪找妈妈怎么样', '小蝌蚪找妈妈是谁画的'] 	 Label: dissimilar
import pandas as pd
import csv

# 查看数据
data = pd.read_csv('test_forstu.tsv', sep='\t', header=0)
data
text_atext_b
0雪弗兰创酷要多少钱?这酒多少钱?
1我开了一个汽车轮胎店怎样做才能是店里的生意好起来大家说开汽车轮胎店,补车抬,这种店好吗?
2广东省有多少个县级市?广东省有多少个地级市?
3哪里可以下载到国语版的韩剧。国语版韩剧在哪里下载,要能下载的,不要在线看。
4怎样注销淘宝号淘宝号怎么注销啊?
.........
15599高考地理非选择题有没有答题技巧关于地理高考答题思路
15600世界上有没有吃人的狗世界上最大的蛇吃人,是什么蛇?
15601什么矿泉水好喝?什么矿泉水好喝
15602求这个日本女演员的姓名!求这个日本女演员姓名。
15603肉如何快速解冻?怎么能让肉快速解冻

15604 rows × 2 columns

print(data.shape)
(15604, 2)
test_file = 'test_forstu.tsv'
data = pd.read_csv(test_file, sep='\t')
data1 = list(data.values)
label_map = {0: '0', 1: '1'}

2.7 效果展示

examples = preprocess_prediction_data(data1, tokenizer)
results = predict(
        model,
        examples,
        label_map=label_map,
        batch_size=batch_size,
        pad_token_id=vocab.token_to_idx.get('[PAD]', 0))


for idx, text in enumerate(data1):
        print('Data: {} \t Label: {}'.format(text, results[idx]))
data2 = []
for i in range(len(data1)):
        data2.extend(results[i])

# print(data2)
data['label'] = data2
print(data.shape)

2.8 存储数据

data.to_csv('result.csv',sep='\t')
data_watch = pd.read_csv('result.csv', sep='\t', header=0)
v', sep='\t', header=0)
data_watch

3. 项目总结

本项目是《零基础实践深度学习七日打卡营》比赛基线,在经过多次实践,纯调参的极限在78%,要想达到更高的准确度必须上预训练模型。ENRIE is all you need!

作者简介

  • 作者:行远见大
  • 经历:一枚学 AI 刚满三个月的零基础编程小白
  • 我的口号:向开源致敬,一同建设飞桨开源社区
  • 常住地址:常年混迹在 AI Studio 平台和各类 PaddlePaddle 群
  • QQ:1206313185 添加时请备注添加原因和 AI Studio 的 ID
  • 感谢小伙伴们一键三连(喜欢♡、fork〧、关注+)支持,点 ♡ 数越多,更新越快~
Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐