但不幸的一面是,lstm的結構很複雜,是以,我們需要花上一些力氣,才能把lstm以及它的訓練算法弄明白。在搞清楚lstm之後,我們再介紹一種lstm的變體:gru (gated recurrent unit)。 它的結構比lstm簡單,而效果卻和lstm一樣好,是以,它正在逐漸流行起來。最後,我們仍然會動手實作一個lstm。
長短時記憶網絡是啥

梯度消失到底意味着什麼?在《零基礎入門深度學習(4):循環神經網絡》中我們已證明,權重數組w最終的梯度是各個時刻的梯度之和,即:
假設某輪訓練中,各時刻的梯度以及最終的梯度之和如下圖:
我們就可以看到,從上圖的t-3時刻開始,梯度已經幾乎減少到0了。那麼,從這個時刻開始再往之前走,得到的梯度(幾乎為零)就不會對最終的梯度值有任何貢獻,這就相當于無論t-3時刻之前的網絡狀态h是什麼,在訓練中都不會對權重數組w的更新産生影響,也就是網絡事實上已經忽略了t-3時刻之前的狀态。這就是原始rnn無法處理長距離依賴的原因。
既然找到了問題的原因,那麼我們就能解決它。從問題的定位到解決,科學家們大概花了7、8年時間。終于有一天,hochreiter和schmidhuber兩位科學家發明出長短時記憶網絡,一舉解決這個問題。
其實,長短時記憶網絡的思路比較簡單。原始rnn的隐藏層隻有一個狀态,即h,它對于短期的輸入非常敏感。那麼,假如我們再增加一個狀态,即c,讓它來儲存長期的狀态,那麼問題不就解決了麼?如下圖所示:
新增加的狀态c,稱為單元狀态(cell state)。我們把上圖按照時間次元展開:
lstm的關鍵,就是怎樣控制長期狀态c。在這裡,lstm的思路是使用三個控制開關。第一個開關,負責控制繼續儲存長期狀态c;第二個開關,負責控制把即時狀态輸入到長期狀态c;第三個開關,負責控制是否把長期狀态c作為目前的lstm的輸出。三個開關的作用如下圖所示:
接下來,我們要描述一下,輸出h和單元狀态c的具體計算方法。
長短時記憶網絡的前向計算
前面描述的開關是怎樣在算法中實作的呢?這就用到了門(gate)的概念。門實際上就是一層全連接配接層,它的輸入是一個向量,輸出是一個0到1之間的實數向量。假設w是門的權重向量,是偏置項,那麼門可以表示為:
我們先來看一下遺忘門:
下圖顯示了遺忘門的計算:
接下來看看輸入門:
上式中,wi是輸入門的權重矩陣,bi是輸入門的偏置項。下圖表示了輸入門的計算:
下圖表示輸出門的計算:
lstm最終的輸出,是由輸出門和單元狀态共同确定的:
下圖表示lstm最終輸出的計算:
式1到式6就是lstm前向計算的全部公式。至此,我們就把lstm前向計算講完了。
長短時記憶網絡的訓練
熟悉我們這個系列文章的同學都清楚,訓練部分往往比前向計算部分複雜多了。lstm的前向計算都這麼複雜,那麼,可想而知,它的訓練算法一定是非常非常複雜的。現在隻有做幾次深呼吸,再一頭紮進公式海洋吧。
lstm訓練算法架構
lstm的訓練算法仍然是反向傳播算法,對于這個算法,我們已經非常熟悉了。主要有下面三個步驟:
關于公式和符号的說明
首先,我們對推導中用到的一些公式、符号做一下必要的說明。
接下來的推導中,我們設定gate的激活函數為sigmoid函數,輸出的激活函數為tanh函數。他們的導數分别為:
從上面可以看出,sigmoid和tanh函數的導數都是原函數的函數。這樣,我們一旦計算原函數的值,就可以用它來計算出導數的值。
誤差項沿時間的反向傳遞
下面,我們要把式7中的每個偏導數都求出來。根據式6,我們可以求出:
根據式4,我們可以求出:
因為:
我們很容易得出:
将上述偏導數帶入到式7,我們得到:
式8到式12就是将誤差沿時間反向傳播一個時刻的公式。有了它,我們可以寫出将誤差項向前傳遞到任意k時刻的公式:
将誤差項傳遞到上一層
我們假設目前為第l層,定義l-1層的誤差項是誤差函數對l-1層權重輸入的導數,即:
式14就是将誤差傳遞到上一層的公式。
權重梯度的計算
對于
我們已經求得了誤差項
,很容易求出t時刻的
:
将各個時刻的梯度加在一起,就能得到最終的梯度:
對于偏置項
的梯度,也是将各個時刻的梯度加在一起。下面是各個時刻的偏置項梯度:
下面是最終的偏置項梯度,即将各個時刻的偏置項梯度加在一起:
的權重梯度,隻需要根據相應的誤差項直接計算即可:
以上就是lstm的訓練算法的全部公式。因為這裡面存在很多重複的模式,仔細看看,會發覺并不是太複雜。
當然,lstm存在着相當多的變體,讀者可以在網際網路上找到很多資料。因為大家已經熟悉了基本lstm的算法,是以了解這些變體比較容易,是以本文就不再贅述了。
長短時記憶網絡的實作
在下面的實作中,lstmlayer的參數包括輸入次元、輸出次元、隐藏層次元,單元狀态次元等于隐藏層次元。gate的激活函數為sigmoid函數,輸出的激活函數為tanh。
激活函數的實作
我們先實作兩個激活函數:sigmoid和tanh。
lstm初始化
和前兩篇文章代碼架構一樣,我們把lstm的實作放在lstmlayer類中。
根據lstm前向計算和方向傳播算法,我們需要初始化一系列矩陣和向量。這些矩陣和向量有兩類用途,一類是用于儲存模型參數,例如
;另一類是儲存各種中間計算結果,以便于反向傳播算法使用,它們包括
,以及各個權重對應的梯度。
在構造函數的初始化中,隻初始化了與forward計算相關的變量,與backward相關的變量沒有初始化。這是因為構造lstm對象的時候,我們還不知道它未來是用于訓練(既有forward又有backward)還是推理(隻有forward)。
前向計算的實作
forward方法實作了lstm的前向計算:
從上面的代碼我們可以看到,門的計算都是相同的算法,而門和的計算僅僅是激活函數不同。是以我們提出了calc_gate方法,這樣減少了很多重複代碼。
反向傳播算法的實作
backward方法實作了lstm的反向傳播算法。需要注意的是,與backword相關的内部狀态變量是在調用backward方法之後才初始化的。這種延遲初始化的一個好處是,如果lstm隻是用來推理,那麼就不需要初始化這些變量,節省了很多記憶體。
算法主要分成兩個部分,一部分使計算誤差項:
另一部分是計算梯度:
梯度下降算法的實作
下面是用梯度下降算法來更新權重:
梯度檢查的實作
和recurrentlayer一樣,為了支援梯度檢查,我們需要支援重置内部狀态:
最後,是梯度檢查的代碼:
我們隻對做了檢查,讀者可以自行增加對其他梯度的檢查。下面是某次梯度檢查的結果:
gru
前面我們講了一種普通的lstm,事實上lstm存在很多變體,許多論文中的lstm都或多或少的不太一樣。在衆多的lstm變體中,gru (gated recurrent unit)也許是最成功的一種。它對lstm做了很多簡化,同時卻保持着和lstm相同的效果。是以,gru最近變得越來越流行。
gru對lstm做了兩個大改動:
将輸入門、遺忘門、輸出門變為兩個門:更新門(update gate)zt和重置門(reset gate)rt。
将單元狀态與輸出合并為一個狀态:h。
gru的前向計算公式為:
下圖是gru的示意圖:
gru的訓練算法比lstm簡單一些,留給讀者自行推導,本文就不再贅述了。
小結
至此,lstm——也許是結構最複雜的一類神經網絡——就講完了,相信拿下前幾篇文章的讀者們搞定這篇文章也不在話下吧!現在我們已經了解循環神經網絡和它最流行的變體——lstm,它們都可以用來處理序列。但是,有時候僅僅擁有處理序列的能力還不夠,還需要處理比序列更為複雜的結構(比如樹結構),這時候就需要用到另外一類網絡:遞歸神經網絡(recursive neural network),巧合的是,它的縮寫也是rnn。
原文釋出時間為:2011-01-20
本文來自雲栖社群合作夥伴dbaplus