tf-dl-merge-1

251 阅读1小时+

Tensorflow 深度学习(二)

原文:Tensorflow for deep learning

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:全连接的深度网络

本章将向您介绍全连接的深度网络。全连接网络是深度学习的主力军,用于成千上万的应用。全连接网络的主要优势在于它们是“结构不可知的”。也就是说,不需要对输入做出特殊的假设(例如,输入由图像或视频组成)。我们将利用这种通用性,使用全连接的深度网络来解决本章后面的化学建模问题。

我们简要探讨支撑全连接网络的数学理论。特别是,我们探讨全连接架构是“通用逼近器”,能够学习任何函数的概念。这个概念解释了全连接架构的通用性,但也伴随着我们深入讨论的许多注意事项。

虽然结构不可知使全连接网络非常广泛适用,但这种网络的性能往往比针对问题空间结构调整的专用网络要弱。我们将在本章后面讨论全连接架构的一些限制。

什么是全连接的深度网络?

全连接神经网络由一系列全连接层组成。全连接层是从ℝ m到ℝ n的函数。每个输出维度都依赖于每个输入维度。在图 4-1 中,全连接层的图示如下。

FCLayer.png

图 4-1. 深度网络中的全连接层。

让我们更深入地了解全连接网络的数学形式。让x ∈ ℝ m表示全连接层的输入。让y i ∈ ℝ是全连接层的第 i 个输出。那么y i ∈ ℝ的计算如下:

y i = σ ( w 1 x 1 + ⋯ + w m x m )

在这里,σ 是一个非线性函数(暂时将σ视为前一章介绍的 Sigmoid 函数),w i 是网络中可学习的参数。完整的输出y如下:

y = σ ( w 1,1 x 1 + ⋯ + w 1,m x m ) ⋮ σ ( w n,1 x 1 + ⋯ + w n,m x m )

请注意,可以直接堆叠全连接网络。具有多个全连接网络的网络通常被称为“深度”网络,如图 4-2 所示。

multilayer_fcnet.png

图 4-2。一个多层深度全连接网络。

作为一个快速实现的注意事项,注意单个神经元的方程看起来非常类似于两个向量的点积(回想一下张量基础的讨论)。对于一层神经元,通常为了效率目的,将y计算为矩阵乘积是很方便的:

y = σ ( w x )

其中 sigma 是一个矩阵在ℝ n×m,非线性σ是逐分量应用的。

全连接网络中的“神经元”

全连接网络中的节点通常被称为“神经元”。因此,在文献中,全连接网络通常被称为“神经网络”。这种命名方式在很大程度上是历史的偶然。

在 1940 年代,沃伦·S·麦卡洛克和沃尔特·皮茨发表了一篇关于大脑的第一个数学模型,认为神经元能够计算布尔量上的任意函数。这项工作的后继者稍微完善了这个逻辑模型,通过使数学“神经元”成为在零和一之间变化的连续函数。如果这些函数的输入足够大,神经元就会“发射”(取值为一),否则就是静止的。通过可调权重的添加,这个描述与之前的方程匹配。

这才是真正的神经元行为吗?当然不是!一个真实的神经元(图 4-3)是一个极其复杂的引擎,拥有超过 100 万亿个原子,以及数以万计的不同信号蛋白质,能够对不同信号做出反应。微处理器比一个一行方程更好地类比于神经元。

neuron.png

图 4-3。神经元的更生物学准确的表示。

在许多方面,生物神经元和人工神经元之间的这种脱节是非常不幸的。未经培训的专家读到令人激动的新闻稿,声称已经创建了拥有数十亿“神经元”的人工神经网络(而大脑只有 1000 亿个生物神经元),并且合理地认为科学家们已经接近创造人类水平的智能。不用说,深度学习的最新技术距离这样的成就还有几十年(甚至几个世纪)的距离。

当您进一步了解深度学习时,您可能会遇到关于人工智能的夸大宣传。不要害怕指出这些声明。目前的深度学习是一套在快速硬件上解决微积分问题的技术。它不是终结者的前身(图 4-4)。

terminator.png

图 4-4。不幸的是(或者也许是幸运的),这本书不会教你如何构建一个终结者!

AI 寒冬

人工智能经历了多轮繁荣和衰退的发展。这种循环性的发展是该领域的特点。每一次学习的新进展都会引发一波乐观情绪,其中预言家声称人类水平(或超人类)的智能即将出现。几年后,没有这样的智能体现出来,失望的资助者退出。由此产生的时期被称为 AI 寒冬。

迄今为止已经有多次 AI 寒冬。作为一种思考练习,我们鼓励您考虑下一次 AI 寒冬将在何时发生。当前的深度学习进展解决了比以往任何一波进步更多的实际问题。AI 是否可能最终脱颖而出,摆脱繁荣和衰退的周期,或者您认为我们很快就会迎来 AI 的“大萧条”?

使用反向传播学习全连接网络

完全连接的神经网络的第一个版本是感知器(图 4-5),由 Frank Rosenblatt 在 1950 年代创建。这些感知器与我们在前面的方程中介绍的“神经元”是相同的。

perceptron.png

图 4-5。感知器的示意图。

感知器是通过自定义的“感知器”规则进行训练的。虽然它们在解决简单问题时有一定用处,但感知器在根本上受到限制。1960 年代末 Marvin Minsky 和 Seymour Papert 的书《感知器》证明了简单感知器无法学习 XOR 函数。图 4-6 说明了这个说法的证明。

xor2.gif

图 4-6。感知器的线性规则无法学习感知器。

这个问题通过多层感知器(另一个称为深度全连接网络的名称)的发明得以解决。这一发明是一个巨大的成就,因为早期的简单学习算法无法有效地学习深度网络。 “信用分配”问题困扰着它们;算法如何决定哪个神经元学习什么?

解决这个问题的完整方法需要反向传播。反向传播是学习神经网络权重的通用规则。不幸的是,关于反向传播的复杂解释在文献中泛滥。这种情况很不幸,因为反向传播只是自动微分的另一个说法。

假设f ( θ , x )是代表深度全连接网络的函数。这里x是完全连接网络的输入,θ是可学习的权重。然后,反向传播算法简单地计算∂f ∂θ。在实践中,实现反向传播以处理所有可能出现的f函数的复杂性。幸运的是,TensorFlow 已经为我们处理了这一点!

通用收敛定理

前面的讨论涉及到了深度全连接网络是强大逼近的想法。McCulloch 和 Pitts 表明逻辑网络可以编码(几乎)任何布尔函数。Rosenblatt 的感知器是 McCulloch 和 Pitt 的逻辑函数的连续模拟,但被 Minsky 和 Papert 证明在根本上受到限制。多层感知器试图解决简单感知器的限制,并且在经验上似乎能够学习复杂函数。然而,从理论上讲,尚不清楚这种经验能力是否存在未被发现的限制。 1989 年,George Cybenko 证明了多层感知器能够表示任意函数。这一演示为全连接网络作为学习架构的普遍性主张提供了相当大的支持,部分解释了它们持续受欢迎的原因。

然而,如果在上世纪 80 年代后期人们已经理解了反向传播和全连接网络理论,为什么“深度”学习没有更早变得更受欢迎呢?这种失败的很大一部分是由于计算能力的限制;学习全连接网络需要大量的计算能力。此外,由于对好的超参数缺乏理解,深度网络非常难以训练。因此,计算要求较低的替代学习算法,如 SVM,变得更受欢迎。深度学习近年来的流行部分原因是更好的计算硬件的增加可用性,使计算速度更快,另一部分原因是对能够实现稳定学习的良好训练方案的增加理解。

通用逼近是否令人惊讶?

通用逼近性质在数学中比人们可能期望的更常见。例如,Stone-Weierstrass 定理证明了在闭区间上的任何连续函数都可以是一个合适的多项式函数。进一步放宽我们的标准,泰勒级数和傅里叶级数本身提供了一些通用逼近能力(在它们的收敛域内)。通用收敛在数学中相当常见的事实部分地为经验观察提供了部分理由,即许多略有不同的全连接网络变体似乎具有通用逼近性质。

通用逼近并不意味着通用学习!

通用逼近定理中存在一个关键的微妙之处。全连接网络可以表示任何函数并不意味着反向传播可以学习任何函数!反向传播的一个主要限制是没有保证全连接网络“收敛”;也就是说,找到学习问题的最佳可用解决方案。这个关键的理论差距让几代计算机科学家对神经网络感到不安。即使在今天,许多学者仍然更愿意使用具有更强理论保证的替代算法。

经验研究已经产生了许多实用技巧,使反向传播能够为问题找到好的解决方案。在本章的其余部分中,我们将深入探讨许多这些技巧。对于实践数据科学家来说,通用逼近定理并不是什么需要太认真对待的东西。这是令人放心的,但深度学习的艺术在于掌握使学习有效的实用技巧。

为什么要使用深度网络?

通用逼近定理中的一个微妙之处是,事实上它对只有一个全连接层的全连接网络也成立。那么,具有多个全连接层的“深度”学习有什么用呢?事实证明,这个问题在学术和实践领域仍然颇具争议。

在实践中,似乎更深层的网络有时可以在大型数据集上学习更丰富的模型。(然而,这只是一个经验法则;每个实践者都有许多例子,深度全连接网络表现不佳。)这一观察结果导致研究人员假设更深层的网络可以更有效地表示复杂函数。也就是说,相比具有相同数量的神经元的较浅网络,更深的网络可能能够学习更复杂的函数。例如,在第一章中简要提到的 ResNet 架构,具有 130 层,似乎胜过其较浅的竞争对手,如 AlexNet。一般来说,对于固定的神经元预算,堆叠更深层次会产生更好的结果。

文献中提出了一些关于深度网络优势的错误“证明”,但它们都有漏洞。深度与宽度的问题似乎涉及到复杂性理论中的深刻概念(研究解决给定计算问题所需的最小资源量)。目前看来,理论上证明(或否定)深度网络的优越性远远超出了我们数学家的能力范围。

训练全连接神经网络

正如我们之前提到的,全连接网络的理论与实践有所不同。在本节中,我们将向您介绍一些关于全连接网络的经验观察,这些观察有助于从业者。我们强烈建议您使用我们的代码(在本章后面介绍)来验证我们的说法。

可学习表示

一种思考全连接网络的方式是,每个全连接层都会对问题所在的特征空间进行转换。在工程和物理学中,将问题的表示转换为更易处理的形式的想法是非常古老的。因此,深度学习方法有时被称为“表示学习”。(有趣的事实是,深度学习的一个主要会议被称为“国际学习表示会议”。)

几代分析师已经使用傅立叶变换、勒让德变换、拉普拉斯变换等方法,将复杂的方程和函数简化为更适合手工分析的形式。一种思考深度学习网络的方式是,它们实现了一个适合手头问题的数据驱动转换。

执行特定于问题的转换能力可能非常强大。标准的转换技术无法解决图像或语音分析的问题,而深度网络能够相对轻松地解决这些问题,这是由于学习表示的固有灵活性。这种灵活性是有代价的:深度架构学习到的转换通常比傅立叶变换等数学变换要不那么通用。尽管如此,将深度变换纳入分析工具包中可以成为一个强大的问题解决工具。

有一个合理的观点认为,深度学习只是第一个有效的表示学习方法。将来,可能会有替代的表示学习方法取代深度学习方法。

激活函数

我们之前介绍了非线性函数σ作为 S 形函数。虽然 S 形函数是全连接网络中的经典非线性,但近年来研究人员发现其他激活函数,特别是修正线性激活(通常缩写为 ReLU 或 relu)σ ( x ) = max ( x , 0 )比 S 形单元效果更好。这种经验观察可能是由于深度网络中的梯度消失问题。对于 S 形函数,几乎所有输入值的斜率都为零。因此,对于更深的网络,梯度会趋近于零。对于 ReLU 函数,输入空间的大部分部分斜率都不为零,允许非零梯度传播。图 4-7 展示了 S 形和 ReLU 激活函数并排的情况。

activation-functions.png

图 4-7。S 形和 ReLU 激活函数。

全连接网络记忆

全连接网络的一个显著特点是,给定足够的时间,它们倾向于完全记住训练数据。因此,将全连接网络训练到“收敛”实际上并不是一个有意义的度量。只要用户愿意等待,网络将继续训练和学习。

对于足够大的网络,训练损失趋向于零是非常常见的。这一经验观察是全连接网络的通用逼近能力最实用的证明之一。然而,请注意,训练损失趋向于零并不意味着网络已经学会了一个更强大的模型。相反,模型很可能已经开始记忆训练集的怪癖,这些怪癖并不适用于任何其他数据点。

值得深入探讨这里我们所说的奇特之处。高维统计学的一个有趣特性是,给定足够大的数据集,将有大量的虚假相关性和模式可供选择。在实践中,全连接网络完全有能力找到并利用这些虚假相关性。控制网络并防止它们以这种方式行为不端对于建模成功至关重要。

正则化

正则化是一个通用的统计术语,用于限制记忆化,同时促进可泛化的学习。有许多不同类型的正则化可用,我们将在接下来的几节中介绍。

不是您的统计学家的正则化

正则化在统计文献中有着悠久的历史,有许多关于这个主题的论文。不幸的是,只有一部分经典分析适用于深度网络。在统计学中广泛使用的线性模型可能与深度网络表现出截然不同,而在那种情况下建立的许多直觉对于深度网络来说可能是错误的。

与深度网络一起工作的第一条规则,特别是对于具有先前统计建模经验的读者,是相信经验结果胜过过去的直觉。不要假设对于建模深度架构等技术的过去知识有太多意义。相反,建立一个实验来系统地测试您提出的想法。我们将在下一章更深入地讨论这种系统化实验过程。

Dropout

Dropout 是一种正则化形式,它随机地删除一些输入到全连接层的节点的比例(图 4-8)。在这里,删除一个节点意味着其对应激活函数的贡献被设置为 0。由于没有激活贡献,被删除节点的梯度也降为零。

dropout.png

图 4-8。在训练时,Dropout 随机删除网络中的神经元。从经验上看,这种技术通常为网络训练提供强大的正则化。

要删除的节点是在梯度下降的每一步中随机选择的。底层的设计原则是网络将被迫避免“共适应”。简而言之,我们将解释什么是共适应以及它如何在非正则化的深度架构中出现。假设深度网络中的一个神经元学习了一个有用的表示。那么网络中更深层的其他神经元将迅速学会依赖于该特定神经元获取信息。这个过程将使网络变得脆弱,因为网络将过度依赖于该神经元学到的特征,而这些特征可能代表数据集的一个怪癖,而不是学习一个普遍规则。

Dropout 可以防止这种协同适应,因为不再可能依赖于单个强大的神经元的存在(因为该神经元在训练期间可能会随机消失)。因此,其他神经元将被迫“弥补空缺”并学习到有用的表示。理论上的论点是,这个过程应该会产生更强大的学习模型。

在实践中,dropout 有一对经验效果。首先,它防止网络记忆训练数据;使用 dropout 后,即使对于非常大的深度网络,训练损失也不会迅速趋向于 0。其次,dropout 倾向于略微提升模型对新数据的预测能力。这种效果通常适用于各种数据集,这也是 dropout 被认为是一种强大的发明而不仅仅是一个简单的统计技巧的部分原因。

你应该注意,在进行预测时应关闭 dropout。忘记关闭 dropout 可能导致预测比原本更加嘈杂和无用。我们将在本章后面正确讨论如何处理训练和预测中的 dropout。

大型网络如何避免过拟合?

对于传统训练有素的统计学家来说,最令人震惊的一点是,深度网络可能经常具有比训练数据中存在的内部自由度更多的内部自由度。在传统统计学中,这些额外的自由度的存在会使模型变得无用,因为不再存在一个保证模型学到的是“真实”的经典意义上的保证。

那么,一个拥有数百万参数的深度网络如何能够在只有数千个示例的数据集上学习到有意义的结果呢?Dropout 可以在这里起到很大的作用,防止蛮力记忆。但是,即使没有使用 dropout,深度网络也会倾向于学习到有用的事实,这种倾向可能是由于反向传播或全连接网络结构的某种特殊性质,我们尚不理解。

早停止

正如前面提到的,全连接网络往往会记住放在它们面前的任何东西。因此,在实践中,跟踪网络在一个保留的“验证”集上的表现,并在该验证集上的表现开始下降时停止网络,通常是很有用的。这种简单的技术被称为早停止。

在实践中,早停止可能会很棘手。正如你将看到的,深度网络的损失曲线在正常训练过程中可能会有很大的变化。制定一个能够区分健康变化和明显下降趋势的规则可能需要很大的努力。在实践中,许多从业者只是训练具有不同(固定)时代数量的模型,并选择在验证集上表现最好的模型。图 4-9 展示了训练和测试集准确率随着训练进行而通常变化的情况。

earlystopping.png

图 4-9. 训练和测试集的模型准确率随着训练进行而变化。

我们将在接下来的章节中更深入地探讨与验证集一起工作的正确方法。

权重正则化

从统计学文献中借鉴的一种经典正则化技术惩罚那些权重增长较大的学习权重。根据前一章的符号表示,让ℒ ( x , y )表示特定模型的损失函数,让θ表示该模型的可学习参数。那么正则化的损失函数定义如下

ℒ ' ( x , y ) = ℒ ( x , y ) + α ∥ θ ∥

其中∥ θ ∥是权重惩罚,α是一个可调参数。惩罚的两种常见选择是L¹和L²惩罚

∥θ∥ 2 = ∑ i=1 N θ i 2∥θ∥ 1 = ∑ i=1 N | θ i |

其中∥θ∥ 2和∥θ∥ 1分别表示L¹和L²的惩罚。从个人经验来看,这些惩罚对于深度模型来说往往不如 dropout 和早停止有用。一些从业者仍然使用权重正则化,因此值得了解如何在调整深度网络时应用这些惩罚。

训练全连接网络

训练全连接网络需要一些技巧,超出了您在本书中迄今为止看到的内容。首先,与之前的章节不同,我们将在更大的数据集上训练模型。对于这些数据集,我们将向您展示如何使用 minibatches 来加速梯度下降。其次,我们将回到调整学习率的话题。

Minibatching

对于大型数据集(甚至可能无法完全装入内存),在每一步计算梯度时无法在整个数据集上进行。相反,从业者通常选择一小部分数据(通常是 50-500 个数据点)并在这些数据点上计算梯度。这小部分数据传统上被称为一个 minibatch。

在实践中,minibatching 似乎有助于收敛,因为可以在相同的计算量下进行更多的梯度下降步骤。minibatch 的正确大小是一个经验性问题,通常通过超参数调整来设置。

学习率

学习率决定了每个梯度下降步骤的重要性。设置正确的学习率可能会有些棘手。许多初学者设置学习率不正确,然后惊讶地发现他们的模型无法学习或开始返回 NaN。随着 ADAM 等方法的发展,这种情况已经得到了显著改善,但如果模型没有学到任何东西,调整学习率仍然是值得的。

在 TensorFlow 中的实现

在这一部分中,我们将向您展示如何在 TensorFlow 中实现一个全连接网络。在这一部分中,我们不需要引入太多新的 TensorFlow 原语,因为我们已经涵盖了大部分所需的基础知识。

安装 DeepChem

在这一部分中,您将使用 DeepChem 机器学习工具链进行实验(完整披露:其中一位作者是 DeepChem 的创始人)。有关 DeepChem 的详细安装说明可以在线找到,但简要地说,通过conda工具进行的 Anaconda 安装可能是最方便的。

Tox21 数据集

在我们的建模案例研究中,我们将使用一个化学数据集。毒理学家对使用机器学习来预测给定化合物是否有毒非常感兴趣。这个任务非常复杂,因为当今的科学只对人体内发生的代谢过程有有限的了解。然而,生物学家和化学家已经研究出一套有限的实验,可以提供毒性的指示。如果一个化合物在这些实验中是“命中”的,那么人类摄入后可能会有毒。然而,这些实验通常成本很高,因此数据科学家旨在构建能够预测这些实验结果的机器学习模型,用于新分子。

最重要的毒理学数据集之一称为 Tox21。它由 NIH 和 EPA 发布,作为数据科学倡议的一部分,并被用作模型构建挑战中的数据集。这个挑战的获胜者使用了多任务全连接网络(全连接网络的一种变体,其中每个网络为每个数据点预测多个数量)。我们将分析来自 Tox21 集合中的一个数据集。该数据集包含一组经过测试与雄激素受体相互作用的 10,000 种分子。数据科学挑战是预测新分子是否会与雄激素受体相互作用。

处理这个数据集可能有些棘手,因此我们将利用 DeepChem 部分作为 MoleculeNet 数据集收集。DeepChem 将 Tox21 中的每个分子处理为长度为 1024 的比特向量。然后加载数据集只需几个简单的调用到 DeepChem 中(示例 4-1)。

示例 4-1. 加载 Tox21 数据集
import deepchem as dc

_, (train, valid, test), _ = dc.molnet.load_tox21()
train_X, train_y, train_w = train.X, train.y, train.w
valid_X, valid_y, valid_w = valid.X, valid.y, valid.w
test_X, test_y, test_w = test.X, test.y, test.w

这里的 X 变量保存处理过的特征向量,y 保存标签,w 保存示例权重。标签是与雄激素受体相互作用或不相互作用的化合物的二进制 1/0。Tox21 拥有不平衡数据集,其中正例远远少于负例。w 保存建议的每个示例权重,给予正例更多的重视(增加罕见示例的重要性是处理不平衡数据集的常见技术)。为简单起见,我们在训练过程中不使用这些权重。所有这些变量都是 NumPy 数组。

Tox21 拥有比我们这里将要分析的更多数据集,因此我们需要删除与这些额外数据集相关联的标签(示例 4-2)。

示例 4-2. 从 Tox21 中删除额外的数据集
# Remove extra tasks
train_y = train_y[:, 0]
valid_y = valid_y[:, 0]
test_y = test_y[:, 0]
train_w = train_w[:, 0]
valid_w = valid_w[:, 0]
test_w = test_w[:, 0]

接受占位符的小批量

在之前的章节中,我们创建了接受固定大小参数的占位符。在处理小批量数据时,能够输入不同大小的批次通常很方便。假设一个数据集有 947 个元素。那么以小批量大小为 50,最后一个批次将有 47 个元素。这将导致 第三章 中的代码崩溃。幸运的是,TensorFlow 对这种情况有一个简单的解决方法:使用 None 作为占位符的维度参数允许占位符在该维度上接受任意大小的张量(示例 4-3)。

示例 4-3. 定义接受不同大小小批量的占位符
d = 1024
with tf.name_scope("placeholders"):
  x = tf.placeholder(tf.float32, (None, d))
  y = tf.placeholder(tf.float32, (None,))

注意 d 是 1024,即我们特征向量的维度。

实现隐藏层

实现隐藏层的代码与我们在上一章中看到的用于实现逻辑回归的代码非常相似,如 示例 4-4 所示。

示例 4-4. 定义一个隐藏层
with tf.name_scope("hidden-layer"):
  W = tf.Variable(tf.random_normal((d, n_hidden)))
  b = tf.Variable(tf.random_normal((n_hidden,)))
  x_hidden = tf.nn.relu(tf.matmul(x, W) + b)

我们使用 tf.name_scope 将引入的变量分组在一起。请注意,我们使用全连接层的矩阵形式。我们使用形式 xW 而不是 Wx,以便更方便地处理一次输入的小批量。(作为练习,尝试计算涉及的维度,看看为什么会这样。)最后,我们使用内置的 tf.nn.relu 激活函数应用 ReLU 非线性。

完全连接层的其余代码与上一章中用于逻辑回归的代码非常相似。为了完整起见,我们展示了用于指定网络的完整代码在例 4-5 中使用。作为一个快速提醒,所有模型的完整代码都可以在与本书相关的 GitHub 存储库中找到。我们强烈建议您尝试运行代码。

例 4-5。定义完全连接的架构
with tf.name_scope("placeholders"):
  x = tf.placeholder(tf.float32, (None, d))
  y = tf.placeholder(tf.float32, (None,))
with tf.name_scope("hidden-layer"):
  W = tf.Variable(tf.random_normal((d, n_hidden)))
  b = tf.Variable(tf.random_normal((n_hidden,)))
  x_hidden = tf.nn.relu(tf.matmul(x, W) + b)
with tf.name_scope("output"):
  W = tf.Variable(tf.random_normal((n_hidden, 1)))
  b = tf.Variable(tf.random_normal((1,)))
  y_logit = tf.matmul(x_hidden, 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
  y_expand = tf.expand_dims(y, 1)
  entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=y_logit, labels=y_expand)
  # Sum all contributions
  l = tf.reduce_sum(entropy)

with tf.name_scope("optim"):
  train_op = tf.train.AdamOptimizer(learning_rate).minimize(l)

with tf.name_scope("summaries"):
  tf.summary.scalar("loss", l)
  merged = tf.summary.merge_all()

向隐藏层添加 dropout

TensorFlow 负责为我们实现 dropout,内置原语tf.nn.dropout(x, keep_prob),其中keep_prob是保留任何给定节点的概率。回想一下我们之前的讨论,我们希望在训练时打开 dropout,在进行预测时关闭 dropout。为了正确处理这一点,我们将引入一个新的占位符keep_prob,如例 4-6 所示。

例 4-6。为丢失概率添加一个占位符
keep_prob = tf.placeholder(tf.float32)

在训练期间,我们传入所需的值,通常为 0.5,但在测试时,我们将keep_prob设置为 1.0,因为我们希望使用所有学习节点进行预测。通过这种设置,在前一节中指定的完全连接网络中添加 dropout 只是一行额外的代码(例 4-7)。

例 4-7。定义一个带有 dropout 的隐藏层
with tf.name_scope("hidden-layer"):
  W = tf.Variable(tf.random_normal((d, n_hidden)))
  b = tf.Variable(tf.random_normal((n_hidden,)))
  x_hidden = tf.nn.relu(tf.matmul(x, W) + b)
  # Apply dropout
  x_hidden = tf.nn.dropout(x_hidden, keep_prob)

实现小批量处理

为了实现小批量处理,我们需要在每次调用sess.run时提取一个小批量的数据。幸运的是,我们的特征和标签已经是 NumPy 数组,我们可以利用 NumPy 对数组的方便语法来切片数组的部分(例 4-8)。

例 4-8。在小批量上进行训练
step = 0
for epoch in range(n_epochs):
  pos = 0
  while pos < N:
    batch_X = train_X[pos:pos+batch_size]
    batch_y = train_y[pos:pos+batch_size]
    feed_dict = {x: batch_X, y: batch_y, keep_prob: dropout_prob}
    _, summary, loss = sess.run([train_op, merged, l], feed_dict=feed_dict)
    print("epoch %d, step %d, loss: %f" % (epoch, step, loss))
    train_writer.add_summary(summary, step)

    step += 1
    pos += batch_size

评估模型准确性

为了评估模型的准确性,标准做法要求在未用于训练的数据上测量模型的准确性(即验证集)。然而,数据不平衡使这一点变得棘手。我们在上一章中使用的分类准确度指标简单地衡量了被正确标记的数据点的比例。然而,我们数据集中 95%的数据被标记为 0,只有 5%被标记为 1。因此,全 0 模型(将所有内容标记为负面的模型)将实现 95%的准确性!这不是我们想要的。

更好的选择是增加正例的权重,使其更重要。为此,我们使用 MoleculeNet 推荐的每个示例权重来计算加权分类准确性,其中正样本的权重是负样本的 19 倍。在这种加权准确性下,全 0 模型的准确率将达到 50%,这似乎更为合理。

对于计算加权准确性,我们使用sklearn.metrics中的函数accuracy_score(true, pred, sample_weight=given_sample_weight。这个函数有一个关键字参数sample_weight,让我们可以为每个数据点指定所需的权重。我们使用这个函数在训练集和验证集上计算加权指标(例 4-9)。

例 4-9。计算加权准确性
train_weighted_score = accuracy_score(train_y, train_y_pred, sample_weight=train_w)
print("Train Weighted Classification Accuracy: %f" % train_weighted_score)
valid_weighted_score = accuracy_score(valid_y, valid_y_pred, sample_weight=valid_w)
print("Valid Weighted Classification Accuracy: %f" % valid_weighted_score)

虽然我们可以自己重新实现这个函数,但有时使用 Python 数据科学基础设施中的标准函数会更容易(并且更少出错)。了解这种基础设施和可用函数是作为一名实践数据科学家的一部分。现在,我们可以训练模型(在默认设置下进行 10 个时期)并评估其准确性:

Train Weighted Classification Accuracy: 0.742045
Valid Weighted Classification Accuracy: 0.648828

在第五章中,我们将向您展示系统地提高这种准确性的方法,并更仔细地调整我们的完全连接模型。

使用 TensorBoard 跟踪模型收敛

现在我们已经指定了我们的模型,让我们使用 TensorBoard 来检查模型。让我们首先在 TensorBoard 中检查图结构(图 4-10)。

该图与逻辑回归的图类似,只是增加了一个新的隐藏层。让我们扩展隐藏层,看看里面有什么(图 4-11)。

fcgraph.png

图 4-10。可视化全连接网络的计算图。

hidden_expand.png

图 4-11。可视化全连接网络的扩展计算图。

您可以看到这里如何表示新的可训练变量和 dropout 操作。一切看起来都在正确的位置。让我们通过查看随时间变化的损失曲线来结束(图 4-12)。

fcnet_loss_curve.png

图 4-12。可视化全连接网络的损失曲线。

正如我们在前一节中看到的那样,损失曲线呈下降趋势。但是,让我们放大一下,看看这个损失在近距离下是什么样子的(图 4-13)。

fcnet_zoomed_loss.png

图 4-13。放大损失曲线的一部分。

请注意,损失看起来更加崎岖!这是使用小批量训练的代价之一。我们不再拥有在前几节中看到的漂亮、平滑的损失曲线。

回顾

在本章中,我们向您介绍了全连接深度网络。我们深入研究了这些网络的数学理论,并探讨了“通用逼近”的概念,这在一定程度上解释了全连接网络的学习能力。我们以一个案例研究结束,您在该案例中训练了一个深度全连接架构的 Tox21 数据集。

在本章中,我们还没有向您展示如何调整全连接网络以实现良好的预测性能。在第五章中,我们将讨论“超参数优化”,即调整网络参数的过程,并让您调整本章介绍的 Tox21 网络的参数。

第五章:超参数优化

训练一个深度模型和训练一个好的深度模型是非常不同的事情。虽然从互联网上复制粘贴一些 TensorFlow 代码以运行第一个原型是很容易的,但要将该原型转变为高质量模型则更加困难。将原型转变为高质量模型的过程涉及许多步骤。我们将在本章的其余部分探讨其中一个步骤,即超参数优化。

首次近似地说,超参数优化是调整模型中所有不是通过梯度下降学习的参数的过程。这些量被称为“超参数”。考虑一下前一章中的全连接网络。虽然全连接网络的权重可以从数据中学习,但网络的其他设置不能。这些超参数包括隐藏层的数量、每个隐藏层的神经元数量、学习率等。如何系统地找到这些量的良好值?超参数优化方法为我们提供了这个问题的答案。

回想一下我们之前提到过,模型性能是在一个保留的“验证”集上进行跟踪的。超参数优化方法系统地在验证集上尝试多种超参数选择。表现最佳的超参数值集合然后在第二个保留的“测试”集上进行评估,以衡量真实的模型性能。不同的超参数优化方法在它们用来提出新的超参数设置的算法上有所不同。这些算法从显而易见的到相当复杂的不等。在这些章节中,我们只会涵盖一些较简单的方法,因为更复杂的超参数优化技术往往需要大量的计算能力。

作为一个案例研究,我们将调整第四章中介绍的 Tox21 毒性全连接网络,以获得良好的性能。我们强烈鼓励您(一如既往)使用与本书相关的GitHub 存储库中的代码自行运行超参数优化方法。

超参数优化不仅适用于深度网络!

值得强调的是,超参数优化不仅适用于深度网络。大多数形式的机器学习算法都有无法通过默认学习方法学习的参数。这些参数也被称为超参数。在本章的后面部分,您将看到随机森林(另一种常见的机器学习方法)的一些超参数示例。

然而值得注意的是,深度网络往往对超参数选择更为敏感,而不同于其他算法。虽然随机森林可能会因为超参数的默认选择而表现稍差,但深度网络可能完全无法学习。因此,掌握超参数优化是一名潜在的深度学习者的关键技能。

模型评估和超参数优化

在之前的章节中,我们只是简要地讨论了如何判断一个机器学习模型是否好。任何模型性能的测量都必须评估模型的泛化能力。也就是说,模型能否对它从未见过的数据点进行预测?模型性能的最佳测试是创建一个模型,然后在模型构建之后可用的数据上进行前瞻性评估。然而,这种测试方式通常难以定期进行。在设计阶段,一名实践数据科学家可能希望评估许多不同类型的模型或学习算法,以找到最佳的那个。

解决这一困境的方法是将可用数据集的一部分作为验证集“保留”。这个验证集将用于衡量不同模型的性能(具有不同的超参数选择)。最好还要有第二个保留集,即测试集,用于评估超参数选择方法选择的最终模型的性能。

假设您有一百个数据点。一个简单的程序是使用其中 80 个数据点来训练潜在模型,使用 20 个保留的数据点来验证模型选择。然后可以通过模型在保留的 20 个数据点上的“分数”来跟踪所提出模型的“好坏”。通过提出新设计并仅接受那些在保留集上表现更好的模型,可以逐步改进模型。

然而,在实践中,这个过程会导致过拟合。从业者很快会了解保留集的特殊性,并调整模型结构以在保留集上人为提高分数。为了应对这一问题,从业者通常将保留集分为两部分:一部分用于超参数验证,另一部分用于最终模型验证。在这种情况下,假设您保留了 10 个数据点用于验证,另外 10 个用于最终测试。这将被称为 80/10/10 数据分割。

为什么测试集是必要的?

值得注意的一个重要观点是,超参数优化方法本身就是一种学习算法形式。特别是,它们是一种用于设置不易通过基于微积分的分析处理的不可微量的学习算法。超参数学习算法的“训练集”就是保留的验证集。

总的来说,在训练集上衡量模型性能并没有太多意义。与往常一样,学到的量必须具有泛化性,因此有必要在不同的集合上测试性能。由于训练集用于基于梯度的学习,验证集用于超参数学习,因此测试集是必要的,以评估学到的超参数在新数据上的泛化能力。

黑盒学习算法

黑盒学习算法假设它们试图优化的系统没有结构信息。大多数超参数方法都是黑盒的;它们适用于任何类型的深度学习或机器学习算法。

总的来说,黑盒方法不像白盒方法(如梯度下降)那样具有良好的可扩展性,因为它们往往会在高维空间中迷失。由于黑盒方法缺乏来自梯度的方向信息,它们甚至可能在 50 维空间中迷失(在实践中优化 50 个超参数是相当具有挑战性的)。

要理解为什么,假设有 50 个超参数,每个超参数有 3 个潜在值。那么黑盒算法必须盲目搜索一个大小为3 50的空间。这是可以做到的,但通常需要大量的计算能力。

度量,度量,度量。

在选择超参数时,您希望选择那些使您设计的模型更准确的超参数。在机器学习中,度量是一个函数,用于衡量经过训练模型的预测准确性。超参数优化是为了优化使度量在验证集上最大化(或最小化)的超参数。虽然这一听起来很简单,但准确性的概念实际上可能相当微妙。假设您有一个二元分类器。是更重要的是永远不要将假样本误标为真样本,还是永远不要将真样本误标为假样本?如何选择满足应用需求的模型超参数?

答案是选择正确的指标。在本节中,我们将讨论许多不同的分类和回归问题的指标。我们将评论每个指标强调的特点。没有最佳指标,但对于不同的应用程序,有更合适和不太合适的指标。

指标不能替代常识!

指标是非常盲目的。它们只优化一个数量。因此,盲目优化指标可能导致完全不合适的结果。在网络上,媒体网站经常选择优化“用户点击”这一指标。然后,一些有抱负的年轻记者或广告商意识到像“当 X 发生时,您绝对不会相信发生了什么”这样的标题会导致用户点击的比例更高。于是,点击诱饵诞生了。虽然点击诱饵标题确实会诱使读者点击,但它们也会让读者失去兴趣,并导致他们避免在充斥着点击诱饵的网站上花费时间。优化用户点击导致用户参与度和信任度下降。

这里的教训是普遍的。优化一个指标往往会以另一个数量为代价。确保您希望优化的数量确实是“正确”的数量。机器学习似乎仍然需要人类判断,这是不是很有趣呢?

二元分类指标

在介绍二元分类模型的指标之前,我们认为您会发现学习一些辅助量是有用的。当二元分类器对一组数据点进行预测时,您可以将所有这些预测分为四类之一(表 5-1)。

表 5-1. 预测类别

类别含义
真阳性(TP)预测为真,标签为真
假阳性(FP)预测为真,标签为假
真阴性(TN)预测为假,标签为假
假阴性(FN)预测为假,标签为真

我们还将介绍表 5-2 中显示的符号。

表 5-2. 正负

类别含义
P正标签的数量
N负标签的数量

一般来说,最小化假阳性和假阴性的数量是非常可取的。然而,对于任何给定的数据集,通常由于信号的限制,往往不可能同时最小化假阳性和假阴性。因此,有各种指标提供假阳性和假阴性之间的各种权衡。这些权衡对于应用程序可能非常重要。假设您正在设计乳腺癌的医学诊断。那么,将一个健康患者标记为患有乳腺癌将是一个假阳性。将一个乳腺癌患者标记为没有这种疾病将是一个假阴性。这两种结果都是不可取的,设计正确的平衡是生物伦理学中一个棘手的问题。

我们将展示一些不同的指标,平衡不同比例的假阳性和假阴性(表 5-3)。每个比例都优化了不同的平衡,我们将更详细地探讨其中一些。

表 5-3. 二元指标表

指标定义
准确率(TP + TN)/(P + N)
精确率TP/(TP + FP)
召回率TP/(TP + FN) = TP/P
特异性TN/(FP + TN) = TN/N
假阳性率(FPR)FP/(FP + TN) = FP/N
假阴性率(FNR)FN/(TP + FN) = FN/P

准确率是最简单的指标。它简单地计算分类器正确预测的比例。在简单的应用中,准确率应该是从业者首选的指标。在准确率之后,精确度召回率是最常测量的指标。精确度简单地衡量了被预测为正类的数据点实际上是正类的比例。召回率则衡量了分类器标记为正类的正类标记数据点的比例。特异度衡量了被正确分类的负类标记数据点的比例。假阳率衡量了被错误分类为正类的负类标记数据点的比例。假阴率是被错误标记为负类的正类标记数据点的比例。

这些指标强调分类器性能的不同方面。它们还可以用于构建一些更复杂的二元分类器性能测量。例如,假设您的二元分类器输出类别概率,而不仅仅是原始预测。那么,就会出现选择截断的问题。也就是说,在什么正类概率下您将输出标记为实际正类?最常见的答案是 0.5,但通过选择更高或更低的截断,通常可以手动调整精确度、召回率、FPR 和 TPR 之间的平衡。这些权衡通常以图形方式表示。

接收器操作特征曲线(ROC)绘制了真正率和假正率之间的权衡,随着截断概率的变化(参见图 5-1)。

roc_intro3.png

图 5-1。接收器操作特征曲线(ROC)。

接收器操作特征曲线(ROC-AUC)下的曲线下面积(AUC)是一个常用的指标。ROC-AUC 指标很有用,因为它提供了二元分类器在所有截断选择下的全局图像。一个完美的指标将具有 ROC-AUC 1.0,因为真正率将始终被最大化。作为比较,一个随机分类器将具有 ROC-AUC 0.5。ROC-AUC 在不平衡数据集中通常很有用,因为全局视图部分考虑了数据集中的不平衡。

多类别分类指标

许多常见的机器学习任务需要模型输出不仅仅是二元分类标签。例如,ImageNet 挑战(ILSVRC)要求参赛者构建能够识别提供图像中的一千个潜在对象类别中的哪一个的模型。或者在一个更简单的例子中,也许您想要预测明天的天气,提供的类别是“晴天”、“下雨”和“多云”。如何衡量这种模型的性能?

最简单的方法是使用准确率的直接泛化,它衡量了被分类器正确标记的数据点的比例(表 5-4)。

表 5-4。多类别分类指标

指标定义
准确率正确标记的数量/数据点数量

我们注意到确实存在诸如精确度、召回率和 ROC-AUC 等数量的多类别泛化,并鼓励您在感兴趣的情况下查阅这些定义。在实践中,有一个更简单的可视化方法,即混淆矩阵,它效果很好。对于一个具有k个类别的多类别问题,混淆矩阵是一个k×k的矩阵。(i, j)-th 单元格表示被标记为类别i且真实标签为类别j的数据点的数量。图 5-2 展示了一个混淆矩阵。

confusion_matrix.png

图 5-2。一个 10 类分类器的混淆矩阵。

不要低估人眼从简单可视化中捕捉到系统性失败模式的能力!查看混淆矩阵可以快速理解许多更复杂的多类别指标可能忽略的内容。

回归指标

您在几章前学习了回归指标。简要回顾一下,皮尔逊R²和 RMSE(均方根误差)是很好的默认值。

我们之前只简要介绍了* R ²的数学定义,但现在将更深入地探讨它。让x i代表预测值,y i代表标签。让x ¯和y ¯分别代表预测值和标签的平均值。那么皮尔逊R*(注意没有平方)是

R = ∑((xi - x ¯)(yi - y ¯))/(√(∑(xi - x ¯)²)√(∑(yi - y ¯)²))

这个方程可以重写为

R = cov(x,y)/(σ(x)σ(y))

其中 cov 代表协方差,σ代表标准差。直观地说,皮尔逊R度量了预测值和标签从它们的平均值归一化的联合波动。如果预测值和标签不同,这些波动将发生在不同点,并且倾向于抵消,使R²变小。如果预测值和标签趋于一致,波动将一起发生,并使R²变大。我们注意到R²限制在 0 到 1 之间的范围。

RMSE 度量了预测值和真实值之间的误差的绝对量。它代表均方根误差,大致类似于真实数量和预测数量之间的误差的绝对值。从数学上讲,RMSE 定义如下(使用与之前相同的符号):

均方根误差(RMSE)= √(∑(xi - yi)² / N)

超参数优化算法

正如我们在本章前面提到的,超参数优化方法是用于在验证集上找到优化所选指标的超参数值的学习算法。一般来说,这个目标函数是不可微分的,因此任何优化方法必须是一个黑盒。在本节中,我们将向您展示一些简单的黑盒学习算法,用于选择超参数值。我们将使用来自第四章的 Tox21 数据集作为案例研究,以演示这些黑盒优化方法。Tox21 数据集足够小,使实验变得容易,但足够复杂,使超参数优化并不是微不足道的。

在启动之前,我们注意到这些黑盒算法都不是完美的。很快你会看到,在实践中,需要大量人为输入来优化超参数。

超参数优化不能自动化吗?

机器学习的一个长期梦想是自动选择模型的超参数。诸如“自动统计学家”等项目一直致力于消除超参数选择过程中的一些繁琐工作,并使模型构建更容易为非专家所掌握。然而,在实践中,通常为了增加便利性而付出了性能的巨大代价。

近年来,有大量的工作集中在改进模型调整的算法基础上。高斯过程、进化算法和强化学习都被用来学习模型的超参数和架构,几乎没有人为输入。最近的研究表明,借助大量的计算能力,这些算法可以超越专家在模型调整方面的表现!但是开销很大,需要数十到数百倍的计算能力。

目前,自动模型调整仍然不太实用。本节中涵盖的所有算法都需要大量手动调整。然而,随着硬件质量的提高,我们预计超参数学习将变得越来越自动化。在短期内,我们强烈建议所有从业者掌握超参数调整的复杂性。精通超参数调整是区分专家和新手的技能。

建立一个基准线

超参数调整的第一步是找到一个基准线。基准线是由一个强大的(通常非深度学习)算法可以实现的性能。一般来说,随机森林是设置基准线的绝佳选择。如图 5-3 所示,随机森林是一种集成方法,它在输入数据和输入特征的子集上训练许多决策树模型。这些个体树然后对结果进行投票。

random_forest_new2.png

图 5-3。随机森林的示意图。这里 v 是输入特征向量。

随机森林往往是相当强大的模型。它们对噪声具有容忍性,不担心其输入特征的规模。(虽然对于 Tox21 我们不必担心这一点,因为我们所有的特征都是二进制的,但一般来说,深度网络对其输入范围非常敏感。为了获得良好的性能,最好对输入范围进行归一化或缩放。我们将在后面的章节中回到这一点。)它们还倾向于具有强大的泛化能力,不需要太多的超参数调整。对于某些数据集,要想用深度网络超越随机森林的性能可能需要相当大的复杂性。

我们如何创建和训练一个随机森林?幸运的是,在 Python 中,scikit-learn 库提供了一个高质量的随机森林实现。有许多关于 scikit-learn 的教程和介绍,所以我们只会展示构建 Tox21 随机森林模型所需的训练和预测代码(示例 5-1)。

示例 5-1。在 Tox21 数据集上定义和训练一个随机森林
from sklearn.ensemble import RandomForestClassifier

# Generate tensorflow graph
sklearn_model = RandomForestClassifier(
    class_weight="balanced", n_estimators=50)
print("About to fit model on training set.")
sklearn_model.fit(train_X, train_y)

train_y_pred = sklearn_model.predict(train_X)
valid_y_pred = sklearn_model.predict(valid_X)
test_y_pred = sklearn_model.predict(test_X)

weighted_score = accuracy_score(train_y, train_y_pred, sample_weight=train_w)
print("Weighted train Classification Accuracy: %f" % weighted_score)
weighted_score = accuracy_score(valid_y, valid_y_pred, sample_weight=valid_w)
print("Weighted valid Classification Accuracy: %f" % weighted_score)
weighted_score = accuracy_score(test_y, test_y_pred, sample_weight=test_w)
print("Weighted test Classification Accuracy: %f" % weighted_score)

这里的train_Xtrain_y等是在上一章中定义的 Tox21 数据集。回想一下,所有这些量都是 NumPy 数组。n_estimators指的是我们森林中的决策树数量。设置 50 或 100 棵树通常会提供良好的性能。Scikit-learn 提供了一个简单的面向对象的 API,具有fit(X, y)predict(X)方法。该模型根据我们的加权准确性指标实现了以下准确性:

Weighted train Classification Accuracy: 0.989845
Weighted valid Classification Accuracy: 0.681413

回想一下,来自第四章的全连接网络取得了良好的性能:

Train Weighted Classification Accuracy: 0.742045
Valid Weighted Classification Accuracy: 0.648828

看起来我们的基线比我们的深度学习模型获得了更高的准确性!是时候卷起袖子开始工作了。

研究生下降

尝试好的超参数的最简单方法是手动尝试多种不同的超参数变体,看看哪种有效。这种策略可能会出奇地有效和有教育意义。深度学习从业者需要建立对深度网络结构的直觉。鉴于理论的非常薄弱,经验性工作是学习如何构建深度学习模型的最佳方法。我们强烈建议尝试许多不同的全连接模型变体。要有系统性;在电子表格中记录您的选择和结果,并系统地探索空间。尝试理解各种超参数的影响。哪些使网络训练进行得更快,哪些使其变慢?哪些设置范围完全破坏了学习?(这些很容易找到,不幸的是。)

有一些软件工程技巧可以使这种搜索更容易。创建一个函数,其参数是您希望探索的超参数,并让其打印出准确性。然后尝试新的超参数组合只需要一个函数调用。示例 5-2 展示了这个函数签名在 Tox21 案例研究中的全连接网络中会是什么样子。

示例 5-2。将超参数映射到不同的 Tox21 全连接网络的函数
def eval_tox21_hyperparams(n_hidden=50, n_layers=1, learning_rate=.001,
                           dropout_prob=0.5, n_epochs=45, batch_size=100,
                           weight_positives=True):

让我们逐个讨论这些超参数。n_hidden控制网络中每个隐藏层中的神经元数量。n_layers控制隐藏层的数量。learning_rate控制梯度下降中使用的学习率,dropout_prob是训练步骤中不丢弃神经元的概率。n_epochs控制通过总数据的次数,batch_size控制每个批次中的数据点数量。

weight_positives是这里唯一的新超参数。对于不平衡的数据集,通常有助于对两类示例进行加权,使它们具有相等的权重。对于 Tox21 数据集,DeepChem 为我们提供了要使用的权重。我们只需将每个示例的交叉熵项乘以权重以执行此加权(示例 5-3)。

示例 5-3。对 Tox21 加权正样本
entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=y_logit, labels=y_expand)
# Multiply by weights
if weight_positives:
  w_expand = tf.expand_dims(w, 1)
  entropy = w_expand * entropy

为什么选择超参数值的方法被称为研究生下降?直到最近,机器学习一直是一个主要的学术领域。设计新的机器学习算法的经过考验的方法是描述所需的方法给一个新的研究生,并要求他们解决细节。这个过程有点像一种仪式,通常需要学生痛苦地尝试许多设计替代方案。总的来说,这是一个非常有教育意义的经历,因为获得设计美学的唯一方法是建立起一个记忆工作和不工作的设置。

网格搜索

在尝试了一些超参数的手动设置之后,这个过程将开始变得非常乏味。有经验的程序员往往会诱惑简单地编写一个for循环,迭代所需的超参数选择。这个过程更多或少是网格搜索方法。对于每个超参数,选择一个可能是好的超参数的值列表。编写一个嵌套的for循环,尝试所有这些值的组合以找到它们的验证准确性,并跟踪最佳表现者。

然而,这个过程中有一个微妙之处。深度网络对用于初始化网络的随机种子的选择非常敏感。因此,值得重复每个超参数设置的选择多次,并对结果进行平均以减少方差。

如示例 5-4 所示,执行此操作的代码很简单。

示例 5-4。对 Tox21 完全连接网络超参数进行网格搜索
scores = {}
n_reps = 3
hidden_sizes = [50]
epochs = [10]
dropouts = [.5, 1.0]
num_layers = [1, 2]

for rep in range(n_reps):
  for n_epochs in epochs:
    for hidden_size in hidden_sizes:
      for dropout in dropouts:
        for n_layers in num_layers:
          score = eval_tox21_hyperparams(n_hidden=hidden_size, n_epochs=n_epochs,
                                         dropout_prob=dropout, n_layers=n_layers)
          if (hidden_size, n_epochs, dropout, n_layers) not in scores:
            scores[(hidden_size, n_epochs, dropout, n_layers)] = []
          scores[(hidden_size, n_epochs, dropout, n_layers)].append(score)
print("All Scores")
print(scores)

avg_scores = {}
for params, param_scores in scores.iteritems():
  avg_scores[params] = np.mean(np.array(param_scores))
print("Scores Averaged over %d repetitions" % n_reps)

随机超参数搜索

对于有经验的从业者来说,往往会很诱人地重复使用在以前的应用中有效的神奇超参数设置或搜索网格。这些设置可能很有价值,但也可能导致我们走入歧途。每个机器学习问题都略有不同,最佳设置可能位于我们以前未考虑的参数空间的某个区域。因此,尝试超参数的随机设置(其中随机值是从一个合理范围内选择的)通常是值得的。

尝试随机搜索还有一个更深层次的原因。在高维空间中,常规网格可能会错过很多信息,特别是如果网格点之间的间距不大的话。选择网格点的随机选择可以帮助我们避免陷入松散网格的陷阱。图 5-4 说明了这一事实。

random_grid.png

图 5-4。说明为什么随机超参数搜索可能优于网格搜索。

我们如何在软件中实现随机超参数搜索?一个巧妙的软件技巧是预先抽样所需的随机值并将其存储在列表中。然后,随机超参数搜索简单地变成了对这些随机抽样列表进行网格搜索。这里有一个例子。对于学习率,通常很有用的是尝试从.1 到.000001 等范围内的广泛范围。示例 5-5 使用 NumPy 来抽样一些随机学习率。

示例 5-5。对学习率进行随机抽样
n_rates = 5
learning_rates = 10**(-np.random.uniform(low=1, high=6, size=n_rates))

我们在这里使用了一个数学技巧。请注意,.1 = 10^(-1),.000001 = 10^(-6)。使用np.random.uniform在 1 和 6 之间对实值进行抽样很容易。我们可以将这些抽样值提升到一个幂以恢复我们的学习率。然后learning_rates保存了一个值列表,我们可以将其输入到前一节的网格搜索代码中。

读者的挑战

在本章中,我们只涵盖了超参数调整的基础知识,但所涵盖的工具非常强大。作为挑战,尝试调整完全连接的深度网络,以实现高于随机森林的验证性能。这可能需要一些工作,但这个经验是非常值得的。

回顾

在本章中,我们介绍了超参数优化的基础知识,即选择模型参数的值,这些值无法在训练数据上自动学习。特别是,我们介绍了随机和网格超参数搜索,并演示了在上一章中介绍的 Tox21 数据集上优化模型的代码的使用。

在第六章中,我们将回顾深度架构,并向您介绍卷积神经网络,这是现代深度架构的基本构建块之一。

第六章:卷积神经网络

卷积神经网络允许深度网络学习结构化空间数据(如图像、视频和文本)上的函数。从数学上讲,卷积网络提供了有效利用数据局部结构的工具。图像满足某些自然的统计特性。让我们假设将图像表示为像素的二维网格。在像素网格中彼此接近的图像部分很可能一起变化(例如,图像中对应桌子的所有像素可能都是棕色的)。卷积网络学会利用这种自然的协方差结构以有效地学习。

卷积网络是一个相对古老的发明。卷积网络的版本早在上世纪 80 年代就在文献中提出过。虽然这些旧卷积网络的设计通常相当合理,但它们需要超过当时可用硬件的资源。因此,卷积网络在研究文献中相对默默无闻。

这一趋势在 2012 年 ILSVRC 挑战赛中戏剧性地逆转,该挑战赛是关于图像中物体检测的,卷积网络 AlexNet 实现的错误率是其最近竞争对手的一半。AlexNet 能够利用 GPU 在大规模数据集上训练旧的卷积架构。这种旧架构与新硬件的结合使得 AlexNet 能够在图像物体检测领域显著超越现有技术。这一趋势仅在继续,卷积神经网络在处理图像方面取得了巨大的提升。几乎可以说,现代几乎所有的图像处理流程现在都由卷积神经网络驱动。

卷积网络设计也经历了复兴,将卷积网络推进到远远超过上世纪 80 年代基本模型的水平。首先,网络变得更加深层,强大的最新网络达到了数百层深度。另一个广泛的趋势是将卷积架构泛化到适用于新数据类型。例如,图卷积架构允许卷积网络应用于分子数据,如我们在前几章中遇到的 Tox21 数据集!卷积架构还在基因组学、文本处理甚至语言翻译中留下了痕迹。

在本章中,我们将介绍卷积网络的基本概念。这些将包括构成卷积架构的基本网络组件,以及指导这些组件如何连接的设计原则的介绍。我们还将提供一个深入的示例,演示如何使用 TensorFlow 训练卷积网络。本章的示例代码改编自 TensorFlow 文档中有关卷积神经网络的教程。如果您对我们所做的更改感兴趣,请访问 TensorFlow 网站上的原始教程。与往常一样,我们鼓励您在本书的相关GitHub 存储库中逐步完成本章的脚本。

卷积架构简介

大多数卷积架构由许多基本原语组成。这些原语包括卷积层和池化层等层。还有一组相关的词汇,包括局部感受野大小、步幅大小和滤波器数量。在本节中,我们将简要介绍卷积网络基本词汇和概念的基础。

局部感受野

局部感受野的概念源自神经科学,神经元的感受野是影响神经元放电的身体感知部分。神经元在处理大脑看到的感官输入时有一定的“视野”。这个视野传统上被称为局部感受野。这个“视野”可以对应于皮肤的一小块或者一个人的视野的一部分。图 6-1 展示了一个神经元的局部感受野。

neuron_receptive_field.png

图 6-1. 一个神经元的局部感受野的插图。

卷积架构借用了这个概念,计算概念上的“局部感受野”。图 6-2 提供了应用于图像数据的局部感受野概念的图示表示。每个局部感受野对应于图像中的一组像素,并由一个单独的“神经元”处理。这些“神经元”与全连接层中的神经元直接类似。与全连接层一样,对传入数据(源自局部感受图像补丁)应用非线性变换。

local_receiptive_input.png

图 6-2. 卷积网络中“神经元”的局部感受野(RF)。

这样的“卷积神经元”层可以组合成一个卷积层。这一层可以被看作是一个空间区域到另一个空间区域的转换。在图像的情况下,一个批次的图像通过卷积层被转换成另一个。图 6-3 展示了这样的转换。在接下来的部分,我们将向您展示卷积层是如何构建的更多细节。

conv_receptive.png

图 6-3. 一个卷积层执行图像转换。

值得强调的是,局部感受野不一定局限于图像数据。例如,在堆叠的卷积架构中,其中一个卷积层的输出馈送到下一个卷积层的输入,局部感受野将对应于处理过的特征数据的“补丁”。

卷积核

在上一节中,我们提到卷积层对其输入中的局部感受野应用非线性函数。这种局部应用的非线性是卷积架构的核心,但不是唯一的部分。卷积的第二部分是所谓的“卷积核”。卷积核只是一个权重矩阵,类似于与全连接层相关联的权重。图 6-4 以图解的方式展示了卷积核如何应用到输入上。

sliding_kernal.png

图 6-4. 一个卷积核被应用到输入上。卷积核的权重与局部感受野中对应的数字逐元素相乘,相乘的数字相加。请注意,这对应于一个没有非线性的卷积层。

卷积网络背后的关键思想是相同的(非线性)转换应用于图像中的每个局部感受野。在视觉上,将局部感受野想象成在图像上拖动的滑动窗口。在每个局部感受野的位置,非线性函数被应用以返回与该图像补丁对应的单个数字。正如图 6-4 所示,这种转换将一个数字网格转换为另一个数字网格。对于图像数据,通常以每个感受野大小的像素数来标记局部感受野的大小。例如,在卷积网络中经常看到 5×5 和 7×7 的局部感受野大小。

如果我们想要指定局部感受野不重叠怎么办?这样做的方法是改变卷积核的步幅大小。步幅大小控制感受野在输入上的移动方式。图 6-4 展示了一个一维卷积核,分别具有步幅大小 1 和 2。图 6-5 说明了改变步幅大小如何改变感受野在输入上的移动方式。

stride_size.png

图 6-5。步幅大小控制局部感受野在输入上的“滑动”。这在一维输入上最容易可视化。左侧的网络步幅为 1,而右侧的网络步幅为 2。请注意,每个局部感受野计算其输入的最大值。

现在,请注意我们定义的卷积核将一个数字网格转换为另一个数字网格。如果我们想要输出多个数字网格怎么办?这很容易;我们只需要添加更多的卷积核来处理图像。卷积核也称为滤波器,因此卷积层中的滤波器数量控制我们获得的转换网格数量。一组卷积核形成一个卷积层

多维输入上的卷积核

在本节中,我们主要将卷积核描述为将数字网格转换为其他数字网格。回想一下我们在前几章中使用的张量语言,卷积将矩阵转换为矩阵。

如果您的输入具有更多维度怎么办?例如,RGB 图像通常具有三个颜色通道,因此 RGB 图像在正确的情况下是一个秩为 3 的张量。处理 RGB 数据的最简单方法是规定每个局部感受野包括与该补丁中的像素相关联的所有颜色通道。然后,您可以说局部感受野的大小为 5×5×3,对于一个大小为 5×5 像素且具有三个颜色通道的局部感受野。

一般来说,您可以通过相应地扩展局部感受野的维度来将高维张量推广到更高维度的张量。这可能还需要具有多维步幅,特别是如果要分别处理不同维度。细节很容易解决,我们将探索多维卷积核作为您要进行的练习。

池化层

在前一节中,我们介绍了卷积核的概念。这些核将可学习的非线性变换应用于输入的局部补丁。这些变换是可学习的,并且根据通用逼近定理,能够学习局部补丁上任意复杂的输入变换。这种灵活性赋予了卷积核很大的能力。但同时,在深度卷积网络中具有许多可学习权重可能会减慢训练速度。

与使用可学习变换不同,可以使用固定的非线性变换来减少训练卷积网络的计算成本。一种流行的固定非线性是“最大池化”。这样的层选择并输出每个局部感受补丁中激活最大的输入。图 6-6 展示了这个过程。池化层有助于以结构化方式减少输入数据的维度。更具体地说,它们采用局部感受野,并用最大(或最小或平均)函数替换字段的每个部分的非线性激活函数。

maxpool.jpeg

图 6-6。最大池化层的示例。请注意,每个彩色区域(每个局部感受野)中的最大值报告在输出中。

随着硬件的改进,池化层变得不那么有用。虽然池化仍然作为一种降维技术很有用,但最近的研究倾向于避免使用池化层,因为它们固有的丢失性(无法从池化数据中推断出输入中的哪个像素产生了报告的激活)。尽管如此,池化出现在许多标准卷积架构中,因此值得理解。

构建卷积网络

一个简单的卷积架构将一系列卷积层和池化层应用于其输入,以学习输入图像数据上的复杂函数。在构建这些网络时有很多细节,但在其核心,架构设计只是一种复杂的乐高堆叠形式。图 6-7 展示了一个卷积架构可能是如何由组成块构建起来的。

cnnimage.png

图 6-7。一个简单的卷积架构的示例,由堆叠的卷积和池化层构成。

膨胀卷积

膨胀卷积或空洞卷积是一种新近流行的卷积层形式。这里的见解是为每个神经元在局部感受野中留下间隙(atrous 意味着a trous,即法语中的“带孔”)。这个基本概念在信号处理中是一个古老的概念,最近在卷积文献中找到了一些好的应用。

空洞卷积的核心优势是每个神经元的可见区域增加。让我们考虑一个卷积架构,其第一层是具有 3×3 局部感受野的普通卷积。然后,在架构中更深一层的第二个普通卷积层中的神经元具有 5×5 的感受野深度(第二层中局部感受野中的每个神经元本身在第一层中具有局部感受野)。然后,更深的两层的神经元具有 7×7 的感受视图。一般来说,卷积架构中第N层的神经元具有大小为(2N + 1) × (2N + 1)的感受视图。这种感受视图的线性增长对于较小的图像是可以接受的,但对于大型图像很快就会成为一个负担。

空洞卷积通过在其局部感受野中留下间隙实现了可见感受野的指数增长。一个“1-膨胀”卷积不留下间隙,而一个“2-膨胀”卷积在每个局部感受野元素之间留下一个间隙。堆叠膨胀层会导致局部感受野大小呈指数增长。图 6-8 说明了这种指数增长。

膨胀卷积对于大型图像非常有用。例如,医学图像在每个维度上可以延伸到数千个像素。创建具有全局理解的普通卷积网络可能需要不合理深的网络。使用膨胀卷积可以使网络更好地理解这些图像的全局结构。

dilated_convolution.png

图 6-8。一个膨胀(或空洞)卷积。为每个神经元在局部感受野中留下间隙。图(a)描述了一个 1-膨胀的 3×3 卷积。图(b)描述了将一个 2-膨胀的 3×3 卷积应用于(a)。图(c)描述了将一个 4-膨胀的 3×3 卷积应用于(b)。注意,(a)层的感受野宽度为 3,(b)层的感受野宽度为 7,(c)层的感受野宽度为 15。

卷积网络的应用

在前一节中,我们介绍了卷积网络的机制,并向您介绍了构成这些网络的许多组件。在本节中,我们描述了一些卷积架构可以实现的应用。

目标检测和定位

目标检测是检测照片中存在的对象(或实体)的任务。目标定位是识别图像中对象存在的位置,并在每个出现的位置周围绘制“边界框”的任务。图 6-9 展示了标准图像上检测和定位的样子。

detection_and_localization.jpg

图 6-9。在一些示例图像中检测和定位的对象,并用边界框标出。

为什么检测和定位很重要?一个非常有用的定位任务是从自动驾驶汽车拍摄的图像中检测行人。不用说,自动驾驶汽车能够识别所有附近的行人是非常重要的。目标检测的其他应用可能用于在上传到社交网络的照片中找到所有朋友的实例。另一个应用可能是从无人机中识别潜在的碰撞危险。

这些丰富的应用使得检测和定位成为大量研究活动的焦点。本书中多次提到的 ILSVRC 挑战专注于检测和定位在 ImagetNet 集合中找到的对象。

图像分割

图像分割是将图像中的每个像素标记为其所属对象的任务。分割与目标定位相关,但要困难得多,因为它需要准确理解图像中对象之间的边界。直到最近,图像分割通常是通过图形模型完成的,这是一种与深度网络不同的机器学习形式,但最近卷积分割已经崭露头角,并使图像分割算法取得了新的准确性和速度记录。图 6-10 显示了应用于自动驾驶汽车图像数据的图像分割的示例。

nvidia_digits.png

图 6-10。图像中的对象被“分割”为各种类别。图像分割预计将对自动驾驶汽车和机器人等应用非常有用,因为它将实现对场景的细粒度理解。

图卷积

到目前为止,我们向您展示的卷积算法期望其输入为矩形张量。这样的输入可以是图像、视频,甚至句子。是否可能将卷积推广到不规则输入?

卷积层背后的基本思想是局部感受野的概念。每个神经元计算其局部感受野中的输入,这些输入通常构成图像输入中相邻的像素。对于不规则输入,例如图 6-11 中的无向图,这种简单的局部感受野的概念是没有意义的;没有相邻的像素。如果我们可以为无向图定义一个更一般的局部感受野,那么我们应该能够定义接受无向图的卷积层。

graph_example.png

图 6-11。由边连接的节点组成的无向图的示例。

如图 6-11 所示,图由一组由边连接的节点组成。一个潜在的局部感受野的定义可能是将其定义为一个节点及其邻居的集合(如果两个节点通过边连接,则被认为是邻居)。使用这种局部感受野的定义,可以定义卷积和池化层的广义概念。这些层可以组装成图卷积架构。

这种图卷积架构可能在哪些地方证明有用?在化学中,分子可以被建模为原子形成节点,化学键形成边缘的无向图。因此,图卷积架构在化学机器学习中特别有用。例如,图 6-12 展示了图卷积架构如何应用于处理分子输入。

graphconv_graphic_v2.png

图 6-12。展示了一个图卷积架构处理分子输入的示意图。分子被建模为一个无向图,其中原子形成节点,化学键形成边缘。"图拓扑"是对应于分子的无向图。"原子特征"是向量,每个原子一个,总结了局部化学信息。改编自“一次性学习的低数据药物发现”。

使用变分自动编码器生成图像

到目前为止,我们描述的应用都是监督学习问题。有明确定义的输入和输出,任务仍然是(使用卷积网络)学习一个将输入映射到输出的复杂函数。有没有无监督学习问题可以用卷积网络解决?回想一下,无监督学习需要“理解”输入数据点的结构。对于图像建模,理解输入图像结构的一个好的衡量标准是能够“采样”来自输入分布的新图像。

什么是“采样”图像的意思?为了解释,假设我们有一组狗的图像数据集。采样一个新的狗图像需要生成一张不在训练数据中的新狗图像!这个想法是,我们希望得到一张狗的图片,这张图片可能已经被包含在训练数据中,但实际上并没有。我们如何用卷积网络解决这个任务?

也许我们可以训练一个模型,输入词标签如“狗”,并预测狗的图像。我们可能能够训练一个监督模型来解决这个预测问题,但问题在于我们的模型只能在输入标签“狗”时生成一张狗的图片。现在假设我们可以给每只狗附加一个随机标签,比如“dog3422”或“dog9879”。那么我们只需要给一只新的狗附加一个新的随机标签,比如“dog2221”,就可以得到一张新的狗的图片。

变分自动编码器形式化了这些直觉。变分自动编码器由两个卷积网络组成:编码器网络和解码器网络。编码器网络用于将图像转换为一个平坦的“嵌入”向量。解码器网络负责将嵌入向量转换为图像。为了确保解码器可以生成不同的图像,会添加噪音。图 6-13 展示了一个变分自动编码器。

variational_autoencoder.png

图 6-13。变分自动编码器的示意图。变分自动编码器由两个卷积网络组成,编码器和解码器。

在实际实现中涉及更多细节,但变分自动编码器能够对图像进行采样。然而,朴素的变分编码器似乎生成模糊的图像样本,正如图 6-14 所示。这种模糊可能是因为L²损失不会严厉惩罚图像的模糊(回想我们关于L²不惩罚小偏差的讨论)。为了生成清晰的图像样本,我们将需要其他架构。

variational-autoencoder-faces.jpg

图 6-14。从一个训练有素的人脸数据集上训练的变分自动编码器中采样的图像。请注意,采样的图像非常模糊。

对抗模型

L2 损失会严厉惩罚大的局部偏差,但不会严重惩罚许多小的局部偏差,导致模糊。我们如何设计一个替代的损失函数,更严厉地惩罚图像中的模糊?事实证明,编写一个能够解决问题的损失函数是相当具有挑战性的。虽然我们的眼睛可以很快发现模糊,但我们的分析工具并不那么快捕捉到这个问题。

如果我们能够“学习”一个损失函数会怎样?这个想法起初听起来有点荒谬;我们从哪里获取训练数据呢?但事实证明,有一个聪明的想法使这变得可行。

假设我们可以训练一个单独的网络来学习损失。让我们称这个网络为鉴别器。让我们称制作图像的网络为生成器。生成器可以与鉴别器对抗,直到生成器能够产生逼真的图像。这种架构通常被称为生成对抗网络,或 GAN。

由 GAN 生成的面部图像(图 6-15)比朴素变分自动编码器生成的图像要清晰得多(图 6-14)!GAN 已经取得了许多其他有希望的成果。例如,CycleGAN 似乎能够学习复杂的图像转换,例如将马转变为斑马,反之亦然。图 6-16 展示了一些 CycleGAN 图像转换。

GAN_faces.png

图 6-15。从一个在面部数据集上训练的生成对抗网络(GAN)中采样的图像。请注意,采样的图像比变分自动编码器生成的图像更清晰。

CycleGAN.jpg

图 6-16。CycleGAN 能够执行复杂的图像转换,例如将马的图像转换为斑马的图像(反之亦然)。

不幸的是,生成对抗网络在实践中仍然具有挑战性。使生成器和鉴别器学习合理的函数需要许多技巧。因此,虽然有许多令人兴奋的 GAN 演示,但 GAN 尚未发展到可以广泛部署在工业应用中的阶段。

在 TensorFlow 中训练卷积网络

在这一部分,我们考虑了一个用于训练简单卷积神经网络的代码示例。具体来说,我们的代码示例将演示如何使用 TensorFlow 在 MNIST 数据集上训练 LeNet-5 卷积架构。和往常一样,我们建议您通过运行与本书相关的GitHub 存储库中的完整代码示例来跟随。

MNIST 数据集

MNIST 数据集包含手写数字的图像。与 MNIST 相关的机器学习挑战包括创建一个在数字训练集上训练并推广到验证集的模型。图 6-17 展示了从 MNIST 数据集中绘制的一些图像。

minst_images.png

图 6-17。来自 MNIST 数据集的一些手写数字图像。学习挑战是从图像中预测数字。

对于计算机视觉的机器学习方法的发展,MNIST 是一个非常重要的数据集。该数据集足够具有挑战性,以至于明显的非学习方法往往表现不佳。与此同时,MNIST 数据集足够小,以至于尝试新架构不需要非常大量的计算资源。

然而,MNIST 数据集大多已经过时。最佳模型实现了接近百分之百的测试准确率。请注意,这并不意味着手写数字识别问题已经解决!相反,很可能是人类科学家已经过度拟合了 MNIST 数据集的架构,并利用其特点实现了非常高的预测准确性。因此,不再建议使用 MNIST 来设计新的深度架构。尽管如此,MNIST 仍然是一个非常好的用于教学目的的数据集。

加载 MNIST

MNIST 代码库位于Yann LeCun 的网站上。下载脚本从网站下载原始文件。请注意脚本如何缓存下载,因此重复调用download()不会浪费精力。

作为一个更一般的说明,将机器学习数据集存储在云中,并让用户代码在处理之前检索数据,然后输入到学习算法中是非常常见的。我们在第四章中通过 DeepChem 库访问的 Tox21 数据集遵循了相同的设计模式。一般来说,如果您想要托管一个大型数据集进行分析,将其托管在云端并根据需要下载到本地机器进行处理似乎是一个不错的做法。(然而,对于非常大的数据集,网络传输时间变得非常昂贵。)请参见示例 6-1。

示例 6-1。这个函数下载 MNIST 数据集
def download(filename):
  """Download the data from Yann's website, unless it's already here."""
  if not os.path.exists(WORK_DIRECTORY):
    os.makedirs(WORK_DIRECTORY)
  filepath = os.path.join(WORK_DIRECTORY, filename)
  if not os.path.exists(filepath):
    filepath, _ = urllib.request.urlretrieve(SOURCE_URL + filename, filepath)
    size = os.stat(filepath).st_size
    print('Successfully downloaded', filename, size, 'bytes.')
  return filepath

此下载检查WORK_DIRECTORY的存在。如果该目录存在,则假定 MNIST 数据集已经被下载。否则,脚本使用urllib Python 库执行下载并打印下载的字节数。

MNIST 数据集以字节编码的原始字符串形式存储像素值。为了方便处理这些数据,我们需要将其转换为 NumPy 数组。函数np.frombuffer提供了一个方便的方法,允许将原始字节缓冲区转换为数值数组(示例 6-2)。正如我们在本书的其他地方所指出的,深度网络可能会被占据广泛范围的输入数据破坏。为了稳定的梯度下降,通常需要将输入限制在一个有界范围内。原始的 MNIST 数据集包含从 0 到 255 的像素值。为了稳定性,这个范围需要被移动,使其均值为零,范围为单位(从-0.5 到+0.5)。

示例 6-2。从下载的数据集中提取图像到 NumPy 数组
def extract_data(filename, num_images):
  """Extract the images into a 4D tensor [image index, y, x, channels].

 Values are rescaled from [0, 255] down to [-0.5, 0.5].
 """
  print('Extracting', filename)
  with gzip.open(filename) as bytestream:
    bytestream.read(16)
    buf = bytestream.read(IMAGE_SIZE * IMAGE_SIZE * num_images * NUM_CHANNELS)
    data = numpy.frombuffer(buf, dtype=numpy.uint8).astype(numpy.float32)
    data = (data - (PIXEL_DEPTH / 2.0)) / PIXEL_DEPTH
    data = data.reshape(num_images, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS)
    return data

标签以简单的文件形式存储为字节字符串。有一个包含 8 个字节的标头,其余的数据包含标签(示例 6-3)。

示例 6-3。这个函数将从下载的数据集中提取标签到一个标签数组中
def extract_labels(filename, num_images):
  """Extract the labels into a vector of int64 label IDs."""
  print('Extracting', filename)
  with gzip.open(filename) as bytestream:
    bytestream.read(8)
    buf = bytestream.read(1 * num_images)
    labels = numpy.frombuffer(buf, dtype=numpy.uint8).astype(numpy.int64)
  return labels

在前面示例中定义的函数的基础上,现在可以下载并处理 MNIST 训练和测试数据集(示例 6-4)。

示例 6-4。使用前面示例中定义的函数,此代码片段下载并处理 MNIST 训练和测试数据集
# Get the data.
train_data_filename = download('train-images-idx3-ubyte.gz')
train_labels_filename = download('train-labels-idx1-ubyte.gz')
test_data_filename = download('t10k-images-idx3-ubyte.gz')
test_labels_filename = download('t10k-labels-idx1-ubyte.gz')

# Extract it into NumPy arrays.
train_data = extract_data(train_data_filename, 60000)
train_labels = extract_labels(train_labels_filename, 60000)
test_data = extract_data(test_data_filename, 10000)
test_labels = extract_labels(test_labels_filename, 10000)

MNIST 数据集并没有明确定义用于超参数调整的验证数据集。因此,我们手动将训练数据集的最后 5,000 个数据点指定为验证数据(示例 6-5)。

示例 6-5。提取训练数据的最后 5,000 个数据集用于超参数验证
VALIDATION_SIZE = 5000  # Size of the validation set.

# Generate a validation set.
validation_data = train_data[:VALIDATION_SIZE, ...]
validation_labels = train_labels[:VALIDATION_SIZE]
train_data = train_data[VALIDATION_SIZE:, ...]
train_labels = train_labels[VALIDATION_SIZE:]

选择正确的验证集

在示例 6-5 中,我们使用训练数据的最后一部分作为验证集来评估我们学习方法的进展。在这种情况下,这种方法相对无害。测试集中的数据分布在验证集中得到了很好的代表。

然而,在其他情况下,这种简单的验证集选择可能是灾难性的。在分子机器学习中(使用机器学习来预测分子的性质),测试分布几乎总是与训练分布截然不同。科学家最感兴趣的是前瞻性预测。也就是说,科学家希望预测从未针对该属性进行测试的分子的性质。在这种情况下,使用最后一部分训练数据进行验证,甚至使用训练数据的随机子样本,都会导致误导性地高准确率。分子机器学习模型在验证时具有 90%的准确率,而在测试时可能只有 60%是非常常见的。

为了纠正这个错误,有必要设计验证集选择方法,这些方法要尽力使验证集与训练集不同。对于分子机器学习,存在各种算法,大多数使用各种数学估计图的不相似性(将分子视为具有原子节点和化学键边的数学图)。

这个问题在许多其他机器学习领域也会出现。在医学机器学习或金融机器学习中,依靠历史数据进行预测可能是灾难性的。对于每个应用程序,重要的是要批判性地思考所选验证集上的性能是否实际上是真实性能的良好代理。

TensorFlow 卷积原语

我们首先介绍用于构建我们的卷积网络的 TensorFlow 原语(示例 6-6)。

示例 6-6。在 TensorFlow 中定义 2D 卷积
tf.nn.conv2d(
    input,
    filter,
    strides,
    padding,
    use_cudnn_on_gpu=None,
    data_format=None,
    name=None
)

函数tf.nn.conv2d是内置的 TensorFlow 函数,用于定义卷积层。这里,input被假定为形状为(batch, height, width, channels)的张量,其中batch是一个小批量中的图像数量。

请注意,先前定义的转换函数将 MNIST 数据读入此格式。参数filter是形状为(filter_height, filter_width, channels, out_channels)的张量,指定了在卷积核中学习的非线性变换的可学习权重。strides包含滤波器步幅,是长度为 4 的列表(每个输入维度一个)。

padding控制输入张量是否被填充(如图 6-18 中的额外零)以确保卷积层的输出与输入具有相同的形状。如果padding="SAME",则填充input以确保卷积层输出与原始输入图像张量具有相同形状的图像张量。如果padding="VALID",则不添加额外填充。

conv_padding.png

图 6-18。卷积层的填充确保输出图像具有与输入图像相同的形状。

示例 6-7 中的代码定义了 TensorFlow 中的最大池化。

示例 6-7。在 TensorFlow 中定义最大池化
tf.nn.max_pool(
    value,
    ksize,
    strides,
    padding,
    data_format='NHWC',
    name=None
)

tf.nn.max_pool函数执行最大池化。这里valuetf.nn.conv2dinput具有相同的形状,即(batch, height, width, channels)ksize是池化窗口的大小,是长度为 4 的列表。stridespadding的行为与tf.nn.conv2d相同。

卷积架构

本节中定义的架构将与 LeNet-5 非常相似,LeNet-5 是最初用于在 MNIST 数据集上训练卷积神经网络的原始架构。在 LeNet-5 架构被发明时,计算成本非常昂贵,需要多周的计算才能完成训练。如今的笔记本电脑幸运地足以训练 LeNet-5 模型。图 6-19 展示了 LeNet-5 架构的结构。

lenet5.png

图 6-19。LeNet-5 卷积架构的示意图。

更多计算会有什么不同?

LeNet-5 架构已有几十年历史,但实质上是解决数字识别问题的正确架构。然而,它的计算需求使得这种架构在几十年来相对默默无闻。因此,有趣的是,今天有哪些研究问题同样被解决,但仅仅受限于缺乏足够的计算能力?

一个很好的应用是视频处理。卷积模型在处理视频方面非常出色。然而,在大型视频数据集上存储和训练模型是不方便的,因此大多数学术论文不会报告视频数据集的结果。因此,要拼凑出一个良好的视频处理系统并不容易。

随着计算能力的增强,这种情况可能会发生变化,视频处理系统可能会变得更加普遍。然而,今天的硬件改进与过去几十年的硬件改进之间存在一个关键区别。与过去几年不同,摩尔定律的放缓明显。因此,硬件的改进需要更多的比自然晶体管缩小更多的东西,通常需要在架构设计上付出相当大的智慧。我们将在后面的章节中回到这个话题,并讨论深度网络的架构需求。

让我们定义训练 LeNet-5 网络所需的权重。我们首先定义一些用于定义权重张量的基本常量(示例 6-8)。

示例 6-8。为 LeNet-5 模型定义基本常量
NUM_CHANNELS = 1
IMAGE_SIZE = 28
NUM_LABELS = 10

我们定义的架构将使用两个卷积层交替使用两个池化层,最后是两个完全连接的层。请记住,池化不需要可学习的权重,因此我们只需要为卷积和完全连接的层创建权重。对于每个tf.nn.conv2d,我们需要创建一个与tf.nn.conv2dfilter参数对应的可学习权重张量。在这种特定的架构中,我们还将添加一个卷积偏置,每个输出通道一个(示例 6-9)。

示例 6-9。为卷积层定义可学习的权重
conv1_weights = tf.Variable(
    tf.truncated_normal([5, 5, NUM_CHANNELS, 32],  # 5x5 filter, depth 32.
                        stddev=0.1,
                        seed=SEED, dtype=tf.float32))
conv1_biases = tf.Variable(tf.zeros([32], dtype=tf.float32))
conv2_weights = tf.Variable(tf.truncated_normal(
    [5, 5, 32, 64], stddev=0.1,
    seed=SEED, dtype=tf.float32))
conv2_biases = tf.Variable(tf.constant(0.1, shape=[64], dtype=tf.float32))

请注意,卷积权重是 4 维张量,而偏置是 1 维张量。第一个完全连接的层将卷积层的输出转换为大小为 512 的向量。输入图像从大小IMAGE_SIZE=28开始。经过两个池化层(每个将输入减少 2 倍),我们最终得到大小为IMAGE_SIZE//4的图像。我们相应地创建完全连接权重的形状。

第二个完全连接的层用于提供 10 路分类输出,因此其权重形状为(512,10),偏置形状为(10),如示例 6-10 所示。

示例 6-10。为完全连接的层定义可学习的权重
fc1_weights = tf.Variable(  # fully connected, depth 512.
    tf.truncated_normal([IMAGE_SIZE // 4 * IMAGE_SIZE // 4 * 64, 512],
                        stddev=0.1,
                        seed=SEED,
                        dtype=tf.float32))
fc1_biases = tf.Variable(tf.constant(0.1, shape=[512], dtype=tf.float32))
fc2_weights = tf.Variable(tf.truncated_normal([512, NUM_LABELS],
                                              stddev=0.1,
                                              seed=SEED,
                                              dtype=tf.float32))
fc2_biases = tf.Variable(tf.constant(
    0.1, shape=[NUM_LABELS], dtype=tf.float32))

所有权重定义完成后,我们现在可以自由定义网络的架构。该架构有六层,模式为 conv-pool-conv-pool-full-full(示例 6-11)。

示例 6-11。定义 LeNet-5 架构。调用此示例中定义的函数将实例化架构。
def model(data, train=False):
  """The Model definition."""
  # 2D convolution, with 'SAME' padding (i.e. the output feature map has
  # the same size as the input). Note that {strides} is a 4D array whose
  # shape matches the data layout: [image index, y, x, depth].
  conv = tf.nn.conv2d(data,
                      conv1_weights,
                      strides=[1, 1, 1, 1],
                      padding='SAME')
  # Bias and rectified linear non-linearity.
  relu = tf.nn.relu(tf.nn.bias_add(conv, conv1_biases))
  # Max pooling. The kernel size spec {ksize} also follows the layout of
  # the data. Here we have a pooling window of 2, and a stride of 2.
  pool = tf.nn.max_pool(relu,
                        ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1],
                        padding='SAME')
  conv = tf.nn.conv2d(pool,
                      conv2_weights,
                      strides=[1, 1, 1, 1],
                      padding='SAME')
  relu = tf.nn.relu(tf.nn.bias_add(conv, conv2_biases))
  pool = tf.nn.max_pool(relu,
                        ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1],
                        padding='SAME')
  # Reshape the feature map cuboid into a 2D matrix to feed it to the
  # fully connected layers.
  pool_shape = pool.get_shape().as_list()
  reshape = tf.reshape(
      pool,
      [pool_shape[0], pool_shape[1] * pool_shape[2] * pool_shape[3]])
  # Fully connected layer. Note that the '+' operation automatically
  # broadcasts the biases.
  hidden = tf.nn.relu(tf.matmul(reshape, fc1_weights) + fc1_biases)
  # Add a 50% dropout during training only. Dropout also scales
  # activations such that no rescaling is needed at evaluation time.
  if train:
    hidden = tf.nn.dropout(hidden, 0.5, seed=SEED)
  return tf.matmul(hidden, fc2_weights) + fc2_biases

如前所述,网络的基本架构交替使用tf.nn.conv2dtf.nn.max_pool和非线性,以及最后一个完全连接的层。为了正则化,在最后一个完全连接的层之后应用一个 dropout 层,但只在训练期间。请注意,我们将输入作为参数data传递给函数model()

网络中仍需定义的唯一部分是占位符(示例 6-12)。我们需要定义两个占位符,用于输入训练图像和训练标签。在这个特定的网络中,我们还定义了一个用于评估的单独占位符,允许我们在评估时输入更大的批次。

示例 6-12。为架构定义占位符
BATCH_SIZE = 64
EVAL_BATCH_SIZE = 64

train_data_node = tf.placeholder(
    tf.float32,
    shape=(BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))
train_labels_node = tf.placeholder(tf.int64, shape=(BATCH_SIZE,))
eval_data = tf.placeholder(
    tf.float32,
    shape=(EVAL_BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))

有了这些定义,我们现在已经处理了数据,指定了输入和权重,并构建了模型。我们现在准备训练网络(示例 6-13)。

示例 6-13。训练 LeNet-5 架构
# Create a local session to run the training.
start_time = time.time()
with tf.Session() as sess:
  # Run all the initializers to prepare the trainable parameters.
  tf.global_variables_initializer().run()
  # Loop through training steps.
  for step in xrange(int(num_epochs * train_size) // BATCH_SIZE):
    # Compute the offset of the current minibatch in the data.
    # Note that we could use better randomization across epochs.
    offset = (step * BATCH_SIZE) % (train_size - BATCH_SIZE)
    batch_data = train_data[offset:(offset + BATCH_SIZE), ...]
    batch_labels = train_labels[offset:(offset + BATCH_SIZE)]
    # This dictionary maps the batch data (as a NumPy array) to the
    # node in the graph it should be fed to.
    feed_dict = {train_data_node: batch_data,
                 train_labels_node: batch_labels}
    # Run the optimizer to update weights.
    sess.run(optimizer, feed_dict=feed_dict)

这个拟合代码的结构看起来与本书迄今为止看到的其他拟合代码非常相似。在每一步中,我们构建一个 feed 字典,然后运行优化器的一步。请注意,我们仍然使用小批量训练。

评估经过训练的模型

我们现在有一个正在训练的模型。我们如何评估训练模型的准确性?一个简单的方法是定义一个错误度量。与前几章一样,我们将使用一个简单的分类度量来衡量准确性(示例 6-14)。

示例 6-14。评估经过训练的架构的错误
def error_rate(predictions, labels):
  """Return the error rate based on dense predictions and sparse labels."""
  return 100.0 - (
      100.0 *
      numpy.sum(numpy.argmax(predictions, 1) == labels) /
      predictions.shape[0])

我们可以使用这个函数来评估网络在训练过程中的错误。让我们引入一个额外的方便函数,以批处理的方式评估任何给定数据集上的预测(示例 6-15)。这种便利是必要的,因为我们的网络只能处理固定批量大小的输入。

示例 6-15。一次评估一批数据
def eval_in_batches(data, sess):
  """Get predictions for a dataset by running it in small batches."""
  size = data.shape[0]
  if size < EVAL_BATCH_SIZE:
    raise ValueError("batch size for evals larger than dataset: %d"
                     % size)
  predictions = numpy.ndarray(shape=(size, NUM_LABELS),
                              dtype=numpy.float32)
  for begin in xrange(0, size, EVAL_BATCH_SIZE):
    end = begin + EVAL_BATCH_SIZE
    if end <= size:
      predictions[begin:end, :] = sess.run(
          eval_prediction,
          feed_dict={eval_data: data[begin:end, ...]})
    else:
      batch_predictions = sess.run(
          eval_prediction,
          feed_dict={eval_data: data[-EVAL_BATCH_SIZE:, ...]})
      predictions[begin:, :] = batch_predictions[begin - size:, :]
  return predictions

现在我们可以在训练过程中的内部for循环中添加一些仪器(instrumentation),定期评估模型在验证集上的准确性。我们可以通过评分测试准确性来结束训练。示例 6-16 展示了添加了仪器的完整拟合代码。

示例 6-16。训练网络的完整代码,添加了仪器
# Create a local session to run the training.
start_time = time.time()
with tf.Session() as sess:
  # Run all the initializers to prepare the trainable parameters.
  tf.global_variables_initializer().run()
  # Loop through training steps.
  for step in xrange(int(num_epochs * train_size) // BATCH_SIZE):
    # Compute the offset of the current minibatch in the data.
    # Note that we could use better randomization across epochs.
    offset = (step * BATCH_SIZE) % (train_size - BATCH_SIZE)
    batch_data = train_data[offset:(offset + BATCH_SIZE), ...]
    batch_labels = train_labels[offset:(offset + BATCH_SIZE)]
    # This dictionary maps the batch data (as a NumPy array) to the
    # node in the graph it should be fed to.
    feed_dict = {train_data_node: batch_data,
                 train_labels_node: batch_labels}
    # Run the optimizer to update weights.
    sess.run(optimizer, feed_dict=feed_dict)
    # print some extra information once reach the evaluation frequency
    if step % EVAL_FREQUENCY == 0:
      # fetch some extra nodes' data
      l, lr, predictions = sess.run([loss, learning_rate,
                                     train_prediction],
                                    feed_dict=feed_dict)
      elapsed_time = time.time() - start_time
      start_time = time.time()
      print('Step %d (epoch %.2f), %.1f ms' %
            (step, float(step) * BATCH_SIZE / train_size,
             1000 * elapsed_time / EVAL_FREQUENCY))
      print('Minibatch loss: %.3f, learning rate: %.6f' % (l, lr))
      print('Minibatch error: %.1f%%'
            % error_rate(predictions, batch_labels))
      print('Validation error: %.1f%%' % error_rate(
          eval_in_batches(validation_data, sess), validation_labels))
      sys.stdout.flush()
  # Finally print the result!
  test_error = error_rate(eval_in_batches(test_data, sess),
                          test_labels)
  print('Test error: %.1f%%' % test_error)

读者的挑战

尝试自己训练网络。您应该能够达到<1%的测试错误!

回顾

在这一章中,我们向您展示了卷积网络设计的基本概念。这些概念包括构成卷积网络核心构建模块的卷积和池化层。然后我们讨论了卷积架构的应用,如目标检测、图像分割和图像生成。我们以一个深入的案例研究结束了这一章,向您展示了如何在 MNIST 手写数字数据集上训练卷积架构。

在第七章中,我们将介绍循环神经网络,另一个核心深度学习架构。与为图像处理而设计的卷积网络不同,循环架构非常适合处理顺序数据,如自然语言数据集。