在前面的文章系列文章中,咱們介紹了全鏈接神經網絡和卷積神經網絡,以及它們的訓練和使用。他們都只能單獨的取處理一個個的輸入,前一個輸入和後一個輸入是徹底沒有關係的。可是,某些任務須要可以更好的處理序列的信息,即前面的輸入和後面的輸入是有關係的。好比,當咱們在理解一句話意思時,孤立的理解這句話的每一個詞是不夠的,咱們須要處理這些詞鏈接起來的整個序列;當咱們處理視頻的時候,咱們也不能只單獨的去分析每一幀,而要分析這些幀鏈接起來的整個序列。這時,就須要用到深度學習領域中另外一類很是重要神經網絡:循環神經網絡(Recurrent Neural Network)。RNN種類不少,也比較繞腦子。不過讀者不用擔憂,本文將一如既往的對複雜的東西剝繭抽絲,幫助您理解RNNs以及它的訓練算法,並動手實現一個循環神經網絡。python
RNN是在天然語言處理領域中最早被用起來的,好比,RNN能夠爲語言模型來建模。那麼,什麼是語言模型呢?git
咱們能夠和電腦玩一個遊戲,咱們寫出一個句子前面的一些詞,而後,讓電腦幫咱們寫下接下來的一個詞。好比下面這句:github
我昨天上學遲到了,老師批評了____。算法
咱們給電腦展現了這句話前面這些詞,而後,讓電腦寫下接下來的一個詞。在這個例子中,接下來的這個詞最有多是『我』,而不太多是『小明』,甚至是『吃飯』。數組
語言模型就是這樣的東西:給定一個一句話前面的部分,預測接下來最有可能的一個詞是什麼。網絡
語言模型是對一種語言的特徵進行建模,它有不少不少用處。好比在語音轉文本(STT)的應用中,聲學模型輸出的結果,每每是若干個可能的候選詞,這時候就須要語言模型來從這些候選詞中選擇一個最可能的。固然,它一樣也能夠用在圖像到文本的識別中(OCR)。app
使用RNN以前,語言模型主要是採用N-Gram。N能夠是一個天然數,好比2或者3。它的含義是,假設一個詞出現的機率只與前面N個詞相關。咱們以2-Gram爲例。首先,對前面的一句話進行切詞:dom
我 昨天 上學 遲到 了 ,老師 批評 了 ____。python2.7
若是用2-Gram進行建模,那麼電腦在預測的時候,只會看到前面的『了』,而後,電腦會在語料庫中,搜索『了』後面最可能的一個詞。無論最後電腦選的是否是『我』,咱們都知道這個模型是不靠譜的,由於『了』前面說了那麼一大堆其實是沒有用到的。若是是3-Gram模型呢,會搜索『批評了』後面最可能的詞,感受上比2-Gram靠譜了很多,但仍是遠遠不夠的。由於這句話最關鍵的信息『我』,遠在9個詞以前!ide
如今讀者可能會想,能夠提高繼續提高N的值呀,好比4-Gram、5-Gram.......。實際上,這個想法是沒有實用性的。由於咱們想處理任意長度的句子,N設爲多少都不合適;另外,模型的大小和N的關係是指數級的,4-Gram模型就會佔用海量的存儲空間。
因此,該輪到RNN出場了,RNN理論上能夠往前看(日後看)任意多個詞。
循環神經網絡種類繁多,咱們先從最簡單的基本循環神經網絡開始吧。
下圖是一個簡單的循環神經網絡如,它由輸入層、一個隱藏層和一個輸出層組成:
納尼?!相信第一次看到這個玩意的讀者心裏和我同樣是崩潰的。由於循環神經網絡實在是太難畫出來了,網上全部大神們都不得不用了這種抽象藝術手法。不過,靜下心來仔細看看的話,其實也是很好理解的。若是把上面有W的那個帶箭頭的圈去掉,它就變成了最普通的全鏈接神經網絡。x是一個向量,它表示輸入層的值(這裏面沒有畫出來表示神經元節點的圓圈);s是一個向量,它表示隱藏層的值(這裏隱藏層面畫了一個節點,你也能夠想象這一層實際上是多個節點,節點數與向量s的維度相同);U是輸入層到隱藏層的權重矩陣(讀者能夠回到第三篇文章零基礎入門深度學習(3) - 神經網絡和反向傳播算法,看看咱們是怎樣用矩陣來表示全鏈接神經網絡的計算的);o也是一個向量,它表示輸出層的值;V是隱藏層到輸出層的權重矩陣。那麼,如今咱們來看看W是什麼。循環神經網絡的隱藏層的值s不只僅取決於當前此次的輸入x,還取決於上一次隱藏層的值s。權重矩陣 W就是隱藏層上一次的值做爲這一次的輸入的權重。
若是咱們把上面的圖展開,循環神經網絡也能夠畫成下面這個樣子:
如今看上去就比較清楚了,這個網絡在t時刻接收到輸入以後,隱藏層的值是,輸出值是。關鍵一點是,的值不只僅取決於,還取決於。咱們能夠用下面的公式來表示循環神經網絡的計算方法:
式1是輸出層的計算公式,輸出層是一個全鏈接層,也就是它的每一個節點都和隱藏層的每一個節點相連。V是輸出層的權重矩陣,g是激活函數。式2是隱藏層的計算公式,它是循環層。U是輸入x的權重矩陣,W是上一次的值做爲這一次的輸入的權重矩陣,f是激活函數。
從上面的公式咱們能夠看出,循環層和全鏈接層的區別就是循環層多了一個權重矩陣 W。
若是反覆把式2帶入到式1,咱們將獲得:
從上面能夠看出,循環神經網絡的輸出值,是受前面歷次輸入值、、、、...影響的,這就是爲何循環神經網絡能夠往前看任意多個輸入值的緣由。
對於語言模型來講,不少時候光看前面的詞是不夠的,好比下面這句話:
個人手機壞了,我打算____一部新手機。
能夠想象,若是咱們只看橫線前面的詞,手機壞了,那麼我是打算修一修?換一部新的?仍是大哭一場?這些都是沒法肯定的。但若是咱們也看到了橫線後面的詞是『一部新手機』,那麼,橫線上的詞填『買』的機率就大得多了。
在上一小節中的基本循環神經網絡是沒法對此進行建模的,所以,咱們須要雙向循環神經網絡,以下圖所示:
當遇到這種從將來穿越回來的場景時,不免處於懵逼的狀態。不過咱們仍是能夠用屢試不爽的老辦法:先分析一個特殊場景,而後再總結通常規律。咱們先考慮上圖中,的計算。
從上圖能夠看出,雙向卷積神經網絡的隱藏層要保存兩個值,一個A參與正向計算,另外一個值A'參與反向計算。最終的輸出值取決於和。其計算方法爲:
和則分別計算:
如今,咱們已經能夠看出通常的規律:正向計算時,隱藏層的值與有關;反向計算時,隱藏層的值與有關;最終的輸出取決於正向和反向計算的加和。如今,咱們仿照式1和式2,寫出雙向循環神經網絡的計算方法:
從上面三個公式咱們能夠看到,正向計算和反向計算不共享權重,也就是說U和U'、W和W'、V和V'都是不一樣的權重矩陣。
前面咱們介紹的循環神經網絡只有一個隱藏層,咱們固然也能夠堆疊兩個以上的隱藏層,這樣就獲得了深度循環神經網絡。以下圖所示:
咱們把第i個隱藏層的值表示爲、,則深度循環神經網絡的計算方式能夠表示爲:
BPTT算法是針對循環層的訓練算法,它的基本原理和BP算法是同樣的,也包含一樣的三個步驟:
最後再用隨機梯度降低算法更新權重。
循環層以下圖所示:
使用前面的式2對循環層進行前向計算:
注意,上面的、、都是向量,用黑體字母表示;而U、V是矩陣,用大寫字母表示。向量的下標表示時刻,例如,表示在t時刻向量s的值。
咱們假設輸入向量x的維度是m,輸出向量s的維度是n,則矩陣U的維度是,矩陣W的維度是。下面是上式展開成矩陣的樣子,看起來更直觀一些:
在這裏咱們用手寫體字母表示向量的一個元素,它的下標表示它是這個向量的第幾個元素,它的上標表示第幾個時刻。例如,表示向量s的第j個元素在t時刻的值。表示輸入層第i個神經元到循環層第j個神經元的權重。表示循環層第t-1時刻的第i個神經元到循環層第t個時刻的第j個神經元的權重。
BTPP算法將第l層t時刻的偏差項值沿兩個方向傳播,一個方向是其傳遞到上一層網絡,獲得,這部分只和權重矩陣U有關;另外一個是方向是將其沿時間線傳遞到初始時刻,獲得,這部分只和權重矩陣W有關。
咱們用向量表示神經元在t時刻的加權輸入,由於:
所以:
咱們用a表示列向量,用表示行向量。上式的第一項是向量函數對向量求導,其結果爲Jacobian矩陣:
同理,上式第二項也是一個Jacobian矩陣:
其中,diag[a]表示根據向量a建立一個對角矩陣,即
最後,將兩項合在一塊兒,可得:
上式描述了將沿時間往前傳遞一個時刻的規律,有了這個規律,咱們就能夠求得任意時刻k的偏差項:
式3就是將偏差項沿時間反向傳播的算法。
循環層將偏差項反向傳遞到上一層網絡,與普通的全鏈接層是徹底同樣的,這在前面的文章零基礎入門深度學習(3) - 神經網絡和反向傳播算法中已經詳細講過了,在此僅簡要描述一下。
循環層的加權輸入與上一層的加權輸入關係以下:
上式中是第l層神經元的加權輸入(假設第l層是循環層);是第l-1層神經元的加權輸入;是第l-1層神經元的輸出;是第l-1層的激活函數。
因此,
式4就是將偏差項傳遞到上一層算法。
如今,咱們終於來到了BPTT算法的最後一步:計算每一個權重的梯度。
首先,咱們計算偏差函數E對權重矩陣W的梯度。
上圖展現了咱們到目前爲止,在前兩步中已經計算獲得的量,包括每一個時刻t 循環層的輸出值,以及偏差項。
回憶一下咱們在文章零基礎入門深度學習(3) - 神經網絡和反向傳播算法介紹的全鏈接網絡的權重梯度計算算法:只要知道了任意一個時刻的偏差項,以及上一個時刻循環層的輸出值,就能夠按照下面的公式求出權重矩陣在t時刻的梯度:
在式5中,表示t時刻偏差項向量的第i個份量;表示t-1時刻循環層第i個神經元的輸出值。
咱們下面能夠簡單推導一下式5。
咱們知道:
由於對W求導與無關,咱們再也不考慮。如今,咱們考慮對權重項求導。經過觀察上式咱們能夠看到只與有關,因此:
按照上面的規律就能夠生成式5裏面的矩陣。
咱們已經求得了權重矩陣W在t時刻的梯度,最終的梯度是各個時刻的梯度之和:
式6就是計算循環層權重矩陣W的梯度的公式。
----------數學公式超高能預警----------
前面已經介紹了的計算方法,看上去仍是比較直觀的。然而,讀者也許會困惑,爲何最終的梯度是各個時刻的梯度之和呢?咱們前面只是直接用了這個結論,實際上這裏面是有道理的,只是這個數學推導比較繞腦子。感興趣的同窗能夠仔細閱讀接下來這一段,它用到了矩陣對矩陣求導、張量與向量相乘運算的一些法則。
咱們仍是從這個式子開始:
由於與W徹底無關,咱們把它看作常量。如今,考慮第一個式子加號右邊的部分,由於W和都是W的函數,所以咱們要用到大學裏面都學過的導數乘法運算:
所以,上面第一個式子寫成:
咱們最終須要計算的是:
咱們先計算式7加號左邊的部分。是矩陣對矩陣求導,其結果是一個四維張量(tensor),以下所示:
接下來,咱們知道,它是一個列向量。咱們讓上面的四維張量與這個向量相乘,獲得了一個三維張量,再左乘行向量,最終獲得一個矩陣:
接下來,咱們計算式7加號右邊的部分:
因而,咱們獲得了以下遞推公式:
這樣,咱們就證實了:最終的梯度是各個時刻的梯度之和。
----------數學公式超高能預警解除----------
同權重矩陣W相似,咱們能夠獲得權重矩陣U的計算方法。
式8是偏差函數在t時刻對權重矩陣U的梯度。和權重矩陣W同樣,最終的梯度也是各個時刻的梯度之和:
具體的證實這裏就再也不贅述了,感興趣的讀者能夠練習推導一下。
不幸的是,實踐中前面介紹的幾種RNNs並不能很好的處理較長的序列。一個主要的緣由是,RNN在訓練中很容易發生梯度爆炸和梯度消失,這致使訓練時梯度不能在較長序列中一直傳遞下去,從而使RNN沒法捕捉到長距離的影響。
爲何RNN會產生梯度爆炸和消失問題呢?咱們接下來將詳細分析一下緣由。咱們根據式3可得:
上式的定義爲矩陣的模的上界。由於上式是一個指數函數,若是t-k很大的話(也就是向前看很遠的時候),會致使對應的偏差項的值增加或縮小的很是快,這樣就會致使相應的梯度爆炸和梯度消失問題(取決於大於1仍是小於1)。
一般來講,梯度爆炸更容易處理一些。由於梯度爆炸的時候,咱們的程序會收到NaN錯誤。咱們也能夠設置一個梯度閾值,當梯度超過這個閾值的時候能夠直接截取。
梯度消失更難檢測,並且也更難處理一些。總的來講,咱們有三種方法應對梯度消失問題:
如今,咱們介紹一下基於RNN語言模型。咱們首先把詞依次輸入到循環神經網絡中,每輸入一個詞,循環神經網絡就輸出截止到目前爲止,下一個最可能的詞。例如,當咱們依次輸入:
我 昨天 上學 遲到 了
神經網絡的輸出以下圖所示:
其中,s和e是兩個特殊的詞,分別表示一個序列的開始和結束。
咱們知道,神經網絡的輸入和輸出都是向量,爲了讓語言模型可以被神經網絡處理,咱們必須把詞表達爲向量的形式,這樣神經網絡才能處理它。
神經網絡的輸入是詞,咱們能夠用下面的步驟對輸入進行向量化:
上面這個公式的含義,能夠用下面的圖來直觀的表示:
使用這種向量化方法,咱們就獲得了一個高維、稀疏的向量(稀疏是指絕大部分元素的值都是0)。處理這樣的向量會致使咱們的神經網絡有不少的參數,帶來龐大的計算量。所以,每每會須要使用一些降維方法,將高維的稀疏向量轉變爲低維的稠密向量。不過這個話題咱們就再也不這篇文章中討論了。
語言模型要求的輸出是下一個最可能的詞,咱們可讓循環神經網絡計算計算詞典中每一個詞是下一個詞的機率,這樣,機率最大的詞就是下一個最可能的詞。所以,神經網絡的輸出向量也是一個N維向量,向量中的每一個元素對應着詞典中相應的詞是下一個詞的機率。以下圖所示:
前面提到,語言模型是對下一個詞出現的機率進行建模。那麼,怎樣讓神經網絡輸出機率呢?方法就是用softmax層做爲神經網絡的輸出層。
咱們先來看一下softmax函數的定義:
這個公式看起來可能很暈,咱們舉一個例子。Softmax層以下圖所示:
從上圖咱們能夠看到,softmax layer的輸入是一個向量,輸出也是一個向量,兩個向量的維度是同樣的(在這個例子裏面是4)。輸入向量x=[1 2 3 4]通過softmax層以後,通過上面的softmax函數計算,轉變爲輸出向量y=[0.03 0.09 0.24 0.64]。計算過程爲:
咱們來看看輸出向量y的特徵:
咱們不難發現,這些特徵和機率的特徵是同樣的,所以咱們能夠把它們看作是機率。對於語言模型來講,咱們能夠認爲模型預測下一個詞是詞典中第一個詞的機率是0.03,是詞典中第二個詞的機率是0.09,以此類推。
可使用監督學習的方法對語言模型進行訓練,首先,須要準備訓練數據集。接下來,咱們介紹怎樣把語料
我 昨天 上學 遲到 了
轉換成語言模型的訓練數據集。
首先,咱們獲取輸入-標籤對:
輸入 | 標籤 |
---|---|
s | 我 |
我 | 昨天 |
昨天 | 上學 |
上學 | 遲到 |
遲到 | 了 |
了 | e |
而後,使用前面介紹過的向量化方法,對輸入x和標籤y進行向量化。這裏面有意思的是,對標籤y進行向量化,其結果也是一個one-hot向量。例如,咱們對標籤『我』進行向量化,獲得的向量中,只有第2019個元素的值是1,其餘位置的元素的值都是0。它的含義就是下一個詞是『我』的機率是1,是其它詞的機率都是0。
最後,咱們使用交叉熵偏差函數做爲優化目標,對模型進行優化。
在實際工程中,咱們可使用大量的語料來對模型進行訓練,獲取訓練數據和訓練的方法都是相同的。
通常來講,當神經網絡的輸出層是softmax層時,對應的偏差函數E一般選擇交叉熵偏差函數,其定義以下:
在上式中,N是訓練樣本的個數,向量是樣本的標記,向量是網絡的輸出。標記是一個one-hot向量,例如,若是網絡的輸出,那麼,交叉熵偏差是(假設只有一個訓練樣本,即N=1):
咱們固然能夠選擇其餘函數做爲咱們的偏差函數,好比最小平方偏差函數(MSE)。不過對機率進行建模時,選擇交叉熵偏差函數更make sense。具體緣由,感興趣的讀者請閱讀參考文獻7。
完整代碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/rnn.py (python2.7)
爲了加深咱們對前面介紹的知識的理解,咱們來動手實現一個RNN層。咱們複用了上一篇文章零基礎入門深度學習(4) - 卷積神經網絡中的一些代碼,因此先把它們導入進來。
import numpy as np
from cnn import ReluActivator, IdentityActivator, element_wise_op
咱們用RecurrentLayer類來實現一個循環層。下面的代碼是初始化一個循環層,能夠在構造函數中設置卷積層的超參數。咱們注意到,循環層有兩個權重數組,U和W。
class RecurrentLayer(object):
def __init__(self, input_width, state_width,
activator, learning_rate):
self.input_width = input_width
self.state_width = state_width
self.activator = activator
self.learning_rate = learning_rate
self.times = 0 # 當前時刻初始化爲t0
self.state_list = [] # 保存各個時刻的state
self.state_list.append(np.zeros(
(state_width, 1))) # 初始化s0
self.U = np.random.uniform(-1e-4, 1e-4,
(state_width, input_width)) # 初始化U
self.W = np.random.uniform(-1e-4, 1e-4,
(state_width, state_width)) # 初始化W
在forward方法中,實現循環層的前向計算,這部分比較簡單。
def forward(self, input_array):
'''
根據『式2』進行前向計算
'''
self.times += 1
state = (np.dot(self.U, input_array) +
np.dot(self.W, self.state_list[-1]))
element_wise_op(state, self.activator.forward)
self.state_list.append(state)
在backword方法中,實現BPTT算法。
def backward(self, sensitivity_array,
activator):
'''
實現BPTT算法
'''
self.calc_delta(sensitivity_array, activator)
self.calc_gradient()
def calc_delta(self, sensitivity_array, activator):
self.delta_list = [] # 用來保存各個時刻的偏差項
for i in range(self.times):
self.delta_list.append(np.zeros(
(self.state_width, 1)))
self.delta_list.append(sensitivity_array)
# 迭代計算每一個時刻的偏差項
for k in range(self.times - 1, 0, -1):
self.calc_delta_k(k, activator)
def calc_delta_k(self, k, activator):
'''
根據k+1時刻的delta計算k時刻的delta
'''
state = self.state_list[k+1].copy()
element_wise_op(self.state_list[k+1],
activator.backward)
self.delta_list[k] = np.dot(
np.dot(self.delta_list[k+1].T, self.W),
np.diag(state[:,0])).T
def calc_gradient(self):
self.gradient_list = [] # 保存各個時刻的權重梯度
for t in range(self.times + 1):
self.gradient_list.append(np.zeros(
(self.state_width, self.state_width)))
for t in range(self.times, 0, -1):
self.calc_gradient_t(t)
# 實際的梯度是各個時刻梯度之和
self.gradient = reduce(
lambda a, b: a + b, self.gradient_list,
self.gradient_list[0]) # [0]被初始化爲0且沒有被修改過
def calc_gradient_t(self, t):
'''
計算每一個時刻t權重的梯度
'''
gradient = np.dot(self.delta_list[t],
self.state_list[t-1].T)
self.gradient_list[t] = gradient
有意思的是,BPTT算法雖然數學推導的過程很麻煩,可是寫成代碼卻並不複雜。
在update方法中,實現梯度降低算法。
def update(self):
'''
按照梯度降低,更新權重
'''
self.W -= self.learning_rate * self.gradient
上面的代碼不包含權重U的更新。這部分實際上和全鏈接神經網絡是同樣的,留給感興趣的讀者本身來完成吧。
循環層是一個帶狀態的層,每次forword都會改變循環層的內部狀態,這給梯度檢查帶來了麻煩。所以,咱們須要一個reset_state方法,來重置循環層的內部狀態。
def reset_state(self):
self.times = 0 # 當前時刻初始化爲t0
self.state_list = [] # 保存各個時刻的state
self.state_list.append(np.zeros(
(self.state_width, 1))) # 初始化s0
最後,是梯度檢查的代碼。
def gradient_check():
'''
梯度檢查
'''
# 設計一個偏差函數,取全部節點輸出項之和
error_function = lambda o: o.sum()
rl = RecurrentLayer(3, 2, IdentityActivator(), 1e-3)
# 計算forward值
x, d = data_set()
rl.forward(x[0])
rl.forward(x[1])
# 求取sensitivity map
sensitivity_array = np.ones(rl.state_list[-1].shape,
dtype=np.float64)
# 計算梯度
rl.backward(sensitivity_array, IdentityActivator())
# 檢查梯度
epsilon = 10e-4
for i in range(rl.W.shape[0]):
for j in range(rl.W.shape[1]):
rl.W[i,j] += epsilon
rl.reset_state()
rl.forward(x[0])
rl.forward(x[1])
err1 = error_function(rl.state_list[-1])
rl.W[i,j] -= 2*epsilon
rl.reset_state()
rl.forward(x[0])
rl.forward(x[1])
err2 = error_function(rl.state_list[-1])
expect_grad = (err1 - err2) / (2 * epsilon)
rl.W[i,j] += epsilon
print 'weights(%d,%d): expected - actural %f - %f' % (
i, j, expect_grad, rl.gradient[i,j])
須要注意,每次計算error以前,都要調用reset_state方法重置循環層的內部狀態。下面是梯度檢查的結果,沒問題!
至此,咱們講完了基本的循環神經網絡、它的訓練算法:BPTT,以及在語言模型上的應用。RNN比較燒腦,相信拿下前幾篇文章的讀者們搞定這篇文章也不在話下吧!然而,循環神經網絡這個話題並無完結。咱們在前面說到過,基本的循環神經網絡存在梯度爆炸和梯度消失問題,並不能真正的處理好長距離的依賴(雖然有一些技巧能夠減輕這些問題)。事實上,真正獲得普遍的應用的是循環神經網絡的一個變體:長短時記憶網絡。它內部有一些特殊的結構,能夠很好的處理長距離的依賴,咱們將在下一篇文章中詳細的介紹它。如今,讓咱們稍事休息,準備挑戰更爲燒腦的長短時記憶網絡吧。