使用注意力机制的LSTM的机器翻译

作者: PaddlePaddle
日期: 2021.05
摘要: 本示例教程介绍如何使用飞桨完成一个机器翻译任务。通过使用飞桨提供的LSTM的API,组建一个sequence to sequence with attention的机器翻译的模型,并在示例的数据集上完成从英文翻译成中文的机器翻译。

一、环境配置

本教程基于Paddle 2.1 编写,如果你的环境不是本版本,请先参考官网安装 Paddle 2.1 。

  1. import paddle
  2. import paddle.nn.functional as F
  3. import re
  4. import numpy as np
  5. print(paddle.__version__)
  1. 2.1.0

二、数据加载

2.1 数据集下载

将使用 http://www.manythings.org/anki/ 提供的中英文的英汉句对作为数据集,来完成本任务。该数据集含有23610个中英文双语的句对。

  1. !wget -c https://www.manythings.org/anki/cmn-eng.zip && unzip cmn-eng.zip
  1. --2021-05-18 18:00:27-- https://www.manythings.org/anki/cmn-eng.zip
  2. Resolving www.manythings.org (www.manythings.org)... 172.67.173.198, 104.21.55.222, 2606:4700:3031::6815:37de, ...
  3. Connecting to www.manythings.org (www.manythings.org)|172.67.173.198|:443... connected.
  4. HTTP request sent, awaiting response... 200 OK
  5. Length: 1062383 (1.0M) [application/zip]
  6. Saving to: cmn-eng.zip
  7. cmn-eng.zip 100%[===================>] 1.01M 902KB/s in 1.1s
  8. 2021-05-18 18:00:29 (902 KB/s) - cmn-eng.zip saved [1062383/1062383]
  9. Archive: cmn-eng.zip
  10. inflating: cmn.txt
  11. inflating: _about.txt
  1. !wc -l cmn.txt
  1. 24360 cmn.txt

2.2 构建双语句对的数据结构

接下来通过处理下载下来的双语句对的文本文件,将双语句对读入到python的数据结构中。这里做了如下的处理。

  • 对于英文,会把全部英文都变成小写,并只保留英文的单词。

  • 对于中文,为了简便起见,未做分词,按照字做了切分。

  • 为了后续的程序运行的更快,通过限制句子长度,和只保留部分英文单词开头的句子的方式,得到了一个较小的数据集。这样得到了一个有5508个句对的数据集。

  1. MAX_LEN = 10
  1. lines = open('cmn.txt', encoding='utf-8').read().strip().split('n')
  2. words_re = re.compile(r'w+')
  3. pairs = []
  4. for l in lines:
  5. en_sent, cn_sent, _ = l.split('t')
  6. pairs.append((words_re.findall(en_sent.lower()), list(cn_sent)))
  7. # create a smaller dataset to make the demo process faster
  8. filtered_pairs = []
  9. for x in pairs:
  10. if len(x[0]) < MAX_LEN and len(x[1]) < MAX_LEN and
  11. x[0][0] in ('i', 'you', 'he', 'she', 'we', 'they'):
  12. filtered_pairs.append(x)
  13. print(len(filtered_pairs))
  14. for x in filtered_pairs[:10]: print(x)
  1. 5687
  2. (['i', 'won'], ['我', '赢', '了', '。'])
  3. (['he', 'ran'], ['他', '跑', '了', '。'])
  4. (['i', 'quit'], ['我', '退', '出', '。'])
  5. (['i', 'quit'], ['我', '不', '干', '了', '。'])
  6. (['i', 'm', 'ok'], ['我', '沒', '事', '。'])
  7. (['i', 'm', 'up'], ['我', '已', '经', '起', '来', '了', '。'])
  8. (['we', 'try'], ['我', '们', '来', '试', '试', '。'])
  9. (['he', 'came'], ['他', '来', '了', '。'])
  10. (['he', 'runs'], ['他', '跑', '。'])
  11. (['i', 'agree'], ['我', '同', '意', '。'])

2.3 创建词表

接下来分别创建中英文的词表,这两份词表会用来将英文和中文的句子转换为词的ID构成的序列。词表中还加入了如下三个特殊的词:

  • <pad>: 用来对较短的句子进行填充。

  • <bos>: “begin of sentence”, 表示句子的开始的特殊词。

  • <eos>: “end of sentence”, 表示句子的结束的特殊词。

Note: 在实际的任务中,可能还需要通过<unk>(或者<oov>)特殊词来表示未在词表中出现的词。

  1. en_vocab = {}
  2. cn_vocab = {}
  3. # create special token for pad, begin of sentence, end of sentence
  4. en_vocab['<pad>'], en_vocab['<bos>'], en_vocab['<eos>'] = 0, 1, 2
  5. cn_vocab['<pad>'], cn_vocab['<bos>'], cn_vocab['<eos>'] = 0, 1, 2
  6. en_idx, cn_idx = 3, 3
  7. for en, cn in filtered_pairs:
  8. for w in en:
  9. if w not in en_vocab:
  10. en_vocab[w] = en_idx
  11. en_idx += 1
  12. for w in cn:
  13. if w not in cn_vocab:
  14. cn_vocab[w] = cn_idx
  15. cn_idx += 1
  16. print(len(list(en_vocab)))
  17. print(len(list(cn_vocab)))
  1. 2584
  2. 2055

2.4 创建padding过的数据集

接下来根据词表,将会创建一份实际的用于训练的用numpy array组织起来的数据集。

  • 所有的句子都通过<pad>补充成为了长度相同的句子。

  • 对于英文句子(源语言),将其反转了过来,这会带来更好的翻译的效果。

  • 所创建的padded_cn_label_sents是训练过程中的预测的目标,即,每个中文的当前词去预测下一个词是什么词。

  1. padded_en_sents = []
  2. padded_cn_sents = []
  3. padded_cn_label_sents = []
  4. for en, cn in filtered_pairs:
  5. # reverse source sentence
  6. padded_en_sent = en + ['<eos>'] + ['<pad>'] * (MAX_LEN - len(en))
  7. padded_en_sent.reverse()
  8. padded_cn_sent = ['<bos>'] + cn + ['<eos>'] + ['<pad>'] * (MAX_LEN - len(cn))
  9. padded_cn_label_sent = cn + ['<eos>'] + ['<pad>'] * (MAX_LEN - len(cn) + 1)
  10. padded_en_sents.append([en_vocab[w] for w in padded_en_sent])
  11. padded_cn_sents.append([cn_vocab[w] for w in padded_cn_sent])
  12. padded_cn_label_sents.append([cn_vocab[w] for w in padded_cn_label_sent])
  13. train_en_sents = np.array(padded_en_sents)
  14. train_cn_sents = np.array(padded_cn_sents)
  15. train_cn_label_sents = np.array(padded_cn_label_sents)
  16. print(train_en_sents.shape)
  17. print(train_cn_sents.shape)
  18. print(train_cn_label_sents.shape)
  1. (5687, 11)
  2. (5687, 12)
  3. (5687, 12)

三、网络构建

将会创建一个Encoder-AttentionDecoder架构的模型结构用来完成机器翻译任务。

首先将设置一些必要的网络结构中用到的参数。

  1. embedding_size = 128
  2. hidden_size = 256
  3. num_encoder_lstm_layers = 1
  4. en_vocab_size = len(list(en_vocab))
  5. cn_vocab_size = len(list(cn_vocab))
  6. epochs = 20
  7. batch_size = 16

3.1 Encoder部分

在编码器的部分,通过查找完Embedding之后接一个LSTM的方式构建一个对源语言编码的网络。飞桨的RNN系列的API,除了LSTM之外,还提供了SimleRNN, GRU供使用,同时,还可以使用反向RNN,双向RNN,多层RNN等形式。也可以通过dropout参数设置是否对多层RNN的中间层进行dropout处理,来防止过拟合。

除了使用序列到序列的RNN操作之外,也可以通过SimpleRNN, GRUCell, LSTMCell等API更灵活的创建单步的RNN计算,甚至通过继承RNNCellBase来实现自己的RNN计算单元。

  1. # encoder: simply learn representation of source sentence
  2. class Encoder(paddle.nn.Layer):
  3. def __init__(self):
  4. super(Encoder, self).__init__()
  5. self.emb = paddle.nn.Embedding(en_vocab_size, embedding_size,)
  6. self.lstm = paddle.nn.LSTM(input_size=embedding_size,
  7. hidden_size=hidden_size,
  8. num_layers=num_encoder_lstm_layers)
  9. def forward(self, x):
  10. x = self.emb(x)
  11. x, (_, _) = self.lstm(x)
  12. return x

3.2 AttentionDecoder部分

在解码器部分,通过一个带有注意力机制的LSTM来完成解码。

  • 单步的LSTM:在解码器的实现的部分,同样使用LSTM,与Encoder部分不同的是,下面的代码,每次只让LSTM往前计算一次。整体的recurrent部分,是在训练循环内完成的。

  • 注意力机制:这里使用了一个由两个Linear组成的网络来完成注意力机制的计算,它用来计算出目标语言在每次翻译一个词的时候,需要对源语言当中的每个词需要赋予多少的权重。

  • 对于第一次接触这样的网络结构来说,下面的代码在理解起来可能稍微有些复杂,你可以通过插入打印每个tensor在不同步骤时的形状的方式来更好的理解。

  1. # only move one step of LSTM,
  2. # the recurrent loop is implemented inside training loop
  3. class AttentionDecoder(paddle.nn.Layer):
  4. def __init__(self):
  5. super(AttentionDecoder, self).__init__()
  6. self.emb = paddle.nn.Embedding(cn_vocab_size, embedding_size)
  7. self.lstm = paddle.nn.LSTM(input_size=embedding_size + hidden_size,
  8. hidden_size=hidden_size)
  9. # for computing attention weights
  10. self.attention_linear1 = paddle.nn.Linear(hidden_size * 2, hidden_size)
  11. self.attention_linear2 = paddle.nn.Linear(hidden_size, 1)
  12. # for computing output logits
  13. self.outlinear =paddle.nn.Linear(hidden_size, cn_vocab_size)
  14. def forward(self, x, previous_hidden, previous_cell, encoder_outputs):
  15. x = self.emb(x)
  16. attention_inputs = paddle.concat((encoder_outputs,
  17. paddle.tile(previous_hidden, repeat_times=[1, MAX_LEN+1, 1])),
  18. axis=-1
  19. )
  20. attention_hidden = self.attention_linear1(attention_inputs)
  21. attention_hidden = F.tanh(attention_hidden)
  22. attention_logits = self.attention_linear2(attention_hidden)
  23. attention_logits = paddle.squeeze(attention_logits)
  24. attention_weights = F.softmax(attention_logits)
  25. attention_weights = paddle.expand_as(paddle.unsqueeze(attention_weights, -1),
  26. encoder_outputs)
  27. context_vector = paddle.multiply(encoder_outputs, attention_weights)
  28. context_vector = paddle.sum(context_vector, 1)
  29. context_vector = paddle.unsqueeze(context_vector, 1)
  30. lstm_input = paddle.concat((x, context_vector), axis=-1)
  31. # LSTM requirement to previous hidden/state:
  32. # (number_of_layers * direction, batch, hidden)
  33. previous_hidden = paddle.transpose(previous_hidden, [1, 0, 2])
  34. previous_cell = paddle.transpose(previous_cell, [1, 0, 2])
  35. x, (hidden, cell) = self.lstm(lstm_input, (previous_hidden, previous_cell))
  36. # change the return to (batch, number_of_layers * direction, hidden)
  37. hidden = paddle.transpose(hidden, [1, 0, 2])
  38. cell = paddle.transpose(cell, [1, 0, 2])
  39. output = self.outlinear(hidden)
  40. output = paddle.squeeze(output)
  41. return output, (hidden, cell)

四、训练模型

接下来开始训练模型。

  • 在每个epoch开始之前,对训练数据进行了随机打乱。

  • 通过多次调用atten_decoder,在这里实现了解码时的recurrent循环。

  • teacher forcing策略: 在每次解码下一个词时,给定了训练数据当中的真实词作为了预测下一个词时的输入。相应的,你也可以尝试用模型预测的结果作为下一个词的输入。(或者混合使用)

  1. encoder = Encoder()
  2. atten_decoder = AttentionDecoder()
  3. opt = paddle.optimizer.Adam(learning_rate=0.001,
  4. parameters=encoder.parameters()+atten_decoder.parameters())
  5. for epoch in range(epochs):
  6. print("epoch:{}".format(epoch))
  7. # shuffle training data
  8. perm = np.random.permutation(len(train_en_sents))
  9. train_en_sents_shuffled = train_en_sents[perm]
  10. train_cn_sents_shuffled = train_cn_sents[perm]
  11. train_cn_label_sents_shuffled = train_cn_label_sents[perm]
  12. for iteration in range(train_en_sents_shuffled.shape[0] // batch_size):
  13. x_data = train_en_sents_shuffled[(batch_size*iteration):(batch_size*(iteration+1))]
  14. sent = paddle.to_tensor(x_data)
  15. en_repr = encoder(sent)
  16. x_cn_data = train_cn_sents_shuffled[(batch_size*iteration):(batch_size*(iteration+1))]
  17. x_cn_label_data = train_cn_label_sents_shuffled[(batch_size*iteration):(batch_size*(iteration+1))]
  18. # shape: (batch, num_layer(=1 here) * num_of_direction(=1 here), hidden_size)
  19. hidden = paddle.zeros([batch_size, 1, hidden_size])
  20. cell = paddle.zeros([batch_size, 1, hidden_size])
  21. loss = paddle.zeros([1])
  22. # the decoder recurrent loop mentioned above
  23. for i in range(MAX_LEN + 2):
  24. cn_word = paddle.to_tensor(x_cn_data[:,i:i+1])
  25. cn_word_label = paddle.to_tensor(x_cn_label_data[:,i])
  26. logits, (hidden, cell) = atten_decoder(cn_word, hidden, cell, en_repr)
  27. step_loss = F.cross_entropy(logits, cn_word_label)
  28. loss += step_loss
  29. loss = loss / (MAX_LEN + 2)
  30. if(iteration % 200 == 0):
  31. print("iter {}, loss:{}".format(iteration, loss.numpy()))
  32. loss.backward()
  33. opt.step()
  34. opt.clear_grad()
  1. epoch:0
  2. iter 0, loss:[7.6239414]
  3. iter 200, loss:[3.0424228]
  4. epoch:1
  5. iter 0, loss:[3.3344]
  6. iter 200, loss:[3.135097]
  7. epoch:2
  8. iter 0, loss:[2.8372972]
  9. iter 200, loss:[2.9874132]
  10. epoch:3
  11. iter 0, loss:[2.4977226]
  12. iter 200, loss:[2.347312]
  13. epoch:4
  14. iter 0, loss:[2.1353514]
  15. iter 200, loss:[2.1293092]
  16. epoch:5
  17. iter 0, loss:[2.0924835]
  18. iter 200, loss:[2.0193372]
  19. epoch:6
  20. iter 0, loss:[1.9638586]
  21. iter 200, loss:[1.9775124]
  22. epoch:7
  23. iter 0, loss:[1.8319316]
  24. iter 200, loss:[1.6078386]
  25. epoch:8
  26. iter 0, loss:[1.5061388]
  27. iter 200, loss:[1.4000171]
  28. epoch:9
  29. iter 0, loss:[1.6096058]
  30. iter 200, loss:[1.5511936]
  31. epoch:10
  32. iter 0, loss:[1.4739537]
  33. iter 200, loss:[1.2266061]
  34. epoch:11
  35. iter 0, loss:[1.3779855]
  36. iter 200, loss:[1.3114413]
  37. epoch:12
  38. iter 0, loss:[1.146878]
  39. iter 200, loss:[1.2473543]
  40. epoch:13
  41. iter 0, loss:[1.0685896]
  42. iter 200, loss:[1.1657724]
  43. epoch:14
  44. iter 0, loss:[0.87895143]
  45. iter 200, loss:[0.8481859]
  46. epoch:15
  47. iter 0, loss:[0.77661693]
  48. iter 200, loss:[0.76900244]
  49. epoch:16
  50. iter 0, loss:[0.8463232]
  51. iter 200, loss:[0.8217341]
  52. epoch:17
  53. iter 0, loss:[0.5898639]
  54. iter 200, loss:[0.6576902]
  55. epoch:18
  56. iter 0, loss:[0.51171196]
  57. iter 200, loss:[0.53207105]
  58. epoch:19
  59. iter 0, loss:[0.49052936]
  60. iter 200, loss:[0.5290806]

五、使用模型进行机器翻译

根据你所使用的计算设备的不同,上面的训练过程可能需要不等的时间。(在一台Mac笔记本上,大约耗时15~20分钟)

完成上面的模型训练之后,可以得到一个能够从英文翻译成中文的机器翻译模型。接下来通过一个greedy search来实现使用该模型完成实际的机器翻译。(实际的任务中,你可能需要用beam search算法来提升效果)

  1. encoder.eval()
  2. atten_decoder.eval()
  3. num_of_exampels_to_evaluate = 10
  4. indices = np.random.choice(len(train_en_sents), num_of_exampels_to_evaluate, replace=False)
  5. x_data = train_en_sents[indices]
  6. sent = paddle.to_tensor(x_data)
  7. en_repr = encoder(sent)
  8. word = np.array(
  9. [[cn_vocab['<bos>']]] * num_of_exampels_to_evaluate
  10. )
  11. word = paddle.to_tensor(word)
  12. hidden = paddle.zeros([num_of_exampels_to_evaluate, 1, hidden_size])
  13. cell = paddle.zeros([num_of_exampels_to_evaluate, 1, hidden_size])
  14. decoded_sent = []
  15. for i in range(MAX_LEN + 2):
  16. logits, (hidden, cell) = atten_decoder(word, hidden, cell, en_repr)
  17. word = paddle.argmax(logits, axis=1)
  18. decoded_sent.append(word.numpy())
  19. word = paddle.unsqueeze(word, axis=-1)
  20. results = np.stack(decoded_sent, axis=1)
  21. for i in range(num_of_exampels_to_evaluate):
  22. en_input = " ".join(filtered_pairs[indices[i]][0])
  23. ground_truth_translate = "".join(filtered_pairs[indices[i]][1])
  24. model_translate = ""
  25. for k in results[i]:
  26. w = list(cn_vocab)[k]
  27. if w != '<pad>' and w != '<eos>':
  28. model_translate += w
  29. print(en_input)
  30. print("true: {}".format(ground_truth_translate))
  31. print("pred: {}".format(model_translate))
  1. she died of stomach cancer
  2. true: 她死于胃癌。
  3. pred: 她死于胃癌。
  4. we know
  5. true: 我们知道。
  6. pred: 我们知道。
  7. he applied for the scholarship
  8. true: 他申請了獎學金。
  9. pred: 他申請了獎學門。
  10. he went bankrupt
  11. true: 他破产了。
  12. pred: 他破产了。
  13. i ll try harder next time
  14. true: 下次我會更加努力。
  15. pred: 我會去学习几次。
  16. he tires easily
  17. true: 他很容易觉得累。
  18. pred: 他很容易觉得累。
  19. i ll do what i can to help you
  20. true: 我會盡力幫你。
  21. pred: 我會盡力幫你。
  22. you ought to have come here earlier
  23. true: 你應該早點來的。
  24. pred: 你最好去睡觉。
  25. i m going to change my shirt
  26. true: 我要去換我的襯衫。
  27. pred: 我要去換我的襯衫。
  28. i almost didn t meet her
  29. true: 我幾乎沒有遇見她。
  30. pred: 我幾乎沒有信她。

The End

你还可以通过变换网络结构,调整数据集,尝试不同的参数的方式来进一步提升本示例当中的机器翻译的效果。同时,也可以尝试在其他的类似的任务中用飞桨来完成实际的实践。