Seq2Seq那些事

1前言

本篇博客主要記錄的是使用Tensorflow搭建Seq2Seq模型,主要包括3個部分的敘述:第一,Seq2Seq模型的訓練過程及原理。第二,復現基於SouGouS新聞語料庫的文本摘要的應用。第三,Seq2Seq模型中存在的問題及相應的Trick。
本篇博客參考多篇博客完成,主要是作爲自己的學習筆記使用,但最終還是摻雜自己的理解和自己的親身實現過程。後面會給出參考博客的鏈接。

2淺談Seq2Seq

2.1Seq2Seq概要

一般來說,Seq2Seq模型主要是用來解決將一個序列X轉化爲另一個序列Y的一類問題,此處有點類似於隱馬爾科夫模型,通過一系列隨機變量X,去預測另外一系列隨機變量Y。但是不同的是,隱馬爾科夫模型中的隨機序列與隨機變量系列一一對應而Seq2Seq模型則並不是指一一對應的關係。Seq2Seq模型主要的應用包括機器翻譯,自動摘要等一些端到端的生成應用。

目前來說,對於Seq2Seq生成模型來說,主要的思路是將該問題作爲條件語言模型,在已知輸入序列和前序生成序列的條件下,最大化下一目標詞的概率,而最終希望得到的是整個輸出序列的生成出現的概率最大:
圖1
說明:1.其中T表示輸出序列的時間序列大小,y1:t-1表示輸出序列的前t-1個時間點對應的輸出,X爲輸入序列。通常情況下,訓練模型的時候y1:t-1使用的是ground truth tokens,然而在測試過程中,ground truth tokens便不可知,需要使用前期預測到的y‘1:t-1來表示,這將會引發問題7 Exposure Bias,相應的解決的trick會在第4部分提出。
2.在預測輸出序列的每個token時,採用的都是最大化下一目標詞(token)的概率,來得到token,對於整個句子或者說序列來說,這種解法是貪心策略,帶來的是局部最佳。對於一個端到端的生成應用來說,將會追求整個序列的最佳,換句話說,希望最後的生成序列的tokens順序排列的聯合概率最大,找到一個全局最優。

2.2Seq2Seq模型推導

Seq2Seq模型是基於輸入序列,預測未知輸出序列的模型。它有兩個部分組成,對輸入序列的Encoder編碼階段和生成輸出序列的Decoder解碼階段。定義輸入序列[x1,x2,…,xm],由m個固定長度爲d的向量構成;輸出序列爲[y1,y2,…,yn],由n個固定長度爲d的向量構成;
圖2
上圖中可以看出,Encoder使用RNN編碼後形成語義向量C.再將C作爲輸出序列模型Decoder的輸入。解碼過程中每一個時間點t的輸入是上一個時刻隱層狀態ht-1和中間語義向量C和上一個時刻的預測輸出yt-1.之後將每個時刻的yt相乘得到整個序列出現的概率。其中f是非線性的激活函數。
圖4
最後Seq2Seq兩個部分(Encoder和Decoder)聯合訓練的目標函數是最大化條件似然函數。其中θ爲模型的參數,N爲訓練集的樣本個數。
圖5
下圖爲網上某篇博客上的圖,展示的是一個機器翻譯的多層Seq2Seq的模型。
圖2

2.3Seq2Seq模型上的Attention注意力機制

儘管Seq2Seq中的Encoder可以將RNN替換成LSTM來增強最終語義向量C對長的輸入序列的信息表上,但是由於傳統的Seq2Seq模型對輸入序列進行編碼輸出的語義向量C是固定的,一個向量並不能很好的編碼出輸入序列所有包含的信息,而解碼階段則受限於該固定長度的向量表示。因此,此篇論文中Neural Machine Translation by Jointly Learning to Align and Translate引入Attention機制。

論文中提出,將Encoder中的每一個時刻的隱藏狀態都保存至一個列表中[h1,h2,…,hm],在Decoder解碼每一個時刻i的輸出時,都需要計算Encoder的每個時刻的隱藏狀態hi與Decoder的輸出時刻的前一個時刻的關係si-1的關係,進而得到Encoder的每個時刻的隱藏狀態對Decoder該時刻的影響程度。如此,Decoder的每個時刻的輸出都將獲得不同的Encoder的序列隱藏狀態對它的影響,從而得到不同的語義向量Ci
圖2
如上圖,Decoder階段的每個時刻的隱藏狀態si,都會根據由Encoder階段的隱藏狀態序列對Decoder階段上一個時刻(i-1)的隱藏狀態的影響也就是我們的語義向量Ci和上一時刻的的狀態si-1,上一個時刻的輸出yi-1三者通過一個非線性函數得出。
圖12
其中,Ci是根據Encoder編碼階段的各個隱藏狀態(向量)的權重和。
圖8
其中,每個時刻的權重aij表示Encoder編碼階段的第j個隱藏狀態對Decoder解碼階段的第i個隱藏狀態的權重影響。
圖9
其中,eij爲Encoder編碼階段的第j個隱藏狀態和Decoder解碼階段的第i-1個隱藏狀態的聯合前饋網絡關係。
圖10
整個計算Ci的過程爲:分別計算Encoder編碼階段的每個隱藏狀態和Decoder解碼階段的第i-1個隱藏狀態前饋關係,再進行Softmax歸一化處理計算出該Encoder編碼階段的隱藏狀態的權重aij,最後將所有的Encoder編碼階段的隱藏狀態的進行權重加和。

3基於Seq2Seq模型的文本摘要應用復現

本次實踐主要是採用SouGouS新聞語料庫,基於Seq2Seq模型進行的文本摘要的代碼實現,儘管網上已經有大神已經實現了的,但是自己能跟着大神的代碼走一遍,理解一遍,將會比只看不動手來的強。因爲其中涉及到很多細節性的編碼問題和細節性的模型處理問題。主要參考rockingdingo大神的實現。python版本爲2.7.X.

3.1SouGouS新聞語料庫處理

數據集下載地址選擇的是精簡版下載。
step1 提取出新聞內容和標題
cat ./news_sohusite_xml.dat | iconv -f gbk -t utf-8 -c | grep "<content>" > corpus.txt
cat ./news_sohusite_xml.dat | iconv -f gbk -t utf-8 -c | grep "<contenttitle>" > corpus_title.txt
step2 選出了10萬行數據樣本
head -10000 corpus.txt >corpus_10000.txt
head -10000 corpus_title.txt >corpus_title_10000.txt
step3 數據預處理
主要的工作爲:文本的清洗工作,特徵字符的刪除,日期替換,數字替換。詞彙表的建立,語句的分詞工作,將語句的分詞替換成詞彙表的詞的id組成。data_util.py.

# 文本的預處理工作
# step1 獲取出文本內容
data_content,data_title = get_title_content(content_fp,title_fp)
ndexs = np.arange(len(data_content))
# step2 文本清洗工作
for index,content,title in zip(indexs,data_content,data_title):
    data_content[index] = remove_tag(content).encode('utf-8')
    data_title[index] = remove_tag(title).encode('utf-8')
# step3 劃分數據,訓練集,驗證集,測試集
get_train_dev_sets(data_content,data_title,train_rate=0.7,dev_rate=0.1,
                    tr_con_path=src_train_path,tr_title_path=dest_train_path,
                       dev_con_path=src_dev_path,dev_title_path=dest_dev_path,
                       test_con_path=src_test_path,test_title_path=dest_test_path
                       )
# step4 將各個樣本的語句進行切分,並將各個語句中的詞轉換成詞彙表中該詞對應的id.
prepare_headline_data(root_path,vocabulary_size=80000,tokenizer=jieba_tokenizer)
3.2Seq2Seq+Attention模型搭建

Tensorfolw Github提供了一個基於Seq2Seq模型實現的textSum可參考其做一定程度的修改。構建模型的文件是seq2seq_model.py.
step1 Encoder+Decoder+attention層的構建
tensorflow中提供了5個構造seq2seq函數,這裏使用的是embedding_attention_seq2seq主要介紹內部詳細實現。文件爲seq2seq_attn.py

basic_rnn_seq2seq:最簡單版本,輸入和輸出都是embedding的形式;最後一步的state vector作爲decoder的initial state;encoder和decoder用相同的RNN cell, 但不共享權值參數;
tied_rnn_seq2seq:同1,但是encoder和decoder共享權值參數
embedding_rnn_seq2seq:同1,但輸入和輸出改爲id的形式,函數會在內部創建分別用於encoder和decoder的embedding matrix
embedding_tied_rnn_seq2seq:同2,但輸入和輸出改爲id形式,函數會在內部創建分別用於encoder和decoder的embedding matrix
embedding_attention_seq2seq:同3,但多了attention機制.

# 1.Encoder編碼器的構造,size爲隱藏層單元數,num_layers爲LSTM的層數
single_cell = tf.contrib.rnn.GRUCell(size)
if use_lstm:
    single_cell = tf.contrib.rnn.BasicLSTMCell(size, state_is_tuple=True)
cell = single_cell
if num_layers > 1:
    cell = tf.contrib.rnn.MultiRNNCell([single_cell] * num_layers, state_is_tuple=True)
# 1.1進入tf.contrib.legacy_seq2seq.embedding_attention_seq2seq部分
#num_encoder_symbols爲編碼器部分的詞彙表的大小,embedding_size爲詞向量的大小,encoder_outputs爲輸入的各個句子的各個詞對應的ids
encoder_cell = core_rnn_cell.EmbeddingWrapper(cell,
        embedding_classes=num_encoder_symbols,embedding_size=embedding_size)
#encoder_outputs爲編碼器的輸出, encoder_state爲編碼器的隱藏層的狀態
encoder_outputs, encoder_state = core_rnn.static_rnn(encoder_cell, encoder_inputs, dtype=dtype)

# 2.Decoder解碼器的構造+attention,output_projection爲Decoder的輸出參數元組(W, B)
# num_decoder_symbols爲Decoder端的詞彙表大小
from tensorflow.python.ops import array_ops
output_size = None
    if output_projection is None:
        decoder_cell = core_rnn_cell.OutputProjectionWrapper(cell, num_decoder_symbols)
        output_size = num_decoder_symbols
# 2.1 將編碼器的輸出進行concat操作,作爲attention的輸入
top_states = [array_ops.reshape(e, [-1, 1, cell.output_size]) for e in encoder_outputs]
attention_states = array_ops.concat(top_states, 1)
# 2.2embedding_attention_decoder函數是構造encoder和attention的計算關係
return embedding_attention_decoder(
          decoder_inputs,
          encoder_state,
          attention_states,
          cell,
          num_decoder_symbols,
          embedding_size,
          num_heads=num_heads,
          output_size=output_size,
          output_projection=output_projection,
          feed_previous=feed_previous,
          initial_state_attention=initial_state_attention)

step2 seq2seq的損失函數

# 真實的labels,此處採用的loss函數爲sampled_softmax_loss,後面會講述到爲什麼是這個loss
labels = tf.reshape(labels, [-1, 1])
local_w_t = tf.cast(w_t, tf.float32)
local_b = tf.cast(b, tf.float32)
local_inputs = tf.cast(inputs, tf.float32)
loss_op = tf.cast(tf.nn.sampled_softmax_loss(
                weights=local_w_t,
                biases=local_b,
                labels=labels,
                inputs=local_inputs,
                num_sampled=num_samples,
                num_classes=self.target_vocab_size),
            tf.float32)

step3 梯度計算和優化

# Gradients and SGD update operation for training the model.
# 1.獲取出tf session中trainable=True的參數變量。
params = tf.trainable_variables()
# 2.設置參數更新優化器
opt = tf.train.GradientDescentOptimizer(self.learning_rate)
# 3.求參數的梯度值,其中self.losses[b]爲目標值(代價函數的表達式)
gradients = tf.gradients(self.losses[b], params)
# 4.梯度修剪,修正梯度值,用於控制梯度爆炸的問題。梯度爆炸和梯度彌散的原因一樣,
#都是因爲鏈式法則求導的關係,導致梯度的指數級衰減。爲了避免梯度爆炸,需要對梯度進行修剪。
clipped_gradients, norm = tf.clip_by_global_norm(gradients,max_gradient_norm)
# 5.求取更新參數的tensorflow的節點
self.updates.append(opt.apply_gradients(zip(clipped_gradients, params), global_step=self.global_step))

step4 模型訓練

# 4.1 根據隨機選取的bucket_id,批量選出輸入模型的三類feed數據,編碼器的inputs和解碼器的inputs,target_weights對encoder_inputs進行指示,爲1表示已經預測的,爲0表示PAD部分。
encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          train_set, bucket_id)
# 4.2 訓練的Feed數據的構造
input_feed = {}
for l in xrange(encoder_size):
    input_feed[self.encoder_inputs[l].name] = encoder_inputs[l]
for l in xrange(decoder_size):
    input_feed[self.decoder_inputs[l].name] = decoder_inputs[l]
    input_feed[self.target_weights[l].name] = target_weights[l]
# 4.3 由於目標詞是由於Decoder左移一位了,所以需要再添加一位
last_target = self.decoder_inputs[decoder_size].name
input_feed[last_target] = np.zeros([self.batch_size], dtype=np.int32)
train_ops = [self.updates[bucket_id],  # Update Op that does SGD.
             self.gradient_norms[bucket_id],  # Gradient norm.
             self.losses[bucket_id]]  # Loss for this batch.
outputs = session.run(train_ops, input_feed)

4Seq2Seq模型中存在的問題及相應解決的trick

問題1:tensorflow中的seq2seq例子爲什麼需要bucket?

問題描述:在處理序列問題時,每個batch中的句子的長度其實是不一的,通常做法是取batch中語句最長的length作爲序列的固定的長度,不足的補PAD。如果batch裏面存在一個非常長的句子,那麼其他的句子的都需要按照這個作爲輸入序列的長度,訓練模型時這將造成不必要的計算浪費。
加入bucket的trick:相當於對序列的長度做一個分段,切分成多個固定長度的輸入序列,比如說小於100爲一個bucket,大於100小於150爲另一個bucket…。每一個bucket都是一個固定的computation graph。這樣一來,對於模型輸入序列的固定長度將不再單一,從一定程度上減少了計算資源的浪費。

問題2:Sampled Softmax

**問題描述:**Seq2Seq模型的代價函數的loss便是sampled_softmax_loss。爲什麼不是softmax_loss呢?我們都知道對於Seq2Seq模型來說,輸入和輸出序列的class便是詞彙表的大小,而對於訓練集來說,輸入和輸出的詞彙表的大小是比較大的。爲了減少計算每個詞的softmax的時候的資源壓力,通常會減少詞彙表的大小,但是便會帶來另外一個問題,由於詞彙表的詞量的減少,語句的Embeding的id表示時容易大頻率的出現未登錄詞‘UNK’。於是,希望尋找到一個能使seq2seq模型使用較大詞彙表,但又不怎麼影響計算效率的解決辦法。
trick:《On Using Very Large Target Vocabulary for Neural Machine Translation》論文中提出了計算詞彙表的softmax的時候,並不採用全部的詞彙表中的詞,而是進行一定手段的sampled的採樣,從而近似的表示詞彙表的loss輸出。sampled採樣需要定義好候選分佈Q。即按照什麼分佈去採樣。

問題3:Encoder階段的Beam Search

問題描述:我們知道在Seq2Seq模型的最終目的是希望生成的序列發生的概率最大,也就是生成序列的聯合概率最大。在實際預測輸出序列的每個token的時候,採用的都是最大化下一目標詞(token)的概率,因爲Decoder的當前時刻的輸出是根據前一時刻的輸出,上一個時刻的隱藏狀態和語義向量Ci.通過依次求每個時刻的條件概率最大來近似獲得生成序列的發生最大的概率,這種做法屬於貪心思維的做法,獲得是局部最優的生成序列。
trick:《Sequence-to-Sequence Learning as Beam-Search Optimization》論文中提出Beam-Search來優化上述的局部最優化問題。Beam-Search屬全局解碼算法,Encoder解碼的目的是要得到生成序列的概率最大,可以把它看作是圖上的一個最優路徑問題:每一個時刻對應的節點大小爲整個詞彙表,路徑長度爲輸出序列的長度。可以由動態規劃的思想求得生成序列發生的最大概率。假設詞彙表的大小爲v,輸出序列的長度爲n.設t時刻各個節點(各個詞w)對應的最優路徑爲dt=[d1,d2,…,dv].則下一個時刻(t+1)的各個節點對應的最優路徑爲dt加上t時刻的各個節點(各個詞w)到(t+1)的各個節點(各個詞w)的最短距離,算法的複雜度爲o(nv^2).因爲詞彙表的大小v比較大,容易造成算法的複雜度比較大。爲了降低算法的複雜度,採用Beam Search算法,每步t只保留K個最優解(之前是保留每個時刻的整個詞彙表各個節點的最優解),算法複雜度爲o(nKv).
圖11

問題4:Exposure Bias

**問題描述:**Seq2Seq模型訓練的過程中,編碼部分的下一個時刻的輸出,是需要根據上一個時刻的輸出和上一個時刻的隱藏狀態和語義變量Ci.此時上一個時刻的輸出使用的是真實的token。而在驗證Seq2Seq模型的時候,由於不知道上一個時刻的真實token,上一個時刻的輸出使用的是上上個時刻的預測的輸出token。這將引發Exposure Bias(曝光偏差問題)。
trick: 使用Beam Search的Encoder的方式也能一定程度上降低Exposure Bias問題,因爲其考慮了全局解碼概率,而不僅僅依賴與前一個詞的輸出,所以模型前一個預測錯誤而帶來的誤差傳遞的可能性就降低了。論文Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks中提出了DAD的方法,論文中提到Exposure Bias的主要問題是訓練過程中模型不曾接觸過自己預測的結果,在測試過程中一旦預測出現錯誤,那麼模型將進入一個訓練過程中從未見過的狀態,從而導致誤差傳播。論文中提出了一個訓練過程逐漸地迫使模型處理自己的錯誤,因爲在測試過程中這是必須經歷的。DAD提出了一種退火算法來解決這個問題,在訓練過程中引入一個概率值參數εi ,每次以εi的概率選取真實的token作爲輸入, 1-εi的概率選取自己的prediction作爲輸入。逐漸降低εi,最終模型全都利用自己的prediction作爲下一步的輸入,和測試過程一致。

問題5:OOV和低頻詞

**問題描述:**OOV表示的是詞彙表外的未登錄詞,低頻詞則是詞彙表中的出現次數較低的詞。在Decoder階段時預測的詞來自於詞彙表,這就造成了未登錄詞難以生成,低頻詞也比較小的概率被預測生成。
trick:論文Abstractive Text Summarization using Seq2Seq RNNs and Beyond中使用Pointer-Generator機制來解決OOV和低頻詞問題。由於文本摘要的任務的特點,很多OOV 或者不常見的的詞其實可以從輸入序列中找到,因此一個很自然的想法就是去預測一個開關(switch)的概率P(si=1)=f(hi,yi-1,ci),如果開關打開了,就是正常地預測詞表;如果開關關上了,就需要去原文中指向一個位置作爲輸出。
圖13
17年的論文Get To The Point: Summarization with Pointer-Generator Networks使用Pointer-Generator Networks)使用Pointer-Generator Networks解決OOV問題,pointer-generator network相當於在每次摘要生成過程中,都會把原文中的詞彙動態地加入到詞表中去。

問題6:連續生成重複詞的問題

問題描述:在Seq2Seq的解碼階段,生成序列是很可能會生成連續的重複詞。
trick:論文Get To The Point: Summarization with Pointer-Generator Networks使用Pointer-Generator Networks)中使用Coverage mechanism來緩解重複詞的問題,模型中維護一個Coverage向量,這個向量是過去所有預測步計算的attention分佈的累加和,表示着該模型已經關注過原文的哪些詞,並且讓這個coverage向量影響當前步的attention計算。其中ci表示之前時刻的預測的attention分佈和。
圖15
此外,該論文中添加了一個coverage loss用於懲罰對重複的attention。ai表示當前時刻的attention,ci表示之前時刻的預測計算的attention分佈的累加和。
圖14

5參考鏈接

Text Summarization 綜述
Google實現的Text_Sum
tensorflow學習筆記(十一):seq2seq Model相關接口介紹
Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章