基于TensorFlow框架的Seq2Seq英法机器翻译模型

1,022 阅读17分钟
原文链接: zhuanlan.zhihu.com

前言

本篇文章的内容主要是基于英法平行语料(English-French Parallel Corpus)来实现一个简单的英法翻译模型,代码框架采用TensorFlow 1.6。

本篇文章与去年我在知乎专栏《机器不学习》上发表的《从Encoder到Decoder实现Seq2Seq模型》文章类似。

天雨粟:从Encoder到Decoder实现Seq2Seq模型

那为什么今天还要重新写这篇文章,对这篇文章和代码更新的原因有以下4个方面:

  • 去年文章接口实现采用TensorFlow 1.1实现,有些接口已经发生变化,导致代码下载以后部分片段无法正常运行;
  • 文章部分写作内容描述不够清晰,本篇文章对一些表达不当的地方进行重构;
  • 之前的Seq2Seq模型是对单词的字母进行排序,数据处理部分相对较为简单。而此次将采用英法平行语料来构建翻译模型,增加一些数据处理操作;
  • 专栏下一篇文章准备写关于改进版本的Machine Translation模型,包括使用BiRNN和Attention机制的模型(将采用Keras实现),此篇文章可以来做些许铺垫。

运行环境

写专栏的目的主要在于通过代码理解算法,在之前的文章中,有很多同学会提一些接口的问题,恕平时较忙,无法一一回答,关于接口和代码的基础问题请自行百度或Google,基本都能得到解决。本篇文章与代码所基于的环境:

  • 系统环境:Mac OS High Sierra
  • Python环境:Python 3.6
  • TensorFlow版本:1.6
  • Anaconda (Jupyter Notebook)

代码完整地址:NELSONZHAO/zhihu

欢迎各位Star和Fork。

正文

本文主要包含两个部分:数据处理与模型构建。在数据处理部分,我们将会把原始的平行语料转化为我们模型所需要的数据。在模型构建部分,我们将会一步步基于TensorFlow 1.6构建最基本的Seq2Seq模型,并基于我们所拥有的平行语料进行训练与预测。


第一部分 数据处理

在数据处理部分,我们主要包括以下四个步骤:

1. 加载数据。本篇文章使用的数据是English-French平行语料(parallel corpus)。

    • small_vocab_en文件包含了英文原始语料,其中每一行代表一个完整的句子
    • small_vocab_fr文件包含了法语原始语料,其中每一行代表一个完整的句子

2. 数据探索。即对语料文本的统计性描述。

3. 数据预处理。

    • 构造英文词典(vocabulary),对单词进行编码
    • 构造法语词典(vocabulary),对单词进行编码

4. 语料转换。

    • 将原始英文文本转换为机器可识别的编码
    • 将原始法语文本转换位机器可识别的编码

1. 加载数据

由于本篇文章主要是来做英文到法语的翻译,因此我们输入的是英文,期望输出的是法语。因此,我们称英文是source,法语为target。后面文章中变量的命名也采用这种方式。

我们的数据为平行语料,即每一行为一个完整的句子,两个文件的同一行对应着同一句话的英法两种语言的表达。

2. 描述性统计

描述性统计主要在于帮助我们了解数据。

上面的代码主要是对加载进来的source_text和target_text进行统计分析。我们的原始语料均采用了小写处理。

  • 4-5行代码统计了在source文本中的唯一字符串(包括单词与标点)个数;
  • 9-13行代码对source文本进行了分句与分词,统计了其中的句子个数、平均句子长度和最大句子长度;
  • 16-17行代码对target文本进行了分句与分词,统计了其中的句子个数、平均句子长度和最大句子长度;
  • 24-28行代码分别print了英法语料的前10个句子。

通过统计分析,我们可以看到我们的样本共有13W多的句子,其中英文句子的平均长度为13.2,法语句子的平均长度为14.2。英文句子的最大长度为17,法语句子相对更长,其最大句子长度为23。

3. 数据预处理

机器翻译模型的基本架构是Seq2Seq模型,在构造模型之前,我们需要先对语料进行处理。即将文本语料转化为机器所能识别的数字。例如,对英文句子:I love machine learning and deep learning,编码为数字[28, 29, 274, 873, 12, 983, 873]。因此本部分主要完成以下几个任务:

  • 根据语料构造英文与法语的字典(vocabulary)
  • 构造英语与法语的映射,即将单词转换为数字的字典
  • 构造英语与法语的反向映射,即从数字转换为单词的字典

构造词典(Vocabulary)

上述代码分别对source和target文本进行单词的统计,构造了词典。由于我们文本较少,所以包含的唯一词也较少。在英文语料中,我们的词典大小为227,在法语语料中,词典大小为354。

构造映射

有了词典以后,我们就可以根据词典来构造单词的映射,将语料文本转化为机器可识别的编码。

首先,我们先定义了特殊字符<PAD>,<UNK>,<GO>,<EOS>。

  • <PAD>:由于翻译问题的特殊性,我们的句子长度往往是不一致的,而在RNN处理batch数据时,我们需要保证batch中的句子长度一致,此时需要通过<PAD>对长度不足的句子进行补全;
  • <UNK>:Unknown字符,用来处理模型未见过的生僻单词;
  • <GO>:翻译句子时,用来告诉句子开始进行翻译。仅在target中使用;
  • <EOS>:翻译句子时,用来告诉句子结束翻译。仅在target中使用。

第二部分代码块我们根据词典和特殊字符分别构造了英法的单词映射。可以看到,英文的映射词典大小为229,法语的映射词典大小为358。

4. 语料转换

有了上述数据预处理的结果,我们就可以对原始语料进行文本转换,即将文本转换为数字。在转换过程中,由于我们LSTM只能处理定长的数据,因此我们需要保证输入语料的长度Tx与输出语料的长度Ty保持固定。假设Tx=20,则对于不足20个单词的句子进行PAD,对超过20个单词的句子进行截断。

例如,对于输入句子”I love machine learning and deep learning",编码后为[28, 29, 274, 873, 12, 983, 873, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]。

在这里,我们实现一个函数来进行转换,该函数接受一个完整的句子,并返回映射结果。

  • sentence参数是一个完整的句子;
  • map_dict是单词到数字的映射,即我们上面生成的source_vocab_to_int和target_vocab_to_int;
  • max_length是指最大句子长度;
  • is_target是用来说明是否对target句子进行处理,因为在处理source和target中有个不同,在target中,我们需要在每个句子的最后添加<EOS>字符,而在source中不需要做这个操作。

函数构造后,我们就可以对英文语料和法语语料分别进行处理。

其中,我指定了英文句子的最大长度为20,也就是说,对不足20个单词的句子进行补全,对超过20个单词的句子进行截断。法语句子相对较长,我选用25作为最大长度。

我们随机选择一个处理后的结果查看,如上所示,两个句子分别对应了英法两种表达,进行处理后就变为了数字编码,并且不足长度的都用0进行了补全。

至此,我们的数据处理部分就基本完成,这一部分我们将文本语料通过构造词典、映射等方式转化为了机器可识别的数字编码。下一部分我们将开始对模型进行构建。


第二部分 模型构建

在第一部分的数据构建中,我们已经将原始的语料文本转化为机器能够识别的数字编码。在第二部分,我们将分三块构建我们的模型。首先在基本的Seq2Seq模型中,我们输入一个序列,通过Encoder端将序列转换为一个固定长度的向量(Context Vector),这个向量包含了输入序列的信息,将这个向量再作为输入传递给Decoder端生成新的序列。我们以翻译为例,如下图所示:

我们输入”我爱机器学习“,期望它能够生成其对应的英文翻译:”I love machine learning“。从输入到输出,共经历了以下几个步骤:

  1. 对”我爱机器学习“文本进行分词得到四个词”我“,”爱“,”机器“,”学习“;
  2. 将每个词进行Embedding嵌入,即转化为稠密向量;
  3. 输入给LSTM进行学习;
  4. LSTM学习序列得到固定长度的Context Vector;
  5. Context Vector作为输入传输给Decoder端,进行新词生成;
  6. 得到翻译结果”I love machine learning“。

在上图中,左下角部分是Encoder端,右上角部分是Decoder端,因此,我们在函数和代码的构件上也大题遵循模型结构。我们的模型代码模块主要分为以下四个部分:

  • 模型输入 model_inputs
  • Encoder端 encoder_layer
  • Decoder端
    • Decoder输入端 decoder_layer_inputs
    • Decoder训练 decoder_layer_train
    • Decoder预测/推断 decoder_layer_inference
  • Seq2Seq模型

1. 模型输入

模型输入主要是实现tensor的构造。

这个函数构造了inputs,targets等tensor。具体来说,inputs是一个二维tensor,第一个维度代表batch size,第二个维度是序列长度,targets同理。如下图所示:

inputs是经过编码以后的文本序列(图中数字我瞎写的,仅供说明图),如果我们此时只给模型传入一个batch,则inputs的shape为[1, 4],其中1对应batch的大小,4对应序列长度。targets同理。

2. Encoder端

Encoder端即编码端,其目的是通过学习序列来将其映射为一个固定长度向量。具体实现代码为:

首先,我们要对输入的inputs进行词向量嵌入,进而使用LSTM进行序列学习。通过tf.nn.dynamic_rnn方法返回LSTM的序列状态,其中encoder_states即为我们所需的Context Vector。

具体而言,encoder_embed是经过嵌入的词向量,如果指定encoder_embed_size为100,则我们得到的每个词的嵌入向量就是100维,如上图所示。LSTM最终的状态结果就是Context Vector。

3. Decoder端

在Decoder端,我们分为三个模块:

  • decoder_layer_inputs:主要用来处理Decoder的输入;
  • decoder_layer_train:用于训练过程中的Decoder;
  • decoder_layer_infer:用于测试过程中的Decoder。

( 1 ) Decoder输入

Decoder端输入需要特殊处理,如下图所示:

在Decoder端,我们用Context Vector和上一轮的输出结果来生成当前阶段的单词。当我们在最后一个阶段时,上一轮输入结果的learning,此时根据learning我们输出了<EOS>,代表着翻译的结束。我们可以发现,最后一的输出词并没有再作为输入来进行预测,也就是说<EOS>并不会作为下一阶段的输入。我们可以知道,由于Decoder端最后一个输出词并不参与到新一轮的输入中,我们可以将输出结果的最后一个单词去掉。

另外,我们需要在Decoder端用“<GO>”告诉它翻译的开始。因此,我们的函数就用来完成这两个功能:

第10行代码用来将整个batch中的每一个句子的最后一个字符去掉,第12行代码则在每个句子前面添加一个起始符“<GO>”。

具体如下图:

先删除最后一个字符“<EOS>”,在加入起始词“<GO>”。

( 2 ) Decoder训练

完成了input的构建,我们需要再构造decoder端的训练(train)与预测(infer)函数。为什么这里train和infer要分开呢?我们知道在LSTM中,会将上一轮的输出结果作为下一轮的输入,例如在预测“learning”的时候,我们会将上一轮的输出“machine”作为输入,但如果上一轮的输入不够准确,或者有误差,那么就意味着会影响到后续的所有预测结果。

那怎么解决呢?具体来说,由于我们训练过程中知道每一轮的真实输入(ground truth)是啥,因此,我们可以强制用真实输入来进行训练,这种方式叫做Teacher Forcing,如下图所示:

在第一行中,我们用普通的方式训练,即用上一轮的输出作为下一轮的输入,当我们在某一轮出错时,即love本身应该输出machine,但却输出了apple,这种错误会不断累积到后续的训练中,将会导致翻译结果完全错误;而采用Teacher Forcing时,由于我们知道训练样本真实的targets应该是I love machine learning,因此我们的每一轮输入都用真实标记去训练,这样可以保证在训练过程中缓解误差对后续训练的影响。

但是Teacher Forcing方法仅适用于训练过程,因为在预测过程中,我们无法得知真实标记,只能将前一个的输出作为当前的输入。因此在这里我们需要构建两个函数来区分训练和预测过程。

在TensorFlow中通过TrainingHelper构造一个helper对象,并传入BasicDecoder中。

在dynamic_decode方法中,impute_finished为True时,当预测被标记为结束时,会自动复制前一轮状态向量,并将输出都置为0。

Python boolean. If True, then states for batch entries which are marked as finished get copied through and the corresponding outputs get zeroed out.

( 3 ) Decoder预测

在预测阶段,构造GreedyEmbeddingHelper对象传入BasicDecoder中。

TensorFlow的接口中对GreedyEmbeddingHelper的定义为:
A helper for use during inference.
Uses the argmax of the output (treated as logits) and passes the result through an embedding layer to get the next input.

( 4 ) Decoder层

Decoder层对上述函数进行了组装。

上面函数主要定义了几个内容:

  1. 第19行到20行代码对Decoder端的输入数据进行Embedding;
  2. 第26行代码用来构造LSTM层;
  3. 第29行代码用来构造全连接层;
  4. 第31-49行代码用来调用train和infer获取logits。

4. Seq2Seq模型

通过上面的函数,我们已经将Encoder和Decoder端的各个模块都定义完成,下面再构造一个函数来将这些组件拼在一起。

上面的代码主要分为三步:

  1. 第24行代码来获得Encoder端对输入源序列的编码结果;
  2. 第27行代码用来处理Decoder端的输入;
  3. 第29行代码调用decoder端获得train和infer的输出。

第三部分 模型训练与预测

经过数据处理部分与模型构建部分,我们完成了训练前的准备工作。接下来我们需要定义超参数,启动我们的图,并喂入数据进行训练。

定义的超参数如下:

设置了10轮迭代,1层LSTM,encoder与decoder的嵌入词向量维度均为100维,并指定每训练50轮打印一次结果。

由于我们的训练语料比较少,仅有13W条,而机器翻译这种待学习参数规模较大的模型需要大量的训练文本,因此我在这里并没有划分train和validation,用了所有的13W数据进行了train。

经过模型训练,我们可以看到训练的Loss最终在0.01左右徘徊,大家也可以自己重新调整参数进行训练。

预测部分我们将输入一些训练数据本身有的句子,来看看翻译效果如何:

---------------------------------------【例子1】---------------------------------------

我们输入“the united states is never beautiful during march , and it is usually relaxing in summer .”

模型给出的结果是“les états-unis est jamais belle en mars , et il est relaxant habituellement en hiver . <EOS>”

Google翻译的结果是“les États - unis n'est jamais beau en mars, et il est habituellement relaxant en été .”

不懂法语没关系,我们将模型给出的法语翻译结果再用Google翻译到英文看看:“the united states is beautiful in march, and it's relaxing, usually in the winter.”

可以看到我们模型将原来的英文Summer翻译成了winter,说明模型捕捉到了这里的季节单词,但具体的季节却捕捉错误。另外句子前半句没有捕捉到否定词“never”。

---------------------------------------【例子2】---------------------------------------

我们输入“I dislike grapefruit , lemons , and peaches .”

模型给出的结果是“je n'aime pamplemousses , les citrons et les mangues . <EOS>”

Google翻译的结果是“je déteste les pamplemousses, citrons, et les pêches.”

我们将模型给出的法语翻译结果再用Google翻译到英文看看:“i don't like grapefruit, lemon and mango.”

这次翻译结果相对还可以,模型将“peaches”错翻译为“mango”,同样它捕捉到了这是水果,但具体是什么水果没有很好地学习到。

总体来说,模型在训练数据的拟合上还是可以的。读者也可以自行尝试其他训练数据中的句子进行测试。但如果输入训练过程中没出现的句子,翻译的结果就会大打折扣。

总结

本篇文章基于TensorFlow 1.6版本构建了基础的Seq2Seq模型,并通过模型实现了一个简单的英法翻译模型。即通过Encoder端对输入序列进行学习,得到编码以后的Context Vector,再将Context Vector传入给Decoder端进行学习,生成翻译结果。在Decoder端,train阶段采用teacher forcing方式,用ground truth作为输入;而预测阶段则采用前一轮生成结果作为输入。最终模型在训练数据翻译效果上还算不错。

本篇文章主要目的是想通过代码让大家对基础Seq2Seq模型和翻译模型有一个大概了解,因此对于模型的一些实现上做了简化。本模型主要有以下几点不足:

  • 训练语料过少。对于翻译模型这种大规模依赖数据的模型不是很适合;
  • 未划分训练集与测试集。这也是由于训练语料过少导致的,划分train和validation将使得模型训练数据更少;
  • 评估指标过于简单。一般来说,翻译模型的效果用BLEU(bilingual evaluation understudy)进行评估,文中仅简单地使用了loss来观察训练的收敛;
  • 对于语料数据的补全过于简单。一般来说,语料的句子长度不一致,大语料中不仅包括长句,还有一些短句。可以看出本文的语料中几乎都是短句子,英文句子最大长度是17,法语句子最大长度是23。而实际中们的句子可能更长,此时将短句子按照最大句子长度补全就显得不是那么合适,在工程上效率很低,因此多采用bucket对不同长度的语料进行分组。

基本的Seq2Seq模型相对比较简单,实际上,对于模型的改进还有很多方面,例如加入BiRNN,捕捉到更全面的输入序列的信息;加入Attention,在翻译每个单词时会使用不同的context vector等。后续专栏将会基于Keras对翻译模型进行更进一步的改进。

专栏:机器不学习
个人简介:天雨粟
GitHub: NELSONZHAO
转载请联系作者获得授权。