您當前的位置:首頁 > 攝影

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

作者:由 徐志強 發表于 攝影時間:2016-09-11

本篇教程的程式碼在

Github

上。這裡是迴圈神經網路教程的最後一部分,前幾部分別是:

迴圈神經網路教程第一部分-RNN簡介

迴圈神經網路教程第二部分-用python,numpy,theano實現一個RNN

迴圈神經網路教程第三部分-BPTT和梯度消失

本篇中我們將學習LSTM(長短項記憶)網路和GRU(門限遞迴單元)。LSTM由

Sepp Hochreiter and Jürgen Schmidhuber

在1997年第一次提出,現在是深度學習在NLP上應用的最廣泛的模型。GRU,在2014年第一次提出,是LSTM的簡單變種,和LSTM有很多相似特性。我們首先看一下LSTM,然後會看一下GRU和它有什麼不同。

LSTM網路

在第三部分中,我們看到了消失梯度問題是如何阻止標準的RNN學習到長距離依賴的。LSTM透過使用一個門限機制來克服消失梯度問題。為了理解其中的意思,先看一下LSTM如何計算隱狀態

s_{t}

(我使用

\circ

表示逐元素相乘):

i = \sigma(x_{t}U^{i}+s_{t-1}W^{i})

f=\sigma(x_{t}U^{f}+s_{t-1}W^f)

o=\sigma(x_{t}U^{o}+s_{t-1}W^{o})

g=tanh(x_{t}U^{g}+s_{t-1}W^g)

c_{t}=c_{t-1}\circ f+g\circ i

s_{t}=tanh(c_{t})\circ o

這些公式初看上去很複雜,但實際上沒有那麼難。首先,注意到LSTM層只是計算隱狀態的另一種方法。之前,我們計算隱狀態

s_{t}=tanh(Ux_{t}+Ws_{t-1})

,這裡的輸入是當前時刻

t

的輸入和之前的隱狀態

s_{t-1}

,輸出是一個新的隱狀態

s_{t}

。LSTM單元做的也是同樣的事,只是用了不同的方式,這是理解整個全域性的關鍵。你本質上可以把LSTM(GRU)視作一個黑盒,在給定當前輸入和之前隱狀態後,以某種方式計算下一個隱狀態。

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

現在讓我們來理解一下LSTM單元是如何隱狀態的。Chris Olah寫了一篇關於這個問題的很優秀的文章,為了避免重複他的工作,我在這裡只是給一個簡單的解釋。我建議你去讀他的文章來做深入的瞭解並進行視覺化,總結一下:

i,f,o

被稱作輸入、遺忘和輸出門。注意到它們的計算公式是一致的,只是用了不同的引數矩陣。它們被稱作門是因為sigmoid函式把這些向量的值擠壓到了0和1之間,把它們和其他的向量逐元素相乘,就定義了你想讓其他向量能“剩下”多少。輸入門定義了針對當前輸入得到的隱狀態能留下多少。遺忘門定義了你想留下多少之前的狀態。最後,輸出門定義了你想暴露多少內部狀態給外部網路(更高層和下一時刻)。所有門都有相同的維度

d_{s}

,即隱狀態的大小。

g

是根據當前的輸入和之前的隱狀態計算得到的一個“候選”狀態。它和普通的RNN有完全相同的計算公式,只是我們把引數

U, W

重新命名為

U^{g}, W^{g}

。然而,和在RNN中把

g

作為新的隱狀態不同,我們使用上面的輸入門從中挑選一部分結果。

c_{t}

是單元的內部記憶。它是之前的記憶

c_{t-1}

乘以遺忘門加上新得到的隱狀態

g_{t}

乘以輸入門得到的結果。因此,直觀上可以認為它是我們如何組合之前的記憶和新的輸入而得到的結果。我們可以選擇完全忽略舊的記憶(遺忘門全0)或者完全忽略計算得到的新狀態(輸入門全0),但是大多數時候會選擇這兩個極端之間的結果。

給定當前記憶

c_{t}

,我們最終根據記憶和輸出門的乘積計算得到輸出隱狀態

s_{t}

。在網路中,不是所有的內部記憶都會和其他單元使用的隱狀態相關。

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

直觀上,普通RNN可以認為是LSTM的一個特例。如果你把輸入門全部固定為1,遺忘門全部固定為0(你通常會忘記之前的記憶),輸出門全都固定為1(你暴露出全部記憶),你得到的幾乎就是標準RNN,只是多了一個額外的tanh把輸出壓縮了一些。門機制讓LSTM能顯式地對長期依賴進行建模。透過學習門的引數,網路能夠學會如何表示它的記憶。

值得注意的是,也存在一些基本LSTM架構的變種。一個常見的變種建立peephole連線,讓門不僅依賴於之前的隱狀態

s_{t-1}

,也依賴於之前的內部狀態

c_{t-1}

,在門方程中新增一個額外項。也存在許多其他的變種。

LSTM: A Search Space Odyssey

這篇文章用實驗比較了一些不同的LSTM架構。

GRUS

GRU層中的思想和LSTM層十分相似,公式如下:

z=\sigma (x_{t}U^z+s_{t-1}W^z)

r=\sigma(x_{t}U^r+s_{t-1}W^r)

h=tanh(x_{t}U^h+(s_{t-1}\circ r)W^h)

s_{t}=(1-z)\circ h+z\circ s_{t-1}

GRU有兩個門,重置門

r

,更新門

z

。直觀上,重置門決定了如何組合新輸入和之前的記憶,更新門決定了留下多少之前的記憶。如果我們把重置門都設為1,更新門都設為0,也同樣得到了普通的RNN模型。使用門機制的基本思想和LSTM相同,都是為了學習長期依賴,但是也有一些重要的不同之處:

GRU有兩個門,LSTM有三個門。

GRU沒有不同於隱狀態的內部記憶

c_{t}

,沒有LSTM中的輸出門。

輸入門和遺忘門透過更新門

z

進行耦合,重置門

r

被直接應用於之前的隱狀態。因此,LSTM中的重置門的責任實質上被分割到了

r

z

中。

在計算輸出時,沒有使用第二個非線性單元。

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

GRU VS LSTM

現在你已經看到了兩個能夠解決消失梯度問題的模型,你可能會疑惑:使用哪一個?GRU非常新,它們之間的權衡沒有得到完全的研究。根據Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling和 An Empirical Exploration of Recurrent Network Architectures的實驗結果,兩者之前沒有很大差別。在許多工中,兩種結構產生了差不多的效能,調整像層大小這樣的引數可能比選擇合適的架構更重要。GRU的引數更少,因而訓練稍快或需要更少的資料來泛化。另一方面,如果你有足夠的資料,LSTM的強大表達能力可能會產生更好的結果。

實現

讓我們回到第二部分中實現的語言模型,現在在RNN中使用GRU單元。這裡沒有什麼重要的原因關於為什麼在這一部分中使用GUR而不是LSTM(除了我想更熟悉一下GRU外)。它們的實現幾乎相同,因此你可以很容易地根據改變後的公式把GRU的程式碼修改成LSTM的。

這裡基於的是之前的Theano實現,注意到GRU只是另一種計算隱狀態的方式,所以這裡我們只需要在前向傳播函式中改變之前的隱狀態計算方式。

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

的初始化方式,因為現在它們的大小變化了。這裡並沒有展示出初始化的程式碼,但是在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來更新引數

在第二部分中,我們使用最簡單的隨機梯度下降(SGD)來更新我們的引數,事實證明這並不是一個好主意。如果你把學習率設得很低,SGD保證能找到一個好的解,但在實際中會花費很長的時間。有一些常用的SGD的變種,包括 (Nesterov) Momentum Method,AdaGrad,AdaDelta和RmsProp,這篇文章中對這些方法有一個很好的綜述。我也打算在將來的文章的文章中仔細探索每一個方法的實現。針對教程本部分,我準備選用rmsprop,它的基本思想是

根據之前的梯度和逐引數調整學習率

。直觀上,這意味著頻繁出現的特徵會獲得較小的學習率,稀有的特徵會獲得較大的學習率。

rmsprop的實現很簡單。針對每個引數,我們儲存一個快取變數,在梯度下降時,我們如下更新引數和快取變數(以

W

為例):

cacheW

=

decay

*

cacheW

+

1

-

decay

*

dW

**

2

W

=

W

-

learning_rate

*

dW

/

np

sqrt

cacheW

+

1e-6

衰減率通常設為0。9或0。95,加上1e-6項是為了防止除0。

新增一個嵌入層

使用word2vec和Glove這樣的詞嵌入模型是提高模型精度的一個常用手段。相對於使用one-hot向量表示詞,使用word2vec和Glove學習到的低維向量中含有一定的語義——相似的詞有著相似的向量。使用這些向量是預訓練的一種形式。直觀上,你告訴神經網路哪些詞是相似的,以便於它可以更少地學習語言知識。使用預訓練的向量在你沒有大量的資料時非常有用,因為它能讓網路可以對未見過的詞進行泛化。我在實驗中沒有使用預訓練的詞向量,但是新增一個嵌入層(程式碼中的矩陣

E

)很容易。嵌入矩陣只是一個查詢表——第i個列向量對應於詞表中的第i個詞。透過更新矩陣

E

,我們也可以自己學習詞向量,但只能特定於我們的任務,不如可以下載到的在上億個文件訓練的詞向量那麼通用。

新增第二個GRU層

在我們的網路中新增第二個層可以讓模型捕捉到更高層的互動。你也可以再新增額外的層,但我在實驗中沒有嘗試。在新增2到3個層後,你可能會接著觀察到損失值在降低,當然除非你有大量的資料,更多的層不可能會產生很大的影響,甚至可能導致過擬合。

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

迴圈神經網路教程第四部分-用Python和Theano實現GRULSTM迴圈神經網路

向網路中新增第二個層是很簡單的,我們只需要修改前向傳播中的計算過程和初始化函式。

# 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效能的技巧,但是其中最重要的一個可能是用批次資料更新模型引數。相較於每次學習一個句子,你應該把相同長度的句子分成組(甚至把所有句子填充到相同長度),然後進行大規模矩陣乘法,並按批將梯度加和。這是因為大規模矩陣乘法可以用GPU高效處理。如果不這麼做,使用GPU帶來的加速是很少的,訓練過程會非常緩慢。

所以,對於訓練大規模的模型,我強烈建議使用一個針對性能最佳化過的深度學習庫。用上面的程式碼需要訓練幾天或幾周的模型用這些庫只需要訓練幾小時。我個人比較喜歡Keras,它非常易用,並帶有一些關於RNN的非常好的例子。

結果

為了免去你用幾天時間訓練模型的痛苦,我訓練了一個和教程二中的模型很相似的一個模型。我使用的詞表大小是8000,把詞對映到了48維向量,並用了兩個128維的GRU層。這個IPython notebook包含了載入模型的程式碼,你可以直接使用它,修改它,用它來生成文字。

下面是一些網路輸出的非常不錯的例子(我添加了首字母大寫):

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 )

觀察這些句子在多個時刻的語義依賴是非常有意思的。例如,機器人和自動地是明顯相關的,開關括號也是。我們的網路能學習到這些,看起來非常酷!

PS:RNN系列教程翻譯算是告一段落了,裡面的一些實驗我還沒有做,後續也會去做。RNN對於序列建模來說很強大,在自動問答,機器翻譯,影象描述生成中都有使用,後續會翻譯或自己寫一些這方面的內容,^_^!!!

標簽: T1  LSTM  dot  GRU  RNN