BPTT算法
簡單RNN的結構如下圖所示,它在普通的三層全連接網絡上做出了改動,也就是在每次前饋運算的時候,考慮上一次的輸出。
這個因素的引入讓RNN變得能夠根據上文得到輸出,即不同的輸入序列將會對應不同的輸出。
上面的模型寫出前饋公式的遞歸形式很簡單
其中兩個phi表示激勵函數和整流函數,也就是隱層的激勵層和輸出層的softmax歸一層。當我們想用梯度下降對模型進行優化時,將會出現一個問題;遞歸式中的s_t-1該怎麼處理呢?
只看隱層前的V矩陣,它和遞歸不遞歸沒有任何關係,我們可以用一般方法求出梯度。
但是U和W不一樣,因爲如果s_t-1可以用上一次前饋的式子展開,而裏面又包含了U和W項,這就讓求導也要逐層前溯;簡而言之,時間點t的loss function的偏導數計算,要考慮時間1到時間t所有的W和U,並將它們求和。花書《深度學習》裏用了計算圖展開的形式來推導梯度公式,我很喜歡;如果要看文字和公式型的推導,RNN(二)· BPTT 算法. 這篇文章寫的相當不錯。
總之通過計算,可以得到相對簡潔的形式。
從當前時間t向前推一層,事實上就是在梯度計算鏈之間添了一層d_(t)/d_(t-1),在編程實踐時我們只需要用一個從t到1的循環,每次循環求當前時間的dW和dU,然後乘上d_(t)/d_(t-1),進入下一次循環。
SimpleRNN的Python實現
RNN的激活函數一般選擇tanh,因爲tanh的導數數值一般大於sigmoid,這樣在上面提到的d_(t)/d_(t-1)的連乘過程中就不容易出現梯度消失。
首先定義tanh層和softmax層,需要配備自求導方法。
import numpy as np
class Tanh:
def __init__(self):
self.out = None
def forward(self, x):
self.out = np.tanh(x)
return self.out
def backward(self, dz):
return dz*(1-self.out**2)
def softmax(input_X):
"""
Arguments:
input_X -- a numpy column vector
Return :
A: a numpy array same shape with input_X
"""
exp_a = np.exp(input_X)
sum_exp_a = np.sum(exp_a,axis=1)
ret = exp_a/sum_exp_a
return ret
class SoftMax:
'''
softmax,歸一化層,把輸出轉概率
該層的輸出代表每一分類的概率
'''
def __init__ (self):
self.y_hat = None
def forward(self,X):
self.X = X
self.y_hat = softmax(X)
return self.y_hat
def backward(self,labels):
dx = (self.y_hat-labels)
return dx
然後按照上面的正向傳播和反向傳播公式實現RNN,注意,實現反向傳播時,記錄一些歷史數據是求導所必須的;這也就是爲什麼我在存儲x和h時,使用的是矩陣的表而不是簡單的矩陣。
class RNN:
def __init__(self,input_sz,hidden_sz,output_sz):
'''
帶有記憶單元和反饋的全連接層,用於RNN
同樣擁有input_sz,output_sz,另設一個output_sz大小的mem向量
每次前向傳播的公式是V*f(h*W+x*U+b)+c
'''
self.hid_sz = hidden_sz
self.W = np.random.randn(hidden_sz,hidden_sz)*0.1 #從ht-1到ht的連接權
self.U = np.random.randn(input_sz,hidden_sz)*0.1 #從X到ht的連接權
self.b = np.random.randn(1,hidden_sz)*0.01 #在隱層激活前增加的偏置
self.V = np.random.randn(hidden_sz,output_sz)*0.1 #從h到output的連接權
self.c = np.random.randn(1,output_sz)*0.01 #在輸出前增加的偏置
self.X = [] #輸入
self.h = [np.zeros((1,hidden_sz))] #memory 的值
self.tanh = Tanh() #激活層
self.softmax = SoftMax() #輸出的歸一化層
def forward(self,X):
X = X.reshape(1,-1)
self.X.append(X)
out = X
out = self.h[-1].dot(self.W)+X.dot(self.U)+self.b
out = self.tanh.forward(out)
self.h.append(out)
out = out.dot(self.V)+self.c
out = self.softmax.forward(out)
self.out = out
return out
def fit(self, seq, y, lr):
#因爲RNN涉及時間相關的導數,我們先用表記錄每一步的偏導,最後合併更新
dW = np.zeros(self.W.shape)
dU = np.zeros(self.U.shape)
dc = np.zeros(self.c.shape)
db = np.zeros(self.b.shape)
dV = np.zeros(self.V.shape)
dtan = []
for i in range(len(seq)):
self.forward(seq[i])
dz = self.softmax.backward(y[i]) #經過softmax前的導數
dc += dz #c偏置的導數就是輸出層的導數
dV += self.h[-1].T.dot(dz) #V的導數
dtan.append(self.tanh.backward(1))
dz = dz.dot(self.V.T)
for i in range(len(dtan)-1,-1,-1):
dz *= dtan[i]
dW += self.h[i].T.dot(dz)
dU += self.X[i].T.dot(dz)
db += dz
dz = dz.dot(self.W)
self.V -= dV*lr
self.c -= dc*lr
self.W -= dW*lr
self.U -= dU*lr
self.b -= db*lr
def clear(self):
self.X = []
self.h = [np.zeros((1,self.hid_sz))]
def predict(self, seq):
y = []
for i in range(len(seq)):
y.append(self.forward(seq[i]))
self.clear()
return y
RNN的訓練資料是一段序列,必須按照序列的固定順序被網絡讀入並反向傳播,這纔是BPTT存在的意義所在。因此fit方法的輸入是sequence,而且訓練標籤y也是相同長度的sequence(其實也可以不是,但是一般我們認爲每次前向傳播的輸出都有意義,不應該捨去某一個)。
網絡的歷史輸出,也就是h會對我們測試數據造成影響,因此使用前和使用後都要進行clear操作把表清空。
然後我們簡單設計一個任務讓RNN完成,用兩段包含元素相同,但是順序不同的序列讓RNN訓練,看RNN是否能完成識別並給出正確的分類。
X0 = np.array([[[1]],[[2]],[[1]]])
y0 = np.array([[[0,1]],[[0,1]],[[0,1]]])
X1 = np.array([[[2]],[[1]],[[1]]])
y1 = np.array([[[1,0]],[[1,0]],[[1,0]]])
網絡設置10個隱藏單元,輸出爲2,因爲是二分類任務.
設置0.02學習率並進行4000次迭代。
model = RNN(1,10,2)
for epoch in range(4000):
model.fit(X0,y0,0.02)
model.clear()
model.fit(X1,y1,0.02)
model.clear()
經過訓練後,模型對序列的預測如下。
實現了正確分類,可以看出RNN收斂而且序列的順序的確對輸出產生了影響。