6.7. 门控循环单元(GRU)

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

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

6.7.1. 门控循环单元

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

6.7.1.1. 重置门和更新门

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

6.7. 门控循环单元(GRU) - 图1 与上一时间步隐藏状态 6.7. 门控循环单元(GRU) - 图2 ,输出由激活函数为sigmoid函数的全连接层计算得到。

门控循环单元中重置门和更新门的计算 图 6.4 门控循环单元中重置门和更新门的计算

具体来说,假设隐藏单元个数为

6.7. 门控循环单元(GRU) - 图4 ,给定时间步 6.7. 门控循环单元(GRU) - 图5 的小批量输入 6.7. 门控循环单元(GRU) - 图6 (样本数为 6.7. 门控循环单元(GRU) - 图7 ,输入个数为 6.7. 门控循环单元(GRU) - 图8 )和上一时间步隐藏状态 6.7. 门控循环单元(GRU) - 图9 。重置门 6.7. 门控循环单元(GRU) - 图10 和更新门 6.7. 门控循环单元(GRU) - 图11 的计算如下:

6.7. 门控循环单元(GRU) - 图12

其中

6.7. 门控循环单元(GRU) - 图136.7. 门控循环单元(GRU) - 图14 是权重参数, 6.7. 门控循环单元(GRU) - 图15 是偏差参数。“多层感知机”一节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门 6.7. 门控循环单元(GRU) - 图16 和更新门 6.7. 门控循环单元(GRU) - 图17 中每个元素的值域都是 6.7. 门控循环单元(GRU) - 图18

6.7.1.2. 候选隐藏状态

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

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

门控循环单元中候选隐藏状态的计算。这里的\ :math:`\odot`\ 是按元素乘法 图 6.5 门控循环单元中候选隐藏状态的计算。这里的

6.7. 门控循环单元(GRU) - 图22 是按元素乘法

具体来说,时间步

6.7. 门控循环单元(GRU) - 图23 的候选隐藏状态 6.7. 门控循环单元(GRU) - 图24 的计算为

6.7. 门控循环单元(GRU) - 图25

其中

6.7. 门控循环单元(GRU) - 图266.7. 门控循环单元(GRU) - 图27 是权重参数, 6.7. 门控循环单元(GRU) - 图28 是偏差参数。从上面这个公式可以看出,重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。

6.7.1.3. 隐藏状态

最后,时间步

6.7. 门控循环单元(GRU) - 图29 的隐藏状态 6.7. 门控循环单元(GRU) - 图30 的计算使用当前时间步的更新门 6.7. 门控循环单元(GRU) - 图31 来对上一时间步的隐藏状态 6.7. 门控循环单元(GRU) - 图32 和当前时间步的候选隐藏状态 6.7. 门控循环单元(GRU) - 图33 做组合:

6.7. 门控循环单元(GRU) - 图34

门控循环单元中隐藏状态的计算。这里的\ :math:`\odot`\ 是按元素乘法 图 6.6 门控循环单元中隐藏状态的计算。这里的

6.7. 门控循环单元(GRU) - 图36 是按元素乘法

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

6.7. 门控循环单元(GRU) - 图376.7. 门控循环单元(GRU) - 图386.7. 门控循环单元(GRU) - 图39 )之间一直近似1。那么,在时间步 6.7. 门控循环单元(GRU) - 图406.7. 门控循环单元(GRU) - 图41 之间的输入信息几乎没有流入时间步 6.7. 门控循环单元(GRU) - 图42 的隐藏状态 6.7. 门控循环单元(GRU) - 图43 。实际上,这可以看作是较早时刻的隐藏状态 6.7. 门控循环单元(GRU) - 图44 一直通过时间保存并传递至当前时间步 6.7. 门控循环单元(GRU) - 图45 。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

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

  • 重置门有助于捕捉时间序列里短期的依赖关系;
  • 更新门有助于捕捉时间序列里长期的依赖关系。

6.7.2. 读取数据集

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

  1. In [1]:
  1. import d2lzh as d2l
  2. from mxnet import nd
  3. from mxnet.gluon import rnn
  4.  
  5. (corpus_indices, char_to_idx, idx_to_char,
  6. vocab_size) = d2l.load_data_jay_lyrics()

6.7.3. 从零开始实现

我们先介绍如何从零开始实现门控循环单元。

6.7.3.1. 初始化模型参数

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

  1. In [2]:
  1. num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
  2. ctx = d2l.try_gpu()
  3.  
  4. def get_params():
  5. def _one(shape):
  6. return nd.random.normal(scale=0.01, shape=shape, ctx=ctx)
  7.  
  8. def _three():
  9. return (_one((num_inputs, num_hiddens)),
  10. _one((num_hiddens, num_hiddens)),
  11. nd.zeros(num_hiddens, ctx=ctx))
  12.  
  13. W_xz, W_hz, b_z = _three() # 更新门参数
  14. W_xr, W_hr, b_r = _three() # 重置门参数
  15. W_xh, W_hh, b_h = _three() # 候选隐藏状态参数
  16. # 输出层参数
  17. W_hq = _one((num_hiddens, num_outputs))
  18. b_q = nd.zeros(num_outputs, ctx=ctx)
  19. # 附上梯度
  20. params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
  21. for param in params:
  22. param.attach_grad()
  23. return params

6.7.3.2. 定义模型

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

  1. In [3]:
  1. def init_gru_state(batch_size, num_hiddens, ctx):
  2. return (nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx), )

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

  1. In [4]:
  1. def gru(inputs, state, params):
  2. W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
  3. H, = state
  4. outputs = []
  5. for X in inputs:
  6. Z = nd.sigmoid(nd.dot(X, W_xz) + nd.dot(H, W_hz) + b_z)
  7. R = nd.sigmoid(nd.dot(X, W_xr) + nd.dot(H, W_hr) + b_r)
  8. H_tilda = nd.tanh(nd.dot(X, W_xh) + nd.dot(R * H, W_hh) + b_h)
  9. H = Z * H + (1 - Z) * H_tilda
  10. Y = nd.dot(H, W_hq) + b_q
  11. outputs.append(Y)
  12. return outputs, (H,)

6.7.3.3. 训练模型并创作歌词

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

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

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

  1. In [6]:
  1. d2l.train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens,
  2. vocab_size, ctx, corpus_indices, idx_to_char,
  3. char_to_idx, False, num_epochs, num_steps, lr,
  4. clipping_theta, batch_size, pred_period, pred_len,
  5. prefixes)
  1. epoch 40, perplexity 151.879138, time 0.62 sec
  2. - 分开 我想你的让我 你不你的让我 你想你的让我 你不你的让我 你想你的让我 你不你的让我 你想你的让我
  3. - 不分开 我想你的让我 你不你的可爱 我想你的让我 你不你的可爱 我想你的让我 你不你的可爱 我想你的让我
  4. epoch 80, perplexity 32.884267, time 0.62 sec
  5. - 分开 我想要你的微笑 一定好人 我想就这样的怒笑 我想想你的爱写 让我想这样 你不再再想 我不要再想
  6. - 不分开 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想
  7. epoch 120, perplexity 5.959521, time 0.62 sec
  8. - 分开 我想要这样牵着你的手不放开 爱可不可以简简单单没有伤害 靠着我的肩膀 在我胸口睡著 一壶好
  9. - 不分开 不要再这样打我妈妈 我说的话你甘会默 不要再这样打我妈妈 难道你手不会痛吗 我想你这样牵着一起
  10. epoch 160, perplexity 1.769119, time 0.62 sec
  11. - 分开 我想要这样牵着你 别想躲 说你眼睛看着我 别发抖 快给我抬起头 有话去对医药箱说 别怪我 别怪我
  12. - 不分开远远封 但后来到 你怎么我不活 有话去对医药箱说 别怪我 别怪我 说你怎么面对我 甩开球我满腔的怒火

6.7.4. 简洁实现

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

  1. In [7]:
  1. gru_layer = rnn.GRU(num_hiddens)
  2. model = d2l.RNNModel(gru_layer, vocab_size)
  3. d2l.train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,
  4. corpus_indices, idx_to_char, char_to_idx,
  5. num_epochs, num_steps, lr, clipping_theta,
  6. batch_size, pred_period, pred_len, prefixes)
  1. epoch 40, perplexity 154.878442, time 0.04 sec
  2. - 分开 我不的让我爱爱女人 坏坏的让我爱爱女女人 坏坏的让我爱爱女女人 坏坏的让我爱爱女女人 坏坏的让我爱
  3. - 不分开 我想你的让我爱爱女女人 坏坏的让我爱爱女女人 坏坏的让我爱爱女女人 坏坏的让我爱爱女女人 坏坏的让
  4. epoch 80, perplexity 32.361461, time 0.04 sec
  5. - 分开 我想要这样 我不要再想 我不要再想 我不要这样 我不要再想 我不要再想 我不要这样 我不要再想
  6. - 不分开 爱爱女人 你果我不想 我不要再想 我不要再想 我不要这样 我不要再想 我不要再想 我不要这样
  7. epoch 120, perplexity 4.719229, time 0.04 sec
  8. - 分开 我想就这样牵着你的手不放开 爱可不可以简简单单没有伤害 靠着我的肩膀 在我胸口睡著 像这样
  9. - 不分开 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生活
  10. epoch 160, perplexity 1.421567, time 0.04 sec
  11. - 分开 想带你骑单车 想和你看棒球 想这样没担忧 唱着歌 一直走 我想就这样牵着你的手不放开 爱可
  12. - 不分开 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生活

6.7.5. 小结

  • 门控循环神经网络可以更好地捕捉时间序列中时间步距离较大的依赖关系。
  • 门控循环单元引入了门的概念,从而修改了循环神经网络中隐藏状态的计算方式。它包括重置门、更新门、候选隐藏状态和隐藏状态。
  • 重置门有助于捕捉时间序列里短期的依赖关系。
  • 更新门有助于捕捉时间序列里长期的依赖关系。

6.7.6. 练习

  • 假设时间步 6.7. 门控循环单元(GRU) - 图46 。如果只希望用时间步 6.7. 门控循环单元(GRU) - 图47 的输入来预测时间步 6.7. 门控循环单元(GRU) - 图48 的输出,每个时间步的重置门和更新门的理想的值是多少?
  • 调节超参数,观察并分析对运行时间、困惑度以及创作歌词的结果造成的影响。
  • 在相同条件下,比较门控循环单元和不带门控的循环神经网络的运行时间。

6.7.7. 参考文献

[1] Cho, K., Van Merriënboer, B., Bahdanau, D., & Bengio, Y. (2014). Onthe properties of neural machine translation: Encoder-decoderapproaches. arXiv preprint arXiv:1409.1259.

[2] Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empiricalevaluation of gated recurrent neural networks on sequence modeling.arXiv preprint arXiv:1412.3555.