对对联是中国传统的文学艺术形式之一,它要求前后两句对仗工整,意境相近或呼应。在当今人工智能技术的支持下,我们可以利用循环神经网络(RNN)来实现对对联的自动生成。
LSTM模型
LSTM(Long Short-Term Memory,长短期记忆网络)是一种循环神经网络(RNN)的变体,专门用于解决RNN面临的长期依赖问题。相比于传统的RNN模型,在处理长序列时,LSTM能够更好地捕捉到序列中的长期依赖关系。
LSTM通过引入称为“细胞状态”(cell state)的内部状态来实现这一点,该状态可以在模型的不同时间步之间流动,并允许网络选择性地保留或遗忘信息。LSTM包含了三个关键的门控机制,它们分别是输入门(input gate)、遗忘门(forget gate)和输出门(output gate),通过这些门控机制,LSTM可以根据当前的输入和过去的状态来决定应该如何更新细胞状态和输出。
具体来说,LSTM的运行过程如下:
- 输入门决定了应该更新细胞状态的程度,它通过使用sigmoid函数将当前输入与过去的状态结合起来,并输出一个0到1之间的值。
- 遗忘门决定了应该从细胞状态中删除多少信息,它通过使用sigmoid函数将当前输入与过去的状态结合起来,并输出一个0到1之间的值。
- 细胞状态根据输入门和遗忘门的结果进行更新。输入门控制着应该增加多少新信息到细胞状态中,而遗忘门控制着应该删除多少旧信息。这一步骤可以通过将输入门的输出与当前输入相乘并加上遗忘门的输出与过去的细胞状态相乘来实现。
- 输出门决定了最终的LSTM输出。它使用sigmoid函数将当前输入和过去的状态结合起来,并通过tanh函数将细胞状态进行压缩,然后将其与输出门的结果相乘,最终得到LSTM的输出。
通过引入细胞状态和三个门控机制,LSTM能够有效地捕捉到序列中的长期依赖关系,并且在训练时可以更好地控制信息的流动和保留。这使得LSTM成为处理自然语言处理(NLP)、语音识别、机器翻译等需要建模长序列依赖关系的任务的有力工具。
Seq2Seq模型
Seq2Seq(Sequence to Sequence,序列到序列)模型是一种经典的神经机器翻译模型,它由编码器和解码器两部分组成,可以将一个序列转换成另一个序列。该模型广泛应用于自然语言处理、对话系统、文本摘要等领域。
Seq2Seq的基本思想是将源序列作为输入,经过编码器,将源序列编码成一个固定长度的向量,再将该向量作为初始隐藏状态传递给解码器,最终输出目标序列。具体来说,Seq2Seq的运行过程如下:
- 编码器通过RNN(循环神经网络)将源序列中的每个单词依次输入,并在每个时间步产生一个隐藏状态,同时保留最终时间步的隐藏状态作为向量表示源序列。
- 解码器接收到编码器传递的向量作为初始隐藏状态,并将开始标记作为第一个输入进行解码操作。在每个时间步,解码器会根据当前输入和上一个时间步的隐藏状态产生一个新的隐藏状态,并将其作为下一个时间步的输入。
- 解码器直到生成结束标记或达到最大输出长度时停止运行,同时将每个时间步的输出结果作为目标序列。
Seq2Seq模型的核心思想是引入了编码器和解码器,通过将源序列转化为一个固定长度的向量,使得模型可以处理变长的输入和输出序列。此外,Seq2Seq模型还可以使用注意力机制来帮助解决输入序列中不同位置对输出序列的贡献度不同的问题,从而提高翻译质量。、
实战--对对联
1.数据集介绍
采用开源的对联数据集couplet-clean-dataset,该数据集过滤了 couplet-dataset中的低俗、敏感内容。这个数据集包含70w多条训练样本,1000条验证样本和1000条测试样本。下面列出一些训练集中对联样例:
上联:晚风摇树树还挺
下联:晨露润花花更红
2. 安装并导入环境包
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 load_dataset
3. 查看数据内容
print ('训练集和测试集大小:', len(train_data), len(test_data))
print('展示五幅对联:')
for i in range(5):
couplet_idx = (int)(np.random.rand() * 1000) # 随机选择0~1000里面的对联
print (train_data[couplet_idx]) # 每幅对联由两句话组成,first表示上联,second表示下联。每个中文字之间有\x02作为分割标志
vocab = Vocab.load_vocabulary(**train_data.vocab_info)
trg_idx2word = vocab.idx_to_token
vocab_size = len(vocab)
pad_id = vocab[vocab.pad_token]
bos_id = vocab[vocab.bos_token]
eos_id = vocab[vocab.eos_token]
print ('pad_id={}, bos_id={}, eos_id={}'.format(pad_id, bos_id, eos_id))
pad_id=0, bos_id=1, eos_id=2
couplet_first = '此处输入想要查看的文本的token ID' # 可以自行输入文字并查看其tokenID 将 晚风摇树树还挺 改成想查看token id的文字
print(vocab.to_indices([c for c in couplet_first]))
def create_data_loader(dataset):
data_loader = paddle.io.DataLoader(
dataset,
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
device = "gpu"
device = paddle.set_device(device)
batch_size = 128
num_layers = 2
embed_dim = 256
hidden_size =256
dropout = 0.2 # 自行选择每层舍弃的神经元比例,推荐范围(0-0.5)
learning_rate = 0.001 # 自行选择学习率 推荐范围(0.0001-0.1)
max_epoch = 2 # 自行选择训练次数 推荐范围(1-5)
model_path = './couplet_models'
log_freq = 200
train_loader = create_data_loader(train_ds)
test_loader = create_data_loader(test_ds)
print(len(train_ds), len(train_loader), batch_size)
4. 模型定义
from work.seq2seq_encoder import *
from work.attn import *
from work.seq2seq_decoder import *
from work.model_utils import *
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.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):
return lstm_forward(
src,
src_length,
trg,
self.encoder,
self.decoder,
self.num_layers,
self.hidden_size,
self.eos_id)
from work.cross_entropy import CrossEntropyCriterion
model = paddle.Model(
Seq2SeqAttnModel(
vocab_size,
embed_dim,
hidden_size,
num_layers,
pad_id
))
5. 模型的训练
optimizer = paddle.optimizer.Adam(
learning_rate=learning_rate, parameters=model.parameters())
ppl_metric = Perplexity()
model.prepare(optimizer, CrossEntropyCriterion(), ppl_metric)
model.fit(train_data=train_loader,
epochs=max_epoch,
eval_freq=1,
save_freq=1,
save_dir=model_path,
log_freq=log_freq)
6. 预测模型
from work.infer import Seq2SeqAttnInferModel
def post_process_seq(seq, bos_idx, eos_idx, output_bos=False, output_eos=False):
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
beam_size = 5
embed_dim = hidden_size
model_infer = paddle.Model(
Seq2SeqAttnInferModel(
vocab_size,
embed_dim,
hidden_size,
num_layers,
bos_id=bos_id,
eos_id=eos_id,
beam_size=beam_size,
max_out_len=256))
model_infer.load('couplet_models/model_18')
idx = 0
it = 0
for data in test_loader():
inputs = data[:2]
finished_seq = model_infer.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_f = [trg_idx2word[id] for id in test_ds[idx][0]][1:-1]
word_list_s = [trg_idx2word[id] for id in id_list]
sequence = "上联: "+"".join(word_list_f)+"\t下联: "+"".join(word_list_s) + "\n"
print(sequence)
break
idx += batch_size
break
it += 1
if it == 5:
break
7.效果
利用训练好的模型来给我们输入的上联生成下联
couplet_first = '晚风摇树树还挺' # 写入想要生成下联的对联 如 '晚风摇树树还挺'
couplet_len = len(couplet_first)
couplet_first_token = [bos_id] + vocab.to_indices([c for c in couplet_first]) + [eos_id]
couplet_len += 2
x = paddle.fluid.dygraph.to_variable([couplet_first_token])
coup_len = paddle.fluid.dygraph.to_variable([couplet_len])
finished_seq = model_infer.predict_batch(inputs=list([x, coup_len]))[0]
finished_seq = np.transpose(finished_seq, [0, 2, 1])
for beam in finished_seq[0]:
couplet_output = ''
for tokenId in beam:
if tokenId == eos_id: break
couplet_output += trg_idx2word[tokenId]
print('上联:', couplet_first, '\t下联:', couplet_output)
上联: 晚风摇树树还挺 下联: 春雨润花花更香 上联: 晚风摇树树还挺 下联: 夜雨敲窗花更香 上联: 晚风摇树树还挺 下联: 晨露润花花更香 上联: 晚风摇树树还挺 下联: 春雨润花花更红 上联: 晚风摇树树还挺 下联: 春雨润花花更芳
总结
通过对LSTM以及seq2seq模型的学习,实现了让AI自动根据上联生成下联,从模型验证结果可以看出,经过训练后的网络模型都能够根据上联自动生成相对较为工整的下联,若能继续提升训练迭代次数,将会获得更为优秀的效果。