[轉]從Encoder到Decoder實現Seq2Seq模型

前言python

最基礎的seq2seq模型包含了三個部分,即encoder、decoder以及鏈接二者的中間狀態向量,encoder經過學習輸入,將其編碼成一個固定大小的狀態向量s,繼而將s傳給decoder,decoder再經過對狀態向量s的學習來進行輸出。git

圖中每一個box表明一個rnn單元,一般是lstm或者gru。其實基礎的seq2seq是有不少弊端的,首先encoder將輸入編碼爲固定大小狀態向量的過程其實是一個信息「信息有損壓縮」的過程,若是信息量越大,那麼這個轉化向量的過程對信息的損失就越大,同時,隨着sequence length的增長,意味着時間維度上的序列很長,RNN模型也會出現梯度彌散。最後,基礎的模型鏈接Encoder和Decoder模塊的組件僅僅是一個固定大小的狀態向量,這使得Decoder沒法直接去關注到輸入信息的更多細節。因爲基礎Seq2Seq的種種缺陷,隨後引入了Attention的概念以及Bi-directional encoder layer等,因爲本篇文章主要是構建一個基礎的Seq2Seq模型,對其餘改進tricks先不作介紹。github

總結起來講,基礎的Seq2Seq主要包括Encoder,Decoder,以及鏈接二者的固定大小的State Vector。api

1. 數據集網絡

數據集包括source與target:架構

- source_data: 每一行是一個單詞ide

- target_data: 每一行是通過字母排序後的「單詞」,它的每一行與source_data中每一行一一對應函數

例如,source_data的第一行是hello,第二行是what,那麼target_data中對應的第一行是ehllo,第二行是ahtw。

2. 數據預覽oop

咱們先把source和target數據加載進來,能夠看一下前10行,target的每一行是對source源數據中的單詞進行了排序。下面咱們就將基於這些數據來訓練一個Seq2Seq模型,來幫助你們理解基礎架構。學習

3. 數據預處理

在神經網絡中,對於文本的數據預處理無非是將文本轉化爲模型可理解的數字,這裏都比較熟悉,不做過多解釋。但在這裏咱們須要加入如下四種字符,<PAD>主要用來進行字符補全,<EOS>和<GO>都是用在Decoder端的序列中,告訴解碼器句子的起始與結束,<UNK>則用來替代一些未出現過的詞或者低頻詞。

  • < PAD>: 補全字符。
  • < EOS>: 解碼器端的句子結束標識符。
  • < UNK>: 低頻詞或者一些未遇到過的詞等。
  • < GO>: 解碼器端的句子起始標識符。

 

 

經過上面步驟,咱們能夠獲得轉換爲數字後的源數據與目標數據。

 

4. 模型構建

Encoder

模型構建主要包括Encoder層與Decoder層。在Encoder層,咱們首先須要對定義輸入的tensor,同時要對字母進行Embedding,再輸入到RNN層。

在這裏,咱們使用TensorFlow中的tf.contrib.layers.embed_sequence來對輸入進行embedding。

咱們來看一個栗子,假如咱們有一個batch=2,sequence_length=5的樣本,features = [[1,2,3,4,5],[6,7,8,9,10]],使用

tf.contrib.layers.embed_sequence(features,vocab_size=n_words, embed_dim=10)

那麼咱們會獲得一個2 x 5 x 10的輸出,其中features中的每一個數字都被embed成了一個10維向量。

官方關於tf.contrib.layers.embed_sequence()的解釋以下:
Maps a sequence of symbols to a sequence of embeddings.
Typical use case would be reusing embeddings between an encoder and decoder.

 

 

Decoder

在Decoder端,咱們主要要完成如下幾件事情:

  • 對target數據進行處理
  • 構造Decoder
    • Embedding
    • 構造Decoder層
    • 構造輸出層,輸出層會告訴咱們每一個時間序列的RNN輸出結果
    • Training Decoder
    • Predicting Decoder

下面咱們會對這每一個部分進行一一介紹。

 

1. target數據處理

咱們的target數據有兩個做用:

  • 在訓練過程當中,咱們須要將咱們的target序列做爲輸入傳給Decoder端RNN的每一個階段,而不是使用前一階段預測輸出,這樣會使得模型更加準確。(這就是爲何咱們會構建Training和Predicting兩個Decoder的緣由,下面還會有對這部分的解釋)。
  • 須要用target數據來計算模型的loss。

 

咱們首先須要對target端的數據進行一步預處理。在咱們將target中的序列做爲輸入給Decoder端的RNN時,序列中的最後一個字母(或單詞)實際上是沒有用的。咱們來用下圖解釋:

 

 

 

咱們此時只看右邊的Decoder端,能夠看到咱們的target序列是[<go>, W, X, Y, Z, <eos>],其中<go>,W,X,Y,Z是每一個時間序列上輸入給RNN的內容,咱們發現,<eos>並無做爲輸入傳遞給RNN。所以咱們須要將target中的最後一個字符去掉,同時還須要在前面添加<go>標識,告訴模型這表明一個句子的開始。

 

 

 

如上圖,所示,紅色和橙色爲咱們最終的保留區域,灰色是序列中的最後一個字符,咱們把它刪掉便可。

咱們使用tf.strided_slice()來進行這一步處理。

 

其中tf.fill(dims, value)參數會生成一個dims形狀並用value填充的tensor。舉個栗子:tf.fill([2,2], 7) => [[7,7], [7,7]]。tf.concat()會按照某個維度將兩個tensor拼接起來。

2. 構造Decoder

  • 對target數據進行embedding。
  • 構造Decoder端的RNN單元。
  • 構造輸出層,從而獲得每一個時間序列上的預測結果。
  • 構造training decoder。
  • 構造predicting decoder。

注意,咱們這裏將decoder分爲了training和predicting,這兩個encoder其實是共享參數的,也就是經過training decoder學得的參數,predicting會拿來進行預測。那麼爲何咱們要分兩個呢,這裏主要考慮模型的robust。

在training階段,爲了可以讓模型更加準確,咱們並不會把t-1的預測輸出做爲t階段的輸入,而是直接使用target data中序列的元素輸入到Encoder中。而在predict階段,咱們沒有target data,有的只是t-1階段的輸出和隱層狀態。

 

 

上面的圖中表明的是training過程。在training過程當中,咱們並不會把每一個階段的預測輸出做爲下一階段的輸入,下一階段的輸入咱們會直接使用target data,這樣可以保證模型更加準確。

 

 

這個圖表明咱們的predict階段,在這個階段,咱們沒有target data,這個時候前一階段的預測結果就會做爲下一階段的輸入。

固然,predicting雖然與training是分開的,但他們是會共享參數的,training訓練好的參數會供predicting使用。

decoder層的代碼以下:

 

 

構建好了Encoder層與Decoder之後,咱們須要將它們鏈接起來build咱們的Seq2Seq模型。

 

 

定義超參數

# 超參數
# Number of Epochs
epochs = 60
# Batch Size
batch_size = 128
# RNN Size
rnn_size = 50
# Number of Layers
num_layers = 2
# Embedding Size
encoding_embedding_size = 15
decoding_embedding_size = 15
# Learning Rate
learning_rate = 0.001

定義loss function、optimizer以及gradient clipping

目前爲止咱們已經完成了整個模型的構建,但尚未構造batch函數,batch函數用來每次獲取一個batch的訓練樣本對模型進行訓練。

在這裏,咱們還須要定義另外一個函數對batch中的序列進行補全操做。這是啥意思呢?咱們來看個例子,假如咱們定義了batch=2,裏面的序列分別是

[['h', 'e', 'l', 'l', 'o'],
 ['w', 'h', 'a', 't']]

那麼這兩個序列的長度一個是5,一個是4,變長的序列對於RNN來講是沒辦法訓練的,因此咱們這個時候要對短序列進行補全,補全之後,兩個序列會變成下面的樣子:

[['h', 'e', 'l', 'l', 'o'],
 ['w', 'h', 'a', 't', '<PAD>']]

這樣就保證了咱們每一個batch中的序列長度是固定的。

 

感謝@Gang He提出的錯誤。此處代碼已修正。修改部分爲get_batches中的兩個for循環,for target in targets_batch和for source in sources_batch(以前的代碼是for target in pad_targets_batch和for source in pad_sources_batch),由於咱們用sequence_mask計算了每一個句子的權重,該權重做爲參數傳入loss函數,主要用來忽略句子中pad部分的loss。若是是對pad之後的句子進行loop,那麼輸出權重都是1,不符合咱們的要求。在這裏作出修正。GitHub上代碼也已修改。

至此,咱們完成了整個模型的構建與數據的處理。接下來咱們對模型進行訓練,我定義了batch_size=128,epochs=60。訓練loss以下:

 

模型預測

咱們經過實際的例子來進行驗證。

輸入「hello」:

 

 

輸入「machine」:

 

輸入「common」:

 

 

總結

至此,咱們實現了一個基本的序列到序列模型,Encoder經過對輸入序列的學習,將學習到的信息轉化爲一個狀態向量傳遞給Decoder,Decoder再基於這個輸入獲得輸出。除此以外,咱們還知道要對batch中的單詞進行補全保證一個batch內的樣本具備相同的序列長度。

咱們能夠看到最終模型的訓練loss相對已經比較低了,而且從例子看,其對短序列的輸出仍是比較準確的,但一旦咱們的輸入序列過長,好比15甚至20個字母的單詞,其Decoder端的輸出就很是的差。

完整代碼已上傳至GitHub

實戰代碼

下面咱們就將利用TensorFlow來構建一個基礎的Seq2Seq模型,經過向咱們的模型輸入一個單詞(字母序列),例如hello,模型將按照字母順序排序輸出,即輸出ehllo。

相關文章
相關標籤/搜索