环境:jupyter notebook
初始设置:
!pip install -Uqq fastbook
import fastbook
fastbook.setup_book()
from fastai.vision.all import *
from fastbook import *
matplotlib.rc('image',cmap='Greys')#将图片映射为灰度图片
在第2章中,我们已经看到了实际训练各种模型是什么样子的,现在让我们看看背后的情况,看看到底发生了什么。我们将从使用计算机视觉
开始,介绍深度学习的基本工具和概念。
确切地说,我们将讨论数组、张量和广播
的作用,广播是一种用于表达它们的强大技术。我们将解释随机梯度下降(SGD
),通过自动更新权值来学习的机制。我们将讨论基本分类任务的损失函数的选择
,以及小批量
的作用。我们也会描述基本神经网络实际在做的数学运算。最后,我们将把所有这些片段放在一起。
在以后的章节中,我们还将深入研究其他应用程序,并了解这些概念和工具是如何推广的。但这一章是关于奠定基石的。坦白地说,这也使这一章成为最难的章节之一,因为这些概念是如何相互依赖的。就像拱门一样,所有的石头都要放在合适的位置,这样结构才不会倒塌。就像拱门一样,一旦发生这种情况,它就是一个强大的结构,可以支撑其他东西。但它需要一些耐心来组装。
让我们开始吧。第一步是考虑如何在计算机中表示图像。
为了理解计算机视觉模型中发生了什么,我们首先要理解计算机是如何处理图像的。 我们将使用计算机视觉中最著名的数据集之一,MNIST
,来进行实验。 MNIST包含手写数字的图像
,由美国国家标准与技术研究所收集,并由Yann Lecun和他的同事整理成一个机器学习数据集。 Lecun于1998年在Lenet-5中使用了MNIST,这是第一个能够实际识别手写数字序列的计算机系统。 这是人工智能史上最重要的突破之一。
深度学习的故事是由一小撮专注的研究人员坚韧和毅力的故事。 在早期的希望(和炒作!)之后,神经网络在20世纪90年代和2000年代失去了人们的青睐,只有少数研究人员一直在努力让它们发挥作用。 他们中的三位,扬·勒昆、约书亚·本吉奥和杰弗里·欣顿,在2018年获得了计算机科学的最高荣誉——图灵奖(通常被认为是“计算机科学的诺贝尔奖”),尽管机器学习和统计界对此深表怀疑和不感兴趣。
Geoff Hinton曾说过,即使学术论文的结果比之前发表的任何论文都好得多,也会被顶级期刊和会议拒绝,就因为他们使用了神经网络。Yann Lecun关于卷积神经网络的研究(我们将在下一节中研究)表明,这些模型可以阅读手写文本——这是以前从未实现过的。然而,他的突破被大多数研究人员忽视了,即使它被用于商业阅读10%的支票在美国!
除了这三位图灵奖得主,还有许多其他的研究人员,他们为我们取得今天的成就而奋斗。例如,Jurgen Schmidhuber(许多人认为他应该分享图灵奖)是许多重要思想的先驱,包括与他的学生Sepp Hochreiter在长期短期记忆(LSTM)体系结构上的合作(广泛用于语音识别和其他文本建模任务,并在<>中的IMDb示例中使用)。也许最重要的是,Paul Werbos在1974年发明了神经网络的反向传播技术,这一技术在本章中展示,并被广泛用于训练神经网络(Werbos 1994)。几十年来,他的开发几乎完全被忽视,但今天,它被认为是现代人工智能最重要的基础。
这对我们所有人都是一个教训!在你的深度学习之旅中,你将面临许多障碍,既有技术上的,也有(甚至更困难)周围那些不相信你会成功的人带来的障碍。有一种方法肯定会失败,那就是停止尝试。我们已经看到,在每一个成为世界级实践者的fast.ai学生中,唯一一致的特征,就是他们都很顽强。
在这个最初的教程中,我们将尝试创建一个模型,可以将任何图像分类为3或7。所以让我们下载一个MNIST的样本,它包含了这些数字的图像:
path = untar_data(URLs.MNIST_SAMPLE)
Path.Base_PATH = path
我们可以通过使用ls (fastai添加的一个方法)来查看这个目录中的内容。这个方法返回一个名为L
的特殊fastai类的对象,它具有Python内置列表的所有相同功能,以及更多的功能。它的一个方便的特性是,当打印时,它会在列出项目本身之前显示项目的数量(如果有超过10个项目,它只显示前几个项目):
path.ls()
out:
(#3) [Path('train'),Path('labels.csv'),Path('valid')]
MNIST数据集遵循机器学习数据集的通用布局:训练集和验证集(和/或测试集)分别放在不同的文件夹中。让我们看看训练集里面有什么
(path/'train').ls()
out:
(#2) [Path('train/7'),Path('train/3')]
一个文件夹是数字3,一个文件夹是数字7。用机器学习的说法,我们说“3”和“7”是这个数据集中的标签(或目标)
。让我们看看这些文件夹中的一个(使用sorted
以确保我们得到的文件顺序相同):
threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
threes
out:
(#6131) [Path('train/3/10.png'),Path('train/3/10000.png'),Path('train/3/10011.png'),Path('train/3/10031.png'),Path('train/3/10034.png'),Path('train/3/10042.png'),Path('train/3/10052.png'),Path('train/3/1007.png'),Path('train/3/10074.png'),Path('train/3/10091.png')...]
正如我们所料,它充满了图像文件。现在我们来看一看。这是一张手写数字3的图片,取自著名的MNIST手写数字数据集:
im3_path = threes[1]
im3 = Image.open(im3_path)
im3
这里我们使用的是Python Imaging Library (PIL)中的Image
类,PIL是用于打开、操作和查看图像的最广泛使用的Python包。Jupyter了解PIL图像,所以它会自动为我们显示图像。
在计算机中,一切都用数字表示。为了查看组成这个图像的数字,我们必须将其转换为NumPy数组
或PyTorch张量
。例如,这是图像的一部分转换为NumPy数组:
array(im3)[4:10,4:10]
#对tensor也是同样的操作:
#tensor(im3)[4:10,4:10]
out:
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)
我们可以对数组进行切片,只选取数字顶部的部分,然后使用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个像素。(这比你从手机摄像头中得到的图像要小得多,手机摄像头有数百万像素,但对于我们最初的学习和实验来说,这是一个方便的尺寸。我们很快就会制作更大的全彩图像。)
所以,现在你已经看到了一个图像对于计算机来说是什么样的,让我们回忆一下我们的目标:创建一个可以识别3s和7的模型。你怎么可能去找一台电脑来做这件事呢?
警告:停下来想一想!:在您继续阅读之前,请花一点时间考虑一下计算机是如何能够识别这两个不同的数字的。它能观察到哪些特征呢?它如何能够识别这些特性?它如何将它们结合在一起?当你尝试自己解决问题时,学习效果最好,而不是仅仅阅读别人的答案;所以,离开这本书几分钟,拿出一张纸和一支笔,记下一些想法……
那么,这是第一个想法:我们如何找到3s中每个像素的平均像素值,然后对7s进行同样的操作。这将给我们两个组平均值,定义我们所谓的“理想”3和7。然后,为了将图像分类为一个数字或另一个数字,我们可以看到图像最接近这两个理想数字中的哪一个。这似乎总比什么都没有好,所以这将是一个很好的基线。
术语:基线:一个简单的模型,你相信它应该运行得相当好。它应该很容易执行,也很容易测试,这样你就可以测试每一个改进的想法,确保它们总是比基线更好。如果没有一个合理的基线,就很难知道你fancy的模型是否真的很好。创建基线的一个好方法就是我们在这里所做的:考虑一个简单的、易于实现的模型。另一个好方法是四处搜索,找到其他已经解决类似问题的人,并下载和运行他们的代码在您的数据集上。理想情况下,两种方法都试试!
我们的简单模型的第一步是得到两组像素值的平均值。在这个过程中,我们将学习很多整洁的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(seven_tensors),len(three_tensors)
out:
(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 images(Jupyter默认会显示为图像),我们需要使用fastai的show_image
函数来显示它:
show_image(three_tensors[1])
对于每个像素位置,我们想要计算该像素的所有图像强度的平均值。为了做到这一点,我们首先把这个列表中的所有图像合并成一个三维张量。描述这样一个张量最常见的方法是称它为3阶张量
。我们经常需要把集合中的单个张量叠加成一个张量。毫无疑问,PyTorch提供了一个名为stack
的函数,我们可以使用它来实现这个目的。
PyTorch中的一些操作,比如取平均值,要求我们将整型转换为浮点型。因为我们稍后会用到它,我们现在也会将堆叠张量转换为浮点数
。在PyTorch中进行类型转换非常简单,只需输入你想要进行类型转换的类型名,然后将其作为一个方法来处理。
一般来说,当图像是浮点数时,像素值应该在0到1之间,所以我们也会在这里除以255:
stacked_sevens = torch.stack(seven_tensors).float()/255
stacked_threes = torch.stack(three_tensors).float()/255
stacked_threes.shape
out: torch.Size([6131, 28, 28])
也许张量最重要的属性是它的形状。这告诉你每个轴的长度。在本例中,我们可以看到我们有6131张图片,每张图片的大小为28×28像素。这个张量并没有特别说明第一个轴是图像的数量,第二个是高度,第三个是宽度——张量的语义完全取决于我们,以及我们如何构造它。就PyTorch而言,它只是内存中的一堆数字。
张量形状的长度是它的秩:
len(stacked_threes.shape)
out:3
记住和练习这些张量术语非常重要:秩是张量中轴或维度的数量;形状是张量每个轴的大小。
要小心,因为术语“维度”有时会以两种方式使用。考虑到我们生活在“三维空间”中,其中的物理位置可以用一个3维向量v来描述。但是根据PyTorch,属性v.ndim(看起来确实像v的“维度数”)等于1,而不是3 ! 为什么? 因为v是一个向量,它是一个秩为1的张量,这意味着它只有一个轴(即使这个轴的长度是3)。换句话说,有时候维度被用来表示轴的大小(“空间是三维的”); 其他时候,它用于秩或轴的数量(“一个矩阵有两个维度”)。当感到困惑时,我发现将所有语句转换为秩、轴和长度是很有帮助的,这些都是明确的术语。
我们可以通过ndim
直接得到一个张量的秩:
stacked_threes.dim
out:3
最后,我们可以计算出理想3是什么样的。我们计算所有图像张量的均值,通过取堆叠的3阶张量沿维数0的均值。这是索引所有图像的维度。
换句话说,对于每个像素位置,这将计算该像素在所有图像上的平均值。结果将是每个像素位置的一个值,或单个图像。这里是:
mean3 = stacked_threes.mean(0)
show_image(mean3);
根据这个数据集,这是理想的数字3! (你可能不喜欢,但这就是三号峰值表现的样子。)你可以看到,在所有图像都认为应该是黑暗的地方,它是非常黑暗的,但在图像不一致的地方,它就变得模糊和模糊。
让我们对第7做同样的事情,但把所有步骤放在一起,以节省时间:
mean7 = stacked_sevens.mean(0)
show_image(mean7)
现在让我们任意选择一个3,测量它到“理想位数”的距离。
停下来:停下来想一想! 你如何计算一个特定的图像与我们的每个理想数字有多相似? 记住,在你继续阅读之前,放下这本书,记下一些想法! 研究表明,当你通过解决问题、实验和自己尝试新想法来参与学习过程时,记忆力和理解力会显著提高。
这是一个样本3:
a_3 = stacked_threes[1]
show_images(a_3)
我们如何确定它与理想的距离呢? 我们不能只是把图像像素和理想数字之间的差加起来。有些差异是正的,有些是负的,这些差异会被抵消,从而导致在某些地方太暗而另一些地方太亮的图像可能会显示出与理想的总体差异为零。那将是误导!
为了避免这种情况,数据科学家有两种主要的方法来测量这种情况下的距离:
现在让我们试试这两个:
dist_3_abs = (a_3 - mean3).abs().mean()
dist_3_sqr = ((a_3 - mean3)**2).mean().sqrt()
dist_3_abs,dist_3_sqr
out:(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
out: (tensor(0.1586), tensor(0.3021))
在这两种情况下,我们的3和“理想”3之间的距离小于与理想7之间的距离。在这种情况下,我们的简单模型会给出正确的预测。
PyTorch已经提供了这两种损失函数。你会在torch.nn.function
里面找到这些, PyTorch团队建议将其导入为F
(并且在fastai中该名称默认可用):
F.l1_loss(a_3.float(),mean7),F.l2_loss(a_3.float(),mean7).sqrt()
out:(tensor(0.1586), tensor(0.3021))
在这里mse
表示均方误差,l1
表示平均绝对值的标准数学术语(在数学中它被叫做L1范数)
直观地说,L1范数和均方误差(MSE)之间的区别是后者会比前者惩罚更大的错误(对小错误更宽容)。
我们刚刚完成了PyTorch张量的各种数学运算。如果您以前在NumPy中进行过一些数值编程,那么您可能会认为它们类似于NumPy数组。让我们来看看这两个非常重要的数据结构。
NumPy是Python中科学和数值编程中使用最广泛的库。它提供了与PyTorch非常相似的功能和API; 然而,它不支持使用GPU或计算梯度,这两者都是深度学习的关键。因此,在本书中,我们将尽可能地使用PyTorch张量而不是NumPy数组。
(请注意fastai为NumPy和PyTorch添加了一些功能,使它们彼此更加相似。如果本书中的任何代码在你的电脑上不起作用,很有可能你忘记在笔记本的开头添加这样一行:from fastai.vision.all import *。)
但是什么是数组和张量,为什么要关心这些呢?
与许多语言相比,Python速度较慢。任何在Python、NumPy或PyTorch中运行速度快的东西都可能是用另一种语言(特别是C)编写(和优化)的编译对象的包装器。事实上,NumPy数组和PyTorch张量可以比使用纯Python快数千倍完成计算。
NumPy数组是一个多维数据表,所有的项都是同一类型的。因为它可以是任何类型,所以它们甚至可以是数组的数组,最里面的数组可能具有不同的大小——这称为“交错数组”。我们所说的“多维表”是指,例如,一个列表(维度为1),一个表或矩阵(维度为2),一个“表中的表”或“立方体”(维度为3),等等。如果这些项都是一些简单的类型,如整数或浮点数,那么NumPy将把它们存储在内存中作为一个紧凑的C数据结构。这就是NumPy的闪光点。NumPy有各种各样的运算符和方法,它们可以在这些紧凑的结构上以与优化后的C语言相同的速度运行计算,因为它们是用优化后的C语言编写的。
PyTorch张量与NumPy数组几乎是一样的,但是有一个额外的限制来解锁一些额外的功能。它的相同之处在于,它也是一个多维数据表,包含所有相同类型的项。然而,限制是一个张量不能使用任何旧的类型——它必须使用一个基本的数字类型来表示所有的分量。例如,PyTorch张量不能被锯齿化。它始终是一个规则形状的多维矩形结构。
NumPy支持的绝大多数方法和操作符在这些结构上也被PyTorch支持,但是PyTorch张量还有额外的功能。一个主要的能力是这些结构可以在GPU上运行,在这种情况下,它们的计算将为GPU优化,并可以运行得更快(给定大量的值来工作)。此外,PyTorch可以自动计算这些操作的导数,包括操作的组合。正如你将看到的,如果没有这种能力,深度学习在实践中是不可能实现的。
那么,我们的基线模型有用吗?为了量化这一点,我们必须定义一个度量(评价指标)。
回想一下,度量是一个数字,它是根据我们模型的预测和数据集中的正确标签计算出来的,以便告诉我们模型有多好。例如,我们可以使用在前一节中看到的函数,均方误差或平均绝对误差,并在整个数据集上取它们的平均值。然而,这两个数字对大多数人来说都不是很容易理解的; 在实践中,我们通常使用准确性作为分类模型的度量。
正如我们已经讨论过的,我们想在一个验证集(测试集)上计算我们的度量。这样我们就不会无意中过拟合——也就是说,训练一个模型,使它只在我们的训练数据上工作。对于我们在这里第一次使用的像素相似度模型来说,这并不是一个真正的风险,因为它没有经过训练的组件,但我们将使用一个验证集来遵循正常的实践,并为之后的第二次尝试做好准备。
为了得到一个验证集,我们需要从训练中完全抽离一些数据,这样它就不会被模型看到。事实证明,MNIST数据集的创建者已经为我们做到了这一点。还记得有一个单独的目录叫做valid吗?这就是这个目录的用途!
首先,我们从那个目录中为数字3和7创建张量。我们将使用这些张量来计算我们第一次尝试的模型的质量度量,该度量是从理想图像到图像的距离
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,valid_7_tens
out: (torch.Size([1010, 28, 28]), torch.Size([1028, 28, 28]))
养成检查形状的习惯是很好的。这里我们看到两个张量,一个代表由1,010张图片组成的3的验证集28×28,一个代表由1,028张图片组成的7的验证集28×28。
我们最终想要写出一个函数,is_3
,来决定任意图像是3还是7。它将通过决定我们的两个“理想数字”中的哪一个更接近这个任意的图像来做到这一点。为此,我们需要定义一个距离的概念,即一个计算两幅图像之间距离的函数。
我们可以编写一个简单的函数来计算平均绝对误差,使用一个非常类似于我们在上一节中编写的表达式:
def mnist_distance(a,b): return (a-b).abs().mean((-1,-2)) #只计算张量的最后两个维度(最后两个维度索引了一个数)
mnist_distance(a_3,mean3)
out:tensor(0.1114)
这与我们之前计算的这两幅图像之间的距离值相同,即理想的3:mean3和任意的样本3:a_3,它们都是形状为[28,28]的单图像张量。
但是为了计算总体精度的度量,我们需要为验证集中的每个图像计算到理想3的距离。我们怎么计算呢? 我们可以对所有堆叠在验证集张量valid_3_tens
中的单图像张量进行循环,valid_3_tens的形状为[1010,28,28]
,表示1010张图像。但是有一个更好的方法。
当我们使用这个完全相同的距离函数时,非常有趣的事情发生了,这个函数设计用于比较两个单一的图像,但传入一个参数valid_3_tens
,这个张量表示3的验证集:
valid_3_dist = mnist_distance(valid_3_tens,mean3)
valid_3_dist,valid_3_dist.shape
out:
(tensor([0.1328, 0.1523, 0.1245, ..., 0.1383, 0.1280, 0.1138]),
torch.Size([1010]))
它没有抱怨形状不匹配,而是将每个图像的距离作为一个长度为1010(我们的验证集中3的个数)的向量(即1级张量)返回。这是怎么发生的?
再看一下我们的函数mnist_distance
,你会看到这里有减法(a-b)
。当PyTorch试图在两个不同等级的张量之间执行简单的减法操作时,它会使用广播。也就是说,它会自动展开小阶张量使其大小与大阶张量相同。广播是使张量代码更容易编写的一个重要功能。
在广播之后,两个参数张量具有相同的秩,PyTorch对两个相同秩的张量应用其通常的逻辑: 它对两个张量的每个对应元素执行操作,并返回张量结果。例如:
tensor([1,2,3])+tensor([1]) #广播运算
out:tensor([2, 3, 4])
所以在这种情况下,PyTorch将mean3,一个2级张量,表示一幅图像,就像它是同一幅图像的1010个副本,然后从验证集中的3个副本中减去每一个副本。你认为这个张量的形状是什么?在你看到下面的答案之前,试着自己弄明白:
(valid_3_tens - mean3).shape
out:torch.Size([1010, 28, 28])
我们正在计算我们的“理想3”和验证集中的每一个3之间的差值,对于每一个28×28图像,结果是形状[1010,28,28]
。
关于广播是如何实现的,有几点很重要,这使得它不仅在表现方面有价值,而且在性能方面也有价值:
在PyTorch中完成的所有广播和元素操作和函数都是如此。要创建高效的PyTorch代码,这是最重要的技术。
下一个我们看到的运算是abs
,你们现在可以猜到当应用到一个张量时会发生什么。它将该方法应用于张量中的每个单独元素,并返回结果的一个张量(也就是说,它应用了“elementwise”方法)。在这个例子中,我们会得到1010个绝对值矩阵。
最后,我们的函数调用mean((-1,-2))
。元组(-1,-2)
表示坐标轴的范围。在Python中,-1
表示最后一个元素,-2
表示倒数第二个元素。在这种情况下,这告诉PyTorch我们想要取张量最后两个轴上的值的平均值。最后两个轴是图像的水平和垂直尺寸。取最后两个轴的平均值后,我们只剩下第一个张量轴,它在我们的图像上索引,这就是为什么我们的最终尺寸是(1010)
。换句话说,对于每一张图像,我们平均了图像中所有像素的强度。
我们可以使用mnist_distance
来计算出一个图像是否为3,使用下面的逻辑: 如果数字与理想3之间的距离小于与理想7之间的距离,那么它就是3。这个函数将自动进行广播,并以元素方式应用,就像所有PyTorch函数和操作符一样:
def is_3(x): return mnist_distance(x, mean3) < mnist_distance(x, mean7)
is_3(s_3),is_3(a_3).float()
out: (tensor(True), tensor(1.))
在整个测试集上进行测试:
is_a(valid_3_tens)
out:tensor([ True, True, True, …, False, True, True])
现在,我们可以计算每个3和7的准确率,通过对所有3和所有7的函数取平均值:
accuracy_3s = is_3(valid_3_tens).float().mean()
accuracy_7s =(1 - is_3(valid_3_tens).float()).mean()
accuracy_3s,accuracy_7s,(accuracy_3s + accuracy_7s) / 2
out:(tensor(0.9168), tensor(0.9854), tensor(0.9511))
这看起来是个不错的开始! 我们在3s和7s上都获得了超过90%的准确度,我们已经看到了如何使用广播方便地定义度量。
但老实说:3s和7s是看起来非常不同的数字。目前我们只对10个可能的数字中的2个进行分类。所以我们需要做得更好!
为了做得更好,也许是时候尝试一种能够真正学习的系统了——也就是说,它能够自动地自我修改以提高性能。换句话说,是时候谈谈训练过程和SGD了。
亚瑟·塞缪尔这样描述机器学习的:
假设我们安排了一些自动的方法,根据实际性能来测试任何当前权重分配的有效性,并提供了一种机制来改变权重分配,从而使性能最大化。我们不需要深入研究这种程序的细节,就可以看到它可以完全自动化,也可以看到这样编程的机器会从它的经验中“学习”。
正如我们所讨论的,这是让我们拥有一个可以越来越好(可以学习)的模型的关键。但是我们的像素相似度方法并没有做到这一点。我们没有任何类型的权重分配,或者任何基于测试权重分配的有效性来改进的方法。换句话说,我们不能通过修改一组参数来改进像素相似度方法。为了利用深度学习的力量,我们首先要按照阿瑟·塞缪尔(Arthur Samuel)所描述的方式来表示我们的任务。
而不是试图找到一个图像之间的相似性和一个理想的形象,“我们可以看看每个像素和想出一套每一个的重量,这样最高权重与那些最有可能是黑色像素为特定类别。例如,右下角的像素不太可能被激活为7,所以他们应该有一个低权重的7,但他们可能被激活为8,所以他们应该有一个高权重的8。这可以表示为每个可能类别的一个函数和权重值集——例如数字8的概率:
def pr_eight(x,w):return (x*w).sum()
这里我们假设x
是图像,用向量表示——换句话说,所有的行首尾相连地叠成一条长线。我们假设权值是向量w
,如果我们有这个函数,那么我们只需要一些方法来更新权值,使它们更好一些。有了这种方法,我们可以重复这个步骤很多次,使权重越来越好,直到达到我们所能达到的效果。
我们想要找到向量w
的特定值,使得函数的结果对于那些实际上是8的图像是高的,而对于那些不是8的图像是低的。寻找最佳向量w
是一种寻找识别8的最佳函数的方法。(因为我们还没有使用深度神经网络,所以我们受到了函数实际功能的限制——我们将在本章的后面修复这个限制。)
更具体地说,下面是我们将需要的步骤,将这个函数转换为机器学习分类器:
这七个步骤是训练所有深度学习模型的关键。深度学习完全依赖于这些步骤,这是非常令人惊讶和违反直觉的。这个过程可以解决如此复杂的问题,这是令人惊讶的。但是,正如您将看到的,它确实如此!
有许多不同的方法来完成这七个步骤中的每一个,我们将在本书的其余部分学习它们。 这些细节对深度学习实践者来说意义重大,但事实证明,每种方法都遵循一些基本原则。 以下是一些指导方针:
在将这些步骤应用到图像分类问题之前,让我们用一个更简单的例子来说明它们是什么样子的。首先,我们将定义一个非常简单的二次函数——假设这是我们的损失函数,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')
现在我们看看如果我们稍微增加或减少参数会发生什么——调整。这就是某一点的斜率:
我们可以在斜率的方向稍微改变我们的权重,再次计算我们的损失和调整,并重复几次。最终,我们将到达曲线上的最低点:
这个基本思想可以追溯到艾萨克·牛顿,他指出我们可以用这种方法优化任意函数。无论我们的函数变得多么复杂,梯度下降的基本方法都不会有明显的改变。在本书后面我们将看到的唯一小的变化是一些方便的方法,通过找到更好的步骤,我们可以使它更快。
一个神奇的步骤是我们计算梯度的位。正如我们提到的,我们使用微积分作为性能优化;它可以让我们更快地计算出,当我们调整参数时,我们的损失会上升还是下降。换句话说,梯度会告诉我们要改变每个权重多少才能使我们的模型更好。
我们刚才提到,你不需要自己计算任何梯度。这怎么可能呢?令人惊讶的是,PyTorch能够自动计算几乎任何函数的导数!更重要的是,它的速度非常快。大多数情况下,它至少和你手工创建的任何导数函数一样快。让我们看一个例子。
xt = tensor(3.)require_grad_()
注意到特殊方法requires_grad_
?这是我们用来告诉PyTorch的神奇咒语,我们想要在那个值(3.)上计算关于那个变量(xt)的梯度。它本质上是标记变量。
如果您来自数学或物理领域,这个API可能会让您感到困惑。在这些上下文中,函数的“梯度”只是另一个函数(即它的导数),所以您可能期望与梯度相关的api为您提供一个新函数。但在深度学习中,“梯度”通常是指函数在特定参数值处的导数值。PyTorch API也把焦点放在参数上,而不是你实际计算梯度的函数上。一开始可能会觉得有点倒退,但这只是一个不同的视角。
现在我们用这个值来计算函数。请注意,PyTorch不仅输出计算出的值,还需要注意的是,它有一个梯度函数,将在需要时用于计算我们的梯度:
yt = f(xt)
yt
out:tensor(9., grad_fn=)
最后,我们让PyTorch为我们计算梯度:
yt.backward()
这里的“反向”指的是反向传播,这是计算每一层导数的过程。当我们从头开始计算深度神经网络的梯度时。这被称为网络的“向后通过”,而不是计算激活的“向前通过”。如果backword
仅仅被称为calculate_grad
,生活可能会更容易,但搞深度学习的人真的喜欢在任何地方添加术语!
我们现在可以通过检查张量的梯度属性来查看梯度:
xt.grad
out:tensor(6.)
现在,我们将重复前面的步骤,但给函数添加一个向量实参:
xt = tensor([3.,4.,10.]).require_grad_()
xt
out:tensor([ 3., 4., 10.], requires_grad=True)
然后我们把sum
加到我们的函数中,这样它就可以取一个向量(即一个秩1的张量),并返回一个标量(即一个秩0的张量):
def f(x): return (x**2).sum()
yt = f(xt)
yt
out:tensor(125., grad_fn=)
我们的梯度是2*xt
,正如我们所期望的那样!
yt.backward()
xt.grad
out:tensor([ 6., 8., 20.])
梯度函数只告诉我们函数的斜率,它们并没有告诉我们参数调整的距离。但它给了我们一些概念;如果斜率非常大,那可能意味着我们需要做更多的调整,而如果斜率非常小,那可能意味着我们接近最优值。
step
决定如何根据梯度的值来改变我们的参数是深度学习过程中的一个重要部分。几乎所有的方法都是从将梯度乘以一个很小的数开始的,这个数叫做学习率(LR)。学习速率通常是0.001到0.1之间的数字,尽管它可以是任何数字。通常,人们只是通过尝试一些学习速率来选择一个学习速率,然后在训练后找到最好的模型(我们将在本书后面向您展示一个更好的方法,称为学习速率查找器 )。一旦你选择了一个学习速率,你可以使用这个简单的函数来调整你的参数:
w -= gradient(w)*lr
这被称为使用优化器步骤逐步执行参数。
如果你选择的学习速度过低,这可能意味着你必须做很多步骤:
但是选择一个过高的学习速率会更糟糕——它实际上会导致更大的损失,正如我们所看到的:
如果学习速率过高,它可能也会“到处弹”,而不是真正的收敛; 下图显示了这是如何通过许多步骤 才训练成功的结果:
现在让我们在一个端到端示例中应用所有这些。
我们已经知道如何使用梯度来求最小值。现在我们来看一个SGD示例,看看如何使用最小值来训练模型以更好地适应数据。
让我们从一个简单的、合成的示例模型开始。想象一下,你正在测量过山车经过驼峰顶部时的速度。它开始的时候会很快,然后在上山的时候慢慢变慢;它在顶部是最慢的,然后在下坡的时候再次加速。你想要建立一个速度如何随时间变化的模型。如果你每秒钟手动测量20秒的速度,它可能看起来像这样:
time = torch.arange(0,20).float();time
out: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
的函数。
我们想要清楚地区分函数的输入(我们测量过山车速度的时间)和它的参数(定义我们尝试的哪个二次函数的值)。因此,让我们把参数收集到一个参数中,从而将输入t
和函数签名中的参数params
分开:
def f(t,params):
a,b,c = params
return a*(t**2) + b*t + c
换句话说,我们把找到最适合数据的函数的问题,限制在找到最适合的二次函数上。这大大简化了问题,因为每个二次函数都完全由三个参数a
,b
和c
定义。因此,要找到最佳二次函数,我们只需要找到a
, b
和c
的最佳值。
如果我们能解决这个二次函数的三个参数的问题,我们就能将同样的方法应用到其他更复杂的具有更多参数的函数上——比如神经网络。让我们先找到f
的参数,然后我们再回来,用一个神经网络对MNIST数据集做同样的事情。
我们首先需要定义“最好”的含义。我们通过选择损失函数来精确地定义这一点,它将根据预测和目标返回一个值,函数的低值对应于“更好的”预测。对于连续数据,通常使用均方根误差:
def mse(prds,tagets): return ((preds - targets)**2).mean().sqrt()
现在,让我们来看看我们的7步过程。
首先,我们将参数初始化为随机值,并使用requires_grad_
告诉PyTorch我们想要跟踪它们的梯度:
params = torch.randn(3).require_grad_()
orig_params = params.clone()
preds = f(time,params)
让我们创建一个小函数来看看我们的预测与目标有多接近,并看看:
def show_preds(preds,ax=None):
if ax is None: ax = plt.subplot()[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
out:tensor(160.6979, grad_fn=)
我们现在的目标是改善这一点。要做到这一点,我们需要知道梯度。
下一步是计算梯度。换句话说,计算参数需要如何改变的近似值:
loss.backword()
params.grad
out:tensor([-165.5151, -10.6402, -0.7900])
params.grad * 1e-5
out: tensor([-1.6552e-03, -1.0640e-04, -7.8996e-06])
我们可以使用这些梯度来改进我们的参数。我们需要选择一个学习速率(我们将在下一章讨论如何在实践中做到这一点;现在我们只使用1e-5,或0.00001):
params #orig_params
out:tensor([-0.7658, -0.7506, 1.3525], requires_grad=True)
现在我们需要根据我们刚刚计算的梯度来更新参数:
lr = 1e-5
params.data -= lr*params.grad.data
params.grad = None
要理解这一点,就得记住"近代史"。为了计算梯度,我们调用loss
的backward
。但loss
本身就是由mse
计算得到的,它把preds
作为输入,preds
又是params
作为输入参数的函数f
的输出, params
是我们最初调用required_grads_
-的对象,这是使我们能够在loss
调用backward
的最根本的调用。这个函数调用链表示函数的数学组合,这使得PyTorch能够在底层使用微积分的链式规则来计算这些梯度。
让我们看看损失是否有所改善:
preds = f(time,params) #使用更新后的参数计算preds
mes(preds,speed)
out:tensor(160.4228, grad_fn=)
看下图像:
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
现在我们进行迭代。通过循环和执行许多改进,我们希望达到一个好的结果:
for i in range(10):apply_step(params)
out:
160.42279052734375
160.14772033691406
159.87269592285156
159.59768676757812
159.3227081298828
159.04774475097656
158.7728271484375
158.4979248046875
158.22305297851562
157.9481964111328
params = orig_params.detach().requires_grad_()
正如我们所希望的,损失正在减少! 但是,仅仅看这些损失数字就可以掩盖这样一个事实:在寻找最佳可能的二次函数的过程中,每次迭代都代表了一个完全不同的二次函数。如果我们不打印出损失函数,而是在每一步画出函数,我们就可以直观地看到这个过程。然后我们可以看到形状是如何接近对于我们的数据来说的最佳二次函数的:
_,axs = plt.subplot(1,4,figsize=(12,3))
for ax in axs: show_pred(apply_step(params,False),ax)
plt.tight
我们只是武断地决定在10个epoch之后停止训练。在实践中,我们将观察训练和验证损失,以及我们的度量来决定何时停止,正如我们已经讨论过的。
总而言之,在一开始,我们的模型的权值可以是随机的(从头开始训练),也可以来自于一个预先训练的模型(迁移学习)。 在第一种情况下,我们从输入中得到的输出与我们想要的没有任何关系,即使在第二种情况下,经过训练的模型也很有可能不能很好地完成我们的目标任务。 所以模型需要学习 更好的权值。
首先,我们使用损失函数 将模型给出的输出与目标(我们已经标记了数据,所以我们知道模型应该给出什么结果)进行比较,该函数返回一个我们希望通过改进权值使其尽可能低的数字。为此,我们从训练集中取一些数据项(如图像),并将它们输入到我们的模型中。我们使用损失函数比较相应的目标,我们得到的分数告诉我们我们的预测有多错误。然后我们稍微改变一下权重,让它变得更好。
为了找到如何改变权值使损失更好一点,我们使用微积分来计算梯度。(实际上,我们让PyTorch为我们做这件事!)让我们打个比方。想象一下,你在山里迷路了,车停在最低处。为了找到回到它的路,你可能会在一个随机的方向上徘徊,但这可能不会有多大帮助。既然你知道你的车在最低点,你最好是下坡。总是朝着最陡的下坡方向迈出一步,你最终会到达你的目的地。我们使用梯度的大小(即斜率的陡度)来告诉我们该走多远的一步;具体来说,我们将梯度乘以一个我们选择的称为学习率的数字来决定步长。然后迭代,直到到达最低点,也就是我们的停车场,然后我们就可以停下来了。
我们已经有了自变量x
,这些是图像本身。我们将把它们连接成一个张量,并将它们从矩阵列表(一个3级张量)变成向量列表(一个2级张量)。我们可以用view
来做这个,这是一个PyTorch方法,它改变了张量的形状而不改变它的内容。-1
是view
的一个特殊的参数,意思是“使这个轴尽可能大以适合所有的数据”:
train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1,28*28)
每张图片需要有一个标签,数字3的标签为1
,7为0
:
train_y = tensor([1]*len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape,train_y.shape
out:(torch.Size([12396, 784]), torch.Size([12396, 1]))
PyTorch中的Dataset
在索引时需要返回一个(x,y)
元组。Python提供了一个zip
函数,当与list
结合时,它提供了一个简单的方法来获得这个功能:
dset = list(zip(train_x,train_y))
x,y = dest[0]
x.shape,y
out:(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))
valid_set = list(zip(valid_x,valid_y))
现在我们需要为每个像素(最初是随机的)赋值(这是我们7步过程中的 初始化 步骤):
def init_params(size,std=1.0):return (torch.randn(size)*std).requires_grad_()
wights = init_params((28*28,1)) #这个形状是为了矩阵运算的规则吧?
权重*像素
的函数不够灵活——当像素等于0时,它总是等于0(即,它的截距
为0)。我们仍然需要b
。我们也将它初始化为一个随机数:
bias = init_params(1)
在神经网络中,方程y=w*x+b
中的w
称为权重,b
称为偏差。权重和偏差一起构成了参数。
我们现在可以对一幅图像进行预测:
(train_x[0]*weights.T).sum() + bias
out:tensor([20.2336], grad_fn=)
虽然我们可以使用Python for
循环来计算每个图像的预测,但这将非常缓慢。因为Python循环不运行在GPU上,而且Python通常是一种很慢的循环语言,所以我们需要在模型中使用更高级的函数来表示尽可能多的计算。
在这种情况下,有一种非常方便的数学运算,它为矩阵的每一行计算w*x
——它被称为矩阵乘法
。
在Python中,矩阵乘法用@
操作符表示。 让我们试一试:
def linearl(xb): return xb*weights+bias
preds = linear1(train_x)
preds
out:
tensor([[20.2336],
[17.0644],
[15.2384],
...,
[18.3804],
[23.8567],
[28.6816]], grad_fn=)
第一个元素和我们之前计算的是一样的。这个方程batch@weights + bias
,是任何神经网络的两个基本方程之一(另一个是激活函数
,我们一会儿就会看到)。
让我们检查一下我们的准确性。为了决定输出是3还是7,我们可以检查它是否大于0.5,所以我们可以用以下方法计算每张图片的准确率(使用广播,所以没有循环!)
corrects = (preds > 0.5).float() == train_y
corrects
out:
tensor([[ True],
[ True],
[ True],
...,
[False],
[False],
[False]])
corrects.float().mean().item()
out:0.49080348014831543
现在让我们看看权重的一个小变化会带来怎样的准确性变化:
weights[0] *= 1.0001
preds = linear1(train_x)
((preds > 0.0).float() == train_y).float().mean().item()
out: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,模型将无法从这个数字中学习。
用数学术语来说,准确率是一个几乎在所有地方都是常数的函数(除了在阈值0.5处),所以它的导数几乎在所有地方都是零(在阈值处是无穷大)。这就给出了0或无限的梯度,这对于更新模型是无用的。
相反,我们需要一个损失函数,当我们的权重能带来更好的预测时,我们就能得到更好的损失。那么,“稍微好一点的预测”到底是什么样的呢?在这种情况下,这意味着如果正确答案是3,分数就会高一些,如果正确答案是7,分数就会低一些。
现在我们来写一个这样的函数。它的形式是什么?
损失函数接收的不是图像本身,而是模型的预测。让我们用一个参数,prds
,表示0到1之间的值,其中每个值都表示对图像为3的预测。它是一个向量(即1级张量),在图像上建立索引。
损失函数的目的是测量预测值和真实值之间的差异——也就是目标值(也就是标签)。让我们做另一个参数,trgts
,值为0或1,它告诉图像实际上是不是3。它也是一个向量(即,另一个秩1张量),在图像上索引。
举个例子,假设我们有三个图像,我们知道是3,7和3。假设我们的模型有很高的信心(0.9)预测第一个是3,有轻微的信心(0.4)预测第二个是7,有相当的信心(0.2)预测第三个是7。这意味着我们的损失函数将接收这些值作为其输入:
trgts = tensot([1,0,1])
preds = tensor([0.9,0.4,0.2])
以下是第一次尝试测量预测和目标之间的距离的损失函数:
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的距离,然后取所有这些距离的均值。
注意:阅读文档: 像这样学习PyTorch函数是很重要的,因为在Python中循环张量的速度是Python的,而不是C/CUDA的速度! 现在试着运行help(torch.where)来阅读这个函数的文档,或者,更好的是,在PyTorch文档站点上查找它。
让我们试试我们的prds
和trgts
:
torch.where(trgts==1, 1-prds, prds)
out: tensor([0.1000, 0.4000, 0.8000])
您可以看到,当预测更准确,这个函数返回的数字更低。在PyTorch中,我们总是假设损失函数的值越低越好。由于最终损失需要一个标量,因此mnist_loss
取前一个张量的均值:
mnist_loss(prds,trgts)
out: tensor(0.4333)
例如,如果我们将对一个“错误”目标的预测从0.2改为0.8,损失将会下降,这表明这是一个更好的预测:
mnist_loss(tensor([0.9,0.4,0.8]),trgts)
out:tensor(0.2333)
当前定义的mnist_loss
的一个问题是,它假设预测总是在0到1之间。因此,我们需要确保这是真的! 事实上,有一个函数就是这么做的,让我们来看看。
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, trgets):
predictions = predictions.sigmoid()
return torch.where(targets==1 , 1-predictions ,predictions).mean()
现在我们可以确信我们的损失函数是可行的,即使预测值不在0到1之间。所需要的只是更高的预测对应更高的置信度认为图像就是3。
在定义了损失函数之后,现在是时候回顾一下我们这么做的原因了。毕竟,我们已经有了一个度量标准,即总体精度。那么我们为什么要定义损失呢?
关键的区别在于,度量是为了推动人类的理解,而损失是为了推动自动学习。 为了推动自动学习,损失必须是一个具有有意义的导数的函数。 它不能有大的平坦部分和大的跳跃,而是必须相当平滑。 这就是为什么我们设计了一个损失函数,可以对信心水平的微小变化做出反应。 这一要求意味着,有时候它并不能真正准确地反映我们试图实现的目标,而是我们真正的目标和一个可以使用其梯度进行优化的函数之间的妥协。 我们为数据集中的每一项计算loss函数,然后在epoch结束时,对所有的loss值求平均值,并报告epoch的总体平均值。
另一方面,参数是我们真正关心的数字。这些是在每个epoch结束时打印出来的值,它们告诉我们模型的实际运行情况。在判断模型的性能时,重要的是我们学会关注这些指标,而不是损失。
现在我们有了一个适合驱动SGD的损失函数,我们可以考虑下一阶段学习过程中涉及的一些细节,即基于梯度改变或更新权值。这被称为优化步骤。
为了采取优化步骤,我们需要计算一个或多个数据项的损失。我们应该用多少? 我们可以为整个数据集计算它,然后取平均值,或者我们可以为单个数据项计算它。但这两种方法都不理想。为整个数据集计算它将花费非常长的时间。为单个项目计算它不会使用太多的信息,所以它会导致一个非常不精确和不稳定的梯度。也就是说,您将遇到更新权重的麻烦,但只考虑如何改善模型在单个项目上的性能。
因此,我们在两者之间做出妥协: 每次计算几个数据项的平均损失。这称为小批处理 (mini-batches)。小批处理中数据项的数量称为批大小(batch size)。更大的批处理规模意味着您将从损失函数中获得更准确和稳定的数据集梯度估计,但这将花费更长的时间,并且每个epoch将处理更少的小批处理。选择一个好的批处理大小是作为深度学习实践者需要做出的决策之一,以便快速准确地训练模型。我们将在本书中讨论如何做出这种选择。
使用小批量而不是在单个数据项上计算梯度的另一个原因是,在实践中,我们几乎总是在GPU等加速器上进行训练。这些加速器只有在一次有很多工作要做的时候才会表现得很好,所以如果我们可以给它们很多数据项来处理,这是很有帮助的。使用小批量是最好的方法之一。但是,如果您给它们提供了太多的数据,让它们一次无法工作,它们就会耗尽内存——让gpu高兴也是很棘手的!
正如我们在数据增强的讨论中所看到的,如果我们能在训练过程中改变一些东西,我们就能得到更好的泛化。我们可以改变的一件简单而有效的事情是我们在每个小批处理中放入的数据项。在创建小批量之前,我们通常不是简单地为每个epoch按顺序枚举我们的数据集,而是在每个epoch上随机打乱。PyTorch和fastai提供了一个名为DataLoader
的类,它将为你做洗牌和小批量整理。
DataLoader
可以接受任何Python集合,并将其转换为多个批的迭代器,如下所示:
coll = range(15)
dl = DataLoader(coll,batch_size = 5,shuffle = True)
list(dl)
out:
[tensor([ 3, 12, 8, 10, 2]),
tensor([ 9, 4, 7, 14, 5]),
tensor([ 1, 13, 0, 6, 11])]
为了训练模型,我们不仅需要任何Python集合,还需要包含自变量和因变量的集合(即模型的输入和目标)。包含自变量和因变量元组的集合在PyTorch中称为数据集(Dataset)
。下面是一个非常简单的数据集示例:
ds = L(enumerate(string.ascii_lowercase))
ds
out:(#26) [(0, ‘a’),(1, ‘b’),(2, ‘c’),(3, ‘d’),(4, ‘e’),(5, ‘f’),(6, ‘g’),(7, ‘h’),(8, ‘i’),(9, ‘j’)…]
当我们将数据集
传递给DataLoader
时,我们将返回许多批,这些批本身就是张量的元组,表示独立变量和因变量的批:
dl = DataLoader(ds, batch_size= 6,shuffle=True)
dl
out:
[(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为模型编写我们的第一个训练循环!
现在是时候实现我们在梯度下降那一节中看到的流程了。在代码中,我们的进程将在每个epoch中实现如下内容:
for x,y in dl:
pred = model(x)
loss = loss_fun(pred,y)
loss.backward()
parameters -= parameters.grad*lr
首先,让我们再次初始化我们的参数:
wights = init_params((28*28,1))
bias = init_params(1)
DataLoader
可以从Dataset
中创建:
dl = DataLoader(dset,batch_size = 256)
xb,yb = fist(dl)
xb.shape,yb.shape
out: (torch.Size([256, 784]), torch.Size([256, 1]))
对验证集也做相同的操作:
valid_dl = DataLoader(valid_dset,batch_size=256)
让我们创建一个用于测试的大小为4的小批量数据集:
batch = train_x[:4]
batch.shape
out: torch.Size([4, 784])
preds = linear1(batch)
preds
out:
tensor([[-2.1876],
[-8.3973],
[ 2.5000],
[-4.9473]], grad_fn=)
loss = minist_loss(preds, train_y[:4])
loss
out: tensor(0.7419, grad_fn=)
现在我们可以计算梯度了:
loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad
out: (torch.Size([784, 1]), tensor(-0.0061), tensor([-0.0420]))
让我们把所有的代码放在一个函数中:
def calc_grad(xb,xy,model):
preds = model(xb)
loss = mnist_loss(preds, yb)
loss.backward()
测试一下:
calc_grad(batch,train_y[:4],linear1)
weights.grad.mean(),bias.grad
out: (tensor(-0.0121), tensor([-0.0840]))
但是如果我们调用两次会发生什么呢?
calc_grad(batch,train_y[:4],linear1)
weights.grad.mean(),bias.grad
out: (tensor(-0.0182), tensor([-0.1260]))
梯度已经改变了! 原因是loss.backward
实际上是将损失梯度添加 到当前存储的任何梯度中。所以,我们必须先将当前的梯度设置为0:
weights.grad.zero_()
bias.grad.zero_();
Inplace操作:PyTorch中名称以下划线结尾的方法就地修改它们的对象。例如,
bias.zero_()
将张量bias
的所有元素都设为0。
我们剩下的唯一步骤就是根据梯度和学习速率更新权重和偏差。当我们这样做时,我们必须告诉PyTorch不要采取这一步的梯度——否则,当我们试图在下一批计算导数时,事情会变得非常混乱! 如果我们给一个张量的data
属性赋值,那么PyTorch将不会采用这一步的梯度。以下是一个epoch的基本训练循环:
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。所以我们对每个项目的准确性可以通过以下方法计算(使用广播,所以没有循环!)
(preds >0.0).float()==train_y[:4]
out:
tensor([[False],
[False],
[ True],
[False]])
这给了我们这个函数来计算我们的验证集精度:
def batch_accuracy(xb,yb):
preds = xb.sigmoid() #?不应该是preds = model(xb).sigmoid()吗?下面的cell有纠正这一点
correct = (preds > 0.5) == yb
return correct.float().mean()
我们可以检查它的工作:
batch_accuracy(linear1(batch),train_y[:4])
out: tensor(0.2500)
然后把这些批次放在一起:
def validate_epoch(model):
accs = [batch_accuracy(model(xb),yb) for xb,yb in valid_dl] #这里batch_size=256
return round(torch.stack(accs).mean().item(),4)
validate_epoch(linear1)
out: 0.5261
这是我们的起点。让我们训练一个epoch,看看准确性是否提高:
lr = 1.
params = weights,bias
train_epoch(linear1,lr,params)
validate_epoch(linear1)
out: 0.6663
然后再做一些:
for i in range(20):
train_epoch(linear1,lr,params)
print(validate_epoch(linear1),end=' ')
out:
0.8265 0.89 0.9183 0.9275 0.9398 0.9466 0.9505 0.9525 0.9559 0.9579 0.9598 0.9608 0.9613 0.9618 0.9633 0.9637 0.9647 0.9657 0.9672 0.9677
看上去不错! 我们已经达到了与“像素相似度”方法相同的精度,并且我们已经创建了一个通用的基础。我们的下一步将是创建一个对象,该对象将为我们处理SGD步骤。在PyTorch中,它被称为optimizer(优化器)
。
因为这是一个通用的基础,所以PyTorch提供了一些有用的类来简化它的实现。我们可以做的第一件事是用PyTorch的nn.Linear
替换linear1
。模块是继承自PyTorch nn.Model
类的类对象。该类的对象的行为与标准Python函数相同,因为你可以使用括号调用它们,它们将返回模型的激活状态。
nn.Linear
做了和我们的init_params
和linear1
一起做的事。它在一个类中包含了权重和偏置。在这里我们这样重复我们的上一节的模型:
linear_model = nn.Linear(28*28,1)
每个PyTorch模块都知道它有哪些可以训练的参数; 它们可以通过参数方法获得:
w,b = linear_model.paramters()
w.shape,b.shape
out: (torch.Size([1, 784]), torch.Size([1]))
我们可以用这个信息来创建一个优化器:
class BasicOpitm:
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 = BasicOpitm(linear_model.parameters(), lr)
我们的训练循环现在可以简化为:
def train_loop(model):
for xb,yb in dl:
calc_grad(xb,yb,model)
opt.step()
opt.sero_grad()
我们的验证函数根本不需要更改:
validate_epoch(linear_model)
out: 0.4608
让我们把我们的小训练循环放在一个函数中,让事情变得更简单:
def train_model(model, epochs):
for i in range(epochs):
train_epoch(model)
print(validate_epoch(model),end=' ')
结果与前一节相同:
train_model(linear_model,20)
out: 0.4932 0.7685 0.8554 0.9135 0.9345 0.9482 0.957 0.9633 0.9657 0.9677 0.9696 0.9716 0.9736 0.9745 0.976 0.977 0.9775 0.9775 0.978 0.9785
fastai提供了SGD
类,默认情况下,它做的事情和我们的BasicOptim
一样:
linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(),lr)
train_model(linear_model, 20)
out: 0.4932 0.8178 0.8496 0.914 0.9345 0.9482 0.957 0.9618 0.9657 0.9672 0.9692 0.9711 0.9741 0.975 0.976 0.9775 0.9775 0.978 0.9785 0.9789
fastai也提供Learner.fit
, 我们可以用它代替train_model
。为了创造一个Leaner
, 我们首先需要通过传递我们的训练集和验证集DataLoader
来创建一个DataLoaders
。
dls = DataLoaders(dl, valid_dl)
要在不使用应用程序(如cnn_learner
)的情况下创建一个Learner
,我们需要传递我们在本章中创建的所有元素:DataLoaders
、模型、优化函数(将传递参数)、loss函数,以及可选的任何要打印的评估指标:
learn = Learner(dls, nn.Linear(28*28,1),opt_func=SGD,loss_func=mnist_loss, metrics=batch_accuracy)
现在,我们调用fit
:
learn.fit(10, lr=lr)
out:
epoch | train_loss | valid_loss | batch_accuracy | time |
---|---|---|---|---|
0 | 0.636709 | 0.503144 | 0.495584 | 00:00 |
1 | 0.429828 | 0.248517 | 0.777233 | 00:00 |
2 | 0.161680 | 0.155361 | 0.861629 | 00:00 |
3 | 0.072948 | 0.097722 | 0.917566 | 00:00 |
4 | 0.040128 | 0.073205 | 0.936212 | 00:00 |
5 | 0.027210 | 0.059466 | 0.950442 | 00:00 |
6 | 0.021837 | 0.050799 | 0.957802 | 00:00 |
7 | 0.019398 | 0.044980 | 0.964181 | 00:00 |
8 | 0.018122 | 0.040853 | 0.966143 | 00:00 |
9 | 0.017330 | 0.037788 | 0.968106 | 00:00 |
正如您所看到的,PyTorch和fastai类没有什么神奇之处。它们只是方便的预包装件,让你的生活更轻松! (它们还提供了许多额外的功能,我们将在以后的章节中使用。)
有了这些类,我们现在可以用神经网络代替线性模型。
到目前为止,我们有一个优化函数参数的通用程序,我们已经在一个非常无聊的函数上进行了试验:一个简单的线性分类器。线性分类器的功能受到很大的限制。为了使它更复杂一点(并能够处理更多的任务),我们需要在两个线性分类器之间添加一些非线性的东西——这就是神经网络。
以下是一个基础的神经网络的完整定义:
def simple_net(xb):
res = xb@w1 + b1
res = res.max(tensor(0.0))
res = res@w2 + b2
return res
就是这样! 我们在simple_net
中所拥有的只是两个线性分类器,它们之间有一个max
函数。
其中,w1
和w2
是权重张量,b1
和b2
是偏置张量;它们是随机初始化过的参数,就像我们在之前节做的那样:
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))
被称为rectified linear unit,也称为ReLU。我们认为我们都同意,rectified linear unit听起来相当奇特和复杂……但实际上,它除了res.max(tensor(0.0))
之外没有别的东西——换句话说,就是用0替换每个负数。这个小函数也可以在PyTorch中作为F.relu
使用:
plot_function(F.relu)
J:深度学习中有大量的术语,包括rectified linear unit等术语。正如我们在本例中看到的那样,绝大多数术语并不比用一小行代码就能实现复杂。现实情况是,学者们要想发表论文,就需要让他们的论文听起来尽可能地让人印象深刻和复杂。其中一种方法就是引入行话。不幸的是,这导致该领域变得更加令人生畏和难以进入。你必须学习术语,否则论文和教程对你来说就没有什么意义了。但这并不意味着你必须发现术语令人生畏。只要记住,当你遇到一个你以前没有见过的单词或短语时,它几乎肯定是指一个非常简单的概念。
基本的想法是,通过使用更多的线性层,我们可以让我们的模型做更多的计算,因此可以建模更复杂的函数。但是把一个线性层直接放在另一个线性层后面是没有意义的,因为当我们把东西相乘然后再把它们相加多次时,可以用把不同的东西相乘然后相加一次来代替!也就是说,一系列任意数量的连续线性层可以用具有不同参数集的单一线性层代替。
但是如果我们在它们之间放一个非线性函数,比如max
,那么这个就不再成立了。现在,每一个线性层实际上是与其他层解耦的,并且可以做自己有用的工作。max
函数特别有趣,因为它的操作就像一个简单的if
语句。
S:数学上,我们说两个线性函数的复合是另一个线性函数。所以,我们可以把任意多的线性分类器叠加在一起,如果它们之间没有非线性函数,就和一个线性分类器是一样的。
令人惊讶的是,我们可以从数学上证明这个小函数可以以任意高的精度解决任何可计算的问题,如果你能找到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
函数完全相同。可以出现在模型中的大多数函数也具有相同的模块形式。一般来说,它只是用nn替换F并改变大小写。当使用nn.Sequential
的时候,PyTorch要求我们使用模块版本。因为模块是类,所以我们必须实例化它们,这就是为什么你会在这个例子中看到nn.ReLU()。
因为nn.Sequential
是一个模块,我们可以获取它的参数,它将返回它所包含的所有模块的所有参数的列表。让我们试一试! 由于这是一个更深的模型,我们将使用较低的学习速率和更多的epoch。
leaner = Leaner(dls, simple_net, opt_func=SGD, loss_func=mnist_loss, metrics=batch_accuracy)
Learn.fit(40,0.1)
Out:
Epoch | Train_loss | Valid_loss | Batch_accuracy | Time |
---|---|---|---|---|
0 | 0.333021 | 0.396112 | 0.512267 | 00:00 |
… | … | … | … | … |
39 | 0.014168 | 0.020576 | 0.982336 | 00:00 |
为了节省空间,我们没有在这里显示40行输出;训练过程记录在learn.recorder
中。输出表存储在values
属性中,因此我们可以将训练的准确性绘制为:
plt.plot(L(learn.recorder.values).itemgot(2));#就是取上表的前两列作为横轴和纵轴的意思吧。
Out:
我们可以查看最终的(训练集准确率):
Learn.recorder.values[-1][2]
Out: 0.98233562707901
现在,我们有了一些相当神奇的东西:
1.一个函数,它可以解决任何问题,任何精度水平(神经网络)
2.给定正确的一组参数求任意函数最佳参数集的一种方法(随机梯度下降法)
这就是为什么深度学习可以做一些看起来很神奇,很奇妙的事情。相信这种简单技术的组合可以真正解决任何问题,是我们发现许多学生必须采取的最大步骤之一。这似乎好得令人难以置信——事情肯定应该比这更困难和复杂吧?我们的建议:尝试一下!我们刚刚在MNIST数据集上尝试了它,您已经看到了结果。因为我们自己从头开始做所有的事情(除了计算梯度),你知道没有什么特殊的魔法隐藏在幕后。
没有必要只停留在两个线性层上。只要我们在每一对线性层之间加上一个非线性,我们想加多少就加多少。然而,正如您将了解到的,模型越深入,在实践中优化参数就越困难。在本书的后面,您将学习一些简单但非常有效的技术来训练更深层的模型。
我们已经知道,一个带有两个线性层的非线性足以近似任何函数。那么我们为什么要使用更深层的模型呢? 原因是性能。对于更深层的模型(即有更多层的模型),我们不需要使用太多的参数; 事实证明,我们可以使用更小的矩阵和更多的层,得到比我们得到更大的矩阵和更少的层更好的结果。
这意味着我们可以更快地训练模型,它将占用更少的内存。在20世纪90年代,研究人员非常关注通用近似定理,以至于很少有人对不止一个非线性进行实验。这一理论而非实践基础阻碍了该领域多年的发展。然而,一些研究人员对深层模型进行了实验,最终能够证明这些模型在实践中表现得更好。最终,理论结果显示了为什么会发生这种情况。今天,很少有人使用只有一个非线性的神经网络。当我们使用相同方法训练一个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)
Out:
epoch | train_loss | valid_loss | accuracy | time |
---|---|---|---|---|
0 | 0.146566 | 0.028014 | 0.996075 | 00:14 |
接近100%的准确率! 这与我们简单的神经网络有很大的不同。但是,正如你将在本书的其余部分学到的,你需要使用一些小技巧来获得如此好的结果。你已经知道了关键的基础部分。(当然,即使您知道了所有的技巧,您也几乎总是想要使用PyTorch和fastai提供的预构建类,因为它们使您不必自己考虑所有的小细节。)
祝贺您: 您现在知道如何从头创建和训练深度神经网络了! 我们已经经历了相当多的步骤来达到这一点,但您可能会惊讶于它实际上是多么简单。
现在我们到了这个阶段,这是一个很好的机会来定义和回顾一些术语和关键概念。
神经网络包含很多数字,但它们只有两种类型:被计算的数字,以及这些数字的计算参数。
这给了我们两个最重要的行话需要学习:
· 激活:被计算的数字(通过线性和非线性层)
· 参数:随机初始化和优化的数字(即定义模型的数字)
在本书中,我们将经常讨论激活和参数。记住它们有非常具体的含义。他们是数字。它们不是抽象的概念,而是你的模型中实际存在的具体数字。成为一名优秀的深度学习实践者的一部分,是要习惯于实际观察你的激活和参数,并绘制它们,测试它们是否表现正确。
我们的激活和参数都包含在张量中。这些只是规则形状的数组——例如,一个矩阵。
矩阵有行和列;我们称这些为轴 或维度。张量的维数就是它的秩。有一些特殊的张量:
· 零阶:标量
· 一阶:向量
· 二阶:矩阵
神经网络包含许多层。每一层都是线性 或非线性 的。在神经网络中,我们通常交替使用这两种层。有时人们把一个线性层和它的后续非线性一起称为一个单层。是的,这令人困惑。有时非线性被称为激活函数。
以下总结了SGD中的关键概念:
术语 | 含义 |
---|---|
Relu | 对于负数返回0,对于正数不做改动的函数 |
Mini-batch | 一个由输入和标签组合成两个数组的小组。随机梯度下降的一步就是在这个批上进行更新的,而不是基于整个epoch。 |
Forward pass | 将该模型应用于某些输入并计算预测结果。 |
Loss | 表示我们的模型做得好(或不好)的值。 |
Gradient(梯度) | 损失关于模型某些参数的导数。 |
Backward pass | 计算损失相对于所有模型参数的梯度。 |
Gradient desent | 在与梯度相反的方向上迈出一步,使模型参数更好一些。 |
Learning rate 学习率 | 应用SGD更新模型参数时所采取的步长的大小。 |
Questionnaire
电脑上是如何显示灰度图片的?那彩色图片呢?
灰度:背景白色像素存储为数字0,黑色存储为数字255,灰色阴影位于两者之间。
彩色:用(R,G,B)表示每个像素的色彩,取值范围为(0255,0255,0~255)。
MINIST_SAMPLE数据集的文件和文件夹是如何组织起来的?为什么这样组织?
训练集和验证集(和/或测试集)分别放在不同的文件夹中。
解释为什么“像素相似”方法可以对数字分类。
通过计算训练集中每种数字的平均样子,然后看待分类数字和平均样子的远近来判断属于哪个类。
什么是列表推导式?现在创建一个从列表中选择奇数并且使它们翻倍的list 列表推导式。
列表推导式和字典推导式是Python的一个很棒的特性。它包括三个部分:正在迭代的集合、一个可选的过滤器和对每个元素做的操作。它不仅写起来更短,而且比用循环创建相同列表的其他方法快得多。
a = [1,2,3]
b = [i*2 for i in a if i % 2 != 0]
什么是“三阶张量”?
有三个索引轴的张量
张量的阶数和形状有什么区别?你如何从形状得知阶数?
张量的阶数是索引轴的数量,形状是每个轴的长度。形状的长度就是阶数。
什么是RMSE和L1范数?
RMSE:均方根误差,也叫L2范数
L1范数:平均绝对值误差
当一次计算上千个数时,你如何做到比Python循环快上几千倍?
使用广播运算,这会调用C进行运算,比单纯使用Python循环快上千倍
创建一个包含1到9的3×3的张量或数组。使它翻倍。选择右下角的四个数字。
a = tensor(range(1,10)).reshape(3,-1)*2
print(a)
a[-2:,-2:]
什么是广播运算?
当PyTorch试图在两个不同等级的张量之间执行简单的减法操作时,它会使用广播。也就是说,它会自动展开小阶张量使其大小与大阶张量相同。广播是使张量代码更容易编写的一个重要功能。
评估(metrics)通常是使用训练集计算得到的还是验证集?为什么?
验证集,如果使用训练集,由于已经训练好了,评估的效果当然很好,我们应该用模型没有见过的数据进行评估。
什么是随机梯度下降?(SGD)
随机梯度下降是通过自动更新权值来学习的机制,它通过在小批量数据上进行梯度更新,而不是一次计算整个数据集的梯度,使计算更快速。
为什么SGD使用小批数据?
为了采取优化步骤,我们需要计算一个或多个数据项的损失(loss)。我们应该用多少数据项? 我们可以为整个数据集计算它,然后取平均值,或者我们可以为单个数据项计算它。但这两种方法都不理想。为整个数据集计算它将花费非常长的时间。为单个项目计算它不会使用太多的信息,所以它会导致一个非常不精确和不稳定的梯度。也就是说,您将遇到更新权重的麻烦,但只考虑如何改善模型在单个项目上的性能。
因此,我们在两者之间做出妥协: 每次计算几个数据项的平均损失。这称为***小批处理 (mini-batches)***。小批处理中数据项的数量称为批大小(batch size)。*更大的批处理规模意味着您将从损失函数中获得更准确和稳定的数据集梯度估计,但这将花费更长的时间,*并且每个epoch将处理更少的小批处理。选择一个好的批处理大小是作为深度学习实践者需要做出的决策之一,以便快速准确地训练模型。我们将在本书中讨论如何做出这种选择。
使用小批量而不是在单个数据项上计算梯度的另一个原因是,在实践中,我们几乎总是在GPU等加速器上进行训练。这些加速器只有在一次有很多工作要做的时候才会表现得很好,所以如果我们可以给它们很多数据项来处理,这是很有帮助的。使用小批量是最好的方法之一。但是,如果您给它们提供了太多的数据,让它们一次无法工作,它们就会耗尽内存——让gpu高兴也是很棘手的!
机器学习中的SGD的七个步骤是什么?
如何初始化模型中的权重?
对参数赋值
什么是“损失(loss)”?
预测值和实际值的差距
为什么我们不能总是使用高学习率?
可能造成反复震荡,迭代很多次才能达到收敛,或者根本不收敛,loss越来越大。
什么是“梯度”?
告诉我们哪里是函数的下降(或上升)方向,以及下降(或上升)的幅度
你需要知道怎么亲自算梯度吗?
不需要,Pytorch为我们自动计算梯度,只需要使计算梯度的张量调用requires_grad_()
函数即可
我们为什么不能用准确率作为损失函数?
因为权重的变化可能不能导致准确率的变化,这样就对我们下一次调整权重没有什么影响。
画出sigmoid函数。它的形状有什么特别的?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mgEgqyqk-1643873149052)(D:\研究僧\AI\DL\images\sigmoid.png)]
特别地光滑,单调上升,输出值在0-1之间
损失函数和metric的区别是什么?
损失函数用于机器的自动学习,metric用于给人展示模型的表现
使用学习率去计算新权重的函数叫什么?
step()
DataLoader
函数是干嘛的?在创建小批量之前,我们通常不是简单地为每个epoch按顺序枚举我们的数据集,而是在每个epoch上随机打乱。PyTorch和fastai提供了一个名为DataLoader
的类,它将为你做洗牌(shuffle)和小批量整理(batch-size)。
DataLoader
可以接受任何Python集合,并将其转换为多个批的迭代器。
当我们将数据集
传递给DataLoader
时,我们将返回许多批,这些批本身就是张量的元组。
DataLoader
可以从Dataset
中创建。
写出SGD中每一个epoch的基础步骤的伪代码。
for x,y in dl:
pred = model(x)
loss = loss_fun(pred,y)
loss.backward()
parameters -= parameters.grad*lr
写出一个函数,输入两个参数[1,2,3,4]
和abcd
,它可以返回[(1,'a'),(2,'b'),(3,'c'),(4,'d)]
。输出的数据结构有什么特别之处?
def z(index,value):
c=[(i,j) for i,j in list(zip(index,value))]
return c
index = [1,2,3,4]
value='abcd'
c = z(index,value)
c
输出的结构是两个参数的元素一一组合而成的元组的列表
view
是做什么用的?重构tensor的形状。并且可以仅指定一些轴的大小,剩下的轴用-1
代替,函数也能根据目前的大小自动计算剩下的轴的大小。
仅仅是weights*x
的函数就太简单了,一般会在线性函数后面加上偏置
@
运算符是做什么的?矩阵乘法
backward()
方法是做什么的?计算参数的梯度
为什么我们需要对梯度重新置为0?
改变了权重后,原来的梯度不再有意义,为了继续进行梯度下降,要重新计算梯度。
loss.backward
实际上是将损失梯度添加 到当前存储的任何梯度中。所以,我们必须先将当前的梯度设置为0
我们需要给Learner
传递哪些参数?
DataLoaders、模型(层的组合)、loss函数、优化器、mertic
给出训练循环中的基本步骤的Python代码或伪代码。
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_()
什么是“ReLU”?画出一个值从-2
到2
的图像。
Rectified Linear Unit, 就是把所有负数映射为0,所有正数不变的函数。
plot_function(nn.Relu,(-2,2))
什么是“激活函数”?
模型中的非线性层,比如Relu、max函数
F.relu
和nn.ReLU
的区别何在?
F.Relu是一个函数,nn的Relu是一个模块(类),可以调用相应的属性。
nn.ReLU
是一个PyTorch模块,它的功能与F.relu
函数完全相同。可以出现在模型中的大多数函数也具有相同的模块形式。一般来说,它只是用nn替换F并改变大小写。当使用nn.Sequential
的时候,PyTorch要求我们使用模块版本。因为模块是类,所以我们必须实例化它们,这就是为什么你会在这个例子中看到nn.ReLU()。
普遍逼近定理表明,任何函数都可以用一个非线性来近似。那么为什么我们通常使用更多层呢?
更高效,计算更快。
我们已经知道,一个带有两个线性层的非线性足以近似任何函数。那么我们为什么要使用更深层的模型呢? 原因是性能。对于更深层的模型(即有更多层的模型),我们不需要使用太多的参数; 事实证明,我们可以使用更小的矩阵和更多的层,得到比我们得到更大的矩阵和更少的层更好的结果。
这意味着我们可以更快地训练模型,它将占用更少的内存。
根据本章内容,从头创建你的Learner
写出一个函数,输入两个参数[1,2,3,4]
和abcd
,它可以返回[(1,'a'),(2,'b'),(3,'c'),(4,'d)]
。输出的数据结构有什么特别之处?
def z(index,value):
c=[(i,j) for i,j in list(zip(index,value))]
return c
index = [1,2,3,4]
value='abcd'
c = z(index,value)
c
输出的结构是两个参数的元素一一组合而成的元组的列表
view
是做什么用的?重构tensor的形状。并且可以仅指定一些轴的大小,剩下的轴用-1
代替,函数也能根据目前的大小自动计算剩下的轴的大小。
仅仅是weights*x
的函数就太简单了,一般会在线性函数后面加上偏置
@
运算符是做什么的?矩阵乘法
backward()
方法是做什么的?计算参数的梯度
为什么我们需要对梯度重新置为0?
改变了权重后,原来的梯度不再有意义,为了继续进行梯度下降,要重新计算梯度。
loss.backward
实际上是将损失梯度添加 到当前存储的任何梯度中。所以,我们必须先将当前的梯度设置为0
我们需要给Learner
传递哪些参数?
DataLoaders、模型(层的组合)、loss函数、优化器、mertic
给出训练循环中的基本步骤的Python代码或伪代码。
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_()
什么是“ReLU”?画出一个值从-2
到2
的图像。
Rectified Linear Unit, 就是把所有负数映射为0,所有正数不变的函数。
plot_function(nn.Relu,(-2,2))
什么是“激活函数”?
模型中的非线性层,比如Relu、max函数
F.relu
和nn.ReLU
的区别何在?
F.Relu是一个函数,nn的Relu是一个模块(类),可以调用相应的属性。
nn.ReLU
是一个PyTorch模块,它的功能与F.relu
函数完全相同。可以出现在模型中的大多数函数也具有相同的模块形式。一般来说,它只是用nn替换F并改变大小写。当使用nn.Sequential
的时候,PyTorch要求我们使用模块版本。因为模块是类,所以我们必须实例化它们,这就是为什么你会在这个例子中看到nn.ReLU()。
普遍逼近定理表明,任何函数都可以用一个非线性来近似。那么为什么我们通常使用更多层呢?
更高效,计算更快。
我们已经知道,一个带有两个线性层的非线性足以近似任何函数。那么我们为什么要使用更深层的模型呢? 原因是性能。对于更深层的模型(即有更多层的模型),我们不需要使用太多的参数; 事实证明,我们可以使用更小的矩阵和更多的层,得到比我们得到更大的矩阵和更少的层更好的结果。
这意味着我们可以更快地训练模型,它将占用更少的内存。
Learner