TensorFlow 实战Google深度学习框架(第2版)第八章读书笔记

第八章:循环神经网络

* 8.1循环神经网络简介
* 8.2长短时记忆网络(LSTM)结构
* 8.3循环神经网络的变种
       * 8.3.1双向循环神经网络和深层循环神经网络
       * 8.3.2循环神经网络的dropout
* 8.4循环神经网络样例应用

第6章中讲解了卷积神经网络的网络结构,井介绍了如何使用卷积神经网络解决图像识别问题。
本章中将介绍另外一种常用的神经网络结构一一循环神经网络(recurrentneural network, RNN)
以及循环神经网络中的一个重要结构一一长短时记忆网络(long short-term memory, LSTM)。
本章还将介绍循环神经网络在时序分析问题中的应用。

-8.1- 循环神经网络简介

本节将介绍循环神经网络的基本知识并通过机器翻译问题说明循环神经网络是如何被应用的。
这一节中将给出一个具体的样例来说明一个最简单的循环神经网络的前向传播时是如何工作的。

循环神经网络(recurτent neural network,RNN)源自于1982年由Saratha Sathasivam提出的霍普菲尔德网络。霍普菲尔德网络因为实现困难,在其提出时并且没有被合适地应用。该网络结构也于1986年后被全连接神经网络以及一些传统的机器学习算法所取代。然而,传统的机器学习算法非常依赖于人工提取的特征,使得基于传统机器学习的图像识别、语音识别以及自然语言处理等问题存在特征提取的瓶颈。而基于全连接神经网络的方法也存在参数太多、无法利用数据中时间序列信息等问题。
随着更加有效的循环神经网络结构被不断提出,循环神经网络挖掘数据中的时序信息以及语义信息的深度表达能力被充分利用,并在语音识别、语言模型、机器翻译以及时序分析等方面实现了突破。

循环神经网络的主要用途是处理和预测序列数据。在之前介绍的全连接神经网络或卷积神经网络模型中,网络结构都是从输入层到隐含层再到输出层,层与层之间是全连接或部分连接的,但每层之间的节点是无连接的。
考虑这样一个问题,如果要预测句子的下一个单词是什么,一般需要用到当前单词以及前面的单词,因为句子中前后单词并不是独立的。比如,当前单词是“很”,前一个单词是“天空”,那么下一个单词很大概率是“蓝”。
循环神经网络的来源就是为了刻画一个序列当前输出之前信息的关系。从网络结构上,循环神经网络会记忆之前的信息,并利用之前的信息影响后面结点的输出。也就是说,循环神经网络的隐藏层之间的结点是有连接的,隐藏层的输入不仅包括输入层的输出,还包括上一时刻隐藏层的输出。

图8-1

图8-1展示了一个典型的循环神经网络。在每一时刻,循环神经网络会针对该时刻的输入结合当前模型的状态给出一个输出,并更新模型状态。从图8-1中可以看到,循环神经网络的主体结构的输入除了来自输入层,还有一个循环的边来提供上一时刻的隐藏状态(hidden state)。在每一个时刻,循环神经网络的模块在读取了和之后会生成新的隐藏状态,并产生本时刻的输出。由于模块A中的运算和变量在不同时刻是相同的,因此循环神经网络理论上可以被看作是同一神经网络结构被无限复制的结果。正如卷积神经网络在不同的空间位置共享参数,循环神经网络是在不同时间位置共享参数,从而能够使用有限的参数处理任意长度的序列。

图8-2

将完整的输入输出序列展开,可以得到图8-2所展示的结构。在图8-2中可以更加清楚地看到循环神经网络在每一个时刻会有一个输入,然后根据循环神经网络前一时刻的状态计算新的状态,并输出。循环神经网络当前的状态是根据上一时刻的状态和当前的输入共同决定的。在时刻t,状态浓缩了前面序列,, ··· ,的信息,用于作为输出的参考。由于序列的长度可以无限延长,维度有限的状态不可能将序列的全部信息都保存下来,因此模型必须学习只保留与后面任务,, ··· 相关的最重要的信息。

循环网络的展开在模型训练中有重要意义。从8-2图中可以看到,循环神经网络对长度为的序列展开之后,可以视为一个有个中间层的前馈神经网络。这个前馈神经网络没有循环链接,因此可以直接使用反向传播算法进行训练,而不需要任何特别的优化算法。这样的训练方法称为“沿时间反向传播”(Back-Propagation Through Time),是训练循环神经网络最常见的方法。

从循环神经网络的结构特征可以很容易看出它最擅长解决与时间序列相关的问题。循环神经网络也是处理这类问题时最自然的神经网络结构。对于一个序列数据,可以将这个序列上不同时刻的数据依次传入循环神经网络的输入层,而输出可以是对序列中下一个时刻的预测,也可以是对当前时刻信息的处理结果(比如语音识别结果)。循环神经网络要求每一个时刻都有一个输入,但是不一定每个时刻都需要有输出。在过去几年中,循环神经网络已经被广泛地应用在语音识别、语言模型、机器翻译以及时序分析等问题上,并取得了巨大的成功。

图8-3

以机器翻译为例来介绍循环神经网络是如何解决实际问题的。循环神经网络中每一个时刻的输入为需要翻译的句子中的单词。如图8-3所示,需要翻译的句子为ABCD,那么循环神经网络第一段每一个时刻的输入就分别是A、B、C和D,然后用“”作为待翻译句子的结束符。在第一段中,循环神经网络没有输出。从结束符“”开始,循环神经网络进入翻译阶段。该阶段中每一个时刻的输入是上一个时刻的输出,而最终得到的输出就是句子ABCD的翻译结果。从图8-3中可以看到句子ABCD对应的翻译结果就是XYZ,当网络输出“_”时翻译结束。机器翻译的相关模型将在第9章中进行进一步介绍。

图8-4

如之前所介绍,循环神经网络可以看作是同一神经网络结构在时间序列上被复制多次的结果,这个被复制多次的结构被称之为循环体。如何设计循环体的网络结构是循环神经网络解决实际问题的关键。图8-4展示了一个最简单的循环体结构。这个循环体中只使用了一个类似全连接层的神经网络结构。下面将通过图8-4中所展示的神经网络来介绍循环神经网络前向传播的完整流程。

循环神经网络中的状态是通过一个向量来表示的,这个向量的维度也称为循环神经网络隐藏层的大小,假设其为。
从图8-4中可以看出,循环体中的神经网络的输入有两部分,一部分为上一时刻的状态,另一部分为当前时刻的输入样本
对于时间序列数据来说(比如不同时刻商品的销量),每一时刻的输入样例可以是当前时刻的数值(比如销量值);
对于语言模但来说,输入样例可以是当前单词对应的单词向量(word embedding)。

假设输入向量的维度为,隐藏状态的维度为,那么图8-4中循环体的全连接层神经网络的输入大小为。也就是将上一时刻的状态与当前时刻的输入拼接成一个大的向量作为循环体中神经网络的输入。因为该全连接层的输出为当前时刻的状态,于是输出层的节点个数也为,循环体中的参数个数为个。
从图8-4中可以看到,循环体中的神经网络输出不但提供给了下一时刻作为状态,同时也会提供给当前时刻的输出。注意到循环体状态与最终输出的维度通常不同,因此为了将当前时刻的状态转化为最终的输出,循环神经网络还需要另外一个全连接神经网络来完成这个过程。这和卷积神经网络中最后的全连接层的意义是一样的。类似的,不同时刻用于输出的全连接神经网络中的参数也是一致的。

图8-5

为了让读者对循环神经网络的前向传播有一个更加直观的认识,图8-5展示了一个循环神经网络前向传播的具体计算过程。在图8-5中,假设状态的维度为2,输入、输出的维度都为1,而且
循环体中的全连接层中权重为:

循环体中全联接层偏置项的大小为:,
用于输出的全连接层权重矩阵为:

用于输出的全连接层偏置项大小为:。

那么在时刻,因为没有上一时刻,所以将状态初始化为,而当前的输入为1,所以拼接得到的向量为,通过循环体中的全连接层神经网络得到的结果为:

这个结果将作为下一时刻的输入状态,同时循环神经网络也会使用该状态生成输出。 将该向盘作为输入提供给用于输出的全连接神经网络可以得到时刻的最终输出:

使用时刻的状态可以类似地推导得出时刻的状态为,而时刻的输出为。在得到循环神经网络的前向传播结果之后,可以和其他神经网络类似地定义损失函数。循环神经网络唯一的区别在于因为它每个时刻都有一个输出,所以循环神经网络的总损失为所有时刻(或者部分时刻)上的损失函数的总和。

以下代码实现了这个简单的循环神经网络前向传播的过程:

import numpy as np 

X = [1, 2]
state = [0.0, 0.0]

# 分开定义不同输入部分的权重以方便操作。
w_cell_state = nap.asarray([[0.1, 0.2], [0.3, 0.4]])
w_cell_input = np.asarray([0.5, 0.6])
b_cell = np.asarray([0.1, -0.1])

# 定义用于输出的全连接层参数。
w_output = np.asarray([[1.0], [2.0]])
b_output = 0.1 

# 按照时间顺序执行循环神经网络的前向传播过程。
for i in range(len(X)):
    # 计算循环体中的全连接层神经网络。
    before_activation = np.dot(state, w_cell_state) + X[i]*w_cell_input + b_cell 
    state = np.tanh(before_activation)

    # 根据当前时刻状态计算最终输出。
    final_output = np.dot(state, w_output) + b_output

    # 输出每个时刻的信息。
    print("before activation: ", before_activation)
    print("state: ", state)
    print("output: ", final_output)

"""
运行以上程序可以得到输出:
before activation: [0.6 0.5]
state: [0.53704957 0.46211716]
output: [1.56128388]
before activation: [1.2923401 1.39225678]
state: [0.85973818 0.88366641]
output: [2.72707101]
改输出和图8-5中的数字是一致的。
"""

和其他神经网络类似,在定义完损失函数之后,套用第4章中介绍的优化框架TensorFlow就可以自动完成模型训练的过程。

这里唯一需要特别指出的是,理论上循环神经网络可以支持任意长度的序列,然而在实际训练过程中,如果序列过长,
* 一方面会导致优化时出现梯度消散梯度爆炸的问题,
* 另一方面,展开后的前馈神经网络会占用过大的内存,
所以实际中一般会规定一个最大长度,当序列长度超过规定长度之后会对序列进行截断

-8.2- 长短时记忆网络(LSTM)结构

本节中将介绍循环神经网络中最重要的结构一一长短时记忆网络(long short-term memory,LSTM)的网络结构。
在这一节中将大致介绍LSTM结构中的主要元素,并给出具体的TensorFlow程序来实现一个使用了LSTM结构的循环神经网络。

循环神经网络通过保存历史信息来帮助当前的决策,例如使用之前出现的单词来加强对当前文字的理解。循环神经网络可以更好地利用传统神经网络结构所不能建模的信息,但同时这也带来了更大的技术挑战一一长期依赖(long-term dependencies)问题。

在有些问题中,模型仅仅需要短期内的信息来执行当前的任务。比如预测短语“大海的颜色是蓝色”中的最后一个单词“蓝色”时,模型并不需要记忆这个短语之前更长的上下文信息一一因为这一句话已经包含了足够的信息来预测最后一个词。在这样的场景中,相关的信息和待预测的词的位置之间的间隔很小,循环神经网络可以比较容易地利用先前信息。

但同样也会有一些上下文场景更加复杂的情况。比如当模型试着去预测段落“某地开设了大量工厂,空气污染十分严重......这里的天空都是灰色的”的最后一个单词时,仅仅根据短期依赖就无法很好的解决这种问题。因为只根据最后一小段,最后一个词可以是“蓝色的”或者“灰色的”。但如果模型需要预测清楚具体是什么颜色,就需要考虑先前提到但离当前位置较远的上下文信息。因此,当前预测位置和相关信息之间的文本间隔就有可能变得很大。当这个间隔不断增大时,类似图8-4中给出的简单循环神经网络有可能会丧失学习到距离如此远的信息的能力。或者在复杂语言场景中,有用信息的间隔有大有小、长短不一,循环神经网络的性能也会受到限制。

图8-6

长短时记忆网络(long short-term memory,LSTM)的设计就是为了解决这个问题。在很多的任务上,采用LSTM结构的循环神经网络比标准的循环神经网络表现更好。LSTM结构是由Sepp Hochreiter和Jiirgen Schrnidhuber于1997年提出的,它是一种特殊的循环体结构。如图8-6所示,与单一tanh循环体结构不同,LSTM是一种拥有三个“门”结构的特殊网络结构。

LSTM靠一些“门”的结构让信息有选择性地影响循环神经网络中每个时刻的状态。所谓“门”的结构就是一个使用sigmoid神经网络和一个按位做乘法的操作,这两个操作合在一起就是一个“门”的结构。之所以该结构叫做“门”是因为使用sigmoid作为激活函数的全连接神经网络层会输出一个0到1之间的数值,描述当前输入有多少信息量可以通过这个结构。于是这个结构的功能就类似于一扇门,当门打开时(sigmoid神经网络层输出为1时),全部信息都可以通过;当门关上时(sigmoid神经网络层输出为0时),任何信息都无法通过。本节下面的篇幅将介绍每一个“门”是如何工作的。

为了使循环神经网更有效的保存长期记忆,图8-6中遗忘门输入门至关重要,它们是LSTM结构的核心。

* 遗忘门 遗忘门的作用是让循环神经网络“忘记”之前没有用的信息。比如一段文章中先介绍了某地原来是绿水蓝天,但后来被污染了。于是在看到被污染了之后,循环神经网络应该“忘记”之前绿水蓝天的状态。这个工作是通过“遗忘门”来完成的。“遗忘门”会根据当前的输入和上一时刻输出决定哪一部分记忆需要被遗忘。假设状态的维度为。“遗忘门”会根据当前的输入,和上一时刻输出计算一个维度为的向量,它在每一维度上的值都在(0,1)范围内。再将上一时刻的状态与向量按位相乘,那么取值接近0的维度上的信息就会被“忘记”,而取值接近1的维度上的信息会被保留。
* 输入门 在循环神经网络“忘记”了部分之前的状态后,它还需要从当前的输入补充最新的记忆。这个过程就是“输入门”完成的。如图8-6所示,“输入门”会根据和决定哪些信息加入到状态中生成新的状态。比如当看到文章中提到环境被污染之后,模型需要将这个信息写入新的状态。这时“输入门”和需要写入的新状态都从和计算产生。

通过“遗忘门”和“输入门”,LSTM结构可以更加有效地决定哪些信息应该被遗忘,哪些信息应该得到保留。

* 输出门 LSTM结构在计算得到新的状态后需要产生当前时刻的输出,这个过程是通过“输出门”完成的。“输出门”会根据最新的状态、上一时刻的输出和当前的输入来决定该时刻的输出。比如当前的状态为被污染,那么“天空的颜色”后面的单词很可能就是“灰色的”。

相比图8-4中展示的循环神经网络,使用LSTM结构的循环神经网络的前向传播是一个相对比较复杂的过程。具体LSTM每个“门”的公式定义如下:

其中、、、是4个维度为的参数矩阵。

图8-7

图8-7用流程图的形式表示了上面的公式。
在TensorFlow中,LSTM结构可以被很简单地实现。以下代码展示了在TensorFlow中实现使用LSTM结构的循环神经网络的前向传播过程。

# 定义一个LSTM结构。
# 在TensorFlow中通过一句简单的命令就可以实现一个完整LSTM结构。
# LSTM中使用的变量也会在该函数中自动被声明。
lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_hidden_size)

# 将LSTM中的状态初始化为全0数组。
# BasicLSTMCell类提供了zero_state()函数来生成全零的初始状态。
# state是一个包含两个张量的LSTMStateTuple类,其中state.c和state.h分别对应了图8-7中c状态和h状态。
# 和其他神经网络类似,在优化循环神经网络时,每次也会使用一个batch的训练样本。以下代码中,batch_size给出了一个batch的大小。
state = lstm.zero_state(batch_size, tf.float32)

# 定义损失函数。
loss = 0.0 
# 虽然在测试时循环神经网络可以处理任意长度的序列,但是在训练中为了将循环网络展开成前馈神经网络,我们需要知道训练数据的序列长度。
# 在以下代码中,用num_steps来表示这个长度。
# 第9章中将介绍使用dynamic_rnn动态处理变长序列的方法。
for i in range(num_steps):
    # 在第一个时刻声明LSTM结构中使用的变量,在之后的时刻都需要复用之前定义好的变量。
    if i > 0:
        tf.get_variable_scope().reuse_variable()

    # 每一步处理时间序列中的一个时刻。
    # 将当前输入current_input(图8-7中的Xt)和前一时刻状态state(H(t-1)和C(t-1))传入定义的LSTM结构可以得到当前LSTM的输出lstm_output(Ht)和更新后状态state(Ht和Ct)。
    # lstm_output用于输出给其他层,state用于输出给下一时刻,它们在dropout等方面可以有不同的处理方式。
    lstm_output, state = lstm(current_input, state)
    # 将当前时刻LSTM结构的输出传入一个全连接层得到最后的输出。
    final_output = fully_connected(lstm_output)
    # 计算当前时刻输出的损失
    loss += calc_loss(final_output, excepted_output)

# 使用类似第4章中介绍的方法训练模型。

通过上面这段代码看出,通过TensorFlow可以非常方便地实现使用LSTM结构的循环神经网络,而且不需要用户对LSTM内部结构有深入的了解。

-8.3- 循环神经网络的变种

本节中将介绍一些常用的循环神经网络的变种。

在以上几节中已经完整地介绍了使用LSTM结构的循环神经网络。这一节将再介绍循环神经网络的几个常用变种以及它们所解决的问题,同时也会给出如何使用TensorFlow来实现这些变种。

-8.3.1- 双向循环神经网络和深层循环神经网络
* 双向循环神经网络(bi-directional RNN)

在经典的循环神经网络中,状态的传输是从前往后单向的。然而,在有些问题中,当前时刻的输出不仅和之前的状态有关系,也和之后的状态相关。这时就需要使用双向循环神经网络(bi-directional RNN)来解决这类问题。例如预测一个语句中缺失的单词不仅需要根据前文来判断,也需要根据后面的内容,这时双向循环网络就可以发挥它的作用。

图8-8

双向循环神经网络是由两个独立的循环神经网络叠加在一起组成的。输出由这两个循环神经网络的输出拼接而成。图8-8展示了一个双向循环神经网络的结构图。从图8-8中可以看到,双向循环神经网络的主体结构就是两个单向循环神经网络的结合。在每一个时刻t,输入会同时提供给这两个方向相反的循环神经网络。两个网络独立进行计算,各自产生该时刻的新状态和输出,而双向循环网络的最终输出是这两个单向循环神经网络的输出的简单拼接。两个循环神经网络除方向不同以外,其余结构完全对称。每一层网络中的循环体可以自由选用任意结构,如前面介绍过的简单RNN、LSTM均作为双向循环网络的循环体。双向循环神经网络的前向传播过程和单向的循环神经网络十分类似,这里不再赘述。更多关于双向神经网络的介绍可以参考Mike Schuster和Kuldip K. Paliwal发表的论文《Bidirectionalrecurrent neural networks》。

* 深层循环神经网络(Deep RNN)

深层循环神经网络(Deep RNN)是循环神经网络的另外一种变种。为了增强模型的表达能力,可以在网络中设置多个循环层,将每层循环网络的输出传给下一层进行处理。

图8-9

在图8-2描述的单层循环网络中,每一时刻的输入到输出之间只有一个全连接层,因此在到的路径上是一个很浅的神经网络,从输入中提取抽象信息的能力将受到限制。图8-9给出了深层循环神经网络的结构示意图。从图8-9中可以看到,在一个层的深层循环网络中,每一时刻的输入到输出之间有个循环体,网络因此可以从输入中抽取更加高层的信息。和卷积神经网络类似,每一层的循环体中参数是一致的,而不同层中的参数可以不同。

为了更好地支持深层循环神经网络,TensorFlow中提供了MultiRNNCell类来实现深层循环神经网络的前向传播过程。以下代码展示如何使用这个类:

# 定义一个基本的LSTM网络结构作为循环体的基础结构。
# 深层循环神经网络也支持使用其他的循环体结构。
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell

# 通过MultiRNNCell类实现深层循环神经网络中每一个时刻的前向传播过程。
# 其中,number_of_layers表示有多少层,也就是图8-9中从Xt到Ht需要经过多少个LSTM结构。
# 注意,从TensorFlow 1.1版本起, 不能使用[lstm_cell(lstm_size)]*N的形式来初始化MultiRNNCell,否则TensorFlow会在每一层之间共享参数。
stacked_lstm = tf.nnn.rnn_cell.MultiRNNCell(
        [lstm_cell(lstm_size) for _ in range(number_of_layers)]
    )

# 和经典的循环神经网络一样,可以通过zero_state函数来获取初始状态。
state = stacked_lstm.zero_state(batch_size, tf.float32)

# 和8.2节中给出的代码一样,计算每一时刻的前向传播结果。
for i in range(len(num_steps)):
    if i > 0 :
        tf.get_variable_scope().reuse_variables()
    stacked_lstm_output = fully_connected(stacked_lstm_output)
    loss += calc_loss(final_output, excepted_output)

从以上代码可以看到,在TensorFlow中只需要在BasicLSTMCell的基础上再封装一层MultiRNNCell就可以非常容易地实现深层循环神经网络了。

-8.3.2- 循环神经网络的dropout

6.4节介绍过在卷积神经网络上使用dropout的方法。通过dropout,可以让卷积神经网络更加健壮(robust)。类似的,在循环神经网络中使用dropout也有同样的功能。
而且,类似卷积神经网络只在最后的全连接层中使用dropout,循环神经网络一般只在不同层循环体结构之间使用dropout,而不在同一层的循环体结构之间使用。也就是说从时刻传递到时刻时,循环神经网络不会进行状态的dropout;而在同一个时刻中,不同层循环体之间会使用dropout。

图8-10

图8-10展示了循环神经网络使用dropout的方法。假设要从x_{t-2}t+1y_{t+1}x_{t-2}t-2t-1tt+1t+1$时刻的第一层循环体结构传递到同一时刻内更高层的循环体结构时,会再次使用dropout。

在Tensorflow中,使用tf.nn.rnn_cell.DropoutWrapper类可以很容易实现dropout功能。以下代码展示了如何在TensorFlow中实现带dropout的循环神经网络。

# 定义LSTM结构。
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell

# 使用DropoutWrapper类来实现dropout功能。
# 该类通过两个参数来控制dropout的概率,
# 一个参数为input_keep_prob,它可以用来控制输入的dropout概率;
# 另一个参数为output_keep_prob,它可以用来控制输出的dropout概率。

# 在使用DropoutWrapper的基础上定义MultiRNNCell:
stacked_lstm = run_cell.MultiRNNCell(
        [tf.nn.rnn_cell.DropoutWrapper(lstm_cell(lstm_size)) for _ in range(number_of layers)]
    )

# 和8.3.1节中深层循环神经网络样例程序类似,运行前向传播过程。

-8.4- 循环神经网络样例应用

本节中将结合TensorFlow对这些网络结构的支持,介绍如何时序预测问题设计和使用循环神经网络。

在以上几节中已经介绍了不同循环神经网络的网络结构,井给出了具体的TensorFlow程序来实现这些循环神经网络的前向传播过程。这一节将以时序预测为例,利用循环神经网络实现对函数取值的预测。

图8-11

图8-11给出了函数的函数图像。下面的篇幅将给出具体的TensorFlow程序来实现预测正弦函数。因为循环神经网络模型预测的是离散时刻的取值,所以在程序中需要将连续的函数曲线离散化。所谓离散化就是在一个给定的区间内,通过有限个采样点模拟一个连续的曲线。比如在以下程序中每隔SAMPLE_ITERVAL对函数进行一次来样,来样得到的序列就是函数离散化之后的结果。以下程序为预测离散化之后的函数:

# -*- coding: utf-8 -*-

import numpy as np 
import tensorflow as tf 
# 加载matplotlib工具包,使用该工具可以对预测的sin()函数曲线进行绘图。
import matplotlib as mpl 
mpl.use('Agg')
from matplotlib import pyplot as plt 

HIDDEN_SIZE = 30 
NUM_LAYERS = 2 
TIMESTEPS = 10 
TRAINING_STEPS = 10000
BATCH_SIZE = 32 
TRAINING_EXAMPLES = 10000
TESTING_EXAMPLES = 1000
SAMPLE_GAP = 0.01

def generate_data(seq): 
    X = []
    y = [] 
    # 序列的第i项和后面的TIMESTEPS-1项合在一起作为输入:
    # 第i+TIMESTEPS个点的函数值。
    for i in range(len(seq) - TIMESTEPS):
        X.append([seq[i: i+TIMESTEPS]])
        y.append([seq[i+TIMESTEPS]])
    return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)

def lstm_model(X, y, is_training):
    # 使用多层的LSTM结构。
    cell = tf.nn.rnn_cell.MultiRNNCell(
        [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)]
    )
    
    # 使用TensorFlow接口将多层的LSTM结构连接成RNN网络并计算其前向传播结果。
    output, _ = tf.nnn.dynamic_rnn(cell, X, dtype=tf.float32)
    # outputs是顶层LSTM在每一步的输出结果,它的维度是[batch_size, time, HIDDEN_SIZE]。
    # 在本问题中只关注最后一个时刻的输出结果。
    output = outputs[:, -1, :]

    # 对LSTM网络的输出再做加一层全连接层计算损失。注意这里默认的损失为平均平方差损失函数。
    predictions = tf.contrib.layers.fully_connected(
        output, 
        1, 
        activation_fn=None
    )

    # 只在训练时计算损失函数和优化步骤。
    # 测试时直接返回预测结果。
    if not is_training:
        return predictions, None, None

    # 计算损失函数
    loss = tf.losses.mean_sequared_error(
        labels=y, 
        predictions=predictions
    )

    # 创建模型优化器并得到优化步骤。
    train_op = tf.contrib.layers.optimize_loss(
        loss, 
        tf.train.get_global_step(), 
        optimizer="Agagrad", 
        learning_rate=0.1
    )
    return predictions, loss, train_op 

def train(sess, train_X, train_y):
    # 将训练数据以数据集的方式提供给计算图。
    ds = tf.data.Dataset.from_tensor_slices((train_X, train_y))
    ds = ds.repeat().shuffle(1000).batch(BATCH_SIZE)
    X, y = ds.make_one_shot_iterator().get_next() 

    # 调用模型,得到预测结果、损失函数、和训练操作。
    with tf.variable_scope("model"):
        predictions, loss, train_op = lstm_model(X, y, True)

    # 初始化变量。
    sess.run(tf.gloabl_variables_initializer())
    for i in range(TRAINING_STEPS):
        _, 1 = sess.run([train_op, loss])
        for i % 100 == 0:
            print("train step: " + str(i) + ", loss: ", + str(l))

def run_eval(sess, test_x, test_y):
    #  将测试数据以数据集的方式提供给计算图。
    ds = tf.data.Dataset.from_tensor_slices((test_X, test_y))
    ds = ds.batch(1)
    X, y = ds.make_one_shot_iterator().get_next() 
    
    # 调用模型得到计算结果,这里不需要输入真实的y值
    with tf.variable_scope("model", reuse=True):
        prediction, _, _ = lstm_model(X, [0.0], False)

    # 将预测结果存入一个数组。
    predictions = [] 
    for i in range(TESTING_EXAMPLES):
        p, l = sess.run([prediction, y])
        predictions.append(p)
        labels.append(l)

    # 计算rmse作为评价指标。
    predictions = np.array(predictions).squeeze()
    labels = np.array(labels).squeeze()
    rmse = np.sqrt(((predictions-labels)**2).mean(axis=0))
    print("Mean Seqare Error is: %f" % rmse)

    # 对预测的sin函数曲线进行绘图, 得到的结果如图8-12所示。
    plt.figure()
    plt.plot(predictions, label='predictions')
    plt.plot(labels, label='real_sin')
    plt.legend()
    plt.show()

# 用正弦函数生成训练和测试数据集合。
# numpy.linspace()函数可以创建一个等差序列的数组,它常用的参数有3个,
# 第1个参数表示起始值,第2个参数表示终止值,第3个参数表示数列的长度。例如,linespace(1,10,10)产生的数组是array([1,2,3,4,5,6,7,8,9,10])。
test_start = (TRAINING_EXAMPLES + TIMESTEPS) * SAMPLE_GAP
test_end = test_start + (TESTING_EXAMPLES + TIMESTEPS) * SAMPLE_GAP 
train_X, train_y = generate_data(np.sin(np.linspace(
        0, 
        test_start, 
        TRAINING_EXAMPLES + TIMESTEPS, 
        dtype=tf.float32
    )))

with tf.Session() as sess:
    # 训练模型。
    train(sess, train_X, train_y)
    # 使用训练好的模型对测试数据进行预测。
    run_eval(sess, test_X, test_y)

"""
运行以上程序可以得到输出:
train step: 0, loss: 0.444037
train step: 100, loss: 0.00600867
train step: 200, loss: 0.00496772
train step: 300, loss: 0.00326087
···
train step: 9900, loss: 3.15211e-06
Root Mean Square Error is: 0.001906
从输出可以看出通过循环神经网络可以非常精确的预测正弦函数sin的取值。
"""
图8-12

从图8-12中可以看出,预测得到的结果和真实的函数几乎是重合的。也就是说通过循环神经网络可以非常好地预测函数的取值。

你可能感兴趣的:(TensorFlow 实战Google深度学习框架(第2版)第八章读书笔记)