门控循环单元(GRU)
引用翻译:《动手学深度学习》
在一个递归神经网络中计算梯度,矩阵的长积会导致梯度消失或发散。
可能会遇到这样的情况:早期的观察结果对于预测所有未来的观察结果来说是非常重要的。考虑一下这样一种有点矫揉造作的情况,即第一个观察包含一个校验和,而目标是在序列的最后辨别校验和是否正确。
在这种情况下,第一个符号的影响是至关重要的。我们希望有一些机制,将重要的早期信息储存在记忆单元中。如果没有这样的机制,我们将不得不给这个观察分配一个非常大的梯度,因为它影响到所有后续的观察。
我们可能会遇到这样的情况:一些符号是不相关的观测。例如,在解析一个网页的时候,可能会有一些辅助的HTML代码,这些代码对于评估网页上所传达的情感是不相关的。我们希望有一些机制来跳过潜在状态表示中的这些符号。
我们可能会遇到这样的情况:一个序列的各个部分之间存在着逻辑上的中断。例如,一本书的章节之间可能有一个过渡,证券市场的熊市和牛市之间可能有一个过渡,等等;在这种情况下,最好能有一种方法来重置我们的内部状态表示。
已经提出了一些方法来解决这个问题。最早的方法之一是Hochreiter和Schmidhuber的长短时记忆(LSTM),1997175,Cho等人的门控递归单元(GRU),2014176是一个稍微精简的变体,通常具有可比的性能,而且计算速度明显加快。更多细节见Chung等人,2014177。
一、隐藏状态的门控
普通RNN和GRU之间的关键区别在于,后者支持隐藏状态的门控。这意味着我们有专门的机制来控制隐藏状态何时被更新,何时被重置。
这些机制是经过学习的,它们解决了上面列出的问题。例如,如果第一个符号非常重要,我们将学习在第一次观察后不更新隐藏状态。同样地,我们将学习跳过不相关的临时观察。
最后,我们将学会在需要的时候重置隐性状态。我们将在下面详细讨论这个问题。
二、重置闸门和更新闸门
我们首先需要介绍的是重置门和更新门。我们把它们设计成条目为(0,1)的向量,这样我们就可以进行凸式组合,例如,一个隐藏状态和一个替代状态的组合。例如,一个重置变量将允许我们控制我们可能还想记住多少以前的状态。同样地,一个更新变量将允许我们控制新状态中有多少只是旧状态的副本。
我们首先通过工程闸门来生成这些变量。下图说明了GRU中复位和更新门的输入,给定的是当前时间步骤的输入𝑋𝑡和前一时间步骤的隐藏状态𝐻𝑡-1。输出由一个全连接层给出,其激活函数为sigmoid。
这里,我们假设有h个隐藏单元,对于给定的时间步骤t,小批量输入为𝑋𝑡 ∈ 𝑅𝑛×𝑑(例子数:n,输入数:d),上一个时间步骤的隐藏状态为𝐻𝑡-1∈ 𝑅𝑛×?。然后,复位门𝑅𝑡 ∈ 𝑅𝑛×?和更新门𝑍𝑡 ∈ 𝑅𝑛×?计算如下。
R
t
=
σ
(
X
t
W
x
r
+
H
t
?
1
W
h
r
+
b
r
)
R_t = σ(X_t W_{xr} + H_{t?1} W_{hr} + b_r )
Rt?=σ(Xt?Wxr?+Ht?1?Whr?+br?)
Z
t
=
σ
(
X
t
W
x
z
+
H
t
?
1
W
h
z
+
b
z
)
Z_t = σ(X_t W_{xz} + H_{t?1} W_{hz} + b_z )
Zt?=σ(Xt?Wxz?+Ht?1?Whz?+bz?)
from IPython.display import SVG
SVG(filename= '../img/gru_1.svg')
?
图:GRU中的重置和更新门。
这里,
W
x
r
W_{xr}
Wxr? ,
W
x
z
W_{xz}
Wxz? ∈
R
d
×
h
R^{d×h}
Rd×h 和
W
h
r
W_{hr}
Whr? ,
W
h
z
W_{hz}
Whz? ∈
R
h
×
h
R^{h×h}
Rh×h 是权重参数,
b
r
b_r
br? ,
b
z
b_z
bz? ∈
R
1
×
h
R^{1×h}
R1×h 是偏差。我们使用一个sigmoid函数来将数值转换到区间(0,1)。
三、复位门的作用
我们首先将复位门与常规的潜伏状态更新机制结合起来。在一个传统的深层RNN中,我们会有一个更新的形式
H
t
=
t
a
n
h
(
X
t
W
x
h
+
H
t
?
1
W
h
h
+
b
h
)
.
H_t = tanh(X_t W_{xh} + H_{t?1} W_{hh} + b_h ).
Ht?=tanh(Xt?Wxh?+Ht?1?Whh?+bh?).
这与上一节的讨论基本相同,只是采用了tanh形式的非线性,以确保隐藏状态的值保持在区间(-1,1)内。如果我们希望能够减少以前状态的影响,我们可以将𝐻𝑡-1与𝑅𝑡元素相乘。
只要𝑅𝑡中的条目接近于1,我们就可以恢复一个传统的深度RNN。
对于𝑅𝑡中所有接近于0的条目,隐藏状态是以𝑋𝑡为输入的MLP的结果。
因此,任何预先存在的隐藏状态都被 "重置 "为默认状态。这导致了下面这个新的隐藏状态的候选值(它是一个候选值,因为我们仍然需要纳入更新门的动作)。
此处 等号前面的是 H?_t ,注意上面的符号
H
~
t
=
t
a
n
h
(
X
t
W
x
h
+
(
R
t
⊙
H
t
?
1
)
W
h
h
+
b
h
)
H?_t = tanh(X_t W_{xh} + (R_t ⊙ H_{t?1} ) W_{hh} + b_h )
H~t?=tanh(Xt?Wxh?+(Rt?⊙Ht?1?)Whh?+bh?)
下图说明了应用复位门后的计算流程。符号⊙表示张量之间的点状乘法。
四、行动中的更新门
接下来,我们需要将更新门的作用纳入其中。这决定了新状态𝐻𝑡在多大程度上只是旧状态𝐻𝑡-1,以及新的候选状态
H
~
t
H?_t
H~t?(H?_t)被使用的程度。门控变量𝑍𝑡可用于此目的,只需在两个候选状态之间进行元素凸组合即可。这就导致了GRU的最终更新方程。
H
t
=
Z
t
⊙
H
t
?
1
+
(
1
?
Z
t
)
⊙
H
~
t
.
H_t = Z_t ⊙ H_{t?1} + (1 ? Z_t ) ⊙ H?_t .
Ht?=Zt?⊙Ht?1?+(1?Zt?)⊙H~t?.
SVG(filename= '../img/gru_2.svg')
Fig. 10.8.2: GRU中的候选隐藏状态计算。乘法是以元素方式进行的。
每当更新门接近于1时,我们就简单地保留旧状态。在这种情况下,来自𝑋𝑡的信息基本上被忽略了,实际上跳过了依赖链中的时间步骤t。
当它接近0时,新的潜伏状态𝐻𝑡接近候选潜伏状态H?_t。这些设计可以帮助应对RNNs中的梯度消失问题,并更好地捕捉时间步长距离大的时间序列的依赖性。总之,GRU有以下两个突出的特点。
-
重置门有助于捕捉时间序列中的短期依赖关系。 -
更新门有助于捕捉时间序列中的长期依赖关系。
五、从头开始实施
为了更好地理解这个模型,让我们从头开始实现一个GRU。
1、读取数据集
我们首先阅读我们在第10.5节中使用的The Time Machine语料库。下面给出了读取数据集的代码。
import sys
sys.path.insert(0, '..')
import d2l
import torch
import torch.nn as nn
from d2l import RNNModel
from d2l import load_data_time_machine
from d2l import train_and_predict_rnn
from d2l import train_and_predict_rnn_nn
torch.set_default_tensor_type('torch.cuda.FloatTensor')
corpus_indices, vocab = load_data_time_machine()
SVG(filename= '../img/gru_3.svg')
?
Fig. 10.8.3: 图10.8.3:GRU中的隐藏状态计算。和以前一样,乘法是按元素进行的。
2、初始化模型参数
下一步是初始化模型参数。我们从方差为0.01的高斯中抽取权重,并将偏置设置为0。我们将所有与更新和重置门以及候选隐藏状态本身有关的条款实例化。随后,我们将梯度附加到所有参数上。
num_inputs, num_hiddens, num_outputs = len(vocab), 256, len(vocab)
device = d2l.try_gpu()
print('Using', device)
Using cpu
def get_params():
def _one(shape):
return torch.randn(shape, device=device).normal_(std=0.01)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((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 = _one((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
3、定义模型
现在我们将定义隐藏状态初始化函数init_gru_state。就像第10.5节中定义的init_rnn_state函数一样,这个函数返回一个由带有形状(批量大小,隐藏单元的数量)的tesor组成的元组,并且所有的值都设置为0。
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros(size=(batch_size, num_hiddens), device=device), )
现在我们准备定义实际的模型。它的结构与基本的RNN单元相同,只是更新方程更加复杂。
实际上就是把之前定义的公式用代码列出来
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:
m = nn.Sigmoid()
Z = m(torch.matmul(X.float(), W_xz) + torch.matmul(H.float(), W_hz) + b_z)
R = m(torch.matmul(X.float(), W_xr) + torch.matmul(H.float(), W_hr) + b_r)
h = nn.Tanh()
H_tilda = h(torch.matmul(X.float(), W_xh) + torch.matmul(R * H.float(), W_hh) + b_h)
H = Z * H.float() + (1 - Z) * H_tilda
Y = torch.matmul(H.float(), W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
4、训练和预测
训练和预测的工作方式与之前完全相同。也就是说,我们需要定义一个历时数,截断的步骤数,最小批量大小,学习率以及我们应该如何积极地剪断梯度。最后,我们根据旅行者和时间旅行者的前缀创建一个50个字符的字符串。
num_epochs, num_steps, batch_size, lr, clipping_theta = 100, 35, 32, 1, 1
prefixes = ['traveller', 'time traveller']
train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens,
corpus_indices, vocab, device, False, num_epochs,
num_steps, lr, clipping_theta, batch_size, prefixes)
epoch 50, perplexity 11.929022, time 449.93 sec
epoch 100, perplexity 9.153454, time 436.90 sec
- travellere the the the the the the the the the the the the
- time travellere the the the the the the the the the the the the
六、简洁的实现
在nn模块中,我们可以直接调用𝐺𝑅𝑈模块中的𝑟n𝑛类。这封装了在上面明确提出的所有配置细节。代码的速度明显加快,因为它使用了编译后的操作符,而不是之前详细说明的Python的许多细节。
gru_layer = nn.GRU(input_size=num_inputs, hidden_size=num_hiddens)
model = RNNModel(gru_layer, num_hiddens, len(vocab))
model.to(device)
train_and_predict_rnn_nn(model, num_hiddens, init_gru_state, corpus_indices, vocab,
device, num_epochs*5, num_steps, lr,
clipping_theta, batch_size, prefixes)
七、摘要
1、对于时间步长的时间序列,门控递归神经网络能更好地捕捉到依赖关系。
2、重置门有助于捕捉时间序列中的短期依赖关系。
3、更新门有助于捕捉时间序列中的长期依赖关系。
4、只要重置门打开,GRU就包含基本的RNNs作为其极端情况。它们可以根据需要忽略序列。
八、练习
1、比较rnn.RNN和rnn.GRU实现的运行时间、困惑度和提取的字符串。
2、假设我们只想用时间步骤t′的输入来预测时间步骤t>t′的输出。每个时间步骤的复位和更新门的最佳值是什么?
3、调整超参数,观察和分析对运行时间、困惑度和写出的歌词的影响。
4、如果你只实现GRU的一部分,会发生什么?也就是说,实现一个只有一个复位门的递归单元。同样地,实现一个只有更新门的循环单元。
|