动手学PyTorch | (34) 门控循环单元(GRU)

一世执手 提交于 2019-11-30 01:45:18

上⼀节介绍了循环神经网络中的梯度计算方法。我们发现,当时间步数T较大或者当前时间步t较小时,循环神经⽹络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但⽆法解决梯度衰减的问题。 通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。

⻔控循环神经网络(gated recurrent neural network)的提出,正是为了更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可以学习的⻔来控制信息的流动。其中,⻔控循环单元(gated recurrent unit,GRU)是一种常⽤的⻔控循环神经⽹络。另一种常⽤的⻔控循环神经网络则将在下一节中介绍。

目录

1. 门控循环单元

2. 读取数据集

3. 从0开始实现

4. 简洁实现

5.小结


1. 门控循环单元

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

  • 重置门和更新门

如下图所示,⻔控循环单元中的重􏰀置门和更新门的输⼊均为当前时间步输入

与上一时间步隐藏状态

,输出由激活函数为sigmoid函数的全连接层计算得到。

具体来说,假设隐藏单元个数为h,给定时间步t的小批量输入

(样本数为n(batch_size),输入个数为d)和上一时间步隐藏状态

,重置门

和更新门

的计算如下:

其中

是权重参数,

是偏差参数。多层感知机中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,􏰀重置门

和更新门

中每个元素的值域都是[0,1].
  • 候选隐藏状态

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

)。如果􏰀重置门中元素值接近0,那么意味着重􏰀置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为[-1,1]:

具体来说,时间步t的候选隐藏状态

的计算为:

其中

是权重参数,

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

最后,时间步t的隐藏状态

的计算使用当前时间步的更新门

来对上一时间步的隐藏状态

和当前时间步的候选隐藏状态

做组合:

值得注意的是,更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如上图所示。假设更新门在时间步

到t(

<t)之间一直近似1.那么,在时间步t'到t之间的输入信息⼏乎没有流入时间步t的隐藏状态

.实际上,这可以看作是较早时刻的隐藏状态

⼀直通过时间保存并传递⾄当前时间步t.这个设计可以应对循环神经⽹络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

我们对门控循环单元的设计稍作总结:

1)重置⻔有助于捕捉时间序列里短期的依赖关系;

2)更新⻔有助于捕捉时间序列里长期的依赖关系。

 

2. 读取数据集

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

 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() print(torch.__version__, device)

 

3. 从0开始实现

  • 初始化模型参数
 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])
  • 定义模型

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

 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 #state是元组     outputs = []     for X in inputs: #(num_steps,batch_size,input_size)         #更新门         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) + R * torch.matmul(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,)
  • 训练模型并创作歌词

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

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

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

 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类即可。

 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)

 

5.小结

1)⻔控循环神经网络可以更好地捕捉时间序列中时间步距离较大的依赖关系。

2)⻔控循环单元引⼊了⻔的概念,从⽽修改了循环神经网络中隐藏状态的计算⽅式。它包括􏰀重置门、更新门、候选隐藏状态和隐藏状态。

3)􏰀重置门有助于捕捉时间序列里短期的依赖关系。

4)更新⻔有助于捕捉时间序列里长期的依赖关系。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!