TensorFlow-学习手册-全-
TensorFlow 学习手册(全)
译者:飞龙
前言
深度学习在过去几年中已经成为构建从数据中学习的智能系统的首要技术。深度神经网络最初受到人类大脑学习方式的粗略启发,通过大量数据训练以解决具有前所未有准确度的复杂任务。随着开源框架使这项技术广泛可用,它已成为任何涉及大数据和机器学习的人必须掌握的技能。
TensorFlow 目前是领先的深度学习开源软件,被越来越多的从业者用于计算机视觉、自然语言处理(NLP)、语音识别和一般预测分析。
本书是一本针对数据科学家、工程师、学生和研究人员设计的 TensorFlow 端到端指南。本书采用适合广泛技术受众的实践方法,使初学者可以轻松入门,同时深入探讨高级主题,并展示如何构建生产就绪系统。
在本书中,您将学习如何:
-
快速轻松地开始使用 TensorFlow。
-
使用 TensorFlow 从头开始构建模型。
-
训练和理解计算机视觉和 NLP 中流行的深度学习模型。
-
使用广泛的抽象库使开发更加简单和快速。
-
通过排队和多线程、在集群上训练和在生产中提供输出来扩展 TensorFlow。
-
还有更多!
本书由具有丰富工业和学术研究经验的数据科学家撰写。作者采用实用直观的例子、插图和见解,结合实践方法,适合寻求构建生产就绪系统的从业者,以及希望学习理解和构建灵活强大模型的读者。
先决条件
本书假设读者具有一些基本的 Python 编程知识,包括对科学库 NumPy 的基本了解。
本书中涉及并直观解释了机器学习概念。对于希望深入了解的读者,建议具有一定水平的机器学习、线性代数、微积分、概率和统计知识。
本书使用的约定
本书中使用以下排版约定:
斜体
指示新术语、URL、电子邮件地址、文件名和文件扩展名。
常量宽度
用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
常量宽度粗体
显示用户应该按原样输入的命令或其他文本。
常量宽度斜体
显示应替换为用户提供的值或由上下文确定的值的文本。
使用代码示例
可下载补充材料(代码示例、练习等)https://github.com/Hezi-Resheff/Oreilly-Learning-TensorFlow。
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您要复制代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。出售或分发包含 O'Reilly 图书示例的 CD-ROM 需要许可。回答问题并引用本书中的示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。
我们感激,但不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Learning TensorFlow by Tom Hope, Yehezkel S. Resheff, and Itay Lieder (O’Reilly). Copyright 2017 Tom Hope, Itay Lieder, and Yehezkel S. Resheff, 978-1-491-97851-1.”
如果您觉得您对代码示例的使用超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。
致谢
作者要感谢为这本书提供反馈意见的审阅者:Chris Fregly、Marvin Bertin、Oren Sar Shalom 和 Yoni Lavi。我们还要感谢 Nicole Tache 和 O'Reilly 团队,使写作这本书成为一种乐趣。
当然,感谢所有在 Google 工作的人,没有他们就不会有 TensorFlow。
第一章:介绍
本章提供了 TensorFlow 及其主要用途的高层概述:实现和部署深度学习系统。我们首先对深度学习进行了非常简要的介绍。然后展示 TensorFlow,展示了它在构建机器智能方面的一些令人兴奋的用途,然后列出了它的主要特性和属性。
深入探讨
从大型公司到新兴初创公司,工程师和数据科学家正在收集大量数据,并使用机器学习算法来回答复杂问题并构建智能系统。在这个领域的任何地方,与深度学习相关的算法类最近取得了巨大成功,通常将传统方法远远甩在后面。深度学习今天被用于理解图像、自然语言和语音的内容,应用范围从移动应用到自动驾驶汽车。这一领域的发展速度惊人,深度学习正在扩展到其他领域和数据类型,比如用于药物发现的复杂化学和基因结构,以及公共卫生保健中的高维医疗记录。
深度学习方法,也被称为深度神经网络,最初受到人类大脑庞大的相互连接的神经元网络的粗略启发。在深度学习中,我们将数百万个数据实例输入到神经元网络中,教导它们从原始输入中识别模式。深度神经网络接受原始输入(比如图像中的像素值)并将它们转换为有用的表示,提取更高级的特征(比如图像中的形状和边缘),通过组合越来越小的信息片段来捕捉复杂的概念,解决挑战性任务,比如图像分类。这些网络通过自动学习来构建抽象表示,通过适应和自我纠正来拟合数据中观察到的模式。自动构建数据表示的能力是深度神经网络相对于传统机器学习的关键优势,传统机器学习通常需要领域专业知识和手动特征工程才能进行任何“学习”。

图 1-1 图像分类与深度神经网络的示例。网络接受原始输入(图像中的像素值)并学习将其转换为有用的表示,以获得准确的图像分类。
这本书是关于谷歌的深度学习框架 TensorFlow。多年来,深度学习算法已经在谷歌的许多产品和领域中被使用,比如搜索、翻译、广告、计算机视觉和语音识别。事实上,TensorFlow 是谷歌用于实现和部署深度神经网络的第二代系统,继承了 2011 年开始的 DistBelief 项目。
TensorFlow 于 2015 年 11 月以 Apache 2.0 许可证的开源框架形式发布给公众,已经在业界掀起了风暴,其应用远远超出了谷歌内部项目。其可扩展性和灵活性,加上谷歌工程师们继续维护和发展的强大力量,使 TensorFlow 成为进行深度学习的领先系统。
使用 TensorFlow 进行人工智能系统
在深入讨论 TensorFlow 及其主要特性之前,我们将简要介绍 TensorFlow 在一些尖端的现实世界应用中的使用示例,包括谷歌及其他地方。
预训练模型:全面的计算机视觉
深度学习真正闪耀的一个主要领域是计算机视觉。计算机视觉中的一个基本任务是图像分类——构建接收图像并返回最佳描述类别集的算法和系统。研究人员、数据科学家和工程师设计了先进的深度神经网络,可以在理解视觉内容方面获得高度准确的结果。这些深度网络通常在大量图像数据上进行训练,需要大量时间、资源和精力。然而,趋势逐渐增长,研究人员正在公开发布预训练模型——已经训练好的深度神经网络,用户可以下载并应用到他们的数据中(图 1-2)。

图 1-2。使用预训练 TensorFlow 模型的高级计算机视觉。
TensorFlow 带有有用的实用程序,允许用户获取和应用尖端的预训练模型。我们将在本书中看到几个实际示例,并深入了解细节。
为图像生成丰富的自然语言描述
深度学习研究中一个令人兴奋的领域是为视觉内容生成自然语言描述(图 1-3)。这个领域的一个关键任务是图像字幕——教导模型为图像输出简洁准确的字幕。在这里,也提供了结合自然语言理解和计算机视觉的先进预训练 TensorFlow 模型。

图 1-3。通过图像字幕从图像到文本(示例说明)。
文本摘要
自然语言理解(NLU)是构建人工智能系统的关键能力。每天产生大量文本:网络内容、社交媒体、新闻、电子邮件、内部企业通信等等。最受追捧的能力之一是总结文本,将长篇文档转化为简洁连贯的句子,提取原始文本中的关键信息(图 1-4)。正如我们将在本书中看到的,TensorFlow 具有强大的功能,可以用于训练深度 NLU 网络,也可以用于自动文本摘要。

图 1-4。智能文本摘要的示例插图。
TensorFlow:名字的含义是什么?
深度神经网络,正如我们所示的术语和插图所暗示的,都是关于神经元网络的,每个神经元学习执行自己的操作,作为更大图像的一部分。像图像这样的数据作为输入进入这个网络,并在训练时适应自身,或在部署系统中预测输出。
张量是在深度学习中表示数据的标准方式。简单来说,张量只是多维数组,是对具有更高维度的数据的扩展。就像黑白(灰度)图像被表示为像素值的“表格”一样,RGB 图像被表示为张量(三维数组),每个像素具有三个值对应于红、绿和蓝色分量。
在 TensorFlow 中,计算被看作是一个数据流图(图 1-5)。广义上说,在这个图中,节点表示操作(如加法或乘法),边表示数据(张量)在系统中流动。在接下来的章节中,我们将深入探讨这些概念,并通过许多示例学会理解它们。

图 1-5。数据流计算图。数据以张量的形式流经由计算操作组成的图,构成我们的深度神经网络。
高层概述
TensorFlow 在最一般的术语中是一个基于数据流图的数值计算软件框架。然而,它主要设计为表达和实现机器学习算法的接口,其中深度神经网络是其中的主要算法之一。
TensorFlow 设计时考虑了可移植性,使这些计算图能够在各种环境和硬件平台上执行。例如,使用基本相同的代码,同一个 TensorFlow 神经网络可以在云端训练,分布在许多机器的集群上,或者在单个笔记本电脑上进行训练。它可以部署用于在专用服务器上提供预测,或者在 Android 或 iOS 等移动设备平台上,或者树莓派单板计算机上。当然,TensorFlow 也与 Linux、macOS 和 Windows 操作系统兼容。
TensorFlow 的核心是 C++,它有两种主要的高级前端语言和接口,用于表达和执行计算图。最发达的前端是 Python,大多数研究人员和数据科学家使用。C++前端提供了相当低级的 API,适用于在嵌入式系统和其他场景中进行高效执行。
除了可移植性,TensorFlow 的另一个关键方面是其灵活性,允许研究人员和数据科学家相对轻松地表达模型。有时,将现代深度学习研究和实践视为玩“乐高”积木是有启发性的,用其他块替换网络块并观察结果,有时设计新的块。正如我们将在本书中看到的,TensorFlow 提供了有用的工具来使用这些模块化块,结合灵活的 API,使用户能够编写新的模块。在深度学习中,网络是通过基于梯度下降优化的反馈过程进行训练的。TensorFlow 灵活支持许多优化算法,所有这些算法都具有自动微分功能——用户无需提前指定任何梯度,因为 TensorFlow 会根据用户提供的计算图和损失函数自动推导梯度。为了监视、调试和可视化训练过程,并简化实验,TensorFlow 附带了 TensorBoard(图 1-6),这是一个在浏览器中运行的简单可视化工具,我们将在本书中始终使用。

图 1-6。TensorFlow 的可视化工具 TensorBoard,用于监视、调试和分析训练过程和实验。
TensorFlow 的灵活性对数据科学家和研究人员的关键支持是高级抽象库。在计算机视觉或 NLU 的最先进深度神经网络中,编写 TensorFlow 代码可能会耗费精力,变得复杂、冗长和繁琐。Keras 和 TF-Slim 等抽象库提供了对底层库中的“乐高积木”的简化高级访问,有助于简化数据流图的构建、训练和推理。对数据科学家和工程师的另一个关键支持是 TF-Slim 和 TensorFlow 附带的预训练模型。这些模型是在大量数据和强大计算资源上进行训练的,这些资源通常难以获得,或者至少需要大量努力才能获取和设置。例如,使用 Keras 或 TF-Slim,只需几行代码就可以使用这些先进模型对传入数据进行推理,还可以微调模型以适应新数据。
TensorFlow 的灵活性和可移植性有助于使研究到生产的流程顺畅,减少了数据科学家将模型推送到产品部署和工程师将算法思想转化为稳健代码所需的时间和精力。
TensorFlow 抽象
TensorFlow 还配备了抽象库,如 Keras 和 TF-Slim,提供了对 TensorFlow 的简化高级访问。这些抽象,我们将在本书后面看到,有助于简化数据流图的构建,并使我们能够用更少的代码进行训练和推断。
但除了灵活性和可移植性之外,TensorFlow 还具有一系列属性和工具,使其对构建现实世界人工智能系统的工程师具有吸引力。它自然支持分布式训练 - 实际上,它被谷歌和其他大型行业参与者用于在许多机器的集群上训练大规模网络的海量数据。在本地实现中,使用多个硬件设备进行训练只需要对用于单个设备的代码进行少量更改。当从本地转移到分布式时,代码也基本保持不变,这使得在云中使用 TensorFlow,如在亚马逊网络服务(AWS)或谷歌云上,特别具有吸引力。此外,正如我们将在本书后面看到的那样,TensorFlow 还具有许多旨在提高可伸缩性的功能。这些功能包括支持使用线程和队列进行异步计算,高效的 I/O 和数据格式等等。
深度学习不断快速发展,TensorFlow 也在不断更新和增加新的令人兴奋的功能,带来更好的可用性、性能和价值。
总结
通过本章描述的一系列工具和功能,很明显为什么在短短一年多的时间里 TensorFlow 吸引了如此多的关注。本书旨在首先迅速让您了解基础并准备好工作,然后我们将深入探讨 TensorFlow 的世界,带来令人兴奋和实用的示例。
第二章:随波逐流:TensorFlow 快速入门
在本章中,我们将从两个可工作的 TensorFlow 示例开始我们的旅程。第一个(传统的“hello world”程序),虽然简短简单,但包含了我们在后续章节中深入讨论的许多重要元素。通过第二个,一个首个端到端的机器学习模型,您将开始您的 TensorFlow 最新机器学习之旅。
在开始之前,我们简要介绍 TensorFlow 的安装。为了方便快速启动,我们仅安装 CPU 版本,并将 GPU 安装推迟到以后。如果你不知道这意味着什么,那没关系!如果你已经安装了 TensorFlow,请跳到第二部分。
安装 TensorFlow
如果您使用的是干净的 Python 安装(可能是为学习 TensorFlow 而设置的),您可以从简单的pip安装开始:
$ pip install tensorflow
然而,这种方法的缺点是 TensorFlow 将覆盖现有软件包并安装特定版本以满足依赖关系。如果您还将此 Python 安装用于其他目的,这样做将不起作用。一个常见的解决方法是在一个由virtualenv管理的虚拟环境中安装 TensorFlow。
根据您的设置,您可能需要或不需要在计算机上安装virtualenv。要安装virtualenv,请键入:
$ pip install virtualenv
查看http://virtualenv.pypa.io获取更多说明。
为了在虚拟环境中安装 TensorFlow,您必须首先创建虚拟环境——在本书中,我们选择将其放在~/envs文件夹中,但请随意将其放在您喜欢的任何位置:
$ cd ~
$ mkdir envs
$ virtualenv ~/envs/tensorflow
这将在/envs*中创建一个名为*tensorflow*的虚拟环境(将显示为*/envs/tensorflow文件夹)。要激活环境,请使用:
$ source ~/envs/tensorflow/bin/activate
提示现在应该改变以指示已激活的环境:
(tensorflow)$
此时pip install命令:
(tensorflow)$ pip install tensorflow
将在虚拟环境中安装 TensorFlow,而不会影响您计算机上安装的其他软件包。
最后,为了退出虚拟环境,您需要键入:
(tensorflow)$ deactivate
此时,您应该会得到常规提示符:
$
在~/.bashrc 中添加别名
描述进入和退出虚拟环境的过程可能会太繁琐,如果您打算经常使用它。在这种情况下,您可以简单地将以下命令附加到您的~/.bashrc文件中:
alias tensorflow="source ~/envs/tensorflow/bin/activate"
并使用命令tensorflow来激活虚拟环境。要退出环境,您仍然会使用deactivate。
现在我们已经基本安装了 TensorFlow,我们可以继续进行我们的第一个工作示例。我们将遵循已经建立的传统,并从一个“hello world”程序开始。
Hello World
我们的第一个示例是一个简单的程序,将单词“Hello”和“ World!”组合在一起并显示输出——短语“Hello World!”。虽然简单直接,但这个示例介绍了 TensorFlow 的许多核心元素以及它与常规 Python 程序的不同之处。
我们建议您在计算机上运行此示例,稍微玩弄一下,并查看哪些有效。接下来,我们将逐行查看代码并分别讨论每个元素。
首先,我们运行一个简单的安装和版本检查(如果您使用了 virtualenv 安装选项,请确保在运行 TensorFlow 代码之前激活它):
import tensorflow as tf
print(tf.__version__)
如果正确,输出将是您在系统上安装的 TensorFlow 版本。版本不匹配是后续问题的最有可能原因。
示例 2-1 显示了完整的“hello world”示例。
示例 2-1。“Hello world”与 TensorFlow
import tensorflow as tf
h = tf.constant("Hello")
w = tf.constant(" World!")
hw = h + w
with tf.Session() as sess:
ans = sess.run(hw)
print (ans)
我们假设您熟悉 Python 和导入,那么第一行:
import tensorflow as tf
不需要解释。
IDE 配置
如果您从 IDE 运行 TensorFlow 代码,请确保重定向到安装包的虚拟环境。否则,您将收到以下导入错误:
ImportError: No module named tensorflow
在 PyCharm IDE 中,通过选择 Run→Edit Configurations,然后将 Python Interpreter 更改为指向/envs/tensorflow/bin/python*,假设您使用*/envs/tensorflow作为虚拟环境目录。
接下来,我们定义常量"Hello"和" World!",并将它们组合起来:
import tensorflow as tf
h = tf.constant("Hello")
w = tf.constant(" World!")
hw = h + w
此时,您可能会想知道这与用于执行此操作的简单 Python 代码有何不同(如果有的话):
ph = "Hello"
pw = " World!"
phw = h + w
这里的关键点是每种情况下变量hw包含的内容。我们可以使用print命令来检查这一点。在纯 Python 情况下,我们得到这个:
>`print``phw`HelloWorld!
然而,在 TensorFlow 情况下,输出完全不同:
>`print``hw`Tensor("add:0",shape=(),dtype=string)
可能不是您期望的!
在下一章中,我们将详细解释 TensorFlow 的计算图模型,到那时这个输出将变得完全清晰。TensorFlow 中计算图的关键思想是,我们首先定义应该发生的计算,然后在外部机制中触发计算。因此,TensorFlow 代码行:
hw = h + w
不计算h和w的总和,而是将求和操作添加到稍后要执行的计算图中。
接下来,Session对象充当外部 TensorFlow 计算机制的接口,并允许我们运行已经定义的计算图的部分。代码行:
ans = sess.run(hw)
实际上计算hw(作为先前定义的h和w的总和),随后打印ans显示预期的“Hello World!”消息。
这完成了第一个 TensorFlow 示例。接下来,我们将立即进行一个简单的机器学习示例,这个示例已经展示了 TensorFlow 框架的许多潜力。
MNIST
MNIST(混合国家标准技术研究所)手写数字数据集是图像处理和机器学习中最研究的数据集之一,并在人工神经网络(现在通常称为深度学习)的发展中发挥了重要作用。
因此,我们的第一个机器学习示例应该致力于手写数字的分类(图 2-1 显示了数据集的随机样本)。在这一点上,为了保持简单,我们将应用一个非常简单的分类器。这个简单模型足以正确分类测试集的大约 92%——目前可用的最佳模型可以达到 99.75%以上的正确分类,但在我们达到那里之前还有几章要学习!在本书的后面,我们将重新访问这些数据并使用更复杂的方法。

图 2-1. 100 个随机 MNIST 图像
Softmax 回归
在这个示例中,我们将使用一个称为softmax 回归的简单分类器。我们不会详细介绍模型的数学公式(有很多好的资源可以找到这些信息,如果您以前从未见过这些信息,我们强烈建议您这样做)。相反,我们将尝试提供一些关于模型如何解决数字识别问题的直觉。
简而言之,softmax 回归模型将找出图像中每个像素的数字在该位置具有高(或低)值的趋势。例如,图像中心对于零来说往往是白色的,但对于六来说是黑色的。因此,图像中心的黑色像素将是反对图像包含零的证据,并支持它包含六的证据。
在这个模型中的学习包括找到告诉我们如何累积每个数字的存在证据的权重。使用 softmax 回归,我们将不使用图像中像素布局中的空间信息。稍后,当我们讨论卷积神经网络时,我们将看到利用空间信息是制作出色的图像处理和对象识别模型的关键元素之一。
由于我们在这一点上不打算使用空间信息,我们将我们的图像像素展开为一个长向量表示为x(Figure 2-2)。然后
xw⁰ = ∑x[i]
将是包含数字 0 的图像的证据(同样地,我们将为其他每个数字有 个权重向量,)。

Figure 2-2。MNIST 图像像素展开为向量并按列堆叠(从左到右按数字排序)。虽然空间信息的丢失使我们无法识别数字,但在这个图中明显的块结构是 softmax 模型能够对图像进行分类的原因。基本上,所有的零(最左边的块)共享相似的像素结构,所有的一(从左边第二个块)也是如此,等等。
这意味着我们将像素值相加,每个值乘以一个权重,我们认为这个像素在图像中数字零的整体证据中的重要性。
例如,w⁰[38]如果第 38 个像素具有高强度,则将是一个较大的正数,指向该数字为零,如果在这个位置的高强度值主要出现在其他数字中,则将是一个较大的负数,如果第 38 个像素的强度值告诉我们这个数字是否为零,则为零。³
一次为所有数字执行此计算(计算出现在图像中的每个数字的证据)可以通过单个矩阵操作表示。如果我们将每个数字的权重放在矩阵W的列中,那么每个数字的证据的长度为 10 的向量是
[xw⁰, ···, xw⁹] = xW
分类器的学习目的几乎总是为了评估新的示例。在这种情况下,这意味着我们希望能够判断我们在训练数据中没有见过的新图像中写的是什么数字。为了做到这一点,我们首先对 10 个可能数字中的每一个的证据进行求和(即计算xW)。最终的分配将是“赢得”最多证据的数字:
数字 = argmax(xW)
我们首先完整地呈现这个示例的代码(Example 2-2),然后逐行走过它并详细讨论。您可能会发现在这个阶段有许多新颖的元素,或者一些拼图的部分缺失,但我们的建议是暂时接受它。一切将在适当的时候变得清晰。
示例 2-2。使用 softmax 回归对 MNIST 手写数字进行分类
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
DATA_DIR = '/tmp/data'
NUM_STEPS = 1000
MINIBATCH_SIZE = 100
data = input_data.read_data_sets(DATA_DIR, one_hot=True)
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=y_pred, labels=y_true))
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
with tf.Session() as sess:
# Train
sess.run(tf.global_variables_initializer())
for _ in range(NUM_STEPS):
batch_xs, batch_ys = data.train.next_batch(MINIBATCH_SIZE)
sess.run(gd_step, feed_dict={x: batch_xs, y_true: batch_ys})
# Test
ans = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
print "Accuracy: {:.4}%".format(ans*100)
如果您在您的机器上运行代码,您应该会得到如下输出:
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-labels-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte.gz
Accuracy: 91.83%
就是这样!如果您之前在其他平台上组合过类似的模型,您可能会欣赏到其简单性和可读性。然而,这些只是附加的好处,我们真正感兴趣的是从 TensorFlow 的计算图模型中获得的效率和灵活性。
你得到的准确度值将略低于 92%。如果再运行程序一次,你会得到另一个值。这种随机性在机器学习代码中非常常见,你可能以前也见过类似的结果。在这种情况下,源是手写数字在学习过程中呈现给模型的顺序发生了变化。因此,训练后学到的参数在每次运行时略有不同。
因此,运行相同的程序五次可能会产生这样的结果:
Accuracy: 91.86%
Accuracy: 91.51%
Accuracy: 91.62%
Accuracy: 91.93%
Accuracy: 91.88%
现在我们将简要地查看这个示例的代码,并看看与之前的“hello world”示例有什么新的地方。我们将逐行分解它:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
这个示例中的第一个新元素是我们使用外部数据!我们不再下载 MNIST 数据集(在http://yann.lecun.com/exdb/mnist/免费提供),然后加载到我们的程序中,而是使用内置工具来动态检索数据集。对于大多数流行的数据集,存在这样的工具,当处理小数据集时(在这种情况下只有几 MB),这种方式非常合理。第二个导入加载了我们稍后将使用的工具,用于自动下载数据,并根据需要管理和分区数据:
DATA_DIR = '/tmp/data'
NUM_STEPS = 1000
MINIBATCH_SIZE = 100
在这里,我们定义了一些在程序中使用的常量,它们将在首次使用时的上下文中进行解释:
data = input_data.read_data_sets(DATA_DIR, one_hot=True)
MNIST 阅读工具的read_data_sets()方法会下载数据集并将其保存在本地,为程序后续使用做好准备。第一个参数DATA_DIR是我们希望数据保存在本地的位置。我们将其设置为'/tmp/data',但任何其他位置也同样适用。第二个参数告诉工具我们希望数据如何标记;我们现在不会深入讨论这个问题。
请注意,这就是输出的前四行,表明数据已经正确获取。现在我们终于准备好设置我们的模型了:
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
在之前的示例中,我们看到了 TensorFlow 的常量元素,现在它被placeholder和Variable元素补充。现在,知道变量是计算中操作的元素,而占位符在触发时必须提供。图像本身(x)是一个占位符,因为当运行计算图时,我们将提供它。大小[None, 784]表示每个图像的大小为 784(28×28 像素展开成一个单独的向量),None表示我们目前没有指定一次使用多少个这样的图像:
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
在下一章中,这些概念将会更深入地讨论。
在大类机器学习任务中的一个关键概念是,我们希望从数据示例(在我们的情况下是数字图像)到它们已知标签(图像中数字的身份)的函数。这种设置被称为监督学习。在大多数监督学习模型中,我们尝试学习一个模型,使得真实标签和预测标签在某种意义上接近。在这里,y_true和y_pred分别表示真实标签和预测标签的元素:
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=y_pred, labels=y_true))
我们选择用于此模型的相似度度量是所谓的交叉熵,这是当模型输出类别概率时的自然选择。这个元素通常被称为损失函数:
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
模型的最后一部分是我们将如何训练它(即我们将如何最小化损失函数)。一个非常常见的方法是使用梯度下降优化。这里,0.5是学习率,控制我们的梯度下降优化器如何快速地调整模型权重以减少总体损失。
我们将在本书后面讨论优化器以及它们如何适应计算图。
一旦我们定义了我们的模型,我们希望定义我们将使用的评估过程,以便测试模型的准确性。在这种情况下,我们对正确分类的测试示例的比例感兴趣:⁶
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
与“hello world”示例一样,为了利用我们定义的计算图,我们必须创建一个会话。其余操作都在会话中进行:
with tf.Session() as sess:
首先,我们必须初始化所有变量:
sess.run(tf.global_variables_initializer())
这在机器学习和优化领域具有一些特定的含义,当我们使用初始化是一个重要问题的模型时,我们将进一步讨论这些含义
for _ in range(NUM_STEPS):
batch_xs, batch_ys = data.train.next_batch(MINIBATCH_SIZE)
sess.run(gd_step, feed_dict={x: batch_xs, y_true: batch_ys})
在梯度下降方法中,模型的实际训练包括在“正确的方向”上进行多次步骤。在这种情况下,我们将进行的步数NUM_STEPS设置为 1,000 步。有更复杂的方法来决定何时停止,但稍后再讨论!在每一步中,我们向数据管理器请求一组带有标签的示例,并将它们呈现给学习者。MINIBATCH_SIZE常数控制每一步使用的示例数量。
最后,我们第一次使用sess.run的feed_dict参数。回想一下,在构建模型时我们定义了占位符元素。现在,每当我们想要运行包含这些元素的计算时,我们必须为它们提供一个值。
ans = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
为了评估我们刚刚学习的模型,我们运行之前定义的准确性计算操作(回想一下,准确性被定义为正确标记的图像的比例)。在这个过程中,我们提供一组从未被模型在训练过程中看到的测试图像:
print "Accuracy: {:.4}%".format(ans*100)
最后,我们将结果打印为百分比值。
图 2-3 显示了我们模型的图形表示。

图 2-3。模型的图形表示。矩形元素是变量,圆圈是占位符。左上角框表示标签预测部分,右下角框表示评估。这里,b是一个偏差项,可以添加到模型中。
模型评估和内存错误
在使用 TensorFlow 时,与任何其他系统一样,重要的是要注意正在使用的资源,并确保不超出系统的容量。在评估模型时可能会出现的一个潜在问题是在测试集上测试模型的性能。在这个示例中,我们通过一次性提供所有测试示例来评估模型的准确性:
feed_dict={x: data.test.images, y_true: data.test.labels}
ans = sess.run(accuracy, feed_dict)
如果所有的测试示例(这里是data.test.images)无法在您使用的系统内存中容纳,那么在这一点上会出现内存错误。例如,如果您在典型的低端 GPU 上运行此示例,很可能会出现这种情况。
解决这个问题的简单方法(获得更多内存的机器是一个临时解决方案,因为总会有更大的数据集)是将测试过程分成批处理,就像我们在训练过程中所做的那样。
总结
恭喜!到目前为止,您已经安装了 TensorFlow 并使用了两个基本示例。您已经看到了本书中将使用的一些基本构建模块,并且希望已经开始对 TensorFlow 有所了解。
接下来,我们将深入了解 TensorFlow 使用的计算图模型。
¹ 我们建议读者参考官方TensorFlow 安装指南以获取更多详细信息,特别是有关 GPU 安装的不断变化的细节。
² 添加“偏差项”是常见的,这相当于在看到像素值之前我们相信图像是哪些数字。如果您之前见过这个,那么尝试将其添加到模型中并查看它如何影响结果。
³ 如果您熟悉 softmax 回归,您可能意识到这是它工作方式的简化,特别是当像素值与数字图像一样相关时。
⁴ 在整个过程中,在运行示例代码之前,请确保DATA_DIR适合您正在使用的操作系统。例如,在 Windows 上,您可能会使用类似c:\tmp\data的路径。
⁵ 从 TensorFlow 1.0 开始,这也包含在tf.losses.softmax_cross_entropy中。
⁶ 从 TensorFlow 1.0 开始,这也包含在tf.metrics.accuracy中。
第三章:理解 TensorFlow 基础知识
本章演示了 TensorFlow 是如何构建和使用简单直观的示例的关键概念。您将了解 TensorFlow 作为一个数据流图的数值计算库的基础知识。更具体地说,您将学习如何管理和创建图,并介绍 TensorFlow 的“构建块”,如常量、占位符和变量。
计算图
TensorFlow 允许我们通过创建和计算相互作用的操作来实现机器学习算法。这些交互形成了我们所谓的“计算图”,通过它我们可以直观地表示复杂的功能架构。
什么是计算图?
我们假设很多读者已经接触过图的数学概念。对于那些对这个概念是新的人来说,图指的是一组相互连接的实体,通常称为节点或顶点。这些节点通过边连接在一起。在数据流图中,边允许数据以有向方式从一个节点流向另一个节点。
在 TensorFlow 中,图的每个节点代表一个操作,可能应用于某些输入,并且可以生成一个输出,传递给其他节点。类比地,我们可以将图计算看作是一个装配线,其中每台机器(节点)要么获取或创建其原材料(输入),处理它,然后按顺序将输出传递给其他机器,生产子组件,最终在装配过程结束时产生一个最终产品。
图中的操作包括各种函数,从简单的算术函数如减法和乘法到更复杂的函数,我们稍后会看到。它们还包括更一般的操作,如创建摘要、生成常量值等。
图计算的好处
TensorFlow 根据图的连接性优化其计算。每个图都有自己的节点依赖关系集。当节点y的输入受到节点x的输出的影响时,我们说节点y依赖于节点x。当两者通过边连接时,我们称之为直接依赖,否则称为间接依赖。例如,在图 3-1(A)中,节点e直接依赖于节点c,间接依赖于节点a,独立于节点d。

图 3-1.(A)图依赖的示例。 (B)计算节点 e 根据图的依赖关系进行最少量的计算—在这种情况下仅计算节点 c、b 和 a。
我们始终可以识别图中每个节点的完整依赖关系。这是基于图的计算格式的一个基本特征。能够找到模型单元之间的依赖关系使我们能够在可用资源上分配计算,并避免执行与无关子集的冗余计算,从而以更快更有效的方式计算事物。
图、会话和获取
粗略地说,使用 TensorFlow 涉及两个主要阶段:(1)构建图和(2)执行图。让我们跳入我们的第一个示例,并创建一些非常基本的东西。
创建图
导入 TensorFlow 后(使用import tensorflow as tf),会形成一个特定的空默认图。我们创建的所有节点都会自动与该默认图关联。
使用tf.<*operator*>方法,我们将创建六个节点,分配给任意命名的变量。这些变量的内容应被视为操作的输出,而不是操作本身。现在我们用它们对应变量的名称来引用操作和它们的输出。
前三个节点各被告知输出一个常量值。值5、2和3分别分配给a、b和c:
a = tf.constant(5)
b = tf.constant(2)
c = tf.constant(3)
接下来的三个节点中的每一个都将两个现有变量作为输入,并对它们进行简单的算术运算:
d = tf.multiply(a,b)
e = tf.add(c,b)
f = tf.subtract(d,e)
节点d将节点a和b的输出相乘。节点e将节点b和c的输出相加。节点f将节点e的输出从节点d的输出中减去。
voilà!我们有了我们的第一个 TensorFlow 图!图 3-2 显示了我们刚刚创建的图的示例。

图 3-2. 我们第一个构建的图的示例。每个由小写字母表示的节点执行其上方指示的操作:Const 用于创建常量,Add、Mul 和 Sub 分别用于加法、乘法和减法。每条边旁边的整数是相应节点操作的输出。
请注意,对于一些算术和逻辑操作,可以使用操作快捷方式,而不必应用tf.<*operator*>。例如,在这个图中,我们可以使用*/+/-代替tf.multiply()/tf.add()/tf.subtract()(就像我们在第二章的“hello world”示例中使用+代替tf.add()一样)。表 3-1 列出了可用的快捷方式。
表 3-1. 常见的 TensorFlow 操作及其相应的快捷方式
| TensorFlow 运算符 | 快捷方式 | 描述 |
|---|---|---|
tf.add() |
a + b |
对a和b进行逐元素相加。 |
tf.multiply() |
a * b |
对a和b进行逐元素相乘。 |
tf.subtract() |
a - b |
对a和b进行逐元素相减。 |
tf.divide() |
a / b |
计算a除以b的 Python 风格除法。 |
tf.pow() |
a ** b |
返回将a中的每个元素提升到其对应元素b的结果,逐元素。 |
tf.mod() |
a % b |
返回逐元素取模。 |
tf.logical_and() |
a & b |
返回a & b的真值表,逐元素。dtype必须为tf.bool。 |
tf.greater() |
a > b |
返回a > b的真值表,逐元素。 |
tf.greater_equal() |
a >= b |
返回a >= b的真值表,逐元素。 |
tf.less_equal() |
a <= b |
返回a <= b的真值表,逐元素。 |
tf.less() |
a < b |
返回a < b的真值表,逐元素。 |
tf.negative() |
-a |
返回a中每个元素的负值。 |
tf.logical_not() |
~a |
返回a中每个元素的逻辑非。仅与dtype为tf.bool的张量对象兼容。 |
tf.abs() |
abs(a) |
返回a中每个元素的绝对值。 |
tf.logical_or() |
a | b |
返回a | b的真值表,逐元素。dtype必须为tf.bool。 |
创建会话并运行
一旦我们完成描述计算图,我们就准备运行它所代表的计算。为了实现这一点,我们需要创建并运行一个会话。我们通过添加以下代码来实现这一点:
sess = tf.Session()
outs = sess.run(f)
sess.close()
print("outs = {}".format(outs))
Out:
outs = 5
首先,我们在tf.Session中启动图。Session对象是 TensorFlow API 的一部分,它在 Python 对象和我们端的数据之间进行通信,实际的计算系统在其中为我们定义的对象分配内存,存储中间变量,并最终为我们获取结果。
sess = tf.Session()
然后,执行本身是通过Session对象的.run()方法完成的。当调用时,该方法以以下方式完成我们图中的一组计算:从请求的输出开始,然后向后工作,计算必须根据依赖关系集执行的节点。因此,将计算的图部分取决于我们的输出查询。
在我们的示例中,我们请求计算节点f并获得其值5作为输出:
outs = sess.run(f)
当我们的计算任务完成时,最好使用sess.close()命令关闭会话,确保我们的会话使用的资源被释放。即使我们不必这样做也能使事情正常运行,这是一个重要的实践:
sess.close()
示例 3-1。自己试试吧!图 3-3 显示了另外两个图示例。看看你能否自己生成这些图。

图 3-3。你能创建图 A 和图 B 吗?(要生成正弦函数,请使用 tf.sin(x))。
构建和管理我们的图
如前所述,一旦导入 TensorFlow,一个默认图会自动为我们创建。我们可以创建额外的图并控制它们与某些给定操作的关联。tf.Graph()创建一个新的图,表示为一个 TensorFlow 对象。在这个例子中,我们创建另一个图并将其分配给变量g:
import tensorflow as tf
print(tf.get_default_graph())
g = tf.Graph()
print(g)
Out:
<tensorflow.python.framework.ops.Graph object at 0x7fd88c3c07d0>
<tensorflow.python.framework.ops.Graph object at 0x7fd88c3c03d0>
此时我们有两个图:默认图和g中的空图。当打印时,它们都会显示为 TensorFlow 对象。由于g尚未分配为默认图,我们创建的任何操作都不会与其相关联,而是与默认图相关联。
我们可以使用tf.get_default_graph()来检查当前设置为默认的图。此外,对于给定节点,我们可以使用*<node>*.graph属性查看它关联的图:
g = tf.Graph()
a = tf.constant(5)
print(a.graph is g)
print(a.graph is tf.get_default_graph())
Out:
False
True
在这个代码示例中,我们看到我们创建的操作与默认图相关联,而不是与g中的图相关联。
为了确保我们构建的节点与正确的图相关联,我们可以使用一个非常有用的 Python 构造:with语句。
with 语句
with语句用于使用上下文管理器定义的方法包装一个代码块——一个具有特殊方法函数.__enter__()用于设置代码块和.__exit__()用于退出代码块的对象。
通俗地说,在许多情况下,执行一些需要“设置”(如打开文件、SQL 表等)的代码,然后在最后“拆除”它总是非常方便的,无论代码是否运行良好或引发任何异常。在我们的例子中,我们使用with来设置一个图,并确保每一段代码都将在该图的上下文中执行。
我们使用with语句与as_default()命令一起使用,它返回一个上下文管理器,使这个图成为默认图。在处理多个图时,这非常方便:
g1 = tf.get_default_graph()
g2 = tf.Graph()
print(g1 is tf.get_default_graph())
with g2.as_default():
print(g1 is tf.get_default_graph())
print(g1 is tf.get_default_graph())
Out:
True
False
True
with语句也可以用于启动一个会话,而无需显式关闭它。这个方便的技巧将在接下来的示例中使用。
Fetches
在我们的初始图示例中,我们通过将分配给它的变量作为sess.run()方法的参数来请求一个特定节点(节点f)。这个参数称为fetches,对应于我们希望计算的图的元素。我们还可以通过输入请求节点的列表来要求sess.run()多个节点的输出:
with tf.Session() as sess:
fetches = [a,b,c,d,e,f]
outs = sess.run(fetches)
print("outs = {}".format(outs))
print(type(outs[0]))
Out:
outs = [5, 2, 3, 10, 5, 5]
<type 'numpy.int32'>
我们得到一个包含节点输出的列表,根据它们在输入列表中的顺序。列表中每个项目的数据类型为 NumPy。
NumPy
NumPy 是一个流行且有用的 Python 包,用于数值计算,提供了许多与数组操作相关的功能。我们假设读者对这个包有一些基本的了解,本书不会涉及这部分内容。TensorFlow 和 NumPy 紧密耦合——例如,sess.run()返回的输出是一个 NumPy 数组。此外,许多 TensorFlow 的操作与 NumPy 中的函数具有相同的语法。要了解更多关于 NumPy 的信息,我们建议读者参考 Eli Bressert 的书籍SciPy and NumPy(O'Reilly)。
我们提到 TensorFlow 仅根据依赖关系集计算必要的节点。这也体现在我们的示例中:当我们请求节点d的输出时,只计算节点a和b的输出。另一个示例显示在图 3-1(B)中。这是 TensorFlow 的一个巨大优势——不管我们的整个图有多大和复杂,因为我们可以根据需要仅运行其中的一小部分。
自动关闭会话
使用with子句打开会话将确保会话在所有计算完成后自动关闭。
流动的 Tensor
在本节中,我们将更好地理解节点和边在 TensorFlow 中实际上是如何表示的,以及如何控制它们的特性。为了演示它们的工作原理,我们将重点放在用于初始化值的源操作上。
节点是操作,边是 Tensor 对象
当我们在图中构造一个节点,就像我们用tf.add()做的那样,实际上是创建了一个操作实例。这些操作直到图被执行时才产生实际值,而是将它们即将计算的结果作为一个可以传递给另一个节点的句柄。我们可以将这些句柄视为图中的边,称为 Tensor 对象,这也是 TensorFlow 名称的由来。
TensorFlow 的设计是首先创建一个带有所有组件的骨架图。在这一点上,没有实际数据流入其中,也没有进行任何计算。只有在执行时,当我们运行会话时,数据进入图中并进行计算(如图 3-4 所示)。这样,计算可以更加高效,考虑整个图结构。

图 3-4。在运行会话之前(A)和之后(B)的示例。当会话运行时,实际数据会“流”通过图。
在上一节的示例中,tf.constant()创建了一个带有传递值的节点。打印构造函数的输出,我们看到它实际上是一个 Tensor 对象实例。这些对象有控制其行为的方法和属性,可以在创建时定义。
在这个示例中,变量c存储了一个名为Const_52:0的 Tensor 对象,用于包含一个 32 位浮点标量:
c = tf.constant(4.0)
print(c)
Out:
Tensor("Const_52:0", shape=(), dtype=float32)
构造函数说明
tf.*<operator>*函数可以被视为构造函数,但更准确地说,这实际上根本不是构造函数,而是一个工厂方法,有时做的事情远不止创建操作对象。
使用源操作设置属性
TensorFlow 中的每个 Tensor 对象都有属性,如name、shape和dtype,帮助识别和设置该对象的特性。在创建节点时,这些属性是可选的,当缺失时 TensorFlow 会自动设置。在下一节中,我们将查看这些属性。我们将通过查看由称为源操作的操作创建的 Tensor 对象来实现。源操作是创建数据的操作,通常不使用任何先前处理过的输入。通过这些操作,我们可以创建标量,就像我们已经使用tf.constant()方法遇到的那样,以及数组和其他类型的数据。
数据类型
通过图传递的数据的基本单位是数字、布尔值或字符串元素。当我们打印出上一个代码示例中的 Tensor 对象c时,我们看到它的数据类型是浮点数。由于我们没有指定数据类型,TensorFlow 会自动推断。例如,5被视为整数,而带有小数点的任何内容,如5.1,被视为浮点数。
我们可以通过在创建 Tensor 对象时指定数据类型来明确选择要使用的数据类型。我们可以使用属性dtype来查看给定 Tensor 对象设置的数据类型:
c = tf.constant(4.0, dtype=tf.float64)
print(c)
print(c.dtype)
Out:
Tensor("Const_10:0", shape=(), dtype=float64)
<dtype: 'float64'>
明确要求(适当大小的)整数一方面更节省内存,但另一方面可能会导致减少精度,因为不跟踪小数点后的数字。
转换
确保图中的数据类型匹配非常重要——使用两个不匹配的数据类型进行操作将导致异常。要更改 Tensor 对象的数据类型设置,我们可以使用tf.cast()操作,将相关的 Tensor 和感兴趣的新数据类型作为第一个和第二个参数传递:
x = tf.constant([1,2,3],name='x',dtype=tf.float32)
print(x.dtype)
x = tf.cast(x,tf.int64)
print(x.dtype)
Out:
<dtype: 'float32'>
<dtype: 'int64'>
TensorFlow 支持许多数据类型。这些列在表 3-2 中。
支持的张量数据类型表 3-2
| 数据类型 | Python 类型 | 描述 |
|---|---|---|
DT_FLOAT |
tf.float32 |
32 位浮点数。 |
DT_DOUBLE |
tf.float64 |
64 位浮点数。 |
DT_INT8 |
tf.int8 |
8 位有符号整数。 |
DT_INT16 |
tf.int16 |
16 位有符号整数。 |
DT_INT32 |
tf.int32 |
32 位有符号整数。 |
DT_INT64 |
tf.int64 |
64 位有符号整数。 |
DT_UINT8 |
tf.uint8 |
8 位无符号整数。 |
DT_UINT16 |
tf.uint16 |
16 位无符号整数。 |
DT_STRING |
tf.string |
变长字节数组。张量的每个元素都是一个字节数组。 |
DT_BOOL |
tf.bool |
布尔值。 |
DT_COMPLEX64 |
tf.complex64 |
由两个 32 位浮点数组成的复数:实部和虚部。 |
DT_COMPLEX128 |
tf.complex128 |
由两个 64 位浮点数组成的复数:实部和虚部。 |
DT_QINT8 |
tf.qint8 |
用于量化操作的 8 位有符号整数。 |
DT_QINT32 |
tf.qint32 |
用于量化操作的 32 位有符号整数。 |
DT_QUINT8 |
tf.quint8 |
用于量化操作的 8 位无符号整数。 |
张量数组和形状
潜在混淆的一个来源是,Tensor这个名字指的是两个不同的东西。在前面的部分中使用的Tensor是 Python API 中用作图中操作结果的句柄的对象的名称。然而,tensor也是一个数学术语,用于表示n维数组。例如,一个 1×1 的张量是一个标量,一个 1×n的张量是一个向量,一个n×n的张量是一个矩阵,一个n×n×n的张量只是一个三维数组。当然,这可以推广到任何维度。TensorFlow 将流经图中的所有数据单元都视为张量,无论它们是多维数组、向量、矩阵还是标量。TensorFlow 对象称为 Tensors 是根据这些数学张量命名的。
为了澄清两者之间的区别,从现在开始我们将前者称为大写 T 的张量,将后者称为小写 t 的张量。
与dtype一样,除非明确说明,TensorFlow 会自动推断数据的形状。当我们在本节开始时打印出 Tensor 对象时,它显示其形状为(),对应于标量的形状。
使用标量对于演示目的很好,但大多数情况下,使用多维数组更实用。要初始化高维数组,我们可以使用 Python 列表或 NumPy 数组作为输入。在以下示例中,我们使用 Python 列表作为输入,创建一个 2×3 矩阵,然后使用一个大小为 2×3×4 的 3D NumPy 数组作为输入(两个大小为 3×4 的矩阵):
import numpy as np
c = tf.constant([[1,2,3],
[4,5,6]])
print("Python List input: {}".format(c.get_shape()))
c = tf.constant(np.array([
[[1,2,3,4],
[5,6,7,8],
[9,8,7,6]],
[[1,1,1,1],
[2,2,2,2],
[3,3,3,3]]
]))
print("3d NumPy array input: {}".format(c.get_shape()))
Out:
Python list input: (2, 3)
3d NumPy array input: (2, 3, 4)
get_shape()方法返回张量的形状,以整数元组的形式。整数的数量对应于张量的维数,每个整数是沿着该维度的数组条目的数量。例如,形状为(2,3)表示一个矩阵,因为它有两个整数,矩阵的大小为 2×3。
其他类型的源操作构造函数对于在 TensorFlow 中初始化常量非常有用,比如填充常量值、生成随机数和创建序列。
随机数生成器在许多情况下具有特殊重要性,因为它们用于创建 TensorFlow 变量的初始值,这将很快介绍。例如,我们可以使用tf.random.normal()从正态分布生成随机数,分别将形状、平均值和标准差作为第一、第二和第三个参数传递。另外两个有用的随机初始化器示例是截断正态,它像其名称暗示的那样,截断了所有低于和高于平均值两个标准差的值,以及均匀初始化器,它在某个区间a,b)内均匀采样值。
这些方法的示例值显示在图 3-5 中。
![
图 3-5. 从(A)标准正态分布、(B)截断正态分布和(C)均匀分布[–2,2]生成的 50,000 个随机样本。
熟悉 NumPy 的人会认识到一些初始化器,因为它们具有相同的语法。一个例子是序列生成器tf.linspace(a, b, *n*),它从a到b创建*n*个均匀间隔的值。
当我们想要探索对象的数据内容时,使用tf.InteractiveSession()是一个方便的功能。使用它和.eval()方法,我们可以完整查看值,而无需不断引用会话对象:
sess = tf.InteractiveSession()
c = tf.linspace(0.0, 4.0, 5)
print("The content of 'c':\n {}\n".format(c.eval()))
sess.close()
Out:
The content of 'c':
[ 0.1.2.3.4.]
交互式会话
tf.InteractiveSession()允许您替换通常的tf.Session(),这样您就不需要一个变量来保存会话以运行操作。这在交互式 Python 环境中非常有用,比如在编写 IPython 笔记本时。
我们只提到了一些可用源操作。表 3-2 提供了更多有用初始化器的简短描述。
| TensorFlow 操作 | 描述 |
|---|---|
tf.constant(*value*) |
创建一个由参数*value*指定的值或值填充的张量 |
tf.fill(*shape*, *value*) |
创建一个形状为*shape*的张量,并用*value*填充 |
tf.zeros(*shape*) |
返回一个形状为*shape*的张量,所有元素都设置为 0 |
tf.zeros_like(*tensor*) |
返回一个与*tensor*相同类型和形状的张量,所有元素都设置为 0 |
tf.ones(*shape*) |
返回一个形状为*shape*的张量,所有元素都设置为 1 |
tf.ones_like(*tensor*) |
返回一个与*tensor*相同类型和形状的张量,所有元素都设置为 1 |
tf.random_normal(*shape*, *mean*, *stddev*) |
从正态分布中输出随机值 |
tf.truncated_normal(*shape*, *mean*, *stddev*) |
从截断正态分布中输出随机值(其大小超过平均值两个标准差的值被丢弃并重新选择) |
tf.random_uniform(*shape*, *minval*, *maxval*) |
在范围[*minval*, *maxval*)内生成均匀分布的值 |
tf.random_shuffle(*tensor*) |
沿着其第一个维度随机洗牌张量 |
矩阵乘法
这个非常有用的算术运算是通过 TensorFlow 中的tf.matmul(A,B)函数来执行的,其中A和B是两个 Tensor 对象。
假设我们有一个存储矩阵A的张量和另一个存储向量x的张量,并且我们希望计算这两者的矩阵乘积:
Ax = b
在使用matmul()之前,我们需要确保两者具有相同数量的维度,并且它们与预期的乘法正确对齐。
在以下示例中,创建了一个矩阵A和一个向量x:
A = tf.constant([ [1,2,3],
[4,5,6] ])
print(A.get_shape())
x = tf.constant([1,0,1])
print(x.get_shape())
Out:
(2, 3)
(3,)
为了将它们相乘,我们需要向x添加一个维度,将其从一维向量转换为二维单列矩阵。
我们可以通过将张量传递给 tf.expand_dims() 来添加另一个维度,以及作为第二个参数的添加维度的位置。通过在第二个位置(索引 1)添加另一个维度,我们可以得到期望的结果:
x = tf.expand_dims(x,1)
print(x.get_shape())
b = tf.matmul(A,x)
sess = tf.InteractiveSession()
print('matmul result:\n {}'.format(b.eval()))
sess.close()
Out:
(3, 1)
matmul result:
[[ 4]
[10]]
如果我们想翻转一个数组,例如将列向量转换为行向量或反之亦然,我们可以使用 tf.transpose() 函数。
名称
每个张量对象也有一个标识名称。这个名称是一个固有的字符串名称,不要与变量的名称混淆。与 dtype 一样,我们可以使用 .name 属性来查看对象的名称:
with tf.Graph().as_default():
c1 = tf.constant(4,dtype=tf.float64,name='c')
c2 = tf.constant(4,dtype=tf.int32,name='c')
print(c1.name)
print(c2.name)
Out:
c:0
c_1:0
张量对象的名称只是其对应操作的名称(“c”;与冒号连接),后跟产生它的操作的输出中的张量的索引——可能有多个。
重复的名称
在同一图中的对象不能具有相同的名称——TensorFlow 禁止这样做。因此,它会自动添加下划线和数字以区分两者。当它们与不同的图关联时,当然,这两个对象可以具有相同的名称。
名称范围
有时在处理大型、复杂的图时,我们希望创建一些节点分组,以便更容易跟踪和管理。为此,我们可以通过名称对节点进行层次分组。我们可以使用 tf.name_scope("*prefix*") 以及有用的 with 子句来实现:
with tf.Graph().as_default():
c1 = tf.constant(4,dtype=tf.float64,name='c')
with tf.name_scope("prefix_name"):
c2 = tf.constant(4,dtype=tf.int32,name='c')
c3 = tf.constant(4,dtype=tf.float64,name='c')
print(c1.name)
print(c2.name)
print(c3.name)
Out:
c:0
prefix_name/c:0
prefix_name/c_1:0
在这个例子中,我们将包含在变量 c2 和 c3 中的对象分组到作用域 prefix_name 下,这显示为它们名称中的前缀。
当我们希望将图分成具有一定语义意义的子图时,前缀特别有用。这些部分以后可以用于可视化图结构。
变量、占位符和简单优化
在本节中,我们将介绍两种重要的张量对象类型:变量和占位符。然后我们将继续进行主要事件:优化。我们将简要讨论优化模型的所有基本组件,然后进行一些简单的演示,将所有内容整合在一起。
变量
优化过程用于调整给定模型的参数。为此,TensorFlow 使用称为 变量 的特殊对象。与其他每次运行会话时都会“重新填充”数据的张量对象不同,变量可以在图中保持固定状态。这很重要,因为它们当前的状态可能会影响它们在下一次迭代中的变化。与其他张量一样,变量可以用作图中其他操作的输入。
使用变量分为两个阶段。首先,我们调用 tf.Variable() 函数来创建一个变量并定义它将被初始化的值。然后,我们必须显式执行初始化操作,通过使用 tf.global_variables_initializer() 方法运行会话,该方法为变量分配内存并设置其初始值。
与其他张量对象一样,变量只有在模型运行时才会计算,如下例所示:
init_val = tf.random_normal((1,5),0,1)
var = tf.Variable(init_val, name='var')
print("pre run: \n{}".format(var))
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
post_var = sess.run(var)
print("\npost run: \n{}".format(post_var))
Out:
pre run:
Tensor("var/read:0", shape=(1, 5), dtype=float32)
post run:
[[ 0.859621350.648858550.25370994 -0.373807910.63552463]]
请注意,如果我们再次运行代码,我们会看到每次都会创建一个新变量,这可以通过自动将 _1 连接到其名称来表示:
pre run:
Tensor("var_1/read:0", shape=(1, 5), dtype=float32)
当我们想要重用模型时(复杂模型可能有许多变量!)可能会非常低效;例如,当我们希望用多个不同的输入来喂它时。为了重用相同的变量,我们可以使用 tf.get_variables() 函数而不是 tf.Variable()。有关更多信息,请参阅附录中的 “模型结构”。
占位符
到目前为止,我们已经使用源操作来创建我们的输入数据。然而,TensorFlow 为输入值提供了专门的内置结构。这些结构被称为占位符。占位符可以被认为是将在稍后填充数据的空变量。我们首先构建我们的图形,只有在执行时才用输入数据填充它们。
占位符有一个可选的shape参数。如果没有提供形状或传递为None,那么占位符可以接受任何大小的数据。通常在对应于样本数量(通常是行)的矩阵维度上使用None,同时固定特征的长度(通常是列):
ph = tf.placeholder(tf.float32,shape=(None,10))
每当我们定义一个占位符,我们必须为它提供一些输入值,否则将抛出异常。输入数据通过session.run()方法传递给一个字典,其中每个键对应一个占位符变量名,匹配的值是以列表或 NumPy 数组形式给出的数据值:
sess.run(s,feed_dict={x: X_data,w: w_data})
让我们看看另一个图形示例,这次使用两个输入的占位符:一个矩阵x和一个向量w。这些输入进行矩阵乘法,创建一个五单位向量xw,并与填充值为-1的常量向量b相加。最后,变量s通过使用tf.reduce_max()操作取该向量的最大值。单词reduce之所以被使用,是因为我们将一个五单位向量减少为一个标量:
x_data = np.random.randn(5,10)
w_data = np.random.randn(10,1)
with tf.Graph().as_default():
x = tf.placeholder(tf.float32,shape=(5,10))
w = tf.placeholder(tf.float32,shape=(10,1))
b = tf.fill((5,1),-1.)
xw = tf.matmul(x,w)
xwb = xw + b
s = tf.reduce_max(xwb)
with tf.Session() as sess:
outs = sess.run(s,feed_dict={x: x_data,w: w_data})
print("outs = {}".format(outs))
Out:
outs = 3.06512
优化
现在我们转向优化。我们首先描述训练模型的基础知识,对过程中的每个组件进行简要描述,并展示在 TensorFlow 中如何执行。然后我们演示一个简单回归模型优化过程的完整工作示例。
训练预测
我们有一些目标变量,我们希望用一些特征向量来解释它。为此,我们首先选择一个将两者联系起来的模型。我们的训练数据点将用于“调整”模型,以便最好地捕捉所需的关系。在接下来的章节中,我们将专注于深度神经网络模型,但现在我们将满足于一个简单的回归问题。
让我们从描述我们的回归模型开始:
f(x[i]) = w^(T)x[i] + b
(w被初始化为行向量;因此,转置x将产生与上面方程中相同的结果。)
y[i] = f(x[i]) + ε[i]
f(x[i])被假定为一些输入数据x[i]的线性组合,带有一组权重w和一个截距b。我们的目标输出y[i]是f(x[i])与高斯噪声ε[i]相加后的嘈杂版本(其中i表示给定样本)。
与前面的例子一样,我们需要为输入和输出数据创建适当的占位符,为权重和截距创建变量:
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
w = tf.Variable([[0,0,0]],dtype=tf.float32,name='weights')
b = tf.Variable(0,dtype=tf.float32,name='bias')
一旦定义了占位符和变量,我们就可以写下我们的模型。在这个例子中,它只是一个多元线性回归——我们预测的输出y_pred是我们的输入容器x和我们的权重w以及一个偏置项b的矩阵乘法的结果:
y_pred = tf.matmul(w,tf.transpose(x)) + b
定义损失函数
接下来,我们需要一个好的度量标准,用来评估模型的性能。为了捕捉我们模型预测和观察目标之间的差异,我们需要一个反映“距离”的度量标准。这个距离通常被称为一个目标或损失函数,我们通过找到一组参数(在这种情况下是权重和偏置)来最小化它来优化模型。
没有理想的损失函数,选择最合适的损失函数通常是艺术和科学的结合。选择可能取决于几个因素,比如我们模型的假设、它有多容易最小化,以及我们更喜欢避免哪种类型的错误。
均方误差和交叉熵
也许最常用的损失是 MSE(均方误差),其中对于所有样本,我们平均了真实目标与我们的模型在样本间预测之间的平方距离:
这种损失具有直观的解释——它最小化了观察值与模型拟合值之间的均方差差异(这些差异被称为残差)。
在我们的线性回归示例中,我们取向量 y_true(y),真实目标,与 y_pred(ŷ),模型的预测之间的差异,并使用 tf.square() 计算差异向量的平方。这个操作是逐元素应用的。然后使用 tf.reduce_mean() 函数对平方差异进行平均:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
另一个非常常见的损失函数,特别适用于分类数据,是交叉熵,我们在上一章中在 softmax 分类器中使用过。交叉熵由以下公式给出
对于具有单个正确标签的分类(在绝大多数情况下都是这样),简化为分类器放置在正确标签上的概率的负对数。
在 TensorFlow 中:
loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true,logits=y_pred)
loss = tf.reduce_mean(loss)
交叉熵是两个分布之间相似性的度量。由于深度学习中使用的分类模型通常为每个类别输出概率,我们可以将真实类别(分布 p)与模型给出的每个类别的概率(分布 q)进行比较。两个分布越相似,我们的交叉熵就越小。
梯度下降优化器
我们接下来需要弄清楚如何最小化损失函数。在某些情况下,可以通过解析方法找到全局最小值(存在时),但在绝大多数情况下,我们将不得不使用优化算法。优化器会迭代更新权重集,以逐渐减少损失。
最常用的方法是梯度下降,其中我们使用损失相对于权重集的梯度。稍微更技术性的说法是,如果我们的损失是某个多元函数 F(w̄),那么在某点 w̄[0] 的邻域内,F(w̄) 的“最陡”减少方向是通过从 w̄[0] 沿着 F 在 w̄[0] 处的负梯度方向移动而获得的。
所以如果 w̄[1] = w̄[0]-γ∇F(w̄[0]) 其中 ∇F(w̄[0]) 是在 w̄[0] 处评估的 F 的梯度,那么对于足够小的 γ:
F(w̄[0]) ⩾ F(w̄[1])
梯度下降算法在高度复杂的网络架构上表现良好,因此适用于各种问题。更具体地说,最近的进展使得可以利用大规模并行系统来计算这些梯度,因此这种方法在维度上具有很好的扩展性(尽管对于大型实际问题仍可能非常耗时)。对于凸函数,收敛到全局最小值是有保证的,但对于非凸问题(在深度学习领域基本上都是非凸问题),它们可能会陷入局部最小值。在实践中,这通常已经足够好了,正如深度学习领域的巨大成功所证明的那样。
采样方法
目标的梯度是针对模型参数计算的,并使用给定的输入样本集x[s]进行评估。对于这个计算,我们应该取多少样本?直觉上,计算整个样本集的梯度是有意义的,以便从最大可用信息中受益。然而,这种方法也有一些缺点。例如,当数据集需要的内存超过可用内存时,它可能会非常慢且难以处理。
一种更流行的技术是随机梯度下降(SGD),在这种技术中,不是将整个数据集一次性提供给算法进行每一步的计算,而是顺序地抽取数据的子集。样本数量从一次一个样本到几百个不等,但最常见的大小在大约 50 到大约 500 之间(通常称为mini-batches)。
通常使用较小的批次会更快,批次的大小越小,计算速度就越快。然而,这样做存在一个权衡,即小样本导致硬件利用率降低,并且往往具有较高的方差,导致目标函数出现大幅波动。然而,事实证明,一些波动是有益的,因为它们使参数集能够跳到新的、潜在更好的局部最小值。因此,使用相对较小的批次大小在这方面是有效的,目前是首选的方法。
TensorFlow 中的梯度下降
TensorFlow 非常容易和直观地使用梯度下降算法。TensorFlow 中的优化器通过向图中添加新操作来计算梯度,并且梯度是使用自动微分计算的。这意味着,一般来说,TensorFlow 会自动计算梯度,从计算图的操作和结构中“推导”出梯度。
设置的一个重要参数是算法的学习率,确定每次更新迭代的侵略性有多大(或者换句话说,负梯度方向的步长有多大)。我们希望损失的减少速度足够快,但另一方面又不要太大,以至于我们超过目标并最终到达损失函数值更高的点。
我们首先使用所需的学习率使用GradientDescentOptimizer()函数创建一个优化器。然后,我们通过调用optimizer.minimize()函数并将损失作为参数传递来创建一个 train 操作,用于更新我们的变量:
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
train = optimizer.minimize(loss)
当传递给sess.run()方法时,train 操作就会执行。
用示例总结
我们已经准备就绪!让我们将本节讨论的所有组件结合起来,优化两个模型的参数:线性回归和逻辑回归。在这些示例中,我们将创建具有已知属性的合成数据,并看看模型如何通过优化过程恢复这些属性。
示例 1:线性回归
在这个问题中,我们感兴趣的是检索一组权重w和一个偏差项b,假设我们的目标值是一些输入向量x的线性组合,每个样本还添加了一个高斯噪声ε[i]。
在这个练习中,我们将使用 NumPy 生成合成数据。我们创建了 2,000 个x样本,一个具有三个特征的向量,将每个x样本与一组权重w([0.3, 0.5, 0.1])的内积取出,并添加一个偏置项b(-0.2)和高斯噪声到结果中:
import numpy as np
# === Create data and simulate results =====
x_data = np.random.randn(2000,3)
w_real = [0.3,0.5,0.1]
b_real = -0.2
noise = np.random.randn(1,2000)*0.1
y_data = np.matmul(w_real,x_data.T) + b_real + noise
嘈杂的样本显示在图 3-6 中。

图 3-6. 用于线性回归的生成数据:每个填充的圆代表一个样本,虚线显示了没有噪声成分(对角线)的预期值。
接下来,我们通过优化模型(即找到最佳参数)来估计我们的权重w和偏置b,使其预测尽可能接近真实目标。每次迭代计算对当前参数的更新。在这个例子中,我们运行 10 次迭代,使用sess.run()方法在每 5 次迭代时打印我们估计的参数。
不要忘记初始化变量!在这个例子中,我们将权重和偏置都初始化为零;然而,在接下来的章节中,我们将看到一些“更智能”的初始化技术可供选择。我们使用名称作用域来将推断输出、定义损失、设置和创建训练对象的相关部分分组在一起:
NUM_STEPS = 10
g = tf.Graph()
wb_ = []
with g.as_default():
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
with tf.name_scope('inference') as scope:
w = tf.Variable([[0,0,0]],dtype=tf.float32,name='weights')
b = tf.Variable(0,dtype=tf.float32,name='bias')
y_pred = tf.matmul(w,tf.transpose(x)) + b
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
with tf.name_scope('train') as scope:
learning_rate = 0.5
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
train = optimizer.minimize(loss)
# Before starting, initialize the variables. We will 'run' this first.
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(NUM_STEPS):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
print(10, sess.run([w,b]))
然后,我们得到了结果:
(0, [array([[ 0.30149955, 0.49303722, 0.11409992]],
dtype=float32), -0.18563795])
(5, [array([[ 0.30094019, 0.49846715, 0.09822173]],
dtype=float32), -0.19780949])
(10, [array([[ 0.30094025, 0.49846718, 0.09822182]],
dtype=float32), -0.19780946])
仅经过 10 次迭代,估计的权重和偏置分别为 = [0.301, 0.498, 0.098] 和 = -0.198。原始参数值为 w = [0.3,0.5,0.1] 和 b = -0.2。
几乎完美匹配!
示例 2:逻辑回归
我们再次希望在模拟数据设置中检索权重和偏置组件,这次是在逻辑回归框架中。这里,线性部分 w^Tx + b 是一个称为逻辑函数的非线性函数的输入。它的有效作用是将线性部分的值压缩到区间 [0, 1]:
Pr(y[i] = 1|x[i]) =
然后,我们将这些值视为概率,从中生成二进制的是/1 或否/0 的结果。这是模型的非确定性(嘈杂)部分。
逻辑函数更加通用,可以使用不同的参数集合来控制曲线的陡峭程度和最大值。我们使用的这种逻辑函数的特殊情况也被称为sigmoid 函数。
我们通过使用与前面示例中相同的权重和偏置来生成我们的样本:
N = 20000
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# === Create data and simulate results =====
x_data = np.random.randn(N,3)
w_real = [0.3,0.5,0.1]
b_real = -0.2
wxb = np.matmul(w_real,x_data.T) + b_real
y_data_pre_noise = sigmoid(wxb)
y_data = np.random.binomial(1,y_data_pre_noise)
在输出进行二值化之前和之后的结果样本显示在图 3-7 中。

图 3-7. 用于逻辑回归的生成数据:每个圆代表一个样本。在左图中,我们看到通过将输入数据的线性组合输入到逻辑函数中生成的概率。右图显示了从左图中的概率中随机抽样得到的二进制目标输出。
我们在代码中唯一需要更改的是我们使用的损失函数。
我们想要在这里使用的损失函数是交叉熵的二进制版本,这也是逻辑回归模型的似然度:
y_pred = tf.sigmoid(y_pred)
loss = -y_true*tf.log(y_pred) - (1-y_true)*tf.log(1-y_pred)
loss=tf.reduce_mean(loss)
幸运的是,TensorFlow 已经有一个指定的函数可以代替我们使用:
tf.nn.sigmoid_cross_entropy_with_logits(labels=,logits=)
我们只需要传递真实输出和模型的线性预测:
NUM_STEPS = 50
with tf.name_scope('loss') as scope:
loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true,logits=y_pred)
loss = tf.reduce_mean(loss)
# Before starting, initialize the variables. We will 'run' this first.
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(NUM_STEPS):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
print(50, sess.run([w,b]))
让我们看看我们得到了什么:
(0, [array([[ 0.03212515, 0.05890014, 0.01086476]],
dtype=float32), -0.021875083])
(5, [array([[ 0.14185661, 0.25990966, 0.04818931]],
dtype=float32), -0.097346731])
(10, [array([[ 0.20022796, 0.36665651, 0.06824245]],
dtype=float32), -0.13804035])
(15, [array([[ 0.23269908, 0.42593899, 0.07949805]],
dtype=float32), -0.1608445])
(20, [array([[ 0.2512995 , 0.45984453, 0.08599731]],
dtype=float32), -0.17395383])
(25, [array([[ 0.26214141, 0.47957924, 0.08981277]],
dtype=float32), -0.1816061])
(30, [array([[ 0.26852587, 0.49118528, 0.09207394]],
dtype=float32), -0.18611355])
(35, [array([[ 0.27230808, 0.49805275, 0.09342111]],
dtype=float32), -0.18878292])
(40, [array([[ 0.27455658, 0.50213116, 0.09422609]],
dtype=float32), -0.19036882])
(45, [array([[ 0.27589601, 0.5045585 , 0.09470785]],
dtype=float32), -0.19131286])
(50, [array([[ 0.27656636, 0.50577223, 0.09494986]],
dtype=float32), -0.19178495])
需要更多的迭代才能收敛,比前面的线性回归示例需要更多的样本,但最终我们得到的结果与原始选择的权重非常相似。
总结
在这一章中,我们学习了计算图以及我们可以如何使用它们。我们看到了如何创建一个图以及如何计算它的输出。我们介绍了 TensorFlow 的主要构建模块——Tensor 对象,代表图的操作,用于输入数据的占位符,以及作为模型训练过程中调整的变量。我们学习了张量数组,并涵盖了数据类型、形状和名称属性。最后,我们讨论了模型优化过程,并看到了如何在 TensorFlow 中实现它。在下一章中,我们将深入探讨在计算机视觉中使用的更高级的深度神经网络。
第四章:卷积神经网络
在本章中,我们介绍卷积神经网络(CNNs)以及与之相关的构建块和方法。我们从对 MNIST 数据集进行分类的简单模型开始,然后介绍 CIFAR10 对象识别数据集,并将几个 CNN 模型应用于其中。尽管小巧快速,但本章介绍的 CNN 在实践中被广泛使用,以获得物体识别任务中的最新结果。
CNN 简介
在过去几年中,卷积神经网络作为一种特别有前途的深度学习形式获得了特殊地位。根植于图像处理,卷积层已经在几乎所有深度学习的子领域中找到了应用,并且在大多数情况下非常成功。
全连接和卷积神经网络之间的根本区别在于连续层之间连接的模式。在全连接的情况下,正如名称所示,每个单元都连接到前一层的所有单元。我们在第二章中看到了一个例子,其中 10 个输出单元连接到所有输入图像像素。
另一方面,在神经网络的卷积层中,每个单元连接到前一层中附近的(通常很少)几个单元。此外,所有单元以相同的方式连接到前一层,具有相同的权重和结构。这导致了一种称为卷积的操作,给这种架构命名(请参见图 4-1 以了解这个想法的示例)。在下一节中,我们将更详细地介绍卷积操作,但简而言之,对我们来说,这意味着在图像上应用一小部分“窗口”权重(也称为滤波器),如稍后的图 4-2 所示。

图 4-1。在全连接层(左侧),每个单元都连接到前一层的所有单元。在卷积层(右侧),每个单元连接到前一层的一个局部区域中的固定数量的单元。此外,在卷积层中,所有单元共享这些连接的权重,如共享的线型所示。
有一些常常被引用为导致 CNN 方法的动机,来自不同的思想流派。第一个角度是所谓的模型背后的神经科学启发。第二个涉及对图像性质的洞察,第三个与学习理论有关。在我们深入了解实际机制之前,我们将简要介绍这些内容。
通常将神经网络总体描述为计算的生物学启发模型,特别是卷积神经网络。有时,有人声称这些模型“模仿大脑执行计算的方式”。尽管直接理解时会产生误导,但生物类比具有一定的兴趣。
诺贝尔奖获得者神经生理学家 Hubel 和 Wiesel 早在 1960 年代就发现,大脑中视觉处理的第一阶段包括将相同的局部滤波器(例如,边缘检测器)应用于视野的所有部分。神经科学界目前的理解是,随着视觉处理的进行,信息从输入的越来越广泛的部分集成,这是按层次进行的。
卷积神经网络遵循相同的模式。随着我们深入网络,每个卷积层查看图像的越来越大的部分。最常见的情况是,这将被全连接层跟随,这些全连接层在生物启发的类比中充当处理全局信息的更高级别的视觉处理层。
第二个角度,更加注重硬性事实工程方面,源于图像及其内容的性质。当在图像中寻找一个对象,比如一只猫的脸时,我们通常希望能够无论其在图像中的位置如何都能检测到它。这反映了自然图像的性质,即相同的内容可能在图像的不同位置找到。这种性质被称为不变性——这种类型的不变性也可以预期在(小)旋转、光照变化等方面存在。
因此,在构建一个对象识别系统时,应该对平移具有不变性(并且,根据情况,可能还对旋转和各种变形具有不变性,但这是另一回事)。简而言之,因此在图像的不同部分执行完全相同的计算是有意义的。从这个角度来看,卷积神经网络层在所有空间区域上计算图像的相同特征。
最后,卷积结构可以被看作是一种正则化机制。从这个角度来看,卷积层就像全连接层,但是我们不是在完整的矩阵空间中寻找权重,而是将搜索限制在描述固定大小卷积的矩阵中,将自由度的数量减少到卷积的大小,这通常非常小。
正则化
术语正则化在本书中被广泛使用。在机器学习和统计学中,正则化主要用于指的是通过对解的复杂性施加惩罚来限制优化问题,以防止对给定示例的过度拟合。
过拟合发生在规则(例如,分类器)以解释训练集的方式计算时,但对未见数据的泛化能力较差。
正则化通常通过添加关于期望结果的隐式信息来实现(这可能采取的形式是说在搜索函数空间时我们更希望有一个更平滑的函数)。在卷积神经网络的情况下,我们明确表示我们正在寻找相对低维子空间中的权重,这些权重对应于固定大小的卷积。
在本章中,我们涵盖了与卷积神经网络相关的层和操作类型。我们首先重新审视 MNIST 数据集,这次应用一个准确率约为 99%的模型。接下来,我们将转向更有趣的对象识别 CIFAR10 数据集。
MNIST:第二次
在本节中,我们再次查看 MNIST 数据集,这次将一个小型卷积神经网络应用作为我们的分类器。在这样做之前,有几个元素和操作我们必须熟悉。
卷积
卷积操作,正如你可能从架构的名称中期待的那样,是卷积神经网络中连接层的基本手段。我们使用内置的 TensorFlow conv2d():
tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
在这里,x是数据——输入图像,或者是在网络中进一步应用之前的卷积层后获得的下游特征图。正如之前讨论的,在典型的 CNN 模型中,我们按层次堆叠卷积层,并且特征图只是一个常用术语,指的是每个这样的层的输出。查看这些层的输出的另一种方式是处理后的图像,是应用滤波器和其他操作的结果。在这里,这个滤波器由W参数化,表示我们网络中学习的卷积滤波器的权重。这只是我们在图 4-2 中看到的小“滑动窗口”中的一组权重。

图 4-2。相同的卷积滤波器——一个“滑动窗口”——应用于图像之上。
这个操作的输出将取决于x和W的形状,在我们的情况下是四维的。图像数据x的形状将是:
[None, 28, 28, 1]
这意味着我们有未知数量的图像,每个图像为 28×28 像素,具有一个颜色通道(因为这些是灰度图像)。我们使用的权重W的形状将是:
[5, 5, 1, 32]
初始的 5×5×1 表示在图像中要进行卷积的小“窗口”的大小,在我们的情况下是一个 5×5 的区域。在具有多个颜色通道的图像中(RGB,如第一章中简要讨论的),我们将每个图像视为 RGB 值的三维张量,但在这个单通道数据中它们只是二维的,卷积滤波器应用于二维区域。稍后,当我们处理 CIFAR10 数据时,我们将看到多通道图像的示例以及如何相应地设置权重W的大小。
最终的 32 是特征图的数量。换句话说,我们有卷积层的多组权重——在这种情况下有 32 组。回想一下,卷积层的概念是沿着图像计算相同的特征;我们希望计算许多这样的特征,因此使用多组卷积滤波器。
strides参数控制滤波器W在图像(或特征图)x上的空间移动。
值[1, 1, 1, 1]表示滤波器在每个维度上以一个像素间隔应用于输入,对应于“全”卷积。此参数的其他设置允许我们在应用滤波器时引入跳跃—这是我们稍后会应用的常见做法—从而使得生成的特征图更小。
最后,将padding设置为'SAME'意味着填充x的边界,使得操作的结果大小与x的大小相同。
激活函数
在线性层之后,无论是卷积还是全连接,常见的做法是应用非线性激活函数(参见图 4-3 中的一些示例)。激活函数的一个实际方面是,连续的线性操作可以被单个操作替代,因此深度不会为模型的表达能力做出贡献,除非我们在线性层之间使用非线性激活。

图 4-3。常见的激活函数:逻辑函数(左)、双曲正切函数(中)、修正线性单元(右)
池化
在卷积层后跟随输出的池化是常见的。技术上,池化意味着使用某种本地聚合函数减少数据的大小,通常在每个特征图内部。
这背后的原因既是技术性的,也是更理论性的。技术方面是,池化会减少下游处理的数据量。这可以极大地减少模型中的总参数数量,特别是在卷积层之后使用全连接层的情况下。
应用池化的更理论的原因是,我们希望我们计算的特征不受图像中位置的微小变化的影响。例如,一个在图像右上部寻找眼睛的特征,如果我们稍微向右移动相机拍摄图片,将眼睛略微移动到图像中心,这个特征不应该有太大变化。在空间上聚合“眼睛检测器特征”使模型能够克服图像之间的这种空间变化,捕捉本章开头讨论的某种不变性形式。
在我们的示例中,我们对每个特征图的 2×2 块应用最大池化操作:
tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
最大池化输出预定义大小的每个区域中的输入的最大值(这里是 2×2)。ksize参数控制池化的大小(2×2),strides参数控制我们在x上“滑动”池化网格的幅度,就像在卷积层的情况下一样。将其设置为 2×2 网格意味着池化的输出将恰好是原始高度和宽度的一半,总共是原始大小的四分之一。
Dropout
我们模型中最后需要的元素是dropout。这是一种正则化技巧,用于强制网络将学习的表示分布到所有神经元中。在训练期间,dropout 会“关闭”一定比例的层中的单位,通过将它们的值设置为零。这些被丢弃的神经元是随机的,每次计算都不同,迫使网络学习一个即使在丢失后仍能正常工作的表示。这个过程通常被认为是训练多个网络的“集成”,从而增加泛化能力。在测试时使用网络作为分类器时(“推断”),不会进行 dropout,而是直接使用完整的网络。
在我们的示例中,除了我们希望应用 dropout 的层之外,唯一的参数是keep_prob,即每一步保持工作的神经元的比例:
tf.nn.dropout(layer, keep_prob=keep_prob)
为了能够更改此值(我们必须这样做,因为对于测试,我们希望这个值为1.0,表示根本没有丢失),我们将使用tf.placeholder并传递一个值用于训练(.5)和另一个用于测试(1.0)。
模型
首先,我们定义了一些辅助函数,这些函数将在本章中广泛使用,用于创建我们的层。这样做可以使实际模型简短易读(在本书的后面,我们将看到存在几种框架,用于更抽象地定义深度学习构建块,这样我们可以专注于快速设计我们的网络,而不是定义所有必要的元素的繁琐工作)。我们的辅助函数有:
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def conv_layer(input, shape):
W = weight_variable(shape)
b = bias_variable([shape[3]])
return tf.nn.relu(conv2d(input, W) + b)
def full_layer(input, size):
in_size = int(input.get_shape()[1])
W = weight_variable([in_size, size])
b = bias_variable([size])
return tf.matmul(input, W) + b
让我们更仔细地看看这些:
weight_variable()
这指定了网络的全连接或卷积层的权重。它们使用截断正态分布进行随机初始化,标准差为 0.1。这种使用截断在尾部的随机正态分布初始化是相当常见的,通常会产生良好的结果(请参见即将介绍的随机初始化的注释)。
bias_variable()
这定义了全连接或卷积层中的偏置元素。它们都使用常数值.1进行初始化。
conv2d()
这指定了我们通常会使用的卷积。一个完整的卷积(没有跳过),输出与输入大小相同。
max_pool_2×2
这将将最大池设置为高度/宽度维度的一半大小,并且总体上是特征图大小的四分之一。
conv_layer()
这是我们将使用的实际层。线性卷积如conv2d中定义的,带有偏置,然后是 ReLU 非线性。
full_layer()
带有偏置的标准全连接层。请注意,这里我们没有添加 ReLU。这使我们可以在最终输出时使用相同的层,我们不需要非线性部分。
定义了这些层后,我们准备设置我们的模型(请参见图 4-4 中的可视化):
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])
conv1 = conv_layer(x_image, shape=[5, 5, 1, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 7*7*64])
full_1 = tf.nn.relu(full_layer(conv2_flat, 1024))
keep_prob = tf.placeholder(tf.float32)
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)

图 4-4. 所使用的 CNN 架构的可视化。
随机初始化
在前一章中,我们讨论了几种类型的初始化器,包括此处用于卷积层权重的随机初始化器:
initial = tf.truncated_normal(shape, stddev=0.1)
关于深度学习模型训练中初始化的重要性已经说了很多。简而言之,糟糕的初始化可能会使训练过程“卡住”,或者由于数值问题完全失败。使用随机初始化而不是常数初始化有助于打破学习特征之间的对称性,使模型能够学习多样化和丰富的表示。使用边界值有助于控制梯度的幅度,使网络更有效地收敛,等等。
我们首先为图像和正确标签定义占位符x和y_。接下来,我们将图像数据重新整形为尺寸为 28×28×1 的 2D 图像格式。回想一下,我们在之前的 MNIST 模型中不需要数据的空间方面,因为所有像素都是独立处理的,但在卷积神经网络框架中,考虑图像时利用这种空间含义是一个重要的优势。
接下来我们有两个连续的卷积和池化层,每个层都有 5×5 的卷积和 32 个特征图,然后是一个具有 1,024 个单元的单个全连接层。在应用全连接层之前,我们将图像展平为单个向量形式,因为全连接层不再需要空间方面。
请注意,在两个卷积和池化层之后,图像的尺寸为 7×7×64。原始的 28×28 像素图像首先缩小到 14×14,然后在两个池化操作中缩小到 7×7。64 是我们在第二个卷积层中创建的特征图的数量。在考虑模型中学习参数的总数时,大部分将在全连接层中(从 7×7×64 到 1,024 的转换给我们提供了 3.2 百万个参数)。如果我们没有使用最大池化,这个数字将是原来的 16 倍(即 28×28×64×1,024,大约为 51 百万)。
最后,输出是一个具有 10 个单元的全连接层,对应数据集中的标签数量(回想一下 MNIST 是一个手写数字数据集,因此可能的标签数量是 10)。
其余部分与第二章中第一个 MNIST 模型中的内容相同,只有一些细微的变化:
train_accuracy
我们在每 100 步打印模型在用于训练的批次上的准确率。这是在训练步骤之前完成的,因此是对模型在训练集上当前性能的良好估计。
test_accuracy
我们将测试过程分为 10 个包含 1,000 张图像的块。对于更大的数据集,这样做非常重要。
以下是完整的代码:
mnist = input_data.read_data_sets(DATA_DIR, one_hot=True)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=
y_conv, labels=y_))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(STEPS):
batch = mnist.train.next_batch(50)
if i % 100 == 0:
train_accuracy = sess.run(accuracy, feed_dict={x: batch[0],
y_: batch[1],
keep_prob: 1.0})
print "step {}, training accuracy {}".format(i, train_accuracy)
sess.run(train_step, feed_dict={x: batch[0], y_: batch[1],
keep_prob: 0.5})
X = mnist.test.images.reshape(10, 1000, 784)
Y = mnist.test.labels.reshape(10, 1000, 10)
test_accuracy = np.mean([sess.run(accuracy,
feed_dict={x:X[i], y_:Y[i],keep_prob:1.0})
for i in range(10)])
print "test accuracy: {}".format(test_accuracy)
这个模型的性能已经相当不错,仅经过 5 个周期,准确率就超过了 99%,¹这相当于 5,000 步,每个步骤的迷你批次大小为 50。
有关多年来使用该数据集的模型列表以及如何进一步改进结果的一些想法,请查看http://yann.lecun.com/exdb/mnist/。
CIFAR10
CIFAR10是另一个在计算机视觉和机器学习领域有着悠久历史的数据集。与 MNIST 类似,它是一个常见的基准,各种方法都会被测试。CIFAR10是一个包含 60,000 张尺寸为 32×32 像素的彩色图像的数据集,每张图像属于以下十个类别之一:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。
针对这个数据集的最先进的深度学习方法在分类这些图像方面与人类一样出色。在本节中,我们首先使用相对较简单的方法,这些方法将运行相对较快。然后,我们简要讨论这些方法与最先进方法之间的差距。
加载 CIFAR10 数据集
在本节中,我们构建了一个类似于用于 MNIST 的内置input_data.read_data_sets()的 CIFAR10 数据管理器。²
首先,下载数据集的 Python 版本并将文件提取到本地目录中。现在应该有以下文件:
-
data_batch_1, data_batch_2, data_batch_3, data_batch_4, data_batch_5
-
test_batch
-
batches_meta
-
readme.html
data_batch_X文件是包含训练数据的序列化数据文件,test_batch是一个类似的包含测试数据的序列化文件。batches_meta文件包含从数字到语义标签的映射。.html文件是 CIFAR-10 数据集网页的副本。
由于这是一个相对较小的数据集,我们将所有数据加载到内存中:
class CifarLoader(object):
def __init__(self, source_files):
self._source = source_files
self._i = 0
self.images = None
self.labels = None
def load(self):
data = [unpickle(f) for f in self._source]
images = np.vstack([d["data"] for d in data])
n = len(images)
self.images = images.reshape(n, 3, 32, 32).transpose(0, 2, 3, 1)\
.astype(float) / 255
self.labels = one_hot(np.hstack([d["labels"] for d in data]), 10)
return self
def next_batch(self, batch_size):
x, y = self.images[self._i:self._i+batch_size],
self.labels[self._i:self._i+batch_size]
self._i = (self._i + batch_size) % len(self.images)
return x, y
在这里我们使用以下实用函数:
DATA_PATH="*`/path/to/CIFAR10`*"defunpickle(file):withopen(os.path.join(DATA_PATH,file),'rb')asfo:dict=cPickle.load(fo)returndictdefone_hot(vec,vals=10):n=len(vec)out=np.zeros((n,vals))out[range(n),vec]=1returnout
unpickle()函数返回一个带有data和labels字段的dict,分别包含图像数据和标签。one_hot()将标签从整数(范围为 0 到 9)重新编码为长度为 10 的向量,其中除了标签位置上的 1 之外,所有位置都是 0。
最后,我们创建一个包含训练和测试数据的数据管理器:
class CifarDataManager(object):
def __init__(self):
self.train = CifarLoader(["data_batch_{}".format(i)
for i in range(1, 6)])
.load()
self.test = CifarLoader(["test_batch"]).load()
使用 Matplotlib,我们现在可以使用数据管理器来显示一些 CIFAR10 图像,并更好地了解这个数据集中的内容:
def display_cifar(images, size):
n = len(images)
plt.figure()
plt.gca().set_axis_off()
im = np.vstack([np.hstack([images[np.random.choice(n)] for i in range(size)])
for i in range(size)])
plt.imshow(im)
plt.show()
d = CifarDataManager()
print "Number of train images: {}".format(len(d.train.images))
print "Number of train labels: {}".format(len(d.train.labels))
print "Number of test images: {}".format(len(d.test.images))
print "Number of test images: {}".format(len(d.test.labels))
images = d.train.images
display_cifar(images, 10)
Matplotlib
Matplotlib是一个用于绘图的有用的 Python 库,设计得看起来和行为类似于 MATLAB 绘图。这通常是快速绘制和可视化数据集的最简单方法。
display_cifar()函数的参数是images(包含图像的可迭代对象)和size(我们想要显示的图像数量),并构建并显示一个size×size的图像网格。这是通过垂直和水平连接实际图像来形成一个大图像。
在显示图像网格之前,我们首先打印训练/测试集的大小。CIFAR10 包含 50K 个训练图像和 10K 个测试图像:
Number of train images: 50000
Number of train labels: 50000
Number of test images: 10000
Number of test images: 10000
在图 4-5 中生成并显示的图像旨在让人了解 CIFAR10 图像实际上是什么样子的。值得注意的是,这些小的 32×32 像素图像每个都包含一个完整的单个对象,该对象位于中心位置,即使在这种分辨率下也基本上是可识别的。

图 4-5。100 个随机的 CIFAR10 图像。
简单的 CIFAR10 模型
我们将从先前成功用于 MNIST 数据集的模型开始。回想一下,MNIST 数据集由 28×28 像素的灰度图像组成,而 CIFAR10 图像是带有 32×32 像素的彩色图像。这将需要对计算图的设置进行轻微调整:
cifar = CifarDataManager()
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
conv1 = conv_layer(x, shape=[5, 5, 3, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 8 * 8 * 64])
full_1 = tf.nn.relu(full_layer(conv2_flat, 1024))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y_conv,
y_))
train_step = tf.train.AdamOptimizer(1e-3).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
def test(sess):
X = cifar.test.images.reshape(10, 1000, 32, 32, 3)
Y = cifar.test.labels.reshape(10, 1000, 10)
acc = np.mean([sess.run(accuracy, feed_dict={x: X[i], y_: Y[i],
keep_prob: 1.0})
for i in range(10)])
print "Accuracy: {:.4}%".format(acc * 100)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(STEPS):
batch = cifar.train.next_batch(BATCH_SIZE)
sess.run(train_step, feed_dict={x: batch[0], y_: batch[1],
keep_prob: 0.5})
test(sess)
这第一次尝试将在几分钟内达到大约 70%的准确度(使用批量大小为 100,自然取决于硬件和配置)。这好吗?截至目前,最先进的深度学习方法在这个数据集上实现了超过 95%的准确度,但是使用更大的模型并且通常需要许多小时的训练。
这与之前介绍的类似 MNIST 模型之间存在一些差异。首先,输入由大小为 32×32×3 的图像组成,第三维是三个颜色通道:
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
同样,在两次池化操作之后,我们这次剩下的是大小为 8×8 的 64 个特征图:
conv2_flat = tf.reshape(conv2_pool, [-1, 8 * 8 * 64])
最后,为了方便起见,我们将测试过程分组到一个名为test()的单独函数中,并且我们不打印训练准确度值(可以使用与 MNIST 模型中相同代码添加回来)。
一旦我们有了一些可接受的基线准确度的模型(无论是从简单的 MNIST 模型还是从其他数据集的最先进模型中派生的),一个常见的做法是通过一系列的适应和更改来尝试改进它,直到达到我们的目的所需的内容。
在这种情况下,保持其他所有内容不变,我们将添加一个具有 128 个特征图和 dropout 的第三个卷积层。我们还将把完全连接层中的单元数从 1,024 减少到 512:
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
conv1 = conv_layer(x, shape=[5, 5, 3, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv3 = conv_layer(conv2_pool, shape=[5, 5, 64, 128])
conv3_pool = max_pool_2x2(conv3)
conv3_flat = tf.reshape(conv3_pool, [-1, 4 * 4 * 128])
conv3_drop = tf.nn.dropout(conv3_flat, keep_prob=keep_prob)
full_1 = tf.nn.relu(full_layer(conv3_drop, 512))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
这个模型将需要稍长一点的时间来运行(但即使没有复杂的硬件,也不会超过一个小时),并且可以达到大约 75%的准确度。
这仍然与最佳已知方法之间存在相当大的差距。有几个独立适用的元素可以帮助缩小这个差距:
模型大小
对于这种数据集和类似数据集,大多数成功的方法使用更深的网络和更多可调参数。
其他类型的层和方法
通常与这里介绍的层一起使用的是其他类型的流行层,比如局部响应归一化。
优化技巧
更多关于这个的内容以后再说!
领域知识
利用领域知识进行预处理通常是很有帮助的。在这种情况下,这将是传统的图像处理。
数据增强
基于现有数据集添加训练数据可能会有所帮助。例如,如果一张狗的图片水平翻转,那么显然仍然是一张狗的图片(但垂直翻转呢?)。小的位移和旋转也经常被使用。
重用成功的方法和架构
和大多数工程领域一样,从一个经过时间验证的方法开始,并根据自己的需求进行调整通常是正确的方式。在深度学习领域,这经常通过微调预训练模型来实现。
我们将在本章中介绍的最终模型是实际为这个数据集产生出色结果的模型类型的缩小版本。这个模型仍然紧凑快速,在大约 150 个 epochs 后达到约 83%的准确率:
C1, C2, C3 = 30, 50, 80
F1 = 500
conv1_1 = conv_layer(x, shape=[3, 3, 3, C1])
conv1_2 = conv_layer(conv1_1, shape=[3, 3, C1, C1])
conv1_3 = conv_layer(conv1_2, shape=[3, 3, C1, C1])
conv1_pool = max_pool_2x2(conv1_3)
conv1_drop = tf.nn.dropout(conv1_pool, keep_prob=keep_prob)
conv2_1 = conv_layer(conv1_drop, shape=[3, 3, C1, C2])
conv2_2 = conv_layer(conv2_1, shape=[3, 3, C2, C2])
conv2_3 = conv_layer(conv2_2, shape=[3, 3, C2, C2])
conv2_pool = max_pool_2x2(conv2_3)
conv2_drop = tf.nn.dropout(conv2_pool, keep_prob=keep_prob)
conv3_1 = conv_layer(conv2_drop, shape=[3, 3, C2, C3])
conv3_2 = conv_layer(conv3_1, shape=[3, 3, C3, C3])
conv3_3 = conv_layer(conv3_2, shape=[3, 3, C3, C3])
conv3_pool = tf.nn.max_pool(conv3_3, ksize=[1, 8, 8, 1], strides=[1, 8, 8, 1],
padding='SAME')
conv3_flat = tf.reshape(conv3_pool, [-1, C3])
conv3_drop = tf.nn.dropout(conv3_flat, keep_prob=keep_prob)
full1 = tf.nn.relu(full_layer(conv3_drop, F1))
full1_drop = tf.nn.dropout(full1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
这个模型由三个卷积层块组成,接着是我们之前已经见过几次的全连接和输出层。每个卷积层块包含三个连续的卷积层,然后是一个池化层和 dropout。
常数C1、C2和C3控制每个卷积块中每个层的特征图数量,常数F1控制全连接层中的单元数量。
在第三个卷积层之后,我们使用了一个 8×8 的最大池层:
conv3_pool = tf.nn.max_pool(conv3_3, ksize=[1, 8, 8, 1], strides=[1, 8, 8, 1],
padding='SAME')
由于在这一点上特征图的大小为 8×8(在前两个池化层之后,每个轴上都将 32×32 的图片减半),这样全局池化每个特征图并保留最大值。第三个块的特征图数量设置为 80,所以在这一点上(在最大池化之后),表示被减少到只有 80 个数字。这使得模型的整体大小保持较小,因为在过渡到全连接层时参数的数量保持在 80×500。
总结
在本章中,我们介绍了卷积神经网络及其通常由各种构建模块组成。一旦你能够正确运行小型模型,请尝试运行更大更深的模型,遵循相同的原则。虽然你可以随时查看最新的文献并了解哪些方法有效,但通过试错和自己摸索也能学到很多。在接下来的章节中,我们将看到如何处理文本和序列数据,以及如何使用 TensorFlow 抽象来轻松构建 CNN 模型。
¹ 在机器学习和特别是深度学习中,epoch指的是对所有训练数据的一次完整遍历;即,当学习模型已经看到每个训练示例一次时。
² 这主要是为了说明的目的。已经存在包含这种数据包装器的开源库,适用于许多流行的数据集。例如,查看 Keras 中的数据集模块(keras.datasets),特别是keras.datasets.cifar10。
³ 参见谁在 CIFAR-10 中表现最好?以获取方法列表和相关论文。
第五章:文本 I:处理文本和序列,以及 TensorBoard 可视化
在本章中,我们将展示如何在 TensorFlow 中处理序列,特别是文本。我们首先介绍循环神经网络(RNN),这是一类强大的深度学习算法,特别适用于自然语言处理(NLP)。我们展示如何从头开始实现 RNN 模型,介绍一些重要的 TensorFlow 功能,并使用交互式 TensorBoard 可视化模型。然后,我们探讨如何在监督文本分类问题中使用 RNN 进行词嵌入训练。最后,我们展示如何构建一个更高级的 RNN 模型,使用长短期记忆(LSTM)网络,并如何处理可变长度的序列。
序列数据的重要性
我们在前一章中看到,利用图像的空间结构可以导致具有出色结果的先进模型。正如在那一章中讨论的那样,利用结构是成功的关键。正如我们将很快看到的,一种极其重要和有用的结构类型是顺序结构。从数据科学的角度来看,这种基本结构出现在许多数据集中,跨越所有领域。在计算机视觉中,视频是随时间演变的一系列视觉内容。在语音中,我们有音频信号,在基因组学中有基因序列;在医疗保健中有纵向医疗记录,在股票市场中有金融数据,等等(见图 5-1)。

图 5-1。序列数据的普遍性。
一种特别重要的具有强烈顺序结构的数据类型是自然语言——文本数据。利用文本中固有的顺序结构(字符、单词、句子、段落、文档)的深度学习方法处于自然语言理解(NLU)系统的前沿,通常将传统方法远远甩在后面。有许多类型的 NLU 任务需要解决,从文档分类到构建强大的语言模型,从自动回答问题到生成人类级别的对话代理。这些任务非常困难,吸引了整个学术界和工业界 AI 社区的努力和关注。
在本章中,我们专注于基本构建模块和任务,并展示如何在 TensorFlow 中处理序列,主要是文本。我们深入研究了 TensorFlow 中序列模型的核心元素,从头开始实现其中一些,以获得深入的理解。在下一章中,我们将展示更高级的文本建模技术,使用 TensorFlow,而在第七章中,我们将使用提供更简单、高级实现方式的抽象库来实现我们的模型。
我们从最重要和流行的用于序列(特别是文本)的深度学习模型类开始:循环神经网络。
循环神经网络简介
循环神经网络是一类强大且广泛使用的神经网络架构,用于建模序列数据。RNN 模型背后的基本思想是序列中的每个新元素都会提供一些新信息,从而更新模型的当前状态。
在前一章中,我们探讨了使用 CNN 模型进行计算机视觉的内容,讨论了这些架构是如何受到当前科学对人类大脑处理视觉信息方式的启发。这些科学观念通常与我们日常生活中对顺序信息处理方式的常识直觉非常接近。
当我们接收新信息时,显然我们的“历史”和“记忆”并没有被抹去,而是“更新”。当我们阅读文本中的句子时,随着每个新单词,我们当前的信息状态会被更新,这不仅取决于新观察到的单词,还取决于前面的单词。
在统计学和概率论中的一个基本数学构造,通常被用作通过机器学习建模顺序模式的基本构件是马尔可夫链模型。比喻地说,我们可以将我们的数据序列视为“链”,链中的每个节点在某种程度上依赖于前一个节点,因此“历史”不会被抹去,而是被延续。
RNN 模型也基于这种链式结构的概念,并且在如何确切地维护和更新信息方面有所不同。正如它们的名称所示,循环神经网络应用某种形式的“循环”。如图 5-2 所示,在某个时间点t,网络观察到一个输入x[t](句子中的一个单词),并将其“状态向量”从上一个向量h[t-1]更新为h[t]。当我们处理新的输入(下一个单词)时,它将以某种依赖于h[t]的方式进行,因此依赖于序列的历史(我们之前看到的单词影响我们对当前单词的理解)。如图所示,这种循环结构可以简单地被视为一个长长的展开链,链中的每个节点执行相同类型的处理“步骤”,基于它从前一个节点的输出获得的“消息”。当然,这与先前讨论的马尔可夫链模型及其隐马尔可夫模型(HMM)扩展密切相关,这些内容在本书中没有讨论。

图 5-2。随时间更新的循环神经网络。
基础 RNN 实现
在本节中,我们从头开始实现一个基本的 RNN,探索其内部工作原理,并了解 TensorFlow 如何处理序列。我们介绍了一些强大的、相当低级的工具,TensorFlow 提供了这些工具用于处理序列数据,您可以使用这些工具来实现自己的系统。
在接下来的部分中,我们将展示如何使用更高级别的 TensorFlow RNN 模块。
我们从数学上定义我们的基本模型开始。这主要包括定义循环结构 - RNN 更新步骤。
我们简单的基础 vanilla RNN 的更新步骤是
h[t] = tanh(W[x]**x[t] + W[h]h[t-1] + b)
其中W[h],W[x]和b是我们学习的权重和偏置变量,tanh(·)是双曲正切函数,其范围在[-1,1]之间,并且与前几章中使用的 sigmoid 函数密切相关,x[t]和h[t]是之前定义的输入和状态向量。最后,隐藏状态向量乘以另一组权重,产生出现在图 5-2 中的输出。
MNIST 图像作为序列
为了初尝序列模型的强大和普适性,在本节中,我们实现我们的第一个 RNN 来解决您现在熟悉的 MNIST 图像分类任务。在本章的后面,我们将专注于文本序列,并看看神经序列模型如何强大地操纵它们并提取信息以解决 NLU 任务。
但是,你可能会问,图像与序列有什么关系?
正如我们在上一章中看到的,卷积神经网络的架构利用了图像的空间结构。虽然自然图像的结构非常适合 CNN 模型,但从不同角度查看图像的结构是有启发性的。在前沿深度学习研究的趋势中,先进模型尝试利用图像中各种顺序结构,试图以某种方式捕捉创造每个图像的“生成过程”。直观地说,这一切归结为图像中相邻区域在某种程度上相关,并试图对这种结构建模。
在这里,为了介绍基本的 RNN 以及如何处理序列,我们将图像简单地视为序列:我们将数据中的每个图像看作是一系列的行(或列)。在我们的 MNIST 数据中,这意味着每个 28×28 像素的图像可以被视为长度为 28 的序列,序列中的每个元素是一个包含 28 个像素的向量(参见图 5-3)。然后,RNN 中的时间依赖关系可以被想象成一个扫描头,从上到下(行)或从左到右(列)扫描图像。

图 5-3。图像作为像素列的序列。
我们首先加载数据,定义一些参数,并为我们的数据创建占位符:
import tensorflow as tf
# Import MNIST data
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
# Define some parameters
element_size = 28
time_steps = 28
num_classes = 10
batch_size = 128
hidden_layer_size = 128
# Where to save TensorBoard model summaries
LOG_DIR = "logs/RNN_with_summaries"
# Create placeholders for inputs, labels
_inputs = tf.placeholder(tf.float32,shape=[None, time_steps,
element_size],
name='inputs')
y = tf.placeholder(tf.float32, shape=[None, num_classes],
name='labels')
element_size是我们序列中每个向量的维度,在我们的情况下是 28 个像素的行/列。time_steps是序列中这样的元素的数量。
正如我们在之前的章节中看到的,当我们使用内置的 MNIST 数据加载器加载数据时,它以展开的形式呈现,即一个包含 784 个像素的向量。在训练期间加载数据批次时(我们稍后将在本节中介绍),我们只需将每个展开的向量重塑为[batch_size, time_steps, element_size]:
batch_x, batch_y = mnist.train.next_batch(batch_size)
# Reshape data to get 28 sequences of 28 pixels
batch_x = batch_x.reshape((batch_size, time_steps, element_size))
我们将hidden_layer_size设置为128(任意值),控制之前讨论的隐藏 RNN 状态向量的大小。
LOG_DIR是我们保存模型摘要以供 TensorBoard 可视化的目录。随着我们的学习,您将了解这意味着什么。
TensorBoard 可视化
在本章中,我们还将简要介绍 TensorBoard 可视化。TensorBoard 允许您监视和探索模型结构、权重和训练过程,并需要对代码进行一些非常简单的添加。更多细节将在本章和本书后续部分提供。
最后,我们创建了适当维度的输入和标签占位符。
RNN 步骤
让我们实现 RNN 步骤的数学模型。
我们首先创建一个用于记录摘要的函数,稍后我们将在 TensorBoard 中使用它来可视化我们的模型和训练过程(在这个阶段理解其技术细节并不重要):
# This helper function, taken from the official TensorFlow documentation,
# simply adds some ops that take care of logging summaries
def variable_summaries(var):
with tf.name_scope('summaries'):
mean = tf.reduce_mean(var)
tf.summary.scalar('mean', mean)
with tf.name_scope('stddev'):
stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
tf.summary.scalar('stddev', stddev)
tf.summary.scalar('max', tf.reduce_max(var))
tf.summary.scalar('min', tf.reduce_min(var))
tf.summary.histogram('histogram', var)
接下来,我们创建在 RNN 步骤中使用的权重和偏置变量:
# Weights and bias for input and hidden layer
with tf.name_scope('rnn_weights'):
with tf.name_scope("W_x"):
Wx = tf.Variable(tf.zeros([element_size, hidden_layer_size]))
variable_summaries(Wx)
with tf.name_scope("W_h"):
Wh = tf.Variable(tf.zeros([hidden_layer_size, hidden_layer_size]))
variable_summaries(Wh)
with tf.name_scope("Bias"):
b_rnn = tf.Variable(tf.zeros([hidden_layer_size]))
variable_summaries(b_rnn)
使用 tf.scan()应用 RNN 步骤
现在,我们创建一个函数,实现了我们在前一节中看到的基本 RNN 步骤,使用我们创建的变量。现在应该很容易理解这里使用的 TensorFlow 代码:
def rnn_step(previous_hidden_state,x):
current_hidden_state = tf.tanh(
tf.matmul(previous_hidden_state, Wh) +
tf.matmul(x, Wx) + b_rnn)
return current_hidden_state
接下来,我们将这个函数应用到所有的 28 个时间步上:
# Processing inputs to work with scan function
# Current input shape: (batch_size, time_steps, element_size)
processed_input = tf.transpose(_inputs, perm=[1, 0, 2])
# Current input shape now: (time_steps, batch_size, element_size)
initial_hidden = tf.zeros([batch_size,hidden_layer_size])
# Getting all state vectors across time
all_hidden_states = tf.scan(rnn_step,
processed_input,
initializer=initial_hidden,
name='states')
在这个小的代码块中,有一些重要的元素需要理解。首先,我们将输入从[batch_size, time_steps, element_size]重塑为[time_steps, batch_size, element_size]。tf.transpose()的perm参数告诉 TensorFlow 我们想要交换的轴。现在,我们的输入张量中的第一个轴代表时间轴,我们可以通过使用内置的tf.scan()函数在所有时间步上进行迭代,该函数重复地将一个可调用(函数)应用于序列中的元素,如下面的说明所述。
tf.scan()
这个重要的函数被添加到 TensorFlow 中,允许我们在计算图中引入循环,而不仅仅是通过添加更多和更多的操作复制来“展开”循环。更技术上地说,它是一个类似于 reduce 操作符的高阶函数,但它返回随时间的所有中间累加器值。这种方法有几个优点,其中最重要的是能够具有动态数量的迭代而不是固定的,以及用于图构建的计算速度提升和优化。
为了演示这个函数的使用,考虑以下简单示例(这与本节中的整体 RNN 代码是分开的):
import numpy as np
import tensorflow as tf
elems = np.array(["T","e","n","s","o","r", " ", "F","l","o","w"])
scan_sum = tf.scan(lambda a, x: a + x, elems)
sess=tf.InteractiveSession()
sess.run(scan_sum)
让我们看看我们得到了什么:
array([b'T', b'Te', b'Ten', b'Tens', b'Tenso', b'Tensor', b'Tensor ',
b'Tensor F', b'Tensor Fl', b'Tensor Flo', b'Tensor Flow'],
dtype=object)
在这种情况下,我们使用tf.scan()将字符顺序连接到一个字符串中,类似于算术累积和。
顺序输出
正如我们之前看到的,在 RNN 中,我们为每个时间步获得一个状态向量,将其乘以一些权重,然后获得一个输出向量——我们数据的新表示。让我们实现这个:
# Weights for output layers
with tf.name_scope('linear_layer_weights') as scope:
with tf.name_scope("W_linear"):
Wl = tf.Variable(tf.truncated_normal([hidden_layer_size,
num_classes],
mean=0,stddev=.01))
variable_summaries(Wl)
with tf.name_scope("Bias_linear"):
bl = tf.Variable(tf.truncated_normal([num_classes],
mean=0,stddev=.01))
variable_summaries(bl)
# Apply linear layer to state vector
def get_linear_layer(hidden_state):
return tf.matmul(hidden_state, Wl) + bl
with tf.name_scope('linear_layer_weights') as scope:
# Iterate across time, apply linear layer to all RNN outputs
all_outputs = tf.map_fn(get_linear_layer, all_hidden_states)
# Get last output
output = all_outputs[-1]
tf.summary.histogram('outputs', output)
我们的 RNN 的输入是顺序的,输出也是如此。在这个序列分类示例中,我们取最后一个状态向量,并通过一个全连接的线性层将其传递,以提取一个输出向量(稍后将通过 softmax 激活函数传递以生成预测)。这在基本序列分类中是常见的做法,我们假设最后一个状态向量已经“积累”了代表整个序列的信息。
为了实现这一点,我们首先定义线性层的权重和偏置项变量,并为该层创建一个工厂函数。然后我们使用tf.map_fn()将此层应用于所有输出,这与典型的 map 函数几乎相同,该函数以元素方式将函数应用于序列/可迭代对象,本例中是在我们序列的每个元素上。
最后,我们提取批次中每个实例的最后输出,使用负索引(类似于普通 Python)。稍后我们将看到一些更多的方法来做这个,并深入研究输出和状态。
RNN 分类
现在我们准备训练一个分类器,方式与前几章相同。我们定义损失函数计算、优化和预测的操作,为 TensorBoard 添加一些更多摘要,并将所有这些摘要合并为一个操作:
with tf.name_scope('cross_entropy'):
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=y))
tf.summary.scalar('cross_entropy', cross_entropy)
with tf.name_scope('train'):
# Using RMSPropOptimizer
train_step = tf.train.RMSPropOptimizer(0.001, 0.9)\
.minimize(cross_entropy)
with tf.name_scope('accuracy'):
correct_prediction = tf.equal(
tf.argmax(y,1), tf.argmax(output,1))
accuracy = (tf.reduce_mean(
tf.cast(correct_prediction, tf.float32)))*100
tf.summary.scalar('accuracy', accuracy)
# Merge all the summaries
merged = tf.summary.merge_all()
到目前为止,您应该熟悉用于定义损失函数和优化的大多数组件。在这里,我们使用RMSPropOptimizer,实现了一个众所周知且强大的梯度下降算法,带有一些标准的超参数。当然,我们可以使用任何其他优化器(并在本书中一直这样做!)。
我们创建一个包含未见过的 MNIST 图像的小测试集,并添加一些用于记录摘要的技术操作和命令,这些将在 TensorBoard 中使用。
让我们运行模型并查看结果:
# Get a small test set
test_data = mnist.test.images[:batch_size].reshape((-1, time_steps,
element_size))
test_label = mnist.test.labels[:batch_size]
with tf.Session() as sess:
# Write summaries to LOG_DIR -- used by TensorBoard
train_writer = tf.summary.FileWriter(LOG_DIR + '/train',
graph=tf.get_default_graph())
test_writer = tf.summary.FileWriter(LOG_DIR + '/test',
graph=tf.get_default_graph())
sess.run(tf.global_variables_initializer())
for i in range(10000):
batch_x, batch_y = mnist.train.next_batch(batch_size)
# Reshape data to get 28 sequences of 28 pixels
batch_x = batch_x.reshape((batch_size, time_steps,
element_size))
summary,_ = sess.run([merged,train_step],
feed_dict={_inputs:batch_x, y:batch_y})
# Add to summaries
train_writer.add_summary(summary, i)
if i % 1000 == 0:
acc,loss, = sess.run([accuracy,cross_entropy],
feed_dict={_inputs: batch_x,
y: batch_y})
print ("Iter " + str(i) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
if i % 10:
# Calculate accuracy for 128 MNIST test images and
# add to summaries
summary, acc = sess.run([merged, accuracy],
feed_dict={_inputs: test_data,
y: test_label})
test_writer.add_summary(summary, i)
test_acc = sess.run(accuracy, feed_dict={_inputs: test_data,
y: test_label})
print ("Test Accuracy:", test_acc)
最后,我们打印一些训练和测试准确率的结果:
Iter 0, Minibatch Loss= 2.303386, Training Accuracy= 7.03125
Iter 1000, Minibatch Loss= 1.238117, Training Accuracy= 52.34375
Iter 2000, Minibatch Loss= 0.614925, Training Accuracy= 85.15625
Iter 3000, Minibatch Loss= 0.439684, Training Accuracy= 82.81250
Iter 4000, Minibatch Loss= 0.077756, Training Accuracy= 98.43750
Iter 5000, Minibatch Loss= 0.220726, Training Accuracy= 89.84375
Iter 6000, Minibatch Loss= 0.015013, Training Accuracy= 100.00000
Iter 7000, Minibatch Loss= 0.017689, Training Accuracy= 100.00000
Iter 8000, Minibatch Loss= 0.065443, Training Accuracy= 99.21875
Iter 9000, Minibatch Loss= 0.071438, Training Accuracy= 98.43750
Testing Accuracy: 97.6563
总结这一部分,我们从原始的 MNIST 像素开始,并将它们视为顺序数据——每列(或行)的 28 个像素作为一个时间步。然后,我们应用了 vanilla RNN 来提取对应于每个时间步的输出,并使用最后的输出来执行整个序列(图像)的分类。
使用 TensorBoard 可视化模型
TensorBoard 是一个交互式基于浏览器的工具,允许我们可视化学习过程,以及探索我们训练的模型。
运行 TensorBoard,转到命令终端并告诉 TensorBoard 相关摘要的位置:
tensorboard--logdir=*`LOG_DIR`*
在这里,*LOG_DIR*应替换为您的日志目录。如果您在 Windows 上并且这不起作用,请确保您从存储日志数据的相同驱动器运行终端,并按以下方式向日志目录添加名称,以绕过 TensorBoard 解析路径的错误:
tensorboard--logdir=rnn_demo:*`LOG_DIR`*
TensorBoard 允许我们为单独的日志目录分配名称,方法是在名称和路径之间放置一个冒号,当使用多个日志目录时可能会有用。在这种情况下,我们将传递一个逗号分隔的日志目录列表,如下所示:
tensorboard--logdir=rnn_demo1:*`LOG_DIR1`*,rnn_demo2:*`LOG_DIR2`*
在我们的示例中(有一个日志目录),一旦您运行了tensorboard命令,您应该会得到类似以下内容的信息,告诉您在浏览器中导航到哪里:
Starting TensorBoard b'39' on port 6006
(You can navigate to http://10.100.102.4:6006)
如果地址无法使用,请转到localhost:6006,这个地址应该始终有效。
TensorBoard 递归地遍历以*LOG_DIR*为根的目录树,寻找包含 tfevents 日志数据的子目录。如果您多次运行此示例,请确保在每次运行后要么删除您创建的*LOG_DIR*文件夹,要么将日志写入*LOG_DIR*内的单独子目录,例如*LOG_DIR*/run1/train,*LOG_DIR*/run2/train等,以避免覆盖日志文件,这可能会导致一些“奇怪”的图形。
让我们看一些我们可以获得的可视化效果。在下一节中,我们将探讨如何使用 TensorBoard 对高维数据进行交互式可视化-现在,我们专注于绘制训练过程摘要和训练权重。
首先,在浏览器中,转到标量选项卡。在这里,TensorBoard 向我们显示所有标量的摘要,包括通常最有趣的训练和测试准确性,以及我们记录的有关变量的一些摘要统计信息(请参见图 5-4)。将鼠标悬停在图表上,我们可以看到一些数字。
图 5-4。TensorBoard 标量摘要。
在图形选项卡中,我们可以通过放大来获得计算图的交互式可视化,从高级视图到基本操作(请参见图 5-5)。
图 5-5。放大计算图。
最后,在直方图选项卡中,我们可以看到在训练过程中权重的直方图(请参见图 5-6)。当然,我们必须明确将这些直方图添加到我们的日志记录中才能查看它们,使用tf.summary.histogram()。
图 5-6。学习过程中权重的直方图。
TensorFlow 内置的 RNN 函数
前面的示例教会了我们一些使用序列的基本和强大的方法,通过几乎从头开始实现我们的图。在实践中,当然最好使用内置的高级模块和函数。这不仅使代码更短,更容易编写,而且利用了 TensorFlow 实现提供的许多低级优化。
在本节中,我们首先以完整的新代码的新版本呈现。由于大部分整体细节没有改变,我们将重点放在主要的新元素tf.contrib.rnn.BasicRNNCell和tf.nn.dynamic_rnn()上:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
element_size = 28;time_steps = 28;num_classes = 10
batch_size = 128;hidden_layer_size = 128
_inputs = tf.placeholder(tf.float32,shape=[None, time_steps,
element_size],
name='inputs')
y = tf.placeholder(tf.float32, shape=[None, num_classes],name='inputs')
# TensorFlow built-in functions
rnn_cell = tf.contrib.rnn.BasicRNNCell(hidden_layer_size)
outputs, _ = tf.nn.dynamic_rnn(rnn_cell, _inputs, dtype=tf.float32)
Wl = tf.Variable(tf.truncated_normal([hidden_layer_size, num_classes],
mean=0,stddev=.01))
bl = tf.Variable(tf.truncated_normal([num_classes],mean=0,stddev=.01))
def get_linear_layer(vector):
return tf.matmul(vector, Wl) + bl
last_rnn_output = outputs[:,-1,:]
final_output = get_linear_layer(last_rnn_output)
softmax = tf.nn.softmax_cross_entropy_with_logits(logits=final_output,
labels=y)
cross_entropy = tf.reduce_mean(softmax)
train_step = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(final_output,1))
accuracy = (tf.reduce_mean(tf.cast(correct_prediction, tf.float32)))*100
sess=tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
test_data = mnist.test.images[:batch_size].reshape((-1,
time_steps, element_size))
test_label = mnist.test.labels[:batch_size]
for i in range(3001):
batch_x, batch_y = mnist.train.next_batch(batch_size)
batch_x = batch_x.reshape((batch_size, time_steps, element_size))
sess.run(train_step,feed_dict={_inputs:batch_x,
y:batch_y})
if i % 1000 == 0:
acc = sess.run(accuracy, feed_dict={_inputs: batch_x,
y: batch_y})
loss = sess.run(cross_entropy,feed_dict={_inputs:batch_x,
y:batch_y})
print ("Iter " + str(i) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
print ("Testing Accuracy:",
sess.run(accuracy, feed_dict={_inputs: test_data, y: test_label}))
tf.contrib.rnn.BasicRNNCell 和 tf.nn.dynamic_rnn()
TensorFlow 的 RNN 单元是表示每个循环“单元”执行的基本操作(请参见本章开头的图 5-2 进行说明),以及其关联状态的抽象。它们通常是rnn_step()函数及其所需的相关变量的“替代”。当然,有许多变体和类型的单元,每个单元都有许多方法和属性。我们将在本章末尾和本书后面看到一些更高级的单元。
一旦我们创建了rnn_cell,我们将其输入到tf.nn.dynamic_rnn()中。此函数替换了我们基本实现中的tf.scan(),并创建了由rnn_cell指定的 RNN。
截至本文撰写时,即 2017 年初,TensorFlow 包括用于创建 RNN 的静态和动态函数。这是什么意思?静态版本创建一个固定长度的展开图(如图 5-2)。动态版本使用tf.While循环在执行时动态构建图,从而加快图的创建速度,这可能是显著的。这种动态构建在其他方面也非常有用,其中一些我们将在本章末尾讨论变长序列时提及。
请注意,contrib指的是这个库中的代码是由贡献者贡献的,并且仍需要测试。我们将在第七章中更详细地讨论contrib库。BasicRNNCell在 TensorFlow 1.0 中被移动到contrib作为持续开发的一部分。在 1.2 版本中,许多 RNN 函数和类被移回核心命名空间,并在contrib中保留别名以实现向后兼容性,这意味着在撰写本文时,前面的代码适用于所有 1.X 版本。
文本序列的 RNN
我们在本章开始时学习了如何在 TensorFlow 中实现 RNN 模型。为了便于说明,我们展示了如何在由 MNIST 图像中的像素组成的序列上实现和使用 RNN。接下来我们将展示如何在文本序列上使用这些序列模型。
文本数据具有与图像数据明显不同的一些属性,我们将在这里和本书的后面进行讨论。这些属性可能使得一开始处理文本数据有些困难,而文本数据总是需要至少一些基本的预处理步骤才能让我们能够处理它。为了介绍在 TensorFlow 中处理文本,我们将专注于核心组件并创建一个最小的、人为的文本数据集,这将让我们直接开始行动。在第七章中,我们将应用 RNN 模型进行电影评论情感分类。
让我们开始吧,展示我们的示例数据并在进行的过程中讨论文本数据集的一些关键属性。
文本序列
在之前看到的 MNIST RNN 示例中,每个序列的大小是固定的——图像的宽度(或高度)。序列中的每个元素都是一个由 28 个像素组成的密集向量。在 NLP 任务和数据集中,我们有一种不同类型的“图片”。
我们的序列可以是由单词组成的句子,由句子组成的段落,甚至由字符组成的单词或段落组成的整个文档。
考虑以下句子:“我们公司为农场提供智能农业解决方案,具有先进的人工智能、深度学习。”假设我们从在线新闻博客中获取这个句子,并希望将其作为我们机器学习系统的一部分进行处理。
这个句子中的每个单词都将用一个 ID 表示——一个整数,通常在 NLP 中被称为令牌 ID。因此,例如单词“agriculture”可以映射到整数 3452,单词“farm”到 12,“AI”到 150,“deep-learning”到 0。这种以整数标识符表示的表示形式与图像数据中的像素向量在多个方面都非常不同。我们将在讨论词嵌入时很快详细阐述这一重要观点,并在第六章中进行讨论。
为了使事情更具体,让我们从创建我们简化的文本数据开始。
我们的模拟数据由两类非常短的“句子”组成,一类由奇数组成,另一类由偶数组成(数字用英文书写)。我们生成由表示偶数和奇数的单词构建的句子。我们的目标是在监督文本分类任务中学习将每个句子分类为奇数或偶数。
当然,对于这个简单的任务,我们实际上并不需要任何机器学习——我们只是为了说明目的而使用这个人为的例子。
首先,我们定义一些常量,随着我们的进行将会解释:
import numpy as np
import tensorflow as tf
batch_size = 128;embedding_dimension = 64;num_classes = 2
hidden_layer_size = 32;times_steps = 6;element_size = 1
接下来,我们创建句子。我们随机抽取数字并将其映射到相应的“单词”(例如,1 映射到“One”,7 映射到“Seven”等)。
文本序列通常具有可变长度,这当然也适用于所有真实的自然语言数据(例如在本页上出现的句子)。
为了使我们模拟的句子具有不同的长度,我们为每个句子抽取一个介于 3 和 6 之间的随机长度,使用np.random.choice(range(3, 7))——下限包括,上限不包括。
现在,为了将所有输入句子放入一个张量中(每个数据实例的批次),我们需要它们以某种方式具有相同的大小—因此,我们用零(或PAD符号)填充长度短于 6 的句子,使所有句子大小相等(人为地)。这个预处理步骤称为零填充。以下代码完成了所有这些:
digit_to_word_map = {1:"One",2:"Two", 3:"Three", 4:"Four", 5:"Five",
6:"Six",7:"Seven",8:"Eight",9:"Nine"}
digit_to_word_map[0]="PAD"
even_sentences = []
odd_sentences = []
seqlens = []
for i in range(10000):
rand_seq_len = np.random.choice(range(3,7))
seqlens.append(rand_seq_len)
rand_odd_ints = np.random.choice(range(1,10,2),
rand_seq_len)
rand_even_ints = np.random.choice(range(2,10,2),
rand_seq_len)
# Padding
if rand_seq_len<6:
rand_odd_ints = np.append(rand_odd_ints,
[0]*(6-rand_seq_len))
rand_even_ints = np.append(rand_even_ints,
[0]*(6-rand_seq_len))
even_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_odd_ints]))
odd_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_even_ints]))
data = even_sentences+odd_sentences
# Same seq lengths for even, odd sentences
seqlens*=2
让我们看一下我们的句子,每个都填充到长度 6:
even_sentences[0:6]
Out:
['Four Four Two Four Two PAD',
'Eight Six Four PAD PAD PAD',
'Eight Two Six Two PAD PAD',
'Eight Four Four Eight PAD PAD',
'Eight Eight Four PAD PAD PAD',
'Two Two Eight Six Eight Four']
odd_sentences[0:6]
Out:
['One Seven Nine Three One PAD',
'Three Nine One PAD PAD PAD',
'Seven Five Three Three PAD PAD',
'Five Five Three One PAD PAD',
'Three Three Five PAD PAD PAD',
'Nine Three Nine Five Five Three']
请注意,我们向我们的数据和digit_to_word_map字典中添加了PAD单词(标记),并分别存储偶数和奇数句子及其原始长度(填充之前)。
让我们看一下我们打印的句子的原始序列长度:
seqlens[0:6]
Out:
[5, 3, 4, 4, 3, 6]
为什么保留原始句子长度?通过零填充,我们解决了一个技术问题,但又创建了另一个问题:如果我们简单地将这些填充的句子通过我们的 RNN 模型,它将处理无用的PAD符号。这将通过处理“噪音”损害模型的正确性,并增加计算时间。我们通过首先将原始长度存储在seqlens数组中,然后告诉 TensorFlow 的tf.nn.dynamic_rnn()每个句子的结束位置来解决这个问题。
在本章中,我们的数据是模拟的——由我们生成。在实际应用中,我们将首先获得一系列文档(例如,一句话的推文),然后将每个单词映射到一个整数 ID。
因此,我们现在将单词映射到索引—单词标识符—通过简单地创建一个以单词为键、索引为值的字典。我们还创建了反向映射。请注意,单词 ID 和每个单词代表的数字之间没有对应关系—ID 没有语义含义,就像在任何具有真实数据的 NLP 应用中一样:
# Map from words to indices
word2index_map ={}
index=0
for sent in data:
for word in sent.lower().split():
if word not in word2index_map:
word2index_map[word] = index
index+=1
# Inverse map
index2word_map = {index: word for word, index in word2index_map.items()}
vocabulary_size = len(index2word_map)
这是一个监督分类任务—我们需要一个以 one-hot 格式的标签数组,训练和测试集,一个生成实例批次的函数和占位符,和通常一样。
首先,我们创建标签并将数据分为训练集和测试集:
labels = [1]*10000 + [0]*10000
for i in range(len(labels)):
label = labels[i]
one_hot_encoding = [0]*2
one_hot_encoding[label] = 1
labels[i] = one_hot_encoding
data_indices = list(range(len(data)))
np.random.shuffle(data_indices)
data = np.array(data)[data_indices]
labels = np.array(labels)[data_indices]
seqlens = np.array(seqlens)[data_indices]
train_x = data[:10000]
train_y = labels[:10000]
train_seqlens = seqlens[:10000]
test_x = data[10000:]
test_y = labels[10000:]
test_seqlens = seqlens[10000:]
接下来,我们创建一个生成句子批次的函数。每个批次中的句子只是一个对应于单词的整数 ID 列表:
def get_sentence_batch(batch_size,data_x,
data_y,data_seqlens):
instance_indices = list(range(len(data_x)))
np.random.shuffle(instance_indices)
batch = instance_indices[:batch_size]
x = [[word2index_map[word] for word in data_x[i].lower().split()]
for i in batch]
y = [data_y[i] for i in batch]
seqlens = [data_seqlens[i] for i in batch]
return x,y,seqlens
最后,我们为数据创建占位符:
_inputs = tf.placeholder(tf.int32, shape=[batch_size,times_steps])
_labels = tf.placeholder(tf.float32, shape=[batch_size, num_classes])
# seqlens for dynamic calculation
_seqlens = tf.placeholder(tf.int32, shape=[batch_size])
请注意,我们已经为原始序列长度创建了占位符。我们将很快看到如何在我们的 RNN 中使用这些。
监督词嵌入
我们的文本数据现在被编码为单词 ID 列表—每个句子是一个对应于单词的整数序列。这种原子表示,其中每个单词用一个 ID 表示,对于训练具有大词汇量的深度学习模型来说是不可扩展的,这在实际问题中经常出现。我们可能会得到数百万这样的单词 ID,每个以 one-hot(二进制)分类形式编码,导致数据稀疏和计算问题。我们将在第六章中更深入地讨论这个问题。
解决这个问题的一个强大方法是使用词嵌入。嵌入本质上只是将编码单词的高维度 one-hot 向量映射到较低维度稠密向量。因此,例如,如果我们的词汇量大小为 100,000,那么每个单词在 one-hot 表示中的大小将相同。相应的单词向量或词嵌入大小为 300。因此,高维度的 one-hot 向量被“嵌入”到具有更低维度的连续向量空间中。
在第六章中,我们深入探讨了词嵌入,探索了一种流行的无监督训练方法,即 word2vec。
在这里,我们的最终目标是解决文本分类问题,并且我们将在监督框架中训练词向量,调整嵌入的词向量以解决下游分类任务。
将单词嵌入视为基本的哈希表或查找表是有帮助的,将单词映射到它们的密集向量值。这些向量是作为训练过程的一部分进行优化的。以前,我们给每个单词一个整数索引,然后句子表示为这些索引的序列。现在,为了获得一个单词的向量,我们使用内置的tf.nn.embedding_lookup()函数,它有效地检索给定单词索引序列中每个单词的向量:
with tf.name_scope("embeddings"):
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size,
embedding_dimension],
-1.0, 1.0),name='embedding')
embed = tf.nn.embedding_lookup(embeddings, _inputs)
我们很快将看到单词向量表示的示例和可视化。
LSTM 和使用序列长度
在我们开始的介绍性 RNN 示例中,我们实现并使用了基本的 vanilla RNN 模型。在实践中,我们经常使用略微更高级的 RNN 模型,其主要区别在于它们如何更新其隐藏状态并通过时间传播信息。一个非常流行的循环网络是长短期记忆(LSTM)网络。它通过具有一些特殊的记忆机制与 vanilla RNN 不同,这些机制使得循环单元能够更好地存储信息长时间,从而使它们能够比普通 RNN 更好地捕获长期依赖关系。
这些记忆机制并没有什么神秘之处;它们只是由一些添加到每个循环单元的更多参数组成,使得 RNN 能够克服优化问题并传播信息。这些可训练参数充当过滤器,选择哪些信息值得“记住”和传递,哪些值得“遗忘”。它们的训练方式与网络中的任何其他参数完全相同,使用梯度下降算法和反向传播。我们在这里不深入讨论更多技术数学公式,但有很多很好的资源深入探讨细节。
我们使用tf.contrib.rnn.BasicLSTMCell()创建一个 LSTM 单元,并将其提供给tf.nn.dynamic_rnn(),就像我们在本章开始时所做的那样。我们还使用我们之前创建的_seqlens占位符给dynamic_rnn()提供每个示例批次中每个序列的长度。TensorFlow 使用这个长度来停止超出最后一个真实序列元素的所有 RNN 步骤。它还返回所有随时间的输出向量(在outputs张量中),这些向量在真实序列的真实结尾之后都是零填充的。因此,例如,如果我们原始序列的长度为 5,并且我们将其零填充为长度为 15 的序列,则超过 5 的所有时间步的输出将为零:
with tf.variable_scope("lstm"):
lstm_cell = tf.contrib.rnn.BasicLSTMCell(hidden_layer_size,
forget_bias=1.0)
outputs, states = tf.nn.dynamic_rnn(lstm_cell, embed,
sequence_length = _seqlens,
dtype=tf.float32)
weights = {
'linear_layer': tf.Variable(tf.truncated_normal([hidden_layer_size,
num_classes],
mean=0,stddev=.01))
}
biases = {
'linear_layer':tf.Variable(tf.truncated_normal([num_classes],
mean=0,stddev=.01))
}
# Extract the last relevant output and use in a linear layer
final_output = tf.matmul(states[1],
weights["linear_layer"]) + biases["linear_layer"]
softmax = tf.nn.softmax_cross_entropy_with_logits(logits = final_output,
labels = _labels)
cross_entropy = tf.reduce_mean(softmax)
我们取最后一个有效的输出向量——在这种情况下,方便地在dynamic_rnn()返回的states张量中可用,并通过一个线性层(和 softmax 函数)传递它,将其用作我们的最终预测。在下一节中,当我们查看dynamic_rnn()为我们的示例句子生成的一些输出时,我们将进一步探讨最后相关输出和零填充的概念。
训练嵌入和 LSTM 分类器
我们已经有了拼图的所有部分。让我们把它们放在一起,完成端到端的单词向量和分类模型的训练:
train_step = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(_labels,1),
tf.argmax(final_output,1))
accuracy = (tf.reduce_mean(tf.cast(correct_prediction,
tf.float32)))*100
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(1000):
x_batch, y_batch,seqlen_batch = get_sentence_batch(batch_size,
train_x,train_y,
train_seqlens)
sess.run(train_step,feed_dict={_inputs:x_batch, _labels:y_batch,
_seqlens:seqlen_batch})
if step % 100 == 0:
acc = sess.run(accuracy,feed_dict={_inputs:x_batch,
_labels:y_batch,
_seqlens:seqlen_batch})
print("Accuracy at %d: %.5f" % (step, acc))
for test_batch in range(5):
x_test, y_test,seqlen_test = get_sentence_batch(batch_size,
test_x,test_y,
test_seqlens)
batch_pred,batch_acc = sess.run([tf.argmax(final_output,1),
accuracy],
feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
print("Test batch accuracy %d: %.5f" % (test_batch, batch_acc))
output_example = sess.run([outputs],feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
states_example = sess.run([states[1]],feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
正如我们所看到的,这是一个非常简单的玩具文本分类问题:
Accuracy at 0: 32.81250
Accuracy at 100: 100.00000
Accuracy at 200: 100.00000
Accuracy at 300: 100.00000
Accuracy at 400: 100.00000
Accuracy at 500: 100.00000
Accuracy at 600: 100.00000
Accuracy at 700: 100.00000
Accuracy at 800: 100.00000
Accuracy at 900: 100.00000
Test batch accuracy 0: 100.00000
Test batch accuracy 1: 100.00000
Test batch accuracy 2: 100.00000
Test batch accuracy 3: 100.00000
Test batch accuracy 4: 100.00000
我们还计算了由dynamic_rnn()生成的一个示例批次的输出,以进一步说明在前一节中讨论的零填充和最后相关输出的概念。
让我们看一个这些输出的例子,对于一个被零填充的句子(在您的随机数据批次中,您可能会看到不同的输出,当然要寻找一个seqlen小于最大 6 的句子):
seqlen_test[1]
Out:
4
output_example[0][1].shape
Out:
(6, 32)
这个输出有如预期的六个时间步,每个大小为 32 的向量。让我们看一眼它的值(只打印前几个维度以避免混乱):
output_example[0][1][:6,0:3]
Out:
array([[-0.44493711, -0.51363373, -0.49310589],
[-0.72036862, -0.68590945, -0.73340571],
[-0.83176643, -0.78206956, -0.87831545],
[-0.87982416, -0.82784462, -0.91132098],
[ 0. , 0. , 0. ],
[ 0. , 0. , 0. ]], dtype=float32)
我们看到,对于这个句子,原始长度为 4,最后两个时间步由于填充而具有零向量。
最后,我们看一下dynamic_rnn()返回的状态向量:
states_example[0][1][0:3]
Out:
array([-0.87982416, -0.82784462, -0.91132098], dtype=float32)
我们可以看到它方便地为我们存储了最后一个相关输出向量——其值与零填充之前的最后一个相关输出向量匹配。
此时,您可能想知道如何访问和操作单词向量,并探索训练后的表示。我们将展示如何做到这一点,包括交互式嵌入可视化,在下一章中。
堆叠多个 LSTMs
之前,我们专注于一个单层 LSTM 网络以便更容易解释。添加更多层很简单,使用MultiRNNCell()包装器将多个 RNN 单元组合成一个多层单元。
举个例子,假设我们想在前面的例子中堆叠两个 LSTM 层。我们可以这样做:
num_LSTM_layers = 2
with tf.variable_scope("lstm"):
lstm_cell_list =
[tf.contrib.rnn.BasicLSTMCell(hidden_layer_size,forget_bias=1.0)
for ii in range(num_LSTM_layers)]
cell = tf.contrib.rnn.MultiRNNCell(cells=lstm_cell_list,
state_is_tuple=True)
outputs, states = tf.nn.dynamic_rnn(cell, embed,
sequence_length = _seqlens,
dtype=tf.float32)
我们首先像以前一样定义一个 LSTM 单元,然后将其馈送到tf.contrib.rnn.MultiRNNCell()包装器中。
现在我们的网络有两层 LSTM,当尝试提取最终状态向量时会出现一些形状问题。为了获得第二层的最终状态,我们只需稍微调整我们的索引:
# Extract the final state and use in a linear layer
final_output = tf.matmul(states[num_LSTM_layers-1][1],
weights["linear_layer"]) + biases["linear_layer"]
总结
在这一章中,我们介绍了在 TensorFlow 中的序列模型。我们看到如何通过使用tf.scan()和内置模块来实现基本的 RNN 模型,以及更高级的 LSTM 网络,用于文本和图像数据。最后,我们训练了一个端到端的文本分类 RNN 模型,使用了词嵌入,并展示了如何处理可变长度的序列。在下一章中,我们将深入探讨词嵌入和 word2vec。在第七章中,我们将看到一些很酷的 TensorFlow 抽象层,以及它们如何用于训练高级文本分类 RNN 模型,而且付出的努力要少得多。
第六章:文本 II:词向量、高级 RNN 和嵌入可视化
在本章中,我们深入探讨了在第五章中讨论的与文本序列处理相关的重要主题。我们首先展示了如何使用一种称为word2vec的无监督方法训练词向量,以及如何使用 TensorBoard 交互地可视化嵌入。然后我们在监督文本分类任务中使用预训练的词向量,在大量公共数据上进行训练,并介绍了在最先进系统中经常使用的更高级的 RNN 组件。
词嵌入简介
在第五章中,我们介绍了 RNN 模型和在 TensorFlow 中处理文本序列。作为监督模型训练的一部分,我们还训练了词向量,将单词 ID 映射到低维连续向量。这样做的原因是为了实现可扩展的表示,可以输入到 RNN 层中。但是使用词向量还有更深层次的原因,我们接下来会讨论。
考虑出现在图 6-1 中的句子:“我们公司为农场提供智能农业解决方案,具有先进的人工智能、深度学习。”这句话可能来自于一条推广公司的推文。作为数据科学家或工程师,我们现在可能希望将其作为先进机器智能系统的一部分进行处理,该系统可以筛选推文并自动检测信息内容(例如公众情绪)。
在传统的自然语言处理(NLP)方法中,每个单词都会用 N 个 ID 来表示,比如一个整数。因此,正如我们在上一章中提出的,单词“agriculture”可能被映射到整数 3452,单词“farm”到 12,“AI”到 150,“deep-learning”到 0。
虽然这种表示在一些基本的 NLP 任务中取得了出色的结果,并且在许多情况下仍然经常使用(例如在基于词袋的文本分类中),但它存在一些主要的固有问题。首先,通过使用这种原子表示,我们丢失了单词内部编码的所有含义,关键的是,我们因此丢失了单词之间语义接近的信息。在我们的例子中,我们当然知道“agriculture”和“farm”之间有很强的关联,“AI”和“deep-learning”也是如此,而深度学习和农场通常没有太多关联。这并没有反映在它们的任意整数 ID 中。
这种查看数据的方式的另一个重要问题源于典型词汇量的规模,很容易达到庞大的数字。这意味着天真地说,我们可能需要保留数百万个这样的单词标识符,导致数据稀疏性增加,进而使学习变得更加困难和昂贵。
对于图像,比如我们在第五章第一节中使用的 MNIST 数据,情况并非如此。虽然图像可以是高维的,但它们在像素值方面的自然表示已经编码了一些语义含义,并且这种表示是密集的。实际上,像我们在第五章中看到的 RNN 模型需要密集的向量表示才能很好地工作。
因此,我们希望使用携带语义含义的单词的密集向量表示。但是我们如何获得它们呢?
在第五章中,我们训练了监督词向量来解决特定任务,使用了标记数据。但是对于个人和组织来说,获取标记数据往往是昂贵的,需要耗费大量资源、时间和精力来手动标记文本或以某种方式获取足够的标记实例。然而,获取大量未标记数据往往是一个不那么艰巨的任务。因此,我们希望找到一种方法来利用这些数据以无监督的方式训练词表示。
实际上有许多方法可以进行无监督训练词嵌入,包括传统的 NLP 方法和许多使用神经网络的新方法,这些方法无论新旧,都在核心上依赖于分布假设,这最容易通过语言学家约翰·弗斯的一句著名引言来解释:“你可以通过它的伙伴来认识一个词。” 换句话说,倾向于出现在相似上下文中的词往往具有相似的语义含义。
在本书中,我们专注于基于神经网络的强大词嵌入方法。在第五章中,我们看到了如何将它们作为下游文本分类任务的一部分进行训练。我们现在展示如何以无监督的方式训练词向量,然后如何使用在网络上大量文本训练的预训练向量。
Word2vec
Word2vec 是一个非常著名的无监督词嵌入方法。实际上,它更像是一系列算法,所有这些算法都以某种方式利用单词出现的上下文来学习它们的表示(符合分布假设的精神)。我们专注于最流行的 word2vec 实现,它训练一个模型,给定一个输入词,通过使用所谓的跳字来预测单词的上下文。这实际上相当简单,下面的例子将会演示。
再次考虑我们的例句:“Our company provides smart agriculture solutions for farms, with advanced AI, deep-learning.” 我们(为了简单起见)将一个词的上下文定义为它的直接邻居(“它的伙伴”)——即,它左边的词和右边的词。因此,“company”的上下文是[our, provides],“AI”的上下文是[advanced, deep-learning],等等(参见图 6-1)。

图 6-1。从文本生成跳字。
在跳字 word2vec 模型中,我们训练一个模型来根据输入词预测上下文。在这种情况下,这意味着我们生成训练实例和标签对,如(our,company),(provides,company),(advanced,AI),(deep-learning,AI),等等。
除了从数据中提取的这些对,我们还抽取“假”对——也就是,对于给定的输入词(如“AI”),我们还会抽取随机噪声词作为上下文(如“monkeys”),这个过程被称为负采样。我们使用真实对和噪声对结合起来构建我们的训练实例和标签,用于训练一个学习区分它们之间差异的二元分类器。这个分类器中的可训练参数是向量表示——词嵌入。我们调整这些向量以产生一个能够在二元分类设置中区分一个词的真实上下文和随机抽样的上下文的分类器。
TensorFlow 提供了许多实现 word2vec 模型的方式,随着复杂性和优化水平的增加,使用多线程和更高级的抽象来优化和缩短代码。我们在这里介绍了一种基本方法,它将向您介绍核心思想和操作。
让我们直接开始在 TensorFlow 代码中实现核心思想。
跳字
我们首先准备我们的数据并提取跳字。就像在第五章中一样,我们的数据包括两类非常短的“句子”,一类由奇数组成,另一类由偶数组成(用英文写的数字)。为了简单起见,我们在这里让句子大小相等,但这对于 word2vec 训练并不重要。让我们开始设置一些参数并创建句子:
import os
import math
import numpy as np
import tensorflow as tf
from tensorflow.contrib.tensorboard.plugins import projector
batch_size=64
embedding_dimension = 5
negative_samples =8
LOG_DIR = "logs/word2vec_intro"
digit_to_word_map = {1:"One",2:"Two", 3:"Three", 4:"Four", 5:"Five",
6:"Six",7:"Seven",8:"Eight",9:"Nine"}
sentences = []
# Create two kinds of sentences - sequences of odd and even digits
for i in range(10000):
rand_odd_ints = np.random.choice(range(1,10,2),3)
sentences.append(" ".join([digit_to_word_map[r] for r in rand_odd_ints]))
rand_even_ints = np.random.choice(range(2,10,2),3)
sentences.append(" ".join([digit_to_word_map[r] for r in rand_even_ints]))
让我们来看看我们的句子:
sentences[0:10]
Out:
['Seven One Five',
'Four Four Four',
'Five One Nine',
'Eight Two Eight',
'One Nine Three',
'Two Six Eight',
'Nine Seven Seven',
'Six Eight Six',
'One Five Five',
'Four Six Two']
接下来,就像在第五章中一样,我们通过创建一个以单词为键、索引为值的字典,将单词映射到索引,并创建反向映射:
# Map words to indices
word2index_map ={}
index=0
for sent in sentences:
for word in sent.lower().split():
if word not in word2index_map:
word2index_map[word] = index
index+=1
index2word_map = {index: word for word, index in word2index_map.items()}
vocabulary_size = len(index2word_map)
为了准备 word2vec 的数据,让我们创建跳字模型:
# Generate skip-gram pairs
skip_gram_pairs = []
for sent in sentences:
tokenized_sent = sent.lower().split()
for i in range(1, len(tokenized_sent)-1) :
word_context_pair = [[word2index_map[tokenized_sent[i-1]],
word2index_map[tokenized_sent[i+1]]],
word2index_map[tokenized_sent[i]]]
skip_gram_pairs.append([word_context_pair[1],
word_context_pair[0][0]])
skip_gram_pairs.append([word_context_pair[1],
word_context_pair[0][1]])
def get_skipgram_batch(batch_size):
instance_indices = list(range(len(skip_gram_pairs)))
np.random.shuffle(instance_indices)
batch = instance_indices[:batch_size]
x = [skip_gram_pairs[i][0] for i in batch]
y = [[skip_gram_pairs[i][1]] for i in batch]
return x,y
每个 skip-gram 对由目标和上下文单词索引组成(由word2index_map字典给出,并不对应于每个单词表示的实际数字)。让我们来看一下:
skip_gram_pairs[0:10]
Out:
[[1, 0],
[1, 2],
[3, 3],
[3, 3],
[1, 2],
[1, 4],
[6, 5],
[6, 5],
[4, 1],
[4, 7]]
我们可以生成单词索引序列的批次,并使用我们之前创建的逆字典查看原始句子:
# Batch example
x_batch,y_batch = get_skipgram_batch(8)
x_batch
y_batch
[index2word_map[word] for word in x_batch]
[index2word_map[word[0]] for word in y_batch]
x_batch
Out:
[6, 2, 1, 1, 3, 0, 7, 2]
y_batch
Out:
[[5], [0], [4], [0], [5], [4], [1], [7]]
[index2word_map[word] for word in x_batch]
Out:
['two', 'five', 'one', 'one', 'four', 'seven', 'three', 'five']
[index2word_map[word[0]] for word in y_batch]
Out:
['eight', 'seven', 'nine', 'seven', 'eight',
'nine', 'one', 'three']
最后,我们创建我们的输入和标签占位符:
# Input data, labels
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
TensorFlow 中的嵌入
在第五章中,我们在监督 RNN 的一部分中使用了内置的tf.nn.embedding_lookup()函数。这里也使用了相同的功能。在这里,单词嵌入可以被视为查找表,将单词映射到向量值,这些向量值在训练过程中被优化以最小化损失函数。正如我们将在下一节中看到的,与第五章不同的是,这里我们使用了一个考虑到任务非监督性质的损失函数,但是嵌入查找仍然是相同的,它有效地检索给定单词索引序列中每个单词的向量:
with tf.name_scope("embeddings"):
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_dimension],
-1.0, 1.0),name='embedding')
# This is essentially a lookup table
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
噪声对比估计(NCE)损失函数
在我们介绍 skip-grams 时,我们提到我们创建了两种类型的上下文-目标单词对:在文本中出现的真实单词和通过插入随机上下文单词生成的“虚假”嘈杂对。我们的目标是学会区分这两者,帮助我们学习一个良好的单词表示。我们可以自己绘制随机嘈杂的上下文对,但幸运的是,TensorFlow 带有一个专门为我们的任务设计的有用的损失函数。当我们评估损失时(在会话中运行),tf.nn.nce_loss()会自动绘制负(“噪声”)样本:
# Create variables for the NCE loss
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_dimension],
stddev=1.0 / math.sqrt(embedding_dimension)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
loss = tf.reduce_mean(
tf.nn.nce_loss(weights = nce_weights, biases = nce_biases, inputs = embed,
labels = train_labels, num_sampled = negative_samples, num_classes =
vocabulary_size))
我们不会深入讨论这个损失函数的数学细节,但可以将其视为一种对用于分类任务中的普通 softmax 函数的有效近似,正如在之前的章节中介绍的那样。我们调整我们的嵌入向量以优化这个损失函数。有关更多详细信息,请参阅官方 TensorFlow文档和其中的参考资料。
我们现在准备好进行训练了。除了在 TensorFlow 中获取我们的单词嵌入之外,我们接下来介绍两个有用的功能:调整优化学习率和交互式可视化嵌入。
学习率衰减
正如在之前的章节中讨论的那样,梯度下降优化通过朝着最小化损失函数的方向进行小步调整权重。learning_rate超参数控制这些步骤的侵略性。在模型的梯度下降训练过程中,通常会逐渐减小这些步骤的大小,以便让我们的优化过程在接近参数空间中的良好点时“安定下来”。这个小改动实际上经常会显著提升性能,并且是一个一般性的良好实践。
tf.train.exponential_decay()对学习率应用指数衰减,衰减的确切形式由一些超参数控制,如下面的代码所示(有关详细信息,请参阅官方 TensorFlow 文档bit.ly/2tluxP1)。在这里,仅作为示例,我们每 1,000 步衰减一次,衰减的学习率遵循阶梯函数——一种类似于楼梯的分段常数函数,正如其名称所暗示的那样:
# Learning rate decay
global_step = tf.Variable(0, trainable=False)
learningRate = tf.train.exponential_decay(learning_rate=0.1,
global_step= global_step,
decay_steps=1000,
decay_rate= 0.95,
staircase=True)
train_step = tf.train.GradientDescentOptimizer(learningRate).minimize(loss)
使用 TensorBoard 进行训练和可视化
我们像往常一样在会话中训练我们的图,添加了一些代码行,使 TensorBoard 中的交互式可视化更加酷炫,这是一种用于可视化高维数据(通常是图像或单词向量)的新工具,于 2016 年底引入 TensorFlow。
首先,我们创建一个 TSV(制表符分隔值)元数据文件。该文件将嵌入向量与我们可能拥有的相关标签或图像连接起来。在我们的情况下,每个嵌入向量都有一个标签,就是它代表的单词。
然后,我们将 TensorBoard 指向我们的嵌入变量(在这种情况下,只有一个),并将它们链接到元数据文件。
最后,在完成优化但在关闭会话之前,我们将词嵌入向量归一化为单位长度,这是一个标准的后处理步骤:
# Merge all summary ops
merged = tf.summary.merge_all()
with tf.Session() as sess:
train_writer = tf.summary.FileWriter(LOG_DIR,
graph=tf.get_default_graph())
saver = tf.train.Saver()
with open(os.path.join(LOG_DIR,'metadata.tsv'), "w") as metadata:
metadata.write('Name\tClass\n')
for k,v in index2word_map.items():
metadata.write('%s\t%d\n' % (v, k))
config = projector.ProjectorConfig()
embedding = config.embeddings.add()
embedding.tensor_name = embeddings.name
# Link embedding to its metadata file
embedding.metadata_path = os.path.join(LOG_DIR,'metadata.tsv')
projector.visualize_embeddings(train_writer, config)
tf.global_variables_initializer().run()
for step in range(1000):
x_batch, y_batch = get_skipgram_batch(batch_size)
summary,_ = sess.run([merged,train_step],
feed_dict={train_inputs:x_batch,
train_labels:y_batch})
train_writer.add_summary(summary, step)
if step % 100 == 0:
saver.save(sess, os.path.join(LOG_DIR, "w2v_model.ckpt"), step)
loss_value = sess.run(loss,
feed_dict={train_inputs:x_batch,
train_labels:y_batch})
print("Loss at %d: %.5f" % (step, loss_value))
# Normalize embeddings before using
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
normalized_embeddings_matrix = sess.run(normalized_embeddings)
检查我们的嵌入
让我们快速看一下我们得到的词向量。我们选择一个单词(one)并按照它们与其接近程度的顺序对所有其他词向量进行排序,降序排列:
ref_word = normalized_embeddings_matrix[word2index_map["one"]]
cosine_dists = np.dot(normalized_embeddings_matrix,ref_word)
ff = np.argsort(cosine_dists)[::-1][1:10]
for f in ff:
print(index2word_map[f])
print(cosine_dists[f])
现在让我们看看与one向量的词距离:
Out:
seven
0.946973
three
0.938362
nine
0.755187
five
0.701269
eight
-0.0702622
two
-0.101749
six
-0.120306
four
-0.159601
我们看到,代表奇数的词向量与one相似(在点积方面),而代表偶数的词向量与之不相似(并且与one向量的点积为负)。我们学习了嵌入向量,使我们能够区分偶数和奇数——它们各自的向量相距甚远,因此捕捉了每个单词(奇数或偶数数字)出现的上下文。
现在,在 TensorBoard 中,转到嵌入选项卡。这是一个三维交互式可视化面板,我们可以在嵌入向量空间中移动并探索不同的“角度”,放大等(参见图 6-2 和 6-3)。这使我们能够以视觉舒适的方式理解我们的数据并解释模型。我们可以看到,奇数和偶数在特征空间中占据不同的区域。
图 6-2. 词嵌入的交互式可视化。
图 6-3. 我们可以从不同角度探索我们的词向量(在具有大词汇量的高维问题中特别有用)。
当我们有大量嵌入向量时,例如在具有更大词汇量的实际文本分类任务中,这种类型的可视化效果非常出色,例如在第七章中,或在嵌入投影仪TensorFlow 演示中。在这里,我们只是让您尝试如何交互地探索您的数据和深度学习模型。
预训练嵌入,高级 RNN
正如我们之前讨论的,词嵌入是深度学习模型中文本的强大组件。在许多应用中看到的一种流行方法是首先使用诸如 word2vec 之类的方法在大量(未标记的)文本上训练词向量,然后在监督文档分类等下游任务中使用这些向量。
在前一节中,我们从头开始训练了无监督的词向量。这种方法通常需要非常大的语料库,例如维基百科条目或网页。在实践中,我们经常使用预训练的词嵌入,这些词嵌入在如前几章中介绍的预训练模型中以类似的方式训练,并且可在线获取。
在本节中,我们展示了如何在 TensorFlow 中使用预训练的词嵌入进行简化的文本分类任务。为了使事情更有趣,我们还借此机会介绍了一些在现代深度学习应用中经常使用的更有用和更强大的组件,用于自然语言理解:双向 RNN 层和门控循环单元(GRU)单元。
我们将扩展和调整我们从第五章中的文本分类示例,只关注已更改的部分。
预训练词嵌入
在这里,我们展示了如何将基于网络数据训练的词向量并入(虚构的)文本分类任务中。嵌入方法被称为GloVe,虽然我们在这里不深入讨论细节,但总体思想与 word2vec 类似——通过单词出现的上下文学习单词的表示。关于该方法及其作者以及预训练向量的信息可在项目的网站上找到。
我们下载了 Common Crawl 向量(840B 个标记),然后进行我们的示例。
首先设置下载的单词向量的路径和一些其他参数,就像第五章中所示的那样:
importzipfileimportnumpyasnpimporttensorflowastfpath_to_glove="*`path/to/glove/file`*"PRE_TRAINED=TrueGLOVE_SIZE=300batch_size=128embedding_dimension=64num_classes=2hidden_layer_size=32times_steps=6
然后我们创建人为的、简单的模拟数据,也如同第五章中所示(详细信息请参见那里):
digit_to_word_map = {1:"One",2:"Two", 3:"Three", 4:"Four", 5:"Five",
6:"Six",7:"Seven",8:"Eight",9:"Nine"}
digit_to_word_map[0]="PAD_TOKEN"
even_sentences = []
odd_sentences = []
seqlens = []
for i in range(10000):
rand_seq_len = np.random.choice(range(3,7))
seqlens.append(rand_seq_len)
rand_odd_ints = np.random.choice(range(1,10,2),
rand_seq_len)
rand_even_ints = np.random.choice(range(2,10,2),
rand_seq_len)
if rand_seq_len<6:
rand_odd_ints = np.append(rand_odd_ints,
[0]*(6-rand_seq_len))
rand_even_ints = np.append(rand_even_ints,
[0]*(6-rand_seq_len))
even_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_odd_ints]))
odd_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_even_ints]))
data = even_sentences+odd_sentences
# Same seq lengths for even, odd sentences
seqlens*=2
labels = [1]*10000 + [0]*10000
for i in range(len(labels)):
label = labels[i]
one_hot_encoding = [0]*2
one_hot_encoding[label] = 1
labels[i] = one_hot_encoding
接下来,我们创建单词索引映射:
word2index_map ={}
index=0
for sent in data:
for word in sent.split():
if word not in word2index_map:
word2index_map[word] = index
index+=1
index2word_map = {index: word for word, index in word2index_map.items()}
vocabulary_size = len(index2word_map)
让我们回顾一下它的内容——只是一个从单词到(任意)索引的映射:
word2index_map
Out:
{'Eight': 7,
'Five': 1,
'Four': 6,
'Nine': 3,
'One': 5,
'PAD_TOKEN': 2,
'Seven': 4,
'Six': 9,
'Three': 0,
'Two': 8}
现在,我们准备获取单词向量。我们下载的预训练 GloVe 嵌入中有 220 万个单词,而在我们的玩具示例中只有 9 个。因此,我们只取出出现在我们自己微小词汇表中的单词的 GloVe 向量:
def get_glove(path_to_glove,word2index_map):
embedding_weights = {}
count_all_words = 0
with zipfile.ZipFile(path_to_glove) as z:
with z.open("glove.840B.300d.txt") as f:
for line in f:
vals = line.split()
word = str(vals[0].decode("utf-8"))
if word in word2index_map:
print(word)
count_all_words+=1
coefs = np.asarray(vals[1:], dtype='float32')
coefs/=np.linalg.norm(coefs)
embedding_weights[word] = coefs
if count_all_words==vocabulary_size -1:
break
return embedding_weights
word2embedding_dict = get_glove(path_to_glove,word2index_map)
我们逐行查看 GloVe 文件,获取我们需要的单词向量,并对它们进行归一化。一旦我们提取出需要的九个单词,我们就停止这个过程并退出循环。我们函数的输出是一个字典,将每个单词映射到它的向量。
下一步是将这些向量放入矩阵中,这是 TensorFlow 所需的格式。在这个矩阵中,每行索引应该对应单词索引:
embedding_matrix = np.zeros((vocabulary_size ,GLOVE_SIZE))
for word,index in word2index_map.items():
if not word == "PAD_TOKEN":
word_embedding = word2embedding_dict[word]
embedding_matrix[index,:] = word_embedding
请注意,对于PAD_TOKEN单词,我们将相应的向量设置为 0。正如我们在第五章中看到的,我们在调用dynamic_rnn()时会忽略填充的标记,告诉它原始序列长度。
现在我们创建我们的训练和测试数据:
data_indices = list(range(len(data)))
np.random.shuffle(data_indices)
data = np.array(data)[data_indices]
labels = np.array(labels)[data_indices]
seqlens = np.array(seqlens)[data_indices]
train_x = data[:10000]
train_y = labels[:10000]
train_seqlens = seqlens[:10000]
test_x = data[10000:]
test_y = labels[10000:]
test_seqlens = seqlens[10000:]
def get_sentence_batch(batch_size,data_x,
data_y,data_seqlens):
instance_indices = list(range(len(data_x)))
np.random.shuffle(instance_indices)
batch = instance_indices[:batch_size]
x = [[word2index_map[word] for word in data_x[i].split()]
for i in batch]
y = [data_y[i] for i in batch]
seqlens = [data_seqlens[i] for i in batch]
return x,y,seqlens
然后我们创建我们的输入占位符:
_inputs = tf.placeholder(tf.int32, shape=[batch_size,times_steps])
embedding_placeholder = tf.placeholder(tf.float32, [vocabulary_size,
GLOVE_SIZE])
_labels = tf.placeholder(tf.float32, shape=[batch_size, num_classes])
_seqlens = tf.placeholder(tf.int32, shape=[batch_size])
请注意,我们创建了一个embedding_placeholder,我们向其中提供单词向量:
if PRE_TRAINED:
embeddings = tf.Variable(tf.constant(0.0, shape=[vocabulary_size,
GLOVE_SIZE]),
trainable=True)
# If using pretrained embeddings, assign them to the embeddings variable
embedding_init = embeddings.assign(embedding_placeholder)
embed = tf.nn.embedding_lookup(embeddings, _inputs)
else:
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size,
embedding_dimension],
-1.0, 1.0))
embed = tf.nn.embedding_lookup(embeddings, _inputs)
我们的嵌入是用embedding_placeholder的内容初始化的,使用assign()函数将初始值分配给embeddings变量。我们设置trainable=True告诉 TensorFlow 我们希望更新单词向量的值,通过优化它们以适应当前任务。然而,通常有用的是将trainable=False,不更新这些值;例如,当我们没有太多标记数据或有理由相信单词向量已经“很好”地捕捉到我们想要的模式时。
还有一个步骤缺失,以完全将单词向量纳入训练中——用embedding_matrix喂embedding_placeholder。我们很快就会做到这一点,但现在我们继续构建图并引入双向 RNN 层和 GRU 单元。
双向 RNN 和 GRU 单元
双向 RNN 层是我们在第五章中看到的 RNN 层的一个简单扩展。它们的基本形式只包括两个普通的 RNN 层:一个从左到右读取序列的层,另一个从右到左读取。每个都产生一个隐藏表示,左到右向量 ,和右到左向量 。然后将它们连接成一个向量。这种表示的主要优势在于它能够捕捉单词的上下文,从两个方向,这使得对自然语言和文本中的基础语义有更丰富的理解。在实践中,在复杂任务中,它通常会导致更高的准确性。例如,在词性标注中,我们希望为句子中的每个单词输出一个预测的标签(如“名词”,“形容词”等)。为了预测给定单词的词性标签,有必要获取其周围单词的信息,从两个方向。
门控循环单元(GRU)单元是 LSTM 单元的一种简化。它们也有记忆机制,但参数比 LSTM 少得多。当可用数据较少时,它们经常被使用,并且计算速度更快。我们在这里不详细介绍数学细节,因为对于我们的目的来说并不重要;有许多在线资源解释 GRU 以及它与 LSTM 的区别。
TensorFlow 配备了tf.nn.bidirectional_dynamic_rnn(),这是dynamic_rnn()的扩展,用于双向层。它接受cell_fw和cell_bw RNN 单元,分别是从左到右和从右到左的向量。在这里,我们使用GRUCell()作为我们的前向和后向表示,并添加了用于正则化的 dropout,使用内置的DropoutWrapper():
with tf.name_scope("biGRU"):
with tf.variable_scope('forward'):
gru_fw_cell = tf.contrib.rnn.GRUCell(hidden_layer_size)
gru_fw_cell = tf.contrib.rnn.DropoutWrapper(gru_fw_cell)
with tf.variable_scope('backward'):
gru_bw_cell = tf.contrib.rnn.GRUCell(hidden_layer_size)
gru_bw_cell = tf.contrib.rnn.DropoutWrapper(gru_bw_cell)
outputs, states = tf.nn.bidirectional_dynamic_rnn(cell_fw=gru_fw_cell,
cell_bw=gru_bw_cell,
inputs=embed,
sequence_length=
_seqlens,
dtype=tf.float32,
scope="BiGRU")
states = tf.concat(values=states, axis=1)
我们通过沿着适当的轴使用tf.concat()来连接前向和后向状态向量,然后添加一个线性层,后跟 softmax,就像第五章中一样:
weights = {
'linear_layer': tf.Variable(tf.truncated_normal([2*hidden_layer_size,
num_classes],
mean=0,stddev=.01))
}
biases = {
'linear_layer':tf.Variable(tf.truncated_normal([num_classes],
mean=0,stddev=.01))
}
# extract the final state and use in a linear layer
final_output = tf.matmul(states,
weights["linear_layer"]) + biases["linear_layer"]
softmax = tf.nn.softmax_cross_entropy_with_logits(logits=final_output,
labels=_labels)
cross_entropy = tf.reduce_mean(softmax)
train_step = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(_labels,1),
tf.argmax(final_output,1))
accuracy = (tf.reduce_mean(tf.cast(correct_prediction,
tf.float32)))*100
现在我们准备开始训练。我们通过将我们的embedding_matrix传递给embedding_placeholder来初始化它。重要的是要注意,我们在调用tf.global_variables_initializer()之后这样做——以相反的顺序进行会用默认初始化器覆盖预训练向量:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(embedding_init, feed_dict=
{embedding_placeholder: embedding_matrix})
for step in range(1000):
x_batch, y_batch,seqlen_batch = get_sentence_batch(batch_size,
train_x,train_y,
train_seqlens)
sess.run(train_step,feed_dict={_inputs:x_batch, _labels:y_batch,
_seqlens:seqlen_batch})
if step % 100 == 0:
acc = sess.run(accuracy,feed_dict={_inputs:x_batch,
_labels:y_batch,
_seqlens:seqlen_batch})
print("Accuracy at %d: %.5f" % (step, acc))
for test_batch in range(5):
x_test, y_test,seqlen_test = get_sentence_batch(batch_size,
test_x,test_y,
test_seqlens)
batch_pred,batch_acc = sess.run([tf.argmax(final_output,1),
accuracy],
feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
print("Test batch accuracy %d: %.5f" % (test_batch, batch_acc))
print("Test batch accuracy %d: %.5f" % (test_batch, batch_acc))
摘要
在本章中,我们扩展了关于处理文本序列的知识,为我们的 TensorFlow 工具箱添加了一些重要工具。我们看到了 word2vec 的基本实现,学习了核心概念和思想,并使用 TensorBoard 对嵌入进行了 3D 交互式可视化。然后我们加入了公开可用的 GloVe 词向量,以及允许更丰富和更高效模型的 RNN 组件。在下一章中,我们将看到如何使用抽象库,包括在真实文本数据上使用 LSTM 网络进行分类任务。
第七章:TensorFlow 抽象和简化
本章的目的是让您熟悉 TensorFlow 的重要实用扩展。我们首先描述抽象是什么以及为什么它们对我们有用,然后简要回顾一些流行的 TensorFlow 抽象库。然后我们更深入地研究其中两个库,演示一些它们的核心功能以及一些示例。
章节概述
正如大多数读者可能知道的那样,在编程的上下文中,术语抽象指的是一层代码,“在”现有代码之上执行原始代码的目的驱动泛化的代码。抽象是通过将与某些高阶功能相关的代码片段分组和包装在一起的方式形成的,以便方便地将它们重新框架在一起。结果是简化的代码,更容易编写、阅读和调试,通常更容易和更快地使用。在许多情况下,TensorFlow 的抽象不仅使代码更清晰,还可以显著减少代码长度,从而显著缩短开发时间。
为了让我们开始,让我们在 TensorFlow 的上下文中说明这个基本概念,并再次查看一些构建 CNN 的代码,就像我们在第四章中所做的那样:
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1],
padding='SAME')
def conv_layer(input, shape):
W = weight_variable(shape)
b = bias_variable([shape[3]])
h = tf.nn.relu(conv2d(input, W) + b)
hp = max_pool_2x2(h)
return hp
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
x = tf.placeholder(tf.float32, shape=[None, 784])
x_image = tf.reshape(x, [-1, 28, 28, 1])
h1 = conv_layer(x_image, shape=[5, 5, 1, 32])
h2 = conv_layer(h1, shape=[5, 5, 32, 64])
h3 = conv_layer(h2, shape=[5, 5, 64, 32])
在原生 TensorFlow 中,为了创建一个卷积层,我们必须根据输入和期望输出的形状定义和初始化其权重和偏置,应用具有定义步幅和填充的卷积操作,最后添加激活函数操作。很容易忘记其中一个基本组件或出错。此外,多次重复此过程可能有些繁琐,感觉可以更有效地完成。
在前面的代码示例中,我们通过使用消除这个过程中一些冗余的函数来创建了自己的小抽象。让我们将该代码的可读性与另一个版本进行比较,该版本完全相同,但没有使用任何函数:
x = tf.placeholder(tf.float32, shape=[None, 784])
x_image = tf.reshape(x, [-1, 28, 28, 1])
W1 = tf.truncated_normal([5, 5, 1, 32], stddev=0.1)
b1 = tf.constant(0.1, shape=[32])
h1 = tf.nn.relu(tf.nn.conv2d(x_image, W1,
strides=[1, 1, 1, 1], padding='SAME') + b1)
hp1 = tf.nn.max_pool(h1, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
W2 = tf.truncated_normal([5, 5, 32, 64], stddev=0.1)
b2 = tf.constant(0.1, shape=[64])
h2 = tf.nn.relu(tf.nn.conv2d(hp1, W2,
strides=[1, 1, 1, 1], padding='SAME') + b2)
hp2 = tf.nn.max_pool(h2, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
W3 = tf.truncated_normal([5, 5, 64, 32], stddev=0.1)
b3 = tf.constant(0.1, shape=[32])
h3 = h1 = tf.nn.relu(tf.nn.conv2d(hp2, W3,
strides=[1, 1, 1, 1], padding='SAME') + b3)
hp3 = tf.nn.max_pool(h3, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
即使只有三层,结果代码看起来也相当混乱和令人困惑。显然,随着我们进展到更大更先进的网络,像这样的代码将很难管理和传递。
除了更典型的中等大小的代码批处理外,长而复杂的代码通常会在抽象库中为我们“包装起来”。这在相对简单的模型中特别有效,几乎不需要任何定制。作为下一节内容的预览,您已经可以看到在contrib.learn中,一个适用于 TensorFlow 的可用抽象之一,定义和训练线性回归模型的核心,类似于第三章末尾的模型,只需两行代码即可完成:
regressor = learn.LinearRegressor(feature_columns=feature_columns,
optimizer=optimizer)
regressor.fit(X, Y, steps=200, batch_size=506)
高级调查
在撰写本书时,有不少出色的 TensorFlow 开源扩展可用。其中一些流行的扩展包括:
-
tf.contrib.learn -
TFLearn
-
TF-Slim
-
Keras
虽然需要安装 TFLearn,但contrib.learn和 TF-Slim(现在是tf.contrib.slim)已与 TensorFlow 合并,因此无需安装。2017 年,Keras 获得了官方的 Google 支持,并且已经移入tf.contrib作为 1.1 版本(tf.contrib.keras)。名称contrib指的是该库中的代码是“贡献的”,仍然需要测试以查看是否得到广泛接受。因此,它仍可能发生变化,并且尚未成为核心 TensorFlow 的一部分。
contrib.learn最初是作为 TensorFlow 的独立简化接口而创建的,最初被称为Scikit Flow,旨在使那些从“一行代码”机器学习的scikit-learn世界过渡的人更容易创建复杂的网络。通常情况下,它后来被合并到 TensorFlow 中,现在被视为其Learn 模块,具有广泛的文档和示例,可在官方 TensorFlow 网站上找到。
与其他库一样,contrib.learn的主要目标是使配置、训练和评估我们的学习模型变得简单。对于非常简单的模型,您可以使用现成的实现来训练,只需几行代码。正如我们很快将看到的,contrib.learn的另一个巨大优势是功能,可以非常方便地处理数据特征。
虽然contrib.learn更加透明和低级,其他三个扩展更加清晰和抽象,每个都有自己的特长和小优势,根据用户的需求可能会很有用。
TFLearn 和 Keras 充满了功能,并且具有许多各种类型的最新建模所需的元素。与所有其他库不同的是,这些库是专门为与 TensorFlow 通信而创建的,而 Keras 支持 TensorFlow 和 Theano(一种流行的深度学习库)。
TF-Slim 主要用于轻松设计复杂的卷积网络,并且有各种预训练模型可用,使我们免于自己训练这些昂贵的过程。
这些库非常动态,不断变化,开发人员添加新模型和功能,并偶尔修改其语法。
Theano
Theano是一个 Python 库,允许您以高效的方式操作涉及张量数组的符号数学表达式,因此它可以作为一个深度学习框架,与 TensorFlow 竞争。Theano 存在的时间更长,因此比 TensorFlow 更成熟一些,后者仍在不断变化和发展,但正在迅速成为领头羊(许多人普遍认为它已经是领先的库,具有许多优势,超过其他框架)。
在接下来的章节中,我们演示如何使用这些扩展,以及一些示例。我们首先关注contrib.learn,演示它如何轻松让我们训练和运行简单的回归和分类模型。接下来我们介绍 TFLearn,并重新访问前几章介绍的更高级模型——CNN 和 RNN。然后我们简要介绍自编码器,并演示如何使用 Keras 创建一个。最后,我们结束本章,简要介绍 TF-Slim,并展示如何使用加载的预训练最先进的 CNN 模型对图像进行分类。
contrib.learn
使用contrib.learn不需要任何安装,因为它已经与 TensorFlow 合并:
import tensorflow as tf
from tensorflow.contrib import learn
我们从contrib.learn的现成评估器(模型的花哨名称)开始,可以快速高效地训练。这些预定义的评估器包括简单的线性和逻辑回归模型,简单的线性分类器和基本的深度神经网络。表 7-1 列出了我们可以使用的一些流行的评估器。
表 7-1. 流行的内置 contrib.learn 评估器
| 评估器 | 描述 |
|---|---|
LinearRegressor() |
线性回归模型,用于根据特征值的观察来预测标签值。 |
LogisticRegressor() |
用于二元分类的逻辑回归评估器。 |
LinearClassifier() |
线性模型,将实例分类为多个可能的类别之一。当可能的类别数为 2 时,这是二元分类。 |
DNNRegressor() |
用于 TensorFlow 深度神经网络(DNN)模型的回归器。 |
DNNClassifier() |
用于 TensorFlow DNN 模型的分类器。 |
当然,我们也希望使用更先进和定制的模型,为此contrib.learn让我们方便地包装我们自己制作的估计器,这是我们将在接下来讨论的功能之一。一旦我们准备好部署一个估计器,无论是为我们制作还是我们自己制作的,步骤基本相同:
-
我们实例化评估器类来创建我们的模型:
model=learn.*`<``some_Estimator``>`*() -
然后我们使用我们的训练数据拟合它:
model.fit() -
我们评估模型,看它在某个给定数据集上的表现如何:
model.evaluate() -
最后,我们使用我们拟合的模型来预测结果,通常是针对新数据:
model.predict()
这四个基本阶段也存在于其他扩展中。
contrib提供许多其他功能和特性;特别是,contrib.learn有一种非常巧妙的方式来处理我们的输入数据,这将是下一小节的重点,我们将在其中讨论线性模型。
线性回归
我们从contrib.learn的一个最强大的功能开始,即线性模型。我们说一个模型是线性的,每当它由特征的加权和的函数定义时,或更正式地f(w[1]x[1] + w[2]x[2] +...+ w[n]x[n]),其中f可以是任何类型的函数,如恒等函数(如线性回归中)或逻辑函数(如逻辑回归中)。尽管在表达能力上有限,线性模型具有许多优点,如清晰的可解释性、优化速度和简单性。
在第三章中,我们使用原生 TensorFlow 创建了自己的线性回归模型,首先创建了一个包含输入和目标数据占位符的图形,一组参数的变量,损失函数和优化器。在定义了模型之后,我们运行了会话并获得了结果。
在接下来的部分中,我们首先重复这个完整的过程,然后展示如何使用contrib.learn来做到这一点要容易得多。在这个例子中,我们使用波士顿房屋数据集,可以使用sklearn库下载。波士顿房屋数据集是一个相对较小的数据集(506 个样本),包含有关马萨诸塞州波士顿地区住房的信息。这个数据集中有 13 个预测变量:
-
CRIM:按城镇人均犯罪率
-
ZN:用于超过 25,000 平方英尺地块的住宅用地比例
-
INDUS:每个城镇非零售业务面积的比例
-
CHAS:查尔斯河虚拟变量(如果地块边界河流则为 1;否则为 0)
-
NOX:一氧化氮浓度(每千万份之)
-
RM:每个住宅的平均房间数
-
AGE:1940 年之前建造的自住单位比例
-
DIS:到波士顿五个就业中心的加权距离
-
RAD:到径向高速公路的可达性指数
-
TAX:每 1 万美元的全额财产税率
-
PTRATIO:按城镇的师生比
-
B:1000(Bk - 0.63)²,其中 Bk 是按城镇划分的黑人比例
-
LSTAT:人口的较低地位%
目标变量是以千美元为单位的自住房屋的中位数价值。在这个例子中,我们尝试通过使用这 13 个特征的一些线性组合来预测目标变量。
首先,我们导入数据:
from sklearn import datasets, metrics, preprocessing
boston = datasets.load_boston()
x_data = preprocessing.StandardScaler().fit_transform(boston.data)
y_data = boston.target
接下来,我们使用与第三章中相同的线性回归模型。这次我们跟踪“损失”,以便测量均方误差(MSE),即实际目标值与我们预测值之间的平方差的平均值。我们使用这个度量作为我们的模型表现如何的指标:
x = tf.placeholder(tf.float64,shape=(None,13))
y_true = tf.placeholder(tf.float64,shape=(None))
with tf.name_scope('inference') as scope:
w = tf.Variable(tf.zeros([1,13],dtype=tf.float64,name='weights'))
b = tf.Variable(0,dtype=tf.float64,name='bias')
y_pred = tf.matmul(w,tf.transpose(x)) + b
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
with tf.name_scope('train') as scope:
learning_rate = 0.1
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
train = optimizer.minimize(loss)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(200):
sess.run(train,{x: x_data, y_true: y_data})
MSE = sess.run(loss,{x: x_data, y_true: y_data})
print(MSE)
Out:
MSE = 21.9036388397
经过 200 次迭代,我们打印出训练集计算的 MSE。现在我们执行完全相同的过程,但使用contrib.learn的线性回归估计器。定义、拟合和评估模型的整个过程只需要几行代码:
-
线性回归模型是使用
learn.LinearRegressor()实例化的,并提供有关数据表示和优化器类型的知识:reg = learn.LinearRegressor( feature_columns=feature_columns, optimizer=tf.train.GradientDescentOptimizer( learning_rate=0.1) ) -
使用
.fit()训练regressor对象。我们传递协变量和目标变量,并设置步数和批量大小:reg.fit(x_data, boston.target, steps=NUM_STEPS, batch_size=MINIBATCH_SIZE) -
MSE 损失由
.evaluate()返回:MSE = regressor.evaluate(x_data, boston.target, steps=1)
以下是完整的代码:
NUM_STEPS = 200
MINIBATCH_SIZE = 506
feature_columns = learn.infer_real_valued_columns_from_input(x_data)
reg = learn.LinearRegressor(
feature_columns=feature_columns,
optimizer=tf.train.GradientDescentOptimizer(
learning_rate=0.1)
)
reg.fit(x_data, boston.target, steps=NUM_STEPS,
batch_size=MINIBATCH_SIZE)
MSE = reg.evaluate(x_data, boston.target, steps=1)
print(MSE)
Out:
{'loss': 21.902138, 'global_step': 200}
输入数据的某种表示形式作为一个名为feature_columns的处理变量传递给回归器实例化。我们很快会回到这个问题。
DNN 分类器
与回归一样,我们可以使用contrib.learn来应用一个开箱即用的分类器。在第二章中,我们为 MNIST 数据创建了一个简单的 softmax 分类器。DNNClassifier估计器允许我们使用大大减少的代码量执行类似的任务。此外,它允许我们添加隐藏层(DNN 的“深”部分)。
与第二章一样,我们首先导入 MNIST 数据:
import sys
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
DATA_DIR = '/tmp/data' if not 'win32' in sys.platform else "c:\\tmp\\data"
data = input_data.read_data_sets(DATA_DIR, one_hot=False)
x_data, y_data = data.train.images,data.train.labels.astype(np.int32)
x_test, y_test = data.test.images,data.test.labels.astype(np.int32)
请注意,在这种情况下,由于估计器的要求,我们以其类别标签形式传递目标:
one_hot=False
对于每个样本返回一个单个整数,对应于正确的数字类别(即从 0 到[类别数] - 1 的值),而不是每个标签都是一个向量,其中每个标签是一个向量,其中 1 在对应于正确类别的索引中。
接下来的步骤与我们在前面的示例中所采取的步骤类似,只是在定义模型时,我们添加了类别数(10 个数字)并传递一个列表,其中每个元素对应于具有指定单位数的隐藏层。在这个例子中,我们使用一个具有 200 个单位的隐藏层:
NUM_STEPS = 2000
MINIBATCH_SIZE = 128
feature_columns = learn.infer_real_valued_columns_from_input(x_data)
dnn = learn.DNNClassifier(
feature_columns=feature_columns,
hidden_units=[200],
n_classes=10,
optimizer=tf.train.ProximalAdagradOptimizer(
learning_rate=0.2)
)
dnn.fit(x=x_data,y=y_data, steps=NUM_STEPS,
batch_size=MINIBATCH_SIZE)
test_acc = dnn.evaluate(x=x_test,y=y_test, steps=1)["accuracy"]
print('test accuracy: {}'.format(test_acc))
Out:
test accuracy: 0.977
尽管不如我们在第四章中的 CNN 模型(超过 99%)那么好,但这里的测试准确性(约 98%)比简单 softmax 示例中的要好得多(约 92%),这是由于添加了一个隐藏层。在图 7-1 中,我们看到模型的准确性随着隐藏层中单位数的增加而增加。

图 7-1。单个隐藏层中添加的单位数与 MNIST 分类测试准确性的关系。
使用<*Estimator*>.predict()方法,我们可以预测新样本的类别。在这里,我们将使用预测来演示如何分析我们模型的性能——哪些类别被最好地识别,以及发生了哪些类型的典型错误。绘制混淆矩阵可以帮助我们理解这些行为。我们从scikit-learn库中导入创建混淆矩阵的代码:
from sklearn.metrics import confusion_matrix
y_pred = dnn.predict(x=x_test,as_iterable=False)
class_names = ['0','1','2','3','4','5','6','7','8','9']
cnf_matrix = confusion_matrix(y_test, y_pred)
混淆矩阵显示在图 7-2 中。其行对应于真实数字,列对应于预测数字。例如,我们看到模型有时将5误分类为3,将9误分类为4和7。

图 7-2。显示每个真实标签(行)的预测数字(列)的混淆矩阵。
FeatureColumn
contrib.learn的一个最好的功能是处理不同类型的特征,有时可能会有点棘手。为了简化事情,contrib.learn为我们提供了FeatureColumn抽象。
使用FeatureColumn,我们可以在执行一系列定义在其上的转换的同时,保持数据中单个特征的表示。FeatureColumn可以是原始列之一,也可以是根据我们的转换添加的任何新列。这些可能包括通过将其编码为稀疏向量(通常称为虚拟编码)来为分类数据创建合适和有效的表示形式,创建特征交叉以查找特征交互作用,以及桶化(数据的离散化)。所有这些都可以在将特征作为单个语义单元(例如,所有虚拟向量)的情况下完成。
我们使用FeatureColumn抽象来指定输入数据的每个特征的形式和结构。例如,假设我们的目标变量是height,我们尝试使用两个特征weight和species来预测它。我们制作自己的合成数据,其中身高是通过将每个体重除以一个系数 100 并添加一个根据物种变化的常数生成的:为 Humans 添加 1,为 Goblins 添加 0.9,为 ManBears 添加 1.1。然后我们为每个实例添加正态分布的噪声:
import pandas as pd
N = 10000
weight = np.random.randn(N)*5+70
spec_id = np.random.randint(0,3,N)
bias = [0.9,1,1.1]
height = np.array([weight[i]/100 + bias[b] for i,b in enumerate(spec_id)])
spec_name = ['Goblin','Human','ManBears']
spec = [spec_name[s] for s in spec_id]
图 7-3 展示了数据样本的可视化。

图 7-3. 左:三种物种(Goblins,Humans 和 ManBears)身高的直方图(分布中心分别为 1.6、1.7 和 1.8)。右:身高与体重的散点图。
我们的目标变量是一个数值型的身高 NumPy 数组height,我们的协变量是数值型的体重 NumPy 数组weight和表示每个物种名称的字符串列表spec。
我们使用 Pandas 库将数据表示为数据框(表),以便我们可以方便地访问每一列:
df = pd.DataFrame({'Species':spec,'Weight':weight,'Height':height})
图 7-4 展示了我们的数据框的样子。

图 7-4. 身高-物种-体重数据框的十行。身高和体重是数值型的;物种是具有三个类别的分类变量。
Pandas
Pandas 是 Python 中非常流行和有用的库,用于处理关系型或带标签的数据,如表格数据,多维时间序列等。有关如何使用 Pandas 的更多信息,我们建议读者参考 Wes McKinney 的书Python for Data Analysis(O’Reilly)。
我们首先指定每个特征的性质。对于Weight,我们使用以下FeatureColumn命令,指示它是一个连续变量:
from tensorflow.contrib import layers
Weight = layers.real_valued_column("Weight")
Layers
contrib.layers不是contrib.learn的一部分,而是 TensorFlow Python API 的另一个独立子部分,提供用于构建神经网络层的高级操作和工具。
传递给函数的名称(在本例中为Weight)非常重要,因为它将用于将FeatureColumn表示与实际数据关联起来。
Species是一个分类变量,意味着其值没有自然顺序,因此不能在模型中表示为单个变量。相反,它必须被扩展和编码为多个变量,取决于类别的数量。FeatureColumn为我们做到了这一点,因此我们只需使用以下命令指定它是一个分类特征,并指定每个类别的名称:
Species = layers.sparse_column_with_keys(
column_name="Species", keys=['Goblin','Human','ManBears'])
接下来,我们实例化一个估计器类并输入我们的FeatureColumn列表:
reg = learn.LinearRegressor(feature_columns=[Weight,Species])
到目前为止,我们已经定义了数据在模型中的表示方式;在拟合模型的下一阶段中,我们需要提供实际的训练数据。在波士顿房屋的例子中,特征都是数值型的,因此我们可以将它们直接输入为x_data和目标数据。
在这里,contrib.learn要求我们使用一个额外的封装输入函数。该函数以原生形式(Pandas 数据框,NumPy 数组,列表等)获取预测变量和目标数据作为输入,并返回一个张量字典。在这些字典中,每个键都是一个FeatureColumn的名称(之前作为输入给出的Weight和Species的名称),其值需要是包含相应数据的张量。这意味着我们还需要在函数内部将值转换为 TensorFlow 张量。
在我们当前的示例中,该函数接收我们的数据框,创建一个名为feature_cols的字典,然后将数据框中每一列的值存储为对应键的张量。然后将该字典和目标变量作为张量返回。键必须与我们用来定义FeatureColumn的名称匹配:
def input_fn(df):
feature_cols = {}
feature_cols['Weight'] = tf.constant(df['Weight'].values)
feature_cols['Species'] = tf.SparseTensor(
indices=[[i, 0] for i in range(df['Species'].size)],
values=df['Species'].values,
dense_shape=[df['Species'].size, 1])
labels = tf.constant(df['Height'].values)
return feature_cols, labels
Species的值需要按照它们的FeatureColumn规范以稀疏格式进行编码。为此,我们使用tf.SparseTensor(),其中每个i索引对应一个非零值(在这种情况下,一个列矩阵中的所有行)。
例如,以下内容:
SparseTensor(indices=[[0, 0], [2, 1], [2, 2]], values=[2, 5, 7],
dense_shape=[3, 3])
表示密集张量:
[[2, 0, 0]
[0, 0, 0]
[0, 5, 7]]
我们以以下方式将其传递给.fit()方法:
reg.fit(input_fn=lambda:input_fn(df), steps=50000)
这里,input_fn()是我们刚刚创建的函数,df是包含数据的数据框,我们还指定了迭代次数。
请注意,我们以lambda函数的形式传递函数,而不是函数的输出,因为.fit()方法需要一个函数对象。使用lambda允许我们传递输入参数并将其保持在对象形式中。我们可以使用其他方法来实现相同的结果,但lambda可以胜任。
拟合过程可能需要一段时间。如果您不想一次完成所有操作,可以将其分成几个部分(请参阅下面的注释)。
分割训练过程
由于模型的状态在分类器中得以保留,因此可以进行迭代拟合。例如,我们可以将其分成五个部分,而不是像之前那样连续进行所有的 50,000 次迭代:
reg.fit(input_fn=lambda:input_fn(df), steps=10000)
reg.fit(input_fn=lambda:input_fn(df), steps=10000)
reg.fit(input_fn=lambda:input_fn(df), steps=10000)
reg.fit(input_fn=lambda:input_fn(df), steps=10000)
reg.fit(input_fn=lambda:input_fn(df), steps=10000)
并实现相同的结果。如果我们想要在训练过程中跟踪模型,这可能会很有用;然而,后面我们将看到有更好的方法来实现这一点。
现在让我们通过查看估计的权重来看看模型的表现如何。我们可以使用.get_variable_value()方法获取变量的值:
w_w = reg.get_variable_value('linear/Weight/weight')
print('Estimation for Weight: {}'.format(w_w))
s_w = reg.get_variable_value('linear/Species/weights')
b = reg.get_variable_value('linear/bias_weight')
print('Estimation for Species: {}'.format(s_w + b))
Out:
Estimation for Weight: [[0.00992305]]
Estimation for Species: [[0.90493023]
[1.00566959]
[1.10534406]]
我们请求Weight和Species的权重值。Species是一个分类变量,因此它的三个权重值用作不同的偏差项。我们看到模型在估计真实权重(Weight为0.01,Goblins、Humans和ManBears的Species分别为0.9、1、1.1)方面表现得相当不错。我们可以使用.get_variable_names()方法获取变量的名称。
在更复杂的情况下,我们可以使用相同的过程处理许多类型的特征及其交互。表 7-2 列出了一些您可以使用contrib.learn进行的有用操作。
表 7-2. 有用的特征转换操作
| 操作 | 描述 |
|---|---|
layers.sparse_column_with_keys() |
处理分类值的转换 |
layers.sparse_column_with_hash_bucket() |
处理您不知道所有可能值的分类特征的转换 |
layers.crossed_column() |
设置特征交叉(交互) |
layers.bucketized_column() |
将连续列转换为分类列 |
使用contrib.learn创建自制 CNN
接下来,我们将使用contrib.learn来创建自己的估计器。为此,我们首先需要构建一个模型函数,其中我们自制的网络将驻留,并包含我们的训练设置的对象。
在下面的示例中,我们创建一个自定义的 CNN 估计器,它与第四章开头使用的相同,并再次用于对 MNIST 数据进行分类。我们首先创建一个包含数据、操作模式(训练或测试)和模型参数的估计器函数。
在 MNIST 数据中,像素以向量形式连接在一起,因此需要对它们进行重塑:
x_image = tf.reshape(x_data, [-1, 28, 28, 1])
我们通过使用contrib.layers功能来构建网络,使得层构建过程更简单。
使用layers.convolution2d(),我们可以在一行命令中设置所有内容:我们传递输入(上一层的输出),然后指定特征映射的数量(32)、滤波器的大小(5×5)和激活函数(relu),并初始化权重和偏置。输入的维度会自动识别,不需要指定。此外,与在较低级别的 TensorFlow 中工作时不同,我们不需要单独定义变量和偏置的形状:
conv1 = layers.convolution2d(x_image, 32, [5,5],
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
填充默认设置为'SAME'(像素数不变),导致输出形状为 28×28×32。
我们还添加了标准的 2×2 池化层:
pool1 = layers.max_pool2d(conv1, [2,2])
然后我们重复这些步骤,这次是为 64 个目标特征映射:
conv2 = layers.convolution2d(pool1, 64, [5,5],
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
pool2 = layers.max_pool2d(conv2, [2,2])
接下来,我们将展平 7×7×64 的张量,并添加一个全连接层,将其减少到 1,024 个条目。我们使用fully_connected()类似于convolution2d(),只是我们指定输出单元的数量而不是滤波器的大小(这只有一个):
pool2_flat = tf.reshape(pool2, [-1, 7*7*64])
fc1 = layers.fully_connected(pool2_flat, 1024,
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
然后我们使用keep_prob添加了 dropout,该参数设置在函数中(训练/测试模式),并添加了最终的具有 10 个输出条目的全连接层,对应于 10 个类别:
fc1_drop = layers.dropout(fc1, keep_prob=params["dropout"],
is_training=(mode == 'train'))
y_conv = layers.fully_connected(fc1_drop, 10, activation_fn=None)
通过定义具有损失和优化器学习率的训练对象,我们完成了模型函数的定义。
现在我们有一个封装整个模型的函数:
def model_fn(x, target, mode, params):
y_ = tf.cast(target, tf.float32)
x_image = tf.reshape(x, [-1, 28, 28, 1])
# Conv layer 1
conv1 = layers.convolution2d(x_image, 32, [5,5],
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
pool1 = layers.max_pool2d(conv1, [2,2])
# Conv layer 2
conv2 = layers.convolution2d(pool1, 64, [5,5],
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
pool2 = layers.max_pool2d(conv2, [2,2])
# FC layer
pool2_flat = tf.reshape(pool2, [-1, 7*7*64])
fc1 = layers.fully_connected(pool2_flat, 1024,
activation_fn=tf.nn.relu,
biases_initializer=tf.constant_initializer(0.1),
weights_initializer=tf.truncated_normal_initializer(stddev=0.1))
fc1_drop = layers.dropout(fc1, keep_prob=params["dropout"],
is_training=(mode == 'train'))
# Readout layer
y_conv = layers.fully_connected(fc1_drop, 10, activation_fn=None)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_conv, labels=y_))
train_op = tf.contrib.layers.optimize_loss(
loss=cross_entropy,
global_step=tf.contrib.framework.get_global_step(),
learning_rate=params["learning_rate"],
optimizer="Adam")
predictions = tf.argmax(y_conv, 1)
return predictions, cross_entropy, train_op
我们通过使用contrib.learn.Estimator()来实例化评估器,然后就可以开始了。一旦定义好,我们可以像以前一样使用它的功能:
from tensorflow.contrib import layers
data = input_data.read_data_sets(DATA_DIR, one_hot=True)
x_data, y_data = data.train.images,np.int32(data.train.labels)
tf.cast(x_data,tf.float32)
tf.cast(y_data,tf.float32)
model_params = {"learning_rate": 1e-4, "dropout": 0.5}
CNN = tf.contrib.learn.Estimator(
model_fn=model_fn, params=model_params)
print("Starting training for %s steps max" % 5000)
CNN.fit(x=data.train.images,
y=data.train.labels, batch_size=50,
max_steps=5000)
test_acc = 0
for ii in range(5):
batch = data.test.next_batch(2000)
predictions = list(CNN.predict(batch[0], as_iterable=True))
test_acc = test_acc + (np.argmax(batch[1],1) == predictions).mean()
print(test_acc/5)
Out:
0.9872
与较低级别的 TensorFlow 相比,使用contrib.learn和contrib.layers可以大大减少代码行数。更重要的是,代码更有组织性,更容易跟踪、调试和编写。
通过这个例子,我们结束了本章的contrib.learn部分。现在我们将继续介绍 TFLearn 库的一些功能。
TFLearn
TFLearn 是另一个库,它允许我们以非常干净、压缩的方式创建复杂的自定义模型,同时仍然具有合理的灵活性,我们很快就会看到。
安装
与之前的库不同,TFLearn 首先需要安装。使用pip进行安装非常简单:
pip install tflearn
如果这样不起作用,可以从GitHub下载并手动安装。
在库成功安装后,您应该能够导入它:
import tflearn
CNN
TFLearn 的许多功能与前一节中涵盖的contrib.learn类似;然而,相比之下,在 TFLearn 中创建自定义模型更简单、更清晰。在下面的代码中,我们使用了之前用于 MNIST 数据的相同 CNN 模型。
模型构建通过regression()进行包装和最终化,我们在其中设置损失和优化配置,就像我们在contrib.learn中为训练对象做的那样(这里我们只是简单地指定了'categorical_crossentropy'作为损失,而不是显式定义它):
from tflearn.layers.core import input_data, dropout, fully_connected
from tflearn.layers.conv import conv_2d, max_pool_2d
from tflearn.layers.normalization import local_response_normalization
from tflearn.layers.estimator import regression
# Data loading and basic transformations
import tflearn.datasets.mnist as mnist
X, Y, X_test, Y_test = mnist.load_data(one_hot=True)
X = X.reshape([-1, 28, 28, 1])
X_test = X_test.reshape([-1, 28, 28, 1])
# Building the network
CNN = input_data(shape=[None, 28, 28, 1], name='input')
CNN = conv_2d(CNN, 32, 5, activation='relu', regularizer="L2")
CNN = max_pool_2d(CNN, 2)
CNN = local_response_normalization(CNN)
CNN = conv_2d(CNN, 64, 5, activation='relu', regularizer="L2")
CNN = max_pool_2d(CNN, 2)
CNN = local_response_normalization(CNN)
CNN = fully_connected(CNN, 1024, activation=None)
CNN = dropout(CNN, 0.5)
CNN = fully_connected(CNN, 10, activation='softmax')
CNN = regression(CNN, optimizer='adam', learning_rate=0.0001,
loss='categorical_crossentropy', name='target')
# Training the network
model = tflearn.DNN(CNN,tensorboard_verbose=0,
tensorboard_dir = 'MNIST_tflearn_board/',
checkpoint_path = 'MNIST_tflearn_checkpoints/checkpoint')
model.fit({'input': X}, {'target': Y}, n_epoch=3,
validation_set=({'input': X_test}, {'target': Y_test}),
snapshot_step=1000,show_metric=True, run_id='convnet_mnist')
在这里添加的另一层是局部响应归一化层,我们在第四章中简要提到过。有关此层的更多详细信息,请参阅即将发布的说明。
tflearn.DNN()函数在某种程度上相当于contrib.learn.Estimator()——它是 DNN 模型的包装器,我们用它来实例化模型,并传递我们构建的网络。
在这里,我们还可以设置 TensorBoard 和检查点目录,TensorBoard 日志的详细程度(0-3,从基本的损失和准确度报告到其他指标如梯度和权重),以及其他设置。
一旦我们有一个准备好的模型实例,我们就可以执行标准操作。表 7-3 总结了 TFLearn 中模型的功能。
表 7-3. 标准 TFLearn 操作
| 函数 | 描述 |
|---|---|
evaluate(*X*, *Y*, *batch_size=128*) |
对给定样本进行模型评估。 |
fit(*X*, *Y*, *n_epoch=10*) |
使用输入特征*X*和目标*Y*对网络进行训练。 |
get_weights(*weight_tensor*) |
获取变量的权重。 |
load(*model_file*) |
恢复模型权重。 |
predict(*X*) |
获取给定输入数据的模型预测。 |
save(*model_file*) |
保存模型权重。 |
set_weights(*tensor*, *weights*) |
为张量变量分配给定值。 |
与contrib.learn类似,通过使用.fit()方法执行拟合操作,我们向其中提供数据和控制训练设置:要执行的周期数,训练和验证批量大小,显示的度量,保存的摘要频率等。在拟合过程中,TFLearn 显示一个漂亮的仪表板,使我们能够在线跟踪训练过程。
局部响应归一化
局部响应归一化(LRN)层通过对局部输入区域进行归一化来执行一种横向抑制。这是通过将输入值除以某个深度半径内所有输入的加权平方和来完成的,我们可以手动选择。其结果是激活神经元与其周围局部环境之间的激活对比增加,产生更显著的局部极大值。这种方法鼓励抑制,因为它将减少大但均匀的激活。此外,归一化对于防止神经元在输入可能具有不同尺度时饱和是有用的(ReLU 神经元具有无界激活)。有更现代的替代方法用于正则化,例如批量归一化和丢失,但了解 LRN 也是很好的。
在拟合模型后,我们在测试数据上评估性能:
evaluation = model.evaluate({'input': X_test},{'target': Y_test})
print(evaluation):
Out:
0.9862
并形成新的预测(再次使用它们作为对先前评估的“健全性检查”):
pred = model.predict({'input': X_test})
print((np.argmax(Y_test,1)==np.argmax(pred,1)).mean())
Out:
0.9862
在 TFLearn 中的迭代训练步骤和周期
在 TFLearn 中,每次迭代是对一个示例的完整传递(前向和后向)。训练步骤是要执行的完整传递次数,由您设置的批量大小(默认为 64)确定,周期是对所有训练示例的完整传递(在 MNIST 的情况下为 50,000)。图 7-5 显示了 TFLearn 中交互式显示的示例。

图 7-5。TFLearn 中的交互式显示。
RNN
通过构建一个完全功能的文本分类 RNN 模型,我们结束了对 TFLearn 的介绍,这大大简化了我们在第五章和第六章中看到的代码。
我们执行的任务是对电影评论进行情感分析,进行二元分类(好或坏)。我们将使用一个著名的 IMDb 评论数据集,包含 25,000 个训练样本和 25,000 个测试样本:
from tflearn.data_utils import to_categorical, pad_sequences
from tflearn.datasets import imdb
# IMDb dataset loading
train, test, _ = imdb.load_data(path='imdb.pkl', n_words=10000,
valid_portion=0.1)
X_train, Y_train = train
X_test, Y_test = test
我们首先准备数据,这些数据具有不同的序列长度,通过使用tflearn.data_utils.pad_sequences()进行零填充来使序列相等,并将最大序列长度设置为 100:
X_train = pad_sequences(X_train, maxlen=100, value=0.)
X_test = pad_sequences(X_test, maxlen=100, value=0.)
现在我们可以用一个张量表示数据,其中样本在其行中,单词 ID 在其列中。正如在第五章中解释的那样,这里的 ID 是用来任意编码实际单词的整数。在我们的情况下,有 10,000 个唯一的 ID。
接下来,我们使用tflearn.embedding()将每个单词嵌入到连续的向量空间中,将我们的二维张量[*samples*, *IDs*]转换为三维张量[*samples*, *IDs*, *embedding-size*],其中每个单词 ID 现在对应于大小为 128 的向量。在此之前,我们使用input_data()将数据输入/馈送到网络(使用给定形状创建了一个 TensorFlow 占位符):
RNN = tflearn.input_data([None, 100])
RNN = tflearn.embedding(RNN, input_dim=10000, output_dim=128)
最后,我们添加了一个 LSTM 层和一个全连接层来输出二元结果:
RNN = tflearn.lstm(RNN, 128, dropout=0.8)
RNN = tflearn.fully_connected(RNN, 2, activation='softmax'
以下是完整的代码:
from tflearn.data_utils import to_categorical, pad_sequences
from tflearn.datasets import imdb
# Load data
train, test, _ = imdb.load_data(path='imdb.pkl', n_words=10000,
valid_portion=0.1)
X_train, Y_train = train
X_test, Y_test = test
# Sequence padding and converting labels to binary vectors
X_train = pad_sequences(X_train, maxlen=100, value=0.)
X_test = pad_sequences(X_test, maxlen=100, value=0.)
Y_train = to_categorical(Y_train, nb_classes=2)
Y_test = to_categorical(Y_test, nb_classes=2)
# Building an LSTM network
RNN = tflearn.input_data([None, 100])
RNN = tflearn.embedding(RNN, input_dim=10000, output_dim=128)
RNN = tflearn.lstm(RNN, 128, dropout=0.8)
RNN = tflearn.fully_connected(RNN, 2, activation='softmax')
RNN = tflearn.regression(RNN, optimizer='adam', learning_rate=0.001,
loss='categorical_crossentropy')
# Training the network
model = tflearn.DNN(RNN, tensorboard_verbose=0)
model.fit(X_train, Y_train, validation_set=(X_test, Y_test),
show_metric=True, batch_size=32)
在本节中,我们只是简单地尝试了一下 TFLearn。该库有很好的文档和许多示例,非常值得一看。
Keras
Keras 是最流行和强大的 TensorFlow 扩展库之一。在本章中我们调查的扩展中,Keras 是唯一支持 Theano 和 TensorFlow 两者的库。这是因为 Keras 完全抽象了其后端;Keras 有自己的图数据结构来处理计算图并与 TensorFlow 通信。
实际上,由于这一点,甚至可能定义一个使用 TensorFlow 或 Theano 的 Keras 模型,然后切换到另一个。
Keras 有两种主要类型的模型可供使用:顺序和函数。顺序类型适用于简单的架构,我们只需按线性方式堆叠层。函数 API 可以支持更通用的具有多样化层结构的模型,如多输出模型。
我们将快速查看每种模型类型使用的语法。
安装
在 TensorFlow 1.1+中,Keras 可以从contrib库中导入;然而,对于旧版本,需要外部安装。请注意,Keras 需要numpy、scipy和yaml依赖项。与 TFLearn 类似,Keras 也可以使用pip安装:
pip install keras
或者从GitHub下载并安装:
python setup.py install
默认情况下,Keras 将使用 TensorFlow 作为其张量操作库。如果设置为使用 Theano,可以通过更改名为$HOME/.keras/keras.json的文件中的设置来切换(对于 Linux 用户——根据您的操作系统修改路径),其中除了其他本章不重要的技术设置外,还有backend属性:
{
"image_data_format": "channels_last",
"epsilon": 1e-07,
"floatx": "float32",
"backend": "tensorflow"
}
如果我们想要访问后端,可以通过首先导入它来轻松实现:
from keras import backend as K
然后我们可以像在 TensorFlow 中一样使用它进行大多数张量操作(也适用于 Theano)。例如,这样:
input = K.placeholder(shape=(10,32))
等同于:
tf.placeholder(shape=(10,32))
顺序模型
使用顺序类型非常简单——我们定义它并可以简单地开始添加层:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(units=64, input_dim=784))
model.add(Activation('softmax'))
或者等效地:
model = Sequential([
Dense(64, input_shape=(784,),activation='softmax')
])
密集层是一个全连接层。第一个参数表示输出单元的数量,输入形状是输入的形状(在这个例子中,权重矩阵的大小将为 784×64)。Dense()还有一个可选参数,我们可以在第二个例子中指定并添加激活函数。
在定义模型之后,在训练之前,我们使用.compile()方法设置其学习配置。它有三个输入参数——损失函数、优化器和另一个度量函数,用于评估模型的性能(在训练模型时不用作实际损失):
model.compile(loss='categorical_crossentropy',
optimizer='sgd',
metrics=['accuracy'])
我们可以使用.optimizers来设置优化器的更精细分辨率(学习率、方法等)。例如:
optimizer=keras.optimizers.SGD(lr=0.02, momentum=0.8, nesterov=True))
最后,我们将数据传递给.fit()并设置 epoch 数和批量大小。与之前的库一样,我们现在可以轻松评估其表现并使用新的测试数据进行预测:
from keras.callbacks import TensorBoard, EarlyStopping, ReduceLROnPlateau
early_stop = EarlyStopping(monitor='val_loss', min_delta=0,
patience=10, verbose=0, mode='auto')
model.fit(x_train, y_train, epochs=10, batch_size=64,
callbacks=[TensorBoard(log_dir='/models/autoencoder',)
early_stop])
loss_and_metrics = model.evaluate(x_test, y_test, batch_size=64)
classes = model.predict(x_test, batch_size=64)
请注意,在fit()方法中添加了一个callbacks参数。回调函数是在训练过程中应用的函数,我们可以通过将它们的列表传递给.fit()方法来查看统计信息并做出动态训练决策。
在这个例子中,我们插入了两个回调函数:TensorBoard,指定其输出文件夹,和提前停止。
提前停止
提前停止用于防止过拟合,通过防止学习器进一步改善对训练数据的拟合而增加泛化误差。在这种意义上,它可以被认为是一种正则化形式。在 Keras 中,我们可以指定要监视的最小变化(min_delta)、在多少个不改进的 epoch 后停止(patience)以及所需变化的方向(mode)。
函数模型
函数模型和顺序模型之间的主要实际区别在于,这里我们首先定义输入和输出,然后实例化模型。
首先根据其形状创建一个输入张量:
inputs = Input(shape=(784,))
然后我们定义我们的模型:
x = Dense(64, activation='relu')(inputs)
x = Dense(32, activation='relu')(x)
outputs = Dense(10, activation='softmax')(x)
正如我们所看到的,层就像函数一样起作用,给功能模型赋予了其名称。
现在我们实例化模型,将输入和输出都传递给Model:
model = Model(inputs=inputs, outputs=outputs)
其他步骤如前所述:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=10, batch_size=64)
loss_and_metrics = model.evaluate(x_test, y_test, batch_size=64)
classes = model.predict(x_test, batch_size=64)
我们将通过介绍自动编码器的概念来结束本节,然后展示如何使用 Keras 实现一个自动编码器。
自动编码器
自动编码器是神经网络,试图输出输入的重构。在大多数情况下,输入在降维后进行重构。降维将是我们的主要关注点;然而,自动编码器也可以用于实现“过完备”表示(用于更稳定的分解),实际上会增加维度。
在降维中,我们希望将大小为n的每个数据向量转换为大小为m的向量,其中m < n,同时尽量保留尽可能多的重要信息。一个非常常见的方法是使用主成分分析(PCA),我们可以用新减少的特征的一些线性组合来表示每个原始数据列x[j](所有数据点对应于一个原始特征),称为主成分,使得x[j] = Σ[i=1]^mw[i]b[i]。
然而,PCA 仅限于对数据向量进行线性变换。
自动编码器是更通用的压缩器,允许复杂的非线性变换,并找到可见单元和隐藏单元之间的非平凡关系(事实上,PCA 就像是一个一层“线性自动编码器”)。模型的权重是通过减少给定的损失函数并使用优化器(例如 SGD)自动学习的。
降低输入维度的自动编码器会创建一个称为隐藏层的瓶颈层,其单元数量少于输入层,强制数据在较低维度中表示(图 7-6)然后进行重构。对于重构(解码),自动编码器提取代表性特征,捕捉一些隐藏的抽象,比如眼睛的形状,汽车的轮子,运动类型等,我们可以用这些特征重构原始输入。

图 7-6。自动编码器的示例——一个典型的自动编码器将具有相同数量的单元的输入和输出层,以及瓶颈隐藏层,其中数据的维度被减少(压缩)。
与我们迄今看到的一些模型一样,自动编码器网络可以将层堆叠在一起,并且可以包括卷积,就像 CNN 中一样。
由于自动编码器对数据的特定性,目前不太适用于真实世界的数据压缩问题——它们最适用于与它们训练的数据相似的数据。它们目前的实际应用主要用于提取较低维度的表示,去噪数据以及降低维度的数据可视化。去噪有效是因为网络学习了图像的重要抽象,同时丢失了不重要的图像特定信号,如噪音。
现在让我们使用 Keras 构建一个玩具 CNN 自动编码器。在这个例子中,我们将在带噪声的 CIFAR10 数据图像的一个类别上训练自动编码器,然后使用它对相同类别的测试集进行去噪。在这个例子中,我们将使用功能模型 API。
首先我们通过 Keras 加载图像,然后我们只选择与标签1(汽车类)对应的图像:
from keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model
from keras.callbacks import TensorBoard, ModelCheckpoint
from keras.datasets import cifar10
import numpy as np
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train = x_train[np.where(y_train==1)[0],:,:,:]
x_test = x_test[np.where(y_test==1)[0],:,:,:]
接下来我们进行一些预处理,首先将数据转换为float32,然后将其归一化到[0,1]范围内。这种归一化将允许我们在像素级别执行逐元素比较,我们很快就会看到。首先是类型转换:
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
然后我们添加一些高斯噪声来创建带噪声的数据集,并裁剪小于 0 或大于 1 的值:
x_train_n = x_train + 0.5 *\
np.random.normal(loc=0.0, scale=0.4, size=x_train.shape)
x_test_n = x_test + 0.5 *\
np.random.normal(loc=0.0, scale=0.4, size=x_test.shape)
x_train_n = np.clip(x_train_n, 0., 1.)
x_test_n = np.clip(x_test_n, 0., 1.)
现在我们声明输入层(CIFAR10 数据集中的每个图像都是 32×32 像素,带有 RGB 通道):
inp_img = Input(shape=(32, 32, 3))
接下来,我们开始添加我们通常的“乐高积木”层。我们的第一层是一个 2D 卷积层,第一个参数是滤波器的数量(因此是输出图像的数量),第二个是每个滤波器的大小。与其他库一样,Keras 会自动识别输入的形状。
我们使用一个 2×2 的池化层,将每个通道的像素总数减少 4 倍,创建所需的瓶颈。在另一个卷积层之后,通过应用上采样层,我们恢复每个通道的相同单位数。这是通过在像素的附近四倍化每个像素(重复数据的行和列)来完成的,以获得每个图像中相同数量的像素。
最后,我们添加一个卷积输出层,回到三个通道:
img = Conv2D(32, (3, 3), activation='relu', padding='same')(inp_img)
img = MaxPooling2D((2, 2), padding='same')(img)
img = Conv2D(32, (3, 3), activation='relu', padding='same')(img)
img = UpSampling2D((2, 2))(img)
decoded = Conv2D(3, (3, 3), activation='sigmoid', padding='same')(img)
我们声明功能模型格式,传递输入和输出:
autoencoder = Model(inp_img, decoded)
接下来,我们编译模型,定义损失函数和优化器——在这种情况下,我们使用 Adagrad 优化器(只是为了展示另一个例子!)。对于图像的去噪,我们希望我们的损失能够捕捉解码图像与原始、去噪前图像之间的差异。为此,我们使用二元交叉熵损失,将每个解码像素与其对应的原始像素进行比较(现在在[0,1]之间):
autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')
模型定义后,我们用 10 个训练周期来拟合它:
tensorboard=TensorBoard(log_dir='<*`some_path`*>',histogram_freq=0,write_graph=True,write_images=True)model_saver=ModelCheckpoint(filepath='<*`some_path`*>',verbose=0,period=2)autoencoder.fit(x_train_n,x_train,epochs=10,batch_size=64,shuffle=True,validation_data=(x_test_n,x_test),callbacks=[tensorboard,model_saver])
希望模型能够捕捉一些内部结构,然后将其推广到其他嘈杂图像,并因此对其进行去噪。
我们将测试集用作每个周期结束时损失评估的验证数据(模型不会在此数据上进行训练),还用于在 TensorBoard 中进行可视化。除了 TensorBoard 回调,我们还添加了一个模型保存回调,并设置为每两个周期保存一次权重。
稍后,当我们希望加载我们的权重时,我们需要重建网络,然后使用Model.load_weights()方法,将我们的模型作为第一个参数,将保存的权重文件路径作为第二个参数(有关在第十章中保存模型的更多信息):
inp_img=Input(shape=(32,32,3))img=Conv2D(32,(3,3),activation='relu',padding='same')(inp_img)img=MaxPooling2D((2,2),padding='same')(img)img=Conv2D(32,(3,3),activation='relu',padding='same')(img)img=UpSampling2D((2,2))(img)decoded=Conv2D(3,(3,3),activation='sigmoid',padding='same')(img)autoencoder=Model(inp_img,decoded)Model.load_weights(autoencoder,'*`some_path`*')
h5py 要求
为了保存模型,需要安装h5py包。这个包主要用于存储大量数据并从 NumPy 中操作数据。您可以使用pip安装它:
pip install h5py
图 7-7 显示了我们选择类别的去噪测试图像,不同训练周期的结果。

图 7-7。自动编码之前的嘈杂 CIFAR10 图像(上排)和自动编码之后的图像(下排)。底部 4 行显示了增加训练周期后的结果。
Keras 还有一堆预训练模型可供下载,如inception、vgg和resnet。在本章的下一部分和最后一部分中,我们将讨论这些模型,并展示如何下载和使用 TF-Slim 扩展来进行分类的预训练 VGG 模型的示例。
TF-Slim 的预训练模型
在本章的这一部分,我们将介绍这里要涵盖的最后一个抽象概念,TF-Slim。TF-Slim 通过提供简化的语法来定义 TensorFlow 中的卷积神经网络而脱颖而出——它的抽象使得以一种干净、简洁的方式构建复杂网络变得容易。与 Keras 一样,它还提供了各种预训练的 CNN 模型供下载和使用。
我们通过学习 TF-Slim 的一些一般特性和优点来开始本节,说明为什么它是构建 CNN 的绝佳工具。在本节的第二部分中,我们将演示如何下载和部署一个预训练模型(VGG)进行图像分类。
TF-Slim
TF-Slim 是 TensorFlow 的一个相对较新的轻量级扩展,类似于其他抽象,它允许我们快速直观地定义和训练复杂模型。TF-Slim 不需要任何安装,因为它已经与 TensorFlow 合并。
这个扩展主要是关于卷积神经网络。CNN 以混乱的样板代码而闻名。TF-Slim 的设计目标是优化非常复杂的 CNN 模型的创建,使其能够通过使用高级层、变量抽象和参数作用域进行优雅编写、易于解释和调试。
除了让我们能够创建和训练自己的模型之外,TF-Slim 还提供了可以轻松下载、阅读和使用的预训练网络:VGG、AlexNet、Inception 等。
我们从简要描述 TF-Slim 的一些抽象特性开始这一部分。然后我们将重点放在如何下载和使用预训练模型上,以 VGG 图像分类模型为例进行演示。
使用 TF-Slim 创建 CNN 模型
使用 TF-Slim,我们可以通过定义初始化、正则化和设备来轻松创建一个变量。例如,在这里,我们定义了从截断正态分布初始化的权重,使用 L2 正则化,并放置在 CPU 上(我们将在第九章讨论跨设备分配模型部分):
import tensorflow as tf
from tensorflow.contrib import slim
W = slim.variable('w',shape=[7, 7, 3 , 3],
initializer=tf.truncated_normal_initializer(stddev=0.1),
regularizer=slim.l2_regularizer(0.07),
device='/CPU:0')
与本章中看到的其他抽象一样,TF-Slim 可以减少大量样板代码和冗余复制。与 Keras 或 TFLearn 一样,我们可以在抽象级别定义一个层操作,包括卷积操作、权重初始化、正则化、激活函数等,都可以在一个命令中完成:
net = slim.conv2d(inputs, 64, [11, 11], 4, padding='SAME',
weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
weights_regularizer=slim.l2_regularizer(0.0007), scope='conv1')
TF-Slim 甚至将其优雅性扩展到了更远的地方,通过使用repeat、stack和arg_scope命令提供了一种紧凑地复制层的方法。
repeat可以避免我们反复复制粘贴相同的行,例如,与其有这种冗余复制:
net = slim.conv2d(net, 128, [3, 3], scope='con1_1')
net = slim.conv2d(net, 128, [3, 3], scope='con1_2')
net = slim.conv2d(net, 128, [3, 3], scope='con1_3')
net = slim.conv2d(net, 128, [3, 3], scope='con1_4')
net = slim.conv2d(net, 128, [3, 3], scope='con1_5')
我们可以这样输入:
net = slim.repeat(net, 5, slim.conv2d, 128, [3, 3], scope='con1')
但这仅适用于具有相同大小的层的情况。当这种情况不成立时,我们可以使用stack命令,允许我们连接不同形状的层。因此,与其这样:
net = slim.conv2d(net, 64, [3, 3], scope='con1_1')
net = slim.conv2d(net, 64, [1, 1], scope='con1_2')
net = slim.conv2d(net, 128, [3, 3], scope='con1_3')
net = slim.conv2d(net, 128, [1, 1], scope='con1_4')
net = slim.conv2d(net, 256, [3, 3], scope='con1_5')
我们可以这样写:
slim.stack(net, slim.conv2d, [(64, [3, 3]), (64, [1, 1]),
(128, [3, 3]), (128, [1, 1]),
(256, [3, 3])], scope='con')
最后,我们还有一个称为arg_scope的作用域机制,允许用户将一组共享参数传递给同一作用域中定义的每个操作。例如,假设我们有四个具有相同激活函数、初始化、正则化和填充的层。我们可以简单地使用slim.arg_scope命令,指定共享参数,如下面的代码所示:
with slim.arg_scope([slim.conv2d],
padding='VALID',
activation_fn=tf.nn.relu,
weights_initializer=tf.truncated_normal_initializer(stddev=0.02)
weights_regularizer=slim.l2_regularizer(0.0007)):
net = slim.conv2d(inputs, 64, [11, 11], scope='con1')
net = slim.conv2d(net, 128, [11, 11], padding='VALID', scope='con2')
net = slim.conv2d(net, 256, [11, 11], scope='con3')
net = slim.conv2d(net, 256, [11, 11], scope='con4')
arg_scope命令中的各个参数仍然可以被覆盖,我们也可以将一个arg_scope嵌套在另一个中。
在这些示例中,我们使用了conv2d():然而,TF-Slim 还有许多其他用于构建神经网络的标准方法。表 7-4 列出了一些可用选项。要查看完整列表,请参阅文档。
表 7-4。TF-Slim 中可用的层类型
| Layer | TF-Slim |
|---|---|
| BiasAdd | slim.bias_add() |
| BatchNorm | slim.batch_norm() |
| Conv2d | slim.conv2d() |
| Conv2dInPlane | slim.conv2d_in_plane() |
| Conv2dTranspose (Deconv) | slim.conv2d_transpose() |
| FullyConnected | slim.fully_connected() |
| AvgPool2D | slim.avg_pool2d() |
| Dropout | slim.dropout() |
| Flatten | slim.flatten() |
| MaxPool2D | slim.max_pool2d() |
| OneHotEncoding | slim.one_hot_encoding() |
| SeparableConv2 | slim.separable_conv2d() |
| UnitNorm | slim.unit_norm |
为了说明 TF-Slim 在创建复杂 CNN 时有多方便,我们将构建 Karen Simonyan 和 Andrew Zisserman 在 2014 年提出的 VGG 模型(有关更多信息,请参见即将发布的说明)。VGG 是一个很好的例子,说明了如何使用 TF-Slim 紧凑地创建具有许多层的模型。在这里,我们构建 16 层版本:13 个卷积层加上 3 个全连接层。
创建它时,我们利用了我们刚提到的两个特性:
-
我们使用
arg_scope功能,因为所有卷积层具有相同的激活函数和相同的正则化和初始化。 -
许多层都是相同的副本,因此我们还利用了
repeat命令。
结果非常引人注目——整个模型仅用 16 行代码定义:
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu,
weights_initializer=tf.truncated_normal_initializer(0.0, 0.01),
weights_regularizer=slim.l2_regularizer(0.0005)):
net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='con1')
net = slim.max_pool2d(net, [2, 2], scope='pool1')
net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='con2')
net = slim.max_pool2d(net, [2, 2], scope='pool2')
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='con3')
net = slim.max_pool2d(net, [2, 2], scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='con4')
net = slim.max_pool2d(net, [2, 2], scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='con5')
net = slim.max_pool2d(net, [2, 2], scope='pool5')
net = slim.fully_connected(net, 4096, scope='fc6')
net = slim.dropout(net, 0.5, scope='dropout6')
net = slim.fully_connected(net, 4096, scope='fc7')
net = slim.dropout(net, 0.5, scope='dropout7')
net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc8')
VGG 和 ImageNet 挑战
ImageNet 项目是一个大型图像数据库,旨在研究视觉对象识别。截至 2016 年,它包含超过 1000 万个手工注释的图像。
自 2010 年以来,每年都会举行一场名为 ImageNet 大规模视觉识别挑战(ILSVRC)的比赛,研究团队试图在 ImageNet 收集的子集中自动分类、检测和定位对象和场景。在 2012 年的挑战中,当时由 Alex Krizhevsky 创建的深度卷积神经网络 AlexNet 取得了令人瞩目的进展,成功将前 5 名(前 5 个选择的类别)的分类错误率降至仅 15.4%,以较大的优势赢得了比赛。
在接下来的几年里,错误率不断下降,从 2013 年的 ZFNet 的 14.8%,到 2014 年的 GoogLeNet(引入 Inception 模块)的 6.7%,再到 2015 年的 ResNet 的 3.6%。视觉几何组(VGG)是 2014 年比赛中的另一个 CNN 竞争对手,也取得了令人印象深刻的低错误率(7.3%)。许多人更喜欢 VGG 而不是 GoogLeNet,因为它具有更好、更简单的架构。
在 VGG 中,唯一使用的空间维度是非常小的 3×3 滤波器,步幅为 1,以及 2×2 最大池化,步幅再次为 1。它的优越性是通过使用的层数来实现的,介于 16 和 19 之间。
下载和使用预训练模型
接下来我们将演示如何下载和部署预训练的 VGG 模型。
首先,我们需要克隆存放实际模型的存储库,方法是运行:
git clone https://github.com/tensorflow/models
现在我们在计算机上拥有建模所需的脚本,并且可以通过设置路径来使用它们:
importsyssys.path.append("<*`some_path`*> + models/slim")
接下来,我们将下载预训练的 VGG-16(16 层)模型——它可以在GitHub上找到,还有其他模型,如 Inception、ResNet 等:
fromdatasetsimportdataset_utilsimporttensorflowastftarget_dir='<*`some_path`*> + vgg/vgg_checkpoints'
下载的检查点文件包含有关模型和变量的信息。现在我们想要加载它并将其用于对新图像进行分类。
然而,在此之前,我们首先必须准备好我们的输入图像,将其转换为可读的 TensorFlow 格式,并执行一些预处理,以确保它被调整大小以匹配模型训练时的图像大小。
我们可以将图像加载到 TensorFlow 中,可以作为 URL 链接或桌面图像。对于 URL 链接,我们可以使用urllib2将图像加载为字符串(需要导入),然后使用tf.image_decode_jpeg()将其解码为 Tensor:
import urllib2
url = ("https://somewebpage/somepicture.jpg")
im_as_string = urllib2.urlopen(url).read()
im = tf.image.decode_jpeg(im_as_string, channels=3)
或者,对于 PNG:
im = tf.image.decode_png(im_as_string, channels=3)
要从计算机加载图像,我们可以在目标目录中创建一个文件名队列,然后使用tf.WholeFileReader()读取整个图像文件:
filename_queue = tf.train.string_input_producer(
tf.train.match_filenames_once("./images/*.jpg"))
image_reader = tf.WholeFileReader()
_, image_file = image_reader.read(filename_queue)
image = tf.image.decode_jpeg(image_file)
不要担心这一步的细节;我们将在第八章中更深入地讨论队列和读取数据。
接下来,我们要调整图像的大小,使其与 VGG 训练时的图像大小匹配。为此,我们首先从 VGG 脚本中提取所需的大小(在本例中为 224):
from nets import vgg
image_size = vgg.vgg_16.default_image_size
然后我们将原始图像和图像尺寸传递给 VGG 预处理单元,在那里图像将被调整大小以保持纵横比(图像的宽高比),然后裁剪:
from preprocessing import vgg_preprocessing
processed_im = vgg_preprocessing.preprocess_image(image,
image_size,
image_size,
is_training=False)
接下来,我们使用tf.expand_dims()将一个维度 1 插入到张量的形状中。这是为了将批处理维度添加到单个元素(将[*高度*, *宽度*, *通道*]更改为[1, *高度*, *宽度*, *通道*]):
processed_images= tf.expand_dims(processed_im, 0)
现在我们从之前克隆的脚本创建模型。我们将图像和类别数传递给模型函数。模型具有共享参数;因此,我们像之前看到的那样使用arg_scope调用它,并在脚本中使用vgg_arg_scope()函数定义共享参数。该函数如下代码片段所示。
vgg_16()返回逻辑值(作为每个类别的证据),我们可以通过使用tf.nn.softmax()将其转换为概率。我们使用参数is_training来指示我们感兴趣的是形成预测而不是训练:
with slim.arg_scope(vgg.vgg_arg_scope()):
logits, _ = vgg.vgg_16(processed_images,
num_classes=1000,
is_training=False)
probabilities = tf.nn.softmax(logits)
def vgg_arg_scope(weight_decay=0.0005):
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu,
weights_regularizer=slim.l2_regularizer(weight_decay),
biases_initializer=tf.zeros_initializer):
with slim.arg_scope([slim.conv2d], padding='SAME') as arg_sc:
return arg_sc
现在,在开始会话之前,我们需要使用slim.assign_from_checkpoint_fn()加载下载的变量,我们将其传递给包含目录:
import os
load_vars = slim.assign_from_checkpoint_fn(
os.path.join(target_dir, 'vgg_16.ckpt'),
slim.get_model_variables('vgg_16'))
最后,主要事件——我们运行会话,加载变量,并输入图像和所需的概率。
我们可以通过以下行获取类别名称:
from datasets import imagenet
imagenet.create_readable_names_for_imagenet_labels()
我们提取给定图像的五个具有最高概率的类别,以及概率:
names = []
with tf.Session() as sess:
load_vars(sess)
network_input, probabilities = sess.run([processed_images,
probabilities])
probabilities = probabilities[0, 0:]
names_ = imagenet.create_readable_names_for_imagenet_labels()
idxs = np.argsort(-probabilities)[:5]
probs = probabilities[idxs]
classes = np.array(names_.values())[idxs+1]
for c,p in zip(classes,probs):
print('Class: '+ c + ' |Prob: ' + str(p))
在这个例子中,我们将显示在图 7-8 中的图像作为输入传递给预训练的 VGG 模型。

图 7-8. 瑞士的湖边。
以下是前五个选择的类别及其概率的输出结果:
Output: Class: lakeside, lakeshore |Prob: 0.365693
Class: pelican |Prob: 0.163627
Class: dock, dockage, docking facility |Prob: 0.0608374
Class: breakwater, groin, groyne, mole, bulwark, seawall, jetty |Prob: 0.0393285
Class: speedboat |Prob: 0.0391587
正如您所看到的,分类器在捕捉图像中的不同元素方面表现得相当不错。
摘要
我们从讨论抽象的重要性开始了本章,然后进行了高层次的覆盖,然后专注于一些流行的 TensorFlow 扩展:contrib.learn,TFLearn,Keras 和 TF-Slim。我们重新访问了前几章的模型,使用了现成的contrib.learn线性回归和线性分类模型。然后我们看到了如何使用FeatureColumn抽象来处理特征和预处理,整合 TensorBoard,并创建我们自己的自定义估算器。我们介绍了 TFLearn,并演示了如何使用它轻松构建 CNN 和 RNN 模型。使用 Keras,我们演示了如何实现自动编码器。最后,我们使用 TF-Slim 创建了复杂的 CNN 模型,并部署了一个预训练模型。
在接下来的章节中,我们将涵盖扩展规模,包括排队和线程,分布式计算和模型服务。
第八章:队列、线程和读取数据
在本章中,我们介绍了在 TensorFlow 中使用队列和线程的方法,主要目的是简化读取输入数据的过程。我们展示了如何编写和读取 TFRecords,这是高效的 TensorFlow 文件格式。然后我们演示了队列、线程和相关功能,并在一个完整的工作示例中连接所有要点,展示了一个包括预处理、批处理和训练的图像数据的多线程输入管道。
输入管道
当处理可以存储在内存中的小数据集时,比如 MNIST 图像,将所有数据加载到内存中,然后使用 feeding 将数据推送到 TensorFlow 图中是合理的。然而,对于更大的数据集,这可能变得难以管理。处理这种情况的一个自然范式是将数据保留在磁盘上,并根据需要加载其中的块(比如用于训练的小批量),这样唯一的限制就是硬盘的大小。
此外,在实践中,一个典型的数据管道通常包括诸如读取具有不同格式的输入文件、更改输入的形状或结构、归一化或进行其他形式的预处理、对输入进行洗牌等步骤,甚至在训练开始之前。
这个过程的很多部分可以轻松地解耦并分解为模块化组件。例如,预处理不涉及训练,因此可以一次性对输入进行预处理,然后将其馈送到训练中。由于我们的训练无论如何都是批量处理示例,原则上我们可以在运行时处理输入批次,从磁盘中读取它们,应用预处理,然后将它们馈送到计算图中进行训练。
然而,这种方法可能是浪费的。因为预处理与训练无关,等待每个批次进行预处理会导致严重的 I/O 延迟,迫使每个训练步骤(急切地)等待加载和处理数据的小批量。更具可扩展性的做法是预取数据,并使用独立的线程进行加载和处理以及训练。但是,当需要重复读取和洗牌许多保存在磁盘上的文件时,这种做法可能变得混乱,并且需要大量的簿记和技术性来无缝运行。
值得注意的是,即使不考虑预处理,使用在前几章中看到的标准馈送机制(使用feed_dict)本身也是浪费的。feed_dict会将数据从 Python 运行时单线程复制到 TensorFlow 运行时,导致进一步的延迟和减速。我们希望通过某种方式直接将数据读取到本机 TensorFlow 中,避免这种情况。
为了让我们的生活更轻松(和更快),TensorFlow 提供了一套工具来简化这个输入管道过程。主要的构建模块是标准的 TensorFlow 文件格式,用于编码和解码这种格式的实用工具,数据队列和多线程。
我们将逐一讨论这些关键组件,探索它们的工作原理,并构建一个端到端的多线程输入管道。我们首先介绍 TFRecords,这是 TensorFlow 推荐的文件格式,以后会派上用场。
TFRecords
数据集当然可以采用许多格式,有时甚至是混合的(比如图像和音频文件)。将输入文件转换为一个统一的格式,无论其原始格式如何,通常是方便和有用的。TensorFlow 的默认标准数据格式是 TFRecord。TFRecord 文件只是一个包含序列化输入数据的二进制文件。序列化基于协议缓冲区(protobufs),简单地说,它通过使用描述数据结构的模式将数据转换为存储,独立于所使用的平台或语言(就像 XML 一样)。
在我们的设置中,使用 TFRecords(以及 protobufs/二进制文件)相比仅使用原始数据文件有许多优势。这种统一格式允许整洁地组织输入数据,所有相关属性都保持在一起,避免了许多目录和子目录的需求。TFRecord 文件实现了非常快速的处理。所有数据都保存在一个内存块中,而不是分别存储每个输入文件,从而减少了从内存读取数据所需的时间。还值得注意的是,TensorFlow 自带了许多针对 TFRecords 进行优化的实现和工具,使其非常适合作为多线程输入管道的一部分使用。
使用 TFRecordWriter 进行写入
我们首先将输入文件写入 TFRecord 格式,以便我们可以处理它们(在其他情况下,我们可能已经将数据存储在这种格式中)。在这个例子中,我们将 MNIST 图像转换为这种格式,但是相同的思想也适用于其他类型的数据。
首先,我们将 MNIST 数据下载到 save_dir,使用来自 tensorflow.contrib.learn 的实用函数:
from__future__importprint_functionimportosimporttensorflowastffromtensorflow.contrib.learn.python.learn.datasetsimportmnistsave_dir="*`path/to/mnist`*"# Download data to save_dirdata_sets=mnist.read_data_sets(save_dir,dtype=tf.uint8,reshape=False,validation_size=1000)
我们下载的数据包括训练、测试和验证图像,每个都在一个单独的 拆分 中。我们遍历每个拆分,将示例放入适当的格式,并使用 TFRecordWriter() 写入磁盘:
data_splits = ["train","test","validation"]
for d in range(len(data_splits)):
print("saving " + data_splits[d])
data_set = data_sets[d]
filename = os.path.join(save_dir, data_splits[d] + '.tfrecords')
writer = tf.python_io.TFRecordWriter(filename)
for index in range(data_set.images.shape[0]):
image = data_set.images[index].tostring()
example = tf.train.Example(features=tf.train.Features(feature={
'height': tf.train.Feature(int64_list=
tf.train.Int64List(value=
[data_set.images.shape[1]])),
'width': tf.train.Feature(int64_list=
tf.train.Int64List(value =
[data_set.images.shape[2]])),
'depth': tf.train.Feature(int64_list=
tf.train.Int64List(value =
[data_set.images.shape[3]])),
'label': tf.train.Feature(int64_list=
tf.train.Int64List(value =
[int(data_set.labels[index])])),
'image_raw': tf.train.Feature(bytes_list=
tf.train.BytesList(value =
[image]))}))
writer.write(example.SerializeToString())
writer.close()
让我们分解这段代码,以理解不同的组件。
我们首先实例化一个 TFRecordWriter 对象,给它一个对应数据拆分的路径:
filename = os.path.join(save_dir, data_splits[d] + '.tfrecords')
writer = tf.python_io.TFRecordWriter(filename)
然后我们遍历每个图像,将其从 NumPy 数组转换为字节字符串:
image = data_set.images[index].tostring()
接下来,我们将图像转换为它们的 protobuf 格式。tf.train.Example 是用于存储我们的数据的结构。Example 对象包含一个 Features 对象,它又包含一个从属性名称到 Feature 的映射。Feature 可以包含一个 Int64List、一个 BytesList 或一个 FloatList(这里没有使用)。例如,在这里我们对图像的标签进行编码:
tf.train.Feature(int64_list=tf.train.Int64List(value =
[int(data_set.labels[index])]))
这里是实际原始图像的编码:
tf.train.Feature(bytes_list=tf.train.BytesList(value =[image]))
让我们看看我们保存的数据是什么样子。我们使用 tf.python_io.tf_record_iterator 来实现这一点,这是一个从 TFRecords 文件中读取记录的迭代器:
filename = os.path.join(save_dir, 'train.tfrecords')
record_iterator = tf.python_io.tf_record_iterator(filename)
seralized_img_example= next(record_iterator)
serialized_img 是一个字节字符串。为了恢复保存图像到 TFRecord 时使用的结构,我们解析这个字节字符串,使我们能够访问我们之前存储的所有属性:
example = tf.train.Example()
example.ParseFromString(seralized_img_example)
image = example.features.feature['image_raw'].bytes_list.value
label = example.features.feature['label'].int64_list.value[0]
width = example.features.feature['width'].int64_list.value[0]
height = example.features.feature['height'].int64_list.value[0]
我们的图像也保存为字节字符串,因此我们将其转换回 NumPy 数组,并将其重新整形为形状为 (28,28,1) 的张量:
img_flat = np.fromstring(image[0], dtype=np.uint8)
img_reshaped = img_flat.reshape((height, width, -1))
这个基本示例应该让您了解 TFRecords 以及如何写入和读取它们。在实践中,我们通常希望将 TFRecords 读入一个预取数据队列作为多线程过程的一部分。在下一节中,我们首先介绍 TensorFlow 队列,然后展示如何将它们与 TFRecords 一起使用。
队列
TensorFlow 队列类似于普通队列,允许我们入队新项目,出队现有项目等。与普通队列的重要区别在于,就像 TensorFlow 中的任何其他内容一样,队列是计算图的一部分。它的操作像往常一样是符号化的,图中的其他节点可以改变其状态(就像变量一样)。这一点一开始可能会有点困惑,所以让我们通过一些示例来了解基本队列功能。
入队和出队
在这里,我们创建一个字符串的 先进先出(FIFO)队列,最多可以存储 10 个元素。由于队列是计算图的一部分,它们在会话中运行。在这个例子中,我们使用了一个 tf.InteractiveSession():
import tensorflow as tf
sess= tf.InteractiveSession()
queue1 = tf.FIFOQueue(capacity=10,dtypes=[tf.string])
在幕后,TensorFlow 为存储这 10 个项目创建了一个内存缓冲区。
就像 TensorFlow 中的任何其他操作一样,要向队列添加项目,我们创建一个操作:
enque_op = queue1.enqueue(["F"])
由于您现在已经熟悉了 TensorFlow 中计算图的概念,因此定义enque_op并不会向队列中添加任何内容——我们需要运行该操作。因此,如果我们在运行操作之前查看queue1的大小,我们会得到这个结果:
sess.run(queue1.size())
Out:
0
运行操作后,我们的队列现在有一个项目在其中:
enque_op.run()
sess.run(queue1.size())
Out:
1
让我们向queue1添加更多项目,并再次查看其大小:
enque_op = queue1.enqueue(["I"])
enque_op.run()
enque_op = queue1.enqueue(["F"])
enque_op.run()
enque_op = queue1.enqueue(["O"])
enque_op.run()
sess.run(queue1.size())
Out:
4
接下来,我们出队项目。出队也是一个操作,其输出评估为对应于出队项目的张量:
x = queue1.dequeue()
x.eval()
Out: b'F'
x.eval()
Out: b'I'
x.eval()
Out: b'F'
x.eval()
Out: b'O'
请注意,如果我们再次对空队列运行x.eval(),我们的主线程将永远挂起。正如我们将在本章后面看到的,实际上我们使用的代码知道何时停止出队并避免挂起。
另一种出队的方法是一次检索多个项目,使用dequeue_many()操作。此操作要求我们提前指定项目的形状:
queue1 = tf.FIFOQueue(capacity=10,dtypes=[tf.string],shapes=[()])
在这里,我们像以前一样填充队列,然后一次出队四个项目:
inputs = queue1.dequeue_many(4)
inputs.eval()
Out:
array([b'F', b'I', b'F', b'O'], dtype=object)
多线程
TensorFlow 会话是多线程的——多个线程可以使用同一个会话并行运行操作。单个操作具有并行实现,默认情况下使用多个 CPU 核心或 GPU 线程。然而,如果单个对sess.run()的调用没有充分利用可用资源,可以通过进行多个并行调用来提高吞吐量。例如,在典型情况下,我们可能有多个线程对图像进行预处理并将其推送到队列中,而另一个线程则从队列中拉取预处理后的图像进行训练(在下一章中,我们将讨论分布式训练,这在概念上是相关的,但有重要的区别)。
让我们通过一些简单的示例来介绍在 TensorFlow 中引入线程以及与队列的自然互动,然后在 MNIST 图像的完整示例中将所有内容连接起来。
我们首先创建一个容量为 100 个项目的 FIFO 队列,其中每个项目是使用tf.random_normal()生成的随机浮点数:
from __future__ import print_function
import threading
import time
gen_random_normal = tf.random_normal(shape=())
queue = tf.FIFOQueue(capacity=100,dtypes=[tf.float32],shapes=())
enque = queue.enqueue(gen_random_normal)
def add():
for i in range(10):
sess.run(enque)
再次注意,enque操作实际上并没有将随机数添加到队列中(它们尚未生成)在图执行之前。项目将使用我们创建的add()函数进行入队,该函数通过多次调用sess.run()向队列中添加 10 个项目。
接下来,我们创建 10 个线程,每个线程并行运行add(),因此每个线程异步地向队列中添加 10 个项目。我们可以(暂时)将这些随机数视为添加到队列中的训练数据:
threads = [threading.Thread(target=add, args=()) for i in range(10)]
threads
Out:
[<Thread(Thread-77, initial)>,
<Thread(Thread-78, initial)>,
<Thread(Thread-79, initial)>,
<Thread(Thread-80, initial)>,
<Thread(Thread-81, initial)>,
<Thread(Thread-82, initial)>,
<Thread(Thread-83, initial)>,
<Thread(Thread-84, initial)>,
<Thread(Thread-85, initial)>,
<Thread(Thread-86, initial)>]
我们已经创建了一个线程列表,现在我们执行它们,以短间隔打印队列的大小,从 0 增长到 100:
for t in threads:
t.start()
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
Out:
10
84
100
最后,我们使用dequeue_many()一次出队 10 个项目,并检查结果:
x = queue.dequeue_many(10)
print(x.eval())
sess.run(queue.size())
Out:
[ 0.05863889 0.61680967 1.05087686 -0.29185265 -0.44238046 0.53796548
-0.24784896 0.40672767 -0.88107938 0.24592835]
90
协调器和 QueueRunner
在现实场景中(正如我们将在本章后面看到的),有效地运行多个线程可能会更加复杂。线程应该能够正确停止(例如,避免“僵尸”线程,或者在一个线程失败时一起关闭所有线程),在停止后需要关闭队列,并且还有其他需要解决的技术但重要的问题。
TensorFlow 配备了一些工具来帮助我们进行这个过程。其中最重要的是tf.train.Coordinator,用于协调一组线程的终止,以及tf.train.QueueRunner,它简化了让多个线程与无缝协作地将数据入队的过程。
tf.train.Coordinator
我们首先演示如何在一个简单的玩具示例中使用tf.train.Coordinator。在下一节中,我们将看到如何将其作为真实输入管道的一部分使用。
我们使用与上一节类似的代码,修改add()函数并添加一个协调器:
gen_random_normal = tf.random_normal(shape=())
queue = tf.FIFOQueue(capacity=100,dtypes=[tf.float32],shapes=())
enque = queue.enqueue(gen_random_normal)
def add(coord,i):
while not coord.should_stop():
sess.run(enque)
if i == 11:
coord.request_stop()
coord = tf.train.Coordinator()
threads = [threading.Thread(target=add, args=(coord,i)) for i in range(10)]
coord.join(threads)
for t in threads:
t.start()
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
10
100
100
任何线程都可以调用coord.request_stop()来让所有其他线程停止。线程通常运行循环来检查是否停止,使用coord.should_stop()。在这里,我们将线程索引i传递给add(),并使用一个永远不满足的条件(i==11)来请求停止。因此,我们的线程完成了它们的工作,将全部 100 个项目添加到队列中。但是,如果我们将add()修改如下:
def add(coord,i):
while not coord.should_stop():
sess.run(enque)
if i == 1:
coord.request_stop()
然后线程i=1将使用协调器请求所有线程停止,提前停止所有入队操作:
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
time.sleep(0.01)
print(sess.run(queue.size()))
Out:
10
17
17
tf.train.QueueRunner 和 tf.RandomShuffleQueue
虽然我们可以创建多个重复运行入队操作的线程,但最好使用内置的tf.train.QueueRunner,它正是这样做的,同时在异常发生时关闭队列。
在这里,我们创建一个队列运行器,将并行运行四个线程以入队项目:
gen_random_normal = tf.random_normal(shape=())
queue = tf.RandomShuffleQueue(capacity=100,dtypes=[tf.float32],
min_after_dequeue=1)
enqueue_op = queue.enqueue(gen_random_normal)
qr = tf.train.QueueRunner(queue, [enqueue_op] * 4)
coord = tf.train.Coordinator()
enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
coord.request_stop()
coord.join(enqueue_threads)
请注意,qr.create_threads()将我们的会话作为参数,以及我们的协调器。
在这个例子中,我们使用了tf.RandomShuffleQueue而不是 FIFO 队列。RandomShuffleQueue只是一个带有以随机顺序弹出项目的出队操作的队列。这在训练使用随机梯度下降优化的深度神经网络时非常有用,这需要对数据进行洗牌。min_after_dequeue参数指定在调用出队操作后队列中将保留的最小项目数,更大的数字意味着更好的混合(随机抽样),但需要更多的内存。
一个完整的多线程输入管道
现在,我们将所有部分组合在一起,使用 MNIST 图像的工作示例,从将数据写入 TensorFlow 的高效文件格式,通过数据加载和预处理,到训练模型。我们通过在之前演示的排队和多线程功能的基础上构建,并在此过程中介绍一些更有用的组件来读取和处理 TensorFlow 中的数据。
首先,我们将 MNIST 数据写入 TFRecords,使用与本章开头使用的相同代码:
from__future__importprint_functionimportosimporttensorflowastffromtensorflow.contrib.learn.python.learn.datasetsimportmnistimportnumpyasnpsave_dir="*`path/to/mnist`*"# Download data to save_dirdata_sets=mnist.read_data_sets(save_dir,dtype=tf.uint8,reshape=False,validation_size=1000)data_splits=["train","test","validation"]fordinrange(len(data_splits)):print("saving "+data_splits[d])data_set=data_sets[d]filename=os.path.join(save_dir,data_splits[d]+'.tfrecords')writer=tf.python_io.TFRecordWriter(filename)forindexinrange(data_set.images.shape[0]):image=data_set.images[index].tostring()example=tf.train.Example(features=tf.train.Features(feature={'height':tf.train.Feature(int64_list=tf.train.Int64List(value=[data_set.images.shape[1]])),'width':tf.train.Feature(int64_list=tf.train.Int64List(value=[data_set.images.shape[2]])),'depth':tf.train.Feature(int64_list=tf.train.Int64List(value=[data_set.images.shape[3]])),'label':tf.train.Feature(int64_list=tf.train.Int64List(value=[int(data_set.labels[index])])),'image_raw':tf.train.Feature(bytes_list=tf.train.BytesList(value=[image]))}))writer.write(example.SerializeToString())writer.close()
tf.train.string_input_producer()和 tf.TFRecordReader()
tf.train.string_input_producer()只是在幕后创建一个QueueRunner,将文件名字符串输出到我们的输入管道的队列中。这个文件名队列将在多个线程之间共享:
filename = os.path.join(save_dir ,"train.tfrecords")
filename_queue = tf.train.string_input_producer(
[filename], num_epochs=10)
num_epochs参数告诉string_input_producer()将每个文件名字符串生成num_epochs次。
接下来,我们使用TFRecordReader()从这个队列中读取文件,该函数接受一个文件名队列并从filename_queue中逐个出队文件名。在内部,TFRecordReader()使用图的状态来跟踪正在读取的 TFRecord 的位置,因为它从磁盘加载输入数据的“块之后的块”:
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example(
serialized_example,
features={
'image_raw': tf.FixedLenFeature([], tf.string),
'label': tf.FixedLenFeature([], tf.int64),
})
tf.train.shuffle_batch()
我们解码原始字节字符串数据,进行(非常)基本的预处理将像素值转换为浮点数,然后使用tf.train.shuffle_batch()将图像实例洗牌并收集到batch_size批次中,该函数内部使用RandomShuffleQueue并累积示例,直到包含batch_size + min_after_dequeue个元素:
image = tf.decode_raw(features['image_raw'], tf.uint8)
image.set_shape([784])
image = tf.cast(image, tf.float32) * (1. / 255) - 0.5
label = tf.cast(features['label'], tf.int32)
# Randomly collect instances into batches
images_batch, labels_batch = tf.train.shuffle_batch(
[image, label], batch_size=128,
capacity=2000,
min_after_dequeue=1000)
capacity和min_after_dequeue参数的使用方式与之前讨论的相同。由shuffle_batch()返回的小批次是在内部创建的RandomShuffleQueue上调用dequeue_many()的结果。
tf.train.start_queue_runners()和总结
我们将简单的 softmax 分类模型定义如下:
W = tf.get_variable("W", [28*28, 10])
y_pred = tf.matmul(images_batch, W)
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y_pred,
labels=labels_batch)
loss_mean = tf.reduce_mean(loss)
train_op = tf.train.AdamOptimizer().minimize(loss)
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
init = tf.local_variables_initializer()
sess.run(init)
最后,我们通过调用tf.train.start_queue_runners()创建线程将数据入队到队列中。与其他调用不同,这个调用不是符号化的,实际上创建了线程(因此需要在初始化之后完成):
from __future__ import print_function
# Coordinator
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess,coord=coord)
让我们看一下创建的线程列表:
threads
Out:
[<Thread(Thread-483, stopped daemon 13696)>,
<Thread(Thread-484, started daemon 16376)>,
<Thread(Thread-485, started daemon 4320)>,
<Thread(Thread-486, started daemon 13052)>,
<Thread(Thread-487, started daemon 7216)>,
<Thread(Thread-488, started daemon 4332)>,
<Thread(Thread-489, started daemon 16820)>]
一切就绪后,我们现在准备运行多线程过程,从读取和预处理批次到将其放入队列再到训练模型。重要的是要注意,我们不再使用熟悉的feed_dict参数——这样可以避免数据复制并提供加速,正如本章前面讨论的那样:
try:
step = 0
while not coord.should_stop():
step += 1
sess.run([train_op])
if step%500==0:
loss_mean_val = sess.run([loss_mean])
print(step)
print(loss_mean_val)
except tf.errors.OutOfRangeError:
print('Done training for %d epochs, %d steps.' % (NUM_EPOCHS, step))
finally:
# When done, ask the threads to stop
coord.request_stop()
# Wait for threads to finish
coord.join(threads)
sess.close()
直到抛出tf.errors.OutOfRangeError错误,表示队列为空,我们已经完成训练:
Out:
Done training for 10 epochs, 2299500 steps.
未来的输入管道
在 2017 年中期,TensorFlow 开发团队宣布了 Dataset API,这是一个新的初步输入管道抽象,提供了一些简化和加速。本章介绍的概念,如 TFRecords 和队列,是 TensorFlow 及其输入管道过程的基础,仍然处于核心地位。TensorFlow 仍然在不断发展中,自然会不时发生令人兴奋和重要的变化。请参阅问题跟踪器进行持续讨论。
总结
在本章中,我们看到了如何在 TensorFlow 中使用队列和线程,以及如何创建一个多线程输入管道。这个过程可以帮助增加吞吐量和资源利用率。在下一章中,我们将进一步展示如何在分布式环境中使用 TensorFlow,在多个设备和机器之间进行工作。
第九章:分布式 TensorFlow
在本章中,我们讨论了使用 TensorFlow 进行分布式计算。我们首先简要调查了在机器学习中分布模型训练的不同方法,特别是深度学习。然后介绍了为支持分布式计算而设计的 TensorFlow 元素,最后通过一个端到端的示例将所有内容整合在一起。
分布式计算
分布式 计算,在最一般的术语中,意味着利用多个组件来执行所需的计算或实现目标。在我们的情况下,这意味着使用多台机器来加快深度学习模型的训练。
这背后的基本思想是通过使用更多的计算能力,我们应该能够更快地训练相同的模型。尽管通常情况下确实如此,但实际上更快多少取决于许多因素(即,如果您期望使用 10 倍资源并获得 10 倍加速,您很可能会感到失望!)。
在机器学习环境中有许多分布计算的方式。您可能希望利用多个设备,无论是在同一台机器上还是跨集群。在训练单个模型时,您可能希望在集群上计算梯度以加快训练速度,无论是同步还是异步。集群也可以用于同时训练多个模型,或者为单个模型搜索最佳参数。
在接下来的小节中,我们将详细介绍并行化的许多方面。
并行化发生在哪里?
在并行化类型的分类中,第一个分割是位置。我们是在单台机器上使用多个计算设备还是跨集群?
在一台机器上拥有强大的硬件与多个设备变得越来越普遍。云服务提供商(如亚马逊网络服务)现在提供这种类型的平台设置并准备就绪。
无论是在云端还是本地,集群配置在设计和演进方面提供了更多的灵活性,设置可以扩展到目前在同一板上使用多个设备所不可行的程度(基本上,您可以使用任意大小的集群)。
另一方面,虽然同一板上的几个设备可以使用共享内存,但集群方法引入了节点之间通信的时间成本。当需要共享的信息量很大且通信相对缓慢时,这可能成为一个限制因素。
并行化的目标是什么?
第二个分割是实际目标。我们是想使用更多硬件使相同的过程更快,还是为了并行化多个模型的训练?
在开发阶段经常需要训练多个模型,需要在模型或超参数之间做出选择。在这种情况下,通常会运行几个选项并选择表现最佳的一个。这样做是很自然的。
另一方面,当训练单个(通常是大型)模型时,可以使用集群来加快训练速度。在最常见的方法中,称为数据并行,每个计算设备上都存在相同的模型结构,每个副本上运行的数据是并行化的。
例如,当使用梯度下降训练深度学习模型时,该过程由以下步骤组成:
-
计算一批训练样本的梯度。
-
对梯度求和。
-
相应地对模型参数应用更新。
很明显,这个模式的第 1 步适合并行化。简单地使用多个设备计算梯度(针对不同的训练样本),然后在第 2 步中聚合结果并求和,就像常规情况下一样。
同步与异步数据并行
在刚才描述的过程中,来自不同训练示例的梯度被聚合在一起,以对模型参数进行单次更新。这就是所谓的同步训练,因为求和步骤定义了一个流必须等待所有节点完成梯度计算的点。
有一种情况可能更好地避免这种情况,即当异构计算资源一起使用时,因为同步选项意味着等待节点中最慢的节点。
异步选项是在每个节点完成为其分配的训练示例的梯度计算后独立应用更新步骤。
TensorFlow 元素
在本节中,我们将介绍在并行计算中使用的 TensorFlow 元素和概念。这不是完整的概述,主要作为本章结束的并行示例的介绍。
tf.app.flags
我们从一个与并行计算完全无关但对本章末尾的示例至关重要的机制开始。实际上,在 TensorFlow 示例中广泛使用flags机制,值得讨论。
实质上,tf.app.flags是 Python argparse模块的包装器,通常用于处理命令行参数,具有一些额外和特定的功能。
例如,考虑一个具有典型命令行参数的 Python 命令行程序:
'python distribute.py --job_name="ps" --task_index=0'
程序distribute.py传递以下内容:
job_name="ps"
task_index=0
然后在 Python 脚本中提取这些信息,使用:
tf.app.flags.DEFINE_string("job_name", "", "name of job")
tf.app.flags.DEFINE_integer("task_index", 0, "Index of task")
参数(字符串和整数)由命令行中的名称、默认值和参数描述定义。
flags机制允许以下类型的参数:
-
tf.app.flags.DEFINE_string定义一个字符串值。 -
tf.app.flags.DEFINE_boolean定义一个布尔值。 -
tf.app.flags.DEFINE_float定义一个浮点值。 -
tf.app.flags.DEFINE_integer定义一个整数值。
最后,tf.app.flags.FLAGS是一个结构,包含从命令行输入解析的所有参数的值。参数可以通过FLAGS.arg访问,或者在必要时通过字典FLAGS.__flags访问(然而,强烈建议使用第一种选项——它设计的方式)。
集群和服务器
一个 TensorFlow 集群只是参与计算图并行处理的节点(也称为任务)的集合。每个任务由其可以访问的网络地址定义。例如:
parameter_servers = ["localhost:2222"]
workers = ["localhost:2223",
"localhost:2224",
"localhost:2225"]
cluster = tf.train.ClusterSpec({"parameter_server": parameter_servers,
"worker": workers})
在这里,我们定义了四个本地任务(请注意,localhost:*XXXX*指向当前机器上端口XXXX,在多台计算机设置中,localhost将被 IP 地址替换)。任务分为一个参数服务器和三个工作节点。参数服务器/工作节点分配被称为作业。我们稍后在本章中进一步描述这些在训练期间的作用。
每个任务必须运行一个 TensorFlow 服务器,以便既使用本地资源进行实际计算,又与集群中的其他任务通信,以促进并行化。
基于集群定义,第一个工作节点上的服务器(即localhost:2223)将通过以下方式启动:
server = tf.train.Server(cluster,
job_name="worker",
task_index=0)
由Server()接收的参数让它知道自己的身份,以及集群中其他成员的身份和地址。
一旦我们有了集群和服务器,我们就构建计算图,这将使我们能够继续进行并行计算。
在设备之间复制计算图
如前所述,有多种方法可以进行并行训练。在“设备放置”中,我们简要讨论如何直接将操作放置在集群中特定任务上。在本节的其余部分,我们将介绍对于图间复制所必需的内容。
图间 复制 指的是常见的并行化模式,其中在每个 worker 任务上构建一个单独但相同的计算图。在训练期间,每个 worker 计算梯度,并由参数服务器组合,参数服务器还跟踪参数的当前版本,以及可能是训练的其他全局元素(如全局步骤计数器等)。
我们使用tf.train.replica_device_setter()来在每个任务上复制模型(计算图)。worker_device参数应该指向集群中当前任务。例如,在第一个 worker 上我们运行这个:
with tf.device(tf.train.replica_device_setter(
worker_device="/job:worker/task:%d" % 0,
cluster=cluster)):
# Build model...
例外是参数服务器,我们不在其上构建计算图。为了使进程不终止,我们使用:
server.join()
这将在并行计算的过程中保持参数服务器的运行。
管理的会话
在这一部分,我们将介绍我们将在模型的并行训练中使用的机制。首先,我们定义一个Supervisor:
sv = tf.train.Supervisor(is_chief=...,
logdir=...,
global_step=...,
init_op=...)
正如其名称所示,Supervisor用于监督训练,在并行设置中提供一些必要的实用程序。
传递了四个参数:
is_chief(布尔值)
必须有一个单一的chief,负责初始化等任务。
logdir(字符串)
存储日志的位置。
global_step
一个 TensorFlow 变量,将在训练期间保存当前的全局步骤。
init_op
一个用于初始化模型的 TensorFlow 操作,比如tf.global_variables_initializer()。
然后启动实际会话:
with sv.managed_session(server.target) as sess:
# Train ...
在这一点上,chief 将初始化变量,而所有其他任务等待这个过程完成。
设备放置
在本节中我们讨论的最终 TensorFlow 机制是设备放置。虽然这个主题的全部内容超出了本章的范围,但概述中没有提到这种能力是不完整的,这在工程高级系统时非常有用。
在具有多个计算设备(CPU、GPU 或这些组合)的环境中操作时,控制计算图中每个操作将发生的位置可能是有用的。这可能是为了更好地利用并行性,利用不同设备的不同能力,并克服某些设备的内存限制等限制。
即使您没有明确选择设备放置,TensorFlow 也会在需要时输出所使用的放置。这是在构建会话时启用的:
tf.Session(config=tf.ConfigProto(log_device_placement=True))
为了明确选择一个设备,我们使用:
with tf.device('/gpu:0'):
op = ...
'/gpu:0'指向系统上的第一个 GPU;同样,我们可以使用'/cpu:0'将操作放置在 CPU 上,或者在具有多个 GPU 设备的系统上使用'/gpu:X',其中X是我们想要使用的 GPU 的索引。
最后,跨集群的放置是通过指向特定任务来完成的。例如:
with tf.device("/job:worker/task:2"):
op = ...
这将分配给集群规范中定义的第二个worker任务。
跨 CPU 的放置
默认情况下,TensorFlow 使用系统上所有可用的 CPU,并在内部处理线程。因此,设备放置'/cpu:0'是完整的 CPU 功率,'/cpu:1'默认情况下不存在,即使在多 CPU 环境中也是如此。
为了手动分配到特定的 CPU(除非您有非常充分的理由这样做,否则让 TensorFlow 处理),必须使用指令定义一个会话来分离 CPU:
config = tf.ConfigProto(device_count={"CPU": 8},
inter_op_parallelism_threads=8,
intra_op_parallelism_threads=1)
sess = tf.Session(config=config)
在这里,我们定义了两个参数:
-
inter_op_parallelism_threads=8,意味着我们允许八个线程用于不同的操作 -
intra_op_parallelism_threads=1,表示每个操作都有一个线程
这些设置对于一个 8-CPU 系统是有意义的。
分布式示例
在本节中,我们将所有内容整合在一起,以端到端的方式展示了我们在第四章中看到的 MNIST CNN 模型的分布式训练示例。我们将使用一个参数服务器和三个工作任务。为了使其易于重现,我们将假设所有任务都在单台机器上本地运行(通过将localhost替换为 IP 地址,如前所述,可以轻松适应多机设置)。像往常一样,我们首先呈现完整的代码,然后将其分解为元素并加以解释:
import tensorflow as tf
from tensorflow.contrib import slim
from tensorflow.examples.tutorials.mnist import input_data
BATCH_SIZE = 50
TRAINING_STEPS = 5000
PRINT_EVERY = 100
LOG_DIR = "/tmp/log"
parameter_servers = ["localhost:2222"]
workers = ["localhost:2223",
"localhost:2224",
"localhost:2225"]
cluster = tf.train.ClusterSpec({"ps": parameter_servers, "worker": workers})
tf.app.flags.DEFINE_string("job_name", "", "'ps' / 'worker'")
tf.app.flags.DEFINE_integer("task_index", 0, "Index of task")
FLAGS = tf.app.flags.FLAGS
server = tf.train.Server(cluster,
job_name=FLAGS.job_name,
task_index=FLAGS.task_index)
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
def net(x):
x_image = tf.reshape(x, [-1, 28, 28, 1])
net = slim.layers.conv2d(x_image, 32, [5, 5], scope='conv1')
net = slim.layers.max_pool2d(net, [2, 2], scope='pool1')
net = slim.layers.conv2d(net, 64, [5, 5], scope='conv2')
net = slim.layers.max_pool2d(net, [2, 2], scope='pool2')
net = slim.layers.flatten(net, scope='flatten')
net = slim.layers.fully_connected(net, 500, scope='fully_connected')
net = slim.layers.fully_connected(net, 10, activation_fn=None,
scope='pred')
return net
if FLAGS.job_name == "ps":
server.join()
elif FLAGS.job_name == "worker":
with tf.device(tf.train.replica_device_setter(
worker_device="/job:worker/task:%d" % FLAGS.task_index,
cluster=cluster)):
global_step = tf.get_variable('global_step', [],
initializer=tf.constant_initializer(0),
trainable=False)
x = tf.placeholder(tf.float32, shape=[None, 784], name="x-input")
y_ = tf.placeholder(tf.float32, shape=[None, 10], name="y-input")
y = net(x)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(y, y_))
train_step = tf.train.AdamOptimizer(1e-4)\
.minimize(cross_entropy, global_step=global_step)
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
init_op = tf.global_variables_initializer()
sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0),
logdir=LOG_DIR,
global_step=global_step,
init_op=init_op)
with sv.managed_session(server.target) as sess:
step = 0
while not sv.should_stop() and step <= TRAINING_STEPS:
batch_x, batch_y = mnist.train.next_batch(BATCH_SIZE)
_, acc, step = sess.run([train_step, accuracy, global_step],
feed_dict={x: batch_x, y_: batch_y})
if step % PRINT_EVERY == 0:
print "Worker : {}, Step: {}, Accuracy (batch): {}".\
format(FLAGS.task_index, step, acc)
test_acc = sess.run(accuracy, feed_dict={x: mnist.test.images,
y_: mnist.test.labels})
print "Test-Accuracy: {}".format(test_acc)
sv.stop()
为了运行这个分布式示例,我们从四个不同的终端执行四个命令来分派每个任务(我们将很快解释这是如何发生的):
python distribute.py --job_name="ps" --task_index=0
python distribute.py --job_name="worker" --task_index=0
python distribute.py --job_name="worker" --task_index=1
python distribute.py --job_name="worker" --task_index=2
或者,以下将自动分派四个任务(取决于您使用的系统,输出可能全部发送到单个终端或四个单独的终端):
import subprocess
subprocess.Popen('python distribute.py --job_name="ps" --task_index=0',
shell=True)
subprocess.Popen('python distribute.py --job_name="worker" --task_index=0',
shell=True)
subprocess.Popen('python distribute.py --job_name="worker" --task_index=1',
shell=True)
subprocess.Popen('python distribute.py --job_name="worker" --task_index=2',
shell=True)
接下来,我们将检查前面示例中的代码,并突出显示这与我们迄今在书中看到的示例有何不同。
第一个块处理导入和常量:
import tensorflow as tf
from tensorflow.contrib import slim
from tensorflow.examples.tutorials.mnist import input_data
BATCH_SIZE = 50
TRAINING_STEPS = 5000
PRINT_EVERY = 100
LOG_DIR = "/tmp/log"
在这里我们定义:
BATCH_SIZE
在每个小批次训练中要使用的示例数。
TRAINING_STEPS
我们将在训练中使用的小批次总数。
PRINT_EVERY
打印诊断信息的频率。由于在我们使用的分布式训练中,所有任务都有一个当前步骤的计数器,因此在某个步骤上的print只会从一个任务中发生。
LOG_DIR
训练监督员将把日志和临时信息保存到此位置。在程序运行之间应该清空,因为旧信息可能导致下一个会话崩溃。
接下来,我们定义集群,如本章前面讨论的:
parameter_servers = ["localhost:2222"]
workers = ["localhost:2223",
"localhost:2224",
"localhost:2225"]
cluster = tf.train.ClusterSpec({"ps": parameter_servers, "worker": workers})
我们在本地运行所有任务。为了使用多台计算机,将localhost替换为正确的 IP 地址。端口 2222-2225 也是任意的,当然(但在使用单台机器时必须是不同的):在分布式设置中,您可能会在所有机器上使用相同的端口。
在接下来的内容中,我们使用tf.app.flags机制来定义两个参数,我们将通过命令行在每个任务调用程序时提供:
tf.app.flags.DEFINE_string("job_name", "", "'ps' / 'worker'")
tf.app.flags.DEFINE_integer("task_index", 0, "Index of task")
FLAGS = tf.app.flags.FLAGS
参数如下:
job_name
这将是'ps'表示单参数服务器,或者对于每个工作任务将是'worker'。
task_index
每种类型工作中任务的索引。因此,参数服务器将使用task_index = 0,而对于工作任务,我们将有0,1和2。
现在我们准备使用我们在本章中定义的集群中当前任务的身份来定义此当前任务的服务器。请注意,这将在我们运行的四个任务中的每一个上发生。这四个任务中的每一个都知道自己的身份(job_name,task_index),以及集群中其他每个人的身份(由第一个参数提供):
server = tf.train.Server(cluster,
job_name=FLAGS.job_name,
task_index=FLAGS.task_index)
在开始实际训练之前,我们定义我们的网络并加载要使用的数据。这类似于我们在以前的示例中所做的,所以我们不会在这里再次详细说明。为了简洁起见,我们使用 TF-Slim:
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
def net(x):
x_image = tf.reshape(x, [-1, 28, 28, 1])
net = slim.layers.conv2d(x_image, 32, [5, 5], scope='conv1')
net = slim.layers.max_pool2d(net, [2, 2], scope='pool1')
net = slim.layers.conv2d(net, 64, [5, 5], scope='conv2')
net = slim.layers.max_pool2d(net, [2, 2], scope='pool2')
net = slim.layers.flatten(net, scope='flatten')
net = slim.layers.fully_connected(net, 500, scope='fully_connected')
net = slim.layers.fully_connected(net, 10, activation_fn=None, scope='pred')
return net
在训练期间要执行的实际处理取决于任务的类型。对于参数服务器,我们希望机制主要是为参数提供服务。这包括等待请求并处理它们。要实现这一点,只需要这样做:
if FLAGS.job_name == "ps":
server.join()
服务器的.join()方法即使在所有其他任务终止时也不会终止,因此一旦不再需要,必须在外部终止此进程。
在每个工作任务中,我们定义相同的计算图:
with tf.device(tf.train.replica_device_setter(
worker_device="/job:worker/task:%d" % FLAGS.task_index,
cluster=cluster)):
global_step = tf.get_variable('global_step', [],
initializer=tf.constant_initializer(0),
trainable=False)
x = tf.placeholder(tf.float32, shape=[None, 784], name="x-input")
y_ = tf.placeholder(tf.float32, shape=[None, 10], name="y-input")
y = net(x)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(y, y_))
train_step = tf.train.AdamOptimizer(1e-4)\
.minimize(cross_entropy, global_step=global_step)
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
init_op = tf.global_variables_initializer()
我们使用tf.train.replica_device_setter()来指定这一点,这意味着 TensorFlow 变量将通过参数服务器进行同步(这是允许我们进行分布式计算的机制)。
global_step变量将保存跨任务训练期间的总步数(每个步骤索引只会出现在一个任务上)。这样可以创建一个时间线,以便我们始终知道我们在整个计划中的位置,从每个任务分开。
其余的代码是我们在整本书中已经看过的许多示例中看到的标准设置。
接下来,我们设置一个Supervisor和一个managed_session:
sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0),
logdir=LOG_DIR,
global_step=global_step,
init_op=init_op)
with sv.managed_session(server.target) as sess:
这类似于我们在整个过程中使用的常规会话,只是它能够处理分布式的一些方面。变量的初始化将仅在一个任务中完成(通过is_chief参数指定的首席任务;在我们的情况下,这将是第一个工作任务)。所有其他任务将等待这个任务完成,然后继续。
会话开启后,我们开始训练:
while not sv.should_stop() and step <= TRAINING_STEPS:
batch_x, batch_y = mnist.train.next_batch(BATCH_SIZE)
_, acc, step = sess.run([train_step, accuracy, global_step],
feed_dict={x: batch_x, y_: batch_y})
if step % PRINT_EVERY == 0:
print "Worker : {}, Step: {}, Accuracy (batch): {}".\
format(FLAGS.task_index, step, acc)
每隔PRINT_EVERY步,我们打印当前小批量的当前准确率。这将很快达到 100%。例如,前两行可能是:
Worker : 1, Step: 0.0, Accuracy (batch): 0.140000000596
Worker : 0, Step: 100.0, Accuracy (batch): 0.860000014305
最后,我们运行测试准确率:
test_acc = sess.run(accuracy,
feed_dict={x: mnist.test.images, y_: mnist.test.labels})
print "Test-Accuracy: {}".format(test_acc)
请注意,这将在每个工作任务上执行,因此相同的输出将出现三次。为了节省计算资源,我们可以只在一个任务中运行这个(例如,只在第一个工作任务中)。
总结
在本章中,我们涵盖了关于深度学习和机器学习中并行化的主要概念,并以一个关于数据并行化集群上分布式训练的端到端示例结束。
分布式训练是一个非常重要的工具,既可以加快训练速度,也可以训练那些否则不可行的模型。在下一章中,我们将介绍 TensorFlow 的 Serving 功能,允许训练好的模型在生产环境中被利用。
第十章:使用 TensorFlow 导出和提供模型
在本章中,我们将学习如何使用简单和高级的生产就绪方法保存和导出模型。对于后者,我们介绍了 TensorFlow Serving,这是 TensorFlow 中最实用的用于创建生产环境的工具之一。我们将从快速概述两种简单的保存模型和变量的方法开始:首先是通过手动保存权重并重新分配它们,然后是使用Saver类创建训练检查点以及导出我们的模型。最后,我们将转向更高级的应用程序,通过使用 TensorFlow Serving 在服务器上部署我们的模型。
保存和导出我们的模型
到目前为止,我们已经学习了如何使用 TensorFlow 创建、训练和跟踪模型。现在我们将学习如何保存训练好的模型。保存当前权重状态对于明显的实际原因至关重要——我们不想每次都从头开始重新训练模型,我们也希望有一种方便的方式与他人分享我们模型的状态(就像我们在第七章中看到的预训练模型一样)。
在这一部分,我们将讨论保存和导出的基础知识。我们首先介绍了一种简单的保存和加载权重到文件的方法。然后我们将看到如何使用 TensorFlow 的Saver对象来保持序列化模型检查点,其中包含有关权重状态和构建图的信息。
分配加载的权重
在训练后重复使用权重的一个天真但实用的方法是将它们保存到文件中,稍后可以加载它们并重新分配给模型。
让我们看一些例子。假设我们希望保存用于 MNIST 数据的基本 softmax 模型的权重,我们从会话中获取它们后,将权重表示为 NumPy 数组,并以我们选择的某种格式保存它们:
import numpy as np
weights = sess.run(W)
np.savez(os.path.join(path, 'weight_storage'), weights)
鉴于我们构建了完全相同的图,我们可以加载文件并使用会话中的.assign()方法将加载的权重值分配给相应的变量:
loaded_w = np.load(path + 'weight_storage.npz')
loaded_w = loaded_w.items()[0][1]
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,
labels=y_true))
gd_step = tf.train.GradientDescentOptimizer(0.5)\
.minimize(cross_entropy)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
with tf.Session() as sess:
# Assigning loaded weights
sess.run(W.assign(loaded_w))
acc = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
print("Accuracy: {}".format(acc))
Out:
Accuracy: 0.9199
接下来,我们将执行相同的过程,但这次是针对第四章中用于 MNIST 数据的 CNN 模型。在这里,我们有八组不同的权重:两个卷积层 1 和 2 的滤波器权重及其对应的偏置,以及两组全连接层的权重和偏置。我们将模型封装在一个类中,以便方便地保持这八个参数的更新列表。
我们还为要加载的权重添加了可选参数:
if weights is not None and sess is not None:
self.load_weights(weights, sess)
以及在传递权重时分配其值的函数:
def load_weights(self, weights, sess):
for i,w in enumerate(weights):
print("Weight index: {}".format(i),
"Weight shape: {}".format(w.shape))
sess.run(self.parameters[i].assign(w))
在整个过程中:
class simple_cnn:
def __init__(self, x_image,keep_prob, weights=None, sess=None):
self.parameters = []
self.x_image = x_image
conv1 = self.conv_layer(x_image, shape=[5, 5, 1, 32])
conv1_pool = self.max_pool_2x2(conv1)
conv2 = self.conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = self.max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 7*7*64])
full_1 = tf.nn.relu(self.full_layer(conv2_flat, 1024))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
self.y_conv = self.full_layer(full1_drop, 10)
if weights is not None and sess is not None:
self.load_weights(weights, sess)
def weight_variable(self,shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial,name='weights')
def bias_variable(self,shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial,name='biases')
def conv2d(self,x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1],
padding='SAME')
def max_pool_2x2(self,x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def conv_layer(self,input, shape):
W = self.weight_variable(shape)
b = self.bias_variable([shape[3]])
self.parameters += [W, b]
return tf.nn.relu(self.conv2d(input, W) + b)
def full_layer(self,input, size):
in_size = int(input.get_shape()[1])
W = self.weight_variable([in_size, size])
b = self.bias_variable([size])
self.parameters += [W, b]
return tf.matmul(input, W) + b
def load_weights(self, weights, sess):
for i,w in enumerate(weights):
print("Weight index: {}".format(i),
"Weight shape: {}".format(w.shape))
sess.run(self.parameters[i].assign(w))
在这个例子中,模型已经训练好,并且权重已保存为cnn_weights。我们加载权重并将它们传递给我们的 CNN 对象。当我们在测试数据上运行模型时,它将使用预训练的权重:
x = tf.placeholder(tf.float32, shape=[None, 784])
x_image = tf.reshape(x, [-1, 28, 28, 1])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
sess = tf.Session()
weights = np.load(path + 'cnn_weight_storage.npz')
weights = weights.items()[0][1]
cnn = simple_cnn(x_image, keep_prob, weights, sess)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits=cnn.y_conv,
labels=y_))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(cnn.y_conv, 1),
tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
X = data.test.images.reshape(10, 1000, 784)
Y = data.test.labels.reshape(10, 1000, 10)
test_accuracy = np.mean([sess.run(accuracy,
feed_dict={x:X[i], y_:Y[i],keep_prob:1.0})
for i in range(10)])
sess.close()
print("test accuracy: {}".format(test_accuracy))
Out:
Weight index: 0 Weight shape: (5, 5, 1, 32)
Weight index: 1 Weight shape: (32,)
Weight index: 2 Weight shape: (5, 5, 32, 64)
Weight index: 3 Weight shape: (64,)
Weight index: 4 Weight shape: (3136, 1024)
Weight index: 5 Weight shape: (1024,)
Weight index: 6 Weight shape: (1024, 10)
Weight index: 7 Weight shape: (10,)
test accuracy: 0.990100026131
我们可以获得高准确度,而无需重新训练。
Saver 类
TensorFlow 还有一个内置的类,我们可以用于与前面的示例相同的目的,提供额外有用的功能,我们很快就会看到。这个类被称为Saver类(在第五章中已经简要介绍过)。
Saver添加了操作,允许我们通过使用称为检查点文件的二进制文件保存和恢复模型的参数,将张量值映射到变量的名称。与前一节中使用的方法不同,这里我们不必跟踪我们的参数——Saver会自动为我们完成。
使用Saver非常简单。我们首先通过tf.train.Saver()创建一个 saver 实例,指示我们希望保留多少最近的变量检查点,以及可选的保留它们的时间间隔。
例如,在下面的代码中,我们要求只保留最近的七个检查点,并且另外指定每半小时保留一个检查点(这对于性能和进展评估分析可能很有用):
saver = tf.train.Saver(max_to_keep=7,
keep_checkpoint_every_n_hours=0.5)
如果没有给出输入,那么默认情况下会保留最后五个检查点,并且every_n_hours功能会被有效地禁用(默认设置为10000)。
接下来,我们使用saver实例的.save()方法保存检查点文件,传递会话参数、文件保存路径以及步数(global_step),它会自动连接到每个检查点文件的名称中,表示迭代次数。在训练模型时,这会创建不同步骤的多个检查点。
在这个代码示例中,每 50 个训练迭代将在指定目录中保存一个文件:
DIR="*`path/to/model`*"withtf.Session()assess:forstepinrange(1,NUM_STEPS+1):batch_xs,batch_ys=data.train.next_batch(MINIBATCH_SIZE)sess.run(gd_step,feed_dict={x:batch_xs,y_true:batch_ys})ifstep%50==0:saver.save(sess,os.path.join(DIR,"model"),global_step=step)
另一个保存的文件名为checkpoint包含保存的检查点列表,以及最近检查点的路径:
model_checkpoint_path: "model_ckpt-1000"
all_model_checkpoint_paths: "model_ckpt-700"
all_model_checkpoint_paths: "model_ckpt-750"
all_model_checkpoint_paths: "model_ckpt-800"
all_model_checkpoint_paths: "model_ckpt-850"
all_model_checkpoint_paths: "model_ckpt-900"
all_model_checkpoint_paths: "model_ckpt-950"
all_model_checkpoint_paths: "model_ckpt-1000"
在下面的代码中,我们使用Saver保存权重的状态:
fromtensorflow.examples.tutorials.mnistimportinput_dataDATA_DIR='/tmp/data'data=input_data.read_data_sets(DATA_DIR,one_hot=True)NUM_STEPS=1000MINIBATCH_SIZE=100DIR="*`path/to/model`*"x=tf.placeholder(tf.float32,[None,784],name='x')W=tf.Variable(tf.zeros([784,10]),name='W')y_true=tf.placeholder(tf.float32,[None,10])y_pred=tf.matmul(x,W)cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,labels=y_true))gd_step=tf.train.GradientDescentOptimizer(0.5)\ .minimize(cross_entropy)correct_mask=tf.equal(tf.argmax(y_pred,1),tf.argmax(y_true,1))accuracy=tf.reduce_mean(tf.cast(correct_mask,tf.float32))saver=tf.train.Saver(max_to_keep=7,keep_checkpoint_every_n_hours=1)withtf.Session()assess:sess.run(tf.global_variables_initializer())forstepinrange(1,NUM_STEPS+1):batch_xs,batch_ys=data.train.next_batch(MINIBATCH_SIZE)sess.run(gd_step,feed_dict={x:batch_xs,y_true:batch_ys})ifstep%50==0:saver.save(sess,os.path.join(DIR,"model_ckpt"),global_step=step)ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))Out:Accuracy:90.87%
现在我们只需使用saver.restore()为相同的图模型恢复我们想要的检查点,权重会自动分配给模型:
tf.reset_default_graph()
x = tf.placeholder(tf.float32, [None, 784],name='x')
W = tf.Variable(tf.zeros([784, 10]),name='W')
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,
labels=y_true))
gd_step = tf.train.GradientDescentOptimizer(0.5)\
.minimize(cross_entropy)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
saver = tf.train.Saver()
with tf.Session() as sess:
saver.restore(sess, os.path.join(DIR,"model_ckpt-1000"))
ans = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
print("Accuracy: {:.4}%".format(ans*100))
Out:
Accuracy: 90.87%
在恢复之前重置图
加载的变量需要与当前图中的变量配对,因此应该具有匹配的名称。如果由于某种原因名称不匹配,那么可能会出现类似于这样的错误:
NotFoundError: Key W_1 not found in checkpoint
[[Node: save/RestoreV2_2 = RestoreV2[
dtypes=[DT_FLOAT], _device="/job:localhost/replica:0
/task:0/cpu:0"](_recv_save/Const_1_0, save/RestoreV2_2
/tensor_names, save/RestoreV2_2/shape_and_slices)]]
如果名称被一些旧的、无关紧要的图使用,就会发生这种情况。通过使用tf.reset_default_graph()命令重置图,您可以解决这个问题。
到目前为止,在这两种方法中,我们需要重新创建图以重新分配恢复的参数。然而,Saver还允许我们恢复图而无需重建它,通过生成包含有关图的所有必要信息的.meta检查点文件。
关于图的信息以及如何将保存的权重合并到其中(元信息)被称为MetaGraphDef。这些信息被序列化——转换为一个字符串——使用协议缓冲区(参见“序列化和协议缓冲区”),它包括几个部分。网络架构的信息保存在graph_def中。
这里是图信息的文本序列化的一个小样本(更多关于序列化的内容将在后面介绍):
meta_info_def {
stripped_op_list {
op {
name: "ApplyGradientDescent"
input_arg {
name: "var"
type_attr: "T"
is_ref: true
}
input_arg {
name: "alpha"
type_attr: "T"
}...
graph_def {
node {
name: "Placeholder"
op: "Placeholder"
attr {
key: "_output_shapes"
value {
list {
shape {
dim {
size: -1
}
dim {
size: 784
}
}
}
}
}...
为了加载保存的图,我们使用tf.train.import_meta_graph(),传递我们想要的检查点文件的名称(带有.meta扩展名)。TensorFlow 已经知道如何处理恢复的权重,因为这些信息也被保存了:
tf.reset_default_graph()DIR="*`path/to/model`*"withtf.Session()assess:saver=tf.train.import_meta_graph(os.path.join(DIR,"model_ckpt-1000.meta"))saver.restore(sess,os.path.join(DIR,"model_ckpt-1000"))ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))
然而,仅仅导入图并恢复权重是不够的,会导致错误。原因是导入模型并恢复权重并不会给我们额外访问在运行会话时使用的变量(fetches和feed_dict的键)——模型不知道输入和输出是什么,我们希望计算什么度量等等。
解决这个问题的一种方法是将它们保存在一个集合中。集合是一个类似于字典的 TensorFlow 对象,我们可以以有序、可访问的方式保存我们的图组件。
在这个例子中,我们希望访问度量accuracy(我们希望获取)和 feed 键x和y_true。我们在将模型保存为train_var的名称之前将它们添加到一个集合中:
train_var = [x,y_true,accuracy]
tf.add_to_collection('train_var', train_var[0])
tf.add_to_collection('train_var', train_var[1])
tf.add_to_collection('train_var', train_var[2])
如所示,saver.save()方法会自动保存图结构以及权重的检查点。我们还可以使用saver.export_meta.graph()显式保存图,然后添加一个集合(作为第二个参数传递):
train_var = [x,y_true,accuracy]
tf.add_to_collection('train_var', train_var[0])
tf.add_to_collection('train_var', train_var[1])
tf.add_to_collection('train_var', train_var[2])
saver = tf.train.Saver(max_to_keep=7,
keep_checkpoint_every_n_hours=1)
saver.export_meta_graph(os.path.join(DIR,"model_ckpt.meta")
,collection_list=['train_var'])
现在我们从集合中检索图,从中可以提取所需的变量:
tf.reset_default_graph()DIR="*`path/to/model`*"withtf.Session()assess:sess.run(tf.global_variables_initializer())saver=tf.train.import_meta_graph(os.path.join(DIR,"model_ckpt.meta")saver.restore(sess,os.path.join(DIR,"model_ckpt-1000"))x=tf.get_collection('train_var')[0]y_true=tf.get_collection('train_var')[1]accuracy=tf.get_collection('train_var')[2]ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))Out:Accuracy:91.4%
在定义图形时,请考虑一旦图形已保存和恢复,您想要检索哪些变量/操作,例如前面示例中的准确性操作。在下一节中,当我们谈论 Serving 时,我们将看到它具有内置功能,可以引导导出的模型,而无需像我们在这里做的那样保存变量。
TensorFlow Serving 简介
TensorFlow Serving 是用 C++编写的高性能服务框架,我们可以在生产环境中部署我们的模型。通过使客户端软件能够访问它并通过 Serving 的 API 传递输入,使我们的模型可以用于生产(图 10-1)。当然,TensorFlow Serving 旨在与 TensorFlow 模型无缝集成。Serving 具有许多优化功能,可减少延迟并增加预测的吞吐量,适用于实时、大规模应用。这不仅仅是关于预测的可访问性和高效服务,还涉及灵活性——通常希望出于各种原因保持模型更新,例如获得额外的训练数据以改进模型,对网络架构进行更改等。

图 10-1。Serving 将我们训练好的模型链接到外部应用程序,使客户端软件可以轻松访问。
概述
假设我们运行一个语音识别服务,并且我们希望使用 TensorFlow Serving 部署我们的模型。除了优化服务外,对我们来说定期更新模型也很重要,因为我们获取更多数据或尝试新的网络架构。稍微更技术化一点,我们希望能够加载新模型并提供其输出,卸载旧模型,同时简化模型生命周期管理和版本策略。
一般来说,我们可以通过以下方式实现 Serving。在 Python 中,我们定义模型并准备将其序列化,以便可以被负责加载、提供和管理版本的不同模块解析。Serving 的核心“引擎”位于一个 C++模块中,只有在我们希望控制 Serving 行为的特定调整和定制时才需要访问它。
简而言之,这就是 Serving 架构的工作方式(图 10-2):
-
一个名为
Source的模块通过监视插入的文件系统来识别需要加载的新模型,这些文件系统包含我们在创建时导出的模型及其相关信息。Source包括子模块,定期检查文件系统并确定最新相关的模型版本。 -
当它识别到新的模型版本时,source会创建一个loader。加载器将其servables(客户端用于执行计算的对象,如预测)传递给manager。根据版本策略(渐进式发布、回滚版本等),管理器处理可服务内容的完整生命周期(加载、卸载和提供)。
-
最后,管理器提供了一个接口,供客户端访问可服务的内容。

图 10-2。Serving 架构概述。
Serving 的设计特别之处在于它具有灵活和可扩展的特性。它支持构建各种插件来定制系统行为,同时使用其他核心组件的通用构建。
在下一节中,我们将使用 Serving 构建和部署一个 TensorFlow 模型,展示一些其关键功能和内部工作原理。在高级应用中,我们可能需要控制不同类型的优化和定制;例如,控制版本策略等。在本章中,我们将向您展示如何开始并理解 Serving 的基础知识,为生产就绪的部署奠定基础。
安装
Serving 需要安装一些组件,包括一些第三方组件。安装可以从源代码或使用 Docker 进行,我们在这里使用 Docker 来让您快速开始。Docker 容器将软件应用程序与运行所需的一切(例如代码、文件等)捆绑在一起。我们还使用 Bazel,谷歌自己的构建工具,用于构建客户端和服务器软件。在本章中,我们只简要介绍了 Bazel 和 Docker 等工具背后的技术细节。更全面的描述出现在书末的附录中。
安装 Serving
Docker 安装说明可以在 Docker 网站上找到。
在这里,我们演示使用Ubuntu进行 Docker 设置。
Docker 容器是从本地 Docker 镜像创建的,该镜像是从 dockerfile 构建的,并封装了我们需要的一切(依赖安装、项目代码等)。一旦我们安装了 Docker,我们需要下载 TensorFlow Serving 的 dockerfile。
这个 dockerfile 包含了构建 TensorFlow Serving 所需的所有依赖项。
首先,我们生成镜像,然后可以运行容器(这可能需要一些时间):
docker build --pull -t $USER/tensorflow-serving-devel -f
Dockerfile.devel .
现在我们在本地机器上创建了镜像,我们可以使用以下命令创建和运行容器:
docker run -v $HOME/docker_files:/host_files
-p 80:80 -it $USER/tensorflow-serving-devel
docker run -it $USER/tensorflow-serving-devel命令足以创建和运行容器,但我们对此命令进行了两次添加。
首先,我们添加-v $HOME/home_dir:/docker_dir,其中-v(卷)表示请求共享文件系统,这样我们就可以方便地在 Docker 容器和主机之间传输文件。在这里,我们在主机上创建了共享文件夹docker_files,在我们的 Docker 容器上创建了host_files。另一种传输文件的方法是简单地使用命令docker cp foo.txt *mycontainer*:/foo.txt。第二个添加是-p <*host port*>:<*container port*>,这使得容器中的服务可以通过指定的端口暴露在任何地方。
一旦我们输入我们的run命令,一个容器将被创建和启动,并且一个终端将被打开。我们可以使用命令docker ps -a(在 Docker 终端之外)查看我们容器的状态。请注意,每次使用docker run命令时,我们都会创建另一个容器;要进入现有容器的终端,我们需要使用docker exec -it <*container id*> bash。
最后,在打开的终端中,我们克隆并配置 TensorFlow Serving:
git clone --recurse-submodules https://github.com/tensorflow/serving
cd serving/tensorflow
./configure
就是这样,我们准备好了!
构建和导出
现在 Serving 已经克隆并运行,我们可以开始探索其功能和如何使用它。克隆的 TensorFlow Serving 库是按照 Bazel 架构组织的。Bazel 构建的源代码组织在一个工作区目录中,里面有一系列分组相关源文件的包。每个包都有一个BUILD文件,指定从该包内的文件构建的输出。
我们克隆库中的工作区位于/serving文件夹中,包含WORKSPACE文本文件和/tensorflow_serving包,稍后我们将返回到这里。
现在我们转向查看处理训练和导出模型的 Python 脚本,并看看如何以一种适合进行 Serving 的方式导出我们的模型。
导出我们的模型
与我们使用Saver类时一样,我们训练的模型将被序列化并导出到两个文件中:一个包含有关变量的信息,另一个包含有关图形和其他元数据的信息。正如我们很快将看到的,Serving 需要特定的序列化格式和元数据,因此我们不能简单地使用Saver类,就像我们在本章开头看到的那样。
我们要采取的步骤如下:
-
像前几章一样定义我们的模型。
-
创建一个模型构建器实例。
-
在构建器中定义我们的元数据(模型、方法、输入和输出等)以序列化格式(称为
SignatureDef)。 -
使用构建器保存我们的模型。
首先,我们通过使用 Serving 的 SavedModelBuilder 模块创建一个构建器实例,传递我们希望将文件导出到的位置(如果目录不存在,则将创建)。SavedModelBuilder 导出表示我们的模型的序列化文件,格式如下所需:
builder = saved_model_builder.SavedModelBuilder(export_path)
我们需要的序列化模型文件将包含在一个目录中,该目录的名称将指定模型及其版本:
export_path_base = sys.argv[-1]
export_path = os.path.join(
compat.as_bytes(export_path_base),
compat.as_bytes(str(FLAGS.model_version)))
这样,每个版本将被导出到一个具有相应路径的不同子目录中。
请注意,export_path_base 是从命令行输入的,使用 sys.argv 获取,版本作为标志保留(在上一章中介绍)。标志解析由 tf.app.run() 处理,我们很快就会看到。
接下来,我们要定义输入(图的输入张量的形状)和输出(预测张量)签名。在本章的第一部分中,我们使用 TensorFlow 集合对象来指定输入和输出数据之间的关系及其相应的占位符,以及用于计算预测和准确性的操作。在这里,签名起到了类似的作用。
我们使用创建的构建器实例添加变量和元图信息,使用 SavedModelBuilder.add_meta_graph_and_variables() 方法:
builder.add_meta_graph_and_variables(
sess, [tag_constants.SERVING],
signature_def_map={
'predict_images':
prediction_signature,
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
classification_signature,
},
legacy_init_op=legacy_init_op)
我们需要传递四个参数:会话、标签(用于“服务”或“训练”)、签名映射和一些初始化。
我们传递一个包含预测和分类签名的字典。我们从预测签名开始,可以将其视为在 TensorFlow 集合中指定和保存预测操作,就像我们之前看到的那样:
prediction_signature = signature_def_utils.build_signature_def(
inputs={'images': tensor_info_x},
outputs={'scores': tensor_info_y},
method_name=signature_constants.PREDICT_METHOD_NAME)
这里的 images 和 scores 是我们稍后将用来引用我们的 x 和 y 张量的任意名称。通过以下命令将图像和分数编码为所需格式:
tensor_info_x = utils.build_tensor_info(x)
tensor_info_y = utils.build_tensor_info(y_conv)
与预测签名类似,我们有分类签名,其中我们输入关于分数(前 k 个类的概率值)和相应类的信息:
# Build the signature_def_map
classification_inputs = utils.build_tensor_info(
serialized_tf_example)
classification_outputs_classes = utils.build_tensor_info(
prediction_classes)
classification_outputs_scores = utils.build_tensor_info(values)
classification_signature = signature_def_utils.build_signature_def(
inputs={signature_constants.CLASSIFY_INPUTS:
classification_inputs},
outputs={
signature_constants.CLASSIFY_OUTPUT_CLASSES:
classification_outputs_classes,
signature_constants.CLASSIFY_OUTPUT_SCORES:
classification_outputs_scores
},
method_name=signature_constants.CLASSIFY_METHOD_NAME)
最后,我们使用 save() 命令保存我们的模型:
builder.save()
简而言之,将所有部分整合在一起,以准备在脚本执行时序列化和导出,我们将立即看到。
以下是我们主要的 Python 模型脚本的最终代码,包括我们的模型(来自第四章的 CNN 模型):
import os
import sys
import tensorflow as tf
from tensorflow.python.saved_model import builder
as saved_model_builder
from tensorflow.python.saved_model import signature_constants
from tensorflow.python.saved_model import signature_def_utils
from tensorflow.python.saved_model import tag_constants
from tensorflow.python.saved_model import utils
from tensorflow.python.util import compat
from tensorflow_serving.example import mnist_input_data
tf.app.flags.DEFINE_integer('training_iteration', 10,
'number of training iterations.')
tf.app.flags.DEFINE_integer(
'model_version', 1, 'version number of the model.')
tf.app.flags.DEFINE_string('work_dir', '/tmp', 'Working directory.')
FLAGS = tf.app.flags.FLAGS
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial,dtype='float')
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial,dtype='float')
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def main(_):
if len(sys.argv) < 2 or sys.argv[-1].startswith('-'):
print('Usage: mnist_export.py [--training_iteration=x] '
'[--model_version=y] export_dir')
sys.exit(-1)
if FLAGS.training_iteration <= 0:
print('Please specify a positive
value for training iteration.')
sys.exit(-1)
if FLAGS.model_version <= 0:
print ('Please specify a positive
value for version number.')
sys.exit(-1)
print('Training...')
mnist = mnist_input_data.read_data_sets(
FLAGS.work_dir, one_hot=True)
sess = tf.InteractiveSession()
serialized_tf_example = tf.placeholder(
tf.string, name='tf_example')
feature_configs = {'x': tf.FixedLenFeature(shape=[784],
dtype=tf.float32),}
tf_example = tf.parse_example(serialized_tf_example,
feature_configs)
x = tf.identity(tf_example['x'], name='x')
y_ = tf.placeholder('float', shape=[None, 10])
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
x_image = tf.reshape(x, [-1,28,28,1])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
y = tf.nn.softmax(y_conv, name='y')
cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4)\
.minimize(cross_entropy)
values, indices = tf.nn.top_k(y_conv, 10)
prediction_classes = tf.contrib.lookup.index_to_string(
tf.to_int64(indices),
mapping=tf.constant([str(i) for i in xrange(10)]))
sess.run(tf.global_variables_initializer())
for _ in range(FLAGS.training_iteration):
batch = mnist.train.next_batch(50)
train_step.run(feed_dict={x: batch[0],
y_: batch[1], keep_prob: 0.5})
print(_)
correct_prediction = tf.equal(tf.argmax(y_conv,1),
tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
y_: mnist.test.labels})
print('training accuracy %g' % accuracy.eval(feed_dict={
x: mnist.test.images,
y_: mnist.test.labels, keep_prob: 1.0}))
print('training is finished!')
export_path_base = sys.argv[-1]
export_path = os.path.join(
compat.as_bytes(export_path_base),
compat.as_bytes(str(FLAGS.model_version)))
print 'Exporting trained model to', export_path
builder = saved_model_builder.SavedModelBuilder(export_path)
classification_inputs = utils.build_tensor_info(
serialized_tf_example)
classification_outputs_classes = utils.build_tensor_info(
prediction_classes)
classification_outputs_scores = utils.build_tensor_info(values)
classification_signature = signature_def_utils.build_signature_def(
inputs={signature_constants.CLASSIFY_INPUTS:
classification_inputs},
outputs={
signature_constants.CLASSIFY_OUTPUT_CLASSES:
classification_outputs_classes,
signature_constants.CLASSIFY_OUTPUT_SCORES:
classification_outputs_scores
},
method_name=signature_constants.CLASSIFY_METHOD_NAME)
tensor_info_x = utils.build_tensor_info(x)
tensor_info_y = utils.build_tensor_info(y_conv)
prediction_signature = signature_def_utils.build_signature_def(
inputs={'images': tensor_info_x},
outputs={'scores': tensor_info_y},
method_name=signature_constants.PREDICT_METHOD_NAME)
legacy_init_op = tf.group(tf.initialize_all_tables(),
name='legacy_init_op')
builder.add_meta_graph_and_variables(
sess, [tag_constants.SERVING],
signature_def_map={
'predict_images':
prediction_signature,
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
classification_signature,
},
legacy_init_op=legacy_init_op)
builder.save()
print('new model exported!')
if __name__ == '__main__':
tf.app.run()
tf.app.run() 命令为我们提供了一个很好的包装器,用于处理解析命令行参数。
在我们介绍 Serving 的最后部分中,我们使用 Bazel 实际导出和部署我们的模型。
大多数 Bazel BUILD 文件仅包含构建规则的声明,指定输入和输出之间的关系,以及构建输出的步骤。
例如,在这个 BUILD 文件中,我们有一个 Python 规则 py_binary 用于构建可执行程序。这里有三个属性,name 用于规则的名称,srcs 用于处理以创建目标(我们的 Python 脚本)的文件列表,deps 用于链接到二进制目标中的其他库的列表:
py_binary(
name = "serving_model_ch4",
srcs = [
"serving_model_ch4.py",
],
deps = [
":mnist_input_data",
"@org_tensorflow//tensorflow:tensorflow_py",
"@org_tensorflow//tensorflow/python/saved_model:builder",
"@org_tensorflow//tensorflow/python/saved_model:constants",
"@org_tensorflow//tensorflow/python/saved_model:loader",
"@org_tensorflow//tensorflow/python/saved_model:
signature_constants",
"@org_tensorflow//tensorflow/python/saved_model:
signature_def_utils",
"@org_tensorflow//tensorflow/python/saved_model:
tag_constants",
"@org_tensorflow//tensorflow/python/saved_model:utils",
],
)
接下来,我们使用 Bazel 运行和导出模型,进行 1,000 次迭代训练并导出模型的第一个版本:
bazel build //tensorflow_serving/example:serving_model_ch4
bazel-bin/tensorflow_serving/example/serving_model_ch4
--training_iteration=1000 --model_version=1 /tmp/mnist_model
要训练模型的第二个版本,我们只需使用:
--model_version=2
在指定的子目录中,我们将找到两个文件,saved_model.pb 和 variables,它们包含有关我们的图(包括元数据)和其变量的序列化信息。在接下来的行中,我们使用标准的 TensorFlow 模型服务器加载导出的模型:
bazel build //tensorflow_serving/model_servers:
tensorflow_model_server
bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server
--port=8000 --model_name=mnist
--model_base_path=/tmp/mnist_model/ --logtostderr
最后,我们的模型现在已经被提供并准备在 localhost:8000 上运行。我们可以使用一个简单的客户端实用程序 mnist_client 来测试服务器:
bazel build //tensorflow_serving/example:mnist_client
bazel-bin/tensorflow_serving/example/mnist_client
--num_tests=1000 --server=localhost:8000
总结
本章讨论了如何保存、导出和提供模型,从简单保存和重新分配权重使用内置的Saver实用程序到用于生产的高级模型部署机制。本章的最后部分涉及 TensorFlow Serving,这是一个非常好的工具,可以通过动态版本控制使我们的模型商业化准备就绪。Serving 是一个功能丰富的实用程序,具有许多功能,我们强烈建议对掌握它感兴趣的读者在网上寻找更深入的技术资料。
附录 A. 模型构建和使用 TensorFlow Serving 的提示
模型结构化和定制化
在这个简短的部分中,我们将专注于两个主题,这些主题延续并扩展了前几章——如何构建一个合适的模型,以及如何定制模型的实体。我们首先描述如何通过使用封装来有效地重构我们的代码,并允许其变量被共享和重复使用。在本节的第二部分,我们将讨论如何定制我们自己的损失函数和操作,并将它们用于优化。
模型结构化
最终,我们希望设计我们的 TensorFlow 代码高效,以便可以重用于多个任务,并且易于跟踪和传递。使事情更清晰的一种方法是使用可用的 TensorFlow 扩展库之一,这些库在第七章中已经讨论过。然而,虽然它们非常适合用于典型的网络,但有时我们希望实现的具有新组件的模型可能需要较低级别 TensorFlow 的完全灵活性。
让我们再次看一下前一章的优化代码:
import tensorflow as tf
NUM_STEPS = 10
g = tf.Graph()
wb_ = []
with g.as_default():
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
with tf.name_scope('inference') as scope:
w = tf.Variable([[0,0,0]],dtype=tf.float32,name='weights')
b = tf.Variable(0,dtype=tf.float32,name='bias')
y_pred = tf.matmul(w,tf.transpose(x)) + b
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
with tf.name_scope('train') as scope:
learning_rate = 0.5
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
train = optimizer.minimize(loss)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(NUM_STEPS):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
print(10, sess.run([w,b]))
我们得到:
(0, [array([[ 0.30149955, 0.49303722, 0.11409992]],
dtype=float32), -0.18563795])
(5, [array([[ 0.30094019, 0.49846715, 0.09822173]],
dtype=float32), -0.19780949])
(10, [array([[ 0.30094025, 0.49846718, 0.09822182]],
dtype=float32), -0.19780946])
这里的整个代码只是简单地一行一行堆叠。对于简单和专注的示例来说,这是可以的。然而,这种编码方式有其局限性——当代码变得更加复杂时,它既不可重用也不太可读。
让我们放大视野,思考一下我们的基础设施应该具有哪些特征。首先,我们希望封装模型,以便可以用于各种任务,如训练、评估和形成预测。此外,以模块化的方式构建模型可能更有效,使我们能够对其子组件具有特定控制,并增加可读性。这将是接下来几节的重点。
模块化设计
一个很好的开始是将代码分成捕捉学习模型中不同元素的函数。我们可以这样做:
def predict(x,y_true,w,b):
y_pred = tf.matmul(w,tf.transpose(x)) + b
return y_pred
def get_loss(y_pred,y_true):
loss = tf.reduce_mean(tf.square(y_true-y_pred))
return loss
def get_optimizer(y_pred,y_true):
loss = get_loss(y_pred,y_true)
optimizer = tf.train.GradientDescentOptimizer(0.5)
train = optimizer.minimize(loss)
return train
def run_model(x_data,y_data):
wb_ = []
# Define placeholders and variables
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
w = tf.Variable([[0,0,0]],dtype=tf.float32)
b = tf.Variable(0,dtype=tf.float32)
print(b.name)
# Form predictions
y_pred = predict(x,y_true,w,b)
# Create optimizer
train = get_optimizer(y_pred,y_data)
# Run session
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(10):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
run_model(x_data,y_data)
run_model(x_data,y_data)
这里是结果:
Variable_9:0 Variable_8:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), -0.20805186]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), -0.20003076]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), -0.20003042]
Variable_11:0 Variable_10:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), -0.20805186]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), -0.20003076]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), -0.20003042]
现在我们可以重复使用具有不同输入的代码,这种划分使其更易于阅读,特别是在变得更加复杂时。
在这个例子中,我们两次调用了主函数并打印了创建的变量。请注意,每次调用都会创建不同的变量集,从而创建了四个变量。例如,假设我们希望构建一个具有多个输入的模型,比如两个不同的图像。假设我们希望将相同的卷积滤波器应用于两个输入图像。将创建新的变量。为了避免这种情况,我们可以“共享”滤波器变量,在两个图像上使用相同的变量。
变量共享
通过使用tf.get_variable()而不是tf.Variable(),可以重复使用相同的变量。我们使用方式与tf.Variable()非常相似,只是需要将初始化器作为参数传递:
w = tf.get_variable('w',[1,3],initializer=tf.zeros_initializer())
b = tf.get_variable('b',[1,1],initializer=tf.zeros_initializer())
在这里,我们使用了tf.zeros_initializer()。这个初始化器与tf.zeros()非常相似,只是它不会将形状作为参数,而是根据tf.get_variable()指定的形状排列值。
在这个例子中,变量w将被初始化为[0,0,0],如给定的形状[1,3]所指定。
使用get_variable(),我们可以重复使用具有相同名称的变量(包括作用域前缀,可以通过tf.variable_scope()设置)。但首先,我们需要通过使用tf.variable_scope.reuse_variable()或设置reuse标志(tf.variable.scope(reuse=True))来表明这种意图。下面的代码示例展示了如何共享变量。
标志误用的注意事项
每当一个变量与另一个变量具有完全相同的名称时,在未设置reuse标志时会抛出异常。相反的情况也是如此——期望重用的名称不匹配的变量(当reuse=True时)也会导致异常。
使用这些方法,并将作用域前缀设置为Regression,通过打印它们的名称,我们可以看到相同的变量被重复使用:
def run_model(x_data,y_data):
wb_ = []
# Define placeholders and variables
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
w = tf.get_variable('w',[1,3],initializer=tf.zeros_initializer())
b = tf.get_variable('b',[1,1],initializer=tf.zeros_initializer())
print(b.name,w.name)
# Form predictions
y_pred = predict(x,y_true,w,b)
# Create optimizer
train = get_optimizer(y_pred,y_data)
# Run session
init = tf.global_variables_initializer()
sess.run(init)
for step in range(10):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
sess = tf.Session()
with tf.variable_scope("Regression") as scope:
run_model(x_data,y_data)
scope.reuse_variables()
run_model(x_data,y_data)
sess.close()
输出如下所示:
Regression/b:0 Regression/w:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), array([[-0.20805186]], dtype=float32)]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), array([[-0.20003076]], dtype=float32)]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), array([[-0.20003042]], dtype=float32)]
Regression/b:0 Regression/w:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), array([[-0.20805186]], dtype=float32)]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), array([[-0.20003076]], dtype=float32)]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), array([[-0.20003042]], dtype=float32)]
tf.get_variables()是一个简洁、轻量级的共享变量的方法。另一种方法是将我们的模型封装为一个类,并在那里管理变量。这种方法有许多其他好处,如下一节所述
类封装
与任何其他程序一样,当事情变得更加复杂,代码行数增加时,将我们的 TensorFlow 代码放在一个类中变得非常方便,这样我们就可以快速访问属于同一模型的方法和属性。类封装允许我们维护变量的状态,然后执行各种训练后任务,如形成预测、模型评估、进一步训练、保存和恢复权重,以及与我们的模型解决的特定问题相关的任何其他任务。
在下一批代码中,我们看到一个简单的类包装器示例。当实例化时创建模型,并通过调用fit()方法执行训练过程。
@property 和 Python 装饰器
这段代码使用了@property装饰器。装饰器只是一个以另一个函数作为输入的函数,对其进行一些操作(比如添加一些功能),然后返回它。在 Python 中,装饰器用@符号定义。
@property是一个用于处理类属性访问的装饰器。
我们的类包装器如下:
class Model:
def __init__(self):
# Model
self.x = tf.placeholder(tf.float32,shape=[None,3])
self.y_true = tf.placeholder(tf.float32,shape=None)
self.w = tf.Variable([[0,0,0]],dtype=tf.float32)
self.b = tf.Variable(0,dtype=tf.float32)
init = tf.global_variables_initializer()
self.sess = tf.Session()
self.sess.run(init)
self._output = None
self._optimizer = None
self._loss = None
def fit(self,x_data,y_data):
print(self.b.name)
for step in range(10):
self.sess.run(self.optimizer,{self.x: x_data, self.y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step, self.sess.run([self.w,self.b]))
@property
def output(self):
if not self._output:
y_pred = tf.matmul(self.w,tf.transpose(self.x)) + self.b
self._output = y_pred
return self._output
@property
def loss(self):
if not self._loss:
error = tf.reduce_mean(tf.square(self.y_true-self.output))
self._loss= error
return self._loss
@property
def optimizer(self):
if not self._optimizer:
opt = tf.train.GradientDescentOptimizer(0.5)
opt = opt.minimize(self.loss)
self._optimizer = opt
return self._optimizer
lin_reg = Model()
lin_reg.fit(x_data,y_data)
lin_reg.fit(x_data,y_data)
然后我们得到这个:
Variable_89:0
0 [array([[ 0.32110521, 0.4908163 , 0.09833425]],
dtype=float32), -0.18784374]
4 [array([[ 0.30250472, 0.49442694, 0.10041162]],
dtype=float32), -0.1999902]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_89:0
0 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
4 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
将代码拆分为函数在某种程度上是多余的,因为相同的代码行在每次调用时都会重新计算。一个简单的解决方案是在每个函数的开头添加一个条件。在下一个代码迭代中,我们将看到一个更好的解决方法。
在这种情况下,不需要使用变量共享,因为变量被保留为模型对象的属性。此外,在调用两次训练方法model.fit()后,我们看到变量保持了它们的当前状态。
在本节的最后一批代码中,我们添加了另一个增强功能,创建一个自定义装饰器,自动检查函数是否已被调用。
我们可以做的另一个改进是将所有变量保存在字典中。这将使我们能够在每次操作后跟踪我们的变量,就像我们在第十章中看到的那样,当我们查看保存权重和模型时。
最后,添加了用于获取损失函数值和权重的额外函数:
class Model:
def __init__(self):
# Model
self.x = tf.placeholder(tf.float32,shape=[None,3])
self.y_true = tf.placeholder(tf.float32,shape=None)
self.params = self._initialize_weights()
init = tf.global_variables_initializer()
self.sess = tf.Session()
self.sess.run(init)
self.output
self.optimizer
self.loss
def _initialize_weights(self):
params = dict()
params['w'] = tf.Variable([[0,0,0]],dtype=tf.float32)
params['b'] = tf.Variable(0,dtype=tf.float32)
return params
def fit(self,x_data,y_data):
print(self.params['b'].name)
for step in range(10):
self.sess.run(self.optimizer,{self.x: x_data, self.y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step,
self.sess.run([self.params['w'],self.params['b']]))
def evaluate(self,x_data,y_data):
print(self.params['b'].name)
MSE = self.sess.run(self.loss,{self.x: x_data, self.y_true: y_data})
return MSE
def getWeights(self):
return self.sess.run([self.params['b']])
@property_with_check
def output(self):
y_pred = tf.matmul(self.params['w'],tf.transpose(self.x)) + \
self.params['b']
return y_pred
@property_with_check
def loss(self):
error = tf.reduce_mean(tf.square(self.y_true-self.output))
return error
@property_with_check
def optimizer(self):
opt = tf.train.GradientDescentOptimizer(0.5)
opt = opt.minimize(self.loss)
return opt
lin_reg = Model()
lin_reg.fit(x_data,y_data)
MSE = lin_reg.evaluate(x_data,y_data)
print(MSE)
print(lin_reg.getWeights())
以下是输出:
Variable_87:0
0 [array([[ 0.32110521, 0.4908163 , 0.09833425]],
dtype=float32), -0.18784374]
4 [array([[ 0.30250472, 0.49442694, 0.10041162]],
dtype=float32), -0.1999902]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_87:0
0 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
4 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_87:0
0.0102189
[-0.19999036]
自定义装饰器检查属性是否存在,如果不存在,则根据输入函数设置它。否则,返回属性。使用functools.wrap(),这样我们就可以引用函数的名称:
import functools
def property_with_check(input_fn):
attribute = '_cache_' + input_fn.__name__
@property
@functools.wraps(input_fn)
def check_attr(self):
if not hasattr(self, attribute):
setattr(self, attribute, input_fn(self))
return getattr(self, attribute)
return check_attr
这是一个相当基本的示例,展示了我们如何改进模型的整体代码。这种优化可能对我们简单的线性回归示例来说有些过度,但对于具有大量层、变量和特征的复杂模型来说,这绝对是值得的努力。
定制
到目前为止,我们使用了两个损失函数。在第二章中的分类示例中,我们使用了交叉熵损失,定义如下:
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y_true))
相比之下,在前一节的回归示例中,我们使用了平方误差损失,定义如下:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
这些是目前在机器学习和深度学习中最常用的损失函数。本节的目的是双重的。首先,我们想指出 TensorFlow 在利用自定义损失函数方面的更一般能力。其次,我们将讨论正则化作为任何损失函数的扩展形式,以实现特定目标,而不考虑使用的基本损失函数。
自制损失函数
本书(以及我们的读者)以深度学习为重点来看待 TensorFlow。然而,TensorFlow 的范围更广泛,大多数机器学习问题都可以以一种 TensorFlow 可以解决的方式来表述。此外,任何可以在计算图框架中表述的计算都是从 TensorFlow 中受益的好候选。
主要特例是无约束优化问题类。这些问题在科学(和算法)计算中非常常见,对于这些问题,TensorFlow 尤为有用。这些问题突出的原因是,TensorFlow 提供了计算梯度的自动机制,这为解决这类问题的开发时间提供了巨大的加速。
一般来说,对于任意损失函数的优化将采用以下形式
def my_loss_function(key-variables...):
loss = ...
return loss
my_loss = my_loss_function(key-variables...)
gd_step = tf.train.GradientDescentOptimizer().minimize(my_loss)
任何优化器都可以用于替代 GradientDescentOptimizer。
正则化
正则化是通过对解决方案的复杂性施加惩罚来限制优化问题(有关更多详细信息,请参见第四章中的注释)。在本节中,我们将看一下特定情况下,惩罚直接添加到基本损失函数中的附加形式。
例如,基于第二章中 softmax 示例,我们有以下内容:
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y_true))
total_loss = cross_entropy + LAMBDA * tf.nn.l2_loss(W)
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(total_loss)
与第二章中的原始版本的区别在于,我们将 LAMBDA * tf.nn.l2_loss(W) 添加到我们正在优化的损失中。在这种情况下,使用较小的权衡参数 LAMBDA 值对最终准确性的影响很小(较大的值会有害)。在大型网络中,过拟合是一个严重问题,这种正则化通常可以拯救一命。
这种类型的正则化可以针对模型的权重进行,如前面的示例所示(也称为权重衰减,因为它会使权重值变小),也可以针对特定层或所有层的激活进行。
另一个因素是我们使用的函数——我们可以使用 l1 而不是 l2 正则化,或者两者的组合。所有这些正则化器的组合都是有效的,并在各种情境中使用。
许多抽象层使正则化的应用变得简单,只需指定滤波器数量或激活函数即可。例如,在 Keras(在第七章中审查的非常流行的扩展)中,我们提供了适用于所有标准层的正则化器,列在表 A-1 中。
表 A-1. 使用 Keras 进行正则化
| 正则化器 | 作用 | 示例 |
|---|---|---|
l1 |
l1 正则化权重 |
Dense(100, W_regularizer=l1(0.01))
|
l2 |
l2 正则化权重 |
|---|
Dense(100, W_regularizer=l2(0.01))
|
l1l2 |
组合 l1 + l2 正则化权重 |
|---|
Dense(100, W_regularizer=l1l2(0.01))
|
activity_l1 |
l1 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l1(0.01))
|
activity_l2 |
l2 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l2(0.01))
|
activity_l1l2 |
组合 l1 + l2 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l1l2(0.01))
|
在模型过拟合时,使用这些快捷方式可以轻松测试不同的正则化方案。
编写自己的操作
TensorFlow 预装了大量本地操作,从标准算术和逻辑操作到矩阵操作、深度学习特定函数等等。当这些操作不够时,可以通过创建新操作来扩展系统。有两种方法可以实现这一点:
-
编写一个“从头开始”的 C++ 版本的操作
-
编写结合现有操作和 Python 代码创建新操作的 Python 代码
我们将在本节的其余部分讨论第二个选项。
构建 Python op 的主要原因是在 TensorFlow 计算图的上下文中利用 NumPy 功能。为了说明,我们将使用 NumPy 乘法函数构建前一节中的正则化示例,而不是 TensorFlow op:
import numpy as np
LAMBDA = 1e-5
def mul_lambda(val):
return np.multiply(val, LAMBDA).astype(np.float32)
请注意,这是为了说明的目的,没有特别的原因让任何人想要使用这个而不是原生的 TensorFlow op。我们使用这个过度简化的例子是为了将焦点转移到机制的细节而不是计算上。
为了在 TensorFlow 内部使用我们的新创建,我们使用py_func()功能:
tf.py_func(my_python_function, [input], [output_types])
在我们的情况下,这意味着我们计算总损失如下:
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
然而,这样做还不够。回想一下,TensorFlow 会跟踪每个 op 的梯度,以便对我们整体模型进行基于梯度的训练。为了使这个与新的基于 Python 的 op 一起工作,我们必须手动指定梯度。这分为两个步骤。
首先,我们创建并注册梯度:
@tf.RegisterGradient("PyMulLambda")
def grad_mul_lambda(op, grad):
return LAMBDA*grad
接下来,在使用函数时,我们将这个函数指定为 op 的梯度。这是使用在上一步中注册的字符串完成的:
with tf.get_default_graph().gradient_override_map({"PyFunc": "PyMulLambda"}):
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
将所有内容放在一起,通过我们基于新 Python op 的正则化 softmax 模型的代码现在是:
import numpy as np
import tensorflow as tf
LAMBDA = 1e-5
def mul_lambda(val):
return np.multiply(val, LAMBDA).astype(np.float32)
@tf.RegisterGradient("PyMulLambda")
def grad_mul_lambda(op, grad):
return LAMBDA*grad
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy =
tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits\
(logits=y_pred, labels=y_true))
with tf.get_default_graph().gradient_override_map({"PyFunc": "PyMulLambda"}):
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(total_loss)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
现在可以使用与第二章中首次介绍该模型时相同的代码进行训练。
在计算梯度时使用输入
在我们刚刚展示的简单示例中,梯度仅取决于相对于输入的梯度,而不是输入本身。在一般情况下,我们还需要访问输入。这很容易做到,使用op.inputs 字段:
x = op.inputs[0]
其他输入(如果存在)以相同的方式访问。
TensorFlow Serving 所需和推荐的组件
在本节中,我们添加了一些在第十章中涵盖的材料的细节,并更深入地审查了 TensorFlow Serving 背后使用的一些技术组件。
在第十章中,我们使用 Docker 来运行 TensorFlow Serving。那些喜欢避免使用 Docker 容器的人需要安装以下内容:
Bazel
Bazel 是谷歌自己的构建工具,最近才公开。当我们使用术语构建时,我们指的是使用一堆规则从源代码中创建输出软件,以非常高效和可靠的方式。构建过程还可以用来引用构建输出所需的外部依赖项。除了其他语言,Bazel 还可以用于构建 C++应用程序,我们利用这一点来构建用 C++编写的 TensorFlow Serving 程序。Bazel 构建的源代码基于一个工作区目录,其中包含一系列包含相关源文件的嵌套层次结构的包。每个软件包包含三种类型的文件:人工编写的源文件称为targets,从源文件创建的生成文件,以及指定从输入派生输出的步骤的规则。
每个软件包都有一个BUILD文件,指定从该软件包内的文件构建的输出。我们使用基本的 Bazel 命令,比如bazel build来从目标构建生成的文件,以及bazel run来执行构建规则。当我们想要指定包含构建输出的目录时,我们使用-bin标志。
下载和安装说明可以在Bazel 网站上找到。
gRPC
远程过程调用(RPC)是一种客户端(调用者)-服务器(执行者)交互的形式;程序可以请求在另一台计算机上执行的过程(例如,一个方法)(通常在共享网络中)。gRPC 是由 Google 开发的开源框架。与任何其他 RPC 框架一样,gRPC 允许您直接调用其他机器上的方法,从而更容易地分发应用程序的计算。gRPC 的伟大之处在于它如何处理序列化,使用快速高效的协议缓冲区而不是 XML 或其他方法。
下载和安装说明可以在GitHub上找到。
接下来,您需要确保使用以下命令安装了 Serving 所需的依赖项:
sudo apt-get update && sudo apt-get install -y \
build-essential \
curl \
libcurl3-dev \
git \
libfreetype6-dev \
libpng12-dev \
libzmq3-dev \
pkg-config \
python-dev \
python-numpy \
python-pip \
software-properties-common \
swig \
zip \
zlib1g-dev
最后,克隆 Serving:
git clone --recurse-submodules https://github.com/tensorflow/serving
cd serving
如第十章所示,另一个选择是使用 Docker 容器,实现简单干净的安装。
什么是 Docker 容器,为什么我们要使用它?
Docker 本质上解决了与 VirtualBox 的 Vagrant 相同的问题,即确保我们的代码在其他机器上能够顺利运行。不同的机器可能具有不同的操作系统以及不同的工具集(安装的软件、配置、权限等)。通过复制相同的环境——也许是为了生产目的,也许只是与他人分享——我们保证我们的代码在其他地方与在原始开发机器上运行的方式完全相同。
Docker 的独特之处在于,与其他类似用途的工具不同,它不会创建一个完全操作的虚拟机来构建环境,而是在现有系统(例如 Ubuntu)之上创建一个容器,在某种意义上充当虚拟机,并利用我们现有的操作系统资源。这些容器是从本地 Docker 镜像创建的,该镜像是从dockerfile构建的,并封装了我们需要的一切(依赖安装、项目代码等)。从该镜像中,我们可以创建任意数量的容器(当然,直到内存用尽为止)。这使得 Docker 成为一个非常酷的工具,我们可以轻松地创建包含我们的代码的完整多个环境副本,并在任何地方运行它们(对于集群计算非常有用)。
一些基本的 Docker 命令
为了让您更加熟悉使用 Docker,这里简要介绍一些有用的命令,以最简化的形式编写。假设我们已经准备好一个 dockerfile,我们可以使用docker build <*dockerfile*>来构建一个镜像。然后,我们可以使用docker run <*image*>命令创建一个新的容器。该命令还将自动运行容器并打开一个终端(输入exit关闭终端)。要运行、停止和删除现有容器,我们分别使用docker start <*container id*>、docker stop <*container id*>和docker rm <*container id*>命令。要查看所有实例的列表,包括正在运行和空闲的实例,我们输入docker ps -a。
当我们运行一个实例时,我们可以添加-p标志,后面跟一个端口供 Docker 暴露,以及-v标志,后面跟一个要挂载的主目录,这将使我们能够在本地工作(主目录通过容器中的/mnt/home路径进行访问)。


浙公网安备 33010602011771号