【Pytorch with fastai】第 4 章 :底层训练数字分类器

   🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

像素:计算机视觉的基础

停下来想一想!

第一次尝试:像素相似度

行话:基线

列出理解

停下来想一想!

忘记数学也没关系

NumPy 数组和 PyTorch 张量

使用广播计算指标

随机梯度下降

计算梯度

以学习率步进

端到端 SGD 示例

第一步:初始化参数

第 2 步:计算预测

第三步:计算损失

第 4 步:计算梯度

第 5 步:步权重

第 6 步:重复该过程

第 7 步:停止

总结梯度下降

MNIST 损失函数

行话:参数

阅读文档

Sigmoid

SGD 和小批量

把它们放在一起

就地操作

创建优化器

添加非线性

更深入

行话回顾


在 第 2 章中了解了训练各种模型的情况之后,现在让我们深入了解一下到底发生了什么。我们将首先使用计算机视觉来介绍深度学习的基本工具和概念。

确切地说,我们将讨论数组和张量的作用以及广播的作用,广播是一种用于表达地使用它们的强大技术。我们将解释随机梯度下降 (SGD),这是一种通过自动更新权重进行学习的机制。我们将讨论为我们的基本分类任务选择损失函数,以及小批量的作用。我们还将描述基本神经网络正在执行的数学运算。最后,我们将把所有这些部分放在一起。

在接下来的章节中,我们还将深入研究其他应用程序,并了解这些概念和工具如何推广。但这一章是关于奠定基石的。坦率地说,这也使这一章成为最难的章节之一,因为这些概念是如何相互依赖的。就像拱门一样,所有的石头都需要到位才能使结构保持不变。也像拱门一样,一旦发生,它就是一个可以支撑其他东西的强大结构。但组装需要一些耐心。

让我们开始。第一步是考虑图像在计算机中的表示方式。

像素:计算机视觉的基础

为了理解计算机视觉模型中发生了什么,我们首先 必须了解计算机如何处理图像。我们将使用计算机视觉中最著名的数据集之一 MNIST进行实验。MNIST 包含手写数字的图像,由 美国国家标准与技术研究所,并由 Yann Lecun 及其同事整理成机器学习数据集。Lecun 于 1998 年在LeNet-5中使用了 MNIST ,这是第一个展示对手写数字序列的实际有用识别的计算机系统。这是人工智能历史上最重要的突破之一。

坚韧与深度学习

深度学习的故事是少数敬业的研究人员坚韧不拔的故事之一。在早期的希望(和炒作!)之后,神经网络 在 1990 年代和 2000 年代失宠,只有少数研究人员一直在努力让它们运作良好。他们中的三人,Yann Lecun、Yoshua Bengio 和 Geoffrey Hinton,在 2018 年获得了计算机科学界的最高荣誉图灵奖(通常被认为是“计算机科学的诺贝尔奖”),尽管他们对此深表怀疑和不感兴趣,但他们还是获得了胜利。更广泛的机器学习和统计社区。

Hinton 曾讲述过,学术论文显示出比以前发表的任何论文都好得多的结果,会被顶级期刊和会议拒绝,仅仅因为它们使用了神经网络。Lecun 在卷积神经网络方面的工作,我们 将在下一节研究,表明这些模型可以阅读手写文本——这是以前从未实现过的。然而,大多数研究人员都忽略了他的突破,即使它在美国被商业用于读取 10% 的支票!

除了这三位图灵奖得主之外,许多其他研究人员也在努力让我们达到今天的水平。例如,Jurgen Schmidhuber(许多人认为他应该在 图灵奖)开创了许多重要的想法,包括与他的学生 Sepp Hochreiter 一起研究长短期记忆 (LSTM) 架构(广泛用于 语音识别和其他文本建模任务,并在第 1 章的 IMDb 示例中使用)。也许最重要的是,Paul Werbos 在 1974 年发明了神经网络的反向传播,即 本章展示的技术,普遍用于训练神经网络(Werbos 1994)。几十年来,他的发展几乎完全被忽视,但今天它被认为是现代人工智能最重要的基础。

这里给我们所有人上了一课!在您的深度学习之旅中,您将面临许多障碍,包括技术障碍和(甚至更难)由您周围不相信您会成功的人提出的障碍。有一种保证会失败的方法,那就是停止尝试。我们已经看到,每个成为世界级从业者的 fast.ai 学生唯一一致的特征是他们都非常顽强。

对于这个初始教程,我们将尝试创建一个可以将任何图像分类为 3 或 7 的模型。因此,让我们下载一个仅包含这些数字图像的 MNIST 示例:

path = untar_data(URLs.MNIST_SAMPLE) 

ls我们可以使用, a查看此目录中的内容 fastai添加的方法。此方法返回一个名为 的特殊 fastai 类的对象L,它具有 Python 内置的所有相同功能list,以及更多功能。它的一个方便的功能是,在打印时,它会在列出项目本身之前显示项目的数量(如果有超过 10 个项目,它只显示前几个):

path.ls()
(#9) [Path('cleaned.csv'),Path('item_list.txt'),Path('trained_model.pkl'),Path('
 > models'),Path('valid'),Path('labels.csv'),Path('export.pkl'),Path('history.cs
 > v'),Path('train')]

MNIST 数据集遵循机器学习数据集的通用布局:训练集和验证(和/或测试)集的单独文件夹。让我们看看训练集中的内容:

(path/'train').ls()
(#2) [Path('train/7'),Path('train/3')]

有一个3s的文件夹,一个7s的文件夹。在机器学习用语中,我们说“3”和“7”是这个数据集中的标签 (或目标)。让我们看一下其中一个文件夹(sorted用于确保我们都获得相同的文件顺序):

threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
threes
(#6131) [Path('train/3/10.png'),Path('train/3/10000.png'),Path('train/3/10011.pn
 > g'),Path('train/3/10031.png'),Path('train/3/10034.png'),Path('train/3/10042.p
 > ng'),Path('train/3/10052.png'),Path('train/3/1007.png'),Path('train/3/10074.p
 > of'),Path('train/3/10091.png')...]

正如我们所料,它充满了图像文件。让我们来一个 现在看一个。这是一个手写数字 3 的图像,取自著名的 MNIST 手写数字数据集:

im3_path = threes[1]
im3 = Image.open(im3_path)
im3

这里我们使用Python Imaging LibraryImage (PIL) 中的类 ,它是用于打开、操作和查看图像的最广泛使用的 Python 包。Jupyter 知道 PIL 图像,因此它会自动为我们显示图像。

在计算机中,一切都表示为一个数字。要查看构成此图像的数字,我们必须将其转换为NumPy 数组PyTorch 张量。例如,下面是图像的一部分转换为 NumPy 数组的样子:

array(im3)[4:10,4:10]
array([[  0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,  29],
       [  0,   0,   0,  48, 166, 224],
       [  0,  93, 244, 249, 253, 187],
       [  0, 107, 253, 253, 230,  48],
       [  0,   3,  20,  20,  15,   0]], dtype=uint8)

4:10表示我们请求了从索引 4(包括)到 10(不包括)的行,对于列也是如此。NumPy 索引从上到下和从左到右,因此这部分位于图像的左上角附近。这与 PyTorch 张量相同:

tensor(im3)[4:10,4:10]
tensor([[  0,   0,   0,   0,   0,   0],
        [  0,   0,   0,   0,   0,  29],
        [  0,   0,   0,  48, 166, 224],
        [  0,  93, 244, 249, 253, 187],
        [  0, 107, 253, 253, 230,  48],
        [  0,   3,  20,  20,  15,   0]], dtype=torch.uint8)

我们可以对数组进行切片以仅选择其中包含数字顶部的部分,然后使用 Pandas DataFrame 使用 渐变,它清楚地向我们展示了图像是如何从像素值创建的:

im3_t = tensor(im3)
df = pd.DataFrame(im3_t[4:15,4:22])
df.style.set_properties(**{'font-size':'6pt'}).background_gradient('Greys')

您可以看到背景白色像素存储为数字 0,黑色是数字 255,灰色阴影介于两者之间。整个图像包含 28 个像素和 28 个像素,总共 784 个像素。(这比你从手机摄像头获得的图像要小得多,它有数百万像素,但对于我们最初的学习和实验来说是一个方便的尺寸。我们将很快建立更大的全彩色图像。)

所以,现在您已经看到了图像在计算机中的样子,让我们回顾一下我们的目标:创建一个可以识别 3 和 7 的模型。你怎么能得到一台电脑来做到这一点?

停下来想一想!

在继续阅读之前,请花点时间思考一下计算机如何能够识别这两个数字。它可以查看哪些类型的功能?它如何能够识别这些特征?怎么可能把它们结合起来?当您尝试自己解决问题而不是仅仅阅读别人的答案时,学习效果最好;所以离开这本书几分钟,拿起一张纸和笔,记下一些想法。

第一次尝试:像素相似度

所以,这是第一个想法:我们如何找到平均像素值 3s 的每个像素,然后对 7s 做同样的事情。这将给我们两个组平均值,定义我们可能称之为“理想”的 3 和 7。然后,为了将图像分类为一个数字,我们会看到图像与这两个理想数字中的哪一个最相似。这当然看起来总比没有好,所以它将成为一个很好的基线。

行话:基线

您有信心的简单模型应该可以合理执行出色地。它应该易于实现且易于测试,这样您就可以测试您改进的每个想法,并确保它们始终优于您的基准。如果没有一个合理的基线,就很难知道你的超花哨模型是否好用。创建基线的一个好方法是做我们在这里所做的:考虑一个简单、易于实现的模型。另一个好方法是四处搜索以找到解决了与您类似的问题的其他人,然后下载并在您的数据集上运行他们的代码。理想情况下,尝试这两个!

我们简单模型的第 1 步是获取两组像素值的平均值。在这样做的过程中,我们会学到很多巧妙的 Python 数值编程技巧!

让我们创建一个包含所有 3 堆叠的张量 一起。我们已经知道如何创建一个包含单个图像的张量。要创建一个包含目录中所有图像的张量,我们将首先使用 Python 列表推导来创建单个图像张量的普通列表。

在此过程中,我们将使用 Jupyter 对我们的工作进行一些小检查——在这种情况下,确保返回项目的数量看起来合理:

seven_tensors = [tensor(Image.open(o)) for o in sevens]
three_tensors = [tensor(Image.open(o)) for o in threes]
len(three_tensors),len(seven_tensors)
(6131, 6265)

列出理解

列表和字典推导是 Python 的一个很棒的特性。许多 Python 程序员每天都在使用它们,包括本书的作者——它们是“惯用 Python”的一部分。但是来自其他语言的程序员可能以前从未见过它们。很多很棒的教程只是一个网络搜索而已,所以我们现在不会花很长时间讨论它们。这是一个快速解释和示例,可帮助您入门。列表理解看起来像这样:new_list = [f(o) for o in a_list if o>0]a_list在将其传递给函数后,这将返回大于 0的每个元素f。这里分为三个部分:您正在迭代的集合 ( a_list)、一个可选过滤器 ( if o>0) 以及对每个元素执行的操作 ( f(o))。它不仅写起来更短,而且比使用循环创建相同列表的替代方法更快。

我们还将检查其中一张图像是否正常。由于我们现在有张量(默认情况下 Jupyter 将打印为值),而不是 PIL 图像(默认情况下 Jupyter 将显示图像),我们需要使用 fastai 的show_image函数来显示它:

show_image(three_tensors[1]);

对于每个像素位置,我们想要计算该像素强度的所有图像的平均值。为此,我们首先将列表中的所有图像组合成一个 3 维张量。描述这种张量的最常见方法是将其称为rank-3 张量。我们经常需要将集合中的单个张量堆叠成单个张量。不出所料,PyTorch 带有一个stack我们可以用于此目的的函数。

PyTorch 中的某些操作,例如取平均值,需要我们将整数 类型转换为浮点类型。由于我们稍后会需要它,所以我们也将我们的堆叠张量投射到float 现在。在 PyTorch 中进行铸造就像写下你想要的类型的名称一样简单 希望转换为,并将其视为一种方法。

一般来说,当图像是浮点数时,像素值预计在 0 和 1 之间,所以我们这里也将除以 255:

stacked_sevens = torch.stack(seven_tensors).float()/255
stacked_threes = torch.stack(three_tensors).float()/255
stacked_threes.shape
torch.Size([6131, 28, 28])

张量最重要的属性也许是它的形状。这个 告诉你每个轴的长度。在这种情况下,我们可以看到我们有 6,131 张图像,每张图像的大小为 28×28 像素。这个张量并没有具体说明第一个轴是图像的数量,第二个是高度,第三个是宽度——张量的语义完全取决于我们,以及我们如何构造它。就 PyTorch 而言,它只是内存中的一堆数字。

张量形状的长度是它的秩:

len(stacked_threes.shape)
3

记住并练习这些张量术语对您来说非常重要:等级是张量中的轴数或维度数;shape是张量每个轴的大小。

请注意,因为“维度”一词有时用于两个方法。考虑到我们生活在“三维空间”中,其中物理位置可以用v长度为 3 的向量 来描述。但根据 PyTorch,属性v.ndim(看起来确实像 的“维数” v)等于 1,不是三个!为什么?因为v是一个向量,它是一个秩为 1 的张量,这意味着它只有一个(即使该轴的长度为 3)。换句话说,有时维度用于表示轴的大小(“空间是三维的”),而其他时候则用于表示秩或轴的数量(“矩阵具有二维”)。当感到困惑时,我发现将所有陈述翻译成等级、轴和长度的术语很有帮助,这些术语是明确的术语。

我们也可以直接得到张量的秩ndim

stacked_threes.ndim
3

最后,我们可以计算出理想 3 的样子。我们通过沿堆叠的 rank-3 张量的 0 维取平均值来计算所有图像张量的平均值。这是索引所有图像的维度。

换句话说,对于每个像素位置,这将计算该像素在所有图像上的平均值。结果将是每个像素位置的一个值,或单个图像。这里是:

mean3 = stacked_threes.mean(0)
show_image(mean3);

根据这个数据集,这是理想的数字 3!(您可能不喜欢它,但这就是 3 号峰值性能的样子。)您可以看到在所有图像都同意它应该是黑暗的地方它非常黑暗,但在图像不同意的地方它变得纤细和模糊。

让我们对 7s 做同样的事情,但将所有步骤放在一起以节省时间:

mean7 = stacked_sevens.mean(0)
show_image(mean7);

 现在让我们选择一个任意的 3 并测量它 与我们的“理想数字”的距离。

停下来想一想!

您如何计算特定图像与我们每个理想数字的相似程度?在继续之前,请记住离开这本书并记下一些想法!研究表明,当您通过自己解决问题、实验和尝试新想法来参与学习过程时,回忆和理解能力会显着提高。

这是一个示例 3:

a_3 = stacked_threes[1]
show_image(a_3);

 我们如何确定它与理想 3 的距离?我们不能只是将这张图片的像素与理想数字之间的差异相加。一些差异是正的,而另一些是负的,这些差异将抵消,从而导致图像在某些地方太暗而在其他地方太亮的图像可能显示为与理想的总差异为零。那会 误导!

为了避免这种情况,数据科学家在这种情况下使用两种主要方法来测量距离:

  • 取差值绝对值的平均值绝对值是用正值代替负值的函数)。这称为平均绝对差L1 范数

  • 取差异平方的平均值(这使得一切都为正),然后取平方根(取消平方)。这称为均方根误差(RMSE) 或L2 norm

忘记数学也没关系

在本书中,我们通常假设你已经完成了高中数学,并且至少记住了一些——但每个人都忘记了一些事情!这一切都取决于你在此期间碰巧有理由练习什么。也许你已经忘记了平方根是什么,或者它们是如何工作的。没问题!任何时候你遇到本书没有完全解释的数学概念,不要只是继续前进;相反,停下来查一下。确保您了解基本概念、它的工作原理以及我们可能使用它的原因。刷新你的最佳场所之一认识的是可汗学院。例如,可汗学院对平方根有很好的介绍

现在让我们尝试这两种方法:

dist_3_abs = (a_3 - mean3).abs().mean()
dist_3_sqr = ((a_3 - mean3)**2).mean().sqrt()
dist_3_abs,dist_3_sqr
(tensor(0.1114), tensor(0.2021))
dist_7_abs = (a_3 - mean7).abs().mean()
dist_7_sqr = ((a_3 - mean7)**2).mean().sqrt()
dist_7_abs,dist_7_sqr
(tensor(0.1586), tensor(0.3021))

在这两种情况下,我们的 3 和“理想” 3 之间的距离都小于到理想 7 的距离,因此我们的简单模型在这种情况下将给出正确的预测。

PyTorch 已经提供了这两种损失函数 你会在里面找到这些torch.nn.functional,PyTorch 团队建议将其导入为F(并且默认情况下在 fastai 中以该名称可用):

F.l1_loss(a_3.float(),mean7), F.mse_loss(a_3,mean7).sqrt()
(tensor(0.1586), tensor(0.3021))

这里,MSE代表均方误差 并l1指的是平均绝对值的标准数学术语(在数学中称为L1 norm)。

直观地说,L1 范数和均方误差 (MSE) 之间的区别在于,后者会比前者更严厉地惩罚更大的错误(并且对小错误更宽容)。

当我第一次遇到这个 L1 的东西时,我查了一下它到底是什么意思。我在谷歌上发现它是一个使用绝对值的向量范数,所以我查找了“向量范数”并开始阅读:给定一个向量空间 V 在实数或复数的域 F 上,V 上的范数是一个非负的-对任何函数 p 赋值:V → \[0,+∞) 具有以下性质:对于所有 a ∈ F 和所有 u,v ∈ V,p(u + v) ≤ p(u) + p(v)...那么我停止阅读。“呃,我永远不会懂数学!” 我想,第一千次。从那以后,我了解到,每次在实践中出现这些复杂的数学术语时,我都可以用一点点代码来替换它们!就像,L1 损失正好等于,其中(a-b).abs().mean()a并且b是张量。我想数学家的想法和我的想法不同……我会确保在这本书中,每次出现一些数学术语时,我都会给你一些与之相同的代码,并用常识进行解释条款是怎么回事。

我们刚刚完成了对 PyTorch 张量的各种数学运算。如果您之前在 NumPy 中进行过数值编程,您可能会认为这些类似于 NumPy 数组。让我们看一下这两个重要的数据结构。

NumPy 数组和 PyTorch 张量

NumPy是最广泛使用的科学库 和 Python 中的数字编程。它提供了与 PyTorch 提供的类似的功能和类似的 API;但是,它不支持使用 GPU 或计算梯度,这对深度学习都很重要。因此,在本书中,我们通常会尽可能使用 PyTorch 张量而不是 NumPy 数组。

(请注意,fastai 为 NumPy 和 PyTorch 添加了一些功能,以使它们彼此更加相似。如果本书中的任何代码在您的计算机上无法运行,则可能是您忘记在开头包含这样的一行你的笔记本: from fastai.vision.all import *.)

但是什么是数组和张量,你为什么要关心?

与许多语言相比,Python 很慢。Python、NumPy 或 PyTorch 中任何快速的东西都可能是用另一种语言(特别是 C)编写(和优化)的编译对象的包装器。事实上,NumPy 数组和 PyTorch 张量完成计算的速度比使用纯 Python

NumPy 数组是一个多维数据表,所有项目都属于同一类型。因为它可以是任何类型,它们甚至可以是数组的数组,最里面的数组可能有不同的大小——这被称为锯齿状数组。通过“多维表”,我们 例如,表示列表(一维)、表格或矩阵(二维)、表格或立方体(三维)等。如果项目都是简单类型,例如整数或浮点数,NumPy 会将它们作为紧凑的 C 数据结构存储在内存中。这就是 NumPy 大放异彩的地方。NumPy 有各种各样的运算符和方法,可以以与优化的 C 相同的速度在这些紧凑结构上运行计算,因为它们是用优化的 C 编写的。

PyTorch 张量与 NumPy 数组几乎相同,但有一个额外的限制,可以解锁额外的功能。相同之处在于它也是一个多维数据表,所有项目都属于同一类型。然而,限制是张量不能只使用任何旧类型——它必须对所有分量使用单一的基本数字类型。因此,张量不像真正的数组那样灵活。例如,PyTorch 张量不能是锯齿状的。它始终是一个规则形状的多维矩形结构。

PyTorch 也支持 NumPy 在这些结构上支持的绝大多数方法和运算符,但 PyTorch 张量具有额外的功能。一项主要功能是这些结构可以存在于 GPU 上,在这种情况下,它们的计算将得到优化 对于 GPU 并且可以运行得更快(考虑到很多值要处理)。此外,PyTorch 可以自动计算这些操作的导数,包括操作的组合。正如您将看到的,如果没有这种能力,就不可能在实践中进行深度学习。

如果您不知道 C 是什么,请不要担心:您根本不需要它。简而言之,它是一种低级(低级意味着更类似于计算机内部使用的语言) 语言,与 Python 相比速度非常快。为了在使用 Python 编程时利用它的速度,请尽量避免编写循环,并用直接在数组或张量上工作的命令替换它们。

对于 Python 程序员来说,这可能是最重要的新编码技能 学习如何有效地使用数组/张量 API。我们将在本书后面展示更多技巧,但这里是您现在需要了解的关键内容的摘要。

要创建数组或张量,请将列表(或列表列表,或列表列表等)传递给arrayor tensor

data = [[1,2,3],[4,5,6]]
arr = array (data)
tns = tensor(data)
arr # numpy
array([[1, 2, 3],
       [4, 5, 6]])
tns # pytorch
tensor([[1, 2, 3],
        [4, 5, 6]])

随后的所有操作都显示在张量上,但 NumPy 数组的语法和结果是相同的。

您可以选择一行(请注意,与 Python 中的列表一样,张量是 0 索引的,因此 1 指的是第二行/列):

tns[1]
tensor([4, 5, 6])

或者一列,通过使用:来表示所有的第一个轴(我们有时将张量/数组的维度称为):

tns[:,1]
tensor([2, 5])

您可以将这些与 Python 切片语法[start:end](被排除在外)结合end使用来选择行或列的一部分:

tns[1,1:3]
tensor([5, 6])

您可以使用标准运算符,例如+-*/

tns+1
tensor([[2, 3, 4],
        [5, 6, 7]])

张量有一个类型:

tns.type()
'torch.LongTensor'

并且会根据需要自动更改该类型;例如,从intfloat

tns*1.5
tensor([[1.5000, 3.0000, 4.5000],
        [6.0000, 7.5000, 9.0000]])

那么,我们的基线模型好吗?为了量化这一点,我们必须定义一个指标。

使用广播计算指标

回想一下,指标是一个根据 预测我们的模型和我们数据集中的正确标签,以告诉我们我们的模型有多好。例如,我们可以使用我们在上一节中看到的任何一个函数,均方误差或平均绝对误差,并在整个数据集上取它们的平均值。然而,这些数字对于大多数人来说都不是很容易理解的。在实践中,我们通常使用准确率 作为分类模型的指标。

正如我们所讨论的,我们想要计算我们的指标 在验证集上。这样我们就不会无意中过度拟合——也就是说,训练一个模型只在我们的训练数据上运行良好。对于我们在这里作为第一次尝试使用的像素相似性模型来说,这并不是真正的风险,因为它没有经过训练的组件,但无论如何我们都会使用验证集来遵循正常做法并为以后的第二次尝试做好准备.

为了得到一个验证集,我们需要从训练中完全删除一些数据,所以模型根本看不到它。事实证明,MNIST 数据集的创建者已经为我们完成了这项工作。你 还记得有一个名为valid的独立目录吗?这就是这个目录的用途!

因此,首先,让我们从该目录为我们的 3s 和 7s 创建张量。这些是我们将用来计算衡量我们第一次尝试模型质量的指标的张量,该模型衡量与理想图像的距离:

valid_3_tens = torch.stack([tensor(Image.open(o))
                            for o in (path/'valid'/'3').ls()])
valid_3_tens = valid_3_tens.float()/255
valid_7_tens = torch.stack([tensor(Image.open(o))
                            for o in (path/'valid'/'7').ls()])
valid_7_tens = valid_7_tens.float()/255
valid_3_tens.shape,valid_7_tens.shape
(torch.Size([1010, 28, 28]), torch.Size([1028, 28, 28]))

养成边走边检查形状的习惯是件好事。这里我们看到两个张量,一个代表 1,010 张 28×28 图像的 3s 验证集,一个代表 1,028 张 28×28 图像的 7s 验证集。

我们最终想要编写一个函数,is_3来决定任意图像是 3 还是 7。它将通过确定任意图像更接近我们的两个“理想数字”中的哪一个来做到这一点。为此,我们需要定义距离的概念——即计算两个图像之间距离的函数。

我们可以编写一个简单的函数,使用与我们在上一节中编写的表达式非常相似的表达式来计算平均绝对误差:

def mnist_distance(a,b): return (a-b).abs().mean((-1,-2))
mnist_distance(a_3, mean3)
tensor(0.1114)

mean3这与我们之前为这两个图像(理想 3和任意样本 3 )之间的距离计算的值相同a_3,它们都是形状为 的单图像张量 [28,28]

但是要计算整体准确度的指标,我们需要计算验证集中每个图像到理想 3 的距离。我们如何进行计算?我们可以对堆叠在我们的验证集张量中的所有单张图像张量编写一个循环valid_3_tens,它的形状[1010,28,28] 代表 1,010 张图像。但是有一个更好的方法。

当我们采用这个完全相同的距离函数时会发生一些有趣的事情,该函数旨在比较两个单张图像,但作为参数传入valid_3_tens,表示 3s 验证集的张量:

valid_3_dist = mnist_distance(valid_3_tens, mean3)
valid_3_dist, valid_3_dist.shape
(tensor([0.1050, 0.1526, 0.1186,  ..., 0.1122, 0.1170, 0.1086]),
 torch.Size([1010]))

它没有抱怨形状不匹配,而是将每个图像的距离作为长度为 1,010(我们的验证集中 3 的数量)的向量(即 rank-1 张量)返回。那是怎么发生的?

再看看我们的函数mnist_distance,然后 你会看到我们有减法(a-b)。神奇的技巧是,当 PyTorch 尝试在两个不同等级的张量之间执行简单的减法运算时,将使用 广播:它会自动扩展具有较小等级的张量,使其与具有较大等级的张量具有相同的大小。广播是一项重要的功能,它使张量代码更容易编写。

广播后,两个参数张量具有相同的等级,PyTorch 将其通常的逻辑应用于相同等级的两个张量:它对两个张量的每个对应元素执行操作,并返回张量结果。例如:

tensor([1,2,3]) + tensor(1)
tensor([2, 3, 4])

因此,在这种情况下,PyTorch 将mean3表示单个图像的 rank-2 张量视为同一图像的 1,010 个副本,然后从我们验证集中的每 3 个副本中减去每个副本。你希望这个张量有什么形状?在查看此处的答案之前,请尝试自己弄清楚:

(valid_3_tens-mean3).shape
torch.Size([1010, 28, 28])

我们正在计算我们的理想 3 与验证集中的 1,010 个 3 中的每一个之间的差异,对于 28×28 图像中的每一个,得到形状[1010,28,28]

关于广播的方式有几个要点 实施,这使得它不仅对表现力而且对性能都很有价值:

  • PyTorch实际上并没有复制mean31,010 次。它假装它是那种形状的张量,但不分配任何额外的内存。

  • 它在 C 中完成整个计算(或者,如果您使用的是 GPU,则在 CUDA 中,相当于 GPU 上的 C),比纯 Python 快数万倍(在 GPU 上快数百万倍! )。

在 PyTorch 中完成的所有广播和元素操作和函数都是如此。这是最重要的 让您了解创建高效 PyTorch 代码的技术。

接下来在mnist_distance我们看到abs。您现在可能能够猜到这在应用于张量时会发生什么。它将方法应用于张量中的每个单独元素,并返回结果的张量(即,它应用方法elementwise)。所以在这种情况下,我们将得到 1,010 个绝对值矩阵。

最后,我们的函数调用mean((-1,-2)). 元组(-1,-2) 表示一系列轴。在 Python 中,-1指的是最后一个元素,并且-2指的是倒数第二个元素。所以在这种情况下,这告诉 PyTorch 我们想要取范围内由张量的最后两个轴索引的值的平均值。最后两个轴是图像的水平和垂直尺寸。在最后两个轴上取平均值后,我们只剩下第一个张量轴,它对我们的图像进行索引,这就是我们最终大小为(1010). 换句话说,对于每张图像,我们平均了该图像中所有像素的强度。

我们将在本书中学习更多关于广播的知识,尤其是在第 17 章中,并且还会定期练习。

我们可以使用mnist_distance以下逻辑来判断图像是否为 3:如果所讨论的数字与理想​​ 3 之间的距离小于与理想 7 的距离,则它是 3。此函数将自动执行广播并按元素应用,就像所有 PyTorch 函数和运算符一样:

def is_3(x): return mnist_distance(x,mean3) < mnist_distance(x,mean7)

让我们在示例案例中对其进行测试:

is_3(a_3), is_3(a_3).float()
(tensor(True), tensor(1.))

请注意,当我们将布尔响应转换为浮点数时,我们会得到 1.0forTrue0.0for False

感谢广播,我们还可以在完整的 3s 验证集上对其进行测试:

is_3(valid_3_tens)
tensor([True, True, True,  ..., True, True, True])

现在我们可以计算每个 3 和 7 的准确度,方法是取所有 3 的函数的平均值以及所有 7 的反函数:

accuracy_3s =      is_3(valid_3_tens).float() .mean()
accuracy_7s = (1 - is_3(valid_7_tens).float()).mean()

accuracy_3s,accuracy_7s,(accuracy_3s+accuracy_7s)/2
(tensor(0.9168), tensor(0.9854), tensor(0.9511))

这看起来是一个不错的开始!我们在 3s 和 7s 上都获得了超过 90% 的准确率,并且我们已经了解了如何使用广播方便地定义指标。但说实话:3s 和 7s 是非常不同的数字。到目前为止,我们只对 10 个可能的数字中的 2 个进行分类。所以我们需要做得更好!

为了做得更好,也许是时候尝试一个真正的系统了 学习——可以自动修改自身以提高其性能。换句话说,是时候谈谈训练过程和SGD了。

随机梯度下降

你还记得我们在第 1 章中引用的 Arthur Samuel 描述机器学习的方式吗?

假设我们安排了一些自动方法来测试任何当前权重分配在实际性能方面的有效性,并提供一种改变权重分配的机制以最大化性能。我们不需要深入了解这样一个程序的细节,就可以看到它可以完全自动化,并看到这样编程的机器会从它的经验中“学习”。

正如我们所讨论的,这是让我们拥有一个可以变得越来越好——可以学习的模型的关键。但是我们的像素相似性方法并没有真正做到这一点。我们没有任何类型的权重分配,或任何基于测试权重分配有效性的改进方式。换句话说,我们不能通过修改一组参数来真正改进我们的像素相似度方法。为了利用深度学习的力量,我们首先必须以 Samuel 描述的方式来表示我们的任务。

与其试图找到图像和“理想图像”之间的相似性,我们可以查看每个单独的像素并为每个像素提出一组权重,以便最高权重与最有可能的像素相关联特定类别的黑色。例如,右下角的像素不太可能被 7 激活,因此它们对于 7 应该具有较低的权重,但它们很可能会被 3 激活,因此它们应该具有较高的权重3. 这可以表示为每个可能类别的一个函数和一组权重值——例如,成为数字 3 的概率:

def pr_three(x, w): return (x*w).sum()

在这里,我们假设这x是图像,表示为一个向量——换句话说,所有的行首尾相连地堆叠成一条长线。我们假设权重是一个向量w。如果我们有这个功能,我们只需要一些方法来更新权重以使它们更好一点。使用这种方法,我们可以多次重复该步骤,使权重越来越好,直到它们达到我们所能达到的水平。

我们希望找到向量的特定值,这些值w导致我们的函数结果对于那些为 3 的图像来说是高的,而对于那些不是 3 的图像来说是低的。寻找最佳向量w是寻找识别3s的最佳函数的一种方式。(因为我们还没有使用深度神经网络,我们受限于我们的函数可以做什么——我们将在本章后面解决这个限制。)

更具体地说,以下是将这个函数变成机器学习分类器所需的步骤:

  1. 初始化权重。

  2. 对于每张图像,使用这些权重来预测它看起来是 3 还是 7。

  3. 基于这些预测,计算模型的好坏(它的损失)。

  4. 计算梯度,它衡量每个权重的变化如何改变损失。

  5. 根据该计算逐步(即更改)所有权重。

  6. 返回第 2 步并重复该过程。

  7. 迭代直到您决定停止训练过程(例如,因为模型足够好或者您不想再等待)。

这七个步骤,如图 4-1 所示,是所有深度学习模型训练的关键。事实证明,深度学习完全依赖于这些步骤,这非常令人惊讶和违反直觉。令人惊奇的是,这个过程可以解决如此复杂的问题。但是,正如您将看到的,它确实如此!

图 4-1。梯度下降过程

这七个步骤中的每一个都有很多方法,我们将在本书的其余部分中学习它们。这些是对深度学习从业者产生重大影响的细节,但事实证明,每个细节的一般方法都遵循一些基本原则。以下是一些指导方针:

Initialize

我们将参数初始化为随机值。这听起来可能令人惊讶。我们当然可以做出其他选择,例如将它们初始化为该类别的像素被激活的次数百分比——但由于我们已经知道我们有一个改进这些权重的例程,结果证明只是从随机权重开始效果很好。

Loss

这就是塞缪尔在谈到根据实际表现测试任何当前重量分配的有效性时所指的。如果模型的性能良好,我们需要一个函数来返回一个很小的数字(标准方法是将小损失视为好,将大损失视为坏,尽管这只是一种约定)。

Step

确定体重应该增加还是减少的一个简单方法就是尝试一下:稍微增加重量,看看损失是增加还是减少。一旦你找到了正确的方向,你就可以多或少地改变这个数量,直到你找到一个合适的数量。但是,这很慢!正如我们将看到的,微积分的魔力使我们能够直接计算出在哪个方向以及大致多少来改变每个重量,而无需尝试所有这些小的改变。做到这一点的方法是计算梯度。这只是性能优化;通过使用较慢的手动过程,我们也会得到完全相同的结果。

Stop

一旦我们决定了训练模型的 epoch 数(在前面的列表中给出了一些建议),我们就会应用这个决定。对于我们的数字分类器,我们会继续训练,直到模型的准确性开始变差,或者我们的时间用完了。

在将这些步骤应用到我们的图像分类问题之前,让我们用一个更简单的例子来说明它们的样子。首先,我们将定义一个非常简单的函数,即二次函数——假设这是我们的损失函数,并且x是函数的权重参数:

def f(x): return x**2

这是该函数的图表:

plot_function(f, 'x', 'x**2')

我们之前描述的步骤序列首先为参数选择一个随机值,然后计算损失值:

plot_function(f, 'x', 'x**2')
plt.scatter(-1.5, f(-1.5), color='red');

现在我们看看如果我们稍微增加或减少我们的参数会发生什么——调整。这只是特定点的斜率:

我们可以在斜坡的方向稍微改变我们的权重,再次计算我们的损失和调整,重复几次。最终,我们将到达曲线的最低点:

这个基本思想可以追溯到艾萨克牛顿,他指出我们可以通过这种方式优化任意函数。无论我们的函数变得多么复杂,这种梯度下降的基本方法都不会发生显着变化。我们将在本书后面看到的唯一细微变化是一些方便的方法,我们可以通过找到更好的步骤来加快速度。

计算梯度

一个神奇的步骤是我们计算梯度的地方。正如我们 提到,我们使用微积分作为性能优化;当我们向上或向下调整参数时,它可以让我们更快地计算出我们的损失是上升还是下降。换句话说,梯度会告诉我们必须改变多少权重才能使我们的模型更好。

你可能记得在你的高中微积分课上, 函数的导数告诉您其参数的变化将改变其结果的程度。如果没有,请不要担心;一旦高中毕业,我们很多人就会忘记微积分!但是在继续之前,您需要对什么是导数有一些直观的了解,因此,如果您对这一切都非常模糊,请前往可汗学院并完成有关基本导数的课程。您不必知道如何自己计算它们;你只需要知道什么是导数。

关于导数的关键点是:对于任何函数,例如我们在上一节中看到的二次函数,我们都可以计算它的导数。导数是另一个函数。它计算变化,而不是值。例如,二次函数在值为 3 处的导数告诉我们函数在值为 3 处变化的速度。更具体地说,您可能还记得梯度被定义为上升/运行;也就是值的变化 函数,除以参数值的变化。当我们知道我们的功能将如何变化时,我们就知道我们需要做些什么来使它更小。这是机器学习的关键:有办法 更改函数的参数以使其更小。微积分为我们提供了一种计算捷径,即导数,它可以让我们直接计算函数的梯度。

需要注意的一件重要事情是,我们的函数有很多需要调整的权重,所以当我们计算导数时,我们不会得到一个数字,而是很多——每个权重的梯度。但是这里没有什么数学上的棘手问题。您可以计算关于一个重量的导数并将所有其他重量视为常数,然后对其他重量重复此操作。这就是为每个权重计算所有梯度的方式。

我们刚才提到您不必自己计算任何梯度。这个怎么可能?令人惊讶的是,PyTorch 能够自动计算几乎任何函数的导数!更重要的是,它的速度非常快。大多数情况下,它至少与您可以手动创建的任何衍生函数一样快。让我们看一个例子。

首先,让我们选择一个我们想要梯度的张量值:

xt = tensor(3.).requires_grad_()

注意特殊方法requires_grad_?这就是我们用来告诉 PyTorch 我们想要计算相对于该变量在该值处的梯度的神奇咒语。它本质上是标记变量,因此 PyTorch 将记住如何计算您将要求的其他直接计算的梯度。

如果你来自数学或物理学,这个 API 可能会让你失望。在这些情况下,函数的“梯度”只是另一个函数(即,它的派生函数),因此您可能期望与梯度相关的 API 为您提供一个新函数。但在深度学习中,“梯度”通常是指函数在特定参数值处的导数值PyTorch API 也将重点放在参数上,而不是您实际计算梯度的函数。一开始可能会觉得落后,但这只是一个不同的视角。

现在我们用那个值计算我们的函数。请注意 PyTorch 如何不仅打印计算的值,而且还注意到它有一个梯度函数,它将在需要时用于计算我们的梯度:

yt = f(xt)yt
tensor(9., grad_fn=<PowBackward0>)

最后,我们告诉 PyTorch 为我们计算梯度:

yt.backward()

这里的“backward”指的是反向传播,也就是名字 给出计算每一层的导数的过程。我们将在第 17 章看到这是如何完成的,那时我们从头开始计算深度神经网络的梯度。这被称为网络的后向传递,与前向传递相反,前向传递是计算激活的地方。生活会backward如果只是被称为 可能会更容易calculate_grad,但是深度学习的人真的很喜欢在他们可以添加的任何地方添加行话!

我们现在可以通过检查grad张量的属性来查看梯度:

xt.grad
tensor(6.)

如果你还记得你的高中微积分规则,那么 的导数 x**22*x,我们有x=3,所以梯度应该是2*3=6,这就是 PyTorch 为我们计算的!

现在我们将重复前面的步骤,但为我们的函数添加一个向量参数:

xt = tensor([3.,4.,10.]).requires_grad_()
xt
tensor([ 3.,  4., 10.], requires_grad=True)

我们将添加sum到我们的函数中,以便它可以接受一个向量(即,秩为 1 的张量)并返回一个标量(即,秩为 0 的张量):

def f(x): return (x**2).sum()

yt = f(xt)
yt
tensor(125., grad_fn=<SumBackward0>)

2*xt正如我们所期望的,我们的梯度是!

yt.backward()xt.grad
tensor([ 6.,  8., 20.])

梯度只告诉我们函数的斜率;他们没有告诉我们调整参数的确切程度。但它们确实让我们了解了多远:如果斜率非常大,这可能表明我们需要进行更多调整,而如果斜率非常小,则可能表明我们接近最优值。

以学习率步进

决定如何根据梯度值改变我们的参数是深度学习过程的重要组成部分。差不多全部 方法从将梯度乘以一些小的数字(称为学习率(LR))的基本思想开始。学习率通常是介于 0.001 和 0.1 之间的数字,尽管它可以是任何值。人们通常通过尝试几个来选择学习率,并在训练后找到最佳模型(我们将在本书后面向您展示一种更好的方法,称为学习率查找器)。一旦你选择了一个学习率,你可以使用这个简单的函数来调整你的参数:

w -= w.grad * lr

这称为步进参数,使用优化步骤

如果您选择的学习率太低,则可能意味着必须执行很多步骤。图 4-2说明了这一点。

图 4-2。低 LR 梯度下降

但是选择一个太高的学习率会更糟——它会导致损失变得更糟, 如图 4-3 所示

图 4-3。具有高 LR 的梯度下降

如果学习率太高,它也可能会“反弹”,而不是发散;图 4-4显示了这如何导致采取许多步骤来成功训练。

图 4-4。带弹性 LR 的梯度下降

现在让我们在一个端到端的例子中应用所有这些。

端到端 SGD 示例

我们已经看到了如何使用梯度来最小化我们的损失。现在 是时候查看一个 SGD 示例,看看如何使用找到最小值来训练模型以 更好地拟合数据。

让我们从一个简单的综合示例模型开始。想象一下,当过山车越过驼峰时,您正在测量它的速度。它会开始很快,然后随着上山而变慢;它在顶部会最慢,然后在下坡时会再次加速。您想建立一个速度如何随时间变化的模型。如果您每秒手动测量速度 20 秒,它可能看起来像这样:

time = torch.arange(0,20).float(); time
tensor([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13.,
 > 14., 15., 16., 17., 18., 19.])
speed = torch.randn(20)*3 + 0.75*(time-9.5)**2 + 1
plt.scatter(time,speed);

我们添加了一些随机噪声,因为手动测量并不精确。这意味着要回答这个问题并不容易:过山车的速度是多少?使用 SGD,我们可以尝试找到与我们的观察结果相匹配的函数。我们不能考虑所有可能的函数,所以我们假设它是二次函数;即形式的函数a*(time**2)+(b*time)+c

我们想要清楚地区分函数的输入(我们测量过山车速度的时间)和它的参数(定义我们正在尝试的二次方的值)。因此,让我们在一个参数中收集参数params,从而在函数的签名中将输入t和参数分开:

def f(t, params):
    a,b,c = params
    return a*(t**2) + (b*t) + c

换句话说,我们将寻找适合数据的最佳可想象函数的问题限制为寻找最佳 二次函数。这大大简化了问题,因为每个二次函数都完全由三个参数ab和定义c。因此,要找到最佳二次函数,我们只需要找到 、 和 的a最佳bc

如果我们可以为二次函数的三个参数解决这个问题,我们将能够将相同的方法应用于具有更多参数的其他更复杂的函数,例如神经网络。让我们f首先找到参数,然后我们将返回并使用神经网络对 MNIST 数据集做同样的事情。

我们需要首先定义“最佳”的含义。我们通过选择一个损失函数来精确定义这一点,它将根据预测和目标返回一个值,其中函数的较低值对应于“更好”的预测。对于连续数据,通常使用均方误差

def mse(preds, targets): return ((preds-targets)**2).mean().sqrt()

现在,让我们完成我们的七步流程。

第一步:初始化参数

首先,我们将参数初始化为随机值,并告诉 PyTorch 我们要使用以下方法跟踪它们的梯度requires_grad_

params = torch.randn(3).requires_grad_()

第 2 步:计算预测

接下来,我们计算预测:

preds = f(time, params)

让我们创建一个小函数来看看我们的预测与我们的目标有多接近,然后看看:

def show_preds(preds, ax=None):
    if ax is None: ax=plt.subplots()[1]
    ax.scatter(time, speed)
    ax.scatter(time, to_np(preds), color='red')
    ax.set_ylim(-300,100)
show_preds(preds)

这看起来不是很接近——我们的随机参数表明过山车最终会倒退,因为我们有负速度!

第三步:计算损失

我们计算损失如下:

loss = mse(preds, speed)
loss
tensor(25823.8086, grad_fn=<MeanBackward0>)

我们现在的目标是改进这一点。为此,我们需要知道渐变。

第 4 步:计算梯度

下一步是计算梯度,或者是参数需要如何变化的近似值:

loss.backward()params.grad
tensor([-53195.8594,  -3419.7146,   -253.8908])
params.grad * 1e-5
tensor([-0.5320, -0.0342, -0.0025])

我们可以使用这些梯度来改进我们的参数。我们需要选择一个学习率(我们将在下一章讨论如何在实践中做到这一点;现在,我们只使用 1e-5 或 0.00001):

params
tensor([-0.7658, -0.7506,  1.3525], requires_grad=True)

第 5 步:步权重

现在我们需要根据刚刚计算的梯度更新参数:

lr = 1e-5
params.data -= lr * params.grad.data
params.grad = None

理解这一点取决于记住最近的历史。为了计算梯度,我们调用backwardloss但这loss本身是由 计算的mse,它又将preds其作为输入,这是使用f作为输入计算的params,这是我们最初调用的对象required_grads_——这是现在允许我们调用的原始backward调用loss。这个函数调用链代表了函数的数学组成,这使得 PyTorch 能够在底层使用微积分的链式法则来计算这些梯度。

让我们看看损失是否有所改善:

preds = f(time,params)
mse(preds, speed)
tensor(5435.5366, grad_fn=<MeanBackward0>)

看看剧情:

show_preds(preds)

我们需要重复几次,所以我们将创建一个函数来应用一个步骤:

def apply_step(params, prn=True):
    preds = f(time, params)
    loss = mse(preds, speed)
    loss.backward()
    params.data -= lr * params.grad.data
    params.grad = None
    if prn: print(loss.item())
    return preds

第 6 步:重复该过程

现在我们迭代。通过循环和执行许多改进,我们希望达到一个好的结果:

for i in range(10): apply_step(params)
5435.53662109375
1577.4495849609375
847.3780517578125
709.22265625
683.0757446289062
678.12451171875
677.1839599609375
677.0025024414062
676.96435546875
676.9537353515625

正如我们所希望的那样,损失正在下降!但是只看这些损失数字掩盖了这样一个事实,即每次迭代都代表一个完全不同的二次函数,正在尝试寻找最佳二次函数。如果不是打印损失函数,而是在每一步绘制函数,我们就可以直观地看到这个过程。然后我们可以看到形状如何接近我们数据的最佳二次函数:

_,axs = plt.subplots(1,4,figsize=(12,3))
for ax in axs: show_preds(apply_step(params, False), ax)
plt.tight_layout()

第 7 步:停止

我们只是决定在 10 个 epoch 后任意停止。在实践中,我们会观察训练和验证损失以及我们的指标来决定何时停止,正如我们所 讨论的那样。

总结梯度下降

既然您已经了解了每一步会发生什么,那么让我们再看一个查看我们对梯度下降过程的图形表示(图 4-5)并快速回顾一下。

图 4-5。梯度下降过程

一开始,我们模型的权重可以是随机的 (从头开始训练)或来自预训练模型(迁移学习)。在第一种情况下,我们从输入中获得的输出与我们想要的没有任何关系,即使在第二种情况下,预训练模型也可能不会很好地完成我们所针对的特定任务. 所以模型需要学习更好的权重。

我们首先使用损失函数将模型给我们的输出与我们的目标(我们有标记的数据,所以我们知道模型应该给出什么结果)进行比较,它返回一个我们希望通过改进我们的权重。为此,我们从训练集中获取一些数据项(例如图像)并将它们提供给我们的模型。我们使用我们的损失函数比较相应的目标,我们得到的分数告诉我们我们的预测有多错误。然后我们稍微改变一下权重,让它稍微好一点。

为了找到如何改变权重以使损失更好一点,我们使用微积分来计算梯度。(实际上,我们让 PyTorch 为我们做这件事!)让我们考虑一个类比。想象一下,你迷失在山里,你的车停在最低点。为了找到回到它的路,你可能会在一个随机的方向上徘徊,但这可能无济于事。由于您知道您的车辆处于最低点,因此最好还是下坡。通过始终朝着最陡峭的下坡方向迈出一步,您最终应该到达目的地。我们使用梯度的大小(即坡度的陡度)来告诉我们要迈出多大的步幅;具体来说,我们将梯度乘以我们选择的称为学习率的数字来决定步长。然后我们迭代直到我们到达最低点,这将是我们的停车场;那我们就可以停下来了

我们刚刚看到的所有内容都可以直接转置到 MNIST 数据集,除了损失函数。现在让我们看看如何定义一个好的训练目标。

MNIST 损失函数

我们已经有了我们x的 s——即我们的自变量,图像本身。 我们将它们全部连接成一个张量,并将它们从矩阵列表(秩为 3 的张量)更改为向量列表(秩为 2 的张量)。我们可以使用 来做到这一点view,这是一种 PyTorch 方法,可以改变张量的形状而不改变其内容。-1是一个特殊参数view,意思是“使这个轴尽可能大以适应所有数据”:

train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1, 28*28)

我们需要为每个图像添加一个标签。我们将使用13s 和0 7s:

train_y = tensor([1]*len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape,train_y.shape
(torch.Size([12396, 784]), torch.Size([12396, 1]))

PyTorch 中的ADataset需要返回(x,y)索引时的元组。Python 提供了一个zip函数,当与 结合使用时 list,它提供了一种简单的方法来获得此功能:

dset = list(zip(train_x,train_y))
x,y = dset[0]
x.shape,y
(torch.Size([784]), tensor([1]))
valid_x = torch.cat([valid_3_tens, valid_7_tens]).view(-1, 28*28)
valid_y = tensor([1]*len(valid_3_tens) + [0]*len(valid_7_tens)).unsqueeze(1)
valid_dset = list(zip(valid_x,valid_y))

现在我们需要一个(最初是随机的)每个像​​素的权重(这是 我们七步过程中的初始化步骤):

def init_params(size, std=1.0): return (torch.randn(size)*std).requires_grad_()
weights = init_params((28*28,1))

该函数weights*pixels不够灵活——当像素等于 0 时它总是等于 0(即,它的截距为 0)。您可能还记得高中数学中直线的公式是y=w*x+b; 我们仍然需要b. 我们也将其初始化为一个随机数:

bias = init_params(1)

在神经网络中,w方程y=w*x+b中的 称为 权重,而b称为偏差。权重和偏差共同构成了参数

行话:参数

模型的权重偏差。权重在w方程w*x+b中,偏差b在方程中。

我们现在可以计算一个图像的预测:

(train_x[0]*weights.T).sum() + bias
tensor([20.2336], grad_fn=<AddBackward0>)

虽然我们可以使用 Pythonfor循环来计算预测 每个图像,那会很慢。因为 Python 循环不在 GPU 上运行,而且 Python 通常是一种用于循环的慢速语言,所以我们需要使用更高级别的函数在模型中表示尽可能多的计算。

在这种情况下,有一个非常方便的数学 计算w*x矩阵每一行的运算——称为矩阵乘法。 图 4-6显示了矩阵乘法的样子。

图 4-6。矩阵乘法

此图像显示了两个矩阵AB相乘。结果的每一项,我们将其称为AB,包含其对应行的A每一项乘以对应列的每一项B,相加。例如,第 1 行第 2 列(带红色边框的黄点)计算为 a1,1*b1,2+a1,2*b2,2. 如果您需要复习矩阵乘法,我们建议您查看可汗学院的 “矩阵乘法简介”,因为这是深度学习中最重要的数学运算。

在 Python 中,矩阵乘法用@运算符表示。让我们尝试一下:

def linear1(xb): return xb@weights + bias
preds = linear1(train_x)
preds
tensor([[20.2336],
        [17.0644],
        [15.2384],
        ...,
        [18.3804],
        [23.8567],
        [28.6816]], grad_fn=<AddBackward0>)

第一个元素与我们之前计算的相同,如 我们期待。这个方程batch @ weights + bias是任何神经网络的两个基本方程之一(另一个是激活函数,我们稍后会看到)。

让我们检查一下我们的准确性。要确定输出代表 3 还是 7,我们只需检查它是否大于 0,因此可以计算每个项目的准确度(使用广播,所以没有循环!)如下:

corrects = (preds>0.0).float() == train_y
corrects
tensor([[ True],
        [ True],
        [ True],
        ...,
        [False],
        [False],
        [False]])corrects.float().mean().item()
0.4912068545818329

现在让我们看看对于其中一个权重的微小变化,准确度的变化是什么:

weights[0] *= 1.0001
preds = linear1(train_x)((preds>0.0).float() == train_y).float().mean().item()
0.4912068545818329

正如我们所看到的,我们需要梯度来使用 SGD 改进我们的模型,并且为了计算梯度,我们需要一个损失函数来表示我们的模型有多好。这是因为梯度是衡量损失函数如何随着权重的微小调整而变化的量度。

所以,我们需要选择一个损失函数。显而易见的方法是使用作为我们的度量标准的准确度作为我们的损失函数。在这种情况下,我们将计算我们对每张图像的预测,收集这些值以计算总体准确度,然后计算每个权重相对于该总体准确度的梯度。

不幸的是,我们在这里遇到了一个重大的技术问题。函数的梯度是它的斜率,或者说它的陡度,可以定义为上升超过运行——即函数的值是多少 上升或下降,除以我们改变了多少输入。我们可以在数学上写成:

(y_new – y_old) / (x_new – x_old)

x_new当与 非常相似时,这给出了梯度的一个很好的近似值x_old,这意味着它们的差异非常小。但只有当预测从 3 变为 7 时,准确性才会发生变化,反之亦然。问题是权重从x_old到的微小变化x_new不太可能导致任何预测发生变化,因此(y_new – y_old)几乎总是为 0。换句话说,梯度几乎在所有地方都是 0。

权重值的非常小的变化通常根本不会改变准确性。这意味着将准确率用作损失函数是没有用的——如果我们这样做了,大多数情况下我们的梯度将为 0,并且模型将无法从该 数字中学习。

用数学术语来说,准确度是一个几乎在任何地方都是恒定的函数(除了阈值 0.5),因此它的导数几乎在任何地方都是零(并且在阈值处为无穷大)。然后,这会给出 0 或无限的梯度,这对于更新模型是无用的。

相反,我们需要一个损失函数,当我们的权重导致更好的预测时,它会给我们带来更好的损失。那么“稍微好一点的预测”到底是什么样的呢?那么,在这种情况下,这意味着如果正确答案是3,分数会高一点,或者如果正确答案是7,分数会低一点。

现在让我们编写这样一个函数。它采取什么形式?

损失函数接收的不是图像本身,而是模型的预测。因此,让我们prds为 0 和 1 之间的值创建一个参数 ,其中每个值是图像为 3 的预测。它是在图像上索引的向量(即 rank-1 张量)。

损失函数的目的是测量预测值和真实值之间的差异——即目标(又名标签)。因此,让我们trgts用 0 或 1 的值创建另一个参数 ,来判断图像实际上是否为 3。它也是一个在图像上索引的向量(即另一个 rank-1 张量)。

例如,假设我们有三张我们知道是 3、7 和 3 的图像。假设我们的模型以高置信度0.90.4,并且有合理的信心(0.2),但错误的是,最后一个是 7。这意味着我们的损失函数将接收这些值作为其输入:

trgts  = tensor([1,0,1])
prds   = tensor([0.9, 0.4, 0.2])

predictions这是测量和之间距离的损失函数的第一次尝试targets

def mnist_loss(predictions, targets):
    return torch.where(targets==1, 1-predictions, predictions).mean()

我们正在使用一个新功能,torch.where(a,b,c). 这与运行列表推导相同 [b[i] if a[i] else c[i] for i in range(len(a))],只是它以 C/CUDA 速度在张量上工作。用简单的英语来说,这个函数将测量每个预测与 1 的距离(如果它应该为 1),以及它与 0 的距离(如果它应该为 0),然后它将取所有这些距离的平均值。

阅读文档

了解这样的 PyTorch 函数很重要,因为在 Python 中循环张量以 Python 速度执行,而不是 C/CUDA 速度!尝试help(torch.where)立即运行以阅读此函数的文档,或者更好的是,在 PyTorch 文档站点上查找它。

prds让我们在我们的and上试试trgts

torch.where(trgts==1, 1-prds, prds)
tensor([0.1000, 0.4000, 0.8000])

您可以看到,当预测更准确、准确预测更有信心(绝对值更高)以及不准确预测的可信度较低时,此函数返回的数字较小。在 PyTorch 中,我们总是假设损失函数的值越低越好。由于我们需要一个标量来表示最终损失,mnist_loss因此取前一个张量的平均值:

mnist_loss(prds,trgts)
tensor(0.4333)

例如,如果我们将一个“假”目标的预测从 更改0.20.8,损失将下降,表明这是一个更好的预测:

mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)
tensor(0.2333)

当前定义的一个问题mnist_loss是它假设预测总是在 0 和 1 之间。那么,我们需要确保情况确实如此!碰巧有一个函数可以做到这一点——让我们来看看。

Sigmoid

sigmoid函数总是输出一个介于 0 和 1 之间的数字。它的定义 如下:

def sigmoid(x): return 1/(1+torch.exp(-x))

PyTorch 为我们定义了一个加速版本,所以我们真的不需要我们自己的。这是深度学习中的一个重要功能,因为我们经常希望确保值在 0 和 1 之间。它看起来像这样:

plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)

正如你所看到的,它接受任何输入值,无论是正的还是负的,并将其平滑成一个介于 0 和 1 之间的输出值。它也是一条只会上升的平滑曲线,这使得 SGD 更容易找到有意义的梯度。

让我们更新mnist_loss以首先应用于sigmoid输入:

def mnist_loss(predictions, targets):
    predictions = predictions.sigmoid()
    return torch.where(targets==1, 1-predictions, predictions).mean()

现在我们可以确信我们的损失函数会起作用,即使预测不在 0 和 1 之间。所需要的只是更高的预测对应于更高的置信度。

定义了损失函数后,现在是概括我们为什么这样做的好时机。毕竟,我们已经有了一个指标,那就是整体准确度。那么我们为什么要定义损失呢?

关键区别在于,度量是为了推动人类理解,而损失是为了推动自动化学习。为了推动自动学习,损失必须是具有有意义导数的函数。它不能有大的平坦部分和大的跳跃,而是必须相当平滑。这就是为什么我们设计了一个损失函数来响应置信水平的微小变化。这个要求意味着有时它并不能真正反映我们想要实现的目标,而是我们的真实目标和可以使用其梯度优化的函数之间的折衷。为我们数据集中的每个项目计算损失函数,然后在一个时期结束时,对损失值进行平均,并报告该时期的总体平均值。

另一方面,指标是我们关心的数字。这些是在每个 epoch 结束时打印的值,它们告诉我们模型的运行情况。在判断模型的性能时,重要的是我们要学会关注这些指标,而不是损失。

SGD 和小批量

现在我们有了适合驱动 SGD 的损失函数,我们可以 考虑下一阶段学习过程中涉及的一些细节,即根据梯度改变或更新权重。这称为优化步骤

为了采取优化步骤,我们需要计算一个或多个数据项的损失。我们应该使用多少?我们可以为整个数据集计算它并取平均值,或者我们可以为单个数据项计算它。但这些都不是理想的。为整个数据集计算它需要很长时间。为单个项目计算它不会使用太多信息,因此会导致不精确和不稳定的梯度。您会遇到更新权重的麻烦,但只考虑如何提高模型在该单个项目上的性能。

所以我们妥协:我们一次计算几个数据项的平均损失。这称为 小批量。小批量中数据项的数量称为 批量大小。较大的批大小意味着您将从损失函数中获得对数据集梯度的更准确和稳定的估计,但这将花费更长的时间,并且每个时期您将处理更少的小批。作为深度学习从业者,为了快速准确地训练模型,选择一个好的批量大小是您需要做出的决定之一。我们将在本书中讨论如何做出这种选择。

使用小批量而不是计算单个数据项的梯度的另一个很好的理由是,在实践中,我们几乎总是在 GPU 等加速器上进行训练。这些加速器只有在一次有很多工作要做的情况下才能很好地发挥作用,所以如果我们可以给它们提供大量数据项来处理,这将很有帮助。使用小批量是最好的方法之一。然而,如果你给他们太多的数据来一次处理,他们就会耗尽内存——让 GPU 满意也很棘手!

正如您在第 2 章对数据增强的讨论中看到的那样,如果我们可以在训练期间改变事物,我们将获得更好的泛化。我们可以改变的一件简单而有效的事情是我们在每个小批量中放入了哪些数据项。我们不是简单地按顺序为每个 epoch 枚举我们的数据集,而是我们通常做的是在每个 epoch 上随机打乱它,然后再创建小批量。PyTorch 和 fastai 提供了一个类,它会为你进行洗牌和小批量整理,称为DataLoader.

ADataLoader可以接受任何 Python 集合并将其转换为多个批次的迭代器,如下所示:

coll = range(15)
dl = DataLoader(coll, batch_size=5, shuffle=True)
list(dl)
[tensor([ 3, 12,  8, 10,  2]),
 tensor([ 9,  4,  7, 14,  5]),
 tensor([ 1, 13,  0,  6, 11])]

为了训练模型,我们不仅需要任何 Python 集合,还需要包含自变量和因变量(模型的输入和目标)的集合。包含自变量和因变量元组的集合在 PyTorch 中称为Dataset. 这是一个非常简单的示例Dataset

ds = L(enumerate(string.ascii_lowercase))
ds
(#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f '),(6, 'g'),(7,
 > 'h'),(8, 'i'),(9, 'j')...]

当我们将 a 传递Dataset给 a时,DataLoader我们将返回许多批次,这些批次本身就是张量元组,代表批次的独立变量和因 变量:

dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)
[(tensor([17, 18, 10, 22,  8, 14]), ('r', 's', 'k', 'w', 'i', 'o')),
 (tensor([20, 15,  9, 13, 21, 12]), ('u', 'p', 'j', 'n', 'v', 'm')),
 (tensor([ 7, 25,  6,  5, 11, 23]), ('h', 'z', 'g', 'f', 'l', 'x')),
 (tensor([ 1,  3,  0, 24, 19, 16]), ('b', 'd', 'a', 'y', 't', 'q')),
 (tensor([2, 4]), ('c', 'e'))]

我们现在准备使用 SGD 为模型编写第一个训练循环!

把它们放在一起

是时候实现我们在 图 4-1中看到的流程了。在代码中,我们的流程将在每个 epoch 中实现如下:

for x,y in dl:
    pred = model(x)
    loss = loss_func(pred, y)
    loss.backward()
    parameters -= parameters.grad * lr

首先,让我们重新初始化我们的参数:

weights = init_params((28*28,1))
bias = init_params(1)

ADataLoader可以从 a 创建Dataset

dl = DataLoader(dset, batch_size=256)
xb,yb = first(dl)
xb.shape,yb.shape
(torch.Size([256, 784]),torch.Size([256, 1]))

我们将对验证集执行相同的操作:

valid_dl = DataLoader(valid_dset, batch_size=256)

让我们创建一个大小为 4 的小批量进行测试:

batch = train_x[:4]
batch.shape
torch.Size([4, 784])
preds = linear1(batch)
preds
tensor([[-11.1002],
        [  5.9263],
        [  9.9627],
        [ -8.1484]], grad_fn=<AddBackward0>)
loss = mnist_loss(preds, train_y[:4])
loss
tensor(0.5006, grad_fn=<MeanBackward0>)

现在我们可以计算梯度:

loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad
(torch.Size([784, 1]), tensor(-0.0001), tensor([-0.0008]))

让我们将所有内容放在一个函数中:

def calc_grad(xb, yb, model):
    preds = model(xb)
    loss = mnist_loss(preds, yb)
    loss.backward()

并测试它:

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
(tensor(-0.0002), tensor([-0.0015]))

但是看看如果我们调用它两次会发生什么:

calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
(tensor(-0.0003), tensor([-0.0023]))

渐变变了!这样做的原因是loss.backward 的梯度添加loss到当前存储的任何梯度中。因此,我们必须首先将当前梯度设置为 0:

weights.grad.zero_()
bias.grad.zero_();

就地操作

PyTorch 中名称以下划线结尾的方法修改他们的对象到位。例如,bias.zero_将张量的所有元素设置bias为 0。

我们唯一剩下的步骤是根据梯度和学习率更新权重和偏差。当我们这样做时,我们必须告诉 PyTorch 不要也采用这一步的梯度——否则,当我们尝试在下一批计算导数时,事情会变得混乱!如果我们分配给data张量的属性,PyTorch 将不会采用该步骤的梯度。这是我们一个时期的基本训练循环:

def train_epoch(model, lr, params):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        for p in params:
            p.data -= p.grad*lr
            p.grad.zero_()

我们还想通过查看验证集的准确性来检查我们的表现。要确定输出代表 3 还是 7,我们只需检查它是否大于 0.5。所以我们对每个项目的准确性可以计算(使用广播,所以没有循环!)如下:

(preds>0.5).float() == train_y[:4]
tensor([[False],
        [ True],
        [ True],
        [False]])

这给了我们这个函数来计算我们的验证准确性:

def batch_accuracy(xb, yb):
    preds = xb.sigmoid()
    correct = (preds>0.5) == yb
    return correct.float().mean()

我们可以检查它是否有效:

batch_accuracy(linear1(batch), train_y[:4])
tensor(0.5000)

然后将批次放在一起:

def validate_epoch(model):
    accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl]
    return round(torch.stack(accs).mean().item(), 4)
validate_epoch(linear1)
0.5219

那是我们的出发点。让我们训练一个 epoch,看看准确性是否提高:

lr = 1.
params = weights,bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)
0.6883

然后再做一些:

for i in range(20):
    train_epoch(linear1, lr, params)
    print(validate_epoch(linear1), end=' ')
0.8314 0.9017 0.9227 0.9349 0.9438 0.9501 0.9535 0.9564 0.9594 0.9618 0.9613
 > 0.9638 0.9643 0.9652 0.9662 0.9677 0.9687 0.9691 0.9691 0.9696

看起来不错!我们已经与我们的“像素相似性”方法具有大致相同的准确性,并且我们已经创建了一个可以构建的通用基础。我们的下一步将是创建一个对象来为我们处理 SGD 步骤。在 PyTorch 中,它被称为优化器

创建优化器

因为这是一个通用的基础,所以 PyTorch 提供了一些有用的类来使其更容易实现。我们可以做的第一件事是linear1用 PyTorch 的 nn.Linear模块替换我们的函数。模块是继承的类的对象 来自 PyTorchnn.Module类。此类对象的行为与标准 Python 函数相同,因为您可以使用括号调用它们,并且它们将返回模型的激活。

nn.Linearinit_params和我们linear 一起做同样的事情。它包含单个类中的权重偏差。以下是我们如何复制上一节中的模型:

linear_model = nn.Linear(28*28,1)

每个 PyTorch 模块都知道它有哪些可以训练的参数;它们可以通过以下parameters方法获得:

w,b = linear_model.parameters()
w.shape,b.shape
(torch.Size([1, 784]),torch.Size([1]))

我们可以使用这些信息来创建优化器:

class BasicOptim:
    def __init__(self,params,lr): self.params,self.lr = list(params),lr

    def step(self, *args, **kwargs):
        for p in self.params: p.data -= p.grad.data * self.lr

    def zero_grad(self, *args, **kwargs):
        for p in self.params: p.grad = None

我们可以通过传入模型的参数来创建优化器:

opt = BasicOptim(linear_model.parameters(), lr

我们的训练循环现在可以简化:

def train_epoch(model):
    for xb,yb in dl:
        calc_grad(xb, yb, model)
        opt.step()
        opt.zero_grad()

我们的验证功能根本不需要改变:

validate_epoch(linear_model)
0.4157

让我们把我们的小训练循环放在一个函数中,让事情变得更简单:

def train_model(model, epochs):
    for i in range(epochs):
        train_epoch(model)
        print(validate_epoch(model), end=' ')

结果与上一节相同:

train_model(linear_model, 20)
0.4932 0.8618 0.8203 0.9102 0.9331 0.9468 0.9555 0.9629 0.9658 0.9673 0.9687
 > 0.9707 0.9726 0.9751 0.9761 0.9761 0.9775 0.978 0.9785 0.9785

fastai 提供了SGD默认情况下做同样事情的类 作为我们的 BasicOptim

linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)
0.4932 0.852 0.8335 0.9116 0.9326 0.9473 0.9555 0.9624 0.9648 0.9668 0.9692
 > 0.9712 0.9731 0.9746 0.9761 0.9765 0.9775 0.978 0.9785 0.9785

fastai 也提供了Learner.fit,我们可以用它来代替 train_model. 要创建一个Learner,我们首先需要 DataLoaders通过传入我们的训练和验证来创建一个DataLoader

dls = DataLoaders(dl, valid_dl)

在不使用应用程序的情况下创建一个Learner(例如 cnn_learner),我们需要传入我们在本章中创建的所有元素:DataLoaders模型、优化函数(将传递参数)、损失函数以及可选的任何要打印的指标:

learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)

现在我们可以调用fit

learn.fit(10, lr=lr)
epochtrain_lossvalid_lossbatch_accuracytime
00.6368570.5035490.49558400:00
10.5457250.1702810.86604500:00
20.1992230.1848930.83120700:00
30.0865800.1078360.91118700:00
40.0451850.0784810.93277700:00
50.0291080.0627920.94651600:00
60.0225600.0530170.95534800:00
70.0196870.0465000.96221800:00
80.0182520.0419290.96516200:00
90.0174020.0385730.96761500:00

如您所见,PyTorch 和 fastai 类没有什么神奇之处。它们只是方便的预包装件,让您的生活更轻松!(它们还提供了许多我们将在以后的章节中使用的额外功能。)

有了这些类,我们现在可以用神经网络替换我们的线性模型。

添加非线性

到目前为止,我们已经有了一个优化函数参数的通用过程,并且我们已经在一个无聊的函数上进行了尝试:一个简单的线性分类器。线性分类器在它可以做什么方面受到限制。让它更复杂一点(并且能够处理更多 任务),我们需要在两个线性分类器之间添加一些非线性的(即,不同于 ax+b)——这就是给我们一个神经网络的原因。

以下是基本神经网络的完整定义:

def simple_net(xb):
    res = xb@w1 + b1
    res = res.max(tensor(0.0))
    res = res@w2 + b2
    return res

而已!我们所拥有的simple_net只是两个线性分类器,max它们之间有一个函数。

这里w1w2是权重张量,b1b2是偏置张量;也就是说,最初随机初始化的参数,就像我们在上一节中所做的那样:

w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30,1))
b2 = init_params(1)

关键是它w1有 30 个输出激活(这意味着w2必须有 30 个输入激活,所以它们匹配)。这意味着第一层可以构建 30 个不同的特征,每个特征代表不同的像素组合。您可以将其更改30为您喜欢的任何内容,以使模型或多或少复杂。

这个小函数res.max(tensor(0.0))称为整流线性单元,也称为ReLU。我们认为我们都可以同意 整流线性单元听起来很花哨和复杂……但实际上,它只不过是 res.max(tensor(0.0))——换句话说,用零替换每个负数。这个小功能在 PyTorch 中也可用 F.relu

plot_function(F.relu)

深度学习中有大量的行话,包括诸如整流线性单元之类的术语。正如我们在这个例子中看到的那样,这个行话的绝大部分并不比可以在短行代码中实现更复杂。现实情况是,为了让学者们发表论文,他们需要让论文听起来尽可能令人印象深刻和复杂。他们这样做的一种方法是引入行话。不幸的是,这导致该领域变得比应有的更加令人生畏和难以进入。你必须学习行话,否则论文和教程对你来说意义不大。但这并不意味着您必须发现行话令人生畏。请记住,当您遇到以前从未见过的单词或短语时,几乎可以肯定它指的是一个非常简单的概念。

基本思想是,通过使用更多的线性层,我们可以得到我们的 model 进行更多的计算,因此对更复杂的函数进行建模。但是将一个线性层直接放在另一个线性层之后是没有意义的,因为当我们将事物相乘然后多次相加时,可以将不同的事物相乘并相加一次来代替!也就是说,可以将一行中任意数量的一系列线性层替换为具有不同参数集的单个线性层。

但是如果我们在它们之间放置一个非线性函数,例如max,这个 不再是真的。现在每个线性层都在某种程度上与其他线性层解耦,可以做自己有用的工作。该max函数特别有趣,因为它作为一个简单的if 语句运行。

在数学上,我们说两个线性函数的合成是另一个线性函数。因此,我们可以将任意数量的线性分类器堆叠在彼此之上,并且它们之间没有非线性函数,它将与一个线性分类器相同。

w1令人惊讶的是,如果你能找到正确的参数,并且w2如果你让这些矩阵足够大,这个小函数可以在数学上证明,这个小函数可以以任意高的准确度解决任何可计算的问题。对于任何任意摆动的函数,我们可以将其近似为一堆连接在一起的线;使它更接近摆动 函数,我们只需要使用较短的行。这被称为普遍逼近定理。我们这里的三行代码称为。第一和第三层被称为线性层,第二行代码被称为非线性或 激活函数

就像上一节一样,我们可以利用 PyTorch 用更简单的代码替换这段代码:

simple_net = nn.Sequential(
    nn.Linear(28*28,30),
    nn.ReLU(),
    nn.Linear(30,1)
)

nn.Sequential创建一个模块,依次调用每个列出的层或函数。

nn.ReLU是一个 PyTorch 模块,其功能与函数完全相同F.relu。大多数可以出现在模型中的函数也具有相同的模块形式。通常,这只是替换和更改大小写的F情况nn。使用时nn.Sequential,PyTorch 要求我们使用模块版本。由于模块是类,我们必须实例化它们,这就是你nn.ReLU在这个 例子中看到的原因。

因为nn.Sequential是一个模块,所以我们可以获取它的参数,这将返回它包含的所有模块的所有参数的列表。让我们试试吧!由于这是一个更深的模型,我们将使用较低的学习率和更多的 epoch:

learn = Learner(dls, simple_net, opt_func=SGD,
                loss_func=mnist_loss, metrics=batch_accuracy)
learn.fit(40, 0.1)

为了节省空间,我们没有在这里显示 40 行输出;训练过程记录在 中learn.recorder,输出表存储在values属性中,因此我们可以绘制训练过程中的准确率:

plt.plot(L(learn.recorder.values).itemgot(2));

我们可以查看最终的准确率:

learn.recorder.values[-1][2]

 0.982826292514801

在这一点上,我们有一些相当神奇的东西:

  • 在给定正确的参数集的情况下,可以以任何精度(神经网络)解决任何问题的函数

  • 一种为任何函数找到最佳参数集的方法(随机梯度下降)

这就是为什么深度学习可以做如此奇妙的事情。相信这种简单技术的组合可以真正解决任何问题是我们发现许多学生必须采取的最大步骤之一。这似乎好得令人难以置信——事情肯定比这更困难和复杂吗?我们的建议:试试看!我们刚刚在 MNIST 数据集上进行了尝试,您已经看到了结果。而且由于我们自己从头开始做所有事情(除了计算梯度),你知道幕后没有什么特别的魔法。

更深入

没有必要只停留在两个线性层。我们可以添加尽可能多的 如我们所愿,只要我们在每对线性之间添加一个非线性 层。然而,正如您将了解到的,模型越深入,在实践中优化参数就越困难。在本书的后面部分,您将了解一些用于训练更深层次模型的简单但非常有效的技术。

我们已经知道,具有两个线性层的单个非线性足以逼近任何函数。那么我们为什么要使用更深层次的模型呢? 原因是性能。使用更深的模型(具有更多层的模型),我们不需要使用尽可能多的参数;事实证明,我们可以使用具有更多层的更小矩阵,并获得比使用更大矩阵和更少层获得更好的结果。

这意味着我们可以更快地训练模型,并且需要 占用更少的内存。在 1990 年代,研究人员非常关注通用逼近定理,以至于很少有人尝试不止一种非线性。这种理论而非实践的基础阻碍了该领域多年。然而,一些研究人员确实对深度模型进行了实验,最终能够证明这些模型在实践中可以表现得更好。最终,理论结果的发展表明了为什么会发生这种情况。今天,很少有人使用只有一个非线性的神经网络。

下面是我们使用第 1 章中看到的相同方法训练 18 层模型时发生的情况:

dls = ImageDataLoaders.from_folder(path)
learn = cnn_learner(dls, resnet18, pretrained=False,
                    loss_func=F.cross_entropy, metrics=accuracy)
learn.fit_one_cycle(1, 0.1)
epochtrain_lossvalid_lossaccuracytime
00.0820890.0095780.99705600:11

准确率接近 100%!相比起来有很大的不同 我们简单的神经网络。但是,正如您将在本书的其余部分中学到的那样,您只需要使用一些小技巧就可以自己从头开始获得如此出色的结果。您已经知道关键的基础部分。(当然,即使您知道所有技巧,您也几乎总是希望使用 PyTorch 和 fastai 提供的预构建类,因为它们使您不必自己考虑所有小细节。)

行话回顾

恭喜:您现在知道如何创建和训练深度神经 网络从零开始!我们已经完成了很多步骤来达到这一点,但您可能会惊讶于它实际上是多么简单。

现在我们已经到了这一点,这是定义和回顾一些行话和关键概念的好机会。

神经网络包含很多数字,但它们只有两种类型:计算的数字,以及计算这些数字的参数。这给了我们两个最重要的行话来学习:

激活

计算的数字(通过线性和非线性层)

参数

随机初始化和优化的数字(即定义模型的数字)

我们将在本书中经常讨论激活和参数。请记住,它们具有特定的含义。它们是数字。它们不是抽象概念,而是模型中实际的特定数字。成为一名优秀的深度学习实践者的一部分是习惯于查看你的激活和参数,绘制它们并测试它们是否行为正确。

我们的激活和参数都包含在张量中。这些是 简单的规则形状的数组——例如,矩阵。矩阵有行和列;我们称这些为轴维度。号码 张量的维度是它的。有一些特殊的张量:

  • Rank-0:标量

  • Rank-1:向量

  • Rank-2:矩阵

神经网络包含许多层。每一层都是 线性非线性。我们通常在神经网络中的这两种层之间交替。有时人们将线性层及其随后的非线性一起称为单层。是的,这令人困惑。有时非线性被称为 激活函数

表 4-1总结了与 SGD 相关的关键概念。

表 4-1。深度学习词汇
Term意义
ReLU

对负数返回 0 且不更改正数的函数。

Mini-batch

一小组输入和标签聚集在两个数组中。在这批(而不是整个时期)上更新梯度下降步骤。

Forward pass

将模型应用于某些输入并计算预测。

Loss

一个值,表示我们的模型做得好(或坏)。

Gradient

损失对模型某些参数的导数。

Backward pass

计算关于所有模型参数的损失梯度。

Gradient descent

朝着与梯度相反的方向迈出一步,使模型参数更好一点。

Learning rate

我们在应用 SGD 更新模型参数时所采取的步长。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值