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。所以:
h t = t a n h ( U x t + W h t − 1 ) a t = s o f t m a x ( V h t ) h_t=tanh(Ux_t+Wh_{t-1}) \\a_t=softmax(Vh_t) ht=tanh(Uxt+Wht1)at=softmax(Vht)

损失函数,这里我们选择交叉熵:

L o s s = − ∑ i = 1 n y t ( i ) ln ⁡ a t ( i ) Loss =- \sum_{i=1}^{n}y_t(i)\ln a_t(i) Loss=i=1nyt(i)lnat(i)
其中 y t ( i ) y{_t}(i) yt(i)表示真实值(标签)的第 i i i个值, a t ( i ) a_t(i) at(i)表示输出值的第 i i i个值, n n n表示输出向量的长度。

2. 反向传播

反向传播是将误差通过输出层->隐含层->输入层逐层反传,并通过激活函数的导函数将误差分摊给各层的所有单元,从而获得各层单元的修正信号,并以信号作为依据修正各单元的权重。RNN的反向传播是从最后一个时间将累积的残差传递回来,所以梯度是从最后一个时刻开始计算,然后依次往前更新。

从结构图可以看出,Simple-RNN一共包含三个权重:
隐含层到输出层权重:V
输入层到隐含曾权重:U,W

这里将上面前向传播的式子拆开:
l a y e r 1 t = U x t + W h t − 1         h t = t a n h ( l a y e r 1 t ) l a y e r 2 t = V h t             a t = s o f t m a x ( l a y e r 2 t ) layer1_t=Ux_t+Wh_{t-1}    h_t=tanh(layer1_t)\\layer2_t=Vh_t      a_t=softmax(layer2_t) layer1t=Uxt+Wht1    ht=tanh(layer1t)layer2t=Vht      at=softmax(layer2t)
对于时刻 t t t,先看最简单的权重 V V V,它的更新只与输出层的误差有关,说白了就是一个全连接层。关于softmax和交叉熵的求导过程可以参考这篇博客: 简单易懂的softmax交叉熵损失函数求导

t t t时刻,总的 L o s s Loss Loss l a y e r 2 i layer2_i layer2i的偏导为(这里下标 i i i表示输出向量的第 i i i项):
α L o s s α l a y e r 2 i = ∑ j = 1 n ( α L o s s j α a j α a j α l a y e r 2 i ) \frac{\alpha Loss}{\alpha layer2_i}=\sum_{j=1}^{n}(\frac{\alpha Loss_j}{\alpha a_j}\frac{\alpha a_j}{\alpha layer2_i}) αlayer2iαLoss=j=1n(αajαLossjαlayer2iαaj)
首先看 L o s s j Loss_j Lossj a j a_j aj的偏导,
α L o s s j α a j = − y j 1 a j \frac{\alpha Loss_j}{\alpha a_j} = -y_j\frac{1}{a_j} αajαLossj=yjaj1
然后看 a j a_j aj l a y e r 2 i layer2_i layer2i的偏导。注意这里是 j j j i i i
α a j α l a y e r 2 i = { a i ( 1 − a i ) , if  i = j − a j a i , 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αaj={ai(1ai),ajai,if i=jelse
∇ l a y e r 2 i = α L o s s α l a y e r 2 i = ∑ j ≠ i n − y j 1 a j ( − a j a i ) + ( − y i 1 a i ) ( a i ( 1 − a i ) ) = ∑ j ≠ i n y j a i + y i a i − y i = a i ∑ j = 1 n y j − y i       \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    layer2i=αlayer2iαLoss=j̸=inyjaj1(ajai)+(yiai1)(ai(1ai))=j̸=inyjai+yiaiyi=aij=1nyjyi   
针对分类问题,真值y是一个独热码,只有一位为1其余均为0,所以上述梯度为:
∇ l a y e r 2 i = a i − y i \nabla layer2_i=a_i-y_i layer2i=aiyi
总的这一层的梯度为:
∇ l a y e r 2 = − α L o s s α l a y e r 2 = − [ ∇ l a y e r 2 1 , . . . , ∇ l a y e r 2 n ] \nabla layer2=-\frac{\alpha Loss}{\alpha layer2}= -[\nabla layer2_1,...,\nabla layer2_n] layer2=αlayer2αLoss=[layer21,...,layer2n]
这里加上一个负号,是为了让后面更新权重的时候用加法。如果这里不加负号,后面更新权重就用减法。最后 ∇ l a y e r 2 \nabla layer2 layer2其实是一个向量 ∇ l a y e r 2 = − [ a 1 , a 2 , . . . , ( a k − 1 ) , . . . , a n ] \nabla layer2=-[a_1,a_2,...,(a_k-1),...,a_n] layer2=[a1,a2,...,(ak1),...,an],这里 k k k就是标签中独热码为1的那一位,也就是除了这一位其它位的梯度都一样是softmax的值本身。
∇ V = − α L o s s α l a y e r 2 α l a y e r 2 α V = h t T ∇ l a y e r 2 \nabla V= -\frac{\alpha Loss}{\alpha layer2}\frac{\alpha layer2}{\alpha V}=h_t^T\nabla layer2 V=αlayer2αLossαVαlayer2=htTlayer2
然后,再来看 W 和 U W和U WU,由于它们的梯度依赖于之前的状态,所以需要从后向前依次求解:
首先是 t a n h ( x ) tanh(x) tanh(x)的梯度:
α t a n h ( x ) α x = 1 − t a n h ( x ) 2 \frac{\alpha tanh(x)}{\alpha x}=1-tanh(x)^2 αxαtanh(x)=1tanh(x)2
然后
∇ l a y e r 1 = ( 1 − h t 2 ) ∗ ( V T ∇ l a y e r 2 + W T ∇ l a y e r 1 n e x t ) \nabla layer1 = (1-h_t^2)*(V^T\nabla layer2+W^T\nabla layer1_{next}) layer1=(1ht2)(VTlayer2+WTlayer1next)
∇ W = h t − 1 T ∇ l a y e r 1 ∇ U = x t ∇ l a y e r 1 \nabla W=h_{t-1}^T \nabla layer1 \\\nabla U=x_t \nabla layer1 W=ht1Tlayer1U=xtlayer1
这里 ∇ l a y e r 1 n e x t \nabla layer1_{next} layer1next是下一时刻的 ∇ l a y e r 1 \nabla layer1 layer1,最后时刻 ∇ l a y e r 1 n e x t \nabla layer1_{next} layer1next初始化为0,然后每向前计算一次, ∇ l a y e r 1 n e x t \nabla layer1_{next} layer1next就更新为这次的 ∇ l a y e r 1 \nabla layer1 layer1

对每一个时刻,累积计算增量:
V u p d a t e + = ∇ V W u p d a t e + = ∇ W U u p d a t e + = ∇ U V_{update} += \nabla V\\ W_{update}+=\nabla W \\ U_{update}+=\nabla U Vupdate+=VWupdate+=WUupdate+=U
最后,更新权重:
V + = a l p h a ∗ V u p d a t e W + = a l p h a ∗ W u p d a t e U + = a l p h a ∗ U u p d a t e V+=alpha*V_{update}\\ W+=alpha*W_{update}\\ U+=alpha*U_{update} V+=alphaVupdateW+=alphaWupdateU+=alphaUupdate
其中 a l p h a alpha alpha是学习率。好了,至此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)

测试运行把这两份代码,放在一个文件就好了。如果预测结果不对,就再跑一次,样本有限有时候收敛的不好。放个结果图:
在这里插入图片描述

都看到这里了,点个赞呗!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值