Gated RNN

RNN 的问题

因为 BPTT 会发生梯度消失和梯度爆炸的问题,所以不擅长学习时序数据的长期依赖关系。

梯度消失和梯度爆炸

RNN 层通过向过去传递“有意义的梯度”,能够学习时间方向上的依赖关系。此时梯度(理论上)包含了那些应该学到的有意义的信息,通过将这些信息向过去传递,RNN 层学习长期的依赖关系。但是,如果这个梯度在中途变弱,则权重参数将不会被更新。

RNN 层在时间方向上的梯度传播

RNN 层在时间方向上的梯度传播

在反向传播中,梯度主要经过 tanh 以及矩阵乘法两个函数。梯度消失和梯度爆炸正是由于这两个函数的特点导致的。

梯度爆炸的对策

解决梯度爆炸一般采用梯度裁剪(gradients clipping)的办法。

\[ \text{if} \ ||\hat{\textbf{g}}|| \ge \text{threshold:} \\ \ \ \ \ \hat{\textbf{g}} = \frac{\text{threshold}}{||\hat{\textbf{g}}||} \hat{\textbf{g}} \]

假设可以将神经网络用到的所有参数的梯度整合成一个,并用符号\(\hat{\textbf{g}}\)表示。如果梯度的 L2 范数\(||\hat{\textbf{g}}||\)大于或者等于阈值,就按上面的方式修正梯度。

def clip_grads(grads, max_norm):
total_norm = 0
for grad in grads:
total_norm += np.sum(grad ** 2)
total_norm = np.sqrt(total_norm)

rate = max_norm / (total_norm + 1e-6)
if rate < 1:
for grad in grads:
grad *= rate

梯度消失和 LSTM

下面是 LSTM 与 RNN 的接口比较图。

LSTM 增加了路径 \(c\)。这个 \(c\) 称为记忆单元,它只在 LSTM 内部发挥作用,并不向外层输出。

LSTM 的结构

输出门

LSTM 有记忆单元 \(c_t\)。这个 \(c_t\) 存储了时刻 \(t\) 时的 LSTM 的记忆。然后,基于这个充满必要信息的记忆,向外部的层(和下一时刻的 LSTM)输出隐藏状态 \(h_t\)

如右图所示,当前的记忆单元 \(c_t\) 是基于三个输入 \(c_{t-1}, h_{t-1}, x_t\) 经过某种计算计算出来的。这里的重点是 \(h_t = \tanh(c_t)\)

下面考虑对 \(\tanh(c_t)\) 施加门。这作用在于针对 \(\tanh(c_t)\) 的各个元素,调整它们作为下一时刻的隐藏状态的重要程度。由于这个门管理下一个隐藏状态 \(h_t\) 的输出,所以称为输出门(output gate)

输出门的开合程度(流出比例)根据输入 \(x_t\) 和上一个状态 \(h_{t-1}\) 求出。sigmoid 函数用 \(\sigma\) 表示。下面是输出门的输出计算方式。

\[ o = \sigma(x_t W_x^{(o)} + h_{t-1} W_h^{(o)} + b^{(o)}) \]

最后,得到输出 \(h_t = o \odot \tanh(c_t)\)。这里的乘积 \(\odot\) 是对应元素的乘积。

遗忘门

现在,我们在 \(c_{t-1}\) 上添加一个忘记不必要记忆的门,称为遗忘门(forget gate)

遗忘门的计算方式与输出门的计算方式类似,最终也是算出一个权重,来决定 \(c_{t-1}\) 的重要程度。

\[ f = \sigma(x_t W_x^{(f)} + h_{t-1} W_h^{(f)} + b^{(f)}) \]

最后,得到输出 \(c_t = f \odot c_{t-1}\)

新的记忆单元

遗忘门从上一时刻的记忆单元删除了应该忘记的东西,下面我们往这个记忆单元增加一些应当记住的新信息。

如上图所示,基于 \(\tanh\) 节点计算出的结果被加到上一时刻的记忆单元 \(c_{t-1}\) 上,这样一来,新的信息就被加到记忆单元中。这个节点的作用不是门,所以不用 sigmoid 函数。

\[ g = \tanh(x_t W_x^{(g)} + h_{t-1} W_h^{(g)} + b^{(g)}) \]

通过将这个 \(g\) 加到 \(c_{t-1}\) 上,从而形成新的记忆。

输入门

输入门用于控制新增信息的权重,是用来控制 \(g\) 的。

由于这是一个控制门,所以使用 \(\sigma\) 函数。

\[ i = \sigma(x_t W_x^{(i)} + h_{t-1} W_h^{(i)} + b^{(i)}) \]

由此,最终得到的 \(c_t,h_t\) 可以表示为

\[ \begin{align*} c_t &= f \odot c_{t-1} + i \odot g \\ h_t &= o \odot \tanh(c_t) \end{align*} \]

LSTM 的梯度流动

为什么 LSTM 不会引起梯度消失呢?可以通过观察记忆单元 \(c\) 来看出。

记忆单元的反向传播仅仅经过加法和乘法,加法会将梯度保持原状返回,而乘法节点并不是矩阵乘积,不会因为执行次数的叠加而产生梯度消失和梯度爆炸。

LSTM 的实现

下面列出 LSTM 中涉及到的所有计算。

\[ \begin{align} f &= \sigma(x_t W_x^{(f)} + h_{t-1} W_h^{(f)} + b^{(f)}) \\ g &= \tanh(x_t W_x^{(g)} + h_{t-1} W_h^{(g)} + b^{(g)}) \\ i &= \sigma(x_t W_x^{(i)} + h_{t-1} W_h^{(i)} + b^{(i)}) \\ o &= \sigma(x_t W_x^{(o)} + h_{t-1} W_h^{(o)} + b^{(o)}) \\ c_t &= f \odot c_{t-1} + i \odot g \\ h_t &= o \odot \tanh(c_t) \end{align} \]

其中的式(1)~(4)可以合并为一个式子。

整合之后的计算图如下所示。

class LSTM:
def __init__(self, Wx, Wh, b):
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