Language Model and Recurrent Neural Networks

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


語言模型

語言模型的應用很常見,例如輸入法會自動計算你接下來可能要輸入的字或詞,搜索引擎會自動補全你可能要搜索的內容等等。所以一句話描述,語言模型就是一個用來預測接下來的詞的模型,可以描述爲P(x(t)x(t1),x(t2),...,x(0))P(x^{(t)}|x^{(t-1)},x^{(t-2)},...,x^{(0)})。這樣我們就可以根據這個概率來估計或者採樣出下面將要出現的詞。

1.基於統計的語言模型

傳統的語言模型是根據詞頻來預測的,那麼上面那個式可以寫成
P(x(t)x(t1),x(t2),...,x(0))=P(x(t),x(t1),x(t2),...,x(0))P(x(t1),x(t2),...,x(0)) P(x^{(t)}|x^{(t-1)},x^{(t-2)},...,x^{(0)})=\frac{P(x^{(t)},x^{(t-1)},x^{(t-2)},...,x^{(0)})}{P(x^{(t-1)},x^{(t-2)},...,x^{(0)})}

對於給定的一個語料庫,我們只需要統計一下出現次數就行
P(x(t)x(t1),x(t2),...,x(0))=count(x(t),x(t1),x(t2),...,x(0))count(x(t1),x(t2),...,x(0))P(x^{(t)}|x^{(t-1)},x^{(t-2)},...,x^{(0)}) = \frac{count(x^{(t)},x^{(t-1)},x^{(t-2)},...,x^{(0)})}{count(x^{(t-1)},x^{(t-2)},...,x^{(0)})}

但是文本序列越長,需要統計的序列也越長,計算量就越大,這顯然不合理的,於是聰明的人們就想,要不就簡化一下吧,假定後面出現的這個次只跟前面的k=n-1個詞有關就好了,於是乎,n-gram就出現了
P(x(t)x(t1),...,x(tn+1))P(x^{(t)}|x^{(t-1)},...,x^{(t-n+1)})

所以n=1時的語言模型就等於各個詞的出現頻率,n=2時的語言模型就是隻用前一個詞預測後一個詞的條件概率……

基於統計的方式去計算語言模型會有一些問題。假設我要計算的某個詞的概率P(x(t)P(x(t1)))P(x^{(t)}|P(x^{(t-1)})),但尷尬的是我語料庫裏面[x(t1),x(t)][x^{(t-1)},x^{(t)}]這個組合從來沒出現過,那計算這個語言模型的時候概率就爲0咯?

這顯然是不科學的,不能因爲沒見過這個詞組而說它的條件概率就爲0,於是人們想到可以對它進行一些平滑操作。至於詳細的平滑操作到底有哪些,咱就不深入討論了,方法有很多,也各有利弊,可以理解成最簡單的辦法就是把所有出現的可能性都加上一個固定的很小的常數,那麼就可以把未出現過的變成一個小概率。

平滑操作雖然可以避免分子或分母爲0時帶來的問題,但它根本上並沒有辦法解決統計上n-gram的稀疏性問題。即便語料庫再大,也總有不出現的配對,而且n越大就越稀疏,存儲空間的需求也是越大,搜索起來就越慢。

2.基於固定滑動窗口的語言模型

上面我們一直討論的是語言模型是用詞頻來計算的,但如果這個P(y|x)是個神經網絡,那上面提到的困難就迎刃而解了。

與n-gram一樣,我們假設輸入是前面n-1個詞,那麼我們就可以通過一個神經網絡,以這n-1個詞的詞向量作爲輸入,輸出下一個詞的概率,假設這裏我們n=5:

圖一 使用滑動窗口的語言模型

這個過程乍一看跟CBOW算法很像,但CBOW預測的是中間詞,這裏預測的是下一個詞。還有個細節是CBOW的輸入層到隱藏層之間的權值其實是詞向量,這個過程除了詞向量之外是沒有額外權值的,但這裏圖中藍色那層已經是詞嵌入向量e,這裏到隱藏層h之間還有一個權值矩陣W。更多詳細細節就不繼續討論了,關於CBOW的詳細描述請戳這裏

然而,固定滑動窗口這個辦法也有些問題。首先,窗口大小我們不好取,取小了,距離較遠的詞不能加入計算,哪怕它們相互之間是有關係的;取大了,模型參數的shape也會變大,計算量也就越大了。其次,每個詞的處理是與位置有關的,也就是e(1)e^{(1)}~e(4)e^{(4)}它們是各自與一個權重相乘,而非權值共享的。最後,也就是它最天然的問題了,窗口是固定的,窗口以外的詞就沒辦法加入訓練,這個模型是沒有記憶的

CS224n課堂上提出第二點的這個問題,我個人其實認爲並不是很嚴重的問題,不同的詞序確實是會導致產生不同的上下文環境,所以權值共享是不是十分必要,我個人持保留意見,只不過這個順序關係是基於權值來確定那就確實過分生硬了。

3.基於循環神經網絡的語言模型

用RNN來建立語言模型可以比較好的解決上面提到的問題,因爲RNN的循環結構使得語言模型可以以任意的長度作爲輸入而不會引起模型參數的增加,而且RNN在每次計算的時候會使用當期的input以及上期的output統一作爲輸入,這樣可以使得模型具備一定的“記憶”能力。

圖二 使用RNN的語言模型

關於RNN的技術細節,我們接下來將詳細討論。

4.語言模型的採樣過程

在inference過程,我們可能需要使用語言模型生成文本,這就涉及到採樣(sampling)。所謂採樣,就是根據模型輸出的各個詞概率分佈,抽取適當的詞作爲估計值。所謂“適當”,其實是可以根據實際情況選取適合的策略,例如可以採用貪心策略,即每一步都採取最大概率的預測詞作爲結果,又或者更爲靈活一點,按照概率分佈進行加權抽樣。更魯棒一點,可以採用Beam Search採樣出更好的結果。那麼怎麼樣的結果纔算好呢?

5.語言模型的性能評估

我們知道分類模型可以用精準率、召回率或者準確率之類的評估指標來衡量性能,迴歸模型可以則用MAE、MSE、SSE之類的指標。對語言模型而言,它本質上是一個分類模型,並且使用交叉熵作爲損失函數,但用準確率作爲語言模型的評估未免太過苛刻,畢竟自然語言是十分靈活的,同樣的意思可以有多種表述方式。這裏我們介紹使用困惑度(perplexity)作爲評估指標,CS224N課堂說這是語言模型的標準評估指標。
perplexity=t=1T(1PLM(x(t+1)x(t),...,x(1)))1T perplexity = \prod_{t=1}^{T}{(\frac{1}{P_{LM}{(x^{(t+1)}|x^{(t)},...,x^{(1)})}})^{\frac{1}{T}}}

細心的朋友可能會發現,如果我們對困惑度取對數,那麼就等於交叉熵。所以不難反推出,困惑度就是exp(交叉熵)。e爲底的指數函數是單調遞增的,那麼困惑度與交叉熵一樣,都是越數值越小越好。

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而已。

參考資料

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