动手学深度学习 6.7 门控循环单元(GRU)

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)


上一节介绍了循环神经网络中的梯度计算方法。我们发现,当时间步数较大或者时间步较小时,循环神经网络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。

通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。

门控循环神经网络(gated recurrent neural network)的提出,正是为了更好地捕捉时间序列中时间步距离较大的依赖关系

它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是一种常用的门控循环神经网络。另一种常用的门控循环神经网络则将在下一节中介绍。

1. 门控循环单元

下面将介绍门控循环单元的设计。它引入了重置门(reset gate)和更新门(update gate)的概念,从而修改了循环神经网络中隐藏状态的计算方式。

1.1 重置门 和 更新门

如图6.4所示,门控循环单元中的重置门和更新门的输入均为当前时间步输入 \(X_t\) 与上一时间步隐藏状态 \(H_{t−1}\) ,输出由激活函数为sigmoid函数的全连接层计算得到。

具体来说,假设隐藏单元个数为h,给定时间步 t 的小批量输入 \(X_t \in \mathbb{R}^{n\times d}\)(样本数为n,输入个数为d)和上一时间步隐藏状态 \(H_{t-1} \in \mathbb{R}^{n\times h}\)

重置门 \(R_{t} \in \mathbb{R}^{n\times h}\) 和更新门 \(Z_{t} \in \mathbb{R}^{n\times h}\)的计算如下:

其中 \(W_{xr},W_{xz} \in \mathbb{R}^{d \times h}\)\(W_{hr},W_{hz} \in \mathbb{R}^{h \times h}\) 是权重参数, \(b_{r},b_{z} \in \mathbb{R}^{1 \times h}\) 是偏差参数。

3.8节(多层感知机)节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门 \(R_t\) 和更新门 \(Z_t\) 中每个元素的值域都是[0,1]

1.2 候选隐藏状态

接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所示,我们将当前时间步重置门的输出上一时间步隐藏状态做按元素乘法(符号为⊙)。

如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态

然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为[−1,1]。

具体来说,时间步t的候选隐藏状态 \(H_t\) 的计算为

从上面这个公式可以看出,重置门 控制了上一时间步的隐藏状态 如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。

可以与正常循环神经网络(下图公式)的隐藏状态计算明显看出不同。

1.3 隐藏状态

最后,时间步t的隐藏状态 \(H_t\) 的计算使用当前时间步的更新门 \(Z_t\) 来对上一时间步的隐藏状态 \(H_{t−1}\) 和当前时间步的候选隐藏状态 \(\tilde{H_t}\) 做组合:

可以很明显看出,如果更新门趋于1,那么就近乎不更新,直接保留上一步骤的隐藏状态 \(H_{t−1}\) 。如果趋于0,则完全不保存上一步骤的隐藏状态 \(H_{t−1}\)

我们这里简单概述重置门与更新门的关系与作用。

GRU 想做到:在决定“下一步怎么思考”时,可以选择性地忽略旧记忆(由 r 控制),但在真正更新隐藏状态时再决定是否保留旧记忆(由 z 控制)。

也就是:重置门r 控制“在更新时看不看旧记忆”更新门z 控制“最终隐藏状态要保留多少旧记忆”

这两个作用独立,缺一不可。

2. 读取数据集

为了实现并展示门控循环单元,下面依然使用周杰伦歌词数据集来训练模型作词。这里除门控循环单元以外的实现已在6.2节(循环神经网络)中介绍过。以下为读取数据集部分。

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

(corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()

3. 从零开始实现

3.1 初始化模型参数

下面的代码对模型参数进行初始化。超参数num_hiddens定义了隐藏单元的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)

def get_params():
def _one(shape):
ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
return torch.nn.Parameter(ts, requires_grad=True)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))

W_xz, W_hz, b_z = _three() # 更新门参数
W_xr, W_hr, b_r = _three() # 重置门参数
W_xh, W_hh, b_h = _three() # 候选隐藏状态参数

# 输出层参数
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q])

也就是下图所需的参数

=

3.2 定义模型

下面的代码定义隐藏状态初始化函数init_gru_state。同6.4节(循环神经网络的从零开始实现)中定义的init_rnn_state函数一样,它返回由一个形状为(批量大小, 隐藏单元个数)的值为0的Tensor组成的元组。

即初始化的隐藏状态H: \(H_{0} \in \mathbb{R}^{n\times h}\)

1
2
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )

下面根据门控循环单元的计算表达式定义模型。

1
2
3
4
5
6
7
8
9
10
11
12
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(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
H_tilda = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(R * H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)

3.3 训练模型并创作歌词

我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。

1
2
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']

我们每过40个迭代周期便根据当前训练的模型创作一段歌词。

1
2
3
4
5
d2l.train_and_predict_rnn(gru, get_params, init_gru_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)

4 简洁实现

在PyTorch中我们直接调用nn模块中的GRU类即可。

1
2
3
4
5
6
7
8
lr = 1e-2 # 注意调整学习率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)

d2l.train_and_predict_rnn_pytorch(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)