第十四章——循環神經網絡(Recurrent Neural Networks)(第二部分)


本章共兩部分,這是第二部分:

第十四章——循環神經網絡(Recurrent Neural Networks)(第一部分)

第十四章——循環神經網絡(Recurrent Neural Networks)(第二部分)

 

14.4 深度RNN

堆疊多層cell是很常見的,如圖14-12所示,這就是一個深度RNN。

圖14-12 深度RNN(左),隨時間展開(右)

在TensorFlow中實現深度RNN,需要創建多個cell並將它們堆疊到一個MultiRNNCell中。下面的代碼創建了三個完全相同的cell(也可以創建三個擁有不同神經元個數的cell):

n_neurons = 100
n_layers = 3

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([basic_cell] * n_layers)
outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)

14.4.1 多GPU分布式訓練深度RNN

先跳過

14.4.2 應用Dropout

如果創建了一個很深的RNN,可能會造成過擬合。為防止過擬合,常用的技術就是dropout(在第十一章介紹過)。可以簡單地在RNN之前或者之后增加一個dropout層,但如果想在RNN層之間使用dropout,需要使用DropoutWrapper。下面的代碼在RNN每層的輸入都應用dropout,drop概率是50%。

keep_prob = 0.5

cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
cell_drop = tf.contrib.rnn.DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([cell_drop] * n_layers)
rnn_outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)

如果是要在輸出使用dropout,可以設置put_keep_prob。

上面代碼存在很大的問題,就是會在訓練和測試時都應用dropout(回憶一下十一章,dropout只能在訓練的時候使用)。不幸的是,DropoutWrapper還不支持is_training占位符。所以要么自己實現一個DropoutWrapper,要么創建兩個圖(一個用於訓練,一個用於測試)。

14.4.3 訓練時刻過多的難點

在長序列上訓練RNN,需要運行好多個時刻,使得RNN被展開為一個很深的模型。和任何其他的模型一樣也會遭受梯度消失(爆炸)問題(十一章)。 之前提到的技巧對深度展開RNN也是有效的:合適的參數初始化、不飽和激活函數(比如ReLU)、Batch Normalization、Gradient Clipping、faster optimizers。不過,如果用RNN去處理很長(比如100)的序列,訓練將變得極其緩慢。

最簡單最常見的解決方案是,訓練的時候只展開一部分時刻,這被稱作truncated backpropagation through time。在TensorFlow中實現是,只需截掉一部分輸入序列即可。不過這也有一個問題,那就是模型不能學習長期模式(long-term patterns)。一個變通方案是使得縮短的訓練數據同時包含最新的和陳舊的訓練數據(比如,一個序列包含前五個月的月度數據,前五周的數據,以及前五天的數據)。不過這一方案也是有局限的:如果去年的詳細數據真的很重要,怎么辦?如果前年有一件很明顯的大事必須考慮在內(比如選舉結果),那又怎么辦?

除了訓練時間長,RNN面臨的另一個問題是隨着長時間運行,前期記憶的淡忘。事實上隨着數據穿過RNN,每一時刻都有一些信息丟失掉。不久之后,RNN的狀態中就找不到第一次所輸入數據的蹤跡了。這可能是致命的。比如,在電影評論上面做情感分析。開頭一句話是“我愛這部電影”,但剩下的問題都是在累積該電影還能改進的地方。如果RNN忘掉了開頭那幾個字,很可能就誤解了這個評論。未解決這一問題,多種類型的具有長期記憶(long-term memory)功能的cell被引入,最出名的就是LSTM cell。

14.5 LSTM Cell

長短期記憶(Long Short-Term Memory,LSTM)cell由Sepp Hochreiter和Jürgen Schmidhuber於1997年提出。隨后經歷了很多研究者的改進,比如Alex Graves,Haşim SakWojciech Zaremba等。如果把LSTM cell看作黑盒,它和一個基本的cell差不多,只不過表現更好:訓練是更容易收斂,更容易發現數據中的長期依賴。在TensorFlow中,可以簡單地使用BasicLSTMCell替換掉BasicRNNCell:

lstm_cell = tf.contrib.rnn.BasicLSTMCell(num_units=n_neurons)

LSTM cell要管理兩個狀態向量,為了性能原因它們默認是分開的。可以在創建BasicLSTMCell的時候設置state_is_tuple=False來改變這一行為。

圖14-13是一個基本的LSTM cell:

圖14-13 LSTM cell

如果不看中間的淡黃色盒子,LSTM cell和常規的cell是類似的,除了它的狀態被切分為了兩個向量:$\textbf{h}_{(t)}$和$\textbf{c}_{(t)}$(c代表cell)。可以將$\textbf{h}_{(t)}$看做短期狀態,$\textbf{c}_{(t)}$代表長期記憶。

現在來看一下盒子中到底是什么邏輯。核心思想就是,這一神經網絡可以學習長期狀態中應該保持什么,應該丟掉什么,應該讀取什么。當長期狀態$\textbf{c}_{(t-1)}$從左到右穿過神經網絡時,它首先通過一個遺忘門(forget gate),丟掉一些記憶,然后通過加法運算增加一些新的記憶,增加的記憶是經過輸入門(input gate)篩選過的。其結果$\textbf{c}_{(t)}$被直接輸出了,不經過任何變換。所有,在每一時刻,都有一些信息被丟掉,一些信息被添加。此外,經過剛才的加法運算,長期記憶會被復制一份,先應用tanh函數,然后又經過輸出門(output gate)過濾,參與生成短期記憶$\textbf{h}_{(t)}$(與這一時刻的輸出$\textbf{y}_{(t)}$相等)。接着讓我們看一下,新的記憶是從哪來的,以及這些門是如何工作的。

首先,當前時刻的輸入$\textbf{x}_{(t)}$和前一時刻的短期記憶$\textbf{h}_{(t-1)}$供應給4個不同的全連接層。這4個全連接層有不同的目的:

  • 最重要的一層是輸出$\textbf{g}_{(t)}$的那個。它擁有類似基本cell的分析當前時刻輸入$\textbf{x}_{(t)}$和前一時刻短期記憶$\textbf{h}_{(t-1)}$的角色。對於基本的cell,$\textbf{y}_{(t)}$和$\textbf{h}_{(t)}$會直接輸出。不過LSTM cell的這一層不會直接輸出,還會部分地保存在長期記憶中。
  • 另外的三層是門控制器(gate controllers)。他們使用logistic激活函數,輸出的范圍是0到1。其輸出用於按元素點乘運算。所以如果輸出0,門被關閉;如果輸出1,門被打開。明確來講:
    • 遺忘門(forget gate,由$\textbf{f}_{(t)}$控制)決定哪些長期記憶應該被遺忘。
    • 輸入門(input gate,由$\textbf{i}_{(t)}$控制)決定$\textbf{g}_{(t)}$的哪些內容應該被添加到長期記憶。
    • 輸出門(output gate,由$\textbf{o}_{(t)}$控制)決定哪些長期記憶應該被讀取和輸出。

LSTM一個實例輸出的計算公式:

其中,

  • $W_{xi},W_{xf},W_{xo},W_{xg}$是4個全連接層關於輸入向量$\textbf{x}_{(t)}$的權重矩陣。
  • $W_{hi},W_{hf},W_{ho},W_{hg}$是4個全連接層關於短期記憶$\textbf{h}_{(t-1)}$的權重矩陣。
  • $b_{i},b_{f},b_{o},b_{g}$是4個全連接層的偏置項。TensorFlow會將$b_{f}$初始化為全是1的矩陣,而不是全是0,這會使得訓練初期沒有東西被遺忘。

14.5.1 Peephole Connections

在基本的LSTM cell中,控制門的狀態只由當前時刻的輸入$\textbf{x}_{(t)}$和前一時刻的短期記憶$\textbf{h}_{(t-1)}$決定。如果讓長期記憶也參與控制門的管理可能會更好一點。這一思想由Felix Gers和Jürgen Schmidhuber在2000年提出。他們提出了一種LSTM變種,增加了一個被稱作peephole connections的連接:前一時刻長期記憶$\textbf{c}_{(t-1)}$也作為遺忘門和輸出門控制器的一個輸入,當前時刻長期記憶$\textbf{c}_{(t)}$也作為輸出門控制器的一個輸入。

在TensorFlow中實現peephole connections,可以用LSTMCell代替BasicLSTMCell並設置use_peepholes=True:

lstm_cell = tf.contrib.rnn.LSTMCell(num_units=n_neurons, use_peepholes=True)

還有很多其他的LSTM cell變種,最有名的要數GRU cell。

14.6 GRU Cell

Gated Recurrent Unit (GRU) cell在2014年的一篇論文中提出, 該論文同時提出了我們先前提到的Encoder–Decoder神經網絡。

圖14-14 GRU cell

GRU cell是LSTM cell的簡化版,但是表現的同樣好(2015年的論文LSTM: A Search Space Odyssey表明,所有LSTM變種的表現大致相同)。主要簡化的部分如下:

  • 兩個狀態向量被合並成了單獨的$\textbf{h}_{(t)}$。
  • 一個門控制器($\textbf{z}_{(t)}$)同時控制遺忘門和輸入門。如果門控制器輸出1,輸入門被打開同時遺忘門被關閉。如果控制器輸出0,輸入門被關閉同時遺忘門被打開。換句話說,如果一個記憶需要被存儲,那么該位置原先的記憶會被清除。
  • 不再使用輸出門,整個狀態矩陣都會輸出。不會,有一個新的門控制器($\textbf{h}_{(r)}$),來控制先前的哪些記憶需要傳遞給主層。

GRU一個實例輸出的計算公式:

在TensorFlow中創建GRU cell:

gru_cell = tf.contrib.rnn.GRUCell(num_units=n_neurons)

LSTM和GRU cells是近些年RNNs取得成功的重要原因,尤其是在自然語言處理領域。

14.7 自然語言處理

大部分最先進的nlp應用,比如機器翻譯,自動摘要,語法分析,情感分析等等,都基於(或部分基於)RNNs。本節需要提取了解一下TensorFlow的Word2VecSeq2Seq教程。

14.7.1 Word Embeddings

首先要解決的,就是詞表示的問題(對於中文來講,一般第一步是分詞。英文有天然的空格對詞進行分割。當然中文不進行分詞也是可以的,比如以單字或者二字串作為特征)。詞表示的一個方案是one-hot向量。假設詞表有50000個詞,那么第n個詞表示為一個50000維向量,第n個位置是1,其他位置全是0。然而,詞表這么大,這一稀疏表示效率很低。

更理想的是,我們希望相同意義的詞有相似的表示形式,以便模型可以將其學到的模式推廣到所有相似的詞。比如,如果模型學到“I drink milk”是一個有效的句子,並且知道“milk”和“water”近似但是和“shoes”差別較大,那模型就能知道“I drink water”也是一個合法的句子,而“I drink shoes”很可能不是。

一個常見的解決方案是,用一個更小更稠密的向量(比如150維)來表示詞表中的每個詞,這被稱作embedding。並且需要一個神經網絡通過訓練,找到每個詞最好的embedding。訓練初期,embedding都是隨機選擇的,但是通過反向傳播會變得越來越好。這意味着相似的詞會收斂到相似的向量,並且向量的維度可能會有實際的意義。比如,向量的不同維可能會表示性別,單數/復數(英語的單復數),形容詞/名稱,等等。(更多信息可參考Christopher Olah的著名博客,以及Sebastian Ruder的一系列博客

在TensorFlow中,需要創建一個變量來表示詞表中每個詞的embedding(會被隨機初始化):

vocabulary_size = 50000
embedding_size = 150
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))

然后假設你想把“I drink milk”這句話提供給神經網絡進行訓練。預處理的第一步是將句子表示成已知單詞的列表。比如去掉不必要的特殊字符,將字典外的單字表示成一個預定義的標記(比如“[UNK]”),將數字替換成“[NUM]”,將URLs替換成“[URL]”,等等。如果是字典內的單詞,就將其表示為它在字典中的id(從0到49999),比如[72, 3335, 288]。此時,就可以使用embedding_lookup()函數來獲取相應的embedding了:

train_inputs = tf.placeholder(tf.int32, shape=[None]) # from ids...
embed = tf.nn.embedding_lookup(embeddings, train_inputs) # ...to embeddings

如果你的模型學到了不錯的word embeddings,就可以高效地使用在所有nlp應用中了。

14.7.2 基於Encoder–Decoder神經網絡的機器翻譯

首先我們看一個簡單的機器翻譯模型,將英語句子譯為法語,如圖14-15:

圖14-15 一個簡單的機器翻譯模型

 

英語句子作為encoder的輸入,decoder輸出法語譯文。法語的真實譯文是decoder的輸入,不過向后推了一個時刻(第一個時刻的輸入是<go>,第二個時刻的輸入才是Je)。換句話說,decoder在當前時刻的輸入,應該是其上一時刻的輸出(盡管實際上並不是這個輸出)。decoder的輸入以語句起始符號(比如<go>)開頭,輸出以語句終止符號(比如<eos>)結尾。

作為encoder輸入的英語句子是被顛倒了的。比如“I drink milk”轉換成了“milk drink I”。這確保了英語句子的開頭在最后輸入給了encoder,並最先由decoder翻譯。

在每一步,decoder輸出譯文詞典(本例中是法語詞典)中每一個詞的分值,然后再由Softmax層將分值轉換成概率。比如,在第一步中,“Je”的概率可能是20%,“Tu”的概率可能是1%,等等。概率最大的單詞將被輸出。這與常規的分類任務很相似,所以可以用softmax_cross_entropy_with_logits()函數訓練該模型。

在用模型做預測的時候(訓練之后),並沒有目標語句輸入給decoder。簡單地把前一時期的輸出作為當前時期的輸入就可以了。如圖14-17(圖中省略掉了embedding lookup):

圖14-16 預測時期,將前一步的輸出,作為當前步的輸入

現在,我們已經知道機器翻譯的整體架構了。不過,如果查看TensorFlow的sequence-to-sequence教程,並學習rnn/translate/seq2seq_model.py(位於TensorFlow models)的源碼,會發現有些不同:

  • 首先,我們假設了所有的輸入序列(包括encoder和decoder)都是定長的。但是很明顯,句子的長度是不定的。這有多種處理方式——比如,static_rnn()和dynamic_rnn()函數使用sequence_length參數來描述句子的長度。不過,教程中使用了另一個方案(可能是由於性能原因):將一個句子切割成不同的組,每組長度相同(比如,一個矩陣的1-6個單詞分為一組,7-12個單詞分為另一組,等等)。較短的組使用特殊標記(比如“<pad>”)進行填充。例如“I drink milk”轉換成“<pad> <pad> <pad> milk drink I”,然后翻譯為“Je bois du lait <eos> <pad>”。當然,我們希望忽略EOS之后的內容。教程中的實現方式是使用一個target_weights向量,。比如對於目標語句“Je bois du lait <eos> <pad>”,這一向量是[1.0, 1.0, 1.0, 1.0, 1.0, 0.0](進行填充的位置就是0.0)。
  • 其次,由於詞表很大,輸出每個詞可能的概率去計算交叉熵是很慢的。一個解決方案是decoder輸出一個小得多的向量,比如1000維,然后使用抽樣技術估計損失。這一Sampled Softmax技術在2015年被提出。在TensorFlow中可以使用sampled_softmax_loss()函數。
  • 其次,教程中的實現使用了attention機制(attention mechanism)。RNN的attention機制超出了本書的范圍,不過可以參考machine translationmachine readingimage captions
  • 最后,教程中的實現使用了tf.nn.legacy_seq2seq模塊,這使得創建多種Encoder–Decoder模型變得簡單。比如,embedding_rnn_seq2seq()創建的Encoder–Decoder模型自動進行word embeddings。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM