本节将介绍另一种常用的门控循环神经网络:长短期记忆(long short-term memory,LSTM)[1]。它比门控循环单元的结构稍微复杂一点。
LSTM 中引入了3个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate),以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。
与门控循环单元中的重置门和更新门一样,如图6.7所示,长短期记忆的门的输入均为当前时间步输入$\boldsymbol{X}_t$与上一时间步隐藏状态$\boldsymbol{H}_{t-1}$,输出由激活函数为sigmoid函数的全连接层计算得到。如此一来,这3个门元素的值域均为$[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}$分别计算如下:
$$ \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} $$其中的$\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$。它的计算与上面介绍的3个门类似,但使用了值域在$[-1, 1]$的tanh函数作为激活函数,如图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}$是偏差参数。
我们可以通过元素值域在$[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,过去的记忆细胞将一直通过时间保存并传递至当前时间步。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。
有了记忆细胞以后,接下来我们还可以通过输出门来控制从记忆细胞到隐藏状态$\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展示了长短期记忆中隐藏状态的计算。
下面我们开始实现并展示长短期记忆。和前几节中的实验一样,这里依然使用周杰伦歌词数据集来训练模型作词。
import d2ltorch as d2lt
import torch
from torch import nn
(corpus_indices, char_to_idx, idx_to_char,
vocab_size) = d2lt.load_data_jay_lyrics()
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
device = d2lt.try_gpu()
def get_params():
def _one(shape):
return torch.normal(mean=torch.zeros(shape), std=0.01).to(device)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens).to(device))
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 = torch.zeros(num_outputs).to(device)
# 附上梯度
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.requires_grad_()
return params
在初始化函数中,长短期记忆的隐藏状态需要返回额外的形状为(批量大小, 隐藏单元个数)的值为0的记忆细胞。
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros(batch_size, num_hiddens).to(device),
torch.zeros(batch_size, num_hiddens).to(device))
下面根据长短期记忆的计算表达式定义模型。需要注意的是,只有隐藏状态会传递到输出层,而记忆细胞不参与输出层的计算。
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 = torch.sigmoid(torch.mm(X, W_xi) + torch.mm(H, W_hi) + b_i)
F = torch.sigmoid(torch.mm(X, W_xf) + torch.mm(H, W_hf) + b_f)
O = torch.sigmoid(torch.mm(X, W_xo) + torch.mm(H, W_ho) + b_o)
C_tilda = torch.tanh(torch.mm(X, W_xc) + torch.mm(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
同上一节一样,我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
我们每过40个迭代周期便根据当前训练的模型创作一段歌词。
d2lt.train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens,
vocab_size, device, 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 212.669890, time 0.45 sec - 分开 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 - 不分开 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 我不的我 epoch 80, perplexity 65.111137, time 0.43 sec - 分开 我想你这你 我不要这你 我不要这我 我不要这我 我不要这我 我不要这我 我不要这我 我不要这我 我 - 不分开 我想你这你 我不要这你 我不要这我 我不要这我 我不要这我 我不要这我 我不要这我 我不要这我 我 epoch 120, perplexity 15.788646, time 0.42 sec - 分开 我想你这生你的你的手 手感开开 你来我开已经天透透看透不不多 说是我我的肩膀 你 在我我已睡你的你 - 不分开 你是我不不不要一场悲人 不不说说 你我已已已在在家透透透想不能 我有我的肩你在在元前 深埋在美索 epoch 160, perplexity 4.286593, time 0.43 sec - 分开 我想带你的微笑每天都能看到 我知道这里很美但家乡的你更美 我想想你的肩膀 你 在我胸口睡著 像这 - 不分开 我不要这生我 我不能 我不我 爱情走的太快就像龙卷风 不不开受我已无 不不躲 想不我 我想要 我不
在PyTorch中我们可以直接调用nn
模块中的LSTM
类。
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2lt.RNNModel(lstm_layer, num_hiddens, vocab_size)
d2lt.train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,
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 14.682463, time 0.08 sec - 分开 爱情来的快就像龙卷风 娘子她人在江南等我 泪不休 语沉默娘子她人在江南等我 泪不休 语沉默娘子她 - 不分开 让我遇见你的那已 还小的溪边 我想就这样牵着 还开始打我妈妈 说小二 有些我 泪不着我 别怪我 epoch 80, perplexity 2.020084, time 0.08 sec - 分开 我已经是开 杵在这里 我只想带你的那个人已经不熟我 上你一九四三 回头看的的段 还是分手 它一直 - 不分开不了我不能到受 我妈 这辈子依旧每日折一枝杨柳 在那里在停留 还为分手 那个人在小村外的溪边河口默默 epoch 120, perplexity 1.225078, time 0.08 sec - 分开 我不能承受我已无处可躲 我不要再想 我不要再想 我不 我不 我不要再想你 爱情来的太快就像龙卷风 - 不分开不能承受我已无处可躲 我不要再想 我不要再想 我不 我不 我不要再想你 爱情来的太快就像龙卷风 离不 epoch 160, perplexity 1.112746, time 0.08 sec - 分开 我不懂不球 屉色入不 和你看到我妈妈 让味型的黑猫笑起来像 它在空中停枚枪所 人 我不达米茶模 - 不分开不能承不能蟑蟑 离开始共动 一切到底什么我想要 却发现迷了路怎么找也找不着 心血来潮起个大早 怎么我
[1] Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780.