Language Model and Recurrent Neural Networks (二)

本文是我去年十月份在公司的團隊技術分享會裏面分享過的內容,分享這個內容的初衷是我發現自己對RNN(本文均指Recurrent Neural Networks而非Recursive Neural Networks)比較陌生,想找個時間攻克一下,以便以後有此類工作需求可以快速上手。另外本文加入了CS224N關於語言模型和RNN的課堂內容。因此本文屬於科普性質的文章,基本上RNN的細節會涉及到,但並非每個細節都會深入去研究。
因爲篇幅較長,所以重新編輯了一下,把標題提到的兩部分分開了,本文是RNN部分。


RNN淺析

1.神經網絡種類

相信我們之中大部分人,接觸神經網路都是從前饋神經網絡開始的,在我印象中,以前大多數時候都被叫做BP神經網絡或者人工神經網絡,當然隨着深度學習的發展,這些稱呼基本上已經消失了。

Karpathy把神經網絡按輸入輸出的數量或者映射關係歸納成幾種類型:

圖一 神經網絡種類

One-to-One這種類型應該是最常見的,譬如Wide&Deep之類的對單個輸入數據產生一種輸出的網絡,又或者AutoEncoder,都屬於此類。One-to-many可能稍微少見一點,比較典型的應用應該是Image Captioning,以圖像作爲輸入,接着生成若干詞的文本。Many-to-one是以不定長的序列作爲輸入,返回固定的內容,例如文本分類、情感分析。Many-to-many分兩種情況,一個是非對其的,例如機器翻譯,不同目標語言翻譯出來的詞數和順序都不一定跟原語言相同;另一種是對其的情形,這種就是序列標註的相關應用了,需要對每個詞產生對應的tag,例如詞性標註、命名實體識別、分詞等等。

2.循環神經網絡

因爲通常它的輸入和輸出是固定size的,並且前饋神經網絡對歷史信息沒有記憶,所以普通的前饋神經網絡並不擅長處理序列類型的數據。RNN在這方面做了專門的改進:

圖二 循環神經網絡

圖中等號左邊是RNN的結構,右邊是攤開(unroll)之後的樣子。注意:這裏只是邏輯上或者流程上的“展開”,而非物理上的展開,因此圖中每一個A是一模一樣的東西,並非每個時間步上都有一個獨立的RNN

不難看出,RNN的每個time step需要以上期的狀態和本期輸入統一作爲輸入值,也即,每一期的計算都綜合了當前和歷史的信息,而不是固定的幾個上下文信息。圖中的A代表RNN的cell,我們可以先觀察一下里面的結構:

圖三 循環神經網絡內部結構

接着我們用數學描述一下這個過程:

ht=tanh(Win[ht1,xt]+bin) h_{t} = tanh(W_{in}\cdot{[h_{t-1},x_{t}]}+b_{in})

上式表示ht1h_{t-1}xtx_t先拼接起來,再乘以權值加上偏置,這個過程其實跟前饋神經網絡沒什麼區別。根本區別就在於,RNN的每個cell的輸出都會copy一份傳遞到下一個時間步,而下一個時間步又重複這樣的操作。當然,每一步也可以根據實際場景的需要增加一個輸出層的計算

y^t=f(Woutht+bout) \hat{y}_{t} = f(W_{out}\cdot{h_{t}}+b_{out})

這裏囉嗦一句,很多深度學習框架都有unit這個概念,並且RNN的相關API中都要用戶傳入unit的數量,那麼unit是什麼?據我參考各個文檔得出來的結果是,unit其實就是隱藏層的維度,即上圖tanh輸出的維度。

3.傳統RNN的缺點

模型參數的更新是依靠鏈式法則進行的,而越靠前的層,其梯度信息就包含越多的偏導項,如果偏導項數值較小,在多次乘積之後就可能造成梯度消失(gradient vanishing),如果較大則可能造成梯度爆炸(gradient exploding)。在梯度消失情況下,RNN因爲時間步的增多導致誤差信號在往早期的時間步傳遞中變得越來越弱;在梯度爆炸情況下,模型參數的優化過程會出現大幅度的變動。但無論哪種情況都導致相同的結果,那就是模型無法收斂。

梯度消失和梯度爆炸並不是RNN特有的問題,而是Deep Learning中很普遍的問題,也正是這兩個問題導致發展早期,網絡層數不能加得太深。

既然出現了問題,聰明的人們總是會想到辦法去解決。對於梯度爆炸,人們提出了gradient clipping的方式去避免單步偏導過大,也即是只需要把梯度按一個閾值去截斷就能比較好的解決。但梯度消失這個問題是沒辦法解決的,咱總歸不能刻意去放大梯度信號吧?

上面提到的是一個general problem,也是從訓練角度發現的問題,下面咱們提一個在序列建模中比較特有的問題,而這是則是從推斷角度發現的。

假設h是一個實數,咱們建立一個不包含bias和誤差項ϵ\epsilon的一階自迴歸模型:ht=αht1h_{t} = \alpha\cdot{h_{t-1}},遞歸下去可以得到ht=i=1tαihtih_t = \sum_{i=1}^{t}{\alpha}^{i}{\cdot}h_{t-i}。不難發現,time step距離越遠的項的權值是呈指數膨脹或者衰減的,在很多實際中,呈指數衰減的情況會比較多,因爲距離越遠權重越大不太合乎常理,如果權重α<1|\alpha| < 1,那麼距離越遠的項對最終結果的影響會越小,模型對歷史信息的記憶能力就十分有限了。

RNN亦是如此,所以RNN不具備長期記憶能力。但最見鬼的是,實際情況並不總是時間距離越遠影響越小,例如在氣象數據上可能有明顯的週期性特徵,在文本數據中,上文很遠很遠的地方可能有一個對下文產生關鍵影響的詞。傳統RNN又不具備長時記憶,這就要求必須在這個基礎上做一些改進。

4.LSTM & GRU

LSTM是RNN的改進版本,它是爲了增加RNN的長時依賴能力,而GRU則是LSTM的簡化版本,爲了保證性能的同時可以減少參數並快速計算。

咱們先看看LSTM的內部結構(繼續感謝Colah)

圖四 LSTM的cell結構

看着很複雜?那就對了,接下來我就可以繼續裝13了。

與傳統RNN一樣,主要計算過程的input都是當期輸入xtx_t和上期輸出(這裏講的輸出是hidden state)ht1h_{t-1}。但與傳統RNN不同的是LSTM有個獨立的記憶單元CtC_{t},也叫cell state,這個東西可以通過一條綠色通道通往下游,以達到長期的記憶能力。

再來,咱們看看圖中的三個σ\sigma。這三個東東就是LSTM中的門限單元,分別叫輸入門(input gate),遺忘門(forget gate)和輸出門(output gate)。三個門作用的共同點就是控制信息量,它們都使用sigmoid函數作爲激活函數,而sigmoid函數的值域是(0,1),所以這三個門可以做到對信息的屏蔽和開放。而這三個門作用上的不同點在於它們負責的地方是不一樣的:輸入門控制本期新增的信息量,即控制多少新信息加入;遺忘門控制歷史記憶單元的信息量,即控制多少舊信息加入;輸出門控制本期最終輸出hidden state的輸出量,即控制多少綜合信息輸出

怎麼樣,還是很繞?咱們從數學式角度看看,首先三個門的計算:
it=σ(Wi[ht1,xt]+bi)ft=σ(Wf[ht1,xt]+bf)ot=σ(Wo[ht1,xt]+bo) i_t = \sigma(W_{i}\cdot{[h_{t-1},x_{t}]}+b_{i}) \\ f_t = \sigma(W_{f}\cdot{[h_{t-1},x_{t}]}+b_{f}) \\ o_t = \sigma(W_{o}\cdot{[h_{t-1},x_{t}]}+b_{o}) \\
它們的計算方式是一樣的,只是各自有各自的參數。接着,咱們看看記憶單元的更新:
Ct=ftCt1+itC~t C_{t} = f_{t}*{C_{t-1}} + i_{t}*{\tilde{C}_{t}}
其中,本期的cell state計算跟三個門類似,但激活函數不同:
C~t=tanh(Wc[ht1,xt]+bc) \tilde{C}_{t}= tanh(W_{c}\cdot{[h_{t-1},x_{t}]}+b_{c})
最後,輸出並傳遞到下期作爲輸入的hidden state需要用輸出門控制一下:
ht=ottanh(Ct) h_{t} = o_{t}*tanh(C_{t})
上面的*表示按位相乘。

GRU基本上與LSTM是類似的,只是在遺忘門這裏做了簡化,也不需要額外的cell state。先看看圖

圖五 GRU的cell結構

GRU只有兩個門,分別叫更新門(update gate)和重置門(reset gate)。更新門相當於LSTM的遺忘門和輸入門,GRU在這塊做了簡化,用一個門代替了LSTM的兩個門:
zt=σ(Wz[ht1,xt]+bz)rt=σ(Wr[ht1,xt]+br) z_{t} = \sigma(W_{z}\cdot{[h_{t-1},x_{t}]}+b_{z}) \\ r_{t} = \sigma(W_{r}\cdot{[h_{t-1},x_{t}]}+b_{r}) \\
本期hidden state的計算:
h~t=tanh(Wh[rtht1,xt]+bh) \tilde{h}_{t} = \tanh(W_{h}\cdot[{r_{t} * h_{t-1}},x_{t}] + b_{h})
hidden state的更新計算:
ht=ztht1+(1zt)h~t h_{t} = z_{t} * h_{t-1} + (1-z_{t}) * \tilde{h}_{t}
這裏公式是搬原論文的,Colah博客中的更新計算權值是跟上式調過來的,但是我個人認爲這個沒多大關係。

LSTM跟GRU到底該用哪個呢?很多人都說它們效果都差不了太多,我覺得還是要根據情況決定,LSTM畢竟參數多,表徵能力自然更強,如果訓練數據足夠的話,可以默認使用LSTM;GRU參數少速度快,數據量不多或者對速度要求比較高的話可以優先考慮GRU。

5.bidirectional RNN & multi-layers RNN

普通RNN默認是一個順序去處理序列的,例如我們習慣都是從左到右。但在NLP領域,我們在某個time step上只知道某個方向上的歷史信息是遠遠不夠的,要知道,我們人類理解一個詞、一段話所表達的意思也不是隻根據上文去理解,還要根據下文去綜合考慮,於是雙向RNN就應運而生了。

圖六 bidirectional RNN

例如上圖的輸入序列 the movie was terribly exciting!,如果我們要做情感分析,只用單向RNN的話,很容易到’terribly’這個位置就產生負面的特徵,因爲它之前的序列’the movie was’是偏中性的,而terribly這個詞本身偏負面。但如果考慮它的後文’exciting’,那麼就很容易得到準確的判斷。所以我們需要添加兩個方向的RNN,以便綜合考慮整個語境。

這裏提到的RNN可以是傳統RNN,也可以是LSTM或者GRU,但無論哪種,兩個方向得到的hidden state都可以拼接起來作爲後面layer的輸入。但必須注意的是,在自然語言生成之類的場景,雙向RNN是不適用的,因爲我們在input的時候不知道下文是什麼,所以雙向RNN只適用於上下文都完整的情況,例如NER、文本分類等。

除了雙向RNN之外,還有一種高級版本是多層堆疊成多層RNN,就像前饋網絡一樣。爲什麼要堆疊起來?原因也很簡單,網絡越大,表徵能力也越強唄。

圖七 bidirectional RNN

6.BPTT

RNN模型的優化方法有個特別的名字——BPTT(back-propagation through time)。聽起來好像有點複雜,其實跟普通的backprop沒有什麼區別。

那還說啥呢?咱們可以回想一下上面提到的神經網絡多種類型。RNN的特點是它並不是每個時間步都會產生output(這裏指的輸出層的輸出結果),有可能在最後一步再產生一個output,有可能每一步產生的hidden state會用作後面層的輸入,也有可能每一步都直接產生一個output。但不管哪種情況,只要我們把RNN按時間維度展開成一個流程圖,我們就能發現,每一步的誤差信號都包括來自自身的(spatial)以及來自下游的(temporal)。

圖八 BPTT

來自自身的部分很好理解,因爲跟普通前饋網絡是一樣的。來自下游的部分的,可能稍微複雜一點,但要理解誤差傳遞的話,其實只需要把整個計算流程畫出來,每條帶權邊都求偏導,再找到對應的backprop方向就okay了,不要被BPTT這個名字嚇怕了,本質上其實還是chain rule而已。

參考資料

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