一、提出背景
对于传统的循环神经网络而言,过去的所有信息只存储在一个隐状态中,实际上这是一种比较简单粗暴的方法,使用这种方法构建循环神经网络存在着 长期信息保存(Long-term Information Retention)和 短期输入缺失(Short-term Input Absence)的问题。
长期信息保存:
在处理序列数据时,RNN需要记住之前的时间步的信息以便在后续时间步中使用。然而,标准的RNN在长时间序列中往往难以有效地保存长期信息。当 RNN 试图通过反向传播来学习长期依赖关系时,梯度可能会变得非常小(消失)或非常大(爆炸),这使得模型很难学习到远距离的时间依赖关系。
短期输入缺失:
在处理序列数据时,有时在一段时间内可能没有新的输入,或者新输入的信息对当前任务并不关键。这种情况下,模型需要能够在没有新输入的情况下继续处理任务,同时保持之前获取的重要信息。例如,在语音识别中,说话人可能会有一个短暂的停顿,但是模型仍然需要基于之前的语音片段来预测接下来的内容。
基于上述问题,长短期记忆网络LSTM(Long-Short Term Memory)被提出。
二、实现思路
遗忘门 forget gate:
遗忘门用于决定哪些信息应该从 LSTM 单元的状态中丢弃。后续会把遗忘门的输出作为一个权重使用,Sigmoid函数能够确保遗忘门的输出介于0和1之间。
输入门 input gate:
输入门用于控制哪些新信息会进入 LSTM 单元的状态。后续会把输入门的输出作为一个权重使用,Sigmoid函数能够确保输入门的输出介于0和1之间。
输出门 output gate:
输出门用于决定哪些信息应该被输出给下一个时间步。后续会把输出门的输出作为一个权重使用,Sigmoid函数能够确保输出门的输出介于0和1之间。
候选记忆单元 candidate memory:
可以看到,候选记忆单元的计算过程和标准RNN中隐状态的计算过程是一样的。tanh使得候选记忆单元的取值在-1到1之间。需要注意的是,此处计算出来的只是候选的记忆单元,并不是真正的记忆单元。
记忆单元 memory:
如下面的式子所示,真正记忆单元的取值是过去记忆单元(代表过去的信息)与候选记忆单元(代表当前的信息)的加权和,其中,过去记忆单元和候选记忆单元的权重分别取决于遗忘门和输入门“门打开的程度”。
隐状态 hidden state:
lstm中隐状态的计算方式如下。tanh能够将记忆单元的值映射到-1到1之间,而输出门的输出Ot则能够决定应该传递多少当前时刻的信息给下一时刻。如果Ot=0,可以理解为:Ht=0,没有信息传递给下一时刻,下一时刻的输出只与下一时刻的输入有关,与过去的信息完全无关。
输出层(图中没有画出):
输出层与标准rnn的输出层一致。
三、从零开始实现LSTM
lstm.py:
导入依赖包:
import torch
from torch import nn
import seqDataLoader as loader
定义模型参数:
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
# 按照标准差为0.01的高斯分布初始化权重
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
def three():
return normal((num_inputs, num_hiddens)), normal((num_hiddens, num_hiddens)), torch.zeros(num_hiddens, device=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 = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=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_(True)
return params
隐状态初始化:
def init_lstm_state(batch_size, num_hiddens, device):
return torch.zeros((batch_size, num_hiddens), device=device), \
torch.zeros((batch_size, num_hiddens), device=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 = []
# inputs的维度:(num_steps,batch_size,vocab_size);X的维度:(batch_size,vocab_size)
for X in inputs:
# @表示矩阵乘法,I、F、O、C_candidate的尺寸都为 (batch_size,num_hiddens)
I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
C_candidate = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
# *表示对应元素相乘,C、H的尺寸同样为 (batch_size,num_hiddens)
C = F * C + I * C_candidate
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
# cat操作后,outputs的形状变为 (num_steps * batch_size,num_outputs)
return torch.cat(outputs, dim=0), (H, C)
主函数测试:
if __name__=='__main__':
batch_size, num_steps = 3, 10
train_iter, vocab = loader.loadData(batch_size, num_steps, 'loseyourself.txt')
vocab_size, num_hiddens, device = len(vocab), 256, torch.device('cpu')
num_epochs, lr = 250, 1
model = rnnModel.RNNModelScratch(vocab_size, num_hiddens, device, get_lstm_params, init_lstm_state, lstm)
rnnModel.trainRNN(model, train_iter, vocab, lr, num_epochs, device)
参考链接:
《动手学深度学习》 — 动手学深度学习 2.0.0 documentationhttps://zh-v2.d2l.ai/