1. 程式人生 > >用python實現LSTM/GRU

用python實現LSTM/GRU

本文翻譯自 
程式碼在Github上 
這是Part4 ,RNN教程的最後一部分; 
在這一部分,主要學習LSTM神經網路和GRU。LSTM在1997年首次提出,幾乎是最流行的用於自然語言處理的深度學習模型。GRUs在2014年首次提出,是LSTMs的簡單變體。讓我們關注LSTMs,再看看GRUs有什麼不同。 
LSTM NETWORKS

前面提到,梯度消失問題能夠阻止標準RNNs學習長距離的依賴關係。LSTMs通過門控機制來克服梯度消失。為了理解其中的含義,讓我們看看LSTM是如何計算隱含層狀態St. 
這裡寫圖片描述 
這些等式看起來相當的複雜,但是比更不像想象中的那麼難。首先,注意LSTM層僅僅是計算隱含層狀態的另一種方法;之前我們使用這裡寫圖片描述

來計算隱含層狀態;模組的輸入為xt,當前的輸入處於第t步,s(t-1)是上一步的狀態。輸出是新的隱含層狀態; 
LSTM單元做了同樣的事情,只是方式不同。理解下面的圖是關鍵;你可以把LSTM單元當作黑箱來對待,根據當前的輸入和之前的隱含層狀態,計算下一個隱含層狀態; 
這裡寫圖片描述 
讓我們有個直覺的感受:LSTM 是如何計算隱含層狀態的;相關部落格 
這裡做簡單的解釋,讀上述部落格能夠更深入的理解和好的視覺化。但是,總結如下:

尤其是,在基本的LSTM框架下存在一些變體。一個通常的做法是構建窺視孔連結,它允許門不僅僅依賴於之前的隱含層狀態St-1,而且依賴於先前的內部狀態Ct-1,在門等式上新增一個新項;這裡有很多變體,https://arxiv.org/pdf/1503.04069.pdf

“>這篇文章評價了不同的LASTM架構; 
GRUS 
GRU層背後的思想和LSTM層背後的思想相似,等式如下: 
這裡寫圖片描述 
GRU有兩個門,一個重置門r和一個更新門,直觀的,重置門決定了如何把新的輸入與之前的記憶相結合,更新門決定多少先前的記憶起作用。如果我們把所有reset設定為全1,更新門設定為全0,又達到了普通RNN的形式;使用一個門機制學習長距離依賴的基本思想與LSTM相同,但是有如下不同點: 
GRU有兩個門 ,LSTM有三個門; 
GRU不能處理 
輸入和遺忘門能夠被更新門z耦合,重置門r能夠直接應用於之前的隱含狀態。因此,重置門的職責在LSTM中被拆分為r和z; 
計算輸出時不應用第二非線性函式; 
這裡寫圖片描述
 
GRU VS LSTM 
現在,有兩個模型來解決梯度消失的問題,哪個更有效呢?GRUs相當的新,對它的評價沒有被完全的探索;根據經驗進行評價,相關文章http://jmlr.org/proceedings/papers/v37/jozefowicz15.pdf“>part1,part2沒有明確的勝利者。在許多工中,兩個模型能產生相當的表現,看起來選擇像層數這樣的超引數相比與選擇框架更重要。GRUs擁有更少的引數,可能訓練的更快些或需要更少的資料來訓練;如果你有足夠多的資料,具有更強的表達能力的LASTs可能導致更好的結果 
IMPLEMENTATION 
讓我們轉向part2所述語言模型的實現,讓我們在RNN中使用GRU單元,沒有原則性的原因為什麼使用GRUs而不是用LSTMs. 
他們的實現幾乎是完全相同的,所以可以把GRU中的程式碼很容易的轉化為LSTM中的程式碼,僅僅需要改變幾個等式; 
以前面的Theano實現為程式碼基礎,記得GRU(LSTM)層僅僅計算隱含層狀態的另一種方式。因此,所有需要做的就是改變在我們的前向傳播方法中隱含層狀態的計算

def forward_prop_step(x_t, s_t1_prev):
      # This is how we calculated the hidden state in a simple RNN. No longer!
      # s_t = T.tanh(U[:,x_t] + W.dot(s_t1_prev))

      # Get the word vector
      x_e = E[:,x_t]

      # GRU Layer
      z_t1 = T.nnet.hard_sigmoid(U[0].dot(x_e) + W[0].dot(s_t1_prev) + b[0])
      r_t1 = T.nnet.hard_sigmoid(U[1].dot(x_e) + W[1].dot(s_t1_prev) + b[1])
      c_t1 = T.tanh(U[2].dot(x_e) + W[2].dot(s_t1_prev * r_t1) + b[2])
      s_t1 = (T.ones_like(z_t1) - z_t1) * c_t1 + z_t1 * s_t1_prev

      # Final output calculation
      # Theano's softmax returns a matrix with one row, we only need the row
      o_t = T.nnet.softmax(V.dot(s_t1) + c)[0]

      return [o_t, s_t1]

在我們的實現中,我們加入了偏置單元b,c;這相當的典型以至於沒在等式中展現。當然,我們需要改變我們的引數U和W的初始化,因為他們現在有不同的size. 
初始化程式碼沒有展示,但是在Github中,我還加入字嵌入層E, 
這是相當的簡單。但是梯度怎麼樣?我們能夠通過鏈式法則得到E,W,U,b,和c的梯度,就像之前做的一樣,但是在實際中,大多人使用像支援表示式自動分劃的theano這樣的庫。如果出於某種原因,自己計算梯度,你跟可能是因為想模快化不同的單元,生成運用鏈式規則進行自動分化的不同版本;下面是theano計算梯度:

# Gradients using Theano
dE = T.grad(cost, E)
dU = T.grad(cost, U)
dW = T.grad(cost, W)
db = T.grad(cost, b)
dV = T.grad(cost, V)
dc = T.grad(cost, c)

為了得到更好的結果,在我們的實現中,我們使用了額外的技巧 
使用RMSPROP進行引數更新(USING RMSPROP FOR PARAMETER UPDATES) 
在part2我們使用了隨機梯度下降法的基礎版本進行引數更新。這證明不是個好的方案,如果設定學習率足夠低的話,SGD能夠保證向一個好的解決方案取得進展,但是在實際中會花費很長時間。存在很多通用的SGD變體。包括:http://101.96.8.164/www.cs.toronto.edu/~fritz/absps/momentum.pdf“> (Nesterov) Momentum Methodhttp://www.magicbroom.info/Papers/DuchiHaSi10.pdf“>AdaGrad等等; 
這個部落格介紹了許多這些函式的概述, 
在我們的教程中,選擇rmsprop,rmsprop背後的基本思想是根據先前梯度的和來調整學習率per-parameter。直觀的 ,它意味著頻繁出現的特徵得到更小的學習率(因為它的梯度的和將會更大),稀有的特徵得到更大的學習率; 
rmsprop的實現相當的簡單;對於每個引數,有一個快取變數。 
在梯度下降時,我們更新引數和此變數;

cacheW = decay * cacheW + (1 - decay) * dW ** 2
W = W - learning_rate * dW / np.sqrt(cacheW + 1e-6)

衰退典型的被設定為0.9或0.95,1e-6 是為了避免0的出現; 
加入嵌入層(ADDING AN EMBEDDING LAYER) 
使用例如word2vect和GloVe單詞嵌入是一個流行的方法提高我們精度。代替使用one-hot vector來表達單詞,使用word2vec或GloVe學習得到的攜帶語義的低維向量(形似的單詞具有相似的向量),使用這些向量是訓練前預處理的一種方式,直觀的,能夠告訴神經網路那些單詞是相似的,以至於需要更少的學習語言;在你沒有大量資料的時候,使用預訓練向量很有效,因為它允許神經網路推廣到沒有見過的單詞。我沒有使用過預處理單詞向量,但是加入一個嵌入層使它們更容易的插入進來;嵌入矩陣(E)就是一個查詢表。第i列向量對應於我們的單詞表中的第i個單詞; 
通過更新E進行單詞的向量表示的學習更新;但是,這與我們的特殊任務相關,並不是可以下載使用大量文件尋訓練的模型進行通用;

新增第二個GRU層(ADDING A SECOND GRU LAYER) 
在神經網路中,加入第二層能夠使我們的模型捕捉到更高水平相互作用;你能夠加入額外的層;你將會發現在2-3層之後,結果會衰退,除非你擁有大量的資料,更多的層次不太可能造成大的差異,可能導致過擬合; 
這裡寫圖片描述

向我們的神經網路中新增第二層是簡單的,我們僅僅需要修改前向傳播計算和初始化函式;

# GRU Layer 1
z_t1 = T.nnet.hard_sigmoid(U[0].dot(x_e) + W[0].dot(s_t1_prev) + b[0])
r_t1 = T.nnet.hard_sigmoid(U[1].dot(x_e) + W[1].dot(s_t1_prev) + b[1])
c_t1 = T.tanh(U[2].dot(x_e) + W[2].dot(s_t1_prev * r_t1) + b[2])
s_t1 = (T.ones_like(z_t1) - z_t1) * c_t1 + z_t1 * s_t1_prev

# GRU Layer 2
z_t2 = T.nnet.hard_sigmoid(U[3].dot(s_t1) + W[3].dot(s_t2_prev) + b[3])
r_t2 = T.nnet.hard_sigmoid(U[4].dot(s_t1) + W[4].dot(s_t2_prev) + b[4])
c_t2 = T.tanh(U[5].dot(s_t1) + W[5].dot(s_t2_prev * r_t2) + b[5])
s_t2 = (T.ones_like(z_t2) - z_t2) * c_t2 + z_t2 * s_t2_prev

有關效能 
有很多技巧來優化RNN效能,但是最重要的一個可能是批量處理你的更新。一次不僅僅學習一句話,可以把同等長度的句子聚集在一起,然後執行大的矩陣相乘,把整個集合的梯度相加;這是因為大的矩陣相乘在GRU中很高效處理。如果不這樣,使用GPU只能獲得少量的效能優化,訓練將會非常慢; 
所以,如果你想訓練大的模型,我建議使用現存的深度學習庫深度學習庫連結,它們進行了效能優化;上面的程式碼要花費數天訓練的模型,使用這些庫只需要花費幾個小時; 
個人喜歡Keras,它使用簡單,而且帶有好的例子; 
結果(RESULTS) 
為了避免花費數天訓練一個模型的痛苦經歷,我訓練了一個和Part2中相似的模型。我使用了大小為8000的單詞表。把單詞對映到48維的向量,使用兩層128維的GRU層。 
原始碼中包括載入模型的程式碼,可以修改模型,使用模型來生成文字; 
這裡有神經網路生成的文字的示例: 
I am a bot , and this action was performed automatically . 
I enforce myself ridiculously well enough to just youtube. 
I’ve got a good rhythm going ! 
There is no problem here, but at least still wave ! 
It depends on how plausible my judgement is . 
( with the constitution which makes it impossible ) 
很興奮能夠看出這些句子中跨越多個單詞之間的語義相關;