15现代循环神经网络—GRU与LSTM
目录
-
-
1.门控循环单元 GRU
-
- 关注一个序列
- 门
- 候选隐状态(candidate hidden state)
- 隐状态
- 总结
- 从零开始代码实现
- 代码简洁实现
-
2.长短期记忆网络 LSTM
-
- 门
- 候选记忆单元(candidate memory cell)
- 记忆单元
- 隐状态
- 代码
-
1.门控循环单元 GRU
GRU 是最近几年提出来的,在 LSTM 之后,是一个稍微简化的变体,通常能够提供同等的效果,并且计算速度更快
在某些情况下,希望存在某些机制能够实现:
- 希望某些机制能够在一个记忆元里存储重要的早期信息
- 希望某些机制能够跳过隐状态表示中的此类词元
- 希望某些机制能够重置内部状态表示
做 RNN 的时候处理不了太长的序列
- 因为序列信息全部放在隐藏状态中,当时间到达一定长度的时候,隐藏状态中会累积过多的信息,不利于相对靠前的信息的抽取
关注一个序列

在观察一个序列的时候,不是每个观察值都同等重要
- 对于一个猫的图片的序列突然出现一只老鼠,老鼠的出现很重要,第一次出现猫也很重要,但是之后再出现猫就不那么重要了
- 在一个句子中,可能只是一些关键字或者关键句比较重要
- 视频处理中,其实帧与帧之间很多时候都差不多,但是在切换场景的时候,每次的切换是比较重要的
在 RNN 中没有特别关心某些地方的机制,对于它来讲仅仅是一个序列,而门控循环单元可以通过一些额外的控制单元,使得在构造隐藏状态的时候能够挑选出相对来说更加重要的部分**(注意力机制在这方面强调得更多一点)**
- 更新门(update gate):能关注的机制 ,能够将信息尽量放在隐藏状态中,控制新状态中有多少个是旧状态的副本
- 重置门(reset gate):能遗忘的机制 ,能够遗忘输入或者隐藏状态中的一些信息,控制"可能还想记住"的过去状态的数量
门

- 上图表示门控循环单元模型,输入是由当前时间步的输入和前一时间步的隐状态给出;重置门和更新门的输出是由使用 sigmoid 激活函数的两个全连接层给出
- Xt:输入
- H(t-1):隐藏状态
- Rt:重置门.如果是 RNN 的话,所表示的是使用 sigmoid 作为激活函数对应的隐藏状态的计算
- Zt:更新门.计算方式和 RNN 中隐藏状态以及 Rt 的计算方式是一样的
- 门可以认为是和隐藏状态同样长度的向量,它的计算方式和 RNN 中隐藏状态的计算方式是一样的
候选隐状态(candidate hidden state)
并不是真正的隐藏状态,只是用来生成真正的隐藏状态

- Rt 与 H(t-1) 按元素乘法,对于一个样本来讲,Rt 和 H(t-1)是一个长度相同的向量,所以可以按照元素做乘法
- Rt 是一个取值为 0~1 的值,Rt 越靠近 0 , Rt 与 H(t-1)按元素乘法得到的结果就越接近 0 ,也就相当于将上一时刻的隐藏状态忘掉
- 极端情况下,如果 Rt 全部变成 0 的话,就相当于从当前时刻开始,前面的信息全部不要,隐藏状态变成 0 ,从初始化状态开始,任何预先存在的隐状态都会被重置为默认值
- 另外一个极端情况:如果 Rt 全是 1 的话,就表示,将当前时刻之前所有的信息全部拿过来做更新,就等价于 RNN 中隐藏状态的更新方式
- 实际上 Rt 是一个可以学习的参数,所以它会根据前面的信息来学习哪些信息是能够进入到下一轮隐藏状态的更新,哪一些信息需要进行舍弃,这些操作都是自动进行的,因此被叫做控制单元
隐状态
-
门控循环单元与普通的循环神经网络之间的关键区别在于:前者支持隐状态的门控,这意味着模型有专门的机制来确定应该何时更新隐状态,以及应该何时重置隐状态(这些机制都是可学习的)
-
真正的隐藏状态的计算方式如下所示

-
Zt 也是一些取值为 0~1 的一些数字组成的
-
假设 Zt 都等于 1 ,即 Ht 等于 H(t-1),相当于不使用 Xt 来更新隐藏状态,直接将过去的状态当成现在的状态,模型只保留旧状态 ,此时,来自 Xt 的信息基本上被忽略.当整个子序列的所有时间步的更新门都接近于 1 ,则无论序列的长度如何,在序列起始时间步的旧隐状态都将很容易保留并传递到序列结束
-
假设 Zt 都等于 0 , Ht 等于候选隐状态 Ht tittle .相当于回到了 RNN 的情况,不看过去的隐藏状态,只看现在更新的隐藏状态,能够帮助处理循环神经网络中的梯度消失问题,并且能够更好地捕获时间步距离很长的序列的依赖关系
总结

- GRU 中引入了两个额外的门,每个门可以学习的参数和 RNN 一样多,整个可学习的权重数量是 RNN 的三倍
- Rt 和 Zt 都是控制单元,用来输出取值为 0~1 的数值
- Rt 用来衡量在更新新的隐藏状态的时候,要用到多少过去隐藏状态的信息
- Zt 用来衡量在更新新的隐藏状态的时候,需要用到多少当前Xt相关的信息
- 当 Zt 全为 0 , Rt 全为 1 时,等价于 RNN
- 当 Zt 全为 1 时,直接忽略掉当前 Xt
- GRU 通过引入 Rt 和 Zt ,从而能够在各种极端情况之间进行调整
- 门控循环神经网络可以更好地捕获时间步距离很长的序列上的依赖关系
- 重置门有助于捕获序列中的短期依赖关系
- 更新门有助于捕获序列中的长期依赖关系
- 重置门打开时,门控循环单元包含基本循环神经网络
- 更新门打开时,门控循环单元可以跳过子序列
从零开始代码实现
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)
#初始化模型参数
#下一步是初始化模型参数。 我们从标准差为0.01的高斯分布中提取权重, 并将偏置项设为0,超参数num_hiddens定义隐藏单元的数量, 实例化与更新门、重置门、候选隐状态和输出层相关的所有权重和偏置。
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
#定义模型
#现在我们将定义隐状态的初始化函数init_gru_state。 与RNN中定义的init_rnn_state函数一样, 此函数返回一个形状为(批量大小,隐藏单元个数)的张量,张量的值全部为零。
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)
代码解读
2.长短期记忆网络 LSTM
- 长期以来,隐变量模型存在着长期信息保存和短期输入缺失的问题,解决这个问题最早的方法之一就是 LSTM
- 发明于90年代
- 使用的效果和 GRU 相差不大,但是使用的东西更加复杂
门
-
长短期记忆网络的设计灵感来自于计算机的逻辑门
-
长短期记忆网络引入了记忆元(memory cell),或简称为单元(cell)(有些文献认为记忆元是隐状态的一种特殊类型,它们与隐状态具有相同的形状,其设计目的是用于记录附加的信息)
-
长短期记忆网络有三个门:忘记门(重置单元的内容,通过专用机制决定什么时候记忆或忽略隐状态中的输入)、输入门(决定何时将数据读入单元)、输出门(从单元中输出条目),门的计算和 GRU 中相同,但是命名不同
- 忘记门(forget gate):将值朝 0 减少
- 输入门(input gate):决定是否忽略掉输入数据
- 输出门(output gate):决定是否使用隐状态

-
类似于门控循环单元,当前时间步的输入 和前一个时间步的隐状态 作为数据送入长短期记忆网络的门中,由三个具有 sigmoid 激活函数的全连接层 处理,以计算输入门、遗忘门和输出门的值(这三个门的值都在 0~1 的范围内)
候选记忆单元(candidate memory cell)

候选记忆元的计算与输入门、遗忘门、输出门的计算类似,但是使用了 tanh 函数作为激活函数,函数的值在 -1~1 之间
记忆单元

- 在长短期记忆网络中,通过输入门和遗忘门来控制输入和遗忘(或跳过):输入门 It 控制采用多少来自 Ct tilde 的新数据,而遗忘门 Ft 控制保留多少过去的记忆元 C(t-1) 的内容
- 如果遗忘门始终为 1 且输入门始终为 0 ,则过去的记忆元 C(t-1) 将随时间被保存并传递到当前时间步(引入这种设计是为了缓解梯度消失的问题,并更好地捕获序列中的长距离依赖关系)
- 上一时刻的记忆单元会作为状态输入到模型中
- LSTM 和 RNN/GRU 的不同之处在于: LSTM 中的状态有两个, C 和 H
隐状态

- 在长短期记忆网络中,隐状态 Ht 仅仅是记忆元 Ct 的 tanh 的门控版本 ,因此确保了 Ht 的值始终在 -1~1 之间
- tanh 的作用:将 Ct 的值限制在 -1 和 1 之间
- Ot 控制是否输出, Ot 接近 1 ,则能有效地将所有记忆信息传递给预测部分 ; Ot 接近 0 ,表示丢弃当前的 Xt 和过去所有的信息,只保留记忆元内的所有信息,而不需要更新隐状态

LSTM 和 GRU 所想要实现的效果是差不多的,但是结构更加复杂
1. C :一个数值可能比较大的辅助记忆单元
2. C 中包含两项: 当前的 Xt 和过去的状态(在 GRU 中只能二选一,这里可以实现两个都选)
长短期记忆网络包含三种类型的门:输入门、遗忘门和输出门
长短期记忆网络的隐藏层输出包括“隐状态”和“记忆元”。只有隐状态会传递到输出层,而记忆元完全属于内部信息
长短期记忆网络可以缓解梯度消失和梯度爆炸
长短期记忆网络是典型的具有重要状态控制的隐变量自回归模型。多年来已经提出了其他许多变体,例如,多层、残差连接、不同类型的正则化。但是由于序列的长距离依赖性,训练长短期记忆网络和其他序列模型(如门控循环单元)的成本较高
代码
就代码而言,LSTM比GRU多了一个C,其余地方整体相同。
#简洁实现代码对比
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)
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)
#只有一个gru_layer = nn.GRU(num_inputs, num_hiddens)改为 lstm_layer = nn.LSTM(num_inputs, num_hiddens)
NModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
#只有一个gru_layer = nn.GRU(num_inputs, num_hiddens)改为 lstm_layer = nn.LSTM(num_inputs, num_hiddens)
代码解读
