【翻译】Sklearn与TensorFlow机器学习实用指南 —— 第14章 循环神经网络(上)

浏览: 1497

作者:ApacheCN【翻译】  Python机器学习爱好者

Python爱好者社区专栏作者

GitHub:https://github.com/apachecn/hands_on_Ml_with_Sklearn_and_TF


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

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

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

循环神经元

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

记忆单元

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

输入和输出序列

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

Python爱好者社区历史文章大合集:

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门免费视频课程!!!

【最新免费微课】小编的Python快速上手matplotlib可视化库!!!

崔老师爬虫实战案例免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

推荐 0
本文由 Python爱好者社区 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册