Microsoft Cognitive Toolkit 简明教程

CNTK - Recurrent Neural Network

现在,让我们了解如何在 CNTK 中构建循环神经网络 (RNN)。

Introduction

我们学习了如何使用神经网络对图像进行分类,这是深度学习中的标志性任务之一。但是,神经网络另一个出色且正在进行大量研究的领域是循环神经网络 (RNN)。在这里,我们将了解什么是 RNN 以及如何在需要处理时间序列数据的情况下使用它。

What is Recurrent Neural Network?

循环神经网络 (RNN) 可以定义为能够随时间推理的特殊类型的 NN。RNN 主要用于需要处理随时间变化的值(即时间序列数据)的情况。为了更好地理解它,让我们在常规神经网络和循环神经网络之间进行一个小比较 -

  1. 众所周知,在常规神经网络中,我们只能提供一个输入。这限制了它只生成一个预测。举个例子,我们可以使用常规神经网络来执行翻译文本工作。

  2. 另一方面,在循环神经网络中,我们可以提供一系列样本,这些样本生成单个预测。换句话说,使用 RNN,我们可以基于输入序列预测输出序列。例如,RNN 在翻译任务中已经有一些成功的实验。

Uses of Recurrent Neural Network

RNN 可以以多种方式使用。其中一些如下 -

Predicting a single output

在深入了解 RNN 如何基于序列预测单个输出的步骤之前,让我们看看一个基本的 RNN 是什么样子的 -

single output

正如我们在上面的图表中看到的那样,RNN 包含到输入的环回连接,并且每当我们输入一系列的值时,它都会将序列中的每个元素作为时间步长进行处理。

此外,由于环回连接,RNN 可以将生成的输出与序列中下一个元素的输入相结合。通过这种方式,RNN 将在整个序列中构建一个内存,可用于进行预测。

为了使用 RNN 进行预测,我们可以执行以下步骤 -

  1. 首先,要创建初始隐藏状态,我们需要提供输入序列的第一个元素。

  2. 接下来,要生成更新的隐藏状态,我们需要获取初始隐藏状态并将其与输入序列中的第二个元素相结合。

  3. 最后,要生成最终的隐藏状态并预测 RNN 的输出,我们需要获取输入序列中的最后一个元素。

通过这种方式,借助这个环回连接,我们可以教 RNN 识别随时间发生的模式。

Predicting a sequence

上面讨论的基本 RNN 模型还可以扩展到其他用例。例如,我们可以使用它基于单个输入预测一系列值。在这种情况下,为了使用 RNN 进行预测,我们可以执行以下步骤 -

  1. 首先,若要创建初始隐藏状态并预测输出序列中的第一个元素,我们需要将输入样本馈入神经网络。

  2. 在其后,若要生成更新的隐藏状态和输出序列中的第二个元素,我们需要将初始的隐藏状态与同一个样本相结合。

  3. 最后,若要再次更新隐藏状态并预测输出序列中的最终元素,我们要再次馈送该样本。

Predicting sequences

我们已经了解了如何基于序列预测一个值以及如何基于单个值预测序列。现在我们来看一看我们如何为序列预测序列。在这种情况下,为了利用 RNN 做出预测,我们可以执行以下步骤:

  1. 首先,若要创建初始隐藏状态并预测输出序列中的第一个元素,我们需要获取输入序列中的第一个元素。

  2. 在其后,若要更新隐藏状态并预测输出序列中的第二个元素,我们需要获取初始的隐藏状态。

  3. 最后,若要预测输出序列中的最终元素,我们需要获取更新过的隐藏状态和输入序列中的最终元素。

Working of RNN

为了理解循环神经网络 (RNN) 的工作原理,我们需要首先了解网络中的循环层的运作机制。因此,我们首先讨论一下如何利用标准循环层预测输出。

Predicting output with standard RNN layer

就像我们之前讨论过的那样,RNN 中的一个基本层与神经网络中的一个常规层非常不同。在上一个部分,我们还在图表中演示了 RNN 的基本架构。为了首次更新顺序中步骤的隐藏状态,我们可以使用以下公式:

rnn layer

在上一个等式中,我们通过计算初始隐藏状态和一组权重之间的点积来计算新的隐藏状态。

现在,对于下一步,当前时间步骤的隐藏状态被用作顺序中下一步的初始隐藏状态。正因为如此,为了第二次更新时间步骤的隐藏状态,我们可以重复第一次执行的计算,如下所示:

first step

接下来,我们可以重复针对第三步和顺序中的最后一步更新隐藏状态的过程,如下所示:

last step

当我们在序列中处理完以上所有步骤后,我们可以按照如下计算输出值:

output

对于以上公式,我们使用了第三组权重和最终时间步骤隐藏状态。

Advanced Recurrent Units

基本循环层的主要问题是梯度消失问题,并且由此导致它不善于学习长期关联。简而言之,基本循环层不是很好地处理较长的序列。因此,以下其他一些循环层类型更适合于处理更长的序列:

Long-Short Term Memory (LSTM)

long short term memory

Hochreiter和Schmidhuber提出了长短期记忆(LSTMs)网络。它解决了让基本递归层长时间记住事物的问题。LSTM的架构在图中以上给出。正如我们所看到的,它具有输入神经元、记忆细胞和输出神经元。为了解决梯度消失问题,长短期记忆网络使用显式记忆单元(存储先前的值)和以下门-

  1. * 遗忘门*——顾名思义,它告诉存储单元忘记之前的值。存储单元存储值,直到门,即“遗忘门”,告诉它忘记它们为止。

  2. * 输入门*——顾名思义,它将新内容添加到存储单元。

  3. * 输出门*——顾名思义,输出门决定何时将向量从存储单元传递到下一个隐藏状态。

Gated Recurrent Units (GRUs)

gated recurrent units

Gradient recurrent units (GRU)是 LSTM 网络的一个小变种。它的门少一个并且连接方式与 LSTM 略有不同。其架构在上图中显示。它具有输入神经元、门控存储单元和输出神经元。门控循环单元网络具有以下两个门:

  1. Update gate − 它决定以下两件事−

  2. * 复位门 *− 复位门的功能很像 LSTM 网络的遗忘门。唯一的区别在于它的位置略有不同。

与长短期记忆网络相比,门控循环单元网络的速度稍快,并且运行起来也更容易。

Creating RNN structure

在开始预测任何数据源的输出之前,我们需要先构建 RNN,而构建 RNN 与我们在上一节中构建常规神经网络非常相似。下面是构建一个 RNN 的代码−

from cntk.losses import squared_error
from cntk.io import CTFDeserializer, MinibatchSource, INFINITELY_REPEAT, StreamDefs, StreamDef
from cntk.learners import adam
from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
BATCH_SIZE = 14 * 10
EPOCH_SIZE = 12434
EPOCHS = 10

Staking multiple layers

我们还可以在 CNTK 中堆叠多个循环层。例如,我们可以使用以下层组合−

from cntk import sequence, default_options, input_variable
from cntk.layers import Recurrence, LSTM, Dropout, Dense, Sequential, Fold
features = sequence.input_variable(1)
with default_options(initial_state = 0.1):
   model = Sequential([
      Fold(LSTM(15)),
      Dense(1)
   ])(features)
target = input_variable(1, dynamic_axes=model.dynamic_axes)

如我们在上面的代码中看到的,我们有以下两种方法可以在 CNTK 中对 RNN 进行建模−

  1. 首先,如果我们只想要循环层的最终输出,我们可以将 Fold 层与循环层(例如 GRU、LSTM,甚至是 RNNStep)结合使用。

  2. 其次,作为一种替代方法,我们还可以使用 Recurrence 块。

Training RNN with time series data

在构建模型后,让我们看看如何在 CNTK 中训练 RNN −

from cntk import Function
@Function
def criterion_factory(z, t):
   loss = squared_error(z, t)
   metric = squared_error(z, t)
   return loss, metric
loss = criterion_factory(model, target)
learner = adam(model.parameters, lr=0.005, momentum=0.9)

现在要将数据加载到训练过程中,我们必须从一组 CTF 文件中反序列化序列。以下代码有 create_datasource 函数,它是一个有用的实用函数,可用于创建训练和测试数据源。

target_stream = StreamDef(field='target', shape=1, is_sparse=False)
features_stream = StreamDef(field='features', shape=1, is_sparse=False)
deserializer = CTFDeserializer(filename, StreamDefs(features=features_stream, target=target_stream))
   datasource = MinibatchSource(deserializer, randomize=True, max_sweeps=sweeps)
return datasource
train_datasource = create_datasource('Training data filename.ctf')#we need to provide the location of training file we created from our dataset.
test_datasource = create_datasource('Test filename.ctf', sweeps=1) #we need to provide the location of testing file we created from our dataset.

现在,当我们设置好数据源、模型和损失函数时,就可以开始训练过程了。这与我们在上一节中使用基本神经网络时所做的事情非常相似。

progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
   features: train_datasource.streams.features,
   target: train_datasource.streams.target
}
history = loss.train(
   train_datasource,
   epoch_size=EPOCH_SIZE,
   parameter_learners=[learner],
   model_inputs_to_streams=input_map,
   callbacks=[progress_writer, test_config],
   minibatch_size=BATCH_SIZE,
   max_epochs=EPOCHS
)

我们将获得如下所示的输出:

Output−

average  since  average  since  examples
loss      last  metric  last
------------------------------------------------------
Learning rate per minibatch: 0.005
0.4      0.4    0.4      0.4      19
0.4      0.4    0.4      0.4      59
0.452    0.495  0.452    0.495   129
[…]

Validating the model

实际上,使用 RNN 进行预测与使用任何其他 CNK 模型进行预测非常相似。唯一的区别是,我们需要提供序列而不是单个样本。

现在,由于我们的 RNN 终于完成了训练,我们可以使用一些样本序列进行测试,从而验证模型,如下所示−

import pickle
with open('test_samples.pkl', 'rb') as test_file:
test_samples = pickle.load(test_file)
model(test_samples) * NORMALIZE

Output−

array([[ 8081.7905],
[16597.693 ],
[13335.17 ],
...,
[11275.804 ],
[15621.697 ],
[16875.555 ]], dtype=float32)