-
seq2seq介紹
1.1 簡單介紹
Seq2Seq技術,全稱Sequence to Sequence,該技術突破了傳統的固定大小輸入問題架構,開通了将經典深度神經網絡模型(DNNs)運用于在翻譯,文本自動摘要和機器人自動問答以及一些回歸預測任務上,并被證明在英語-法語翻譯、英語-德語翻譯以及人機短問快答的應用中有着不俗的表現。
1.2 模型的提出
提出:Seq2Seq被提出于2014年,最早由兩篇文章獨立地闡述了它主要思想,分别是Google Brain團隊的《Sequence to Sequence Learning with Neural Networks》和Yoshua Bengio團隊的《Learning Phrase Representation using RNN Encoder-Decoder for Statistical Machine Translation》。這兩篇文章針對機器翻譯的問題不謀而合地提出了相似的解決思路,Seq2Seq由此産生。
1.3 核心思想
Seq2Seq解決問題的主要思路是通過深度神經網絡模型(常用的是LSTM,長短記憶網絡,一種循環神經網絡)http://dataxujing.coding.me/深度學習之RNN/。将一個作為輸入的序列映射為一個作為輸出的序列,這一過程由編碼(Encoder)輸入與解碼(Decoder)輸出兩個環節組成, 前者負責把序列編碼成一個固定長度的向量,這個向量作為輸入傳給後者,輸出可變長度的向量。
圖1:Seq2Seq示意圖
由上圖所示,在這個模型中每一時間的輸入和輸出是不一樣的,比如對于序列資料就是将序列項依次傳入,每個序列項再對應不同的輸出。比如說我們現在有序列“A B C EOS” (其中EOS=End of Sentence,句末辨別符)作為輸入,那麼我們的目的就是将“A”,“B”,“C”,“EOS”依次傳入模型後,把其映射為序列“W X Y Z EOS”作為輸出。
1.4 模型應用
seq2seq其實可以用在很多地方,比如機器翻譯,自動對話機器人,文檔摘要自動生成,圖檔描述自動生成。比如Google就基于seq2seq開發了一個對話模型[5],和論文[1,2]的思路基本是一緻的,使用兩個LSTM的結構,LSTM1将輸入的對話編碼成一個固定長度的實數向量,LSTM2根據這個向量不停地預測後面的輸出(解碼)。隻是在對話模型中,使用的語料是((input)你說的話-我答的話(input))這種類型的pairs 。而在機器翻譯中使用的語料是(hello-你好)這樣的pairs。
此外,如果我們的輸入是圖檔,輸出是對圖檔的描述,用這樣的方式來訓練的話就能夠完成圖檔描述的任務。等等,等等。
可以看出來,seq2seq具有非常廣泛的應用場景,而且效果也是非常強大。同時,因為是端到端的模型(大部分的深度模型都是端到端的),它減少了很多人工處理和規則制定的步驟。在 Encoder-Decoder 的基礎上,人們又引入了attention mechanism等技術,使得這些深度方法在各個任務上表現更加突出。
1.5 Paper
首先介紹幾篇比較重要的 seq2seq 相關的論文:
[1] Cho et al., 2014 . Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation.
[2] Sutskever et al., 2014. Sequence to Sequence Learning with Neural Networks.
[3] Bahdanau et al., 2014. Neural Machine Translation by Jointly Learning to Align and Translate.
[4] Jean et. al., 2014. On Using Very Large Target Vocabulary for Neural Machine Translation.
[5] Vinyals et. al., 2015. A Neural Conversational Model. Computer Science.
2. Encoder-Decoder結構
2.1 經典的Encoder-Decoder結構
圖2:Encoder-Decoder示意圖
- Encoder意思是将輸入序列轉化成一個固定長度的向量
- Decoder意思是将輸入的固定長度向量解碼成輸出序列
- 其中編碼解碼的方式可以是RNN,CNN等
- 在機器翻譯:輸入(hello) -> 輸出 (你好)。輸入是1個英文單詞,輸出為2個漢字。 在對話機器中:我們提(輸入)一個問題,機器會自動生成(輸出)回答。這裡的輸入和輸出顯然是長度沒有确定的序列(sequences)。
- 要知道,在以往的很多模型中,我們一般都說輸入特征矩陣,每個樣本對應矩陣中的某一行。就是說,無論是第一個樣本還是最後一個樣本,他們都有一樣的特征次元。但是對于翻譯這種例子,難道我們要讓每一句話都有一樣的字數嗎,那樣的話估計五言律詩和七言絕句又能大火一把了,哈哈。但是這不科學呀,是以就有了 seq2seq 這種結構。
圖3:經典的Encoder-Decoder示意圖
上圖中,C是encoder輸出的最終狀态,向量C通常為RNN中的最後一個隐節點(h,Hidden state),或是多個隐節點的權重總和,作為decoder的初始狀态;W是encoder的最終輸出,作為decoder的初始輸入。
圖4:經典的Encoder-Decoder示意圖(LSTM or CNN)
上圖為seq2seq的encode和decode結構,采用CNN/LSTM模型。在RNN中,目前時間的隐藏狀态是由上一時間的狀态和目前時間的輸入x共同決定的,即
- 【編碼階段】
得到各個隐藏層的輸出然後彙總,生成語義向量
也可以将最後的一層隐藏層的輸出作為語義向量C
- 【解碼階段】
這個階段,我們要根據給定的語義向量C和輸出序列y1,y2,…yt1來預測下一個輸出的單詞yt,即
也可以寫做
其中g()代表的是非線性激活函數。在RNN中可寫成yt=g(yt1,ht,C),其中h為隐藏層的輸出。
2.2 Paper中的結構解析
- --->Cho et al., 2014 . Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation.
圖5:論文[1] 模型按時間展開的結構
算是比較早提出Encoder-Decoder這種結構的,其中 Encoder 部分應該是非常容易了解的,就是一個RNNCell(RNN ,GRU,LSTM 等) 結構。每個 timestep, 我們向 Encoder 中輸入一個字/詞(一般是表示這個字/詞的一個實數向量),直到我們輸入這個句子的最後一個字/詞 XT ,然後輸出整個句子的語義向量 c(一般情況下, c=hXT , XT 是最後一個輸入)。因為 RNN 的特點就是把前面每一步的輸入資訊都考慮進來了,是以理論上這個 c 就能夠把整個句子的資訊都包含了,我們可以把 c 當成這個句子的一個語義表示,也就是一個句向量。在 Decoder 中,我們根據 Encoder 得到的句向量 c, 一步一步地把蘊含在其中的資訊分析出來。
論文[1]中的公式表示如下:
ht=f(ht-1,yt−1,c)
同樣,根據 ht 我們就能夠求出 yt 的條件機率:
P(yt|yt−1,yt−2,...,y1,c)=g(ht,yt−1,c)
- 這裡有兩個函數 f 和 g , 一般來說, f 函數結構應該是一個 RNNCell 結構或者類似的結構(論文[1]原文中用的是 GRU);
- g 函數一般是 softmax (或者是論文 [4] 中提出的 sampled_softmax 函數)。
- 我們可以先這樣來了解:在 Encoder 中我們得到了一個涵蓋了整個句子資訊的實數向量 c ,現在我們一步一步的從 c 中抽取資訊。
- 首先給 Decoder 輸入一個啟動信号 y0(如特殊符号), 然後Decoder 根據 h0,y0,c ,就能夠計算出 y1 的機率分布了
- 同理,根據 h1,y1,c 可以計算y2 的機率分布…以此類推直到預測到結束的特殊标志 ,才結束預測。
論文[1]Cho et al. 中除了提出 Encoder-Decoder 這樣一個偉大的結構以外,還有一個非常大的貢獻就是首次提出了 Gated Recurrent Unit (GRU)這個使用頻率非常高的RNN結構。
注意到在論文[1]Cho et al. 的模型結構中(如 圖1 所示),中間語義 c 不僅僅隻作用于 decoder 的第 1 個時刻 ,而是每個時刻都有 c 輸入。是以,在這篇論文中, Decoder 預測第 t 個 timestep 的輸出時可以表示為:
p(yt)=f(ht,yt−1,c)
而在下面的論文[2] 中,Decoder 預測第 t 個 timestep 的輸出時可以表示為:
p(yt)=f(ht,yt−1)
- --->Sutskever et al., 2014. Sequence to Sequence Learning with Neural Networks.
圖6:論文[2] 模型結構
- 在論文[2] 中,Encoder 最後輸出的中間語義隻作用于 Decoder 的第一個時刻,這樣子模型了解起來其實要比論文[1] 更容易一些。
- Encoder-Decoder 其實是最簡單的
- 論文[2] seq2seq 模型結構(原文為 4 層 LSTM,這裡展示的是 1 層 LSTM)
- 圖中的 Encoder 和 Decoder 都隻展示了一層的普通的 LSTMCell。從上面的結構中,我們可以看到,整個模型結構還是非常簡單的。 EncoderCell 最後一個時刻的狀态 [cXT,hXT] 就是上面說的中間語義向量 c ,它将作為 DecoderCell 的初始狀态。然後在 DecoderCell 中,每個時刻的輸出将會作為下一個時刻的輸入。以此類推,直到 DecoderCell 某個時刻預測輸出特殊符号 結束。
- 論文 [2]Sutskever et al. 也是我們在看 seq2seq 資料是最經常提到的一篇文章, 在原論文中,上面的Encoder 和 Decoder 都是 4 層的 LSTM,但是原理其實和 1 層 LSTM 是一樣的。原文有個小技巧思想在上面的郵件對話模型結構沒展示出來,就是原文是應用在機器翻譯中的,作者将源句子順序颠倒後再輸入 Encoder 中,比如源句子為“A B C”,那麼輸入 Encoder 的順序為 “C B A”,經過這樣的處理後,取得了很大的提升,而且這樣的處理使得模型能夠很好地處理長句子。此外,Google 那篇介紹機器對話的文章(論文[5] )用的就是這個 seq2seq 模型。
- --->Bahdanau et al., 2014. Neural Machine Translation by Jointly Learning to Align and Translate.
圖7:論文[3] 模型結構
- 注意機制(Attention Mechanism),作為Seq2Seq中的重要組成部分,注意機制最早由Bahdanau等人于2014年提出,該機制存在的目的是為了解決RNN中隻支援固定長度輸入的瓶頸。在該機制環境下,Seq2Seq中的編碼器被替換為一個雙向循環網絡(bidirectional RNN)。
- 在Decoder進行預測的時候,Encoder 中每個時刻的隐藏狀态都被利用上了,這樣子,Encoder 就能利用多個語義資訊(隐藏狀态)來表達整個句子的資訊了。
- Encoder用的是雙向GRU,這個結構其實非常直覺,在這種 seq2seq 中效果也要比單向的 GRU 要好。
- ---->Jean et. al., 2014. On Using Very Large Target Vocabulary for Neural Machine Translation.
- 論文[4]介紹了機器翻譯在訓練時經常用到的一個方法(小技巧)sample_softmax ,主要解決詞表數量太大的問題。
- sampling softmax解決了softmax分母部分計算量大的問題,在詞向量中用的較多。
- 不是本節重點詳見[6]。
- --->Vinyals et. al., 2015. A Neural Conversational Model. Computer Science.
介紹了Google機器對話,用的模型就是[論文2]中的模型。
3. seq2seq模型Python實作
本節主要講解如何用tensorflow及keras實作seq2seq2模型,我們後期的聯信文本聊天機器人的主要訓練模型就采用seq2seq
3.1 tensorflow實作seq2seq
Tensorflow 1.0.0 版本以後,開發了新的seq2seq接口,棄用了原來的接口。舊的seq2seq接口是tf.contrib.legacy_seq2seq下,新的接口在tf.contrib.seq2seq下。
新seq2seq接口與舊的相比最主要的差別是它是動态展開的,而舊的是靜态展開的。
- 靜态展開(static unrolling) :指的是定義模型建立graph的時候,序列的長度是固定的,之後傳入的所有序列都得是定義時指定的長度。這樣所有的句子都要padding到指定的長度,很浪費存儲空間,計算效率也不高
- 動态展開(dynamic unrolling):使用控制流ops處理序列,可以不需要事先指定好序列長度
- 不管靜态還是動态,輸入的每一個batch内的序列長度都要一樣
in[4]: tf.__version__
Out[4]: '1.5.0'
_allowed_symbols = [
"sequence_loss",
"Decoder",
"dynamic_decode",
"BasicDecoder",
"BasicDecoderOutput",
"BeamSearchDecoder",
"BeamSearchDecoderOutput",
"BeamSearchDecoderState",
"Helper",
"CustomHelper",
"FinalBeamSearchDecoderOutput",
"gather_tree",
"GreedyEmbeddingHelper",
"SampleEmbeddingHelper",
"ScheduledEmbeddingTrainingHelper",
"ScheduledOutputTrainingHelper",
"TrainingHelper",
"BahdanauAttention",
"LuongAttention",
"hardmax",
"AttentionWrapperState",
"AttentionWrapper",
"AttentionMechanism",
"tile_batch"]
熟悉這些接口最好的方法就是閱讀API文檔,然後使用它們。
3.1.1 經典的seq2seq模型
圖7:論文[2] 模型結構
輸入的序列為['A', 'B', 'C', ''],輸出序列為['W', 'X', 'Y', 'Z', '']
這裡Encoder對輸入序列進行編碼,将最後一時刻輸出的hidden state(下文的final state)作為輸入序列的編碼向量。 Decoder将終止符作為初始輸入(也可以使用其他符号如等),Encoder的final state作為初始狀态,然後生成序列直到遇上終止符。
結構很簡單,隻要實作Encoder與Decoder再将他們串起來即可。
論文[2]中的Encoder使用的是一個4層的單向LSTM,這一部分使用RNN的接口即可,還不需要用到Seq2Seq中的接口。第一張圖中的模型架構雖然闡述清楚了Encoder-Decoder這種架構,但是具體實作上,不是直接将序列['A', 'B', 'C', '']輸入到Encoder中,Encoder的完整架構如下圖所示:
---------------------Encoder----------------
圖8:Encoder結構
- input:不是原始的序列,而是将序列中的每個元素都轉換為字典中對應的id。不管是train還是inference階段,為了效率都是一次輸入一個mini-batch,是以需要為input定義一個int型rank=2的placeholder。
- embedding:定義為trainable=True的變量,這樣即使使用pre-trained的詞向量也可以在訓練模型的過程中調優。
- MultiLayer_LSTM:接收的輸入是序列中每個元素對應的詞向量。
其中,tf.nn.dynamic_rnn方法接收encoder執行個體以及embbeded向量之後,就會輸出包含每個時刻hidden state的outputs以及final state,如果初始狀态為0的話,不需要顯式的聲明zero_state再将其作為參數傳入,隻需要指定state的dtype,這個方法中會将初始狀态自動初始化為0向量
------------------Decoder----------------------
圖9:Encoder結構
- input:與encoder的一樣,也是序列元素對應的id。
- embedding:視情況而定需不需要與encoder的embedding不同,比如在翻譯中,源語言與目智語言的詞向量空間就不一樣,但是像文本摘要這種都是基于一種語言的,encoder與decoder的embedding matrix是可以共用的。
- Dense_Layer:與encoder僅輸出hidden state不同,decoder需要輸出每個時刻詞典中各token的機率,是以還需要一個dense layer将hidden state向量轉換為次元等于vocabulary_size的向量,然後再将dense layer輸出的logits經過softmax層得到最終的token機率。
- Decoder的定義需要區分inference階段還是train階段。
- inference階段,decoder的輸出是未知的,對于生成['W', 'X', 'Y', 'Z', '']序列,是在decoder輸出token 'W'之後,再将'W'作為輸入,結合此時的hidden state,推斷出下一個token 'X',以此類推直到輸出為或達到最長序列長度之後終止。
- 而在train階段,decoder應該輸出的序列是已知的,不管最終output的結果是什麼,都将已知序列中的token依次輸入。train的階段如果也将輸出的結果再作為輸入,一旦前面的一步錯了,都會放大誤差,導緻訓練過程更不穩定。
- decoder将用到seq2seq中的TrainingHelper, GreedyEmbeddingHelper, BasicDecoder三個類,以及dynamic_decode方法,還将用到tensorflow.python.layers.core下的Dense類。
1.BasicDecoder
實作decoder最先關注到的就是BasicDecoder,它的構造函數與參數的定義如下:
__init__( cell, helper, initial_state, output_layer=None )
- cell: An RNNCell instance.
- helper: A Helper instance.
- initial_state: A (possibly nested tuple of…) tensors and TensorArrays. The initial state of the RNNCell.
- output_layer: (Optional) An instance of tf.layers.Layer, i.e., tf.layers.Dense. Optional layer to apply to the RNN output prior to storing the result or sampling.
- cell:在這裡就是一個多層LSTM的執行個體,與定義encoder時無異
- helper:這裡隻是簡單說明是一個Helper執行個體,第一次看文檔的時候肯定還不知道這個Helper是什麼,不用着急,看到具體的Helper執行個體就明白了
- initial_state:encoder的final state,類型要一緻,也就是說如果encoder的final state是tuple類型(如LSTM的包含了cell state與hidden state),那麼這裡的輸入也必須是tuple。直接将encoder的final_state作為這個參數輸入即可
- output_layer:對應的就是架構圖中的Dense_Layer,隻不過文檔裡寫tf.layers.Dense,但是tf.layers下隻有dense方法,Dense的執行個體還需要from tensorflow.python.layers.core import Dense。
BasicDecoder的作用就是定義一個封裝了decoder應該有的功能的執行個體,根據Helper執行個體的不同,這個decoder可以實作不同的功能,比如在train的階段,不把輸出重新作為輸入,而在inference階段,将輸出接到輸入。
2.TrainingHelper
構造函數與參數如下:
__init__( inputs, sequence_length, time_major=False, name=None )
- inputs: A (structure of) input tensors.
- sequence_length: An int32 vector tensor.
- time_major: Python bool. Whether the tensors in inputs are time major. If False (default), they are assumed to be batch major.
- name: Name scope for any created operations.
- inputs:對應Decoder架構圖中的embedded_input,time_major=False的時候,inputs的shape就是[batch_size, sequence_length, embedding_size] ,time_major=True時,inputs的shape為[sequence_length, batch_size, embedding_size]
- sequence_length:這個文檔寫的太簡略了,不過在源碼中可以看出指的是目前batch中每個序列的長度(self._batch_size = array_ops.size(sequence_length))。
- time_major:決定inputs Tensor前兩個dim表示的含義 name:如文檔所述
TrainingHelper用于train階段,next_inputs方法一樣也接收outputs與sample_ids,但是隻是從初始化時的inputs傳回下一時刻的輸入。
3.GreedyEmbeddingHelper
__init__( embedding, start_tokens, end_token )
- embedding: A callable that takes a vector tensor of ids (argmax ids), or the params argument for embedding_lookup. The returned tensor will be passed to the decoder input.
- start_tokens: int32 vector shaped [batch_size], the start tokens.
- end_token: int32 scalar, the token that marks end of decoding.
A helper for use during inference.
Uses the argmax of the output (treated as logits) and passes the result through an embedding layer to get the next input.
官方文檔已經說明,這是用于inference階段的helper,将output輸出後的logits使用argmax獲得id再經過embedding layer來擷取下一時刻的輸入。
- embedding:params argument for embedding_lookup,也就是 定義的embedding 變量傳入即可。
- start_tokens: batch中每個序列起始輸入的token_id
- end_token:序列終止的token_id
4.dynamic_decode
dynamic_decode( decoder, output_time_major=False, impute_finished=False, maximum_iterations=None, parallel_iterations=32, swap_memory=False, scope=None)
這個方法很直覺,将定義好的decoder執行個體傳入,其他幾個參數文檔介紹的很清楚。很值得學習的是其中如何使用control flow ops來實作dynamic的過程。
------------------代碼--------------------
綜合使用上述接口實作基本Encoder-Decoder模型的代碼如下
import tensorflow as tf
from tensorflow.contrib.seq2seq import *
from tensorflow.python.layers.core import Dense
classSeq2SeqModel(object):def__init__(self, rnn_size, layer_size, encoder_vocab_size,
decoder_vocab_size, embedding_dim, grad_clip, is_inference=False):# define inputs
self.input_x = tf.placeholder(tf.int32, shape=[None, None], name='input_ids')
# define embedding layerwith tf.variable_scope('embedding'):
encoder_embedding = tf.Variable(tf.truncated_normal(shape=[encoder_vocab_size, embedding_dim], stddev=0.1),
name='encoder_embedding')
decoder_embedding = tf.Variable(tf.truncated_normal(shape=[decoder_vocab_size, embedding_dim], stddev=0.1),
name='decoder_embedding')
# define encoderwith tf.variable_scope('encoder'):
encoder = self._get_simple_lstm(rnn_size, layer_size)
with tf.device('/cpu:0'):
input_x_embedded = tf.nn.embedding_lookup(encoder_embedding, self.input_x)
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(encoder, input_x_embedded, dtype=tf.float32)
# define helper for decoderif is_inference:
self.start_tokens = tf.placeholder(tf.int32, shape=[None], name='start_tokens')
self.end_token = tf.placeholder(tf.int32, name='end_token')
helper = GreedyEmbeddingHelper(decoder_embedding, self.start_tokens, self.end_token)
else:
self.target_ids = tf.placeholder(tf.int32, shape=[None, None], name='target_ids')
self.decoder_seq_length = tf.placeholder(tf.int32, shape=[None], name='batch_seq_length')
with tf.device('/cpu:0'):
target_embeddeds = tf.nn.embedding_lookup(decoder_embedding, self.target_ids)
helper = TrainingHelper(target_embeddeds, self.decoder_seq_length)
with tf.variable_scope('decoder'):
fc_layer = Dense(decoder_vocab_size)
decoder_cell = self._get_simple_lstm(rnn_size, layer_size)
decoder = BasicDecoder(decoder_cell, helper, encoder_state, fc_layer)
logits, final_state, final_sequence_lengths = dynamic_decode(decoder)
ifnot is_inference:
targets = tf.reshape(self.target_ids, [-1])
logits_flat = tf.reshape(logits.rnn_output, [-1, decoder_vocab_size])
print ('shape logits_flat:{}'.format(logits_flat.shape))
print ('shape logits:{}'.format(logits.rnn_output.shape))
self.cost = tf.losses.sparse_softmax_cross_entropy(targets, logits_flat)
# define train op
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars), grad_clip)
optimizer = tf.train.AdamOptimizer(1e-3)
self.train_op = optimizer.apply_gradients(zip(grads, tvars))
else:
self.prob = tf.nn.softmax(logits)
def_get_simple_lstm(self, rnn_size, layer_size):
lstm_layers = [tf.contrib.rnn.LSTMCell(rnn_size) for _ in xrange(layer_size)]
return tf.contrib.rnn.MultiRNNCell(lstm_layers)
---------------執行個體----------------
#随機序列生成器
def random_sequences(length_from, length_to, vocab_lower, vocab_upper, batch_size):
def random_length():
if length_from == length_to:
return length_from
return np.random.randint(length_from, length_to + 1)
while True:
yield [
np.random.randint(low=vocab_lower, high=vocab_upper, size=random_length()).tolist()
for _ in range(batch_size)
]
建構一個随機序列生成器友善後面生成序列,其中 length_from 和 length_to表示序列的長度範圍從多少到多少,vocab_lower 和 vocab_upper 表示生成的序列值的範圍從多少到多少,batch_size 即是批的數量。
#填充序列
def make_batch(inputs, max_sequence_length=None):
sequence_lengths = [len(seq) for seq in inputs]
batch_size = len(inputs)
if max_sequence_length is None:
max_sequence_length = max(sequence_lengths)
inputs_batch_major = np.zeros(shape=[batch_size, max_sequence_length], dtype=np.int32)
for i, seq in enumerate(inputs):
for j, element in enumerate(seq):
inputs_batch_major[i, j] = element
inputs_time_major = inputs_batch_major.swapaxes(0, 1)
return inputs_time_major, sequence_lengths
生成的随機序列的長度是不一樣的,需要對短的序列用來填充,而可設為0,取最長的序列作為每個序列的長度,不足的填充,然後再轉換成time major形式。
#建構圖
encoder_inputs = tf.placeholder(shape=(None, None), dtype=tf.int32, name='encoder_inputs')
ecoder_inputs = tf.placeholder(shape=(None, None), dtype=tf.int32, name='decoder_inputs')
decoder_targets = tf.placeholder(shape=(None, None), dtype=tf.int32, name='decoder_targets')
建立三個占位符,分别為encoder的輸入占位符、decoder的輸入占位符和decoder的target占位符。
embeddings = tf.Variable(tf.random_uniform([vocab_size, input_embedding_size], -1.0, 1.0), dtype=tf.float32)
encoder_inputs_embedded = tf.nn.embedding_lookup(embeddings, encoder_inputs)
decoder_inputs_embedded = tf.nn.embedding_lookup(embeddings, decoder_inputs)
将encoder和decoder的輸入做一個嵌入操作,對于大詞彙量這個能達到降維的效果,嵌入操作也是很常用的方式了。在seq2seq模型中,encoder和decoder都是共用一個嵌入層即可。嵌入層的向量形狀為[vocab_size, input_embedding_size],初始值從-1到1,後面訓練會自動調整。
encoder_cell = tf.contrib.rnn.LSTMCell(encoder_hidden_units)
encoder_outputs, encoder_final_state = tf.nn.dynamic_rnn(
encoder_cell, encoder_inputs_embedded,
dtype=tf.float32, time_major=True,
)
decoder_cell = tf.contrib.rnn.LSTMCell(decoder_hidden_units)
decoder_outputs, decoder_final_state = tf.nn.dynamic_rnn(
decoder_cell, decoder_inputs_embedded,
initial_state=encoder_final_state,
dtype=tf.float32, time_major=True, scope="plain_decoder",
)
建立encoder和decoder的LSTM神經網絡,encoder_hidden_units 為LSTM隐層數量,設定輸入格式為time major格式。這裡我們不關心encoder的循環神經網絡的輸出,我們要的是它的最終狀态encoder_final_state,将其作為decoder的循環神經網絡的初始狀态。
decoder_logits = tf.contrib.layers.linear(decoder_outputs, vocab_size)
decoder_prediction = tf.argmax(decoder_logits, 2)
stepwise_cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
labels=tf.one_hot(decoder_targets, depth=vocab_size, dtype=tf.float32),
logits=decoder_logits,
)
loss = tf.reduce_mean(stepwise_cross_entropy)
train_op = tf.train.AdamOptimizer().minimize(loss)
對于decoder的循環神經網絡的輸出,因為我們要一個分類結果,是以需要一個全連接配接神經網絡,輸出層神經元數量是詞彙的數量。輸出層最大值對應的神經元即為預測的類别。輸出層的激活函數用softmax,損失函數用交叉熵損失函數。
#建立會話
with tf.Session(graph=train_graph) as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(epochs):
batch = next(batches)
encoder_inputs_, _ = make_batch(batch)
decoder_targets_, _ = make_batch([(sequence) + [EOS] for sequence in batch])
decoder_inputs_, _ = make_batch([[EOS] + (sequence) for sequence in batch])
feed_dict = {encoder_inputs: encoder_inputs_, decoder_inputs: decoder_inputs_,
decoder_targets: decoder_targets_,
}
_, l = sess.run([train_op, loss], feed_dict)
loss_track.append(l)
if epoch == 0or epoch % 1000 == 0:
print('loss: {}'.format(sess.run(loss, feed_dict)))
predict_ = sess.run(decoder_prediction, feed_dict)
for i, (inp, pred) in enumerate(zip(feed_dict[encoder_inputs].T, predict_.T)):
print('input > {}'.format(inp))
print('predicted > {}'.format(pred))
if i >= 20:
break
建立會話開始執行,每次生成一批數量,用 make_batch 分别建立encoder輸入、decoder的target和decoder的輸入。其中target需要在後面加上[EOS],它表示句子的結尾,同時輸入也加上[EOS]表示編碼開始。每訓練1000詞輸出看看效果。
3.1.2 Attention Seq2Seq模型
下面我們梳理一下帶Attention的seq2seq的結構
-------------Bi-RNN Encoder-----------------------
----------------Attention-Decoder------------------
詳細的分析可以參見參考文獻【14】。
3.1.3 tf-seq2seq開源架構
2017年4月11日,Google的大腦研究團隊釋出了 tf-seq2seq這個開源的TensorFlow架構,它能夠輕易進行實驗而達到現有的效果,團隊制作了該架構的代碼庫和子產品等,能夠最好地支援其功能。去年,該團隊釋出了Google神經機器翻譯(GoogleNeural Machine Translation,GNMT),它是一個序列到序列sequence-to-sequence(“seq2seq”)的模型,目前用于Google翻譯系統中。雖然GNMT在翻譯品質上有長足的進步,但是它還是受限于訓練的架構無法對外部研究人員開放的短闆。
3.2 keras實作seq2seq
在官方的keras執行個體中有完整的seq2seq,可以參考參考文獻【15】。
4.參考文獻
[1] https://www.jianshu.com/p/124b777e0c55
[2] http://blog.csdn.net/Zsaang/article/details/71516253
[3] http://blog.csdn.net/u012223913/article/details/77487610#t0
[4] http://blog.csdn.net/jerr__y/article/details/53749693
[5] http://blog.csdn.net/malefactor/article/details/50550211
[6] http://blog.csdn.net/wangpeng138375/article/details/75151064
[7] https://google.github.io/seq2seq/
[8] https://github.com/DataXujing/seq2seq
[9] https://www.w3cschool.cn/tensorflow_python/tensorflow_python-i8jh28vt.html
[10] http://www.tensorfly.cn/
[11] http://blog.csdn.net/thriving_fcl/article/details/74165062
[12] http://blog.csdn.net/wangyangzhizhou/article/details/77977655
[13] https://www.bilibili.com/video/av12005043/
[14] http://blog.csdn.net/thriving_fcl/article/details/74853556
[15] https://github.com/keras-team/keras/blob/master/examples/lstm_seq2seq.py