RNN入門與實踐
作者:葉虎
編輯:黃俊嘉
引言
遞迴神經網路(Recurrent Neural Network, RNN)是神經網路家族的重要成員,而且也是深度學習領域中的得力干將,因為深度學習廣泛應用的領域如語音識別,機器翻譯等都有RNN的身影。與經典的神經網路不同,RNN主要解決的是樣本資料為序列的建模問題,如語音序列,語言序列。因為對於序列資料來說,大部分情況下序列的每個元素並不是相互獨立,其存在依賴關係,而RNN特別適合這類建模問題。本文會介紹RNN的原理及應用,並動手實現一個RNN預測模型。
RNN原理
RNN處理的是序列建模問題。給定一個長度為T輸入序列{x0,x1...,xt,....,xT},這裡 表示的是序列在t時刻的輸入特徵向量,這裡的t時刻並不一定真的指的是時間,只是用來表明這是一個序列輸入問題。現在要得到每個時刻的隱含特徵{h0,h1...,ht,....,hT} ,這些隱含特徵用於後面層的特徵輸入。如果採用傳統的神經網路模型,只需要計算:
其中f為非線性啟用函式。但是這樣明顯忽略了這是一個序列輸入問題,即丟失了序列中各個元素的依賴關係。對於RNN模型來說,其在計算t時刻的特徵時,不僅考慮當前時刻的輸入特徵xT ,而且引入前一個時刻的隱含特徵ht-1 ,其計算過程如下:
顯然這樣可以捕捉到序列中依賴關係,可以認為是一個ht-1記憶特徵,其提取了前面t-1個時刻的輸入特徵,有時候又稱ht-1為舊狀態,而ht為新狀態。因此,RNN模型特別適合序列問題。從結構上看,RNN可以看成有環的神經網路模型,如圖1所示。不過可以將其展開成普通的神經網路模型,準確地說展開成T個普通的神經網路模型。但是這T個神經網路不是割立的,其所使用引數是一樣的,即權重共享。這樣每一個時刻,RNN執行的是相同的計算過程,只不過其輸入不一樣而已。所以本質上,RNN也只不過多個普通的神經網路通過權值共享連線而成。
圖1 RNN模型及展開簡圖
(來源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)
還有一點,RNN可以提取一組特徵{h0,h1...,ht,....,hT},但是並不是所有的特徵都會送入後面的層,如果你只是需要根據輸入序列進行分類,可能你僅需要最後時刻的特徵hT。這和具體的應用場景相關。
RNN訓練
RNN模型像其他神經網路模型一樣也是採用梯度下降法訓練,相應的也需要計算梯度。計算梯度也是採用BP演算法,但是由於RNN的特殊性,其對應的BP演算法又稱為BPTT(Backpropagation Through Time)。BPTT的背後含義是梯度還要在時間層進行反向傳播,這很好理解,比如ht的梯度ht-1,....,h0還要對做貢獻。這從數學公式上可以看出來的,本質上還是鏈式規則。但是你可能知道梯度消失的問題,在RNN模型中其同樣存在。梯度消失的問題在RNN上表現為ht的梯度傳播距離可能有限,這帶來的一個直接後果是:RNN對長依賴序列問題(long-term dependencies)無效。這使得經典的RNN模型的應用很受限,所以才會出現RNN的變種如LSTM,它們可以很好地解決這類問題。
RNN應用
RNN主要應用在輸入為序列資料的業務場景。其中一個很重要的領域是自然語言處理,如語言模型及機器翻譯等。RNN還可以對具有周期性特徵的資料建立預測模型。對於序列問題,可以用下圖來說明RNN的應用:
圖2 RNN處理序列問題(來源:cs231n)
其中one to one是典型的神經網路的應用,給定一個輸入,預測一個輸出。而其他的情形都需要應用RNN模型。one to many的一個例子是影象標註(Image Captioning),輸入一個圖片,得到對圖片的語言描述,這是一個序列輸出。對於many to one,其應用例項如情感分類(Sentiment Classification),給定一句話判斷其情感,其中輸入是序列。第一種Many to many的典型應用場景是機器翻譯,比如一句英文,輸出一句中文,這時輸入與輸出都是序列。第二種many to many可以應用在視訊分類問題(Video classification on frame level),輸入一段視訊,對每一幀圖片分類。因此,可見RNN模型廣泛應用在各種業務場景中。
RNN實踐
最後我們使用RNN模型實現一個簡單的二進位制加法器。任何一個整數都可以用一個二進位制串來表示,給定兩個二進位制串,我們希望生成表示其和的二進位制串。一個二進位制串可以看成一個序列,這可以用RNN來搭建模型。先上圖來說明:
圖3 二進位制加法器(來源:angelfire.com)
二進位制器加法器從左向右開始計算,通過兩個運算數對應位上二進位制數來得到新的二進位制數。但是你要考慮運算溢位的問題,圖上彩色方框中的1表示的是運算溢位後的“攜帶位”,你需要將其傳遞給下一位的運算。好吧,這是序列依賴關係,到了RNN發揮作用了。你就想象著上一個時刻的隱含特徵儲存這個“攜帶位”資訊就可以了,這樣當前時刻的運算就可以捕獲到前面運算溢位得到的“攜帶位”。不過,這裡的時刻指的是位置。
那麼,現在開始設計這個RNN模型,首先肯定的這是many to many的例子。假定二進位制串長度為L,那麼時間步長為L,而且每個時刻的輸入特徵的維度是2。利用RNN模型,我們可以得到每個時刻的隱含特徵,這個特徵維度大小可以自定義,這裡我們取16。將隱含特徵送入輸出層,得到預測結果,我們希望預測輸出的維度是1,並且值限制在0和1這兩個數。此時可以使用sigmoid啟用函式,將值限制在[0,1]範圍內,這個值大於0.5取1,反之取0。Python實現的程式碼如下:
import numpy as np
# sigmoid
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
# sigmoid導數
def sigmoid_derivative(output):
return output * (1.0 - output)
# 生成整數與二進位制數轉化字典
int2binary = {}
binary_dim = 8
largest_number = pow(2, binary_dim)
binary = np.unpackbits(np.array([range(largest_number)], dtype=np.uint8).T,
axis=1)
for i in range(largest_number):
int2binary[i] = binary[i]
# 模型引數
input_dim = 2
hidden_dim = 16
output_dim = 1
learing_rate = 1e-1
# 初始化模型引數
# 模型: h(t) = sigmoid(Ux + Vh(t-1)) -> output(t) = sigmoid(Wh(t))
U = np.random.randn(input_dim, hidden_dim)
V = np.random.randn(hidden_dim, hidden_dim)
W = np.random.randn(hidden_dim, output_dim)
# 初始化引數梯度
dU = np.zeros_like(U)
dV = np.zeros_like(V)
dW = np.zeros_like(W)
iterations = 20000
# 訓練過程:不使用batch
for i in range(iterations):
# 生成一個簡單的加法問題 (a+b = c), a, b 除以2防止c溢位
a_int = np.random.randint(largest_number / 2)
a = int2binary[a_int]
b_int = np.random.randint(largest_number / 2)
b = int2binary[b_int]
c_int = a_int + b_int
c = int2binary[c_int]
d = np.zeros_like(c)
# 訓練樣本
X = np.array([a, b]).T
y = np.array([c]).T
loss = 0 # 損失函式
hs = [] # 儲存每個時間步長下的隱含特徵
hs.append(np.zeros((1, hidden_dim))) # 初始化0時刻特徵為0
os = [] # 儲存每個時間步長的預測值
# forward過程
for t in range(binary_dim):
# 當前時刻特徵
xt = X[binary_dim - t - 1]
# 隱含層
ht = sigmoid(xt.dot(U) + hs[-1].dot(V))
# 輸出層
ot = sigmoid(ht.dot(W))
# 儲存結果
hs.append(ht)
os.append(ot)
# 計算loss,採用L1
loss += np.abs(ot - y[binary_dim - t - 1])[0][0]
# 預測值
d[binary_dim - t - 1] = np.round(ot)[0][0]
# backward過程
future_d_ht = np.zeros((1, hidden_dim)) # 從上一個時刻傳遞的梯度
for t in reversed(range(binary_dim)):
xt = X[binary_dim - t - 1].reshape(1, -1)
ht = hs[t+1]
ht_prev = hs[t]
ot = os[t]
# d_loss/d_ot
d_ot = ot - y[binary_dim - t - 1]
d_ot_output = sigmoid_derivative(ot) * d_ot
dW += ht.T.dot(d_ot_output)
d_ht = d_ot_output.dot(W.T) + future_d_ht # 別忘來了上一時刻傳入的梯度
d_ht_output = sigmoid_derivative(ht) * d_ht
dU += xt.T.dot(d_ht_output)
dV += ht_prev.T.dot(d_ht_output)
# 更新future_d_ht
future_d_ht = d_ht_output.dot(V.T)
# SGD更新引數
U -= learing_rate * dU
V -= learing_rate * dV
W -= learing_rate * dW
# 重置梯度
dU *= 0
dV *= 0
dW *= 0
# 輸出loss和預測結果
if (i % 1000 == 0):
print("loss:" + str(loss))
print("Pred:" + str(d))
print("True:" + str(c))
out = 0
for index, x in enumerate(reversed(d)):
out += x * pow(2, index)
print(str(a_int) + " + " + str(b_int) + " = " + str(out))
print("------------")
最後經過一定訓練步長之後,得到的二進位制加法器效果還是非常好的:
總結
本文簡單介紹了RNN的原理以及應用場景,並給出了一個RNN的純Python例項,後序大家可以學習更復雜的應用例項,也可以深入瞭解RNN的變種如LSTM等模型。
參考資料
1. Recurrent Neural Networks Tutorial, Part 1 – Introduction to RNNs: http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/.
2. Understanding LSTM Networks: http://colah.github.io/posts/2015-08-Understanding-LSTMs/.
3. Anyone Can Learn To Code an LSTM-RNN in Python (Part 1: RNN): http://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/.