RNN(循環神經網絡)是一種具備長時記憶能力的神經網絡模型,被普遍用於序列標註問題。一個典型的RNN結構圖以下所示:算法
從圖中能夠看到,一個RNN一般由三小層組成,分別是輸入層、隱藏層和輸出層。與通常的神經網絡不一樣的是,RNN的隱藏層存在一條有向反饋邊,正是這種反饋機制賦予了RNN記憶能力。要理解左邊的圖可能有點難度,咱們將其展開成右邊的這種更加直觀的形式,其中RNN的每一個神經元接受當前時刻的輸入$x_t$以及上一時刻隱單元的輸出$s_{t-1}$,計算出當前神經元的輸入$s_t$。三個權重矩陣$U$, $V$和$W$就是要經過梯度降低來擬合的參數。整個優化過程叫作BPTT(BackPropagation Through Time, BPTT)。網絡
形式化以下:架構
$$ { s }_{ t }=\tanh \left( U{ x }_{ t }+W{ s }_{ t-1 } \right) \\ { \hat { y } }_{ t }=softmax\left( V{ s }_{ t } \right) \tag{1}$$ide
一樣地,定義交叉熵損失函數以下:函數
$$ { E }_{ t }\left( { y }_{ t },{ \hat { y } }_{ t } \right) =-{ y }_{ t }log{ \hat { y } }_{ t }\\ { E\left( y,\hat { y } \right) }=\sum _{ t }^{ }{ { E }_{ t }\left( { y }_{ t },{ \hat { y } }_{ t } \right) } \\ =-\sum _{ t }^{ }{ { y }_{ t }log{ \hat { y } }_{ t } } \tag{2}$$學習
下面咱們將舉個具體的例子。 優化
咱們的目標是經過梯度降低來擬合參數矩陣$U$, $V$和$W$。如同求損失時的加和,有$\frac { \partial E }{ \partial W } =\sum _{ t }^{ }{ \frac { \partial { E }_{ t } }{ \partial W } } $。spa
爲了計算這些梯度,咱們使用鏈式法則。咱們將以$E_3$爲例,作以下推導。設計
$$ \frac { \partial { E }_{ 3 } }{ \partial V } =\frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial V } \\ =\frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial { z }_{ 3 } } \frac { \partial { z }_{ 3 } }{ \partial V } \\ =\left( { \hat { y } }_{ 3 }-{ y }_{ 3 } \right) \otimes { s }_{ 3 } \tag{3}$$3d
在上面式子中,$z_3=V s_3$,$\otimes$表示兩個向量的外積。對$V$的偏導是簡單的,由於$t=3$時間步的對$V$的偏導只與${ \hat { y } }_{ 3 }$,$y_3$和$s_3$有關。可是,對於$\frac { \partial { E }_{ 3 } }{ \partial W }$就沒有這麼簡單了,如圖:
推導過程以下:
$$ \frac { \partial { E }_{ 3 } }{ \partial W } =\frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial { s }_{ 3 } } \frac { \partial { s }_{ 3 } }{ \partial W } \\ =\sum _{ k=0 }^{ 3 }{ \frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial { s }_{ 3 } } \frac { \partial { s }_{ 3 } }{ \partial { s }_{ k } } \frac { \partial { s }_{ k } }{ \partial W } } \tag{4} $$
上式中,咱們能夠看到,這與標準的BP算法並沒有太多不一樣,惟一的區別在於須要對各時間步求和。這也是標準RNN難以訓練的緣由:序列(句子)可能很長,多是20個字或更多,所以須要反向傳播多個層。在實踐中,許多人將時間步進行截斷來控制傳播層數。
BPTT實現的代碼以下:
def bptt(self, x, y): T = len(y) # Perform forward propagation o, s = self.forward_propagation(x) # We accumulate the gradients in these variables dLdU = np.zeros(self.U.shape) dLdV = np.zeros(self.V.shape) dLdW = np.zeros(self.W.shape) delta_o = o delta_o[np.arange(len(y)), y] -= 1. # For each output backwards... for t in np.arange(T)[::-1]: dLdV += np.outer(delta_o[t], s[t].T) # Initial delta calculation: dL/dz delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2)) # Backpropagation through time (for at most self.bptt_truncate steps) for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]: # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step) # Add to gradients at each previous step dLdW += np.outer(delta_t, s[bptt_step-1]) dLdU[:,x[bptt_step]] += delta_t # Update delta for next step dL/dz at t-1 delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2) return [dLdU, dLdV, dLdW]
標準RNN難以學習到文本的上下文依賴,例如「The man who wore a wig on his head went inside」,句子要表達的是帶着假髮的男人進去了而不是假髮進去了,這一點對於標準RNN的訓練很難。爲了理解這個問題,咱們先看看上面的式子:
$$ \frac { \partial { E }_{ 3 } }{ \partial W } =\sum _{ k=0 }^{ 3 }{ \frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial { s }_{ 3 } } \frac { \partial { s }_{ 3 } }{ \partial { s }_{ k } } \frac { \partial { s }_{ k } }{ \partial W } } \tag{5}$$
注意,其中的$\frac { \partial { s }_{ 3 } }{ \partial { s }_{ k } } $仍然包含着鏈式法則,例如$\frac { \partial { s }_{ 3 } }{ \partial { s }_{ 1 } } =\frac { \partial { s }_{ 3 } }{ \partial { s }_{ 2 } } \frac { \partial { s }_{ 2 } }{ \partial { s }_{ 1 } } $。
因此上面的式子(5)能夠重寫爲式子(6),即逐點導數的雅克比矩陣:
$$ \frac { \partial { E }_{ 3 } }{ \partial W } =\sum _{ k=0 }^{ 3 }{ \frac { \partial { E }_{ 3 } }{ \partial { \hat { y } }_{ 3 } } \frac { \partial { \hat { y } }_{ 3 } }{ \partial { s }_{ 3 } } \left( \prod _{ j=k+1 }^{ 3 }{ \frac { \partial { s }_{ j } }{ \partial { s }_{ j-1 } } } \right) \frac { \partial { s }_{ k } }{ \partial W } } \tag{6} $$
而tanh函數和其導數圖像以下:
可見,tanh函數(sigmoid函數也不例外)的兩端都有接近0的導數。當出現這種狀況時,咱們認爲相應的神經元已經飽和。參數矩陣將以指數方式快速收斂到0,最終在幾個時間步後徹底消失。來自「遙遠」的時間步的權重迅速爲0,從而不會對如今的學習狀態產生貢獻:學不到遠處上下文依賴。
很容易想象,根據咱們的激活函數和網絡參數,若是雅可比矩陣的值很大,將會產生梯度爆炸。首先,梯度爆炸是顯而易見的,權重將漸變爲NaN(不是數字),程序將崩潰。其次,將梯度剪切到預約義的閾值是一種很是簡單有效的梯度爆炸解決方案。固然,梯度消失問題影響更加惡劣,由於要知道它們什麼時候發生或如何處理它們並不簡單。
目前,已經有幾種方法能夠解決梯度消失問題。正確初始化$W$矩陣能夠減小消失梯度的影響。正規化也是如此。更優選的解決方案是使用Relu代替tanh或S形激活函數。ReLU導數是0或1的常數,所以不太可能遇到梯度消失。更流行的解決方案是使用長短時間記憶單元(LSTM)或門控循環單元(GRU)架構。LSTM最初是在1997年提出的,也是今天NLP中使用最普遍的模型。GRU,最初於2014年提出,是LSTM的簡化版本。這兩種RNN架構都明確地設計用於處理梯度消失並有效地學習遠程依賴性。
參考英文博客:http://www.wildml.com/2015/10/recurrent-neural-networks-tutorial-part-3-backpropagation-through-time-and-vanishing-gradients/