遞歸神經網絡(RNN)簡介


在此之前,我們已經學習了前饋網絡的兩種結構——多層感知器和卷積神經網絡,這兩種結構有一個特點,就是假設輸入是一個獨立的沒有上下文聯系的單位,比如輸入是一張圖片,網絡識別是狗還是貓。但是對於一些有明顯的上下文特征的序列化輸入,比如預測視頻中下一幀的播放內容,那么很明顯這樣的輸出必須依賴以前的輸入, 也就是說網絡必須擁有一定的”記憶能力”。為了賦予網絡這樣的記憶力,一種特殊結構的神經網絡——遞歸神經網絡(Recurrent Neural Network)便應運而生了。網上對於RNN的介紹多不勝數,這篇《Recurrent Neural Networks Tutorial》對於RNN的介紹非常直觀,里面手把手地帶領讀者利用python實現一個RNN語言模型,強烈推薦。為了不重復作者 Denny Britz的勞動,本篇將簡要介紹RNN,並強調RNN訓練的過程與多層感知器的訓練差異不大(至少比CNN簡單),希望能給讀者一定的信心——只要你理解了多層感知器,理解RNN便不是事兒:-)。

RNN的基本結構
首先有請讀者看看我們的遞歸神經網絡的容貌:

乍一看,好復雜的大家伙,沒事,老樣子,看我如何慢慢將其拆解,正所謂見招拆招,我們來各個擊破。
上圖左側是遞歸神經網絡的原始結構,如果先拋棄中間那個令人生畏的閉環,那其實就是簡單”輸入層=>隱藏層=>輸出層”的三層結構,我們在多層感知器的介紹中已經非常熟悉,然而多了一個非常陌生的閉環,也就是說輸入到隱藏層之后,隱藏層還會給自己也來一發,環環相扣,暈亂復雜。
我們知道,一旦有了環,就會陷入“先有蛋還是先有雞”的邏輯困境,為了跳出困境我們必須人為定義一個起始點,按照一定的時間序列規定好計算順序,做到有條不紊,於是實際上我們會將這樣帶環的結構展開成一個序列網絡,也就是上圖右側被“unfold”之后的結構。先別急着能理解RNN,我們來點輕松的,先介紹這樣的序列化網絡結構包含的參數記號:

網絡某一時刻的輸入xtxt,和之前介紹的多層感知器的輸入一樣,xtxt是一個nn維向量,不同的是遞歸網絡的輸入將是一整個序列,也就是x=[x1,...,xt−1,xt,xt+1,...xT]x=[x1,...,xt−1,xt,xt+1,...xT],對於語言模型,每一個xtxt將代表一個詞向量,一整個序列就代表一句話。
htht代表時刻tt的隱藏狀態
otot代表時刻tt的輸出
輸入層到隱藏層直接的權重由UU表示,它將我們的原始輸入進行抽象作為隱藏層的輸入
隱藏層到隱藏層的權重WW,它是網絡的記憶控制者,負責調度記憶。
隱藏層到輸出層的權重VV,從隱藏層學習到的表示將通過它再一次抽象,並作為最終輸出。
RNN的Forward階段
上一小節我們簡單了解了網絡的結構,並介紹了其中一些記號,是時候介紹它具體的運作過程了。首先在t=0t=0的時刻,U,V,WU,V,W都被隨機初始化好,h0h0通常初始化為0,然后進行如下計算:
s1=Ux1+Wh0h1=f(s1)o1=g(Vh1)
s1=Ux1+Wh0h1=f(s1)o1=g(Vh1)
這樣時間就向前推進,此時的狀態h1h1作為時刻0的記憶狀態將參與下一次的預測活動,也就是
s2=Ux2+Wh1h2=f(s2)o2=g(Vh2)
s2=Ux2+Wh1h2=f(s2)o2=g(Vh2)
,以此類推
st=Uxt+Wht−1ht=f(Uxt+Wht−1)ot=g(Vht)
st=Uxt+Wht−1ht=f(Uxt+Wht−1)ot=g(Vht)
其中ff可以是tanh,relu,logistictanh,relu,logistic任君選擇,gg通常是softmaxsoftmax也可以是其他,也是隨君所欲。
值得注意的是,我們說遞歸神經網絡擁有記憶能力,而這種能力就是通過WW將以往的輸入狀態進行總結,而作為下次輸入的輔助。可以這樣理解隱藏狀態:
h=f(現有的輸入+過去記憶總結)
h=f(現有的輸入+過去記憶總結)
RNN的Backward階段
上一小節我們說到了RNN如何做序列化預測,也就是如何一步步預測出o1,o2,....ot−1,ot,ot+1.....o1,o2,....ot−1,ot,ot+1.....,接下來我們來了解網絡的知識U,V,WU,V,W是如何煉成的。
其實沒有多大新意,我們還是利用在之前講解多層感知器和卷積神經網絡用到的backpropagation方法。也就是將輸出層的誤差CostCost,求解各個權重的梯度∇U,∇V,∇W∇U,∇V,∇W,然后利用梯度下降法更新各個權重。現在問題就是如何求解各個權重的梯度,其它的所有東西都在之前介紹中談到了,所有的trick都可以復用。
由於是序列化預測,那么對於每一時刻tt,網絡的輸出otot都會產生一定誤差etet,誤差的選擇任君喜歡,可以是cross entropy也可以是平方誤差等等。那么總的誤差為E=∑tetE=∑tet,我們的目標就是要求取
∇U=∂E∂U=∑t∂et∂U∇V=∂E∂V=∑t∂et∂V∇W=∂E∂W=∑t∂et∂W
∇U=∂E∂U=∑t∂et∂U∇V=∂E∂V=∑t∂et∂V∇W=∂E∂W=∑t∂et∂W
我們知道輸出ot=g(Vst)ot=g(Vst),對於任意的CostCost函數,求取∇V∇V將是簡單的,我們可以直接求取每個時刻的∂et∂V∂et∂V,由於它不存在和之前的狀態依賴,可以直接求導取得,然后簡單地求和即可。我們重點關注∇W,∇U∇W,∇U的計算。
回憶之前我們介紹多層感知器的backprop算法,我們知道算法的trick是定義一個δ=∂e∂sδ=∂e∂s,首先計算出輸出層的δLδL,再向后傳播到各層δL−1,δL−2,....δL−1,δL−2,....,那么如何計算δδ呢?先看下圖:

之前我們推導過,只要關注當前層次發射出去的鏈接即可,也就是
δht=(VTδot+WTδht+1).∗f′(st)
δth=(VTδto+WTδt+1h).∗f′(st)

只要計算出所有的δot,δhtδto,δth,就可以通過以下計算出∇W,∇U∇W,∇U:
∇W=∑t⎡⎣⎢⎢⎢⎢⎢⎢⎢δh0,th0,t−1,...,δh0,thi,t−1,...,δh0,thm,t−1...δhj,th0,t−1,...,δhj,thi,t−1,...,δhj,thm,t−1...δhn,th0,t−1,...,δhn,thi,t−1,...,δhn,thm,t−1⎤⎦⎥⎥⎥⎥⎥⎥⎥=∑tδht×ht−1∇U=∑t⎡⎣⎢⎢⎢⎢⎢⎢⎢δh0,tx0,t,...,δh0,txi,t,...,δh0,txm,t...δhj,tx0,t,...,δhj,txi,t,...,δhj,txm,t...δhn,tx0,t,...,δhn,txi,t,...,δhn,txm,t⎤⎦⎥⎥⎥⎥⎥⎥⎥=∑tδht×xt
∇W=∑t[δ0,thh0,t−1,...,δ0,thhi,t−1,...,δ0,thhm,t−1...δj,thh0,t−1,...,δj,thhi,t−1,...,δj,thhm,t−1...δn,thh0,t−1,...,δn,thhi,t−1,...,δn,thhm,t−1]=∑tδth×ht−1∇U=∑t[δ0,thx0,t,...,δ0,thxi,t,...,δ0,thxm,t...δj,thx0,t,...,δj,thxi,t,...,δj,thxm,t...δn,thx0,t,...,δn,thxi,t,...,δn,thxm,t]=∑tδth×xt

其中××表示兩個向量的外積。這樣看來,只要你熟悉MLP的backprop算法,RNN寫起程序來和MLP根本沒有多大差異!手寫naive的demo至少比CNN容易很多。
RNN的訓練困難
雖然上一節中,我們強調了RNN的訓練程序和MLP沒太大差異,雖然寫程序容易,但是訓練起來卻是千難萬阻。為什么呢?因為我們的網絡是根據輸入而展開的,輸入越長,展開的網絡越深,那么對於“深度”網絡訓練有什么困難呢?最常見的是“gradient explode”和“gradient vanish”。這種問題在RNN中如何體現呢?為了強調這個問題,我們模仿Yoshua Bengio的論文《On the difficulty of training recurrent neural networks》的推導,重寫一下RNN的梯度求解過程,為了推導方便,我們人為地為W,UW,U打上標簽Wt,UtWt,Ut,即認為當確定好時間長度TT,RNN就變成普通的MLP。打上標簽后的RNN變成如下:

假如對於時刻t+1t+1產生的誤差et+1et+1,我們想計算它對於W1,W2,....,Wt,Wt+1W1,W2,....,Wt,Wt+1的梯度,可以如下計算:
∂et+1∂Wt+1=∂et+1∂ht+1∂ht+1∂Wt+1
∂et+1∂Wt+1=∂et+1∂ht+1∂ht+1∂Wt+1
∂et+1∂Wt=∂et+1∂ht+1∂ht+1∂ht∂ht∂Wt
∂et+1∂Wt=∂et+1∂ht+1∂ht+1∂ht∂ht∂Wt
∂et+1∂Wt−1=∂et+1∂ht+1∂ht+1∂ht∂ht∂ht−1∂ht−1∂Wt−1
∂et+1∂Wt−1=∂et+1∂ht+1∂ht+1∂ht∂ht∂ht−1∂ht−1∂Wt−1
......
......

反復運用鏈式法則,我們可以求出每一個∇W1,∇W2,....,∇Wt,∇Wt+1∇W1,∇W2,....,∇Wt,∇Wt+1,需要注意的是,實際RNN模型對於W,UW,U都是不打標簽的,也就是在不同時刻都是共享同樣的參數,這樣可以大大減少訓練參數,和CNN的共享權重類似。對於共享參數的RNN,我們只需將上述的一系列式子抹去標簽並求和,就可以得到Yoshua Bengio論文中所推導的梯度計算式子:
∂et∂W=∑1≤k≤t∂et∂ht∏k<i≤t∂hi∂hi−1∂+hk∂W
∂et∂W=∑1≤k≤t∂et∂ht∏k<i≤t∂hi∂hi−1∂+hk∂W

其中∂+hk∂W∂+hk∂W代表不利用鏈式法則直接求導,也就是假如對於函數f(h(x))f(h(x)),對其直接求導結果如下:
∂f(h(x))∂x=f′(h(x))
∂f(h(x))∂x=f′(h(x))
也就是將h(x)h(x)看成常數了。網上許多RNN教程都用Yoshua Bengio類似的推導,卻省略了這個小步驟,使得初學者常常搞得暈頭轉向,摸不着頭腦。論文中證明了:
||∏k<i≤t∂hi∂hi−1||≤ηt−k
||∏k<i≤t∂hi∂hi−1||≤ηt−k
從而說明了這是梯度求導的一部分環節是一個指數模型,當η<1η<1時,就會出現”gradient vanish”問題,而當η>1η>1時,“gradient explode”也就產生了。
為了克服”gradient vanish”的問題,LSTM和GRU模型便后續被推出了,為什么LSTM和GRU可以克服gradient vanish問題呢?由於它們都有特殊的方式存儲”記憶”,那么以前gradient比較大的”記憶”不會像簡單的RNN一樣馬上被抹除,因此可以一定程度上克服gradient vanish問題。
另一個簡單的技巧可以用來克服gradient explode的問題就是gradient clipping,也就是當你計算的gradient超過閾值cc的或者小於閾值−c−c時候,便把此時的gradient設置成cc或−c−c。這種trick的表現形式如下圖虛線所示:

上圖所示是RNN的Error Sufface,可以看到RNN的Error Sufface要么非常陡峭,要么非常平坦,如果不采取任何措施,當你的參數在某一次更新之后,剛好碰到陡峭的地方,此時gradient變得非常大,那么你的參數更新也會非常大,很容易導致震盪問題。而如果你采取了gradient clipping這個技巧,那么即使你不幸碰到陡峭的地方,gradient也不會explode,因為被你限制在某個閾值cc。
有趣的是,正是因為訓練深度網絡的困難,才導致神經網絡這種古老模型沉寂了幾十年,不過現在硬件的發展,訓練數據的增多,神經網絡重新得以復蘇,並以重新以深度學習的外號殺出江湖。
參考引用
《Recurrent Neural Networks Tutorial》
《On the difficulty of training recurrent neural networks》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM