GRU、LSTM、双向循环神经网络
动手学深度学习笔记
-
一、门控循环单元(GRU)
-
- 1.重置门和更新门
- 2.候选隐状态
- 3.隐状态
- 4.PyTorch代码
-
二、长短期记忆网络(LSTM)
-
输入通道、遗忘通道和输出通道
-
存储单元
-
中间状态
-
基于PyTorch的代码实现
-
三、深度循环神经网络
-
四、双向循环神经网络
-
为了更好地理解GRU和LSTM的工作原理与应用方法,在学习过程中建议先深入研究RNN的基础代码实现,并参考这份PyTorch相关的教程资源(详细介绍了RNN模型的基本概念)。
一、门控循环单元(GRU)
1.重置门和更新门
重置门让我们能够调节记忆能力中的留存信息数量;更新门则决定了新状态中有多少来自旧信息。

两个⻔的输出是由使用sigmoid激活函数的两个全连接层给出。
2.候选隐状态
时间步t的候选隐状态\pmb{\tilde H}_t \in \mathbb R^{n×h},计算如下:

\pmb{\tilde H}_t=tanh(\pmb{X}_t\pmb{W}_{xh}+(\pmb{R}_{t}\odot \pmb{H}_{t-1})\pmb{W}_{hh}+\pmb{b}_h)
在其中,在时间步t上定义的运算符⊙代表按元素乘法的Hadamard积。通过将\pmb{R}_{t}与\pmb{H}_{t-1}进行按元素相乘的操作,可以弱化过去状态对当前计算的影响。为了保证候选隐态向量的数值范围限定在(-1, 1),我们采用双曲正切函数tanh作为非线性激活函数。
3.隐状态
\pmb{H}_t=\pmb{Z}_t\odot\pmb{H}_{t-1}+(1-\pmb{Z}_{t})\odot\pmb{\tilde H}_t
每当更新门 \pmb{Z}_t 接近1时, 模型倾向于仅保留前一状态, 此时新输入的信息基本未被考虑. 反之则不然, 当 \pmb{Z}_t 接近0时, 新的隐层单元的状态将趋向于模仿候选隐层单元的状态.

小结 :
这些设计能够有效解决RNN中的梯度消失问题,并有助于识别长期依赖关系。
当整个子序列的所有时间步的更新开启度均趋近于1时,在其长度的变化不会影响的情况下(即无论其长度如何),这些旧隐状态将在起始时刻得以保留,并在其结束时刻传递出去。
⻔控循环单元具有以下两个显著特征:
• 重置⻔有助于捕获序列中的短期依赖关系。
• 更新⻔有助于捕获序列中的⻓期依赖关系。
4.PyTorch代码
- 从零实现
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def get_params(vocab_size, num_hiddens, device):
"""初始化模型参数"""
num_inputs = num_outputs = vocab_size
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_xz, W_hz, b_z = three() # 更新门参数
W_xr, W_hr, b_r = three() # 重置门参数
W_xh, W_hh, b_h = three() # 候选隐状态参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
def init_gru_state(batch_size, num_hiddens, device):
"""隐状态的初始化函数"""
return (torch.zeros((batch_size, num_hiddens), device=device), )
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
- 训练
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

- 简洁实现
# 简洁实现
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

二、长短期记忆网络(LSTM)
1.输入门、遗忘门和输出门

\pmb{I}_t=\sigma(\pmb{X}_t\pmb{W}_{xi}+\pmb{H}_{t-1}\pmb{W}_{hi}+\pmb{b}_i) \\ \pmb{F}_t=\sigma(\pmb{X}_t\pmb{W}_{xf}+\pmb{H}_{t-1}\pmb{W}_{hf}+\pmb{b}_f)\\ \pmb{O}_t=\sigma(\pmb{X}_t\pbb_{xo}+\pbb_{H}_{t-1}\pbw_{ho}+\pb_b_o)通过带有sigmoid激活函数的全连接层进行信息整合与计算,在确定输入、遗忘和输出门的信息状态时发挥了关键作用;这些门的信息均处于0到1之间。
2.记忆元
候选记忆单元:
\bm{\tilde C}_t = \text{tanh}(\bm X_t \bm W_{xc} + \bm H_{t-1} \bm W_{hc} + \bm b_c)
记忆单元:
\bm C_t = \bm F_t \odot \bm C_{t-1} + \bm I_t \odot \bm{\tilde C}_t
当遗忘门始终保持打开状态且输入门始终保持关闭状态时,则过去的状态信息\bm C_{t-1}会持续留存并传递至当前时刻。
这种设计的引入旨在缓解梯度消失的问题,并且能够更有效地捕捉序列中的长距离依赖关系。
3.隐状态
\pmb{H}_t=\pmb{O}_t\odot tanh(\pmb{C}_t)
\pmb{H}_t的取值恒定限定于区间(−1, 1)内。当输出门趋近于1时,则能够高效地将所有记忆信息传递给预测部分。当输出门趋近于0时,则仅保留记忆元内的所有信息,并无需对隐状态进行更新操作。

小结
- 短期记忆网络有三个主要的通道:输入通道、遗忘通道和输出通道。
- 短期记忆网络的隐藏层输出由"隐状态"和"记忆元"组成。其中只有隐状态会被传递给输出层节点进行信息处理,而记忆元则属于内部信息,不参与后续计算过程。
- 短期记忆网络能够有效缓解梯度消失问题以及防止梯度爆炸现象的发生。
4.PyTorch代码
- 从零实现LSTM
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
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 = []
for X in inputs:
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_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
C = F * C + I * C_tilda
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H, C)
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
- 简洁实现
# 简洁实现
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
三、深度循环神经网络

\pmb{H}_t^{(l)}=\phi_l(\pmb{H}_t^{(l-1)}\pmb{W}_{xh}^{(l)}+\pmb{H}_{t-1}^{(l)}\pmb{W}_{hh}^{(l)}+\pmb{b}_h^{(l)})
\pmb{O}_t=\pmb{H}_t^{(l)}\pmb{W}_{hq}+\pmb{b}_q
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
四、双向循环神经网络
\overrightarrow{\pmb H}_t=\phi(\pmb{X}_t\pmb{W}_{xh}^{(f)}+\overrightarrow{\pmb H}_{t-1}\pmb{W}_{hh}^{(f)}+\pmb{b}_h^{(f)})\\ \overleftarrow{\pmb H}_t=\phi(\pmb{X}_t\pmb{W}_{xh}^{(b)}+\overleftarrow{\pmb H}_{t-1}\pmb{W}_{hh}^{(b)}+\pmb{b}_h^{(b)})
通过将前向时序表示\overrightarrow{\boldsymbol{H}}_t与反向时序表示\overleftarrow{\boldsymbol{H}}_t进行结合处理, 从而得到输出层的时序表示 \boldsymbol{H}_{ t }\in \mathbb {R}^{n\times 2h}。
其中, \boldsymbol{H}_{ t } = \overrightarrow{\boldsymbol{H}}_ t \oplus \overleftarrow{\boldsymbol{H}}_ t, \boldsymbol{O}_{ t } = \boldsymbol{H}_{ t }\cdot W_{ hq } + b_{ q }.
注意:
双向循环神经网络一般不用于预测,因为预测的时候是看不到后面的
