循環神經網絡(一般RNN)推導

本文章的例子來自於WILDML

vanillaRNN是相比於LSTMs和GRUs簡單的循環神經網絡,可以說是最簡單的RNN。

RNN結構

RNN結構

RNN的一個特點是所有的隱層共享參數(U,V,W) ,整個網絡只用這一套參數。

RNN前向傳導

st=tanh(Uxt+Wst1)
ot=softmax(Vst)

stt 時刻隱層的狀態值,爲向量。
ott 時刻輸出的值(這裏是輸入一個xt 就有一個輸出ot ,這個是不必要的,也可以在全部x輸入完之後開始輸出,根據具體應用來設計模型)

本文例子介紹:RNN語言模型

關於語言模型的介紹就不說了,是NLP基礎。這裏只說說輸入和輸出的內容。

語言模型的生成屬於無監督學習,只需要大量的文本即可生成。我們只需要做的是構造訓練數據。

構造過程:
1. 生成詞典vocab。(分詞、去掉低頻詞)
2. 將語料中的句子轉爲word_id序列,並在頭尾加上開始和結束id。
3. 生成訓練數據:對於每個句子,輸入爲前len(sent)-1的序列,輸出爲後len(sent)-1的序列(也就是輸入一個詞就預測下一個詞)

如,“我 在 沙灘 上 玩耍”輸入的向量爲[0,5,85,485,416,55] ,輸出的向量爲[5,85,485,416,55,1]

假設我們的詞彙有8000個,採用one-hot向量,則每個輸入xt 爲8000維,對應的位置爲1,其他爲0。隱層設置100個神經元。
則列出網絡所有參數和輸入輸出的shape,方便推導:
xtR8000
otR8000
stR100
UR100×8000
VR8000×100
WR100×100

總參數量爲2HC+H2 ,即1,610,000。

損失函數(loss function)採用交叉熵:
Et(yt,y^t)=ytlogy^t
E(y,y^)=tEt(yt,y^t)=tytlogy^t
其中yt 爲t時刻正確的詞語,y^t 爲t時刻預測的詞語。

反向傳播

反向傳播目的就是求預測誤差E 關於所有參數(U,V,W) 的梯度,即EUEVEW

如下圖所示,每個時刻t預測的詞都有相應的誤差,我們需要求這些誤差關於參數的所有梯度,最後進行參數的下降調整操作(由於目標是降低Loss function,所以是梯度下降,如果是目標是最大似然,則爲梯度上升)。
誤差生成

我們這裏以計算E3 關於參數的梯度爲例(其他Et 都需要計算):

E3V=E3y^3y^3V=E3y^3y^3z3z3V=(y^3y3)×s3

爲8000x100的向量,其中z3=Vs3 ,用到了softmax的求導公式。

可見關於V的梯度用不到上一層的狀態值,所以不需要累計。

BPTT(Backpropagation Through Time)

下面來求解關於W的梯度:
E3W=E3y^3y^3s3s3W

由於s3=tanh(Ux3+Ws2) 依賴s2 ,而s2 依賴Ws1 ,以此類推。
下圖爲鏈式關係:
鏈式關係
所以,

E3W=k=03E3y^3y^3s3s3skskW

可見由於W在所有隱層中共享,許多變量都依賴W,導致求導鏈變長,這就是BPTT的特點,將每層的影響都累計起來。

下圖爲各鏈接之間的導數,在所有層中不會改變,也體現了傳播的路徑。
誤差傳導

跟一般的反向傳播一樣,這裏也定義一個Delta 向量:
δ(3)2=E3s3s3s2s2z2
其中z2=Ux2+Ws1 ,在本例子中爲一個100x1的向量。

所以E3W 可以寫成:

E3W=k=03δ(3)kzkW

爲100x100的矩陣。

同理E3U 可以寫成:

E3U=k=03δ(3)kzkU

爲100x8000的矩陣。

至此,關於(U,V,W) 的梯度都求解完畢。

下面,用代碼來解釋這個過程會更加清晰明瞭:

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]

delta_o爲(yy^)RT×8000
從T-1時刻開始計算直到0時刻。

梯度消失問題

tanh函數及其導數的圖像:
tanh函數及其導數

可見tanh導數的值域是(0,1],兩端都非常平緩並趨於0。
再看我們的梯度公式:

E3W=k=03E3y^3y^3s3(j=k+13sjsj1)skW

sksk1 用的就是tanh導數,在訓練的後期,梯度會變得比較小,如果幾個趨於0的值相乘的話,乘積就會變得非常小,就會出現梯度消失現象。同樣的情況也會出現在sigmoid函數。
由於遠距離的時刻的梯度貢獻接近於0,因此很難學習到遠距離的依賴關係。

也很容易想象到當導數都很大的時候,就會出現梯度爆炸的情況,但是它的受重視程度不如梯度消失問題,原因有二:
1. 梯度爆炸很明顯,梯度值會變成NaN,程序會崩潰。
2. 用一個預定義值來裁剪梯度值是解決梯度爆炸的一個非常簡單實用的辦法,而梯度消失問題則很難解決。

幸好還有一些辦法來解決梯度消失問題。
1. 合適的參數初始化可以減少梯度消失的影響。
2. 使用ReLU激活函數
3. LSTM和GRU架構。

Reference

http://www.wildml.com/2015/10/recurrent-neural-networks-tutorial-part-3-backpropagation-through-time-and-vanishing-gradients/
http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-2-implementing-a-language-model-rnn-with-python-numpy-and-theano/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章