天天看點

【NLP】一文了解Seq2Seq

  1. 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)輸出兩個環節組成, 前者負責把序列編碼成一個固定長度的向量,這個向量作為輸入傳給後者,輸出可變長度的向量。

【NLP】一文了解Seq2Seq

圖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結構

【NLP】一文了解Seq2Seq

圖2:Encoder-Decoder示意圖

  • Encoder意思是将輸入序列轉化成一個固定長度的向量
  • Decoder意思是将輸入的固定長度向量解碼成輸出序列
  • 其中編碼解碼的方式可以是RNN,CNN等
  • 在機器翻譯:輸入(hello) -> 輸出 (你好)。輸入是1個英文單詞,輸出為2個漢字。 在對話機器中:我們提(輸入)一個問題,機器會自動生成(輸出)回答。這裡的輸入和輸出顯然是長度沒有确定的序列(sequences)。
  • 要知道,在以往的很多模型中,我們一般都說輸入特征矩陣,每個樣本對應矩陣中的某一行。就是說,無論是第一個樣本還是最後一個樣本,他們都有一樣的特征次元。但是對于翻譯這種例子,難道我們要讓每一句話都有一樣的字數嗎,那樣的話估計五言律詩和七言絕句又能大火一把了,哈哈。但是這不科學呀,是以就有了 seq2seq 這種結構。
【NLP】一文了解Seq2Seq

圖3:經典的Encoder-Decoder示意圖

上圖中,C是encoder輸出的最終狀态,向量C通常為RNN中的最後一個隐節點(h,Hidden state),或是多個隐節點的權重總和,作為decoder的初始狀态;W是encoder的最終輸出,作為decoder的初始輸入。

【NLP】一文了解Seq2Seq

圖4:經典的Encoder-Decoder示意圖(LSTM or CNN)

上圖為seq2seq的encode和decode結構,采用CNN/LSTM模型。在RNN中,目前時間的隐藏狀态是由上一時間的狀态和目前時間的輸入x共同決定的,即

【NLP】一文了解Seq2Seq
  • 【編碼階段】

得到各個隐藏層的輸出然後彙總,生成語義向量

【NLP】一文了解Seq2Seq

也可以将最後的一層隐藏層的輸出作為語義向量C

【NLP】一文了解Seq2Seq
  • 【解碼階段】

這個階段,我們要根據給定的語義向量C和輸出序列y1,y2,…yt1來預測下一個輸出的單詞yt,即

【NLP】一文了解Seq2Seq

也可以寫做

【NLP】一文了解Seq2Seq

其中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.
【NLP】一文了解Seq2Seq

圖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.
【NLP】一文了解Seq2Seq

圖6:論文[2] 模型結構

  • 在論文[2] 中,Encoder 最後輸出的中間語義隻作用于 Decoder 的第一個時刻,這樣子模型了解起來其實要比論文[1] 更容易一些。
  • Encoder-Decoder 其實是最簡單的
【NLP】一文了解Seq2Seq
  • 論文[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.
【NLP】一文了解Seq2Seq

圖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模型

【NLP】一文了解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----------------

【NLP】一文了解Seq2Seq

圖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----------------------

【NLP】一文了解Seq2Seq

圖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-----------------------

【NLP】一文了解Seq2Seq
【NLP】一文了解Seq2Seq

----------------Attention-Decoder------------------

【NLP】一文了解Seq2Seq
【NLP】一文了解Seq2Seq

詳細的分析可以參見參考文獻【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

繼續閱讀