精华内容
下载资源
问答
  • 循环神经网络之前向反向传播算法

    千次阅读 2018-11-04 17:23:32
    今天我们学习输出和模型间有反馈的神经网络,循环神经网络(Recurrent Neual Networks),其广泛应用于自然语言处理中的语音识别,书写识别和机器翻译等领域。 1.RNN简介 前面介绍的DNN和CNN之中,训练样本的输入和...

    前面我们已经介绍了深度神经网络卷积神经网络,这些算法都是前向反馈,模型的输出和模型本身没有关联关系。今天我们学习输出和模型间有反馈的神经网络,循环神经网络(Recurrent Neual Networks),其广泛应用于自然语言处理中的语音识别,书写识别和机器翻译等领域。

    1.RNN简介

    前面介绍的DNN和CNN之中,训练样本的输入和输出都是确定的。但对于训练样本输入是连续的序列,训练样本长度不同的样本,比如一段连续的语音和手写文字,DNN和CNN是比较难处理的。而对于上述问题,RNN则是比较擅长,那么RNN是怎么做到的呢?

    RNN假设输入样本是基于序列的,比如是从序列索引1到序列索引τ,对于其中的任意序列索引号t,输入是对应样本序列的x(t)x^{(t)}。而模型在序列索引号t位置的隐藏状态h(t)h^{(t)},则由x(t)x^{(t)}和t-1时刻的隐藏状态h(t1)h^{(t-1)}共同决定。在任意序列索引号t,也有相对应的模型预测输出oto^{t}。通过预测输出o(t)o^{(t)}和训练序列的真实输出y(t)y^{(t)},以及损失函数L(t)L^{(t)},我们就可以用和DNN类似的方法来训练模型,接着用来预测测试样本的输出,下面我们来看看循环神经网络的模型。

    2.RNN模型

    01

    循环神经网络有多种模型结构,这里我们介绍最主流的模型结构。上图中左边是没有按时间序列展开的图,右边是按照时间序列展开的结构,我们重点看右边的模型结构。这里描述了在序列索引号t附近的RNN模型,下面针对一些参数做具体说明。

    • x(t)x^{(t)}代表在序列索引号t时训练样本的输入,同样x(t1)x^{(t-1)}x(t+1)x^{(t+1)}分别代表t-1时刻和t+1时刻训练样本的输入。
    • h(t)h^{(t)}代表在序列索引号t时模型的隐藏状态,h(t)h^{(t)}x(t)x^{(t)}h(t1)h^{(t-1)}共同决定。
    • o(t)o^{(t)}代表在序列索引号t时模型的输出,o(t)o^{(t)}由模型当前的隐藏状态h(t)h^{(t)}决定。
    • L(t)L^{(t)}代表在序列索引号t时模型的损失函数。
    • y(t)y^{(t)}代表在序列索引号t时训练样本序列的真实输出。
    • U,W,VU,W,V矩阵是模型的线形关系参数,在整个RNN网络间是共享的,这点和DNN不同。正是因为参数的共享,体现了RNN模型循环反馈的思想。

    3.RNN前向传播算法

    根据上面介绍的模型,我们来看一下RNN前向传播算法,对于任意时刻序列索引号t,能够得到当前的隐藏状态。其中σ为RNN的激活函数,一般是tanh,b为偏倚系数。
    h(t)=σ(z(t))=σ(Ux(t)+Wh(t1)+b) h^{(t)} = \sigma(z^{(t)}) = \sigma (Ux^{(t)} + Wh^{(t-1)} + b)
    序列索引号t时模型的输出o(t)o^{(t)}
    o(t)=Vh(t)+c o^{(t)} = Vh^{(t)} + c
    最终能够得到模型的预测输出,由于RNN是识别类的分类模型,所以下式激活函数一般是softmax函数。
    y^(t)=σ(o(t)) \hat{y}^{(t)}= \sigma (o ^{(t)})
    最后通过损失函数L(t)L^{(t)},比如对数似然损失函数,我们可以量化模型在当前位置的损失,即y^(t)\hat{y}^{(t)}y(t)y^{(t)}的差距。

    4.RNN反向传播算法

    RNN反向传播算法和DNN思路相同,即通过梯度下降法进行迭代,得到合适的RNN模型参数U,W,V,b,c,传播过程中所有的参数在序列中各个位置是共享的,即反向传播中我们更新的是相同的参数。为了简化描述,反向传播时损失函数采用对数损失函数,隐藏层的激活函数为tanh函数,输出的激活函数为softmax函数。

    对于RNN,由于我们在序列各位置都有损失函数,因此最终的损失函数L为
    L=t=1τL(t) L = \sum_{t=1}^{\tau}L^{(t)}
    其中V,c的梯度计算比较简单,如下所示
    Lc=t=1τLtc=t=1τL(t)o(t)o(t)c=t=1τ(y^(t)y(t)) \frac{\partial L}{\partial c} = \sum _{t=1}^{\tau} \frac{\partial L^{t}}{\partial c} = \sum _{t=1}^{\tau} \frac{\partial L^{(t)}}{o^{(t)}} \frac{\partial o^{(t)}}{\partial c} = \sum_{t=1}^{\tau} (\hat{y}^{(t)} - y^{(t)})

    LV=t=1τLtV=t=1τL(t)o(t)o(t)V=t=1τ(y^(t)y(t))(h(t))T \frac{\partial L}{\partial V} = \sum _{t=1}^{\tau} \frac{\partial L^{t}}{\partial V} = \sum _{t=1}^{\tau} \frac{\partial L^{(t)}}{o^{(t)}} \frac{\partial o^{(t)}}{\partial V} = \sum_{t=1}^{\tau} (\hat{y}^{(t)} - y^{(t)})(h^{(t)})^T

    针对W,U,b的梯度计算比较复杂,从RNN模型可以看出,在反向传播时,在某一序列位置t的梯度损失,由当前位置的输出对应的梯度损失和序列索引位置t+1时的梯度损失两部分共同决定。对于W在某一序列位置t的梯度损失需要反向传播一步步来进行计算,此处定义序列索引t位置的隐藏状态梯度为
    δ(t)=Lh(t) \delta^{(t)} = \frac{\partial L}{\partial h^{(t)}}
    这样便可以像DNN一样从δ(t+1)\delta^{(t+1)}递推得到δ(t)\delta^{(t)}
    δ(t)=Lo(t)o(t)h(t)+Lh(t+1)h(t+1)h(t)=VT(y^(t)y(t))+WTδ(t+1)diag(1(h(t+1))2) \delta^{(t)} = \frac{\partial L}{\partial o^{(t)}}\frac{\partial o^{(t)}}{\partial h^{(t)}} + \frac{\partial L}{\partial h^{(t + 1)}}\frac{\partial h^{(t + 1)}}{\partial h^{(t)}} = V^T(\hat{y}^{(t)} - y^{(t)}) + W^T \delta^{(t+1)} diag(1-(h^{(t+1)})^2)
    对于δ(τ)\delta^{(\tau)}后面没有其他的序列索引,因此有
    δ(τ)=Lo(τ)o(τ)h(τ)=VT(y^(τ)y(τ)) \delta^{(\tau)} = \frac{\partial L}{\partial o^{(\tau)}}\frac{\partial o^{(\tau)}}{\partial h^{(\tau)}} = V^T(\hat{y}^{(\tau)} - y^{(\tau)})
    有了δ(t)\delta^{(t)}之后计算W,U,b也就很方便了,相应表达式如下所示
    LW=t=1τLh(t)h(t)W=t=1τdiag(1(h(t))2)δ(t)(h(t1))T \frac{\partial L}{\partial W} = \sum_{t=1}^{\tau} \frac{\partial L}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial W} = \sum _{t=1} ^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}(h^{(t-1)})^T

    LU=t=1τLh(t)h(t)U=t=1τdiag(1(h(t))2)δ(t)(x(t))T \frac{\partial L}{\partial U} = \sum_{t=1}^{\tau} \frac{\partial L}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial U} = \sum _{t=1} ^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}(x^{(t)})^T

    Lb=t=1τLh(t)h(t)b=t=1τdiag(1(h(t))2)δ(t) \frac{\partial L}{\partial b} = \sum_{t=1}^{\tau} \frac{\partial L}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial b} = \sum _{t=1} ^{\tau}diag(1-(h^{(t)})^2)\delta^{(t)}

    4.RNN梯度爆炸和梯度消失

    RNN虽然理论上可以很好的解决序列数据的训练,但存在梯度爆炸和梯度消失问题,当序列很长的时候问题尤为严重。为什么会出现梯度爆炸和梯度消失问题呢?我们来看看反向传播过程中UU的变化
    LU=t=1τLh(t)h(t)U=t=1τLh(t+1)h(t+1)h(t)h(t)U=t=1τδ(t+1)h(t+1)h(t)diag(1(h(t))2)(x(t))T \frac{\partial L}{\partial U} = \sum_{t=1}^{\tau} \frac{\partial L}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial U} = \sum _{t=1} ^{\tau}\frac{\partial L}{\partial h^{(t+1)}} \frac{\partial h^{(t+1)}}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial U} = \sum _{t=1} ^{\tau}\delta^{(t+1)} \frac{\partial h^{(t+1)}}{\partial h^{(t)}} diag(1-(h^{(t)})^2)(x^{(t)})^T
    因为h(t)=tanh(z(t))=tanh(Ux(t)+Wh(t1)+b)h^{(t)} = \tanh (z^{(t)}) = \tanh (Ux^{(t)} + Wh^{(t-1)} + b),所以h(t+1)h(t)\frac{\partial h^{(t+1)}}{\partial h^{(t)}}
    h(t+1)h(t)=diag(1(h(t+1))2)WT \frac{\partial h^{(t+1)}}{\partial h^{(t)}} = diag(1-(h^{(t+1)})^2)W^T
    如下图所示,其中tanh1{\tanh}' \leq 1 ​。对于训练过程中大部分情况tanh的导数是小于1的,如果W也是大于0小于1的值,那么传播下去便会趋于0。同理当W很大时,传播下去便会趋于无穷,上述就是RNN之中梯度消失和梯度爆炸的原因。
    02

    5.其他

    03

    04

    上面总结了通用的RNN模型的前向传播算法和反向传播算法,当然RNN还有很多其他的模型,比如多层RNN、双向循环RNN(如上图所示),在前向和反向传播时公式自然也会不同,但基本原理类似,有兴趣可查询其他资料继续学习。

    RNN存在梯度爆炸和梯度消失问题,但怎么解决呢,下篇文章我们来介绍LSTM算法,看如何解决传播时出现的梯度爆炸和梯度消失问题。

    6.推广

    更多内容请关注公众号谓之小一,若有疑问可在公众号后台提问,随时回答,欢迎关注,内容转载请注明出处。
    在这里插入图片描述

    展开全文
  • 循环神经网络反向传播算法其实只是BP算法的一个简单变体而已。 首先我们先看看循环神经网络的前向传播算法: 需要注意的是,该RNN中前一时刻到当前时刻只有一个权重矩阵,该权重矩阵与时间并没有什么关系。...

    循环神经网络的反向传播算法其实只是BP算法的一个简单变体而已。

    首先我们先看看循环神经网络的前向传播算法:

    需要注意的是,该RNN中前一时刻到当前时刻只有一个权重矩阵,该权重矩阵与时间并没有什么关系。整个前向传播算法与BP网络的前向传播算法的差别是多了一个前一时刻隐藏层的信息而已。在我们这边的前向传播算法可能与大家平时看到的会有点出入,因为这个前向传播算法将传播过程中的各个阶段都拆分开来表示。在进入激活函数前先用额外的两个变量表示,分别是进入隐藏层激活函数e前的和进入输出层激活函数g前的。进行这样的拆分是为了更好的实现反向传播算法,即链式求导法则。

    RNN的前向传播算法是以时间的推移进行的,其实就是一些时序上的数据,比如音频中分帧后的得到的一系列的音频信号帧。RNN的输入数据与其他神经网络如DNN的输入数据不同的是,RNN的输入数据样本不能打乱,必须按时间进行输入,而其他的神经网络输入数据可以打乱。所RNN与其他神经网络的前向传播算法和反向传播算法的实现其实并没有什么特别之处,只是多了几个变量。因此RNN的关于时间的t循环可以看做,第t个训练样本(或者第t个批量训练样本),但是一定要保持先后顺序,不然学习出来的可能没有什么用处。

    接下来给出BPTT算法:

    从BPTT算法可以看出,要求的梯度必须先求的梯度,因为从前向传播算法我们可以知道是直接决定的值的。类似的要求的梯度必须先求的梯度(上述算法中有错误,从第6行开始应该都改为,请大家注意一下)。这就是神经网络求导中的链式法则(chain rule)。

    BPTT算法详解:

    其中,

                                       
           其中,

    其中,

    其中,

                                                                    

           其中,

           

           其中,         

                                     

           其中,

          

           其中,

                        

           其中,

    第10步骤中的就是用来更新第5步骤中右边的

    展开全文
  • 循环神经网络反向传播算法其实只是BP算法的一个简单变体而已。 首先我们先看看循环神经网络的前向传播算法: 需要注意的是,该RNN中前一时刻到当前时刻只有一个权重矩阵,该权重矩阵与时间并没有什么...

    http://blog.csdn.NET/linmingan/article/details/50958304

    循环神经网络的反向传播算法其实只是BP算法的一个简单变体而已。

    首先我们先看看循环神经网络的前向传播算法:


    需要注意的是,该RNN中前一时刻到当前时刻只有一个权重矩阵,该权重矩阵与时间并没有什么关系。整个前向传播算法与BP网络的前向传播算法的差别是多了一个前一时刻隐藏层的信息而已。在我们这边的前向传播算法可能与大家平时看到的会有点出入,因为这个前向传播算法将传播过程中的各个阶段都拆分开来表示。在进入激活函数前先用额外的两个变量表示,分别是进入隐藏层激活函数e前的和进入输出层激活函数g前的。进行这样的拆分是为了更好的实现反向传播算法,即链式求导法则。

    RNN的前向传播算法是以时间的推移进行的,其实就是一些时序上的数据,比如音频中分帧后的得到的一系列的音频信号帧。RNN的输入数据与其他神经网络如DNN的输入数据不同的是,RNN的输入数据样本不能打乱,必须按时间进行输入,而其他的神经网络输入数据可以打乱。所RNN与其他神经网络的前向传播算法和反向传播算法的实现其实并没有什么特别之处,只是多了几个变量。因此RNN的关于时间的t循环可以看做,第t个训练样本(或者第t个批量训练样本),但是一定要保持先后顺序,不然学习出来的可能没有什么用处。

    接下来给出BPTT算法:


    从BPTT算法可以看出,要求的梯度必须先求的梯度,因为从前向传播算法我们可以知道是直接决定的值的。类似的要求的梯度必须先求的梯度(上述算法中有错误,从第6行开始应该都改为,请大家注意一下)。这就是神经网络求导中的链式法则(chain rule)。

    BPTT算法详解:

     

    其中,

                                       
           其中,

     

    其中,

     

    其中,

                                                                    

           其中,

           

           其中,         

                                     

           其中,

          

           其中,

                        

           其中,

    第10步骤中的就是用来更新第5步骤中右边的

    展开全文
  • 本文着重讨论循环神经网络(Recurrent Neural Network,RNN)的数学推导过程(主要是反向过程),不会对RNN做全面的介绍,其特点,缺点假定读者已经了解,并且假定读者已经掌握全连接(FC)神经网络的原理和相关函数...

    2e0e71233bf039b7897b37119754d578.png

    本文着重讨论循环神经网络(Recurrent Neural Network,RNN)的数学推导过程(主要是反向过程),不会对RNN做全面的介绍,其特点,缺点假定读者已经了解,并且假定读者已经掌握全连接(FC)神经网络的原理和相关函数的求导。当然,不清楚也没关系,下面的文章了解一下:

    Numpy实现神经网络框架(1)
    Numpy实现神经网络框架(2)——梯度下降、反向传播
    Numpy实现神经网络框架(3)——线性层反向传播推导及实现
    Softmax与交叉熵损失的实现及求导


    本文后半部分更新了一种方法,使用更简单的结构来表示循环层,同时作为LSTM的铺垫


    正文开始

    为了使本文看起来更有条理性,并对相关符号进行说明,先从普通的神经网络开始,一个简单的全连接网络可以表示如下

    dd837bbda896ab69680edd94cbed1ec2.png

    其中

    是输入样本,
    是隐藏层的激活值,
    是网络的输出。使用
    表示输入层到隐藏层的权重矩阵,
    表示隐藏层的阈值向量;
    表示隐藏层到输出层的权重矩阵,
    表示输出层的阈值向量,那么其计算公式:

    其中

    分别是隐藏层的
    加权输入激活函数
    是输出层加权输入和激活函数,
    可以使用tanh,sigmoid函数等等,
    通常使用softmax函数

    然后,再来看一个简单的RNN图示

    4ba417f287e309b002500346b01d4ba7.png

    上图相比全连接网络,就

    节点多了一个环。如果光看这个的话肯定是一头雾水的,所以将上图展开

    d6b203f8b0095ce9e957c3ef8cce88b8.png

    其中

    是一个向量序列,分别是时间序列
    的输入,相应的,
    是隐藏层——现在叫做
    循环层——的激活值,由于
    一般设置为0,所以上面有虚线表示

    可以看到,循环层不光取决于当前时刻的输入

    ,还受到其
    上一时刻的激活值影响,也就是说循环层具有记忆性,接着来看看计算公式

    相比全连接网络,主要变化在于

    也就是上式中标红的部分,

    是循环层上一时刻的激活值参与当前时刻计算的权重矩阵。另外一个值得注意的是,在正向计算的过程中,随着时间的推移,只有
    是不断变化的,各种参数
    等都是
    各个时刻共享

    上面的图示中,循环层只画了一个,实际中也可以使用多个,组成深度网络。不过即使有多个循环层,它们也是独立进行正、反向计算,所以只用一个循环层的例子弄清它的原理即可。接着是带上损失函数的图像

    0477f497c52be14ccefa072e0202e1ab.png

    表示对应输入
    的标签值,
    表示第
    个时间序列的损失,另外这里的损失函数使用交叉熵损失函数,详细原理及反向推导可参见文章开头的链接

    下面,我们从一个具体例子入手(包含三个时间序列),来推导RNN的反向过程

    b5c8c08f85a15687951fe8be4f54af95.png

    反向传播

    首先,将正向计算的图拿过来,并且将其他节点都挖掉,只保留代表输入的节点,可以看作是RNN正向过程的一个框架,从箭头方向能够看正向过程的计算方向

    5c4c001d095e746c4137788d28432ce1.png

    那么相应的,反向过程从直觉上来说,应该是按照与正向过程相反的方向进行计算,大致可以表示如下

    c64c00ff86389e792bcef69ca167eff3.png

    表示
    三个损失的梯度,它们沿着与正向过程相反的方向计算,至于如何计算,就是我们下面要讨论的

    因为循环层是网络中唯一与之前的时刻有关联的,所以先把与之前时间序列无关的变量的梯度给求出来。为了具体的解释这句话的涵义,来看一张更详细的图

    3709937fffde8516f6b643cefb26371c.png

    然后将

    的公式拿过来,展开

    可以看到,真正参与计算

    的上一时刻的变量是
    ,而不是

    所以对于

    这些变量,由于它们并不影响到其他时刻的损失,所以只有当前时刻的损失
    需要对它们计算梯度:
    ,如下图所示

    17fbbe0a30c98309850fa620e4719825.png
    红框中的变量不影响其他时刻的损失

    首先来看输出

    ,由于函数
    通常是softmax函数

    根据前面那篇softmax与交叉熵损失函数的文章可知,

    对于
    的梯度可以不用算,直接计算
    对于
    的梯度

    然后是

    求偏导

    可以得到当前时刻损失对于这两个参数的梯度

    因为这两个参数每个时刻的梯度都是独立的,所以可以直接将所有时刻的梯度求和得到总梯度

    然后是循环层激活函数

    求导(可以使用多种激活函数,所以不展开)

    整理得到

    其中

    表示按元素相乘,从这里开始,就牵涉到循环层了,需要将之前时刻的输出值考虑进来,为了简单起见,我们先只针对
    求梯度,将整个流程理顺,然后再推出其他的参数的梯度

    我们首先从正向计算的方向来推导,从最简单的

    入手,因为
    不存在(或者为0)

    容易得到

    所以

    的梯度为

    然后再来看

    的公式,并且把
    展开

    接着再计算

    关于
    的偏导

    代入得到

    所以

    的梯度为

    最后是

    计算梯度

    的梯度

    整理得到

    以上是从正向计算入手,通过求导来得到参数的梯度,虽然也能看出规律,但是还是不如相反的方向计算更符合直觉。所以我们将上面上个式子调整一下

    因为总梯度等于各个时刻梯度之和

    所以将公式调整后总体梯度并不会改变。接着我们从调整后的公式入手,从反向推导得到相同的结论。首先从

    开始,示意图如下:

    a074245966c880a643f40522e5f2e48e.png

    因为没有

    时刻,所以
    时只用考虑当前时刻的梯度传播,只要一路往回传即可

    再此基础上,我们考虑

    的相关梯度向
    传播,首先从前面的变形后的公式可以得到

    图示如下

    0f94abbab502db6396eeed63db28dfe6.png

    上图中蓝底十字表示将

    乘上
    ,再与
    相加,即前面公式中的

    接着,再将

    的累积梯度一起传回
    ,还是先把公式拿来

    图示如下

    6fc72ea99cac6f6a98ec89d8738b11bb.png

    上图中将

    时刻也增加了一个加号,但是由于没有
    传来的梯度,所以没有影响

    另外前面的公式还是有点复杂,我们继续简化,再添加一个变量

    表示当前时刻损失对
    的直接梯度(上图垂直方向),加上
    时刻传来的累积梯度(水平方向):

    各个时刻的

    的梯度可以写成

    整体梯度

    最终表示

    cc78f74512bd5336685d9b04c5393c67.png

    这样一来,和之前猜测的结构基本是一样的。这是一个符合直觉的结果,因为

    会影响到它当前时刻及之后时刻的损失,所以
    在自然也对
    计算梯度,再来看看
    的公式

    由于
    是当前和后续时间序列损失的累积梯度,所以计算
    的偏导时不需要再将
    展开,因此容易得到其他参数的梯度

    以及

    的梯度(如果需要往上一层传梯度的话,不是上一时刻)

    所有需要计算的梯度的公式已经推导出来了,最后简单实现一个循环层

    class RecurrentLayer(object):
        def __init__(self, shape):
            '''
            shape: (输出向量长度,输入向量长度)
            '''
            # 初始化正向计算的参数
            self.h = 0
            self.b = np.zeros(shape[0])
            self.W_xh = np.random.randn(*shape)
            self.W_hh = np.random.randn(shape[0], shape[0])
            # 保存各个时刻的输入、激活值
            self.h_list = []
            self.x_list = []
            # 初始化反向计算梯度
            self.delta = 0      # 反向计算中的累积梯度
            self.d_W_xh = 0     # 全部时刻损失对于 W_xh 的梯度
            self.d_W_hh = 0     # 全部时刻损失对于 W_hh 的梯度
            self.d_b = 0        # 全部时刻损失对于 b 的梯度
    
        def tanh(self, x):  # tanh激活函数
            ex = np.exp(x)
            esx = np.exp(-x)
            return (ex - esx) / (ex + esx)
    
        def forward(self, x):
            '''
            x: i时刻的输入
            '''
            # 保存前一时刻激活值 和 当前时刻输入
            self.h_list.append(self.h)
            self.x_list.append(x)
            # 计算并返回当前时刻激活值
            u = np.dot(self.W_xh, x) + np.dot(self.W_hh, self.h) + self.b
            self.h = self.tanh(u)
            return self.h
    
        def backward(self, d_h):
            '''
            d_h: 反向计算中 i 刻损失 L_i 对 h_i 的梯度
            '''
            d_u = d_h * (1-np.square(self.h))   # 当前时刻损失 L_i 对 u_i 的梯度
            self.delta = d_u + self.delta       # 计算 u_i 的累积梯度
            self.h = self.h_list.pop()          # 弹出 i-1时刻的激活值
            # 根据 u_i 的累积梯度,计算参数的梯度并累加
            self.d_W_xh = self.d_W_xh + np.dot(self.delta, self.x_list.pop().T)
            self.d_W_hh = self.d_W_hh + np.dot(self.delta, self.h.T)
            self.d_b = self.d_b + self.delta
    
            res = np.dot(self.W_xh.T, self.delta)       # x_i 的梯度,往回传
            tmp = self.W_hh.T * (1-np.square(self.h))   # u_i 对于 u_{i-1} 的梯度
            self.delta = np.dot(tmp, self.delta)        # 更新累积梯度,以便 i-1 时刻反向计算时使用
            return res

    更新部分

    以下部分的内容作为下一篇关于LSTM(long short term memory,长短时记忆网络)的铺垫,不会涉及LSTM的内容,只是将循环层的结构进行简化——整体功能当然是和原来一样——以便帮助理解下一篇的LSTM

    先说结果,结构变化后的循环层内部包含一个线性层。其实就是将循环层模块化,把它本身视作一个小型网络,不过只从输入输出的角度来看的话,它是没有区别的:

    a9fa3cc6a7ce6103147f568f9b8de078.png

    上图表示正向过程的输入输出,水平方向的

    是沿时间序列传递的,垂直方向的
    是传给网络中下一层作为其输入。因为正向计算比较容易理解,所以直接将内部结构摆上来

    1d7e84900155c6162c78b39bd3c5603f.png

    其中每一个黑色边框的矩形表示某种操作,或者说进行某些计算,例如

    9aadb32c69351002cfe9cf443032375d.png

    上图中蓝圈中标记的部分本别表示

    ,即将输入的变量与权重矩阵相乘,另外加号:

    3e3d9708abde8fbec4281431fc67cf4e.png

    表示将两个箭头来源表示的变量进行相加,再加上阈值(如果有的话),其实就是对应着我们之前的式子:

    然后是

    7b03e8c64e793f467a90ac18c217ab74.png

    自然表示

    了,也就是将输入的变量传给激活函数计算然后输出。需要注意的是

    e17f0f29ab6990205ca0caff92a82974.png

    减号并不是减去什么,而是一个分流的表示,只是将输入的变量向两个(或多个)方向传递。之所以这么额外弄这么一个方块,因为在反向过程中,这里的减号变成了加号,表示将这两个方向的梯度进行相加。自然的,正向过程中的加号在反向过程中也变成了减号,如下图所示

    745743047b2e2333460ea73476356c53.png

    这里说明一下:

    在前面的讨论中没有出现过,它表示在
    之后所有的时间序列的损失
    的累积梯度,而不单单是
    的梯度。除此之外,
    表示将输入的量乘上
    的导数,例如
    ,则
    表示的操作为

    为了验证上图所表示的计算结果与之前一样,首先将前面的一些变量拿来回顾一下,方便后面讨论

    首先,从最后一个时刻

    开始,因为
    ,所以

    1c243cbc159672321c7c3f8925713ee4.png

    上图两个矩形表示的计算的结果为:

    然后将

    传入给减号进行分流

    59d5493a4e1ac5330e50222c07531285.png

    按照图示,我们可以得到

    关于

    的梯度与我们之前算的结果是一样的,然后是:

    传到
    时刻

    得到了

    ,到这一步结果已经明显了,
    就不用算了,自然都是一样的。既然得到了每一个时刻的
    ,那么参数的梯度也好算了

    8d5c4f4d6bcad79a51285721cabd0156.png

    根据图示,我们也可以轻松得到参数的梯度

    这样一来,得到了和我们之前计算的一样的结果,但是如果只是这样的话是没必要的,需要进一步简化,即将其内部的部分“组件”打包成一个独立的线性层来简化复杂度

    首先,来观察

    的计算

    其中红色部分可以写成矩阵乘法的形式

    然后我们令

    所以

    的计算公式就变成了

    可以用下图表示

    4c74943282733ae453b8db520d044ea2.png

    其中concat的矩形其作用不言自明,然后是下图

    74089bb2f91a8f93f8689c0c2834a87c.png

    这两部分加起来不就是一个带激活函数的线性层吗?

    cf461a5de88adb9825490c34f887c5e8.png

    正向过程就不推导了,直接来看反向过程

    d2ef363cae2e3ecf95a8c3e80d321256.png

    再来验证一下线性层反向过程的计算,对于线性层来说,其反向过程的输入为加号部分的输出,即:

    当这两个梯度之后传入线性层的反向函数后,首先是乘上激活函数的导数,得到

    接着我们就可以计算梯度了

    以及

    然后是线性层反向函数的输出

    可以看到,计算得到的结果和原来是一样的,所以内部包含的线性层是可以独立实现的:

    class Linear(object):
        def __init__(self, shape):
            '''
            shape: 权重矩阵的shape,由循环层构造
            '''
            self.W = np.random.randn(*shape)
            self.b = np.zeros(shape[0])
            # 保存输入输出的列表
            self.input_list = []
            self.output_list = []
            # 全部时刻损失对参数的梯度
            self.d_W = 0
            self.d_b = 0
    
        def tanh(self, x):  # tanh激活函数
            ex = np.exp(x)
            esx = np.exp(-x)
            return (ex - esx) / (ex + esx)
    
        def forward(self, x):
            '''
            x: 由循环层拼接 x_i 和 h_{i-1} 而成的列向量
            '''
            output = self.tanh(np.dot(self.W, x) + self.b)
            # 保存正向计算的输入输出
            self.input_list.append(x)
            self.output_list.append(output)
            return output
    
        def backward(self, d_h):
            '''
            d_h: 当前时刻损失 L_i 及之后时刻损失对 h_i 的梯度之和
            '''
            input_ = self.input_list.pop()
            output = self.output_list.pop()
            delta = d_h * (1 - np.square(output))
            # 计算当前时刻参数的梯度并累加
            self.d_b = self.d_b + delta
            self.d_W = self.d_W + np.dot(delta, input_.T)
            return np.dot(self.W.T, delta)  # 传递梯度给循环层

    基本上和普通的线性层是一样的,只不过多了两个列表保存各个时刻的输入输出用来进行反向过程的计算

    然后是循环层,代码就简单多了

    class Recurrent(object):
        def __init__(self, shape):
            '''
            shape: (输出向量长度,输入向量长度)
            '''
            linear_shape = (shape[0], shape[0]+shape[1])
            self.linear = Linear(linear_shape)
            self.h = np.zeros((shape[0],1)) # 不能再直接初始化为0
            self.d_h_ = 0                   # 反向计算时,L_{i+1} 对 h_i 的梯度
    
        def forward(self, x):
            s = np.vstack(x, self.h)        # 拼接 x_i 及 h_{i-1} 
            self.h = self.linear.forward(s) # 更新 h_i
            return self.h
    
        def backward(self, d_h):
            d_s = self.linear.backward(d_h + self.d_h_)
            self.d_h_ = d_s[-len(d_h):]     # 分割线性层的输出,更新 L_{i} 对 h_{i-1} 的梯度
            return d_s[:-len(d_h)]          # 返回 L_{i} 对 x_i 的梯度

    由此,我们通过将循环层内部的一些计算过程打包为一个独立的线性层,从而降低了复杂度,以便我们更好的理解

    展开全文
  • 在前面我们讲到了DNN,以及DNN的特例CNN的模型和前向反向传播算法,这些算法都是前向反馈的,模型的输出和模型本身没有关联关系。今天我们就讨论另一类输出和模型间有反馈的神经网络:循环神经网络(Recurrent Neural...
  • 循环神经网络反向传播算法其实只是BP算法的一个简单变体而已。 首先我们先看看循环神经网络的前向传播算法: 需要注意的是,该RNN中前一时刻到当前时刻只有一个权重矩阵,该权重矩阵与时间并没有什么关系。...
  • 前面五小节,我们简单学习了卷积神经网络(CNN)反向传播算法的推导,然后我们自己实现了一个浅层卷积神经网络。卷积神经网络在本系列中先告一段落,我们开始学习循环神经网络的相关知识。本系列的主旨是基础学习,...
  • 这篇文章是看了刘建平老师的循环神经网络(RNN)模型与前向反向传播算法后的笔记,包括一些公式推导,都是自己的理解,如有错误,欢迎指出。 RNN模型 RNN主流模型:   1. x(t)x^{(t)}x(t)代表在序列索引号ttt...
  • 深度学习(四):卷积神经网络(CNN)模型结构,前向传播算法和反向传播算法介绍。 建议在读本文之前,重点读下前2篇文章。如果不了解DNN的前向和反向传播的话,理解本篇文章会有难度。 这些算法都是前向反馈的,模型...
  • 反向传播算法是人工神经网络训练时采用的一种通用方法,在现代深度学习中得到了大 规模的应用。全连接神经网络(多层感知器模型,MLP),卷积神经网络(CNN),循环神 经网络(RNN)中都有它的实现版本。算法从多元复合...
  • RNN网络结构Elman神经网络是最早的循环神经网络,由Elman于1990年提出,又称为SRN(Simple Recurrent Network, 简单循环网络)。RNN考虑了时序信息,当前时刻的输出不仅和当前时刻的输入有关,还和前面所有时刻的...

空空如也

空空如也

1 2 3 4 5 ... 12
收藏数 225
精华内容 90
关键字:

循环神经网络反向传播算法