當前位置:
首頁 > 知識 > 從 Encoder到Decoder 實現 Seq2Seq 模型

從 Encoder到Decoder 實現 Seq2Seq 模型

本文作者天雨粟,本文首發於知乎專欄「機器不學習」,AI 研習社獲得授權轉載


前言

好久沒有更新專欄,今天我們來看一個簡單的Seq2Seq實現,我們將使用TensorFlow來實現一個基礎版本的Seq2Seq,主要幫助理解Seq2Seq中的基礎架構。

最基礎的Seq2Seq模型包含了三個部分,即Encoder、Decoder以及連接兩者的中間狀態向量,Encoder通過學習輸入,將其編碼成一個固定大小的狀態向量S,繼而將S傳給Decoder,Decoder再通過對狀態向量S的學習來進行輸出。

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

總結起來說,基礎的Seq2Seq主要包括Encoder,Decoder,以及連接兩者的固定大小的State Vector。

實戰代碼

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

版本信息:Python 3 / TensorFlow 1.1

1. 數據集

數據集包括source與target:

- source_data: 每一行是一個單詞

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

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

2. 數據預覽

我們先把source和target數據載入進來,可以看一下前10行,target的每一行是對source源數據中的單詞進行了排序。下面我們就將基於這些數據來訓練一個Seq2Seq模型,來幫助大家理解基礎架構。

3. 數據預處理

在神經網路中,對於文本的數據預處理無非是將文本轉化為模型可理解的數字,這裡都比較熟悉,不作過多解釋。但在這裡我們需要加入以下四種字元,

主要用來進行字元補全,和都是用在Decoder端的序列中,告訴解碼器句子的起始與結束,則用來替代一些未出現過的詞或者低頻詞。

: 補全字元。

: 解碼器端的句子結束標識符。

: 低頻詞或者一些未遇到過的詞等。

: 解碼器端的句子起始標識符。

通過上面步驟,我們可以得到轉換為數字後的源數據與目標數據。

4. 模型構建

Encoder

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

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

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


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序列是[, W, X, Y, Z, ],其中,W,X,Y,Z是每個時間序列上輸入給RNN的內容,我們發現,並沒有作為輸入傳遞給RNN。因此我們需要將target中的最後一個字元去掉,同時還需要在前面添加標識,告訴模型這代表一個句子的開始。

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

我們使用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 Epochsepochs = 60# Batch Sizebatch_size = 128# RNN Sizernn_size = 50# Number of Layersnum_layers = 2# Embedding Sizeencoding_embedding_size = 15decoding_embedding_size = 15# Learning Ratelearning_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", "

"]]

這樣就保證了我們每個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(https://github.com/NELSONZHAO/zhihu/)。

新人福利

關注 AI 研習社(okweiwu),回復1領取

【超過 1000G 神經網路 / AI / 大數據,教程,論文】

完全圖解RNN、RNN變體、Seq2Seq、Attention機制


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 AI研習社 的精彩文章:

如何用 Python 和深度神經網路發現即將流失的客戶?
Intel Movidius 挑戰賽:基於 Movidius 神經計算棒的 CNN 自動圖像標記

TAG:AI研習社 |