1. 从神经网络谈起
了解神经网络的都知道,神经网络作为一种非线性模型,在监督学习领域取得了state-of-art的效果,其中反向传播算法的提出居功至伟,到如今仍然是主流的优化神经网络参数的算法. 递归神经网络、卷积神经网络以及深度神经网络作为人工神经网络的”变种”,仍然延续了ANN的诸多特质,如权值连接,激励函数,以神经元为计算单元等,只不过因为应用场景的不同衍生了不同的特性,如:处理变长数据、权值共享等。
为了介绍RNN,先简单的介绍ANN. ANN的结构很容易理解,一般是三层结构(输入层-隐含层-输出层). 隐含层输出o j 和输出层输出o k 如下。其中n e t j 为隐含层第j 个神经元的输入,u 为输入层和隐含层的连接权值矩阵,v 为隐含层和输出层之间的连接权值矩阵.
o j o k n e t j n e t k = f ( n e t j ) = f ( n e t k ) = ∑ i ( x i u i , j ) + b j = ∑ j ( o j v j , k ) + b k
定义损失函数为E p = 1 2 ∑ k ( o k − d k ) 2 ,其中p 为样本下标,o k 为第k 个输出层神经元的输出,d k 为样本在第k 个编码值。然后分别对参数v j , k 、u i , j 进行求导,可得:
∂ E p ∂ v j , k = ∂ E p ∂ n e t k ∂ n e t k ∂ v j , k = ∂ E p ∂ n e t k o j = ∂ E p ∂ o k ∂ o k ∂ n e t k o j = ( o k − d k ) o k ( 1 − o k ) o j
∂ E p ∂ u i , j = ∂ E p ∂ n e t j ∂ n e t j ∂ u i , j = x i ∑ k ∂ E p ∂ n e t k ∂ n e t k ∂ o j ∂ o j ∂ n e t j = x i ∑ k ∂ E p ∂ n e t k v j , k o j ( 1 − o j ) = x i o j ( 1 − o j ) ∑ k ∂ E p ∂ n e t k v j , k
从对∂ E p ∂ u i , j 的推导可以得到反向传播的核心思路,令误差项β k = ∂ E p ∂ n e t k , 则有:
β k = o l ( 1 − o l ) ∑ l β l w l k
反向传播的实质是基于梯度下降的优化方法,只不过在优化的过程使用了一种更为优雅的权值更新方式。
传统的神经网络一般都是全连接结构,且非相邻两层之间是没有连接的。一般而言,定长输入的样本很容易通过神经网络来解决,但是类似于NLP中的序列标注这样的非定长输入,前向神经网络却无能为力。
于是有人提出了循环神经网络(Recurrent Neural Network),这是一种无法像前向神经网络一样有可以具象的网络结构的模型,一般认为是网络隐层节点之间有相互作用的连接,其实质可以认为是多个具有相同结构和参数的前向神经网络的stacking, 前向神经网络的数目和输入序列的长度一致,且序列中毗邻的元素对应的前向神经网络的隐层之间有互联结构,其图示( 图片来源 )如下.
上图只是一个比较抽象的结构,下面是一个以时间展开的更为具体的结构(图片来源 ).
从图中可以看出,输出层神经元的输入输出和前向神经网络中没有什么差异,仅仅在于隐层除了要接收输入层的输入外,还需要接受来自于自身的输入(可以理解为t时刻的隐层需要接收来自于t-1时刻隐层的输入, 当然这仅限于单向RNN的情况,在双向RNN还需要接受来自t+1时刻的输入).
RNN的隐层是控制信息传递的重要单元,不同时刻隐层之间的连接权值决定了过去时刻对当前时刻的影响,所以会存在时间跨度过大而导致这种影响会削弱甚至消失的现象,称之为梯度消失,改进一般都是针对隐层做文章,LSTM(控制输入量,补充新的信息然后输出),GRU(更新信息然后输出)等都是这类的改进算法.
下图为某时刻隐层单元的结构示意图(图片来源 ).
虽说处理的是不定长输入数据,但是某个时刻的输入还是定长的。令t时刻:输入x t ∈ R x d i m ,t隐层输出h t ∈ R h d i m , 输出层y t ∈ R y d i m , RNN和CNN有着同样的共享权值的属性,输入层到隐层的权值矩阵V ∈ R x d i m × h d i m , 隐层到输出层的权值矩阵W ∈ R h d i m × y d i m , 不同时刻隐层自连接权值矩阵U ∈ R h d i m × h d i m 1 . RNN有着类似于CNN的权值共享特点,所以不同时刻的U,V,W都是相同的,所有整个网络的学习目标就是优化这些参数以及偏置. RNN和普通神经网络一样,也有超过三层的结构,下文的推导仅以三层为例.
3. RNN推导
令隐含层的激励函数f ( x ) , 输出层的激励函数为g ( x ) . 则有:
h t n e t t h y t n e t t y = f ( n e t t h ) = x t V + h t − 1 U + b h = g ( n e t t y ) = h t W + b y
对于单个样本,定义我们的cost function E t = 1 2 | | d t − y t | | 2 ,其中d t 为t时刻的真实输出,y t 是t时刻的预测输出。则对权值W j , k 、V i , j 、U j , r 的求导分别如下。其中j 表示隐层单元下标,k 表示输出层下标,i 表示输入层下标,r 表示下一时刻隐含层下标.n e t t h j 表示t时刻隐层第j个神经元的加权输入,n e t t y k 表示t时刻输出层第k个神经元的加权输入。
∂ E ∂ W j k ∂ E ∂ V i j ∂ E ∂ U j r = ∂ E ∂ n e t t y k ∂ n e t t y k ∂ W j k = ∂ E ∂ n e t t y k h t j = ( y t k − d t k ) y t k ( 1 − y t k ) h t j = ∂ E ∂ n e t t h j ∂ n e t t h j ∂ V i j = ∂ E ∂ n e t t h j x t i = ( ∑ k ∂ E ∂ n e t t y k ∂ n e t t y k ∂ h t j ∂ h t j ∂ n e t t h j + ∑ r ∂ E ∂ n e t t + 1 h r ∂ n e t t + 1 h r ∂ h t j ∂ h t j ∂ n e t t h j ) x t i = ( ∑ k ∂ E ∂ n e t t y k W j k + ∑ r ∂ E ∂ n e t t + 1 h r U j r ) ∂ h t j ∂ n e t t h j x t i = ∂ E ∂ n e t t h r ∂ n e t t h r ∂ U j r = ∂ E ∂ n e t t h r h t − 1 j = ( ∑ k ∂ E ∂ n e t t y k ∂ n e t t y k ∂ h t r ∂ h t r ∂ n e t t h r + ∑ j ∂ E ∂ n e t t + 1 h j ∂ n e t t + 1 h j ∂ h t r ∂ h t r ∂ n e t t h r ) h t − 1 j = ( ∑ k ∂ E ∂ n e t t y k W r k + ∑ l ∂ E ∂ n e t t + 1 h l U l r ) ∂ h t r ∂ n e t t h r h t − 1 j
令δ t y , k 为t时刻输出层y第k个神经元的误差项,令δ t h , j 为t时刻隐含层h第j个神经元的误差项, 则有:
δ t y , k δ t h , j = ∂ E ∂ n e t t y k = ( y t k − d t k ) y t k ( 1 − y t k ) = ∂ E ∂ n e t t h j = ( ∑ k ∂ E ∂ n e t t y k W j k + ∑ r ∂ E ∂ n e t t + 1 h r U j r ) ∂ h t j ∂ n e t t h j = ( ∑ k δ t y , k W j k + ∑ r δ t + 1 h , j U j r ) ∂ h t j ∂ n e t t h r
所以不难得到误差方向传递时的权值计算方式:
∂ E ∂ W j k ∂ E ∂ V i j ∂ E ∂ U j r = δ t y , k h t j = δ t h , j x t i = δ t h , r h t − 1 j
写成矩阵的形式即是(其中⋅ 表示矩阵对应位置的元素相乘):
δ t y δ t h ∂ E ∂ W j k ∂ E ∂ V i j ∂ E ∂ U j r = ( d t − y t ) ⋅ g ′ ( n e t t y ) = ( δ t y W T + δ t + 1 h U T ) ⋅ f ′ ( n e t t h ) = ( h t ) T δ t y = ( x t ) T δ t h = ( h t − 1 ) T δ t h
上述 公式其实在了解反向传播之后就能够很容易推导,对于W j k 、U j r 的推导套用反向传播公式即可,而对于V i , j 需要加上来自于下一时刻的误差,如以上式子中红色部分所示.
4. RNN实现
推导出了公式,用代码实现就很简单了,可以看到前向传递和反向传播过程中的都用到了矩阵的相关运算,封装好一个矩阵计算的类,然后重载相关的运算符,便可以十分方便的进行相关的计算了。矩阵类的实现见https://github.com/kymo/SUDL/blob/master/matrix/matrix.h ,然后直接按照上述的矩阵运算方式填写代码就可以了,详见:https://github.com/kymo/SUDL/blob/master/rnn/rnn.cpp .
5. LSTM推导
相比于普通的RNN而言,LSTM无非是多了几个门结构,求导的过程略微繁琐一点,但是只要清楚当前待求导变量到输出的路径,变可以依据链式求导法则得到对应的求导公式。LSTM是为了缓解RNN的梯度弥散问题而提出的一种变种模型,之所以能够缓解这个问题,是因为在原始的RNN中,梯度的传递是乘法的过程,如果梯度很小,那么从T时刻传递到后面的梯度只会越来越小,甚至消失,在优化空间中相当于一部分参数进行更新,而另外一部分参数几乎不变,那么问题的较优解也就很难收敛到。而LSTM通过推导会发现,梯度是以一种累加的方式进行反向传递的,从而一定程度上客服了累乘导致的梯度弥散的问题。下面要推导的LSTM 是不加peelhole的结构。要推导LSTM首先要了解LSTM的几个门结构:输入门、输出门以及遗忘门。从传统的神经网络的角度来看的话,三种门分别对应了三个并行的神经网络层,如下图所示(其中蓝色和黄色表示神经元层结构,有输入、输出和相应的激励函数;红色表示向量对应项相乘;淡绿色表示向量加)。
三种门的计算方式如下:
n e t f t f t n e t i t i t n e t o t o t = x t w f x + h t − 1 w f h + f b i a s = g ( n e t f t ) = x t w i x + h t − 1 w i h + i b i a s = g ( n e t i t ) = x t w f o + h t − 1 w f o + o b i a s = g ( n e t o t )
另外,LSTM中也引入一个Cell State的中间状态,该状态有选择的保存了过去的历史信息,不会像RNN一样存在越久远的记忆越模糊的问题。该Cell State的更新方式主要是依赖于过去时刻的Cell State(c t − 1 )以及当前时刻产生的新的信息(c ̃ t ),当前产生的新的信息的计算方式如下:
n e t c t c ̃ t = x t w c x + h t − 1 w c h + c b i a s = h ( n e t c t )
遗忘门用来控制过去时刻的Cell State有多少需要被保留,输入门用来控制增加多少新的信息到Cell State中,公式表述如下:
c t = f t ⋅ c t − 1 + i t ⋅ c ̃ t
而h t 的更新则利用了c t ,具体如下:
h t = h ( c t ) ⋅ o t
输出层的计算方式和一般的RNN无异,令输出层、输入门、输出门、遗忘门、新cell state的误差项分别为δ t y , δ t i , δ t o ,δ t f ,δ t c , 则有:
δ t y δ t i δ t o δ t f δ t c = ∂ E ∂ n e t y t = ∂ E ∂ n e t i t = ∂ E ∂ n e t o t = ∂ E ∂ n e t f t = ∂ E ∂ n e t c t
另外我们也需要定义两个新的中间变量ϵ t c 以及ϵ t h ,其值为:
ϵ t h ϵ t c = ∂ E ∂ h t = ∂ E ∂ n e t y t ⋅ ∂ n e t y t ∂ h t + ∂ E ∂ n e t i t + 1 ∂ n e t i t + 1 ∂ h t + ∂ E ∂ n e t f t + 1 ∂ n e t f t + 1 ∂ h t + ∂ E ∂ n e t o t + 1 ∂ n e t o t + 1 ∂ h t + ∂ E ∂ n e t c t + 1 ∂ n e t c t + 1 ∂ h t = δ t y ⋅ ( w y ) T + δ t + 1 i ⋅ ( w i h ) T + δ t + 1 f ⋅ ( w f h ) T + δ t + 1 o ⋅ ( w o h ) T + δ t + 1 c ⋅ ( w c h ) T = ∂ E ∂ c t = ∂ E ∂ h t ⋅ ∂ h t ∂ c t + ∂ E ∂ c t + 1 ⋅ ∂ c t + 1 ∂ c t = ϵ t h ⋅ o t ⋅ h ′ ( c t ) + ϵ t + 1 c ⋅ f t + 1
so, 另外一波公式来袭~~
δ t y δ t i δ t o δ t f δ t c = ∂ E ∂ n e t y t = ∂ E ∂ y t ⋅ ∂ y t ∂ n e t y t = ( y t − d t ) ⋅ g ′ ( n e t y t ) = ∂ E ∂ n e t i t = ∂ E ∂ i t ⋅ ∂ i t ∂ n e t i t = ∂ E ∂ c t ⋅ ∂ c t ∂ i t ⋅ g ′ ( n e t i t ) = ϵ t c ⋅ c ̃ t ⋅ g ′ ( n e t i t ) = ∂ E ∂ n e t o t = ∂ E ∂ o t ⋅ ∂ o t ∂ n e t o t = ∂ E ∂ h t ⋅ ∂ h t ∂ o t ⋅ g ′ ( n e t o t ) = ϵ t h ⋅ h ( c t ) ⋅ g ′ ( n e t o t ) = ∂ E ∂ n e t f t = ∂ E ∂ f t ⋅ ∂ f t ∂ n e t f t = ∂ E ∂ c t ⋅ ∂ c t ∂ f t ⋅ g ′ ( n e t f t ) = ϵ t c ⋅ c t − 1 ⋅ g ′ ( n e t f t ) = ∂ E ∂ n e t c t = ∂ E ∂ c ̃ t ⋅ ∂ c ̃ t ∂ n e t c t = ∂ E ∂ c t ⋅ ∂ c t ∂ c ̃ t ⋅ g ′ ( n e t c t ) = ϵ t c ⋅ i t ⋅ g ′ ( n e t c t )
okay,现在对权值进行求导,首先看w i x ,根据链式求导法则有:
∂ E ∂ w i x j , k ∂ E ∂ w i x = ∂ E ∂ ( n e t i t ) k ⋅ ∂ ( n e t i t ) k ∂ w i x j , k = ( δ t i ) k ⋅ x t j = ( x t ) T δ t i
剩下的参数不难推知:
∂ E ∂ w i x ∂ E ∂ w i h ∂ E ∂ w o x ∂ E ∂ w o h ∂ E ∂ w f x ∂ E ∂ w f h ∂ E ∂ w c x ∂ E ∂ w c h ∂ E ∂ w y ∂ E ∂ o b i a s ∂ E ∂ i b i a s ∂ E ∂ f b i a s ∂ E ∂ c b i a s ∂ E ∂ y b i a s = ( x t ) T δ t i = ( h t − 1 ) T δ t i = ( x t ) T δ t o = ( h t − 1 ) T δ t o = ( x t ) T δ t f = ( h t − 1 ) T δ t f = ( x t ) T δ t c = ( h t − 1 ) T δ t c = ( h t ) T δ t y = δ t o = δ t i = δ t f = δ t c = δ t y
总算推完了,代码实现就很简单了~详见 https://github.com/kymo/SUDL/blob/master/rnn/lstm.cpp 对于LSTM,GRU的推导也类似,了解清楚误差传递的来源和去向,就很容易得到对当前参数的推导链式规则了.
参考资料
http://www.cnblogs.com/YiXiaoZhou/p/6058890.html http://www.cs.toronto.edu/~graves/preprint.pdf