Sequence to Sequence Learning with Neural Networks–使用 Seq2Seq 完成翻译

  • A+
所属分类:深度学习
摘要这一篇文章会主要介绍Seq2Seq模型的原理,与一个具体的例子实现, 翻译法语到英语。这一篇内容有点长,可能看的时间会有点长。

文章简介

这一篇主要会介绍 Seq2Seq 模型,并使用其实现一个简单的翻译(英语到法语的翻译)的功能。主要参考自 Pytorch 官方的一个例子,TRANSLATION WITH A SEQUENCE TO SEQUENCE NETWORK AND ATTENTION。这个我会拆成两篇文章,一篇使用传统的 Seq2Seq,另一篇加入Attention。

主要参考的论文Sequence to Sequence Learning with Neural Networks

 

Seq2Seq介绍

在之前的几篇文章里,RNN for Image Classification(RNN图片分类–MNIST数据集)RNN完成姓名分类,我们介绍的都是多对一的情况(也就是只有一个 output),但在面对翻译这个问题的时候,传统的 RNN 结构可能就不在适用了,因为我们需要让模型输出多个值

However, it is not clear how to apply an RNN to problems whose input and the output sequences have different lengths with complicated and non-monotonic relationships. (input 和 output 没有对应关系, 且长度不一致时, 传统的RNN结构无法处理)。为了解决上面的问题,因此提出了 Sequence-to-Sequence 模型。

 

Seq2Seq介绍

为了解决上面提出的问题,人们提出了关于 Seq2Seq 模型,这个模型的主要思路如下:

  • Encoder : Uses a multilayered Long Short-Term Memory (LSTM) to map the input sequence to a vector of a fixed dimensionality. (将输入序列映射为一个向量)
  • Decoder : And then another deep LSTM to decode the target sequence from the vector. (接着使用另外一个 LSTM 网络,将上面的向量还原为一个序列)

整体的结构如下图所示(其实感觉论文里这张图片讲解的不是很清晰,后面再贴几张我觉得比较好的解释 Seq2Seq 的图片)

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

这里再详细说明一下。对于 Seq2Seq 来说,整个系统分为两个部分。

  • Encoder 部分将输入的一句话转为一个 vector;Decoder 负责将这个 vector 展开,变为翻译后的句子。
  • Decoder 会将 Encoder 最后的 hidden state 作为自己的初始的 hidden state。
  • Decoder 初始输入为一个标志符, 之后每次将自己这次的输出作为下一次的输入。

下面的这几张图片来自 : Encoder-decoderモデルとTeacher Forcing,Scheduled Sampling,Professor Forcing

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

对于 Decoder 的输入,除了上面的这种方式以外,还有 Teacher Forcing 这种模式。

首先是普通的形式,直接将 Decoder 每一次的输出作为下一次的输入:

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

接着是 Teacher Forcing 模式,他会每次将正确答案作为输入

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

在实际使用中,会有两种变化的形式。第一种是每次在训练的时候进行掷骰子, 有一点概率使用普通的模式,一定的概率使用Teacher Forcing模式. (我们这次实现就会使用这样的方式)

 

Seq2Seq 中 Decoder 的扩展

Scheduled Sampling

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

Professor Forcing

还有一种方式会做模型的融合,这个具体可以查看这篇论文, Professor Forcing: A New Algorithm for Training Recurrent Networks

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

论文的一些发现

这一部分是在论文,Sequence to Sequence Learning with Neural Networks,中作者最后的一些总结。

  • First, we used two different LSTMs: one for the input sequence and another for the output sequence, because doing so increases the number model parameters at negligible computational cost and makes it natural to train the LSTM on multiple language pairs simultaneously.
  • Second, we found that deep LSTMs significantly outperformed shallow LSTMs, so we chose an LSTM with four layers. (深的LSTM效果会更好)
  • Third, we found it extremely valuable to reverse the order of the words of the input sentence. So for example, instead of mapping the sentence a, b, c to the sentence α, β, γ, the LSTM is asked to map c, b, a to α, β, γ, where α, β, γ is the translation of a, b, c.(将input的sequence逆序输入)—这个后来被证明只对部分语言有效, 即翻译前后单词的语义有对应的关系.

关于最后一部分, 作者发现将训练的句子按逆序的顺序输入网络会有更好的效果。

 

关于Vector的可视化结果

One of the attractive features of our model is its ability to turn a sequence of words into a vector of fixed dimensionality. (作者将encoder之后的vector降维后进行可视化,效果图如下所示)

The two-dimensional projections are obtainedusing PCA. (原文是使用了PCA的降维方法)

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

于是他得到了下面的一些结论:

The figure clearly shows that the representations are sensitive to the order of words while being fairly insensitive to the replacement of an active voice with a passive voice. (语句的顺序是敏感的, 如左侧的图, 交换 Mary 和 John 之后聚类的结果是不同的;但是对主动语态与被动语态是不敏感的, 如右图, 只与表达的意思是有关的)

 

Seq2Seq 实验实现

下面会简单重复一下 Seq2Seq 的实验,主要代码均参考自TRANSLATION WITH A SEQUENCE TO SEQUENCE NETWORK AND ATTENTION,我只简单修改了一下 train 部分的代码。本文的完整代码已经上传 Github,可以参考链接,Seq2Seq模型代码

这一部分最终想要实现的目标如下:

  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 .

主要的思想,再用一句话概括就是:

An encoder network condenses an input sequence into a vector, and a decoder network unfolds that vector into a new sequence. 

导入需要的库

首先我们导入需要用到的库,以及定义 device,后面可以使用 GPU 进行训练。

  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")

数据预处理

接下来定义一些函数,用作数据的预处理。关于数据的下载,可以点击后面的链接,数据下载链接

首先我们定义一个函数,可以将每一个每个单词都有一个 index 与之对应,其中 SOS_tokenc表示句子开头, EOS_token 表示句子的结尾.:

  1. SOS_token = 0
  2. EOS_token = 1
  3. class Lang:
  4.     """word → index (word2index) and index → word (index2word) dictionaries
  5.        A count of each word word2count to use to later replace rare words.
  6.     """
  7.     def __init__(self, name):
  8.         self.name = name
  9.         self.word2index = {}
  10.         self.word2count = {}
  11.         self.index2word = {0: "SOS", 1: "EOS"}
  12.         self.n_words = 2  # Count SOS and EOS
  13.     def addSentence(self, sentence):
  14.         for word in sentence.split(' '):
  15.             self.addWord(word)
  16.     def addWord(self, word):
  17.         if word not in self.word2index:
  18.             self.word2index[word] = self.n_words
  19.             self.word2count[word] = 1
  20.             self.index2word[self.n_words] = word
  21.             self.n_words += 1
  22.         else:
  23.             self.word2count[word] += 1

接着我们定义数据预处理的函数,可以将 Unicode 转换为 ASCII,大写变小写,留下重要的标点,去掉大部分的标点。

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

对于上面的数据预处理函数,我们看下面的例子:

  1. # 转换为ASCII, 大写变小写, 留下重要的标点, 去掉大部分的标点
  2. normalizeString('I am a Boy!~$%^&')
  3. """
  4. 'i am a boy ! '
  5. """

接着定义函数,来逐行读取文件,并对每一行做预处理。因为我们每一行文件都包含原语言和翻译后的语言,所以返回的 pair。源文件数据格式如下所示:

  1. I am cold.    J'ai froid.

下面是具体读取文件的函数.

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

为了加快训练的速度, 我们把句子长度最大设置为10, 同时我们过滤句子后使得其开头变为如 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. # 会去掉单词个数超过10个的句子
  2. # 会去掉不是以特定开头的句子
  3. filterPairs([['i am a girl','i am a boy'],
  4.              ['how are you','how are you'], # 不是特定开头的会被去掉
  5.             ['i am a girl i am a girl i am a girl','i am a girl i am a girl']]) # 句子太长会被去掉

接下来把上面整个数据处理的流程写成一个函数。整个数据处理的流程如下所示:

  • Read text file and split into lines, split lines into pairs;
  • Normalize text, filter by length and content;
  • Make word lists from sentences in pairs;
  1. def prepareData(lang1, lang2, reverse=False):
  2.     """开始读取语言的文件
  3.     """
  4.     # 读取文件, 返回的是句子对
  5.     input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
  6.     print("Read %s sentence pairs" % len(pairs))
  7.     # 过滤掉句子对中较长的句子, 和
  8.     pairs = filterPairs(pairs)
  9.     print("Trimmed to %s sentence pairs" % len(pairs))
  10.     print("Counting words...")
  11.     for pair in pairs:
  12.         input_lang.addSentence(pair[0])
  13.         output_lang.addSentence(pair[1])
  14.     print("Counted words:")
  15.     print(input_lang.name, input_lang.n_words)
  16.     print(output_lang.name, output_lang.n_words)
  17.     return input_lang, output_lang, pairs

最后,开始读取数据即可。

  1. # 开始读取数据
  2. input_lang, output_lang, pairs = prepareData('eng', 'fra', True)

我们看一下最终的效果, paris 中保存了语言对,即法语与对应的英语

  1. print(random.choice(pairs))
  2. """
  3. ['il a accepte de faire le travail .', 'he s agreed to do the job .']
  4. """

input_lang 和 output_lang 中分别保存了原始语言和翻译后的语言的单词, 单词对应的 index(word2index), 和每个单词出现的频数(word2count)。

  1. print(output_lang.word2count.get('ok')) # 不同单词出现的次数
  2. print(output_lang.word2index.get('ok')) # 每个单词的 index

 

Seq2Seq 模型

接下来就是来创建 Model 了。在这里,我想再次强调一下 Seq2Seq 的思想。对比传统的单层的RNN 来说, 可以不需要输入和输出是相同的长度的.

下面是完整的思想,这里的原文还是很不错的,可以直接读一下:

Unlike sequence prediction with a single RNN, where every input corresponds to an output, the seq2seq model frees us from sequence length and order, which makes it ideal for translation between two languages.

Consider the sentence "Je ne suis pas le chat noir" → "I am not the black cat". Most of the words in the input sentence have a direct translation in the output sentence, but are in slightly different orders, e.g. "chat noir" and "black cat". Because of the "ne/pas" construction there is also one more word in the input sentence. It would be difficult to produce a correct translation directly from the sequence of input words.

With a seq2seq model the encoder creates a single vector which, in the ideal case, encodes the "meaning" of the input sequence into a single vector — a single point in some N dimensional space of sentences.

下面分开,具体的讲一下 Encoder 和 Decoder 的架构。

 

Encoder 的实现

The encoder of a seq2seq network is a RNN that outputs some value for every word from the input sentence. For every input word the encoder outputs a vector and a hidden state, and uses the hidden state for the next input word.

具体实现代码如下,这个部分还是与之前写 RNN 的代码是相同的。

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

我们做一下 Encoder 的测试,确保这部分是没问题的。

  1. test_data = tensorsFromPair(random.choice(pairs))
  2. test_data[0] # 句子对应的 index
  3. """
  4. tensor([[ 118],
  5.         [ 245],
  6.         [ 214],
  7.         [ 246],
  8.         [1541],
  9.         [   5],
  10.         [   1]], device='cuda:0')
  11. """
  12. # encoder测试
  13. encoder1 = EncoderRNN(input_lang.n_words, 256).to(device)
  14. output, hidden = encoder1(test_data[0].unsqueeze(1))
  15. print(output.shape) # shape, [seq_len, batch, ]
  16. print(hidden.shape)
  17. """
  18. torch.Size([7, 1, 256])
  19. torch.Size([1, 1, 256])
  20. """

 

Decoder 部分

The decoder is another RNN that takes the encoder output vector(s) and outputs a sequence of words to create the translation.

In the simplest seq2seq decoder we use only last output of the encoder. This last output is sometimes called the context vector as it encodes context from the entire sequence. This context vector is used as the initial hidden state of the decoder.

At every step of decoding, the decoder is given an input token and hidden state. The initial input token is the start-of-string token, and the first hidden state is the context vector (the encoder’s last hidden state).

关于 decoder 的部分需要注意的是:hidden_state 不是初始化的了,而是要传入 encoder最后的hidden_state。我一开始写的时候没注意到这点,最后模型的 loss 一直无法下降,所以这里需要注意一下。整体的代码如下。

  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.output_size = output_size
  6.         self.embedding = nn.Embedding(self.output_size, self.hidden_size)
  7.         self.gru = nn.GRU(self.hidden_size, self.hidden_size)
  8.         self.out = nn.Linear(self.hidden_size, self.output_size)
  9.         self.softmax = nn.LogSoftmax(dim=1)
  10.     def forward(self, x, hidden_state):
  11.         # 这里的hidden_state需要传入, 传入的是encoder最后输出的那个向量
  12.         self.hidden = hidden_state
  13.         # 只能预测一个, 输入下一个
  14.         embedded = self.embedding(x).view(1,1,-1) # 计算词向量
  15.         output, hidden = self.gru(embedded, self.hidden)
  16.         output = self.out(output[0]) # 从 hidden_size 转换为 output_size
  17.         output = self.softmax(output)
  18.         return output, hidden

我们还是测试一下这部分是否存在问题。

  1. # Decoder的输入
  2. test_data = torch.tensor([[SOS_token]]).to(device)
  3. # output_lang.n_words = 2803
  4. # output的单词数量有2803个
  5. # 这里的hidden是上面encoder输出的hidden_state
  6. decoder1 = DecoderRNN(256, output_lang.n_words).to(device)
  7. output, hidden = decoder1(test_data, hidden)
  8. # 这里有2803个单词, 每个单词的概率
  9. output.shape
  10. """
  11. torch.Size([1, 2803])
  12. """

 

开始训练!!!

训练数据的准备

To train, for each pair we will need an input tensor (indexes of the words in the input sentence) and target tensor (indexes of the words in the target sentence). While creating these vectors we will append the EOS token to both sequences.

这一部分主要完成的作用是将每句话都转为 Index, 并且在句末加上终止符.

  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)

我们看一下一个例子的效果:

  1. # 将一句话中的每个字母转为Index, 并在结尾加上终止符
  2. tensorFromSentence(output_lang, 'i am a boy')
  3. """
  4. tensor([[  2],
  5.         [ 16],
  6.         [ 42],
  7.         [472],
  8.         [  1]], device='cuda:0')
  9. """

 一些辅助函数

下面会定义一些辅助函数,首先是用来打印训练的耗时

  1. # helper function
  2. import time
  3. import math
  4. def asMinutes(s):
  5.     """将秒转换为分钟
  6.     """
  7.     m = math.floor(s / 60)
  8.     s -= m * 60
  9.     return '%dm %ds' % (m, s)
  10. def timeSince(since, percent):
  11.     """打印已经花费的时间和预计花费的时间
  12.        预计花费的时间, 用 完成百分比的时间/现在完成的百分比 来预测
  13.     """
  14.     now = time.time()
  15.     s = now - since
  16.     es = s / (percent)
  17.     rs = es - s
  18.     return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

接着是最后用来绘制 loss 的下降曲线的函数。

  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(figsize=(14,7))
  7.     fig, ax = plt.subplots()
  8.     # this locator puts ticks at regular intervals
  9.     loc = ticker.MultipleLocator(base=0.2)
  10.     ax.yaxis.set_major_locator(loc)
  11.     plt.plot(points)

定义训练函数

接下来就是定义训练使用的函数了,会有两个函数。首先是第一个train, 这个每次训练一个pair,如下所示:

  1. teacher_forcing_ratio = 0.5 # 50%的概率使用teacher_forcing的模式
  2. def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
  3.     encoder_optimizer.zero_grad()
  4.     decoder_optimizer.zero_grad()
  5.     input_length = input_tensor.size(0)
  6.     target_length = target_tensor.size(0)
  7.     # encoder_outputs = torch.zeros(max_length, encoder.hidden_size).to(device)
  8.     # Encoder
  9.     encoder_output, encoder_hidden = encoder1(input_tensor.unsqueeze(1))
  10.     # Decoder
  11.     loss = 0
  12.     decoder_hidden = encoder_hidden
  13.     decoder_input = torch.tensor([[SOS_token]]).to(device)
  14.     # 判断是使用哪一种模式
  15.     use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
  16.     if use_teacher_forcing:
  17.         # Teacher forcing: Feed the target as the next input
  18.         for di in range(target_length):
  19.             decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
  20.             loss = loss + criterion(decoder_output, target_tensor[di])
  21.             decoder_input = target_tensor[di] # Teacher Forcing
  22.     else:
  23.         # Without teacher forcing: use its own predictions as the next input
  24.         for di in range(target_length):
  25.             decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
  26.             topv, topi = decoder_output.topk(1)
  27.             decoder_input = topi.squeeze().detach() # detach from history as input
  28.             loss = loss + criterion(decoder_output, target_tensor[di])
  29.             if decoder_input.item() == EOS_token:
  30.                 break
  31.     # 反向传播, 进行优化
  32.     loss.backward()
  33.     encoder_optimizer.step()
  34.     decoder_optimizer.step()
  35.     return loss.item() / target_length

下面是一个整体的训练流程。

  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.     # 初始化优化器
  7.     encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
  8.     decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
  9.     # 初始化样本
  10.     training_pairs = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]
  11.     criterion = nn.NLLLoss()
  12.     for iter in range(1, n_iters+1):
  13.         training_pair = training_pairs[iter-1]
  14.         input_tensor = training_pair[0]
  15.         target_tensor = training_pair[1]
  16.         loss = train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)
  17.         print_loss_total = print_loss_total + loss
  18.         plot_loss_total = 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.                                          iteriter / 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)

正式开始训练

接下来就是激动人心的时刻了,开始正式的训练。

  1. hidden_size = 256
  2. encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
  3. decoder1 = DecoderRNN(hidden_size, output_lang.n_words).to(device)
  4. trainIters(encoder1, decoder1, n_iters=100000, print_every=500, plot_every=10)

最终的结果如下所示,Loss总体来说是一直在下降的。

Sequence to Sequence Learning with Neural Networks--使用 Seq2Seq 完成翻译

模型的评测

Evaluation is mostly the same as training, but there are no targets so we simply feed the decoder's predictions back to itself for each step. Every time it predicts a word we add it to the output string, and if it predicts the EOS token we stop there. We also store the decoder's attention outputs for display later.

最后就是模型的评测的阶段了,我们使用他来实际进行一下句子的翻译。

  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
  6.         encoder_output, encoder_hidden = encoder1(input_tensor.unsqueeze(1))
  7.         # Decoder
  8.         decoder_input = torch.tensor([[SOS_token]], device=device)  # SOS
  9.         decoder_hidden = encoder_hidden
  10.         decoded_words = []
  11.         for di in range(max_length):
  12.             decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden  )
  13.             topv, topi = decoder_output.data.topk(1)
  14.             if topi.item() == EOS_token:
  15.                 """遇到终止符就停止
  16.                 """
  17.                 decoded_words.append('<EOS>')
  18.                 break
  19.             else:
  20.                 """把decode的word加入数组中
  21.                 """
  22.                 decoded_words.append(output_lang.index2word[topi.item()])
  23.             # 下一个的输入是上一个的输出
  24.             decoder_input = topi.squeeze().detach()
  25.         return decoded_words

下面是随机挑选一些句子来进行翻译:

  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 = evaluate(encoder, decoder, pair[0])
  7.         output_sentence = ' '.join(output_words)
  8.         print('<', output_sentence)
  9.         print('')

最后我们看一下最终的效果:

  1. evaluateRandomly(encoder1, decoder1, n=70)
  2. """
  3. > je suis egalement heureuse .
  4. = i m happy too .
  5. < i m happy too . <EOS>
  6. > j attends de toi un travail serieux .
  7. = i am expecting some serious work from you .
  8. < i am expecting expecting serious serious serious . <EOS>
  9. > tu es chanceuse .
  10. = you re fortunate .
  11. < you re fortunate . <EOS>
  12. """

整体来说还是可以的,但是我们仔细看会发现一些问题,如会出现一个单词反复出现很多次,如第二句中的serious. 这一部分的改进就不在这里细说了。

总结

上面就是所有关于简单的 Sequence to Sequence(Seq2Seq) 的介绍了, 介绍了包括原理和一个简单的例子实现。下面有 Notebook 的代码链接,大家可以尝试一下这个模型。

Notebook的代码链接如下Seq2Seq模型代码

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: