Run this notebook online:Binder or Colab: Colab

9.3. 深度循环神经网络

到目前为止,我们只讨论了具有一个单向隐藏层的循环神经网络。 其中,隐变量和观测值与具体的函数形式的交互方式是相当随意的。 只要交互类型建模具有足够的灵活性,这就不是一个大问题。 然而,对于一个单层来说,这可能具有相当的挑战性。 之前在线性模型中,我们通过添加更多的层来解决这个问题。 而在循环神经网络中,我们首先需要确定如何添加更多的层, 以及在哪里添加额外的非线性,因此这个问题有点棘手。

事实上,我们可以将多层循环神经网络堆叠在一起, 通过对几个简单层的组合,产生了一个灵活的机制。 特别是,数据可能与不同层的堆叠有关。 例如,我们可能希望保持有关金融市场状况 (熊市或牛市)的宏观数据可用, 而微观数据只记录较短期的时间动态。

fig_deep_rnn描述了一个具有\(L\)个隐藏层的深度循环神经网络, 每个隐状态都连续地传递到当前层的下一个时间步和下一层的当前时间步。

深度循环神经网络结构 .. _fig_deep_rnn:

9.3.1. 函数依赖关系

我们可以将深度架构中的函数依赖关系形式化, 这个架构是由 fig_deep_rnn中描述了\(L\)个隐藏层构成。 后续的讨论主要集中在经典的循环神经网络模型上, 但是这些讨论也适应于其他序列模型。

假设在时间步\(t\)有一个小批量的输入数据 \(\mathbf{X}_t \in \mathbb{R}^{n \times d}\) (样本数:\(n\),每个样本中的输入数:\(d\))。 同时,将\(l^\mathrm{th}\)隐藏层(\(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\), 第\(l\)个隐藏层的隐状态使用激活函数\(\phi_l\),则:

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

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

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

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

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

与多层感知机一样,隐藏层数目\(L\)和隐藏单元数目\(h\)都是超参数。 也就是说,它们可以由我们调整的。 另外,用门控循环单元或长短期记忆网络的隐状态 来代替 (9.3.1)中的隐状态进行计算, 可以很容易地得到深度门控循环神经网络或深度长短期记忆神经网络。

9.3.2. 简洁实现

实现多层循环神经网络所需的许多逻辑细节在高级API中都是现成的。 简单起见,我们仅示范使用此类内置函数的实现方式。 以长短期记忆网络模型为例, 该代码与之前在 Section 9.2中使用的代码非常相似, 实际上唯一的区别是我们指定了层的数量, 而不是使用单一层这个默认值。 像往常一样,我们从加载数据集开始。

%load ../utils/djl-imports
%load ../utils/plot-utils
%load ../utils/Functions.java
%load ../utils/PlotUtils.java

%load ../utils/StopWatch.java
%load ../utils/Accumulator.java
%load ../utils/Animator.java
%load ../utils/Training.java
%load ../utils/timemachine/Vocab.java
%load ../utils/timemachine/RNNModel.java
%load ../utils/timemachine/RNNModelScratch.java
%load ../utils/timemachine/TimeMachine.java
%load ../utils/timemachine/TimeMachineDataset.java
NDManager manager = NDManager.newBaseManager();
int batchSize = 32;
int numSteps = 35;

TimeMachineDataset dataset = new TimeMachineDataset.Builder()
        .setManager(manager)
        .setMaxTokens(10000)
        .setSampling(batchSize, false)
        .setSteps(numSteps)
        .build();
dataset.prepare();
Vocab vocab = dataset.getVocab();

像选择超参数这类架构决策也跟 Section 9.2中的决策非常相似。 因为我们有不同的词元,所以输入和输出都选择相同数量,即vocabSize。 隐藏单元的数量仍然是\(256\)。 唯一的区别是,我们现在通过setNumLayers()的值来设定隐藏层数。

int vocabSize = vocab.length();
int numHiddens = 256;
int numLayers = 2;
Device device = manager.getDevice();
LSTM lstmLayer =
        LSTM.builder()
                .setNumLayers(numLayers)
                .setStateSize(numHiddens)
                .optReturnState(true)
                .optBatchFirst(false)
                .build();

RNNModel model = new RNNModel(lstmLayer, vocabSize);

9.3.3. [训练]与预测

由于使用了长短期记忆网络模型来实例化两个层,因此训练速度被大大降低了。

int numEpochs = Integer.getInteger("MAX_EPOCH", 500);

int lr = 2;
TimeMachine.trainCh8(model, dataset, vocab, lr, numEpochs, device, false, manager);
INFO Training on: 1 GPUs.
INFO Load MXNet Engine Version 1.8.0 in 0.071 ms.
perplexity: 1.0, 61398.4 tokens/sec on gpu(0)
time traveller with a slight accession ofcheerfulness really thi
traveller he have and so ovey alo in timethat is the germ o

9.3.4. 小结

  • 在深度循环神经网络中,隐状态的信息被传递到当前层的下一时间步和下一层的当前时间步。

  • 有许多不同风格的深度循环神经网络, 如长短期记忆网络、门控循环单元、或经典循环神经网络。 这些模型在深度学习框架的高级API中都有涵盖。

  • 总体而言,深度循环神经网络需要大量的调参(如学习率和修剪) 来确保合适的收敛,模型的初始化也需要谨慎。

9.3.5. 练习

  1. 基于我们在 Section 8.5中讨论的单层实现, 尝试从零开始实现两层循环神经网络。

  2. 在本节训练模型中,比较使用门控循环单元替换长短期记忆网络后模型的精确度和训练速度。

  3. 如果增加训练数据,你能够将困惑度降到多低?

  4. 在为文本建模时,是否可以将不同作者的源数据合并?有何优劣呢?