Swift-和-TensorFlow-深度学习教程-全-

Swift 和 TensorFlow 深度学习教程(全)

原文:Deep Learning with Swift for TensorFlow

协议:CC BY-NC-SA 4.0

一、机器学习基础

毫无疑问,我们从事的是铸造神灵的工作。 1

——帕梅拉·麦科达克

如今,除了量子计算(Preskill,2018)和区块链(Nakamoto,2008),人工智能(AI)是计算机科学最迷人的领域之一。自 2000 年代中期以来,工业界的大肆宣传导致了对人工智能初创公司的大量投资。全球领先的科技公司,如苹果、谷歌、亚马逊、脸书和微软,仅举几例,正在迅速收购世界各地有才华的人工智能初创公司,以加速人工智能研究,并反过来改进自己的产品。

考虑像 Apple Watch 这样的便携式设备。它使用机器智能来分析你的实时运动感觉数据,以跟踪你的脚步、站立时间、游泳次数、睡眠时间等。它还可以根据手腕皮肤下暂时的血液颜色变化计算您的心率,提醒您心跳不规则,执行心电图(ECG),测量运动期间血液中的耗氧量(VO max)等等。另一方面,iPhone 和 iPad 等设备使用来自相机传感器的激光雷达信息来即时创建周围的深度图。这些信息然后与机器智能相结合,以提供计算摄影功能,如强度可调的散景效果,沉浸式增强现实(AR)功能,如 AR 对象上周围的反射和照明,人类进入场景时的对象遮挡,等等。像 Siri 这样的个人语音助理可以理解你的语音,让你完成各种任务,例如控制你的家庭配件,在 HomePod 上播放音乐,给别人打电话和发短信,等等。机器智能技术因快速图形处理单元(GPU)而成为可能。如今,便携式设备上的 GPU 足够快,可以处理用户的数据,而不必将其发送到云服务器。这种方法有助于保持用户数据的私密性,从而防止不良暴露和使用(Sharma 和 Bhalley,2016 年)。事实上,上面提到的所有功能都可以通过设备上的机器智能来实现。

你可能会惊讶,人工智能并不是一项新技术。它实际上可以追溯到 20 世纪 40 年代,人们根本不认为它有用和酷。它有许多起伏。人工智能技术的普及主要有三次。它在这些时代有不同的名字,现在我们普遍认为它是深度学习。20 世纪 40-60 年代间,艾被称为“控制论”;大约在 20 世纪 80-90 年代,它被称为“连接主义”;从 2006 年开始,我们将人工智能称为“深度学习”。

在过去的某个时候,也有一种误解,许多研究人员认为,如果宇宙中所有事物工作方式的所有规则都被编程到计算机中,那么它就会自动变得智能。但这一想法受到了人工智能现状的强烈挑战,因为我们现在知道有更简单的方法来让机器模仿类似人类的智能。

在人工智能研究的早期,数据很少。计算机器也很慢。这些是淹没人工智能系统流行的主要因素之一。但现在我们有了互联网,地球上很大一部分人相互交流,迅速产生海量数据,这些数据存储在各自公司的服务器上。(刘冰等人,2009 年)找到了一种以更快速度运行深度学习算法的方法。大数据集和高性能计算(HPC)的结合使研究人员快速推进了最先进的深度学习算法。这本书的重点是从简单的概念开始向你介绍这些先进的算法。

在这一章中,我们将介绍机器学习的基本概念,这些概念对其继任者深度学习领域仍然有效。第章 2 重点讲述清楚理解深度学习算法所需的数学。因为深度学习是一门经验学科,如果我们自己不能编程,那么只理解深度学习算法的数学方程是没有用的。此外,计算机是通过执行数值计算来测试数学定理的(图灵,1936)。第三章介绍了一种强大的、经过编译的、快速的深度学习编程语言,称为 Swift for TensorFlow,它扩展了苹果的 Swift 语言(已经能够进行差异化编程),以包括深度学习特有的 TensorFlow 库功能。TensorFlow 是一个深度学习专用的库,值得整个章节 4 专门介绍它。然后我们在第五章中深入研究神经网络的基础知识。最后我们会在第六章编程一些高级的计算机视觉算法。

但让我们首先区分人工智能、机器学习和深度学习这三个术语,因为它们有时会互换使用。人工智能,也称为机器智能,代表了一套可用于使机器智能化的算法。人工智能系统通常包含硬编码的规则,程序遵循这些规则来从数据中获取一些意义(Russell & Norvig,2002),例如,使用硬编码的英语语法规则在句子中找到名词,使用 if 和 else 条件防止机器人掉入陷阱,等等。如今这些系统被认为是弱智能的。另一个术语是机器学习(ML),与人工智能算法不同的是,它使用数据来从中获得洞察力(Bishop,2006),例如,使用 k-最近邻等非参数算法对图像进行分类,使用决策树方法对文本进行分类,等等。ML 使用数据来学习,并且也已知表现弱于深度学习。最后,目前最先进的 AI 是深度学习。深度学习(DL)也使用数据进行学习,但采用分层方式(LeCun 等人,2015 年),从大脑中获取灵感。DL 算法可以很容易地学习非常复杂的数据集的映射,而不会损害准确性,但它们的表现反而比机器学习算法更好。如果你画一个维恩图,如图 1-1 所示,你会看到深度学习是机器学习的一个子集,而人工智能领域是这两个领域的超集。

img/484421_1_En_1_Fig1_HTML.jpg

图 1-1

代表人工智能、机器学习和深度学习算法之间重叠(未精确缩放)的维恩图。每套给出了属于那个领域的算法的几个例子。

现在,我们可以从简单的机器学习概念开始深度学习之旅。

1.1 机器学习

机器学习算法通过从数据中学习自身来学习执行某些任务,同时提高其性能。机器学习的广泛接受的定义(Mitchell 等人,1997)如下:“如果计算机程序在任务 T 的性能(由 P 测量)随着经验 E 而提高,则称该计算机程序从关于某类任务 T 和性能测量 P 的经验 E 中学习。”其思想是编写一个计算机程序,该计算机程序可以通过一些性能测量来更新其状态,以通过体验可用数据以良好的性能执行期望的任务。让这个程序学习不需要人工干预。

基于这个定义,有三个基本概念可以帮助我们让机器学习,即经验、任务和性能测量。本节将讨论这些想法。在 1.4 节中,我们将看到这些思想是如何用数学方法表达的,这样就可以编写一个学习型计算机程序。在 1.4 节之后,你会意识到这个简单的定义形成了机器如何学习的基础,并且书中讨论的每个机器学习范例都可以用这个定义来隐含地表达。

在我们进一步进行之前,澄清机器学习算法由各种基本组件组成是很重要的。它的学习部分被称为模型,它只是一个数学函数。现在让我们继续理解这些基本思想。

1.1.1 经验

经验是模型为了学习执行任务而进行的多次观察。这些观察结果是来自可用数据集的样本。在学习过程中,总是需要一个模型来观察数据。

数据可以是各种形式,例如图像、视频、音频、文本、触觉等。每个样本,也被称为样本,从数据上可以用其特征来表示。例如,图像样本的特征是其像素,其中每个像素由红色、绿色和蓝色值组成。所有这些颜色的不同亮度值一起代表电磁辐射光谱的可见范围(我们的眼睛可以感知)中的单一颜色。

除了特征,每个样本有时还可能包含一个对应的标签向量,也称为目标向量,代表样本所属的类别。例如,鱼图像样本可能具有代表鱼的相应标签向量。标签通常用 one-hot 编码(也称为 - k 编码的 1- ,其中 k 是类的数量)来表示,这是一种表示,其中整个向量中只有单个索引的值为 1,其他所有索引都设置为 0。假设每个指数代表某一类,值为 1 的指数假设代表样本所属的类。例如,假设[1 0 0]向量代表一只狗,而[0 1 0]和[0 0 1]向量分别代表一条鱼和一只鸟。这意味着鸟类的所有图像样本都具有相应的标签向量[0 0 1],同样,狗和鱼的图像样本也将具有它们自己的标签。

我们之前列出的样本特征是原始特征,也就是说,这些特征不是由人类精心挑选的。有时,在机器学习中,特征选择对模型的性能起着重要的作用。例如,对于像人脸识别这样的任务,高分辨率图像比低分辨率图像处理起来要慢。因为深度学习可以直接在原始数据上工作,性能非常好,所以我们不会特别讨论特征选择。但是,当代码清单中需要以正确的格式获取数据时,我们将介绍一些预处理技术。我们建议感兴趣的读者参考(泽奥多里德斯和库特鲁姆巴斯,2009 年)教科书来了解特征选择。

在深度学习中,我们可能需要对数据进行预处理。预处理是应用于原始样本的一系列函数,将它们转换成所需的特定形式。这种期望的形式通常是基于模型的设计和手头的任务来决定的。例如,以 16 KHz 采样的原始音频波形每秒有 16,384 个样本被表示为向量。即使对于一个短的音频记录,比如说 5 秒,这个向量的维数也会变得非常大,也就是说,一个 81,920 元素长的向量!我们的模型需要更长的时间来处理。这就是预处理变得有用的地方。然后,我们可以使用快速傅立叶变换(Heideman 等人,1985)函数对每个原始音频波形样本进行预处理,以将其转换为频谱图表示。现在,该图像的处理速度比之前冗长的原始音频波形快得多。有不同的方法来预处理数据,选择取决于模型设计和手头的任务。我们将在本书中介绍不同类型数据的一些预处理步骤,并不详尽,只要有需要。

1.1.2 任务

任务是模型处理样本特征以返回样本的正确标签的动作。设计机器学习模型主要有两个任务,即回归和分类。还有更多有趣的任务,我们将在后面的章节中介绍和编程,它们只是这两个基本任务的扩展。

例如,对于一幅鱼的图像,模型应该返回[0 1 0]向量。因为这里图像被映射到它的标签,这个任务通常被称为图像分类。这是分类任务的一个简单示例。

回归任务的一个很好的例子是对象检测。我们可能想要在图像中检测一个物体的位置,比如球。这里,特征是图像像素,标签是图像中对象的坐标。这些坐标表示对象的边界框,即对象在给定图像中出现的位置。这里,我们的目标是训练一个模型,该模型将图像特征作为输入,并预测对象的正确结合框坐标。因为预测输出是实值的,所以对象检测被认为是回归任务。

绩效衡量

一旦我们设计了一个执行任务的模型,下一步就是让它学习并评估它在给定任务上的表现。为了评估,使用某种形式的性能测量(或度量)。性能度量可以采用各种形式,如准确性、F1 分数、精确度和召回率等,来描述模型执行任务的好坏。请注意,在训练和测试阶段,应该使用相同的性能指标来评估模型。

根据经验,只要有可能,就必须尝试选择一个单一数字的性能指标。在我们之前的图像分类示例中,可以很容易地使用准确度作为性能度量。精度定义为被模型正确分类的图像(或其他样本)总数的一部分。如下所示,也可以使用多数字性能指标,但这使得从一组训练模型中确定哪个模型表现最佳变得更加困难。

让我们考虑两个图像分类器 C 1 和 C 2 ,它们的任务是预测图像中是否包含汽车。如表 1-1 所示,如果分类器 C 1 的精度为 0.92,分类器 C 2 的精度为 0.99,那么很明显 C 2 的性能优于 C 1

表 1-1

分类器 C 1 和 C 2 在图像识别任务中的准确度。

|

分类者

|

准确

|
| --- | --- |
| C 1 | 92% |
| C 2 | 99% |

现在让我们考虑这两个分类器的精确度和召回率,这是一个两个数字的评估度量。精度召回被定义为分类器分别正确标记为汽车的测试或验证集中所有和汽车图像的的分数。对于我们的任意分类器,这些度量值如表 1-2 所示。

表 1-2

分类器 C 1 和 C 2 在图像识别任务中的精度和召回率。

|

分类者

|

精确

|

回忆

|
| --- | --- | --- |
| C 1 | 98% | 95% |
| C 2 | 95% | 90% |

现在似乎还不清楚哪种型号的性能更优越。相反,我们可以将精确度和召回率转化为一个单一的数字指标。有多种方法可以实现这一点,如均值或 F 1 得分。在这里,我们将找到它的 F 1 分数。 F

$$ {F}_1=\frac{2}{\frac{1}{\mathrm{Precision}}+\frac{1}{\mathrm{Recall}}} $$

(1.1)

表 1-3 通过将每个分类器的精度和召回值放入等式 1.1 来显示每个分类器的 F 1 分数。

从表 1-3 中,简单看一下 F 1 的分数,我们很容易得出分类器 C 2 比 C 1 表现更好的结论。在实践中,使用一个单一的评估指标对于确定训练模型的优越性非常有帮助,并且可以加速您的研究或部署过程。

表 1-3

分类器 C 1 和 C 2 在一个图像识别任务上的精度、召回率和 F 1 得分。

|

分类者

|

精确

|

回忆

|

F1 分数

|
| --- | --- | --- | --- |
| C 1 | 98% | 85% | 91% |
| C 2 | 95% | 90% | 92.4% |

已经讨论了机器学习的基本思想,我们现在将把我们的焦点转向不同的机器学习范例。

1.2 机器学习范例

机器学习通常根据数据集经验的种类分为四类,允许的模型如下:监督学习(SL)、非监督学习(UL)、半监督学习(SSL)和强化学习(RL)。我们简要讨论这些机器学习范例中的每一个。

监督学习

在训练期间,当一个模型利用带标签的数据样本来学习执行任务时,这种类型的机器学习被称为监督学习。它被称为“有监督的”,因为属于数据集的每个样本都有相应的标签。在监督学习中,在训练期间,机器学习模型的目标是从样本映射到它们相应的目标。在推断过程中,监督模型必须预测任何给定样本的正确标签,包括训练过程中未看到的样本。

我们之前已经讨论了图像分类任务的概念,这是 SL 的一个例子。例如,你可以通过在苹果的照片应用程序中输入照片中出现的对象的类别来搜索照片。另一个有趣的 SL 任务是自动语音识别(ASR ),其中一系列音频波形被模型转录成表示音频记录中所说单词的文本序列。例如,Siri、Google Assistant、Cortana 和其他便携式设备上的个人语音助手都使用语音识别来将你所说的话转换成文本。在撰写本文时,SL 是生产中最成功和最广泛使用的机器学习。

1.2.2 无监督学习

无监督学习是一种机器学习,模型只允许观察样本特征,不允许观察标签。UL 通常旨在学习模型隐藏特征中数据集的一些有用表示。这个学习到的表示可以在以后使用这个模型执行任何期望的任务。在撰写本文时,深度学习社区对 UL 非常感兴趣。

例如,UL 可用于降低高维数据样本的维度,正如我们之前所讨论的,这有助于通过模型更快地处理数据样本。另一个例子是密度估计,目标是估计数据集的概率密度。在密度估计之后,模型可以产生与属于它被训练的数据集的样本相似的样本。正如我们将在后面看到的,UL 算法可以用来完成各种有趣的任务。

值得注意的是,UL 被称为“无监督的”,因为数据集中不存在标注,但我们仍然需要将标注与预测一起输入损失函数(这是第 1.3 节中讨论的最大似然估计的基本要求)以训练模型。在这种情况下,我们自己为样本假定一些适当的标签。例如,在生成式对抗性网络中(Goodfellow 等人,2014 年),从生成器生成的数据点的标签被赋予假标签(或 0),而从数据集采样的数据点被赋予真实标签(或 1)。另一个例子是自编码器(Vincent 等人,2008),其中标签是相应的样本图像本身。

1.2.3 半监督学习

半监督学习关注的是在训练期间,从一小组标记样本中训练一个模型,并预测(使用当前半训练模型)未标记样本为伪目标(也称为软目标)。从训练过程中经历的数据类型的角度来看,SSL 介于监督和非监督学习之间,因为它同时观察标记和未标记的样本。当我们有一个大的数据集,只包含少量的标记样本(因为它们很费力,因此获取起来很昂贵)和大量的未标记样本时,SSL 特别有用。有趣的是,用于训练该模型的 SSL 技术可以大大提高其性能。

我们在书中没有涉及半监督学习。对于半监督学习的严格理解,我们请感兴趣的读者参考(Chapelle 等人,2006)教科书。

强化学习

强化学习是基于代理人与环境交互所获得的奖励,通过多次试验(称为情节)使其累积奖励(加权平均奖励序列,也称为回报)最大化,以实现其目标。RL 是一种机器学习的范式,涉及一系列决策过程。

代理通过采取一些行动来作用于世界,比如说它向前移动。在此之后,环境的状态得到更新,并且环境将奖励返回(或给予)给代理。根据行为科学的观点,奖励或者是积极的或者是消极的,并且可以分别被认为是世界对主体的好的或者坏的反应。我们更感兴趣的是回报,而不是当前的阶梯奖励,因为代理的目标是在每集的过程中最大化回报。在这里,一个情节是一个主体和它的环境之间从开始到结束的一系列相互作用。一集的例子如下:代理人的游戏,其中当满足某个条件时游戏结束,代理人试图在恶劣的环境条件下生存,直到由于某种事故而死亡。参见图 1-2 了解代理与其环境之间相互作用的示意图。

img/484421_1_En_1_Fig2_HTML.png

图 1-2

强化学习代理和环境之间的相互作用。

代理感知环境 S t -1 的先前状态,并对其状态 S t 改变的环境采取动作 A t ,并将其返回给代理。代理还从描述代理的当前状态有多好的环境接收标量奖励 R t 。虽然当代理动作时环境的状态改变,但它也可能自己改变。在多智能体强化学习中,也可能有其他智能体使自己的收益最大化。

强化学习是一个非常有趣的机器学习领域,在撰写本文时正在积极研究。它也被认为更接近于人类(或其他哺乳动物)通过进行行为修正来学习的方式,即在奖励的基础上强化一个动作。最近的一项工作(Minh et al .,2015)表明,一种称为深度强化学习的深度学习和强化学习的组合甚至可以超越人类级别的游戏能力。

不幸的是,我们在书中没有讨论强化学习。感兴趣的读者可以参考(萨顿和巴尔托,2018)教科书,了解该领域的基本知识。对于深度强化学习的进展,我们建议的作品(Mnih 等人,2015;舒尔曼等人,2017)。

现在让我们看看最大似然估计的基本思想,它有助于构建机器学习算法。

1.3 最大似然估计

这里描述的设置在整个机器学习文献中都是假设的。在此设置的约束下,模型的参数被估计。解决参数估计问题有两种基本方法,即最大似然估计贝叶斯推断。我们将关注最大似然估计,因为这是我们在整本书中用来训练神经网络的。建议对贝叶斯推理感兴趣的读者阅读(Bishop,1995)教材的第二章。关于最大似然函数起源的详细注释,请参考(Akaike,1973)。

为了解决一个机器学习问题,我们需要一个包含一组 N 个数据点(或样本)的数据集ⅅ,也就是ⅅ $$ \chi =\left{{\left.\left({\mathrm{x}}{(i)},{t}{(i)}\right)\right}}_{i=1}^N\right. $$。假设每个数据点都是相同的,并且独立于联合数据生成分布Pd(xt )进行采样,这是一个概率密度函数(PDF),意味着数据样本 x 和相应的目标 t 是连续的随机变量。注意,目标随机变量 t 的分布实际上取决于模型执行的任务,即目标的分布对于回归和分类任务分别是连续的和离散的。我们也将Pd(xt )称为数据概率密度函数(或数据 PDF)。我们只能访问从数据 PDF 中采样的数据集ⅅ,而不能访问分布本身。因此,我们不能访问比我们可用的更多的数据点。

因为数据集ⅅ是从数据 PDF 中采样的,所以它以统计方式描述了数据 PDF 本身。我们在参数化机器学习中的目标是通过估计我们自己选择的参数化概率密度函数Pm(xy | θ )(或者简单地说Pm(xy )的参数来近似数据 PDF 的映射这里, θ 代表参数,随机变量 xy 是数据样本和相应的预测值给定 x 作为输入。我们实际上将通过使用最大似然函数更新参数值来最小化预测变量 y 和目标变量 t 之间的距离,也称为损失或误差。

模型和数据 pdf 是完全不同的分布,因此它们的样本在统计上也是不同的。但是我们希望来自模型 PDF 的预测 y 类似于来自给定数据样本 x 的数据 PDF 的相应目标 t 。如前所述,我们无法访问数据 PDF 的参数,因此我们不能简单地将其参数值复制到模型函数的参数值中。但是我们确实有数据集 𝒳 供我们使用,我们可以利用它来近似数据 PDF,因为这是它的统计描述。

现在我们将描述最大似然估计来用我们的参数化模型 PDF 近似数据 PDF。因为 𝒳 中的数据点是独立且同分布的,它们的联合概率由下式给出

$$ {P}_m\left(\chi;\left|\theta \right.\right)=\prod \limits_{i=1}N{P}_m\left({\mathbf{x}}{(i)},{\mathbf{t}}^{(i)}\left|\theta \right.\right)=\mathrm{\mathcal{L}}\left(\theta;\left|\chi \right.\right) $$

(1.2)

这里,pm(ⅅ|θ)是条件模型 PDF,读作“给定参数的数据集的联合概率”函数ℒ( θ |ⅅ)是给定固定和有限数据集ⅅ.的参数 θ似然函数您也可以将此函数解释为“可能是对数据集ⅅ进行采样的数据 PDF 的良好近似”为了减少混乱,我们将隐含地假设模型和对数似然函数中的参数条件。我们将使用贝叶斯定理将联合数据概率分布转换为我们关心的条件分布。

$$ \mathrm{\mathcal{L}}=\prod \limits_{i=1}N{P}_m\left({\mathbf{x}}{(i)},{\mathbf{t}}^{(i)}\right)=\prod \limits_{i=1}N{P}_m\left({\mathbf{t}}{(i)}\left|{\mathbf{x}}{(i)}\right.;\right){P}_m\left({\mathbf{x}}{(i)}\right) $$

(1.3)

通过最大化似然函数来近似数据 PDF,这需要更新模型 PDF 的参数值。更具体地说,如 1.4 节中简要描述的和 5.1 节中详细描述的,参数是用迭代法更新的。在数值上,先取似然的负对数,然后最小化,这样更方便。这相当于最大化似然函数。

$$ L=-\ln;\mathrm{\mathcal{L}}=-\sum \limits_{i=1}N\ln;{P}_m\left({\mathbf{t}}{(i)};\left|{\mathbf{x}}^{(i)}\right.\right)-\sum \limits_{i=1}N\ln;{P}_m\left({\mathbf{x}}{(i)}\right) $$

(1.4)

对数函数在这里起着非常重要的作用。它将乘积转化为总和,这有助于稳定数值计算。这是因为接近零的值的乘积是小得多的值,由于计算设备的有限精度表示能力,这些值可能被舍入。这也是机器学习频繁使用对数函数的原因。

负对数似然函数可被视为由 L 表示的损失误差函数。).我们的目标是通过更新其参数值来最小化损失函数,使得我们的参数化模型 PDF 使用来自数据 PDF 的可用固定和有限数据样本来近似数据 PDF。请注意,该等式中的第二项对模型 PDF 的参数估计没有贡献,因为它不依赖于模型的参数,并且只是一个负的附加项,可以忽略该附加项以最大化似然函数。损失函数现在可以简单地用下面的等式来描述:

$$ L=-\sum \limits_{i=1}N\ln;{P}_m\left({\mathbf{t}}{(i)};\left|{\mathbf{x}}^{(i)}\right.\right) $$

(1.5)

最小化损失函数相当于最小化负对数似然函数,负对数似然函数可以进一步被认为是最大化对数似然函数,因此术语最大似然估计。这里,我们的模型 PDF 表示给定数据样本的目标的条件分布Pm(t|x)。我们将在后面看到,神经网络是模拟这种条件分布的框架。并且也基于目标随机变量的分布,使用该方程产生不同的损失函数。

作为旁注,假设来说,如果似然函数完美地近似数据 PDF,那么可用的数据集 𝒳 和其他相同的数据集可以从中采样。但是在实践中,不可能完美地近似数据 PDF,但是我们可以近似地近似它。这就是我们在机器学习中所做的一切。

现在让我们看看机器学习算法的元素,并使(Mitchell et al .,1997)给出的机器学习的模糊定义在数学上具体化。

1.4 机器学习算法的要素

在本节中,我们描述了适用于 1.2 节中简要讨论的所有机器学习范例的机器学习算法的基本元素。机器学习算法有四个关键组成部分,即数据、模型、损失函数和优化。或者,也可以使用正则化技术来提高模型的泛化能力,并平衡偏差和方差的权衡(见 1.5 节)。

请记住,在机器学习中,我们的主要目标是训练一个应该在看不见的数据上表现良好的模型,因为我们的训练数据集将不会包含用户未来生成的数据。

1.4.1 数据

数据集中存在的数据充当机器学习模型的经验。当模型首次用随机参数值初始化时,它不知道如何很好地执行某项任务。这种知识是通过迭代地将模型暴露给数据而获得的(通常是小样本计数,也称为小批量)。随着模型经历更多的样本,它逐渐学会更准确地执行任务。

一个数据集仅仅是一个结构化的,通常是表格(或矩阵)形式的数据点排列。表 1-4 显示了一个数据集的任意例子。该数据集包含人的一些特征(或特性),其中每一行包含单个人的特性。每行包含某个人的身高(厘米)、年龄(岁)和体重(千克)值。注意,特征及其对应的目标可以是任意维的张量值(这里是 0 维,即标量变量)。

表 1-4

分别用 x 1 ,x 2 和 t 标量表示身高、年龄和体重的人的数据集。

|

身高( x 1 )

|

年龄( x 2 )

|

重量( t )

|
| --- | --- | --- |
| One hundred and fifty-one point seven | Twenty-five | Forty-seven point eight |
| One hundred and thirty-nine point seven | Twenty | Thirty-six point four |
| One hundred and thirty-six point five | Eighteen | Thirty-one point eight |
| One hundred and fifty-six point eight | Twenty-eight | Fifty-three |

给定数据集,我们必须决定任务的特征和目标。换句话说,为一项任务选择正确的特征完全取决于我们的决定。例如,我们可以使用这个数据集来执行一个回归任务,根据一个人的身高和年龄来预测体重。在这个设置中,我们的特征向量 x 包含身高 x 1 和年龄 x 2 ,即x=【x1x2,对于每个人(称为样本),而目标 t 是 a 的权重例如,x=【136.5 18】和 t = 31.8 是给定数据集中第三人的特征向量和目标标量。

在机器学习文献中,模型的数据点输入也被称为示例样本,其特征也被称为属性。例如,汽车的特征可以是颜色、轮胎尺寸、最高速度等等。另一个例子可以是药物,其特征可以包括化学式、每种化学元素的比例等等。目标,也称为标签硬目标(当区别于软目标时),是对应于给定样本的期望输出。例如,给定图像的特征(像素值),目标可以是椅子、桌子等等,用向量表示。还要注意,对于数据集中的给定样本,所有目标始终保持不变。在极少数情况下,我们在半监督学习中为未标记的样本生成软目标,而在监督学习中目标总是被假定为不可变的。

1.4.1.1 设计矩阵

表示数据集最流行的方式是设计矩阵,如表 1-4 所述。设计矩阵是样本和目标的集合,每行包含一个样本(每列包含一个描述样本的特征)和相应的目标。目标值用于监督学习任务。只有样本包含特征而不是目标。目标是我们期望我们的模型预测给定样本特征的期望输出。

1.4.1.2 独立和相同的样本

一个好的数据集应该在统计上代表样本在数据生成概率密度函数中的总体分布。请记住,我们只能访问数据集,而不能访问 PDF。由于数据集在统计上代表一种概率分布,因此样本的分布方式总有一些模式。机器学习算法旨在发现数据集中的这种模式,以执行所需的任务。我们只能通过盯着一组样本,在低维(最多三维)上很容易地直观判断模式,但机器学习算法可以理解更高维(甚至数十亿)上的模式。例如,我们可以说“如果一个人的身高在[110,160]范围内,那么他的体重将在[30,80]范围内”,等等。但如果我们添加锻炼时间、营养摄入等特征,那么样本就会有更复杂的详细模式,这对我们来说可能很难集体处理,但机器学习算法可以为我们分析它,甚至更准确。

为了让任何机器学习算法在任务中表现良好,我们需要数据集上的两个约束来发现样本之间的模式。首先,每个样本的值应该独立于另一个样本,也就是说,样本不应该相关,尽管每个样本中的单个特征应该相关。第二,样本应该是相同的,也就是说,它们相应的特征应该是相似的,并且在分布上表现出一些模式。

也就是说,符合这种约束的自然照片的假想数据集可能包含河流、车辆、天空、水下生物、植物、人类、动物等的图像,其中每个样本都是一张照片,其特征是红、绿、蓝颜色组合的像素值。这满足了相同的要求,因为,例如,风景图像将总是包含天空和地面。此外,该自然数据集也符合独立约束,因为没有图像影响其他图像的特征(像素)值。这种数据集的一个很好的例子是 ImageNet (Russakovsky 等人,2015 年)。

1.4.1.3 数据集分割

现在我们知道了什么是数据集以及它是如何表示的,下一步是使用它来学习它在模型中的层次表示。但是在模型训练之前,数据集必须被分成多个子集。在训练机器学习模型之前,这是一个非常重要的步骤,原因将在下面讨论。

实际上,该模型通常有大量参数,很容易使整个数据集过拟合。通过过拟合,我们的意思是该模型在数据集上表现很好,但在现实世界中肯定会遇到的看不见的例子上表现很差。另一个术语是欠拟合,这意味着模型在它被训练的数据集或看不见的数据集上都表现不佳。欠拟合和过拟合之间的平衡是一个困扰整个机器学习领域的问题,也被称为偏差和方差权衡(参见 1.5 节)。

我们将整个数据集主要分成三个子集,即训练集、测试集和验证集。如果整个数据集包含具有相同 id 的示例,那么每个子集也将包含具有相同 id 的示例。这些子集在建立良好的机器学习模型中发挥着至关重要的作用,解释如下。

训练集包含用于调整模型可学习参数的示例和目标。该模型被训练以迭代地最小化给定输入特征样本的预测输出和期望目标值之间的误差。作为模型在看不见的例子上表现良好的先决条件,它最初应该在训练过程中被允许经历的训练集上表现良好。

测试集包含模型在训练过程中不允许经历的例子。这是唯一用于测试模型性能的。在现实世界中,测试集被精心设计为包含来自数据集的示例,这些示例通常很难很好地执行。这样做是为了选择更好的机器学习模型。测试集还可能包含机器学习模型在现实中可能经历的示例。我们通常更关心我们可能从用户那里期待的真实世界的数据,例如,智能手机拍摄的照片,而不是卡通人物的图像。所以我们的测试集应该包含智能手机点击的照片。我们想要的最终结果是,我们的模型能够在看不见的例子上产生更接近真实目标的良好预测。如果一个给定的模型在看不见的例子上表现很好,就说它有一个好的泛化特性——否则就是坏的。

验证集,也称为开发集,用于为机器学习算法选择一组可能的超参数配置、模型架构等。与测试集不同,验证集在训练过程中用于评估模型的性能。但是重用同一个验证集会导致机器学习算法过拟合。为了克服这个问题,研究人员有时会使用多个交叉验证集。然后在多个验证集上逐一评估机器学习算法,以评估其在未知示例上的准确性。

根据经验,数据集应该分为 70%的训练集和 30%的测试和验证集。但是如果数据集有大量的样本,那么您可能不需要遵循这个标准。方法应该是分割数据集,使其在准确性方面代表对错误分类示例的良好估计。有关更多细节,我们请读者参考(Ng,2018)中关于设计机器学习工作流的指南,主要用于工业应用的监督学习任务,而在本书中,我们的目标是向您介绍高级深度学习算法并对它们进行编程。

1.4.2 型号

我们在上一节中讨论了数据集。目标是在机器学习算法中利用数据集,该算法学习对未知样本进行预测。为了实现这一点,我们需要一个机器学习模型。

在机器学习中,模型是一个具有可学习系数的数学函数。在本文中,函数的系数被称为参数。在训练之前,用小的随机参数值初始化机器学习模型。这些参数在优化阶段,也称为训练阶段期间缓慢变化。目的是有一个机器学习模型,在看不见的例子上表现良好。在推理阶段,机器学习模型被用于现实世界的应用中,在现实世界中,它会遇到在训练阶段没有看到的例子。

有两种广泛用于近似数据概率密度函数的模型,即参数模型和非参数模型。我们将在下文中简要讨论这些模型。

1.4.2.1 非参数模型

非参数模型使用整个数据集来预测给定测试样本特征的标签。他们使用一个核函数来衡量样本之间的相似性。对于所有训练示例和一个测试示例,该函数被迭代调用。核函数的选择在不同的核方法之间是不同的。例如,径向基核方法使用径向基函数;k-最近邻回归和分类方法使用诸如曼哈顿、欧几里德距离等函数。核函数也可以很容易地扩展,以产生一个神经网络。

对非参数模型的深入解释超出了本书的范围。我们建议您参考该文献中的(Bishop,2006)和(Murphy,2012)教科书。

1.4.2.2 参数模型

参数模型是包含可调系数(也称为参数)的函数。参数模型通过更新其参数值来执行任务。与总是使用整个数据集进行预测的非参数模型不同,参数模型只学习一次数据集的表示,并使用其参数中存在的知识进行预测。很难解释参数模型中存在的知识,这是一个活跃的研究领域。请参考(卡特等,2019;Olah et al .,2017,2018)对神经网络的特征进行深度可视化。非参数模型融合了训练和测试概念。参数模型有时训练很慢,但推理很快,也就是说,对于设备上或云上服务的用户来说,它们是实时部署的良好候选。

本书的主要焦点是在第五章中介绍的参数模型。在 1.1 节中,我们没有讨论模型如何学习。在下文中,我们将解释学习(或训练)过程。

损失函数

损失函数计算预测值和目标值之间的距离。它通过找出预测值和目标值之间的距离来衡量模型在预测给定输入的正确输出方面有多好,其中距离的概念由基于预测分布的各种损失函数(见第 5.5 节)定义。注意,损失函数在其他教材中也被称为误差函数代价函数目标函数。在本书中,我们更喜欢损失函数这个术语。

在训练期间,损失函数将模型的可学习参数导向这些值,使得模型预测相应输入的期望输出。请注意,损失函数不能用作评估模型的性能指标。

对于回归任务,损失函数的最常见选择是由以下等式定义的误差平方和:

$$ L\left(\chi; \theta \right)=\frac{1}{2};\sum \limits_{i=1}N{\left({y}{(i)}-{t}^{(i)}\right)}², $$

(1.6)

其中l(𝒳θ 为损失函数, y (i) 为输入样本的预测值x【I】t(I)为其对应的目标值, θ 为可学习参数。数据集中总共有 N 个样本。该损失函数中的平方确保总损失保持非负。损失函数中的分数项有其自身的重要性,因为它将损失函数的导数简化如下:

$$ {\nabla}_{\theta }L=\frac{\mathrm{d}L}{\mathrm{d}{y}{(i)}}={y}{(i)}-{t}^{(i)} $$

(1.7)

注意,一旦我们的模型被训练,我们将可学习参数表示为 θ* ,损失函数表示为l(𝒳θ* )或简单的 L ( θ* )保持抽象,对于数据集的哪个子集使用损失函数。

现在我们有了经验数据集和学习任务的模型,机器学习算法的最后一个组件是优化器(和可选的正则化器),它用于使模型从数据中学习,这将在接下来讨论。

优化器

优化,也称为训练,是使用损失函数更新模型的可学习参数,以最小化目标和预测之间的误差的过程。在训练期间,我们优化我们的机器学习模型,这是一个两步过程,即计算关于模型的每个可学习参数的误差梯度,并更新参数值。

在第一步中,我们计算损失函数相对于机器学习模型的每个可学习参数的梯度。为了完成这项任务,我们使用了一种称为误差反向传播 (Rumelhart 等人,1986 年)(也称为反向传播,或简称为反向传播)的高效算法。该算法只是微积分链式法则(见 2.3 节)计算梯度的连续应用。计算梯度的一个更通用的算法是自动微分(见 3.3 节),误差反向传播是它的一个特例。

在第二步中,我们使用在优化的第一步中获得的梯度信息来更新模型的参数。由于损失函数的梯度给出了其输出增加最多的方向,我们在梯度的负方向上以小步长迭代更新参数,因为我们的目标是最小化损失函数。用于优化的该参数更新步骤的学习算法被称为梯度下降,如以下等式所述:

$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta;{\nabla}_{\theta \left(\tau \right)};L $$

(1.8)

这里, τ 表示优化过程的时间步长, θ ( τ ) 表示时间步长 τ 的参数值,同样 θ (τ+1) 表示时间步长τ+1 的参数值。术语∇θ(τ)l表示在时间步长 τ 上用算法微分计算的关于权重参数 θ (τ) 的梯度。由于梯度值通常很大,我们必须采用超参数的小步长,用术语 η 表示,称为步长(或学习速率),以最小化损失函数,其中 η ∈ (0,1)。为了采取小的步骤,学习速率被乘以关于模型的每个参数的损失函数的负梯度。优化是一个连续的迭代过程,在每一步迭代中,参数 θ (τ) 被加上一个小步长更新—ηθ(τ)l以降低预测与目标之间的误差。

简而言之,单个训练步骤包括交替地向前传播输入特征信号,然后向后传播由损失函数计算和发出的误差信号。反向传播计算梯度,然后用于更新模型的参数。这种普通的梯度下降技术可能并不总是给出最好的结果。但是,正如在 5.6 节中详细描述的那样,对它还有各种各样的修改,以便进行更鲁棒的优化。

请注意,模型可能会遇到 1.5 节中讨论的过拟合和欠拟合问题。对于任何数据集的分割,如果达到这些状态之一,模型就不再有用。为了克服这些问题,通常在同一数据集上训练具有不同能力的不同机器学习模型。另一个解决方案是在训练模型时使用正则化技术,这将在接下来的第 5.7 节中详细讨论。

正则化子

尽管正则化可能被认为是机器学习算法的可选元素,但它在训练更一般化的模型中起着重要作用。正则化是对数据、模型、损失函数或优化器进行的任何修改,以减少模型的泛化误差(即,模型应该具有低偏差和方差)。

这里,我们描述了损失函数的广泛使用的正则化项,称为 L 2 范数罚,或岭回归 (Hoerl & Kennard,1970),其将损失函数修改如下:

$$ L\left(\mathbf{x};\theta;\right)=\frac{1}{2};\sum \limits_{i=1}N{\left({y}{(i)}-{t}^{(i)}\right)}²+\frac{\lambda }{2};{\left\Vert \theta \right\Vert}_2² $$

(1.9)

其中∩θ2计算参数的 L 2 范数, λ 表示正则项的权重,通常设置为 1。用这个新的损失函数训练模型有助于减少我们关心的泛化误差,也就是说,该模型将在看不见的样本上表现得更好。

在 5.7 节,我们将讨论各种其他的正则化技术。现在,让我们把注意力从训练模型转移到偏差和方差的概念上。

1.5 偏差和方差的权衡

偏差和方差是不同数据集上模型性能的特征。偏差与训练集上的模型性能有关,而方差与验证集上的模型性能有关。我们希望找到一个低偏差和方差的模型。但是,实际上,我们通常用一种交换另一种。此外,偏差和方差权衡不仅影响传统的机器学习方法,而且困扰着整个机器学习领域。

在讨论偏差和方差的权衡之前,让我们先介绍一下泛化这个术语。如果该模型在看不见的数据点上表现得非常好,则称其具有良好的泛化特性。我们的目标是在机器学习中找到这样的模型。

主要有三种情况与模型的偏差和方差有关。虽然第四种情况(没有提到,但可以从其他三种情况推断出来)可能是可取的,但从统计上来看而不是是可能的,即使在理论上也是如此;否则,我们可能永远不会要求训练模型。

在第一种情况下,当模型在训练集上表现良好,但在多个验证集上表现不佳时,分别被称为具有低偏差和高方差。这意味着模型有足够数量的参数来很好地执行训练集,但它过拟合训练集(因为参数的数量超过了发现底层数据 PDF 的要求)或记住了训练集的映射,并且没有近似底层数据生成 PDF。这是因为如果它已经近似了数据分布,它也应该能够在验证集上表现良好,因为像训练集一样,验证集也在统计上表示数据 PDF。

在第二种情况下,当模型在训练集和多个验证集上都表现良好时,据说分别具有 偏差 和低 方差。在这里,模型已经近似了基础数据 PDF,所有数据集都是出于训练和测试目的从该 PDF 中采样的。在实践中,我们努力寻找这种模式。

在第三种情况下,当模型在训练集和多个验证集上都表现不佳时,分别被称为具有高偏差和高方差。这里,模型没有足够数量的参数来近似训练集,因此,在多个验证集上也表现不佳。因为模型容量低,即使在训练集上表现也不好,所以据说欠适应

1.6 为什么要深度学习?

我们讨论传统机器学习方法的各种问题,这些问题可以通过深度学习方法轻松解决。这激励我们研究和探索深度学习方法,正如我们将在下面看到的。

维度的诅咒

在实践中,我们通常会有几千甚至几百万个维度的样本。用传统的机器学习方法,如 k-最近邻回归器或分类器、决策树和其他方法处理这样的样本变得非常低效,有时甚至难以处理。

考虑设计机器学习算法的问题,该算法旨在近似可用数据集的映射。现在假设可用数据样本 x 的维数为 1,并且是标量变量,即 x ∈ ℝ.我们可以从用定积分区间划分输入的一维向量空间开始。而这样做,我们实际上是把输入向量空间分成了 M 这样的一维单元格(见图 1-3 (a))。我们可以在这条线上绘制训练集中的每个数据样本。我们需要在每个单元中至少有一个训练点,以便做出正确的预测。由于我们有每个样本的标签信息,当我们需要预测一个新的未知样本的标签时,我们可以简单地在这条线上绘制它,并将其分配给在该单元中具有最大计数的标签,用于分类任务。在回归的情况下,我们可以取训练点的所有实值标签的平均值,并将该值赋给这个测试样本。

img/484421_1_En_1_Fig3_HTML.jpg

图 1-3

(a)一维、(b)二维和(c)三维数据点的维数灾难。存在对样本的指数要求 M d 用于预测新样本的标签,其中 M = 4 是沿着每个维度的部分的数量,而 d 是维度的数量。

在一维输入空间中工作似乎非常容易。但是当我们跳到更高维度的空间,我们开始看到问题迅速出现。现在假设变量 x ∈ ℝ 2 的二维输入向量空间由分别代表 x 轴和 y 轴的变量 x 1x 2 组成。现在可以将每个训练点绘制为矩阵中的一个点。我们可以再次将 xx1 和 xx2 变量的 x 轴和 y 轴划分为 M 段,这样我们就得到了M2 单元格(参见图 1-3 (b))。我们可以使用前面描述的过程再次简单地预测这个测试样本的标签。当我们移动到三维向量空间 x ∈ ℝ 3 时,其中 x、y 和 z 轴分别由 x 1x2 和 x 3 变量表示,然后我们得到 M 3 截面(见图随着维数增加的趋势,我们可以看到训练集中所需的标记样本的数量随着维数的线性增加而呈指数增加。我们至少需要总共Md 个样本来近似数据集映射,其中 d 是样本特征空间的维度。维度和所需样本数量之间的这种指数关系被称为维度诅咒 (Bellman,2015)。

幸运的是,我们可以用深度神经网络模型解决维数灾难,我们将在本书后面看到。现在我们讨论传统机器学习算法的另一个问题,称为平滑度假设。

1.6.2 无效平滑度假设

传统的机器学习算法假设输入变量值的微小变化不会导致预测输出变量值的突然变化。这被称为平滑度假设。这意味着对于任何两个相似的输入值(例如,看起来相似的图像),预测标签应该总是与真实标签相同。直观地说,这也适用于我们的视觉系统,因为当两幅图像相似或不相似时,我们可以区分它们。当我们的稀疏训练数据没有填满所有单元时,这种假设是有帮助的;然后,我们可以简单地将输入值插值到更接近可用训练样本的位置,并将相同的标签分配给我们的测试样本。

在(Szegedy 等人,2013 年)的工作之前,这种观点在很大程度上被认为对非线性方法有效。作者表明,我们可以有意构建看起来相似但被深度神经网络错误分类的输入图像。从几何学上来说,我们可以说这个假设是有效的,但只对线性模型有效,而对于非线性模型,这个假设不成立。但是,由于我们对处理高维数据感兴趣,因为它携带重要的信息,并且由于传统机器学习方法的弱表示能力,我们无法实现良好的准确性。这促使我们采用深度学习方法。

接下来,我们看看深度学习相对于传统机器学习方法所提供的一些优势。

1.6.3 深度学习优势

正如我们之前讨论的,传统的机器学习方法存在各种问题。这阻碍了我们对高维数据的分析。如这里所讨论的,深度学习方法相对于传统的机器学习方法有各种优势。

与传统的机器学习方法不同,深度学习已知可以给出高度准确的结果,有时甚至在某些任务上超过人类水平的表现。深度学习的想法大致是受大脑神经系统处理直接从我们的世界获得的原始数据的方式的启发。类似于我们的眼睛和耳朵等感觉器官在发送到大脑之前对数据进行预处理的事实,我们有时也会在将数据用于深度学习模型之前对数据进行预处理,但这种预处理使用了完全不同的功能序列。但是请注意,关于大脑本身,我们还有许多事情不知道。深度学习方法使用原始数据作为输入样本,并对其进行处理,以学习内部分层高维空间中的抽象信息。我们不需要为模型选择重要的特征。因为选择过程在人类中是固执己见的,一些人会发现某一组特征对任务很重要,而另一些人会更喜欢其他一些特征。深度学习通过简单地处理原始数据本身来解决这个特征手工制作问题。深度学习中的模型具有多层类似神经元的功能,其中每层通常具有不同的维度。高维输入进入模型,并在不同的层中转换成不同的维度,直到生成预测输出的最后一层。承担图像分类的任务。在这里,初始图层(更接近输入)可能会从影像数据集中学习精细比例的细节,如边缘、颜色梯度等,而中间层(称为隐藏图层)可能会学习更粗糙比例的细节,如圆形、矩形等形状;靠近最后一层的层(称为预测层)可以学习眼球、身体形状等特征;然后最终预测层将预测给定图像样本的标签。深度学习中的这些模型有许多名称,如深度学习模型、人工神经网络、深度神经网络或简单的神经网络,名称的列表还在继续。第五章致力于介绍神经网络和训练它们的各种成功技术。

我们还注意到,从随机存取存储器(RAM)和计算速度的角度来看,当我们有大量样本并且每个样本都可以在大维度上表示时,传统方法有时是难以处理的。深度学习模型利用了并行处理比顺序处理快得多的事实。这些模型并行处理样本的特征。虽然机器学习在 2006 年复兴(Bengio et al .,2007;Hinton 等人,2006 年;Ranzato 等人,2007 年),大约在 2009 年,很明显图形处理单元(GPU)可以加速深度学习模型(刘冰等人,2009 年),甚至比中央处理单元(CPU)设备快 10 或 20 倍,用于训练和推理过程。从那以后,像 Nvidia 这样的公司已经投入了极大的努力 2 来构建更快的 GPU,并不断改进架构设计。谷歌也在努力建造更快的并行处理设备,他们称之为张量处理单元(TPUs)。云甚至边缘设备上都有 TPU。在撰写本文时,著名的平台即服务(PaaS)如 Google Colaboratory 和 Kaggle 提供了对云上 GPU 和 TPU 设备的免费访问,用于深度学习。这有助于极大地加速深度学习研究。这本书里写的所有代码都可以在 Google Colaboratory 平台上执行。

深度学习方法可以被认为是传统机器学习方法的继承者,因为它们具有各种有趣的优点。几乎本章中针对传统方法(除了非参数方法)讨论的每个想法和概念都可以移植,而几乎不需要对深度学习框架进行任何修改。从根本上说,深度学习的定义保持不变:最大似然估计的思想非常适合,因为深度学习模型本质上是参数模型,深度学习也需要 1.4 节中讨论的学习算法的所有元素,并且每种类型的机器学习都可以用深度学习方法来执行(并且更好)。我们将从第五章开始,用简单的神经网络深入研究深度学习。

1.7 摘要

本章介绍了与机器学习相关的基本概念。我们讨论了机器学习的各种范例,并介绍了帮助训练机器学习模型的最大似然估计的概念。然后,我们深入研究了机器学习算法的各种基本元素。我们还引入了偏差和方差的概念来理解模型的泛化特性。最后,我们揭示了深度学习方法相对于传统机器学习方法的优势。

我们将在以下章节中从基本概念开始研究深度学习。但在深入之前,在下一章,我们将研究不同数学分支的各种主题,这些主题对于理解深度学习是必不可少的。

二、基础数学

世界上发生的任何事情,其意义都不是某种最大值或最小值。

—莱昂哈德·欧拉

本章介绍了理解神经网络基础所必需的数学知识。我们强调,如果你的数学概念生疏了,不要跳过这一章。学习后面的章节时,你可以参考这一章。

在第 2.1 节中,我们介绍了线性代数,它用于利用神经网络进行预测,并计算损失函数相对于神经网络的梯度,这使得学习成为可能。线性代数运算的有效实现,如基本线性代数子程序(BLAS),也允许在现代硬件加速器如 GPU 上使用并行处理进行快速计算。线性代数在设计神经网络的结构中也起着重要的作用。神经网络可被视为一个概率框架,用于近似可用数据集的概率分布(第 1.3 节),这需要理解第 2.2 节中讨论的概率论。与确定性编程不同,神经网络的概率方法使其在处理值可能在连续空间中变化的输入数据时具有鲁棒性。在第 2.3 节中,我们引入了微分学来计算神经网络损失函数的梯度,这有助于确定如何训练神经网络。

2.1 线性代数

本节介绍不同的矩阵和向量,重要的一元和二元矩阵运算,以及规范。这些数据结构概括在张量的概念下,在 4.1 节中讨论。

矩阵和向量

有各种类型的矩阵和向量,但我们将自己限制在深度学习领域中重要的那些。我们首先介绍不同的矩阵,然后讨论一些重要的向量。

当一个方阵(具有相同的行数和列数)沿主对角线的所有属性都为 1 时,该矩阵称为单位矩阵,用 I n 表示,其中In∈ℝn×n表示例如,5 阶的单位矩阵被写成如下:

$$ \left[\begin{array}{l}1\kern0.5em 0\kern0.5em 0\kern0.5em 0\kern0.5em 0\ {}\begin{array}{cccc}0& 1& 0& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}0& 0& 1& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}0& 0& 0& \begin{array}{cc}1& 0\end{array}\end{array}\ {}\begin{array}{cccc}0& 0& 0& \begin{array}{cc}0& 1\end{array}\end{array}\end{array}\right] $$

(2.1)

我们还可以找到一个给定矩阵的逆矩阵,用A?? 1 表示,其中 A 是可逆矩阵。逆矩阵由下面的等式定义:

$$ {\mathbf{A}}^{-1};\mathbf{A}={\mathbf{I}}_n $$

(2.2)

其中I??n是一个 n 阶单位矩阵。注意,矩阵 A 必须是可逆的,以产生其逆矩阵A1。

对角矩阵 D 定义为除了 i = j 之外所有元素为零的矩阵。例如,下面是一个对角矩阵:

$$ \left[\begin{array}{l}2.5\kern0.5em 0\kern0.86em 0\kern0.5em \begin{array}{cc}0& 0\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 8& \kern0.36em 0& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& -3& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& \kern0.36em 0& \begin{array}{cc}4& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& \kern0.36em 0& \begin{array}{cc}0& 5\end{array}\end{array}\end{array}\right] $$

(2.3)

我们可以通过使用 diag(.)运算符。如果我们的矩阵是 D,那么 diag( D )返回一个向量 x ,包含沿着 D 主对角线的元素,如下:

$$ \mathbf{x}=\left[\begin{array}{l}2.5\ {}\kern0.36em 8\ {}-3\ {}\kern0.36em 4\ {}\kern0.36em 5\end{array}\right] $$

(2.4)

当我们转置一个矩阵 S 并得到相同的矩阵时,那么这种矩阵称为对称矩阵。从形式上来说,对称矩阵是其转置返回相同矩阵的矩阵,即ST=S:

$$ \mathbf{S}=\left[\begin{array}{l}0\kern0.5em 8\kern0.5em 5\kern0.5em \begin{array}{cc}6& 2\end{array}\ {}\begin{array}{cccc}8& 4& 3& \begin{array}{cc}7& 9\end{array}\end{array}\ {}\begin{array}{cccc}5& 3& 7& \begin{array}{cc}1& 1\end{array}\end{array}\ {}\begin{array}{cccc}6& 7& 1& \begin{array}{cc}3& 6\end{array}\end{array}\ {}\begin{array}{cccc}2& 9& 1& \begin{array}{cc}6& 5\end{array}\end{array}\end{array}\right]={\mathbf{S}}^T $$

(2.5)

在前面的等式中,矩阵 S 是对称的,因为它等于它自己的转置。

我们现在介绍一些在深度学习中有意义的向量。假设两个向量 uvRn使得uTv= 0,假设这两个向量都是非零向量,则称为正交向量。当一个向量的欧氏范数为 1 时,则称为单位向量,即∨u2= 1。现在我们假设两个向量 uv 也是单位向量;那么这两个向量被称为正交向量。形式上,当两个单位向量在性质上正交时,那么它们被简单地称为正交向量

2.1.2 一元矩阵运算

我们介绍一些适用于矩阵的一元算子。转置是神经网络中对矩阵常用的一元运算之一。矩阵 B转置给出一个新的矩阵 A ,其行元素被其自己的列元素交换,使用以下规则:

$$ {A}_{i,j}={B}_{j,i}\forall i\ne j $$

(2.6)

例如,假设对以下具有不同行数和列数的非对称矩阵进行转置:

$$ \mathbf{S}=\left[\begin{array}{l}-2\kern0.5em 0\kern0.5em 5\ {}\begin{array}{ccc}\kern0.36em 4& 2& 9\end{array}\end{array}\right] $$

(2.7)

$$ {\mathbf{S}}^T=\left[\begin{array}{c}-2\kern0.5em 4\ {}\begin{array}{cc}\kern0.24em 0& 2\end{array}\ {}\begin{array}{cc}\kern0.24em 5& 9\end{array}\end{array}\right] $$

(2.8)

可以清楚地看到,行与它们对应的列进行了互换,即矩阵 S ∈ ℝ 2×3 ,而其转置st∈ℝ3×2。另一种算子称为对角线算子,用 diag(.),前面讨论的也是一元运算符的例子。如果需要计算所有主对角线条目的总和,那么我们使用 Tr(.).数学上用以下等式描述:

$$ Tr\left(\mathbf{D}\right)=\sum \limits_i{D}_{i,i} $$

(2.9)

在我们之前考虑过的对角矩阵 D 上应用追踪算子给出如下:

$$ \mathbf{D}=\left[\begin{array}{l}2.5\kern0.5em 0\kern0.86em 0\kern0.5em \begin{array}{cc}0& 0\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 8& \kern0.36em 0& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& -3& \begin{array}{cc}0& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& \kern0.36em 0& \begin{array}{cc}4& 0\end{array}\end{array}\ {}\begin{array}{cccc}\kern0.48em 0& 0& \kern0.36em 0& \begin{array}{cc}0& 5\end{array}\end{array}\end{array}\right],\mathbf{x}=\left[\begin{array}{l}2.5\ {}\kern0.36em 8\ {}-3\ {}\kern0.36em 4\ {}\kern0.36em 5\end{array}\right],s=\sum \limits_i{x}_i=16.5 $$

(2.10)

通过对沿着矩阵 D 的主对角线的元素求和,我们得到总和等于 16.5。

二进制矩阵运算

我们针对两种特殊情况介绍了一些重要的矩阵二元运算:一个操作数是矩阵,另一个是标量,两个操作数都是矩阵。这种分类简化了对矩阵上二元运算的理解。

一些最简单的矩阵运算是矩阵和标量之间的运算。假设一个标量 s 和矩阵 A ∈ ℝ m×n 产生另一个相同形状的矩阵b∈ℝm×n,当某个运算符作用于操作数 sA 之间时。这里,运算符可以是任何基本运算符,如加、减、乘或除。当在 sA 之间应用这些操作符中的任何一个时,则 sA 的每个元素之间会单独发生一次操作。例如,加法运算可以写成:

$$ S+\mathbf{A}=S+{A}_{i,j} $$

(2.11)

假设矩阵 A ∈ ℝ 2×2 和标量s= 3:

$$ \mathbf{A}=\left[\begin{array}{cc}1& 5\ {}4& 9\end{array}\right] $$

(2.12)

$$ s\mathbf{A}=3;\left[\begin{array}{cc}1& 5\ {}4& 9\end{array}\right]=\left[\begin{array}{cc}3.1& 3.5\ {}3.4& 3.9\end{array}\right]=\left[\begin{array}{cc}3& 15\ {}12& 27\end{array}\right] $$

(2.13)

这里,为了简单起见,我们用点来表示标量之间的乘法。注意标量 s 乘以矩阵 A 的每个元素。

讨论了标量和矩阵之间的运算后,我们现在介绍两个矩阵之间最重要的运算之一,称为矩阵乘法。这是允许神经网络在现代并行处理硬件加速器上有效工作的基本操作。

让我们假设两个矩阵x∈ℝm×ny∈ℝn×o相乘在一起产生一个新的矩阵z∈ℝm×oXY 的矩阵乘法简单写成 Z = XY 。这些矩阵必须满足某些条件,它们之间的乘法运算才有效。我们要求 X 的列数必须等于 Y 的行数,这在相乘后导致矩阵 Z 的行数和列数分别等于 XY 的行数和列数。形式上,两个矩阵之间的乘积定义如下:

$$ {Z}_{i,j}=\sum \limits_k{X}_{i,k};{Y}_{k,j} $$

(2.14)

这里, ijk 是从 1 开始直到它们的维度大小的索引——分别定义变量 mno 。对于矩阵 XY ,我们假设 m = 2, n = 3, o = 2。通过在它们之间应用矩阵乘法,我们得到 Z ∈ ℝ 2×2 如下:

$$ \mathbf{X}=\left[\begin{array}{cc}1& 6\kern0.5em 2\ {}3& \begin{array}{cc}1& 4\end{array}\end{array}\right],\mathbf{Y}=\left[\begin{array}{c}\begin{array}{cc}5& 1\end{array}\ {}\begin{array}{cc}2& 2\end{array}\ {}\begin{array}{cc}3& 2\end{array}\end{array}\right] $$

(2.15)

$$ \mathbf{Z}=\left[\begin{array}{cc}1.5+6.2+2.3& 1.1+6.2+2.2\ {}3.5+1.2+4.3& 3.1+1.2+4.2\end{array}\right]=\left[\begin{array}{cc}23& 17\ {}29& 13\end{array}\right] $$

(2.16)

前面的例子显示了 X ∈ ℝ 2×3Y ∈ ℝ 3×2 之间的矩阵乘法返回 Z ∈ ℝ 2×2 。还要注意,矩阵乘法 YX 是不可能的,因为它不满足矩阵乘法的维数要求。因此,矩阵乘法不是交换运算。

请注意,矩阵乘法只是左侧矩阵的行和右侧矩阵的列之间的点积的重复应用。这也是为什么在一些像 NumPy 这样的库中找到一个名为dot的函数是非常常见的,而另一些像 Swift for TensorFlow 将其命名为matMul

矩阵之间还存在另一种乘积,称为按元素的乘积Hadamard 乘积,由 P = QR 表示,但是在用于 TensorFlow 的 Swift 中,我们使用*运算符来表示相同的乘积。这里,我们要求 QR 必须具有相同的形状,并且 Hadamard 运算产生一个与 QR 形状相同的 P 矩阵。它由下列等式给出:

$$ {P}_{i,j}={Q}_{i,j}\cdot {R}_{i,j} $$

(2.17)

下面给出了一个基于元素的产品的实例:

$$ \mathbf{Q}=\left[\begin{array}{cc}1& 3\ {}2& 4\end{array}\right],\mathbf{R}=\left[\begin{array}{cc}6& 9\ {}4& 1\end{array}\right] $$

(2.18)

$$ \mathbf{P}=\mathbf{Q}\odot \mathbf{R}=\left[\begin{array}{cc}1.6& 3.9\ {}2.4& 4.1\end{array}\right]=\left[\begin{array}{cc}6& 27\ {}8& 4\end{array}\right] $$

(2.19)

哈达玛乘积生成与 QR 形状相同的矩阵 P

2.1.4 规范

在深度学习中,有时可能需要测量向量的大小。为了完成这个任务,使用了一个叫做 norm 的函数。范数p 维向量空间中测量向量离原点的距离。向量的范数由 L p 或∨xp表示,并使用以下公式计算:

$$ {\left\Vert x\right\Vert}_p={\left(\sum \limits_i{\left|{x}_i\right|}p\right)}{\frac{1}{p}} $$

(2.20)

其中 p 必须满足条件 p ≥ 1。

有些规范在深度学习中是如此的司空见惯,以至于有了自己的名字。欧几里德范数L2范数就是这样一个例子,其中 p = 2,它测量一个向量离原点的欧几里德距离。另一种感兴趣的定额是L1定额其中 p = 1。它还测量到原点的距离,但在零值和非常小的非零值之间的差异很重要的情况下,它更有帮助。如果我们向向量的每个元素添加一个小的非负值 e ,那么 L 2 范数与它自身的平方成比例地增加,但是当非常小的变化具有高显著性时, L 1 范数优于 L 2

在下一节继续讨论概率论之前,我们再讨论两个范数:max 范数和 Frobenius 范数。 max 范数表示为 L 简单来说就是向量中一个元素的最大幅度的绝对值,其写法如下:

$$ {\left\Vert x\right\Vert}_{\infty }=\underset{i}{\max};\left|{x}_i\right| $$

(2.21)

最后,我们还可以使用 Frobenius 范数来度量矩阵到原点的距离。它类似于向量的 L 2 范数。但很少使用,写如下:

$$ {\left\Vert \mathbf{A}\right\Vert}_F=\sqrt{\sum \limits_{i,j}{A}_{i,j}²} $$

(2.22)

现在让我们熟悉一下与神经网络有关的另一个重要的数学分支——概率论。

2.2 概率论

深度学习就是对看不见的数据点做出决策,因为现实世界是高度不确定的。例如,一些不确定性在于确定事件的发生,例如“你将在明天早上 5 点醒来”或“这场音乐会将会很好”这些都是不确定的事件,因为它们可能会发生,也可能不会发生;因此,它们出现的概率在[0,1]范围内。相比之下,有些事件是完全肯定会发生的,例如,“太阳明天会升起”或“水是由氢和氧组成的”,所以它们的概率是 1。从未发生的事件,例如,“蓖麻油比水更粘”或“植物不在土壤中生长”,其概率等于 0。概率论在构建深度学习问题中起着至关重要的作用。

概率是一个数学框架,用于使用位于[0,1]范围内的实数来量化不确定事件。如果一个事件 e 有很高的发生概率,那么它的概率用一个更接近于 1 的实数来量化,而一个最不可能的事件有更接近于 0 的概率。形式上,一个事件 e概率,用 P ( e )表示,定义为无限次实验时,事件 e 发生的次数与总试验次数 T 的比值, T → ∞。概率中的另一个重要术语是随机变量,它被定义为可以从一组定义的有效值中取一个特定值的变量,其中每个值都有可能从给定的概率分布中被采样。这里, e 是随机变量;并且,从数学上讲,一个事件发生的概率 e 写成如下:

$$ P\left(\mathbf{e}=e\right)=\frac{n}{T}. $$

(2.23)

这里, e 是随机变量 e 发生的事件或值, n 是实验进行 T → ∞次时发生事件 e 的次数。注意,有时我们简单地把 P ( e )写成相当于把P(e=e)。也不要混淆概率论中的粗体小写字母符号和线性代数中的向量符号。这里使用这种符号是为了方便。

我们现在将使用一个简单的例子,以通俗易懂的方式解释概率论的基础知识。下面这个例子,如图 2-1 所示,改编自(Bishop,2006)教材的课本。让我们假设一个随机变量,我们称之为一天中的时间段,用 t = { m,a,e,b 表示,读作“ t 是一组值 m,a,e,b ,其中 m,a,e,b 分别代表早晨、下午、晚上和就寝时间。基于从 t 采样的值,我们可以决定采取由集合 a = { s,p,r,w 表示的动作,其中 s,p,r,w 分别简单地表示学习、玩耍、休息和锻炼。图 2-1 为随机变量 ta 的示意图。

img/484421_1_En_2_Fig1_HTML.jpg

图 2-1

描述集合 t = {m,a,e,b}和 a = {s,p,r,w}中事件联合和有条件发生的概率的网格

在图 2-1 中,每行代表从 t 可以取的事件集{ m,a,e,b 中采样的第 i 个指标值,每列代表从 a 可以取的事件集{ s,p,r,w 中采样的第 j 个指标值。术语 r ic j 分别表示事件实例的数量 t ia j 。这里, i = 1、…、 Mj = 1、…、 N 分别是沿行和列的索引, M = N = 4。我们用P(t=tI)来表示一个事件发生的概率tI,它简单地由下面的等式给出:

$$ P\left(\mathbf{t}={t}_i\right)=\frac{r_i}{T} $$

(2.24)

这里, T 是执行的实验总数。同样,事件发生的概率aj

$$ P\left(\mathbf{a}={\mathbf{a}}_j\right)=\frac{a_j}{T}. $$

给出(2.25)

2.2.1 联合概率

随机变量 t 采样值tIa 采样值 a j 时的概率,用P(a=ajt=事件 t

注意,任意数量的随机变量的联合概率是对称的;因此,P(a=ajt=ttI)与写作P(t=ttIa

**### 条件概率

在给定另一个事件已经发生并且这两个事件相关的情况下,当我们必须找到一个事件的概率时,那么这种概率被称为条件概率。按照我们的例子,让我们假设 t 已经发生并取值tI;而给定这个,我们就要求 a 取值 a j 的概率。这用P(a=aj|t=tI)来表示, 读作“事件发生的概率 a j 假定事件 t i 已经发生”,并由单元格中实例数在第( ij 个索引处的比率给出,用nij表示 形式上,这由下面的等式给出:

$$ P\left(\mathbf{a}={\mathbf{a}}_j;\left|\mathbf{t}={t}_i\right.\right)=\frac{n_{ij}}{r_i} $$

(2.27)

类似地,假定aj已经发生,事件tI发生的条件概率由nij与由 c j 给出的 j 列中事件总数的比值给出。形式上,P(t=tI|a=aj)由以下等式描述:

$$ P\left(\mathbf{t}={t}_i;\left|\mathbf{a}={\mathbf{a}}_j\right.\right)=\frac{n_{ij}}{c_j} $$

(2.28)

基本规则

现在我们已经充分掌握了各种概率的知识,利用这些知识,我们将推导出概率论的两个基本规则,即和与积规则。首先,参见图 2-1 中的$$ {r}_i=\sum \limits_j{n}_{ij} $$,因为 r * i * 中的事件总数仅仅是该行每个单元格中所有事件的总和。使用等式 2.24 和 2.26 ,我们得到

$$ P\left(\mathbf{t}={t}_i\right)=\sum \limits_jP\left(\mathbf{t}={t}_i,\mathbf{a}={a}_j\right)=\frac{r_i}{T} $$

(2.29)

这被称为概率的和。它也被称为边际概率,因为它是通过将所有变量边缘化或求和得到的,除了这里期望的一个变量,即 t 。同样,我们可以把P(a=aj)写成

$$ P\left(\mathbf{a}={a}_j\right)=\sum \limits_iP\left(\mathbf{t}={t}_i,\mathbf{a}={a}_j\right)=\frac{c_j}{T} $$

(2.30)

通过边缘化随机变量 t 。接下来,我们将使用已经讨论过的条件概率来推导概率的乘积规则。利用等式 2.26 、 2.28 和 2.30 ,我们得到

$$ P\left(\mathbf{t}={t}_i,\mathbf{a}={a}_j\right)=\frac{n_{ij}}{T}=\frac{n_{ij}}{c_j}\cdot \frac{c_j}{T} $$

(2.31)

$$ P\left(\mathbf{t}={t}_i,\mathbf{a}={a}_j\right)=P\left(\mathbf{t}={t}_i\left|\mathbf{a}={a}_j\right.;\right);P\left(\mathbf{a}={a}_j\right). $$

(2.32)

这叫做概率的乘积法则。请注意,它只是将两个随机变量的联合概率分解为它们的边际概率和条件概率的乘积。

到目前为止,我们已经对符号非常精确了,但是它使更大的方程变得复杂,并且降低了它们的适用性。我们现在选择一种更简单的符号来表示这样的概率方程。让我们将随机变量 ta 的概率分布分别表示为 P ( t )和 P ( a ),而P(tI)和P(aj】现在我们可以更简洁地重写等式 2.30 和 2.32 如下:

$$ P\left(\mathbf{a}\right)=\sum \limits_iP\left(\mathbf{t},\mathbf{a}\right) $$

*(2.33)

$$ P\left(\mathbf{t},\mathbf{a}\right)=P\left(\mathbf{t};\left|\mathbf{a}\right.\right);P\left(\mathbf{a}\right) $$

(2.34)*

这里,方程 2.33 和 2.34 分别代表概率的和与积法则。我们现在将在下文中介绍概率链规则的概念。

链式法则

现在我们知道,两个随机变量的联合概率可以分解为它们的条件概率和边际概率的乘积。但是“如果我们想要分解两个以上随机变量的概率呢?”这就是概率链式法则的作用。概率的链规则被简单地定义为在尽可能多的随机变量上重复应用联合概率的乘积规则。因此,联合概率被分解为所有随机变量的边际概率和条件概率的乘积。

我们假设四个随机变量,分别是, x 1x 2x 3x 4 。我们可以将它们的联合概率写成P(x1x 2x 3x 4 )可以使用如下等式描述的乘积法则进一步分解:

$$ P\left({\mathbf{x}}_1,{\mathbf{x}}_2,{\mathbf{x}}_3,{\mathbf{x}}_4\right)=P\left({\mathbf{x}}_4\left|{\mathbf{x}}_1,{\mathbf{x}}_2,{\mathbf{x}}_3\right.\right);P\left({\mathbf{x}}_1,{\mathbf{x}}_2,{\mathbf{x}}_3\right) $$

(2.35)

)

使用等式 2.36 和 2.37 ,等式 2.35 可以以其分解形式重写如下:

$$ P\left({\mathbf{x}}_1,{\mathbf{x}}_2,{\mathbf{x}}_3,{\mathbf{x}}_4\right)=P\left({\mathbf{x}}_1\right)P\left({\mathbf{x}}_2\left|{\mathbf{x}}_1\right.\right);P\left({\mathbf{x}}_3\left|{\mathbf{x}}_1,{\mathbf{x}}_2\right.\right);P\left({\mathbf{x}}_4\left|{\mathbf{x}}_1,{\mathbf{x}}_2,{\mathbf{x}}_3\right.\right) $$

(2.38)

概率的链式法则的一般公式可以写成:

$$ P\left({\mathbf{x}}_1,\dots, {\mathbf{x}}_n\right)=\prod \limits_{i=1}^nP\left({\mathbf{x}}_i\left|{\mathbf{x}}_i,\dots, {\mathbf{x}}_n\right.\right). $$

(2.39)

切记P(x|x)=P(x)。前面演示链式法则的例子可能看起来有点复杂;因此,让我们假设一个更简单的版本,它通过选择随机变量的名称来简化,即 abcd

我们现在将在这些随机变量上分解这个概率分布,这些随机变量由 P ( abcd )表示,在下面的等式中一步一步地分解:

$$ P\left(\mathbf{a},\mathbf{b},\mathbf{c},\mathbf{d}\right)=P\left(\mathbf{a}\left|\mathbf{b},\mathbf{c},\mathbf{d}\right.\right);P\left(\mathbf{b},\mathbf{c},\mathbf{d}\right) $$

(2.40)

$$ P\left(\mathbf{b},\mathbf{c},\mathbf{d}\right)=P\left(\mathbf{b}\left|\mathbf{c},\mathbf{d}\right.\right);P\left(\mathbf{c},\mathbf{d}\right) $$

(2.41)

$$ P\left(\mathbf{c},\mathbf{d}\right)=P\left(\mathbf{c}\left|\mathbf{d}\right.\right);P\left(\mathbf{d}\right) $$

(2.42)

将方程 2.42 代入 2.41 中,然后将 2.41 代入 2.40 中,得到

$$ P\left(\mathbf{a},\mathbf{b},\mathbf{c},\mathbf{d}\right)=P\left(\mathbf{a}\left|\mathbf{b},\mathbf{c},\mathbf{d}\right.\right);P\left(\mathbf{b}\left|\mathbf{c},\mathbf{d}\right.\right);P\left(\mathbf{c}\left|\mathbf{d}\right.\right)P\left(\mathbf{d}\right). $$

(2.43)

注意,分解后的方程 2.38 和 2.43 是对称的。简而言之,概率链规则帮助我们将任意数量的随机变量的联合概率描述为它们的条件概率和边际概率的乘积,并且更一般地通过等式 2.39 获得。

贝叶斯规则

我们还可以利用联合概率是对称的这一事实来推导两个随机变量的条件概率之间的关系。我们可以将随机变量 xy 的联合概率重写为

$$ p\left(\mathbf{x},\mathbf{y}\right)=P\left(\mathbf{y},\mathbf{x}\right) $$

(2.44)

利用乘积法则,我们得到

$$ \boldsymbol{P}\left(\mathbf{x}\left|\mathbf{y}\right.\right)=\frac{\boldsymbol{P}\left(\mathbf{y}\left|\mathbf{x}\right.\right);\boldsymbol{P}\left(\mathbf{x}\right)}{\boldsymbol{P}\left(\mathbf{y}\right)} $$

(2.45)

这里,等式 2.45 被称为贝叶斯规则,其通过联合概率的对称性质来关联条件概率。

2.3 微积分

微积分分支分为两个子分支,即微积分。因为神经网络训练需要计算偏导数,所以学习微分学是很重要的,也是本节的主题。我们不关心积分。我们先介绍函数的概念,然后解释微分学。本节受(Deisenroth 等人,2020 年)教科书第五章的启发。

功能

微积分的核心是函数的概念。数学函数,表示为 f : 𝔸 → 𝔹,是从集合𝔸到集合𝔹的映射,将𝔸的每个元素关联到𝔹.的唯一元素假设变量 x ∈ 𝔸和y∈𝔹;那么y=f(x)就是将单个元素 x ∈ 𝔸映射到唯一元素 y ∈ 𝔹.的函数 f 换句话说,函数 f (。)转换输入 x 并返回输出 y

2.3.1.1 一元函数

当输入变量 x 到函数 f (。)是一个标量,并且函数返回一个标量输出 y ,那么这个函数被称为单变量(或者标量 ) 函数,如图 2-2 所示。换句话说,一元函数只是一个从标量实数到标量实数的映射, f : ℝ → ℝ.

img/484421_1_En_2_Fig2_HTML.png

图 2-2

标量函数 f(。)从 x ∈ ℝ到 y ∈ ℝ的映射

考虑一个标量函数 g :从集合𝔹到集合ℂ的𝔹 → ℂ映射,并取函数 f 的输出。)作为其输入,即z=g(y)=g(f(x))。这可以被视为多个函数的顺序链接,其中的 g 也被称为复合函数,如图 2-3 所示。我们也可以把复合函数写成gf(x)其中∘算子表示 g (。)和 f (。)功能。注意,函数的链接可以存在任意多的函数。我们将在后面看到,神经网络只是简单的复合向量函数。

img/484421_1_En_2_Fig3_HTML.png

图 2-3

复合标量函数 g ∘ f(x)映射

2.3.1.2 多元函数

前面描述的一元函数是函数的最简单形式。但是在深度学习中,我们会要求处理高维输入变量的函数。我们可以把这个定义扩展到一个输入包含多个变量的函数,这个函数叫做多元函数(如图 2-4 定义为 f : ℝ m → ℝ其中输入变量x∈ℝm(行向量)。一个真实世界的例子是速度 s ( dt ),它是两个变量的函数,即距离 d ∈ ℝ和时间 t ∈ ℝ,定义为 s ( dt)=d/t我们可以将这两个输入变量表示为一个二维行向量 x = d t 变量。另一个例子是求和运算符σIxI,它返回一个向量 x 的所有标量元素之和。**

![img/484421_1_En_2_Fig4_HTML.png 图 2-4 一个多元函数 f(。)从 x ∈ ℝ m 到 y ∈ ℝ的映射在(a)中明确示出,在(b)中简洁示出#### 2.3.1.3 矢量函数向量函数f:ℝm→ℝ→n只是多元函数f:ℝm→ℝ.的扩展版在 vector 函数中,我们有一个集合了 n 多元函数f=f1fn的一行 vector,其中yI=fI(当 f 行向量中的每个多元函数应用于输入向量变量 x 时,返回一个标量yI=fI(x)。然后我们将这些输出沿着 n 列堆叠起来,创建一个向量y=f(x)∈ℝn。换句话说,对输入向量变量x∈ℝm应用向量函数 f 产生一个输出向量变量y∈ℝn(见图 [2-5 ,从而映射 m-n注意,向量被认为是列向量更合适,但是我们认为所有的向量(另外提到的)都是行向量。这种考虑是为了方便数据集如何排列(见 1.4.1 小节),如何处理,以及我们如何执行我们的神经网络功能的微分,我们将在后面看到。**img/484421_1_En_2_Fig5_HTML.png

图 2-5

一个向量函数 f(。)从 x ∈ ℝ m 到 y ∈ ℝ n 的映射在(a)中明确示出,在(b)中紧凑示出

2.3.1.4 矩阵函数

我们还可以定义更高维的函数,比如一个矩阵函数,定义为从向量到矩阵的映射f:ℝm→ℝo或从矩阵到矩阵的映射f:ℝ×m×n→ℝo 我们来看看矩阵函数中的向量到矩阵的映射,它只是一个列向量中的向量函数的集合,其中列向量中有nf=f1fnfj:ℝmf 列 vector 中的每个向量函数应用于输入向量变量x∈ℝm时,它返回一个向量yj=fj(然后我们将这些输出沿着 q 行堆叠起来,创建一个矩阵y=f(x**)∈ℝn×o。**

![img/484421_1_En_2_Fig6_HTML.jpg 图 2-6 矩阵函数 F(。)从 x ∈ ℝ m 到 Y ∈ ℝ n×o 的映射下面的文本解释了我们在前面的文本中描述的函数的区别。我们不会讨论张量函数,因为神经网络中的所有微分都可以用向量函数本身来描述。但我们将对矩阵函数进行微分,以展示 Swift 语言(3.4 节)的算法微分(AD)(3.3 节)中使用的一个巧妙技巧,使导数计算变得容易。### 2.3.2 一元函数的微分在从函数的导数开始之前,我们引入差商的概念。差商被定义为当输入改变一个小值时输出函数值的变化量。这种求导方法被称为数值求导 :$$ \frac{\delta y}{\delta x}:= \frac{f\left(x+\delta x\right)-f(x)}{\delta x}. $$

(2.46)

这里, δ 是一个希腊字母δ用来表示一个小值。在上式中,δy=f(x+δx)—f(x)是输入 δx 有微小变化时输出函数值的变化。当 δx = 0 时,则 δy = 0。该比率代表割线(将图形分成两部分或更多部分的线)的斜率,如图 2-7 所示。

img/484421_1_En_2_Fig7_HTML.png

图 2-7

一元函数从(x,f(x))到(x + δx,f(x + δx))的割线

当输入值的这种明显的小变化变得可以忽略不计时,也就是说,它接近 0,表示为 δx → 0,我们得到在 x 处切线的斜率。切线是在点 x 处平行于曲线的线,代表曲线在 f ( x )处的斜率。如果函数 f (,则导数存在。)是可微的。对于一个要可微的函数,它必须在输入空间的每一点都是连续的,一个极限必须存在于微分点,并且该极限也必须存在于接近它的给定点的左边和右边。如果一个函数满足这三个要求,它就被认为是可微的。请记住,我们在神经网络文献中处理的所有函数都是可微的。函数 f 的导数(。)定义为

$$ \frac{\mathrm{d}y}{\mathrm{d}x}:= \underset{h\to 0}{\lim};\frac{f;\left(x+h\right)-f(x)}{h} $$

(2.47)

这里,h0,是一个非常非常小的正数。一个函数的正切 f (。)在点 x 是 f′(x)的导数,指向输出函数值最陡上升的方向。我们用 f 的导数来表示。)在 x 处为f'(x)。如果函数的输入是一个常数,那么导数就是 0,因为常数不能变化。

一些可微函数,如 sigmoid、softmax、ReLU 等,在神经网络中是如此常见,以至于我们通常宁愿记住它们的导数函数,而不是再次找到它们。这些列出的功能称为激活功能,在第 5.4 节中详细讨论。

2.3.2.1 微分法则

有时我们需要用初等算术运算来计算复合函数或多函数的导数。这里,我们遵循下面描述的四个微分规则。以下一般规则适用于单变量函数:

  1. 求和规则😦f(x)+g(x)=f(x)+g′(x)

  2. 产品规则😦f(x)g(x)=f(x)g(x)+f(x)

  3. 链式法则😦f(g(x))=f(g(x)g(x)

  4. 商法则 : $$ \left(\frac{f(x)}{g(x)}\right)=\frac{f{\hbox{'}}(x)g(x)-f(x){g}{\hbox{'}}(x)}{{\left(g(x)\right)}²} $$

2.3.3 多元函数的微分

我们前面讨论过一元函数的微分。但是当我们必须处理一个多输入变量的函数,称为多元函数时,偏导数的概念就被利用了。在偏导数中,函数的导数是针对每个变量单独计算的,同时将其他变量视为常数,每个常数输入变量的偏导数为零。多元函数的偏导数 f (。)关于某输入变量 x 1 的写法如下:

$$ \frac{\partial f\left(\mathbf{x}\right)}{\partial {x}_1}=\underset{h\to 0}{\lim};\frac{f\left({x}_1+h,\dots, {x}_m\right)-f\left({x}_1\right)}{h} $$

(2.48)

这里∂f(x)/∂x1 是 f (。)相对于x1 意味着在这个偏导数的计算过程中其他变量保持不变。这个计算函数 f 的偏导数的过程。)对行向量x∈ℝm中的每个输入变量重复。然后我们将这些偏导数累加到一个行向量中,称之为多元函数 f梯度(。)定义如下:

$$ {\nabla}_{\mathbf{x}}f=\frac{\mathrm{d}f\left(\mathbf{x}\right)}{\mathrm{d}{x}_1}=\left[\begin{array}{c}{\lim}_{h\to 0};\frac{f\left({x}_1+h,\dots, {x}_m\right)-f\left({x}_1\right)}{h}\ {}\vdots \ {}{\lim}_{h\to 0};\frac{f\left({x}_1,\dots, {x}_m+h\right)-f\left({x}_m\right)}{h}\end{array}\right] $$

(2.49)

因为梯度是多元函数的导数,我们可以写成 df(x)/dx,而∂f(x)/∂xI表示 f (。)相对于单个输入变量 x i 。有时候我们也会用 grad f (。)而不是 nabla 符号∇xf来表示梯度。我们可以用更简洁的方式重写前面的等式,如下所示:

$$ {\nabla}_{\mathbf{x}}f=\left[\begin{array}{c}\frac{\partial f}{\partial {x}_1}\ {}\vdots \ {}\frac{\partial f}{\partial {x}_m}\end{array}\right] $$

(2.50)

后面为了方便,我们将假设梯度∇xf∈ℝm为矩阵而不是向量。正如我们将在后面看到的,对于向量函数(例如,密集连接的神经网络),这个梯度的行维度大小将增加,从而它将成为一个包含向量函数偏导数的矩阵。

偏导数的 2.3.3.1 规则

通过将之前讨论的单变量函数的规则扩展如下,可以容易地获得用于计算更高维函数的偏导数的规则:

  1. 求和规则 : $$ \frac{\partial }{\partial x};\left(f\left(\mathbf{x}\right)+g\left(\mathbf{x}\right)\right)=\frac{\partial f\left(\mathbf{x}\right)}{\partial x}+\frac{\partial g\left(\mathbf{x}\right)}{\partial x} $$

  2. 产品规则 : $$ \frac{\partial }{\partial x};\left(f\left(\mathbf{x}\right);g\left(\mathbf{x}\right)\right)=\frac{\partial f}{\partial x};g\left(\mathbf{x}\right)+f\left(\mathbf{x}\right)\frac{\partial g}{\partial x} $$

  3. 链式法则 : $$ \frac{\partial }{\partial x};\left(f\circ g\right)\left(\mathbf{x}\right)=\frac{\partial }{\partial x};\left(f\left(g\left(\mathbf{x}\right)\right)\right)=\frac{\partial f}{\partial g};\frac{\partial g}{\partial x} $$

我们已经取消了偏导数的商规则,因为在神经网络中,我们将主要处理这三个规则。请注意,这些规则类似于衍生产品的规则。请记住,函数在偏微分过程中的顺序非常重要,因为现在涉及到了矩阵和向量,并且与标量函数相比,这些数据结构上的一些操作(例如乘法)是不可交换的。

接下来,我们介绍向量函数的微分,这是训练神经网络的关键。

2.3.4 向量函数微分

我们以前已经讨论过向量函数。让我们考虑一个向量函数f:ℝm→ℝnmn 维向量空间的映射。如前所述,我们有一个包含多元函数映射的行向量f=[f1fn】fI:ℝm→ℝ.向量函数的导数可以用极限定义写成如下:

$$ {\nabla}_{\mathbf{x}}\mathbf{f}==\left[\begin{array}{ccc}{\lim}_{h\to 0};\frac{f_1\left({x}_1+h,\dots, {x}_n\right)-{f}_1\left({x}_1\right)}{h}& \dots & {\lim}_{h\to 0};\frac{f_{\mathrm{n}}\left({x}_1+h,\dots, {x}_n\right)-{f}_{\mathrm{n}}\left({x}_1\right)}{h}\ {}\dots & \ddots & \dots \ {}{\lim}_{h\to 0};\frac{f_1\left({x}_1,\dots, {x}_n+h\right)-{f}_1\left({x}_n\right)}{h}& \dots & {\lim}_{h\to 0};\frac{f_n\left({x}_1,\dots, {x}_n+h\right)-{f}_n\left({x}_n\right)}{h}\end{array}\right] $$

(2.51)

这里将∇xf∈ℝn×m矩阵与向量函数 f ( x )的一阶偏导数累加,称为雅可比矩阵。注意,包含矩阵的导数或其他高维函数的高维张量也称为雅可比矩阵;类似于张量,雅可比也是存储偏导数的张量的广义名称。我们可以将雅可比矩阵∇xf改写如下:

$$ {\mathbf{J}}_{\mathbf{f}}={\nabla}_{\mathbf{x}}\mathbf{f}=\left[\begin{array}{ccc}\frac{\partial {f}_1\left(\mathbf{x}\right)}{\partial {x}_1}& \dots & \frac{\partial {f}_n\left(\mathbf{x}\right)}{\partial {x}_1}\ {}\dots & \ddots & \dots \ {}\frac{\partial {f}_1\left(\mathbf{x}\right)}{\partial {x}_m}& \dots & \frac{\partial {f}_n\left(\mathbf{x}\right)}{\partial {x}_m}\end{array}\right] $$

(2.52)

这里, J f 是函数 f ( x )的雅可比∇ x f 的另一种表示方式。注意每一行都是多元函数fI(x)的梯度。正如我们前面所讨论的,雅可比矩阵可以被认为是多个多元函数梯度的累加,每个函数都堆积在不同的行中,从而得到一个偏导数矩阵。换句话说,多元函数的导数是一个函数行向量中只有一个函数时雅可比的特例。

接下来,我们介绍矩阵函数的微分和一个便于计算的简单技巧。

2.3.5 矩阵函数微分

这里,我们考虑一个简单的情况,我们计算输出矩阵变量y∈ℝn×o相对于输入向量变量 x ∈ ℝ m 其中f:ℝm→ℝn×这里我们设置 n = 3, o = 4, m = 2。这描述了一个从 m 维向量空间到 n × o 维矩阵空间的函数映射,即f:ℝ2→ℝ3×4。我们知道,存储输出矩阵相对于输入向量的偏导数的雅可比矩阵将是一个张量jf∈ℝ(3×4)×2。我们演示了两种方法来计算这个雅可比。

第一种方法,如图 2-8 所示,雅可比矩阵很简单。这里我们简单计算一下 Y 相对于向量 x 的每个标量输入变量的偏导数。这些偏导数分别是∂y/∂x1 和∂y/∂x2。现在我们把所有这些偏导数整理成一个形状为(3 × 4) × 2 的雅可比张量。

img/484421_1_En_2_Fig8_HTML.jpg

图 2-8

计算矩阵 Y ∈ ℝ 3×4 相对于向量 x ∈ ℝ 2 的梯度的简单方法

第二种方法如图 2-9 所示,雅可比矩阵的计算也很简单,但需要对之前的过程稍加修改。第一步,我们将输出矩阵展平成一个 -或 12 维向量,即$$ \overline {\mathbf{y}}\in {\mathrm{\mathbb{R}}}^{12} $$。在第二步中,我们计算这个新的输出向量 y 相对于输入向量 x 的偏导数,这导致两个偏导数∂y/∂x1 和∂y/∂x2,每个都属于ℝ 12 向量空间。我们将这些收集在一个形状为 no × m 或 12 × 2 的雅可比矩阵中。最后,在第三步中,我们将其重塑回 a(n×om—或(3 × 4) × 2 维雅可比张量,该张量现在包含输出矩阵 Y 相对于输入向量 x 的偏导数。

img/484421_1_En_2_Fig9_HTML.jpg

图 2-9

计算矩阵 Y ∈ ℝ 3×4 相对于向量 x ∈ ℝ 2 的梯度的另一种向量整形方法

在第四章中介绍的 Swift for TensorFlow 中的 TensorFlow 库采用了第二种方法来计算雅可比张量,这是因为第二种方法简单高效。这也使得在算法微分(第 3.3 节)中使用链式法则通过雅可比向量乘积(JVP)和向量雅可比乘积(VJP)分别在正向模式和反向模式算法微分中计算链式复合函数的偏导数变得容易。我们将在下一章描述这些术语。

请注意,第二种方法可能容易出错,因为张量的重新排序可能会破坏计算预期最终正确输出的路径,即这里的雅可比矩阵,但幸运的是它不会。为了理解这一点,我们回到线性代数来寻求一点帮助。这种方法之所以成为可能,是因为张量空间本质上是同构的,也就是说,任何空间都可以通过矩阵乘法等线性代数运算转换到另一个空间。这个变换可以再次被还原,把我们带到原始的张量空间。所以矩阵可以被改造成向量,反之亦然,同样的方法也适用于更高维的数据结构。

2.4 总结

数学对于理解深度神经网络至关重要。在这一章中,我们讨论了三个数学分支的各种主题,即线性代数、概率论和微分学。这些都有助于设计和训练机器学习模型,我们将在后面的章节中看到。如果你在试图理解神经网络或对它们编程时迷失在更高层次的抽象中,我们建议你使用这一章作为参考。

下一章是关于用 Swift 编程语言进行微分编程。我们将广泛使用微分概念来描述算法微分,并描述 Swift 的 API 来轻松计算函数和数据类型的导数。**

三、可微分编程

Swift 是 LLVM 编译器的语法糖。

—克里斯·拉特纳

在本章中,我们首先通过比较(第 3.1 节)Swift 语言与 Python 语言的强大功能,启发您采用 Swift 语言进行深度学习。我们还引入了一种称为“TensorFlow 的 Swift”的扩展语言(3.2 节),用于通用编程以及学习和研究深度学习领域。对自动计算复合函数的导数至关重要的算法(通过从用户那里抽象出复杂性)称为算法微分,将在 3.3 节中详细介绍和讨论。然后介绍 Swift 语言的各种基本和先进的强大功能(3.4 节)。由于当前 Python 语言大量用于数值计算,我们展示了 Swift for TensorFlow 如何轻松访问(第 3.5 节)Python 的内置和库。最后,我们以总结结束本章(第 3.6 节)。

本章从“差异化编程宣言”(Wei et al .,2018)中得到很大启发,该宣言为 Swift 提供一流的差异化功能支持奠定了基础,使其成为一种更加通用的编程语言。

3.1 Swift 无处不在

同时,数据科学研究团体使用的重要数字处理库(Buitinck 等人,2013;哈里斯等人,2020),机器学习(阿巴迪等人,2016;Chollet 等人,2015;Paszke et al .,2017,2019),量子计算(Aleksandrowicz et al .,2019),量子机器学习(Bergholm et al .,2018),计算神经科学(Taylor et al .,2018),其他都是用 Python 写的。虽然 Python 现在非常重要,但它不是一种好的通用编程语言(即,可用于软件开发栈的各个部分),它有许多严重的缺点,如性能慢、内存管理差、因为 GIL 而没有多线程支持、因为它是一种解释型语言而没有严肃的调试工具、高度动态的类型系统、没有对移动开发的真正支持等等。为了探索 Python 语言的替代方案,特别是机器学习,社区在不同的差异化编程研究领域对 Julia 语言进行了一些努力(Bezanson 等人,2012 年,2017 年)。但是 Julia 也有一些严重的缺点,例如不直观(如果你已经是 C++或 Python 程序员)、复杂且混乱的语法、膨胀的特性列表、怪异的变量范围、不安全且高度动态的类型系统(如 Python,允许同名变量的重新声明)、不支持移动开发等等。由于这些和其他缺点,Python 和 Julia 仍然不适合与数值计算有关的各种研究领域。

机器学习库通常用扩展的 C++语言编程,例如 CUDA (Zeller,2011),以允许在硬件加速器上执行。Python 只是与 C++代码库进行接口,使底层代码抽象,从而允许您使用简单的 Python 语法编写深度学习应用程序。因此,经过训练的机器学习模型必须加载到 C++中,以便在生产中进行低延迟部署。您的程序和低级 C++代码之间的这种差距是巨大的,它阻碍了您轻松地对特定于程序的优化代码库进行更改。Swift for TensorFlow 的目标是成为轻松编写低级优化程序所需的唯一语言,同时为您提供对高级 API 的访问,从而消除您的代码和实际机器学习代码库之间的差距。

我们知道 Swift 是一种静态类型的语言,但它也允许动态类型。例如,尽管协议(见第 3.4.6 小节)不是类型,而是一致性类型必须遵循的规则,但您仍然可以将协议用作实体的类型,该实体的实际自定义类型在运行时确定。但是 Python 没有类型的概念,除了完全动态的object类。这给机器学习应用程序带来了一个问题,因为知道实例的类型反而允许编译器报告编译时错误,这使得研究人员、从业人员和学习者同样高效。这就是 Swift for TensorFlow 大放异彩的地方!但是 Python 不会检查任何错误,直到在执行过程中遇到错误。由于机器学习训练是耗时的,并且在某一语句中遇到错误(该语句直到通过某些条件才被执行)会使程序崩溃,并且训练可能需要重新开始,这使得机器学习编程非常低效。

与 Python 不同,Swift 是一种基于 LLVM 编译器的通用、快速的编译语言,并且具有通过 LLDB 调试器进行调试的本机支持。Swift 还支持自动引用计数,您几乎永远不需要担心内存管理。Python 的 GIL 有效地阻止了设备多核的使用,因此不支持多线程。但是你可以用 Swift 轻松编写多线程程序。正如已经提到的,Python 不允许应用程序开发,而现在所有运行在苹果设备上的应用程序都是用 Swift 语言编写的。

Swift 是一种开源语言,不仅能在 macOS 上完美运行,还能在 Linux 和 Windows 操作系统上运行。Swift 用于为苹果设备构建应用。Swift 在前端和服务器后端也工作得很好,分别用 SwiftUI 2 构建用户界面(UI)和用 Vapor、 3 等库处理数据库。Swift 还可以用于处理亚马逊网络服务(AWS) Lambda 运行时。 4 现在 Swift 的编译器也支持一级可微编程,这使得编写参数优化算法变得很容易。可微分编程是一种编程范例,其中使用算法微分来计算类型和函数的导数。

几乎软件堆栈的任何部分都可以在 Swift 中轻松开发。在不到十年的时间里,Swift 已经迅速成为一种通用编程语言。现在我们将看看用于 TensorFlow 语言的 Swift,它扩展了原始的 Swift 语言,以便于编写深度学习算法。基本上,Swift for TensorFlow 引入了 TensorFlow 库和 Swift 中没有的各种深度学习特定功能。

3.2 Swift for TensorFlow

Swift for TensorFlow (S4TF)使机器学习项目的原型化变得很容易。虽然也有可能部署 S4TF 模型,但我们不会触及这个领域。由于 S4TF 具有新手和有经验的程序员友好的语法,学习曲线可以很短。S4TF 也可以作为第一语言学习编程,甚至机器学习。而且 S4TF 有很多 Python 用户会觉得有趣的强大特性。

除了在 CPU 上运行深度学习程序,S4TF 还可以在 GPU 或 TPUs 等硬件加速器上执行。在撰写本文时,S4TF 在自己的 X10 库中为 TensorFlow 使用了 XLA 编译器。这允许你在硬件加速器上执行你的算法,如 GPU 或 TPU,只需对你的代码做很小的修改,我们将在后面的章节中训练我们的机器学习算法时看到。但在未来,TensorFlow 将与 MLIR 一起构建(Lattner and Pienaar,2019;Lattner 等人,2020)编译器,其中 MLIR 代表“机器学习中间表示”MLIR 将成为机器学习库的标准。MLIR 将允许机器学习代码的设备无关的执行,也就是说,任何新的硬件加速器都将很容易得到支持。

S4TF 不仅在 Xcode 集成开发环境(IDE)中运行良好,而且在 macOS、Linux 和 Windows 操作系统的浏览器中的 Google Colaboratory 和本地 Jupyter Notebook 上也运行良好。但在 Xcode 中构建应用程序是有益的,因为它允许智能的上下文感知代码完成、高度受控的调试、断点、创建和执行测试、甚至在源代码编译之前修复程序警告和错误等等。它甚至允许您实时查看磁盘读写数据传输统计数据、发送或接收的网络数据,以及最重要的内存消耗。每年在苹果全球开发者大会(WWDC)期间,Xcode 都会不断增加新功能,以提高开发者的工作效率。这些用于开发惊人的苹果平台应用程序的 Xcode 功能也有助于开发更好的机器学习应用程序。

我原本计划用 Xcode 编写本书中呈现的所有深度学习程序,但已经在深度学习领域的许多用户在 Linux 上使用 Python,所以他们可能不会使用 macOS。为了方便他们,我使用了谷歌合作平台来构建和训练书中介绍的所有深度学习模型。因此,您可以简单地启动您最喜欢的浏览器,并立即开始尝试代码示例。你们中拥有 MAC 的人可能会在 Xcode 中执行相同的代码。

在 S4TF 中,您可以简单地导入预构建甚至安装的 Python 库,并像在 Python 程序中一样使用它们。这被称为 Python 互操作性(详细讨论见 3.5 节)。由于 Swift 的语法非常类似于 Python,调用这些函数是无缝的,感觉就像你在用 Python 编写一样。一个有趣的事实:你可以导入 Python 的任何深度学习库(例如 PyTorch)并在 S4TF 中定义和训练机器学习模型!您还可以导入 Python 的可视化库用于绘图目的。Python 互操作性有助于使用研究人员和从业者非常喜爱的 Python 库,同时仍然使用 S4TF 的其他强大功能。

在未来,iOS、iPadOS、macOS、watchOS 和 tvOS 开发人员将能够完全使用 S4TF 在纯 Swift 中编写机器学习应用程序,而不需要苹果提供的任何高级库,如 Core ML、自然语言等等。这样,你将能够灵活地编程(通过使用新的优化器、损失函数等来定义和训练新的神经层。)并在设备上分发部署了最近研究的机器学习算法的应用程序。这些设备上高度定制的模型还将维护您用户的数据隐私。希望有一天,你也能够在神经引擎上运行你的模型,以便在设备上快速处理数据。有趣的是,您已经可以使用当前的 S4TF 工具链在 Xcode 中构建 macOS 应用程序。书上写的程序都用的是 S4TF 0.11 版。

下一节将详细描述计算神经网络导数的技术。本节并非理解 Swift(3.4 节)的依赖项,但我们强调您至少要阅读一次,以便您可以在机器学习之外的其他领域高效地使用这项技术。

3.3 算法差异

这里我们讨论算法微分 (AD),也叫自动微分,自动计算一个函数的导数。首先,我们讨论计算导数的各种编程方法(3.3.1 小节)。然后我们描述 AD 的两种模式(3.3.2 小节)。最后,我们展示 AD 是如何实现的(3.3.3 小节)。

注意,AD 不是专门用于机器学习,而是已经应用于各种领域,例如计算流体动力学(比朔夫等人,2007;穆勒和库斯丁,2005;托马斯等人,2010)、最优控制(瓦尔特,2007)、工程设计优化(卡萨诺瓦等人,2000;福斯和埃文斯,2002 年)。(Baydin 等人,2017 年)用更长的篇幅回顾广告。

方案编制方法

确定函数的导数主要有四种方法,即手动求导、数值求导、符号求导和算法求导。我们简单看一下前三种区分技术。由于 Swift 使用算法区分技术实现了区分功能,我们对此进行了更深入的技术探讨。理解算法差异并不是使用差异 API 的先决条件,但是了解它可能会帮助你编写更好的差异程序。

3.3.1.1 手动、数字和符号微分

手动区分是一种非常简单但耗时的程序区分方法。过去,机器学习研究人员使用它来寻找损失函数的导数,并将其插入到优化算法中,如 L-BFGS(朱等,1994 年)或随机梯度下降(SGD)(博图,1998 年)。在这里,人们在纸笔上写下一个函数的导数表达式,然后在计算机上编写该导数函数的程序。

数值微分与导数的有限差分近似有关,在第 2.3 节详细讨论。虽然它很容易实现,但其简单性带来了一些缺点,如不准确的近似,因为值可能会由于机器精度限制(Jerrell,1997 年)而被四舍五入,并采用 O ( m )计算多元函数 f 的梯度$$ {\nabla}_{\mathbf{x}}f={\left[\frac{\partial f}{\partial {x}_1}\dots \frac{\partial f}{\partial {x}_m}\right]}^T $$:ℝm→ℝ其中 x ∈ ℝ * m * 是输入向量。梯度、雅可比和海森的评估时间很重要,因为深度学习算法广泛使用向量和矩阵微分。

符号微分是计算导数的另一种方法。在符号微分中,程序操纵函数表达式以获得其导数表达式(Grabmeier 和 Kaltofen,2003)。然后,我们可以简单地用新导出的导数表达式来计算函数在某一点的导数。函数表达式到其导数表达式的转换是通过使用 2.3.2 和 2.3.3 小节中讨论的微分规则来实现的。

虽然符号微分消除了手动微分和数字微分的局限性和弱点,但导数表达式存在“表达式膨胀”的问题(Corliss,1988),这使得它们阅读和理解起来复杂而晦涩。此外,手动和符号微分阻碍了可微分程序的表达能力,因为它们不允许可微分函数包含控制流语句。为了克服这些障碍,我们求助于算法微分技术。

3.3.1.2 算法微分

算法微分是基于这样一个事实,即任何数值计算都是由一组导数已知的有限基本运算组成的(维尔马,2000;Griewank 和 Walther,2008 年)。通过应用微分的链式法则,可以计算这些函数的导数,从而获得整个表达式的导数。这些基本运算包括二元和一元算术运算以及超越函数,如对数、指数和三角函数。

AD 甚至可以区分控制流,如条件分支、循环、控制转移语句和递归。这是可能的,因为最终,任何数值程序(甚至包括控制转移语句)在执行时,将总是产生具有输入、中间和输出值的数值执行轨迹,这是使用微分链规则计算导数所需的唯一东西。

3.3.2 累积模式

AD 可以通过两种方式实现,即正向模式和反向模式。您编写原始程序,该程序在执行时会产生一个执行跟踪(也称为正向原始/执行跟踪)。然后 AD 的正向和反向模式分别使用它生成一个相应的正向切线和一个反向伴随轨迹,这代表了数值程序的导数对应部分。生成的轨迹接受输入值并返回导数函数的输出。请注意,您使用 AD 的任何一种模式来计算数值程序的导数,而不是同时使用两种模式。

我们考虑一个多元复合函数 f ( x 1x2)= sin(x1)+log2(x2),其计算图形如图 3-1 所示,我们的目标是计算下文解释了这两种方法。

img/484421_1_En_3_Fig1_HTML.png

图 3-1

多元函数 f(x 1 ,x2)= sin(x1)+log2(x2)的计算图

由于 AD 要求初等函数应该是可微的,因此有导数,我们从图 3-1 所示的计算图中列出所有函数开始:

$$ {\displaystyle \begin{array}{l}a=\sin \left({x}_1\right)\ {}b={\log}_2\left({x}_2\right)\ {}c=a+b\end{array}} $$

现在我们列出它们的导数如下:

$$ {\displaystyle \begin{array}{l}\frac{\mathrm{d}a}{\mathrm{d}{x}_1}=\cos \left({x}_1\right)\times \frac{\mathrm{d}{x}_1}{\mathrm{d}{x}_1}=\cos \left({x}_1\right)\ {}\frac{\mathrm{d}b}{\mathrm{d}{x}_2}=\frac{1}{x_2}\times \frac{\mathrm{d}{x}_2}{\mathrm{d}{x}_2}=\frac{1}{x_2}\ {}\frac{\mathrm{d}c}{\mathrm{d}a}=\frac{\mathrm{d}a}{\mathrm{d}a}+\frac{\mathrm{d}b}{\mathrm{d}a}=1+0=1\end{array}} $$

$$ \frac{\mathrm{d}c}{\mathrm{d}b}=\frac{\mathrm{d}a}{\mathrm{d}b}+\frac{\mathrm{d}b}{\mathrm{d}b}=0+1=1 $$

img/484421_1_En_3_Fig2_HTML.png

图 3-2

任意向量复合函数 g ∘ f(x)的计算图。

现在,我们将使用链式法则中的这些导数来寻找复合函数f(x1,x2)相对于前向和反向模式 ADs 中的每个输入变量的偏导数,如下所述。我们演示寻找偏导数∂c/∂x2 的正向模式以及∂c/∂x1 和∂c/∂x2 的反向模式。对于正向模式,我们用撇号表示每个变量的偏导数,例如a′=∂a/∂x2(也叫正切)。对于反向模式,我们用一个横杠表示每个输出变量 c 的偏导数,例如$$ \overline {\mathrm{a}}=\partial c/\partial a $$(也叫伴随)。

3.3.2.1 前进模式

在正向模式中,我们执行正向原始(数值程序)和正向切线追踪(数值程序的衍生物)。我们将输入值( x 1x 2 )传递给函数 f ( x 1x 2 )并开始跟踪正向原始程序,存储中间值 abc 。通过相应的原始追踪步骤,我们也追踪相应变量的正切值。在开始追踪之前,我们将特定输入变量相对于自身的导数设置为 1,将其他变量相对于该变量的导数设置为 0。

img/484421_1_En_3_Fig3_HTML.png

图 3-3

任意向量复合函数 g ∘ f (x)的前向模式算法微分计算图。

表 3-1

前向模式算法微分的计算轨迹。

|

正向原始迹线

|

正向切线迹线

|
| --- | --- |
| $$ {\displaystyle \begin{array}{l}{x}_1\kern3.479999em =3\pi /2\ {}{x}_2\kern3.479999em =4\ {}a\kern0.6em =\sin \left({x}_1\right)\kern1.08em =-1\ {}b\kern0.6em ={\log}_2\left({x}_2\right)\kern0.72em =2\ {}c\kern0.6em =a+b\kern1.68em =1\end{array}} $$ | $$ {\displaystyle \begin{array}{l}{x}_1^{\hbox{'}}\kern4.439998em =0\ {}{x}_2^{\hbox{'}}\kern4.439998em =1\ {}{a}^{\hbox{'}}\kern0.6em =\cos \left({x}_1\right)\ast {x}_1^{\hbox{'}}\kern0.72em =0\ {}{b}^{\hbox{'}}\kern0.6em =\left(1/{x}_2\right)\ast {x}_2^{\hbox{'}}\kern0.84em =1/4\ {}{c}^{\hbox{'}}\kern0.6em ={a}{\hbox{'}}+{b}{\hbox{'}}\kern1.92em =0.25\end{array}} $$ |

请注意,链规则中使用了初等函数的现有导数,从输入变量开始,计算输出变量相对于输入变量的偏导数。这里,我们得到∂c/∂x2=c′= 0.25。

前向模式广告中的“前向”一词来源于这样一个事实,即我们依次计算输入变量、中间变量、然后输出变量相对于输入变量的切线。前向模式 AD 也称为前推 AD ,因为我们使用链式法则将输入变量的导数推向输出变量。

现在考虑两个任意向量函数f:ℝm→ℝng:ℝn→ℝo和输入向量变量x∈ℝm(我们将其表示为 a 这里我们感兴趣的是复合函数 gf ,如图 3-2 所示。因为这些函数由初等函数组成,类似于前面的多元函数的例子,我们已经将它们的偏导数存储在它们各自的雅可比矩阵中,即jf∈ℝm×njg∈ℝn×o现在,我们执行正向原始追踪,如下所示:

$$ \mathbf{a}=\mathbf{f}\left(\mathbf{x}\right) $$

$$ \mathbf{b}=\mathbf{g}\left(\mathbf{a}\right) $$

img/484421_1_En_3_Fig4_HTML.png

图 3-4

任意向量复合函数 g ∘ f (x)的逆向算法微分计算图。

这里我们有中间变量 a ∈ ℝ 1×n 和输出变量 b ∈ ℝ 1 ×o 。我们将设置 d x /d x ,这是一个 1 × m 矩阵,表示为x’,只有一个元素设置为等于 1。我们现在可以执行正向切线追踪(如图 3-3 所示)如下:

$$ {\displaystyle \begin{array}{l}{\boldsymbol{a}}{\hbox{'}}={\left({\mathbf{J}}_{\mathbf{f}}T\cdot {\mathbf{x}}{\hbox{'}T}\right)}T\ {}{\boldsymbol{b}}{\hbox{'}}={\left({\mathbf{J}}_{\mathbf{g}}T\cdot {\mathbf{a}}{\hbox{'}T}\right)}T\end{array}} $$

这里,我们有中间变量的导数a′∈ℝn和输出变量的导数b′∈ℝo,它们与它们的前向原始追踪变量对应项具有相同的维数。因为 we 矩阵相乘,用算子、雅可比和向量(表示为矩阵)表示,前向模式 AD 可以表示为雅可比向量乘积 (JVP)的链式应用。

当我们在点a′,即$$ {\mathbf{J}}_{\mathbf{f}}\left|{}_{\mathbf{x}={\mathbf{a}}^{\hbox{'}}}\right. $$处对Jf求值时,其中a′中只有一个变量等于 1,而其余的都是零,这需要 n 个计算步骤来使用前向模式方法找到所有输出变量相对于单个输入变量的偏导数;因此,其复杂程度的顺序是 𝒪 ( n )。在特殊情况下,当我们有 m = 1 并且 n ≥ 1 时,雅可比矩阵可以一步计算出来。但是当nm 时,我们使用另一种技术进行快速计算,如下所述。

3.3.2.2 反向模式

在反向模式中,我们执行正向原始(数值程序)和反向伴随追踪(数值程序的衍生物)。与正向模式不同,在正向模式中,我们并排计算原始和切线跟踪,反向模式 AD 是一个两步过程。这里,我们首先执行正向原始追踪,然后在第二步中执行反向伴随追踪。第一步,也称为正向传递,我们将输入值( x 1x 2 )传递给函数f(x1x 2 )并开始跟踪正向原始程序,存储中间值 a第二步,也称为向后传递,我们从输出变量开始向输入变量追踪变量的伴随值。在开始跟踪之前,我们将标量输出变量相对于自身的导数设置为 1。

表 3-2

反向模式算法微分的计算轨迹

|

正向原始迹线

|

反向伴随迹

|
| --- | --- |
| $$ {\displaystyle \begin{array}{l}{x}_1=3\pi /2\ {}{x}_2=4\ {}a=\sin \left({x}_1\right)=-1\ {}b={\log}_2\left({x}_2\right)=2\ {}c=a+b=1\end{array}} $$ | $$ {\displaystyle \begin{array}{l}\overline{c}=1\ {}\overline{b}=1\ {}\overline{a}=1\ {}{\overline{x}}_2=\overline{b}\cdot \mathrm{d}{x}_2/\mathrm{d}{x}_2=1.1/4=0.25\ {}{\overline{x}}_1=\overline{a}\cdot \mathrm{d}{x}_1/\mathrm{d}{x}_1=1\end{array}} $$ |

在这里,我们得到$$ \partial c/\partial {x}_1=\overline {x_1}=1 $$$$ \partial c/\partial {x}_2=\overline {x_2}=0.25 $$

反向模式 AD 中的“反向”一词来源于我们依次计算输出变量相对于输出变量、中间变量、然后是输入变量的偏导数。反向模式 AD 也称为拉回 AD ,因为我们使用链式法则将输出变量的导数拉向输入变量。

从前面的前向模式 AD 讨论中,我们考虑关于向量函数 fg 及其雅可比矩阵 J fJ g 的相同假设。然后我们执行正向原始追踪(如图 3-4 所示),如下所示:

$$ \mathbf{a}=\mathbf{f}\left(\mathbf{x}\right) $$

$$ \mathbf{b}=\mathbf{g}\left(\mathbf{a}\right) $$

这里,我们又有了中间变量 a ∈ ℝ n 和输出变量 b ∈ ℝ o 。我们将设置 d b /d b ,它是一个 1 × o 矩阵,表示为$$ \overline{\mathbf{b}} $$,只有一个元素设置为等于 1。我们现在可以如下执行反向伴随追踪:

$$ {\displaystyle \begin{array}{l}\overline{\mathbf{a}}=\overline{\mathbf{b}}\cdot {\mathbf{J}}_{\mathbf{g}}^{\boldsymbol{T}}\ {}\overline{\mathbf{x}}=\overline{\mathbf{a}}\cdot {\mathbf{J}}_{\mathbf{f}}^{\boldsymbol{T}}\end{array}} $$

这里,我们有关于中间变量$$ \overline{\mathbf{a}}\in {\mathrm{\mathbb{R}}}^{1\times n} $$和输入变量$$ \overline{\mathbf{x}}\in {\mathrm{\mathbb{R}}}^{1\times m} $$的输出导数,它们与它们的前向原始追踪变量对应变量具有相同的维数。因为我们将向量和雅可比矩阵相乘,所以反向模式 AD 可以表示为向量-雅可比乘积 (VJP)的链式应用。

当我们在点$$ \overline{\mathbf{b}} $$评估 J ** g ** 时,即$$ {\mathbf{J}}_{\mathbf{g}}\left|{}_{\mathbf{b}=\overline{\mathbf{b}}}\right. $$处,其中$$ \overline{\mathbf{b}} $$中只有一个变量等于 1,其余的都是零,这需要 m 个计算步骤,以使用逆向模式方法找到所有输出变量相对于所有中间和输入变量的偏导数;所以它的复杂程度顺序是 𝒪 ( m )。在特殊情况下,当我们有 m = 1 并且 n ≥ 1 时,雅可比矩阵可以一步计算出来。

当我们在下一章研究神经网络时,我们将看到神经网络的梯度计算总是这种特殊情况,因为损失函数发出标量值。这就是为什么今天的深度学习库只实现反向模式 AD 的主要原因(尽管 Swift 实现了正向和反向模式,这使它成为一种更通用的语言,可以应用于除深度学习之外的其他各种领域),因为输出标量相对于所有中间和输入变量的梯度可以在一次反向传递中计算出来。在本书中,我们还关注使用 Swift 的反向模式 AD 功能(第 3.4.9 小节)。

3.3.3 实施方法

这里我们讨论两种实现 AD 的方法,即操作符重载(OO)和源代码转换(SCT)。

3.3.3.1 运算符重载

通过为数字自定义类型定义高级操作符(参见 3.4.8 小节),也称为操作符重载(OO),可以很容易地实现算法差异。一些 Python 库如 autograded(Maclaurin 等人,2015;Maclaurin,2016)、5py torch(Paszke et Al .,2017,2019)、6python IC tensor flow(Abadi et Al .,2016)、7Keras(Chollet et Al .,2015)、8Theano(Al-Rfou et Al .,2016)、这些被称为嵌入式(他们的宿主语言是 Python) 特定领域语言(DSL),因为它们解决了特定领域的问题(这里是深度学习),并且作为库分发。

清单 3-1 中的代码给出了一个简单的例子(改编自(Wei et al .,2018))使用双数实现 Swift 前向模式算法微分的运算符重载,用于计算给定表达式的导数。

struct DualNumber<T: FloatingPoint> {
  var value: T
  var derivative: T
}

extension DualNumber {
  /// Applies sum rule
  static func + (left: Self, right: Self) -> Self {
    DualNumber(
      value: left.value + right.value,
      derivative: left.derivative + right.derivative)
  }
  /// Applies sum rule
  static func - (left: Self, right: Self) -> Self {
    DualNumber(
      value: left.value - right.value,
      derivative: left.derivative - right.derivative)
  }
  /// Applies product rule
  static func * (left: Self, right: Self) -> Self {
    DualNumber(
      value: left.value * right.value,
      derivative: left.derivative * right.value +
                  left.value * right.derivative)
  }
  /// Applies quotient rule
  static func / (left: Self, right: Self) -> Self {
    DualNumber(
      value: left.value / right.value,
      derivative: (left.derivative * right.value -
                   left.value * right.derivative) /
                   (right.value * right.value))
  }
}

let x = DualNumber(value: 3, derivative: 1)
let expression = x*x*x + x
print("expression value: \(expression.value)")
print("expression derivative: \(expression.derivative)")

Listing 3-1Demonstrate operator-overloaded forward-mode algorithmic differentiation using dual numbers

输出

expression value: 30.0
expression derivative: 28.0

我们声明了一个包含两个存储属性的DualNumber结构,这两个属性是符合FloatingPoint协议的T类型的valuederivative,。然后DualNumber扩展到包括各种高级运算符,如加、减、乘、除。每个函数都返回一个包含valuederivativeDualNumber实例。这里,加法和减法、乘法和除法分别使用和、积和商的微分规则来初始化要返回的DualNumber实例的derivative属性。

然后我们用 3 的value和 1 的derivative初始化DualNumber文字x。这里设置derivative等于 1 意味着x相对于自身的导数等于 1。我们声明了计算x3+xexpression文字,其相对于x的导数为 3 x 2 + 1。当我们访问derivative属性时,我们得到了value 3 * 3 2 + 1 = 28。

注意,任何 DSL 的局限性在于,它受限于只能实现 AD 的一种模式,即正向或反向模式。但是编译器级别的代码转换,如 Swift 语言,让我们实现这两者。

3.3.3.2 源代码转换

实现 AD 的一个更好的方法是执行程序的编译器级源代码转换(SCT ),这允许在语言本身中实现正向和反向模式!程序员可以简单地调用 API 来区分使用正向或反向模式的函数。SCT 在编译过程中为在编译器级别执行优化提供了更多的控制,以实现快速区分。

一个 SCT 优化示例如下。在数值执行跟踪期间,考虑一个条件语句(在每个条件传递中包含不同的数值计算)。在正向正切或反向伴随跟踪期间,DSL 将需要跟踪这些条件中的每一个,以形成计算图来优化程序执行性能。如果有 c 条条件语句,那么就需要运行跟踪 c 次;现在,如果您还考虑控制转移语句,那么将需要更多的跟踪。相比之下,编译器级 SCT 可以考虑 Swift 的每个特性,并在编译过程中优化所有可能的情况。这将只需要跟踪一次来累积变量值,并且偏导数将被快速计算。

SCT 正是 Swift 解决上述技术缺点的方法。尽管讨论 SCT 超出了本书的范围,但我们确实讨论了 Swift 中可用的各种差异化 API(第 3.4.9 小节)。但在此之前,让我们在接下来的章节中熟悉一下 Swift 语言本身。

3.4 Swift 语言

在本节中,我们将通过 Swift 语言的足够多的功能来理解书中的机器学习程序。熟悉 Python 或 C++的有经验的程序员会发现 Swift 语法很容易掌握。对于初级程序员来说,Swift 代码感觉像是在读英文句子,因此代码语句背后的意图变得直观易懂。除了简单的语法之外,Swift 还是一种快速、跨平台、编译和类型安全的语言,具有诸如泛型、回溯建模、协议、可选等现代特性。涵盖 Swift 的所有特性超出了本书的范围。苹果在线提供的神奇 Swift 图书 10 将对对 Swift 其他功能感兴趣的读者很有帮助。

我们从简单的值开始(第 3.4.1 小节),然后看看如何在 Swift 中使用各种集合类型(第 3.4.2 小节)。3.4.3 小节介绍了控制流语句。第 3.4.4 节讨论了 Swift 中的三种功能形式。然后我们引入自定义类型(3.4.5 小节)。第 3.4.6 小节讨论了 Swift 的一些强大而现代的功能。第 3.4.7 小节展示了如何轻松处理 Swift 中的错误。3.4.8 小节介绍了类型的自定义运算符。最后,我们在第 3.4.9 小节中介绍了 Swift 的算法差异化功能,以此结束本节。

价值观

在 Swift 中,您分别使用关键字varlet将实例声明为变量或常量。您可以在程序执行期间更改变量实例的值,而常量实例的值不能更改。

遵循 Swift 约定,您应该用小写字母 camel 声明一个实例。

var firstNumber = 10
let pi = 3.14159
var secondNumber: Float = 123.456

Listing 3-2Declare instances containing simple values

这里,firstNumber被推断为类型Int。前面代码中声明的常量pi被推断为Double类型,因为它包含十进制值。在某些情况下,您可能需要声明一个浮点数。在这种情况下,只需提供Float作为变量的类型信息。您还可以使用type(of:)功能验证每个实例的类型。

print("firstNumber: \(type(of: firstNumber))")
print("pi: \(type(of: pi))")
print("secondNumber: \(type(of: secondNumber))")

Listing 3-3Check the type of instance

输出

firstNumber: Int
pi: Double
secondNumber: Float

类型信息是从分配给实例的值中自动推断出来的。在其他情况下,您可以通过编写类型名称,后跟由冒号分隔的实例来提供类型信息。

这是一个很好的例子,表明在 Swift 中阅读代码语句就像阅读英语句子一样。Swift 简单的语法让新手学习编程变得很容易。

在某些情况下,实例可能不包含值。为了表示实例的这种状态,Swift 提供了可选值,这些值要么包含一个值,要么不包含任何由nil表示的值。若要定义可选的,请在声明期间提供类型信息,后跟一个问号(?)不带任何空格。

var optionalInt: Int? = 5
if let value = optionalInt {
  print("optionalInt has value: \(value)")
}

Listing 3-4Declare an optional-type instance

输出

optionalInt has value: 5

这里,optionalInt是值为 5 的类型Int?。它本来可以包含nil,因为它是可选的。然后,if let语句提取并绑定value常量中的值,供if语句的代码块使用。这叫做可选绑定

您还可以定义 computed 属性,该属性计算值而不是存储值。计算的属性必须始终声明为变量实例。参见清单 3-24 中的示例。

3.4.2 集合

您可以在 Swift 中声明三种集合,即数组、集合和字典。集合是一种通用结构,在无法推断类型信息的情况下,您必须为其提供类型信息。每个集合类型都符合Sequence协议,该协议允许您迭代它的元素。

3.4.2.1 阵列

数组是元素的有序集合。你访问一个元素,它的下标代表它在数组中的位置。在 Swift 中,数组是零索引的,也就是说,数组的第一个元素的位置是零,第二个元素的位置是一,依此类推。

let deepLearningPioneers = ["Geoffrey Hinton", "Yoshua Bengio",
"Yann LeCun", "Jürgen Schmidhuber"]
for name in deepLearningPioneers {
  print(name)
}

Listing 3-5Iterate over an array instance

输出

Geoffrey Hinton
Yoshua Bengio
Yann LeCun
Jürgen Schmidhuber

还可以用enumerated()实例方法访问每个元素的索引位置。清单 3-6 中的代码对数组进行排序和枚举。

for (index, name) in deepLearningPioneers.sorted().enumerated() {
  print("\(index). \(name)")
}

Listing 3-6Access each element with its index in a sorted array

输出

0\. Geoffrey Hinton
1\. Jürgen Schmidhuber
2\. Yann LeCun
3\. Yoshua Bengio

3.4.2.2 集

集合是唯一元素的无序集合。在声明中,指定实例属于Set类型。您可以执行数学集合运算。集合也有一些类似于标准库中数组的方法。

let oddNumbers: Set<Int> = [1, 3, 5, 7, 9]
let evenNumbers: Set = [0, 2, 4, 6, 8]
let wholeNumbers = oddNumbers.union(evenNumbers).sorted()
print("wholeNumbers: \(wholeNumbers)")
print("Even and odd numbers are subset of whole numbers: \(evenNumbers.union(oddNumbers).isSubset(of: wholeNumbers) ? "" : "")")

Listing 3-7Set operations on even, odd, and whole numbers

输出

偶数和奇数是整数的子集

您可以定义一个集合,该集合的Set类型后跟一个尖括号中的类型信息,例如Set<Int>,因为Set是一个通用结构类型。但是如果信息可用,您可以省略类型信息。尽管集合是无序的,但是您可以使用sorted()实例方法对它们进行排序。注意这里三元运算符的用法写为condition ? result1 : result2。如果条件为真,则返回result1,否则返回result2。在这里,偶数和奇数的并集是整数的子集,这是一个 true 语句,因此返回"``"

3.4.2.3 词典

字典是无序元素的集合,其中每个元素都是一个键和值对。通过在实例中使用键作为下标来访问值。

let wheelsCount = ["unicycle": 1, "bicycle": 2, "rickshaw": 3, "car": 4]
print("A rickshaw has \(wheelsCount["rickshaw"]!) wheel(s).")

Listing 3-8Define a dictionary and access its value

输出

A rickshaw has 3 wheel(s).

注意,我们在访问人力车的轮数时使用感叹号。返回一个可选值,因为字典中可能不存在某个键。使用感叹号来访问可选值被称为强制展开。请注意,这可能会失败,所以在您不确定字典中是否存在键的情况下,请尝试使用可选绑定,如下所示。

if let unicycle = wheelsCount["unicycle"] {
  print("A unicycle has \(unicycle) wheel(s).")
}

Listing 3-9Safely access a dictionary value with optional binding

输出

A unicycle has 1 wheel(s).

前面代码中使用的if条件语句是下面讨论的许多其他控制流的一部分。

控制流程

Swift 提供各种控制流语句。您可以使用三种循环来遍历一系列语句。条件语句允许您根据条件的真实性执行特定的语句。还可以使用控制转移语句将执行控制转移给某些语句。

3.4.3.1 环线

Swift 有三种循环,分别是for-in循环、while循环、repeat-while循环。每一个都有其重要性,如下所述。

for-in循环允许您迭代地访问集合实例中的每个元素。您还可以在迭代期间通过调用集合上的enumerated()实例方法来访问每个元素的索引。

for value in 1...5 {
  print("5 x \(value) = \(5 * value)")
}

Listing 3-10Declare a for-in loop to iterate over a range of values

输出

5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25

这个例子简单地在从 1 到 5 的范围内迭代。您可以使用1..<5语法排除范围中的最后一个数字(5)。

let fruits = ["Apple", "Banana", "Orange", "Watermelon"]
let colors = ["red", "yellow", "orange", "green"]
for (fruit, color) in zip(fruits, colors) {
  print("\(fruit) has \(color) color.")
}

Listing 3-11Access corresponding elements of two arrays simultaneously

输出

Apple has red color.
Banana has yellow color.
Orange has orange color.
Watermelon has green color.

zip(_:_:)函数与符合Sequence的两个实例一起使用,可以在相应的索引位置访问每个实例中的元素。这里,"Apple""red"分别处于fruitscolors的零位。运行for-in循环在第一次迭代中访问这两个文件,并打印“苹果有红色。”类似地,为每个对应的元素对打印其他句子。

for-in循环适用于在一个范围或Sequence的一致性实例上迭代的情况。但是当迭代次数未知时,while 循环会很有帮助。While 循环只是在条件为真时运行一段代码,否则终止。while 循环有两种,即while循环和repeat-while循环。

while循环在开始时检查条件,并重复一组语句,直到条件变为假。相反,除了不需要条件检查的第一遍以外,当所有遍的条件都为真时,repeat-while循环执行一组语句。

var a = 5
while a > 0 {
  print(a, terminator: " ")
  a -= 1
}

Listing 3-12Demonstrate the while loop

输出

5 4 3 2 1

这里,while循环在开始时检查a的值是否大于零。如果该条件为真,则执行花括号中的语句。但是当a的值变得等于或小于零时,循环终止,将控制返回到右花括号后的代码,如果有的话。

var b = 0
repeat {
  print(b, terminator: " ")
  b += 1
  if b > 6 { break }
} while b > 0

Listing 3-13Demonstrate the repeat-while loop

输出

1 2 3 4 5

在前面的repeat-while循环中,花括号之间的一组语句在第一次循环中执行。它首先用终止字符" "而不是"\n"(回车符,将光标移动到换行符)打印出b的值,然后通过给b加 1 来更新它的值。最后,如果b的值超过 6,它就中断循环。这是循环终止条件。运行这些语句一次后,检查条件b是否大于零,以再次执行循环。注意while循环可能无限执行一系列语句,您必须提供一个终止条件,就像我们在前面的代码中提供的那样(循环内部)。

3.4.3.2 条件语句

Swift 支持两种条件语句,即ifswitch

if语句中,如果条件为真,则执行一段代码。此外,如果您的if条件为假,您可以定义一个要执行的else代码块。如果您想检查一个以上的成功条件,您还可以在ifelse代码块之间提供一个else if语句,因为它不能在一个if语句中用逗号分隔。

let marks = 75
if marks < 60 {
  print("Poor")
} else if marks >= 60 && marks <= 80 {
  print("Average")
} else {
  print("Excellent")
}

Listing 3-14Demonstrate an if-else statement

输出

Average

这个例子展示了如何将if条件语句与elseelse if语句结合起来,对代码执行进行细粒度控制。因为marks在 60 和 80 的范围内,所以这段代码打印“平均值”

另一个有用的条件语句是switchswitch语句用于比较单个实例和多个可能的值。

switch marks {
case let x where x < 60:
  print("Poor")
case 60...80:
  print("Average")
  fallthrough
default:
  print("Excellent")
}

Listing 3-15Demonstrate the switch statement

输出

Average
Excellent

这里,switch语句将单个变量与许多可能的情况进行比较。注意如何使用letvar关键字定义一个局部变量或常量,并在那里比较它的值。您还可以检查位于特定区间的值。在switch语句中,default充当if语句中的else块。如果你的switch陈述不够详尽,就必须包括在内。当任何第一次遇到的情况被匹配并且其代码被执行时,程序的控制被转移到switch语句的右花括号之后的代码行。但是如果您想执行后续的 case 代码,而不管它是否与值匹配,您可以使用fallthrough控制转移语句。如果这个例子中没有包含fallthrough,那么它将只打印“Average”fallthrough语句模仿 C 和 C++语言中switch条件语句的行为。

3.4.3.3 控制转移报表

Swift 提供了五种控制转移语句,即continuebreakreturnfallthroughthrow。下面的错误处理中讨论了throw语句。和fallthrough已经在前面的文本中讨论过。return语句与guard语句一样用于函数中。您也已经看到了清单 3-13 中的break语句的例子。

这里,我们重点关注循环中使用的continue控制转移语句。

let totalSteps = 50
let range = stride(from: 0, to: totalSteps, by: 10)
print("range: \(Array(range))")
for step in range {
  if step == 30 { continue }
  print("step: \(step)")
}

Listing 3-16Use a continue control transfer statement in a for-in loop

输出

range: [0, 10, 20, 30, 40]
step: 0
step: 10
step: 20
step: 40

在这段代码中,stride(from:to:by:)函数产生一系列值,从 0 到 40,间隔 10,不包括最后一个值,即 50。通过转换为数组来打印范围,显示它所描述的值。要包含最后一个值,在这个函数中用through替换to参数。然后for - in循环遍历所有的值,打印每个值,但不是 30。这是通过使用一个continue语句完成的,该语句将执行控制转移到下一次迭代的for - in循环的第一条语句,跳过所有后续语句。

3.4.3.4 提前退出

guard语句中,guard关键字后面的代码如果是条件就必须为真,或者如果是可选的就必须包含一个值,以便执行guard语句后面的代码。在可选绑定的情况下,实例在出现guard语句的代码中是可用的,以供进一步使用。但是如果失败,那么执行else代码块中的代码。这个else块还必须包含一些控制转移语句,将控制转移到编写guard语句的代码块之外。

func welcome(language: [String: String]) {
  guard let name = language["name"] else {
    print("Welcome!")
    return
  }
  print("Welcome \(name)!")
}
welcome(language: [:])
welcome(language: ["name": "Swift"])

Listing 3-17Demonstrate the usage of the guard statement

输出

Welcome!
Welcome Swift!

这里,welcome(language:)函数以一个字典作为参数。然后,guard语句试图从字典中提取"name"键的值。如果字典中没有"name"键,可选绑定可能会失败。在这种情况下,else代码块被执行,它简单地向控制台打印一条"Welcome!"消息。但是当该值存在时,它被存储在一个名称常量中,该常量用于打印更精细的消息。这个例子使用了一个函数特性,这将在下面详细讨论。

3.4.4 关闭和功能

在 Swift 中,闭包是功能的代码块,可以在程序的不同部分重用。说得更清楚一点,Swift 提供了三种闭包,function 是其中之一:

  1. 全局函数是命名闭包,可以通过用它们的名字显式引用它们来调用。

  2. 嵌套函数是在另一个函数中声明的命名闭包。

  3. 闭包表达式是一个未命名的功能块,无论在哪里声明,它都会被隐式执行。

调用一个函数意味着通过引用该函数的名称来执行写在该函数内部的代码语句。

3.4.4.1 全球职能

定义一个全局函数,用关键字func后跟名字,圆括号中的参数列表,花括号中的函数体包含函数的功能。

enum Vehicle {
  case car, train
}
func drive(vehicle: Vehicle) -> String {
  switch vehicle {
    case .car:
      return "Vroom vroom!"
    case .train:
      return "Choo choo!"
  }
}
let sound = drive(vehicle: .car)
print(sound)
print(type(of: drive))

Listing 3-18Declare a global function

输出

Vroom vroom!
(Vehicle) -> String

前面的函数drive(vehicle:)接受一个Vehicle枚举类型的车辆参数(稍后讨论),表示两辆车辆。基于vehicle值,它返回一个字符串,代表它在驾驶时发出的声音。最后,我们打印出sound

在 Swift 中,每个函数都是一个引用类型。Swift 中只有类和函数是引用类型。这里,drive(vehicle:)是类型(Vehicle) -> String的函数。你可以理解为“名为drive的函数接受一个Vehicle类型的参数值并返回一个String值。”使用type(of:)功能打印该功能的类型,确认该功能的类型。

3.4.4.2 嵌套函数

嵌套函数是在另一个函数内部声明的函数。

func outerFunction() -> () -> Int {
  func innerFunction() -> Int {
    print("Running inner function.")
    return 0
  }
  print("Running outer function.")
  return innerFunction
}
let someInnerFunction = outerFunction()
print("someInnerFunction type: \(type(of: someInnerFunction))")
let someInt = someInnerFunction()
print("someInt: \(someInt)")

Listing 3-19Declare a nested function

输出

Running outer function.
someInnerFunction type: () -> Int
Running inner function.
someInt: 0

outerFunction()的类型是() -> () -> Int,它不接受任何参数,返回一个() -> Int类型的函数。嵌套函数innerFunction()的类型为() -> Int,返回 0。在返回前面的innerFunction()之前,调用outerFunction()只会在外部函数中打印一条语句。someInnerFunction()中的变量是innerFunction(),当被调用时,在其中打印一条语句并返回 0,然后存储在someInt中。

3.4.4.3 闭包表达式

闭包表达式是简单但功能强大的无名函数代码块。闭包写在一组左花括号和右花括号内。Closure 就像一个命名函数一样接受圆括号中的参数,并表示右箭头后面的返回类型。闭包的主体写在关键字in之后。稍后,我们将经常使用闭包表达式来计算损失函数相对于神经网络的梯度。

let isPositive = { (_ x: Float) -> Bool in
  return x > 0
}
print(isPositive(-5))

Listing 3-20Declare a closure to return Bool representing an integer’s positivity

输出

false

这里,闭包有类型(Float) -> Bool,也就是说,它接受一个Int并返回一个Bool。跟在in关键字后面的主体计算操作并返回结果。这个闭包向isPositive(_:)实例返回一个函数,稍后调用这个函数来检查-5 是否为正数,并正确地打印出它不是正数。

func isEven(_ x: Int, also hasProperty: (Int) -> Bool) -> Bool {
  x % 2 == 0 && hasProperty(x)
}
let number = 2
let isEvenAndPositive = isEven(number, also: isPositive)
print(isEvenAndPositive)
let isEvenAndFibonacciNumber = isEven(number) { number in
  fibonacciNumbers.contains(number)
}
print(isEvenAndFibonacciNumber)

Listing 3-21Demonstrate trailing closures

输出

true
true

函数isEven(_:also:)接受一个数字和一个表示该数字第二个属性的闭包also。将数字 2 和isPositive函数传递给isEven(_:also:)会返回 true,表示 2 是一个正整数。数组fibonacciNumbers在清单 3-31 中声明。

当闭包是函数的最后一个参数时,为了简单起见,可以去掉它的参数标签和函数调用的圆括号,只在开始和结束的花括号中写闭包。这被称为尾随闭包。为了给isEven(_:also:)提供一个自定义闭包,我们只需编写一个闭包,它返回一个布尔值,表明斐波那契数列中存在一个数字。在我们的例子中,2 是偶数,也包含在斐波那契数列中,所以它返回 true。

闭包要强大得多,解释它们的所有特性超出了本书的范围。例如,Swift 还有多个尾随闭包,允许你写多个闭包作为函数参数的尾随闭包。我们建议您参考在线提供的 Swift 官方书籍,以深入了解 closures 和许多其他功能。

自定义类型

您还可以在 Swift 中声明称为自定义类型的新类型。它可以是任何枚举、结构和类类型。枚举和结构是值类型(复制实例),类是引用类型(只引用实例,不复制实例)。每种类型在不同的情况下都很有用。

遵循命名惯例,您应使用大写字母 camel 来声明类型,以便与 Swift 标准库中已定义的其他类型保持一致。

3.4.5.1 枚举

枚举允许您定义一组彼此具有相似关系的类型。例如,您可以定义一个包含一组颜色案例的Rainbow枚举。请注意,这些颜色是它们右边的类型。使用enum关键字来定义您的枚举。

清单 3-22 声明一个枚举并根据一个实例打印一条语句

enum Rainbow {

case violet, indigo, blue, green, yellow, orange, red

}

// Rainbow.violet is similar to the following approach.

let favoriteColor: Rainbow = .indigo

switch favoriteColor {

case .violet:

print("\(favoriteColor) has low wavelength and high frequency.")

case .red:

print("\(favoriteColor) has high wavelength and low frequency.")

default :

print("\(favoriteColor) has wavelength and frequency within the range of visibleimg/484421_1_En_3_Figa_HTML.gif

}

输出

indigo has wavelength and frequency within the range of visibleimg/484421_1_En_3_Figb_HTML.gif

我们已经声明了一个名为Rainbow的枚举,它包含了彩虹中自然出现的所有主色。Rainbow的值是用case关键字声明的,它们本身就是独立的值。

通过将实例的值设置为类型为Rainbowindigo来声明favoriteColor实例。然后,通过对照不同的颜色检查favoriteColor的值,打印出合适的语句。注意这里是如何使用点语法的。编译器自动理解在每个switch案例中被比较的值是Rainbow类型的,所以使用点语法就足够了。

3.4.5.2 结构

结构是程序的基本构造块,它可以包含属性、方法和下标,以便为类型添加功能。结构也可以符合协议并扩展更多的特性。我们将在整本书中广泛使用结构。

清单 3-23 中的结构代码片段声明了一个Mammal结构和一个名为LivingZone的嵌套枚举类型。Mammal有两个属性,分别是livingZonelegsCount。这里,legsCount是一个可选的整数实例,因为一些哺乳动物可能没有腿,而另一些可能在它们的一生中失去了腿。livingZoneLivingZone描述哺乳动物生存环境的实例。

struct Mammal {
  enum LivingZone {
    case land, water
  }
  var livingZone: LivingZone
  var legsCount: Int?
}
let human = Mammal(livingZone: .land, legsCount: 2)
let injuredHuman = Mammal(livingZone: .land, legsCount: 0)
let fish = Mammal(livingZone: .water, legsCount: nil)

Listing 3-23Declare a nested structure and initialize its instances

我们已经声明了三个名为humaninjuredHumanfishMammal实例,每个实例都用不同的实例属性值初始化。你可以把human的例子理解为“人类生活在陆地上,有两条腿。”同样,injuredHuman可以描述为“受伤的人也生活在陆地上,但失去了双腿。”最后,您可以将fish实例理解为“鱼生活在水下,生存不需要腿。”

extension Mammal {
  var description: String {
    var text = "Lives \(livingZone == .land ? "on" ? "in") \(livingZone)"
    if let legsCount = legsCount {
      if legsCount == 0 {
        text += " and cannot walk because it has \(legsCount) legs."
      } else if legsCount > 0 {
          text += " and can walk with its \(legsCount) legs."
      }
    } else {
      text += " and swims."
    }
    return text
  }
}
print(human.description)
print(injuredHuman.description)
print(fish.description)

Listing 3-24Extend Mammal to include a curated description

输出

Lives on land and can walk with its 2 legs.
Lives on land and cannot walk because it has 0 legs.
Lives in water and swims.

前面的代码扩展了Mammal结构,以包含一个哺乳动物实例的精选描述。看看如何在description计算实例属性中修改text实例属性,以包含基于Mammal实例属性的细化信息。最终打印出来的句子对每个实例进行了有意义的描述。注意如何扩展Mammal结构,即使在声明实例之后,仍然允许那些实例使用扩展的功能;这适用于 Swift 中的所有自定义类型。

也可以将类型实例作为函数调用。在我们的例子中,这种函数编程方法使得在神经网络中执行前向传递变得很方便。您可以通过在自定义类型中声明一个带有任意数量参数的callAsFunction()来实现。我们将在后面章节描述神经网络模型的结构中声明这个函数。

但是有时您可能需要做的不仅仅是创建带有属性、下标和方法的实例。您可能需要从其他类型继承功能,重写某些功能,等等。类在您的类型中提供这样的功能,如下所述。

3.4.5.3 班级

就像结构一样,类也是程序的基本构建块,它可以包含属性、方法和下标,以便为类型添加功能。与结构类似,类也可以符合协议,并可以扩展更多的特性。

除了与结构共享功能之外,类还允许您从其他类继承属性、方法和下标,重写它们,进行类型转换以检查实例的类,取消实例的初始化,以及允许对类实例的多个引用。但是本书并没有讨论所有的特性。我们只浏览对理解书中的深度学习程序很重要的功能。

class Rocket {
  var name: String? = nil
  var vacuumThrust: Int = 0
  var description: String {
    return "\(name ?? "Rocket") has \(vacuumThrust) kN
thrust in vacuum."
  }
  init(name: String? = nil, vacuumThrust: Int = 0) {
    self.name = name
    self.vacuumThrust = vacuumThrust
  }
}
var rocketA = Rocket()
var rocketB = Rocket(name: "ABCRocket")
print(rocketA.description)
print(rocketB.description)

Listing 3-25Declare the Rocket class with stored and computed properties and an initializer

输出

Rocket has 0 kN thrust in vacuum.
ABCRocket has 0 kN thrust in vacuum.

前面的代码声明了一个包含两个存储属性和一个计算属性的基类Rocket。基于名称和火箭在太空中施加的推力,生成描述。我们声明两个Rocket实例并打印它们的描述。

注意,如果存储的属性不包含初始值,类不会自动获得初始化器(以init关键字开始的代码块)的实现,初始化器必须由您提供。

final class CargoRocket: Rocket {
  var payload: Int
  override var description: String {
    return "\(name!) carries \(payload) kg and has \(vacuumThrust) kN thrust in vacuum."
  }
  init(name: String?, vacuumThrust: Int, payload: Int) {
    self.payload = payload
    super.init(name: name, vacuumThrust: vacuumThrust)
  }
}
var falcon9 = CargoRocket(name: "Falcon 9",
vacuumThrust: 8_227, payload: 22_800)
var falconHeavy = CargoRocket(name:
"Falcon Heavy", vacuumThrust: 24_681, payload: 63_800)
print(falcon9.description)
print(falconHeavy.description)

Listing 3-26Inherit features of Rocket and refine to carry payload

输出

Falcon 9 carries 22800 kg and has 8227 kN thrust in vacuum.
Falcon Heavy carries 63800 kg and has 24681 kN thrust in vacuum.

一些火箭比另一些更强大,可以携带更大的有效载荷。这些火箭除了普通火箭的特点之外,还有一个有效载荷的特点。使用这个想法,前面的代码继承了来自Rocket的特性,并提供了一个额外的存储属性payload。我们还修改了description来更详细地描述CargoRocket。你必须在这里用override关键字标记描述。注意到那个final关键词了吗?这意味着这个CargoRocket类不允许被任何其他类进一步继承。

我们创建了CargoRocket的两个实例,即falcon9falconHeavy。每一个都有不同的能力,我们在初始化器中提供了细节。然后通过访问description computed 属性将详细信息正确地输出到控制台。

3.4.6 现代特征

Swift 还具有现代编程能力,即扩展、协议、泛型和差异化。有了扩展,您可以提供甚至无法访问源代码的类型的实现。协议允许您为一个类型定义一组标准(需求)。实现一个协议的所有需求的类型被称为符合该协议。Swift 提供泛型,使您的代码可用于多种可能的场景。在编译器中构建的对微分的一流支持,让您可以构建可微分的程序,从计算流体动力学(Kutz,2017 年)到机器人手运动(Akkaya 等人,2019 年),以及深度强化学习。区别特征在第 3.4.9 小节中讨论。

3.4.6.1 扩展公司

扩展允许您通过提供新功能的实现来扩展类型的功能。也可以在 Swift 中做追溯建模。换句话说,您可以扩展甚至无法访问源代码的任何类型,例如,Swift 的标准库类型,如 Array 和 Tensor(在 TensorFlow 库中可用),甚至可以扩展其他库中声明的类型,如 Foundation、Vision 等。

extension Int {
  mutating func raised(to power: Self) {
    assert(power > 0, "`power` must be a non-negative integer.")
    if power == 0 {
      self = 1
      return
    }
    var result = 1
    for _ in 1...power {
      result = result * self
    }
    self = result
  }
}
var number = 5
number.raised(to: 3)
print("Now `number` is \(number).")

Listing 3-27Extend Int to mutate its value when raised to some power

输出

Now `number` is 125.

前面的例子扩展了Int以包含一个实例方法raised(to:),该方法通过将自身提升到方法调用期间给定的某个power值来改变实例的值。该方法被标记为mutating,因为Int在标准库中被声明为一个结构,而结构是值类型,与引用类型的类不同,它们不能修改自身。我们首先写一个断言,确保power不是负数。然后,如果power为零,则if条件将实例的值设置为等于 1。稍后,一个for - in循环迭代地计算实例提升到power的值。最后,一旦计算完成,实例被设置为等于result

扩展能够提供计算属性、方法、下标、初始化器、嵌套类型的实现,甚至使类型符合协议等等。

在第六章中,我们将看到如何扩展 TensorFlow 的Layer协议,通过与 Python 语言的互操作来定义自定义的检查点写入和读取实例方法。这将展示我们可以利用这些现代而强大的功能侵入 Swift。

3.4.6.2 议定书

大多数程序员通常都熟悉面向对象编程。相比之下,Swift 从诞生之日起就被设计成一种面向协议的语言。因为 Swift 也是面向对象的,我们首先从协议声明开始,然后在合适的场景下使其他类型如枚举、结构和类符合它们。

protocol ProfileProtocol {
  let name: String { get }
  var age: Int { get set }
  var email: Int { get set }
}
struct Person: ProfileProtocol {
  let name: String
  var age: Int
  var email: String
}
var rahulbhalley = Person(name: "Rahul Bhalley", age: 24,
email: "rahulbhalley@icloud.com")

Listing 3-28Declare and use ProfileProtocol

这里,ProfileProtocol有三个要求,分别是String类型的可获取nameInt类型的可获取可设置ageString类型的可获取可设置email。它要求一致性类型提供它们的实现。Person结构通过提供需求的实现来采用ProfileProtocol

您还可以扩展协议来提供实现,而不仅仅是需求信息。这样,符合该协议的类型会自动获得那些实现。

extension ProfileProtocol {
  var details: String { "Name: \(name), age: \(age), email: \(email)" }
}
print(rahulbhalley.details)

Listing 3-29Extend ProfileProtocol to provide common implementation

输出

Name: Rahul Bhalley, age: 24, email: rahulbhalley@icloud.com

现在,符合ProfileProtocol的任何类型,包括以前的实现,都可以访问details属性。注意,如果函数体、闭包、计算属性或返回某个值的下标中只有一个语句,我们可以去掉 return 关键字。

3.4.6.3 仿制药

泛型允许您编写单个代码块,并使用许多不同的可能数据类型执行它。这是被称为多态性的面向对象编程的基本原则之一。Swift 语法使得实现和使用通用代码块变得非常容易。在 Swift 中,您可以定义属性、下标、函数、方法甚至枚举、结构和类的泛型。

func swapValues<T>(_ x: inout T, _ y: inout T) {
  let temporaryX = x
  x = y
  y = temporaryX
}
// Swapping String values
var x = "x"
var y = "y"
swapValues(&x, &y)
print("x: \(x) y: \(y)")
// Swapping custom type instance values
swapValues(&falcon9, &falconHeavy)
print(falcon9.description)
print(falconHeavy.description)

Listing 3-30Swap values of two variables of the same type

输出

x: y, y: x
Falcon Heavy carries 63800 kg and has 24681 kN thrust in vacuum.
Falcon 9 carries 22800 kg and has 8227 kN thrust in vacuum.

Swift 已经提供了swap(_:_:)函数来交换任意两个相同类型实例的值。但是为了熟悉泛型编程,我们在前面的代码中实现了自己的版本,名为swapValues(_:_:)

这里,T是一个类型占位符,其类型在使用该函数之前是未知的。当您在函数调用中提供变量时,编译器会推断出该类型。inout关键字允许您修改函数体中的参数值,并将这些修改写回外部变量。函数调用中变量前的&符号(&)清楚地告诉你,它们的值在函数体内是可变的。请注意,您应该始终用关键字var声明这些实例,否则它们将不会变异。您可以将函数声明理解为“声明一个名为swapValues的通用函数,带有类型占位符T,并带有两个inout变量xy.

我们已经成功地交换了两个字符串的值,甚至我们自己的结构实例!您可以交换任意两个变量实例的值,只要它们属于同一类型。接下来,我们使用两个泛型类型来描述泛型的更复杂的用法。

func allCommonAndUniqueElements<T: Sequence, U: Sequence>
(_ left: T, _ right: U) -> [T.Element]
where T.Element: Equatable, T.Element == U.Element {
  var result = [T.Element]()
  for leftItem in left {
    for rightItem in right {
      if leftItem == rightItem && !result.contains(leftItem) {
        result.append(leftItem)
      }
    }
  }
  return result
}
let fibonacciNumbers = [0, 1, 1, 2, 3, 5, 8, 13, 21]
let oddNumbers = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
let commonNumbers =
allCommonAndUniqueElements(fibonacciNumbers, oddNumbers)
print(commonNumbers)

Listing 3-31Find common and unique elements from two Array<Int> instances

输出

[1, 3, 5, 13, 21]

前面的allCommonAndUniqueElements(_:_:)有两个通用类型TU,它们都符合Sequence协议。where子句后的语句声明T序列的每个元素必须符合Equatable(必须相等),并且TU应该具有相同类型的元素。该函数将分别符合TUleftright作为参数,返回类型为T的元素数组。最后,调用该函数只是查找并返回一组唯一的和常见的元素。

let machineLearning = "machine learning"
let deepLearning = "deep learning"
let commonCharacters =
allCommonAndUniqueElements(machineLearning, deepLearning)
print(commonCharacters)

Listing 3-32Find common and unique elements from two Array<Character> or String instances

输出

["a", "i", "n", "e", " ", "l", "r", "g"]

这个通用函数适用于任何序列实例,并且其元素可以相等。对于String实例也是如此,因为它是一个由Character组成的数组。

struct Pair<T, U> {
  var x: T
  var y: U
  var description: String { "x: \(x) and y: \(y)" }
}
let pair = Pair(x: "x", y: 5)
print(pair.description)
let anotherPair = Pair<Vehicle,
CargoRocket>(x: .car, y: falconHeavy)
print(type(of: pair))
print(type(of: anotherPair))

Listing 3-33Demonstrate the generic type

输出

x: x and y: 5
Pair<String, Int>
Pair<Vehicle, CargoRocket>

就像泛型函数一样,我们也可以在 Swift 中声明泛型类型,使该类型在各种不同的场景中可重用。这里,Pair结构被声明为一个泛型类型,有两个泛型类型TU。这两个存储的属性属于不同的泛型类型。两个实例pairanotherPair分别用泛型类型Pair<String, Int>Pair<Vehicle, CargoRocket>声明。这可以通过打印它们的类型信息来确认。

泛型不仅仅限于结构,还适用于枚举和类。它们可以被视为在 Swift 中实施多态性的一种非常强大的方式。

Swift 的另一个现代而强大的功能是差异化,这将在第 3.4.9 小节中详细讨论。

错误处理

错误处理是在容易出错的情况下使程序更加健壮的一种方式。有了错误处理,您可以将程序从崩溃中拯救出来,并通过将错误信息打印到控制台来更清楚地了解错误。Swift 的简单语法有助于使错误处理代码更易于阅读和理解。

在 Swift 中处理错误的一种便捷方法是使用do-catch代码块。

enum RequestError: Error {
  case noInternet
  case notFound
  case timeOut
}
func ping(website link: String) throws -> String {
  if link == "Wrong address" {
    throw RequestError.notFound
  } else if link == "No connection" {
    throw RequestError.noInternet
  } else if link == "Request timeout" {
    throw RequestError.timeOut
  }
  return #"Website: "\#(link)" is live."#
}
do {
  let pingResponse = try ping(website: "Wrong address")
  print(pingResponse)
} catch RequestError.noInternet {
  print("Could not connect to the Internet.")
} catch let pingError as RequestError {
  print("Error: \(pingError)")
}

Listing 3-34Respond to possible errors when requesting a web page

输出

Error: notFound

要表示错误,只需让您的自定义类型(这里是RequestError)符合Error协议。它是一个空协议,仅用于表示错误。枚举类型非常适合 Swift 中的错误表示。

只有函数会抛出错误。在参数列表后面写throws关键字,告诉编译器这个函数可以抛出错误。并在可能抛出错误的函数调用前编写try关键字。最后,当错误发生时,使用throw关键字抛出一个错误,后跟错误,例如throw RequestError.noInternet

前面的代码显示了如何在 ping 某个网站失败时抛出错误。可能的错误在RequestError枚举中被写成案例。对do块中的ping(website:)函数的调用需要一个用于 ping 的链接。如果在查验时出现错误,则在catch块中抛出一个适当的错误。您应该考虑操作website参数标签值,看看为每个值打印了什么错误消息。

这里需要注意的一件有趣的事情是在字符串中使用双引号时使用的散列符号(#)。在字符串双引号之前和之后使用 hash 可以让您在字符串本身内部写双引号。插值的一个附加变化是在反斜杠()和左圆括号之间使用了散列符号。

高级操作员

Swift 支持在有意义的语法中使用函数轻松实现类型上的自定义运算符。清单 3-35 是添加两个结构实例的简单例子。

struct Point3D {
  var (x, y, z) = (0.0, 0.0, 0.0)
  var description: String {
    "Coordinates: (\(x), \(y), \(z))"
  }
}
extension Point3D {
  static func + (left: Self, right: Self) -> Self {
    Point3D(x: left.x + right.x, y: left.y + right.y, z: left.z + right.z)
  }
}
var pointA = Point3D(x: 1, y: 2, z: 3)
var pointB = Point3D(x: 4, y: 5, z: 6)
print((pointA + pointB).description)

Listing 3-35Declare a structure and implement an advanced operator

输出

Coordinates: (5.0, 7.0, 9.0)

当你定义自己的结构时,编译器不知道任何数学运算对它意味着什么。在这里,您可以为特定的运算符定义自己的运算,例如,为加号运算符定义加法,如前面的代码所示。这个加号(+)实例方法在添加两个参数的每个元素后返回Point3D实例。该操作只是添加了两个Point3D实例,这可以通过前面的描述来验证。

通常深度学习中的领域特定语言(DSL)像 python NIC tensor flow(Abadi 等人,2016),PyTorch (Paszke 等人,2017,2019),autograd (Maclaurin 等人,2015;Maclaurin,2016),以及许多其他人在 C++代码库中使用该功能来实现张量运算和自动微分(通常是反向模式)。相比之下,Swift 采取了一种激进的方法来解决这个问题,即在编译器中实施自动微分,以获得最佳性能,并允许静态编译的语言功能和 Swift 的优势。

区别

Swift 为算法差异化功能提供一流的支持。区分已经在编译器内部被烘焙,使得 Swift 的类型系统是可区分的。令人惊奇的是,你不必限制你的程序的表达能力,你可以自由地编写控制流语句、循环和递归,程序仍然是可微分的!您用清单 3-36 中的语句导入 _ Differentiation 库。

import _Differentiation

Listing 3-36Import the _Differentiation library

注意库名前面的下划线吗?这意味着该库尚未准备好用于生产(但很快就会准备好),因此不能用于在 Xcode 中构建应用程序以供分发。但是您可以在 Swift 中使用这个特性来学习、练习和研究差异化编程(甚至深度学习,我们将在后面看到)!如果你是一名应用程序开发人员,那么,在未来,你将能够开发并向你的用户分发不同编程的应用程序。

一旦导入了_Differentiation库,您就可以访问所有可用的差异化 API。下面的代码清单期望这个 import 语句在顶部。不可能在一本书中涵盖编程的每个细节。我们建议您参考(魏等,2018)。

3.4.9.1 可微类型

严格地说,只有函数可以微分,因此可以有导数或偏导数。更具体地说,如果一个函数的参数和结果值都是可微的,那么这个函数就是可微的。从程序上来说,这意味着参数和结果类型必须是可微的,因为函数体内部的计算也是可微的。并且这些类型不仅限于 Swift 的类型系统中由FloatDouble类型表示的标量实数。但是它们也可以扩展到任何维度的向量、矩阵或张量数据结构。Swift 使得区分这种定制类型和功能成为可能。

在撰写本文时,Swift 提供了用Differentiable协议声明可区分定制类型的语法上最有意义、数学上最合理的方式。

关于 Swift 的类型系统,一个有趣的事实是,诸如FloatIntDoubleStringDictionaryArray等类型是在 Swift 的标准库中定义的,而不是在 Swift 的编译器中定义的。这允许增加数值计算的类型,并将它们作为语言的一等公民对待。

有不同的数学分支允许对各种类型的参数和结果进行函数微分:

  1. 初等微积分:数学的一个分支,我们简单地计算函数标量结果相对于输入标量的导数。

  2. 向量微积分:数学的一个分支,涉及向量场作为参数/结果的微分。这个分支进一步延伸到矩阵和张量领域。

  3. 微分几何:数学的一个分支,其中函数在流形上微分。流形是高维空间中的连通区域,其中彼此靠近的点似乎在欧几里得空间中。

接下来,我们介绍允许自定义类型执行区分的通用协议。该协议也共同满足了上述数学子领域的要求。

3.4.9.2 可区分协议

遵循 Swift 的面向协议的编程范式,为了符合不同分支中区分的数学理论,Swift 在Differentiable协议中引入了可区分类型的概念。从数学上来说,能够表示实数的类型(如FloatDouble)是可微的,而其他的则不是(如StringInt)。当您尝试区分一个不可区分的类型时,Swift 编译器会发出一条人类可读的错误消息。例如,清单 3-37 中的代码无法区分。

@differentiable
func failedDifferentiation(_ input: Float) -> Float {
  Float(Int(x))
}

Listing 3-37Show that differentiation of an Int instance is not possible

这段代码没有编译并报告一个错误:“函数不可微。”尽管输入和返回类型都是可微分的,但函数内部的计算却不是。这是因为该值被转换成Int,然后再转换回Float,这使得计算不可微,并且Int不能表示实数。每当您的代码中出现此类错误时,Swift 将通过警告或错误信息(在这种情况下)引导您对代码进行修改,尤其是在 Xcode 中。这样 Swift 对于机器学习的初学者理解微分和机器学习本身是非常有帮助的。

任何符合Differentiable协议的类型都可以作为参数传递给一个可微分函数,并从该函数返回。这意味着我们可以在纯 Swift 中计算函数结果相对于其参数的导数或偏导数(即,不像许多 Python 库那样使用 C 或 C++代码)。注意,对于一个可微的函数来说,函数体内部的计算也必须是可微的(参见清单 3-37 )。

Swift 已经为其基本类型(如FloatDoubleArrayDictionary等)提供了一致性。)到其标准库中的Differentiable协议。但是这种一致性不仅限于基本类型,还扩展到用户定义的自定义类型(例如,TensorFlow 库中的Tensor自定义类型),它可以包含实例和类型属性(存储的和计算的)、下标和方法。(避免声明枚举类型,因为它们不能有存储属性。)如果有某种类型目前不符合Differentiable协议,您可以简单地扩展它,编译器将通过代码生成自动综合所有差异化的特定需求,这将在下面讨论。

让我们编码一个名为Point3D(改编自清单 3-35 )的三维向量,并使其可微。

struct Point3D: Differentiable, AdditiveArithmetic {
  var (x, y, z) = (0.0, 0.0, 0.0)
  @noDerivative
  var description: String {
    "Coordinates: (\(x), \(y), \(z))"
  }
}
var pointA = Point3D(x: 5, y: 2, z: 3)
var pointB = Point3D(x: 5, y: 2, z: 3)
let result = valueWithGradient(at: pointA) { pointA in
  (pointA + pointB).y
}
print("sum: \(result.value)")
print("𝛁sum:  \(result.gradient.description)")

Listing 3-38Make the Point3D structure differentiable

```py

**输出**

sum: 4.0
𝛁sum: Point3D(x: 0.0, y: 1.0, z: 0.0)


这里,`Point3D`符合`Differentiable`和`AdditiveArithmetic`协议从`AdditiveArithmetic`开始采用增加高级操作符的实现`Differentiable`协议的采用使得`Point3D`具有可区分性由于`Differentiable`协议要求属性符合`Differentiable`本身,我们标记了描述`@noDerivative`,因为`String`是不可微的`Point3D`成功符合`Differentiable`,因为所有属性现在都是可微的,包括被推断为`Double`的`x``y`和`z`

`valueWithGradient(at:in:)`函数返回`(pointA + pointB).y`函数的结果及其相对于`pointA`的导数,作为命名元组这允许您使用点语法(.),便于理解返回的是什么值注意,输出必须始终是一个可微分标量(这里,`Double`),仅用于计算其相对于变化参数的梯度(这里,`pointA`)这是因为`gradient(at:in:)`及其变体实现了反向模式 AD这里,我们得到`(pointA + pointB).y`输出在`pointA`的偏导数这就是为什么𝛁 `result`中的`x`和`z`的值都为零

当您将任何自定义类型(例如,前面的`Point3D`)符合`Differentiable`协议时,Swift 的编译器会自动合成各种要求的实施,如下所示请参考(魏等,2018)了解更多详情:

1.  **结构类型**:编译器自动提供`TangentVector`结构的实现`TangentVector`结构包含每个可微分存储属性的导数那些标有`@noDerivative`属性的属性不会被引入到合成的`TangentVector`中

2.  **属性**:访问`zeroTangentVector`属性返回一个实例,其中所有可微分的存储属性都用零值初始化(在我们的例子中是`Point3D(x: 0.0, y: 0.0, z: 0.0)`)

3.  **实例方法**:文档中描述的`move(along:)`变异方法是“沿给定方向移动`self`在黎曼几何中,这相当于指数地图,在测地曲面上沿着给定的切向量移动`self`”在内部,`move(along:)`通过添加`along`参数的相应属性来变异`self`的所有可微分属性这里,`along`需要一个调用该方法的相同类型的实例

在清单 3-38 中,我们简单地将`Point3D`与`AdditiveArithmetic`进行了整合,并且自动提供了高级运算符`+`用于可微加法运算现在我们将看看如何构造我们自己的可微函数

#### 3.4.9.3 @可微属性

Swift 允许两种属性,即声明属性和类型属性属性为修改行为的声明提供了更多信息。*声明属性*应用于类似函数的声明(包括方法属性和初始化器),而*类型属性*应用于声明的类型举个简单的例子,如果函数返回的结果没有被使用或存储在另一个变量中,那么应用于函数的`@discardableResult`声明属性不会发出警告

Swift 提供了`@differentiable`属性,可用于注释声明和声明类型让我们看看如何使用`@differentiable`声明属性来声明一个可微函数

```py
extension Double {
  @differentiable
  var cubed: Self { self * self * self }
}
let x: Double = 5
print("\(x)³ = \(x.cubed)")
let grad = gradient(at: x) { x in x.cubed }
print("Gradient of x³ at \(x) is \(grad)")

Listing 3-39Declare and demonstrate the usage of the @differentiable declaration attribute on a computed property

输出

5.0³ = 125.0
Gradient of x³ at 5.0 is 75.0

使用回溯建模,我们首先声明名为cubed的可微分计算属性。对@differentiable声明属性的简单使用使得计算出的属性是可区分的。然后我们简单地将cubed的梯度设为 5,并将其结果存储在grad常量中。数学上,dx3/dx= 3xx2x = 5 处计算得出 75,因此我们的代码也给出了预期的结果。

这就是 Swift 的基本特征。接下来,我们继续讨论 S4TF 特有的特性,这些特性使它成为机器学习的强大语言。第四章专门介绍 TensorFlow 和相关库中的机器学习特定功能。下一节将展示 S4TF 如何与 Python 语言进行互操作,以及如何轻松访问它的内置和自定义函数。

3.4.9.4 差异化原料药

差异化 API 遵循一种命名模式。让我们通过考虑一些例子来解释如何阅读这些闭包。首先,参数标签atinof分别以变量、闭包、闭包为输入。在这里,gradient(at:in:)可以读作“计算闭合在一点的梯度,并返回梯度值。”函数gradient(of:)读起来也很简单,“计算闭包的梯度并返回它”,可以在一个期望值上求值。前缀valueWith返回包含valuegradientpullback或其他闭包的命名元组。

下面列出了 Swift 中的各种正向模式差异闭包。但我们不讨论这些,因为它们仍处于开发和实验阶段:

  1. differential(at:in:)valueWithDifferential(at:in:)

  2. derivative(of:), derivative(at:in:), valueWithDerivative(of:)valueWithDerivative(at:in:)

我们主要关注书中的反模式算法微分函数,讨论如下:

  1. pullback(at:in:)valueWithPullback(at:in:):它们计算闭包的标量输出相对于输入标量变量(即单变量或多变量)的偏导数,并返回一个pullback闭包。这个闭包以输出相对于自身的导数作为自变量,即 d y /d y 其中 y 是输出变量。我们通常设置 d y /d y = 1。在回调评估期间,该值在链式规则中被乘以 dy/dx= dy/dy⋅dy/dx,其中 x 是返回的pullback闭包的输入变量。

  2. gradient(at:in:)valueWithGradient(at:in:):基于pullback(at:in:)valueWithPullback(at:in:);因此,它们的工作方式与它们相同,但总是设置 d y /d y = 1,并简单地返回评估的梯度,而不是pullback闭包。

  3. gradient(of:)valueWithGradient(of:):这些也是基于pullback(at:in:)valueWithPullback(at:in:);因此,它们的工作方式和它们一样,但是返回一个渐变闭包,当在某个点求值时,它返回一个闭包求值的元组valuegradient值。

我们已经在清单 3-37 、 3-38 和 3-39 中展示了我们后来感兴趣的一些差异闭包。现在我们演示如何定义函数的自定义导数。

3.4.9.5 定制衍生品

我们知道立方函数f(x)=x3的导数是f'(x)= 3x2。但是如果我们想定制这个函数的导数呢?Swift 允许我们使用@derivative(of:)函数声明属性在反向模式微分中定义函数的自定义导数,而@transpose(of:)用于正向模式微分,但它仍在开发中。让我们定义所需的自定义导数。

func cube(_ x: Float) -> Float {
  x * x * x
}
let anotherX: Float = 4
print("Before customization, df/dx =", gradient(at: anotherX, in: { x in
  cube(x)
}))

@derivative(of: cube)
func vjpCube(_ x: Float) -> (value: Float, pullback: (Float) -> Float) {
  (value: cube(x), pullback: { chain in chain * 2 * x })
}
print("After customization, df/dx =", gradient(at: anotherX, in: { x in cube(x) }))

Listing 3-40Demonstrate the declaration of custom derivatives in reverse-mode differentiation

输出

Before customization, df/dx = 48.0
After customization, df/dx = 8.0

我们声明了一个返回参数x的立方的cube(_:)函数,并初始化了一个等于 4 的Float常数anotherX。然后使用gradient(at:in:)函数,我们计算cube(_:)闭包在anotherX点的导数。

接下来,我们声明了一个vjpCube(_:)函数,并对其应用了@derivative(of: cube)属性,这告诉编译器根据该函数返回的pullback闭包声明来计算cube(_:)闭包的导数。该函数返回一个命名的元组,其中分别包含类型为Float(Float) -> Floatvaluepullback。在vjpCube(_:)的函数体内,我们将cube(x)返回为value,将chain * 2 * x返回为pullback闭包。这里,chain代表 d y /d y ,如果传递给gradient(at:in:) variants,其值为 1,但如果传递给pullback(at:in:) variants,其值实际上取决于你。现在,当我们在anotherX计算cube(_:)闭包的梯度时,我们成功地获得了 8 作为输出。

我们还可以用@transpose(of:)函数声明属性定义前向模式微分的自定义导数;但是,不幸的是,在撰写本文时,它还处于试验和开发阶段。

3.4.9.6 停止导数传播

你也可以阻止导数通过整个计算图的子图传播。Swift 提供了两个闭包,即withoutDerivative(at:)withoutDerivative(at:in:),用于停止计算导数。在这里,at参数接受一个在某个点上计算的数学表达式,它不会参与整个表达式的导数计算,而只是返回它本身的值。in参数接受一个不需要计算其导数的闭包,它通过在给定点计算闭包内部的数学表达式来返回值。

let yetAnotherX: Float = 5
let result1 = valueWithGradient(at: yetAnotherX) { x in
    x * x * withoutDerivative(at: x)
}
print("result1: \(result1)")
let result2 = valueWithGradient(at: yetAnotherX) { x in
    x * x * withoutDerivative(at: x) { y in
        y + 10
    }
}
print("result2: \(result2)")

Listing 3-41Demonstrate the stopping of gradient computation through a graph

输出

result1: (value: 125.0, gradient: 50.0)
result2: (value: 375.0, gradient: 150.0)

此示例中的所有表达式的值都是 5。在这里,result1的闭包表达式计算x3,计算后返回 125。但是它的导数是 3x2 应该返回 75,但是因为withoutDerivative(at: x)不涉及导数,所以我们得到(x??)'⋅(x+10)= 2x⋅(x+10),计算后得到 50。

result2的闭包表达式演示了一个更复杂的计算x2⋅(x+10),计算后返回 375。当它的导数 3x2+20x求值为 5 时,我们应该得到 175,但我们没有。这是因为子表达式 x + 10 从不参与导数计算,实际的导数在这里是 2 x ⋅ ( x + 10),当值为 5 时返回 150。

停止梯度计算在训练各种神经网络中起着重要作用。例如,生成对手网络包含两个不同的连接的神经网络,也就是说,它们作为单个神经网络,但是它们应该被单独训练,尽管它们一起作为整个单个计算图。为了停止计算网络参数的导数,我们可以将执行计算的闭包传递给withoutDerivative(at:in:)函数的in参数。一些著名的重要深度学习任务,如图像风格化、对立示例生成和其他任务,也需要停止通过子计算图的梯度传播。

这就是 Swift 的基本特征。下一节将展示 Swift 如何轻松地与 Python 语言进行互操作,并轻松访问其内置和自定义功能以及任何已安装的库。

3.5 Python 互操作性

我们知道 Python 是目前机器学习非常重要的语言。如果您已经是一名 Python 程序员,您可能知道有许多重要而有用的库是用 Python 编写的。我们可能仍然希望使用这些库来保持我们的生产力。因此,无需在 Swift 中重写所有这些库,您可以直接在 Swift 中轻松地与 Python 的内置函数和安装在您系统上的所有 Python 库进行互操作。

在内部,当 S4TF 访问 Python 实体时,就会调用 Python 解释器来执行这个过程并将数据返回给 S4TF。所以 Python 操作的执行时间取决于 Python 的解释器而不是 S4TF 的编译器。为了使与 Python 动态特性的交互成为可能,S4TF 引入了PythonObject结构,它可以存储 Python 解释器返回的任何数据。S4TF 中任何从 Python 导入的 Python 库、类、实例或任何其他实体都用PythonObject表示。这样你也可以在 S4TF 中对PythonObject执行操作,我们将在下面看到。请注意,S4TF 中可用的 Python 库可以通过包含一个带有代码的包而在 Swift 中可用。包(网址:“ https://github.com/pvieito/PythonKit.git ”,。Xcode 中 Package.swift 文件中的 branch("master "))。

本节假设清单 3-42 中的 import 语句被写在本节中每个代码清单的顶部。

import PythonKit

Listing 3-42Import the PythonKit library

清单 3-43 中的代码展示了一个简单的例子,演示如何在 S4TF 中使用 Python 的 NumPy 库。

let np: PythonObject = Python.import("numpy")
let x = np.array([1, 4, 2, 5], dtype: np.float32)
let y = np.array([5, 2, 4, 1], dtype: np.float32)
print(x * y)

Listing 3-43Add two NumPy arrays

输出

[5\. 8\. 8\. 5.]

我们首先导入 Python 的 NumPy 库。然后我们声明并初始化两个 NumPy 数组xy。最后,我们印刷他们的产品。注意在 S4TF 中使用简单的乘法运算符(星号)直接将 Python 对象相乘是多么容易。这是因为PythonObject实现了这样的高级运算符。

注意,Swift 和 Python 中都存在一些全局函数和关键字,如typeimport等。由于我们将要编写的程序是在 S4TF 中,这使得编译器很难理解我们希望编译器调用哪个全局函数。因此,为了防止这种不必要的情况在实践中出现,S4TF 为 Python 实体添加了一个名称空间,在 Python 和 S4TF 的全局函数之间添加了一个分离层,从而使 S4TF 编译器完全清楚应该调用哪种语言的全局函数。参见清单 3-44 中 Python 名称空间如何有效分离这两种语言的全局函数的例子。

import Python
let myNamePy: PythonObject = "Rahul Bhalley"
print("myNamePy Swift type: \(type(of: myNamePy))") print("myNamePy Python type: \(Python.type(myNamePy))")

Listing 3-44Demonstrate the Python namespace

输出

myNamePy Swift type: PythonObject
myNamePy Python type: str

在前面的代码中,我们将myNamePy文字初始化为PythonObject。然后我们可以在 Python 名称空间中使用 Swift 的type(of:)函数和 Python 的type()方法。值得注意的是,尽管 Swift 没有任何名称空间的概念,但这种行为可以通过使用类型方法和枚举来实现。

如前所述,Python 是一种动态类型语言,其中每种类型都继承自基类object。在某个时候,你声明的变量可能包含一个str值,而在另一个瞬间,它可以存储一个int值,甚至任何其他用户定义的类。Python 中也没有类型安全检查。但是 S4TF 是一种可以静态和动态执行的强类型语言。S4TF 的动态操作能力让我们能够以动态的方式与 Python 进行互操作,而不会损害 Python 的动态特性。即使在 S4TF 中,这也是用户期望的最基本的 Pythonic 需求。S4TF 没有直接提供 Python 的基本类型,而是提供了对PythonObject类型的访问,该类型充当 S4TF 和 Python 的基本类型之间的隔离。你可以简单地在 S4TF 中声明一个PythonObject类型的变量,它的行为和 Python 变量一样,也就是说,它的值可以在不同的类型之间进行操作(比如Python.strPython.float等)。).除了在这样的实例上调用方法,我们还可以直接在这些 Python 实例上执行算术运算。

注意,这些操作实际上是由 Python 解释器执行的,但是PythonObject的操作符是在 S4TF 中声明的。参见清单 3-45 中关于PythonObject s 的操作示例

var firstNumber: PythonObject = 30
var secondNumber: PythonObject = 6
let result = secondNumber / firstNumber
print("The result is \(result).")
print("Swift type is \(type(of: result)).")
print("Python type is \(Python.type(result)).")

Listing 3-45Demonstrate operations on PythonObjects

输出

The result is 5.
Swift type is PythonObject.
Python type is int.

我们声明并初始化两个PythonObject变量firstNumbersecondNumber,分别有 30 和 6 个值。虽然这些是PythonObject s,但是我们可以对它们进行加、乘、减、除等各种运算。这里,我们将secondNumber除以firstNumber

因为互操作性让我们可以直接从 S4TF 与 Python 对象进行交互,所以我们还可以在这些语言之间执行类型转换!要将 S4TF 类型转换为 Python 的对应类型,只需用PythonObject类型对 S4TF 实例进行类型转换。如果您需要执行 Python 到 S4TF 类型的转换,您将使用您想要的 S4TF 类型对PythonObject进行类型转换。但是这个类型转换在 S4TF 中返回一个可选类型,也就是PythonObject?,因为这个转换可能会失败。例如,当您将 Python 的str对象类型转换为 Swift 的Int类型时,这种转换是不可能的,并且会返回一个nil

毫无疑问,这个特性在某些情况下非常有用,特别是机器学习,当我们必须用 NumPy 的ndarray对象的值初始化 S4TF 的Tensor实例时,因为我们可能需要通过某个硬件加速器上的神经网络实例传递这些值,这几乎总是这样。清单 3-46 描述了这些语言之间的简单类型转换。

// Conversion from Swift to Python type
let swiftFive: Int = 5 // Swift Int value
let pyFive = PythonObject(swiftFive) // Python int value
print("Python type of pyFive is \(Python.type(pyFive)).")
print("Swift type of pyFive is \(type(of: pyFive)).")
// Conversion from Python to Swift type
let pyDescription: PythonObject = "Python interoperability
feature is beautiful!" // Python str type
if let swiftDescription = "Swift’s \(String(pyDescription))" {
  print("swiftDescription (conversion accomplished!): \
(swiftDescription)")
}

Listing 3-46Type conversions between Swift types and PythonObject

输出

Python type of pyFive is int.
Swift type of pyFive is PythonObject.
swiftDescription (conversion accomplished!): Swift’s Python interoperability feature is beautiful!

数据科学家工具箱中最重要的工具之一是可视化,即使是机器学习研究人员和实践者也需要它。借助 Python 的互操作性,我们可以很容易地在 S4TF 中进行数据可视化。简单的例子见清单 3-47 。确保您已经通过 Pip 包管理器安装了 Matplotlib 库;否则,只需在终端中运行以下命令:pip install --upgrade matplotlib

img/484421_1_En_3_Fig5_HTML.png

图 3-5

通过与 S4TF 的互操作,使用 Python 的 Matplotlib 库绘制的指数函数图

// Import Python libraries
let np = Python.import("numpy")
let plt = Python.import("matplotlib.pyplot")

// Declare variables
let x = np.linspace(0, 5, 50)
let y = np.exp(x)

// Plot the values
plt.xlabel("Values")
plt.ylabel("Exponent of values")
plt.title("Exponential Function")
plt.plot(x, y, color: "violet")
plt.show()

Listing 3-47Plot an exponential function

运行前面的代码会显示一个图像(见图 3-5 )显示范围为[0,5]的指数函数的曲线图。这里,Matplotlib 库的plt模块执行xy变量的绘制。

Python 互操作性帮助我们在 S4TF 中使用 Python 众所周知的强大库。这为深度学习的当前 Python 用户轻松过渡到 S4TF 消除了障碍。

3.6 摘要

本章重点介绍了 Swift 语言的编程。Swift 引入了具有编译器级实施的差异化编程,而 Swift for TensorFlow 语言是 Swift 实施深度学习特定功能的扩展。本章从激励当前的深度学习社区(使用 Python)采用 Swift 语言开始。接下来,我们介绍了在 Swift 中实现的算法差异特性的具体细节。然后提供了 Swift 语言的快速浏览,使您能够轻松理解 Swift 的各种基本功能和强大功能。最后,我们介绍了 Python 互操作性特性,该特性使 Swift for TensorFlow 的用户可以直接从 Swift for TensorFlow 使用他们喜爱的 Python 库。

现在我们已经准备好理解 TensorFlow 的基础知识(将在下一章介绍),这将允许我们对深度学习进行编程。

四、TensorFlow 基础知识

现在最重要的事情不再是争论 Swift 是否应该存在差异化编程(因为 Swift + ML 太重要了!),而是搞清楚应该在语言中落地的最佳形式!

—理查德·魏在推特上

这一简短的实用章节旨在介绍 Swift for TensorFlow 的一些深度学习特定功能。第 4.1 节介绍了张量数据结构的概念,它实质上是神经网络进行预测的基础。阅读本章后,你将能够加载数据集(第 4.2 节),编写自己的神经网络(第 4.3 节),训练你的模型并测试其准确性(第 4.4 节)。除了所有这些,在 4.5 节,你还将学习如何实现你自己的新层,激活函数,损失函数,和优化器。这将有助于原型化您的研究代码或实现深度学习算法的高级构建模块。本章要求对机器学习有所了解。我们建议您通过阅读第一章来更新您的概念。

4.1 张量

在第二章中,我们已经学习了标量、向量和矩阵的概念以及一些重要的运算。这里,我们将它们形象化以帮助我们理解,并引入张量的概念来概括它们。理解张量很重要,因为构成本书主题的神经网络本质上是操纵张量值来进行预测的。

张量是一种可以在 n 维空间中存储数值的数据结构,其中 n ≥ 0。有一些常见的张量如标量、向量和矩阵,它们的维数(称为)分别是零、一和二。当我们需要存储秩大于 2 的高维值时,我们使用术语张量。换句话说,张量概括了前面提到的所有低维数据结构。

TensorFlow 提供了用于初始化任意维度张量的Tensor类型。它提供了两个重要的实例计算属性,即rankshaperank属性返回一个代表Tensor实例维数的Int值。例如,vector 实例的rank为 1。shape属性返回一个代表每个维度中元素数量的Array值。比如一个包含三行两列的矩阵,有[4,3]的shape(程序化),数学上我们写成[4 × 3]。清单 4-1 展示了一些Tensor的例子,这些例子在图 4-1 中可以看到。

img/484421_1_En_4_Fig1_HTML.jpg

图 4-1

各种张量及其相应的秩和形状属性的可视化。在这里,每个方块包含一些数值

import TensorFlow

let scalar = Tensor<Float>(10)
let vector = Tensor<Float>(ones: [5])
let matrix = Tensor<Float>(zeros: [4, 3])
let tensor = Tensor<Float>(repeating: 2, shape: [4, 3, 2])

// Print `Tensor`s
print("scalar: \(scalar)")
print("vector: \(vector)")
print("matrix:\n\(matrix)")
print("tensor:\n\(tensor)")
print()

// Ranks
print("scalar rank: \(scalar.rank)")
print("vector rank: \(vector.rank)")
print("matrix rank: \(matrix.rank)")
print("tensor rank: \(tensor.rank)")
print()

// Shapes
print("scalar shape: \(scalar.shape)")
print("vector shape: \(vector.shape)")
print("matrix shape: \(matrix.shape)")
print("tensor shape: \(tensor.shape)")
print()

Listing 4-1Declare Tensor
instances of various dimensions

输出

scalar: 10.0
vector: [1.0, 1.0, 1.0, 1.0, 1.0]
matrix:
[[0.0, 0.0, 0.0],
 [0.0, 0.0, 0.0],
 [0.0, 0.0, 0.0],
 [0.0, 0.0, 0.0]]
tensor:
[[[2.0, 2.0],
  [2.0, 2.0],
  [2.0, 2.0]],

 [[2.0, 2.0],
  [2.0, 2.0],
  [2.0, 2.0]],

 [[2.0, 2.0],
  [2.0, 2.0],
  [2.0, 2.0]],

 [[2.0, 2.0],
  [2.0, 2.0],
  [2.0, 2.0]]]

scalar rank: 0
vector rank: 1
matrix rank: 2
tensor rank: 3

scalar shape: []
vector shape: [5]
matrix shape: [4, 3]
tensor shape: [4, 3, 2]

请注意,Tensor是一个泛型类型,需要我们为占位符类型Scalar传递类型。占位符类型Scalar的类型是在初始化期间在尖括号中提供的,尖括号允许我们在那个Tensor实例中存储指定Scalar类型的元素。例如,清单 4-1 中声明的所有张量对于Scalar都有一个Float类型。注意Scalar是符合TensorFlowScalar协议的Tensor的类型占位符,因为Float符合那个协议,我们可以将Scalar设置为Float类型。

在清单 4-1 中,我们声明了不同等级和形状的各种张量,然后打印出来用于演示。注意,Tensor类型有各种各样的初始化器可以灵活初始化。我们声明scalarvectormatrixtensorTensor的实例。scalar实例的rank为 0,而shape为[ ]。vector实例的rank为 1,而shape为[5]。matrix实例有 2 的rank和【4,3】的shape,即四行三列。tensor实例有 3 的rank和[4,3,2]的shape。所有这些情况都可以在图 4-1 中看到。

接下来,我们讨论 TensorFlow 中的数据集加载。

4.2 数据集加载

在撰写本文时,TensorFlow 允许加载图像和文本域中的各种数据集。但是我们会关注图像数据集。在加载这些数据集之前,您需要在 Xcode Swift 包的 Package.swift 文件中添加 swift-models 包,如下所示,或者您可以在 Xcode 项目设置中添加包 URL:

.package(name: "TensorFlowModels",
    url:
    "https://github.com/tensorflow/swift-models.git", .branch("master"))

如果你正在使用谷歌实验室,那么在你的 Jupyter 笔记本的顶部写下以下声明:

%install '.package(url: "https://github.com/tensorflow/swift-models", .branch("master"))' Datasets

这将加载 tensorflow/swift-models 存储库,并仅构建数据集库。但是如果你想建立其他的库,比如 TrainingLoop 和 Checkpoints,那么就把它们写在 Datasets 被写的地方,但是用一个空格分开。现在,我们可以导入数据集库,如下所示:

import Datasets

在整本书中,我们隐含地假设这个 import 语句是在我们加载任何数据集的地方编写的。现在让我们看一下与数据加载相关的一些概念。

4.2.1 时期和批次

有两个与数据集采样相关的主要概念,即时期和小批量(或简称为批量)。

批次是一组单独的数据样本,其中批次大小定义了该批次中样本的数量。例如,批量大小为 64 的一批图像(每个图像的形状为[256 × 128 × 3],其中 256 是高度(或行数),128 是宽度(或列数),3 是颜色通道数)的形状为[64 × 256 × 128 × 3]。在训练期间,一批数据样本与相应的目标标签一起通过模型进行预测(如果我们正在进行监督学习)。

epoch 是模型批量体验整个数据集的次数。单个历元包含一系列不同的多个批次(形成整个数据集),我们在训练样本批次的过程中对其进行迭代。对于随机学习,我们通常在每个历元迭代期间洗牌。

let dataset = MNIST(batchSize: 64)
let epochCount = 2

epochLoop: for (epochStep, epoch) in dataset.training.prefix(epochCount).shuffled().enumerated() {
  batchLoop: for (batchStep, batch) in epoch.enumerated() {
    let data = batch.data
    let label = batch.label
    print("epochStep: \(epochStep) | batchStep: \(batchStep) | data shape: \(data.shape) | label shape: \(label.shape)")
    break epochLoop
  }
}

Listing 4-2Demonstrate epochs

and batches

输出

epochStep: 0 | batchStep: 0 | data shape: [64, 28, 28, 1] | label shape: [64]

让我们来分解清单 4-2 中发生的这么多事情。首先,我们在dataset常量中加载批量为 64 个样本的MNIST数据集。然后我们宣布epochCount中的纪元数量为 2。

首先,我们有一个标记为epochLoop的纪元循环。我们使用prefix(_:)实例方法遍历datasettraining实例计算属性,该方法接受历元数(这里是epochCount,并在每个迭代步骤中依次返回一个batch数据点。在每一步,我们还用随机抽样的shuffled()实例方法打乱批次顺序。

然后我们有一个标记为batchLoop的批处理循环。我们遍历一个epoch实例中的每个batch。在batchLoop的主体中,我们从batch实例中提取datalabel实例存储的属性。然后我们打印出这两者的shape以及epochLoopbatchLoop的迭代步骤。

我们在两个循环中都使用了enumerated()方法来获取序列中迭代元素的索引。我们为for-in循环定义了epochLoopbatchLoop标签,作为控制流语句的参考。你可以把epochLoopbatchLoop看作是循环的名字。如果我们简单地编写了break语句,后面没有任何带标签的循环语句,那么 epoch 循环将执行两次,batch 循环将执行一次,也就是说,这将只停止 batch 循环执行多次。通过编写break epochLoop,我们告诉编译器简单地停止 epoch 循环本身的迭代,并执行其右花括号后的代码。

这里,我们加载 MNIST 数据集(LeCun,1998),它是从 0 到 9 的手写数字(每个图像一个)及其相应标签的灰度图像的集合。它们是由不同的人写的。正如我们在前面的清单中从shape看到的,每张图像的高度和宽度都是 28 像素,只有一个颜色通道使其成为灰度。这个数据集中的每个样本都是一个表示图像像素值(如Tensor<Float>)的data和其对应的label值(如Tensor<Int32>)的元组。

机器学习算法的另一个重要部分是接下来讨论的模型定义。

4.3 定义模型

我们可以很容易地定义模型架构,其属性可以在训练过程中进行区分。TensorFlow 中定义模型的方式主要有两种,一种是使结构符合特殊协议,另一种是使用受 Keras 启发的Sequential结构。

神经网络协议

TensorFlow 提供了两种协议,即LayerModule,用于定义神经网络。可区分的模型定义结构必须符合这些协议中的任何一个。这些协议要求我们提供模型的InputOutput类型的实现为typealias,定义callAsFunction(_:)方法,并声明至少一个Layer -或Module-符合或Sequential实例属性,其参数将在训练期间更新。

例如,让我们定义一个称为 LeNet 的卷积神经网络(LeCun 等人,1998)。

struct LeNet: Layer {
  typealias Input = Tensor<Float>
  typealias Output = Tensor<Float>

  var convBlock = Sequential {
    Conv2D<Float>(filterShape: (5, 5, 3, 6), activation: relu)
    MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    Conv2D<Float>(filterShape: (5, 5, 6, 16), activation: relu)
    MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
  }
  var flatten = Flatten<Float>()
  var denseBlock = Sequential {
    Dense<Float>(inputSize: 16 * 5 * 5, outputSize: 120, activation: relu)
    Dense<Float>(inputSize: 120, outputSize: 84, activation: relu)
    Dense<Float>(inputSize: 84, outputSize: 10, activation: identity)
  }

  @differentiable
  func callAsFunction(_ input: Input) -> Output {
    input.sequenced(through: convBlock, flatten, denseBlock)
  }
}

Listing 4-3Define the LeNet model by conforming to the Layer protocol

我们使用 Swift 的关键字typealias定义了两个类型别名。这让我们可以在任何可以使用现有类型的地方使用现有类型的新名称。我们为Tensor<Float>类型定义了InputOutput类型名称。然后我们定义多个符合Layer协议的神经层,例如FlattenDenseConv2D。这里,Dense是一个密集连接层(在 5.3.1 小节中解释过),Conv2D是一个卷积层(在 6.1 节中解释过),Flatten层只是对Conv2D层(这里)的输出张量进行整形,使其成为秩为 2 的批量向量。然后我们定义可微分的callAsFunction(_:)方法,它接受类型Input的输入并返回一个Output类型的值。在主体内部,我们在实例input上使用sequenced(through:)实例方法。sequenced(through:)是在Differentiable协议上定义的协议方法,因此可以被任何符合它的类型访问。它接受逗号分隔的符合Differentiable的实例,并通过它们顺序处理input。也就是说,在这里,input首先由convBlock实例处理,其输出然后由flatten处理,然后其输出最终由denseBlock处理,后者再次返回类型为Tensor<Float>的新输出。这个输出然后由这个函数返回。注意,如果函数体、闭包、计算属性或返回某个值的下标中只有一个语句,我们可以去掉return关键字。

接下来,我们解释这里使用的Sequential结构。

层的顺序

我们可以用 TensorFlow 中定义为结构的Sequential轻松定义一个多层神经网络。你可能会从 Keras 的设计中发现它的相似之处。

我们已经使用Sequential定义了清单 4-3 中的convBlockdenseBlock。这样,我们可以简单地将多个神经层传递到 Sequential,每个神经层在不同的行中,后跟左花括号。我们甚至不需要像在协议一致性中那样定义callAsFunction(_:)实例方法。对Sequential实例的输入从第一层(最靠近左花括号)到最后一层(最靠近右花括号)依次处理。如 5.3.1 小节所述,神经网络中的小序列层被称为神经块,例如,convBlockdenseBlock就是我们所说的神经块的典型例子。

加载数据集并定义模型后,现在让我们看看如何在数据集上训练模型。

4.4 培训和测试

在本节中,我们首先介绍 TensorFlow 中模型可微分参数的检查点。我们还使用定制的训练循环在 CIFAR-10 数据集上训练我们的 LeNet 模型。然后,我们再次用 Keras 风格的训练方法训练我们的模型。

4.4.1 检查点

训练神经网络是一项耗费精力和时间的任务。根据数据集和神经网络的大小,模型训练可以从几分钟到甚至几个月不等!训练找到模型的一组新参数值,对于该组新参数值,数据集具有非常低的损失值和高精度(在分类的情况下)。我们不希望我们花在训练模型上的时间被浪费。所以我们可以把最优的参数值写在磁盘上保存训练进度。这被称为检查点。当我们需要使用训练好的模型进行推理(例如,图像分类)时,我们可以简单地将参数从磁盘读入模型,并通过训练好的模型传递要分类的图像。

TensorFlow 允许我们创建模型的检查点。我们只需要使我们的模型结构符合Checkpointable协议。我们不需要写任何东西,只需要在Checkpointable后面加上左花括号和右花括号(见清单 4-4 ),所有在模型实例上可调用的方法都可以用于检查点目的。这之所以成为可能,是因为在Checkpointable协议中实现了检查点方法。

extension LeNet: Checkpointable {}

Listing 4-4Conform LeNet to the Checkpointable protocol

我们只是用扩展使LeNet符合Checkpointable!让我们声明一个目录的路径,我们希望在这个目录中读写检查点。这是通过从基础模块中定义URL实例来完成的,如清单 4-5 所示。

import Foundation
let checkpointDirectory = URL(
  fileURLWithPath: "/Users/rahulbhalley/Desktop/Checkpoints",
  isDirectory: true)

Listing 4-5Declare directory location for checkpointing the model

我们将isDirectory设置为true,以确保这个位置指向目录而不是任何文件。writeCheckpoint(to:name:)readCheckpoint(from:name:)都可能抛出错误,所以我们将使用一个do - catch块进行错误处理,并用try关键字调用这些方法。我们将在下面训练模型时直接演示这一点,而不是在这里演示。

4.4.2 模型优化

让我们用随机梯度下降来训练我们的 LeNet 并节省检查点。清单 4-6 演示了培训。

// Define the default device
let device = Device.defaultXLA

// Load CIFAR 10 dataset
let dataset = CIFAR10(batchSize: 128, on: device)

// Initialize the LeNet model
var model = LeNet()
model = .init(copying: model, to: device)

// Initialize the optimizer
var optimizer = SGD(for: model, learningRate: 0.01, momentum: 0.9)
optimizer = .init(copying: optimizer, to: device)

Listing 4-6Train the LeNet model and save checkpoints

首先,我们声明一个默认的 XLA 设备,所有的处理都将在这个设备上进行。我们将 CIFAR-10 数据集加载到dataset常量中,并将其放在device上。然后我们在model变量中初始化我们的LeNet,并将其复制到device。最后,我们为model to初始化 SGD 优化器(也复制到device),使0.01learningRate0.9momentum(在 5.6.2 小节中解释)。

func trainingStep(samples: Tensor<Float>, labels: Tensor<Int32>) {
    // Compute gradients
    let 𝛁θmodel = gradient(at: model) { model -> Tensor<Float> in
    let logits = model(samples)
    let loss = softmaxCrossEntropy(logits: logits, labels: labels)
    return loss
  }
  optimizer.update(&model, along: 𝛁θmodel)
}

Listing 4-7Define one training step for the model

在清单 4-7 中,我们定义了一个函数trainingStep(samples:labels:),它接受samples和它们的labels作为参数。它为传递给modelsamples计算logits,然后与labels一起用于计算 softmax 交叉熵损失。gradient(at:in:)函数将model作为at参数标签的一个参数,并接受一个闭包,该闭包计算并返回logitslabels之间的损失,这个过程在前一行中描述过。然后计算相对于model所有参数的标量损耗梯度。最后,optimizer沿着其梯度𝛁θ model的方向更新model的可微参数。这结束了一个训练步骤。

func trainingLoop(epochCount: Int = 5) {
  epochLoop: for (epochStep, epoch) in dataset.training.prefix(epochCount).enumerated() {
    batchLoop: for (batchStep, batch) in epoch.enumerated() {
      // Get data
      let samples = Tensor<Float>(copying: batch.data, to: device)
      let labels = Tensor<Int32>(copying: batch.label, to: device)

      // Training step
      trainingStep(samples: samples, labels: labels)
    }

    // Print statistics
    print("epoch: \(epochStep + 1)/\(epochCount)\ttest accuracy: \(testAccuracy)")

    // Write checkpoint
    do {
      try model.writeCheckpoint(to: checkpointDirectory, name: "\(type(of: model))")
    } catch {
      print(error)
    }
  }
}
// Train the model
trainingLoop()

Listing 4-8Define a training loop executable for multiple epochs

输出

epoch: 1/5       test accuracy: 0.5064
epoch: 2/5       test accuracy: 0.5618
epoch: 3/5       test accuracy: 0.5933
epoch: 4/5       test accuracy: 0.6088
epoch: 5/5       test accuracy: 0.6187

在清单 4-8 中,我们定义了一个名为trainingLoop(epochCount:)的训练循环函数,它将历元数作为参数(默认epochCount为 5)。我们已经解释了数据采样(见 4.2 节)。对于每个batch,我们通过将采样的sampleslabels传递给trainingStep(samples:labels:)函数来执行单个训练步骤。在每个epoch之后,我们在验证数据集上打印与模型准确性相关的统计数据,并且我们还将训练好的模型的参数写入到checkpointDirectory目录中,其中name是模型的类型,即 LeNet。因为检查点写入和读取方法都可能抛出错误,所以我们在一个do - catch块中使用了try语句。

var testAccuracy: Float {
  let totalSamples = 10000
  var correct = 0
  for batch in dataset.validation {
    let (data, label) = (batch.data, batch.label)
    let prediction = softmax(model(data)).argmax(squeezingAxis: 1)
    for index in 0..<data.shape[0] {
      if prediction[index] == label[index] { correct += 1 }
    }
  }
  return Float(correct) / Float(totalSamples)
}

Listing 4-9Define a computed property to calculate accuracy of the model on a validation set

在清单 4-9 中,我们声明了一个唯一可获取的计算属性testAccuracy,它计算validation集合上model的精度。我们将验证集中的样本总数设置为 10000,并从零个correct分类开始。遍历validation集合中的所有批次,我们生成prediction,并将其与每个对应的label进行比较,如果predictionlabel匹配,则有条件地将correct变量加 1。请注意,我们在 softmax 激活的 logits 上使用argmax(squeezingAxis:)方法来获取包含具有最高值的元素的向量的索引(请记住第一章中的内容,一个独热编码向量中的每个索引都属于某个类)。最后,我们返回正确分类的分数。

对模型进行训练后,我们得到 LeNet 在 CIFAR-10 上的训练精度等于 0.6187。

4.4.3 训练循环

您可能已经注意到,定义训练步骤和循环、准确性属性以及其他内容会使程序变得稍微复杂一些。我们可以使用 swift-models 包中的 TrainingLoop 库来使我们的程序变得更小。它的灵感也来自 Keras 训练设计。训练循环目前集中在分类任务上。

让我们从头开始复制前面的程序,看看 TrainingLoop 是如何运行的。

import Datasets
import TensorFlow
import TrainingLoop

// Configurations
let epochs = 5
let device = Device.defaultXLA

// Load CIFAR 10 dataset
let dataset = CIFAR10(batchSize: 128, on: device)

// Initialize the LeNet model
var model = LeNet()

// Initialize the optimizer
var optimizer = SGD(for: model, learningRate: 0.01, momentum: 0.9)

// Train and test the model
let trainingProgress = TrainingProgress()
var trainingLoop = TrainingLoop(
  training: dataset.training,
  validation: dataset.validation,
  optimizer: optimizer,
  lossFunction: softmaxCrossEntropy,
  callbacks: [trainingProgress.update])

try! trainingLoop.fit(&model, epochs: epochs, on: device)

Listing 4-10Training LeNet on MNIST with the TrainingLoop library

在清单 4-10 中,我们导入了数据集、TensorFlow 和 TrainingLoop。然后我们声明一些配置,比如将epochs设置为5,将device设置为Device.defaultXLA。数据集、模型(使用清单 4-9 中的LeNet结构)和optimizer被初始化,如清单 4-9 所示。

然后我们声明TrainingProgress类的trainingProgress实例,它跟踪与训练和测试相关的统计数据,比如损失和准确性。我们定义的下一个变量是trainingLoop,它将训练集和验证集、优化器、损失函数(这里是softmaxCrossEntropy)和回调作为参数。然后我们调用trainingLoop上的fit(_:epochs:on:)方法,将模型作为inout参数、epochsdevice进行传递,我们希望对其进行训练。这将执行训练过程并实时打印统计数据,而无需编写复杂的函数。训练完成后,该模型在训练集和验证集上的精度分别为 0.5607 和 0.5625。

注意,当使用 TrainingLoop 进行训练时,我们不需要将modeloptimizer复制到devicefit(_:epochs:on:)方法为我们处理这一切。另一个需要注意的重要事情是,callbacks参数在循环的各种事件的训练过程中执行一系列函数。这里,我们只传递了统计跟踪函数,但是我们将看到如何通过定义我们自己的回调函数来保存检查点!

4.5 从零开始进行研究

在本节中,我们实现了密集层、swish 激活函数、 L 、 1 损失函数和随机梯度下降优化器。这些例子演示了当某些东西还不能开箱即用时,如何实现自己的层、激活函数、损失函数和优化器,以用于研究或生产目的。这些例子也鼓励使用各种协议。我们鼓励您阅读 TensorFlow 的 API 文档 1 和代码库 234 ,以更深入地理解这些和许多其他协议。

4.5.1 层

定义新的神经层类似于我们定义自己的神经网络。我们不使用Sequential,而是让结构符合LayerModule协议。尽管Differentiable协议可以实现与我们在 5.2 节构建线性模型时看到的相同的行为。现在,让我们通过使我们的结构符合Layer协议来定义密集层(见 5.3.1 小节)。

struct DenseLayer<Scalar: TensorFlowFloatingPoint>: Layer {
  typealias Input = Tensor<Scalar>
  typealias Output = Tensor<Scalar>

  var weight: Tensor<Scalar>
  var bias: Tensor<Scalar>

  init(inputSize: Int, outputSize: Int) {
    weight = Tensor<Scalar>(randomNormal: [inputSize, outputSize])
    bias = Tensor(zeros: [outputSize])
  }

  @differentiable
  func callAsFunction(_ input: Input) -> Output {
    matmul(input, weight) + bias
  }
}

Listing 4-11Define the 

dense layer

DenseLayer是一个泛型,其Scalar类型占位符符合TensorFlowFloatingPointDenseLayer本身符合Layer。然后我们定义了Tensor<Scalar> type的两个类型别名,即InputOutput,在可微分的callAsFunction(_:)实例方法中我们用它们作为我们稠密层的输入和输出类型。DenseLayer还有两个Tensor<Scalar>类型的存储属性,即weightbias,是该层的参数。DenseLayer的初始化程序接受层的输入和输出特征的数量。该信息随后用于初始化weightbias参数属性。最后,在正向传递期间,callAsFunction(_:)接受输入,对其执行仿射变换(即,inputweightmatmul(_:_:)函数矩阵相乘,并加上bias),并返回输出。

激活功能

在对输入进行仿射变换之后,我们应用激活函数。许多激活函数分别变换张量的每个元素。图 4-2 所示的 swish 函数(Ramachandran 等人,2017)是激活函数的一个很好的例子。它由以下等式给出:

$$ \mathrm{swish}(x)=x\cdot \sigma \left(\beta x\right) $$

img/484421_1_En_4_Fig2_HTML.png

图 4-2

β = 1 及其导数(橙色)的 swish 激活函数图(蓝色)

这里, β 是一个可学习的或常数项。这一项通常设置为等于 1。因此 swish 函数变成如下:

$$ \mathrm{swish}(x)=x\cdot \sigma (x) $$

清单 4-12 演示了如何声明自己的激活函数(这里是 swish 激活函数)。

@differentiable
func swishActivation<Scalar: TensorFlowFloatingPoint>(_ input: Tensor<Scalar>) -> Tensor<Scalar> {
  input * sigmoid(input)
}

Listing 4-12Define the swish activation function

因为我们希望激活函数swishActivation(_:)是可微分的,所以我们用属性@differentiable来标记它。我们声明一个符合TensorFlowFloatingPoint的泛型类型Scalar。这个函数接受input参数并返回Tensor<Scalar>类型的输出。在swishActivation(_:)的主体中,我们将input与经过 sigmoid 函数转换的input按元素相乘,并返回输出。我们可以在任何神经层的输出之后使用这个激活函数。

损失函数

模型参数的值更新的方向由神经网络的预测(也称为 logits)和关于每个参数的相应目标(也称为标签)之间的损失梯度来引导。

在这里,我们描述如何在 TensorFlow 中实现自己的损失函数。我们展示了由以下等式给出的 L 1 损耗的简单实现。L1loss 计算逻辑和标签的每个元素之间的平均绝对误差(MAE ):

$$ \frac{1}{k}\sum \limits_{i=1}^k\left\Vert {t}_i-{y}_i\right\Vert $$

这里, y it i 分别是第 i 个索引逻辑和目标, k 是这些向量中每一个的元素个数。运算符∨。∑计算绝对值,即将向量的任何值的负号转换为正号。清单 4-13 展示了L1 损失的实现。

@differentiable(wrt: logits)
func l1Loss<Scalar: TensorFlowFloatingPoint>(
  logits: Tensor<Scalar>,
  labels: Tensor<Scalar>
) -> Tensor<Scalar> {
  abs(labels - logits).mean()
}

Listing 4-13Define the L2 loss function

这里,l1Loss(logits:labels:)函数将logitslabels作为输入参数,并返回一个类型为Tensor<Scalar>的值,其中Scalar是一个符合TensorFlowFloatingPoint的类型占位符。这个闭包的主体计算logitslabels的对应元素之间的差,然后取其绝对值,并找到它们的平均值。

在训练期间,我们计算关于模型的每个参数的损失梯度,这给了我们最陡上升的方向。我们的目标是最小化损失函数,损失函数简单地由模型的预测和目标组成,因此我们在梯度的负方向上在参数空间中采取小的步骤。这被称为基于梯度的优化,在 5.1 节中讨论。接下来,我们看看如何在 TensorFlow 中定义我们自己的优化器。

优化器

我们可以通过使新的优化器class符合Optimizer协议来定义新的优化器。清单 4-14 通过重新定义随机梯度下降(SGD)优化器证明了这一点。

class SGDOptimizer<Model: Differentiable>: Optimizer
where Model.TangentVector: VectorProtocol & ElementaryFunctions & KeyPathIterable, Model.TangentVector.VectorSpaceScalar == Float
{
  // The learning rate
  var learningRate: Float

  init(for model: Model, learningRate: Float) {
    self.learningRate = learningRate
  }

  func update(_ model: inout Model, along direction: Model.TangentVector) {
    model.move(along: direction.scaled(by: -learningRate))
  }

  required init(copying other: SGDOptimizer<Model>, to device: Device) {
    learningRate = other.learningRate
  }
}

Listing 4-14Define the stochastic gradient descent optimizer

优化器必须始终被定义为一个类。我们定义了符合Optimizer协议的SGDOptimizer类。我们还定义了一个符合Differentiable协议的通用类型Model。然后我们为Model定义一些条件符合。我们说ModelTangentVector必须符合VectorProtocolElementaryFunctions,必须是KeyPathIterable。每个协议之间的&符号(&)将所有这些协议组成一个协议。虽然这实际上并没有创建任何新的协议,但是这个协议组合表现为一个单一的协议。通过这种方式,Model符合VectorProtocol(用于向量运算等等)、ElementaryFunctions(用于算术)和KeyPathIterable(用于能够通过Model实例上的KeyPath迭代其属性)。然后我们还要求ModelTangentVectorVectorSpaceScalar(顾名思义,基本上是向量空间中的标量值)是Float类型。

我们为这个名为learningRate的类声明一个实例属性,它是优化的学习率。我们声明一个初始化器,它将Model类型的modelFloat类型的learningRate作为参数。这里,传递model只是为了找到Model的类型,而不是为了在优化器中使用它。这是预期的行为。讨论请参考 GitHub 问题。 5 然后,更新实例方法简单地在相对于模型参数的损失函数的梯度的负方向上更新model。它以Model.TangentVectorinout Modelalong为自变量。TangentVector存储Model的梯度。inout参数反映传递给函数的实例的变化。模型上的move(along:)实例方法在用learningRate将其缩放至较小值后,在梯度的负方向更新其参数。我们总是将实例传递给前缀为&符号的inout参数。

4.6 总结

本章重点介绍了深度学习编程,并介绍了 S4TF 的 TensorFlow 库。我们从解释如何创建张量实例开始。接下来,我们看到了如何在 TensorFlow 中加载数据集。我们还学习了如何创建深度学习模型,并对其进行训练和测试。我们还创建了模型的检查点。最后,我们学习了如何在 TensorFlow 中出于研究目的从头开始创建层、激活和损失函数以及优化器。在下一章,我们将了解神经网络的基础知识。

五、神经网络

我喜欢胡说八道;它唤醒了我的脑细胞。

—苏斯博士

本章涵盖了神经网络的基础知识,也就是深度学习。我们讨论如下各种基础主题:基于梯度的输入和函数参数优化(5.1 节)、线性模型(5.2 节)、深度和密集神经网络(5.3 节)、激活函数(5.4 节)、损失函数(5.5 节)、优化(5.6 节)和正则化(5.7 节)技术。最后,我们在第 5.8 节总结了这一章。

5.1 基于梯度的优化

在这一节中,我们介绍最大值、最小值和鞍点的概念。接下来,我们介绍输入和参数优化。输入优化将用于寻找函数的最大值和最小值。另一方面,参数优化将用于使用可用的函数映射数据集来查找函数本身。这两种优化在深度学习中都扮演着重要的角色,并将贯穿全书。在这里,我们主要关注在线的基于梯度的学习策略。在第 5.6 节中介绍了用于大型深度学习模型的更有效的梯度下降方法。

我们限制自己研究无约束最优化方法,因为它简单,并且满足我们演示书中提出的深度学习方法的要求。对约束优化感兴趣的读者可以参考(Deisenroth et al .,2020)教材的第 7 章。

最大值、最小值和鞍点

这里,我们考虑一个标量函数 f : ℝ → ℝ.函数在某一点的导数有三种可能的值:正、负或零。在第一种情况下,当导数在某一点为正时,则函数随着输入的增加而增加。在第二种情况下,当导数在某一点为负时,函数的输出随着输入的增加而减少。换句话说,当输入少量增加时,导数的符号给出了函数增加的方向,可以是负的,也可以是正的。在第三种情况下,当输出不随输入的变化而变化时,那么导数在该点为零。

img/484421_1_En_5_Fig1_HTML.png

图 5-1

方程f(x)= 5x3+2x23x描述的函数有一个最大值和一个最小值。函数上一点的正切给出了梯度的斜率

图 5-1 为蓝色函数f(x)= 5x3+2x23x及其衍生函数f'(x)= 15x2 f 上的绿点( af(a)=(3/5,1.44)和( bf(b)=(1/3,0.59)。)(蓝线)分别是函数的最大值和最小值。在这些点上,导数为零,即f'(a)= 0,f'(b)= 0。在水平轴上,从x= 1 开始,函数增加但缓慢减少,直到达到 x = a 。如前所述,这可以通过相同输入范围 x 中的相应红色导数线来验证;导数首先很大,但慢慢减小,直到在点 x = a 处变为零。这是函数 f ()变成零。函数上一点的正切给出了梯度的斜率。

img/484421_1_En_5_Fig2_HTML.png

图 5-2

任意标量函数的最大值、最小值和鞍点的可视化

同理,从 x = ax = b 开始,函数递减半个距离;然后对于另一半,它的变化率增加(但仍然是负的),使得它的斜率开始接近零,导数值也是如此。

函数 f (。)在 f ( a 处具有最大值,因为该函数在 x = a 前后的输出小于点 a 处的输出。形式上, f ( a ϵ)小于f(x=a)其中 ϵ 是一个小正数。另一方面,函数的最小值位于 x = b ,即 f ( b )的值最小。因为 f ( b ϵ)的值大于 f ( b ),所以函数 f (。)在 b 处有最小值。在最后一种情况下,当在一定的输入范围内时,输出 f ( x )保持不变,然后在这些输入值处的导数保持为零,满足条件f'(xϵ)= 0。该范围内的所有点称为鞍点。简单来说,在最大值,最小值,鞍点 x ,函数 f 的导数(。)始终为零,即f'(x)= 0。

最大值、最小值和鞍点的概念(见图 5-2 )不仅仅限于一元函数,也同样适用于高维函数,尽管很难在平面上可视化(如一张纸)。

输入优化

在高中,通常遇到的数学问题如下:找出给定固定函数输出最小值和最大值的输入值。这些值分别被称为函数的最小值最大值,如前所述。我们还知道,函数输出对输入的导数描述了输入增加时输出的变化率。

我们可以利用导数的方向信息,在数值上找到固定函数的最大值或最小值。例如,可以通过在输出相对于输入的导数的负方向上以小步长迭代地更新(或优化)输入值来找到函数的最小值(因为导数给出了函数增加最多的方向)。我们可以把基于梯度的最优化(柯西,1847)方程写成:

$$ {x}_{\left(\tau +1\right)}\leftarrow {x}_{\left(\tau \right)}-\eta {\nabla}_{x_{\left(\tau \right)}}f $$

(5.1)

我们已经在方程 1.8 中遇到了一个更新函数参数的类似方程。这里,我们在多个步骤中迭代更新函数 f 的输入变量 x 的值。在这个等式中, η 是一个在范围(0,1)内的小正数,称为步长(或学习速率,在深度学习文献中, τ 表示时间步长,使得x(τ+1)x 在时间步长( τ + 1)的值

另一方面,有时我们可能需要找到一个函数的最大值。在这种情况下,我们可以简单地以小的步长在与导数相同的方向上移动输入,由下面的等式描述:

$$ {x}_{\left(\tau +1\right)}\leftarrow {x}_{\left(\tau \right)}+\eta {\nabla}_{x_{\left(\tau \right)}}f $$

(5.2)

在基于梯度的优化中, η 项起着非常重要的作用。我们希望找到遵循输入更新的平滑轨迹的最佳输入值。因为导数在某一点上通常有一个大值,所以更新可能遵循一个不规则的轨迹,在最佳值附近表现出有弹性的行为。为了缓解这个问题,我们用 η 项缩小了导数值,这有助于按照平滑的更新轨迹更新值。

在清单 5-1 中,我们将最大化函数f(x)= 5x3+2x23x

var x: Float = 0
let η: Float = 0.01
let maxIterations = 100

@differentiable
func f(_ x: Float) -> Float {
  return 4 * pow(x, 3) + 2 * pow(x, 2) - 3 * x
}

print("Before optimization, ", terminator: "")
print("x: \(x) and f(x): \(f(x))")

Listing 5-1Declare configuration variables and function f(x) = 5x3 + 2x2 − 3x to demonstrate maxima and minima optimization

输出

Before optimization, x: 0.0 and f(x): 0.0

我们先定义前面的函数f(x)= 5x3+2x2—3x。我们通过用@differentiable属性标记它来使它可区分。然后Float类型的输入变量x的初始值被设置为 0,步长η被定义为设置为 0.01 的Float常数。我们可以看到优化前函数的输入输出值为零。

// Optimization loop
for iteration in 1...maxIterations {
  /// Derivative of `f` w.r.t. `x`.

  let 𝛁xF = gradient(at: x, in: { x -> Float in
    return f(x)
  })
  // Optimization step: update `x` to maximize `f`.
  x += η * 𝛁xF
}
print("After gradient ascent, ", terminator: "")
print("input: \(x) and output: \(f(x))")

Listing 5-2Find the maxima of the function f(x) = 5x3 + 2x2  3x

```py

**输出**

After gradient ascent, input: -0.5999994 and output: 1.4399999


清单 5-2 展示了寻找最大值的优化过程。我们迭代`maxIterations`,逐渐优化输入`x`。在每个迭代步骤中,我们计算函数`f`相对于输入`x`的导数,并将其存储在常数𝛁 `xF`中。使用`gradient(at:in:)`功能计算导数。参数标签`at``in`将输入`x`和一个返回标量的闭包作为参数。闭包以`x`为参数,返回`Float`计算`f(x)`。Swift 自动为我们计算出`f`相对于`x`的导数。输入`x`的优化更新步骤简单地将`η`缩放的导数加到自身上。我们可以很容易地验证优化的输入值`x`非常接近函数`f(x)`的最大值,如图 5-1 所示。

初始化可优化变量的值时必须谨慎。如果我们将`x`初始化为 1,那么就不可能找到函数 *f* ( *x* )的局部最大值,因为它的全局最大值在无穷远处。需要注意的是,在最小化的情况下,在深度学习的背景下,我们实际上希望找到函数的全局最小值(或给定数据集的最小值函数),但实际上我们只能找到与其更接近的局部最小值。所以这个函数不是一个很好的例子,但仍然展示了,在简单的标量实值空间中,当我们训练大型深度学习模型时,数百万或数十亿个变量可能会发生什么。

尽管在步长`η`前加一个负号来计算最小值很简单,但在清单 5-3 中,我们来看看 Swift 中微分闭包的优秀设计。这也可以被认为是采用 Swift 进行深度学习的动机之一。

// Optimization loop
for _ in 1...maxIterations {
/// Derivative of f w.r.t. input.
let 𝛁xF = gradient(at: x) { x in f(x) }
// Optimization step: update x to minimize f.
x.move(along: 𝛁xF.scaled(by: -η))
}
print("After gradient descent, ", terminator: "")
print("input: (x) and output: (f(x))")

Listing 5-3Find the minima of the function f(x) = 5x3 + 2x2 − 3x


**输出**

After gradient descent, input: 0.33333316 and output: -0.5925926


我们首先通过再次执行清单 5-1 将所有变量重置为初始值。注意,清单 5-3 中的大部分代码与清单 5-2 中的相似。但是我们在`gradient(at:in:)`函数中为`in`参数标签使用了尾随闭包。我们还省略了返回类型信息,因为编译器可以从上下文中推断出来,也就是说,当我们在闭包体内的`in`关键字后调用`f(x)`时,使用返回值的类型。同样,当函数只有一条语句并且返回某个值时,我们可以省略`return`关键字,使代码可读性更好。此外,闭包的主体只有一行,所以我们把它压缩成一行代码。有关函数和闭包的更多信息,请参见第 3.4 节。

最后,我们在`Float`类型上使用`move(along:)`方法。Swift 对`move(along:)`的文档描述说,“沿着给定的方向移动`self`。在黎曼几何中,这相当于指数地图,在测地曲面上沿着给定的切向量移动`self`。”Swift 类型系统中的每一个可微分类型都自动获得了`move(along:)`方法的实现。该方法将与可微分变量相关联的值`TangentVector`作为参数,并更新变量本身。这里,`f`相对于`x`的导数是𝛁 `xF`,并且具有类型`Float.TangentVector`。我们将它传递给`move(along:)`,在这里𝛁 `xF`首先被负步长`-η`缩放,以找到函数`f`的最小值。经过输入的迭代优化,我们最终近似函数的极小值接近真实极小值。

接下来,我们介绍参数优化,并解释它与输入优化有何不同和相似之处。

### 参数优化

通常,在深度学习中,我们的目的是优化可微函数。这是通过优化函数的系数(也称为*参数*)而不是输入来实现的。我们还得到一组固定的包含输入和目标对的训练数据点。这些对是从一些未知的数据生成概率分布中采样的。我们的目标是使用可用的数据集来近似这个数据生成分布。我们通过最大化函数和数据集参数的对数似然性(见 1.3 节)来做到这一点,因为数据集在统计上代表了数据生成分布。换句话说,我们在由一组可变参数和方程结构限制的函数空间中搜索一个函数,该函数是数据生成函数的良好近似,因此对于给定的一组输入,它预测的输出更接近其对应的目标值。

#### 5.1.3.1 推论

以苹果设备上的照片应用为例。Photos 应用程序使用深度学习模型来处理设备上的图像和视频,以预测对象、场景、人脸、动物等,并允许搜索带文本的媒体内容。这就叫推断。形式上,对于给定的输入样本(例如图像),预测输出(例如图像的类别)的过程被称为*推理*(或*正向传递*)。一个稍微复杂一点的推理例子是语音处理。当你对 Siri、Google Assistant 或其他语音助手说话时,你的语音会被发送到各自公司的服务器(有时在设备上)上进行处理,通过一系列部署的经过训练的自然语言处理(NLP)模型进行预测,这些模型包括但不限于语音识别、语法分析和词性标注。不同模型做出的预测代表不同的东西。例如,语音识别模型预测说出的单词序列;词性标注模型将不同的单词标记、分类或归类为名词、副词等。有趣的是,我们已经在前面的文章中看到了推理的作用。当我们在清单 5-1 、 5-2 和 5-3 中预测最优输入值的标量输出时,我们推断出了我们的模型。描述推理过程的图形见图 5-3 。

![img/484421_1_En_5_Fig3_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig3_HTML.png)

图 5-3

一种推理过程,其中输入 x 被提供给模型函数 f(.)来预测输出 y

但是在我们的模型准备好做出正确的预测之前,它必须通过接下来讨论的基于梯度的参数优化过程来训练。

#### 5.1.3.2 优化

我们首先考虑参数优化的问题陈述。正如前面已经讨论过的,我们在一个数据集中有一组输入和输出对,这些输入和输出对是从一些未知的数据分布中抽取的。设计任何机器学习算法的基本目标都是用我们选择的一些参数化密度函数(也称为*模型*)来近似真正的数据生成函数。换句话说,我们希望学习从输入到它们相应的目标值的映射。

让我们首先澄清输入和参数优化问题之间的区别。为了更清楚地区分这些问题,我们使用同一个函数 *f* (。)作为运行实例。在输入优化中,我们有一个具有固定参数集的函数,我们更新输入值,直到找到给定函数返回零输出的那个值,从而给出函数的最小值或最大值。相比之下,在参数优化中,我们在数据集中获得了一组固定的输入和目标对,以及一个我们自己选择的参数化函数,其系数可以更新。这里,我们的目标是找到一个最能代表输入和目标的映射的函数,换句话说,就是最接近数据生成函数的函数。

![img/484421_1_En_5_Fig4_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig4_HTML.png)

图 5-4

损失函数映射 L(y,t)其中 y = f(x)和 t 分别是预测和目标变量。最终的输出变量 e 称为误差或损失,我们希望将其最小化

乍一看,与输入优化类似,人们可能会考虑通过优化参数来解决这个问题,以便为给定数据集找到最小值函数。在最小化的情况下,可以通过迭代地进行以下更新来找到该函数,直到我们搜索的函数发出的输出值对于给定的数据集接近于零:

![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta {\nabla}_{\theta_{\left(\tau \right)}}f $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ3.png)

(5.3)

这里我们表示函数 *f* 的参数(。)加上一个希腊字母 *θ* (称为θ)。如果我们假设函数形式为*f*(*x*)=*ax*<sup>3</sup>+*bx*<sup>2</sup>+*CX*由系数 *a**b**c* 参数化,那么 *θ* = { *a* 前面的等式单独更新每个参数值。

请注意,等式 5.3 仅考虑输入,而不考虑其对应的目标。这仅仅意味着找到从输入到零的函数映射, *f* : *x* → 0,而我们希望近似映射 *f* : *x**t* ,其中 *x**t* 分别是输入和目标变量。映射*f*:*x**0*并不是真正想要的解决方案,因为它不代表数据集映射。

那么,我们如何找到所需的数据集映射呢?为了找到映射,我们改为在我们的方程中引入损失函数 *L* ( *f* ( *x* ), *t* )来近似未知的数据生成函数映射(见图 5-4 )。损失函数有助于学习我们的模型 *f* 的映射。),间接的。它告诉我们的模型的预测 *y* 距离给定输入 *x* 的目标 *t* 有多远。它将模型的预测值和目标值作为参数,并为变量 *e* 返回一个标量值,表示预测值和目标值之间的距离(或误差)。我们努力使用关于损失函数的每个参数的梯度信息来最小化误差项 e。如果我们通过梯度下降技术优化我们的损失函数以最小化这个误差,我们的预测将逐渐开始接近期望的目标。作为这个优化过程的结果,我们将能够自动找到数据集所表示的函数映射。换句话说,我们将能够在由模型方程描述的有限函数空间中,近似表示数据生成 PDF 的函数,从该函数中对给定数据集进行采样。

我们只需要计算损失函数相对于模型参数的偏导数,然后通过梯度下降过程迭代更新这些参数,如方程 5.4 所述。这样做,直到损失函数发出模型预测和期望目标之间的误差,对于数据集中给定的相应输入样本更接近于零:

![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta {\nabla}_{\theta_{\left(\tau \right)}}\;L\left(f\left({x}^{(i)}\right),{t}^{(i)};\theta \right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ4.png)

(5.4)

因为我们正在处理一个回归问题,使用一个被称为误差平方和的流行损失函数是合适的,它由方程 5.5 :

![$$ {L}_{SOS}\left(\mathbf{x},\mathbf{t};\theta \right)=\frac{1}{2}\;\sum \limits_{i=1}^N{\left(f\left({x}^{(i)}\right)-{t}^{(i)}\right)}² $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ5.png)

(5.5)描述

这个损失函数总是返回一个非负输出。仅当预测等于目标值时,它返回的最小值为零,这意味着预测函数完全复制了数据生成函数的映射。清单 5-4 显示了如何计算标量目标 *t* 和预测 *y* 之间的误差平方和。

```py
@differentiable
func sumOfSquaresError(_ t: Float, _ y: Float) -> Float {
  0.5 * pow(t - y, 2)
}

Listing 5-4Sum of squared errors function

我们用属性@differentiable标记sumOfSquaresError(_:_:)。类似于等式 5.5 ,我们将目标t和预测y之间的差提高到 2 的幂,然后减半。

让我们在清单 5-5 中定义我们的数据生成函数。

/// Data generating function
func g(_ x: Float) -> Float {
  4 * powf(x, 3) + 2 * powf(x, 2) - 3 * x
}

Listing 5-5Data-generating function g(x) = 5x3 + 2x2 − 3x

我们将模型函数声明为一个存储了可微分属性的结构,如清单 5-6 所示。

struct Function: Differentiable {
  var a, b, c: Float
  init() {
    (a, b, c) = (1, 1, 1)
  }
  func callAsFunction(_ x: Float) -> Float {
    a * pow(x, 3) + b * pow(x, 2) - c * x
  }
}

Listing 5-6Declare a model as a Function structure

当我们对这个损失函数的输出 e (称为误差)对预测输出进行偏导数时,我们得到一个简单的偏导数如下:

$$ \frac{\partial e}{\partial y}=y-t $$

(5.6)

如果我们的模型是一个深度神经网络,那么这个误差通过关于先前变量的微分链规则进一步反向传播,然后在链的更深处传播,等等。在 Swift 中,这是通过使用一种称为算法微分的更通用的偏导数计算技术来完成的(参见第 3.3 节)。

在通过损失函数进行参数优化的更有意义的问题公式化之后,参数更新方程 5.3 现在变成如下:

$$ \theta =\theta -\eta;{\nabla}_{\theta};L $$

(5.7)

这里, L 是损失函数;并且类似于方程 5.3 ,我们更新方程 5.7 中的参数 θ 。让我们看看清单 5-7 中的参数优化。

var x: Float = 0
let η: Float = 0.01
let epochCount = 174

// Model
var f = Function()

// Dataset
let inputs = Float)
let outputs = inputs.map{ g($0) }

print("Before optimization")
dump(f)

// Optimization loop
for _ in 1...epochCount {
  for (x, t) in zip(inputs, outputs) {
    /// Derivative of `E` w.r.t. every differentiable parameter of `f`.
    let 𝛁θE = gradient(at: f) { f -> Float in
      let y = f(x)
      let error = sumOfSquaresError(t, y)
      return error
  }
  // Optimization step: update θ to minimize `error`.
  f.move(along: 𝛁θE.scaled(by: -η))
}
print("After optimization")
dump(f)

Listing 5-7Find the minima function having free parameters equation f(x) = ax3 + bx2 − cx for a dataset sampled from function g(x) = 5x3 + 2x2 − 3x

```py

**输出**

Before optimization
▽ ParametersOptimization.Function

  • a: 1.0
  • b: 1.0
  • c: 1.0
    After optimization
    ▽ ParametersOptimization.Function
  • a: 4.9880642
  • b: 2.0008333
  • c: -2.9924128

我们首先定义函数 *g* (。)然后对与范围[1.0,1.0]内的`inputs`相对应的`outputs`数组进行采样,其中每个连续值的差值为 0.01,构成我们的数据集。这里,`map(_:)`是一个在`Collection`协议上声明的实例方法,并采用一个闭包来应用于`Collection`实例的每个元素。

我们的目标是近似数据生成函数的映射。为了实现这一点,我们首先定义一个名为`Function`的可微分结构,它包含三个存储的属性`a``b``c,`,每个属性代表`Function`实例的一个特定系数。用于近似的函数的设计由等式*f*(*x*)=*ax*<sup>3</sup>+*bx*<sup>2</sup>-CX 描述,并已写入`callAsFunction(_:)`方法中。在这里,可以从`callAsFunction(_:)`方法中删除`@differentiable`属性的使用,因为结构本身符合`Differentiable`协议,该协议自动使该函数可区分。我们还声明了一个名为`f``Function`实例。

为了计算预测和目标之间的误差平方和,由方程 5.5 描述,我们将使用来自清单 5-4 的标有`@differentiable`属性的函数`sumOfSquaresError(_:_:)`。

我们将`epochCount`常量设置为 174。为了通过访问相应的输入和输出数据点进行迭代,我们在`for-in`循环中使用了`zip(_:_:)`函数。

在这个循环中,类似于前面的例子,我们计算梯度,但是这次是相对于`Function`的实例`f`的。这里,将`f`作为参数传递给`gradient(at:in:)`意味着计算误差函数相对于`f`实例的所有可微属性的偏导数。我们首先预测输出,并将其存储在不可变实例`y`中,然后传递给目标实例`t`旁边的`sumOfSquaresError(_:_:)`函数。这将返回我们的函数`f`所做预测的误差。在`gradient(at:in:)`函数的右括号之后,我们得到 e 相对于所有系数`a``b``c`的梯度,这些系数存储在类型`Function.TangentVector`的𝛁 `θE`实例中。回想一下第三章中的内容,即`TangentVector`是可微分数据类型的关联类型;这适用于所有可区分的基本类型和自定义类型。最后,我们利用存储在𝛁 `θE`中的梯度信息来更新`f`实例中的可微分属性。如前所述,这样做 174 次,最终非常接近数据生成函数 g(x)。

通过转储`f`实例的值,我们可以看到其系数的值非常接近函数 *g* ( *x* )。因此,我们在误差函数的帮助下,通过参数优化成功地近似了数据生成函数。

![img/484421_1_En_5_Fig5_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig5_HTML.png)

图 5-5

线性回归模型 y = wx + b 的计算图,其中我们还计算了损失 e = L(y,t)。(a)显式图(示出仿射变换)和(b)隐式图(假设来自(a)的操作)。在后面的图中,采用(b),我们隐含地考虑仿射变换。此后,我们将对图形中的操作进行颜色编码

接下来,我们讨论一些处理回归和分类问题的基本线性模型。

## 5.2 线性模型

线性模型是最简单的神经网络形式,可以执行回归和分类任务。尽管它们无法学习高度复杂的数据集映射,但它们确实适用于较简单的情况;由于体积小,它们处理输入的速度很快。

神经网络模型本质上是在给定输入随机变量 *x* 的情况下,对输出随机变量 *y* 的概率分布进行建模的框架,即 *P* ( *y* | *x* ),其中在给定相同样本 *x* 的情况下, *y* 必须更接近目标 *t* 。这种说法是有效的,适用于 1.2 节简要讨论的各种机器学习。

### 5.2.1 回归

回归任务与预测给定输入实值变量的输出实值变量有关。这里,输入和输出都是张量,可以有任何想要的维数。有趣的是,我们已经在前一节研究了回归。在本节中,我们将讨论各种不同容量的简单回归模型,这将进一步向我们介绍机器学习中偏差和方差的重要概念。我们将主要讨论线性和多项式模型。偏差和方差的权衡有助于我们选择模型可能的正确容量来解决给定的问题。在这里,我们介绍非常简单的回归模型,旨在预测标量输出。

#### 5.2.1.1 线性回归

我们从回归任务的最简单模型开始,称为*线性模型*(如图 5-5 所示)。它表示输入和输出实值标量之间的一元函数映射 *f* : ℝ → ℝ。顾名思义,这个模型学习了输入 *x* 和输出 *f* ( *x* )标量之间的一个线性关系,这个关系在几何上表示一个平面上的一条直线。线性模型由下面的等式给出:

![$$ f(x)= wx+b $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ8.png)

(5.8)

这里,方程 5.8 描述的是二维空间(或平面)中的直线。在机器学习的上下文中,术语 *w**b* 分别称为*权重*(或*斜率*)和*偏差*(或*截距*),统称为*参数*。这个偏倚术语不应与 1.5 节中讨论的统计偏倚相混淆。

我们将线性模型拟合到方程 5.8 。参见清单 5-8 。

```py
struct LinearModel: Differentiable {
  var w, b: Float
  init() {
    (w, b) = (1, 1)
  }
  func callAsFunction(_ x: Float) -> Float {
    w * x + b
  }
}

Listing 5-8Declare a linear model

我们已经声明了一个由权重项w和偏差项b组成的线性模型结构。接下来,我们将模型与来自数据生成函数 g 的样本进行拟合。).

import Foundation

let η: Float = 0.01
let epochCount = 174

// Model
var model = LinearModel()

// Dataset
let inputs = Float)
let outputs = inputs.map{ g($0) }

print("Before optimization")
dump(f)

// Optimization loop
for _ in 1...epochCount {
  for (x, t) in zip(inputs, outputs) {
    /// Derivative of `E` w.r.t. every differentiable parameter of `model`.
    let 𝛁θE = gradient(at: f) { f -> Float in
      let y = model(x)
      let error = sumOfSquaresError(t, y)
      return error
    }
    // Optimization step: update θ to minimize `error`.
    model.move(along: 𝛁θE.scaled(by: -η))
  }
  print("After optimization")
  dump(f)

Listing 5-9Train to fit the LinearModel to samples from function g(x)

```py

**输出**

Before optimization
▽ LinearRegression.LinearModel

  • w: 1.0
  • b: 1.0
    After optimization
    ▽ LinearRegression.LinearModel
  • w: 4.9880642
  • b: 2.0008333

不幸的是,我们的线性模型无法近似函数 *g* (。)因为它只有两个参数,因此容量较小。现在,我们求助于更高容量的模型来学习映射。

#### 5.2.1.2 多项式回归

我们已经看到,线性模型不能很好地学习映射,因为输入和目标之间的关系不是线性的。因为函数 *g* (。)是 3 阶多项式,我们必须使用多项式函数来实验学习映射。多项式回归模型(如图 5-6 所示)也学习标量值之间的一种映射,即 *f* : ℝ → ℝ,由下式给出:

![$$ f(x)=\sum \limits_{i=0}^m{w}_i{x}^i $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ9.png)

(5.9)

对于 *m* = 3,我们得到与数据函数 *g* 同阶的多项式(。)即给出如下:

![$$ f(x)={w}_0+{w}_1x+{w}_2{x}²+{w}_3{x}³ $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ10.png)

(5.10)

这里, *w* <sub>0</sub> 是一个偏置项,通常写成 *b* 。偏置为 *b* 的输入始终为*x*0= 1,因此我们忽略等式中的显示。我们将它从模型定义中删除,因为它是一个冗余项,并且相对于 *b* 的偏导数始终为零。

![img/484421_1_En_5_Fig6_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig6_HTML.png)

图 5-6

多项式回归模型![$$ y=\sum \limits_{i=0}^m{w}_i{x}^i $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq2.png)的计算图,其中我们还计算了损失 e = L(y,t)

还要注意,线性模型是多项式模型的特例,当其阶数为 1:

![$$ f(x)=b+{w}_1x $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equa.png)

多项式模型比线性模型具有优势,因为它可以具有更多的自适应参数,这给予它更多的学习能力。这意味着该模型可以更精确地近似数据生成函数。

让我们宣布我们的多项式回归模型具有选择任意顺序的灵活性。

struct PolynomialModel: Differentiable {
var weights: [Float]
var bias: Float = 1
@noDerivative var order: Int

init(order: Int) {
weights = Array(repeating: 1, count: order)
self.order = order
}

@differentiable
func callAsFunction(input: Float) -> Float {
var output = bias
for index in 0..<order {
output += weights[index] * pow(x, Float(index))
}
return output
}
}

Listing 5-10Declare a polynomial model


我们定义一个包含作为可微存储属性的`weights`数组和`bias`以及不可微存储属性的`order``PolynomialModel`结构。可微分的`callAsFunction(_:)`实例方法从`Float`映射到`Float`类型值。在该方法中,初始设置为等于`bias``output`局部变量被迭代地添加了一个加权的`input`,其中对应于来自`weights`数组的每个`weight``input`被赋给`weight``index`,因此给出了多项式方程的`result`。最后,我们从这个方法返回`output`。

我们简单地将`model`初始化为清单 5-6 中`PolynomialModel`的实例,顺序设置为 3。我们在相同的数据集上为相同的时期训练模型。通过这样做,我们能够近似数据生成函数 *g* 的系数。).这表明多项式模型比线性模型具有更大的容量,因此,它能够近似更复杂的数据生成分布。

在这里,我们可以很容易地做出原则性的猜测来尝试多项式模型,因为我们可以访问数据生成函数 *g* (。).但是在现实世界的问题中,我们实际上无法事先知道首先尝试哪种模型。我们唯一的选择是训练不同容量的多个模型,并选择一个在测试集上给出最低泛化误差的模型。一个更有原则的方法是研究解决与你相同问题的研究论文,在你的数据集上尝试这些模型,并对算法进行微小的修改,直到你得到好的结果。

![img/484421_1_En_5_Fig7_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig7_HTML.png)

图 5-7

多重回归模型![$$ y=\sum \limits_{i=1}^m{w}_i{x}_i+b $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq3.png)的计算图,其中我们还计算了损失 e = L(y,t)

#### 5.2.1.3 多元回归

之前,我们访问了回归模型,在那里我们学习了标量输入和目标值之间的映射。但有时数据集中的输入样本可能包含多个特征,我们将其表示为向量 **x** ∈ ℝ <sup>*m*</sup> ,目标具有标量值 *t* ∈ ℝ.多元函数*f*:ℝ<sup>*m*</sup>→ℝ描述的模型称为*多元回归模型*,任务称为*多元回归*(见图 5-7 )。我们把这个模型的方程写成:

![$$ f\left(\mathbf{x}\right)=\sum \limits_{i=1}^m{w}_i{x}_i+b $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ11.png)

(5.12)

这里, *w* <sub>*i*</sub> 被称为权重,表示输入变量 *x* <sub>*i*</sub> 在预测正确输出中的重要性, *b* 是偏差项,其中每个变量都是标量。通过将所有输入变量及其对应的权重表示为*m*-维向量**x**=*x*<sub>1</sub>*x*<sub>*m*</sub>**w**=[*w*<sub>1</sub>*w*<sub>*m*这叫做输入 **x** :</sub>

![$$ f\left(\mathbf{x}\right)=\mathbf{wx}+b $$的*仿射变换*(5.13)方程 5.13 中的仿射变换也可以被认为是一个稠密层,我们将在后面看到。现在,我们将研究分类任务的线性模型。### 分类将类别标签分配给输入样本的任务被称为*分类*。我们讨论两种类型的分类任务,即二元和多类分类。#### 5.2.2.1 二元分类法在二元分类中,我们将样本标记为属于两个可能类别中的一个,即 *K* = 2。输出是一个标量值,其中任一类都表示为 1 或 0。为了将预测输出限制在范围(0,1)内,我们应用*s 形函数* n *σ* (。),也叫 *logistic sigmoid 函数*或 *sigmoid 函数*(韩和 Moraga,1995),写法如下:![$$ \sigma (x)=\frac{1}{1+{e}^{-x}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ13.png)

(5.14)

其中 *e* (。)是一个指数函数(见图 3-5 )。sigmoid 函数的图形呈“S”形(见图 5-8 ),很好地限定了范围(0,1)内的输出。

![img/484421_1_En_5_Fig8_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig8_HTML.png)

图 5-8

逻辑 sigmoid 激活函数(蓝色)及其导数(橙色)的图表

这个二元分类模型是一个多元函数 *f* : ℝ <sup>*m*</sup> → {0,1},写为:

![$$ \sigma (y)=\frac{1}{1+{e}^{-\left(\mathbf{wx}+b\right)}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ14.png)

(5.15)

这里,仿射变换的输入 **x** 产生标量预测 logit *y* (网络输出层中激活函数的输入),其中 *y* = **wx** + *b* 。然后,我们对这些 logit 单位应用 sigmoid 函数元素,以归一化范围(0,1)中的预测标量值。因为我们在这个模型的 logit 上应用了 sigmoid 函数,所以我们也称这个模型为*sigmoid 分类**逻辑回归*(一个误称,但在文献中常用)模型。处理样本以产生预测值的过程被称为*正向传播*,因为数据正通过从输入层开始到输出层的一些内部计算(仿射变换后是激活函数)被转换成有用的信息。

这些元素式函数用于限制输出逻辑单元和隐藏单元的范围(如在深度神经网络中),并被称为*激活函数*。它们有助于深度网络从输出层到输入层更好的梯度流动,使学习有效,并形成 5.4 节讨论的主题。

#### 5.2.2.2 多类分类法

在多类分类中,类似于二元分类,我们将样本标记为属于*多个*可能类别中的一个。在这里,班级不止两个。输出是一个向量,其大小与可能的标签(或目标)的数量相同。这里,标签通常用一个热点编码(也称为 1-of- *K* 编码,其中 *K* 是类的数量)来表示,这是一种表示,其中整个向量中只有单个索引的值为 1,而所有其他索引都被设置为零。假设每个指数代表某个类,值为 1 的指数就是样本所属的类。因为我们只希望标签向量的一个索引等于 1,所以我们使用 sigmoid 函数的多类推广,称为 softmax 函数,如下所示:

![$$ \mathrm{softmax}{\left(\mathbf{x}\right)}_i=\frac{e^{{\mathbf{x}}_i}}{\sum_{j=1}^K{e}^{{\mathbf{x}}_j}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ15.png)

(5.16)

这里,为向量 **x** 的第 *i* 个索引元素计算 softmax。分子计算第 *i* 个索引值的指数,分母计算向量所有元素的指数之和。假设*y*<sub>*I*</sub>= soft max(**x**)<sub>*I*</sub>然后所有元素之和 *y* <sub>*i*</sub> ,其中 *i* = {1、…, *K* }和**y**=*y 换句话说,softmax 将向量 **x** 规格化,使得其结果向量 **y** 的所有元素之和等于 1。*

我们通过考虑从 *m-**n* 维向量的向量到向量函数**f**:ℝ<sup>*m*</sup>→ℝ<sup>*n*</sup>映射来构造多类分类器,其中 *m**n* 分别是样本特征和目标类的数量。对于一个输入样本向量**x**∈ℝ<sup>*m*</sup>,我们得到![$$ \hat{\mathbf{x}}=\mathbf{xW}+\mathbf{b} $$其中![$$ \hat{\mathbf{x}}\in {\mathrm{\mathbb{R}}}^{1\times n} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq5.png)为对数,**w**∈ℝ<sup>*n*</sup>为权重,**b**∈ℝ<sup>*n*</sup>为然后,我们将 softmax 应用于 logits ![$$ \hat{\mathbf{x}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq6.png)以产生用于类预测的归一化值的向量![$$ \mathbf{y}=\mathrm{softmax}\Big(\hat{\mathbf{x}\Big)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq7.png)。

![img/484421_1_En_5_Fig9_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig9_HTML.png)

图 5-9

描述前向传播的三层神经网络的计算图。灰色的圆角方形框表示任意参数化函数,也就是说,它可以是任何神经层

因为我们在该模型的逻辑上应用了 softmax 函数,所以我们也将该模型称为 *softmax 分类模型**多类分类器*,或者简称为 *softmax 分类器*。我们将在后面看到,类似于 softmax 分类器,密集神经网络是使用输入向量(或包含多个向量或样本的矩阵)和权重的矩阵乘法来构建的,随后添加偏置向量并应用激活函数(不仅限于 sigmoid 和 softmax ),但是对于多个层,其中前一层的输出被输入到下一层。这里,仿射变换 **xW** + **b** 是密集或全连接的层操作。

## 5.3 深度神经网络

在上一节中,我们参观了各种线性模型。这些模型的局限性在于它们只能近似样本和目标之间的线性关系。但是在现实世界中,就样本特征之间的相关性以及样本特征与目标之间的关系而言,数据集更加复杂。虽然 softmax 模型在像 MNIST 这样的小而简单的数据集上获得了很好的准确性,但我们可以做得更好。所以我们求助于非线性模型,比如深度神经网络。深度神经网络能够解决分类以及回归问题。

*深度神经网络*是一类可学习的模型,其中样本与其目标之间的映射通过称为*神经层*的一系列连锁的多个高维函数来学习。“深度”一词来自于非线性神经网络中有多个层的事实。出于同样的原因,我们将研究这种机器学习模型的领域称为“深度学习”,而不是“机器学习”。有各种各样的神经层,如密集层(接下来讨论),循环和注意层,卷积层(在第六章讨论),等等。请注意,该定义存在一个小例外,即神经网络的所有隐藏层并不总是从输入层到输出层完全链接在一起,而是链接一系列称为*神经块*的几个隐藏神经层,而其他层可能与其他更远的层有跳跃连接(见第 6.3 节)。

让我们考虑一个三层深度神经网络,可以写成**y**=**f**<sup>(3)</sup>(**f**<sup>(2)</sup>(**f**<sup>(1)</sup>(**x**))或**y**=**f**<sup>(3)</sup>**f**这里, **x** 是样本特征向量, **y** 是预测向量。我们将神经层表示为一个向量函数 **f** (。)其中上标的自然数是层在序列链中的位置。在这里,**f**<sup></sup>(。)、 **f** <sup>(2)</sup> (。),以及 **f** <sup>(3)</sup> (。)分别是*输入**隐藏**最终*(或*输出* ) *神经层*。第一层的输出**f**<sup>【1】</sup>(。)返回新的特征向量**f**<sup>(1)</sup>(**x**),该向量成为第二层 **f** <sup>(2)</sup> (。).第二层**f**(2)∘**f**<sup>(1)</sup>(**x**)的输出成为第三层 **f** <sup>(3)</sup> (。).那么最后第三层**f**<sup>(3)</sup>**f**<sup>(2)</sup>**f**<sup>(1)</sup>(**x**)的输出就是预测向量 **y******

任何一层的输出都可以认为是其输入特征到一个新的维数向量所表示的不同特征的变换,例如第一层的输出特征向量 **f** <sup>(1)</sup> ( **x** )就是第二层神经网络的输入特征向量 **f** <sup>(2)</sup> (。).这里,为了清楚起见,我们省略了激活函数,但是每一层的输出都遵循激活函数的元素式应用。输入特征向量到期望输出预测向量的变换被称为*正向传播**正向传递*。除了跳过连接之外,所有深度神经网络都可以用这种方法来描述。

接下来,我们介绍一种最简单的深度神经网络,称为密集神经网络。

### 密集神经网络

一个*密集神经网络*,也称为*密集连接**全连接神经网络*,由一个以上顺序连接的密集层组成。前面讨论的 softmax 模型是只有一层的密集神经网络的特例。深度密集神经网络具有输入层、多个隐藏层和最终层,每个都是密集层类型。*密集层*是简单的矢量到矢量函数,映射到每个层的不同维度,限制是任何给定层的输出维度必须与其后续层的输入维度相匹配。密集层的输出(如图 5-10 所示)通过其输入特征向量的仿射变换进行计算,然后应用激活函数。一层的输出特征用作密集神经层的序列链中的下一层的输入特征。这个过程一直持续到我们计算出最终层的输出,预测出我们关心的结果。

![img/484421_1_En_5_Fig10_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig10_HTML.png)

图 5-10

(a)显式和(b)隐式形式的稠密层(粉红色)的计算图。这里,**y**=**f**(**x**)=**xw**+**b**其中 **x** ∈ ℝ <sup>m</sup>**y** ∈ ℝ <sup>n</sup>**W** ∈ ℝ <sup>m×n</sup>**b**

让我们考虑一个***【L】***-层深度密集神经网络,其中它的每一层在前向传递期间的特征计算可以由以下等式描述:

![$$ {\mathbf{z}}^{(l)}={\mathbf{x}}^{\left(l-1\right)}\times {\mathbf{W}}^{(l)}+{\mathbf{b}}^{(l)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ16.png)

(5.17)

![$$ {\mathbf{X}}^{(l)}={\mathbf{a}}^{\left(l\;\right)}\;\left({\mathbf{z}}^{(l)}\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ17.png)

(5.18)

这里, *l* = 1,…, *L* 是层索引。项 **W** <sup>( *l* )</sup>**b**<sup>(*l*)</sup>分别是第 *l* 层的权重矩阵和偏置向量。乘法符号(×)表示矩阵乘法。术语**x**<sup>(*l*)</sup>**z**<sup>(*l*)</sup>**a**<sup>(*l*)</sup>分别是 *l* 层的特征、激活和激活功能。作为特例,当 *l* = 1 时,我们得到 **x** <sup>(0)</sup> ,这是模型的输入样本特征向量。我们假设所有向量为行矩阵,以使矩阵乘法成为可能,这使得在后面的章节中使用小批量样本进行并行计算成为可能。

我们已经知道,通过密集神经网络顺序地正向传播样本特征 **x** <sup>( *l* )</sup> 来产生预测 **y** 是很简单的。该预测然后通过损失函数 *L* ( **y****t** )以及目标 **t** 来计算标量误差 *e* 。

接下来是计算相对于每层参数的误差梯度∂*e*/∂**w**(*l*)。这是通过执行微分的链式法则来完成的。用于 TensorFlow 的 Swift 使用反向模式算法微分来寻找损失相对于神经网络的每个参数的偏导数。还记得第 2.3.5 小节,关于任何神经层的高维参数的导数可以很容易地用雅可比矩阵表示,方法是在应用链式法则之前对它们进行整形。这为计算神经网络中的偏导数提供了一种有效且通用的方法。

接下来,我们看看一些流行的激活函数。

## 5.4 激活功能

神经网络学习映射具有小实数值的特征和目标张量,因为它们有意义地表示手边的任务。激活函数有助于维持小范围的值,使值不会不受控制地收缩或增长,否则会破坏我们预测标签分布范围内的值的目的。激活函数还有助于模型更好的泛化和更快的收敛。

激活函数用于网络的隐藏层和输出层。当应用于层中时,它们强制执行期望的概率分布。输出单元激活函数的选择基于模型的任务,而隐藏单元激活函数的选择影响神经网络的训练(或收敛)速度和性能。

为了找到更好的激活函数,已经有了很长的研究历史。我们为神经网络的输出和隐藏单元提供了各种重要的激活函数,并讨论了它们的优缺点。激活函数可以是线性的、非线性的,或者甚至是线性和非线性函数的组合。我们主要关注非线性激活函数,因为具有线性激活的神经网络可以由单层网络来描述,这使得表示能力无用。这也破坏了深度神经网络的目标,因为深度的概念是为了帮助网络学习数据集的分层和更丰富的表示。这个目的是通过使用非线性激活函数来实现的,这使得网络能够表示更大范围的函数,并且因此能够学习更复杂的非线性映射。

现在我们更深入地看看各种激活函数及其导数。

### 乙状结肠

图 5-8 中所示的 sigmoid 函数在 5.2.2 小节中引入,并由方程 5.14 给出。sigmoid 是一个标量函数,并应用于张量的元素方面,分别转换其每个元素。它只是将输入的值重新缩放到范围(0,1)。

当逻辑 sigmoid 函数应用于标量 logit 时,它学习伯努利分布,因此有助于学习执行二元分类任务。在这种情况下,输出是标量值,并且两个类 *K* = 2 中的任何一个都可以用 1 或 0 来表示,因为其概率是可能的输入类。在另一种情况下,当我们希望学习多类分类时(当一个样本属于多个类时),我们对预测向量应用 sigmoid。在这种情况下,输出是一个向量值,类用 index 表示(没有一键编码),多个元素可以是 1。

sigmoid 功能也可用于激活隐藏单元。但它不应用于隐藏图层,因为对于大值(正或负)而言,它相对于输入值(来自仿射变换的要素)的导数几乎为零。让我们仔细看看这个问题。

我们知道,在神经网络中重复应用微分链规则意味着将两个连续复合函数的偏导数相乘。现在考虑用 sigmoid 作为激活函数的多个复合神经层函数(例如,密集的)。sigmoid 激活值相对于仿射变换值∂**x**(*l*)/∂**z**<sup>(*l*)</sup>(它们是 sigmoid 的输入)的梯度将需要乘以仿射变换值相对于其权重参数矩阵∂**z**<sup>(*l*)</sup>/∂**w 的雅可比利用链式法则,我们得到下面的等式:**

**![$$ \frac{\partial \boldsymbol{L}}{\partial {\mathbf{W}}^{(l)}}=\frac{\partial \boldsymbol{L}}{\partial {\mathbf{X}}^{(l)}}\cdot \frac{\partial {\mathbf{X}}^{(l)}}{\partial {\mathbf{Z}}^{(l)}}\cdot \frac{\partial {\boldsymbol{Z}}^{(l)}}{\partial {\mathbf{W}}^{(l)}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ18.png)**

**(5.19)**

我们知道,任意密集层的仿射变换 **z** <sup>( *l* )</sup> 是前一层的激活**x**<sup>(*l*—1)</sup>与当前层的权重矩阵**W**<sup>(*l*)</sup>的矩阵相乘,并加上一个偏置向量 **b** <sup>( *然后激活该变换,以产生激活的特征向量**x**(*l*)*</sup>。这简直就是密层操作。

让我们仔细看看前面等式中的中间项,它是激活向量 **x** <sup>( *l* )</sup> 相对于仿射变换向量**z**<sup>(*l*)</sup>:

![$$ \frac{\partial {\mathbf{X}}^{(l)}}{\partial {\boldsymbol{Z}}^{(l)}}={\sigma}^{\hbox{'}}\left({\mathbf{X}}^{\left(l-1\right)}\times {\mathbf{W}}^{(l)}+{\mathbf{b}}^{(l)}\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ19.png)

(5.20)的梯度

这里,*σ*′(。)是由下式给出的 sigmoid 函数的导数,如图 5-8 :

![$$ {\sigma}^{\hbox{'}}(x)=\sigma (x)\;\left(1-\sigma (x)\right)=\frac{e^{-x}}{{\left({e}^{-x}+1\right)}²} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ20.png)

(5.21)

记住:我们使用激活将输入值绑定在一个小范围内。这是因为仿射变换可以使值变大。在误差反向传播期间,我们必须通过导数激活函数来传递这些变换的特征。当我们通过 sigmoid 导数函数*’(*l*)+**b**<sup>(*l*)</sup>在方程 5.20 中输入一个大的(正的或负的)值(例如,**x**<sup>(*l*—1)</sup>×**W**<sup>【l)】),输出可以非常接近零(见图 5-8 )。也就是说,链式法则中关于仿射变换的∂**x**(*l*)</sup>/∂**z**<sup>(*l*)</sup>的激活值在乘以其他导数时给出了损失标量相对于权重矩阵的几乎为零的导数,如下所示:*

 *![$$ \frac{\partial \boldsymbol{L}}{\partial {\mathbf{W}}^{(l)}}=\frac{\partial \boldsymbol{L}}{\partial {\mathbf{X}}^{(l)}}\cdot \left(\mathrm{close}\kern0.17em \mathrm{to}\;0\right)\cdot \frac{\partial {\boldsymbol{Z}}^{(l)}}{\partial {\mathbf{W}}^{(l)}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ21.png)

(5.22)

![img/484421_1_En_5_Fig11_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig11_HTML.png)

图 5-11

ReLU 激活函数(蓝色)及其导数(橙色)的图表

在训练过程中,我们使用雅可比∂*l*/∂**w**(*l*)和∂*l*/∂**b**<sup>(*l*)</sup>损失相对于每层的 *l* 参数 **W** 和 **b** 的梯度,通过基于梯度的优化来更新它们的值当激活函数相对于变换特征的梯度较小时,损失相对于参数的偏导数也较小。这意味着我们几乎没有对参数进行任何更新,因此,神经网络几乎没有学习任何东西!这被称为*消失梯度问题*。而当网络越深入,这个问题就变得严重得多。这是因为许多小值将沿深度相乘。相对于更接近损失函数的层的参数的偏导数将具有小的值,而那些远离损失函数(更接近输入层)的层可能具有零偏导数!这就是为什么使用激活函数是重要的,该激活函数允许更高的偏导数值,并且仍然将变换的特征限制在期望的小范围内。接下来,我们将看看一些更好的激活函数。

### 5.4.2 Softmax

另一个重要的激活函数是 softmax 函数,具体针对逻辑函数,由方程 5.16 给出,在第 5.2 节中介绍。

这个激活函数作用于一个向量并产生另一个向量,该向量的所有元素之和等于 1。它把每个元素(代表一个类)变成一个概率。换句话说,softmax 函数对向量进行归一化。该功能用于学习多类分类任务。

### 5.4.3 ReLU

在神经网络研究的早期,sigmoid 函数被大量用作隐藏单元的激活函数,但现在趋势已经改变。让我们来看看一个著名的激活函数叫做*整流线性单元* (ReLU) (Jarrett et al .,2009;奈尔和辛顿,2010 年;Glorot et al .,2011)由下式给出,如图 5-11 :

![$$ \mathrm{ReLU}(x)=\max \left(0,x\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ22.png)

(5.23)

ReLU 激活作为输入特征 *x* 在范围 0,∞)内的线性函数(其输出等于其输入值),但将负值箝位为零。这个激活功能是隐藏单元的首选。还要注意,ReLU 在零输入时是不可微的。这是因为它的极限从左边接近(较低值通过加一个很小的数 *h* → 0)到零,在零,从右边接近(较高值通过减去一个很小的数 *h* → 0)到零)是不存在的;因此,ReLU 函数不是连续的。(记住一个函数必须是连续的才是可微的。)

简单来说,从左右接近的导数,以及在零点的导数,是不相等的。这使得 ReLU 在零处不连续。但是在软件实现中,为了允许梯度计算,ReLU 的导数被有意设置为等于零,以便网络可以学习。

但是梯度消失问题的解决方案呢?我们再来看 ReLU 的导函数:

![$$ {\mathrm{ReLU}}^{\hbox{'}}(x)=\left\{\begin{array}{l}1,\kern0.96em \mathrm{if}\;x&gt;0\;\\ {}0,\kern0.96em \mathrm{if}\;x\le 0\;\end{array}\right. $$(5.24)我们可以看到,对于大的变换特征,我们不会得到非常小的导数值,而是 1。所以 ReLU 减轻了梯度消失的问题。但是 ReLU 的导数有一个问题。如果变换后的特征值为零或负,则 ReLU 相对于该特征值的梯度将为零。这是有问题的,因为如果许多变换的特征值是零或负的,那么所得到的零偏导数将不会为在用于参数更新的链规则应用中受影响的参数提供任何方向。这就是所谓的*将死再路*或*将死再路*问题。最近对 ReLU 进行了许多改进,接下来将讨论这些改进。### ELUReLU 的一个改进是由(Clevert et al .,2015)研究的*指数线性单元* (ELU),如图 5-12 所示,该单元已被证明在图像识别任务中表现良好。ELU 的方程式如下:![$$ ELU(x)=\left\{\begin{array}{l}x,\kern2.639999em \mathrm{if}\;x&gt;0\;\\ {}\alpha \left({e}^x-1\right),\kern0.84em \mathrm{if}\;x&lt;0\;\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ24.png)

(5.25)

就像 ReLU 一样,如果输入 *x* 大于零,它会发出相同的值。当输入小于零时,输出值略小于零。这样,ELU 在一定程度上缓解了死亡率上升的问题。常数项 *α* 一般设置在 0.1-0.3 之间,即α ∈ [0.1,0.3]。

ELU 函数的导数(如图 5-12 所示)如下:

![img/484421_1_En_5_Fig12_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig12_HTML.png)

图 5-12

ELU 激活函数图(蓝色)及其α = 0.2 的导数图(橙色)

![$$ ELU(x)=\left\{\begin{array}{l}1.,\kern3.479999em \mathrm{if}\;x&gt;0\;\\ {} ELU(x)+\alpha, \kern0.96em \mathrm{if}\;x\le 0\;\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ25.png)

(5.26)

我们可以看到,对于大于零的输入 *x* ,导数为 1。对于小于或等于零的输入,导数是输入的 ELU 加上 *α* 值。通过这种行为,我们避免了死 ReLU 问题。但是 ELU 的一个缺点是,由于指数函数 *e* <sup>*x*</sup> 的引入,ELU 在计算上比 ReLU 要昂贵一些。

### 5 . 4 . 5 leaky 注意到

*漏整流线性单元*(简称 LeakyReLU)激活函数(Maas 等人,2013)避免了 eLU 的指数项问题,由以下等式给出:

![$$ \mathrm{LeakyReLU}(x)=\max \left(0,x\right)+\alpha \cdot \min \left(0,x\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ26.png)

(5.27)

你可以这样写:

![$$ \mathrm{LeakyReLU}(x)=\left\{\begin{array}{l}x,\kern1.2em \mathrm{if}\;x&gt;0\\ {}\alpha x,\kern0.84em \mathrm{if}\;x\le 0.\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ27.png)

(5.28)

如果输入大于零,LeakyReLU 将输出设置为等于输入 *x* 。在另一种情况下,我们用小的 *α* 项(称为*负斜率*)缩放负输入,通常设置为等于 0.01:

![$$ {\mathrm{LeakyReLU}}^{\hbox{'}}(x)=\left\{\begin{array}{l}1,\kern1.2em \mathrm{if}\;x&gt;0\\ {}\alpha, \kern1.08em \mathrm{if}\;x\le 0.\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ28.png)

(5.29)

查看 LeakyReLU 的导数,我们可以看到,如果输入 *x* 小于或等于零,则输出为 1,否则为 *α* 。LeakyReLU 的导数是线性的,对于零值和负值,它简单地等于 *α* (或 0.01,在我们的选择中),因此,避免了死 ReLU 问题。

请注意,与 ReLU 类似,LeakyReLU 在零处也是不可微的,但被有意设为可微的。此外,LeakyReLU 不涉及像 eLU 那样的任何指数函数计算,因此它的计算成本更低。

### 5.4.6 村庄

到目前为止讨论的所有激活都有梯度爆炸问题。最近引入的激活函数称为*比例指数线性单元* (Klambauer 等人,2017)(或简称为 SELU,如图 5-13 ),消失和爆炸梯度问题根本不可能(参见(Klambauer 等人,2017)研究的附录中的定理 2 和 3!在某些情况下,当网络的参数用 LeCun 初始化技术初始化并且网络使用 alpha dropout 时,然后在每个隐藏层中使用 SELU 激活,网络自动地自正常化。并且网络可以被认为是高斯分布。

![img/484421_1_En_5_Fig13_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Fig13_HTML.png)

图 5-13

SELU 激活函数(蓝色)及其导数(橙色)的图形

有趣的是,SELU 的方程式很容易理解:

![$$ \mathrm{SELU}(x)=\lambda \kern0.24em \left\{\begin{array}{l}x,\kern2.52em \mathrm{if}\;x&gt;0\\ {}\alpha \left({e}^x-1\right),\kern0.72em \mathrm{if}\;x\le 0\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ29.png)

(5.30)

如果输入大于 0,则输出为输入 *x* 本身。另一方面,如果输入小于或等于 0,则输出是输入的指数乘以α*α*并减去α*α*。

alpha *α* 和 lambda λ的值呢?(Klambauer 等人,2017)的作者精确计算了它们的值如下,它们是常数:

![$$ a\approx 1.6732632423543772848170429916717 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ30.png)

(5.31)

![$$ \lambda \approx 1.0507009873554804934193349852946 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ31.png)

(5.32)

SELU 的导数也很容易理解 1

![$$ {\mathrm{SELU}}^{\hbox{'}}(x)=\lambda \kern0.24em \left\{\begin{array}{l}1,\kern1.56em \mathrm{if}\;x&gt;0\\ {}\alpha {e}^x,\kern0.84em \mathrm{if}\;x\le 0\end{array}\right. $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ32.png)

(5.33)

对于大于 0 的输入 *x* ,导数输出为 1。对于小于或等于 0 的输入,输出是输入的指数乘以α*α*。

SELU 函数在前文讨论的条件下对神经网络进行自归一化。它还能快速收敛网络。而且根本没有渐变消失或者爆炸。

## 5.5 损失函数

在本节中,我们将探讨一些著名的损失函数,即平方和损失、sigmoid 交叉熵损失和 softmax 交叉熵损失。我们还研究了这些损失函数的导数的有趣性质。

### 平方和

观察以样本分布为条件的目标分布的概率由以下等式给出:

![$$ P\left(\mathbf{t}\left|\mathbf{x}\right.\right)=\prod \limits_{k=1}^KP\left({t}_k\left|\mathbf{x}\right.\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ33.png)

(5.34)

对于回归任务,我们假设目标分布是样本 **x** 的确定性函数,并添加了小的高斯噪声。我们将不进行这个损失函数的推导,但是根据这个假设,我们得到平方和损失函数如下:

![$$ L\left(\mathbf{y},\mathbf{t}\right)=\frac{1}{2}\;\sum \limits_{n=1}^N\sum \limits_{k=1}^K{\left({y}_k^{(n)}-{t}_k^{(n)}\right)}² $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ34.png)

(5.35)

而现在如果只有一个目标变量,那么输出也是标量,平方和损失函数变成如下:

![$$ L\left(\mathbf{y},\mathbf{t}\right)=\frac{1}{2}\;\sum \limits_{n=1}^N{\left({y}^{(n)}-{t}^{(n)}\right)}² $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ35.png)

(5.36)

这里,我们假设应用在 logit *z* 、??(*n*)上的恒等(或线性)激活函数。所以误差对 logit 的偏导数简单地变成如下:

![$$ \frac{\partial e}{\partial {z}^{(n)}}={y}^{(n)}-{t}^{(n)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ36.png)

(5.37)

尽管在给定样本分布的情况下,平方和损失函数可以学习目标分布的条件映射,但是它假设目标属于高斯分布,而表示目标的类本质上是不能用该损失函数建模的二元变量。因此,对于接下来讨论的分类任务,我们求助于交叉熵损失函数。

### 5 . 5 . 2s 形交叉熵

当我们执行二元分类任务时,通过应用 sigmoid 激活函数,神经网络的输出分布被表示为伯努利分布。观察以样本为条件的类的概率由下面的等式给出:

![$$ P\left(t\left|\mathbf{x}\right.\right)={y}^t{\left(1-y\right)}^{1-t} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ37.png)

(5.38)

观察具有模型分布 *P* ( *t* | **x** )的数据生成分布的可能性由以下等式给出:

![$$ l=\prod \limits_{n=1}^N{\left({y}^{(n)}\right)}^{t^{(n)}}{\left(1-{y}^{(n)}\right)}^{\left(1-{t}^{(n)}\right)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ38.png)

(5.39)

当我们取这个似然函数的负对数时(5.39),我们得到由以下等式给出的两类的交叉熵损失:

![$$ L\left(\mathbf{y},\mathbf{t}\right)=-\ln\;l=-\sum \limits_{n=1}^N\left({t}^{(n)}\;\ln\;{y}^{(n)}+\left(1-{t}^{(n)}\right)\;\ln\;\left(1-{y}^{(n)}\right)\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ39.png)

(5.40)

如 1.3 节所述,我们将负对数似然性最小化,这相当于将数据分布的似然性最大化。注意,总误差只是每个预测和目标对的单个误差的总和。第 *n* 个模式(或样本)的误差相对于 logit*z*<sup>(*n*)</sup>的导数结果如下:

![$$ \frac{\partial e}{\partial {z}^{(n)}}={y}^{(n)}-{t}^{(n)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ40.png)

(5.41)

接下来,我们看看 softmax 交叉熵,它允许我们为多类分类任务的输出的多项式分布建模。

### 5.5.3 软件最大交叉熵

当我们执行多类分类任务时,通过应用 softmax 激活函数,神经网络的输出分布被表示为 Multinoulli 分布(见等式 5.16)。观察以样本为条件的类的概率由下面的等式给出:

![$$ P\left({\mathbf{t}}^{(n)}\left|{\mathbf{x}}^{(n)}\right.\right)=\prod \limits_{k=1}^K{\left({y}_k^{(n)}\right)}^{t_k^{(n)}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ41.png)

(5.42)

观察具有模型分布的数据生成分布*P*(**t**<sup>(*n*)</sup>|**x**<sup>(*n*)</sup>)的可能性由以下等式给出:

![$$ l=\prod \limits_{n=1}^NP\left({\mathbf{t}}^{(n)}\left|{\mathbf{x}}^{(n)}\right.\right)=\prod \limits_{n=1}^N\prod \limits_{k=1}^K{\left({y}_k^{(n)}\right)}^{t_k^{(n)}} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ42.png)

(5.43)

当我们取似然函数的负对数时,我们得到由以下等式给出的多个类别的交叉熵损失:

![$$ L\left(\mathbf{y},\mathbf{t}\right)=-\ln\;l=-\sum \limits_{n=1}^N\sum \limits_{k=1}^K{t}_k^{(n)}\;\ln\;{y}_k^{(n)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ43.png)

(5.44)

如 1.3 节所述,我们将负对数似然性最小化,这相当于将数据分布的似然性最大化。注意,这里的总误差仅仅是预测向量和目标向量对中每个类别的单个误差之和。对于第 *n* 个模式(或样本)和第 *k* 个类,误差相对于 logit*z*??(*n*)的导数为:

![$$ \frac{\partial e}{\partial {z}_k^{(n)}}={y}_k^{(n)}-{t}_k^{(n)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ44.png)

【5.45】

请注意,激活和损失函数的选择非常匹配,平方和、sigmoid 和 softmax 交叉熵损失相对于其各自逻辑的偏导数具有相同的形式。

现在我们更深入地看看最常用的优化技术来训练神经网络。

## 5.6 优化

有两种类型的优化,即无约束的(基于梯度的;我们关注这个)和约束(凸优化)。我们的目标是找到函数的全局最小值,以便更好地推广到看不见的数据点,但是对于非凸函数,很难找到全局最小值。所以,在深度学习中,我们试图找到尽可能接近全局最小值(其确切值未知)的局部最小值。在凸函数的情况下,只存在一个极小值,即局部极小值等于期望的全局极小值,但这些函数的表达性较差。

### 梯度下降

通过最大化似然函数来执行参数化模型的训练。这要求我们修改模型的参数,以便模拟产生数据的分布。我们通过使用关于这些参数的损失的梯度信息更新参数值并执行梯度下降来做到这一点,如下所述。

#### 5.6.1.1 批量梯度下降

当我们考虑整个数据集来计算关于模型的损失梯度,并进一步使用它来更新参数时,这种方法被称为*批量梯度下降*。它近似于预测值和目标值之间相对于待降模型的最真实的误差梯度。

对于数据集ⅅ= {(**x**<sup>(*I*)</sup>,**t**<sup>(*I*)</sup>)},关于参数向量 *θ* 的损失由 *L* 给出。标量损失 *L* 在时间步长*相对于参数 *θ* 的梯度为![$$ {\nabla}_{\theta_{\left(\tau \right)}}\;L $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq8.png)。然后,该梯度可用于计算下一时间步*θ*(*τ*+1)的参数更新,其梯度下降方程如下:*

*![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta {\nabla}_{\theta_{\left(\tau \right)}}\;L\left(X;\theta \right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ45.png)*

*(5.46)*

这将在参数更新的每个时间步计算数据集中所有数据点相对于所有参数的导数。这在计算上非常昂贵,因为实际上数据集的大小可以从千兆字节到甚至千兆字节。由于物理设备(深度学习环境中的 CPU、GPU 和 TPU)的内存限制,所有数据都无法加载到内存中。为了解决这个问题,有两种方法来计算近似整个数据集的梯度,即在线梯度下降和随机梯度下降,接下来描述。

#### 5.6.1.2 在线梯度下降

实际上,真实世界的数据集非常大。并且在每个训练步骤计算整个数据集的损失梯度可能对计算要求很高。但是可以选择仅使用单个数据点的误差信息来计算梯度,该梯度可以用于更新模型的参数。对数据集中的每个数据点重复这样做可以提高模型的整体性能。这种方法称为*在线梯度下降*,在第 5.1 节中介绍。

在线梯度下降对于真实世界的应用是有用的,其中来自流的数据点可以用于改进学习系统(Bishop,2006)。这种技术可以用于生成建模,但是对于监督学习,它的标签必须提前知道。在半监督学习的情况下,模型的预测可以用作看不见的输入数据点的目标,这有望改善模型的性能。

表示第 *i* 个数据点 **x** <sup>( *i* )</sup> 为***L***<sup>(*I*)</sup>给出在时间步长 *τ* 为![$$ {\nabla}_{\theta_{\left(\tau \right)}}\;{L}^{(i)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq9.png)时损失相对于参数 *θ* 的梯度。然后,该梯度可用于计算下一时间步*θ*<sub>(*τ*+1)</sub>的参数更新,其梯度下降方程如下:

![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta {\nabla}_{\theta_{\left(\tau \right)}}\;L\left({\mathbf{x}}^{(i)},{\mathbf{t}}^{(i)};\theta \right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ46.png)

(5.47)

这里, *η* ∈ (0,1)是一个小的非零正*步长*也就是著名的*学习率*。由于梯度值可能很大,学习率用于控制所采取的更新步骤的大小。此外,损失函数的梯度给出了损失函数的标量输出值增加最多的方向。相反,负梯度给出了 *L* 下降最多的方向。因为我们希望预测值和目标值之间的误差接近于零,所以我们通过在梯度的负方向上采取小步骤来实现这一点。这就是方程 5.47 给出的梯度下降算法。

#### 5.6.1.3 随机梯度下降

*随机梯度下降* (SGD),也称为*小批量梯度下降*,采用一种激进的方法来计算梯度,结合了两者的优点。它计算一小组样本的梯度。因为数据点的小样本在统计上描述了数据集本身,所以它的梯度也大致类似于近似整个数据集的梯度。

对于一组 *m* 小批量样本(数量通常在 10 到 256 之间),随机梯度下降法计算 *m* 样本𝔻 <sup>( *i:i+m* )</sup> 的偏导数。这给出了下面的小批量随机梯度下降更新步骤:

![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-\eta {\nabla}_{\theta_{\left(\tau \right)}}\;L\left({\mathbf{x}}^{\left(i:i+m\right)},{\mathbf{t}}^{\left(i:i+m\right)};\theta \right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ47.png)

(5.48)

这里,相对于模型的参数,为来自数据集𝔻的 *m* 个样本计算梯度,然后使用该梯度来更新其参数。尽管我们在一个序列中采样了样本的一个子集,但在实践中,我们更喜欢随机采样,以在训练时调用随机性。这也导致模型中的正则化效果。

随机梯度下降技术也比分批梯度下降技术更快地收敛模型,并且表现接近分批梯度下降技术。接下来描述的技术遵循相同的梯度下降思想,但是为了更快更好地收敛,引入了一些修改。

### 势头

用 SGD 学习可能会很慢。将学习速率设置得太低会减慢学习过程,或者甚至会使损失函数陷入局部最小值。另一方面,高学习率虽然使我们更快地降低损失,但它可能使低损失值在最优值附近振荡,甚至可能使训练发散。

动量通过考虑过去梯度的平均值来加速学习。这种方式也抑制了振荡:

![$$ {v}_{\left(\tau \right)}=\gamma {v}_{\left(\tau -1\right)}+\eta\;{\nabla}_{\theta_{\left(\tau \right)}}\;L $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ48.png)

(5.49)

更新如下:

![$$ {\theta}_{\left(\tau +1\right)}\leftarrow {\theta}_{\left(\tau \right)}-{v}_{\left(\tau \right)} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ49.png)

(5.50)

动量项γ的值通常选自一组值{0.5,0.9,0.999}。

接下来,我们看一些正则化技术来推广深度神经网络。

## 5.7 正规化

深度神经网络在各种情况下容易过拟合模型,例如训练样本数量少、参数数量大等。这阻止了模型很好地推广到看不见的样本。但我们可以将正则化应用于机器学习算法的不同组件,如数据集、架构、损失函数和优化方法。*正则化*是对数据、模型、损失函数或优化器进行的任何修改,以减少模型的泛化误差。我们将讨论通过改变算法的不同部分来调整模型的各种策略。

### 数据集

我们知道数据集为学习模型提供了经验。因此,数据集应该展示出我们想要很好地执行的数据的良好统计描述,也就是看不见的数据点。在这里,我们讨论两种技术来实现这一点。

#### 5.7.1.1 数据增加

由于深度神经网络具有大量可学习的参数,该模型可以很容易地根据训练集(如果它的规模很小)进行微调,因此会过拟合。概括模型的最简单方法是在非常大的数据集上对其进行训练。但是,在实践中,很难获得大的标记数据集。我们可以通过将可用样本的修改版本附加到相同的数据集来扩充数据集,而不是寻找更多的数据样本。在图像数据集的情况下,我们可以对每个样本应用以下变换(甚至随机多次),以随机大小裁剪,以一定概率水平和垂直翻转,并改变图像数据点的对比度、亮度和其他配置。(Krizhevsky 等人,2017 年)表明,数据增强有助于正则化深度神经网络(在他们的情况下,用于图像分类任务)。

#### 5.7.1.2 对抗训练

众所周知,最先进的神经网络表现得和人类一样好,在某些情况下甚至更好,例如,在图像中识别物体。虽然这些神经网络功能强大,能够很好地推广到未知样本,但它们仍然容易受到高度工程化的数据点(Szegedy 等人,2013 年)的影响,这些数据点被称为对立的例子。我们已经在 1.6.2 小节中简要理解了对立例子的概念。为了使神经网络对未知样本更加稳健,我们可以生成对立样本并将其添加到训练集中。在对立的例子上训练模型,称为*对立训练*,有助于调整神经网络,从而提高模型在测试集上的性能。

我们简单地提到一个对立样本的正反两面。在撰写本文时,谷歌搜索使用图像验证码接触敌对样本,并要求人类对图像进行分类。这样做是为了防止自动程序反复访问网站,这可能会使服务器过载。对抗性例子的一个负面用例是创建路标的对抗性样本,这些样本在人类看来很好,但却骗过了卷积网络(卷积网络处理来自自动驾驶汽车前置摄像头的图像流)。在这种情况下,对立的例子可能是致命的,例如,如果由于道路正在施工,标志牌意味着向司机发出减速信号,但汽车认为限速为 60 英里/小时,因此很可能会发生事故。

### 架构

机器学习算法的学习组件是模型。该模型非常容易过拟合数据集。为了防止这个问题,我们提出了两种技术来正则化模型。

#### 5.7.2.1 辍学单位

在任何测试集上实现良好准确性的最简单方法是通过对模型集合进行训练,并用每个模型评估每个测试样本,对每个模型的预测进行平均(分别针对每个测试样本)。(Szegedy 等人,2015 年)以六个模型的合奏赢得了 ILSVRC 竞赛。但是训练和推断许多神经网络模型对于现实世界的应用变得不切实际。

一种更简单的技术是 dropout,只需训练一个模型就可以学习指数数量的模型。 *dropout* (Srivastava 等人,2014 年)技术简单地关闭一个层的多个激活单元,在训练之前定义一些概率。这通常通过将二进制掩码(其值是从伯努利分布中随机采样的)与概率 *p* 相乘来实现,其中*p*∈【0,1】。在实践中,我们通常对隐藏层应用 dropout,并将 *p* 的值设置为 0.5 或 0.8。辍学可以被认为是以一种记忆和计算有效的方式学习一个只有一个模型的模型集合。

#### 5.7.2.2·知识蒸馏

另一种避免使用模型集合进行高精度预测的方法是使用知识提取技术。*知识* *蒸馏*的思想是将一个笨重模型(庞大的或一群模型)的知识转移到一个单一的小模型中。(Hinton 等人,2015 年)表明,如果繁琐的模型表现出良好的泛化能力,则在小模型中有可能获得良好的分类精度增益。

当小模型在与笨重模型相同的数据集上独立训练时,它具有体面的性能(小于笨重模型)。但当它使用相同的训练技术(优化器和其他超参数)使用笨重模型的预测作为软目标进行训练时,它的表现优于以前的小模型。这表明,与通过使用一般化的繁琐模型的知识来学习映射相比,小模型通过其自身从零开始准确学习数据集映射的能力更差。在小模型的知识转移中使用的损失函数包括最小化(a)由繁琐模型预测的软目标和小模型预测之间的误差,以及(b)对于给定样本,真实目标和小模型预测之间的误差。

知识提炼有利于生产中的实际应用,因为它有助于快速训练模型,该模型具有更少的推理时间,因此生产中的延迟更低,并且是轻量级的、性能更好的模型。

### 损失函数

损失函数在训练模型中起着重要的作用。它定义了学习算法要达到的目标。但是我们可以对损失函数施加一些约束,以对模型产生正则化效果。

#### 5.7.3.1 标准处罚

我们通常用大量参数训练模型。在实践中,模型的参数可能会根据训练数据集进行微调(即过拟合)以获得正确的预测。如果仔细观察单个参数,您会注意到值在正负方向变得非常大,这通常是过拟合的原因。这个问题可以通过在损失函数中对模型参数的大小施加一些约束来正则化模型来防止。深度学习使用的范数惩罚主要有两种,分别是 *L* <sup>1</sup> 和*L*2 范数惩罚。

损失函数增加的 *L* <sup>1</sup> 定额罚项写为:

![$$ \overline{L}\left(\mathbf{x};\theta \right)=L\left(\mathbf{x};\theta \right)+{\left\Vert \mathbf{w}\right\Vert}_1 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ50.png)

(5.51)

这里,∩**w**<sub>1</sub>是在原损失函数 *L* ( **x** )上增加的权重参数的 *L* <sup>1</sup> 范数; *θ* 以产生修改的损失函数![$$ \overline{L}\left(\mathbf{x};\theta \right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq10.png)。术语![$$ {\left\Vert \mathbf{w}\right\Vert}_1=\sum \limits_i\left|{w}_i\right| $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_IEq11.png)是单个权重绝对值的总和。机构群体非常感兴趣的另一个定额罚款是我们已经在第 1.4.5 小节中讨论过的*1*2 定额罚款。有关详细信息,请参考该指南,有关这些规范重要性的详细信息,请参见第 2.1.4 小节。

### 优化

需要注意的是,如果模型训练时间较长,优化也可能导致模型过拟合。我们可以对优化技术进行修改,以减轻模型中的过拟合问题。

#### 5.7.4.1 提前停车

具有大量参数的神经网络能够过度适应训练数据集。当我们训练模型时,验证误差也随着训练误差的减小而减小。但是在迭代了多个时期之后,验证错误可能会开始增加,从而降低我们模型的性能。防止此问题的最著名、最可靠且最容易的方法是在每次验证误差减小后存储一组参数,并在满足训练终止条件时返回具有最低验证误差的模型的参数。这种技术被称为*提前停止*。当验证误差在预定数量的迭代步骤中没有进一步减小时,也可以终止训练过程。

#### 5.7.4.2 渐变剪辑

我们知道,当梯度消失或爆炸时,模型无法学习,因此无法在看不见的样本上很好地执行,并且不太通用。在第 5.4 节,我们看了一些激活函数,可以帮助减轻梯度消失和爆炸问题。在这里,我们看一个更简单的技术来缓解这个问题,而不需要对模型的架构做任何修改。我们可以通过在进行优化步骤之前对梯度向量进行一些修改来调整优化过程。

同时,深度学习社区使用的最著名的剪切梯度技术是由(Mikolov 等人,2012 年)和(Pascanu 等人,2013 年)引入的。请注意,已知这两种技术会产生类似的结果。

第一种方法(Mikolov 等人,2012 年)是在一个小批量中对所有参数的梯度进行*剪裁(或夹紧、限制),然后执行优化步骤。第二种方法(Pascanu et al .,2013)是*剪切梯度的范数*(见方程 5.52),然后执行优化步骤:*

*![$$ {\nabla}_{\theta }L\leftarrow \frac{\nabla_{\theta }L\cdot v}{\left\Vert {\nabla}_{\theta }L\right\Vert },\mathrm{if}\;\left\Vert {\nabla}_{\theta }L\right\Vert &gt;v $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_5_Chapter_TeX_Equ51.png)*

*(5.52)*

这里, *v* 称为范数阈值,∇<sub>θ</sub>l 为损耗相对于模型参数的梯度。范数通常被选择为欧几里德范数,并且如果它大于阈值,我们执行梯度范数剪裁,而普通梯度剪裁被无条件地应用。在第二种方法中,您可以考虑通过以下方式更新梯度:首先对其进行归一化(即,通过欧几里德范数将其除以梯度向量距原点的长度),然后用梯度阈值项 *v* 对其进行缩放。

#### 5.7.4.3 辍学率

众所周知,基于自适应学习率的优化器能够将损失函数快速收敛到局部最小值,但它们通常会陷入鞍点,从而使优化变得困难。众所周知,具有动量的标准随机梯度下降能够找到好的局部最小值,并且不容易卡在鞍点,但是收敛损失函数非常慢。但是我们希望快速收敛到一个好的局部极小值,而不牺牲任何一个。这可以通过在基于自适应学习率的优化器上使用学习率下降(,Lin et al .,2019)来实现。

*学习率下降*的想法类似于前文讨论的单位下降。我们简单地从每层的参数样本中以概率 *p* 丢弃一组学习率。这是通过在二进制掩码(从伯努利分布采样)和该层中每个参数的学习率之间应用哈达玛乘积来实现的。

学习率下降有助于找到损失函数下降的新的随机路径,使得收敛对鞍点和不良局部最小值更鲁棒。

## 5.8 摘要

在这一章中,我们学习了与神经网络相关的各种概念,从基础开始,到高级主题结束。我们首先理解了输入和参数优化之间的区别。我们研究了与回归任务(即线性、多项式和多元回归模型)和分类任务(即二元和多类分类模型)相关的各种线性模型。然后我们理解了深度神经网络,或者更确切地说是密集神经网络。然后我们访问了各种激活函数,并对其进行了分析。我们还研究了三种常用的损失函数。然后重点介绍了不同的基于梯度的优化技术和正则化技术。

我们现在将在下一章研究卷积神经网络,它是专门为解决深度学习的计算机视觉问题而设计的。*

# 六、计算机视觉

> 所有的模型都是错的,但有些是有用的。 <sup>1</sup>
> 
> *——乔治盒*

在本章中,我们将了解深度学习在计算机视觉任务中的作用。在第 6.1 节,我们讨论一种特殊的神经网络,称为卷积神经网络,旨在解决计算机视觉问题。与第五章讨论的密集神经网络相比,它有一些主要优势(第 6.2 节)。我们介绍一种减轻梯度消失问题的技术(6.3 节)。在 6.4 节中,我们实现了一个深度卷积神经网络来执行图像分类任务。最后,我们在第 6.5 节对本章和本书进行了总结。

## 6.1 卷积神经网络

在本节中,我们将讨论一类重要的神经网络,称为卷积神经网络(LeCun 等人,1989)。卷积神经网络,也称为卷积网络或简称为 ConvNet,被发明来处理具有一些空间局部信息特征的网格状数据。例如,在 2D 图像中,不同位置的小块可能包含球、脸和其他东西。诸如语音的 1D 时间序列数据可能包含不同时间帧片段中的语音。卷积网络学习许多称为过滤器的小参数张量,这些张量有助于提取基本特征。在图像环境中,特征包括边缘、曲线、对象形状、颜色渐变等,而在音频波形环境中,语音的基本特征可能包括音素、语调、音色等。

![img/484421_1_En_6_Fig1_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig1_HTML.png)

图 6-1

(a)RGB 图像和(b)双声道立体声音频张量卷积运算的摘要示意图

在第五章中,通过引入密集神经网络,我们进入了深度神经网络领域。基于密集网络的图像分类器适用于维数较小的图像数据,例如来自 MNIST 数据集的 784 维。但是当输入图像尺寸增加时,密集网络中的参数数量增长非常快。我们已经看到,密集层中的参数是从前一层中的每个单元( *m* 个单元)到当前层中的每个单元( *n* 个单元)的连接。这里,从前一层到当前层的连接总数是 *m* × *n* 。如果我们通过增加层的任何一个单元来增加层的容量,那么参数的数量会增加得非常快。例如,考虑尺寸为 32 `×` 32 `×` 3 的图像,其总共具有 3072 个特征(像素值)。现在,如果第一层有 1024 个特征单元,那么参数的总数是(3072`×`1024)+1024(额外的 1024 是偏差)= 3146752。目前,从内存的角度来看,这似乎是一个可管理的参数数量。但是,如果我们考虑一个合理大小的图像,那么参数的数量很快就达到大约 1.54 亿,确切地说是 154,141,696,这只是第一层!这是密集层的固有性质,其不允许密集神经网络随着大的输入维度大小和隐藏激活中的大量特征而缩放。在现实世界的计算机视觉应用中使用密集网络变得很困难,因为在实践中,图像数据点通常很大。卷积层通过其固有的设计(6.2 节)避开了这些问题,这促使我们在密集网络上使用卷积网络来处理大维度的数据。

与密集层中的矩阵乘法不同,卷积层使用一种称为卷积的数学运算,这使这些网络被命名为卷积神经网络。这个简单的操作是卷积网络与深度学习文献中的其他神经网络如此不同的原因。一般来说,如果网络中至少有一层使用卷积运算,则神经网络称为卷积网络(Goodfellow 等人,2016)。基于深度学习的当前研究趋势,这个定义可能并不总是有助于对网络进行分类,因为整个网络可能由不同种类的神经层组成。例如,LSTNet (Lai et al .,2018)和 Tacotron 2 (Shen et al .,2018)等网络包含卷积层和递归层,这混淆了这些网络按照此定义分类为递归网络或卷积网络。另一方面,注意机制用于密集网络(Vaswani 等人,2017)甚至卷积网络(Parmar 等人,2018;李等,2019)。我们强调,对网络的更好描述是,它由某些块组成(包含少数同构或异构类型的神经层实例),而不是基于单层操作来命名神经网络。如果一个神经网络在每一层中使用相同的操作,我们可以将该神经网络命名为以该操作为前缀的神经网络,例如卷积神经网络、循环神经网络等等。

特征和核张量具有相同的深度维度大小,但是不同的空间维度大小,其中核在空间上更小。每个内核生成一个深度维度大小为 1 的单一特征图(对于(a)以绿色阴影显示,其中灰色阴影由其他过滤器创建,对于(b)以浅绿色显示)。在(b)中,蓝色特征重叠以产生绿色输出标量特征,而在(a)中,棋盘核应用于输入特征以产生标量特征,当完全卷积时形成矩形形状。

在下文中,我们将解释卷积网络中使用的各种层,以及对数据点维度进行下采样和上采样的方法。

### 卷积层

称为*卷积层*的层的基本要求是应用卷积运算。我们首先用简单的语言(没有数学复杂性)解释卷积运算以及相关的超参数,然后给出计算输出维度大小的公式。

卷积运算是具有不同空间但相同深度维度大小的两个张量的函数。卷积操作通过将*特征*张量的空间小部分与*滤波器*张量(也称为*内核**参数*)重叠开始。因为两个张量的深度是相同的,所以过滤器沿着特征张量的整个深度重叠其空间维度内的所有特征值(见图 6-1 )。然后,我们在这些重叠值之间应用 Hadamard 乘积,以产生一个新的临时张量,其维数与滤波器的维数相同。现在,我们对这个临时张量中的所有值求和,以输出一个标量值。这相当于滤波器和输入特征张量重叠之间的点积运算。然后,我们在特征张量上空间地跨越(或移动)相同的滤波器,以重叠另一组值。(在图像的上下文中,stride,2)将使过滤器在 x 轴上移动 1 个像素值,在 y 轴上移动 2 个像素值,一次一个方向。但实际上,滤波器通常在所有轴上步进相同的量)。现在,我们再次取滤波张量和特征张量的重叠值的点积来生成另一个标量数。重复这个过程,直到滤波器一次跨过整个特征张量。这产生深度维度大小为 1 的特征图。我们将很快学会如何计算它的空间维度大小。这被称为*卷积运算*。换句话说,卷积是通过跨越滤波器直到整个特征张量被遍历一次,在被滤波器张量重叠的特征张量之间的点积的迭代应用。音频和图像数据点卷积运算的具体例子分别见图 6-2 和 6-3 。

![img/484421_1_En_6_Fig2_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig2_HTML.png)

图 6-2

输入 **x** 和滤波器 **f** 矢量之间的卷积运算的例子,其产生矢量 **y**

这里,滤波器首先对输入的第 0 和第 1 个索引处的值执行点积,并在输出向量的第 0 个索引处产生 0。然后,滤波器向右跨两步,再次对重叠的输入值执行点积,并在输出的第一个索引处产生 4。并且对输入的剩余值重复相同的过程。

![img/484421_1_En_6_Fig3_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig3_HTML.png)

图 6-3

输入矩阵 **X** 和滤波器矩阵 **F** 之间的卷积运算示例,其产生矩阵 **Y**

这里,filter 首先点乘左上角的 2x2 方阵,并在结果矩阵中产生标量(在(0,0)索引处)。然后,滤波器在 x 轴或 y 轴上跨两步,再次执行点积。重复这个过程,直到它卷积整个输入矩阵。

由于深度学习的想法是学习数据中复杂模式的有用表示,所以我们在卷积中不仅仅使用一个滤波器,而是使用多个滤波器。如果每个滤波器产生单个特征图,那么 *n* 个滤波器,每个滤波器对特征张量应用一个卷积运算,产生对应于每个滤波器的 *n* 个特征图。这些特征图沿着深度维度堆叠,以创建输出特征张量。输出特征张量的深度维度大小等于卷积层中的过滤器数量。

注意,从数学上讲,卷积运算只涉及一个滤波器和一个输入张量。但是在深度学习的上下文中,卷积层具有一个特征张量,并且可以具有多个滤波器,其中在特征张量和每个滤波器之间分别应用卷积运算,以产生深度维度大小与滤波器数量相同的新特征张量。

### 尺寸计算

现在我们知道了卷积运算是如何工作的。这里,我们将讨论当滤波器在给定的特征张量上卷积时,新的特征张量的输出维数的计算。

让我们从假设任意输入特征张量 t∈ℝ<sup>*a*×*b*×*c*×*d*</sup>开始,其中 *a**b**c**d* 是维度大小。对输入张量 t 的卷积运算的应用产生了另一个特征张量 t’∈ℝ<sup>*a*’×*b*’×*c*’×*d*</sup>其中*a**b**c*’和*d*’是对应的输出维数

因为特征张量的选择是任意的,所以可以选择用 2D 音频张量、3D 图像张量或具有特定维度大小的其他张量来替换它。为了避免混淆,我们将假设图像张量 I∈ℝ<sup>*h*×*w*×*c*</sup>和音频矩阵**a**∈ℝ<sup>*t*×*c*</sup>代替任意张量 t,以更具体地理解输出张量维数的计算。这里, *h、w、c**t* 分别是高度、宽度、通道(或深度)和时间维度尺寸。

假设有两个滤波器![$$ {\mathrm{F}}_{\mathrm{I}}\in {\mathbb{R}}^{f_h\times {f}_w\times c} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_IEq1.png)和![$$ {\mathbf{F}}_{\mathbf{A}}\in {\mathbb{R}}^{f_t\times c} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_IEq2.png)分别用于图像 I 和音频 A 张量,其中 F <sub>I</sub> 是 3 阶张量,**F**A 是 2 阶张量(或矩阵),并且通道 *c* 具有用于图像和音频的单独值。这里, *f* <sub>* h *</sub>*f* <sub>* w *</sub> 是对应于图像张量 I 的高度 *h* 和宽度 *w* 维度尺寸的滤波器尺寸,对于音频张量 A, *f* <sub>* t *</sub> 是沿时间维度的滤波器尺寸*t 让我们也考虑卷积运算的其他超参数,例如步长大小 *s* ,零填充大小 *p* ,以及膨胀因子 *d* 。(不用担心;这些稍后解释。)因此,在定义了所有必需的术语后,使用以下公式计算输出特征张量维度大小:*

![$$ o=\frac{\left(i+2p-\left(d\left(f-1\right)+1\right)\right)}{s}+1 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ1.png)

(6.1)

![img/484421_1_En_6_Fig4_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig4_HTML.png)

图 6-4

图 6-2 和 6-3 中输入周围的零填充示例。这里,(a)的零填充为 2,而(b)的零填充为 1

这里, *i* 是张量期望轴的输入维数大小, *o* 是其对应的输出维数大小。它可以是输入张量的任何维度,例如,在我们的例子中, *h**w**t* 。我们还用 *f* 指定滤波器大小,在我们的例子中,它可以是 *f* <sub>*h*</sub>*f* <sub>*w*</sub> ,或者 *f* <sub>*t*</sub> 。影响输出尺寸大小的其他变量(或超参数)是步幅 *s* ,填充尺寸 *p* 和膨胀 *d* 。在理解输出尺寸大小的计算之前,让我们先熟悉一下这些超参数。

#### 6.1.2.1·斯特雷德

步幅 *s* 是滤波器在输入特征张量上允许的(空间或时间)方向上的移动步长(一次一个)。在图像的上下文中,如果跨距为 2,则过滤器在允许的方向(x 和 y 轴)上移动 2 个像素值,然后点乘重叠。类似地,在诸如音频的时间数据中,步长为 16 的滤波器将在时间上(在 x 轴上)移动 16 个样本,并在重叠之间取点积。所以*步幅*就是滤波器在张量上的移动步长。

#### 6.1.2.2 衬垫

另一个被称为*填充*(或*零填充*)的超参数是沿着所有深度通道的特征张量的空间或时间维度周围的零值的边界。填充 *p* 为正整数值,如图 6-4 所示。

#### 6.1.2.3 扩张

在 2015 年,(Yu 和 Koltun,2015)为卷积层引入了一个新的超参数,称为*膨胀*(也称为 *à trous 卷积*或【带孔卷积】)。没有膨胀的卷积产生的特征张量具有前一层的小感受野。*感受域*是任何先前层中负责特定特征单元预测的特征数量(见图 6-5 )。为了增加感受野,我们需要增加滤波器的大小(或滤波器中参数值的数量)。但是引入卷积层的目标之一是减少内存占用。这就是扩张卷积缓解过量内存分配问题的地方。通过在滤波器中使用*膨胀,我们可以在不增加滤波器中参数数量的情况下增加特征张量的感受域。为了在保持相同数量的参数值的同时增加感受野,我们简单地通过 *d* ∈ ℤ <sup>+</sup> 扩张来隔开参数值。通常,一个过滤器有一个膨胀;当我们增加扩张时,感受野(沿着网络深度在相应层和远处层之间)增加。扩张卷积的示例见图 6-6 。还请注意,除了内存足迹之外,膨胀在过去几年中在各种任务上取得了许多成功,例如图像分割(Yu 和 Koltun,2015 年)、原始音频波形建模(Oord 等人,2016 年 a)、记忆效率以及循环神经网络中消失和爆炸梯度问题的抑制(Chang 等人,2017 年)和关键字定位(Coucke 等人,2019 年),仅举几例。*

![img/484421_1_En_6_Fig5_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig5_HTML.png)

图 6-5

三层 1D 卷积网络中的感受野

所有的滤波器都是 2D 向量,并以步长 1 应用于数据。这里,阴影最后一层的 **x** <sup>(4)</sup> 特征单元的感受野对于 **x** <sup>(3)</sup> 为 2,对于**x**<sup>【2】</sup>为 4,对于 **x** <sup>(1)</sup> 为 8。

![img/484421_1_En_6_Fig6_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig6_HTML.png)

图 6-6

三层 1D 扩张卷积网络中的感受野

这里,**f**<sup>(1)</sup>**f**<sup>(2)</sup>**f**<sup>【3】</sup>的膨胀分别为 1、2、4,是 2D 向量。(过滤器中的浅色区域显示膨胀,并且只是一个空白空间,其中输入的重叠区域没有点积。)所有的过滤器对数据应用步长 1。我们在**x**<sup>【2】</sup>中用 1 个单位补零,以防止**x**<sup>【3】</sup>中分辨率大幅降低。这里,阴影化的最后一层的**x**<sup>【4】</sup>特征单元的感受野对于**x**<sup>【3】</sup>为 4,对于**x**<sup>【2】</sup>为 6,对于 **x** <sup>(1)</sup> 为 12。请注意,在非扩张卷积网络的相应层中,感受野呈指数增长。核的膨胀有效地增加了感受野,而不增加内存需求。

#### 6.1.2.4 的例子

让我们看一个音频张量的简单例子,其形状是ℝ <sup>16384×2</sup> ,其中声道 *c* 是 2,时间长度 *t* 是 16384。这是一个具有 16,384 个样本的双声道音频(或立体声音频)(一个数据点包含 16,384 个标量幅度值)。如果我们将其与形状为ℝ <sup>16×2</sup> 的滤波器矩阵 **F** 进行卷积,步长 *s* 为 4,零填充 *p* 输入张量的边界厚度为 6,膨胀 *d* 为 1,则我们得到形状为ℝ <sup>4096×1</sup> 的输出张量,计算如下:

![$$ {t}^{\prime }=\frac{16384+2(6)-\left(1\left(16-1\right)+1\right)}{4}+1=4096 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equa.png)

卷积后特征张量的新形状是ℝ <sup>4096×1</sup> 。因为我们只使用了一个滤波器,所以输出通道尺寸 *c* 等于 1。

类似地,也可以计算卷积输入图像张量的输出张量形状。让我们假设形状ℝ <sup>28×28×1</sup> 的灰度图像,其中通道 *c* 是 1,宽度 *w* 和高度 *h* 都是 28 像素。filter(*f*<sub>*h*</sub>*f* <sub>*w*</sub> )、stride *s* 、zero-padding *p* 和 exploation*d*的值分别为(5,5)、1、0 和 1。按照这种配置,卷积运算产生输出宽度 *w* 和高度 *h* 尺寸等于 24:

![$$ {w}^{\prime }=\frac{28+2(0)-\left(1\left(5-1\right)+1\right)}{1}+1=24 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equb.png)

在这两个示例中,将膨胀设置为 1 与普通卷积运算相同(默认情况下,将膨胀设置为 1)。

请注意,您应该只使用那些影响特定特征尺寸的过滤器尺寸。例如,在这个公式中,一起使用 *f* <sub>*w*</sub>*w* 来计算 *w* 是合适的,因为 *f* <sub>*w*</sub> 的值影响张量的宽度尺寸 *w* 而不是 *h* 。当 *f* <sub>*w*</sub>*h* 是这个公式的自变量时,这个公式的一个无意义的用法就是试图计算*h*’。

在最近的深度学习模型中,自编码器中的编码器网络和生成对手网络中的鉴别器网络通常使用滤波器大小ℝ <sup>4×4</sup> 、步距(2,2)和填充 1 的卷积。这将尺寸减小了 2 倍。例如,具有指定配置的尺寸为ℝ<sup>1024×1024×*c*的图像被下采样到ℝ<sup>512×512×*c*</sup>大小。有趣的是,当自编码器中的解码器网络和生成对抗网络中的生成器网络使用这些相同的超参数进行转置卷积运算时,我们得到了一个双维度大小的输出特征张量。也就是说,如果一幅图像的维数为ℝ <sup>512×512× *c*</sup> ,那么在应用这个转置卷积后,我们得到一个ℝ<sup>1024×1024×*c*</sup>大小的特征张量。</sup>

实际上,大小为ℝ <sup>2×2</sup> 的滤波器也非常常用。特别是在视觉模型中,通常使用较大的过滤器尺寸和跨度会导致性能下降。值得注意的是,最近的一些研究(林等,2013;Szegedy 等人,2014 年;伊恩多拉等人,2016;Oord 等人,2016a,b,c;Springenberg 等人,2014)也使用ℝ <sup>1×1</sup> 滤波器尺寸。这乍一看似乎很奇怪,但它让网络在不改变空间维度大小的情况下学习更深层次的表示。并且还具有多个ℝ <sup>1×1</sup> 滤波器产生多个输出特征图,因此是特征张量。

还要注意,作为经验法则,当构建用于分类任务的卷积网络时,一个简单的想法是增加深度维度大小,并相应地减少空间维度大小。在到达具有少量特征单元的层之后,将卷积的特征张量整形为平坦张量,然后通过小型密集网络。这种趋势已经在深度学习文献中普遍存在(Krizhevsky 等人,2017;Simonyan 和 Zisserman,2014 年;Szegedy 等人,2015,2016;伊恩多拉等人,2016;谭和乐,2019)。

### 6.1.3 汇集层

卷积网络中常用的另一个重要层是池层。*池层*的作用是减少特征张量除深度维度尺寸外的所有维度尺寸。池进一步减少了下一层中卷积运算的计算需求,因为它的输入现在是更小的特征张量。我们已经在清单 4-3 的微型图像分类器中使用了池操作。

任何池函数都将池大小(也称为池窗口)、跨度和零填充作为其参数。使用这些参数,池化函数用单个标量值对特征张量的小边界(由池化窗口定义)中的所有标量值进行汇总。汇集函数在特征张量的每个深度索引上单独操作,这减少了时间、空间或时空维度大小,而保持深度维度大小不变。这里,池大小仅用于确定标量值在特征张量上的位置。

在深度学习文献中,我们会遇到各种各样的池操作。但这里我们讨论的是最常用的池化操作,即最大池化和平均池化。

我们知道,池函数将特征张量上的位置(由池窗口描述)中的标量值在每个深度维度中单独总结为单个标量数。在*最大池函数*的情况下,池数的值就是池窗口中的最大值。*平均池函数*通过取池窗口中所有数字的平均值来计算标量值。正如过滤器在特征张量上卷积一样,池窗口在特征张量上移动给定的步幅值,以确定要池化的值的位置。

对输入要素张量应用池函数时,以下公式计算输出要素张量的维度大小:

![$$ o=\frac{\left(i-f\right)+2p}{s}+1 $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ2.png)

(6.2)

这里, *i**o* 为输入和输出尺寸大小。在图像特征张量 I∈ℝ<sup>*h*×*w*×*c*</sup>*i* 可以是宽度 *w* 或高度 *h* ,而对于音频特征矩阵**a**∈ℝ<sup>*t*×*c*</sup>*其他参数 *f* 、 *s* 和 *p* 表示池窗口大小、池窗口的步幅以及特征张量周围的零填充。请注意,该公式类似于公式 6.1 ,但是在应用卷积运算时,没有计算输出维度大小的膨胀参数。*

汇集是一个常数函数,因此不能区分,因为没有与之相关联的可调参数,关于这些参数可以获得损失梯度。(泽勒和弗格斯,2012 年)提出了一个可区分的池函数,以受益于池操作的学习。但是它并没有变得太受欢迎,在实践中也很少使用。有趣的是,(Springenberg 等人,2014 年)发现,在卷积网络中,用大步长卷积运算代替汇集运算可以实现类似的精度。这种方法使网络完全卷积,结构简单。全卷积网络比使用池操作更好的一个可能原因是,当使用池操作对特征张量进行下采样(空间)时,会发生一些信息丢失,而卷积层可以在对特征张量进行下采样时学习保留对任务重要的信息。

### 6.1.4 上采样

我们已经看到,卷积层对特征张量的空间大小进行下采样。当希望网络的输出大小小于输入大小时,这很有用。最常见的例子是图像分类(Krizhevsky 等人,2017 年),对象检测(Redmon 等人,2016 年;雷德蒙和法尔哈迪,2017,2018;Girshick 等人,2014 年;Girshick,2015),以及音频分类(Hershey 等人,2017)。但是,在某些情况下,我们需要输出大小大于输入特征张量。这些任务包括生成模型(文森特等人,2008 年;拉德福德等人,2015 年)和激活特征图可视化(泽勒和弗格斯,2014 年)。

数据点的上采样可以通过应用各种上采样层来实现,例如双三次、最近邻和其他插值,然后是卷积(Dong 等人,2014;金等人,2016 年)或转置卷积(泽勒等人,2010 年)。

#### 6.1.4.1 转置卷积层

*转置卷积*,也被称为*分数步长卷积**反卷积*(一个误称),在对特征张量进行上采样的同时学习自己的一组参数。它在空间上放大了特征张量,其中,就像卷积一样,输出深度维度的大小取决于所使用的滤波器的数量。转置卷积运算可以被认为是使用相同自变量(滤波器、步幅、填充和膨胀)的卷积运算的反向应用,其在空间上对特征张量进行上采样,而不是下采样。转置卷积也具有与卷积层相同的属性,即稀疏连通性、参数共享和平移等方差,这将在 6.2 节中讨论。

给定图像张量 I∈ℝ<sup>*h*×*w*×*c*</sup>的形状、滤波器![$$ f\in {\mathbb{R}}^{f_h\times {f}_w\times c} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_IEq3.png)和超参数,即步幅 *s* 和填充 *p* ,我们可以计算转置卷积运算的输出特征张量维数,公式如下:

![$$ o=s\left(i-1\right)+f-2p $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ3.png)

(6.3)

这里, *i**o* 表示输入和输出特征张量的维数。考虑形状ℝ <sup>128×128×3</sup> 的图像张量和转置卷积运算,其中超参数滤波器大小ℝ <sup>6×6×3</sup> ,步长为(2,2),零填充为 1。当该滤波器应用于转置卷积运算时,我们得到形状ℝ <sup>256×256×1</sup> 的输出张量。类似于方程 6.2 中的公式,当适当使用该公式时,可以正确计算输入张量的任何输出维数。

#### 6.1.4.2 棋盘伪影去除

转置卷积已成功用于图像超分辨率(施等,2016a)和图像生成(等,2015)。但如果转置卷积的超参数设置不正确,可能会在生成的数据如音频(Donahue et al .,2018)或图像(Odena et al .,2016)中产生棋盘状伪影。图像中的棋盘格伪像被视为不规则的亮色像素,而在原始音频中,它可以被解释为噪声。

(Odena 等人,2016 年)的发现表明,在卷积运算之后使用简单的上采样运算完全减轻了棋盘伪影问题。虽然转置卷积比卷积方法之后的上采样操作具有更大的表示能力(Shi 等人,2016b),但是后者获得了更好的性能。结果令人震惊,因为在生成的数据点中完全消除了棋盘伪影。根据这项工作,研究人员可以专注于改进生成模型,生成实值数据点。

## 6.2 突出特点

有趣的是,卷积网络可以处理任意维数的张量。卷积网络还有许多其他重要特性。在本节中,我们将讨论这样的功能,并了解对于各种深度学习任务(主要与计算机视觉相关),卷积网络为何是比密集网络更好的选择。

### 本地连接

在第五章中讨论的密集连接层在包含参数的矩阵和输入特征向量(可视为矩阵)之间应用矩阵乘法,以产生输出特征向量。在这种情况下,输出特征向量中的每个神经元都连接到输入特征向量中的每个神经元。这使得该层非常密集,但也无法扩展到更大的输入张量和模型容量(从记忆的角度来看),如 6.1 节所述。

卷积层采用激进的方法来计算输出张量。它考虑输入特征张量和参数张量(称为核或滤波器),与输入张量相比,它们具有非常小的尺寸。当在滤波器和输入张量之间应用点积时,产生输出张量的单个神经元。换句话说,输出张量的某个单个神经元只与输入张量的一小部分相连。这被称为输出张量神经元的*感受野*。相反,在致密层的情况下,每个输出张量神经元连接到输入张量中的每个神经元。卷积的方法使得卷积层中的连通性是稀疏的(或局部的)。这也加快了计算速度。

还要注意,感受野在更近的层之间通常很小。但是当网络变得更深时,远离输入层的层中的神经元的感受野变得更大。

### 参数共享

在卷积层,在输入张量的不同位置重复使用相同的核来计算输出张量。也就是说,同一组参数在不同的输入位置之间共享。这些参数也被称为*绑定参数*,因为输入的任何位置的参数值都取决于不同位置的相同参数值。相比之下,致密层中的一组参数将输出单元连接到每个输入神经元。并且每个输出神经元都有其自己的独立参数集连接到相同的输入神经元。这反过来增加了内存需求。此外,如此密集的连接也使得计算输出的效率很低。另一方面,卷积层中的所有输出神经元具有连接到相邻输入神经元的相同的一小组参数。这些参数在输入张量的不同位置重复使用,以计算每个输出标量值(滤波器和输入张量重叠之间的点积)。参数的小尺寸和跨不同输入张量位置的共享使得计算更快并且存储更有效。

尽管与过滤器相比,真实世界的图像在空间维度尺寸上较大,但过滤器可以自动学习检测(或激活输出张量中的某些神经元)输入中的基本特征,如边缘、对比度、颜色梯度等。当在原始音频波形中使用卷积时,它可以学习诸如音色、语调、语音等特征的表示。由于约束参数,这成为可能,因为基本模式在数据的整个空间维度中是相似的,并且可以使用相同的参数在不同的位置检测。

### 翻译等值

由于参数共享,作为副作用,卷积层也是平移等变的。如果输入被 x 轴或 y 轴上的一些像素值平移并遵循卷积运算,则得到的张量将与卷积遵循相同平移运算的情况相同。例如,假设一个函数 *f* (。)将输入张量沿 y 轴平移 5 个像素,以及另一个函数 *g* (。)应用卷积。现在,由于平移等方差性质,当这些函数中的一个跟随另一个时,得到的张量将是相同的*f*(*g*(*x*)=*g*(*f*(*x*))。

考虑用卷积神经网络 *f* ()在原始音频波形 *x* 中识别一个口语单词。).让我们假设一个单词“hello”出现 2 到 3 秒。所以这个词会在这个时间段被转录为 *f* ( *x* )。然后我们用翻译函数 *g* ()将*g*(*f*(*x*))转录输出翻译成 5-6 秒。).这个翻译说这个单词在 5 到 6 秒之间被识别。在另一种情况下,假设我们将“hello”声音翻译成 5-6 秒音频波形的时间帧片段 *g* ( *x* ),然后应用卷积*f*(*g*(*x*))。现在,单词将在 5-6 秒的时间段内被转录。这意味着*f*(*g*(*x*)=*g*(*f*(*x*))并且卷积运算是平移等变的。

类似地,在图像的情况下,将图像沿着期望的轴平移一些像素并应用卷积的结果将与我们在平移函数之后应用卷积的结果相同。这表明卷积展现的平移等方差对于不同的输入特征维度大小(对于像音频或图像这样的数据)及其相应的滤波器大小仍然有效。

请注意,卷积并不等同于所有类型的平移,如缩放、旋转、扭曲等。一些研究(贾德伯格等人,2015;Sabour 等人,2017;Zhang,2019)为消除卷积网络中的这一问题做出了贡献。

## 6.3 快捷连接

我们已经讨论了具有非线性激活的深度神经网络能够学习高度复杂的数据集映射。接下来,您可能会猜测向网络中添加更多的层将有助于提高其性能(比如说,准确性)。但是,理论上是这样,但实际操作起来,这并没有看起来那么容易,就是把多层叠加起来。正如 5.4 节所讨论的,较深的网络会遇到梯度消失的问题。令人惊讶的是,它非常严重,网络越深,其性能下降越多,而越接近逻辑层的层梯度越大,学习速度越快。相比之下,越靠近输入层的层具有越小(消失)的梯度,因此学习速度越慢;有些人甚至会完全停止学习。

![img/484421_1_En_6_Fig7_HTML.png](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Fig7_HTML.png)

图 6-7

双层神经块的剩余连接

层操作的选择是任意的。第一层处理输入 **x** 并用 **a** 激活它。),而第二层首先预测输出并将其添加到该神经块的输入中,即形成快捷连接,然后应用激活以生成输出 **y** 。

但是我们希望使用更深的网络来获得更好的性能。那么如何才能规避这个问题呢?一种解决方案是使用剩余学习框架(He et al .,2016)。让我们从描述这个框架的等式来理解它:

![$$ \mathbf{y}=\mathbf{f}\left(\mathbf{x}\right)+\mathbf{x} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ4.png)

(6.4)

在方程 6.4 中, **x****y** 为输入和输出变量。这里, **f** (。)是包含一个以上神经层(通常是两个)的神经块(或复合函数)。*快捷连接*,也叫*剩余连接*,是由神经块 **f** 的输入 **x** (这里也叫*剩余*)相加而成。)至其输出 **y** 。

根据等式 6.4 ,中间输出 **f** ( **x** )的尺寸必须等于其输入 **x** 的尺寸,以便逐元素加法成为有效运算。当不是这种情况时,我们简单地将输入 **x** 投影到与 **f** ( **x** )相同的尺寸,投影参数 **W** <sub>s</sub> 如下,其中下标 *s* 代表快捷投影:

![$$ \mathbf{y}=\mathbf{f}\left(\mathbf{x}\right)+\mathbf{x}{\mathbf{W}}_s $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ5.png)

(6.5)

在神经块 **f** ()有两层,那么方程 6.4 可以写成如下,并在图 6-7 中可视化:

![$$ \mathbf{y}={\mathbf{f}}^{(2)}\left({\mathbf{f}}^{(1)}\left(\mathbf{x}\right)\right)+\mathbf{x} $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ6.png)

(6.6)

用于剩余连接的神经块中的神经层的类型是任意选择的,并且基于要执行的任务。现在,如果我们选择所有神经层作为密集层操作,那么我们可以将方程 6.4 重写如下:

![$$ \mathbf{y}={\mathbf{a}}^{(2)}\left({\mathbf{W}}^{(2)}\left({\mathbf{a}}^{(1)}\left({\mathbf{W}}^{(1)}\mathbf{x}\right)\right)+\mathbf{x}\right) $$](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-swift-tf/img/484421_1_En_6_Chapter_TeX_Equ7.png)

(6.7)

这里,术语 **x****W****a** (。)分别是输入向量、权重矩阵和激活函数。圆括号中的上标索引表示这些元素所属的层。为了简单起见,我们省略了偏差项。注意,在任何快捷连接中,神经块的输入被添加到该块的最后(未激活的)层的输出,然后跟随该最后层的激活功能。

接下来,我们将构建自己的残差卷积网络,并训练它执行图像识别任务。

## 6.4 图像识别

在本节中,我们将构建一个深度卷积神经网络,称为残差网络(或 ResNet) (He et al .,2016),在玩具数据集上执行图像识别任务。ResNet 提出了上一节中讨论的剩余连接的概念。基于层数(例如 18、34、50、101 和 152 层),有各种 ResNet 架构。我们构建了一个 18 层深度残差网络,并在 CIFAR-10 数据集上对其进行训练,看看我们的 ResNet 是否比我们之前在第四章中训练的 LeNet 表现得更好。事实上,研究人员通常还会在 ImageNet 数据集上训练大型卷积网络,因为学习其底层映射很困难,因为有近 120 万张图像属于 1000 个类别。

旁注:虽然高速公路连接(Srivastava 等人,2015 年)被发明得更早,但它们并没有取得太大的成功,因为后来残差连接在速度、有效的更深模型训练(高达 1000 层的实验验证)以及简单的无参数操作方面胜过了它们。

现在我们来看看`ResNet18`模型的架构,它基本上有 18 个卷积层。残差块由两个卷积和批量归一化层序列组成,为简单起见,我们将其表示为`ConvBN`。残差块的输入通过第一个`ConvBN`块传递,后面是激活函数(在我们的例子中是 ReLU)。接下来,该中间输出通过另一个`ConvBN`模块,其输出与剩余模块的输入相加,并遵循激活操作。为了使剩余相加成为可能,第二个`ConvBN`模块的输出尺寸必须与剩余输入尺寸相同。换句话说,第一卷积层的输入滤波器必须等于第二卷积层的输出滤波器。如果不是这种情况,我们通过因子 2 对残差输入进行下采样,并通过名为`projection`的独立卷积层将其滤波器数量(通常是输入滤波器的两倍)与第二`ConvBN`模块的输出进行匹配。

在编程剩余块之前,让我们导入一些库。

```py
import Dispatch
import Foundation

import Datasets
import TensorFlow
import TrainingLoop

import PythonKit
let np = Python.import("numpy")

Listing 6-1Import libraries

接下来要做的事情是声明一些配置常数,例如网络的图像样本通道、CIFAR-10 数据集中的类的数量、训练模型的时期数量、样本的小批量大小,以及一些类型别名,以便于初始化将在其上执行每个训练相关操作的设备。

let inChannels: Int = 3
let classCount = 10
let epochCount = 25
let batchSize = 128

let device = Device.defaultXLA

typealias TFloat = Tensor<Float>
typealias Input = Tensor<Float>
typealias Output = Tensor<Float>

let imagenetteDataset = Imagenette(batchSize: batchSize, inputSize: .resized320, outputSize: imageSize, on: device)

Listing 6-2Initialize configuration properties, device, and type aliases for the ResNet18 model and load the CIFAR-10 dataset

网络接受的输入通道有三个,CIFAR-10 数据集的图像大小为ℝ 32×32×3 维,网络可以预测的类别数设置为 10,网络将经历数据集 50 次,每个样本为一批 128 幅图像,批维数为ℝ 128×32×32×3 。我们将device设置为 XLA 后端,默认设备将被自动选择,也就是说,如果选择了一个硬件加速器(如下所述),则为 CPU。

如果你在 Google Colaboratory 上编程,你可以选择硬件加速器为 GPU 或 TPU,方法是在菜单栏中点击运行时➤更改运行时类型,然后从弹出菜单中的硬件加速器下拉列表中选择任一设备,并点击保存按钮。现在device将自动设置为 GPU 或 TPU 加速器与 XLA 后端,ResNet18将在选定的加速器上训练。在我的实验中,我选择了 GPU。

img/484421_1_En_6_Fig8_HTML.png

图 6-8

残差卷积块的详细架构

输入经过卷积和批量标准化层、ReLU 激活以及另一系列卷积和批量标准化层。该输出与剩余输入相加,然后用 ReLU 激活剩余输入,给出剩余模块的最终输出。残差输入首先被投影(或下采样)到与第二批归一化层的输出相同的维度(即,输入的一半)和通道(即,输入的两倍),但是如果通道和维度相同,则它被原样传递到残差连接。

现在我们将对清单 6-3 中的ConvBN块进行编程。

struct ConvBN: Layer {
  var conv: Conv2D<Float>
  var norm: BatchNorm<Float>

  init(
    filterShape: (Int, Int, Int, Int),
    strides: (Int, Int) = (1, 1),
    padding: Padding = .same
  ) {
    conv = Conv2D(filterShape: filterShape, strides: strides, padding: padding, useBias: false)
    norm = BatchNorm(featureCount: filterShape.3, momentum: 0.1, epsilon: 1e-5)
  }

  @differentiable
  func callAsFunction(_ input: Input) -> Output {
    input.sequenced(through: conv, norm)
  }
}

Listing 6-3Program a convolutional and batch normalization layers block

让我们从名为convConv2D实例的初始化开始,它以filterShapestridespadding作为参数。第一个自变量filterShape是四个Int值的元组,其中第一、第二、第三和第四值分别代表内核的高度、内核的宽度、内核的深度(即,输入特征图的过滤器)和内核的数量(即,输出特征图的过滤器的期望数量或深度维度大小)。第二个自变量strides是两个Int值的元组,其在第一和第二索引处分别表示在应用卷积运算之前在高度(即,垂直)和宽度(即,水平)方向上采取的步骤。第三个参数是我们已经讨论过的Padding枚举类型的padding。在 TensorFlow 中,我们不需要计算零填充并将其放在卷积或池化等层中。我们可以将padding设置为.valid.same。两者的区别在于.same在输入周围应用零填充(如果需要)以产生与输入相同的空间或时间维度的输出,而.valid在输入周围不应用任何零填充,并且输出的空间或时间维度可能与输入的空间或时间维度相同,也可能不同。但是在我们的代码示例中,我们将主要使用Padding枚举的.same用例。最后,我们没有在我们的conv实例中使用偏差项,并将useBias参数设置为等于false

接下来,注意批量标准化层norm采用的featureCount参数等于卷积层conv的输出滤波器数量。我们还将normmomentumepsilon分别设为等于0.10.00001

img/484421_1_En_6_Fig9_HTML.jpg

图 6-9

18 层剩余卷积网络的详细结构

输入x∈ℝb×224×224×3经过卷积和批量归一化层和 ReLU 激活,随后是最大池操作。这里, b 是最小批量。然后,输出经过一系列八个残差块,其输出经过全局平均池操作、展平层(将多维张量转换为批量向量)和密集层,后者产生罗吉斯向量 y ∈ ℝ b×10

现在我们将对清单 6-4 中的剩余程序块进行编程,如图 6-8 所示。

struct ResidualBlock: Layer {
  var convBN1: ConvBN
  var convBN2: ConvBN
  var projection: ConvBN

  init(
    inFilters: Int,
    outFilters: Int
  ) {
    if inFilters == outFilters {
      convBN1 = ConvBN(filterShape: (3, 3, inFilters, outFilters))
      convBN2 = ConvBN(filterShape: (3, 3, outFilters, outFilters))
      // In this case, we don't use `projection`.
      projection = ConvBN(filterShape: (1, 1, 1, 1))
    } else {
      convBN1 = ConvBN(filterShape: (3, 3, inFilters, outFilters), strides: (2, 2))
      convBN2 = ConvBN(filterShape: (3, 3, outFilters, outFilters), strides: (1, 1))
      projection = ConvBN(filterShape: (1, 1, inFilters, outFilters), strides: (2, 2))
    }
  }

  @differentiable
  func callAsFunction(_ input: Input) -> Output {
    let residual = convBN1.conv.filter.shape[2] != convBN2.conv.filter.shape[3] ? projection(input) : input
    let convBN1Output = relu(convBN1(input))
    let convBN2Output = relu(convBN2(convBN1Output) + residual)
    return convBN2Output
  }
}

Listing 6-4Program a convolutional residual block and consider the downsampling for different input and output filters to it

残差块是 ResNet 模型的基本构造块。残余块的可视化和解释见图 6-8 。在清单 6-4 中,名为ResidualBlock的残差块结构有三个存储属性,即projectionconvBN1ConvBN类型的convBN2。在其初始化器中,它接受前两个Int参数,即inFiltersoutFilters。初始化层时,我们检查inFiltersoutFilters是否相等。如果这是真的,那么我们为convBN1convBN2设置内核的高度和宽度等于 3 个单位。convBN1convBN2的输入通道设置为等于inFiltersoutFilters,两者的输出通道设置为等于outFilters

两层的strides也默认为(1, 1)。在这种情况下,由于默认情况下padding.samestrides(1, 1),输入输出特征尺寸将相同;因此,我们不需要投影输入,我们将projection实例初始化为默认值。

ResidualBlockinFiltersoutFilters不同时,我们将convBN1strides设置为等于(2, 2),而对于convBN1convBN2的其他参数与前一种情况相同。使用此stridesconvBN1,输入要素的空间维度将减半。因此,我们将projection实例的所有参数设置为与convBN1相同,以将输入投射到与convBN1convBN2序列将投射的维度相同的维度。

callAsFunction(_:)方法的正向传递过程中,如果两个ConvBN序列的输入和输出滤波器不相同,但在其他情况下等于输入本身,我们首先将剩余输入设置为等于投影输入。然后我们应用convBN1并用relu激活它,运行通过convBN2,加上剩余输入,用relu激活它返回剩余块的输出。

清单 6-5 显示了 ResNet18 模型结构。ResNet18 模型如图 6-9 所示。

struct ResNet18: Layer {
  var initialConvBNBlock = Sequential {
    ConvBN(filterShape: (7, 7, inChannels, 64), strides: (2, 2))
    MaxPool2D<Float>(poolSize: (3, 3), strides: (2, 2), padding: .same)
  }
  var block1 = Sequential {
    ResidualBlock(inFilters: 64, outFilters: 64)
    ResidualBlock(inFilters: 64, outFilters: 64)
  }
  var block2 = Sequential {
    ResidualBlock(inFilters: 64,  outFilters: 128)
    ResidualBlock(inFilters: 128, outFilters: 128)
  }
  var block3 = Sequential {
    ResidualBlock(inFilters: 128, outFilters: 256)
    ResidualBlock(inFilters: 256, outFilters: 256)
  }
  var block4 = Sequential {
    ResidualBlock(inFilters: 256, outFilters: 512)
    ResidualBlock(inFilters: 512, outFilters: 512)
  }
  var globalAvgPool = GlobalAvgPool2D<Float>()
  var flatten = Flatten<Float>()
  var classifier: Dense<Float>

  init(classCount: Int) {
    classifier = Dense(inputSize: 512, outputSize: classCount)
  }

  @differentiable
  func callAsFunction(_ input: Input) -> Output {
    let initialConvBNOutput = maxPool(relu(initialConvBN(input)))
    let convFeatures = initialConvBNOutput.sequenced(through: block1, block2, block3, block4)
    let logits = convFeatures.sequenced(through: globalAvgPool, flatten, classifier)
    return logits
  }
}

Listing 6-5Define the residual convolutional network with 18 layers

如图 6-9 所示,该结构有四个块,每个块包含两个ResidualBlock,还有initialConvBNmaxPool属性,执行卷积和批量归一化以及一个最大池操作。有一个名为globalAvgPool的平均池属性和一个类型为Flattenflatten属性。最后,我们有一个Dense类型的classifier属性用于预测logits。在正向传递中,输入张量通过initialConvBNrelumaxPoolblock1block2block3block4传递。输出是输入图像的卷积特征,然后汇集、展平(即整形为批量矢量),并通过classifier层进行预测。

接下来,我们侵入Layer协议,用 NumPy 数组实现读和写检查点方法。

extension Layer {
  public func writeCheckpoint(to file: String) throws {
    var parameters = Array<PythonObject>()
    for keyPath in self.recursivelyAllWritableKeyPaths(to: TFloat.self) {
      parameters.append(self[keyPath: keyPath].makeNumpyArray())
    }
    np.save(file, np.array(parameters))
  }

  public mutating func readCheckpoint(from file: String) throws {
    let parameters = np.load(file)
    for (index, keyPath) in self.recursivelyAllWritableKeyPaths(to: TFloat.self).enumerated() {
      self[keyPath: keyPath] = TFloat(numpy: parameters[index])!
    }
  }
}

Listing 6-6Implement custom checkpoint reading and writing methods on the Layer protocol using the NumPy library with Python interoperability

我们在Layer协议上声明了两个实例方法,所有符合它的类型都可以自动使用它们,即writeCheckpoint(to:)readCheckpoint(from:)。检查点写方法声明了一个PythonObject类型的参数数组。然后我们通过可以写入的KeyPath遍历Tensor<Float>类型的所有属性。在通过调用makeNumpyArray()方法将TFloat实例转换为 NumPy 数组之后,我们将每个属性添加到parameters数组中。在循环之后,我们将 NumPy 数组类型转换参数保存到文件位置。

在检查点读取方法中,我们加载检查点文件。然后,我们递归地迭代指向TFloat类型的所有可写的KeyPath,并通过强制展开将keyPath keyPath处的符合层的实例的属性设置为等于从索引处的file加载的 NumPy 个参数。这样,我们将参数加载到我们的Layer-一致性实例中。

接下来,我们定义一个函数writeCheckpoint(_:event:)来保存训练期间每个时期结束后的检查点。我们将把它传递给数组中的参数callbacks。在训练过程中,callbacks接受一组基于特定事件调用的函数。

func writeCheckpoint<L: TrainingLoopProtocol>(_ loop: inout L, event: TrainingLoopEvent) throws {
  DispatchQueue.global(qos: .userInitiated).async {
    switch event {
    case .epochEnd:
      do {
        try preTrainingModel.writeCheckpoint(to: "ResNet18.npy")
      } catch {
        print(error)
      }
      default: break
    }
  }
}

Listing 6-7Define a function to automatically write checkpoint during training

在清单 6-7 中,我们定义了一个占位符类型L符合TrainingLoopProtocol的函数writeCheckpoint(_:event:),它带有两个参数:inout类型的loopTrainingLoopEvent类型的event。这个函数会抛出一个错误。如果您想在训练期间通过传递给callbacks为其他定制任务定义您的函数,您应该使用相同的参数,并在函数体内定义定制功能。

在主体内部,我们有一个包含在DispatchQueue中的switch语句,用于使用另一个线程来执行这个任务(您可能不需要编写这个语句,但是我在代码执行方面遇到了一些问题)。switch语句将事件参数与各种情况进行比较。因为我们想在每个时期结束时保存检查点,所以我们试图在event等于.epochEnd的时候写检查点。还有许多其他的event案例,你可以利用它们来定制训练循环。

// Initialize the model and optimizer
var model = ResNet18(classCount: classCount)
var optimizer = SGD(for: model, learningRate: 0.1, momentum: 0.9)

// Training setup
let trainingProgress = TrainingProgress()
var trainingLoop = TrainingLoop(
  training: dataset.training,
  validation: dataset.validation,
  optimizer: optimizer,
  lossFunction: softmaxCrossEntropy,
  callbacks: [trainingProgress.update, writeCheckpoint])

// Train the model
try! trainingLoop.fit(&model, epochs: epochs, on: device)

Listing 6-8Initialize the model and optimizer, and train the model

在清单 6-8 中,我们已经用具有classCount类的ResNet18初始化了model用于预测。优化器optimizer用随机梯度下降优化器初始化,该优化器具有学习速率0.1和等于0.9的动量设置。然后分别初始化TrainingProgressTrainingLoop实例trainingProgresstrainingLoop。对于trainingvalidation,用datasets初始化trainingLoop,优化器被设置为等于optimizer,损失函数被设置为softmaxCrossEntropycallbacks是包含更新方法trainingProgress.updatewriteCheckpoint函数的数组。

最后,我们对model进行训练,在训练集和验证集上分别达到 0.9960 和 0.7305 的图像分类精度。我们的 ResNet18 模型的性能优于在相同数据集上训练的小 LeNet(参见第四章),后者在训练集和验证集上的精度分别仅为 0.5607 和 0.5625。

6.5 结论

在这一章中,我们研究了卷积神经网络对于处理计算机视觉问题非常有用,并且比密集网络有许多优势。我们还研究了一种减轻梯度消失问题的技术。最后,我们训练了一个深度卷积网络来对图像进行分类,其性能优于一个较小的卷积网络,表明深度非线性模型优于浅层模型。

这本书用 Swift 为 TensorFlow 介绍了深度学习学科。但我们只是触及了深度学习领域的皮毛,还有很多东西需要了解!我希望你喜欢用 Swift 语言理解和编程深度学习。直到下一次…

posted @   绝不原创的飞龙  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2020-10-02 《线性代数》(同济版)——教科书中的耻辱柱
点击右上角即可分享
微信分享提示