NLP中Sequence-to-Sequence model程式碼詳解
在NLP領域,sequence to sequence模型有很多應用,比如機器翻譯、自動應答機器人等。在看懂了相關的論文後,我開始研讀TensorFlow提供的原始碼,剛開始看時感覺非常晦澀,現在基本都弄懂了,我在這裡主要介紹Sequence-to-Sequence Models用到的理論,然後對原始碼進行詳解。
sequence-to-sequence模型
在NLP中最為常見的模型是language model,它的研究物件是單一序列,而本文中的sequence to sequence模型同時研究兩個序列。經典的sequence-to-sequence模型由兩個RNN網路構成,一個被稱為“encoder”,另一個則稱為“decoder”,前者負責把variable-length序列編碼成fixed-length向量表示,後者負責把fixed_length向量表示解碼成variable-length輸出,它的基本網路結構如下,
其中每一個小圓圈代表一個cell,比如GRUcell、LSTMcell、multi-layer-GRUcell、multi-layer-GRUcell等。這裡比較直觀的解釋就是,encoder的最終隱狀態c包含了輸入序列的所有資訊,因此可以使用c進行解碼輸出。儘管“encoder”或者“decoder”內部存在權值共享,但encoder和decoder之間一般具有不同的一套引數。在訓練sequence-to-sequence模型時,類似於有監督學習模型,最大化目標函式。
Github原始碼解析
整個工程主要使用了四個原始檔,seq2seq.py檔案是一個用於建立sequence-to-sequence模型的庫,data_utils.py中包含了對原始資料進行預處理的一些操作,seq2seq_model.py用於定義machine translation模型,translate.py用於訓練和測試所定義的翻譯模型。因為原始碼較長,下面僅針對每個.py檔案,對理解起來可能有困難的程式碼塊進行解析。
seq2seq.py檔案
這個檔案中比較重要的兩個庫函式basic_rnn_seq2seq和embedding_attention_seq2seq已經在上一部分作了介紹,這裡主要介紹其它的幾個功能函式。
(1)sequence_loss_by_example(logits, targets, weights)
這個函式用於計算所有examples的加權交叉熵損失,logits引數是一個2D Tensor構成的列表物件,每一個2D Tensor的尺寸為[batch_size x num_decoder_symbols],函式的返回值是一個1D float型別的Tensor,尺寸為batch_size,其中的每一個元素代表當前輸入序列example的交叉熵。另外,還有一個與之類似的函式sequence_loss,它對sequence_loss_by_example函式返回的結果進行了一個tf.reduce_sum運算,因此返回的是一個標稱型float Tensor。
(2)model_with_buckets(encoder_inputs, decoder_inputs, targets, weights, buckets, seq2seq)
for j, bucket in enumerate(buckets):
with variable_scope.variable_scope(variable_scope.get_variable_scope(),
reuse=True if j > 0 else None):
# 函式seq2seq有兩個返回值,因為tf.nn.seq2seq.embedding_attention_seq2seq函式有兩個返回值
bucket_outputs, _ = seq2seq(encoder_inputs[:bucket[0]],
decoder_inputs[:bucket[1]])
outputs.append(bucket_outputs)
if per_example_loss:
losses.append(sequence_loss_by_example(
outputs[-1], targets[:bucket[1]], weights[:bucket[1]],
softmax_loss_function=softmax_loss_function))
else:
losses.append(sequence_loss(
outputs[-1], targets[:bucket[1]], weights[:bucket[1]],
softmax_loss_function=softmax_loss_function))
這個函式建立了一個支援bucketing策略的sequence-to-sequence模型,它仍然屬於Graph的定義階段。具體來說,這段程式定義了length(buckets)個graph,每個graph的輸入為總模型的輸入“佔位符”的一部分,但這些graphs共享模型引數,函式的返回值outputs和losses均為列表物件,尺寸為[length(buckets)],其中每一個元素為當前graph的bucket_outputs和bucket_loss。
data_utils.py檔案
(1)create_vocabulary(vocabulary_path, data_path, max_vocabulary_size)
這個函式用於根據輸入檔案建立詞庫,在這裡data_path引數表示輸入原始檔的路徑,vocabulary_path表示輸出檔案的路徑,vocabulary_path檔案中每一行代表一個單詞,且按照其在data_path中的出現頻數從大到小排列,比如第1行為r”_EOS”,第2行為r”_UNK”,第3行為r’I’,第4行為r”have”,第5行為r’dream’,……
(2)def data_to_token_ids(data_path, target_path, vocabulary_path)
這個函式用於把字串為元素的資料檔案轉換為以int索引為元素的檔案,在這裡data_path表示輸入源資料檔案的路徑,target_path表示輸出索引資料檔案的路徑,vocabulary_path表示詞庫檔案的路徑。整個函式把資料檔案中的每一行轉換為在詞庫檔案中的索引值,兩單詞的索引值之間用空格隔開,比如返回值檔案的第一行為’1 123 235’,第二行為‘3 1 234 554 879 355’,……
seq2seq_model.py檔案
機器學習模型的定義過程,一般包括輸入變數定義、輸入資訊的forward propagation和誤差資訊的backward propagation三個部分,這三個部分在這個程式檔案中都得到了很好的體現,下面我們結合程式碼分別進行介紹。
(1)輸入變數的定義
# Feeds for inputs.
self.encoder_inputs = []
self.decoder_inputs = []
self.target_weights = []
for i in xrange(buckets[-1][0]): # Last bucket is the biggest one.
self.encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in xrange(buckets[-1][1] + 1):
self.decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
self.target_weights.append(tf.placeholder(dtype, shape=[None],
name="weight{0}".format(i)))
# Our targets are decoder inputs shifted by one.
targets = [self.decoder_inputs[i + 1]
for i in xrange(len(self.decoder_inputs) - 1)]
與前面的幾個樣例不同,這裡輸入資料採用的是最常見的“佔位符”格式,以self.encoder_inputs為例,這個列表物件中的每一個元素表示一個佔位符,其名字分別為encoder0, encoder1,…,encoder39,encoder{i}的幾何意義是編碼器在時刻i的輸入。這裡需要注意的是,在訓練階段執行sess.run()函式時會再次用到這些變數名字。另外,跟language model類似,targets變數是decoder inputs平移一個單位的結果,讀者可以結合當前模型的損失函式進行理解。
(2)輸入資訊的forward propagation
# Training outputs and losses.
if forward_only:
# 返回每一個bucket子圖模型對應的output和loss
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, True),
softmax_loss_function=softmax_loss_function)
# If we use output projection, we need to project outputs for decoding.
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]
]
else:
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)
從程式碼中可以看到,輸入資訊的forward popagation分成了兩種情況,這是因為整個sequence to sequence模型在訓練階段和測試階段資訊的流向是不一樣的,這一點可以從seq2seqf函式的do_decode引數值體現出來,而do_decoder取值對應的就是tf.nn.seq2seq.embedding_attention_seq2seq函式中的feed_previous引數,forward_only為True也即feed_previous引數為True時進行模型測試,為False時進行模型訓練。這裡還應用到了一個很重要的函式tf.nn.seq2seq.model_with_buckets,我麼在seq2seq檔案中對其進行講解。
(3)誤差資訊的backward propagation
# 返回所有bucket子graph的梯度和SGD更新操作,這些子graph共享輸入佔位符變數encoder_inputs,區別在於,
# 對於每一個bucket子圖,其輸入為該子圖對應的長度。
params = tf.trainable_variables()
if not forward_only:
self.gradient_norms = []
self.updates = []
opt = tf.train.GradientDescentOptimizer(self.learning_rate)
for b in xrange(len(buckets)):
gradients = tf.gradients(self.losses[b], params)
clipped_gradients, norm = tf.clip_by_global_norm(gradients,
max_gradient_norm)
self.gradient_norms.append(norm)
self.updates.append(opt.apply_gradients(
zip(clipped_gradients, params), global_step=self.global_step))
這一段程式碼主要用於計算損失函式關於引數的梯度。因為只有訓練階段才需要計算梯度和引數更新,所以這裡有個if判斷語句。並且,由於當前定義除了length(buckets)個graph,故返回值self.updates是一個列表物件,尺寸為length(buckets),列表中第i個元素表示graph{i}的梯度更新操作。
# Input feed: encoder inputs, decoder inputs, target_weights, as provided.
input_feed = {}
for l in xrange(encoder_size):
input_feed[self.encoder_inputs[l].name] = encoder_inputs[l]
for l in xrange(decoder_size):
input_feed[self.decoder_inputs[l].name] = decoder_inputs[l]
input_feed[self.target_weights[l].name] = target_weights[l]
......
if not forward_only:
output_feed = [self.updates[bucket_id], # Update Op that does SGD.
self.gradient_norms[bucket_id], # Gradient norm.
self.losses[bucket_id]] # Loss for this batch.
else:
output_feed = [self.losses[bucket_id]] # Loss for this batch.
for l in xrange(decoder_size): # Output logits.
output_feed.append(self.outputs[bucket_id][l])
outputs = session.run(output_feed, input_feed)
模型已經定義完成了,這裡便開始進行模型訓練了。上面的兩個for迴圈用於為之前定義的輸入佔位符賦予具體的數值,這些具體的數值源自於get_batch函式的返回值。當session.run函式開始執行時,當前session會對第bucket_id個graph進行引數更新操作。