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.”
如果您觉得您对代码示例的使用超出了合理使用范围或上述许可,请随时通过[email protected]与我们联系。
致谢
作者要感谢为这本书提供反馈意见的审阅者: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 中实现它。在下一章中,我们将深入探讨在计算机视觉中使用的更高级的深度神经网络。
标签:sess,示例,lrn,merge,使用,tf,TensorFlow,我们 From: https://www.cnblogs.com/apachecn/p/18011867