文章目录


在上一章中,您学习了一些在实践中训练模型的重要实用技术。选择学习率和 epoch 数等考虑因素对于获得良好的结果非常重要。

在本章中,我们将研究另外两种类型的计算机视觉问题:多标签分类和回归。第一个发生在您想要预测每个图像多个标签(或有时根本没有)时,第二个发生在您的标签是一个或多个数字时——一个数量而不是一个类别。

在此过程中,我们将更深入地研究深度学习模型中的输出激活、目标和损失函数。

多标签分类

多标签分类是指识别 图像中可能不完全包含一种类型的对象的对象类别。可能有不止一种对象,或者您要查找的类中可能根本没有对象。

例如,这对我们的熊来说是一个很好的方法 分类器。我们在第 2 章推出的熊分类器的一个问题 是,如果用户上传的东西不是任何种类的熊,模型仍然会说它是灰熊、黑色或泰迪熊——它没有能力预测“根本不是熊”。事实上,在我们完成这一章之后,你可以回到你的图像分类器应用程序并尝试使用多标签技术重新训练它,然后通过传入一个不是您认可的任何课程。

在实践中,我们并没有看到很多人为此目的训练多标签分类器的例子——但我们经常看到用户和开发人员都抱怨这个问题。看来这个简单的解决方案根本没有被广泛理解或欣赏!因为在实践中可能更常见的是一些匹配为零或多于一个的图像,我们可能应该期望在实践中多标签分类器比单标签分类器更广泛适用。

首先让我们看看多标签数据集是什么样的;然后我们将解释如何为我们的模型做好准备。你会看到模型的架构与前一章没有变化;只有损失函数可以。让我们从数据开始。

数据

对于我们的示例,我们将使用 PASCAL 数据集,它可以具有 每个图像不止一种分类对象。

我们首先照常下载和提取数据集:

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

这个数据集与我们之前看到的不同,它不是由文件名或文件夹构成的,而是带有一个 CSV 文件,告诉我们每个图像使用什么标签。我们可以通过将 CSV 文件读入 Pandas DataFrame 来检查它:

df = pd.read_csv(path/'train.csv')
df.head()

如您所见,每个图像中的类别列表显示为以空格分隔的字符串。

PANDAS AND DATAFRAMES

不,它实际上不是熊猫!Pandas是一个 Python 库,用于 操作和分析表格和时间序列数据。主要类是 DataFrame,它表示一个行和列的表。

您可以从 CSV 文件、数据库表、Python 字典和许多其他来源获取 DataFrame。在 Jupyter 中,DataFrame 以格式化表的形式输出,如下所示。

您可以使用该属性访问 DataFrame 的行和列iloc,就像它是一个矩阵一样:

df.iloc[:,0]
0       000005.jpg
1       000007.jpg
2       000009.jpg
3       000012.jpg
4       000016.jpg
           ...
5006    009954.jpg
5007    009955.jpg
5008    009958.jpg
5009    009959.jpg
5010    009961.jpg
Name: fname, Length: 5011, dtype: object
df.iloc[0,:]# Trailing :s are always optional (in numpy, pytorch, pandas, etc.),#   so this is equivalent:df.iloc[0]
fname       000005.jpg
labels           chair
is_valid          True
Name: 0, dtype: object

您还可以通过直接索引到 DataFrame 来按名称获取列:

df['fname']
0       000005.jpg
1       000007.jpg
2       000009.jpg
3       000012.jpg
4       000016.jpg
           ...
5006    009954.jpg
5007    009955.jpg
5008    009958.jpg
5009    009959.jpg
5010    009961.jpg
Name: fname, Length: 5011, dtype: object

您可以创建新列并使用列进行计算:

df1 = pd.DataFrame()
df1['a'] = [1,2,3,4]
df1
df1['b'] = [10, 20, 30, 40]df1['a'] + df1['b']
0    11
1    22
2    33
3    44
dtype: int64

Pandas 是一个快速灵活的库,是每个数据科学家 Python 工具箱的重要组成部分。不幸的是,它的 API 可能相当 令人困惑和令人惊讶,因此需要一段时间才能熟悉它。如果您以前没有使用过 Pandas,我们建议您阅读教程;我们特别喜欢Pandas 的创建者 Wes McKinney的Python for Data Analysis (O'Reilly)。它还涵盖了其他重要的库,例如matplotlibNumPy. 我们将尝试简要描述我们遇到的 Pandas 功能,但不会深入到 McKinney 书中的详细程度。

现在我们已经看到了数据的样子,让我们为模型训练做好准备。

构造数据块

我们如何从DataFrame对象转换为DataLoaders对象? 我们通常建议尽可能使用数据块 API 创建 DataLoaders对象,因为它提供了灵活性和简单性的良好组合。在这里,我们将向您展示我们DataLoaders在实践中使用数据块 API 构建对象所采取的步骤,并以该数据集为例。

正如我们所见,PyTorch 和 fastai 有两个主要的类来表示和访问训练集或验证集:

Dataset

返回单个项目的自变量和因变量的元组的集合

DataLoader

提供小批量流的迭代器,其中每个小批量是一批自变量和一批因变量的元组

最重要的是,fastai 提供了两个类来将你的训练和验证集结合在一起:

Datasets

包含训练Dataset和验证的迭代器Dataset

DataLoaders

包含训练DataLoader和验证的对象DataLoader

由于 aDataLoader构建在 a 之上Dataset并为其添加了额外的功能(将多个项目整理成一个小批量),因此通常最容易从创建和测试开始Datasets,然后查看DataLoaders是否正常工作。

当我们创建一个DataBlock,我们逐渐建立,一步一步,和 使用笔记本一路检查我们的数据。这是确保您在编码时保持动力并密切关注任何问题的好方法。调试起来很容易,因为您知道如果出现问题,它就在您刚刚键入的代码行中!

让我们从最简单的情况开始,这是一个不带参数创建的数据块:

dblock = DataBlock()

我们可以从中创建一个Datasets对象。唯一需要的是一个源——在这种情况下,我们的 DataFrame:

dsets = dblock.datasets(df)

这包含一个train和一个valid数据集,我们可以对其进行索引:

dsets.train[0]
(fname       008663.jpg
 labels      car person
 is_valid    False
 Name: 4346, dtype: object,
 fname       008663.jpg
 labels      car person
 is_valid    False
 Name: 4346, dtype: object)

如您所见,这只是两次返回 DataFrame 的一行。这是因为默认情况下,数据块假定我们有两个东西:输入和目标。我们将需要从 DataFrame 中获取适当的字段,我们可以通过传递get_xget_y函数来做到这一点:

dblock = DataBlock(get_x = lambda r: r['fname'], get_y = lambda r: r['labels'])
dsets = dblock.datasets(df)
dsets.train[0]
'005620.jpg', 'aeroplane')

如您所见,我们不是以通常的方式定义函数,而是 使用 Python 的lambda关键字。这只是定义然后引用函数的快捷方式。以下更详细的方法是相同的:

def get_x(r): return r['fname']
def get_y(r): return r['labels']
dblock = DataBlock(get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
('002549.jpg', 'tvmonitor')

Lambda 函数非常适合快速迭代,但它们与序列化不兼容,因此我们建议您使用更详细的 如果你想Learner在训练后导出你的方法(如果你只是在试验,lambdas 很好)。

我们可以看到,需要将自变量转换为完整路径,以便我们可以将其作为图像打开,并且需要将因变量拆分为空格字符(这是 Pythonsplit函数的默认值),以便它变成了一个列表:

def get_x(r): return path/'train'/r['fname']
def get_y(r): return r['labels'].split(' ')
dblock = DataBlock(get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
(Path('/home/sgugger/.fastai/data/pascal_2007/train/008663.jpg'),
 ['car', 'person'])

要真正打开图像并转换为张量,我们需要使用一组变换;块类型将为我们提供这些。我们可以使用之前使用过的相同块类型,但有一个例外:ImageBlock将再次正常工作,因为我们有一个路径 这指向一个有效的图像,但它 CategoryBlock不起作用。问题是块返回一个整数,但我们需要能够为每个项目有多个标签。为了解决这个问题,我们使用MultiCategoryBlock. 这种类型的块需要接收一个字符串列表,就像我们在这种情况下一样,所以让我们测试一下:

dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
                   get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
(PILImage mode=RGB size=500x375,
 TensorMultiCategory([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,
 > 0., 0., 0., 0., 0., 0.]))

如您所见,我们的类别列表的编码方式与常规CategoryBlock. 在这种情况下,我们有一个整数表示存在哪个类别,基于它在我们的词汇中的位置。然而,在这种情况下,我们有一个 0 列表,在该类别存在的任何位置都有一个 1。例如,如果第二个和第四个位置有一个 1,这意味着该图像中存在第二个和第四个词汇项。这是众所周知的作为one-hot 编码。我们不能轻易使用类别索引列表的原因是每个列表的长度不同,而 PyTorch 需要张量,其中所有内容都必须具有相同的长度。

Ont-hot编码

使用 0 的向量,每个位置都有 1在数据中表示,以编码整数列表。

让我们检查一下这个例子的类别代表什么(我们使用了方便的torch.where函数,它告诉我们条件为真或假的所有索引):

idxs = torch.where(dsets.train[0][1]==1.)[0]
dsets.train.vocab[idxs]
(#1) ['dog']

使用 NumPy 数组、PyTorch 张量和 fastai 的L类,我们可以直接使用列表或向量进行索引,这使得很多代码(比如这个例子)更加清晰和简洁。

到目前为止,我们一直忽略该列is_valid,这意味着 DataBlock默认情况下一直使用随机拆分。要明确选择验证集的元素,我们需要编写一个函数并将其传递给splitter(或使用 fastai 的预定义函数或类之一)。它将获取项目(这里是我们的整个 DataFrame)并且必须返回两个(或更多)整数列表:

def splitter(df):
    train = df.index[~df['is_valid']].tolist()
    valid = df.index[df['is_valid']].tolist()
    return train,valid

dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
                   splitter=splitter,
                   get_x=get_x,
                   get_y=get_y)

dsets = dblock.datasets(df)
dsets.train[0]
(PILImage mode=RGB size=500x333,
 TensorMultiCategory([0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,
 > 0., 0., 0., 0., 0., 0.]))

正如我们所讨论的,a将 aDataLoader中的项目整理Dataset 成一个小批量。这是张量的元组,其中每个张量只是将项目中该位置的Dataset项目堆叠起来。

现在我们已经确认各个项目看起来没问题,还有一步,我们需要确保我们可以创建我们的DataLoaders,即确保每个项目的大小相同。为此,我们可以使用 RandomResizedCrop

dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
                   splitter=splitter,
                   get_x=get_x,
                   get_y=get_y,
                   item_tfms = RandomResizedCrop(128, min_scale=0.35))
dls = dblock.dataloaders(df)

现在我们可以显示我们的数据样本:

dls.show_batch(nrows=1, ncols=3)
【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

请记住,如果您在 DataLoaders从您的DataBlock. 发生在您的DataBlock,您可以使用summary我们在上一章中介绍的方法。

我们的数据现在已准备好用于训练模型。正如我们将看到的,当我们创建我们的 时,什么都不会改变Learner,但在幕后,fastai 库将为我们选择一个新的损失函数:二元交叉熵。

二元交叉熵

现在我们将创建我们的Learner. 我们在 第 4 章,一个Learner对象包含四个主要的东西:模型、一个DataLoaders对象、一个Optimizer和要使用的损失函数。我们已经有了自己的DataLoaders,我们可以利用 fastai 的resnet模型(稍后我们将学习如何从头开始创建),并且我们知道如何创建 SGD优化器。所以让我们专注于确保我们有一个合适的损失函数。为此,让我们使用cnn_learner 创建一个Learner,这样我们就可以查看它的激活:

learn = cnn_learner(dls, resnet18)

 我们还看到 a 中的模型Learner通常是 a 的对象 类继承自nn.Module,我们可以使用括号调用它,它将返回模型的激活。你应该将它作为一个小批量传递给你的自变量。我们可以通过从我们的获取一个小批量DataLoader然后将其传递给模型来进行尝试:

x,y = dls.train.one_batch()
activs = learn.model(x)
activs.shape
torch.Size([64, 20])

想一想为什么activs会有这种形状——我们有 64 个批量大小,我们需要计算 20 个类别中每个类别的概率。以下是其中一个激活的样子:

activs[0]
tensor([ 2.0258, -1.3543,  1.4640,  1.7754, -1.2820, -5.8053,  3.6130,  0.7193,
 > -4.3683, -2.5001, -2.8373, -1.8037,  2.0122,  0.6189,  1.9729,  0.8999,
 > -2.6769, -0.3829,  1.2212,  1.6073],
       device='cuda:0', grad_fn=<SelectBackward>)

了解如何手动获取小批量并将其传递给模型,并查看激活和损失,这对于调试模型非常重要。它对学习也很有帮助,因此您可以准确地看到正在发生的事情。

它们还没有缩放到 0 和 1 之间,但是我们在 第 4 章中学习了如何使用该sigmoid函数来做到这一点。我们还看到了如何基于此计算损失——这是我们在 第 4 章中的损失函数,加上log前一章中讨论的:

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

请注意,因为我们有一个单热编码的因变量,所以我们 不能直接使用nll_lossor softmax(因此我们不能使用cross_entropy):

  • softmax,正如我们所看到的,要求所有预测总和为 1,并且倾向于推动一个激活比其他激活大得多(因为 )的使用exp;但是,我们很可能有多个我们确信会出现在图像中的对象,因此将激活的最大总和限制为 1 并不是一个好主意。同样的道理,如果我们不认为任何类别出现在图像中,我们可能希望总和小于1。

  • nll_loss,如我们所见,仅返回一个激活的值:与项目的单个标签对应的单个激活。当我们有多个标签时,这没有意义。

另一方面,由于 PyTorch 的元素操作的魔力,mnist_losslog一起使用的binary_cross_entropy函数 提供了我们所需要的东西。每个激活都将与每列的每个目标进行比较,因此我们不必做任何事情来使此函数适用于多个列。

我真正喜欢使用诸如 PyTorch 之类的库以及广播和元素操作的一件事是我经常发现我可以编写对单个项目或一批项目同样有效的代码,而无需更改。binary_cross_entropy就是一个很好的例子。通过使用这些操作,我们不必自己编写循环,并且可以依赖 PyTorch 来根据我们正在使用的张量的等级进行我们需要的循环。

PyTorch 已经为我们提供了这个功能。事实上,它提供了许多版本,名称相当混乱!

F.binary_cross_entropy及其等效模块 nn.BCELoss计算 one-hot-encoded 目标上的交叉熵,但是 不包括初始sigmoid. 通常,对于 one-hot-encoded 您想要的目标F.binary_cross_entropy_with_logits(或 nn.BCEWithLogitsLoss),它们在单个函数中同时执行 sigmoid 和二元交叉熵,如前面的示例中所示。

单标签数据集(如 MNIST 或 Pet 数据集)的等价物,其中目标被编码为单个整数,F.nll_loss或者nn.NLLLoss 对于没有初始 softmax 的版本,F.cross_entropy或者 nn.CrossEntropyLoss对于具有初始 softmax 的版本。

由于我们有一个单热编码目标,我们将使用BCEWithLogitsLoss

loss_func = nn.BCEWithLogitsLoss()
loss = loss_func(activs, y)
loss
tensor(1.0082, device='cuda:0', grad_fn=<BinaryCrossEntropyWithLogitsBackward>)

我们不需要告诉 fastai 使用这个损失 函数(尽管我们可以根据需要),因为它会自动为我们选择。fastai 知道DataLoaders有多个类别标签,所以它会nn.BCEWithLogitsLoss默认使用。

与上一章相比的一个变化是我们使用的度量:因为这是一个多标签问题,我们不能使用准确率 功能。这是为什么?好吧,准确度是将我们的输出与我们的目标进行比较,如下所示:

def accuracy(inp, targ, axis=-1):
    "Compute accuracy with `targ` when `pred` is bs * n_classes"
    pred = inp.argmax(dim=axis)
    return (pred == targ).float().mean()

预测的类别是激活度最高的类别(这就是这样argmax做的)。在这里它不起作用,因为我们可以对单个图像进行多个预测。将 sigmoid 应用于我们的激活后(使它们介于 0 和 1 之间),我们需要通过选择一个阈值 来决定哪些是 0,哪些是 1 。每个高于阈值的值将被视为 1,每个低于阈值的值将被视为 0:

def accuracy_multi(inp, targ, thresh=0.5, sigmoid=True):
    "Compute accuracy when `inp` and `targ` are the same size."
    if sigmoid: inp = inp.sigmoid()
    return ((inp>thresh)==targ.bool()).float().mean()

如果我们accuracy_multi直接作为指标传递,它将使用 的默认值 threshold,即 0.5。我们可能想要调整该默认值并创建一个accuracy_multi具有不同默认值的新版本。为了解决这个问题,Python 中有一个函数 称为partial。它允许我们函数与一些参数或关键字参数绑定,从而创建该函数的新版本,无论何时调用它,始终包含这些参数。例如,这是一个带有两个参数的简单函数:

def say_hello(name, say_what="Hello"): return f"{say_what} {name}."
say_hello('Jeremy'),say_hello('Jeremy', 'Ahoy!')
('Hello Jeremy.', 'Ahoy! Jeremy.')

我们可以使用以下命令切换到该函数的法语版本partial

f = partial(say_hello, say_what="Bonjour")
f("Jeremy"),f("Sylvain")
('Bonjour Jeremy.', 'Bonjour Sylvain.')

我们现在可以训练我们的模型了。让我们尝试将指标的准确度阈值设置为 0.2:

learn = cnn_learner(dls, resnet50, metrics=partial(accuracy_multi, thresh=0.2))
learn.fine_tune(3, base_lr=3e-3, freeze_epochs=4)

选择一个门槛很重要。如果你选择一个阈值 这太低了,您经常无法选择正确标记的对象。我们可以通过更改我们的指标然后调用 来查看这一点validate,它会返回验证损失和指标:

learn.metrics = partial(accuracy_multi, thresh=0.1)
learn.validate()
(#2) [0.10436797887086868,0.93057781457901]

如果您选择的阈值太高,您将只选择模型非常有信心的对象:

learn.metrics = partial(accuracy_multi, thresh=0.99)
learn.validate()
(#2) [0.10436797887086868,0.9416930675506592]

我们可以通过尝试几个级别并查看最有效的方法来找到最佳阈值。如果我们只抓取一次预测,这会快得多:

preds,targs = learn.get_preds()

然后我们可以直接调用度量。请注意,默认 get_preds情况下为我们应用输出激活函数(在本例中为 sigmoid),因此我们需要告诉 accuracy_multi不要应用它:

accuracy_multi(preds, targs, thresh=0.9, sigmoid=False)
TensorMultiCategory(0.9554)

我们现在可以使用这种方法来找到最佳阈值水平:

xs = torch.linspace(0.05,0.95,29)
accs = [accuracy_multi(preds, targs, thresh=i, sigmoid=False) for i in xs]
plt.plot(xs,accs);

【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

在这种情况下,我们使用验证集来选择一个超参数(阈值),这是验证的目的 放。有时学生们表达了他们对我们可能过度拟合验证集的担忧,因为我们正在尝试很多值来看看哪个是最好的。但是,正如您在图中看到的那样,在这种情况下更改阈值会导致曲线平滑,因此我们显然没有选择不合适的异常值。这是一个很好的例子,说明您必须注意理论(不要尝试大量超参数值,否则您可能会过度拟合验证集)与实践(如果关系平滑,则可以这样做)之间的差异。

本章专门讨论多标签分类的部分到此结束。接下来,我们将看一个回归问题。

回归

很容易将深度学习模型视为分类为领域,如计算机视觉NLP等。事实上,fastai 就是这样对其应用程序进行分类的——主要是因为大多数人习惯于这样思考事物。

但实际上,这隐藏了一个更有趣和更深入的视角。模型由其独立和依赖定义 变量,以及它的损失函数。这意味着除了简单的基于域的拆分之外,还有更多的模型。也许我们有一个独立变量是图像,而依赖变量是文本(例如,从图像生成标题);或者也许我们有一个自变量是文本,而依赖变量是图像(例如,从标题生成图像——这实际上是深度学习可以做到的!);或者我们将图像、文本和表格数据作为自变量,并且我们正试图预测产品购买……可能性真的是无穷无尽的。

能够超越固定的应用程序来制作自己的小说 新问题的解决方案,它有助于真正理解数据块 API(也许还有中间层 API,我们将在本书后面看到)。作为一个例子,让我们考虑图像回归的问题。这是指从一个数据集中学习,其中自变量是图像,因变量是一个或多个浮点数。我们经常看到人们将图像回归视为一个完全独立的应用程序——但正如您将在此处看到的,我们可以将其视为数据块 API 之上的另一个 CNN。

我们将直接跳到一个有点棘手的图像回归变体,因为我们知道你已经准备好了!我们要做一个关键点模型。一个关键点是指 到图像中表示的特定位置——在这种情况下,我们将使用人的图像,我们将在每张图像中寻找人脸的中心。这意味着我们实际上将为每个图像预测 两个值:人脸中心的行和列。

组装数据

本节我们将使用Biwi Kinect Head Pose 数据集。我们将照常下载数据集:

path = untar_data(URLs.BIWI_HEAD_POSE)

让我们看看我们有什么!

path.ls()
(#50) [Path('13.obj'),Path('07.obj'),Path('06.obj'),Path('13'),Path('10'),Path('
 > 02'),Path('11'),Path('01'),Path('20.obj'),Path('17')...]

有 24 个目录,编号从 01 到 24(它们对应于拍摄的不同人物),每个目录都有一个对应的.obj文件(我们这里不需要)。让我们看一下这些目录之一:

(path/'01').ls()
(#1000) [Path('01/frame_00281_pose.txt'),Path('01/frame_00078_pose.txt'),Path('0
 > 1/frame_00349_rgb.jpg'),Path('01/frame_00304_pose.txt'),Path('01/frame_00207_
 > pose.txt'),Path('01/frame_00116_rgb.jpg'),Path('01/frame_00084_rgb.jpg'),Path
 > ('01/frame_00070_rgb.jpg'),Path('01/frame_00125_pose.txt'),Path('01/frame_003
 > 24_rgb.jpg')...]

在子目录中,我们有不同的框架。它们每个都带有一个图像(_rgb.jpg)和一个姿势文件(_pose.txt)。我们可以很容易地用 递归获取所有图像文件get_image_files,然后编写一个函数,将图像文件名转换为其关联的姿势文件:

img_files = get_image_files(path)
def img2pose(x): return Path(f'{str(x)[:-7]}pose.txt')
img2pose(img_files[0])
Path('13/frame_00349_pose.txt')

让我们看一下我们的第一张图片:

im = PILImage.create(img_files[0])
im.shape
(480, 640)
im.to_thumb(160)
【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

Biwi 数据集网站用于解释姿势文本文件的格式 与每个图像相关联,显示头部中心的位置。这个细节对我们的目的并不重要,所以我们只展示我们用来提取头部中心点的函数:

cal = np.genfromtxt(path/'01'/'rgb.cal', skip_footer=6)
def get_ctr(f):
    ctr = np.genfromtxt(img2pose(f), skip_header=3)
    c1 = ctr[0] * cal[0][0]/ctr[2] + cal[0][2]
    c2 = ctr[1] * cal[1][1]/ctr[2] + cal[1][2]
    return tensor([c1,c2])

 此函数将坐标作为两个项目的张量返回:

get_ctr(img_files[0])
tensor([384.6370, 259.4787])

我们可以将此函数传递给DataBlockas get_y,因为它负责标记每个项目。我们将图像大小调整为输入大小的一半,以加快训练速度。

需要注意的重要一点是,我们不应该只使用随机分配器。同样的人出现在 该数据集中有多个图像,但我们希望确保我们的模型可以推广到它尚未见过的人。数据集中的每个文件夹都包含一个人的图像。因此,我们可以创建一个仅返回一个人的拆分器函数True,从而生成一个仅包含该人图像的验证集。

与前面的数据块示例的唯一其他区别是第二个块是PointBlock. 这是必要的,以便 fastai 知道 标签代表坐标;这样,它就知道在进行数据增强时,它应该对这些坐标进行与图像相同的增强:

biwi = DataBlock(
    blocks=(ImageBlock, PointBlock),
    get_items=get_image_files,
    get_y=get_ctr,
    splitter=FuncSplitter(lambda o: o.parent.name=='13'),
    batch_tfms=[*aug_transforms(size=(240,320)),
                Normalize.from_stats(*imagenet_stats)]
)

点和数据增强

我们不知道其他库(fastai 除外)会自动且正确地将数据增强应用于坐标。因此,如果您正在使用另一个库,则可能需要针对此类问题禁用数据增强。

在进行任何建模之前,我们应该查看我们的数据以确认它看起来不错:

dls = biwi.dataloaders(path)
dls.show_batch(max_n=9, figsize=(8,6))
【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

看起来不错!除了直观地查看批次之外,还可以查看底层张量(尤其是作为学生;这将有助于澄清您对模型真正看到的内容的理解):

xb,yb = dls.one_batch()
xb.shape,yb.shape
(torch.Size([64, 3, 240, 320]), torch.Size([64, 1, 2]))

确保您了解为什么这些是我们小批量的形状。

这是因变量中一行的示例:

yb[0]
tensor([[0.0111, 0.1810]], device='cuda:0')

如您所见,我们不必使用单独的图像回归应用程序;我们所要做的就是标记数据并告诉 fastai 自变量和因变量代表什么类型的数据。

创建我们的Learner. 我们将使用与以前相同的函数,并带有一个新参数,我们将准备好训练我们的模型。

训练模型

像往常一样,我们可以使用cnn_learner创建我们的Learner. 还记得第 1 章中我们如何y_range告诉 fastai 我们的目标范围吗?我们将在这里做同样的事情(fastai 和 PyTorch 中的坐标总是在 –1 和 +1 之间重新调整):

learn = cnn_learner(dls, resnet18, y_range=(-1,1))

y_range在 fastai using 中实现sigmoid_range,定义如下:

def sigmoid_range(x, lo, hi): return torch.sigmoid(x) * (hi-lo) + lo

如果y_range已定义,则将其设置为模型的最后一层。花点时间思考一下这个函数做了什么,以及为什么它强制模型输出范围内的激活(lo,hi)

这是它的样子:

plot_function(partial(sigmoid_range,lo=-1,hi=1), min=-4, max=4)
【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

我们没有指定损失函数,这意味着 我们得到了 fastai 选择的默认值。让我们看看它为我们挑选了什么:

dls.loss_func
FlattenedLoss of MSELoss()

这是有道理的,因为当坐标用作因变量时, 大多数时候,我们可能会试图预测尽可能接近的东西;这基本上就是 MSELoss(均方误差损失)所做的。如果要使用不同的损失函数,可以cnn_learner使用loss_func 参数传递给它。

另请注意,我们没有指定任何指标。这是因为 MSE 已经是这项任务的有用指标(尽管在我们取平方根之后它可能更容易解释)。

我们可以使用学习率查找器选择一个好的学习率:

learn.lr_find()

【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

我们将尝试 1e-2 的 LR:

lr = 1e-2
learn.fine_tune(3, lr)

一般来说,当我们运行这个时,我们会得到大约 0.0001 的损失,这对应于这个平均坐标预测误差:

math.sqrt(0.0001)
0.01

这听起来非常准确!但重要的是看一看 在我们的结果中Learner.show_results。左侧有实际(ground truth)坐标,右侧有我们模型的预测:

learn.show_results(ds_idx=1, max_n=3, figsize=(6,8))
【Pytorch with fastai】第 6 章 :其他计算机视觉问题-LMLPHP

 令人惊奇的是,我们只用了几分钟的计算,就创建了如此精确的关键点模型,而且没有任何特殊的领域特定应用程序。这就是在灵活的 API 上构建并使用迁移学习的力量!尤其令人惊讶的是,我们能够如此有效地使用迁移学习,即使在完全不同的任务之间也是如此;我们的预训练模型经过训练可以进行图像分类,并且我们针对图像回归进行了微调。

结论

在乍一看完全不同的问题(单标签分类、多标签分类和回归)中,我们最终使用相同的模型,但输出数量不同。损失函数是变化的一件事,这就是为什么 仔细检查您是否针对您的问题使用了正确的损失函数,这一点很重要。

fastai 将自动尝试从您构建的数据中选择正确的数据,但如果您使用纯 PyTorch 构建您 DataLoader的 s,请确保您认真考虑选择的损失函数,并记住您很可能需要 以下内容:

  • nn.CrossEntropyLoss用于单标签分类

  • nn.BCEWithLogitsLoss用于多标签分类

  • nn.MSELoss用于回归

11-16 06:58