赞
踩
RNN 之所以不擅长学习时序数据的长期依赖关系,是因为 BPTT 会发生梯度消失和梯度爆炸的问题。
考虑长度为 T 的时序数据,关注从第 T 个正确解标签传递出的梯度如何变化。此时,关注时间方向上的梯度,可知反向传播的梯度流经 tanh、“+”和 MatMul(矩阵乘积)运算。“+”的反向传播将上游传来的梯度原样传给下游,因此梯度的值不变。那么,剩下的 tanh 和 MatMul 运算会怎样变化呢?
tanh:当 y = tanh ( x ) y = \tanh(x) y=tanh(x) 时,它的导数是 d y d x = 1 − y 2 \frac{dy}{dx} = 1 − y^2 dxdy=1−y2 。它的值小于 1.0,并且随着 x 远离 0,它的值在变小。这意味着,当反向传播的梯度经过 tanh 节点时,它的值会越来越小。因此,如果经过 tanh 函数 T 次,则梯度也会减小 T 次。(梯度消失)
MatMul:假定从上游传来梯度 d h d\mathbf{h} dh,此时 MatMul 节点的反向传播通过矩阵乘积 d h W h T d\mathbf{h}\mathbf{W}_h^T dhWhT 计算梯度。之后,根据时序数据的时间步长,将这个计算重复相应次数。梯度的大小随时间步长呈指数级增加,这就是梯度爆炸(exploding gradients)。也可能梯度呈指数级减小,这就是梯度消失(vanishing gradients)。
解决梯度爆炸有既定的方法,称为梯度裁剪(gradients clipping)。这是一个非常简单的方法,它的伪代码如下所示:
i
f
∥
g
^
∥
≥
t
h
r
e
s
h
o
l
d
:
g
^
=
t
h
r
e
s
h
o
l
d
∥
g
^
∥
g
^
这里假设可以将神经网络用到的所有参数的梯度整合成一个,并用符号
g
^
\hat{g}
g^ 表示。另外,将阈值设置为 threshold。此时,如果梯度的 L2 范数大于或等于阈值,就按上述方法修正梯度,这就是梯度裁剪。
LSTM 与 RNN 的接口的不同之处在于,LSTM 还有路径 c。这个 c 称为记忆单元(或者简称为“单元”),相当于 LSTM 专用的记忆部门。
记忆单元的特点是,仅在 LSTM 层内部接收和传递数据。也就是说,记忆单元在 LSTM 层内部结束工作,不向其他层输出。而 LSTM 的隐藏状态 h 和 RNN 层相同,会被(向上)输出到其他层。
LSTM 有记忆单元 c t c_t ct。这个 c t c_t ct 存储了时刻 t 时 LSTM 的记忆,可以认为其中保存了从过去到时刻 t 的所有必要信息(或者以此为目的进行了学习)。然后,基于这个充满必要信息的记忆,向外部的层(和下一时刻的 LSTM)输出隐藏状态 h t h_t ht。上图为 LSTM 输出经 tanh 函数变换后的记忆单元。
隐藏状态
h
t
h_t
ht 对记忆单元
c
t
c_t
ct 仅仅应用了 tanh 函数。这里考虑对
t
a
n
h
(
c
t
)
tanh(c_t)
tanh(ct) 施加门。换句话说,针对
t
a
n
h
(
c
t
)
tanh(c_t)
tanh(ct) 的各个元素,调整它们作为下一时刻的隐藏状态的重要程度。由于这个门管理下一个隐藏状态
h
t
h_t
ht 的输出,所以称为 输出门(output gate)。由下式计算:
o
=
σ
(
x
t
W
x
(
o
)
+
h
t
−
1
W
h
(
o
)
+
b
(
o
)
)
\mathbf{o}=\sigma(\mathbf{x}_t\mathbf{W}_x^{(o)}+\mathbf{h}_{t-1}\mathbf{W}_h^{(o)}+\mathbf{b}^{(o)})
o=σ(xtWx(o)+ht−1Wh(o)+b(o))
则
h
t
h_t
ht 可由
o
\mathbf{o}
o 和
t
a
n
h
(
c
t
)
tanh(c_t)
tanh(ct) 的乘积计算出来。这里说的“乘积”是对应元素的乘积,也称为阿达玛乘积。计算如下所示:
h
t
=
o
⊙
tanh
(
c
t
)
\mathbf{h}_t=\mathbf{o}\odot\tanh(\mathbf{c}_t)
ht=o⊙tanh(ct)
在记忆单元 c t − 1 c_{t−1} ct−1 上添加一个忘记不必要记忆的门,这里称为遗忘门(forget gate)。
将遗忘门进行的一系列计算表示为
σ
\sigma
σ,其中有遗忘门专用的权重参数,此时的计算如下:
f
=
σ
(
x
t
W
x
(
f
)
+
h
t
−
1
W
h
(
f
)
+
b
(
f
)
)
\mathbf{f}=\sigma(\mathbf{x}_t\mathbf{W}_x^{(f)}+\mathbf{h}_{t-1}\mathbf{W}_h^{(f)}+\mathbf{b}^{(f)})
f=σ(xtWx(f)+ht−1Wh(f)+b(f))
然后,
c
t
c_t
ct 由这个
f
\mathbf{f}
f 和上一个记忆单元
c
t
−
1
c_{t−1}
ct−1 的对应元素的乘积求得:
c
t
=
f
⊙
c
t
−
1
\mathbf{c}_t=\mathbf{f}\odot\mathbf{c}_{t-1}
ct=f⊙ct−1
现在我们还想向这个记忆单元添加一些应当记住的新信息,为此我们添加新的 tanh 节点。
基于 tanh 节点计算出的结果被加到上一时刻的记忆单元
c
t
−
1
c_{t−1}
ct−1 上。这样一来,新的信息就被添加到了记忆单元中。这个 tanh 节点的作用不是门,而是将新的信息添加到记忆单元中。因此,它不用 sigmoid 函数作为激活函数,而是使用 tanh 函数。tanh 节点进行的计算如下所示:
g
=
tanh
(
x
t
W
x
(
g
)
+
h
t
−
1
W
h
(
g
)
+
b
(
g
)
)
\mathbf{g}=\tanh(\mathbf{x}_t\mathbf{W}_x^{(g)}+\mathbf{h}_{t-1}\mathbf{W}_h^{(g)}+\mathbf{b}^{(g)})
g=tanh(xtWx(g)+ht−1Wh(g)+b(g))
输入门判断新增信息 g 的各个元素的价值有多大。输入门不会不经考虑就添加新信息,而是会对要添加的信息进行取舍。此时进行的计算如下
所示:
i
=
σ
(
x
t
W
x
(
i
)
+
h
t
−
1
W
h
(
i
)
+
b
(
i
)
)
\mathbf{i}=\sigma(\mathbf{x}_t\mathbf{W}_x^{(i)}+\mathbf{h}_{t-1}\mathbf{W}_h^{(i)}+\mathbf{b}^{(i)})
i=σ(xtWx(i)+ht−1Wh(i)+b(i))
为什么LSTM不会引起梯度消失呢?
我们仅关注记忆单元此时,记忆单元的反向传播仅流过“+”和“×”节点。“+”节点将上游传来的梯度原样流出,所以梯度没有变化(退化)。
而“×”节点的计算并不是矩阵乘积,而是对应元素的乘积(阿达玛积)。这里的 LSTM 的反向传播进行的不是矩阵乘积计算,而是对应元素的乘积计算,而且每次都会基于不同的门值进行对应元素的乘积计算。这就是它不会发生梯度消失(或梯度爆炸)的原因。
“×”节点的计算由遗忘门控制(每次输出不同的门值)。遗忘门认为“应该忘记”的记忆单元的元素,其梯度会变小;而遗忘门认为“不能忘记”的元素,其梯度在向过去的方向流动时不会退化。因此,LSTM的记忆单元不会(难以)发生梯度消失,可以期待记忆单元能够保存(学习)长期的依赖关系。
class LSTM: def __init__(self, Wx, Wh, b): """ Wx: 输入`x`用的权重参数(整合了4个权重) Wh: 隐藏状态`h`用的权重参数(整合了4个权重) b: 偏置(整合了4个偏置) """ self.params = [Wx, Wh, b] self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] self.cache = None def forward(self, x, h_prev, c_prev): Wx, Wh, b = self.params N, H = h_prev.shape A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b f = A[:, :H] g = A[:, H:2*H] i = A[:, 2*H:3*H] o = A[:, 3*H:] f = sigmoid(f) g = np.tanh(g) i = sigmoid(i) o = sigmoid(o) c_next = f * c_prev + g * i h_next = o * np.tanh(c_next) self.cache = (x, h_prev, c_prev, i, f, g, o, c_next) return h_next, c_next def backward(self, dh_next, dc_next): Wx, Wh, b = self.params x, h_prev, c_prev, i, f, g, o, c_next = self.cache tanh_c_next = np.tanh(c_next) ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2) dc_prev = ds * f di = ds * g df = ds * c_prev do = dh_next * tanh_c_next dg = ds * i di *= i * (1 - i) df *= f * (1 - f) do *= o * (1 - o) dg *= (1 - g ** 2) dA = np.hstack((df, dg, di, do)) dWh = np.dot(h_prev.T, dA) dWx = np.dot(x.T, dA) db = dA.sum(axis=0) self.grads[0][...] = dWx self.grads[1][...] = dWh self.grads[2][...] = db dx = np.dot(dA, Wx.T) dh_prev = np.dot(dA, Wh.T) return dx, dh_prev, dc_prev
class TimeLSTM: def __init__(self, Wx, Wh, b, stateful=False): self.params = [Wx, Wh, b] self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)] self.layers = None self.h, self.c = None, None self.dh = None self.stateful = stateful def forward(self, xs): Wx, Wh, b = self.params N, T, D = xs.shape H = Wh.shape[0] self.layers = [] hs = np.empty((N, T, H), dtype='f') if not self.stateful or self.h is None: self.h = np.zeros((N, H), dtype='f') if not self.stateful or self.c is None: self.c = np.zeros((N, H), dtype='f') for t in range(T): layer = LSTM(*self.params) self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c) hs[:, t, :] = self.h self.layers.append(layer) return hs def backward(self, dhs): Wx, Wh, b = self.params N, T, H = dhs.shape D = Wx.shape[0] dxs = np.empty((N, T, D), dtype='f') dh, dc = 0, 0 grads = [0, 0, 0] for t in reversed(range(T)): layer = self.layers[t] dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc) dxs[:, t, :] = dx for i, grad in enumerate(layer.grads): grads[i] += grad for i, grad in enumerate(grads): self.grads[i][...] = grad self.dh = dh return dxs def set_state(self, h, c=None): self.h, self.c = h, c def reset_state(self): self.h, self.c = None, None
继续阅读:
《深度学习进阶:自然语言处理(第1章)》-读书笔记
《深度学习进阶:自然语言处理(第2章)》-读书笔记
《深度学习进阶:自然语言处理(第3章)》-读书笔记
《深度学习进阶:自然语言处理(第4章)》-读书笔记
《深度学习进阶:自然语言处理(第5章)》-读书笔记
《深度学习进阶:自然语言处理(第6章)》-读书笔记
《深度学习进阶:自然语言处理(第7章)》-读书笔记
《深度学习进阶:自然语言处理(第8章)》-读书笔记
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。