1. 程式人生 > 其它 >零基礎入門深度學習 | 第五章: 迴圈神經網路

零基礎入門深度學習 | 第五章: 迴圈神經網路

無論即將到來的是大資料時代還是人工智慧時代,亦或是傳統行業使用人工智慧在雲上處理大資料的時代,作為一個有理想有追求的程式設計師,不懂深度學習這個超熱的技術,會不會感覺馬上就out了? 現在救命稻草來了,《零基礎入門深度學習》系列文章旨在講幫助愛程式設計的你從零基礎達到入門級水平。零基礎意味著你不需要太多的數學知識,只要會寫程式就行了,沒錯,這是專門為程式設計師寫的文章。雖然文中會有很多公式你也許看不懂,但同時也會有更多的程式碼,程式設計師的你一定能看懂的(我周圍是一群狂熱的Clean Code程式設計師,所以我寫的程式碼也不會很差)。

文章列表

零基礎入門深度學習(1) - 感知器 零基礎入門深度學習(2) - 線性單元和梯度下降

零基礎入門深度學習(3) - 神經網路和反向傳播演算法 零基礎入門深度學習(4) - 卷積神經網路 零基礎入門深度學習(5) - 迴圈神經網路 零基礎入門深度學習(6) - 長短時記憶網路(LSTM) 零基礎入門深度學習(7) - 遞迴神經網路

往期回顧

在前面的文章系列文章中,我們介紹了全連線神經網路和卷積神經網路,以及它們的訓練和使用。他們都只能單獨的取處理一個個的輸入,前一個輸入和後一個輸入是完全沒有關係的。但是,某些任務需要能夠更好的處理序列的資訊,即前面的輸入和後面的輸入是有關係的。比如,當我們在理解一句話意思時,孤立的理解這句話的每個詞是不夠的,我們需要處理這些詞連線起來的整個序列;當我們處理視訊的時候,我們也不能只單獨的去分析每一幀,而要分析這些幀連線起來的整個序列。

這時,就需要用到深度學習領域中另一類非常重要神經網路:迴圈神經網路(Recurrent Neural Network)。RNN種類很多,也比較繞腦子。不過讀者不用擔心,本文將一如既往的對複雜的東西剝繭抽絲,幫助您理解RNNs以及它的訓練演算法,並動手實現一個迴圈神經網路。

語言模型

RNN是在自然語言處理領域中最先被用起來的,比如,RNN可以為語言模型來建模。那麼,什麼是語言模型呢?

我們可以和電腦玩一個遊戲,我們寫出一個句子前面的一些詞,然後,讓電腦幫我們寫下接下來的一個詞。比如下面這句:

我昨天上學遲到了,老師批評了____。

我們給電腦展示了這句話前面這些詞,然後,讓電腦寫下接下來的一個詞。在這個例子中,接下來的這個詞最有可能是『我』,而不太可能是『小明』,甚至是『吃飯』。

語言模型就是這樣的東西:給定一個一句話前面的部分,預測接下來最有可能的一個詞是什麼。

語言模型是對一種語言的特徵進行建模,它有很多很多用處。比如在語音轉文字(STT)的應用中,聲學模型輸出的結果,往往是若干個可能的候選詞,這時候就需要語言模型來從這些候選詞中選擇一個最可能的。當然,它同樣也可以用在影象到文字的識別中(OCR)。

使用RNN之前,語言模型主要是採用N-Gram。N可以是一個自然數,比如2或者3。它的含義是,假設一個詞出現的概率只與前面N個詞相關。我們以2-Gram為例。首先,對前面的一句話進行切詞:

我 昨天 上學 遲到 了 ,老師 批評 了 ____。

如果用2-Gram進行建模,那麼電腦在預測的時候,只會看到前面的『了』,然後,電腦會在語料庫中,搜尋『了』後面最可能的一個詞。不管最後電腦選的是不是『我』,我們都知道這個模型是不靠譜的,因為『了』前面說了那麼一大堆實際上是沒有用到的。如果是3-Gram模型呢,會搜尋『批評了』後面最可能的詞,感覺上比2-Gram靠譜了不少,但還是遠遠不夠的。因為這句話最關鍵的資訊『我』,遠在9個詞之前!

現在讀者可能會想,可以提升繼續提升N的值呀,比如4-Gram、5-Gram.......。實際上,這個想法是沒有實用性的。因為我們想處理任意長度的句子,N設為多少都不合適;另外,模型的大小和N的關係是指數級的,4-Gram模型就會佔用海量的儲存空間。

所以,該輪到RNN出場了,RNN理論上可以往前看(往後看)任意多個詞。

迴圈神經網路是啥

迴圈神經網路種類繁多,我們先從最簡單的基本迴圈神經網路開始吧。

基本迴圈神經網路

下圖是一個簡單的迴圈神經網路如,它由輸入層、一個隱藏層和一個輸出層組成:

納尼?!相信第一次看到這個玩意的讀者內心和我一樣是崩潰的。因為迴圈神經網路實在是太難畫出來了,網上所有大神們都不得不用了這種抽象藝術手法。不過,靜下心來仔細看看的話,其實也是很好理解的。如果把上面有W的那個帶箭頭的圈去掉,它就變成了最普通的全連線神經網路。x是一個向量,它表示輸入層的值(這裡面沒有畫出來表示神經元節點的圓圈);s是一個向量,它表示隱藏層的值(這裡隱藏層面畫了一個節點,你也可以想象這一層其實是多個節點,節點數與向量s的維度相同);U是輸入層到隱藏層的權重矩陣(讀者可以回到第三篇文章零基礎入門深度學習(3) - 神經網路和反向傳播演算法,看看我們是怎樣用矩陣來表示全連線神經網路的計算的);o也是一個向量,它表示輸出層的值;V是隱藏層到輸出層的權重矩陣。那麼,現在我們來看看W是什麼。迴圈神經網路的隱藏層的值s不僅僅取決於當前這次的輸入x,還取決於上一次隱藏層的值s。權重矩陣 W就是隱藏層上一次的值作為這一次的輸入的權重。

如果我們把上面的圖展開,迴圈神經網路也可以畫成下面這個樣子:

現在看上去就比較清楚了,這個網路在t時刻接收到輸入Xt之後,隱藏層的值是St,輸出值是Ot,關鍵一點是,St的值不僅僅取決於Xt,還取決於St-1。我們可以用下面的公式來表示迴圈神經網路的計算方法:

式1是輸出層的計算公式,輸出層是一個全連線層,也就是它的每個節點都和隱藏層的每個節點相連。V是輸出層的權重矩陣,g是啟用函式。式2是隱藏層的計算公式,它是迴圈層。U是輸入x的權重矩陣,W是上一次的值St-1作為這一次的輸入的權重矩陣,f是啟用函式。

從上面的公式我們可以看出,迴圈層和全連線層的區別就是迴圈層多了一個權重矩陣 W。

如果反覆把式2帶入到式1,我們將得到:

從上面可以看出,迴圈神經網路的輸出值Ot,是受前面歷次輸入值Xt,Xt-1,Xt-2,Xt-3...影響的,這就是為什麼迴圈神經網路可以往前看任意多個輸入值的原因。

雙向迴圈神經網路

對於語言模型來說,很多時候光看前面的詞是不夠的,比如下面這句話:

我的手機壞了,我打算____一部新手機。

可以想象,如果我們只看橫線前面的詞,手機壞了,那麼我是打算修一修?換一部新的?還是大哭一場?這些都是無法確定的。但如果我們也看到了橫線後面的詞是『一部新手機』,那麼,橫線上的詞填『買』的概率就大得多了。

在上一小節中的基本迴圈神經網路是無法對此進行建模的,因此,我們需要雙向迴圈神經網路,如下圖所示:

當遇到這種從未來穿越回來的場景時,難免處於懵逼的狀態。不過我們還是可以用屢試不爽的老辦法:先分析一個特殊場景,然後再總結一般規律。我們先考慮上圖中,y2的計算。

從上圖可以看出,雙向卷積神經網路的隱藏層要儲存兩個值,一個A參與正向計算,另一個值A'參與反向計算。最終的輸出值y2取決於A2和A2'。其計算方法為:

A2和A2'則分別計算:

現在,我們已經可以看出一般的規律:正向計算時,隱藏層的值St和St-1有關;反向計算時,隱藏層的值St'與S't-1有關;最終的輸出取決於正向和反向計算的加和。現在,我們仿照式1和式2,寫出雙向迴圈神經網路的計算方法:

從上面三個公式我們可以看到,正向計算和反向計算不共享權重,也就是說U和U'、W和W'、V和V'都是不同的權重矩陣。

深度迴圈神經網路

前面我們介紹的迴圈神經網路只有一個隱藏層,我們當然也可以堆疊兩個以上的隱藏層,這樣就得到了深度迴圈神經網路。如下圖所示:

我們把第i個隱藏層的值表示為s_{t}^{(i)},s_{t}^{'(i)} ,則深度迴圈神經網路的計算方式可以表示為:

迴圈神經網路的訓練

迴圈神經網路的訓練演算法:BPTT

BPTT演算法是針對迴圈層的訓練演算法,它的基本原理和BP演算法是一樣的,也包含同樣的三個步驟:

1、前向計算每個神經元的輸出值;

2、反向計算每個神經元的誤差項,它是誤差函式E對神經元j的加權輸入偏導數;

3、計算每個權重的梯度。

最後再用隨機梯度下降演算法更新權重。

迴圈層如下圖所示:

前向計算

使用前面的式2對迴圈層進行前向計算:

注意,上面的St,Xt,St-1都是向量,用黑體字母表示;而U、V是矩陣,用大寫字母表示。向量的下標表示時刻,例如,St表示在t時刻向量s的值。

我們假設輸入向量x的維度是m,輸出向量s的維度是n,則矩陣U的維度是nXm,矩陣W的維度是nXn。下面是上式展開成矩陣的樣子,看起來更直觀一些:

在這裡我們用手寫體字母表示向量的一個元素,它的下標表示它是這個向量的第幾個元素,它的上標表示第幾個時刻。例如,s^{t}_{j} 表示向量s的第j個元素在t時刻的值。Uji表示輸入層第i個神經元到迴圈層第j個神經元的權重。Wji表示迴圈層第t-1時刻的第i個神經元到迴圈層第t個時刻的第j個神經元的權重。

誤差項的計算

BTPP演算法將第l層t時刻的誤差項delta ^{l}_{t} 值沿兩個方向傳播,一個方向是其傳遞到上一層網路,得到delta ^{l-1}_{t} ,這部分只和權重矩陣U有關;另一個是方向是將其沿時間線傳遞到初始t1時刻,得到delta ^{l}_{1} ,這部分只和權重矩陣W有關。

我們用向量nett表示神經元在t時刻的加權輸入,因為:

因此:

我們用a表示列向量,用a^{T} 表示行向量。上式的第一項是向量函式對向量求導,其結果為Jacobian矩陣:

同理,上式第二項也是一個Jacobian矩陣:

其中,diag[a]表示根據向量a建立一個對角矩陣,即

最後,將兩項合在一起,可得:

上式描述了將delta 沿時間往前傳遞一個時刻的規律,有了這個規律,我們就可以求得任意時刻k的誤差項delta_{k}

式3就是將誤差項沿時間反向傳播的演算法。

迴圈層將誤差項反向傳遞到上一層網路,與普通的全連線層是完全一樣的,這在前面的文章《零基礎入門深度學習 | 第三章:神經網路和反向傳播演算法》中已經詳細講過了,在此僅簡要描述一下。

迴圈層的加權輸入netl與上一層的加權輸入netl-1關係如下:

上式中net^{l}_{t} 是第l層神經元的加權輸入(假設第l層是迴圈層);net^{l-1}_{t} 是第l-1層神經元的加權輸入;a^{l-1}_{t} 是第l-1層神經元的輸出;f^{l-1} 是第l-1層的啟用函式。

所以,

式4就是將誤差項傳遞到上一層演算法。

權重梯度的計算

現在,我們終於來到了BPTT演算法的最後一步:計算每個權重的梯度。

首先,我們計算誤差函式E對權重矩陣W的梯度frac {partial E} {partial W}

上圖展示了我們到目前為止,在前兩步中已經計算得到的量,包括每個時刻t 迴圈層的輸出值St,以及誤差項delta _{t}

回憶一下我們在文章零基礎入門深度學習(3) - 神經網路和反向傳播演算法 介紹的全連線網路的權重梯度計算演算法:只要知道了任意一個時刻的誤差項delta _{t} ,以及上一個時刻迴圈層的輸出值St-1,就可以按照下面的公式求出權重矩陣在t時刻的梯度

在式5中,delta ^{t}_{i} 表示t時刻誤差項向量的第i個分量;S^{t-1}_{i} 表示t-1時刻迴圈層第i個神經元的輸出值。

我們下面可以簡單推導一下式5。

我們知道:

因為對W求導與Uxt無關,我們不再考慮。現在,我們考慮對權重項Wji求導。通過觀察上式我們可以看到Wji只與net^{t}_{j}

有關,所以:

按照上面的規律就可以生成式5裡面的矩陣。

我們已經求得了權重矩陣W在t時刻的梯度 nabla w_{t}E ,最終的梯度nabla wE 各個時刻的梯度之和:

式6就是計算迴圈層權重矩陣W的梯度的公式。

----------數學公式超高能預警----------

前面已經介紹了nabla wE 的計算方法,看上去還是比較直觀的。然而,讀者也許會困惑,為什麼最終的梯度是各個時刻的梯度之和呢?我們前面只是直接用了這個結論,實際上這裡面是有道理的,只是這個數學推導比較繞腦子。感興趣的同學可以仔細閱讀接下來這一段,它用到了矩陣對矩陣求導、張量與向量相乘運算的一些法則。

我們還是從這個式子開始:

因為Uxt與W完全無關,我們把它看做常量。現在,考慮第一個式子加號右邊的部分,因為W和f(nett-1)都是W的函式,因此我們要用到大學裡面都學過的導數乘法運算:

因此,上面第一個式子寫成:

我們最終需要計算的是nabla wE

我們先計算式7加號左邊的部分。矩陣對矩陣求導,其結果是一個四維張量(tensor),如下所示:

接下來,我們知道St-1=f(nett-1),它是一個列向量。我們讓上面的四維張量與這個向量相乘,得到了一個三維張量delta^{T}_{t} ,再左乘行向量最終得到一個矩陣:

接下來,我們計算式7加號右邊的部分:

於是,我們得到了如下遞推公式:

這樣,我們就證明了:最終的梯度各個時刻的梯度之和。

----------數學公式超高能預警解除----------

同權重矩陣W類似,我們可以得到權重矩陣U的計算方法。

式8是誤差函式在t時刻對權重矩陣U的梯度。和權重矩陣W一樣,最終的梯度也是各個時刻的梯度之和:

具體的證明這裡就不再贅述了,感興趣的讀者可以練習推導一下。

RNN的梯度爆炸和消失問題

不幸的是,實踐中前面介紹的幾種RNNs並不能很好的處理較長的序列。一個主要的原因是,RNN在訓練中很容易發生梯度爆炸和梯度消失,這導致訓練時梯度不能在較長序列中一直傳遞下去,從而使RNN無法捕捉到長距離的影響。

為什麼RNN會產生梯度爆炸和消失問題呢?我們接下來將詳細分析一下原因。我們根據式3可得:

上式的beta 定義為矩陣的模的上界。因為上式是一個指數函式,如果t-k很大的話(也就是向前看很遠的時候),會導致對應的誤差項的值增長或縮小的非常快,這樣就會導致相應的梯度爆炸和梯度消失問題(取決於beta大於1還是小於1)。

通常來說,梯度爆炸更容易處理一些。因為梯度爆炸的時候,我們的程式會收到NaN錯誤。我們也可以設定一個梯度閾值,當梯度超過這個閾值的時候可以直接擷取。

梯度消失更難檢測,而且也更難處理一些。總的來說,我們有三種方法應對梯度消失問題:

1、合理的初始化權重值。初始化權重,使每個神經元儘可能不要取極大或極小值,以躲開梯度消失的區域。

2、使用relu代替sigmoid和tanh作為啟用函式。原理請參考上一篇文章《零基礎入門深度學習 | 第四章:卷積神經網路》的啟用函式一節。

3、使用其他結構的RNNs,比如長短時記憶網路(LTSM)和Gated Recurrent Unit(GRU),這是最流行的做法。我們將在以後的文章中介紹這兩種網路。

RNN的應用舉例—基於RNN的語言模型

現在,我們介紹一下基於RNN語言模型。我們首先把詞依次輸入到迴圈神經網路中,每輸入一個詞,迴圈神經網路就輸出截止到目前為止,下一個最可能的詞。例如,當我們依次輸入:

我 昨天 上學 遲到 了

神經網路的輸出如下圖所示:

其中,s和e是兩個特殊的詞,分別表示一個序列的開始和結束。

向量化

我們知道,神經網路的輸入和輸出都是向量,為了讓語言模型能夠被神經網路處理,我們必須把詞表達為向量的形式,這樣神經網路才能處理它。

神經網路的輸入是詞,我們可以用下面的步驟對輸入進行向量化:

1、建立一個包含所有詞的詞典,每個詞在詞典裡面有一個唯一的編號。

2、任意一個詞都可以用一個N維的one-hot向量來表示。其中,N是詞典中包含的詞的個數。假設一個詞在詞典中的編號是i,v是表示這個詞的向量,Vj是向量的第j個元素,則:

上面這個公式的含義,可以用下面的圖來直觀的表示:

使用這種向量化方法,我們就得到了一個高維、稀疏的向量(稀疏是指絕大部分元素的值都是0)。處理這樣的向量會導致我們的神經網路有很多的引數,帶來龐大的計算量。因此,往往會需要使用一些降維方法,將高維的稀疏向量轉變為低維的稠密向量。不過這個話題我們就不再這篇文章中討論了。

語言模型要求的輸出是下一個最可能的詞,我們可以讓迴圈神經網路計算計算詞典中每個詞是下一個詞的概率,這樣,概率最大的詞就是下一個最可能的詞。因此,神經網路的輸出向量也是一個N維向量,向量中的每個元素對應著詞典中相應的詞是下一個詞的概率。如下圖所示:

Softmax層

前面提到,語言模型是對下一個詞出現的概率進行建模。那麼,怎樣讓神經網路輸出概率呢?方法就是用softmax層作為神經網路的輸出層。

我們先來看一下softmax函式的定義:

這個公式看起來可能很暈,我們舉一個例子。Softmax層如下圖所示:

從上圖我們可以看到,softmax layer的輸入是一個向量,輸出也是一個向量,兩個向量的維度是一樣的(在這個例子裡面是4)。輸入向量x=[1 2 3 4]經過softmax層之後,經過上面的softmax函式計算,轉變為輸出向量y=[0.03 0.09 0.24 0.64]。計算過程為:

我們來看看輸出向量y的特徵:

1、每一項為取值為0-1之間的正數;

2、所有項的總和是1。

我們不難發現,這些特徵和概率的特徵是一樣的,因此我們可以把它們看做是概率。對於語言模型來說,我們可以認為模型預測下一個詞是詞典中第一個詞的概率是0.03,是詞典中第二個詞的概率是0.09,以此類推。

語言模型的訓練

可以使用監督學習的方法對語言模型進行訓練,首先,需要準備訓練資料集。接下來,我們介紹怎樣把語料

我 昨天 上學 遲到 了

轉換成語言模型的訓練資料集。

首先,我們獲取輸入-標籤對:

輸入

標籤

s

昨天

昨天

上學

上學

遲到

遲到

e

然後,使用前面介紹過的向量化方法,對輸入x和標籤y進行向量化。這裡面有意思的是,對標籤y進行向量化,其結果也是一個one-hot向量。例如,我們對標籤『我』進行向量化,得到的向量中,只有第2019個元素的值是1,其他位置的元素的值都是0。它的含義就是下一個詞是『我』的概率是1,是其它詞的概率都是0。

最後,我們使用交叉熵誤差函式作為優化目標,對模型進行優化。

在實際工程中,我們可以使用大量的語料來對模型進行訓練,獲取訓練資料和訓練的方法都是相同的。

交叉熵誤差

一般來說,當神經網路的輸出層是softmax層時,對應的誤差函式E通常選擇交叉熵誤差函式,其定義如下:

在上式中,N是訓練樣本的個數,向量yn是樣本的標記,向量On是網路的輸出。標記yn是一個one-hot向量,例如y1=[1,0,0,0],如果網路的輸出o=[0.03,0.09,0.24,0.64],那麼,交叉熵誤差是(假設只有一個訓練樣本,即N=1):

我們當然可以選擇其他函式作為我們的誤差函式,比如最小平方誤差函式(MSE)。不過對概率進行建模時,選擇交叉熵誤差函式更make sense。具體原因,感興趣的讀者請閱讀參考文獻7。

RNN的實現

完整程式碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/rnn.py (python2.7)

為了加深我們對前面介紹的知識的理解,我們來動手實現一個RNN層。我們複用了上一篇文章零基礎入門深度學習(4) - 卷積神經網路中的一些程式碼,所以先把它們匯入進來。

1、import numpy as np

2、from cnn import ReluActivator, IdentityActivator, element_wise_op

我們用RecurrentLayer類來實現一個迴圈層。下面的程式碼是初始化一個迴圈層,可以在建構函式中設定卷積層的超引數。我們注意到,迴圈層有兩個權重陣列,U和W。

class RecurrentLayer(object):
    def __init__(self, input_width, state_width,
                 activator, learning_rate):
        self.input_width = input_width
        self.state_width = state_width
        self.activator = activator
        self.learning_rate = learning_rate
        self.times = 0       # 當前時刻初始化為t0
        self.state_list = [] # 儲存各個時刻的state
        self.state_list.append(np.zeros(
            (state_width, 1)))           # 初始化s0
        self.U = np.random.uniform(-1e-4, 1e-4,
            (state_width, input_width))  # 初始化U
        self.W = np.random.uniform(-1e-4, 1e-4,
            (state_width, state_width))  # 初始化W

在forward方法中,實現迴圈層的前向計算,這部分比較簡單。

    def forward(self, input_array):
        '''
        根據『式2』進行前向計算
        '''
        self.times += 1
        state = (np.dot(self.U, input_array) +
                 np.dot(self.W, self.state_list[-1]))
        element_wise_op(state, self.activator.forward)
        self.state_list.append(state)

在backword方法中,實現BPTT演算法。

    def backward(self, sensitivity_array, 
                 activator):
        '''
        實現BPTT演算法
        '''
        self.calc_delta(sensitivity_array, activator)
        self.calc_gradient()
    def calc_delta(self, sensitivity_array, activator):
        self.delta_list = []  # 用來儲存各個時刻的誤差項
        for i in range(self.times):
            self.delta_list.append(np.zeros(
                (self.state_width, 1)))
        self.delta_list.append(sensitivity_array)
        # 迭代計算每個時刻的誤差項
        for k in range(self.times - 1, 0, -1):
            self.calc_delta_k(k, activator)
    def calc_delta_k(self, k, activator):
        '''
        根據k+1時刻的delta計算k時刻的delta
        '''
        state = self.state_list[k+1].copy()
        element_wise_op(self.state_list[k+1],
                    activator.backward)
        self.delta_list[k] = np.dot(
            np.dot(self.delta_list[k+1].T, self.W),
            np.diag(state[:,0])).T
    def calc_gradient(self):
        self.gradient_list = [] # 儲存各個時刻的權重梯度
        for t in range(self.times + 1):
            self.gradient_list.append(np.zeros(
                (self.state_width, self.state_width)))
        for t in range(self.times, 0, -1):
            self.calc_gradient_t(t)
        # 實際的梯度是各個時刻梯度之和
        self.gradient = reduce(
            lambda a, b: a + b, self.gradient_list,
            self.gradient_list[0]) # [0]被初始化為0且沒有被修改過
    def calc_gradient_t(self, t):
        '''
        計算每個時刻t權重的梯度
        '''
        gradient = np.dot(self.delta_list[t],
            self.state_list[t-1].T)
        self.gradient_list[t] = gradient

有意思的是,BPTT演算法雖然數學推導的過程很麻煩,但是寫成程式碼卻並不複雜。

在update方法中,實現梯度下降演算法。

    def update(self):
        '''
        按照梯度下降,更新權重
        '''
        self.W -= self.learning_rate * self.gradient

上面的程式碼不包含權重U的更新。這部分實際上和全連線神經網路是一樣的,留給感興趣的讀者自己來完成吧。

迴圈層是一個帶狀態的層,每次forword都會改變迴圈層的內部狀態,這給梯度檢查帶來了麻煩。因此,我們需要一個reset_state方法,來重置迴圈層的內部狀態。

    def reset_state(self):
        self.times = 0       # 當前時刻初始化為t0
        self.state_list = [] # 儲存各個時刻的state
        self.state_list.append(np.zeros(
            (self.state_width, 1)))      # 初始化s0

最後,是梯度檢查的程式碼。

def gradient_check():
    '''
    梯度檢查
    '''
    # 設計一個誤差函式,取所有節點輸出項之和
    error_function = lambda o: o.sum()
    rl = RecurrentLayer(3, 2, IdentityActivator(), 1e-3)
    # 計算forward值
    x, d = data_set()
    rl.forward(x[0])
    rl.forward(x[1])
    # 求取sensitivity map
    sensitivity_array = np.ones(rl.state_list[-1].shape,
                                dtype=np.float64)
    # 計算梯度
    rl.backward(sensitivity_array, IdentityActivator())
    # 檢查梯度
    epsilon = 10e-4
    for i in range(rl.W.shape[0]):
        for j in range(rl.W.shape[1]):
            rl.W[i,j] += epsilon
            rl.reset_state()
            rl.forward(x[0])
            rl.forward(x[1])
            err1 = error_function(rl.state_list[-1])
            rl.W[i,j] -= 2*epsilon
            rl.reset_state()
            rl.forward(x[0])
            rl.forward(x[1])
            err2 = error_function(rl.state_list[-1])
            expect_grad = (err1 - err2) / (2 * epsilon)
            rl.W[i,j] += epsilon
            print 'weights(%d,%d): expected - actural %f - %f' % (
                i, j, expect_grad, rl.gradient[i,j])

需要注意,每次計算error之前,都要呼叫reset_state方法重置迴圈層的內部狀態。下面是梯度檢查的結果,沒問題!

小結

至此,我們講完了基本的迴圈神經網路、它的訓練演算法:BPTT,以及在語言模型上的應用。RNN比較燒腦,相信拿下前幾篇文章的讀者們搞定這篇文章也不在話下吧!然而,迴圈神經網路這個話題並沒有完結。我們在前面說到過,基本的迴圈神經網路存在梯度爆炸和梯度消失問題,並不能真正的處理好長距離的依賴(雖然有一些技巧可以減輕這些問題)。事實上,真正得到廣泛的應用的是迴圈神經網路的一個變體:長短時記憶網路。它內部有一些特殊的結構,可以很好的處理長距離的依賴,我們將在下一篇文章中詳細的介紹它。現在,讓我們稍事休息,準備挑戰更為燒腦的長短時記憶網路吧。

參考資料

1、RECURRENT NEURAL NETWORKS TUTORIAL

2、Understanding LSTM Networks

3、The Unreasonable Effectiveness of Recurrent Neural Networks

4、Attention and Augmented Recurrent Neural Networks

5、On the difficulty of training recurrent neural networks, Bengio et al.

6、Recurrent neural network based language model, Mikolov et al.

7、Neural Network Classification, Categorical Data, Softmax Activation, and Cross Entropy Error, McCaffrey