第五章:图像分类
原文:
www.bookstack.cn/read/th-fastai-book/0661b9d7375f45ab.md
译者:飞龙
现在您了解了深度学习是什么、它的用途以及如何创建和部署模型,现在是时候深入了!在理想的世界中,深度学习从业者不必了解每个细节是如何在底层工作的。但事实上,我们还没有生活在理想的世界中。事实是,要使您的模型真正起作用并可靠地工作,您必须正确处理很多细节,并检查很多细节。这个过程需要能够在训练神经网络时查看内部情况,找到可能的问题,并知道如何解决它们。
因此,从本书开始,我们将深入研究深度学习的机制。计算机视觉模型的架构是什么,自然语言处理模型的架构是什么,表格模型的架构是什么等等?如何创建一个与您特定领域需求匹配的架构?如何从训练过程中获得最佳结果?如何加快速度?随着数据集的变化,您必须做出哪些改变?
我们将从重复第一章中查看的相同基本应用程序开始,但我们将做两件事:
- 让它们变得更好。
- 将它们应用于更多类型的数据。
为了做这两件事,我们将不得不学习深度学习难题的所有部分。这包括不同类型的层、正则化方法、优化器、如何将层组合成架构、标记技术等等。但我们不会一次性把所有这些东西都扔给你;我们将根据需要逐步引入它们,以解决与我们正在处理的项目相关的实际问题。
从狗和猫到宠物品种
在我们的第一个模型中,我们学会了如何区分狗和猫。就在几年前,这被认为是一个非常具有挑战性的任务——但今天,这太容易了!我们将无法向您展示训练模型时的细微差别,因为我们在不担心任何细节的情况下获得了几乎完美的结果。但事实证明,同一数据集还允许我们解决一个更具挑战性的问题:找出每张图像中显示的宠物品种是什么。
在第一章中,我们将应用程序呈现为已解决的问题。但这不是实际情况下的工作方式。我们从一个我们一无所知的数据集开始。然后我们必须弄清楚它是如何组合的,如何从中提取我们需要的数据,以及这些数据是什么样子的。在本书的其余部分,我们将向您展示如何在实践中解决这些问题,包括理解我们正在处理的数据以及在进行建模时测试的所有必要中间步骤。
我们已经下载了宠物数据集,并且可以使用与第一章相同的代码获取到该数据集的路径:
from fastai2.vision.all import * path = untar_data(URLs.PETS)
现在,如果我们要理解如何从每个图像中提取每只宠物的品种,我们需要了解数据是如何布局的。数据布局的细节是深度学习难题的重要组成部分。数据通常以以下两种方式之一提供:
- 表示数据项的个别文件,例如文本文档或图像,可能组织成文件夹或具有表示有关这些项信息的文件名
- 数据表(例如,以 CSV 格式)中的数据,其中每行是一个项目,可能包括文件名,提供表中数据与其他格式(如文本文档和图像)中数据之间的连接
有一些例外情况——特别是在基因组学等领域,可能存在二进制数据库格式或甚至网络流——但总体而言,您将处理的绝大多数数据集将使用这两种格式的某种组合。
要查看数据集中的内容,我们可以使用ls
方法:
path.ls()
(#3) [Path('annotations'),Path('images'),Path('models')]
我们可以看到这个数据集为我们提供了images和annotations目录。数据集的网站告诉我们annotations目录包含有关宠物所在位置而不是它们是什么的信息。在本章中,我们将进行分类,而不是定位,也就是说我们关心的是宠物是什么,而不是它们在哪里。因此,我们暂时会忽略annotations目录。那么,让我们来看看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
的类。这个类可以被认为是普通 Python list
类型的增强版本,具有用于常见操作的附加便利。例如,当我们在笔记本中显示这个类的对象时,它会以这里显示的格式显示。首先显示的是集合中的项目数,前面带有#
。在前面的输出中,你还会看到列表后面有省略号。这意味着只显示了前几个项目,这是件好事,因为我们不希望屏幕上出现超过 7000 个文件名!
通过检查这些文件名,我们可以看到它们似乎是如何结构化的。每个文件名包含宠物品种,然后是一个下划线(_
),一个数字,最后是文件扩展名。我们需要创建一段代码,从单个Path
中提取品种。Jupyter 笔记本使这变得容易,因为我们可以逐渐构建出可用的东西,然后用于整个数据集。在这一点上,我们必须小心不要做太多假设。例如,如果你仔细观察,你可能会注意到一些宠物品种包含多个单词,因此我们不能简单地在找到的第一个_
字符处中断。为了让我们能够测试我们的代码,让我们挑选出一个这样的文件名:
fname = (path/"images").ls()[0]
从这样的字符串中提取信息的最强大和灵活的方法是使用regular expression,也称为regex。正则表达式是一种特殊的字符串,用正则表达式语言编写,它指定了一个一般规则,用于决定另一个字符串是否通过测试(即“匹配”正则表达式),并且可能用于从另一个字符串中提取特定部分。在这种情况下,我们需要一个正则表达式从文件名中提取宠物品种。
我们没有空间在这里为您提供完整的正则表达式教程,但有许多优秀的在线教程,我们知道你们中的许多人已经熟悉这个神奇的工具。如果你不熟悉,那完全没问题——这是一个让你纠正的绝佳机会!我们发现正则表达式是我们编程工具包中最有用的工具之一,我们的许多学生告诉我们,这是他们最兴奋学习的事情之一。所以赶紧去谷歌搜索“正则表达式教程”吧,然后在你看得很开心之后回到这里。书籍网站也提供了我们喜欢的教程列表。
亚历克西斯说
正则表达式不仅非常方便,而且还有有趣的起源。它们之所以被称为“regular”,是因为它们最初是“regular”语言的示例,这是乔姆斯基层次结构中最低的一级。这是语言学家诺姆·乔姆斯基开发的一种语法分类,他还写了《句法结构》,这是一项寻找人类语言基础形式语法的开创性工作。这是计算的魅力之一:你每天使用的工具可能实际上来自太空船。
当你编写正则表达式时,最好的方法是首先针对一个示例尝试。让我们使用findall
方法来对fname
对象的文件名尝试一个正则表达式:
re.findall(r'(.+)_\d+.jpg$', fname.name)
['great_pyrenees']
这个正则表达式提取出所有字符,直到最后一个下划线字符,只要后续字符是数字,然后是 JPEG 文件扩展名。
现在我们确认了正则表达式对示例的有效性,让我们用它来标记整个数据集。fastai 提供了许多类来帮助标记。对于使用正则表达式进行标记,我们可以使用RegexLabeller
类。在这个例子中,我们使用了数据块 API,我们在第二章中看到过(实际上,我们几乎总是使用数据块 API——它比我们在第一章中看到的简单工厂方法更灵活):
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)
这些行实现了一个我们称之为预调整的 fastai 数据增强策略。预调整是一种特殊的图像增强方法,旨在最大限度地减少数据破坏,同时保持良好的性能。
预调整
我们需要我们的图像具有相同的尺寸,这样它们可以整合成张量传递给 GPU。我们还希望最小化我们执行的不同增强计算的数量。性能要求表明,我们应该尽可能将我们的增强变换组合成更少的变换(以减少计算数量和损失操作的数量),并将图像转换为统一尺寸(以便在 GPU 上更有效地处理)。
挑战在于,如果在调整大小到增强尺寸之后执行各种常见的数据增强变换,可能会引入虚假的空白区域,降低数据质量,或两者兼而有之。例如,将图像旋转 45 度会在新边界的角落区域填充空白,这不会教会模型任何东西。许多旋转和缩放操作将需要插值来创建像素。这些插值像素是从原始图像数据派生的,但质量较低。
为了解决这些挑战,预调整采用了图 5-1 中显示的两种策略:
- 将图像调整为相对“大”的尺寸,即明显大于目标训练尺寸。
- 将所有常见的增强操作(包括调整大小到最终目标大小)组合成一个,并在 GPU 上一次性执行组合操作,而不是单独执行操作并多次插值。
第一步是调整大小,创建足够大的图像,使其内部区域有多余的边距,以允许进一步的增强变换而不会产生空白区域。这个转换通过调整大小为一个正方形,使用一个大的裁剪尺寸来实现。在训练集上,裁剪区域是随机选择的,裁剪的大小被选择为覆盖图像宽度或高度中较小的那个。在第二步中,GPU 用于所有数据增强,并且所有潜在破坏性操作都一起完成,最后进行单次插值。
图 5-1。训练集上的预调整
这张图片展示了两个步骤:
- 裁剪全宽或全高:这在
item_tfms
中,因此它应用于每个单独的图像,然后再复制到 GPU。它用于确保所有图像具有相同的尺寸。在训练集上,裁剪区域是随机选择的。在验证集上,总是选择图像的中心正方形。 - 随机裁剪和增强:这在
batch_tfms
中,因此它一次在 GPU 上应用于整个批次,这意味着速度快。在验证集上,只有调整大小到模型所需的最终大小。在训练集上,首先进行随机裁剪和任何其他增强。
要在 fastai 中实现此过程,您可以使用Resize
作为具有大尺寸的项目转换,以及RandomResizedCrop
作为具有较小尺寸的批处理转换。如果在aug_transforms
函数中包含min_scale
参数,RandomResizedCrop
将为您添加,就像在上一节中的DataBlock
调用中所做的那样。或者,您可以在初始Resize
中使用pad
或squish
而不是crop
(默认值)。
图 5-2 显示了一个图像经过缩放、插值、旋转,然后再次插值(这是所有其他深度学习库使用的方法),显示在右侧,以及一个图像经过缩放和旋转作为一个操作,然后插值一次(fastai 方法),显示在左侧。
图 5-2。fastai 数据增强策略(左)与传统方法(右)的比较
您可以看到右侧的图像定义不够清晰,在左下角有反射填充伪影;此外,左上角的草完全消失了。我们发现,在实践中,使用预调整显著提高了模型的准确性,通常也会加快速度。
fastai 库还提供了简单的方法来检查您的数据在训练模型之前的外观,这是一个非常重要的步骤。我们将在下一步中看到这些。
检查和调试 DataBlock
我们永远不能假设我们的代码完美运行。编写DataBlock
就像编写蓝图一样。如果您的代码中有语法错误,您将收到错误消息,但是您无法保证您的模板会按照您的意图在数据源上运行。因此,在训练模型之前,您应该始终检查您的数据。
您可以使用show_batch
方法来执行此操作:
dls.show_batch(nrows=1, ncols=3)
查看每个图像,并检查每个图像是否具有正确的宠物品种标签。通常,数据科学家使用的数据可能不如领域专家熟悉:例如,我实际上不知道这些宠物品种中的许多是什么。由于我不是宠物品种的专家,我会在这一点上使用谷歌图像搜索一些这些品种,并确保图像看起来与我在输出中看到的相似。
如果在构建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 pipelines Collecting items from /home/sgugger/.fastai/data/oxford-iiit-pet/images Found 7390 items 2 datasets of sizes 5912,1478 Setting up Pipeline: PILBase.create Setting up Pipeline: partial -> Categorize Building one sample Pipeline: PILBase.create starting from /home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpg applying PILBase.create gives PILImage mode=RGB size=375x500 Pipeline: partial -> Categorize starting from /home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpg applying partial gives american_bulldog applying Categorize gives TensorCategory(12) Final sample: (PILImage mode=RGB size=375x500, TensorCategory(12)) Setting up after_item: Pipeline: ToTensor Setting up before_batch: Pipeline: Setting up after_batch: Pipeline: IntToFloatTensor Building one batch Applying item_tfms to the first sample: Pipeline: ToTensor starting from (PILImage mode=RGB size=375x500, TensorCategory(12)) applying ToTensor gives (TensorImage of size 3x500x375, TensorCategory(12)) Adding the next 3 samples No before_batch transform to apply Collating items in a batch Error! It's not possible to collate your items in a batch Could not collate the 0-th members of your tuples because got the following shapes: torch.Size([3, 500, 375]),torch.Size([3, 375, 500]),torch.Size([3, 333, 500]), torch.Size([3, 375, 500])
您可以看到我们如何收集数据并拆分数据,如何从文件名转换为样本(元组(图像,类别)),然后应用了哪些项目转换以及如何在批处理中无法整理这些样本(因为形状不同)。
一旦您认为数据看起来正确,我们通常建议下一步应该使用它来训练一个简单的模型。我们经常看到人们将实际模型的训练推迟得太久。结果,他们不知道他们的基准结果是什么样的。也许您的问题不需要大量花哨的领域特定工程。或者数据似乎根本无法训练模型。这些都是您希望尽快了解的事情。
对于这个初始测试,我们将使用与第一章中使用的相同简单模型:
learn = cnn_learner(dls, resnet34, metrics=error_rate) learn.fine_tune(2)
epoch | train_loss | valid_loss | error_rate | time |
0 | 1.491732 | 0.337355 | 0.108254 | 00:18 |
epoch | train_loss | valid_loss | error_rate | time |
— | — | — | — | — |
0 | 0.503154 | 0.293404 | 0.096076 | 00:23 |
1 | 0.314759 | 0.225316 | 0.066306 | 00:23 |
正如我们之前简要讨论过的,当我们拟合模型时显示的表格展示了每个训练周期后的结果。记住,一个周期是对数据中所有图像的完整遍历。显示的列是训练集中项目的平均损失、验证集上的损失,以及我们请求的任何指标——在这种情况下是错误率。
请记住损失是我们决定用来优化模型参数的任何函数。但是我们实际上并没有告诉 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:5')
我们的批量大小是 64,因此在这个张量中有 64 行。每行是一个介于 0 和 36 之间的整数,代表我们 37 种可能的宠物品种。我们可以通过使用Learner.get_preds
来查看预测(我们神经网络最后一层的激活)。这个函数默认返回预测和目标,但由于我们已经有了目标,我们可以通过将其赋值给特殊变量_
来有效地忽略它们:
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])
实际预测是 37 个介于 0 和 1 之间的概率,总和为 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 种宠物品种)。这意味着我们需要比单个列更多的激活:我们需要一个激活每个类别。例如,我们可以创建一个预测 3 和 7 的神经网络,返回两个激活,每个类别一个——这将是创建更一般方法的一个很好的第一步。让我们只是使用一些标准差为 2 的随机数(因此我们将randn
乘以 2)作为示例,假设我们有六个图像和两个可能的类别(其中第一列代表 3,第二列代表 7):
acts = torch.randn((6,2))*2 acts
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]])
在第四章中,我们的神经网络为每个图像创建了一个单一激活,然后通过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)
术语:指数函数(exp)
定义为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 的数字。指数还有一个很好的特性:如果我们激活中的某个数字略大于其他数字,指数将放大这个差异(因为它呈指数增长),这意味着在 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
在inputs
和1-inputs
之间进行选择。当我们将二元分类作为具有两个类别的一般分类问题处理时,它变得更容易,因为(正如我们在前一节中看到的)现在有两列包含等同于inputs
和1-inputs
的内容。因此,我们只需要从适当的列中进行选择。让我们尝试在 PyTorch 中实现这一点。对于我们合成的 3 和 7 的示例,假设这些是我们的标签:
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]])
然后对于每个targ
项,我们可以使用它来使用张量索引选择sm_acts
的适当列,如下所示:
idx = range(6) sm_acts[idx, targ]
tensor([0.6025, 0.4979, 0.1332, 0.0034, 0.4041, 0.3661])
FastAI 之书(面向程序员的 FastAI)(三)(2)https://developer.aliyun.com/article/1483220