學習 Tensorflow attention sequence to sequence model 並利用於機器翻譯的模型

Posted by Cyrus Chiu on 2017-02-24

Tensorflow 官方 Sequence-to -Sequence Models 學習

Tensoflow 這份教學是利用 seq2seq model 並加上 attention 機制[1-4],來實做英文對法文的機器翻譯。

操作方式很簡單,將官方提供的程式碼整份 clone 下來,cd 到 models/tutorials/rnn/translate 目錄下

python translate.py --data_dir [訓練資料位址] --train_dir [訓練記錄與模型存放位址]

機器就會自動開始下載資料集、前處理、並且開始訓練了。重複執行也沒有關係,程式會先檢查是否有已下載的資料集,已前處理完成的資料,甚至是已訓練好的模型,並且自動從當下的狀態開始繼續訓練。如果不帶參數,單純執行 python translate.py ,那麼會預設使用 /tmp/ 作為存放的目錄。

translate.py

顯然,translate.py 是這裡面的主程式,接下來來看看這隻程式做了什麼事情。

def main(_):
if FLAGS.self_test:
self_test()
elif FLAGS.decode:
decode()
else:
train()

這段程式碼顯示,如果不下其他指令,那麼會自動執行訓練,否則是執行測試,或是 decode,也就是預測法文的模式。

我們先將 train() 的主要功能拆成以下幾個區塊,並分開來討論。

with tf.Session() as sess:
# 區塊1,模型初始化
# Create model.
print("Creating %d layers of %d units." % (FLAGS.num_layers, FLAGS.size))
# 透過 create_model() 方法創建一個 seq2seq_model
model = create_model(sess, False)
# 區塊2,讀入資料
# Read data into buckets and compute their sizes.
print ("Reading development and training data (limit: %d)."
% FLAGS.max_train_data_size)
# read_data 函數讀取 train, dev 的路徑,
dev_set = read_data(from_dev, to_dev)
train_set = read_data(from_train, to_train, FLAGS.max_train_data_size)
train_bucket_sizes = [len(train_set[b]) for b in xrange(len(_buckets))]
train_total_size = float(sum(train_bucket_sizes))
train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size
for i in xrange(len(train_bucket_sizes))]
while True:
# 區塊3,建立 batch
# Choose a bucket according to data distribution. We pick a random number
# in [0, 1] and use the corresponding interval in train_buckets_scale.
random_number_01 = np.random.random_sample()
bucket_id = min([i for i in xrange(len(train_buckets_scale))
if train_buckets_scale[i] > random_number_01])
# Get a batch and make a step.
start_time = time.time()
encoder_inputs, decoder_inputs, target_weights = model.get_batch(
train_set, bucket_id)
# 區塊4,訓練
_, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
target_weights, bucket_id, False)

1.模型初始化

train() 首先會透過 create_model() 函數來建立 seq2seq model。該函數會呼叫seq2seq_model.py,並初始化一個 seq2seqModel class。

def __init__(self,
source_vocab_size, # 英文單詞表的數量
target_vocab_size, # 法文單詞表的數量
buckets, # buckets 於下面詳述
size, # 模型每個 layer 的 neuron size
num_layers,
max_gradient_norm, # 訓練 RNN 時 clip 梯度的值
batch_size,
learning_rate,
learning_rate_decay_factor,
use_lstm=False,
num_samples=512, # sampled softmax size
forward_only=False, # train時為False, decode時為true
dtype=tf.float32):

可以看到,這個 class 的輸入都是一些常見訓練 RNN 的參數。

bucket 預設是 [(5, 10), (10, 15), (20, 25), (40, 50)],舉例來說,對 (5,10) 這個 bucket 而言:

  • 英文輸入:I go .
  • 分詞:["I", "go", "."]
  • 編碼:[PAD PAD "." "go" "I"]

那麼我們會把輸入編碼到長度5,

  • 法文輸入:Je vais .
  • 分詞:["Je", "vais", "."]
  • 編碼:[GO "Je" "vais" "." EOS PAD PAD PAD PAD PAD]

並且把輸出編碼到長度 10。

Bucket 是工程上使用的一種方式。理論上 RNN 可以輸出任意長度的句子,但這樣勢必會因為每句話的長度不同,而產生許多無用的 graph。使用 Bucket 可以減少產生大量,並可能會有不少重複的 graph。若有一長度為 ( 6, 16 ) 的 (英文, 法文) 句子,那麼則會被分配到 (20, 25) 這個 bucket。並且英文會被 padding 至長度 20,法文會被 padding 至長度 25。

2.讀入資料

create_model() 之後,接下來是 read_data() 函數。

def read_data(source_path, target_path, max_size=None):
data_set = [[] for _ in _buckets]
# 讀入英文檔案
with tf.gfile.GFile(source_path, mode="r") as source_file:
# 讀入法文檔案
with tf.gfile.GFile(target_path, mode="r") as target_file:
# 每次讀入一行例如 ( '1 2 3 4 5\n', '99 98 97 96 95\n') 的(英,法)句對
source, target = source_file.readline(), target_file.readline()
counter = 0
# 逐行處理,去除 \n,並且 tokenize 化
while source and target and (not max_size or counter < max_size):
counter += 1
if counter % 100000 == 0:
print(" reading data line %d" % counter)
sys.stdout.flush()
source_ids = [int(x) for x in source.split()]
target_ids = [int(x) for x in target.split()]
target_ids.append(data_utils.EOS_ID)
# 這邊計算每句話的長度,並且分配到適合該長度的 bucket 之中
for bucket_id, (source_size, target_size) in enumerate(_buckets):
if len(source_ids) < source_size and len(target_ids) < target_size:
data_set[bucket_id].append([source_ids, target_ids])
break
source, target = source_file.readline(), target_file.readline()
return data_set

這個函數預設會讀入英/法文已被編碼為數字的檔案giga-fren.release2.fixed.en.ids40000giga-fren.release2.fixed.fr.ids40000 長相如下:

$ head -5 giga-fren.release2.fixed.en.ids40000
8874 25544 347 8874 1646 347 1202 75 2334 347 1060 3 1871 596 347 17249 347 7113 347 1641 347 2891 347 12106 347 3 902 347 1513 347 4892 7614 2002 7 33 596 1869
1368 3344
4892
12106
3899
$ head -5 giga-fren.release2.fixed.fr.ids40000
64 30 9546 294 231 349 64 30 9546 8 249 349 2114 864 349 48 551 5 3004 14 588 836 349 26391 349 26643 349 1278 349 4233 349 3 349 453 3 349 1163 349 3085 2488 8350 14 40
1089 14 261
9146
4951
2523

max_size=30會忽略長度超過30的句子,若設0或是None的話,會全部讀進來。逐行處理之後,回傳一個長度為 4 的 data_set。
為什麼長度為 4 呢?因為我們的 bucket 預設是長度 4 的 list:[(5, 10), (10, 15), (20, 25), (40, 50)]。因此 data_set 的長度也為 4。
其中 data_set[0],由於 bucket_size 是 (5,10) ,因此存放著例如 [[12106], [4951, 2]] 這樣長度為 (1,2) 的 tokenized 編碼結果。

3.取得 batch

random_number_01 = np.random.random_sample()
bucket_id = min([i for i in xrange(len(train_buckets_scale))
if train_buckets_scale[i] > random_number_01])
encoder_inputs, decoder_inputs, target_weights = model.get_batch(
train_set, bucket_id)

get_batch() 是定義在 seq2seq_model.py 之下的一個方法,需要的參數除了方法本身的

  • data:即 read_data() 回傳的 data_set。
  • bucket_id:因為對於不同的 bucket 有不同的 graph,假設我們有一個 minibatch ,若是跟這一批資料最接近的 bucket id 是 2,那我們在訓練的時候只要 minimize loss[2] 即可。

之外,還需要一個 class 本身的 property: batch_size

def get_batch(self, data, bucket_id):
# 根據傳進來的 bucket_id 決定這次的 encoder, deocder size,例如 5, 10
encoder_size, decoder_size = self.buckets[bucket_id]
encoder_inputs, decoder_inputs = [], []
# Get a random batch of encoder and decoder inputs from data,
# pad them if needed, reverse encoder inputs and add GO to decoder.
for _ in xrange(self.batch_size):
# 前面提過 data 是一個長度為4的list,data[i] 存放長度符合 bucket[i] 的資料
encoder_input, decoder_input = random.choice(data[bucket_id])
# Encoder inputs are padded and then reversed.
encoder_pad = [data_utils.PAD_ID] * (encoder_size - len(encoder_input))
encoder_inputs.append(list(reversed(encoder_input + encoder_pad)))
# Decoder inputs get an extra "GO" symbol, and are padded then.
decoder_pad_size = decoder_size - len(decoder_input) - 1
decoder_inputs.append([data_utils.GO_ID] + decoder_input +
[data_utils.PAD_ID] * decoder_pad_size)
# Now we create batch-major vectors from the data selected above.
batch_encoder_inputs, batch_decoder_inputs, batch_weights = [], [], []
# Batch encoder inputs are just re-indexed encoder_inputs.
for length_idx in xrange(encoder_size):
batch_encoder_inputs.append(
np.array([encoder_inputs[batch_idx][length_idx]
for batch_idx in xrange(self.batch_size)], dtype=np.int32))
# Batch decoder inputs are re-indexed decoder_inputs, we create weights.
for length_idx in xrange(decoder_size):
batch_decoder_inputs.append(
np.array([decoder_inputs[batch_idx][length_idx]
for batch_idx in xrange(self.batch_size)], dtype=np.int32))
# Create target_weights to be 0 for targets that are padding.
# 這個 weights 是給模型訓練用的,有目標值的地方為1,其他為0
# 有目標值的地方,指的是 decoder_input 平移1格的結果
batch_weight = np.ones(self.batch_size, dtype=np.float32)
for batch_idx in xrange(self.batch_size):
# We set weight to 0 if the corresponding target is a PAD symbol.
# The corresponding target is decoder_input shifted by 1 forward.
if length_idx < decoder_size - 1:
target = decoder_inputs[batch_idx][length_idx + 1]
if length_idx == decoder_size - 1 or target == data_utils.PAD_ID:
batch_weight[batch_idx] = 0.0
batch_weights.append(batch_weight)
return batch_encoder_inputs, batch_decoder_inputs, batch_weights

以上面這個例子來說,假設我們把 batch_size 設為 8,那麼回傳的結果中每個 array 的長度皆為 8,並且因為 bucket 是 (5, 10),所以 encoder_inputs 長度為 5,decoder_inputs 長度為 10。

下面的回傳結果可以理解成 batch_size=8,英文輸入序列:[ 0, 0, 0, 0, 12106],法文輸入序列:[1, 4951, 2, 0, 0, 0, 0, 0, 0, 0]。其中 0 代表 PAD_ID,1 代表 GO_ID,2 代表 EOS_ID。並且 weight:[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],代表 1, 4951 是有對應到輸出的。

# batch_encoder_inputs
[array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([12106, 12106, 12106, 12106, 12106, 12106, 12106, 12106], dtype=int32)]
# batch_decoder_inputs
[array([1, 1, 1, 1, 1, 1, 1, 1], dtype=int32),
array([4951, 4951, 4951, 4951, 4951, 4951, 4951, 4951], dtype=int32),
array([2, 2, 2, 2, 2, 2, 2, 2], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=int32)]
#batch_weights
[array([ 1., 1., 1., 1., 1., 1., 1., 1.], dtype=float32),
array([ 1., 1., 1., 1., 1., 1., 1., 1.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
array([ 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)]

4.訓練

上面得到了 get_batch() 回傳的三個值,之後就是進入訓練的本身了

_, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
target_weights, bucket_id, False)

step() 同樣是定義在 seq2seq_model.py 之下的一個方法,主要的功能是把輸入與輸出等建立 placeholder 與轉為 feed_dict,以進入訓練的主程序。

以下這段程式會建立一個 bucket 版本的 seq2seq 模型,需要輸入先前提及的 encoder_inputs、decoder_inputs、target_weights。另外的 targets 表示的是訓練目標,其實就是 decoder_input 的現在位置+1。具體的參數說明可以參考官方文件

self.outputs, self.losses = tf.contrib.legacy_seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets,
seq2seq=lambda x, y: seq2seq_f(x, y, False),
softmax_loss_function=softmax_loss_function)

上面這段程式碼中的 seq2seq 參數 seq2seq_f(x, y, False) 也是定義在 seq2seq_model.py 裡面的。指的是將 x: encoder_input 與 y: decoder_input 輸入,回傳的就是這個 seq2seq model 的 output 與 state。False 這個參數則是 seq2seq_f() 裡面自行定義作為 do_decode or not 的 Boolean。我們把相關的程式碼列出來如下,可以看到多為 tensorflow 之中對於 RNN 的設定。其中比較特別的是 sampled_softmax_loss 以及 seq2seq_f

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_t = tf.get_variable("proj_w", [self.target_vocab_size, size], dtype=dtype)
w = tf.transpose(w_t)
b = tf.get_variable("proj_b", [self.target_vocab_size], dtype=dtype)
output_projection = (w, b)
def sampled_loss(labels, inputs):
labels = tf.reshape(labels, [-1, 1])
# We need to compute the sampled_softmax_loss using 32bit floats to
# avoid numerical instabilities.
local_w_t = tf.cast(w_t, tf.float32)
local_b = tf.cast(b, tf.float32)
local_inputs = tf.cast(inputs, tf.float32)
return tf.cast(
tf.nn.sampled_softmax_loss(
weights=local_w_t,
biases=local_b,
labels=labels,
inputs=local_inputs,
num_sampled=num_samples,
num_classes=self.target_vocab_size),
dtype)
softmax_loss_function = sampled_loss
# Create the internal multi-layer cell for our RNN.
def single_cell():
return tf.contrib.rnn.GRUCell(size)
if use_lstm:
def single_cell():
return tf.contrib.rnn.BasicLSTMCell(size)
cell = single_cell()
if num_layers > 1:
cell = tf.contrib.rnn.MultiRNNCell([single_cell() for _ in range(num_layers)])
# The seq2seq function: we use embedding for the input and attention.
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
return tf.contrib.legacy_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,
dtype=dtype)

sampled_softmax_loss 是用在當有大量的輸出類別必須被 predict 的時候,舉例來說,像是英翻法這樣的翻譯工作,法文的詞典(target_vocab_size) size 有 40000 之多,這時候我們採用 sampled_softmax_loss 可以快速有效地建立一個 softmax classifier。其中的參數num_sampled指的是 sampling 的數目,在這邊是512。num_classes指的就是實際的 class 數目,在這邊就是以法文詞典的數目來代表。要注意的是 num_sampled不可以大於 num_classes 就是了。細節可參考官方文件說明

seq2seq_f() 直接呼叫了 tf.contrib.legacy_seq2seq.embedding_attention_seq2seq()。這個 embedding_attention_seq2seq 是一個帶有 embedding + sequence to sequence 並帶有 attention 機制的模型。encoder_input 首先進入一個 embedding layer,轉為 word vector,之後進入一個 encoder RNN。這個 encoder RNN 的每一個 time step 會被記錄下來,作為 attention 機制的參考。接下來,decoder_input 會進入另一個新建立的 embedding layer,在同樣轉為 word vector 之後,進入一個 attention deocder RNN。這個 deocder 是由 encoder 的最後一個 time step 的 state 進行初始化,其後每一個輸入就是 decoder_input 經過 embedding 之後的 word vector,並且具有對 encoder output 專注的 attention 機制。

tf.contrib.legacy_seq2seq.embedding_attention_seq2seq() 之中的參數 feed_previous,當他為 False 的時候 decoder 會使用前面給的 decoer_input 作為輸入,也就是一般在訓練階段的作法。當值為 True 的時候,前面給的 decoder_input 只有第一個值(通常是 GO symbol,代表一個句子的開始) 會作為 decoder 的輸入,而 decoder 的下一個 input,則是 decoder 的前一個 output,也就是只給 deocder 第一個 input,後面讓他自由發揮的意思。這也是一般在 decode/predict 時候的作法。官方文件

以上是這些程式碼比較核心的部分,後面大概就是利用tf.train.GradientDescentOptimizer進行訓練,clip gradient 並 print 一些 epoch、loss、perplexity 等資訊。

perplexity

最後面簡單介紹一下 perplexity,這個指標就如同 precision recall 等指標一樣,用來評估一個模型的好壞,只不過在語言模型裡面,我們使用 perplexity 這個指標。perplexity 越小,代表模型的校能越好。translate.py之中對於 perplexity 的定義是:

perplexity = math.exp(float(loss)) if loss < 300 else float("inf")

而數學定義:
$$perplexity=e^{-l}, l=\frac{1}{M}\sum_{i=1}^{m}\log\left(p(s_i) \right )$$
正是math.exp(float(loss))


[1] Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation
[2] Sequence to Sequence Learning with Neural Networks
[3] Neural Machine Translation by Jointly Learning to Align and Translate
[4] On Using Very Large Target Vocabulary for Neural Machine Translation