基本介绍

支持多种内置数据集

基本操作

collect

Stack

Stack用于把输入样本叠加成一个批次,输入样本必须具有相同的长度

from paddlenlp.data import Stack

a = [1, 2, 3, 4]
b = [3, 4, 5, 6]
c = [5, 6, 7, 8]
result = Stack()([a, b, c])
print(result)

执行结果

[[1, 2, 3, 4],
 [3, 4, 5, 6],
 [5, 6, 7, 8]]

Pad

Pad用于将某个元素填充成和最长元素相等的长度

from paddlenlp.data import Pad

a = [1, 2, 3, 4]
b = [5, 6, 7]
c = [8, 9]
result = Pad(pad_val=0)([a, b, c])
print(result)

执行结果

[[1, 2, 3, 4],
 [5, 6, 7, 0],
 [8, 9, 0, 0]]

Tuple

将多个batchify函数包装在一起,输入功能将应用于相应的输入字段

from paddlenlp.data import Stack, Pad, Tuple

data = [
        [[1, 2, 3, 4], [1]],
        [[5, 6, 7], [0]],
        [[8, 9], [1]],
       ]
batchify_fn = Tuple(Pad(pad_val=0), Stack()) # 第一个数据填充,第二个数据堆叠
ids, label = batchify_fn(data)
print(ids)
print(label)

执行结果

ids:
[[1, 2, 3, 4],
[5, 6, 7, 0],
[8, 9, 0, 0]]
label: [[1], [0], [1]]

Dict

将多个batchify函数包装在一起,输入功能将应用于相应的输入字段。根据不同字段名称应用响应的处理。

from paddlenlp.data import Stack, Pad, Dict

data = [
        {'labels':[1], 'token_ids':[1, 2, 3, 4]},
        {'labels':[0], 'token_ids':[5, 6, 7]},
        {'labels':[1], 'token_ids':[8, 9]},
       ]
batchify_fn = Dict({'token_ids':Pad(pad_val=0), # 对token_ids元素填充
                    'labels':Stack()})#对labels元素堆叠
ids, label = batchify_fn(data)
print(ids)
print(label)

执行结果

ids:
[[1, 2, 3, 4],
[5, 6, 7, 0],
[8, 9, 0, 0]]
label: [[1], [0], [1]]

Vocab

从词语转换为索引(顺序数字),或从索引转换为词语

senta_word_dict.zip

from paddlenlp.data import Vocab

# 数据集地址 https://bj.bcebos.com/paddlenlp/data/senta_word_dict.txt

vocab_file_path = './senta_word_dict.txt'

vocab = Vocab.load_vocabulary(  # Initialize the Vocab
    vocab_file_path,
    unk_token='[UNK]',
    pad_token='[PAD]')
tokens = vocab.to_tokens([0, 1, 2, 3])
print(tokens)  # ['[PAD]', '[UNK]', '一斤三', '意面屋']

tokens = vocab.to_indices(['[PAD]', '[UNK]', '一斤三', '意面屋'])
print(tokens)  # [0, 1, 2, 3]

执行结果

['[PAD]', '[UNK]', '一斤三', '意面屋']
[0, 1, 2, 3]

数据集读取

读取内置数据集

from paddlenlp.datasets import load_dataset
# 加载名称为msra_ner的内置数据集
train_ds, test_ds = load_dataset("msra_ner", splits=("train", "test"))

加载本地数据集

from paddlenlp.datasets import load_dataset
# 读取和squad格式相同的本地数据集
train_ds, dev_ds = load_dataset("squad", 
                                data_files=("my_train_file.json", 
                                            "my_dev_file.json"))
test_ds = load_dataset("squad", data_files="my_test_file.json")

从本地文件创建数据集

直接通过函数读取

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:
            words, labels = line.strip('\n').split('\t')
            words = words.split('\002')
            labels = labels.split('\002')
            yield {'tokens': words, 'labels': labels}

# data_path为read()方法的参数
map_ds = load_dataset(read, data_path='train.txt',lazy=False)
iter_ds = load_dataset(read, data_path='train.txt',lazy=True)

通过继承系统类读取

from paddle.io import Dataset
from paddlenlp.datasets import MapDataset

class MyDataset(Dataset):
    def __init__(self, path):

        def load_data_from_source(path):
            ...
            ...
            return data

        self.data = load_data_from_source(path)

    def __getitem__(self, idx):
        return self.data[idx]

    def __len__(self):
        return len(self.data)

ds = MyDataset(data_path)  # paddle.io.Dataset
new_ds = MapDataset(ds)    # paddlenlp.datasets.MapDataset

数据处理

Dataset中通常为原始数据,需要经过一定的数据处理并进行采样组batch,而后通过paddle.io.DataLoader为训练或预测使用,PaddleNLP中为其中各环节提供了相应的功能支持。

基于预训练模型的数据处理

from paddlenlp.transformers import BertTokenizer
from paddlenlp.datasets import load_dataset

tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
train_ds = load_dataset('lcqmc', splits='train')

print(train_ds[0]) # {'query': '喜欢打篮球的男生喜欢什么样的女生', 'title': '爱打篮球的男生喜欢什么样的女生', 'label': 1}

def convert_example(example, tokenizer):
    tokenized_example = tokenizer(text=example['query'],
                                  text_pair=example['title'])
    # 加上label用于训练
    tokenized_example['label'] = [example['label']]
    return tokenized_example

from functools import partial

trans_func = partial(convert_example,
                     tokenizer=tokenizer)

train_ds.map(trans_func)
print(train_ds[0])

执行结果

{'input_ids': [101, 1599, 3614, 2802, 5074, 4413, 4638, 4511, 4495,1599, 3614, 784, 720, 3416, 4638, 1957, 4495, 102,4263, 2802, 5074, 4413, 4638, 4511, 4495, 1599, 3614,784, 720, 3416, 4638, 1957, 4495, 102],
'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],
'label': [1]}

基于非预训练模型的数据处理

from paddlenlp.data import JiebaTokenizer, Vocab
from paddlenlp.datasets import load_dataset

train_ds = load_dataset('chnsenticorp', splits='train')

print(train_ds[0]) # {'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。服务吗,一般', 'label': 1}

# 从本地词典文件构建Vocab
vocab = Vocab.load_vocabulary('./senta_word_dict.txt', 
                              unk_token='[UNK]', 
                              pad_token='[PAD]')

# 使用Vocab初始化JiebaTokenizer
tokenizer = JiebaTokenizer(vocab)

def convert_example(example, tokenizer):
    input_ids = tokenizer.encode(example["text"])
    valid_length = [len(input_ids)]
    label = [example["label"]]
    return input_ids, valid_length, label

trans_fn = partial(convert_example, tokenizer=tokenizer)
train_ds.map(trans_fn)

print(train_ds[0])

执行结果

(
[417329, 128448, 140437, 173188, 118001, 213058, 595790, 1106339, 940533, 947744, 169206, 421258, 908089, 982848, 1106339, 35413, 1055821, 4782, 377145, 4782, 238721, 4782, 642263, 4782, 891683, 767091, 4783, 672971, 774154, 1250380, 1106339, 340363, 146708, 1081122, 4783, 1, 943329, 1008467, 319839, 173188, 909097, 1106339, 1010656, 261577, 1110707, 1106339, 770761, 597037, 1068649, 850865, 4783, 1, 993848, 173188, 689611, 1057229, 1239193, 173188, 1106339, 146708, 427691, 4783, 1, 724601, 179582, 1106339, 1250380],
[67],
[1])

综合项目

波士顿房价预测

housing.zip

# 加载飞桨、Numpy和相关类库
import paddle
from paddle.nn import Linear
import paddle.nn.functional as F
import numpy as np
import os
import random
import matplotlib.pyplot as plt

def load_data():
    # 从文件导入数据
    datafile = './housing.data'
    data = np.fromfile(datafile, sep=' ', dtype=np.float32)

    # 每条数据包括14项,其中前面13项是影响因素,第14项是相应的房屋价格中位数
    feature_names = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE',
                     'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT','MEDV']
    feature_num = len(feature_names)

    # 将原始数据进行Reshape,变成[N, 14]这样的形状
    data = data.reshape([data.shape[0] // feature_num, feature_num])

    # 将原数据集拆分成训练集和测试集
    # 这里使用80%的数据做训练,20%的数据做测试
    # 测试集和训练集必须是没有交集的
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[:offset]

    # 计算train数据集的最大值,最小值,平均值
    maximums = training_data.max(axis=0)
    minimums = training_data.min(axis=0)
    avgs = training_data.sum(axis=0) / training_data.shape[0]

    # 记录数据的归一化参数,在预测时对数据做归一化
    global max_values
    global min_values
    global avg_values
    max_values = maximums
    min_values = minimums
    avg_values = avgs

    # 对数据进行归一化处理
    for i in range(feature_num):
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

    # 训练集和测试集的划分比例
    training_data = data[:offset]
    test_data = data[offset:]

    return training_data, test_data


class Regressor(paddle.nn.Layer):

    # self代表类的实例自身
    def __init__(self):
        # 初始化父类中的一些参数
        super(Regressor, self).__init__()

        # 定义一层全连接层,输入维度是13,输出维度是1
        self.fc = Linear(in_features=13, out_features=1)

    # 网络的前向计算
    def forward(self, inputs):
        x = self.fc(inputs)
        return x


# 声明定义好的线性回归模型
model = Regressor()
# 开启模型训练模式
model.train()
# 加载数据
training_data, test_data = load_data()
# 定义优化算法,使用随机梯度下降SGD
# 学习率设置为0.01
opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())

EPOCH_NUM = 80  # 设置外层循环次数
BATCH_SIZE = 10  # 设置batch大小

iters = []
costs = []

# 定义外层循环
i = 0
for epoch_id in range(EPOCH_NUM):

    np.random.shuffle(training_data) # 随机打乱样本

    # 将训练数据进行拆分,每个batch包含10条数据
    mini_batches = [training_data[k:k + BATCH_SIZE] for k in range(0, len(training_data), BATCH_SIZE)]

    # 定义内层循环
    for iter_id, mini_batch in enumerate(mini_batches):
        i += 1
        iters.append(i)
        # print("shape:", mini_batch.shape) # (10, 14)
        x = np.array(mini_batch[:, :-1])  # 获得当前批次训练数据
        y = np.array(mini_batch[:, -1:])  # 获得当前批次训练标签(真实房价)

        # 将numpy数据转为飞桨动态图tensor形式
        house_features = paddle.to_tensor(x)
        prices = paddle.to_tensor(y)

        # 前向计算
        predicts = model(house_features)

        # 计算损失
        loss = F.square_error_cost(predicts, label=prices)
        avg_loss = paddle.mean(loss)
        costs.append( avg_loss.numpy()[0])
        if iter_id % 20 == 0:
            print("epoch: {}, iter: {}, loss is: {}".format(epoch_id, iter_id, avg_loss.numpy()))

        avg_loss.backward() # 反向传播
        opt.step() # 最小化loss,更新参数
        opt.clear_grad() # 清除梯度

# 保存模型参数,文件名为LR_model.pdparams
paddle.save(model.state_dict(), 'LR_model.pdparams')
print("模型保存成功,模型参数保存在LR_model.pdparams中")


def load_one_example():
    # 从上边已加载的测试集中,随机选择一条作为测试数据
    idx = np.random.randint(0, test_data.shape[0])
    idx = -10
    one_data, label = test_data[idx, :-1], test_data[idx, -1]
    # 修改该条数据shape为[1,13]
    one_data = one_data.reshape([1, -1])

    return one_data, label


# 参数为保存模型参数的文件地址
model_dict = paddle.load('LR_model.pdparams')
model.load_dict(model_dict) #从state_dict设置参数和持久缓冲区
model.eval() # 设置为评估模式, 只影响某些模块,如'Dropout'和'BatchNorm'

# 参数为数据集的文件地址
one_data, label = load_one_example()
# 将数据转为动态图的variable格式
one_data = paddle.to_tensor(one_data)
predict = model(one_data)

# 对结果做反归一化处理
predict = predict * (max_values[-1] - min_values[-1]) + avg_values[-1]
# 对label数据做反归一化处理
label = label * (max_values[-1] - min_values[-1]) + avg_values[-1]

print("Inference result is {}, the corresponding label is {}".format(predict.numpy(), label))

# 绘图
plt.figure("Trainning")
plt.title("Trainning")
plt.plot(iters, costs, color="red", label="cost")
plt.legend()
plt.savefig("trainning.png")
plt.show()

快递单实体抽取

text_a  label
黑龙江省双鸭山市尖山区八马路与东平行路交叉口北40米韦业涛1860000xxxx A1-BA1-IA1-IA1-IA2-BA2-IA2-IA2-IA3-BA3-IA3-IA4-BA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IA4-IP-BP-IP-IT-BT-IT-IT-IT-IT-IT-IT-IT-IT-IT-I
......
  • 实体标记规则:
实体类型 符号 示例
姓名 P 张三
电话 T 13512345678
A1 广东省
A2 深圳市
A3 南山区
详细地址 A4 XXXX路XXXX大厦
  • 位置标记规则:
符号 位置
B 起始位置
I 中间或结束位置
O 无关字符
  • 标注示例:
喻晓*          P-BP-IP-I
云南省          A1-BA1-IA1-I
楚雄彝族自治州  A2-BA2-IA2-IA2-IA2-IA2-IA2-I
南华县          A3-BA3-IA3-I
东街古城路37号    A4-BA4-IA4-IA4-IA4-IA4-IA4-IA4-I
1851338xxxx   T-BT-IT-IT-IT-IT-IT-IT-IT-IT-IT-I
  • 第一步:安装paddlenlp
!pip install paddlenlp==2.1.0
from paddle.utils.download import get_path_from_url
URL = "https://paddlenlp.bj.bcebos.com/paddlenlp/datasets/waybill.tar.gz"
get_path_from_url(URL, "./")
  • 第三步:训练
# 预训练ERNIE模型实现快递单检测

# 下载并解压数据集
from paddle.utils.download import get_path_from_url

URL = "https://paddlenlp.bj.bcebos.com/paddlenlp/datasets/waybill.tar.gz"
get_path_from_url(URL, "./")

##################### 加载数据集 #######################
from functools import partial

import paddle
from paddlenlp.datasets import MapDataset
from paddlenlp.data import Stack, Tuple, Pad
from paddlenlp.transformers import ErnieTokenizer, ErnieForTokenClassification
from paddlenlp.metrics import ChunkEvaluator
from utils import convert_example, evaluate, predict, load_dict


def load_dataset(datafiles):
    def read(data_path):
        with open(data_path, 'r', encoding='utf-8') as fp:
            next(fp)  # 跳过第一行
            for line in fp.readlines():
                words, labels = line.strip('\n').split('\t')
                words = words.split('\002')
                labels = labels.split('\002')
                yield words, labels

    # 根据datafiles决定读取单个文件还是所有文件
    if isinstance(datafiles, str):
        return MapDataset(list(read(datafiles)))
    elif isinstance(datafiles, list) or isinstance(datafiles, tuple):
        return [MapDataset(list(read(datafile))) for datafile in datafiles]


# Create dataset, tokenizer and dataloader.
# 返回MapDataset对象
train_ds, dev_ds, test_ds = load_dataset(datafiles=('./data/train.txt', './data/dev.txt', './data/test.txt'))

# 打印
# 每条数据包含一句文本和这个文本中每个汉字以及数字对应的label标签
# 之后,还需要对输入句子进行数据处理,如切词,映射词表id等
for i in range(5):
    print(train_ds[i])

# 数据读入
label_vocab = load_dict('./data/tag.dic')
# Tokenizer用于将原始输入文本转化成模型可以接受的输入数据形式。
# PaddleNLP对于各种预训练模型已经内置了相应的Tokenizer,指定想要使用的模型名字即可加载
tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')

# partial函数:为convert_example函数设置固定参数
trans_func = partial(convert_example, tokenizer=tokenizer, label_vocab=label_vocab)

# 通过调用trans_func对样本进行转换
# map方法:执行指定的函数,并用结果进行替换
train_ds.map(trans_func)
dev_ds.map(trans_func)
test_ds.map(trans_func)
print(train_ds[0])

# Pad类:将输入的数据样本按照指定axis填充成最大长度,pad_val参数为要填充的值
# Stack: 堆叠
ignore_label = -1
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(),  # seq_len (Stack将样本堆叠成一个批次)
    Pad(axis=0, pad_val=ignore_label)  # labels
): fn(samples)

# 对数据集划分批次
# DataLoader:返回一个迭代器,每次从数据集返回一个批次数据
train_loader = paddle.io.DataLoader(dataset=train_ds,  # 数据集
                                    batch_size=36,  # 批次大小
                                    return_list=True,  # 是否返回列表
                                    collate_fn=batchify_fn)  # 堆叠成小批次,该参数为0,则在第0维上堆叠
dev_loader = paddle.io.DataLoader(dataset=dev_ds,
                                  batch_size=36,
                                  return_list=True,
                                  collate_fn=batchify_fn)
test_loader = paddle.io.DataLoader(dataset=test_ds,
                                   batch_size=36,
                                   return_list=True,
                                   collate_fn=batchify_fn)

# 加载预训练模型
# Define the model netword and its loss
model = ErnieForTokenClassification.from_pretrained("ernie-1.0", num_classes=len(label_vocab))
# 设置Fine-Tune优化策略,模型配置
# ChunkEvaluator: 用于设置每个chunk检测精度、召回率、F1等指标
metric = ChunkEvaluator(label_list=label_vocab.keys(), suffix=True)
# ignore_index指定哪些target值忽略不计算loss,负数表示不忽略
loss_fn = paddle.nn.loss.CrossEntropyLoss(ignore_index=ignore_label)
# AdamW: 可以解决Adam优化器中L2正则化失败的问题
optimizer = paddle.optimizer.AdamW(learning_rate=2e-5, parameters=model.parameters())

# 模型训练与评估
step = 0
for epoch in range(10):
    for idx, (input_ids, token_type_ids, length, labels) in enumerate(train_loader):
        logits = model(input_ids, token_type_ids)  # 预测
        loss = paddle.mean(loss_fn(logits, labels))  # 指定loss
        loss.backward()  # 反向传播计算
        optimizer.step()  # 优化器更新参数
        optimizer.clear_grad()  # 清楚优化器梯度,否则梯度会累积
        step += 1
        print("epoch:%d - step:%d - loss: %f" % (epoch, step, loss))

    evaluate(model, metric, dev_loader)  # 评估

    paddle.save(model.state_dict(),  # 将所有待持久化参数写入字典中
                './ernie_result/model_%d.pdparams' % step)  # 保存路径
# model.save_pretrained('./checkpoint')
# tokenizer.save_pretrained('./checkpoint')

#################### 模型预测 ##################
preds = predict(model, test_loader, test_ds, label_vocab)  # 预测
file_path = "ernie_results.txt"
with open(file_path, "w", encoding="utf8") as fout:
    fout.write("\n".join(preds))

# Print some examples
print(
    "The results have been saved in the file: %s, some examples are shown below: "
    % file_path)
print("\n".join(preds[:10]))
  • utils.py文件
import numpy as np
import paddle
import paddle.nn.functional as F
from paddlenlp.data import Stack, Tuple, Pad

def load_dict(dict_path):
    vocab = {}
    i = 0
    for line in open(dict_path, 'r', encoding='utf-8'):
        key = line.strip('\n')
        vocab[key] = i
        i+=1
    return vocab

def convert_example(example, tokenizer, label_vocab):
    tokens, labels = example
    tokenized_input = tokenizer(
        tokens, return_length=True, is_split_into_words=True)

    # Token '[CLS]' and '[SEP]' will get label 'O'
    # 处理[CLS]和[SEP]标记
    labels = ['O'] + labels + ['O']
    tokenized_input['labels'] = [label_vocab[x] for x in labels]
    return tokenized_input['input_ids'], tokenized_input['token_type_ids'], tokenized_input['seq_len'], tokenized_input['labels']

# 装饰器:禁用动态图
@paddle.no_grad()
def evaluate(model, metric, data_loader):
    model.eval()
    metric.reset()

    for input_ids, seg_ids, lens, labels in data_loader:
        logits = model(input_ids, seg_ids)
        preds = paddle.argmax(logits, axis=-1)
        n_infer, n_label, n_correct = metric.compute(None, lens, preds, labels)
        metric.update(n_infer.numpy(), n_label.numpy(), n_correct.numpy())
        precision, recall, f1_score = metric.accumulate()
    print("eval precision: %f - recall: %f - f1: %f" %
          (precision, recall, f1_score))
    model.train()

def predict(model, data_loader, ds, label_vocab):
    pred_list = []
    len_list = []
    for input_ids, seg_ids, lens, labels in data_loader:
        logits = model(input_ids, seg_ids)
        pred = paddle.argmax(logits, axis=-1)
        pred_list.append(pred.numpy())
        len_list.append(lens.numpy())
    preds = parse_decodes(ds, pred_list, len_list, label_vocab)
    return preds

def parse_decodes(ds, decodes, lens, label_vocab):
    decodes = [x for batch in decodes for x in batch]
    lens = [x for batch in lens for x in batch]
    id_label = dict(zip(label_vocab.values(), label_vocab.keys()))

    outputs = []
    for idx, end in enumerate(lens):
        sent = ds.data[idx][0][:end]
        tags = [id_label[x] for x in decodes[idx][1:end]]
        sent_out = []
        tags_out = []
        words = ""
        for s, t in zip(sent, tags):
            if t.endswith('-B') or t == 'O':
                if len(words):
                    sent_out.append(words)
                tags_out.append(t.split('-')[0])
                words = s
            else:
                words += s
        if len(sent_out) < len(tags_out):
            sent_out.append(words)
        outputs.append(''.join(
            [str((s, t)) for s, t in zip(sent_out, tags_out)]))
    return outputs

# 预测
def predict(model, data_loader, ds, label_vocab):
    pred_list = []
    len_list = []
    for input_ids, seg_ids, lens, labels in data_loader:
        logits = model(input_ids, seg_ids)
        pred = paddle.argmax(logits, axis=-1)
        pred_list.append(pred.numpy())
        len_list.append(lens.numpy())
    preds = parse_decodes(ds, pred_list, len_list, label_vocab)
    return preds

文本语义相似度计算

{'query': '喜欢打篮球的男生喜欢什么样的女生', 'title': '爱打篮球的男生喜欢什么样的女生', 'label': 1}
{'query': '我手机丢了,我想换个手机', 'title': '我想买个新手机,求推荐', 'label': 1}
{'query': '大家觉得她好看吗', 'title': '大家觉得跑男好看吗?', 'label': 0}
{'query': '求秋色之空漫画全集', 'title': '求秋色之空全集漫画', 'label': 1}
{'query': '晚上睡觉带着耳机听音乐有什么害处吗?', 'title': '孕妇可以戴耳机听音乐吗?', 'label': 0}
  • 代码
# 语义匹配

"""
# 正式开始实验之前首先通过如下命令安装最新版本的 paddlenlp
!python -m pip install --upgrade paddlenlp==2.0.2 -i https://pypi.org/simple

# paddlenlp 会自动下载 lcqmc 数据集解压到 "${HOME}/.paddlenlp/datasets/LCQMC/lcqmc/lcqmc/" 目录下
!ls ${HOME}/.paddlenlp/datasets/LCQMC/lcqmc/lcqmc
"""

import time
import os
import numpy as np

import paddle
import paddle.nn.functional as F
from paddlenlp.datasets import load_dataset
import paddlenlp
from paddlenlp.data import Stack, Pad, Tuple
from functools import partial
import paddle.nn as nn
from paddlenlp.transformers import LinearDecayWithWarmup

# 一键加载 Lcqmc 的训练集、验证集
# lcqmc:A Large-scale Chinese Question Matching Corpus 语义匹配数据集
train_ds, dev_ds = load_dataset("lcqmc", splits=["train", "dev"])

# 输出训练集的前 20 条样本
for idx, example in enumerate(train_ds):
    if idx <= 20:
        print(example)

##################### 数据预处理 ##################
# 因为是基于预训练模型 ERNIE-Gram 来进行,所以需要首先加载 ERNIE-Gram 的 tokenizer,
# 后续样本转换函数基于 tokenizer 对文本进行切分

tokenizer = paddlenlp.transformers.ErnieGramTokenizer.from_pretrained('ernie-gram-zh')


# 将 1 条明文数据的 query、title 拼接起来,根据预训练模型的 tokenizer 将明文转换为 ID 数据
# 返回 input_ids( 表示输入文本的token ID) 和 token_type_ids(属于输入的第一个句子还是第二个句子)

def convert_example(example, tokenizer, max_seq_length=512, is_test=False):
    query, title = example["query"], example["title"]

    # 样本格式转换,返回字的索引(索引为1表示开头, 索引为2表示结束)、类型(0:问题, 1:答案)
    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
        label = np.array([example["label"]], dtype="int64")
        return input_ids, token_type_ids, label
    else:  # 在预测或者评估阶段,不返回 label 字段
        return input_ids, token_type_ids  # 返回索引、类型


### 对训练集的前5条数据进行转换
# for i in range(5):
#     input_ids, token_type_ids, label = convert_example(train_ds[i], tokenizer)
#     print(input_ids)
#     print(token_type_ids)
#     print(label)
#     print("--------------------")


# 为了后续方便使用,我们给 convert_example 赋予一些默认参数
# partial函数:为convert_example函数设置固定参数
# 训练集和验证集的样本转换函数
trans_func = partial(convert_example,
                     tokenizer=tokenizer,
                     max_seq_length=512)

# 我们的训练数据会返回 input_ids, token_type_ids, labels 3 个字段
# 因此针对这 3 个字段需要分别定义 3 个组 batch 操作
# 将前两个字段填充(Pad),后一个堆叠(Stack)
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 会自动对训练数据进行切分
# DataLoader:返回一个迭代器,每次从数据集返回一个批次数据
train_data_loader = paddle.io.DataLoader(dataset=train_ds.map(trans_func),  # 数据集
                                         batch_sampler=batch_sampler,  # 样本批处理器
                                         collate_fn=batchify_fn,  # 分成小批次,该参数为0,则在第0维上堆叠
                                         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)

# 我们基于 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)

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.
# 生成权重衰减参数(不包括偏置,LayerNorm的参数)
decay_params = [
    p.name for n, p in model.named_parameters()
    if not any(nd in n for nd in ["bias", "norm"])
]

# 定义 Optimizer AdamW: 可以解决Adam优化器中L2正则化失败的问题
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):  # start为下标起始位置

        input_ids, token_type_ids, labels = batch
        probs = model(input_ids=input_ids, token_type_ids=token_type_ids)  # 预测
        loss = criterion(probs, labels)  # 计算loss
        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)


####################### 使用预训练好的模型预测 ############################
"""
# 下载我们基于 Lcqmc 事先训练好的语义匹配模型并解压
! wget https://paddlenlp.bj.bcebos.com/models/text_matching/ernie_gram_zh_pointwise_matching_model.tar
! tar -xvf ernie_gram_zh_pointwise_matching_model.tar

# 测试数据由 2 列文本构成 tab 分隔
# Lcqmc 默认下载到如下路径
! head -n10 "${HOME}/.paddlenlp/datasets/LCQMC/lcqmc/lcqmc/test.tsv"
"""
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


# 预测数据的转换函数
# 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_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")

# 刚才下载的模型解压之后存储路径为 ./pointwise_matching_model/ernie1.0_base_pointwise_matching.pdparams
# state_dict = paddle.load("pointwise_matching_model/ernie1.0_base_pointwise_matching.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_pred
        print(text_pair)