对联自动生成器(NLP教程:教你如何用AI自动生成对联)

对联自动生成器

FlyAI
自然语言处理(NLP)教程

循环神经网络最重要的特点就是可以将序列作为输入和输出,而对联的上联和下联都是典型的序列文字,那么,能否使用神经网络进行对对联呢?答案是肯定的。本项目使用网络上收集的对联数据集地址作为训练数据,运用Seq2Seq + 注意力机制网络完成了根据上联对下联的任务。

体验对对联Demo:

项目流程
处理数据
Seq2Seq + Attention 模型解读
模型代码实现
训练神经网络
数据处理
创建词向量字典和词袋字典
在原始数据集中,对联中每个汉字使用空格进行分割,格式如下所示:
室 内 崇 兰 映 日,林 间 修 竹 当 风
翠 岸 青 荷 , 琴 曲 潇 潇 情 辗 转,寒 山 古 月 , 风 声 瑟 瑟 意 彷 徨
由于每个汉字表示一个单一的词,因此不需要对原始数据进行分词。在获取原始数据之后,需要创建两个字典,分别是字到词向量的字典和字到词袋的字典,这样做是为了将词向量输入到网络中,而输出处使用词袋进行分类。在词袋模型中,添加三个关键字 ‘ “ ‘, ‘ ” ‘ 和 ‘ ~ ‘ ,分别代表输入输出的起始,结束和空白处的补零,其关键字分别为1,2,0。

class Processor(Base): ## Processor是进行数据处理的类
   def __init__(self):
       embedding_path = os.path.join(DATA_PATH, ’embedding.json’) ##加载词向量字典
‘words.json’) ## 加载词袋列表
with open(embedding_path, encoding=’utf-8′) as f:
       with open(words_list_path, encoding=’utf-8′) as f:
           self.word2ix = {w:i for i,w in enumerate(word_list, start = 3)}
‘“’] = 1 ##句子开头为1
‘”’] = 2 ##句子结尾为2
‘~’] = 0 ##padding的内容为0
for w,i in self.word2ix.items()}
40 ##最大序列长度
对上联进行词向量编码def input_x(self, upper): ##upper为输入的上联
       word_list = []
#review = upper.strip().split(‘ ‘)
‘“’] + upper.strip().split(‘ ‘) + [‘”’] ##开头加符号1,结束加符号2
for word in review:                        
           if embedding_vector is not None:
if len(embedding_vector) == 200:
# 给出现在编码词典中的词汇编码
lambda x: float(x),embedding_vector)) ## convert element type from str to float in the list
       
if len(word_list) >= self.max_sts_len:
           origanal_len = self.max_sts_len
else:
           for i in range(len(word_list), self.max_sts_len):
0 for j in range(200)]) ## 词向量维度为200
for j in range(200)]) ## 最后一行元素为句子实际长度
       return word_list
对真实下联进行词袋编码def input_y(self, lower):
       word_list = [1] ##开头加起始符号1
for word in lower:
           if word_idx is not None:
               
2) ##结束加终止符号2
       if len(word_list) >= self.max_sts_len:
           word_list = word_list[:self.max_sts_len]
else:
           for i in range(len(word_list), self.max_sts_len):
0) ## 不够长度则补0  
##最后一个元素为句子长度
return word_list
 Seq2Seq + Attention 模型解读Seq2Seq 模型可以被认为是一种由编码器和解码器组成的翻译器,其结构如下图所示:

编码器(Encoder)和解码器(Decoder)通常使用RNN构成,为提高效果,RNN通常使用LSTM或RNN,在上图中的RNN即是使用LSTM。Encoder将输入翻译为中间状态C,而Decoder将中间状态翻译为输出。序列中每一个时刻的输出由的隐含层状态,前一个时刻的输出值及中间状态C共同决定。

Attention 机制在早先的Seq2Seq模型中,中间状态C仅由最终的隐层决定,也就是说,源输入中的每个单词对C的重要性是一样的。这种方式在一定程度上降低了输出对位置的敏感性。而Attention机制正是为了弥补这一缺陷而设计的。在Attention机制中,中间状态C具有了位置信息,即每个位置的C都不相同,第i个位置的C由下面的公式决定:
公式中,Ci代表第i个位置的中间状态C,Lx代表输入序列的全部长度,hj是第j个位置的Encoder隐层输出,而aij为第i个C与第j个h之间的权重。通过这种方式,对于每个位置的源输入就产生了不同的C,也就是实现了对不同位置单词的‘注意力’。权重aij有很多的计算方式,本项目中使用使用小型神经网络进行映射的方式产生aij。

 模型代码实现Encoder
Encoder的结构非常简单,是一个简单的RNN单元,由于本项目中输入数据是已经编码好的词向量,因此不需要使用nn.Embedding() 对input进行编码。
class Encoder(nn.Module):
def __init__(self, embedding_dim, hidden_dim, num_layers=2, dropout=0.2):
#词向量维度,本项目中是200维
#RNN隐层维度
#RNN层数
#dropout
       self.rnn = nn.GRU(embedding_dim, hidden_dim,
#dropout层
   def forward(self, input_seqs, input_lengths, hidden=None):
# src = [sent len, batch size]
       # embedded = [sent len, batch size, emb dim]
#将输入转换成torch中的pack格式,使得RNN输入的是真实长度的句子而非padding后的
#outputs, hidden = self.rnn(packed, hidden)
       outputs, output_lengths = torch.nn.utils.rnn.pad_packed_sequence(outputs)
# outputs, hidden = self.rnn(embedded, hidden)
# outputs = [sent len, batch size, hid dim * n directions]
# hidden = [n layers, batch size, hid dim]
# outputs are always from the last layer
return outputs, hidden
Attentation机制Attentation权重的计算方式主要有三种,本项目中使用concatenate的方式进行注意力权重的运算。
代码实现如下:
class Attention(nn.Module):
def __init__(self, hidden_dim):
       self.hidden_dim = hidden_dim
2, hidden_dim)
       self.v.data.normal_(mean=0, std=1. / np.sqrt(self.v.size(0)))
   def forward(self, hidden, encoder_outputs):
#  encoder_outputs:(seq_len, batch_size, hidden_size)
#  hidden:(num_layers * num_directions, batch_size, hidden_size)
0)
-1].repeat(max_len, 1, 1)
# (seq_len, batch_size, hidden_size)
# compute attention score
return F.softmax(attn_energies, dim=1)  # normalize with softmax
   def score(self, hidden, encoder_outputs):
# (seq_len, batch_size, 2*hidden_size)-> (seq_len, batch_size, hidden_size)
2)))
1, 2, 0)  # (batch_size, hidden_size, seq_len)
1), 1).unsqueeze(1)  # (batch_size, 1, hidden_size)
# (batch_size, 1, seq_len)
return energy.squeeze(1)  # (batch_size, seq_len)

Decoder
Decoder同样是一个RNN网络,它的输入有三个,分别是句子初始值,hidden tensor 和Encoder的output tensor。在本项目中句子的初始值为‘“’代表的数字1。由于初始值tensor使用的是词袋编码,需要将词袋索引也映射到词向量维度,这样才能与其他tensor合并。
完整的Decoder代码如下所示:
class Decoder(nn.Module):
def __init__(self, output_dim, embedding_dim, hidden_dim, num_layers=2, dropout=0.2):
##编码维度
##RNN隐层单元数
##词袋大小
##RNN层数
       self.attention = Attention(hidden_dim)
                         num_layers=num_layers, dropout=dropout)
2, output_dim)
def forward(self, input, hidden, encoder_outputs):
# input = [bsz]
# hidden = [n layers * n directions, batch size, hid dim]
# encoder_outputs = [sent len, batch size, hid dim * n directions]
0)
# input = [1, bsz]
       # embedded = [1, bsz, emb dim]
       # (batch_size, seq_len)
1).bmm(encoder_outputs.transpose(0, 1)).transpose(0, 1)
# (batch_size, 1, hidden_dim * n_directions)
# (1, batch_size, hidden_dim * n_directions)
2)
# emb_con = [1, bsz, emb dim + hid dim]
       # outputs = [sent len, batch size, hid dim * n directions]
# hidden = [n layers * n directions, batch size, hid dim]
0), hidden[-1], context.squeeze(0)), dim=1)
1)
# outputs = [sent len, batch size, vocab_size]
return output, hidden, attn_weight
在此之上,定义一个完整的Seq2Seq类,将Encoder和Decoder结合起来。在该类中,有一个叫做teacher_forcing_ratio的参数,作用为在训练过程中强制使得网络模型的输出在一定概率下更改为ground truth,这样在反向传播时有利于模型的收敛。该类中有两个方法,分别在训练和预测时应用。
Seq2Seq类名称为Net,代码如下所示:
class Net(nn.Module):
def __init__(self, encoder, decoder, device, teacher_forcing_ratio=0.5):
       self.decoder = decoder.to(device)
       self.teacher_forcing_ratio = teacher_forcing_ratio
   def forward(self, src_seqs, src_lengths, trg_seqs):
# src_seqs = [sent len, batch size]
# trg_seqs = [sent len, batch size]
1]
0]
       # tensor to store decoder outputs
       # hidden used as the initial hidden state of the decoder
# encoder_outputs used to compute context
       # first input to the decoder is the <sos> tokens
0, :]
       for t in range(1, max_len): # skip sos
           outputs[t] = output
           output = (trg_seqs[t] if teacher_force else output.max(1)[1])
return outputs
   def predict(self, src_seqs, src_lengths, max_trg_len=30, start_ix=1):
0]
1]
       outputs = torch.zeros(max_trg_len, batch_size, trg_vocab_size).to(self.device)
       output = torch.LongTensor([start_ix] * batch_size).to(self.device)
       for t in range(1, max_trg_len):
           outputs[t] = output
1)[1]
#attn_weights[t] = attn_weight
return outputs, attn_weights
 训练神经网络
训练过程包括定义损失函数,优化器,数据处理,梯队下降等过程。由于网络中tensor型状为(sentence len, batch, embedding), 而加载的数据形状为(batch, sentence len, embedding),因此有些地方需要进行转置。
定义网络,辅助类等代码如下所示:
# 数据获取辅助类
en=Encoder(200,64) ##词向量维度200,rnn隐单元64
9133,200,64) ##词袋大小9133,词向量维度200,rnn隐单元64
##定义Seq2Seq实例
##使用交叉熵损失函数
optimizer = Adam(network.parameters()) ##使用Adam优化器
model = Model(data)
训练过程如下所示:
lowest_loss = 10
# 得到训练和测试的数据
for epoch in range(args.EPOCHS):
   
# 得到训练和测试的数据
# 读取数据; shape:(sen_len,batch,embedding)
#x_train shape: (batch,sen_len,embed_dim)
#y_train shape: (batch,sen_len)
0]
#input_lengths = [30 for i in range(batch_len)] ## batch内每个句子的长度
-1,0]
   #input_lengths = list(map(lambda x: int(x),input_lengths))
for x in input_lengths]
-1]
   
-1,:] ## 除去长度信息
#shape:(batch,sen_len,embedding)
   y_train = y_train[:,:-1] ## 除去长度信息
#shape:(batch,sen_len)
   y_train = y_train.to(device)
   seq_pairs = sorted(zip(x_train.contiguous(), y_train.contiguous(),input_lengths), key=lambda x: x[2], reverse=True)
#input_lengths = sorted(input_lengths, key=lambda x: input_lengths, reverse=True)
   x_train = torch.stack(x_train,dim=0).permute(1,0,2).contiguous()
0).permute(1,0).contiguous()
   outputs = network(x_train,input_lengths,y_train)
   #_, prediction = torch.max(outputs.data, 2)
   optimizer.zero_grad()
   # calculate the loss according to labels
-1, outputs.shape[2]), y_train.view(-1))
   # backward transmit loss
   # adjust parameters using Adam
   print(loss)
   # 若测试准确率高于当前最高准确率,则保存模型
   if loss < lowest_loss:
       model.save_model(network, MODEL_PATH, overwrite=True)
“step %d, best lowest_loss %g” % (epoch, lowest_loss))
“/” + str(args.EPOCHS))
 结束语
通过使用Seq2Seq + Attention模型,我们完成了使用神经网络对对联的任务。经过十余个周期的训练后,神经网络将会对出与上联字数相同的下联,但是,若要对出工整的对联,还需训练更多的周期,读者也可以尝试其他的方法来提高对仗的工整性。

体验对对联Demo:

对对联小活动
小伙伴们一起对出沙雕优雅的新年对联
可在下方留言你觉得最最最最最有趣的对联内容
来,一起玩吖~
往期精彩
1.NLP实战|如何用280多万条豆瓣影评预测电影评分?
2.Python之Numpy ndarray 基本介绍 1
3.Python之Numpy ndarray 基本介绍 2
4.Python之Numpy ndarray 基本介绍 3
5.Python之Numpy ndarray 基本介绍 4

点击”阅读原文”可查看本项目详情并下载代码样例
获取更多项目样例开源代码 请PC端访问:www.flyai.com

对联自动生成器相关文章

版权声明