Skip to content

Latest commit

 

History

History
214 lines (160 loc) · 12.8 KB

File metadata and controls

214 lines (160 loc) · 12.8 KB

6.7 门控循环单元(GRU)

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

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

6.7.1 门控循环单元

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

6.7.1.1 重置门和更新门

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

图6.4 门控循环单元中重置门和更新门的计算
具体来说,假设隐藏单元个数为$h$,给定时间步$t$的小批量输入$\boldsymbol{X}_t \in \mathbb{R}^{n \times d}$(样本数为$n$,输入个数为$d$)和上一时间步隐藏状态$\boldsymbol{H}_{t-1} \in \mathbb{R}^{n \times h}$。重置门$\boldsymbol{R}_t \in \mathbb{R}^{n \times h}$和更新门$\boldsymbol{Z}_t \in \mathbb{R}^{n \times h}$的计算如下:

$$ \begin{aligned} \boldsymbol{R}t = \sigma(\boldsymbol{X}t \boldsymbol{W}{xr} + \boldsymbol{H}{t-1} \boldsymbol{W}_{hr} + \boldsymbol{b}r),\ \boldsymbol{Z}t = \sigma(\boldsymbol{X}t \boldsymbol{W}{xz} + \boldsymbol{H}{t-1} \boldsymbol{W}{hz} + \boldsymbol{b}_z), \end{aligned} $$

其中$\boldsymbol{W}{xr}, \boldsymbol{W}{xz} \in \mathbb{R}^{d \times h}$和$\boldsymbol{W}{hr}, \boldsymbol{W}{hz} \in \mathbb{R}^{h \times h}$是权重参数,$\boldsymbol{b}_r, \boldsymbol{b}_z \in \mathbb{R}^{1 \times h}$是偏差参数。3.8节(多层感知机)节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门$\boldsymbol{R}_t$和更新门$\boldsymbol{Z}_t$中每个元素的值域都是$[0, 1]$。

6.7.1.2 候选隐藏状态

接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所示,我们将当前时间步重置门的输出与上一时间步隐藏状态做按元素乘法(符号为$\odot$)。如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为$[-1, 1]$。

图6.5 门控循环单元中候选隐藏状态的计算
具体来说,时间步$t$的候选隐藏状态$\tilde{\boldsymbol{H}}_t \in \mathbb{R}^{n \times h}$的计算为

$$\tilde{\boldsymbol{H}}t = \text{tanh}(\boldsymbol{X}t \boldsymbol{W}{xh} + \left(\boldsymbol{R}t \odot \boldsymbol{H}{t-1}\right) \boldsymbol{W}{hh} + \boldsymbol{b}_h),$$

其中$\boldsymbol{W}{xh} \in \mathbb{R}^{d \times h}$和$\boldsymbol{W}{hh} \in \mathbb{R}^{h \times h}$是权重参数,$\boldsymbol{b}_h \in \mathbb{R}^{1 \times h}$是偏差参数。从上面这个公式可以看出,重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。

6.7.1.3 隐藏状态

最后,时间步$t$的隐藏状态$\boldsymbol{H}_t \in \mathbb{R}^{n \times h}$的计算使用当前时间步的更新门$\boldsymbol{Z}t$来对上一时间步的隐藏状态$\boldsymbol{H}{t-1}$和当前时间步的候选隐藏状态$\tilde{\boldsymbol{H}}_t$做组合:

$$\boldsymbol{H}_t = \boldsymbol{Z}t \odot \boldsymbol{H}{t-1} + (1 - \boldsymbol{Z}_t) \odot \tilde{\boldsymbol{H}}_t.$$

图6.6 门控循环单元中隐藏状态的计算
值得注意的是,更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如图6.6所示。假设更新门在时间步$t'$到$t$($t' < t$)之间一直近似1。那么,在时间步$t'$到$t$之间的输入信息几乎没有流入时间步$t$的隐藏状态$\boldsymbol{H}_t$。实际上,这可以看作是较早时刻的隐藏状态$\boldsymbol{H}_{t'-1}$一直通过时间保存并传递至当前时间步$t$。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

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

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

6.7.2 读取数据集

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

import tensorflow as tf
from tensorflow import keras
import time
import math
import numpy as np
import sys
sys.path.append("..") 
import d2lzh_tensorflow2 as d2l
(corpus_indices, char_to_idx, idx_to_char,vocab_size) = d2l.load_data_jay_lyrics()

6.7.3 从零开始实现

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

6.7.3.1 初始化模型参数

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

num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size

def get_params():
    def _one(shape):
        return tf.Variable(tf.random.normal(shape=shape,stddev=0.01,mean=0,dtype=tf.float32))

    def _three():
        return (_one((num_inputs, num_hiddens)),
                _one((num_hiddens, num_hiddens)),
                tf.Variable(tf.zeros(num_hiddens), dtype=tf.float32))

    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 = tf.Variable(tf.zeros(num_outputs), dtype=tf.float32)
    # 附上梯度
    params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]

    return params

6.7.3.2 定义模型

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

def init_gru_state(batch_size, num_hiddens):
    return (tf.zeros(shape=(batch_size, num_hiddens)), )

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

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:
        X=tf.reshape(X,[-1,W_xh.shape[0]])
        Z = tf.sigmoid(tf.matmul(X, W_xz) + tf.matmul(H, W_hz) + b_z)
        R = tf.sigmoid(tf.matmul(X, W_xr) + tf.matmul(H, W_hr) + b_r)
        H_tilda = tf.tanh(tf.matmul(X, W_xh) + tf.matmul(R * H, W_hh) + b_h)
        H = Z * H + (1 - Z) * H_tilda
        Y = tf.matmul(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H,)

6.7.3.3 训练模型并创作歌词

我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为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, corpus_indices, idx_to_char,
                          char_to_idx, False, num_epochs, num_steps, lr,
                          clipping_theta, batch_size, pred_period, pred_len,
                          prefixes)

输出:

 - 分开的的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的
 - 不分开的的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的妈的
epoch 80, perplexity 123.106788, time 4.11 sec
 - 分开的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的
 - 不分开的的的找找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找的的找
epoch 120, perplexity 52.382330, time 4.08 sec
 - 分开的 如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的
 - 不分开的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的的的如如的
epoch 160, perplexity 1046.588638, time 4.02 sec
 - 分开暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴
 - 不分开暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴暴

6.7.4 简洁实现

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

gru_layer = keras.layers.GRU(num_hiddens,time_major=True,return_sequences=True,return_state=True)
model = d2l.RNNModel(gru_layer, vocab_size)
d2l.train_and_predict_rnn_keras(model, num_hiddens, vocab_size,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes)

输出:

epoch 40, perplexity 141.880921, time 3.01 sec
 - 分开  说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 
 - 不分开  说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 说 
epoch 80, perplexity 31.483826, time 2.88 sec
 - 分开                                                  
 - 不分开                                                  
epoch 120, perplexity 24.846631, time 2.94 sec
 - 分开我 我说你的模我 我说你的模我 我说你的模我 我说你的模我 我说你的模我 我说你的模我 我说你的模我
 - 不分开我爱女人可见的模样打我 我说你的模我 我说你的模我 我说你的模我 我说你的模我 我说你的模我 我说你
epoch 160, perplexity 165.971063, time 2.91 sec
 - 分开 还在祭糗蕃还我骷髅头像童话不达米亚平原几斤像养养 还在糗较翻还我骷髅头像童话不达米亚平原几斤像养养
 - 不分开 一切限抄蜘蛛 还在糗杂草还我骷髅头像童话不达米亚平原几斤像养养 还在糗较翻还我骷髅头像童话不达米亚

小结

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

参考文献

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

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


注:除代码外本节与原书此节基本相同,原书传送门