解析Tensorflow官方English-Franch翻译器demo

今天我们来解析下Tensorflow的Seq2Seq的demo。继上篇博客的PTM模型之后,Tensorflow官方也开放了名为translate的demo,这个demo对比之前的PTM要大了很多(首先,空间上就会需要大约20个G,另外差点把我的硬盘给运行死),但是也实用了很多。模型采用了encoder-decoder的框架结果,佐以attention机制来实现论文中的英语法语翻译功能。同时,模型的基础却来自之前的PTM模型。下面,让我们来一起来了解一下这个神奇的系统吧!

 

论文介绍及基础描写:

这个英语法语翻译器融合了多篇论文的核心内容,所以在学习的过程中其实我们可以变相的了解这些技巧。首先,Cho在论文Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation一文中指出可以通过encoder-decoder这种类似于编码-解码的框架来做机器翻译。这个框架在后续论文中掀起了不小的波澜,很多作品延续了这种sequence-to-sequence的框架在其他的一些领域,比如image caption。该框架说白了就是两个RNN,一个作为编码器,一个作为解码器。至于RNN cell的结构,虽然本项目支持使用LSTM, default的模型为GRU(Gated Recurrent Uint),一个LSTM的简化版模型。另外,为了使翻译的效果达到最好,2014年Bahdanau在论文Neural Machine Translation By Jointy Learning to Align and Translate中提出的attention机制也被运用了起来。背景介绍到此,下面我们赶快来看看代码吧!

 

代码:

较之PTM模型,机器翻译的demo代码有足足个file之多,可见其难度。(其实也没那么难,都是些犹如纸老虎般的存在,充其量也就在数量上吓唬吓唬人罢了。)项目源代码可以在Tensorflow官方github中找到,地址点击这里。打开项目后我们发现了3个文件,分别是data_utils.py, seq2seq_model.py以及translate.py。这三个文件里,data_utils.py是类似于helper function般的存在,如其名,是一个集合了对输入raw的数据做处理的一个file。file本身不是用来运行的,只是被translate.py所使用而已。这个文件本身也可以在你自己的Tensorflow框架的本地库内找到,所需要做的只是输入from tensorflow.models.rnn.translate import data_utils,就可以取得库里的函数了。为方便用户,这个data_utils文件里的函数还自带检测数据库是否存在于本地路径的功能,如果你没来得及或不知道如何下载数据库,只需要指定路径后运行库里的函数即可,是不是很方便?

Encoder-decoder模型存在于第二个文件中,即seq2seq_model.py文件。文件作为一个类包含了组建机器翻译所需要的神经网络图框架。与data_utils文件一样,他们都是被我们的“main”文件,translate.py使用的。

了解了三个文件的关系后,我们也了解了系统运行的过程以及解读顺序,那么,我们先来了解一下程序运行的大体顺序。运行该程序的方式为cd到文件所在的目录下后在terminal里面输入“python translate.py --data_dir /tmp/ --train_dir /tmp/”就可以了。 其中tmp为默认的tmp文件夹,如果你希望长期保留运行结果,建议在别的文件夹里进行实验。另外,你可以改变一些模型的参数,如把模型改为两层,每层改为256个神经元等。方法为在刚才的comment后面加上两横杠,更改参数名,等于号及更改后参数量。如改变模型为两层的框架的方法是“--num_layers=2”。那么在输入参数后系统是怎么运行的呢?我们在translate.py里找到了main函数的代码如下:

1
2
3
4
5
6
7
def main(_):
   if FLAGS.self_test:
     self_test()
   elif FLAGS.decode:
     decode()
   else :
     train()

 该代码显示,一般情况下如果你没表明要运行decode模式或者测试模式,训练模式将自动开始。那么很明显,我们的主程序在训练模式,也就train函数里。那么我们就顺藤摸瓜来看看train函数吧。

首先,train函数的开始为运行data_utils库的prepare_wmt_data函数,这里的输入为我们在之前手动输入的data_dir外还有英语和法语的单词数。这里除了data_dir是我们之前手动输入的外,英法语的单词数默认都是4万。通过这个神奇的prepare_wmt_data函数,我们可以得到英语,法语两种语言的训练以及测试资料外,还可以获得英语及法语单词的单词表存放路径。那么这个函数是如何工作的呢?我们这就来一探究竟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def prepare_wmt_data(data_dir, en_vocabulary_size, fr_vocabulary_size, tokenizer = None ):
   """Get WMT data into data_dir, create vocabularies and tokenize data.
   Args:
     data_dir: directory in which the data sets will be stored.
     en_vocabulary_size: size of the English vocabulary to create and use.
     fr_vocabulary_size: size of the French vocabulary to create and use.
     tokenizer: a function to use to tokenize each data sentence;
       if None, basic_tokenizer will be used.
   Returns:
     A tuple of 6 elements:
       (1) path to the token-ids for English training data-set,
       (2) path to the token-ids for French training data-set,
       (3) path to the token-ids for English development data-set,
       (4) path to the token-ids for French development data-set,
       (5) path to the English vocabulary file,
       (6) path to the French vocabulary file.
   """
   # Get wmt data to the specified directory.
   # 建立训练集并取得他们的路径
   train_path = get_wmt_enfr_train_set(data_dir)
   dev_path = get_wmt_enfr_dev_set(data_dir)
 
   # Create vocabularies of the appropriate sizes.
   # 建立英语及法语的单词表
   fr_vocab_path = os.path.join(data_dir, "vocab%d.fr" % fr_vocabulary_size)
   en_vocab_path = os.path.join(data_dir, "vocab%d.en" % en_vocabulary_size)
   create_vocabulary(fr_vocab_path, train_path + ".fr" , fr_vocabulary_size, tokenizer)
   create_vocabulary(en_vocab_path, train_path + ".en" , en_vocabulary_size, tokenizer)
 
   # Create token ids for the training data.
   # 将输入数据里的单词数字化以方便运算
   fr_train_ids_path = train_path + ( ".ids%d.fr" % fr_vocabulary_size)
   en_train_ids_path = train_path + ( ".ids%d.en" % en_vocabulary_size)
   data_to_token_ids(train_path + ".fr" , fr_train_ids_path, fr_vocab_path, tokenizer)
   data_to_token_ids(train_path + ".en" , en_train_ids_path, en_vocab_path, tokenizer)
 
   # Create token ids for the development data.
   # 将测试数据里的单词数字化以方便运算
   fr_dev_ids_path = dev_path + ( ".ids%d.fr" % fr_vocabulary_size)
   en_dev_ids_path = dev_path + ( ".ids%d.en" % en_vocabulary_size)
   data_to_token_ids(dev_path + ".fr" , fr_dev_ids_path, fr_vocab_path, tokenizer)
   data_to_token_ids(dev_path + ".en" , en_dev_ids_path, en_vocab_path, tokenizer)
 
   return (en_train_ids_path, fr_train_ids_path,
           en_dev_ids_path, fr_dev_ids_path,
           en_vocab_path, fr_vocab_path)

在data_utils库里,prepare_wmt_data整合了其他的helper函数,其中,get_wmt_enfr_train_set和get_wmt_enfr_dev_set两个函数测试了训练集的路径是否存在后,视情况下载训练集(如果训练集还未下载),之后便是解压了训练包中所用的具体集并提供其路径。虽然对于测试集有不同的应对方法,其逻辑大同小异,具体实现过程大家可以阅读这两个函数去一探究竟,这里将不做细说。

之后函数建立了两个不同的单词表,即英语单词表及法语单词表。在建立表的过程中,逻辑近似于之前讨论过的Word2Vec的create_dataset函数,即从海量数据里建立一个对应的字典来统计输入词。其中,值得注意的是这里有一个tokenizer输入的选项,默认的tokenizer是在符号处截断句子,其代码如下:

1
2
3
4
5
6
7
8
_WORD_SPLIT = re. compile (b "([.,!?\"':;)(])" )
 
def basic_tokenizer(sentence):
   """Very basic tokenizer: split the sentence into a list of tokens."""
   words = []
   for space_separated_fragment in sentence.strip().split():
     words.extend(re.split(_WORD_SPLIT, space_separated_fragment))
   return [w for w in words if w]

 该项目鼓励读者们去采用更好的tokenizer去取得更好的结果。最后,不同于Word2Vec模型的最大地方在于我们将这个单词集保存后备用,也就是说,在第一次运算耗时可能很长后,之后在运行将会比较方便。

在建立完词典后,通过运用data_to_token_ids函数,我们可以将输入转化为数字序列并将其保存,这样可以方便我们系统的运用。其原理也可以在Word2Vec的demo代码里找到逻辑,即运用单词在词典里的位置来代替单词。具体内容请参考博客Python Tensorflow下的Word2Vec代码解释。所得的结果在保存在各自的文件中已备后用后,我们可以直接运用这些资料来训练我们的系统了。接下来,让我们重新回到translation.py文件的train()函数来继续了解它的机制。在得到了输入及训练资料后,我们看到了熟悉的with tf.Session() as sess,这里大家都明白怎么回事了吧,我们进入session了,可以开始建立模型并运行了。那么很明显,我们现在的任务就是建立模型。这里,我们看到了model = create_model(sess, False), 这个函数的具体内容就在train()函数之上,很好找。那么,它是怎么建立模型的呢?走进该函数后我们发现它其实就是个包装盒子,运用了系统的另一个文件,即seq2seq_model.py库里Seq2SeqModel类来得到模型,之后便是取得模型目前的状态。如果模型已经训练并保存,我们即呼唤之前训练的模型并返回。反之则初始化所有参数并返回模型。

现在,让我们来看看这个模型本身。在seq2seq_model.py文件里有三个函数:init函数,step函数及get_batch函数。目前,我们在制造模型阶段,所以先来看看这个init函数。

1
2
3
4
def __init__( self , source_vocab_size, target_vocab_size, buckets, size,
                num_layers, max_gradient_norm, batch_size, learning_rate,
                learning_rate_decay_factor, use_lstm = False ,
                num_samples = 512 , forward_only = False ):

 这个函数的parameter列很长,有源语言和目标语言各自的单词数量,框架的层数,每层的神经元数,用于clipped的梯度的最大数值,训练batch的大小,learning rate,learning rate减少的比例及sampled softmax所接收的sample数量。这些都是常见的训练神经网络的参数。参数use_lstm也比较好理解,默认的false表明我们将会运用到GRU,即简易版本的lstm。设置为True时便是使用传统的LSTM cell了。现在,有两个参数我没讲解到,buckets参数和forward_only参数。这两个参数挺有意思。bucket参数的存在是针对机器翻译的,他的格式为一个充满(I,O)的list,I代表这最大输入长度,O代表着输出的最大长度。当输入或输出超出这个距离后,我们将超出的部分放入下一个batch。至于forward_only参数,其存在是因为两种不同的训练方式。一种方式为在训练中根据两种语言各自训练input及output,这是默认方式,即该参数设为Flase。如果我们设为True后,将会在训练output语言时运用output目标的开头后由输入语言取得剩下的数据。两种方法在官方的document里有详细讲解,这里附上链接供有兴趣的读者加深了解。

之后,在assign了变量后,我们又见到了熟悉的RNN模型框架,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# If we use sampled softmax, we need an output projection.
output_projection = None
softmax_loss_function = None
# Sampled softmax only makes sense if we sample less than vocabulary size.
if num_samples > 0 and num_samples < self .target_vocab_size:
   w = tf.get_variable( "proj_w" , [size, self .target_vocab_size])
   w_t = tf.transpose(w)
   b = tf.get_variable( "proj_b" , [ self .target_vocab_size])
   output_projection = (w, b)
 
   def sampled_loss(inputs, labels):
     labels = tf.reshape(labels, [ - 1 , 1 ])
     return tf.nn.sampled_softmax_loss(w_t, b, inputs, labels, num_samples,
             self .target_vocab_size)
   softmax_loss_function = sampled_loss
 
# Create the internal multi-layer cell for our RNN.
single_cell = tf.nn.rnn_cell.GRUCell(size)
if use_lstm:
   single_cell = tf.nn.rnn_cell.BasicLSTMCell(size)
cell = single_cell
if num_layers > 1 :
   cell = tf.nn.rnn_cell.MultiRNNCell([single_cell] * num_layers)
 
# The seq2seq function: we use embedding for the input and attention.
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
   return tf.nn.seq2seq.embedding_attention_seq2seq(
       encoder_inputs, decoder_inputs, cell,
       num_encoder_symbols = source_vocab_size,
       num_decoder_symbols = target_vocab_size,
       embedding_size = size,
       output_projection = output_projection,
       feed_previous = do_decode)

 这里,我们先了解到如果我们的参数num_samples大于0但小于目标单词量时,我们再次如同PTB模型那样运用一个projection layer来减少空间的占据。之后,我们默认celll类型是GRU,但是如果用户设定了用lstm,我们即更新single_cell变量到lstm cell框架。当网络层大于1时(默认为3层),我们的cell变成了一个多层RNN框架,由单层cell累积组成。这个逻辑已经在之前关于PTM模型的博客里有过介绍,如果你不熟悉这个设定,请看之前PTB模型的博客。在之后,我们发现设计了一个seq2seq_f的内部函数,这个函数为模型加入attention机制,之后我们会用到。

如同往常,我们为源语言,目标语言的训练输入以及目标权重建立placeholder,因为训练目标是输入目标的下一句,我们设定目标为目标语言输入的现在位置+1。之后便是建立训练输出及计算loss的时刻了。基本方法如下:

1
2
3
4
5
self .outputs, self .losses = tf.nn.seq2seq.model_with_buckets(
           self .encoder_inputs, self .decoder_inputs, targets,
           self .target_weights, buckets,
           lambda x, y: seq2seq_f(x, y, False ),
           softmax_loss_function = softmax_loss_function)

我们将会运用Tensorflow库seq2seq里的model_with_buckets函数来完成。这个函数是什么呢?让我们来一探究竟。该函数的目标是建立一个bucket版本的seq2seq模型。模型取得encoder, decoder的输入,目标和权重的Tensor,输入和输出大小配对列表叫做buckets的参数后,要求一个sequence to sequence模型,softmax_loss_function函数(default是None),每例子的loss(default None)及名字(default None)。输出是(output, losses)tuple,output指的是每一个bucket的输出,losses这该bucket的loss数值。了解了这些后,我们发掘这里的sequence to sequence函数输入我们运用了lambda匿名函数,核心是我们的attention模型。这是我们训练的基础,但是当forward_only被设为True时,训练后,我们又多了一步,及重新编写buckets的output为揉合输出及之前projection的output。代码如下:

1
2
3
4
5
6
if output_projection is not None :
   for b in xrange ( len (buckets)):
     self .outputs[b] = [
         tf.matmul(output, output_projection[ 0 ]) + output_projection[ 1 ]
         for output in self .outputs[b]
     ]

 之后便是传统的RNN训练方法,即运用GradientDecentOptimizer,并运用clip_by_global_norm及appy_gradient函数。这些知识点在之前的PTB模型里以详细介绍,这里将不再重复。

 建立模型之后,在train()函数里,我们读入测试和训练的数据,计算训练的bucket并选择输入的句子属于哪个bucket (= [sum(train_bucket_sizes[:i + 1]) / train_total_size for i in xrange(len(train_bucket_sizes))]),之后便是具体训练的循环步骤。

步骤里,系统在运用seq2seq_model类里的get_batch和step两个函数,在取得了一个batch的数据后训练一个步骤的数据,并在一定的步骤后展出结果。代码的逻辑还是很清晰的,只是值得注意的是第205行的sys.stdout.flush()代码,这代码的存在是为了实时把运算结果展示在terminal的。除此之外,大家可以仔细阅读代码来加深了解,这里将不再细说。

就此,系统训练的步骤讲完了。但当我们准备测试我们的训练结果时,我们该怎么办呢?这里,我们就要讲讲这个decode()函数了。根据官方的说明,我们在训练模型后模型参数等全部资料全部都是有好好的保存的,所以我们不需要再次训练了,我们只需要运行“python translate.py --decode --data_dir /tmp/ --train_dir /tmp/"即可,一个interactive session将会打开供我们运作。

这个decode函数本身是用来测试系统的,所以在初始化英语法语的单词表后,系统读取我们输入的一行句子后,运行一下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 读取一行话
sentence = sys.stdin.readline()
while sentence:
   # Get token-ids for the input sentence.
   # 把输入转换成token_ids
   token_ids = data_utils.sentence_to_token_ids(tf.compat.as_bytes(sentence), en_vocab)
   # Which bucket does it belong to?
   # 选择输入所对应的bucket大小
   bucket_id = min ([b for b in xrange ( len (_buckets))
                    if _buckets[b][ 0 ] > len (token_ids)])
   # Get a 1-element batch to feed the sentence to the model.
   # 取得一个一个element的batch并通过step函数来取得运行结果
   encoder_inputs, decoder_inputs, target_weights = model.get_batch(
       {bucket_id: [(token_ids, [])]}, bucket_id)
   # Get output logits for the sentence.
   _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs,
                                    target_weights, bucket_id, True )
   # This is a greedy decoder - outputs are just argmaxes of output_logits.
   outputs = [ int (np.argmax(logit, axis = 1 )) for logit in output_logits]
   # If there is an EOS symbol in outputs, cut them at that point.
   if data_utils.EOS_ID in outputs:
     outputs = outputs[:outputs.index(data_utils.EOS_ID)]
   # Print out French sentence corresponding to outputs.
   print ( " " .join([tf.compat.as_str(rev_fr_vocab[output]) for output in outputs]))
   print ( "> " , end = "")
   sys.stdout.flush()
   sentence = sys.stdin.readline()

 通过这种方式,我们可以验证我们系统的好坏。可惜的是第一,我不懂法语。第二,系统在读取第1770000行句子时系统卡死了,差点废了我的硬盘,我没有得到测试结果,可能是模型过大我的电脑承受不起的缘故吧。对于这个错误我会研究一下,不过如果读者们有类似的情况并知道为何如此,请务必让我知道!谢谢大家!


你可能感兴趣的:(RNN/LSTM,tensorflow,词向量与语言模型)