长短期记忆(LSTM)¶
本节将介绍另一种常用的门控循环神经网络:长短期记忆(long short-term memory,简称 LSTM)[1]。它比门控循环单元的结构稍微复杂一点。
长短期记忆¶
LSTM 中引入了三个门:输入门(input gate)、遗忘门(forget gate)和输出门(output gate),以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。
输入门、遗忘门和输出门¶
同门控循环单元中的重置门和更新门一样,如图 6.7 所示,长短期记忆的门的输入均为当前时间步输入 \(\boldsymbol{X}_t\) 与上一时间步隐藏状态 \(\boldsymbol{H}_{t-1}\),输出由激活函数为 sigmoid 函数的全连接层计算得到。如此一来,这三个门元素的值域均为 \([0,1]\)。
长短期记忆中输入门、遗忘门和输出门的计算。
具体来说,假设隐藏单元个数为 \(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}\) 分别计算如下:
其中的 \(\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}\) 是偏差参数。
候选记忆细胞¶
接下来,长短期记忆需要计算候选记忆细胞 \(\tilde{\boldsymbol{C}}_t\)。它的计算同上面介绍的三个门类似,但使用了值域在 \([-1, 1]\) 的 tanh 函数做激活函数,如图 6.8 所示。
长短期记忆中候选记忆细胞的计算。
具体来说,时间步 \(t\) 的候选记忆细胞 \(\tilde{\boldsymbol{C}}_t \in \mathbb{R}^{n \times h}\) 的计算为
其中的 \(\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}\) 是偏差参数。
记忆细胞¶
我们可以通过元素值域在 \([0, 1]\) 的输入门、遗忘门和输出门来控制隐藏状态中信息的流动:这一般也是通过使用按元素乘法(符号为 \(\odot\))来实现。当前时间步记忆细胞 \(\boldsymbol{C}_t \in \mathbb{R}^{n \times h}\) 的计算组合了上一时间步记忆细胞和当前时间步候选记忆细胞的信息,并通过遗忘门和输入门来控制信息的流动:
如图 6.9 所示,遗忘门控制上一时间步的记忆细胞 \(\boldsymbol{C}_{t-1}\) 中的信息是否传递到当前时间步,而输入门则可以控制当前时间步的输入 \(\boldsymbol{X}_t\) 通过候选记忆细胞 \(\tilde{\boldsymbol{C}}_t\) 如何流入当前时间步的记忆细胞。如果遗忘门一直近似 1 且输入门一直近似 0,过去的记忆细胞将一直通过时间保存并传递至当前时间步。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。
长短期记忆中记忆细胞的计算。这里的乘号是按元素乘法。
隐藏状态¶
有了记忆细胞以后,接下来我们还可以通过输出门来控制从记忆细胞到隐藏状态 \(\boldsymbol{H}_t \in \mathbb{R}^{n \times h}\) 的信息的流动:
这里的 tanh 函数确保隐藏状态元素值在 -1 到 1 之间。需要注意的是,当输出门近似 1 时,记忆细胞信息将传递到隐藏状态供输出层使用;当输出门近似 0 时,记忆细胞信息只自己保留。图 6.10 展示了长短期记忆中隐藏状态的计算。
长短期记忆中隐藏状态的计算。这里的乘号是按元素乘法。
读取数据集¶
下面我们开始实现并展示长短期记忆。和前几节中的实验一样,我们依然使用周杰伦歌词数据集来训练模型作词。
In [1]:
import gluonbook as gb
from mxnet import nd
from mxnet.gluon import rnn
(corpus_indices, char_to_idx, idx_to_char,
vocab_size) = gb.load_data_jay_lyrics()
从零开始实现¶
我们先介绍如何从零开始实现长短期记忆。
初始化模型参数¶
以下部分对模型参数进行初始化。超参数num_hiddens
定义了隐藏单元的个数。
In [2]:
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
ctx = gb.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
定义模型¶
在初始化函数中,长短期记忆的隐藏状态需要返回额外的形状为(批量大小,隐藏单元个数)的值为 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)
训练模型并创作歌词¶
同上一节一样,我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为 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]:
gb.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 214.220953, time 0.79 sec
- 分开 我不的我 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你
- 不分开 我不的我 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你 我不你
epoch 80, perplexity 67.741340, time 0.79 sec
- 分开 我想想这你 我不要这不 我不要这 我不要这我 我不要这不 我不要这不 我不要这不 我不要这不 我不
- 不分开 我想要你想你的你 我想想你想你 我不要这 我不要这我 我不要这不 我不要这不 我不要这不 我不要这
epoch 120, perplexity 15.496748, time 0.79 sec
- 分开 我想带你 你我我这难着 我有你没生我 爱知你觉 我该了这节奏 后知后觉 我该了好生活 我知好好 我
- 不分开 我想要你 我不要这样我 你知不觉 我该了这节奏 后知后觉 我该了好生活 我知好好 我该了好节奏 后
epoch 160, perplexity 4.129405, time 0.79 sec
- 分开 我想带你 我对我这想要你 我知起没了你 快你悄我满都猜到 我想就这样牵着着的手不放开 爱不不暴够圈
- 不分开 我要你这样活我妈妈 我的你手不会痛吗 不要再这样打我妈妈 难道你手不会痛吗 不要我这样打我想妈 我
简洁实现¶
在 Gluon 中我们可以直接调用rnn
模块中的LSTM
类。
In [7]:
lstm_layer = rnn.LSTM(num_hiddens)
model = gb.RNNModel(lstm_layer, vocab_size)
gb.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 222.001477, time 0.08 sec
- 分开 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我
- 不分开 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我
epoch 80, perplexity 66.473953, time 0.08 sec
- 分开 我想你的爱爱我 爱爱你 我想要我 你不我 我不要 我不要 我不了 我不了 我不了 我不了 我不了
- 不分开 你想我这想 我不要这不 我不要这 我不要这 我不不了 你不了这 你不了 我不了 你不了 我不了 我
epoch 120, perplexity 14.742035, time 0.08 sec
- 分开 我想你 你爱我 说不 是不了 我想就这样着我 爱知球我 我已要这节我 不知不觉 我该了这节我 不知
- 不分开 我已要这样 我不要这样 我不要觉 我不了这生活 我知后觉 我该了这节活 后知后觉 我该了好生活 我
epoch 160, perplexity 4.068531, time 0.08 sec
- 分开 我想带这生活 不天歌 一直我 想想就这样牵着你的手不放开 爱可不可以简简单单没有伤害 你 靠着我的
- 不分开 让已经够不起 后知后觉 我该了这生活 我该好好生活 静静悄悄默默离开 陷入了危险边缘Baby 我
小结¶
- 长短期记忆的隐藏层输出包括隐藏状态和记忆细胞。只有隐藏状态会传递进输出层。
- 长短期记忆的输入门、遗忘门和输出门可以控制信息的流动。
- 长短期记忆可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。
练习¶
- 调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
- 在相同条件下,比较长短期记忆、门控循环单元和不带门控的循环神经网络的运行时间。
- 既然候选记忆细胞已通过使用 tanh 函数确保值域在 -1 到 1 之间,为什么隐藏状态还需再次使用 tanh 函数来确保输出值域在 -1 到 1 之间?
参考文献¶
[1] Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780.