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