TensorFlow中Sequence-to-Sequence樣例程式碼詳解
在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模型時,類似於有監督學習模型,最大化目標函式
這裡的編碼器為雙向RNN架構,定義條件概率
TensorFlow中seq2seq庫函式
盡算上述演算法看起來比較複雜,但TensorFlow已經把它們封裝成了可以直接呼叫的函式,官方教程已經對這些庫函式做了大體介紹,但我感覺講的還是不夠透徹,故在這裡重新敘述一下(還有一些其他的函式,但考慮到它們的介面引數都是相似的,就不做太多介紹了~)。
(1)outputs, states = basic_rnn_seq2seq(encoder_inputs, decoder_inputs, cell)
輸入引數 :
encoder_inputs: 它是一個二維tensor構成的列表物件,其中每一個二維tensor代表某一時刻的輸入,其尺寸為[batch_size x input_size],這裡的 batch_size具體指某一時刻輸入的單詞個數,input_size指encoder的長度;
decoder_inputs: 它是一個二維tensor構成的列表物件,其中每一個二維tensor代表某一時刻的輸入,其尺寸為[batch_size x output_size],這裡的 batch_size具體指某一時刻輸入的單詞個數,output_size指decoder的長度;
cell: 它是一個rnn_cell.RNNCell或者multi-layer-RNNCell物件,其中定義了cell函式和hidden units的個數;
輸出引數 :
outputs: 它是一個二維tensor構成的列表物件,其中每一個二維tensor代表某一時刻輸出,其尺寸為[batch_size x output_size],這裡的
batch_size具體指某一時刻輸入的單詞個數,output_size指decoder的長度;
state: 它是一個二維tensor,表示每一個decoder cell在最後的time-step的狀態,其尺寸為[batch_size x cell.state_size],這裡的
cell.state_size可以表示一個或者多個子cell的狀態,視輸入引數cell而定;
(2)outputs, states = embedding_attention_seq2seq(encoder_inputs, decoder_inputs, …)
輸入引數 :
encoder_inputs: 與上面的基本函式,它是一個一維tensor構成的列表物件,其中每一個一維tensor的尺寸為[batch_size],代表某一時刻的輸入;
decoder_inputs: 與encoder_inputs的解釋類似;
cell: 它是一個rnn_cell.RNNCell或者multi-layer-RNNCell物件,其中定義了cell函式和hidden units的個數;
num_encoder_symbols: 具體指輸入詞庫的大小,也即輸入單詞one-hot表示後的向量長度;
num_decoder_symbols: 具體指輸出詞庫的大小;
embedding_size: 詞庫中每一個單詞“巢狀”後向量的長度;
num_heads: 預設為1(具體的意義我還沒弄明白);
output_projection: 為None或者 (W, B) 元組物件,其中W的尺寸為[output_size x num_decoder_symbols],B的尺寸為 [num_decoder_symbols],
顯然,解碼器每一時刻的輸出僅共享偏置引數B,權值引數不共享;
feed_previous: 為True時用於模型測試階段,基於貪婪演算法生成輸出序列,為False時用於訓練模型引數;
initial_state_attention: 設定初始attention的狀態,也即上圖中
輸出引數 :
outputs: 它是一個二維tensor構成的列表物件,其中每一個二維tensor代表某一時刻輸出,其尺寸為[batch_size x num_decoder_symbols];
state: 它是一個二維tensor,表示每一個decoder cell在最後的time-step的狀態,其尺寸為[batch_size x cell.state_size],這裡的
cell.state_size可以表示一個或者多個子cell的狀態,視輸入引數cell而定;
sequence-to-sequence模型實現中的技巧
做理論和做工程還是有區別的,在對sequence-to-sequence模型進行實現時,Google的工程師們使用了sample softmax策略和bucketing策略,下面我們分別對其進行講解。
sample softmax策略
解碼器RNN序列在每一時刻的輸出層為softmax分類器,在對上面的目標函式求梯度時,表示式中會出現對整個target vocabulary的求和項,顯然這樣做的計算量是非常大的,於是大牛們想到了用target vocabulary中的一個子集,來近似對整個詞庫的求和,子集中word的選取採用的是均勻取樣的策略,從而降低了每次梯度更新步驟的計算複雜度,在tensorflow中可以採用tf.nn.sampled_softmax_loss函式。
bucketing策略
bucketing策略可以用於處理不同長度的訓練樣例,如果我們把訓練樣例的輸入和輸出長度固定,那麼在訓練整個網路的時候,必然會引入很多的PAD輔助單詞,而這些單詞卻包含了無用資訊;如果不引入PAD輔助單詞,每一個樣例作為一個graph的話,因為每一個樣例的輸入尺寸和輸出尺寸一般是不一樣的,所以每一個樣例定義出的graph也是不一樣的,因此就會定義出非常多的graph,儘管這些graph有相似的sub-graph,但是在訓練的時候不能夠進行平行計算,勢必會大大降低模型的訓練效率。所以,一個折中的方法就是,可以設定若干個buckets,每個bucket指定一個輸入和輸出長度,比如教程給的例子buckets = [(5, 10), (10, 15), (20, 25), (40, 50)],這樣的話,經過bucketing策略處理後,會把所有的訓練樣例分成4份,其中每一份的輸入序列和輸出序列的長度分別相同。為了更好地理解原始碼中bucketing的使用,我們這裡補充講述一下。TensorFlow是先定義出Graph,模型的訓練過程就是對Graph中引數進行更新。對於本例中的Graph而言,Graph中encoder部分的長度為40,decoder部分的長度為50,在每次採用梯度下降法更新模型引數時,會隨機地從4個buckets中選擇一個,並從中隨機選取batch個訓練樣例,此時相當於對當前Graph中的引數進行優化,但考慮到4個graph之間存在“weight share”,因此每個batch中樣例的長度不一樣也是可以的。
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’,……