循环神经网络教程第三部分-BPTT和梯度消失

作者:徐志强
链接:https://zhuanlan.zhihu.com/p/22338087
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本篇是 循环神经网络教程的第三部分。

前一篇教程中,我们从头开始实现了RNN,但并没有深入到BPTT如何计算梯度的细节中去。在本部分,我们将对BPTT做一个简短的介绍,并解释它和传统的反向传播有什么不同。然后,我们会试着去理解梯度消失问题,它导致了LSTM和GRU这两个目前在NLP中最流行、最强大的模型的发明。梯度消失问题最初由Sepp Hochreiter在1991年发现,在最近由于深度结构的使用又引起了人们的关注。

为了完全理解本部分的内容,我建议先熟悉一下偏微分和基本的反向传播是怎么工作的。如果你不熟悉的话,在这里这里这里可以找打很好的资料,它们的难度逐级递增。

随时间的反向传播(BPTT)

让我们先迅速回忆一下RNN的基本公式,注意到这里在符号上稍稍做了改变(o变成\hat{y} ),这只是为了和我参考的一些资料保持一致。
s_{t}=tanh(Ux_{t}+Ws_{t-1})
\hat{y}_{t}=softmax(Vs_{t})

同样把损失值定义为交叉熵损失,如下:
E_{t}(y_{t}, \hat{y}_{t})=-y_{t}log(\hat{y}_{t})
E(y, \hat{y})=\sum_{t}{E_{t}(y_{t}, \hat{y}_{t})}= -\sum_{t}{y_{t}log\hat{y}_{t}}
这里,y_{t}表示时刻t正确的词,\hat{y}_{t}是我们的预测。通常我们会把整个句子作为一个训练样本,所以总体错误是每一时刻的错误的加和。

我们的目标是计算错误值相对于参数U, V, W的梯度以及用随机梯度下降学习好的参数。就像我们要把所有错误相加一样,我们同样会把每一时刻针对每个训练样本的梯度值相加:\frac{\partial{E}}{\partial{W}}=\sum_{t}{\frac{\partial{E}_{t}}{\partial{W}}}
为了计算梯度,我们使用链式求导法则,主要是用反向传播算法往后传播错误。下文使用E_{3}作为例子,主要是为了描述方便。

\frac{\partial{E_{3}}}{\partial{V}} =\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}} \frac{\partial{\hat{y}_{3}}}{\partial{V}}=\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}} \frac{​{\partial{\hat{y}_{3}}}}{\partial{z_{3}}}\frac{\partial{z_{3}}}{\partial{V}}=(\hat{y}_{3}-y_{3})\otimes s_{3}
上面z_{3}=Vs_{3}\otimes 是向量的外积。如果你不理解上面的公式,不要担心,我在这里跳过了一些步骤,你可以自己尝试来计算这些梯度值。这里我想说明的一点是梯度值只依赖于当前时刻的结果\hat{y}_{3}, y_{3}, s_{3}。根据这些,计算V的梯度就只剩下简单的矩阵乘积了。

但是对于梯度\frac{\partial{E}_{3}}{\partial{W}} 情况就不同了,我们可以像上面一样写出链式法则。
\frac{\partial{E_{3}}}{\partial{W}}=\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{W}}

注意到这里的s_{3}=tanh(Ux_{t}+Ws_{2})依赖于s_{2}s_{2}依赖于Ws_{1},等等。所以为了得到W的梯度,我们不能将s_{2}看作常量。我们需要再次使用链式法则,得到的结果如下:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{s_{k}}}\frac{\partial{s_{k}}}{\partial{W}}}

我们把每一时刻得到的梯度值加和,换句话说,W在计算输出的每一步中都使用了。我们需要通过将t=3时刻的梯度反向传播至t=0时刻。

注意到这里和我们在深度前向神经网络中使用的标准反向传播算法是一致的,关键不同在于我们把每一时刻针对W的不同梯度做了加和。在传统神经网络中,不需要在层之间共享参数,就不需要做任何加和。在我看来,BPTT是应用于展开的RNN上的标准反向传播的另一个名字。就像反向传播一样,你也可以定义一个反向传递的delta向量,例如,\delta ^{(3)}_{2}=\frac{\partial{E}_{3}}{z_{2}}                          =\frac{\partial{E}_{3}}{\partial{s_{3}}} \frac{\partial{s}_{3}}{\partial{s_{2}}} \frac{\partial{s}_{2}}{\partial{z_{2}}} ,其中z_{2}=Ux_{2}+Ws_{1}

在代码中,BPTT的一个简易实现如下:

def bptt(self, x, y):
    T = len(y)
    # Perform forward propagation
    o, s = self.forward_propagation(x)
    # We accumulate the gradients in these variables
    dLdU = np.zeros(self.U.shape)
    dLdV = np.zeros(self.V.shape)
    dLdW = np.zeros(self.W.shape)
    delta_o = o
    delta_o[np.arange(len(y)), y] -= 1.
    # For each output backwards...
    for t in np.arange(T)[::-1]:
        dLdV += np.outer(delta_o[t], s[t].T)
        # Initial delta calculation: dL/dz
        delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))
        # Backpropagation through time (for at most self.bptt_truncate steps)
        for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
            # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)
            # Add to gradients at each previous step
            dLdW += np.outer(delta_t, s[bptt_step-1])              
            dLdU[:,x[bptt_step]] += delta_t
            # Update delta for next step dL/dz at t-1
            delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)
    return [dLdU, dLdV, dLdW]

这会让你明白为什么标准RNN很难训练:序列会变得很长,可能有20个词或更多,因而就需要反向传播很多层。实践中,很多人会把发现传播截断至几步。

梯度消失问题

在教程前一部分,我提到RNN很难学到长范围的依赖——相隔几步的词之间的交互。这是有问题的因为英语中句子的意思通常由相距不是很近的词来决定:“The man who wore a wig on his head went inside”。这个句子讲的是一个男人走了进去,而不是关于假发。但是普通的RNN不可能捕捉这样的信息。要理解为什么,让我们先仔细看一下上面计算的梯度:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{s_{k}}}\frac{\partial{s_{3}}}{\partial{W}}}
注意到\frac{\partial{s_{3}}}{\partial{s_{k}}}也需要使用链式法则,例如,\frac{\partial{s_{3}}}{\partial{s_{1}}}=\frac{\partial{s_{3}}}{\partial{s_{2}}}\frac{\partial{s_{2}}}{\partial{s_{1}}}。注意到因为我们是用向量函数对向量求导数,结果是一个矩阵(称为Jacobian Matrix),矩阵元素是每个点的导数。我们可以把上面的梯度重写成:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}(\prod_{j=k+1}^{3} \frac{\partial{s_{j}}}{\partial{s_{j-1}}})\frac{\partial{s_{k}}}{\partial{W}}}
可以证明上面的Jacobian矩阵的二范数(可以认为是一个绝对值)的上界是1。这很直观,因为激活函数tanh把所有制映射到-1和1之间,导数值得界限也是1:

你可以看到tanh和sigmoid函数在两端的梯度值都为0,接近于平行线。当这种情况出现时,我们就认为相应的神经元饱和了。它们的梯度为0使得前面层的梯度也为0。矩阵中存在比较小的值,多个矩阵相乘会使梯度值以指数级速度下降,最终在几步后完全消失。比较远的时刻的梯度值为0,这些时刻的状态对学习过程没有帮助,导致你无法学习到长距离依赖。消失梯度问题不仅出现在RNN中,同样也出现在深度前向神经网中。只是RNN通常比较深(例子中深度和句子长度一致),使得这个问题更加普遍。

很容易想到,依赖于我们的激活函数和网络参数,如果Jacobian矩阵中的值太大,会产生梯度爆炸而不是梯度消失问题。梯度消失比梯度爆炸受到了更多的关注有两方面的原因。其一,梯度爆炸容易发现,梯度值会变成NaN,导致程序崩溃。其二,用预定义的阈值裁剪梯度可以简单有效的解决梯度爆炸问题。梯度消失出现的时候不那么明显而且不好处理。

幸运的是,已经有一些方法解决了梯度消失问题。合适的初始化矩阵W可以减小梯度消失效应,正则化也能起作用。更好的方法是选择ReLU而不是sigmoid和tanh作为激活函数。ReLU的导数是常数值0或1,所以不可能会引起梯度消失。更通用的方案时采用长短项记忆(LSTM)或门限递归单元(GRU)结构。LSTM在1997年第一次提出,可能是目前在NLP上最普遍采用的模型。GRU,2014年第一次提出,是LSTM的简化版本。这两种RNN结构都是为了处理梯度消失问题而设计的,可以有效地学习到长距离依赖,我们会在教程的下一部分进行介绍。

PS:前两篇教程中的图都没有显示出来,目前不知道是咋回事,希望这一篇能显示出来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值