10.3. 深度循环神经网络
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

到目前为止,我们专注于定义由序列输入、单个循环神经网络隐藏层和输出层组成的网络。尽管在任何时间步的输入和相应的输出之间只有一个隐藏层,但从某种意义上说,这些网络是“深”的。第一个时间步的输入可以影响最后一个时间步 \(T\)(通常是100或1000个步之后)的输出。这些输入在到达最终输出之前,会经过 \(T\) 次循环层的应用。然而,我们也常常希望保留表达在给定时间步的输入与同一时间步的输出之间复杂关系的能力。因此,我们经常构建的循环神经网络不仅在时间方向上是深的,在输入到输出的方向上也是深的。这正是我们在开发多层感知机和深度卷积神经网络时已经遇到过的“深度”概念。

构建这类深度循环神经网络的标准方法非常简单:我们将多个循环神经网络堆叠在一起。给定一个长度为 \(T\) 的序列,第一个循环神经网络产生一个同样长度为 \(T\) 的输出序列。这些输出又构成了下一个循环神经网络层的输入。在这个简短的章节中,我们将说明这种设计模式,并提供一个简单的例子来展示如何编写这样的堆叠循环神经网络。在下面的 图 10.3.1 中,我们展示了一个具有 \(L\) 个隐藏层的深度循环神经网络。每个隐藏状态都对一个序列输入进行操作,并产生一个序列输出。此外,每个时间步的任何循环神经网络单元(图 10.3.1 中的白框)都依赖于同一层前一个时间步的值和前一层同一时间步的值。

../_images/deep-rnn.svg

图 10.3.1 深度循环神经网络的架构。

形式上,假设我们在时间步 \(t\) 有一个小批量输入 \(\mathbf{X}_t \in \mathbb{R}^{n \times d}\)(样本数 \(=n\);每个样本中的输入数 \(=d\))。在同一时间步,令第 \(l\) 个隐藏层(\(l=1,\ldots,L\))的隐藏状态为 \(\mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h}\)(隐藏单元数 \(=h\)),输出层变量为 \(\mathbf{O}_t \in \mathbb{R}^{n \times q}\)(输出数:\(q\))。设 \(\mathbf{H}_t^{(0)} = \mathbf{X}_t\),使用激活函数 \(\phi_l\) 的第 \(l\) 个隐藏层的隐藏状态计算如下:

(10.3.1)\[\mathbf{H}_t^{(l)} = \phi_l(\mathbf{H}_t^{(l-1)} \mathbf{W}_{\textrm{xh}}^{(l)} + \mathbf{H}_{t-1}^{(l)} \mathbf{W}_{\textrm{hh}}^{(l)} + \mathbf{b}_\textrm{h}^{(l)}),\]

其中,权重 \(\mathbf{W}_{\textrm{xh}}^{(l)} \in \mathbb{R}^{h \times h}\)\(\mathbf{W}_{\textrm{hh}}^{(l)} \in \mathbb{R}^{h \times h}\),以及偏置 \(\mathbf{b}_\textrm{h}^{(l)} \in \mathbb{R}^{1 \times h}\),是第 \(l\) 个隐藏层的模型参数。

最后,输出层的计算仅基于最后一个 \(L\) 层的隐藏状态:

(10.3.2)\[\mathbf{O}_t = \mathbf{H}_t^{(L)} \mathbf{W}_{\textrm{hq}} + \mathbf{b}_\textrm{q},\]

其中权重 \(\mathbf{W}_{\textrm{hq}} \in \mathbb{R}^{h \times q}\) 和偏置 \(\mathbf{b}_\textrm{q} \in \mathbb{R}^{1 \times q}\) 是输出层的模型参数。

与多层感知机一样,隐藏层数 \(L\) 和隐藏单元数 \(h\) 是我们可以调整的超参数。常见的循环神经网络层宽度(\(h\))在 \((64, 2056)\) 范围内,常见的深度(\(L\))在 \((1, 8)\) 范围内。此外,我们可以通过将 (10.3.1) 中的隐藏状态计算替换为 LSTM 或 GRU 的计算,轻松地得到一个深度门控循环神经网络。

import torch
from torch import nn
from d2l import torch as d2l
from mxnet import np, npx
from mxnet.gluon import rnn
from d2l import mxnet as d2l

npx.set_np()
import jax
from flax import linen as nn
from jax import numpy as jnp
from d2l import jax as d2l
import tensorflow as tf
from d2l import tensorflow as d2l

10.3.1. 从零开始实现

为了从零开始实现一个多层循环神经网络,我们可以将每个层视为一个具有自身可学习参数的 RNNScratch 实例。

class StackedRNNScratch(d2l.Module):
    def __init__(self, num_inputs, num_hiddens, num_layers, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.rnns = nn.Sequential(*[d2l.RNNScratch(
            num_inputs if i==0 else num_hiddens, num_hiddens, sigma)
                                    for i in range(num_layers)])
class StackedRNNScratch(d2l.Module):
    def __init__(self, num_inputs, num_hiddens, num_layers, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.rnns = [d2l.RNNScratch(num_inputs if i==0 else num_hiddens,
                                    num_hiddens, sigma)
                     for i in range(num_layers)]
class StackedRNNScratch(d2l.Module):
    num_inputs: int
    num_hiddens: int
    num_layers: int
    sigma: float = 0.01

    def setup(self):
        self.rnns = [d2l.RNNScratch(self.num_inputs if i==0 else self.num_hiddens,
                                    self.num_hiddens, self.sigma)
                     for i in range(self.num_layers)]
class StackedRNNScratch(d2l.Module):
    def __init__(self, num_inputs, num_hiddens, num_layers, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.rnns = [d2l.RNNScratch(num_inputs if i==0 else num_hiddens,
                                    num_hiddens, sigma)
                     for i in range(num_layers)]

多层前向计算只是逐层执行前向计算。

@d2l.add_to_class(StackedRNNScratch)
def forward(self, inputs, Hs=None):
    outputs = inputs
    if Hs is None: Hs = [None] * self.num_layers
    for i in range(self.num_layers):
        outputs, Hs[i] = self.rnns[i](outputs, Hs[i])
        outputs = torch.stack(outputs, 0)
    return outputs, Hs
@d2l.add_to_class(StackedRNNScratch)
def forward(self, inputs, Hs=None):
    outputs = inputs
    if Hs is None: Hs = [None] * self.num_layers
    for i in range(self.num_layers):
        outputs, Hs[i] = self.rnns[i](outputs, Hs[i])
        outputs = np.stack(outputs, 0)
    return outputs, Hs
@d2l.add_to_class(StackedRNNScratch)
def forward(self, inputs, Hs=None):
    outputs = inputs
    if Hs is None: Hs = [None] * self.num_layers
    for i in range(self.num_layers):
        outputs, Hs[i] = self.rnns[i](outputs, Hs[i])
        outputs = jnp.stack(outputs, 0)
    return outputs, Hs
@d2l.add_to_class(StackedRNNScratch)
def forward(self, inputs, Hs=None):
    outputs = inputs
    if Hs is None: Hs = [None] * self.num_layers
    for i in range(self.num_layers):
        outputs, Hs[i] = self.rnns[i](outputs, Hs[i])
        outputs = tf.stack(outputs, 0)
    return outputs, Hs

作为一个例子,我们在*时间机器*数据集上训练一个深度 GRU 模型(与 9.5节 中相同)。为简单起见,我们将层数设置为 2。

data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                              num_hiddens=32, num_layers=2)
model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_48_0.svg
data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                              num_hiddens=32, num_layers=2)
model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_51_0.svg
data = d2l.TimeMachine(batch_size=1024, num_steps=32)
rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                              num_hiddens=32, num_layers=2)
model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_54_0.svg
data = d2l.TimeMachine(batch_size=1024, num_steps=32)
with d2l.try_gpu():
    rnn_block = StackedRNNScratch(num_inputs=len(data.vocab),
                              num_hiddens=32, num_layers=2)
    model = d2l.RNNLMScratch(rnn_block, vocab_size=len(data.vocab), lr=2)
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_57_0.svg

10.3.2. 简洁实现

幸运的是,实现多层循环神经网络所需的许多逻辑细节在高级 API 中已经准备就绪。我们的简洁实现将使用这些内置功能。代码泛化了我们之前在 10.2节 中使用的代码,让我们能够明确指定层数,而不是选择默认的单层。

class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_inputs, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        self.rnn = nn.GRU(num_inputs, num_hiddens, num_layers,
                          dropout=dropout)

幸运的是,实现多层循环神经网络所需的许多逻辑细节在高级 API 中已经准备就绪。我们的简洁实现将使用这些内置功能。代码泛化了我们之前在 10.2节 中使用的代码,让我们能够明确指定层数,而不是选择默认的单层。

class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout)

Flax 在实现循环神经网络时采用了一种极简主义的方法。定义循环神经网络的层数或将其与 dropout 结合的功能并不是开箱即用的。我们的简洁实现将使用所有内置功能,并在其上添加 num_layersdropout 特性。代码泛化了我们之前在 10.2节 中使用的代码,允许明确指定层数,而不是选择默认的单层。

class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    num_hiddens: int
    num_layers: int
    dropout: float = 0

    @nn.compact
    def __call__(self, X, state=None, training=False):
        outputs = X
        new_state = []
        if state is None:
            batch_size = X.shape[1]
            state = [nn.GRUCell.initialize_carry(jax.random.PRNGKey(0),
                    (batch_size,), self.num_hiddens)] * self.num_layers

        GRU = nn.scan(nn.GRUCell, variable_broadcast="params",
                      in_axes=0, out_axes=0, split_rngs={"params": False})

        # Introduce a dropout layer after every GRU layer except last
        for i in range(self.num_layers - 1):
            layer_i_state, X = GRU()(state[i], outputs)
            new_state.append(layer_i_state)
            X = nn.Dropout(self.dropout, deterministic=not training)(X)

        # Final GRU layer without dropout
        out_state, X = GRU()(state[-1], X)
        new_state.append(out_state)
        return X, jnp.array(new_state)

幸运的是,实现多层循环神经网络所需的许多逻辑细节在高级 API 中已经准备就绪。我们的简洁实现将使用这些内置功能。代码泛化了我们之前在 10.2节 中使用的代码,让我们能够明确指定层数,而不是选择默认的单层。

class GRU(d2l.RNN):  #@save
    """The multilayer GRU model."""
    def __init__(self, num_hiddens, num_layers, dropout=0):
        d2l.Module.__init__(self)
        self.save_hyperparameters()
        gru_cells = [tf.keras.layers.GRUCell(num_hiddens, dropout=dropout)
                     for _ in range(num_layers)]
        self.rnn = tf.keras.layers.RNN(gru_cells, return_sequences=True,
                                       return_state=True, time_major=True)

    def forward(self, X, state=None):
        outputs, *state = self.rnn(X, state)
        return outputs, state

选择超参数等架构决策与 10.2节 中的非常相似。我们选择与不同词元数量相同的输入和输出数,即 vocab_size。隐藏单元数仍然是32。唯一的区别是,我们现在通过指定 num_layers 的值来选择一个非平凡的隐藏层数。

gru = GRU(num_inputs=len(data.vocab), num_hiddens=32, num_layers=2)
model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_82_0.svg
model.predict('it has', 20, data.vocab, d2l.try_gpu())
'it has for and the time th'
gru = GRU(num_hiddens=32, num_layers=2)
model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)

# Running takes > 1h (pending fix from MXNet)
# trainer.fit(model, data)
# model.predict('it has', 20, data.vocab, d2l.try_gpu())
gru = GRU(num_hiddens=32, num_layers=2)
model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_89_0.svg
model.predict('it has', 20, data.vocab, trainer.state.params)
'it has is the prough said '
gru = GRU(num_hiddens=32, num_layers=2)
with d2l.try_gpu():
    model = d2l.RNNLM(gru, vocab_size=len(data.vocab), lr=2)
trainer.fit(model, data)
../_images/output_deep-rnn_d70a11_93_0.svg
model.predict('it has', 20, data.vocab)
'it has the time traveller '

10.3.3. 小结

在深度循环神经网络中,隐藏状态信息被传递到当前层的下一个时间步和下一层的当前时间步。存在许多不同类型的深度循环神经网络,如LSTMs、GRUs或普通循环神经网络。方便的是,这些模型都作为深度学习框架高级API的一部分提供。模型的初始化需要小心。总的来说,深度循环神经网络需要大量的工作(如学习率和裁剪)来确保适当的收敛。

10.3.4. 练习

  1. 用LSTM替换GRU,并比较准确性和训练速度。

  2. 增加训练数据以包含多本书。你能将困惑度降低到什么程度?

  3. 在建模文本时,你是否愿意组合不同作者的来源?为什么这是一个好主意?可能会出现什么问题?