循环神经网络教程第3部分 BPTT

来源:互联网 发布:关键词外包优化 编辑:程序博客网 时间:2024/04/28 06:45

在本教程的前面部分,我们从头实现了RNN,但没有详细介绍如何通过BPTT算法计算梯度。在本部分中,我们将简要概述BPTT并解释它与传统反向传播的区别。然后我们将尝试理解消失梯度问题,这导致了LSTM和GRU的发展,这两个是目前应用于NLP(和其他领域)最流行的模型。消失梯度问题最初是由Sepp Hochreiter于1991年发现的,最近由于深度架构的应用的增加而受到关注。
要完全理解这一部分,我建议你先熟悉微分链式规则和基本反向传播原理。如果你不熟悉,你可以以增加难度的顺序在这里,这里和这里找到优秀的教程。

BPTT

让我们快速回顾一下RNN的基本方程。注意,现在o变成了y^,这只是为了参考的一些文献保持一致。

sty^t=tanh(Uxt+Wst1)=softmax(Vst)

我们也定义了损失(或错误)是交叉熵损失:

Et(yt,y^t)E(y,y^)=ytlogy^t=tEt(yt,y^t)=tytlogy^t

这里yt 是时刻t正确的单词(输出), y^t 是预测值。我们通常把整个序列(一个句子)当作一个训练样本,总的损失就是每个时刻(单词)的损失之和。

记住,我们的目标是计算参数UVW的误差梯度,然后使用随机梯度下降学习好的参数。就像把损失值加和一样,我们把一个训练样本每个时刻的梯度也加起来:EW=tEtW

为了计算这些梯度,我们使用微分链式的规则。这是从错误开始应用反向传播的反向传播算法。对于本文的其余部分,我们将使用E3作为例子。

E3V=E3y^3y^3V=E3y^3y^3m3m3V=y3y^3(y^3y^23)s3=(y^3y3y3)s3

在上式中,m3=Vs3, and 是 两个向量的外积。我想要说明的一点是E3V只取决于当前时间步长的值y^3y3s3。如果你有了这些,计算V的梯度就是简单矩阵乘法。

但是计算 E3W(and for U) 就是另外一回事了。我们在后向传播上那样,定义后向传播的δ向量(残差),针对第 l层的每一个节点 i,我们计算出其“残差”δ(l)i,该残差表明了该节点对最终输出值的残差产生了多少影响。RNN的每一层只有一个节点。以前三层为例:

s3=tanh(z3),z3=Ux3+Ws2s2=tanh(z2),z2=Ux2+Ws1s1=tanh(z1),z1=Ux1+Ws0s0=tanh(z0),z0=Ux0+Ws1

δ(3)3=E3z3=E3s3s3z3=E3s3f(z3)δ(3)2=E3z2=E3s3s3z3z3s2s2z2=δ(3)3Wf(z2)δ(3)1=E3z1=E3s3s3z3z3s2s2z2z2s1s1z1=δ(3)2Wf(z1)δ(3)0=E3z0=E3s3s3z3z3s2s2z2z2s1s1z1z1s0s0z0=δ(3)1Wf(z0)

E3W=δ(3)3s2+δ(3)2s1+δ(3)1s0+δ(3)0s1

用代码实现一个原生的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个单词或更多的句子非常困难了,因为你需要后向传播很多层。在实践中,许多人将反向传播截断到几个步骤。

梯度消失问题

让我们再仔细看一下我们上面计算的梯度:

E3W=δ(3)3s2+δ(3)2s1+δ(3)1s0+δ(3)0s1

我们可以把上面的梯度写成:

E3W=δ(3)3(s2+wf(z2)s1+wf(z2)s1+w2f(z2)f(z1)s0+w3f(z2)f(z1)f(z0)s1)

因为tanhsigmoid)激活函数将所有值映射到-1和1之间的范围内,导数的取值也限制在(0,1](在sigmoid的情况下为(0,1/4]):

可以看出 tanh and sigmoid 函数在两头趋于直线,导数趋于0。这种情况下对应的神经元是饱和的, 它们的梯度是0,并且驱使前面层的梯度也朝0发展。因此,有了矩阵中很小的值以及多个矩阵乘法(特别是t-k),梯度值以指数的速度收缩,最终在几个时刻之后完全消失。来自“远处”时刻的梯度贡献值变为零,这些时刻的状态对正在学习的内容没有贡献:最终学习不到远程依赖关系。梯度消失不仅仅发生在RNN的中。它们也发生在非常深的前馈神经网络中。这只是RNN通常非常深(就像我们的例句一样长),使得这个问题更常见。

很容易想象,根据我们的激活函数和网络参数,如果Jacobian矩阵的值很大,梯度将会爆炸而不是消失。这被称为梯度爆炸问题。梯度消失比梯度爆炸更受关注的原因有两方面的。首先,梯度爆炸是显而易见的,梯度将成为NaN(不是一个数字),你的程序会崩溃。其次,通过预定义的阈值(如这篇论文所讨论的那样)中截取梯度,对爆炸梯度来说是非常简单有效的解决方案。梯度消失有更多的问题,因为当它们发生时不是很明显,处理它们的方法也不是很容易想到。

幸运的是,有几种方法可以缓解梯度消失问题。 W矩阵的适当初始化可以减少梯度消失的影响。也可以通过正则化缓解。更好的解决方案是使用ReLU而不是tanhsigmoid激活函数。 ReLU导数是0或1的常数,因此它不太可能受到梯度消失。更常见的解决方案是使用LSTM或GRU。 LSTM是1997年首次提出的,并且是目前NLP中最广泛使用的模型。 GRU,2014年首次提出,是LSTM的简化版本。这两种RNN架构都明确地被设计用来处理梯度消失并可以有效地学习远程依赖。我们将在本教程的下一部分介绍它们。

0 0