Simple-RNN 前向反向傳播 原理及代碼詳解

RNN是最簡單的循環神經網絡,稱謂Simple-RNN,它是LSTM的基礎。下面看結構:
在這裏插入圖片描述
輸入層到隱含層的權重用U表示,隱含層到隱含層的權重用W表示,隱含層到輸出層的權重用V表示。

1. 前向傳播

可以將網絡看成是一個三層結構,與普通BP神經網絡不同的是,輸入層有兩個部分,一個是上一時刻的隱層向量(ht-1);一個是本次的輸入向量(xt)。隱含層(ht)也會有兩個走向,一個作爲本次輸出到達輸出層;同時ht也作爲下一時刻的輸入。
公式如下,在時間步t時:
            ht = f(U.xt + W.ht-1)
            at = g(V.ht)
其中f是隱含層激活函數,一般是tanh。g是輸出層激活函數,一般是softmax。所以:
ht=tanh(Uxt+Wht1)at=softmax(Vht)h_t=tanh(Ux_t+Wh_{t-1}) \\a_t=softmax(Vh_t)

損失函數,這裏我們選擇交叉熵:

Loss=i=1nyt(i)lnat(i)Loss =- \sum_{i=1}^{n}y_t(i)\ln a_t(i)
其中yt(i)y{_t}(i)表示真實值(標籤)的第ii個值,at(i)a_t(i)表示輸出值的第ii個值,nn表示輸出向量的長度。

2. 反向傳播

反向傳播是將誤差通過輸出層->隱含層->輸入層逐層反傳,並通過激活函數的導函數將誤差分攤給各層的所有單元,從而獲得各層單元的修正信號,並以信號作爲依據修正各單元的權重。RNN的反向傳播是從最後一個時間將累積的殘差傳遞回來,所以梯度是從最後一個時刻開始計算,然後依次往前更新。

從結構圖可以看出,Simple-RNN一共包含三個權重:
隱含層到輸出層權重:V
輸入層到隱含曾權重:U,W

這裏將上面前向傳播的式子拆開:
layer1t=Uxt+Wht1    ht=tanh(layer1t)layer2t=Vht      at=softmax(layer2t)layer1_t=Ux_t+Wh_{t-1}    h_t=tanh(layer1_t)\\layer2_t=Vh_t      a_t=softmax(layer2_t)
對於時刻tt,先看最簡單的權重VV,它的更新只與輸出層的誤差有關,說白了就是一個全連接層。關於softmax和交叉熵的求導過程可以參考這篇博客: 簡單易懂的softmax交叉熵損失函數求導

tt時刻,總的LossLosslayer2ilayer2_i的偏導爲(這裏下標ii表示輸出向量的第ii項):
αLossαlayer2i=j=1n(αLossjαajαajαlayer2i)\frac{\alpha Loss}{\alpha layer2_i}=\sum_{j=1}^{n}(\frac{\alpha Loss_j}{\alpha a_j}\frac{\alpha a_j}{\alpha layer2_i})
首先看LossjLoss_jaja_j的偏導,
αLossjαaj=yj1aj\frac{\alpha Loss_j}{\alpha a_j} = -y_j\frac{1}{a_j}
然後看aja_jlayer2ilayer2_i的偏導。注意這裏是jjii
αajαlayer2i={ai(1ai),if i=jajai,else\frac{\alpha a_j}{\alpha layer2_i} = \begin{cases} a_i(1-a_i), & \text{if $i=j$} \\ -a_ja_i, & \text{else} \end{cases}
layer2i=αLossαlayer2i=jinyj1aj(ajai)+(yi1ai)(ai(1ai))=jinyjai+yiaiyi=aij=1nyjyi   \nabla layer2_i=\frac{\alpha Loss}{\alpha layer2_i}=\sum_{j \neq i}^{n}-y_j\frac{1}{a_j}(-a_ja_i) + (-y_i\frac{1}{a_i})(a_i(1-ai)) \\=\sum_{j \neq i}^{n}y_ja_i+y_ia_i-y_i \\=a_i\sum_{j=1}^{n}y_j-yi   
針對分類問題,真值y是一個獨熱碼,只有一位爲1其餘均爲0,所以上述梯度爲:
layer2i=aiyi\nabla layer2_i=a_i-y_i
總的這一層的梯度爲:
layer2=αLossαlayer2=[layer21,...,layer2n]\nabla layer2=-\frac{\alpha Loss}{\alpha layer2}= -[\nabla layer2_1,...,\nabla layer2_n]
這裏加上一個負號,是爲了讓後面更新權重的時候用加法。如果這裏不加負號,後面更新權重就用減法。最後layer2\nabla layer2其實是一個向量layer2=[a1,a2,...,(ak1),...,an]\nabla layer2=-[a_1,a_2,...,(a_k-1),...,a_n],這裏kk就是標籤中獨熱碼爲1的那一位,也就是除了這一位其它位的梯度都一樣是softmax的值本身。
V=αLossαlayer2αlayer2αV=htTlayer2\nabla V= -\frac{\alpha Loss}{\alpha layer2}\frac{\alpha layer2}{\alpha V}=h_t^T\nabla layer2
然後,再來看WUW和U,由於它們的梯度依賴於之前的狀態,所以需要從後向前依次求解:
首先是tanh(x)tanh(x)的梯度:
αtanh(x)αx=1tanh(x)2\frac{\alpha tanh(x)}{\alpha x}=1-tanh(x)^2
然後
layer1=(1ht2)(VTlayer2+WTlayer1next)\nabla layer1 = (1-h_t^2)*(V^T\nabla layer2+W^T\nabla layer1_{next})
W=ht1Tlayer1U=xtlayer1\nabla W=h_{t-1}^T \nabla layer1 \\\nabla U=x_t \nabla layer1
這裏layer1next\nabla layer1_{next}是下一時刻的layer1\nabla layer1,最後時刻layer1next\nabla layer1_{next}初始化爲0,然後每向前計算一次,layer1next\nabla layer1_{next}就更新爲這次的layer1\nabla layer1

對每一個時刻,累積計算增量:
Vupdate+=VWupdate+=WUupdate+=UV_{update} += \nabla V\\ W_{update}+=\nabla W \\ U_{update}+=\nabla U
最後,更新權重:
V+=alphaVupdateW+=alphaWupdateU+=alphaUupdateV+=alpha*V_{update}\\ W+=alpha*W_{update}\\ U+=alpha*U_{update}
其中alphaalpha是學習率。好了,至此RNN的反向傳播就結束了。下面放出詳細代碼:(代碼中的權重和層的乘積是右乘,上面公式中寫的左乘,不過相信這應該不算什麼)

import numpy as np


class RNN:

    def __init__(self, in_shape, unit, out_shape):
        '''
        :param in_shape: 輸入x向量的長度
        :param unit: 隱層大小
        :param out_shape: 輸出y向量的長度
        '''
        self.U = np.random.random(size=(in_shape, unit))
        self.W = np.random.random(size=(unit, unit))
        self.V = np.random.random(size=(unit, out_shape))

        self.in_shape = in_shape
        self.unit = unit
        self.out_shape = out_shape

        self.start_h = np.random.random(size=(self.unit,))  # 初始隱層狀態

    @staticmethod
    def tanh(x):
        return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))

    @staticmethod
    def tanh_der(y):
        return 1 - y*y

    @staticmethod
    def softmax(x):
        tmp = np.exp(x)
        return tmp/sum(tmp)

    @staticmethod
    def softmax_der(y, y_):
        j = np.argmax(y_)
        tmp = y[j]
        y = -y[j]*y
        y[j] = tmp*(1-tmp)
        return y

    @staticmethod
    def cross_entropy(y, y_):
        '''
        交叉熵
        :param y:預測值
        :param y_: 真值
        :return:
        '''
        return sum(-np.log(y)*y_)

    @staticmethod
    def cross_entropy_der(y, y_):
        j = np.argmax(y_)
        return -1/y[j]

    def inference(self, x, h_1):
        '''
        前向傳播
        :param x: 輸入向量
        :param h_1: 上一隱層
        :return:
        '''
        h = self.tanh(np.dot(x, self.U) + np.dot(h_1, self.W))
        y = self.softmax(np.dot(h, self.V))
        return h, y

    def train(self, x_data, y_data, alpha=0.1, steps=100):
        '''
        訓練RNN
        :param x_data: 輸入樣本
        :param y_data: 標籤
        :param alpha: 學習率
        :param steps: 迭代倫次
        :return:
        '''
        for step in range(steps):  # 迭代倫次
            print("step:", step+1)
            for xs, ys in zip(x_data,y_data):  # 每個樣本
                h_list = []
                h = self.start_h  # 初始化初始隱層狀態
                h_list.append(h)
                y_list = []
                losses = []
                for x, y_ in zip(xs, ys):  # 前向傳播
                    h, y = self.inference(x, h)
                    loss = self.cross_entropy(y=y, y_=y_)
                    h_list.append(h)
                    y_list.append(y)
                    losses.append(loss)
                print("loss:", np.mean(losses))
                V_update = np.zeros(shape=self.V.shape)
                U_update = np.zeros(shape=self.U.shape)
                W_update = np.zeros(shape=self.W.shape)
                next_layer1_delta = np.zeros(shape=(self.unit,))

                for i in range(len(xs))[::-1]:  # 反向傳播
                    layer2_delta = -self.cross_entropy_der(y_list[i], ys[i])*self.softmax_der(y_list[i], ys[i])  # 輸出層誤差
                    # 當前隱層梯度 = 下一隱層梯度 * 下一隱層權重 + 輸出層梯度 * 輸出層權重
                    layer1_delta = self.tanh_der(h_list[i+1])*(np.dot(layer2_delta, self.V.T) + np.dot(next_layer1_delta, self.W.T))

                    V_update += np.dot(np.atleast_2d(h_list[i+1]).T, np.atleast_2d(layer2_delta))  # V增量
                    W_update += np.dot(np.atleast_2d(h_list[i]).T,  np.atleast_2d(layer1_delta))  # W增量
                    U_update += np.dot(np.atleast_2d(xs[i]).T,  np.atleast_2d(layer1_delta))  # U增量

                    next_layer1_delta = layer1_delta  # 更新下一隱層的梯度等於當前隱層的梯度
                self.W += W_update * alpha
                self.V += V_update * alpha
                self.U += U_update * alpha
                # print(self.W,self.V,self.U)

    def predict(self, xs, return_sequence=False):
        '''
        RNN預測
        :param xs: 單個樣本
        :param return_sequence: 是否返回整個輸出序列
        :return:
        '''

        y_list = []
        h_list = []
        h = self.start_h
        for x in xs:
            h, y = self.inference(x,h)
            y_list.append(y)
            h_list.append(h)
        if return_sequence:
            return h_list, y_list
        else:
            return h_list[-1], y_list[-1]

然後做一個簡單地測試:

預測26個字母中的下一個字母

這裏偷懶只寫了前面9個字母"abcdefghi",相對需要的樣本比較少。

class RNNTest:

    def __init__(self, hidden_num, all_chars):
        '''
        創建一個rnn
        :param hidden_num: 隱層數目
        :param all_chars: 所有字符集
        '''
        self.all_chars = all_chars
        self.len = len(all_chars)
        self.rnn = RNN(self.len, hidden_num, self.len)

    def str2onehots(self, string):
        '''
        字符串轉獨熱碼
        :param string:
        :return:
        '''
        one_hots = []
        for char in string:
            one_hot = np.zeros((self.len,),dtype=np.int)
            one_hot[self.all_chars.index(char)] = 1
            one_hots.append(one_hot)
        return one_hots

    def vector2char(self, vector):
        '''
        預測向量轉字符
        :param vector:
        :return:
        '''
        return self.all_chars[int(np.argmax(vector))]

    def run(self, x_data, y_data, alpha=0.1, steps=100):

        x_data_onehot = [self.str2onehots(xs) for xs in x_data]
        y_data_onehot = [self.str2onehots(ys) for ys in y_data]
        self.rnn.train(x_data_onehot, y_data_onehot, alpha=alpha, steps=steps) # 訓練
        vector_f = self.rnn.predict(self.str2onehots("f"), False)[1] # 預測f下一個字母
        vector_ab = self.rnn.predict(self.str2onehots("ab"), False)[1] # 預測ab的下一個字母
        print("f.next=",self.vector2char(vector_f))
        print("ab.next=",self.vector2char(vector_ab))


# 測試:下一個字母
x_data = ["abc","bcd","cdef","fgh","a","bc","abcdef"]
y_data = ["bcd","cde","defg","ghi","b","cd","bcdefg"]
all_chars = "abcdefghi"

rnn_test = RNNTest(10, all_chars)
rnn_test.run(x_data,y_data)

測試運行把這兩份代碼,放在一個文件就好了。如果預測結果不對,就再跑一次,樣本有限有時候收斂的不好。放個結果圖:
在這裏插入圖片描述

都看到這裏了,點個讚唄!

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