深度学习的数学与架构-全-
深度学习的数学与架构(全)
原文:
zh.annas-archive.org/md5/6630382191e0d18f8b988ff4aec86752译者:飞龙
第一章:深度学习概述
本章涵盖了
-
初识机器学习与深度学习
-
一个简单的机器学习模型:猫脑
-
理解深度神经网络
深度学习已经改变了计算机视觉、自然语言和语音处理,特别是人工智能。从一袋半和谐的技巧中,没有哪一个在现实生活中的问题上有满意的效果,人工智能已经变成了一个强大的工具,可以解决工业界面临的实际问题,并且规模很大。这在我们鼻子底下发生的一场革命。要领先这场革命的曲线,理解其背后的原理和抽象概念比简单地记住某些实践指南的“如何做”步骤至关重要。这正是数学发挥作用的地方。
在第一章中,我们概述了深度学习。这需要我们使用后续章节中解释的一些概念。如果在这一章的结尾有一些未解决的问题,请不要担心:它的目的是引导你的思维向这个困难的主题靠拢。随着后续章节中个别概念变得更加清晰,你应该考虑回来重新阅读这一章。
1.1 初识机器/深度学习:计算范式转变
做出决定和/或预测是生活的基本要求。这样做本质上涉及到接收一系列感官或知识输入并处理它们以生成决策或估计。
例如,猫的大脑经常在以下选项之间做出选择:
-
逃离它前面的物体
-
忽略它前面的物体
-
接近它前面的物体并发出咕噜声。
猫的大脑通过处理如它感知到的物体前面的硬度、物体的尖锐度等感官输入来做出这个决定。这是一个分类问题的例子,其中输出是可能的一组类别中的一个。
生活中的一些其他分类问题例子如下:
-
买入、持有还是卖出某种股票,基于如该股票的价格历史和该股票在最近时期的价格变动等输入。
-
物体识别(来自图像):
-
这是一个汽车还是长颈鹿?
-
这是一个人类还是非人类?
-
这是一个无生命物体还是生命物体?
-
面部识别——这是汤姆、迪克、玛丽、爱因斯坦还是梅西?
-
-
从视频中识别动作:
-
这个人是在跑还是在没跑?
-
这个人是在拿起东西还是没在拿起?
-
这个人是在做暴力行为还是没在做?
-
-
自然语言处理(NLP)来自数字文档:
-
这篇新闻文章属于政治领域还是体育领域?
-
这个查询短语是否与存档中的特定文章匹配?
-
有时候生活需要的是定量估计而不是分类。狮子的大脑需要估计跳多远才能落在猎物顶部,通过处理如下输入:
另一个定量估计的例子是根据房屋所有者的当前收入、社区犯罪统计数据等因素来估算房屋的价格。制造这种定量估计器的机器被称为回归器。
这里有一些日常生活中需要的定量估计的例子:
-
从图像中定位对象:识别包围对象位置的矩形
-
从历史股价和其他世界事件中预测股价
-
两份文档之间的相似度得分
有时候,可以从定量估计中生成分类输出。例如,前面描述的猫脑可以将输入(硬度、尖锐度等)组合起来生成一个定量威胁分数。如果这个威胁分数很高,猫就会逃跑。如果威胁分数接近零,猫就会忽略它面前的物体。如果威胁分数是负数,猫就会走向物体并发出咕噜声。
许多这些例子都在图 1.1 中展示。在每个实例中,一个机器——即大脑——将感官或知识输入转换为决策或定量估计。机器学习的目标是模仿这种机器。
注意,机器学习在达到与人类大脑相当的水平之前还有很长的路要走。人类大脑可以独立处理成千上万个这样的问题。另一方面,在目前的开发状态下,机器学习几乎无法创造一个能够做出广泛决策和估计的通用机器。我们主要是在尝试制造单独的机器来解决单个任务(如股票选择器或汽车识别器)。

图 1.1 生活中决策和定量估计的例子
在这个阶段,你可能会有疑问,“等等:将输入转换为输出——这难道不就是计算机在过去 30 年或更长时间里一直在做的事情吗?我听说的这种范式转变究竟是什么?”答案是,这确实是一种范式转变,因为我们并没有为机器提供一个逐步的指令集——也就是说,一个程序——来将输入转换为输出。相反,我们为问题开发了一个数学模型。
让我们用一个例子来说明这个想法。为了简单和具体,我们将考虑一个假设的猫脑,它一生中只需要做出一个决定:是从它面前逃跑、忽略物体还是靠近并咕噜咕噜叫。这个决定,然后,就是我们将讨论的模型的输出。在这个玩具例子中,决策是基于仅有的两个定量输入(即特征):物体的感知硬度和锐度(如图 1.1 所示)。我们不提供任何逐步指令,例如“如果锐度大于某个阈值,则逃跑。”相反,我们试图识别一个参数化的函数,该函数接受输入并将其转换为所需的决策或估计。最简单的此类函数是输入的加权求和:
y(硬度, 锐度) = w[0] × 硬度 + w[1] × 锐度 + b
权重 w[0]、w[1] 和偏差 b 是函数的参数。输出 y 可以解释为威胁分数。如果威胁分数超过阈值,猫就会逃跑。如果它接近 0,猫就会忽略物体。如果威胁分数为负,猫就会靠近并咕噜咕噜叫。对于更复杂的任务,我们将使用更复杂的函数。
注意,一开始权重是未知的;我们需要通过一个称为模型训练的过程来估计它们。
总体而言,通过机器学习解决问题有以下阶段:
-
我们设计一个参数化模型函数(例如,加权求和)带有未知参数(权重)。这构成了模型架构。选择正确的模型架构是机器学习工程师专业知识发挥作用的领域。
-
然后我们通过模型训练来估计权重。
-
一旦估计了权重,我们就有一个完整的模型。这个模型可以接受任意输入,不一定之前见过,并生成输出。一个训练好的模型处理任意现实生活输入并发出输出的过程称为推理。
在最流行的机器学习类型中,称为监督学习,我们在开始训练之前准备训练数据。训练数据包括示例输入项,每个都有其对应的期望输出。¹ 训练数据通常是通过人工创建的:一个人检查每个输入项并产生期望的输出(即目标输出)。这通常是机器学习中最艰巨的部分。
例如,在我们的假设猫脑例子中,一些可能的训练数据项如下
输入:硬度 = 0.01,锐度 = 0.02 → 威胁 = -0.90 → 决策:“靠近并咕噜咕噜叫”
输入:硬度 = 0.50,锐度 = 0.60 → 威胁 = 0.01 → 决策:“忽略”
输入:硬度 = 0.99,锐度 = 0.97 → 威胁 = 0.90 → 决策:“逃跑”
假设硬度和锐度的输入值介于 0 和 1 之间。
训练过程中究竟发生了什么?回答:我们迭代处理输入的训练数据项。对于每个输入项,我们知道期望的(即目标)输出。在每次迭代中,我们调整模型权重值,以便模型函数在该特定输入项上的输出至少接近相应的目标输出。例如,假设在给定的一次迭代中,权重值是 w[0] = 20 和 w[1] = 10,以及 b = 50。在输入(硬度 = 0.01,锐度 = 0.02)下,我们得到输出威胁分数 y = 50.3,这与期望的 y = −0.9 相差甚远。我们将调整权重:例如,减少偏差,使 w[0] = 20,w[1] = 10,以及 b = 40。相应的威胁分数 y = 40.3 仍然远远达不到期望值,但它已经接近了。在我们对许多训练数据项进行这样的操作后,权重将开始接近其理想值。请注意,如何确定权重值的调整方法在这里没有讨论;它需要一些更深入的数学知识,将在以后讨论。
如前所述,这种迭代调谐权重的过程被称为训练或学习。在学习开始时,权重具有随机值,因此机器的输出通常不符合期望的输出。但随着时间的推移,更多的训练迭代发生,机器“学习”生成正确的输出。那时,模型就准备好在现实世界中部署了。给定任意输入,模型将在推理过程中(希望)输出接近期望的输出。
想想看,这可能就是活脑工作的方式。它们包含各种任务的数学模型等效物。在这里,权重是大脑中不同神经元之间连接(即突触)的强度。一开始,参数未调谐;大脑会反复犯错误。例如,婴儿的大脑在识别可食用物体时经常犯错误——任何有孩子的父母都知道我们在说什么。但每个例子都会调谐参数(吃带有$符号的绿色和白色矩形东西会招来很多责备——将来不应该吃它们等)。最终,这个机器调谐其参数以产生更好的结果。
在这里有一个需要注意的微妙之处。在训练过程中,机器正在调整其参数,以便只在其训练数据输入上产生预期的结果——仅限于训练数据输入。当然,在训练过程中,它只能看到所有可能输入的一小部分——我们不是在从已知输入到已知输出的查找表中构建。因此,当这台机器被发布到世界上时,它主要运行的是它以前从未见过的输入数据。我们有什么保证它能对从未见过的数据进行正确的输出呢?坦白说,没有保证。只是在大多数现实生活中的问题中,输入并不是真正随机的。它们有一个模式。希望机器在训练过程中能看到足够的输入来捕捉这个模式。然后,它在未见输入上的输出将接近预期的值。训练数据的分布越接近现实生活,这种情况就越有可能发生。
1.2 机器学习的函数逼近视角:模型及其训练
如第 1.1 节所述,为了创建一个像大脑一样的机器来进行分类或估计,我们必须找到一个数学函数(模型),它将输入转换为相应的期望输出。然而,遗憾的是,在典型的现实生活情况中,我们并不知道这个转换函数。例如,我们不知道一个函数,它接受过去的股价、世界事件等等,并估计股票的未来价格——这阻止了我们构建股价估计器并致富。我们拥有的只是训练数据——一组已知输出的输入。那么我们该怎么办呢?答案是:我们将尝试模拟未知函数。这意味着我们将创建一个函数,它将是未知函数的代理或替代品。从这个角度来看,机器学习不过是函数逼近——我们只是在尝试逼近未知的分类或估计函数。
让我们简要回顾一下上一节的主要思想。在机器学习中,我们试图解决那些可以抽象地看作是将一组输入转换为输出的问题。输出要么是一个类别,要么是一个估计值。由于我们不知道真正的转换函数,我们试图提出一个模型函数。我们首先通过使用我们对问题的物理理解来设计一个具有可调参数值的模型函数,它可以作为真实函数的代理。这就是模型架构,可调参数也被称为权重。最简单的模型架构是输出是输入值的加权总和。确定模型架构并不完全确定模型——我们还需要确定实际的参数值(权重)。这就是训练的作用所在。在训练过程中,我们找到一组最优的权重,将训练输入转换为尽可能接近相应训练输出的输出。然后我们将这个机器部署到世界上:其权重被估计,函数被完全确定,因此对于任何输入,它只需应用函数并生成输出。这被称为推理。当然,训练输入只是所有可能输入的一小部分,因此无法保证推理会在所有实际输入上产生期望的结果。模型的成功取决于所选模型架构的适当性和训练数据的质量和数量。
获取训练数据
在掌握机器学习之后,最大的挑战竟然是训练数据的获取。当从业者能够承担得起时,通常的做法是使用人类手动生成与训练数据输入相对应的输出(这些目标输出有时被称为真实值)。这个过程被称为人工标注或人工整理,涉及一群人类查看大量的训练数据输入并生成相应的真实值输出。对于一些经过深入研究的问题,我们可能足够幸运,可以从互联网上获得训练数据;否则,这变成了一项艰巨的挑战。关于这一点,我们稍后再详细讨论。
现在,让我们通过一个具体的例子来研究模型构建的过程:图 1.1 中所示的猫脑机器。
1.3 一个简单的机器学习模型:猫脑
为了简单和具体,我们将处理一个假设的猫,这只猫在一生中只需要做出一个决定:是否要从它面前的东西逃跑,忽略它,或者走近并咕噜咕噜叫。它根据它面前的东西的两个定量输入(如图 1.1 所示)来做出这个决定。
注意:本章是对机器/深度学习的一个轻量级概述。因此,它依赖于我们稍后将要介绍的一些数学概念。尽管如此,我们鼓励你现在阅读这一章,也许在消化了关于矢量和矩阵的章节后再重新阅读。
1.3.1 输入特征
输入特征是 x[0],表示 硬度,和 x[1],表示 尖锐度。不失一般性,我们可以 归一化 输入。这是一个相当流行的技巧,其中输入值在最小可能值 v[min] 和最大可能值 v[max] 之间变换为介于 0 和 1 之间的值。要将任意输入值 v 转换为归一化值 v[norm],我们使用以下公式

方程式 1.1
用数学术语来说,通过方程式 1.1,v ∈ [v[min], v[max]] → v[norm] ∈ [0,1] 将值 v 从输入域 [v[min], v[max]] 映射到输出值 v[norm] 在范围 [0,1] 内。
一个两元素向量
简洁地表示一个单个输入实例。
1.3.2 输出决策
最终输出是多类别的,可以取三个可能值之一:0,表示猫避开前面的物体;1,表示忽略物体;2,表示接近物体并发出咕噜声。在机器学习中,可以直接计算类别。然而,在这个例子中,我们将让我们的模型估计一个 威胁分数。它的解释如下:威胁高度正值 = 逃跑,威胁接近零 = 忽略,威胁高度负值 = 接近并咕噜(负威胁是有吸引力的)。
我们可以根据威胁分数,通过将威胁分数 y 与阈值 δ 进行比较,来做出最终的运行/忽略/接近决策:

方程式 1.2
1.3.3 模型估计
现在是至关重要的步骤:我们需要估计将输入向量转换为输出的函数。为了稍微滥用术语,我们将这个函数以及输出表示为 y。用数学符号表示,我们想要估计 y(
)。
当然,我们不知道理想函数。我们将尝试从训练数据中估计这个未知函数。这是通过两个步骤完成的:
-
模型架构选择—设计一个参数化的函数,我们期望它是未知理想函数的良好代理或替代品
-
训练—估计所选函数的参数,使得在训练输入上的输出尽可能接近相应的输出
1.3.4 模型架构选择
这是各种机器学习算法彼此不同的步骤。在这个玩具猫脑的例子中,我们将使用最简单的模型。我们的模型有三个参数,w[0],w[1],b。它们可以用一个单元素的两个元素向量
和一个常数偏置 b ∈ ℝ(在这里,ℝ 表示所有实数的集合,ℝ² 表示所有元素为实的二维向量的集合,等等)来紧凑地表示。它产生威胁分数,y,其计算如下

方程式 1.3
注意,b 是一个稍微特殊的参数。它是一个常数,不与任何输入相乘。在机器学习中,通常将其称为 偏置;其他参数作为权重与输入相乘。
1.3.5 模型训练
一旦选择了模型架构,我们就知道将要使用精确的参数函数来模拟未知函数 y(
),该函数将输入转换为输出。我们仍然需要估计该函数的参数。因此,我们有一个具有未知参数的函数,并且这些参数需要从一组具有已知输出的输入(训练数据)中估计。我们将选择参数,使得在训练数据输入上的输出尽可能接近相应的输出。
迭代训练
这个问题已经被数学家研究,在数学上被称为 函数拟合 问题。然而,随着机器学习的出现,发生了变化的是规模。在机器学习中,我们处理包含数百万个项目的训练数据。这改变了解决方案的哲学。数学家使用 闭式解,其中参数通过直接解决涉及 所有 训练数据项目的方程来估计。在机器学习中,我们寻求迭代解决方案,一次处理 少量 训练数据项目(或者可能只有一个)。在迭代解决方案中,没有必要将所有训练数据都保存在计算机的内存中。我们只需一次加载一小部分,并处理那一部分。我们将用我们的猫脑例子来举例说明。
具体来说,训练过程的目标是估计参数 w[0],w[1],b 或者等价地,从方程 1.3 估计向量
以及常数 b,以便在训练数据输入 (x[0],x[1]) 上的输出 y(x[0],x[1]) 尽可能地匹配相应的已知训练数据输出(即真实值 [GT])。
让训练数据由 N + 1 个输入
((0)),((1)),⋯
^((N)) 组成。在这里,每个
^((i)) 是一个表示单个训练数据输入实例的 2 × 1 向量。相应的期望威胁值(输出)是 y**[gt]((0)),*y**[gt]*((1)),⋯ y[gt]^((N)),例如(在这里,下标 gt 表示真实值)。等价地,我们可以说训练数据由 N + 1 个(输入,输出)对组成:
(
^((0)), y**[gt]^((0))), (
^((1)), y**[gt]((1)))⋯(((N)), y**[gt]^((N)))
假设
表示模型(尚未知的)最优参数。然后,给定一个任意的输入
,机器将估计威胁值 y**[predicted] =
^T
+ b。在 i[th] 训练数据对((
^((i)), y**[gt]^((i)))),机器将估计
y**[predicted]^((i)) =
T*((i*)) + b
while the desired output is y[gt]^((i)). Thus the squared error (aka loss) made by the machine on the i[th] training data instance is ²
e[i]² = (y**[predicted]((*i*))−*y**[gt]*((i)))²
The overall loss on the entire training data set is obtained by adding the loss from each individual training data instance:

训练的目的是找到一组模型参数(即权重),
,以最小化总误差 E。我们如何做到这一点将在后面描述。
在大多数情况下,不可能为最优的
,b 提出一个封闭形式的解。相反,我们采取算法 1.1 中描述的迭代方法。
算法 1.1 训练监督模型
使用随机值初始化参数
,b
⊳ 当误差不够小的时候继续迭代
while (E[2] = Σ[i = 0]^(i=N) (
^T
[i] ¸ b — y[gt]^((i)))² > threshold) do
⊳ 遍历所有训练数据实例
for ∀[i] ∈ 2 [0, N] do
⊳ 在引入梯度后,3.3 节中提供了详细信息
调整
,b 以减少 E²
end for
end while
⊳ 记录最终参数值作为最优值
[]←
, b[]← b
在这个算法中,我们从一个随机的参数值开始,并不断调整参数,使得总误差至少减少一点。我们一直这样做,直到误差足够小。
在纯粹数学的意义上,我们继续迭代直到误差最小。但在实践中,我们通常在结果足够准确以解决问题时停止。值得再次强调的是,这里的 误差 仅指训练数据上的误差。
1.3.6 推断
最后,一个经过训练的机器(具有最优参数
[],b[])被部署到世界中。它将接收新的输入
并推断 y**[预测](
) =
[]^T
+ b[]。分类将通过阈值 y**[预测] 来实现,如方程 1.2 所示。
1.4 机器学习的几何视图
猫脑模型每个输入都是一个包含两个数字的数组:x[0](表示物体的硬度),x[1](表示物体的锋利度)或者,等价地,一个 2 × 1 的向量
。一个很好的心理图像是将输入视为高维空间中的一个点。输入空间通常被称为 特征空间——一个模型要检查的所有特征都表示在这个空间中。在这种情况下,特征空间的维度是两个,但在现实生活中的问题中,它将是几百或几千或更多。输入的精确维度会随着问题而变化,但它是点的直觉仍然存在。
输出 y 也应该被视为另一个高维空间中的一个点。在这个玩具问题中,输出空间的维度是一维的,但在现实问题中,它将更高。然而,通常输出维度的数量远小于输入维度的数量。
从几何学的角度来看,机器学习模型本质上是将特征空间中的一个点映射到输出空间中的一个点。预期模型要执行的分类或估计工作在输出空间中比在特征空间中更容易。特别是,对于分类工作,属于不同类别的输入点预期将映射到输出空间中的不同簇。
让我们继续使用我们的猫脑模型来阐述这个想法。如前所述,我们的特征空间是二维的,有两个坐标轴 X[0] 表示硬度,X[1] 表示锋利度。³ 这个二维空间中的单个点用坐标值 (x[0],x[1]) 的小写表示(见图 1.2)。如图所示,建模威胁分数的一个好方法是从线 x[0] + x[1] = 1 的距离进行测量。

图 1.2 猫脑模型的 2D 输入点空间。左下角显示硬度低和锋利度低的物体(–),而右上角显示硬度高和锋利度高的物体(+)。中间值接近对角线($)。
从坐标几何学来看,在一个具有坐标轴 X[0] 和 X[1] 的二维空间中,点 (a, b) 到直线 x[0] + x[1] = 1 的有向距离为 y = (a+b–1)/√2。通过检查 y 的符号,我们可以确定输入点属于分隔线的哪一侧。在图 1.2 所示的简单情况下,观察告诉我们威胁分数可以用从对角线 x[0] + x[1] – 1 = 0 的有向距离 y 来代理。我们可以通过阈值化 y 来做出运行/忽略/接近的决策。接近零的值意味着忽略,正值意味着逃跑,负值意味着接近并咕噜。从高中几何学来看,任意输入点 (x[0]=a, x[1]=b) 到直线 x[0] + x[1] – 1 = 0 的距离是 (a+b–1)/√2。因此,函数 y(x[0], x[1]) = (x[0] + x[1]–1)/√2 是猫脑威胁估计函数的一个可能模型。训练应该收敛到 w[0] = 1/√2, w[1] = 1/√2 和 b = –1/√2。
因此,我们简化的猫脑威胁分数模型是

方程 1.4
它将二维输入点,表示猫面前物体的硬度和尖锐度,映射到一个对应于从分隔线有向距离的 1D 值。这个距离,在物理上可以解释为威胁分数,使得通过阈值化可以分离出类别(负威胁、中性、正威胁),如图 1.2 所示。不同的类别在输出空间中形成不同的簇,由输出空间中的 +, –, 和 $ 符号表示。输入的低值产生负威胁(猫会接近并咕噜):例如,y(0, 0) = –1/√2。输入的高值产生高威胁(猫会逃跑):例如,y(1, 1) = 1/√2。输入的中值产生接近零的威胁(猫会忽略物体):例如,y(0.5, 0.5) = 0。当然,因为问题如此简单,我们可以通过简单的观察得出模型参数。在现实生活中的情况下,这将需要训练。
几何视图在更高维的情况下也成立。一般来说,一个 n-维输入向量
被映射到一个 m-维输出向量(通常 m < n),这样问题在输出空间中就变得简单得多。一个 3D 特征空间的例子如图 1.3 所示。

图 1.3 一个模型将输入(特征)空间中的点映射到一个输出空间,在那里更容易分离类别。例如,在这个图中,属于两个类别(红色 (+) 和绿色 (–))的输入特征点在 3D 特征空间的圆柱体体积中分布。模型将圆柱体展开成矩形。特征点被映射到一个二维平面输出空间,在那里可以通过简单的线性分隔器来区分两个类别。

图 1.4 两个类别(由浅色和深色阴影表示)不能通过一条线来分离。需要一个曲线分离器。在 3D 中,这相当于说没有平面可以分离表面;需要一个曲面。在更高维度的空间中,这相当于说没有超平面可以分离类别;需要一个曲线。
1.5 机器学习中的回归与分类
如在 1.1 节中简要概述的,有两种类型的机器学习模型:回归器 和 分类器。
在 回归器 中,模型试图在给定特定输入的情况下发出一个期望的值。例如,1.3 节中猫脑模型的第一阶段(威胁评分估计器)是一个回归器模型。
另一方面,分类器有一组预定义的类别。给定一个特定的输入,它们试图发出输入所属的 类别。例如,完整的猫脑模型有三个类别:1) 逃跑,(2) 忽视,和 (3) 接近并咕噜。因此,它接受一个输入(硬度和尖锐度值)并发出一个输出决策(即类别)。
在这个例子中,我们通过阈值化回归器的输出(见方程式 1.2)将回归器转换为分类器。也有可能创建直接输出类别而不需要中间回归器的模型。
1.6 线性与非线性模型
在图 1.2 中,我们遇到了一个相当简单的情况,其中类别可以通过一条线(在更高维度的表面上是一个超平面)来分离。这种情况在现实生活中并不常见。如果属于不同类别的点如图 1.4 所示,会怎样呢?在这种情况下,我们的模型架构不应再是一个简单的加权组合。它是一个非线性函数。例如,检查图 1.4 中的曲线分离器。从函数逼近的角度来看,非线性模型也是有意义的。最终,我们的目标是逼近非常复杂且高度非线性的函数,这些函数可以模拟生活所要求的分类或估计过程。直观上看,使用 非线性函数 来模拟它们似乎更好。
机器学习中一个非常流行的非线性函数是 sigmoid 函数,之所以这样命名是因为它看起来像字母 S。sigmoid 函数通常用希腊字母 σ 表示。它定义为

方程式 1.5
双曲正切函数的图形如图 1.5 所示。因此,我们可以使用以下流行的模型架构(仍然相当简单),它采用没有参数的 sigmoid 函数的加权输入总和:

方程式 1.6

图 1.5 双曲正切函数图形
Sigmoid 函数引入了非线性。这种架构可以处理比仅加权求和更复杂的分类任务。事实上,方程 1.6 描述了神经网络的基本构建块。
1.7 通过多个非线性层提高表达能力:深度神经网络
在第 1.6 节中,我们提到,将非线性添加到基本加权和中产生了一个能够处理更复杂任务的模型架构。在机器学习的术语中,非线性模型具有更多的 表达能力。
现在考虑一个现实生活中的问题:比如,构建一个狗识别器。输入空间包括像素位置和像素颜色(x,y,r,g,b,其中 r,g,b 表示像素颜色的红色、绿色和蓝色分量)。输入维度很大(与图像中的像素数量成正比)。图 1.6 给出了典型深度学习系统(如狗图像识别器)必须处理的背景和前景可能变化的简要一瞥。

图 1.6 深度学习系统(此处为狗图像识别器)必须处理的背景和前景变化的简要概述
在这里我们需要一台具有真正高表达能力的机器。我们如何以原则性的方式创建这样的机器?
我们不是一次性从输入生成输出,而是采取级联的方法如何?我们将从输入生成一系列中间或隐藏输出,其中每个隐藏输出本质上是一个单个逻辑回归单元。然后我们添加另一个层,该层以前一层的输出作为输入,依此类推。最后,我们将最外层隐藏层的输出组合成最终输出。
我们以下列方程描述该系统。请注意,我们已对权重添加了上标以标识层(层 0 最接近输入;层 L 是最后一层,最远离输入)。我们还使下标成为二维的(因此给定层的权重成为一个矩阵)。第一个下标标识目标节点,第二个下标标识源节点(参见图 1.7)。

图 1.7 多层神经网络
聪明的读者可能会注意到以下方程没有显式的偏差项。这是因为,为了符号的简洁性,我们将它滚入权重集中,并假设其中一个输入(例如,x[0] = 1)和相应的权重(例如,w[0])是偏差。
层 0:从 n + 1 个输入生成 n 个隐藏输出

方程式 1.7
层 1:从层 0 的 n[0] 个隐藏输出生成 n[1] 个隐藏输出

方程式 1.8
最终层 (L):从 n[L − 1] 个前一层隐藏输出生成 m + 1 个可见输出

方程式 1.9
这些方程式在图 1.7 中展示。图 1.7 中描述的机器可以非常强大,具有巨大的表达能力。我们可以系统地调整其表达能力以适应手头的问题。这样它就变成了一个神经网络。我们将用本书的其余部分来研究这一点。
摘要
在本章中,我们概述了机器学习,一直谈到深度学习。用玩具猫脑的例子说明了这些思想。本章中使用了某些数学概念(例如,向量),但没有适当介绍,鼓励你在介绍了向量和矩阵之后重新阅读本章。
我们希望本章能给你留下以下心理图像:
-
机器学习是一种与传统的计算范式根本不同的计算范式。在传统计算中,我们向计算机提供一系列逐步的指令序列,告诉它要做什么。在机器学习中,我们构建一个数学模型,试图逼近从输入中生成分类或估计的未知函数。
-
模型函数的数学性质是由分类或估计任务的物理性质和复杂性规定的。模型有参数。参数值是从训练数据中估计的——具有已知输出的输入。参数值被优化,以便模型在训练输入上的输出尽可能接近训练输出。
-
机器的另一种几何视图是将多维输入空间中的点映射到输出空间中的一个点的变换。
-
分类/估计任务越复杂,逼近函数就越复杂。在机器学习的术语中,复杂任务需要具有更大表达能力的机器。更高的表达能力来自非线性(例如,Sigmoid 函数;参见方程式 1.5)和更简单机器的分层组合。这把我们带到了深度学习,它不过是一个多层非线性机器。
-
复杂的模型函数通常是通过组合更简单的基函数来构建的。
系好你的安全带:乐趣即将变得更加刺激。
¹ 如果你有一些机器学习的经验,你会意识到我们在这里谈论的是“监督”学习。还有不需要已知输出就能学习的机器——所谓的“无监督”机器——我们稍后会讨论它们。↩
² 在这个背景下,请注意,将误差/损失平方以使其符号独立是一个常见的做法。如果我们希望输出为 10,那么输出为 9.5 或 10.5,我们同样高兴或不高兴。因此,+5 或-5 的误差实际上是相同的;因此,我们使误差符号独立。↩
³ 我们使用 X[0],X[1] 作为坐标符号,而不是更常见的 X,Y,这样在进入更高维空间时不会用完符号。↩
⁴ 在数学中,向量可以拥有无限多个元素。这样的向量不能表示为数组——但在这本书中我们主要会忽略它们。↩
第二章:机器学习中的向量、矩阵和张量
本章涵盖
-
向量和矩阵及其在数据科学中的作用
-
使用特征值和特征向量工作
-
寻找超椭圆的轴
在本质上,机器学习,以及所有计算机软件,都是关于数字处理的。我们将一组数字输入到机器中,并得到一组不同的数字作为输出。然而,这不能是随机的。适当地组织这些数字并将它们分组成有意义的对象,这些对象进入并从机器中出来,这是非常重要的。这就是向量矩阵发挥作用的地方。这些是数学家使用了几百年的概念——我们只是在机器学习中重新使用它们。
在本章中,我们将从机器学习角度研究向量和矩阵,从基础知识开始,迅速过渡到高级概念,限制 ourselves 到与机器学习相关的主题。
我们为本书及本章讨论的大部分概念提供了基于 Jupyter Notebook 的 Python 实现。完整的、功能齐全的代码(在安装 Python 和 Jupyter Notebook 后)可以下载并执行,地址为 mng.bz/KMQ4。本章相关的代码可以在 mng.bz/d4nz 找到。
2.1 向量和它们在机器学习中的作用
让我们回顾一下第 1.3 节中介绍的猫脑机器学习模型。它接受两个数字作为输入,代表猫面前物体的硬度和锋利度。猫脑处理输入并生成一个输出威胁分数,这将导致决定 逃跑、忽略 或 靠近并咕噜咕噜叫。这两个输入数字通常一起出现,将它们组合成一个单一对象将非常方便。这个对象将是两个数字的有序序列,第一个代表硬度,第二个代表锋利度。这样的对象是向量的一个完美例子。
因此,可以将 向量 理解为两个或更多数字的有序序列,也称为数字的 数组。¹ 向量构成了一种紧凑的方式来表示一组数字,这些数字共同代表某个实体。在本书中,向量用带顶箭头的低字母表示,数组用方括号表示。例如,第 1.3 节中猫脑模型的输入是一个向量
,其中 x[0] 代表硬度,x[1] 代表锋利度。
机器学习模型的输出也常常表示为向量。例如,考虑一个以图像为输入并输出一系列数字的对象识别模型,这些数字表示图像包含狗、人类或猫的概率。这种模型的输出是一个三元素向量
,其中数字 y[0] 表示图像包含狗的概率,y[1] 表示图像包含人类的概率,y[2] 表示图像包含猫的概率。图 2.1 显示了可能的输入图像和相应的输出向量。

图 2.1 输入图像及其对应的输出向量,表示图像包含狗、人类和/或猫的概率。显示了示例输出向量。
在像神经网络这样的多层机器中,层的输入和输出可以是向量。我们通常也将模型函数的参数(参见第 1.3 节)表示为向量。这一点在第 2.3 节中有说明。
表 2.1 玩具文档及其对应的描述它们的特征向量。适合特征向量的单词是粗体的。特征向量的第一个元素表示单词枪出现的次数,第二个元素是暴力。
| Docid | 文档 | 特征向量 |
|---|---|---|
| d[0] | 玫瑰很漂亮。没有人讨厌玫瑰。 | [0 0] |
| d[1] | 枪支暴力在美国已经达到了流行病的比例。 | [1 1] |
| d[2] | 枪支暴力的问题实际上被过度炒作。可以找到许多涉及暴力的例子,但没有枪支。 | [2 2] |
| d[3] | 枪支是为暴力倾向的人准备的。暴力滋生枪支。枪支滋生暴力。 | [3 3] |
| d[4] | 我喜欢枪支但讨厌暴力。我从未参与过暴力。但我拥有许多枪支。枪支暴力对我来说是无法理解的。我确实相信枪支拥有者是地球上最反暴力的人。从不使用枪支的人容易陷入无意义的暴力。 | [5 5] |
| d[5] | 昨晚旧金山发生了一起武装抢劫,使用了枪支。 | [1 0] |
| d[6] | 暴力行为通常涉及武器。 | [0 1] |
在机器学习和数据科学中,一个特别重要的概念是 特征向量 的概念。这本质上是一个向量,它描述了在特定机器学习问题中处理的对象的各种属性。我们将通过自然语言处理(NLP)领域的一个例子来说明这个概念。假设我们有一组文档。我们想要创建一个文档检索系统,其中,给定一个新的文档,我们必须检索系统中相似的文档。这本质上归结为以定量方式估计文档之间的相似性。我们将在稍后详细研究这个问题,但现在,我们想要指出,处理这个问题的最自然的方法是为每个文档创建特征向量,这些向量定量描述了文档。在 2.5.6 节中,我们将看到如何测量这些向量之间的相似性;在这里,让我们专注于简单地为文档创建描述向量。做这件事的一个流行方法是选择一组有趣的词(我们通常排除像“and”、“if”和“to”这样的词,这些词在所有文档中都存在),计算每个文档中这些有趣词的出现次数,并创建一个包含这些值的向量。表 2.1 展示了一个包含六个文档及其相应特征向量的玩具示例。为了简单起见,我们只考虑了可能的一组单词中的两个:枪 和 暴力,无论是复数还是单数,大写还是小写。
作为另一个例子,图像中的像素序列也可以被视为一个特征向量。在计算机视觉任务中,神经网络通常期望这个特征向量。
2.1.1 向量的几何视角及其在机器学习中的重要性
向量也可以从几何学的角度来理解。最简单的例子是一个二维向量
。它的两个元素可以被认为是 x 和 y,这是二维空间中的笛卡尔坐标,在这种情况下,向量对应于该空间中的一个点。具有 n 个元素的向量代表 n 维空间中的点。将机器学习模型的输入和输出视为点的能力使我们能够将模型本身视为一种几何变换,它将输入点映射到某些高维空间中的输出点。我们已经在 1.4 节中看到了这一点。这是一个我们将在整本书中使用的极其强大的概念。
向量表示空间中的一个点。此外,一个坐标值数组,例如
,描述了在给定坐标系中一个点的位置 in a given coordinate system。因此,一个坐标值数组(的集合)可以被视为向量的定量表示。参见图 2.2 以获得对此的直观理解。

图 2.2 描述点 P 相对于点 O 的位置的向量。基本的思维图是一个带箭头的线。这与你在高中可能学过的向量的定义一致:向量有一个大小(箭头线的长度)和方向(由箭头指示)。在平面上,这相当于有序对数字 x,y,其中 x 和 y 的几何解释如图所示。在此背景下,值得注意的是,只有点 O 和 P 的相对位置才是重要的。如果两个点都移动,保持它们的关系不变,向量不会改变。
对于一个现实生活中的例子,考虑这本书的一页的平面。假设我们想从左下角到达页面的右上角。让我们称左下角为 O,右上角为 P。我们可以向右移动宽度(8.5 英寸)到达左下角,然后向上移动高度(11 英寸)到达右上角。因此,如果我们选择一个以左下角为原点,X轴沿着宽度,Y轴沿着高度的坐标系,点 P 对应于数组表示
。但我们也可能沿着对角线从左下角到右上角移动到达 P。无论哪种方式,我们最终都会到达同一个点 P。
这导致了一个难题。向量
代表了抽象的几何概念“P相对于O的位置”,与我们选择的坐标轴无关。另一方面,数组表示依赖于坐标系的选择。例如,数组
仅在特定的坐标轴选择(与页面边缘平行)和参考点(左下角)下表示右上角点 P。理想情况下,为了明确,我们应该指定坐标系以及数组表示。为什么在机器学习中我们从不这样做呢?因为在机器学习中,只要我们坚持任何固定的坐标系,坐标系统的具体选择并不重要。机器学习是关于最小化损失函数(我们将在后面学习)。因此,点的绝对位置无关紧要,只有相对位置才是重要的。
存在明确的规则(我们将在后面学习),这些规则说明了当坐标系改变时向量如何变换。在需要时我们将调用这些规则。在机器学习计算中使用的所有向量必须始终使用相同的坐标系或适当地进行变换。
另一点:平面空间,例如本书所写的纸张平面,是二维的(2D)。我们生活的机械世界是三维的(3D)。人类的想象力通常无法看到更高维度。在机器学习和数据科学中,我们经常谈论具有数千维度的空间。你可能无法在心中看到这些空间,但这并不是一个致命的限制。你可以在心中使用三维类比。它们在许多情况下都有效。然而,重要的是要记住,这并不总是正确的。稍后将会展示一些在更高维度中低维直觉失效的例子。
2.2 使用 PyTorch 进行向量操作代码
PyTorch 是由 Facebook 的人工智能团队开发的开源机器学习库。它是目前开发深度学习应用中最优雅的实用工具之一。在本书中,我们旨在使你熟悉 PyTorch 以及类似的编程范式,同时介绍相关的数学知识。假设你具备 Python 基础知识。强烈建议你尝试本书中的所有代码片段(在安装了适当的包,如 PyTorch 之后)。
本书中的所有 Python 代码都是通过 Jupyter Notebook 生成的。在代码片段之前提供了所展示的理论材料的摘要。
2.2.1 使用 PyTorch 的向量介绍代码
列表 2.1 展示了如何使用 PyTorch 创建和访问向量和子向量,以及如何切片和切块向量。
注意:一个完全功能的代码示例,展示如何创建向量及其元素访问,可通过 Jupyter Notebook 执行,可以在mng.bz/xm8q找到。
列表 2.1 通过 PyTorch 介绍向量
v = torch.tensor([0.11, 0.01, 0.98, 0.12, 0.98, ①
,0.85, 0.03, 0.55, 0.49, 0.99,
0.02, 0.31, 0.55, 0.87, 0.63],
dtype=torch.float64) ②
first_element = v[0]
third_element = v[2] ③
last_element = v[-1]
second_last_element = v[-2] ④
second_to_fifth_elements = v[1:4] ⑤
first_to_third_elements = v[:2]
last_two_elements = v[-2:] ⑥
num_elements_in_v = len(v)
u = np.array([0.11, 0.01, 0.98, 0.12, 0.98, 0.85, 0.03,
0.55, 0.49, 0.99, 0.02, 0.31, 0.55, 0.87,
0.63])
u = torch.from_numpy(u) ⑦
diff = v.sub(u) ⑧
u1 = u.numpy() ⑨
① torch.tensor 表示一个多维数组。向量是一个一维张量,可以通过直接指定值来初始化。
② 默认情况下,张量元素是浮点数。我们可以强制张量成为其他类型,如 float64(双精度)。
③ 方括号运算符让我们能够访问单个向量元素。
④ 负索引从数组的末尾开始计数。-1 表示最后一个元素。-2 表示倒数第二个元素。
⑤ 冒号运算符从向量中切片掉一系列元素。
⑥ 冒号之前的内容表示数组的开始。冒号之后的内容表示数组的结束。
⑦ 火炬张量可以从 NumPy 数组初始化。
⑧ 火炬张量与其 NumPy 版本之间的差异为零。
⑨ 火炬张量可以转换为 NumPy 数组。
2.3 矩阵及其在机器学习中的作用
有时候,将一组数字分组成一个向量是不够的。我们必须将几个向量收集到另一个组中。例如,考虑训练机器学习模型的输入。在这里,我们有几个输入实例,每个实例由一系列数字组成。如第 2.1 节所述,单个输入实例的数字序列可以分组成一个向量。我们如何表示整个输入实例集合?这就是数学世界中矩阵概念派上用场的地方。一个 矩阵 可以看作是按固定行数和列数排列的数字的矩形数组。矩阵的每一行是一个向量,每一列也是一个向量。因此,矩阵可以被视为行向量的集合。它也可以被视为列向量的集合。我们可以将构成机器学习模型训练输入的整个数字集合表示为一个矩阵,其中每一行向量对应一个单独的训练实例。
再次考虑我们熟悉的猫脑问题。如前所述,机器的单个输入实例是一个向量
,其中 x[0] 描述了猫面前物体的硬度。现在考虑一个包含许多此类输入实例的训练数据集,每个实例都有一个已知的输出威胁分数。你可能还记得第 1.1 节中提到的,机器学习的目标是创建一个函数,以尽可能少的总体误差将这些输入映射到相应的输出。我们的训练数据可能看起来像表 2.2 所示(注意,在实际问题中,训练数据集通常很大——经常是数百万个输入输出对,但在这个玩具问题中,我们将有 8 个训练数据实例)。
表 2.2 我们玩具机器学习猫脑的示例训练数据集
| 输入值:硬度 | 输入值:锋利度 | 输出:威胁分数 | |
|---|---|---|---|
| 0 | 0.11 | 0.09 | −0.8 |
| 1 | 0.01 | 0.02 | −0.97 |
| 2 | 0.98 | 0.91 | 0.89 |
| 3 | 0.12 | 0.21 | −0.68 |
| 4 | 0.98 | 0.99 | 0.95 |
| 5 | 0.85 | 0.87 | 0.74 |
| 6 | 0.03 | 0.14 | −0.88 |
| 7 | 0.55 | 0.45 | 0.00 |
从表 2.2 中,我们可以将对应硬度和锋利的列收集到一个矩阵中,如图 2.1 所示——这是该问题的训练数据集的紧凑表示。²

方程 2.1
矩阵 X 的每一行是特定的输入实例。不同的行代表不同的输入实例。另一方面,不同的列代表不同的特征元素。例如,矩阵 X 的第 0 行是向量 [x[00] x[01]],代表第 0 个输入实例。其元素 x[00] 和 x[01] 代表不同的特征元素,分别是第 0 个训练输入实例的硬度和锐度。
2.3.1 数字图像的矩阵表示
数字图像也经常表示为矩阵。在这里,每个元素代表图像在特定像素位置(x,y坐标)的亮度。通常,亮度值被归一化到 0 到 255 的整数范围内。0 是黑色,255 是白色,128 是灰色。³以下是一个小图像的例子,宽度为 9 像素,高度为 4 像素:

公式 2.2
亮度从左到右和从上到下逐渐增加。I[00] 代表左上角的像素,是黑色。I[3, 8] 代表右下角的像素,是白色。中间的像素是介于黑白之间的各种灰度。实际图像显示在图 2.2 中。

图 2.3 公式 2.2 中矩阵 I[4, 9] 对应的图像
2.4 使用 PyTorch 介绍矩阵、张量和图像的 Python 代码
为了编程目的,你可以将张量视为多维数组。标量是零维张量。向量是一维张量。矩阵是二维张量。RGB 图像是三维张量(颜色通道 × 高度 × 宽度)。64 张图像的一批是四维张量(64 × 颜色通道 × 高度 × 宽度)。
列表 2.2 使用 PyTorch 介绍矩阵
X = torch.tensor( ①
[
[0.11, 0.09], [0.01, 0.02], [0.98, 0.91],
[0.12, 0.21], [0.98, 0.99], [0.85, 0.87],
[0.03, 0.14], [0.55, 0.45] ②
]
)
print("Shape of the matrix is: {}".format(X.shape)) ③
first_element = X[0, 0] ④
row_0 = X[0, :] ⑤
row_1 = X[1, 0:2] ⑥
column_0 = X[:, 0] ⑦
column_1 = X[:, 1] ⑧
① 矩阵是一个数字的二维数组:即,一个二维张量。一个机器学习模型的整个训练数据输入集可以被视为一个矩阵。每个输入实例是一行。行数 ≡ 训练示例数,列数 ≡ 训练实例大小
② 猫脑训练数据输入:8 个示例,每个示例有两个值(硬度,锐度)。通过指定值创建了一个 8 × 2 的张量。
③ 张量的形状是一个列表。对于一个矩阵,第一个列表元素是行数;第二个列表元素是列数。
④ 方括号提取单个矩阵元素。
⑤ 独立的冒号运算符表示所有可能的索引。
⑥ 冒号运算符表示索引的范围。
⑦ 第 0 列
⑧ 第 1 列
列表 2.3 矩阵的切片和切块
first_3_training_examples = X[:3, ] ①
②
print("Sharpness of 5-7 training examples is: {}"
.format(X[5:8, 1])) ③
① 可以通过冒号运算符指定行和列的范围来切片(提取)子矩阵。
② 提取前三个训练示例(行)
③ 提取第 5 到第 7 个训练示例的锐度特征
列表 2.4 PyTorch 中的张量和图像
tensor = torch.rand((5, 5, 3)) ①
②
I49 = torch.tensor([[0, 8, 16, 24, 32, 40, 48, 56, 64], ③
[64, 72, 80, 88, 96, 104, 112, 120, 128],
[128, 136, 144, 152, 160, 168, 176, 184, 192],
[192, 200, 208, 216, 224, 232, 240, 248, 255]],
) ④
img = torch.tensor(cv2.imread('../../Figures/dog3.jpg')) ⑤
img_b = img[:, :, 0] ⑥
img_g = img[:, :, 1] ⑥
img_r = img[:, :, 2] ⑥
img_cropped = img[0:100, 0:100, :] ⑦
① PyTorch 张量可以用来表示张量。一个向量是一个 1-张量,一个矩阵是一个 2-张量,一个标量是一个 0-张量。
② 创建一个指定维度的随机张量
③ 所有图像都是张量。高度为 H、宽度为 W 的 RGB 图像是一个形状为 [3, H, W] 的 3-张量。
④ 图 2.3 中显示的 4 × 9 单通道图像
⑤ 从磁盘读取 199 × 256 × 3 的图像
⑥ 常规的切片切割操作有效。提取图像中如图 2.4 所示的红、绿、蓝通道。
⑦ 裁剪出如图 2.5 所示的 100 × 100 子图像。

图 2.4 PyTorch 中的张量和图像

图 2.5 狗的裁剪图像
2.5 机器学习中的基本向量和矩阵运算
在本节中,我们介绍了几个基本的向量和矩阵运算及其在图像处理、计算机视觉和机器学习中的重要性,并通过示例进行演示。本节旨在提供一个以应用为中心的线性代数入门。但并非旨在对矩阵和向量运算进行全面回顾,对于这些内容,请参考线性代数教科书。

图 2.6 方程 2.3 中显示的矩阵 I[4, 9] 的转置对应的图像。这相当于将图像旋转 90°。
2.5.1 矩阵和向量转置
在方程 2.2 中,我们遇到了表示微小图像的矩阵 I[4, 9]。假设我们想要将图像旋转 90°,使其看起来像图 2.5。原始矩阵 I[4, 9] 和其转置 I[4,]^T[9] = I[9, 4] 如下所示:

方程 2.3
通过比较方程 2.2 和方程 2.3,你可以很容易地看出,一个可以通过交换行和列索引从另一个获得。这种操作通常被称为 矩阵转置。
形式上,一个具有 m 行和 n 列的矩阵 A[m, n] 的转置是一个具有 n 行和 m 列的另一个矩阵。这个转置矩阵,表示为 A[n,]^T[m],满足 A^T[i, j] = A[j, i]。例如,矩阵 I[4, 9] 中第 0 行第 6 列的值是 48;在转置矩阵中,相同的值出现在第 6 行和第 0 列。在矩阵术语中,I[4, 9][0,6] = I[9,]^T[4][6,0] = 48。
向量转置是矩阵转置的特殊情况(因为所有向量都是矩阵——一个具有 n 个元素的列向量是一个 n × 1 的矩阵)。例如,一个任意向量及其转置如下所示:

方程 2.4

方程 2.5
2.5.2 两个向量的点积及其在机器学习中的作用
在 1.3 节中,我们看到了最简单的机器学习模型,其中输出是通过取输入的加权总和(然后添加一个常数偏置值)来生成的。这个模型/机器的特征是权重 w[0]、w[1] 和偏置 b。以表 2.2 的行为例。例如,对于第 0 行,输入值是接近物体的硬度=0.11 和柔软度=0.09。相应的模型输出将是 y = w[0] × 0.11 + w[1] × 0.09 + b。实际上,训练的目标是选择 w[0]、w[1] 和 b,使得模型输出尽可能接近已知的输出;也就是说,y = w[0] × 0.11 + w[1] × 0.09 + b 应尽可能接近 -0.8,y = w[0] × 0.01 + w[1] × 0.02 + b 应尽可能接近 -0.97,也就是说,一般来说,给定一个输入实例
,模型输出是 y = x[0]w[0] + x[1]w[1] + b。
我们将在本章中反复回到这个模型。但首先,让我们考虑一个不同的问题。在这个玩具示例中,我们只有三个模型参数:两个权重,w[0] 和 w[1],以及一个偏置 b。因此,将模型输出直接写成 y = x[0]w[0] + x[1]w[1] + b 并不是很混乱。但是,对于更长的特征向量(即更多的权重),它将变得难以处理。对于特定的输入实例,有没有一种紧凑的方式来表示模型输出,而不论输入的大小如何?
结果是肯定的——我们可以使用来自数学世界的称为“点积”的操作。我们已经在 2.1 节中看到,模型输入的单个实例可以简洁地表示为一个向量,例如
(它可以有任意数量的输入值)。我们也可以将权重集合表示为向量
——它将具有与输入向量相同数量的项。点积是两个向量
和
的逐元素乘积。形式上,给定两个向量
和
,两个向量的点积定义为

方程式 2.6
换句话说,两个向量对应元素的乘积之和是两个向量的点积,表示为
⋅
。
注意:点积符号可以简洁地表示模型输出为 y =
⋅
+ b。即使输入和权重的数量很大,这种表示法的大小也不会增加。
再次考虑我们(现在已经很熟悉的)猫脑示例。假设权重向量是
且偏置值 b = 5。那么从表 2.2 中 0 号输入实例的模型输出将是
。这些权重和偏置参数的选择并不理想,因为模型输出 5.51 与期望的输出 -0.89 相去甚远。我们很快就会看到如何获得更好的参数值。现在,我们只需注意点积提供了一种简洁的方式来表示简单的加权求和模型输出。
注意:点积仅在向量具有相同维度时才定义。
有时点积也被称为 内积,表示为 ⟨
,
⟩。严格来说,短语“内积”更为通用;它也适用于无限维向量。在这本书中,我们将经常互换使用这些术语,牺牲数学严谨性以增强理解。
2.5.3 矩阵乘法和机器学习
向量是矩阵的特殊情况。因此,矩阵-向量乘法是矩阵-矩阵乘法的特殊情况。我们将从这里开始。
矩阵-向量乘法
在 2.5.2 节中,我们看到了给定一个权重向量,比如
,以及偏置值 b = 5,单个输入实例上的加权求和模型输出可以通过向量-向量点积
表示。如方程 2.1 所示,在训练过程中,我们同时处理许多训练数据实例。在现实生活中,我们通常处理成千上万的输入实例,每个实例有数百个值。有没有一种方法可以紧凑地表示整个训练数据集的模型输出,使其独立于输入实例的数量和大小?
答案是肯定的。我们可以从数学世界的矩阵-向量乘法中借用这个想法。矩阵 X 和列向量
的乘积是另一个向量,表示为 X
。其元素是 X 的行向量与列向量
之间的点积。例如,给定模型权重向量
和偏置值 b = 5,我们熟悉的猫脑模型(方程 2.1)在玩具训练数据集上的输出可以通过以下步骤获得:

方程 2.7
添加偏置值 5,玩具训练数据集上的模型输出为

方程 2.8
通常,我们简单模型的输出(输入元素的偏置加权求和)可以紧凑地表示为
= X
+
。
矩阵-矩阵乘法
将矩阵乘以向量的概念推广后,我们可以定义矩阵乘以矩阵。一个有m行和p列的矩阵,例如A[m, p],可以与另一个有p行和n列的矩阵相乘,例如B[p, n],生成一个有m行和n列的矩阵,例如C[m, n]:例如,C[m, n] = A[m, p] B[p, n]。注意,左矩阵的列数必须与右矩阵的行数相匹配。结果矩阵的元素i, j,即C[i, j],是通过A的第i行向量的元素与B的第j列向量的元素逐点相乘得到的。以下示例说明了这个概念:

通过加粗的方式举例说明了C[2, 1]的计算。
注意:矩阵乘法不是交换的。一般来说,AB ≠ BA。
在这一点上,敏锐的读者可能已经注意到了点积是矩阵乘法的一种特殊情况。例如,两个向量
和
的点积等价于将其中一个向量转置后,再与另一个向量进行矩阵乘法。换句话说,

这个想法在更高维的情况下也适用。一般来说,给定两个向量
和
,这两个向量的点积定义为

方程 2.9
矩阵乘法的另一个特殊情况是行向量矩阵乘法。例如,
^TA =
或者

矩阵乘积的转置
给定两个矩阵A和B,其中A的列数与B的行数相匹配(也就是说,可以相乘),它们的乘积的转置是各自转置的乘积,顺序相反。这个规则也适用于矩阵-向量乘法。以下方程捕捉了这个规则:

方程 2.10
2.5.4 向量的长度(L2 范数):模型误差
假设一个机器学习模型应该输出一个目标值ȳ,但它输出y。我们感兴趣的是模型犯的错误。错误是目标值与实际输出之间的差异。
平方误差
当发生计算错误时,我们只关心计算值与理想值之间的差距。我们并不关心计算值是大于还是小于理想值。例如,如果目标(理想)值是 2,那么计算值 1.5 和 2.5 的误差是相同的——我们对这两个值都同样满意或不满意。因此,通常的做法是将误差值进行平方。例如,如果目标值是 2,计算值是 1.5,那么误差是 (1.5 − 2)² = 0.25。如果目标值是 2.5,误差是 (2.5 − 2)² = 0.25。平方操作本质上消除了误差值的符号。然后我们可以跟一个平方根,但也可以不这样做。
你可能会问,“但是等等:平方改变了数量的值。我们难道不在乎误差的精确值吗?” 答案是,我们通常不在乎;我们只关心误差的 相对 值。如果目标是 2,我们希望输出值为 2.1 的误差小于输出值为 2.5 的误差;误差的精确值并不重要。
让我们将这种平方的思想应用到机器学习模型误差中。如前文第 2.5.3 节所述,给定一个模型权重向量,比如
,以及偏差值 b = 5,单个输入实例的加权求和模型输出,比如
,是
。相应的目标(理想)输出,来自表 2.2,是 −0.8。平方误差 e² = (−0.8−5.51)² = 39.82 给出了模型参数 3, 2, 5 的好坏程度。例如,如果我们改用一个权重向量
和偏差值 −1,我们得到模型输出
。输出与目标完全相同。相应的平方误差 e² = (−0.8−(−0.8))² = 0。这个(零误差)立即告诉我们,1, 1, −1 作为模型参数的选择比 3, 2, 5 要好得多。
通常情况下,一个有偏重的加权求和模型所犯的错误可以表示如下。如果用
表示权重向量,用
表示偏差,则对应于输入实例
的输出可以表示为 y =
⋅
+ b。用 ȳ 表示相应的目标(真实值)。那么错误被定义为 e = (y−ȳ)²。
因此,我们看到我们可以通过取模型输出和真实值之间的差异并对其进行平方来计算单个训练实例的误差。我们如何将这个概念扩展到整个训练数据集?对应于整个训练输入集的输出集可以表示为输出向量 y = X
+
。相应的目标输出向量,由整个真实值集组成,可以表示为
。整个训练集中目标和模型输出之间的差异可以表示为另一个向量
-
。在我们的特定例子中:

因此,整个训练数据集的总误差是通过取输出和真实向量之间的差异,平方其元素并将它们相加来获得的。回忆方程 2.9,这正是如果我们取差异向量的点积会发生的事情。这恰好是向量的平方模或长度或L2 范数的定义:向量与自身的点积。在先前的例子中,整体训练(平方)误差是:

形式上,向量
的长度,表示为||
||,定义为
。这个量有时被称为向量的 L2 范数。
特别是,给定一个输出向量为
的机器学习模型和一个目标向量为
的向量,错误等于差异向量的模或 L2 范数

2.5.5 向量长度的几何直觉
对于一个二维向量
,如图 2.2 所示,L2 范数
不过是直角三角形的斜边,其边是向量的元素。在更高维的情况下,这种直觉同样适用。
一个单位向量是一个长度为 1 的向量。对于任何向量
,相应的单位向量可以通过将该向量的每个元素除以该向量的长度来获得。例如,给定
,长度
和相应的单位向量
。单位向量通常表示一个方向。
注意:单位向量通常用帽子符号表示,而不是小箭头,如û^Tû = 1。
在机器学习中,训练的目标通常是使错误向量(模型输出向量与目标真实向量之间的差异)的长度最小化。
2.5.6 点积的几何直觉:特征相似度
再次考虑表 2.1 中描述的文档检索问题。我们有一组文档,每个文档都由其自身的特征向量描述。给定这样一对文档,我们必须找到它们的相似度。这本质上归结为估计两个特征向量之间的相似度。在本节中,我们将看到一对向量的点积可以用作它们之间相似度的度量。
例如,考虑表 2.1 中对应于 d[5] 和 d[6] 的特征向量。它们是
和
。它们之间的点积是 1 × 0 + 0 × 1 = 0。这是很低的,与我们直觉认为它们之间没有共同的感兴趣词汇相符,因此文档非常不相似。另一方面,d[3] 和 d[4] 的特征向量之间的点积是
。这是很高的,与我们直觉认为它们在感兴趣词汇上有许多共同之处,是相似文档相符。因此,我们得到了一个重要概念的初步认识。简单来说,相似的向量有较大的点积,不相似的向量有接近零的点积。
我们将不断回顾这个估计特征向量之间相似度的问题,并使用越来越巧妙的方法来解决它。作为一个初步尝试,我们现在将更详细地研究点积如何衡量向量之间的相似度。首先,我们将证明一个向量在另一个向量上的分量是由点积产生的。利用这一点,我们将展示一对向量的“相似度/一致性”可以通过它们之间的点积来估计。特别是,我们将看到如果向量指向大致相同的方向,它们的点积将高于当向量相互垂直时的情况。如果向量指向相反的方向,它们的点积将是负数。
点积衡量一个向量在另一个向量上的分量
首先考察一个特殊情况:向量在坐标轴上的分量。这可以通过将向量的长度与向量与相关坐标轴之间的角度的余弦值相乘来获得。如图 2.7a 所示,一个向量
可以沿 X 和 Y 轴分解为两个分量,如下

注意向量长度的保持:


(a) 2D 向量在坐标轴上的分量。注意 ||
|| 是斜边的长度。

(b) 点积作为另一个向量上的一个向量的分量
⋅
=
^T
= a[x]b[x] + a[y]b[y] = ||
|| ||
||cos(θ)。
图 2.7 向量分量和点积
现在我们来研究一个向量在另一个任意向量方向上的分量的一般情况(见图 2.7b)。一个向量
在另一个向量
上的分量是
⋅
=
^T
。这相当于 ||
|| ||
||cos(θ),其中 θ 是向量
和
之间的角度。(这已在附录中讨论的二维情况 A.1 中得到证明。如果您想获得更深的直觉,可以阅读它。)
点积衡量两个向量之间的协议
点积可以用向量之间角度的余弦来表示。给定两个向量
和
,如果 θ 是它们之间的角度,我们得到(见图 2.7b)

方程 2.11
使用余弦表达点积使其更容易看出它衡量的是两个向量之间的协议(也称为相关性)。如果向量具有相同的方向,它们之间的角度是 0,余弦是 1,这意味着最大协议。随着向量之间角度的增加,余弦值逐渐减小,直到两个向量相互垂直且余弦值为零,这意味着没有相关性——向量相互独立。如果它们之间的角度是 180°,余弦值为-1,这意味着向量是反相关的。因此,两个向量的点积与它们的方向协议成正比。
在所有这些中,向量长度扮演什么角色?两个向量的点积也正比于向量的长度。这意味着较大向量之间的同意度得分更高(美国总统和德国总理之间的协议比您和我之间的协议更重要)。
如果您想让协议得分对向量长度保持中性,您可以使用沿相同方向单位长度的向量的归一化点积:

点积和两个单位向量之间的差异
要深入了解点积如何指示两个方向之间的共识或相关性,考虑两个单位向量
和
。它们之间的差异是
。
注意,由于它们是单位向量,
。差异向量的长度

从最后一个等式可以看出,较大的点积意味着较小的差异:也就是说,向量之间有更多的共识。
2.6 向量的正交性及其物理意义
尝试将物体移动到与您推的方向垂直的方向。你会发现这是不可能的。角度越大,你的力向量变得越不有效(最终在 90° 角时完全无效)。这就是为什么在水平面上行走很容易(你是在与重力吸引方向垂直的方向上移动,所以重力向量无效),但在向上倾斜的表面上行走则更难(重力向量对你产生了一些反作用)。
这些物理概念在点积的概念中被数学化地捕捉。两个向量
(例如,推力向量)和
(例如,被推物体的位移向量)的点积是 ||
|| ||
||cosθ,其中 θ 是两个向量之间的角度。当 θ 为 0(两个向量对齐)时,cosθ = 1,是 cosθ 的最大可能值,因此推力是最有效的。随着 θ 的增加,cosθ 减少,推力变得越来越不有效。最后,当 θ = 90° 时,cosθ = 0,推力变得完全无效。
如果两个向量的点积为零,则这两个向量是正交的。从几何上看,这意味着向量彼此垂直。从物理上看,这意味着两个向量是独立的:一个不能影响另一个。你可以这样说,正交向量之间没有共同之处。例如,d[5] 的特征向量是
,而 d[6] 的特征向量是
(见表 2.1)。这些是正交的(点积为零),你可以很容易地看到,两个文档中都没有共同的特征词(枪,暴力)。
2.7 Python 代码:通过 PyTorch 的基本向量和矩阵运算
在本节中,我们使用 Python PyTorch 代码来阐述前面讨论的许多概念。
NOTE 完整功能的代码可在 Jupyter Notebook 中执行,可在mng.bz/ryzE找到。
2.7.1 矩阵转置的 PyTorch 代码
以下列表显示了矩阵转置的 PyTorch 代码。
列表 2.5 转置
I49 = torch.stack([torch.arange(0, 72, 8), torch.arange(64, 136, 8), ①
torch.arange(128, 200, 8), torch.arange(192, 264, 8)])
I49_t = torch.transpose(I49, 0, 1) ②
for i in range(0, I49.shape[0]):
for j in range(0, I49.shape[1]):
assert I49[i][j] == I49_t[j][i] ③
assert torch.allclose(I49_t, I49.T, 1e-5) ④
① torch.arange 函数创建一个元素从 start 到 stop 以 step 为增量的向量。这里我们创建一个 4 × 9 的图像,对应于方程 2.2 中的 I[4,9],如图 2.3 所示。
② 转置运算符交换行和列。4 × 9 的图像变为 9 × 4 的图像(见图 2.6。位置 (i, j) 的元素与位置 (j, i) 的元素交换。
③ 原始矩阵和转置矩阵的交换元素相等。
④ .T 运算符检索数组的转置。
2.7.2 PyTorch 点积的代码
两个向量
和
的点积表示一个向量沿另一个向量的分量。考虑两个向量
= [a[1] a[2] a[3]] 和
= [b[1] b[2] b[3]]。那么
.
= a[1]b[1] + a[2]b[2] + a[3]b[3]。
列表 2.6 点积
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
a_dot_b = torch.dot(a, b)
print("Dot product of these two vectors is: "
"{}".format(a_dot_b)) ①
# Dot product of perpendicular vectors is zero
vx = torch.tensor([1, 0]) # a vector along X-axis
vy = torch.tensor([0, 1]) # a vector along Y-axis
print("Example dot product of orthogonal vectors:"
" {}".format(torch.dot(vx, vy))) ②
① 输出 32:1 ∗ 4 + 2 ∗ 5 + 3 ∗ 6
② 输出 0:1 ∗ 0 + 0 ∗ 1
2.7.3 PyTorch 矩阵-向量乘法的代码
考虑一个具有 m 行和 n 列的矩阵 A[m, n],它与一个具有 n 个元素的向量
[n] 相乘。结果是 m 个元素的列向量
[m]。在以下示例中,m = 3,n = 2。

一般而言,

列表 2.7 矩阵-向量乘法
X = torch.tensor([[0.11, 0.09], [0.01, 0.02], [0.98, 0.91], [0.12, 0.21], ①
[0.98, 0.99], [0.85, 0.87], [0.03, 0.14], [0.55, 0.45],
[0.49, 0.51], [0.99, 0.01], [0.02, 0.89], [0.31, 0.47],
[0.55, 0.29], [0.87, 0.76], [0.63, 0.24]]) ②
w = torch.rand((2, 1)) ③
b = 5.0
= torch.matmul(X, w) + b ④
① 线性模型由权重向量
和偏置 b 组成。对于每个训练数据实例
[i],模型输出 y**[i] =
[i]^T
+ b。对于训练数据矩阵 X(其行是训练数据实例),模型输出 X
+
= 
② Cat-brain 15 × 2 训练数据矩阵(方程 2.7)
③ 权重向量的随机初始化
④ 模型训练输出:
= X
+ b。标量 b 会自动复制以创建一个向量。
2.7.4 PyTorch 矩阵-矩阵乘法的代码
考虑一个具有 m 行和 p 列的矩阵 A[m, p]。让我们用另一个具有 p 行和 n 列的矩阵 B[p, n] 相乘。结果矩阵 C[m, n] 包含 m 行和 n 列。请注意,左矩阵 A 的列数应与右矩阵 B 的行数相匹配:

一般而言,

列表 2.8 矩阵-矩阵乘法
A = torch.tensor([[1, 2], [3, 4], [5, 6]])
B = torch.tensor([[7, 8], [9, 10]])
C = torch.matmul(A, B) ①
②
w = torch.tensor([1, 2, 3])
x = torch.tensor([4, 5, 6])
assert torch.dot(w, x) == torch.matmul(w.T, x) ③
① C = AB ⟹ C[i, j] 是 A 的第 i 行与 B 的第 j 列的点积。
② 
③ 点积可以看作是一个行矩阵乘以一个列矩阵。
2.7.5 PyTorch 中矩阵乘积转置的代码
给定两个矩阵 A 和 B,其中 A 的列数与 B 的行数相匹配,它们的乘积的转置是各自转置的乘积(顺序相反):(AB)^T = BTAT。
列表 2.9 矩阵乘积的转置
assert torch.all(torch.matmul(A, B).T == torch.matmul(B.T, A.T)) ①
assert torch.all(torch.matmul(A.T, x).T == torch.matmul(x.T, A)) ②
① 声明 (AB)^T 和 BTAT 相等
② 也适用于矩阵-向量乘法:(AT*)*T =
^TA
2.8 多维直线和平面方程与机器学习
从几何学的角度来看,机器学习分类器实际上做什么?我们在第 1.4 节中提供了答案的概要。请回顾该内容,特别是图 1.2 和 1.3。我们在这里将简要总结。
分类器的输入是特征向量。这些向量可以看作是多维特征空间中的点。分类的任务就是将属于不同类的点分开。这些点可能在输入空间中杂乱无章。这是模型的工作,将它们转换到不同的(输出)空间中,在那里更容易分开类。这种转换的视觉示例已在图 1.3 中提供。
分离器的几何性质是什么?在非常简单的情况下,例如图 1.2 所示的情况,分离器是二维空间中的一条线。在现实生活中的情况下,分离器通常是高维空间中的一条线或一个平面。在更复杂的情况下,分离器是一个曲面,如图 1.4 所示。
在本节中,我们将研究高维空间中两种类型分离器(直线和平面,也称为超线和超平面)背后的数学和几何。
2.8.1 多维直线方程
在高中几何中,我们学习了 y = mx + c 作为直线的方程。但这并不容易应用于更高维度。在这里,我们将研究一种更好的直线表示方法,它对任何有限维空间都同样有效。
如图 2.8 所示,连接向量
和
的直线可以看作是我们将遇到的点的集合,如果我们
-
从点
开始 -
沿着方向
−
移动

图 2.8 在连接两个向量
和
的直线上,任意点
可以表示为
=
+ α(
−
)。
通过旅行不同的距离,可以得到线上的不同点。用 α 表示这个任意距离,连接向量
和
的直线方程可以表示为

方程式 2.12
方程式 2.12 表示,连接
和
的直线上的任意点都可以表示为
和
的加权组合,权重为 α 和 1 − α。通过改变 α,我们可以得到直线上的不同点。不同的 α 值范围产生直线上的不同线段。如图 2.8 所示,α 在 0 和 1 之间的值产生在
和
之间的点。α 的负值产生在
的左侧。α 大于 1 的值产生在
的右侧。这个直线方程适用于任何维度,而不仅仅是二维。
2.8.2 多维平面及其在机器学习中的作用
在 1.5 节中,我们遇到了分类器。让我们再看看它们。假设我们想要创建一个分类器,帮助我们根据仅有的三个输入变量:1) 动量,即股价变化的速率(正动量意味着股价在上升,反之亦然);2) 上个季度支付的 股息;以及 3) 波动性,即股价在上个季度的波动幅度。让我们在特征空间中绘制所有训练点,坐标轴对应于变量 动量、股息、波动性。图 2.9 显示,类别可以通过三维特征空间中的一个平面来分离。

图 2.9 一个玩具机器学习分类器,用于股票买入与不买入决策。加号 (+) 表示不买入,减号 (-) 表示买入。决策是基于三个输入变量:动量、股息和波动性。
从几何学的角度来看,我们的模型简单地对应于这个平面。位于平面之上的输入点表示买入决策(破折号 [-]),而位于平面之下的输入点表示不买入决策(加号 [+])。一般来说,你想要买入高动量股票,因此动量轴较高端的点更有可能被标记为 买入。然而,这并不是唯一的指标。对于更波动的股票,我们要求更高的 动量 来从 不买入 切换到 买入。这就是为什么随着我们向右移动(波动性更高),平面向上倾斜(动量更高)。此外,对于股息率更高的股票,我们要求更低的 动量。这就是为什么随着我们向更高的 股息率 移动,平面向下倾斜(动量更低)。
实际问题通常有更多的维度(因为决策中涉及更多的输入),分隔符变成超平面。此外,在实际问题中,点在输入空间中通常交织得太紧密,以至于任何分隔符都无法工作。我们首先必须应用一个变换,将点映射到一个更容易分离的输出空间。鉴于它们在机器学习中的类别分隔符的重要性,我们将在本节中研究超平面。
在高中三维几何中,我们学习了 ax + by + cz + d = 0 作为平面的方程。现在我们将研究它在更高维度上工作的版本。
从几何学的角度来看,给定一个平面(在任何维度上),我们可以找到一个称为 法线方向 的方向,表示为 n̂,使得
-
如果我们在平面上取任意一对点,比如说
[0] 和
, … -
连接
和
[0] 的线,即向量
−
[0],与 n̂ 正交。
因此,如果我们知道平面上的一个固定点,比如说
[0],那么平面上的所有点都将满足
n̂ · (
−
[0]) = 0
或者
n̂^T(
−
[0]) = 0
因此,我们可以将平面的方程表示为
n̂^T
− n̂^T
[0] = 0
方程 2.13
方程 2.13 在图 2.10 中以图形方式表示。

图 2.10 平面上的法线在平面的所有点上都是相同的。这是平面的基本性质。n̂ 表示法线方向。设
[0] 为平面上的一个点。平面上所有其他点,表示为
,都将满足方程 (
−
[0]) ⋅ n̂ = 0。这从物理上说明,连接平面上的已知点
[0] 和平面上的任意其他任意点
的直线与法线 n̂ 成直角。这种表述适用于任何维度。
在 1.3 节中,我们遇到了最简单的机器学习模型:输入的加权和以及偏差。将输入表示为
,权重表示为
,偏差表示为 b,该模型被描述为
^T
+ b = 0
方程 2.14
比较方程 2.13 和 2.14,我们得到几何意义:方程 1.3 的简单模型实际上是一个平面分离器。其权重向量
对应于平面的方向(法线)。偏差 b 对应于平面的位置(平面上的一个固定点)。在训练过程中,我们正在学习权重和偏差——这本质上是在学习将训练输入分开的最优平面的方向和位置。为了与机器学习范式保持一致,从现在起我们将超平面的方程写作方程 2.14,其中包含某个常数
和 b。
注意,
不一定是单位长度的向量。由于右侧为零,如果需要,我们可以将两边都除以 ||
|| 来转换为类似于方程 2.13 的形式。
表达式
^T
+ b 的符号具有特殊意义。对于所有满足
^T
+ b < 0 的点
,它们位于超平面的同一侧。对于所有满足
^T
+ b > 0 的点
,它们位于超平面的另一侧。当然,对于所有满足
^T
+ b = 0 的点
,它们位于超平面上。
应该注意的是,三维方程 ax + by + cz + d = 0 是方程 2.14 的一个特例,因为 ax + by + cz + d = 0 可以重写为

这与
^T
+ b = 0 相同,其中
和
。顺便提一下,这告诉我们,在三维空间中,平面 ax + by + cz + d = 0 的法向量是
。
2.9 线性组合,向量张成,基向量,和共线性保持
到目前为止,应该很清楚,机器学习和数据科学都是关于高维空间中的点。因此,我们有必要对这些空间有一个相当的了解。例如,给定一个空间,我们可能需要问,“是否可以用一组少数几个向量来表示空间中的所有点?我们真正需要多少个向量才能达到这个目的?”本节致力于研究这些问题。
2.9.1 线性相关
考虑图 2.11 中显示的向量(点)。在 2D 中的对应向量是

我们可以找到四个标量 α[0] = 2, α[1] = 2, α[2] = 2, 和 α[3] = −3,使得

如果我们能找到这样的标量,它们不全为零,我们说向量
[0],
[1],
[2], 和
[3] 是 线性相关 的。需要记住的几何图像是,对应于线性相关向量的点位于包含它们的单一直线上。

图 2.11 2D 平面中的线性相关点
共线性意味着线性相关
证明:设
,
和
是三个共线向量。根据方程 2.12,存在某个 α ∈ ℝ,使得
= (1−α)
+ α
这个方程可以重写为
α[1]
+ α[2]
+ α[3]
= 0
其中 α[1] = (1−α), α[2] = α 和 α[3] = −1。因此,我们已经证明了三个共线的向量
,
, 和
也必须是线性相关的。
线性组合
给定一组向量
[1],
[2], ….
[n] 和一组标量权重 α[1], α[2], …α[n],它们的加权求和 α[1]
[1] + α[2]
[2] + + … α**[n]
[n] 被称为 线性组合。
线性相关的多维定义
一组向量
[1]、
[2]、……
[n] 如果存在一组权重 α[1]、α[2]、…… α[n](不全为零),使得 α[1]
[1] + α[2]
[2] + + … α**[n]
[n] = 0,则称这些向量线性相关。例如,行向量 [1 1] 和 [2 2] 是线性相关的,因为 –2[1 1] + [2 2] = 0。
2.9.2 向量集合的张量积
给定一组向量
[1]、
[2]、……
[n],它们的张量积定义为所有可以由原始集合的线性组合构成的向量集合。这包括原始向量。
例如,考虑两个向量
和
。这两个向量的张量积是包含这两个向量的整个平面。任何向量,例如向量
,都可以表示为加权求和 18
[x⊥] + 97
[y⊥]。
你可能已经认出
和
是熟悉的二维平面中的笛卡尔坐标轴(X-轴和Y-轴,分别)。
2.9.3 向量空间、基向量和封闭性
我们一直在非正式地讨论向量空间。现在是时候更精确地定义它们了。
向量空间
在 n 维度中的向量(点)集合形成一个向量空间,当且仅当该集合上定义了加法和标量乘法运算。特别是,这意味着可以取向量空间成员的线性组合。
基向量
给定一个向量空间,一组张量积空间中的向量被称为该空间的基。例如,对于空间ℝ²,两个向量
和
是基向量。这本质上意味着ℝ²中的任何向量都可以表示为这两个向量的线性组合。这个概念可以扩展到更高维度。对于ℝ^n,向量
构成一个基。
警觉的读者可能已经猜到了,基向量与坐标轴有关。事实上,上面描述的基向量构成了笛卡尔坐标轴。
到目前为止,我们只看到了基向量的例子,这些向量是相互正交的,例如前面展示的ℝ²中的两个基向量的点积:
。然而,基向量不必是正交的。任何一对线性无关的向量在ℝ²中形成一个基。因此,基向量绝不是唯一的。尽管如此,正交向量是最方便的,我们将在后面看到。
最小和完备基
需要n个向量来张成具有n维度的空间。这意味着空间的基集合将至少包含与空间维度一样多的元素。那么多的基向量也足以形成一个基。例如,需要恰好n个向量来形成(即张成)ℝ^n的基。
一个相关的事实是,在ℝ^n中,任何包含m个向量且m > n的集合将是线性相关的。换句话说,在n-维空间中,线性无关向量集合的最大大小是n。
封闭性
如果一个向量集合中的任意一对向量的线性组合也属于该集合,则称该向量集合在线性组合下是封闭的。考虑向量集合ℝ²。回想一下,这是包含两个实数元素的向量的集合。取ℝ²中的任意一对向量
和
:例如,
和
。这两个向量的任意线性组合也将包含两个实数——也就是说,将属于ℝ²。我们称ℝ²是一个向量空间,因为它在线性组合下是封闭的。
考虑空间ℝ²。从几何上讲,这代表一个二维平面。让我们在这个平面上取两个点
和
。
和
的线性组合在几何上对应于连接它们的直线上的点。我们知道,如果两个点位于一个平面上,那么整个直线也将位于该平面上。因此,在二维中,平面在线性组合下是封闭的。这是向量空间中封闭性概念的几何直觉。它可以扩展到任意维度。
另一方面,球面上点的集合在线性组合下不是封闭的,因为连接该集合中任意一对点的直线不会完全位于该球面的表面上。
2.10 线性变换:几何和代数解释
机器学习或数据科学系统的输入通常是特征向量(在第 2.1 节中介绍),它们位于高维空间中。特征向量的每个单独维度对应于输入的一个特定属性。因此,特征向量是特定输入实例的描述符。它可以被视为特征空间中的一个点。我们通常将点转换到更容易进行我们试图进行的分析的空间中。例如,如果我们正在构建一个分类器,我们试图将输入转换到不同类别的点更加分离的空间中(参见一般性的第 1.3 节和特定的图 1.3)。有时我们进行变换以简化数据,消除数据中变化很少的轴。鉴于它们在机器学习中的重要性,在本节中,我们将研究变换的基本知识。
非正式地说,变换是一种将一组点(向量)映射到另一组点的操作。给定一个由 n × 1 向量组成的集合 S,任何 m × n 矩阵 T 都可以被视为一个变换。如果
属于集合 S,则与矩阵 T 的乘法将(变换)
映射到向量 T
。我们稍后将会看到,矩阵乘法是保持共线性的变换的一个子类——变换前位于直线上的点在变换后将继续位于(可能不同的)直线上。例如,考虑以下矩阵

在第 2.14 节中,我们将看到这是一种称为旋转矩阵的特殊类型的矩阵;现在,只需将其视为矩阵的一个例子。R 是一个变换算子,它将二维平面上的一个点映射到同一平面上的另一个点。用数学符号表示,R : ℝ² → ℝ²。实际上,如图 2.14 所示,这种变换(通过矩阵 R 的乘法)将二维平面上的一个点的位置向量旋转了 45°。
在这种变换中,输出点和输入点可能属于不同的空间。例如,考虑以下矩阵

很容易看出,这个矩阵将三维点投影到二维 X-Y 平面上:

因此,这种变换(通过矩阵 P 的乘法)将点从三维投影到二维。在数学术语中,P : ℝ³ → ℝ²。
变换 R 和 P 具有共同属性:它们保持共线性。这意味着一组原本位于直线上的向量(点)
、
、
、⋯ 在变换后仍然保持直线。
让我们检查一下在 2.9 节中的例子中的旋转变换。在那里我们看到了四个向量:

这些向量都位于一条直线上 L : x = y。这些向量的旋转变换版本是

很容易看出,变换后的向量也位于一条(不同的)直线上。事实上,
^′,
^′,
^′,
^′ 位于 Y 轴上,这是原始直线 y = x 的 45° 旋转版本。
很容易看出,变换后的向量也位于一条(不同的)直线上。事实上,
^′,
^′,
^′,
^′ 位于 Y 轴上,这是原始直线 y = x 的 45° 旋转版本。
由矩阵 P 表示的投影变换也保持共线性。考虑三维空间中的四个共线向量:

对应的变换向量

也位于二维空间中的一条直线上。
保持共线性的变换类被称为 线性变换。它们总是可以表示为矩阵乘法。相反,所有的矩阵乘法都代表线性变换。稍后提供更正式的定义。
2.10.1 线性变换的多维通用定义
一个函数 ϕ 是线性变换当且仅当它满足
ϕ(α
+ β
) = αϕ(
) + βϕ(
) ∀ α, β ∈ ℝ
方程 2.15
换句话说,一个变换是线性的当且仅当两个向量的线性组合的变换与单个向量的变换的线性组合(具有相同的权重)相同。(这可以记住为:线性变换意味着线性组合的变换与变换的线性组合相同。)与旋转或投影矩阵(前面已展示)的乘法是一个线性变换。
2.10.2 所有矩阵-向量乘法都是线性变换
让我们验证矩阵乘法是否满足线性映射的定义(方程 2.15)。设
,
∈ ℝ^n 是两个任意的 n-维向量,A[m, n] 是一个任意的具有 n 列的矩阵。然后按照矩阵-向量乘法的标准规则,
A(α
+ β
) = α(A
) + β(A
)
它模仿了方程 2.15,其中 ϕ 被矩阵 A 替换。因此,我们已经证明了所有矩阵乘法都是线性变换。反之则不成立。特别是,作用在无限维向量上的线性变换不是矩阵。但所有作用在有限维向量上的线性变换都可以表示为矩阵。(证明稍微复杂一些,将省略。)
因此,在有限维度中,矩阵乘法和线性变换是同一件事。在 2.3 节中,我们看到了矩阵的数组视图。相应的几何视图,即所有矩阵都表示线性变换,在本节中已经介绍。
让我们通过研究一个 不是 线性的变换的例子来结束本节。考虑以下函数
ϕ(
) = ||
||
对于
∈ ℝ^n。这个函数 ϕ 将 n 维向量映射到一个标量,即向量的长度,ϕ : ℝ^n → ℝ。我们将检查它是否满足方程 2.15 中 α[1] = α[2] = 1 的条件。对于两个特定的向量
,
∈ ℝ^n,

现在

和

显然,这两个不相等;因此,我们违反了方程 2.15:ϕ 是一个非线性映射。
2.11 多维数组、多元线性变换和张量
我们经常在机器学习的背景下听到 张量 这个词。谷歌著名的机器学习平台被命名为 TensorFlow。在本节中,我们将向您介绍张量的概念。
2.11.1 数组视图:数字的多维数组
张量可以被视为一个广义的 n 维数组——尽管严格来说,并非所有多维数组都是张量。当我们学习多元线性变换时,我们将了解更多关于多维数组和张量之间区别的信息。现在,我们不必过于担心这种区别。一个向量可以被视为一个 1 维张量,一个矩阵是一个 2 维张量,而一个标量是一个 0 维张量。
在 2.3 节中,我们了解到数字图像被表示为二维数组(矩阵)。一个彩色图像——其中每个像素由三种颜色,R、G 和 B(红色、绿色和蓝色)表示——是一个多维数组或张量的例子。这是因为它可以被视为三个图像的组合:R、G 和 B 图像,分别。
神经网络中每一层的输入和输出也是张量。
2.12 线性系统和矩阵逆
今天的机器学习通常是一个迭代过程。给定一组训练数据,你希望估计一组机器参数,这些参数将在训练输入上产生目标值(或接近它们的近似值)。训练输入的数量和参数集的大小通常非常大。这使得我们无法有一个闭式解,其中我们一次性求解未知参数。解决方案通常是迭代的。我们从参数的猜测值集合开始,通过处理训练数据来迭代地改进猜测。
话虽如此,在现实生活中,我们经常遇到更小的问题。我们最好使用更传统的闭式技术,因为它们要快得多,也更准确。本节致力于对这些技术进行一些了解。
让我们回到我们熟悉的猫脑问题,并参考其训练数据表 2.2。和之前一样,我们仍在讨论一个具有三个参数的加权求和模型:权重 w[0]、w[1] 和偏置 b。让我们专注于表中的前三行,为了方便起见,这里再次以表 2.2 的形式呈现。
表 2.3 基于玩具机器学习的猫脑示例训练数据集
| 输入值:硬度 | 输入值:锋利度 | 输出:威胁分数 | |
|---|---|---|---|
| 0 | 0.11 | 0.09 | −0.8 |
| 1 | 0.01 | 0.02 | −0.97 |
| 2 | 0.98 | 0.91 | 0.89 |
训练数据表明,当硬度值为 0.11 且锋利度值为 0.09 时,我们期望系统的输出与目标值 -0.8 匹配或接近,等等。换句话说,我们估计的参数 w[0]、w[1]、b 的值应理想地满足
0.11w[0] + 0.09w[1] + b = –0.8
0.01w[0] + 0.02w[1] + b = –0.97
0.98w[0] + 0.91 w[1] + b = 0.89
我们可以通过矩阵乘法表达如下方程:

我们如何获得使这个方程成立的 w[0]、w[1]、b 的值?也就是说,我们如何解这个方程?有正式的方法(稍后讨论)可以直接解这类方程,以求解 w[0]、w[1] 和 b(在这个非常简单的例子中,你可能“看到”w[0] = 1、w[1] = 1、b = −1 可以解这个方程,但我们需要一个通用方法)。
这个方程是称为线性系统的一类方程的例子。在 n 个未知数 x[1]、x[2]、x[3]、⋯、x[n] 中的线性系统,
a[11]x[1] + a[12]x[2] + a[13]x[3] + … + a[1n]x[n] = b[1]
a[21]x[1] + a[22]x[2] + a[23]x[3] + … + a[2n]x[n] = b[2]
⁞
a[n1]x[1] + a[n2]x[2] + a[n3]x[3] + … + a[nn]**x[n] = b[n]
可以通过矩阵和向量表示为
A
= 
其中

虽然等价,但矩阵表示更紧凑且与维度无关。在机器学习中,我们通常有很多变量(数千个),这种紧凑性产生了显著差异。此外,A
=
看起来与我们非常熟悉的单变量方程相似:ax = b。事实上,许多直觉可以从一维转移到高维。
一维方程的解是什么?你可能在上五年级时就学过了:ax = b 的解是 x = a^(−1)b,其中 a^(−1) = 1/a,a ≠ 0。
我们可以在所有维度中使用相同的符号。A
=
的解是
= A^(−1)
,其中 A^(−1) 是矩阵的逆。逆矩阵 A^(−1) 有矩阵的行列式,1/det(A), 作为因子。我们不会在这里讨论行列式和逆矩阵的计算——你可以在任何标准的线性代数教科书中找到这些——但会陈述一些有助于理解行列式和逆矩阵的事实:
-
逆矩阵 A^(−1) 与矩阵 A 的关系与标量 a^(−1) 与标量 a 的关系相同。a^(−1) 存在当且仅当 a ≠ 0。类似地,A^(−1) 存在当 det(A) ≠ 0,其中 det(A) 指的是矩阵的行列式。
-
标量 a 和其逆 a^(−1) 的乘积是 1。类似地,AA^(−1) = A^(−1)A = I,其中 I 表示单位矩阵,它是标量算术中 1 的高维类似物。它是一个对角线项为 1,其他项为 0 的矩阵。n-维单位矩阵如下:

当没有下标时,可以从上下文中推断出维度。对于任何矩阵 A,IA = AI = A。对于任何向量
,I
=
^TI =
。这些可以通过矩阵乘法的规则轻松验证。
计算行列式和矩阵逆有完全精确但繁琐的规则。尽管这个概念很重要,但在生活中我们很少需要计算它们,因为所有线性代数软件包都提供了执行此操作的例程。此外,计算矩阵逆不是好的编程实践,因为它在数值上是不稳定的。我们不会在这里讨论行列式或矩阵逆的直接计算(除了附录 A.2 中,我们展示了如何计算 2 × 2 矩阵的行列式)。我们将讨论伪逆,这在机器学习中具有更重要的意义。
2.12.1 行列式为零或接近零的线性系统,以及病态系统
我们之前看到,线性系统 A
=
的解是
= A^(−1)
。但 A^(−1) 有 1/det(A) 作为因子。如果行列式为零怎么办?
简短的回答:当行列式为零时,线性系统无法精确求解。我们仍然可以尝试找到一个近似答案(见第 2.12.3 节),但精确解是不可能的。
让我们借助一个例子更仔细地考察一下这种情况。考虑以下方程组:
* x*[1] + x[2] = 2
2x[1] + 2x[2] = 4
它可以重写为一个具有方阵的线性系统:

但你可以很快看出,这个方程组无法求解。第二个方程实际上与第一个方程相同。事实上,我们可以通过将第一个方程乘以一个标量,2,来得到第二个方程。因此,我们实际上只有一个方程,而不是两个,所以这个方程组无法求解。现在考察矩阵 A 的行向量。它们是 [1 1] 和 [2 2]。它们是线性相关的,因为 −2[1 1] + [2 2] = 0。现在考察矩阵 A 的行列式(附录 A.2 展示了如何计算 2 × 2 矩阵的行列式)。它是 2 × 1 − 1 × 2 = 0。这些结果不是巧合。任何一个都意味着另一个。事实上,以下关于线性系统 A
=
(对于方阵)的陈述是等价的:
-
矩阵 A 有一个行/列可以表示为其他行的加权求和。
-
矩阵 A 的行或列线性相关。
-
矩阵 A 的行列式为零(这样的矩阵被称为 奇异矩阵)。
-
矩阵 A 的逆(即 A^(−1))不存在。A 被称为 奇异的。
-
这个线性系统无法求解。
系统试图告诉你,你拥有的方程比你想的少,你无法求解这个方程组。
有时行列式不是正好为零,而是接近于零。虽然理论上可以求解,但这样的系统是 数值不稳定的。输入的微小变化会导致结果发生剧烈变化。例如,考虑这个几乎奇异的矩阵:

方程 2.16
它的行列式是 0.002,接近于零。设
为一个向量。

方程 2.17
(注意* A^(-1) 的元素有多大。这是由于除以一个非常小的行列式,进而导致下文所示的不稳定性。)方程的解为
。但如果我们将
稍微改变一下,使其变为
,解就会变成一个截然不同的
。这是固有的不稳定性,并源于矩阵 A 的近奇异性。这样的线性系统被称为病态的*。
2.12.2 矩阵的逆、行列式和奇异性测试的 PyTorch 代码
通过线性代数包 linalg 的单个函数调用即可求逆矩阵和计算行列式。
列表 2.10 可逆矩阵的矩阵逆(非零行列式)
def determinant(A):
return torch.linalg.det(A)
def inverse(A):
return torch.linalg.inv(A)
A = torch.tensor([[2, 3], [2, 2]], dtype=torch.float ①
A_inv = inverse(A) ②
I = torch.eye(2) ③
assert torch.all(torch.matmul(A, A_inv) == I) ④
assert torch.all(torch.matmul(A_inv, A) == I)
assert torch.all(torch.matmul(I, A) == A)
assert torch.all(A == torch.matmul(A,I)) ⑤
① 
② 
③ PyTorch 函数torch.eye( n )生成一个大小为 n 的单位矩阵 I *
④ 验证
⑤ ** I 就像 1。验证*AI = IA = * A *
奇异矩阵是指行列式为零的矩阵。这样的矩阵是不可逆的。具有奇异矩阵的线性方程组无法求解。
列表 2.11 奇异矩阵
B = torch.tensor([[1, 1], [2, 2]], dtype=torch.float) ①
try:
B_inv = inverse(B) ②
except RuntimeError as e:
print("B cannot be inverted: {}".format(B, e))
① 
② 行列式 = 1 × 2 - 2 × 1 = 0。奇异矩阵;尝试计算逆矩阵会导致运行时错误。
2.12.3 机器学习中的超定和欠定线性系统
如果矩阵* A 不是方阵呢?这意味着方程的数量不等于未知数的数量。这样的系统有意义吗?出人意料的是,它是有意义的。一般来说,机器学习系统属于这一类:方程的数量对应于收集的训练数据实例的数量,而未知数的数量是模型中权重的函数,这是特定模型家族选择来表示系统的函数。这些是相互独立的。如前所述,我们通常迭代地解决这些系统。然而,了解非方阵的线性系统 A *是很重要的,以获得洞察。
假设矩阵* A 是 m * × * n ( m 行和 n *列),有两种可能的情况:
-
情况 1:* m * > * n *(方程多于未知数;超定系统)
-
情况 2:* m * < * n *(方程少于未知数;欠定系统)
例如,表 2.2 导致一个超定线性系统。让我们写下方程组:
0.11 * w[0] + 0.09 * w[1] + b = –0.8
0.01 * w[0] + 0.02 * w[1] + b = –0.97
0.98 * w[0] + 0.91 * w[1] + b = 0.89
0.12 * w[0] + 0.21 * w[1] + b = –0.68
0.98 * w[0] + 0.99 * w[1] + b = 0.95
0.85 * w[0] + 0.87 * w[1] + b = 0.74
0.03 * w[0] + 0.14 * w[1] + b = –0.88
0.55 * w[0] + 0.45 * w[1] + b = 0.00
这会产生以下超定线性系统:

方程式 2.18
这是一个非方阵的 15 × 3 线性系统。只有 3 个未知数需要求解 (w[0], w[1], b),而方程有 15 个。这非常冗余:我们只需要三个方程,就可以通过线性系统求解技术(第 2.12 节)来解决这个问题。但重要的是要注意这一点:方程并不完全一致。没有一组单一的未知数值可以满足所有方程。换句话说,训练数据是嘈杂的——这是现实生活中的机器学习系统的一个几乎普遍现象。因此,我们必须找到一个解决方案,在整个方程中都是最优的(尽可能减少错误)。
我们希望求解它,使得整体误差 ||A
−
|| 最小化。换句话说,我们正在寻找
,使得 A
尽可能接近
。这种闭式(即非迭代)方法是机器学习和数据科学的一个极其重要的先导。我们将在多个地方重新讨论这个问题,特别是在第 2.12.4 节和 4.5 节。
2.12.4 矩阵的摩尔-彭若斯伪逆
伪逆是一种解决超定或欠定线性系统的便捷技术。假设我们有一个超定系统,其非必要方阵 m × n 矩阵 A:
A
= 
由于 A 不保证是方阵,我们通常不能取其行列式或逆。所以,通常的 A^(−1)
是不工作的。在这个时候,我们注意到,尽管不能取逆,但矩阵转置总是可能的。让我们将方程的两边乘以 A^T:
A
=
⇔ A^TA
= A^T
注意到 A^TA 是一个方阵:其维度是 (m×n) × (n×m) = m × m。暂时不进行证明,我们假设它是可逆的。那么
A
=
⇔ A^TA
= A^T
⇔
= (ATA*)(−1)A^T*
嗯嗯,还不错;我们似乎找到了一些东西。事实上,我们刚刚推导出了矩阵 A 的 伪逆,表示为 A ^+ = (ATA*)(−1)A^T*。与逆矩阵不同,伪逆不需要矩阵是方阵且行线性无关。与常规线性系统类似,我们得到(可能非方阵)方程组的解为 A
=
⇔
= A ^+
。
基于伪逆的解实际上最小化了误差 ||A
−
||。我们将在第 2.12.5 节中提供一个直观的证明。同时,鼓励你编写 Python 代码来评估 (ATA*)(−1)A^T*
并验证它大约给出了方程 2.18 预期的答案
。
2.12.5 矩阵的伪逆:一种美丽的几何直觉
矩阵 A[m × n] 可以用其列向量表示为 ![[1],
[2], …
[n]],其中
[1] …
[n] 都是 m-维向量。然后如果
,我们得到 A
= x[1]
[1] + x[2]
[2] + ⋯ x[n]
[n]。换句话说,A
只是 A 的列向量的线性组合,其中
的元素作为权重(鼓励你写出一个小的 3 × 3 系统并验证这一点)。所有形式为 A
的向量空间(即 A 的列向量的线性空间)被称为 A 的 列空间。
线性方程组 A
=
的解可以看作是寻找
,使其最小化 A
和
之间的差异:即最小化 ||A
−
||。这意味着我们正在尝试找到一个点,该点在 A 的列空间中,并且离点
最近的。请注意,这种解释并不假设矩阵 A 是方阵。它也不假设行列式不为零。在友好的情况下,当矩阵 A 是方阵且可逆时,我们可以找到一个向量
,使得 A
完全等于
,这使得 ||A
−
|| = 0。如果 A 不是方阵,我们将尝试找到一个
,使得 A
比任何其他在 A 的列空间中的向量更接近
。从数学上讲,⁴

方程 2.19
从几何学上,我们直观地知道,在 A 的列空间中,离
最近的点是通过对
从垂直于 A 的列空间(见图 2.12)进行投影得到的。这个垂直线与列空间的交点被称为
在 A 的列空间上的 投影。我们正在寻找的方程 2.19 的解向量
应该对应于
在 A 的列空间上的投影。这反过来意味着
− A
与 A 的列空间中的所有向量正交(垂直)。我们用 A
表示 A 的列空间中的任意向量,对于任意的
。因此,对于所有这样的
,

为了使前一个方程对所有向量
都成立,我们必须有 A^T(
−A
) = 0。
因此,我们有

这正是摩尔-彭罗斯伪逆。

图 2.12 解线性系统 A
=
等价于找到 A 的列空间中与
最近的点。这意味着我们必须从
向 A 的列空间引一条垂线。如果 A
表示该垂线与列空间的交点(即投影),则差向量
− A
对应于连接
和其投影 A
的线。这条线将与 A 的列空间中的所有向量垂直。等价地,它对于任意
都与 A
垂直。
对于一个以机器学习为中心的示例,考虑本章前面提到的猫脑对应超定系统。有 15 个训练示例,每个示例都指定了输入和期望输出。
我们的目标是确定三个未知数 w[0]、w[1] 和 b,使得对于每个训练输入
,模型输出

方程 2.20
尽可能接近期望输出(即真实值)ȳ[i]。
注意:在这里我们使用了一个巧妙的方法:我们在输入的右侧添加了 1,这使得我们可以用一个紧凑的矩阵-向量乘法来表示整个系统(包括偏差)。我们称之为增广——我们在输入行向量右侧添加一个额外的 1。
将所有训练示例整理在一起,我们得到

方程 2.21
这可以紧凑地表示为
X
= 
其中 X 是右侧列全为 1 的增广输入矩阵。目标是使 ||
–
|| 最小化。为此,我们构建了超定线性系统
X
= 
注意,这并不是一个经典的方程组——方程的数量多于未知数。我们不能通过矩阵求逆来解这个方程组。然而,我们可以使用伪逆机制来解它。得到的解提供了“最佳拟合”或“最佳努力”解,它最小化了所有训练示例的总误差。
精确的数值系统(为方便参考在此重复)如下

方程 2.22
我们使用伪逆公式求解
= (XTX*)(–1)X^T*
2.12.6 使用 PyTorch 代码解决超定系统
注意:本节的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/PPJ2找到。
列表 2.12 使用伪逆求解超定系统
def pseudo_inverse(A):
return torch.matmul(torch.linalg.inv(torch.matmul(A.T, A)), A.T)
①
X = torch.column_stack((X, torch.ones(15))) ②
③
w = torch.matmul(pseudo_inverse(X), y) ④
print("The solution is {}".format(w)) ⑤
① X 是方程 2.22 中的增广数据矩阵
② Pytorch 列堆叠操作符
向矩阵中添加一列。在这里,添加的列全是 1
③ 容易验证方程 2.22 的解大致为 w[0] = 1, w[1] = 1, b = −1。但方程并不一致:没有一个解能完美地适应所有这些方程。伪逆找到“最佳拟合”解:它最小化了所有方程的总误差。
④ 预期解接近 [1, 1, −1]
⑤ 解为 [1.08, 0.90, −0.96]
2.13 特征值和特征向量:机器学习的瑞士军刀
机器学习和数据科学都是关于在大量高维数据中寻找模式。输入是第 2.1 节中介绍的高维空间中的特征向量。每个特征向量可以看作是输入实例的特征空间描述符中的一个点。有时我们会转换这些特征向量——将特征点映射到一个更友好的空间——通过降低维度来简化数据。这是通过消除数据中变化很少的轴来实现的。特征值和特征向量是机器学习工程师或数据科学家在简化数据和寻找大量多维数据中的广泛模式时的宝贵工具。在第四章中,我们将研究如何使用这些工具来简化数据和在大量多维数据中寻找广泛模式。
让我们先非正式地看看特征向量。它们是方阵的性质。如前所述,矩阵可以看作是线性变换,它将一个空间中的向量(点)映射到相同或不同空间中的不同向量(点)。但典型的线性变换会留下一些空间中的点(几乎)不受影响。这些点被称为 特征向量。它们是变换的重要物理方面。让我们看一个简单的例子。假设我们在 3D 空间中围绕 Z-轴(见图 2.13)旋转点。Z-轴上的点在旋转后仍将保持在原地。一般来说,旋转轴上的点在旋转后不会移动。旋转轴是旋转变换的特征向量。

图 2.13 在旋转过程中,旋转轴上的点位置不变。
将这个想法扩展,当用矩阵 A 对向量
进行 变换 时,是否存在不改变(至少在方向上)的向量?结果是肯定的。这些就是所谓的 特征向量——当通过矩阵 A 进行线性变换时,它们不会改变方向。为了更精确,如果
是方阵 A 的特征向量,⁵ 则有
A
= λ
因此,线性变换(即,矩阵 A 的乘法)改变了
的长度,但没有改变其方向,因为 λ
与
平行。
我们如何获得 λ 和
?嗯,
A
= λ 
⇔ A
- λ
= 
⇔ (A - λ I)
= 
其中 I 表示单位矩阵。
当然,我们只对非平凡解感兴趣,其中
≠
。在这种情况下,A – λI 不能是可逆的,因为如果是的话,我们可以得到矛盾的解
= (A – λ I)^(–1)
=
。因此,(A − λI) 是不可逆的,这意味着行列式
det(A − λI) = 0
对于一个 n × n 矩阵 A,这会产生一个 n 次的多项式方程,对于未知数 λ 有 n 个解。因此,一个 n × n 矩阵有 n 个特征值,不一定都是不同的。
让我们计算一个 3 × 3 矩阵的特征值和特征向量,只是为了好玩。我们使用的矩阵是精心选择的,这一点很快就会变得明显。但就现在而言,把它看作是一个任意矩阵:

方程 2.23
我们将计算矩阵 A 的特征值和特征向量:

因此,

这里,i = √–1。如果需要,鼓励您从高中代数中复习一下虚数和复数的知识。
因此,我们找到了(正如预期的那样)三个特征值:1,e^(i π/4),和 e^(–i π/4)。每个都会产生一个特征向量。以下是一个例子,让我们计算对应于特征值 1 的特征向量:

因此,
是矩阵 A 对应于特征值 1 的一个特征向量。同样,对于任何实数 k,
也是。实际上,如果 λ,
是矩阵 A 的一个特征值,特征向量对,那么
A
= λ
⇔ A(k
) = λ(k
)
即,λ,(k
) 也是一个矩阵 A 的特征值,特征向量对。换句话说,我们只能确定特征向量到一个固定比例因子。我们可以将特征向量取为单位长度(
^T
= 1),这样不失一般性。
我们示例矩阵的特征向量实际上是 Z-轴。这不是一个巧合。我们的矩阵 A 实际上是在 Z-轴上的旋转。旋转矩阵的特征值总是 1。相应的特征向量将是旋转轴。在 3D 中,其他两个特征值将是复数,产生旋转角度。 这在 2.14 节中有详细说明。
2.13.1 特征向量和线性无关
对应于不等特征值的矩阵的特征向量是线性无关的。让我们来证明这一点以获得一些洞察。设 λ[1],
[1] 和 λ[2],
[2] 是矩阵 A 的特征值,特征向量对,其中 λ[1] ≠ λ[2]。那么
A
[1] = λ[1]
[1]
A
[2] = λ[2]
[2]
如果可能的话,让存在两个常数 α[1] 和 α[2] 使得
α[1]
[1] + α[2]
[2] = 0
方程式 2.24
换句话说,假设两个特征向量是线性相关的。我们将证明这个假设会导致不可能的情况。
将方程 2.24 乘以 A,我们得到
α[1]A
[1] + α[2]A
[2] = 0
⇔ α[1]λ[1]
[1] + α[2]λ[2]
[2] = 0
此外,我们还可以将方程 2.24 乘以 λ[2]。因此我们得到
α[1]λ[1]
[1] + α[2]λ[2]
[2] = 0
α[1]λ[2]
[1] + α[2]λ[2]
[2] = 0
相减,我们得到
α1
[1] = 0
根据假设,α[1] ≠ 0,λ[1] ≠ λ[2] 并且
[1] 不是全零。因此,它们的乘积不可能为零。我们的原始假设(两个特征向量线性相关)一定是错误的。
2.13.2 对称矩阵和正交特征向量
对称矩阵的两个对应于不同特征值的特征向量是互相正交的。让我们来证明这一点以获得更多的洞察。一个矩阵 A 是对称的当且仅当 A^T = A。如果 λ[1],
[1] 和 λ[2],
[2] 是对称矩阵 A 的特征值,特征向量对,那么
A
[1] = λ[1]
[1]
方程式 2.25
A
[2] = λ[2]
[2]
方程式 2.26
对方程 2.25 进行转置,
[1]^T A^T = λ[1]
[1]^T
从右乘以
[2],我们得到
[1]^T A^T
[2] = λ[1]
[1]^T
[2]
⇔
[1]^T A
[2 ] = λ[1]
[1]^T
[2]
其中最后一个等式来源于矩阵的对称性。此外,将方程 2.26 左乘以
[1]^T,我们得到
[1]^T A
[2] = λ[2]
[1]^T
[2]
因此
[1]^T A
[2] = λ[1]
[1]^T
[2]
[1]^T A
[2] = λ[2]
[1]^T
[2]
相减得到
0 = (λ[1] - λ[2])
[1]^T
[2]
由于 λ[1] ≠ λ[2],我们必须有
[1]^T
[2] = 0,这意味着这两个特征向量是正交的。因此,如果 A 是一个 n × n 的对称矩阵,其特征向量为
[1],
[2],…
[n],那么
[i]^T
[j] = 0 对于所有满足 λ[i] ≠ λ[j] 的 i,j。
2.13.3 计算特征向量和特征值的 PyTorch 代码
注意:本节的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/1rEZ 找到。
列表 2.13 特征值和向量
from torch import linalg as LA
A = torch.tensor([[0.707, 0.707, 0],
[-0.707, 0.707, 0], [0, 0, 1]]) ①
l, e = LA.eig(A) ②
print("Eigen values are {}".format(l))
print("Eigen vectors are {}".format(e.T)) ③
①
在三维空间中绕 Z 轴旋转点。
旋转轴是 Z 轴:[0 0 1]^T
② torch.linalg 包中的 eig() 函数计算特征值和向量。
③ 特征值或向量可能包含涉及 j = √-1 的复数
2.14 正交(旋转)矩阵及其特征值和特征向量
在所有变换中,旋转变换由于其机械世界中的高度可观察行为而具有特殊的直观吸引力。此外,它们在开发和分析多个机器学习工具中发挥着重要作用。在本节中,我们概述了旋转(即正交)矩阵。(本节的 Jupyter Notebook 的完整功能代码可在 mng.bz/2eNN 找到。)
2.14.1 旋转矩阵
图 2.14 显示了一个点 (x, y) 围绕原点以角度 θ 旋转。原始点的位置向量与 X-轴形成角度 α。旋转后,点的新的坐标是 (x^′, y^′)。请注意,根据定义,旋转不会改变旋转中心的距离;这正是圆圈所表示的。

图 2.14 在原点周围平面的旋转。根据定义,旋转不会改变旋转中心(由圆圈指示)的距离。
一些著名的旋转矩阵如下:
- 围绕原点以角度 θ 进行平面旋转(见图 2.14):

方程 2.27
- 在 3D 空间中围绕 Z-轴以角度 θ 进行旋转:

方程 2.28
注意,z 坐标在此旋转中不受影响:

这个旋转矩阵有一个特征值为 1,对应的特征向量是 Z-轴——你应该验证这一点。这意味着当通过前面的矩阵(旋转)变换时,Z-轴上的点映射到自身,这与 z 坐标在此旋转中保持不变的属性相一致。
- 在 3D 空间中围绕 X-轴以角度 θ 进行旋转:

方程 2.29
注意,X 坐标在此旋转中不受影响,X-轴是该矩阵的特征向量:

- 在 3D 空间中围绕 Y-轴以角度 θ 进行旋转:

方程 2.30
注意,Y 坐标在此旋转中不受影响,Y-轴是该矩阵的特征向量:

列表 2.14 旋转矩阵
def rotation_matrix_2d(theta): ①
return torch.tensor([[cos(radians(theta)), -sin(radians(theta))],
[sin(radians(theta)), cos(radians(theta))]])
def rotation_matrix_3d(theta, axis): ②
if axis == 0: ③
return torch.tensor([[1, 0, 0],
[0, cos(radians(theta)),-sin(radians(theta))],
[0, sin(radians(theta)),cos(radians(theta))]])
elif axis == 1: ④
return torch.tensor([[cos(radians(theta)),0,-sin(radians(theta))],
[0, 1, 0],
[sin(radians(theta)),0,cos(radians(theta))]])
elif axis == 2: ⑤
return torch.tensor([[cos(radians(theta)),-sin(radians(theta)),0],
[sin(radians(theta)),cos(radians(theta)),0],
[0, 0, 1]])
① 返回在原点周围以角度 theta 进行的平面 2D 旋转的矩阵。因此,与该矩阵的乘法将点移动到新位置。原始点和新点位置向量之间的角度是 theta(见图 2.14)。
② 返回一个矩阵,该矩阵将点在 3D 空间中围绕所选轴旋转 theta 度。旋转轴可以是 0、1 或 2,分别对应于 X-、Y- 或 Z-轴。
③ R[3dx] 来自方程 2.29
④ R[3dy] 来自方程 2.30
⑤ R[3dz] 来自方程 2.28
列表 2.15 应用旋转矩阵
u = torch.tensor.array([1, 1, 1], dtype=torch.float) ①
R3dz = rotation_matrix_3d(45, 2) ②
v = torch.matmul(R3dz, u_row) ③
R3dx = rotation_matrix_3d(45, 0) ④
w = torch.matmul(R3dx, u_row) ⑤
① 创建向量
(见图 2.15)
② R[3dz] 来自方程 2.28,45°关于 Z-轴
③
(见图 2.15)是通过 R[3dz] 旋转的
。
④ R[3dx] 来自方程 2.28,45°关于 X-轴
⑤
(见图 2.15)是通过 R[3dx] 旋转
得到的。

图 2.15 旋转可视化。在这里,原始向量 u 首先绕 Z 轴旋转 45 度得到向量 v,然后再次绕 X 轴旋转 45 度得到向量 w。
2.14.2 旋转矩阵的正交性
一个矩阵 R 是 正交的 当且仅当它的转置也是它的逆:即,R^TR = RR^T = I。所有旋转矩阵都是正交矩阵。所有正交矩阵都表示某种旋转。 例如:

鼓励您同样验证,这里显示的所有旋转矩阵都是正交的。
正交性和长度保持
正交性意味着旋转保持长度。给定任意向量
和旋转矩阵 R,令
= R
为旋转后的向量。由于很容易看出这两个向量的长度(大小)相等,因此
,
相等。
||
|| =
^T
= (R
)^T(R
) =
TRTR
=
^TI
=
^T
= ||
||
从基本的矩阵理论中,我们知道
(AB)^T = BTAT
取消旋转角度
取消旋转角度相当于反转旋转矩阵,这相当于转置旋转矩阵。例如,考虑平面内的旋转。假设一个点
通过矩阵 R 绕原点旋转到向量
。因此

现在,我们可以通过旋转 −θ 从
回到
。相应的旋转矩阵是

换句话说,R^T 反转了旋转:即,以负角度旋转。
2.14.3 PyTorch 代码用于旋转矩阵的正交性
让我们通过在 PyTorch 中创建一个旋转矩阵,给它一个转置,并验证原始矩阵和转置的乘积是单位矩阵来验证旋转矩阵的正交性。
列表 2.16 旋转矩阵的正交性
R_30 = rotation_matrix_2d(30) ①
assert torch.allclose(
torch.linalg.inv(R_30),
R_30.T
) ②
assert torch.allclose(
torch.matmul(R_30, R_30.T),
torch.eye(2) ③
)
u = torch.tensor([[4],[0]], dtype=torch.float)
v = torch.matmul(R_30, u) ④
assert torch.linalg.norm(u) ==torch.linalg.norm(v) ⑤
R_neg30 = rotation_matrix_2d(-30)
w = torch.matmul(R_neg30, v) ⑥
assert torch.all(w == u)
assert torch.allclose(R_30, R_neg30.T)
assert torch.allclose(
torch.matmul(R_30, R_neg30),
torch.eye(2)) ⑦
① 创建一个旋转矩阵,R[30]
② 旋转矩阵的逆矩阵与它的转置相同。
③ 乘以旋转矩阵及其逆矩阵得到单位矩阵。
④ 一个向量
被矩阵R[30]旋转以得到向量
,R[30]
=
.
⑤ 向量的范数与它的长度相同。旋转保持向量的长度 ||R
|| = ||
||.
⑥ 先旋转一个角度,然后旋转该角度的负值,将向量带回到其原始位置。负角度的旋转等同于逆旋转。
⑦ 旋转一个角度的矩阵是旋转相同角度负值的矩阵的逆。
2.14.4 旋转矩阵的特征值和特征向量:寻找旋转轴
设λ,
为旋转矩阵R的一个特征值,特征向量对。那么R
= λ
对等式的两边进行转置,
TRT = λ
分别将等式的左右两边乘以等价的实体R
和λ
,我们得到
TRT(R
) = λ
(λ
) ⇔
T(RTR)
= λ²
^T
⇔
^T(I)!矩阵转置 = λ²
*^T
⇔
^T
= λ²
^T
⇔ λ² = 1 ⇔ λ = 1
(负解λ = −1 对应于反射。)因此,所有旋转矩阵都有一个特征值 1。相应的特征向量
满足R
=
。这是旋转轴:旋转后保持原位的点的集合。
2.14.5 旋转矩阵的特征值和向量的 PyTorch 代码
以下列表显示了旋转轴的代码。
列表 2.17 旋转轴
R = torch.tensor([[0.7071, 0.7071, 0],
[-0.7071, 0.7071, 0], ①
[0, 0, 1]])
l, e = LA.eig(R) ②
③
axis_of_rotation = e[:, torch.where(l == 1.0)] ④
axis_of_rotation = torch.squeeze(axis_of_rotation)
assert torch.allclose(
axis_of_rotation.real,
torch.tensor([0, 0, 1], dtype=torch.float) ⑤
)
p = torch.randint(0, 10, (1,)) * axis_of_rotation
assert torch.allclose(torch.matmul(R, p.real), p.real) ⑥
① 
关于Z轴旋转。所有旋转矩阵都将有一个特征值 1。相应的特征向量
是旋转轴(在这里,是Z轴)。
② PyTorch 函数 eig()计算特征值和特征向量。
③ PyTorch 函数 where()返回指定条件为真的索引。
④ 获取特征值 1 的特征向量
⑤ 旋转轴是Z轴。
⑥ 取一个随机点
在这个轴上并对此点应用旋转;其位置不改变。
2.15 矩阵对角化
在 2.12 节中,我们研究了线性系统及其在机器学习中的重要性。我们还指出,通过矩阵求逆解决线性系统的标准数学过程从机器学习的角度来看并不理想。在本节中,我们将看到一种不通过矩阵求逆解决线性系统的方法。此外,本节将帮助我们发展理解二次型以及最终主成分分析(PCA)所必需的洞察力,PCA 是数据科学中最重要工具之一。
考虑一个 n × n 矩阵 A,它具有 n 个线性无关的特征向量。设 S 是一个矩阵,其列由这些特征向量组成。也就是说,

和

然后

其中

是一个对角矩阵,其对角线上的元素是 A 的特征值,其余位置都是 0。
因此,我们有
AS = SΛ
这导致
A = SΛS^(−1)
和
Λ = S^(−1)AS
如果 A 是对称的,那么它的特征向量是正交的。那么 S^TS = SS^T = I ⇔ S^(−1) = S^T,我们得到 A 的对角化:A = SΛS^T 注意,对角化不是唯一的:一个给定的矩阵可能以多种方式对角化。
2.15.1 矩阵对角化的 PyTorch 代码
现在我们将研究 PyTorch 对 2.15 节中我们所学数学的实现。像往常一样,我们在这里只展示直接相关的代码部分。
注意:本节完全功能的代码,可通过 Jupyter Notebook 执行,可在mng.bz/RXJn找到。
列表 2.18 矩阵的对角化
def diagonalize(matrix): ①
try:
l, e = torch.linalg.eig(matrix) ②
sigma = torch.diag(l) ③
return e, torch.diag(l), torch.linalg.inv(e) ④
except np.linalg.LinAlgError:
print("Cannot diagonalize matrix!")
A = torch.tensor([[0.7071, 0.7071, 0],
[-0.7071, 0.7071, 0], ⑤
[0, 0, 1]])
S, sigma, S_inv = diagonalize(A)
A1 = torch.matmul(S, torch.matmul(sigma, S_inv)) ⑥
assert torch.allclose(A, A1.real) ⑦
① 对角化是将矩阵 A = SΣS^(−1) 分解。S 是一个矩阵,其列由 A 的特征向量组成。Σ 是一个对角矩阵,其对角线上的元素是 A 的特征值。
② PyTorch 函数 eig() 返回特征值和向量。
③ PyTorch 函数 diag() 创建一个给定值的对角矩阵。
④ 返回三个因子
⑤ 创建矩阵 A
⑥ 从其因子重建 A
⑦ 验证重建的矩阵与原始矩阵相同
2.15.2 通过对角化解决线性系统而不进行求逆
对角化有许多实际应用。现在让我们研究一个。一般来说,矩阵求逆(即计算 A^(−1))是一个非常复杂的过程,数值上不稳定。因此,当可能时,应避免通过
= A^(−1)
解决 A
=
。在具有 n 个不同特征值的平方对称矩阵的情况下,对角化可以提供帮助。我们可以分多步解决这个问题。我们首先对 A 进行对角化:
A = SΛS^T
然后
A
= 
可以表示为:
SΛS^T
= 
其中 S 是以 A 的特征向量为列的矩阵:
S = ![[1]
[2] …
[n]]
(由于 A 是对称的,这些特征向量是正交的。因此 S^TS = SS^T = I。)解可以通过一系列非常简单的步骤获得:

首先解决
S
[1] = 
as
[1] = S^T 
注意,转置和矩阵-向量乘法都是简单且数值稳定的操作,与矩阵求逆不同。然后我们得到
Λ(S^T
) =
[1]
现在解决
Λ
[2] =
[1]
as
[2] = Λ^(–1)
[1]
注意,由于 Λ 是对角矩阵,求逆是平凡的:

方程 2.31
作为最后一步,解决
S^T
=
[2]
as
= S
[2]
因此,我们没有经过任何复杂的或不稳定的步骤就获得了
。
2.15.3 使用对角化解决线性系统的 PyTorch 代码
让我们尝试解决以下方程组:
x + y + z = 8
2x + 2y + 3z = 15
x + 3y + 3z = 16
这可以用矩阵和向量表示为
A
= 
其中

注意,A 是对称矩阵。它有正交的特征向量。以 A 的特征向量为列的矩阵是正交的。它的转置和逆是相同的。
列表 2.19 使用对角化解决线性系统
A = torch.tensor([[1, 2, 1], [2, 2, 3], [1, 3, 3]],
dtype=torch.float) ①
assert torch.all(A == A.T) ②
b = torch.tensor([8, 15, 16], dtype=torch.cfloat) ③
x_0 = torch.matmul(torch.linalg.inv(A),
b.real) ④
w, S = torch.linalg.eig(A) ⑤
1 = torch.matmul(S.T, b) ⑥
2 = torch.matmul(torch.diag(1/ w), y1) ⑦
x_1 = torch.matmul(S, y2) ⑧
assert torch.allclose(x_0, x_1.real) ⑨
① 创建一个对称矩阵 A
② 声明 A 可能是对称的
③ 创建一个向量 
④ 使用矩阵求逆解决 A
=
,
= A^(−1)
。
注意:矩阵求逆是数值不稳定的。
⑤ 通过对角化解决 A
=
。A = SΣS^T。
.
⑥ 1. 解决:S
[1] =
作为
[1] = S^T
(无需矩阵求逆)
⑦ 2. 解决:Λ
[2] =
[1] 作为
[2] = Λ^(-1)
[1](对角矩阵求逆很容易;见方程 2.31。)
⑧ 3. 解:S^T
=
[2] 作为
= S
[2](无需矩阵求逆)
⑨ 验证两个解相同
⑨ 验证两个解相同
2.15.4 使用对角化计算矩阵幂
如果矩阵 A 可以对角化,那么

对于对角矩阵

第 n 次幂简单地是

如果我们需要在不同时间计算矩阵 A 的各种幂,我们应该预先计算矩阵 S,然后只需进行 O(m) 次操作即可计算任何幂——与原始计算所需的 (nm³) 次操作相比。
2.16 对称矩阵的谱分解
我们在 2.15 节中看到,具有不同特征值的平方对称矩阵可以分解为
A = SΛS^T
其中
A = ![图片[1]
[2] …
[n]]
因此,

这个方程可以重写为
A = λ[1]
[1]
[1]^T + λ[2]
[2]
[2]^T + … + λ[n]
[n]
[n]^T
方程 2.32
因此,一个平方对称矩阵可以用其特征值和特征向量来表示。这是谱分解定理。
2.16.1 矩阵谱分解的 PyTorch 代码
以下列表显示了本节的相关代码。
列表 2.20 矩阵的谱分解
def spectral_decomposition(A):
assert len(A.shape) == 2 ①
and A.shape[0] == A.shape[1] ②
and torch.all(A == A.T) ③
l, e = torch.linalg.eig(A) ④
assert len(torch.unique(l.real)) == A.shape[0],
``Eigen values are not distinct!"
C = torch.zeros((A.shape[0],
A.shape[0], ⑤
A.shape[0]))
for i, lambda_i in enumerate(l):
e_i = e[:, i]
e_i = e_i.reshape((3, 1))
C[i, :, :] = (lambda_i * torch.matmul(e_i, e_i.T)).real ⑥
return C
A = torch.tensor([[1, 2, 1], [2, 2, 3], [1, 3, 3]]).float()
C = spectral_decomposition(A)
A1 = C.sum(axis=0) ⑦
assert torch.allclose(A, A1) ⑧
① 断言 A 是一个二维张量(即,矩阵)
② A 是平方的:即,A.shape[0](行数)≜ A.shape[1](列数)
③ 断言 A 是对称的:即,A = = A^T
④ PyTorch 函数 eig() 返回特征向量和值。
⑤ 定义一个形状为 n × n × n 的三维张量 C 来存储方程 2.32 中的 n 个分量。每个项 λ[i]
[i]
**^T 是一个 n × n 矩阵。这里有 n 个这样的项,所有这些项都紧凑地存储在张量 C 中。
⑥ 计算 C[i] = λ[i]
[i]
**^T
⑦ 通过添加存储在 C 中的分量来重建 A
⑧ 验证从谱分量重建的矩阵与原始矩阵匹配
2.17 与机器学习相关的一个应用:寻找超椭圆的轴
在机器学习中,高维空间中椭圆的概念(也称为超椭圆)以各种形式不断出现。在这里,我们将对这些概念进行初步回顾。我们稍后还会重新审视这些概念。
回想一下高中数学中椭圆的方程:

这是一个相当简单的椭圆:它是二维的,以原点为中心,其主轴和副轴与坐标轴对齐。将
表示为位置向量,相同的方程可以写成
^TΛ
= 1
其中
是一个对角矩阵。写成这种形式,方程可以扩展到二维以上的 n-维轴对齐椭圆,以原点为中心。现在让我们对轴应用一个旋转 R。然后每个向量
都会变换为 R
。在新(旋转)坐标系中椭圆的方程是
(R
^T Λ (R
) = 1
⇔
^T (R^T ΛR)
= 1
其中
A = (R^TΛR).
椭圆的广义方程是
^TA
= 1
注意以下内容:
-
椭圆不再是轴对齐的。
-
矩阵 A 不再是对角的。
-
A 是对称的。我们可以很容易地验证 A^T = (RT*Λ*R*)*T = RT*Λ*TR = R^TΛR(记住,对角矩阵的转置是其本身)。
如果我们还想摆脱“以原点为中心”的假设,我们得到
(
−μ)^TA(
−μ) = 1
方程 2.33
现在,让我们反过来思考这个问题。假设我们有一个通用的 n-维椭圆。我们如何计算其轴的方向?
显然,如果我们能旋转坐标系,使得中间的矩阵是对角的,我们就完成了。对角化(见第 2.15 节)是答案。具体来说,我们找到具有 A 的特征向量的矩阵 S 作为其列。这是一个旋转矩阵(因为 A 是对称的)。我们通过应用这个矩阵来旋转坐标系。在这个新的坐标系中,椭圆是轴对齐的。换句话说,新的坐标轴——这些是 A 的特征向量——给出了椭圆的轴。
2.17.1 用于超椭圆的 PyTorch 代码
让我们尝试找到由方程 5x² + 6xy + 5y² = 20 描述的超椭圆的轴。请注意,我们用作示例的实际椭圆是二维的(以便于可视化),但我们开发的代码将是通用的,并可以扩展到多个维度。
椭圆方程可以用矩阵和向量表示为
^TA
= 1,其中

要找到超椭圆的轴,我们需要变换坐标系,使得中间的矩阵变为对角矩阵。以下是实现方法:如果我们把 A 对角化成 SΣS^(−1),那么椭圆方程变为
TS*Σ*S*(−1)
= 1,其中 Σ 是对角矩阵。由于 A 是对称的,其特征向量是正交的。因此,包含这些特征向量作为列的矩阵是正交的:即,S^(−1) = S^T.换句话说,S* 是一个旋转矩阵。因此,椭圆方程变为
TS*Σ*ST
= 1 或 (
TS*)Σ(*ST
) = 1 或
^TΣ
= 1,其中
= S^T
。由于 Σ 是对角矩阵,这是期望的形式。记住,S 是一个旋转矩阵。因此,通过 S 旋转坐标系,坐标轴与椭圆轴对齐。
列表 2.21 超椭圆的轴
ellipse_eq = sy.Eq(5*x**2 + 5*y**2 +
6*x*y, 20) ①
A = torch.tensor([[5, 3], [3, 5]]).float()
l, S = torch.linalg.eig(A)
x_axis_vec = torch.zeros((A.shape[0])) ②
first_eigen_vec = S[:, 0] ③
dot_prod = torch.dot(x_axis_vec, first_eigen_vec) ④
theta = math.acos(dot_prod)
theta = math.degrees(theta) ⑤
① 椭圆的方程:5x² + 6x**y + 5y² = 20 或
^TA
= 20,其中 
② X 轴向量
③ 椭圆的主轴
④ 两个向量之间的点积是它们之间角度的余弦值。
⑤ 椭圆主轴与 X 轴之间的角度:45°(见图 2.16)

图 2.16 注意到椭圆的主轴与 X 轴形成 45 度的角度。通过这个角度旋转坐标系,将椭圆轴与坐标轴对齐。随后,第一个主向量也将沿着这个方向。
概述
-
在机器学习中,向量是一维数字数组,矩阵是二维数字数组。机器学习模型的输入和输出通常表示为向量或矩阵。在多层模型中,每个单独层的输入和输出也以向量或矩阵的形式表示。图像是表示像素颜色值的二维数字数组。因此,它们以矩阵的形式表示。
-
一个 n-维向量可以看作是 ℝ^n 空间中的一个点。所有模型都可以看作是从输入空间映射到输出空间的函数。模型设计得使得在输出空间中解决感兴趣的问题更容易。
-
两个向量
= [x[1] x[2] … x[n]] 和
= [y[1] y[2] … y[n]] 的点积是一个标量量
⋅
= x[1]y[1] + x[2]y[2] + ⋯ + x[n]y[n]。它是衡量向量相似程度的一个指标。点积在机器学习中得到了广泛的应用。例如,在监督机器学习中,我们训练模型,使其输出尽可能接近已知输出,这些已知输出是针对一组已知输入点(称为训练数据)的。在这里,点积的某种变体通常用于衡量模型输出和已知输出的相似度。如果两个向量的点积为零,则这两个向量是正交的。这意味着向量之间没有相似性,彼此独立。一个向量与其自身的点积是向量的模长或长度的平方
⋅
= ||
||² = x[1]x[1] + x[2]x[2] + ⋯ + x[n]x[n]。 -
给定一组向量
[1],
[2], ⋯,
[n],其加权求和 a[1]
[1] + a[2]
[2] + ⋯ + a[n]
[n](其中 a[1], a[2], ⋯, a[n] 是任意标量)被称为线性组合。特别是,如果系数 a[1], a[2], ⋯, a[n] 非负且它们的和为 1,则这种线性组合被称为 凸 组合。如果可以找到一组系数 a[1], a[2], ⋯, a[n](不全为零),使得线性组合是一个零向量(意味着所有元素都是零),则称向量
[1],
[2], ⋯,
[n] 是 线性相关 的。另一方面,如果唯一获得零向量线性组合的方法是使所有系数为零,则称这些向量是 线性无关 的。 -
矩阵和向量的一个重要应用是解线性方程组。这样的系统可以用矩阵向量形式表示为 A
=
,其中我们求解满足该方程的未知向量
。当且仅当 A 可逆时,该系统有精确解。这意味着 A 是一个方阵(行数等于列数)且行向量线性无关。如果行向量线性无关,则列向量也线性无关,反之亦然。如果行和列线性无关,则 A 的行列式保证不为零。因此,行/列的线性无关性和非零行列式是等价条件。如果满足其中任何一个条件,线性系统就有精确且唯一的解。在实践中,这种要求往往不满足,我们得到的是一个超定或欠定系统。在这种情况下,Moore-Penrose 逆导出一种最佳逼近形式。从几何上看,Moore-Penrose 方法得到的是在由 A 的列向量张成的向量空间中,最接近
的点。等价地,Moore-Penrose 解
[*] 得到的是在由 A 的列向量张成的向量空间中,最接近
的点。 -
对于一个方阵 A,如果且仅如果 Aê = λê,我们称 λ 为 A 的特征值(一个标量),ê 为 A 的特征向量(一个单位向量)。在物理上,特征向量 ê 是一个单位向量,其方向在矩阵 A 变换时不会改变。变换可以通过标量尺度因子 λ(即特征值)放大其长度。
一个 n × n 的矩阵 A 有 n 个特征值/特征向量对。特征值不一定都是唯一的。对应于不同特征值的特征向量是线性无关的。如果矩阵 A 是对称的,满足 A^T = A,则对应于不同特征值的特征向量是正交的。旋转矩阵是一个行向量彼此正交且列向量也彼此正交的矩阵。这样的矩阵也被称为正交矩阵。一个正交矩阵 R 满足方程 R^TR = I,其中 I 是单位矩阵。在矩阵 A 是旋转矩阵 R 的特殊情况下,一个特征值总是 1。对应的特征向量是旋转轴。一个具有 n 个线性无关特征向量的矩阵 A 可以分解为 A = SΛS^(−1),其中 S = ![[1]
[2] …
[n]] 是以 A 的特征向量作为其列的矩阵,Λ 是对角矩阵,其对角线上的元素是 A 的特征值。这种分解称为矩阵对角化,并导致求解线性系统的一种数值稳定方法。 -
一个平方对称矩阵 A 可以用其特征向量和特征值来表示,即 A = λ[1]
[1]
[1]^T + λ[2]
[2]
[2]^T + ... + λ[n]
[n]
[n]^T。这被称为矩阵 A 的谱分解。
¹ 在数学中,向量可以有无穷多个元素。这样的向量不能表示为数组——但在这本书中我们主要会忽略它们。↩
² 我们通常使用大写字母来表示矩阵。↩
³ 在数字计算机中,范围在 0..255 之间的数字可以用一个字节的存储来表示;因此选择了这种方案。↩
⁴ 数学符号∀代表“对所有”。因此,∀
∈ ℜ^n 表示“n-维空间中的所有向量 y”。↩
⁵ 你只能计算方阵的特征向量和特征值。↩
第三章:分类器和向量微积分
我们在 1.3 节中首次了解了机器学习的核心概念。然后,在 2.8.2 节中,我们考察了分类器作为一个特殊情况。但到目前为止,我们还没有涉及误差最小化的主题:给定一个或多个训练示例,我们如何调整权重和偏差,使机器更接近理想的期望?我们将通过讨论梯度的概念来研究这个主题。
注意:本章的完整 PyTorch 代码以完全功能性和可执行的 Jupyter 笔记本的形式,可在mng.bz /4Zya找到。
3.1 图像分类的几何视图
为了固定我们的想法,考虑一个机器,它能够分类图像是否包含汽车或长颈鹿。这样的分类器,只有两个类别,被称为二元分类器。第一个问题是如何表示输入。
3.1.1 输入表示
汽车与长颈鹿的场景属于我们分析视觉场景的特殊问题类别。在这里,输入是 3D 场景中各个点的亮度级别,这些点被投影到二维图像平面上。图像的每个元素代表实际场景中的一个点,被称为像素。图像是表示在给定时间点像素值的二维数组。它通常被缩放到固定大小,例如 224 × 224。因此,图像可以被视为一个矩阵:

矩阵的每个元素,X[i, j],是像素颜色值,范围在[0,255]之间。
图像光栅化
在前面的章节中,我们总是将向量视为机器学习系统的输入。输入的向量表示使我们能够将其视为高维空间中的一个点。这导致了关于分类的许多几何洞察。但在这里,我们的输入是一个图像,它更像是矩阵而不是向量。我们能否将图像(矩阵)表示为向量?
答案是肯定的。矩阵总可以通过称为光栅化的过程转换为向量。在光栅化过程中,我们从左到右和从上到下迭代矩阵的元素,将连续遇到的元素存储到一个向量中。结果是光栅化的向量。它具有与原始矩阵相同的元素,但它们的组织方式不同。光栅化向量的长度等于矩阵的行数和列数的乘积。早期矩阵X的光栅化向量有 224 × 224 = 50176 个元素

其中 x[i] ∈ [0,255] 是图像像素的值。因此,一个 224 × 224 的输入图像可以被视为一个 50,176 维空间中的向量(等价地,一个点)。
3.1.2 分类器作为决策边界
我们可以看到,输入图像可以通过光栅化转换为向量。每个向量可以看作是高维空间中的一个点。但是,对应于任何给定对象或类别的点,比如 长颈鹿 或 汽车,并不是在整个空间中随机分布的。相反,它们占据输入高维空间中一个小的子空间。这是因为类别的成员之间总是存在固有的共性。例如,所有的长颈鹿主要是黄色,带有一点黑色,而汽车有相对固定的形状。这导致包含相同对象的图像中的像素值有某种相似性。总的来说,这意味着属于一个类别的点松散地形成一个 簇。
备注:从几何学的角度来看,分类器是一个超曲面,它将我们想要识别的类别的点簇分开。这个表面形成了一个 决策边界——关于特定输入点属于哪个类别的决策是通过查看点属于表面的哪一侧来做出的。

(a) 汽车与长颈鹿分类器

(b) 马与斑马分类器
图 3.1 分类问题的几何描述。在多维输入空间中,每个数据实例对应一个点。在图 3.1a 中,标记为 c 的点表示汽车,标记为 g 的点表示长颈鹿。这是一个简单的情况:点形成合理的不同簇,因此可以使用相对简单的表面,即超平面进行分类。超平面的精确参数——方向和位置——是通过训练确定的。在图 3.1b 中,标记为 h 的点表示马,标记为 z 的点表示斑马。这个情况稍微复杂一些:分类需要使用曲线(非平面)的表面,即超球面。超球面的参数——半径和中心——是通过训练确定的。
图 3.1a 展示了长颈鹿和汽车分类问题的光栅化空间示例。对应于长颈鹿的点被标记为 g,对应于汽车的点被标记为 c。这是一个简单的情况。在这里,将对应于 汽车 的点簇与对应于 长颈鹿 的点簇分开的分类表面(也称为决策边界)是一个超平面,如图 3.1a 所示。
备注:在超过三维的情况下,我们通常将表面称为 超曲面,将平面称为 超平面。
图 3.1b 展示了一个更复杂的例子:图像中的马和斑马分类。在这里,对应于马的点被标记为 h,而对应于斑马的点被标记为 z。在这个例子中,我们需要一个非线性(曲线)的表面(例如超球面)来分离这两个类别。
3.1.3 简要建模
很不幸,在典型场景中,我们并不知道分离表面。实际上,我们甚至不知道属于一个感兴趣类别的所有点。我们所知道的就是一组采样的输入集
[i](训练输入)以及相应的类别
[i](真实标签)。包含所有训练输入和真实标签的完整集合——{
[i],
[i]} 对于一个大的i值集合——被称为训练数据。当我们想要教一个婴儿识别汽车时,我们会给婴儿展示几辆汽车样品,并说“这是一辆汽车。”对于神经网络来说,训练数据扮演着同样的角色。
从仅有的这个训练数据集{
[i],
[i]} ∀[i] ∈ [1, n],我们必须找到一个足够好的分类表面的近似,当呈现一个随机场景时,我们可以将其映射到一个输入点
,检查该点位于表面的哪一侧,并识别类别(汽车或长颈鹿)。这个过程是开发一个最佳猜测,以形成一个决策边界,该边界区分各种感兴趣类别,被称为建模分类器。
注意:训练图像
[i] 的真实标签 (
[i]) 通常是通过人工创建的。为训练图像手动生成标签的过程是机器学习中最痛苦的部分之一,目前正在进行大量研究工作以减轻这一问题。
如 1.3 节所述,建模有两个步骤:
-
模型架构选择:选择参数化模型函数 ϕ(
;
, b)。这个函数接受一个输入向量
并输出类别 y。它有一组参数
, b,最初是未知的。这个函数通常从一系列经过验证和测试的已知函数中选择;对于简单问题,我们可能选择线性模型,而对于更复杂的问题,我们选择非线性模型。模型设计者根据他们对问题的理解做出选择。记住,在这个阶段,参数仍然是未知的——我们只是决定了模型的函数族。 -
模型训练:估计参数
, b,使得 ϕ 在训练数据输入上产生已知的正确输出(尽可能接近)。这通常通过迭代过程来完成。对于每个训练数据实例
[i],我们评估 y**[i] = ϕ(
[i ];
, b). 这个产生的输出与相应的已知输出 ȳ[i] 进行比较。它们的差值,e[i] = ||y**[i] − ȳ[i]||,被称为 训练误差。所有训练数据上的训练误差之和是总训练误差。我们迭代调整参数
, b,使得总训练误差持续下降。这意味着在每次迭代中,我们调整参数,使得模型输出 y**[i] 对于所有 i 都稍微接近目标输出 ȳ[i]。如何调整参数以减少误差构成了本章的主要内容,将在第 3.3 节中介绍。
函数 ϕ(
;
, b) 代表决策边界超曲面。例如,在图 3.1 所示的二值分类问题中,ϕ(
;
, b) 可能代表一个平面(由虚线表示)。平面一侧的点被分类为汽车,而另一侧的点被分类为长颈鹿。在这里,
ϕ(
;
, b) =
^T
+ b
从方程 2.14 我们知道这个方程代表一个平面。
在图 3.1b 中,不存在良好的平面分离——我们需要一个非线性分离器,如用虚线表示的球形分离器。在这里,

这个方程代表一个球面。
应该注意的是,在典型的现实生活案例中,分离表面不对应于任何已知的几何表面(参见图 3.2)。但在本章中,我们将继续使用简单的例子来阐述基本概念。

图 3.2 在现实生活中的问题中,表面通常不是像平面或球面这样的已知表面。而且,分类通常并不完美——一些点落在分离器的错误一侧。
3.1.4 二元分类中超曲面函数的符号
在二元分类器的特殊情况中,表示决策边界的表达式 ϕ(
;
, b) 的 符号 具有特殊的意义。为了理解这一点,考虑一个对应于以下方程的二维平面上的直线
y + 2x + 1 = 0
线上的所有点都满足这个方程的 x, y 坐标值。这条线将二维平面分为两个半平面。一个半平面上的所有点的 x, y 值使得 y + 2x + 1 为负。另一个半平面上的所有点的 x, y 值使得 y + 2x + 1 为正。这如图 3.3 所示。这个想法可以扩展到其他表面和更高维度。因此,在二元分类中,一旦我们估计了一个最优决策表面 ϕ(
;
, b),对于任何输入向量
,我们可以计算 ϕ(
;
, b) 的符号来预测类别。

图 3.3 给定点 (x[0], y[0]) 和分隔线 y + 2x + 1 = 0,我们可以根据 y[0] + 2x[0] + 1 的符号来判断点位于分隔线的哪一侧。
3.2 错误,即损失函数
如前所述,在训练过程中,我们调整参数
, b,以便误差持续下降。让我们推导出这个误差,即损失函数的定量表达式。稍后,我们将看到如何最小化它。
总体而言,训练数据由一组标记的输入(训练数据实例与已知的真实值配对)组成:

现在我们定义一个损失函数。在特定的训练数据实例上,损失函数实际上衡量了机器在该特定训练数据——输入-目标对 (
^((i)), y^((i))) 上犯的错误。尽管有许多更适合此问题的复杂错误函数,但为了简单起见,我们现在使用平方误差函数(在 2.5.4 节中介绍过)。第 i 个训练数据元素的平方误差是模型输出的平方与期望或目标输出的平方差:

方程 3.1
训练过程中的总损失(即平方误差)为

方程 3.2
注意,这个总误差不是任何特定训练数据实例的函数。相反,它是整个训练数据集的总体误差。这是我们通过调整
和 b 来最小化的。更准确地说,我们估计
和 b,以最小化 L(
, b)。
3.3 最小化损失函数:梯度向量
训练的目标是估计权重和偏差参数
, b,这些参数将最小化 L。这通常通过迭代过程来完成。我们以
, b 的随机值开始,并调整这些值,使得损失 L(
, b) = E²(
, b) 迅速下降。这样做多次可能使我们接近
, b 的最优值。这是训练模型过程背后的基本思想。重要的是要注意,我们是在最小化总误差:这防止我们对任何特定的训练实例过度依赖。如果训练数据是一个良好采样的集合,那么在训练数据集上最小化损失的参数
, b 也会在推理过程中表现良好。
我们如何“调整”
, b 以使损失 L = E² 的值下降?这正是梯度发挥作用的地方。对于任何函数 L(
, b),相对于
, b 的梯度——即 ∇![, b]L(
, b)——指示了 L 发生最大变化的方向。梯度是一维微积分中导数的类似物。直观上,沿着函数梯度的方向下降似乎是使函数值最小化的最佳策略。
从几何角度来说,如果我们从对应于 L(
, b) 的任意表面点开始,沿着梯度 ∇![, b]L(
, b) 的方向移动,我们将以最快的速度走向最小值(这一点将在本节的其余部分详细讨论)。因此,在训练过程中,我们通过沿着 ∇![, b]L(
, b) 移动步子来迭代地走向最小值。请注意,梯度是相对于权重,而不是输入的。整体算法在算法 3.2 中展示。
算法 3.2 训练监督模型(总体思路)
使用随机值初始化
, b。
while L(
, b) > threshold do

在新的
, b 上重新计算 L。
end while
[] ←
, b[] ← b
注意以下要点:
-
在每次迭代中,我们沿着误差函数的梯度调整
, b。我们将在第 3.3 节中看到,这是 L 的最大变化方向。因此,L 以最大速率减少。 -
μ 是学习率:较大的值意味着更长的步长,而较小的值意味着更短的步长。在算法 3.2 中概述的最简单方法,在所有地方都采取等大小的步长。在后面的章节中,我们将研究更复杂的方法,其中我们试图感知我们距离最小值有多近,并相应地调整步长:
-
当远离最小值时,我们采取更长的步长,以快速进步。
-
当接近最小值时,我们采取更短的步长,以避免超过最小值。
-
-
从数学上讲,我们应该持续迭代,直到损失最小化(即损失的梯度为零)。但在实践中,我们只需迭代到准确度足够好,以满足当前目的。
3.3.1 梯度:以机器学习为中心的介绍
在机器学习中,我们将输出建模为输入的参数函数。我们定义一个损失函数,该函数量化了模型输出与训练输入集上已知理想输出之间的差异。然后我们尝试获得最小化这个损失的参数值。这实际上确定了那些将在训练输入集上使模型函数输出尽可能接近理想的参数。
损失函数取决于
(模型输入),ȳ(训练数据上的已知理想输出,即真实值),以及
(参数)。在这里,我们只对损失函数相对于参数的行为感兴趣,所以我们忽略了其他所有内容,并将损失函数表示为参数的函数,即 L(
)。
注意:为了简洁起见,在这里我们使用符号 w 来表示所有参数——权重以及偏差。
我们试图回答的核心问题是这样的:给定一个损失 L(
) 和当前的参数值
,参数
的最佳变化是什么,以最大限度地减少损失?等价地,我们希望确定
,使得 δL = L(
+
) - L(
) 尽可能地负。为了达到这个目标,我们将研究损失函数 L(w) 和参数值变化
在越来越复杂的几种场景中的关系。¹

(a) 直线:L(w) = 2w + 1, dL/dw = m

(b) 抛物线:L(w) = w², dL/dw = 2w
图 3.4 一维中 δL 与 δw 的关系,用两条示例曲线表示:一条直线和一个抛物线。一般来说,δL = (dL/dw) δw。为了减少损失,δw 必须与导数 dL/dw 具有相反的符号。在 (a) 中,这意味着我们总是必须向左移动(减少 w)以减少 L。在 (b) 中,如果我们处于左侧(例如,点 Q),导数是负的,我们必须向右移动以减少 L。但如果我们处于右侧,导数是正的,我们必须向左移动以减少 L。从几何上看,这相当于沿着切线“向下”移动。
一维损失函数
为了简化,我们首先在一维中考察这个主题——这意味着有一个单个参数 w。我们将研究的第一个例子是最简单的情况:一个一维的线性损失函数,如图 3.5 所示。一维的线性损失函数可以表示为 L(w) = mw + c。如果我们通过一个小的量 δw 改变参数 w,损失 δL 的相应变化是多少?我们有 δL = L(w + δw) − L(w) = (m(w + δw)+c) − (m(w)+c) = m δw,这给出了 δL/δw = m,一个常数。根据定义,导数 dL/dw = lim[δw→0] δL/δw,这导致 dL/dw = m。因此,对于直线 L(w) = mw + c,L 相对于 w 的变化率在每处都是常数,并且等于斜率 m。将这些放在一起,我们得到 δL = m δw = dL/dw δw。
现在我们来研究一个稍微复杂一些、非线性但仍为一维的情况——一个抛物线损失函数,如图 3.4 所示。这个抛物线可以表示为 L(w) = w²。如果我们通过一个小的量 δw 改变参数 w,损失 δL 的相应变化是多少?我们有 δL = L(w + δw) − L(w) = (w + δw)² − w² = (2wδw + δw²)。对于无限小的 δw,δw² 变得可以忽略不计,我们得到 lim[δw→0] δL = lim[δw→0] (2wδw + δw²) = 2wδw 和 dL/dw = lim[δw→0] δL/δw = 2w。结合所有这些,我们得到与线性情况相同的方程 δL = dL/dw δw。当然,在直线的情况下,这个表达式对所有 δw 都成立,而在非线性曲线上,这个表达式只在小的 δw 下成立。
δL 和 δw
通常,对于所有一维损失函数 L(w),参数变化 δw 引起的改变 δL 可以表示如下:

方程 3.3
为了减少 L,δL 必须是负的。从方程 3.3 中,我们可以看出这需要 δw(w 的变化)和 dL/dw(导数)具有相反的符号。
从几何角度来说,损失函数表示一条曲线,其中损失 L(w) 沿着 Y 轴绘制,与参数 w 沿着 X 轴绘制(见图 3.4 中的示例)。任何点的切线可以看作是该点无限小邻域内曲线本身的局部近似。任何点的导数代表该点处曲线切线的斜率。
注意:方程 3.3 基本上告诉我们,为了减少损失值,我们必须沿着切线移动,如果导数是负的,则向右(即正 δw)移动;如果导数是正的,则向左(即负 δw)移动。
多维损失函数
如果有多个可调参数,我们的损失函数将是多个变量的函数,这意味着我们有一个高维向量
和一个损失函数 L(
)。我们的目标是计算由小向量位移
引起的 L(
) 的变化 δL。
我们立即注意到与一维情况的一个基本区别:参数变化是一个向量,
,它不仅有一个表示 ||
|| 的大小,还有一个表示单位向量
的方向。我们可以在 w 空间中迈出相同大小的步伐,而 L(
) 的变化将因方向不同而不同。这种情况在图 3.5 中得到说明,该图展示了示例损失函数 L(
) ≡ L(w[0], w[1]) = 2w[0]² + 3w[1]²,其中 w[0] 和 w[1] 是两个独立变量。让我们通过几个具体的例子来考察这个损失函数是如何变化的。

图 3.5 展示了表面 L(
) ≡ L(w[0], w[1]) = 2w[0]² + 3w[1]² 对
≡ (w[0], w[1]) 的绘图。从表面上的一个示例点 P ≡ (w[0]=3, w[1]=4, L = 66) 出发,我们可以向许多方向移动以减少 L。其中一些方向用箭头表示。最大减少发生在我们沿着深色箭头移动时:这发生在
沿着
= [-12, -24]^T 变化时,这是 P 点处 L(
) 的负梯度。
假设我们处于
。L(
) 的对应值是 2 ∗ 3² + 3 ∗ 4² = 66。现在,假设我们从这一点进行一个小位移:
。新的值是 L(
+
) = L(3.0003, 4.0004) = 2 ∗ 3.0003² + 3 ∗ 4.0004² ≈ 66.0132066。因此,这个位移向量
导致 L 的变化 δL = 66.01320066 – 66 = 0.01320066。
另一方面,如果位移是
,我们得到 L(
+
) = L(3.0004, 4.0003) = 2 ∗ 3.0004² + 3 ∗ 4.0003² ≈ 66.0120006。因此,这个位移导致 L 的变化 δL = 66.0120006 − 66 = 0.0120006。位移向量
和
有相同的长度
但不同的方向。它们对函数值造成的变化是不同的。这证明了我们的论点:在多变量损失函数中,损失函数的变化不仅取决于位移在参数空间中的大小,还取决于位移的方向。
参数空间中位移向量
与损失函数 L(
) 的整体变化之间有什么一般关系?为了考察这个问题,我们需要知道什么是偏导数。
偏导数
函数 L(w) 的导数 dL/dw 表示函数相对于 w 的变化率。但如果 L 是多个变量的函数,当只有一个变量变化时,它会如何变化?这个问题引出了偏导数的概念。
多变量函数的偏导数是相对于恰好一个变量的导数,将其他变量视为常数。例如,给定 L(
) ≡ L(w[0], w[1]) = 2w[0]² + 3w[1]²,相对于 w[0] 和 w[1] 的偏导数是

多维函数的总变化
偏导数估计在只有一个变量变化而其他变量保持不变的情况下函数的变化。如果我们想估计所有变量同时变化时函数值的改变,我们应该如何估计?
总变化可以通过对偏导数的加权组合来估计。令
和
分别表示点和位移向量:

然后

公式 3.4
方程 3.4 实际上说明,L 的总变化是通过将各个变量位移引起的变化相加得到的。L 相对于 w[i] 变化的变化率是 ∂L/∂w[i]。变量 w[i] 的位移是 δw[i]。因此,位移的第 i 个元素引起的变化是 ∂L/∂w[i] δw[i]——这可以从方程 3.3 得出。总变化是通过将位移向量的各个元素引起的变化相加得到的:即对所有 i 从 0 到 n 进行求和。这导致了方程 3.4。因此,方程 3.4 只是方程 3.3 的多维版本。
梯度
能够紧凑地表示方程 3.4 会很好。为了做到这一点,我们定义了一个称为 梯度 的量:所有偏导数的向量。
给定一个 n 维函数 L(
),其梯度定义为

公式 3.5
使用梯度,我们可以将方程 3.4 重新写为

公式 3.6
方程 3.6 告诉我们,由参数空间中从
到
的位移
引起的 L(
) 的总变化 δL,是梯度向量 ∇L(
) 和位移向量
的点积。这是方程 3.3 的精确多维类似物。
回想一下,从第 2.5.6.2 节中,两个向量(固定大小)的点积在向量方向对齐时达到最大值。这为梯度向量提供了一个物理解释:其方向是参数空间中多维函数变化最快的方向。它是导数的多维对应物。这就是为什么在机器学习中,当我们想要最小化损失函数时,我们会沿着损失函数梯度向量的方向改变参数值。
梯度在最小值处为零
函数的任何 最优值(即最大值或最小值)都是拐点。这意味着函数在最优值点转折。换句话说,最优值一侧的梯度方向与另一侧相反。如果我们试图平滑地从正值过渡到负值,我们必须在中间某处穿过零。因此,梯度在精确的拐点最大值或最小值处为零)。这在二维中最容易看出,如图 3.6 所示。然而,这个想法是通用的:它也适用于更高维的情况。梯度在最优值处变为零的事实常被用来代数计算最优值。以下例子说明了这一点。

图 3.6 最小值总是拐点,意味着函数在该点转折。如果我们考虑最小值两侧的任意两点 P[−] 和 P [+],则梯度在一侧为正,在另一侧为负。假设梯度变化平滑,那么在最小值之间必须为零。
考虑简单的示例函数 L(w[0], w[1]) = √w[0]² + w[1]²。其最优值出现在梯度为零时:

解为 w[0] = 0, w[1] = 0 函数在其原点达到最小值,这与我们的直觉相符。
3.3.2 水平面表示和损失最小化
在图 3.5 中,我们绘制了损失函数 L(
) 与参数值
的关系。在本节中,我们研究了一种不同的可视化损失表面的方法。这将进一步揭示梯度和最小化的见解。
我们将继续使用上一个小节中的简单示例函数。考虑一个场 L(w[0], w[1]) = √(w[0]² + w[1]²)。其定义域是由轴 W[0] 和 W[1] 定义的无限二维平面。请注意,该函数在以原点为中心的同心圆上具有常数值。例如,在圆 w[0]² + w[1]² = 1 的所有点上,函数具有常数函数值 1。在圆 w[0]² + w[1]² = 25 的所有点上,函数具有常数函数值 5。这种定义域上的常数函数值曲线在二维中称为 等高线。这如图 3.7 中的热图所示。等高线的概念可以推广到更高维度,其中我们具有等高面或等高超曲面。请注意,虽然图 3.5 中的
,L(
) 在 (n+1)-维空间中(其中 n 是
的维度),但等高面/等高线表示是在 n-维空间中。在定义域上的任何一点,沿着哪个方向函数值的变化最大?答案是 沿着梯度的方向。变化的幅度对应于梯度的幅度。在当前示例中,假设我们位于点 (w[0], w[1])。通过此点存在一个等高线:以原点为中心通过 (w[0], w[1]) 的圆。如果我们沿着这个圆的周长移动——即沿着这个圆的切线——函数值不会改变。换句话说,在任何一点,通过该点的等高线的切线是 最小变化的方向。另一方面,如果我们垂直于切线移动,函数值的变化最大。切线的垂线称为 法线。这是梯度的方向。在定义域上的任何一点,梯度总是垂直于通过该点的等高线,指示函数值最大变化的方向。在图 3.7 中,梯度都与同心圆的半径平行。
回想一下,在训练机器学习模型时,我们本质上是在一组可调参数的术语下定义一个损失函数,并通过调整(微调)参数来最小化损失。我们从随机点开始,迭代地向最小值前进。从几何上看,这可以看作是从定义域上的任意一点开始,并继续沿着最小化函数值的方向移动。当然,我们希望尽可能少地进行迭代,以到达函数值的最低点。在图 3.7 中,最小值在原点,这也是所有同心圆的中心。无论我们从哪里开始,我们都需要始终沿着径向向内移动,以达到函数√(w[0]² + w[1]²)的最小值(0,0)。
在更高维的情况下,等高线变为等高面。给定任何函数L(
),其中
] ∈ ℝ^n,我们定义等高面为L(
) = constant。如果我们沿着等高面移动,L(
)的变化是最小的(0)。函数在任意点的梯度是穿过该点的等高面的法线。这是函数值变化最快的方向。沿着梯度移动,我们从一个等高面过渡到另一个等高面,如图 3.8 所示。在这里,函数是 3D 的:L(
) = L(w[0], w[1], w[2]) = w[0]² + w[1]² + w[2]²。对于各种常数值,等高面w[0]² + w[1]² + w[2]² = constant是同心球面,以原点为中心。任意点的梯度向量沿着通过该点的球面向外辐射的半径方向。

图 3.7 展示了函数L(w[0], w[1]) = √(w[0]² + w[1]²)的定义域,以函数值的热图形式呈现。梯度方向向外辐射,如箭头线所示。热图强度沿梯度(即半径)变化最快。这是快速达到热图所表示函数的较低值的方向。

图 3.8 3D 中的梯度示例:函数 L(w[0], w[1], w[2]) = L(
) = w[0]² + w[1]² + w[2]². 等值面 L(
) = constant 是以原点为中心的同心球体。图中部分展示了这样一个表面。∇L(
) = k[w[0] w[1] w[2]]^T—梯度指向径向外。沿着梯度移动,我们从一级表面移动到另一级表面,对应于 L(
) 的最大变化。沿着与梯度垂直的任何方向移动,我们保持在同一级表面(球体)上,这对应于函数值的变化为零。D[θ](
) 表示沿着与梯度成 θ 角的位移方向的偏导数。如果 l̂ 表示这个位移方向,D[θ](
) = ∇L(
) ⋅ l̂。
另一个例子如图 3.9 所示。这里函数是 3D 的:L(
) = f(w[0], w[1], w[2]) = w[0]² + w[1]². 对于常数的各种值,等值面 w[0]² + w[1]² = constant 是以 w[2] 为轴的同轴圆柱体。图中部分展示了这样一个表面。在任意点的梯度向量沿着属于该点的圆柱体的平面圆的向外指向的半径。沿着梯度移动,我们从一级表面移动到另一级表面,对应于 L(
) 的最大变化。沿着与梯度垂直的任何方向移动,我们保持在同一级表面(圆柱体)上,这对应于函数值的变化为零。D[θ](
) 表示沿着与梯度成 θ 角的位移方向的偏导数。如果 l̂ 表示这个位移方向,D[θ](
) = ∇L(
) ⋅ l̂。

图 3.9 3D 中的梯度示例:函数 L(w[0], w[1], w[2]) = L(
) = w[0]² + w[1]². 等值面 f(
) = constant 是同轴的圆柱体。图中部分展示了这样一个表面:∇L(
) = k[w[0] w[1] 0]^T. 梯度垂直于圆柱体的曲面,沿着圆的向外半径。沿着梯度移动,我们从一级表面移动到另一级表面,对应于 L(
) 的最大变化。沿着与梯度垂直的任何方向移动,我们保持在同一级表面(圆柱体)上,这对应于函数值的变化为零。
到目前为止,我们已经研究了参数空间中无限小位移引起的损失值的变化。在实践中,训练过程中参数更新时程序性位移是小的,但不是无限小的。在这些情况下,有没有什么方法可以改进近似?这将在下一节中讨论。
3.4 损失函数的局部近似
方程 3.6 表达了参数空间中位移
对应的损失值变化 δL。当损失函数是线性的或位移的幅度无限小的时候,方程是完全正确的。在实践中,我们通过小量(但不是无限小量)调整参数值。在这种情况下,方程 3.6 只是大致正确的:||
|| 的幅度越大,近似越差。
泰勒级数提供了一种方法,通过在参数空间中的位移来近似任何点的局部邻域中的多维函数。它是一个无限级数,这意味着方程只有在我们将无限多个项相加时才是完全正确的(零近似)。当然,我们无法用计算机程序添加无限多个项。但我们可以通过包括越来越多的项来尽可能提高近似的精度。在实践中,我们最多包括到第二项。任何超过这一项的都是多余的,因为改进太小,无法通过当前计算机的浮点系统实现。首先,我们将研究一维的泰勒级数。
3.4.1 1D 泰勒级数回顾
假设我们试图描述特定点 w 附近的曲线 L(w)。如果我们无限接近于 w,那么,如第 3.3 节所述,我们可以用直线来近似曲线:

但在一般情况下,如果我们描述的是特定点附近的连续(光滑)函数,我们使用泰勒级数。泰勒级数允许我们用函数在该点的值及其导数来描述该点附近的函数。在 1D 中这样做相对简单:

方程 3.7
注意,项会逐渐变小(因为它们涉及到越来越小的数 δw 的高次幂)。因此,尽管级数延伸到无穷大,但在实践中,我们通过省略高阶项来忽略可忽略的精度损失。我们通常使用一阶近似(或者最多二阶)。方程 3.7 可以重写为

注意,第二项包含 (δw)² 作为因子,在位移 δw 很小的时候几乎为零。所以,对于非常小的 δw,我们只包括第一项。然后我们得到 δL = (δw/1!) (dL/dw),这与方程 3.3 相同。如果 δw 稍大,并且我们想要更高的精度,我们可以包括二阶项。在实践中,如前所述,这种情况几乎从未发生过。
泰勒级数的一个实用例子是指数函数e^x在x = 0 附近的展开

其中我们使用了以下事实:dn*/*dxn (e^x)|[x = 0] = e^x|[x = 0] = 1 对所有n成立。
3.4.2 多维泰勒级数和 Hessian 矩阵
在方程 3.7 中,我们用导数在点附近的小邻域内表示一个单变量函数。在更高维度中,我们能做类似的事情吗?是的。我们只需将一阶导数替换为梯度。我们将二阶导数替换为其多维对应物:Hessian 矩阵。多维泰勒级数如下

方程 3.8
其中H(L(
)),称为 Hessian 矩阵,定义为

方程 3.9
Hessian 矩阵是对称的,因为
。此外,请注意,泰勒展开假设函数在邻域内是连续的。
方程 3.8 允许我们在参数空间中点
附近的小邻域内计算L的值。如果我们从
通过向量
移动,我们到达
+
。那里的损失是L(
+
),它通过方程 3.8 以原始点的损失L(
)和位移
来表示。

方程 3.10
注意,第一项与方程 3.6 相同,第二项是位移的平方。由于小量的平方更小,对于非常小的位移,第二项消失,我们本质上又回到了方程 3.6。这被称为一阶近似。对于稍微大一点的位移,我们可以包括涉及 Hessian 的二阶项,以改进近似。如前所述,这在实践中几乎从未做过。
3.5 梯度下降、误差最小化和模型训练的 PyTorch 代码
在本节中,我们研究 PyTorch 示例,其中模型通过最小化误差通过梯度下降进行训练。在我们展示代码之前,我们从实际的角度简要回顾一下主要思想。本节的完整代码可以在mng.bz/4Zya找到。
3.5.1 线性模型的 PyTorch 代码
如果我们试图预测的真实基础函数非常简单,线性模型就足够了。否则,我们需要非线性模型。在这里,我们将研究线性模型。在机器学习中,我们确定与手头问题相关的输入和输出变量,并将问题表述为从输入变量生成输出。所有输入都由向量
一起表示。有时有多个输出,有时只有一个输出。相应地,我们有一个输出向量
或输出标量 y。让我们用 f 表示从输入向量生成输出的函数:即,y = f(
)。
在现实生活中的问题中,我们不知道 f。机器学习的核心是从一组观察到的输入
[i] 和它们相应的输出 y[i] 中估计 f。每个观察结果可以表示为一个对 ⟨
[i], y[i]⟩。我们用已知函数 ϕ 来建模未知函数 f。ϕ 是一个参数化函数。尽管 ϕ 的性质是已知的,但其参数值是未知的。这些参数值通过训练“学习”得到。这意味着我们估计参数值,以使观察的整体误差最小化。
如果
,b 表示当前的参数集(权重,偏差),那么模型将在观察到的输入
[i] 上输出 ϕ(
[i],
, b)。因此,这个 i 次观察的误差是 e[i]² = (ϕ(
[i],
, b)−y[i])²。我们可以批量处理几个观察结果,并将误差加起来得到批量误差 L = Σ[i = 0]^(i = N) (e^((i)))²。
误差是参数集
的函数。问题是,我们如何调整
以使误差 e[i]² 减少?我们知道函数的值在沿着参数梯度的方向移动时变化最大。因此,我们调整参数
,b 如下:

每次调整都会减少误差。从一组随机的参数值开始,并足够多次地这样做,可以得到期望的模型。
一个简单且流行的模型ϕ是线性函数(预测值是输入向量和参数向量之间的点积加上偏置):ỹ[i] = ϕ(
[i],
, b) =
^T
+ b = ∑[j]w[j]x[j] + b. 我们最初的实现(列表 3.1)只是简单地模仿了这个公式。对于更复杂的模型ϕ(具有数百万个参数和非线性),我们无法获得这样的闭合形式梯度。在这种情况下,我们使用一种称为自动微分(自动梯度计算)的技术,它不需要闭合形式梯度。这将在下一节中讨论。
备注:在现实世界的问题中,我们不会知道将输入映射到输出的真实底层函数。但在这里,为了获得洞察力,我们将假设已知的输出函数,并通过添加噪声来使它们稍微更真实。
列表 3.1 PyTorch 线性模型(需要闭合形式的梯度公式)
x = 10 * torch.randn(N) ①
= 1.5 * x + 2.73 _obs = y + (0.5 * torch.randn(N)) ②
for step in range(num_steps):
y_pred = w*x + b ③
mean_squared_error = torch.mean(
(y_pred - y_obs) ** 2) ④
w_grad = torch.mean(2 * ((y_pred - y_obs)* x))
b_grad = torch.mean(2 * (y_pred - y_obs)) ⑤
w = w - learning_rate * w_grad
b = b - learning_rate * b_grad ⑥
print("True function: y = 1.5*x + 2.73")
print("Learned function: y_pred = {}*x + {}".format(w[0], b[0]))
① 生成随机输入值
② 通过将一个简单的已知函数应用于输入并添加噪声来生成输出值。让我们看看我们学习到的函数是否与已知的底层函数相匹配。
③ 我们用任意参数值初始化的模型
④ 模型误差是观测值与预测值之间的(平方)差。
⑤ 使用微积分计算误差的梯度。仅适用于此类简单模型。
⑥ 沿着误差梯度调整权重和偏置
输出如下:
True function: y = 1.5*x + 2.73
Learned function: y_pred = 1.50059*x + 2.746823
3.5.2 Autograd:PyTorch 自动梯度计算
在列表 3.1 中的 PyTorch 代码,对于这个特定的模型架构,我们使用了微积分来计算梯度。这种方法不适用于具有数百万个权重和可能非线性复杂函数的更复杂模型。为了可扩展性,我们使用像 PyTorch Autograd 这样的自动微分软件库。库的用户无需担心如何计算梯度——他们只需构建模型函数。一旦函数被指定,PyTorch 就会通过 Autograd 技术找出如何计算其梯度。
要使用 Autograd,我们明确告诉 PyTorch 在创建变量时跟踪该变量的梯度,通过设置requires_grad = True。PyTorch 会记住一个计算图,每次我们使用跟踪变量创建表达式时,该图都会更新。图 3.10 展示了计算图的一个示例。

图 3.10 Autograd 分析
以下列表,实现了 PyTorch 中的线性模型,依赖于 PyTorch 的 Autograd 进行梯度计算。请注意,此方法不需要闭合形式的梯度。
列表 3.2 使用 PyTorch 进行线性建模
def update_parameters(params, learning_rate): ①
with torch.no_grad(): ②
for i, p in enumerate(params):
params[i] = p - learning_rate * p.grad
for i in range(len(params)):
params[i].requires_grad = True ③
x = 10 * torch.randn(N) ④
y = 1.5 * x + 2.73
y_obs = y + (0.5 * torch.randn(N)) ⑤
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
params = [b, w] ⑥
for step in range(num_steps):
y_pred = params[0] + params[1] * x
mean_squared_error = torch.mean((y_pred - y_obs) ** 2) ⑦
mean_squared_error.backward() ⑧
update_parameters(params, learning_rate) ⑨
print("True function: y = 1.5*x + 2.73")
print("Learned function: y_pred = {}*x + {}"
.format(params[1].data[0], params[0].data.[0]))
① 更新参数:沿着误差梯度调整权重和偏置
② 在参数更新期间不跟踪梯度
③ 恢复梯度跟踪
④ 生成随机训练输入
⑤ 生成训练输出:将简单的已知函数应用于输入,然后添加噪声。让我们看看我们学习到的函数是否与已知的底层函数匹配。
⑥ 我们的模型,使用任意参数值初始化
⑦ 模型误差是(平方的)误差相对于
之间。
⑧ 反向传播:计算偏导数
误差相对于每个变量的导数
每个变量之间的观察值和预测值之间的偏导数,并将它们存储在变量的“grad”字段中
⑨ 使用这些偏导数更新参数
输出如下:
True function: y = 1.5*x + 2.73
Learned function: y_pred = 1.50059*x + 2.74783
3.5.3 PyTorch 中的非线性模型
在 3.1 和 3.2 列表中,我们将线性模型拟合到已知为线性的数据分布。从输出中,我们可以看到这些模型收敛到了底层输出函数的良好近似。这也在图 3.11 中以图形方式展示。但如果底层输出函数是非线性的呢?
首先,3.3 列表尝试在非线性数据分布上使用线性模型。正如预期(并通过输出以及图 3.12 所展示),这个模型表现不佳,因为我们使用了不合适的模型架构。进一步的训练将不会有所帮助。

图 3.11 线性数据的线性近似。到第 1,000 步,模型已经或多或少收敛到真实的潜在函数。

图 3.12 非线性数据的线性近似。显然,模型并没有收敛到接近期望/真实函数的任何东西。我们的模型架构是不充分的。
列表 3.3 非线性数据的线性近似
x = 10 * torch.rand(N, 1) ①
y = x**2 - x + 2.0
y_obs = y + (0.5 * torch.rand(N, 1) - 0.25) ②
w = torch.rand(1, requires_grad=True)
b = torch.rand(1, requires_grad=True)
params = [b, w]
for step in range(num_steps):
y_pred = params[0] + params[1] * x ③
mean_squared_error = torch.mean((y_pred - y_obs) ** 2)
mean_squared_error.backward()
update_parameters(params, learning_rate)
print("True function: y = 1.5*x + 2.73")
print("Learned function: y_pred = {}*x + {}"
.format(params[1].data[0], params[0].data[0]))
① 生成随机输入训练数据
② 生成训练输出:将已知的非线性函数应用于输入,然后添加噪声
③ 按照列表 3.2 的方式训练线性模型
这里是输出:
True function: y=x² -x + 2
Learned function: y_pred = 8.79633331299*x + -13.4027605057
接下来,3.4 列表尝试使用非线性模型。正如预期(并通过输出以及图 3.13 所展示),非线性模型表现良好。在现实生活中的问题中,我们通常假设非线性,并相应地选择模型架构。

图 3.13 如果我们使用非线性模型,它或多或少会收敛到真实的潜在函数。
列表 3.4 使用 PyTorch 进行非线性建模
params = [w0, w1, w2]
for step in range(num_steps):
y_pred = params[0] + params[1] * x + params[2] * (x**2)
mean_squared_error = torch.mean((y_pred -y_obs) ** 2)
mean_squared_error.backward()
update_parameters(params, learning_rate)
print("True function: y= 2 - x + x²")
print("Learned function: y_pred = {} + {}*x + {}*x²"
.format(params[0].data[0],
params[1].data[0],
params[2].data[0]))
这里是输出:
True function: y= 2 - x + x²
Learned function: y_pred = 1.87116754055+-0.953767299652*x+0.996278882027*x²
3.5.4 PyTorch 中猫脑的线性模型
在 2.12.5 节中,我们通过伪逆直接解决了猫脑问题。现在,让我们在相同的数据集上训练一个 PyTorch 模型。正如预期,模型参数将收敛到一个接近伪逆技术获得的解(这是一个简单的训练数据集);但在下面的列表中,我们展示了我们的第一个相对复杂的 PyTorch 模型。
列表 3.5 我们第一个现实的 PyTorch 模型(解决猫脑问题)
X = torch.tensor([[0.11, 0.09], ... [0.63, 0.24]]) ①
X = torch.column_stack((X, torch.ones(15))) ①
②
y = torch.tensor([-0.8, ... 0.37]) ①
class LinearModel(torch.nn.Module):
def __init__(self, num_features):
super(LinearModel, self).__init__()
self.w = torch.nn.Parameter( ③
torch.randn(num_features, 1))
def forward(self, X):
y_pred = torch.mm(X, self.w) ④
return y_pred
model = LinearModel(num_features=num_unknowns)
loss_fn = torch.nn.MSELoss(reduction='sum') ⑤
optimizer = torch.optim.SGD(model.parameters(), lr=1e-2) ⑥
for step in range(num_steps):
y_pred = model(X)
loss = loss_fn(y_pred, y)
optimizer.zero_grad() ⑦
loss.backward() ⑧
optimizer.step() ⑨
solution_gd = torch.squeeze(model.w.data)
print("The solution via gradient descent is {}".format(solution_gd))
① X,
根据方程 2.22 创建(见第 2.12.3 节),它很容易验证方程 2.22 的解大致为 w[0] = 1,w[1] = 1,b = −1。但是方程不一致:没有一种解能完美地适合所有这些。我们期望学习到的模型接近 y = x[0] + x[1] − 1。
② 向数据矩阵 X 添加一列全 1 的列以增强数据矩阵
③ 参数是 Torch Tensor 的类型(子类),适用于模型参数(权重+偏置)
④ 线性模型:
= X
(X 被增强,且
包含偏置)
⑤ 预制类用于计算平方误差损失
⑥ 预制类用于使用误差梯度更新权重
⑦ 将所有偏导数置为零
⑧ 通过自动微分计算偏导数
⑨ 使用 backward() 步骤中计算的梯度更新参数
输出如下:
The solution via gradient descent is [ 1.0766 0.8976 -0.9581]
3.6 凸和非凸函数,以及全局和局部最小值
凸表面(见图 3.14)有一个唯一的最佳点(最大值/最小值):全局最佳点。² 在这样的表面上,无论我们处于何处,如果我们沿着参数空间中的梯度移动,我们最终会达到全局最小值。另一方面,在非凸表面上,我们可能会陷入局部最小值。例如,在图 3.14b 中,如果我们从箭头线标记的梯度点开始,沿着梯度向下移动,我们将到达一个局部最小值。在最小值处,梯度为零,我们将永远不会从这个点移动出去。

(a) 凸函数

(b) 非凸函数
图 3.14 凸与非凸函数。凸函数只有一个全局最佳点(最小值或最大值),没有局部最佳点。沿着梯度向下移动可以保证达到全局最小值。友好的误差函数是凸的。非凸函数有一个或多个局部最佳点。沿着梯度移动可能会达到局部最小值,并且永远发现不了全局最小值。不友好的误差函数是非凸的。
曾经有一段时间,研究人员投入大量精力试图避免局部最小值。为此开发了特殊技术(如模拟退火)来避免它们。然而,神经网络通常不会做任何特别的事情来处理局部最小值和非凸函数。通常,局部最小值已经足够好。或者我们可以从不同的随机点重新训练,这可能会帮助我们逃离局部最小值。
3.7 凸集和函数
在第 3.6 节中,我们简要介绍了凸函数以及凸性如何告诉我们函数是否有局部极小值。在本节中,我们将更详细地研究凸函数。特别是,我们学习如何判断给定的函数是否是凸函数。我们还讨论了一些凸函数的重要性质,这些性质将在以后很有用——例如,当我们研究概率和统计中的 Jensen 不等式时,在附录中。我们将主要在二维空间中阐述这些思想,但它们可以很容易地扩展到更高维。
3.7.1 凸集
不正式地说,一个点集被称为凸集,当且仅当连接该集中任意两点之间的直线完全位于该集内。例如,如果我们用直线连接图 3.15 左侧阴影区域内的任意一对点,那么该直线上的所有点也将位于阴影区域内。这如图中的点 A 和 B 所示。任何此类区域中的所有点集构成一个凸集。

图 3.15 凸集和非凸集。左侧阴影区域中的点构成一个凸集。连接该阴影区域内任意一对点的直线完全位于阴影区域内:例如,AB。右侧阴影区域中的点构成一个非凸集。例如,连接点 C 和 D 的直线穿过非阴影区域,尽管两个端点都属于阴影区域。
相反,如果一个点集包含至少一对点,它们的连接线包含不属于该集的点,则该点集是非凸的。例如,图 3.15 右侧的阴影区域包含一对点 C 和 D,它们的连接线穿过不属于阴影区域的点。
凸集的边界总是凸曲线。
3.7.2 凸曲线和曲面
考虑一个函数 g(x). 让我们选取曲线 y = g(x) 上的任意两点:A ≡ (x[1], y[1] = g(x[1])) 和 B ≡ (x[2], y[2] = g(x[2])). 现在考虑连接 A 和 B 的线段 L. 从第 2.8.1 节(方程 2.12 和图 2.8),我们知道线段 L 上的所有点 C 可以表示为 A 和 B 坐标的加权平均,权重之和为 1。因此,C ≡ (α[1]x[1] + α[2]x[2], α[1]y[1] + α[2]y[2]),其中 α[1] + α[2] = 1。比较 C 与其在曲线上的对应点 D,它具有相同的 X 坐标:
D ≡ (α[1]x[1] + α[2]x[2], g(α[1]x[1] + α[2]x[2])).
只有当 g(x) 是凸函数时,C 才会始终位于 D 上,或者
α[1]y[1] + α[2]y[2] = α[1]g(x[1]) + α[2]g(x[2]) ≥ g(α[1]x[1] + α[2]x[2])
从连接曲线上一对点的割线上的任何一点向 X-轴引垂线,这条垂线将在曲线的较低点(即在 Y-坐标上较小)处截断曲线。
这在图 3.19 的左侧得到了说明,其中函数 g(x) = x²(已知是凸函数)和 A ≡ (−3,9) 以及 B ≡ (5,25),α[1] = 0.3,α[2] = 0.7。可以看出,线上的加权平均点 C 位于曲线对应点 D 的上方。右侧说明了非凸函数 g(x) = x³,其中 A ≡ (−8,−512) 和 B ≡ (5,125),α[1] = 0.3,α[2] = 0.7。图中显示了连接曲线上的点 A 和 B 的线上的一个加权平均点 (C):C 位于曲线上的点 D 下方,该点的 X-坐标相同。

图 3.16 凸和非凸曲线。A 和 B 是曲线上的一个点对。C = 0.3A + 0.7B 是 A 和 B 坐标的加权平均,权重之和为 1。C 位于连接 A 和 B 的线上。左侧的曲线是凸的:C 位于对应的曲线点 D 之上。右侧的曲线是非凸的:C 位于对应的曲线点 D 之下。
我们不必限制自己只考虑两个点。我们可以对曲线上任意数量的点进行加权平均,权重之和为 1。对应于加权平均的点将位于曲线之上(即位于具有相同 X-坐标的曲线上点之上)。这一思想也扩展到更高维度,如以下所述。
定义 1
一般而言,一个多维函数 g(
) 是凸的,当且仅当
-
给定函数表面(如果函数是一维的,则为曲线)上的任意一组点((
[1],g(
[1])),(
[2],g(
[2])),⋯,(
[n],g(
[n])), -
给定一个任意权重集 α[1],α[2],⋯,α[n],它们的和为 1(即,Σ[i]^n[= 1] α[i] = 1),
-
函数输出的加权总和超过或等于加权总和的函数输出:

方程式 3.11
仔细思考可以发现,定义 1 意味着凸曲线始终向上和/或向右卷曲。这导致凸性的另一个等价定义。
定义 2
一般而言,一个多维函数 g(
) 是凸的,当且仅当
- 一维函数 g(x) 是凸的,当且仅当它在任何地方都具有正曲率:

方程式 3.12
- 一个多维函数 g(
) 是凸的,当且仅当其 Hessian 矩阵(见 3.4.2,方程 3.9)是正半定的,即 Hessian 矩阵的所有特征值都大于或等于零。这正是方程 3.12 的多维类似物。
需要注意的一个微妙之处是,如果二阶导数在所有地方都是负的,或者 Hessian 矩阵是负半定的,则曲线或表面被称为凹的。这与非凸曲线不同,在非凸曲线中,二阶导数在某些地方是正的,在其他地方是负的。凹函数的负值是凸函数。但非凸函数的负值仍然是非凸的。
在任何地方都向上弯曲的函数始终位于其切线之上。这导致凸函数的另一个等价定义。
定义 3
通常,一个多维函数 g(
) 是凸的,当且仅当
-
一个函数 g(x) 是凸的,当且仅当曲线 S ≡ (x, g(x)) 上的所有点在任何点 A 处都位于切线 T 之上,且 S 仅在 A 处接触 T。
-
一个函数 g(
) 是凸的,当且仅当表面 S ≡ (
, g(
)) 上的所有点在任何点 A 处都位于切平面 T 之上,且 S 仅在 A 处接触 T。
这在图 3.17 中得到了说明。

图 3.17 左侧的曲线是凸的。如果我们曲线上的任何点 A 处画一条切线,整个曲线都位于切线之上,仅在 A 处接触切线。右侧的曲线是非凸的:曲线的一部分位于切线之上,另一部分位于切线之下。
3.7.3 凸性与泰勒级数
在 3.4.1 节中,方程 3.7,我们看到了函数在点 x 附近的单变量泰勒展开。如果我们只保留泰勒展开中的直到一阶导数的项,并忽略所有后续项,那么这相当于用 x 处的切线来近似 x 处的函数(见图 3.17)。这是曲线的线性近似。如果我们保留一个额外的项(即,直到二阶导数),我们得到曲线的二次近似。如果函数的二阶导数始终为正(如凸函数),则函数的二次近似将始终大于或等于线性近似。换句话说,局部地,曲线将弯曲,使其位于切线之上。这将二阶导数定义(定义 2)与切线定义(定义 3)的凸性联系起来。
3.7.4 凸函数的例子
函数 g(x) = x² 是凸函数。验证这一点最简单的方法是计算 d²g/dx² = d2x/dx = 2,这始终是正的。事实上,任何偶数次幂的 x,例如 x⁴ 或 x⁶,都是凸函数。g(x) = e^x 也是凸函数。这可以通过计算其二阶导数来轻松验证。g(x) = logx 是凹函数。因此,g(x) = −logx 是凸函数。
乘以正标量保持凸性。凸函数的和也是一个凸函数。
摘要
我们希望您能从本章留下以下心理图像:
-
机器学习问题的输入可以被视为向量,或者等价地,是高维特征空间中的点。分类不过是将属于单个类别的点簇在这个空间中分离出来。
-
一个分类器可以从几何上视为高维特征空间中的超曲面(也称为决策边界),它将对应于单个类别的点簇分开。在训练过程中,我们收集具有已知类别的样本输入并识别最佳分离相应点的表面。在推理过程中,给定一个未知输入,我们确定这个点位于决策边界的哪一侧——这告诉我们类别。
-
对于二分类器(也称为二元分类器),如果我们把点代入分类器超平面的函数中,相应输出的符号就给出了类别。
-
为了计算最佳分离训练数据的超曲面决策边界,我们首先选择一个参数函数族来模拟这个表面(例如,对于简单问题,使用超平面)。然后我们估计最佳参数值,以最佳方式分离训练数据,通常以迭代的方式进行。
-
为了估计最佳分离训练数据的参数值,我们定义一个损失函数来衡量模型输出与整个训练数据集中已知期望输出之间的差异。然后,从随机初始值开始,我们迭代调整参数值,使损失值逐渐降低。
-
在每次迭代中,通过计算损失函数的梯度来估计最优减少损失的参数值的调整。
-
多维函数的梯度确定了参数空间中对应函数最大变化的方向。因此,损失函数的梯度确定了我们可以调整参数以最大程度减少损失的方向。
-
函数的最大值或最小值处的梯度为零,这始终是一个拐点。这可以用来识别我们何时达到最小值。然而,在实践中,在机器学习中,我们经常进行早期停止:当损失足够低时终止训练迭代。
-
多维泰勒级数可用于在一点邻域内创建对光滑函数的局部近似。该函数用从该点的位移、一阶导数(梯度)、二阶导数(海森矩阵)等来表示。这可以用来对参数空间中位移引起的损失值变化进行更高精度的近似。
-
损失函数可以是凸的或非凸的。在凸函数中,没有局部最小值,只有一个全局最小值。因此,梯度下降法保证收敛到全局最小值。非凸函数可以同时具有局部和全局最小值。所以,基于梯度的下降法可能会陷入局部最小值。
¹ 如果某个量如 w 的变化极小,我们使用符号 dw 来表示该变化。如果变化小但不是极小的,我们使用符号 δw。↩
² 虽然该理论适用于最优、最大或最小值,但为了简洁起见,这里我们只谈论最小值↩
第四章:机器学习中的线性代数工具
本章节涵盖
-
二次型
-
在数据科学中应用主成分分析(PCA)
-
使用机器学习应用检索文档
在大量高维数据中寻找模式是机器学习和数据科学中的关键。数据通常以大型矩阵的形式出现(例如,在 2.3 节中展示的一个玩具示例,以及方程 2.1 中),数据矩阵的行代表单个输入实例的特征向量。因此,行数与观察到的输入实例数量相匹配,列数与特征向量的大小相匹配——即特征空间中的维度数。从几何学的角度来看,每个特征向量(即数据矩阵的行)代表特征空间中的一个点。这些点在空间中不是均匀分布的。相反,属于特定类别的点集占据该空间的一个小子区域。这导致数据矩阵中存在某些结构。线性代数为我们提供了研究矩阵结构所需工具。
在本章中,我们研究线性代数工具来分析矩阵结构。本章展示了某些复杂的数学,我们鼓励您坚持不懈地学习,包括定理证明。对证明的直观理解将使您对本章的其余部分有更深刻的洞察。
注意:本章节的完整 PyTorch 代码以完全功能化和可执行的 Jupyter 笔记本形式,可在mng.bz/aoYz找到。
4.1 特征数据点的分布和真实维度
例如,考虑确定文档之间相似度的问题。这对于像谷歌这样的文档搜索公司来说是一个重要问题。给定一个查询文档,系统需要从存档中检索与查询文档匹配的文档,并按相似度排序。为此,我们通常为每个文档创建一个向量表示。然后,一对文档表示的向量的点积可以用作文档之间相似度的定量估计。因此,每个文档都由一个文档描述符向量表示,其中词汇表中的每个词都与向量中的一个固定索引相关联。该索引位置存储的值是该词在文档中的频率(出现次数)。
注意:通常省略介词和连词,使用单数形式;来自同一词根的复数和其他变体通常合并为一个词。
词汇表中的每个词在文档空间中都有自己的维度。如果一个词在文档中未出现,我们在该文档描述向量中该词的索引位置放置一个零。我们为存档中的每个文档存储一个描述向量。理论上,文档描述向量是一个极其长的向量:其长度与系统中文档词汇表的大小相匹配。但这个向量只存在于概念上。在实践中,我们并不明确存储描述向量的全部内容。我们为文档中出现的每个唯一词存储一个<词,频率>对——但我们不明确存储未出现的词。这是文档向量的稀疏表示。相应的完整表示可以在必要时从稀疏表示中构建。在文档中,某些词经常一起出现(例如,迈克尔和杰克逊,或枪和暴力)。例如,在给定的一组文档中,枪出现的次数与暴力出现的次数大致相等:如果一个出现,另一个也大多数时候出现。对于一个描述向量或,等价地,表示文档的特征空间中的一个点,对应于词枪的索引位置上的值将大致等于对应于词暴力的值。如果我们将这些点投影到由这些相关词的轴形成的超平面上,所有点都围绕一个 45 度直线(其方程为x = y)分布,如图 4.1 所示。

图 4.1 文档描述空间。词汇表中的每个词对应一个独立的维度。点表示文档特征向量在由术语枪和暴力对应的轴形成的平面上的投影。
在图 4.1 中,代表文档的所有点都集中在 45 度线附近,其余的平面都是空白的。我们能否将定义该平面的两个轴合并,并用围绕大多数数据集中的单一线替换它们?结果是,我们可以这样做。这样做减少了数据表示中的维度数——我们正在用一个维度替换一对相关维度——从而简化了表示。这导致存储成本降低,更重要的是,提供了额外的见解。我们实际上从文档中发现了一个新的主题,称为枪支暴力。
作为另一个例子,考虑一个由坐标 X,Y,Z 表示的 3D 点集。如果所有点的 Z 坐标都接近零,则数据集中在 X,Y 平面周围。我们可以(并且应该)通过将它们投影到 Z = 0 平面上来用二维表示这些点。这样做只会略微近似点的位置(它们被投影到一个它们最初就接近的平面上)。在更现实的例子中,数据点可能聚集在 3D 空间中的任意平面上(而不是 Z = 0 平面)。我们仍然可以通过在它们接近的平面上投影来将这些数据点的维度降低到 2D。
通常情况下,如果一组数据点在空间中分布,使得这些点围绕该空间中的一个低维子空间(如平面或直线)聚集,我们可以将这些点投影到子空间上,并对数据进行降维处理。我们有效地将子空间中的距离近似为零:因为这些距离按定义很小,所以近似并不太差。从另一个角度来看,我们消除了较小的子空间内的变异,并保留了较大的子空间内的变异。这种表示方式更加紧凑,并且更容易进行更好的分析和洞察,因为我们消除了不重要的扰动,并专注于主要模式。
这些思想构成了称为主成分分析(PCA)的技术的基础。它是数据科学家和机器学习实践者工具箱中最重要工具之一。这些思想也是文档检索技术潜在语义分析(LSA)的基础——这是解决机器学习中自然语言处理(NLP)问题的基本方法。本章致力于研究导致 PCA 和 LSA 的一系列方法。我们检查了一个基本的文档检索系统以及相应的 Python 代码。
4.2 二次型及其最小化
给定一个平方对称矩阵 A,标量量 Q =
^TA
被称为二次型。这些在机器学习的各种情况下都可以看到。
例如,回想一下我们在高中学习的圆的方程
(x[0]−α[0])² + (x[1]−α[1])² = r²
其中圆心为 (α[0],α[1]),半径为 r。这个方程可以重写为

如果我们将位置向量
表示为
,并将圆心
表示为
,则前面的方程可以紧凑地表示为
(
−
)^TI(
−
) = r²
注意,这个方程的左边是一个二次型。基于 x[0],x[1] 的原始方程仅适用于二维。基于矩阵的方程是维度无关的:它表示任意维空间中的超球面。对于二维空间,两个方程变为相同。
现在,考虑椭圆的方程:

你可以验证这可以紧凑地写成矩阵形式

或者,等价地,

方程 4.1
其中
。再次,矩阵表示是维度无关的。换句话说,方程 4.1 表示一个超椭球体。注意,如果椭圆轴与坐标轴对齐,矩阵 A 是对角的。如果我们旋转坐标系,每个位置向量都由正交矩阵 R 旋转。方程 4.1 的变换如下(我们使用了方程 2.10 中的矩阵乘积转置规则):

将 R^TAR 替换为 A,我们得到与方程 4.1 相同的方程,但 A 不再是对角矩阵。
对于具有任意轴的通用椭球体,矩阵 A 具有非零的非对角项,但仍然是对称的。因此,多维超椭球体由一个二次型表示。超球面是这个特殊情况之一。
二次型也出现在方程 3.8 所示的多维泰勒展开的第二项中:
是 Hessian 矩阵中的二次型。二次型还有另一个巨大的应用,即 PCA,它如此重要,以至于我们专门用一节来介绍它 4.4)。
4.2.1 最小化二次型
一个重要的问题是,什么选择
可以最大化或最小化二次型?例如,因为二次型是多维泰勒级数的一部分,当我们想要确定最佳移动方向以最小化损失 L(
) 时,我们需要最小化二次型。稍后,我们将看到这个问题也是 PCA 计算的核心。
如果
是一个任意长度的向量,我们可以通过简单地改变
的长度来使 Q 随意变大或变小。因此,使用任意长度
优化 Q 并不是一个很有趣的问题:相反,我们想知道
的哪个 方向 优化 Q。在本节的其余部分,我们将讨论具有单位向量 Q = x̂^TAx̂ 的二次型(记住 x̂ 表示一个单位长度的向量,满足 x̂^Tx̂ = ||x̂||² = 1)。等价地,我们可以使用不同的形式,Q = *
TA*/T
,但我们将在这里使用前者。我们实际上是在搜索所有可能的方向 x̂,检查哪个方向最小化 Q = x̂^TAx̂。
使用矩阵对角化(第 2.15 节),
Q = x̂^TAx̂ = x̂TSΛSTx̂
其中 S = ![[1]
[2] …
[n]] 是以 A 的特征向量为列的矩阵,Λ 是一个对角矩阵,其对角线上的元素是 A 的特征值,其余位置都是 0。代入
ŷ = S^Tx̂
我们得到
Q = x̂^TAx̂ = x̂TS*Λ*STx̂ = ŷ^TΛŷ
方程 4.2
注意,由于 A 是对称的,其特征向量是正交的。这表明 S 是一个正交矩阵:即,S^TS = SS^T = I。回顾第 2.14.2.1 节,对于正交矩阵 S,变换 S^Tx̂ 保持长度。因此,ŷ = S^Tx̂ 是一个单位长度的向量。为了更精确,
||ŷ||² = ||S^Tx̂||² = (STx̂*)*T(S^Tx̂) = x̂TSSTx̂ = x̂^Tx̂ = 1 因为 SS^T = I
因此,展开方程 4.2 的右侧,我们得到

方程 4.3
我们可以假设特征值按大小递减的顺序排列(如果不是,我们可以重新编号它们)。
考虑这个 引理(小证明):当 Σ[i]^n[= 1] y[i]² = 1 且 λ[1] ≥ λ[2] ≥ ⋯ λ[n] 时,量 Σ[i]^n[= 1] λ[i] y[i]² 达到其最大值,其中 y[1] = 1,y[2] = ⋯ y[n] = 0。
以下是一个 直观的证明。如果可能,假设最大值出现在 ŷ 的某个其他值。我们受限于 ŷ 是一个单位向量,因此我们必须保持 Σ[i]^n[= 1] y[i]² = 1。
特别是,ŷ 的任何元素都不能超过 1。如果我们把第一个元素从 1 减少到一个更小的值,比如说 √1-ϵ,其他某个元素必须相应增加以补偿(即,保持单位长度属性)。相应地,假设最大化 Q 的假设 ŷ 是

其中 δ > 0。
如果我们将整个质量从后一项转移到第一项,会发生什么?

做这件事不会改变 ŷ 的长度,因为第一项和其他项的平方和仍然是 1 − ϵ + δ。但是,Q = Σ[i]^n[= 1] λ[i]² 的值在第二种情况下更高(其中
[1] 以牺牲另一个项为代价而增强),因为对于任何 j > 1(由于,λ[1] > λ[2]⋯ 的假设),λ1 > λ1 + λ[j]δ。因此,每当第一项小于 1 而其他某些项大于零时,我们可以通过将全部质量转移到第一项来增加 Q,而不会失去 ŷ 的单位长度属性。
这意味着为了最大化方程 4.3 的右侧,我们必须让单位向量 ŷ 的第一个元素(对应于最大特征值)为 1,其他地方都为 0。任何其他情况都会违反条件,即相应的二次型 Q = Σ[i]^n[= 1] λ[i]² 是一个最大值。
因此,我们已经证明了 Q 的最大值出现在
。相应的 x̂ = Sŷ =
[1] - 对应于 A 的最大特征值的特征向量。
因此,当 x̂ 沿着对应于 A 的最大特征值的特征向量时,二次型 Q = x̂^TAx̂ 达到其最大值。相应的最大 Q 等于 A 的最大特征值。类似地,当 x̂ 沿着对应于最小特征值的特征向量时,二次型的最小值出现。
如上所述,许多机器学习问题可以归结为最小化一个二次型。我们将在后面的章节中研究其中的一些。
4.2.2 对称正(半)定矩阵
一个对称的 n × n 矩阵 A 是正半定的,当且仅当
^TA
≥ 0 ∀
∈ ℝ^n
换句话说,一个正半定矩阵产生一个非负的二次型,其中所有 n × 1 向量
都是非负的。如果我们不允许等式成立,我们得到对称正定矩阵。因此,一个对称的 n × n 矩阵 A 是正定的,当且仅当
^TA
> 0 ∀
∈ ℝ^n
从方程 4.2 和 4.3 可以看出,如果所有 λ[i] 都是正的或零,则 Q 是正的或零(因为
[i]²s 都是非负的)。因此,对称正(半)定性与矩阵的所有特征值都大于(或等于)零的条件等价。
4.3 矩阵的谱范数和 Frobenius 范数
向量是一个具有模和方向的实体。向量
的范数 ||
|| 表示其模。对于矩阵是否有等价的概念?答案是肯定的,我们将研究两个这样的想法。
4.3.1 特征范数
在第 2.5.4 节中,我们看到了向量的长度(也称为模)
是 ||
|| =
^T
. 对于矩阵 A 是否存在一个等价的模的概念?
好吧,一个矩阵可以被看作是一个向量的放大器。矩阵 A 将向量
放大到
= A
。因此,我们可以取所有可能的
中 ||A
|| 的最大可能值;这是 A 的模的一个度量。当然,如果我们考虑任意长度的向量,我们可以通过简单地缩放
来使
随意增大,对于任何 A 来说这都是无趣的。相反,我们想要检查
的哪个方向被放大得最多,以及放大了多少。
我们使用单位向量 x̂ 来考察这个问题:||Ax̂|| 的最大(或最小)值是多少,以及 x̂ 的哪个方向实现了它?这个量

被称为矩阵 A 的 谱范数。请注意,A
是一个向量,||A
||[2] 是它的长度。(我们有时会省略下标 2 并将谱范数表示为 ||A||。)
现在考虑向量 Ax̂。它的模是
||Ax̂|| = (Ax̂)^T(Ax̂) = x̂TATAx̂
这是一个二次型。从第 4.2 节,我们知道当 x̂ 与 A^TA 的最大(最小)特征值对齐时,它将被最大化(最小化)。因此,谱范数由 A^TA 的最大特征值给出

方程 4.4
其中 σ[1] 是 A^TA 的最大特征值。它也是 A 的最大奇异值(平方)。我们将在第 4.5 节再次看到 σ[1],当我们研究奇异值分解(SVD)时。
4.3.2 Frobenius 范数
矩阵模的一个替代度量是 Frobenius 范数,定义为

方程 4.5
换句话说,它是所有矩阵元素的均方根。
可以证明 Frobenius 范数等于矩阵所有奇异值(A^TA 的特征值)之和的均方根

方程 4.6
4.4 主成分分析
假设我们有一组数字集合,X = {x^((0)), x^((1)),⋯, x^((n))}。我们想要了解这些点是如何紧密排列的。换句话说,我们想要测量这些数字的分布范围。图 4.2 展示了这样的分布。

图 4.2 点的 1D 分布。极端点之间的距离不是点分布范围的公平表示:分布不是均匀的,极端点与其他点相距甚远。大多数点都在一个更紧密排列的区域中。
注意,点不必均匀分布。特别是,极端点 (x[max], x[min]) 可能与其他大多数点相距甚远(如图 4.2)。因此,(x[max]– x[min])/(n+1) 并不是这里点平均分布范围的公平表示。大多数点都在一个更紧密排列的区域中。获取分布的统计合理方法是首先获得均值:

然后获得数字与均值的平均距离:

(如果我们愿意,我们可以取平方根并使用σ,但通常没有必要承担额外的计算负担)。这个标量量σ是衡量 1D 中点的平均排列密度或分布范围的好方法。你可能已经注意到,前面的方程不过是统计学中著名的方差公式。我们能否将这个概念扩展到高维数据?
让我们先在二维中考察这个想法。像往常一样,我们用 X[0],X[1],等等来命名坐标轴,而不是 X,Y,以便于扩展到多维度。一个单独的 2D 数据点表示为
。数据集是 {
^((0)),
^((1)), ⋯,
^((n))}。
均值是直接的。我们有两个均值,而不是一个:

因此,我们现在有一个均值向量:

现在我们来做方差。我们面临的一个直接问题是,在二维平面上有无限多个可能的方向。我们可以沿着其中的任何一个测量方差,并且对于每个选择都会不同。当然,我们可以沿着 X[0] 和 X[1] 轴测量方差:

σ[00] 和 σ[11] 告诉我们沿着 仅一个 轴 X[0] 和 X[1] 的方差。但在一般情况下,两个轴上都会存在联合变化。为了处理联合变化,让我们引入一个交叉项:

这些方程可以用矩阵向量表示法简洁地写出:

注意:在 C 的表达式中,我们并没有取向量的点积 (
^((i))−
) 和 (
^((i))−
)。点积将是 (
((*i*))−)*T(
^((i*))−
)。在这里,乘积的第二元素被转置,而不是第一元素。因此,结果是矩阵。点积将产生一个标量。)
前面的方程式是通用的,这意味着它们可以扩展到任何维度。更准确地说,给定一组 n 维多维数据点 X = {
^((0)),
^((1)),⋯,
^((n))),我们可以定义

方程式 4.7

方程式 4.8
注意到均值已经变成了一个向量(对于一维数据,它是一个标量)并且一维的标量方差 σ 已经变成了一个矩阵 C。这个矩阵被称为 协方差矩阵。(n+1)-维均值和协方差矩阵也可以定义为

方程式 4.9
其中

方程式 4.10
对于 i = j,σ[ii] 实际上是数据沿 i 维的方差。因此,矩阵 C 的对角元素包含了沿坐标轴的方差。非对角元素对应于交叉协方差。
注意:方程式 4.8 和 4.9 是等价的。
4.4.1 最大扩展方向
最大扩展/方差的指向是什么?首先考虑一个由单位向量 l̂ 指定的任意方向。回忆起任何向量沿一个方向的分量是由向量与单位方向向量的点积给出的,l̂ 沿 l̂ 的数据点的分量由
X ^′ = {l̂ T((0)), l̂ T((1)),⋯, l̂ T((n))}
注意:记住图 2.8b,它显示了另一个向量中一个向量的分量是由它们之间的点积给出的?l̂ T((i)) 是点积,因此是标量值。
沿方向 l̂ 的扩展由 X ^′ 中标量值的方差给出。X ^′ 中值的均值由

以及方差为

注意到 C ^′ = l̂ ^TCl̂ 是数据成分沿方向 l̂ 的方差。因此,它代表了数据沿该方向的分布范围。沿着哪个方向 l̂,这种分布 l̂ ^TCl̂ 达到最大?这是最大化 C ^′ = l̂ ^TCl̂ 的方向 l̂。这个最大化方向可以通过我们讨论过的二次型优化技术来识别,即 4.2。应用该技术,我们得到以下结果:
-
当 l̂ 沿着协方差矩阵 C 的最大特征值对应的特征向量时,方差达到最大。这个方向被称为多维数据的第一主轴。
-
沿着主轴的数据向量的分量被称为第一主成分。
-
沿着第一主轴的方差值,由协方差矩阵的对应特征值给出,被称为第一主值。
-
第二主轴是协方差矩阵对应于第二大的特征值的特征向量。第二主成分和值被定义为类似。
-
主轴彼此正交,因为它们是对称协方差矩阵的特征向量。
PCA 的实际意义是什么?为什么我们想知道点分布沿最大分布方向的方向?从 4.4.2 到 4.4.5 的章节致力于回答这个问题。
4.4.2 PCA 与降维
在 4.1 节中,我们看到了当数据点围绕一个低维子空间聚集时,将它们投影到子空间并降低数据表示的维度是有益的。降维后的数据可以更紧凑地表示,并且更容易得出见解和分析。在数据点围绕直线或超平面聚集的特定情况下,可以通过去除对应于相对较小的主成分值的主成分来使用 PCA 生成一个低维数据表示。这项技术对数据的维度没有限制。直线或超平面可以在空间中的任何位置,具有任意方向。

(a) 从 2D 到 1D 的降维

(b) 从 3D 到 2D 的降维
图 4.3 通过 PCA 进行降维。原始数据点用实心小圆圈表示,空心圆圈表示低维表示。
例如,考虑图 4.3a 中显示的二维分布。在这里,数据是二维的,并绘制在平面上,但数据的主要分布是沿着图中粗双箭头线所示的 1D 线。垂直于该线的方向(如图中数据点到线的短垂线所示)的分布非常小。主成分分析(PCA)揭示了这种内部结构。由于数据是二维的,有两个主成分值),但其中之一比另一个小得多:这表明降维是可能的。对应于较大主成分值的主轴沿着最大分布的线。沿着其他主轴的小扰动可以消除,而损失的信息很少。用每个数据点在第一个主成分轴上的投影来替换每个数据点,将二维数据集转换为 1D 数据集,揭示数据中的真实基本模式(直线),消除噪声(短垂线),并降低存储成本。
在图 4.3b 中,数据是三维的,但数据点在三维空间中的一个平面上聚集(如图中的矩形所示)。数据的主要分布是沿着这个平面,而垂直于该平面的方向(如图中从数据点到平面的短垂线所示)的分布很小。主成分分析(PCA)揭示了这一点:有三个主成分值(因为数据是三维的),但其中之一比其他两个小得多,表明降维是可能的。对应于小主成分值的主轴垂直于该平面。我们可以忽略这些扰动(图 4.3b 中的垂线)而损失的信息很少。这相当于将数据投影到由前两个主成分轴形成的平面上。这样做可以揭示数据的基本模式(平面),消除噪声(短垂线),并降低存储成本。
4.4.3 PyTorch 代码:PCA 和降维
在本节中,我们提供了一个 PyTorch 代码示例,用于列表 4.1 中的 PCA 计算。然后我们分别提供了应用于相关数据集和无相关数据集的 PyTorch 代码,列表 4.2 和 4.3。结果如图 4.4 所示。

(a) 相关数据的 PCA

(b) 无相关数据的 PCA
图 4.4 PCA 结果。在(a)中,数据点围绕直线y = 2x分布。因此,一个主成分值比另一个大得多,表明降维将有效。在(b)中,两个主成分值都很大。降维将不会有效。
注意:本节完整的 PyTorch 代码以完全功能性和可执行的 Jupyter 笔记本形式,可在mng.bz/aoYz找到。
列表 4.1 PCA 计算
def pca(X): ①
covariance_matrix = torch.cov(X.T)
l, e = torch.linalg.eig(covariance_matrix)
return l, e
① 返回主值和向量
注意:列表 4.1 中 PCA 计算的完全功能代码可在mng.bz/DRYR找到。
列表 4.2 对合成相关数据的 PCA
x_0 = torch.normal(0, 100, (N,)) ①
x_1 = 2 * x_0 + torch.normal(0, 20, (N,)) ②
③
X = torch.column_stack((x_0, x_1))
④
principal_values, principal_vectors = pca(X) ⑤
X_proj = torch.matmul(X, first_princpal_vec) ⑥
① 随机特征向量
② 相关特征向量加少量噪声
③ 数据矩阵主要沿 y = 2x 方向扩展
④ 一个大主值和一个小主值
⑤ 沿 y = 2x 的第一主向量
⑥ 通过投影到第一个主向量进行降维
输出如下:
Principal values are: [62.6133, 48991.0469]
First Principal Vector is: [-0.44, -0.89]
注意:列表 4.2 中 PCA 计算的完全功能代码可在mng.bz/gojl找到。
列表 4.3 对合成非相关数据的 PCA
x_0 = torch.normal(0, 100, (N,))
x_1 = torch.normal(0, 100, (N,)) ①
X = torch.column_stack((x_0, x_1))
principal_values, principal_vectors = pca(X) ②
① 随机非相关特征向量对
② 主值彼此接近。数据点的分布在这两个方向上相当。
这里是输出:
Principal values are [ 9736.4033, 7876.6592]
注意:列表 4.3 中 PCA 计算的完全功能代码可在mng.bz/e5Kz找到。
4.4.4 PCA 的局限性
PCA 假设潜在模式在本质上呈线性。当这并不成立时,PCA 将无法捕捉到正确的潜在模式。这通过图 4.5a 中的示意图和列表 4.3 中的实验结果来展示。图 4.5b 显示了运行列表 4.4 的结果,其中我们合成了非线性相关数据并执行了 PCA。底部直线表示第一个主轴。将数据投影到这个轴上会导致数据位置(信息损失)出现较大误差。线性 PCA 表现不佳。

(a) 带有弯曲潜在模式的 2D 数据分布示意图

(b) 对合成(计算机生成)非线性相关数据的 PCA 结果。底部直线表示第一个主轴。
图 4.5 非线性相关数据。点分布在曲线周围,而不是直线周围。不可能找到一条直线,使得所有点都靠近它。
列表 4.4 对合成非线性相关数据的 PCA
x_0 = torch.normal(0, 100, (N,))
x_1 = 2 * (x_0 ** 2) + torch.normal(0, 5, (N,))
X = torch.column_stack((x_0, x_1))
principal_values, principal_vectors = pca(X) ①
① 主向量未能捕捉到潜在分布
输出如下:
Principal values are [9.3440e+03, 5.3373e+08]
Mean loss in information: 68.0108526887 - high
4.4.5 PCA 与数据压缩
如果我们想在固定的字节数据预算内表示一个大型多维数据集,我们可以通过最小损失精度来丢弃哪些信息?显然,答案是较小的主值方向上的主成分——丢弃它们实际上是有帮助的,如第 4.4.2 节所述。为了压缩数据,我们通常执行 PCA,然后用数据点在第一个几个主轴上的投影来替换它们;这样做可以减少需要存储的数据分量数量。这是 JPEG 98 图像压缩技术背后的基本原理。
4.5 奇异值分解
奇异值分解(SVD)可能是机器学习中最重要的线性代数工具。在许多其他方面,PCA 和 LSA 的实现都是基于 SVD 构建的。我们将在本节中阐述基本思想。
注意:SVD 有几种稍微不同的形式。我们选择了看起来最直观的一种。
SVD 定理表明,任何矩阵 A,无论是奇异的还是非奇异的,矩形的还是方阵的,都可以分解为三个矩阵的乘积
A = UΣV^T
方程式 4.11
其中(假设矩阵 A 是 m × n)
-
Σ 是一个 m × n 的对角矩阵。其对角元素包含 A^TA 的特征值的平方根。这些也被称为 A 的奇异值。奇异值按降序出现在Σ的对角线上。
-
V 是一个 n × n 的正交矩阵,其列包含 A^TA 的特征向量。
-
U 是一个 m × m 的正交矩阵,其列包含 AA^T 的特征向量。
4.5.1 SVD 定理的非正式证明
我们将通过一系列引理非正式地证明 SVD 定理。通过这些引理,我们将获得额外的见解。
引理 1
A^TA 是对称正定矩阵。它的特征值(也称为奇异值)是非负的。它的特征向量(也称为奇异向量)是正交的。
引理 1 的证明
假设 A 有 m 行和 n 列。那么 A^TA 是一个 n × n 的方阵
(ATA*)*T = AT*(*AT)^T = A^TA
这证明了 A^TA 是对称的。对于任何
,
TATA
= (A
)^T(A
) = ||A
||² > 0
这,根据第 4.2.2 节,证明了矩阵 A^TA 是对称和正半定的。因此,其特征值都是正的或零。
我们在第 2.13 节中证明了对称矩阵有正交的特征向量。这证明了奇异向量是正交的。
设 (λ[i], v̂[1]),对于 i ∈ [1, n] 是 A^TA 的特征值、特征向量对——也称为 A 的奇异值、奇异向量对。注意,不失一般性,我们可以假设 λ[1] ≥ λ[2] ≥ ⋯ λ[n],因为如果不是这样,我们总是可以重新编号特征值和特征向量)。
现在,根据定义,
A^TAv̂[i] = λ[i]v̂[i] 对于所有i ∈ [1, n]
从引理 1,单向量是正交的,因此

方程 4.12
注意v̂[i]s 是单位向量(这就是为什么我们使用帽子符号而不是箭头)。如第 2.13 节所述,如果改变特征向量的长度,特征向量仍然是特征向量。我们可以自由选择任何长度的特征向量,只要我们一致选择。我们在这里选择单位长度的特征向量。
引理 2
AA^T是对称正半定的。它的特征值是非负的,特征向量是正交的。
引理 2 的证明
(AAT*)*T = (AT*)*TA^T = AA^T
此外,
TAAT
= (AT*)*T(A^T
) = ||(A^T
)|| ≥ 0
以此类推。
引理 3
1/√λ[i] ⋅ Av̂[1],对于所有i ∈ [1, n]是一个正交的单位向量集合。
引理 3 的证明
让我们取一对这些向量的点积:

由于λ[j], v̂[j]是A^TA的特征值,特征向量对,前面的方程可以重写为

这,使用方程 4.12,可以重写为

引理 4
如果(λ[i], v̂[i])是A^TA的特征值,特征向量对,那么λ[i], û[i] = 1/√λ[i] ⋅ Av̂[i]是AA^T的特征值,特征向量对。
引理 4 的证明
给定
A^TAv̂[i] = λ[i]v̂[i]
将等式的两边左乘以A,我们得到
AA^T**Av̂[i] = λ[i] Av̂[i]
AA^T(Av̂[i]) = λ[i] (Av̂[i])
将
[i] = Av̂[i]代入最后一个方程,我们得到
AA^T
[i] = λ[i]
[i]
这证明了
[i] = Av̂[i]是AA^T的一个特征向量,其对应的特征值为λ[i]。乘以 1/√λ[i]将其转换为根据引理 3 的单位向量。这完成了引理的证明。
4.5.2 SVD 定理的证明
现在,我们已经准备好检查 SVD 定理的证明。
情况 1:A 的行数多于列数
如果m,A的行数大于或等于n,A的列数,我们定义

注意以下内容:
-
从引理 1,我们知道A^TA的特征值是正的。这使得平方根,√λ[i]s,是实数。
-
U 是一个 m × m 的正交矩阵,其列是 AA^T 的特征向量。由于 AA^T 是 m × m 的,它有 m 个特征值和特征向量。其中前 n 个是 û[1] = 1/√λ[1] ⋅ Av̂[1],û[2] = 1/√λ[2] ⋅ Av̂[2],… ,û[n] = 1/√λ[i] ⋅ Av̂[n](根据引理 4,我们知道这些是 AA^T 的特征向量)。在这种情况下,根据我们的初始假设,n < m。因此 AA^T 有 (m − n) 个更多的特征向量,û[n + 1],⋯ û[m]。
-
V 是一个 n × n 的正交矩阵,其列是 A^TA 的特征向量,即 v̂[1],v̂[2],⋯ ,v̂[n])。
考虑矩阵乘积 UΣ。根据基本的矩阵乘法规则(第 2.5 节),我们可以看到

注意到 U 的最后几列,û[n + 1],⋯ ,û[m],在 Σ 中乘以所有零并消失。因此,

U 的后列——那些用 u 命名的列——未能幸存,因为它们乘以 Σ 底部的零。
因此,我们已经证明了
AV = UΣ
然后
AVV^T = UΣV^T
由于 V 是正交的,VV^T = I。因此
A = UΣV^T
这完成了奇异值定理的证明。
情况 2:A 的行数少于列数
如果 m,矩阵 A 的行数小于或等于 n,矩阵 A 的列数,我们有

证明遵循类似的路线。
4.5.3 应用 SVD:PCA 计算
我们将首先用一个玩具数据集来说明这个想法。考虑一个包含五个点的 3D 数据集。我们使用上标来表示数据实例的索引,下标来表示分量。因此,第 i 个数据实例向量表示为 [x[0]^((i)) x[1]^((i)) x[2]^((i))]。我们用矩阵表示整个数据集,其中每个特征实例作为一个行向量出现。数据矩阵是

我们将假设数据已经减去了均值。现在检查矩阵乘积 X^TX,使用普通的矩阵乘法规则:

从方程 4.10 和 4.9,

因此 X^TX 是数据集 X 的协方差矩阵。这对于任意维度和任意特征实例数量都成立。
如果我们创建一个数据矩阵 X,其中每个数据实例形成一个行,X^TX 得到数据集的协方差矩阵。这个矩阵的特征值和特征向量是主成分。因此,对 X 进行 SVD 得到数据的 PCA(假设先前的均值减法)。
4.5.4 应用 SVD:求解任意线性系统
线性系统是一组联立线性方程
A
= 
我们第一次在 2.12 节中遇到了线性系统。可以使用矩阵求逆来解决这样的系统:
= A^(-1)
然而,用矩阵求逆来解决线性系统是不理想的,原因有很多。首先,它是数值不稳定的。矩阵逆在其分母中包含矩阵的行列式。如果行列式接近零,逆矩阵将包含非常大的数。
中的微小噪声将被这些大数相乘,导致计算解
中的大误差。在这种情况下,基于逆的解可能非常不准确。此外,行列式可能为零:这发生在矩阵的一行是其他行的线性组合时,表明我们拥有的方程比我们想象的要少。那么,如果矩阵一开始就不是方阵呢?这发生在我们拥有的方程比未知数多(过定系统)或比未知数少(欠定系统)时。在这些情况下,逆是不可计算的,系统不能完全求解。
即使在这些情况下,我们也希望得到某种意义上的最佳近似解;在方阵的情况下,我们希望得到精确解。我们如何做到这一点?答案:我们使用奇异值分解(SVD)。步骤如下:
-
A
=
可以重写为U(ΣV^T
) =
。然后我们解U
[1] =
。这可以通过使用U的正交性轻松完成,因为
[1] = U^T
。 -
现在,我们有Σ(V^T
) =
[1]。求解Σ
[2] =
[1]。这可以很容易地完成,因为对于任何对角矩阵
,我们可以轻松地计算![]()
因此,
[2] = Σ^(−1)
[1]。 -
现在,我们有V^T
=
[2]。这也可以通过使用V的正交性轻松解决:
= V
[2]
因此,我们求解了
而没有对矩阵A进行求逆:
-
对于可逆的方阵A,这种方法得到的解与基于矩阵逆的方法相同。
-
对于非方阵,这归结为摩尔-彭罗斯逆矩阵,并得到最佳努力解。
4.5.5 矩阵的秩
在第 2.12 节中,我们研究了线性方程组。这样的系统可以用矩阵-向量形式表示:
A
= 
A 和
的每一行都贡献一个方程。如果我们有与未知数一样多的独立方程,则该方程组是可解的。这是最简单的情况;矩阵 A 是方阵且可逆。det(A) 非零,且 A^(−1) 存在。
有时情况会误导。考虑以下系统:

虽然有三个行和显然有三个方程,但这些方程并不独立。例如,第三个方程可以通过将前两个方程相加得到。我们实际上只有两个方程,而不是三个。我们说这个线性系统是 退化的。对于这样的系统 A
=
的以下所有陈述都是正确的:
-
线性方程组是退化的。
-
det(A) = 0。
-
A^(−1) 不能计算,且 A 不可逆。
-
A 的行线性相关。存在一个行的线性组合,其和为零。例如,在前一个例子中,
[0] +
[1] −
[2] = 0。 -
至少 A 的一个奇异值(A^TA 的特征值)是零。线性无关行的数量等于非零特征值的数量。
矩阵中线性无关行的数量被称为其 秩。可以证明矩阵的非零奇异值与其秩一样多。也可以证明矩阵中线性无关列的数量与线性无关行的数量相匹配。因此,秩也可以定义为矩阵中线性无关列的数量。
一个具有 m 行和 n 列的非方矩形矩阵的秩 r = min(m, n)。这样的矩阵永远不可逆。我们通常求助于 SVD 来解它们。
一个具有 n 行和 n 列的方阵是可逆的(非零行列式)当且仅当它具有秩 n。这样的矩阵被称为满秩矩阵。满秩矩阵是可逆的。它们可以通过矩阵逆计算来求解,但逆计算并不总是数值稳定的。SVD 也可以在这里应用,具有良好的数值特性。
非满秩矩阵是退化的。因此,秩是矩阵非退化的度量。
4.5.6 使用 SVD 解线性方程组的 PyTorch 代码
本节中的列表展示了基于 PyTorch 的 SVD 实现,并演示了一个通过 SVD 解线性方程组的应用。
列表 4.5 使用矩阵求逆和 SVD 解可逆线性方程组
A = torch.tensor([[1, 2, 1], [2, 2, 3], [1, 3, 3]]).float()
b = torch.tensor([8, 15, 16]).float() ①
x_0 = torch.matmul(torch.linalg.inv(A), b) ②
U, S, V_t = torch.linalg.svd(A) ③
y1 = torch.matmul(U.T, b) ④
S_inv = torch.diag(1 / S)
y2 = torch.matmul(S_inv, y1) ⑤
x_1 = torch.matmul(V_t.T, y2) ⑥
assert torch.allclose(x_0, x_1) ⑦
① 简单的测试线性方程组
② 矩阵求逆在数值上是不稳定的;SVD 更好。
③ A = USV^T ⟹ A
=
≜ USV^T
= 
④ 解 U
[1] =
. 记住 U^(−1) = U^T 因为 U 是正交的。
⑤ 解 S
[2] =
[1]。记住 S^(−1) 很容易,因为 S 是对角矩阵。
⑥ 解 V^T
=
[2]。记住 V^(−T) = V,因为V是正交的。
⑦ 这两种解是相同的。
下面是输出:
Solution via inverse: [1.0, 2.0, 3.0]
Solution via SVD: [1.0, 2.0, 3.0]
列表 4.6 通过伪逆和 SVD 求解超定线性系统
A = torch.tensor([[0.11, 0.09], [0.01, 0.02],
[0.98, 0.91], [0.12, 0.21],
[0.98, 0.99], [0.85, 0.87],
[0.03, 0.14], [0.55, 0.45],
[0.49, 0.51], [0.99, 0.01],
[0.02, 0.89], [0.31, 0.47],
[0.55, 0.29], [0.87, 0.76],
[0.63, 0.24]]) ①
A = torch.column_stack((A, torch.ones(15)))
b = torch.tensor([-0.8, -0.97, 0.89, -0.67,
0.97, 0.72, -0.83, 0.00,
0.00, 0.00, -0.09, -0.22,
-0.16, 0.63, 0.37])
x_0 = torch.matmul(torch.linalg.pinv(A), b) ②
U, S, V_t = torch.linalg.svd(A, full_matrices=False) ③
y1 = torch.matmul(U.T, b)
S_inv = torch.diag(1 / S)
y2 = torch.matmul(S_inv, y1)
x_1 = torch.matmul(V_t.T, y2)
assert torch.allclose(x_0, x_1) ④
① 猫脑数据集:非方阵
② 通过伪逆求解
③ 通过 SVD 求解
④ 这两种解是相同的。
输出如下:
Solution via pseudo-inverse: [ 1.0766, 0.8976, -0.9582]
Solution via SVD: [ 1.0766, 0.8976, -0.9582]
解决基于 SVD 的线性系统的完整代码可以在mng.bz/OERn找到。
4.5.7 使用 SVD 进行 PCA 计算的 PyTorch 代码
以下列表展示了使用 SVD 进行 PCA 计算。
列表 4.7 直接计算 PCA 和使用 SVD
① ② ③
principal_values, principal_vectors = pca(X) ④
X_mean = X - torch.mean(X, axis=0)
⑤
U, S, V_t = torch.linalg.svd(X_mean) ⑥
V = V_t.T ⑦
① 协方差矩阵的特征值产生主值。
② 协方差矩阵的特征向量产生主向量。
③ 从协方差矩阵直接计算 PCA
④ 数据矩阵
⑤ 矩阵S的对角元素产生主值。
⑥ 从 SVD 进行 PCA
⑦ 矩阵V的列产生主向量。
输出如下:
Principal components obtained via PCA:
[[-0.44588404 -0.89509073]
[-0.89509073 0.44588404]]
Principal components obtained via SVD:
[[-0.44588404 0.89509073]
[-0.89509073 -0.44588404]]
4.5.8 应用 SVD:矩阵的最佳低秩逼近
给定一个秩为p的矩阵A,我们有时想用秩r的矩阵来逼近它,其中r < p。我们如何获得A的最佳秩r逼近?
动机
我们为什么要这样做呢?好吧,考虑一个如 4.5.3 节中所示的数据矩阵X。如 4.4.2 节中解释的那样,我们通常希望消除数据中的小方差(可能是由于噪声引起的),并获取底层的大变化模式。用低秩矩阵替换数据矩阵通常可以达到这个目的。然而,我们必须记住,当底层模式是非线性时(如图 4.5a 所示),这并不适用。
逼近误差
我们所说的最佳逼近是什么意思?我们可以将 Frobenius 范数视为矩阵的模。因此,给定一个矩阵A及其秩r的逼近A[r],逼近误差是e = ||A − A[r]||[F]。
方法
为了巩固我们的想法,让我们考虑一个m × n的矩阵A。从 4.5 节中,我们知道它将会有min(m, n)个奇异值。设其秩为p ≤ min(m, n)。我们想要用秩r(<p)的矩阵来逼近这个矩阵。
让我们重写 SVD 表达式。我们将假设m > n。同样,通常情况下,我们按降序排列奇异值:λ[1] ≥ λ[2] ≥ λ[n]。我们将划分U,Σ,V:

可以证明 U[1]Σ[1]V[1]^T 是一个秩r的矩阵。此外,它是A的最佳秩r逼近。
4.6 机器学习应用:文档检索
现在我们将本章中讨论的几个概念结合到一个说明性的玩具示例中:我们在第 2.1 节首次遇到的文档检索问题。简要回顾一下,我们有一组文档{d[0],⋯, d[6]}。给定一个查询短语,我们必须检索与查询短语匹配的文档。我们将使用词袋模型:也就是说,我们的匹配方法不关注一个词在文档中的位置;它只关注词在文档中出现的次数*。尽管这种技术不是最复杂的,但由于其概念简单,它很受欢迎。
我们的文档如下:
-
**d[0]: 玫瑰很美丽。没有人讨厌玫瑰。
-
**d[1]: 在美国,枪 暴力已经达到流行病的规模。
-
**d[2]: 枪 暴力问题实际上被过度炒作。可以找到许多没有涉及枪的暴力实例。
-
**d[3]: 枪是倾向暴力的人用的。暴力滋生枪。枪滋生暴力。
-
**d[4]: 我喜欢枪但讨厌暴力。我从未参与过暴力。但我拥有很多枪。对我来说,枪支暴力是无法理解的。我相信枪主是地球上最反暴力的人。从不使用枪的人容易陷入无意义的暴力。
-
**d[5]: 昨晚旧金山发生了一起武装抢劫,使用了枪。
-
**d[6]: 暴力行为通常涉及武器。
4.6.1 使用 TF-IDF 和余弦相似度
在讨论 PCA 之前,让我们看看一些更基础的文档检索技术。这些技术基于词频-逆文档频率(TF-IDF)和余弦相似度。
词频
词频(TF)定义为文档中特定词的出现次数。(在此上下文中,请注意,在这本书中,我们使用词和词有些互换。)在略微宽松的定义中,任何与词出现次数成比例的量也称为词频。例如,枪在d[0],d[6]中的 TF 为 0,在d[1]中为 1,在d[3]中为 3,等等。请注意,我们是不区分大小写的。此外,单复数(枪和guns)以及来自同一词根的各种词形(如violence和violent)通常映射到相同的词。
逆文档频率
某些词,如the,几乎出现在所有文档中。在文档检索过程中应该忽略这些词。我们如何降低它们的权重?
IDF 是通过取所有包含该词的文档的分数的倒数,然后取绝对值得到的。对于在大多数文档中出现的词,IDF 权重非常低。对于相对冷门的专业术语,IDF 权重很高。
文档特征向量
每个文档都由一个文档特征向量表示。它有与词汇表大小一样多的元素(即,所有文档中不同单词的数量)。每个单词在向量中都有一个固定的索引位置。给定一个特定的文档,对应特定单词的索引位置上的值包含该单词的 TF 乘以该单词的 IDF。因此,每个文档都是在一个具有与词汇表大小一样多维度的空间中的一个点。沿着特定维度的坐标值与单词在文档中重复的次数成正比,对于常见单词有一个降权因子。
对于像 Google 这样的现实生活中的文档检索系统,这个向量非常长。但不用担心:这个向量是概念性的——它永远不会明确存储在计算机的内存中。我们存储文档特征向量的稀疏版本:一个包含唯一单词及其 TF×IDF 得分的列表。
余弦相似度
在 2.5.6.2 节中,我们看到了两个向量之间的点积衡量了它们之间的协议。给定两个向量
和
,我们知道
⋅
= ||
|| ||
||cos(θ),其中运算符|| ⋅ ||表示向量的长度,θ是两个向量之间的角度(见图 2.7b)。当向量指向同一方向且它们之间的角度为零时,余弦值达到最大可能值,即 1。随着向量之间角度的增加,余弦值逐渐减小,直到两个向量相互垂直且余弦值为零,这意味着没有相关性:向量相互独立。
点积的大小也与两个向量的长度成正比。我们不希望使用完整的点积作为衡量向量之间相似度的标准,因为即使两个长向量方向不一致,它们也会有一个很高的相似度得分。相反,我们希望使用余弦值,其定义为

方程式 4.13
文档向量之间的余弦相似度是衡量文档之间术语共享程度的一种原则性方法。如果两个文档之间共享许多重复的单词,则余弦相似度更高。
4.6.2 隐含语义分析
余弦相似度以及类似的技术存在一个显著的缺点。为了看到这一点,检查d[5]和d[6]之间的余弦相似度。它是零。但人类显然可以看出这两个文档是相似的。
发生了什么问题?答案:我们只测量文档中术语之间的直接重叠。枪和暴力这两个词在许多其他文档中也一起出现,表明它们之间存在某种程度的相似性。因此,只包含枪的文档与只包含暴力的文档有一些相似性——但文档向量的余弦相似度并没有考虑这种次级证据。这是 LSA 试图克服的盲点。
词语的意义是通过它们所伴随的词语来认识的。也就是说,如果某些术语在许多文档中一起出现(如前例中的枪和暴力),它们很可能具有一些语义相似性。这些术语应该被分组到一个共同的语义相似术语库中。这样的库被称为主题。文档相似度应该从共同主题的角度来衡量,而不是从显式的共同术语来衡量。我们特别感兴趣的是那些能够区分我们语料库中文档的主题:也就是说,不同文档对主题的订阅程度应该有很大的差异。
几何上,一个主题是文档特征空间中的一个子空间。在经典潜在语义分析中,我们只考虑线性子空间,一个主题可以被视为文档特征空间中的一个方向或方向的线性组合(超平面)。特别是,空间中的任何方向线都是一个主题:它是一个表示坐标轴方向加权组合的子空间,这意味着它是词汇术语的加权组合。我们当然对具有高变异性的主题感兴趣。这些对应于文档向量分布良好的方向,这意味着文档向量在这个主题上得到了很好的区分。我们通常会对主题集进行剪枝,消除那些变异不足的主题。
通过这次讨论,主题的数学定义开始显现。主题是文档向量矩阵的主成分,其中每个文档描述向量沿着其行排列。以主题来衡量文档相似度的优点是,两个文档可能没有许多共同的精确单词,但它们可能仍然有一个共同的主题。这种情况发生在它们共享属于同一主题的单词时。本质上,它们共享了许多在其他文档中一起出现的单词。因此,即使共同单词的数量很少,我们也可以有很高的文档相似度。

图 4.6 展示了我们的玩具数据集d[0],⋯ d[6]的文档向量。词汇表中的每个词对应一个单独的维度。点表示文档特征向量在由术语枪(s)和暴力对应的轴形成的平面上的投影。
例如,在我们的玩具文档语料库中,枪 和 暴力 非常相关(两者或两者都不太可能在文档中出现)。枪-暴力 作为主题出现。如果我们用这个主题而不是单个单词来表达文档向量,我们会看到其他情况下可能忽略的相似性。也就是说,我们看到了 潜在语义 相似性。例如,d[5] 和 d[6] 之间的余弦相似度不为零。这是潜在语义分析的核心思想,如图 4.6 所示。
基于主题提取,让我们重新审视我们的文档检索示例。文档矩阵(以文档向量为行)看起来像表 4.1。行对应文档,列对应术语。每个单元格包含术语频率。术语 枪 和 暴力 在大多数文档中出现的次数相等,表明存在明显的相关性。因此,枪-暴力 是一个主题。主成分(右特征向量)识别主题。通常,我们省略了介词、连词、逗号等。整体步骤如下(Python 代码请见列表 4.8):
-
创建一个文档-术语 m × n 矩阵。其行对应文档(m 个文档),其列对应术语(n 个术语)。
-
对矩阵执行奇异值分解。这会产生 U、S 和 V 矩阵。V 是一个 n × n 的正交矩阵,而 S 是一个对角矩阵。
-
矩阵 V 的列产生主题。这些是 X 的行的主向量:即 X^TX 的特征向量,或者等价地,X 的协方差矩阵。
-
每个主题向量(矩阵 V 中的列)的连续元素告诉我们对应术语对该主题的贡献。每个列是 n × 1,描述了系统中 n 个术语的贡献。
-
S 的对角元素告诉我们对应主题的权重(重要性)。这些是 X^TX 的特征值:即 X 的行向量的主值。
-
检查权重,并选择一个截止值。所有低于该权重的主题都被丢弃——相应的 V 的列被丢弃。这产生了一个具有较少列但行数相同的矩阵 V;这些是我们感兴趣的主题向量。我们已经降低了问题的维度。如果保留的主题数量是 t,则减少的 V 是 m × t。
-
通过(乘以)原始文档术语矩阵 X 到这个新矩阵 V,我们得到一个 m × t 的文档主题矩阵(它具有与 X 相同的行数但较少的列)。这是 X 到主题空间的投影:即文档向量的基于主题的表示。
-
从此以后,文档主题矩阵的行将被视为文档表示。文档相似度将通过计算这些行的余弦相似度来计算,而不是原始文档词矩阵的行。这种在主题空间中的余弦相似度将捕获许多在原始输入空间中不可见的间接连接。
表 4.1 玩具示例数据集的文档矩阵
| 暴力 | 枪 | 美国 | ⋯ | 玫瑰 | |
|---|---|---|---|---|---|
| d[0] | 0 | 0 | 0 | ⋯ | 2 |
| d[1] | 1 | 1 | 1 | ⋯ | 0 |
| d[2] | 2 | 2 | 0 | ⋯ | 0 |
| d[3] | 3 | 3 | 0 | ⋯ | 0 |
| d[4] | 5 | 5 | 0 | ⋯ | 0 |
| d[5] | 0 | 1 | 0 | ⋯ | 0 |
| d[6] | 1 | 0 | 0 | ⋯ | 0 |
4.6.3 执行 LSA 的 PyTorch 代码
以下列表演示了如何从表 4.1 计算我们的玩具数据集的 LSA。本节的完整功能代码可在 mng.bz/E2Gd 找到。
列表 4.8 计算 LSA
terms = ["violence", "gun", "america", "roses"] ①
X = torch.tensor([[0, 0, 0, 2],
[1, 1, 1, 0],
[2, 2, 0, 0],
[3, 3, 0, 0],
[5, 5, 0, 0],
[0, 1, 0, 0],
[1, 0, 0, 0]]).float() ②
U, S, V_t = torch.linalg.svd(X) ③
V = V_t.T
rank = 1
U = U[:, :rank]
V = V[:, :rank] ④
topic0_term_weights = list(zip(terms, V[:, 0])) ⑤
def cosine_similarity(vec_1, vec_2):
vec_1_norm = torch.linalg.norm(vec_1)
vec_2_norm = torch.linalg.norm(vec_2)
return torch.dot(vec_1, vec_2) / (vec_1_norm * vec_2_norm)
d5_d6_cosine_similarity = cosine_similarity(X[5], X[6]) ⑥
doc_topic_projection = torch.dot(X, V)
d5_d6_lsa_similarity = cosine_similarity(doc_topic_projection[5],
doc_topic_projection[6])
① 为了简单起见,仅考虑四个术语
② 文档词矩阵。每一行描述一个文档。每一列包含一个术语的 TF 分数。为了简单起见,忽略了 IDF。
③ 对文档词矩阵执行 SVD。结果矩阵 V 的列对应于主题。这些是 X^TX 的特征向量:文档词矩阵的主向量。一个主题对应于文档特征空间中最大方差的方向。
④ S 表示主值对角矩阵。这些表示主题权重(重要性)。我们选择一个截止值并丢弃所有低于该权重的主题(降维)。仅保留 V 的前几列。此数据集的主值(主题权重)显示在输出中。本例中仅保留一个主题。
⑤ 主题向量的元素表示对应术语对主题的贡献。
⑥ 在特征空间中的余弦相似度无法捕获 d, d6 相似度。LSA 成功。
输出如下:
Principal Values from S matrix: 8.89, 2.00, 1.00, 0.99
(Topic 0 has disproportionately high weight. We discard others)
topic0_term_weights (Topic zero is about "gun" and "violence"):
[
('violence', -0.706990662151775)
('gun', -0.7069906621517749)
('america', -0.018122010384881156)
('roses', 2.9413274625621952e-18)
]
Document 5, document 6 Cosine similarity in original space: 0.0
Document 5, document 6 Cosine similarity in topic space: 1.0
4.6.4 在大型数据集上计算 LSA 和 SVD 的 PyTorch 代码
假设我们有一个包含 500 个文档的词汇表,词汇表包含 3 个术语。这是一个不切实际的短词汇表,但它使我们能够轻松地可视化文档向量空间。每个文档向量是一个 3 × 1 向量,共有 500 个这样的向量。它们共同构成一个 500 × 3 的数据矩阵 X。在此数据集中,术语 x0 和 x1 是相关的:x0 在文档中随机出现 0 到 100 次,而 x1 的出现次数是 x0 的两倍,除了小的随机波动。第三个术语的频率在 0 到 5 之间。从第 4.6 节中,我们知道 x0 和 x1 一起形成一个主题,而 x2 单独形成另一个主题。我们期望每个主题都有一个主成分。
列表 4.9 创建数据集,计算奇异值分解(SVD),绘制数据集,并显示前两个主成分。第三个奇异值相对于第一个来说很小。我们可以忽略这个维度——它对应于x0 − x1 主题中的小随机变化。奇异值被打印出来,并在图 4.7 中与数据点一起以图形方式展示。

图 4.7 潜在语义分析。请注意,垂直轴线实际上比图中看起来要小得多。
列表 4.9 使用 SVD 的 LSA
num_examples = 500
x0 = torch.normal(0, 100, (num_examples,)).round()
random_noise = torch.normal(0, 2, (num_examples,)).round()
x1 = 2*x0 + random_noise
x2 = torch.normal(0, 5, (num_examples,)).round()
X = torch.column_stack((x0, x1, x2)) ①
②
U, S, V_t = torch.linalg.svd(X) ③
V = V_t.T ③
① 3D 数据集:前两个轴是线性相关的;第三个轴有小的接近零的随机值。
② 第三个奇异值相对较小;我们忽略它
③ 前两个主向量代表主题。将数据点投影到它们上,可以得到两个主题的文档描述符。
这里是输出:
Singular values are: 4867.56982, 118.05858, 19.68604
摘要
在本章中,我们研究了机器学习和数据科学中使用的几个线性代数工具:
-
使二次型x̂^TAx̂最大(最小)的方向(单位向量)是矩阵A对应于最大(最小)特征值的特征向量。当x̂沿着这些方向时,二次型的幅度是A的最大(最小)特征值。
-
给定一个在n + 1 维空间中的点集X = {
^((0)),
^((1)),
^((2)), ⋯,
^((n))},我们可以定义均值向量和协方差矩阵为

沿任意方向(单位向量)l̂的方差是l̂ ^TCl̂。这是一个二次型。因此,多维空间中一组数据点的最大(最小)方差发生在协方差矩阵对应于最大(最小)特征值的特征向量上。这个方向被称为数据的第一主轴。后续的特征向量,按特征值递减的顺序排序,是相互正交(垂直)的,并产生后续的最大方差方向。这种技术被称为主成分分析(PCA)。
-
在许多实际情况下,较大的方差对应于数据的真实潜在模式,而较小的方差对应于噪声(如测量误差)。将数据投影到对应于较大特征值的协方差矩阵的特征轴上,可以得到相对无噪声的较低维数据。投影的数据点也与真实潜在模式更接近,从而获得更好的洞察。这被称为降维。
-
单值分解(SVD)允许我们将任意 m × n 矩阵 A 分解为三个矩阵的乘积:A = UΣV^T,其中 U 和 V 是正交矩阵,Σ 是对角矩阵。矩阵 V 的列是 A^TA 的特征向量。U 的列是 AA^T 的特征向量。Σ 的对角线元素是按降序排列的 A^TA 的特征值。
-
SVD 提供了一种数值稳定的线性方程组 A
=
的解法。特别是,对于非方阵,它提供了最接近的近似:即 A
,它最小化了 ||A
−
||。 -
给定一个数据集 X,其中行对应于个体实例的数据向量,列对应于特征值,X^TX 得到协方差矩阵。因此,X^TX 的特征向量是数据的主成分。由于 X 的奇异值分解的列向量是 X^TX 的特征向量,因此奇异值分解是计算主成分分析(PCA)的有效方法。
-
当使用机器学习数据科学进行文档检索时,词袋模型用包含文档中每个术语的词频(出现次数)的文档向量来表示文档。
-
TF-IDF 是一种用于文档匹配和检索的余弦相似度技术。
-
潜在语义分析(LSA)进行主题建模:我们对文档向量进行主成分分析以识别主题。将文档向量投影到主题轴上,使得 LSA 能够看到超越直接术语重叠的潜在(间接)相似性。
第五章:机器学习中的概率分布
本章涵盖
-
概率分布 在机器学习中的作用
-
使用二项式、多项式、分类、伯努利、贝塔和狄利克雷分布
-
熵和交叉熵在机器学习中的重要性
生活中常常需要我们估计事件发生的概率或在不确定的情况下做出决策。概率和统计学是这种情况下常用的工具箱。在机器学习中,我们将大特征向量作为输入。如前所述,我们可以将这些特征向量视为高维空间中的点。例如,尺寸为 224 × 224 的灰度图像可以视为 50,176 维空间中的点,每个像素对应一个特定的维度。具有共同特征(如动物图像)的输入将对应于该空间中点的一个簇。概率分布为分析这种在任意高维空间中松散结构化的点分布提供了有效工具。我们不仅可以简单地开发一个给定输入输出类别的机器,还可以将概率分布拟合到满足某些感兴趣特性的输入点簇(或它们的变换版本)。这通常能为我们试图解决的问题提供更多见解。
例如,假设我们正在尝试设计一个推荐系统。我们可以设计一个或多个分类器,它们会发出是否向人 Y 推荐产品 X 的是/否决策。另一方面,我们可以将概率分布拟合到特定的人群。这样做可能会导致代表不同群体的点簇之间出现显著的重叠——例如,喝黑咖啡的人和初创公司创始人。我们可能不知道解释,甚至不知道因果关系(如果有的话)的方向。但我们看到了相关性,并可能利用它设计一个更好的推荐系统。
在机器学习中,概率模型被用于处理涉及大量(可能无限多)类别的问题时。例如,假设我们正在创建一个机器,它不仅能在图像中识别猫,还能为每个像素标记其属于或不属于猫。实际上,机器将图像像素分割为前景与背景。这被称为语义分割。将这个问题视为一个分类问题是很困难的:我们通常设计一个系统,为每个像素输出一个前景的概率。
概率模型也用于无监督学习和最小监督学习:例如,在变分自编码器(VAEs)中,我们将在第十四章节中讨论。
本章介绍了概率的基本概念,并从机器学习的角度讨论了概率分布(包括多元分布),并提供了具体示例。像往常一样,我们强调多元统计的几何视图。本章的另一个同样重要的目标是使您熟悉 PyTorch distributions,这是我们在整本书中使用的 PyTorch 统计包。所有讨论的分布都附有 PyTorch distributions的代码片段。
注意:本章的完整 PyTorch 代码以完全功能性和可执行的 Jupyter 笔记本形式,可在mng .bz/8NVg找到。
5.1 概率:经典频率主义观点
考虑一个虚构的城市 Statsville。假设我们随机选择 Statsville 的一个成年居民。这个人的身高超过 6 英尺的概率是多少?小于 3 英尺的概率是多少?在 5 英尺 5 英寸到 6 英尺之间吗?这个人的体重在 50 到 70 公斤之间的概率是多少?(物理学家可能会在这里使用质量这个词,但我们选择坚持使用更常见的体重这个词)?大于 100 公斤的概率是多少?这个人的家距离市中心正好 6 公里的概率是多少?这个人的体重在 50 到 70 公斤范围内并且他们的身高在 5 英尺 5 英寸到 6 英尺范围内的概率是多少?这个人的体重大于 90 公斤并且他们的家在市中心 5 公里范围内的概率是多少?
所有这些问题都可以通过采用以下方法在频率主义范式中得到解答:
计算满足感兴趣事件标准或标准的人口规模:例如,Statsville 成年人中身高超过 6 英尺的人数。将这个数字除以总人口规模(在这里,Statsville 的成年人数量)。这就是满足该标准/标准的概率(机会)。
形式上,

公式 5.1
例如,假设这个城市有 10 万名成年人。其中,有 25,000 人身高 6 英尺或更高。那么满足感兴趣事件(即有利结果的数量)的人口规模是 25,000。总人口规模(即可能结果的数量)是 100,000。因此,

由于总人口总是任何事件所属人群的超集,所以分母总是大于或等于分子。因此,概率总是小于或等于 1。
5.1.1 随机变量
当我们谈论概率时,一个相关的问题是,“概率是什么的概率?”最简单的答案是,“事件发生的概率。”例如,在前一小节中,我们讨论了成年人 Statsville 居民身高小于 6 英尺的事件的概率。稍加思考就可以发现,事件总是对应于一个感兴趣的数值实体取特定值或位于特定值范围内的数值。这个实体被称为随机变量。例如,Statsville 成年人的身高可以是一个随机变量,我们可以谈论它小于 6 英尺的概率;Statsville 成年人的体重也可以是一个随机变量,我们可以谈论它小于 60 公斤的概率。在预测股市表现时,道琼斯指数可能是一个随机变量:我们可以谈论这个随机变量超过 19,000 点的概率。在讨论病毒的传播时,感染人数的总量可能是一个随机变量,我们可以谈论它小于 2,000 人的概率,等等。
随机变量的定义特征是每个允许的值(或值的范围)都与一个概率相关联(即随机变量取该值或值范围的概率)。例如,对于 Statsville 的成年人,我们可以允许只有三个体重范围:S[1],小于 60 公斤;S[2],介于 60 至 90 公斤之间;和S[3],大于 90 公斤。然后我们可以有一个相应的随机变量 X 来表示量化体重。它可以取三个值之一:X = 1(对应于 S[1] 中的体重),X = 2(对应于 S[2] 中的体重),或 X = 3(对应于 S[3] 中的体重)。每个值都伴随着一个固定的概率:例如,在 5.1 节 5.1 的例子中,p(X = 1) = 0.25,p(X = 2) = 0.5,p(X = 3) = 0.25,分别。这样的随机变量,其值来自可数集合,被称为离散随机变量。
随机变量也可以是连续的。对于连续随机变量 X,我们将其值在一个无限小的范围内与一个概率相关联,p(x ≤ X < x + δx),其中 δx → 0。这被称为概率密度,在 5.6 节 5.6 中有更详细的解释。
注意:在这本书中,我们总是使用大写字母来表示随机变量。通常,相同的小写字母表示随机变量的一个特定值:例如,p(X = x)表示随机变量 X 取值 x 的概率,而 p(X∈{x, x + δx}) 表示随机变量 X 取值在 x 和 x + δx 之间的概率。此外,请注意,有时我们使用字母 X 来表示一个数据集。这种流行但容易混淆的惯例在文献中很常见——通常,从上下文中可以明显看出其用法。
5.1.2 总体直方图
直方图帮助我们可视化离散随机变量。让我们继续以 Statsville 为例。我们只对 Statsville 成年人的三个体重范围感兴趣:S[1]: 低于 60 公斤;S[2]: 介于 60 至 90 公斤之间;以及S[3]: 超过 90 公斤。假设 Statsville 成年人这些体重范围内的数量如表 5.1 所示。
表 5.1 Statsville 城市成年人重量的频率表
| S[1]: 低于 60 公斤 | S[2]: 介于 60 至 90 公斤之间 | S[3]: 超过 90 公斤 |
|---|---|---|
| 25,000 | 50,000 | 25,000 |
同样的信息可以通过图 5.1 所示的直方图来可视化。直方图的 X 轴对应于第 5.1.1 节中离散随机变量的可能值。Y 轴显示了相应体重范围内的总体规模。有 25,000 人在 S[1]范围内,50,000 人在 S[2]范围内,25,000 人在 S[3]范围内。这些类别共同构成了 Statsville 的整个成年人口——每个成年人属于一个或另一个类别。这可以通过将所有类别的 Y 轴值相加来验证:25,000 + 50,000 + 25,000 = 100,000,这是 Statsville 的成年人口。
5.2 概率分布
图 5.1 及其等价表格 5.1 可以很容易地转换为概率,如表 5.2 所示。该表显示了代表随机选择的 Statsville 成年居民量化体重的离散随机变量 X 的允许值的概率。表 5.2 代表形式上所知的概率分布:一个数学函数,它接受一个随机变量作为输入,并输出它取任何允许值的概率。它必须在随机变量的所有可能值上定义。
注意,集合{S[1],S[2],S[3]}是穷尽的,因为在某种意义上,所有可能的 X 值都属于其中一个范围——我们不可能有一个不属于这些范围的权重。在集合论术语中,这些范围的并集,S[1] ∪ S[2] ∪ S[3],覆盖了一个包含整个总体(所有可能的 X 值)的空间。

图 5.1 描述 Statsville 成年人重量的直方图,对应表 5.1
表 5.2 Statsville 成年人量化体重的概率分布
| S[1]: 低于 60 公斤 | S[2]: 介于 60 至 90 公斤之间 | S[3]: 超过 90 公斤 |
|---|---|---|
| p(X = 1) = 25,000/100,000 = 0.25 | p(X = 2) = 50,000/100,000 = 0.5 | p(X = 3) = 25,000/100,000 = 0.25 |
备注:集合论运算符∪表示集合的并。
范围也是互斥的,因为任何给定的 X 观测只能属于一个范围,永远不可能更多。在集合论术语中,任何一对范围的交集为零:S[1] ∩ S[2] = S[1] ∩ S[3] = S[2] ∩ S[3] = ϕ。
备注:集合论运算符∩表示集合交集。
对于一组穷尽且互斥的事件,给出这些事件概率的函数称为概率分布。例如,我们这个微小例子中的概率分布包括三个概率:P(X = 1) = 0.25,P(X = 2) = 0.5,和 P(X = 3) = 0.25。这如图 5.2 所示,这是一个三点图。

图 5.2 Statsville 成年人重量的概率分布图,对应于表 5.2。事件 E[1] ≡ X = 1 ⟹ 重量在 S[1] 范围内,事件 E[2] ≡ X = 2 ⟹ 重量在 S[2] 范围内,事件 E[3] ≡ x = 3 ⟹ 重量在 S[3] 范围内。
5.3 概率论的基本概念
在本节中,我们简要地涉及了不可能事件和必然事件;穷尽且互斥事件的概率之和;以及独立事件。
5.3.1 不可能事件和必然事件的概率
不可能事件的概率为零(例如,太阳从西边升起的概率)。必然发生的事件的概率为 1(例如,太阳从东边升起的概率)。像作者在网球比赛中击败罗杰·费德勒这样的不可能事件具有低但非零的概率,例如 0.001。高度可能的事件(例如罗杰·费德勒在网球比赛中击败作者)的概率接近但并不完全等于 1,例如 0.999。
5.3.2 穷尽且互斥事件
考虑事件 E[1]、E[2]、E[3],它们对应于 Statsville 成年人从 5.2 节中量化的体重,分别属于 S[1]、S[2]、或 S[3] 范围,或者说,E[1] 是对应于 X = 1 的事件,E[2] 是对应于 X = 2 的事件,E[3] 是对应于 X = 3 的事件。这些事件是穷尽的:它们的并集覆盖了整个总体空间。这意味着 Statsville 成年人的所有量化体重都属于 S[1]、S[2]、或 S[3] 中的一个范围。这些事件也是互斥的:它们的交集为零,意味着总体中的任何成员不能属于多个范围。例如,如果一个重量属于 S[1],它就不能属于 S[2] 或 S[3]。对于这类事件,以下成立:
互斥事件的概率之和给出了它们中任何一个发生的概率。
例如,对于事件 E[1]、E[2]、E[3],
p(E[1] 或 E[2]) = p(E[1]) + p(E[2])
方程式 5.2
求和法则指出,
穷尽且互斥事件概率之和总是 1。
例如,
p(E[1]) + p(E[2]) + p(E[3]) = p(E[1] or E[2] or E[3]) = 1
这在直观上是显而易见的。我们只是在说,我们可以肯定地说,E[1] 或 E[2] 或 E[3] 中至少有一个会发生。
一般而言,给定一组穷尽且互斥的事件 E[1], E[2], ⋯, E[n],

方程 5.3
5.3.3 独立事件
考虑两个事件 E[1] ≡ “Statsville 成年居民的体重小于 60 公斤” 和 G[1] ≡ “Statsville 成年居民的住宅距离市中心 5 公里以内”。这些事件之间没有任何影响。知道人口中某个成员体重小于 60 公斤并不能揭示他们住宅距离市中心的距离,反之亦然。我们说 E[1] 和 G[1] 是 独立事件。正式地,
如果一个事件的发生不影响另一个事件发生的概率,那么这组事件是独立的。
5.4 联合概率及其分布
给定一个 Statsville 的成年居民,让 E[1] 仍然是他们体重小于 60 公斤的事件。相应概率是 p(E[1])。同样,让 G[1] 是他们住宅距离市中心小于 5 公里的事件。相应概率是 p(G[1])。现在考虑一个居民体重小于 60 公斤且住宅距离市中心小于 5 公里的概率。这个概率,表示为 p(E[1], G[1]),被称为联合概率。正式地,
一组事件的联合概率是所有这些事件同时发生的概率。
乘法法则指出,独立事件的联合概率可以通过乘以它们的单个概率来获得。因此,对于当前示例,p(E[1], G[1]) = p(E[1])p(G[1])。
让我们用一个稍微更复杂的例子继续我们的联合概率讨论。为了方便查阅,我们将权重类别、对应人口和概率分布汇总在表 5.3 中。同样,我们将居民住宅距离市中心距离量化为三个范围:D[1] ≡ 小于 5 公里,D[2] ≡ 5 到 15 公里之间,D[3] ≡ 大于 15 公里。表 5.4 显示了相应的人口和概率分布。事件 {E[1], E[2], E[3]} 和 {G[1], G[2], G[3]} 的联合概率分布显示在表 5.5 中。
表 5.3 Statsville 成年居民体重的人口概率分布表。E[1],E[2],E[3] 是穷尽且互斥的事件,p(E[1]) + p(E[2]) + p(E[3]) = 1。
| 低于 60 公斤(范围 S[1]) | 60 到 90 公斤(范围 S[2]) | 高于 90 公斤(范围 S[3]) |
|---|---|---|
| 事件 E[1] ≡ 重量 ∈ S[1] | 事件 E[2] ≡ 重量 ∈ S[2] | 事件 E[3] ≡ 重量 ∈ S[3] |
| 人口规模 = 25,000 | 人口规模 = 50,000 | 人口规模 = 25,000 |
| p(S[1]) = 25,000/100,000 = 0.25 | p(S[2]) = 50,000/100,000 = 0.5 | p(S[3]) = 25,000/100,000 = 0.25 |
表 5.4 统计城居民家庭距离市中心距离的人口概率分布表。G[1],G[2],G[3] 是穷尽且互斥的事件,p(G[1]) + p(G[2]) + p(G[3]) = 1。
| 小于 5 公里(范围 D[1]) | 5 至 15 公里(范围 D[2]) | 大于 15 公里(范围 D[3]) |
|---|---|---|
| 事件 G[1] ≡ 距离 ∈ D[1] | 事件 G[2] ≡ 距离 ∈ D[2] | 事件 G[3] ≡ 距离 ∈ D[3] |
| 人口规模 = 20,000 | 人口规模 = 60,000 | 人口规模 = 20,000 |
| p(G[1]) = 20,000/100,000 = 0.20 | p(G[1]) = 60,000/100,000 = 0.6 | p(G[1]) = 20,000/100,000 = 0.20 |
表 5.5 独立事件的联合概率分布。表中所有元素的总和为 1。
| 小于 60 公斤 (E[1]) | 60 至 90 公斤 (E[2]) | 超过 90 公斤 (E[3]) | |
|---|---|---|---|
| --- | --- | --- | |
| 小于 5 公里 (G[1]) | p(E[1], G[1])= 0.25 × 0.2= 0.05 | p(E[2], G[1])= 0.5 × 0.2= 0.1 | p(E[3], G[1])= 0.25 × 0.2= 0.05 |
| 5 至 15 公里之间 (G[2]) | p(E[1], G[2])= 0.25 × 0.6= 0.15 | p(E[2], G[2])= 0.5 × 0.6= 0.3 | p(E[3], G[2])= 0.25 × 0.6= 0.15 |
| 超过 15 公里 (G[3]) | p(E[1], G[3])= 0.25 × 0.2= 0.05 | p(E[2], G[3])= 0.5 × 0.2= 0.1 | p(E[3], G[3])= 0.25 × 0.2= 0.05 |
关于表 5.5 我们可以做出以下陈述:
-
表 5.5 中所有元素的总和为 1。换句话说,p(E[i], G[j]) 是一个合适的概率分布,表示事件 E[i] 和事件 G[j] 同时发生的概率:在这里,(i, j) ∈ {1,2,3} × {1,2,3}。
-
p(E[i], G[j]) = p(E[i])p(G[j]) ∀(i, j) ∈ {1,2,3} × {1,2,3}。这是因为事件是独立的。
注意:符号 × 表示 笛卡尔积。集合 {1,2,3} × {1,2,3} 的笛卡尔积是集合 {(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)}。符号 ∀ 表示“对于所有”。读作 ∀(i, j) ∈ {1,2,3} × {1,2,3},即对于笛卡尔积中的所有对 (i, j)。
通常,给定一组独立事件 E[1],E[2],⋯,E[n],所有事件同时发生的联合概率 p(E[1], E[2],⋯, E[n]) 是它们各自发生概率的乘积:

方程式 5.4
注意:符号 ∏ 表示“乘积”。
5.4.1 边缘概率
假设我们没有个体概率 p(E[i]) 和 p(G[j])。我们拥有的只是联合概率分布:即表 5.5。我们能从这些中找到个体概率吗?如果能,那么如何?
为了回答这个问题,考虑表 5.5 中的特定行或列——比如说,最上面的行。在这一行中,E 的值遍历所有可能性(E 的整个空间),但 G 固定在 G[1]。如果 G[1] 要发生,只有三种可能性:它与 E[1],E[2],或 E[3] 同时发生。相应的联合概率是 p(E[1], G[1]),p(E[2], G[1]),和 p(E[3], G[1])。如果我们把它们加起来,我们得到 G[1] 与 E[1] 或 E[2] 或 E[3] 同时发生的概率:即事件 (E[1], G[1]) 或 (E[2], G[1]) 或 (E[3], G[1])。因此,我们已经考虑了 G[1] 可以发生的所有情况。总和代表了事件 G[1] 发生的概率。因此,p(G[1]) 可以通过将对应于 G[1] 的行的所有概率相加,并在边缘写下它来获得(这就是为什么它被称为边缘概率)。同样,通过将中间列的所有概率相加,我们获得 p(E[2]) 的概率,等等。表 5.6 显示了带有边缘概率的更新后的表 5.5。
表 5.6 显示边缘概率的联合概率分布
| 小于 60 公斤 (E[1]) | 60 至 90 公斤之间 (E[2]) | 超过 90 公斤 (E[3]) | G 的边缘概率 | |
|---|---|---|---|---|
| 小于 5 公里 (G[1]) | p(E[1], G[1])= 0.25 × 0.2= 0.05 | p(E[2], G[1])= 0.5 × 0.2= 0.1 | p(E[3], G[1])= 0.25 × 0.2= 0.05 | p(G[1])= 0.05 + 0.1 + 0.05= 0.2 |
| 5 至 15 公里之间 (G[2]) | p(E[1], G[2])= 0.25 × 0.6= 0.15 | p(E[2], G[2])= 0.5 × 0.6= 0.3 | p(E[3], G[2])= 0.25 × 0.6= 0.15 | p(G[2])= 0.15 + 0.3 + 0.15= 0.6 |
| 超过 15 公里 (G[3]) | p(E[1], G[3])= 0.25 × 0.2= 0.05 | p(E[2], G[3])= 0.5 × 0.2= 0.1 | p(E[3], G[3])= 0.25 × 0.2= 0.05 | p(G[3])= 0.05 + 0.1 + 0.05= 0.2 |
| E 的边缘概率 | p(E[1])= 0.05 + 0.15 + 0.05= 0.25 | p(E[2])= 0.1 + 0.3 + 0.1= 0.5 | p(E[3])= 0.05 + 0.15 += 0.25 |
通常,给定一组穷尽且互斥的事件 E[1],E[2],⋯,E[n],另一个事件 G,以及联合概率 p(E[1], G), p(E[2], G), ⋯, p(E[n], G),

方程式 5.5
通过对所有可能的 E[i] 值求和,我们消除了 E。这是因为 E 是互斥且穷尽的;对它们的求和会导致一个被消去的特定事件(记住,特定事件的概率是 1)。
5.4.2 依赖事件及其联合概率分布
到目前为止,我们共同考虑的事件是“体重”和“居民家离市中心距离”。它们是相互独立的——它们的联合是各自概率的乘积。现在,让我们讨论一个不同的情况,其中变量是相互关联的,知道一个变量的值确实有助于我们预测另一个变量。例如,Statsville 成年居民的体重和身高不是独立的:通常,身高较高的人体重较重,反之亦然。
如同往常,我们用一个玩具例子来理解这个概念。我们将身高量化为三个范围,H[1] ≡ 低于 5 英尺 5 英寸,H[2] ≡ 介于 5 英尺 5 英寸和 6 英尺之间,以及 H[3] ≡ 超过 6 英尺。令 z 为对应身高的随机变量。关于身高,我们有三种可能的事件:F[1] ≡ z ∈ H[1],F[2] ≡ z ∈ H[2],和 F[3] ≡ z ∈ H[3]。身高和体重的联合概率分布显示在表 5.7 中。
表 5.7 相关事件的联合概率分布
| 低于 60 公斤 (E[1]) | 60 至 90 公斤 (E[2]) | 超过 90 公斤 (E[3]) | |
|---|---|---|---|
| 低于 5 英尺 5 英寸 (F[1])** | p(E[1], F[1]) = 0.25 | p(E[2], F[1]) = 0 | p(E[3], F[1]) = 0 |
| 介于 5 英尺 5 英寸和 6 英尺之间 (F[2]) | p(E[1], F[2]) = 0. | p(E[2], F[2]) = 0.5 | p(E[3], F[2]) = 0 |
| 超过 6 英尺 (F[3]). | p(E[1], F[3]) = 0 | p(E[2], F[3]) = 0 | p(E[3], F[3]) = 0.25 |
注意以下关于表 5.7 的内容:
-
表 5.7 中所有元素的总和为 1。换句话说,p(E[i], F[j]) 是一个合适的概率分布,表示事件 E[i] 和事件 F[j] 同时发生的概率。这里 (i, j) ∈ {1,2,3} × {1,2,3}。
-
p(E[i], F[j]) = 0 if i ≠ j ∀(i, j) ∈ {1,2,3} × {1,2,3}。这实际上意味着事件是完全相关的:E[1] 的发生意味着 F[1] 的发生,反之亦然,E[2] 的发生意味着 F[2] 的发生,反之亦然,E[3] 的发生意味着 F[3] 的发生,反之亦然。换句话说,Statsville 每个体重低于 60 公斤的成年居民身高也低于 5 英尺 5 英寸,等等。(在现实生活中,这种完美的相关性很少存在;但 Statsville 是一个神话般的小镇。)
5.5 几何视图:相关变量和独立变量的样本点分布
让我们看看与表 5.5 和 5.7 对应的点分布的图形视图。独立变量和相关变量的点分布看起来有根本性的不同;这与主成分分析 (PCA) 和降维有关,我们已在第 4.4 节中讨论过。
我们使用基于矩形桶的技术来可视化二维离散事件的联合。例如,我们有三个与权重相关的事件,E[1]、E[2]、E[3],以及三个与距离相关的事件,G[1]、G[2]、G[3]。因此,联合分布有 3 × 3 = 9 种可能的事件(E[i],G[j]),∀(i, j) ∈ {1,2,3} × {1,2,3},如表 5.5 所示。这九个事件中的每一个都由一个小矩形(联合事件的桶)表示;总共,我们有一个 3 × 3 的矩形桶网格。为了可视化样本点分布,我们从联合分布中抽取了 1,000 个样本。联合事件样本放置在其桶内的随机位置(即,桶内的所有点被选中的概率相等)。请注意,点在概率高的桶内集中,反之亦然。

(a)

(b)
图 5.3 联合概率分布的图形可视化。矩形代表不同离散事件的桶。(a)来自表 5.5 的独立事件)。所有九个事件的概率都不是微不足道的,所有九个矩形都有相对较高的样本点集中度。不适合 PCA。(b)来自表 5.7(非独立事件)。事件(E[1],F[1])、(E[2],F[2])和(E[3],F[3])的概率非常高,而其他事件的概率可以忽略不计。样本点集中在对角线上的矩形上。适合 PCA。
独立表 5.5 和非独立表 5.7 的联合变量对的点分布图分别显示在图 5.3a 和 5.3b 中。我们看到,独立事件的样本点分布在整个域上分布得相对对称,而相关事件的样本点分布则围绕特定的线条(在这种情况下,对角线)较窄。这在一般情况下以及更高维的情况下都成立。你应该对独立与非独立点分布有这种心理图景。如果我们采样独立事件(不相关),所有可能的事件组合(如{E[1], G[1]},{E[1], G[2]},{E[1], G[3]},⋯,{E[3], G[3]})都有非微不足道的发生概率(见表 5.5),这相当于说,没有任何事件有非常高的发生概率记住,概率总和为 1,所以如果某些事件有非常低的概率[接近零],其他事件必须有高概率[接近一]来补偿)。这阻止了点在空间的小区域内集中。所有桶都将有许多点。换句话说,独立事件的联合概率样本在整个种群空间中扩散(例如,见图 5.3a)。
另一方面,如果事件是相关的,联合概率样本将集中在联合空间中某些高概率区域。例如,在表 5.7 中,事件(E[1], F[1]),(E[2], F[2]),(E[3], F[3])比其他组合更有可能。因此,样本点集中在相应的对角线上(见图 5.3b)。
如果这没有让你想起 PCA(第 4.4 节),你应该重新阅读那一节。如图 5.3a 所示的相关事件是降维的良好候选:两个维度本质上携带相同的信息,如果我们知道其中一个,我们就可以推导出另一个。我们可以去掉一个高度相关的维度,而不会丢失显著的信息。
5.6 连续随机变量和概率密度
到目前为止,我们已经将随机变量量化并使其离散化。例如,体重已经被量化为三个区间——小于 60 公斤、60 至 90 公斤之间,以及大于 90 公斤——并且每个区间都分配了概率。如果我们想了解更细粒度的概率,比如 0 至 10 公斤、10 至 20 公斤、20 至 30 公斤等等,怎么办?嗯,我们必须创建更多的区间。每个区间覆盖的值范围更窄(人口空间中更小的一部分),但数量更多。在所有情况下,遵循频率主义方法,我们计算每个区间中成年 Statsville 人的数量,将其除以总人口规模,并将这个值称为属于该区间的概率。
如果我们想要更细的粒度呢?我们创建更多的区间,每个区间覆盖的人口空间部分更小。在极限情况下,我们有一个无限多的区间,每个区间覆盖的人口空间部分无限小。它们共同覆盖了人口空间——大量非常小的部分可以覆盖任意区域。在这个极限情况下,概率分布函数被称为概率密度函数。正式地,
对于连续随机变量 X 的概率密度函数 p(x) 定义为 X 位于 x 和 x + δx 之间的概率,其中 δx → 0
p(x) = lim[δx→0] 概率(x ≤ X < x + δx)
备注:有一点遗憾的是,随机变量 X 的典型符号与数据集(数据向量的集合)的符号也相同,即 X。但通常情况下,上下文足以区分它们。
这里有一点理论上的细微差别。我们说的是 p(x) 是随机变量 X 位于 x 和 x + δx 之间的概率。这并不完全等同于说 p(x) 是 X 等于 x 的概率。但由于 δx 无穷小,它们实际上是相同的东西。
考虑事件集 E = lim[δx→0] {x ≤ X < x + δx} 对所有可能的 x 值。所有可能的 x 值范围从负无穷大到无穷大:x ∈ [−∞,∞]。存在无限多个这样的事件,每个事件都非常窄,但共同覆盖了整个域 x ∈ [−∞,∞]。换句话说,它们是穷尽的。它们也是互斥的,因为 x 不能同时属于它们中的多个。它们是之前看到的离散事件 E[1]、E[2]、E[3] 的连续对应物。
在连续空间中,事件集 E = lim[δx → 0]{x ≤ X < x + δx} 的穷尽性和互斥性意味着我们可以应用方程式 5.3,但求和将被积分所取代,因为变量是连续的。
连续域中的求和规则表示为

方程式 5.6
方程 5.6 是方程 5.3 的连续对应物。在物理上意味着我们可以肯定地说 x 位于区间 −∞ 到 ∞ 的某个地方。
随机变量也可以是多维的(即,一个向量)。那么概率密度函数表示为 p(
)。
连续多维概率密度函数的求和法则为

方程 5.7
其中 D 是
的定义域——即包含向量
所有可能值的空间。
例如,二维向量
的定义域是 XY 平面。请注意,方程 5.7 中的积分是一个 多维 积分(例如,对于二维
,它是 ∬![∈D] p(
) d
= 1)。
注意:为了符号的简洁性,我们通常用一个单独的积分符号来表示多维积分。定义域中的向量符号(例如,
∈ D),以及 d
中的向量符号,都表示多个维度。
你可能还记得从初等积分微积分中,方程 5.6 对应于 p(x)(或 p(
)) 曲线下的面积。在更高维的情况下,方程 5.7 对应于 p(
) 超曲面下的体积。因此,单变量概率密度曲线下的总面积总是 1。在更高维的情况下,多变量概率密度函数的超曲面下的体积总是 1。
5.7 分布的性质:期望值、方差和协方差
在本章的开头,我们提到生成式机器学习模型通常是通过将一个已知家族的分布拟合到可用训练数据来开发的。因此,我们从已知家族中假设一个参数化分布,并估计最适合训练数据的精确参数。大多数分布家族都是用诸如均值、方差等直观属性来参数化的。理解这些概念及其几何意义对于理解基于它们的模型至关重要。
在本节中,我们解释了所有分布都通用的几个属性/参数。稍后,当我们讨论单个分布时,我们将它们与这些分布的参数联系起来。我们还展示了如何通过 PyTorch distributions 包编程地获取每个单个分布的这些值。
5.7.1 期望值(又称均值)
如果我们多次从给定的分布中抽取随机变量并取抽样值的平均值,我们期望得到什么值?平均值将更接近概率较高的值(因为这些在抽样中出现得更频繁)。如果我们抽样足够多次,对于给定的概率分布,这个平均值总是稳定到一个固定值,即该分布的期望值。
形式上,
给定一个离散分布 D,其中离散随机变量 X 可以从集合 {x[1], x[2],⋯, x[n]} 中取任何值,相应的概率为 {p(x[1]), p(x[2])⋯, p(x[n])},期望值由以下公式给出

公式 5.8
其中 x[k] → D 表示从分布 D 中抽取的第 k 个样本。总体而言,方程 5.8 表示“从分布中抽取的大量样本的平均值或期望值趋近于所有可能样本值的概率加权和”。当我们抽样时,高概率值比低概率值出现得更频繁,所以大量样本的平均值被拉向高概率值。
对于多维随机变量:
给定一个离散分布,其中离散的多维随机变量 X 可以从集合 {
[1],
[2],⋯,
[n]} 中取任何值,相应的概率为 {p(
[1]), p(
[2]),⋯, p(
[n])},期望值由以下公式给出

公式 5.9
对于连续随机变量(注意求和被替换为积分):
对于取值从 −∞ 到 ∞ 的连续随机变量 X 的期望值是

公式 5.10
物理学中的期望值和质心
在物理学中,我们有质量中心或形心的概念。如果我们有一组具有质量的点,整个点集可以被一个单独的点所代替。这个点被称为形心。形心的位置是各个点位置的加权平均值,权重为它们的各自质量。如果我们把单个点的概率视为质量,那么统计学中的期望值概念对应于物理学中的形心概念。
随机变量任意函数的期望值
到目前为止,我们已经看到了随机变量本身的期望值。这个概念可以扩展到随机变量的函数。
随机变量函数的期望值是该函数在随机变量所有可能值处的概率加权和。形式上,

方程 5.11
期望值和点积
在方程 2.6 中,我们研究了两个向量之间的点积。进一步地,在第 2.5.6.2 节中,我们看到两个向量之间的点积衡量了这两个向量之间的协议。如果它们指向同一方向,点积就更大。在本节中,我们展示了随机变量的函数期望值可以看作是表示概率的向量和表示函数本身的向量的点积。
首先,让我们考虑离散情况。我们的随机变量可以取值 x[i],i ∈ {1, n}。现在,想象一个向量
和一个向量
。从方程 5.11 中,我们看到随机变量 X 的函数期望值 𝔼(f(X)) 与
^T
=
⋅
相同。当
和
对齐时,这个值会很高;因此,当随机变量的高函数值与高概率相一致时,随机变量的函数期望值就很高,反之亦然。在连续情况下,这些向量有无限多个分量,求和被积分所取代,但基本思想保持不变。
随机变量线性组合的期望值
期望值是一个线性算子。这意味着随机变量线性组合的期望值是随机变量期望值的线性组合(具有相同的权重)。形式上,
𝔼(α[1]X[1] + α[2]X[2] ⋯ α[n]X[n]) = α[1]𝔼(X[1]) + α[2]𝔼(X[2]) + ⋯ α[n]𝔼(X[n])
方程 5.12
5.7.2 方差、协方差和标准差
当我们从给定的点分布中抽取大量样本时,我们通常想知道点集的分布范围。分布范围不仅仅是测量分布中两点之间最大距离的问题。相反,我们想知道点是如何紧密排列的。如果大多数点都适合在一个非常小的球内,那么即使一个或两个点离球很远,我们也称之为 小分布 或 高包装密度。
这在机器学习中为什么很重要?让我们从几个非正式的例子开始。如果我们发现点在单个点周围的一个小区域内紧密排列,我们可能想要用那个点替换整个分布,而不会造成很大的误差。或者如果点围绕一条直线紧密排列,我们可以用那条直线替换整个分布。这样做给我们提供了一个更简单的低维表示,并且通常会导致对数据的理解更加容易把握整体。这是因为围绕特定点或方向的微小变化通常是由噪声引起的,而大的变化是由有意义的事物引起的。通过消除小的变化并关注大的变化,我们捕捉到了主要的信息内容。(这可能是为什么老年人倾向于更好地形成整体观点的原因:也许他们头部的神经元太少,无法保留他们多年来积累的大量记忆数据。他们的大脑执行降维。)这是主成分分析(PCA)和降维的基本思想,我们在第 4.4 节中看到了这一点。
方差——或者其平方根,标准差——衡量分布中的点围绕期望值的密集程度:即点分布的分散程度。形式上,概率分布的方差定义为如下:

方程 5.13
通过比较方程 5.13 与方程 5.10 和 5.11,我们看到方差是样本点x与均值μ的距离(x − μ)²的期望值。所以如果更可能的(更频繁出现的)样本点位于均值附近的一个短距离内,方差就小,反之亦然。也就是说,方差衡量了点围绕均值的密集程度。
协方差:高维度的方差
将期望值的观念从单变量情况扩展到多变量情况是直接的。在单变量情况下,我们取一个标量量ξ的概率加权平均值。得到的期望值是一个标量,μ = ∫[ξ = −∞]^∞ ξ p(ξ)dξ。在多变量情况下,我们取一个向量量
的概率加权平均值。得到的期望值是一个向量,
= ∫![∈D]
p(
)d
.
将方差的观念扩展到多元情况并不简单。这是因为我们可以以无限多种可能的方向遍历多维随机向量的域(向量定义的空间)——想想在二维平面上我们可以走多少种可能的方向——并且每个方向上的分布或包装密度可能不同。例如,在图 5.3b 中,沿主对角线的分布范围远大于垂直方向的分布范围。
多维点分布的协方差是一个矩阵,它使我们能够轻松地测量任何所需方向上的分布或包装密度。它还使我们能够轻松地找出最大分布发生方向及其分布情况。
考虑一个取向量值的多元随机变量 X,其值为
。设 l̂ 为一个任意方向(如往常一样,我们使用顶部的帽子表示表示方向的单位长度向量)来测量 X 的包装密度。我们在第 2.5.2 节和 2.5.6 节中讨论了在方向 l̂ 上
的点积(即
^Tl̂) 是 x 沿 l̂ 的投影或分量(有效值)。因此,随机向量
在方向 l̂ 上的分布或包装密度与点积(也称为分量或投影)l̂^T
的分布相同。这个投影 l̂^T
是一个标量量:我们可以使用单变量公式来测量其方差。
注意:在此上下文中,我们可以将
^Tl̂ 和 l̂^T
互换使用。点积是对称的。
投影的期望值是

方差由

现在,由于标量的转置与标量本身相同,我们可以将积分内的平方项写成标量 l̂^T(
-
) 和其转置的乘积:

使用方程 2.10,

由于

其中

方程 5.14
为了简化,我们省略括号中的 X,并简单地写成 ℂ(X) 为 ℂ。将一个取向量值的 d-维随机变量 X 的协方差矩阵视为如下是等效的:

方程 5.15
其中

是随机向量
的第 i 个和第 j 个维度的协方差。
ℂ(X) 或 ℂ 是随机变量 X 的 协方差矩阵。稍加思考就可以发现方程 5.14 和 5.15 是等价的。
以下事项值得关注:
-
从方程 5.14 可以看出,ℂ 是由 d × 1 向量 (
−
) 和它们的转置 (
−
)^T,1 × d 向量的乘积之和。因此,ℂ 是一个 d × d 矩阵。 -
这个矩阵与我们在测量方差或扩散方向 l̂ 无关。我们可以预先计算 ℂ;当我们需要测量任何方向 l̂ 的方差时,我们可以评估二次型 l̂^T ℂl̂ 来获得该方向的方差。因此,ℂ 是分布的通用属性,就像
。ℂ 被称为分布的 协方差。 -
协方差是多变量方差的单变量对应物。
通过比较方程 5.13 和 5.14 的表达式,可以明显看出协方差是多变量方差的类比。
方差和期望值
如前所述,方差是样本点 x 与均值 μ 的距离 (x − μ)² 的期望值。通过比较方程 5.13,5.10 和 5.11,可以很容易地看出这一点,并导致以下公式(我们使用线性组合期望值原理):
var(X) = 𝔼((X − μ)²) = 𝔼(X²) − 𝔼(2μX) + 𝔼(μ²)
由于 μ 是一个常数,我们可以将其从期望值中提取出来(这是线性组合期望值原理的一个特例)。因此,我们得到
var(X) = 𝔼(X²) − 2μ𝔼(X) + μ²𝔼(1)
但 μ = 𝔼(X). 此外,常数的期望值就是该常数。因此,𝔼(1) = 1。
因此,
var(X) = 𝔼(X²) − 2μ² + μ²𝔼(1) = 𝔼(X²) − μ²
或者
var(X) = 𝔼(X²) − 𝔼(X)²
方程 5.16
5.8 从分布中进行抽样
从随机变量的概率分布中抽取样本得到的是可能值集合中的一个任意值。如果我们抽取许多样本,高概率值出现的次数会比低概率值多。抽取的点在可能值的域中形成一个云,概率较高的区域比低概率区域更密集。换句话说,在样本点云中,高概率值被过度表示。因此,一组样本点通常被称为样本点云。当然,我们希望样本点云是整个群体的良好表示,这样分析云中的点就能对整个群体有洞察。在单变量情况下,样本值是一个标量,在数轴上表示为一个点。在多变量情况下,样本值是一个向量,在更高维度的空间中表示为一个点。
计算汇总统计量(如均值和方差)通常很有用,以描述群体。如果我们知道一个分布,我们可以使用闭式表达式来获得这些属性。许多标准分布及其均值和方差的闭式方程在 5.9 节中讨论。但通常,我们不知道潜在的分布。在这种情况下,可以使用样本均值和样本方差。给定一组n个样本X =
[1],
[2]⋯
[n]来自任何分布,样本均值和方差的计算如下

在某些情况下,例如高斯分布(我们将在稍后讨论),可以从理论上证明样本均值和方差是最佳的(给定样本数据对真实均值和方差的最好猜测)。此外,随着样本数量的增加,样本均值接近真实均值,并且随着样本数量的增加,我们得到对真实均值的良好近似。在下一小节中,我们将了解更多关于“足够”是多少的信息。
大数定律:需要多少样本才算足够?
非正式地说,大数定律表明,如果我们从一个概率分布中抽取大量样本值,它们的平均值应该接近分布的期望值。在极限情况下,无限多个样本的平均值将匹配均值。
在实践中,我们无法抽取无限数量的样本,因此不能保证样本均值会与期望值(真实均值)在现实生活中的抽样中一致。但如果样本数量很大,它们不会相差太远。这不仅仅是一个理论问题。赌场设计游戏,其中赌场赢得赌注的概率略高于客人赢得赌注的概率。结果的期望值是赌场赢而不是客人。在赌场进行的非常大量的赌注中,这正是发生的事情——这也是为什么赌场总体上赚钱,尽管他们可能会输掉个别赌注。
“大量样本”是多少个样本?这并没有一个精确的定义。但有一点是明确的:如果方差较大,就需要抽取更多的样本来使大数定律适用。
让我们用一个例子来说明这一点。考虑一个赌注游戏。假设著名的足球俱乐部巴塞罗那,出于未知的原因,同意与硅谷的机器学习专家足球俱乐部进行大量比赛。我们可以对一支球队下注 100 美元。如果该队获胜,我们将收回 200 美元:也就是说,我们赚了 100 美元。如果该队输掉比赛,我们将输掉赌注:也就是说,我们亏了 100 美元。这个赌注游戏发生在一个没有人了解这些俱乐部声誉的国家。一个赌徒在第一场比赛中下注巴塞罗那并赢得了 100 美元。基于这一观察,赌徒能否说通过下注巴塞罗那,他们每次都期望赢得 100 美元?显然不能。
但假设赌徒下了 100 次赌注,赢了 99 次 100 美元,输了 1 次 100 美元。现在赌徒可以有些信心地预期,通过下注巴塞罗那,他们将赢得 100 美元(或接近这个数字)。基于这些观察,对巴塞罗那投注的样本平均收益为 0.99 × (100) + 0.01 × (−100) = 98。样本标准差为√(.99 × (98 - 100)² + 0.01 × (98 - (-100))²) = 19.8997。相对于样本均值,样本标准差为 19.8997/98 = 0.203。
接下来,考虑同样的比赛,但现在巴塞罗那足球俱乐部正在对阵皇家马德里足球俱乐部。由于两队实力相当(巴塞罗那的理论胜率为 0.5),结果不再是单方面的。假设经过 100 场比赛,巴塞罗那赢得了 60 场,皇家马德里赢得了 40 场。对巴塞罗那投注的样本平均收益为 0.6 × (100) + 0.4 × (−100) = 20。样本标准差为√(.6 × (20 - 100)² + 0.4 × (20 - (-100))²) = 97.9795。相对于样本均值,样本标准差为 97.9795/20 = 4.89897。这个数字比之前的 0.203 大得多。在这种情况下,即使经过 100 次试验,赌徒也不能非常自信地预测预期的胜利是样本均值,即 20。
整体直觉如下:
如果我们取足够大的样本数量,它们的平均值将接近期望值。构成“足够大的”样本数量的确切定义尚不清楚。然而,方差(相对于均值)越大,所需的样本数量就越多。
5.9 一些著名的概率分布
在本节中,我们介绍了一些在深度学习中经常使用的概率分布和密度函数。我们将使用 PyTorch 代码片段来演示如何为每个分布设置、采样以及计算期望值、方差/协方差等属性。请注意以下内容:
-
在代码片段中,对于每个分布,我们使用
-
PyTorch
distributions函数调用 -
从公式中直接评估(为了理解数学)
这两者应该得到相同的结果。在实际应用中,你应该使用 PyTorch 的
distributions函数调用而不是原始公式。 -
-
在代码片段中,对于每个分布,
-
我们使用 PyTorch 的
distributions函数调用评估理论均值和方差。 -
我们评估样本均值和方差。
当样本集足够大时,样本均值和理论均值应该很接近。方差也是如此。
-
注意:这些分布的完整功能代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/8NVg 找到。
另一点需要记住的是:在机器学习中,我们经常处理概率的对数。由于流行的分布是指数分布,这导致计算更加简单。有了这个,让我们深入了解概率分布。
5.9.1 均匀随机分布
考虑一个连续随机变量 x,它可以取一个固定紧凑范围内的任何值,例如 [a,b],以相等的概率,而 x 取值在范围之外的几率是零。相应的 p(x) 是一个均匀概率分布。形式上表述为,

方程式 5.17
方程式 5.17 表示 p(x) 是常数,为 1/(b-a),当 x 在 a 和 b 之间时,对于其他 x 的值则为零。注意常数的选择是如何巧妙地使得曲线下的总面积为 1。此方程在图 5.4 中以图形方式表示,列表 5.1 展示了 PyTorch 代码用于计算单变量均匀随机分布的对数概率。

图 5.4 单变量(单变量)均匀随机概率密度函数。概率 p(x) 在区间 [−10,10] 内是常数,0.05,在其他所有地方都是零。因此,它描绘了方程 5.17 中 b = 10,a = −10 的情况。曲线下的面积是宽度为 20、高度为 0.05 的阴影矩形的面积,20 × 0.05 = 1。细长的矩形表示对应于事件 E = {x ≤ X < x + δx} 的无穷小区间。如果我们从这个分布中抽取一个随机样本 x,那么样本值在 4 和 4 + δx 之间的概率,当 δx → 0 时,是 p(4) = 0.05。样本值在 15 和 15 + δx 之间的概率,当 δx → 0 时,是 p(15) = 0。
列表 5.1 单变量均匀随机分布的对数概率
from torch.distributions import Uniform ①
a = torch.tensor([1.0], dtype=torch.float) ②
b = torch.tensor([5.0], dtype=torch.float)
ufm_dist = Uniform(a, b) ③
X = torch.tensor([2.0], dtype=torch.float) ④
def raw_eval(X, a, b):
return torch.log(1 / (b - a))
log_prob = ufm_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, a, b) ⑥
assert torch.isclose(log_prob, raw_eval_log_prob, atol=1e-4) ⑦
① 导入 PyTorch 均匀分布
② 设置分布参数
③ 实例化一个均匀分布对象
④ 实例化一个单点测试数据集
⑤ 使用 PyTorch 评估概率
⑥ 使用公式评估概率
⑦ 断言概率匹配
注意:完全功能的均匀分布代码,可通过 Jupyter Notebook 执行,可在mng.bz/E2Jr找到。
均匀分布的期望值
我们这样做是为了单变量情况,尽管计算可以很容易地扩展到多变量情况。将方程 5.17 中的概率密度函数代入连续变量的期望值表达式,方程 5.10,

方程 5.18
注意:积分的极限改变了,因为 p(x) 在区间 [a, b] 外部为零。
总体而言,方程 5.18 与我们的直觉相符。期望值正好位于均匀区间的中间,如图 5.5 所示。
均匀分布的方差
如果我们看图 5.5,直观上很明显,样本的填充密度与矩形的宽度有关。宽度越小,填充越紧密,方差越小,反之亦然。让我们看看数学是否支持这种直觉:

方程 5.19

图 5.5 单变量(单变量)均匀随机概率密度函数。中间的实线表示期望值。交互式可视化(您可以更改参数并观察图形如何随之变化)可在mng.bz/E2Jr找到。
图 5.5 显示,方程 5.19 中的方差与矩形的宽度平方成正比:即,(b − a)²。
这里是 PyTorch 代码,用于计算均匀随机分布的均值和方差。
列表 5.2 均匀随机分布的均值和方差
num_samples = 100000 ①
②
samples = ufm_dist.sample([num_samples]) ③
sample_mean = samples.mean() ④
dist_mean = ufm_dist.mean ⑤
assert torch.isclose(sample_mean, dist_mean, atol=0.2)
sample_var = ufm_dist.sample([num_samples]).var() ⑥
dist_var = ufm_dist.variance ⑦
assert torch.isclose(sample_var, dist_var, atol=0.2)
① 样本点数量
② 100000 × 1
③ 从在列表 5.1 中实例化的 ufm_dist 获取样本
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
多元均匀分布
均匀分布也可以是多元的。在这种情况下,随机变量是一个向量,
而不是一个单一值,而是一系列值)。其域是一个多维体积,而不是 X 轴,并且图形具有超过两个维度。例如,这是一个双变量均匀随机分布:

方程 5.20
在这里,(x,
) ∈ [a[1], b[1]] × [a[2], b[2]] 表示在二维 XY 平面上的一个矩形区域,其中 x 在 a[1] 和 b[1] 之间,而
在 a[2] 和 b[2] 之间。方程 5.20 在图 5.6 中以图形方式展示。在一般的多维情况下,:

方程 5.21

图 5.6 双变量均匀随机概率密度 在域 (x, y) ∈ [−10,10] × [−10,10] 内,概率 p(x, y) 是常数,0.0025,而在区间之外的所有地方都是零。宽度为 20 × 20、高度为 0.0025 的盒子的体积为 20 * 20 * 0.0025 = 1。
在这里,V 是以 D 为底的超维盒子的体积。方程 5.21 表示 p(
) 在域 D 内对
是常数,而对于其他 x 的值则是零。当不为零时,它有一个常数值,即体积 V 的倒数:这使得密度函数下的总体积为 1。
5.9.2 高斯(正态)分布
这可能是世界上最著名的分布。让我们再次考虑 Statsville 成年居民的体重。如果 Statsville 像真正的城市一样,最可能的体重是大约 75 公斤:最大比例的人口将会有这个体重。接近这个值(比如 70 或 80 公斤)的体重也很可能,尽管比 75 公斤的可能性略低。远离 75 公斤的体重仍然不太可能,以此类推。我们离 75 公斤越远,具有该体重的百分比人口就越低。像 40 和 110 公斤这样的异常值不太可能。非正式地说,高斯概率密度函数看起来像一条钟形曲线。中心值具有最高的概率。当我们远离中心时,概率逐渐下降。然而,在理论上,它永远不会完全消失(函数p(x)永远不会等于 0),尽管对于所有实际目的来说,它几乎为零。这种行为在数学上被描述为渐近趋于零。图 5.7 显示了高斯概率密度函数。正式地,

方程式 5.22

图 5.7 一元高斯随机概率密度函数,μ = 0 和 σ = 4。钟形曲线在中心最高,当我们远离中心时,曲线越来越低,渐近接近零。x = 0 的值具有最高的概率,对应于概率密度函数的中心。请注意,曲线是对称的。因此,例如,随机样本在-5 附近的概率与 5 的概率相同(0.04):即p(-5)= p(5)= 0.04。一个交互式可视化(您可以更改参数并观察图形如何因此变化)可以在mng.bz/NYJX找到。
在这里,μ和σ是参数;μ对应于中心(例如,在图 5.7 中,μ = 0)。参数σ控制钟形的宽度。较大的σ意味着当我们远离中心时,p(x)下降得更慢。
高斯(正态)概率密度函数非常流行,以至于我们有一个特殊的符号表示它:𝒩(x, μ, σ²)。可以证明(但这非常繁琐,所以我们在这里省略证明):

这确立了𝒩(x;μ, σ²)是一个真正的概率(满足方程 5.7 中的求和规则)。
列表 5.3 一元正态分布的对数概率
from torch.distributions import Normal ①
mu = torch.tensor([0.0], dtype=torch.float) ②
sigma = torch.tensor([5.0], dtype=torch.float)
uvn_dist = Normal(mu, sigma) ③
X = torch.tensor([0.0], dtype=torch.float) ④
def raw_eval(X, mu, sigma):
K = 1 / (math.sqrt(2 * math.pi) * sigma)
E = math.exp( -1 * (X - mu) ** 2 * (1 / (2 * sigma ** 2)))
return math.log(K * E)
log_prob = uvn_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, mu, sigma) ⑥
assert log_prob == raw_eval_log_prob ⑦
① 导入 PyTorch 一元正态分布
② 设置分布参数
③ 实例化一个一元正态分布对象
④ 实例化一个单点测试数据集
⑤ 使用 PyTorch 评估概率
⑥ 使用公式评估概率
⑦ 断言概率匹配
注意:此正态分布的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/NYJX 找到。
多变量高斯
高斯分布也可以是多变量的。然后随机变量 x 是一个向量
,如通常情况。参数 μ 也变成一个向量
,参数 σ 变成矩阵 Σ。与单变量情况一样,这些参数与期望值和方差相关。高斯多变量概率分布函数是

方程式 5.23
方程 5.23 描述了随机向量
落在以点
为中心、尺寸为 δ
的无穷小体积内的概率密度函数。(想象一个边长为 δ
的微小长方体,其顶点在
。)向量
和矩阵 Σ 是参数。与单变量情况一样,
对应于随机向量的最可能值。图 5.8 展示了三维空间中两个变量的高斯正态分布)的形状由参数 Σ 控制。
列表 5.4 多变量正态分布的对数概率
from torch.distributions import MultivariateNormal ①
mu = torch.tensor([0.0, 0.0], dtype=torch.float) ②
C = torch.tensor([[5.0, 0.0], [0.0, 5.0]], dtype=torch.float)
mvn_dist = MultivariateNormal(mu, C) ③
X = torch.tensor([0.0, 0.0], dtype=torch.float) ④
def raw_eval(X, mu, C):
K = (1 / (2 * math.pi * math.sqrt(C.det())))
X_minus_mu = (X - mu).reshape(-1, 1)
E1 = torch.matmul(X_minus_mu.T, C.inverse())
E = math.exp(-1 / 2\. * torch.matmul(E1, X_minus_mu))
return math.log(K * E)
log_prob = mvn_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, mu, C) ⑥
assert log_prob == raw_eval_log_prob ⑦
① 导入 PyTorch 多变量正态分布
② 设置分布参数
③ 实例化一个多变量正态分布对象
④ 实例化一个单点测试数据集
⑤ 使用 PyTorch 评估概率
⑥ 使用公式评估概率
⑦ 断言概率匹配

图 5.8 双变量高斯随机概率密度函数。它是一个钟形表面:在中心最高,随着我们远离中心而降低,趋于零。x = 0,
= 0 的概率最高,对应于概率密度函数的中心。钟形有一个圆形底部,Σ 矩阵是单位矩阵 𝕀 的标量倍。一个交互式可视化(你可以更改参数并观察图形如何随之变化)可在 mng.bz/NYJX 找到。
高斯分布的期望值
将方程 5.22 中的概率密度函数代入连续变量的期望值表达式,即方程 5.10,我们得到

代入 

将 u = y² 和方程 5.6 代入

注意到第一个项中的积分的极限是相同的。这是因为无论 y → ∞ 还是 y → −∞,u = y² →∞。但是具有相同上下限的积分是零。因此,第一个项是零。因此,
𝔼[高斯](X) = μ
方程 5.24
直观上看,这是完全合理的。概率密度
在 x = μ 处达到峰值(最大化)。在这个 x 处,指数变为零,使得
达到其最大可能值 1。这正好在钟形曲线的中间,如图 5.10 所示。当然,如果密度是对称的并且在中间达到峰值,期望值与中间值相同。类似地,在多元情况下,多维高斯随机变量 X 在 d-维域 ℝ^d(即
∈ ℝ^d)中取向量值
,其期望值为
𝔼[高斯](X) = 
方程 5.25

图 5.9 单变量(单变量)正态(高斯)随机概率密度函数,μ = 0 和 σ = 4。中间的实线表示期望值。
高斯分布的方差
通过将方程 5.22 代入方程 5.13 的积分形式来获得高斯分布的方差。数学推导在书的附录中展示;这里我们只陈述结果。
高斯分布的概率密度函数
的方差是 σ²,标准差是它的平方根(σ)。这在直观上是合理的。σ 出现在概率密度函数
表达式中的负指数的分母中。因此,p(x) 是 σ 的增函数:也就是说,对于给定的 x 和 μ,较大的 σ 意味着较大的 p(x)。换句话说,较大的 σ 意味着当我们远离中心时,概率衰减得更慢:更宽的钟形曲线,更大的分散,因此更大的方差。图 5.10 展示了这一点。

(a) 不同的 μ 但相同的 σ。

(b) 相同的 μ 但不同的 σ。
图 5.10 变化的 μ 和 σ 的高斯密度。改变 μ 会移动曲线的中心。较大的 σ(方差)意味着更宽的钟形曲线⇒更分散。请注意,由于曲线下的总面积必须为 1,因此较宽的曲线高度较小。
列表 5.5 单变量高斯的均值和方差
num_samples = 100000 ①
②
samples = uvn_dist.sample([num_samples]) ③
sample_mean = samples.mean() ④
dist_mean = uvn_dist.mean ⑤
assert torch.isclose(sample_mean, dist_mean, atol=0.1)
sample_var = uvn_dist.sample([num_samples]).var() ⑥
dist_var = uvn_dist.variance ⑦
assert torch.isclose(sample_var, dist_var, atol=0.1)
① 样本点数量
② 100000 × 1
③ 从 uvn_dist 获取样本
在列表 5.3 中实例化
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
多元高斯分布的协方差和钟形表面的几何形状
将单变量高斯概率密度方程 5.22 与多元高斯概率密度方程 5.23 进行比较,我们直观地感觉到矩阵 Σ 是单变量方差 σ² 的多元对应物。确实如此。形式上,对于在方程 5.23 中给出的多元高斯随机变量概率分布,协方差矩阵由以下方程给出
ℂ[gaussian](X) = Σ
方程 5.26
如表 5.11 所示,Σ 规定了钟形概率密度函数底部的形状。
很容易看出,方程 5.23 中的指数是一个二次型(在第 4.2 节中介绍)。因此,它定义了一个超椭圆,如图 5.11 和第 2.17 节所示。所有二次型和超椭圆的性质都适用于这里。
列表 5.6 多元正态分布的均值和方差
num_samples = 100000 ①
②
samples = mvn_dist.sample([num_samples]) ③
sample_mean = samples.mean() ④
dist_mean = mvn_dist.mean ⑤
assert torch.allclose(sample_mean, dist_mean, atol=1e-1)
sample_var = mvn_dist.sample([num_samples]).var() ⑥
dist_var = mvn_dist.variance ⑦
assert torch.allclose(sample_var, dist_var, atol=1e-1)
① 样本点数量
② 100000 × 1
③ 从 uvn_dist 获取样本
在列表 5.4 中实例化
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
让我们看看高斯协方差矩阵 Σ 的几何性质。考虑方程 5.23 的二维版本。我们重写
和
——两个二维向量。还有
——一个 2 × 2 矩阵。方程 5.23 的概率密度函数变为

方程 5.27
(使用你在第三章中学到的知识,让自己确信方程 5.27 是方程 5.23 的二维类似物。)
如果我们将表面 p(x, y) 对 (x, y) 进行绘图,在三维空间中看起来像一个钟形。钟形底部的形状,在 (x, y) 平面上,由 2 × 2 矩阵 Σ 控制。特别是,
-
如果 Σ 是一个对角矩阵,其对角线元素相等,那么钟形在所有方向上都是对称的,其底部是圆形的。
-
如果 Σ 是一个对角矩阵,其对角线元素不相等,那么钟形曲线的底部是椭圆形的。椭圆的轴与坐标轴对齐。
-
对于一个一般的 Σ 矩阵,钟形底部的形状是椭圆形的。椭圆的轴不一定与坐标轴对齐。
-
Σ 的特征向量给出了钟形表面椭圆底部的轴线。
现在,如果我们从方程 5.27 中采样分布,我们将在图 5.8 所示表面的基平面上得到一组点 (x, y)。表面在点 (x, y) 处的 z 坐标(表示 p(x, y))越高,它被选中的概率就越大。如果我们绘制大量样本,相应的点云将或多或少地类似于钟形表面的底部。
图 5.11 展示了由不同协方差矩阵 Σ 生成的各种点云。将其与图 5.10 进行比较。
样本点云的几何:协方差和最大或最小分散度的方向
我们已经看到,如果一个多元分布有一个协方差矩阵 ℂ,它在任何特定方向 l̂ 上的方差(分散度)是 l̂^T ℂl̂。那么最大分散度的方向是什么?
提出这个问题等同于问“哪个方向 l̂ 最大化了二次型 l̂^T ℂl̂?”在 4.2 节中,我们了解到,当方向 l̂ 与矩阵 ℂ 的最大或最小特征值对应的特征向量对齐时,这种二次型会被最大化或最小化。因此,分布的最大分散度发生在协方差矩阵对应最大特征值的特征向量上。这导致了 4.4 节中的 PCA 技术。
接下来,我们将讨论高斯分布的协方差以及由多次采样多元高斯分布形成的点云的几何形状。你可能想看看图 5.11,它展示了由不同协方差矩阵 Σ 生成的各种点云。

(a) 

(b) 

(c) 

(d) 
图 5.11 由具有相同的
= [0,0]^T但不同的Σs 采样的多元高斯分布形成的点云。这些点云对应于多元高斯概率密度的钟形曲线的底部。除了(a)之外的所有点云在旋转后可以替换为一个一元高斯分布,以使坐标轴与Σ的特征向量对齐(降维)。有关详细信息,请参阅第 4.4、4.5 和 4.6 节。可以在mng.bz/NYJX找到钟形曲线底部的交互式等高线图。
多元高斯点云和超椭圆
方程 5.23 中指数项的分子 (
−
)*T***Σ**(−1)(
−
),正如我们在第 4.2 节中讨论的那样,是一个二次型。这也应该让你想起我们在第 2.17 节中看到的超椭圆,方程 2.33 和方程 4.1。
现在考虑p(
)与
的图像。这是n + 1 维空间中的一个超曲面,其中随机变量
是n-维的。例如,如果随机高斯变量
是 2D 的,那么(
, p(
))在 3D 中的图像如图 5.8 所示。这是一个钟形表面。方程 5.23 中概率密度函数分子对应的二次型决定了这个钟形底部的形状和大小。
如果矩阵Σ是对角线(具有相等的对角线元素),则底部是圆形的——这是图 5.8 中显示的特殊情况。否则,钟形的底部是椭圆形的。协方差矩阵Σ的特征向量对应于椭圆形底部的轴的方向。特征值对应于轴的长度。
5.9.3 二项分布
假设我们有一个包含人物照片的数据库。此外,假设我们知道其中 20%的照片包含名人,而剩余的 80%则不包含。如果我们从这个数据库中随机选取三张照片,那么其中两张包含名人的概率是多少?这正是二项分布所处理的问题类型。
在以计算机视觉为中心的机器学习设置中,我们可能会检查选定的照片并尝试预测它们是否包含名人。但到目前为止,让我们将注意力限制在更简单的任务上,即盲目地从汇总统计数据中预测概率。
如果我们选择一张照片,它包含名人的概率是π = 0.2。
NOTE 这与表示圆周与直径比的自然数π无关。我们只是遵循流行惯例重新使用符号π。
照片不包含名人的概率是 1 − π = 0.8。从那可以计算出,例如,前两张样本照片包含名人但最后一张包含非名人的概率:即事件{S, S, F}(其中 S 表示找到名人的成功,F 表示找到名人的失败)。使用方程 5.4,事件{S, S, F}的概率是π × π × (1−π) = 0.2 × 0.2 × 0.8。然而,还有许多其他可能的组合。
在三次试验中可能出现的所有组合都显示在表 5.8 中。在表中,事件 ID 3、5 和 6 对应于两次成功和一次失败。它们发生的概率分别为 0.8 × 0.2 × 0.2、0.2 × 0.8 × 0.2 和 0.2 × 0.2 × 0.8。如果其中任何一个发生,我们在三次试验中就有两张名人照片。因此,使用方程 5.3,在三次试验中选出两张名人照片的整体概率是这些事件概率的总和:0.8 × 0.2 × 0.2 + 0.2 × 0.8 × 0.2 + 0.2 × 0.2 × 0.8 = 0.096。
表 5.8 三次试验的所有可能组合
| 事件 ID | 事件 | 概率 |
|---|---|---|
| 0 | {F, F, F} | (1−π) × (1−π) × (1−π) = 0.8 × 0.8 × 0.8 |
| 1 | {F, F, S} | (1−π) × (1−π) × π = 0.8 × 0.8 × 0.2 |
| 2 | {F, S, F} | (1−π) × π × (1−π) = 0.8 × 0.2 × 0.8 |
| 3 | {F, S, S} | (1−π) × π × π = 0.8 × 0.2 × 0.2 |
| 4 | {S, F, F} | π × (1−π) × (1−π) = 0.2 × 0.8 × 0.8 |
| 5 | {S, F, S} | π × (1−π) × π = 0.2 × 0.8 × 0.2 |
| 6 | {S, S, F} | π × π × (1−π) = 0.2 × 0.2 × 0.8 |
| 7 | {S, S, S} | π × π × π = 0.2 × 0.2 × 0.2 |
在一般情况下,当试验次数超过三次时,列举所有可能的成功和失败组合将变得极其繁琐,这些组合可能发生在一组n次试验中。幸运的是,我们可以推导出一个公式。但在做那之前,让我们用更广泛的方式来表述二项分布的任务:
给定一个过程,在任意一次试验中都有二元结果(成功或失败),并且给定成功的概率在试验中是一个已知的常数(比如说,π),二项分布处理的是在n次试验中观察到k次成功的概率。
想象有 n 个连续事件,其中每个单独的事件可以是 S 或 F。表 5.8 显示了 n = 3 的此类事件。每个项目有两个可能值 (S 或 F),并且有 n 个项目。因此,总共可以有 2 × 2 × ⋯2 = 2^n 个可能的事件。
我们只对发生 k 次事件 S(因此发生 (n − k) 次事件 F)的事件感兴趣。在 n 个事件中有多少是这样的?好吧,问这个问题等同于问有多少种方式可以从总共 n 个可能的槽位中选择 k 个槽位。以另一种方式提出相同的问题可以是,“有多少种不同的 n 个项目的排列方式,其中每个项目是 S 或 F,且 S 的总数是 k?” 从组合理论中,答案是

每个事件都有 π^k × (1−π)^(n − k) 的概率。因此,在 n 次试验中成功 k 次的整体概率是
。
形式上,如果 X 是一个表示 n 次试验中成功次数的随机变量,且每次试验成功的概率是某个常数 π,

方程 5.28
k 可以取哪些值?当然,在 n 次试验中,我们不可能有超过 n 次的成功;因此,k 的最大可能值是 n。所有介于 0 和 n 之间的整数值都是可能的:

右侧是 (a + b)^n 的著名二项式展开的通项公式,其中 a = π,b = 1 − π。因此,我们得到

方程 5.29
这与直觉相符,因为给定 n,k 只能取 0,1,⋯,n 的值;方程 5.29 左侧概率之和对应于某个概率为 1 的特定事件。
此外,将 n = 3,k = 2,和 π = 0.2 代入方程 5.28 得到 3!/2!1! (0.2)², (0.8)^(3-2) = 0.096:这正是我们从显式枚举中得到的结果。
列表 5.7 二项式分布的对数概率
from torch.distributions import Binomial ①
num_trials = 3 ②
p = torch.tensor([0.2], dtype=torch.float)
binom_dist = Binomial(num_trials, probs=p) ③
X = torch.tensor([1], dtype=torch.float) ④
def nCk(n, k):
f = math.factorial
return f(n) * 1\. / (f(k) * f(n-k))
def raw_eval(X, n, p):
result = nCk(n, X) * (p ** X) * (1 - p) ** (n - X)
return torch.log(result)
log_prob = binom_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, num_trials, p) ⑥
assert torch.isclose(log_prob, raw_eval_log_prob, atol=1e-4) ⑦
导入 PyTorch 二项式分布
设置分布参数
实例化一个二项式分布对象
实例化一个单点测试数据集
使用 PyTorch 评估概率
使用公式评估概率
断言概率匹配
注意:二项式分布的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/DRJ0 找到。
二项式分布的期望值
我们已经看到,二项式分布处理一个随机变量 X,它描述了 n 次试验中的成功次数,其中每次试验成功的概率是一个常数 π(再次强调,这与表示圆周与直径比值的 π 没有关系)。这个 X 可以取任何介于 0 到 n 之间的整数值。因此,

我们可以省略第一个项,它有乘数 k = 0。因此我们得到

我们可以分解 n! = n(n − 1)! 和 π^k = π π^(k − 1)。同时,n − k = (n − 1) − (k − 1)。这给我们

用 j 替换 k - 1,用 m 替换 n - 1,我们得到

方程式 5.30
求和式中的量与方程 5.29 中的量类似(应求和为 1)。这使我们得到
𝔼[binomial] (X) = nπ
方程式 5.31
方程式 5.31 表示,如果 π 是单次试验成功的概率,那么在 n 次试验中成功的期望次数是 n**π。例如,如果单次试验成功的概率是 0.2,那么 100 次试验中成功的期望次数是 20——这几乎是直观的。
二项分布的方差
描述在 n 次试验中成功的次数的二项随机变量的方差,其中每次试验成功的概率是一个常数 π 是
var[binomial] = nπ (1 − π)
方程式 5.32
证明遵循与期望值相同的步骤。
列表 5.8 二项分布的均值和方差
num_samples = 100000 ①
②
samples = binom_dist.sample([num_samples]) ③
sample_mean = samples.mean() ④
dist_mean = binom_dist.mean ⑤
assert torch.isclose(sample_mean, dist_mean, atol=0.2)
sample_var = binom_dist.sample([num_samples]).var() ⑥
dist_var = binom_dist.variance ⑦
assert torch.isclose(sample_var, dist_var, atol=0.2)
① 样本点数量
② 100000 × 1
③ 从在列表 5.7 中实例化的 ufm_dist 获取样本
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
5.9.4 多项分布
再次考虑我们在 5.9.3 节中讨论的例子问题。我们有一个人的照片数据库。但不是两个类别,名人和非名人,我们有四个类别:
-
阿尔伯特·爱因斯坦的照片(第 1 类):照片的 10%
-
居里夫人的照片(第 2 类):照片的 42%
-
高斯的照片(第 3 类):照片的 4%
-
其他照片(第 4 类):照片的 44%
如果我们从数据库中随机选择一张照片(即进行随机试验),
-
选择第 1 类(选择爱因斯坦照片)的概率是 π[1] = 0.1。
-
选择第 2 类(选择居里夫人照片)的概率是 π[2] = 0.42。
-
选择第 3 类(选择高斯照片)的概率是 π[3] = 0.04。
-
选择第 4 类(选择上述任一照片)的概率是 π[4] = 0.44。
注意到 π[1] + π[2] + π[3] + π[4] = 1。这是因为这些类别是相互排斥且穷尽的,所以每个试验中恰好有一个这样的类别发生。
基于所有这些,让我们提出问题:“在 10 次随机试验中,第 1 类发生 1 次,第 2 类发生 2 次,第 3 类发生 1 次,第 4 类发生剩余的 6 次,这种概率是多少?”这就是多项分布处理的问题。
形式上,
-
设 C[1],C[2],⋯,C[m] 是一组 m 个类别,使得在任何随机试验中,恰好选择这些类别中的一个,相应的概率为 π[1],π[2],⋯,π[m]。
-
设 X[1],X[2],⋯,X[m] 为一组随机变量。X[i] 对应于在 n 次试验中类别 C[i] 发生的次数。
-
然后是描述选择类别 C[1] 被选中 k[1] 次,类别 C[2] 被选中 k[2] 次,类别 C[3] 被选中 k[m] 次的多项式概率函数是

方程 5.33
其中

我们可以验证,对于 m = 2,这变成了二项分布(方程 5.28)。一个值得注意的点是,如果我们单独查看 m 个变量 X[1],X[2],⋯,X[m] 中的任何一个,它的分布是二项分布。
让我们计算我们开始时的示例的最终概率:在一个包含 10 次随机试验的集合中,类别 1 发生 1 次,类别 2 发生 2 次,类别 3 发生 1 次,类别 4 发生剩余的 6 次。这是

列表 5.9 多项分布的对数概率
from torch.distributions import Multinomial ①
num_trials = 10 ②
P = torch.tensor([0.1, 0.42, 0.04, 0.44], dtype=torch.float)
multinom_dist = Multinomial(num_trials, probs=P) ③
X = torch.tensor([1, 2, 1, 6], dtype=torch.float) ④
def raw_eval(X, n, P):
f = math.factorial
result = f(n)
for p, x in zip(P, X):
result *= (p ** x) / f(x)
return math.log(result)
log_prob = multinom_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, num_trials, P) ⑥
assert torch.isclose(log_prob, raw_eval_log_prob, atol=1e-4) ⑦
① 导入 PyTorch 多项分布
② 设置分布参数
③ 实例化多项分布对象
④ 实例化单个测试数据集
⑤ 使用 PyTorch 评估概率
⑥ 使用公式评估概率
⑦ 断言概率匹配
注意:完全功能的关于多项分布的代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/l1gz 找到。
多项分布的期望值
每个随机变量 X[1],X[2],⋯,X[m] 分别服从二项分布。因此,根据方程 5.31 中的二项分布期望值公式,
𝔼[multinomial](X[i]) = nπ[i]
方程 5.34
多项分布的方差
随机变量 X[1],X[2],⋯,X[m] 的变化,根据方程 5.32 中的二项分布方差公式,是
var[multinomial](X[i]) = nπ[i](1−π[i])
方程 5.35
如果每个 X[1],X[2],⋯,X[m] 是一个标量,那么我们可以将一个随机向量
考虑在内。这样一个随机变量的期望值是

并且协方差是

方程 5.36
其中对角线项类似于二项方差 σ[ii] = nπ[i](1−π[i]) ∀i ∈ [1, m] 和非对角线项是 σ[ij] = −nπ[i]π[j] ∀(i, j) ∈ [1, m] × [1, m]。对角线中的交叉协方差项是负的,因为一个元素的增加意味着其他元素的减少。
列表 5.10 多项分布的均值和方差
num_samples = 100000 ①
②
samples = multinom_dist.sample([num_samples]) ③
sample_mean = samples.mean(axis=0) ④
dist_mean = multinom_dist.mean ⑤
assert torch.allclose(sample_mean, dist_mean, atol=0.2)
sample_var = multinom_dist.sample([num_samples]).var(axis=0) ⑥
dist_var = multinom_dist.variance ⑦
assert torch.allclose(sample_var, dist_var, atol=0.2)
① 样本点数
② 100000 × 1
③ 从列表 5.9 中实例化的 ufm_dist 获取样本
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
5.9.5 伯努利分布
伯努利分布是二项分布的一种特殊情况,其中 n = 1:也就是说,只进行一次成功或失败试验。成功的概率是 π,失败的概率是 1 − π。
换句话说,让 X 是一个离散随机变量,以概率 π 取值为 1(成功),以概率 1 − π 取值为 0(失败)。X 的分布是伯努利分布:
p(X = 1) = π
p(X = 0) = 1 - π
列表 5.11 伯努利分布的对数概率
from torch.distributions import Bernoulli ①
p = torch.tensor([0.3], dtype=torch.float) ②
bern_dist = Bernoulli(p) ③
X = torch.tensor([1], dtype=torch.float) ④
def raw_eval(X, p):
prob = p if X == 1 else 1-p
return math.log(prob)
log_prob = bern_dist.log_prob(X) ⑤
raw_eval_log_prob = raw_eval(X, p) ⑥
assert torch.isclose(log_prob, raw_eval_log_prob, atol=1e-4) ⑦
① 导入 PyTorch 伯努利分布
② 设置分布参数
③ 实例化伯努利分布对象
④ 实例化一个单点测试数据集
⑤ 使用 PyTorch 计算概率
⑥ 使用公式计算概率
⑦ 断言概率匹配
注意:完全功能的伯努利分布代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/BRwq 找到。
伯努利分布的期望值
如果只有两个类别,成功 和 失败,我们无法直接谈论期望值。如果我们进行,比如说,100 次试验,得到 30 成功 和 70 失败,平均是 0.3 成功,这不是一个有效结果。在这个二元系统中,我们不能有分数的 成功 或 失败。
然而,如果我们引入一个人工构造,我们仍然可以讨论伯努利分布的期望值。我们给这些二元实体分配数值:成功 = 1 和 失败 = 0。那么 X 的期望值是

方程式 5.37
伯努利分布的方差
同样,如果我们给这些二元实体分配数值—成功 = 1 和 失败 = 0—伯努利分布的方差是

方程式 5.38
列表 5.12 伯努利分布的均值和方差
num_samples = 100000 ①
②
samples = bern_dist.sample([num_samples]) ③
sample_mean = samples.mean() ④
dist_mean = bern_dist.mean ⑤
assert torch.isclose(sample_mean, dist_mean, atol=0.2)
sample_var = bern_dist.sample([num_samples]).var() ⑥
dist_var = bern_dist.variance ⑦
assert torch.isclose(sample_var, dist_var, atol=0.2)
① 样本点数量
② 100000 × 1
③ 从列表 5.11 中实例化的 ufm_dist 获取样本
④ 样本均值
⑤ 通过 PyTorch 函数计算均值
⑥ 样本方差
⑦ 通过 PyTorch 函数计算方差
5.9.6 分类分布和 one-hot 向量
再次考虑第 5.9.4 节中引入的示例问题。我们有一个包含四个类别照片的数据库:
-
阿尔伯特·爱因斯坦的照片(类别 1):10%
-
玛丽·居里的照片(类别 2):42%
-
卡尔·弗里德里希·高斯的照片(类别 3):4%
-
其他照片(类别 4):44%
如果我们从数据库中随机选择一张照片,
-
选择类别 1 的概率是 π[1] = 0.1。
-
选择类别 2 的概率是 π[2] = 0.42。
-
选择类别 3 的概率是 π[3] = 0.04。
-
选择类别 4 的概率是 π[4] = 0.44。
如前所述,π[1] + π[2] + π[3] + π[4] = 1,因为类别是互斥且穷尽的,所以每个试验中恰好只有一个类别发生。
在多项分布中,我们进行了n次试验,询问每个特定类别会发生多少次。如果我们只进行一次试验呢?那么我们就得到了类别分布。
类别分布是多项分布的特殊情况(试验次数n = 1)。它也是伯努利分布的扩展,其中我们不仅仅有两个类别,即成功和失败,我们可以有任意数量的类别。
形式上,
-
设 C[1], C[2], ⋯, C[m] 是一组m个类别,在任意随机试验中,恰好选择这些类别中的一个,相应的概率为π[1], π[2], ⋯, π[m]。我们有时将所有类别的概率一起称为向量
![eq_05-38-a2.png]()
-
设 X[1], X[2], ⋯, X[m] 是一组随机变量。X[i]对应于在n次试验中类别C[i]出现的次数。
-
然后,类别概率函数描述了在单次试验中每个类别C[1], C[2],等等的概率。
独热向量
我们可以使用独热向量紧凑地表达类别分布的单次试验结果。这是一个有m个元素的向量。恰好有一个元素是 1;所有其他元素都是 0。1 表示在特定试验中发生了m个可能类别中的哪一个。例如,在照片数据库的例子中,如果某个特定的试验中出现了一张玛丽·居里的照片,相应的独热向量是
。
类别分布的概率
我们可以将独热向量X视为具有类别分布的随机变量。请注意,每个单独的类别遵循伯努利分布。类别C[i]在任意给定试验中发生的概率是
p(C[i]) = π[i]
我们可以紧凑地表达所有类别的概率分布

方程式 5.39
其中
是一个独热向量。请注意,方程式 5.39 中的所有幂次除了一个都是 0;因此相应的因子评估为 1。剩余的幂次是 1。因此,整体概率总是评估为π[i],其中i是试验中发生的类别的索引。
类别分布的期望值
由于我们正在讨论类别,期望值和方差在这个上下文中没有意义。我们遇到了类似的情况,即伯努利分布。我们为每个类别分配了数值,并人为地定义了期望值和方差。类似的思路也可以应用在这里:我们可以讨论独热向量的期望值和方差(该向量由数值 0 和 1 组成)。但这仍然是一个人为的结构。
给定一个随机变量 X,其实例是遵循具有 m 个类别及其相应概率 π[1]、π[2]、⋯、π[m] 的分类分布的单热向量
,

方程 5.40
我们省略了分类分布的方差。
摘要
在本章中,我们首先从机器学习的角度探讨了概率和统计学。我们还介绍了 PyTorch distributions 包,并在数学之后立即用 PyTorch distributions 代码示例说明了每个概念。
-
特定事件类型的概率被定义为所有可能事件总体中该特定类型事件所占的比例。
-
随机变量是一个可以取预定义的可能值范围内的任何值的变量。随机变量可以是离散的或连续的。与离散随机变量取特定值相关联的概率。与连续随机变量在特定值附近的无限小范围内的值相关联的概率,称为该值的概率密度。
-
概率的加法法则表明,一组互斥事件的概率之和是其中一个或另一个事件发生的概率。如果事件集是穷尽的(即,它们覆盖了所有可能事件的整个空间),那么它们的和是 1,因为其中一个或另一个事件必须发生。对于连续随机变量,对可能值域上的概率密度函数进行积分得到 1。
-
一组事件的联合概率是所有这些事件同时发生的概率。如果事件是独立的,联合概率是它们各自概率的乘积。
-
从随机变量的概率分布中抽取样本返回可能值集中的任意值。如果我们抽取许多样本,高概率值出现的频率高于低概率值。样本点占据可能值域中的一个区域(称为样本点云)。在样本点云中,概率较高的区域比低概率区域更密集。
-
随机变量的期望值是大量(接近无穷大)样本云中点的值的平均值。它等于随机变量所有可能值的加权总和,其中每个值的权重是其发生的概率。对于连续随机变量,这归结为对随机变量的值和概率密度乘积在可能值域上的积分。期望值的物理意义在于,它是整个分布的单点表示。
-
随机变量的方差是样本点值从平均值到平均值的平均平方距离的平方根,在非常大的样本云中(接近无穷大)。它等于所有可能的随机变量值从平均值到平方距离的加权总和。每个值的权重是其发生的概率。对于连续随机变量,这归结为积分——在可能值的域上——随机变量值与平均值的平方距离的乘积和概率密度。从物理上讲,方差是分布中点围绕其平均值的扩散程度的度量。在多变量情况下,这种扩散取决于方向。由于在二维或更多维度的空间中有无限可能的方向,我们不能谈论一个单一的方差值。相反,我们计算一个协方差矩阵,用它来计算沿任何指定方向的扩散。与这个协方差矩阵对应的最大特征值的特征向量给出了最大扩散的方向。那个特征值给出了最大扩散。与下一个最大特征值对应的特征向量给出了下一个最高扩散的正交方向,依此类推。
-
主成分分析(PCA)是多变量统计学中的一种技术,用于识别数据最大扩散的方向。它使用协方差矩阵的特征向量和特征值。
-
高斯分布是最重要的概率分布。高斯随机变量有一个值,其发生的概率最高。随着与该最高概率值的距离增加,概率会平滑地降低。概率密度函数是连续的,看起来像钟形表面。钟形的中心是最高概率值,这也恰好是高斯随机变量的期望值。协方差矩阵决定了钟形表面的底部形状。当协方差矩阵对角线时,它是圆形的,对角线上的值相等;通常情况下,它是椭圆形的,椭圆的轴沿着协方差矩阵的特征向量。
高斯分布的样本点云是椭圆形的。它对应于钟形概率密度函数的底部。最长的扩散对应于椭圆的主轴,这对应于协方差矩阵对应于最大特征值的特征向量。在 GitHub 仓库中,我们提供了一个交互式可视化工具,可以观察在改变参数值时一维和二维高斯分布的形状。请查看交互式可视化部分,
mng.bz/NYJX。
第六章:机器学习的贝叶斯工具
本章涵盖
-
无监督机器学习模型
-
贝叶斯定理、条件概率、熵、交叉熵和条件熵
-
模型参数的最大似然估计(MLE)和最大后验估计(MAP)
-
证据最大化
-
KLD
-
高斯混合模型(GMM)和 GMM 参数的 MLE 估计
统计学的贝叶斯方法试图通过建模系统的不确定性和普遍信念及知识来模拟世界。这与频率主义范式形成对比,在频率主义范式中,概率是通过反复观察现象并测量事件发生的频率来严格测量的。机器学习,尤其是无监督机器学习,与统计学的贝叶斯范式非常接近——这是本章的主题。
在第一章中,我们主要讨论了监督机器学习,其中训练数据是标记的:每个输入值都伴随着一个手动创建的期望输出值。标记训练输入是一个手动、劳动密集的过程,并且往往是构建基于机器学习系统的最痛苦点。这导致了近年来对无监督机器学习的极大兴趣,我们在未标记的训练数据上构建模型。这是如何做到的?
通用方法最好通过几何图形来可视化。每个输入数据实例都是高维空间中的一个点。这些点在所有可能输入的空间中形成一个整体模式。如果所有输入都具有共同属性,那么点不会在输入空间中随机分布。相反,它们占据输入空间中的一个具有确定形状的区域。如果输入具有多个类别,那么每个类别在空间中占据一个单独的簇。有时我们首先对输入进行变换——变换的选择或学习是为了使变换后的点比原始输入点更清晰地显示出模式。然后我们确定一个概率分布,其样本点云与(可能变换过的)训练数据点云的形状相匹配。我们可以从这个分布中采样生成伪输入。我们还可以通过观察它落入哪个簇来对任意输入进行分类。
注意:本章的完整 PyTorch 代码以完全功能性和可执行的形式,作为 Jupyter 笔记本,可在mng.bz /WdZa找到。
6.1 条件概率和贝叶斯定理
如往常一样,讨论伴随着示例。在此背景下,我们首先对第 5.4 节(您可能想回顾第 5.4、5.4.1 和 5.4.2 节中的联合概率主题)中的联合概率和边缘概率概念进行复习。
考虑两个随机变量:Statsville 成年居民的身高和体重。体重(表示为 W)可以取三个量化值:E[1]、E[2]、E[3]。身高 (H) 也可以取三个量化值:F[1]、F[2]、F[3]。表 6.1 展示了它们的联合概率。
6.1.1 联合和边缘概率再探讨
一眼就能看出表 6.1 中的概率主要集中在主对角线上,这表明事件是相关的。这可以通过检查一个联合概率——比如 p(E[1], F[1])——以及相应的边缘概率 p(F[1]) 和 p(E[1]) 来验证。我们可以看到 p(E[1], F[1]) = 0.2 ≠ p(F[1]) × p(E[1]) = 0.26 × 0.26,这表明随机变量体重 W 和身高 H 不是独立的。相比之下,看看表 5.6。在那个例子中,对于任何有效的 i、j 对,p(E[i], G[j]) = p(G[i]) × p(E[j]):两个事件(居民的体重和家到市中心的距离)是独立的。注意以下内容:
表 6.1 示例人口规模和变量 W = {E[1], E[2], E[3]} 和 H = {F[1], F[2], F[3]} 的联合概率分布,显示边缘概率
| 低于 60 kg (E[1]) | 60 至 90 kg (E[2]) | 超过 90 kg (E[3]) | Fs 的边缘概率 | |
|---|---|---|---|---|
| 低于 160 cm (F[1]) | 人数 = 20,000p(E[1], F[1]) = 0.2 | 人数 = 4,000p(E[2], F[1]) = 0.04 | 人数 = 2,000p(E[3], F[1]) = 0.02 | 人数 = 26,000;p(F[1]) = 0.2 + 0.04 + 0.02 = 0.26 |
| 160 至 183 cm 之间 (F[2]) | 人数 = 4,000p(E[1], F[2]) = 0.04 | 人数 = 40,000p(E[2], F[2]) = 0.4 | 人数 = 4,000p(E[3], F[2]) = 0.04 | 人数 = 48,000;p(F[2]) = 0.04 + 0.4 + 0.04 = 0.48 |
| 超过 183 cm (F[3]) | 人数 = 2,000p(E[1], F[3]) = 0.02 | 人数 = 4,000p(E[2], F[3]) = 0.04 | 人数 = 20,000p(E[3], F[3]) = 0.2 | 人数 = 26,000;p(F[3]) = 0.02 + 0.04 + 0.2 = 0.26 |
| E*s 的边缘概率 | p(E[1])= 0.2 + 0.04 + 0.02 = 0.26 | p(E[2])= 0.04 + 0.4 + 0.04 = 0.48 | p(E[3])= 0.02 + 0.04 + 0.2 = 0.26 | 总人口 = 100,000; 总概率 = 1 |
-
联合概率—这是特定值组合同时发生的概率。表 6.1 中的每个单元格描述了一个联合概率:例如,居民体重在 60 至 90 公斤之间 并且身高超过 183 厘米的概率是 p(E[2], F[3]) = 0.04。
-
求和法则—所有可能的变量组合的联合概率之和为 1(表 6.1 中的右下角单元格):

概率之和是其中一个或另一个相应事件发生的概率。在这里,我们添加了所有可能的事件组合——这些组合中的任何一个都肯定会发生。因此,总和为 1,这与我们的直觉相符。
- 变量的边缘概率—这是通过“消除”其他变量得到的(表 6.1 中的最右边列和最底行):

我们添加了所有其他变量的所有可能组合,因此总和代表了这个单一变量的概率。
- 边缘概率—这些概率之和为 1:

边缘概率之和是所有可能联合概率之和。
- 相关变量与独立变量—只有当变量是独立的,边缘概率的乘积才等于联合概率:
p(F[i] , E[j]) ≠ p(F[i]) × p(E[j]) ⟺ 对于表 5.6 中的相关变量
p(G[i] , E[j]) = p(G[i]) × p(E[j]) ⟺ 对于表 6.1 中的独立变量
您应该验证在表 6.1 中,对于体重和身高变量,这个条件不成立。在表 5.6 中,对于体重和从市中心到家的距离变量,这个条件成立。
6.1.2 条件概率
假设我们知道某人的身高在 160 至 183 厘米之间(H = F[2])。那么,该人体重超过 90 公斤(W = E[3])的概率是多少?在统计学中,这个概率表示为 p(W = E[3]|H = F[2])。它读作“在 H = F[2] 的条件下 W = E[3] 的概率”,也称为“在 H = F[2] 条件下 W = E[3] 的概率”。
这是一个条件概率的例子。请注意,如果我们知道身高在 160 至 183 厘米之间(H = F[2]),我们的宇宙就限制在表 6.1 的第二行。特别是,我们的种群规模不是 100,000(即 Statsville 的全部人口)。而是 48,000:满足给定条件 H = F[2] 的种群规模。使用频率主义定义,

表 6.2 展示了在条件概率添加后的表 6.1。
表 6.2 示例人口规模以及变量 W = {E[1], E[2], E[3]} 和 H = {F[1], F[2], F[3]} 的联合、边缘和条件概率(成年 Statsville 居民的体重和身高)。(这是添加了条件概率的表 6.1)。
| 小于 60 kg (E[1]) | 60 至 90 kg (E[2]) | 大于 90 kg (E[3]) | F*s 的边缘概率 | |
|---|---|---|---|---|
| 小于 160 cm (F[1]) | 人口 = 4,000p(E[1], F[1]) = 0.04p(E[1]| F[1]) = p(E[1], F[1]) / p(F[1]) = 0.154p(F[1]| E[1]) = p(E[1], F[1]) / p(E[1]) = 0.154 | 人口 = 2,000p(E[2], F[1]) = 0.02p(E[2]| F[1]) = p(E[2], F[1]) / p(F[1]) = 0.077p(F[1]| E[2]) = p(E[2], F[1]) / p(E[2]) = 0.077 | 人口 = 1,000p(E[3], F[1]) = 0.01p(E[3]| F[1]) = p(E[3], F[1]) / p(F[1]) = 0.037p(F[1]| E[3]) = p(E[3], F[1]) / p(E[3]) = 0.037 | 人口 = 26,000;p(F[1]) = 0.04 + 0.02 + 0.01 = 0.07 |
| 160 cm 至 183 cm 之间 (F[2]) | 人口 = 4,000p(E[1], F[2]) = 0.04p(E[1]| F[2]) = p(E[1], F[2]) / p(F[2]) = 0.083p(F[2])| E[1]) = p(E[1], F[2]) / p(E[1]) = 0.154 | 人口 = 40,000p(E[2], F[2]) = 0.4p(E[2]| F[2]) = p(E[2], F[2]) / p(F[2]) = 0.83p(F[2])| E[2]) = p(E[2], F[2]) / p(E[2]) = 0.83 | 人口 = 4,000p(E[3], F[2]) = 0.04p(E[3]| F[2]) = p(E[3], F[2]) / p(F[2]) = 0.083p(F[2])| E[3]) = p(E[3], F[2]) / p(E[3]) = 0.154 | 人口 = 48,000;p(F[2]) = 0.04 + 0.4 + 0.04 = 0.48 |
| 大于 183 cm (F[3]) | 人口 = 2,000p(E[1], F[3]) = 0.02p(E[1]|F[3]) = p(E[1], F[3]) / p(F[3]) = 0.077p(F[3]|E[1]) = p(E[1], F[33]) / p(E[1]) = 0.077 | 人口 = 4,000p(E[2], F[3]) = 0.04p(E[2]|F[3]) = p(E[2], F[3]) / p(F[3]) = 0.154p(F[3]|E[2]) = p(E[2], F[33]) / p(E[2]) = 0.083 | 人口 = 20,000p(E[3], F[3]) = 0.2p(E[3]|F[3]) = p(E[3], F[3]) / p(F[3]) = 0.77p(F[3]|E[3]) = p(E[3], F[33]) / p(E[3]) = 0.77 | 人口 = 26,000;p(F[3]) = 0.02 + 0.04 + 0.2 = 0.26 |
| E*s 的边缘概率 | p(E[1]) = 0.2 + 0.04 + 0.02 = 0.26 | p(E[2]) = 0.04 + 0.4 + 0.04 = 0.48 | p(E[3]) = 0.02 + 0.04 + 0.2 = 0.26 | 总人口 = 100,000;总概率 = 1 |
6.1.3 贝叶斯定理
如表 6.2 所示,一般来说,

这是贝叶斯定理的精髓。我们可以推广并说以下内容:给定两个随机变量 X 和 Y,在 Y 取值
的条件下,X 取值 x 的条件概率由这两个变量的联合概率与条件的边缘概率之比给出

方程 6.1
有时我们会省略随机变量的名称,直接使用数值。使用这种符号,贝叶斯定理可以表述为

注意,分母是边缘概率,可以通过对联合概率求和得到。例如,对于连续变量,贝叶斯定理可以写成

贝叶斯定理可以进一步推广到超过两个变量和多维:

方程式 6.2

方程式 6.3
通常的做法是省略随机变量的名称(大写),只保留值(小写),并且非正式地陈述这些方程

如果随机变量是独立的会发生什么?好吧,让我们检查一下方程 6.1。如果 X 和 Y 是独立的,

因此

这使得直观上有意义:如果 X 和 Y 是独立的,知道 Y 对 p(X = x) 没有任何影响,所以给定 Y 的 X 的概率与 X 的概率相同。
6.2 熵
假设一份每日气象公告告知美国人民昨天撒哈拉沙漠是否下雨。这份公告中包含很多整体信息吗?实际上并没有——它几乎总是报告显而易见的事情。不下雨的概率非常高,几乎可以肯定不会下雨),与结果相关的不确定性非常低。即使没有公告,如果我们猜测结果“不下雨”,我们几乎每次都会猜对。同样,一份每日新闻公告告诉我们昨天印度切拉彭齐是否下雨——一个几乎总是下雨的地方——包含很少的信息内容,因为我们即使没有公告也可以非常确定地猜测结果。换句话说,与“撒哈拉沙漠下雨与不下雨”或“切拉彭齐下雨与不下雨”的概率分布相关的风险很低。这是这样一个事实的直接后果:其中一个事件的概率接近 1,而其他事件的概率接近 0:概率密度函数(PDF)在一个位置上非常高大,在其他地方则非常低。
另一方面,一份每日报告是否在旧金山下雨的公告具有相当大的兴趣,因为“下雨”和“不下雨”的概率相当。没有这份公告,我们无法有很大把握地猜测结果。
熵的概念试图量化与随机事件相关的不确定性。如果任何单个事件发生的概率极高(意味着其他事件发生的概率非常低,因为总和为 1),则不确定性低——我们几乎可以肯定高概率事件会发生。另一方面,如果有多个事件具有相当高的概率,则不确定性高——我们无法预测哪个事件会发生。熵捕捉了系统中的这种不确定性概念。让我们看看另一个例子。
假设我们有一些很小的图像,宽度为四个像素,高度为四个像素,每个像素可以是四种可能的颜色之一:G(绿色)、R(红色)、B(蓝色)或 Y(黄色)。图 6.1 中显示了两个这样的图像。我们想要对这些图像进行编码。最简单的事情是为每种颜色使用两个位的表示:
G(绿色) = 00
R(红色) = 01
B(蓝色) = 10
Y(黄色) = 11

图 6.1 展示了两个 4×4 像素的图像,像素颜色分布不同。在左侧的图像中,四种颜色 R、G、B 和 Y 出现的概率相等。在右侧的图像中,一种颜色(绿色)比其他颜色更可能。左侧的图像具有更高的熵(不确定性):我们无法非常确定地预测任何颜色。在右侧的图像中,我们可以相对确定地预测绿色。
左侧的整个 16 像素图像可以用字符串 00 00 00 00 01 01 01 01 10 10 10 10 11 11 11 11 表示。在这里,我们已经按照光栅扫描顺序迭代了像素,从左到右,从上到下。存储 16 像素图像所需的位数总数是 16×2=32 位。右侧的图像可以表示为 00 00 00 00 00 00 00 00 00 00 00 00 01 01 10 11。所需的位数总数是 16×2=32 位。两种图像需要的存储量相同。但是,这是最优的吗?
考虑右侧的图像。颜色 G 出现的频率远高于其他颜色。我们可以利用这个事实来减少存储图像所需的位数总数。不一定需要使用相同数量的位来表示每种颜色。对于出现频率更高(概率更高)的颜色使用更短的表示,对于出现频率较低(概率较低)的颜色使用更长的表示怎么样?这是可变位率编码技术背后的核心原理。例如,我们可以使用以下表示:
G(绿色) = 0
R(红色) = 10
B(蓝色) = 110
Y(黄色) = 111
因此,右侧的图像可以表示为 0 0 0 0 0 0 0 0 0 0 0 0 10 10 110 111。
注意:这是一个被称为前缀编码的例子:没有两种颜色共享相同的前缀。这使我们能够在看到代码时立即识别颜色。例如,如果我们看到开头的 0 位,我们立即知道颜色是绿色,因为没有任何其他颜色代码以 0 开头。如果我们看到 10,我们立即知道颜色是红色,因为没有任何其他颜色代码以 10 开头,以此类推。
使用这种新的颜色代码,我们需要 12 × 1 = 12 比特来存储 12 个绿色像素,2 × 2 = 4 比特来存储 2 个红色像素,1 × 3 = 3 比特来存储单个蓝色像素,以及 1 × 3 = 3 比特来存储单个黄色像素——总共 22 个像素。相当于我们需要 22/16 = 1.375 比特每像素。这比我们使用简单的固定比特率编码所需的 32 个像素每比特要少。
注意:您刚刚学习了霍夫曼编码,这是图像压缩中的一个重要技术。
新的表示是否会导致左侧图像的存储空间更小?在那里,我们需要 4 × 1 = 4 比特来存储四个绿色像素,4 × 2 = 8 比特来存储四个红色像素,4 × 3 = 12 比特来存储四个蓝色像素,以及 4 × 3 = 12 比特来存储单个黄色像素:总共 36 个像素,36/16 = 2.25 比特每像素。在这里,可变比特率编码比固定比特率编码效果更差。
因此,图像中各种像素颜色的概率分布会影响可以实现的压缩程度。如果像素颜色的分布是这样的,即某些颜色比其他颜色更有可能,我们可以为它们分配较短的代码,以减少整个图像的存储空间。从另一个角度来看,如果系统与低不确定性相关——某些颜色或多或少是肯定发生的——我们可以实现高压缩。我们为几乎肯定的颜色分配较短的代码,从而实现压缩。另一方面,如果系统与高不确定性相关——所有颜色或多或少是同样可能的,并且没有颜色以高概率发生——可变比特率编码将不会非常有效。我们如何量化这个概念?换句话说,我们能否检查图像中的像素颜色分布并估计可变比特率编码是否有效?答案仍然是熵。正式地,
熵衡量与概率分布相关的整体不确定性。
熵是一个当所有事情或多或少同样可能时较高的度量,而当少数项目比其他项目有更高的概率时,它是一个较低的度量。它衡量系统中的不确定性。如果所有事情都是同样可能的,我们无法以任何额外的确定性预测任何一项。这样的系统具有高熵。另一方面,如果某些项目比其他项目更有可能,我们可以相对确定地预测它们。这样的系统具有低熵。
在离散单变量情况下,对于可以取离散值 x[1]、x[2]、x[3]、⋯、x[n] 的随机变量 X,其概率为 p(x[1])、p(x[2])、p(x[3])、⋯、p(x[n]),熵定义为

方程式 6.4
对数是以自然底数 e 为底。
让我们将方程 6.4 应用于图 6.1 中的图像,看看结果是否与我们的直觉一致。计算结果显示在表 6.3 中。熵的概念同样适用于连续和多维随机变量。
表 6.3 图 6.1 中图像对的熵计算。右侧图像的熵较低,可以压缩得更多。
| 左图像 | 右图像 |
|---|---|
| x[1] = G, p(x[1]) = 4/16 = 0.25 | x[1] = G, p(x[1]) = 12/16 = 0.75 |
| x[2] = R , p(x[2]) = 4/16 = 0.25 | x[2] = R , p(x[2]) = 2/16 = 0.125 |
| x[3] = B, p(x[3]) = 4/16 = 0.25 | x[3] = B, p(x[3]) = 1/16 = 0.0625 |
| x[4] = Y, p(x[4]) = 4/16 = 0.25 | x[4] = Y, p(x[4]) = 1/16 = 0.0625 |
| ℍ = −(0.25 log(0.25)+0.25 log(0.25) + + 0.25 log(0.25) + 0.25 log(0.25)) = 1.386294 | ℍ = −(0.75 log(0.75)+0.125 log(0.125) + + 0.0625 log(0.0625) + 0.0625 log(0.0625)) = 0.822265 |
对于一个单变量连续随机变量 X,其取值 x ∈ {−∞,∞},其概率为 p(x),

方程 6.5
对于一个在域 D 中取值
的连续多维随机变量 X,(
∈ D),其概率为 p(
),

方程 6.6
6.2.1 熵的几何直觉
从几何上讲,熵是 PDF 倾斜程度的函数(见图 6.2)。如果所有输入的概率大致相等,密度函数在所有地方的高度大致平坦且均匀(见图 6.2a)。相应的样本点云具有扩散的质量:没有点的高浓度区域。这样的系统具有高不确定性或高熵(见图 6.2b)。

(a) 较平坦、较宽的 PDF 对应较高的熵。熵 = 12.04。

(b) 扩散的样本点云对应较高的熵。

(c) 在概率密度函数中较高的、较窄的峰值对应较低的熵。熵 = 7.44。

(d) 集中的样本点云对应较低的熵。
图 6.2 峰值和平坦分布的熵
另一方面,如果所有可能输入中的一些具有不成比例的高概率,PDF 在某些区域有高峰,而在其他地方高度较低(见图 6.2c)。相应的样本点云在密度函数的峰值附近有高浓度区域,而在其他地方浓度较低(见图 6.2d)。这样的系统具有低不确定性和低熵。
注意:由于所有概率之和为 1,如果其中一些概率较高,其他概率必须较低。我们不能同时所有概率都高或都低。
6.2.2 高斯分布的熵
越宽的高斯分布,其峰值越低,越接近均匀分布。一元高斯分布的方差 σ 决定了其胖瘦(见图 5.10b)。因此,我们预计高斯分布的熵是 σ 的增函数。确实如此。在本节中,我们推导了一元情况下高斯分布的熵,并简单地陈述了多元情况下的结果。
对于具有方程 5.22(为方便起见在此重复)给出的 PDF 的随机变量 x,

从中我们得到

使用方程 6.6,熵为

记住方程 5.6 中的概率和规则,∫[x = −∞]^∞ p(x) dx = 1,我们得到

现在,根据定义(见第 5.7.2 节),

因此,

方程 6.7
多变量高斯分布的熵如下:

方程 6.8
列表 6.1 展示了计算高斯分布熵的 Python PyTorch 代码。
注意:用于计算高斯分布熵的完整代码,可通过 Jupyter Notebook 执行,可在mng.bz/zx7B找到。
列表 6.1 计算高斯分布的熵
def entropy_gaussian_formula(sigma):
return 0.5 * torch.log(2 * math.pi * math.e * sigma * sigma) ①
p = Normal(0, 10) ②
H_formula = entropy_gaussian_formula(p.stddev) ③
H = p.entropy() ④
assert torch.isclose(H_formula, H) ⑤
① 方程 6.7
② 实例化高斯分布
③ 使用直接公式计算熵
④ 使用 PyTorch 接口计算熵
⑤ 断言两种不同方式计算的熵相等
6.3 交叉熵
考虑一个 监督 分类问题,其中我们需要分析一张图像并识别以下哪些对象存在:猫,狗,飞机,或 汽车。我们假设在这些图像的宇宙中,这些对象之一总是存在的。给定一个输入图像,我们的机器输出四个概率:p(猫),p(狗),p(飞机),和 p(汽车)。在训练过程中,对于每个训练数据实例,我们有一个真实值(GT):一个已知的类别,该训练数据实例属于该类别。我们必须估计网络输出与 GT 之间的差异——这是该数据实例的损失。我们调整机器参数以最小化损失,并继续这样做,直到损失停止下降。
我们如何定量估计损失——已知 GT 与网络发出的各种类别的概率之间的差异?一种原则性的方法是使用交叉熵损失。以下是它是如何工作的。
考虑一个随机变量 X,它可以取四个可能值:X = 1 表示 猫,X = 2 表示 狗,X = 3 表示 飞机,X = 4 表示 汽车。该随机变量的概率密度函数 p(X = 1) ≡ p(猫),p(X = 2) ≡ p(狗),p(X = 3) ≡ p(飞机),p(X = 4) ≡ p(汽车)。对于选择四个可能类别之一的目标真实值(GT),其概率密度函数是一个独热向量(其中一个元素为 1,其余为 0)。这样的随机变量和相应的概率密度函数可以与每个 GT 和机器输出相关联。以下是一些示例,这些示例也在图 6.3 中以图形方式展示。GT 猫 的概率密度函数(独热向量)如图 6.3a 所示:
我们如何定量估计损失——已知 GT 与网络发出的各种类别的概率之间的差异?一种原则性的方法是使用交叉熵损失。以下是它是如何工作的。

良好预测的概率密度函数如图 6.3b 所示:

差异预测的概率密度函数如图 6.3c 所示:


(a) 真实值概率

(b) 良好预测:概率与真实值相似。交叉熵损失 = 0.22。

(c) 差异预测:概率与真实值不相似。交叉熵损失 = 1.38。
图 6.3 交叉熵损失
让 X[gt] 表示特定 GT 的这样一个随机变量,p[gt] 表示相应的概率密度函数。同样,让 X[pred] 和 p[pred] 表示机器预测的随机变量和概率密度函数。考虑以下表达式:

方程 6.9
这是交叉熵的表达式。它是两个概率密度函数 p[gt] 和 p[pred] 之间差异的定量度量:也就是说,用 p[pred] 近似 p[gt] 会造成多少误差。等价地,交叉熵衡量了当正确概率密度函数是 p[gt] 时,机器输出预测 p[pred] 的效果如何。
为了了解 ℍ[c](X[gt], X[pred]) 如何衡量概率密度函数之间的差异,仔细检查该表达式。记住,Σ[i]⁴[= 1] p[gt] (i) = Σ[i]⁴[= 1] p[pred] (i) = 1(使用方程 5.3 中的概率和规则):
情况 1:当 p[gt](i) 较高(接近 1)的 i 值。
情况 1a:如果 p[pred](i) 也接近 1,那么 log (p[pred](i)) 将接近零(因为 log 1 = 0)。因此,p[gt](i)log (p[pred](i)) 将接近零,因为任何数与接近零的数的乘积都接近零。这些项将对 ℍ[c](X[gt], X[pred]) 的贡献很小。
情况 1b:另一方面,在 p[gt](i) 较高的 i 值处,如果 p[pred](i) 较低(接近零),则 −log (p[pred](i)) 将非常高(因为 log 0 → − ∞)。
情况 2:当 p[gt](i) 较低(接近 0)的 i 值。这些值将较低,并且对 ℍ[c](X[gt], X[pred]) 的贡献很小,因为任何数与接近零的数的乘积都接近零。
因此,总体而言,只有在情况 1b 中,即 p[gt](i) 较高而 p[pred](i) 较低时,才会出现大的贡献——也就是说,p[gt] 和 p[pred] 非常不同。如果 p[gt](i) 较低而 p[pred](i) 较高会怎样?它们也是不同的,所以这些项不会贡献太多!确实如此,但如果存在这样的项,则必须有其他项中 p[gt](i) 较高而 p[pred](i) 较低。这是因为所有 p[gt](i) 和 p[pred](i) 的总和都必须是 1。无论如何,如果存在差异,交叉熵就会很高。
例如,考虑 X[gt] = X[gt_cat] 和 X[pred] = X[good_pred] 或 X[pred] = X[bad_pred] 的情况。我们知道 p[gt_cat] 是一个 one-hot 选择向量,这意味着它有一个元素为 1,其余为 0。只有一个项幸存,对应于 i = 0,并且

我们看到,在相似性较低的地方(预测不佳),交叉熵较高。
最后,我们准备正式定义两个任意随机变量的交叉熵。设 X[1],X[2] 是一对取值来自相同输入域 D(即 x ∈ D),分别具有概率 p1,p2 的随机变量:

方程 6.10
注意到方程 6.10 中的交叉熵在 Y = X 时简化为熵(方程 6.5, 6.6),列出 6.2 展示了使用 Python PyTorch 计算高斯熵的代码。
注意:用于计算交叉熵的完整功能代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/0mjN 找到。
列出 6.2 计算交叉熵
def cross_entropy(X_gt, X_pred):
H_c = 0
for x_gt, x_pred in zip(X_gt, X_pred):
H_c += -1 * (x_gt * torch.log (x_pred)) ①
return H_c
X_gt = torch.Tensor([1., 0., 0., 0.]) ②
X_good_pred = torch.Tensor([0.8, 0.15, 0.04, 0.01]) ③
X_bad_pred = torch.Tensor([0.25, 0.25, 0.25, 0.25]) ④
H_c_good = cross_entropy(X_gt, X_good_pred) ⑤
H_c_bad = cross_entropy(X_gt, X_bad_pred) ⑥
① 直接计算
从方程 6.9 得到的交叉熵
② 真实值(one-hot 向量)的概率密度函数
③ 良好预测的概率密度函数
④ 坏预测的概率密度函数
⑤ X[gt] 和 X[good_pred] 之间的交叉熵具有低值
⑥ X[gt] 和 X[bad_pred] 之间的交叉熵具有高值
6.4 KL 散度
在第 6.3 节中,我们看到了交叉熵,ℍ[c](X[1], X[2]),衡量了两个随机变量 X[1] 和 X[2] 的分布差异,其概率为 p1 和 p2。但是交叉熵作为一个差异度量具有一个奇特性质。如果 X[1] = X[2],交叉熵 ℍ[c](X[1], X[2]) 会简化为熵 ℍ(X[1])。这有点反直觉:我们期望相同事物的两个副本之间的差异为零。
我们应该将交叉熵视为具有偏移量的差异度。让我们用 D(X[1], X[2]) 表示纯差异度量。那么

这意味着纯差异度量

这个纯差异度量,D(X[1], X[2]),被称为 Kullback–Leibler 散度(KL 散度或 KLD)。正如预期的那样,当两个随机变量相同的时候,它为 0。
形式上,KLD 如下:

方程式 6.11
对于连续的单变量随机变量,

方程式 6.12
对于连续的多变量随机变量,

方程式 6.13
让我们考察 KLD 的某些性质:
-
相同随机变量之间的 KLD 为零。如果 X[1] = X[2],p1 = p2∀x ∈ D,那么对数项在每一个 x 处消失,KLD 为零。
-
非相同概率分布之间的 KL 散度总是正的。我们可以通过检查方程 6.11 来看到这一点。在所有 x 的值中,当 p1 > p2 时,对数项是正的(因为大于 1 的数的对数是正的)。另一方面,在所有 x 的值中,当 p1 < p2 时,对数项是负的(因为小于 1 的数的对数是负的)。但是,正项因为 p1 在这些点上的值更高而获得更高的权重。在这种情况下,值得注意的是,对于任何一对 PDF,一个不能在所有点上均匀高于另一个。这是因为它们都必须加起来等于 1。如果一个 PDF 在某个地方更高,那么它必须在其他地方更低以补偿。
-
对于一个分类问题的 GT PDF p[gt] 和机器预测 p[pred],最小化交叉熵 ℍ(gt, pred) 在逻辑上等同于最小化 KL 散度 D(gt, pred)。这是因为熵 ℍ(gt) 是一个常数,与机器参数无关。
-
KL 散度不是对称的:D(X[1], X[2]) ≠ D(X[2], X[1])。
6.4.1 高斯分布之间的 KL 散度
由于高斯概率分布非常重要,在本小节中,我们研究两个具有 PDFs p1 = 𝒩(x; μ[1], σ[1]) 和 p2 = 𝒩(x; μ[2], σ[2]) 的高斯随机变量 X[1] 和 X[2] 之间的 KL 散度。我们推导出单变量情况的表达式,并简单地陈述多变量情况的表达式:

展开括号,我们得到

展开平方项,我们得到

由于

两个单变量高斯随机变量 X[1],X[2] 之间 KL 散度的最终方程,其 PDFs 为 𝒩(x; μ[1], σ[1]) 和 𝒩(x; μ[2], σ[2]),变为

方程式 6.14
两个 d-维高斯随机变量 X[1],X[2] 之间的 KL 散度,其 PDFs 为 𝒩(
; μ[1], Σ[1]) 和 𝒩(
; μ[2], Σ[2]) 是

方程式 6.15
其中,运算符 tr 表示矩阵的迹(对角元素之和),运算符 det 表示行列式。
列表 6.3 显示了计算 KL 散度的 Python PyTorch 代码。
注意:用于计算 KL 散度的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/KMyj找到。
列表 6.3 计算 KL 散度
from torch.distributions import kl_divergence
p = Normal(0, 5)
q = Normal(0, 10) ①
r = Normal(0, 20)
kld_p_p = kl_divergence(p, p)
kld_p_q = kl_divergence(p, q)
kld_q_p = kl_divergence(q, p) ②
kld_p_r = kl_divergence(p, r) ③
assert kld_p_p == 0 ④
assert kld_p_q != kld_q_p ⑤
assert kld_p_q < kld_p_r \5
① 实例化三个高斯分布
具有相同的均值但不同的
标准差
② 计算各种分布对之间的 KL 散度
③ 一个分布与自身之间的 KL 散度为 0。
④ KL 散度不是对称的。
⑤ 见图 6.4。
在图 6.4 中,我们比较了具有相同 μ 但不同 σ 的三个高斯分布 p,q 和 r。KLD(p, q) < KLD(p, r) 因为 σ[p] 比 σ[r] 更接近 σ[q]。
在图 6.4 中,我们比较了均匀分布 p 与两个具有不同 μ 但相同 σ 的高斯分布 q 和 r。KLD(p, q) < KLD(p, r) 因为 μ[p] 比 μ[r] 更接近 μ[q]。
6.5 条件熵
在第 6.2 节中,我们学习了熵衡量系统中的不确定性。在之前的第 6.1.2 节中,我们研究了条件概率,它衡量在另一组随机变量的已知固定值条件下,一组随机变量发生概率。在本节中,我们将这两个概念结合成一个新的概念,称为 条件熵。
考虑表 6.2 中的以下问题。在高度变量 H 的值为 F[1] 的条件下,权重变量 W 的熵是多少?如第 6.1.1 节所述,条件实际上将我们的宇宙限制为表的一行(在这种情况下,是顶部行)。我们可以使用方程 6.5 通过数学方法计算该行元素的熵,


(a) p ≡ 𝒩(μ = 0, σ = 5), q ≡ 𝒩(μ = 0, σ = 10), r ≡ 𝒩(μ = 0, σ = 20)

(b) p ≡ U(a = −20, b = 20), q ≡ 𝒩(μ = 0, σ = 20), r ≡ 𝒩(μ = −50, σ = 20)
图 6.4 示例分布之间的 KLD
同样,

ℍ(W|H = F[i]) 是在 H = F[i] 且 i = 1 或 2 或 3 时 W 的熵。那么,给定 H 的 W 的整体条件熵是什么?即,ℍ(W|H)?为了计算这个值,我们取条件熵 ℍ(W|H = F[i]) 在所有可能的 i 值上的期望值(即,概率加权的平均值;参见方程 5.8):

这个想法可以推广。形式上,给定两个可以取值 x ∈ D[x] 和
∈ D[y] 的随机变量 X 和 Y,

方程 6.16

方程 6.17
6.5.1 条件熵的链式法则
这条规则说明:
ℍ(X|Y) = ℍ(X, Y) − ℍ(Y)
方程 6.18
这可以从方程 6.17 推导出来。

应用贝叶斯定理(方程 6.1),

方程 6.19
6.6 模型参数估计
假设我们有一组从分布中采样的输入数据点 X = {
^((1)),
^((2)),⋯,
^((n))}。我们将这个集合统称为 训练数据。请注意,我们 不 假设它是 标记 训练数据——我们不知道与输入
^((i))对应的输出。此外,假设根据我们对问题的了解,我们已经决定使用哪种模型族。当然,仅仅知道家族是不够的;在我们能够使用模型之前,我们需要知道(或估计)模型参数。例如,我们的模型族可能是高斯,𝒩(x;
, Σ)。在我们知道参数
和 Σ 的实际值之前,我们并不完全了解模型,也无法使用它。
我们如何从未标记的训练数据中估计模型参数?这正是本节要讨论的内容。目前,我们正在讨论这个问题,而不涉及任何特定的模型架构,因此让我们用通用符号 θ 表示模型参数。例如,当处理高斯模型时,θ = {
, Σ}。
6.6.1 概率、证据、后验概率和先验概率
在着手解决参数估计问题之前,了解当前上下文中“似然”、“证据”、“后验概率”和“先验概率”这些术语的含义非常重要。方程 6.20 对其进行了说明。使用贝叶斯定理,

方程 6.20
首先,我们来考察一下似然项。利用数据实例之间相互独立的事实,

现在,p(
^((i))|θ) 实际上是我们所选择的分布族的概率密度。例如,如果所讨论的模型是高斯模型,那么给定 θ = {
, Σ},这将变为

这基本上是高斯概率密度函数的表达式:方程 5.23 的重述(但在方程 5.23 中,我们在符号中省略了“给定 θ”的部分,并将 p(
|θ) 简单地表示为 p(
))。因此,我们可以始终使用单个训练数据实例的独立性,从所选模型族的概率密度函数中表达似然。
现在,让我们来考察先验概率,p(θ)。它通常来自某种物理约束——不涉及输入。一个非常流行的方法是,在其他条件相同的情况下,我们更喜欢幅度较小的参数。按照这个说法,总幅度 ||θ||² 越大,先验概率就越低。例如,我们可能使用
p(θ) ∝ e^(−||θ||²)
方程 6.21
对于偏好长度(幅度)最小的参数向量,一个间接的依据可以在奥卡姆剃刀原则中找到。它声明,“Entia non sunt multiplicanda praeter necessitatem”,这大致可以翻译为“不应该无谓地增加实体。”在机器学习和其他学科中,这通常被解释为“偏好最简明的表示。”
如前所述,我们可以总是表达似然和先验项。使用它们,我们可以制定不同的范例,每个范例都有一个不同的量要优化,以从训练数据中估计未知概率分布参数。这些技术可以广泛地分为以下几类:
-
最大似然参数估计(MLE)
-
最大后验(MAP)参数估计
我们接下来概述它们。您会注意到,在所有方法中,我们通常预先选择一个分布族作为模型,然后通过最大化一个概率或另一个概率来估计参数值。
在本章的后面部分,我们将探讨高斯分布族的特殊情况下的 MLE。接下来,我们将探讨高斯混合模型中的 MLE。稍后,我们将探讨证据最大化:我们将在变分自编码器的背景下讨论它。
对数似然技巧
如果我们选择一个概率密度函数为指数分布的分布族(最明显的例子是高斯分布),我们通常不是最大化似然函数,而是最大化其对数,也就是所谓的对数似然。我们可以这样做,因为任何最大化一个量也会最大化它的对数,反之亦然。但是,对数在指数概率函数的情况下简化了表达式。如果我们注意到这一点,这一点就会变得明显:
log(e^x) = x
log(Π e(*x*((i)))) = Σ x^((i))
6.6.2 最大似然参数估计(MLE)
在参数的最大似然估计(MLE)中,我们问,“什么参数值将最大化训练数据实例的联合似然?”在这个背景下,记住似然是在给定特定参数值的情况下,一个数据实例发生的概率(方程 6.20)。用数学表达式来说,
MLE 估计什么值的θ将最大化p(X|θ)。几何上的思维图如下:我们想要估计模型概率分布中的未知参数,使得如果我们从这个分布中抽取许多样本,样本点云将主要与训练数据重叠。
通常我们使用对数似然技巧,最大化对数似然而不是实际似然。
对于某些模型,例如高斯模型,这个最大化问题可以通过解析方法解决,并得到闭式解(如第 6.8 节所示)。对于其他模型,例如高斯混合模型(GMMs),最大化问题没有闭式解,我们寻求迭代解(如第 6.9.4 节所示)。
6.6.3 最大后验(MAP)参数估计和正则化
我们不是询问什么参数值最大化了训练数据实例发生的概率,而是可以问,“给定训练数据,最可能的参数值是什么?”用数学表达式表示,在 MAP 中,我们直接估计最大化p(θ|X)的θ。使用公式 6.20,

公式 6.22
由于分母与θ无关,相对于θ最大化分子最大化了分数。因此
在 MAP 参数估计中,我们寻找最大化p(X|θ)p(θ)的参数θ。
-
第一个因素,p(X|θ),是我们 MLE 中优化的内容,它来自模型定义(如多元高斯模型的公式 5.23)。
-
第二个因素,p(θ),是先验项,它通常激励优化系统选择具有预定义属性(如较小的参数幅度)的解,公式 6.21。
以这种方式看,MAP 估计相当于带有正则化的最大似然(MLE)参数估计。正则化是优化中常用的一种技术。在正则化优化中,我们在要最大化或最小化的表达式中添加一个项。这个项有效地激励系统从可能的解集中选择未知参数幅度最小的解。很容易看出,MAP 估计本质上是在 MLE 之上施加先验概率项。这个额外的项充当正则化器,激励系统选择幅度最低的参数,同时仍然试图最大化训练数据的似然。
公式 6.22 可以另一种方式来解释。当我们没有训练数据时,我们所能做的就是根据我们对系统的先验信念来估计参数:先验项p(θ)。当训练数据集X到达时,它通过似然项p(X|θ)影响系统。随着越来越多的训练数据到来,先验项(其幅度不随训练数据变化)的影响力越来越小,后验概率p(θ|X)越来越受似然的影响。
6.7 隐变量和证据最大化
假设我们有一个种群(例如,我们最喜欢的城镇 Statsville 的成年居民)的高度和体重数据。单个数据实例看起来是这样的:

虽然数据没有明确标记或分类,但我们知道数据点可以被聚类成两个不同的类别,男性 和 女性。可以合理地预期,每个类别的分布比整体分布简单得多。例如,在这里,男性和女性的分布可能是单独的高斯分布(可能,女性的平均值将出现在更矮和更轻的身高和体重值)。组合分布不符合我们之前讨论过的任何分布(稍后我们将看到它是一个高斯混合分布)。
我们将更详细地探讨这些情况与高斯混合模型和变分自编码器的关系。在这里,我们只指出,在这些情况下,引入一个用于类别的变量通常是有益的,比如 Z。在这个例子中,Z 是离散的:它可以取两个值之一,男性 或 女性。然后我们可以将整体分布建模为简单分布的组合,每个对应于 Z 的一个特定值。
这样的变量 Z,它们不是观察数据 X 的组成部分,而是为了便于建模而引入的,被称为 潜在 或 隐藏 变量/参数。潜在变量通过通常的贝叶斯表达式与观察变量相连接:

我们如何估计 Z 的分布?一种方法是可以问,“如果我们从这个分布中抽取随机样本,那么隐藏变量的分布是什么,会最大化返回这些特定训练数据点的概率?” 这种方法的哲学依据如下:我们假设训练数据点相当典型,在未知数据分布中出现的概率很高。因此,我们试图找到一个分布,使得训练数据点将具有最高的概率。
从几何学的角度来看,每个数据点(向量)可以看作是在某个 d-维空间中的一个点,其中 d 是向量
[i] 的元素数量。训练数据点通常占据该空间内的一个区域。我们正在寻找一个质量大部分与训练数据区域对齐的分布。换句话说,与训练数据点相关的概率尽可能高——样本分布云与训练数据云大部分重叠。
用数学表达,我们想要识别 p(
|
) 和 p(
),它们最大化了数量

方程式 6.23
如同往常,我们从所选模型家族的 PDF 中得到p(
^((i))|
),并通过某些物理约束得到p(
)。
6.8 高斯分布的最大似然参数估计
我们用一个一维例子来看这个问题,但推导出的结果也适用于高维情况。假设我们正在尝试预测一个成年 Statsville 居民的性别,给定该居民的身高位于指定范围[a, b]。为此,我们收集了一组成年女性Statsville 居民的身高样本。这些身高样本构成了我们的训练数据。让我们用x^((1)), x^((2)), ⋯, x^((n))来表示它们。根据物理考虑,我们预计成年 Statsville 女性的身高分布是一个具有未知均值和方差的高斯分布。我们的目标是通过对训练数据进行最大似然估计(MLE)来确定它们,这有效地估计了一个样本云与训练数据点分布最大匹配的分布。
让我们用μ和σ表示分布的(尚未知的)均值和方差。然后,根据方程 5.22,我们得到

使用对数似然技巧,

要使相对于μ最大化,我们求解

或者

或者

最后,我们得到一个关于训练数据的未知μ的闭式表达式:

同样,要使相对于σ最大化,我们求解

或者

或者

最后,我们得到一个关于训练数据的未知σ的闭式表达式:

因此,我们可以看到,对于一个高斯分布,最大似然解与训练数据的样本均值和方差相一致。一旦我们得到了均值和标准差,我们可以通过以下方程计算一个女性居民的身高属于指定范围[a, b]的概率:

方程式 6.24
在多维情况下:
给定一个训练数据集,{
^((1)),
^((2)),⋯,
^((n))},最佳拟合高斯分布的均值

方程式 6.25
以及协方差矩阵

方程式 6.26
我们在本节开始时提出了估计成年 Statsville 居民为女性的概率问题,前提是他们的身高在指定的范围 [a, b] 内,当我们提供成年 Statsville 女性居民的 n 个身高值训练数据集时。现在让我们重新审视这个问题。使用(标量形式的)方程 6.25 和 6.26,我们可以估计 μ 和 σ,从而定义一个高斯概率分布
p(x) = 𝒩(x; μ, σ)
使用这个方法,给定任何身高 x,我们可以计算居民为女性的概率 p(x)。让我们用 PyTorch 来看看这个例子。
6.8.1 Python PyTorch 代码用于最大似然估计
假设我们假设 Statsville 成年女性的身高值遵循高斯分布。如果我们知道这个高斯分布的参数(μ 和 σ),我们就完全知道了高斯分布。这使我们能够估计许多有趣的事情:例如,成年 Statsville 居民的预期身高,或者成年 Statsville 居民的身高在某个范围(如 160 至 170 厘米之间)的概率。问题是,在典型的现实情况下,我们不知道参数 μ 厘米和 σ。我们所有的是成年 Statsville 女性居民的身高值的大型数据集 X——训练数据。我们必须使用这些数据来估计未知的参数 μ 厘米和 σ。一旦我们有了这些,我们就有了从其中可以预测感兴趣事件概率的估计分布(即模型)。
正如我们在 6.6.2 节中看到的,MLE 是一种技术,当已知分布所属的家族但不知道参数的确切值时,用于从给定的训练数据中估计参数。列表 6.4 展示了 PyTorch 对高斯家族的 MLE 实现。
注意:使用 MLE 和 MAP 估计模型参数的完整代码,可通过 Jupyter Notebook 执行,可在 mng.bz/9Mv7 找到。
列表 6.4 高斯的最大似然估计
sample_mean = X.mean() ①
sample_std = X.std()
gaussian_mle = Normal(sample_mean, sample_std) ②
a, b = torch.Tensor([160]), torch.Tensor([170]) ③
prob = gaussian_mle.cdf(b) - gaussian_mle.cdf(a)
① 估计高斯 MLE 参数
和 Σ。它们等于训练数据的样本均值和样本协方差。参见方程 6.25 和 6.26。
② 定义具有估计参数的高斯分布
③ 一旦估计出高斯分布,我们就可以用它来预测概率。
6.8.2 使用梯度下降的 Python PyTorch 代码进行最大似然估计
在列表 6.4 中,我们使用闭式解计算了最大似然估计。现在,让我们尝试使用不同的方法来计算最大似然估计:梯度下降。在实际场景中,我们不会使用梯度下降来计算最大似然估计,因为闭式解是可用的。然而,我们在这里讨论这种方法,以突出使用梯度下降的一些挑战以及如何通过最大后验估计来解决这些挑战。
我们的目标是使用梯度下降来最大化似然函数。这也可以被视为最小化负对数似然函数。我们选择使用似然函数的对数,因为这会导致计算更简单,而不会损失泛化能力。(如果你想要快速复习梯度下降,请参阅第 3.5 节。)以下为负对数似然方程:

方程式 6.27
列表 6.5 和 6.6 展示了 PyTorch 代码的最小化过程。
列表 6.5 训练数据的 Gaussian 负对数似然
def neg_log_likelihood(X, mu, sigma): ①
N = X.shape[0]
X_minus_mu = torch.sub(X, mu)
t1 = torch.mul(0.5 * N,
torch.log(2 * np.pi * torch.pow(sigma, 2))) ②
t2 = torch.div(torch.matmul(X_minus_mu.T, X_minus_mu),
2 * torch.pow(sigma, 2)) ③
return t1 + t2 ④
① 方程式 6.27
② n/2 log 2 πσ²
③ (Σ[i = 1]^n (x[i] – μ)²)/(2σ²)
④ 注意所有训练数据 X 都在一个操作中压缩。这种向量操作在 PyTorch 中是并行且非常高效的。
列表 6.6 通过梯度下降最小化 MLE 损失
def minimize(X, mu, sigma, loss_fn, num_iters=100, lr = 0.001): ①
②
for i in range(num_iters):
loss = loss_fn(X, mu, sigma) ③
loss.backward() ④
mu.data -= lr * mu.grad
sigma.data -= lr * sigma.grad ⑤
mu.grad.data.zero_()
sigma.grad.data.zero_() ⑥
mu = Variable(torch.Tensor([5]).type(dtype), requires_grad=True)
sigma = Variable(torch.Tensor([5]).type(dtype), requires_grad=True)
minimize(X, mu, sigma, neg_log_likelihood)
① 负对数似然(列表 6.5)
② 迭代训练
③ 计算损失
④ 计算损失相对于 μ 和 σ 的梯度。PyTorch 将梯度存储在 μ.grad 和 σ.grad 中。
⑤ 通过学习速率和更新参数来缩放梯度
⑥ 更新后重置梯度为零

(a) 最大似然估计爆炸:μ[init] = 1,σ[init] = 1。

(b) 最大似然估计收敛:μ[init] = 100,σ[init] = 10。

(c) 最大后验估计收敛:μ[init] = 1,σ[init] = 1。
图 6.5 使用最大似然估计和最大后验估计进行高斯参数估计。在图 6.5a 中,最大似然估计爆炸,因为 μ 和 σ 的初始化远离 μ[expected] 和 σ[expected]。然而,在图 6.5b 中,最大似然估计收敛,因为 μ 和 σ 的初始化接近 μ[expected] 和 σ[expected]。图 6.5c 展示了对于最大后验估计,μ 和 σ 即使初始化远离 μ[expected] 和 σ[expected],也能收敛到 μ.[expected] 和 σ[expected]。
图 6.5 展示了 μ 和 σ 如何随着梯度下降的每次迭代而变化。我们期望 μ 和 σ 分别接近 μ[expected] 和 σ[expected]。然而,当 μ 和 σ 从远离 μ[expected] 和 σ[expected] 的状态开始,如图 6.5a) 所示时,它们不会收敛到期望值,反而变成了非常大的数字。另一方面,当它们被实例化为更接近 μ[expected] 和 σ[expected] 的值,如图 6.5b) 所示时,它们会收敛到期望值。最大似然估计(MLE)对初始值非常敏感,并且没有机制来防止参数爆炸。这就是为什么 MAP 估计更受欢迎。先验 p(θ) 作为正则化项,防止参数变得过大。图 6.5c 展示了即使 μ 和 σ 从远离的位置开始,它们如何使用 MAP 收敛到期望值。
MAP 损失函数如下。请注意,它与负对数似然函数相同,但增加了两个额外的项—μ² 和 σ²—作为正则化项:

方程 6.28
列表 6.7 带正则化的高斯负对数似然
def neg_log_likelihood_reg(X, mu, sigma, k=0.2): ①
N = X.shape[0]
X_minus_mu = torch.sub(X, mu)
t1 = torch.mul(0.5 * N,
torch.log(2 * np.pi * torch.pow(sigma, 2))) ②
t2 = torch.div(torch.matmul(X_minus_mu.T, X_minus_mu),
2 * torch.pow(sigma, 2)) ③
loss_likelihood = t1 + t2 ④
loss_reg = k * (torch.pow(mu, 2) + torch.pow(sigma, 2)) ⑤
return loss_likelihood + loss_reg ⑥
① 方程 6.28
② n/2 log 2 πσ²
③ (Σ[i = 1]^n (x[i] – μ)²)/(2σ²)
④ 负对数似然
⑤ 正则化
⑥ 注意所有训练数据 X 都在一个操作中压缩。这种向量操作在 PyTorch 中是并行且非常高效的。
6.9 高斯混合模型
在许多现实生活中的问题中,我们在第五章中学到的简单单峰(单峰值)概率分布无法模拟数据的真实潜在分布。例如,考虑这样一种情况,我们被给出了许多成年 Statsville 居民的身高。假设 Statsville 中有两个成年类别:男性和女性。我们拥有的身高数据是未标记的,这意味着我们不知道给定的身高数据实例是否与男性或女性相关联。因此,数据是一维的,有两个类别。图 6.6 描述了这种情况。我们在第五章中讨论的简单概率分布都无法拟合图 6.6。但是图 6.6a 中的两个部分钟形表明,我们应该能够混合一对高斯分布(每个都像钟形)来模拟这种分布。这也与我们知道分布代表的是两个类别而不是一个类别,每个类别都可以合理地单独用高斯分布来表示的知识相符。点云也表明有两个独立的点簇。虽然单个高斯分布不起作用,但两个独立的单维高斯分布的组合可以(并且,我们将很快看到,将会)起作用。

(a) PDF

(b) 样本点分布
图 6.6 Statsville 成年男性和女性居民一维身高数据的概率密度函数(PDF)和样本点分布
现在我们来讨论一个稍微复杂一些的问题,其中数据是二维的,并且有三个类别。这里我们给出了 Statsville 居民三个类别的体重和身高:成年女性、成年男性和儿童。再次强调,数据是未标记的,这意味着我们不知道给定的(身高,体重)数据实例是否与男性、女性或儿童相关联。这如图 6.7 所示。再次强调,我们在第五章中研究的简单概率分布都无法拟合这种情况。但是 PDF 显示了三个钟形峰值,点云显示了三个簇,而问题的物理性质表明有三个独立的类别,每个类别都可以合理地用高斯分布来表示。虽然单个高斯分布不起作用,但三个独立的二维高斯分布的组合可以(并且,我们将很快看到,将会)起作用。
高斯混合模型(GMM)是特定数量高斯成分的加权组合。

(a) PDF

(b) 样本点分布
图 6.7 Statsville 儿童、成年男性和成年女性二维(身高,体重)数据的概率密度函数(PDF)和样本点分布
例如,在我们的第一个问题中,只有一个维度和两个类别,我们选择两个一维高斯分布的混合。对于第二个问题,我们采用三个二维高斯分布的混合。每个单独的高斯组件对应一个特定的类别。
6.9.1 GMM 的概率密度函数
形式上,
GMM 的 PDF 为

方程 6.29
其中 π[k] 是第 k 个高斯组件的权重,满足

K 是类别数或高斯组件数,方程 5.23 中定义的 𝒩(
;
[k], Σ[k]) 是第 k 个高斯组件的概率密度函数(PDF)。这样的高斯混合模型模拟了一个 K 峰 PDF 或等价地,一个 K 聚类样本点云。
例如,图 6.6 中所示的 PDF 和样本点云对应以下高斯混合模型:

图 6.7 中所示的二维三类别问题、PDF 和样本点云对应以下高斯混合模型:

GMM 的 PDF 和样本点分布取决于 π[k]、μ[k]、Σ[k] 和 K 的值。特别是,K 影响 PDF 中的峰值数量(尽管如果两个峰值非常接近,有时它们会合并)。它还影响样本点云中的聚类数量(再次,如果两个聚类太接近,它们可能不会在视觉上明显区分)。π[k] 调节山丘的相对高度。μ[k] 和 Σ[k] 影响 PDF 中的单个山丘以及样本点云中的单个聚类。具体来说,μ[k] 调节 PDF 中第 k 个峰的位置和样本点云中第 k 个聚类的质心。Σ[k] 调节第 k 个单个山丘和样本点云中第 k 个聚类的形状。图 6.8、6.9、6.10 和 6.11 展示了一些具有这些参数不同值的示例 GMM。
图 6.8 展示了一对高斯分布以及作为组件的各种高斯混合模型(GMM),参数值各不相同。图 6.9 描述了具有各种 π[k] 的高斯混合模型。图 6.10 展示了具有非圆形基(非对称 Σ)和不同 μ 的高斯混合模型。

(a) 高斯组件 μ[1] = 152, μ[2] = 175, σ[1] = σ[2] = 9

(b) π[1] = 0.5, π[2] = 0.5 的高斯混合模型

(c) π[1] = 0.7, π[2] = 0.3 的高斯混合模型

(d) GMM 中π[1] = 0.3, π[2] = 0.7
图 6.8 各种 GMMs(实线)具有相同的高斯组件(分别用虚线和虚线表示),但π[1]和π[2]的值不同
另一种可视化高斯混合模型(GMMs)的方法是通过样本点分布。图 6.11 展示了从一对二维高斯分布中抽取的样本点,以及从具有这些高斯分布作为组件且具有不同混合选择概率的高斯混合模型中抽取的点。

(a) π[1] = 0.5, π[2] = 0.5

(b) π[1] = 0.4, π[2] = 0.6

(c) π[1] = 0.7, π[2] = 0.3

(d) π[1] = 0.3, π[2] = 0.7
图 6.9 具有圆形基的两维 GMMs,

注意山丘的相对高度取决于π。

(a) 

(b) 
图 6.10 具有椭圆形基的两维 GMMs,π[1] = 0.3, π[2] = 0.7。注意山丘基部的形状取决于Σ,以及山丘位置取决于
s。

(a)

(b)
图 6.11 (a)
. (b) 从具有与(a)中相同的三个高斯组件和π[1] = π[2] = 0.4, π[3] = 0.2 的 GMM 中抽取的 1,000 个随机样本。注意 GMM 样本分布形状如何模仿组件高斯分布的合并样本分布形状。
可以证明方程 6.29 是一个合适的概率:即在所有可能的输入空间(d-维空间中所有可能的
的值)上求和为 1。以下是证明概要:

6.9.2 用于类别选择的潜在变量
让我们更详细地讨论 GMMs。特别是,我们查看方程 6.29 中各种术语的物理意义。
在深入探讨之前,让我们引入一个辅助随机变量Z,它实际上是一个类别选择器。在方程 6.29 的上下文中,Z可以在[1⋯K]范围内取离散值。因此,它遵循分类分布(参见第 5.9.6 节)。从物理上讲,Z = k意味着已经选择了第k个类别——即高斯混合模型的第k个组件。
注意:如往常一样,我们用大写字母表示随机变量,用小写字母表示它在特定实例中取的具体值。
例如,在图 6.6 中显示的两个类别问题中,Z 可以取两个值之一:1(表示成年女性)或 2(表示成年男性)。在图 6.7 中显示的三个类别问题中,Z 可以取三个值之一:1(成年女性)、2(成年男性)或 3(儿童)。Z 被称为 潜在(隐藏)随机变量,因为其值不能直接观察到。这与输入随机变量
的值是明确观察到的形成对比。你可能会将 Z 识别为 GMM 中的潜在变量(潜在变量在 6.7 节中介绍)。
考虑联合概率 p(X =
, Z = k), 我们有时非正式地表示为 p(
, k). 这是输入变量
与类别 k 同时发生的概率。使用贝叶斯定理,
p(
, k) = p(
|k)p(k)
条件概率项 p(
|k) 是在选择了第 k 类时的
的概率。这意味着它是第 k 个高斯成分的概率密度函数,根据假设这是一个高斯分布。因此,使用方程 5.23,
p(
|k) = 𝒩(
;
[k], Σ[k]) k ∈ [1, K]
另一方面,p(Z = k), 我们有时非正式地称之为 p(k),是输入属于某一类别的先验概率(即,不考虑输入)。让我们如下表示它:
p(k) = π^k, ∀k ∈ {1, K}
这通常被建模为属于类别 k 的训练数据点的 分数:

其中 N[k] 是属于类别 k 的训练数据实例的数量,而 N 是训练数据实例的总数。
从这个方程中,我们得到
p(
, k) = p(k)p(
|k) = π^k 𝒩(
;
[k], Σ[k]) k ∈ [1, K]
从方程 5.5 中,我们得到边缘概率 p(x)

这与方程 6.29 相同。
这导致以下物理解释:
-
GMM 可以被视为 K 个高斯成分的加权求和。方程 6.29 描述了整体 GMM 的概率密度函数。
-
权重π[k]是组件选择概率。具体来说,π[k]可以解释为选择第 k 个子类的先验概率p(Z = k),也称为p(k),即第 k 个子类的比例——模型化为属于第 k 个子类的人口比例。π[k]是在具有K个类别的分类分布中的概率。π[k]的总和为 1。从高斯混合模型(GMM)中采样可以看作是两步过程:
-
随机选择一个组件。第 k 个组件被选择的概率是π[k]。所有π[k]的总和为 1,这表示必须选择一个或另一个组件。
-
从所选的高斯组件中随机采样。生成向量
的概率是𝒩(
;
[k], Σ[k])。
-
-
每个K个高斯组件模型一个单独的类别。从几何上讲,这些组件对应于样本点云中的聚类或 GMM 概率密度函数(PDF)中的峰值。
-
第 k 个高斯组件,𝒩(
;
[k], Σ[k]),可以解释为条件概率,p(
|k)。这是似然——在选择了第 k 个子类的情况下,数据值
发生的概率。 -
乘积π[k] 𝒩(
;
[k], Σ[k])然后代表联合概率p(
, k) = p(
|k) p(k)。 -
所有联合子类概率之和是数据值
的边缘概率p(
)。
列表 6.8 高斯混合模型分布
from torch.distributions.mixture_same_family import MixtureSameFamily ①
pi = Categorical(torch.tensor([0.4, 0.4, 0.2])) ②
mu = torch.tensor([[175.0, 70.0], [152.0, 55.0], [135.0, 40.0]]) ③
sigma = torch.tensor([[[30.0, 20.0], [20.0, 30.0]], ④
[[50.0, 0.0], [0.0, 10.0]],
[[20.0, 0.0], [0.0, 20.0]]])
gaussian_components = MultivariateNormal(mu, sigma) ⑤
gmm = MixtureSameFamily(pi, gaussian_components) ⑥
① Pytorch 支持同一家族(此处为高斯)的分布混合
② 三种类别(男性、女性、儿童)的先验概率:分类分布
③ 三种类别(男性、女性、儿童)的平均身高和体重
④ 男性、女性、儿童三种类别的协方差矩阵
⑤ 创建组件高斯分布
⑥ 创建高斯混合模型(GMM)
6.9.3 通过 GMM 进行分类
涉及 GMM 的典型实际问题如下。提供一组未标记的输入数据 X(训练数据)。重要的是要注意,这是一个无监督的机器学习——训练数据没有附带已知的输出类别。问题的物理性质表明数据中的子类(用索引 [1⋯K] 表示)。目标是分类任何任意的输入
:也就是说,将其映射到 K 个类别中的一个。为此,我们必须拟合一个 GMM(即推导出 π[k],
[k], Σ[k] 对于所有 k ∈ [1 ⋯ K])的值)。给定一个任意的
,我们计算所有类别的 p(k|
)。产生 p(k|
) 最大值的 k 是与
对应的类别。我们是怎样计算 p(k|
) 的呢?
使用贝叶斯定理,

方程式 6.30
如果我们知道所有的 GMM 参数,评估方程 6.30 是直接的。我们通过将输入
分配给产生最高 p(Z = k|X = x) 值的簇 k 来对输入进行分类。从几何上看,这是将输入分配给具有“最接近”均值的簇——距离由相应分布的方差进行归一化。基本上,我们是在测量与均值的距离,但在高方差簇中,我们对与均值的距离更加宽容。这从直觉上是有意义的:如果一个簇分布广泛(即高方差),那么相对于簇均值较远的点可以被认为是属于该簇的。另一方面,与紧密排列的簇的均值相同距离的点可能被认为是簇外部的。
6.9.4 GMM 参数的最大似然估计(GMM 适配)
GMM(高斯混合模型)完全由其参数集 θ = {π^k,
[k], Σ[k] ∀k ∈ [1 ⋯ K]} 描述。但我们是怎样估计这些参数值的呢?在典型的现实情况下,这些参数并没有给我们。我们只有一组观察到的未标记的训练数据点 X = {
^((i))},例如 Statsville 居民的体重和身高值。
从几何学的角度来看,训练数据集中的每个数据实例对应于多维特征空间中的一个单独点。训练数据集是一个自然聚集成高斯子云的点云(否则,我们就不应该尝试 GMMs)。我们的 GMM 模仿这个数据集应该有与数据中自然聚类的数量一样多的组件。参数值π[k],
[k],Σ[k]对于*k * ∈ [1 ⋯ K]应该被估计,使得 GMM 的样本点云尽可能多地与训练数据点云重叠。这正是本节我们试图解决的问题的基本问题。
注意:我们不估计K,即类别数量;而是使用一个固定的K值,通常从问题的物理条件中估计得出。例如,在涉及男人、女人和孩子的问题上,很明显K = 3。
在第 6.8 节中,我们对一个简单的高斯分布进行了最大似然估计(MLE)。我们计算了给定高斯概率分布的所有训练数据的联合对数似然表达式。然后,我们计算该表达式相对于参数的梯度,并将其等于零。我们能够解这个方程,从而推导出参数的封闭形式解,
和Σ(方程 6.25 和 6.26)。这意味着我们将方程简化为一种形式,其中未知(需要求解)的量单独出现在左边,而右边只有已知实体。
不幸的是,在使用 GMMs 时,将对数似然梯度的值设为零会导致一个没有封闭形式解的方程。因此,我们不能将方程简化为未知量π[k]s,μ[k]s,和Σ[k]单独出现在左边,而只有已知实体(
[i]s)出现在右边的形式。因此,我们必须求助于迭代近似。我们将通过将对数似然梯度的值设为零得到的方程重写,使得未知量μs 和σs 单独出现在右边。它看起来像这样
π^k = f1
[k] = f2
Σ[k] = f3
其中f[1],f[2],f[3]是一些函数,其确切性质在目前并不重要。请注意,右边也包含未知量:θ包含π[k]s,μ[k]s,和Σ[k]。我们无法直接求解这样的方程,但我们可以使用迭代松弛,其工作原理大致如下:
-
从随机的π[k]s,
[k]s,和Σ[k]s 值开始。 -
通过将π[k]s,
[k]s,和Σ[k]s 的当前值代入函数f[1],f[2],和f[3]来评估右边。 -
使用步骤 2 中估计的值来设置新的π[k]s、
[k]s 和Σ[k]s 的值。 -
重复步骤 1-3,直到参数值不再明显变化。
实际函数f[1]、f[2]、f[3]在方程 6.36、6.37、6.38 中给出。随着迭代的进行,π[k]s、
[k]s 和Σ[k]s 的值开始收敛到它们的真实值。这不是幸运的巧合。如果我们遵循算法 6.3,可以证明每一次迭代都会改善近似,即使改善的量非常小。最终,我们达到一个点,此时近似不再明显改善。这被称为固定点,我们应该停止迭代并宣布当前值为最终值。
图 6.12 展示了迭代 GMM 拟合算法的进展。图 6.12a 展示了采样训练数据分布。图 6.12b 展示了初始拟合的 GMM:参数基本上是随机的,GMM 看起来与目标训练数据分布毫不相似。它缓慢地改进,直到迭代 15 次,它紧密地匹配了目标分布 6.12d)。

(a) 训练数据点云(拟合的目标)

(b) 第 0 步拟合的 GMM 样本点云

(c) 第 5 步拟合的 GMM 样本点云

(d) 第 15 步拟合的 GMM 样本点云。它几乎与目标匹配。
图 6.12 GMM 参数最大似然估计的进展
现在我们来讨论细节。我们已经知道观察到的数据集X。哪个参数集θ将最大化给定参数集的这些数据点的条件概率p(X|θ)?换句话说,哪些模型参数将最大化训练数据的整体似然?那些将是我们对未知模型参数的最佳猜测。这是我们在第 6.6.2 节中遇到的 MLE。
设{
^((1)),
((*n*)),⋯((n))}为观察数据点的集合,即训练数据。从方程 6.29,

从此以后,为了简便起见,我们省略了“给定 θ”的部分,并将 p(
^((i))|θ) 简单地称为 p(
^((i))). 如同往常,我们不是直接最大化似然,而是最大化其对数,即 log-likelihood。这将产生与直接最大化似然相同的参数。
由于 x^((i))s 是独立的,根据方程 5.4,它们的联合概率是
p(
((1)))*p*(((2)))⋯p(
^((n)))
相应的对数联合概率是

方程 6.31
在这一点上,我们开始看到 GMMs 的一个特殊困难。我们有一个和的对数,这不是一个很容易处理的表达式;乘积的对数则更容易处理。但让我们继续前进。
为了识别将最大化对数联合概率的参数
¹, Σ[1],
², Σ[2], ⋯,我们取对数联合概率对这些参数的梯度,将它们设为零,并求解参数值(如第 3.3.1 节所述)。在这里,我们以
[1] 为例展示这个过程:
∇![[1]]log(p(
((1)))*p*(((2)))⋯p(
^((n)))) = 0
由于乘积的对数是各个对数的和,我们得到

将方程 6.29 应用于我们的最大化问题,我们得到

由于梯度是一个线性算子,我们可以将其移到求和符号内部:

由于 d/dx log (f(x)) = 1/f(x) df/dx,我们得到

现在,如果 x[1] 和 x[2] 是独立变量,则 dx[2]/dx[1] = 0。因此,

只有与 k = 1 对应的单个项在分子中的微分(梯度)过程中幸存。所以,

现在 d/dx e^(–(x – μ)²) = –2(x – μ) e^(–(x – μ)²),在多维情况下,

将方程 5.23 带入我们的最大化问题,我们得到

此外,通过一点努力,你可以证明关于二次型梯度的以下内容:
∇
^TA
) = A
方程 6.32
将方程 6.32 应用于我们的问题,我们得到

将等式两边乘以常数 Σ[1],我们得到

代入

方程式 6.33
我们得到

这个表达式在 γ[i1] 中也有 μ[1]。在方程的左侧不可能单独提取 μ[1]。换句话说,我们无法为 μ[1] 创建一个 封闭形式 的解。因此,我们必须迭代求解。
我们可以将前面的方程重写为

其中

方程式 6.34
以类似的方式,我们可以推导出 π[1] 和 Σ[1] 的相应表达式。让我们收集更新 GMM 参数的所有方程:

方程式 6.35

方程式 6.36

方程式 6.37

方程式 6.38
方程式 6.36,6.37,和 6.38 提供了我们在本节开头关于迭代松弛的上下文中看到的函数 f[1],f[2],和 f[3] 的定义。我们可以以类似的方式处理 k = 2⋯K。
γ[ik] 的物理意义
在计算对数似然梯度时,我们遇到了实体 γ[ik]。它作为乘性权重出现在计算方程 6.37 和 6.38 中 μ[k] 和 Σ[k] 的最终迭代表达式中。它不是一个任意实体。通过比较方程 6.33 和 6.30,我们可以看到
γ[ik] = p(k|
^((i)))
换句话说,量 γ[ik] 实际上是后验概率:给定第 i 个数据点的类别 k 的条件概率。
这为我们提供了观察方程 6.35,6.36,6.37,和 6.38 的新方法:
-
方程式 6.35 实质上根据当前参数值将 N[1] 赋值为集中在类别 1 中的概率质量。
-
方程式 6.36 将 π[1] 赋值为根据当前参数值在类别 1 中的分数质量。
-
方程式 6.37 将 μ[1] 赋值为所有训练数据点的质心。每个数据点的贡献都通过后验概率加权,该概率是根据当前参数值计算出的该数据点属于类别 1 的概率。
-
方程式 6.38 将 Σ[1] 赋值为训练数据点的协方差。每个数据点的贡献都通过后验概率加权,该概率是根据当前参数值计算出的该数据点属于类别 1 的概率。
算法 6.3 将方程 6.33,6.36,6.37,和 6.38 结合成一个完整的迭代 MLE GMM 参数的方法。它是一类称为期望最大化的通用算法的例子。
算法 6.3 GMM 拟合(从未标记的训练数据中估计 GMM 参数的 MLE)
输入:X =
^((1)),
^((2)), … ,
^((n))
使用随机值初始化参数Θ = {π^k , μ[k] , Σ[k] k ∈ [1, K]}
⊳ 重复 E 步和 M 步直到似然停止增加
while (似然增加) do
⊳E 步

⊳M 步

end while
返回{x[1] , μ[1], Σ[1], x[2], μ[2], Σ[2] , … , x[K], μ[K], Σ[K]}
注意:完全功能的高斯混合模型代码,可通过 Jupyter Notebook 执行,可在mng.bz/j4er找到。
列表 6.9 GMM 拟合
while (curr_likelihood - prev_likelihood) < 1e-4: ①
# E Step ②
pi = gmm.mixture_distribution.probs ③
components = gmm.component_distribution ④
⑤
log_gamma_numerators = components.log_prob(
X.unsqueeze(1)) + torch.log(pi).repeat(n, 1) ⑥
⑦
log_gamma_denominators = torch.logsumexp(
log_gamma_numerators, dim=1, keepdim=True).
⑧
log_gamma = log_gamma_numerators - log_gamma_denominators
self.gamma = torch.exp(log_gamma)
# M Step ⑨
n = X.shape[0] ⑩
N = torch.sum(gamma, 0)
pi = N / n ⑪
mu = ((X.T @ gamma)/N).T ⑫
x_minus_mu = (X.repeat(K, 1, 1) - gmm.component_distribution.unsqueeze(1).
repeat(1, n, 1))
⑬
x_minus_mu_squared = x_minus_mu.unsqueeze(3) @ x_minus_mu.unsqueeze(2)
⑭
sigma = torch.sum(gamma.T.unsqueeze(2).unsqueeze(3) * x_minus_mu_squared,
axis=1) / N.unsqueeze(1).unsqueeze(1).repeat(1, d, d)
prev_likelihood = curr_likelihood
curr_likelihood = torch.sum(gmm.log_prob(X)) ⑮
① 重复直到似然增加可以忽略不计
② 使用当前的
[k]s 和Σ[k]s 计算后验概率γ[i,k] = p(Z = k|X = x[i]),方程 6.33
③ 维度为[K]的张量,包含所有[k]的π[k]值
④ 对于所有k,高斯对象𝒩(
,
[k], Σ[k])
⑤ 对于所有 i, k,计算γ[i, k]分子的对数,方程 6.33
⑥ 在实践中,涉及指数的概率趋近于 0。因此,我们使用对数概率。
⑦ 对于所有 i, k,计算γ[i, k]分母的对数,方程 6.33
⑧ 向量计算[n × K]张量γ[i, k],方程 6.33
⑨ 使用 E 步得到的γ[i, k] = p(Z = k|X)更新所有k的
[k]和Σ[k],通过方程 6.36,6.37,和 6.38
⑩ 数据点的数量
⑪ 对于所有 k,更新π[k]的向量,方程 6.36
⑫ 对于所有 k,[K × d]张量
[k]的向量更新,方程 6.37
⑬ 对于所有l, k,向量计算(
[i] –
[k]) (
[i] –
[k])^T
⑭ 对于所有 k,[K × d × d]张量Σ[k]的向量更新,方程 6.38
⑮ 对数似然,方程 6.31
摘要
在本章中,我们探讨了在不确定系统中进行决策的贝叶斯工具。我们讨论了条件概率和贝叶斯定理,它们将条件概率与联合概率和边缘概率联系起来。
-
条件概率是在另一个事件已经发生的情况下,一个事件发生的概率。在机器学习中,我们通常对给定模型预测输入的参数为θ时,输入
的条件概率p(
|θ)感兴趣。这个条件概率被称为输入的似然。我们还对条件概率p(θ|
)感兴趣,这被称为后验概率。 -
联合概率是一组事件同时发生的概率。如果事件是独立的,联合概率是它们各自概率的乘积。无论事件是否独立,贝叶斯定理都将联合概率和条件概率联系起来。在机器学习中,特别感兴趣的是贝叶斯定理表达式,它将输入和参数的似然、联合和后验概率联系起来:p(
, θ) = p(
|θ)p(θ) 和 p(
|θ)p(θ) / p(
)。p(
|θ)是所选分布族的概率分布函数。p(θ)是我们对系统信念的先验概率,没有数据。一个流行的选择是p(θ) ∝ e^(−||θ||²),这意味着参数的绝对值越大,概率越小,反之亦然。 -
熵模拟了一个系统中的不确定性。所有事件具有或多或少相似概率的系统往往具有高熵。具有特定可能事件子集概率显著高而其他事件概率显著低的系统往往具有低熵。等价地,低熵系统的概率密度函数往往具有高尖峰,它们的样本点云在某些区域有高浓度的点。高熵系统往往具有平坦的概率密度函数和扩散的样本点云。
-
交叉熵使我们能够量化我们的模型与已知真实值之间的好坏。
-
库尔巴克-莱布勒散度为我们提供了两个概率分布之间差异的度量。
-
最大似然估计(MLE)和最大后验估计(MAP)是估计模型参数的两种范式。MLE 最大化p(X|θ),而 MAP 最大化p(X|θ)p(θ)。MLE 本质上试图估计概率分布参数,这些参数最大化概率分布样本点云与训练数据点云之间的重叠。MAP 是带有正则化条件的 MLE。正则化条件通过先验概率项p(θ)注入,它倾向于具有某些属性(如小的参数幅度)的解,这些属性是我们根据经验知识而没有数据相信是真实的。高斯分布的 MLE 有一个封闭形式的解。最佳拟合训练数据的概率分布的均值和方差(在多维情况下为协方差)是训练数据集上的样本均值和样本方差或协方差。
-
在机器学习系统中,潜在变量是辅助变量,它们不能直接观察到,但可以从输入中推导出来。它们有助于表达优化目标或要最小化的损失。
-
高斯混合模型(GMM)是一种无监督概率模型,它适合于训练数据集中具有多个簇的多类数据分布,每个簇对应一个不同的类别。在这里,最大似然估计(MLE)不产生封闭形式的解,而是产生一个迭代解来估计混合权重、均值和方差。
第七章:函数近似:神经网络如何模拟世界
本章涵盖
-
将现实世界问题表达为数学函数
-
理解神经网络的基本构建块
-
通过神经网络近似函数
迄今为止,计算一直由冯·诺伊曼架构主导,其中处理器和程序是分开的。程序存储在内存中,由处理器取回并执行。这种方法的优点是,可以加载内存中解决不同问题的不同程序,并且相同的处理器可以执行它们。但神经网络具有根本不同的架构。没有独立的处理器和程序;相反,有一个单一的实体,称为神经网络,它可以在专用硬件或冯·诺伊曼计算机上运行。在本章中,我们将详细讨论这一范式。
注意:本章的完整 PyTorch 代码以功能齐全和可执行的 Jupyter 笔记本形式,可在mng.bz/K4zj找到。
7.1 神经网络:从 10,000 英尺的高度看
在 1.7 节中,我们概述了神经网络。 (此时,你可能想快速复习一下 1 章。) 我们指出,人类执行的大多数智能任务都可以用我们称之为目标函数的数学函数来表示。因此,为了开发执行智能任务的机器,我们需要拥有能够模拟目标函数的机器。虽然这让我们对开发自动化解决方案抱有希望,但我们面临着两个严重的困难:
-
除了任意复杂外,各种现实生活问题的底层目标函数彼此完全不同。几乎没有任何共同模式。
-
对于大多数问题,我们不知道其底层的目标函数。
尽管如此,我们仍希望为执行现实生活中的智能任务找到一个机械化的可重复解决方案。我们不想从头开始,为每个这样的问题设计底层功能。这正是神经网络发挥作用的地方:
神经网络提供了一个统一的框架来模拟极其广泛的任意复杂函数。
虽然整体神经网络模拟了一个复杂的函数,但其构建块是一个相当基本的单元,称为神经元。神经元代表一个相对简单的函数。完整的神经网络由许多神经元组成,它们之间有加权连接。通过操纵神经元的数量、它们之间的连接性和连接权重,可以使其近似任何特定问题的任意目标函数。
神经网络可以表示的函数的多样性和复杂性被称为其表达能力。表达能力随着神经网络中神经元数量的增加以及它们之间连接数量的增加而增加。目标函数越复杂,神经网络建模所需的表达能力就越多。我们如何使神经网络模型/近似/表达与特定感兴趣问题的特定目标函数相对应?答案:我们可以调整神经网络的以下两个方面:
-
架构——神经元数量以及它们之间的连接
-
参数值——神经元之间连接的权重
架构通常是根据问题的性质来选择的。一些流行的架构被频繁地重用,神经网络工程师通常会选择一个历史上已知对类似当前问题的有效架构。我们将在本书的后面部分查看几个流行的架构——例如,在第十一章节中。
神经网络可以分为两大类:监督学习和无监督学习。在监督神经网络中,我们为试图解决的问题中一组采样输入值识别所需的输出值。这些采样输入的期望输出通常通过称为标记(又称人工标注或人工整理)的过程手动选择。一组<采样输入,期望输出>对构成了监督训练数据。训练数据输入的期望输出集有时被统称为真实值或目标输出。
在训练过程中,参数值(又称权重)被调整,以便网络在训练输入上的输出尽可能接近相应的真实值。如果一切顺利,在训练结束时,我们将得到一个神经网络,其在训练输入上的输出接近真实值。这个训练好的神经网络随后被部署到现实世界,在那里它执行推理——它对从未见过的输入生成输出。如果我们选择了具有足够表达能力的架构,并且用足够的训练数据正确训练了网络,那么在推理过程中它应该会发出准确的结果。请注意,我们无法保证推理过程中的正确结果;我们只能做出概率性陈述,即我们的输出有p的概率是正确的。
无监督神经网络不需要手动标记的地面真实数据——它们只需在训练输入上工作。标记训练数据的劳动成本高昂且麻烦。因此,相当多的研究工作投入到无监督、半监督(训练数据中的一部分是手动标记的)或自监督(标记的训练数据是通过程序而不是手动创建的)神经网络中。然而,在撰写本文时,无监督和半监督神经网络技术还不够成熟,使用它们达到期望的准确度水平更困难。本书后面我们将探讨无监督方法,包括变分自编码器(第十四章)。但就目前而言,我们主要讨论监督方法。
重要的是要注意,在架构选择或训练过程中,我们不需要被逼近的函数(目标函数)或逼近函数(建模函数)的封闭形式表示。这是非常重要的。在大多数情况下,我们无法知道目标函数——我们只知道样本输入和真实值对(训练数据)。至于建模函数,即使我们知道建模神经网络的架构,它所表示的整体函数如此复杂,以至于几乎无法处理。因此,我们不需要知道目标或建模函数的封闭形式,这使得这项技术变得实用。
7.2 表达现实世界问题:目标函数
考虑经典的投资者问题:是否卖出股票。问题输入可能包括购买价格、当前价格、投资者是否喜欢专家建议卖出或不出售等。可以通过一个函数来解决该问题,该函数接受这些输入并输出 0(不卖)或 1(卖)。如果我们能够模拟这个函数,我们就会有一个解决这个现实世界问题的机械解决方案。
就像这个例子一样,大多数现实世界问题都可以表达为目标函数。我们收集所有可能影响结果的可量化变量:这些构成了输入变量。输入变量以数值实体表示:标量、向量、矩阵等。输出也以称为输出变量的数值变量表示。给定一个特定的输入(例如,购买价格和当前价格的特定值),我们的模型函数会输出一个输出(0 或 1 表示不卖或卖),这是针对该特定输入的问题解决方案。
我们通常用符号 x 表示输入变量;一系列输入变量通常表示为一个向量
。输出变量用符号 y 表示。整体目标函数通常表示为 y = f(
)。我们经常使用下标来表示向量的各种元素 (x[0],x[1],⋯,x[i]),并使用上标来表示输入实例,如 y^((0)) = f(
^((0))), y^((1)) = f(
^((1))), ⋯, y^((i)) = f(
^((i)))。但在某些情况下,我们将使用下标来表示训练数据的不同项。具体用法应从上下文中明显看出。
数值量可以以两种不同的形式出现:连续和分类。连续变量可以取给定范围内的无限多个实数值。例如,在我们“卖或不卖股票”的问题中,股票价格可以取任何大于零的值。分类变量可以取一组有限允许值中的一个,其中值代表一个类别。一个特殊的分类情况是二元变量,其中只有两个类别。例如,在我们股票销售问题中的专家建议只能取两个值:0 或 1,分别对应建议的两个类别,“不卖”和“卖”。
在本节中,我们讨论三种不同的目标函数家族:逻辑函数、通用函数和分类器。
7.2.1 实际问题中的逻辑函数
这些是输入和输出都是二元变量的函数:只能取两个值,0(即“否”或“不触发”)和 1(即“是”或“触发”)。模拟逻辑函数的机器通常被添加到执行其他任务的独立机器之上,以下例子将清楚地说明这一点:
- 逻辑或—要了解逻辑或函数,让我们回顾一下第一章中讨论过的那个神话中的猫。假设我们正在尝试构建一台机器,帮助这只可怜的生物做出二进制决策:是逃跑还是接近面前的事物并发出咕噜声。由于非常胆小,这只猫会避开任何看起来坚硬或尖锐的东西。只有当面前的事物既不坚硬也不尖锐时,它才会靠近并发出咕噜声。让我们假设有一个独立的机器输出一个二进制决策 0(不坚硬)或 1(坚硬)。另一个机器输出一个二进制决策 0(不尖锐)或 1(尖锐)。逻辑或机器将两个独立机器的二元决策结合起来,如图 7.1a 所示。

(a) 逻辑或:一只胆小的猫,如果面前的事物硬度超过阈值 t[0] 或尖锐度超过阈值 t[1] 就会逃跑

(b) 逻辑与:一只不那么胆小的猫,它会避开硬度超过阈值t[0] 且尖锐度超过阈值t[1]的东西

(c) 多输入逻辑或:如果自动驾驶汽车看到前方有行人、车辆或道路弯曲,它就会刹车
图 7.1 逻辑运算符(或、与)在现实生活中的例子
-
逻辑与—我们也在猫脑的例子中展示了这一点。想象一只稍微不那么胆小的猫,它会避开又硬又尖的东西。但它不仅仅害怕硬和尖。它的脑部可以被图 7.1b 中显示的机器系统所模拟。
-
逻辑非—考虑一个机器,如果它看到任何未经授权的人进入受限区域,就会发出警报。假设我们还有一个独立的机器:一个面部识别器,它可以识别所有授权人员的面孔。它会发出一个二进制决策 1(识别到的面孔)或 0(未识别到的面孔)。整个系统接收面部识别器的输出,并对其执行逻辑非操作。
-
多输入逻辑或—想象一个机器,它决定自动驾驶汽车是否需要刹车。假设有三个独立的探测器,如果汽车前方看到人、车辆或道路弯曲,它们分别发出 1。如果这些独立的探测器中的任何一个发出 1,就必须刹车。这如图 7.3 所示。
-
多输入逻辑与—考虑一个帮助风险投资家决定是否投资初创企业的机器。假设有三个独立的机器,当以下条件满足时,它们会发出 1:(1)CEO 有成功的记录,(2)产品引起了目标客户的兴趣,(3)产品足够新颖。如果所有三个独立的机器都发出 1,机器将决定投资。因此,当条件(1)满足且条件(2)满足且条件(3)满足时,机器输出 1。这是一个三输入与的例子。
-
逻辑异或—假设我们正在构建一个社交媒体网站。假设我们有一个独立的探测器,对于任何一个人,如果他们喜欢摇滚音乐,就发出 1,否则发出 0。使用关于两个人的这些信息,问题是要决定他们是否应该被推荐为彼此的朋友。如果他们两人都喜欢摇滚音乐或都讨厌它,那么友谊潜力就很高。但如果一个人喜欢摇滚而另一个人不喜欢,他们可能不是好朋友。因此,条件 1 是第一个人对摇滚音乐的亲和力高,条件 2 是第二个人对摇滚音乐的亲和力高。当其中一个条件为真而另一个不为真时,这两个条件的异或为 1。这个机器在异或的非为真时输出 1,这意味着两个人都不喜欢摇滚音乐或两个人都喜欢摇滚音乐。图 7.4 描述了这一点。
-
m-out-of-n 触发器——想象我们正在尝试创建一个人脸检测器。我们已经有鼻子、眼睛、嘴唇和耳朵的独立部分检测器。如果我们检测到,比如说,这些部分中的任意两个一起,我们就足够自信地宣布检测到人脸。在计算机视觉中,我们经常遇到遮挡问题,其中一个重要对象因为另一个对象挡住了摄像机的视线而变得对摄像机不可见。计算机视觉算法总是试图对遮挡具有鲁棒性,这意味着即使在遮挡发生时,它们也希望输出正确的结果。这就是为什么我们不希望强制所有部分检测器都发出正信号;我们希望在部分遮挡的情况下也能检测到人脸。因此,当,比如说,检测到n个部分(如眼睛和嘴唇)中的两个时,我们的机器输出 1。

图 7.2 逻辑非和异或在实际问题中的应用示例。一个社交媒体系统只有在 A 和 B 两人都喜欢或都不喜欢摇滚音乐时,才会向他们推荐友谊。友谊 = ¬ (A 的摇滚音乐亲和力 ⊕ B 的摇滚音乐亲和力),其中 ¬ 表示逻辑非,⊕ 表示逻辑异或。

(a) 判别式人脸检测器分类器)。输出是分类的(人脸或非人脸)。

(b) 生成式人脸检测器。输出是连续的(图像包含人脸的概率)。
图 7.3 人脸检测器以图像为输入,输出一个分类的或连续的变量。
7.2.2 实际问题中的分类函数
分类器是一个输出为分类的函数。输入可以是连续的或分类的。因此,给定一个输入,函数会选择一个类别(即类)或另一个类别。例如,人脸检测器可以是一个分类器。它的输入是一个图像,它的输出是一个分类的(二元)变量,可以取两个可能的值之一:1(人脸)或 0(非人脸)。这如图 7.3a 所示。正如我们在第 2.3 节中看到的,任何图像都可以表示为一个向量
。因此,人脸检测器的分类函数可以写成如下函数

如何设计函数ϕ(
)是本章的主要话题之一。
几何上,每个标量输入变量在输入空间中形成一个单独的维度。所有这些标量输入变量的所有可能组合共同形成一个多维空间,称为输入空间(或特征空间)。每个特定的输入值组合是这个空间中的一个点(由输入向量
表示)。
例如,在图像中,每个像素可以被视为一个单独的输入标量变量,它可以取任何介于 RGB =
,
,
(十六进制) (黑色) 和 RGB =
,
,
(十六进制) (白色) 之间的 3 字节像素颜色值。输入的维度与图像中的像素数量相同。例如,一个 224 × 224 的图像形成一个 50, 176 维的输入空间。每个特定的图像是这个空间中的一个点。面分类函数 ϕ(
) 将该点映射到 0(非面部)或 1(面部)。
对于一个更简单的例子,考虑我们熟悉的来自第 1.4 节的猫脑示例。有两个输入变量:x[0],表示 硬度;和 x[1],表示 尖锐度。整体输入空间是二维的,其中特定的输入组合由 2D 向量
表示。我们的目标是构建一个机器,给定任何硬度与尖锐度的输入组合,将其分类为 威胁性 或 非威胁性。这相当于设计一个函数,将任意输入向量
∈ ℝ² 映射到 0 或 1:

分类器的几何视图:决策边界
从几何学的角度来看,分类器将特征空间划分为不同的区域,每个区域对应一个类别。例如,考虑第 1.4 节中的简单猫脑模型。有两个输入变量,硬度 (x[0]) 和尖锐度 (x[1])。因此,我们有一个二维的输入特征空间,在几何上对应于一个平面。硬度与尖锐度的每一种组合都由一个特定的向量
= [x[0], x[1]] 表示,对应于平面上的一个 点。请注意,与图 7.1a 和 7.1b 中显示的机器不同,这里我们谈论的是一个以硬度与尖锐度——即二维特征平面上的一个点——作为输入的机器,并将其映射到对应于 威胁 与 非威胁 的分类决策的离散空间。这如图 7.4a 所示。
实线将 威胁 和 非威胁 区域分开。这种将属于不同类别的输入空间中的区域分开的曲线称为 决策边界。估计决策边界实际上等同于构建分类器。

(a) 猫脑威胁模型决策边界。实线曲线对应于分隔威胁和非威胁区域的真实决策边界。虚线代表一个近似的线性决策边界:它正确分类了大多数点,但错误地将实线和虚线之间的点分类。
在实践中,我们通过手动标记从每个区域获取一些样本点。

(b) 好的训练数据。来自每个类别的样本点大致覆盖了属于该类别的输入空间区域。这导致了一个良好的决策边界(实线)。

(c) 坏的训练数据。来自单个类别的样本点没有覆盖属于该类别的输入空间区域。这导致了一个不良的决策边界(虚线)。
图 7.4 分类器、决策边界和训练数据。来自不同类别的数据点用不同的符号(加号和点)标记。
虚线代表一个近似的线性决策边界,它粗略地完成了工作,但错误地将实线和虚线之间的点分类。(线性决策边界更容易用神经网络表示,但它们对于复杂问题是不够的。)
让我们简要地看一下图 7.4a 中的实线曲线。在硬度值较低时,尖锐度阈值较高(如果猫前面的物体不是很硬,它必须非常尖锐才能被认定为威胁)。从x[0] = 0 增加到x[0] = 20,这个阈值(作为威胁所需的尖锐度)大致线性下降。超过x[0] = 20,阈值以更快的速度下降——如果猫前面的物体足够硬,它不需要非常尖锐就能构成威胁。超过x[0] = 52 左右,尖锐度就不再重要了:足够硬的物体即使不尖锐也是威胁。这本质上是一个非线性情况。
为了简化神经网络实现,我们可能想要用直线来近似实线曲线——虚线并不是一个太差的近似,但这样做会引入误差。如图 7.4a 所示,真实曲线和近似曲线之间的区域将被错误分类。
图 7.4a 只是一个示意图。在现实中,我们并不知道输入空间中与感兴趣类别相对应的确切区域。我们通过人工标注识别了一些输入空间上的样本点,以及它们的正确类别(即真实情况)。这样的样本集,即 <输入点,正确输出也称为真实情况> 对,被称为 训练数据。猫脑问题的示例训练数据集如图 7.4b 所示(来自不同类别的真实情况训练数据点用不同的符号标记:加号和点)。我们通过训练神经网络创建的决策边界被优化以尽可能好地分类训练数据点(仅此而已)。如果训练数据点的分布是真实分布的合理反映——也就是说,每个类别的样本点大致覆盖了输入空间中对应类别的整个区域——那么在该数据集上训练得到的决策边界将是好的。但如果,如图 7.4c 所示,训练数据没有反映输入空间中类别的真实分布,那么在该数据上训练得到的决策边界可能是不好的。
与猫脑示例不同,大多数现实生活中的输入空间有数百甚至数千个维度。决策边界作为超曲面的想法在更高维度中仍然成立。对于更高维度的输入空间,超平面充当线性分离器。在更高维输入空间的简单问题中,这样的线性分离器就足够了。在更复杂的情况下,我们可以有其他曲线超曲面作为非线性分离器。我们可能无法在脑海中可视化超空间,但我们可以用三维类似物来形成心理图像。图 7.5 显示了三维输入空间中的一些平面决策边界。

(a) 训练数据。来自每个类别的输入空间区域的样本点

(b) 差的决策边界。平面方向错误。
需要修正。

(c) 差的决策边界。平面有正确的方向但位置错误。b 需要修正。

(d) 最佳决策边界。平面有正确的
,b。适当训练。
图 7.5 具有线性决策边界超平面的分类器)。这种决策边界是通过第 7.3.3 节中介绍的感知器创建的。来自不同类别的数据点用不同的符号标记(加号和点)。
图 7.5a 展示了输入向量的三维空间以及一组训练数据点。任务是将其分类为两个类别。在这种情况下,训练点可以通过超平面决策边界进行划分。图 7.5b 和 7.5c 展示了一些划分训练数据不佳的平面,而图 7.5d 展示了一个最优的平面分离器。这些平面之间的唯一区别是
和 b 的值。这表明存在
和 b 的值,这些值可以最优地划分训练数据。这些最优值是通过称为 训练 的过程确定的,我们将在下一章详细讨论。
符号的重要性:决策边界的数学表达式
在输入向量
的空间中,ϕ(
) = 0 的方程代表一个表面。如果空间是二维的,表面就变成了一条曲线。例如,图 7.4b 中的实线虚线可以看作是 ϕ(
) ≡
^T
+ b = 0 的特殊情况,在这种情况下变为

那是,

在三维空间中,我们有平面和球面等表面。在超过三个维度的情况下,我们有超平面、超球面等等。例如,图 7.6 中的平面对应于

方程 7.1
那是,

在 3.1.4 节中,我们了解到,对于空间中的任意点
,ϕ(
) 的符号告诉我们点
属于 ϕ(
) = 0 的表面哪一侧。因此,如果我们估计与决策边界对应的表面,给定任意点,我们可以确定该点属于哪个划分。换句话说,我们可以对点进行分类。估计决策边界等同于构建分类器。例如,图 7.4b 中的线对应于 0.62x[0] + x[1] − 26.14 = 0。0.62x[0] + x[1] − 26.14 < 0 的点位于一侧,并用点表示。0.62x[0] + x[1] − 26.14 > 0 的点位于另一侧,并用加号(+)表示。
这个想法可以扩展到更高维度。图 7.6 展示了在 3D 输入空间中的相同概念。平面对应于方程x[0] + x[1] + x[2] = 0。x[0] + x[1] + x[2] < 0 的点位于一个侧面(如图 7.6 中的-所示),而x[0] + x[1] + x[2] > 0 的点位于另一侧面(如图 7.6 中的+所示)。

图 7.6 ϕ(
)的符号意义 ≡
^T
+ b。注意,ϕ(
) = 0 意味着
位于平面上,ϕ(
)为负意味着平面的一个侧面,ϕ(
)为正意味着平面的另一侧面。
7.2.3 现实世界问题中的通用函数
存在一些问题,分类输出变量不足以解决问题,需要连续输出变量:例如,估计自动驾驶车辆应该行驶的速度。使用诸如正在穿越的道路的速度限制、相邻车辆的速度等信息作为输入,我们需要估计自动驾驶车辆应该行驶多快。
另一个需要注意的情况是,当我们需要将输出变量建模为连续变量而不是分类变量时,例如,当我们建模某个事件发生的概率时。例如,再次考虑人脸检测器。给定一个图像作为输入,人脸分类函数输出 0 表示“不是人脸”,输出 1 表示“是人脸”。这样的函数被称为“判别函数”。我们也可以有一个输出图像包含人脸概率的函数。这样的函数被称为“生成函数”,一个例子如图 7.6 所示。
7.3 基本构建块或神经元:感知器
在 7.2 节中,我们了解到大多数现实世界问题都可以表示为函数。这是一个好消息,但坏消息是,这些函数通常是未知的,而且各种问题背后的函数彼此之间差异很大。虽然可能估计它们,但如果我们不采用通用框架而单独攻击它们,几乎没有希望开发出可重复的解决方案。
神经网络提供了一个有效的框架,可以机械地模拟极其广泛的复杂函数。此外,目标函数不必是封闭形式的——样本输入输出对就足够了。它们可以通过连接相当简单的构建块实例(不出所料地称为神经元)来表示(建模)非常复杂的函数。换句话说,完整的神经网络可以具有巨大的表达能力,尽管单个神经元非常简单。稍后,在第 7.3.4、7.4、7.5 等节中,我们将讨论神经网络如何模拟越来越复杂的函数。但首先,在本节中,我们检查构建块:神经元。
7.3.1 海维塞德阶跃函数
海维塞德阶跃函数,通常简称为阶跃函数,是一个对于负数参数取值为 0,对于正数参数取值为 1 的函数:

方程 7.2
方程 7.2 等价于以下算法。图 7.7 显示了此方程的图形。
算法 7.4 海维塞德阶跃函数作为算法
if x < 0 then
返回 0
else
返回 1
结束 if

图 7.7 海维塞德阶跃函数图
7.3.2 超平面
在第 2.8 节中,我们讨论了超平面。它们由方程 2.14 表示。在第 2.9 节的图中,我们看到了超平面在分类器中的作用;我们在这里简要回顾这个概念。
在第 2.1.1 节中,我们看到了d-元素向量是d-维空间中点的几何类比。用
表示输入向量空间中的向量(或等价地,点)。对于固定的向量
和固定的标量 b,该空间中超平面的方程是
^T
+ b = 0
(意味着满足此方程的所有点
都位于平面上)。向量
是平面的法线。当我们观察到如果我们取平面上的任意两点,比如说
[0] 和
[1],那么
^T
[0] + b = 0
^T
[1] + b = 0
相减,我们得到
^T(
[1] -
[0]) = 0
但 (
[1] -
[0]) 是连接平面上两个任意点的向量。这意味着连接平面上任何一对点的线垂直于
*(在第 2.5.2 节中,我们讨论了点积,在第 2.6 节中,我们看到了如果两个向量的点积为零,则这两个向量是正交的——相互垂直)。因此,
垂直于平面上的所有线。换句话说,
垂直于平面。
超平面
^T
+ b = 0 将空间划分为两个区域,这两个区域的
^T
+ b 表达式的符号不同。也就是说,超平面可以作为决策边界,如图 7.6 所示。不仅超平面,任何超曲面都可以以这种方式划分空间。这对于任何维度的
都成立:
-
如果表达式
^T
+ b 的值为 零,则点
位于 超平面
^T
+ b = 0 上。 -
如果表达式
^T
+ b 的符号为 负,则点
位于 超平面 的 一侧。 -
如果表达式
^T
+ b 的符号为 正,则点
位于 超平面 的 另一侧。
7.3.3 感知器和分类
感知器将步进函数和超平面结合成一个单一函数。它表示的函数
P(
) ≡ ϕ(
^T
+ b)
方程 7.3
其中 ϕ 是来自方程 7.2 的 Heaviside 步进函数。结合我们在第 7.3.1 和 7.3.2 节中的见解,我们可以看到感知器函数将 (
, b) 平面一侧的所有点映射到零,并将同一平面另一侧的所有点映射到 1。换句话说,它作为一个线性分类器,将 (
, b) 平面作为决策边界。图 7.8 绘制了 2D 输入空间的感知器函数(该图本身是在 3D 空间中的:它将决策边界一侧的点映射到 Z = 0 平面,并将另一侧的点映射到 Z = 1 平面)。

图 7.8 虽然输入空间是 2D 的,但感知器图是 3D 的。决策边界由长对角线直线表示。它将决策边界一侧的点映射到 Z = 0 平面,另一侧的点映射到 Z = 1 平面。
当然,在现实生活中,我们不知道与类别对应的精确区域。相反,我们采样了带有手动标记类别的输入点作为训练数据。决策表面必须仅基于此训练数据构建。在图 7.4 中,我们看到了一个二维示例决策边界和一些好的和坏的训练数据集。为了在更高维度的采样训练数据集中获得心理图像,请再次查看图 7.5a。在这种情况下,单个超平面决策边界足以划分训练点。这意味着单个基于感知器的神经网络就足以作为分类器。图 7.5b 和 7.5c 描述了一些(具有特定 (
, b) 值的感知器)划分训练数据不佳的平面。图 7.5d 显示了一个最优感知器(平面分离器)。这告诉我们,存在最优的
和 b 值,可以最优地划分训练数据,以及不良的值。如前所述,好的值是通过训练确定的,我们将在第八章中介绍。
感知器有效地使用 平面 决策表面进行划分。这仅在简单问题中有效。对于平面决策表面将 不会 适用的情形,请参见图 7.9。它描述了一个问题,其中一个类别映射到两个平面之间的点集(标记为 +),另一个类别映射到输入空间中的其余点。使用单个平面无法实现所需的划分,因此无法用单个感知器来模拟这种决策边界。稍后我们将看到如何使用多个感知器来模拟这种复杂的决策边界。

图 7.9 多平面决策边界。一个类别对应于两个平面之间的区域中的点(标记为 +)。其余的点对应于其他类别(标记为 –)。这种决策边界 不能 用单个平面表示。
注意:感知器的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/9Ne7 找到。
以下列表展示了感知器的 PyTorch 代码。
列表 7.1 感知器
def fully_connected_layer(X, w, b): ①
X = torch.cat((X, torch.ones(
[X.shape[0], 1], dtype=torch.float32)), dim=1) ②
W = torch.cat((W, b.unsqueeze(dim=1)), dim=1) ③
y = torch.matmul(W, X.transpose(0, 1)) ④
y = torch.heaviside(y, torch.tensor(1.0)) ⑤
return y.transpose(0, 1)
def Perceptron(X, W, b): ⑥
return fully_connected_layer(X, W, b)
① X : n × d 张量;每一行是一个大小为 d 的输入向量。w : m × d 张量。
② 添加一列 1。X ↦ n × (d+1) 张量。
③ 结合权重和偏差
④ X 和 W 的矩阵乘法
⑤ 应用 Heaviside 阶跃函数
⑥ 单个感知器
7.3.4 使用感知器建模常见逻辑门
神经网络通过连接称为感知器的简单构建块的重复实例(通过加权边连接)提供了一种结构化的方式来建模复杂函数。在本节中,我们探讨了通过感知器进行函数建模的想法。我们首先从使用单个感知器表示的极其简单的逻辑函数(与、或、非、投票)建模开始。然后我们来看 XOR 函数,这是不能使用单个感知器表示的简单函数之一;我们看到它可以用多个感知器来建模。接下来我们讨论 Cybenko 定理,该定理表明,大多数感兴趣的函数可以通过感知器以我们想要的任何精度进行建模,输入和输出之间有一个隐藏层。不幸的是,这比听起来要少一些实际应用:关键在于,尽管任何函数都可以建模到任何精度,但建模所需的感知器数量没有限制。目标函数越复杂,所需的感知器就越多。在实践中,我们通常使用多层而不是一层。
感知器用于逻辑与
图 7.18 展示了这个感知器。它接受两个输入,x[0]和x[1],分别用w[0] = 1 和w[1] = 1 加权;偏置为-1.5(实际上,广泛的偏置值都可以)。总的来说,感知器实现了函数ϕ(x[0] + x[1]−1.5)。当x[0] + x[1] − 1.5 ≥ 0 时,该函数发出 1:也就是说,x[0] + x[1] ≥ 1.5。由于变量是二元的(意味着它们只能取 0 或 1 的值),这只有在两个输入都是 1 的情况下才会发生。如果其中一个是零,它们的和就小于 1.5,y就是 0。

(a) 逻辑与函数的感知器:y = x[0] ∨ x[1]

(b) 逻辑或函数的感知器:y = x[0] ∧ x[1]

(c) 逻辑非函数的感知器:y = ¬x[0]
图 7.10 中的函数是二元的,意味着它们可以是 0 或 1。
这种情况在图 7.11a 中用几何方式表示。粗对角线对应于x[0] + x[1] ≥ 1.5。它将平面分为无阴影和阴影部分。无阴影半平面的所有点输出值y = 0,阴影半平面的所有点输出值y = 1。只有四个可能的输入点:(x[0] = 1, x[1] = 1),(x[0] = 1, x[1] = 1),(x[0] = 1, x[1] = 1),(x[0]=1, x[1] = 1)。点(x[0]=1, x[1] = 1)位于阴影侧,其他点位于无阴影侧——这正是逻辑与函数。

(a) 逻辑与决策边界:x[0] + x[1] = 1.5

(b) 逻辑或决策边界:x[0] + x[1] = 0.5

(c) 逻辑非决策边界:x[0] = 0.5
图 7.11 展示了图 7.10 中的感知器的几何视图。每个点代表一个输入点([x[0] x[1]] 矢量)。阴影点映射到输出值为 1,无阴影点映射到输出值为 0。粗线表示决策边界。
逻辑或感知器
图 7.10b 展示了该感知器。它接受两个输入,x[0] 和 x[1],分别乘以 w[0] = 1 和 w[1] = 1,偏置为 −0.5。总体而言,感知器实现了函数 ϕ(x[0] + x[1]−0.5)。当 x[0] + x[1] − 0.5 ≥ 0 时,该函数输出 1:即 x[0] + x[1] ≥ 0.5。由于变量是二进制(0 或 1),这只有在输入为 1 或两个输入都为 1 时才会发生。只有当两者都为零时,它们的和为零(小于 0.5),并且 y 为 0。
这种情况在图 7.11b 中以几何方式表示。粗线对应于 x[0] + x[1] ≥ 0.5,将输入平面分为无阴影的半平面(该半平面上的所有点输出 y = 0)和有阴影的半平面输出 y = 1。在四个可能的输入点中,(0,0)位于无阴影的一侧(y = 1),其余三个位于阴影一侧(y = 1),这正是逻辑或函数。
逻辑非感知器
该感知器在图 7.10c 和 7.11c 中展示,现在应该已经很直观了。
注意:使用感知器建模各种逻辑门的全功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/jBRr 找到。
列表 7.2 使用感知器建模逻辑门
# Logical AND
X = torch.tensor([[0., 0.], ①
[0., 1.],
[1., 0.],
[1., 1.]], dtype=torch.float32)
W = torch.tensor([[1.0, 1.0]], dtype=torch.float32) ②
b = torch.tensor([-1.5]) ③
Y = Perceptron(X=X, W=W, b=b, activation=torch.heaviside) ④
# Logical OR
X = torch.tensor([[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.]], dtype=torch.float32)
W = torch.tensor([[1.0, 1.0]], dtype=torch.float32)
b = torch.tensor([-1.5])
Y = Perceptron(X=X, W=W, b=b, activation=torch.heaviside)
# Logical NOT
X = torch.tensor([[0],
[1.]
], dtype=torch.float32)
W = torch.tensor([[-1.0]], dtype=torch.float32)
b = torch.tensor([0.5])
Y = Perceptron(X=X, W=W, b=b, activation=torch.heaviside)
① 输入数据点
② 实例化权重
③ 实例化偏置
④ 输出
7.4 向更强大的表达力:多层感知器(MLPs)
有一个非常简单的逻辑函数,出人意料的是,不能用单个感知器来建模:异或。我们现在来讨论它。

(a) 逻辑异或感知器的几何视图。决策边界有 两条 线,因此使用单个感知器是不可能的。

(b) 逻辑异或函数的 MLP。注意,权重和偏置在括号中有上标,表示层索引。这是一个两层网络。层 0 是隐藏层。
图 7.12 逻辑异或:几何和感知器视图
7.4.1 逻辑异或的 MLP
图 7.12a 显示了平面上可能的四个输入点以及如何划分平面以建模 XOR 函数。点(0,0)、(1,1)(未着色)都映射到输出 0,应该在决策边界的同一侧,而点(0,1)、(1,0)(着色)映射到输出值 1,应该在决策边界的另一侧。
很容易看出,在这个平面上不可能画一条直线,使得着色点在一侧,未着色点在另一侧。记住,感知器本质上引入了一个线性决策边界。因此,不可能有一个单独的感知器来模拟这个函数。
然而,通过多个感知器可以建模逻辑 XOR 函数。其中一个这样的模型在图 7.12b 中展示。
7.5 感知器的分层网络:MLP 或神经网络
XOR 示例告诉我们,我们无法仅用单个感知器做很多事情。我们必须将多个感知器连接成一个网络来解决实际问题。这是一个神经网络。我们如何组织这样一个连接的感知器网络?
7.5.1 分层
分层是将感知器组织成神经网络的流行方式。图 7.12b 是我们第一个 MLP 的例子——本书的其余部分大部分都在讨论 MLP。
注意 XOR 网络(图 7.12b)中的感知器是如何组织的:
-
层从输入到输出用递增的整数编号。
-
层i中感知器的输出仅作为输入提供给层i + 1 中的感知器。不允许其他连接。这使网络易于管理,并便于通过称为反向传播的技术在训练期间更新权重,我们将在下一章讨论这一技术。
-
除了最后一层之外的所有层的输出都是不可见的(不直接贡献于输出)。这样的层被称为隐藏层。在图 7.12b 中,层 0 是隐藏的。
-
每个权重和偏置元素只属于一个层。在本书中,我们用括号内的上标表示权重或偏置元素的层索引。
-
具有两个或更多隐藏层的 MLP 可以称为深度神经网络。这是“深度学习”中“深度”一词的起源。
7.5.2 使用 MLP 建模逻辑函数
任何逻辑函数都可以表示为真值表。因此,如果我们能证明所有真值表都可以通过 MLP 实现,我们就完成了。这是我们采取的方法。
注意:在以下讨论中,两个变量之间的符号(如乘法)不表示逻辑与,而加号表示逻辑或。
表 7.1 逻辑函数y = x̄[0]x[1] ¸ x[0]x̄[1]的真值表
| x[0] | x[1] | y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
让我们从简单的二维逻辑函数 y = x̄[0]x[1] + x[0]x̄[1] 开始。表 7.1 展示了相应的真值表。为了创建等效的 MLP,我们必须选择对应于 y = 1 的行。每一行都可以表示为输入变量或其补码的 AND。例如,行 x[0] = 0 和 x[1] = 1,y = 1 对应于 x̄[0]x[1]——我们试图实现的函数的第一个项——可以通过图 7.13a 中显示的感知器实现。行 x[0] = 1 和 x[1] = 0,y = 1 对应于 x[0]x̄[1]——我们试图实现的函数的第二个项——可以通过图 7.13b 中显示的感知器实现。我们已经实现了函数的各个项;剩下的只是将它们 OR 到一起形成一个最终的 MLP,如图 7.13c 所示。这个逻辑函数是我们熟悉的逻辑 XOR,图 7.13 中的整体函数与图 7.12 相同。以这种方式,可以使用 MLP 对任意数量的变量中的任意逻辑表达式进行建模。

(a) 对于 x̄[0]x[1] 的感知器

(b) 对于 x[0]x̄[1] 的感知器

(c) 对于 x̄[0]x[1] + x[0]x̄[1] 的 MLP
图 7.13 对应于表 7.1 的逻辑函数的 MLP
列表 7.3 多层感知器 (MLP)
def MLP(X, W0, W1, b0, b1): ①
y0 = fully_connected_layer(X=X, W=W0, b=b0)
return fully_connected_layer(X=y0, W=W1, b=b1)
① MLP
7.5.3 Cybenko 的通用近似定理
任何在区间 x ∈ (a, b) 上连续的函数 y = f(x) 都可以用该区间内的一组塔(垂直矩形)来近似。这是微积分中积分平均值定理的直接结果。这一想法在图 7.14 中得到展示,其中复杂函数(由曲线表示)被一系列不同高度的塔近似。塔越细,塔的数量越多,近似就越准确。

图 7.14 使用塔近似复杂函数
在 7.5.3.1 节中,我们展示了任何塔(具有任意高度和位置)都可以用 MLP 构建出来。通过将这些 MLP 相加,我们可以近似整个函数。这就是 Cybenko 定理的精髓。
注意:尽管 Cybenko 定理保证了任何连续函数都可以使用具有单个隐藏层的 MLP 进行建模,但该 MLP 中的感知器数量可能变得任意大而不切实际。这就是为什么在现实生活中,我们很少尝试用单个隐藏层来近似复杂函数。我们稍后会看到,额外的层可以帮助我们减少所需的感知器数量。
特别是,任何决策边界都可以用这种方式进行建模。当然,对于许多问题,所需的感知器数量可能变得无法实现,使得这种模型实际上无法实现。

(a) ϕ(x):正w产生正常步进

(b) 正常步进的感知器

(c) ϕ(−x):负w产生横向翻转步进

(d) 翻转步进的感知器

(e) ϕ(−x+5):改变偏置将步进向左或向右移动

(f) 横向移动的步进感知器

(g) (ϕ(x+5)+ϕ(−x+5)−1.5):将左移的步进与翻转的右移步进进行 AND 操作得到一个塔

(h) 1D 塔的 MLP
图 7.15 使用感知器生成 1D 塔

(a) 沿x[0] (x)方向的 2D 步进函数

(b) 沿x[0] (x)方向的 2D 步进函数感知器

(c) 沿x[0] (x)方向的 2D 步进函数方程

(d) 沿x[0] (x)方向的翻转 2D 步进

(e) 沿x[0] (x)方向的翻转 2D 步进感知器

(f) 沿x[0] (x)方向的翻转 2D 步进方程

(g) 沿x[0] (x)方向的 2D 波

(h) 沿x[0] (x)方向的 2D 波 MLP

(i) 沿x[0] (x)方向的 2D 波方程

(j) 沿x[1] (y)方向的 2D 波

(k) 沿x[1] (y)方向的 2D 波 MLP

(l) 沿x[1] (y)方向的 2D 波 MLP
图 7.16 使用感知器生成 2D 步进和波
使用 MLP 生成塔
基本思想在图 7.15 中展示。我们可以明显地通过实现 y = ϕ(x) 的感知器生成一个规则的步长。相应的图示在图 7.15a 中。通过赋予一个偏置 5,我们可以将这个步长向左移动。相应的函数是 y = ϕ(x+5)。此外,使用负权重可以在水平方向上翻转步长。相应的函数是 y = ϕ(−x),其图示在图 7.15c 中。通过赋予一个偏置 5,我们可以将翻转的步长向右移动。图 7.15e 展示了翻转并右移的步长,对应于函数 y = ϕ(−x+5),其感知器在图 7.15f 中展示。逻辑与一个左移的步长与一个翻转并右移的步长在 1D 中产生一个塔。这对应于函数 y = ϕ(ϕ(x+5)+ϕ(−x+5)−1.5),其图示在图 7.15g 中。
同样的思想也适用于更高维度的输入。我们可以使用方程 7.4 生成一个沿 x[0] 方向对齐的两个变量(2D 步长)。这个方程的图示在图 7.16a 中,实现该方程的感知器在图 7.16b 中展示。相同步长的翻转版本可以通过方程 7.5 生成。这个方程的图示在图 7.16d 中,感知器的实现展示在图 7.16e 中。
在 1D 情况下,我们通过结合一个规则的步长及其翻转和移位的版本来生成一个塔。在 2D 中,这个过程稍微复杂一些。在这里,通过将一个特定坐标轴上的步长与其翻转和移位的版本结合,可以生成该轴上的波函数。因此,每个维度都有单独的波。沿 x[0] 轴的波对应于方程 7.6;其图示在图 7.16g 中。它由图 7.16h 中的 MLP 实现。同样,沿 x[1] 轴的 2D 波可以通过方程 7.7 生成,并在图 7.16j 中绘制。相应的 MLP 在图 7.16k 中展示。
要创建一个塔,我们必须在两个独立的维度上对一对波进行逻辑与操作。最终的塔函数在方程 7.8 中展示;相应的塔图示在图 7.17a 中;MLP 在图 7.17b 中展示。任何连续的 2D 表面都可以通过组合这样的 2D 塔来近似到任意精度:

(a) 2D 塔

(b) 2D 塔的 MLP(方程 7.8)
图 7.17 使用感知器生成 2D 塔

方程 7.4

方程式 7.5

方程式 7.6

方程式 7.7

方程式 7.8
注意:使用感知器近似表面的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/WrKa 找到。
列表 7.4 1D 中的感知器和 MLP
x = torch.linspace(start=-10, end=10, steps=100) ①
# 1D S curves - positive weight ②
w = torch.tensor([1.0], dtype=torch.float32)
b = torch.tensor([0.0])
y = Perceptron(X=x.unsqueeze(dim=1), W=w.unsqueeze(dim=1), b=b)
# 1D S curves - negative weight + shift\quad ③
w = torch.tensor([-1.0], dtype=torch.float32)
b = torch.tensor([5.0])
y = Perceptron(X=x.unsqueeze(dim=1), W=w.unsqueeze(dim=1), b=b)
# 1D towers (Cybenko) - various W0\quad ④
W0 = torch.tensor([[1.0], [-1.0]], dtype=torch.float32)
b0 = torch.tensor([5.0, 5.0])
W1 = torch.tensor([[1.0, 1.0]], dtype=torch.float32)
b1 = torch.tensor([0.0])
y = MLP(X=x.unsqueeze(dim=1), W0=W0, W1=W1, b0=b0, b1=b1)
① 100D 数组
② 参见图 7.15a 和 7.15b。
③ 参见图 7.15e 和 7.15f。
④ 参见图 7.15g 和 7.15h。
列表 7.5 2D 中的感知器和 MLP
X = torch.linspace(start=-1, end=1, steps=100) ①
Y = torch.linspace(start=-1, end=1, steps=100) ①
gridX, gridY = torch.meshgrid(X, Y) ②
X = torch.tensor([(y, x) for y, x in
zip(gridY.reshape(-1), gridX.reshape(-1))) ③
# 2D Step function in X-direction ④
W = torch.tensor([[1.0, 0.0]], dtype=torch.float32)
b = torch.tensor([0.0], dtype=torch.float32)
Z = Perceptron(X=X, W=W, b=b)
# 2D Flipped Step function along X-direction ⑤
W = torch.tensor([[-1.0, 0.0]], dtype=torch.float32)
b = torch.tensor([0.0], dtype=torch.float32)
Z = Perceptron(X=X, W=W, b=b)
# 2D wave along X-direction ⑥
W0 = torch.tensor([[1.0, 0.0],
[-1.0, 0.0]], dtype=torch.float32)
b0 = torch.tensor([0.5, 0.5], dtype=torch.float32)
W1 = torch.tensor([[1.0, 1.0]], dtype=torch.float32)
b1 = torch.tensor([-1.0])
Z = MLP(X=X, W0=W0, W1=W1, b0=b0, b1=b1)
# 2D wave along Y-direction ⑦
W0 = torch.tensor([[0.0, 1.0],
[0.0, -1.0]], dtype=torch.float32)
b0 = torch.tensor([0.5, 0.5], dtype=torch.float32)
W1 = torch.tensor([[1.0, 1.0]], dtype=torch.float32)
b1 = torch.tensor([-1.0])
Z = MLP(X=X, W0=W0, W1=W1, b0=b0, b1=b1)
# 2D Tower ⑧
W0 = torch.tensor([[1.0, 0.0],
[-1.0, 0.0],
[0.0, 1.0],
[0.0, -1.0]], dtype=torch.float32)
b0 = torch.tensor([0.5, 0.5, 0.5, 0.5], dtype=torch.float32)
W1 = torch.tensor([[1.0, 1.0, 1.0, 1.0]], dtype=torch.float32)
b1 = torch.tensor([-3.5])
Z = MLP(X=X, W0=W0, W1=W1, b0=b0, b1=b1)
① 100D 数组
② 100 × 100 矩阵
③ 10,000 × 1 矩阵
④ 参见方程式 7.4 和图 7.16a 以及 7.16b。
⑤ 参见方程式 7.5 和图 7.16d 以及 7.16e。
⑥ 参见方程式 7.6 和图 7.16g 以及 7.16h。
⑦ 参见方程式 7.7 和图 7.16j 以及 7.16k。
⑧ 参见方程式 7.8 和图 7.17a 以及 7.17b。
7.5.4 多边形决策边界的 MLP
我们已经看到,分类器是神经网络的一个重要用例。在第 7.2.2.1 节中,我们还看到分类器本质上是在高维特征空间中建模决策边界。在本节中,我们通过一个固定多边形界定的简单类别来建模,以理解这个过程。
图 7.18a 显示了一个特征空间,其中要识别的类别对应于由四条直线界定的矩形区域(阴影部分):
x[0] = –5 x[0] = 5
x[1] = –2 x[1] = 2
这些线中的每一行都将特征空间划分为两个半平面,如图 7.18a 中的负号和正号所示。包含感兴趣类别特征点的区域由所有正号表示。

(a) 包围感兴趣类别的决策边界的示例特征空间(阴影部分)

(b) 仅在阴影区域内的点触发的 MLP
图 7.18 使用 MLP 建模矩形决策区域
对应于感兴趣类别的阴影区域是满足 x[0] ≥ − 5 AND x[0] ≤ 5 AND x[1] ≥ − 2 AND x[1] ≤ 2 的区域。现在考虑感知器 ϕ(x[0]+5)。它在 x[0] ≥ − 5 的区域触发(输出 1)。同样,感知器 ϕ(- x[0]+5)、ϕ(x[1]+2)和 ϕ(- x[1]+2)分别在 x[0] ≤ 5、x[1] ≥ − 2 和 x[1] ≤ 2 的区域触发。因此,通过逻辑上 AND 这些感知器的输出,我们得到一个只在感兴趣阴影区域触发的多层感知器(MLP)。图 7.53 展示了此 MLP。它实现了以下函数:

平面上的所有形状都可以用多边形来近似。因此,给定足够的感知器,平面上的任何形状都可以以任意精度描绘出来。
摘要
在本章中,我们概述了如何将各种现实世界问题建模为函数评估:
-
任何智能任务都可以通过一个函数来建模。特别值得注意的是分类任务,在这种任务中,给定一个输入,我们从预定的可能类别列表中估计输入所属的类别。例如,二元分类器可以将输入图像分为两类:包含人脸的和不包含人脸的。分类任务可以通过具有分类输出的函数来近似。
-
神经网络提供了一种结构化的方法来近似任意函数(包括分类函数)。这就是它们模仿智能的方式。
-
神经网络是通过组合一个称为感知器的基本构建块来创建的。感知器是一个简单的函数,它返回对输入加权总和(加上偏差)应用步进函数的结果。感知器实际上是一个线性分类器,它通过超平面将空间分为两个半空间。感知器的权重和偏差对应于超平面的方向和位置——它们可以被调整到尽可能多地分离对应于单个类别的区域。
-
单个感知器只能近似相对简单的函数,例如可以通过超平面分离特征点的分类器。感知器不能近似更复杂的函数,例如需要用曲面分离输入区域的分类器。多层感知器(MLP)是感知器的组合,其中一层感知器的输出作为下一层感知器的输入。神经网络本质上是一个 MLP,可以近似这样的任意复杂函数。
-
简单的逻辑函数,如 AND、OR 和 NOT,可以用单个感知器来模拟。逻辑 XOR 不能。对于 XOR 和其他复杂的逻辑函数,我们需要 MLPs。有一种机械的方法可以构建任何逻辑函数的 MLP。逻辑函数总是可以用真值表来表示。真值表的每一行可以看作是输入的逻辑 AND 函数,最终输出是输入的逻辑 OR。由于 AND 和 OR 可以用感知器来模拟,任何真值表都可以模拟为感知器的组合(一个 MLP)。
-
MLP 表示任意函数的能力被称为其表达能力。感知器数量和/或连接数量越多,神经网络的表达能力就越强。
-
Cybenko 定理证明了神经网络是一个通用逼近器(意味着它可以逼近任何函数)。基本思想是任何函数都可以被逼近到任意程度的精度,作为矩形(塔)的总和。该定理表明,可以使用 MLPs 在任何维度的空间中构建塔。
-
神经网络可以逼近平面上任何形状到任意精度。这是因为所有形状都可以用矩形逼近,并且我们可以用 MLPs 在平面上逼近矩形。
-
在现实生活中的问题中,对应于类别的区域是未知的,但我们会手动标记样本输入点带有期望的输出(真实值)来创建监督训练数据。我们调整权重和偏差,尽可能逼近训练数据。这个过程被称为训练。如果训练数据集不能很好地代表真实数据集,即使经过训练,神经网络也可能不准确。
第八章:训练神经网络:前向传播和后向传播
本章涵盖
-
Sigmoid 函数作为 Heaviside 步函数的微分替代
-
神经网络中的分层:将线性层表示为矩阵-向量乘法
-
回归损失、前向和后向传播及其数学
到目前为止,我们已经看到神经网络通过用数学函数建模决策过程来做出复杂的现实生活决策。这些函数可以变得非常复杂,但幸运的是,我们有一个简单的构建块,称为 感知器,可以系统地重复以建模任何任意函数。我们甚至不需要显式地知道正在建模的函数的闭式形式。我们需要的只是一个合理大小的样本输入集和相应的正确输出。这个输入和输出对的集合被称为 训练数据。有了这个训练数据,我们可以 训练 一个多层感知器(MLP,也称为神经网络),使其在从未见过的输入上产生合理的正确输出。
这种需要我们知道训练数据集中每个输入的输出的神经网络被称为 监督 神经网络。训练输入的正确输出通常是通过一种称为 标注 的手动过程生成的。标注既昂贵又耗时。许多研究正在进行中,旨在开发无监督、半监督和自监督网络,以消除或最小化标注过程。但到目前为止,无监督和自监督网络在一般情况下的准确性并不匹配监督网络。在本章中,我们专注于监督神经网络。在第十四章中,我们将研究无监督网络。
这个“训练”神经网络的过程是什么?它本质上估计了使网络在训练输入上发出尽可能接近已知正确输出的参数值。在本章中,我们将讨论这是如何完成的。但在那之前,我们必须学习一些其他的东西。
注意:本章的完整 PyTorch 代码以完全功能性和可执行的 Jupyter 笔记本形式,可在 mng.bz/YAXa 获取。
8.1 可微的类似步骤函数
在方程 7.3 中,我们将感知器表示为 Heaviside 步函数 ϕ 和仿射变换
^T
+ b 的组合。这是我们贯穿第七章所使用的感知器,并且我们能够表达(建模)几乎所有感兴趣的功能。
尽管 Heaviside 步进函数具有强大的表达能力,但它有一个缺点:它在 x = 0 处有间断,并且不可微分。为什么可微性很重要?正如我们将在第八章(并在第 3.3 节中略见一斑)中看到的那样,神经网络的优化训练需要评估损失函数相对于权重的梯度向量。由于梯度实际上是一个偏导数的向量,因此可微性对于训练是必需的。
在本节中,我们讨论一些可微分的函数,同时可以模拟 Heaviside 步进函数。其中最重要的是 S 形函数。
8.1.1 S 形函数
S 形函数以其特征的 S 形曲线图而命名 8.1)。相应的方程是

方程 8.1

图 8.1 1D S 形函数的图像
参数化 S 形函数
我们可以将方程 1.5 参数化为

方程 8.2
这使我们能够
-
通过改变 w 调整 S 曲线线性部分的陡峭程度
-
通过改变 b 调整曲线的位置
图 8.2 展示了参数化的 S 形曲线如何随着参数 w 和 b 的不同值而变化。特别是,请注意,对于大的 w 值,参数化的 S 形函数几乎与 Heaviside 步进函数无法区分(比较图 8.2a 中的虚线曲线与图 7.7),尽管它仍然是可微分的。这正是我们在神经网络中所期望的。

图 8.2 方程 8.2 中对应于各种参数值的 S 形曲线
S 形函数的一些性质
S 形函数有几个有趣的性质,其中一些性质在此列出,并附有证明概要。
- 正 x 的表达式:

方程 8.3
通过将方程 1.5 的分子和分母都乘以 e[x],可以轻松证明这个表达式。
- 负 x 的 S 形函数:

方程 8.4
- S 形函数的导数:

方程 8.5
图 8.3 显示了 S 形函数的导数图像叠加在 S 形函数图像本身上。正如预期的那样,导数在 S 形曲线的中间(S 形函数大致线性上升的地方)达到最大值,在两端(S 形函数饱和和平坦,几乎不变化的地方)接近零。

图 8.3 1D sigmoid 函数(实线)及其导数(虚线)的图
8.1.2 双曲正切函数

图 8.4 1D tanh 函数的图
sigmoid 函数的另一种选择是双曲正切函数 tanh,如图 8.4 所示。它与 sigmoid 函数非常相似,但输出值的范围是从[−1,1],而不是[0,1]。本质上,它是将 sigmoid 函数拉伸并平移,使其以 0 为中心。tanh 函数的方程式如下

方程式 8.6
为什么 tanh 比 sigmoid 更受欢迎?为了理解这一点,请考虑图 8.5。它比较了 sigmoid 和 tanh 函数的导数。如图所示,函数在x = 0 附近的导数(梯度)比 sigmoid 函数的导数要高得多。更强的梯度意味着收敛速度更快,因为权重更新是以更大的步骤发生的。请注意,这主要适用于数据集中在 0 附近的情况:在大多数预处理步骤中,我们在将其输入神经网络之前对数据进行标准化(使其均值为 0)。

图 8.5 1D sigmoid 和 tanh 函数的导数图
8.2 为什么分层?
在第 7.5 节中,我们遇到了将分层作为组织多个感知器首选方法的想法。分层网络的主要特性是任何层的神经元只从前一层的输出中获取输入。这意味着只存在连续层之间的连接。在多层感知器(MLP)中不存在其他连接,这极大地简化了网络的评估和训练,这一点在我们讨论前向传播和反向传播时将变得明显。
为什么要有层?我们已经看到,多个感知器使我们能够模拟单一生成器无法解决的问题(例如第 7.4.1 节中讨论的 XOR 问题)。从理论上讲,使用组织在单个隐藏层中的神经元可以模拟所有数学函数(因此解决所有可量化的问题)(参见第 7.5.3 节中 Cybenko 的定理和证明)。然而,这并不意味着单个隐藏层是进行所有建模的最有效方式。如果我们将它们组织在多个层中,我们通常可以用更少的感知器来模拟复杂问题。
为什么额外的层有帮助?主要原因是额外的非线性。每一层都引入它自己的非线性函数(例如 sigmoid)。通过适当的参数化,非线性函数可以模拟更复杂的函数。因此,模型中非线性函数的数量越多,通常意味着表达力越强。
8.3 线性层
在流行的神经网络架构中使用了各种类型的层。在随后的章节中,我们将探讨不同类型的层,例如卷积层。但在这个部分,我们检查最简单和最基本的层类型:线性层。在这里,前一层的每个感知器都与下一层的每个感知器相连。这样的层也被称为全连接层。因此,如果前一层有 m 个神经元,下一层有 n 个神经元,那么就有 mn 个连接,每个连接都有自己的权重。
注意:我们交替使用“神经元”和“感知器”这两个词。
图 8.6 显示了一个线性层,它是更大的 MLP 的一部分。图 8.7 显示了一个带有线性层的更大的 MLP。与前面的章节一致,我们使用上标表示层 ID,下标表示源和目标 ID。
从层 (l − 1) 的第 k 个神经元到层 l 的第 j 个神经元的连接权重表示为 w[jk]^((l))。这里下标顺序是目标 (j) 后跟源 (k)。这稍微有些反直觉,但普遍遵循,因为它简化了矩阵表示(稍后描述)。注意以下几点:
-
我们将单个感知器(加权求和后跟 sigmoid)拆分为两个独立的层,加权求和和 sigmoid。
-
我们使用 sigmoid 函数而不是 Heaviside 函数作为非线性函数。
8.3.1 线性层表示为矩阵-向量乘法
让我们在 MLP 的背景下回顾一下感知器。正如我们在方程 7.3 中看到的,单个感知器对其输入进行加权求和,然后对结果执行阶跃函数。在 MLP 中,第 l 层中任何感知器的输入来自前一层:(l − 1)层。

图 8.6 线性层输出层 l 来自层 (l − 1)。权重矩阵的第 1 行(来自所有输入神经元,层 (l − 1),它们相加形成输出神经元 1)的权重以粗体显示。

图 8.7 多层神经网络:这是一个完整的深度神经网络,其中一部分如图 8.6 所示。
令 a[0]^((l − 1)),a[1]^((l − 1)),⋯,a[m]^((l − 1)) 表示层 (l − 1) 中 m 个神经元的输出(图 8.6 中节点的最左侧输入列)。令 a[0]((*l*)),*a*[1]((l)),⋯,a[n]^((l))) 表示层 l 中 n 个神经元的输出。请注意,我们通常使用符号 a,代表激活,来表示单个神经元的输出。现在考虑层 l 中的第 j 个神经元。例如,检查图 8.6 中的 z[1]^((l)):注意进入它的权重和它们的源激活。其输出是 a[j]^((l)),其中

我们可以将这些方程中的求和重写为权重向量和激活向量之间的点积:

所有 j 的完整方程集可以用矩阵-向量乘法以超级紧凑的方式写出,

方程式 8.7
其中
- W^((l)) 是一个 n × m 矩阵,表示从层 l − 1 到层 l* 的 所有连接 的权重:

方程式 8.8
^((l)) 表示层 l 的激活。将 Sigmoid 函数应用于向量等同于将其逐个应用于向量的每个元素:

矩阵-向量表示法通过以全局方式处理所有权重、偏置、激活等,使我们免于处理下标。
8.3.2 线性层 MLP 的正向传播和全局输出函数
方程式 8.7 描述了单个线性层的正向传播。具有全连接(即线性)层 0⋯L 的 MLP 在输入
上的最终输出可以通过重复应用此方程获得:
MLP(
) =
^((L)) = y = σ(W((*L*))…*σ*(*W*((1))σ(W^((0))
+
((0)))+((1)))⋯+
^((L)))
方程式 8.9
在计算机实现中,这个表达式通过重复应用线性层逐步评估:

方程式 8.10
很容易看出方程式 8.10 是方程式 8.7 的重述。
仔细检查这些方程揭示了一个美丽的特性。复杂的方程式 8.9 永远不会被明确评估。相反,我们按照方程式 8.10 逐层评估连续层的输出,一次评估一层。每一层都可以通过将前一层的输出作为输入来评估。不需要其他输入。也就是说,我们可以直接从输入
评估
^((0)),然后从
^((0)) 评估
^((1)),从
^((1)) 评估
^((2)),以此类推,直到
^((L))(这是 MLP 的全局输出)。在评估过程中,我们只需要在任何给定时间内存中保留前一层和当前层。这个过程极大地简化了实现以及概念化,被称为 正向传播。
列表 8.1 PyTorch 代码用于正向传播
def Z(x, W_l, b_l): ①
return torch.matmul(W_l, x) + b_l
def A(z_l): ②
return torch.sigmoid(z_l)
def forward(x, W, b): ③
L = len(W) - 1
a_l = x
for l in range(0, L + 1): ④
z_l = Z(a_l, W[l], b[l]) ⑤
a_l = A(z_l) ⑥
return a_l
① x: 层 l-1 的激活(1 维向量) W[l]: 层 l 的权重矩阵 b[l]: 层 l 的偏差向量
② Sigmoid 激活函数(非线性层)
③ x: 1 维输入向量 W: 从层 0 到 L 的矩阵列表 b: 从层 0 到 L 的向量列表
④ 遍历层 0 到 L
⑤ 计算 Z
⑥ 计算激活
8.4 训练和反向传播
在整本书中,我们一直在讨论这个过程的点点滴滴。在第 1.1 节和第 3.3 节(特别是算法 3.1),我们看到了训练监督模型的过程概述(如果需要,请重新阅读)。训练是一个迭代过程,通过这个过程估计神经网络的参数。目标是估计参数(权重和偏差),使得在训练输入上,神经网络的输出尽可能接近已知的真实输出。
通常,迭代过程会逐渐改善(接近目标)。在每次迭代中,我们对参数进行小的调整。在这里,“参数”指的是 MLP 的权重和偏差,即第 8.2 节中的w[jk]^((l))s 和b[j]^((l))s。我们不断调整参数,使得在每次迭代中,训练数据输入上的输出越来越接近真实值(GT)。最终,经过多次迭代,我们希望收敛到最优值。请注意,没有保证迭代过程会收敛到最佳可能的参数值。训练可能会完全偏离轨道或陷入局部最小值。(局部最小值在第 3.6 节中解释;如果需要,请重新阅读。)没有好的方法知道我们是否已经达到了权重和偏差的最优值(全局最小值)。我们通常在测试数据上运行神经网络,如果结果令人满意,我们就停止训练。测试数据应该在训练期间保留,这意味着我们永远不应该使用测试数据来训练。在不幸的情况下,网络没有达到所需的准确度水平,我们通常会添加更多的训练数据,或者尝试修改损失函数和/或不同的架构。简单地从不同的随机起点重新训练网络也可能有效。这是一个充满试验和错误的实验科学。
我们如何知道如何在每次迭代中调整参数值?我们定义一个损失(也称为误差)函数。有许多流行的损失函数公式,我们稍后会回顾其中许多,但它们的共同特性是当神经网络输出与已知输出(GT)更一致时,损失会降低,反之亦然。因此,如果 y 表示神经网络的输出,而 ȳ 是 GT,则损失的一个合理表达式是均方误差(MSE)函数 (y − ȳ)²。现在,我们使用 MSE 损失作为我们的代表性损失函数。稍后我们将讨论其他函数。
一旦损失函数被定义,我们就有了神经网络训练目标的清晰、定量定义。目标是使整个训练数据集的总损失最小化。请注意条款“整个训练数据集”:我们不希望在损失其他数据的情况下,只在少数几个输入实例上表现良好。如果我们必须在所有 100 个训练输入实例上产生 10%的错误与在 50 个训练输入实例上产生 0%的错误但在剩余的 50 个实例上产生 40%的错误之间做出选择,我们更倾向于前者。
多层感知器(MLP)中的每个权重 w[jk]^((l)) 都通过一个与 δw[jk]^((l))成比例的量进行调整。同样,每个偏置 b[j]^((l))也通过一个与 δb[j]^((l))成比例的量进行调整。我们可以通过说我们有一个权重向量
和偏置向量
来简洁地表示所有这些。在每次迭代中,我们通过改变
的量 δ
和
的量 δ
来改变它们,以便它们的新值是
− rδ
和
− rδ
,其中 r 是一个称为学习率的常数,需要在训练开始时设置)。在这个上下文中,值得注意的是,在 8.3.1 节中,我们用矩阵表示了 MLP 中的权重集合,而在这里我们用向量来指代同一事物。这些并不矛盾,因为我们总是可以将矩阵的元素(即从上到下和从左到右遍历矩阵的元素)转换成一个向量。
我们如何估计调整量 δ
和 δ
?这就是梯度概念发挥作用的地方。这些内容在 3.3.1、3.3.2 和 3.5 节中已有详细讨论(再次提醒,如有必要,请重新阅读)。一般来说,如果损失 𝕃 表示为参数的函数,例如 𝕃(
,
),那么使损失最优降低的参数变化由损失相对于参数的梯度 ∇![MLP, b]𝕃(
, b) 得出。本章稍后将在算法 3.2 中描述高级过程。这里我们来看看其核心内容。
8.4.1 损失及其最小化:训练目标
给定一个训练数据集 𝕃,它是一组 <输入,GT 输出> 对 𝕃 = {⟨
, ȳ⟩},损失可以表示为

方程 8.11
其中
= MLP(
)
如方程 8.9 所示。
现在再次考虑方程 8.7。我们可以将每一层的权重矩阵 W^((l)) 转换为一个向量,然后将这些从连续层中依次连接的向量组合成一个巨大的权重向量
,即 MLP 中所有权重的向量:

同样,我们可以形成一个包含 MLP 中所有偏差的巨大向量:

训练的最终目标是找到
和
,它们将最小化损失 𝕃。在第三章中,我们了解到我们可以通过设置梯度 ∇![MLP]𝕃 = 0 和 ∇![MLP]𝕃 = 0 来求解最小值。从方程 8.9 和 8.11 的组合中计算损失梯度是不可行的。相反,我们采用迭代解法:在损失表面上进行 梯度下降,如下一节所述。
列表 8.2 用于 MSE 损失的 PyTorch 代码
def mse_loss(a_L, y): ①
return 1./ 2 * torch.pow((a_L - y), 2) ②
① a: 层 L 的激活(1D 向量): 真实值(1D 向量)
② 见方程 8.11。
8.4.2 损失表面和梯度下降
几何上,损失函数 𝕃(
,
) 可以看作是在高维空间中的一个曲面。这个空间的域对应于
中的所有维度加上
中的所有维度。这如图 8.8 中的二维域所示。在第三章中,我们也看到了,给定一个函数 𝕃(
,
),向最小值前进的最佳方式是沿着负梯度在参数空间中行走。我们采用这种方法来最小化损失。我们计算损失函数相对于权重和偏差的梯度,并通过与这些梯度的(负)成比例的量来更新权重和偏差向量。重复这样做可以使我们接近最小值。在图 8.8 中,梯度下降路径用实线箭头表示,而到最小值的任意非最优路径用虚线箭头表示。

图 8.8 一个代表性的损失 𝕃(w, b)。注意,
和
都已经在这个图中被简化为 1D。
因此,梯度下降中更新权重和偏差的方程是

方程 8.12
其中 r 是一个常数。在这里

方程 8.13
向量更新方程 8.12 可以用标量分量表示为

方程 8.14
注意,我们必须在每次迭代中重新评估这些偏导数,因为它们的值会在每次迭代中改变。
8.4.3 为什么梯度提供了下降的最佳方向
为什么沿着梯度更新可以最优地减少函数?这在第三章中有详细讨论。在这里,我们简要回顾一下这个想法。使用多维泰勒展开,我们可以在已知点的邻域内评估一个函数。例如,我们可以评估 𝕃(
+ δ
),其中 δ
是从
出发的小偏移量,如下所示

方程 8.15
其中 H,称为 海森矩阵,其定义如方程 3.9。由于我们没有离开
太远,||δ
|| 很小。这意味着二次和更高阶的项可以忽略不计,我们可以将它们省略(当 ||δ
|| → 0 时,近似是完美的):

但我们知道点积 (
)^T ▽
𝕃将在两个向量指向同一方向时达到其最大值:也就是说,

对于某个比例常数 r。
在实现中,r 被称为 学习率。较高的学习率会导致优化过程更快地进展,但也存在超过最小值的危险。我们将在后面更详细地了解这些内容。现在,只需注意r是系统的可调超参数。
因此,从𝕃(
)到𝕃(
–
)的最大值减少发生在δ
沿着负梯度方向时。这就是为什么我们在梯度下降中朝着负梯度移动:这是达到最小值最快的方式。图 8.8 中的直线箭头说明了梯度的方向。虚线箭头显示了一个任意的非梯度路径以供比较。
我们可以类似地处理偏置向量
。
8.4.4 梯度下降和局部最小值
我们应该注意,梯度下降可能会陷入一个 局部最小值。图 8.9 展示了这一点。

图 8.9 具有局部和全局最小值的非凸函数。根据点,梯度下降可能会带我们到其中一个。
在早期时代,优化技术努力避免局部最小值并收敛到全局最小值。像模拟退火和隧道这样的技术被精心设计来避免局部最小值。现代神经网络采取了不同的态度:它们并不非常努力地避免局部最小值。有时局部最小值是一个可接受的(足够准确)解。否则,我们可以重新训练神经网络:它将从随机位置开始,所以这次它可能达到更好的最小值。

图 8.10 层 0, …, L的 MLP,每层一个神经元。再次强调,我们将每一层都分解为一个加权求和和一个 sigmoid 函数。
8.4.5 反向传播算法
我们已经看到,梯度下降通过反复更新权重和偏置(方程 8.12)来进展。这相当于通过方程 8.14 反复使用单个偏导数更新单个权重和偏置。
从方程 8.9 和 8.11 获得梯度 ∇![]𝕃(
,
), ∇![]𝕃(
,
) 的闭式解,或者,等价地,获得偏导数 ∂𝕃/∂w[jk]^((l)), ∂𝕃/∂b[j]^((l)) 的闭式解,是非常困难的。反向传播是一种算法,允许我们像前向传播(方程 8.10)一样逐层评估梯度并更新权重和偏置。
简单网络的反向传播算法
我们首先讨论一个简单的 MLP 的反向传播,该 MLP 每层只有一个神经元。由此产生的主要简化是,单个权重和偏置不再需要下标,在连续两层之间只有一个权重和一个偏置。然而,它们仍然需要上标来表示层 ID。图 8.10 显示了这个 MLP。我们使用 MSE 损失(方程 8.11),但我们只在一个输入输出对 x[i], y[i] 上工作。通过重复相同的步骤,可以很容易地推导出总损失(对所有训练数据实例求和)。
我们首先定义一个辅助变量:

δ^((l)) 的物理意义是它表示损失相对于层 l 的(预激活)输出的变化率(记住,在这个网络中,层 l 有一个神经元)。
让我们为图 8.10 中的 MLP 建立几个重要方程:
- 任意层 l 的前向传播 l ∈ {0, L}

方程 8.16

方程 8.17
- 损失—在这里我们处理单个训练数据实例,x[i],其 GT 输出为 ȳ[i]:

- 损失相对于权重和偏置的偏导数,以最后一层辅助变量 L 为条件—使用偏导数的链式法则,

检查右侧的项,我们看到

(层 L 的辅助变量)。并且使用前向传播方程,

一起,它们导致

同样,

因此,我们得到以下一对方程,分别表示损失相对于权重和偏置的偏导数,以最后一层的辅助变量为条件:

方程 8.18

方程 8.19
- 最后一层的辅助变量 L—使用偏导数的链式法则,

使用方程 8.5 对 Sigmoid 函数的导数进行计算,我们得到
δ^((L)) = (a^((L))−ȳ[i]) σ(z((*L*)))(1−*σ*(*z*((L))))
使用方程 8.17,这导致
δ^((L)) = (a^((L))−ȳ[i]) a((*L*))(1−*a*((L)))
方程 8.20
- 关于任意层 l 的辅助变量,损失相对于权重和偏置的偏导数——使用偏导数的链式法则,

使用辅助变量的定义和前向传播方程 8.16,这导致

方程 8.21
类似地,

使用辅助变量的定义和前向传播方程 8.16,这导致

方程 8.22
- 任意层 l 的辅助变量——使用偏导数的链式法则,

使用辅助变量的定义和前向传播方程 8.16,这导致

这将得到
δ^((l)) = δ^((l+1)) w^((l+1)) a((*l*))(1−*a*((l)))
方程 8.23
我们首次遇到逐层处理属性是在第 8.3.2 节中,与前向传播方程相关。让我们在训练简单网络的情况下回顾一下这一点。考虑方程 8.16 和 8.17。我们使用一些权重 w^((l)) 和偏置 b^((l)) 的值初始化系统。使用这些值,我们可以评估第 0 层的输出。首先,我们可以轻松地评估 z^((0)) 和 a^((0))(因为所有输入都是已知的):

一旦我们有了 z^((0)) 和 a^((0)),我们可以使用它们通过方程 8.16 和 8.17 来评估 z^((1)) 和 a^((1))。但如果我们有 z^((1)) 和 a^((1)),我们再次可以通过方程 8.16 和 8.17 使用它们来评估 z^((2)) 和 a^((2))。以此类推,我们可以一直进行到层 L,以获得 a^((L)),这是多层感知器 (MLP) 的最终输出。换句话说,我们可以迭代地评估连续层的输出,只使用前一层的输出。不需要知道其他层。在任何给定迭代中,我们只需要记住前一层的输出:我们可以从那里构建当前层。将方程 8.16 和 8.17 应用于层 0 到 L 的单个序列被称为 前向传播。
可以应用类似的技巧来评估辅助变量,但我们需要进行反向计算。我们可以通过方程 8.20 评估最后一层的辅助变量,δ^((L))。但是一旦我们有了 δ^((L)),我们可以通过方程 8.23 评估 δ^((L − 1))。从那里,我们可以评估 δ^((L − 2))。我们可以以此类推,一直计算到层 0,依次评估 δ((*L*)),*δ*((L − 1)),⋯,δ^((0))。每次评估一个 δ^((l)),我们也可以通过方程 8.21 和 8.22 分别评估同一层的 ∂𝕃/∂w^((l)) 和 ∂𝕃/∂b^((l))。我们还可以使用刚刚估计的偏导数立即更新该层的权重和偏置,因为当前值在训练过程中将不再需要。因此,从最后一层开始,我们可以以这种方式更新所有层的权重和偏置,直到层 0。这就是反向传播。
当然,我们必须同时进行:一次前向传播,为层 0 到 L 设置 z 和 a 的值,然后是层 L 到 0 的反向传播层。重复这些步骤,直到收敛。
注意:前向传播、均方误差损失和反向传播的完整代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/pJrw 找到。
列表 8.3 PyTorch 代码用于前向和反向传播
def forward_backward(x, y, W, b):
L = len(W) - 1
a = []
for l in range(0, L+1):
a_prev = x if l == 0 else a[l-1] ①
z_l = Z(a_prev, W[l], b[l])
a_l = A(z_l)
a.append(a_l)
loss = mse_loss(a[L], y) ②
deltas = [None for _ in range(L + 1)]
W_grads = [None for _ in range(L + 1)] ③
b_grads = [None for _ in range(L + 1)]
a_L = a[L] ④
deltas[L] = (a_L - y) * a_L * (1 - a_L)
W_grads[L] = torch.matmul(deltas[L], a[L - 1].T) ⑤
b_grads[L] = deltas[L]
for l in range(L-1, -1, -1): ⑥
a_l = a[l]
deltas[l] = torch.matmul(W[l+1].T, deltas[l + 1]) * a_l * (1 - a_l)
W_grads[l] = torch.matmul(deltas[l], a[l - 1].T)
b_grads[l] = deltas[l]
return loss, W_grads, b_grads
① 前向传播
② 计算均方误差损失
③ 存储层 0 到 L 的 δ((*l*)),*∂*𝕃**/***∂w*((l)),∂𝕃/∂b^((l)) 的数组
④ 激活最后一层 - a^((L))
⑤ 计算层 L 的 δ 和梯度
⑥ 计算层 0 到 L − 1 的 δ 和梯度
在任意线性层网络上的反向传播算法
在 8.4.5.1 节中,我们看到了一个只有每层一个神经元的简单网络。只有一个连接,因此每层只有一个权重、一个激活和一个辅助变量。因此,我们可以省略所有这些变量的下标(尽管我们必须保留表示层的上标)。现在我们考察一个更通用的网络,由线性层 0,⋯,L 组成。这个网络的任意切片如图 8.6 所示。
最终目标是评估损失相对于权重和偏置的偏导数。使用它们,我们可以更新当前的权重和偏置,以最优地减少损失。
我们的整体策略如下。我们再次使用辅助变量。我们首先推导出允许我们计算最后一层辅助变量的表达式。然后我们推导出一个表达式,允许我们在给定层 l + 1 的辅助变量的情况下,计算任意层 l 的辅助变量。由于我们可以直接计算最后一层 L 的辅助变量,我们可以使用这个表达式来计算倒数第二层 L − 1 的辅助变量。但是一旦我们有了它们,我们就可以计算层 L − 2 的辅助变量。我们就这样一直进行下去,直到达到层 0。因此我们可以计算所有的辅助变量。我们还推导出允许我们从辅助变量中计算损失相对于权重和偏差的偏导数的表达式。这给了我们所需的一切。由于我们首先计算与最后一层相关的事物,然后迭代地朝向初始层进行,这个过程被称为 反向传播。
你将注意到接下来推导的表达式与为单神经元每层网络推导的表达式之间的相似性。差异如下解释:
- 前向传播(任意层 l)—通过这个网络的前向传播已在第 8.3.1 节中描述,可以用方程 8.7 简要表示,此处重复以方便参考)。左侧是针对单个神经元的标量方程;右侧是针对整个层的向量方程。它们是等价的:

方程 8.24
索引 j 和 k 遍历相关层中的所有神经元。按照惯例,我们总是用这些变量表示层中的任意神经元。变量 l 用于索引层。在索引权重时,我们通常用 j 表示目标,用 k 表示源——记住权重是按(目标,源)意外地索引的,以简化数学。通常,向量对应整个层。单个向量元素对应特定的神经元,并由 j 或 k 索引。
- 损失—与简单的网络不同,在这里,最终的 L 层可以包含多个神经元。因此,损失函数变为

方程 8.25
其中求和发生在最后一层的所有神经元上。注意,
^((L))是 MLP 的输出,即
^((L)) =
= MLP(
)对于训练输入
(见方程式 8.10)。对应于
的 GT 输出是常数向量ȳ。
越接近ȳ,损失越小。注意,我们需要在整个训练数据集上平均损失。这里我们展示了单个训练数据实例的损失计算。计算只需为每个训练数据实例复制,然后取平均值。
- 辅助变量——由于一个层有多个神经元,我们为每个神经元有一个辅助变量。因此,辅助变量有一个下标来标识该层中的特定神经元。它继续有一个上标来表示其层。我们定义

- 最后一层的辅助变量

使用方程式 8.25 并观察,由于a[j]s 彼此独立,因此在相对于a[j]^((L))的微分过程中,求和中只有一个项——第j项会幸存,我们得到

此外,使用 8.24 中的左下方程和方程式 8.5,我们得到

结合这些,我们得到

方程式 8.26

方程式 8.27
在这里,∘ 表示两个向量之间的 Hadamard 积。它基本上是一个元素级对应向量元素乘积的向量。因此,

方程式 8.28

方程式 8.29
方程式 8.26 和 8.27 是相同的。前者是一个表示最后一层单个辅助变量的标量方程。后者是一个表示最后一层所有辅助变量的向量方程。如果我们已经执行了前向传递并有了其结果,包括a[j]^((L))s 以及训练数据的 GT,我们可以直接计算这些方程。
- 任意层的辅助变量—这比每层只有一个神经元的情形显著不同且更难以理解。我们试图评估在一般情况下的 δ[j]^((l)) = ∂𝕃/∂z[j]^((l)):即对于任意层 l。损失并不直接依赖于内部层变量 z[j]^((l))。损失直接依赖于仅最后一层的激活,这些激活依赖于前一层,依此类推。任何一层中的 z 形成一个完整的依赖集,对于损失 𝕃,这意味着损失可以用这些变量来表示,而无需其他变量。特别是,我们可以将损失表示为 𝕃(z[0]^((l+1)), z[1]^((l+1)), z[2]((*l*+1)),⋯)。你可以形成一个心理图像,*z[j]*((l)) 通过下一层的所有 z 扩散到 𝕃,即 z[0]^((l+1)), z[1]^((l+1)), z[2]^((l+1)),等等。然后,使用偏导数的链式法则,

现在,根据定义,

并且使用方程 8.24,

而

将所有这些结合起来,我们得到单个辅助变量的标量表达式。它及其整个层的等价向量方程在此处展示:

方程 8.30

方程 8.31
这里,∘ 表示前面解释过的 Hadamard 乘法,W^((+1l)) 是表示从层 l 到层 (l+1)* 的所有连接的矩阵(参见方程 8.8)。方程 8.30 和 8.31 允许我们在前向传播的结果 (as) 可用的情况下,从 δ^((l+1))s 评估 δ^((l))s。我们已经表明,最后一层的辅助变量可以直接从该层的激活中计算出来。因此,我们可以评估所有层的辅助变量。
- 关于辅助变量的损失相对于权重和偏差的导数—我们已经看到了如何计算辅助变量。现在我们将关于权重和偏差的损失的偏导数用这些变量来表示。这将为我们提供沿着负梯度更新权重和偏差所需的梯度,这是最小化损失的最优移动:

方程 8.32

方程 8.33
方程 8.32 和 8.33 是等价的。第一个是标量,与层 l 中的单个权重相关,第二个描述了整个层。同样,方程 8.34 和 8.35 也是等价的:

方程式 8.34

方程式 8.35
第一个是标量,与层 l 中的单个偏差有关,第二个描述了整个层。
8.4.6 将所有内容组合起来:整体训练算法
之前,我们讨论了前向传播:将输入向量
通过一系列线性层传递并生成输出预测。我们学习了 MSE 损失,𝕃,它计算输出预测与 GT,y 的偏差。我们还学习了如何使用反向传播计算 𝕃 对参数 W 和 b 的梯度。在以下算法中,我们描述了这些组件如何在训练过程中结合:
算法 8.5 训练神经网络
使用随机值初始化
, b
while 𝕃 > threshold do
⊳ 前向传播
for l ← 0 to L do
^((l)) = W^((l))
^((l–1)) +
^((l))
^((l)) = σ(
^((l)))
end for
⊳ 损失
𝕃 = 1/2 ||
^((L)) – ȳ||²
⊳ 最后一层的梯度
^((L)) = (
^((L)) – ȳ) ∘
^((L)) ∘ (
–
^((L)))
▽W^((L))𝕃 =
((*L*))(((L–1)))^T
▽b^((L))𝕃 =
^((L))
⊳ 剩余层的梯度
for l ← L – 1 to 0 do
^((l)) = ((W((*l*+1)))*T*
^((l+1)))
^((l)) ∘ (
–
^((l)))
▽W^((l))𝕃 =
^((l)) (
((*l*–1)))T
▽b^((l))𝕃 =
^((l))
end for
⊳ 参数更新
W = W – r▽[W]𝕃
b = b – r▽[b]𝕃
end while
8.5 使用 PyTorch 训练神经网络
现在我们已经了解了训练过程是如何工作的,让我们看看如何在 PyTorch 中实现它。为此,让我们看以下示例。考虑一家试图解决需求预测问题的电子商务公司:该公司希望估计即将到来的周内将售出的手机数量,以便相应地管理其库存。我们的目标是开发一个可以进行此类预测的模型。让我们假设特定周的销量是三个变量的函数:(a)上周售出的手机数量,(b)提供的折扣,以及(c)到下一个假期的周数。我们将这些变量分别称为prev_week_sales、discount_fraction和weeks_to_next_holidays。这个例子可以建模为一个回归问题,其中我们从一个形式为[prev_week_sales, discount_fraction, weeks_to_next_holidays]的输入向量中预测即将到来的周内售出的手机数量。
备注:本节完全功能的代码,可通过 Jupyter Notebook 执行,可在mng.bz/O1Ra找到。
从历史数据中,我们生成一个包含过去N周三个变量值的大数据集X。X表示为一个N x 3 的矩阵,其中每一行代表一个单独的训练数据实例,而N是可用的数据点的总数。我们还有一个长度为N的 GT 向量ȳ,其中包含训练数据集中每周的实际手机销量。表 8.1 显示了我们的训练集的样本数据点。
表 8.1 需求预测的样本训练数据
| 上周销量 | 折扣比例 (%) | 到下一个假期的周数 | 销售单位数 |
|---|---|---|---|
| 76,440 | 63 | 2 | 94,182 |
| 41,512 | 50 | 3 | 51,531 |
| 77,395 | 77 | 9 | 95,938 |
| … | … | … | … |
| 21,532 | 70 | 4 | 28,559 |
备注:在本节中,X和ȳ指的是整个批次的训练数据实例。由于数据集很大,这在实际设置中可能不可行。为了解决这个问题,我们通常使用X和ȳ的迷你批次。我们将在下一章正式介绍迷你批次的概念。
关于数据集的一个重要观点是,每个特征的值域完全不同。例如,上周的销售量以数万个单位的数字表示,而折扣分数是一个介于 0 和 100 之间的百分比数字。在机器学习中,将所有值归一化到共同尺度是一个好的实践,因为这样做可以帮助提高训练速度并减少陷入局部最小值的机会。对于我们的例子,让我们使用最小-最大归一化将所有特征缩放到 0-1 范围。以下代码片段展示了如何在 PyTorch 中执行最小-最大归一化。在接下来的讨论中,我们假设我们正在处理归一化后的数据:
def min_max_norm(X, y):
X, y = X.clone(), y.clone() ①
X_min, X_max = torch.min(X, dim=0)[0],
torch.max(X, dim=0)[0] ②
X = (X - X_min) / (X_max - X_min) ③
y_min, y_max = torch.min(y, dim=0)[0],
torch.max(y, dim=0)[0] ④
y = (y - y_min) / (y_max - y_min) ⑤
return X, y
① 复制数据,以免损坏原始数据
② 计算X每一列的最小值和最大值
③ 归一化X
④ 计算最小值和最大值y
⑤ 归一化y
为了解决回归问题,我们首先定义一个两层神经网络模型,它可以接受形式为[prev_week_sales,discount_fraction,is_holidays_ongoing]的 3D 输入向量,并生成输出预测。以下代码片段给出了 PyTorch 实现:
class TwoLayeredNN(torch.nn.Module):
def __init__(self, input_size, hidden1_size, hidden2_size, output_size):
super(TwoLayeredNN, self).__init__()
self.model = torch.nn.Sequential( ①
torch.nn.Linear(input_size, hidden1_size), ②
torch.nn.Sigmoid(),
torch.nn.Linear(hidden1_size, hidden2_size), ③
torch.nn.Sigmoid(),
torch.nn.Linear(hidden2_size, output_size) ④
)
def forward(self, X): ⑤
return self.model(X)
nn = TwoLayeredNN(input_size=X.shape[-1], hidden1_size=10,
hidden2_size=5, output_size=1)
① 将网络定义为一系列线性层和 sigmoid 层
② 第一个隐藏层,权重矩阵大小为 input_size × hidden1_size)
③ 第二个隐藏层,权重矩阵大小为 hidden1_size × hidden2_size)
④ 输出层,权重矩阵大小为 hidden2_size × output_size)
⑤ X是一个N × 3 的矩阵。每一行是一个(3D 向量),代表一个单独的数据点。
PyTorch 中的神经网络模型应该继承自torch.nn.Module并实现forward方法。我们的两层神经网络包含两个线性层,每个线性层后面跟着一个 sigmoid(非线性)激活层。最后,我们有一个将最终激活转换为输出预测的线性层。这些层通过torch.nn.Sequential类链接在一起,形成两层神经网络。每次我们使用nn(X)调用模型时,都会调用forward方法,并将输入X传递到各个层以获得最终输出。
现在我们已经定义了神经网络及其前向传递,我们需要定义损失函数。我们可以使用方程 8.11 中定义的均方误差损失。损失函数本质上比较神经网络模型预测的需求与 GT 的实际需求,当差异较大时返回较大的值,当差异较小时返回较小的值。均方误差损失在 PyTorch 中通过torch.nn.MSELoss类提供。以下代码片段展示了示例调用:
loss = torch.nn.MSELoss() ①
loss(y_pred, y_gt) ②
① 实例化损失函数
② 计算损失 _pred:神经网络的输出 _gt:真实值
最后,我们需要一种方法来计算损失相对于模型参数的梯度,这样我们就可以开始训练过程。幸运的是,我们不需要显式地计算梯度,因为 PyTorch 会自动使用自动微分(autograd)为我们完成这项工作。(有关 autograd 的更多详细信息,请参阅第 3.1 节。)对于我们的当前示例,我们可以通过调用 loss.backward() 来指示 PyTorch 运行反向传播并计算梯度。有了这个,我们就准备好开始训练了。训练神经网络的 PyTorch 代码如下所示。
列表 8.4 训练神经网络
nn = TwoLayeredNN(input_size=X.shape[-1], ①
hidden1_size=10,
hidden2_size=5,
output_size=1)
loss = torch.nn.MSELoss() ②
optimizer = torch.optim.SGD(nn.parameters(), lr=0.2, ③
momentum=0.9)
num_iters = 1000
for i in range(num_iters): ④
y_out = nn(X) ⑤
mse_loss = loss(y_out, y) ⑥
optimizer.zero_grad() ⑦
mse_loss.backward() ⑧
optimizer.step() ⑨
① 实例化神经网络
② 实例化损失函数
③ 实例化优化器
④ 训练循环
⑤ 前向传播
⑥ 计算损失
⑦ 清除梯度并防止上一步的梯度累积
⑧ 运行反向传播(计算梯度)
⑨ 更新权重
在训练循环中,我们迭代地运行前向传播,计算损失,计算梯度,并更新权重。神经网络以随机权重初始化,因此在训练循环的早期迭代中做出任意预测。这导致初始损失值很高。然而,随着训练的进行,权重被更新以最小化损失值,预测的需求更接近实际 GT。为了更新权重,我们使用所谓的 优化器。在训练过程中,通过在 loss 对象上调用 backward() 函数来计算梯度。随后,optimizer.step 调用更新权重和偏差。在这个例子中,我们使用了基于随机梯度下降的优化器,可以使用 torch.optim.SGD 来调用。PyTorch 提供了各种优化器,如 Adam、AdaGrad 等,这些将在下一章中详细讨论。我们通常运行训练循环,直到损失达到足够低的值,以被接受。一旦训练循环完成,我们就拥有了一个可以轻松接受新数据点并生成输出预测的模型。
摘要
-
Sigmoid 函数 σ(x) = 1/(1+e^(–x)* 有一个 S 形的图像,是 Heaviside 步函数的微分形式,因此被用于感知器中。因此,感知器函数整体变为 P(
) ≡ σ(
^T
+ b)。它由
和 b 参数化,分别控制 S 形曲线的斜率和位置。 -
神经网络通过近似解决特定问题的函数来解决需要智能的现实生活中问题。它们由多个通过加权边相互连接的感知器组成。我们不是随意连接感知器,而是将它们连接成层。在分层网络中,一个感知器仅与前一层的感知器连接。不允许存在层内和其他连接。
-
监督神经网络为输入值样本集手动生成输出(真实值)。这个由输入和已知输出组成的数据集称为训练数据集。
-
损失被定义为神经网络在训练数据输入上生成的真实值与实际输出之间的不匹配。计算损失的最简单方法是计算神经网络生成的输出和真实向量之间的欧几里得距离。这被称为均方误差(MSE)损失。从数学上讲,给定一个训练数据集 𝕜,它是一组<input, GT 输出>对 𝕜 = {⟨
, ȳ⟩},损失可以表示为

其中输出为
[i] = MLP(
[i]).
-
训练是优化特定神经网络连接权重和偏置的过程,以使损失最小。注意,在推理过程中,神经网络通常看到它在训练期间从未见过的数据。推理输出只有在训练输入的分布大致匹配整体输入分布时才是好的。
-
我们通过迭代调整权重和偏置来最小化损失。到达多元函数最近最小值的最快方式是遵循梯度。因此,我们根据损失函数的梯度调整权重和偏置。从数学上讲,

- 前向传播是使用神经网络从输入生成输出的过程:更具体地说,是一个多层感知器(MLP)。因此,MLP 通过前向传播进行推理。分层网络的一个美丽特性是我们可以一次处理一层,从层 0(最接近输入层)迭代到输出层。从数学上讲,

其中 W^((l)),
^((l)) 代表层 l 的权重和偏置,而
^((l)) 代表层 l 的激活输出,这同时也是层 l + 1 的输入。
- 反向传递是一个生成所有权重和偏置相对于损失函数梯度的过程。它依赖于前一个正向传递的结果,并从输出层向输入层进行。它使用辅助变量
^((l)),这些变量可以通过从最后一个(最接近输出层)层反向迭代到第一个(最接近输入层)层来计算——因此得名反向传播——并且所有所需的梯度都可以从这些辅助变量中计算出来。数学上,

- 训练过程通过在训练数据集上交替进行正向和反向传递来推进。
第九章:损失、优化和正则化
本章涵盖了
-
损失函数的几何和代数介绍
-
softmax 的几何直觉
-
包括动量、Nesterov、AdaGrad、Adam 和 SGD 在内的优化技术
-
正则化及其与贝叶斯方法的关系
-
训练过程中的过拟合和 dropout
到现在为止,你应该已经深刻地认识到神经网络本质上是一种函数逼近器。特别是,神经网络分类器在特征空间(每个输入特征组合都是一个特定点的空间)中模拟了类之间的决策边界。监督分类器在这个空间中标记样本训练数据输入,并带有——可能是手动生成的——类标签(真实标签)。训练过程迭代地学习一个函数,这个函数本质上创建了决策边界,将样本训练数据点分离成单个类别。如果训练数据集是可能输入的真实分布的合理代表,那么网络(模型类边界的学到的函数)将以良好的准确性对从未见过的输入进行分类。
当我们选择一个特定的神经网络架构(具有固定的一组层,每个层具有固定的一组具有特定连接的感知器)时,我们本质上冻结了我们用作函数逼近器的函数族。我们仍然需要“学习”各种感知器(也称为神经元)之间的连接器的确切权重。训练过程迭代地设置这些权重,以便最好地分类训练数据点。为此,我们设计一个损失函数来衡量网络输出与期望结果之间的偏差。网络持续尝试最小化这个损失。有各种各样的损失函数可供选择。
通过迭代最小化损失的过程称为优化。我们还有许多优化算法可供选择。在本章中,我们研究损失函数、优化算法以及相关的主题,如 L1 和 L2 正则化和 dropout。我们还了解过拟合,这是在训练神经网络时需要避免的一个潜在陷阱。
注意:本章的完整 PyTorch 代码以完全功能性和可执行的 Jupyter 笔记本形式,可在mng.bz/aZv9找到。
9.1 损失函数
损失函数本质上衡量了神经网络输出的不良程度。在监督网络的情况下,单个训练数据实例的损失是神经网络的实际输出(也称为预测)与特定训练输入实例上已知的或手动标记的理想输出(真实标签[GT])之间的距离。总训练损失是通过将所有训练数据实例的损失相加得到的。训练本质上是一个迭代优化过程,该过程最小化总训练损失。
9.1.1 损失的量化与几何视角
损失表面及其最小化在 8.4.2 节中详细描述。这里我们只做简要回顾。
一个完整的神经网络可以用以下方程描述

方程 9.1
方程 9.1 表明,给定一个输入
,权重为
和偏置为
的神经网络会发出输出向量或预测向量
。权重和偏置可以组织成层;这个方程并不关心这一点。向量
,
分别表示从所有层聚合的所有权重和偏置的集合。评估函数 f(⋅) 等同于在网络中进行一次前向传递。特别是,给定一个训练输入实例
^((i)),神经网络发出
^((i)) = f(
^((i))|
,
)。我们将
^((i)) 称为第 i 个训练数据实例的输出。
在监督训练期间,对于每个训练输入实例
^((i)),我们都有 GT(已知的输出),ȳ^((i))。我们将 ȳ^((i))称为GT 向量(如通常所做,我们使用上标索引表示训练数据实例)。
理想情况下,输出向量
^((i)) 应该与 GT 向量 ȳ^((i))相匹配。它们之间的不匹配是针对该训练数据实例 𝕃((*i*))(((i)), ȳ^((i))) 的损失,我们有时将其表示为 𝕃((*i*))(((i)), ȳ^((i))). 所有训练数据实例的总体训练损失(由优化过程最小化)是所有训练数据实例损失的加和:

方程 9.2
其中求和是针对所有训练数据实例的,n 是训练数据集的大小。请注意,对所有训练数据点的这种求和是计算每个训练数据实例损失所必需的。因此,一个epoch,即对所有训练数据实例的单次训练循环,需要 O(n²) 的时间复杂度,其中 n 是训练数据点的数量。训练通常需要许多个 epoch。这使得训练过程非常昂贵。在 9.2.2 节中,我们研究减轻这种影响的方法。

图 9.1 损失表面可以看作是一个峡谷。
注意:在本章中,我们用n表示训练数据点的数量,用N表示输出向量的维度。对于分类器,输出向量的维度N与类别的数量相匹配。我们还使用上标(i)来索引训练数据点,下标j来索引输出向量维度。对于分类器,j表示类别。
我们可以将 𝕃(
,
) 视为高维空间中的超曲面。图 8.8 和 8.9 展示了损失表面的低维示例。这些都是说明性的例子。实际上,损失表面通常是高维且非常复杂的。一个很好的心理图像是峡谷(见图 9.1)。在任何一点“向下走”实际上遵循局部梯度的负方向,即损失表面的梯度在 8.4.2 节中引入。沿着梯度向下走并不总是导致全局最小值。例如,沿着虚线箭头向下走将带我们到一个局部最小值,而全局最小值是水流的方向,由实线箭头指示。(也参见 8.4.4 节和图 8.9。)
许多损失公式都是可能的,量化了不匹配 𝕃((*i*))(((i)), ȳ^((i))); 其中一些将在以下小节中描述。
9.1.2 回归损失
回归损失是最简单的损失公式。它是输出向量和 GT 向量之间差异的 L2 范数。这个损失在方程 8.11 中引入。我们在此重申:第i个训练数据实例的损失是

其中求和是对输出向量的分量进行的。N是类别的数量。GT 向量和输出向量都是N-维的。
注意:完整的回归损失功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/g1a8找到。
列表 9.1 PyTorch 代码用于回归损失
from torch.nn.functional import mse_loss ①
y_pred = torch.tensor([-0.10, -0.24, 1.43, -0.14, -0.59]) ②
y_gt = torch.tensor([ 0.59, -1.92, -1.27, -0.40, 0.50]) ③
loss = mse_loss(y_pred, y_gt, reduction='sum') ④
① 导入回归损失(均方误差损失)
② N 维预测向量
③ N 维真实向量
④ 计算回归损失
9.1.3 交叉熵损失
交叉熵损失在 6.3 节中讨论了熵的上下文。如果需要,现在重新阅读那部分是个好时机。在这里,我们快速回顾一下这个想法。
交叉熵损失通常用于衡量分类问题中分类器神经网络输出与相应 GT 之间的不匹配。在这里,GT 是一个长度等于类数的 one-hot 向量。除了一个元素外,其余元素都是 0。唯一的非零元素是 1,它出现在对应于该训练数据实例正确类别的索引处。
因此,GT 向量看起来像 ȳ^((i)) = [0,…,0,1,0,…,0]。预测向量应该具有介于 0 和 1 之间的值。预测向量
^((i)) 的每个元素表示特定类的概率。换句话说,
^((i)) = [p[0], p[1],…, p[N−1]],其中 p[j] 是输入 i 属于第 j 类的概率。在第 6.3 节中,我们用一个示例图像分类器来说明,该分类器预测图像是否包含猫(类别 0)、狗(类别 1)、飞机(类别 2)或汽车(类别 3)。四个类别中总有一个假设存在于图像中。如果对于第 i 个训练数据实例,GT 向量是一张猫的图片,那么我们就有 ȳ^((i)) = [1,0,0,0]。预测向量
^((i)) = [0.8,0.15,0.04,0.01] 是好的,而
^((i)) = [0.25,0.25,0.25,0.25] 是不好的。请注意,GT 以及预测向量的元素之和总是 1,因为它们是概率。从数学上讲,给定一个训练数据集 X,

给定这样的真实标签 GT 和预测向量,交叉熵损失(CE 损失)是

方程 9.3
其中求和是对预测向量的元素进行的,N 是类数。
交叉熵损失背后的直觉
注意,方程 9.3 的求和中只保留了一个元素——对应于 GT 类别的元素。其他元素消失,因为它们被乘以 0 的 GT 值。正确的 GT 类别的预测概率(对数)乘以 1。因此,CE 损失始终简化为 −log(y[j*]((i))),其中 j^* 是 GT 类别。如果这个概率是 1,那么 CE 损失变为 0,这是正确的,因为以 1 的概率预测了正确的类别。如果正确类别的预测概率是 0,那么 CE 损失是 −log(0) = ∞,这也是正确的,因为这是最糟糕的预测。预测对正确类别的预测越接近 1,损失就越小。
注意:完整的交叉熵损失代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/g1a8 找到。
列表 9.2 PyTorch 交叉熵损失代码
import torch
y_pred = torch.tensor([0.8, 0.15, 0.04, 0.01]) ①
y_gt = torch.tensor([1., 0., 0., 0.]) ②
loss = -1 * torch.dot(y_gt, torch.log(y_pred)) ①
① N 维预测向量
② N 维 one-hot 真实标签向量
③ 计算交叉熵损失
两个类别的特殊情况
如果N = 2(即我们只有两个类别)会发生什么?让我们用y^((i))表示第i个训练输入的类别 0 的预测概率:即
[0]^((i)) = y((*i*))。由于这些是概率,对另一个类别的预测[1]((i)) = 1 − y((*i*))。此外,用*ȳ*((i))表示第i个训练输入的类别 0 的 GT 概率。那么 1 − ȳ^((i))是类别 1 的 GT 概率。(我们稍微滥用了符号——到目前为止,ȳ表示一个向量,但在这里它表示一个标量。)
然后,根据方程式 9.3,第i个训练数据实例的 CE 损失变为
𝕃((*i*))(*y*((i)), ȳ^((i))) = −ȳ^((i)) log(y^((i))) − (1−ȳ^((i))) log(1−y^((i)))
方程式 9.4
注意:二进制交叉熵损失的完整代码,可通过 Jupyter Notebook 执行,可在mng.bz/g1a8找到。
列表 9.3 PyTorch 代码用于二进制交叉熵损失
from torch.nn.functional import binary_cross_entropy ①
y_pred = torch.tensor([0.8]) ②
y_gt = torch.tensor([1.]) ③
loss = binary_cross_entropy(y_pred, y_gt) ④
① 导入二进制交叉熵损失
② 输出类别 0 的概率 - y[0]。单个值就足够了,因为 y[1] = 1 − y[0]。
③ 真实值是 0 或 1。
④ 计算交叉熵损失
9.1.4 图像和向量不匹配的二进制交叉熵损失
给定一对归一化的张量(例如图像或向量),其元素值均在 0 到 1 之间,可以使用两分类 CE 损失的一个变体来估计张量之间的不匹配。请注意,像素强度值在 0 到 255 之间的图像可以通过将每个像素强度值除以 255 来归一化,从而将其转换为 0 到 1 的范围。这种两种图像的比较在图像自编码器中得到了应用,例如。我们稍后研究自编码器;在这里,我们将在以下侧边栏中提供简要概述。
自编码器
自编码器以图像为输入,从图像中创建一个低维描述符——这个描述符通常被称为图像的嵌入——并尝试从嵌入中重建输入图像。图像嵌入是图像的压缩表示。重建是一个有损过程:信号中的微小、细微的变化会丢失,只保留基本部分。损失是输入图像和重建图像之间的不匹配。通过最小化这个损失,我们激励系统在嵌入大小预算内尽可能多地保留输入的本质。
用ȳ表示输入图像。用
^((i))表示由自编码器输出的重建图像。二进制交叉熵损失定义为

方程式 9.5
注意,在这里 N 是图像中的像素数,而不是之前的类别数。求和是针对图像中的像素。此外,GT 向量 ȳ^((i)) 不是一个 one-hot 向量;而是输入图像。尽管有这些差异,方程 9.5 的思想与方程 9.4 相同。
为什么它有效?
当输入与 GT 匹配时,二元交叉熵损失达到最小值。我们将在下面概述证明。(注意,为了简化,我们省略了上标和下标。)我们有
−𝕃 = ȳlog(y) + (1 − ȳ)log(1 − y)
在最小值处,∂𝕃/∂y = 0。

因此,二元交叉熵损失的最小值发生在网络输出与 GT 匹配时。但这并不意味着当输出与 GT 匹配时,这种损失变为零。
注意:即使在输出与输入匹配的理想情况下,二元交叉熵损失也不一定是零(尽管在理想情况下,损失确实是 最小 的,这意味着对于输入和输出不匹配的非理想情况,损失更高)。
检查方程 9.5,当两个输入匹配时,我们有 −𝕃(ȳ,
)|![ = ȳ]。我们直观地期望这种损失为零,因为输出是理想的。但事实并非如此。例如,如果,对于 ȳ[j]^((i)) = y[j]^((i)) = 0.25

事实上,二元交叉熵损失仅在特殊情况下为零,例如 y[j]^((i)) = ȳ[j]^((i)) = 1。
9.1.5 Softmax
假设我们正在构建一个分类器:例如,我们在第 6.3 节中展示的图像分类器,它预测图像是否包含猫(类别 0)、狗(类别 1)、飞机(类别 2)或汽车(类别 3)。我们的分类器可以发出一个与输入图像对应的分数向量
。分数向量中的元素 j 对应于 j[th] 类。我们取分数向量的最大值,并将其称为神经网络预测的图像标签。例如,在示例图像分类器中,一个分数向量可能是 [9.99 10 0.01 -10]。由于最高分数出现在索引 1 处,我们得出结论,该图像包含狗类别 1)。
分数是无界的;它们可以是范围 [−∞,∞] 内的任何实数。然而,通常情况下,当损失函数涉及同一范围内的有界数字集时,神经网络表现更好。训练会更快地收敛到更好的最小值,推理也更准确。因此,将示例分数转换为概率是可取的。这些数字将在范围 [0,1] 内(并且向量的元素之和为 1)。
Softmax 函数将无界分数转换为概率。给定一个分数向量
= [s[0] s[1] s[2] … s[N-1]],相应的 softmax 向量是

方程式 9.6
几个值得注意的点:
-
向量具有尽可能多的类别元素。
-
上一个向量中元素的总和为 1。
-
向量的第 j 个元素代表类别 j 的预测概率。
-
该公式可以处理任意得分,包括负得分。
因此,在我们的这个包含四个类别(猫、狗、飞机、汽车)的示例分类问题中,得分向量
= [9.99 10 0.01 –10]
将产生 softmax 向量
softmax (
) = [0.497 0.502 2.30e–5 1.04e–9].
猫的概率是 0.497,狗的概率略高,为 0.502。飞机和汽车的概率要低得多:神经网络预测图像是狗的图像,但它并不十分自信;它也可能是猫的。
为什么叫 softmax?
Softmax 函数是 argmaxonehot 函数的平滑(可微)近似,它输出一个对应于最大得分索引的 one-hot 向量。argmaxonehot 函数是 不连续的。为了看到这一点,考虑一对二类得分向量:

对其执行 argmaxonehot 操作将分别产生以下 one-hot 向量:

因此我们看到,向量 argmaxonehot(
) 和 argmaxonehot(
) 相对于彼此非常遥远,尽管点
和
非常接近。另一方面,相应的 softmax 向量是

虽然预测的类别仍然与 argmaxonehot 向量中的那些匹配,但 softmax 向量彼此非常接近。得分越接近,softmax 概率也越接近。换句话说,softmax 是连续的。
图 9.2 几何地描述了这一点。argmaxonehot 函数作为得分向量 [s0, s1](分别用于选择类别 0 和 1)的函数,如图 9.2a 和 9.2c 所示。这些是在 (s0, s1) 平面上的步函数,s0 = s1 是决策边界。它们的 softmax 近似如图 9.2b 和 9.2d 所示。在 8.1 节中,我们介绍了 1D sigmoid 函数(见图 8.1),它近似 1D 步函数。这里我们看到其高维类似物。

(a) 步函数:z = 1 如果 s0 >= s1,否则 z = 0

(b) Softmax:步函数的微分近似

(c) 步函数:z = 1 如果s1 >= s0,否则z = 0

(d) Softmax:步函数的微分近似
图 9.2 在(s0, s1)平面上,两类的 argmaxonehot 和 softmax(分数向量的函数[s0, s1])。决策边界是 45[o]线 s0 = s1。
列表 9.4 PyTorch 代码实现 softmax
from torch.nn.functional import softmax ①
scores = torch.tensor([9.99, 10, 0.01, -10]) ②
output = softmax(scores, dim=0) ③
① 导入 softmax 函数
② 分数通常是神经网络的原始、未经归一化的输出。
③ 计算 softmax
9.1.6 Softmax 交叉熵损失
从前面的讨论中,应该清楚,将分类器神经网络的最后一层设置为 softmax 层是可取的。然后,给定一个输入,网络将输出每个类的概率。在训练过程中,我们可以通过 CE 损失(见第 9.1.3 节)来评估这些概率与已知 GT 概率之间的损失。这可以通过 CE 损失(见第 9.1.3 节)来实现。因此,softmax 通常在分类器训练过程中跟随 CE 损失。因此,组合(softmax CE 损失)在许多深度学习包中作为单个操作提供,例如 PyTorch。这很方便,因为我们不需要调用 softmax 然后 CE 损失。但将它们结合的更深层次的原因是,这种组合在数值上往往更好。
让我们通过一个例子来看看 softmax CE 损失是如何随着输出预测的变化而变化的。再次考虑图像分类问题,我们希望将图像分类为四个类别之一:猫(类别 0)、狗(类别 1)、飞机(类别 2)或汽车(类别 3)。图 9.3 展示了这一点。假设我们正在分类的图像实际上包含狗类别 1)。GT 表示为一个 one-hot 向量[0 1 0 0]。如果我们的分类器预测的向量是[0.498 0.502 0 0],它几乎以相同的概率预测了猫和狗。这是一个不良的预测,因为我们理想上希望它自信地预测狗(类别 1)。因此,CE 损失较高(0.688)。另一方面,如果我们的分类器预测[0.003 0.997 0 0],它高度确定(概率为 0.997)图像包含狗。这是一个好的预测,因此 CE 损失较低(0.0032)。softmax CE 损失可能是目前用于分类器训练的最流行的损失方法。

图 9.3 良好和不良输出预测的 softmax 输出和交叉熵损失
备注:完整的 softmax CE 损失功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/g1a8找到。
列表 9.5 PyTorch 代码实现 softmax 交叉熵损失
from torch.nn.functional import cross_entropy
scores = torch.tensor([[9.99, 10, 0.01, -10]])
y_gt = torch.tensor([1]) ①
loss = cross_entropy(scores, y_gt) ②
① 真实类别索引范围从 0 到 _ –1
② 计算 softmax 交叉熵损失
9.1.7 Focal 损失
随着训练的进行,我们应该关注哪里?当存在 数据不平衡 时,这个问题变得尤为重要,这意味着某些类别的训练数据实例数量显著少于其他类别。在这种情况下,并非所有训练数据都同等重要。我们必须明智地使用我们的训练数据实例。
直观地说,提高表现不佳的训练数据实例的性价比更高。换句话说,与其试图从网络表现良好的示例(所谓的“简单”示例)中榨取每一滴汁液,不如关注网络表现不佳的示例(“困难”示例)。
为了停止关注简单示例,转而关注困难示例,我们可以对远离 GT 的训练数据实例的损失赋予更多权重,反之亦然:即,对接近 GT 的训练数据实例的损失赋予较少权重。再次考虑方程式 9.4 的二进制 CE 损失。第 i 个训练实例的损失可以重新写为以下形式:

注意:在接下来的小节中,为了简化符号,我们省略了上标(i),尽管它仍然隐含存在。
现在,当 GT 是类别 1(即 ȳ = 1)时,实体 (1−y) 衡量预测与 GT 的偏差。我们可以将损失乘以这个值来加重良好预测的损失,减轻不良预测的损失。在实践中,我们乘以 (1−y)^γ,其中 γ 是某个值(例如 γ = 2)。同样,当 GT 是类别 0(即 ȳ = 0)时,实体 y 衡量预测与 GT 的偏差。在这种情况下,我们乘以 y^γ。因此,整体损失变为

我们可以有一个相对简化的表达式

方程式 9.7
其中

方程式 9.7 是 focal loss 的流行表达式。它在 γ 不同值下的图形显示在图 9.4 中。注意,随着 GT 的概率向右增加,损失变得越来越微弱,直到在底部变平。
注意:完整的 focal loss 代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/g1a8 找到。
列表 9.6 PyTorch 代码实现 focal loss
def focal_loss(y, y_gt, gamma):
y_t = (y_gt * y) + ((1 - y_gt) * (1 - y)) ①
loss = -1 * torch.pow((1 - y_t), gamma) * torch.log(y_t)
return loss
① y[t] = y 如果 y[gt] 是 1
y[t] = 1 − y 如果 y[gt] 是 0
9.1.8 Hinge loss
只有在理想条件下,softmax CE 损失才会变为零:正确的类别有一个有限的分数,而其他类别的分数为负无穷。因此,该损失将继续推动网络向改进方向前进,直到达到理想状态(在实践中永远不会发生)。有时我们更喜欢在正确类别的分数达到最大时停止改变网络,我们不再关心增加正确和错误分数之间的距离。这就是 hinge 损失出现的地方。

图 9.4 Focal 损失图(各种 γ 值)
一个铰链门只能向一个方向打开,但不能向另一个方向打开。同样,如果某个良好标准不满足,则 hinge 损失函数会增加,如果标准满足,则损失函数变为零(并且不再进一步减少)。这就像说,“如果你不是我的朋友,我们之间的距离可以从小到大变化(无界),但我不会区分朋友。所有我的朋友都与我保持零距离。”
多类支持向量机损失:分类的 hinge 损失
再次考虑我们的老朋友,这个分类器预测图像是否包含猫(类别 0)、狗(类别 1)、飞机(类别 2)或汽车(类别 3)。我们的分类器输出一个与输入图像对应的输出向量
。在这里,输出是分数:y[j] 是对应于第 j 个类别的分数。(在本小节中,我们省略了表示训练数据索引的上标,以简化符号。)
给定一个(训练数据实例,GT 标签)对 (
, c)(即,GT 类别对应于输入
是 c),多类支持向量机(SVM)损失是

方程式 9.8
其中 m 是一个通常 m = 1 的边界)。
要理解这一点,首先考虑没有边界的方程:

在方程 9.8 中,我们对除了匹配 GT 的类别之外的所有类别进行求和。换句话说,我们只对错误类别进行求和。对于这些类别,我们希望分数 y[j] 小于正确类别的分数 y[c]。有两种可能性:
-
良好输出—错误类别的分数小于正确类别的分数:
y[j] − y[c] < 0 ⟹ max(0, y[j] − y[c]) = 0
损失的贡献是零(我们不区分正确分数:所有朋友都与我保持零距离)。
-
不良输出—错误类别的分数大于正确类别的分数:
y[j] > y[c] ⟹ max(0, y[j] – y[c]) = y[j] − y[c]
损失的贡献是正的(非朋友之间的距离是正的,并且随着非朋友程度的增加而变化)。
在实际设置中,边缘设置为正数,通常为 1,以惩罚正确类别的分数仅略高于错误类别的预测。这迫使分类器学会以高置信度预测正确类别。图 9.8 显示了良好和不良输出预测的 hinge 损失如何不同。

图 9.5 良好和不良输出预测的 Hinge 损失
关于多类 SVM 损失的一个心理模型是它很懒惰。一旦正确的类别分数超过错误分数的边缘 m,它就停止变化。如果正确的类别分数继续上升,损失不会改变,这意味着它不会推动机器在这个点之后继续改进。这种行为与 softmax CE 损失不同,softmax CE 损失试图推动机器为正确的类别达到无限分数。
9.2 优化
神经网络模型定义了一个损失函数,该函数估计网络输出的不良程度。在监督训练期间,特定训练实例输入的输出与该特定训练实例的已知输出(GT)进行比较。GT 和网络生成的输出之间的差异称为损失。我们可以将单个训练数据实例的损失相加,并计算所有训练数据上的总损失。
这些损失当然是网络参数的函数,
,
。我们可以想象一个维度为 dim(
) + dim(
) 的空间。在这个空间的每一个点上,我们都有一个总训练损失的值。因此,我们可以想象一个损失表面——一个高度代表损失值的表面——它定义在网络参数(权重和偏差)的高维域上。
优化不过是找到这个表面上的最低点。在训练过程中,我们从网络参数的随机值开始:这就像在表面上随机选择一个点开始。然后我们不断地在损失表面上沿着负梯度的方向局部下降。我们希望这最终能带我们到最小值或足够低的位置。继续我们关于损失表面像峡谷的类比,最小值在海平面。这个最小值为我们提供了网络参数值(权重和偏差),这些值将在训练数据上产生最小的损失。如果训练数据集充分代表了问题,训练好的模型将在未见过的数据上表现良好。
这个朝着最小值前进的过程,即对训练数据集进行权重和偏差的迭代更新以实现最小损失,被称为优化。基本数学原理在第八章 8.4.2(方程 8.12)中介绍。在这里,我们研究了许多实际细节和变体。
在每次迭代中,我们更新权重和偏差,所以如果t表示迭代次数,
[t]表示t次迭代的权重值,δ
[t]表示t次迭代的权重更新,等等:

方程式 9.9
基本更新是沿着负梯度的方向(参见方程 8.12):

方程式 9.10
这里,𝕃(
[t],
[t])表示t次迭代的损失。理想情况下,我们应该评估每个训练数据实例的损失并取平均值。但这将意味着我们必须为每次迭代处理每个训练数据实例,这是非常昂贵的。相反,我们使用采样(参见 9.2.2):
-
常数η被称为学习率(LR)。较大的 LR 会导致更大的步长(每次更新对权重和偏差的调整更大)反之亦然。我们最初使用较大的 LR 值:当网络完全未训练时,我们希望向最小值迈出大步。另一方面,当我们接近最小值时,我们希望迈出更小的步子,以免超出它。LR 通常是一个很小的数字,比如η = 0.01。
-
在随机梯度下降(SGD;一种流行的方法)中,LR η 通常在一个 epoch(一个 epoch 是遍历所有训练数据的一次)中保持不变。然后在一个或多个 epoch 之后降低 LR。这个过程被称为学习率衰减。因此,LR 并不完全是一个常数。我们可以将其写作η[t]来表示其时间性质,但我们选择保持简单,因为(至少在 SGD 中)它变化不频繁。
由于底层神经网络的权重和偏差在每次迭代中都会变化,我们必须在每次迭代中重新评估损失及其梯度。
需要进行多少次迭代?通常,这是一个很大的数字。我们多次在整个训练数据集上迭代。在这次训练中,请注意,为了确保收敛,在每个 epoch 之后随机打乱训练数据的顺序是极其重要的。在接下来的章节中,我们将探讨这个过程的一些实际细节。
9.2.1 优化的几何视图
这个主题在 8.4.2 节中有详细描述。如果需要,你被鼓励重新回顾那个讨论。
总体来说,神经网络优化是一个迭代过程。理想情况下,在每次迭代中,我们计算当前参数(权重和偏置)相对于损失函数的梯度,并通过沿着负梯度方向移动来获得它们的改进值。
9.2.2 随机梯度下降和小批量
我们如何计算损失函数的梯度?对于每个训练数据实例,损失是不同的。合乎逻辑的做法是平均它们。但如我们之前提到的,这会导致一个实际的问题:我们每次迭代都必须处理整个训练数据集。如果训练数据集的大小是n,那么一个 epoch 是每次迭代的O(n²)操作,我们必须处理所有的n个训练数据实例来计算梯度,而一个 epoch 有n次迭代)。由于n通常是一个很大的数字,经常是数百万,O(n²)是过于昂贵的。
在 SGD 中,我们不是在整个训练数据集上平均来产生梯度。相反,我们在训练数据的一个随机样本子集上平均。这个随机样本的子集被称为小批量。梯度是通过平均小批量中的损失来计算的(而不是整个训练数据集)。这个梯度用于更新权重和偏置参数。
9.2.3 SGD 的 PyTorch 代码
现在,让我们在 PyTorch 中实现 SGD。
注意:SGD 的完整功能代码,可通过 Jupyter Notebook 执行,可以在mng.bz/ePyG找到。
让我们考虑 6.9 节中讨论的例子。我们的目标是构建一个模型,该模型可以使用身高和体重作为输入数据来预测 Statsville 居民是男性、女性还是儿童。为此,让我们假设我们有一个包含各种 Statsville 居民身高和体重的庞大数据集X。X的形状是(num_samples,2),其中每一行代表单个居民的(height, weight)对。相应的标签存储在 y[gt]中,它包含num_samples个元素。y[gt]的每一行可以是 0、1 或 2,具体取决于居民是男性、女性还是儿童。图 9.6 显示了X的一个示例分布。

图 9.6 Statsville 居民的身高和体重。类别 0(男性)由最右侧的簇表示,类别 1(女性)由中间的簇表示,类别 2(儿童)由最左侧的簇表示。
在训练模型之前,我们必须首先将数据转换为适合训练的格式。我们通过继承torch.utils.data.Dataset来实现这一点,并实现__len__和__getitem__方法。可以通过调用data_set[i]来访问第i个训练数据实例。记住,在 SGD 中,我们每次迭代都输入包含batch_size个元素的 minibatch。这可以通过调用__getitem__方法batch_size次来实现。然而,我们不是自己这样做,而是使用 PyTorch 的DataLoader,它提供了一个方便的包装器。在生产环境中推荐使用DataLoader,因为它提供了一个简单的 API,通过它可以(1)创建 minibatch,2)通过多进程加速数据加载时间,3)在每轮中随机打乱数据以防止过拟合。以下代码创建了一个自定义的 PyTorch 数据集。
列表 9.7 PyTorch 代码用于创建自定义数据集
from torch.utils.data import Dataset, DataLoader
class StatsvilleDataset(Dataset): ①
def __init__(self, X, y_gt):
self.X = X
self.y_gt = y_gt
def __len__(self): ②
return len(self.X)
def __getitem__(self, i): ③
return self.X[i], self.y_gt[i]
dataset = StatsvilleDataset(X, y_gt) ④
data_loader = DataLoader(dataset, batch_size=10, ⑤
shuffle=True)
① 继承自 torch.utils.data.Dataset
② 返回数据集的大小
③ 返回第i个训练数据元素
④ 实例化数据集
使用批大小为 10 并开启随机打乱的方式实例化数据加载器
我们下一步是创建一个分类器模型,该模型可以接收身高和体重数据(X)作为输入并预测输出类别。在这里,我们创建了一个简单的神经网络模型,该模型由两个线性层和一个 softmax 层组成。softmax 层的输出有三个值,分别代表三个类别(男性、女性和儿童)的概率。请注意,在正向传播过程中,我们没有调用 softmax 层,因为我们的损失函数,PyTorch 的 CE 损失,期望输入的是原始、未归一化的分数。因此,我们将第二个线性层的输出传递给损失函数。然而,在预测过程中,我们将分数传递给 softmax 层以获得一个概率向量,然后通过 argmax 获取预测类别。请注意,我们有一个函数用于初始化线性层的权重:这很重要,因为权重的起始值通常会影响到收敛。如果模型起始点离最小值太远,它可能永远无法收敛。
列表 9.8 PyTorch 代码用于创建自定义神经网络模型
class Model(torch.nn.Module): ①
def __init__(self, input_size, hidden_size, output_size):
super(Model, self).__init__()
self.linear1 = torch.nn.Linear(input_size, hidden_size) ②
self.linear2 = torch.nn.Linear(hidden_size, output_size)
self.softmax = torch.nn.Softmax(dim=1)
def forward(self, X): ③
scores = self.linear2(self.linear1(X))
return scores
def predict(self, X): ④
scores = self.forward(X)
y_pred = torch.argmax(self.softmax(scores), dim=1)
return y_pred
def initialize_weights(m):
if isinstance(m, torch.nn.Linear):
torch.nn.init.xavier_uniform_(m.weight.data) ⑤
torch.nn.init.constant_(m.bias.data, 0)
model = Model(input_size=2, hidden_size=100, output_size=3)
model.apply(initialize_weights)
① 继承自 torch.nn.Module
② 实例化线性层和 softmax 层
③ 将输入通过两个线性层进行前向传播
④ 预测输出类索引
⑤ 初始化权重以帮助模型更好地收敛
现在我们已经有了数据集和模型,接下来定义我们的损失函数并实例化我们的 SGD 优化器
列表 9.9 PyTorch 代码用于损失函数和 SGD 优化器
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.02) ①
① 使用学习率=0.02 实例化 SGD 优化器
现在我们定义训练循环,这本质上是对整个数据集的一次遍历。我们以batch_size的大小对数据集进行批处理迭代,运行前向传播,计算梯度,并沿着负梯度方向更新权重。请注意,我们在每次迭代中调用optimizer.zero_grad()以防止之前步骤的梯度累积。
列表 9.10 一个训练循环的 PyTorch 代码
def train_loop(epoch, data_loader, model, loss_fn, optimizer):
for X_batch, y_gt_batch in data_loader: ①
scores = model(X_batch) ②
loss = loss_fn(scores, y_gt_batch) ③
optimizer.zero_grad() ④
loss.backward() ⑤
optimizer.step() ⑥
① 以批处理方式遍历数据集
② 将模型前向传播以计算分数
③ 计算交叉熵损失
④ 清除上一步累积的梯度
⑤ 运行反向传播并计算梯度
⑥ 更新权重
这样,我们就准备好训练我们的模型了。以下代码展示了如何进行训练。图 9.7 显示了每个 epoch 结束时的输出预测和损失。

图 9.7 每个 epoch 结束时模型的预测。注意损失是如何随着每个 epoch 减少的。一开始,所有训练数据点都被错误地分类为类别 1。在 epoch 1 结束后,大部分训练数据被正确分类,分类器输出的分布已经接近真实值。损失持续下降直到 epoch 4,尽管改进的视觉效果更难察觉)。
列表 9.11 运行训练循环num_epochs次的 PyTorch 代码
num_epochs = 2
for epoch in range(num_epochs):
train_loop(epoch, data_loader, model, loss_fn, optimizer)
9.2.4 动量
对于高维度的现实损失表面,峡谷类比非常恰当。损失表面几乎不像一个有光滑壁的瓷杯;它更像是大峡谷的墙壁(见图 9.8)。此外,梯度估计(通常在 minibatch 上执行)是噪声的。因此,梯度估计永远不会在一个方向上对齐——它们往往四处游走。尽管如此,大多数梯度都有一个很好的下降分量。其他(非下降分量)是有些随机的(见图 9.8)。所以如果我们平均它们,下降分量会相互加强并得到增强,而非下降分量则会相互抵消并减弱。

图 9.8 动量。在损失表面不同点上的噪声随机梯度估计(粗实箭头)在方向上不一致,但它们都有一个显著的下降分量(细实箭头)。非下降分量(细虚线箭头)指向随机方向。因此,平均化往往加强下降分量并抵消非下降分量。
此外,如果存在一个小平坦区域,其前后都有下山区域,那么基于梯度的传统方法会陷入这个小平坦区域(因为那里的梯度为零)。但是,与过去的平均可以让我们有非零的更新,从而将优化从局部小平坦区域中带出,以便它可以继续下山。
在这个背景下,一个很好的心理图像是一个球在山丘上滚下。山丘表面是粗糙的,球并不是直接向下滚动。相反,它采取了一个之字形路径。但随着它的移动,它聚集了向下的动量,其向下的速度变得更大。
按照这个理论,我们取当前迭代计算的梯度与之前迭代中使用的更新的加权平均值:

方程 9.11
其中 γ,η 是小于 1 的正常数。权重和偏差参数使用方程 9.9 以通常的方式更新。
展开动量方程的递归
展开递归方程 9.11,我们看到

假设 δ
[−1] = 0,我们得到

方程 9.12
因此,
-
我们正在对过去迭代的梯度进行加权求和。然而,这并不完全是一个加权平均,如稍后所述。
-
较旧的梯度由 γ 的更高次幂加权。由于 γ < 1,权重会随着迭代次数的增加而减少(长期迭代的影响较小)。
-
从现在开始向时间开始(第 0 次迭代)的梯度权重之和是

现在,使用泰勒展开,

因此,基于动量的梯度下降中过去梯度的权重之和为 η/(1–γ) ≠ 1。换句话说,这并不完全是一个加权平均(其中权重应加起来等于 1)。这是一个有些不理想的特点,在稍后讨论的 Adam 算法中得到纠正。
可以对偏差进行类似的分析。
9.2.5 几何视图:常量损失等高线、梯度下降和动量
考虑一个具有微小的两个元素权重向量
和没有偏差的网络。此外,假设损失函数是 𝕃 = ||
||² = w[0]² + w[1]²。常量损失等高线是以原点为中心的同心圆。圆的半径表示损失的大小。
如果我们沿着圆的周长移动,损失不会改变。损失在垂直于该方向的正交方向上变化最大:圆的半径。这个直观的观察通过评估梯度得到证实。

因此梯度沿着半径方向,负梯度方向是径向向内的。所以如果我们径向向内移动,损失会迅速减少。如果我们垂直于半径移动(即沿着圆周),损失保持不变;当然,我们是在沿着恒定损失等高线移动。
优化过程使我们从外部的较大半径圆逐渐过渡到内部的较小半径圆。最小值位于原点;理想情况下,一旦达到原点,优化就应该停止。
图 9.9 展示了对于简单损失函数 𝕃 = ||
||² = w[0]² + w[1]² 的优化过程。我们从任意一对权重值开始,并通过方程 9.9 重复更新它们。对于图 9.9a,我们使用不带动量的更新(方程 9.10)。对于图 9.9b,我们使用带动量的更新(方程 9.11)。

(a) 梯度下降不带动量时的恒定损失等高线轨迹

(b) 梯度下降带动量时的恒定损失等高线轨迹
图 9.9 损失函数 𝕃 = ||
||² = w[0]² + w[1]² 的恒定损失等高线和优化轨迹。损失表面是一个以原点为顶点,其底面在 w[0],w[1] 平面平行平面上的圆。随着我们接近原点的最小值,优化过程沿着圆锥向越来越小的横截面下降。更新用箭头表示。注意动量版本如何以更少的步骤到达较小的圆。
当恒定损失等高线是中心为原点的同心圆时,损失表面是一个以原点为顶点的圆锥。横截面是与 w[0],w[1] 平面平行的圆。随着我们接近最小值,优化过程通过越来越小的圆形横截面沿着圆锥的内壁下降。全局最小值位于原点,对应于零损失。零损失等高线是一个半径为零的圆,实际上是一个单点:原点。
在这两种情况下,随着我们接近最小值,进度会减慢(步长变得较小)。这是因为随着我们接近最小值,梯度的幅度会越来越小(想象一个碗:当我们接近底部时,它变得越平)。然而,如果我们有动量,这种效应在一定程度上会被抵消。因此,我们可以看到,当我们有动量时,到达较小半径的圆需要更少的步骤。
9.2.6 Nesterov 加速梯度
基于动量的梯度下降的一个问题是它可能会越过最小值。这可以在图 9.10a 中看到,损失通过一系列更新而减少,然后,当我们接近最小值时,一个更新(用虚线箭头表示)越过最小值并增加了损失(用虚线圆表示)。

(a) 动量梯度下降越过最小值。损失一度减少,然后增加(用虚线圆和箭头表示)

(b) Nesterov 在我们即将越过最小值时减小步长。
图 9.10 图(a)显示了基于动量的梯度下降越过最小值。过冲更新用虚线箭头表示。一个半径大于上一步的圆用虚线轮廓表示。Nesterov 在即将过冲时采取较小的步长,效果更好。
这是 Nesterov 加速梯度优化试图解决的问题。
在 Nesterov 的技术中,我们做以下操作:
-
通过将上一步的更新加到当前点上,估计这次更新将带我们到何处(即目标点)。
-
计算估计目标点的梯度。这与传统的基于动量的方法不同,后者取当前点的梯度。
-
对梯度(在估计的目标点)和上一步的更新取加权平均值。这就是当前步的更新。
从数学上讲,

方程 9.13
其中 γ, η 是小于 1 的常数。权重和偏差使用方程 9.9 的常规方式更新。
这为什么有帮助呢?好吧,考虑以下内容:
-
当我们离最小值有一定距离(没有过冲的可能性)时,估计目标点的梯度与当前点的梯度大致相同,因此我们以类似于基于动量的梯度下降的方式向最小值前进。
-
但当我们接近最小值,并且当前的更新可能会使我们越过它(见图 9.10a 中的虚线箭头)时,估计目标点的梯度将位于最小值的另一侧。就像之前一样,想象这个损失表面是一个以原点为顶点、以顶点之上的基座为底部的圆锥。我们已经沿着圆锥的一侧壁向下移动,并刚刚开始向上爬。因此,估计目标点的梯度与上一步的方向相反。当我们取它们的加权平均值时,它们在某些维度上会相互抵消,导致幅度减小。结果,较小的步长将减轻过冲现象。
注意:动量和 Nesterov 加速梯度的完整功能代码,可通过 Jupyter 笔记本执行,可以在mng.bz/p9KR找到。
9.2.7 AdaGrad
基于动量的优化方法(方程 9.11)和 Nesterov 方法(方程 9.13)都存在一个严重的缺点:它们将参数向量
,
的所有维度同等对待。但损失表面在所有维度上并不对称。某些维度的斜率可能很高,而其他维度的斜率可能很低。我们不能用单个学习率(LR)控制所有维度。如果我们设置 LR 较高,高斜率维度将表现出过多的方差。如果我们设置它较低,低斜率维度几乎无法向最小值方向进步。
我们需要的是按参数的学习率。然后每个学习率将适应其特定维度的斜率。这正是 AdaGrad 试图实现的目标。具有历史较大梯度的维度具有较小的学习率,而具有历史较小梯度的维度具有较大的学习率。
我们如何跟踪梯度历史大小?为了做到这一点,AdaGrad 维护一个状态向量,在其中累积训练过程中看到的梯度向量的每个维度的平方偏导数的总和:

其中 ∘ 表示 Hadamard 算子(两个向量的逐元素乘积)。我们可以将前面的方程表达得更简洁一些:

展开递归,我们得到

假设
[−1] = 0,
[t] 是一个向量,它保存了所有训练迭代中每个维度的平方斜率的累积总和。对于具有历史高斜率的维度,
[t] 中相应的元素较大,反之亦然。整体更新向量看起来是这样的:

方程 9.14
其中 ϵ 是一个非常小的常数,用于防止除以零。然后我们使用方程 9.9 通常是更新权重。对于偏差也存在一组平行的方程。
使用 AdaGrad,在损失梯度中,在早期迭代中看到大斜率的维度在更新期间被赋予较少的重要性。我们强调那些在过去没有取得太多进展的维度。这有点像说,“我将减少对总是说话的人的关注,而将更多关注那些不常说话的人。”
9.2.8 根均方传播
AdaGrad 算法的一个显著缺点是向量
[t] 的幅度随着迭代的进行而不断增加。这导致所有维度的 LR 变得越来越小。最终,当迭代次数很高时,LR 接近零,更新几乎不起作用;向最小值的进展几乎完全停止。因此,AdaGrad 在现实生活中是一个不实用的算法,尽管按组件 LR 的想法是好的。
根均方传播(RMSProp)解决了这个缺点,而没有牺牲 AdaGrad 的维度自适应性质。在这里,同样有一个状态向量,但它的方程是

将其与 AdaGrad 的状态向量方程进行比较:

它们几乎相同,但项被(1−γ)和γ加权,其中 0 < γ < 1 是一个常数。这个递归方程中的特定权重对具有一个非常有趣的效果。为了看到它,我们必须展开递归:

因此,
[t] 是过去项梯度平方幅度向量的加权总和。从现在开始回溯到时间的起点(第 0 次迭代),权重为(1−γ),(1−γ)γ,(1−γ)γ²,⋯,(1−γ)γ^t。如果我们加上这些权重,我们得到

当迭代次数变得很高(t → ∞)时,这变成

其中使用了泰勒展开来评估括号中的项。
对于大量的迭代,权重的总和趋近于 1。这意味着在多次迭代之后,RMSProp 状态向量实际上取了过去项梯度平方幅度向量的加权平均。随着更多迭代的进行,权重被重新分配,较旧的项被弱化,但整体幅度不会增加。这消除了 AdaGrad 中的消失 LR 问题。RMSProp 继续降低具有高累积偏导数的维度的 LR,但这样做不会使 LR 变得无限小。
RMSProp 的整体更新方程是

方程式 9.15
对于偏差也有一个平行的方程组。权重和偏差参数使用方程 9.9 以通常的方式更新。
9.2.9 Adam 优化器
基于动量的梯度下降法随着迭代的进行,越来越增强下坡分量。另一方面,RMSProp 会减少看到大梯度的维度的学习率(LR),反之亦然,以平衡所有维度的进度率。
这两种都是理想性质。我们希望有一个优化算法能够结合它们,而这个算法就是 Adam。它正越来越多地成为大多数深度学习研究者的首选优化器。
Adam 优化算法维护两个状态向量:

方程 9.16

方程 9.17
其中 0 < β[1] < 1 和 0 < β[2] < 1 是常数。注意以下内容:
-
方程 9.16 实质上是方程 9.11 的动量方程,有一个显著的区别。我们将权重项改为 β[1] 和 (1−β[1])。正如我们所见,随着 t → ∞,这导致状态向量成为所有过去梯度的加权平均值。这比原始动量方案有所改进。
-
第二个状态向量基本上是 RMSProp 方程 9.15 中的那个。
使用这两个状态向量,Adam 创建更新向量如下:

方程 9.18
分子中的
[t] 吸收了动量方法(平均增强)的好处。否则,方程几乎与 RMSProp 相同;这些好处也被吸收了。
最后,使用方程 9.9 以常规方式更新权重和偏差参数。
偏差校正
状态向量中过去值的权重之和
[t],
[t] 只有在 t 的较大值时才会趋近于 ∞。为了改善 t 较小值时的近似,Adam 引入了偏差校正:

方程 9.19

方程 9.20
我们使用偏差校正的实体 v̂[t] 和 ŝ[t] 而不是
[t]* 和
[t]*,在方程 9.18 中。
列表 9.12 为各种优化器提供的 PyTorch 代码
from torch import optim
sgd_optimizer = optim.SGD([params], lr=0.01) ①
sgd_momentum_optimizer = optim.SGD([params], lr=0.01,
momentum=0.9) ②
sgd_nesterov_momentum_optimizer = optim.SGD([params], lr=0.01, ③
momentum=0.9, nesterov=True)
adagrad_optimizer = optim.Adagrad([params], lr=0.001)
rms_prop_optimizer = RMSprop([params], lr=1e-2, ④
alpha=0.99)
adam_optimizer = optim.Adam([params], lr=0.001,
betas=(0.9, 0.999))
① 将学习率设置为 0.01
② 将动量设置为 0.9
③ 将 Nesterov 标志设置为 True
④ 将平滑常数设置为 0.99 (γ 在 9.15)
9.3 正则化
假设我们正在教一个婴儿识别汽车。我们向他们展示红色汽车、蓝色汽车、黑色汽车、大汽车、小汽车、中等汽车、圆顶汽车、矩形顶汽车等等。很快,婴儿的大脑意识到有太多种类难以全部记住。所以大脑开始形成 抽象:神秘的共同特征被存储在婴儿的大脑中,并带有 汽车 这个标签。大脑已经学会了分类一个名为汽车的抽象实体。尽管它无法 记住 它所看到的每一辆汽车,但它可以 识别 汽车。我们可以说它已经发展了 经验。神经网络也是如此。我们不想我们的网络记住每一个训练实例。相反,我们希望网络形成 抽象,这样它就能在推理过程中识别对象,即使训练过程中从未见过推理过程中遇到的对象实例的精确相似性。
过拟合和欠拟合
如果网络相对于训练实例的数量具有过多的表达能力(太多感知器或,等价地,太多权重),则网络可以并且通常会死记硬背训练实例。这种现象称为过拟合。过拟合导致网络在训练数据上表现非常好,但在测试或实际推理中表现很差。换句话说,网络已经调整自己以适应训练数据的每一个角落、弯道和曲折,因此在训练数据上表现良好——损害了测试数据的表现。这如图 9.11 所示。正则化指的是一系列技巧,通常试图防止过拟合。这是本节的主题。
另有一种现象称为欠拟合,其中网络简单地没有足够的表达能力来模拟训练数据。欠拟合的症状是网络在训练和测试数据上表现都差。如果我们看到这种情况,我们应该尝试一个更复杂的网络,具有更多的感知器。

图 9.11 过拟合:二元分类器的数据点。属于不同类别的点被视觉上划分为正方形和圆形。实心正方形/圆形表示训练数据,空心正方形/圆形表示测试数据。有一些异常的训练数据实例(正方形区域中的实心圆圈)。估计的决策边界(实线)变得扭曲以适应它们,这导致许多测试点(空心正方形/圆形)被错误分类。扭曲的实线曲线是一个过拟合决策边界的例子。如果我们选择一个“更简单”的决策边界(虚线直线),两个异常训练点将被错误分类,但机器在测试中的表现会更好。
9.3.1 最小描述符长度:奥卡姆剃刀原理看优化
那些最小化特定损失函数的权重和偏置集是唯一的吗?让我们考察一个单个感知器(方程 7.3)的输出 ϕ(
^T
+ b)。假设 ϕ 是 Heaviside 阶跃函数(见 7.3.1 节)。假设
[], b[] 是最小化损失函数的权重和偏置。很容易看出,如果我们对权重进行缩放,例如 α
[] 对于所有正实数 α,感知器的输出将保持不变。因此,权重向量 7
[] 也将最小化损失函数。
这对于任意神经网络(由许多感知器组成)通常都是正确的:最小化损失函数的权重和偏置集不是唯一的。神经网络如何选择一个?哪一个才是正确的?我们可以使用奥卡姆剃刀原理来回答这个问题。
奥卡姆剃刀是一个哲学原则。其拉丁文的直译为,“实体不应超出必要性而增加。”这大致意味着 在充分的解释中,最简单的一个是最好的。在机器学习中,这个原则通常被解释如下:
在使损失最小化的候选神经网络参数值(权重和偏差)集合中,“最简单”的一个应该被选择。
一般思路如下。假设我们正在尝试最小化 𝕃(θ)(在这里我们用 θ 来表示
和
的组合)。我们还希望解决方案尽可能简单。为了实现这一点,我们在原始损失项中添加了对“简单性”偏离的惩罚。因此,我们最小化
𝕃(θ) + λR(θ)
这里,
-
表达式 R(θ) 表示不简单性的度量。有时被称为 正则化惩罚。将正则化惩罚添加到损失中,激励网络在尝试最小化原始损失项 𝕃(θ) 的同时,尝试最小化不简单性 R(θ)(或者,等价地,最大化简单性)。
-
λ 是一个超参数。其值应通过试错仔细选择。如果 λ 太低,这类似于没有正则化,网络容易过拟合。如果 λ 太高,正则化惩罚将占主导地位,网络将不足以最小化实际的损失项。
有两种流行的估计 R(θ) 的方法,分别在 9.3.2 和 9.3.3 节中概述。两者都试图最小化参数向量(基本上是网络描述符)的某些范数(长度)。这就是为什么正则化可以被视为最小化描述符长度。
9.3.2 L2 正则化
在 L2 正则化中,我们假设短向量更简单。换句话说,简单性与 L2 范数(也称为欧几里得范数)的平方成反比。因此,
R(θ) = (||
||²+||
||²)
总体来说,我们最小化

方程 9.21
将其与方程 9.2 进行比较。
L2 正则化到目前为止是最受欢迎的正则化形式。从现在开始,我们通常用 𝕃(
,
) 来表示 L2 正则化的版本(即方程 9.21)。超参数 λ 在 PyTorch 中通常被称为 权重衰减。权重衰减通常设置为一个很小的数字,这样方程 9.21 的第二项(权重向量的范数)就不会淹没实际的损失项。以下代码展示了如何实例化一个启用了正则化的优化器。
列表 9.13 启用 L2 正则化的 PyTorch 代码
from torch import optim
optimizer = optim.SGD([params], lr=0.2, weight_decay=0.01) ①
① 将权重衰减设置为 0.01

(a) L1 正则化

(b) L2 正则化
图 9.12 L1 和 L2 正则化
9.3.3 L1 正则化
L1 正则化在原则上与 L2 正则化相似,但它将简单性定义为权重和偏置的绝对值之和:
R(θ) = (|
|+|
|)
因此,这里我们最小化

方程式 9.22
9.3.4 稀疏性:L1 与 L2 正则化
L1 正则化倾向于创建稀疏模型,其中许多权重为 0。相比之下,L2 正则化倾向于创建具有低(但非零)权重的模型。
要理解这一点,请考虑图 9.12,该图绘制了 L1 和 L2 正则化下的损失函数及其导数。设 w 为权重向量
的单个元素。在 L1 正则化中,

由于梯度对所有 w 的值都是常数,L1 正则化在所有 w 的值上以相同的步长将权重推向 0。特别是,当接近 0 时,向 0 的步长不会减小其幅度。在 L2 正则化中,

在这里,随着 w 接近 0,梯度的大小持续减小。因此,w 越来越接近 0,但由于更新步骤随着 w 接近 0 而变得越来越小,它可能永远不会达到 0。因此,L2 正则化产生的权重向量比 L1 正则化更密集,而 L1 正则化产生的权重向量稀疏,包含许多 0。
9.3.5 贝叶斯定理和优化的随机视图
在章节 6.6.2 和 6.6.3 中,我们讨论了无监督学习中的最大似然估计(MLE)和最大后验(MAP)(如果需要,请鼓励您回顾这些章节)。在这里,我们将在监督学习的背景下研究它们。
在监督优化过程中,我们有输入和已知输出对的样本(以训练数据的形式)⟨
^((i)), ȳ^((i))⟩。当然,我们可以在每个训练数据点上执行前向传递并生成网络输出

其中 θ 是网络的参数集(代表权重和偏置的总和)。
假设我们将样本训练数据生成视为一个随机过程。我们可以根据当前的网络参数 θ 建模训练实例 T^((i)) = ⟨ȳ^((i)),
^((i))⟩ 的概率。

这在直观上是合理的。在 θ 的最优值处,网络输出 f(
^((i)), θ) 将与 GT ȳ^((i)) 匹配。我们希望我们的模型分布在网络输出与 GT 匹配的位置具有最高的概率密度。概率密度应该随着与该位置的距离增加而减小。
仔细思考一下,我们可以发现这个类似于高斯公式的形式,它导致了一个类似于回归损失的分子,并不是唯一可能的。我们可以在分子中使用任何损失函数,因为它们都具有当网络输出与真实值匹配时达到最小值,随着不匹配的增加而逐渐增大的特性。一般来说,

假设训练实例是相互独立的,整个训练数据集同时出现的概率是各个实例概率的乘积。如果我们用 T 表示整个训练数据集,

然后

在这一点上,我们可以采取下两个小节中描述的两种可能的方法之一。
基于 MLE 的优化
在这种方法中,我们通过最大化似然函数来选择参数集 θ 的最优值

这相当于说我们将选择最优参数 θ,使得训练数据的出现概率最大化。因此,最优参数集 θ[*] 由以下公式得出

显然,最大化似然的最优 θ 是最小化 𝕃(θ) 的那个。所以,最大似然公式实际上就是最小化整个训练数据集上预测值和真实值之间的总不匹配。
MAP 优化
根据贝叶斯定理(方程 6.1),

为了估计最优 θ,我们也可以最大化该方程左侧的后验概率。这相当于说我们将选择最优参数 θ,使得在训练数据集给定的情况下 θ 具有最大的条件概率。因此,最优参数集 θ 的值由以下公式得出

其中最后一个等式是通过贝叶斯定理推导出来的。
观察前面的方程,我们可以看到最右侧项的分母 p(T) 不涉及 θ,因此可以从优化中省略。所以,

我们如何建模 a priori 概率 p(θ)?我们可以使用奥卡姆剃刀原则,并说我们赋予较小的参数值更高的 a priori 概率。因此,我们可以表示为

然后,整体后验概率最大化变为

注意:最大化这个后验概率等价于最小化正则化损失。最大化似然等价于最小化未正则化损失。
9.3.6 Dropout
在 9.3 节的介绍中,我们了解到,过多的表达能力(过多的感知器节点)有时会阻止机器发展出一般的 抽象(也称为 经验)。相反,机器可能会记住训练数据实例(见图 9.11)。这种现象称为过拟合。我们已经看到,缓解这种问题的一种方法是在损失中添加正则化惩罚——例如添加权重的 L2 范数——以阻止网络学习大的权重值。
Dropout 是另一种正则化方法。在这里,有些疯狂的是,我们在训练期间关闭网络中的随机感知器(将它们的值设置为 0)。更准确地说,我们给第 l 层的第 i 个节点(感知器)附加一个概率 p[i]^l。在任何训练迭代中,节点(感知器)关闭的概率为 (1−p)。通常,dropout 只在训练期间启用,在推理期间关闭。
这有什么好处呢?好吧,
-
Dropout 防止网络过度依赖少数节点。相反,网络被迫使用所有节点。
-
等价地,dropout 鼓励训练过程将权重分散到多个节点,而不是过多地依赖于少数节点。这使得效果与 L2 正则化有些相似。
-
Dropout 缓解了 共适应:网络中一组节点以高度相关的方式行为,大多数时候发出相似的输出。这意味着网络可能只保留其中一个,而不会造成显著的精度损失。
Dropout 模拟了一组子网络
考虑一个具有 dropout 的某个神经网络的三个节点的小型中间层。该层的第 k 个输入以概率 p[k] 被激活。这意味着该输入节点关闭的概率是 (1−p[k])。我们可以用 k = 0 或 k = 1 或 k = 2 的 二进制 随机变量 δ[k] 来表示这一点。这个变量 δ[k] 可以取两个可能值之一:0 或 1。它取值为 1 的概率是 p[k]。换句话说,p(δ[k] = 1) = p[k],而 p(δ[k] = 0) = 1 − p[k]。具有 dropout 的这个小三个节点层的输出可以表示为

我们有三个变量 δ[0],δ[1] 和 δ[2],每个变量可以取两个值。总共我们有 2³ = 8 种可能的组合。每种组合对应图 9.13 中显示的一个子节点。这些组合中的每一个都有发生概率 P[i],也在图中显示。这些观察结果导致了一个非常重要的洞察:
输出的期望值——即 𝔼(a[l])——与如果我们随机部署图 9.13 中的子网络时的输出期望值相同,概率为 P[i]。
这为什么很重要呢?好吧,给定一个问题,我们中没有人知道网络部署的正确感知器数量。在这种情况下可以做的事情之一是随机部署各种强度的网络,并取其输出的平均值。我们刚刚已经证明,丢弃(随机关闭输入)可以达到同样的效果。但是部署一个随机打开或关闭输入的网络比部署大量子网络要简单得多。我们只需要部署一个 丢弃 层。
Dropout 的 PyTorch 代码
在 9.2.3 节中,我们创建了一个简单的两层神经网络分类器,根据身高和体重数据预测 Statsville 居民是男性、女性还是儿童。在本节中,我们展示了添加了丢弃层的相同模型。请注意,丢弃层仅在训练期间启用,而不是在推理期间。要在 PyTorch 中实现这一点,您可以在运行推理之前调用 model.eval()。这样,您的训练和推理代码保持不变,但 PyTorch 在幕后知道何时包含丢弃层,何时不包含。
列表 9.14 Dropout
class ModelWithDropout(torch.nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Model, self).__init__()
self.net = torch.nn.Sequential(
torch.nn.Linear(input_size, hidden_size),
torch.nn.Dropout(p=0.2), ①
torch.nn.Linear(hidden_size, output_size),
torch.nn.Dropout(p=0.2)
)
def forward(self, X):
return self.net(X)
① 实例化一个丢弃层,丢弃概率为 0.2

(a) 子网络 0:概率 P[0] = (1−p[0]) (1−p[1]) (1−p[2])

(b) 子网络 1:概率 P[1] = (1−p[0]) (1−p[1]) p[2]

(c) 子网络 2:概率 P[2] = (1−p[0] )p[1] (1−p[2])

(d) 子网络 3:概率 P[3] = (1−p[0] )p[1] p[2]

(e) 子网络 4:概率 P[4] = p[0] (1−p[1]) (1−p[2])

(f) 子网络 5:概率 P[5] = p[0] (1−p[1]) p[2]

(g) 子网络 6:概率 P[6] = p[0] p[1] (1−p[2])

(h) 子网络 7:概率 P[7] = p[0] p[1] p[2]
图 9.13 Dropout 模拟子网络:以神经网络的三节点中间层为例。输入节点 a[k]^((l − 1)) 被激活的概率为 p(δ[k] = 1) = p[k]。
摘要
-
训练是神经网络识别其参数(各个感知器的权重和偏置)最优值的过程。训练是迭代进行的:在每次迭代中,我们估计损失并运行一个优化步骤,以更新参数值以减少损失。经过多次这样做后,我们希望达到最优参数值。
-
在监督神经网络中,损失量化了在采样训练数据实例上所需输出和网络输出之间的不匹配。所需输出(真实值)通常通过人工努力来估计。训练神经网络本质上识别了使损失最小化的神经网络权重和偏差。
-
真实值和网络输出之间的差异可以用许多不同的方式表示。每个都对应于不同的损失函数。将第 i 个训练输入给出的所有可能类别的真实概率表示为向量 ȳ^((i)),并将网络输出表示为 y^((i)),
-
回归损失取网络输出和真实向量之间向量差异的 L2 范数,并在数学上表示为
。 -
如果神经网络是一个分类器,它通常输出一个类概率向量。这意味着
![]()
其中 p[j](
^((i))) 是网络估计的输入属于类别 j 的概率,N 是可能类别的数量。一般来说,给定一个输入,神经网络计算每个类别的得分:得分最高的类别是预测的类别。得分是无界的数字,可以是任意大或小(甚至是负数)。该操作将它们转换为概率。如果![]()
表示得分向量,相应的 softmax 向量是
![]()
其中
。softmax 输出向量由概率组成,这意味着它们是介于 0 和 1 之间的数字,并且它们的总和为 1。 -
给定每个类的概率,分类器可以使用交叉熵损失,它可以表示为
。注意,由于真实向量是 one-hot 编码的,这个表达式中的只有一个项会幸存:对应于所需类别的那个项,用 j^* 表示。损失成为相应网络输出的对数,即 −log(y[j*]((i))), 如果 y[j*]((i)) = 1,则损失为零。这与我们的预期一致:如果网络以概率 1 预测 GT 类,则没有损失。
-
由于在训练过程中,softmax 几乎总是紧跟着交叉熵损失,PyTorch 有一个名为 softmax 交叉熵损失的组合运算符。单独执行这些操作是多余的,因为它具有更好的数值特性。
-
Focal loss 通过对具有更高损失的“困难”示例赋予更多权重来尝试解决数据不平衡问题。
-
Hinge loss 是另一种流行的损失,当以最大分数预测正确类别时变为零。一旦达到这个标准,它就不再尝试改进分数的相对值。
-
-
总损失可以通过将所有单个训练数据实例的损失相加来获得。然而,这要求我们在每次迭代中处理所有训练数据点,这是非常昂贵的。因此,我们采样训练数据点的子集来创建一个小批量,估计小批量中每个输入数据实例的损失,并将它们相加以获得总损失的估计。这个过程被称为随机梯度下降(SGD)。
-
优化是更新神经网络参数(感知器的权重和偏置)的过程,以减少损失。我们可以将损失与权重和偏置值绘制成图,并得到一个在权重和偏置值域上定义的超曲面。我们的最终目标是达到这个损失超曲面的底部(最小点)。优化在几何上等同于沿着超曲面向下移动以减少损失。最陡下降(朝向最近的极小值)沿着负梯度发生。
-
我们可以将多个其他标准与梯度结合,以改善收敛到最小损失值。每个标准都会导致不同的优化技术:
-
由于估计值存在噪声,局部梯度可能并不总是指向最小值,但它将包含一个指向该最小值的主成分,同时还有其他噪声成分。我们不是盲目地跟随当前梯度,而是可以跟随当前梯度估计值和前一次迭代梯度估计值的加权平均方向。这是一个递归过程,有效意味着任何迭代中跟随的方向是当前和从训练开始的所有梯度估计值的加权平均。最近的梯度被赋予更高的权重,而较旧的梯度被赋予较低的权重。所有这些梯度都包含指向最小值的主成分,相互强化,而噪声成分则指向随机方向,并倾向于相互抵消。因此,整体加权总和是一个更可靠的朝向最小值的移动。这被称为基于动量的梯度下降。
-
动量的一个缺点是它可能导致超过最小值。Nesterov 加速梯度通过一步前瞻计算梯度来纠正这一缺点。如果当前更新使我们到达最小值的另一侧,那么梯度将具有相反的方向。我们取基于动量的梯度下降建议的更新和估计目标点处的梯度的加权平均。如果我们离最小值很远,这大致等同于基于动量的梯度下降。但如果我们即将超过最小值,将会发生抵消,更新将小于纯动量情况下的更新。因此,我们减少了超过最小值的机会。
-
AdaGrad 是一种优化技术,它给损失超曲面中不经常变化的轴赋予额外的权重。Adam 优化器结合了许多其他优化器的优点,通常是现代优化器的首选。
-
-
奥卡姆剃刀原理本质上说的是,在足够的解释中,最简单的一个应该被优先考虑。在机器学习中,这导致了正则化。通常有很多解决损失最小化问题的方法。我们希望选择最简单的一个。因此,我们在其他任何损失上添加一个偏离简单性的惩罚(正则化损失)。这激励系统寻求最简单的解决方案。正则化损失通常是参数向量的总长度;因此,正则化推动我们朝着参数绝对值更小的解决方案发展。
-
在没有正则化项的情况下最小化损失函数等价于贝叶斯最大似然估计(MLE)技术。另一方面,带有正则化项的最小化损失等价于最大后验(MAP)估计。
-
过度拟合是一种现象,其中神经网络已经学会了训练数据中的所有细微差别。由于训练数据中的异常细微差别通常是由噪声引起的,这导致在推理过程中的性能变差。过度拟合表现为在训练数据上具有很高的准确率(低损失),但在测试数据上准确率较差。这通常是因为网络的表达能力超过了解决问题所需的程度,并试图记住训练数据中的所有角落和弯道。正则化可以缓解这个问题。另一个技巧是 dropout,我们在训练迭代中故意关闭一个随机子集的神经元。这迫使所有神经元学会用更少的神经元来完成工作。
第十章:神经网络中的 10 种卷积
本章涵盖
-
神经网络的图形和代数视图
-
带自定义权重的二维和三维卷积
-
向神经网络添加卷积层
图像分析通常涉及识别局部模式。例如,为了进行面部识别,我们需要分析眼睛、鼻子和耳朵对应的相邻像素的局部模式。照片的主题可能站在海滩上,面对着大海,但涉及沙子和水的整体画面是不相关的。
卷积是一种特殊的操作,它检查输入信号中的局部模式。这些操作通常用于分析图像:即输入是一个像素的二维数组。为了说明这一点,我们考察了一些特殊用途的卷积操作示例,这些操作检测图像中像素小邻域的边缘、角落和平均光照。一旦我们检测到这些局部属性,我们就可以将它们组合起来,识别更高层次的模式,如耳朵、鼻子和眼睛。我们可以依次将它们组合起来,以检测更高层次的结构,如面部。系统自然地适合多层卷积神经网络
网络——最低层(最接近输入)检测边缘和角落,下一层检测耳朵、眼睛、鼻子等等。
在第 8.3 节中,我们讨论了线性神经网络层(也称为全连接层)。在那里,每个输出都与所有输入相连。这意味着输出是通过取所有输入值的加权线性组合得到的。换句话说,输出是从输入的全局视角得到的。卷积层不同。这些层的特点是:
-
局部连接—只有一小部分相邻的输入值与一个输出值相连。因此,每个输出值只是少量相邻输入值的加权线性组合。结果,只有输入中的局部模式被捕捉到。
-
共享权重—相同的权重在整个输入上滑动。因此,
-
权重的数量大大减少。由于卷积通常用于图像上,输入大小是大量像素),全连接层成本过高。卷积在输入上重复(通常是小的)数量的权重,从而保持权重的数量可管理。
-
提取的局部模式在输入的整个区域都是固定的。如果卷积是一个边缘检测器,它将在整个输入中提取边缘。我们不能在输入的一个区域有边缘检测器,而在另一个区域有角点检测器,例如。当然,在多层网络中,我们可以使用不同的卷积层来捕捉不同的局部模式。特别是,连续的层可以捕捉输入局部模式中的局部模式,依此类推,从而在网络的更高层捕捉到越来越复杂和越来越全局的模式。
-
捕捉到的确切局部模式取决于卷积算子的权重。我们不知道要捕捉输入的哪些局部模式来识别感兴趣的特定高级结构(例如人脸)。这意味着我们不想指定卷积的权重。神经网络的全部目的就是避免这种定制特征工程。相反,我们希望通过第八章中描述的训练过程学习卷积层的权重。损失可以通过卷积像通过全连接层一样进行反向传播。
就像全连接层一样,卷积层可以表示为矩阵-向量乘法。权重矩阵的结构是方程 8.8 的特殊情况,但本质上它是一个矩阵。因此,前向传播方程 8.7 和反向传播方程 8.31 以及 8.33 仍然适用。通过卷积进行的前向传播和反向传播(训练)过程与全连接层完全相同。
由于卷积是在神经网络中学习得到的——而不是预先指定的——我们无法预测这样的层将学习提取哪些局部模式(尽管在实践中,初始层通常学会识别边缘和角点)。我们所知道的是,给定层中的每个输出仅来自前一层中空间相邻的一小部分输入值。最终输出是从对输入的分层局部检查中得到的。
注意:第十章的完整功能代码,可通过 Jupyter Notebook 运行,可在我们的公共 GitHub 仓库中找到,网址为mng.bz/M2lW。
10.1 一维卷积:图形和代数视图
像往常一样,我们通过一系列示例来检验卷积的过程。我们检验了一维、二维和三维的卷积,但为了便于理解,我们首先从一维开始。
最直观地可视化一维卷积的方法是想象一根被拉伸、拉直的绳子(输入数组),在其上滑动一个测量尺(核)。
-
在图 10.1,10.2,和 10.3 中,尺子核以阴影框的形式显示,而绳子(输入数组)以一系列白色框的形式显示。图中的连续步骤代表滑动尺子的连续位置(也称为滑动停止点)。请注意,阴影部分在每个步骤中占据不同的位置。
-
在滑动过程中,连续位置的尺子可以重叠。在图 10.1,10.2,和 10.3 中,它们以不同的量重叠。
-
在现实中,绳子和尺子是离散的 1D 数组。在每个滑动停止点,尺子数组的元素位于绳子数组元素的一个子集上。
-
我们将每个输入数组元素与其上的核元素相乘,并将乘积相加。这相当于对落在当前核(尺子)位置下的输入(绳子)元素进行加权求和,其中核元素作为权重。这个加权求和作为一个单独的输出元素发出。每个瓷砖的滑动停止点产生一个输出元素。当尺子从左到右在整个绳子上滑动时,生成一个 1D 输出数组。

图 10.1 大小为 3 的局部平均核的 1D 卷积,步长为 1,有效填充,在大小为 7 的输入数组上

图 10.2 大小为 3 的局部平均核的 1D 卷积,步长为 2,有效填充

图 10.3 大小为 2 的边缘检测核的 1D 卷积,步长为 1,有效填充。并非所有滑动停止点(即步骤)都显示出来。
以下实体用于 1D 卷积:
-
输入——一个一维数组。在 1D 卷积中,我们通常用符号n来表示输入数组长度。在图 10.1 中,n = 7。
-
输出——一个一维数组。在 1D 卷积中,我们通常用符号o来表示输出数组长度。在图 10.1 中,o = 5。第 10.2 节展示了如何从独立参数计算输出大小。
-
核——一个权重的小数组,其大小是卷积的参数。在 1D 卷积中,我们通常用符号k来表示核大小。在图 10.1 中,k = 3;在图 10.3 中,k = 2。
-
步长—在完成单步之后,内核在输入元素上滑动的数量。我们通常用符号 s 来表示一维卷积中的步长。这是卷积的一个参数。在图 10.1 中,s = 1;在图 10.2 中,步长为 2。步长为 1 意味着在输入的每个后续元素处都有一个滑动停止。因此,输出元素的数量大致与输入相同(它们可能不完全相等,因为接下来会解释的填充)。步长为 2 意味着在每隔一个输入元素处都有一个滑动停止。因此,输出大小大致是输入大小的一半。步长为 3 意味着输出大小大致是输入大小的三分之一。
-
填充—当内核向输入数组的末端滑动时,其部分可能超出输入数组。换句话说,内核的一部分落在幽灵输入元素上。图 10.4 展示了这种情况:幽灵输入数组元素用虚线框表示。有多种策略来处理这种情况:
-
有效填充—每当内核的任何元素超出输入数组时,我们就停止滑动。没有涉及幽灵输入元素;内核的 整个 总是落在有效的输入元素上(因此得名 valid)。请注意,这暗示我们将有比输入更少的输出。如果我们尝试生成与,比如说,最后一个输入元素对应的输出,除了第一个内核元素外,所有内核元素都将落在幽灵元素上的输入之外。因此,我们必须在最右侧的内核元素落在最右侧的输入元素上时停止(参见图 10.1、10.2 和 10.3)。此时,最左侧的内核元素落在第 (N − k) 个输入元素上。我们不生成最后 k 个输入的输出。因此,即使对于有效填充的步长为 1,输出也略小于输入。
-
相同(零)填充—在这里,我们不希望提前停止。如果步长为 1,输出大小与输入大小匹配(因此得名 same)。我们继续滑动内核,直到其左端落在最右侧的输入上。此时,除了最左侧的内核元素外,所有元素都落在幽灵输入元素上。我们假设这些幽灵输入元素具有 0(零填充)的值。
-

图 10.4 3x 大小、步长 2、零填充的 1D 卷积
让我们用 S 来表示输入数组的域。它是一个一维网格:
S = [0, W − 1]
S 中的每一个点都与一个值 X[x] 相关联。这些值共同构成了输入 X。在这个输入点网格上,我们定义了一个输出点子网格 S[o]。S[o] 通过在输入上应用基于步长的跳跃从 S 中获得。假设 s = [s[W]] 表示步长,第一个滑动停止点的绳索左上角在 (x = 0)。下一个滑动停止点在 (x = s[W]),然后是 (x = 2s[W]),以此类推。当我们到达右侧时,我们停止。总的来说,输出网格由核(尺子)在输入体积上扫过时其左上角所停的滑动停止点组成:即 S[o] = {(x = 0), (x = s[W])⋯,}。S[o] 中的每个点都有一个输出。
方程式 10.1 展示了在 1D 卷积中单个输出值的生成过程。X 表示输入,Y 表示输出,而 W 表示核权重:

方程式 10.1
注意,当尺寸为 k[W](尺子)的核在其原点 x 上时,它覆盖了域 [x..(x + k[W])] 内的所有输入像素。这些是参与方程 10.1 的像素。每个输入像素都乘以覆盖它的核元素。将方程 10.1 与图 10.1、10.2 和 10.3 进行匹配。
10.1.1 通过 1D 卷积进行曲线平滑
在本节中,我们从物理和代数角度探讨如何通过卷积进行局部平均,以获得全面的理解。权重向量为
的 1D 核(如图 10.1 所示)本质上是对连续的三组输入值进行移动平均。因此,它是一个局部平均(也称为平滑)算子。如果我们检查原始输入向量与通过核卷积的输入向量的图(图 10.5),这一点就会变得明显。输入(实线)上下波动,而输出则是一条通过输入平均位置的平滑曲线(虚线)。一般来说,通过具有所有相等权重的核(权重必须归一化,即权重的总和为 1)卷积产生的输出是输入的平滑(局部平均)版本。我们为什么要平滑输入向量呢?因为它捕捉了输入数据中的广泛趋势,同时消除了短期波动(通常由噪声引起)。如果你熟悉傅里叶变换和频域,你可以看到这本质上是一个低通滤波器,消除短期、高频噪声,并捕捉输入数据数组中的长期、低频变化。

图 10.5 从图 10.1 中输入数组(实线)和输出数组(虚线)的图形。注意这是输入的平均值版本。这种局部平滑通过消除高频(短期)噪声来捕捉函数的低频(长期)广泛趋势。
10.1.2 通过 1D 卷积进行曲线边缘检测
如前所述,卷积对输入数组的影响在卷积核的权重上发生了根本性的变化。现在让我们检查一个完全不同的核,它可以检测输入数据中的边缘。
一个边缘被定义为输入数组中值的急剧变化。例如,如果输入数组中连续的两个元素在值上有很大的绝对差异,那么这就是一个边缘。如果我们绘制输入数组(即在y轴上绘制输入数组值与数组索引的关系图),则图中将出现边缘。例如,考虑图 10.3 中的输入数组(如图 10.6 所示)。在索引 0 到 3 之间,我们有值在 10 的附近。在索引 4 处,值跳到 51。我们说在索引 3 和 4 之间存在边缘。然后这些值在索引 4 到 7 之间保持在 50 的附近。然后它们在剩余的索引中跳回到 10 的附近。我们说在索引 7 和 8 之间存在另一个边缘。我们在这里检查的卷积将在跳跃的索引 3 和 7 处产生高响应(输出值),在其他索引处产生低响应。这是一个边缘检测卷积(滤波器)。我们为什么要检测边缘呢?因为边缘对于理解图像很重要。信号快速变化的地点比平坦均匀区域提供了更多的语义线索。对人类视觉皮层的实验表明,人类比平坦区域更关注颜色或阴影快速变化的地点。

图 10.6 从图 10.3 中输入数组(实线)和输出数组(虚线)的图形。输出是输入。边缘提供了理解信号的重要线索。
10.1.3 一维卷积作为矩阵乘法
从代数上讲,大小为 3、步长为 1、有效填充的卷积可以用以下方式表示。设输入数组为
= [x[0] x[1] x[2] x[3] x[4] … x[n-3] x[n-2] x[n*-1]]
卷积核是一个大小为 3 的权重矩阵;让我们称它为
= [w[0] w[1] w[2]]
如图 10.1 所示,在卷积的步骤 0 中,我们将这个核放在输入x[0]的 0 号元素上。因此,w[0]落在x[0]上,w[1]落在x[1]上,w[2]落在x[2]上:
[x[0] x[1] x[2] x[3] x[4] … x[n-3] x[n-2] x[n*-1]]
其中粗体字样标识了与核权重对齐的输入元素。我们在对应位置乘以元素并将它们相加,得到输出 y[0] 的第 0 个元素 = w[0]x[0] + w[1]x[1] + w[2]x[2]。然后我们将核向右移动一个位置(假设步长为 1;如果步长为 2,我们将核移动两个位置,依此类推)。因此 w[0] 落在 x[1] 上,w[1] 落在 x[2] 上,w[2] 落在 x[3] 上:
[x[0] x[1] x[2] x[3] x[4] … x[n–3] x[n–2] x[n-1]]
再次,我们在对应位置乘以元素并将它们相加,得到输出 y[1] 的第一个元素 = w[0]x[1] + w[1]x[2] + w[2]x[2]。同样,在下一步中,我们将核向右移动一个位置:
[x[0] x[1] x[2] x[3] x[4] … x[n]]
相应的输出是 y[2] = w[0]x[2] + w[1]x[3] + w[2]x[4]。总体而言,步长 1,有效填充卷积的向量
与权重核
产生输出

你能看出发生了什么吗?我们实际上在这里正在取连续的 核 _ 大小 _ 的线性组合(参见第 2.9 节),即 3 个输入元素。换句话说,输出是输入数组元素的 移动加权局部和。根据实际的权重,我们正在提取输入的局部特性。
对于 有效 填充,最后一个输出由
[x[0] x[1] x[2] x[3] x[4] … x[n–3] x[n–2] x[n-1]]
生成输出
y[n − 3] = w[0]x[n − 3] + w[1]x[n − 2] + w[2]x[n − 1]
对于 相同 的零填充,最后一个输出由
[x[0] x[1] x[2] x[3] x[4] … x[n-1] 0 0]
生成输出
y[n−1] = w[0] ⋅ x[n−1] + w[1] ⋅ 0 + w[2] ⋅ 0
在第 8.3.1 节中,我们看到了全连接层(也称为线性层)可以表示为输入向量与权重矩阵的乘积。现在,我们将卷积表示为矩阵-向量乘积。权重矩阵具有分块对角结构,如方程式 10.2 所示。它是方程式 8.8 的一个特例。因此,前向传播方程式 8.7 和反向传播方程式 8.31 以及 8.33 仍然适用。因此,通过卷积进行的前向传播和反向传播训练与 FC 层完全相同。
方程式 10.2 表达了 核 _ 大小 _ 3,步长 1,有效填充卷积作为权重矩阵 W 与输入向量
的乘积:

方程式 10.2
注意方程式 10.2 中权重矩阵的 稀疏、块对角性质。这是卷积权重矩阵的特征。每一行包含所有在连续位置的核权重。核的大小通常远小于输入向量的大小。当然,为了进行矩阵乘法,权重矩阵的列数必须与输入向量的大小相匹配。因此,除了占据核权重的位置外,还有许多空位。我们用零填充这些空位。因此,权重矩阵的每一行都有所有核权重在某个连续位置出现,其余行则用零填充。核权重的位置随着每一行的连续而向右移动。这就是给权重矩阵带来块对角外观的原因。它也模拟了卷积所需的核滑动。每一行代表一个特定的滑动停止点,并生成输出向量中的一个元素。由于核位于行的固定位置,而其他行元素都是零,因此只有与核位置对应的输入元素被选中。其他输入元素乘以零:即,它们被忽略。
方程式 10.2 描述了步长为 1。例如,如果步长为 2,核权重将在连续行中移动两个位置。这显示在方程式 10.3 中:

方程式 10.3
注意,虽然方程式 10.3 提供了卷积的概念矩阵乘法视图,但这并不是实现卷积的最高效方式。PyTorch 和其他深度学习软件都有非常高效的卷积实现方式。
10.1.4 PyTorch - 使用自定义权重的单维卷积
我们已经讨论了 1D 输入向量与两个特定 1D 核的卷积。我们看到了,具有均匀权重的核,例如
,会导致输入向量的局部平滑,而具有反对称权重的核,例如
,会导致输出向量在输入向量的边缘位置出现峰值。现在我们将看到如何在 PyTorch 中设置 1D 核的权重,并使用该核进行 1D 卷积。
备注:这不是典型的 PyTorch 操作。更典型的操作是创建一个具有卷积层(我们指定大小、步长和填充,但不指定权重)的神经网络,然后训练网络以便学习权重。我们通常不关心学习到的权重的确切值。那么我们为什么要讨论在 PyTorch 中如何设置核的权重呢?主要是为了展示卷积在 PyTorch 中的工作方式,卷积对象的各个参数等等。
列表 10.1 PyTorch 1D 局部平均卷积的代码
import torch
x = torch.tensor( ①
[-1., 4., 11., 14., 21., 25., 30.])
w = torch.tensor([0.33, 0.33, 0.33]) ②
x = x.unsqueeze(0).unsqueeze(0)
w = w.unsqueeze(0).unsqueeze(0) ③
conv1d = torch.nn.Conv1d(1, 1, kernel_size=3, ④
stride=1, padding=[0], bias=False)
conv1d.weight = torch.nn.Parameter(w, requires_grad=False) ⑤
with torch.no_grad(): ⑥
y = conv1d(x) ⑦
① 实例化一个带噪声的输入向量。遵循方程 y = 5x
② 实例化卷积核的权重
③ PyTorch 预期输入和权重为 N × C × L 的形式,其中 N 是批大小,C 是通道数,L 是序列长度。在这里,N 和 C 是 1。torch.unsqueeze 将我们的 L-长度向量转换为 1 × 1 × L 张量。
④ 实例化平滑核
⑤ 设置核权重
⑥ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑦ 执行卷积
列表 10.2 PyTorch 1D 边缘检测的代码
import torch
x = torch.tensor( ①
[10., 11., 9., 10., 101., 99.,
100., 101., 9., 10., 11., 10.])
w = torch.tensor([0.5, -0.5]) ②
x = x.unsqueeze(0).unsqueeze(0) ③
w = w.unsqueeze(0).unsqueeze(0)
conv1d = torch.nn.Conv1d(1, 1, kernel_size=3, ④
stride=1, padding=[0], bias=False)
conv1d.weight = torch.nn.Parameter(w, requires_grad=False) ⑤
with torch.no_grad(): ⑥
y = conv1d(x) ⑦
① 实例化带有边缘的输入向量
② 实例化边缘检测核的权重
③ 将输入和权重转换为 1 × 1 × L
④ 实例化边缘检测核
⑤ 设置核权重
⑥ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑦ 执行卷积
这些列表展示了如何在 PyTorch 中使用 torch.nn.Conv1d 类执行 1D 卷积。这通常用于更大的神经网络,如后续章节中的那些。我们还可以使用 torch.nn.functional.conv1d 直接调用数学卷积操作。这需要输入和权重张量,并返回卷积输出张量,如列表 10.3 所示。
列表 10.3 直接调用卷积函数的 PyTorch 代码
import torch
x = torch.tensor( ①
[10., 11., 9., 10., 101., 99.,
100., 101., 9., 10., 11., 10.])
w = torch.tensor([0.5, -0.5]) ②
x = x.unsqueeze(0).unsqueeze(0) ③
w = w.unsqueeze(0).unsqueeze(0)
y = torch.nn.functional.conv1d(x, w, stride=1) ④
① 实例化输入张量
② 实例化权重张量
③ 将输入和权重转换为 1 × 1 × L
④ 执行卷积
10.2 卷积输出大小
考虑一个大小为 k 的核在大小为 n 的输入上滑动,步长为 s。给定一个大小为 k 的核,如果左端在索引 l,则右端在索引 l + (k − 1)。每次平移将核的左端(以及右端)向前移动 s。如果核的初始位置在索引 0,那么经过 m 次平移后,左端在 ms。相应地,右端在 ms + (k − 1)。假设有效填充,这个右端位置必须小于或等于 (n − 1)(输入数组的最后一个有效位置)。
我们可以平移多少次,直到核溢出输入?换句话说,m 的最大可能值是多少,使得
ms + (k − 1) ≤ (n − 1)
答案是

但每次平移都产生一个输出值。卷积的有效填充输出大小 o 是 m + 1(加一是为了考虑到初始位置)。因此,

如果我们在输入的每侧填充 p 个零,则输入大小变为 n + 2p。相应的输出大小是

方程式 10.4
这可以通过对每个维度重复执行来扩展到任意数量的维度。
10.3 二维卷积:图形和代数视图
人们常说,一张图片胜过千言万语。那么什么是图片呢?就深度学习而言,它是一个离散的二维实体——一个描述固定时间场景的像素值二维数组。每个像素代表一个颜色强度值。颜色值可以是一个表示灰度的单元素,也可以是三维的,对应于 R(红)、G(绿)、B(蓝)强度值。(在继续之前,你可能需要回顾第 2.3 节。)

图 10.7 2D 卷积使用大小为[3,3],步长为[1,1],有效填充的局部平均核。每个像素以一个小矩形表示,矩形中写有像素的灰度值。阴影顺序。连续步骤表示滑动停止。对于被核覆盖的每个像素,落在其上的核元素的权重以小字体写出。
在撰写本文时,图像分析是卷积最流行的应用。这些应用使用卷积来提取局部模式。我们是如何做到这一点的呢?特别是,我们能否将图像光栅化(从而将其转换为矢量)并使用一维卷积?
答案是不。要了解原因,请查看图 10.7。像素在位置(x = 0, y = 0)的空间邻域是什么?如果我们定义像素的邻域为包含该像素在左上角的曼哈顿距离为[2,2]的像素集合,那么(x = 0, y = 0)的邻域由图 10.7 中阴影矩形覆盖的像素集合组成,步骤 0。但这些像素不会是图像光栅化数组表示中的相邻元素。例如,像素(x = 0, y =1),其值为 6,是光栅化数组中的第五个元素,因此不会被视为(x = 0, y = 0)的邻居,(x = 0, y = 0)是光栅化数组中的第 0 个元素。二维邻域在光栅化过程中不会被保留。因此,二维卷积必须是一个超越仅仅将 2D 数组光栅化到 1D 并应用 1D 卷积的专门操作。
欧几里得距离和曼哈顿距离
欧几里得距离衡量两点之间的直线距离,而曼哈顿距离衡量两点之间的距离,但有一个约束,即你只能沿着轴平行行走,就像在曼哈顿的街道上一样)。让我们看看一个例子。
考虑两个点 A (3, 3)和 B (6, 7)。A 和 B 之间的欧几里得距离是线段 AB 的长度,可以计算为√((6 - 3)² + (7 - 3)²) = 5。A 和 B 之间的曼哈顿距离是(6−3) + (7−3) = 3 + 4 = 7。在本章中,我们用[3, 4]表示曼哈顿距离,以分别捕捉每个轴上的距离。
可视化二维卷积的最佳方式是想象一块墙(输入图像)上滑过一个瓦片(核):
-
在图 10.7、10.8 和 10.9 中,阴影矩形表示瓦片(核),而包含它的较大白色矩形表示墙(输入图像)。图中的连续步骤代表滑动瓦片的连续位置(也称为滑动停止)。请注意,阴影矩形在每个步骤中都占据不同的位置。
-
在滑动过程中,连续位置的瓦片可以重叠。在图 10.7、10.8 和 10.9 中,它们重叠的程度不同。
-
在现实中,墙和瓦片是离散的二维数组。在每个滑动停止时,瓦片数组元素位于墙数组元素的一个子集上。
-
我们将每个输入数组元素与其上方的核元素相乘,并将乘积相加。这相当于对输入(墙)元素进行加权求和,这些元素落在当前核(瓦片)的位置下,核元素作为权重。这个加权求和作为一个单独的输出元素发出。每个瓦片滑动停止都会产生一个输出元素。当瓦片在整面墙上从左到右、从上到下滑动时,就生成了一个二维输出数组。
在二维卷积中,输入数组、核大小和步长都是二维向量。正如在 1D 卷积中一样,以下实体在二维卷积中定义:

图 10.8 使用大小为 [3,3]、步长为 [2,2] 的 局部平均核 的二维卷积

图 10.9 使用大小为 2 的 边缘检测核、步长 1 和有效填充的二维卷积。并非所有滑动停止(即步数)都显示出来。注意输出在均匀位置为零,但在核的一半落在低值而另一半落在高值时会出现峰值。
-
输入—一个二维数组。我们通常使用符号 [H, W](分别表示数组的高度和宽度)来表示二维卷积中输入数组的大小。在图 10.7 中,H = 5,W = 5。
-
输出—一个二维数组。我们通常使用符号
= [o[H], o[W]] 来表示二维卷积中输出数组的维度。例如,在图 10.7 中,
= [3,3]。在 10.2 节中,我们看到了如何计算单维度的输出大小。我们必须对每个维度重复该计算一次,以获得高维度的输出大小。 -
内核—一个小的二维权重数组,其大小是卷积的一个参数。我们通常使用符号
= [k[H], k[W]] 来表示二维卷积中的内核大小(高度,宽度)。如果 (x, y) 表示二维内核左上角当前的位置,则右下角位于 (x + k[W] − 1, y + k[H] − 1)。在图 10.7 中,
= [3,3];在图 10.9 中,
= [2,2]。 -
步长—内核在完成单步滑动后覆盖的输入元素数量。我们通常使用符号
= [s[H], s[W]] 来表示二维卷积中的步长大小(高度,宽度)。如果 (x, y) 表示二维内核左上角当前的位置,则下一次移动将使内核的左上角位于 (x + s[W], y)。例如,在图 10.7 中,从步骤 0 到步骤 1 或步骤 1 到步骤 2 的转换。如果这种转换导致瓦片的部分超出墙壁——即 x + s[W] ≥ W——我们将下一次滑动位置设置为内核的左上角落在 (0, y + 1) 上。例如,在图 10.7 中,从步骤 2 到步骤 3 或步骤 5 到步骤 6 的转换。如果 y + s[H] ≥ H,我们停止滑动。步长大小是卷积的一个参数。在图 10.7 中,
= [1,1];在图 10.8 中,步长是
= [2,2]。与一维情况一样,步长
= [1,1] 表示在输入的每个连续元素处都有一个滑动停止。因此,输出元素的数量大致与输入相同(它们可能不完全相等,因为填充)。步长
= [2,2] 表示输入的每一行将产生一半行大小的输出元素,每一列将产生一半列大小的输出元素。因此,输出大小大致是输入大小的四分之一。总的来说,输入到输出大小的减少因子大致等于步长向量中元素的乘积。 -
填充—当内核沿着宽度或高度滑动到输入数组的边缘时,其部分可能超出输入数组。换句话说,内核的一部分覆盖了幽灵输入元素。与一维情况一样,我们通过填充来处理这种情况。二维卷积中的填充策略是一维的简单扩展:
-
有效填充—每当内核的任何元素超出输入数组时(无论是宽度还是高度),我们就停止滑动。没有涉及幽灵输入元素;内核的整个部分始终落在有效输入元素上(因此得名有效)。
-
相同(零)填充——在这里,我们不想提前停止。只要内核的左上角落在有效的输入位置上,我们就继续滑动。所以,如果步长是 1,1,输出大小将与输入大小匹配(因此得名same)。当我们接近输入行的末尾(输入的右端点)时,内核的最右侧列将超出输入范围。同样,当我们向输入的底部滑动时,内核的底部行将超出范围。如果我们接近输入的右下角,最右侧的列和最底部的行都将超出输入范围。规则是,所有在输入数组真实边界之外的幽灵输入值都被零替换。
-
让我们用S表示输入图像域。它是一个二维网格,其域是
S = [0, H − 1] × [0, W − 1]
S中的每个点都是一个具有颜色值(可以是标量——灰度值或三个值的向量,R、G、B)的像素。在这个输入点网格上,我们定义了一个输出点子网格S[o]。S[o]是通过在输入上应用基于步长的移动从S获得的。假设
= [s[H], s[W]]表示 2D 步长向量,第一个滑动停止点在
[0] ≡ (y = 0, x = 0)。下一个滑动停止点在
[1] ≡ (y = 0, x = s[W]),下一个是
[2] ≡ (y = 0, x = 2s[W])。当我们到达右侧时,我们增加y。总的来说,输出网格由内核(砖块)在输入体积上滑动时其左上角停留的滑动停止点组成:S[o] = {
[0],
[1], … }。S[o]中的每个点都有一个输出。
内核也有两个维度(实际上,它对应于输入通道和批次的两个额外维度——为了简单起见,我们现在忽略它们——如第 10.3.3 节所述)。方程 10.5 展示了如何在二维卷积中生成单个输出值。X表示输入,Y表示输出,W表示内核权重:

方程 10.5
注意,内核(瓦片)的起点在X[y, x]。其维度是(k[H], k[W])。因此,它覆盖了域[y..(y + k[H])] × [x..(x + k[W])]中的所有输入像素。这些是参与方程 10.5 的像素。每个输入像素都乘以覆盖它的内核元素。将方程 10.5 与图 10.7、10.8 和 10.9 进行匹配。
10.3.1 通过二维卷积进行图像平滑
在 10.1.1 节中,我们讨论了一维局部平滑。我们观察到它如何消除局部波动,以便更清晰地识别长期模式。在二维中也会发生同样的事情。图 10.10 显示了一幅在带有盐和胡椒噪声的背景上写有文字的图像。噪声没有语义意义;需要分析的是文字(可能通过光学字符识别)。我们可以通过使用具有均匀权重的核(如)进行二维卷积来消除噪声。

结果的去噪/平滑图像显示在图 10.11 中。均匀核的作用是什么?为了了解这一点,请看图 10.8。很明显,核使得每个输出像素成为相邻 3 × 3 输入像素的加权局部平均值。

(a) 输入图像

(b) 平滑/去噪输出图像
图 10.10 通过应用 2D 卷积
到图 10.11a 来去噪/平滑噪声图像
注意:完全功能的图像平滑代码,可通过 Jupyter Notebook 执行,可在mng.bz/aDM7找到。
10.3.2 通过二维卷积进行图像边缘检测
图像中的不是所有像素都具有相同的语义重要性。想象一下一张人站在白色墙前的照片。墙上的像素颜色均匀且无趣。提供最多语义线索的像素属于轮廓:边缘像素。这与人类视觉科学相一致,正如我们之前提到的,实验表明,人类大脑更关注颜色变化明显的区域。人类对待声音的方式也非常相似,忽略均匀的嗡嗡声(这种声音往往会导致睡眠)但会变得警觉,当声音的音量或频率发生变化时。因此,识别图像中的边缘对于图像理解至关重要。
边缘是局部现象。因此,可以通过使用特别选择的核进行二维卷积来识别它们。例如,图 10.11b 中的垂直边缘是通过在图 10.11a 中的图像上使用核

同样,图 10.11c 中的垂直边缘是通过在图 10.11a 中的图像上使用核


(a) 输入图像

(b) 通过对图 10.11a 应用二维卷积
检测到的垂直边缘

(c) 通过对图 10.11a 应用二维卷积
检测到的水平边缘
图 10.11 的图像经常帮助我们分析图像。
这些核如何识别边缘?为了了解这一点,请看图 10.9。在一个具有相等像素值的邻域(例如,平坦的墙壁),图 10.11b 中的核将产生零(正负核元素落在相等值上,它们的加权总和为零)。因此,这个核抑制了均匀区域。另一方面,如果颜色有急剧的变化(核的负半部和正半部落在非常不同的值上,加权总和是一个大的负数或大的正数),它会有很高的响应。
注意:完全功能的边缘检测代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/g4JV 找到。
10.3.3 PyTorch- 使用自定义权重进行二维卷积
我们已经讨论了二维输入数组与两个特定二维核的卷积。我们注意到,具有均匀权重的核,例如
,会导致输入数组的局部平滑,而具有反对称权重的核,例如
,会导致输出数组在输入数组的边缘位置出现峰值。现在我们将看到如何在 PyTorch 中设置二维核的权重并使用该核进行二维卷积。
注意:这不是典型的 PyTorch 操作。更典型的操作是创建一个具有卷积层的神经网络(我们指定大小、步长和填充,但不指定权重),然后训练网络以便学习权重。我们通常不关心学习到的权重的确切值。一个具有二维卷积层的示例神经网络可以在第 10.6 节中看到。
列表 10.4 展示了二维局部平均卷积。虽然我们在章节中看到输入数组是形状为 H × W 的二维张量,但 PyTorch 的卷积接口期望输入为形状为 N × C × H × W 的四维张量:
-
第一个维度,N,代表批大小。在真实的神经网络中,输入是以小批量而不是单个输入实例的方式提供的(这是为了效率原因,如第 9.2.2 节所述)。N 代表小批量中包含的输入图像数量。
-
第二维,C,代表通道数。对于整个神经网络的输入,在 RGB 图像的情况下,我们有三个通道 R(红色)、G(绿色)和 B(蓝色);在灰度图像的情况下,我们只有一个通道。对于其他层,通道数可以是任何东西,这取决于神经网络的架构。通常,离输入更远、离输出更近的层有更多的通道。只有在大输入处的通道具有固定的、明显可辨别的物理意义(如 R、G、B)。连续层输入处的通道则没有。
-
第三维,H,代表高度。
-
第四维,W,代表宽度。
PyTorch 的Conv2D对象的权重张量必须是一个 4D 张量。列表显示了一个大小为 5 × 5 的单色图像作为输入。因此,N = 1,C = 1,H = 5,W = 5。x被实例化为一个 5 × 5 的二维张量。为了将其转换为 4D 张量,我们使用torch.unsqueeze()函数,它向输入添加一个额外的维度。
列表 10.4 PyTorch 2D 局部平均卷积代码
import torch
x = load_img() ①
w = torch.tensor( ②
[
[0.11, 0.11, 0.11],
[0.11, 0.11, 0.11],
[0.11, 0.11, 0.11]
]
)
x = x.unsqueeze(0).unsqueeze(0) ③
w = w.unsqueeze(0).unsqueeze(0)
conv2d = torch.nn.Conv2d(1, 1, kernel_size=2,
stride=1, bias=False) ④
conv2d.weight = torch.nn.Parameter(w, requires_grad=False) ⑤
with torch.no_grad(): ⑥
y = conv2d(x) ⑦
① 加载带有噪声的灰度输入图像
② 实例化卷积核的权重
③ PyTorch 期望输入和权重以N × C × H × W的形式存在,其中N是批量大小,C是通道数,H是高度,W是宽度。在这里,N = 1,因为我们只有一个图像。C = 1,因为我们考虑的是灰度图像。H和W都是 5,因为输入是一个 5 × 5 的数组。unsqueeze 将我们的 5 × 5 张量转换为 1 × 1 × 5 × 5 张量。
④ 实例化 2D 平滑核
⑤ 设置内核权重
⑥ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑦ 运行卷积
列表 10.5 PyTorch 2D 边缘检测代码
import torch
x = load_img() ①
w = torch.tensor( ②
[[-0.25, 0.25],
[-0.25, 0.25]]
)
x = x.unsqueeze(0).unsqueeze(0) ③
w = w.unsqueeze(0).unsqueeze(0)
conv2d = torch.nn.Conv2d(1, 1, kernel_size=2, ④
stride=1, bias=False)
conv2d.weight = torch.nn.Parameter(w, requires_grad=False) ⑤
with torch.no_grad(): ⑥
y = conv2d(x) ⑦
① 加载带有边缘的灰度输入图像
② 实例化卷积核的权重
③ 将输入转换为 1 × 1 × 4 × 4
④ 实例化一个 2D 边缘检测核
⑤ 设置内核权重
⑥ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑦ 运行卷积操作
10.3.4 二维卷积作为矩阵乘法
在 10.1.3 节中,我们看到了如何将 1D 卷积视为通过一个分块对角矩阵(如方程 10.3 所示)乘以输入向量。这个想法可以扩展到更高维度,尽管权重矩阵变得显著更复杂。尽管如此,了解这个矩阵的直观形象是很重要的。在众多事情中,它将帮助我们更好地理解转置卷积。在这个以矩阵乘法为导向的二维卷积视图中,输入图像被表示为一个光栅化的 1D 向量。因此,一个大小为m × n的输入矩阵变成一个mn-大小的向量。相应的权重矩阵有长度为mn的行。每一行对应一个特定的滑动停止点。
为了便于理解,让我们考虑一个 [H, W] = [4,4] 的输入图像(不必在意这个图像是不切实际的很小)。在这张图像上,我们正在执行 [k[H], k[W]] = [2,2] 的内核和步长 [s[H], s[W]] = [1,1] 的 2D 卷积。情况正好如图 10.9 所示。大小为 H = 4, W = 4 的输入图像 X

光栅化到长度为 4 * 4 = 16 的输入向量
。设内核权重为
。考虑连续的滑动停止(图 10.9 中的步骤)。下面显示了在特定步骤中乘以内核权重的光栅化输入向量的确切元素——这些对应于图 10.9 中相同步骤的阴影项:图像 X 和内核 W 之间的 2D 卷积,记为 Y = W ⊛ X,在输入图像 [H, W] = [4,4] 的特殊情况下。对于此图像,使用 [k[H], k[W]] = [2,2] 的内核和步长 [s[H], s[W]] = [1,1] 的有效填充的 2D 卷积可以表示为以下矩阵乘法:

这可以表示为

方程式 10.6
注意以下内容:
-
方程式 10.6 中所示的 2D 卷积权重矩阵是特殊情况,但它说明了一般原理。
-
2D 卷积权重矩阵是块对角线形式,就像 1D 版本一样。内核权重被精确放置以模拟图 10.9。
-
卷积权重矩阵有 9 行和 16 列。因此,它接受一个从 4 × 4 输入图像光栅化的输入向量,并生成一个输出矩阵(可以折叠成一个 3 × 3 卷积输出图像)。
10.4 三维卷积
如果一张图片值千言万语,那么一段视频就值一万言。视频是关于动态现实场景的丰富信息来源。随着基于深度学习的图像分析(2D 卷积)越来越成功,在撰写本文时,视频分析已成为下一个需要征服的研究前沿。
视频本质上是一个三维实体。在所有三个维度上,其表示是离散的。这三个维度对应于空间,它是二维的,具有高度和宽度,以及时间。视频由一系列帧组成。每一帧是一个图像:一个离散的 2D 像素数组。一帧代表特定(采样)点的整个视频场景。帧中的一个像素代表场景在帧对应的时间点上的一个采样空间位置的颜色。因此,视频是一系列帧,代表在空间和时间的一组离散点(像素)上的动态场景。视频跨越一个时空体积(也称为ST 体积),可以想象为一个长方体。每个横截面是一个代表帧的矩形。这如图 10.12 所示。

图 10.12 一个时空体积光阴影长方体(代表视频)。在这个时空体积中,视频的单独帧是横截面矩形。一个单独的帧也以较暗的阴影显示。
为了分析视频,我们需要从这个 3D 体积中提取局部模式。我们能否通过重复的 2D 卷积来实现?
答案是不。当我们一起查看连续的帧时,会有额外的信息,而当我们逐个查看帧时,这些信息是不存在的。例如,想象你被展示了一个半开的门的图片。从这张单独的图片中,你能确定门是打开还是关闭的吗?不能。为了做出这个判断,我们需要看到几个连续的帧。换句话说,逐帧分析视频剥夺了我们一个重要的信息模态:运动,这只有在分析多个连续帧时才能理解。这就是为什么我们需要 3D 卷积。
最直观地可视化 3D 卷积的方式是想象一个砖块在整个房间的体积上滑动。房间对应于卷积的视频输入的时空体积。砖块对应于核。在滑动过程中,砖块会停在连续的位置;我们称之为滑块停止。图 10.13 显示了不同位置的四个滑块停止。每个滑块停止发出一个输出点。当砖块扫过整个输入时空体积时,生成一个输出时空体积。在每个滑块停止处,我们将每个输入像素值乘以覆盖它的核元素,并对这些乘积求和。这实际上是对所有被核(砖块)覆盖的输入(房间)元素的有效加权求和,覆盖的核元素作为权重。

(a) 滑块停止 x = 0, y = 0, t = 0。

(b) 滑块停止 x = 0, y = 0, t = 0。

(c) 滑块停止 x = 0, y = 0, t = 0。

(d) 滑动停止 x = 0, y = 0, t = 0。
图 10.13 3D 卷积的空间时间视图。每个图左侧较大的浅色长方体代表输入 ST 体积(房间)。房间内部的小深色长方体代表核砖块。砖块在整个房间内部体积上滑动。砖块的相邻位置可能在体积上重叠。砖块的每个位置代表一个滑动停止点;对房间(输入点)上所有被砖块覆盖的点进行加权求和。覆盖每个输入点的核点值(核值)作为权重。显示了四个不同的滑动停止点。每个滑动停止点生成一个单独的输出点。当砖块扫过输入体积时,生成一个输出 ST 体积(较小的浅色长方体)。
让我们用 S 表示输入 ST 体积。它是一个三维网格,其域为
S = [0, T − 1] × [0, H − 1] × [0, W − 1]
S 中的每个点都是一个具有颜色值(可以是标量——灰度值或三个值的向量,R、G、B)的像素。在这个输入点网格上,我们定义了一个输出点子网格 S[o]。S[o] 通过对输入应用基于步长的步进从 S 中获得。假设
= [s^T, s[K], s[W]] 表示 3D 步长向量,第一个滑动停止点在
[0] ≡ (t = 0, y = 0, x = 0)。下一个滑动停止点在
[1] ≡ (t = 0, y = 0, x = s[W]),下一个是
[2] ≡ (t = 0, y = 0, x = 2s[W])。当我们到达右侧时,我们增加 y。当我们到达底部时,我们增加 t。当我们到达房间的尽头时,我们停止。S[o] = {
[0],
[1] … } 是核(砖块)的左上角在扫过输入体积时的位置。每个 S[o] 中的点都有一个输出。核也有三个维度(实际上,它还有两个额外的维度对应于输入通道和批量——我们为了简单起见忽略它们,如第 10.4.2.1 节所述)。
方程式 10.7 展示了在 3D 卷积中单个输出值的生成过程。X 表示输入,Y 表示输出,W 表示核权重:

方程式 10.7
注意,核(砖块)的起点在 X[t], y, x 上。其尺寸为 (k^T, k[H], k[W])。因此,它覆盖了域 [t..(t + k^T)] × [y..(y + k[H])] × [x..(x + k[W])] 中的所有输入像素。这些是参与方程 10.7 的像素。每个这些输入像素都乘以覆盖它的核元素。将方程 10.7 与图 10.13 对比。
10.4.1 通过 3D 卷积进行视频运动检测
在动态场景中,移动物体从一个视频帧移动到另一个视频帧。因此,在运动边界处像素被覆盖或暴露。在一个帧中属于背景的像素可能在后续帧中被物体覆盖,反之亦然。如果背景颜色与物体不同,这将在不同时间相同空间位置的像素之间引起颜色差异,如图 10.14 所示。对 ST 体积应用卷积的输出是另一个 ST 体积。图 10.15 显示了将我们的视频运动检测器应用于图 10.14 中所示输入的输出结果。

图 10.14 移动球合成视频的连续帧,为说明目的以叠加方式显示,并逐渐增加不透明度
内核是如何从一系列连续帧中提取运动信息的?如前所述,运动会导致连续帧中相同位置的像素具有不同的颜色。然而,由于噪声,单个孤立的像素对可能具有不同的颜色——我们不能从这个中得出任何结论。如果我们在一个帧的小邻域内平均像素值,并在后续帧的相同邻域内平均像素值,并且这两个平均值不同,那么这是一种更可靠的估计运动的方法。以下是一个 2 × 3 × 3 的三维核,正是为了做到这一点——在两个连续帧的 3 × 3 空间邻域内平均像素值,并从其中一个减去另一个:

减法的结果在运动区域较高,在无运动区域较低。在此背景下,值得注意的是,由于物体颜色均匀,物体内部的像素是不可区分的。因此,在物体的中心没有观察到运动;只有在边界处观察到运动。图 10.15 显示了此 3D 卷积结果的几个帧。
注意:完全功能的视频运动检测代码,可通过 Jupyter Notebook 执行,可以在mng.bz/enJQ找到。

(a) 输出帧 0

(b) 输出帧 1

(c) 输出帧 2

(d) 输出帧 3
图 10.15 将 3D 卷积运动检测器应用于移动球合成视频的结果。灰色表示“无运动”;大部分输出帧都是灰色。白色和黑色表示运动。
10.4.2 PyTorch- 使用自定义权重的三维卷积
在第 10.4.1 节中,我们看到了如何使用 3D 卷积在输入图像序列中检测运动。在本节中,我们将看到如何在 PyTorch 中实现这一点。PyTorch 对 3D 卷积的接口期望输入张量是 5 维的,形式为N × C × D × H × W。除了第 10.4 节中讨论的维度外,还有一个额外的输入通道维度。因此,每个输入通道都有一个单独的砖块。我们正在将它们全部组合(取它们的加权总和):
-
如同在 2D 卷积的案例中讨论的那样(第 10.3.3 节),第一个维度N代表批量大小(为了效率,将批量的小批量输入到实际神经网络中,而不是单个输入实例),而C代表输入通道数。
-
D代表序列长度。在我们的运动检测器示例中,D代表输入到 3D 卷积层的连续图像帧的数量。
-
第三个维度H代表高度,第四个维度W代表宽度。
在我们的运动检测器示例中,我们有一个由五个灰度图像组成的序列作为输入,每个图像的高度为 320,宽度为 320。由于我们只考虑单个图像序列,N = 1。所有图像都是灰度的,这意味着C = 1。序列长度D等于 5。H和W都是 320。
PyTorch 期望 3D 核的形式为C[out] × C[in] × k[T] × k[H] × k[W]:
-
第一个维度C[out]代表输出通道数。你可以将卷积核视为一个 3D 滤波器库,其中每个滤波器产生一个输出通道。C[out]是库中 3D 滤波器的数量。
-
第二个维度C[in]代表输入通道数。这取决于输入张量中的通道数。当我们处理灰度图像时,在顶层输入中C[in]为 1。对于 RGB 图像,在顶层输入中C[in]为 3。对于输入层之后的层,C[in]等于输入到该层的张量中的通道数。
-
第三、第四和第五维度k[T]、k[H]和k[W]分别代表沿T、H和W维度的核大小
在我们的运动检测器示例中,我们有一个单个核,k[T]=2,k[H]=3,k[W] = 3。由于我们只有一个核,C[out] = 1。并且由于我们处理的是灰度图像,C[in]也是 1。
列表 10.6 PyTorch 3D 卷积的代码
import torch
images = load_images() ①
x = torch.tensor(images) ②
w_2d_smoothing = torch.tensor( ③
[[0.11, 0.11, 0.11],
[0.11, 0.11, 0.11],
[0.11, 0.11, 0.11]]).unsqueeze(0)
w = torch.cat(
[-w_2d_smoothing, w_2d_smoothing]) ④
x = x.unsqueeze(0).unsqueeze(0) ⑤
w = w.unsqueeze(0).unsqueeze(0) ⑥
conv3d = nn.Conv3d(1, 1, kernel_size=[2, 3, 3], ⑦
stride=1, padding=0, bias=False)
conv3d.weight = torch.nn.Parameter(w, requires_grad=False)
with torch.no_grad(): ⑧
y = conv3d(x) ⑨
① 加载五个形状为 320 × 320 的灰度图像序列
② 转换为形状为T × H × W = 5 × 320 × 320 的张量
③ 实例化一个形状为 3 × 3 的 2D 平滑核。添加一个额外的维度,以便可以将两个 2D 核堆叠在一起形成一个 3D 核。
④ 沿着第一个维度将 2D 平滑核及其反转版本连接起来,形成一个形状为 2 × 3 × 3 的 3D 核
⑤ 将输入张量转换为N × C × T × H × W = 1 × 1 × 5 × 320 × 320
将 3D 核转换为 C[out] × C[in] × k[T] × k[H] × k[W] = 1 × 1 × 2 × 3 × 3
⑦ 实例化并设置 Conv3d 层的权重
⑧ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑨ 执行卷积操作
10.5 转置卷积或分数步长卷积
如往常一样,我们通过一个例子来探讨这个主题。考虑一个大小为 3 的核
= [w[0] w[1] w[2]] 的 1D 卷积,使用有效填充。让我们考虑一个特殊情况,其中输入大小 n 为 5。根据方程 10.2,这个卷积可以表示为从权重向量
构造的块对角矩阵 W 与输入向量
的乘积,如下所示:

如果我们将输出向量
乘以转置矩阵 W^T,会发生什么?

以下是一些观察结果:
-
我们并没有完全从
中恢复
,但我们已经生成了一个与
大小相同的向量,x̃。通过乘以卷积权重矩阵的转置执行一种上采样,撤销正向卷积产生的下采样。 -
从
中恢复
是不可能的。这是因为当我们从
构造
时,我们乘以 W 并将具有五个独立元素的向量转换为具有三个独立元素的向量——一些信息已经无法恢复。这种直觉与事实一致,即 5 × 3 矩阵 W 是 不可逆的:没有 W^(−1),因此无法得到
= W^(−1)
。 -
在转置卷积过程中,我们将
的元素以与进行正向卷积时相同的比例分配回x̃中的元素(参见图 10.16)。这应该会让你想起第八章中的反向传播。在那里,在方程 8.24 的右侧,我们看到了对于线性层,正向传播相当于乘以一个任意的权重矩阵W(如方程 8.8 所示)。反向传播涉及乘以相同的权重矩阵的转置(方程 8.31)。反向传播进行的是成比例的责任分配——损失以与它们在创建输出中的贡献成比例的比例分配回输入。这里发生的情况也是一样。因此,乘以转置权重矩阵通常以与它对输出贡献相同的比例分配输出。

图 10.16 1D 卷积及其转置
这种思想可以扩展到更高维度。图 10.17 说明了 2D 转置卷积操作。

图 10.17 2D 卷积及其转置
10.5.1 转置卷积的应用:自编码器和嵌入
转置卷积通常在自编码器中是必需的。在此处,我们简要概述自编码器,以解释为什么它们需要转置卷积。到目前为止,我们查看的大多数神经网络都是监督分类器的例子,它们接收输入并直接输出输入所属的类别。这不是唯一可能的范式。如第 6.9 节所暗示,我们还可以将输入映射到一个向量,通常称为嵌入,也称为描述向量,它捕捉感兴趣类别的本质方面,并丢弃可变方面。例如,如果感兴趣的类别是人类,那么给定一个图像,嵌入将只捕获识别图像中人类的特征,而忽略背景(天空、海洋、森林、建筑等)。
从输入到嵌入的映射是通过一个称为编码器的神经网络完成的。如果输入是图像,编码器通常包含一系列卷积层。
我们如何训练这个神经网络?我们如何定义它的损失?好吧,一种可能性是嵌入必须保持对原始输入的忠实度:也就是说,我们应该能够从嵌入中重建(至少是近似地)输入。记住,嵌入的大小(自由度较少)比输入小,因此完美的重建是不可能的。尽管如此,我们可以定义损失为原始输入和重建输入之间的差异(例如,欧几里得距离或二值交叉熵损失)。
我们如何从嵌入中重建输入?这就是转置卷积发挥作用的地方。记住,我们在编码器中进行了卷积(可能多次)以生成嵌入。我们可以在嵌入上执行一系列转置卷积以生成与输入相同大小的张量。执行此重建的网络称为解码器。解码器生成我们的重建输入。
我们定义损失为原始输入和重建输入之间的差异。我们可以训练以最小化损失并学习编码器和解码器的权重。这被称为端到端学习,编码器-解码器对被称为自动编码器。
我们使用许多数据实例来训练自动编码器,所有这些实例都属于感兴趣的类别。由于它没有记住整个图像的奢侈(嵌入的大小比输入小),它被迫学习如何保留所有训练图像的共同特征:即描述感兴趣类别的特征。在我们的例子中,自动编码器将学习保留识别人类的特征并丢弃背景。请注意,这也可能导致一种非常有效的压缩技术——嵌入是图像的紧凑表示,其中只保留了感兴趣的物体。
10.5.2 转置卷积输出大小
通过反转方程式 10.8 可以获得转置卷积的输出大小:
o^′ = (n^′−1)s + k − 2p
方程式 10.8
例如,在大小为 n^′ = 3 的
上使用步长 s = 1 的转置卷积(有效填充 p = 0)和一个大小为 k = 3 的内核,会创建一个大小为 o^′ = 5 的输出 x̃。
10.5.3 通过转置卷积进行上采样
在上一节中,我们简要讨论了自动编码器,其中编码器网络将输入图像映射到嵌入,解码器网络试图从嵌入中重建输入图像。编码器网络通过一系列卷积和池化层将高分辨率输入转换为低分辨率嵌入(我们将在下一章详细讨论池化层)。试图从嵌入中重建原始图像的解码器网络必须将低分辨率输入上采样/上采样到高分辨率输出。
许多插值技术,如最近邻、双线性插值和双三次插值,可以用于执行此上采样操作。这些技术通常使用预定义的数学函数将低分辨率输入映射到高分辨率输出。然而,执行上采样的更优方式是通过转置卷积,其中映射函数是在训练过程中学习的,而不是预先定义的。神经网络将学习最佳方式将输入元素分布到更高分辨率的输出图中,以便最终重建误差最小化(即最终输出尽可能接近原始输入图像)。在本章中,我们不深入讨论自动编码器的训练细节;然而,我们展示了如何使用转置卷积对输入图像进行上采样:
-
输入数组被转换为形状为 N × C[in] × H × W 的 4D 张量,其中 N 是批处理大小,C[in] 是输入通道数,H 是高度,W 是宽度。
-
核是一个形状为 C[in] × C[out] × k[H] × k[W] 的 4D 张量,其中 C[in] 是输入通道数,C[out] 是输出通道数,k[H] 是核高度,k[W] 是核宽度。注意这与常规 2D 卷积核不同,常规 2D 卷积核的形状应为 C[out] × C[in] × k[H] × k[W]。本质上,输入和输出通道维度是互换的。
图 10.18 展示了一个输入形状为 1 × 1 × 2 × 2 的示例。卷积核的形状为 1 × 1 × 2 × 2。步长为 2 的转置卷积会产生一个形状为 1 × 1 × 4 × 4 的输出。

图 10.18 使用步长 2 的 2D 转置卷积进行上采样
注意:完全功能的转置卷积代码,可通过 Jupyter Notebook 执行,可在 mng.bz/radD 找到。
列表 10.7 使用转置卷积进行上采样的 PyTorch 代码
import torch
x = torch.tensor([ ①
[5., 6.],
[7., 8.]
])
w = torch.tensor([ ②
[1., 2.],
[3., 4.]
])
x = x.unsqueeze(0).unsqueeze(0) ③
w = w.unsqueeze(0).unsqueeze(0) ④
transpose_conv2d = torch.nn.ConvTranspose2d( ⑤
1, 1, kernel_size=2, stride=2, bias=False)
transpose_conv2d.weight = torch.nn.Parameter(w, ⑥
requires_grad=False)
with torch.no_grad(): ⑦
y = transpose_conv2d(x) ⑧
① 实例化输入张量
② 实例化核权重
③ 将输入张量转换为 N × C[in] × H × W = 1 × 1 × 2 × 2
④ 将核转换为 C[in] × C[out] × k[H] × k[W] = 1 × 1 × 2 × 2
⑤ 实例化转置卷积层
⑥ 设置核权重
⑦ 指示 PyTorch 不计算梯度,因为我们目前不需要它们
⑧ 执行转置卷积。y 的形状为 4 × 4。
10.6 将卷积层添加到神经网络中
到目前为止,我们一直在讨论具有自定义权重的卷积层,这些权重是我们设置的。虽然这让我们对卷积的工作原理有了概念性的理解,但在实际的神经网络中,我们并不自己设置卷积权重。相反,我们期望权重通过反向传播中的损失最小化来学习,正如第八章和第九章所描述的。我们将在下一章中查看流行的神经网络架构。但从编程的角度来看,最重要的学习内容是如何将卷积层添加到神经网络中。这就是我们在下一节要学习的内容。
在设置神经网络的过程中,我们指定其维度但不指定权重。我们还初始化权重值。权重值在反向传播(loss.backward()调用)过程中稍后更新(尽管 PyTorch 允许我们选择查看它们的值)。
10.6.1 在 PyTorch 中向神经网络添加卷积层
让我们看看如何在 PyTorch(完整的神经网络架构将在下一章中详细讨论)中将卷积层作为更大神经网络的一部分来实现:
-
神经网络通常子类化
torch.nn.Module基类并实现forward()方法。神经网络的层在__init__()函数中实例化。 -
使用
torch.nn.Sequential将多个层依次连接起来。第一层的输出被馈送到第二层,依此类推。 -
每个
torch.nn.Conv2d()代表一个单独的卷积层。我们的代码片段实例化了三个这样的卷积层,层之间有其他层(详细信息将在下一章中介绍)。
列表 10.8 PyTorch 示例卷积神经网络的代码
import torch
class SampleCNN(torch.nn.Module):
def __init__(self, num_classes):
super(LeNet, self).__init__()
self.nn = torch.nn.Sequential( ①
torch.nn.Conv2d(
in_channels=1, out_channels=6,
kernel_size=5, stride=1), ②
...
torch.nn.Conv2d(
in_channels=6, out_channels=16,
kernel_size=5, stride=1),
...
torch.nn.Conv2d(
in_channels=16, out_channels=120,
kernel_size=5, stride=1), ③
...
)
def forward(self, x): ④
out = self.nn(x)
return out
① 使用torch.nn.Sequential将一系列层连接起来。
② 实例化卷积层
③ 实现前向传播
④ 执行卷积
10.7 池化
到目前为止,我们已经看到了卷积层如何在输入图像上滑动并生成包含描述图像的重要特征的输出特征图。我们在 1D、2D 和 3D 设置中探讨了这一点。(我们将在下一章中更多地讨论这一点。)卷积层的一个主要缺点是它对输入中特征的定位非常敏感。输入特征位置的微小变化可能导致不同的输出特征图。这种变化可能由于相机角度变化、旋转、裁剪、物体以不同距离从相机存在等原因在现实世界中发生。我们如何处理这种变化并使神经网络更加鲁棒?
一种实现方式是通过下采样。特征图的一个低分辨率版本仍然包含重要的特征,但精度/粒度较低。因此,即使重要特征在高分辨率特征图中位于略微不同的位置,它们在低分辨率特征图中也大致位于相同的位置。这也被称为局部平移不变性。
在卷积神经网络中,下采样操作是通过池化层来执行的。池化层本质上是在整个图像上滑动一个小滤波器。在每个滤波器位置,它们使用池化操作捕获局部补丁的摘要。以下是最流行的两种池化操作类型:
-
最大池化—计算每个补丁的最大值
-
平均池化—计算每个补丁的平均值
图 10.19 详细说明了这一点。输出特征图的大小取决于核大小和池化层的步长。例如,如果我们使用 2 × 2 核和步长为 2,如图 10.19 和 10.20 所示,输出特征图变为输入特征图大小的一半。同样,使用 3 × 3 核和步长= 3 使得输出特征图大小为输入特征图的三分之一。

图 10.19 使用 2 × 2 核和步长 2 的最大池化。结果输出特征图是输入特征图大小的一半。输出特征图中的每个值是输入特征图中相应局部补丁的最大值。
列表 10.9 PyTorch 代码用于最大池化和平均池化
import torch
X = torch.tensor([ ①
[0, 12, 26, 39],
[6, 19, 31, 44],
[12, 25, 38, 50],
[18, 31, 43, 57]
], dtype=torch.float32).unsqueeze(0).unsqueeze(0)
max_pool_2d = torch.nn.MaxPool2d( ②
kernel_size=2, stride=2)
out_max_pool = max_pool_2d(X) ③
avg_pool_2d = torch.nn.AvgPool2d( ④
kernel_size=2, stride=2)
out_avg_pool = avg_pool_2d(X) ⑤
① 实例化一个 4 × 4 输入张量
② 实例化一个步长为 2 的 2 × 2 最大池化层
③ 输出特征图大小为 2 × 2
④ 实例化一个步长为 2 的 2 × 2 平均池化层
⑤ 输出特征图大小为 2 × 2

图 10.20 使用 2 × 2 核和步长 2 的平均池化。结果输出特征图对应于输入特征图中的局部补丁。
摘要
在本章中,我们深入探讨了 1D、2D 和 3D 卷积及其在图像和视频分析中的应用:
-
卷积层有助于捕捉输入数据中的局部模式,因为它们只将一小组相邻的输入值连接到输出值。这与前几章中讨论的完全连接层(也称为线性层)不同,其中所有输入都连接到每个输出值。
-
卷积操作涉及在输入数组上滑动一个核。虽然从概念上可以将其视为矩阵乘法,但由于效率原因,实际上并不是这样实现的。核大小、步长和填充会影响输出的大小。
-
核在完成单步滑动后覆盖的输入元素数量称为步长。
-
当内核达到输入数组的末端时,其部分可能超出数组范围。为了处理这种情况,可以应用多种填充策略。在有效填充中,当内核的任何一个元素超出输入数组时,卷积操作就会停止。在相同(零)填充中,假设所有超出输入数组的内核元素输入值为零。
-
从概念上讲,1D 卷积可以看作是将测量尺(1D 内核)在拉直的绳索(1D 输入数组)上滑动。1D 卷积的实际应用包括曲线中的平滑和边缘检测。
-
从概念上讲,2D 卷积可以看作是将瓦片(2D 内核)在整个墙壁(2D 输入数组)的表面积上滑动。2D 卷积的实际应用包括图像中的平滑和边缘检测。
-
从概念上讲,3D 卷积可以看作是将砖块(3D 内核)在整个房间 3D 输入数组的整个体积上滑动。3D 卷积的实际应用包括图像序列中的运动检测。
-
在转置卷积中,输入数组元素与内核权重相乘,然后分布到输出数组中。转置卷积的实际应用包括上采样,将低分辨率输入转换为高分辨率输出。自动编码器使用转置卷积从嵌入中重建图像。
-
池化层本质上是在输入上滑动内核,捕获每个内核位置的局部区域的摘要。它们有助于提高卷积神经网络对输入特征微小变化的鲁棒性。最流行的两种池化操作是最大池化(计算局部区域的最大值)和平均池化(计算局部区域的平均值)。池化层导致输入数组下采样。输出大小取决于池化内核的大小和步长。
第十一章:用于图像分类和物体检测的神经网络
本章涵盖
-
使用更深的神经网络进行图像分类和物体检测
-
理解卷积神经网络和其他深度神经网络架构
-
纠正神经网络中的不平衡
如果人类看到图 11.1 中的图像,他们可以立即识别其中的物体,将它们分类为鸟类、飞机和超人。在图像分类中,我们希望赋予计算机这种能力——在图像中识别物体并将它们分类到一个或多个已知和预定的类别中。除了识别物体类别外,我们还可以识别图像中物体的位置。物体的位置可以用一个 边界框 来描述:一个边与坐标轴平行的矩形。边界框通常由四个参数指定:[(xtl, ytl),(xbr, ybr)],其中 (xtl, y**tl) 是边界框左上角的 xy 坐标,(xbr, ybr) 是边界框右下角的 xy 坐标。
如果我们还想识别它们在图像中的位置,这被称为 物体检测。图像分类和物体检测是计算机视觉中最基本的问题之一。虽然人脑几乎可以直观地对图像中的物体进行分类和定位,但我们如何训练机器来做这件事呢?在深度学习之前,计算机视觉技术涉及手动制作图像特征(以编码颜色、边缘和形状)并在这些特征之上设计规则来分类/定位物体。然而,这不是一个可扩展的方法,因为图像极其复杂且多样化。想想一个简单的物体,比如汽车。它可以有各种大小、形状和颜色。它可以从远处或近处(尺度)看到,从不同的视角(视角),在阴天或晴天(光照条件)。汽车可以停在繁忙的街道或山路上(背景)。几乎不可能设计出可以处理所有这些变化的特征和规则。

图 11.1 是鸟吗?是飞机吗?是超人吗?
在过去的 10 年中,一类新的算法出现了:卷积神经网络(CNNs)。它们不依赖于手工设计的特征,而是从 数据 中 学习 相关特征。这些模型在多个计算机视觉任务中取得了巨大的成功,实现了(有时甚至超过了)人类水平的准确性。它们在工业界的应用越来越广泛,从医疗诊断到电子商务再到制造业。在本章中,我们详细介绍了用于图像分类和目标检测的一些最流行的深度神经网络架构。我们探讨了它们的显著特征,深入研究了架构细节,以了解它们是如何以及为什么能工作的,并将它们应用于实际问题。
注意:本章的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/vojq 找到。
11.1 用于图像分类的 CNN:LeNet
在第十章中,我们讨论了 1D、2D 和 3D 场景下的卷积操作。我们还看到了如何将单个卷积层作为更大神经网络的一部分来实现。本节展示了如何使用具有多个卷积层的神经网络进行图像分类。(如果需要,鼓励您回顾第十章。)为此,让我们考虑 MNIST 数据集,这是一个包含大量手写数字(0 到 9)的大集合。它包含 60,000 张图像的训练集和 10,000 张图像的测试集。每个图像大小为 28 × 28,包含单个数字的中心裁剪。图 11.2 显示了 MNIST 数据集的样本图像。

图 11.2 MNIST 数据集的样本图像。(来源:“应用于文档识别的基于梯度的学习”;mng.bz/Wz0a.)
我们希望构建一个分类器,该分类器接受 28 × 28 的图像作为输入,并根据图像中包含的数字输出 0 到 9 的标签。为此任务最流行的神经网络架构之一是 LeNet,它由 LeCun 等人在 1998 年发表的论文“应用于文档识别的基于梯度的学习”中提出(mng.bz/Wz0a)。LeNet 架构如图 11.3 所示(LeNet 预期输入图像大小为 32 × 32,因此将 28 × 28 的 MNIST 图像调整为 32 × 32 大小后再输入网络):

图 11.3 LeNet。(来源:“应用于文档识别的基于梯度的学习”;mng.bz/Wz0a.)
-
它由三个具有 5 × 5 内核的卷积层组成,步长为 1。第一个卷积层生成 6 个大小为 28 × 28 的特征图。
第二个卷积层产生 16 个大小为 10 × 10 的特征图,第三个卷积层产生 120 个大小为 1 × 1 的特征图,这些特征图被展平成一个 120 维的向量)
-
前两个卷积层后面跟着下采样(也称为池化)层,这些层对特征图进行局部平均和下采样,从而降低特征图的分辨率和输出对输入中位移和畸变的敏感性。应用了一个大小为 2 × 2 的池化核,将特征图大小减少到原来的一半。有关池化的更多信息,请参阅 10.7 节。
-
每个特征图后面都跟着一个 tanh 激活层。这向网络引入了非线性,增加了其表达能力,因为它现在可以将输出建模为输入的非线性组合。如果我们没有非线性激活函数,无论我们有多少层,神经网络仍然会像单一线性层网络一样表现,因为多个线性层的组合只是另一个线性层。虽然原始的 LeNet 论文使用了 tanh 作为激活函数,但也可以使用几个其他激活函数,如 ReLU 和 sigmoid。ReLU 在 11.2.1.1 节中进行了详细讨论。sigmoid 和 tanh 的详细讨论可以在 8.1 和 8.1.2 节中找到。
-
输出特征图通过两个全连接(FC,也称为线性)层,最终产生一个代表每个类得分的 10 维logits向量。使用 softmax 层将 logits 得分转换为概率。
-
CrossEntropyLoss,在 6.3 节中讨论,用于计算预测概率和真实值之间的差异。
注意:特征图是一个与每个点关联固定大小向量的二维数组(即网格)。图像是特征图的一个例子,每个点是一个像素,关联的向量表示像素的颜色。卷积层将输入特征图转换为输出特征图。输出特征图通常具有更小的宽度和高度,但每个点的向量更长。
LeNet 在 MNIST 数据集上表现非常好,测试准确率超过 99%。接下来将展示 LeNet 的 PyTorch 实现。
11.1.1 使用 PyTorch 在 MNIST 上实现 LeNet 进行图像分类
注意:用于训练 LeNet 的完整功能代码,可通过 Jupyter Notebook 执行,可以在mng.bz/q2gz找到。
列表 11.1 LeNet 的 PyTorch 代码
import torch
class LeNet(torch.nn.Module):
def __init__(self, num_classes):
super(LeNet, self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=1, out_channels=6, ①
kernel_size=5, stride=1),
torch.nn.Tanh(), ②
torch.nn.AvgPool2d(kernel_size=2)) ③
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=6, out_channels=16,
kernel_size=5, stride=1),
torch.nn.Tanh(),
torch.nn.AvgPool2d(kernel_size=2))
self.conv3 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=16, out_channels=120,
kernel_size=5, stride=1),
torch.nn.Tanh())
self.fc1 = torch.nn.Sequential(
torch.nn.Linear(
in_features=120, out_features=84), ④
torch.nn.Tanh())
self.fc2 = torch.nn.Linear(
in_features=84, out_features=num_classes) ⑤
def forward(self, X): ⑥
conv_out = self.conv3(self.conv2(self.conv1(X)))
batch_size = conv_out.shape[0]
conv_out = conv_out.reshape(batch_size, -1) ⑦
logits = self.fc2(self.fc1(conv_out)) ⑧
return logits
def predict(self, X):
logits = self.forward(X)
probs = torch.softmax(logits, dim=1) ⑨
return torch.argmax(probs, 1)
① 5 × 5 卷积
② Tanh 激活
③ 2 × 2 平均池化
④ 第一个全连接层
⑤ 第二个全连接层
⑥ X.shape: N × 3 × 32 × 32。N 是批处理大小。
⑦ conv_out.shape: N × 120 × 1 × 1
⑧ logits.shape: N × 10
⑨ 使用 softmax 计算概率
11.2 向更深层的神经网络迈进
LeNet 模型并不是一个非常深的网络,因为它只有三个卷积层。虽然这对于在像 MNIST 这样的简单数据集上实现准确的结果是足够的,但它并不适用于现实世界的图像分类问题,因为它没有足够的表达能力来模拟复杂图像。因此,我们通常选择具有多个卷积层的更深层的神经网络。增加更多层可以实现以下功能:
-
由于额外的非线性而带来额外的表达能力——由于每一层都带来一组新的可学习参数和额外的非线性,深层网络可以模拟输入数据元素之间更复杂的关系。底层通常学习物体的简单特征,如线条和边缘,而高层学习物体的更抽象特征,如形状或线条集合。
-
以更少的参数达到相同的范围——让我们通过一个例子来检验这一点。考虑两个输出特征图,一个是由输入上的单个 5 × 5 卷积产生的,另一个是由输入上连续应用的两个 3 × 3 卷积产生的。假设步长为 1,相同的(零)填充。图 11.4 说明了这种情况。考虑输出特征图中的一个网格点。在两种情况下,网格点的输出值都是来自输入中的 5 × 5 块。我们称这个指示的 5 × 5 输入块是输出网格点的感受野。因此,在两种情况下,输出网格点是对相同输入的摘要:即,它表达了相同的信息。然而,在更深层的网络中,参数更少。单个 5 × 5 滤波器的参数数量是 25,而两个 3 × 3 滤波器的参数数量是 2 × 9 = 18(假设单通道输入图像)。这是一个 38%的差异。同样,如果我们比较一个 7 × 7 滤波器与三个 3 × 3 滤波器,它们具有相同的感觉野,但 7 × 7 滤波器的参数比 3 × 3 滤波器多 81%。

图 11.4 单个 5 × 5 卷积层与两个 3 × 3 卷积层的比较
现在,让我们看看一些用于图像分类的最流行的深度卷积网络。第一个重新点燃深度学习革命的是AlexNet,它由 Krizhevsky 等人于 2012 年发表。它在 ImageNet 大规模视觉识别挑战(ILSVRC)上显著优于所有之前的最佳算法,这是一个包含 130 万张图片的复杂数据集,跨越 1000 个类别。自从 AlexNet 以来,几个深度网络在之前的状态上取得了进步,如 GoogleNet、VGG 和 ResNet。在本章中,我们讨论了使每个网络工作的关键概念。为了详细了解它们的架构、训练方法和最终结果,我们鼓励您阅读每个部分中链接的原始论文。
11.2.1 VGG(视觉几何组)网络
VGG 系列网络是由牛津大学视觉几何组创建的(arxiv.org/pdf/1409.1556.pdf)。他们的主要贡献是对使用非常小的(3 × 3)卷积核的深度网络进行了彻底评估。他们证明了通过使用 3 × 3 卷积和具有 16-19 个权重层的网络,他们可以在 ILSVRC-2014 挑战赛上超越之前的最先进结果。与之前的工作相比,VGG 网络有两个主要区别:
-
使用较小的(3 × 3)卷积核——之前的网络通常在第一层卷积层使用 7 × 7 或 11 × 11 的大核。VGG 则在整个网络中只使用 3 × 3 核。如第 11.2 节所述,三个 3 × 3 滤波器具有与单个 7 × 7 滤波器相同的感受野。那么用三个较小的滤波器替换 7 × 7 滤波器能带来什么好处?
-
由于在每个卷积层末尾应用了 ReLU 激活函数,因此具有更多的非线性,从而具有更强的表达能力。
-
参数更少(49C²比 27C²),这意味着学习速度更快,对过拟合的鲁棒性更强。
-
-
移除局部响应归一化(LRN)层——LRN 最初是在 AlexNet 架构中引入的。它的目的是双重的:限制 ReLU 层的输出,ReLU 层是一个无界函数,其输出可以大到训练允许的程度;并鼓励侧抑制,其中神经元可以抑制其邻居的活动(这实际上起到了正则化的作用)。VGG 论文表明,添加 LRN 层并没有提高准确率,因此 VGG 选择从其架构中移除它们。
VGG 系列网络有五种不同的配置,这些配置主要区别在于层数(VGG-11、VGG-13、VGG-16 和 VGG-19)。无论具体配置如何,VGG 系列网络都遵循一个共同的结构。在这里,我们讨论这些共同点(详细差异描述可以在原始论文中找到):
-
所有架构都针对 224 × 224 输入图像。
-
所有架构都有五个卷积块(conv blocks):
-
每个块可以包含多个卷积层,并在末尾跟一个最大池化层。
-
所有单独的卷积层都使用 3 × 3 核,步长为 1,填充为 same,因此它们不会改变输出特征图的空间分辨率。
-
单个卷积块内的所有卷积层都有相同大小的输出特征图。
-
每个卷积层后面都跟着一个 ReLU 层,增加非线性。
-
每个卷积块的末尾都有一个最大池化层,将空间分辨率降低到一半。
-
-
由于每个卷积块通过 2 倍下采样,输入特征图被减少了 2⁵(32)次,从而得到 7 × 7 大小的输出特征图。此外,在每个卷积块中,特征图的数量翻倍。
-
所有架构都以三个 FC 层结束:
-
第一个将 51,277 大小的输入转换为 4,096 维输出。
-
第二个将得到的 4,096 维输出转换为另一个 4,096 维输出。
-
最后一个将得到的 4,096 维输出转换为 C 维输出,其中 C 代表类别数。在 ImageNet 分类的情况下,C 是 1,000。
-
VGG-11 的架构图显示在图 11.5 中。左侧的列表示每个层的输入张量的形状。右侧的列表示每个层的输出张量的形状。

图 11.5 VGG-11 架构图。所有形状都是 N × C × H × W 的形式,其中 N 是批量大小,C 是通道数,H 是高度,W 是宽度。
ReLU 非线性
正如我们之前讨论的,非线性层赋予了深度神经网络更多的表达能力来模拟复杂的数学函数。在第八章节中,我们研究了两个非线性函数:sigmoid 和 tanh。然而,VGG 网络(如 AlexNet)由一个不同的非线性层称为修正线性单元(ReLU)组成。为了理解这种选择的合理性,让我们回顾一下 sigmoid 函数并看看它的一些缺点。
图 11.6 绘制了 sigmoid 函数及其导数。如图所示,当输入为 0 时,梯度(导数)最大,随着输入的增加/减少,它迅速减小到 0。这对于 tanh 激活函数也是正确的。这意味着当 sigmoid 层之前神经元的输出要么很高要么很低时,梯度变得很小。虽然这可能在浅层网络中不是问题,但在大型网络中会成为一个问题,因为梯度可能太小,无法有效地进行训练。神经网络的梯度是通过反向传播计算的。根据链式法则,每一层的导数沿着网络相乘,从最终层开始,移动到初始层。如果每一层的梯度都很小,一个小的数乘以另一个小的数会得到一个更小的数。因此,初始层的梯度非常接近 0,使得训练无效。这被称为 梯度消失 问题。

图 11.6 1D sigmoid 函数(虚线曲线)及其导数(实线曲线)的图形
ReLU 函数解决了这个问题。图 11.7 展示了 ReLU 函数的图形。其方程如下
ReLU(x) = max(0, x)
方程 11.1
当 x 大于 0 时,ReLU 的导数是 1(常数),在其他地方都是 0。因此,它不会受到梯度消失问题的影响。今天的大多数深度网络都使用 ReLU 作为它们的激活函数。AlexNet 论文表明,使用 ReLU 非线性可以显著加快训练速度,因为它有助于更快地收敛。

图 11.7 ReLU 函数的图
PyTorch- VGG
现在我们来看看如何在 PyTorch 中实现 VGG 网络。首先,让我们实现单个卷积块,这是 VGG 网络的核心组件。这个卷积块将被多次重复,以形成整个 VGG 网络。
注意:VGG 网络的完整功能代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/7WE4 找到。
列表 11.2 卷积块的 PyTorch 代码
class ConvBlock(nn.Module):
def __init__(self, in_channels, num_conv_layers, num_features):
super(ConvBlock, self).__init__()
modules = []
for i in range(num_conv_layers):
modules.extend([
nn.Conv2d(
in_channels, num_features, ①
kernel_size=3, padding=1), ②
nn.ReLU(inplace=True)
])
in_channels = num_features
modules.append(nn.MaxPool2d(kernel_size=2)) ③
self.conv_block = nn.Sequential(*modules)
def forward(self, x):
return self.conv_block(x)
① 3 × 3 卷积
② ReLU 非线性
③ 2 × 2 最大池化
接下来,让我们实现卷积骨干(conv backbone)构建器,它允许我们通过简单的配置更改创建不同的 VGG 架构。
列表 11.3 卷积骨干的 PyTorch 代码
class ConvBackbone(nn.Module):
def __init__(self, cfg): ①
super(ConvBackbone, self).__init__()
self.cfg = cfg
self.validate_config(cfg)
modules = []
for block_cfg in cfg: ②
in_channels, num_conv_layers, num_features = block_cfg
modules.append(ConvBlock( ③
in_channels, num_conv_layers, num_features))
self.features = nn.Sequential(*modules)
def validate_config(self, cfg):
assert len(cfg) == 5 # 5 conv blocks
for i, block_cfg in enumerate(cfg):
assert type(block_cfg) == tuple and len(block_cfg) == 3
if i == 0:
assert block_cfg[0] == 3 ④
else:
assert block_cfg[0] == cfg[i-1][-1] ⑤
def forward(self, x):
return self.features(x)
① Cfg: [(in_channels, num_conv_layers, num_features),] 通过传递正确的 cfg,可以创建不同的 VGG 网络,而无需重复代码。
② 遍历卷积块配置
③ 实例化列表 11.2 中定义的卷积块
④ 必须有三个输入通道。
⑤ 前一个块的 out_Features 应等于当前块的 in_features。
使用包含每个卷积块配置列表的配置实例化卷积骨干。VGG-11 的配置包含较少的层,而 VGG-19 的配置包含更多的层。卷积骨干的输出被送入分类器,该分类器由三个全连接层组成。卷积骨干和分类器共同构成了 VGG 模块。
列表 11.4 VGG 网络的 PyTorch 代码
class VGG(nn.Module):
def __init__(self, conv_backbone, num_classes):
super(VGG, self).__init__()
self.conv_backbone = conv_backbone ①
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096), ②
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes)
)
def forward(self, x):
conv_features = self.conv_backbone(x)
logits = self.classifier(
conv_features.view(
conv_features.shape[0], -1)) ③
return logits
① 列表 11.3 中定义的骨干网络
② 分类器由三个线性层组成。前两个层后面跟着 ReLU 非线性。
③ 在传递给分类器之前将卷积特征展平
VGG-11 网络可以如下实例化。
列表 11.5 从特定配置实例化 VGG 网络的 PyTorch 代码
vgg11_cfg = [ ①
(3, 1, 64),
(64, 1, 128),
(128, 2, 256),
(256, 2, 512),
(512, 2, 512)
]
vgg11_backbone = ConvBackbone(vgg11_cfg) ①
num_classes = 1000
vgg11 = VGG(vgg11_backbone, num_classes) ①
① 创建 VGG-11 的 cfg
② 实例化卷积骨干
③ 实例化 VGG 网络
虽然我们已经讨论了如何在 PyTorch 中实现 VGG,但在实践中我们并不这样做,因为 torchvision 包已经实现了 VGG 网络,以及几个其他流行的深度网络。建议您使用此处所示的 torchvision 实现:
import torchvision
vgg11 = torchvision.models.vgg11()
11.2.2 Inception:网络内网络范式
之前,我们看到了如何通过增加神经网络的深度——即层数——来提高准确率,因为这样可以增加网络的表达能力。或者,我们也可以通过增加网络的宽度——即每一层的单元数量——来提高准确率。然而,这两种方法都存在两个主要缺点。首先,盲目地增加网络的大小可能导致过拟合,即网络会记住训练数据中某些模式,而这些模式在测试数据中并不适用。其次,在训练和推理过程中都需要更多的计算资源。Szegedy 等人在其论文《通过卷积加深网络》中介绍了 Inception 架构(arxiv.org/pdf/1409.4842v1.pdf),旨在解决这两个问题。Inception 架构在保持计算预算不变的情况下,增加了网络的深度和宽度。在本节中,我们将探讨 Inception 架构背后的主要思想。尽管 Inception_v2、Inception_v3、Inception_ResNet 等对其进行了多次改进,但我们讨论的是原始的:Inception_v1。
之前的深度学习架构通常按顺序堆叠卷积滤波器:每一层应用一组相同大小的卷积滤波器,并将其传递到下一层。每一层的滤波器核大小取决于架构。但是,在这种架构下,我们如何知道我们为每一层选择了正确的核大小?如果我们正在检测一辆车,比如说,车在图像中占据的图像区域(即像素数)在近距离拍摄的图像中与从远处拍摄的图像中是不同的。我们说车这个物体的尺度在这两个图像中是不同的。因此,在不同尺度上识别汽车所需的像素数将不同。较大的核更适合处理较大尺度的信息,反之亦然。被迫选择一个核大小的架构可能不是最优的。Inception 模块通过在每一层使用多个不同大小的核来解决这个问题,并取输出结果的加权组合。网络可以学会更重视适当的核,而不是其他核。Inception 模块的简单实现使用三种核大小在输入上进行卷积:1 × 1、3 × 3 和 5 × 5。还执行了最大池化操作,使用 3 × 3 核,步长为 1,填充为 1(以保持输出和输入大小相同)。输出结果被连接起来,并送入下一个 Inception 模块。参见图 11.8 以获取详细信息。

图 11.8 Inception_v1 架构
这个简单的 Inception 模块存在一个主要缺陷。使用少量 5 × 5 过滤器就会极大地增加参数数量。当我们添加池化层,其中输出过滤器的数量等于前一阶段的过滤器数量时,这变得更加昂贵。因此,将池化层的输出与卷积层的输出连接起来会导致输出特征数量的不可避免增加。为了解决这个问题,Inception 模块在 3 × 3 和 5 × 5 过滤器之前使用 1 × 1 卷积层来减少输入通道数。这极大地减少了 3 × 3 和 5 × 5 卷积的参数数量。虽然这看起来可能有些反直觉,但 1 × 1 卷积比 3 × 3 和 5 × 5 卷积便宜得多。此外,1 × 1 卷积在池化之后应用,见图 11.8。
使用维度降低的 Inception 模块构建了一个神经网络架构,这广为人知为 GoogLeNet。GoogLeNet 有九个这样的 Inception 模块线性堆叠。它有 22 层深(包括池化层为 27 层)。它在最后一个 Inception 模块之后使用全局平均池化。对于如此深的网络,总是存在梯度消失问题;为了防止网络中间部分“死亡”,论文中引入了两个辅助分类器。这是通过对两个中间 Inception 模块的输出应用 softmax 并计算辅助损失在真实值上完成的。总损失函数是辅助损失和真实损失的加权总和。我们鼓励您阅读原始论文以了解细节。
PyTorch- Inception 模块
让我们看看如何在 PyTorch 中实现 Inception 模块。在实践中,我们通常不会这样做,因为包含 Inception 模块的端到端深度网络架构已经在 torchvision 包中实现。然而,我们从头开始实现 Inception 模块以了解其细节。
注意:Inception 模块的全功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/mxn0 找到。
列表 11.6 用于简单 Inception 模块的 PyTorch 代码
class NaiveInceptionModule(nn.Module):
def __init__(self, in_channels, num_features=64):
super(NaiveInceptionModule, self).__init__()
self.branch1x1 = torch.nn.Sequential( ①
nn.Conv2d(
in_channels, num_features,
kernel_size=1, bias=False),
nn.BatchNorm2d(num_features, eps=0.001),
nn.ReLU(inplace=True))
self.branch3x3 = torch.nn.Sequential(
nn.Conv2d( ②
in_channels, num_features,
kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(num_features, eps=0.001),
nn.ReLU(inplace=True))
self.branch5x5 = torch.nn.Sequential( ③
nn.Conv2d(
in_channels, num_features,
kernel_size=5, padding=2, bias=False),
nn.BatchNorm2d(num_features, eps=0.001),
nn.ReLU(inplace=True))
self.pool = torch.nn.MaxPool2d( ④
kernel_size=3, stride=1, padding=1)
def forward(self, x):
conv1x1 = self.branch1x1(x)
conv3x3 = self.branch3x3(x)
conv5x5 = self.branch5x5(x)
pool_out = self.pool(x)
out = torch.cat( ⑤
[conv1x1, conv3x3, conv5x5, pool_out], 1)
return out
① 1 × 1 分支
② 3 × 3 分支
③ 5 × 5 分支
④ 3 × 3 池化
⑤ 连接并行分支的输出
列表 11.7 用于维度降低 Inception 模块的 PyTorch 代码
class Inceptionv1Module(nn.Module):
def __init__(self, in_channels, num_1x1=64,
reduce_3x3=96, num_3x3=128,
reduce_5x5=16, num_5x5=32,
pool_proj=32):
super(Inceptionv1Module, self).__init__()
self.branch1x1 = torch.nn.Sequential(
nn.Conv2d( ①
in_channels, num_1x1,
kernel_size=1, bias=False),
nn.BatchNorm2d(num_1x1, eps=0.001),
nn.ReLU(inplace=True))
self.branch3x3_1 = torch.nn.Sequential( ②
nn.Conv2d(
in_channels, reduce_3x3,
kernel_size=1, bias=False),
nn.BatchNorm2d(reduce_3x3, eps=0.001),
nn.ReLU(inplace=True))
self.branch3x3_2 = torch.nn.Sequential( ③
nn.Conv2d(
reduce_3x3, num_3x3,
kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(num_3x3, eps=0.001),
nn.ReLU(inplace=True))
self.branch5x5_1 = torch.nn.Sequential( ④
nn.Conv2d(
in_channels, reduce_5x5,
kernel_size=5, padding=2, bias=False),
nn.BatchNorm2d(reduce_5x5, eps=0.001),
nn.ReLU(inplace=True))
self.branch5x5_2 = torch.nn.Sequential( ⑤
nn.Conv2d(
reduce_5x5, num_5x5,
kernel_size=5, padding=2, bias=False),
nn.BatchNorm2d(num_5x5, eps=0.001),
nn.ReLU(inplace=True))
self.pool = torch.nn.Sequential( ⑥
torch.nn.MaxPool2d(
kernel_size=3, stride=1, padding=1),
nn.Conv2d(
in_channels, pool_proj,
kernel_size=1, bias=False),
nn.BatchNorm2d(pool_proj, eps=0.001),
nn.ReLU(inplace=True))
def forward(self, x):
conv1x1 = self.branch1x1(x)
conv3x3 = self.branch3x3_2(self.branch3x3_1((x)))
conv5x5 = self.branch5x5_2(self.branch5x5_1((x)))
pool_out = self.pool(x)
out = torch.cat( ⑦
[conv1x1, conv3x3, conv5x5, pool_out], 1)
return out
① 1 × 1 分支
② 3 × 3 分支中的 1 × 1 卷积
③ 3 × 3 分支中的 3 × 3 卷积
④ 5 × 5 分支中的 1 × 1 卷积
⑤ 5 × 5 分支中的 5 × 5 卷积
⑥ 先进行最大池化,然后是 1 × 1 卷积
⑦ 连接并行分支的输出
11.2.3 ResNet:为什么堆叠层以增加深度并不总是可扩展的
我们从一个基本问题开始:学习更好的网络是否像堆叠多个层一样简单?考虑图 11.9 中的图表。

图 11.9 展示了在 CIFAR-10 数据集上 20 层和 56 层网络的训练错误(左)和测试错误(右)。(来源:“用于图像识别的深度残差学习”;arxiv.org/pdf/1512.03385.pdf。)
这张图片来自 ResNet 论文“用于图像识别的深度残差学习”(arxiv.org/pdf/1512.03385.pdf),展示了两个网络在 CIFAR-10 数据集上的训练和测试错误率:一个有 20 层的较浅网络和一个有 56 层的较深网络。令人惊讶的是,较深(56 层)网络的训练和测试错误率更高。这个结果极其反直觉,因为我们预期更深层的网络应该具有更强的表达能力,从而比其较浅的对应网络有更高的准确率/更低的错误率。这种现象被称为退化问题:随着网络深度的增加,准确率变得饱和并迅速下降。我们可能会将其归因于过拟合,但事实并非如此,因为即使是较深网络的训练错误率也更高。另一个可能的原因是梯度消失/爆炸。然而,ResNet 论文的作者调查了每一层的梯度,并确定它们是健康的(没有消失/爆炸)。那么,是什么导致了退化问题,我们该如何解决它?
让我们考虑一个具有n层的较浅架构及其更深的对应架构,后者增加了更多层(n + m层)。较深的架构应该能够实现的损失不高于较浅架构。直观地说,一个简单的解决方案是学习较浅架构的精确n层和额外的m层的恒等函数。实际上这种情况并未发生表明神经网络层很难学习恒等函数。因此,论文提出了“快捷/跳过连接”,使得层能够轻松地学习恒等函数。这种“恒等快捷连接”是 ResNet 的核心思想。让我们看看一个数学类比。设h(x)是我们试图通过一层层(不一定是整个网络)来建模(学习)的函数。我们合理地预期函数g(x) = h(x) − x比h(x)简单,因此更容易学习。但我们已经在输入处有了x。所以如果我们学习g(x)并将其加到x上以获得h(x),我们就有效地通过学习更简单的g(x)函数来建模h(x)。名称residual来自g(x) = h(x) − x。图 11.10 详细展示了这一点。

图 11.10 列展示了具有跳过连接的残差块。
现在让我们回顾一下之前的问题,即退化问题。我们提出,普通的神经网络层通常难以学习恒等函数。在残差学习的情况下,为了学习恒等函数,h(x) = x,层需要学习 g(x)=0。这可以通过将所有层的权重驱动到 0 来轻松实现。这里还有另一种思考方式:如果我们初始化常规神经网络权重和偏差为 0,那么每个层都从“零”函数开始:g(x) = 0。因此,每个带有跳跃连接的层堆栈的输出,h(x) = g(x) + x,已经是恒等函数:当 g(x) = 0 时,h(x) = x。
在实际情况下,需要注意的是,恒等映射不太可能是最优的:网络层将想要学习实际特征。在这种情况下,这种重新表述并没有阻止网络层学习其他函数,就像常规层堆栈一样。我们可以将这种重新表述视为预条件,如果需要,它使得学习恒等函数更容易。此外,通过添加跳跃连接,我们允许梯度直接从层流向层:深层层有直接路径到 x。这允许更好的学习,因为来自底层的信息可以直接传递到高层。
ResNet 架构
现在我们已经看到了基本构建块——一个带有跳跃连接的卷积层堆栈,让我们更深入地了解 ResNet 的架构。ResNet 架构是通过将多个构建块堆叠在一起来构建的。它们遵循与 VGG 相同的理念:
-
卷积层主要使用 3 × 3 的过滤器。
-
对于给定的输出特征图大小,层具有相同数量的过滤器。
-
如果特征图大小减半,则过滤器数量加倍以保持每层的时序复杂度。
与 VGG 使用多个最大池化层不同,ResNet 使用步长为 2 的卷积层进行下采样。其核心架构由以下组件组成:
-
五个卷积层块——第一个卷积层块由一个 7 × 7 的核组成,
stride=2,padding=3,num_features=64,随后是一个 3 × 3 核的最大池化层,stride=2,padding=1。特征图大小从(224, 224)减少到(56, 56)。其余的卷积层(ResidualConvBlock)是通过堆叠多个基本快捷块来构建的。每个基本块使用 3 × 3 的过滤器,如上所述。 -
分类器——在卷积块输出之上运行的平均池化块,随后是一个 FC 层,用于分类。
鼓励您查看原始论文中的图表以了解细节。现在,让我们看看如何在 PyTorch 中实现 ResNet。
PyTorch-ResNet
在本节中,我们讨论如何从头开始实现 ResNet-34。请注意,在实践中很少这样做。torchvision 包为所有 ResNet 架构提供了现成的实现。然而,通过从头开始构建网络,我们可以更深入地理解架构。首先,让我们实现一个基本的跳转连接块(BasicBlock)来了解快捷连接是如何工作的。
注意:ResNet 的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz/5K9q 找到。
列表 11.8 BasicBlock 的 PyTorch 代码
class BasicBlock(nn.Module):
def __init__(self, in_channels, num_features, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = nn.Sequential( ①
nn.Conv2d(
in_channels, num_features,
kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(num_features, eps=0.001),
nn.ReLU(inplace=True))
self.conv2 = nn.Sequential(
nn.Conv2d(
num_features, num_features,
kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(num_features, eps=0.001))
self.downsample = downsample ②
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
conv_out = self.conv2(self.conv1(x))
identity = x
if self.downsample is not None:
identity = self.downsample(x)
assert identity.shape == conv_out.shape,
f"Identity {identity.shape} and conv out {conv_out.shape} have different shapes"
out = self.relu(conv_out + identity) ③
return out
① 实例化两个 3 × 3 大小的卷积层
② 当输入和输出特征图大小不同时,使用 1 × 1 卷积层对输入特征图进行下采样。
③ 创建跳转连接
注意残差块输出是卷积层输入和输出的函数:ReLU(conv_out+x)。这假设 x 和 conv_out 具有相同的形状。(稍后我们将讨论当这种情况不成立时应该怎么做。)此外,添加跳转连接不会增加参数数量。快捷连接是无参数的。这使得从计算角度来看,解决方案成本低,这是快捷连接的一个魅力。
接下来,让我们实现一个由多个基本块堆叠而成的残差卷积块。在处理基本块时,我们必须处理两种情况:
-
情况 1—输出特征图空间分辨率 = 输入特征图空间分辨率 且 输出特征数量 = 输入特征数量。这是最常见的情况。由于特征数量或特征图的空间分辨率没有变化,我们可以通过快捷连接轻松地将输入和输出相加。
-
情况 2—输出特征图空间分辨率 = 输入特征图空间分辨率的一半 且 输出特征数量 = 输入特征数量的两倍。记住 ResNet 使用步长为 2 的卷积层进行下采样。特征数量也加倍。这是通过每个卷积块(除了第二个卷积块)的第一个基本块来实现的。在这种情况下,输入和输出大小不同。那么我们如何将它们作为跳转连接的一部分相加呢?1 × 1 卷积是答案。通过使用
stride=2和num_features=2 * num_input_features的 1 × 1 卷积,输入特征图的空间分辨率减半,输入特征数量通过加倍。
列表 11.9 ResidualConvBlock 的 PyTorch 代码
class ResidualConvBlock(nn.Module):
def __init__(self, in_channels, num_blocks, reduce_fm_size=True):
super(ResidualConvBlock, self).__init__()
num_features = in_channels * 2 if reduce_fm_size else in_channels
modules = []
for i in range(num_blocks): ①
if i == 0 and reduce_fm_size:
stride = 2
downsample = nn.Sequential(
nn.Conv2d( ②
in_channels, num_features,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(num_features, eps=0.001),
)
basic_block = BasicBlock(
in_channels=in_channels, num_features=num_features,
stride=stride, downsample=downsample)
else:
basic_block = BasicBlock(
in_channels=num_features, num_features=num_features, stride=1)
modules.append(basic_block)
self.conv_block = nn.Sequential(*modules)
def forward(self, x):
return self.conv_block(x)
① 残差块是基本块的堆叠
② 1 × 1 卷积用于下采样输入特征图
这样,我们就准备好实现 ResNet-34。
列表 11.10 ResNet-34 的 PyTorch 代码
class ResNet34(nn.Module):
def __init__(self, num_basic_blocks, num_classes):
super(ResNet, self).__init__()
conv1 = nn.Sequential( ①
nn.Conv2d(3, 64, kernel_size=7,
stride=2, padding=3, bias=False),
nn.BatchNorm2d(64, eps=0.001),
nn.ReLU(inplace=True),
nn.MaxPool2d(
kernel_size=3, stride=2, padding=1)
)
assert len(num_basic_blocks) == 4 ②
conv2 = ResidualConvBlock( ③
in_channels=64, num_blocks=num_basic_blocks[0], reduce_fm_size=False)
conv3 = ResidualConvBlock(
in_channels=64, num_blocks=num_basic_blocks[1], reduce_fm_size=True)
conv4 = ResidualConvBlock(
in_channels=128, num_blocks=num_basic_blocks[2], reduce_fm_size=True)
conv5 = ResidualConvBlock(
in_channels=256, num_blocks=num_basic_blocks[3], reduce_fm_size=True)
self.conv_backbone = nn.Sequential(*[conv1, conv2, conv3, conv4, conv5])
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Linear(512, num_classes)
def forward(self, x):
conv_out = self.conv_backbone(x)
conv_out = self.avg_pool(conv_out)
logits = self.classifier( ④
conv_out.view(conv_out.shape[0], -1))
return logits
① 实例化第一个卷积层
② 大小为 4 的列表,指定每个 ResidualConvBlock 的基本块数量
③ 实例化四个残差块
④ 在传递给分类器之前对卷积特征进行展平
如前所述,我们通常不会实现自己的 ResNet。相反,我们使用来自 torchvision 包的现成实现,如下所示:
import torchvision
resnet34 = torchvision.models.resnet34() ①
① 从 torchvision 包中实例化 resnet34
当我们查看 ResNet-34 时,还有更深层次的 ResNet 架构,如 ResNet-50、ResNet-101 和 ResNet-151,它们使用一个名为 BottleneckLayer 的不同版本的 BasicBlock。同样,还有许多受 ResNet 启发的其他变体,如 ResNext、Wide ResNet 等。在这本书中,我们不讨论这些个别变体,因为它们背后的核心思想是相同的。我们鼓励您阅读原始论文,以更深入地了解该主题。
11.2.4 PyTorch Lightning
让我们回顾一下之前讨论的数字分类问题。我们主要讨论了 LeNet 架构,并在 PyTorch 中实现了它。现在,让我们实现训练 LeNet 模型的端到端代码。我们不是使用普通的 PyTorch 来做这件事,而是使用 Lightning 框架,因为它显著简化了模型开发和训练过程。
虽然 PyTorch 有我们训练模型所需的一切,但深度学习远不止于附加层。当涉及到实际训练时,我们需要编写大量的样板代码,正如我们在之前的例子中所看到的。这包括将数据从 CPU 转移到 GPU、实现训练驱动程序等。此外,如果我们需要在多个设备/机器上扩展训练/推理,通常还需要完成另一套集成。
PyTorch Lightning 是一个提供构建模型、数据集等所需 API 的解决方案。它提供了干净的接口和要实现的钩子。底层 Lightning 框架在训练过程中的适当点调用这些钩子。其理念是 Lightning 将研究逻辑留给我们,而自动处理其余的样板代码。此外,Lightning 还带来了多 GPU 训练、16 位浮点数和无需代码更改即可在 TPU 上训练等特性。有关 PyTorch Lightning 的更多详细信息,请参阅 www.pytorchlightning.ai/tutorials。
使用 PyTorch Lightning 训练模型涉及三个主要组件:DataModule、LightningModule 和 Trainer。让我们看看每个组件的作用。
数据模块
DataModule 是一个可共享、可重用的类,它封装了处理数据所需的所有步骤。所有数据模块都必须继承自 LightningDataModule,它提供了可覆盖的方法。在这种情况下,我们将实现 MNIST 作为数据模块。现在,这个数据模块可以在跨越各种模型和架构的多个实验中使用。
列表 11.11 PyTorch 中 MNIST 数据模块的代码
class MNISTDataModule(LightningDataModule):
DATASET_DIR = "datasets"
def __init__(self, transform=None, batch_size=100):
super(MNISTDataModule, self).__init__()
if transform is None:
transform = transforms.Compose(
[transforms.Resize((32, 32)),
transforms.ToTensor()])
self.transform = transform
self.batch_size = batch_size
def prepare_data(self): ①
datasets.MNIST(root = MNISTDataModule.DATASET_DIR,
train=True, download=True)
datasets.MNIST(root=MNISTDataModule.DATASET_DIR,
train=False, download=True)
def setup(self, stage=None):
train_dataset = datasets.MNIST(
root = MNISTDataModule.DATASET_DIR, train=True,
download=False, transform=self.transform)
self.train_dataset, self.val_dataset = random_split( ②
train_dataset, [55000, 5000])
self.test_dataset = datasets.MNIST(
root = MNISTDataModule.DATASET_DIR, train = False,
download = False, transform=self.transform)
def train_dataloader(self): ③
return DataLoader(
self.train_dataset, batch_size=self.batch_size,
shuffle=True, num_workers=0)
def val_dataloader(self): ④
return DataLoader(
self.val_dataset, batch_size=self.batch_size,
shuffle=False, num_workers=0)
def test_dataloader(self): ⑤
return DataLoader(
self.test_dataset, batch_size=self.batch_size,
shuffle=False, num_workers=0)
@property
def num_classes(self): ⑥
return 10
① 下载、标记并准备原始数据
② 将训练数据集分为训练集和验证集
③ 创建训练数据加载器,它提供了一个干净的接口来迭代数据集。它处理批处理、洗牌以及通过多进程获取数据,所有这些都在幕后完成。
④ 创建验证数据加载器
⑤ 创建测试数据加载器
⑥ 数据集中对象类别的数量
LightningModule
LightningModule本质上将所有研究代码组合成一个单一模块,使其自包含。注意DataModule和LightningModule之间的清晰分离——这使得在不同的数据集上训练/评估相同的模型变得容易。同样,不同的模型也可以轻松地在相同的数据集上训练/评估。
Lightning 模块由以下内容组成:
-
在
init方法中定义的模型或模型系统 -
在
training_step中定义的训练循环 -
在
validation_step中定义的验证循环 -
在
testing_step中定义的测试循环 -
在
configure_optimizers中定义的优化器和调度器
让我们看看如何将 LeNet 分类器定义为 Lightning 模块。
列出 11.12 作为 Lightning 模块的 LeNet 的 PyTorch 代码
class LeNetClassifier(LightningModule):
def __init__(self, num_classes): ①
super(LeNetClassifier, self).__init__()
self.save_hyperparameters()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=1, out_channels=6,
kernel_size=5, stride=1),
torch.nn.Tanh(),
torch.nn.AvgPool2d(kernel_size=2))
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=6, out_channels=16,
kernel_size=5, stride=1),
torch.nn.Tanh(),
torch.nn.AvgPool2d(kernel_size=2))
self.conv3 = torch.nn.Sequential(
torch.nn.Conv2d(
in_channels=16, out_channels=120,
kernel_size=5, stride=1),
torch.nn.Tanh())
self.fc1 = torch.nn.Sequential(
torch.nn.Linear(in_features=120, out_features=84),
torch.nn.Tanh())
self.fc2 = torch.nn.Linear(in_features=84,
out_features=num_classes)
self.criterion = torch.nn.CrossEntropyLoss() ②
self.accuracy = torchmetrics.Accuracy()
def forward(self, X): ③
conv_out = self.conv3(
self.conv2(self.conv1(X)))
batch_size = conv_out.shape[0]
conv_out = conv_out.reshape(
batch_size, -1)
logits = self.fc2(self.fc1(conv_out))
return logits ④
def predict(self, X): ⑤
logits = self.forward(X)
probs = torch.softmax(logits, dim=1)
return torch.argmax(probs, 1)
def core_step(self, batch): ⑥
X, y_true = batch
y_pred_logits = self.forward(X)
loss = self.criterion(y_pred_logits, y_true)
accuracy = self.accuracy(y_pred_logits, y_true)
return loss, accuracy
def training_step(self, batch, batch_idx): ⑦
loss, accuracy = self.core_step(batch)
if self.global_step \% 100 == 0:
self.log("train_loss", loss, on_step=True, on_epoch=True)
self.log("train_accuracy", accuracy, on_step=True, on_epoch=True)
return loss
def validation_step(self, batch,
batch_idx, dataset_idx=None): ⑧
return self.core_step(batch)
def validation_epoch_end(self, outputs): ⑨
avg_loss = torch.tensor([x[0] for x in outputs]).mean()
avg_accuracy = torch.tensor([x[1] for x in outputs]).mean()
self.log("val_loss", avg_loss)
self.log("val_accuracy", avg_accuracy)
print(f"Epoch {self.current_epoch},
Val loss: {avg_loss:0.2f}, Accuracy: {avg_accuracy:0.2f}")
return avg_loss
def configure_optimizers(self): ⑩
return torch.optim.SGD(model.parameters(), lr=0.01,
momentum=0.9)
def checkpoint_callback(self): ⑪
return ModelCheckpoint(monitor="val_accuracy", mode="max", save_top_k=1)
① 在init方法中,我们通常定义模型、准则以及训练模型所需的任何其他设置步骤。
② 实例化交叉熵损失
③ 实现模型的正向传递。在这种情况下,输入是一批图像,输出是 logits。X.shape: [batch_size, C, H, W]。
④ Logits.shape: [batch_size, num_classes]
⑤ 运行前向传递,执行 softmax 将结果 logits 转换为概率,并返回概率最高的类别
⑥ 抽象出训练和测试循环之间的共同功能,包括运行前向传递、计算损失和准确率
⑦ 实现基本训练步骤:运行前向传递、计算损失和准确率。记录任何必要的值并返回总损失。
⑧ 在每个 epoch 的测试步骤结束时调用。每个测试步骤的输出都可通过 outputs 获取。在这里,我们通过平均所有测试批次来计算平均测试损失和准确率。
⑨ 实现基本验证步骤:运行前向传递、计算损失和准确率,并返回它们。
⑩ 配置 SGD 优化器
⑪ 实现了保存模型的逻辑。我们使用最佳验证准确率保存模型。
模型与数据独立。这使我们能够在不进行任何代码更改的情况下,在其它数据模块上运行LeNetClassifier模型。注意,我们并没有执行以下步骤:
-
将数据移动到设备上
-
调用
loss.backward -
调用
optimizer.backward -
设置
model.train()或eval() -
重置梯度
-
实现训练循环
所有这些都由 PyTorch Lightning 处理,从而消除了大量的样板代码。
Trainer
我们准备好训练我们的模型,可以使用Trainer类来完成。这种抽象实现了以下功能:
-
我们通过 PyTorch 代码维护对所有方面的控制,而不需要额外的抽象。
-
训练器使用了来自顶级人工智能实验室的贡献者和用户的最佳实践。
-
训练器允许我们覆盖任何我们不希望自动化的关键部分。
列表 11.13 用于 Trainer 的 PyTorch 代码
dm = MNISTDataModule() ①
model = LeNetClassifier(num_classes=dm.num_classes) ②
exp_dir = "/tmp/mnist"
trainer = Trainer( ③
default_root_dir=exp_dir,
callbacks=[model.checkpoint_callback()],
gpus=torch.cuda.device_count(), # Number of GPUs to run on
max_epochs=10,
num_sanity_val_steps=0
)
trainer.fit(model, dm) ④
① 实例化数据集
② 实例化模型
③ 实例化训练器
④ 训练模型
注意,我们不需要编写训练器循环:我们只需调用 trainer.fit 来训练模型。此外,日志自动使我们能够通过 TensorBoard 查看损失和准确度曲线。
列表 11.14 用于模型推理的 PyTorch 代码
X, y_true = (iter(dm.test_dataloader())).next()
with torch.no_grad():
y_pred = model.predict(X) ①
① 运行 model.predict()
要使用训练好的模型进行推理,我们在输入上运行 model.predict。
11.3 物体检测:简史
到目前为止,我们讨论了分类问题,其中我们将图像分类为 N 个对象类别之一。但在许多情况下,这不足以真正描述图像。考虑图 11.11——一个非常逼真的图像,四只动物叠在一起,对着相机摆姿势。知道每只动物的对象类别及其在图像中的位置(边界框坐标)将是有用的。这被称为物体检测/定位问题。那么,我们如何在图像中定位物体呢?
假设我们能够从图像中提取区域,使得每个区域只包含一个对象。然后我们可以运行一个图像分类深度神经网络(我们之前已经讨论过)来分类每个区域,并选择置信度最高的区域。这是第一个基于深度学习的物体检测器之一,基于区域的卷积神经网络(R-CNN;arxiv.org/pdf/1311.2524.pdf)采用的方法。让我们更详细地看看这个方法。
11.3.1 R-CNN
R-CNN 的物体检测方法包括三个主要阶段:
-
选择性搜索以识别感兴趣的区域——这一步使用一种基于计算机视觉的算法,能够提取候选区域。我们不深入选择性搜索的细节;我们鼓励您阅读原始论文以了解细节。选择性搜索为每张图像生成大约 2,000 个区域建议。
-
特征提取——深度卷积神经网络从每个感兴趣区域中提取特征。由于深度神经网络通常需要固定大小的输入,因此区域(可能具有任意大小)在输入深度神经网络之前被扭曲成固定大小。
-
分类/定位——在提取的特征上训练一个特定类别的支持向量机(SVM)来分类区域。此外,添加边界框回归器以微调对象在区域内的位置。在训练过程中,每个区域根据其与 GT 框的重叠情况分配一个基于真实情况的(GT)类别。如果重叠度高,则分配正标签,否则分配负标签。

图 11.11 一个包含不同形状和大小的多个物体的图像
11.3.2 Fast R-CNN
基于 R-CNN 的方法最大的缺点之一是我们必须独立地为每个区域提议提取特征。因此,如果我们为单个图像生成 2,000 个提议,我们就必须运行 2,000 次前向传递来提取区域特征。这是非常昂贵且极其缓慢的(在训练和推理过程中)。此外,训练是一个多阶段管道——选择性搜索、深度网络、特征上的 SVMs 以及边界框回归器——这使得训练和推理都变得繁琐。为了解决这些问题,R-CNN 的作者引入了一种称为 Fast R-CNN 的新技术arxiv.org/pdf/1504.08083.pdf)。它显著提高了速度:在训练时比 R-CNN 快 9 倍,在测试时快 213 倍。此外,它还提高了目标检测的质量。
Fast R-CNN 做出了两个主要贡献:
-
兴趣区域(RoI)池化——如前所述,R-CNN 的一个基本问题是需要多次前向传递来提取单个图像区域提议的特征。那么,我们能否一次性提取特征呢?这个问题通过 RoI 池化得到解决。Fast R-CNN 使用整个图像作为 CNN 的输入,而不是单个区域提议。然后,在 CNN 输出上使用 RoIs(区域提议边界框)来一次性提取区域特征。我们将在 Faster R-CNN 讨论中详细介绍 RoI 池化的细节。
-
多任务损失——Fast R-CNN 消除了使用 SVMs 的需求。相反,深度神经网络同时进行分类和边界框回归。与仅使用深度网络进行特征提取的 R-CNN 不同,Fast R-CNN 更加端到端。它是一个用于区域提议特征提取、分类和回归的单个架构。
高级算法如下:
-
使用选择性搜索为每张图像生成 2,000 个区域提议/RoIs。
-
在 Fast R-CNN 的单次传递中,使用 RoI 池化一次性提取所有 RoI 特征,然后使用分类和回归头进行对象分类和定位。
由于所有区域提议的特征提取都在一次传递中完成,这种方法比 R-CNN 快得多,在 R-CNN 中每个提议都需要单独的前向传递。此外,由于神经网络是端到端训练的——即被要求进行分类和回归——目标检测的准确性也得到了提高。
11.3.3 Faster R-CNN
当我们可以更快时,为什么还要满足于快速?Fast R-CNN 比 R-CNN 快得多。然而,它仍然需要运行选择性搜索来获取区域提议。选择性搜索算法只能在 CPU 上运行。此外,该算法速度慢且耗时。因此,它成为了一个瓶颈。有没有办法摆脱选择性搜索?
考虑到的明显想法是使用深度网络来生成区域提议。这是 Faster R-CNN(FRCNN;arxiv.org/pdf/1506.01497.pdf)的核心思想:它消除了选择性搜索的需求,并允许深度网络学习区域提议。它是最早的近实时对象检测器之一。由于我们使用深度网络来学习区域提议,区域提议也因此变得更好。因此,整体架构的结果精度也大大提高。
我们可以将 FRCNN 视为由两个核心模块组成:
-
区域提议网络(RPN)——这是负责生成区域提议的模块。RPN 被设计为高效地预测具有广泛尺度和宽高比的区域提议。
-
R-CNN 模块——这与 Fast R-CNN 相同。它接收一系列区域提议,并执行 RoI 池化,随后进行分类和回归。
另一个需要注意的重要事项是,区域提议网络(RPN)和 R-CNN 模块共享相同的卷积层:权重是共享的,而不是学习两个独立的网络。在下一节中,我们将详细讨论 Faster R-CNN。
11.4 Faster R-CNN:深入探讨
图 11.12 显示了 FRCNN 的高级架构。卷积层(我们也称其为卷积主干)从输入图像中提取特征图。RPN 在这些特征图上操作,并发出候选区域提议。RoI 池化层为每个感兴趣区域生成一个固定大小的特征向量,并将其传递给一组全连接层,这些全连接层发出关于K个对象类别的 softmax 概率估计(加上一个“背景”类的通配符)以及代表每个K个类别的边界框坐标的四个数字。让我们更详细地看看每个组件。

图 11.12 架构。(来源:“Faster R-CNN:迈向实时提议网络”;arxiv.org/abs/1506.01497)
11.4.1 卷积主干
在原始实现中,FRCNN 使用了 VGG-16 的卷积层作为 RPN 和 R-CNN 模块的卷积骨干。有一个小的修改:在第五个卷积层(conv5)之后的最后一个池化层被移除。正如我们之前讨论 VGG 架构时提到的,VGG 通过最大池化在每个卷积块中将特征图的空间尺寸减少 2。由于移除了最后一个池化层,空间尺寸减少了 2⁴ = 16 的因子。因此,一个 224 × 224 的图像在输出时被缩减到 14 × 14 的特征图。同样,一个 800 × 800 的图像将被缩减到 50 × 50 的特征图。
11.4.2 区域提议网络
RPN 以任意大小的图像作为输入,并输出一组可能包含对象的矩形提议。RPN 在最后共享卷积层输出的卷积特征图上操作。使用 VGG 骨干网络时,输入图像的大小(h, w)缩小到(h/16, w/16)。因此,输入图像中的每个 16 × 16 空间区域被缩减为卷积特征图上的一个单独的点。因此,输出卷积特征图中的每个点代表输入图像中的一个 16 × 16 补丁。RPN 就在这个特征图上操作。另一个需要注意的微妙之处是,虽然卷积特征图中的每个点都选择对应一个 16 × 16 补丁,但它有一个显著更大的感受野(输入特征图中受特定输出特征影响的区域)。因此,特征图上每个点的嵌入实际上是大感受野的摘要。
锚点
物体检测问题的关键方面是物体的大小和形状的多样性。物体可以从非常小(猫)到非常大的大象)。此外,物体可以有不同的长宽比。一些物体可能很宽,一些可能很高,等等。一个简单的解决方案是拥有一个单一的神经网络检测头,能够识别和识别各种大小和形状的物体。正如你可以想象的那样,这将使神经网络检测器的任务变得极其复杂。一个更简单的解决方案是拥有多种神经网络检测头,每个头负责解决一个更简单的问题。例如,一个头只关注大而高的物体,并且只有在图像中存在这样的物体时才会触发。其他头将关注其他大小和长宽比。我们可以将每个头视为负责完成一个单一简单的工作。这种设置极大地帮助并促进了学习。
这就是引入锚点背后的直觉。锚点类似于形状和大小各异的参考框。所有提议都是相对于锚点提出的。每个锚点都由其大小和宽高比唯一表征,并负责检测图像中形状相似的对象。在每次滑动窗口位置,我们都有多个跨越不同大小和宽高比的锚点。原始 FRCNN 架构支持九种锚点配置,涵盖三种大小和三种宽高比,从而支持广泛的形状。这些对应于尺度(8, 16, 32)和宽高比(0.5, 1.0 和 2.0)的锚点框(参见图 11.13)。锚点现在在目标检测器中无处不在。

图 11.13 左列显示了输出卷积特征原始实现中不同大小和宽高比的各种网格点位置。右列显示了特定网格点上的各种锚点。
注意:用于生成锚点的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列表 11.15 在特定网格点生成锚点的 PyTorch 代码
def generate_anchors_at_grid_point(
ctr_x, ctr_y, subsample, scales, aspect_ratios):
anchors = torch.zeros(
(len(aspect_ratios) * len(scales), 4), dtype=torch.float)
for i, scale in enumerate(scales):
for j, aspect_ratio in enumerate(aspect_ratios): ①
w = subsample * scale * torch.sqrt(aspect_ratio)
h = subsample * scale * torch.sqrt(1 / aspect_ratio)
xtl = ctr_x - w / 2 ②
ytl = ctr_y - h / 2
xbr = ctr_x + w / 2
ybr = ctr_y + h / 2
index = i * len(aspect_ratios) + j
anchors[index] = torch.tensor([xtl, ytl, xbr, ybr])
return anchors
① 为不同的尺度和宽高比生成高度和宽度
② 在(ctr_x, ctr_y)周围生成一个宽度为 w,高度为 h 的边界框
列表 11.16 生成给定图像所有锚点的 PyTorch 代码
def generate_all_anchors( ①
input_img_size, subsample, scales, aspect_ratios):
_, h, w = input_img_size
conv_feature_map_size = (h//subsample, w//subsample)
all_anchors = [] ②
ctr_x = torch.arange(
subsample/2, conv_feature_map_size[1]*subsample+1, subsample)
ctr_y = torch.arange(
subsample/2, conv_feature_map_size[0]*subsample+1, subsample)
for y in ctr_y:
for x in ctr_x:
all_anchors.append(
generate_anchors_at_grid_point( ③
x, y, subsample, scales, aspect_ratios))
all_anchors = torch.cat(all_anchors)
return all_anchors
input_img_size = (3, 800, 800) ④
c, height, width = input_img_size
scales = torch.tensor([8, 16, 32], dtype=torch.float)
aspect_ratios = torch.tensor([0.5, 1, 2])
subsample = 16
anchors = generate_all_anchors(input_img_size, subsample, scales, aspect_ratios)
① 这不是生成锚点最有效的方法。我们编写了简单的代码以简化理解。
② 在卷积特征图中的每个点上生成锚点框,这对应于输入中的 16 × 16(子采样,子采样)区域
③ 使用列表 11.15 中定义的函数
④ 定义配置参数并生成锚点
RPN 在输出卷积特征图上滑动一个小型网络。这个小型网络在卷积特征图的 n × n 空间窗口上操作。在每个滑动窗口的位置,它生成一个低维特征向量(VGG 为 512 维),并将其输入到盒子回归层(reg)和盒子分类层(cls)。对于每个以滑动窗口位置为中心的锚框,分类器预测 objectness:一个从 0 到 1 的值,其中 1 表示存在物体,回归器预测相对于锚框的区域提议。这种架构自然地通过一个 n × n 卷积层实现,后面跟着两个兄弟 1 × 1 卷积层(用于 reg 和 cls)。FRCNN 论文中原始实现使用 n = 3,当使用 VGG 背骨时,这导致有效感受野为 228 像素。图 11.14 详细说明了这一点。请注意,这个网络只由卷积层组成。这样的架构被称为 全卷积网络 FCN。FCN 没有输入尺寸限制。因为它们只由卷积层组成,它们可以与任意大小的输入一起工作。在 FCN 中,n × n 和 1 × 1 层的组合相当于在每个卷积特征图的每个点应用 FC 层。此外,因为我们是在特征图上卷积一个卷积网络以生成回归和分类分数,所以卷积权重在特征图的不同位置上是公共/共享的。这使得该方法具有平移不变性。如果图像顶部和底部的猫大小相似,它们将被相同的锚配置(尺度、纵横比)捕获。

图 11.14 RPN 架构。使用 3 × 3 卷积从每个滑动窗口生成一个 512 维特征向量。一个 1 × 1 卷积层(分类器)从 512 维特征中提取,同样,另一个 1 × 1 卷积层(回归器)从 512 维特征向量生成 4k 箱体坐标。
仅由卷积层组成,它们可以与任意大小的输入一起工作。在 FCN 中,n × n 和 1 × 1 层的组合相当于在每个卷积特征图的每个点应用 FC 层。此外,因为我们是在特征图上卷积一个卷积网络以生成回归和分类分数,所以卷积权重在特征图的不同位置上是公共/共享的。这使得该方法具有平移不变性。如果图像顶部和底部的猫大小相似,它们将被相同的锚配置(尺度、纵横比)捕获。
注意:RPN 的全卷积网络完整功能的代码,可通过 Jupyter Notebook 执行,可在 mng.bz/nY48 找到。
列表 11.17 PyTorch 代码为 RPN 的 FCN
class RPN_FCN(nn.Module):
def __init__(self, k, in_channels=512): ①
super(RPN_FCN, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(
in_channels, 512, kernel_size=3,
stride=1, padding=1),
nn.ReLU(True))
self.cls = nn.Conv2d(512, 2*k, kernel_size=1)
self.reg = nn.Conv2d(512, 4*k, kernel_size=1)
def forward(self, x):
out = self.conv(x) ②
rpn_cls_scores = self.cls(out).view( ③
x.shape[0], -1, 2)
rpn_loc = self.reg(out).view( ④
x.shape[0], -1, 4)
⑤
return rpn_cls_scores ,rpn_loc ⑥
① 实例化在输出卷积特征图上卷积的小型网络。它由一个 3 × 3 卷积层组成,后面跟着一个用于分类的 1 × 1 卷积层和一个用于回归的另一个 1 × 1 卷积层。
② 背骨输出:大小为 (batch_size, in_channels, h, w) 的卷积特征图
③ 将 (batch_size, h, w, 2k) 转换为 batch_size, hwk, 2)
④ 将 (batch_size, h, w, 4k) 转换为 batch_size, hwk, 4)
⑤ 表示每个锚框分类分数的 (batch_size, num_anchors, 2) 张量
⑥ (batch_size, num_anchors, 4)表示相对于锚框的框坐标的张量
为 RPN 生成 GT
到目前为止,我们已经生成了许多锚框边界框和一个能够为每个锚框生成分类和回归偏移量的神经网络。在训练 RPN 时,我们需要为每个锚框提供一个目标 GT(分类器和回归器都应该预测的目标),为此,我们需要查看图像中的对象并将它们分配给包含对象的相应锚点。思路如下:在数千个锚点中,包含大部分对象的锚点应该尝试预测和定位对象。 我们之前看到,创建锚点的直觉是为了确保每个锚点负责一种特定的对象形状、宽高比。因此,只有包含对象的锚点负责对其进行分类是有意义的。
要测量对象是否位于锚框内,我们依赖于交集与并集(IoU)分数。两个边界框之间的 IoU 定义为(重叠面积)/(并集面积)。因此,如果两个边界框非常相似,它们的重叠区域很大,它们的并集接近重叠区域,从而产生一个高的 IoU。如果两个边界框差异很大,那么它们的重叠区域最小,导致 IoU 很低(见图 11.15)。

图 11.15 部分,两个区域被它们的并集分割。
FRCNN 为分配锚框标签提供了一些指导方针:
-
我们将正标签 1(表示锚框中存在对象)分配给两种类型的锚点:
-
与 GT 框 IoU 重叠最大的锚点
-
与 GT 框 IoU 重叠大于 0.7 的锚点
-
-
我们将一个非正锚点分配一个负标签 0(这表示锚框中没有对象存在,意味着它只包含背景),如果其 IoU 比率对所有 GT 框都小于 0.3。
-
既不是正标签也不是负标签的锚点不会对训练目标做出贡献。
注意,一个单独的 GT 对象可能将正标签分配给多个锚点。这些输出必须在稍后抑制,以防止重复检测(我们将在后续章节中讨论这一点)。此外,任何部分位于图像之外的锚框都被忽略。
注意:分配锚框 GT 标签的完整功能代码,可通过 Jupyter Notebook 执行,可以在mng.bz/nY48找到。
列出 11.18 PyTorch 代码为每个锚框分配 GT 标签
valid_indices = torch.where(
(anchors[:, 0] >=0) &
(anchors[:, 1] >=0) &
(anchors[:, 2] <=width) & ①
(anchors[:, 3] <=height))[0]
rpn_valid_labels = -1 * torch.ones_like( ②
valid_indices, dtype=torch.int)
valid_anchor_bboxes = anchors[valid_indices] ③
ious = torchvision.ops.box_iou( ④
gt_bboxes, valid_anchor_bboxes)
assert ious.shape == torch.Size(
[gt_bboxes.shape[0], valid_anchor_bboxes.shape[0]])
gt_ious_max = torch.max(ious, dim=1)[0] ⑤
# Find all the indices where the IOU = highest GT IOU
gt_ious_argmax = torch.where( ⑥
gt_ious_max.unsqueeze(1).repeat(1, gt_ious_max.shape[1]) == ious)[1]
anchor_ious_argmax = torch.argmax(ious, dim=0) ⑦
anchor_ious = ious[anchor_ious_argmax, torch.arange(len(anchor_ious_argmax))]
pos_iou_threshold = 0.7
neg_iou_threshold = 0.3
rpn_valid_labels[anchor_ious < neg_iou_threshold] = 0 ⑧
rpn_valid_labels[anchor_ious > pos_iou_threshold] = 1 ⑨
rpn_valid_labels[gt_ious_argmax] = 1 ⑩
① 找到完全位于图像内的有效锚点
② 为每个有效锚点分配 s 标签为-1(不属于任何类别)
③ 获取有效锚框
④ 形状为(num_gt_bboxes, num_valid_anchor_bboxes)的张量,表示 GT 和锚点之间的 IoU
⑤ 找到每个 GT 边界框的最高 IoU
⑥ 找到所有 IOU = 最高 GT IOU 的索引
⑦ 为每个锚框找到最高的 IoU
⑧ 当 IoU < 0.3 时,为负锚点分配 0(背景)
⑨ 当 IoU > 0.7 时,为正锚点分配 1(对象性)
对于每个 GT 框,将具有最高 IoU 的锚点分配为正锚点
处理不平衡
根据我们为锚点分配标签的策略,请注意,负锚点的数量显著多于正锚点的数量。例如,对于示例图像,我们只获得了 24 个正锚点,而负锚点有 7,439 个。如果我们直接在这样一个不平衡的数据集上训练,神经网络通常可以通过将每个锚点分类为负锚点来学习一个局部最小值。在我们的示例中,如果我们预测每个锚点都是负锚点,我们的准确率将是 7439/(7439+22): 99.7%。然而,结果神经网络实际上是无用的,因为它没有学习到任何东西。换句话说,不平衡会导致对主导类的偏差。为了处理这种不平衡,通常有三种策略:
-
欠采样—减少主导类的样本数量。
-
过采样—增加较少主导类的样本数量。
-
加权损失—将误分类较少主导类的成本设置得远高于主导类。
FRCNN 利用欠采样的想法。对于单张图像,存在多个正负锚点。从这些数千个锚点中,我们在图像中随机采样 256 个锚点来计算损失函数,其中采样的正负锚点比例高达 1:1。如果图像中的正样本少于 128 个,我们将使用负样本填充 mini-batch。
为锚框分配目标
我们已经看到了如何采样和分配标签给锚点。接下来要问的问题是,如何确定回归目标:
-
情况 1: 标签 = −1—未采样/无效锚点。这些不会对训练目标做出贡献,因此回归目标无关紧要。
-
情况 2: 标签 = 0—背景锚点。这些锚点不包含任何对象,因此它们也不应该对回归做出贡献。
-
情况 3: 标签 = 1—正锚点。这些锚点包含对象。我们需要为这些锚点生成回归目标。
让我们只考虑正锚点的情况。这里的关键直觉是锚点已经包含大多数对象。否则,它们就不会成为正锚点。因此,锚点和问题对象之间已经存在显著的重叠。因此,从锚框边界框到对象边界框的学习偏移是有意义的。回归器负责学习这个偏移:即,我们必须对锚框边界框进行多少调整才能使其成为对象边界框。FRCNN 采用了以下参数化:
t[x] = (x - x[a])/w[a]
t[y] = (y - y[a])/h[a]
t[w] = log(w/w[a])
t[h] = log(h/h[a])
方程 11.2
其中 x、y、w 和 h 表示 GT 边界框的中心坐标及其宽度和高度,而 x[a]、y[a]、w[a] 和 h[a] 表示锚边界框的中心坐标及其宽度和高度。t[x]、t[y]、t[w] 和 t[h] 是回归目标。回归器实际上是在学习预测锚边界框和 GT 边界框之间的差值。
注意:将回归目标分配给锚框的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列表 11.19 使用 PyTorch 为每个锚框分配回归目标的代码
def transform_bboxes(bboxes): ①
height = bboxes[:, 3] - bboxes[:, 1]
width = bboxes[:, 2] - bboxes[:, 0]
x_ctr = bboxes[:, 0] + width / 2
y_ctr = bboxes[:, 1] + height /2
return torch.stack( ②
[x_ctr, y_ctr, width, height], dim=1)
def get_regression_targets(roi_bboxes, gt_bboxes): ③
assert roi_bboxes.shape == gt_bboxes.shape
roi_bboxes_t = transform_bboxes(roi_bboxes)
gt_bboxes_t = transform_bboxes(gt_bboxes)
tx = (gt_bboxes_t[:, 0] - roi_bboxes_t[:, 0]) / roi_bboxes_t[:, 2]
ty = (gt_bboxes_t[:, 1] - roi_bboxes_t[:, 1]) / roi_bboxes_t[:, 3]
tw = torch.log(gt_bboxes_t[:, 2] / roi_bboxes_t[:, 2])
th = torch.log(gt_bboxes_t[:, 3] / roi_bboxes_t[:, 3])
return torch.stack([tx, ty, tw, th], dim=1) ④
① (n, 4) 张量,格式为 (xtl, ytl, xbr, br)
② (n, 4 张量),格式为 (x, y, w, h)
③ 分别表示感兴趣区域和 GT 的边界框的 (n, 4) 张量
④ 包含回归目标的 (n, 4) 张量
RPN 损失函数
我们已经定义了 RPN 全卷积网络以及如何为 RPN FCN 的输出生成标签和回归目标。现在我们需要讨论使我们能够训练 RPN 的损失函数。正如你所期望的,有两个损失项:
-
分类损失—适用于正负锚框。我们使用任何标准分类器中使用的标准交叉熵损失。
-
回归损失—仅适用于正锚框。在这里,我们使用平滑 L1 损失,其定义为

方程 11.3
我们可以将平滑 L1 损失视为 L1 和 L2 损失的组合。如果值小于 beta,它表现得像 L2 损失(均方误差 [MSE])。否则,它表现得像 L1 损失。在 FRCNN 中,beta 设置为 1。背后的直觉很简单。如果我们使用纯 L2 损失(MSE),则由于损失的二次性质,高损失项会对指数损失产生贡献。这可能导致一个偏差,即通过关注高值项来降低损失。相反,如果我们使用纯 L1 损失,高损失项仍然会贡献更多的损失,但效果是线性的而不是二次的。这仍然对高损失项有轻微的偏差。通过在损失值较小时使用 L2 损失,在损失值较大时使用 L1 损失,我们得到了两者的最佳结合。当损失值较小时,因为我们使用 L2 损失,其贡献是指数/二次的。而当损失值较高时,它仍然通过 L1 损失线性地贡献。因此,网络被激励关注低损失和高损失项。
图像的整体损失可以定义为以下:

方程 11.4
其中,p[i] 是锚点 i 的预测对象概率。p[i]^* 是锚点 i 的真实对象标签。如果锚点是正的,则为 1;如果锚点是负的,则为 0。t[i] = (t[x], t[y], t[w], t[h]) 是锚点 i 的回归预测,t[i]^* = (t[x]^, t[y]^, t[w]^, t[h]^) 是锚点 i 的回归目标,N[cls] 是锚点的数量,N[pos] 是正锚点的数量。
注意:RPN 损失函数的完全功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列表 11.20 RPN 损失函数的 PyTorch 代码
def rpn_loss(
rpn_cls_scores, rpn_loc, rpn_labels,
rpn_loc_targets, lambda_ = 10): ①
classification_criterion = nn.CrossEntropyLoss(
ignore_index=-1) ②
reg_criterion = nn.SmoothL1Loss(reduction="sum")
cls_loss = classification_criterion(rpn_cls_scores, rpn_labels)
positive_indices = torch.where(rpn_labels==1)[0] ③
pred_positive_anchor_offsets = rpn_loc[positive_indices]
gt_positive_loc_targets = rpn_loc_targets[positive_indices]
reg_loss = reg_criterion(
pred_positive_anchor_offsets,
gt_positive_loc_targets) / len(positive_indices)
return {
"rpn_cls_loss": cls_loss,
"rpn_reg_loss": reg_loss,
"rpn_total_loss": cls_loss + lambda_* reg_loss
}
① rpn_cls_scores: (num_anchors, 2) 张量,表示每个锚点的 RPN 分类器得分。rpn_loc: (num_anchors, 4) 张量,表示每个锚点的 RPN 回归器预测。rpn_labels: (num_anchors) 表示每个锚点的类别(-1, 0, 1)。rpn_loc_targets: (num_anchors, 4) 张量,表示每个锚点的 RPN 回归器目标。
② 忽略 -1,因为它们没有被采样
③ 找到正锚点
生成区域提议
我们已经讨论了 RPN 的工作原理。RPN 预测每个锚点的对象概率和回归偏移量。下一个任务是生成好的区域 ROI,并用于训练 R-CNN 模块。由于我们对每个锚点都发出对象概率和回归偏移量,因此我们有数千个预测。我们不能使用它们全部作为 ROI。我们需要从这些分数和偏移量中生成最好的 ROI 来训练我们的 R-CNN。一个明显的方法是依赖于对象概率分数:对象概率分数越高,包含对象的概率就越大,因此它是一个好的 ROI。在我们到达那里之前,我们必须做一些基本的处理步骤:
1. 将预测的偏移量转换为边界框。这是通过反转变换序列来完成的
x^∗ = t[x]^∗ ∗ w[a] + x[a]
y^∗ = t[y]^∗ ∗ h[a] + y[a]
w^∗ = e(*t[w]*∗) ∗ w[a]
h^∗ = e(*t[h]*∗) ∗ h[a]
方程 11.5
其中,x^, y^, w^, 和 h^ 表示预测边界框的中心坐标及其宽度和高度,t[x]^, t[y]^, t[w]^, 和 t[h]^ 是 RPN 的定位预测。然后,将边界框转换回 xtl, ytl, xbr, ybr 格式。
2. 预测的边界框可能部分位于图像之外。我们将所有预测的边界框裁剪到图像内部。
3. 移除任何高度或宽度小于 min_roi_threshold 的预测边界框。
一旦完成这些处理步骤,我们将预测的边界框按对象概率分数排序,并选择 N 个候选框。训练期间 N = 12000,测试期间 N = 6000。
注意:完全功能的从 RPN 输出生成区域提议的代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列表 11.21 从 RPN 输出生成区域提议的 PyTorch 代码
rois = generate_bboxes_from_offset(rpn_loc, anchors)
rois = rois.clamp(min=0, max=width) ①
roi_heights = rois[:, 3] - rois[:, 1] ②
roi_widths = rois[:, 2] - rois[:, 0]
min_roi_threshold = 16
valid_idxes = torch.where((roi_heights > min_roi_threshold) &
(roi_widths > min_roi_threshold))[0]
rois = rois[valid_idxes]
valid_cls_scores = rpn_loc[valid_idxes]
objectness_scores = valid_cls_scores[:, 1]
sorted_idx = torch.argsort( ③
objectness_scores, descending=True)
n_train_pre_nms = 12000
n_val_pre_nms = 300
rois = rois[sorted_idx][:n_train_pre_nms] ④
objectness_scores = objectness_scores[
sorted_idx][:n_train_pre_nms] ⑤
① 裁剪 ROI
② 基于 min_roi_threshold 的阈值
③ 根据物体性进行排序
④ 选择最感兴趣的区域。形状:(n_train_pre_nms, 4)。
⑤ 选择最高的物体性分数。形状:(n_train_pre_nms,)。
非极大值抑制(NMS)
许多提议将重叠。我们实际上是以 16 像素的步长选择锚点。因此,即使是合理大小的对象也会被多个锚点选中,每个锚点都会尝试独立地预测对象。当我们查看图 11.16 中的正锚点时,我们可以看到这种重叠的性质。我们希望选择最有效的 ROI 集。但很明显,选择所有相似的提议并不能构成一个好的 ROI 集,因为它们携带了冗余信息。

图 11.16 最小值,其中每个锚点都被分类为负。为了防止这种情况,FRCNN 在训练之前对负锚点进行下采样。
为了解决这个问题,我们使用一种称为非极大值抑制(NMS)的技术。NMS 是一种抑制高度重叠边界框的算法。该算法接受边界框和分数,并按以下方式工作。
算法 11.6 非极大值抑制
输入:一个边界框列表 B,相应的分数 S,以及重叠阈值 N
输出:一个过滤后的边界框列表 D
while 概率在增加 do
选择置信度分数最高的边界框
从 B 中移除它并将其添加到最终列表 D 中
使用 IoU 比较所选边界框与 B 中剩余的框
从 B 中移除所有 IoU 大于阈值的边界框
end while
返回 D
我们使用 0.7 阈值的 NMS 来抑制高度重叠的 ROI,并在 NMS 后选择前N个 ROI 来训练 R-CNN。N在训练期间为 2000,在测试期间为 300。图 11.17 显示了在 NMS 之前和之后的样本图像上的边界框。

图 11.17 左侧列显示了 NMS 之前的边界框(24)。右侧列显示了 NMS 之后的边界框(4)。
注意:完全功能的 NMS 代码,可通过 Jupyter Notebook 执行,可以在mng.bz/nY48找到。
列表 11.22 PyTorch 代码用于 ROI 的 NMS
n_train_post_nms = 2000
n_val_post_nms = 300
nms_threshold = 0.7
post_nms_indices = torchvision.ops.nms( ①
rois, objectness_scores, nms_threshold)
post_nms_rois = rois[post_nms_indices[:n_train_post_nms]]
① 调用由 torchvision 实现的 NMS
11.4.3 快速 R-CNN
在上一节中,我们看到了 RPN 网络如何接受输入图像并输出一组可能包含对象的感兴趣区域。现在让我们讨论 FRCNN 架构的第二部分,它接受 ROI 并生成图像中每个对象的类别概率和边界框坐标。我们之前简要讨论了这一点。在这里,我们更详细地回顾一下。
我们给定了一组 RoI(其中一些包含对象)。我们的任务是训练一个能够定位对象的物体检测器。为此,我们需要提取每个 RoI 对应的特征,并通过一个神经网络(分类器和回归器)传递,该神经网络学习预测类别和回归目标。R-CNN 以一种简单的方式解决了这个问题:它一次提取每个 RoI,将其扭曲成固定大小,并通过深度 CNN 提取 RoI 对应的特征。每个 RoI 都需要单独的前向传递,这使得方法非常慢。问题,一如既往,是:我们能做得更好吗?
RoI 池化
让我们考虑卷积骨干。它通过几个卷积和最大池化层处理整个图像,以生成卷积特征图。我们也看到了 16 的子采样因子:即输入图像中的 16 × 16 像素在特征图中被减少到一个点。还要记住,特征图上每个网格点的嵌入是输入图像中区域的表示/摘要。
关键思想 1是每个 RoI 对应的特征已经存在于卷积特征图中,我们可以通过特征图来提取它们。例如,假设我们的 RoI 是(0, 0, 256, 256)。我们知道输入图像中的(0, 0, 256, 256)区域由 0, 0, 256/16, 256/16)表示:即卷积特征图中的(0, 0, 16, 16)区域。由于卷积特征图中每个点的嵌入是感受野的摘要,我们可以直接使用这些特征作为 RoI 的特征。因此,为了获得(0, 0, 256, 256) RoI 的特征,我们取卷积特征图中(0, 0, 16, 16)区域的所有嵌入。由于我们是在整个图像上直接对卷积特征图进行特征提取,我们可以通过单次前向传递获得所有 RoI 的 RoI 特征。这消除了需要多次前向传递的需求。
关键思想 2如下。我们讨论了一种提取每个 RoI 对应特征的方法,并希望使用这些特征来训练我们的分类器和回归器。然而,存在一个问题。众所周知,RoI 的大小不同。不同大小的 RoI 会导致不同的特征嵌入大小。例如,如果我们的 RoI 是(0, 0, 256, 256),我们的 RoI 特征嵌入是(16, 16, 512):也就是说,所有大小为 512 的嵌入都在卷积特征图的(0, 0, 16, 16)区域。如果我们的 RoI 是(0, 0, 128, 128),那么我们的 RoI 特征嵌入是(8, 8, 512):卷积特征图的(0, 0, 8, 8)区域的所有嵌入。我们知道神经网络通常需要相同大小的输入。那么我们如何处理不同大小的输入嵌入呢?答案是RoI 池化。
让我们固定输入 ROI 特征图的大小,该特征图进入神经网络。我们的任务是减少可变大小的 RoI 特征图到固定大小。如果固定的特征图大小设置为 H,W,并且我们的 RoI 在卷积特征图中对应于 (r,c,h,w),我们将 h 和 w 分别划分为大小为 h/H 和 w/W 的相等大小的块,并对这些块应用最大池化以获得 H,W 特征图。回到我们的例子,让我们固定 H = W = 4。我们期望的固定特征图大小是 (4,4,512)。因此,当我们的 RoI 是 (0, 0, 256, 256) 时,我们的 RoI 特征嵌入是 (16, 16, 512):h = w = 16。我们将 16 × 16 区域划分为四个 (16/4, 16/4) 区域,并对每个区域执行最大池化以获得固定大小的 (4,4,512) 特征。同样,当我们的 RoI 是 (0, 0, 128, 128) 时,h = w = 8。我们将 8 × 8 区域划分为四个 (8/4, 8/4) 区域,并对这些区域执行最大池化以获得固定大小的 (4,4,512) 特征。
精明的读者会注意到,我们已仔细选择 RoI,使其是 H 和 W 的倍数,从而 h/H 和 w/W 分别得到整数值。但在现实中,这种情况很少发生。h/H 和 w/W 往往是浮点数。在这种情况下我们该怎么办?答案是量化:也就是说,我们选择最接近 h/H 和 w/W 的整数(在原始实现中为 floor 操作)。ROIAlign 已经改进了这一点,它使用双线性插值而不是量化。我们在这里不深入探讨 ROIAlign 的细节。
实际上,如果我们有一个大的 RoI,我们将特征图划分为一定数量的较大区域并执行最大池化。当我们有一个小的 RoI 时,我们将特征图划分为一定数量的较小区域并执行最大池化。用于池化的区域大小可以改变,但输出大小保持固定。RoI 池化输出的维度不依赖于输入特征图的大小或 RoI 的大小:它仅由我们将 RoI 划分的部分数量决定—H 和 W(见图 11.18)。

图 11.18 一个具有两个不同大小感兴趣区域的卷积特征图。RoI 池化通过最大池化在单个过程中从每个 RoI 中提取固定大小的输出特征图(在此图像中为 2 × 2)。这使得我们能够为每个 RoI 提取固定大小的代表性特征向量,然后将其输入到进一步的分类器和回归层中进行分类和定位。
因此,RoI 池化的目的是对非均匀大小的输入执行最大池化以获得固定大小的特征图。Fast R-CNN 和 Faster R-CNN 使用 7 × 7 作为固定特征图大小。
Fast R-CNN 架构
给定卷积特征图和一组 RoI,我们已经看到 RoI 池化层如何从特征图中提取固定长度的特征向量。每个特征向量被输入到一系列 FC 层中,最终分支成两个兄弟输出层:一个分类器,它为K个对象类别加上一个“背景”类别产生 softmax 概率估计,另一个回归器为K个对象类别中的每一个产生四个实数值。
为 Fast R-CNN 生成 GT
对于每张图像,我们有一个由 RPN 生成的 RoI 列表和一个 GT 边界框列表。我们如何为每个 RoI 生成 GT 和回归目标?想法与我们的 RPN 相同:我们使用 IoU 分数。算法如下:
-
计算所有 RoI 和 GT 框之间的 IoU。
-
对于每个 RoI,确定具有最高 IoU 的 GT 边界框。
-
如果最高的 IoU 大于一个阈值(0.5),则将相应的 GT 标签分配给 RoI。
-
如果 IoU 在[0.1, 0.5]之间,则分配背景标签。使用 0.1 的下限确保某些与 GT 有较小交集的 RoI 被选为背景。这有助于选择背景的困难示例;这是一种困难负样本挖掘的形式。
注意:完整的 Fast R-CNN RoI 头部的功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列出 11.23 PyTorch 代码用于 Fast R-CNN RoI 头部
class Fast_RCNN_ROI_Head(nn.Module):
def __init__(self, num_classes, H, W, subsample=16, embedding_size=512):
super(Fast_RCNN_ROI_Head, self).__init__()
self.num_classes = num_classes
self.H = H
self.W = W
self.embedding_size = embedding_size
self.subsample = 16
self.roi_head_classifier = nn.Sequential(
nn.Linear(H*W*embedding_size, 4096),
nn.ReLU(True),
nn.Linear(4096, 4096),
nn.ReLU(True),
)
self.cls = torch.nn.Linear(4096, num_classes+1) ①
self.reg = torch.nn.Linear(4096, (num_classes+1)*4)
def forward(self, x, rois): ②
assert x.shape[0] == 1 # This code only supports batch size of 1
roi_pooled_features = torchvision.ops.roi_pool(
x, [rois], output_size=(self.H, self.W), spatial_scale=1/subsample)
roi_pooled_features = roi_pooled_features.view(
-1, self.H*self.W*self.embedding_size)
fc_out = self.roi_head_classifier(roi_pooled_features)
roi_cls_scores = self.cls(fc_out)
roi_loc = self.reg(fc_out)
return roi_cls_scores, roi_loc ③
① num_classes + background
② x : (1, c, h, w)张量,表示卷积特征图。rois: (n, 4)张量,表示 RoI 的边界框
③ roi_cls_scores: (n, num_classes+1)张量,表示每个 RoI 的分类得分。roi_loc: (n, (num_classes + 1) * 4)张量,表示每个 RoI 的回归得分
训练 Fast R-CNN
RPN 为每张图像生成大约 2,000 个 RoI。由于计算限制,我们无法使用所有N个 RoI。相反,我们从中采样一个子集。训练 minibatch 是分层采样的,首先采样K个图像,然后从每个图像中采样R/K个 RoI。在 FRCNN 中,R设置为 128。对于这次讨论,我们假设K = 1:也就是说,每个 minibatch 只有一个图像。那么,对于单个图像的 RoI,我们如何从中采样 128 个 RoI 呢?
一个简单的解决方案是随机采样 128 个 RoI。然而,这会遇到我们之前讨论过的相同数据不平衡问题:我们最终采样背景的频率远高于类别。为了解决这个问题,我们采用与之前类似的采样策略。特别是,对于单个图像,我们采样 128 个 RoI,使得背景与对象的比率为 0.75:0.25。如果少于 32 个 RoI 包含对象,我们将使用更多的背景 RoI 填充 minibatch。
分配目标到 RoI 框
正如 RPN 的情况一样,我们为包含对象的全部 RoI 生成回归目标,即 GT 框相对于感兴趣区域的偏移量。对于所有背景 RoI,回归目标不适用。
Fast R-CNN 损失函数
我们已经定义了 Fast R-CNN 网络以及如何为其输出生成标签和回归目标。我们需要讨论使我们能够训练 Fast R-CNN 的损失函数。正如预期的那样,有两个损失项:
-
分类损失—我们使用任何标准分类器中使用的标准交叉熵损失。
-
回归损失—回归损失仅适用于对象 RoI:背景 RoI 不参与回归。这里我们使用与 RPN 中相同的平滑 L1 损失。
因此,单个 RoI 的整体损失可以定义为以下:

方程 11.6
其中 p 是 RoI 的预测标签,u 是 RoI 的真实标签,t[u] = (t[x], t[y], t[w], t[h]) 是类别 u 的回归预测,v = (v[x], v[y], v[w], v[h]) 是 RoI 的回归目标。因此,整体损失可以定义为

方程 11.7
其中 p[i] 是 RoI i 的预测概率,p[i]^* 是 RoI i 的真实标签;t[i] = (t[x], t[y], t[w], t[h]) 是对应于类别 p[i]^* 的 RoI i 的回归预测,t[i]^* = (t[x]^, t[y]^, t[w]^, t[h]^) 是 RoI i 的回归目标,t[i]^* = (t[x]^, t[y]^, t[w]^, t[h]^) 是 RoI i 的回归目标,N[pos] 是对象 RoI(非背景 RoI)的数量。
注意:完整的 Fast R-CNN 损失函数的代码,可通过 Jupyter Notebook 执行,可以在mng.bz/nY48找到。
列表 11.24 PyTorch 代码用于 Fast R-CNN 损失函数
def rcnn_loss(
roi_cls_scores, ①
roi_loc, ②
roi_labels, ③
rcnn_loc_targets, ④
lambda_ = 1):
classification_criterion = nn.CrossEntropyLoss()
reg_criterion = nn.SmoothL1Loss(reduction="sum")
cls_loss = classification_criterion(roi_cls_scores, roi_labels)
pos_roi_idxes = torch.where(roi_labels>0)[0] ⑤
pred_all_offsets = roi_loc[pos_roi_idxes]
num_pos_rois = len(pos_roi_idxes)
pred_all_offsets = pred_all_offsets.view( ⑥
num_pos_rois, -1, 4)
pred_cls_offsets = pred_all_offsets[
torch.arange(num_pos_rois) , roi_labels[pos_roi_idxes]]
gt_offsets = rcnn_loc_targets[pos_roi_idxes]
reg_loss = reg_criterion(pred_cls_offsets, gt_offsets) / num_pos_rois
return {
"rcnn_cls_loss": cls_loss,
"rcnn_reg_loss": reg_loss,
"rcnn_total_loss": cls_loss + lambda_* reg_loss
}
① (128, num_classes) 张量:每个 RoI 的 RCNN 分类器得分
② (128, num_classes*4) 张量:每个类别、RoI 的 RCNN 回归器预测
③ (128,) 张量:每个 RoI 的真实类别
④ (128, 4) 张量:每个 RoI 的 RoI 回归器目标
⑤找到正 RoI
⑥ (n, num_classes*4) 到 n, num_classes, 4)
Fast R-CNN 推理
我们已经讨论了如何训练 Fast R-CNN 模块。一旦模型训练完成,下一个问题是如何使用该模型生成输出类别和边界框。
Fast R-CNN 模型为每个 RoI 输出一个分类得分和回归偏移量。我们可以安全地忽略背景 RoI。对于其余的 RoI,选择概率最高的类别作为输出标签,并选择相应的偏移量。我们应用与 RPN 类似的后续处理步骤:
-
我们使用 RoI 将偏移量转换回(
xtl,ytl,xbr,ybr)格式。 -
我们将输出边界框裁剪到图像边界内
我们面临一个与之前类似的问题:输出可能包含多个与同一对象对应的边界框。我们以相同的方式处理它,即使用 NMS。然而,有一个区别。在 RPN 的情况下,我们在所有由 RPN 预测的边界框上应用了全局 NMS。这里,NMS 仅应用于属于同一类的边界框。这是对所有类进行的,这在直观上应该是合理的:如果边界框代表不同的类,那么抑制高度重叠的边界框是没有意义的。
注意:完整的 Fast R-CNN 推理代码,可通过 Jupyter Notebook 执行,可在mng.bz/nY48找到。
列表 11.25 PyTorch 代码用于 Fast R-CNN 推理
def fast_rcnn_inference(
frcnn_roi_head, ①
rois, ②
conv_feature_map, ③
nms_threshold=0.7):
frcnn_roi_head.eval() ④
roi_cls_scores, roi_loc = frcnn_roi_head(conv_feature_map, rois)
output_labels = torch.argmax( ⑤
roi_cls_scores, dim=1)
output_probs = nn.functional.softmax(
roi_cls_scores, dim=1)[torch.arange( ⑥
rois.shape[0]), output_labels]
output_offsets = roi_loc.view( ⑦
rois.shape[0], -1, 4)
output_offsets = output_offsets[ ⑧
torch.arange(rois.shape[0]), output_labels]
assert output_offsets.shape == torch.Size( ⑨
[rois.shape[0], 4])
output_bboxes = generate_bboxes_from_offset( ⑩
output_offsets, rois)
rois = output_bboxes.clamp(min=0, max=width) ⑪
post_nms_labels, post_nms_probs, post_nms_boxes = [], [], []
for cls in range(1, frcnn_roi_head.num_classes+1): ⑫
cls_idxes = torch.where(output_labels == cls)[0] ⑬
cls_labels = output_labels[cls_idxes]
cls_bboxes = output_bboxes[cls_idxes]
cls_probs = output_probs[cls_idxes]
keep_indices = torchvision.ops.nms(
cls_bboxes, cls_probs, nms_threshold)
post_nms_labels.append(cls_labels[keep_indices])
post_nms_probs.append(cls_probs[keep_indices])
post_nms_boxes.append(cls_bboxes[keep_indices])
return {
"labels": torch.cat(post_nms_labels),
"probs": torch.cat(post_nms_probs),
"bboxes": torch.cat(post_nms_boxes)
}
①Fast_RCNN_ROI_Head 的训练实例
②ROI 到推理
③ (n, c, h, w)卷积特征图
④设置评估模式
⑤预测的类别是得分最高的类别。
⑥通过 softmax 获得预测概率。选择最高概率作为此预测的概率分数。
⑦将 locs 从(n, num_classes*4)转换为(n, num_classes, 4)
⑧选择对应于预测标签的偏移量
⑨断言我们为每个 ROI 有输出
⑩将偏移量转换为 xtl, ytl, xbr, ybr)
⑪将边界框裁剪到图像内
⑫0 是背景,因此被忽略
⑬对每个类别执行 NMS
11.4.4 训练 Faster R-CNN
如我们所见,FRCNN 由两个子网络组成:
-
一个负责生成包含对象的良好区域提议的 RPN
-
一个 Fast R-CNN,负责从 ROI 列表中进行目标分类和检测
因此,FRCNN 是一个两阶段目标检测器。我们有一个阶段生成良好的区域提议,另一个阶段则对区域提议进行检测,以识别图像中的对象。那么我们如何训练 FRCNN 呢?
一个简单的方法是训练两个独立的网络(RPN 和 Fast R-CNN)。然而,我们不想这样做,因为这很昂贵。此外,如果我们这样做,每个网络将以自己的方式修改卷积层。如前所述,我们希望 RPN 和 Fast R-CNN 模块之间共享卷积层。这确保了效率(只有一个卷积主干,而不是两个独立的骨干)。此外,RPN 和 FRCNN 都在执行类似的任务,所以直观上应该共享相同的卷积特征集。因此,我们需要开发一种技术,允许在两个网络之间共享卷积层,而不是学习两个单独的网络。原始的 FRCNN 论文提出了两种训练模型的技术:
-
交替优化(AltOpt)——我们首先训练 RPN,并使用提议来训练 Fast R-CNN。然后,由 Fast R-CNN 调整的网络被用来初始化 RPN,这个过程是迭代的。这涉及到在训练 RPN 和 Fast R-CNN 之间交替的多个训练轮次。
-
近似联合训练—在训练过程中,RPN 和 Fast R-CNN 网络合并为一个网络。在每个 SGD 迭代中,正向传递生成区域提议,在训练 Fast R-CNN 检测器时,这些提议被当作固定的、预先计算的提议来处理。我们将 RPN 和 Fast R-CNN 损失合并,并按常规进行反向传播。由于我们端到端地一起训练这两个网络,这种训练显著更快。然而,由于我们将 RPN 生成的提议视为固定的,而实际上它们是 RPN 的函数,因此这种优化是近似的。所以我们忽略了一个导数。
这两种技术提供了相似的精度。因此,联合训练,其速度显著更快,是首选的。
11.4.5 其他目标检测范式
到目前为止,我们已经详细介绍了 FRCNN,并讨论了几个对其成功有贡献的关键思想。还开发了几种其他的目标检测范式。其中一些受到了 FRCNN 的启发,借鉴并/或改进了 FRCNN 建立的思想。在本节中,我们将简要地看看其中的一些。
你只需看一次(YOLO)
FRCNN 是一个两阶段检测器:一个 RPN 后跟 Fast R-CNN,它在 RPN 生成的区域提议上运行。YOLO (arxiv.org/pdf/1506.02640.pdf),正如其名所示,是一个单阶段目标检测器。单个卷积网络一次同时预测多个边界框及其类别的概率。YOLO 的一些显著特点如下:
-
YOLO 由于其结构简单得多,速度显著更快(比 FRCNN 快 10 倍),甚至可以用于实时目标检测。
-
与 FRCNN 不同,FRCNN 中的 R-CNN 模块只查看区域提议,YOLO 在训练和测试期间直接查看完整图像。
-
YOLO 的速度是以精度为代价的。虽然 YOLO 速度显著,但其精度不如 FRCNN。
在 YOLO 的基础上进行了几项改进,以提高精度,同时试图保持简单、快速的架构。这些包括 YOLO v2、YOLO v3 等。
多边界框单次检测器 SSD)
SSD (arxiv.org/pdf/1512.02325.pdf)试图在速度和精度之间取得良好的平衡。它是一个类似于 YOLO 的单阶段网络:也就是说,它消除了提议生成(RPN)和随后的特征重采样阶段。它还借鉴了 FRCNN 中的锚点思想:在特征图上应用卷积网络,以相对于一组固定的边界框进行预测。
因此,单个深度网络使用应用于特征图的小卷积滤波器,对一组固定的默认边界框预测类别得分和边界框偏移。为了实现高检测精度,使用不同尺度的特征图在不同尺度上进行预测。
SSD 比 YOLO 更准确;然而,它仍然不如 FRCNN(特别是对于小物体)准确。
特征金字塔网络(FPN)
卷积网络生成的特征图是金字塔式的:随着我们深入,特征图的空间分辨率不断降低,我们期望特征图所表示的语义信息更有意义。高分辨率图具有低级特征,而低分辨率图具有更多语义特征。
在 FRCNN 的情况下,我们只在最后一个卷积图上应用了对象检测。SSD 表明,使用其他特征图进行预测是有用的。但是,SSD 在网络的较高层(超过 VGG 的第四个卷积层[conv4])构建这个金字塔。它特别避免使用底层特征。因此,它错过了重用特征层次中更高分辨率图的机会。FPN 表明,这些特征很重要,特别是在检测小物体时。
FPN(arxiv.org/pdf/1612.03144.pdf)依赖于一种架构,该架构通过自上而下的路径和横向连接将低分辨率、语义强大的特征与高分辨率、语义弱的特征相结合。自下而上的路径是卷积层的正向传递。在自上而下的路径中,存在一个从低分辨率到高分辨率的简单上采样回连(它通过 1 × 1 卷积与自下而上的特征图合并)。这个合并的特征图在每一层都被用来学习和做出预测。
FPN 最初是在 FRCNN 之上实现的。它的准确度更高,但比 YOLO/SSD 风格的方案慢得多。
我们只是简要地提到了其他一些突出的检测范式。它们背后的基本原理是相同的。我们鼓励您阅读论文,以获得更深层次和更好的理解。
摘要
在本章中,我们深入探讨了各种用于对象分类和定位的深度学习技术:
-
LeNet 是一个简单的神经网络,可以从 MNIST 数据集中对手写数字进行分类。
-
简单的网络如 LeNet 不能很好地扩展到更真实的图像分类问题。因此,需要更深层的神经网络,它们具有更强的表达能力。
-
VGG 网络是最受欢迎的深度卷积神经网络之一。它通过使用更多的小 3 × 3)滤波器卷积层来改进先前的最先进深度神经网络。这种架构有两个优点:(1)由于堆叠更多层而增加的非线性,因此具有更强的表达能力;(2)参数数量减少。三个 3 × 3 滤波器有 27C²个参数,而单个 7 × 7 滤波器(覆盖相同感受野)有 49C²个参数(多 81%)。
-
VGG(和 AlexNet)使用 ReLU 非线性层而不是 sigmoid 层,因为它们不受梯度消失问题的影响。使用 ReLUs 加速了训练,从而实现了更快的收敛。
-
Inception 模块提供了一种有效的方法来增加神经网络的深度和宽度,同时保持计算预算不变。在每个卷积层使用多尺度滤波器来学习不同大小的模式,并使用 1 × 1 卷积进行降维(这减少了所需的参数数量,从而提高了计算效率)。
-
ResNet 是另一种流行的卷积神经网络。ResNet 架构的灵感来源于这样一个事实:简单地堆叠层超过一定点并不会有所帮助,甚至会导致训练准确度下降。这是一个反直觉的结果,因为我们预期更深层的网络至少可以学习到与较浅层网络一样多的知识。ResNet 论文的作者们展示了这种情况可能发生,因为神经网络难以学习恒等函数。为了解决这个问题,他们提出了快捷/跳过连接来简化神经网络的学习目标。这是 ResNet 的关键思想,使得训练更深层的神经网络成为可能。
-
Faster R-CNN 是最受欢迎的目标检测器之一。它是一个两阶段网络,包括(1)一个区域提议网络(RPN),负责预测可能包含物体的感兴趣区域;(2)一个 R-CNN 模块,它接收区域提议作为输入,并高效地输出类别得分和边界框。
-
RPN 模块使用多个锚框(在每个卷积特征图上的每个点)来处理不同大小和宽高比的对象。它在卷积特征图上卷积一个小型网络,以在每个滑动窗口位置对物体存在性和边界框进行预测。记住,这个小网络是一个完全卷积网络(FCN),由 3 × 3 卷积后跟 1 × 1 卷积组成,这使得这种方法能够处理任意大小的输入,并使其具有平移不变性。
-
RoI 池化提供了一种有效的方法,可以从不同大小的区域提议中提取固定大小的特征向量,这一切都在一次遍历中完成。这些特征向量被输入到分类器和回归器中,分别用于分类和定位。
-
非极大值抑制(NMS)是一种去除重叠边界框的技术。
-
FRCNN 可以使用两种方法进行训练:替代优化(AltOpt)和近似联合训练。这两种方法都能达到相似的准确度,但近似联合训练的速度要快得多。
-
你只看一次(YOLO)、单次检测器 SSD 和特征金字塔网络(FPN)是其他一些流行的目标检测器。
第十二章:流形、同胚性和神经网络
本章涵盖
-
流形的介绍
-
同胚性的介绍
-
流形和同胚性在神经网络中的作用
这是一章简短的章节,简要介绍了(几乎触及不到表面)一个可能填满整个数学教科书的主题。对流形的严格或甚至全面处理超出了本书的范围。相反,本章主要关注深度学习所需的几何直觉。
12.1 流形
一个流形是曲线、曲面和体积概念的推广,它是一个在任意维度中起作用的统一概念。在机器学习中,输入空间可以被视为一个流形。通常,输入流形对分类并不十分有利。我们需要将这个流形转换(映射)到另一个对当前分类问题更友好的流形上。这正是神经网络所做的事情。
在多层神经网络中,每一层都从一维流形转换(映射)到另一维流形。对于分类问题,期望的最终流形是一个其中类别可以通过线性表面(超平面)分离的流形。最后一层提供这个线性分离器。图 12.1 提供了一个将转换到分类更容易的空间的例子。

图 12.1 输入点位于一个圆柱形流形(表面)上。在这里,两个输入类别(分别用+和-表示)不能通过线性方式分离。如果我们展开(映射)圆柱表面到一个平面上,这两个类别可以通过线性方式分离。
与我们在日常生活中看到和触摸的物理表面不同,流形并不局限于三维。人类的想象力可能在可视化高维流形时失败,但我们可以通常使用三维类比作为替代——它们通常有效,尽管并不总是如此。一个神经网络层或一系列层可以被视为将点从一个流形映射到另一个流形。
流形是局部欧几里得的。为了直观理解这一点,想象一个用非常细的绳子绕着的圆圈。现在,取圆圈上的任意一点,并取包含该点的小圆弧。如果我们切断对应该段落的绳子部分,我们可以将那小段绳子拉直,而不会扭曲或撕裂它。换句话说,所选点周围的圆圈上的小邻域与一条线段有 1:1 的映射。圆圈上的所有点都满足这个性质。这样的曲线被称为局部欧几里得。这个概念可以扩展到更高维度。考虑球面的表面(这是一个二维流形的例子)。想象一个紧密贴合球面的橡皮片。取一个任意点和包含该点的球面小片(见图 12.2c)。如果我们切断对应该片状的橡皮片,它可以被展平成一个平面,而不会扭曲或撕裂。因此,球面是局部欧几里得的。环面表面(甜甜圈形状的物体)是二维流形的另一个例子。一般来说,d-维流形是一个空间(点的集合),其中每个点都有一个包含该点的小邻域,并且可以 1:1 映射到ℝ^d,而不会扭曲或撕裂。例如,一个圆是 1 维流形,它上面的每个点都有一个可以映射到一条线(ℝ¹)的容器弧。球面是一个二维流形,它的每个点都有一个可以 1:1 映射到一个平面(ℝ²)的包含片。我们说流形是局部欧几里得的。图 12.2 说明了这个概念。

(a) 任何任意连续曲线(例如,一个圆)是一个 1 维流形。

(b) 8 曲线是一个非流形。标记点的χ形邻域不能 1:1 映射到一条线段。

(c) 球体是二维流形的一个例子。

(d) 沙漏是一个二维非流形表面的例子。
图 12.2 1 维和 2 维中的流形和非流形
仔细思考一下,你会发现局部欧几里得性质使得微积分成为可能。例如,我们如何计算曲线下方的面积,即区间 x = a 和 x = b 上的 f(x)?公式是 ∫[x=a]^(x=b) f(x) dx。我们取曲线的无限小段,并假设它们是直线,可以投影到 X 轴上的一个很小的线段上。得到的窄四边形可以近似为一个边平行于 X 和 Y 轴的矩形。我们求和(积分)覆盖我们正在计算的相同面积的所有的很小的矩形的面积(见图 12.3)。这个方案依赖于将曲线的微小段表示为直线的可能性。关于计算曲线段的长度,可以提出一个类似的案例。在更高维的情况下,同样的想法适用:微积分依赖于将曲线或表面局部地近似为平面的可能性。任何连续的向量函数的图像
= f(
),其中
是 ℝ^n 的任意开子集——即
∈ 𝕌 ⊂ ℝ^n 且 f(
) ∈ ℝ^m——在 ℝ^m × ℝ^n 中产生一个 (
,
) 流形。

图 12.3 曲线下方的面积是通过局部地用很小的直线段来近似曲线段来计算的——需要局部欧几里得性质
在这个背景下,请注意,整个球面不能映射到一个平面上(试着将球面展开到平面上)。这就是为什么不可能在一张纸上绘制出全球的完美地图,使得所有区域都按比例绘制。通常,极地地区在纸地图上占据不成比例的大面积。但球面表面上的小片区域可以映射到平面上,这足以称这个表面为流形。因此,局部欧几里得中的“局部”一词。
12.1.1 Hausdorff 性质
流形通常还具有另一个性质,称为 Hausdorff 性质。如果我们取流形上的任意一对点——无论它们有多接近——我们都可以找到围绕各自点的由流形上的点组成的不相交邻域对。通俗地说,这意味着如果我们取流形上的任意一对点,我们可以在它们之间找到无限多个点,所有这些点都属于流形。这在图 12.4 中得到了说明。很容易看出这一点对于实数线(ℝ¹)是成立的:取任意一对点,它们之间有足够的点来创建一个以每个点为中心的不相交邻域对。

图 12.4 对于任意一对点,我们可以找到一个包含相应点且由流形上的点组成的不相交邻域对。
12.1.2 第二可数性质
流形是第二可数的。为了解释这一点,我们首先简要概述几个概念。(免责声明:以下解释倾向于易于理解,而不是数学上的严谨。)
开集、闭集和边界
考虑属于区间 A ≡ 0 < x < 1 的点集(你可以想象它为实线的一段)。取这个集合 A 中的任意一点,比如 x = 0.93。你可以找到它左侧的点(比如,0.92)和右侧的点(比如,0.94),这两个点都在同一个集合 A 中。在某种意义上,这个点被属于同一集合的点所包围。因此,它是一个 内部 点。有趣的是,在这个集合中,所有点都是内部点。相比之下,考虑集合 A[c] ≡ 0 ≤ x ≤ 1。这个集合包括之前的集合 A 以及边界点 x = 0 和 x = 1。注意,边界点可以从集合 A 的内部和外部接近。如果我们取边界上任意一点的微小邻域,由所有距离为 ϵ 的点组成,那么在这个邻域内既有在 A 内的点,也有在 A 外的点。A 是一个开集。如果我们将其边界添加到自身,它就变成了闭集 A[c]。
这个概念可以扩展到更高维度。例如,属于单位圆 S ≡ x² + y² < 1 的二维点集是一个开集。如果我们添加边界——圆 S[c] ≡ x² + y² = 1,我们得到一个闭集。所有这些都在图 12.5 中得到了说明。

图 12.5 显示了灰色为开集,无边界;黑色为边界。灰色加黑色为闭集。
有界、紧致和预紧致集合
如果一个集合的所有点都位于彼此固定的距离之内,那么这个集合被认为是 有界 的。之前讨论的集合 A、A[c]、S 和 S[c] 都是 有界 的。一个 紧致 集合既是 有界 的也是 闭 的。集合 A[c] 和 S[c] 是紧致的。如果一个集合可以通过添加其边界来转换成一个紧致集合,那么这个集合是 预紧致 的(例如,A 和 S)。请注意,并非所有开集都是预紧致的:例如,−∞ < x < ∞ 是开集但不是预紧致的。然而,所有预紧致集合都是开集。
流形可能包含边界,也可能不包含边界。圆盘是一个带有边界的二维流形。它的边界是圆的周长,这是一个一维流形。三维球体是一个带有边界的三维流形。其边界是球面的表面,这是一个二维流形。圆盘上不带边界的点的开集也是一个二维流形。一个正方形区域是一个带有边界的二维流形,其边界是正方形,这是一个一维流形。三维立方体是一个带有边界的三维流形,其边界是立方体的表面,这是一个二维流形。一般来说,带有边界的 d-维流形的边界是一个 d − 1 维流形。
现在,让我们回到流形的第二可数性质。流形的第二可数性质意味着每个流形都有一个开集的基。这意味着对于每个流形 M,存在一个可数的集合 U ≡ {U[i]},其中 U[i] 是 M 的紧致子集,并且 M 的任何开子集都可以表示为 U 的元素的并集。这如图 12.6 所示。

图 12.6 显示了球面内包含的区域。
12.2 同胚性
我们一直在谈论圆弧与线段之间的一一对应映射。如果我们把一根绳子紧紧地套在圆弧上,我们只需要抓住绳子的两端并拉出来,就可以得到相应的直线——我们不需要进行任何扭曲或撕裂(见图 12.2 中的黑色弧线)。同样,球面表面上的一个区域可以通过简单地拉伸覆盖在该区域上的一个想象中的橡皮片来一一对应地映射到一个平面上(见图 12.2c 中的区域)。这些都是称为 同胚性 的一般映射类别的例子。形式上,同胚映射包含两个函数 f 和 f^(−1),它们在两个点集 X 和 Y 之间,使得

其中
-
f 是一一对应的:它将每个
映射到一个 唯一的
,并且不同的
被映射到不同的
。 -
f^(−1) 是一一对应的:它将每个
映射到一个 唯一的
,并且不同的
被映射到不同的
。 -
f 是一个连续函数:它将
的邻近值映射到
的邻近值。 -
f^(−1) 是一个连续函数:它将
的邻近值映射到
的邻近值。
可视化同胚性的直观方法是,它通过拉伸或压缩将一个流形转换成另一个流形,但从不通过切割、断裂或折叠。同胚性保持路径连通性。如果任意两点之间都存在一条路径,包括属于该集合的点,则称该点集是路径连通的。
12.3 神经网络与流形之间的同胚性
考虑在实数线上定义的两个类 A 和 B:

与这些类别相对应的 1-流形在其原始空间中并不容易区分(见图 12.7a),因为类别 A 被类别 B“包围”。但如果我们挤压原点并将其向上拉——换句话说,执行特定的同胚变换——将其转换,如图 12.7c 所示,就有可能用一条直线区分转换后的流形。同样,图 12.7b 显示了两个类别A和B:

这些是难以在其原始空间中区分的 2-流形,因为,同样,类别 B 包围了类别 A。但如果我们挤压并拉扯原点以创建图 12.7d 所示的流形,它们就可以通过平面进行区分(见图 12.7d)。

(a) 线性分类器不能区分直线上的点。

(b) 线性分类器不能区分圆盘上的点。

(c) 将直线转换成弯曲形状后,可以使用线性分类器(直线)进行分类。

(d) 将圆盘转换成 3D 钟形形状后,可以使用线性分类器(平面)进行分类。
图 12.7 将同胚变换到更友好的流形有助于分类。
神经网络的线性层执行以下转换,这在方程 8.7 中有详细讨论):

注意,所有这些操作,包括与权重矩阵W相乘、通过
进行平移和 sigmoid 非线性都是连续可逆函数。因此,它们是同胚。将这些操作按顺序应用时的复合操作又是另一个同胚。(严格来说,权重矩阵的乘法只有在权重矩阵 W *是方阵且行列式非零时才是可逆的。如果权重矩阵的行列式为零,该层实际上执行的是降维。)
观察多层神经网络的一种方式是,连续的层通过同胚变换输入流形,使其更容易区分类别。最后一层可能是一个简单的线性分离器(如图 12.7 所示)。
摘要
-
流形是一个超维点的集合空间,它满足三个性质:局部欧几里得、豪斯多夫和第二可数。
-
一个d-流形是一个空间,其中每个点都有一个小的邻域包含该点,并且可以 1:1 地映射到ℝ^d的一个子区域,而不需要折叠、扭曲或撕裂。换句话说,流形上任何一点的局部邻域都可以近似为某种平坦的东西。例如,三维空间中的连续曲线是一个 1-流形:如果我们把曲线想象成一根绳子,任何局部邻域都是一段绳子,可以被拉直成一条直线。三维空间中的任何连续表面都是一个 2-流形:如果我们把表面想象成一块橡皮膜,任何局部邻域都可以被拉平和拉伸成一块平坦的平面区域。这种性质(能够假装曲线或表面可以被局部替换为线性部分)使我们能够进行微积分。
-
同胚性是一种特殊的变换类别,它通过拉伸/压缩而不撕裂、断裂或折叠将一个流形映射到另一个流形。
-
同胚性保持路径连通性。
-
大致来说,神经网络层可以被视为一种同胚变换,它将输入流形上的点映射到一个(希望)更适合最终目标的输出流形。整个多层感知器(神经网络)可以被视为一系列同胚变换,通过一系列步骤将输入流形转换为一个使最终目标更容易实现的流形。特别是,分类神经网络将输入流形映射到一个输出流形,其中属于不同类别的点被很好地分隔开。
第十三章:全贝叶斯模型参数估计
本章涵盖
-
无监督建模的全贝叶斯参数估计
-
将先验信念注入参数估计
-
使用已知或未知均值和精度的高斯似然参数估计
-
正态-伽马和 Wishart 分布
假设我们有一个感兴趣的数据集:比如说,所有包含猫的图像。如果我们把图像表示为高维特征空间中的点,那么我们感兴趣的数据集就构成了该特征空间的一个子空间。现在我们想要为我们的数据集创建一个无监督模型。这意味着我们想要识别一个概率密度函数 p(
),其样本云(通过多次重复采样概率分布而获得的点的集合)与我们的感兴趣子空间大致重叠。当然,我们不知道感兴趣的子空间的确切位置,但我们已经从感兴趣的数据集中收集了一组样本 X:即训练数据。我们可以使用 X 的点云作为未知感兴趣子空间的一个替代。因此,我们本质上是在尝试识别一个概率密度函数 p(
),其样本云大致与 X 重叠。
一旦我们有了模型 p(
),我们就可以用它来生成更多的数据样本;这些将是计算机生成的猫图像。这是生成模型。此外,给定一个新图像
,我们可以通过评估 p(
) 来估计它成为猫图像的概率。
13.1 全贝叶斯估计:非正式介绍
让我们回顾贝叶斯定理:

方程式 13.1
在这里,X = {
[1],
[2],⋯} 表示训练数据集。我们的最终目标是识别似然函数 p(
|θ). 估计似然函数有两个方面:选择函数族和估计参数。我们通常根据对问题的了解预先选择函数族,然后估计模型参数。例如,我们模型似然函数的函数族可能是高斯分布:p(
|θ) = 𝒩(
;
, Σ),正如之前所述,分号将模型变量与模型参数分开)。然后 θ = {
, Σ} 是需要估计的模型参数。我们估计 θ 以使整体似然 p(X|θ) = ∏[i ]p(
[i]|θ) 最好地拟合训练数据 X。
我们想再次强调最佳拟合意味着似然函数的样本云(从p(
|θ)重复抽取的样本)与训练数据集X大体重叠。对于高斯情况,这意味着均值
应该落在训练数据点非常集中的地方,协方差矩阵Σ应该是这样的,即似然函数的椭圆底部尽可能紧密地包含尽可能多的训练数据点。
13.1.1 参数估计和信念注入
参数估计有各种可能的方法。最简单的方法是最大似然参数估计(MLE),在第 6.6.2 节中介绍。在 MLE 中,我们选择最大化观察到的训练数据集似然p(X|θ)的参数值。这有些道理。毕竟,我们知道唯一真实的事情是输入数据集X已经被观察到了——由于这是无监督数据,我们不知道其他任何事情。选择最大化已知真相概率的参数是合理的。如果训练数据集很大,MLE 估计效果良好。
然而,在没有足够大量的训练数据的情况下,将我们对系统的先验知识注入到估计中通常是有帮助的——先验知识可以弥补数据不足。这种将猜测/信念注入系统的过程是通过先验概率密度完成的。为此,我们不能再最大化似然,因为似然忽略了先验。我们必须进行最大后验(MAP)估计,它最大化后验概率。后验概率是似然(依赖于数据)和先验(不依赖于数据;我们将使其反映我们的先验信念)的乘积。
有两种可能的 MAP 范式。我们在第 6.6.3 节中看到了其中之一,在那里我们注入了我们的信念,即未知参数的幅度必须小,并将p(θ) ∝ e^(−||θ||²)作为正则化器。系统被激励选择相对较小的参数值。在本章中,我们研究一个不同的范式;让我们用一个例子来说明它。
假设我们将似然函数建模为高斯分布:p(
|θ) = 𝒩(
;
, Σ). 我们必须从训练数据 X 中估计参数 θ = {
, Σ},为此我们必须最大化后验概率。为了计算后验概率,我们需要先验概率。此外,我们还必须以某种方式注入常数值作为我们对参数值的信念(缺乏观察数据)。那么,将先验概率建模为参数空间中的高斯概率密度函数如何?为了简化起见,我们忽略协方差矩阵参数,可以将均值参数的概率密度建模为 p(
) = 𝒩(
;
[0], Σ[0])。我们实际上是在说,我们相信参数
很可能具有接近
[0] 的值,并且具有 Σ[0] 的置信度。换句话说,我们正在将一个常数值作为对参数
的信念注入其中。我们可以类似地处理协方差。后来,我们证明在这个范式下,在训练数据量较少的情况下,先验概率占主导地位。一旦消化了足够多的训练数据,先验概率的影响就会减弱,解决方案就会越来越接近最大似然估计(MLE)。这就是简而言之的完全贝叶斯参数估计技术。
在本章中,我们讨论了一系列越来越复杂的场景中高斯似然函数参数的贝叶斯估计。在第 13.3 节中,我们处理了要估计的参数方差已知(常数)但均值未知的情况,因此均值被表示为一个(高斯)随机变量。然后,在第 13.6 节中,我们考察了均值已知(常数)但方差未知的情况。最后,在第 13.7 节中,我们考察了两者都未知的情况。对于每个场景,都处理了单变量和多变量情况。
注意:本章的完整功能代码,可通过 Jupyter Notebook 执行,可以在 mng.bz/woYW 找到。
13.2 高斯参数值的最大似然估计(MLE)回顾)
我们已经在第 6.8 节中讨论了这一细节。在这里,我们回顾一下主要结果。假设我们有一个数据集 X = {x^((1)), x^((2)),⋯, x^((n))}。我们决定将数据分布建模为高斯分布 𝒩(x;μ, σ)—我们想要估计参数 μ, σ,以最好地“解释”或“拟合”观察到的数据集 X。最大似然估计是解决此问题的一种简单方法。在这里,我们估计参数,使得 训练过程中观察到的数据的似然性最大化。这可以粗略地理解为估计一个概率密度函数,其峰值与输入空间中训练数据密集区域相吻合。我们在第 6.8 节中讨论了最大似然估计。在这里,我们只是重新陈述了这些表达式。
让我们用 μ 和 σ 表示数据分布的(尚未知的)均值和方差。然后从方程 5.22 中,我们得到

最大化对数似然 p(X|μ, σ) 有一个闭式解:

方程 13.2
因此,最大似然估计的均值和方差本质上就是训练数据样本的均值和方差(参见第 6.6 节中这些表达式的推导)。
多变量高斯最大似然估计的对应表达式是

方程 13.3
这些最大似然估计参数值用于评估 p(x) = 𝒩(x;μ, σ)—即未知数据点 x 来自由训练数据集 X 表示的分布的概率。
13.3 完全贝叶斯参数估计:高斯分布,未知均值,已知精度
当可用数据集较小(即,数据集 X 的大小 n 较小)时,最大似然估计(MLE)可能并不那么准确。在许多问题中,我们对数据集的均值和标准差有一个先验的想法。不幸的是,MLE 提供了没有将这种先验信念融入估计的方法。完全贝叶斯参数估计技术试图解决这个问题:在这里,我们不仅仅是最大化观察数据的似然性。相反,我们最大化估计参数的后验概率。这个后验概率涉及到似然性和先验概率的乘积(见方程 13.1)。似然项捕捉了训练数据的影响——仅最大化它是 MLE,但它不捕捉先验信念的影响。另一方面,先验项不依赖于数据。这就是我们将我们的信念、猜测或先验知识关于数据分布嵌入的地方。因此,我们对数据分布参数的估计将考虑数据和先验猜测。我们将很快看到,这种估计是随着数据集的大小(n,X 的长度)增加,先验项的影响减小,似然项的影响增加。在极限情况下,在数据可用性无限的情况下,贝叶斯推理产生 MLE。在另一个极端情况下,当没有数据可用(n = 0)时,完全贝叶斯参数估计与先验估计相同。
让我们考察贝叶斯参数估计。首先,我们处理一个相对简单的情况,即我们有一个具有已知(常数)方差但未知且已建模的均值的高斯数据分布。数据分布是高斯分布(通常,𝒩(x;μ[n], σ) 中的分号将变量与参数分开):

训练数据集表示为 X = {x^((1)), x^((2)),⋯, x^((n))},其整体似然性为

假设方差是已知的——因此它被视为一个常数而不是随机变量。均值 μ 是未知的,被视为一个具有均值 μ[0] 和方差 σ[0](不要与 μ 和 σ 混淆,它们是数据本身的均值和方差)的高斯随机变量。因此,先验是

未知参数 μ 的后验概率是两个高斯分布的乘积,这本身也是一个高斯分布。让我们用 μ[n] 和 σ[n] 表示这个乘积高斯分布的尚未知的均值和方差。在这里,下标 n 是为了提醒我们,后验是通过处理 X = {x^((1)), x^((2)),⋯, x^((n))} 中的 n 个数据实例来获得的。因此,高斯后验可以表示为

使用贝叶斯定理,

通过比较左右两侧指数上的 μ² 和 μ 的系数,我们确定后验分布的未知参数:

方程 13.4
应该清楚地理解各种名称相近的变量的意义:
-
μ 和 σ 是数据分布 p(x) 的均值和方差——假设为高斯分布。最终目标是估计 μ 和 σ,以最佳地拟合数据集 X。另一方面,μ[0] 和 σ[0] 是参数分布 p(μ) 的均值和方差,它捕捉了我们关于数据均值 μ 的先验信念(记住,根据假设,数据均值也是一个高斯随机变量)。μ[n] 和 σ[n] 是从 n 个数据点样本计算出的后验分布 p(μ|X) 的均值和方差。因为它是由两个高斯分布的乘积,所以它是一个高斯随机变量。
-
未知均值参数的后验分布 p(μ|X) 是一个以 μ[n] 为均值的正态分布。因此,当 μ = μ[n] 时,它将达到最大值。换句话说,未知均值 μ 的 MAP 估计为 μ[MAP] = μ[n]。
-
尽管 μ[n] 是 μ 的最佳估计,但 σ[n] 并没有近似数据的 σ,在这种情况下,σ 是通过假设而知的。在这里,σ[n] 是后验分布的方差,反映了我们对 μ 估计的不确定性。这就是为什么,当数据实例的数量变得非常大时,σ[n] 趋近于 0(表示我们对均值估计没有不确定性或完全有信心)。
我们数据分布的估计为 p(x) = 𝒩(x;μ[n], σ),其中 μ[n] 由方程 13.4 给出。请注意,它是 MLE x̄ 和先验猜测 μ[0] 的组合。使用这个,对于任何任意数据实例 x,我们可以推断 x 属于训练数据集 X 的类的概率。
注意:完全功能的贝叶斯估计代码,具有未知均值和已知方差,可通过 Jupyter Notebook 执行,可以在 mng.bz /ZA75 找到。
列表 13.1 PyTorch- 带有未知均值和已知方差的贝叶斯估计
import torch
def inference_unknown_mean(X, prior_dist, sigma_known):
mu_mle = X.mean()
n = X.shape[0]
mu_0 = prior_dist.mean
sigma_0 = prior_dist.scale ①
mu_n = mu_mle / (1 + sigma_known**2 / (n * sigma_0**2)) +
mu_0 / (1 + n * sigma_0**2 / sigma_known**2) ②
sigma_n = math.sqrt(
(sigma_0**2 * sigma_known**2) /
(n*sigma_0**2+sigma_known**2)) ③
posterior_dist = torch.Normal(mu_n, sigma_n)
return posterior_dist
① 先验参数
② 后验均值,根据方程 13.4
③ 后验标准差,根据方程 13.4
13.4 小量和大量的训练数据,以及强和弱的先验
让我们考察方程 13.4 在 n = 0(无数据)和 n → ∞(大量数据)时的行为:

这与我们关于少量数据时后验主要由先验决定,而大量数据时后验主要由似然决定的观念相符。在大量数据的情况下,参数的方差为零(我们可以说我们有完全的确定性,即均值的最优值是数据的样本均值,也就是均值的 MLE 估计)。一般来说,随着训练数据的增加(即n的值增大),后验会越来越接近似然。这可以通过分析方程 13.4 来看到。这与我们的直觉相符,即少量数据时,我们试图用我们预先存在的(先验)关于参数值的信念来补偿。随着训练数据实例数量的增加,先验的影响减小,似然(它是数据的函数)开始占主导地位。
先验的方差低(即小的σ[0])本质上意味着我们对先验信念的不确定性低(记住,高斯熵/不确定性与其方差成正比)。这样的高置信度先验抵抗被数据淹没,被称为强先验。另一方面,大的σ[0]意味着对先验均值值的信心低。这是一个弱先验,容易被数据淹没。我们可以在方程 13.4 中均值的最终权衡表达式中看到这一点:在第二项的分母中我们有nσ[0]²/σ²。一般来说,随着n的增大,第二项消失,从而消除了先验μ[0]的影响,使得后验均值与 MLE 均值相一致。但是,σ[0]越小,需要更大的n来实现这一点,反之亦然。
13.5 共轭先验
在 13.3 节中,给定高斯似然,选择高斯先验使得后验也属于高斯家族。这大大简化了问题。如果先验来自另一个家族,那么后验——似然和先验的乘积——可能不属于简单或甚至已知的分布家族。
因此,具有高斯先验的高斯似然导致均值后验也是高斯。这样的先验被称为共轭。形式上,对于特定的似然家族,选择使得后验属于与先验相同家族的先验被称为共轭先验。例如,均值的正态分布(已知方差)与正态似然是共轭的。很快我们就会看到,对于正态似然,精度的伽马分布(方差的倒数)导致伽马后验。换句话说,精度的伽马先验与正态似然是共轭的。在多元情况下,我们用 Wishart 分布作为共轭先验。
13.6 完全贝叶斯参数估计:高斯,未知精度,已知均值
在第 13.3 节中,我们讨论了在假设我们以某种方式知道方差 σ 并且只想估计均值 μ 的条件下,完全贝叶斯参数估计。现在我们考察均值已知但方差未知且表示为随机变量的情况。如果我们使用精度 λ 而不是方差 σ,计算将变得简单。它们通过表达式 1/σ² 相关联。因此,我们有一个数据集 X,它被假定为从具有常数均值 μ 的高斯分布中抽取的,而精度 λ 是具有伽马分布的随机变量。因此,数据的概率密度函数是 p(x|μ, λ) = 𝒩(x; μ, 1/√λ)。
我们用伽马分布来建模精度的先验随机变量。似然函数是高斯分布,由于高斯和伽马的乘积仍然是伽马(由于伽马分布的共轭先验性质),因此得到的结果后验是伽马分布。后验的伽马函数参数可以通过系数比较推导出来。后验的最大值是我们对参数的估计。
伽马分布
伽马分布在附录中介绍;如有必要,请先阅读。这里我们陈述相关的性质。具有伽马分布的随机变量 λ 的概率密度函数是具有两个参数 α,β 的函数:

方程式 13.5
伽马分布的最大值
为了最大化伽马概率密度函数 p(λ|X) = λ^((α[n] − 1))e^(−β[n]λ) 对于随机变量 λ,我们对它求导并令其等于零:

13.6.1 估计精度参数
让我们回到当均值已知时对精度参数的完全贝叶斯估计。我们用高斯分布来建模数据分布:p(x|μ, λ) = 𝒩(x; μ, 1/√λ)(我们已经用精度 λ 表达了这种高斯分布,而 λ 与方差 σ 相关,λ = 1/σ ²)。训练数据集是 X = {x^((1)), x^((2)),⋯, x^((n))},其整体似然函数是

我们用参数 α[0],β[0] 的伽马分布来建模精度的先验:
p(λ) = γ(λ;α[0], β[0]) ∝ λ((*α*[0]−1))*e*(−β[0]λ)
我们知道相应的后验——高斯和伽马分布的乘积——是伽马分布(由于伽马分布的共轭先验性质)。让我们将后验表示为
p(λ|X) = γ(λ;α[n], β[n])
从贝叶斯定理,

代入

并比较 λ 和 e 的幂,我们得到

方程式 13.6
注意,与之前一样,在 n 较低时,后验被先验主导,但随着 n 的增加,后验越来越接近似然估计。换句话说,在没有足够数据的情况下,我们让我们的信念主导估计;但如果数据可用,估计则由基于数据的实体似然主导。
给定数据集 X,参数 λ 的 MAP 点估计是通过最大化这个后验分布 p(λ|X) = γ(λ;α[n], β[n]),得到 λ[MAP] = 1/σ[MAP]² = (α[n]–1/β[n])。 (附录 A.5 展示了如何获得伽马分布的最大值。) 因此,我们对训练数据分布的估计是 p(x) = 𝒩(x; μ, σ[MAP]),其中 1/σ[MAP]² = (α[n]–1/β[n])。
给予大量数据,未知精度/方差的 MAP 估计与 MLE 估计相同(证明概要如下):

另一方面,在没有数据的情况下,未知精度/方差的 MAP 估计完全由先验决定(证明概要如下):

注意:具有已知均值和未知方差的贝叶斯估计的完整功能代码,可通过 Jupyter Notebook 执行,可在 mng.bz /2nZ9 找到。
列表 13.2 PyTorch- 具有未知方差和已知均值的贝叶斯估计
import torch
def inference_unknown_variance(X, prior_dist):
sigma_mle = torch.std(X)
n = X.shape[0]
alpha_0 = prior_dist.concentration
beta_0 = prior_dist.rate ①
alpha_n = n / 2 + alpha_0
beta_n = n / 2 * sigma_mle ** 2 + beta_0 ②
posterior_dist = torch.Gamma(alpha_n, beta_n)
return posterior_dist
① 先验参数
② 后验参数
13.7 完全贝叶斯参数估计:高斯,未知均值,未知精度
在第 13.3 节中,我们看到了如果方差已知,均值的共轭先验是高斯分布(也称为正态分布)。同样,当均值已知时,精度的共轭先验是伽马分布。如果两者都未知,我们最终得到正态-伽马分布。
13.7.1 正态-伽马分布
正态-伽马是两个随机变量,例如 μ 和 λ 的概率分布,其密度由四个参数 μ′,*λ*′,α^′,和 β^′ 定义,如下所示:

尽管看起来很复杂,但记住它的简单方法是它是正态分布和伽马分布的乘积。
正态-伽马分布达到最大值在

13.7.2 估计均值和精度参数
与之前一样,我们用高斯分布建模数据分布:p(x|μ , λ) = 𝒩(x; μ, 1/√λ),我们用精度 λ 表达了这个高斯分布,它与方差 σ 相关,λ = 1/σ²)。训练数据集是 X = {x^((1)), x^((2)),⋯, x^((n))},其整体似然为

我们将先验的均值建模为均值 μ[0] 和精度 λ[0]λ 的高斯分布:

我们将先验的精度建模为参数 α[0],β[0] 的伽马分布:

均值和精度参数的整体先验概率是两者的乘积,即参数 μ⁰, λ⁰, α⁰, β⁰ 的正态-伽马分布:

均值和精度参数的后验概率是似然和先验的联合(即乘积)。因此,我们知道它又是另一个正态-伽马分布(由于正态-伽马分布的共轭先验性质):

使用贝叶斯定理,

代入

通过比较系数,可以确定后验分布的未知参数:

方程 13.7
为了获得完全的贝叶斯参数估计,我们取正态-伽马后验概率密度函数的最大值:

因此,数据的最终概率密度函数为

注意:完全功能的贝叶斯估计代码,用于未知均值和未知方差,可通过 Jupyter Notebook 执行,可在mng.bz/1oQy找到。
列表 13.3 PyTorch 代码:正态-伽马分布
import torch
class NormalGamma(): ①
def __init__(self, mu_, lambda_, alpha_, beta_):
self.mu_ = mu_
self.lambda_ = lambda_
self.alpha_ = alpha_
self.beta_ = beta_
@property
def mean(self):
return self.mu_, self.alpha_/ self.beta_
@property
def mode(self):
return self.mu_, (self.alpha_-0.5)/ self.beta_
① 由于 PyTorch 没有实现正态-伽马分布,我们实现了一个简化的版本。
列表 13.4 PyTorch:具有未知均值和未知方差的贝叶斯估计
import torch
def inference_unknown_mean_variance(X, prior_dist):
mu_mle = X.mean()
sigma_mle = X.std()
n = X.shape[0]
mu_0 = prior_dist.mu_
lambda_0 = prior_dist.lambda_
alpha_0 = prior_dist.alpha_
beta_0 = prior_dist.beta_ ①
mu_n = (n * mu_mle + mu_0 * lambda_0) / (lambda_0 + n)
lambda_n = n + lambda_0
alpha_n = n / 2 + alpha_0
beta_n = n / 2 * sigma_mle ** 2 + beta_0 +
0.5* n * lambda_0/ (n + lambda_0) *
(mu_mle - mu_0) ** 2 ②
posterior_dist = NormalGamma(mu_n, lambda_n, alpha_n, beta_n)
return posterior_dist
① 先验参数
② 后验参数
13.8 示例:完全贝叶斯推理
让我们回顾一下第 6.8 节中讨论的问题,即根据身高预测 Statsville 居民是否为女性。为此,我们已经收集了 Statsville 成年女性居民的身高样本。不幸的是,由于不可预见的情况,我们收集的样本非常小。凭借我们对贝叶斯推理的了解,我们不想因此放弃尝试构建模型。根据物理考虑,我们可以假设身高的分布是高斯分布。我们的目标是估计这个高斯分布的参数 (μ, σ)。
注意:此例的完全功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/Pn4g找到。
首先,让我们通过从具有 μ = 152 和 σ = 8 的高斯分布中采样五个点来创建数据集。在现实场景中,我们不知道真实分布的均值和标准差。但为了这个例子,让我们假设平均身高为 152 厘米,标准差为 8 厘米。我们的数据矩阵,X,如下所示:

13.8.1 最大似然估计
如果我们依赖 MLE,我们的方法将是计算数据集的均值和标准差,并使用这个正态分布作为我们的模型。我们使用以下方程来计算正态分布的均值和标准差:

均值,μ,计算结果为 149.68,标准差,σ,为 11.52。这与真实的均值(152)和标准差(8)有显著差异,因为数据点的数量很少。在这种情况下,最大似然估计并不可靠。
13.8.2 贝叶斯推理
我们能否比 MLE 做得更好?一种潜在的方法是使用具有良好先验的贝叶斯推理。我们如何选择一个好的先验?好吧,让我们假设我们知道从一项旧调查中,邻镇 Neighborville 成年女性居民的身高平均值和标准差分别为 150 厘米和 9 厘米。此外,我们没有理由相信 Statsville 的身高分布与邻镇有显著差异。因此,我们可以使用这些信息来“初始化”我们的先验。先验分布编码了我们关于参数值的信念。
由于我们处理的是未知均值和未知方差,我们将先验模型设为正态伽马分布:
p(θ) = 𝒩γ(μ[0], λ[0], α[0], β[0])
我们选择p(θ),使得μ[0] = 150,λ[0] = 100,α[0] = 10.5,和β[0] = 810。这意味着
p(θ) = 𝒩γ(150, 100, 10.5, 810)
p(θ|X)是一个正态伽马分布,其参数可以使用 13.7 节中描述的方程计算。下面展示了计算后验的 PyTorch 代码。
列表 13.5 PyTorch- 使用贝叶斯推理计算后验概率
prior_dist = NormalGamma(150, 100, 10.5, 810) ①
posterior_dist = inference_unknown_mean_variance(X, prior_dist) ②
map_mu, map_precision = posterior_dist.mode ③
map_std = math.sqrt(1 / map_precision) ④
map_dist = Normal(map_mu, map_std) ⑤
① 初始化正态伽马分布
② 计算后验概率
③ 分布的模指的是具有最高概率密度的参数值。
④ 使用精度计算标准差
⑤ map_mu 和 map_std 指的是最大化后验分布的参数值。
使用贝叶斯推理获得的μ和σ的 MAP 估计值分别为 149.98 和 9.56,分别优于 MLE 估计值 149.68 和 11.52(真实的μ和σ分别为 152 和 9)。
现在我们已经估计了参数,我们可以使用公式找出样本落在某个范围内的概率

有关此内容的详细信息,请参阅 6.8 节。
13.9 完全贝叶斯参数估计:多元高斯分布,未知均值,已知精度
这是多元情况;一元版本在 13.3 节中讨论。计算方法与一元情况类似。
我们将数据分布建模为高斯分布 p(
|
, Λ) = 𝒩(
;
, Λ^(−1)),其中我们用精度矩阵 Λ 而不是协方差矩阵 Σ 来表示高斯分布,其中 Λ = Σ^(−1)。训练数据集是 X ≡ {
^((1)),
^((2)), ⋯,
^((i)), ⋯,
^((n))},其整体似然为

我们将均值的前验建模为高斯分布:

后验概率密度是一个高斯分布(因为它是由两个高斯分布的乘积)。让我们用以下符号表示它

使用贝叶斯定理,

让我们检查最右边表达式的指数。

我们忽略了最后一个常数项,因为它们将被合并到整体比例常数中。因此

比较系数:

后验概率在
[n] 处最大化。因此
[MAP] =
[n]
是多元高斯数据分布均值参数的 MAP 估计:p(
) = 𝒩(
;
[n], Λ^(-1))。
注意以下内容:

在数据量较大的情况下,估计的均值参数
[MAP] =
[n] 接近 MLE
[MLE] =
。
在数据量较少的情况下,估计的后验均值参数
[MAP] =
[n] 接近先验
[0]。
注意:完全功能的多元贝叶斯推断高斯似然均值(已知精度)的代码,可通过 Jupyter Notebook 执行,可在 mng.bz/J2AP 找到。
列表 13.6 PyTorch- 多元贝叶斯推断,未知均值
def inference_known_precision(X, prior_dist, precision_known):
mu_mle = X.mean(dim=0)
n = X.shape[0]
mu_0 = prior_dist.mean
precision_0 = prior_dist.precision_matrix ①
precision_n = n * precision_known + precision_0 ②
mu_n = torch.matmul(
n * torch.matmul(
mu_mle.unsqueeze(0), precision_known) + torch.matmul(
mu_0.unsqueeze(0), precision_0),
torch.inverse(precision_n)
)
posterior_dist = MultivariateNormal(
mu_n, precision_matrix=precision_n)
return posterior_dist
① 先验参数
② 后验参数
13.10 完全贝叶斯参数估计:多元,未知精度,已知均值
在第 13.6 节中,我们讨论了单变量情况,现在我们来考察多元情况。对于单变量情况,我们必须查看伽马分布。对于多元情况,我们必须查看威沙特分布。
13.10.1 威沙特分布
假设我们有一个高斯随机数据向量
,其概率密度函数为 𝒩(
;
, Σ). 再次,我们使用 精度矩阵 Λ 而不是协方差矩阵 Σ,其中 Λ = Σ^(−1). 考虑我们已知均值
但想估计精度 Λ 的情况。我们如何表达先验?请注意,p(Λ) 是一个 矩阵 的概率密度函数。到目前为止,我们遇到了标量和向量的概率分布,而不是矩阵。此外,这也不是一个任意的矩阵。我们谈论的是一个 对称、非负定 的矩阵(所有协方差和精度矩阵都属于这一类)。因此,我们寻找的分布不是所有 d² 矩阵元素的联合分布 d 表示数据的维度:即所有
和
向量都是 d × 1)。而是矩阵中 (d(d + 1))/2 个元素的联合分布——对角线及其上或下的元素(对角线上的元素上下相同,因为矩阵是对称的)。
这样的矩阵空间被称为 Wishart 系统集。一个大小为 d × d 的随机精度矩阵 Λ 的概率可以表示为 Wishart 分布。这个分布有两个参数:
-
ν,一个满足 ν > d − 1 的标量
-
W,一个 d × d 的对称非负定矩阵
概率密度函数是

其中
-
𝒲 表示 Wishart。
-
|W|, |Λ| 分别表示矩阵 W 和 Λ 的行列式。
-
Tr(A) 表示矩阵 A 的迹(对角线元素之和)。
-
Γ 表示多元伽马函数

Wishart 是伽马分布的多变量版本。其期望值是
𝔼(Λ) = νW
其最大值出现在
Λ = (ν − d − 1)W 对于 ν ≥ d + 1
13.10.2 估计精度
如前所述,我们将数据分布建模为高斯分布 p(
|
, Λ) = 𝒩(
;
, Λ^(−1)),其中我们用 精度矩阵 Λ 而不是协方差矩阵 Σ 来表示高斯分布,其中 Λ = Σ^(−1)。
训练数据集是 X ≡ {
^((1)),
^((2)),⋯,
^((i)),⋯,
^((n))},其整体似然函数是

我们将精度矩阵的先验概率建模为 Wishart 分布。因此,

后验分布是另一个 Wishart 分布(归功于 Wishart 共轭先验性质):

使用贝叶斯定理对训练数据集X进行估计,

让我们研究一对有用的简单引理。

其中Tr指的是矩阵的迹(对角元素之和)。
-
第一个引理几乎是显而易见的——二次型
^TA
是一个标量,所以当然它与它的迹是相同的。 -
第二个引理直接来自迹的矩阵属性:Tr(BC) = Tr(CB).
使用引理,似然项的指数为

其中

因此,后验密度为

由于Tr(A) + Tr(B) = Tr(A + B),

通过比较系数,我们确定后验分布的未知参数:

其中

后验密度函数的最大值,𝒲(Λ;ν[n], W[n]),给出了数据分布精度参数的估计:Λ = (ν[n] − d − 1)W[n] 对于 ν[n] ≥ d + 1,即,

摘要
-
一个能够模拟底层数据分布的生成模型可能比一个黑盒判别模型更强大。一旦我们选择了一个模型族,我们需要估计模型参数,θ。我们可以使用贝叶斯定理从训练数据X中估计θ的最佳值。
-
后验分布p(θ|X)是似然率p(X|θ)和先验p(θ)的乘积的函数。先验表达了我们对参数值的信念。对于小数据集,后验主要由先验决定,对于大数据集,后验主要由似然率决定。通过一个好的先验分布注入信念,在训练数据非常少的情况下可能是有帮助的。
-
最大似然估计仅依赖于数据,与最大后验估计(MAP)相对,后者既依赖于数据也依赖于先验信息。
-
当方差已知时,我们可以使用贝叶斯估计高斯似然率的均值。当似然率是高斯分布p(X) ∼ N(μ, σ), 我们将先验模型建模为正态分布p(μ) ∼ N(μ[0], σ[0])。后验分布也是一个正态分布p(μ|X) ∼ N(μ[n], σ[n]),其中
和
。我们还可以使用估计的参数对新数据实例进行预测。 -
弱先验意味着我们对先验信念的不确定性程度高/信心低,并且很容易被数据所淹没。相比之下,强先验意味着我们对先验信念的不确定性程度低/信心高,并且能够抵抗数据过载。
-
对于特定的似然族,导致后验属于与先验相同族的先验选择被称为共轭先验。
-
伽马函数是
,伽马分布是
。伽马分布随 α 和 β 的不同值而变化。 -
在已知均值的情况下,对高斯似然精度的贝叶斯估计中,精度 λ 是方差的倒数。我们可以将先验模型化为一个伽马分布
。后验分布也是一个伽马分布,
,其中
和
。 -
在对高斯似然的均值和精度进行贝叶斯估计的情况下,我们将先验模型化为一个正态-伽马分布。后验也是一个正态-伽马分布。后验分布可以用来预测新的数据实例。
-
高斯似然均值的多变量贝叶斯推断被称为精度。我们可以将先验模型化为一个多元正态分布;后验也是一个多元正态分布。
-
Wishart 分布是伽马分布的多变量版本。在已知均值的情况下,使用多元贝叶斯推断高斯似然精度的精度,我们可以将先验模型化为一个 Wishart 分布。相应地,后验也是一个 Wishart 分布。
第十四章:潜空间和生成建模、自编码器以及变分自编码器
本章涵盖
-
使用潜向量表示输入
-
潜空间的几何视图、平滑性、连续性和正则化
-
主成分分析(PCA)和线性潜空间
-
自编码器和重建损失
-
变分自编码器(VAEs)和正则化潜空间
将输入向量映射到转换空间在机器学习中通常是有益的。转换向量被称为潜向量——潜的,因为它不是直接可观察的——而输入是基础的观测向量。潜向量(也称为嵌入)是输入向量的简化表示,其中只包含有助于实现最终目标(例如,估计输入的概率)的特征。
(属于特定类别)被保留,其他特征被遗忘。通常,潜表示的维度少于输入:即,将输入编码到潜向量中会导致维度降低。
从输入到潜空间(反之亦然)的映射通常是通过学习得到的——我们训练一个机器,例如神经网络,来完成这个任务。潜向量需要尽可能忠实地表示分配给它的输入维度内的输入。因此,神经网络被激励去最小化由转换引起的信息损失。后来,我们看到在自编码器中,这是通过从潜向量重建输入并尝试最小化实际输入和重建输入之间的差异来实现的。然而,由于维度的减少,网络没有保留输入中所有内容的奢侈。它必须学习对最终目标至关重要的内容,并仅保留那些内容。因此,嵌入是输入的紧凑表示,它被精简以达到最终目标。
14.1 潜空间几何视图
考虑所有高度为H、宽度为W的数字图像空间,每个像素代表一个 24 位 RGB 颜色值。这是一个巨大的空间,有(2²⁴)^(HW)个点。这个空间中的每一个可能的 RGB × H × W图像都是一个点。但如果一个图像是自然图像,相邻的点往往具有相似的颜色。这意味着对应于自然图像的点相关:它们不是均匀分布在可能图像的空间中。此外,如果图像具有共同属性(例如,它们都是长颈鹿),相应的点在(2²⁴)^(HW)大小的输入空间中形成簇。在随机论中,具有共同属性的自然图像在可能图像空间中的概率分布高度非均匀(低熵)。
图 14.1a 展示了具有某些共同属性的点围绕平面流形聚集。类似地,图 14.1b 展示了具有某些共同属性的点围绕曲面流形聚集。这些点具有共同属性。目前,我们并不关心这个属性是什么,或者流形是平面还是曲面。我们只关心这些感兴趣的点分布在一个流形周围。流形捕捉了这种共同属性的精髓,无论它是什么。如果共同属性是,比如说,图像中存在长颈鹿,那么流形就捕捉了长颈鹿特性:流形上或附近的点都对应着有长颈鹿的图像。如果我们沿着流形移动,我们会遇到各种风格的长颈鹿照片。如果我们远离流形——即沿着垂直于流形的方向移动一段距离——那么点代表有长颈鹿照片的概率就低。

(a) 平面潜在子空间

(b) 曲面潜在子空间
图 14.1 展示了两个潜在子空间的例子,分别对应平面和曲面的流形。实线表示潜在向量,虚线代表投影到潜在子空间所丢失的信息。
给定由感兴趣点的样本(如许多长颈鹿照片)组成的训练数据,我们可以训练一个神经网络来学习这个流形——这是使所有训练数据点到流形的平均距离最小化的最优流形。然后,在推理时间,给定一个任意的输入点,我们可以估计它到流形的距离,这给了我们该输入满足流形所表示属性的概率。
因此,输入向量可以被分解为一个在流形内的分量(图 14.1 中的实线)和一个垂直于流形的分量(图 14.1 中的虚线)。潜在空间建模有效地消除了垂直分量,并保留了在流形内的分量作为潜在向量(也称为嵌入)。等价地,我们是在将输入向量投影到流形上。这是潜在空间建模的核心思想——我们学习一个表示感兴趣属性的流形,并通过潜在向量表示所有输入,这是输入点到该流形上的投影。潜在向量是输入的更紧凑表示,其中只保留了与感兴趣属性相关的信息。
潜在空间建模概述
在潜在空间建模中,我们训练一个神经网络来表示一个流形,输入点满足感兴趣属性分布在这个流形周围。感兴趣的属性可能是属于特定类别,例如包含长颈鹿的图像。因此,学习到的流形是一组满足该属性的点。输入点被投影到这个流形上,以获得输入的潜在向量表示(也称为嵌入)。这相当于丢弃了与流形正交的输入向量分量。被消除的分量与流形正交,因此与感兴趣属性无关(可能代表图像的背景像素),所以投影引起的信息损失不会造成伤害。我们创建了一个更少噪声、更紧凑的输入表示,专注于我们关心的事情。
训练数据由一组采样数据输入组成,所有这些输入都满足感兴趣的属性。系统本质上学习的是流形,该流形位于最优位置以最小化其与所有训练数据点的平均距离。在推理过程中,给定一个任意输入点,其与流形的距离是该输入满足感兴趣属性的概率的指示器。
一个微妙之处在于,潜在向量是原始点位置向量的流形内分量。通过切换到潜在向量表示,我们失去了点在原始高维输入空间中的位置。我们可以通过提供丢失的正交分量的流形位置来回到高维空间,但这样做并不能恢复原始点:它只能恢复原始点在子空间上的投影。我们用流形的位置(一个聚合实体)替换了单个正交分量,但并不能恢复原始点。在投影过程中,一些信息不可避免地丢失了。
潜在空间表示的一个特殊情况是主成分分析(PCA),在第 4.4 节中介绍(第 14.4 节提供了 PCA 的上下文回顾)。它将输入点投影到最优的平面潜在子空间(如图 14.1a)。但除了某些幸运的特殊情况外,最佳的潜在子空间不是一个超平面。它是一个复杂的曲面(见图 14.1b)。神经网络,如自动编码器,可以学习这种非线性投影。
14.2 生成分类器
在推理过程中,我们在前几章中遇到的监督分类器通常会输出输入所属的类别,可能还会附带一个边界框。这种行为有点像黑盒。我们不知道分类器是否很好地掌握了空间,除非通过量化的最终结果。这样的分类器被称为判别分类器。另一方面,潜在空间模型将任意输入点映射到属于感兴趣类别的概率。这样的模型被称为生成模型,它们具有一些理想的特性:

(a) 一个好的判别分类器——平滑的决策边界

(b) 一个不好的判别分类器——不规则的决策边界

(c) 生成模型——没有决策边界(热图表示概率密度)
图 14.2 实心圆表示训练数据点(所有属于感兴趣类别)。虚线曲线表示将感兴趣类别与非感兴趣类别分开的决策边界。在生成模型中,没有决策边界。空间中的每个点都与属于感兴趣类别的概率相关联(如图 14.2c 中的热图所示)
备注:我们可以通过在概率上设置阈值,总是从生成分类器创建一个判别分类器。
-
更平滑、更密集的流形——判别模型学习在输入空间中分离感兴趣数据点和非感兴趣数据点的决策边界。另一方面,生成模型试图使用平滑的概率密度函数来模拟输入空间中感兴趣数据点的分布。因此,生成模型不能学习到一个非常不规则形状的函数,该函数过度拟合训练数据。这如图 14.2 所示,而判别模型可能会收敛到一个与训练数据弯曲和角落过于接近的流形(过度拟合),如图~\ref{fig-discriminative-bad-model}所示。当我们拥有的训练数据较少时,这种判别分类器和生成分类器之间的差异变得特别显著。我们可以通过在概率上设置阈值,总是从生成分类器创建一个判别分类器。
-
额外洞察—生成模型对模型的内部运作提供了更多的洞察。考虑一个识别马匹的模型。假设我们向模型提供一些马匹图像,并且它将它们识别为马(好的)。然后我们向模型提供一些斑马图像,它也将它们识别为马(坏的)。我们是否有一个将所有东西都称为马的无效模型?如果它是一个判别模型,我们必须用完全不同的图像(比如鸟类图像)来测试它以获得答案。但是如果我们有一个生成模型,它说真实马匹图像的概率是,比如说,0.9 以上,而斑马图像的概率大约是 0.7。我们开始看到模型的行为是合理的,并且确实意识到斑马比真正的马“马性”要少。
-
新类实例—生成模型学习属于该类的输入点的分布。与学习分布相关的一个优点是,我们可以采样分布以生成该类的新成员(例如,生成人工马匹图像)。这导致了“生成”模型的名字。如果我们用莎士比亚的作品训练一个生成模型,它将发出类似莎士比亚的文本片段。信不信由你,这已经尝试过并且取得了一些成功。
14.3 潜在空间建模的好处和应用
让我们简要回顾一下为什么我们想要进行潜在空间建模:
-
生成模型通常基于潜在空间模型—第 14.2 节中概述的生成模型的所有好处也适用于潜在空间建模。
-
关注重要事项—不贡献于最终目标的冗余信息被消除,系统专注于真正有区分性的信息。为了可视化这一点,想象一个由站在相同背景前的人的警察肖像组成的数据集。通常用于识别人的潜在空间建模会从表示中消除共同背景,并专注于照片的主题(人)。
-
数据简化表示—潜在向量是输入向量的更紧凑表示(减少了维度,因此更小),没有丢失任何有意义的信息。
-
噪声消除—潜在空间建模消除了数据中与潜在子空间正交的低方差成分。这部分数据大多对感兴趣的问题没有帮助,因此是噪声。
-
转换到对最终目标更友好的流形—我们之前已经看到过这个概念,但在这里让我们看看一个有趣的简单例子。考虑一组在笛卡尔坐标系中的二维点(x,y)。假设我们想要将点分类到两个集合中:那些位于圆内x² + y² = a²的点以及那些位于圆外的点。在原始的笛卡尔空间中,决策边界不是线性的(它是圆形的)。但如果我们将笛卡尔输入点转换到极坐标的潜在空间中——也就是说,每个(x,y)被映射到(r,θ),使得x = rcos(θ),y = rsin(θ)——圆在潜在空间中变成了线r = a。在潜在空间中一个简单的线性分类器r = a可以实现所需的分类。
潜在空间建模的一些应用如下:
-
生成人工图像或文本(如生成模型中的解释所述)。
-
输入之间的相似度估计—如果我们将输入映射到潜在向量,我们可以通过计算潜在向量之间的欧几里得距离来评估输入之间的相似度。为什么这比计算输入向量之间的欧几里得距离更好?假设我们正在构建一个推荐引擎,该引擎建议其他与潜在买家当前浏览的服装项目“相似”的服装项目。我们希望检索看起来相似但并非完全相同的其他服装项目。但相似性是一个主观概念,不能通过输入像素颜色的相似性来衡量。考虑一件在白色底色上有黑色竖条纹的衬衫。如果我们交换条纹颜色和底色颜色,我们得到一件在黑色底色上有白色竖条纹的衬衫。如果我们进行像素到像素的颜色匹配,这些衬衫非常不同,但人类认为它们是相似的。对于这个问题,我们必须训练潜在空间模型,创建神经网络,使得人类感知为相似的图像映射到潜在空间中彼此靠近的点。例如,白色底色上的黑色条纹衬衫和黑色底色上的白色条纹衬衫应该在潜在空间中映射到彼此靠近的潜在向量,尽管它们在输入空间中相距甚远。
-
图像或其他数据压缩—潜在向量用一个尽可能忠实于原始向量的较小维度的向量来近似数据。因此,潜在向量是输入的损失压缩表示。
-
降噪—潜在向量消除了输入信息中的非有意义部分,即噪声。
注意:本章的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/6XG6找到。
14.4 线性潜在空间流形和 PCA
PCA(我们在第 4.4 节中讨论过)将输入数据投影到线性超平面流形上。重新审视这个主题将为本章的其余部分设定正确的背景。考虑一组围绕 X[0] = X[2] 平面紧密聚集的 3D 输入数据点,如图 14.3 所示。

(a) 原始 3D 数据

(b) 通过将第三个主值设为零获得的低维二维表示

(c) 原始数据的主向量。第三个主向量垂直于 X[0] = X[2] 平面;其他两个在平面内。
图 14.3 图 14.3a 中的原始 3D 数据显示高度相关性:点围绕 X[0] = X[2] 平面聚集。第一个主成分对应于最大方差的方向。最后一个(第三个)主成分被简化为二维潜在向量。
注意:我们用 X[0]、X[1]、X[2] 表示连续的轴(维度),而不是更传统的 X、Y、Z,以便于扩展到更高维度。
使用 PCA,我们可以识别出数据在某些维度上的变化很小。当我们进行 PCA 时,我们得到主值和主向量对。最大的主值对应于数据中最大方差的方向。对应的主向量给出该方向,而该主值表示该方向上方差的大小。下一个主值、主向量对是具有下一个最高方差的正交方向,依此类推。例如,在图 14.3 中,对应于较大两个主值的主向量位于 X[0] = X[2] 平面上,而最小的主值对应于垂直于平面的向量。第三个主值显著小于其他值。这告诉我们该轴上的方差很低,并且沿该轴的分量可以相对少地丢失信息:即低重建损失。小主值轴上的变化很可能是噪声,因此消除它们可以清理数据。在图 14.3 中,这实际上将数据投影到 X[0] = X[2] 平面上。
维度降低
PCA 实质上是将输入投影到训练数据的最佳拟合平面。假设所有训练数据点都是具有共同属性的样本,这个平面代表了这种共同属性。通过投影,我们消除了这种共同属性,只保留了数据的判别方面。消除的信息被近似地保存在平面的参数中,并在重建(也称为解码)过程中提供,以将我们映射回与输入相同的维度(但不是完全相同的点)。这是 PCA 降维的本质。
下面是 PCA 基于的降维步骤。这在第 4.5 节中已详细描述,并附有证明;这里我们回顾主要步骤而不进行证明。
注意:这种处理与第 4.5 节中的处理类似,但并不完全相同。在这里,我们将变量 m 和 n 交换,以与我们的使用习惯一致,即用 n 表示数据实例的数量。我们还采用了稍微不同的 SVD 表达方式。
-
将数据表示为矩阵 X,其中每一行是一个单独的数据实例。行数 n 是数据集的大小。列数 d 是数据的原始(输入)维度。因此 X 是一个 n × d 矩阵。
-
计算均值数据向量
![]()
其中
^((i)) 对于 i = 1 到 i = n 表示训练数据向量实例(这些实例构成了矩阵 X 的行)。 -
将坐标系的原点平移到均值处,通过从每个数据向量中减去均值向量来实现:
^((i)) =
^((i)) –
对于所有 i数据矩阵 X 现在的行是减去均值的数据实例。
-
矩阵 X^TX(其中 X 是减去均值的数据矩阵)是协方差矩阵(如第 5.7.2 节中详细讨论的)。矩阵 X^TX 的特征值和特征向量对被称为主值和主向量(统称为主成分)。由于 X^TX 是一个 d × d 矩阵,因此有 d 个标量特征值和 d 个特征向量,每个特征向量的维度为 d × 1。让我们将主成分表示为 (λ[1],
[1]),(λ[2],
[2]),⋯,(λ[dm],
[d])。 -
如果有必要,我们可以假设 λ[1] ≥ λ[2] ≥ ⋯ ≥ λ[d]。如果需要,我们可以通过重新编号主成分来实现这一点)。第一个主成分对应于数据中最大方差的方向(带有几何直觉的证明可以在第 5.7.2 节找到)。对应的主值给出了实际的方差。下一个主值对应于第二高的方差(在第一个主方向正交的方向中),以此类推。对于每个成分,主值给出了实际的方差,而主向量给出了方向。
-
考虑主向量的矩阵:
V = ![[1]
[2] …
[d]]如果我们希望数据是一个具有 m 维度的空间,并且信息损失最小,我们应该丢弃 V 的最后 m 个向量。这消除了 m 个最小方差维度。从 V 中丢弃最后 m 个向量得到一个矩阵
V[d–m] = ![[1]
[2] …
[d–m]]注意,获取 V 矩阵的最佳方式是对减去均值的 X 执行奇异值分解(见第 4.5 节)。
-
预乘截断的主向量矩阵 V[d−m] 与原始数据矩阵 X 相乘,将数据投影到对应于前 d − m 个主成分的空间。因此,要从 d-维数据创建 d − m-维线性编码的潜在向量,
X[d−m] = XV[d−m]
X[d−m] 是降维后的数据集。其维度是 n × (d−m).
可以证明:
XV[d−m] = UΣ[d−m]
其中 U 来自奇异值分解(见第 4.5 节),而 Σ[d−m] 是奇异值分解中Σ对角矩阵的截断版本,其最小的 m 个元素被截掉。这为基于 PCA 的降维提供了一种替代方法。
-
我们如何重建?换句话说,解码器是什么?好吧,为了重建,我们需要保存原始的主向量:即 V 矩阵。如果我们有那个,我们可以在 X[d−m] 的每一行的右侧引入 m 个零,使其再次成为 n × d 矩阵。然后我们后乘以 V^T,这会将坐标系从以主向量为轴的坐标系旋转回以原始输入轴为轴的坐标系。最后,我们将均值
添加到每一行,将原点移回到其原始位置,从而得到重建的数据矩阵 X̃。重建损失是 ||X − X̃||²。请注意,实际上,X̃ 是 UΣV^T,其中Σ的最后一个 m 个对角元素被设置为零。 -
重构的数据 X̃ 与原始数据并不相同。我们在降维过程中丢失的信息(尽管是微小的)是永久丢失的。尽管如此,这种删除信息的原则性方法确保了重建损失在某种意义上是最小的,至少在所有与 X 线性相关的 X̃ 中。
14.4.1 使用 PCA 进行降维的 PyTorch 代码
现在,让我们在 PyTorch 中实现降维。设 X 为表示围绕 X[0] = X[2] 平面聚类的数据矩阵。X 的形状为 [1000,3],其中 X 的每一行代表一个三维数据点。以下列表展示了如何以最小信息损失将 X 投影到低维空间,同时也展示了如何从低维表示中重建原始数据点。请注意,由于我们在降维过程中丢失了信息(尽管是微小的),重建是近似的。
注意:使用 PCA 进行降维的完整功能代码,可通过 Jupyter Notebook 执行,可在mng.bz/7yJg找到。
列表 14.1 PyTorch- PCA 回顾
import torch
X = get_data() ①
X_mean = X.mean(axis=0) ②
X = X - X_mean ③
U, S, Vh = torch.linalg.svd(X, full_matrices=False) ④
V = Vh.T ⑤
V_trimmed = V[:, 0: 2] ⑥
X_proj = torch.matmul(X, V_trimmed) ⑦
X_proj = torch.cat([X_proj, ⑧
torch.zeros((X_proj.shape[0], 1))], axis=1)
X_recon = torch.matmul(X_proj, Vh) ⑨
X_recon = X_recon + X_mean ⑩
① 数据矩阵的形状为(1000,3)
② 存储均值,以便我们稍后重建原始数据点
③ 在执行 SVD 之前减去均值
④ 执行 SVD
⑤ V 的列是主向量。
⑥ 移除最后一个主向量。这是沿着垂直于 X[0] = X[2] 平面的最小方差方向。
⑦ 将输入数据点投影到低维空间
⑧ 用零填充以形成一个 n × d 矩阵
⑨ 后乘以 V^T 以将数据投影回原始空间
⑩ 添加均值
14.5 自动编码器
自编码器是经过训练以生成对应于指定输入的潜在空间表示的神经网络系统。它们可以进行非线性投影,因此比 PCA 系统更强大,见图 14.4)。将输入向量映射到潜在向量的神经网络称为 编码器。我们还训练了一个称为 解码器 的神经网络,它将潜在向量映射回输入空间。解码器的输出是从潜在向量重构的输入。重构输入(即解码器的输出)永远不会与原始输入完全匹配——在编码过程中丢失了信息,无法恢复——但我们可以在系统的约束条件下尽量确保它们尽可能接近。重构损失是原始输入和重构输入之间差异的度量。编码器-解码器对从头到尾训练以最小化重构损失(可能还有其他损失)。这是一个 表示学习 的例子,其中我们学习用较小的潜在向量来表示输入向量,以尽可能接近在规定的尺寸预算内表示输入。潜在空间的预算大小是一个超参数。

图 14.4 具有弯曲潜在模式的 2 维数据分布。不可能找到一个直线或向量,使得所有点都靠近它。PCA 将表现不佳。
注意:超参数是一个 未学习 的神经网络参数。其值基于我们对系统的了解设置,并在训练过程中保持不变。
在自编码器中,期望的输出是隐含已知的:它是输入。因此,不需要人工标记来训练自编码器;它们是 无监督的。自编码器在图 14.5 中以示意图的形式展示。

图 14.5 自编码器的示意图。编码器将输入转换为一个潜在向量。解码器将潜在向量转换回重构输入。我们最小化重构损失——重构输入和原始输入之间的距离。
-
编码器接收一个输入
并将其映射到一个低维潜在向量
。一个用于图像输入的编码神经网络示例在列表 14.2 中展示。注意图像的高度和宽度在每次连续的卷积、ReLU 和最大池化层之后都会逐渐减小。 -
解码器是一个神经网络,它从潜在向量
生成重构图像
。列表 14.3 展示了一个解码器神经网络的示例。注意转置卷积以及图像的高度和宽度如何随着每个连续的转置卷积、批量归一化和 ReLU 序列而不断增加。转置卷积在第 10.5 节中讨论。解码器本质上记得——不是完全准确,但平均意义上——编码过程中丢弃的信息。等价地,它记得潜在空间流形在整体输入空间中的位置。将这一点加回到潜在空间表示中,使我们回到与输入向量相同的维度,但不是相同的输入点。 -
系统最小化编码(重构损失)的信息损失。我们确保对于每个输入,编码器产生的对应潜在向量可以通过解码器映射回一个重构值,该值尽可能接近输入。等价地,每个潜在向量是输入的忠实表示,输入和潜在向量之间存在 1:1 的映射。
-
编码器和解码器不必是对称的。
数学上,

端到端系统被训练以最小化损失 ℒ。
注意:完全功能的自动编码器代码,可通过 Jupyter Notebook 执行,可在 mng.bz/mOzM 找到。
列表 14.2 PyTorch- 自动编码器编码器
from torch import nn
nz = 10
input_image_size = (1, 32, 32) ①
conv_encoder = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ②
nn.Conv2d(32, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ③
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ④
nn.Flatten() ⑤
)
fc = nn.Linear(4096, nz) ⑥
① 以 (c, h, w) 格式输入图像大小
② 减少到 (32, 16, 16)-大小的张量
③ 减少到 (128, 8, 8)-大小的张量
④ 减少到 (256, 4, 4)-大小的张量
⑤ 展平为 4096 大小的张量
⑥ 将 4096 大小的张量减少到 nz 大小的张量
列表 14.3 PyTorch- 自动编码器解码器
from torch import nn
decoder = nn.Sequential(
nn.ConvTranspose2d(self.nz, out_channels=256,
kernel_size=4, stride=1,
padding=0, bias=False), ①
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.ConvTranspose2d(256, 128, kernel_size=2,
stride=2, padding=0, bias=False), ②
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.ConvTranspose2d(128, 32, kernel_size=2,
stride=2, padding=0, bias=False), ③
nn.BatchNorm2d(32),
nn.ReLU(True),
nn.ConvTranspose2d(32, in_channels, kernel_size=2,
stride=2, padding=0, bias=False), ④
nn.Sigmoid()
)
① 将 (nz, 1, 1) 转换为 256, 4, 4)-大小的张量
② 增加到 128, 8, 8)-大小的张量
③ 增加到 32, 16, 16)-大小的张量
④ 增加到 1, 32, 32)-大小的张量
列表 14.4 PyTorch- 自动编码器训练
from torch import nn
from torch.nn import functional as F
conv_out = conv_encoder(X) ①
z = fc(conv_out) ②
Xr = decoder(z) ③
recon_loss = F.mse_loss(Xr, X) ④
① 将输入图像通过卷积编码器传递
② 减少到 nz 维度
③ 通过解码器使用 z 重构图像
④ 计算重构损失
14.5.1 自动编码器和 PCA
重要的是要认识到,自动编码器执行的降维比 PCA 更强大。PCA 是一个线性过程;它只能将数据点投影到最佳拟合超平面。自动编码器可以将任意复杂的非线性超曲面拟合到数据中,仅受编码器-解码器对的表达能力限制。如果编码器和解码器只有一个线性层(没有 ReLU 或其他非线性),那么自动编码器将数据点投影到超平面,类似于 PCA,但不一定是同一个超平面)。
14.6 潜在空间的平滑性、连续性和正则化
最小化重建损失并不产生唯一解。例如,图 14.6 展示了将 2D 输入转换为 1D 潜在空间表示的两个示例,分别是线性和曲线的。经过正则化的实线(solid line)和非正则化的锯齿形流形(dashed line)都很好地拟合了训练数据,具有低重建误差。但前者更平滑,更令人满意。

(a) 线性潜在空间

(b) 曲线潜在空间
图 14.6 从 2D 输入空间映射到 1D 潜在空间的两个示例。两者都显示了正则化(实线)与非正则化(虚线)的潜在空间流形。实心小圆表示训练数据点。
注意标记为p[1]、p[2]和p[3]、p[4](正方形标记)的点对。在输入空间中,它们之间的距离大致相同。但当投影到虚线曲线(非正则化潜在空间)上时,它们的距离(沿曲线测量)变得相当不同。这是不希望的,并且在正则化潜在空间中不会发生(在这里,距离是沿实线测量的)。在高维情况下,这种情况变得更加明显。
包含训练数据集的锯齿形曲线段比平滑的曲线段更长。一个好的潜在流形通常有更少的扭曲和转弯(更平滑),因此在某种意义上“长度”是最小的。这让人想起了我们在第 9.3.1 节中讨论的最小描述符长度(MDL)原则。
我们如何确保选择最平滑的潜在空间,而不是其他也能最小化重建损失的潜在空间?通过在无处不在的重建损失之上添加额外的约束(损失)。回想一下我们在第 6.6.3 和 9.3 节中讨论的正则化概念。在那里,我们引入了一个显式的损失,它惩罚较长的解(这与最大化参数值的后验概率而不是似然性相当)。在本章中,我们探索的一个相关方法是,将潜在空间建模为属于已知家族(例如高斯)的概率分布,并最小化此估计分布与零均值单变量高斯分布之间的差异(KL 散度)。编码器-解码器神经网络对从头到尾训练以最小化一个损失,它是重建损失和此 KL 散度的加权总和。试图保持接近零均值单位方差高斯分布惩罚了从紧凑性和平滑性中偏离。这是变分自编码器(VAEs)的基本思想)。
正则化的总体效果是创建一个更紧凑的潜在空间。如果我们只最小化重建损失,系统可以通过将点映射得非常远(空间是无限的)来实现这一点。正则化对抗这种情况,并激励系统不要将训练点映射得太远。它试图限制对应于训练输入的点在潜在空间中占据的总体积。
14.7 变分自编码器
VAEs 是自编码器的一种特殊情况。它们具有相同的架构:一对神经网络分别对输入向量进行编码和解码。它们也有重建损失项。但它们还有一个额外的损失项,称为 KL 散度损失,我们将在下面简要解释。
注意:在本章中,我们用
表示潜在变量,用
表示输入变量。
14.7.1 VAEs 的几何概述
图 14.7 尝试提供 VAE 潜在空间建模的几何视图。在训练过程中,给定一个输入
,编码器不会直接发出相应的潜在空间表示
。相反,编码器发出来自预先选择的分布族的一组参数。例如,如果预先选择的族是高斯分布,编码器会发出一对参数值
(
), Σ(
)。这些是特定高斯分布 𝒩(
;
(
), Σ(
)) 在潜在空间中的均值和协方差矩阵。与输入
对应的潜在空间表示
是通过采样编码器发出的这个分布获得的。因此,在高斯情况下,我们有
∼ 𝒩(
;
(
), Σ(
))。
注意:符号 ∼ 表示从分布中进行采样。

图 14.7 VAE 潜在空间建模分布的几何描述
这个分布,我们称之为输入
的 潜在空间映射,在图 14.7 中用带有深色边框的空心圆表示。这种映射称为 随机映射。
潜在空间映射分布应该具有狭窄的单峰概率密度函数(例如,方差很小的高斯分布:即小的||Σ||)。概率密度函数的狭窄峰度意味着样本点云形成一个紧密的小簇——从该分布中随机抽取的任何样本很可能接近均值。因此,从这样的分布中采样
与从
到
=
(
)的确定性映射没有太大区别。这种采样以获得潜在向量仅在训练期间进行。在推理期间,我们直接使用编码器发出的均值作为输入的潜在空间表示:即,
=
(
)。
解码器将潜在向量表示
映射回输入空间中的一个点,例如x̃。这是输入向量的重建版本(在图 14.7 中由一个带有黑色边框的小白方块表示)。因此,解码器是根据潜在向量估计(重建)输入的。
14.7.2 VAE 训练、损失和推理
训练包括以下步骤:
-
为q(
|
)选择一个简单的分布族。高斯是一个流行的选择。 -
每个输入
映射到一个单独的分布。编码器神经网络发出该分布的参数。对于高斯情况,编码器发出μ(
),Σ(
)。潜在向量
是从该发出的分布中采样的。 -
解码器神经网络以
作为输入并发出重建输入x̃。
给定输入、重建输入和潜在向量,我们可以计算以下描述的重建损失和 KL 散度损失。训练过程的目的是迭代最小化这些损失。因此,变分自编码器(VAE)被训练以在每个输入批次上最小化以下两个损失项的加权总和。
-
重建损失——正如在自编码器中一样,在适当训练的 VAE 中,重建x̃应该接近原始输入
。因此,重建损失是![]()
-
KL 散度损失—在变分自编码器(VAE)中,我们还有一个与编码器发出的分布与零均值单位方差高斯分布之间的 KL 散度成比例的损失项。KL 散度衡量了两个概率分布之间的差异,并在第 6.4 节中进行了详细讨论。在此,我们声明(根据方程 6.13),VAE 的 KL 散度损失为
![]()
其中 q(
|
) 表示潜在空间映射概率分布,而 p(
) 是一个固定的目标分布。我们希望我们的潜在向量全局分布能够模仿目标分布。目标通常被选为紧凑分布,以便全局潜在向量分布也是紧凑的。预先选择的分布族中流行的选择是高斯分布,而对于固定分布则是零均值单位协方差矩阵的高斯分布:
![]()
应该注意的是,对于上述先验选择,我们可以通过第 14.7.7 节中描述的闭式公式来评估 KLD 损失。
最小化ℒ[kld]实际上要求 q(
|
) 很高——即接近一——在 p(
) 很高的
值处(参见图 14.8),因为那时它们的比率接近一,对数接近零。在 q(
|
) 很低(接近零)的地方的 p(
) 的值并不重要,因为 q(
|
) 作为ℒ[kld]中的一个因子出现——这些项对损失的贡献无论如何都接近零。因此,KL 损失本质上试图确保 q(
|
)的大多数样本点云落在 p(
)样本点云的密集区域。从几何上看,这意味着黑色边界的细小空心圆云与目标分布有大量的重叠质量。如果每个训练数据点都像这样,潜在向量整体全局云也将与目标分布有显著的重叠。由于目标分布通常选择为紧凑型,这反过来又确保了整体潜在向量分布(图 14.7 中的深色填充圆圈)是紧凑的。例如,当目标分布是零均值的单位协方差矩阵高斯𝒩(
;
, I)时,潜在向量的大部分质量都包含在单位半径球内。如果没有 KL 散度项,潜在向量将在潜在空间中扩散。简而言之,KL 损失 正则化潜在空间。

(a) 低 KL 散度损失(高 q(
|
)与高 p(
)相一致

(b) 高 KL 散度损失(高 q(
|
)与低 p(
)相一致
图 14.8 展示了编码器生成的分布(𝒩(
;
(
), Σ(
)))与目标分布 p(
)(此处 p(
) ≡ 𝒩(
;
, I))或,等价地,它们之间低的 KL 散度。
神经网络的编码器-解码器对是端到端训练以最小化重建损失和 KL 损失的加权总和。特别是,编码器学习发出 q(
|
)分布的参数。
在推理过程中,只使用编码器。编码器接收输入
并输出
(
) 和 Σ(
)。我们在这里不进行采样。相反,我们直接使用均值作为输入的潜在空间表示。
注意到每个输入点
映射到一个单独的高斯分布 q(
|
= N(
;
(
), ∑(
)). 所有这些共同建模的总体分布 p(
) 可以非常复杂。然而,这种复杂性并不影响我们的计算,我们的计算只涉及 q(z|x) 和 p(z)。这正是这种方法强大的原因。
14.7.3 VAEs 和贝叶斯定理
在训练过程中,编码器神经网络随机地将特定的输入数据实例,输入空间中的一个点
,映射到潜在空间中的点
~ 𝒩(
;
(
), ∑(
)). 因此,潜在空间映射有效地建模了后验概率 p(
|
)。请注意,我们使用符号 q(
|
) 来表示编码器实际发出的分布,而使用符号 p(
|
) 来表示真正的(未知的)后验概率分布。当然,我们希望这两个分布尽可能接近:也就是说,我们希望它们之间的 KL 散度最小。在本节的后面部分,我们将看到如何最小化 q(
|
) 和 p(
|
) 之间的 KL 散度,从而得到整个 VAE 算法。
解码器将潜在空间中的这个点 (
) 映射回输入空间中的点 x̃。因此,它建模了概率分布 p(
|
)。
潜在向量
的全局分布有效地建模了 p(
)(如图 14.7 中用深色阴影填充的小圆圈所示)。这些概率通过我们熟悉的老朋友,贝叶斯定理相连接:

14.7.4 随机映射导致潜在空间平滑
从狭窄分布中采样编码器的输出与确定性映射相似,但又不完全相同。它相对于直接编码有一个相当意外的优势。每次在训练过程中遇到特定的输入点时,都会将其映射到潜在空间中的不同点——所有这些点都必须解码回输入空间中的相同区域。这强制潜在空间具有整体平滑性:附近的
值都对应于附近的
值。
14.7.5 直接最小化后验概率需要非常昂贵的归一化
第 14.7.3 节中 VAE 的贝叶斯定理表达式给我们一个想法。为什么不训练神经网络直接最大化后验概率 p(
|
),其中 X 表示训练数据集?这从理论上来说是合理的;我们选择的是给定训练数据后验概率最大的潜在空间。当然,我们必须一次优化一个批次,就像我们总是用神经网络做的那样。
我们如何评估后验概率?公式如下:

分母包含对所有值
的求和。记住,每次迭代时,神经网络权重都会改变,所有之前计算出的潜在向量都变得无效。这意味着我们必须在每次迭代中重新计算所有潜在向量,这是不可行的。每次迭代的复杂度是 𝒪(n),每个 epoch 的复杂度是 𝒪(n²),其中 n 是训练数据实例的数量(可能是数百万)。我们必须寻找其他方法。这带我们来到了 证据下界 (ELBO) 类型的解决方案。
14.7.6 ELBO 和 VAEs
我们不知道真实的概率分布 p(
|
)。让我们尝试学习一个尽可能接近 p(
|
) 的近似概率分布 q(
|
)。换句话说,我们希望最小化两者之间的 KL 散度(KL 散度在第 6.4 节中介绍)。这个 KL 散度是

我们可以将其展开为

其中 D 是
的域:即潜在空间,H[q] 是概率分布的熵(熵在第 6.2 节中介绍),E[q](ln(p(
,
))) 是在概率密度 q(
|
) 下 ln(p(
,
)) 的期望值。重新排列项,我们得到

其中,右侧是常数,因为它属于数据的属性,在优化过程中不能调整。将证据下界(ELBO)定义为
ELBO = H[q] + E[q](ln(p(
,
)))
我们得到
KLD(q, p) + ELBO = 常数
因此,最小化 p(
|
) 和其近似 q(
|
) 之间的 KL 散度相当于最大化 ELBO。我们很快就会看到这导致了一种优化变分自编码器的方法。
ELBO 名称的意义
我们为什么称之为证据下界?嗯,答案隐藏在关系 KLD(q, p) + ELBO = ln(p(
)) 中。右侧是证据对数似然。记住,KL 散度始终是非负的。所以,当 KL 散度为零时,ln(p(
)) = ELBO。这意味着证据对数似然不能低于 ELBO 值。因此,ELBO 是证据对数似然的下界;简而言之,它就是证据下界。
ELBO 的物理意义
让我们看看 ELBO 优化的物理意义:
ELBO = H[q] + E[q](ln(p(
,
)))
第一项是熵。正如我们在第 6.2 节中看到的,这是分布扩散程度的度量。如果点在分布中均匀分布——概率密度平坦,没有高峰值——熵就高。当分布有少数高峰值和低值时,熵就低(记住,对于概率密度,高峰值意味着其他地方的低值,因为函数下的总体积是常数:一个)。因此,最大化 ELBO 意味着我们正在寻找一个扩散的分布 q(
|
)。这反过来又鼓励潜在空间中的平滑性,因为我们实际上是在说一个输入点
可以以几乎相等的概率映射到均值
(
)(由编码器发出)周围的任何点。请注意,这与每个输入应映射到潜在空间中唯一点的概念有些冲突。解决方案试图在这两种冲突的要求之间进行优化。
另一个术语——在概率密度 q(
|
) 下对联合密度 p(
,
) 的对数的期望——实际上衡量了这两个之间的重叠程度。最大化它等同于说 q(
|
) 必须在 p(
,
) 高的地方也高。这从直觉上看似乎是正确的。联合密度 p(
,
) = p(
|
)p(
)。它在后验 p(
|
) 和先验 p(
) 都高的时候高。如果 q(
|
) 近似后验,那么它应该在联合密度高的地方也高。
让我们继续探索 ELBO。随着 VAE 优化的算法的出现,将出现更多的物理意义:

重新排列项并简化

最后的表达式提供了更多的物理解释,并导致了 VAE 算法。让我们详细检查最终 ELBO 表达式中最后的两个项。第一个项是 E[q](ln(p(
|
))). 当在相同的
值上 q(
|
) 和 p(
|
) 都高时,这个值是高的。对于给定的
,q(
|
) 在那些可能是输入
的编码输出(即,潜在表示)的
值上是高的。在这些相同的
位置上高 p(
|
) 意味着从这些
位置解码回相同的
值的概率很高。因此,这个项基本上是说如果
以高概率 编码 到
,那么
也应该以高概率 解码 回
。换句话说,从输入到潜在空间再到输入空间的往返不应该使我们远离原始输入。在图 14.7 中,这意味着标记为
的输入点靠近标记为
的输出点。换句话说,最小化重建损失导致 ELBO 最大化。
现在考虑第二个项。它带有一个负号。最大化这个项等价于最小化q(
|
)和p(
)之间的 KL 散度。这是正则化项。从另一个角度来看,这是我们将对潜在空间基本组织结构的信念注入系统的项。记住,KL 散度KLD(q(
|
), p(
))对q(
|
)的小值贡献很少。它主要由q(
|
)的大值主导。从图 14.7 的角度来看,最小化这个 KL 散度基本上确保了大多数空心圆圈落在充满圆圈的高密度区域内。
因此,总的来说,最大化 ELBO 等价于最小化重建损失,并通过最小化从特定先验分布的 KL 散度来实现正则化。这就是我们在 VAEs 中做的事情。在每一次迭代中,我们最小化重建损失(就像在普通的 AE 中一样)以及最小化与已知(或猜测的)先验的散度。请注意,这并不要求我们每次迭代都编码所有训练输入。这种方法是增量式的——一次一个输入或输入批次——就像任何其他神经网络的优化一样。此外,尽管我们最初是从寻找对p(
|
)的近似开始的,但最终的表达式中并没有这一点。只有一个先验p(
),我们可以使用一些合适的固定分布。
14.7.7 先验选择:零均值、单位协方差高斯分布
对于已知的先验,流行的选择是零均值、单位协方差矩阵高斯分布,𝒩(
, I),其中 I 是d × d的单位矩阵,d是潜在空间的维度,
是所有零的d × 1 向量。请注意,最小化从𝒩(
, I)的 KL 散度等价于将大部分质量限制在单位球(以原点为中心,半径为 1 的超球体)内。换句话说,这个 KL 散度项限制了潜在向量不要扩散到ℜ[d]上,而主要保持在单位球内。记住,一个紧凑的潜在向量集在某种意义上对应于输入向量的最简单(最小描述符长度)表示:也就是说,正则化后的潜在空间(第 14.6 节)。
高斯分布的 KL 散度有一个封闭形式的表达式,我们在第 6.4.1 节中推导。我们首先重复方程 6.14 以获得两个高斯分布之间的 KL 散度,然后得到一个特殊情况的公式,其中一个高斯分布是零均值、单位协方差高斯分布:

方程 14.1
其中运算符 tr 表示矩阵的迹(对角线元素之和),运算符 det 表示行列式。根据假设,p(
) = 𝒩(
, I):也就是说,
[p] =
和 Σ[p] = I。因此,

在这一点上,我们引入另一个简化的假设:协方差矩阵 Σ[q]* 是一个对角矩阵*。这意味着矩阵可以紧凑地表示为
Σ[q] =
[q]
其中
[q] 包含主对角线上的元素,并且我们没有在非对角线元素中重复表达零。请注意,这不是一个离奇的假设。我们正在用轴不相关的高斯 q(
|
) 来近似 p(
|
)。
由于这个假设,

很容易看出,表达式 (
[q]²[i] − 2log(
[q][i])) 在
[q][i] = 1 时达到最小值。因此,总的来说,当均值在原点且方差均为 1 时,与零均值、单位协方差高斯分布的 KL 散度最小化。这相当于最小化了以原点为中心、半径为 1 的球外的潜在向量的分散程度。
另一个先验的选择是具有与已知类别数量一样多的组件的高斯混合模型。我们在这里不讨论这个。
14.7.8 重参数化技巧
我们到目前为止已经避免讨论一个棘手的问题。我们说在 VAEs 中,编码器发出概率密度函数 p(
|
) 的均值和方差,从这个概率密度函数中我们 采样 编码器的输出。然而,有一个问题。编码器-解码器对是神经网络,通过反向传播进行学习。这是基于微分的。采样是不可微分的。我们如何处理这个问题?
我们可以使用一个巧妙的方法:所谓的 重参数化技巧。让我们首先在单变量情况下解释它。从高斯分布 𝒩(μ, σ) 中采样可以看作是以下两个步骤的组合:
-
从 x 中随机抽取一个样本,来自 𝒩(0,1)。请注意,这里没有可学习的参数;这是一个来自常数密度函数的样本。
-
将样本(添加μ)缩放(乘以σ)。
这实际上将采样部分从反向传播路径中移除。编码器发出μ和σ,这些是我们学习的可微分实体。采样是从一个常数密度函数中单独进行的。
这个想法可以扩展到多元高斯。从𝒩(
, Σ)中抽取样本可以分解为从𝒩(
, I)中抽取样本,并通过乘以矩阵Σ和通过
进行平移来缩放向量。因此,我们有一个可以通过反向传播学习的多元编码器。
注意:完整的 VAE 代码,可通过 Jupyter Notebook 执行,可以在mng.bz/5QYD找到。
列表 14.5 PyTorch- 重参数化技巧
def reparameterize(mu, log_var):
std = torch.exp(0.5 * log_var) ①
eps = torch.randn_like(std) ②
return mu + eps * std ③
① 将对数方差转换为标准差
② 从𝒩(
, I)中抽取样本
③ 通过乘以Σ进行缩放,并通过
进行平移
列表 14.6 PyTorch- VAE
from torch import nn
nz = 10
input_image_size = (1, 32, 32) ①
conv_encoder = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ②
nn.Conv2d(32, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ③
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), ④
nn.Flatten() ⑤
)
mu_fc = nn.Linear(4096, nz) ⑥
logvar_fc = nn.Linear(4096, nz) ⑦
① 以(c, h, w)格式输入图像大小
② 减少到 32, 16, 16 大小的张量
③ 减少到 128, 8, 8 大小的张量
④ 减少到 256, 4, 4 大小的张量
⑤ 展平为 4096 大小的张量
⑥ 将 4096 大小的张量减少到 nz 大小的μ张量
⑦ 将 4096 大小的张量减少到 nz 大小的log(σ²)张量
列表 14.7 PyTorch- VAE 解码器
from torch import nn
decoder = nn.Sequential(
nn.ConvTranspose2d(self.nz, out_channels=256,
kernel_size=4, stride=1,
padding=0, bias=False), ①
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.ConvTranspose2d(256, 128, kernel_size=2,
stride=2, padding=0, bias=False), ②
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.ConvTranspose2d(128, 32, kernel_size=2,
stride=2, padding=0, bias=False), ③
nn.BatchNorm2d(32),
nn.ReLU(True),
nn.ConvTranspose2d(32, in_channels, kernel_size=2,
stride=2, padding=0, bias=False), ④
nn.Sigmoid()
)
① 将(nz, 1, 1)转换为 256, 4, 4 大小的张量
② 增加到 128, 8, 8 大小的张量
③ 增加到 32, 16, 16 大小的张量
④ 增加到 1, 32, 32 大小的张量
列表 14.8 PyTorch- VAE 损失
recon_loss = F.binary_cross_entropy(Xr, X,
reduction="sum") ①
kld_loss = -0.5 * torch.sum(1 + log_var
- mu.pow(2) - log_var.exp()) ②
total_loss = recon_loss + beta * kld_loss ③
① 二元交叉熵损失
② KLD(q(
|
), p(
)) 其中
~ 𝒩(
, I)
③ 计算总损失
列表 14.9 PyTorch- VAE 训练
conv_out = conv_encoder(X) ①
mu = mu_fc(conv_out) ②
log_var = logvar_fc(conv_out) ③
z = reparameterize(mu, log_var) ④
Xr = self.decoder(z) ⑤
total_loss = recon_loss + beta * kld_loss ⑥
① 将输入图像通过卷积编码器传递
② 计算一个 nz 维度的μ张量
③ 计算一个 nz 维度的张量log(σ²)
④ 通过重参数化技巧抽取z
⑤ 通过解码器使用 z 重建图像
⑥ 计算总损失
自动编码器与 VAE
让我们回顾一下熟悉的 MNIST 数字数据集。它包含一个包含 60,000 个图像的训练集和一个包含 10,000 个图像的测试集。每个图像大小为 28 × 28,包含单个数字的中心裁剪。
之前,我们使用这个数据集进行分类。这里,我们以无监督的方式进行:在训练/测试期间忽略标签。我们在这个数据集上从头到尾训练自动编码器和 VAE,并查看结果(见图 14.9 和 14.10)。

(a) 自动编码器重建图像

(b) VAE 重建图像
图 14.9 比较自编码器和端到端训练的 VAE 在测试集上重建的图像。自编码器和 VAE 在从测试集重建图像方面做得相当不错。

(a) 自编码器潜在空间 nz=2)

(b) VAE 潜在空间 (nz=2)
图 14.10 自编码器和 VAE 学习到的潜在空间之间的差异。我们在 MNIST 上使用nz = 2 训练自编码器和 VAE,并绘制测试集的潜在空间。自编码器仅最小化重建损失,因此只要重建损失低,任何潜在空间都是可接受的。正如预期的那样,学习到的潜在空间是稀疏的,并且具有非常高的分散度。相比之下,VAE 通过正则化最小化重建损失。这是通过最小化学习到的潜在空间与已知先验分布 𝒩(
, I) 之间的 KL 散度来实现的。添加这个正则化项确保潜在空间被约束在单位球内。这可以在图 14.10b 中看到,其中学习到的潜在空间要紧凑得多。
自编码器被训练以最小化输入图像和重建图像之间的 MSE。潜在空间没有其他限制。
VAE 被训练以最大化 ELBO。正如我们在上一节中看到的,我们可以通过以最小化 KL 散度的形式进行正则化来最大化 ELBO,即 VAE 的情况下的 𝒩(
, I)。因此,网络被激励确保学习到的潜在空间被约束在单位球内。
一个需要注意的微小实现细节是我们使用二元交叉熵而不是 MSE 来训练 VAE。在实践中,这导致更好的收敛。
摘要
-
在潜在空间建模中,我们将输入数据点映射到一个低维潜在空间。潜在空间通常是包含具有共同感兴趣属性的点的流形。感兴趣的属性可以是属于特定类别,例如所有由莎士比亚写的段落。潜在向量是输入数据的更简单、更紧凑的表示,其中只保留与感兴趣属性相关的信息,其他信息被消除。
-
在潜在空间建模中,所有训练数据输入都满足感兴趣的属性。例如,我们可以在莎士比亚写的段落上训练一个潜在空间模型。然后,学习到的流形包含对应于各种类似莎士比亚段落的点。远离流形的点不太像莎士比亚。通过检查这个距离,我们可以估计一个段落被莎士比亚写的概率。通过采样概率分布,我们甚至可能能够生成伪莎士比亚段落。
-
从几何角度来说,我们将输入点投影到流形上。PCA 执行一种特殊的潜在空间建模,其中流形是训练数据的最佳拟合超平面。
-
自编码器可以执行比 PCA 更强大的降维。自编码器由一个编码器(E),它将输入数据点映射到低维空间,和一个解码器(D),它将低维表示映射回输入空间。它被训练以最小化重建损失:即输入和重建(编码后解码)向量之间的距离。
-
变分自编码器(VAEs)将潜在空间建模为概率分布,以施加额外的约束(超过重建损失),这样我们就可以生成更正则化的潜在空间。
-
在 VAEs 中,编码器通过一个随机过程(而不是确定性过程)将输入映射到潜在表示。它发出 p(
|
),而不是直接发出
。
通过从 p(
|
) 中采样获得。解码器将潜在空间中的一个点
映射回输入空间。它也被建模为概率分布 p(
|
)。 -
VAE 学习到的潜在空间比自编码器学习到的潜在空间更紧凑、更平滑(因此更受欢迎)。
-
附录
A.1 两个向量之间夹角的点积和余弦
在 2.5.6.1 节中,我们说明了向量
沿另一个向量
的分量是
⋅
=
^T
。这相当于 ||
|| ||
||cos(θ),其中 θ 是向量
和
之间的角度。
在本节中,我们为二维情况提供这个证明,以加深你对点积几何学的直觉。从图 A.2 中,我们可以看到

这可以重新写为

方程 A.1

方程 A.2
使用方程 A.1 中的已知三角恒等式,我们得到


(a) 2D 向量沿坐标轴的分量。注意,||
|| 是斜边的长度。

(b) 作为另一向量沿一个向量的分量的点积
⋅
=
^T
= a[x]b[x] + a[y]b[y] = ||
|| ||
||cos(θ).
图 A.1 向量分量和点积
代入方程 A.2 中的 cosϕ 和 sinϕ,我们得到一个以 cosθ 和 sinθ 为未知数的联立线性方程组:

这个联立线性方程组可以紧凑地写成矩阵-向量形式

这可以简化为

这个方程可以求解得到

因此,||
|| ||
||cosθ = a[x]b[x] + a[y]b[y],这正是我们要证明的。
A.2 矩阵行列式
如果天真地计算行列式,计算过程既繁琐又数值不稳定。你永远不应该手动计算——所有线性代数软件包都提供执行此操作的例程。因此,我们只描述计算 2×2 矩阵行列式的算法。这个行列式可以计算为
det(A) = a[11]a[22] − a[12]a[21]
逆矩阵是

A.3 计算高斯分布的方差
从方程 5.13 的积分形式,我们有

代入方程 5.22,

在其中,我们得到

将
代入,这表示
和 2σ²y² = (x − μ)²,我们得到

使用分部积分,

现在,将 v = y² 代入,这表示 dv/2 = y dy,

因此,

这些极限可以使用洛必达法则来评估:

在这两种情况下,极限都是零,因为无论 y 是趋向于正无穷还是负无穷,e^(y²) 都趋向于正无穷。因此,分母在这两种情况下都趋向于无穷大,导致分数趋向于零。
因此,计算 var[高斯](x) 的第一个项变为
.
第二项

最后这个积分是特殊的一个。为了评估它,我们需要从一维到二维——这可能是极少数情况下,使问题更复杂反而有帮助的情况之一。它值得检查,让我们来看看。设

由于积分变量无关紧要,我们也可以写成

让我们相乘它们:

这个双重积分的积分域(也称为积分区域)是无限大的 XY 平面,其中 x 和 y 都从 −∞ 到 ∞ 变化。这个相同的平面也可以看作是一个无限半径的圆(无限半径的圆与无限长边的矩形相同!)。因此,我们可以切换到极坐标,使用以下变换
x = r cos(θ)
y = r cos(θ)
这意味着

将 v = r² 代入,这表示 dv = 2rdr,我们得到

因此,var[高斯](x) 的第二项计算结果为
。我们已经证明第一项计算结果为零。所以,我们得到
var[高斯](x) = σ²
因此,概率密度函数
中的 σ 是标准差(方差的平方根),而 μ 是期望值。
A.4 统计学中的两个定理
在本节中,我们研究多元统计学中的两个重要不等式:Jensen 不等式和对数和不等式。
A.4.1 Jensen 不等式
考虑一个随机变量 X。现在,让我们将这些视为离散变量,尽管我们将得到的结果同样适用于连续变量。因此,让随机变量取离散值
[1],
[2],⋯,
[n],其概率为 p(
[1]),p(
[2]),⋯,p(
[n]).
现在假设 g(
) 是一个定义域包含这些随机变量的凸函数。从方程 3.11 和章节 3.7 中,我们知道对于任何凸函数 g(
),对于其任意一组输入值
[i],i = 1⋯n 和一组权重 α[i] i = 1⋯n 满足
,函数输出的加权总和大于或等于输入加权总和的函数输出:即,
特别是,让我们选择所有随机变量作为输入值,它们的概率作为权重(α[i] = p(
[i]))。我们可以这样做,因为概率之和为 1,正好与权重应该做到的相同。这导致

方程 A.3
方程 A.3 是詹森不等式。一个很好的记忆方法是:对于凸函数,函数的期望值大于或等于期望值的函数。它也适用于连续随机变量。
A.4.2 对数和不等式
假设我们有两个正数集合 a[1],a[2],⋯,a[n] 和 b[1],b[2],⋯,b[n]。令 a = Σ[i]^n[= 1] a[i] 和 b = Σ[i]^n[= 1] b[i]。给定这些,对数和不等式定理表明,

方程 A.4
为了理解为什么这是真的,让我们给出一个非正式的证明。首先,让我们定义 g(x) = xlogx。这是一个凸函数,因为

现在,根据这个 g 的定义,

最后这个表达式是凸函数输出的加权总和,权重之和为 1(因为 Σ[i]^n[= 1] b[i]/b = 1)。因此,我们可以使用方程 3.11,章节 3.7。然后我们得到

A.5 伽玛函数与分布
要理解伽玛分布,我们需要了解基本的伽玛函数。首先,让我们对伽玛函数做一个概述。
A.5.1 伽玛函数
在某种意义上,伽玛函数是阶乘的推广。阶乘函数仅对整数定义,并由基本方程
n! = n(n − 1)!
伽玛函数定义为

方程 A.5
对方程 A.5 应用分部积分,我们得到

第一个项为零。这是因为

因此,

因此,对于整数值 α = n,Γ(n) = n!. 有其他等价的伽马函数定义,但在这里我们不会讨论它们。相反,让我们来谈谈伽马分布。
A.5.2 伽马分布
具有伽马分布的随机变量的概率密度函数是一个具有两个参数 α 和 β 的函数:

方程 A.6
很容易看出这是一个合适的概率密度函数:

通过代入 y = βx,我们得到

如果 α = 1,伽马分布简化为

这在图 A.2a 中以 β 的几个值进行了绘图。伽马分布有两个项 x^(α−1) 和 e^(−βx),它们具有某种相反的效果:前者随着 x 的增加而增加,而后者随着 x 的增加而减少。在 x 较小的值时,前者占主导地位,乘积随着 x 的增加而增加。但最终,指数函数开始占主导地位,并将乘积向下渐近地拉向零。因此,伽马分布有一个峰值。较大的 β 导致更高的峰值和更陡峭的向零的下降。较大的 α 将峰值进一步向右移动。期望值是 𝔼(x) = α/β,如图 A.2 所示。

(a) 伽马分布:α = 1,各种 β 值

(b) 伽马分布:β = 1,各种 α 值

(c) 伽马分布:α = 2,各种 β 值

(d) 伽马分布:β = 2,各种 α 值
图 A.2 不同 α 和 β 值的伽马分布图。较大的 β 导致更高的峰值和更陡峭的向零的下降。较大的 α 将峰值向右移动。期望值是 𝔼(x) = α/β。
伽马分布的期望值 𝔼(x) = α/β。这可以通过一个小技巧证明,这个技巧如此酷,仅凭这一点就值得讨论:

但伽马分布

或者

使用这个,

伽马分布的最大值
为了最大化伽马概率密度函数 p(λ|X) = λ^((α[n] − 1))e^(−β[n]λ) 对于随机变量 λ,我们取导数并将其等于零:



,我们可以轻松地计算
[0] + 
^((l)),这些变量可以通过从最后一个(最接近输出层)层反向迭代到第一个(最接近输入层)层来计算——因此得名反向传播——并且所有所需的梯度都可以从这些辅助变量中计算出来。数学上,
。


。softmax 输出向量由概率组成,这意味着它们是介于 0 和 1 之间的数字,并且它们的总和为 1。
。
= [k[H], k[W]] 来表示二维卷积中的内核大小(高度,宽度)。如果 (x, y) 表示二维内核左上角当前的位置,则右下角位于 (x + k[W] − 1, y + k[H] − 1)。在图 10.7 中,
和
。我们还可以使用估计的参数对新数据实例进行预测。
,伽马分布是
。伽马分布随 α 和 β 的不同值而变化。
。后验分布也是一个伽马分布,
,其中
和
。



浙公网安备 33010602011771号