[toc]

『深度学习 7 日打卡营·day5』

零基础解锁深度学习神器飞桨框架高层 API,七天时间助你掌握 CV、NLP 领域最火模型及应用。

  1. 课程地址

传送门:https://aistudio.baidu.com/aistudio/course/introduce/6771

  1. 目标
  • 掌握深度学习常用模型基础知识
  • 熟练掌握一种国产开源深度学习框架
  • 具备独立完成相关深度学习任务的能力
  • 能用所学为 AI 加一份年味

对联,是汉族传统文化之一,是写在纸、布上或刻在竹子、木头、柱子上的对偶语句。对联对仗工整,平仄协调,是一字一音的汉语独特的艺术形式,是中国传统文化瑰宝。

这里,我们将根据上联,自动写下联。这是一个典型的序列到序列(sequence2sequence, seq2seq)建模的场景,编码器-解码器(Encoder-Decoder)框架是解决 seq2seq 问题的经典方法,它能够将一个任意长度的源序列转换成另一个任意长度的目标序列:编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用 RNN 实现。


图1:encoder-decoder示意图

这里的 Encoder 采用 LSTM,Decoder 采用带有 attention 机制的 LSTM。


图2:带有attention机制的encoder-decoder示意图

我们将以对联的上联作为 Encoder 的输出,下联作为 Decoder 的输入,训练模型。

生成对联部分结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
上联: 芳 草 绿 阳 关 塞 上 春 风 入 户	下联: 小 桥 流 水 人 家 中 喜 气 盈 门

上联: 致 富 思 源 跟 党 走 下联: 脱 贫 致 富 为 民 圆

上联: 欣 然 入 梦 抱 书 睡 下联: 快 意 临 风 把 酒 眠

上联: 诗 赖 境 奇 赢 感 动 下联: 风 流 人 杰 显 精 神

上联: 栀 子 牵 牛 犁 熟 地 下联: 莲 花 引 蝶 戏 开 花

上联: 廿 载 相 交 成 知 己 下联: 千 秋 不 朽 著 文 章

上联: 润 下联: 修

上联: 设 帏 遇 芳 辰 百 岁 期 颐 刚 一 半 下联: 被 被 逢 盛 世 千 秋 俎 豆 尚 千 秋

上联: 波 光 云 影 满 目 葱 茏 谁 道 人 间 无 胜 地 下联: 鸟 语 花 香 一 帘 幽 梦 我 听 天 下 有 知 音

上联: 眸 中 映 月 心 如 镜 下联: 笔 底 生 花 气 若 虹

上联: 何 事 营 生 闲 来 写 幅 青 山 卖 下联: 此 时 入 梦 醉 去 吟 诗 碧 水 流

上联: 学 海 钩 深 毫 挥 具 见 三 长 足 下联: 书 山 登 绝 顶 摘 来 登 九 重 天

上联: 女 子 千 金 一 笑 贵 下联: 男 儿 万 户 百 年 长

上联: 柏 叶 为 铭 椒 花 献 瑞 下联: 梅 花 作 伴 凤 凤 鸣 春

上联: 家 国 遽 亡 天 涯 有 客 图 恢 复 下联: 江 山 永 在 我 心 无 人 泪 滂 沱

上联: 侍 郎 赋 咏 穷 三 峡 下联: 游 子 吟 诗 醉 九 江

上联: 反 腐 堵 污 流 杜 渐 防 微 不 教 长 堤 崩 蚁 穴 下联: 倡 廉 增 正 气 阳 光 普 照 长 教 大 道 播 春 风

上联: 已 兆 飞 熊 钓 渭 水 下联: 欲 栽 大 木 柱 长 天

上联: 建 生 态 文 明 人 与 自 然 协 调 发 展 下联: 创 文 明 发 展 事 同 事 业 发 展 文 明

上联: 于 自 不 高 于 他 不 下 下联: 以 人 为 本 为 我 无 为

上联: 国 泰 民 安 军 民 人 人 歌 盛 世 下联: 民 安 国 泰 社 会 事 事 颂 和 谐

上联: 金 龙 腾 大 地 看 四 野 平 畴 三 农 报 喜 下联: 玉 兔 跃 神 州 喜 九 州 大 地 万 户 迎 春

上联: 兴 盛 下联: 平 安

上联: 长 安 跑 马 谁 得 意 下联: 广 府 古 城 百 花 芳

AI Studio 平台后续会默认安装 PaddleNLP,在此之前可使用如下命令安装。

1
!pip install --upgrade paddlenlp>=2.0.0b -i https://mirror.baidu.com/pypi/simple
1
2
import paddlenlp
paddlenlp.__version__
1
'2.0.0rc1'
1
2
3
4
5
6
7
8
9
10
11
12
13
import io
import os

from functools import partial

import numpy as np

import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddlenlp.data import Vocab, Pad
from paddlenlp.metrics import Perplexity
from paddlenlp.datasets import CoupletDataset

数据部分

数据集介绍

采用开源的对联数据集couplet-clean-dataset,该数据集过滤了
couplet-dataset
中的低俗、敏感内容。

这个数据集包含 70w 多条训练样本,1000 条验证样本和 1000 条测试样本。

下面列出一些训练集中对联样例:

上联:晚风摇树树还挺 下联:晨露润花花更红

上联:愿景天成无墨迹 下联:万方乐奏有于阗

上联:丹枫江冷人初去 下联:绿柳堤新燕复来

上联:闲来野钓人稀处 下联:兴起高歌酒醉中

加载数据集

paddlenlp.datasets中内置了多个常见数据集,包括这里的对联数据集CoupletDataset


paddlenlp.datasets均继承paddle.io.Dataset,支持paddle.io.Dataset的所有功能:

  • 通过len()函数返回数据集长度,即样本数量。
  • 下标索引:通过下标索引[n]获取第 n 条样本。
  • 遍历数据集,获取所有样本。

此外,paddlenlp.datasets,还支持如下操作:

  • 调用get_datasets()函数,传入 list 或者 string,获取相对应的 train_dataset、development_dataset、test_dataset 等。其中 train 为训练集,用于模型训练; development 为开发集,也称验证集 validation_dataset,用于模型参数调优;test 为测试集,用于评估算法的性能,但不会根据测试集上的表现再去调整模型或参数。
  • 调用apply()函数,对数据集进行指定操作。

这里的CoupletDataset数据集继承TranslationDataset,继承自paddlenlp.datasets,除以上通用用法外,还有一些个性设计:

  • CoupletDataset class中,还定义了transform函数,用于在每个句子的前后加上起始符<s>和结束符</s>,并将原始数据映射成 id 序列。

图3:token-to-id示意图

1
train_ds, dev_ds, test_ds = CoupletDataset.get_datasets(['train', 'dev', 'test'])
1
100%|██████████| 21421/21421 [00:00<00:00, 26153.43it/s]

来看看数据集有多大,长什么样:

1
2
3
4
5
6
7
8
9
10
print(len(train_ds), len(test_ds), len(dev_ds))

# 加入了起始符和终止符
for i in range(5):
print(train_ds[i])

print()

for i in range(5):
print(test_ds[i])
1
2
3
4
5
6
7
8
9
10
11
12
702594 999 1000
([1, 447, 3, 509, 153, 153, 279, 1517, 2], [1, 816, 294, 378, 9, 9, 142, 32, 2])
([1, 594, 185, 10, 71, 18, 158, 912, 2], [1, 14, 105, 107, 835, 20, 268, 3855, 2])
([1, 335, 830, 68, 425, 4, 482, 246, 2], [1, 94, 51, 1115, 23, 141, 761, 17, 2])
([1, 126, 17, 217, 802, 4, 1103, 118, 2], [1, 125, 205, 47, 55, 57, 78, 15, 2])
([1, 1203, 228, 390, 10, 1921, 827, 474, 2], [1, 1699, 89, 426, 317, 314, 43, 374, 2])

([1, 6, 201, 350, 54, 1156, 2], [1, 64, 522, 305, 543, 102, 2])
([1, 168, 1402, 61, 270, 11, 195, 253, 2], [1, 435, 782, 1046, 36, 188, 1016, 56, 2])
([1, 744, 185, 744, 6, 18, 452, 16, 1410, 2], [1, 286, 102, 286, 74, 20, 669, 280, 261, 2])
([1, 2577, 496, 1133, 60, 107, 2], [1, 1533, 318, 625, 1401, 172, 2])
([1, 163, 261, 6, 64, 116, 350, 253, 2], [1, 96, 579, 13, 463, 16, 774, 586, 2])
1
2
3
4
5
6
7
8
vocab, _ = CoupletDataset.get_vocab()
trg_idx2word = vocab.idx_to_token
vocab_size = len(vocab)

pad_id = vocab[CoupletDataset.EOS_TOKEN]
bos_id = vocab[CoupletDataset.BOS_TOKEN]
eos_id = vocab[CoupletDataset.EOS_TOKEN]
print (pad_id, bos_id, eos_id)
1
2 1 2

构造 dataloder

使用paddle.io.DataLoader来创建训练和预测时所需要的DataLoader对象。

paddle.io.DataLoader返回一个迭代器,该迭代器根据batch_sampler指定的顺序迭代返回 dataset 数据。支持单进程或多进程加载数据,快!


接收如下重要参数:

  • batch_sampler:批采样器实例,用于在paddle.io.DataLoader 中迭代式获取 mini-batch 的样本下标数组,数组长度与 batch_size 一致。
  • collate_fn:指定如何将样本列表组合为 mini-batch 数据。传给它参数需要是一个callable对象,需要实现对组建的 batch 的处理逻辑,并返回每个 batch 的数据。在这里传入的是prepare_input函数,对产生的数据进行 pad 操作,并返回实际长度等。

PaddleNLP 提供了许多 NLP 任务中,用于数据处理、组 batch 数据的相关 API。

API 简介
paddlenlp.data.Stack 堆叠 N 个具有相同 shape 的输入数据来构建一个 batch
paddlenlp.data.Pad 将长度不同的多个句子 padding 到统一长度,取 N 个输入数据中的最大长度
paddlenlp.data.Tuple 将多个 batchify 函数包装在一起

更多数据处理操作详见: https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/data.md

1
2
3
4
5
6
7
8
9
10
11
12
13
def create_data_loader(dataset):
data_loader = paddle.io.DataLoader(
dataset,
batch_sampler=None,
batch_size=batch_size,
collate_fn=partial(prepare_input, pad_id=pad_id))
return data_loader

def prepare_input(insts, pad_id):
src, src_length = Pad(pad_val=pad_id, ret_length=True)([inst[0] for inst in insts])
tgt, tgt_length = Pad(pad_val=pad_id, ret_length=True)([inst[1] for inst in insts])
tgt_mask = (tgt[:, :-1] != pad_id).astype(paddle.get_default_dtype())
return src, src_length, tgt[:, :-1], tgt[:, 1:, np.newaxis], tgt_mask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use_gpu = True
device = paddle.set_device("gpu" if use_gpu else "cpu")

batch_size = 128
num_layers = 2
dropout = 0.2
hidden_size =256
max_grad_norm = 5.0
learning_rate = 0.001
max_epoch = 20
model_path = './couplet_models'
log_freq = 200

# Define dataloader
train_loader = create_data_loader(train_ds)
test_loader = create_data_loader(test_ds)

print(len(train_ds), len(train_loader), batch_size)
# 702594 5490 128 共5490个batch

for i in train_loader:
print (len(i))
for ind, each in enumerate(i):
print (ind, each.shape)
break
1
2
3
4
5
6
7
702594 5490 128
5
0 [128, 18]
1 [128]
2 [128, 17]
3 [128, 17, 1]
4 [128, 17]

模型部分

下图是带有 Attention 的 Seq2Seq 模型结构。下面我们分别定义网络的每个部分,最后构建 Seq2Seq 主网络。


图5:带有attention机制的encoder-decoder原理示意图

定义 Encoder

Encoder 部分非常简单,可以直接利用 PaddlePaddle2.0 提供的 RNN 系列 API 的nn.LSTM

  1. nn.Embedding:该接口用于构建 Embedding 的一个可调用对象,根据输入的 size (vocab_size, embedding_dim)自动构造一个二维 embedding 矩阵,用于 table-lookup。查表过程如下:

图5:token-to-id & 查表获取向量示意图

  1. nn.LSTM:提供序列,得到encoder_outputencoder_state

参数:

  • input_size (int) 输入的大小。
  • hidden_size (int) - 隐藏状态大小。
  • num_layers (int,可选) - 网络层数。默认为 1。
  • direction (str,可选) - 网络迭代方向,可设置为 forward 或 bidirect(或 bidirectional)。默认为 forward。
  • time_major (bool,可选) - 指定 input 的第一个维度是否是 time steps。默认为 False。
  • dropout (float,可选) - dropout 概率,指的是出第一层外每层输入时的 dropout 概率。默认为 0。

https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/nn/layer/rnn/LSTM_cn.html

输出:

  • outputs (Tensor) - 输出,由前向和后向 cell 的输出拼接得到。

    如果time_major为 True,则 Tensor 的形状为 [time_steps, batch_size, num_directions * hidden_size]

    如果time_major为 False,则 Tensor 的形状为 [batch_size, time_steps, num_directions * hidden_size],当 direction 设置为 bidirectional 时,num_directions 等于 2,否则等于 1。

  • final_states (tuple) - 最终状态,一个包含 h 和 c 的元组。

    形状为[num_lauers * num_directions, batch_size, hidden_size],当 direction 设置为 bidirectional 时,num_directions 等于 2,否则等于 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Seq2SeqEncoder(nn.Layer):
def __init__(self, vocab_size, embed_dim, hidden_size, num_layers):
super(Seq2SeqEncoder, self).__init__()
self.embedder = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(
input_size=embed_dim, # 句子长度
hidden_size=hidden_size, # 隐藏层大小
num_layers=num_layers, # lstm层数
dropout=0.2 if num_layers > 1 else 0.) # 随机丢弃神经元

def forward(self, sequence, sequence_length):
inputs = self.embedder(sequence)
encoder_output, encoder_state = self.lstm(
inputs, sequence_length=sequence_length)

# y_out, (h, c)
# encoder_output [128, 18, 256] [batch_size,time_steps,hidden_size]
# encoder_state (tuple) - 最终状态,一个包含h和c的元组。 [2, 128, 256] [2, 128, 256] [num_lauers * num_directions, batch_size, hidden_size]
return encoder_output, encoder_state

定义 Decoder

定义 AttentionLayer

  1. nn.Linear线性变换层传入 2 个参数
  • in_features (int) – 线性变换层输入单元的数目。
  • out_features (int) – 线性变换层输出单元的数目。

  1. paddle.matmul用于计算两个 Tensor 的乘积,遵循完整的广播规则,关于广播规则,请参考广播 (broadcasting) 。 并且其行为与 numpy.matmul 一致。
  • x (Tensor) : 输入变量,类型为 Tensor,数据类型为 float32, float64。
  • y (Tensor) : 输入变量,类型为 Tensor,数据类型为 float32, float64。
  • transpose_x (bool,可选) : 相乘前是否转置 x,默认值为 False。
  • transpose_y (bool,可选) : 相乘前是否转置 y,默认值为 False。

  1. paddle.unsqueeze用于向输入 Tensor 的 Shape 中一个或多个位置(axis)插入尺寸为 1 的维度

  2. paddle.add逐元素相加算子,输入 x 与输入 y 逐元素相加,并将各个位置的输出元素保存到返回结果中。

输入 x 与输入 y 必须可以广播为相同形状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class AttentionLayer(nn.Layer):
def __init__(self, hidden_size):
super(AttentionLayer, self).__init__()

# MLP
self.input_proj = nn.Linear(hidden_size, hidden_size)
self.output_proj = nn.Linear(hidden_size + hidden_size, hidden_size)


def forward(self, hidden, encoder_output, encoder_padding_mask):

encoder_output = self.input_proj(encoder_output)

attn_scores = paddle.matmul(
paddle.unsqueeze(hidden, [1]), encoder_output, transpose_y=True)
# print('attention score', attn_scores.shape) #[128, 1, 18]

if encoder_padding_mask is not None:
attn_scores = paddle.add(attn_scores, encoder_padding_mask)

attn_scores = F.softmax(attn_scores)

attn_out = paddle.squeeze(
paddle.matmul(attn_scores, encoder_output), [1])
# print('1 attn_out', attn_out.shape) #[128, 256]

attn_out = paddle.concat([attn_out, hidden], 1)
# print('2 attn_out', attn_out.shape) #[128, 512]

attn_out = self.output_proj(attn_out)
# print('3 attn_out', attn_out.shape) #[128, 256]
return attn_out

定义 Seq2SeqDecoderCell

由于 Decoder 部分是带有 attention 的 LSTM,我们不能复用nn.LSTM,所以需要定义Seq2SeqDecoderCell

  1. nn.LayerList 用于保存子层列表,它包含的子层将被正确地注册和添加。列表中的子层可以像常规 python 列表一样被索引。这里添加了 num_layers=2 层 lstm。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Seq2SeqDecoderCell(nn.RNNCellBase):
def __init__(self, num_layers, input_size, hidden_size):
super(Seq2SeqDecoderCell, self).__init__()
self.dropout = nn.Dropout(0.2)

# num_layers=2时 第一层输入为input_size + hidden_size 第二层输入为 hidden_size
self.lstm_cells = nn.LayerList([
nn.LSTMCell(
input_size=input_size + hidden_size if i == 0 else hidden_size,
hidden_size=hidden_size) for i in range(num_layers)
])

self.attention_layer = AttentionLayer(hidden_size)

def forward(self,
step_input,
states,
encoder_output,
encoder_padding_mask=None):

lstm_states, input_feed = states
new_lstm_states = []

step_input = paddle.concat([step_input, input_feed], 1)
for i, lstm_cell in enumerate(self.lstm_cells):
out, new_lstm_state = lstm_cell(step_input, lstm_states[i])
step_input = self.dropout(out)
new_lstm_states.append(new_lstm_state)
out = self.attention_layer(step_input, encoder_output,
encoder_padding_mask)

return out, [new_lstm_states, out]

定义 Seq2SeqDecoder

有了Seq2SeqDecoderCell,就可以构建Seq2SeqDecoder


  1. paddle.nn.RNN 该 OP 是循环神经网络(RNN)的封装,将输入的 Cell 封装为一个循环神经网络。它能够重复执行 cell.forward() 直到遍历完 input 中的所有 Tensor。
  • cell (RNNCellBase) - RNNCellBase 类的一个实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Seq2SeqDecoder(nn.Layer):
def __init__(self, vocab_size, embed_dim, hidden_size, num_layers):
super(Seq2SeqDecoder, self).__init__()
self.embedder = nn.Embedding(vocab_size, embed_dim)
self.lstm_attention = nn.RNN(
Seq2SeqDecoderCell(num_layers, embed_dim, hidden_size))
self.output_layer = nn.Linear(hidden_size, vocab_size)

def forward(self, trg, decoder_initial_states, encoder_output,
encoder_padding_mask):
inputs = self.embedder(trg)

decoder_output, _ = self.lstm_attention(
inputs,
initial_states=decoder_initial_states,
encoder_output=encoder_output,
encoder_padding_mask=encoder_padding_mask)
predict = self.output_layer(decoder_output)

return predict

构建主网络 Seq2SeqAttnModel

Encoder 和 Decoder 定义好之后,网络就可以构建起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Seq2SeqAttnModel(nn.Layer):
def __init__(self, vocab_size, embed_dim, hidden_size, num_layers,
eos_id=1):
super(Seq2SeqAttnModel, self).__init__()
self.hidden_size = hidden_size
self.eos_id = eos_id
self.num_layers = num_layers
self.INF = 1e9
self.encoder = Seq2SeqEncoder(vocab_size, embed_dim, hidden_size,
num_layers)
self.decoder = Seq2SeqDecoder(vocab_size, embed_dim, hidden_size,
num_layers)

def forward(self, src, src_length, trg):
# encoder_output 各时刻的输出h
# encoder_final_state 最后时刻的输出h,和记忆信号c
encoder_output, encoder_final_state = self.encoder(src, src_length)
# print('encoder_output shape', encoder_output.shape) # [128, 18, 256] [batch_size,time_steps,hidden_size]
# print('encoder_final_states shape', encoder_final_state[0].shape, encoder_final_state[1].shape) #[2, 128, 256] [2, 128, 256] [num_lauers * num_directions, batch_size, hidden_size]

# Transfer shape of encoder_final_states to [num_layers, 2, batch_size, hidden_size]???
encoder_final_states = [
(encoder_final_state[0][i], encoder_final_state[1][i])
for i in range(self.num_layers)
]
# print('encoder_final_states shape', encoder_final_states[0][0].shape, encoder_final_states[0][1].shape) #[128, 256] [128, 256]


# Construct decoder initial states: use input_feed and the shape is
# [[h,c] * num_layers, input_feed], consistent with Seq2SeqDecoderCell.states
decoder_initial_states = [
encoder_final_states,
self.decoder.lstm_attention.cell.get_initial_states(
batch_ref=encoder_output, shape=[self.hidden_size])
]

# Build attention mask to avoid paying attention on padddings
src_mask = (src != self.eos_id).astype(paddle.get_default_dtype())
# print ('src_mask shape', src_mask.shape) #[128, 18]
# print(src_mask[0, :])

encoder_padding_mask = (src_mask - 1.0) * self.INF
# print ('encoder_padding_mask', encoder_padding_mask.shape) #[128, 18]
# print(encoder_padding_mask[0, :])

encoder_padding_mask = paddle.unsqueeze(encoder_padding_mask, [1])
# print('encoder_padding_mask', encoder_padding_mask.shape) #[128, 1, 18]

predict = self.decoder(trg, decoder_initial_states, encoder_output,
encoder_padding_mask)
# print('predict', predict.shape) #[128, 17, 7931]

return predict

定义损失函数

这里使用的是交叉熵损失函数,我们需要将 padding 位置的 loss 置为 0,因此需要在损失函数中引入trg_mask参数,由于 PaddlePaddle 框架提供的paddle.nn.CrossEntropyLoss不能接受trg_mask参数,因此在这里需要重新定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CrossEntropyCriterion(nn.Layer):
def __init__(self):
super(CrossEntropyCriterion, self).__init__()

def forward(self, predict, label, trg_mask):
cost = F.softmax_with_cross_entropy(
logits=predict, label=label, soft_label=False)
cost = paddle.squeeze(cost, axis=[2])
masked_cost = cost * trg_mask
batch_mean_cost = paddle.mean(masked_cost, axis=[0])
seq_cost = paddle.sum(batch_mean_cost)

return seq_cost

执行过程

训练过程

使用高层 API 执行训练,需要调用preparefit函数。

prepare函数中,配置优化器、损失函数,以及评价指标。其中评价指标使用的是 PaddleNLP 提供的困惑度计算 API paddlenlp.metrics.Perplexity

如果你安装了 VisualDL,可以在 fit 中添加一个 callbacks 参数使用 VisualDL 观测你的训练过程,如下:

1
2
3
4
5
6
7
model.fit(train_data=train_loader,
epochs=max_epoch,
eval_freq=1,
save_freq=1,
save_dir=model_path,
log_freq=log_freq,
callbacks=[paddle.callbacks.VisualDL('./log')])

在这里,由于对联生成任务没有明确的评价指标,因此,可以在保存的多个模型中,通过人工评判生成结果选择最好的模型。

本项目中,为了便于演示,已经将训练好的模型参数载入模型,并省略了训练过程。读者自己实验的时候,可以尝试自行修改超参数,调用下面被注释掉的fit函数,重新进行训练。

如果读者想要在更短的时间内得到效果不错的模型,可以使用预训练模型技术,例如《预训练模型 ERNIE-GEN 自动写诗》项目为大家展示了如何利用预训练的生成模型进行训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
model = paddle.Model(Seq2SeqAttnModel(vocab_size, hidden_size, hidden_size, num_layers, pad_id))


optimizer = paddle.optimizer.Adam(
learning_rate=learning_rate, parameters=model.parameters())

model.load('couplet_models/model_18')

model.prepare(optimizer, CrossEntropyCriterion(), Perplexity())

model.fit(train_data=train_loader,
epochs=1,
eval_freq=1,
save_freq=5,
save_dir=model_path,
log_freq=log_freq)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
The loss value printed in the log is the current step, and the metric is the average value of previous step.
Epoch 1/1
step 200/5490 - loss: 29.0412 - Perplexity: 28.2248 - 72ms/step
step 400/5490 - loss: 31.3796 - Perplexity: 28.4654 - 72ms/step
step 600/5490 - loss: 30.2077 - Perplexity: 28.4107 - 72ms/step
step 800/5490 - loss: 30.8788 - Perplexity: 28.4995 - 73ms/step
step 1000/5490 - loss: 28.7426 - Perplexity: 28.7224 - 72ms/step
step 1200/5490 - loss: 32.3817 - Perplexity: 28.8973 - 71ms/step
step 1400/5490 - loss: 31.8954 - Perplexity: 28.9987 - 71ms/step
step 1600/5490 - loss: 30.3898 - Perplexity: 29.0871 - 71ms/step
step 1800/5490 - loss: 32.1190 - Perplexity: 29.1689 - 72ms/step
step 2000/5490 - loss: 32.3143 - Perplexity: 29.2302 - 72ms/step
step 2200/5490 - loss: 31.6980 - Perplexity: 29.2954 - 71ms/step
step 2400/5490 - loss: 29.9607 - Perplexity: 29.3576 - 71ms/step
step 2600/5490 - loss: 31.6618 - Perplexity: 29.3990 - 71ms/step
step 2800/5490 - loss: 35.0776 - Perplexity: 29.4590 - 71ms/step
step 3000/5490 - loss: 30.2568 - Perplexity: 29.5161 - 71ms/step
step 3200/5490 - loss: 29.4113 - Perplexity: 29.5687 - 70ms/step
step 3400/5490 - loss: 32.5356 - Perplexity: 29.6236 - 71ms/step
step 3600/5490 - loss: 30.4489 - Perplexity: 29.6678 - 71ms/step
step 3800/5490 - loss: 30.7146 - Perplexity: 29.6957 - 71ms/step
step 4000/5490 - loss: 31.3794 - Perplexity: 29.7266 - 71ms/step
step 4200/5490 - loss: 33.3526 - Perplexity: 29.7954 - 71ms/step
step 4400/5490 - loss: 30.6265 - Perplexity: 29.8421 - 71ms/step
step 4600/5490 - loss: 30.8788 - Perplexity: 29.8787 - 71ms/step
step 4800/5490 - loss: 28.6094 - Perplexity: 29.9268 - 71ms/step
step 5000/5490 - loss: 31.5489 - Perplexity: 29.9706 - 71ms/step
step 5200/5490 - loss: 31.6076 - Perplexity: 30.0078 - 71ms/step
step 5400/5490 - loss: 31.7482 - Perplexity: 30.0651 - 72ms/step
step 5490/5490 - loss: 31.3067 - Perplexity: 30.0837 - 72ms/step
save checkpoint at /home/aistudio/couplet_models/0
save checkpoint at /home/aistudio/couplet_models/final

模型预测

定义预测网络 Seq2SeqAttnInferModel

预测网络继承上面的主网络Seq2SeqAttnModel,定义子类Seq2SeqAttnInferModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Seq2SeqAttnInferModel(Seq2SeqAttnModel):
def __init__(self,
vocab_size,
embed_dim,
hidden_size,
num_layers,
bos_id=0,
eos_id=1,
beam_size=4,
max_out_len=256):
self.bos_id = bos_id
self.beam_size = beam_size
self.max_out_len = max_out_len
self.num_layers = num_layers
super(Seq2SeqAttnInferModel, self).__init__(
vocab_size, embed_dim, hidden_size, num_layers, eos_id)

# Dynamic decoder for inference
self.beam_search_decoder = nn.BeamSearchDecoder(
self.decoder.lstm_attention.cell,
start_token=bos_id,
end_token=eos_id,
beam_size=beam_size,
embedding_fn=self.decoder.embedder,
output_fn=self.decoder.output_layer)

def forward(self, src, src_length):
encoder_output, encoder_final_state = self.encoder(src, src_length)

encoder_final_state = [
(encoder_final_state[0][i], encoder_final_state[1][i])
for i in range(self.num_layers)
]

# Initial decoder initial states
decoder_initial_states = [
encoder_final_state,
self.decoder.lstm_attention.cell.get_initial_states(
batch_ref=encoder_output, shape=[self.hidden_size])
]
# Build attention mask to avoid paying attention on paddings
src_mask = (src != self.eos_id).astype(paddle.get_default_dtype())

encoder_padding_mask = (src_mask - 1.0) * self.INF
encoder_padding_mask = paddle.unsqueeze(encoder_padding_mask, [1])

# Tile the batch dimension with beam_size
encoder_output = nn.BeamSearchDecoder.tile_beam_merge_with_batch(
encoder_output, self.beam_size)
encoder_padding_mask = nn.BeamSearchDecoder.tile_beam_merge_with_batch(
encoder_padding_mask, self.beam_size)

# Dynamic decoding with beam search
seq_output, _ = nn.dynamic_decode(
decoder=self.beam_search_decoder,
inits=decoder_initial_states,
max_step_num=self.max_out_len,
encoder_output=encoder_output,
encoder_padding_mask=encoder_padding_mask)
return seq_output

解码部分

接下来对我们的任务选择 beam search 解码方式,可以指定 beam_size 为 10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def post_process_seq(seq, bos_idx, eos_idx, output_bos=False, output_eos=False):
"""
Post-process the decoded sequence.
"""
eos_pos = len(seq) - 1
for i, idx in enumerate(seq):
if idx == eos_idx:
eos_pos = i
break
seq = [
idx for idx in seq[:eos_pos + 1]
if (output_bos or idx != bos_idx) and (output_eos or idx != eos_idx)
]
return seq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
beam_size = 10
# init_from_ckpt = './couplet_models/0' # for test
# infer_output_file = './infer_output.txt'

# test_loader, vocab_size, pad_id, bos_id, eos_id = create_data_loader(test_ds, batch_size)
# vocab, _ = CoupletDataset.get_vocab()
# trg_idx2word = vocab.idx_to_token

model = paddle.Model(
Seq2SeqAttnInferModel(
vocab_size,
hidden_size,
hidden_size,
num_layers,
bos_id=bos_id,
eos_id=eos_id,
beam_size=beam_size,
max_out_len=256))

model.prepare()

在预测之前,我们需要将训练好的模型参数 load 进预测网络,之后我们就可以根据对联的上联,生成对联的下联啦!

1
model.load('couplet_models/final')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test_ds = CoupletDataset.get_datasets(['test'])
idx = 0
for data in test_loader():
inputs = data[:2]
finished_seq = model.predict_batch(inputs=list(inputs))[0]
finished_seq = finished_seq[:, :, np.newaxis] if len(
finished_seq.shape) == 2 else finished_seq
finished_seq = np.transpose(finished_seq, [0, 2, 1])

for ins in finished_seq:
for beam in ins:
id_list = post_process_seq(beam, bos_id, eos_id)
word_list_l = [trg_idx2word[id] for id in test_ds[idx][0]][1:-1]
word_list_r = [trg_idx2word[id] for id in id_list]
sequence = "上联: "+" ".join(word_list_l)+"\t下联: "+" ".join(word_list_r) + "\n"
print(sequence)
idx += 1
break

PaddleNLP 更多教程

加入交流群,一起学习吧

现在就加入 PaddleNLP 的 QQ 技术交流群,一起交流 NLP 技术吧!