从Encoder到Decoder实现Seq2Seq模型
從Encoder到Decoder實(shí)現(xiàn)Seq2Seq模型
天雨粟模型師傅 / 果粉?關(guān)注他300 人贊同了該文章更新:感謝@Gang He指出的代碼錯誤。get_batches函數(shù)中第15行與第19行,代碼已經(jīng)重新修改,GitHub已更新。
前言
好久沒有更新專欄,今天我們來看一個簡單的Seq2Seq實(shí)現(xiàn),我們將使用TensorFlow來實(shí)現(xiàn)一個基礎(chǔ)版本的Seq2Seq,主要幫助理解Seq2Seq中的基礎(chǔ)架構(gòu)。
最基礎(chǔ)的Seq2Seq模型包含了三個部分,即Encoder、Decoder以及連接兩者的中間狀態(tài)向量,Encoder通過學(xué)習(xí)輸入,將其編碼成一個固定大小的狀態(tài)向量S,繼而將S傳給Decoder,Decoder再通過對狀態(tài)向量S的學(xué)習(xí)來進(jìn)行輸出。
圖中每一個box代表了一個RNN單元,通常是LSTM或者GRU。其實(shí)基礎(chǔ)的Seq2Seq是有很多弊端的,首先Encoder將輸入編碼為固定大小狀態(tài)向量的過程實(shí)際上是一個信息“信息有損壓縮”的過程,如果信息量越大,那么這個轉(zhuǎn)化向量的過程對信息的損失就越大,同時,隨著sequence length的增加,意味著時間維度上的序列很長,RNN模型也會出現(xiàn)梯度彌散。最后,基礎(chǔ)的模型連接Encoder和Decoder模塊的組件僅僅是一個固定大小的狀態(tài)向量,這使得Decoder無法直接去關(guān)注到輸入信息的更多細(xì)節(jié)。由于基礎(chǔ)Seq2Seq的種種缺陷,隨后引入了Attention的概念以及Bi-directional encoder layer等,由于本篇文章主要是構(gòu)建一個基礎(chǔ)的Seq2Seq模型,對其他改進(jìn)tricks先不做介紹。
總結(jié)起來說,基礎(chǔ)的Seq2Seq主要包括Encoder,Decoder,以及連接兩者的固定大小的State Vector。
實(shí)戰(zhàn)代碼
下面我們就將利用TensorFlow來構(gòu)建一個基礎(chǔ)的Seq2Seq模型,通過向我們的模型輸入一個單詞(字母序列),例如hello,模型將按照字母順序排序輸出,即輸出ehllo。
版本信息:Python 3 / TensorFlow 1.1
1. 數(shù)據(jù)集
數(shù)據(jù)集包括source與target:
- source_data: 每一行是一個單詞
- target_data: 每一行是經(jīng)過字母排序后的“單詞”,它的每一行與source_data中每一行一一對應(yīng)
例如,source_data的第一行是hello,第二行是what,那么target_data中對應(yīng)的第一行是ehllo,第二行是ahtw。2. 數(shù)據(jù)預(yù)覽
我們先把source和target數(shù)據(jù)加載進(jìn)來,可以看一下前10行,target的每一行是對source源數(shù)據(jù)中的單詞進(jìn)行了排序。下面我們就將基于這些數(shù)據(jù)來訓(xùn)練一個Seq2Seq模型,來幫助大家理解基礎(chǔ)架構(gòu)。
3. 數(shù)據(jù)預(yù)處理
在神經(jīng)網(wǎng)絡(luò)中,對于文本的數(shù)據(jù)預(yù)處理無非是將文本轉(zhuǎn)化為模型可理解的數(shù)字,這里都比較熟悉,不作過多解釋。但在這里我們需要加入以下四種字符,<PAD>主要用來進(jìn)行字符補(bǔ)全,<EOS>和<GO>都是用在Decoder端的序列中,告訴解碼器句子的起始與結(jié)束,<UNK>則用來替代一些未出現(xiàn)過的詞或者低頻詞。
- < PAD>: 補(bǔ)全字符。
- < EOS>: 解碼器端的句子結(jié)束標(biāo)識符。
- < UNK>: 低頻詞或者一些未遇到過的詞等。
- < GO>: 解碼器端的句子起始標(biāo)識符。
通過上面步驟,我們可以得到轉(zhuǎn)換為數(shù)字后的源數(shù)據(jù)與目標(biāo)數(shù)據(jù)。
4. 模型構(gòu)建
Encoder
模型構(gòu)建主要包括Encoder層與Decoder層。在Encoder層,我們首先需要對定義輸入的tensor,同時要對字母進(jìn)行Embedding,再輸入到RNN層。
在這里,我們使用TensorFlow中的tf.contrib.layers.embed_sequence來對輸入進(jìn)行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中的每個數(shù)字都被embed成了一個10維向量。
官方關(guān)于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數(shù)據(jù)進(jìn)行處理
- 構(gòu)造Decoder
- Embedding
- 構(gòu)造Decoder層
- 構(gòu)造輸出層,輸出層會告訴我們每個時間序列的RNN輸出結(jié)果
- Training Decoder
- Predicting Decoder
下面我們會對這每個部分進(jìn)行一一介紹。
1. target數(shù)據(jù)處理
我們的target數(shù)據(jù)有兩個作用:
- 在訓(xùn)練過程中,我們需要將我們的target序列作為輸入傳給Decoder端RNN的每個階段,而不是使用前一階段預(yù)測輸出,這樣會使得模型更加準(zhǔn)確。(這就是為什么我們會構(gòu)建Training和Predicting兩個Decoder的原因,下面還會有對這部分的解釋)。
- 需要用target數(shù)據(jù)來計(jì)算模型的loss。
我們首先需要對target端的數(shù)據(jù)進(jìn)行一步預(yù)處理。在我們將target中的序列作為輸入給Decoder端的RNN時,序列中的最后一個字母(或單詞)其實(shí)是沒有用的。我們來用下圖解釋:
我們此時只看右邊的Decoder端,可以看到我們的target序列是[<go>, W, X, Y, Z, <eos>],其中<go>,W,X,Y,Z是每個時間序列上輸入給RNN的內(nèi)容,我們發(fā)現(xiàn),<eos>并沒有作為輸入傳遞給RNN。因此我們需要將target中的最后一個字符去掉,同時還需要在前面添加<go>標(biāo)識,告訴模型這代表一個句子的開始。
如上圖,所示,紅色和橙色為我們最終的保留區(qū)域,灰色是序列中的最后一個字符,我們把它刪掉即可。
我們使用tf.strided_slice()來進(jìn)行這一步處理。
2. 構(gòu)造Decoder
- 對target數(shù)據(jù)進(jìn)行embedding。
- 構(gòu)造Decoder端的RNN單元。
- 構(gòu)造輸出層,從而得到每個時間序列上的預(yù)測結(jié)果。
- 構(gòu)造training decoder。
- 構(gòu)造predicting decoder。
注意,我們這里將decoder分為了training和predicting,這兩個encoder實(shí)際上是共享參數(shù)的,也就是通過training decoder學(xué)得的參數(shù),predicting會拿來進(jìn)行預(yù)測。那么為什么我們要分兩個呢,這里主要考慮模型的robust。
在training階段,為了能夠讓模型更加準(zhǔn)確,我們并不會把t-1的預(yù)測輸出作為t階段的輸入,而是直接使用target data中序列的元素輸入到Encoder中。而在predict階段,我們沒有target data,有的只是t-1階段的輸出和隱層狀態(tài)。
上面的圖中代表的是training過程。在training過程中,我們并不會把每個階段的預(yù)測輸出作為下一階段的輸入,下一階段的輸入我們會直接使用target data,這樣能夠保證模型更加準(zhǔn)確。
這個圖代表我們的predict階段,在這個階段,我們沒有target data,這個時候前一階段的預(yù)測結(jié)果就會作為下一階段的輸入。
當(dāng)然,predicting雖然與training是分開的,但他們是會共享參數(shù)的,training訓(xùn)練好的參數(shù)會供predicting使用。
decoder層的代碼如下:
構(gòu)建好了Encoder層與Decoder以后,我們需要將它們連接起來build我們的Seq2Seq模型。
定義超參數(shù)
# 超參數(shù) # 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
目前為止我們已經(jīng)完成了整個模型的構(gòu)建,但還沒有構(gòu)造batch函數(shù),batch函數(shù)用來每次獲取一個batch的訓(xùn)練樣本對模型進(jìn)行訓(xùn)練。
在這里,我們還需要定義另一個函數(shù)對batch中的序列進(jìn)行補(bǔ)全操作。這是啥意思呢?我們來看個例子,假如我們定義了batch=2,里面的序列分別是
[['h', 'e', 'l', 'l', 'o'],['w', 'h', 'a', 't']]那么這兩個序列的長度一個是5,一個是4,變長的序列對于RNN來說是沒辦法訓(xùn)練的,所以我們這個時候要對短序列進(jìn)行補(bǔ)全,補(bǔ)全以后,兩個序列會變成下面的樣子:
[['h', 'e', 'l', 'l', 'o'],['w', 'h', 'a', 't', '<PAD>']]這樣就保證了我們每個batch中的序列長度是固定的。
感謝@Gang He提出的錯誤。此處代碼已修正。修改部分為get_batches中的兩個for循環(huán),for target in targets_batch和for source in sources_batch(之前的代碼是for target in pad_targets_batch和for source in pad_sources_batch),因?yàn)槲覀冇胹equence_mask計(jì)算了每個句子的權(quán)重,該權(quán)重作為參數(shù)傳入loss函數(shù),主要用來忽略句子中pad部分的loss。如果是對pad以后的句子進(jìn)行l(wèi)oop,那么輸出權(quán)重都是1,不符合我們的要求。在這里做出修正。GitHub上代碼也已修改。至此,我們完成了整個模型的構(gòu)建與數(shù)據(jù)的處理。接下來我們對模型進(jìn)行訓(xùn)練,我定義了batch_size=128,epochs=60。訓(xùn)練loss如下:
模型預(yù)測
我們通過實(shí)際的例子來進(jìn)行驗(yàn)證。
輸入“hello”:
輸入“machine”:
輸入“common”:
總結(jié)
至此,我們實(shí)現(xiàn)了一個基本的序列到序列模型,Encoder通過對輸入序列的學(xué)習(xí),將學(xué)習(xí)到的信息轉(zhuǎn)化為一個狀態(tài)向量傳遞給Decoder,Decoder再基于這個輸入得到輸出。除此之外,我們還知道要對batch中的單詞進(jìn)行補(bǔ)全保證一個batch內(nèi)的樣本具有相同的序列長度。
我們可以看到最終模型的訓(xùn)練loss相對已經(jīng)比較低了,并且從例子看,其對短序列的輸出還是比較準(zhǔn)確的,但一旦我們的輸入序列過長,比如15甚至20個字母的單詞,其Decoder端的輸出就非常的差。
完整代碼已上傳至GitHub。
轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)。編輯于 2018-03-19「真誠贊賞,手留余香」贊賞4 人已贊賞
深度學(xué)習(xí)(Deep Learning)文本處理神經(jīng)網(wǎng)絡(luò)?贊同 300??149 條評論?分享?收藏?總結(jié)
以上是生活随笔為你收集整理的从Encoder到Decoder实现Seq2Seq模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 腾讯2013实习生笔试题+答案1-5aa
- 下一篇: 阿里-2019算法岗笔试编程题-kmp匹