BPTT算法的推導和simpleRNN的實現

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收斂而且序列的順序的確對輸出產生了影響。

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