NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译

作者Sean Robertson

译者:DrDavidSmengfu188

校验:DrDavidS

这是第三篇也是最后一篇“从零开始NLP”教程,我们会在其中编写自己的类与函数来处理数据,从而完成我们的NLP建模任务。我们希望在你完成本篇教程后,你可以紧接着在其后的三篇教程中继续学习 torchtext 是如何帮你完成大量的此类预处理的。

在这个项目中,我们将编写一个把法语翻译成英语的神经网络。

  1. [KEY: > input, = target, < output]
  2. > il est en train de peindre un tableau .
  3. = he is painting a picture .
  4. < he is painting a picture .
  5. > pourquoi ne pas essayer ce vin delicieux ?
  6. = why not try that delicious wine ?
  7. < why not try that delicious wine ?
  8. > elle n est pas poete mais romanciere .
  9. = she is not a poet but a novelist .
  10. < she not not a poet but a novelist .
  11. > vous etes trop maigre .
  12. = you re too skinny .
  13. < you re all alone .

… 取得了不同程度的成功

这是通过seq2seq网络来进行实现的,在这个网络中使用两个递归的神经网络(编码器网络和解码器网络)一起工作使得一段序列变成另一段序列。 编码器网络将输入序列变成一个向量,解码器网络将该向量展开为新的序列。

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图1

我们将使用注意力机制改进这个模型,它可以让解码器学会集中在输入序列的特定范围中。

推荐阅读:

我假设你至少已经了解Python,安装了PyTorch,并且了解什么是张量:

这些内容有利于了解seq2seq网络及其工作机制:

你还可以找到之前类似于编码器和解码器的教程,如使用字符级别特征的RNN网络生成名字使用字符级别特征的RNN网络进行名字分类,学习这些概念也很有帮助。

更多内容请阅读以下论文:

需求如下

  1. from __future__ import unicode_literals, print_function, division
  2. from io import open
  3. import unicodedata
  4. import string
  5. import re
  6. import random
  7. import torch
  8. import torch.nn as nn
  9. from torch import optim
  10. import torch.nn.functional as F
  11. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

加载数据文件

这个项目的数据是一组数以千计的英语到法语的翻译用例。

这个问题在 Open Data Stack Exchange 上 点我打开翻译网址 https://tatoeba.org/ 这个网站的下载地址 https://tatoeba.org/eng/downloads - 更棒的是,有人将这些语言切分成单个文件: https://www.manythings.org/anki/

由于翻译文件太大而不能放到repo中,请在继续往下阅读前,下载数据到 data/eng-fra.txt。该文件是一个使用制表符(table)分割的翻译列表:

  1. I am cold. J'ai froid.

注意

这里 下载数据和解压到相关的路径.

与character-level RNN教程中使用的字符编码类似,我们将用语言中的每个单词 作为独热向量,或者除了单个单词之外(在单词的索引处)的大的零向量. 相较于可能 存在于一种语言中仅有十个字符相比,多数都是有大量的字,因此编码向量很大. 然而,我们会欺骗性的做一些数据修剪,保证每种语言只使用几千字.

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图2

我们之后需要将每个单词对应唯一的索引作为神经网络的输入和目标.为了追踪这些索引我们使用一个帮助类 Lang 类中有 词 → 索引 (word2index) 和 索引 → 词(index2word) 的字典, 以及每个词word2count 用来替换稀疏词汇。

  1. SOS_token = 0
  2. EOS_token = 1
  3. class Lang:
  4. def __init__(self, name):
  5. self.name = name
  6. self.word2index = {}
  7. self.word2count = {}
  8. self.index2word = {0: "SOS", 1: "EOS"}
  9. self.n_words = 2 # Count SOS and EOS
  10. def addSentence(self, sentence):
  11. for word in sentence.split(' '):
  12. self.addWord(word)
  13. def addWord(self, word):
  14. if word not in self.word2index:
  15. self.word2index[word] = self.n_words
  16. self.word2count[word] = 1
  17. self.index2word[self.n_words] = word
  18. self.n_words += 1
  19. else:
  20. self.word2count[word] += 1

这些文件全部采用Unicode编码,为了简化起见,我们将Unicode字符转换成ASCII编码,所有内容小写,并修剪大部分标点符号。

  1. # Turn a Unicode string to plain ASCII, thanks to
  2. # https://stackoverflow.com/a/518232/2809427
  3. def unicodeToAscii(s):
  4. return ''.join(
  5. c for c in unicodedata.normalize('NFD', s)
  6. if unicodedata.category(c) != 'Mn'
  7. )
  8. # Lowercase, trim, and remove non-letter characters
  9. def normalizeString(s):
  10. s = unicodeToAscii(s.lower().strip())
  11. s = re.sub(r"([.!?])", r" \1", s)
  12. s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
  13. return s

为了读取数据文件,我们将按行分开,并将每一行分成两对来读取文件。这些文件都是英语 → 其他语言,所以如果我们想从其他语言翻译 → 英语,添加reverse标志来翻转词语对。

  1. def readLangs(lang1, lang2, reverse=False):
  2. print("Reading lines...")
  3. # Read the file and split into lines
  4. lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8').\
  5. read().strip().split('\n')
  6. # Split every line into pairs and normalize
  7. pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
  8. # Reverse pairs, make Lang instances
  9. if reverse:
  10. pairs = [list(reversed(p)) for p in pairs]
  11. input_lang = Lang(lang2)
  12. output_lang = Lang(lang1)
  13. else:
  14. input_lang = Lang(lang1)
  15. output_lang = Lang(lang2)
  16. return input_lang, output_lang, pairs

由于有很多例句,而且我们想要快速训练模型,因此我们将数据集修剪为长度相对较短且简单的句子。在这里,最大长度是十个单词(包括结尾标点符号),而且我们会对翻译为”I am” 或者 “He is” 形式的句子进行过滤(考虑到之前我们清理过撇号 → ')。

  1. MAX_LENGTH = 10
  2. eng_prefixes = (
  3. "i am ", "i m ",
  4. "he is", "he s ",
  5. "she is", "she s ",
  6. "you are", "you re ",
  7. "we are", "we re ",
  8. "they are", "they re "
  9. )
  10. def filterPair(p):
  11. return len(p[0].split(' ')) < MAX_LENGTH and \
  12. len(p[1].split(' ')) < MAX_LENGTH and \
  13. p[1].startswith(eng_prefixes)
  14. def filterPairs(pairs):
  15. return [pair for pair in pairs if filterPair(pair)]

完整的数据准备过程:

  • 按行读取文本文件,将行拆分成对
  • 规范文本,按长度和内容过滤
  • 从句子中成对列出单词列表
  1. def prepareData(lang1, lang2, reverse=False):
  2. input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
  3. print("Read %s sentence pairs" % len(pairs))
  4. pairs = filterPairs(pairs)
  5. print("Trimmed to %s sentence pairs" % len(pairs))
  6. print("Counting words...")
  7. for pair in pairs:
  8. input_lang.addSentence(pair[0])
  9. output_lang.addSentence(pair[1])
  10. print("Counted words:")
  11. print(input_lang.name, input_lang.n_words)
  12. print(output_lang.name, output_lang.n_words)
  13. return input_lang, output_lang, pairs
  14. input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
  15. print(random.choice(pairs))

输出:

  1. Reading lines...
  2. Read 135842 sentence pairs
  3. Trimmed to 10599 sentence pairs
  4. Counting words...
  5. Counted words:
  6. fra 4345
  7. eng 2803
  8. ['ils ne sont pas encore chez eux .', 'they re not home yet .']

Seq2Seq模型

归神经网络(RNN)是一种对序列进行操作并利用自己的输出作为后序输入的网络

序列到序列网络(Sequence to Sequence network), 也叫做 seq2seq 网络, 又或者是 编码器解码器网络(Encoder Decoder network), 是一个由两个称为编码器解码器的RNN组成的模型。编码器读取输入序列并输出一个矢量,解码器读取该矢量并产生输出序列。

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图3

与每个输入对应一个输出的单个RNN的序列预测不同,seq2seq模型将我们从序列长度和顺序中解放出来,这使得它更适合两种语言的转换。

考虑这句话“Je ne suis pas le chat noir” → “I am not the black cat”。虽然大部分情况下输入输出序列可以对单词进行比较直接的翻译,但是很多时候单词的顺序却略有不同,例如: “chat noir” 和 “black cat”。由于 “ne/pas”结构, 输入的句子中还有另外一个单词.。因此直接从输入词的序列中直接生成正确的翻译是很困难的。

使用seq2seq模型时,编码器会创建一个向量,在理想的情况下,将输入序列的实际语义编码为单个向量 - 序列的一些N维空间中的单个点。

编码器

seq2seq网络的编码器是RNN,它为输入序列中的每个单词输出一些值。 对于每个输入单词,编码器输出一个向量和一个隐状态,并将该隐状态用于下一个输入的单词。

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图4

  1. class EncoderRNN(nn.Module):
  2. def __init__(self, input_size, hidden_size):
  3. super(EncoderRNN, self).__init__()
  4. self.hidden_size = hidden_size
  5. self.embedding = nn.Embedding(input_size, hidden_size)
  6. self.gru = nn.GRU(hidden_size, hidden_size)
  7. def forward(self, input, hidden):
  8. embedded = self.embedding(input).view(1, 1, -1)
  9. output = embedded
  10. output, hidden = self.gru(output, hidden)
  11. return output, hidden
  12. def initHidden(self):
  13. return torch.zeros(1, 1, self.hidden_size, device=device)

解码器

解码器是一个接受编码器输出向量并输出一系列单词以创建翻译的RNN。

简单解码器

在最简单的seq2seq解码器中,我们只使用编码器的最后输出。这最后一个输出有时称为上下文向量因为它从整个序列中编码上下文。该上下文向量用作解码器的初始隐藏状态。

在解码的每一步,解码器都被赋予一个输入指令和隐藏状态. 初始输入指令字符串开始的<SOS>指令,第一个隐藏状态是上下文向量(编码器的最后隐藏状态).

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图5

  1. class DecoderRNN(nn.Module):
  2. def __init__(self, hidden_size, output_size):
  3. super(DecoderRNN, self).__init__()
  4. self.hidden_size = hidden_size
  5. self.embedding = nn.Embedding(output_size, hidden_size)
  6. self.gru = nn.GRU(hidden_size, hidden_size)
  7. self.out = nn.Linear(hidden_size, output_size)
  8. self.softmax = nn.LogSoftmax(dim=1)
  9. def forward(self, input, hidden):
  10. output = self.embedding(input).view(1, 1, -1)
  11. output = F.relu(output)
  12. output, hidden = self.gru(output, hidden)
  13. output = self.softmax(self.out(output[0]))
  14. return output, hidden
  15. def initHidden(self):
  16. return torch.zeros(1, 1, self.hidden_size, device=device)

我们鼓励你训练和观察这个模型的结果,但为了节省空间,我们将直入主题开始讲解注意力机制。

带有注意力机制的解码器

如果仅在编码器和解码器之间传递上下文向量,则该单个向量承担编码整个句子的负担.

注意力机制允许解码器网络针对解码器自身输出的每一步”聚焦”编码器输出的不同部分. 首先我们计算一组注意力权重. 这些将被乘以编码器输出矢量获得加权的组合. 结果(在代码中为attn_applied) 应该包含关于输入序列的特定部分的信息, 从而帮助解码器选择正确的输出单词.

注意权值的计算是用另一个前馈层attn进行的, 将解码器的输入和隐藏层状态作为输入. 由于训练数据中的输入序列(语句)长短不一,为了实际创建和训练此层, 我们必须选择最大长度的句子(输入长度,用于编码器输出),以适用于此层. 最大长度的句子将使用所有注意力权重,而较短的句子只使用前几个.

NLP From Scratch: 基于注意力机制的 seq2seq 神经网络翻译 - 图6

  1. class AttnDecoderRNN(nn.Module):
  2. def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
  3. super(AttnDecoderRNN, self).__init__()
  4. self.hidden_size = hidden_size
  5. self.output_size = output_size
  6. self.dropout_p = dropout_p
  7. self.max_length = max_length
  8. self.embedding = nn.Embedding(self.output_size, self.hidden_size)
  9. self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
  10. self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
  11. self.dropout = nn.Dropout(self.dropout_p)
  12. self.gru = nn.GRU(self.hidden_size, self.hidden_size)
  13. self.out = nn.Linear(self.hidden_size, self.output_size)
  14. def forward(self, input, hidden, encoder_outputs):
  15. embedded = self.embedding(input).view(1, 1, -1)
  16. embedded = self.dropout(embedded)
  17. attn_weights = F.softmax(
  18. self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
  19. attn_applied = torch.bmm(attn_weights.unsqueeze(0),
  20. encoder_outputs.unsqueeze(0))
  21. output = torch.cat((embedded[0], attn_applied[0]), 1)
  22. output = self.attn_combine(output).unsqueeze(0)
  23. output = F.relu(output)
  24. output, hidden = self.gru(output, hidden)
  25. output = F.log_softmax(self.out(output[0]), dim=1)
  26. return output, hidden, attn_weights
  27. def initHidden(self):
  28. return torch.zeros(1, 1, self.hidden_size, device=device)

注意

还有其他通过使用相对位置方法来解决长度限制的注意力机制。 在 基于注意力机制的神经机器翻译的有效途径读一读关于“local attention” 的内容。

训练

准备训练数据

为了训练,对于每一对我们都需要 输入张量(输入句子中的词的索引)和 目标张量(目标语句中的词的索引)。 在创建这些向量时,我们会将EOS标记添加到两个序列中。

  1. def indexesFromSentence(lang, sentence):
  2. return [lang.word2index[word] for word in sentence.split(' ')]
  3. def tensorFromSentence(lang, sentence):
  4. indexes = indexesFromSentence(lang, sentence)
  5. indexes.append(EOS_token)
  6. return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)
  7. def tensorsFromPair(pair):
  8. input_tensor = tensorFromSentence(input_lang, pair[0])
  9. target_tensor = tensorFromSentence(output_lang, pair[1])
  10. return (input_tensor, target_tensor)

训练模型

为了训练我们通过编码器运行输入序列,并跟踪每个输出和最新的隐藏状态. 然后解码器被赋予<SOS> 标志作为其第一个输入, 并将编码器的最后一个隐藏状态作为其第一个隐藏状态.

“Teacher forcing” 是将实际目标输出用作每个下一个输入的概念,而不是将解码器的 猜测用作下一个输入.使用“Teacher forcing” 会使其更快地收敛,但是 当训练好的网络被利用时,它可能表现出不稳定性..

您可以观察“Teacher forcing”网络的输出,这些输出阅读起来是语法连贯的,但却偏离了正确的翻译 - 直觉上它已经学会表示输出语法,并且一旦老师告诉它前几个单词就可以“提取”意义,但是 它没有正确地学习如何从翻译中创建句子。

由于PyTorch的autograd给我们的自由性,我们可以通过简单的if语句来随意选择使用或者不使用“Teacher forcing”。 调高teacher_forcing_ratio来更好地使用它。

  1. teacher_forcing_ratio = 0.5
  2. def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
  3. encoder_hidden = encoder.initHidden()
  4. encoder_optimizer.zero_grad()
  5. decoder_optimizer.zero_grad()
  6. input_length = input_tensor.size(0)
  7. target_length = target_tensor.size(0)
  8. encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
  9. loss = 0
  10. for ei in range(input_length):
  11. encoder_output, encoder_hidden = encoder(
  12. input_tensor[ei], encoder_hidden)
  13. encoder_outputs[ei] = encoder_output[0, 0]
  14. decoder_input = torch.tensor([[SOS_token]], device=device)
  15. decoder_hidden = encoder_hidden
  16. use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
  17. if use_teacher_forcing:
  18. # Teacher forcing: 将目标作为下一个输入
  19. for di in range(target_length):
  20. decoder_output, decoder_hidden, decoder_attention = decoder(
  21. decoder_input, decoder_hidden, encoder_outputs)
  22. loss += criterion(decoder_output, target_tensor[di])
  23. decoder_input = target_tensor[di] # Teacher forcing
  24. else:
  25. # 不适用 teacher forcing: 使用自己的预测作为下一个输入
  26. for di in range(target_length):
  27. decoder_output, decoder_hidden, decoder_attention = decoder(
  28. decoder_input, decoder_hidden, encoder_outputs)
  29. topv, topi = decoder_output.topk(1)
  30. decoder_input = topi.squeeze().detach() # detach from history as input
  31. loss += criterion(decoder_output, target_tensor[di])
  32. if decoder_input.item() == EOS_token:
  33. break
  34. loss.backward()
  35. encoder_optimizer.step()
  36. decoder_optimizer.step()
  37. return loss.item() / target_length

这是一个帮助函数,用于在给定当前时间和进度%的情况下打印经过的时间和估计的剩余时间。

  1. import time
  2. import math
  3. def asMinutes(s):
  4. m = math.floor(s / 60)
  5. s -= m * 60
  6. return '%dm %ds' % (m, s)
  7. def timeSince(since, percent):
  8. now = time.time()
  9. s = now - since
  10. es = s / (percent)
  11. rs = es - s
  12. return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

整个训练过程如下所示:

  • 启动计时器
  • 初始化优化器和准则
  • 创建一组训练队
  • 为进行绘图启动空损失数组

之后我们多次调用train函数,偶尔打印进度 (样本的百分比,到目前为止的时间,狙击的时间) 和平均损失

  1. def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
  2. start = time.time()
  3. plot_losses = []
  4. print_loss_total = 0 # Reset every print_every
  5. plot_loss_total = 0 # Reset every plot_every
  6. encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
  7. decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
  8. training_pairs = [tensorsFromPair(random.choice(pairs))
  9. for i in range(n_iters)]
  10. criterion = nn.NLLLoss()
  11. for iter in range(1, n_iters + 1):
  12. training_pair = training_pairs[iter - 1]
  13. input_tensor = training_pair[0]
  14. target_tensor = training_pair[1]
  15. loss = train(input_tensor, target_tensor, encoder,
  16. decoder, encoder_optimizer, decoder_optimizer, criterion)
  17. print_loss_total += loss
  18. plot_loss_total += loss
  19. if iter % print_every == 0:
  20. print_loss_avg = print_loss_total / print_every
  21. print_loss_total = 0
  22. print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
  23. iter, iter / n_iters * 100, print_loss_avg))
  24. if iter % plot_every == 0:
  25. plot_loss_avg = plot_loss_total / plot_every
  26. plot_losses.append(plot_loss_avg)
  27. plot_loss_total = 0
  28. showPlot(plot_losses)

绘制结果

使用matplotlib完成绘图,使用plot_losses保存训练时的数组。

  1. import matplotlib.pyplot as plt
  2. plt.switch_backend('agg')
  3. import matplotlib.ticker as ticker
  4. import numpy as np
  5. def showPlot(points):
  6. plt.figure()
  7. fig, ax = plt.subplots()
  8. # 该定时器用于定时记录时间
  9. loc = ticker.MultipleLocator(base=0.2)
  10. ax.yaxis.set_major_locator(loc)
  11. plt.plot(points)

评估

评估与训练大部分相同,但没有目标,因此我们只是将解码器的每一步预测反馈给它自身. 每当它预测到一个单词时,我们就会将它添加到输出字符串中,并且如果它预测到我们在那里停止的EOS指令. 我们还存储解码器的注意力输出以供稍后显示.

  1. def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
  2. with torch.no_grad():
  3. input_tensor = tensorFromSentence(input_lang, sentence)
  4. input_length = input_tensor.size()[0]
  5. encoder_hidden = encoder.initHidden()
  6. encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
  7. for ei in range(input_length):
  8. encoder_output, encoder_hidden = encoder(input_tensor[ei],
  9. encoder_hidden)
  10. encoder_outputs[ei] += encoder_output[0, 0]
  11. decoder_input = torch.tensor([[SOS_token]], device=device) # SOS
  12. decoder_hidden = encoder_hidden
  13. decoded_words = []
  14. decoder_attentions = torch.zeros(max_length, max_length)
  15. for di in range(max_length):
  16. decoder_output, decoder_hidden, decoder_attention = decoder(
  17. decoder_input, decoder_hidden, encoder_outputs)
  18. decoder_attentions[di] = decoder_attention.data
  19. topv, topi = decoder_output.data.topk(1)
  20. if topi.item() == EOS_token:
  21. decoded_words.append('<EOS>')
  22. break
  23. else:
  24. decoded_words.append(output_lang.index2word[topi.item()])
  25. decoder_input = topi.squeeze().detach()
  26. return decoded_words, decoder_attentions[:di + 1]

我们可以从训练集中对随机句子进行评估,并打印出输入、目标和输出,从而做出一些主观的质量判断:

  1. def evaluateRandomly(encoder, decoder, n=10):
  2. for i in range(n):
  3. pair = random.choice(pairs)
  4. print('>', pair[0])
  5. print('=', pair[1])
  6. output_words, attentions = evaluate(encoder, decoder, pair[0])
  7. output_sentence = ' '.join(output_words)
  8. print('<', output_sentence)
  9. print('')

训练和评估

有了所有这些帮助函数(它看起来像是额外的工作,但它使运行多个实验更容易),我们实际上可以初始化一个网络并开始训练。

请记住输入句子被已经严格过滤过了。对于这个小数据集,我们可以使用包含256个隐藏节点和单个GRU层的相对较小的网络.在 MacBook CPU 上训练约40分钟后,我们会得到一些合理的结果.

注意

如果你运行这个notebook,你可以训练,中断内核,评估,并在以后继续训练。 注释编码器和解码器初始化的行并再次运行 trainIters

  1. hidden_size = 256
  2. encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
  3. attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)
  4. trainIters(encoder1, attn_decoder1, 75000, print_every=5000)

img

输出:

  1. 1m 47s (- 25m 8s) (5000 6%) 2.8641
  2. 3m 30s (- 22m 45s) (10000 13%) 2.2666
  3. 5m 15s (- 21m 1s) (15000 20%) 1.9537
  4. 7m 0s (- 19m 17s) (20000 26%) 1.7170
  5. 8m 46s (- 17m 32s) (25000 33%) 1.5182
  6. 10m 31s (- 15m 46s) (30000 40%) 1.3280
  7. 12m 15s (- 14m 0s) (35000 46%) 1.2137
  8. 14m 1s (- 12m 16s) (40000 53%) 1.0843
  9. 15m 48s (- 10m 32s) (45000 60%) 0.9847
  10. 17m 34s (- 8m 47s) (50000 66%) 0.8515
  11. 19m 20s (- 7m 2s) (55000 73%) 0.7940
  12. 21m 6s (- 5m 16s) (60000 80%) 0.7189
  13. 22m 53s (- 3m 31s) (65000 86%) 0.6490
  14. 24m 41s (- 1m 45s) (70000 93%) 0.5954
  15. 26m 26s (- 0m 0s) (75000 100%) 0.5257
  1. evaluateRandomly(encoder1, attn_decoder1)

输出:

  1. > nous sommes contents que tu sois la .
  2. = we re glad you re here .
  3. < we re glad you re here . <EOS>
  4. > il est dependant a l heroine .
  5. = he is a heroin addict .
  6. < he is in heroin heroin . <EOS>
  7. > nous sommes les meilleurs .
  8. = we are the best .
  9. < we are the best . <EOS>
  10. > tu es puissant .
  11. = you re powerful .
  12. < you re powerful . <EOS>
  13. > j ai peur des chauves souris .
  14. = i m afraid of bats .
  15. < i m afraid of bats . <EOS>
  16. > tu es enseignant n est ce pas ?
  17. = you re a teacher right ?
  18. < you re a teacher aren t you ? <EOS>
  19. > je suis pret a tout faire pour toi .
  20. = i am ready to do anything for you .
  21. < i am ready to do anything for you . <EOS>
  22. > c est desormais un homme .
  23. = he s a man now .
  24. < he is in an man . <EOS>
  25. > elle est une mere tres avisee .
  26. = she s a very wise mother .
  27. < she s a very wise mother . <EOS>
  28. > je suis completement vanne .
  29. = i m completely exhausted .
  30. < i m completely exhausted . <EOS>

可视化注意力

注意力机制的一个有用的特性是其高度可解释的输出。由于它用于加权输入序列的特定编码器输出,因此我们可以想象,在每个时间步骤中,查看网络最集中的位置。

你可以简单地运行 plt.matshow(attentions) 来查看显示为矩阵的注意力输出,列为输入步骤,行位输出步骤。

  1. output_words, attentions = evaluate(
  2. encoder1, attn_decoder1, "je suis trop froid .")
  3. plt.matshow(attentions.numpy())

img

为了获得更好的观看体验,我们将额外添加轴和标签:

  1. def showAttention(input_sentence, output_words, attentions):
  2. # Set up figure with colorbar
  3. fig = plt.figure()
  4. ax = fig.add_subplot(111)
  5. cax = ax.matshow(attentions.numpy(), cmap='bone')
  6. fig.colorbar(cax)
  7. # Set up axes
  8. ax.set_xticklabels([''] + input_sentence.split(' ') +
  9. ['<EOS>'], rotation=90)
  10. ax.set_yticklabels([''] + output_words)
  11. # Show label at every tick
  12. ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
  13. ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
  14. plt.show()
  15. def evaluateAndShowAttention(input_sentence):
  16. output_words, attentions = evaluate(
  17. encoder1, attn_decoder1, input_sentence)
  18. print('input =', input_sentence)
  19. print('output =', ' '.join(output_words))
  20. showAttention(input_sentence, output_words, attentions)
  21. evaluateAndShowAttention("elle a cinq ans de moins que moi .")
  22. evaluateAndShowAttention("elle est trop petit .")
  23. evaluateAndShowAttention("je ne crains pas de mourir .")
  24. evaluateAndShowAttention("c est un jeune directeur plein de talent .")
  • img/sphx_glr_seq2seq_translation_tutorial_004.png
  • img/sphx_glr_seq2seq_translation_tutorial_005.png
  • img/sphx_glr_seq2seq_translation_tutorial_006.png
  • img/sphx_glr_seq2seq_translation_tutorial_007.png

输出:

  1. input = elle a cinq ans de moins que moi .
  2. output = she s five years younger than me . <EOS>
  3. input = elle est trop petit .
  4. output = she s too slow . <EOS>
  5. input = je ne crains pas de mourir .
  6. output = i m not scared to die . <EOS>
  7. input = c est un jeune directeur plein de talent .
  8. output = he s a talented young player . <EOS>

练习题

  • 尝试使用不同的数据集
    • 另一种语言对(language pair)
    • 人 → 机器 (例如 IOT 命令)
    • 聊天 → 响应
    • 问题 → 回答
  • 将嵌入替换为预先训练过的单词嵌入,例如word2vec或者GloVe
  • 尝试用更多的层次,更多的隐藏单位,更多的句子。比较训练时间和结果。
  • 如果使用一个翻译文件,其中成对有两个相同的短语(I am test \t I am test),您可以将其用作自动编码器。试试这个:
    • 训练为自动编码器
    • 只保存编码器网络
    • 训练一种新的翻译解码器

脚本的总运行时间: (27 minutes 13.758 seconds)