Python-深度学习教程-全-

Python 深度学习教程(全)

原文:Deep Learning with Python

协议:CC BY-NC-SA 4.0

一、机器学习和深度学习简介

深度学习的主题最近非常受欢迎,在这个过程中,出现了几个术语,使区分它们变得相当复杂。人们可能会发现,由于主题之间大量的重叠,将每个领域整齐地分开是一项艰巨的任务。

本章通过讨论深度学习的历史背景以及该领域如何演变成今天的形式来介绍深度学习的主题。稍后,我们将通过简要介绍基础主题来介绍机器学习。从深度学习开始,我们将利用使用基本 Python 从机器学习中获得的构造。第二章开始使用 PyTorch 进行实际实现。

定义深度学习

深度学习是机器学习的一个子领域,它处理的算法非常类似于人脑的过度简化版本,解决了现代机器智能的一个巨大类别。在智能手机的应用生态系统(iOS 和 Android)中可以找到许多常见的例子:相机上的人脸检测,键盘上的自动纠正和预测文本,人工智能增强的美化应用,智能助手,如 Siri/Alexa/Google Assistant,Face-ID(iphone 上的人脸解锁),YouTube 上的视频建议,脸书上的朋友建议,Snapchat 上的 cat filters,都是只为深度学习而制造的最先进的产品。本质上,深度学习在当今的数字生活中无处不在。

说实话,如果没有一些历史背景,定义深度学习可能会很复杂。

简史

人工智能(AI)发展到今天的历程可以大致分为四个部分:即基于规则的系统、基于知识的系统、机器和深度学习。虽然旅程中的粒度转换可以映射为几个重要的里程碑,但我们将覆盖一个更简单的概述。整个进化包含在“人工智能”这个更大的概念中。让我们采取一步一步的方法来解决这个宽泛的术语。

img/478491_2_En_1_Fig1_HTML.jpg

图 1-1

人工智能的前景

深度学习的旅程始于人工智能领域,人工智能是该领域的合法父母,其历史可以追溯到 20 世纪 50 年代。人工智能领域可以简单定义为机器思考和学习的能力。用更通俗的话来说,我们可以把它定义为以某种形式用智能帮助机器的过程,这样它们可以比以前更好地执行任务。上图 1-1 展示了一个简化的人工智能场景,上面提到的各个领域展示了一个子集。我们将在下一节更详细地探讨这些子集。

基于规则的系统

我们诱导到机器中的智能不一定是复杂的过程或能力;像一套规则这样简单的东西可以被定义为智力。第一代人工智能产品只是基于规则的系统,其中一套全面的规则被引导到机器,以映射所有的可能性。一台根据既定规则执行任务的机器会比一台僵化的机器(没有智能的机器)产生更有吸引力的结果。

现代等价物的一个更外行的例子是提款机。一旦通过验证,用户输入他们想要的金额,机器就会根据店内现有的纸币组合,用最少的钞票分发正确的金额。机器解决问题的逻辑(智能)是显式编码(设计)的。机器的设计者仔细考虑了所有的可能性,设计了一个可以用有限的时间和资源有计划地解决任务的系统。

人工智能早期的大部分成功都相当简单。这样的任务可以很容易地形式化描述,就像跳棋或国际象棋。能够容易地正式描述任务的概念是计算机程序能够或不能够容易地完成什么的核心。例如,考虑国际象棋比赛。国际象棋游戏的正式描述将是棋盘的表示、每个棋子如何移动的描述、开始的配置以及游戏结束时的配置的描述。有了这些形式化的概念,将下棋的人工智能程序建模为搜索就相对容易了,而且,如果有足够的计算资源,就有可能产生相对较好的下棋人工智能。

人工智能的第一个时代专注于此类任务,并取得了相当大的成功。该方法的核心是领域的符号表示和基于给定规则的符号操作(使用越来越复杂的算法来搜索解决方案空间以获得解决方案)。

必须注意,这些规则的正式定义是手工完成的。然而,这种早期的人工智能系统是相当通用的任务/问题解决者,任何可以正式描述的问题都可以用通用的方法来解决。

这种系统的关键限制是,国际象棋比赛对人工智能来说是一个相对容易的问题,因为问题集相对简单,可以很容易地形式化。人类日常解决的许多问题却并非如此(自然智能)。例如,考虑诊断一种疾病或把人类的语言转录成文本。这些人类可以完成但很难正式描述的任务,在人工智能的早期是一个挑战。

基于知识的系统

利用自然智能解决日常问题的挑战将人工智能的前景演变成了一种类似于人类的方法——即通过利用大量关于任务/问题领域的知识。鉴于这种观察,随后的人工智能系统依赖于大型知识库来获取关于问题/任务领域的知识。注意,这里使用的术语是知识,而不是信息数据。就知识而言,我们只是指程序/算法可以推理的数据/信息。一个例子可以是一个地图的图形表示,它的边标有距离和交通流量(它是不断更新的),允许程序推理两点之间的最短路径。

这种基于知识的系统代表了第二代人工智能,其中知识由专家汇编,并以允许算法/程序推理的方式表示。这种方法的核心是越来越复杂的知识表示和推理方法,以解决需要这种知识的任务/问题。这种复杂性的例子包括使用一阶逻辑对知识和概率表示进行编码,以捕捉和推理领域固有的不确定性。

这类系统面临并在一定程度上得到解决的主要挑战之一是许多领域固有的不确定性。人类相对擅长在未知和不确定的环境中进行推理。这里的一个关键观察是,即使我们对一个领域的知识也不是黑或白的,而是灰色的。在这个时代,对未知和不确定性的描述和推理取得了很大进展。在一些任务中取得了有限的成功,如在未知和不确定的情况下,依靠利用知识库进行杠杆作用和推理来诊断疾病。

这种系统的关键限制是需要手工编译来自专家的领域知识。收集、编译和维护这样的知识库使得这样的系统不切实际。在某些领域,收集和编辑这样的知识是极其困难的——例如,将语音转录成文本或者将文档从一种语言翻译成另一种语言。虽然人类可以很容易地学会做这样的任务,但手工编译和编码与任务相关的知识却极具挑战性——例如,英语语言和语法、口音和主题的知识。为了应对这些挑战,机器学习是前进的方向。

机器学习

在正式术语中,我们将机器学习定义为人工智能中无需显式编程即可添加智能的领域。人类通过学习获得任何任务的知识。鉴于这一观察,人工智能后续工作的重点在 10 到 20 年间转移到了基于提供给它们的数据来提高它们性能的算法上。该子领域的焦点是开发算法,该算法获取给定数据的任务/问题领域的相关知识。值得注意的是,这种知识获取依赖于标记数据和人类定义的标记数据的适当表示。

例如,考虑诊断疾病的问题。对于这样一项任务,人类专家将收集大量患者患有和未患有相关疾病的病例。然后,人类专家将识别许多有助于做出预测的特征——例如,患者的年龄和性别,以及许多诊断测试的结果,如血压、血糖等。人类专家将汇编所有这些数据,并以适当的形式表示出来——例如,通过缩放/标准化数据等。一旦准备好这些数据,机器学习算法就可以学习如何通过从标记的数据中进行归纳来推断患者是否患有疾病。注意,标记的数据由患有和未患有该疾病的患者组成。因此,从本质上来说,底层机器语言算法本质上是在给定输入(年龄、性别、诊断测试数据等特征)的情况下,寻找能够产生正确结果(疾病或无疾病)的数学函数。寻找最简单的数学函数来预测具有所需精度水平的输出是机器学习领域的核心。例如,与学习一项任务所需的示例数量或算法的时间复杂度相关的问题是 ML 领域已经提供了理论证明的答案的特定领域。这个领域已经成熟到一定程度,给定足够的数据、计算资源和人力资源来设计特征,一大类问题是可以解决的。

主流机器语言算法的关键限制是,将它们应用到一个新的问题领域需要大量的特征工程。例如,考虑在图像中识别物体的问题。使用传统的机器语言技术,这样的问题将需要大量的特征工程工作,其中专家识别并生成将被机器语言算法使用的特征。从某种意义上说,真正的智慧在于对特征的识别;机器语言算法只是学习如何结合这些特征来得出正确的答案。在应用机器语言算法之前,领域专家进行的这种特征识别或数据表示是人工智能中概念和实践的瓶颈。

这是一个概念瓶颈,因为如果领域专家正在识别特征,而机器语言算法只是学习组合并从中得出结论,这真的是人工智能吗?这是一个实际的瓶颈,因为通过传统的机器语言建立模型的过程受到所需的大量特征工程的限制。解决这个问题的人力是有限的。

深度学习

深度学习解决了机器学习系统的主要瓶颈。在这里,我们基本上把智能向前推进了一步,机器以自动化的方式为任务开发相关功能,而不是手工制作。人类从原始数据开始学习概念。例如,一个孩子看到一些特定动物(比如说猫)的例子,他很快就会学会辨认这种动物。学习过程不包括父母识别猫的特征,如它的胡须、皮毛或尾巴。人类学习从原始数据到结论,而没有明确的步骤,即识别特征并提供给学习者。从某种意义上说,人类从数据本身学习数据的适当表示。此外,他们将概念组织成一个层次结构,其中复杂的概念用基本的概念来表达。

深度学习领域的主要重点是学习数据的适当表示,以便这些数据可以用于得出结论。“深度学习”中的“深”字是指直接从原始数据中学习概念的层次结构的思想。深度学习在技术上更合适的术语是表示学习,更实用的术语是自动化特征工程

相关领域的进展

值得注意的是其他领域的进步,如计算能力、存储成本等。在深度学习最近的兴趣和成功中发挥了关键作用。例如,考虑以下情况:

  • 在过去十年中,收集、存储和处理大量数据的能力有了很大提高(例如,Apache Hadoop 生态系统)。

  • 随着众包服务(如 Amazon Mechanical Turk)的出现,生成受监督的训练数据(带标签的数据——例如,用图片中的对象进行注释的图片)的能力有了很大提高。

  • 图形处理单元(GPU)带来的计算能力的巨大提高使并行计算达到了新的高度。

  • 自动微分的理论和软件实现(如 PyTorch 或 Theano)的进步加快了深度学习的开发和研究速度。

虽然这些进步对于深度学习来说是外围的,但它们在实现深度学习的进步方面发挥了很大的作用。

先决条件

阅读这本书的关键先决条件包括 Python 的工作知识以及线性代数、微积分和概率方面的一些课程。如果读者需要了解这些先决条件,他们应该参考以下内容。

  • Mark Pilgrim-a press Publications(2004 年)著深入研究 Python

  • 《线性代数导论》(第五版),吉尔伯特·斯特朗-韦尔斯利-剑桥出版社

  • 吉尔伯特·斯特朗-韦尔斯利-剑桥出版社出版的《微积分》

  • 拉里·乏色曼-施普林格(2010)的所有统计数据(第一节,章节 1 - 5 )

前方的路

这本书关注深度学习的关键概念及其使用 PyTorch 的实际实现。为了使用 PyTorch,您应该对 Python 编程有一个基本的了解。第二章介绍 PyTorch,后续章节讨论 PyTorch 中的其他重要结构。

在深入研究深度学习之前,我们需要讨论机器学习的基本构造。在本章的剩余部分,我们将通过一个虚拟的例子来探索机器学习的初级步骤。为了实现这些构造,我们将使用 Python,并再次使用 PyTorch 来实现。

安装所需的库

为了运行本书中示例的源代码,您需要安装许多库。我们建议安装 Anaconda Python 发行版( https://www.anaconda.com/products/individual ),它简化了安装所需包的过程(使用 conda 或 pip)。您需要的包列表包括 NumPy、matplotlib、scikit-learn 和 PyTorch。

PyTorch 不是作为 Anaconda 发行版的一部分安装的。您应该安装 PyTorch、torchtext 和 torchvision,以及 Anaconda 环境。

请注意,本书中的练习推荐使用 Python 3.6(及更高版本)。我们强烈建议在安装 Anaconda 发行版之后创建一个新的 Python 环境。

使用 Python 3.6 创建一个新环境(在 Linux/Mac 中使用终端或在 Windows 中使用命令提示符),然后安装其他必要的软件包,如下所示:

conda create -n testenvironment python=3.6

conda activate testenvironment
pip install pytorch torchvision torchtext

有关 PyTorch 的更多帮助,请参考 https://pytorch.org/get-started/locally/ 的入门指南。

机器学习的概念

作为人类,我们直观地意识到学习的概念。它只是意味着随着时间的推移,在一项任务上做得更好。这个任务可以是体力的,比如学习开车,也可以是智力的,比如学习一门新语言。机器学习的学科重点是开发能够像人类一样学习的算法;也就是说,随着时间的推移和经验的积累,他们在一项任务上变得更好——从而在没有显式编程的情况下诱导智力。

要问的第一个问题是,为什么我们会对随着时间的推移,随着经验的积累而提高性能的算法的开发感兴趣。毕竟,许多算法的开发和实现都是为了解决不随时间推移而改善的现实问题;它们只是由人类开发,用软件实现,并完成工作。从银行业到电子商务,从汽车导航系统到登月飞船,算法无处不在,而且大多数算法不会随着时间的推移而改进。这些算法只是执行它们想要执行的任务,并不时需要一些维护。我们为什么需要机器学习?

这个问题的答案是,对于某些任务,开发一个通过经验学习/提高其性能的算法比手动开发一个算法更容易。尽管在这一点上,这对于读者来说似乎是不直观的,但我们将在本章中对此建立直觉。

机器学习可以大致分为监督学习,为模型提供带标签的训练数据进行学习,以及非监督学习,训练数据缺少标签。我们也有半监督学习强化学习,但是现在,我们将把范围限制在监督机器学习。监督学习又可以分为两个区域:分类,用于离散结果,以及回归,用于连续结果。

二元分类

为了进一步讨论手头的问题,我们需要精确地定义一些我们一直在直觉上使用的术语,比如任务、学习、经验和改进。我们将从二进制分类的任务开始。

考虑一个抽象的问题领域,其中我们有形式为

$$ D=\left{\left({x}_1,{y}_1\right),\left({x}_2,{y}_2\right),\dots \left({x}_n,{y}_n\right)\right} $$

的数据

其中x∈ℝnt5y= 1。

我们无法访问所有这些数据,只能访问一个子集 SD 。使用 S ,我们的任务是生成一个实现函数 f : xy 的计算过程,这样我们就可以使用 f 对未知数据进行预测( x iyI)∉s让我们把 UD 表示为一组看不见的数据——即( x iyI)∉s和( x i

*我们用未见过的数据

$$ E\left(f,D,U\right)=\frac{\ \sum \limits_{\left({x}_i,{y}_i\right)\in U}\left[f\left({x}_i\right)\ne {y}_i\right]}{\mid U\mid }. $$

的误差来衡量这个任务的性能

我们现在对这个任务有了一个精确的定义,那就是通过生成 f ,基于一些看到的数据 S ,将数据归类到两个类别中的一个( y = 1)。我们使用未知数据 U 上的误差 E ( fDU )来衡量性能(以及性能的提高)。所见数据的大小| S |在概念上等同于经验。在这种背景下,我们想要开发生成这样的函数 f (通常称为模型)的算法。一般来说,机器学习领域研究这种算法的发展,这种算法产生对这种和其他正式任务的看不见的数据进行预测的模型。(我们将在本章后面介绍多个这样的任务。)注意, x 通常被称为输入/输入变量,而 y 被称为输出/输出变量

如同计算机科学中的任何其他学科一样,这种算法的计算特性是一个重要的方面;然而,除此之外,我们还希望有一个模型 f ,它可以实现更低的误差 E ( fDU ),并且∣ S ∣尽可能小。

现在让我们将这个抽象但精确的定义与现实世界的问题联系起来,这样我们的抽象就有了基础。假设一个电子商务网站想要为注册用户定制其登录页面,以显示他们可能有兴趣购买的产品。该网站有用户的历史数据,并希望实现这一功能,以增加销售。现在让我们看看这个现实世界的问题是如何映射到我们前面描述的二进制分类的抽象问题上的。

人们可能注意到的第一件事是,给定一个特定的用户和特定的产品,人们会希望预测该用户是否会购买该产品。由于这是要预测的值,它映射到 y = 1,这里我们将让 y = + 1 的值表示用户将购买该产品的预测,而y= 1 的值表示用户将不会购买该产品的预测。请注意,选择这些值没有特别的原因;我们可以交换这一点(让 y = + 1 表示不买的情况,让y= 1 表示买的情况),不会有任何不同。我们只是使用 y = 1 来表示对数据进行分类的两个感兴趣的类别。接下来,我们假设我们可以将产品的属性和用户的购买和浏览历史表示为x∈ℝn。这一步在机器学习中被称为特征工程,我们将在本章的后面介绍。现在,只要说我们能够生成这样的映射就足够了。这样,我们就有了用户浏览和购买了什么、产品的属性以及用户是否购买了该产品的历史数据映射到{( x 1y 1 ),( x 2y 2 ),…( x n 现在,基于这些数据,我们想要生成一个函数或模型f:xy,我们可以使用它来确定特定用户将购买哪些产品,并使用它来填充用户的登录页面。我们可以通过为用户填充登录页面,查看他们是否购买产品,并评估错误 E ( fDU )来衡量模型在看不见的数据上做得有多好。

回归

本节介绍另一项任务:回归。这里,我们有格式为d= {(x1,y1),(x2, y 2 ),…(xny的数据 我们的任务是生成一个计算程序,实现函数f:xy。 注意,与二进制分类中的二进制分类标签 y = 1 不同,我们使用实值预测。我们用看不见的数据的均方根误差(RMSE)来衡量这个任务的性能

$$ E\left(f,D,U\right)={\left(\frac{\ \sum \limits_{\left({x}_i,{y}_i\right)\in U}\ {\left({y}_i-f\left({x}_i\right)\right)}²}{\mid U\mid}\right)}^{\frac{1}{2}} $$

Note

RMSE 只是取预测值和实际值之间的差值,对其求平方以考虑正负差值,取平均值以聚合所有看不见的数据,最后,求平方根以平衡平方运算。

与回归的抽象任务相对应的一个现实世界的问题是基于个人的金融历史来预测他们的信用分数,信用卡公司可以使用该信用分数来扩展信用额度。

一般化

现在让我们来看看机器学习中最重要的直觉是什么,那就是我们想要开发/生成对看不见的数据具有良好性能的模型。为了做到这一点,首先我们将介绍一个玩具数据集的回归任务。稍后,我们将使用不同复杂程度的相同数据集开发三个不同的模型,并研究结果如何不同,以便直观地理解概化的概念。

在清单 1-1 中,我们通过生成 100 个在-1 和 1 之间等距的值作为输入变量( x )来生成玩具数据集。我们基于y= 2+x+2x2+ϵ生成输出变量( y ,其中$$ \epsilon \sim \mathcal{N}\left(0,0.1\right) $$是正态分布的噪声(随机变化),0 是平均值,0.1 是标准差。清单 1-1 给出了这方面的代码,数据绘制在图 1-2 中。为了模拟可见和不可见的数据,我们使用前 80 个数据点作为可见数据,其余的作为不可见数据。也就是说,我们仅使用前 80 个数据点来构建模型,并使用剩余的数据点来评估模型。

img/478491_2_En_1_Fig2_HTML.jpg

图 1-2

玩具数据集

#import packages
import matplotlib.pyplot as plt
import numpy as np

#Generate a toy dataset
x = np.linspace(-1,1,100)
signal = 2 + x + 2 * x * x
noise = numpy.random.normal(0, 0.1, 100)
y = signal + noise
plt.plot(signal,'b');
plt.plot(y,'g')
plt.plot(noise, 'r')
plt.xlabel("x")
plt.ylabel("y")
plt.legend(["Without Noise", "With Noise", "Noise"], loc = 2)
plt.show()

#Extract training from the toy dataset
x_train = x[0:80]
y_train = y[0:80]
print("Shape of x_train:",x_train.shape)
print("Shape of y_train:",y_train.shape)

Output[]
Shape of x_train: (80,)
Shape of y_train: (80,)

Listing 1-1Generalization vs. Rote Learning

接下来,我们用一个非常简单的算法来生成一个模型,通常称为最小二乘。给定一个格式为 D = {( x 1y 1 ),( x 2y 2 ),…(xny 的数据集 最小二乘模型采用的形式是 y = βx ,其中 β 是一个向量,使得$ {\left\Vert X\beta -y\right\Vert}_2² $被最小化。 这里, X 是一个矩阵,其中每一行是一个 x (因此,x∈ℝ∈m×n,其中 m 是示例的数量,在我们的例子中是 80)。 β 的值可以用封闭形式β=(XTX)—1XTy导出。我们正在掩饰最小二乘法的许多重要细节,但这些都是当前讨论的次要问题。更相关的细节是我们如何将输入变量转换成合适的形式。在我们的第一个模型中,我们将把 x 转换成一个值的向量[ * x * 0x 1x 2 ]。也就是说,如果 x = 2,就转化为【1,2,4】。在此转换之后,我们可以使用之前描述的公式生成最小二乘模型 β 。实际情况是,我们用一个二阶多项式(次数= 2)方程来逼近给定的数据,最小二乘算法只是简单地曲线拟合或生成每个x0,x1,x2 的系数。

我们可以使用 RMSE 度量在看不见的数据上评估模型。我们还可以计算训练数据的 RMSE 度量。图 [1-3 绘制了实际值和预测值,清单 1-2 显示了生成模型的源代码。

img/478491_2_En_1_Fig3_HTML.jpg

图 1-3

阶数= 1 的模型的实际值和预测值

#Create a function to build a regression model with parameterized degree of independent coefficients
def create_model(x_train,degree):
    degree+=1
    X_train = np.column_stack([np.power(x_train,i) for i in range(0,degree)])
    model = np.dot(np.dot(np.linalg.inv(np.dot(X_train.transpose(),X_train)),X_train.transpose()),y_train)
    plt.plot(x,y,'g')
    plt.xlabel("x")
    plt.ylabel("y")
    predicted = np.dot(model, [np.power(x,i) for i in range(0,degree)])
    plt.plot(x, predicted,'r')
    plt.legend(["Actual", "Predicted"], loc = 2)
    plt.title("Model with degree =3")
    train_rmse1 = np.sqrt(np.sum(np.dot(y[0:80] - predicted[0:80], y_train - predicted[0:80])))
    test_rmse1 = np.sqrt(np.sum(np.dot(y[80:] - predicted[80:], y[80:] - predicted[80:])))
    print("Train RMSE(Degree = "+str(degree)+"):", round(train_rmse1,2))
    print("Test RMSE (Degree = "+str(degree)+"):", round(test_rmse1,2))
    plt.show()

#Create a model with degree = 1 using the function
create_model(x_train,1)

Output[]
Train RMSE(Degree = 1): 3.55
Test RMSE (Degree = 1): 7.56

Listing 1-2.Function to build model with parameterized number of co-efficients

类似地,列表 1-3 和图 1-4 对度数=2 的模型重复该练习。

img/478491_2_En_1_Fig5_HTML.jpg

图 1-5

阶数= 8 的模型的实际值和预测值

img/478491_2_En_1_Fig4_HTML.jpg

图 1-4

阶数= 2 的模型的实际值和预测值

#Create a model with degree=2
create_model(x_train,2)

Output[]
Train RMSE (Degree = 3) 1.01
Test RMSE (Degree = 3) 0.43

Listing 1-3.Creating a model with degree=2

接下来,如清单 1-4 所示,我们用最小二乘算法生成另一个模型,但是我们将把 x 转换为x0, x 1x 2x 3x 4也就是说,我们用次数= 8 的多项式来逼近给定的数据。

#Create a model with degree=8
create_model(x_train,8)

Output[]
Train RMSE(Degree = 8): 0.84
Test RMSE (Degree = 8): 35.44

Listing 1-4Model with degree=8

实际值和预测值绘制在图 1-3 、图 1-4 和图 1-5 中。清单 1-2 中提供了创建模型的源代码(函数)。

我们现在已经有了讨论一般化核心概念的所有细节。要问的关键问题是哪个模型更好——度数= 2 的模型,度数= 8 的模型,还是度数= 1 的模型?让我们首先对这三个模型进行一些观察。与所有其他两个模型相比,度数= 1 的模型在可见和不可见数据上都表现不佳。与度数= 2 的模型相比,度数= 8 的模型在可见数据上表现得更好。对于看不见的数据,度数= 2 的模型比度数= 8 的模型执行得更好。表 1-1 应该有助于阐明模型的解释。

表 1-1

比较三种模型的性能

| - ![img/478491_2_En_1_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-dl-zh/raw/master/docs/dl-py/img/478491_2_En_1_Figa_HTML.jpg) |

我们现在考虑模型容量的重要概念,它对应于本例中多项式的次数。我们生成的数据使用带有一些噪声的二阶多项式(次数= 2)。然后,我们尝试使用三个模型(分别为 1 度、2 度和 8 度)来逼近数据。度数越高,模型的表达能力越强,也就是说,它可以容纳更多的变化。这种适应变化的能力对应于模型容量的概念。也就是说,我们说度= 8 的模型比度= 2 的模型具有更高的容量,而度= 2 的模型又比度= 1 的模型具有更高的容量。拥有更高的容量不总是一件好事吗?当我们考虑到所有真实世界的数据集都包含一些噪声,并且更高容量的模型除了拟合数据中的信号之外,还会拟合噪声时,事实证明并非如此。这就是为什么我们观察到,与度数= 8 的模型相比,度数= 2 的模型在看不见的数据上做得更好。在这个例子中,我们知道数据是如何产生的(用一个二阶多项式(次数= 2)和一些噪声);因此,这个观察是相当琐碎的。然而,在现实世界中,我们不知道数据生成的底层机制。这让我们想到了机器学习中的一个基本挑战:模型真的一般化了吗?唯一真正的测试是对看不见的数据的性能。

在某种意义上,容量的概念对应于模型的简单性或简约性。具有高容量的模型可以近似更复杂的数据。这是模型有多少自由变量/系数。在我们的示例中,度= 1 的模型没有足够的能力来逼近数据。这通常被称为欠拟合。相应地,度数= 8 的模型具有额外的容量,并且过拟合数据。

作为一个思想实验,考虑如果我们有一个度数= 80 的模型会发生什么。假设我们有 80 个数据点作为训练数据,我们将有一个 80 次多项式来完美地逼近数据。这是根本没有学习的终极病理案例。该模型有 80 个系数,可以简单地记忆数据。这被称为死记硬背,过度适应的逻辑极端。这就是为什么模型的容量需要根据我们拥有的训练数据量进行调整。如果数据集很小,我们最好训练容量较低的模型。

正规化

基于模型容量、泛化、过拟合和欠拟合的思想,本节讨论正则化。这里的关键思想是惩罚模型的复杂性。最小二乘法的正则化版本采取的形式是 y = βx ,其中 β 是一个向量,使得$$ {\left\Vert X\beta -y\right\Vert}_2²+\lambda {\left\Vert \beta \right\Vert}_2² $$被最小化,而 λ 是控制复杂度的用户定义参数。在这里,通过引入术语$$ \lambda {\left\Vert \beta \right\Vert}_2² $$,我们正在惩罚复杂的模型。要了解为什么会出现这种情况,请考虑使用 10 次多项式拟合最小二乘模型,但向量 β 中的值有八个零和两个非零。与此相反,考虑向量 β 中的所有值都不为零的情况。出于所有实际目的,前一个模型是度数= 2 的模型,并且具有更低的值$$ \lambda {\left\Vert \beta \right\Vert}_2² $$λ 项使我们能够平衡训练数据的准确性和模型的复杂性。较低的 λ 值意味着更简单的模型。调整 λ 的值,我们可以通过平衡过拟合和欠拟合来提高模型在未知数据上的性能。

清单 1-5 展示了在保持模型系数不变但增加 λ 值的情况下,模型在不可见数据上的性能如何变化。

import matplotlib.pyplot as plt
import numpy as np

#Setting seed for reproducibility
np.random.seed(20)

#Create random data
x = np.linspace(-1,1,100)
signal = 2 + x + 2 * x * x
noise = np.random.normal(0, 0.1, 100)
y = signal + noise
x_train = x[0:80]
y_train = y[0:80]

train_rmse = []
test_rmse = []
degree = 80

#Define

a range of values for lambda
lambda_reg_values = np.linspace(0.01,0.99,100)

for lambda_reg in lambda_reg_values: #For each value of lambda, compute build model and compute performance for lambda_reg in lambda_reg_values:
    X_train = np.column_stack([np.power(x_train,i) for i in range(0,degree)])
    model = np.dot(np.dot(np.linalg.inv(np.dot(X_train.transpose(),X_train) + lambda_reg * np.identity(degree)),X_train.transpose()),y_train)
    predicted = np.dot(model, [np.power(x,i) for i in range(0,degree)])
    train_rmse.append(np.sqrt(np.sum(np.dot(y[0:80] - predicted[0:80], y_train - predicted[0:80]))))
    test_rmse.append(np.sqrt(np.sum(np.dot(y[80:] - predicted[80:], y[80:] - predicted[80:]))))

#Plot the performance over train and test dataset.
plt.plot(lambda_reg_values, train_rmse)
plt.plot(lambda_reg_values, test_rmse)
plt.xlabel(r"$\lambda$")
plt.ylabel("RMSE")
plt.legend(["Train", "Test"], loc = 2)
plt.show()

Listing 1-5Regularization

我们可以利用封闭形式β=(XTXλI)—1XTy来计算 β 的值。在清单 1-5 中,我们展示了将度数固定为值 80 并改变 λ 的值。训练 RMSE(已知数据)和测试 RMSE(未知数据)绘制在图 1-6 中。

img/478491_2_En_1_Fig6_HTML.jpg

图 1-6

正规化

我们看到,随着模型容量的增加,测试 RMSE 逐渐降低到最小值,然后逐渐增加,导致过度拟合。

摘要

本章涵盖了深度学习的简史,并介绍了机器学习的基础,包括监督学习的例子(分类和回归)。本章的重点是对看不见的示例进行概化的概念、训练数据的过拟合和欠拟合、模型的容量以及正则化的概念。鼓励读者尝试源代码清单中的示例。在下一章,我们将探索 PyTorch 作为开发深度学习模型的基础框架*

二、PyTorch 简介

最近几年见证了框架和工具的主要发布,以将深度学习大众化。今天,我们有太多的选择。本章旨在提供 PyTorch 的概述。我们将在整本书中广泛使用 PyTorch 来实现深度学习示例。注意,这一章不是 PyTorch 的全面指南,所以你应该参考这一章中建议的额外资料来更深入地理解这个框架。在本书后面的示例实现过程中,将提供一个基本概述和对该主题的必要补充。

事不宜迟,让我们先回顾一下您在考虑 PyTorch 时可能会遇到的一些更广泛的问题。

为什么我们需要深度学习框架?

开发一个深度神经网络并准备好它来解决今天的问题是一项非常艰巨的任务。在一个系统的流程中,有太多的部分需要连接和编排,以实现我们希望通过深度学习实现的目标。为了能够为研究和产品中的实验提供更简单、更快速、更高质量的解决方案,企业需要大量的抽象来完成繁重的基础任务。这将有助于研究人员和开发人员专注于重要的任务,而不是将大部分时间投入到基本操作上。深度学习框架和平台通过简单的功能提供了地面复杂任务的公平抽象,可以被研究人员和开发人员用作解决更大问题的工具。几个流行的选择是 Keras,PyTorch,TensorFlow,MXNet,Caffe,微软的 CNTK 等。

PyTorch 是什么?

PyTorch 是由脸书公司开发的开源机器学习和深度学习库。顾名思义,它是基于 Python 的,旨在通过提供 GPU 的无缝使用和提供最大灵活性和速度的深度学习平台,为 NumPy(在本章的示例中使用)提供更快的替代/替换。

为什么选择 PyTorch?

推荐 PyTorch 很简单。它提供了一个非常容易使用、扩展、开发和调试的框架。因为它是 Pythonic 化的,所以很容易被软件工程社区接受。对于研究人员和开发人员来说,完成任务同样容易。PyTorch 还使得深度学习模型易于生产化。它配备了高性能的 C++运行时,开发人员可以在生产环境中使用,同时避免通过 Python 进行推理。对于大多数熟悉 Python 的 NumPy 包的用户来说,PyTorch 将更容易过渡到。总的来说,PyTorch 为研究人员和开发人员提供了一个优秀的框架和平台,让他们在专注于重要任务的同时解决前沿的深度学习问题,并能够轻松地进行调试、实验和部署。

由于上述原因,PyTorch 在企业中被广泛采用。如果你关注深度学习的媒体,你可能会读到一些文章,提到一个新的大型组织采用 PyTorch。Yann Lecun 是深度学习领域的资深研究员,NYU 大学教授,脸书大学首席科学家(撰写本文时)在 2019 年 11 月发了以下推文:

“neur IPS 的 19 篇提到使用深度学习框架的论文中,超过 69%提到 PyTorch。PyTorch 在深度学习研究(ML/CV/NLP 会议)中遥遥领先。

有了足够的理由证明 PyTorch 是深度学习的一个值得选择,让我们开始吧。

这一切都始于张量

一般来说,深度学习中的任务将围绕处理图像、文本或表格数据(横截面以及时间序列)来生成结果,该结果是数字、标签、更多文本、另一个图像或这些的组合。简单的例子包括将图像分类为狗或猫,预测句子中的下一个单词,为图像生成标题,或者用新样式转换图像(比如 iOS/Android 上的 Prisma 应用程序)。这些任务中的每一项都需要将底层数据存储在特定的结构中。处理和开发这些解决方案将有几个中间阶段,这也需要特定的结构(例如,神经网络的权重)。一个通用于存储、表示和转换的结构是张量。

张量只不过是同一类型(通常是浮点数)对象的多维数组。虽然有点过于简化,但公平地说,在较低的抽象层次上,PyTorch 中的所有计算都是张量和张量上的运算。因此,为了让你能够流利地使用 PyTorch,你有必要对张量及其运算有一个直观的理解。还必须指出,这种对张量及其运算的介绍决不是完整的;您应该参考 PyTorch 文档来了解具体的用例。然而,指出这一章涵盖了张量及其运算的所有概念方面也是必要的。您应该在 Python 终端中尝试本节中的示例。(推荐 Jupyter 笔记本。)内化这种材料的最好方法是阅读概念,打出源代码,然后看着它执行。

张量是表示标量、矢量和矩阵的一种通用方式。张量可以定义为一个 n 维矩阵。一个 0 维张量(即单个数)叫做标量(图 2-1);一维张量被称为矢量;二维张量被称为矩阵;三维张量也叫做立方体;等等。矩阵的维数也称为张量的秩。

img/478491_2_En_2_Fig1_HTML.jpg

图 2-1

0-n 维张量

PyTorch 是一个非常丰富的库,提供了许多功能,为深度学习提供了基础。本章简要介绍 PyTorch 为创建张量和执行数据管理操作、线性代数和数学运算提供的一些功能。

首先,让我们探索构造张量的多种方法。最基本的方法是使用 Python 中的列表来构造张量。下面的练习将演示一系列常用于构建深度学习应用程序的张量运算。为了帮助您更好地参与流程,代码和输出一直保持笔记本风格(交互流程:输入➤输出➤下一个输入➤下一个输出➤等等)。

创建张量

在清单 2-1 中,我们使用嵌套列表构建了一个二维张量。我们把这个张量存储为一个变量,然后看它的形状。

In [1]: import torch
           torch.tensor([[0.1, 0.2],[0.3, 0.4]])
Out[1]:
tensor([[0.1000, 0.2000],
            [0.3000, 0.4000]])

Listing 2-1Creating a 2-Dimensional Tensor

形状表示张量的维数和用于推断张量秩的维数总数。在清单 2-2 中,dimension [2,2]将被推断为秩 2。

清单 2-2 探究了张量的形状。

In [1]: a = torch.tensor([[0.1, 0.2],[0.3, 0.4]])
In [2]: a.shape

Out[2]: torch.Size([2, 2])

In [3]: a
Out[3]:
tensor([[0.1000, 0.2000],
              [0.3000, 0.4000]])

Listing 2-2The Shape of a Tensor

我们可以尝试更多不同形状的例子。清单 2-3 探究不同形状的张量。

In [1]: b = torch.tensor([[0.1, 0.2],[0.3, 0.4],[0.5, 0.6]])

In [2]: b

Out[2]:
tensor([[0.1000, 0.2000],
        [0.3000, 0.4000],
        [0.5000, 0.6000]])

In [3]: b.shape
Out[3]: torch.Size([3, 2])

Listing 2-3The shape of a tensor (continued)

还要注意,我们可以有任意维数的张量,而不仅仅是两个(如前面的例子)。清单 2-4 展示了三维张量的创建。

In [1]: c = torch.tensor([[[0.1],[0.2]],[[0.3],[0.4]]])

In [2]: c.shape
Out[2]: torch.Size([2, 2, 1])

In [3]: c
Out[3]:
tensor([[[0.1000],
         [0.2000]],
         [[0.3000],
         [0.4000]]])

Listing 2-4Creating Tensors with Arbitrary Dimensions

正如我们可以用 Python 列表构建张量一样,我们也可以用 NumPy 数组构建张量。在将 NumPy 代码与 PyTorch 进行交互时,这一功能非常方便。清单 2-5 演示了使用 NumPy 创建张量。

In [1]: a = torch.tensor(numpy.array([[0.1, 0.2],[0.3, 0.4]]))

In [2]: a
Out[2]:
tensor([[0.1000, 0.2000],
        [0.3000, 0.4000]], dtype=torch.float64)

In [3]: a.shape
Out[3]: torch.Size([2, 2])

Listing 2-5Creating Tensors with NumPy

我们还可以使用from_numpy函数从现有的 NumPy n 维数组中创建一个张量。清单 2-6 演示了使用 PyTorch 的内置函数from_numpy从 NumPy 创建张量。

import numpy as np
a = np.array([1, 2, 3, 4, 5])
tensor_a = torch.from_numpy(a)
tensor_a

Output[]
tensor([1, 2, 3, 4, 5])

Listing 2-6Creating Tensors from NumPy

正如我们在引言中提到的,张量是同类型的多维数组。我们可以在构造张量时指定类型。在下面的例子中,我们用 32 位浮点数、64 位浮点数和 16 位浮点数初始化张量。PyTorch 总共定义了八种类型。(有关更多详细信息,请参考 PyTorch 文档。)清单 2-7 演示了用 PyTorch 中可用的几种流行数据类型来构造张量。

In [1]: a = torch.tensor([[0.1, 0.2],[0.3, 0.4]], dtype=torch.float32)

In [2]: a
Out[2]:
tensor([[0.1000, 0.2000],
        [0.3000, 0.4000]])

In [3]: a = torch.tensor([[0.1, 0.2],[0.3, 0.4]], dtype=torch.float64)

In [4]: a
Out[4]:
tensor([[0.1000, 0.2000],
        [0.3000, 0.4000]], dtype=torch.float64)

In [5]: a = torch.tensor([[0.1, 0.2],[0.3, 0.4]], dtype=torch.float16)

In [6]: a
Out[6]:
tensor([[0.1000, 0.2000],
        [0.3000, 0.3999]], dtype=torch.float16)

Listing 2-7Defining Tensor Datatypes

表 2-1 显示了不同的数据类型及其 PyTorch 等价物。

表 2-1

数据类型及其 PyTorch 等价物

|

数据类型

|

PyTorch 当量

|
| --- | --- |
| 32 位浮点 | torch.float32 或 torch.float |
| 64 位浮点 | torch.float64 或 torch.double |
| 16 位浮点 | 火炬.浮动 16 或火炬.半 |
| 8 位整数(无符号) | torch.uint8 |
| 8 位整数(有符号) | torch.int8 |
| 16 位整数(有符号) | torch.int16 或 torch.short |
| 32 位整数(有符号) | torch.int32 或 torch.int |
| 64 位整数(有符号) | torch.int64 或 torch.long |
| 布尔代数学体系的 | 火炬.布尔 |

现在让我们看看构造张量的其他方法。一个常见的需求是构造一个用随机值填充的张量。清单 2-8 演示了创建一个具有随机值的定义形状的张量。

In [1]: r = torch.rand(2,2,2)

In [2]: r
Out[2]:
tensor([[[0.7993, 0.5940],
         [0.3994, 0.7134]],

         [[0.3102, 0.5175],
         [0.6510, 0.7272]]])

In [3]: r.shape
Out[3]: torch.Size([2, 2, 2])

Listing 2-8Creating a Tensor with Random Values

另一个常见的要求是构造一个零张量。清单 2-9 展示了一个定义了全零形状的张量的创建。

In [1]: zeros = torch.zeros(2,2,3)

In [2]: zeros
Out[2]:
tensor([[[0., 0., 0.],
         [0., 0., 0.]],

         [[0., 0., 0.],
         [0., 0., 0.]]])

In [3]: zeros.shape
Out[3]: torch.Size([2, 2, 3])

Listing 2-9Creating

a Tensor Having All Zeros

同样,我们可以构造一个 1 的张量。清单 2-10 演示了创建一个定义了全 1 形状的张量。

In [1]: ones = torch.ones(2,2,3)

In [2]: ones
Out[2]:
tensor([[[1., 1., 1.],
         [1., 1., 1.]],

         [[1., 1., 1.],
         [1., 1., 1.]]])

In [3]: ones.shape
Out[3]: torch.Size([2, 2, 3])

Listing 2-10Creating a Tensor Having All Ones

另一个常见的需求是构造单位矩阵(张量)。清单 2-11 展示了一个单位矩阵张量的创建(即所有对角元素为 1)。

In [1] i = torch.eye(3)

In [2]: i
Out[2]:
tensor([[1., 0., 0.],
        [0., 1., 0.],
        [0., 0., 1.]])

In [3]: i.shape
Out[3]: torch.Size([3, 3])

Listing 2-11Creating an Identity Matix Tensor

我们也可以构造一个填充了任意值的任意形状的张量。清单 2-12 展示了一个任意值张量的创建。

In [1]: f = torch.full((3,3), 0.42)

In [2]: f
Out[2]:
tensor([[0.4200, 0.4200, 0.4200],
        [0.4200, 0.4200, 0.4200],
        [0.4200, 0.4200, 0.4200]])

In [3]: f.shape
Out[3]: torch.Size([3, 3])

Listing 2-12Creating a Tensor Filled with an Arbitrary Value

一个常见的用例也是用线性间隔的浮点数构建张量。清单 2-13 展示了使用线性间隔浮点数创建张量。

In [1]: lin = torch.linspace(0, 20, steps=5)

In [2]: lin
Out[2]: tensor([ 0.,  5., 10., 15., 20.])

Listing 2-13Creating a Tensor with Linearly Spaced Floating-Point Numbers

类似地,清单 2-14 展示了用对数间隔浮点数构建张量。

In [1]: log = torch.logspace(-3, 3, steps=4)

In [2]: log
Out[2]: tensor([1.0000e-03, 1.0000e-01, 1.0000e+01, 1.0000e+03])

Listing 2-14Creating a Tensor with Logarithmically Spaced Floating-Point Numbers

有时我们需要创建维度与现有张量相似的张量。清单 2-15 中的例子说明了这一点。

In [1]: a = torch.tensor([[0.5, 0.5],[0.5, 0.5]])

In [2]: b = torch.zeros_like(a)

In [3]: b
Out[3]:
tensor([[0., 0.],
        [0., 0.]])

In [4]: c = torch.ones_like(a)

In [5]: c
Out[5]:
tensor([[1., 1.],
        [1., 1.]])

Listing 2-15Creating a Tensor with Dimensions Similar to Another Tensor

到目前为止,我们只考虑了浮点数。然而,PyTorch 张量并不局限于浮点数。下面是几个用整数和长整数构造张量的例子。顺便提一下,注意到dtype函数可以用来找到张量包含的对象的类型。清单 2-16 演示了创建一个整数数据类型的张量。

In [1]: i = torch.tensor([[1,2],[3,4]])

In [2]: i
Out[2]:
tensor([[1, 2],
        [3, 4]])

In [3]: i.dtype
Out[3]: torch.int64

In [4]: i = torch.tensor([[1,2],[3,4]], dtype=torch.int)

In [5]: i
Out[5]:
tensor([[1, 2],
        [3, 4]], dtype=torch.int32)

Listing 2-16Creating a Tensor with Integer Datatypes

类似地,清单 2-17 展示了具有整数范围的张量的构造。

In [1]: a = torch.arange(1,10, step=2)

In [2]: a
Out[2]: tensor([1, 3, 5, 7, 9])

Listing 2-17Creating a Tensor with a Range of Integers

同样,我们可以构造整数的随机排列。在清单 2-18 中,我们创建了一个整数随机排列的张量。

In [1]: r = torch.randperm(10)

In [2]: r
Out[2]: tensor([5, 3, 0, 2, 8, 1, 7, 4, 6, 9])

Listing 2-18Creating a Tensor with a Random Permutation of Integers

张量蒙格运算

看过张量和张量构造运算之后,现在让我们更深入地研究张量运算。我们将从访问张量的单个元素开始。下面的例子应该很熟悉,因为它与 Python 中的列表索引操作符相同。清单 2.19 演示了如何访问张量的单个成员。

In [1]: a = torch.tensor([[1,2],[3,4]])

In [2]: a
Out[2]:
tensor([[1, 2],
        [3, 4]])

In [3]: a[0][0]
Out[3]: tensor(1)

In [4]: a[0][1]
Out[4]: tensor(2)

In [5]: a[1][0]
Out[5]: tensor(3)

In [6]: a[1][1]
Out[6]: tensor(4)

In [7]: a.shape
Out[7]: torch.Size([2, 2])

Listing 2-19Accessing Individual Members of a Tensor

要提取仅包含单个值的张量中的数据,应使用item方法。清单 2-20 演示了从张量中访问单个值。

In [1]: a = torch.tensor([[[0.42]]])

In [2]: a
Out[2]: tensor([[[0.4200]]])

In [3]: a.shape
Out[3]: torch.Size([1, 1, 1])

In [4]: a.item()
Out[4]: 0.41999998688697815

Listing 2-20Accessing a Single Value from a Tensor

view方法提供了一种重塑张量的简单方法。本质上,张量中的值被分配在连续的内存块中。PyTorch 张量本质上只是这个连续块上的一个视图。多个索引可以引用同一个存储,并以不同的形状表示张量。清单 2-21 展示了一个重塑张量的简单例子。

In [1]: a = torch.zeros(10)

In [2]: a
Out[2]: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

In [3]: a.shape
Out[3]: torch.Size([10])

In [4]: b = a.view(2,5)

In [5]: b
Out[5]:
tensor([[0., 0., 0., 0., 0.],
            [0., 0., 0., 0., 0.]])

In [6]: b.shape
Out[6]: torch.Size([2, 5])

Listing 2-21Reshaping a Tensor

注意view方法如何重塑张量(元素放置的顺序)是很重要的。清单 2-22 演示了用“视图”方法重新整形后检验张量的大小。

In [1]: a = torch.arange(1,10)

In [2]: a
Out[2]: tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])

In [3]: a.shape
Out[3]: torch.Size([9])

In [4]: b = a.view(3,3)

In [5]: b
Out[5]:
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

In [6]: b.shape
Out[6]: torch.Size([3, 3])

Listing 2-22Verifying the Size of a Tensor After Reshaping with view

cat操作允许你沿着一个给定的维度连接一个张量列表。注意,cat操作有两个参数:要连接的张量列表和执行该操作的维度。清单 2-23 探究了两个张量的连接。

In [1]: a = torch.zeros(2,2)

In [2]: a
Out[2]:
tensor([[0., 0.],
        [0., 0.]])

In [3]: a.shape
Out[3]: torch.Size([2, 2])

In [4]: b = torch.cat([a,a,a],0)

In [5]: b
Out[5]:
tensor([[0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.]])

In [6]: b.shape
Out[6]: torch.Size([6, 2])

In [7]: c = torch.cat([a,a,a],1)

In [8]: c
Out[8]:
tensor([[0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0.]])

In [9]: c.shape
Out[9]: torch.Size([2, 6])

Listing 2-23Concatenating Two Tensors

stack操作允许你通过沿着一个维度堆叠一系列张量来构造一个张量。合成张量的维数将增加一。清单 2-24 显示了堆叠操作如何沿着每个维度进行。注意,stack操作需要两个参数:张量列表和堆叠维度。维数的范围等于要叠加的张量的范围。

In [1]: a = torch.zeros(2,1)

In [2]: a
Out[2]:
tensor([[0.],
        [0.]])

In [3]: a.shape
Out[3]: torch.Size([2, 1])

In [4]: b = torch.stack([a,a,a], 0)

In [5]: b
Out[5]:
tensor([[[0.],
         [0.]],

         [[0.],
         [0.]],

         [[0.],
         [0.]]])

In [6]: b.shape
Out[6]: torch.Size([3, 2, 1])

In [7]: c = torch.stack([a,a,a], 1)

In [8]: c
Out[8]:
tensor([[[0.],
         [0.],
         [0.]],

         [[0.],
         [0.],
         [0.]]])

In [9]: c.shape
Out[9]: torch.Size([2, 3, 1])

In [10]: d = torch.stack([a,a,a], 2)

In [11]: d
Out[11]:
tensor([[[0., 0., 0.]],

        [[0., 0., 0.]]])

In [12]: d.shape
Out[12]: torch.Size([2, 1, 3])

Listing 2-24Stacking Tensors

chunk操作将张量沿给定方向分割成给定数量的部分。注意,第一个参数是张量;第二个参数是零件的数量;第三个参数是分区的方向。清单 2-25 演示了分块张量。

In [1]: a = torch.zeros(10, 1)

In [2]: a
Out[2]:
tensor([[0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.]])

In [3]: a.shape
Out[3]: torch.Size([10, 1])

In [4]: b = torch.chunk(a, 5, 0)

In [5]: b
Out[5]:
(tensor([[0.], [0.]]),
 tensor([[0.], [0.]]),
 tensor([[0.], [0.]]),
 tensor([[0.], [0.]]),
 tensor([[0.], [0.]]))

Listing 2-25Chunking Tensors

注意,当张量沿执行划分的维度的长度不是部分大小的倍数时,最后一个部分的元素比部分大小少。清单 2-26 展示了张量分块/截断的其他例子。

In [1]: d = torch.chunk(a, 3, 0)
In [2]: d

Out[2]:
(tensor([[0.],
         [0.],
         [0.],
         [0.]]),
 tensor([[0.],
         [0.],
         [0.],
         [0.]]),
 tensor([[0.],
         [0.]]))

Listing 2-26Chunking Tensors (continued)

正如chunk方法使你能够将一个张量分割成给定数量的部分一样,split方法做同样的操作,但是给定部分的大小。请注意不同之处。基本上,chunk方法获取零件的数量,而split方法获取零件的尺寸。清单 2-27 展示了分裂张量。

In [1]: a = torch.zeros(10,1)

In [2]: a
Out[2]:
tensor([[0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.],
        [0.]])

In [3]: a.shape
Out[3]: torch.Size([10, 1])

In [4]: b = torch.split(a,2,0)

In [5]: b
Out[5]:
(tensor([[0.],[0.]]),
 tensor([[0.],[0.]]),
 tensor([[0.],[0.]]),
 tensor([[0.],[0.]]),
 tensor([[0.],[0.]]))

Listing 2-27Splitting Tensors

index_select方法允许你沿着给定的维度提取部分张量。注意,该方法有三个参数:要操作的张量、提取数据的维度和包含索引的张量。在清单 2-28 中,我们构建了一个 3x3 张量,然后沿着两个维度中的每一个提取数据。

In [1]: a = torch.FloatTensor([[1 ,2, 3],[4, 5, 6], [7, 8, 9]])

In [2]: a
Out[2]:
tensor([[1., 2., 3.],
        [4., 5., 6.],
        [7., 8., 9.]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: index = torch.LongTensor([0, 1])

In [5]: b = torch.index_select(a, 0, index)

In [6]: b
Out[6]:
tensor([[1., 2., 3.],
        [4., 5., 6.]])

In [7]: b.shape
Out[7]: torch.Size([2, 3])

In [8]: c = torch.index_select(a, 1, index)

In [9]: c
Out[9]:
tensor([[1., 2.],
        [4., 5.],
        [7., 8.]])

In [10]: c.shape
Out[10]: torch.Size([3, 2])

Listing 2-28Extracting Parts of Tensors Using index_select

清单 2-29 中展示的masked_select方法允许你选择给定布尔掩码的元素。

In [1]: a = torch.FloatTensor([[1 ,2, 3],[4, 5, 6], [7, 8, 9]])

In [2]: a
Out[2]:
tensor([[1., 2., 3.],
        [4., 5., 6.],
        [7., 8., 9.]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: mask = torch.ByteTensor([[0, 1, 0],[1, 1, 1],[0, 1, 0]])

In [5]: mask
Out[5]:
tensor([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=torch.uint8)

In [6]: mask.shape
Out[6]: torch.Size([3, 3])

In [7]: b = torch.masked_select(a, mask)

In [8]: b
Out[8]: tensor([2., 4., 5., 6., 8.])

In [9]: b.shape
Out[9]: torch.Size([5])

Listing 2-29Selecting Elements from a Tensor Using masked_select

squeeze方法删除所有值为 1 的维度,如清单 2-30 所示。

In [1]: a = torch.zeros(2,2,1)

In [2]: a
Out[2]:
tensor([[[0.],
         [0.]],

         [[0.],
          [0.]]])

In [3]: a.shape
Out[3]: torch.Size([2, 2, 1])

In [4]: b = a.squeeze()

In [5]: b
Out[5]:
tensor([[0., 0.],
        [0., 0.]])

In [6]: b.shape
Out[6]: torch.Size([2, 2])

Listing 2-30Reshaping a Tensor with the squeeze Method

类似地,unsqueeze方法添加一个值为 1 的新维度,如清单 2-31 所示。请注意如何在三个不同的位置添加额外的维度。

In [1]: a = torch.zeros(2,2)

In [2]: a
Out[2]:
tensor([[0., 0.],
        [0., 0.]])

In [3]: a.shape
Out[3]: torch.Size([2, 2])

In [4]: b = torch.unsqueeze(a, 0)

In [5]: b
Out[5]:
tensor([[[0., 0.],
         [0., 0.]]])

In [6]: b.shape
Out[6]: torch.Size([1, 2, 2])

In [7]: c = torch.unsqueeze(a, 1)

In [8]: c
Out[8]:
tensor([[[0., 0.]],

        [[0., 0.]]])

In [9]: c.shape
Out[9]: torch.Size([2, 1, 2])

In [10]: d = torch.unsqueeze(a, 2)

In [11]: d
Out[11]:
tensor([[[0.],
         [0.]],

        [[0.],
        [0.]]])

In [12]: d.shape
Out[12]: torch.Size([2, 2, 1])

Listing 2-31Reshaping a Tensor with the unsqueeze Method

unbind函数将给定的张量分解成沿给定维度的独立张量。清单 2-32 展示了使用unbind提取张量的各个部分。一个 3×3 张量沿着第一和第二维分解。注意,结果张量作为元组返回。

In [1]: a
Out[1]:
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

In [2]: a.shape
Out[2]: torch.Size([3, 3])

In [3]: torch.unbind(a, 0)
Out[3]: (tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9]))

In [4]: torch.unbind(a, 1)
Out[4]: (tensor([1, 4, 7]), tensor([2, 5, 8]), tensor([3, 6, 9]))

Listing 2-32Extracting Parts of a Tensor using unbind

清单 2-33 展示了使用where方法从现有张量创建一个张量。

In [1]: a = torch.zeros(3,3)

In [2]: a
Out[2]:
tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: b = torch.ones(3,3)

In [5]: b
Out[5]:
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])

In [6]: b.shape
Out[6]: torch.Size([3, 3])

In [7]: c = torch.rand(3,3)

In [8]: c
Out[8]:
tensor([[0.8452, 0.8095, 0.5903],
        [0.7766, 0.6845, 0.4232],
        [0.1080, 0.1946, 0.7541]])

In [9]: c.shape
Out[9]: torch.Size([3, 3])

In [10]: d = torch.where(c > 0.5, a, b)

In [11]: d
Out[11]:
tensor([[0., 0., 0.],
        [0., 0., 1.],
        [1., 1., 0.]])

In [12]: d.shape
Out[12]: torch.Size([3, 3])

Listing 2-33Constructing a Tensor from an Existing Tensor Using the where Method

清单 2-34 中所示的anyall方法分别使您能够检查给定条件在任何或所有情况下是否为真。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.3447, 0.4243, 0.6950],
        [0.8801, 0.8502, 0.7759],
        [0.6685, 0.9172, 0.4557]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: torch.any(a > 0)
Out[4]: tensor(1, dtype=torch.uint8)

In [5]: torch.any(a > 1.0)
Out[5]: tensor(0, dtype=torch.uint8)

In [6]: torch.all(a > 0)
Out[6]: tensor(1, dtype=torch.uint8)

In [7]: torch.all(a > 1.0)
Out[7]: tensor(0, dtype=torch.uint8)

Listing 2-34Conducting Logical Operations on Tensors Using the any and all Methods

view方法允许你重塑张量。清单 2-35 展示了重塑张量。请注意,使用-1 作为某个维度的大小意味着这是根据其他大小推断出来的。

In [1]: a = torch.arange(1,10)

In [2]: a
Out[2]: tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])

In [3]: b = a.view(3,3)

In [4]: b
Out[4]:
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

In [5]: b.shape
Out[5]: torch.Size([3, 3])

In [6]: c = a.view(3,-1)

In [7]: c
Out[7]:
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

In [8]: c.shape
Out[8]: torch.Size([3, 3])

Listing 2-35Reshaping tensors

flatten方法可用于从特定维度开始折叠给定张量的维度。清单 2-36 演示了使用flatten折叠张量的维度。

In [1]: a
Out[1]:
tensor([[[[1., 1.],
          [1., 1.]],

          [[1., 1.],
          [1., 1.]]],

           [[[1., 1.],
           [1., 1.]],

           [[1., 1.],
           [1., 1.]]]])

In [2]: a.shape
Out[2]: torch.Size([2, 2, 2, 2])

In [3]: b = torch.flatten(a)

In [4]: b
Out[4]: tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])

In [5]: b.shape
Out[5]: torch.Size([16])

In [6]: c = torch.flatten(a, start_dim=0)

In [7]: c
Out[7]: tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])

In [8]: c.shape
Out[8]: torch.Size([16])

In [9]: d = torch.flatten(a, start_dim=1)

In [10]: d
Out[10]:
tensor([[1., 1., 1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1., 1., 1.]])

In [11]: d.shape
Out[11]: torch.Size([2, 8])

In [12]: e = torch.flatten(a, start_dim=2)

In [13]: e
Out[13]:
tensor([[[1., 1., 1., 1.],
         [1., 1., 1., 1.]],

         [[1., 1., 1., 1.],
          [1., 1., 1., 1.]]])

In [14]: e.shape
Out[14]: torch.Size([2, 2, 4])

In [15]: f = torch.flatten(a, start_dim=3)

In [16]: f
Out[16]:
tensor([[[[1., 1.],
          [1., 1.]],

          [[1., 1.],
           [1., 1.]]],

           [[[1., 1.],
            [1., 1.]],

           [[1., 1.],
           [1., 1.]]]])

In [17]: f.shape
Out[17]: torch.Size([2, 2, 2, 2])

Listing 2-36Collapsing the Dimensions of a Tensor Using the flatten Method

gather方法允许我们在给定的位置沿着给定的维度从张量中提取值。清单 2-37 展示了使用gather从张量中提取值。

In [1]: a = torch.rand(4,4)

In [2]: a
Out[2]:
tensor([[0.6212, 0.7720, 0.8867, 0.4805],
        [0.0323, 0.7763, 0.2295, 0.8778],
        [0.5836, 0.3244, 0.3011, 0.5630],
        [0.6748, 0.4487, 0.7052, 0.7185]])

In [3]: a.shape
Out[3]: torch.Size([4, 4])

In [4]: b = torch.LongTensor([[0,1,2,3]])

In [5]: b
Out[5]: tensor([[0, 1, 2, 3]])

In [6]: b.shape
Out[6]: torch.Size([1, 4])

In [7]: c = a.gather(0,b)

In [8]: c
Out[8]: tensor([[0.6212, 0.7763, 0.3011, 0.7185]])

In [9]: c.shape
Out[9]: torch.Size([1, 4])

In [10]: d = torch.LongTensor([[0],[1],[2],[3]])

In [11]: d
Out[11]:
tensor([[0],
        [1],
        [2],
        [3]])

In [12]: d.shape
Out[12]: torch.Size([4, 1])

In [13]: e = a.gather(1,d)

In [14]: e
Out[14]:
tensor([[0.6212],
        [0.7763],
        [0.3011],
        [0.7185]])

In [15]: e.shape
Out[15]: torch.Size([4, 1])

Listing 2-37Extracting Values from a Tensor Using the gather Method

类似地,scatter方法可用于将值放入给定位置的给定维度的张量中。清单 2-38 展示了用scatter增加张量的值。

In [1]: a = torch.rand(4,4)

In [2]: a
Out[2]:
tensor([[0.7159, 0.4922, 0.2732, 0.5839],
        [0.0961, 0.9103, 0.9450, 0.6140],
        [0.9439, 0.3156, 0.3493, 0.3125],
        [0.1578, 0.1555, 0.6266, 0.4961]])

In [3]: a.shape
Out[3]: torch.Size([4, 4])

In [4]: index = torch.LongTensor([[0,1,2,3]])

In [5]: index
Out[5]: tensor([[0, 1, 2, 3]])

In [6]: index.shape
Out[6]: torch.Size([1, 4])

In [7]: values = torch.zeros(1,4)

In [8]: values
Out[8]: tensor([[0., 0., 0., 0.]])

In [9]: values.shape
Out[9]: torch.Size([1, 4])

In [10]: result = a.scatter(0, index, values)

In [11]: result
Out[11]:
tensor([[0.0000, 0.4922, 0.2732, 0.5839],
       [0.0961, 0.0000, 0.9450, 0.6140],
       [0.9439, 0.3156, 0.0000, 0.3125],
       [0.1578, 0.1555, 0.6266, 0.0000]])

In [12]: result.shape
Out[12]: torch.Size([4, 4])

In [13]: a
Out[13]:
tensor([[0.7159, 0.4922, 0.2732, 0.5839],
        [0.0961, 0.9103, 0.9450, 0.6140],
        [0.9439, 0.3156, 0.3493, 0.3125],
        [0.1578, 0.1555, 0.6266, 0.4961]])

Listing 2-38Augmenting a Tensor’s Values Using the scatter Method

数学运算

allclose方法允许我们在给定绝对或相对容差水平的情况下,检查两个张量中的值是否相同。这种方法帮助我们根据误差范围比较两个张量,在编写单元测试时非常方便。清单 2-39 说明了在公差水平内验证张量。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.9854, 0.2305, 0.1023],
        [0.2054, 0.7064, 0.6115],
        [0.6231, 0.0024, 0.8337]])

In [3]: b = a + a * 1e-3

In [4]: b
Out[4]:
tensor([[0.9864, 0.2307, 0.1024],
        [0.2056, 0.7071, 0.6121],
        [0.6237, 0.0024, 0.8345]])

In [5]: torch.allclose(a,b,rtol=1e-1)
Out[5]: True

In [6]: torch.allclose(a,b,rtol=1e-2)
Out[6]: True

In [7]: torch.allclose(a,b,rtol=1e-3)
Out[7]: True

In [8]: torch.allclose(a,b,rtol=1e-4)
Out[8]: False

In [9]: torch.allclose(a,b,atol=1e-1)
Out[9]: True

In [10]: torch.allclose(a,b,atol=1e-2)
Out[10]: True

In [11]: torch.allclose(a,b,atol=1e-3)
Out[11]: True

In [12]: torch.allclose(a,b,atol=1e-4)
Out[12]: False

Listing 2-39Validating Whether Given Tensors Are Within a Tolerance Level

argmaxargmin方法允许您获得给定维度上最大值和最小值的索引。清单 2-40 展示了提取张量中最小和最大值的维数。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.6295, 0.0995, 0.9350],
        [0.7498, 0.7338, 0.2076],
        [0.2302, 0.7524, 0.1993]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: torch.argmax(a, dim=0)
Out[4]: tensor([1, 2, 0])

In [5]: torch.argmax(a, dim=1)
Out[5]: tensor([2, 0, 1])

In [6]: torch.argmin(a, dim=0)
Out[6]: tensor([2, 0, 2])

In [7]: torch.argmin(a, dim=1)
Out[7]: tensor([1, 2, 2])

Listing 2-40Extracting Dimensions of Minimum and Maximum Values in a Given Tensor

类似地,清单 2-41 中所示的 argsort 函数给出了给定维度上排序值的索引。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.8380, 0.0738, 0.1025],
        [0.7930, 0.5986, 0.9059],
        [0.2777, 0.9390, 0.0700]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: torch.argsort(a, dim=0)
Out[4]:
tensor([[2, 0, 2],
        [1, 1, 0],
        [0, 2, 1]])

In [5]: torch.argsort(a, dim=1)
Out[5]:
tensor([[1, 2, 0],
        [1, 0, 2],
        [2, 0, 1]])

Listing 2-41Extracting the Indices of Sorted Values of a Tensor

清单 2-42 中展示的cumsum方法允许您计算给定维度上的累积和。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.2221, 0.7963, 0.5464],
        [0.9116, 0.3773, 0.5860],
        [0.5363, 0.7378, 0.3079]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: b = torch.cumsum(a, dim=0)

In [5]: b
Out[5]:
tensor([[0.2221, 0.7963, 0.5464],
        [1.1337, 1.1736, 1.1324],
        [1.6700, 1.9113, 1.4403]])

In [6]: b.shape
Out[6]: torch.Size([3, 3])

In [7]: c = torch.cumsum(a, dim=1)

In [8]: c
Out[8]:
tensor([[0.2221, 1.0183, 1.5647],
        [0.9116, 1.2889, 1.8749],
        [0.5363, 1.2741, 1.5820]])

In [9]: c.shape
Out[9]: torch.Size([3, 3])

Listing 2-42Computing the Cumulative Sum Along a Dimension of the Tensor

类似地,cumprod方法允许您计算给定维度上的累积积。清单 2-43 说明了累积积的计算。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.6971, 0.0358, 0.4075],
        [0.2239, 0.2938, 0.3418],
        [0.2482, 0.2108, 0.0709]])

In [3]: a.shape
Out[3]: torch.Size([3, 3])

In [4]: b = torch.cumprod(a, dim=0)

In [5]: b
Out[5]:
tensor([[0.6971, 0.0358, 0.4075],
        [0.1561, 0.0105, 0.1393],
        [0.0388, 0.0022, 0.0099]])

In [6]: b.shape
Out[6]: torch.Size([3, 3])

In [7]: c = torch.cumprod(a, dim=1)

In [8]: c
Out[8]:
tensor([[0.6971, 0.0250, 0.0102],
        [0.2239, 0.0658, 0.0225],
        [0.2482, 0.0523, 0.0037]])

In [9]: c.shape
Out[9]: torch.Size([3, 3])

Listing 2-43Computing the Cumulative Product Along a Dimension of the Tensor

abs方法允许你计算给定张量元素的绝对值。清单 2-44 展示了张量元素绝对值的计算。

In [1]: a = torch.tensor([[1,-1,1],[1,-1,1],[1,-1,1]])

In [2]: a
Out[2]:
tensor([[ 1, -1,  1],
        [ 1, -1,  1],
        [ 1, -1,  1]])

In [3]: b = torch.abs(a)

In [4]: b
Out[4]:
tensor([[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]])

Listing 2-44Computing the Absolute Value of the elements of a Tensor

clamp功能允许您将元素限制在给定的最小值和最大值之间。清单 2-45 显示了张量内的箝位值。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.1181, 0.2922, 0.6639],
        [0.9170, 0.1552, 0.3636],
        [0.8511, 0.9194, 0.4650]])

In [3]: b = torch.clamp(a, min=0.25, max=0.50)

In [4]: b
Out[4]:
tensor([[0.2500, 0.2922, 0.5000],
        [0.5000, 0.2500, 0.3636],
        [0.5000, 0.5000, 0.4650]])

Listing 2-45Clamping Values Within a Tensor

ceilfloor函数允许你上舍入或下舍入给定张量的元素,如清单 2-46 所示。

In [1]: a = torch.rand(3,3) * 100

In [2]: a
Out[2]:
tensor([[18.6809, 56.6616, 10.2362],
        [74.1378, 87.3797, 62.9137],
        [42.4275, 82.0347, 96.2187]])

In [3]: b = torch.floor(a)

In [4]: b
Out[4]:
tensor([[18., 56., 10.],
        [74., 87., 62.],
        [42., 82., 96.]])

In [5]: c = torch.ceil(a)

In [6]: c
Out[6]:
tensor([[19., 57., 11.],
        [75., 88., 63.],
        [43., 83., 97.]])

Listing 2-46Ceil and floor operations within a tensor

元素式数学运算

现在让我们来看看一些基于元素的数学运算。这些运算被称为逐元素的数学运算,因为对张量的每个元素执行相同的运算。

mul函数允许你执行元素乘法,如清单 2-47 所示。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.6589, 0.9292, 0.0315],
        [0.6033, 0.1030, 0.1090],
        [0.4076, 0.7149, 0.8323]])

In [3]: b = torch.FloatTensor([[0, 1, 0],[1,1,1],[0,1,0]])

In [4]: b
Out[4]:
tensor([[0., 1., 0.],
        [1., 1., 1.],
        [0., 1., 0.]])

In [5]: c = torch.mul(a,b)

In [6]: c
Out[6]:
tensor([[0.0000, 0.9292, 0.0000],
        [0.6033, 0.1030, 0.1090],
        [0.0000, 0.7149, 0.0000]])

Listing 2-47Element-Wise Multiplication

类似地,我们有针对元素划分的div方法。清单 2-48 展示了张量的元素划分。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.9209, 0.8241, 0.6200],
        [0.2758, 0.8846, 0.5146],
        [0.1822, 0.2511, 0.3807]])

In [3]: b = torch.FloatTensor([[1, 2, 1],[2,2,2],[1,2,1]])

In [4]: b
Out[4]:
tensor([[1., 2., 1.],
        [2., 2., 2.],
        [1., 2., 1.]])

In [5]: c = torch.div(a,b)

In [6]: c
Out[6]:
tensor([[0.9209, 0.4121, 0.6200],
        [0.1379, 0.4423, 0.2573],
        [0.1822, 0.1256, 0.3807]])

Listing 2-48Element-Wise Division

张量中的三角运算

在深度学习中,我们还会在训练张量的过程中对它们进行一些三角运算。在本节中,我们将简要介绍 PyTorch 中常用的几个重要函数。清单 2-49 说明了基本的三角运算。

In [1]: a = torch.linspace(-1.0, 1.0, steps=10)

In [2]: a
Out[2]:
tensor([-1.0000, -0.7778, -0.5556, -0.3333, -0.1111, 0.1111, 0.3333,  0.5556,  0.7778,  1.0000])

In [3]: torch.sin(a)
Out[3]:
tensor([-0.8415, -0.7017, -0.5274, -0.3272, -0.1109, 0.1109, 0.3272,  0.5274,  0.7017,  0.8415])

In [4]: torch.cos(a)
Out[4]:
tensor([0.5403, 0.7125, 0.8496, 0.9450, 0.9938, 0.9938, 0.9450, 0.8496, 0.7125, 0.5403])

In [5]: torch.tan(a)
Out[5]:
tensor([-1.5574, -0.9849, -0.6208, -0.3463, -0.1116, 0.1116, 0.3463,  0.6208,  0.9849,  1.5574])

In [6]: torch.asin(a)
Out[6]:
tensor([-1.5708, -0.8911, -0.5890, -0.3398, -0.1113, 0.1113, 0.3398,  0.5890,  0.8911,  1.5708])

In [7]: torch.acos(a)
Out[7]:
tensor([3.1416, 2.4619, 2.1598, 1.9106, 1.6821, 1.4595, 1.2310, 0.9818, 0.6797, 0.0000])

In [8]: torch.atan(a)
Out[8]:
tensor([-0.7854, -0.6610, -0.5071, -0.3218, -0.1107, 0.1107, 0.3218,  0.5071,  0.6610,  0.7854])

Listing 2-49Basic Trigonometric Operations for Tensors

清单 2-50 举例说明了机器学习中经常使用的几个函数——即sigmoidtanhlog1p(计算 y = log(1+x))、erf(高斯误差函数)和erfinv(逆高斯误差函数)。

In [1]: a = torch.linspace(-1.0, 1.0, steps=10)

In [2]: a
Out[2]:
tensor([-1.0000, -0.7778, -0.5556, -0.3333, -0.1111, 0.1111, 0.3333,  0.5556,  0.7778,  1.0000])

In [3]: torch.sigmoid(a)
Out[3]:
tensor([0.2689, 0.3148, 0.3646, 0.4174, 0.4723, 0.5277, 0.5826, 0.6354, 0.6852, 0.7311])

In [4]: torch.tanh(a)
Out[4]:
tensor([-0.7616, -0.6514, -0.5047, -0.3215, -0.1107, 0.1107, 0.3215,  0.5047,  0.6514,  0.7616])

In [5]: torch.log1p(a)
Out[5]:
tensor([   -inf, -1.5041, -0.8109, -0.4055, -0.1178, 0.1054, 0.2877,  0.4418,  0.5754,  0.6931])

In [6]: torch.erf(a)
Out[6]:
tensor([-0.8427, -0.7286, -0.5679, -0.3626, -0.1249, 0.1249, 0.3626,  0.5679,  0.7286,  0.8427])

In [7]: torch.erfinv(a)
Out[7]:
tensor([   -inf, -0.8631, -0.5407, -0.3046, -0.0988, 0.0988, 0.3046,  0.5407, 0.8631,     inf])

Listing 2-50Additional Trigonometric Operations for Tensors

张量的比较运算

现在让我们考虑一些允许我们比较张量元素的运算——即,ge(大于或等于)、le(小于或等于)、eq(等于)和ne(不等于)。清单 2-51 说明了张量的比较运算。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.3340, 0.6635, 0.9417],
        [0.2229, 0.6039, 0.9349],
                [0.1783, 0.6485, 0.0172]])

In [3]: b = torch.rand(3,3)

In [4]: b
Out[4]:
tensor([[0.3854, 0.0581, 0.2514],
[0.0510, 0.8652, 0.0233],
[0.0191, 0.8724, 0.0364]])

In [5]: torch.ge(a,b)
Out[5]:
tensor([[0, 1, 1],
        [1, 0, 1],
        [1, 0, 0]], dtype=torch.uint8)

In [6]: torch.le(a,b)
Out[6]:
tensor([[1, 0, 0],
        [0, 1, 0],
        [0, 1, 1]], dtype=torch.uint8)

In [7]: torch.eq(a,b)
Out[7]:
tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]], dtype=torch.uint8)

In [8]: torch.ne(a,b)
Out[8]:
tensor([[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]], dtype=torch.uint8)

Listing 2-51Comparison Operations for Tensors

线性代数运算

我们现在将使用 PyTorch 张量深入研究一些线性代数运算。

matmul函数允许你将两个张量相乘。清单 2-52 演示了张量的矩阵乘法。

In [1]: a = torch.ones(2,3)

In [2]: a
Out[2]:
tensor([[1., 1., 1.],
        [1., 1., 1.]])

In [3]: a.shape
Out[3]: torch.Size([2, 3])

In [4]: b = torch.ones(3,2)

In [5]: b
Out[5]:
tensor([[1., 1.],
        [1., 1.],
        [1., 1.]])

In [6]: b.shape
Out[6]: torch.Size([3, 2])

In [7]: torch.matmul(a,b)
Out[7]:
tensor([[3., 3.],
        [3., 3.]])

In [8]: c.shape
Out[8]: torch.Size([3, 5])

Listing 2-52Matrix Multiplication Operations for Tensors

addbmm函数(其中bmm代表批量矩阵-矩阵乘积)允许您执行计算 p * m + q * [a1 * b1 + a2 * b2 +...],其中 p 和 q 是标量,m、a1、b1、a2 和 b2 是张量。注意,addbmm函数采用默认值等于 1 的参数pq,并且通过沿第一维堆叠张量来提供诸如a1a2的张量。清单 2-53 展示了张量的批量矩阵-矩阵加法。

In [1]: a = torch.ones(2, 2, 3)

In [2]: a
Out[2]:
tensor([[[1., 1., 1.],
         [1., 1., 1.]],

         [[1., 1., 1.],
          [1., 1., 1.]]])

In [3]: a.shape
Out[3]: torch.Size([2, 2, 3])

In [4]: b = torch.ones(2, 3, 2)

In [5]: b
Out[5]:
tensor([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])

In [6]: b.shape
Out[6]: torch.Size([2, 3, 2])

In [7]: m = torch.ones(2,2)

In [8]: m
Out[8]:
tensor([[1., 1.],
        [1., 1.]])

In [9]: m.shape
Out[9]: torch.Size([2, 2])

In [10]: torch.addbmm(2, m, 3, a, b)
Out[10]:
tensor([[20., 20.],
        [20., 20.]])

In [11]: torch.addbmm(1, m, 1, a, b)
Out[11]:
tensor([[7., 7.],
        [7., 7.]])

In [12]: torch.addbmm(m, a, b)
Out[12]:
tensor([[7., 7.],
        [7., 7.]])

Listing 2-53Batch Matrix-Matrix Addition of Tensors

addmm函数是addbmm的非批处理版本,它允许您执行计算 p * m + q * a * b,其中 p 和 q 是标量,m、a 和 b 是张量。注意,addmm函数将参数pq的默认值设为 1。清单 2-54 展示了非批量矩阵——张量的矩阵加法。

In [1]: a = torch.ones(2, 3)

In [2]: a
Out[2]:
tensor([[1., 1., 1.],
        [1., 1., 1.]])

In [3]: a.shape
Out[3]: torch.Size([2, 3])

In [4]: b = torch.ones(3, 2)

In [5]: b
Out[5]:
tensor([[1., 1.],
        [1., 1.],
        [1., 1.]])

In [6]: b.shape
Out[6]: torch.Size([3, 2])

In [7]: m = torch.ones(2,2)

In [8]: m
Out[8]:
tensor([[1., 1.],
        [1., 1.]])

In [9]: m.shape
Out[9]: torch.Size([2, 2])

In [10]: torch.addmm(m, a, b)
Out[10]:
tensor([[4., 4.],
        [4., 4.]])

In [11]: torch.addmm(2, m, 3, a, b)
Out[11]:
tensor([[11., 11.],
        [11., 11.]])

In [12]: torch.addmm(1, m, 1, a, b)
Out[12]:
tensor([[4., 4.],
        [4., 4.]])

Listing 2-54Non Batch Matrix-Matrix Addition of Tensors

addmv函数(matrix-vector)允许你执行计算 p * m + q * a * b,其中 p 和 q 是标量,m 和 a 是矩阵,b 是向量。请注意,addmv采用默认值等于 1 的参数pq。清单 2-55 展示了张量的矩阵向量加法。

In [1]: a = torch.ones(2, 3)

In [2]: a
Out[2]:
tensor([[1., 1., 1.],
        [1., 1., 1.]])

In [3]: a.shape
Out[3]: torch.Size([2, 3])

In [4]: b = torch.ones(3)

In [5]: b
Out[5]: tensor([1., 1., 1.])

In [6]: b.shape
Out[6]: torch.Size([3])

In [7]: m = torch.ones(2)

In [8]: m
Out[8]: tensor([1., 1.])

In [9]: m.shape
Out[9]: torch.Size([2])

In [10]: torch.addmv(2,m,3,a,b)
Out[10]: tensor([11., 11.])

In [11]: torch.addmv(1,m,1,a,b)
Out[11]: tensor([4., 4.])

In [12]: torch.addmv(m,a,b)
Out[12]: tensor([4., 4.])

Listing 2-55Matrix Vector Addition of Tensors

addr函数允许您执行两个向量的外积,并将其添加到给定的矩阵中。线性代数中两个向量的外积是一个矩阵。比如你有一个 m 元素(1 维)的向量 V,另一个 n 元素(1 维)的向量 U,那么 V 和 U 的外积就是一个 m × n 形状的矩阵。

V= [v1, v2, v3..., vm]
U = [u1, u2, ......un]
V ⊕ U = A
A = [   v1u1, v1u2, .... , v1um,
        v2u1, v2,u2,.......v2um,
        .....
        vnu1, vnu2, .......vnum]

在 PyTorch 中,函数期望第一个参数作为我们需要添加结果外积的矩阵,后面是需要计算外积的向量。在清单 2-56 中,我们创建了两个向量(a 和 b),每个向量有三个元素,并执行外积来创建一个 3 × 3 的矩阵,然后将其添加到另一个矩阵(m)中。

In [1]: a = torch.tensor([1.0, 2.0, 3.0])

In [2]: a
Out[2]: tensor([1., 2., 3.])

In [3]: a.shape
Out[3]: torch.Size([3])

In [4]: b = a

In [5]: m = torch.ones(3,3)

In [6]: m
Out[6]:
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])

In [7]: m.shape
Out[7]: torch.Size([3, 3])

In [8]: torch.addr(m,a,b)
Out[8]:
tensor([[ 2.,  3.,  4.],
        [ 3.,  5.,  7.],
        [ 4.,  7., 10.]])

In [9]: m = torch.zeros(3,3)

In [10]: m
Out[10]:
tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])

In [11]: torch.addr(m,a,b)
Out[11]:
tensor([[1., 2., 3.],
        [2., 4., 6.],
        [3., 6., 9.]])

Listing 2-56Outer Product of Vectors

baddbmm函数允许您执行计算 p1 * m + q * [a1 * b1],p2 * m + q * [a2 * b2],...,其中 p 和 q 是标量,m、p1、a1、b1、p2、a2 和 b2 是张量。注意baddbmm取默认值等于 1 的参数pq,p1、a1、a2 等张量通过沿第一维叠加提供。清单 2-27 说明了baddbmm功能的使用。

In [1]: a = torch.ones(2,2,3)

In [2]: a
Out[2]:
tensor([[[1., 1., 1.],
         [1., 1., 1.]],

         [[1., 1., 1.],
          [1., 1., 1.]]])

In [3]: a.shape
Out[3]: torch.Size([2, 2, 3])

In [4]: b = torch.ones(2,3,2)

In [5]: b
Out[5]:
tensor([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])

In [6]: b.shape
Out[6]: torch.Size([2, 3, 2])

In [7]: m = torch.ones(2, 2, 2)

In [8]: m
Out[8]:
tensor([[[1., 1.],
         [1., 1.]],

         [[1., 1.],
          [1., 1.]]])

In [9]: m.shape
Out[9]: torch.Size([2, 2, 2])

In [10]: torch.baddbmm(1,m,1,a,b)
Out[10]:
tensor([[[4., 4.],
         [4., 4.]],

         [[4., 4.],
          [4., 4.]]])

In [11]: torch.baddbmm(2,m,1,a,b)
Out[11]:
tensor([[[5., 5.],
         [5., 5.]],

         [[5., 5.],
          [5., 5.]]])

In [12]: torch.baddbmm(1,m,2,a,b)
Out[12]:
tensor([[[7., 7.],
         [7., 7.]],

         [[7., 7.],
          [7., 7.]]])

Listing 2-57The baddbmm Function

bmm函数允许你对张量执行批量矩阵乘法,如清单 2-58 所示。

In [1]: a = torch.ones(2,2,3)

In [2]: a
Out[2]:
tensor([[[1., 1., 1.],
         [1., 1., 1.]],

         [[1., 1., 1.],
          [1., 1., 1.]]])

In [3]: a.shape
Out[3]: torch.Size([2, 2, 3])

In [4]: b = torch.ones(2,3,2)

In [5]: b
Out[5]:
tensor([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])

In [6]: b.shape
Out[6]: torch.Size([2, 3, 2])

In [7]: torch.bmm(a,b)
Out[7]:
tensor([[[3., 3.],
         [3., 3.]],

        [[3., 3.],
         [3., 3.]]])

Listing 2-58Batch-Wise Matrix Multiplication

dot函数允许你计算张量的点积,如清单 2-59 所示。

In [1]: a = torch.rand(3)
In [2]: a

Out[2]: tensor([0.3998, 0.6383, 0.1169])

In [3]: b = torch.rand(3)
In [4]: b

Out[4]: tensor([0.9743, 0.2473, 0.7826])

In [5]: torch.dot(a,b)

Out[5]: tensor(0.6389)

Listing 2-59Computing the Dot Product of Tensors

eig函数允许你计算给定矩阵的特征值和特征向量。清单 2-60 演示了计算张量的特征值。我们首先计算特征值,然后确认结果匹配。请注意mm函数的使用,它允许您将两个矩阵相乘。

In [1]: a = torch.rand(3,3)

In [2]: a
Out[2]:
tensor([[0.1090, 0.2947, 0.5896],
        [0.6438, 0.2429, 0.7332],
        [0.5636, 0.9291, 0.3909]])

In [3]: values, vectors = torch.eig(a, eigenvectors=True)

In [4]: values
Out[4]:
tensor([[ 1.5308,  0.0000],
        [-0.3940,  0.1086],
        [-0.3940, -0.1086]])

In [5]: vectors
Out[5]:
tensor([[-0.4097, -0.6717,  0.0000],
        [-0.5973, -0.0767,  0.3048],
        [-0.6894,  0.6114, -0.2761]])

In [6]: values[0,0] * vectors[:,0].reshape(3,1)
Out[6]:
tensor([[-0.6272],
        [-0.9144],
        [-1.0554]])

In [7]: torch.mm(a, vectors[:,0].reshape(3,1))
Out[7]:
tensor([[-0.6272],
        [-0.9144],
        [-1.0554]])

Listing 2-60Computing Eigenvalues for a Tensor

清单 2-61 中展示的cross函数允许你计算两个张量的叉积。

In [1]: a = torch.rand(3)
In [2]: b = torch.rand(3)

In [3]: a
Out[3]: tensor([0.3308, 0.2168, 0.0932])

In [4]: b
Out[4]: tensor([0.3471, 0.2871, 0.6141])

In [5]: torch.cross(a,b)
Out[5]: tensor([ 0.1064, -0.1708,  0.0197])

Listing 2-61Computing the Cross Product of Two Tensors

如清单 2-62 所示,norm函数允许你计算给定张量的范数。

In [1]: a = torch.ones(4)

In [2]: a
Out[2]: tensor([1., 1., 1., 1.])

In [3]: torch.norm(a,1)
Out[3]: tensor(4.)

In [4]: torch.norm(a,2)
Out[4]: tensor(2.)

In [5]: torch.norm(a,3)
Out[5]: tensor(1.5874)

In [6]: torch.norm(a,4)
Out[6]: tensor(1.4142)

In [7]: torch.norm(a,5)
Out[7]: tensor(1.3195)

In [8]: torch.norm(a,float('inf'))
Out[8]: tensor(1.)

Listing 2-62Computing the Norm of a Tensor

renorm函数允许你通过除以范数来归一化一个向量。清单 2-63 演示了张量的规范化操作。

In [1]: a = torch.FloatTensor([[1,2,3,4]])

In [2]: a
Out[2]: tensor([[1., 2., 3., 4.]])

In [3]: torch.renorm(a, dim=0, p=2, maxnorm=1)
Out[3]: tensor([[0.1826, 0.3651, 0.5477, 0.7303]])

Listing 2-63Normalizing a Tensor

摘要

本章简要介绍 PyTorch,重点介绍张量和张量运算。本章讨论的几个张量运算将在接下来的几章中派上用场。你应该花时间和张量一起提高你的 PyTorch 技能。这对于定制深度学习网络和在出现不明错误时容易地调试流程将是非常有价值的。

常见的张量操作有view(重塑张量)size(打印张量的形状/大小)item(从单值张量中提取数据)squeeze(重塑张量)cat(连接张量)。此外,PyTorch 有两个独立的包(torchvision 和 torchtext ),它们提供了一套全面的处理图像(计算机视觉)和文本(自然语言处理)数据集的功能。我们将在第六章“卷积神经网络”和第七章“循环神经网络”中探索这些包的基本实用程序

作为一个库,PyTorch 为研究人员和实践者提供了一种出色的方法来大规模开发和训练深度学习实验,同时为几个构建块提供了简洁的抽象,同时还可以灵活地进行深度定制。在接下来的几章中,在实际实现深度学习模型时,您将看到 PyTorch 如何在后台处理如此多的事情,从而为用户提供大规模加速实验所需的速度和敏捷性。

下一章将关注基本前馈网络的基础——走向深度学习的第一步。

三、前馈神经网络

前馈神经网络是深度学习中最早的实现。这些网络被称为前馈网络,因为网络中的信息只沿一个方向(向前)移动,即从输入节点(单元)向输出单元移动。在本章中,我们将涵盖一些围绕前馈神经网络的关键概念,这些神经网络是深度学习中各种主题的基础。我们将从研究神经网络的结构开始,然后研究它们是如何被训练和用于预测的。我们还将简要了解在不同设置中应该使用的损失函数、在神经元中使用的激活函数,以及可以用于训练的不同类型的优化器。最后,我们将使用 PyTorch 将这些较小的组件缝合到一个成熟的前馈神经网络中。

让我们开始吧。

什么是神经网络?

在抽象层次上,神经网络可以被认为是一个函数

$$ {f}_{\theta }:x\to y $$

它接受一个输入xRn并产生一个输出yRm,其行为由θRp参数化。因此,例如, f θ 可以简单地表示为y=fθ(x)=θ**x

图 3-1 显示了一个神经元(或神经网络内的一个单元)的架构。

img/478491_2_En_3_Fig1_HTML.jpg

图 3-1

前馈网络中的一个单元

单位

一个单元(也称为节点神经元)是神经网络的基本构建模块,参见图 3-1 和图 3-2 。

一个单元/节点/神经元是一个函数,它将一个向量xRn作为输入,并产生一个标量。一个单元由权重向量wRn和由 b 表示的偏差项来参数化。

单位的输出可以描述为

$$ f\left({\sum}_{i=1}^n{x}_i\cdotp {w}_i+b\right) $$

其中 f : RR 称为激活函数

虽然可以使用各种各样的激活函数,但正如我们将在本章后面看到的,通常使用非线性函数。

图 3-2 显示了该装置的详细情况。

img/478491_2_En_3_Fig2_HTML.jpg

图 3-2

神经网络中的单元

神经网络的整体结构

使用该单元作为基本构建块来构建神经网络。这些单元被组织成层,每层包含一个或多个单元。最后一层被称为输出层。输出层之前的所有层被称为隐藏层。第一层,通常称为第 0 ,是输入层。每一层通过权重连接到下一个连续层,权重以迭代的方式被训练/更新。

一层中的单元数量被称为该层的宽度。每层的宽度不必相同,但是尺寸应该一致,我们将在本章后面看到。

层数被称为网络的深度。这就是“深度”(如“深度学习”)概念的来源。

每一层都将前一层产生的输出作为输入,除了第一层消耗输入。最后一层的输出是网络的输出,是基于输入生成的预测。

如前所述,神经网络可以看作是一个函数fθ:xy,它以xRn作为输入,产生yRm我们现在可以更精确地了解 θ 。它只是网络中所有单元的所有权重的集合。

设计神经网络包括定义网络的整体结构,包括层数(深度)和这些层的宽度。图 3-3 显示了一个神经网络的整体结构。

img/478491_2_En_3_Fig3_HTML.jpg

图 3-3

神经网络的结构

以向量形式表达神经网络

让我们更详细地看看神经网络的层及其维度(参见图 3-3 )。如果我们假设输入的维度是xRn并且第一层有 p 1 个单元,那么每个单元都有wRn个权重与之相关联。也就是说,与第一层相关联的权重是形式为$$ {w}_1\in {R}^{n\times {p}_1} $$的矩阵。虽然图 3-3 中没有显示,但是每个p1 单元也有一个与之相关的偏置项。

第一层产生输出$$ {o}_1\in {R}^{p_1} $$,其中$$ {o}_i=f\left({\sum}_{k=1}^n{x}_k\cdotp {w}_k+{b}_i\right) $$。注意,索引 k 对应于每个输入/权重(从 1… n ),索引 i 对应于第一层中的单元(从 1。。p1)。

现在让我们看看第一层的矢量化符号输出。通过矢量化符号,我们简单地表示线性代数运算,例如向量矩阵乘法和对产生向量的向量的激活函数的计算(而不是标量到标量)。第一层的输出可以表示为f(x**w1+b1)。

这里,我们将输入xRn视为维数为 1 × n ,将权重矩阵 w 1 视为维数为n×p1,将偏差项视为维数为 1×p的向量那么请注意,x**w1+b产生一个维数为 1 × p 1 的向量,函数 f 简单变换向量的每个元素产生$ {o}_1\in {R}^{p_1} $

$$ {o}_1\in {R}^{p_1} $$$$ {o}_2\in {R}^{p_2} $$的第二层遵循类似的过程。这可以用矢量化的形式写成f(o1w2+b2)。我们也可以将整个计算以矢量化的形式写到第 2 层,如f(f(x**w1+b1)w2+b2。图 3-4 显示了一个矢量形式的神经网络。

img/478491_2_En_3_Fig4_HTML.jpg

图 3-4

向量形式的神经网络

评估神经网络的输出

现在我们已经了解了神经网络的结构,让我们看看如何根据标记数据评估神经网络的输出。参见图 3-5 。

对于单个数据点,我们可以计算神经网络的输出,我们将其表示为$$ \hat{y} $$。现在我们需要计算我们的神经网络$$ \hat{y} $$的预测与 y 相比有多好。这里出现了损失函数的概念。

损失函数测量$$ \hat{y} $$y 之间的差异,我们用 l 表示。许多损失函数适用于手头的任务,比如二元分类、多类分类或我们将在本章后面讨论的回归(通常使用最大似然法导出,这是一种概率框架,旨在增加找到最佳解释数据的概率分布的可能性)。

损失函数通常计算多个数据点而不是单个数据点上的$$ \hat{y} $$y 之间的差异。图 3-5 展示了$$ \hat{y} $$y 不一致的计算流程。

img/478491_2_En_3_Fig5_HTML.jpg

图 3-5

损失/成本函数和成本/损失的计算

训练神经网络

现在让我们看看神经网络是如何训练的。图 3-6 说明了训练一个神经网络。

假设与前面相同的符号,我们用 θ 表示网络所有层的所有权重和偏差项的集合。让我们假设 θ 已经用随机值初始化。我们用 f NN 表示代表神经网络的整体函数。

如前所述,我们可以取单个数据点,并将神经网络的输出计算为$$ \hat{y} $$。我们还可以利用损失函数$$ l\left(\hat{y},y\right)\hbox{---} $$计算出与实际产量 y 的不一致,即l(fNN(xθy )。

现在让我们计算这个损失函数的梯度,并用𝛻l(fnn(xθy )来表示。

我们现在可以使用最速下降法更新 θθs=θs1αl(fNN(xθ ), 请注意,我们可以对我们的训练集中的不同数据点反复采取许多这样的步骤,直到我们对l(fNN(xθ ), y )有一个合理的好值。

Note

现在,我们将远离损失函数𝛻l(fnn(xθy )的梯度的计算。这些可以很容易地用自动微分法(在本书的其他地方讨论过)来生成(甚至对于任意复杂的损失函数),而不需要手动推导。

img/478491_2_En_3_Fig6_HTML.jpg

图 3-6

训练神经网络

使用最大似然法驱动成本函数

如前所述,成本函数(也称为损失函数)有助于用量化指标确定预测和实际目标之间的差异。基于特定的用例以及目标变量的性质,有几种方法来定义损失函数。损失函数是通过利用一个框架(比如,最大似然法)得出的,在这个框架中,我们最大化或最小化一组感兴趣的结果的参数。使用损失函数计算不一致的量化值。因此,它为模型的训练框架提供了一种估计不一致程度的切实可行的方法,从而更新权重参数,以便减少不一致,从而提高模型性能。

我们现在将研究如何使用最大似然法导出各种损失函数。具体来说,我们将看到深度学习中常用的损失函数——如二进制交叉熵、交叉熵(用于非二进制结果)和平方误差——如何使用最大似然原理推导出来。

二元交叉熵

二进制交叉熵对数损失,衡量分类模型的性能,其中结果是二进制的,并以 0 到 1 之间的概率值的形式表示。随着模型性能降低,测井损失值增加,产生的预测值偏离期望值。理想模型的二进制交叉熵值为 0。

让我们考虑一个简单的例子来理解二元交叉熵的概念,并获得最大似然的基本直觉。我们有一些数据,由 D = {( x 1y 1 ), x 2y 2 ),…(xny n

让我们假设我们已经生成了一个模型,在给定 x 的情况下预测 y 的概率。我们用 f ( xθ 来表示这个模型,其中 θ 表示模型的参数。最大似然背后的思想是找到一个最大化P(D|θ)的 θ 。假设一个伯努利分布,并给定每个例子{( x 1y 1 ),( x 2y 2 ),…(xny

我们可以对两边进行对数运算得出如下:

$$ \mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ {\prod}_{i=1}^nf{\left({x}_i,\theta \right)}^{y_i}\cdotp {\left(1-f\left({x}_i,\theta \right)\right)}^{\left(1-{y}_i\right)} $$

从而简化为以下表达式:

$$ \mathit{\log}\ P\left(\theta \right)={\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right) $$

我们不是最大化 RHS,而是最小化它的负值,如下:

$$ P\left(\theta \right)=-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right) $$

这就引出了下面这个二元交叉熵函数:

$$ -{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right) $$

因此,最大似然的思想使我们能够导出二元交叉熵函数,该函数可以在二元分类的上下文中用作损失函数。

交叉熵

基于二进制交叉熵的思想,现在让我们考虑导出交叉熵损失函数以用于多分类的上下文中。我们假设 y ∈ {0,1,.. k },其中{0,1,.. k }是类。我们还将 n 1n2nk表示为每个 k 类的观察计数。观察$$ {\sum}_{i=1}^k{n}_i=n $$。同样,在这种情况下,让我们假设我们已经以某种方式生成了一个模型,该模型在给定 x 的情况下预测了 y 的概率。我们用 f ( xθ 来表示这个模型,其中 θ 表示模型的参数。让我们再次使用最大似然背后的思想,即找到一个使P(D|θ最大化的 θ 。假设是多项式分布,并给定每个例子{( * x * 1y 1 ),( x 2y 2 ),…(xn

*我们可以对两边进行对数运算得出如下:

$$ \mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ n!-\kern0.5em \mathit{\log}\ {n}_1!\cdotp {n}_2!\cdots {n}_k!+\mathit{\log}\ {\prod}_{i=1}^nf{\left({x}_i,\theta \right)}^{y_i} $$

这可以简化为:

$$ \mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ n!-\kern0.5em \mathit{\log}\ {n}_1!\cdotp {n}_2!\cdots {n}_k!+{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right) $$

术语日志 n !还有 log n 1n2!⋯n??k!不被 θ 参数化,并且可以被安全地忽略,因为我们试图找到最大化P(D|θ)的 θ 。由此,我们有了以下:

$$ \mathit{\log}\ P\left(\theta \right)={\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right) $$

和以前一样,我们不是最大化 RHS,而是最小化它的负值,如下:

$$ P\left(\theta \right)=-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right) $$

这就导致了下面的二元交叉熵函数:

$$ -{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right) $$

因此,最大似然的思想使我们能够导出交叉熵函数,它可以在多分类的情况下用作损失函数。

平方误差

现在让我们讨论使用最大似然法推导回归中使用的平方误差。让我们假设 yR 。与前面的情况不同,我们假设我们有一个预测概率的模型,我们将假设我们有一个预测 y 值的模型。为了应用最大似然思想,我们假设实际的 y 和预测的$$ \hat{y} $$之间的差具有零均值和方差为σ2 的高斯分布。然后,可以显示最小化

$$ {\sum}_{i=1}^n{\left(y-\hat{y}\ \right)}² $$

导致 P ( θ )最小化。

损失函数概述

我们现在总结关于损失函数的三个要点,以及给定手头问题的特定损失函数的适当性。

  1. The binary cross-entropy given by the expression

    $$ -{\sum}_{i=1}^n{y}_i logf\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\left(1-f\left({x}_i,\theta \right)\right) $$

是二元分类的推荐损失函数。当设计神经网络来预测结果的概率时,通常应该使用这种损失函数。在这种情况下,输出层具有单个单元,该单元具有合适的 sigmoid 作为激活函数。

  1. The cross-entropy function given by the expression

    $$ -{\sum}_{i=1}^n{y}_i logf\left({x}_i,\theta \right) $$

是多分类的推荐损失函数。这个损失函数通常应该与设计用来预测每一类结果的概率的神经网络一起使用。在这种情况下,输出图层具有 softmax 单位(每个类一个)。

  1. The squared loss function given by

    $$ {\sum}_{i=1}^n{\left(y-\hat{y}\ \right)}² $$

应该用于回归问题。在这种情况下,输出图层只有一个单元。

几个其他损失函数可以用于分类和回归;涵盖详尽无遗的清单超出了本章的范围。一些值得注意的损失函数是 Huber 损失(回归)和铰链损失(分类)。

激活功能的类型

我们现在来看看一些常用于神经网络的激活函数。

让我们从列举激活函数的几个感兴趣的属性开始。

  • 理论上,当激活函数是非线性的时,两层神经网络可以逼近任何函数(给定隐藏层中足够数量的单元)。因此,我们总是使用非线性激活函数来解决深度学习领域中的问题。

  • 一个连续可微的函数允许计算梯度,并使用基于梯度的方法(优化器)来寻找使数据损失函数最小化的参数。如果一个函数不是连续可微的,基于梯度的方法在网络的训练中将不会取得进展。

  • 使用基于梯度的方法,我们可以从值域有限的函数中获得稳定的性能(相对于无穷大)。

  • 平滑函数是优选的(经验证据),单层的单片函数导致凸误差表面。(这通常不是关于深度学习的考虑因素。)

  • 此外,我们更倾向于期望激活函数关于原点对称,并且在原点()附近表现得像恒等函数。

至此,让我们简单看看激活函数中值得注意的选项。

线性单位

线性单元是将输入转换为 y = w 的最简单单元。 x + b 。顾名思义,该单位没有非线性行为,通常用于生成条件高斯分布的平均值。

线性单元使基于梯度的学习成为一项相当简单的任务(图 3-7 )。

img/478491_2_En_3_Fig7_HTML.jpg

图 3-7

神经网络中的线性单元

乙状结肠激活

sigmoid 激活将输入转换如下:

$$ y=\frac{1}{1+{e}^{-\left( wx+b\right)}}. $$

底层激活函数(图 3-8 )由

$$ f(x)=\frac{1}{1+{e}^{-x}}. $$

给出

Sigmoid 单元可在输出图层中与二进制交叉熵一起用于二进制分类问题。该单元的输出可以在以 x 为条件的输出 y 上模拟伯努利分布。

img/478491_2_En_3_Fig8_HTML.jpg

图 3-8

Sigmoid 函数

Softmax 激活

softmax 图层通常仅在输出图层中与交叉熵损失函数一起用于多分类任务。参见图 3-9 。softmax 层对前一层的输出进行归一化,使其总和为 1。通常,前一层的单元对输入属于特定类别的可能性的非标准化分数进行建模。softmax 层对此进行了标准化,以便输出表示每个类的概率。

img/478491_2_En_3_Fig9_HTML.jpg

图 3-9

Softmax 层

整流器线性单元

与线性变换结合使用的整流线性单元(ReLU)将输入变换为

$$ f(x)=\mathit{\max}\left(0, wx+b\right) $$

底层激活函数为f(x)=max(0, x )。最近,ReLU 更常用作隐藏单元。结果表明,ReLUs 导致较大且一致的梯度,这有助于基于梯度的学习(图 3-10 )。虽然 ReLU 看起来像一个线性单元,但它有一个导数函数,因此可以计算损耗的梯度。最近,ReLU 已经成为隐藏网络激活的最流行的选择。在大多数情况下,一个 ReLU 可以是一个默认的选择,它会在一个合适的时间内产生想要的结果。

img/478491_2_En_3_Fig10_HTML.jpg

图 3-10

整流器线性单元

然而,ReLU 也有一些缺点。当输入接近零时,函数的梯度变为零,因此停留在训练步骤中,训练没有进展。这就是俗称的将死的 ReLU 问题

双曲正切

双曲正切单元对输入(与线性变换结合使用)进行如下变换:

$$ y=\mathit{\tanh}\left( wx+b\right). $$

底层激活函数(图 3-11 )由

$$ f(x)=\mathit{\tanh}(x). $$

给出

双曲正切单位也常用作隐藏单位。

图 3-11 仅涵盖了深度学习激活功能中的少数可用选项。

img/478491_2_En_3_Fig11_HTML.jpg

图 3-11

双曲正切激活函数

在特定的设置或使用案例中,还有更多的方法可用于定制收益。著名的例子包括泄漏 ReLU、参数 ReLU 和 Swish。探索附加激活功能的良好起点是 https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity

反向传播

深度学习最基本的构建模块是反向传播,是误差反向传播的缩写,是一种用于在监督学习中训练神经网络的算法。虽然反向传播是在 20 世纪 70 年代发明的,但它在几年后的 1989 年由 Rumelhart、Hinton 和 Williams 在他们的论文“通过反向传播误差学习表征”中得到推广

之前,我们研究了衡量预测产出和实际产出之间差异的损失函数。网络的权重首先被随机初始化。为了让网络学习(训练),下一个逻辑步骤是调整权重,使得不一致最小(理想情况下为零)。这就是我们与反向传播接口的地方,反向传播是一种直观的算法,能够使用链式法则计算损失相对于权重的梯度。

在前向传递中,网络计算给定输入样本的预测,损失函数测量实际目标值和网络预测值之间的差异。反向传播计算相对于权重和偏差的损失梯度,从而为我们提供权重的微小变化如何影响总体损失的公平的总体情况。然后,我们需要迭代地更新权重,并以小的增量(在梯度的相反方向)达到局部最小值。这个过程叫做梯度下降——即将损失函数降低到最小。因此,网络学习(对权重的迭代和增量更新)能够以最小的不一致正确预测给定输入样本的模式。

对于神经网络,在梯度下降中有几个变量来更新权重。下一节将探讨其中的一些。在下一章中,我们将简要地看一下自动微分,它以编程的方式实现了反向传播的思想。

梯度下降变体

梯度下降技术主要有三种变体。每种方法的不同之处在于用于计算损失梯度的数据量。根据使用的数据量,我们在参数更新的准确性和执行更新所需的时间之间进行权衡。下面,我们讨论在训练深度学习网络中使用的三种不同的变体,稍后(在下面的部分中)我们研究几个流行的梯度下降优化算法。

批量梯度下降

最初的梯度下降被称为批量梯度下降 (BGD)技术。该名称源自用于计算梯度的数据量,在本例中为整个批次。BGD 技术本质上利用整个可用数据集来计算成本函数相对于参数(权重)的梯度。这导致了固有的缓慢,并且在大多数情况下,这是一个不可行的选择,因为我们可能会耗尽内存来加载整个批处理。在大多数常见的场景中,我们大多倾向于避免 BGD 方法,争论小数据集(这在深度学习中是一种罕见的现象)。

随机梯度下降

为了克服来自 BGD 的问题,我们有随机梯度下降(SGD)。使用 SGD,我们计算梯度并更新数据集中每个样本的权重。这一过程大大减少了深度学习硬件中的内存使用,并更快地获得结果。但是,更新的频率远远高于预期。随着更频繁地更新权重,成本函数波动很大。

然而,当目标是将更新收敛到精确的最小值时,SGD 会导致更大的问题。考虑到更新的频繁程度,过早更新的可能性非常高。为了克服这些权衡,我们可能需要在一段时间内缓慢降低学习速率,以帮助网络收敛到局部或全局最小值。

小批量梯度下降

小批量梯度下降 (MBGD)结合了 SGD 和 BGD 的优点。MBGD 不是使用整个数据集(批次)或仅来自数据集的单个样本来计算成本函数相对于参数的梯度,而是利用更小的批次,该批次大于 1 但小于整个数据集。常见的批量有 16/32/64/…1024 等。建议使用 2 的幂范围内的一个数(但不是必需的),因为从计算的角度来看它最合适。

使用 MBGD,更新频率比 SGD 低,但比 BGD 高,并且利用小批量而不是单个样本或整个数据集。这样,方差在更大程度上减小,并且我们在速度上实现了更好的折衷。

基于梯度的优化技术

在接下来的部分,我们将简要讨论深度学习中常用的几种流行的优化技术。每种技术中使用的数学细节超出了本书的范围。

动量梯度下降

我们之前讨论的 SGD 和 BGD 之间的问题用 MBGD 解决了。但是,即使使用了 MBGD,更新的方向仍然会发生变化(虽然比使用 SGD 时要小,但比使用 MGD 时要大)。具有动量的梯度下降利用过去的梯度来计算梯度的指数加权平均值,以进一步平滑参数更新。

图 3-12 说明了更新过程。

img/478491_2_En_3_Fig12_HTML.jpg

图 3-12

动量梯度下降

更新过程可以使用以下公式来简化。首先,我们计算过去梯度的指数加权平均值为 ν t ,其中νt=γνt—1+ηθj(θ)和θ=θ-

*这里的 γ 是一个取值在 0 到 1 之间的超参数。接下来,我们在权重更新中使用这个指数加权平均值,而不是直接使用梯度。

通过利用梯度的指数加权平均值,而不是直接使用梯度,增量步长更平滑和更快,从而克服了围绕最小值振荡的问题。

RMSprop

RMSprop 是 Geoffry Hinton 在 Coursera 的在线课程“机器学习的神经网络”的第 6 讲中提出的一种未公开的优化算法。在核心处,RMSprop 计算每个权重的平方梯度的移动平均值,并将梯度除以均方的平方根。这个复杂的过程应该有助于解码名字均方根 prop 。在这里利用指数平均有助于给予最近的更新比不太最近的更新更多的偏好。

RMSprop 可以表示如下:

对于θ中的每个权重 w,我们有

$$ {\nu}_t=\beta\ {\nu}_{t-1}+\left(1-\beta \right)\ast {g}_t² $$

$$ \Delta  {\mathrm{w}}_t=-\frac{\eta }{\sqrt{\nu_t+\in }\ } $$**g??t*

$$ {w}_{t+1}={w}_t+\Delta  {\mathrm{w}}_t $$

更新权重

其中η–是定义初始学习率的超参数,gt是θ中参数/权重 w 在时间 t 的梯度。我们将∈加到分母上,以避免被零除的情况。

圣经》和《古兰经》传统中)亚当(人类第一人的名字

Adam 是自适应矩估计的简化名称,是深度学习优化器最近最受欢迎的选择。简单地说,Adam 结合了 RMSprop 和带动量的随机梯度下降的优点。从 RMSprop 中,它借用了使用平方梯度来缩放学习速率的思想,并且当与具有动量的 SGD 相比时,它采用梯度的移动平均值的思想,而不是直接使用梯度。

这里,对于θ中的每个权重 w,我们有

$$ {\nu}_t={\beta}_1\ {\nu}_{t-1}+\left(1-\kern0.5em {\beta}_1\right)\ast {g}_t $$

还有

$$ {s}_t={\beta}_2\ {s}_{t-1}+\left(1-\kern0.5em {\beta}_2\right)\ast {g}_t² $$

然后用来计算

$$ \Delta  {\mathrm{w}}_t=-\eta \frac{\nu_t}{\sqrt{s_t+\in }\ } $$**g??t*

最后,权重更新为

$$ {w}_{t+1}={w}_t+\Delta  {\mathrm{w}}_t $$

前面三种类型的优化算法只是深度学习中不同类型用例的可用选项中的一小部分。我们肯定没有涵盖这些主题中每一个的详细深度和数学,所以强烈建议读者更详细地探索前面的优化技术和其他技术。阿达格拉德和阿达德尔塔是热门和强烈推荐的选择。

PyTorch 的实际实现

到目前为止,我们已经提供了前馈神经网络的基本主题的简要概述。我们现在将使用 PyTorch 实现一个简单的网络。引入第一个网络所需的所有构建模块的想法使得 PyTorch 中的懒惰学习(在必要时学习构造)过程更加有效。

清单 3-1 为这个练习导入了必要的 Python 包。

#Import required libraries
import torch as tch
import torch.nn as nn

import numpy as np

from sklearn.datasets import make_blobs
from matplotlib import pyplot

Listing 3-1Importing the Necessary Python Packages

我们将需要 Torch 及其神经网络模块,以及 NumPy、matplotlib(用于可视化)和 sklearn(用于创建虚拟数据集)。虽然有一百万种方法可以创建虚拟数据集,但我们将利用 sklearn 中提供的一个简单函数。

Note

在本书中,我们使用了几个与机器学习相关的流行 Python 包。这些包中的大多数都是随 Anaconda 发行版一起安装的。如果需要的话,将特别调用其他包。

接下来,让我们为神经网络创建一个虚拟数据集。清单 3-2 展示了为练习创建一个玩具(假人)数据集。

samples = 5000

#Let's divide the toy dataset into training (80%) and rest for validation.
train_split = int(samples*0.8)

#Create a dummy classification dataset
X, y = make_blobs(n_samples=samples, centers=2, n_features=64, cluster_std=10, random_state=2020)
y = y.reshape(-1,1)

#Convert the numpy datasets to Torch Tensors
X,y = tch.from_numpy(X),tch.from_numpy(y)
X,y =X.float(),y.float()

#Split the datasets inot train and test(validation)
X_train, x_test = X[:train_split], X[train_split:]
Y_train, y_test = y[:train_split], y[train_split:]

#Print shapes of each dataset
print("X_train.shape:",X_train.shape)
print("x_test.shape:",x_test.shape)
print("Y_train.shape:",Y_train.shape)
print("y_test.shape:",y_test.shape)
print("X.dtype",X.dtype)
print("y.dtype",y.dtype)

Output[]
X_train.shape: torch.Size([4000, 64])
x_test.shape: torch.Size([1000, 64])
Y_train.shape: torch.Size([4000, 1])
y_test.shape: torch.Size([1000, 1])
X.dtype torch.float32
y.dtype torch.float32

Listing 3-2Creating a Toy Dataset

玩具数据集有 5000 个样本,每个样本有 32 个特征,分为 80%训练和 20%测试。让我们创建一个使用 PyTorch 的 NN 模块定义神经网络的类。清单 3-3 定义了用于本练习的神经网络的创建。

#Define a neural network with 3 hidden layers and 1 output layer
#Hidden Layers will have 64,256 and 1024 neurons
#Output layers will have 1 neuron

class NeuralNetwork(nn.Module):

    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(64, 256)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(256, 1024)
        self.relu2 = nn.ReLU()
        self.out = nn.Linear(1024, 1)
        self.final = nn.Sigmoid()

    def forward(self, x):
        op = self.fc1(x)
        op = self.relu1(op)
        op = self.fc2(op)
        op = self.relu2(op)
        op = self.out(op)
        y = self.final(op)
        return y

Listing 3-3Defining a Feed Forward Neural Network

torch.nn模块提供了定义和训练神经网络的基本方法。它包含创建各种类型、大小和复杂性的神经网络的所有必要构件。我们将通过继承这个模块为我们的神经网络创建一个类,并创建一个初始化方法和一个向前传递方法。

__init__方法创建网络的不同部分,并在我们每次用这个类创建一个对象时为我们准备好。本质上,我们使用初始化方法来创建隐藏层、输出层和每个层的激活。nn.Linear(64,256)函数创建一个具有 64 个输入特征和 256 个输出特征的图层。下一层自然会有 256 个输入特征,依此类推。当连接到一个层时,nn.ReLU()nn.Sigmoid()功能增加了激活功能。在初始化函数中创建的每个单独的组件都在forward()方法中连接。

forward方法中,我们连接神经网络的各个组件。第一个隐藏层fc1接受输入数据,并为下一层产生 256 个输出。fc1层被传递给relu1激活层,然后激活层将激活的输出传递给下一层fc2,后者重复相同的过程,以创建最终的输出层,该层具有 sigmoid 激活函数(因为我们的玩具数据集是为二进制分类而制作的)。

在创建一个类为NeuralNetwork的对象并调用forward方法时,我们从网络中获得输出,这些输出是通过将输入矩阵与一个随机初始化的权重矩阵相乘来计算的,该权重矩阵通过一个激活函数传递,并对隐藏层的数量进行重复,直到最终的输出层。起初,网络显然会产生垃圾输出——即预测(这对我们的分类问题没有任何价值,至少现在没有)。

为了对我们给定的问题进行更准确的预测,我们需要训练网络,即反向传播损失并更新损失函数的权重。幸运的是,PyTorch 以一种非常容易使用和直观的方式提供了这些基本的构建模块。清单 3-4 说明了定义神经网络的损失、优化器和训练循环。

#Define function for training a network
def train_network(model,optimizer,loss_function \
                  ,num_epochs,batch_size,X_train,Y_train):
    #Explicitly start model training
    model.train()

    loss_across_epochs = []
    for epoch in range(num_epochs):
        train_loss= 0.0

        for i in range(0,X_train.shape[0],batch_size):

            #Extract train batch from X and Y
            input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
            labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

            #set the gradients to zero before starting to do backpropragation
            optimizer.zero_grad()

            #Forward pass
            output_data  = model(input_data)

            #Caculate loss
            loss = loss_function(output_data, labels)

            #Backpropogate
            loss.backward()

            #Update weights
            optimizer.step()

            train_loss += loss.item() * batch_size

        print("Epoch: {} - Loss:{:.4f}".format(epoch+1,train_loss ))
        loss_across_epochs.extend([train_loss])

    #Predict
    y_test_pred = model(x_test)
    a =np.where(y_test_pred>0.5,1,0)
    return(loss_across_epochs)
###------------END OF FUNCTION--------------

#Create an object of the Neural Network class
model = NeuralNetwork()

#Define loss function
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

#Define Optimizer
adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)

#Define epochs and batch size
num_epochs = 10
batch_size=16

#Calling the function for training and pass model, optimizer, loss and related paramters
adam_loss = train_network(model,adam_optimizer \
                             ,loss_function,num_epochs,batch_size,X_train,Y_train)

Listing 3-4Defining the Loss, Optimizer, and Training Function for the Neural Network

在我们进入清单 3-4 的细节之前,让我们看看我们利用 PyTorch 现成的构建模块定义的各个组件。我们需要定义一个损失函数来衡量我们的预测和实际标签之间的差异。PyTorch 提供了不同结果的损失函数的综合列表。这些损失函数在torch.nn.*下可用。例子有MSELoss(均方误差损失)CrossEntropyLoss(用于多类分类)BCELoss(二元交叉熵损失),用于二元分类。对于我们的用例,我们将利用二元交叉熵损失。

这被定义为loss_function = torch.nn.BCELoss()

接下来,我们为我们的网络定义一个优化器。在本章的前面,我们探讨了 SGD、Adam 和 RMSProp 优化器。Pytorch 提供了一个全面的优化器列表,可用于构建各种类型的神经网络。所有优化器都组织在torch.optim.*下(例如,torch.optim.SGD,用于 SGD 优化器)。对于我们的用例,我们使用 Adam 优化器(大多数用例最推荐的优化器)。在定义优化器时,我们还需要定义在反向传播过程中需要计算梯度的参数。对于神经网络,该列表将是前馈网络中的所有权重。通过在优化器的定义中使用model.parameters(),我们可以很容易地向优化器表示模型权重的完整列表。然后,我们可以为所选的优化器另外定义超参数。默认情况下,PyTorch 为所有必需的超参数提供了相当好的值。然而,我们可以进一步覆盖它们,为我们的用例定制优化器。

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)

最后,我们需要定义批量大小和训练模型所需的历元数。批量指小批量更新中一个批次内的样本数量。覆盖所有样本的所有批次的一次向前和向后通过被称为一个时期。最后,我们将所有这些构造传递给我们的函数来训练我们的模型。让我们详细看看函数中的构造。

在我们的训练函数中,我们定义了一个结构,用所提供的优化器、损失函数、模型对象和训练数据来训练我们的网络。首先,我们用model.train()初始化我们的训练模式模型。将模型对象明确设置为训练模式是必要的;在利用模型进行评估时,这也是必不可少的——即,使用model.eval()显式地将模型设置为评估模式。这确保了模型知道期望何时更新参数以及何时不更新参数。在前面的例子中,我们没有添加评估循环,因为它是一个很小的玩具数据集。然而,在后面的大型数据集示例中,我们将使用单独的函数进行评估。

我们将小批量训练网络。for循环按照我们定义的大小将训练数据分成几批。使用以下代码为一个批次提取训练数据以及相应的标签:

input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

然后,在使用optimizer.zero_grad()开始反向传播之前,我们需要将梯度设置为零。错过这个步骤将会在后续的反向过程中累积梯度,并导致不期望的效果。这种行为是 PyTorch 设计的。然后,我们使用output_data = model(input_data)计算向前传球。向前传递是在我们的类定义中执行forward()函数。它连接我们为网络定义的不同层,最终输出每个样本的预测。一旦我们有了预测,我们就可以使用损失函数计算它与实际标签的偏差,即loss = loss_function(output_data, labels)

为了反向传播我们的损失,PyTorch 提供了一个内置的模块来计算损失相对于权重的梯度。我们简单地调用loss.backward()方法,整个反向传播就完成了。第四章“深度学习中的自动微分”更详细地探讨了 PyTorch 中负责反向传播的自动签名模块。一旦计算出梯度,就该更新我们的模型权重了。这在步骤optimizer.step()中完成。优化器步骤知道需要用梯度更新的参数,因为我们在定义优化器时提供了这些参数。调用optimizer.step()函数更新网络的权重,自动考虑优化器中定义的超参数——在我们的例子中是学习率。

我们对整个训练样本分批重复这个过程。训练过程针对多个时期重复进行,并且随着每次迭代,我们期望损失减少并且权重对齐,以便实现更好的预测准确性。

清单 3-5 使用不同的优化器来说明前面的神经网络的训练过程。由于网络是为玩具数据集训练的,我们将在每个时期后为不同的优化器绘制总损失,而不是绘制验证准确性。我们可以研究图 3-13 中展示的每个优化变量的输出,即跨时段的损失。

#Define loss function
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
num_epochs = 10
batch_size=16

#Define a model object from the class defined earlier
model = NeuralNetwork()

#Train network using RMSProp optimizer
rmsprp_optimizer = tch.optim.RMSprop(model.parameters()
, lr=0.01, alpha=0.9
, eps=1e-08, weight_decay=0.1
, momentum=0.1, centered=True)
print("RMSProp...")
rmsprop_loss = train_network(model,rmsprp_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Train network using Adam optimizer

model = NeuralNetwork()
adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)
print("Adam...")
adam_loss = train_network(model,adam_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Train network using SGD optimizer

model = NeuralNetwork()
sgd_optimizer = tch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
print("SGD...")
sgd_loss = train_network(model,sgd_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Plot the losses for each optimizer across epochs
import matplotlib.pyplot as plt
%matplotlib inline

epochs = range(0,10)

ax = plt.subplot(111)
ax.plot(adam_loss,label="ADAM")
ax.plot(sgd_loss,label="SGD")
ax.plot(rmsprop_loss,label="RMSProp")
ax.legend()
plt.xlabel("Epochs")
plt.ylabel("Overall Loss")
plt.title("Loss across epochs for different optimizers")
plt.show()

Output[]
RMSProp...
Epoch: 1 - Loss:5794.6734
Epoch: 2 - Loss:1680.3092
Epoch: 3 - Loss:1169.5457
Epoch: 4 - Loss:1518.7088
Epoch: 5 - Loss:1727.5753
Epoch: 6 - Loss:661.7122
Epoch: 7 - Loss:532.6023
Epoch: 8 - Loss:2613.1597
Epoch: 9 - Loss:283.5713
Epoch: 10 - Loss:1058.1581

Adam...

Epoch: 1 - Loss:106.7566
Epoch: 2 - Loss:11.5689
Epoch: 3 - Loss:7.8169
Epoch: 4 - Loss:0.2327
Epoch: 5 - Loss:0.0313
Epoch: 6 - Loss:0.0034
Epoch: 7 - Loss:0.0019
Epoch: 8 - Loss:0.0012
Epoch: 9 - Loss:0.0009
Epoch: 10 - Loss:0.0007

SGD...

Epoch: 1 - Loss:801.0526
Epoch: 2 - Loss:131.7263
Epoch: 3 - Loss:296.2784
Epoch: 4 - Loss:240.0572
Epoch: 5 - Loss:248.2811
Epoch: 6 - Loss:248.2784
Epoch: 7 - Loss:248.2759
Epoch: 8 - Loss:248.2733
Epoch: 9 - Loss:248.2708
Epoch: 10 - Loss:248.2684

Listing 3-5Training Model with Various Optimizers

img/478491_2_En_3_Fig13_HTML.jpg

图 3-13

网络跨时段的分布损耗

摘要

本章关于前馈神经网络的内容将作为本书其余部分的概念基础。我们讨论的关键概念是神经网络的整体结构、输入、隐藏和输出层,以及成本函数及其基于最大似然原则的基础。我们还探索了 PyTorch 作为实际实现神经网络的方法。在最后一个练习中,我们在一个玩具数据集上用各种优化器对网络进行了训练,以研究损失是如何随着时代的推移而减少的。

下一章,我们将探讨深度学习中的自动微分。**

四、深度学习中的自动微分

在第三章中探讨随机梯度下降时,我们将损失函数𝛻xl(x)的梯度计算视为黑箱。在这一章中,我们打开了黑盒,涵盖了自动微分的理论和实践,以及探索 PyTorch 的亲笔签名的模块,实现了同样的功能。自动微分是一种成熟的方法,它可以轻松有效地计算任意复杂损失函数的梯度。当涉及到最小化感兴趣的损失函数时,这是至关重要的;构建任何深度学习模型的核心都是一个优化问题,总是使用随机梯度下降来解决,这反过来需要计算梯度。

自动微分不同于数值微分和符号微分。我们首先对这两者进行足够的介绍,以便区分变得清晰。为了便于说明,假设我们感兴趣的函数是 f : RR ,并且我们想要找到 f 的导数,表示为f(x)。

数值微分

数值微分的基本形式来自导数/梯度的定义。它用于估计数学函数的导数。y 相对于 x 的导数更具体地定义了 y 相对于 x 的变化率。一个简单的方法是通过线 x,f(x)和 x+h,f(x+h)计算函数的斜率。

所以,鉴于

$$ {f}^{\prime }(x)=\frac{df}{dx}=\frac{f\left(x+\varDelta x\right)-f(x)}{\varDelta x} $$

我们可以用向前差分法计算出 f(x)为

$$ {f}^{\prime }(x)={D}_{+}(h)=\frac{f\left(x+h\right)-f(x)}{h} $$

h 设置一个适当小的值。同样,我们可以用向后差分法计算f(x)为

$$ {f}^{\prime }(x)=D_(h)=\frac{f(x)-f\left(x-h\right)}{h} $$

同样,通过为 h 设置一个适当小的值。

一种更对称的形式是中心差分法,它将f计算为

$$ {f}^{\prime }(x)={D}_0(h)=\frac{f\left(x+h\right)-f\left(x-h\right)}{2h} $$

外推法是一种使用已知值来预测超出预期的现有已知范围的值的过程。 Richardson 外推法是一种技术,有助于实现仅使用几个数值系列来估计高阶积分。

$$ {f}^{\prime }(x)=\frac{4{D}_0(h)-{D}_0(2h)}{3} $$

前向和后向差分的逼近误差依次为 h ,即O(h)—而中心差分和 Richardson 外推的逼近误差分别为 O ( h 2 )和O(h4)。

数值微分的关键问题是计算成本,它随着损失函数中参数的数量、截断误差和舍入误差而增加。截断误差是我们在计算f(x)时由于 h 不为零而产生的不准确性。舍入误差是使用浮点数和浮点运算所固有的(与使用无限精度数相反,后者的成本高得惊人)。

因此,在构建深度学习模型时,数值微分不是计算梯度的可行方法。数值微分派上用场的唯一地方是快速检查梯度计算是否正确。当您已经手动计算梯度或使用新的/未知的自动微分库时,强烈建议您这样做。理想情况下,这种检查应该在启动 SGD 之前作为自动检查/断言进行。

Note

数值微分在一个名为 Scipy 的 Python 包中实现。我们在这里不涉及它,因为它与深度学习没有直接关系。

符号微分法

符号微分的基本形式是应用于损失函数以得到导数/梯度的一组符号重写规则。考虑两个这样简单的规则

$$ \frac{d\ }{dx}\left(f(x)+g(x)\right)=\frac{d}{dx}\ f(x)+\frac{d}{dx}\ g(x) $$

还有

$$ \frac{d}{dx}\ {x}n=n{x}{\left(n-1\right)} $$

给定一个函数,如f(x)= 2x3+x2,我们可以依次应用符号书写规则,首先到达

$$ {f}^{\prime }(x)=\frac{d}{dx}\ \left(2{x}³\right)+\frac{d}{dx}\ \left({x}²\right) $$

通过应用第一重写规则,和

$$ {f}^{\prime }(x)=6{x}²+2x $$

运用第二条规则。

因此,当我们手动推导梯度时,符号微分是自动化的。当然,这种规则的数量可以很大,并且可以利用更复杂的算法来使这种符号重写更有效。然而,在本质上,符号微分只是一套符号重写规则的应用。符号微分的主要优点是它为导数/梯度生成一个清晰的数学表达式,可以被理解和分析。

符号微分的关键问题是,它仅限于已经定义的符号微分规则,这可能导致我们在试图最小化复杂的损失函数时遇到障碍。例如,当损失函数涉及 if-else 子句或 for/while 循环时。从某种意义上说,符号微分是在微分一个(封闭形式的)数学表达式;它不区分给定的计算过程。

符号微分的另一个问题是,在某些情况下,符号重写规则的天真应用会导致符号项的爆炸(表达式膨胀),并使该过程在计算上不可行。通常,需要大量的计算工作来简化这样的表达式,并产生导数的封闭形式的表达式。

Note

符号微分是在一个名为 SymPy 的 Python 包中实现的。我们在这里不涉及它,因为它与深度学习没有直接关系。

自动微分基础

自动微分背后的第一个关键直觉是,所有感兴趣的函数(我们打算微分的)都可以表示为初等函数的组合,对于这些初等函数,相应的导函数是已知的。因此,复合函数可以用导数的链式法则来求导。这种直觉也是符号分化的基础。

自动微分背后的第二个关键直觉是,我们可以简单地评估它们(对于一组特定的输入值),从而解决表达式膨胀的问题,而不是存储和操纵原始函数的导数的中间符号形式。因为正在评估中间符号形式,所以我们没有简化表达式的负担。注意,这阻止了我们得到导数的封闭形式的数学表达式,就像符号微分给我们的那样;我们通过自动微分得到的是对一组给定值的导数的评估。

自动微分背后的第三个关键直觉是,因为我们正在计算原始形式的导数,我们可以处理任意的计算过程,而不仅仅是封闭形式的数学表达式。也就是说,我们的函数可以包含 if-else 语句、for 循环,甚至递归。自动微分处理任何计算过程的方式是将过程的单次评估(对于给定的一组输入)视为输入变量上初等函数评估的有限列表,以产生一个或多个输出变量。尽管可能有控制流语句(if-else 语句、for 循环等。),最终,有一个特定的函数求值列表,它将给定的输入转换为输出。这种列表/评估轨迹被称为文格特列表

为了理解自动微分对于深度学习用例是如何具体工作的,让我们以一个简单的函数为例,我们将使用链式规则手动计算它,并查看实现它的 PyTorch 等价物。

在深度学习网络中,使用计算图来表示整个流程,计算图是一种有向图,其中节点表示数学运算。这提供了一个容易评估数学表达式。计算图可以被翻译成数据结构,以便使用计算机编程语言有计划地解决问题,从而使得解决更大的问题更加直观。

我们将使用一个相对较小且易于计算的函数来完成我们的示例。

假设 f(x,y,z) = (x + y)*z,我们有三个变量的值,x=1,y =-2,z =3。

我们可以用计算图来表示这个函数,如图 4-1 所示。

img/478491_2_En_4_Fig1_HTML.jpg

图 4-1

计算图

除了输入变量(x、y 和 z),我们还会看到变量 a ,它是存储(x + y)的计算值的中间变量,以及变量 f ,它存储(x + y)z 的最终值,即 a*z

在正向传递中,我们将替换这些值,并得出最终值,如下所示

x = 1,y =-2,z= 3

然后,

(x + y )z = (1 - 2)3 = -3

因此,

f = -3

我们可以使用图 4-2 中所示的计算图对此进行可视化。

img/478491_2_En_4_Fig2_HTML.jpg

图 4-2

带有计算值的计算图

现在,通过自动微分,我们想要找到相对于输入变量(x,y 和 z)的 f 的梯度,输入变量表示为$$ \frac{\partial f}{\partial x} $$$$ \frac{\partial f}{\partial y} $$$$ \frac{\partial f}{\partial z} $$

在前馈网络中,本质上,我们找到损失函数相对于权重的梯度。为了解决这个问题,我们可以使用链式法则。

让我们找出上面方程的偏导数。

我们知道 a = (x + y),z = a * x,因而 f = az。

因此,

$$ \frac{\partial f}{\partial z}=\frac{\partial\ (az)}{\partial z}=a $$=(x+y)=(1–2)=-1

还有

$$ \frac{\partial f}{\partial a}=\frac{\partial\ (az)}{\partial a}=z $$

如果再进一步,我们可以求出 a 关于 x 和 y 的偏导数。

$$ \frac{\partial a}{\partial x}=\frac{\partial\ \left(x+y\right)}{\partial x}=1 $$$$ \frac{\partial a}{\partial y}=\frac{\partial\ \left(x+y\right)}{\partial y}=1 $$

现在,到了我们的最终目标,找到 f 相对于 x、y 和 z 的梯度。我们已经计算了相对于 z 的所需梯度。对于 x 和 y,我们可以利用之前在链式法则中计算的值作为

$$ \frac{\partial f}{\partial x}=\frac{\partial f}{\partial a}\frac{\partial a}{\partial x}=z\ast 1=3 $$

$$ \frac{\partial f}{\partial y}=\frac{\partial f}{\partial a}\frac{\partial a}{\partial y}=z\ast 1=3 $$

我们现在已经计算了所有需要的值。

$$ \frac{\partial f}{\partial x}=3,\kern0.75em \frac{\partial f}{\partial y} $$ = 3 和$$ \frac{\partial f}{\partial z} $$ = -1

本质上,网络会推断 x 和 y 对结果有正面影响,而 z 对结果有负面影响(图 4-3 )。该信息对于减少损失是有用的,并且递增地更新网络的权重以达到最小值。

img/478491_2_En_4_Fig3_HTML.jpg

图 4-3

含有偏导数的计算图

实现自动微分

现在让我们考虑在 PyTorch 中如何实现自动微分。前面的例子非常简单;当我们在纸上探索大型函数(即深度学习函数)的方法时,事情会变得非常复杂。在大多数常见网络中,涉及的参数数量非常多,使得手动编程梯度计算成为一项艰巨的任务。

PyTorch 提供了亲笔签名的包装,从本质上简化了我们的整个过程。回想一下我们在第三章中为玩具神经网络利用的loss.backward()函数。网络计算相对于权重的损失的所有必要梯度。让我们进一步探讨这个问题。

什么是亲笔签名?

PyTorch 中的自动签名包为 tensors 上的所有操作提供了自动区分。它在反向传播过程中为我们的神经网络执行必要的计算。当调用backward()函数时,模块自动计算所有反向传播梯度。我们也可以通过变量的grad属性来访问单独的渐变。

自动签名模块为实现任意标量值函数的自动微分提供了现成的工具(函数/类)。为了能够计算变量的梯度,我们只需要将关键字requires_grad的值设置为True

让我们复制我们用来手动实现自动微分的同一个例子,但是使用 PyTorch(清单 4-1 )。

#Import required libraries
import torch

#Define ensors
x = torch.Tensor([1])
y = torch.Tensor([-2])
z= torch.Tensor([3])

print("Default value for requires_grad for x:",x.requires_grad)

#Set the keyword requires_grad as True (default is False)
x.requires_grad=True
y.requires_grad=True
z.requires_grad=True

print("Updated  value for requires_grad for x:",x.requires_grad)

#Compute a
a = x + y

#Finally define the function f
f = z * a

print("Final value for Function f = ",f)

#Compute gradients

f.backward()

#Print the gradient value
print("Gradient value for x:",x.grad)
print("Gradient value for y:",y.grad)
print("Gradient value for z:",z.grad)

Output[]
Default value for requires_grad for x: False

Updated value for requires_grad for x: True

Final value for Function f = tensor([-3.], grad_fn=<MulBackward0>)
Gradient value for x: tensor([3.])
Gradient value for y: tensor([3.])
Gradient value for z: tensor([-1.])

Listing 4-1Implementing Automatic Differentition (Autograd) in PyTorch

这里的梯度值与我们之前手动计算的值完全匹配。

在前面的例子中,我们首先创建了一个张量,然后将关键字requires_grad指定为True。我们也可以把这个和我们的定义结合起来。

x = torch.autograd.Variable(torch.Tensor([1]),requires_grad=True)

当我们在 PyTorch 中定义一个网络时,很多细节都被考虑到了。当我们定义一个网络层时,用nn.Linear(64, 256)(参考章节 3 的例子),PyTorch 用必要的值创建权重和偏差张量(设置requires_gradTrue)。输入张量不需要梯度;因此,在我们的例子中,我们从不设置它们,而是使用默认值(例如,False)。

摘要

本章讲述了自动微分的基础知识。反向传播是用于训练深度神经网络的自动微分的特例。在现代深度学习文献中,自动微分类似于反向传播,因为它是一个更广义的术语。本章的关键要点是,自动微分能够计算任意复杂损失函数的梯度,是深度学习的关键使能技术之一。你应该理解自动微分的概念,以及它与符号微分和数值微分的区别。

在下一章中,我们将更详细地研究一些与深度学习相关的其他主题,包括性能指标和模型评估,分析过拟合和欠拟合,正则化和超参数调整。最后,我们将把我们迄今为止所涉及的关于深度学习的所有基础知识结合到一个实际例子中,该例子为真实世界的数据集实现了前馈神经网络。

五、训练深度学习模型

到目前为止,我们已经利用玩具数据集来提供深度学习模型的最早实现的概述。在这一章中,我们将围绕深度学习探索几个额外的重要主题,并在一个实际例子中实现它们。我们将深入研究模型性能的细节,并研究过拟合和欠拟合、超参数调整和正则化的细节。最后,我们将结合我们目前所讨论的内容和一个真实的数据集来展示一个使用 PyTorch 的实际例子。

性能指标

在第三章中,当我们设计我们的玩具神经网络时,我们定义了损失函数来衡量预测和实际标签之间的差异。让我们用更有意义的方式来探讨这个话题。基于目标变量的类型(连续或离散),我们将需要不同类型的性能指标。接下来的部分将讨论每个类别中的指标。

分类指标

模型开发过程通常从制定清晰的问题定义开始。这基本上包括定义模型的输入和输出,以及这样一个模型能够交付的影响(有用性)。这种问题定义的一个例子是将产品图像分类成产品类别——这种模型的输入是产品图像,输出是产品类别。这种模型可能有助于在电子商务或在线市场环境中对产品进行自动分类。

定义了问题定义之后,下一个任务是定义性能指标。性能指标的主要目的是告诉我们我们的模型做得有多好。一个简单的性能度量可以是准确性(或者,等价地,误差),它简单地度量了预期输出和模型产生的输出之间的不一致。然而,准确性可能是一个很差的性能指标。两个主要原因是阶级不平衡和不平等的错误分类成本。我们用一个例子来看一下阶层失衡问题。作为我们之前产品分类例子中问题的子问题,考虑区分手机及其配件的情况。移动电话类别的示例数量比移动电话配件的类别少得多。例如,如果 95%的例子是移动电话配件,5%是移动电话,则通过预测多数类可以简单地获得 95%的准确度。因此,在这个例子中,准确性是一个很差的度量选择。

现在让我们通过考虑一个与产品分类问题相关的例子来理解不相等的错误分类成本的问题。考虑将不含过敏原的食品(不含八大过敏原——即牛奶、鸡蛋、鱼、甲壳类贝类、坚果、花生、小麦和大豆)与其他食品(不含过敏原)进行分类的错误。从购买者的角度以及商业的角度来看,与将不含过敏原的产品归类为不含过敏原的产品相比,将不含过敏原的产品归类为不含过敏原的产品的错误明显更多。精度没有捕捉到这一点,因此在这种情况下将是一个糟糕的选择。

另一组度量标准是精度和召回率,它们分别测量预测类中正确恢复的预测的比例,以及报告的预测类的比例(见图 5-1 )。总的来说,精确度和召回率对于类别不平衡是鲁棒的。

img/478491_2_En_5_Fig1_HTML.jpg

图 5-1

精确度和召回率

精确度和召回率通常使用 PR 曲线来可视化,该曲线在 Y 轴上绘制精确度,在 X 轴上绘制召回率(参见图 5-2 )。通过改变分数的决策阈值或模型产生的概率,可以获得不同的精度和召回值,例如,0 表示 A 类,1 表示 B 类,较高的值在一侧表示特定的类。该曲线可用于通过改变阈值来折衷召回的精确度。

img/478491_2_En_5_Fig2_HTML.jpg

图 5-2

PR 曲线

定义为$$ \frac{2 pr}{p+r} $$F 值,其中 p 表示精度 r 表示召回,可以用来概括 PR 曲线。

接收器工作特性(ROC) 曲线在类别不平衡和错误分类成本不等的情况下是有用的。在这种背景下,例子被认为属于两类:积极和消极。

真阳性率测量真阳性相对于实际阳性的比例,真阴性率测量真阴性相对于实际阴性的比例(见图 5-3 )。ROC 曲线在 X 轴上绘制真阳性率,在 Y 轴上绘制假阳性率(见图 5-4 )。曲线下的面积(AUC)用于概括 ROC 曲线。

在许多情况下,标准的度量标准,如准确度、精确度、召回率等。不允许我们真实地捕捉手边业务用例的模型性能。在这种情况下,需要制定适合业务用例的度量标准,记住问题的性质、类别不平衡和错误分类的成本。例如,在我们运行的产品分类示例中,我们可以选择不使用低置信度的预测,而是手动对它们进行分类。手动分类的例子是有成本的,在电子商务网站上错误的类别中显示错误的产品也有不同的成本。对流行产品进行错误分类的成本也不同于(通常更高)对很少购买的产品进行错误分类的成本。在这种情况下,我们可以选择只使用模型中的高可信度预测。要使用的度量的一个可能的选择是错误分类的例子的数量(具有高置信度)和覆盖范围(被高置信度覆盖的例子的数量)。人们也可以通过对两者进行加权平均来考虑这种设置中的误分类成本。(可以基于错误分类成本来选择适当的权重。)

在行业环境中,指标定义是建模过程的关键步骤。从业者应该深入分析业务领域,理解错误分类成本和数据,理解类分布,并相应地设计性能度量。定义不当的度量标准会导致项目走向错误的道路。

img/478491_2_En_5_Fig4_HTML.jpg

图 5-4

受试者工作特征曲线

img/478491_2_En_5_Fig3_HTML.jpg

图 5-3

真阳性和假阳性率

回归度量

与分类指标相比,回归的性能指标相当简单。可以普遍应用于大多数用例的最常见指标是均方误差(MSE)。根据用例,可以使用一些其他指标来获得更有利的结果。考虑预测给定商店的月销售额的问题,其中商店几个月的销售额可能在$5,000 到$50,000 之间。

以下部分探讨了一些流行的选择。

均方误差

我们已经在第三章“前馈神经网络”中探讨了均方误差(MSE)顾名思义,MSE 是实际值和预测值的平方差的平均值。最终结果是一个正数,因为我们取了分歧的平方。本质上,平方运算是有价值的,因为较大的差异会受到更多的惩罚。在您不希望模型更严重地惩罚较大差异的用例中,MSE 不是理想的选择。给定模型的 MSE 越低,该模型的性能越好。

数学上,我们可以将 MSE 定义为

$$ MSE=\frac{1}{n}{\sum}_{i=0}^n{\left({y}_i-{\hat{y}}_i\right)}² $$

$$ RMSE=\sqrt{\frac{\sum_{i=0}^n{\left({y}_i-{\hat{y}}_i\right)}²}{n}} $$

绝对平均误差

平均绝对误差 (MAE)计算预测值和目标值之间的绝对差值的平均值。对于回归用例,结果总是积极的,是比 MSE 更容易解释的性能度量。模型的 MAE 越低,性能越好。

数学上,我们可以将 MAE 定义为

$$ MAE=\frac{1}{n}{\sum}_{i=0}^n\left|{y}_i-{\hat{y}}_i\right| $$

平均绝对百分比误差

平均绝对百分比误差 (MAPE)是 MAE 的百分比当量。鉴于它的相对性质,它是迄今为止最容易解释的回归性能指标。模型的 MAPE 越低,模型的性能越好。

数学上,我们可以把 MAPE 定义为

$$ MAPE=\frac{1}{n}{\sum}_{i=0}^n\frac{\left|{y}_i-{\hat{y}}_i\right|}{y_i} $$

虽然具有高度的可解释性,但 MAPE 在处理小的差异时会感到痛苦。微小偏差的百分比差异通常会导致较大的 MAPE,从而导致误导性结果。例如,假设我们预测给定商店的销售天数,目标值的范围是 0 到 60。当实际值为 2 且预测值为 6 时,MAPE 为 400%,而当实际值为 10 且预测值为 12 时,MAPE 为 20%。

数据采购

数据获取是根据一个问题陈述,为建立模型而收集数据的过程。数据获取可能涉及从生产系统收集旧的(已经生成的)数据,从生产系统收集实时数据,并且在许多情况下,收集由人工操作员标记的数据(通过众包或内部运营团队)。在我们运行的产品分类示例中,产品标题、图片、描述等。将需要从公司目录中收集,标记的数据可以使用众包生成。我们可能还想收集点击数据和销售额来确定受欢迎的产品。(在这些情况下,错误分类的代价很高。)

数据获取通常与定义问题陈述和成功度量的过程一起发生。从业者必须在数据获取过程中扮演积极的角色。通常,在行业环境中,数据采集是一个相当耗时且痛苦的过程。数据采集中的细微错误可能会在后期破坏项目。

为培训、验证和测试拆分数据

一旦获得了用于构建模型的数据,就需要将它分成用于训练、参数调整和上线测试的数据。从概念上讲,现有数据将用于三个不同的目的。第一个目的是训练模型,也就是说,模型将尝试拟合这些数据。第二个目的是确定模型是否过度拟合数据;这个数据集被称为验证集。这些数据不会用于训练,但会推动超参数调整、正则化技术等方面的决策。(我们将在本章后面更详细地讨论这些主题。)数据的第三个目的是确定模型是否真的好到足以投入生产/上线(称为测试集)。

要内化的第一个关键概念是,数据不能为了这三个目的而共享;每个目的都需要数据的不同部分。如果数据的某一部分已用于训练模型,则不能用于调整模型的超参数或用作最终的性能关口(生产/上线)。同样,如果数据的某一部分已用于调整参数,则它不能作为生产/上线的测试数据。因此,从业者需要将数据分成三个部分:培训、参数调整和上线。虽然训练数据应该不同于用于参数调整的数据的想法是直观的,但拥有不同的上线设置背后的推理却不是。内化的关键点是,如果模型已经看到了数据,或者建模者已经看到了数据,那么这些数据已经从根本上驱动了围绕模型的一些决策,并且如果我们需要测试真正盲测,则这些数据不能用于最终的上线测试。真正的盲目意味着从不看数据(和标签)或从不使用它来做出任何建立模型的决定。不能通过查看上线测试集的结果来进一步调整模型。

要内在化的第二个关键点是,三个集合(训练、超参数调整和上线测试)中的每一个都必须是底层数据群体的真实代表。分割数据集时应考虑到这一点。例如,示例在各个类中的分布应该与基础总体相同。如果数据不是真实的表示(也就是说,如果数据在任何方面都有偏差),那么一旦模型投入生产,模型的性能就无法实现。

要内化的第三个关键点是,对于这三个目的中的任何一个,更多的数据总是更好的。因为数据集不能重叠,并且整个数据集是有限的,所以从业者需要仔细选择用于每个目的的数据部分。培训、验证和测试之间 50/25/25 或 60/20/20 的分割是合理的选择。

建立差错率的可实现极限

定义了问题和性能指标,获取了数据并将其分为培训、参数调整和上线测试集,下一步是建立可实现的错误率限制。从概念上讲,这是在给定无限数据供应的情况下人们希望达到的错误率,被称为贝叶斯错误。在人工智能任务中建立错误率的限制通常是通过类似代理的人工标记或适合业务用例的主题变化来完成的。变化可以包括使用该主题的专家、一组人或一组专家来标记数据。建立这个限制是很有价值的,值得花费人力/专家的帮助。首先,它建立了可能达到的最佳结果,在某些情况下,可能不足以满足业务用例(在这种情况下,需要重新考虑问题的表述)。第二,它告诉我们当前的模型离可实现的最佳结果有多远。

用标准选择建立基线

开始建模过程的最佳位置是具有架构和算法的标准选择(基于文献或部分经验)的基线模型——例如,对图像使用卷积神经网络(CNN ),对序列使用长短期记忆(LSTM)网络。(这两个主题将在接下来的章节中讨论。)使用校正线性单元(ReLUs)作为激活单元和批量随机梯度下降(SGD)也是很好的选择。基本上,基线模型建立了一个稻草人,基于对缺点的分析进行改进。

构建自动化的端到端管道

确定基线模型后,建立端到端的全自动管道至关重要,这包括在训练集上训练模型,在参数调整集上进行预测,以及在两个集上计算指标。自动化是非常重要的,因为它使从业者能够通过调整模型架构和超参数来快速迭代新模型。

可视化流程编排

在构建端到端管道时,加入流程编排来可视化激活直方图、梯度、训练和验证集的指标等也是一个好主意。在调试意外行为时,模型训练、权重和性能的可见性非常有用。关键点是首先要为可见性构建自动化和流程编排。这样以后会节省很多时间和精力。

过拟合和欠拟合分析

模型改进的迭代周期的理想目标是开发一个模型,在该模型中,训练集和验证集的性能几乎等于已建立的性能限制(贝叶斯误差的代理)。图 5-5 说明了模型改进过程的最终目的。然而,在迭代开发新模型时,从业者将会遇到欠拟合和过拟合。欠拟合发生在模型在训练集和验证集上的性能几乎相等,但性能低于期望水平的时候。这是一个开发不良的模型的结果,其中的参数没有适当地捕获训练数据中的模式。另一方面,过拟合发生在模型在验证集上的性能显著低于其在训练集上的性能时。这是一个模型的直接结果,该模型已经学习了太多复杂的模式,这些模式在理想情况下应该被视为噪声。这种模型(将数据中的噪声作为有效模式)在训练(可见)数据上表现最佳,但在不可见数据上表现不佳。欠拟合和过拟合并不相互排斥。在模型拟合不足的情况下,我们更正式地将这种情况定义为具有高偏差的模型。类似地,当一个已经从噪声中学习了几个复杂模式的模型在看不见的数据上提供高度不一致的性能时,我们说该模型具有高方差。理想情况下,我们需要一个低偏差和低方差的模型。

检测模型是过拟合还是欠拟合是训练新模型后的第一步。在欠拟合的情况下,关键步骤是增加模型的有效容量,这通常通过修改架构(增加层、宽度等)来完成。在过度拟合的情况下,关键的步骤是正则化方法(本章后面会讲到)或增加数据集的大小。一个重要的可视化是学习曲线,它在 Y 轴上绘制性能指标,在 x 轴上绘制模型可用的训练数据。这对于确定投资获取更多标签数据是否有意义非常有用。

img/478491_2_En_5_Fig5_HTML.jpg

图 5-5

过度拟合和欠拟合

超参数调谐

调整模型的超参数(例如学习率或动量)可以通过网格搜索(其中网格是在一小组值上定义的)或通过随机搜索(其中超参数的值是从用户定义的分布中随机抽取的)手动完成。

在网格搜索中,从业者必须为网络中的每个超参数创建一个潜在值的小子集(因为计算资源是有限的)。训练过程基本上循环通过每个可能的组合,并且具有最佳性能的超参数组合是最终选择。使用网格搜索,有可能不具有超参数的最佳可能组合,因为如果大量选择被添加到网格中,排列被限制到所提供的网格或者在计算上非常昂贵。

随机搜索通常更适合超参数调整。通过随机搜索,模型获得超参数最佳组合的可能性较高,但组合数量相对较少(尽管不能保证)。

调整超参数通常是迭代的和实验性的。

模型容量

让我们简单回顾一下模型容量、过拟合和欠拟合的概念。我们将使用之前拟合回归模型的例子(参见第一章)。

我们有格式为 D = {( x 1y 1 ),( x 2y 2 ),…(xny n 我们的任务是生成一个计算程序,实现函数f:xy。 我们用看不见的数据的均方根误差(RMSE)来衡量这个任务的性能,如下:

$$ E\left(f,D,U\right)={\left(\frac{\sum_{\left({x}_i,{y}_i\right)\in U}\ {\left({y}_i-f\left({x}_i\right)\right)}²\ }{\mid U\mid}\right)}^{\frac{1}{2}}. $$

给定一个形式为 D = {( x 1y 1 ),( x 2y 2 ),…(xny n 我们使用最小二乘模型,其形式为 y = βx ,其中 β 是使$ {\left\Vert X\beta -y\right\Vert}_2² $最小化的向量。 这里, X 是一个矩阵,其中每一行是一个 xβ 的值可以用封闭形式β=(XTX)—1XTy导出。

我们可以把 x 变换成一个值的向量[ x 0x 1x 2 ]。也就是说,如果 x = 2,就转化为【1,2,4】。在这个变换之后,我们可以使用前面的公式生成最小二乘模型 β 。在引擎盖下,我们用一个二阶多项式(次数= 2)方程来逼近给定的数据,最小二乘算法只是简单地曲线拟合或生成每个x0、x1、x2 的系数。

同样,我们可以用最小二乘算法生成另一个模型,但是我们将把 x 变换为[ x 0x 1x 2x 3x 4x 5 也就是说,我们用次数= 8 的多项式来逼近给定的数据。通过增加多项式的次数,我们可以拟合任意数据。很容易看出,如果我们有 n 个数据点,一个次数为 n 的多项式可以完美地拟合这些数据。也很容易看出,这样的模型只是简单地记忆数据。我们可以使用这个例子来开发模型容量、过度拟合和欠拟合的透视图。我们用来拟合数据的多项式的次数基本上代表了模型的能力。度数越大,模型的容量越高。

让我们假设数据是使用带有一些噪声的 5 次多项式生成的。另外,请注意,在拟合数据时,我们对生成数据的过程一无所知。我们必须制作一个最符合数据的模型。本质上,我们不知道有多少数据是模式,有多少数据是噪声

在这样的数据集上,如果我们使用具有足够高容量的模型(多项式的次数大于 5,在最坏的情况下等于数据点的数量),当在训练数据上评估时,我们可以获得完美的模型;然而,这个模型在看不见的数据上表现很差,因为它本质上符合噪声。这太合身了。如果我们使用低容量(小于 5)的模型,它将既不适合训练数据也不适合看不见的数据。这是不合适的。

正则化模型

从前面的例子中可以很容易地看出,在拟合模型时,一个中心问题是准确地获得模型的容量,以便既不过度拟合也不欠拟合数据。规则化可以简单地看作是对模型(或其训练过程)的任何修改,旨在通过系统地限制模型的能力来改善未知数据的误差(以训练数据的误差为代价)。这种系统地限制或调节模型能力的过程是由未用于训练的一部分标记数据来引导的。这些数据通常被称为验证集

在我们运行的例子中,最小二乘法的正则化版本采用的形式是 y = βx ,其中 β 是使![$$ {\left\Vert X\beta -y\right\Vert}_2²+\lambda {\left\Vert \beta \right\Vert}_2² $$最小化的向量, λ 是控制复杂度的用户定义参数。这里,通过引入术语$$ \lambda {\left\Vert \beta \right\Vert}_2² $$,我们惩罚了具有额外容量的模型。要了解为什么会出现这种情况,请考虑使用 10 次多项式拟合最小二乘模型,但向量 β 中的值有 8 个零和 2 个非零值。与此相反,考虑向量 β 中的所有值都不为零的情况。出于所有实际目的,前一个模型是一个 degree = 2 并且具有较低值$$ \lambda {\left\Vert \beta \right\Vert}_2² $$的模型。 λ 项允许我们平衡训练数据的准确性和模型的复杂性。 λ 的较低值意味着型号容量较低。

一个自然的问题是,为什么我们不简单地使用验证集作为指导,并增加前面例子中多项式的次数。既然多项式的次数代表了模型的容量,为什么我们不能用它来调整模型的容量呢?为什么我们需要在模型中引入变化($$ {\left\Vert X\beta -y\right\Vert}_2²+\lambda {\left\Vert \beta \right\Vert}_2² $$而不是之前的$$ {\left\Vert X\beta -y\right\Vert}_2² $$)。答案是我们想要系统地限制模型的容量,我们需要一个细粒度的控制。通过改变模型的程度来改变模型容量是非常粗粒度的、离散的旋钮,而改变 λ 是非常细粒度的。

提前停止

深度学习中最简单的正则化技术之一就是提前停止。给定一个训练集和一个验证集以及一个有足够容量的网络,我们观察到随着训练步数的增加,首先训练集和验证集的误差都减小,然后训练集的误差继续减小,而验证的误差增加(见图 5-6 )。

早期停止的关键思想是跟踪在验证集上给出最佳性能的模型参数/权重,然后在这个验证集上迄今的最佳性能在预定义数量的训练步骤上没有改善之后停止训练。

img/478491_2_En_5_Fig6_HTML.jpg

图 5-6

提前停止

通过限制模型的参数/权重值,早期停止起到了正则化的作用(见图 5-7 )。提前停止限制 w 到起始值附近的一个邻域内(在w0 附近)。所以,如果我们停在 w s 处,ws+1的值是不可能的。这实质上限制了模型的容量。

早期停止是非侵入性的,因为它不需要对模型做任何改变。它也很便宜,因为它只需要存储模型的参数(这是迄今为止验证集上最好的)。它也可以很容易地与其他正则化技术相结合。

img/478491_2_En_5_Fig7_HTML.jpg

图 5-7

提前停止限制 w

标准惩罚

范数惩罚是深度学习(以及一般的机器学习)中一种常见的正则化形式。这个想法仅仅是在神经网络的损失函数中增加一项 r ( θ )(参见第三章),其中 r 通常表示 L 1 范数或者 L 2 范数,而 θ 表示网络的参数/权重。这样,正则化的损失函数就变成了l(fNN(xθ ),y+α**r(θ,而不仅仅是 l ( f )注意, α 项是正则化参数。

Note

一般来说,一个 L p 定额定义为‖xp=(σI|xI|p)1/p据此, L 1 定额定义为‖x1=(σI|xI|11/1I同理, L 2 定额定义为‖x2=(σI||I|2)**

让我们更深入地研究正则化损失函数l(fNN(xθ ),y)+α**r(θ)。应注意以下几点:

  1. 由于我们试图最小化总损失函数l(fNN(xθy)+α**r(θ),我们试图减少l(f)

  2. 接下来对于两组参数,θa和θb,如果l(fNN(xθ ay θ b ), y ),那么优化算法就会选择 θ a 如果r(θa<r(θ

  3. 因此,正则项的作用是将优化导向降低 r ( θ )的 θ 方向。

  4. 很容易看出,当 r 对应于L1 正则化时,较低的 r ( θ )值将导致更稀疏的 θ ,从而降低有效容量。

  5. 很容易看出,当 r 对应于L2 正则化时, r ( θ )的较低值将导致 θ 更接近于 0,从而降低有效容量(见图 5-8 )。

  6. α 项用来控制我们对l(fNN(xθ ), y )对 r ( θ )的重视程度。较高的值 α 意味着更加重视正则化。

必须注意,范数惩罚应用于权重向量,而不是偏差项。背后的原因是,任何正则化都是过度拟合和欠拟合之间的权衡,正则化偏差项会由于太多的欠拟合而导致糟糕的权衡。在训练深度学习网络时,不同的层可以使用不同的值 α ,并且通过使用验证集作为指导的实验来确定合适的值 α

img/478491_2_En_5_Fig8_HTML.jpg

图 5-8

L 2 范数导致θ更接近于零。θ a 由于正则化而被优化算法选取;没有它,θ b 将被选取

拒绝传统社会的人

Dropout 本质上是模型集合/平均的计算廉价替代方案。让我们首先考虑模型集合/平均的关键概念。虽然具有足够容量的单个模型可能会过度拟合,但如果我们对多个模型(根据数据子集、不同权重初始化或不同超参数进行训练)的预测进行平均或多数投票,我们就可以解决过度拟合问题。模型集成/平均是一种非常有用的正则化形式,可以帮助我们处理过拟合问题。然而,考虑到我们必须训练多个模型并对多个模型进行预测(然后通过投票或平均将它们组合起来),这在计算上是相当昂贵的。对于具有多层的深度学习模型,这种计算开销特别高。辍学提供了一个廉价的选择。

dropout 的关键思想是在以概率 p 训练网络时随机丢弃单元及其连接,然后在预测时将学习到的权重乘以 p (见图 5-9 )。让我们用数学表达式的形式来精确地表达这个想法。一个标准的神经网络层可以表示为y=f(w**x+b,其中 y 为输出, x 为输入, f 为激活函数, wb 分别为权向量和偏置项。训练时的一个漏层可以表示为y=f(w(xr)+b,其中 r ~ 伯努利 ( p ),符号⨀表示两个向量的逐点相乘(如果a= 在预测时,漏层可以表示为y=f(pwx)+b)。

很容易看出,dropout 层在训练的同时,实际上训练了多个网络,至于每一个不同的 r ,我们都有一个不同的网络。很容易看出,在预测时间,我们对多个网络进行平均,如y=f(pwx)+b)。

在使用批量随机梯度进行辍学训练时,在整个批次中使用单一值 r 。在相关文献中, p 的推荐值对于输入单元为 0.8,对于隐藏单元为 0.5。发现对丢失有用的范数正则化是最大范数正则化,其中 w 被约束为‖w2<c,其中 c 是用户定义的参数。

img/478491_2_En_5_Fig9_HTML.jpg

图 5-9

拒绝传统社会的人

PyTorch 中的实际实现

现在,我们将通过一个实际的例子来探讨我们到目前为止已经讨论过的主题。出于本练习的目的,我们将使用托管在 https://www.kaggle.com/janiobachmann/bank-marketing-dataset 的银行电话营销数据集。原始数据集来自 UCI 机器学习知识库,由[Moro et al .,2014]提供。与原始数据集相比,Kaggle 上托管的子集是一个平衡的数据集(正样本和负样本的数量相似),并且使练习的目的更加容易。

到目前为止,我们已经探索了使用 Python 制作的玩具数据集,因此我们几乎没有探索在建立深度学习模型之前必不可少的数据处理和数据工程的想法。这适用于所有形式的数据可视化——表格、图像、文本、音频/视频/语音等。在本练习中,我们将了解一些基本的数据处理步骤。尽管大量的数据处理超出了本书的范围,但本文的目的是让您了解现实生活用例可能需要的处理类型。

让我们开始吧。在下载前述数据集之前,您首先需要在 www.kaggle.com 注册并创建一个帐户。在清单 5-1 中,我们为我们的练习导入了基本的 Python 包。

#Import required libraries
import torch.nn as nn
import torch as tch
import numpy as np, pandas as pd
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.metrics import precision_score, recall_score,roc_curve, auc, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import matplotlib.pyplot as plt

Listing 5-1Importing the Required Libraries

Sklearn 是 Python 中的一个机器学习库,它提供了一个全面的算法、度量、数据处理工具和其他实用函数的列表。我们使用 sklearn 中的指标模块来获得方便的函数,这些函数有助于计算模型性能——精度、召回率、准确度等指标。类似地,Pandas 是一个很棒的 Python 包,它提供了处理、操作和探索表格数据帧的综合方法。在我们的练习中,我们将使用 Pandas 来读取和探索数据集,并利用 Pandas 中的一些功能来定制数据集以满足我们在 PyTorch 中的需求。清单 5-2 展示了使用 Pandas 将数据加载到内存中。

#Load data into memory using pandas
df = pd.read_csv("/Users/Downloads/dataset.csv")
print("DF Shape:",df.shape)
df.head()

Out[]
DF Shape: (11162, 17)

Listing 5-2Loading Data into Memory

img/478491_2_En_5_Figa_HTML.jpg

在 Jupyter 笔记本上使用 Pandas 提供了一种迭代探索数据的优雅方式。前面的输出是df.head()命令的结果,它打印数据集的前五行;df.shape命令将数据集的形状表示为[rows x columns]。

在这个数据集中,我们获得了银行电话营销活动的详细信息。该数据集捕获目标客户的详细信息、关于之前和当前营销电话的一些详细信息,以及成功结果存款。客户属性包括年龄工作、婚姻状况(婚姻)、学历、是否有拖欠付款(违约)、当前银行余额(余额)、住房贷款和个人贷款指标。活动属性包括联系类型(联系)、联系时间(日/月)和持续时间(持续时间)、代理执行的联系次数(活动)、前一次联系的天数(p 天)、前一次联系次数(前一次)和前一次结果( poutcome )。

有关数据集中属性的详细说明,请访问 https://archive.ics.uci.edu/ml/datasets/Bank+Marketin g

我们的目标是建立一个深度学习模型,对给定客户和活动组合的结果(存款)进行正确分类。让我们首先看看目标列在数据集中的分布。清单 5-3 展示了探索目标值的分布。

print("Distribution of Target Values in Dataset -")
df.deposit.value_counts()

Out[]:
Distribution of Target Values in Dataset -
no     5873
yes    5289
Name: deposit, dtype: int64

Listing 5-3Distributing the Target Values

我们可以看到,在我们的数据集中,yesno之间有大致相似的分布。清单 5-4 探究了数据集中空值的分布。

#Check if we have 'na' values within the dataset
df.isna().sum()

Out[]:
age          0
job          0
marital      0
education    0
default      0
balance      0
housing      0
loan         0
contact      0
day          0
month        0
duration     0
campaign     0
pdays        0
previous     0
poutcome     0
deposit      0
dtype: int64

Listing 5-4Distributing the NA (Null) Values in the Dataset

数据集没有任何 NA 值或缺失值。在大多数现实生活的数据集中,这可能不成立。研究人员和数据工程师花费大量时间处理缺失值或异常值。以下是您应该独立试验的附加检查:

  • 检查异常值。

    • 确定处理数据中异常值的策略。
      • 平均输入。

      • 用模式输入。

      • 用中位数输入。

      • 使用其他先进技术(基于聚类的回归插补技术来处理值)。

  • 检查缺少的值。

    • 确定处理缺失价值的策略。

    • 删除记录(如果缺失记录的数量< = 3%)。

    • 用类似于离群值的方法估算记录。

接下来,让我们探索数据集中不同的数据类型。深度学习模型只理解数字。更具体地说,PyTorch 只处理 32 位浮点数。我们需要将数据集转换成适合 PyTorch 使用的形式。清单 5-5 探究了不同数据类型的分布。

#Check the distinct datatypes within the dataset
df.dtypes.value_counts()
Out[]:

int64     11
object     6
dtype: int64

Listing 5-5Distributing the Distinct Datatypes

我们有六个基于 object (string)数据类型的列,我们需要在构建模型之前将它们转换成数字标志。我们将把分类列转换成独热编码形式,其中每个类别值都表示为一个二进制标志。但是,在此之前,让我们手动将具有 yes/no 二进制类别的列转换为一个列,并利用一个基于 Pandas 的函数来自动转换剩余的分类列集。清单 5-6 演示了从数据集中提取分类列。

#Extract categorical columns from dataset
categorical_columns = df.select_dtypes(include="object").columns
print("Categorical cols:",list(categorical_columns))

#For each categorical column if values in (Yes/No) convert into a 1/0 Flag
for col in categorical_columns:
    if df[col].nunique() == 2:
        df[col] = np.where(df[col]=="yes",1,0)

df.head()

Listing 5-6Extracting Categorical Columns from the Dataset

img/478491_2_En_5_Figb_HTML.jpg

我们可以看到,我们的目标列存款和少数其他列,包括装载默认住房,已经被转换为二进制标志(手动)。对于具有非二进制分类值的剩余列集,我们可以利用 Pandas get_dummies函数来自动处理它们。清单 5-7 对数据集中的分类变量进行一次性编码。

#For the remaining cateogrical variables;
#create one-hot encoded version of the dataset
new_df = pd.get_dummies(df)

#Define target and predictors for the model
target = "deposit"
predictors = set(new_df.columns) - set([target])
print("new_df.shape:",new_df.shape)
new_df[predictors].head()

Out[]:

new_df.shape: (11162, 49)

Listing 5-7One-Hot Encoding for the Remaining Non-Binary Categorical Variables

img/478491_2_En_5_Figc_HTML.jpg

我们现在已经定义了一个包含所有独立预测值列名的预测值列表,以及一个包含我们的 y(即存款列名)的目标。

Pandas 中的get_dummies函数将new_df数据帧中的所有分类列作为一个热编码形式进行处理。清单 5-7 的上述输出将列的视图限制为前几个;我们可以看到联系人现在转化为联系人 _ 未知联系人 _ 蜂窝等。数据集现在只有数字列。

最后,在设计我们的神经网络之前,我们需要将所有列转换为 float32 数据类型,并拆分为训练和验证数据集,然后转换为 PyTorch 张量。清单 5-8 为训练和验证准备数据集。

#Convert all datatypes within pandas dataframe to Float32
#(Compatibility with PyTorch tensors)
new_df = new_df.astype(np.float32)

#Split dataset into Train/Test [80:20]
X_train,x_test, Y_train,y_test = train_test_split(new_df[predictors],new_df[target],test_size= 0.2)

#Convert Pandas dataframe, first to numpy and then to Torch Tensors
X_train = tch.from_numpy(X_train.values)
x_test  = tch.from_numpy(x_test.values)
Y_train = tch.from_numpy(Y_train.values).reshape(-1,1)
y_test  = tch.from_numpy(y_test.values).reshape(-1,1)

#Print the dataset size to verify
print("X_train.shape:",X_train.shape)
print("x_test.shape:",x_test.shape)
print("Y_train.shape:",Y_train.shape)
print("y_test.shape:",y_test.shape)

Out[]:
X_train.shape: torch.Size([8929, 48])
x_test.shape: torch.Size([2233, 48])
Y_train.shape: torch.Size([8929, 1])
y_test.shape: torch.Size([2233, 1])

Listing 5-8Preparing the Dataset for Training and Validation

我们现在已经为我们的深度学习实验准备好了数据集。在设计我们的网络之前,让我们先准备一些可以在实验中重复使用的基本构件。清单 5-9 展示了在 PyTorch 中训练模型的样板代码。

Note

在本书的练习中,我们总是将数据集分为 80%的训练和 20%的验证(与前面讨论的将其分为训练、验证和测试相反)。在真实的生产实验中,我们建议读者拥有一个单独的测试数据集,可以在投入生产系统之前完成所需的检查。

#Define function to train the network
def train_network(model,optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=0.0):
    loss_across_epochs = []

    for epoch in range(num_epochs):
        train_loss= 0.0

        #Explicitly start model training
        model.train()

        for i in range(0,X_train.shape[0],batch_size):

            #Extract train batch from X and Y
            input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
            labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

            #set the gradients to zero before starting to do backpropragation
            optimizer.zero_grad()

            #Forward pass
            output_data  = model(input_data)

            #Caculate loss
            loss = loss_function(output_data, labels)
            L1_loss = 0

            #Compute L1 penalty to be added with loss
            for p in model.parameters():
                L1_loss = L1_loss + p.abs().sum()

            #Add L1 penalty to loss
            loss = loss + lambda_L1 * L1_loss

            #Backpropogate
            loss.backward()

            #Update weights
            optimizer.step()

            train_loss += loss.item() * input_data.size(0)

        loss_across_epochs.append(train_loss/X_train.size(0))
        if epoch%500 == 0:
            print("Epoch: {} - Loss:{:.4f}".format(epoch,train_loss/X_train.size(0) ))

    return(loss_across_epochs)

Listing 5-9Defining the Function to Train the Model

前面的函数在定义数量的时期内分批循环,并训练我们的神经网络。你已经熟悉这个功能了(参见第三章);当我们使用 L1 正则化时,该函数唯一新增加的是 L1 罚函数的计算。lambda_L1变量是一个超参数,我们可以调整它来控制 L1 正则化的效果。

现在,让我们定义一个函数,该函数可用于绘制各时期的损失、训练和验证数据集的 ROC 曲线,以及评估模型的重要指标。因为这是一个分类用例,我们将使用之前从 sklearn 导入的函数来计算准确度、精确度和召回率。清单 5-10 展示了评估模型的样板代码。

#Define function for evaluating NN
def evaluate_model(model,x_test,y_test,X_train,Y_train,loss_list):

    model.eval() #Explicitly set to evaluate mode

    #Predict on Train and Validation Datasets
    y_test_prob = model(x_test)
    y_test_pred =np.where(y_test_prob>0.5,1,0)
    Y_train_prob = model(X_train)
    Y_train_pred =np.where(Y_train_prob>0.5,1,0)

    #Compute Training and Validation Metrics
    print("\n Model Performance -")
    print("Training Accuracy-",round(accuracy_score(Y_train,Y_train_pred),3))
    print("Training Precision-",round(precision_score(Y_train,Y_train_pred),3))
    print("Training Recall-",round(recall_score(Y_train,Y_train_pred),3))
    print("Training ROCAUC", round(roc_auc_score(Y_train
                                   ,Y_train_prob.detach().numpy()),3))

    print("Validation Accuracy-",round(accuracy_score(y_test,y_test_pred),3))
    print("Validation Precision-",round(precision_score(y_test,y_test_pred),3))
    print("Validation Recall-",round(recall_score(y_test,y_test_pred),3))
    print("Validation ROCAUC", round(roc_auc_score(y_test
                                     ,y_test_prob.detach().numpy()),3))
    print("\n")

    #Plot the Loss curve and ROC Curve

    plt.figure(figsize=(20,5))
    plt.subplot(1, 2, 1)
    plt.plot(loss_list)
    plt.title('Loss across epochs')
    plt.ylabel('Loss')
    plt.xlabel('Epochs')

    plt.subplot(1, 2, 2)

    #Validation
    fpr_v, tpr_v, _ = roc_curve(y_test, y_test_prob.detach().numpy())
    roc_auc_v = auc(fpr_v, tpr_v)

    #Training
    fpr_t, tpr_t, _ = roc_curve(Y_train, Y_train_prob.detach().numpy())
    roc_auc_t = auc(fpr_t, tpr_t)

    plt.title('Receiver Operating Characteristic:Validation')
    plt.plot(fpr_v, tpr_v, 'b', label = 'Validation AUC = %0.2f' % roc_auc_v)
    plt.plot(fpr_t, tpr_t, 'r', label = 'Training AUC = %0.2f' % roc_auc_t)
    plt.legend(loc = 'lower right')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')

    plt.show()

Listing 5-10Defining the Function to Evaluate the Model Performance

最后,在所有必要的构建模块就绪后,是时候定义我们的神经网络并利用前面的帮助器功能来训练和评估深度学习模型了。我们将从没有正则化器的普通神经网络开始;稍后,我们将通过添加 L1、L2 和辍学生来研究效果,并选择最佳者进行预测。清单 5-11 定义了我们神经网络的结构。

#Define Neural Network

class NeuralNetwork(nn.Module):

    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.out = nn.Linear(384, 1)
        self.relu = nn.ReLU()
        self.final = nn.Sigmoid()

    def forward(self, x):
        op = self.fc1(x)
        op = self.relu(op)
        op = self.fc2(op)
        op = self.relu(op)
        op = self.fc3(op)
        op = self.relu(op)
        op = self.out(op)
        y = self.final(op)
        return y

#Define training variables

num_epochs = 500
batch_size= 128
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

#Hyperparameters
weight_decay=0.0 #set to 0; no L2 Regularizer; passed into the Optimizer
lambda_L1=0.0    #Set to 0; no L1 reg; manually added in loss (train_network)

#Create a model instance
model = NeuralNetwork()

#Define optimizer
adam_optimizer = tch.optim.Adam(model.parameters(), lr= 0.001,weight_decay=weight_decay)

#Train model
adam_loss = train_network(model,adam_optimizer,loss_function
                                    ,num_epochs,batch_size,X_train,Y_train,lambda_
                                         L1=0.0)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 - Loss:1.7305
Epoch: 100 - Loss:0.3219
Epoch: 200 - Loss:0.2470
Epoch: 300 - Loss:0.1910
Epoch: 400 - Loss:0.1431

Model Performance -
Training Accuracy- 0.922
Training Precision- 0.89
Training Recall- 0.957
Training ROCAUC 0.981

Validation Accuracy- 0.801
Validation Precision- 0.757
Validation Recall- 0.827
Validation ROCAUC 0.869

Listing 5-11Defining the Structure of the Neural Network

img/478491_2_En_5_Figd_HTML.jpg

我们将历元数定义为 500,批量大小定义为 128,同时保留weight_decay=0lambda_L1=0.0(这基本上消除了 L1 和 L2 正则化子的影响;我们将很快试验这些值)。正如在第三章中,我们为我们的网络使用了带有BCELoss()的 Adam 优化器。我们的网络有三个隐藏层,分别有 96、192 和 384 个神经元。我们可以在神经网络架构中使用不同大小的单元。

如果我们仔细看看训练和验证数据集之间的结果,我们可以看到一个巨大的差距。有助于捕捉这种差异的单一指标是 ROC AUC(曲线下面积);我们的 AUC 为 98%,而培训和验证的 AUC 为 87%。这个差距是巨大的。本质上,我们面临着过度拟合的问题。为了克服过度拟合,我们需要添加正则化子,这将增加模型损失的惩罚,提示模型学习更简单的模式。理想情况下,我们希望在培训和验证之间看到相似的结果。

先说 L1 正则化。我们在train_network()函数中添加了一小段代码,用于计算参数绝对值的总和,并添加到乘以 Lambda(超参数)后计算的损失中。为了启用 L1 正则化,我们需要向lambda_L1变量传递一个非零值。清单 5-12 展示了网络的 L1 正则化。

#L1 Regularization
num_epochs = 500
batch_size= 128

weight_decay=0.0   #Set to 0; no L2 reg
lambda_L1 = 0.0001 #Enables L1 Regularization

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001 ,weight_decay=weight_decay)

#Define hyperparater for L1 Regularization

#Train network
adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0   - Loss:2.0634
Epoch: 100 – Loss:0.4042
Epoch: 200 – Loss:0.3852
Epoch: 300 – Loss:0.3668
Epoch: 400 – Loss:0.3616

Model Performance –
Training Accuracy- 0.84
Training Precision- 0.77
Training Recall- 0.949
Training ROCAUC 0.93

Validation Accuracy- 0.813
Validation Precision- 0.732
Validation Recall- 0.928
Validation ROCAUC 0.894

Listing 5-12L1 Regularization

img/478491_2_En_5_Fige_HTML.jpg

同样,让我们试试 L2 正则化。默认情况下,PyTorch 提供了一种直接通过优化器中的参数启用 L2 正则化的方法。在 Adam 优化中,我们可以使用weight_decay变量来添加它。

清单 5-13 展示了网络的 L2 正则化。

#L2 Regularization
num_epochs = 500
batch_size= 128
weight_decay=0.001 #Enables L2 Regularization
lambda_L1 = 0.00    #Set to 0; no L1 reg

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001,weight_decay=weight_decay)

#Train Network
adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 – Loss:1.8140
Epoch: 100 – Loss:0.3927
Epoch: 200 – Loss:0.3658
Epoch: 300 – Loss:0.3604
Epoch: 400 – Loss:0.3414

Model Performance –
Training Accuracy- 0.862
Training Precision- 0.822
Training Recall- 0.909
Training ROCAUC 0.935

Validation Accuracy- 0.82
Validation Precision- 0.77
Validation Recall- 0.861
Validation ROCAUC 0.9

Listing 5-13L2 Regularization

img/478491_2_En_5_Figf_HTML.jpg

与 L1 类似,我们看到 L2 的结果比没有正规化要好一些。差距缩小,验证 AUC 增加了一小部分。

随着 L1 和 L2 正则化(分别),我们看到训练和验证性能之间的差距减少,以及减少过度拟合。我们现在对我们的用例有了有利的结果。在最终确定结果之前,让我们添加辍学层。清单 5-14 增加了一个丢弃层,在学习过程中随机丢弃 10%的输入神经元。我们将下降层添加到输入层和隐藏层。

#Define Network with Dropout Layers
class NeuralNetwork(nn.Module):
    #Adding dropout layers within Neural Network to reduce overfitting
    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.relu = nn.ReLU()
        self.out = nn.Linear(384, 1)
        self.final = nn.Sigmoid()
        self.drop = nn.Dropout(0.1)  #Dropout Layer

    def forward(self, x):
        op = self.drop(x)  #Dropout for input layer
        op = self.fc1(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 1
        op = self.fc2(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 2
        op = self.fc3(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 3
        op = self.out(op)
        y = self.final(op)
        return y

num_epochs = 500
batch_size= 128

weight_decay=0.0 #Set to 0; no L2 reg
lambda_L1 = 0.0  #Set to 0; no L1 reg

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001
,weight_decay=weight_decay)
#Train model

adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs
,batch_size,X_train,Y_train
,lambda_L1= lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 - Loss:1.9511
Epoch: 100 - Loss:0.4087
Epoch: 200 - Loss:0.3961
Epoch: 300 - Loss:0.3798
Epoch: 400 - Loss:0.3789

Model Performance -
Training Accuracy  - 0.816
Training Precision - 0.766
Training Recall    - 0.885
Training ROCAUC    - 0.899

Validation Accuracy  - 0.802
Validation Precision - 0.74
Validation Recall    - 0.867
Validation ROCAUC    - 0.882

Listing 5-14Dropout Regularization

img/478491_2_En_5_Figg_HTML.jpg

培训和验证绩效之间的差距已经缩小;我们可以在两个数据集上看到相似的性能。

最后,让我们结合所有三种类型的正则化,并研究对模型性能的影响。清单 5-15 展示了 L1、L2 和辍学正规化。

#Create a network with Dropout layer
class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.relu = nn.ReLU()
        self.out = nn.Linear(384, 1)
        self.final = nn.Sigmoid()
        self.drop = nn.Dropout(0.1)  #Dropout Layer

    def forward(self, x):
        op = self.drop(x)  #Dropout for input layer
        op = self.fc1(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 1
        op = self.fc2(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 2
        op = self.fc3(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 3
        op = self.out(op)
        y = self.final(op)
        return y

num_epochs = 500

batch_size= 128

lambda_L1    = 0.0001  #Enabled L1
weight_decay =0.001    #Enabled L2

model = NeuralNetwork()
loss_function = nn.BCELoss()

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001 ,weight_decay=weight_decay)

adam_loss = train_network(model,adam_optimizer,loss_function ,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Epoch: 0 - Loss:2.2951
Epoch: 100 - Loss:0.4887
Epoch: 200 - Loss:0.4865
Epoch: 300 - Loss:0.4617
Epoch: 400 - Loss:0.4647

Model Performance -
Training Accuracy- 0.794
Training Precision- 0.764
Training Recall- 0.826
Training ROCAUC 0.873

Validation Accuracy- 0.807
Validation Precision- 0.758
Validation Recall- 0.843
Validation ROCAUC 0.884

Listing 5-15L1, L2, and Dropout Regularization

img/478491_2_En_5_Figh_HTML.jpg

总的来说,我们在上述三个场景中看到了相似的性能。在一个理想的实验中,没有定义的基准,我们可以使用它来选择哪种正则化类型会更好。我们需要试验不同类型的正则化以及超参数的不同值:lambda 正则化和超参数值(0.0001,0.001,0.005,0.01),dropout layer 值(0.05,0.1,0.2,0.3 等)。).有了所有实验的结果,我们将更加了解哪种类型的正则化最适合数据。

解读深度学习的业务成果

结果还算不错。我们看到培训和验证性能之间的差距很小。(参考 ROC 图中红色和蓝色线之间的间隙。)

总的来说,我们在验证数据集上有 80%的准确率,精确度为 76%,召回率为 84%。这些结果非常令人鼓舞。在对营销活动结果做出的 10 个“是”的预测中,我们的正确率为 7.6 倍,同时涵盖了 84%会积极响应该活动的所有客户。

让我们花点时间来更好地理解这些结果。我们从一个大约有 50-50%正面和负面结果的数据集开始。考虑到业务问题,这将转化为(考虑到营销团队的努力)在锁定 50%的客户方面的巨大努力损失,并产生负面结果。假设我们总共有 100 个客户(因此,50 个正面结果和 50 个负面结果)。针对每个客户,我们有 100 个工作单位(针对 100 次呼叫),最后我们有 50 次成功存款。

然而,凭借大约 76%的准确率和 84%的召回率,我们有了一个经过筛选的客户列表,可以轻松地锁定这些客户。

因此,我们现在的目标不是所有的 100 个客户,而是我们预测正确的客户,这也包括误报。如果我们总共有 50 个正面结果,那么前面的模型具有 84%的召回率和 76%的精确度,我们将预测(x * 0.84)/0.76(x = 50)。因此,我们总共有约 55 个阳性预测,其中 12 个为假阳性,43 个为真阳性(每 100 个预测)。

与前面的场景相比,对于 100 次尝试,我们有 50 次成功存款。在深度学习模型中,对于 55 次尝试(结果预测为 1),我们有 43 次成功存款。

尽管在活动中损失了七笔正存款,但我们已经大大减少了达到几乎相同的成功标准所需的工作量。这些指标可以根据业务需求进一步调整,以适应更有利的结果。

Note

我们还没有涵盖类似的(详细的)回归用例。鼓励读者独立试验回归用例,其中目标变量是连续的。尽管损失函数的选择、输出层的激活和性能度量需要基于用例,但是问题的方法和公式保持不变。我们推荐尝试的一个样本回归数据集是桑坦德集团的价值预测挑战( https://www.kaggle.com/c/santander-value-prediction-challenge/ )。损失函数的一个好选择是 RMSE;输出层的激活将是线性的;性能度量选择可以是 RMSE 或 MSE。

摘要

本章讲述了模型训练的过程。我们还描述了一些关键步骤和分析,为了改进模型,应该系统地执行这些步骤和分析。我们还讨论了深度学习中常用的正则化技术,即规范惩罚和辍学。在文献中还发现了其他一些必须提及的高级/特定领域技术。到目前为止,我们已经使用一个玩具数据集和一个实际数据集,以及两者的结合和一个业务用例,介绍了前馈神经网络和深度学习的所有基本内容。您现在应该对制定用例、定义基准模型的相关度量、评估模型性能以及评估业务可行性有了更加直观的理解。在下一章中,我们将探索深度学习中最重要的主题之一——卷积神经网络——并拥抱计算机视觉领域。

六、卷积神经网络

卷积神经网络(CNN)本质上是一种采用卷积运算(而不是全连接层)作为其一层的神经网络。CNN 是一项令人难以置信的成功技术,它已经被应用于这样的问题,即在要进行预测的输入数据中具有已知的网格状拓扑,如时间序列(一维网格)或图像(二维网格)。CNN 将深度学习引入现代,解决了计算机视觉数字时代最关键的计算问题之一。随着 CNN 的普及,深度学习的研究热潮一直持续到今天。

本章简要介绍了 CNN 的核心概念,并探索了 PyTorch 中的一个简单示例来研究它们的实际实现。我们还将探索迁移学习,其中我们将利用之前训练过的网络作为我们的用例。

让我们从基础开始。

卷积运算

我们先来看看一维的卷积运算。给定一个输入 I ( t )和一个内核 K ( a ),卷积运算由

$$ s(t)={\sum}_aI(a)\cdotp K\left(t-a\right) $$

给出

给定卷积运算的交换性,该运算的等价形式如下:

$$ s(t)={\sum}_aI\left(t-a\right)\cdotp K(a) $$

此外,可以替换负号(翻转)来获得互相关,如下:

$$ s(t)={\sum}_aI\left(t+a\right)\cdotp K(a) $$

深度学习文献和软件实现互换使用术语卷积互相关。运算的本质是,与输入相比,核是一组更短的数据点,当输入与核相似时,卷积运算的输出更高。图 6-1 和图 6-2 说明了这一关键思想。我们采用任意输入和任意核,并执行卷积运算。当内核与输入的特定部分相似时,获得最高值。

img/478491_2_En_6_Fig2_HTML.png

图 6-2

卷积运算—一维

img/478491_2_En_6_Fig1_HTML.png

图 6-1

卷积运算的简单概述

应注意以下几点:

  1. 输入是任意的大量数据点。

  2. 内核是一组在数量上小于输入的数据点。

  3. 从某种意义上说,卷积运算将内核滑过输入,并计算内核与输入部分的相似程度。

  4. 卷积运算在核与输入的一部分最相似的地方产生最高值。

卷积运算可以扩展到二维。给定一个输入 I ( mn )和一个内核 K ( ab ),卷积运算由

$$ s(t)=\sum \limits_a\sum \limits_bI\left(a,b\right)\cdotp K\left(m-a,n-b\right) $$

给出

给定卷积运算的交换性,该运算的等价形式如下:

$$ s(t)=\sum \limits_a\sum \limits_bI\left(m-a,n-b\right)\cdotp K\left(a,b\right) $$

此外,可以替换负号(翻转)来获得互相关,给出如下:

$$ s(t)=\sum \limits_a\sum \limits_bI\left(m+a,n+b\right)\cdotp K\left(a,b\right) $$

图 6-3 以二维图示了卷积运算。请注意,这只是将卷积的思想扩展到二维。

img/478491_2_En_6_Fig3_HTML.png

图 6-3

卷积运算—二维

在介绍了卷积运算之后,我们现在可以更深入地研究 CNN 的关键组成部分,其中使用了卷积层而不是全连接层,这涉及到矩阵乘法。

一个全连通层可以描述为y=f(x**w),其中 x 为输入向量, y 为输出向量, w 为一组权重, f 为激活函数。相应地,一个卷积层可以描述为y=f(s(xw),其中 s 表示输入和权值之间的卷积运算。

现在让我们对比一下全连接层和卷积层。图 6-4 示意性地示出了全连接层,图 6-5 示意性地示出了卷积层。图 6-6 说明了卷积层中的参数共享以及全连接层中的参数共享缺失。应注意以下几点:

img/478491_2_En_6_Fig6_HTML.png

图 6-6

参数共享权重

img/478491_2_En_6_Fig5_HTML.png

图 6-5

卷积层中的稀疏相互作用

img/478491_2_En_6_Fig4_HTML.png

图 6-4

全连接层中的密集相互作用

  • 对于相同数量的输入和输出,全连接层比卷积层有更多的连接和相应的权重。

  • 与全连接层相比,卷积层中产生输出的输入之间的交互较少。这被称为稀疏相互作用

  • 假设内核比输入小得多,并且内核在输入上滑动,则参数/权重在卷积层上共享。因此,卷积层中的唯一参数/权重要少得多。

联营业务

现在让我们来看看合并运算,它几乎总是与卷积一起用于 CNN。池化操作背后的思想是,如果事实上已经发现了特征的确切位置,那么它就不是问题。它只是提供了平移不变性。例如,假设任务是学习识别照片中的人脸。还假设照片中的人脸是倾斜的(通常如此),我们有一个卷积层来检测眼睛。我们想从照片中眼睛的方向提取出它们的位置。汇集操作实现了这一点,并且是 CNN 的重要组成部分。

图 6-7 说明了二维输入的汇集操作。应注意以下几点:

img/478491_2_En_6_Fig7_HTML.png

图 6-7

汇集或二次抽样

  • 函数 f 通常是最大值运算(导致最大池化),但是也可以使用其他变型,例如平均值或L2 范数。

  • 对于二维输入,这是一个矩形部分。

  • 与输入相比,汇集产生的输出在维度上要小得多。

卷积检测器池构建模块

现在让我们来看看卷积检测器池模块,它可以被看作是 CNN 的一个构建模块,并看看我们前面介绍的所有操作是如何协同工作的。参见图 6-8 和图 6-9 。需要注意以下几点。

img/478491_2_En_6_Fig9_HTML.png

图 6-9

给出多个特征图的多个过滤器/内核

img/478491_2_En_6_Fig8_HTML.png

图 6-8

卷积,然后是检测器阶段和合并

  • 检测器级只是一个非线性激活函数。

  • 卷积、检测器和池操作按顺序应用,以将输入转换为输出。输出被称为特征图

  • 输出通常会传递到其他层(卷积层或全连接层)。

  • 多个卷积检测器池模块可以并行应用,消耗相同的输入并产生多个输出或特征图。

如果图像输入由三个通道组成,则对每个通道进行单独的卷积运算,然后在卷积后将输出相加(参见图 6-10 )。

img/478491_2_En_6_Fig10_HTML.png

图 6-10

多通道卷积

在介绍了 CNN 的所有组成元素之后,我们现在可以完整地看一个 CNN 的例子(见图 6-11 )。CNN 由两级卷积检测器池模块组成,每级都有多个滤波器/内核产生多个特征图。在这两个阶段之后,我们有一个产生输出的完全连接的层。一般而言,CNN 可能具有多级卷积检测器池模块(采用多个滤波器),通常后跟一个全连接层。

img/478491_2_En_6_Fig11_HTML.png

图 6-11

完整的 CNN 架构

除了这些基本结构,我们还将探讨一些与卷积层相关的其他主题。

进展

步幅可以定义为过滤器/内核移动的量。当讨论滤波器在输入图像上的滑动时,我们假设该移动只是在预期方向上的一个单位。然而,我们可以用我们选择的一些数字来控制滑动(尽管通常使用一个)。基于用例,我们可以选择一个更合适的数字。更大的步幅通常有助于减少计算、概括特征学习等。

填料

我们还看到,与输入图像的大小相比,应用卷积减小了特征图的大小。在应用大于 1x1 的过滤器并避免边界处的信息丢失之后,零填充是控制维度收缩的通用方法。

批量标准化

批量标准化是一种技术,通过标准化每个小批量的层输入来帮助训练非常深的神经网络。标准化输入有助于稳定学习过程,从而大大减少训练深度网络所需的训练次数。批量标准化层被添加在卷积层之后,并且通常是卷积运算的标准块的一部分。也就是说,卷积层、批量标准化层、激活和最大池操作在同一序列中的组合被定义为一个卷积单元。我们通常在 CNN 中添加几个这样的单元。

过滤器

过滤器类似于内核。在最近的实现(包括 PyTorch)和学术界,术语过滤器内核更常见。一般来说,对于卷积运算,我们使用大小为 3×3 和 5×5 的滤波器。早期的实现也支持 7×7 滤波器。

过滤深度

滤镜深度通常指输入图像中颜色通道数对应的深度。对于后面层中的过滤器,深度对应于前面层中过滤器的数量。对于具有三个颜色通道(即 R、G 和 B)的常规图像,我们使用深度为 3 的滤镜。

过滤器数量

过滤器充当特征提取器;因此,在网络的每个卷积块中有几个滤波器是很常见的。一个示例排列是一个卷积块,具有 32 个大小为 3×3(深度为 3)的滤波器,接着是激活/批量归一化和池化块,接着是另一个具有 64 个滤波器的块(现在深度为 32),依此类推。

总结 CNN 的主要经验

到目前为止,我们已经讨论了 CNN 背后的关键组成概念:卷积运算和池运算,以及它们如何结合使用。现在让我们后退一步,用这些构件来内化 CNN 背后的思想。

  • 首先要考虑的是 CNN 的容量。用卷积运算代替神经网络的至少一个完全连接的层的 CNN 比完全连接的网络具有更小的容量。也就是说,存在全连接网络能够模拟 CNN 不能模拟的数据集。因此,要注意的第一点是,CNN 通过限制容量并因此使训练有效来实现更多。

  • 要考虑的第二个想法是,学习驱动卷积运算的滤波器在某种意义上是表示学习。例如,已学习的过滤器可以学习检测边缘、形状等。这里要考虑的要点是,我们不是手动描述要从输入数据中提取的特征;相反,我们描述的是一个学会设计特性/表现的架构。

  • 要考虑的第三个想法是池操作引入的位置不变性。池操作将特征的位置与其被检测到的事实分开。检测直线的过滤器可能会在图像的任何部分检测到该特征,但池操作会选择检测到该特征的事实(最大池)。

  • 第四个想法是等级。一个 CNN 可以有多个卷积层和汇集层堆叠在一起,然后是一个完全连接的网络。这允许 CNN 建立一个概念层次,其中更抽象的概念基于更简单的概念(参见第一章)。

  • 最后一个想法是在一系列卷积层和汇集层的末端存在一个全连接层。一系列卷积层和汇集层生成特征,标准神经网络学习最终的分类/回归函数。将 CNN 的这一方面与传统的机器学习区分开来是很重要的。在传统的机器学习中,专家会手工设计特征,并将其输入神经网络。在 CNN 中,这些特征/表示是从数据中学习的。

使用 PyTorch 实现基本的 CNN

现代深度学习框架负责我们开发 CNN 所需的大量操作和构造。让我们用一个简单的例子来说明 PyTorch 如何用于定义、训练和评估 CNN。

我们将从 MNIST 的一个例子开始,那里有一组手写数字图像。我们的任务是将给定的图像分类为 0 到 9 之间的数字。

Note

计算机视觉任务是非常计算密集型的,通常需要高端硬件来训练和评估大型鲁棒网络。我们探索的 MNIST 例子是一个微型数据集,读者应该很容易在商用硬件上重现。对于本章中更深入的例子,我们推荐一个免费的、基于网络的、支持 GPU 的计算实例,比如 Kaggle 或 Google Colab。这两个版本都提供了一个标准计算实例,具有大约 16GB RAM 和 16GB GPU 内存,每月配额。出于实验目的,这些都是很好的资源。对于更深入的实验,读者需要探索云(AWS/GCP/Azure)或定制硬件上的深度学习实例。

首先,从 https://www.kaggle.com/c/digit-recognizer/data 下载数据集。

我们将只使用提供了标签的训练数据集。训练数据集将进一步分为训练和验证。现在我们已经准备好了数据,让我们通过导入所需的包来开始实现(清单 6-1 )。

#pytorch utility imports
import torch
from torch.utils.data import DataLoader, TensorDataset

#neural net imports
import torch.nn as nn, torch.nn.functional as F, torch.optim as optim
from torch.autograd import Variable

#import external libraries
import pandas as pd,numpy as np,matplotlib.pyplot as plt, os
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
%matplotlib inline

#Set device to GPU or CPU based on availability

if torch.cuda.is_available():
    device = torch.device('cuda')
else:
    device = torch.device('cpu')

Listing 6-1Importing the Required Packages

我们现在将使用 Pandas 加载数据集(类似于第五章)并分离标签和像素值。请注意,大多数图像数据集都是以简单的图像格式存储的。jpeg 或者。png)放在一个适合 PyTorch 的简单文件夹结构中。然而,为了简化这个例子,我们使用一个数据集,其中像素值作为横截面数据存储在一个. csv 文件中。然后,我们将数据集分为训练和测试,并绘制几个样本。在下一个示例中,我们将使用存储在传统文件夹结构中的数据集。

在本例中,我们将使用由 PyTorch 提供的包装器 TensorDataset 将标签和张量组合成一个统一的数据集。清单 6-2 演示了将数据集加载到内存中。

img/478491_2_En_6_Figa_HTML.jpg

input_folder_path = "/input/data/MNIST/"

#The CSV contains a flat file of images,
#i.e. each 28*28 image is flattened into a row of 784 colums
#(1 column represents a pixel value)
#For CNN, we would need to reshape this to our desired shape

train_df = pd.read_csv(input_folder_path+"train.csv")

#First column is the target/label
train_labels = train_df['label'].values

#Pixels values start from the 2nd column
train_images = (train_df.iloc[:,1:].values).astype('float32')

#Training and Validation Split
train_images, val_images, train_labels, val_labels =
                                         train_test_split(
                                             train_images
                                             ,train_labels
                                             ,random_state=2020
                                             ,test_size=0.2)
#Here we reshape the flat row into [#images,#Channels,#Width,#Height]
#Given this a simple grayscale image, we will have just 1 channel
train_images = train_images.reshape(train_images.shape[0],1,28, 28)
val_images = val_images.reshape(val_images.shape[0],1,28, 28)

#Also, let's plot few samples
for i in range(0, 6):
    plt.subplot(160 + (i+1))
    plt.imshow(train_images[i].reshape(28,28), cmap=plt.get_cmap('gray'))
    plt.title(train_labels[i])

Listing 6-2Loading the Dataset into Memory

接下来,我们将归一化像素值,并将数据集转换为 PyTorch 张量用于训练(清单 6-3 )。

#Covert Train Images from pandas/numpy to tensor and normalize the values
train_images_tensor = torch.tensor(train_images)/255.0
train_images_tensor = train_images_tensor.view(-1,1,28,28)
train_labels_tensor = torch.tensor(train_labels)

#Create a train TensorDataset
train_tensor = TensorDataset(train_images_tensor, train_labels_tensor)

#Covert Validation Images from pandas/numpy to tensor and normalize the values
val_images_tensor = torch.tensor(val_images)/255.0
val_images_tensor = val_images_tensor.view(-1,1,28,28)
val_labels_tensor = torch.tensor(val_labels)

#Create a Validation TensorDataset
val_tensor = TensorDataset(val_images_tensor, val_labels_tensor)

print("Train Labels Shape:",train_labels_tensor.shape)
print("Train Images Shape:",train_images_tensor.shape)
print("Validation Labels Shape:",val_labels_tensor.shape)
print("Validation Images Shape:",val_images_tensor.shape)

#Load Train and Validation TensorDatasets into the data generator for Training
train_loader = DataLoader(train_tensor, batch_size=64
                          , num_workers=2, shuffle=True)
val_loader = DataLoader(val_tensor, batch_size=64, num_workers=2, shuffle=True)

Output[]
Train Labels Shape: torch.Size([33600])
Train Images Shape: torch.Size([33600, 1, 28, 28])
Validation Labels Shape: torch.Size([8400])
Validation Images Shape: torch.Size([8400, 1, 28, 28])

Listing 6-3Normalizing the Data and Preparing the Training/Validation Datasets

准备好训练和验证数据集后,让我们定义网络的下一个重要方面。这包括 CNN 本身、用于训练的功能以及评估和做出预测。这些结构中的大多数都是从我们之前在第五章中的例子中借来的。我们将在这里处理一些新的代码结构。

在我们的 CNN 中,我们需要定义一个卷积单元,如前所述。每个单元组合了一个卷积层,随后是批量标准化(可选)、激活和最大池层。要考虑的一个重要方面是每个卷积单元后的结果图像的大小。

在本例中,我们的原始图像大小为 28×28。当我们通过第一个卷积单元时,图像大小会根据我们定义的内核大小缩小。假设我们已经使用“padding=1”向输入添加了一个填充单元,卷积后原始大小保持不变。然而,使用最大池操作,大小减少了一半(正如我们所希望的)。因此,最初为 28×28 的合成图像将被转换为大小为 14×14×16 的张量(其中 16 是我们定义的过滤器数量)。对于每一个额外的卷积单元,我们将看到数量减少了一半(作为最大池操作的结果)。

因此,在三个连续的卷积单元之后,最终大小将是 7(即,28 -> 14 -> 7)。

全连接层 fc1 的输入节点为 7×7×32(其中 32 是前一个卷积单元中的内核数)。转发功能将这些卷积单元与完全连接的层顺序连接。最后一层将有 10 个输出节点,因为我们在这里有多类分类问题:即将一个数字分类为 0,1,2,3,… 9。最后一层中的 softmax 函数为我们的多类用例将输出裁剪成一组简洁的概率分数。

在清单 6-4 中,我们定义了 CNN 的结构和助手函数来评估模型的性能并生成预测。

#Define conv-net
class ConvNet(nn.Module):
    def __init__(self, num_classes=10):
        super(ConvNet, self).__init__()
        #First unit of convolution
        self.conv_unit_1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))

        #Second unit of convolution

        self.conv_unit_2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))

        #Fully connected layers
        self.fc1 = nn.Linear(7*7*32, 128)
        self.fc2 = nn.Linear(128, 10)

    #Connect the units
    def forward(self, x):
        out = self.conv_unit_1(x)
        out = self.conv_unit_2(out)
        out = out.view(out.size(0), -1)
        out = self.fc1(out)
        out = self.fc2(out)
        out = F.log_softmax(out,dim=1)
        return out

#Define Functions for Model Evaluation and generating Predictions
def make_predictions(data_loader):
    #Explcitly set the model to eval mode
    model.eval()
    test_preds = torch.LongTensor()
    actual = torch.LongTensor()

    for data, target in data_loader:

        if torch.cuda.is_available():
            data = data.cuda()
        output = model(data)

        #Predict output/Take the index of the output with max value
        preds = output.cpu().data.max(1, keepdim=True)[1]

        #Combine tensors from each batch
        test_preds = torch.cat((test_preds, preds), dim=0)
        actual  = torch.cat((actual,target),dim=0)

    return actual,test_preds

#Evalute model

def evaluate(data_loader):
    model.eval()
    loss = 0
    correct = 0

    for data, target in data_loader:
        if torch.cuda.is_available():
            data = data.cuda()
            target = target.cuda()
        output = model(data)
        loss += F.cross_entropy(output, target, size_average=False).data.item()
        predicted = output.data.max(1, keepdim=True)[1]
        correct += (target.reshape(-1,1) == predicted.reshape(-1,1)).float().sum()

    loss /= len(data_loader.dataset)

    print('\nAverage Val Loss: {:.4f}, Val Accuracy: {}/{} ({:.3f}%)\n'.format(
        loss, correct, len(data_loader.dataset),
        100\. * correct / len(data_loader.dataset)))

Listing 6-4Defining the CNN and the Helper Functions

有了重要的构造,我们现在可以创建模型的实例,并定义我们的标准函数和优化器,如清单 6-5 所示。

img/478491_2_En_6_Figb_HTML.jpg

#Create Model  instance
model = ConvNet(10).to(device)

#Define Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
print(model)
Output[]

Listing 6-5Creating a Model Instance and Defining the Loss Function and Optimizer

清单 6-6 展示了为定义数量的时期训练 CNN 模型——在本例中是五个时期。

num_epochs = 5

# Train the model
total_step = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)

        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))
    evaluate(val_loader)

Output[]
Epoch [1/5], Loss: 0.0564
Average Val Loss: 0.0700, Val Accuracy: 8196.0/8400 (97.571%)

Epoch [2/5], Loss: 0.0096
Average Val Loss: 0.0481, Val Accuracy: 8279.0/8400 (98.560%)

Epoch [3/5], Loss: 0.0088
Average Val Loss: 0.0474, Val Accuracy: 8273.0/8400 (98.488%)

Epoch [4/5], Loss: 0.0362

Average Val Loss: 0.0520, Val Accuracy: 8243.0/8400 (98.131%)

Epoch [5/5], Loss: 0.0013
Average Val Loss: 0.0458, Val Accuracy: 8277.0/8400 (98.536%)

Listing 6-6Training a CNN Model

我们可以看到,该模型在验证数据集上取得了相当积极的结果。以 98.5%的准确度(在五个时期内),我们可以断定我们的模型具有良好的性能。

让我们对验证数据集进行预测,并可视化混淆矩阵(参见清单 6-7 )。

img/478491_2_En_6_Figc_HTML.jpg

#Make Predictions on Validation Dataset

actual, predicted = make_predictions(val_loader)
actual,predicted = np.array(actual).reshape(-1,1)
                            ,np.array(predicted).reshape(-1,1)

print("Validation Accuracy-",round(accuracy_score(actual,predicted),4)*100)
print("\n Confusion Matrix\n",confusion_matrix(actual,predicted))

Output[]

Listing 6-7Making Predictions

在 PyTorch 中实现更大的 CNN

这是我们 CNN 的第一个样本。给定小数据集,我们可以在我们的个人计算机(商用硬件)上轻松地训练我们的网络,并且仍然可以获得令人满意的结果。让我们探索一个类似的例子,但是有更复杂的图像。一个很好的例子就是猫和狗的数据集。这里,我们的目标是根据给定的图像将数据集分类为猫或狗。

该数据集最初由微软研究院发布,后来在 https://www.kaggle.com/c/dogs-vs-cats/data 通过 Kaggle 提供。

数据集被托管为一个简单的文件夹,文件名代表标签,因此我们可能必须在使用它之前重新组织数据集。

PyTorch 通过 ImageFolder 和 DataLoader 为图像提供了简洁的抽象。PyTorch 希望数据存储在以下文件夹结构中:

Root/label_1/*
Root/label_2/*
Root/label_N/*

对于我们的用例,这将是以下内容:

/input/train/cats/*
/input/train/dogs/*
/input/test/cats/*
/input/test/dogs/*

为了简化过程,我们在 https://www.kaggle.com/jojomoolayil/catsvsdogs 提供了一个有组织的结构,带有适合 PyTorch 实验的图像。

我们建议在这个实验中使用带 GPU 加速器的 Kaggle 笔记本。右侧栏上的设置显示了训练数据文件夹结构,以及加速器(参见图 6-12 )。我们已经打开了互联网选项,并将加速器设置为 GPU。

img/478491_2_En_6_Fig12_HTML.jpg

图 6-12

Kaggle 笔记本中的环境设置

让我们从所需包的新导入开始。清单 6-8 展示了如何为这个练习导入包。

# Import required libraries
import torch
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from PIL import Image
import matplotlib.pyplot as plt
import glob,os
import matplotlib.image as mpimg

new_path = "/kaggle/input/catsvsdogs/"

Listing 6-8Importing the Packages for This Exercise

确保您已经打开了互联网选项,并选择了加速器作为 GPU。我们使用清单 6-9 中的命令确认 GPU 可用。

#Check if GPU is available
if torch.cuda.is_available():
    device = torch.device('cuda')
else:
    device = torch.device('cpu')
print("Device:",device)

Output[]
Device: cuda

Listing 6-9Enabling the GPU (If Available) in the Kernel

请注意,建议只使用 GPU,而不是命令。然而,对于计算机视觉实验来说,使用 CPU 会慢得多。

我们现在可以探索一组随机的猫和狗的图像。清单 6-10 从训练数据集中随机绘制样本图像。

img/478491_2_En_6_Figd_HTML.jpg

%matplotlib inline
images = []
#Collect Cat images
for img_path in glob.glob(os.path.join(new_path,"train","cat","*.jpg"))[:5]:
    images.append(mpimg.imread(img_path))

#Collect Dog images
for img_path in glob.glob(os.path.join(new_path,"train","dog","*.jpg"))[:5]:
    images.append(mpimg.imread(img_path))

#Plot a grid of cats and Dogs

plt.figure(figsize=(20,10))
columns = 5
for i, image in enumerate(images):
    plt.subplot(len(images) / columns + 1, columns, i + 1)
    plt.imshow(image)

Listing 6-10Plotting Sample Images from the Training Dataset

对于计算机视觉实验,我们总是会对原始数据集应用许多变换。这样做的一个核心原因是,实验中使用的大多数图像大小不同。此外,有时我们可能需要通过扩充现有样本来添加更多的训练样本。一些例子包括用随机旋转增加更多的训练样本、从中心裁剪图像、跨轴翻转、标准化像素值等。PyTorch 提供了一个方便的功能来组合几个这样的转换,并在训练和验证样本上编排它们。在清单 6-11 中,我们编写了一个transformations对象,它将顺序地将所有图像调整为 255×255,从中心向 224×224 裁剪它们,将它们转换为张量,并归一化它们的像素值。

#Compose sequence of transformations for image
transformations = transforms.Compose([
    transforms.Resize(255),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load in each dataset and apply transformations using
# the torchvision.datasets as datasets library
train_set = datasets.ImageFolder(os.path.join(new_path,"train")
                                 , transform = transformations)
val_set = datasets.ImageFolder(os.path.join(new_path,"test")
                               , transform = transformations)

# Put into a Dataloader using torch library
train_loader = torch.utils.data.DataLoader(train_set
                                 , batch_size=32, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_set, batch_size =32, shuffle=True)

Listing 6-11Transforming the Data and Creating the Training and Validation Sets

请注意,train_loaderval_loader是为我们的训练循环处理和创建带有标签的小批量图像的对象。在创建小批量图像之前,transformations对象会确保所有图像都得到适当的放大。

接下来,清单 6-12 定义了我们的 CNN。

#Define Convolutional network
class ConvNet(nn.Module):
    def __init__(self, num_classes=2):
        super(ConvNet, self).__init__()
        #First unit of convolution
        self.conv_unit_1 = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #112

        #Second unit of convolution
        self.conv_unit_2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #56

        #Third unit of convolution
        self.conv_unit_3 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #28

        #Fourth unit of convolution
        self.conv_unit_4 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #14

        #Fully connected layers
        self.fc1 = nn.Linear(14*14*128, 128)
        self.fc2 = nn.Linear(128, 1)
        self.final = nn.Sigmoid()

    def forward(self, x):
        out = self.conv_unit_1(x)
        out = self.conv_unit_2(out)
        out = self.conv_unit_3(out)
        out = self.conv_unit_4(out)

        #Reshape the output
        out = out.view(out.size(0),-1)
        out = self.fc1(out)
        out = self.fc2(out)
        out  = self.final(out)

        return(out)

Listing 6-12Defining the CNN

类似于 MNIST 的例子,全连接层需要输入维度的数量,这将基于卷积单元而不同。因为我们在原始样本中应用了四个卷积单元,所以图像的大小会缩小,为([原始]224->[第一]112->[第二]56->[第三]28->[第四] 14。因此,全连接层将具有 14×14×128 个输入维度,其中 128 是前一单元中的内核数。

清单 6-13 定义了一个评估我们新网络的函数。

def evaluate(model,data_loader):
    loss = []
    correct = 0
    with torch.no_grad():
            for images, labels in data_loader:
                images = images.to(device)
                labels = labels.to(device)

                model.eval()

                output = model(images)

                predicted = output > 0.5
                correct += (labels.reshape(-1,1) == predicted.reshape(-1,1)).float().sum()

                #Clear memory
                del([images,labels])
                if device == "cuda":
                    torch.cuda.empty_cache()

    print('\nVal Accuracy: {}/{} ({:.3f}%)\n'.format(
        correct, len(data_loader.dataset),
        100\. * correct / len(data_loader.dataset)))

Listing 6-13Defining the Evaluation Function

有了这些,让我们定义并创建一个模型实例,并为 10 个时期训练我们的网络。清单 6-14 演示了定义损失函数和优化器,创建模型实例,以及为定义数量的时期进行训练。

num_epochs = 10
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
model = ConvNet()
model.cuda()
adam_optimizer = torch.optim.Adam(model.parameters(), lr= 0.001)

# Train the model
total_step = len(train_loader)
print("Total Batches:",total_step)

for epoch in range(num_epochs):
    model.train()
    train_loss = 0
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass

        outputs = model(images)
        loss = loss_function(outputs.float(), labels.float().view(-1,1))

        # Backward and optimize
        adam_optimizer.zero_grad()
        loss.backward()
        adam_optimizer.step()
        train_loss += loss.item()* labels.size(0)

        #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))
    #Evaluate model after each training epoch
    evaluate(model,val_loader)

Output[]
Total Batches: 625

Epoch [1/10], Loss: 0.6990
Val Accuracy: 3768.0/5000 (75.360%)

Epoch [2/10], Loss: 0.4914
Val Accuracy: 3885.0/5000 (77.700%)

Epoch [3/10], Loss: 0.2088
Val Accuracy: 4141.0/5000 (82.820%)

Epoch [4/10], Loss: 0.2832
Val Accuracy: 4219.0/5000 (84.380%)

Epoch [5/10], Loss: 0.1797

Val Accuracy: 4271.0/5000 (85.420%)

Epoch [6/10], Loss: 0.3226
Val Accuracy: 4248.0/5000 (84.960%)

Epoch [7/10], Loss: 0.2027
Val Accuracy: 4250.0/5000 (85.000%)

Epoch [8/10], Loss: 0.2660
Val Accuracy: 4137.0/5000 (82.740%)

Epoch [9/10], Loss: 0.1867
Val Accuracy: 4286.0/5000 (85.720%)

Epoch [10/10], Loss: 0.1286
Val Accuracy: 4271.0/5000 (85.420%)

Listing 6-14Defining the Loss Function and Optimizer, Creating the Model Instance, and Training for a Defined Number of Epochs

10 个纪元后,性能大致为 85%。几个时代之后,性能肯定会提高;然而,训练这样一个网络所需的时间是昂贵的。我们可能想知道的一个问题是,是否有更快、更容易的替代方法来加速这一过程。事实证明,迁移学习对我们的资源是可用的。关于 CNN 的惊人消息是,一旦一个层被训练,它基本上可以被重新用于另一个任务。对于大多数计算机视觉任务来说,较低级别的特征(例如曲线、边和圆)和几个较高级别的特征总是共同的或相似的。然而,我们可能需要重新训练最后几层,以便专门为我们的用例定制网络。尽管如此,在训练大型网络时,这还是带来了巨大的缓解。

今天,我们有大量经过预训练的网络,这些网络在一个大的数据集语料库上训练了几个小时,几乎代表了我们遇到的最常见的对象。在 PyTorch 下,这些网络中有许多都是现成的。我们可以直接利用它们,而不是从头开始训练我们自己的网络。

欲了解更多关于预训练模型列表的信息,请访问 https://pytorch.org/docs/stable/torchvision/models.html

对于我们的用例,让我们使用 VGGNet。清单 6-15 展示了下载和利用 VGGNet 进行迁移学习。

#Download the model (pretrained)
from torchvision import models
new_model = models.vgg16(pretrained=True)

# Freeze model weights
for param in new_model.parameters():
    param.requires_grad = False

print(new_model.classifier)
Output[]

Sequential(
  (0): Linear(in_features=25088, out_features=4096, bias=True)
  (1): ReLU(inplace=True)
  (2): Dropout(p=0.5, inplace=False)
  (3): Linear(in_features=4096, out_features=4096, bias=True)
  (4): ReLU(inplace=True)
  (5): Dropout(p=0.5, inplace=False)
  (6): Linear(in_features=4096, out_features=1000, bias=True)
)

Listing 6-15Downloading and Initializing the Pretrained Model

预训练网络有六层。最初的网络用于对 1000 个不同的物体进行分类;因此,最后一层有 1000 个输出连接。然而,我们的用例是一个简单的二元分类练习;因此,我们需要替换最后一层来适应我们的用例。清单 6-16 用一个定制层替换预训练网络中的最后一层,该定制层输出一个带有 sigmoid 激活的单个单元。

#Define our custom model last layer
new_model.classifier[6] = nn.Sequential(
                      nn.Linear(new_model.classifier[6].in_features, 256),
                      nn.ReLU(),
                      nn.Dropout(0.4),
                      nn.Linear(256, 1),
                      nn.Sigmoid())

# Find total parameters and trainable parameters
total_params = sum(p.numel() for p in new_model.parameters())
print(f'{total_params:,} total parameters.')
total_trainable_params = sum(
    p.numel() for p in new_model.parameters() if p.requires_grad)
print(f'{total_trainable_params:,} training parameters.')

Output[]
135,309,633 total parameters.
1,049,089 training parameters.

Listing 6-16Replacing the Last Layer with Our Custom Layer

在这里,我们利用了 VGG 预训练模型的现有层,并在最后添加了一个新的全连接层,以针对我们的二进制用例定制网络结构。除了我们添加的层之外,所有层的权重都被冻结,也就是说,除了最后一个完全连接的层之外,模型权重在训练过程中不会更新。

现在让我们为数据集训练 10 个时期的新模型。所有组件都与前面的示例相似。清单 6-17 展示了为我们的用例训练预训练网络。

#Define epochs, optimizer and loss function
num_epochs = 10
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
new_model.cuda()
adam_optimizer = torch.optim.Adam(new_model.parameters(), lr= 0.001)

# Train the model
total_step = len(train_loader)
print("Total Batches:",total_step)

for epoch in range(num_epochs):
    new_model.train()
    train_loss = 0
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass
        outputs = new_model(images)
        loss = loss_function(outputs.float(), labels.float().view(-1,1))

        # Backward and optimize
        adam_optimizer.zero_grad()
        loss.backward()
        adam_optimizer.step()
        train_loss += loss.item()* labels.size(0)

    #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))

    #After each epoch evaluate model
    evaluate(new_model,val_loader)

Output[]
Total Batches: 625

Epoch [1/10], Loss: 0.0140
Val Accuracy: 4933.0/5000 (98.660%)

Epoch [2/10], Loss: 0.0411
Val Accuracy: 4931.0/5000 (98.620%)

Epoch [3/10], Loss: 0.0054
Val Accuracy: 4933.0/5000 (98.660%)

Epoch [4/10], Loss: 0.0017
Val Accuracy: 4937.0/5000 (98.740%)

Epoch [5/10], Loss: 0.0285
Val Accuracy: 4935.0/5000 (98.700%)

Epoch [6/10], Loss: 0.0070
Val Accuracy: 4935.0/5000 (98.700%)

Epoch [7/10], Loss: 0.0310
Val Accuracy: 4940.0/5000 (98.800%)

Epoch [8/10], Loss: 0.0091
Val Accuracy: 4922.0/5000 (98.440%)

Epoch [9/10], Loss: 0.0116

Val Accuracy: 4937.0/5000 (98.740%)

Epoch [10/10], Loss: 0.0442
Val Accuracy: 4930.0/5000 (98.600%)

Listing 6-17Training the Pretrained Model for the Defined Use Case

通过仅仅 10 个时期,我们可以看到我们的预训练模型在验证数据集上给出了大约 98%的准确度。与我们的原始模型(从头开始训练)相比,性能改进是显著的。

CNN 经验法则

对于计算机视觉任务,我们可以描绘一些规则,这些规则可以作为大多数实验的良好起点。

  • 任何给定的计算机视觉任务的起点都应该利用预先训练的网络。从头开始训练网络总是可能的,但是当结果已经可用时,巨大的计算努力将是徒劳的任务。

  • 在模型性能达不到您的基准的情况下,尝试使用其他几个预训练的网络,而不是一个。PyTorch 提供了几种现成的预训练模型。

  • 当您的图像分类任务包括一组非常多样化的图像时,预训练的网络可能不会为您提供最佳性能。在这种情况下,建议逐步解冻更多顶层。这个想法是试验什么级别的特性表示对您的用例有意义。在最坏的情况下,您可能需要从头开始训练整个网络。然而,在大多数情况下,通过预训练网络中的几层或更多层,您很可能能够节省计算工作量。

  • 使用辍学总是一个好主意。

  • 对于大多数用例,ReLUs 可以被盲目地用作事实上的激活函数。

  • 要获得相当可接受的性能,请确保每个类有 6,000 个或更多的训练样本。越多越好。

  • 批量大小应该是 GPU 或 CPU 能够处理的最大值。优化批量大小有助于加快训练过程。

  • 总是推荐使用 GPU。对于大多数常见用例,GPU 性能几乎是 50 倍或更高。获得基于 GPU 的实例的成本已经显著下降。所有主要的云参与者都提供现成的深度学习映像或虚拟机,可以通过合适的计算和 GPU 按需供应。整个繁重的任务(即安装所需的依赖项、包和驱动程序,以及配置深度学习、Python 框架、工作区等。)一点就抽象出来了。成本也下降了,以提供一个负担得起的手段来训练一些实验。今天,你可以以每小时 1 美元的价格为大多数研究项目配备功能强大的 GPU。

  • 许多资源都是免费的。Google Colab 和 Kaggle 提供了开始尝试深度学习的绝佳场所。

摘要

本章讲述了 CNN 的基础知识。关键的要点是卷积运算、汇集运算、它们是如何结合使用的,以及特性是如何通过学习而不是手工设计的。CNN 是深度学习最成功的应用,体现了学习特征/表示而不是手工设计它们的思想。本章中的练习使用一个相当简单的数据集和一个中等大小的数据集从零开始训练来探索 CNN。我们还利用了预训练的网络,并看到了由此带来的性能提升。

在下一章中,我们将探讨循环神经网络,它广泛应用于自然语言处理和语音识别领域。

七、循环神经网络

随着深度学习的出现,自然语言处理(NLP)领域已经见证了显著的增长。这种运动很大程度上可以归功于循环神经网络(RNNs)及其变体。基于语音的 AI 助手、智能手机键盘中文本的自动完成以及基于情感分类的基于文本的评论都是 RNNs 有效解决的问题。

本章首先探讨与 RNNs 相关的基本概念。然后,我们探索更适合现代计算任务的香草 RNN 模型的几个变种。最后,我们将在从我们最喜欢的平台 Kaggle 借来的真实数据集上使用 PyTorch 研究 RNN 的实际实现。

让我们开始吧。

RNNs 简介

循环神经网络(RNNs)本质上是采用递归的神经网络,其使用来自神经网络上的前向传递的信息。本质上,所有的 rnn 都可以描述为一个递归关系。rnn 适用于这样的问题,并且在应用于这些问题时取得了令人难以置信的成功,在这些问题中,要对其进行预测的输入数据是序列形式的(顺序很重要的一系列实体)。序列数据的例子包括时间序列、自然语言处理、语音分析等。

图 7-1 展示了一个规则的 RNN 是如何展开(随时间)形成一个循环神经网络的。在下一节中,我们将探讨 RNN 利用的基础。

img/478491_2_En_7_Fig1_HTML.jpg

图 7-1

一个正规的 RNN 展开了(来源——深度学习www . Deep Learning book . org/contents/rnn . html

让我们从描述 RNN 的运动部件开始。首先,我们介绍一些符号。我们将假设输入由一系列实体组成x【1】x (2) ,…,x()。对应于这个输入,我们需要产生一个序列y【1】y【2】,…,y(τ)或者整个输入序列 y (或者一个不同长度的序列)。不同架构的 RNN 将为不同的用例提供解决方案。图 7-2 展示了基于输入输出长度的 RNN 类型。

*img/478491_2_En_7_Fig2_HTML.jpg

图 7-2

基于输入和输出长度的 RNN 类型

当我们有一个不利用来自先前状态的信息的 RNN 时,我们有一个传统的神经网络。然而,随着循环的出现,我们有了几种新的可能性。如今,NLP 中最常见的用例围绕着多对一和多对多模型。示例包括命名实体识别和机器翻译(例如,将文档从法语翻译成英语)。本章探索了几个简单的例子,但是深入讨论每个变体超出了本书的范围。强烈建议读者独立探索命名实体识别、机器翻译(以及可选的音乐生成)。

让我们从基础开始。

为了区分 RNN 生产的产品(即预测)和理想预期生产的产品(即实际),我们用 RNN 生产的$$ {\hat{y}}{(1)},{\hat{y}}{(2)},\dots, {\hat{y}}^{\left(\tau \right)} $$$$ \hat{y} $$来表示预测。

类似地,我们将表示基本事实,即 RNN 应该理想产生的实际值,表示为y【1】y【2】,…, y ( τ ) 。图 7-3 显示了 RNN 产生的输出(预测)为$$ {\hat{y}}{(1)},{\hat{y}}{(2)},\dots, {\hat{y}}^{\left(\tau \right)} $$。为了计算与实际值的差异,我们将比较这些生成的输出与实际值,表示为y【1】y【2】,…,y()

*rnn 或者为输入序列中的每个实体产生一个输出(多对多),或者为整个序列产生一个输出(多对一),如图 7-2 所示。让我们考虑一个 RNN,它为输入中的每个实体产生一个输出(本质上指的是图 7-1 中所示的展开的网络)。

img/478491_2_En_7_Fig3_HTML.jpg

图 7-3

展开的 RNN(多对多),代表图形 7-1 的一部分

可以使用以下等式来描述 RNN:

$$ {h}{(t)}=\mathit{\tanh}\left(U{x}{(t)}+W{h}^{\left(t-1\right)}+b\right) $$

$$ {\hat{y}}^{(t)}= softmax\left(V{h}^{(t)}+c\right) $$

U 是网络输入的权重, V 是激活函数输出的权重, W 是当前隐藏状态的权重矩阵。

关于 RNN 方程,应注意以下几点:

  1. RNN 计算包括计算序列中实体的隐藏状态。这用 h ( t ) 来表示。

  2. h ( t ) 的计算使用实体 x ( t ) 处的相应输入和之前的隐藏状态h(t—1)

  3. 使用隐藏状态h(t)计算输出$$ {\hat{y}}^{(t)} $$

  4. 在计算当前隐藏状态时,一组权重与输入和前一隐藏状态相关联。这分别由 UW 表示。还有一个偏差项,用 b 表示。

  5. 类似地,在计算输出时,一组权重也与当前隐藏状态相关联。这由 V 表示。还有一个偏差项,用 c 表示。

  6. 此外,在计算隐藏状态时,使用了tanh激活函数(在前面的章节中介绍过)。

  7. softmax激活功能用于输出的计算。

  8. 如等式所述,RNN 可以处理任意大的输入序列。

  9. RNN 的参数— UWVbc 等。-在隐藏层和输出值的计算中共享(对于序列中的每个图元)。

图 7-4 显示了 RNN。注意隐藏状态下与自循环的递归关系。

img/478491_2_En_7_Fig4_HTML.jpg

图 7-4

RNN(使用以前的隐藏状态重复)

图 7-4 还描述了与每个输入相关的每个输出的损失函数。当讨论如何训练 rnn 时,我们将回头参考它。

将 RNN 与我们之前讨论的所有前馈神经网络(包括卷积网络)的不同内在化是至关重要的。关键的区别是隐藏状态,它表示过去看到的实体的汇总(对于同一序列)。

暂时忽略如何训练 RNN,应该清楚如何使用训练过的 RNN。对于给定的输入序列,RNN 将为输入中的每个实体生成一个输出。

现在让我们考虑 RNN 中的一种变化,其中我们使用前一状态产生的输出来代替使用隐藏状态的递归(图 7-5 )。

img/478491_2_En_7_Fig5_HTML.jpg

图 7-5

RNN(使用先前输出的递归)

描述这样一个 RNN 的方程式如下:

$$ {h}{(t)}=\mathit{\tanh}\left(U{x}{(t)}+W{\hat{y}}^{\left(t-1\right)}+b\right) $$

$$ {\hat{y}}^{(t)}= softmax\left(V{h}^{(t)}+c\right) $$

应注意以下几点:

  1. RNN 计算包括计算序列中实体的隐藏状态。这用 h ( t ) 来表示。

  2. h ( t ) 的计算使用实体 x ( t ) 的相应输入和先前的输出$$ {\hat{y}}^{\left(t-1\right)}. $$

  3. 使用隐藏状态h(t)计算输出$$ {\hat{y}}^{(t)} $$

  4. 在计算当前隐藏状态时,一组权重与输入和先前输出相关联。这分别由 UW 表示。还有一个偏差项,用 c 表示。

  5. 计算输出时,权重与隐藏状态相关联。这由 V 表示。还有一个偏差项,用 c 表示。

  6. tanh激活函数用于隐藏状态的计算。

  7. softmax 激活函数用于计算输出。

现在让我们考虑 RNN 的一种变体,其中整个序列只产生一个输出(图 7-6 )。这样的 RNN 是使用以下等式来描述的:

$$ {h}{(t)}=\mathit{\tanh}\left(U{x}{(t)}+W{\hat{y}}^{\left(t-1\right)}+b\right) $$

$$ \hat{y}= softmax\left(V{h}^{\left(\tau \right)}\kern0.5em +c\right) $$

img/478491_2_En_7_Fig6_HTML.jpg

图 7-6

RNN(为整个输入序列产生单个输出)

应注意以下几点:

  1. RNN 计算包括计算序列中实体的隐藏状态。这用 h ( t ) 来表示。

  2. h ( t ) 的计算使用实体 x ( t ) 处的相应输入和之前的隐藏状态h(t—1)

  3. 对输入序列x(1)x(2),…,x(τ)中的每个实体进行 h ( t ) 的计算。

  4. 仅使用最后一个隐藏状态h??(τ)来计算输出$$ \hat{y} $$

  5. 在计算当前隐藏状态时,一组权重与输入和前一隐藏状态相关联。这分别由 UW 表示。还有一个偏差项,用 b 表示。

  6. 计算输出时,权重与隐藏状态相关联。这由 V 表示。还有一个偏差项,用 c 表示。

  7. tanh激活函数用于隐藏状态的计算。

  8. softmax 激活函数用于计算输出。

培训注册护士

本节描述了如何训练注册护士。我们首先需要看看当我们展开递归关系时,RNN 是什么样子的,递归关系是 RNN 的核心。展开对应于 RNN 的递归关系只是通过递归地替换定义递归关系的值来写出方程。

在图 7-1 中的 RNN 的情况下,这是h??(t)。也就是说, h ( t ) 的值由h(t—1)定义,依次由h(t—2)定义,以此类推,直到 h (0) 我们将假设 h (0) 或者由用户预定义,设置为零,或者作为另一个参数/权重被学习(像 WVb 一样被学习)。展开简单来说就是写出用 h (0) 描述 RNN 的方程。当然,为了做到这一点,我们需要确定序列的长度,用 τ 表示。在这一节中,我们将探索展开我们上面探索的几个不同的 rnn。我们将从展开 RNN 开始,之前的隐藏状态用于递归(如图 7-3 所示)。稍后,我们也将探索同样的 RNN 使用以前的输出进行递归,并最终展开一个单输出的 RNN。

图 7-7 示出了与图 7-4 中的 RNN 相对应的展开的 RNN,假设输入序列的大小为 4。类似地,图 7-8 和图 7-9 分别示出了与图 7-5 和图 7-6 所示的 rnn 相对应的展开的 rnn。

img/478491_2_En_7_Fig7_HTML.jpg

图 7-7

展开图 7-4 对应的 RNN

图 7-7 展开图 7-4 所示的递归网络——即递归单元从之前的隐藏状态开始添加。我们可以通过将 h 0 传递给 h 1 来注意到这一点,这是 x (1) 的隐藏状态。类似地,隐藏状态 h 3 被传递到 h 4 ,这是本图中的最后一步。权重 W 和偏差 b 在重复单元之间共享。

img/478491_2_En_7_Fig8_HTML.jpg

图 7-8

展开图 7-5 对应的 RNN

图 7-8 展开图 7-5 所示的递归网络——即从之前的输出状态增加递归单元。我们可以通过引用传递给 h 1$$ \hat{y} $$ (0) 来注意到这一点,即 x (1) 的隐藏状态。类似地,输出状态$$ \hat{y} $$ (3) 被传递到 h 4 ,这是本图中的最后一步。权重 W 和偏差 b 在重复单元之间共享。

img/478491_2_En_7_Fig9_HTML.jpg

图 7-9

展开图 7-6 对应的 RNN(单输出)

展开过程基于输入序列的长度预先已知的假设进行操作,并基于此展开递归。一旦 RNN 展开,我们基本上就有了一个非循环神经网络。

需要学习的参数— UWVbc 等。(在图 7-9 中用黑色表示)-在隐藏层和输出值的计算中共享。我们之前在卷积神经网络的上下文中已经看到了这样的参数共享。

给定给定大小的输入和输出(例如, τ ,在图 7-7 到 7-9 中假设为 4),我们可以展开 RNN,并计算要学习的参数相对于损失函数的梯度(如前面章节所述)。

因此,训练 RNN 简单地首先展开给定大小的输入和相应的期望输出的 RNN,然后通过计算梯度和使用随机梯度下降来训练展开的 RNN。

如前所述,RNNs 可以处理任意长的输入;相应地,它们需要在任意长的输入上被训练。图 7-10 至 7-12 展示了如何针对不同尺寸的输入展开 RNN。请注意,一旦 RNN 展开,训练 RNN 的过程与训练常规神经网络的过程相同,如前几章所述。在图 7-101-7-11 . 1 . 3 中,图 7-4 中描述的 RNN 对于输入尺寸 1、2、3 和 4 展开。

img/478491_2_En_7_Fig10_HTML.jpg

图 7-10

展开图 7-4 对应的 RNN(步骤 1 和步骤 2)

图 7-10 展示了步骤 1 和步骤 2——即依次展开输入序列 x (1) 和 x (2) 。在步骤 1 中,假设我们没有先前的隐藏状态,我们将 h (0) 传递给当前的隐藏状态。在图 7-10 中,我们将时间序列限制为展开,即τ= 4;因此,网络展开为 4 步。图 7-11 和图 7-12 依次演示了增量展开步骤。

img/478491_2_En_7_Fig11_HTML.jpg

图 7-11

展开图 7-4 对应的 RNN(步骤 3)

这里,我们将第三个输入序列连接到展开的网络。权重 U、W 和 V 在整个网络中共享。在下一个也是最后一个步骤中,我们可以看到展开的网络与图 7-7 中所示的网络相同(即,针对四个输入序列展开)。

img/478491_2_En_7_Fig12_HTML.jpg

图 7-12

展开图 7-4 对应的 RNN(步骤 4) |与图 7-7 相同

假设要训练的数据集由不同大小的序列组成,输入序列被分组,使得相同大小的序列归入一组。然后,对于一个组,我们可以展开序列长度的 RNN 并训练它。针对不同组的训练将需要针对不同的序列长度展开 RNN。因此,可以通过展开来训练不同大小输入的 RNN,并根据序列长度展开来训练它。

必须注意的是,训练图 7-4 中所示的展开的 RNN 本质上是一个连续的过程,因为隐藏状态是相互依赖的。在递归超过输出而不是隐藏状态的 RNNs 的情况下(图 7-5 ,可以使用一种叫做老师强制的技术,如图 5-9 所示。这里的关键思想是训练时在h(t)的计算中用y(t—1)代替$$ {\hat{y}}^{\left(t-1\right)} $$。然而,在进行预测时(当模型被部署使用时),使用了$$ {\hat{y}}^{\left(t-1\right)} $$

双向 RNNs

现在让我们看看 RNNs 的另一种变体,双向 RNN。双向 RNN 背后的关键思想是使用序列中位于更远处的实体来对当前实体进行预测。对于我们到目前为止考虑的所有 rnn,我们一直使用序列中的先前实体(由隐藏状态捕获)和当前实体来进行预测。然而,我们并没有利用序列中更靠后的实体的信息来进行预测。双向 RNN 利用这些信息,在许多情况下可以提高预测的准确性(图 7-13 )。

考虑下面这个简单的例子,它来自吴恩达的 Coursera 讲座:

  • 他说,“泰迪熊是漂亮的玩具。”

  • 他说,“泰迪·罗斯福,美国总统。”

在这些句子中,考虑到 NLP 的一个经典案例(预测下一个单词),没有办法正确预测“Teddy”之后的单词(假设单向向前 RNN)。来自右侧的上下文本质上揭示了对下一个单词的准确预测。考虑一个情感分析任务,其中一个模型试图将句子分类为肯定或否定。随着网络中左右语境的建立,双向模型可以有效地在句子中“向前看”,以查看“未来”标记是否会影响当前决策。在情绪分类(多对一 RNN)的情况下,有一些讽刺性的评论,其中肯定词后面的词否定了肯定词的存在——例如,“我喜欢这部电影,有史以来最大的笑话!”在这里,右边的上下文否定了“爱”这个词的存在。

双向 RNN 可以使用以下等式来描述:

$$ {h}_f{(t)}=\mathit{\tanh}\left({U}_f{x}{(t)}+{W}_f{h}^{\left(t+1\right)}+{b}_f\right) $$

$$ {h}_b{(t)}=\mathit{\tanh}\left({U}_b{x}{(t)}+{W}_b{h}^{\left(t-1\right)}+{b}_b\right) $$

$$ {\hat{y}}^{(t)}= softmax\left({V}_b{h}_b{(t)}+{V}_f{h}_f{(t)}+c\right) $$

RNN 计算包括计算序列中实体的前向隐藏状态和后向隐藏状态。这分别由$$ {h}_f^{(t)} $$$$ {h}_b^{(t)} $$表示。$$ {h}_f^{(t)} $$的计算使用实体x??(t)和先前隐藏状态$$ {h}_f^{\left(t-1\right)}. $$的相应输入$$ {h}_b^{(t)} $$的计算使用实体x(t)和先前隐藏状态$$ {h}_b^{\left(t-1\right)}. $$的相应输入

使用隐藏状态$$ {h}_f^{(t)} $$$$ {h}_b^{(t)} $$计算输出$$ {\hat{y}}^{(t)} $$。在计算当前隐藏状态时,一组权重与输入和前一隐藏状态相关联。这分别用 U * f W f U b W b 来表示。还有偏置项,分别用 b f b b * 表示。

类似地,在计算输出时,一组权重与计算输出时的隐藏状态相关联。这用 V bV f 来表示。还有一个偏置项,用 c 表示。tanh激活函数用于隐藏状态的计算。softmax 激活函数用于计算输出。

如等式所述,RNN 可以处理任意大的输入序列。RNN 的参数—Uf,UbW fW bV bV -在隐藏层和输出值的计算中共享(对于序列中的每个图元)。

img/478491_2_En_7_Fig13_HTML.jpg

图 7-13

双向 RNN

消失和爆炸渐变

由于消失和爆炸梯度,训练 rnn 可能具有挑战性(图 7-14 )。消失梯度意味着当在展开的 rnn 上计算梯度时,梯度的值可以下降到非常小的数字(接近零)。类似地,梯度可以增加到非常高的值,这被称为爆炸梯度问题。在这两种情况下,训练 RNN 都是一个挑战。消失或爆炸梯度通常是为网络超参数和参数设置不适当或不需要的值的结果。因此,随着每次增量权重更新,网络需要花费异常长的时间来脱离斜率,并学习用例的最佳权重。

让我们再来看看描述 RNN 的方程。

$$ {h}{(t)}=\mathit{\tanh}\left(U{x}{(t)}+W{h}^{\left(t-1\right)}+b\right) $$

$$ {\hat{y}}^{(t)}= softmax\left(V{h}^{(t)}+c\right) $$

我们可以通过应用链式法则推导出$$ \frac{\partial L}{\partial W} $$的表达式。如图 7-10 所示。

$$ \frac{\partial L}{\partial W}={\sum}_{1\le t\le \tau}\frac{\partial {L}^{(t)}}{\partial {h}^{(t)}}\left[{\sum}_{1\le k\le t}\left[{\prod}_{k\le j\le t-1}\frac{\partial {h}^{\left(j+1\right)}}{\partial {h}^{(j)}}\right]\ \frac{\partial {h}^{(k)}\ }{\partial W}\right] $$

现在让我们关注表达式$$ {\prod}_{k\le j\le t-1}\frac{\partial {h}^{\left(j+1\right)}}{\partial {h}^{(j)}} $$的部分,它涉及 W 的重复矩阵乘法,这有助于消失和爆炸梯度问题。直觉上,这类似于一个实数值一次又一次地相乘,这可能导致乘积缩小到零或爆炸到无穷大。

渐变剪辑

处理爆炸梯度的一个简单技术是,每当梯度超过用户定义的阈值时,重新调整梯度的范数。具体来说,如果梯度用$$ \hat{g}=\frac{\partial L}{\partial W} $$表示,如果$$ \left\Vert \hat{g}\ \right\Vert >c $$,那么我们设置$$ \hat{g}=\frac{c}{\left\Vert \hat{g}\right\Vert }\ \hat{g} $$。这种技术既简单又计算高效,但它确实引入了一个额外的超参数。

如果没有梯度裁剪,参数会大幅下降并流出所需区域。通过限幅,下降步长被限制,并且参数保持在期望的区域内。渐变裁剪将“裁剪”渐变,或者将它们限制在一个阈值,以防止它们变得太大。在图 7-14 中,梯度因过冲而被剪切,成本函数遵循虚线值,而不是其在期望区域外的原始轨迹。

img/478491_2_En_7_Fig14_HTML.jpg

图 7-14

渐变剪辑

长短期记忆

让我们来看看 RNNs 的另一种变体,长短期记忆(LSTM)网络(见图 7-15 )。香草 RNN 有几个权衡,导致网络在学习序列之间的长相关性时表现不佳。总的来说,RNN 更容易产生噪音,在训练时容易过度疲劳。训练它们在计算上也非常昂贵。

LSTMs 非常适合通过使用更直观的方法来解决这些问题。与 rnn 相比,它们通常对噪声更鲁棒,并且更准确地捕捉短期和长期相关性,同时易于调整和训练。LSTMs 还具有比 rnn 更快的计算速度。LSTMs 具有配备便利功能的门,这些功能帮助网络记住长期依赖关系以及忘记无关紧要的依赖关系。在 RNNs 中,先前的隐藏状态是网络记得的唯一先前的记忆。有了 LSTMs,除了之前的隐藏状态,小区状态也被网络记住。

LSTM 网络的核心概念是单元状态和门(输入、输出和遗忘门)。这些门和单元状态包括几个操作,例如 sigmoid 和 tanh 激活、逐点乘法和加法以及向量连接。这些操作帮助单元状态和门训练网络忘记或通过网络传播重要信息。细胞状态连接整个网络的信息,从而有助于在需要时传递序列之间的长依赖性。

LSTM 可以用下面的一组等式来描述。请注意,⨀符号表示两个向量的逐点相乘——也就是说,如果 a = [1,1,2]并且 b = [0.5,0.5,0.5],那么 ab = [0.5,0.5,1]。函数 σgh 为非线性激活函数; WR 为权重矩阵;并且 b 项是偏置项。

$$ {z}{(t)}=g\left({W}_z{x}{(t)}+{R}_z\kern0.5em {\hat{y}}^{\left(t-1\right)}+{b}_z\right) $$

$$ {i}^{(t)}=\sigma \left({W}_i{x}{(t)}+{R}_i {\hat{y}}{\left(t-1\right)}+{p}_i\bigodot {c}^{\left(t-1\right)}+{b}_i\right) $$

$$ {f}^{(t)}=\sigma \left({W}_f{x}t+{R}_f {\hat{y}}{\left(t-1\right)}+{p}_f\bigodot {c}^{\left(t-1\right)}+{b}_f\right) $$

$$ {c}{(t)}={i}{(t)}\bigodot {z}{(t)}+{f}{(t)}\bigodot {c}^{\left(t-1\right)} $$

$$ {o}^{(t)}=\sigma \left({W}_o{x}{(t)}+{R}_o {\hat{y}}{\left(t-1\right)}+{p}_o\bigodot {c}^{(t)}+{b}_o\right) $$

$$ {\hat{y}}{(t)}={o}{(t)}\bigodot h\left({c}^{(t)}\right) $$

应注意以下几点:

  1. LSTM 最重要的元素是细胞状态,用c(t)=I(t)z(t)+f(t)c基于块输入z(t)和先前单元状态c(t—1)更新单元状态。输入门It(t)确定块输入的哪一部分进入单元状态(因此称为)。遗忘门f(t)决定了要保留多少先前的单元格状态。**

  2. 输出$$ {\hat{y}}^{(t)} $$由单元状态c(t)和输出门 o ( * t * ) 决定,决定了单元状态对输出的影响程度。

  3. z ( t ) 项,称为块输入,根据当前输入和先前输出产生一个值。

  4. i ( t ) 项,称为输入门,决定了在单元状态 c ( t ) 下保留多少输入。

  5. 所有的 p 项都是窥视孔连接,允许单元状态的一部分考虑到所讨论的项的计算中。

  6. 单元格状态 c ( i ) 的计算不会遇到渐变消失的问题。(这被称为恒定误差旋转。)然而,LSTMs 受到爆炸梯度的影响,并且在训练时使用梯度剪裁。

img/478491_2_En_7_Fig15_HTML.png

图 7-15

一个长短期记忆网络

实际实现

本节描述了一个用 PyTorch 实现 RNN 和 LSTM 的实例。我们将把练习分成两部分。首先,我们将只使用没有额外处理的普通 RNN 网络(来自 NLP 领域),并在情感分类数据集上训练网络。我们预计这种普通的网络性能会很差。第二,我们将对网络进行重大改进。我们将利用 LSTM 层,而不是 RNN 层,并使网络双向辍学正规化。这样的网络在我们的数据集上会表现得更好。

我们将使用 TorchText 包,它由数据处理工具和 NLP 的流行数据集组成。我们将利用位于 https://www.kaggle.com/columbine/imdb-dataset-sentiment-analysis-in-csv-format 的 Kaggle 上托管的数据集。

我们建议利用 Kaggle 笔记本进行练习(打开互联网选项并启用 GPU 加速器)。

让我们从导入基本包开始(清单 7-1 )。

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import torch
from torch import nn,optim
import torchtext
from torchtext import data

#Check if we have GPU enabled
if torch.cuda.is_available():
    device = "cuda"
else:
    device = "cpu"
print("Device =",device)

input_data_path = "/kaggle/input/imdb-dataset-sentiment-analysis-in-csv-format/"

Listing 7-1Importing the Packages for the RNN

首先,让我们使用 Pandas 在高层次上探索数据集。这里的目标是对数据集有一个粗略的了解。对于本练习的剩余部分,我们将使用基于 TorchText 的包装器来处理 NLP 领域内的训练和验证数据集。清单 7-2 将我们用例的数据读入内存。

img/478491_2_En_7_Figa_HTML.jpg

#Read the csv dataset using pandas
df = pd.read_csv("/input/imdb-dataset-sentiment-analysis-in-csv-format/Train.csv")
print("DF.shape :\n",df.shape)
print("df.label = ",df.label.value_counts())
df.head()

Output[]
DF.shape :  (40000, 2)

df.label =  0    20019
            1    19981
Name: label, dtype: int64

Listing 7-2Reading Data into Memory

我们在数据集中只有两列:“文本”,包含实际的注释,“标签”,包含值 0(负)和 1(正)。正负之间的分布相当均匀。

接下来,我们将使用 TorchText 数据集包装器,它将帮助我们创建基于迭代器的数据集,简化我们需要的数据处理任务。如清单 7-3 所示,我们从定义训练和验证数据集所需的原始数据类型开始。

#Define a custom tokenizer
my_tokenizer  = lambda x:str(x).split()

#Define fields for our input dataset
TEXT = data.Field(sequential=True, lower= True,tokenize = my_tokenizer,use_vocab=True)
LABEL  = data.Field(sequential = False,use_vocab = False)

#Define inut fields as a list of tuples of fields
trainval_fields = [("text",TEXT),("label",LABEL)]

#Contruct dataset
train_data, val_data = data.TabularDataset.splits(path = input_data_path, train = "Train.csv", validation = "Valid.csv", format = "csv", skip_header = True, fields = trainval_fields)

#Build vocabulary
MAX_VOCAB_SIZE = 25000
TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)

#Define iterators for  train and validation
train_iterator  = data.BucketIterator(train_data, device = device
                             , batch_size = 32
                             , sort_key = lambda x:len(x.text)
                             ,sort_within_batch = False
                             ,repeat = False)

val_iterator = data.BucketIterator(val_data, device = device,
                             batch_size= 32
                             , sort_key = lambda x:len(x.text)
                             , sort_within_batch = False
                             , repeat = False)

print(TEXT.vocab.freqs.most_common()[:10])

Output[]
[('the', 511112), ('a', 253702), ('and', 251397), ('of', 229381), ('to', 211883)
, ('is', 164005), ('in', 143530), ('i', 113576), ('this', 110892), ('that', 104153)]

Listing 7-3Defining the Tokenizer, Fields, and Dataset for Training and Validation

在清单 7-3 中,我们处理了一些我们的网络所必需的东西。对于 NLP 用例,在使用数据进行网络训练之前,作为文本处理的一部分,我们需要对数据进行标记化和数值化。你可能已经猜到了,神经网络只处理数字数据。上述两项操作都由 PyTorch 内部巧妙处理。我们可以提供一个现有的标记器——例如 SpaCy(一个开源的高级 NLP 库)PyTorch 会完成剩下的工作。在这个例子中,我们使用一个定制的简单的。接下来,我们为数据集定义必要的字段(原始数据)。Field类对可以用张量表示的常见文本处理数据类型进行建模。此外,它还保存了一个Vocab对象,该对象定义了承载字段中出现的所有单词的数字表示的向量。我们的数据集有两列,“文本”和“标签”,前者是简单的英文注释,后者是数字标签(0/1)。因此,我们将 TEXT 和 LABEL 定义为代表我们的列的两个单独的字段。我们添加了一个参数来定义这个字段所需的标记化函数,一个布尔标志来将文本转换为小写,一个布尔标志来指示这个字段中的数据是连续的。对于标签字段,我们没有顺序数据;因此,我们将其设置为 False。

接下来,我们定义创建数据集时需要的数据字段列表。该列表表示数据集中的每一列。如果我们计划不使用这个数据集中的某个特定列,那么在定义列的列表时,我们需要将列名指定为“None”。我们将这个列表分配给trainval_fields变量。然后,我们创建一个TabularDataset对象,其中包含对数据列进行必要操作的精简列表。请注意,splits()函数实际上并不分割现有的数据集。只有当路径中已经有单独分离的数据集时,才应该使用它。

接下来,我们需要构建词汇表(在我们的字段文本中出现的唯一单词的数字化表示)。这一步非常重要,有几种执行手段。我们可以使用预训练的单词嵌入来创建词汇,或者我们可以定制一个。使用预训练的很简单,所以我们将在下一个例子中使用它。我们将最大词汇量设置为 25,000。该函数还将创建两个额外的单词,总数为 25,002—一个用于所有未知的标记(例如,新单词),另一个用于填充(用于生成等长的句子)。

最后,我们创建迭代器对象。sort_within_batch参数根据sort_key对每个小批量内的数据进行降序排序。当我们想将pack_padded_sequence用于填充的序列数据并将填充的序列张量转换为PackedSequence对象时,这是必要的。我们不会在第一个练习中利用这个特性,但是我们将在下一个练习中使用它,在下一个练习中我们将改进我们的模型。本质上,PyTorch 在序列中添加了填充符,这样所有序列的长度都相等。通过按关键字的降序对数据进行排序,该过程变得高效,并确保网络不会学习填充。最后一行打印 vocab 中最常用的单词,并返回与向量中每个单词相关的索引(嵌入)。

准备好要处理的数据后,我们将构建我们的 RNN 类,如清单 7-4 所示。

class RNNModel(nn.Module):

    def __init__(self,embedding_dim,input_dim,hidden_dim,output_dim):
        super().__init__()
        self.Embedding  = nn.Embedding(input_dim,embedding_dim)
        self.rnn  = nn.RNN(embedding_dim,hidden_dim)
        self.fc  = nn.Linear(hidden_dim,output_dim)

    def forward(self,text):
        embed = self.Embedding(text)
        output, hidden = self.rnn(embed)
        out  = self.fc(hidden.squeeze(0))
        return(out)

#Define model
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

#Create model instance
model = RNNModel(EMBEDDING_DIM, INPUT_DIM,HIDDEN_DIM, OUTPUT_DIM)

Listing 7-4Defining the RNN Class

这段代码的很大一部分与我们在第 5 和 6 章中的实验非常相似。这里新增加的是嵌入层和 RNN 层。RNN 层返回输出以及隐藏层计算(不像我们到目前为止探索的其他层)。输入维度是我们的 vocab 列表的长度。嵌入维数是我们决定在数字上最能代表一个单词的一个值。我们这里用 100,但也可能是 200,300,或者更高。更大的数字并不总是有价值的,而且会显著增加计算量。此外,我们为隐藏层选择 256 维,为输出层选择 1 维(因为结果是二进制的)。

接下来,在清单 7-5 中,我们定义了两个函数,这两个函数将包装给定时期的训练步骤和评估步骤。随后,我们通过另一个函数为每个时期编排训练步骤和评估步骤。

#Define training step
def train(model, data_iterator,optimizer,loss_function):
    epoch_loss,epoch_acc,epoch_denom = 0,0,0

    model.train()    #Explicitly set model to train mode

    for i, batch in enumerate(data_iterator):

        optimizer.zero_grad()
        predictions = model(batch.text)

        loss = loss_function(predictions.reshape(-1,1), batch.label.float().reshape(-1,1))
        acc = accuracy(predictions.reshape(-1,1), batch.label.reshape(-1,1))

        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()
        epoch_acc += acc.item()
        epoch_denom += len(batch)

    return epoch_loss/epoch_denom,epoch_acc, epoch_denom

#Define evaluation step
def evaluate(model, data_iterator,loss_function):
    epoch_loss,epoch_acc,epoch_denom = 0,0,0

    model.eval()     #Explcitly set model to eval mode

    for i, batch in enumerate(data_iterator):
        with torch.no_grad():
            predictions = model(batch.text)

            loss = loss_function(predictions.reshape(-1,1), batch.label.float().reshape(-1,1))
            acc = accuracy(predictions.reshape(-1,1), batch.label.reshape(-1,1))

            epoch_loss += loss.item()
            epoch_acc += acc.item()
            epoch_denom += len(batch)

    return epoch_loss/epoch_denom, epoch_acc, epoch_denom

Listing 7-5Defining the Training and Evaluation Step

在这里,内容与前面的实验相似。我们为训练循环创建必要的样板代码。注意,我们在 evaluate 函数中需要一个助手函数来计算精度(在我们的例子中是二进制结果)。这一部分不是强制性的,但它有助于在每个时期后准确地查看中间结果。清单 7-6 为我们的网络定义了功能和必要的位。

#Compute binary accuracy
def accuracy(preds, y):
    rounded_preds = torch.round(torch.sigmoid(preds))

    #Count the number of correctly predicted outcomes
    correct = (rounded_preds == y).float()
    acc = correct.sum()

    return acc

#Define optimizer, loss function
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCEWithLogitsLoss()

#Transfer components to GPU, if available.
Model = model.to(device)
criterion = criterion.to(device)

Listing 7-6Defining the Accuracy Function, Loss Function, and Optimizer, and Instantiating the Model

最后,在清单 7-7 中,我们用定义损失函数和优化器在五个时期的循环中训练上面实例化的模型。我们在这里定义 5 只是为了说明的目的;对于实际的例子,我们建议根据数据的大小和网络的复杂性增加历元的数量。

n_epochs = 5

for epoch in range(n_epochs):
    #Train and evaluate
    train_loss, train_acc,train_num = train(model, train_iterator, optimizer, criterion)
    valid_loss, valid_acc,val_num = evaluate(model, val_iterator,criterion)

    print("Epoch-",epoch)

    print(f'\tTrain  Loss: {train_loss: .3f} | Train Predicted Correct : {train_acc}
                                   | Train Denom: {train_num} |
                          PercAccuracy: {train_acc/train_num}')
    print(f'\tValid  Loss: {valid_loss: .3f} | Valid Predicted Correct: {valid_acc}
                                        | Val Denom: {val_num}|
                          PercAccuracy: {train_acc/train_num}')
Output[]
Epoch -0
Train  Loss:  0.022 | Train Predicted Correct : 20149.0 | Train Denom: 40000 | PercAccuracy: 0.503725
Valid  Loss:  0.022 | Valid Predicted Correct: 2537.0 | Val Denom: 5000| PercAccuracy: 0.503725

Epoch -1
Train  Loss:  0.022 | Train Predicted Correct : 20048.0 | Train Denom: 40000 | PercAccuracy: 0.5012
Valid  Loss:  0.022 | Valid Predicted Correct: 2497.0 | Val Denom: 5000| PercAccuracy: 0.5012

Epoch -2
Train  Loss:  0.022 | Train Predicted Correct : 20023.0 | Train Denom: 40000 | PercAccuracy: 0.500575
Valid  Loss:  0.022 | Valid Predicted Correct: 2507.0 | Val Denom: 5000| PercAccuracy: 0.500575

Epoch -3
Train  Loss:  0.022 | Train Predicted Correct : 20143.0 | Train Denom: 40000 | PercAccuracy: 0.503575
Valid  Loss:  0.022 | Valid Predicted Correct: 2556.0 | Val Denom: 5000| PercAccuracy: 0.503575

Epoch -4
Train  Loss:  0.022 | Train Predicted Correct : 19996.0 | Train Denom: 40000 | PercAccuracy: 0.4999
Valid  Loss:  0.022 | Valid Predicted Correct: 2492.0 | Val Denom: 5000| PercAccuracy: 0.4999

Listing 7-7Training the Model for Five Epochs

我们可以看到该模型的性能几乎没有提高。虽然五个纪元其实太少了,但我们应该已经看到了小的变化。整体准确性并没有真正增加模型的任何价值。性能差。为了改善我们的结果,我们将在第二次实验中采取更全面的方法。

在我们的第二个实验中,我们将利用 Spacy 的标记器(而不是使用我们的自定义标记器)和预训练的单词嵌入(而不是从头开始训练),并添加双向 LSTM 层(而不是单向 RNN 层)。我们还将增加辍学,以减少过度拟合。

我们实际上需要从头开始,而不是继续使用相同的代码库(尽管变化很小)。

像往常一样,我们从导入所需的包开始,如清单 7-8 所示。

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import torch,torchtext
from torch import nn, optim
from torch.optim import Adam
from torchtext import data

if torch.cuda.is_available():
    device = "cuda"
else:
    device = "cpu"
print("Device =",device)

input_data_path = " /input/imdb-dataset-sentiment-analysis-in-csv-format/"

#Define fields for our input dataset
TEXT = data.Field(sequential=True, lower= True,tokenize = 'spacy', include_lengths = True)
LABEL  = data.Field(sequential = False,use_vocab = False)

#Define a list of tuples of fields
trainval_fields = [("text",TEXT),("label",LABEL)]

#Contruct dataset
train_data, val_data = data.TabularDataset.splits(path = input_data_path, train = "Train.csv", validation = "Valid.csv", format = "csv", skip_header = True, fields = trainval_fields)

#Build Vocab using pretrained
MAX_VOCAB_SIZE = 25000
TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE,   vectors = 'fasttext.simple.300d')
BATCH_SIZE = 64

train_iterator, val_iterator =  data.BucketIterator.splits(
                              (train_data, val_data),
                              batch_size = BATCH_SIZE,
                              sort_key  = lambda x:len(x.text),
                              sort_within_batch = True,
                              device = device)

Listing 7-8Importing the Required Packages

我们将只关注前面代码片段中的变化。在定义数据字段时,我们使用了 Spacy 的 tokenizer。使用字符串spacy作为 tokenize 参数就足够了;PyTorch 在后端管理必要的繁重工作。我们还添加了参数include_length作为true。这是必要的,因为我们稍后会添加填充并对一批中的样本进行排序。为了利用这一点,我们现在需要将样本的长度和文本一起传递给 RNN 模型的类定义中的 forward 函数。

在构建词汇表时,我们使用vectors = 'fasttext.simple.300d'告诉 PyTorch 下载预先训练好的 fasttext 向量,并为我们的文本字段中的单词创建一个嵌入向量。(如果使用的是 Kaggle 内核,应该在笔记本环境设置中开启互联网选项)。这个预训练的向量有 300 个维度。在创建网络实例时,我们需要注意这一变化。这一步可能需要一段时间,取决于你的网速。最后,我们还启用了排序并定义了排序键。PyTorch 下载已定义的预训练向量(通常为 300MN 或更多),并基于 25,000 个令牌为我们的用例创建一个子集。

现在让我们定义我们改进的序列模型,如清单 7-9 所示。

class ImprovedRNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout, pad_idx):

        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
        self.lstm = nn.LSTM(embedding_dim,
                           hidden_dim,
                           num_layers=n_layers,
                           bidirectional=bidirectional,
                           dropout=dropout)
        self.fc = nn.Linear(hidden_dim * 2, output_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, text, text_lengths):

        embedded = self.dropout(self.embedding(text))

        #pack sequence
        packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths)
        packed_output, (hidden, cell) = self.lstm(packed_embedded)

        #unpack sequence
        output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)
        hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))

        return self.fc(hidden)

Listing 7-9Defining the (Improved) RNN Class

请注意,我们在这里做了相当多的添加。我们现在有一个 LSTM 层,而不是香草 RNN。当bidirectional标志被设置为True时,它使我们能够捕捉前向和后向上下文。线性层的尺寸现在将是原始层的两倍,因为我们有一个串联的前向和后向网络。我们最初在定义最初的FIELD;时添加了include_lengths=True,因此,我们的转发函数现在将接受一个额外的参数。在从嵌入输出接收数据后,在将数据传递到线性层之前,打包和解包数据时,此信息是必需的。隐藏层现在将前向和后向网络的输出连接起来,然后再传递给下一层。清单 7-10 定义了模型属性并复制了预训练的权重。

#Define model input parameters
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 300
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5

#Create model instance
model = ImprovedRNN(INPUT_DIM,
            EMBEDDING_DIM,
            HIDDEN_DIM,
            OUTPUT_DIM,
            N_LAYERS,
            BIDIRECTIONAL,
            DROPOUT,
            PAD_IDX)

#Copy pretrained vector weights
model.embedding.weight.data.copy_(pretrained_embeddings)

#Initialize the embedding with 0 for pad as well as unknown tokens
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

print(model.embedding.weight.data)

Output []
torch.Size([25002, 300])

Listing 7-10Defining the Model Properties and Copying the Pretrained Weights

接下来,我们定义训练和评估函数,类似于我们之前的练习。唯一的区别是,我们需要将text_lengths作为模型中的一个附加参数来处理。我们还将定义计算二进制精度所需的精度函数,定义模型的损失函数、优化器,并在 GPU 上加载模型和损失函数(如果可用)。这些步骤与我们之前的练习相同。在清单 7-11 中,我们训练我们改进的模型定义。

#Define train step
def train(model, iterator, optimizer, criterion):

    epoch_loss,epoch_acc,epoch_denom = 0,0,0

    model.train()

    for batch in iterator:

        optimizer.zero_grad()
        text, text_lengths = batch.text
        predictions = model(text, text_lengths).squeeze(1)
        loss = criterion(predictions.reshape(-1,1), batch.label.float().reshape(-1,1))
        acc = accuracy(predictions, batch.label)

        loss.backward()

        optimizer.step()

        epoch_loss += loss.item()
        epoch_acc += acc.item()
        epoch_denom += len(batch)

    return epoch_loss/epoch_denom, epoch_acc, epoch_denom

#Define evaluate step
def evaluate(model, iterator, criterion):

    epoch_loss,epoch_acc,epoch_denom = 0,0,0
    model.eval()

    with torch.no_grad():
        for batch in iterator:
            text, text_lengths = batch.text
            predictions = model(text, text_lengths).squeeze(1)
            loss = criterion(predictions, batch.label.float())
            acc = accuracy(predictions, batch.label)
            epoch_loss += loss.item()
            epoch_acc += acc.item()
            epoch_denom += len(batch)

    return epoch_loss/epoch_denom, epoch_acc, epoch_denom

#Define optimizer, loss funciton and load to GPU
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()

model = model.to(device)
criterion = criterion.to(device)

#similar to previous exercise, we deifne our accuracy function
def accuracy(preds, y):
    rounded_preds = torch.round(torch.sigmoid(preds))

    correct = (rounded_preds == y).float()
    acc = correct.sum()

    return acc

#Finally lets train our model for 5 epochs
N_EPOCHS = 5

for epoch in range(N_EPOCHS):

    train_loss, train_acc,train_num = train(model, train_iterator, optimizer, criterion)
    valid_loss, valid_acc,val_num = evaluate(model, val_iterator, criterion)
    print("Epoch-",epoch)
    print(f'\tTrain  Loss: {train_loss: .3f} | Train Predicted Correct : {train_acc}
                                   | Train Denom: {train_num} |
                          PercAccuracy: {train_acc/train_num}')
    print(f'\tValid  Loss: {valid_loss: .3f} | Valid Predicted Correct: {valid_acc}
                                       | Val Denom: {val_num}|
                          PercAccuracy: {train_acc/train_num}')

Output[]
     Train  Loss:  0.005 | Train Predicted Correct : 34911.0 | Train Denom: 40000 | PercAccuracy: 0.872775
     Valid  Loss:  0.003 | Valid Predicted Correct: 4558.0 | Val Denom: 5000| PercAccuracy: 0.872775
Epoch- 1
     Train  Loss:  0.003 | Train Predicted Correct : 37193.0 | Train Denom: 40000 | PercAccuracy: 0.929825
     Valid  Loss:  0.004 | Valid Predicted Correct: 4557.0 | Val Denom: 5000| PercAccuracy: 0.929825
Epoch- 2
     Train  Loss:  0.002 | Train Predicted Correct : 38079.0 | Train Denom: 40000 | PercAccuracy: 0.951975
     Valid  Loss:  0.003 | Valid Predicted Correct: 4591.0 | Val Denom: 5000| PercAccuracy: 0.951975
Epoch- 3
     Train  Loss:  0.002 | Train Predicted Correct : 38659.0 | Train Denom: 40000 | PercAccuracy: 0.966475
     Valid  Loss:  0.004 | Valid Predicted Correct: 4569.0 | Val Denom: 5000| PercAccuracy: 0.966475
Epoch- 4
     Train  Loss:  0.001 | Train Predicted Correct : 39030.0 | Train Denom: 40000 | PercAccuracy: 0.97575
     Valid  Loss:  0.004 | Valid Predicted Correct: 4564.0 | Val Denom: 5000| PercAccuracy: 0.97575

Listing 7-11Training the Improved Model

如你所见,性能提高了很多。我们只训练了五个纪元的网络,但结果令人印象深刻。建议读者通过对网络进行更改来进行试验。实验可以包括改变预先训练的向量(可能是 glove 而不是 fasttext),在输入数据上处理更多 NLP 相关的动作,添加更多积极的退出,添加更多的纪元,等等。

我们的第二个练习到此结束,在这个练习中,我们试图提高序列模型的性能。我们使用了普通的 RNN 网络、LSTM 网络和双向网络。我们还利用预先训练的嵌入来实现单词的量化表示。(对于几乎所有与 NLP 相关的任务,强烈建议这样做。)还存在门控循环单元(gru ),其非常类似于 LSTMs,但是其计算速度稍快,因为它们具有较少的运算。然而,当谈到性能时,大多数研究人员发现 LSTMs 和 GRUs 非常相似。在 NLP 实验中,使用 LSTMs 和 GRUs 迭代,取其精华是很常见的。你可以在 https://arxiv.org/abs/1412.3555 阅读更多关于这项研究的内容。

讨论 GRUs 的细节超出了本章的范围。鼓励读者自己进一步探索这个话题。

摘要

在本章中,我们介绍了循环神经网络(RNNs)的基础知识。本章的要点是隐藏状态的概念,通过展开(通过时间的反向传播)训练 RNNs,消失和爆炸梯度的问题,以及长短期记忆(LSTM)网络。重要的是内在化 rnn 如何包含允许它们对一系列输入进行预测的内部/隐藏状态——这是一种超越传统神经网络的能力。**

八、深度学习的最新进展

到目前为止,这本书已经讨论了深度学习领域的重要主题:前馈网络,卷积神经网络和循环神经网络。我们描述了它们的实际方面,包括使用 PyTorch 改进的实现、培训、验证和调优模型。尽管我们在基础方面覆盖了很多领域,但仍有大量领域没有触及。深度学习领域最近见证了研究、贡献者和业界对尖端解决方案的采用的巨大增长。更新和变化的绝对速度(增量的和突破性的)是巨大的。即使你一直在读这本书,也可能有几篇突破性的研究论文发表,为深度学习领域的下一门课程量身定制。

在这最后一章,我们介绍了一些与深度学习相关的额外主题,这些主题应该可以帮助你以更有意义的方式研究这个主题。本章仅作为简要介绍,并不深入任何实现细节。建议您探索与这些主题相关的其他资源,以加强您的学术、个人和行业职业感兴趣的领域。

让我们开始吧。

超越计算机视觉中的分类

在第五章中,我们研究了使用卷积神经网络解决的深度学习中的计算机视觉问题。这个想法是新颖的和开创性的。第五章只关注一个关键领域——分类。我们研究了 MNIST 手写数字的经典例子,其中我们将给定的图像分类为 0-9 之间的数字[10 类]。在另一个练习中,我们看了猫和狗之间的二元分类。虽然使用计算技术将图像分类为有意义的标签的能力确实很有价值,但更进一步,它打开了几个对现代用例有深远价值的用例。

本节通过进一步扩展卷积神经网络的概念来探索一些可能性。

目标检测

物体检测,一种与计算机视觉相关的技术,试图区分一幅图像或视频中的一个或多个物体。例如,在猫与狗的分类练习中,对象检测将更进一步,并预测最能捕捉感兴趣对象的矩形边界框。在更复杂的用例中,对象检测可以用于检测图像/视频中的几个对象。

图 8-1 显示了一个复杂的物体检测算法。每个被识别的对象都有边界框来区分它们。

img/478491_2_En_8_Fig1_HTML.jpg

图 8-1

计算机视觉图像中的物体检测来源-github . com/face book research/detectron 2

针对每个人(多个人被识别)的边界框是来自对象检测的结果。

物体检测的现实使用案例包括从 CCTV 视频流中识别汽车,从而跟踪重要路线上的交通状况,在智能手机上使用面部检测,以便自动对焦可以精确地聚焦在重要物体上,从而改善照片,等等。

图象分割法

继物体检测之后,计算机视觉的下一个逻辑步骤是图像分割。图像分割是一种标记技术,将给定的图像分割成多个片段(一组像素),以更精确地定义物体。图像分割和对象检测之间的区别在于,在图像分割下,在图像中识别的对象的定义更精确。也就是说,我们将拥有物体的实际像素轮廓,而不是像物体检测中那样的矩形边界框(见图 8-2 )。

img/478491_2_En_8_Fig2_HTML.jpg

图 8-2

计算机视觉中的图像分割图片来源-github . com/face book research/detectron 2

取代了边界框,我们现在有了更精细的轮廓来捕捉实际的物体。图像分割的实际应用包括交通监控、医疗成像、智能手机相机中的人像模式(数字模拟散景效果——识别人物以模糊背景)。

现代智能手机实现了语义图像分割——识别图像中的对象,并根据所识别的对象类型进一步处理它们。例如,一张脸将被处理以获得美感(平滑瑕疵/阴影等)。);添加模糊效果后,天空会变得不那么清晰;大自然将被彩色处理,以具有一种充满活力的感觉;诸如此类。

要了解更多关于语义图像分割的信息,请访问 https://developer.apple.com/videos/play/wwdc2019/225/

姿态估计

姿态估计是一种预测和跟踪人或物体位置的计算机视觉技术。本质上,姿态估计使用给定人/物体的姿态和方向的组合从图像或视频预测人的身体部分或关节位置。

姿势估计的更复杂版本——也是更难解决的计算机视觉问题——是多人姿势估计(见图 8-3 )。

img/478491_2_En_8_Fig3_HTML.jpg

图 8-3

多人姿势估计图片来源-github . com/face book research/detectron 2

姿态估计的实际应用类似于图像分割和对象检测,尽管姿态估计的应用更有意义和针对性——例如,跟踪人的活动,如跑步、骑自行车等。活动跟踪使安全监控更上一层楼。姿态估计的另一个重要应用涉及电影和增强现实领域。将人类的动作捕捉转换为三维图形角色,其中动作被精确捕捉和转换(称为 VFX 或 VFX),经常用于电影。

要了解更多关于姿态估计的信息,请访问 http://neuralvfx.com/tag/facial-pose-estimation/

生成计算机视觉

除了分类、对象检测、图像分割和姿态估计,我们在计算机视觉中还有另一个热门领域— 生成对抗网络 (GANs)。计算机视觉中的生成模型首先学习训练集的分布,然后生成一些具有小变化的新样本。这些新图像是由模型在监督设置中使用随机噪声和先前学习的模型权重合成生成的。图 8-4 为 GAN 模型生成的图像样本示例。

img/478491_2_En_8_Fig4_HTML.jpg

图 8-4

GAN 生成的样本图像

大多数图像看起来相当逼真,易于辨认,例如马、船、汽车等。训练 GANs 一直是个难题,通常需要大量的计算资源。制作更大尺寸的图像会进一步增加复杂性。然而,GANs 是近年来计算机视觉领域最大的发展之一。ACM 图灵感知奖获得者 Yann LeCun 将它们描述为“过去 10 年机器学习中最有趣的想法”。

gan 的实际应用是无限的。最简单的应用程序是基于文本描述渲染图像的产品。例如,键入“设计一个白天人比车多的繁忙街道的图像”将得到显示这些事情的图像。反之亦然——即,输入图像并接收关于该图像的基于文本的自然语言描述。科技公司百度设计了一种原型设备,它通过一个用自然语言描述周围环境的摄像头来帮助盲人。欲了解更多关于原型的信息,请访问 https://www.youtube.com/watch?v=Xe5RcJ1JY3c

一些新兴的电子商务企业正在利用 GANs 设计图形 t 恤。例如,流行的照片编辑应用 Prisma 和 FaceApp,一个有争议但直观的应用,可以将你现有的照片变成你更老或更年轻的自己,在 2019 年席卷了互联网。

Deepfake 视频现在(或即将)是互联网上的一个主要问题。Deepfakes 可以制作几乎真实的视频,让名人用真实的语音和手势讲述您的输入内容。

具有深度学习的自然语言处理

第七章讨论了循环神经网络(RNNs)和长短期记忆(LSTM)网络,它们可以用来解决现代自然语言处理(NLP)问题。序列模型在语音识别和自然语言处理中的相关任务中也非常有效。近年来,语音数字助理有了显著的进步,比如苹果的 Siri 和亚马逊的 Alexa。这些助手现在可以理解更多带有地域影响和各种口音的语言和讲话,并以非常真实的声音做出回应。他们也能理解并区分你的声音和其他人的声音,当然,准确性的问题仍然存在。在早期,这些改进是通过 LSTM 和门控循环单元(GRUs),另一种类似于 LSTM 的变体。

LSTM 和 GRU 模型仍然有局限性。它们在计算上非常昂贵,并且顺序地处理输入。长期依赖问题仍然存在,尽管这比普通的 RNN 要好得多。

变压器型号

2017 年底,谷歌发表了关于 Transformer network 的研究结果,这是 NLP 的一个开创性的深度学习模型。这篇名为“注意力是你所需要的全部”( https://arxiv.org/pdf/1706.03762.pdf )的论文揭示了语言模型研究领域的一个新转变。

有一段时间,rnn 是处理顺序数据的最佳选择。然而,顺序处理和相对较差的长期依赖性能给大型 NLP 任务带来了各种挑战。变压器网络在这类用例中发挥着至关重要的作用。变压器网络可以并行训练,从而大幅减少计算时间。它们基于自我关注机制,完全免除了递归和卷积(因此,计算速度更快)。

Transformer 模型在 WMT 2014 年英德翻译任务中获得了 28.4 的双语评估替角(BLEU)分数,比现有的最佳结果(包括合奏)提高了两个 BLEU 以上。

来自变压器的双向编码器表示

2018 年,在发布 Transformer networks 一年后,谷歌人工智能语言的研究人员从 Transformers (BERT)开源了一项名为双向编码器表示的 NLP 新技术。

BERT 依赖于一个变压器,但有一些变化。标准变压器由编码器和解码器架构组成;编码器读取文本输入,而解码器产生预测。然而,BERT 只利用编码器部分。因为 BERT 的目标是生成一个语言表示模型,所以这是理想的。BERT 的独特优势之一是其半监督设置。在这种情况下,该过程首先集中在预训练(无监督的),其中使用大量文本数据(主要通过互联网获得)来训练语言表示模型。接下来,针对感兴趣的特定用例,以受监督的方式对模型进行训练和微调。我们在第七章探索的情感分类用例就是一个例子。

你可以在 https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html 找到更多关于伯特的细节。

BERT 使用两种策略进行训练:掩蔽语言建模和下一句预测。对于屏蔽语言建模,在将单词序列输入 BERT 时,每个序列中大约有 15%的单词被替换为[MASK]标记。然后,该模型试图根据序列中其他未屏蔽单词提供的上下文来预测屏蔽单词的原始值。

AllenNLP 发布了一个在后台使用 BERT 的有趣工具(见 https://demo.allennlp.org/masked-lm )。图 8-5 展示了一个简单的演示;该模型以 72%的概率将[MASK]中的单词预测为 car。

img/478491_2_En_8_Fig5_HTML.jpg

图 8-5

allennlp 演示

格罗涅特

我们到目前为止所探讨的计算机视觉课题都与单任务学习有关。也就是说,我们专门设计了一个具有一个损失函数和一个期望结果的网络——将一幅图像分成 n 个不同的类别。数字时代的现代问题有更复杂的要求,需要更全面的方法。考虑一个电子商务市场。当用户上传一张图片来列出一个待售产品时,他们可能不会添加关于该产品的详细和全面的描述。在大多数情况下,上传者会为产品添加一行描述和一个宽泛的类别(这可能不完全正确)。

为了更好地理解这个问题,考虑一个椅子的样本产品列表:“出售一把结实的椅子,仅用了一年,状况像新的一样”这种用户起草的描述缺乏大量信息,而这些信息可能是买家做出更明智决策的理想信息。对买方(以及市场)来说理想的信息属性包括椅子的颜色、椅子的品牌和型号、制造年份等。从工程的角度来看,根据针对提要的基于用户的搜索查询对这样的产品列表进行排序将是一项困难的任务,因为它可能不匹配大多数相关的信息字段。

这个问题的解决方案是通过几个单独的计算机视觉任务来增加额外的信息,例如,一个将图像分类到一个大的类别(家具/工具/车辆/书籍等)的任务。),然后使用另一个型号在一个垂直方向(品牌和型号年份)内进行更具体的分类,依此类推。可能还会出现为每个垂直产品(例如,服装、家具、书籍等)提供个性化模型的需求。考虑到广泛的可能性,我们可能经常面临构建和维护数百个不同模型的挑战。

考虑到 Facebook Marketplace 是一个问题,该公司发布了 GrokNet ,这是一个单一、统一的模型,全面覆盖所有产品。借助统一的模型,该公司能够减少维护和计算成本,并通过消除对每个垂直应用程序的单独模型的需求来提高覆盖率。GrokNet 利用多任务学习方法来训练单个计算机视觉主干。该模型使用 80 个分类损失函数和 3 个嵌入损失,在几个商业垂直行业的 7 个不同数据集上进行训练。

最终模型对给定图像预测如下:

  • 对象类别:“吧台凳”、“围巾”、“地毯”等。

  • 家居属性:物品颜色、材质、装饰风格等。

  • 时尚属性:款式、颜色、材质、袖长等。

  • 车辆属性:品牌、型号、外观颜色、年代等。

  • 搜索查询:用户可能用来在市场搜索中找到产品的文本短语

  • 图片嵌入:一个 256 位的哈希码,用于识别确切的产品,查找相似的产品并进行排序,提高搜索质量

有了对给定图像的如此丰富的预测,给定用户的搜索结果的市场馈送可以用高度相关的结果来定制和定制。预测的图像嵌入可以进一步用于呈现相似的产品列表,使得用户可以做出更明智的决定。此外,整个增强任务是由单个模型而不是模型的集合来执行的。

欲了解更多关于 GrokNet 的信息,请访问 https://ai.facebook.com/research/publications/groknet-unified-computer-vision-model-trunk-and-embeddings-for-commerce/

其他值得注意的研究

本节描述了一些与深度学习领域相关的研究出版物,这些出版物非常令人兴奋,可供人们独立探索,作为该领域的下一步发展。讨论这项研究的任何细节都超出了本书的范围,因此鼓励读者独立探索以下研究论文:

  1. 点唱机:音乐的生成模型

    Jukebox 是一个神经网络,它生成音乐,包括基本的歌唱,作为各种流派和艺术家风格的原始音频。OpenAI 发布了模型的权重和代码,以及一个探索生成样本的工具。

    论文: https://arxiv.org/abs/2005.00341

    代码: https://github.com/openai/jukebox/

  2. 图像 GPT–生成相干图像完成

    经过语言训练的基于转换器的模型可以生成连贯的文本。在像素序列上训练的相同模型可以生成连贯的图像完成和样本。

    论文: https://cdn.openai.com/papers/Generative_Pretraining_from_Pixels_V2.pdf

    代码: https://github.com/openai/image-gpt

  3. 通用音乐翻译网

    一种基于深度学习的跨乐器和风格翻译音乐的方法。该技术基于多域波网自编码器的无监督训练,具有共享编码器和在波形上端到端训练的域独立潜在空间。

    论文: https://arxiv.org/abs/1805.07848

  4. 视频中的实时人脸去识别

    该方法使用前馈编码器-解码器网络架构,以人的面部图像的高级表示为条件,在全自动设置中以高帧速率对实况视频进行面部去识别。

    论文: https://arxiv.org/abs/1911.08348

总结想法

我们想感谢你,读者,感谢你花时间和兴趣通过阅读这本书来研究深度学习的主题。我们真诚地感谢您为这本书付出的努力,并希望我们能够满足您的期望。

深度学习的主题是如此广泛和动态,以至于人们需要进行持续的研究以跟上创新的步伐。我们对这本书的关注一直是传递关于这个主题的抽象而直观的信息的健康组合(用最少的数学运算;抱歉,如果方程是压倒性的),同时使用业界和学术界的领先工具(PyTorch)将急需的实际实现与真实数据集相结合。

我们将感谢您的想法和反馈!

posted @ 2024-10-01 21:02  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报