tf-dl-merge-0

509 阅读1小时+

Tensorflow 深度学习(一)

原文:Tensorflow for deep learning

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书将通过 TensorFlow 向您介绍机器学习的基础知识。TensorFlow 是谷歌的新软件库,用于深度学习,使工程师能够设计和部署复杂的深度学习架构变得简单。您将学习如何使用 TensorFlow 构建能够检测图像中的对象、理解人类文本以及预测潜在药物属性的系统。此外,您将直观地了解 TensorFlow 作为执行张量计算的系统的潜力,并学习如何将 TensorFlow 用于传统机器学习范围之外的任务。

重要的是,《TensorFlow for Deep Learning》是为从业者编写的首批深度学习书籍之一。它通过实际示例教授基本概念,并从基础开始建立对机器学习基础的理解。本书的目标读者是实践开发人员,他们擅长设计软件系统,但不一定擅长创建学习系统。有时我们会使用一些基本的线性代数和微积分,但我们将复习所有必要的基础知识。我们还预计我们的书将对熟悉脚本编写但不一定擅长设计学习算法的科学家和其他专业人士有所帮助。

本书中使用的约定

本书中使用以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽

用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

等宽粗体

显示用户应直接输入的命令或其他文本。

等宽斜体

显示应由用户提供值或由上下文确定值替换的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般说明。

警告

此元素表示警告或注意事项。

使用代码示例

可下载补充材料(代码示例、练习等)位于https://github.com/matroid/dlwithtf

本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们请求许可。例如,编写一个使用本书中几个代码块的程序不需要许可。出售或分发包含 O’Reilly 图书示例的 CD-ROM 需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码整合到产品文档中需要许可。

我们感谢,但不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“TensorFlow for Deep Learning by Bharath Ramsundar and Reza Bosagh Zadeh (O’Reilly). Copyright 2018 Reza Zadeh, Bharath Ramsundar, 978-1-491-98045-3.”

如果您觉得您使用的代码示例超出了合理使用范围或上述给出的许可,请随时通过permissions@oreilly.com与我们联系。

致谢

Bharath 感谢他的博士导师在晚上和周末让他工作在这本书上,并特别感谢他的家人在整个过程中给予的大力支持。

Reza 感谢开源社区,许多软件和计算机科学都是基于这些社区。开源软件是人类知识的最大集中之一,没有整个社区的支持,这本书是不可能的。

第一章:深度学习简介

深度学习已经彻底改变了技术行业。现代机器翻译、搜索引擎和计算机助手都是由深度学习驱动的。随着深度学习将其影响扩展到机器人技术、制药业、能源以及当代技术的所有其他领域,这一趋势只会继续发展。对于现代软件专业人员来说,发展对深度学习原理的工作知识正在迅速变得至关重要。

在本章中,我们将向您介绍深度学习的历史,以及深度学习对研究和商业社区产生的更广泛影响。接下来,我们将介绍一些最著名的深度学习应用。这将包括突出的机器学习架构和基本的深度学习原语。最后,我们将简要展望深度学习在未来几年将走向何方,然后在接下来的几章中深入探讨 TensorFlow。

机器学习正在吞噬计算机科学

直到最近,软件工程师们去学校学习了许多基本算法(图搜索、排序、数据库查询等)。毕业后,这些工程师会走出校门,将这些算法应用到系统中。如今的数字经济大部分是建立在由几代工程师辛苦拼凑在一起的复杂基本算法链上的。大多数这些系统无法自适应。所有的配置和重新配置都必须由经过高度训练的工程师执行,使系统变得脆弱。

机器学习承诺通过使系统能够动态适应来改变软件开发领域。部署的机器学习系统能够从示例数据库中学习所需的行为。此外,这些系统可以在新数据到来时定期重新训练。由机器学习驱动的非常复杂的软件系统能够在不对其代码进行重大更改的情况下显着改变其行为(只需对其训练数据进行更改)。随着机器学习工具和部署变得越来越简单,这一趋势只会加速发展。

随着软件工程系统行为的改变,软件工程师的角色也将发生变化。在某种程度上,这种转变将类似于在开发编程语言之后发生的转变。最初的计算机是经过艰苦编程的。电线网络被连接和互连。然后设置了打孔卡,以便在不更改计算机硬件的情况下创建新程序。在打孔卡时代之后,第一个汇编语言被创建。然后是高级语言如 Fortran 或 Lisp。随后的开发层次创建了像 Python 这样的非常高级语言,具有复杂的预编码算法生态系统。现代计算机科学甚至依赖于自动生成的代码。现代应用程序开发人员使用诸如 Android Studio 之类的工具自动生成他们想要制作的大部分代码。每一波简化的连续浪潮都通过降低进入门槛扩大了计算机科学的范围。

机器学习承诺进一步降低障碍;程序员很快将能够通过改变训练数据来改变系统的行为,可能甚至不需要编写一行代码。在用户端,基于口语和自然语言理解的系统,如 Alexa 和 Siri,将允许非程序员执行复杂的计算。此外,由 ML 驱动的系统可能会更加鲁棒,抵抗错误。重新训练模型的能力意味着代码库可以缩小,可维护性将增加。简而言之,机器学习很可能会彻底颠覆软件工程师的角色。今天的程序员将需要了解机器学习系统学习的方式,并需要了解常见机器学习系统中出现的错误类别。此外,他们需要了解支撑机器学习系统的设计模式(与传统软件设计模式在风格和形式上非常不同)。而且,他们需要了解足够的张量微积分,以了解为什么一个复杂的深度架构在学习过程中可能会出现问题。毫不夸张地说,理解机器学习(理论和实践)将成为未来十年每个计算机科学家和软件工程师都需要掌握的基本技能。

在本章的其余部分,我们将快速介绍现代深度学习的基础知识。本书的其余部分将更深入地讨论我们在这里提到的所有主题。

深度学习基元

大多数深度架构是通过组合和重组一组有限的架构基元构建的。这些基元通常称为神经网络层,是深度网络的基础构建模块。在本书的其余部分,我们将深入介绍这些层。然而,在本节中,我们将简要概述许多深度网络中常见的模块。本节并不旨在对这些模块进行全面介绍。相反,我们的目标是快速概述复杂深度架构的基本构建模块,以激发您的兴趣。深度学习的艺术在于组合和重组这些模块,我们希望向您展示语言的字母表,让您开始深度学习专业知识之路。

全连接层

一个全连接网络将一系列输入转换为一系列输出。这种转换被称为全连接,因为任何输入值都可以影响任何输出值。即使对于相对较小的输入,这些层也会有许多可学习的参数,但它们具有一个很大的优势,即假设输入中没有结构。这个概念在图 1-1 中有所说明。

images/FCLayer@2x.png

图 1-1. 一个全连接层。入站箭头代表输入,出站箭头代表输出。互连线的粗细代表学习权重的大小。全连接层通过学习规则将输入转换为输出。

卷积层

卷积网络假设其输入具有特殊的空间结构。特别是,它假设在空间上彼此接近的输入在语义上是相关的。这种假设对于图像来说最有意义,因为彼此接近的像素很可能在语义上是相关的。因此,卷积层在深度架构中用于图像处理的广泛应用。这个概念在图 1-2 中有所说明。

就像全连接层将列表转换为列表一样,卷积层将图像转换为图像。因此,卷积层可用于执行复杂的图像转换,例如在照片应用程序中应用艺术滤镜。

images/depthcol.jpeg

图 1-2. 卷积层。左侧的红色形状代表输入数据,而右侧的蓝色形状代表输出。在这种情况下,输入的形状为(32,32,3)。也就是说,输入是一个 32 像素乘 32 像素的图像,有三个 RGB 颜色通道。红色输入中的突出区域是“局部感受野”,一组一起处理以创建蓝色输出中的突出区域的输入。

循环神经网络层

循环神经网络(RNN)层是一种原语,允许神经网络从输入序列中学习。该层假设输入从步骤到步骤按照定义的更新规则发展,这个规则可以从数据中学习。这个更新规则提供了给定先前所有状态的情况下下一个状态的预测。RNN 在图 1-3 中有示例。

RNN 层可以从数据中学习这个更新规则。因此,RNN 对于语言建模等任务非常有用,工程师希望构建可以根据历史预测用户将要输入的下一个单词的系统。

images/rnn.jpg

图 1-3. 循环神经网络(RNN)。输入从底部输入到网络中,输出从顶部提取。W 代表学习到的转换(在所有时间步共享)。网络在左侧概念上表示,在右侧展开以演示不同时间步的输入是如何处理的。

长短期记忆单元

在前一节中介绍的 RNN 层理论上能够学习任意序列更新规则。然而,在实践中,这些层无法学习来自遥远过去的影响。这种遥远的影响对于进行稳健的语言建模至关重要,因为复杂句子的含义可能取决于远处单词之间的关系。长短期记忆(LSTM)单元是对 RNN 层的修改,允许来自更深处过去的信号传递到现在。LSTM 单元在图 1-4 中有示例。

images/Long_Short_Term_Memory.png

图 1-4. 长短期记忆(LSTM)单元。在内部,LSTM 单元具有一组特别设计的操作,可以获得 vanilla RNN 的大部分学习能力,同时保留来自过去的影响。请注意,图示展示了许多 LSTM 变体中的一个。

深度学习架构

在前一节中介绍的深度学习原语组合了数百种不同的深度学习模型。其中一些架构在历史上非常重要。其他是首次呈现的新设计,影响了人们对深度学习能做什么的看法。

在本节中,我们介绍了一系列不同的深度学习架构,这些架构对研究社区产生了影响。我们要强调这是一个片段性的历史,不试图穷尽一切。文献中肯定有一些重要的模型没有在这里呈现。

LeNet

LeNet 架构可以说是第一个著名的“深度”卷积架构。于 1988 年推出,用于执行文档的光学字符识别(OCR)。尽管它表现出色,但 LeNet 的计算成本对当时的计算机硬件来说是极端的,因此设计在创作后的几十年里相对默默无闻。这个架构在图 1-5 中有示例。

images/lenet_architecture.png

图 1-5. 用于图像处理的 LeNet 架构。于 1988 年推出,可以说是图像处理的第一个深度卷积模型。

AlexNet

ImageNet 大规模视觉识别挑战赛(ILSVRC)于 2010 年首次组织,旨在测试视觉识别系统的进展。组织者利用了亚马逊的 Mechanical Turk,这是一个在线平台,将工作者与请求者连接起来,以目录化一个大量图像的集合,并附带图像中存在的对象列表。Mechanical Turk 的使用使得数据集的筛选比以前收集的数据集大得多。

挑战举办的头两年,更传统的依赖于 HOG 和 SIFT 特征(手动调整的视觉特征提取方法)的机器学习系统取得了胜利。2012 年,基于对 LeNet 进行修改并在强大的图形处理单元(GPU)上运行的 AlexNet 架构进入并主导了挑战,其错误率是最接近竞争对手的一半。这次胜利极大地推动了计算机视觉中对深度学习架构的(已经萌芽的)趋势。AlexNet 架构如图 1-6 所示。

images/alexnet.jpg

图 1-6。用于图像处理的 AlexNet 架构。这个架构是 ILSVRC 2012 挑战的获胜作品,并引发了对卷积架构的兴趣的复苏。

ResNet

自 2012 年以来,卷积架构一直在 ILSVRC 挑战赛中获胜(以及许多其他计算机视觉挑战)。每年举办比赛时,获胜架构的深度和复杂性都在增加。ResNet 架构是 ILSVRC 2015 挑战的获胜者,特别引人注目;ResNet 架构的深度可达 130 层,而 AlexNet 架构只有 8 层。

历史上,非常深的网络学习起来具有挑战性;当网络变得如此深时,它们会遇到梯度消失问题。随着信号在网络中传播,信号会衰减,导致学习减弱。这种衰减可以用数学方式解释,但效果是每个额外的层会乘法地减少信号的强度,导致网络的有效深度受到限制。

ResNet 引入了一项创新,即控制这种衰减的旁路连接。这些连接允许来自更深层的部分信号无损地通过,从而有效地训练更深的网络。ResNet 旁路连接如图 1-7 所示。

images/resnetb.jpg

图 1-7。ResNet 单元。右侧的身份连接允许未经修改的输入通过单元。这种修改允许有效训练非常深的卷积架构。

神经字幕模型

随着从业者对深度学习基元的使用变得更加熟悉,他们开始尝试混合和匹配基元模块,以创建能够执行比基本目标检测更复杂任务的高阶系统。神经字幕系统会自动生成图像内容的字幕。它们通过将从图像中提取信息的卷积网络与为图像生成描述性句子的 LSTM 层相结合来实现。整个系统是进行端到端训练的。也就是说,卷积网络和 LSTM 网络一起训练,以实现为提供的图像生成描述性句子的目标。

端到端训练是现代深度学习系统的关键创新之一,因为它减少了对输入的复杂预处理的需求。不使用深度学习的图像字幕模型将不得不使用复杂的图像特征化方法,如 SIFT,这些方法无法与字幕生成器一起训练。

神经字幕模型如图 1-8 所示。

images/neural_captioning.png

图 1-8。一个神经字幕架构。使用卷积网络从输入图像中提取相关的输入特征。然后使用递归网络生成一个描述性句子。

Google 神经机器翻译

谷歌的神经机器翻译(Google-NMT)系统使用端到端训练的范例来构建一个生产翻译系统,直接将源语言的句子翻译成目标语言。Google-NMT 系统依赖于 LSTM 的基本构建块,它将 LSTM 叠加了十几次,并在一个极其庞大的翻译句子数据集上进行训练。最终的架构通过将人类和机器翻译之间的差距缩小了多达 60%,为机器翻译带来了突破性的进展。Google-NMT 架构在图 1-9 中有所说明。

images/google-nmt-lstm.png

图 1-9。Google 神经机器翻译系统使用深度递归架构来处理输入句子,并使用第二个深度递归架构生成翻译后的输出句子。

一次性模型

一次性学习可能是机器/深度学习中最有趣的新想法。大多数深度学习技术通常需要大量的数据来学习有意义的行为。例如,AlexNet 架构利用大型 ILSVRC 数据集来学习视觉对象检测器。然而,认知科学的许多研究表明,人类可以从很少的例子中学习复杂的概念。以婴儿第一次了解长颈鹿为例。在动物园里看到一只长颈鹿的婴儿可能有能力从那时起认出她以后看到的所有长颈鹿。

深度学习的最新进展已经开始发明能够实现类似学习成就的架构。只给出一个概念的几个例子(但给出了丰富的附加信息来源),这样的系统可以学习在非常少的数据点上进行有意义的预测。最近的一篇论文(本书的作者之一)使用这个想法来证明一次性架构甚至可以在婴儿无法学习的情境中学习,比如在医药发现中。一种用于药物发现的一次性架构在图 1-10 中有所说明。

images/schematic_v2.png

图 1-10。一次性架构使用一种卷积网络将每个分子转换为一个向量。对苯环氧的向量与实验数据集中的向量进行比较。最相似数据点的标签(对甲苯磺酸)被用于查询。

AlphaGo

围棋是一种古老的棋盘游戏,在亚洲具有广泛的影响力。自 20 世纪 60 年代末以来,计算机围棋一直是计算机科学的一项重大挑战。使计算机国际象棋系统深蓝在 1997 年击败国际象棋大师加里·卡斯帕罗夫的技术并不适用于围棋。问题的一部分在于围棋的棋盘比国际象棋大得多;围棋棋盘的大小为 19×19,而国际象棋为 8×8。由于每步可能有更多的走法,可能的围棋走法树扩展得更快,使得使用当代计算机硬件进行 brute force 搜索对于足够的围棋游戏来说是不够的。图 1-11 展示了一个围棋棋盘。

images/go_board.jpg

图 1-11。围棋棋盘的插图。玩家交替在一个 19×19 的网格上放置白色和黑色的棋子。

谷歌 DeepMind 的 AlphaGo 最终实现了大师级别的计算机围棋。AlphaGo 证明了自己能够在五局比赛中击败世界上最强的围棋冠军李世石。AlphaGo 的一些关键思想包括使用深度价值网络和深度策略网络。价值网络提供了对棋盘位置价值的估计。与国际象棋不同,从棋盘状态很难猜测黑白哪方正在赢得围棋比赛。价值网络通过学习从比赛结果中做出这种预测来解决这个问题。另一方面,策略网络帮助估计在当前棋盘状态下采取的最佳走法。这两种技术与蒙特卡洛树搜索(一种经典搜索方法)的结合帮助克服了围棋游戏中的大分支因子。基本的 AlphaGo 架构在图 1-12 中有所说明。

images/value_policy.jpg

图 1-12。A)AlphaGo 架构的描述。最初,一个用于选择走法的策略网络在专家比赛数据集上进行训练。然后通过自我对弈来完善这个策略。“RL”表示强化学习,“SL”表示监督学习。B)策略网络和价值网络都在游戏棋盘的表示上运行。

生成对抗网络

生成对抗网络(GANs)是一种新型的深度网络,使用两个相互竞争的神经网络,生成器和对手(也称为鉴别器),它们相互对抗。生成器试图从训练分布中抽取样本(例如,尝试生成逼真的鸟类图像)。鉴别器则致力于区分从生成器抽取的样本和真实数据样本。(特定的鸟类是真实图像还是生成器创建的?)这种 GAN 的“对抗”训练似乎能够生成比其他技术更高保真度的图像样本,并且可能有助于使用有限数据训练有效的鉴别器。GAN 架构在图 1-13 中有所说明。

images/gen_adv.jpg

图 1-13。生成对抗网络(GAN)的概念描述。

GAN 已经证明能够生成非常逼真的图像,并可能推动下一代计算机图形工具的发展。这些系统生成的样本现在接近照片级逼真。然而,这些系统仍然存在许多理论和实际的问题需要解决,仍然需要进行大量的研究。

神经图灵机

到目前为止,所提出的大多数深度学习系统学习了具有有限适用领域的复杂函数;例如,目标检测、图像描述、机器翻译或围棋对弈。但是,我们是否可以有学习排序、加法或乘法等一般算法概念的深度架构呢?

神经图灵机(NTM)是第一次尝试制作一个能够学习任意算法的深度学习架构。该架构向类似 LSTM 的系统添加了一个外部存储器,以允许深度架构利用临时空间来计算更复杂的函数。目前,类似 NTM 的架构仍然相当有限,只能学习简单的算法。然而,NTM 方法仍然是一个活跃的研究领域,未来的进展可能会将这些早期的演示转变为实用的学习工具。NTM 架构在图 1-14 中有所说明。

images/neural-turing-machine-tutorial.jpg

图 1-14。神经图灵机的概念描述。它添加了一个外部存储器,深度架构可以读取和写入其中。

深度学习框架

研究人员几十年来一直在实施软件包,以便更容易地构建神经网络(深度学习)架构。直到最近几年,这些系统大多是专用的,仅在学术团体内部使用。这种缺乏标准化的、工业强度的软件使得非专家难以广泛使用神经网络。

这种情况在过去几年发生了巨大变化。谷歌在 2012 年实施了 DistBelief 系统,并利用它构建和部署了许多更简单的深度学习架构。DistBelief 的出现,以及类似的软件包如 Caffe、Theano、Torch、Keras、MxNet 等广泛推动了行业的采用。

TensorFlow 借鉴了这一丰富的知识历史,并基于其中一些软件包(特别是 Theano)的设计原则进行了构建。TensorFlow(和 Theano)特别使用张量的概念作为支持深度学习系统的基本基元。这种对张量的关注使这些软件包与 DistBelief 或 Caffe 等系统有所区别,后者不允许为构建复杂模型提供相同的灵活性。

尽管本书的其余部分将专注于 TensorFlow,但理解其基本原理应该使您能够轻松地将所学到的经验应用于其他深度学习框架。

TensorFlow 的局限性

TensorFlow 目前的一个主要弱点是构建新的深度学习架构相对较慢(初始化一个架构需要几秒钟的时间)。因此,在 TensorFlow 中构建一些动态改变结构的复杂深度架构并不方便。其中一种架构是 TreeLSTM,它利用英语句子的句法解析树执行需要理解自然语言的任务。由于每个句子都有不同的解析树,因此每个句子需要稍微不同的架构。图 1-15 展示了 TreeLSTM 架构。

images/treelstm.png

图 1-15。TreeLSTM 架构的概念描述。每个输入数据点的树形状都不同,因此必须为每个示例构建不同的计算图。

虽然这样的模型可以在 TensorFlow 中实现,但由于当前 TensorFlow API 的限制,这样做需要相当大的创造力。新的框架如 Chainer、DyNet 和 PyTorch 承诺通过使构建新架构足够轻便,以便像 TreeLSTM 这样的模型可以轻松构建来消除这些障碍。幸运的是,TensorFlow 开发人员已经在扩展基本 TensorFlow API(如 TensorFlow Eager)上进行了工作,这将使动态架构的构建更加容易。

一个要点是,深度学习框架的进展是迅速的,今天的新系统可能会成为明天的老旧消息。然而,底层张量计算的基本原理可以追溯到几个世纪前,并且无论未来编程模型如何变化,都将使读者受益匪浅。本书将强调使用 TensorFlow 作为开发对底层张量计算的直观了解的工具。

回顾

在本章中,我们解释了为什么深度学习对现代软件工程师至关重要,并快速浏览了许多深度架构。在下一章中,我们将开始探索 TensorFlow,谷歌用于构建和训练深度架构的框架。在接下来的章节中,我们将深入探讨一些深度架构的实际示例。

机器学习(尤其是深度学习),就像计算机科学的大部分领域一样,是一门非常经验主义的学科。只有通过大量的实践经验才能真正理解深度学习。因此,在本书的剩余部分中,我们包含了许多深入的案例研究。我们鼓励您深入研究这些例子,并动手尝试使用 TensorFlow 实验您自己的想法。仅仅理论上理解算法是远远不够的!

第二章:TensorFlow 基本概念介绍

本章将介绍 TensorFlow 的基本概念。特别是,您将学习如何使用 TensorFlow 执行基本计算。本章的大部分内容将用于介绍张量的概念,并讨论在 TensorFlow 中如何表示和操作张量。这个讨论将需要简要概述一些支撑张量数学的数学概念。特别是,我们将简要回顾基本线性代数,并演示如何使用 TensorFlow 执行基本线性代数运算。

我们将在讨论基本数学之后讨论声明式和命令式编程风格之间的区别。与许多编程语言不同,TensorFlow 主要是声明式的。调用 TensorFlow 操作会向 TensorFlow 的“计算图”中添加一个计算的描述。特别是,TensorFlow 代码“描述”计算,实际上并不执行它们。为了运行 TensorFlow 代码,用户需要创建tf.Session对象。我们介绍了会话的概念,并描述了用户如何在 TensorFlow 中使用它们进行计算。

我们通过讨论变量的概念来结束本章。TensorFlow 中的变量保存张量,并允许进行有状态的计算以修改变量。我们演示如何通过 TensorFlow 创建变量并更新它们的值。

介绍张量

张量是物理和工程等领域中的基本数学构造。然而,从历史上看,张量在计算机科学中的应用较少,计算机科学传统上更多地与离散数学和逻辑相关。随着机器学习的出现及其基础建立在连续的、矢量化的数学上,这种情况已经开始发生显著变化。现代机器学习建立在对张量的操作和微积分之上。

标量、向量和矩阵

首先,我们将给出一些您可能熟悉的张量的简单示例。张量的最简单示例是标量,即从实数中取出的单个常数值(请记住,实数是任意精度的十进制数,允许包含正数和负数)。在数学上,我们用ℝ来表示实数。更正式地说,我们将标量称为秩为 0 的张量。

关于域的说明

在数学上精通的读者可能会反对基于复数或二进制数定义张量是完全有意义的。更一般地说,只要数字来自一个:一个数学上定义了 0、1、加法、乘法、减法和除法的数字集合。常见的域包括实数ℝ、有理数ℚ、复数ℂ和有限域,如ℤ 2。在讨论中,我们将假设张量的值为实数,但是用其他域的值替代是完全合理的。

如果标量是秩为 0 的张量,那么什么构成了秩为 1 的张量?严格来说,秩为 1 的张量是一个向量;一个实数列表。传统上,向量被写成列向量

a b

或者作为行向量

a b

在符号上,长度为 2 的所有列向量表示为ℝ 2×1,而长度为 2 的所有行向量集合是ℝ 1×2。更具体地说,我们可以说列向量的形状是(2, 1),而行向量的形状是(1, 2)。如果我们不想指定一个向量是行向量还是列向量,我们可以说它来自于集合ℝ 2,形状为(2)。这种张量形状的概念对于理解 TensorFlow 计算非常重要,我们将在本章后面再次回到这个概念。

向量最简单的用途之一是表示现实世界中的坐标。假设我们决定一个原点(比如你当前站立的位置)。那么世界上的任何位置都可以用从你当前位置的三个位移值来表示(左右位移、前后位移、上下位移)。因此,向量空间ℝ 3可以表示世界上的任何位置。

举个不同的例子,假设一只猫由它的身高、体重和颜色描述。那么一个视频游戏中的猫可以被表示为一个向量。

height weight color

在空间ℝ 3。这种类型的表示通常被称为特征化。也就是说,特征化是将现实世界实体表示为向量(或更一般地表示为张量)。几乎所有的机器学习算法都是在向量或张量上运行的。因此,特征化过程是任何机器学习流程中的关键部分。通常,特征化系统可能是机器学习系统中最复杂的部分。假设我们有一个如图 2-1 所示的苯分子。

images/Benzene-2D-flat.png

图 2-1。苯分子的表示。

我们如何将这个分子转换成适合查询机器学习系统的向量?对于这个问题有许多潜在的解决方案,其中大多数利用了标记分子的亚片段存在的想法。特定亚片段的存在或不存在通过在二进制向量中设置索引(在{0,1} n)分别设置为 1/0 来标记。这个过程在图 2-2 中有所说明。

images/molecular_fingerprint.jpg

图 2-2。将要进行特征化的分子的亚片段被选择(包含 OH 的那些)。这些片段被哈希成固定长度向量中的索引。这些位置被设置为 1,所有其他位置被设置为 0。

请注意,这个过程听起来(也是)相当复杂。事实上,构建一个机器学习系统最具挑战性的方面之一是决定如何将所涉及的数据转换成张量格式。对于某些类型的数据,这种转换是显而易见的。对于其他类型的数据(比如分子),所需的转换可能相当微妙。对于机器学习从业者来说,通常不需要发明新的特征化方法,因为学术文献非常丰富,但通常需要阅读研究论文以了解将新数据流转换的最佳实践。

现在我们已经确定了秩为 0 的张量是标量(ℝ),而秩为 1 的张量是向量(ℝ n),那么秩为 2 的张量是什么?传统上,秩为 2 的张量被称为矩阵:

a b c d

这个矩阵有两行两列。所有这样的矩阵集合被称为ℝ 2×2。回到我们之前对张量形状的概念,这个矩阵的形状是(2, 2)。矩阵通常用来表示向量的变换。例如,通过矩阵

R α = cos ( α ) –sin ( α ) sin ( α ) cos ( α )

要看到这一点,注意x单位向量(1, 0)通过矩阵乘法转换为向量(cos(α),sin(α))。(我们将在本章后面详细介绍矩阵乘法的定义,但目前只是显示结果)。

cos ( α ) –sin ( α ) sin ( α ) cos ( α ) · 1 0 = cos ( α ) sin ( α )

这种转换也可以以图形方式可视化。图 2-3 展示了最终向量如何对应于原始单位向量的旋转。

unit_circle.png

图 2-3。单位圆上的位置由余弦和正弦参数化。

机器学习程序经常使用矩阵的一些标准数学运算。我们将简要回顾其中一些最基本的运算。

在平面上旋转向量的操作可以通过这个矩阵来执行。

矩阵的转置是一个方便的操作,它将矩阵沿对角线翻转。数学上,假设A是一个矩阵;那么转置矩阵A T由方程A ij T = A ji定义。例如,旋转矩阵R α的转置是

R α T = cos ( α ) sin ( α ) –sin ( α ) cos ( α )

矩阵的加法仅对形状相同的矩阵定义,并且仅是逐元素执行。例如:

1 2 3 4 + 1 1 1 1 = 2 3 4 5

同样,矩阵可以乘以标量。在这种情况下,矩阵的每个元素都简单地逐元素乘以相关的标量:

2 · 1 2 3 4 = 2 4 6 8

此外,有时可以直接相乘两个矩阵。矩阵乘法的概念可能是与矩阵相关的最重要的数学概念。特别注意,矩阵乘法不同于矩阵的逐元素乘法!假设我们有一个形状为(mn)的矩阵A,其中mn列。那么,A可以右乘任何形状为(nk)的矩阵B(其中k是任意正整数),形成形状为(mk)的矩阵AB。对于实际的数学描述,假设A是形状为(mn)的矩阵,B是形状为(nk)的矩阵。那么AB由以下定义:

(AB) ij = ∑ k A ik B kj

我们之前简要展示了一个矩阵乘法方程。现在让我们扩展这个例子,因为我们有了正式的定义:

cos ( α ) –sin ( α ) sin ( α ) cos ( α ) · 1 0 = cos ( α ) · 1 – sin ( α ) · 0 sin ( α ) · 1 – cos ( α ) · 0 = cos ( α ) sin ( α )

基本要点是一个矩阵的行与另一个矩阵的列相乘。

这个定义隐藏了许多微妙之处。首先注意矩阵乘法不是交换的。也就是说,A B ≠ B A 通常不成立。实际上,当 BA 没有意义时,AB 可能存在。例如,假设 A 是一个形状为 (2, 3) 的矩阵,B 是一个形状为 (3, 4) 的矩阵。那么 AB 是一个形状为 (2, 4) 的矩阵。然而,BA 没有定义,因为各自的维度(4 和 2)不匹配。另一个微妙之处是,正如旋转示例中所示,一个形状为 (m, n) 的矩阵可以右乘一个形状为 (n, 1) 的矩阵。然而,一个形状为 (n, 1) 的矩阵简单地是一个列向量。因此,将矩阵乘以向量是有意义的。矩阵-向量乘法是常见机器学习系统的基本构建块之一。

标准乘法的一个最好的特性是它是一个线性操作。更准确地说,如果一个函数 f 被称为线性,那么 f ( x + y ) = f ( x ) + f ( y ) 和 f ( c x ) = c f ( x ) 其中 c 是一个标量。为了证明标量乘法是线性的,假设 a, b, c, d 都是实数。那么我们有

a · ( b · c ) = b · ( a c )a · ( c + d ) = a c + a d

我们在这里利用了标量乘法的交换和分配性质。现在假设相反,A, C, D 现在是矩阵,其中 C, D 的大小相同,并且将 A 右乘以 CD 是有意义的(b 仍然是一个实数)。那么矩阵乘法是一个线性操作符:

A ( b · C ) = b · ( A C )A ( C + D ) = A C + A D

换句话说,矩阵乘法是可分配的,并且与标量乘法交换。事实上,可以证明向量上的任何线性变换对应于矩阵乘法。对于计算机科学的类比,将线性性视为超类中要求的属性。然后标准乘法和矩阵乘法是该抽象方法的具体实现,分别适用于不同子类(实数和矩阵)。

张量

在前面的部分中,我们介绍了标量作为秩为 0 的张量,向量作为秩为 1 的张量,矩阵作为秩为 2 的张量。那么什么是秩为 3 的张量呢?在转向一般定义之前,思考一下标量、向量和矩阵之间的共同点可能有所帮助。标量是单个数字。向量是数字列表。要选择向量的任何特定元素,需要知道它的索引。因此,我们需要一个索引元素进入向量(因此是一个秩为 1 的张量)。矩阵是数字表。要选择矩阵的任何特定元素,需要知道它的行和列。因此,我们需要两个索引元素(因此是一个秩为 2 的张量)。很自然地,秩为 3 的张量是一组数字,其中有三个必需的索引。可以将秩为 3 的张量想象为数字的长方体,如图 2-4 所示。

images/3tensor.gif

图 2-4。秩为 3 的张量可以被视为数字的长方体。

图中显示的秩为 3 的张量T的形状为(N, N, N)。那么张量的任意元素将通过指定(i, j, k)作为索引来选择。

张量和形状之间存在联系。秩为 1 的张量具有 1 维形状,秩为 2 的张量具有 2 维形状,秩为 3 的张量具有 3 维形状。你可能会质疑这与我们之前讨论的行向量和列向量相矛盾。根据我们的定义,列向量的形状是(n, 1)。这难道不会使列向量成为一个秩为 2 的张量(或矩阵)吗?这确实发生了。回想一下,未指定为行向量或列向量的向量的形状是(n)。当我们指定一个向量是行向量还是列向量时,实际上我们指定了一种将基础向量转换为矩阵的方法。这种维度扩展是张量操作中的常见技巧。

注意,另一种思考秩为 3 的张量的方式是将其视为具有相同形状的矩阵列表。假设W是一个形状为(n, n)的矩阵。那么张量 T ijk = ( W 1 , ⋯ , W n ) 包含了Wn个副本。

请注意,黑白图像可以表示为二阶张量。假设我们有一个 224×224 像素的黑白图像。那么,像素(ij)是 1/0 来编码黑/白像素。因此,黑白图像可以表示为形状为(224, 224)的矩阵。现在,考虑一个 224×224 的彩色图像。一个特定像素的颜色通常由三个单独的 RGB 通道表示。也就是说,像素(ij)被表示为一个包含三个数字(rgb)的元组,分别编码像素中的红色、绿色和蓝色的量。rgb通常是从 0 到 255 的整数。因此,彩色图像可以被编码为形状为(224, 224, 3)的三阶张量。继续类比,考虑一个彩色视频。假设视频的每一帧是一个 224×224 的彩色图像。那么一分钟的视频(以 60 帧每秒的速度)将是一个形状为(224, 224, 3, 3600)的四阶张量。进一步地,10 个这样的视频集合将形成一个形状为(10, 224, 224, 3, 3600)的五阶张量。总的来说,张量提供了对数值数据的便捷表示。在实践中,看到高于五阶张量的张量并不常见,但设计任何张量软件以允许任意张量是明智的,因为聪明的用户总会提出设计者没有考虑到的用例。

物理学中的张量

张量在物理学中被广泛用于编码基本物理量。例如,应力张量通常用于材料科学中定义材料内某点的应力。从数学上讲,应力张量是一个形状为(3, 3)的二阶张量:

σ = σ 11 τ 12 τ 13 τ 21 σ 22 τ 23 τ 31 τ 32 σ 33

然后,假设n是一个形状为(3)的向量,编码一个方向。在方向n上的应力T n由向量T n = T · n(注意矩阵-向量乘法)指定。这种关系在图 2-5 中以图形方式描述。

images/stress_energy_small.png

图 2-5。应力分量的三维图示。

作为另一个物理例子,爱因斯坦的广义相对论场方程通常以张量格式表达:

R μν - 1 2 R g μν + Λ g μν = 8πG c 4 T μν

这里R μν是里奇曲率张量,g μν是度规张量,T μν是应力能量张量,其余量是标量。然而,需要注意的是,这些张量和我们之前讨论过的其他张量之间有一个重要的微妙区别。像度规张量这样的量为时空中的每个点提供一个单独的张量(在数字数组的意义上,数学上,度规张量是一个张量场)。之前讨论过的应力张量也是如此,这些方程中的其他张量也是如此。在时空中的特定点,这些量中的每一个都变成了一个对称的秩 2 张量,使用我们的符号形状为(4,4)。

现代张量微积分系统(如 TensorFlow)的一部分力量在于,一些长期用于经典物理学的数学机器现在可以被改编用于解决图像处理和语言理解等应用问题。与此同时,今天的张量微积分系统与物理学家的数学机器相比仍然有限。例如,目前还没有简单的方法来使用 TensorFlow 谈论度规张量这样的量。我们希望随着张量微积分对计算机科学变得更加基础,情况会发生变化,像 TensorFlow 这样的系统将成为物理世界和计算世界之间的桥梁。

数学细节

到目前为止,在本章中的讨论通过示例和插图非正式地介绍了张量。在我们的定义中,张量简单地是一组数字的数组。通常方便将张量视为一个函数。最常见的定义将张量引入为从向量空间的乘积到实数的多线性函数:

T : V 1 × V 2 × ⋯ V n → ℝ

这个定义使用了一些你没有见过的术语。一个向量空间简单地是向量的集合。你已经见过一些向量空间的例子,比如ℝ 3或者一般的ℝ n。我们可以假设V i = ℝ d i而不会失去一般性。正如我们之前定义的,一个函数f是线性的,如果f ( x + y ) = f ( x ) + f ( y )和f ( c x ) = c f ( x )。一个多线性函数简单地是一个在每个参数上都是线性的函数。当提供数组索引作为参数时,这个函数可以被看作是给定多维数组的单个条目。

在本书中我们不会经常使用这个更数学化的定义,但它作为一个有用的桥梁,将你将要学习的深度学习概念与物理学和数学界对张量进行的几个世纪的研究联系起来。

协变和逆变

我们在这里的定义中忽略了许多需要仔细处理的细节,以进行正式处理。例如,我们在这里没有涉及共变和逆变指数的概念。我们所谓的秩-n张量最好描述为一个(pq)-张量,其中n = p + qp是逆变指数的数量,q是共变指数的数量。例如,矩阵是(1,1)-张量。作为一个微妙之处,有些秩为 2 的张量不是矩阵!我们不会在这里仔细探讨这些主题,因为它们在机器学习中并不经常出现,但我们鼓励您了解协变性和逆变性如何影响您构建的机器学习系统。

TensorFlow 中的基本计算

在过去的几节中,我们已经涵盖了各种张量的数学定义。现在是时候使用 TensorFlow 创建和操作张量了。对于本节,我们建议您使用交互式 Python 会话(使用 IPython)跟随进行。许多基本的 TensorFlow 概念在直接实验后最容易理解。

安装 TensorFlow 并入门

在继续本节之前,您需要在您的计算机上安装 TensorFlow。安装的详细信息将取决于您的特定硬件,因此我们建议您查阅官方 TensorFlow 文档以获取更多详细信息。

尽管 TensorFlow 有多种编程语言的前端,但我们将在本书的其余部分中专门使用 TensorFlow Python API。我们建议您安装Anaconda Python,它打包了许多有用的数值库以及基本的 Python 可执行文件。

一旦您安装了 TensorFlow,我们建议您在学习基本 API 时交互地调用它(请参见示例 2-1)。在与 TensorFlow 交互时进行实验时,使用tf.InteractiveSession()会很方便。在 IPython(一个交互式 Python shell)中调用此语句将使 TensorFlow 几乎以命令方式运行,使初学者更容易地玩弄张量。稍后在本章中,您将更深入地了解命令式与声明式风格的区别。

示例 2-1。初始化一个交互式 TensorFlow 会话
>>> import tensorflow as tf
>>> tf.InteractiveSession()
<tensorflow.python.client.session.InteractiveSession>

本节中的其余代码将假定已加载了一个交互式会话。

初始化常量张量

到目前为止,我们已经将张量讨论为抽象的数学实体。然而,像 TensorFlow 这样的系统必须在真实计算机上运行,因此任何张量必须存在于计算机内存中,以便对计算机程序员有用。TensorFlow 提供了许多在内存中实例化基本张量的函数。其中最简单的是tf.zeros()tf.ones()tf.zeros()接受一个张量形状(表示为 Python 元组)并返回一个填充有零的该形状的张量。让我们尝试在 shell 中调用此命令(请参见示例 2-2)。

示例 2-2。创建一个零张量
>>> tf.zeros(2)
<tf.Tensor 'zeros:0' shape=(2,) dtype=float32>

TensorFlow 返回所需张量的引用,而不是张量本身的值。为了强制返回张量的值,我们将使用张量对象的tf.Tensor.eval()方法(请参见示例 2-3)。由于我们已经初始化了tf.InteractiveSession(),这个方法将向我们返回零张量的值。

示例 2-3。评估张量的值
>>> a = tf.zeros(2)
>>> a.eval()
array([ 0.,  0.], dtype=float32)

请注意,TensorFlow 张量的计算值本身是一个 Python 对象。特别地,a.eval()是一个numpy.ndarray对象。NumPy 是 Python 的一个复杂数值系统。我们不会在这里尝试对 NumPy 进行深入讨论,只是注意到 TensorFlow 被设计为在很大程度上与 NumPy 约定兼容。

我们可以调用tf.zeros()tf.ones()来创建和显示各种大小的张量(请参见示例 2-4)。

示例 2-4。评估和显示张量
>>> a = tf.zeros((2, 3))
>>> a.eval()
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)
>>> b = tf.ones((2,2,2))
>>> b.eval()
array([[[ 1.,  1.],
        [ 1.,  1.]],

       [[ 1.,  1.],
        [ 1.,  1.]]], dtype=float32)

如果我们想要一个填充有除 0/1 之外的某个数量的张量呢?tf.fill()方法提供了一个很好的快捷方式来做到这一点(示例 2-5)。

示例 2-5。用任意值填充张量
>>> b = tf.fill((2, 2), value=5.)
>>> b.eval()
array([[ 5.,  5.],
       [ 5.,  5.]], dtype=float32)

tf.constant是另一个函数,类似于tf.fill,允许在程序执行期间不应更改的张量的构建(示例 2-6)。

示例 2-6。创建常量张量
>>> a = tf.constant(3)
>>> a.eval()
3

抽样随机张量

尽管使用常量张量方便测试想法,但更常见的是使用随机值初始化张量。这样做的最常见方式是从随机分布中抽样张量中的每个条目。tf.random_normal允许从指定均值和标准差的正态分布中抽样指定形状的张量中的每个条目(示例 2-7)。

对称性破缺

许多机器学习算法通过对保存权重的一组张量执行更新来学习。这些更新方程通常满足初始化为相同值的权重将继续一起演变的属性。因此,如果初始张量集初始化为一个常量值,模型将无法学习太多。解决这种情况需要破坏对称性。打破对称性的最简单方法是随机抽样张量中的每个条目。

示例 2-7。抽样具有随机正态条目的张量
>>> a = tf.random_normal((2, 2), mean=0, stddev=1)
>>> a.eval()
array([[-0.73437649, -0.77678096],
       [ 0.51697761,  1.15063596]], dtype=float32)

需要注意的一点是,机器学习系统通常使用具有数千万参数的非常大的张量。当我们从正态分布中抽样数千万个随机值时,几乎可以肯定会有一些抽样值远离均值。这样大的样本可能导致数值不稳定,因此通常使用tf.truncated_normal()而不是tf.random_normal()进行抽样。这个函数在 API 方面与tf.random_normal()相同,但会删除并重新抽样所有距离均值超过两个标准差的值。

tf.random_uniform()的行为类似于tf.random_normal(),唯一的区别是随机值是从指定范围的均匀分布中抽样的(示例 2-8)。

示例 2-8。抽样具有均匀随机条目的张量
>>> a = tf.random_uniform((2, 2), minval=-2, maxval=2)
>>> a.eval()
array([[-1.90391684,  1.4179163 ],
       [ 0.67762709,  1.07282352]], dtype=float32)

张量加法和缩放

TensorFlow 利用 Python 的运算符重载,使用标准 Python 运算符使基本张量算术变得简单直观(示例 2-9)。

示例 2-9。将张量相加
>>> c = tf.ones((2, 2))
>>> d = tf.ones((2, 2))
>>> e = c + d
>>> e.eval()
array([[ 2.,  2.],
       [ 2.,  2.]], dtype=float32)
>>> f = 2 * e
>>> f.eval()
array([[ 4.,  4.],
       [ 4.,  4.]], dtype=float32)

张量也可以这样相乘。但是请注意,当两个张量相乘时,我们得到的是逐元素乘法而不是矩阵乘法,可以在示例 2-10 中看到。

示例 2-10。逐元素张量乘法
>>> c = tf.fill((2,2), 2.)
>>> d = tf.fill((2,2), 7.)
>>> e = c * d
>>> e.eval()
array([[ 14.,  14.],
       [ 14.,  14.]], dtype=float32)

矩阵运算

TensorFlow 提供了各种便利设施来处理矩阵。(在实践中,矩阵是最常用的张量类型。)特别是,TensorFlow 提供了快捷方式来创建某些常用矩阵类型。其中最常用的可能是单位矩阵。单位矩阵是指在对角线上除了 1 之外其他地方都是 0 的方阵。tf.eye()允许快速构建所需大小的单位矩阵(示例 2-11)。

示例 2-11。创建一个单位矩阵
>>> a = tf.eye(4)
>>> a.eval()
array([[ 1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.],
       [ 0.,  0.,  0.,  1.]], dtype=float32)

对角矩阵是另一种常见类型的矩阵。与单位矩阵不同,对角矩阵只在对角线上非零。与单位矩阵不同,它们可以在对角线上取任意值。让我们构造一个沿对角线升序值的对角矩阵(示例 2-12)。首先,我们需要一种方法在 TensorFlow 中构造升序值的向量。这样做的最简单方法是调用tf.range(start, limit, delta)。请注意,范围中排除了limitdelta是遍历的步长。然后,生成的向量可以传递给tf.diag(diagonal),它将构造具有指定对角线的矩阵。

示例 2-12。创建对角矩阵
>>> r = tf.range(1, 5, 1)
>>> r.eval()
array([1, 2, 3, 4], dtype=int32)
>>> d = tf.diag(r)
>>> d.eval()
array([[1, 0, 0, 0],
       [0, 2, 0, 0],
       [0, 0, 3, 0],
       [0, 0, 0, 4]], dtype=int32)

现在假设我们在 TensorFlow 中有一个指定的矩阵。如何计算矩阵的转置?tf.matrix_transpose()会很好地完成这个任务(示例 2-13)。

示例 2-13。取矩阵转置
>>> a = tf.ones((2, 3))
>>> a.eval()
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]], dtype=float32)
>>> at = tf.matrix_transpose(a)
>>> at.eval()
array([[ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.]], dtype=float32)

现在,假设我们有一对矩阵,我们想要使用矩阵乘法相乘。最简单的方法是调用tf.matmul()(示例 2-14)。

示例 2-14。执行矩阵乘法
>>> a = tf.ones((2, 3))
>>> a.eval()
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]], dtype=float32)
>>> b = tf.ones((3, 4))
>>> b.eval()
array([[ 1.,  1.,  1.,  1.],
       [ 1.,  1.,  1.,  1.],
       [ 1.,  1.,  1.,  1.]], dtype=float32)
>>> c = tf.matmul(a, b)
>>> c.eval()
array([[ 3.,  3.,  3.,  3.],
       [ 3.,  3.,  3.,  3.]], dtype=float32)

您可以检查这个答案是否与我们之前提供的矩阵乘法的数学定义相匹配。

张量类型

您可能已经注意到了前面示例中的dtype表示。TensorFlow 中的张量有各种类型,如tf.float32tf.float64tf.int32tf.int64。可以通过在张量构造函数中设置dtype来创建指定类型的张量。此外,给定一个张量,可以使用转换函数如tf.to_double()tf.to_float()tf.to_int32()tf.to_int64()等来更改其类型(示例 2-15)。

示例 2-15。创建不同类型的张量
>>> a = tf.ones((2,2), dtype=tf.int32)
>>> a.eval()
array([[0, 0],
       [0, 0]], dtype=int32)
>>> b = tf.to_float(a)
>>> b.eval()
array([[ 0.,  0.],
       [ 0.,  0.]], dtype=float32)

张量形状操作

在 TensorFlow 中,张量只是内存中写入的数字集合。不同的形状是对底层数字集合的视图,提供了与数字集合交互的不同方式。在不同的时间,将相同的数字集合视为具有不同形状的张量可能是有用的。tf.reshape()允许将张量转换为具有不同形状的张量(示例 2-16)。

示例 2-16。操作张量形状
>>> a = tf.ones(8)
>>> a.eval()
array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.], dtype=float32)
>>> b = tf.reshape(a, (4, 2))
>>> b.eval()
array([[ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.],
       [ 1.,  1.]], dtype=float32)
>>> c = tf.reshape(a, (2, 2, 2))
>>> c.eval()
array([[[ 1.,  1.],
        [ 1.,  1.]],

       [[ 1.,  1.],
        [ 1.,  1.]]], dtype=float32)

注意如何使用tf.reshape将原始秩为 1 的张量转换为秩为 2 的张量,然后再转换为秩为 3 的张量。虽然所有必要的形状操作都可以使用tf.reshape()执行,但有时使用诸如tf.expand_dimstf.squeeze等函数执行更简单的形状操作可能更方便。tf.expand_dims向大小为 1 的张量添加额外的维度。它用于通过增加一个维度来增加张量的秩(例如,将秩为 1 的向量转换为秩为 2 的行向量或列向量)。另一方面,tf.squeeze从张量中删除所有大小为 1 的维度。这是将行向量或列向量转换为平坦向量的有用方法。

这也是一个方便的机会来介绍tf.Tensor.get_shape()方法(示例 2-17)。这个方法允许用户查询张量的形状。

示例 2-17。获取张量的形状
>>> a = tf.ones(2)
>>> a.get_shape()
TensorShape([Dimension(2)])
>>> a.eval()
array([ 1.,  1.], dtype=float32)
>>> b = tf.expand_dims(a, 0)
>>> b.get_shape()
TensorShape([Dimension(1), Dimension(2)])
>>> b.eval()
array([[ 1.,  1.]], dtype=float32)
>>> c = tf.expand_dims(a, 1)
>>> c.get_shape()
TensorShape([Dimension(2), Dimension(1)])
>>> c.eval()
array([[ 1.],
       [ 1.]], dtype=float32)
>>> d = tf.squeeze(b)
>>> d.get_shape()
TensorShape([Dimension(2)])
>>> d.eval()
array([ 1.,  1.], dtype=float32)

广播简介

广播是一个术语(由 NumPy 引入),用于当张量系统的矩阵和不同大小的向量可以相加时。这些规则允许像将向量添加到矩阵的每一行这样的便利。广播规则可能相当复杂,因此我们不会深入讨论规则。尝试并查看广播的工作方式通常更容易(示例 2-18)。

示例 2-18。广播的示例
>>> a = tf.ones((2, 2))
>>> a.eval()
array([[ 1.,  1.],
       [ 1.,  1.]], dtype=float32)
>>> b = tf.range(0, 2, 1, dtype=tf.float32)
>>> b.eval()
array([ 0.,  1.], dtype=float32)
>>> c = a + b
>>> c.eval()
array([[ 1.,  2.],
       [ 1.,  2.]], dtype=float32)

注意向量b被添加到矩阵a的每一行。注意另一个微妙之处;我们明确为b设置了dtype。如果没有设置dtype,TensorFlow 将报告类型错误。让我们看看如果我们没有设置dtype会发生什么(示例 2-19)。

示例 2-19。TensorFlow 不执行隐式类型转换
>>> b = tf.range(0, 2, 1)
>>> b.eval()
array([0, 1], dtype=int32)
>>> c = a + b
ValueError: Tensor conversion requested dtype float32 for Tensor with dtype int32:
'Tensor("range_2:0", shape=(2,), dtype=int32)

与 C 语言不同,TensorFlow 在底层不执行隐式类型转换。在进行算术运算时通常需要执行显式类型转换。

命令式和声明式编程

计算机科学中的大多数情况涉及命令式编程。考虑一个简单的 Python 程序(示例 2-20)。

示例 2-20。以命令式方式执行加法的 Python 程序
>>> a = 3
>>> b = 4
>>> c = a + b
>>> c
7

这个程序,当被翻译成机器码时,指示机器对两个寄存器执行一个原始的加法操作,一个包含 3,另一个包含 4。结果是 7。这种编程风格被称为命令式,因为程序明确告诉计算机执行哪些操作。

另一种编程风格是声明式。在声明式系统中,计算机程序是要执行的计算的高级描述。它不会明确告诉计算机如何执行计算。示例 2-21 是示例 2-20 的 TensorFlow 等价物。

示例 2-21。以声明式方式执行加法的 TensorFlow 程序
>>> a = tf.constant(3)
>>> b = tf.constant(4)
>>> c = a + b
>>> c
<tf.Tensor 'add_1:0' shape=() dtype=int32>
>>> c.eval()
7

注意c的值不是7!相反,它是一个符号张量。这段代码指定了将两个值相加以创建一个新张量的计算。实际计算直到我们调用c.eval()才执行。在之前的部分,我们一直在使用eval()方法来模拟 TensorFlow 中的命令式风格,因为一开始理解声明式编程可能会有挑战。

然而,声明式编程对软件工程并不是一个未知的概念。关系数据库和 SQL 提供了一个广泛使用的声明式编程系统的例子。像 SELECT 和 JOIN 这样的命令可以在底层以任意方式实现,只要它们的基本语义得以保留。TensorFlow 代码最好被视为类似于 SQL 程序;TensorFlow 代码指定要执行的计算,细节留给 TensorFlow 处理。TensorFlow 开发人员利用底层缺乏细节来调整执行风格以适应底层硬件,无论是 CPU、GPU 还是移动设备。

值得注意的是,声明式编程的主要弱点是抽象性很差。例如,没有对关系数据库的底层实现有详细了解,长的 SQL 程序可能会变得难以忍受地低效。同样,没有对底层学习算法的理解实现的大型 TensorFlow 程序可能不会运行良好。在本节的其余部分,我们将开始减少抽象,这个过程将贯穿整本书的其余部分。

TensorFlow Eager

TensorFlow 团队最近添加了一个新的实验模块,TensorFlow Eager,使用户能够以命令式方式运行 TensorFlow 计算。随着时间的推移,这个模块很可能会成为新程序员学习 TensorFlow 的首选入口模式。然而,在撰写时,这个模块仍然非常新,并且存在许多问题。因此,我们不会教授您关于 Eager 模式,但鼓励您自行了解。

重要的是要强调,即使 Eager 成熟后,TensorFlow 的很多部分仍然会保持声明式,所以学习声明式的 TensorFlow 是值得的。

TensorFlow 图

在 TensorFlow 中,任何计算都表示为tf.Graph对象的实例。这样的图由一组tf.Tensor对象实例和tf.Operation对象实例组成。我们已经详细介绍了tf.Tensor,但tf.Operation对象是什么?在本章的过程中,您已经看到了它们。对tf.matmul等操作的调用会创建一个tf.Operation实例,以标记执行矩阵乘法操作的需求。

当未明确指定tf.Graph时,TensorFlow 会将张量和操作添加到隐藏的全局tf.Graph实例中。可以通过tf.get_default_graph()获取此实例(示例 2-22)。

示例 2-22。获取默认的 TensorFlow 图
>>> tf.get_default_graph()
<tensorflow.python.framework.ops.Graph>

可以指定 TensorFlow 操作应在除默认之外的图中执行。我们将在未来章节中演示这方面的示例。

TensorFlow 会话

在 TensorFlow 中,tf.Session()对象存储计算执行的上下文。在本章的开头,我们使用tf.InteractiveSession()为所有 TensorFlow 计算设置环境。此调用创建了一个隐藏的全局上下文,用于执行所有计算。然后我们使用tf.Tensor.eval()来执行我们声明指定的计算。在幕后,此调用在这个隐藏的全局tf.Session上下文中进行评估。使用显式上下文进行计算而不是隐藏上下文可能会更方便(通常也更必要)(示例 2-23)。

示例 2-23。显式操作 TensorFlow 会话
>>> sess = tf.Session()
>>> a = tf.ones((2, 2))
>>> b = tf.matmul(a, a)
>>> b.eval(session=sess)
array([[ 2.,  2.],
       [ 2.,  2.]], dtype=float32)

此代码在sess的上下文中评估b,而不是隐藏的全局会话。实际上,我们可以使用另一种符号更明确地表示这一点(示例 2-24)。

示例 2-24。在会话中运行计算
>>> sess.run(b)
array([[ 2.,  2.],
       [ 2.,  2.]], dtype=float32)

事实上,调用b.eval(session=sess)只是调用sess.run(b)的语法糖。

整个讨论可能有点诡辩。鉴于所有不同的方法似乎返回相同的答案,哪个会话正在进行并不重要?直到您开始执行具有状态的计算时,显式会话才能展现其价值,这是您将在下一节中了解的主题。

TensorFlow 变量

本节中的所有示例代码都使用了常量张量。虽然我们可以以任何方式组合和重组这些张量,但我们永远无法更改张量本身的值(只能创建具有新值的新张量)。到目前为止,编程风格一直是函数式而不是有状态的。虽然函数式计算非常有用,但机器学习往往严重依赖有状态的计算。学习算法本质上是更新存储的张量以解释提供的数据的规则。如果无法更新这些存储的张量,学习将变得困难。

tf.Variable()类提供了一个围绕张量的包装器,允许进行有状态的计算。变量对象充当张量的持有者。创建变量非常容易(示例 2-25)。

示例 2-25。创建 TensorFlow 变量
>>> a = tf.Variable(tf.ones((2, 2)))
>>> a
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32_ref>

当我们尝试评估变量a,就像在示例 2-26 中一样,作为张量时会发生什么?

示例 2-26。评估未初始化的变量失败
>>> a.eval()
FailedPreconditionError: Attempting to use uninitialized value Variable

评估失败,因为必须显式初始化变量。初始化所有变量的最简单方法是调用tf.global_variables_initializer。在会话中运行此操作将初始化程序中的所有变量(示例 2-27)。

示例 2-27。评估初始化的变量
>>> sess = tf.Session()
>>> sess.run(tf.global_variables_initializer())
>>> a.eval(session=sess)
array([[ 1.,  1.],
       [ 1.,  1.]], dtype=float32)

初始化后,我们可以获取存储在变量中的值,就像它是一个普通的张量一样。到目前为止,变量没有比普通张量更有趣的地方。只有当我们可以对变量进行赋值时,变量才变得有趣。tf.assign()让我们可以做到这一点。使用tf.assign(),我们可以更新现有变量的值(示例 2-28)。

示例 2-28。为变量赋值
>>> sess.run(a.assign(tf.zeros((2,2))))
array([[ 0.,  0.],
       [ 0.,  0.]], dtype=float32)
>>> sess.run(a)
array([[ 0.,  0.],
       [ 0.,  0.]], dtype=float32)

如果我们尝试为变量a分配一个不是形状(2,2)的值会发生什么?让我们在示例 2-29 中找出答案。

示例 2-29。当形状不相等时,赋值失败
>>> sess.run(a.assign(tf.zeros((3,3))))
ValueError: Dimension 0 in both shapes must be equal, but are 2 and 3 for 'Assign_3'
(op: 'Assign') with input shapes: [2,2], [3,3].

你可以看到 TensorFlow 会抱怨。变量的形状在初始化时是固定的,必须在更新时保持不变。另一个有趣的地方是,tf.assign本身是底层全局tf.Graph实例的一部分。这使得 TensorFlow 程序可以在每次运行时更新其内部状态。在接下来的章节中,我们将大量使用这个特性。

回顾

在这一章中,我们介绍了张量的数学概念,并简要回顾了与张量相关的一些数学概念。然后我们演示了如何在 TensorFlow 中创建张量并在 TensorFlow 中执行相同的数学运算。我们还简要介绍了一些底层的 TensorFlow 结构,比如计算图、会话和变量。如果你还没有完全掌握本章讨论的概念,不要太担心。在本书的剩余部分中,我们将反复使用这些概念,所以会有很多机会让这些想法深入人心。

在下一章中,我们将教你如何使用 TensorFlow 为线性回归和逻辑回归构建简单的学习模型。随后的章节将在这些基础上构建,教你如何训练更复杂的模型。

第三章:使用 TensorFlow 进行线性和逻辑回归

本章将向您展示如何在 TensorFlow 中构建简单但非平凡的学习系统示例。本章的第一部分回顾了构建学习系统的数学基础,特别涵盖了函数、连续性和可微性。我们介绍了损失函数的概念,然后讨论了机器学习如何归结为找到复杂损失函数的最小点的能力。然后我们介绍了梯度下降的概念,并解释了如何使用它来最小化损失函数。我们最后简要讨论了自动微分的算法思想。第二部分重点介绍了这些数学思想支撑的 TensorFlow 概念。这些概念包括占位符、作用域、优化器和 TensorBoard,可以实现学习系统的实际构建和分析。最后一部分提供了如何在 TensorFlow 中训练线性和逻辑回归模型的案例研究。

本章很长,介绍了许多新的概念。如果您在第一次阅读时没有完全理解这些概念的微妙之处,那没关系。我们建议继续前进,以后有需要时再回来参考这里的概念。我们将在本书的其余部分中反复使用这些基础知识,以便让这些思想逐渐沉淀。

数学复习

本节回顾了概念上理解机器学习所需的数学工具。我们试图尽量减少所需的希腊符号数量,而是专注于建立概念理解而不是技术操作。

函数和可微性

本节将为您提供函数和可微性概念的简要概述。函数f是将输入映射到输出的规则。所有计算机编程语言中都有函数,数学上对函数的定义实际上并没有太大不同。然而,在物理学和工程学中常用的数学函数具有其他重要属性,如连续性和可微性。连续函数,粗略地说,是可以在不从纸上抬起铅笔的情况下绘制的函数,如图 3-1 所示。(这当然不是技术定义,但它捕捉了连续性条件的精神。)

continuous_1.gif

图 3-1。一些连续函数。

可微性是函数上的一种平滑条件。它表示函数中不允许有尖锐的角或转折(图 3-2)。

Math_images_4.jpg

图 3-2。一个可微函数。

可微函数的关键优势在于我们可以利用函数在特定点的斜率作为指导,找到函数高于或低于当前位置的地方。这使我们能够找到函数的最小值。可微函数f导数,表示为f ',是另一个函数,提供原始函数在所有点的斜率。概念上,函数在给定点的导数指示了函数高于或低于当前值的方向。优化算法可以遵循这个指示牌,向* f *的最小值靠近。在最小值处,函数的导数为零。

最初,导数驱动的优化的力量并不明显。几代微积分学生都在纸上进行枯燥的最小化函数练习中受苦。这些练习并不有用,因为找到具有少量输入参数的函数的最小值是一个最好通过图形方式完成的微不足道的练习。导数驱动的优化的力量只有在有数百、数千、数百万或数十亿个变量时才会显现出来。在这些规模上,通过解析理解函数几乎是不可能的,所有的可视化都是充满风险的练习,很可能会忽略函数的关键属性。在这些规模上,函数的梯度,一个多变量函数的f '的推广,很可能是理解函数及其行为的最强大的数学工具。我们将在本章后面更深入地探讨梯度。(概念上是这样;我们不会在这项工作中涵盖梯度的技术细节。)

在非常高的层面上,机器学习只是函数最小化的行为:学习算法只不过是适当定义的函数的最小值查找器。这个定义具有数学上的简单性优势。但是,这些特殊的可微函数是什么,它们如何在它们的最小值中编码有用的解决方案,我们如何找到它们呢?

损失函数

为了解决给定的机器学习问题,数据科学家必须找到一种构建函数的方法,其最小值编码了手头的现实世界问题的解决方案。幸运的是,对于我们这位不幸的数据科学家来说,机器学习文献已经建立了一个丰富的损失函数历史,执行这种编码。实际机器学习归结为理解不同类型的可用损失函数,并知道应该将哪种损失函数应用于哪些问题。换句话说,损失函数是将数据科学项目转化为数学的机制。所有的机器学习,以及大部分人工智能,都归结为创建正确的损失函数来解决手头的问题。我们将为您介绍一些常见的损失函数家族。

我们首先注意到,损失函数ℒ必须满足一些数学属性才能有意义。首先,ℒ必须使用数据点x和标签y。我们通过将损失函数写成ℒ ( x , y )来表示这一点。使用我们在上一章中的术语,xy都是张量,ℒ是从张量对到标量的函数。损失函数的函数形式应该是什么?人们常用的一个假设是使损失函数可加性。假设( x i , y i )是示例i的可用数据,并且总共有N个示例。那么损失函数可以分解为

ℒ ( x , y ) = ∑ i=1 N ℒ i ( x i , y i )

(在实践中,ℒ i 对于每个数据点都是相同的。)这种加法分解带来了许多有用的优势。首先是导数通过加法因子化,因此计算总损失的梯度简化如下:

∇ ℒ ( x , y ) = ∑ i=1 N ∇ ℒ i ( x i , y i )

这种数学技巧意味着只要较小的函数ℒ i是可微的,总损失函数也将是可微的。由此可见,设计损失函数的问题归结为设计较小函数ℒ i ( x i , y i )。在我们深入设计ℒ i之前,我们将方便地进行一个小的旁观,解释分类和回归问题之间的区别。

分类和回归

机器学习算法可以广泛地分为监督或无监督问题。监督问题是指数据点x和标签y都是可用的问题,而无监督问题只有数据点x没有标签y。一般来说,无监督机器学习更加困难且定义不明确(“理解”数据点x是什么意思?)。我们暂时不会深入讨论无监督损失函数,因为在实践中,大多数无监督损失都是巧妙地重新利用监督损失。

监督机器学习可以分为分类和回归两个子问题。分类问题是指您试图设计一个机器学习系统,为给定的数据点分配一个离散标签,比如 0/1(或更一般地0 , ⋯ , n)。回归是指设计一个机器学习系统,为给定的数据点附加一个实值标签(在ℝ)。

从高层来看,这些问题可能看起来相当不同。离散对象和连续对象通常在数学和常识上被不同对待。然而,机器学习中使用的一种技巧是使用连续、可微的损失函数来编码分类和回归问题。正如我们之前提到的,机器学习的很大一部分就是将复杂的现实系统转化为适当简单的可微函数的艺术。

在接下来的章节中,我们将向您介绍一对数学函数,这对函数将非常有用,可以将分类和回归任务转换为适当的损失函数。

L²损失

L²损失(读作ell-two损失)通常用于回归问题。L²损失(或者在其他地方通常称为L²范数)提供了一个向量大小的度量:

∥a∥ 2 = ∑ i=1 N a i 2

在这里,a被假定为长度为N的向量。L²范数通常用来定义两个向量之间的距离:

∥a-b∥ 2 = ∑ i=1 N (a i -b i ) 2

L²作为距离测量的概念在解决监督机器学习中的回归问题时非常有用。假设x是一组数据,y是相关标签。让f是一些可微函数,编码我们的机器学习模型。然后为了鼓励f预测y,我们创建L²损失函数。

ℒ ( x , y ) = ∥f(x)-y∥ 2

作为一个快速说明,在实践中通常不直接使用L²损失,而是使用它的平方。

∥a-b∥ 2 2 = ∑ i=1 N (a i -b i ) 2

为了避免在梯度中处理形式为1 / ( x )的术语。我们将在本章和本书的其余部分中反复使用平方L²损失。

概率分布

在介绍分类问题的损失函数之前,介绍概率分布将会很有用。首先,什么是概率分布,为什么我们应该关心它对机器学习有什么作用?概率是一个深奥的主题,因此我们只会深入到您获得所需的最低理解为止。在高层次上,概率分布提供了一个数学技巧,允许您将一组离散选择放松为一个连续的选择。例如,假设您需要设计一个机器学习系统,预测硬币是正面朝上还是反面朝上。看起来正面朝上/朝下似乎无法编码为连续函数,更不用说可微函数了。那么您如何使用微积分或 TensorFlow 的机制来解决涉及离散选择的问题呢?

进入概率分布。与硬选择不同,让分类器预测正面朝上或反面朝上的机会。例如,分类器可能学习预测正面的概率为 0.75,反面的概率为 0.25。请注意,概率是连续变化的!因此,通过使用离散事件的概率而不是事件本身,您可以巧妙地避开微积分无法真正处理离散事件的问题。

概率分布p简单地是所涉及的可能离散事件的概率列表。在这种情况下,p = (0.75, 0.25)。另外,您可以将p : { 0 , 1 } → ℝ视为从两个元素集合到实数的函数。这种观点在符号上有时会很有用。

我们简要指出,概率分布的技术定义更加复杂。将概率分布分配给实值事件是可行的。我们将在本章后面讨论这样的分布。

交叉熵损失

交叉熵是衡量两个概率分布之间距离的数学方法:

H ( p , q ) = - ∑ x p ( x ) log q ( x )

这里pq是两个概率分布。符号p(x)表示p赋予事件x的概率。这个定义值得仔细讨论。与L²范数一样,H提供了距离的概念。请注意,在p = q的情况下,

H ( p , p ) = - ∑ x p ( x ) log p ( x )

这个数量是p的熵,通常简单地写作H(p)。这是分布无序程度的度量;当所有事件等可能时,熵最大。H(p)总是小于或等于H(p, q)。事实上,分布q距离p越远,交叉熵就越大。我们不会深入探讨这些陈述的确切含义,但将交叉熵视为距离机制的直觉值得记住。

另外,请注意,与L²范数不同,H是不对称的!也就是说,H ( p , q ) ≠ H ( q , p )。因此,使用交叉熵进行推理可能有点棘手,最好谨慎处理。

回到具体问题,现在假设p = ( y , 1 - y )是具有两个结果的离散系统的真实数据分布,q = ( y pred , 1 - y pred )是机器学习系统预测的。那么交叉熵损失是

H ( p , q ) = y log y pred + ( 1 - y ) log ( 1 - y pred )

这种损失形式在机器学习系统中被广泛使用来训练分类器。经验上,最小化H(p, q)似乎能够构建出很好地复制提供的训练标签的分类器。

梯度下降

到目前为止,在这一章中,您已经了解了将函数最小化作为机器学习的代理的概念。简而言之,最小化适当的函数通常足以学会解决所需的任务。为了使用这个框架,您需要使用适当的损失函数,比如L²或H(p, q) 交叉熵,以将分类和回归问题转化为适当的损失函数。

可学习权重

到目前为止,在本章中,我们已经解释了机器学习是通过最小化适当定义的损失函数ℒ ( x , y )来实现的。也就是说,我们试图找到最小化它的损失函数ℒ的参数。然而,细心的读者会记得(x,y)是固定的量,不能改变。那么在学习过程中我们改变的是什么参数呢?

输入可学习权重W。假设f(x)是我们希望用机器学习模型拟合的可微函数。我们将规定f由选择W的方式进行参数化。也就是说,我们的函数实际上有两个参数f(W, x)。固定W的值会导致一个仅依赖于数据点x的函数。这些可学习权重实际上是通过最小化损失函数选择的量。我们将在本章后面看到如何使用tf.Variable来编码可学习权重。

但是,现在假设我们已经用适当的损失函数编码了我们的学习问题?在实践中,我们如何找到这个损失函数的最小值?我们将使用的关键技巧是梯度下降最小化。假设f是一个依赖于一些权重W的函数。那么∇ W表示的是在W中会最大程度增加f的方向变化。由此可知,朝着相反方向迈出一步会让我们更接近f的最小值。

梯度的符号

我们已经将可学习权重W的梯度写成了∇ W。有时,使用以下替代符号表示梯度会更方便:

∇ W = ∂ℒ ∂W

将这个方程理解为梯度∇ W编码了最大程度改变损失ℒ的方向。

梯度下降的思想是通过反复遵循负梯度来找到函数的最小值。从算法上讲,这个更新规则可以表示为

W = W - α ∇ W

其中α是步长,决定了新梯度∇ W被赋予多少权重。这个想法是每次都朝着∇ W的方向迈出许多小步。注意∇ W本身是W的一个函数,所以实际步骤在每次迭代中都会改变。每一步都对权重矩阵W进行一点更新。执行更新的迭代过程通常称为学习权重矩阵W

使用小批量高效计算梯度

一个问题是计算∇ W可能非常慢。隐含地,∇ W取决于损失函数ℒ。由于ℒ取决于整个数据集,对于大型数据集来说,计算∇ W可能会变得非常缓慢。在实践中,人们通常在称为minibatch的数据集的一部分上估计∇ W。每个 minibatch 通常包含 50-100 个样本。Minibatch 的大小是深度学习算法中的一个超参数。每个步骤α的步长是另一个超参数。深度学习算法通常具有超参数的集群,这些超参数本身不是通过随机梯度下降学习的。

可学习参数和超参数之间的这种张力是深度结构的弱点和优势之一。超参数的存在为利用专家的强烈直觉提供了很大的空间,而可学习参数则允许数据自己说话。然而,这种灵活性本身很快变成了一个弱点,对于超参数行为的理解有点像黑魔法,阻碍了初学者广泛部署深度学习。我们将在本书的后面花费大量精力讨论超参数优化。

我们通过介绍时代的概念来结束本节。一个时代是梯度下降算法在数据x上的完整遍历。更具体地说,一个时代包括需要查看给定 minibatch 大小的所有数据所需的梯度下降步骤。例如,假设一个数据集有 1,000 个数据点,训练使用大小为 50 的 minibatch。那么一个时代将包括 20 个梯度下降更新。每个训练时代增加了模型获得的有用知识量。从数学上讲,这将对应于训练集上损失函数值的减少。

早期的时代将导致损失函数的急剧下降。这个过程通常被称为在该数据集上学习先验。虽然看起来模型正在快速学习,但实际上它只是在调整自己以适应与手头问题相关的参数空间的部分。后续时代将对应于损失函数的较小下降,但通常在这些后续时代中才会发生有意义的学习。几个时代通常对于一个非平凡的模型来说时间太短,模型通常从 10-1,000 个时代或直到收敛进行训练。虽然这看起来很大,但重要的是要注意,所需的时代数量通常不随手头数据集的大小而增加。因此,梯度下降与数据大小成线性关系,而不是二次关系!这是随机梯度下降方法相对于其他学习算法的最大优势之一。更复杂的学习算法可能只需要对数据集进行一次遍历,但可能使用的总计算量与数据点数量成二次关系。在大数据集的时代,二次运行时间是一个致命的弱点。

跟踪损失函数随着周期数的减少可以是理解学习过程的极其有用的视觉简写。这些图通常被称为损失曲线(见图 3-4)。随着时间的推移,一个经验丰富的从业者可以通过快速查看损失曲线来诊断学习中的常见失败。我们将在本书的过程中对各种深度学习模型的损失曲线给予重要关注。特别是在本章后面,我们将介绍 TensorBoard,这是 TensorFlow 提供的用于跟踪诸如损失函数之类的量的强大可视化套件。

这些规则可以通过链式法则结合起来:

机器学习是定义适合数据集的损失函数,然后将其最小化的艺术。为了最小化损失函数,我们需要计算它们的梯度,并使用梯度下降算法迭代地减少损失。然而,我们仍然需要讨论梯度是如何实际计算的。直到最近,答案是“手动”。机器学习专家会拿出笔和纸,手动计算矩阵导数,以计算学习系统中所有梯度的解析公式。然后这些公式将被手动编码以实现学习算法。这个过程以前是臭名昭著的,不止一位机器学习专家在发表的论文和生产系统中意外梯度错误的故事被发现了多年。

图 3-4。一个模型的损失曲线示例。请注意,这个损失曲线来自使用真实梯度(即非小批量估计)训练的模型,因此比您在本书后面遇到的其他损失曲线更平滑。

这种情况已经发生了显著变化,随着自动微分引擎的广泛可用。像 TensorFlow 这样的系统能够自动计算几乎所有损失函数的梯度。这种自动微分是 TensorFlow 和类似系统的最大优势之一,因为机器学习从业者不再需要成为矩阵微积分的专家。然而,了解 TensorFlow 如何自动计算复杂函数的导数仍然很重要。对于那些在微积分入门课程中受苦的读者,你可能记得计算函数的导数是令人惊讶地机械化的。有一系列简单的规则可以应用于计算大多数函数的导数。例如:

数学显示="block"的d dx e x = e x

数学显示="block"的d dx x n = n x n-1

数学显示="block"的d dx f ( g ( x ) ) = f ' ( g ( x ) ) g ' ( x )

自动微分系统

其中 f ' 用于表示 f 的导数,g ' 用于表示 g 的导数。有了这些规则,很容易想象如何为一维微积分编写自动微分引擎。事实上,在基于 Lisp 的课程中,创建这样一个微分引擎通常是一年级的编程练习。(事实证明,正确解析函数比求导数更加困难。Lisp 使用其语法轻松解析公式,而在其他语言中,等到上编译器课程再做这个练习通常更容易)。

如何将这些规则扩展到更高维度的微积分?搞定数学更加棘手,因为需要考虑更多的数字。例如,给定 X = AB,其中 XAB 都是矩阵,公式变成了

∇ A = ∂L ∂A = ∂L ∂X B T = ( ∇ X ) B T

这样的公式可以组合起来提供一个矢量和张量微积分的符号微分系统。

使用 TensorFlow 进行学习

在本章的其余部分,我们将介绍您学习使用 TensorFlow 创建基本机器学习模型所需的概念。我们将从介绍玩具数据集的概念开始,然后解释如何使用常见的 Python 库创建有意义的玩具数据集。接下来,我们将讨论新的 TensorFlow 想法,如占位符、喂养字典、名称范围、优化器和梯度。下一节将向您展示如何使用这些概念训练简单的回归和分类模型。

创建玩具数据集

在本节中,我们将讨论如何创建简单但有意义的合成数据集,或称为玩具数据集,用于训练简单的监督分类和回归模型。

对 NumPy 的(极其)简要介绍

我们将大量使用 NumPy 来定义有用的玩具数据集。NumPy 是一个允许操作张量(在 NumPy 中称为 ndarray)的 Python 包。示例 3-1 展示了一些基础知识。

示例 3-1。一些基本 NumPy 用法示例
>>> import numpy as np
>>> np.zeros((2,2))
array([[ 0.,  0.],
       [ 0.,  0.]])
>>> np.eye(3)
array([[ 1.,  0.,  0.],
       [ 0.,  1.,  0.],
       [ 0.,  0.,  1.]])

您可能会注意到 NumPy ndarray 操作看起来与 TensorFlow 张量操作非常相似。这种相似性是 TensorFlow 架构师特意设计的。许多关键的 TensorFlow 实用函数具有与 NumPy 中类似函数的参数和形式。出于这个目的,我们不会试图深入介绍 NumPy,并相信读者通过实验来掌握 NumPy 的用法。有许多在线资源提供了 NumPy 的教程介绍。

为什么玩具数据集很重要?

在机器学习中,学会正确使用玩具数据集通常至关重要。学习是具有挑战性的,初学者经常犯的一个最常见的错误是尝试在太早的时候在复杂数据上学习非平凡的模型。这些尝试往往以惨败告终,想要成为机器学习者的人会灰心丧气,认为机器学习不适合他们。

当然,真正的罪魁祸首不是学生,而是真实世界数据集具有许多特殊性。经验丰富的数据科学家已经了解到,真实世界数据集通常需要许多清理和预处理转换才能适合学习。深度学习加剧了这个问题,因为大多数深度学习模型对数据中的不完美非常敏感。诸如广泛范围的回归标签或潜在的强噪声模式等问题可能会使梯度下降方法出现问题,即使其他机器学习算法(如随机森林)也不会有问题。

幸运的是,几乎总是可以解决这些问题,但这可能需要数据科学家具有相当的复杂技能。这些敏感性问题可能是机器学习作为一种技术商品化的最大障碍。我们将深入探讨数据清理策略,但目前,我们建议一个更简单的替代方案:使用玩具数据集!

玩具数据集对于理解学习算法至关重要。给定非常简单的合成数据集,可以轻松判断算法是否学习了正确的规则。在更复杂的数据集上,这种判断可能非常具有挑战性。因此,在本章的其余部分,我们将只使用玩具数据集,同时涵盖基于 TensorFlow 的梯度下降学习的基础知识。在接下来的章节中,我们将深入研究具有真实数据的案例研究。

使用高斯分布添加噪声

早些时候,我们讨论了离散概率分布作为将离散选择转换为连续值的工具。我们也提到了连续概率分布的概念,但没有深入探讨。

连续概率分布(更准确地称为概率密度函数)是用于建模可能具有一系列结果的随机事件的有用数学工具。对于我们的目的,将概率密度函数视为用于模拟数据收集中的某些测量误差的有用工具就足够了。高斯分布被广泛用于噪声建模。

如图 3-5 所示,注意高斯分布可以具有不同的均值μ和标准差σ。高斯分布的均值是它取的平均值,而标准差是围绕这个平均值的扩散的度量。一般来说,将高斯随机变量添加到某个数量上提供了一种结构化的方式,通过使其稍微变化来模糊这个数量。这是一个非常有用的技巧,用于生成非平凡的合成数据集。

gaussian.png

图 3-5。不同均值和标准差的各种高斯概率分布的插图。

我们迅速指出高斯分布也被称为正态分布。均值为μ,标准差为σ的高斯分布写为N ( μ , σ )。这种简写符号很方便,我们将在接下来的章节中多次使用它。

玩具回归数据集

最简单的线性回归形式是学习一维线的参数。假设我们的数据点x是一维的。然后假设实值标签y由线性规则生成

y = w x + b

在这里,wb是必须通过梯度下降从数据中估计出来的可学习参数。为了测试我们是否可以使用 TensorFlow 学习这些参数,我们将生成一个由直线上的点组成的人工数据集。为了使学习挑战稍微困难一些,我们将在数据集中添加少量高斯噪声。

让我们写下我们的直线方程,受到少量高斯噪声的干扰:

y = w x + b + N ( 0 , ϵ )

这里ϵ是噪声项的标准差。然后我们可以使用 NumPy 从这个分布中生成一个人工数据集,如示例 3-2 所示。

示例 3-2. 使用 NumPy 对人工数据集进行抽样
# Generate synthetic data
N = 100
w_true = 5
b_true = 2
noise_scale = .1
x_np = np.random.rand(N, 1)
noise = np.random.normal(scale=noise_scale, size=(N, 1))
# Convert shape of y_np to (N,)
y_np = np.reshape(w_true * x_np + b_true + noise, (-1))

我们使用 Matplotlib 在图 3-6 中绘制这个数据集(您可以在与本书相关的GitHub 存储库中找到确切的绘图代码)以验证合成数据看起来是否合理。如预期的那样,数据分布是一条直线,带有少量测量误差。

lr_data.png

图 3-6. 玩具回归数据分布的绘图。

玩具分类数据集

创建合成分类数据集有点棘手。从逻辑上讲,我们希望有两个不同的、容易分离的点类。假设数据集只包含两种类型的点,(-1,-1)和(1,1)。然后学习算法将不得不学习一个将这两个数据值分开的规则。

  • y[0] = (-1, -1)

  • y[1] = (1, 1)

与以前一样,让我们通过向两种类型的点添加一些高斯噪声来增加一些挑战:

  • y[0] = (-1, -1) + N(0, ϵ)

  • y[1] = (1, 1) + N(0, ϵ)

然而,这里有一点小技巧。我们的点是二维的,而我们之前引入的高斯噪声是一维的。幸运的是,存在高斯的多变量扩展。我们不会在这里讨论多变量高斯的复杂性,但您不需要理解这些复杂性来跟随我们的讨论。

在示例 3-3 中生成合成数据集的 NumPy 代码比线性回归问题稍微棘手,因为我们必须使用堆叠函数np.vstack将两种不同类型的数据点组合在一起,并将它们与不同的标签关联起来。(我们使用相关函数np.concatenate将一维标签组合在一起。)

示例 3-3. 使用 NumPy 对玩具分类数据集进行抽样
# Generate synthetic data
N = 100
# Zeros form a Gaussian centered at (-1, -1)
# epsilon is .1
x_zeros = np.random.multivariate_normal(
    mean=np.array((-1, -1)), cov=.1*np.eye(2), size=(N/2,))
y_zeros = np.zeros((N/2,))
# Ones form a Gaussian centered at (1, 1)
# epsilon is .1
x_ones = np.random.multivariate_normal(
    mean=np.array((1, 1)), cov=.1*np.eye(2), size=(N/2,))
y_ones = np.ones((N/2,))

x_np = np.vstack([x_zeros, x_ones])
y_np = np.concatenate([y_zeros, y_ones])

图 3-7 使用 Matplotlib 绘制了这段代码生成的数据,以验证分布是否符合预期。我们看到数据分布在两个清晰分开的类中。

logistic_data.png

图 3-7. 玩具分类数据分布的绘图。

新的 TensorFlow 概念

在 TensorFlow 中创建简单的机器学习系统将需要您学习一些新的 TensorFlow 概念。

占位符

占位符是将信息输入到 TensorFlow 计算图中的一种方式。将占位符视为信息进入 TensorFlow 的输入节点。用于创建占位符的关键函数是tf.placeholder(示例 3-4)。

示例 3-4. 创建一个 TensorFlow 占位符
>>> tf.placeholder(tf.float32, shape=(2,2))
<tf.Tensor 'Placeholder:0' shape=(2, 2) dtype=float32>

我们将使用占位符将数据点x和标签y馈送到我们的回归和分类算法中。

馈送字典和获取

回想一下,我们可以通过sess.run(var)在 TensorFlow 中评估张量。那么我们如何为占位符提供值呢?答案是构建feed 字典。Feed 字典是 Python 字典,将 TensorFlow 张量映射到包含这些占位符具体值的np.ndarray对象。Feed 字典最好被视为 TensorFlow 计算图的输入。那么输出是什么?TensorFlow 称这些输出为fetches。您已经见过 fetches 了。我们在上一章中广泛使用了它们,但没有这样称呼;fetch 是一个张量(或张量),其值是在计算图中的计算(使用 feed 字典中的占位符值)完成后检索的(示例 3-5)。

示例 3-5。使用 fetches
>>> a = tf.placeholder(tf.float32, shape=(1,))
>>> b = tf.placeholder(tf.float32, shape=(1,))
>>> c = a + b
>>> with tf.Session() as sess:
        c_eval = sess.run(c, {a: [1.], b: [2.]})
        print(c_eval)
[ 3.]

命名空间

在复杂的 TensorFlow 程序中,将在整个程序中定义许多张量、变量和占位符。tf.name_scope(name)为管理这些变量集合提供了一个简单的作用域机制(示例 3-6)。在tf.name_scope(name)调用的作用域内创建的所有计算图元素将在其名称前加上name

这种组织工具在与 TensorBoard 结合使用时最有用,因为它有助于可视化系统自动将图元素分组到相同的命名空间中。您将在下一节中进一步了解 TensorBoard。

示例 3-6。使用命名空间来组织占位符
>>> N = 5
>>> with tf.name_scope("placeholders"):
      x = tf.placeholder(tf.float32, (N, 1))
      y = tf.placeholder(tf.float32, (N,))
>>> x
<tf.Tensor 'placeholders/Placeholder:0' shape=(5, 1) dtype=float32>

优化器

在前两节介绍的基本概念已经暗示了在 TensorFlow 中如何进行机器学习。您已经学会了如何为数据点和标签添加占位符,以及如何使用张量操作定义损失函数。缺失的部分是您仍然不知道如何使用 TensorFlow 执行梯度下降。

实际上,可以直接在 Python 中使用 TensorFlow 原语定义优化算法,TensorFlow 在tf.train模块中提供了一系列优化算法。这些算法可以作为节点添加到 TensorFlow 计算图中。

我应该使用哪个优化器?

tf.train中有许多可能的优化器可用。简短预览中包括tf.train.GradientDescentOptimizertf.train.MomentumOptimizertf.train.AdagradOptimizertf.train.AdamOptimizer等。这些不同优化器之间有什么区别呢?

几乎所有这些优化器都是基于梯度下降的思想。回想一下我们之前介绍的简单梯度下降规则:

W = W - α ∇ W

从数学上讲,这个更新规则是原始的。研究人员发现了许多数学技巧,可以在不使用太多额外计算的情况下实现更快的优化。一般来说,tf.train.AdamOptimizer是一个相对稳健的好默认值。(许多优化方法对超参数的选择非常敏感。对于初学者来说,最好避开更复杂的方法,直到他们对不同优化算法的行为有很好的理解。)

示例 3-7 是一小段代码,它向计算图中添加了一个优化器,用于最小化预定义的损失l

示例 3-7。向 TensorFlow 计算图添加 Adam 优化器
learning_rate = .001
with tf.name_scope("optim"):
  train_op = tf.train.AdamOptimizer(learning_rate).minimize(l)

使用 TensorFlow 计算梯度

我们之前提到,在 TensorFlow 中直接实现梯度下降算法是可能的。虽然大多数用例不需要重新实现tf.train的内容,但直接查看梯度值以进行调试可能很有用。tf.gradients提供了一个有用的工具来实现这一点(示例 3-8)。

示例 3-8。直接计算梯度
>>> W = tf.Variable((3,))
>>> l = tf.reduce_sum(W)
>>> gradW = tf.gradients(l, W)
>>> gradW
[<tf.Tensor 'gradients/Sum_grad/Tile:0' shape=(1,) dtype=int32>]

这段代码符号地拉下了损失l相对于可学习参数(tf.VariableW的梯度。tf.gradients返回所需梯度的列表。请注意,梯度本身也是张量!TensorFlow 执行符号微分,这意味着梯度本身是计算图的一部分。TensorFlow 符号梯度的一个很好的副作用是,可以在 TensorFlow 中堆叠导数。这对于更高级的算法有时可能是有用的。

TensorBoard 的摘要和文件写入器

对张量程序结构有一个视觉理解是非常有用的。TensorFlow 团队提供了 TensorBoard 包来实现这个目的。TensorBoard 启动一个 Web 服务器(默认情况下在 localhost 上),显示 TensorFlow 程序的各种有用的可视化。然而,为了能够使用 TensorBoard 检查 TensorFlow 程序,程序员必须手动编写日志记录语句。tf.train.FileWriter()指定了 TensorBoard 程序的日志目录,tf.summary将各种 TensorFlow 变量的摘要写入指定的日志目录。在本章中,我们只会使用tf.summary.scalar,它总结了一个标量量,以跟踪损失函数的值。tf.summary.merge_all()是一个有用的日志辅助工具,它将多个摘要合并为一个摘要以方便使用。

示例 3-9 中的代码片段为损失添加了一个摘要,并指定了一个日志目录。

示例 3-9。为损失添加一个摘要
with tf.name_scope("summaries"):
  tf.summary.scalar("loss", l)
  merged = tf.summary.merge_all()

train_writer = tf.summary.FileWriter('/tmp/lr-train', tf.get_default_graph())

使用 TensorFlow 训练模型

假设现在我们已经为数据点和标签指定了占位符,并且已经用张量操作定义了一个损失。我们已经在计算图中添加了一个优化器节点train_op,我们可以使用它来执行梯度下降步骤(虽然我们实际上可能使用不同的优化器,但为了方便起见,我们将更新称为梯度下降)。我们如何迭代地执行梯度下降来在这个数据集上学习?

简单的答案是我们使用 Python 的for循环。在每次迭代中,我们使用sess.run()来获取图中的train_op以及合并的摘要操作merged和损失l。我们使用一个 feed 字典将所有数据点和标签输入sess.run()

示例 3-10 中的代码片段演示了这种简单的学习方法。请注意,出于教学简单性的考虑,我们不使用小批量。在接下来的章节中,代码将在训练更大的数据集时使用小批量。

示例 3-10。训练模型的简单示例
n_steps = 1000
with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())
  # Train model
  for i in range(n_steps):
    feed_dict = {x: x_np, y: y_np}
    _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
    print("step %d, loss: %f" % (i, loss))
    train_writer.add_summary(summary, i)

在 TensorFlow 中训练线性和逻辑模型

本节将在前一节介绍的所有 TensorFlow 概念上进行总结,以在我们在本章中之前介绍的玩具数据集上训练线性和逻辑回归模型。

在 TensorFlow 中的线性回归

在本节中,我们将提供代码来定义一个在 TensorFlow 中学习其权重的线性回归模型。这个任务很简单,你可以很容易地在没有 TensorFlow 的情况下完成。然而,在 TensorFlow 中做这个练习是很好的,因为它将整合我们在本章中介绍的新概念。

在 TensorFlow 中定义和训练线性回归

线性回归模型很简单:

y = w x + b

这里wb是我们希望学习的权重。我们将这些权重转换为tf.Variable对象。然后我们使用张量操作构建L²损失:

ℒ ( x , y ) = (y-wx-b) 2

示例 3-11 中的代码在 TensorFlow 中实现了这些数学操作。它还使用tf.name_scope来分组各种操作,并添加了tf.train.AdamOptimizer用于学习和tf.summary操作用于 TensorBoard 的使用。

示例 3-11. 定义线性回归模型
# Generate tensorflow graph
with tf.name_scope("placeholders"):
  x = tf.placeholder(tf.float32, (N, 1))
  y = tf.placeholder(tf.float32, (N,))
with tf.name_scope("weights"):
  # Note that x is a scalar, so W is a single learnable weight.
  W = tf.Variable(tf.random_normal((1, 1)))
  b = tf.Variable(tf.random_normal((1,)))
with tf.name_scope("prediction"):
  y_pred = tf.matmul(x, W) + b
with tf.name_scope("loss"):
  l = tf.reduce_sum((y - y_pred)**2)
# Add training op
with tf.name_scope("optim"):
  # Set learning rate to .001 as recommended above.
  train_op = tf.train.AdamOptimizer(.001).minimize(l)
with tf.name_scope("summaries"):
  tf.summary.scalar("loss", l)
  merged = tf.summary.merge_all()

train_writer = tf.summary.FileWriter('/tmp/lr-train', tf.get_default_graph())

示例 3-12 然后训练这个模型,如之前讨论的(不使用小批量)。

示例 3-12. 训练线性回归模型
n_steps = 1000
with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())
  # Train model
  for i in range(n_steps):
    feed_dict = {x: x_np, y: y_np}
    _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
    print("step %d, loss: %f" % (i, loss))
    train_writer.add_summary(summary, i)

此示例的所有代码都在与本书相关的GitHub 存储库中提供。我们鼓励所有读者运行线性回归示例的完整脚本,以获得对学习算法如何运行的第一手感觉。这个示例足够小,读者不需要访问任何专用计算硬件来运行。

线性回归的梯度

我们建模的线性系统的方程是y = wx + b,其中wb是可学习的权重。正如我们之前提到的,这个系统的损失是ℒ = (y-wx-b) 2。一些矩阵微积分可以用来直接计算w的可学习参数的梯度:

∇ w = ∂ℒ ∂w = - 2 ( y - w x - b ) x T

对于b

∇ b = ∂ℒ ∂b = - 2 ( y - w x - b )

我们将这些方程放在这里,仅供好奇的读者参考。我们不会试图系统地教授如何计算我们在本书中遇到的损失函数的导数。然而,我们将指出,对于复杂系统,通过手工计算损失函数的导数有助于建立对深度网络学习方式的直觉。这种直觉可以作为设计者的强大指导,因此我们鼓励高级读者自行探索这个主题。

使用 TensorBoard 可视化线性回归模型

前一节中定义的模型使用tf.summary.FileWriter将日志写入日志目录*/tmp/lr-train*。我们可以使用示例 3-13 中的命令在此日志目录上调用 TensorBoard(TensorBoard 默认与 TensorFlow 一起安装)。

示例 3-13. 调用 TensorBoard
tensorboard --logdir=/tmp/lr-train

此命令将在连接到 localhost 的端口上启动 TensorBoard。使用浏览器打开此端口。TensorBoard 屏幕将类似于图 3-8。 (具体外观可能会因您使用的 TensorBoard 版本而有所不同。)

tensorboard_lr_raw.png

图 3-8. TensorBoard 面板截图。

转到 Graphs 选项卡,您将看到我们定义的 TensorFlow 架构的可视化,如图 3-9 所示。

lr_graph.png

图 3-9. 在 TensorBoard 中可视化线性回归架构。

请注意,此可视化已将属于各种tf.name_scopes的所有计算图元素分组。不同的组根据计算图中的依赖关系连接。您可以展开所有分组的元素以查看其内容。图 3-10 展示了扩展的架构。

正如您所看到的,有许多隐藏的节点突然变得可见!TensorFlow 的函数,如tf.train.AdamOptimizer,通常会在它们自己的tf.name_scope下隐藏许多内部变量。在 TensorBoard 中展开提供了一种简单的方法,可以查看系统实际创建了什么。虽然可视化看起来相当复杂,但大多数细节都是在幕后,您暂时不需要担心。

lr_expanded.png

图 3-10。架构的扩展可视化。

返回主页选项卡并打开摘要部分。现在您应该看到一个类似于图 3-11 的损失曲线。请注意平滑下降的形状。损失在开始时迅速下降,然后逐渐减少并稳定下来。

lr_loss_tensorboard.png

图 3-11。在 TensorBoard 中查看损失曲线。

视觉和非视觉调试风格

像 TensorBoard 这样的工具是否必要才能充分利用像 TensorFlow 这样的系统?这取决于情况。使用 GUI 或交互式调试器是否是成为专业程序员的必要条件?

不同的程序员有不同的风格。有些人会发现 TensorBoard 的可视化能力成为张量编程工作流程中至关重要的一部分。其他人可能会发现 TensorBoard 并不是特别有用,会更多地使用打印语句进行调试。张量编程和调试的这两种风格都是有效的,就像有些优秀的程序员信誓旦旦地使用调试器,而有些则憎恶它们一样。

总的来说,TensorBoard 对于调试和建立对手头数据集的基本直觉非常有用。我们建议您遵循最适合您的风格。

用于评估回归模型的指标

到目前为止,我们还没有讨论如何评估训练模型是否真正学到了东西。评估模型是否训练的第一个工具是查看损失曲线,以确保其具有合理的形状。您在上一节中学习了如何做到这一点。接下来要尝试什么?

现在我们希望您查看与模型相关的指标。指标是用于比较预测标签和真实标签的工具。对于回归问题,有两个常见的指标:R²和 RMSE(均方根误差)。R²是两个变量之间相关性的度量,取值介于+1 和 0 之间。+1 表示完美相关,而 0 表示没有相关性。在数学上,两个数据集XYR²定义如下:

R 2 = cov(X,Y) 2 σ X 2 σ Y 2

其中 cov(X, Y)是XY的协方差,衡量两个数据集共同变化的程度,而σ X和σ Y是标准差,衡量每个集合的变化程度。直观地说,R²衡量了每个集合中独立变化的多少可以通过它们的共同变化来解释。

多种类型的 R²!

请注意,实践中有两种常见的R²定义。一个常见的初学者(和专家)错误是混淆这两个定义。在本书中,我们将始终使用平方的皮尔逊相关系数(图 3-12)。另一种定义称为确定系数。这种另一种R²通常更加令人困惑,因为它不像平方的皮尔逊相关系数那样有 0 的下限。

在图 3-12 中,预测值和真实值高度相关,R²接近 1。看起来学习在这个系统上做得很好,并成功学习到了真实规则。不要那么快。您会注意到图中两个轴的刻度不同!原来R²不会因为刻度的差异而受到惩罚。为了理解这个系统发生了什么,我们需要考虑图 3-13 中的另一个度量。

lr_pred.png

图 3-12。绘制皮尔逊相关系数。

lr_learned.png

图 3-13。绘制均方根误差(RMSE)。

RMSE 是预测值和真实值之间平均差异的度量。在图 3-13 中,我们将预测值和真实标签作为两个单独的函数绘制,使用数据点x作为我们的 x 轴。请注意,学习到的线并不是真实函数!RMSE 相对较高,诊断了错误,而R²没有发现这个错误。

这个系统发生了什么?为什么尽管经过训练收敛,TensorFlow 仍然没有学习到正确的函数?这个例子很好地说明了梯度下降算法的一个弱点。不能保证找到真正的解决方案!梯度下降算法可能会陷入局部最小值。也就是说,它可能找到看起来不错的解决方案,但实际上并不是损失函数ℒ的最低最小值。

那么为什么要使用梯度下降呢?对于简单的系统,确实往往最好避免梯度下降,而使用其他性能更好的算法。然而,在复杂的系统中,比如我们将在后面的章节中展示的系统,还没有比梯度下降表现更好的替代算法。我们鼓励您记住这一点,因为我们将继续深入学习。

TensorFlow 中的逻辑回归

在本节中,我们将使用 TensorFlow 定义一个简单的分类器。首先考虑分类器的方程是什么。通常使用的数学技巧是利用 S 形函数。S 形函数,在图 3-14 中绘制,通常用σ表示,是从实数ℝ到(0, 1)的函数。这个特性很方便,因为我们可以将 S 形函数的输出解释为事件发生的概率。(将离散事件转换为连续值的技巧是机器学习中的一个常见主题。)

logistic.gif

图 3-14。绘制 S 形函数。

用于预测离散 0/1 变量概率的方程如下。这些方程定义了一个简单的逻辑回归模型:

y 0 = σ ( w x + b )y 1 = 1 - σ ( w x + b )

TensorFlow 提供了用于计算 S 形值的交叉熵损失的实用函数。其中最简单的函数是tf.nn.sigmoid_cross_​entropy_with_logits。(对数几率是 S 形函数的反函数。实际上,这意味着直接将参数传递给 TensorFlow,而不是 S 形值σ ( w x + b )本身)。我们建议使用 TensorFlow 的实现,而不是手动定义交叉熵,因为在计算交叉熵损失时会出现棘手的数值问题。

示例 3-14 在 TensorFlow 中定义了一个简单的逻辑回归模型。

示例 3-14。定义一个简单的逻辑回归模型
# Generate tensorflow graph
with tf.name_scope("placeholders"):
  # Note that our datapoints x are 2-dimensional.
  x = tf.placeholder(tf.float32, (N, 2))
  y = tf.placeholder(tf.float32, (N,))
with tf.name_scope("weights"):
  W = tf.Variable(tf.random_normal((2, 1)))
  b = tf.Variable(tf.random_normal((1,)))
with tf.name_scope("prediction"):
  y_logit = tf.squeeze(tf.matmul(x, W) + b)
  # the sigmoid gives the class probability of 1
  y_one_prob = tf.sigmoid(y_logit)
  # Rounding P(y=1) will give the correct prediction.
  y_pred = tf.round(y_one_prob)

with tf.name_scope("loss"):
  # Compute the cross-entropy term for each datapoint
  entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=y_logit, labels=y)
  # Sum all contributions
  l = tf.reduce_sum(entropy)
with tf.name_scope("optim"):
  train_op = tf.train.AdamOptimizer(.01).minimize(l)

  train_writer = tf.summary.FileWriter('/tmp/logistic-train', tf.get_default_graph())

示例 3-15 中的此模型的训练代码与线性回归模型的代码相同。

示例 3-15。训练逻辑回归模型
n_steps = 1000
with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())
  # Train model
  for i in range(n_steps):
    feed_dict = {x: x_np, y: y_np}
    _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
    print("loss: %f" % loss)
    train_writer.add_summary(summary, i)

使用 TensorBoard 可视化逻辑回归模型

与之前一样,您可以使用 TensorBoard 来可视化模型。首先,像图 3-15 中所示,可视化损失函数。请注意,与以前一样,损失函数遵循一种整齐的模式。损失函数有一个陡峭的下降,然后是逐渐平滑。

logistic_loss_tensorboard.png

图 3-15。可视化逻辑回归损失函数。

您还可以在 TensorBoard 中查看 TensorFlow 图。由于作用域结构类似于线性回归所使用的结构,简化的图显示方式并没有太大不同,如图 3-16 所示。

logistic_graph.png

图 3-16。可视化逻辑回归的计算图。

然而,如果您扩展这个分组图中的节点,就像图 3-17 中所示,您会发现底层的计算图是不同的。特别是,损失函数与线性回归所使用的损失函数有很大不同(这是应该的)。

logistic_expanded.png

图 3-17。逻辑回归的扩展计算图。

评估分类模型的指标

现在您已经为逻辑回归训练了一个分类模型,需要了解适用于评估分类模型的指标。虽然逻辑回归的方程比线性回归的方程复杂,但基本的评估指标更简单。分类准确率只是检查学习模型正确分类的数据点的比例。实际上,稍加努力,就可以推导出逻辑回归模型学习的分隔线。这条线显示了模型学习到的分隔正负示例的边界。(我们将推导这条线从逻辑回归方程中的练习留给感兴趣的读者。解决方案在本节的代码中。)

我们在图 3-18 中显示了学习到的类别和分隔线。请注意,这条线清晰地分隔了正负示例,并且具有完美的准确率(1.0)。这个结果提出了一个有趣的观点。回归通常比分类更难解决。在图 3-18 中,有许多可能的线可以很好地分隔数据点,但只有一条线可以完美地匹配线性回归的数据。

logistic_pred.png

图 3-18。查看逻辑回归的学习类别和分隔线。

回顾

在本章中,我们向您展示了如何在 TensorFlow 中构建和训练一些简单的学习系统。我们首先回顾了一些基础数学概念,包括损失函数和梯度下降。然后,我们向您介绍了一些新的 TensorFlow 概念,如占位符、作用域和 TensorBoard。我们以在玩具数据集上训练线性和逻辑回归系统的案例研究结束了本章。本章涵盖了很多内容,如果您还没有完全掌握,也没关系。本章介绍的基础知识将贯穿本书的其余部分。

在第四章中,我们将向您介绍您的第一个深度学习模型和全连接网络,并向您展示如何在 TensorFlow 中定义和训练全连接网络。在接下来的章节中,我们将探索更复杂的深度网络,但所有这些架构都将使用本章介绍的相同基本学习原则。