1. 程式人生 > >TensorFlow中Sequence-to-Sequence樣例程式碼詳解

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模型時,類似於有監督學習模型,最大化目標函式

θ=argmaxθn=Nt=1TnlogP(ynt|yn<
t
,xn)
  其中 p(yt|y1,..,yt1,c)=g(yt1,st,c)=1Zexp(wTtϕ(yt1,zt,ct)+bt)   其中 wt
稱作輸出投影, bt 稱作輸出偏置,標準化常數計算式為 Z=k:ykVexp(wTkϕ(yt1,zt,ct)+bk)   Dzmitry Bahdanau大牛考慮到fixed-length向量表示會限制encoder-decoder架構的效能,於是進行了改進,使得模型在輸出單一word時,能夠自動查詢到有貢獻的輸入sub-sequence,新的模型架構如下圖所示,


  這裡的編碼器為雙向RNN架構,定義條件概率 p(yi|y1,..,yi1,x)=g(yi1,si,ci) ,其中隱狀態 si 計算公式為 si=f(si1,yi1,ci) ,上下文向量計算公式為 ci=j=1Txαijhj   其中,權值引數 αij=exp(eij)Txk=1exp(eik)    eij=activation(si1,hj) 是一個“alignment model”,用於表徵輸入序列的第j個位置和輸出序列的第i個位置的匹配程度, hj 表示雙向RNN隱狀態的合併,即 hj=[hLj;hRj] ,根據RNN序列的特點, hj 中包含了更多的鄰域窗序列內的資訊,那麼顯然 αij 是對 eij 標準化後的形式, ci 的計算公式的幾何意義就是,對輸入序列中所有位置的資訊進行加權求和,從而達到了在輸出序列的任一time step,都能夠從輸入序列中動態獲取最為相關的子序列資訊的效果,在作者文章中,這種效果被稱作為”attention mechanism”。

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的狀態,也即上圖中 αij 的取值;
  輸出引數 :
   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’,……