《动手学深度学习》
Table Of Contents
《动手学深度学习》
Table Of Contents

6.8. 长短期记忆(LSTM)

本节将介绍另一种常用的门控循环神经网络:长短期记忆(long short-term memory,LSTM)[1]。它比门控循环单元的结构稍微复杂一点。

6.8.1. 长短期记忆

LSTM 中引入了3个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate),以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。

6.8.1.1. 输入门、遗忘门和输出门

与门控循环单元中的重置门和更新门一样,如图6.7所示,长短期记忆的门的输入均为当前时间步输入\(\boldsymbol{X}_t\)与上一时间步隐藏状态\(\boldsymbol{H}_{t-1}\),输出由激活函数为sigmoid函数的全连接层计算得到。如此一来,这3个门元素的值域均为\([0,1]\)

长短期记忆中输入门、遗忘门和输出门的计算

图 6.7 长短期记忆中输入门、遗忘门和输出门的计算

具体来说,假设隐藏单元个数为\(h\),给定时间步\(t\)的小批量输入\(\boldsymbol{X}_t \in \mathbb{R}^{n \times d}\)(样本数为\(n\),输入个数为\(d\))和上一时间步隐藏状态\(\boldsymbol{H}_{t-1} \in \mathbb{R}^{n \times h}\)。 时间步\(t\)的输入门\(\boldsymbol{I}_t \in \mathbb{R}^{n \times h}\)、遗忘门\(\boldsymbol{F}_t \in \mathbb{R}^{n \times h}\)和输出门\(\boldsymbol{O}_t \in \mathbb{R}^{n \times h}\)分别计算如下:

\[\begin{split}\begin{aligned} \boldsymbol{I}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xi} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hi} + \boldsymbol{b}_i),\\ \boldsymbol{F}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xf} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hf} + \boldsymbol{b}_f),\\ \boldsymbol{O}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xo} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{ho} + \boldsymbol{b}_o), \end{aligned}\end{split}\]

其中的\(\boldsymbol{W}_{xi}, \boldsymbol{W}_{xf}, \boldsymbol{W}_{xo} \in \mathbb{R}^{d \times h}\)\(\boldsymbol{W}_{hi}, \boldsymbol{W}_{hf}, \boldsymbol{W}_{ho} \in \mathbb{R}^{h \times h}\)是权重参数,\(\boldsymbol{b}_i, \boldsymbol{b}_f, \boldsymbol{b}_o \in \mathbb{R}^{1 \times h}\)是偏差参数。

6.8.1.2. 候选记忆细胞

接下来,长短期记忆需要计算候选记忆细胞\(\tilde{\boldsymbol{C}}_t\)。它的计算与上面介绍的3个门类似,但使用了值域在\([-1, 1]\)的tanh函数作为激活函数,如图6.8所示。

长短期记忆中候选记忆细胞的计算

图 6.8 长短期记忆中候选记忆细胞的计算

具体来说,时间步\(t\)的候选记忆细胞\(\tilde{\boldsymbol{C}}_t \in \mathbb{R}^{n \times h}\)的计算为

\[\tilde{\boldsymbol{C}}_t = \text{tanh}(\boldsymbol{X}_t \boldsymbol{W}_{xc} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hc} + \boldsymbol{b}_c),\]

其中\(\boldsymbol{W}_{xc} \in \mathbb{R}^{d \times h}\)\(\boldsymbol{W}_{hc} \in \mathbb{R}^{h \times h}\)是权重参数,\(\boldsymbol{b}_c \in \mathbb{R}^{1 \times h}\)是偏差参数。

6.8.1.3. 记忆细胞

我们可以通过元素值域在\([0, 1]\)的输入门、遗忘门和输出门来控制隐藏状态中信息的流动,这一般也是通过使用按元素乘法(符号为\(\odot\))来实现的。当前时间步记忆细胞\(\boldsymbol{C}_t \in \mathbb{R}^{n \times h}\)的计算组合了上一时间步记忆细胞和当前时间步候选记忆细胞的信息,并通过遗忘门和输入门来控制信息的流动:

\[\boldsymbol{C}_t = \boldsymbol{F}_t \odot \boldsymbol{C}_{t-1} + \boldsymbol{I}_t \odot \tilde{\boldsymbol{C}}_t.\]

如图6.9所示,遗忘门控制上一时间步的记忆细胞\(\boldsymbol{C}_{t-1}\)中的信息是否传递到当前时间步,而输入门则控制当前时间步的输入\(\boldsymbol{X}_t\)通过候选记忆细胞\(\tilde{\boldsymbol{C}}_t\)如何流入当前时间步的记忆细胞。如果遗忘门一直近似1且输入门一直近似0,过去的记忆细胞将一直通过时间保存并传递至当前时间步。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

长短期记忆中记忆细胞的计算。这里的\ :math:`\cdot`\ 是按元素乘法

图 6.9 长短期记忆中记忆细胞的计算。这里的\(\cdot\)是按元素乘法

6.8.1.4. 隐藏状态

有了记忆细胞以后,接下来我们还可以通过输出门来控制从记忆细胞到隐藏状态\(\boldsymbol{H}_t \in \mathbb{R}^{n \times h}\)的信息的流动:

\[\boldsymbol{H}_t = \boldsymbol{O}_t \odot \text{tanh}(\boldsymbol{C}_t).\]

这里的tanh函数确保隐藏状态元素值在-1到1之间。需要注意的是,当输出门近似1时,记忆细胞信息将传递到隐藏状态供输出层使用;当输出门近似0时,记忆细胞信息只自己保留。图6.10展示了长短期记忆中隐藏状态的计算。

长短期记忆中隐藏状态的计算。这里的\ :math:`\cdot`\ 是按元素乘法

图 6.10 长短期记忆中隐藏状态的计算。这里的\(\cdot\)是按元素乘法

6.8.2. 读取数据集

下面我们开始实现并展示长短期记忆。和前几节中的实验一样,这里依然使用周杰伦歌词数据集来训练模型作词。

In [1]:
import d2lzh as d2l
from mxnet import nd
from mxnet.gluon import rnn

(corpus_indices, char_to_idx, idx_to_char,
 vocab_size) = d2l.load_data_jay_lyrics()

6.8.3. 从零开始实现

我们先介绍如何从零开始实现长短期记忆。

6.8.3.1. 初始化模型参数

下面的代码对模型参数进行初始化。超参数num_hiddens定义了隐藏单元的个数。

In [2]:
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
ctx = d2l.try_gpu()

def get_params():
    def _one(shape):
        return nd.random.normal(scale=0.01, shape=shape, ctx=ctx)

    def _three():
        return (_one((num_inputs, num_hiddens)),
                _one((num_hiddens, num_hiddens)),
                nd.zeros(num_hiddens, ctx=ctx))

    W_xi, W_hi, b_i = _three()  # 输入门参数
    W_xf, W_hf, b_f = _three()  # 遗忘门参数
    W_xo, W_ho, b_o = _three()  # 输出门参数
    W_xc, W_hc, b_c = _three()  # 候选记忆细胞参数
    # 输出层参数
    W_hq = _one((num_hiddens, num_outputs))
    b_q = nd.zeros(num_outputs, ctx=ctx)
    # 附上梯度
    params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
              b_c, W_hq, b_q]
    for param in params:
        param.attach_grad()
    return params

6.8.4. 定义模型

在初始化函数中,长短期记忆的隐藏状态需要返回额外的形状为(批量大小, 隐藏单元个数)的值为0的记忆细胞。

In [3]:
def init_lstm_state(batch_size, num_hiddens, ctx):
    return (nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx),
            nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx))

下面根据长短期记忆的计算表达式定义模型。需要注意的是,只有隐藏状态会传递到输出层,而记忆细胞不参与输出层的计算。

In [4]:
def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = nd.sigmoid(nd.dot(X, W_xi) + nd.dot(H, W_hi) + b_i)
        F = nd.sigmoid(nd.dot(X, W_xf) + nd.dot(H, W_hf) + b_f)
        O = nd.sigmoid(nd.dot(X, W_xo) + nd.dot(H, W_ho) + b_o)
        C_tilda = nd.tanh(nd.dot(X, W_xc) + nd.dot(H, W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * C.tanh()
        Y = nd.dot(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H, C)

6.8.4.1. 训练模型并创作歌词

同上一节一样,我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。

In [5]:
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']

我们每过40个迭代周期便根据当前训练的模型创作一段歌词。

In [6]:
d2l.train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens,
                          vocab_size, ctx, corpus_indices, idx_to_char,
                          char_to_idx, False, num_epochs, num_steps, lr,
                          clipping_theta, batch_size, pred_period, pred_len,
                          prefixes)
epoch 40, perplexity 210.564923, time 0.77 sec
 - 分开 我不不的我 我不你的我 我不不我 我不你我你你 我不不我你 我不你我你你 我不不我你 我不你我你你
 - 不分开 我不不我你你 我不不我你 我不你我你你 我不不我你 我不你我你你 我不不我你 我不你我你你 我不不
epoch 80, perplexity 63.395037, time 0.76 sec
 - 分开 我想你这你 我想要这你 我不要这样 我不要这样 我不要这样 我不要这样 我不要这样 我不要这样 我
 - 不分开 我想你这你 我想要这你 我不要这样 我不要这样 我不要这样 我不要这样 我不要这样 我不要这样 我
epoch 120, perplexity 14.876521, time 0.76 sec
 - 分开 我想你你 我有多定熬  后有你在我有多难熬恼恼  我有你烦 我有多烦恼  没有你在我有多难难恼
 - 不分开 你是我 你你我 说你  是是了对么 我想就这生活 你你你的爱快 我爱你 你爱我 我想开这样 我想就
epoch 160, perplexity 3.903005, time 0.76 sec
 - 分开 我已到你不堡 有着歌 一直走 白慢的钟 是上完 旧诉的风 印有就  装段完明的白老 从真里 一片村
 - 不分开 你已经 你开我 我想头这样牵着你开开开开 就托 我想是你的脑袋有问题 随便说说 其实我早已经猜透透

6.8.5. 简洁实现

在Gluon中我们可以直接调用rnn模块中的LSTM类。

In [7]:
lstm_layer = rnn.LSTM(num_hiddens)
model = d2l.RNNModel(lstm_layer, vocab_size)
d2l.train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes)
epoch 40, perplexity 227.315959, time 0.08 sec
 - 分开 我不不我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我
 - 不分开 我不不我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我不你我我 我
epoch 80, perplexity 66.630194, time 0.08 sec
 - 分开 我想你你的爱我 想想 你想我想你 我想 你想我的你 我想想你的你 我想想你的你 我想想你的你 我想
 - 不分开 我想你你想你我 想想 你想我想你 我想 我想你的你 我想想你的你 我想想你的你 我想想你的你 我想
epoch 120, perplexity 14.264307, time 0.08 sec
 - 分开 我想儿这里堡  说有你的你笑 我想想你 你不的外在我 想发看觉 我想你这生活 你知后觉 我该了这生
 - 不分开你 我想你你的微笑 我想想你 你我的话样着 我该好你 我不多这节奏 后知后觉 我该了这生活 后知后觉
epoch 160, perplexity 3.707788, time 0.08 sec
 - 分开 我想儿 你爱我 什么我么开 我说就很医着我说攻球 别爱我 你简我 别发打打我的欢 我真多的生活 你
 - 不分开 我已过这生活 不知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生活 不知不觉 你已经离开我

6.8.6. 小结

  • 长短期记忆的隐藏层输出包括隐藏状态和记忆细胞。只有隐藏状态会传递到输出层。
  • 长短期记忆的输入门、遗忘门和输出门可以控制信息的流动。
  • 长短期记忆可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

6.8.7. 练习

  • 调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
  • 在相同条件下,比较长短期记忆、门控循环单元和不带门控的循环神经网络的运行时间。
  • 既然候选记忆细胞已通过使用tanh函数确保值域在-1到1之间,为什么隐藏状态还需要再次使用tanh函数来确保输出值域在-1到1之间?

6.8.8. 扫码直达讨论区

image0

6.8.9. 参考文献

[1] Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780.