2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 【Pytorch with fastai】第 5 章 :图像分类

【Pytorch with fastai】第 5 章 :图像分类

时间:2022-02-11 04:00:41

相关推荐

【Pytorch with fastai】第 5 章 :图像分类

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

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

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

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

🖍foreword

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

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

文章目录

从狗和猫到宠物品种

Presizing(加压)

检查和调试数据块

交叉熵损失

查看激活和标签

Softmax

对数似然

记录日志

模型解释

改进我们的模型

学习率查找器

解冻和迁移学习

判别学习率

选择Epoch数

更深层次的架构

结论

现在您已经了解了深度学习是什么、它的用途以及如何创建和部署模型,是时候让我们更深入地学习了!在一个理想的世界里,深度学习从业者不必知道事情如何在幕后工作的每一个细节。但到目前为止,我们还没有生活在一个理想的世界里。事实是,要使您的模型真正工作并可靠地工作,您必须正确处理很多细节,并且必须检查很多细节。这个过程需要能够在你的神经网络训练和预测时查看它的内部,发现可能的问题,并知道如何解决它们。

因此,从这里开始,我们将深入探讨深度学习的机制。计算机视觉模型、NLP 模型、表格模型等的架构是什么?您如何创建一个符合您特定领域需求的架构?您如何从培训过程中获得最佳结果?你如何让事情变得更快?随着数据集的变化,你必须改变什么?

我们将首先重复我们在第一章中看到的相同的基本应用程序,但我们要做两件事:

让他们变得更好。

将它们应用于更广泛的数据类型。

要做到这两件事,我们必须学习深度学习难题的所有部分。这包括不同类型的层、正则化方法、优化器、如何将层组合成架构、标签技术等等。不过,我们不只是把所有这些东西都扔给你;我们将根据需要逐步引入它们,以解决与我们正在进行的项目相关的实际问题。

从狗和猫到宠物品种

在我们的第一个模型中,我们学习了如何对狗和猫进行分类。就在几年前,这还被认为是一项非常具有挑战性的任务——但今天,这太容易了!我们将无法向您展示具有此问题的训练模型的细微差别,因为我们无需担心任何细节即可获得近乎完美的结果。但事实证明,相同的数据集还允许我们解决一个更具挑战性的问题:找出每张图像中显示的宠物品种。

在第 1 章中,我们将应用程序视为已解决的问题。但这不是现实生活中的事情。我们从一个我们一无所知的数据集开始。然后我们必须弄清楚它是如何组合在一起的,如何从中提取我们需要的数据,以及这些数据是什么样的。在本书的其余部分,我们将向您展示如何在实践中解决这些问题,包括理解我们正在使用的数据并随时测试您的建模所需的所有中间步骤。

我们已经下载了 Pets 数据集,我们可以得到它的路径数据集使用与第 1 章相同的代码:

from fastai.vision.all import *path = untar_data(URLs.PETS)

现在,如果我们要了解如何提取每只宠物的品种从每张图片中,我们将需要了解这些数据是如何布局的。数据布局的这些细节是深度学习难题的重要组成部分。数据通常以以下两种方式之一提供:

表示数据项的单个文件,例如文本文档或图像,可能组织成文件夹或文件名代表有关这些项目的信息

一个数据表(例如,CSV 格式),其中每一行都是一个项目,并且可能包括文件名,提供数据之间的连接表格中的数据和其他格式的数据,例如文本文档和图像

这些规则也有例外——特别是在基因组学等领域,其中可能存在二进制数据库格式甚至网络流——但总体而言,您将使用的绝大多数数据集将使用这两种格式的某种组合。

要查看我们数据集中的内容,我们可以使用以下ls方法:

path.ls()

(#3) [Path('annotations'),Path('images'),Path('models')]

我们可以看到这个数据集为我们提供了图像和注释目录。数据集的网站告诉我们,注释目录包含有关宠物在哪里而不是它们是什么的信息。在本章中,我们将进行分类,而不是本地化,也就是说,我们关心宠物是什么,而不是它们在哪里。因此,我们现在将忽略注释目录。所以,让我们看一下images目录:

(path/"images").ls()

(#7394) [Path('images/great_pyrenees_173.jpg'),Path('images/wheaten_terrier_46.j> pg'),Path('images/Ragdoll_262.jpg'),Path('images/german_shorthaired_3.jpg'),P> ath('images/american_bulldog_196.jpg'),Path('images/boxer_188.jpg'),Path('ima> ges/staffordshire_bull_terrier_173.jpg'),Path('images/basset_hound_71.jpg'),P> ath('images/staffordshire_bull_terrier_37.jpg'),Path('images/yorkshire_terrie> r_18.jpg')...]

fastai 中大多数返回集合的函数和方法都使用类称为L.这个类可以被认为是普通 Pythonlist类型的增强版本,增加了常用操作的便利性。例如,当我们在笔记本中显示此类的对象时,它会以此处显示的格式显示。显示的第一件事是集合中的项目数,前缀为#。您还将在前面的输出中看到该列表以省略号为后缀。这意味着只显示前几个项目——这是一件好事,因为我们不希望屏幕上显示超过 7,000 个文件名!

通过检查这些文件名,我们可以看到它们的结构。每个文件名都包含宠物品种,然后是下划线 (_)、数字,最后是文件扩展名。我们需要创建一段代码,从单个Path.Jupyter notebook 让这一切变得简单,因为我们可以逐渐建立一些可行的东西,然后将其用于整个数据集。在这一点上,我们必须小心不要做出太多假设。例如,如果您仔细观察,您可能会注意到某些宠物品种包含多个单词,因此我们不能简单地在_找到的第一个字符处中断。为了让我们能够测试我们的代码,让我们选择以下文件名之一:

fname = (path/"images").ls()[0]

从字符串中提取信息的最强大、最灵活的方法像这样是使用正则表达式,也称为正则表达式。正则表达式是用正则表达式语言编写的特殊字符串,它指定了一个通用规则,用于确定另一个字符串是否通过测试(即“匹配”正则表达式),也可能用于提取特定部分或部分的其他字符串。在这种情况下,我们需要一个从文件名中提取宠物品种的正则表达式。

我们没有篇幅在这里给你一个完整的正则表达式教程,但是网上有很多优秀的教程,我们知道你们中的很多人已经熟悉了这个很棒的工具。如果你不是,那很好——这是你纠正它的好机会!我们发现正则表达式是我们编程工具包中最有用的工具之一,我们的许多学生告诉我们,这是他们最感兴趣的学习内容之一。所以去谷歌搜索现在“正则表达式教程”,然后在您浏览完之后再回到这里。该书的网站还提供了我们最喜欢的列表。

正则表达式不仅非常方便,而且它们也有有趣的根源。它们是“常规”的,因为它们最初是“常规”语言的示例,是乔姆斯基层次结构中的最低级别。这是由语言学家诺姆·乔姆斯基(Noam Chomsky),他还撰写了《句法结构》,这是一部探索人类语言形式语法的开创性著作。这就是计算的魅力之一:你每天拿到的锤子实际上可能来自宇宙飞船。

当您编写正则表达式时,最好的开始方法是首先针对一个示例进行尝试。让我们使用该findall方法对fname对象的文件名尝试正则表达式:

re.findall(r'(.+)_\d+.jpg$', fname.name)

['great_pyrenees']

此正则表达式会提取出最后一个下划线字符之前的所有字符,只要后续字符是数字数字,然后是 JPEG 文件扩展名。

现在我们确认正则表达式适用于该示例,让我们用它来标记整个数据集。fastai 提供了许多类来帮助标记。对于使用正则表达式进行标记,我们可以使用RegexLabeller该类。在这个例子中,我们使用了我们在第 2 章中看到的数据块 API(事实上,我们几乎总是使用数据块 API——它比我们在第 1 章中看到的简单工厂方法灵活得多):p

pets = DataBlock(blocks = (ImageBlock, CategoryBlock),get_items=get_image_files,splitter=RandomSplitter(seed=42),get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'),item_tfms=Resize(460),batch_tfms=aug_transforms(size=224, min_scale=0.75))dls = pets.dataloaders(path/"images")

我们以前从未见过的这个调用的一个重要部分DataBlock是这两行:

item_tfms=Resize(460),batch_tfms=aug_transforms(size=224, min_scale=0.75)

这些行实现了我们称之为presizing的 fastai 数据增强策略。Presizing 是一种特殊的图像增强方式,旨在最大限度地减少数据破坏,同时保持良好的性能。

Presizing(加压)

我们需要我们的图像具有相同的尺寸,以便它们可以整理成张量以传递给 GPU。我们还希望最小化我们执行的不同增强计算的数量。性能要求表明,我们应该在可能的情况下将我们的增强变换组合成更少的变换(以减少计算数量和有损操作的数量)并将图像变换成统一的尺寸(以便在 GPU 上进行更有效的处理)。

挑战在于,如果在调整到增强后的大小后执行,各种常见的数据增强转换可能会引入虚假的空白区域、降级数据或两者兼而有之。例如,将图像旋转 45 度会使新边界的角落区域充满空白,这不会教给模型任何东西。许多旋转和缩放操作需要插值来创建像素。这些插值像素源自原始图像数据,但质量仍然较低。

为了解决这些挑战,预选采用了两种策略,如图 5-1所示:

将图像调整为相对“大”的尺寸——即,尺寸明显大于目标训练尺寸。

将所有常见的增强操作(包括调整到最终目标大小)组合为一个,并在处理结束时仅在 GPU 上执行一次组合操作,而不是单独执行操作并进行多次插值。

第一步,调整大小,创建足够大的图像,使它们有多余的余量,以允许在其内部区域进行进一步的增强变换,而不会创建空白区域。此转换通过使用较大的裁剪尺寸调整为正方形来实现。在训练集上随机选择裁剪区域,裁剪的大小选择覆盖图像的整个宽度或高度,取较小者。第二步,GPU 用于所有数据增强,所有潜在的破坏性操作一起完成,最后只进行一次插值。

图 5-1。对训练集施压

这张图显示了两个步骤:

裁剪全宽或全高:这是在 中item_tfms,因此在将其复制到 GPU 之前将其应用于每个单独的图像。它用于确保所有图像的大小相同。在训练集上,裁剪区域是随机选择的。在验证集上,始终选择图像的中心正方形。

Random crop and augment:这是在 中batch_tfms,所以它在 GPU 上一次应用于一个批次,这意味着它很快。在验证集上,此处仅将大小调整为模型所需的最终大小。在训练集上,首先进行随机裁剪和任何其他增强。

要在 fastai 中实现此过程,您可以将Resize其用作项目大尺寸RandomResizedCrop的变换,以及较小尺寸的批量变换。如果您在函数中包含参数,RandomResizedCrop则会为您添加,就像在上一节中的调用中所做的那样。或者,您可以使用or代替(默认)作为初始.min_scaleaug_transformsDataBlockpadsquishcropResize

图 5-2显示了经过缩放、插值、旋转然后再次插值的图像(这是所有其他深度学习库使用的方法)(如右侧所示)与已缩放的图像之间的区别并作为一个操作旋转,然后插值一次(fastai 方法),如左侧所示。

图 5-2。fastai 的数据增强策略(左)与传统方法(右)的比较

您可以看到右边的图像不太清晰,左下角有反射填充伪影;而且,左上角的草也完全消失了。我们发现,在实践中,使用 presizing 可以显着提高模型的准确性,并且通常也会导致加速。

fastai 库还提供了简单的方法来在训练模型之前检查数据的外观,这是非常重要的一步。我们接下来会看看那些。

检查和调试数据块

我们永远不能假设我们的代码运行良好。写一个DataBlock就像写一张蓝图。如果您的代码中某处出现语法错误,您将收到一条错误消息,但您不能保证您的模板会按照您的预期在您的数据源上工作。因此,在训练模型之前,您应该始终检查您的数据。

您可以使用以下show_batch方法执行此操作:

dls.show_batch(nrows=1, ncols=3)

查看每张图片,并检查每张图片是否都带有该宠物品种的正确标签。通常,数据科学家处理的数据不像领域专家那样熟悉:例如,我实际上不知道这些宠物品种有多少。由于我不是宠物品种方面的专家,所以此时我会使用 Google 图片来搜索其中一些品种,并确保这些图片看起来与我在此输出中看到的相似。

如果您在构建时犯了错误DataBlock,在此步骤之前您可能不会看到它。要对此进行调试,我们鼓励您使用该summary方法。它将尝试从您提供的源中创建一个批次,其中包含很多详细信息。此外,如果它失败了,您将确切地看到错误发生的时间点,并且库将尝试为您提供一些帮助。例如,一个常见的错误是忘记使用Resize变换,因此您最终会得到不同大小的图片并且无法对它们进行批处理。以下是在这种情况下摘要的样子(请注意,自撰写本文以来,确切的文本可能已经改变,但它会给你一个想法):

pets1 = DataBlock(blocks = (ImageBlock, CategoryBlock),get_items=get_image_files,splitter=RandomSplitter(seed=42),get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'))pets1.summary(path/"images")

Setting-up type transforms pipelinesCollecting items from /home/sgugger/.fastai/data/oxford-iiit-pet/imagesFound 7390 items2 datasets of sizes 5912,1478Setting up Pipeline: PILBase.createSetting up Pipeline: partial -> CategorizeBuilding one samplePipeline: PILBase.createstarting from/home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpgapplying PILBase.create givesPILImage mode=RGB size=375x500Pipeline: partial -> Categorizestarting from/home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpgapplying partial givesamerican_bulldogapplying Categorize givesTensorCategory(12)Final sample: (PILImage mode=RGB size=375x500, TensorCategory(12))Setting up after_item: Pipeline: ToTensorSetting up before_batch: Pipeline:Setting up after_batch: Pipeline: IntToFloatTensorBuilding one batchApplying item_tfms to the first sample:Pipeline: ToTensorstarting from(PILImage mode=RGB size=375x500, TensorCategory(12))applying ToTensor gives(TensorImage of size 3x500x375, TensorCategory(12))Adding the next 3 samplesNo before_batch transform to applyCollating items in a batchError! It's not possible to collate your items in a batchCould not collate the 0-th members of your tuples because got the followingshapes:torch.Size([3, 500, 375]),torch.Size([3, 375, 500]),torch.Size([3, 333, 500]),torch.Size([3, 375, 500])

您可以确切地看到我们如何收集数据并对其进行拆分,我们如何从文件名变为样本(元组(图像,类别)),然后应用了哪些项目转换以及它如何未能批量整理这些样本(因为形状不同)。

一旦您认为您的数据看起来正确,我们通常会推荐下一个步骤应该是用它来训练一个简单的模型。我们经常看到人们将实际模型的训练推迟太久。结果,他们不知道他们的基线结果是什么样的。也许您的问题不需要大量花哨的特定领域工程。或者也许数据似乎根本没有训练模型。这些都是你想尽快知道的事情。

对于这个初始测试,我们将使用我们在第 1 章中使用的相同简单模型:

learn = cnn_learner(dls, resnet34, metrics=error_rate)learn.fine_tune(2)

正如我们之前简要讨论过的,当我们拟合模型向我们展示了每个训练阶段后的结果。请记住,一个时期是对数据中所有图像的完整遍历。显示的列是训练集项目的平均损失、验证集的损失以及我们要求的任何指标——在本例中为错误率。

请记住,损失是我们决定的任何功能用于优化我们模型的参数。但我们实际上并没有告诉 fastai 我们想要使用什么损失函数。那么它在做什么呢?fastai 通常会尝试根据您使用的数据类型和模型选择合适的损失函数。在这种情况下,我们有图像数据和分类结果,因此 fastai 将默认使用交叉熵损失。

交叉熵损失

交叉熵损失是一种类似于我们在上一章中使用的损失函数,但是(正如我们将看到的)有两个好处:

即使我们的因变量有两个以上的类别,它也有效。

它导致更快和更可靠的培训。

要了解交叉熵损失如何对具有两个以上类别的因变量起作用,我们首先必须了解损失函数看到的实际数据和激活是什么样子的。

查看激活和标签

让我们看看我们模型的激活。至从我们的 中获取一批真实数据DataLoaders,我们可以使用one_batch方法:

x,y = dls.one_batch()

如您所见,这会以小批量的形式返回因变量和自变量。让我们看看我们的因变量中包含什么:

y

TensorCategory([11, 0, 0, 5, 20, 4, 22, 31, 23, 10, 20, 2, 3, 27, 18, 23,> 33, 5, 24, 7, 6, 12, 9, 11, 35, 14, 10, 15, 3, 3, 21, 5, 19, 14, 12,> 15, 27, 1, 17, 10, 7, 6, 15, 23, 36, 1, 35, 6,4, 29, 24, 32, 2, 14, 26, 25, 21, 0, 29, 31, 18, 7, 7, 17],> device='cuda:0')

我们的批量大小是 64,所以我们在这个张量中有 64 行。每行都是 0 到 36 之间的单个整数,代表我们 37 种可能的宠物品种。我们可以查看预测(我们神经网络的最后一层)通过使用Learner.get_preds.此函数采用数据集索引(0 表示训练,1 表示有效)或批次迭代器。因此,我们可以通过我们的批次向它传递一个简单的列表来获得我们的预测。它默认返回预测和目标,但由于我们已经有了目标,我们可以通过分配特殊变量来有效地忽略它们_

preds,_ = learn.get_preds(dl=[(x,y)])preds[0]

tensor([7.9069e-04, 6.2350e-05, 3.7607e-05, 2.9260e-06, 1.3032e-05, 2.5760e-05,> 6.2341e-08, 3.6400e-07, 4.1311e-06, 1.3310e-04, 2.3090e-03, 9.9281e-01,> 4.6494e-05, 6.4266e-07, 1.9780e-06, 5.7005e-07,3.3448e-06, 3.5691e-03, 3.4385e-06, 1.1578e-05, 1.5916e-06, 8.5567e-08,> 5.0773e-08, 2.2978e-06, 1.4150e-06, 3.5459e-07, 1.4599e-04, 5.6198e-08,> 3.4108e-07, 2.0813e-06, 8.0568e-07, 4.3381e-07,1.0069e-05, 9.1020e-07, 4.8714e-06, 1.2734e-06, 2.4735e-06])

实际的预测是 0 到 1 之间的 37 个概率,总共加起来为 1:

len(preds[0]),preds[0].sum()

(37, tensor(1.0000))

为了将我们模型的激活转换为这样的预测,我们使用了一种叫做softmax激活函数的东西。

Softmax

在我们的分类模型中,我们在最后一层使用 softmax 激活函数来确保激活都在 0 和 1 之间,并且它们的总和为 1。

Softmax 类似于我们之前看到的 sigmoid 函数。提醒一句,sigmoid 看起来像这样:

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

我们可以将此函数应用于神经网络中的单列激活,并返回一列介于 0 和 1 之间的数字,因此对于我们的最后一层来说,它是一个非常有用的激活函数。

现在想想如果我们想在我们的目标中拥有更多类别(例如我们的 37 个宠物品种)会发生什么。这意味着我们需要更多的激活,而不仅仅是单个列:我们需要每个类别的激活。例如,我们可以创建一个预测 3s 和 7s 的神经网络,它返回两个激活,每个类一个——这将是创建更通用方法的良好第一步。假设我们有六个图像和两个可能的类别(其中第一列代表 3s,第二列是 7s),我们只使用一些标准差为 2 的随机数(所以我们乘以randn2):

acts = torch.randn((6,2))*2acts

tensor([[ 0.6734, 0.2576],[ 0.4689, 0.4607],[-2.2457, -0.3727],[ 4.4164, -1.2760],[ 0.9233, 0.5347],[ 1.0698, 1.6187]])

我们不能直接取这个 sigmoid,因为我们没有得到加为 1 的行(我们希望成为 3 的概率加上成为 7 的概率加起来为 1):

acts.sigmoid()

tensor([[0.6623, 0.5641],[0.6151, 0.6132],[0.0957, 0.4079],[0.9881, 0.2182],[0.7157, 0.6306],[0.7446, 0.8346]])

在第 4 章中,我们的神经网络创建了一个每个图像的激活,我们通过sigmoid函数传递。该单一激活表示模型对输入是 3 的置信度。二元问题是分类问题的一种特殊情况,因为目标可以被视为单个布尔值,就像我们在 中所做的那样mnist_loss。但也可以在具有任意数量类别的更一般的分类器组的上下文中考虑二元问题:在这种情况下,我们碰巧有两个类别。正如我们在熊分类器中看到的那样,我们的神经网络将为每个类别返回一个激活。

那么在二进制情况下,这些激活真正表明了什么?一对激活仅表示输入为 3 与为 7 的相对置信度。总体值,无论是高还是低,都无关紧要——重要的是哪个更高,以及如何很多。

我们预计,由于这只是表示同一问题的另一种方式,我们将能够sigmoid直接在我们的神经网络的两次激活版本上使用。事实上我们能够!我们可以只取神经网络激活之间的差异,因为这反映了我们对输入是 3 比 7 的确定程度,然后取它的 sigmoid:

(acts[:,0]-acts[:,1]).sigmoid()

tensor([0.6025, 0.5021, 0.1332, 0.9966, 0.5959, 0.3661])

第二列(它是 7 的概率)将是从 1 中减去的值。现在,我们需要一种方法来完成所有这些操作,该方法也适用于两列以上。事实证明,这个函数,称为softmax,就是这样:

def softmax(x): return exp(x) / exp(x).sum(dim=1, keepdim=True)

定义为e**x,其中e是一个约等于 2.718 的特殊数。它是自然对数函数的倒数。请注意,exp它始终为正并且增长非常迅速!

让我们检查是否softmax返回与第一列相同的值sigmoid,以及从 1 中减去第二列的值:

sm_acts = torch.softmax(acts, dim=1)sm_acts

tensor([[0.6025, 0.3975],[0.5021, 0.4979],[0.1332, 0.8668],[0.9966, 0.0034],[0.5959, 0.4041],[0.3661, 0.6339]])

softmax是多类别的等价物——sigmoid我们必须在有两个以上类别的任何时候使用它,并且类别的概率必须加到 1,即使只有两个类别,我们也经常使用它,只是为了让事情有点更一致。我们可以创建其他具有所有激活都在 0 和 1 之间并且总和为 1 的属性的函数;然而,没有其他函数与 sigmoid 函数具有相同的关系,我们已经看到它是平滑且对称的。此外,我们很快就会看到 softmax 函数与我们将在下一节中看到的损失函数一起工作得很好。

如果我们有三个输出激活,例如在我们的熊分类器中,为单个熊图像计算 softmax 将类似于图 5-3。

图 5-3。熊分类器上的 softmax 示例

这个函数在实践中做了什么?取指数确保我们所有的数字都是正数,然后除以总和确保我们将得到一堆加起来为 1 的数字。指数也有一个很好的属性:如果我们激活中的一个数字x是轻微的比其他更大,指数会放大它(因为它会增长,嗯……指数地),这意味着在 softmax 中,这个数字将更接近 1。

直观地说,softmax 函数确实想在其他类别中选择一个类别,因此当我们知道每张图片都有明确的标签时,它是训练分类器的理想选择。(请注意,在推理过程中它可能不太理想,因为您可能希望您的模型有时告诉您它无法识别它在训练期间看到的任何类,并且不选择一个类,因为它的激活分数略高. 在这种情况下,使用多个二进制输出列训练模型可能会更好,每个列都使用 sigmoid 激活。)

Softmax 是交叉熵损失的第一部分——第二部分是对数似然。

对数似然

当我们在前一章计算 MNIST 示例的损失时,我们使用了这个:

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

就像我们从 sigmoid 转移到 softmax 一样,我们需要扩展损失函数以不仅仅处理二元分类——它需要能够分类任意数量的类别(在本例中,我们有 37 个类别)。在 softmax 之后,我们的激活值介于 0 和 1 之间,并且对于这批预测中的每一行,总和为 1。我们的目标是 0 到 36 之间的整数。

在二进制情况下,我们曾经在和torch.where之间进行选择。当我们将二元分类视为具有两个类别的一般分类问题时,它变得更加容易,因为(正如我们在上一节中看到的)我们现在有两列包含 和 的等价物。因此,我们需要做的就是从相应的列中进行选择。让我们尝试在 PyTorch 中实现它。对于我们的合成 3 和 7 示例,假设这些是我们的标签:inputs1-inputsinputs1-inputs

targ = tensor([0,1,0,1,1,0])

这些是 softmax 激活:

sm_acts

tensor([[0.6025, 0.3975],[0.5021, 0.4979],[0.1332, 0.8668],[0.9966, 0.0034],[0.5959, 0.4041],[0.3661, 0.6339]])

然后对于 的每一项,我们可以使用它来选择sm_acts使用张量索引targ的适当列 ,如下所示:

idx = range(6)sm_acts[idx, targ]

tensor([0.6025, 0.4979, 0.1332, 0.0034, 0.4041, 0.3661])

要确切了解这里发生了什么,让我们将所有列放在一个表中。在这里,前两列是我们的激活,然后是目标、行索引,最后是前面代码中显示的结果:

查看此表,您可以看到可以通过将targ和idx列作为包含3和7列idx的两列矩阵的索引来计算最后一列。这就是sm_acts[idx, targ]正在做的事情。

这里真正有趣的是,这同样适用于多于两列。要看到这一点,请考虑如果我们为每个数字(0 到 9)添加一个激活列,然后targ包含一个从 0 到 9 的数字会发生什么。只要激活列总和为 1(如果我们使用softmax),我们将有一个损失函数来显示我们对每个数字的预测效果。

我们只从包含正确标签的列中挑选损失。我们不需要考虑其他列,因为根据 softmax 的定义,它们加起来是 1 减去对应于正确标签的激活。因此,使正确标签的激活尽可能高必然意味着我们也在减少剩余列的激活。

sm_acts[range(n), targ]PyTorch 提供了一个功能与(除了它取负数,因为之后应用对数时,我们将有负数)完全相同的功能 ,称为nll_loss(NLL代表负对数似然性):

-sm_acts[idx, targ]

tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])

F.nll_loss(sm_acts, targ, reduction='none')

tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])

尽管有它的名字,但这个 PyTorch 函数并不记录日志。我们将在下一节中了解原因,但首先,让我们看看为什么取对数会很有用。

记录日志

我们在上一节中看到的函数作为损失函数工作得很好,但我们可以稍微改进一下更好的。问题是我们使用的是概率,并且概率不能小于 0 或大于 1。这意味着我们的模型不会关心它预测的是 0.99 还是 0.999。确实,这些数字非常接近——但在另一种意义上,0.999 的置信度是 0.99 的 10 倍。因此,我们希望将我们的数字在 0 和 1 之间转换为负无穷大和 0 之间。有一个数学函数可以做到这一点:对数(可用作torch.log)。它没有为小于 0 的数字定义,如下所示:

plot_function(torch.log, min=0,max=4)

“对数”会响吗?对数函数具有以下恒等式:

y = b**aa = log(y,b)

在这种情况下,我们假设log(y,b)返回log y base b。但是,PyTorch 没有这样定义loglog在 Python 中使用特殊数字e(2.718…) 作为基数。

也许对数是你没有考虑过的东西过去左右。但这是一个数学概念,对于深度学习中的许多事情都非常重要,所以现在是刷新记忆的好时机。了解对数的关键是这种关系:

log(a*b) = log(a)+log(b)

当我们以这种格式看到它时,它看起来有点无聊;但想想这到底意味着什么。这意味着当基础信号呈指数或乘法增加时,对数呈线性增加。例如,这用于地震严重程度的里氏等级和噪音水平的 dB 等级。它也经常用于财务图表,我们希望更清楚地显示复合增长率。计算机科学家喜欢使用对数,因为这意味着可以创建非常非常大和非常非常小的数字的乘法可以用加法代替,这不太可能导致我们的计算机难以处理的尺度。

喜欢日志的不仅仅是计算机科学家!直到电脑出现一直以来,工程师和科学家使用了一种特殊的尺子,称为计算尺,它通过添加对数来进行乘法运算。对数在物理学中被广泛使用,用于乘以非常大或非常小的数字,以及许多其他领域。

取我们概率的正或负对数的平均值(取决于它是正确的还是不正确的类)给了我们负对数似然损失。在 PyTorch 中,nll_loss假设您已经获取了 softmax 的日志,因此它不会为您计算对数。

中的“nll”nll_loss代表“负对数似然”,但它实际上根本不使用对数!它假定您已经获取了日志。PyTorch 有一个名为的函数,它以快速准确的方式log_softmax结合起来log。被设计为之后使用。softmaxnll_losslog_softmax

当我们首先取 softmax,然后取其对数似然时,这种组合称为交叉熵损失。在 PyTorch 中,这可以作为nn.CrossEntropyLoss(在实践中log_softmax,然后是nll_loss):

loss_func = nn.CrossEntropyLoss()

如您所见,这是一堂课。实例化它会给你一个行为像函数的对象:

loss_func(acts, targ)

tensor(1.8045)

所有 PyTorch 损失函数都以两种形式提供,类形式刚刚显示以及一个简单的函数形式,在F命名空间中可用:

F.cross_entropy(acts, targ)

tensor(1.8045)

任何一个都可以正常工作,并且可以在任何情况下使用。我们注意到大多数人倾向于使用 class 版本,这在 PyTorch 的官方文档和示例中更常用,所以我们也倾向于使用它。

默认情况下,PyTorch 损失函数采用所有项目损失的平均值。您可以使用reduction='none'来禁用它:

nn.CrossEntropyLoss(reduction='none')(acts, targ)

tensor([0.5067, 0.6973, 2.0160, 5.6958, 0.9062, 1.0048])

当我们发现一个关于交叉熵损失的有趣特征时考虑它的梯度。的梯度cross_entropy(a,b)softmax(a)-b。由于softmax(a)是模型的最终激活,这意味着梯度与预测和目标之间的差异成正比。这与回归中的均方误差相同(假设没有最终激活函数,例如由 所添加的y_range),因为 的梯度(a-b)**22*(a-b)。因为梯度是线性的,我们不会看到梯度的突然跳跃或指数增加,这应该会导致模型的训练更平滑。

我们现在已经看到了隐藏在损失函数后面的所有部分。但是虽然这为我们的模型做得好(或坏)提供了一个数字,它无助于帮助我们知道它是否有任何好处。现在让我们看看一些方法来解释我们模型的预测。

模型解释

直接解释损失函数非常困难,因为它们被设计成计算机可以区分和优化的东西,而不是人们可以理解的东西。这就是我们有指标的原因。这些不是在优化过程中使用的,只是为了帮助我们这些可怜的人了解正在发生的事情。在这种情况下,我们的准确性看起来已经相当不错了!那么我们在哪里犯错呢?

我们在第 1 章中看到,我们可以使用混淆矩阵来查看我们的模型在哪里做得好和哪里做得不好:

interp = ClassificationInterpretation.from_learner(learn)interp.plot_confusion_matrix(figsize=(12,12), dpi=60)

哦,亲爱的——在这种情况下,混淆矩阵很难阅读。我们有 37 个宠物品种,这意味着我们在这个巨大的矩阵中有 37×37 个条目!相反,我们可以使用该most_confused方法,它只向我们显示混淆矩阵中预测最不正确的单元格(这里至少有 5 个或更多):

interp.most_confused(min_val=5)

[('american_pit_bull_terrier', 'staffordshire_bull_terrier', 10),('Ragdoll', 'Birman', 6)]

由于我们不是宠物品种专家,我们很难知道这些类别错误是否反映了识别品种的实际困难。因此,我们再次求助于谷歌。一点点谷歌搜索告诉我们,这里显示的最常见的类别错误是品种差异,即使是专业的育种者有时也会不同意。因此,这让我们感到欣慰,我们正走在正确的轨道上。

我们似乎有一个很好的基线。我们现在能做些什么来让它变得更好?

改进我们的模型

我们现在将研究一系列技术来改进我们的训练模型并使其变得更好。在此过程中,我们将更多地解释迁移学习以及如何在不破坏预训练权重的情况下尽可能地微调我们的预训练模型。

训练模型时我们需要设置的第一件事是学习率。我们在上一章中看到,要尽可能高效地训练,需要恰到好处,那么我们如何挑选一个好的呢?fastai 为此提供了一个工具。

学习率查找器

在训练模型时,我们可以做的最重要的事情之一是确保我们有正确的学习率。如果我们的学习率太低,可能需要很多很多 epoch 来训练我们的模型。这不仅浪费时间,而且还意味着我们可能会遇到过度拟合的问题,因为每次我们对数据进行完整的传递时,我们都会给我们的模型一个记忆它的机会。

所以让我们让我们的学习率真的很高,对吧?当然,让我们尝试一下,看看会发生什么:

learn = cnn_learner(dls, resnet34, metrics=error_rate)learn.fine_tune(1, base_lr=0.1)

那看起来不太好。这就是发生的事情。优化器朝着正确的方向迈进,但它前进得太远以至于完全超过了最小损失。重复多次使它越来越远,而不是越来越近!

我们如何才能找到完美的学习率——不要太高也不要太低?,研究员Leslie Smith想出了一个绝妙的想法,称为学习率查找器。他的想法是从一个非常非常小的学习率开始,它是如此之小以至于我们永远不会期望它太大而无法处理。我们将它用于一个小批量,然后找出损失是多少,然后将学习率提高一定百分比(例如,每次加倍)。然后我们再做一个小批量,跟踪损失,再次将学习率加倍。我们一直这样做,直到损失变得更糟,而不是更好。这是我们知道我们已经走得太远的地方。然后我们选择一个比这个点低一点的学习率。我们的建议是选择以下之一:

比达到最小损耗的位置小一个数量级(即最小值除以 10)

损失明显减少的最后一点

学习率查找器会计算曲线上的这些点来帮助您。这两个规则通常给出大约相同的值。在第一章中,我们没有指定学习率,使用来自 fastai 库的默认值(即 1e-3):

learn = cnn_learner(dls, resnet34, metrics=error_rate)lr_min,lr_steep = learn.lr_find()

print(f"Minimum/10: {lr_min:.2e}, steepest point: {lr_steep:.2e}")

Minimum/10: 8.32e-03, steepest point: 6.31e-03

我们可以在这张图上看到,在 1e-6 到 1e-3 的范围内,什么都没有发生,模型也没有训练。然后损失开始减少,直到达到最小值,然后再次增加。我们不希望学习率大于 1e-1,因为它会导致训练发散(你可以自己尝试),但是 1e-1 已经太高了:在这个阶段,我们已经离开了亏损稳步下降。

在这个学习率图中,3e-3 左右的学习率似乎是合适的,所以让我们选择:

learn = cnn_learner(dls, resnet34, metrics=error_rate)learn.fine_tune(2, base_lr=3e-3)

学习率查找器图具有对数刻度,这就是为什么1e-3 和 1e-2 之间的中点在 3e-3 和 4e-3 之间。这是因为我们主要关心学习率的数量级。

有趣的是学习率查找器直到 年才被发现,而神经网络自 1950 年代以来一直在开发中。在那段时间里,找到一个好的学习率可能是从业者最重要和最具挑战性的问题。该解决方案不需要任何高级数学、庞大的计算资源、庞大的数据集或其他任何会使任何好奇的研究人员无法访问的东西。此外,史密斯是不是硅谷某些独家实验室的一部分,而是作为海军研究员工作。这一切都意味着:深度学习的突破性工作绝对不需要大量资源、精英团队或先进的数学思想。还有很多工作要做,只需要一点常识、创造力和毅力。

现在我们有一个很好的学习率来训练我们的模型,让我们看看如何微调预训练模型的权重。

解冻和迁移学习

我们在第 1 章中简要讨论了迁移学习的工作原理。我们看到基本思想是一个预训练模型,经过训练可能在数百万个数据点(例如 ImageNet)上,针对另一项任务进行微调。但这究竟意味着什么?

我们现在知道卷积神经网络由许多线性层组成每对之间有一个非线性激活函数,然后是一个或多个最终线性层,最后有一个激活函数,例如 softmax。最后的线性层使用具有足够列的矩阵,使得输出大小与我们模型中的类数相同(假设我们正在进行分类)。

最后的线性层不太可能对我们有任何用处。在迁移学习设置中进行微调,因为它专门用于对原始预训练数据集中的类别进行分类。因此,当我们进行迁移学习时,我们将其删除、丢弃,并用一个新的线性层替换它,该层具有我们所需任务的正确输出数量(在这种情况下,将有 37 个激活)。

这个新添加的线性层将具有完全随机的权重。因此,我们在微调之前的模型具有完全随机的输出。但这并不意味着它是一个完全随机的模型!最后一层之前的所有层都经过仔细训练,通常擅长图像分类任务。正如我们在图片中看到的Zeiler 和 Fergus在第 1 章的论文(参见图1-10到1-13),前几层编码了一般概念,例如寻找梯度和边缘,后面几层编码了对我们仍然有用的概念,例如寻找眼球和毛皮。

我们希望以这样一种方式训练模型,使其能够记住来自预训练模型的所有这些普遍有用的想法,使用它们来解决我们的特定任务(分类宠物品种),并仅根据具体需要调整它们我们的特殊任务。

我们在微调时面临的挑战是用正确实现我们所需任务(分类宠物品种)的权重替换我们添加的线性层中的随机权重,而不会破坏仔细预训练的权重和其他层。一个简单的技巧可以让这种情况发生:告诉优化器只更新那些随机添加的最终层的权重。根本不要改变神经网络其余部分的权重。这称为冻结那些预训练层。

当我们从预训练网络创建模型时,fastai 会自动为我们冻结所有预训练的层。当我们调用该fine_tune方法时,fastai 做了两件事:

训练一个 epoch 的随机添加层,冻结所有其他层

解冻所有层,并根据请求的 epoch 数训练它们

尽管这是一种合理的默认方法,但对于您的特定数据集,您可能会通过稍微不同的方式获得更好的结果。该fine_tune方法具有可用于更改其行为的参数,但如果您想获得自定义行为,直接调用底层方法可能是最简单的。请记住,您可以使用以下语法查看该方法的源代码:

learn.fine_tune??

因此,让我们尝试自己手动执行此操作。首先,我们将在三个时期训练随机添加的层,使用fit_one_cycle.如第 1 章所述,fit_one_cycle建议的方法是在不使用fine_tune.我们将在本书后面看到原因;简而言之,就是fit_one_cycle从低学习率开始训练,第一节训练逐渐增加,最后一节训练又逐渐降低:

learn = cnn_learner(dls, resnet34, metrics=error_rate)learn.fit_one_cycle(3, 3e-3)

然后我们将解冻模型:

learn.unfreeze()

lr_find再次运行,因为要训练的层数更多,并且权重已经训练了三个 epoch,这意味着我们之前发现的学习率不再合适:

learn.lr_find()

(1.0964782268274575e-05, 1.5848931980144698e-06)

请注意,该图与我们使用随机权重时略有不同:我们没有表明模型正在训练的急剧下降。那是因为我们的模型已经被训练过了。在这里,我们在急剧增加之前有一个稍微平坦的区域,我们应该在急剧增加之前取一个点——例如,1e-5。具有最大梯度的点不是我们在这里寻找的,应该被忽略。

让我们以合适的学习率训练:

learn.fit_one_cycle(6, lr_max=1e-5)

这稍微改进了我们的模型,但我们还可以做更多的事情。我们预训练模型的最深层可能不需要那么高的学习率作为最后一个,所以我们可能应该对那些使用不同的学习率——这就是所谓的使用判别学习率。

判别学习率

即使在我们解冻之后,我们仍然非常关心那些预训练权重的质量。我们不会期望这些预训练参数的最佳学习率会与随机添加的参数一样高,即使我们已经调整了这些随机添加的参数几个时期。请记住,预训练的权重已经在数百万张图像上训练了数百个 epoch。

另外,你还记得我们在里面看到的图片吗?第 1 章,展示每一层的学习内容?第一层学习非常简单的基础,例如边缘和梯度检测器;这些可能对几乎任何任务都同样有用。后面的层学习了更复杂的概念,例如“眼睛”和“日落”,它们可能对您的任务根本没有用(例如,您正在对汽车模型进行分类)。因此,让后面的层比前面的层更快地进行微调是有意义的。

因此,fastai 的默认方法是使用判别式学习率。这种技术最初是在我们将在第 10 章介绍的 NLP 迁移学习的 ULMFiT 方法中开发的。就像深度学习中的许多好主意一样,它非常简单:对神经网络的早期层使用较低的学习率,而对后面的层(尤其是随机添加的层)使用较高的学习率。该想法基于Jason Yosinski 等人开发的见解。,他在 年表明,通过迁移学习,神经网络的不同层应该以不同的速度训练,如图 5-4 所示。

图 5-4。不同层和训练方法对迁移学习的影响(由 Jason Yosinski 等人提供)

fastai 允许您在任何预期学习率的地方传递 Python对象slice。传递的第一个值将是神经网络最早层的学习率,第二个值将是最后一层的学习率。中间的层将具有在整个范围内乘法等距的学习率。让我们使用这种方法来复制之前的训练,但是这次我们只将网络的最低​​层设置为 1e-6 的学习率;其他层将扩展到 1e-4。让我们训练一段时间,看看会发生什么:

learn = cnn_learner(dls, resnet34, metrics=error_rate)learn.fit_one_cycle(3, 3e-3)learn.unfreeze()learn.fit_one_cycle(12, lr_max=slice(1e-6,1e-4))

现在微调效果很好!

fastai 可以向我们展示训练和验证损失的图表:

learn.recorder.plot_loss()

正如你所看到的,训练损失越来越好。但请注意,最终验证损失的改善会减慢,有时甚至会变得更糟!这是模型开始过度拟合的点。特别是,该模型对其预测变得过度自信。但这并不意味着它必然会变得不那么准确。看一下每个 epoch 的训练结果表,你会经常看到准确率继续提高,即使验证损失变得更糟。最后,重要的是您的准确性,或更一般地说是您选择的指标,而不是损失。损失只是我们赋予计算机帮助我们优化的功能。

训练模型时必须做出的另一个决定是训练多长时间。我们接下来会考虑。

选择Epoch数

很多时候你会发现你是受限于时间,而不是泛化和准确性,在选择要训练多少个 epoch 时。因此,您的第一种训练方法应该是简单地选择一些 epoch,这些 epoch 将在您乐于等待的时间内进行训练。然后查看训练和验证损失图,如前所示,特别是您的指标。如果您发现即使在您最后的 epoch 中它们仍然在变得更好,您就知道您没有训练太久。

另一方面,您很可能会看到您选择的指标是在训练结束时真的变得更糟。请记住,我们不仅要寻找验证损失变得更糟,还要寻找实际指标。您的验证损失首先会在训练期间变得更糟,因为模型变得过于自信,然后才会变得更糟,因为它错误地记住了数据。我们在实践中只关心后一个问题。请记住,我们的损失函数是我们用来让优化器拥有可以区分和优化的东西的东西;这不是我们在实践中关心的事情。

在 1cycle 训练的日子之前,保存模型是很常见的在每个 epoch 结束时,然后从每个 epoch 保存的所有模型中选择具有最佳精度的模型。这称为提前停止。然而,这不太可能给你最好的答案,因为中间的那些时期发生在学习率有机会达到小值之前,在那里它真的可以找到最好的结果。因此,如果你发现你有过拟合,你应该做的是重新训练你的模型从头开始,这一次根据你之前的最佳结果所在的位置来选择一个 epoch 的总数。

如果你有时间训练更多的 epoch,你可能想用这段时间来训练更多的参数——也就是说,使用更深的架构。

更深层次的架构

通常,具有更多参数的模型可以对您的数据进行更多建模准确。(这种概括有很多警告,它取决于您使用的体系结构的具体情况,但目前这是一个合理的经验法则。)对于我们将在本书中看到的大多数体系结构,您可以通过简单地添加更多层来创建它们的更大版本。但是,由于我们要使用预训练模型,我们需要确保选择一些已经为我们预训练的层。

这就是为什么在实践中,架构往往数量很少的变种。例如,我们使用的 ResNet 架构本章包含 18、34、50、101 和 152 层的变体,在 ImageNet 上进行了预训练。更大(更多层和参数;有时被描述为模型的容量)版本的 ResNet 总是能够给我们更好的训练损失,但它可能会遭受更多的过拟合,因为它有更多的参数需要过拟合。

一般来说,更大的模型能够更好地捕捉数据中真正的底层关系,以及捕捉和记忆单个图像的具体细节。

但是,使用更深的模型将需要更多的 GPU RAM,所以你可能需要降低批次的大小以避免出现内存不足错误。当您尝试在 GPU 中安装太多内容时会发生这种情况,如下所示:

Cuda runtime error: out of memory

发生这种情况时,您可能必须重新启动笔记本电脑。的方式解决它的方法是使用更小的批量大小,这意味着在任何给定时间通过模型传递更小的图像组。您可以通过创建DataLoaderswith 将所需的批量大小传递给调用bs=

更深层次架构的另一个缺点是它们需要相当多的时间训练时间更长。一种可以大大加快速度的技术是混合精度训练。这是指在训练期间尽可能使用精度较低的数字(半精度浮点数,也称为fp16 )。当我们在 年初撰写这些文字时,几乎所有当前NVIDIA GPU 支持一种称为张量核心的特殊功能,可以将神经网络训练速度显着提高 2-3 倍。它们还需要更少的 GPU 内存。要在 fastai 中启用此功能,只需 在创建to_fp16()后添加Learner(您还需要导入模块)。

您无法真正提前知道针对您的特定问题的最佳架构——您需要尝试训练一些。所以现在让我们尝试一个混合精度的 ResNet-50:

from fastai.callback.fp16 import *learn = cnn_learner(dls, resnet50, metrics=error_rate).to_fp16()learn.fine_tune(6, freeze_epochs=3)

你会在这里看到我们已经回到 usingfine_tune,因为它非常方便!我们可以通过freeze_epochs告诉 fastai 在冻结时要训练多少个 epoch。它会自动为大多数数据集适当地改变学习率。

在这种情况下,我们没有看到更深层次模型的明显胜利。记住这一点很有用——对于您的特定情况,更大的模型不一定是更好的模型!确保在开始扩大规模之前尝试小型模型。

结论

在本章中,您学习了一些重要的实用技巧,既可以让您的图像数据为建模做好准备(调整大小、数据块摘要),也可以用于拟合模型(学习率查找器、解冻、判别学习率、设置 epoch 数和使用更深层次的架构)。使用这些工具将帮助您更快地构建更准确的图像模型。

我们还讨论了交叉熵损失。本书的这一部分值得花大量时间阅读。在实践中,您可能不需要自己从头开始实现交叉熵损失,但了解该函数的输入和输出很重要,因为它(或它的变体,我们将在下一节中看到章)几乎用于每个分类模型。因此,当您想要调试模型、将模型投入生产或提高模型的准确性时,您将需要能够查看其激活和损失,并了解正在发生的事情以及原因。如果你不了解你的损失函数,你就不能正确地做到这一点。

如果你还没有“点击”交叉熵损失,别担心——你会成功的!首先,回到前一章,确保你真正理解了mnist_loss.然后逐步完成本章的笔记本单元,我们将逐步完成每一个交叉熵损失。确保您了解每个计算在做什么以及为什么。尝试自己创建一些小张量并将它们传递给函数,看看它们返回什么。

请记住:在实现交叉熵损失时所做的选择并不是唯一可能做出的选择。就像当我们查看回归时,我们可以在均方误差和平均绝对差 (L1) 之间进行选择,我们也可以在这里更改细节。如果您对可能有用的功能有其他想法,请随时在本章的笔记本中尝试一下!(但公平警告:您可能会发现模型训练速度较慢且准确度较低。这是因为交叉熵损失的梯度与激活和目标之间的差异成正比,因此 SGD 总是得到很好的权重的缩放步长。)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。