资讯专栏INFORMATION COLUMN

NLP教程:教你如何自动生成对联

Dr_Noooo / 3235人阅读

摘要:本项目使用网络上收集的对联数据集地址作为训练数据,运用注意力机制网络完成了根据上联对下联的任务。这种方式在一定程度上降低了输出对位置的敏感性。而机制正是为了弥补这一缺陷而设计的。该类中有两个方法,分别在训练和预测时应用。

</>复制代码

  1. 桃符早易朱红纸,杨柳轻摇翡翠群 ——FlyAI Couplets
体验对对联Demo: https://www.flyai.com/couplets

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

项目流程

数据处理

Seq2Seq + Attention 模型解读

模型代码实现

训练神经网络


</>复制代码

  1. 数据处理
创建词向量字典和词袋字典

在原始数据集中,对联中每个汉字使用空格进行分割,格式如下所示:

</>复制代码

  1. ​ 室 内 崇 兰 映 日,林 间 修 竹 当 风
  2. ​ 翠 岸 青 荷 , 琴 曲 潇 潇 情 辗 转,寒 山 古 月 , 风 声 瑟 瑟 意 彷 徨

由于每个汉字表示一个单一的词,因此不需要对原始数据进行分词。在获取原始数据之后,需要创建两个字典,分别是字到词向量的字典和字到词袋的字典,这样做是为了将词向量输入到网络中,而输出处使用词袋进行分类。在词袋模型中,添加三个关键字 " “ ", " ” " 和 " ~ " ,分别代表输入输出的起始,结束和空白处的补零,其关键字分别为1,2,0。

</>复制代码

  1. class Processor(Base): ## Processor是进行数据处理的类
  2. def __init__(self):
  3. super(Processor, self).__init__()
  4. embedding_path = os.path.join(DATA_PATH, "embedding.json") ##加载词向量字典
  5. words_list_path = os.path.join(DATA_PATH, "words.json") ## 加载词袋列表
  6. with open(embedding_path, encoding="utf-8") as f:
  7. self.vocab = json.loads(f.read())
  8. with open(words_list_path, encoding="utf-8") as f:
  9. word_list = json.loads(f.read())
  10. self.word2ix = {w:i for i,w in enumerate(word_list, start = 3)}
  11. self.word2ix["“"] = 1 ##句子开头为1
  12. self.word2ix["”"] = 2 ##句子结尾为2
  13. self.word2ix["~"] = 0 ##padding的内容为0
  14. self.ix2word = {i:w for w,i in self.word2ix.items()}
  15. self.max_sts_len = 40 ##最大序列长度
对上联进行词向量编码

</>复制代码

  1. def input_x(self, upper): ##upper为输入的上联
  2. word_list = []
  3. #review = upper.strip().split(" ")
  4. review = ["“"] + upper.strip().split(" ") + ["”"] ##开头加符号1,结束加符号2
  5. for word in review:
  6. embedding_vector = self.vocab.get(word)
  7. if embedding_vector is not None:
  8. if len(embedding_vector) == 200:
  9. # 给出现在编码词典中的词汇编码
  10. embedding_vector = list(map(lambda x: float(x),embedding_vector)) ## convert element type from str to float in the list
  11. word_list.append(embedding_vector)
  12. if len(word_list) >= self.max_sts_len:
  13. word_list = word_list[:self.max_sts_len]
  14. origanal_len = self.max_sts_len
  15. else:
  16. origanal_len = len(word_list)
  17. for i in range(len(word_list), self.max_sts_len):
  18. word_list.append([0 for j in range(200)]) ## 词向量维度为200
  19. word_list.append([origanal_len for j in range(200)]) ## 最后一行元素为句子实际长度
  20. word_list = np.stack(word_list)
  21. return word_list
对真实下联进行词袋编码

</>复制代码

  1. def input_y(self, lower):
  2. word_list = [1] ##开头加起始符号1
  3. for word in lower:
  4. word_idx = self.word2ix.get(word)
  5. if word_idx is not None:
  6. word_list.append(word_idx)
  7. word_list.append(2) ##结束加终止符号2
  8. origanal_len = len(word_list)
  9. if len(word_list) >= self.max_sts_len:
  10. origanal_len = self.max_sts_len
  11. word_list = word_list[:self.max_sts_len]
  12. else:
  13. origanal_len = len(word_list)
  14. for i in range(len(word_list), self.max_sts_len):
  15. word_list.append(0) ## 不够长度则补0
  16. word_list.append(origanal_len) ##最后一个元素为句子长度
  17. return word_list

</>复制代码

  1. 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。

</>复制代码

  1. 模型代码实现
Encoder

Encoder的结构非常简单,是一个简单的RNN单元,由于本项目中输入数据是已经编码好的词向量,因此不需要使用nn.Embedding() 对input进行编码。

</>复制代码

  1. class Encoder(nn.Module):
  2. def __init__(self, embedding_dim, hidden_dim, num_layers=2, dropout=0.2):
  3. super().__init__()
  4. self.embedding_dim = embedding_dim #词向量维度,本项目中是200维
  5. self.hidden_dim = hidden_dim #RNN隐层维度
  6. self.num_layers = num_layers #RNN层数
  7. self.dropout = dropout #dropout
  8. self.rnn = nn.GRU(embedding_dim, hidden_dim,
  9. num_layers=num_layers, dropout=dropout)
  10. self.dropout = nn.Dropout(dropout) #dropout层
  11. def forward(self, input_seqs, input_lengths, hidden=None):
  12. # src = [sent len, batch size]
  13. embedded = self.dropout(input_seqs)
  14. # embedded = [sent len, batch size, emb dim]
  15. packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths) #将输入转换成torch中的pack格式,使得RNN输入的是真实长度的句子而非padding后的
  16. #outputs, hidden = self.rnn(packed, hidden)
  17. outputs, hidden = self.rnn(packed)
  18. outputs, output_lengths = torch.nn.utils.rnn.pad_packed_sequence(outputs)
  19. # outputs, hidden = self.rnn(embedded, hidden)
  20. # outputs = [sent len, batch size, hid dim * n directions]
  21. # hidden = [n layers, batch size, hid dim]
  22. # outputs are always from the last layer
  23. return outputs, hidden
Attentation机制

Attentation权重的计算方式主要有三种,本项目中使用concatenate的方式进行注意力权重的运算。代码实现如下:

</>复制代码

  1. class Attention(nn.Module):
  2. def __init__(self, hidden_dim):
  3. super(Attention, self).__init__()
  4. self.hidden_dim = hidden_dim
  5. self.attn = nn.Linear(self.hidden_dim * 2, hidden_dim)
  6. self.v = nn.Parameter(torch.rand(hidden_dim))
  7. self.v.data.normal_(mean=0, std=1. / np.sqrt(self.v.size(0)))
  8. def forward(self, hidden, encoder_outputs):
  9. # encoder_outputs:(seq_len, batch_size, hidden_size)
  10. # hidden:(num_layers * num_directions, batch_size, hidden_size)
  11. max_len = encoder_outputs.size(0)
  12. h = hidden[-1].repeat(max_len, 1, 1)
  13. # (seq_len, batch_size, hidden_size)
  14. attn_energies = self.score(h, encoder_outputs) # compute attention score
  15. return F.softmax(attn_energies, dim=1) # normalize with softmax
  16. def score(self, hidden, encoder_outputs):
  17. # (seq_len, batch_size, 2*hidden_size)-> (seq_len, batch_size, hidden_size)
  18. energy = torch.tanh(self.attn(torch.cat([hidden, encoder_outputs], 2)))
  19. energy = energy.permute(1, 2, 0) # (batch_size, hidden_size, seq_len)
  20. v = self.v.repeat(encoder_outputs.size(1), 1).unsqueeze(1) # (batch_size, 1, hidden_size)
  21. energy = torch.bmm(v, energy) # (batch_size, 1, seq_len)
  22. return energy.squeeze(1) # (batch_size, seq_len)
Decoder

Decoder同样是一个RNN网络,它的输入有三个,分别是句子初始值,hidden tensor 和Encoder的output tensor。在本项目中句子的初始值为‘“’代表的数字1。由于初始值tensor使用的是词袋编码,需要将词袋索引也映射到词向量维度,这样才能与其他tensor合并。完整的Decoder代码如下所示:

</>复制代码

  1. class Decoder(nn.Module):
  2. def __init__(self, output_dim, embedding_dim, hidden_dim, num_layers=2, dropout=0.2):
  3. super().__init__()
  4. self.embedding_dim = embedding_dim ##编码维度
  5. self.hid_dim = hidden_dim ##RNN隐层单元数
  6. self.output_dim = output_dim ##词袋大小
  7. self.num_layers = num_layers ##RNN层数
  8. self.dropout = dropout
  9. self.embedding = nn.Embedding(output_dim, embedding_dim)
  10. self.attention = Attention(hidden_dim)
  11. self.rnn = nn.GRU(embedding_dim + hidden_dim, hidden_dim,
  12. num_layers=num_layers, dropout=dropout)
  13. self.out = nn.Linear(embedding_dim + hidden_dim * 2, output_dim)
  14. self.dropout = nn.Dropout(dropout)
  15. def forward(self, input, hidden, encoder_outputs):
  16. # input = [bsz]
  17. # hidden = [n layers * n directions, batch size, hid dim]
  18. # encoder_outputs = [sent len, batch size, hid dim * n directions]
  19. input = input.unsqueeze(0)
  20. # input = [1, bsz]
  21. embedded = self.dropout(self.embedding(input))
  22. # embedded = [1, bsz, emb dim]
  23. attn_weight = self.attention(hidden, encoder_outputs)
  24. # (batch_size, seq_len)
  25. context = attn_weight.unsqueeze(1).bmm(encoder_outputs.transpose(0, 1)).transpose(0, 1)
  26. # (batch_size, 1, hidden_dim * n_directions)
  27. # (1, batch_size, hidden_dim * n_directions)
  28. emb_con = torch.cat((embedded, context), dim=2)
  29. # emb_con = [1, bsz, emb dim + hid dim]
  30. _, hidden = self.rnn(emb_con, hidden)
  31. # outputs = [sent len, batch size, hid dim * n directions]
  32. # hidden = [n layers * n directions, batch size, hid dim]
  33. output = torch.cat((embedded.squeeze(0), hidden[-1], context.squeeze(0)), dim=1)
  34. output = F.log_softmax(self.out(output), 1)
  35. # outputs = [sent len, batch size, vocab_size]
  36. return output, hidden, attn_weight

在此之上,定义一个完整的Seq2Seq类,将Encoder和Decoder结合起来。在该类中,有一个叫做teacher_forcing_ratio的参数,作用为在训练过程中强制使得网络模型的输出在一定概率下更改为ground truth,这样在反向传播时有利于模型的收敛。该类中有两个方法,分别在训练和预测时应用。Seq2Seq类名称为Net,代码如下所示:

</>复制代码

  1. class Net(nn.Module):
  2. def __init__(self, encoder, decoder, device, teacher_forcing_ratio=0.5):
  3. super().__init__()
  4. self.encoder = encoder.to(device)
  5. self.decoder = decoder.to(device)
  6. self.device = device
  7. self.teacher_forcing_ratio = teacher_forcing_ratio
  8. def forward(self, src_seqs, src_lengths, trg_seqs):
  9. # src_seqs = [sent len, batch size]
  10. # trg_seqs = [sent len, batch size]
  11. batch_size = src_seqs.shape[1]
  12. max_len = trg_seqs.shape[0]
  13. trg_vocab_size = self.decoder.output_dim
  14. # tensor to store decoder outputs
  15. outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device)
  16. # hidden used as the initial hidden state of the decoder
  17. # encoder_outputs used to compute context
  18. encoder_outputs, hidden = self.encoder(src_seqs, src_lengths)
  19. # first input to the decoder is the tokens
  20. output = trg_seqs[0, :]
  21. for t in range(1, max_len): # skip sos
  22. output, hidden, _ = self.decoder(output, hidden, encoder_outputs)
  23. outputs[t] = output
  24. teacher_force = random.random() < self.teacher_forcing_ratio
  25. output = (trg_seqs[t] if teacher_force else output.max(1)[1])
  26. return outputs
  27. def predict(self, src_seqs, src_lengths, max_trg_len=30, start_ix=1):
  28. max_src_len = src_seqs.shape[0]
  29. batch_size = src_seqs.shape[1]
  30. trg_vocab_size = self.decoder.output_dim
  31. outputs = torch.zeros(max_trg_len, batch_size, trg_vocab_size).to(self.device)
  32. encoder_outputs, hidden = self.encoder(src_seqs, src_lengths)
  33. output = torch.LongTensor([start_ix] * batch_size).to(self.device)
  34. attn_weights = torch.zeros((max_trg_len, batch_size, max_src_len))
  35. for t in range(1, max_trg_len):
  36. output, hidden, attn_weight = self.decoder(output, hidden, encoder_outputs)
  37. outputs[t] = output
  38. output = output.max(1)[1]
  39. #attn_weights[t] = attn_weight
  40. return outputs, attn_weights

</>复制代码

  1. 训练神经网络

训练过程包括定义损失函数,优化器,数据处理,梯队下降等过程。由于网络中tensor型状为(sentence len, batch, embedding), 而加载的数据形状为(batch, sentence len, embedding),因此有些地方需要进行转置。

定义网络,辅助类等代码如下所示:

</>复制代码

  1. # 数据获取辅助类
  2. data = Dataset()
  3. en=Encoder(200,64) ##词向量维度200,rnn隐单元64
  4. de=Decoder(9133,200,64) ##词袋大小9133,词向量维度200,rnn隐单元64
  5. network = Net(en,de,device) ##定义Seq2Seq实例
  6. loss_fn = nn.CrossEntropyLoss() ##使用交叉熵损失函数
  7. optimizer = Adam(network.parameters()) ##使用Adam优化器
  8. model = Model(data)
训练过程如下所示:

</>复制代码

  1. lowest_loss = 10
  2. # 得到训练和测试的数据
  3. for epoch in range(args.EPOCHS):
  4. network.train()
  5. # 得到训练和测试的数据
  6. x_train, y_train, x_test, y_test = data.next_batch(args.BATCH) # 读取数据; shape:(sen_len,batch,embedding)
  7. #x_train shape: (batch,sen_len,embed_dim)
  8. #y_train shape: (batch,sen_len)
  9. batch_len = y_train.shape[0]
  10. #input_lengths = [30 for i in range(batch_len)] ## batch内每个句子的长度
  11. input_lengths = x_train[:,-1,0]
  12. input_lengths = input_lengths.tolist()
  13. #input_lengths = list(map(lambda x: int(x),input_lengths))
  14. input_lengths = [int(x) for x in input_lengths]
  15. y_lengths = y_train[:,-1]
  16. y_lengths = y_lengths.tolist()
  17. x_train = x_train[:,:-1,:] ## 除去长度信息
  18. x_train = torch.from_numpy(x_train) #shape:(batch,sen_len,embedding)
  19. x_train = x_train.float().to(device)
  20. y_train = y_train[:,:-1] ## 除去长度信息
  21. y_train = torch.from_numpy(y_train) #shape:(batch,sen_len)
  22. y_train = torch.LongTensor(y_train)
  23. y_train = y_train.to(device)
  24. seq_pairs = sorted(zip(x_train.contiguous(), y_train.contiguous(),input_lengths), key=lambda x: x[2], reverse=True)
  25. #input_lengths = sorted(input_lengths, key=lambda x: input_lengths, reverse=True)
  26. x_train, y_train,input_lengths = zip(*seq_pairs)
  27. x_train = torch.stack(x_train,dim=0).permute(1,0,2).contiguous()
  28. y_train = torch.stack(y_train,dim=0).permute(1,0).contiguous()
  29. outputs = network(x_train,input_lengths,y_train)
  30. #_, prediction = torch.max(outputs.data, 2)
  31. optimizer.zero_grad()
  32. outputs = outputs.float()
  33. # calculate the loss according to labels
  34. loss = loss_fn(outputs.view(-1, outputs.shape[2]), y_train.view(-1))
  35. # backward transmit loss
  36. loss.backward()
  37. # adjust parameters using Adam
  38. optimizer.step()
  39. print(loss)
  40. # 若测试准确率高于当前最高准确率,则保存模型
  41. if loss < lowest_loss:
  42. lowest_loss = loss
  43. model.save_model(network, MODEL_PATH, overwrite=True)
  44. print("step %d, best lowest_loss %g" % (epoch, lowest_loss))
  45. print(str(epoch) + "/" + str(args.EPOCHS))

</>复制代码

  1. 小结

通过使用Seq2Seq + Attention模型,我们完成了使用神经网络对对联的任务。经过十余个周期的训练后,神经网络将会对出与上联字数相同的下联,但是,若要对出工整的对联,还需训练更多的周期,读者也可以尝试其他的方法来提高对仗的工整性。


体验对对联Demo: https://www.flyai.com/couplets
获取更多项目样例开源代码 请PC端访问:www.flyai.com

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19915.html

相关文章

  • NLP教程教你如何自动生成对联

    摘要:本项目使用网络上收集的对联数据集地址作为训练数据,运用注意力机制网络完成了根据上联对下联的任务。这种方式在一定程度上降低了输出对位置的敏感性。而机制正是为了弥补这一缺陷而设计的。该类中有两个方法,分别在训练和预测时应用。 桃符早易朱红纸,杨柳轻摇翡翠群 ——FlyAI Couplets 体验对对联Demo: https://www.flyai.com/couplets s...

    Gu_Yan 评论0 收藏0
  • spaCy:如何使用最快的NLP开发库结合Keras来进行深度学习

    摘要:导读工程师可用使用很多工具库来进行自然语言处理,比如等等,在这么多选择中,也许是所有人的推荐。版的终于发布了,它是世界上最快的自然语言处理库。在本文中,我们将使用,因为它是更受欢迎的深度学习库。 导读:工程师可用使用很多工具库来进行自然语言处理,比如 NLTK/CoreNLP/OpenNLP/Rosette/OpenIE 等等,在这么多选择中,spaCy 也许是所有人的推荐。1.0 版的 s...

    BlackFlagBin 评论0 收藏0
  • 首次公开,整理12年积累的博客收藏夹,零距离展示《收藏夹吃灰》系列博客

    摘要:时间永远都过得那么快,一晃从年注册,到现在已经过去了年那些被我藏在收藏夹吃灰的文章,已经太多了,是时候把他们整理一下了。那是因为收藏夹太乱,橡皮擦给设置私密了,不收拾不好看呀。 ...

    Harriet666 评论0 收藏0

发表评论

0条评论

Dr_Noooo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<