今天我们来解析下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行句子时系统卡死了,差点废了我的硬盘,我没有得到测试结果,可能是模型过大我的电脑承受不起的缘故吧。对于这个错误我会研究一下,不过如果读者们有类似的情况并知道为何如此,请务必让我知道!谢谢大家!