Python-循环神经网络快速启动指南-全-

Python 循环神经网络快速启动指南(全)

原文:annas-archive.org/md5/e92cc5bb94ebbc685e165e06708ba6e3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

深度学习DL)是一个日益流行的话题,吸引了各大公司以及各种开发者的关注。在过去的五年里,这一领域经历了巨大的进步,最终让我们将 DL 视为一种具有巨大潜力的颠覆性技术。虚拟助手、语音识别和语言翻译只是 DL 技术直接应用的几个例子。与图像识别或物体检测相比,这些应用使用的是顺序数据,其中每个结果的性质都依赖于前一个结果。例如,要将英语句子翻译成西班牙语,你无法不从头到尾追踪每个单词的变化。对于这些问题,正在使用一种特定类型的模型——递归神经网络RNN)。本书将介绍 RNN 的基础知识,并重点讲解如何利用流行的深度学习库 TensorFlow 进行一些实际的实现。所有示例都附有深入的理论解释,帮助你理解这个强大但稍微复杂的模型背后的基本概念。阅读本书后,你将对 RNN 有信心,并能够在你的具体应用场景中使用这个模型。

本书适用对象

本书面向那些希望通过实际案例了解 RNN 的机器学习工程师和数据科学家。

本书内容概览

第一章,递归神经网络简介,将为你简要介绍 RNN 的基础知识,并将该模型与其他流行的模型进行比较,展示为什么 RNN 是最好的。随后,本章将通过一个示例来说明 RNN 的应用,同时还会让你了解 RNN 面临的一些问题。

第二章,使用 TensorFlow 构建你的第一个 RNN,将探讨如何构建一个简单的 RNN 来解决序列奇偶性识别问题。你还将简要了解 TensorFlow 库以及它如何用于构建深度学习模型。阅读完这一章后,你应该能够完全理解如何在 Python 中使用 TensorFlow,并体会到构建神经网络是多么简单直接。

第三章,生成你自己的书籍章节,将介绍一种更强大的 RNN 模型——门控递归单元GRU)。你将了解它是如何工作的,以及为什么我们选择它而不是简单的 RNN。你还将逐步了解生成书籍章节的过程。通过这一章的学习,你将获得理论和实践上的双重知识,这将使你能够自由地尝试解决中等难度的任何问题。

第四章,创建西班牙语到英语的翻译器,将引导你通过使用 TensorFlow 库实现的序列到序列模型构建一个相当复杂的神经网络模型。你将构建一个简单的西班牙语到英语的翻译器,它可以接受西班牙语句子并输出相应的英语翻译。

第五章,构建个人助手,将探讨 RNN 的实际应用,并指导你构建一个对话聊天机器人。本章展示了一个完整实现的聊天机器人系统,能够构建一个简短的对话。然后,你将创建一个端到端模型,旨在产生有意义的结果。你将使用一个基于 TensorFlow 的高阶库——TensorLayer。

第六章,提升 RNN 的性能,将介绍一些提高 RNN 性能的技巧。本章将专注于通过数据和调优来提升 RNN 的性能。你还将探索如何优化 TensorFlow 库以获得更好的结果。

充分利用本书

你需要具备基本的 Python 3.6.x 知识和 Linux 命令的基础知识。此前使用过 TensorFlow 的经验将会有帮助,但并非必须。

下载示例代码文件

你可以从www.packt.com账户下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packt.com/support,注册后将文件直接发送到你的邮箱。

你可以通过以下步骤下载代码文件:

  1. 登录或注册到www.packt.com

  2. 选择“SUPPORT”标签。

  3. 点击“Code Downloads & Errata”。

  4. 在搜索框中输入书名,按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Recurrent-Neural-Networks-with-Python-Quick-Start-Guide。如果代码有更新,它将会在现有的 GitHub 仓库中同步更新。

我们还有其他来自丰富书籍和视频目录的代码包,访问github.com/PacktPublishing/。赶紧去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图片。你可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789132335_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“将下载的 WebStorm-10*.dmg 磁盘映像文件挂载为系统中的另一个磁盘。”

代码块以如下方式显示:

def generate_data():
    inputs = input_values()
    return inputs, output_values(inputs)

当我们希望你特别注意某段代码时,相关行或项目会以粗体显示:

 loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=Y,    
    logits=prediction)
 total_loss = tf.reduce_mean(loss)

任何命令行输入或输出都以如下方式书写:

import tensorflow as tf
import random

粗体:表示新术语、重要词汇或屏幕上显示的词语。例如,菜单或对话框中的词语会像这样出现在文本中。这里有一个例子:“从管理面板中选择系统信息。”

警告或重要说明通常以这种方式显示。

提示和技巧通常以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果你对本书的任何内容有疑问,请在邮件的主题中提及书名,并通过customercare@packtpub.com联系我们。

勘误:虽然我们已经尽力确保内容的准确性,但难免会出现错误。如果你发现本书中的错误,我们非常感激你能向我们报告。请访问 www.packt.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关信息。

盗版:如果你在互联网上发现我们的作品的非法复制品,无论是何种形式,我们非常感激你能提供该材料的链接地址或网站名称。请通过copyright@packt.com与我们联系,并提供该资料的链接。

如果你有兴趣成为作者:如果你在某个领域有专长,且有意写作或参与编写书籍,请访问 authors.packtpub.com

书评

请留下书评。阅读并使用本书后,不妨在你购买本书的网站上留下评价。潜在读者可以查看并根据你的公正意见做出购买决策,我们也可以了解你对我们产品的看法,我们的作者则能看到你对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packt.com

第一章:引入循环神经网络

本章将介绍循环神经网络RNN)模型的理论部分。了解这种强大架构背后的原理将帮助你更好地掌握本书后续提供的实际示例。由于你可能会经常遇到需要为应用做出关键决策的情况,因此了解这个模型的构成部分是至关重要的。这将帮助你在面对不同情况时做出合适的反应。

本章的前提知识包括基础的线性代数(矩阵运算)。对深度学习和神经网络有一定了解也是一个加分项。如果你是该领域的新手,我建议你首先观看 Andrew Ng 制作的优秀视频系列(www.youtube.com/playlist?list=PLkDaE6sCZn6Ec-XTbcX1uRg2_u4xOEky0);这些视频将帮助你迈出第一步,为你扩展知识做好准备。阅读本章后,你将能够回答如下问题:

  • 什么是 RNN?

  • 为什么 RNN 比其他解决方案更好?

  • 如何训练 RNN?

  • RNN 模型有哪些问题?

什么是 RNN?

RNN 是深度学习家族中一个强大的模型,在过去五年中取得了令人惊讶的成果。它通过利用强大的基于记忆的架构,旨在对顺序数据进行预测。

但它与标准神经网络有何不同?普通(也叫前馈)神经网络像一个映射函数,其中一个输入对应一个输出。在这种架构中,没有两个输入共享知识,每个输入只沿着一个方向移动——从输入节点开始,经过隐藏节点,最终到达输出节点。下面是上述模型的示意图:

相反,循环神经网络(也叫反馈神经网络)使用额外的记忆状态。当输入 A[1](单词I)被加入时,网络会产生输出 B[1](单词love)并将输入 A[1]的信息存储在记忆状态中。当下一个输入 A[2](单词love)加入时,网络会借助记忆状态生成关联的输出 B[2](单词to)。然后,记忆状态会使用来自新输入 A[2]的信息进行更新。这个操作会对每个输入重复进行:

你可以看到,在这种方法中,我们的预测不仅依赖于当前输入,还依赖于之前的数据。这就是为什么 RNN 是处理序列问题的最先进模型。让我们通过一些例子来说明这一点。

典型的前馈网络应用案例是图像识别。我们可以看到它在农业中的应用,例如分析植物,在医疗保健中用于疾病诊断,以及在无人驾驶汽车中用于检测行人。由于这些例子中的任何输出都不需要来自先前输入的特定信息,前馈网络非常适合这种类型的问题。

还有另一类问题,基于序列数据。在这些情况下,预测序列中的下一个元素依赖于所有先前的元素。以下是一些示例:

  • 文本转语音

  • 预测句子中的下一个词

  • 将音频转换为文本

  • 语言翻译

  • 视频字幕生成

RNN 最早是在 1980 年代通过霍普菲尔德网络的发明而提出的。后来,在 1997 年,Hochreiter 和 Schmidhuber 提出了一个先进的 RNN 模型,叫做 长短时记忆LSTM)。它旨在解决一些最简单的递归神经网络模型存在的主要问题,这些问题将在本章稍后揭示。2014 年,Chung 等人提出了 RNN 系列的另一个改进。这种新架构叫做门控递归单元(GRU),以更简单的方式解决了与 LSTM 相同的问题。

在本书的下一章中,我们将介绍上述模型,了解它们的工作原理,并探讨为什么研究人员和大公司每天都在使用它们来解决基本问题。

比较递归神经网络与类似模型

近年来,类似于任何神经网络模型,RNN 因为更容易访问大量结构化数据和计算能力的提升而变得广泛流行。但研究人员通过其他方法(如隐马尔可夫模型)已经解决序列问题几十年了。我们将简要地将这种技术与 RNN 进行比较,并概述两种方法的优点。

隐马尔可夫模型HMM)是一种概率序列模型,旨在为序列中的每个元素分配一个标签(类别)。HMM 计算每个可能序列的概率,并选择最可能的一个。

HMM 和 RNN 都是强大的模型,能够产生惊人的结果,但根据使用场景和可用资源,RNN 可以更为有效。

隐马尔可夫模型

以下是隐马尔可夫模型在解决序列相关任务时的优缺点:

  • 优点: 实现起来较为简单,且在中等难度问题上能像 RNN 一样更快速、高效地工作。

  • 缺点: 随着精度要求的提高,HMM 会变得指数级昂贵。例如,预测句子中的下一个词可能依赖于一个很久之前的词。HMM 需要执行一些昂贵的操作来获取这些信息。这也是该模型不适合处理需要大量数据的复杂任务的原因。

这些昂贵的操作包括计算相对于序列中所有先前元素的每个可能元素的概率。

递归神经网络

以下是递归神经网络在解决与序列相关任务时的优缺点:

  • 优点:在处理复杂任务和大量数据时,表现显著更好,且成本较低。

  • 缺点:构建适合特定问题的正确架构较为复杂。如果准备的数据相对较小,结果不会更好。

通过我们的观察,可以得出结论,RNN 正在逐渐取代大多数实际应用中的 HMM。我们应该了解这两种模型,但在正确的架构和数据下,RNN 往往是更好的选择。

然而,如果你有兴趣深入了解隐马尔可夫模型,我强烈建议你观看一些视频系列(www.youtube.com/watch?v=TPRoLreU9lA)和一些应用实例的论文,如 Degirmenci(哈佛大学)撰写的《隐马尔可夫模型简介》(scholar.harvard.edu/files/adegirmenci/files/hmm_adegirmenci_2014.pdf)或《隐马尔可夫模型在语音处理中的问题与局限性:综述》(pdfs.semanticscholar.org/8463/dfee2b46fa813069029149e8e80cec95659f.pdf)。

理解递归神经网络的工作原理

使用记忆状态,RNN 架构完美地解决了每一个基于序列的问题。在这一章节中,我们将全面解释其工作原理。你将了解神经网络的基本特征,以及 RNN 的独特之处。本节重点讲解理论部分(包括数学公式),但我可以保证,一旦你掌握了基础,任何实际案例都将顺利进行。

为了让解释更易于理解,我们来讨论一下生成文本的任务,特别是基于我最喜欢的书籍系列之一《饥饿游戏》(The Hunger Games)由 Suzanne Collins 编写的,创作一个新章节。

基本的神经网络概述

在最高层次上,解决监督问题的神经网络的工作方式如下:

  1. 获取训练数据(例如图像用于图像识别或句子用于生成文本)

  2. 编码数据(神经网络处理的是数字,因此需要数据的数字表示)

  3. 构建神经网络模型的架构

  4. 训练模型直到你对结果满意为止

  5. 通过做出一个全新的预测来评估你的模型

让我们看看这些步骤如何应用于 RNN。

获取数据

对于基于《饥饿游戏》系列书籍生成新章节的问题,你可以通过复制和粘贴的方式提取《饥饿游戏》系列所有书籍中的文本(《饥饿游戏》,《嘲笑鸟》和《燃烧的旗帜》)。为此,你需要在网上找到这些书籍和内容。

编码数据

我们使用 词嵌入www.analyticsvidhya.com/blog/2017/06/word-embeddings-count-word2veec/)来实现这一目标。词嵌入是将词汇表中的单词或短语映射到实数向量的一种集体称谓。一些方法包括 独热编码word2vecGloVe。你将在接下来的章节中了解更多关于它们的信息。

构建架构

每个神经网络由三组层组成——输入层、隐藏层和输出层。总是有一个输入层和一个输出层。如果神经网络较深,则会有多个隐藏层:

RNN 和标准前馈网络的区别在于其循环隐藏状态。正如下图所示,循环神经网络使用循环隐藏状态。这样,数据从一个时间步传播到另一个时间步,使得每一个时间步都依赖于前一个时间步:

一种常见的做法是展开上述图示,以便更好、更流畅地理解。通过将插图垂直旋转并添加一些符号和标签,基于我们之前选择的示例(基于《饥饿游戏》书籍生成新章节),我们最终得到了以下图示:

这是一个展开的 RNN,具有一个隐藏层。看似相同的(输入 + 隐藏 RNN 单元 + 输出)集合实际上是 RNN 中的不同时间步(或循环)。例如,组合 + RNN + 展示了在时间步 时发生的情况。在每个时间步,这些操作执行如下:

  1. 网络使用任何词嵌入技术对当前时间步(例如,t-1)的词进行编码,并生成一个向量 (生成的向量可以是 ,具体取决于时间步的不同)

  2. 然后,,输入单词I在时间步* t-1 *的编码版本,被插入到 RNN 单元(位于隐藏层)。经过若干方程(此处未显示,但在 RNN 单元内部发生),单元生成输出 和一个记忆状态 。记忆状态是输入 和该记忆状态的前一个值 的结果。对于初始时间步,可以假设 是一个零向量。

  3. 在时间步* t-1 时生成实际的单词(志愿者)发生在解码输出 时,使用的是训练开始时指定的文本语料库*。

  4. 最后,网络继续向前推进多个时间步,直到达到最终步骤,在那里它预测出单词。

你可以看到每一个{…, , …}都包含有关所有先前输入的信息。这使得 RNN(循环神经网络)非常特殊,且在预测序列中的下一个单元时表现得尤为出色。现在让我们来看一下支撑这些操作的数学方程。

文本语料库——示例词汇表中所有单词的数组。

训练模型

该模型的所有“魔法”都在于 RNN 单元。在我们简单的例子中,每个单元呈现相同的方程,只是变量集不同。一个单元的详细版本如下所示:

首先,让我们解释前面图表中出现的新术语:

  • 权重, , ):权重是一个矩阵(或数字),表示它所应用的值的强度。例如,决定了输入 在后续方程中应该被考虑的程度。

    如果 包含较高的值,那么 应该对最终结果有显著的影响。权重值通常是随机初始化的,或者使用某种分布(例如正态/高斯分布)。需要注意的是, , ,以及  在每个步骤中都是相同的。通过反向传播算法,它们会被修改,目的是产生准确的预测。

  • 偏置 (, ): 一个偏移向量(每一层不同),它将一个变化加到输出值上!

  • 激活函数 (tanh): 这个函数决定当前记忆状态  和输出  的最终值。基本上,激活函数将类似以下的多个方程式的结果值映射到期望的范围:如果使用 tanh 函数,则范围为 (-1, 1);如果使用 sigmoid 函数,则范围为 (0, 1);如果使用 ReLu,则范围为 (0, +infinity) (ai.stackexchange.com/questions/5493/what-is-the-purpose-of-an-activation-function-in-neural-networks)

现在,让我们回顾一下计算变量的过程。为了计算  和 ,我们可以做如下操作:

如你所见,记忆状态  是前一个值  和输入  的结果。使用此公式有助于保留关于所有先前状态的信息。

输入  是单词 volunteer 的独热编码表示。回想一下,独热编码是一种词嵌入方式。如果文本语料库包含 20,000 个独特的单词,且 "volunteer" 是第 19 个单词,那么  是一个 20,000 维的向量,所有元素为 0,除了第 19 个位置,其值为 1,表示我们只考虑这个特定的单词。

通过将 、  和  相加,结果被传递到 tanh 激活函数,该函数使用以下公式将结果压缩到 -11 之间:

在此,e = 2.71828(欧拉数),z 是任何实数。

时间步 t 时的输出 是通过 softmax 函数计算得出的。这个函数可以归类为激活函数,但与其他激活函数不同的是,它主要用于输出层,尤其是在需要概率分布时。例如,在分类问题中,预测正确结果可以通过从一个所有元素总和为1的向量中选取概率最高的值来实现。Softmax 就是生成这种向量的函数,具体如下:

在此,e = 2.71828(欧拉数),z 是一个 K 维向量。该公式计算了向量 z 中i^(th)位置值的概率。

应用softmax函数后,变成了与(语料库大小20,000)相同维度的向量,且所有元素的总和为1。有了这一点,从文本语料库中找到预测的单词变得非常简单。

评估模型

一旦对序列中的下一个单词做出假设,我们需要评估这个预测有多准确。为了做到这一点,我们需要将预测的单词 与训练数据中的实际单词(我们称之为 )进行比较。这个操作可以通过损失(代价)函数来完成。这些函数旨在找到预测值与实际值之间的误差。我们选择的函数是交叉熵损失函数,公式如下:

由于我们不会对这个公式进行详细解释,你可以把它当作一个黑箱。如果你对它是如何工作的感兴趣,建议阅读 Michael Nielson 写的文章《改进神经网络的工作方式》(neuralnetworksanddeeplearning.com/chap3.html#introducing_the_cross-entropy_cost_function)。有用的信息是,交叉熵函数在分类问题中表现得非常好。

在计算完误差后,我们进入了深度学习中最复杂且最强大的技术之一——反向传播。

简单来说,我们可以这样表述:反向传播算法会在更新网络的权重和偏置时,逆向遍历所有(或多个)时间步。经过多次重复这一过程,并进行一定数量的训练步骤后,网络能够学习到正确的参数,并能够产生更好的预测结果。

为了澄清任何混淆,训练和时间步骤是完全不同的术语。在一个时间步骤中,我们从序列中获得一个元素并预测下一个元素。一个训练步骤由多个时间步骤组成,时间步骤的数量取决于该训练步骤的序列长度。此外,时间步骤仅在 RNN 中使用,而训练步骤是一个通用的神经网络概念。

每个训练步骤后,我们可以看到来自损失函数的值减小。一旦它超过某个阈值,我们可以说网络已经成功学会了预测文本中的新词。

最后的步骤是生成新的章节。这可以通过选择一个随机词作为开始(例如:games),然后使用先前的公式和预训练的权重与偏置预测下一个词。最终,我们应该得到一些有意义的文本。

标准递归神经网络模型的关键问题

希望现在你已经对递归神经网络的工作原理有了很好的理解。不幸的是,这个简单的模型在处理更长和更复杂的序列时无法做出好的预测。其背后的原因在于所谓的梯度消失/爆炸问题,这使得网络无法高效地学习。

如你所知,训练过程通过反向传播算法更新权重和偏置。让我们进一步深入数学解释。为了知道该调整多少参数(权重和偏置),网络计算损失函数相对于当前参数值的导数(在每个时间步骤)。当对多个时间步骤使用相同的参数集进行此操作时,导数的值可能会变得过大或过小。由于我们用它来更新参数,过大的值可能导致权重和偏置未定义,过小的值则可能导致没有显著更新,从而没有学习

导数是表示变化率的一种方式;也就是说,它表示函数在某个特定点的变化量。在我们的例子中,这是损失函数相对于给定权重和偏置的变化率。

这个问题最早由 Bengio 等人在 1994 年提出,这导致了 LSTM 网络的引入,旨在解决梯度消失/爆炸问题。稍后在本书中,我们将揭示 LSTM 是如何以出色的方式解决这一问题的。另一个同样克服这一挑战的模型是门控递归单元。在第三章,生成你的章节,你将看到如何做到这一点。

想了解更多关于梯度消失/爆炸问题的信息,建议回顾斯坦福大学的《深度学习自然语言处理》课程第 8 讲(www.youtube.com/watch?v=Keqep_PKrY8)和论文《训练递归神经网络的困难》(proceedings.mlr.press/v28/pascanu13.pdf)。

总结

在本章中,我们通过理论解释和一个具体示例介绍了递归神经网络模型。目的是掌握这个强大系统的基础知识,以便更好地理解编程练习。总体来说,本章包括了以下内容:

  • RNN 简要介绍

  • RNN 与其他流行模型的区别

  • 通过一个示例说明 RNN 的使用

  • 标准 RNN 的主要问题

在下一章中,我们将通过第一个实际的递归神经网络练习进行讲解。你将了解流行的 TensorFlow 库,这使得构建机器学习模型变得容易。接下来的部分将为你提供一个很好的第一次实践经验,并为解决更复杂的问题做好准备。

外部链接

第二章:使用 TensorFlow 构建你的第一个 RNN

在本章中,你将获得构建循环神经网络RNN)的实践经验。首先,你将学习最广泛使用的机器学习库——TensorFlow。从学习基础知识到掌握一些基本技术,你将合理理解如何将这个强大的库应用于你的应用中。然后,你将开始一个相对简单的任务,构建一个实际的模型。这个过程将向你展示如何准备数据、训练网络并进行预测。

总结一下,本章的主题包括以下内容:

  • 你将要构建什么?:你的任务介绍

  • TensorFlow 简介:开始学习 TensorFlow 框架的第一步

  • 编写 RNN 代码:你将经历编写第一个神经网络的过程。包括完成解决方案所需的所有步骤

本章的前提条件是基本的 Python 编程知识,以及对循环神经网络的基本理解,参见第一章,介绍循环神经网络。阅读本章后,你应该能够全面了解如何使用 Python 和 TensorFlow,并明白构建神经网络是多么简单直接。

你将要构建什么?

你进入实际应用领域的第一步将是构建一个简单的模型,该模型用于确定一个比特序列的奇偶性 (mathworld.wolfram.com/Parity.html)。这是 OpenAI 在 2018 年 1 月发布的一个热身练习 (blog.openai.com/requests-for-research-2/)。这个任务可以这样解释:

给定一个长度为50的二进制字符串,确定其中是否包含偶数个或奇数个 1。如果该数字是偶数,则输出0,否则输出1

本章稍后将详细解释解决方案,并讨论一些难点及其应对方法。

TensorFlow 简介

TensorFlow 是 Google 构建的一个开源库,旨在帮助开发人员创建各种类型的机器学习模型。深度学习领域的最新进展促使了对一种易于快速构建神经网络的方法的需求。TensorFlow 通过提供丰富的 API 和工具来解决这个问题,帮助开发者专注于他们的具体问题,而不必处理数学方程和可扩展性问题。

TensorFlow 提供了两种主要的模型编程方式:

  • 基于图的执行

  • 急切执行

基于图的执行

基于图的执行是一种表示数学方程和函数的替代方式。考虑表达式 a = (bc) + (de),我们可以用图形表示如下:

  1. 将表达式分解为以下部分:

    • x = bc*

    • y = de*

    • a = x+y

  2. 构建以下图形:

从之前的示例可以看出,使用图形可以并行计算两个方程。这样,代码可以分布到多个 CPU/GPU 上。

该示例的更复杂变体被用于 TensorFlow 中训练大型模型。按照这种方法,基于图的 TensorFlow 执行在构建神经网络时需要一个两步法。首先应构建图架构,然后执行它以获取结果。

这种方法使你的应用程序运行得更快,并且能够分布到多个 CPU、GPU 等设备上。不幸的是,它也带来了一些复杂性。理解这种编程方式是如何工作的,以及无法像以前那样调试代码(例如,在程序的任何点打印值),使得基于图的执行(更多细节请见 smahesh.com/blog/2017/07/10/understanding-tensorflow-graph/)对于初学者来说有些挑战。

尽管这种技术可能会引入一种新的编程方式,我们的示例将基于它。做出这个决定的原因在于,外面有更多的资源,而且几乎你遇到的每一个 TensorFlow 示例都是基于图的。此外,我认为理解基础知识至关重要,即使它们引入了不熟悉的技术。

急切执行

急切执行是一种由 Google 最近推出的方法,正如文档中所述 (www.tensorflow.org/guide/eager),它使用以下内容:

一种命令式编程环境,立即评估操作,而不是构建图形:操作返回具体的值,而不是构建一个计算图以便稍后运行。这使得开始使用 TensorFlow 和调试模型变得更加容易,同时也减少了模板代码。

如你所见,学习这种新的编程技术没有额外负担,调试也很顺畅。为了更好地理解,我建议查看 TensorFlow 2018 年大会的这篇教程 (www.youtube.com/watch?v=T8AW0fKP0Hs)。

我必须声明,一旦你学会如何操作 TF API,在图计算和急切执行(eager execution)上构建模型会变得非常容易。如果一开始前者看起来很复杂,不用慌张——我可以向你保证,花时间理解它是值得的。

编写递归神经网络代码

如前所述,我们的任务目标是构建一个递归神经网络,用于预测比特序列的奇偶性。我们将以略有不同的方式来处理这个问题。由于序列的奇偶性取决于 1 的数量,我们将对序列的元素进行求和,找出结果是否为偶数。如果是偶数,我们将输出0,否则输出1

本章的这一部分包括代码示例,并执行以下步骤:

  • 生成用于训练模型的数据

  • 构建 TensorFlow 图(使用 TensorFlow 内置的递归神经网络函数)

  • 使用生成的数据训练神经网络

  • 评估模型并确定其准确性

生成数据

让我们重新审视 OpenAI 的任务(blog.openai.com/requests-for-research-2/)。如文中所述,我们需要生成一个包含 100,000 个长度为 50 的随机二进制字符串的数据集。换句话说,我们的训练集将由 100,000 个示例组成,递归神经网络将接受 50 个时间步长。最后一个时间步的结果将被视为模型的预测值。

确定序列和求和任务可以被视为一个分类问题,其中结果可以是050之间的任何类别。机器学习中的一种标准做法是将数据编码为易于解码的数字格式。那为什么要这么做呢?大多数机器学习算法只能接受数字数据,因此我们总是需要对输入/输出进行编码。这意味着我们的预测也将以编码格式输出。因此,理解这些预测背后的实际值是至关重要的。这意味着我们需要能够轻松地将它们解码为人类可理解的格式。一种常见的分类问题数据编码方式是独热编码(one-hot encoding)。

这是该技术的一个示例。

假设某个特定序列的预测输出为30。我们可以通过引入一个1x50的数组来编码这个数字,其中除了第 30 个位置上的数字,其余位置都是0——[0, 0,..., 0, 1, 0, ..., 0, 0, 0]

在准备实际数据之前,我们需要导入所有必需的库。为此,请访问此链接(www.python.org/downloads/)来在你的计算机上安装 Python。在命令行/终端窗口中安装以下软件包:

pip3 install tensorflow

完成这一步后,创建一个名为ch2_task.py的新文件,并导入以下库:

import tensorflow as tf
import random

准备数据需要输入和输出值。输入值是一个三维数组,大小为[100000, 50, 1],包含100000个项目,每个项目包含50个一元素数组(值为01),示例如下:

[ [ [1], [0], [1], [1], …, [0], [1] ]
[ [0], [1], [0], [1], …, [0], [1] ]
[ [1], [1], [1], [0], …, [0], [0] ]
[ [1], [0], [0], [0], …, [1], [1] ] ]

以下示例展示了实现过程:

num_examples = 100000
num_classes = 50

def input_values():
    multiple_values = [map(int, '{0:050b}'.format(i)) for i in range(2**20)]
    random.shuffle(multiple_values)
    final_values = []
    for value in multiple_values[:num_examples]:
        temp = []
        for number in value:
            temp.append([number])
        final_values.append(temp)
    return final_values

这里,num_classes是我们 RNN 中的时间步数(在这个例子中是50)。前面的代码返回一个包含100000个二进制序列的列表。虽然这种写法不是特别符合 Python 的风格,但这样写使得跟踪和理解变得更加容易。

首先,我们从初始化multiple_values变量开始。它包含了前2²⁰=1,048,576个数字的二进制表示,其中每个二进制数都用零填充以适应50的长度。获得如此多的示例可以最小化任何两个之间的相似性。我们使用map函数与int结合,目的是将生成的字符串转换为数字。

这是一个简短的示例,说明它是如何工作的。我们想要在multiple_values数组中表示数字22的二进制版本是'10',所以在'{0:050b}'.format(i)中,i = 2生成的字符串是'00000000000000000000000000000000000000000000000010'(前面有 48 个零以适应长度为50)。最后,map函数将这个字符串转换为数字,并且不会去掉前面的零。

然后,我们打乱multiple_values数组,确保相邻元素之间有所不同。这在反向传播过程中非常重要,因为我们在训练网络时是逐步遍历数组,并在每一步使用单个示例来训练网络。如果数组中相似的值彼此紧挨着,可能会导致偏倚结果和不正确的未来预测。

最后,我们进入一个循环,遍历所有的二进制元素,并构建一个与之前看到的类似的数组。需要注意的是num_examples的使用,它会对数组进行切片,因此我们只选择前100,000个值。

本部分的第二部分展示了如何生成预期的输出(输入集中每个列表中所有元素的总和)。这些输出用于评估模型并在反向传播过程中调整weight/biases。以下示例展示了实现方式:

def output_values(inputs):
    final_values = []
    for value in inputs:
        output_values = [0 for _ in range(num_classes)]
        count = 0
        for i in value:
            count += i[0]
        if count < num_classes:
            output_values[count] = 1
        final_values.append(output_values)
    return final_values

inputs参数是我们之前声明的input_values()的结果。output_values()函数返回inputs中每个成员的独热编码表示列表。如果[[0], [1], [1], [1], [0], ..., [0], [1]]序列中所有元素的总和为48,那么它在output_values中的对应值就是[0, 0, 0, ..., 1, 0, 0],其中1位于第48的位置。

最后,我们使用generate_data()函数获取网络输入和输出的最终值,如下例所示:

def generate_data():
    inputs = input_values()
    return inputs, output_values(inputs)

我们使用之前的函数来创建这两个新变量:input_valuesoutput_values = generate_data()。需要注意的是这些列表的维度:

  • input_values的大小为[num_examples, num_classes, 1]

  • output_values的大小为[num_examples, num_classes]

其中,num_examples = 100000num_classes = 50

构建 TensorFlow 图

构建 TensorFlow 图可能是构建神经网络中最复杂的部分。我们将仔细检查所有步骤,确保你能够全面理解。

TensorFlow 图可以看作是递归神经网络模型的直接实现,包括在第一章中介绍的所有方程和算法,引入递归神经网络

首先,我们从设置模型的参数开始,如下例所示:

X = tf.placeholder(tf.float32, shape=[None, num_classes, 1])
Y = tf.placeholder(tf.float32, shape=[None, num_classes])
num_hidden_units = 24
weights = tf.Variable(tf.truncated_normal([num_hidden_units, num_classes]))
biases = tf.Variable(tf.truncated_normal([num_classes]))

XY 被声明为 tf.placeholder,这会在图中插入一个占位符(用于一个始终会被馈送的张量)。占位符用于在训练网络时预期接收数据的变量。它们通常保存网络训练输入和预期输出的值。你可能会对其中一个维度是 None 感到惊讶。原因是我们在训练网络时使用了批量。批量是由多个来自训练数据的元素堆叠在一起形成的集合。当指定维度为 None 时,我们让张量决定这个维度,并通过其他两个值来计算它。

根据 TensorFlow 文档:张量是对向量和矩阵的一个推广,允许更高维度的表示。在 TensorFlow 内部,张量被表示为 n 维数组,元素类型是基本数据类型。

在使用批量进行训练时,我们将训练数据拆分成若干个小的数组,每个数组的大小为 batch_size。然后,我们不再一次性使用所有示例来训练网络,而是一次使用一个批量。

这样做的优点是,所需的内存更少,学习速度更快。

weightbiases 被声明为 tf.Variable,它在训练过程中持有某个值,这个值是可以修改的。当一个变量首次引入时,应该指定其初始值、类型和形状。类型和形状保持不变,不能更改。

接下来,让我们构建 RNN 单元。如果你回忆起第一章,引入递归神经网络,在时间步长 t 时,输入被传入 RNN 单元,以产生一个输出,,以及一个隐藏状态,。然后,隐藏状态和时间步长 *t*+1 时的新输入被传入一个新的 RNN 单元(该单元与前一个共享相同的权重和偏差)。它会产生自己的输出,,以及隐藏状态,。这个模式会在每个时间步长中重复。

使用 TensorFlow,之前的操作仅需要一行代码:

rnn_cell = tf.contrib.rnn.BasicRNNCell(num_units=num_hidden_units)

正如你已经知道的,每个单元都需要一个应用于隐状态的激活函数。默认情况下,TensorFlow 选择tanh(非常适合我们的用例),但你可以指定任何你想要的函数。只需添加一个名为activation的额外参数。

weightsrnn_cell中,你可以看到一个名为num_hidden_units的参数。正如这里所述(stackoverflow.com/questions/37901047/what-is-num-units-in-tensorflow-basiclstmcell),num_hidden_units是神经网络学习能力的直接表现。它决定了记忆状态的维度,,以及输出的维度,

下一步是生成网络的输出。这也可以通过一行代码实现:

outputs, state = tf.nn.dynamic_rnn(rnn_cell, inputs=X, dtype=tf.float32)

由于X是一个输入序列的批次,因此outputs表示每个时间步在所有序列中的输出批次。为了评估预测,我们需要批次中每个输出的最后一个时间步的值。这可以通过以下三步来实现,如下所述:

  • 我们从最后一个时间步获得值:outputs = tf.transpose(outputs, [1, 0, 2])

这将把输出的张量从(1000, 50, 24)重塑为(50, 1,000, 24),以便可以使用以下方法获取每个序列中最后一个时间步的输出:last_output = tf.gather(outputs, int(outputs.get_shape()[0]) - 1)

让我们回顾以下图表,以理解如何获得这个last_output

前面的图表展示了如何将一个输入示例的50个时间步输入到网络中。这个操作应该对每个具有50个时间步的独立示例执行1,000次,但为了简便起见,我们这里只展示了一个示例。

在迭代地遍历每个时间步后,我们产生50个输出,每个输出的维度为(24, 1)。因此,对于一个具有50个输入时间步的示例,我们会产生50个输出时间步。将所有输出以数学形式表示时,得到一个(1,000, 50, 24)的矩阵。矩阵的高度为1,000——即单独的示例数量。矩阵的宽度为50——即每个示例的时间步数量。矩阵的深度为24——即每个元素的维度。

为了做出预测,我们只关心每个示例中的output_last,由于示例的数量为1,000,我们只需要1,000个输出值。正如在前面的示例中所见,我们将矩阵(1000, 50, 24)转置为(50, 1000, 24),这样可以更容易地从每个示例中获得output_last。然后,我们使用tf.gather来获取last_output张量,其大小为(1000, 24, 1)。

构建图的最后几行包括:

  • 我们预测特定序列的输出:
     prediction = tf.matmul(last_output, weights) + biases

使用新获得的张量last_output,我们可以利用权重和偏置来计算预测值。

  • 我们根据期望值来评估输出:
    loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=Y,    
    logits=prediction)
    total_loss = tf.reduce_mean(loss)

我们可以将流行的交叉熵损失函数与softmax结合使用。如果你还记得第一章《介绍递归神经网络》中的内容,softmax函数会将张量转换为强调最大值并抑制显著低于最大值的值。这是通过将初始数组中的值归一化为总和为1的数值来实现的。例如,输入[0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3]变为[0.125, 0.138, 0.153, 0.169, 0.125, 0.138, 0.153]。交叉熵是一个损失函数,用于计算label(期望值)与logits(预测值)之间的差异。

由于tf.nn.softmax_cross_entropy_with_logits_v2返回一个长度为batch_size(在下面声明)的 1 维张量,我们使用tf.reduce_mean来计算该张量中所有元素的平均值。

最后一步,我们将看到 TensorFlow 如何简化我们优化权重和偏置的过程。一旦我们获得了损失函数,就需要执行反向传播算法,调整权重和偏置以最小化损失。这可以通过以下方式完成:

learning_rate = 0.001
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss=total_loss)

learning_rate是模型的超参数之一,在优化损失函数时使用。调节这个值对提高性能至关重要,因此可以随意调整并评估结果。

最小化损失函数的误差是通过使用 Adam 优化器完成的。这里的(stats.stackexchange.com/questions/184448/difference-between-gradientdescentoptimizer-and-adamoptimizer-tensorflow)提供了一个很好的解释,说明了为什么它优于梯度下降法。

我们刚刚构建了递归神经网络的架构。现在,让我们将所有内容整合起来,如下例所示:

X = tf.placeholder(tf.float32, shape=[None, num_classes, 1])
Y = tf.placeholder(tf.float32, shape=[None, num_classes])

num_hidden_units = 24

weights = tf.Variable(tf.truncated_normal([num_hidden_units, num_classes]))
biases = tf.Variable(tf.truncated_normal([num_classes]))

rnn_cell = tf.contrib.rnn.BasicRNNCell(num_units=num_hidden_units, activation=tf.nn.relu)
outputs1, state = tf.nn.dynamic_rnn(rnn_cell, inputs=X, dtype=tf.float32)
outputs = tf.transpose(outputs1, [1, 0, 2])

last_output = tf.gather(outputs, int(outputs.get_shape()[0]) - 1)
prediction = tf.matmul(last_output, weights) + biases

loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=Y, logits=prediction)
total_loss = tf.reduce_mean(loss)

learning_rate = 0.001
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss=total_loss)

下一步任务是使用 TensorFlow 图与之前生成的数据结合训练神经网络。

训练 RNN

在本节中,我们将讲解 TensorFlow 程序的第二部分——执行带有预定义数据的计算图。为了实现这一点,我们将使用Session对象,它封装了一个执行张量对象的环境。

我们训练的代码如下所示:

batch_size = 1000
number_of_batches = int(num_examples/batch_size)
epoch = 100
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer()) 
    X_train, y_train = generate_data()
    for epoch in range(epoch):
        iter = 0
        for _ in range(number_of_batches):
            training_x = X_train[iter:iter+batch_size]
            training_y = y_train[iter:iter+batch_size]
            iter += batch_size
            _, current_total_loss = sess.run([optimizer, total_loss], 
            feed_dict={X: training_x, Y: training_y})
            print("Epoch:", epoch, "Iteration:", iter, "Loss", current_total_loss)
            print("__________________")

首先,我们初始化批次大小。在每次训练步骤中,网络会根据所选批次中的示例进行调优。然后,我们计算批次数量以及迭代次数——这决定了我们的模型应该遍历训练集多少次。tf.Session()将代码封装在 TensorFlow 的Session中,而sess.run(tf.global_variables_initializer())stackoverflow.com/questions/44433438/understanding-tf-global-variables-initializer)确保所有变量都保持其值。

然后,我们将训练集中的一个独立批次存储在training_xtraining_y中。

训练网络的最后一个也是最重要的部分是使用sess.run()。通过调用这个函数,你可以计算任何张量的值。此外,你可以按照顺序在列表中指定任意数量的参数——在我们的例子中,我们指定了优化器和损失函数。还记得在构建图时,我们为当前批次的值创建了占位符吗?这些值应当在运行Session时通过feed_dict参数传入。

训练这个网络大约需要四到五个小时。你可以通过检查损失函数的值来验证它是否在学习。如果值在减小,那么网络正在成功地调整权重和偏置。如果值没有减小,你可能需要做一些额外的调整来优化性能。这些将在第六章中讲解,提升你的 RNN 性能

评估预测结果

使用一个全新的示例来测试模型可以通过以下方式完成:

prediction_result = sess.run(prediction, {X: test_example})
largest_number_index = prediction_result[0].argsort()[-1:][::-1]

print("Predicted sum: ", largest_number_index, "Actual sum:", 30)
print("The predicted sequence parity is ", largest_number_index % 2, " and it should be: ", 0)

这里,test_example是一个大小为(1 x num_classes x 1)的数组。

test_example为以下内容:

[[[1],[0],[0],[1],[1],[0],[1],[1],[1],[0],[1],[0],[0],[1],[1],[0],[1],[1],[1],[0],
[1],[0],[0],[1],[1],[0],[1],[1],[1],[0],[1],[0],[0],[1],[1],[0],[1],[1],[1],[0],
[1],[0],[0],[1],[1],[0],[1],[1],[1],[0]]]

上述数组中所有元素的总和等于30。通过最后一行prediction_result[0].argsort()[-1:][::-1],我们可以找到最大数字的索引。这个索引将告诉我们序列的和。最后一步,我们需要找到这个数字除以2后的余数,这将给我们序列的奇偶性。

训练和评估是在你运行python3 ch2_task.py后一起进行的。如果你只想做评估,可以将程序中第 70 行到第 91 行的代码注释掉,再重新运行。

总结

在本章中,你探索了如何构建一个简单的循环神经网络来解决识别序列奇偶性的问题。你对 TensorFlow 库及其在构建深度学习模型中的应用有了简要的了解。希望本章的学习能够让你对深度学习的知识更加自信,并激励你在这个领域继续学习和成长。

在下一章中,你将通过实现一个更复杂的神经网络来生成文本。你将获得理论和实践经验。这将使你学习到一种新的网络类型——GRU,并理解如何在 TensorFlow 中实现它。此外,你还将面临正确格式化输入文本的挑战,并将其用于训练 TensorFlow 图。

我可以向你保证,激动人心的学习经历即将到来,我迫不及待希望你成为其中的一部分。

外部链接

第三章:生成你自己的书籍章节

本章将进一步探索 TensorFlow 库,并了解如何利用它来解决复杂的任务。特别是,你将构建一个神经网络,通过学习现有章节中的模式,生成一本书的新(不存在的)章节。此外,你还将掌握更多 TensorFlow 的功能,例如保存/恢复模型等。

本章还将介绍一个新的、更强大的递归神经网络模型——门控递归单元(GRU)。你将了解它的工作原理,以及为什么我们选择它而不是简单的 RNN。

总结来说,本章节的主题包括以下内容:

  • 为什么使用 GRU 网络?你将了解 GRU 网络是如何工作的,它解决了哪些问题,以及它的优势是什么。

  • 生成书籍章节—你将一步一步地了解生成书籍章节的过程。这包括收集和格式化训练数据,构建 GRU 模型的 TensorFlow 图,训练网络,最后逐字生成文本。

到章节结束时,你应该已经获得了理论和实践知识,这将使你能够自由地实验解决中等难度的问题。

为什么使用 GRU 网络?

近年来,递归神经网络模型呈现出令人着迷的成果,这些成果甚至可以在实际应用中看到,如语言翻译、语音合成等。GRU 的一个非凡应用就是文本生成。通过当前最先进的模型,我们可以看到十年前只存在于梦想中的结果。如果你想真正欣赏这些成果,我强烈建议你阅读 Andrej Karpathy 的文章《递归神经网络的非理性有效性》(karpathy.github.io/2015/05/21/rnn-effectiveness/)。

话虽如此,我们可以介绍门控递归单元(GRU),它是这些卓越成果背后的模型。另一个类似的模型是长短期记忆网络LSTM),它稍微先进一些。这两种架构都旨在解决消失梯度问题——这是简单 RNN 模型的一个主要问题。如果你还记得第一章,介绍递归神经网络,这个问题代表着网络无法学习长距离的依赖关系,因此它无法对复杂任务做出准确预测。

GRU 和 LSTM 都通过所谓的“门”来处理这个问题。这些门决定了哪些信息需要被抹去或传递到预测中。

我们将首先关注 GRU 模型,因为它更简单、更容易理解,之后,你将有机会在接下来的章节中探索 LSTM 模型。

如上所述,GRU 的主要目标是对长序列产生优异的结果。它通过引入更新门和重置门来修改标准 RNN 单元,以实现这一目标。就输入、记忆状态和输出而言,这个网络与普通 RNN 模型的工作方式相同。关键的不同在于每个时间步长内单元的具体细节。通过下图,你将更好地理解这一点:

以下是前述图表的符号说明:

插图展示了一个单一的 GRU 单元。该单元接收  和  作为输入,其中  是输入词汇在时间步长上的向量表示,  和  是来自上一步* t-1 *的记忆状态。此外,单元输出当前时间步长 t 的计算记忆状态。如果你还记得之前的内容,记忆状态的作用是通过所有时间步传递信息,并决定是否保留或丢弃知识。前述过程你应该已经在第一章《递归神经网络介绍》中有所了解。

新颖和有趣的部分是 GRU 单元内部发生了什么。计算的目标是决定从  和  中哪些信息应该被传递或删除。这个决策过程由以下一组方程式来处理:

  • 第一个方程表示更新门。它的目的是决定过去的信息应当传递多少到未来。为此,首先我们将输入  与其自身的权重  相乘,然后将结果与上一步的记忆状态  和其权重  相乘的结果相加。此权重的具体值是在训练过程中确定的。如下截图所示:

  • 第二个方程介绍了重置门。顾名思义,这个门用于决定应该丢弃多少过去的信息。同样,使用  和  来计算它的值。不同之处在于,我们的网络不是使用相同的权重,而是学习了一组不同的权重—— 和 。这在下面的截图中有展示:

更新门和重置门在生成值时,都会使用 sigmoid 作为最终步骤。如果你还记得第一章《介绍递归神经网络》中的内容,sigmoid 激活函数(www.youtube.com/watch?v=WcDtwxi7Ick&t=3s)是一种激活函数,它将输入值压缩在 01 之间:

如果你有两个向量 [1, 2, 3][0, -1, 4],那么 Hadamard 乘积就是 [1*0, 2*(-1), 3*4] = [0, -2, 12]。这在下面的截图中有展示:

  • 最终方程计算当前时间步 t 的记忆状态 。为此,我们使用临时内部记忆状态 ,前一时刻的记忆状态  和更新门 。同样,我们使用逐元素乘法来决定更新门要传播多少信息。我们通过一个例子来说明:

假设你想对一本书的评论进行情感分析,以确定人们对这本书的感受。假设评论开始是这样的:这本书非常激动人心,我非常喜欢。它揭示了一个年轻女性的故事……。在这里,我们希望保留评论的前半部分,直到结尾,以便做出准确的预测。在这种情况下,网络将学会将  接近 1,这样  就会接近 0。这样,所有未来的记忆状态将主要保留关于这部分信息(这本书非常激动人心,我非常喜欢。),而不会考虑接下来的任何无关信息。

将上述方程结合起来,结果是一个强大的模型,它能够学习在任何步骤保持完整或部分信息,从而增强最终的预测。你可以很容易地看到,这个解决方案是如何通过让网络(根据权重)决定什么应该影响预测,来解决梯度消失问题的。

生成你的书籍章节

在完成了本章的理论部分后,我们准备进入编码部分。我希望你能掌握 GRU 模型的基本原理,并且在看到 TensorFlow 程序中的符号时能感到轻松。它由五个部分组成,其中大部分内容你应该在第二章,用 TensorFlow 构建你的第一个 RNN中有所接触:

  • 获取书籍文本:这一部分非常直接。你的任务是确保有大量的纯文本为训练做好准备。

  • 编码文本:这一部分可能比较有挑战性,因为我们需要将编码与适当的维度进行对接。有时候,这个操作可能会比预期花费更多时间,但它是完美编译程序的必要条件。编码算法有很多种,我们将选择一个相对简单的算法,这样你就能完全理解它的真正含义。

  • 构建 TensorFlow 图:这一步你应该从第二章,用 TensorFlow 构建你的第一个 RNN中有所了解。我们将使用类似的步骤,唯一的区别是现在操作单元是 GRU,而不是普通的 RNN。

  • 训练网络:这一步你应该从第二章,用 TensorFlow 构建你的第一个 RNN中有所了解。我们将再次使用批处理来加速训练并减少内存占用。

  • 生成你的新文本:这是我们程序中的新步骤,也是独特的步骤。我们将使用已经训练好的权重和偏置来预测词语序列。使用适当的超参数和大量数据集,可以生成可以理解的段落,读者很容易认为这些段落是真实的。

你将在一个新的文件中编写代码,文件名为 ch3_task.py。首先,使用以下代码安装 Python 库:

pip3 install tensorflow
pip3 install numpy

然后,打开ch3_task.py并导入之前的库,如下所示:

import numpy as np
import tensorflow as tf
import sys
import collections

现在是探索步骤的时候了。

获取书籍文本

构建任何机器学习任务的第一步是获取数据。在专业环境中,通常会将数据划分为训练、验证和测试数据。通常的分配比例为 60%、20%、20%。人们常常将验证数据与测试数据混淆,甚至忽略使用前者。验证数据用于在调整超参数时评估模型。相比之下,测试数据仅用于对模型进行总体评估。你不应该使用测试数据来对模型进行调整。由于该任务是生成文本,我们的数据将仅用于训练。然后,我们可以利用该模型逐个猜测单词。

我们的目标是根据饥饿游戏书籍生成有意义的新章节。我们应该将文本存储在一个名为the_hunger_games.txt的新文件中。

首先,我们需要使用该文件构建我们的字典。这将通过两个名为get_words(file_name)build_dictionary(words)的函数完成,如下所示:

def get_words(file_name):
    with open(file_name) as file:
        all_lines = file.readlines()
    lines_without_spaces = [x.strip() for x in all_lines]
    words = []
    for line in lines_without_spaces:
        words.extend(line.split())
    words = np.array(words)
    return words

前一个函数的目的是创建the_hunger_games.txt中所有单词的列表。现在,让我们使用以下代码构建实际的字典:

def build_dictionary(words):
    most_common_words = collections.Counter(words).most_common()
    word2id = dict((word, id) for (id, (word, _)) in enumerate(most_common_words))
    id2word = dict((id, word) for (id, (word, _)) in enumerate(most_common_words))
    return most_common_words, word2id, id2word

这里我们使用 Python 内置库 collections。它可以轻松创建一个元组列表,其中每个元组由一个字符串(单词)和该单词在列表words中出现的次数组成。因此,most_common_words 不包含任何重复元素。

字典word2idid2word将每个单词与一个数字关联,从而确保可以直接访问所有单词。

最后,我们执行get_words()build_dictionary()函数,以便可以全局访问单词和字典,如下所示:

words = get_words("the_hunger_games.txt")
most_common_words, word2id, id2word = build_dictionary(words)
most_common_words_length = len(most_common_words)

编码文本

这一部分展示了如何使用流行的独热编码对我们的数据集进行编码。进行此操作的原因在于,任何神经网络都通过某种数字表示来处理字符串。

首先,我们声明section_length = 20,表示我们编码数据集中单个片段的长度。该数据集是一个由多个片段组成的集合,每个片段包含 20 个独热编码的单词。

然后,我们将 20 个单词的片段存储在input_values数组中。第 21 个单词作为该特定片段的输出值。这意味着,在训练过程中,网络会学习到像我喜欢阅读非小说类书籍……我能找到这些类型的(从训练集中提取的 20 个单词示例序列)之后会出现书籍

接下来是独热编码(one-hot encoding),这也相当简单。我们创建两个零数组,维度为(num_sections, section_length, most_common_words_length)——用于输入,(num_sections, most_common_words_length)——用于输出。我们遍历input_values,并找到每个词在每个部分中的索引。使用这些索引,我们用1替换独热数组中的值。

相关的代码在以下示例中:

section_length = 20

def input_output_values(words):
    input_values = []
    output_values = []
    num_sections = 0
    for i in range(len(words) - section_length):
        input_values.append(words[i: i + section_length])
        output_values.append(words[i + section_length])
        num_sections += 1

    one_hot_inputs = np.zeros((num_sections, section_length, most_common_words_length))
    one_hot_outputs = np.zeros((num_sections, most_common_words_length))

    for s_index, section in enumerate(input_values):
        for w_index, word in enumerate(section):
            one_hot_inputs[s_index, w_index, word2id[word]] = 1.0
        one_hot_outputs[s_index, word2id[output_values[s_index]]] = 1.0

    return one_hot_inputs, one_hot_outputs   

最后,我们将编码后的词汇存储在两个全局变量中(我们还使用了上一部分中的words参数),如下所示:

training_X, training_y = input_output_values(words)

构建 TensorFlow 图

这一步构建了我们程序中最基本的部分——神经网络图。

首先,我们开始初始化模型的超参数,如下例所示:

learning_rate = 0.001
batch_size = 512
number_of_iterations = 100000
number_hidden_units = 1024

人们通常会反复尝试上述值,直到模型得到较好的结果:

在定义了上述参数后,接下来是指定我们的图结构。这在以下代码片段中演示:

  • 我们从 TensorFlow 的占位符 X 开始,它存储当前批次的训练数据,Y 存储当前批次的预测数据。这在以下代码中展示:
      X = tf.placeholder(tf.float32, shape=[batch_size, section_length,                                most_common_words_length])
      y = tf.placeholder(tf.float32, shape=[batch_size, most_common_words_length])
  • 然后,我们使用正态分布初始化权重和偏置。权重的维度是[number_hidden_units, most_common_words_length],这确保了我们预测中的正确乘法。同样的逻辑也适用于维度为[most_common_words_length]的偏置。以下示例展示了这一过程:
      weights = tf.Variable(tf.truncated_normal([num_hidden_units, 
      most_common_words_length]))
      biases = 
      tf.Variable(tf.truncated_normal([most_common_words_length]))
  • 接下来,我们指定 GRU 单元。第一部分章节中学到的所有复杂逻辑都隐藏在前一行代码背后。第二章,使用 TensorFlow 构建你的第一个 RNN,解释了为什么我们随后传递参数num_units,如下示例所示:
      gru_cell = tf.contrib.rnn.GRUCell(num_units=num_hidden_units)
  • 然后,我们使用 GRU 单元和输入 X 来计算输出。一个重要步骤是对这些输出进行[1, 0, 2]的转置操作。
      outputs, state = tf.nn.dynamic_rnn(gru_cell, inputs=X, 
       dtype=tf.float32)
      outputs = tf.transpose(outputs, perm=[1, 0, 2])

      last_output = tf.gather(outputs, int(outputs.get_shape()[0]) - 1)

让我们通过以下示意图来理解如何获得这个last_output

示意图展示了如何将一个包含section_length步的输入例子插入到网络中。这个操作应该对每个包含section_length步的单独例子执行batch_size次,但为了简化起见,我们这里只展示了一个例子。

在每次迭代地经过每个时间步后,我们会生成一个section_length数量的输出,每个输出的维度为[most_common_words_length, 1]。因此,对于一个section_length输入时间步的例子,我们会产生section_length个输出步。将所有输出数学表示会得到一个[batch_size, section_length, most_common_words_length]矩阵。矩阵的高度是batch_size——单个批次中的例子数量。矩阵的宽度是section_length——每个例子的时间步数。矩阵的深度是most_common_words_length——每个元素的维度。

为了进行预测,我们只关注每个例子的output_last,并且由于例子的数量是batch_size,我们只需要batch_size个输出值。如前所示,我们将矩阵[batch_size, section_length, most_common_words_length]重塑为[section_length, batch_size, most_common_words_length],这样可以更容易地从每个例子中获取output_last。接着,我们使用tf.gather来获取last_output张量。

以下是上述解释的代码实现:

  • 现在我们已经得到了包含这些最终步骤值的数组,我们可以进行预测,并将其与标签值(如前面示例第四行所示)结合使用,以计算此训练步骤的损失。由于损失的维度与标签(预期输出值)和 logits(预测输出值)相同,我们使用tf.reduce_mean来生成一个单一的total_loss。以下代码演示了这一过程:
      prediction = tf.matmul(last_output, weights) + biases

      loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=y, 
      logits=prediction)
      total_loss = tf.reduce_mean(loss)
  • 最终,total_loss 在反向传播中使用,目的是通过调整模型的权重和偏置来提高模型的性能。这是通过在训练过程中运行的 tf.train.AdamOptimizer 实现的,详细内容将在接下来的章节中介绍:
      optimizer = 
     tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(
     loss=total_loss)

训练网络

一旦模型建立完成,我们需要使用预先收集的数据来训练它。这个操作遵循下面的代码片段:

  • 我们首先初始化所有的 TensorFlow 变量。然后,我们使用 iter_offset 确保从数据中提取正确的批次。以下代码展示了这一过程:
      with tf.Session() as sess:
          sess.run(tf.global_variables_initializer())
          iter_offset = 0
  • 接下来,tf.train.Saver() 创建了一个保存器对象,它定期将模型保存在本地。这有助于我们在训练过程中出现中断的情况下恢复模型。此外,它还可以帮助我们在预测阶段查找预训练的参数,从而不必每次进行预测时都重新训练模型:
      saver = tf.train.Saver()

现在,真正的训练开始了。我们循环遍历训练数据,并使用每个批次计算优化器。这些计算将最小化损失函数,我们可以通过打印损失值看到它的减少,下面的代码片段展示了这一点:

  • 首先,我们需要将数据划分为批次。我们通过 iter_offset 参数来实现这一点。它跟踪每个批次的下限,确保我们总是从训练集获取下一个批次,以下代码展示了这一过程:
      for iter in range(number_of_iterations):
          length_X = len(training_X)

          if length_X != 0:
              iter_offset = iter_offset % length_X

          if iter_offset <= length_X - batch_size:
              training_X_batch = training_X[iter_offset: iter_offset +               
                batch_size]
              training_y_batch = training_y[iter_offset: iter_offset + 
               batch_size]
              iter_offset += batch_size
          else:
              add_from_the_beginning = batch_size - (length_X - 
               iter_offset)
              training_X_batch = 
               np.concatenate((training_X[iter_offset: length_X], X[0:                         
               add_from_the_beginning]))
              training_y_batch = 
               np.concatenate((training_y[iter_offset:  
              length_X], y[0: add_from_the_beginning]))
              iter_offset = add_from_the_beginning
  • 接下来,我们应该通过计算 optimizertotal_loss 来进行训练。我们可以运行一个 TensorFlow 会话,使用当前批次的输入和输出。最后,我们应该打印损失函数的值,以便跟踪我们的进展。如果网络训练成功,损失函数的值应该在每一步都减少:
        _, training_loss = sess.run([optimizer, total_loss], feed_dict=
         {X: training_X_batch, y: training_y_batch})
        if iter % 10 == 0:
            print("Loss:", training_loss)
            saver.save(sess, 'ckpt/model', global_step=iter)

通常,这个训练过程需要几个小时才能完成。你可以通过增加计算能力来加速这个过程。我们将在第六章中讨论一些加速的方法,提升你的 RNN 性能

生成你的新文本

在成功训练模型之后,接下来是生成你新的 饥饿游戏 章节的时候。

前面的代码可以分为两部分:

  • 使用自定义输入训练模型

  • 预测序列中的下一个 1,000 个词

让我们探索下面的代码片段:

  • 一开始,我们初始化了一个包含 21 个词的自定义输入(我们使用了 (section_length + 1) 来匹配模型的维度)。这个输入用于提供我们预测的起始点。接下来,我们将用这个输入训练现有的网络,以便优化权重和偏置,以应对即将到来的预测,下面的示例展示了这一过程:
      starting_sentence = 'I plan to make the world a better place 
       because I love seeing how people grow and do in their lives '
  • 然后,我们可以从 ckpt 文件夹恢复已保存的模型,并使用最新的输入对其进行训练,如下面的代码所示:
      with tf.Session() as sess:
          sess.run(tf.global_variables_initializer())
          model = tf.train.latest_checkpoint('ckpt')
          saver = tf.train.Saver()
          saver.restore(sess, model)
  • 然后,我们应该将该输入编码成一个维度为[1, section_length, most_common_words_length]的 one-hot 向量。这个数组应该被输入到我们的模型中,以便生成的预测单词能够遵循顺序。你可能会注意到,我们省略了最后一个单词,稍后会添加它,以生成text_next_X数组(见以下代码)。我们这样做是为了给文本生成提供一个无偏的起点。下面的示例中进行了演示:
      generated_text = starting_sentence
      words_in_starting_sentence = starting_sentence.split()
      test_X = np.zeros((1, section_length, 
      most_common_words_length))

      for index, word in enumerate(words_in_starting_sentence[:-1]):
          if index < section_length:
              test_X[0, index, word2id[word]] = 1
  • 最后,我们应该使用编码后的输入句子来训练网络。为了保持训练的无偏起点,我们需要在开始预测之前,添加句子中的最后一个单词,如下例所示。稍微有些令人困惑的部分可能是np.concatenate方法。我们首先应重塑text_X,以便轻松附加最后一部分,然后再重塑结果以适应预测评估。这在以下示例中展示:
        _ = sess.run(prediction, feed_dict={X: test_X})

        test_last_X = np.zeros((1, 1, most_common_words_length))
        test_last_X[0, 0, word2id[words_in_starting_sentence[-1]]] = 1
        test_next_X = np.reshape(np.concatenate((test_X[0, 1:], 
        test_last_X[0])), (1, section_length, most_common_words_length)
  • 最后一步实际上是生成单词。在每一步(共 1,000 步)中,我们可以使用当前的test_next_X来计算预测。然后,我们可以从当前的test_next_X中移除第一个字符,并附加预测结果。通过这种方式,我们始终保持一个包含 20 个单词的集合,其中最后一个元素是一个全新的预测单词。这个过程在以下示例中进行了展示:
         for i in range(1000):
             test_prediction = prediction.eval({X: test_next_X})[0]
             next_word_one_hot = prediction_to_one_hot(test_prediction)
             next_word = id2word[np.argmax(next_word_one_hot)]
             generated_text += next_word + " "
             test_next_X = 
              np.reshape(np.concatenate((test_next_X[0,1:],
                                np.reshape(next_word_one_hot, (1, 
                                most_common_words_length)))),
                                (1, section_length,   
                                 most_common_words_length))
                 print("Generated text: ", generated_text)

prediction_to_one_hot方法将预测结果编码成一个 one hot 编码数组。这个方法在以下示例中定义:

    def prediction_to_one_hot(prediction):
        zero_array = np.zeros(np.shape(prediction))
        zero_array[np.argmax(prediction)] = 1
        return zero_array

运行代码后,你应该能在控制台看到最终的章节输出。如果单词之间存在不一致的情况,你需要调整一些超参数。我将在最后一章中解释如何优化你的模型并获得良好的性能。请使用上面的代码片段训练网络,并随时告诉我你的性能表现。我会非常高兴看到你的最终结果。

总结

在本章中,你学习了如何使用门控循环单元(GRU)神经网络构建一个书籍章节生成器的过程。你了解了这个强大模型背后的原理,以及如何通过几行代码在 TensorFlow 中将其付诸实践。此外,你还面临了准备和清理数据的挑战,以确保模型能够正确训练。

在下一章中,你将通过实现第一个现实中的实际应用——语言翻译器,进一步巩固你的技能。你可能已经使用过在线的 Google 翻译软件,并且对其出色的表现感到惊讶。在下一章中,你将了解像这样一个复杂系统背后的原理,以及为什么它的准确度在近年来大幅提高。

我希望当前章节能提升你对深度学习的知识,并且让你对探索递归神经网络的世界感到兴奋。我迫不及待想看到你开始下一节。

外部链接

第四章:创建一个西班牙语到英语的翻译器

本章将通过引入当今最强大语言翻译系统核心的最前沿概念,进一步推动你的神经网络知识。你将构建一个简单版本的西班牙语到英语的翻译器,该翻译器接受西班牙语句子并输出其英文等效句子。

本章包括以下部分:

  • 理解翻译模型:本节完全专注于该系统背后的理论。

  • 什么是 LSTM 网络:我们将了解这一先进版本的循环神经网络背后是什么。

  • 理解带注意力机制的序列到序列网络:你将掌握这个强大模型背后的理论,了解它实际做了什么,以及为什么它在不同问题中被广泛使用。

  • 构建西班牙语到英语的翻译器:本节完全专注于将到目前为止获得的知识实现为一个可运行的程序。它包括以下内容:

    • 训练模型

    • 预测英文翻译

    • 评估模型的准确性

理解翻译模型

机器翻译通常采用所谓的统计机器翻译,基于统计模型。这种方法效果很好,但一个关键问题是,对于每一对语言,我们都需要重建架构。幸运的是,在 2014 年,Cho 等人arxiv.org/pdf/1406.1078.pdf)发布了一篇论文,旨在通过日益流行的循环神经网络来解决这个问题及其他问题。该模型被称为序列到序列(sequence-to-sequence),通过提供足够的数据,可以在任意语言对上进行训练。此外,它的强大之处在于能够匹配不同长度的序列,例如在机器翻译中,英文句子和西班牙语句子的长度可能不同。我们来看看这些任务是如何完成的。

首先,我们将介绍以下图表并解释它的组成:

该架构有三个主要部分:编码器RNN 网络(左侧)、中间状态(由中间箭头标记)和解码器RNN 网络(右侧)。将西班牙语句子Como te llamas?(西班牙语)翻译为What is your name?(英语)的操作流程如下:

  • 使用编码器 RNN 将西班牙语句子编码为中间状态

  • 使用该状态和解码器 RNN,生成英文的输出句子

这个简单方法适用于短小且简单的句子,但实际上,翻译模型的真正应用在于更长且更复杂的序列。这就是为什么我们将使用强大的 LSTM 网络和注意力机制来扩展我们的基本方法。接下来让我们在各节中探讨这些技术。

什么是 LSTM 网络?

LSTM长短期记忆)网络是一种先进的 RNN 网络,旨在解决梯度消失问题,并在长序列上取得优异的结果。在前一章中,我们介绍了 GRU 网络,它是 LSTM 的简化版本。两者都包括记忆状态,用于决定每个时间步应该传播哪些信息。LSTM 单元如下所示:

让我们介绍主要的方程式,这些方程式将澄清前面的图示。它们与门控递归单元(请参见第三章,生成你自己的书章节)的方程式相似。以下是每个给定时间步 t 发生的事情:

输出门,它决定了当前预测中哪些信息是重要的,以及哪些信息应该保留以便未来使用。 被称为 输入门,它决定了我们在当前向量(单元)上应该关注多少。 是新记忆单元的值。 遗忘门,它决定了应该忘记当前向量中的多少信息(如果遗忘门为 0,我们就完全忘记过去)。这四个,,有相同的方程洞察(以及相应的权重),但 使用的是 tanh,而其他的使用 sigmoid。

最后,我们得到了最终的记忆单元 和最终的隐藏状态

最终的记忆单元将输入门和遗忘门分开,并决定保留多少先前的输出 以及多少当前输出 应该向前传播(简单来说,这意味着:忘记过去还是不忘,是否接受当前输入)。 点乘 符号叫做哈达玛积——如果 x = [1, 2, 3]y = [4, 5, 6],则 x dot y = [1*4, 2*5, 3*6] = [4, 10, 18]

最终的隐藏状态定义如下:

它决定是否暴露此特定时间步的单元内容。由于当前单元的某些信息 可能在 中被省略,我们将 向前传递,以便在下一个时间步使用。

这个相同的系统在神经网络中被多次重复。通常情况下,多个 LSTM 单元会堆叠在一起,并使用共享的权重和偏置。

提升你对 LSTM 理解的两个重要来源是 Colah 的文章理解 LSTM 网络colah.github.io/posts/2015-08-Understanding-LSTMs/)和斯坦福大学的 LSTM 讲座(www.youtube.com/watch?v=QuELiw8tbx8),讲解者是 Richard Socher。

理解带有注意力机制的序列到序列网络

既然你已经理解了 LSTM 网络的工作原理,让我们退后一步,看看整个网络架构。正如我们之前所说,我们使用的是带有注意力机制的序列到序列模型。这个模型由 LSTM 单元组成,分为编码器和解码器部分。

在一个简单的序列到序列模型中,我们输入一个给定长度的句子,并创建一个向量来捕获该句子中的所有信息。之后,我们使用该向量来预测翻译。你可以在本章末尾的外部链接部分阅读更多关于这一过程的精彩 Google 论文(arxiv.org/pdf/1409.3215.pdf)。

这种方法是可行的,但像在任何情况下,我们都可以并且必须做得更好。在这种情况下,一个更好的方法是使用注意力机制。这种方法受到人类翻译语言方式的启发。人类不会先阅读输入句子,然后在试图写下输出句子时把文本隐藏起来。他们在翻译的过程中持续跟踪原始句子。这就是注意力机制的工作原理。在解码器的每个时间步,网络会决定使用编码器输入中的哪些部分以及多少部分。为了做出这个决策,特定的权重被分配给每个编码器单词。实际上,注意力机制试图解决递归神经网络的一个根本问题——记住长期依赖关系的能力。

一个很好的注意力机制示例可以在这里看到:

是输入, 是预测的输出。你可以看到表示注意力权重的,每个权重都附加在相应的输入上。这些权重在训练过程中学习,并决定特定输入对最终输出的影响。这使得每个输出依赖于所有输入状态的加权组合。

不幸的是,注意力机制是有代价的。请考虑以下内容,来自一篇 WildML 的文章(www.wildml.com/2016/01/attention-and-memory-in-deep-learning-and-nlp/):

如果我们更仔细地观察注意力机制的公式,我们可以看到注意力是有代价的。我们需要计算每一对输入和输出单词的注意力值。如果你有一个 50 个单词的输入序列,并生成一个 50 个单词的输出序列,那么就会有 2500 个注意力值。这并不算太糟糕,但如果你进行字符级别的计算,并处理包含数百个标记的序列,上述注意力机制可能会变得代价高昂。

尽管如此,注意力机制仍然是机器翻译领域的一种最先进的模型,能产生优秀的结果。前面的陈述仅表明还有很大的改进空间,因此我们应该尽可能地为其发展做出贡献。

构建西班牙语到英语的翻译器

我希望前面的部分已经让你对我们即将构建的模型有了清晰的理解。现在,我们将实际动手编写我们翻译系统背后的代码。最终,我们应该得到一个经过训练的网络,能够预测任何西班牙语句子的英语版本。让我们开始编程吧。

准备数据

第一步,像往常一样,是收集所需的数据并为训练做准备。我们的系统变得越复杂,整理数据并将其转换为正确格式的过程就越复杂。我们将使用来自 OpenSubtitles 免费数据源的西班牙语到英语的短语(opus.nlpl.eu/OpenSubtitles.php)。我们将使用data_utils.py脚本来完成这个任务,您可以在提供的 GitHub 仓库中找到该脚本(github.com/simonnoff/Recurrent-Neural-Networks-with-Python-Quick-Start-Guide)。在该仓库中,您还可以找到有关从 OpenSubtitles 下载哪些数据集的更多详细信息。该文件计算了以下属性,这些属性可在我们的模型中进一步使用:

  • spanish_vocab:一个包含所有西班牙语训练集单词的集合,按频率排序

  • english_vocab:一个包含所有英语训练集单词的集合,按频率排序

  • spanish_idx2word:一个包含键和值的字典,其中键是单词在spanish_vocab中的顺序

  • spanish_word2idxspanish_idx2word的反向版本

  • english_idx2word:一个包含键和值的字典,其中键是单词在english_vocab中的顺序

  • english_word2idxenglish_idx2word的反向版本

  • X:一个包含数字数组的数组。我们通过首先逐行读取西班牙文文本文件,并将这些单词存储在单独的数组中来生成这个数组。然后,我们将每个句子的数组编码成一个数字数组,每个单词都用它在 spanish_word2idx 中的索引替换。

  • Y:一个包含数字数组的数组。我们通过首先逐行读取英文文本文件,并将这些单词存储在单独的数组中来生成这个数组。然后,我们将每个句子的数组编码成一个数字数组,每个单词都用它在 english_word2idx 中的索引替换。

你将在接下来的部分中看到这些集合在模型训练和测试期间的使用方式。下一步是构建 TensorFlow 图。

构建 TensorFlow 图

作为初步步骤,我们导入所需的 Python 库(你可以在 neural_machine_translation.py 文件中看到这一点):

import tensorflow as tf
import numpy as np
from sklearn.model_selection import train_test_split
import data_utils
import matplotlib.pyplot as plt

tensorflownumpy 应该已经对你很熟悉了。matplotlib 是一个用于数据可视化的便捷 Python 库(稍后你将看到我们如何使用它)。然后,我们使用 sklearntrain_test_split 函数将数据拆分为随机的训练和测试数组。

我们还导入了 data_utils,它用于访问上一部分提到的数据集合。

在拆分数据之前,一个重要的修改是确保 XY 中的每个数组都进行了填充,以表示新序列的开始:

def data_padding(x, y, length = 15):
    for i in range(len(X)):
        x[i] = x[i] + (length - len(x[i])) * [spanish_word2idx['<pad>']]
        y[i] = [english_word2idx['<go>']] + y[i] + (length - len(y[i])) * [english_word2idx['<pad>']]

然后,我们按如下方式拆分数据:

X_train,  X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.1)

现在,到了定义实际的 TensorFlow 图的时间。我们从确定输入和输出序列长度的变量开始:

input_sequence_length = 15
output_sequence_length = 16

然后,我们计算每个词汇表的大小:

spanish_vocab_size = len(spanish_vocab) + 2 # + <pad>, <unk>
english_vocab_size = len(english_vocab) + 4 # + <pad>, <eos>, <go>

<pad> 符号用于对齐时间步,<go> 用于指示解码器序列的开始,<eos> 表示空白位置。

之后,我们初始化了 TensorFlow 的占位符:

encoder_inputs = [tf.placeholder(dtype=tf.int32, shape=[None], name="encoder{}".format(i)) for i in range(input_sequence_length)]

decoder_inputs = [tf.placeholder(dtype=tf.int32, shape=[None], name="decoder{}".format(i)) for i in range(output_sequence_length)]

targets = [decoder_inputs[i] for i in range(output_sequence_length - 1)]
targets.append(tf.placeholder(dtype = tf.int32, shape=[None], name="last_output"))

target_weights = [tf.placeholder(dtype = tf.float32, shape = [None], name="target_w{}".format(i)) for i in range(output_sequence_length)]
  • encoder_inputs:这个变量保存西班牙文训练输入单词的值。

  • decoder_inputs:这个变量保存英文训练输入单词的值。

  • target:这个变量保存英文预测的真实值。它与 decoder_inputs 的长度相同,每个单词是下一个预测的单词。

  • target_weights:这是一个张量,用于为所有预测值分配权重。

构建图的最后两个步骤是生成输出并优化网络的权重和偏差。

前者使用方便的 TensorFlow 函数 tf.contrib.legacy_seq2seq.embedding_attention_seq2seqwww.tensorflow.org/api_docs/python/tf/contrib/legacy_seq2seq/embedding_attention_seq2seq),该函数构建一个带有注意力机制的序列到序列网络,并返回解码器网络生成的输出。实现如下:

size = 512 # num_hidden_units
embedding_size = 100

with tf.variable_scope("model_params"):
    w_t = tf.get_variable('proj_w', [english_vocab_size, size], tf.float32)
    b = tf.get_variable('proj_b', [english_vocab_size], tf.float32)
    w = tf.transpose(w_t)
    output_projection = (w, b)

    outputs, states = tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
                      encoder_inputs,
                      decoder_inputs,
                      tf.contrib.rnn.BasicLSTMCell(size),
                      num_encoder_symbols=spanish_vocab_size,
                      num_decoder_symbols=english_vocab_size,
                      embedding_size=embedding_size,
                      feed_previous=False,
                      output_projection=output_projection,
                      dtype=tf.float32)

让我们讨论一下函数的参数:

  • encoder_inputsdecoder_inputs 包含每一对西班牙语和英语句子的训练数据。

  • tf.contrib.rnn.BasicLSTMCell(size) 是用于序列模型的 RNN 单元。这是一个具有 size=512)个隐藏单元的 LSTM 单元。

  • num_encoder_symbolsnum_decoder_symbols 是模型的西班牙语和英语词汇表。

  • embedding_size 表示每个单词的嵌入向量的长度。这个向量可以通过 word2vec 算法获得,并帮助网络在反向传播过程中学习。

  • feed_previous 是一个布尔值,表示是否在某个时间步使用先前的输出作为下一个解码器输入。

  • output_projection 包含一对网络的权重和偏置。正如前面的代码块所示,权重的形状是 [english_vocab_size, size],偏置的形状是 [english_vocab_size]

在计算输出之后,我们需要通过最小化该模型的损失函数来优化这些权重和偏置。为此,我们将使用 tf.contrib.legacy_seq2seq.sequence_loss TensorFlow 函数,如下所示:

loss = tf.contrib.legacy_seq2seq.sequence_loss(outputs, targets, target_weights, softmax_loss_function = sampled_loss)

learning_rate = 5e-3 (5*10^(-3) = 0.005)
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)

我们提供预测的 outputs,以及网络的实际值 targets。此外,我们提供了标准 softmax 损失函数的轻微修改。

最后,我们定义了优化器,它的目标是最小化损失函数。

为了澄清 sample_loss 变量的混淆,我们将给出它的定义:

def sampled_loss(labels, logits):
    return tf.nn.sampled_softmax_loss(
        weights=w_t,
        biases=b,
        labels=tf.reshape(labels, [-1, 1]),
        inputs=logits,
        num_sampled=size,
        num_classes=english_vocab_size
    )

这个 softmax 函数仅用于训练。你可以通过 TensorFlow 文档了解更多关于它的内容(www.tensorflow.org/api_docs/python/tf/nn/sampled_softmax_loss)。

这些方程式生成了一个功能完备的 TensorFlow 图,用于我们的带有注意力机制的序列到序列模型。再一次,你可能会惊讶于构建一个强大的神经网络以获得优秀的结果竟然只需要这么少的代码。

接下来,我们将数据集合插入到这个图中,并实际训练模型。

训练模型

训练神经网络是通过使用与之前相同的模式来完成的:

def train():
    init = tf.global_variables_initializer()
    saver = tf.train.Saver()

    with tf.Session() as sess:
        sess.run(init)
        for step in range(steps):
            feed = feed_dictionary_values(X_train, Y_train)
            sess.run(optimizer, feed_dict=feed)
            if step % 5 == 4 or step == 0:
                loss_value = sess.run(loss, feed_dict = feed)
                losses.append(loss_value)
                print("Step {0}/{1} Loss {2}".format(step, steps, 
                loss_value))
            if step % 20 == 19:
                saver.save(sess, 'ckpt/', global_step = step)

前面的实现中有一个有趣的部分是 feed_dictionary_values 函数,它通过 X_trainY_train 来构建占位符:

def feed_dictionary_values(x, y, batch_size):
    feed = {}
    indices_x = np.random.choice(len(x), size=batch_size, replace=False)
    indices_y = np.random.choice(len(y), size=batch_size, replace=False)

    for i in range(input_sequence_length):
        feed[encoder_inputs[i].name] = np.array([x[j][i] for j in indices_x], dtype=np.int32)

    for i in range(output_sequence_length):
        feed[decoder_inputs[i].name] = np.array([y[j][i] for j in indices_y], dtype=np.int32)

    feed[targets[len(targets)-1].name] = np.full(shape = [batch_size], fill_value=english_word2idx['<pad>'], dtype=np.int32)

    for i in range(output_sequence_length - 1):
        batch_weights = np.ones(batch_size, dtype=np.float32)
        target = feed[decoder_inputs[i+1].name]
        for j in range(batch_size):
            if target[j] == english_word2idx['<pad>']:
                batch_weigths[j] = 0.0
        feed[target_weights[i].name] = batch_weigths

    feed[target_weights[output_sequence_length - 1].name] = np.zeros(batch_size, dtype=np.float32)

    return feed

让我们逐行分析上面的函数。

它需要返回一个字典,包含所有占位符的值。回想一下,它们的名称是:

"encoder0", "encoder1", ..., "encoder14" (input_sequence_length=15), "decoder0", "decoder1" through to "decoder15" (output_sequence_length=16), "last_output", "target_w0", "target_w1", and so on, through to "target_w15" 

indices_x 是一个大小为 64 (batch_size) 的数组,包含从 0len(X_train) 范围内随机选择的索引。

indices_y 是一个大小为 64 (batch_size) 的数组,包含从 0len(Y_train) 范围内随机选择的索引。

"encoder-" 的值通过从 indices_x 中找到每个索引的数组并收集特定编码器的值来获取。

类似地,"decoder-" 的值是通过从 indices_y 中查找每个索引的数组,并收集该解码器的特定值来获得的。

请考虑以下示例:假设我们的 X_train[[x11, x12, ...], [x21, x22, ...], ...]indices_x[1, 0, ...],那么 "encoder0" 将是 [x21, x11, ...],并且将包含 X_train 中所有数组的第 0 个元素,这些数组的索引已存储在 indices_x 中。

last_output 的值是一个大小为 batch_size 的数组,数组中的值全部是 3(即 "<pad>" 符号的关联索引)。

最后,"target_w-" 元素是大小为 batch_size 的 1 和 0 组成的数组。这些数组在解码器数组中 "<pad>" 值的索引位置上包含 0。我们用以下例子来说明这一点:

如果 "decoder0" 的值是 [10, 8, 3, ...],其中 3 是 "<pad>"en_idx2word 数组中的索引,那么我们的 "target0" 将是 [1, 1, 0, ...]

最后一个 "target15" 是一个只有 0 的数组。

牢记前面的计算,我们可以开始训练我们的网络。这个过程需要一些时间,因为我们需要迭代 1,000 步。与此同时,我们将在每 20 步时存储训练好的参数,之后可以用于预测。

预测翻译

在我们训练好模型后,将使用其参数把一些西班牙语句子翻译成英语。让我们创建一个名为 predict.py 的新文件,并将预测代码写入其中。逻辑如下:

  • 定义与训练时完全相同的序列到序列模型架构

  • 使用已经训练好的权重和偏置生成输出

  • 编码一组西班牙语句子,准备进行翻译

  • 预测最终结果并打印出相应的英语句子

如你所见,这个流程相当直接:

  1. 为了实现这一点,我们首先导入两个 Python 库以及 neural_machine_translation.py 文件(用于训练)。
          import tensorflow as tf
          import numpy as np
          import neural_machine_translation as nmt
  1. 然后,我们定义与相关占位符配套的模型:
          # Placeholders
          encoder_inputs = [tf.placeholder(dtype = tf.int32, shape = 
          [None],   
          name = 'encoder{}'.format(i)) for i in   
          range(nmt.input_sequence_length)]
          decoder_inputs = [tf.placeholder(dtype = tf.int32, shape = 
          [None],   
          name = 'decoder{}'.format(i)) for i in   
          range(nmt.output_sequence_length)]
          with tf.variable_scope("model_params", reuse=True):
              w_t = tf.get_variable('proj_w', [nmt.english_vocab_size, 
              nmt.size], tf.float32)
              b = tf.get_variable('proj_b', [nmt.english_vocab_size], 
               tf.float32)
              w = tf.transpose(w_t)
               output_projection = (w, b)

              outputs, states = 
               tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
                        encoder_inputs,
                        decoder_inputs,
                        tf.contrib.rnn.BasicLSTMCell(nmt.size),
                        num_encoder_symbols = nmt.spanish_vocab_size,
                        num_decoder_symbols = nmt.english_vocab_size,
                        embedding_size = nmt.embedding_size,
                        feed_previous = True,
                        output_projection = output_projection,
                        dtype = tf.float32)
  1. 使用 TensorFlow 函数的输出,我们计算最终的翻译结果如下:
        outputs_proj = [tf.matmul(outputs[i], output_projection[0]) +   
        output_projection for i in range(nmt.output_sequence_length)]
  1. 下一步是定义输入句子,并使用编码字典进行编码:
       spanish_sentences = ["Como te llamas", "Mi nombre es", "Estoy 
         leyendo un libro","Que tal", "Estoy bien", "Hablas espanol", 
         "Que hora es", "Hola", "Adios", "Si", "No"]

         spanish_sentences_encoded = [[nmt.spanish_word2idx.get(word, 
          0) for word in sentence.split()] for sentence in 
          spanish_sentences]

       for i in range(len(spanish_sentences_encoded)):
           spanish_sentences_encoded[i] += (nmt.input_sequence_length -
           len(spanish_sentences_encoded[i])) * 
           [nmt.spanish_word2idx['<pad>']]

如你所见,我们也在对输入句子进行填充,使它们与占位符 (nmt.input_sequence_length) 的长度匹配。

  1. 最后,我们将使用 spanish_sentences_encoded 和前述的 TensorFlow 模型来计算 outputs_proj 的值,并得到我们的结果:
          saver = tf.train.Saver()
          path = tf.train.latest_checkpoint('ckpt')
          with tf.Session() as sess:
             saver.restore(sess, path)

           feed = {}
                for i in range(nmt.input_sequence_length):
                  feed[encoder_inputs[i].name] =   
            np.array([spanish_sentences_encoded[j][i] for j in    
            range(len(spanish_sentences_encoded))], dtype = np.int32)

             feed[decoder_inputs[0].name] =  
           np.array([nmt.english_word2idx['<go>']] *     
          len(spanish_sentences_encoded), dtype = np.int32)

            output_sequences = sess.run(outputs_proj, feed_dict = feed)

              for i in range(len(english_sentences_encoded)):
                   ouput_seq = [output_sequences[j][i] for j in 
                    range(nmt.output_sequence_length)]
                    words = decode_output(ouput_seq)

                for j in range(len(words)):
                   if words[j] not in ['<eos>', '<pad>', '<go>']:
                       print(words[j], end=" ")

                 print('\n--------------------------------')
  1. 现在,我们定义 decode_output 函数,并详细解释其功能:
         def decode_output(output_sequence):
             words = []
            for i in range(nmt.output_sequence_length):
                smax = nmt.softmax(output_sequence[i])
                maxId = np.argmax(smax)
               words.append(nmt.english_idx2word[maxId])
              return words
  1. 我们以与占位符名称相同的方式准备数据字典。对于 encoder_inputs,我们使用来自 spanish_sentences_encoded 的值。对于 decoder_inputs,我们使用保存在模型检查点文件夹中的值。

  2. 使用前述数据,我们的模型计算 output_sentences

  3. 最后,我们使用decode_output函数将预测的output_sentences矩阵转换为实际的句子。为此,我们使用english_idx2word字典。

在运行前面的代码之后,你应该能看到原始的西班牙语句子以及其英文翻译。正确的输出如下:

1--------------------------------
Como te llamas
What's your name

2--------------------------------
Mi nombre es
My name is
3--------------------------------
Estoy leyendo un libro
I am reading a book
4--------------------------------
Que tal
How are you
5--------------------------------
Estoy bien
I am good
6--------------------------------
Hablas espanol
Do you speak Spanish
7--------------------------------
Que hora es
What time is it
8--------------------------------
Hola
Hi
9--------------------------------
Adios
Goodbye
10--------------------------------
Si 
Yes
11--------------------------------
No
No

接下来,我们将看到如何评估我们的结果,并识别我们的翻译模型表现如何。

评估最终结果

翻译模型通常使用所谓的 BLEU 评分来评估(www.youtube.com/watch?v=DejHQYAGb7Q)。这是一个自动生成的评分,它将人类生成的翻译与预测进行比较。它检查特定单词的出现与否、它们的排序以及任何扭曲——也就是说,它们在输出中的分离程度。

BLEU 评分的范围在01之间,0表示预测与人类生成的句子没有匹配的单词,1则表示两个句子完全匹配。

不幸的是,这个评分对单词断裂的位置较为敏感。如果单词断裂的位置不同,评分可能会完全不准确。

一个好的机器翻译模型,例如谷歌的多语言神经机器翻译系统,在西班牙语到英语的翻译中得分大约为38.0 (0.38*100)。这是一个表现异常出色的模型示例。结果相当显著,但正如你所见,仍然有很大的改进空间。

总结

本章带你通过使用 TensorFlow 库实现的序列到序列模型构建了一个相当复杂的神经网络模型。

首先,你了解了理论部分,理解了模型的工作原理及其应用为何能取得显著的成就。此外,你学习了 LSTM 网络的工作原理,并且明白了为什么它被认为是最佳的 RNN 模型。

其次,你看到了如何将这里学到的知识通过几行代码付诸实践。此外,你理解了如何准备数据以适配序列到序列模型。最后,你成功地将西班牙语句子翻译成了英语。

我真心希望本章能够让你对深度学习的知识更有信心,并赋予你可以应用于未来应用程序的新技能。

外部链接

第五章:构建你的个人助手

在本章中,我们将将全部注意力集中在构建对话型聊天机器人时递归神经网络的实际应用上。通过利用你最新掌握的序列模型知识,你将创建一个端到端的模型,旨在得到有意义的结果。你将使用一个基于 TensorFlow 的高级库,名为 TensorLayer。这个库可以让你更轻松地创建像聊天机器人这样的复杂系统的简单原型。我们将涵盖以下主要话题:

  • 我们在构建什么?:这是对具体问题及其解决方案的更详细介绍。

  • 准备数据:和往常一样,任何深度学习模型都需要这一步,因此在这里提及它至关重要。

  • 创建聊天机器人网络:你将学习如何使用 TensorLayer 构建聊天机器人所需的序列到序列模型图。

  • 训练聊天机器人:这一步将数据和网络图结合起来,以找到最合适的权重和偏置的组合。

  • 构建对话:最后一步使用已经训练好的模型,并结合示例句子,生成有意义的对话。

我们在构建什么?

本章的重点是带你一步步构建一个能够回答不同问题的简单对话型聊天机器人。近年来,聊天机器人越来越受欢迎,我们可以在许多实际应用中看到它们。

你可以在以下一些领域看到此软件的应用:

  • 客户与企业之间的沟通,其中聊天机器人帮助用户找到他们需要的东西,或者提供支持如果某些东西没有正常工作。例如,Facebook 提供了一种非常方便的方式来为你的企业实现聊天机器人。

  • 语音控制系统背后的个人助手,如 Amazon Alexa、Apple Siri 等:你将体验一个完整的端到端类人对话,可以设置提醒、订购产品等。

我们的简单示例将展示一个稍微扩展版的 TensorLayer 聊天机器人代码示例(github.com/tensorlayer/seq2seq-chatbot)。我们将使用由预收集推文组成的数据集,并将利用序列到序列模型。回顾之前的章节,这种模型使用两个递归神经网络,第一个是编码器,第二个是解码器。稍后我们将详细介绍如何使用这种架构来构建聊天机器人。

准备数据

在本节中,我们将专注于如何将我们的数据(在此案例中为推文)转换以满足模型的要求。我们将首先看到,如何使用来自 GitHub 任务仓库中的data/文件夹中的文件,模型可以帮助我们提取所需的推文。然后,我们将看看如何通过一组简单的函数,将数据拆分并转换为所需的结果。

一个重要的文件是data.py,位于data/twitter文件夹内。它将纯文本转换为数字格式,以便我们轻松训练网络。我们不会深入探讨其实现,因为你可以自己查看。在运行代码后,我们会生成三个重要的文件:

  • idx_q.npy:这是一个包含所有单词在不同句子中索引表示的数组,构成了聊天机器人问题的内容。

  • idx_a.npy:这是一个包含所有单词在不同句子中索引表示的数组,构成了聊天机器人回答的内容。

  • metadata.pkl:它包含了用于此数据集的索引到单词idx2w)和单词到索引w2idx)字典。

现在,让我们聚焦于这个数据的实际应用。你可以在本章 GitHub 仓库中的ch5_task.py文件的前 20 行查看它的使用。

首先,我们导入几个将在整个程序中使用的 Python 库:

import time
import tensorflow as tflow
import tensorlayer as tlayer
from sklearn.utils import shuffle
from tensorlayer.layers import EmbeddingInputlayer, Seq2Seq, DenseLayer, retrieve_seq_length_op2

以下是这些库的详细说明,附带描述:

  • time:这是用于跟踪操作所花时间的变量。你将在接下来的部分看到它的使用,在那里我们将训练网络。

  • tensorflow:它仅用于执行少数操作(初始化变量、使用 Adam 优化器优化网络,以及初始化 TensorFlow 会话:tf.Session())。

  • tensorlayer:正如你已经知道的,TensorLayer(tensorlayer.readthedocs.io/en/stable/)是一个基于 TensorFlow 的深度学习库。它提供了广泛的方法和类,使得任何开发者都能轻松构建复杂任务的解决方案。这个库将帮助我们轻松构建和训练我们的序列到序列模型。

  • shuffle:我们用它来打乱trainXtrainY中表示不同句子的所有数组。你将在接下来的部分看到我们如何获取trainXtrainY

  • EmbeddingInputlayer:一个 TensorLayer 类,表示序列到序列模型的输入层。正如你所知道的,每个Seq2Seq模型都有两个输入层,编码器和解码器。

  • Seq2Seq:一个 TensorLayer 类,用于构建类似于下图所示的序列到序列模型:

  • DenseLayer:TensorLayer 表示的全连接(密集)层。有多种类型的层执行不同的转换,且在特定场景中使用。例如,我们已经使用过递归层,它用于时间序列数据。还有用于图像的卷积层等等。你可以通过这个视频了解更多内容(www.youtube.com/watch?v=FK77zZxaBoI)。

  • retrieve_seq_length_op2:一个 TensorLayer 函数,用于计算序列长度,排除任何零填充部分。我们将会在编码和解码序列中使用这个函数。

导入库后,我们需要按如下方式访问数据:

from data.twitter import data
metadata, idx_q, idx_a = data.load_data(PATH='data/twitter/')
(trainX, trainY), (testX, testY), (validX, validY) = data.split_dataset(idx_q, idx_a)

首先,我们从 GitHub 仓库中的data/twitter/文件夹加载metadataidx_qidx_a。其次,我们使用split_dataset方法将编码器(idx_q)和解码器(idx_a)数据分为训练集(70%)、测试集(15%)和验证集(15%)。

最后,我们将trainX, trainY, testX, testY, validX, validY转换为 Python 列表,然后使用 TensorLayer 函数tlayer.prepro.remove_pad_sequences()从每个列表的末尾去除填充(零元素)。

将前面的操作结合起来,就得到了明确的训练、测试和验证数据。你将在本章后面看到我们如何在训练和预测中使用这些数据。

创建聊天机器人网络

本节是最重要的部分之一,因此你需要确保自己能够充分理解它,以便掌握我们应用程序的整体概念。我们将介绍将用于训练和预测的网络图。

但首先,让我们定义模型的超参数。这些是预定义的常量,对于确定模型的表现非常重要。正如你将在下一章中学到的那样,我们的主要任务是调整超参数的值,直到我们对模型的预测满意为止。在这种情况下,选择了一组初始的超参数。当然,为了更好的性能,必须对它们进行一些优化。本章不会专注于这一部分,但我强烈建议你使用本书最后一章(第六章,提升你的 RNN 性能)中的技术来优化这些参数。当前的超参数选择如下:

batch_size = 34
embedding_dimension = 1024
learning_rate = 0.0001
number_epochs = 1000

以下是这些超参数的简要解释:

  • batch_size:此项决定每个批次应包含多少元素。通常,训练是在批次上进行的,其中数据被分割成子数组,每个子数组的大小为batch_size

  • embedding_dimension:此项决定词嵌入向量的大小。输入中的一个单词会被编码成大小为embedding_dimension的向量。

  • learning_rate:其值决定了网络学习的速度。通常这是一个非常小的值(0.001, 0.0001)。如果在训练过程中损失函数没有下降,通常的做法是减小学习率。

  • number_epochs:此项决定训练迭代次数(epoch)。在每次迭代开始时,我们会打乱数据,并且由于一个 epoch 的大小太大,无法一次性输入计算机,我们将其分成多个较小的批次。然后,我们使用这些批次来训练网络。在每次迭代之后,我们会再次打乱数据并执行第二个 epoch。这个操作会根据我们设置的 epoch 数量进行。

在确定了超参数集后,接下来需要为我们构建模型提供额外的值:

xseq_len = len(trainX)
yseq_len = len(trainY)
assert xseq_len == yseq_len

n_step = int(xseq_len/batch_size)

w2idx = metadata['w2idx']
idx2w = metadata['idx2w']

xvocab_size = len(metadata['idx2w'])
start_id = xvocab_size
end_id = xvocab_size+1

w2idx.update({'start_id': start_id})
w2idx.update({'end_id': end_id})
idx2w = idx2w + ['start_id', 'end_id']

xvocab_size = yvocab_size = xvocab_size + 2

让我们逐行检查每一项:

xseq_len = len(trainX)
yseq_len = len(trainY)
assert xseq_len == yseq_len

我们使用xseq_lenyseq_len来存储编码器和解码器输入序列的长度。然后,我们确保这两个值相等,否则程序将会中断。

n_step = int(xseq_len/batch_size):通过这个,我们存储训练即将执行的步数。这个值仅用于打印训练状态,稍后我们会在本章中看到它的用法。

我们使用w2idxidx2w以两种格式存储词典(词作为字典键,ID 作为字典键)。这些字典在预测聊天机器人响应时使用:

w2idx = metadata['w2idx']
idx2w = metadata['idx2w']

我们设置start_id = xvocab_sizeend_id = xvocab_size + 1以确保这两个索引的唯一性。它们用于表示单个句子的开始和结束:

xvocab_size = len(metadata['idx2w'])
start_id = xvocab_size
end_id = xvocab_size+1

最后,我们扩展这些字典,包含起始和结束元素。我们数据的一个示例集如下:

  • encode_seqs(输入编码器句子):['how', 'are', 'you', '<PAD_ID>']

  • decode_seqs(输入解码器句子):['<START_ID>', 'I', 'am', 'fine', '<PAD_ID>']

  • target_seqs(预测的解码器句子):['I', 'am', 'fine', '<END_ID>', '<PAD_ID>']

  • target_mask(每个序列应用的掩码):[1, 1, 1, 1, 0]。这是一个与target_seqs大小相同的数组,但在应用填充的地方为0,其他地方为1。你可以通过阅读这篇很棒的 Quora 回答来了解更多关于循环神经网络中掩码的知识(www.quora.com/What-is-masking-in-a-recurrent-neural-network-RNN)。

下一步是定义我们的模型结构。我们从引入模型的占位符开始:

encode_seqs = tf.placeholder(dtype=tf.int64, shape=[batch_size, None], name="encode_seqs")
decode_seqs = tf.placeholder(dtype=tf.int64, shape=[batch_size, None], name="decode_seqs")
target_seqs = tf.placeholder(dtype=tf.int64, shape=[batch_size, None], name="target_seqs")
target_mask = tf.placeholder(dtype=tf.int64, shape=[batch_size, None], name="target_mask")

如你所见,这是之前展示的相同变量集。每个变量都有一个batch_size维度和tf.int64类型。然后,我们按如下方式计算模型输出:

net_out, _ = model(encode_seqs, decode_seqs, is_train=True, reuse=False)

上述代码行的目的是通过输入编码器和解码器序列来找到网络的输出。我们将在接下来的部分定义并解释model方法。

最后,我们定义损失函数和优化器:

loss = tl.cost.cross_entropy_seq_with_mask(logits=net_out.outputs, target_seqs=target_seqs, input_mask=target_mask, name='cost')

optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)

如你所见,损失函数是应用了掩码的交叉熵,以确保每个输入序列的长度相同。logits(预测输出)来自前面的模型输出,并通过net_out.outputs访问。target_seqs是每个输入的预期结果。

模型的优化器是AdamOptimizer,并通过 TensorFlow 内置函数tf.train.AdamOptimizer来定义。像往常一样,我们传递learning_rate来决定loss函数最小化的速率。

最后的步骤是定义并解释model函数:

def model(encode_seqs, decode_seqs, is_train=True, reuse=False):
   with tf.variable_scope("model", reuse=reuse):
        with tf.variable_scope("embedding") as vs:
            net_encode = EmbeddingInputlayer(
                inputs = encode_seqs,
                vocabulary_size = xvocab_size,
                embedding_size = embedding_dimension,
                name = 'seq_embedding')
                vs.reuse_variables()
            net_decode = EmbeddingInputlayer(
                inputs = decode_seqs,
                vocabulary_size = xvocab_size,
                embedding_size = embedding_dimension,
                name = 'seq_embedding')
           net_rnn = Seq2Seq(net_encode, net_decode,
                cell_fn = tf.contrib.rnn.BasicLSTMCell,
                n_hidden = embedding_dimension,
                initializer = tf.random_uniform_initializer(-0.1, 0.1),
                encode_sequence_length = 
                retrieve_seq_length_op2(encode_seqs),
                decode_sequence_length = 
                retrieve_seq_length_op2(decode_seqs),
                initial_state_encode = None,
                n_layer = 3,
                return_seq_2d = True,
                name = 'seq2seq')
                net_out = DenseLayer(net_rnn, n_units=xvocab_size, 
                act=tf.identity, name='output')
      return net_out, net_rnn

TensorLayer 尽可能简化了构建序列到序列模型的过程。它使用了四个主要组件:

  • net_encode:使用EmbeddingInputlayer类的编码器网络。

  • net_decode:使用EmbeddingInputlayer类的解码器网络。

  • net_rnn: 一个将两个上述网络组合起来的序列到序列模型。它使用Seq2Seq 类实现。

  • net_out: 最终的全连接(密集)层,产生最终结果。该层建立在序列到序列网络之上。

使用EmbeddingInputlayer (tensorlayer.readthedocs.io/en/stable/modules/layers.html#tensorlayer.layers.EmbeddingInputlayer) 类初始化net_encodenet_decode。使用了三个重要的参数:inputsvocabulary_sizeembedding_sizeinputs 是我们在前面部分中定义的 encode_seqsdecode_seqs。在两种情况下,vocabulary_size 等于 xvocab_sizeembedding_size 等于 embedding_dimension。这个嵌入层将输入向量转换为指定 embedding_dimension 大小之一。

net_rnn 将编码器和解码器层结合成一个完整的序列到序列模型。以下是其参数:

  • cell_fn: 整个网络中使用的 RNN 单元。在我们的情况下,这是BasicLSTMCell

  • n_hidden: 每个网络层中隐藏单元的数量。

  • initializer: 用于定义参数(权重、偏置)的分布。

  • encode_sequence_length: 指定编码器输入序列的长度。它使用retrieve_seq_length_op2 (tensorlayer.readthedocs.io/en/stable/modules/layers.html#tensorlayer.layers.retrieve_seq_length_op2) 方法来处理encode_seqs

  • decode_sequence_length: 指定解码器输入序列的长度。它使用retrieve_seq_length_op2 方法处理decode_seqs

  • initial_state_encode: 如果为None,编码器网络的初始状态为零状态,并可以由占位符或另一个循环神经网络自动设置。

  • n_layer: 每个两个网络(编码器和解码器)堆叠的 RNN 层的数量。

  • return_seq_2d: 如果为 True,返回 2D Tensor [n_example, 2 * n_hidden],以便在其后堆叠 DenseLayer

最后,我们使用一个全连接(密集)层net_out来计算网络的最终输出。它将Seq2Seq 网络作为前一层,词汇表大小(xvocab_size)作为单元数,并使用tf.identity 作为激活函数。通常用于在设备之间显式传输张量的场景(例如,从 GPU 到 CPU)。在我们的情况下,我们使用它来构建复制前一层数值的虚拟节点。

最后需要指出的是reuse参数和vs.reuse_variables()方法调用的使用。在训练过程中,我们并没有重用模型的参数(权重和偏置),因此reuse = False,但是在预测聊天机器人响应时,我们会利用预训练的参数,因此将reuse = True。该方法调用会触发下一组计算的重用。

到这里,我们已经完成了模型的定义。从现在开始,剩下的只有两个部分:训练和预测。

训练聊天机器人

一旦我们定义了模型图,我们就希望使用输入数据来训练它。然后,我们将拥有一组经过良好调整的参数,可以用于准确的预测。

首先,我们指定 TensorFlow 的 Session 对象,它封装了执行操作(加法、减法等)对象和评估张量(占位符、变量等)对象的环境:

sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=False))
sess.run(tf.global_variables_initializer())

关于config参数的一个很好的解释可以在stackoverflow.com/questions/44873273/what-do-the-options-in-configproto-like-allow-soft-placement-and-log-device-plac找到。总结来说,一旦我们指定了allow_soft_placement,只有在没有注册 GPU 的情况下,操作才会在 CPU 上执行。如果该值为 false,则不允许在 GPU 上执行任何操作。

只有在运行第二行代码(sess.run(tf.global_variables_initializer()))后,所有的变量才会实际持有它们的值。最初,它们只存储一个持久的张量。

现在,我们将使用train()函数来训练网络,函数定义如下:

def train():
    print("Start training")
    for epoch in range(number_epochs):
        epoch_time = time.time()
        trainX_shuffled, trainY_shuffled = shuffle(trainX, trainY, 
        random_state=0)
        total_err, n_iter = 0, 0

        for X, Y in tl.iterate.minibatches(inputs=trainX_shuffled, 
        targets=trainY_shuffled, batch_size=batch_size, shuffle=False):

            X = tl.prepro.pad_sequences(X)

            _decode_seqs = tl.prepro.sequences_add_start_id(Y, 
             start_id=start_id, remove_last=False)
            _decode_seqs = tl.prepro.pad_sequences(_decode_seqs)   

            _target_seqs = tl.prepro.sequences_add_end_id(Y, 
             end_id=end_id)
            _target_seqs = tl.prepro.pad_sequences(_target_seqs)
            _target_mask = tl.prepro.sequences_get_mask(_target_seqs)

            _, err = sess.run([optimizer, loss],
                                {encode_seqs: X,
                                decode_seqs: _decode_seqs,
                                target_seqs: _target_seqs,
                                target_mask: _target_mask})

            if n_iter % 200 == 0:
                print("Epoch[%d/%d] step:[%d/%d] loss:%f took:%.5fs" % 
                (epoch, number_epochs, n_iter, n_step, err, time.time() 
                 - epoch_time))

                total_err += err; n_iter += 1

让我们逐行解释前面的代码做了什么。

该实现有两个嵌套循环,其中外层循环决定训练应该遍历整个数据集多少次。通常使用 epoch 来完成此任务,目的是加强模型的准确性。因为权重和偏置在仅仅一次传播中,往往无法从某个示例中学到足够的内容。所以,我们应该多次遍历每个示例——在我们这个例子中是 1,000 次(epoch 的数量)。

在进入一个 epoch 迭代后,我们使用sklearn中的shuffle函数打乱数据,为进入内部循环做准备。然后,我们使用tl.iterate.minibatches将数据分割成子数组,每个子数组的大小为batch_size。内部循环中的每一次迭代都会使用当前批次的数据训练网络。

在计算优化器之前,我们对 X(编码器批数据)和 Y(解码器批数据)做一些小的修改。如你所记得,模型有一个编码器输入(encoder_seqs)、一个解码器输入(decoder_seqs)和一个目标输出(target_seqs),并且它们被结合进两个 RNN 中。

第一个递归神经网络是编码器,它接受encoder_seqs作为输入。在上面的代码块中,它标记为X。我们只需要在将此序列应用到网络之前给它添加填充。填充是将零添加到序列的末尾,使其与固定长度匹配,这个长度是由训练集中最长的序列决定的。这个网络会生成一个向量,之后它会被用到第二个 RNN 中。

第二个递归神经网络接收来自第一个 RNN 的编码向量和解码器输入序列(decoder_seqs),并返回预测结果。在训练过程中,我们将预测结果与目标序列(target_seqs)进行比较,而目标序列恰好与预测结果相同。

让我们澄清一下前面的陈述。假设你有一个输入句子Hello, how are you?,它的回应是I am fine.。第一个句子进入编码器网络。第二个句子是第二个解码器网络的预期输出。我们需要将这个预期输出与解码器实际产生的输出进行比较。我们先得到第一个单词I,并尝试预测下一个单词am,然后得到am并尝试预测fine,依此类推。刚开始时,我们的预测可能会偏差很大,但随着时间推移,权重和偏差应该会调整得更加准确。下面的图表可以辅助说明:

如你所见,我们需要在decoder_seqs中添加一个起始符号,在target_seqs中添加一个结束符号。这正是_decode_seqs = tl.prepro.sequences_add_start_id(Y, start_id=start_id, remove_last=False)_target_seqs = tl.prepro.sequences_add_end_id(Y, end_id=end_id)所做的事情,其中start_id = xvocab_sizeend_id = xvocab_size+1。最后,我们给两个序列都添加填充,使它们长度相等。

在实际训练之前,我们从_target_seqs中提取_target_mask。回想一下,如果_target_seqs = ["I", "am", "fine", "<END_ID>", "<PAD_ID>"],那么_target_mask = [1, 1, 1, 1, 0]

最终,我们使用之前定义的序列数组来训练我们的网络。这可能需要一些时间,因此我们在每 200 次迭代后添加了打印语句。我建议你在训练时让计算机整夜运行,这样你就能从数据中提取最大潜力。

下一步是使用我们的模型预测实际输出。让我们看看它能多好地完成这个任务。

构建对话

这一步实际上与训练步骤非常相似。第一个区别是我们不会对预测结果进行评估,而是使用输入生成结果。第二个区别是我们使用已训练好的变量集来生成此结果。你将看到本章后面如何实现这一点。

为了更清楚地说明,我们首先初始化一个新的序列到序列的模型。其目的是使用已经训练好的权重和偏置,并根据不同的输入集进行预测。我们只有一个编码器和解码器序列,其中编码器序列是输入句子,而解码器序列则是一次输入一个单词。我们定义新模型如下:

encode_seqs2 = tf.placeholder(dtype=tf.int64, shape=[1, None], name="encode_seqs")
decode_seqs2 = tf.placeholder(dtype=tf.int64, shape=[1, None], name="decode_seqs")
net, net_rnn = model(encode_seqs2, decode_seqs2, is_train=False, reuse=True)
y = tf.nn.softmax(net.outputs)

如你所见,它遵循与训练架构完全相同的模式,唯一的区别是我们的序列矩阵形状为 1,而不是 batch_size

需要注意的一点是,在计算网络结果时,我们必须重用训练过程中使用的相同参数。这一步至关重要,因为它确保了我们的预测是最近训练结果的产物。

最后,我们使用 softmax 函数计算最终输出y。这通常在最后一层完成,以确保我们的向量值加起来为 1,并且是分类过程中的必要步骤。

在定义好我们的新模型后,接下来就是进行实际预测的时刻。我们遵循以下模式:

  1. 生成一个初始句子,作为对话的开端。

  2. 使用 word2idx 字典将句子转换为单词索引列表。

  3. 决定我们希望对话有多少次来回交互(在我们的案例中,这将是五次)。

  4. 通过将初始句子传入 net_rnn(如前所定义)来计算编码器的最终状态。

  5. 最后,我们使用之前预测的单词和网络迭代地预测下一个单词。在第一次迭代时,我们使用先前定义的 start_id 作为解码器的第一个单词。

这些步骤在以下代码片段中执行:

def predict():
    seeds = ["happy birthday have a nice day",
            "the presidential debate held last night was spectacular"]
    for seed in seeds:
        seed_id = [w2idx[w] for w in seed.split(" ")]
        for _ in range(5):  # 5 Replies
            # 1\. encode, get state
            state = sess.run(net_rnn.final_state_encode,
                            {encode_seqs2: [seed_id]})

            # 2\. decode, feed start_id, get first word
            o, state = sess.run([y, net_rnn.final_state_decode],
                            {net_rnn.initial_state_decode: state,
                            decode_seqs2: [[start_id]]})

            w_id = tl.nlp.sample_top(o[0], top_k=3)
            w = idx2w[w_id]

            # 3\. decode, feed state iteratively
            sentence = [w]
            for _ in range(30): # max sentence length
                o, state = sess.run([y, net_rnn.final_state_decode],
                                {net_rnn.initial_state_decode: state,
                                decode_seqs2: [[w_id]]})
                w_id = tl.nlp.sample_top(o[0], top_k=2)
                w = idx2w[w_id]
                if w_id == end_id:
                    break
                sentence = sentence + [w]

            print(" >", ' '.join(sentence))

一个有趣的地方在于,# 2\. decode, feed start_id, get first word# 3\. decode, feed state iteratively 执行的动作完全相同,但步骤 #2 是一个特殊的情况,专注于仅预测第一个单词。步骤 #3 使用第一个单词,迭代地预测后续所有单词。

tl.nlp.sample_top(o[0], top_k=3) 可能也会让你感到困惑。这一行从概率数组 o[0] 中采样一个索引,考虑的候选项只有三个。同样的功能也适用于 w_id = tl.nlp.sample_top(o[0], top_k = 2)。你可以在 TensorLayer 文档中了解更多内容(tensorlayer.readthedocs.io/en/stable/modules/nlp.html#sampling-functions)。

最后,我们打印出由 30 个单词组成的句子(我们限制了每个句子的单词数)。如果你训练了足够长的时间,你应该能看到一些不错的结果。如果结果不满意,那就需要进行大量的工作。你将在接下来的第六章中了解更多内容,提升你的 RNN 性能

总结

本章展示了一个完整的聊天机器人系统实现,该系统能够构建一个简短的对话。原型详细展示了构建智能聊天机器人的每个阶段,包括收集数据、训练网络和进行预测(生成对话)。

对于网络的架构,我们使用强大的编码器-解码器序列到序列模型,该模型利用两个递归神经网络,并通过编码器向量连接它们。在实际实现中,我们使用一个建立在 TensorFlow 之上的深度学习库——TensorLayer。通过引入简单的一行代码实现标准模型,如序列到序列,它简化了大部分工作。此外,该库在训练之前对数据进行预处理时也非常有用。

下一章将重点讨论构建递归神经网络(以及任何深度学习模型)过程中,可能是最重要的部分——如何提升性能并使程序返回令人满意的结果。正如你已经看到的,在大多数基础/中等难度的示例中,构建神经网络遵循类似的模式。困难的部分是确保实现确实有效并产生有意义的结果。这将是我们下一章的重点——希望你能喜欢。

外部链接

第六章:提高你的 RNN 性能

本章介绍了一些提高递归神经网络模型效果的技术。通常,模型的初始结果可能令人失望,因此你需要找到改善它们的方法。可以通过多种方法和工具来实现这一点,但我们将重点关注两个主要领域:

  • 通过数据和调整来改善 RNN 模型性能

  • 优化 TensorFlow 库以获得更好的结果

首先,我们将看到如何通过增加数据量以及调整超参数,来获得显著更好的结果。然后,我们将重点放在如何充分利用 TensorFlow 的内建功能。这两种方法适用于任何涉及神经网络模型的任务,因此,下次你想使用卷积网络进行图像识别或用 GAN 修复一个重新缩放的图像时,可以应用相同的技巧来完善你的模型。

改善你的 RNN 模型

在使用 RNN(或任何其他网络)解决问题时,你的过程大致如下:

首先,你提出模型的想法,其超参数、层数、网络的深度等。然后,实现并训练模型,以产生一些结果。最后,评估这些结果并进行必要的修改。通常情况下,你不会在第一次运行时得到有意义的结果。这个周期可能会多次发生,直到你对结果感到满意为止。

考虑到这种方法,一个重要的问题浮现出来:我们如何改变模型,使得下一个周期产生更好的结果?

这个问题与理解网络结果密切相关。现在让我们来讨论这个问题。

如你所知,在每次模型训练的开始,你需要准备大量优质数据。这一步应发生在上述循环的想法部分之前。然后,在想法阶段,你应该构思出实际的神经网络及其特征。接下来是代码阶段,在该阶段,你用数据来支持模型并进行实际训练。有一点很重要——一旦你的数据收集完成,你需要将其分为三部分:训练(80%)、验证(10%)和测试(10%)

代码阶段仅使用训练部分数据。然后,实验阶段使用验证部分数据来评估模型。根据这两项操作的结果,我们将进行必要的调整。

在你完成所有必要的循环并确定模型表现良好后,你只应使用测试数据。测试数据将帮助你理解在未见过的数据上你得到的准确率。

在每个周期结束时,你需要评估你的模型表现如何。根据结果,你会发现你的模型总是出现欠拟合高偏差)或过拟合高方差)(程度不同)。你应该努力使偏差和方差都很低,这样几乎不会发生欠拟合或过拟合。下图可能帮助你更好地理解这一概念:

通过查看前面的图表,我们可以给出以下定义:

  • 欠拟合(高偏差):当网络没有足够地受到训练数据的影响,并且过于泛化预测时,就会发生欠拟合。

  • 恰到好处(低偏差,低方差):当网络能够做出高质量的预测时,无论是在训练期间,还是在测试时的通用情况下。

  • 过拟合(高方差):当网络受到训练数据的影响过大,并且对新输入做出错误决策时,就会发生过拟合。

前面的图表有助于理解高偏差和高方差的概念,但将这些应用到实际案例中比较困难。问题在于我们通常处理的是多于两维的数据。因此,我们将使用模型生成的损失(错误)函数值来对更高维的数据进行相同的评估。

假设我们正在评估第四章中的西班牙语到英语的翻译神经网络,创建一个西班牙语到英语的翻译器。我们可以假设,最小的错误率是由人类完成的,约为 1.5%。现在我们将基于我们网络可能给出的所有错误组合来评估结果:

  • 训练数据误差:2%;验证数据误差:14%:高方差

  • 训练数据误差:14%;验证数据误差:15%:高偏差

  • 训练数据误差:14%;验证数据误差:30%:高方差,高偏差

  • 训练数据误差:2%;验证数据误差:2.4%:低方差,低偏差

所期望的结果是既有低方差又有低偏差。当然,得到这种改进需要大量的时间和精力,但最终是值得的。

现在你已经熟悉了如何读取模型结果并评估模型的表现。接下来,我们来看一下如何降低模型的方差和偏差

我们如何降低方差?(解决过拟合)

一种非常有用的方法是收集和转换更多的数据。这将使模型具有更好的泛化能力,并在训练集和验证集上都表现良好。

我们如何降低偏差?(解决欠拟合)*

这可以通过增加网络的深度来实现——即改变层数和隐藏单元的数量,并调整超参数。

接下来,我们将讨论这两种方法,并了解如何有效地使用它们来提高我们神经网络的性能。

通过数据改善性能

大量的优质数据对于任何深度学习模型的成功至关重要。可以做一个很好的比较:对于其他算法,数据量的增加并不一定能提高性能:

但这并不意味着收集更多数据总是正确的方法。例如,如果我们的模型出现了欠拟合,更多数据并不会提高性能。另一方面,解决过拟合问题可以通过使用正是这种方法来完成。

使用数据提升模型性能分为三个步骤:选择数据处理数据转换数据。需要注意的是,所有这三步应该根据你具体的问题来完成。例如,对于某些任务,如识别图像中的数字,一个格式良好的数据集很容易找到。而对于更为具体的任务(例如分析植物图像),你可能需要进行实验并做出一些非平凡的决策。

选择数据

这是一种相当直接的技术。你要么收集更多数据,要么发明更多的训练示例。

查找更多数据可以通过在线数据集集合来完成 (skymind.ai/wiki/open-datasetshttps://skymind.ai/wiki/open-datasets)。其他方法包括抓取网页,或者使用 Google 搜索的高级选项 (www.google.com/advanced_search)。

另一方面,发明或增加数据是一个具有挑战性和复杂的问题,特别是当我们尝试生成文本或图像时。例如,最近创建了一种增强文本的新方法 (www.quora.com/What-data-augmentation-techniques-are-available-for-deep-learning-on-text)。这个方法通过将一句英语句子翻译成另一种语言,然后再翻译回英语来完成。这样,我们得到了两句稍有不同但有意义的句子,从而大大增加和多样化了我们的数据集。另一种增强数据的有趣技术,专门针对 RNN 语言模型,可以在论文《神经网络语言模型中的数据噪声作为平滑》中找到 (arxiv.org/abs/1703.02573)。

处理数据

在选择所需数据后,接下来是处理阶段。这可以通过以下三个步骤来完成:

  • 格式化:这涉及将数据转换为最适合你应用的格式。例如,假设你的数据是来自成千上万的 PDF 文件中的文本。你应该提取文本并将数据转换为 CSV 格式。

  • 清理:通常情况下,数据可能是缺失的。例如,如果你从互联网上抓取了书籍元数据,某些条目可能会缺少数据(例如 ISBN、写作日期等)。你的任务是决定是修复这些元数据还是舍弃整个书籍的元数据。

  • 采样:使用数据集的一小部分可以减少计算时间,并在确定模型准确性时加速训练过程。

前面步骤的顺序并没有固定,你可以多次回头再做。

数据转换

最后,你需要使用缩放、分解和特征选择等技术来转换数据。首先,最好使用 Matplotlib(一个 Python 库)或 TensorFlow 的 TensorBoard(www.tensorflow.org/guide/summaries_and_tensorboard)来绘制/可视化你的数据。

缩放是一种将每个条目转换为特定范围内(0-1)的数字,而不降低其有效性的技术。通常,缩放是在激活函数的范围内完成的。如果你使用的是 sigmoid 激活函数,请将数据缩放到 0 到 1 之间的值。如果使用的是双曲正切(tanh)激活函数,则将数据缩放到-1 到 1 之间的值。这适用于输入(x)和输出(y)。

分解是一种将某些特征分解为其组件并使用它们的技术。例如,特征“时间”可能包含分钟和小时,但我们只关心分钟。

特征选择是构建模型时最重要的决策之一。选择最合适特征的一个很好的教程是 Jason Brownlee 的特征选择简介machinelearningmastery.com/an-introduction-to-feature-selection/)。

数据的处理和转换可以使用众多 Python 库来实现,例如 NumPy 等。这些工具在数据处理时非常有用。

在完成所有前面的步骤(可能需要多次操作)之后,你可以继续构建你的神经网络模型。

通过调优提高性能

在选择、处理和转换数据之后,是时候进行第二次优化技术——超参数调优。这种方法是构建模型中最重要的组成部分之一,你需要花时间将其执行得当。

每个神经网络模型都有参数和超参数。这是两个不同的值集合。参数是模型在训练过程中学习到的,例如权重和偏差。另一方面,超参数是预先定义的值,这些值在仔细观察后选择。在标准的递归神经网络中,超参数集合包括隐藏单元数、层数、RNN 模型类型、序列长度、批次大小、迭代次数(epoch)和学习率。

你的任务是识别所有可能组合中的最佳选择,以使网络表现得相当好。这是一个相当具有挑战性的任务,通常需要大量时间(几个小时、几天甚至几个月)和计算能力。

根据 Andrew Ng 关于超参数调优的教程(www.coursera.org/lecture/deep-neural-network/hyperparameters-tuning-in-practice-pandas-vs-caviar-DHNcc),我们可以将此过程分为两种不同的技术:PandasCaviar

Pandas方法遵循熊猫(即动物)养育后代的方式。我们用一组特定的参数初始化模型,然后在每次训练操作后改进这些值,直到取得令人满意的结果。如果你缺乏计算能力或多 GPU 来同时训练神经网络,这种方法是理想的选择。

Caviar方法遵循鱼类繁殖的方式。我们同时引入多个模型(使用不同的参数集),并在训练的同时追踪结果。这种技术可能需要更多的计算能力。

现在问题变成了:我们如何决定哪些内容应包含在超参数集内?

总结一篇关于超参数优化的精彩文章(neupy.com/2016/12/17/hyperparameter_optimization_for_neural_networks.html#tree-structured-parzen-estimators-tpe),我们可以定义五种调优方法:

  • 网格搜索

  • 随机搜索

  • 手动调优

  • 贝叶斯优化

  • 树结构 Parzen 估计器TPE

在深度学习之旅的初期阶段,你主要会使用网格搜索、随机搜索和手动调优。后两种技术在理解和实施上更为复杂。我们将在接下来的部分讨论这两种方法,但请记住,对于简单的任务,你可以选择正常的手动调优。

网格搜索

这是找到合适超参数的最直接方法。它遵循以下图表中的方法:

在这里,我们生成所有可能的超参数值组合,并进行单独的训练周期。对于小型神经网络来说,这种方法可行,但对于更复杂的任务来说则不切实际。这就是为什么我们应该使用以下部分列出的更好的方法。

随机搜索

这种技术类似于网格搜索。你可以参考这里的图表:

我们不是考虑所有可能的组合,而是随机抽取一小部分值,并用这些值来训练模型。如果我们发现某一组位置接近的点表现较好,我们可以更仔细地检查这个区域,并专注于它。

手动调优

更大的网络通常需要更多的训练时间。这就是为什么前述方法不适用于这种情况。在这些情况下,我们通常使用手动调优技术。其理念是初步尝试一组值,然后评估性能。之后,我们的直觉和学习经验可能会引导我们提出一系列特定的修改步骤。我们进行这些调整,并学到新的东西。经过若干次迭代,我们对未来改进所需的变动有了充分的理解。

贝叶斯优化

这种方法是一种无需手动确定不同值的超参数学习方式。它使用高斯过程,利用一组先前评估过的参数及其结果精度,对未观察到的参数做出假设。一个采集函数利用这些信息来建议下一组参数。更多信息,我建议观看 Hinton 教授的讲座,超参数的贝叶斯优化www.youtube.com/watch?v=cWQDeB9WqvU)。

树状帕尔岑估计器(TPE)

这种方法背后的理念是,在每次迭代中,TPE 会收集新的观察结果,迭代结束时,算法会决定应该尝试哪一组参数。更多信息,我建议阅读这篇精彩的文章,神经网络超参数优化neupy.com/2016/12/17/hyperparameter_optimization_for_neural_networks.html#tree-structured-parzen-estimators-tpe)。

优化 TensorFlow 库

本节主要关注可以直接在代码中实现的实用建议。TensorFlow 团队提供了一套庞大的工具集,可以用来提高你的性能。这些技术正在不断更新,以取得更好的结果。我强烈推荐观看 TensorFlow 2018 年大会上的训练性能视频(www.youtube.com/watch?v=SxOsJPaxHME)。此视频附带了精心整理的文档,也是必读之物(www.tensorflow.org/performance/)。

现在,让我们更详细地探讨如何实现更快、更可靠的训练。

首先,我们从 TensorFlow 提供的插图开始,展示了训练神经网络的总体步骤。你可以将这个过程分为三个阶段:数据处理执行训练优化梯度

  1. 数据处理 (第 1 步):此阶段包括获取数据(本地或来自网络)并将其转化为符合我们需求的格式。这些转换可能包括数据增强、批处理等。通常,这些操作是在CPU上执行的。

  2. 执行训练(第 2a、2b 和 2c 步):此阶段包括在训练期间进行前向传播计算,这需要一个特定的神经网络模型——LSTM、GPU,或者在我们这个案例中是基础的 RNN。这些操作使用强大的GPUTPU

  3. 优化梯度(第 3 步):此阶段包括最小化损失函数以优化权重的过程。这个操作同样是在GRUTPU上执行的。

这张图展示了上述步骤:

接下来,让我们解释如何改进这些步骤。

数据处理

你需要检查数据加载和转换是否成为性能瓶颈。你可以通过几种方法来进行检查,其中一些方法包括估算执行这些任务所需的时间,并跟踪 CPU 使用情况。

一旦你确定这些操作会降低模型的性能,就该应用一些有效的技巧来加速这一过程。

如我们所说,这些操作(数据加载和转换)应该在 CPU 上执行,而不是 GPU 上,这样你就可以为训练腾出 GPU。为确保这一点,可以按照以下方式包装你的代码:

with tf.device('/cpu:0'):
    # call a function that fetches and transforms the data
    final_data = fetch_and_process_data()

然后,你需要专注于数据的加载(获取)和转换过程。

改进数据加载

TensorFlow 团队一直在努力使这一过程尽可能简单,提供了tf.data API(www.tensorflow.org/performance/performance_guide),其效果非常好。要了解更多内容并学习如何高效使用它,我推荐观看 TensorFlow 关于tf.data的演讲(www.youtube.com/watch?v=uIcqeP7MFH0)。在此之前,你所看到的标准feed_dict方法应该始终被替代。

改善数据转换

数据转换可以有不同的形式,例如裁剪图像、分割文本、渲染和批处理文件。TensorFlow 为这些技术提供了解决方案。例如,如果你在训练前裁剪图像,最好使用tf.image.decode_and_crop_jpeg,该函数只解码图像中需要的部分。另一个可以优化的地方是批处理过程。TensorFlow 库提供了两种方法:

batch_normalization = tf.layers.batch_normalization(input_layer, fused=True, data_format='NCHW')

第二种方法如下:

batch_normalizaton = tf.contrib.layers.batch_norm(input_layer, fused=True, data_format='NCHW')

让我们澄清一下这些行:

  • 批量归一化是对神经网络模型进行的一种操作,目的是加速训练过程。有关更多细节,请参阅这篇精彩的文章,神经网络中的批量归一化towardsdatascience.com/batch-normalization-in-neural-networks-1ac91516821c

  • fused参数指示该方法是否应将执行批量归一化所需的多个操作合并为一个内核。

  • data_format参数指的是传递给某个操作(如求和、除法、训练等)的张量的结构。可以在 TensorFlow 性能指南中的数据格式部分找到一个很好的解释(www.tensorflow.org/performance/)。

执行训练

现在,让我们进入训练阶段。在这里,我们使用 TensorFlow 内置的函数来初始化递归神经网络单元,并使用预处理过的数据计算它们的权重。

根据你的情况,优化训练的不同技术可能更合适:

  • 对于小型和实验性的模型,你可以使用tf.nn.rnn_cell.BasicLSTMCell。不幸的是,这种方法效率低下,并且比以下优化版本占用更多的内存。因此,除非你只是做实验,否则推荐使用它。

  • 上述代码的优化版本是tf.contrib.rnn.LSTMBlockFusedCell。当你无法使用 GPU 或 TPU,并且想要运行更高效的单元时,应使用此方法。

  • 最佳的单元集可以在tf.contrib.cudnn_rnn.*下找到(例如,GPU 单元的CuddnnCompatibleGPUCell等)。这些方法在 GPU 上进行了高度优化,比前面的那些方法性能更好。

最后,你应该始终使用 tf.nn.dynamic_rnn 进行训练(参见 TensorFlow 文档:www.tensorflow.org/api_docs/python/tf/nn/dynamic_rnn)并传递特定的单元。此方法通过偶尔在 GPU 和 CPU 之间交换内存,优化了递归神经网络的训练,从而使得训练大型序列成为可能。

优化梯度

最后一种优化技巧实际上会提升我们反向传播算法的性能。回顾前几章,你在训练过程中要通过调整模型的权重和偏差来最小化损失函数。通过不同的内置 TensorFlow 优化器(如 tf.train.AdamOptimizertf.train.GradientDescentOptimizer)可以实现这些权重和偏差的调整(优化)。

TensorFlow 提供了通过此代码在多个 TPU 上分布处理的能力:

optimizer = tf.contrib.tpu.CrossShardOptimizer(existing_optimizer)

在这里,existing_optimizer = tf.train.AdamOptimizer(),你的训练步骤将如下所示:train_step = optimizer.minimize(loss)

总结

在本章中,我们讨论了许多优化模型性能的新颖且令人兴奋的方法,既有通用的策略,也有特别使用 TensorFlow 库的优化方法。

第一部分介绍了通过选择、处理和转换数据,以及调优超参数来提升 RNN 性能的技巧。你还学习了如何更深入地理解模型,现在你知道该怎么做才能让模型表现得更好。

第二部分专门聚焦于使用内置的 TensorFlow 函数以提升模型性能的实际方法。TensorFlow 团队旨在通过提供分布式环境和优化技术,让你可以通过少量代码快速实现想要的结果。

将本章中介绍的两种技巧结合起来,将有助于你在深度学习领域的知识,并且让你能够在不担心性能问题的情况下,尝试更复杂的模型。你获得的知识适用于任何神经网络模型,因此你可以自信地将相同的技术应用于更广泛的问题集。

外部链接

posted @ 2025-07-10 11:38  绝不原创的飞龙  阅读(43)  评论(0)    收藏  举报