开发者社区> apachecn_飞龙> 正文

《Scikit-Learn与TensorFlow机器学习实用指南》第14章 循环神经网络

简介: 第14章 循环神经网络 来源:ApacheCN《Sklearn 与 TensorFlow 机器学习实用指南》翻译项目 译者:@akonwang @alexcheen @飞龙 校对:@飞龙 击球手击出垒球,你会开始预测球的轨迹并立即开始奔跑。
+关注继续查看

第14章 循环神经网络

来源:ApacheCN《Sklearn 与 TensorFlow 机器学习实用指南》翻译项目

译者:@akonwang @alexcheen @飞龙

校对:@飞龙

击球手击出垒球,你会开始预测球的轨迹并立即开始奔跑。你追踪着它,不断调整你的移动步伐,最终在观众的一片雷鸣声中抓到它。无论是在听完朋友的话语还是早餐时预测咖啡的味道,你时刻在做的事就是在预测未来。在本章中,我们将讨论循环神经网络 – 一类预测未来的网络(当然,是到目前为止)。它们可以分析时间序列数据,诸如股票价格,并告诉你什么时候买入和卖出。在自动驾驶系统中,他们可以预测行车轨迹,避免发生交通意外。更一般地说,它们可在任意长度的序列上工作,而不是截止目前我们讨论的只能在固定长度的输入上工作的网络。举个例子,它们可以把语句,文件,以及语音范本作为输入,使得它们在诸如自动翻译,语音到文本或者情感分析(例如,读取电影评论并提取评论者关于该电影的感觉)的自然语言处理系统中极为有用。

更近一步,循环神经网络的预测能力使得它们具备令人惊讶的创造力。你同样可以要求它们去预测一段旋律的下几个音符,然后随机选取这些音符的其中之一并演奏它。然后要求网络给出接下来最可能的音符,演奏它,如此周而复始。在你知道它之前,你的神经网络将创作一首诸如由谷歌 Magenta 工程所创造的《The one》的歌曲。类似的,循环神经网络可以生成语句,图像标注以及更多。目前结果还不能准确得到莎士比亚或者莫扎特的作品,但谁知道几年后他们能生成什么呢?

在本章中,我们将看到循环神经网络背后的基本概念,他们所面临的主要问题(换句话说,在第11章中讨论的消失/爆炸的梯度),以及广泛用于反抗这些问题的方法:LSTM 和 GRU cell(单元)。如同以往,沿着这个方式,我们将展示如何用 TensorFlow 实现循环神经网络。最终我们将看看及其翻译系统的架构。

循环神经元

到目前为止,我们主要关注的是前馈神经网络,其中激活仅从输入层到输出层的一个方向流动(附录 E 中的几个网络除外)。 循环神经网络看起来非常像一个前馈神经网络,除了它也有连接指向后方。 让我们看一下最简单的 RNN,它由一个神经元接收输入,产生一个输出,并将输出发送回自己,如图 14-1(左)所示。 在每个时间步t(也称为一个帧),这个循环神经元接收输入 x(t)x(t) 以及它自己的前一时间步长 y(t1)y(t−1) 的输出。 我们可以用时间轴来表示这个微小的网络,如图 14-1(右)所示。 这被称为随着时间的推移展开网络。

你可以轻松创建一个循环神经元层。 在每个时间步t,每个神经元都接收输入向量 x(t)x(t) 和前一个时间步 y(t1)y(t−1) 的输出向量,如图 14-2 所示。 请注意,输入和输出都是向量(当只有一个神经元时,输出是一个标量)。

每个循环神经元有两组权重:一组用于输入 x(t)x(t),另一组用于前一时间步长 y(t1)y(t−1) 的输出。 我们称这些权重向量为 wxwxwywy。如公式 14-1 所示(b是偏差项,φ(·)是激活函数,例如 ReLU),可以计算单个循环神经元的输出。

就像前馈神经网络一样,我们可以使用上一个公式的向量化形式,对整个小批量计算整个层的输出(见公式 14-2)。

  • Y(t)Y(t)是m \times n_{neurons}tm矩阵,包含在最小批次中每个实例在时间步‘t‘处的层输出(‘m‘是小批次中的实例数,n_{neurons}$ 是神经元数)。
  • X(t)X(t)m×ninputsm×ninputs 矩阵,包含所有实例的输入的 (ninputsninputs 是输入特征的数量)。
  • WxWxninputs×nneuronsninputs×nneurons 矩阵,包含当前时间步的输入的连接权重的。
  • WyWynneurons×nneuronsnneurons×nneurons 矩阵,包含上一个时间步的输出的连接权重。
  • 权重矩阵 WxWxWyWy 通常连接成单个权重矩阵W,形状为 (ninputs+nneurons)×nneurons(ninputs+nneurons)×nneurons(见公式 14-2 的第二行)
  • b是大小为 nneuronsnneurons 的向量,包含每个神经元的偏置项。

注意,Y(t)Y(t)X(t)X(t)Y(t1)Y(t−1) 的函数,它是 X(t1)X(t−1)Y(t2)Y(t−2) 的函数,它是 X(t2)X(t−2)Y(t3)Y(t−3) 的函数,等等。 这使得 Y(t)Y(t) 是从时间t = 0开始的所有输入(即 X(0)X(0)X(1)X(1),…,X(t)X(t))的函数。 在第一个时间步,t = 0,没有以前的输出,所以它们通常被假定为全零。

记忆单元

由于时间t的循环神经元的输出,是由所有先前时间步骤计算出来的的函数,你可以说它有一种记忆形式。一个神经网络的一部分,跨越时间步长保留一些状态,称为存储单元(或简称为单元)。单个循环神经元或循环神经元层是非常基本的单元,但本章后面我们将介绍一些更为复杂和强大的单元类型。

一般情况下,时间步t处的单元状态,记为 h(t)h(t)h代表“隐藏”),是该时间步的某些输入和前一时间步的状态的函数:h^{(t)} = f(h^{(t-1), x^{(t)})h^{(t)} = f(h^{(t-1), x^{(t)})。 其在时间步`t`处的输出,表示为 y(t)y(t),也和前一状态和当前输入的函数有关。 在我们已经讨论过的基本单元的情况下,输出等于单元状态,但是在更复杂的单元中并不总是如此,如图 14-3 所示。

输入和输出序列

RNN 可以同时进行一系列输入并产生一系列输出(见图 14-4,左上角的网络)。 例如,这种类型的网络对于预测时间序列(如股票价格)非常有用:你在过去的N天内给出价格,并且它必须输出向未来一天移动的价格(即从N - 1天前到明天)。

或者,你可以向网络输入一系列输入,并忽略除最后一个之外的所有输出(请参阅右上角的网络)。 换句话说,这是一个向量网络的序列。 例如,你可以向网络提供与电影评论相对应的单词序列,并且网络将输出情感评分(例如,从-1 [恨]+1 [爱])。

相反,你可以在第一个时间步中为网络提供一个输入(而在其他所有时间步中为零),然后让它输出一个序列(请参阅左下角的网络)。 这是一个向量到序列的网络。 例如,输入可以是图像,输出可以是该图像的标题。

最后,你可以有一个序列到向量网络,称为编码器,后面跟着一个称为解码器的向量到序列网络(参见右下角的网络)。 例如,这可以用于将句子从一种语言翻译成另一种语言。 你会用一种语言给网络喂一个句子,编码器会把这个句子转换成单一的向量表示,然后解码器将这个向量解码成另一种语言的句子。 这种称为编码器 - 解码器的两步模型,比用单个序列到序列的 RNN(如左上方所示的那个)快速地进行翻译要好得多,因为句子的最后一个单词可以 影响翻译的第一句话,所以你需要等到听完整个句子才能翻译。

TensorFlow 中的基本 RNN

首先,我们来实现一个非常简单的 RNN 模型,而不使用任何 TensorFlow 的 RNN 操作,以更好地理解发生了什么。 我们将使用 tanh 激活函数创建由 5 个循环神经元的循环层组成的 RNN(如图 14-2 所示的 RNN)。 我们将假设 RNN 只运行两个时间步,每个时间步输入大小为 3 的向量。 下面的代码构建了这个 RNN,展开了两个时间步骤:

n_inputs = 3
n_neurons = 5
X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])
Wx = tf.Variable(tf.random_normal(shape=[n_inputs, n_neurons], dtype=tf.float32))
Wy = tf.Variable(tf.random_normal(shape=[n_neurons, n_neurons], dtype=tf.float32))
b = tf.Variable(tf.zeros([1, n_neurons], dtype=tf.float32))
Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b)
init = tf.global_variables_initializer()

这个网络看起来很像一个双层前馈神经网络,有一些改动:首先,两个层共享相同的权重和偏差项,其次,我们在每一层都有输入,并从每个层获得输出。 为了运行模型,我们需要在两个时间步中都有输入,如下所示:

    # Mini-batch: instance 0,instance 1,instance 2,instance 3
    X0_batch = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 0, 1]])  # t = 0
    X1_batch = np.array([[9, 8, 7], [0, 0, 0], [6, 5, 4], [3, 2, 1]])  # t = 1
    with tf.Session() as sess:
        init.run()
    Y0_val, Y1_val = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

这个小批量包含四个实例,每个实例都有一个由两个输入组成的输入序列。 最后,Y0_valY1_val在所有神经元和小批量中的所有实例的两个时间步中包含网络的输出:

>>> print(Y0_val) # output at t = 0
[[-0.2964572 0.82874775 -0.34216955 -0.75720584 0.19011548] # instance 0
[-0.12842922 0.99981797 0.84704727 -0.99570125 0.38665548] # instance 1
[ 0.04731077 0.99999976 0.99330056 -0.999933 0.55339795] # instance 2
[ 0.70323634 0.99309105 0.99909431 -0.85363263 0.7472108 ]] # instance 3
>>> print(Y1_val) # output at t = 1
[[ 0.51955646 1\. 0.99999022 -0.99984968 -0.24616946] # instance 0
[-0.70553327 -0.11918639 0.48885304 0.08917919 -0.26579669] # instance 1
[-0.32477224 0.99996376 0.99933046 -0.99711186 0.10981458] # instance 2
[-0.43738723 0.91517633 0.97817528 -0.91763324 0.11047263]] # instance 3

这并不难,但是当然如果你想能够运行 100 多个时间步骤的 RNN,这个图形将会非常大。 现在让我们看看如何使用 TensorFlow 的 RNN 操作创建相同的模型。

完整代码

import numpy as np
import tensorflow as tf

if __name__ == '__main__':
    n_inputs = 3
    n_neurons = 5
    X0 = tf.placeholder(tf.float32, [None, n_inputs])
    X1 = tf.placeholder(tf.float32, [None, n_inputs])
    Wx = tf.Variable(tf.random_normal(shape=[n_inputs, n_neurons], dtype=tf.float32))
    Wy = tf.Variable(tf.random_normal(shape=[n_neurons, n_neurons], dtype=tf.float32))
    b = tf.Variable(tf.zeros([1, n_neurons], dtype=tf.float32))
    Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
    Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b)
    init = tf.global_variables_initializer()

    # Mini-batch: instance 0,instance 1,instance 2,instance 3
    X0_batch = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 0, 1]])  # t = 0
    X1_batch = np.array([[9, 8, 7], [0, 0, 0], [6, 5, 4], [3, 2, 1]])  # t = 1
    with tf.Session() as sess:
        init.run()
        Y0_val, Y1_val = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

    print(Y0_val,'\n')
    print(Y1_val)

时间上的静态展开

static_rnn()函数通过链接单元来创建一个展开的 RNN 网络。 下面的代码创建了与上一个完全相同的模型:

X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, [X0, X1],
                                                dtype=tf.float32)
Y0, Y1 = output_seqs

首先,我们像以前一样创建输入占位符。 然后,我们创建一个BasicRNNCell,你可以将其视为一个工厂,创建单元的副本以构建展开的 RNN(每个时间步一个)。 然后我们调用static_rnn(),向它提供单元工厂和输入张量,并告诉它输入的数据类型(用来创建初始状态矩阵,默认情况下是全零)。 static_rnn()函数为每个输入调用单元工厂的__call __()函数,创建单元的两个副本(每个单元包含 5 个循环神经元的循环层),并具有共享的权重和偏置项,像前面一样。static_rnn()函数返回两个对象。 第一个是包含每个时间步的输出张量的 Python 列表。 第二个是包含网络最终状态的张量。 当你使用基本的单元时,最后的状态就等于最后的输出。

如果有 50 个时间步长,则不得不定义 50 个输入占位符和 50 个输出张量。而且,在执行时,你将不得不为 50 个占位符中的每个占位符输入数据并且还要操纵 50 个输出。我们来简化一下。下面的代码再次构建相同的 RNN,但是这次它需要一个形状为[None,n_steps,n_inputs]的单个输入占位符,其中第一个维度是最小批量大小。然后提取每个时间步的输入序列列表。 X_seqs是形状为n_steps的 Python 列表,包含形状为[None,n_inputs]的张量,其中第一个维度同样是最小批量大小。为此,我们首先使用transpose()函数交换前两个维度,以便时间步骤现在是第一维度。然后,我们使 unstack()函数沿第一维(即每个时间步的一个张量)提取张量的 Python 列表。接下来的两行和以前一样。最后,我们使用stack()函数将所有输出张量合并成一个张量,然后我们交换前两个维度得到最终输出张量,形状为[None, n_steps,n_neurons](第一个维度是小批量大小)。

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
X_seqs = tf.unstack(tf.transpose(X, perm=[1, 0, 2]))

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, X_seqs,
                                                dtype=tf.float32)
outputs = tf.transpose(tf.stack(output_seqs), perm=[1, 0, 2])

现在我们可以通过给它提供一个包含所有小批量序列的张量来运行网络:

X_batch = np.array([
        # t = 0      t = 1 
        [[0, 1, 2], [9, 8, 7]], # instance 1
        [[3, 4, 5], [0, 0, 0]], # instance 2
        [[6, 7, 8], [6, 5, 4]], # instance 3
        [[9, 0, 1], [3, 2, 1]], # instance 4
    ])

with tf.Session() as sess:
    init.run()
    outputs_val = outputs.eval(feed_dict={X: X_batch})

我们得到所有实例,所有时间步长和所有神经元的单一outputs_val张量:

但是,这种方法仍然会建立一个每个时间步包含一个单元的图。 如果有 50 个时间步,这个图看起来会非常难看。 这有点像写一个程序而没有使用循环(例如,Y0 = f(0,X0)Y1 = f(Y0,X1)Y2 = f(Y1,X2);…;Y50 = f(Y49,X50))。 如果使用大图,在反向传播期间(特别是在 GPU 内存有限的情况下),你甚至可能会发生内存不足(OOM)错误,因为它必须在正向传递期间存储所有张量值,以便可以使用它们在反向传播期间计算梯度。

幸运的是,有一个更好的解决方案:dynamic_rnn()函数。

时间上的动态展开

dynamic_rnn()函数使用while_loop()操作,在单元上运行适当的次数,如果要在反向传播期间将 GPU内 存交换到 CPU 内存,可以设置swap_memory = True,以避免内存不足错误。 方便的是,它还可以在每个时间步(形状为[None, n_steps, n_inputs])接受所有输入的单个张量,并且在每个时间步(形状[None, n_steps, n_neurons])上输出所有输出的单个张量。 没有必要堆叠,拆散或转置。 以下代码使用dynamic_rnn()函数创建与之前相同的 RNN。 这太好了!

完整代码

import numpy as np
import tensorflow as tf
import pandas as pd

if __name__ == '__main__':
    n_steps = 2
    n_inputs = 3
    n_neurons = 5

    X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])

    basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
    outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

    init = tf.global_variables_initializer()

    X_batch = np.array([
        [[0, 1, 2], [9, 8, 7]],  # instance 1
        [[3, 4, 5], [0, 0, 0]],  # instance 2
        [[6, 7, 8], [6, 5, 4]],  # instance 3
        [[9, 0, 1], [3, 2, 1]],  # instance 4
    ])

    with tf.Session() as sess:
        init.run()
        outputs_val = outputs.eval(feed_dict={X: X_batch})

    print(outputs_val)

在反向传播期间,while_loop()操作会执行相应的步骤:在正向传递期间存储每次迭代的张量值,以便在反向传递期间使用它们来计算梯度。

处理变长输入序列

到目前为止,我们只使用固定大小的输入序列(全部正好两个步长)。 如果输入序列具有可变长度(例如,像句子)呢? 在这种情况下,你应该在调用dynamic_rnn()(或static_rnn())函数时设置sequence_length参数;它必须是一维张量,表示每个实例的输入序列的长度。 例如:

n_steps = 2
n_inputs = 3
n_neurons = 5

reset_graph()

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
seq_length = tf.placeholder(tf.int32, [None])
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32,
                                    sequence_length=seq_length)

例如,假设第二个输入序列只包含一个输入而不是两个输入。 为了适应输入张量X,必须填充零向量(因为输入张量的第二维是最长序列的大小,即 2)

X_batch = np.array([
        # step 0     step 1
        [[0, 1, 2], [9, 8, 7]], # instance 1
        [[3, 4, 5], [0, 0, 0]], # instance 2 (padded with zero vectors)
        [[6, 7, 8], [6, 5, 4]], # instance 3
        [[9, 0, 1], [3, 2, 1]], # instance 4
    ])
seq_length_batch = np.array([2, 1, 2, 2])

当然,你现在需要为两个占位符Xseq_length提供值:

with tf.Session() as sess:
    init.run()
    outputs_val, states_val = sess.run(
        [outputs, states], feed_dict={X: X_batch, seq_length: seq_length_batch})

现在,RNN 输出序列长度的每个时间步都会输出零向量(查看第二个时间步的第二个输出):

此外,状态张量包含每个单元的最终状态(不包括零向量):

处理变长输出序列

如果输出序列长度不一样呢? 如果事先知道每个序列的长度(例如,如果知道长度与输入序列的长度相同),那么可以按照上面所述设置sequence_length参数。 不幸的是,通常这是不可能的:例如,翻译后的句子的长度通常与输入句子的长度不同。 在这种情况下,最常见的解决方案是定义一个称为序列结束标记(EOS 标记)的特殊输出。 任何在 EOS 后面的输出应该被忽略(我们将在本章稍后讨论)。

好,现在你知道如何建立一个 RNN 网络(或者更准确地说是一个随着时间的推移而展开的 RNN 网络)。 但是你怎么训练呢?

训练 RNN

为了训练一个 RNN,诀窍是在时间上展开(就像我们刚刚做的那样),然后简单地使用常规反向传播(见图 14-5)。 这个策略被称为时间上的反向传播(BPTT)。

就像在正常的反向传播中一样,展开的网络(用虚线箭头表示)有第一个正向传递。然后使用损失函数评估输出序列 (其中 tmintmintmaxtmax 是第一个和最后一个输出时间步长,不计算忽略的输出),并且该损失函数的梯度通过展开的网络向后传播(实线箭头);最后使用在 BPTT 期间计算的梯度来更新模型参数。 请注意,梯度在损失函数所使用的所有输出中反向流动,而不仅仅通过最终输出(例如,在图 14-5 中,损失函数使用网络的最后三个输出 Y(2)Y(2)Y(3)Y(3)Y(4)Y(4),所以梯度流经这三个输出,但不通过 Y(0)Y(0)Y(1)Y(1))。 而且,由于在每个时间步骤使用相同的参数Wb,所以反向传播将做正确的事情并且总结所有时间步骤。

训练序列分类器

我们训练一个 RNN 来分类 MNIST 图像。 卷积神经网络将更适合于图像分类(见第 13 章),但这是一个你已经熟悉的简单例子。 我们将把每个图像视为 28 行 28 像素的序列(因为每个MNIST图像是28×28像素)。 我们将使用 150 个循环神经元的单元,再加上一个全连接层,其中包含连接到上一个时间步的输出的 10 个神经元(每个类一个),然后是一个 softmax 层(见图 14-6)。

建模阶段非常简单, 它和我们在第 10 章中建立的 MNIST 分类器几乎是一样的,只是展开的 RNN 替换了隐层。 注意,全连接层连接到状态张量,其仅包含 RNN 的最终状态(即,第 28 个输出)。 另请注意,y是目标类的占位符。

n_steps = 28
n_inputs = 28
n_neurons = 150
n_outputs = 10

learning_rate = 0.001

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.int32, [None])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

logits = tf.layers.dense(states, n_outputs)
xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y,
                                                          logits=logits)
loss = tf.reduce_mean(xentropy)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss)
correct = tf.nn.in_top_k(logits, y, 1)
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

init = tf.global_variables_initializer()

现在让我们加载 MNIST 数据,并按照网络的预期方式将测试数据重塑为[batch_