06. PyTorch迁移学习¶
注意: 本笔记本使用了
torchvision
的新多权重支持API(在torchvision
v0.13+中可用)。
到目前为止,我们已经手动构建了几个模型。
但它们的性能一直不佳。
你可能会想,是否已经存在一个针对我们问题的性能良好的模型?
而在深度学习领域,答案通常是肯定的。
我们将通过使用一种强大的技术——迁移学习来了解如何实现这一点。
什么是迁移学习?¶
迁移学习 使我们能够采用另一个模型从另一个问题中学到的模式(也称为权重),并将它们应用于我们自己的问题。
例如,我们可以采用一个计算机视觉模型从 ImageNet(包含数百万张不同物体的图像)等数据集中学到的模式,并将它们用于我们的 FoodVision Mini 模型。
或者,我们可以采用一个 语言模型(一个通过大量文本学习语言表示的模型)的模式,并将其作为分类不同文本样本的模型的基础。
前提是:找到一个表现良好的现有模型,并将其应用于你自己的问题。
迁移学习在计算机视觉和自然语言处理(NLP)中的应用示例。在计算机视觉中,一个计算机视觉模型可能在 ImageNet 上的数百万张图像上学习模式,然后利用这些模式推断另一个问题。对于 NLP,一个语言模型可能通过阅读所有维基百科(甚至更多)来学习语言结构,然后将这些知识应用于不同的问题。
为什么要使用迁移学习?¶
使用迁移学习有两个主要好处:
- 可以利用一个已经在类似我们自己的问题上证明有效的现有模型(通常是神经网络架构)。
- 可以利用一个已经在类似我们自己的数据上学习了模式的模型。这通常会导致在使用较少自定义数据的情况下实现出色的结果。
我们将为我们的FoodVision Mini问题测试这些方法,我们将采用一个在ImageNet上预训练的计算机视觉模型,并尝试利用其底层学习到的表示来对披萨、牛排和寿司的图像进行分类。
研究与实践都支持使用迁移学习。
最近一篇机器学习研究论文的发现建议从业者尽可能使用迁移学习。
一项从从业者角度研究从头开始训练与使用迁移学习哪个更好的研究发现,迁移学习在成本和时间方面更为有益。来源: 如何训练你的ViT?数据、增强和正则化在视觉变换器中的应用 论文第6节(结论)。
Jeremy Howard(fastai 创始人)也是迁移学习的坚定支持者。
真正产生影响的事情(迁移学习),如果我们能在迁移学习上做得更好,这将是一个改变世界的事情。突然之间,更多的人可以用更少的资源和更少的数据做出世界级的工作。 — Jeremy Howard在Lex Fridman播客上的讲话
在哪里找到预训练模型¶
深度学习的世界是一个令人惊叹的地方。
如此令人惊叹,以至于世界各地的许多人都分享他们的工作。
通常,最先进研究的代码和预训练模型在发布后的几天内就会被公开。
而且,有几个地方你可以找到预训练模型,用于你自己的问题。
位置 | 那里有什么? | 链接 |
---|---|---|
PyTorch 领域库 | 每个 PyTorch 领域库(如 torchvision 、torchtext )都带有某种形式的预训练模型。这些模型可以直接在 PyTorch 中使用。 |
torchvision.models , torchtext.models , torchaudio.models , torchrec.models |
HuggingFace Hub | 来自世界各地组织的许多不同领域(视觉、文本、音频等)的预训练模型系列。还有许多不同的数据集。 | https://huggingface.co/models, https://huggingface.co/datasets |
timm (PyTorch 图像模型)库 |
几乎所有最新的计算机视觉模型以及许多其他有用的计算机视觉功能,都以 PyTorch 代码的形式提供。 | https://github.com/rwightman/pytorch-image-models |
Paperswithcode | 一系列附有代码实现的最新最先进的机器学习论文。你还可以在这里找到不同任务上模型性能的基准。 | https://paperswithcode.com/ |
有了上述高质量资源,在开始每一个深度学习问题时,询问“我的问题是否存在预训练模型?”应该成为一种常见的做法。
练习: 花5分钟浏览
torchvision.models
和 HuggingFace Hub Models 页面,你发现了什么?(这里没有正确答案,只是为了练习探索)
我们将涵盖的内容¶
我们将从 torchvision.models
中获取一个预训练模型,并对其进行定制,以便在我们的 FoodVision Mini 问题上工作(并有望改进)。
主题 | 内容 |
---|---|
0. 环境设置 | 在前几节中,我们已经编写了不少有用的代码,让我们下载它并确保我们可以再次使用它。 |
1. 获取数据 | 让我们获取我们一直在使用的披萨、牛排和寿司图像分类数据集,以尝试改进我们模型的结果。 |
2. 创建数据集和数据加载器 | 我们将使用在第05章中编写的 data_setup.py 脚本来设置我们的数据加载器。 |
3. 获取并定制预训练模型 | 在这里,我们将从 torchvision.models 下载一个预训练模型,并将其定制为我们自己的问题。 |
4. 训练模型 | 让我们看看新的预训练模型在我们披萨、牛排、寿司数据集上的表现如何。我们将使用前一章中创建的训练函数。 |
5. 通过绘制损失曲线评估模型 | 我们的第一个迁移学习模型表现如何?它是过拟合还是欠拟合? |
6. 对测试集中的图像进行预测 | 查看模型的评估指标是一回事,但查看其在测试样本上的预测是另一回事,让我们可视化,可视化,可视化! |
如何获取帮助?¶
本课程的所有资料都可以在GitHub上找到。
如果你遇到问题,可以在课程的GitHub讨论页面上提问。
当然,还有PyTorch文档和PyTorch开发者论坛,这是关于PyTorch所有事项非常有帮助的地方。
0. 环境设置¶
让我们开始,首先导入/下载本节所需的模块。
为了节省编写额外代码的工作量,我们将利用上一节 05. PyTorch Going Modular 中创建的一些 Python 脚本(如 data_setup.py
和 engine.py
)。
具体来说,我们将从 pytorch-deep-learning
仓库下载 going_modular
目录(如果尚未下载的话)。
我们还将获取 torchinfo
包(如果尚未安装)。
torchinfo
将在后续帮助我们以可视化方式展示模型。
注意: 截至 2022 年 6 月,本笔记本使用
torch
和torchvision
的 nightly 版本,因为需要torchvision
v0.13+ 来使用更新的多权重 API。您可以使用以下命令安装这些版本。
# For this notebook to run with updated APIs, we need torch 1.12+ and torchvision 0.13+
try:
import torch
import torchvision
assert int(torch.__version__.split(".")[1]) >= 12, "torch version should be 1.12+"
assert int(torchvision.__version__.split(".")[1]) >= 13, "torchvision version should be 0.13+"
print(f"torch version: {torch.__version__}")
print(f"torchvision version: {torchvision.__version__}")
except:
print(f"[INFO] torch/torchvision versions not as required, installing nightly versions.")
!pip3 install -U torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
import torch
import torchvision
print(f"torch version: {torch.__version__}")
print(f"torchvision version: {torchvision.__version__}")
torch version: 1.13.0.dev20220620+cu113 torchvision version: 0.14.0.dev20220620+cu113
# Continue with regular imports
import matplotlib.pyplot as plt
import torch
import torchvision
from torch import nn
from torchvision import transforms
# Try to get torchinfo, install it if it doesn't work
try:
from torchinfo import summary
except:
print("[INFO] Couldn't find torchinfo... installing it.")
!pip install -q torchinfo
from torchinfo import summary
# Try to import the going_modular directory, download it from GitHub if it doesn't work
try:
from going_modular.going_modular import data_setup, engine
except:
# Get the going_modular scripts
print("[INFO] Couldn't find going_modular scripts... downloading them from GitHub.")
!git clone https://github.com/mrdbourke/pytorch-deep-learning
!mv pytorch-deep-learning/going_modular .
!rm -rf pytorch-deep-learning
from going_modular.going_modular import data_setup, engine
现在让我们设置设备无关的代码。
注意: 如果你正在使用 Google Colab,并且你还没有开启 GPU,现在是时候通过
Runtime -> Change runtime type -> Hardware accelerator -> GPU
来开启一个 GPU 了。
# Setup device agnostic code
device = "cuda" if torch.cuda.is_available() else "cpu"
device
'cuda'
1. 获取数据¶
在开始使用迁移学习之前,我们需要一个数据集。
为了了解迁移学习与之前模型构建尝试的比较,我们将下载用于FoodVision Mini的相同数据集。
让我们编写一些代码,从课程GitHub下载pizza_steak_sushi.zip
数据集,然后解压它。
我们还可以确保如果已经拥有数据,它不会重新下载。
import os
import zipfile
from pathlib import Path
import requests
# Setup path to data folder
data_path = Path("data/")
image_path = data_path / "pizza_steak_sushi"
# If the image folder doesn't exist, download it and prepare it...
if image_path.is_dir():
print(f"{image_path} directory exists.")
else:
print(f"Did not find {image_path} directory, creating one...")
image_path.mkdir(parents=True, exist_ok=True)
# Download pizza, steak, sushi data
with open(data_path / "pizza_steak_sushi.zip", "wb") as f:
request = requests.get("https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip")
print("Downloading pizza, steak, sushi data...")
f.write(request.content)
# Unzip pizza, steak, sushi data
with zipfile.ZipFile(data_path / "pizza_steak_sushi.zip", "r") as zip_ref:
print("Unzipping pizza, steak, sushi data...")
zip_ref.extractall(image_path)
# Remove .zip file
os.remove(data_path / "pizza_steak_sushi.zip")
data/pizza_steak_sushi directory exists.
很好!
现在我们有了之前一直在使用的相同数据集,即一系列以标准图像分类格式呈现的披萨、牛排和寿司图像。
接下来,让我们创建指向训练和测试目录的路径。
# Setup Dirs
train_dir = image_path / "train"
test_dir = image_path / "test"
2. 创建数据集和数据加载器¶
由于我们已经下载了 going_modular
目录,我们可以使用在 05. PyTorch Going Modular 部分创建的 data_setup.py
脚本来准备和设置我们的数据加载器。
但由于我们将使用 torchvision.models
中的预训练模型,我们需要先准备特定的图像变换。
2.1 为 torchvision.models
创建变换(手动创建)¶
注意: 截至
torchvision
v0.13+,关于如何使用torchvision.models
创建数据变换的方法有所更新。我将之前的方法称为“手动创建”,新的方法称为“自动创建”。本笔记本将展示这两种方法。
当使用预训练模型时,重要的是输入到模型中的自定义数据与模型原始训练数据准备方式相同。
在 torchvision
v0.13+ 之前,为了为 torchvision.models
中的预训练模型创建变换,文档指出:
所有预训练模型期望输入图像以相同方式归一化,即形状为 (3 x H x W) 的 3 通道 RGB 图像的小批次,其中 H 和 W 至少为 224。
图像必须加载到
[0, 1]
范围内,然后使用mean = [0.485, 0.456, 0.406]
和std = [0.229, 0.224, 0.225]
进行归一化。你可以使用以下变换进行归一化:
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
好消息是,我们可以通过组合以下变换来实现上述操作:
变换编号 | 所需变换 | 执行变换的代码 |
---|---|---|
1 | 大小为 [batch_size, 3, height, width] 的小批次,其中高度和宽度至少为 224x224。 |
使用 torchvision.transforms.Resize() 将图像调整为 [3, 224, 224] ,并使用 torch.utils.data.DataLoader() 创建图像批次。 |
2 | 值在 0 到 1 之间。 | 使用 torchvision.transforms.ToTensor() |
3 | 均值为 [0.485, 0.456, 0.406] (每个颜色通道的值)。 |
使用 torchvision.transforms.Normalize(mean=...) 调整图像的均值。 |
4 | 标准差为 [0.229, 0.224, 0.225] (每个颜色通道的值)。 |
使用 torchvision.transforms.Normalize(std=...) 调整图像的标准差。 |
注意: ^
torchvision.models
中的一些预训练模型可能具有不同于[3, 224, 224]
的大小,例如,一些模型可能接受[3, 240, 240]
的输入。对于特定的输入图像大小,请参阅文档。
问题: 均值和标准差值来自哪里?为什么我们需要这样做?
这些值是从数据中计算得出的。具体来说,是从 ImageNet 数据集中对一组图像计算得出的均值和标准差。
我们也不是必须这样做。神经网络通常能够自行计算出合适的数据分布(它们会自行计算均值和标准差的位置),但在开始时设置这些值可以帮助我们的网络更快地达到更好的性能。
让我们组合一系列 torchvision.transforms
来执行上述步骤。
# Create a transforms pipeline manually (required for torchvision < 0.13)
manual_transforms = transforms.Compose([
transforms.Resize((224, 224)), # 1. Reshape all images to 224x224 (though some models may require different sizes)
transforms.ToTensor(), # 2. Turn image values to between 0 & 1
transforms.Normalize(mean=[0.485, 0.456, 0.406], # 3. A mean of [0.485, 0.456, 0.406] (across each colour channel)
std=[0.229, 0.224, 0.225]) # 4. A standard deviation of [0.229, 0.224, 0.225] (across each colour channel),
])
太棒了!
现在我们已经准备好了一系列手动创建的变换来准备我们的图像,接下来让我们创建训练和测试数据加载器。
我们可以使用在05. PyTorch Going Modular Part 2中创建的data_setup.py
脚本中的create_dataloaders
函数来创建这些数据加载器。
我们将设置batch_size=32
,这样我们的模型每次可以看到32个样本的小批次。
并且我们可以使用上面创建的变换管道来变换我们的图像,通过设置transform=manual_transforms
。
注意: 我在这个笔记本中包含了手动创建变换的内容,因为你可能会遇到使用这种风格的资源。同样重要的是要注意,由于这些变换是手动创建的,它们也是无限可定制的。所以如果你想在你的变换管道中包含数据增强技术,你可以做到。
# Create training and testing DataLoaders as well as get a list of class names
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(train_dir=train_dir,
test_dir=test_dir,
transform=manual_transforms, # resize, convert images to between 0 & 1 and normalize them
batch_size=32) # set mini-batch size to 32
train_dataloader, test_dataloader, class_names
(<torch.utils.data.dataloader.DataLoader at 0x7fa9429a3a60>, <torch.utils.data.dataloader.DataLoader at 0x7fa9429a37c0>, ['pizza', 'steak', 'sushi'])
2.2 为 torchvision.models
创建转换(自动创建)¶
如前所述,当使用预训练模型时,重要的是输入到模型中的自定义数据与模型原始训练数据以相同方式准备。
上面我们看到了如何手动为预训练模型创建转换。
但从 torchvision
v0.13+ 开始,增加了一个自动创建转换的功能。
当你从 torchvision.models
设置模型并选择你想要使用的预训练模型权重时,例如,假设我们想要使用:
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
其中,
EfficientNet_B0_Weights
是我们想要使用的模型架构权重(torchvision.models
中有许多不同的模型架构选项)。DEFAULT
表示最佳可用权重(在 ImageNet 中表现最好)。- 注意: 根据你选择的模型架构,你可能会看到其他选项,如
IMAGENET_V1
和IMAGENET_V2
,通常版本号越高越好。不过,如果你想获得最佳可用权重,DEFAULT
是最简单的选项。更多信息请参阅torchvision.models
文档。
- 注意: 根据你选择的模型架构,你可能会看到其他选项,如
让我们试试看。
# Get a set of pretrained model weights
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = best available weights from pretraining on ImageNet
weights
EfficientNet_B0_Weights.IMAGENET1K_V1
现在,要访问与我们的 weights
相关联的变换,我们可以使用 transforms()
方法。
这实际上是在说:“获取用于在 ImageNet 上训练 EfficientNet_B0_Weights
的数据变换。”
# Get the transforms used to create our pretrained weights
auto_transforms = weights.transforms()
auto_transforms
ImageClassification( crop_size=[224] resize_size=[256] mean=[0.485, 0.456, 0.406] std=[0.229, 0.224, 0.225] interpolation=InterpolationMode.BICUBIC )
注意auto_transforms
与manual_transforms
非常相似,唯一的区别在于auto_transforms
是随我们选择的模型架构一起提供的,而我们必须手动创建manual_transforms
。
通过weights.transforms()
自动创建转换的好处在于,你可以确保使用与预训练模型在训练时相同的数据转换。
然而,自动创建转换的缺点是缺乏定制性。
我们可以像之前一样,使用auto_transforms
通过create_dataloaders()
创建DataLoader。
# Create training and testing DataLoaders as well as get a list of class names
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(train_dir=train_dir,
test_dir=test_dir,
transform=auto_transforms, # perform same data transforms on our own data as the pretrained model
batch_size=32) # set mini-batch size to 32
train_dataloader, test_dataloader, class_names
(<torch.utils.data.dataloader.DataLoader at 0x7fa942951460>, <torch.utils.data.dataloader.DataLoader at 0x7fa942951550>, ['pizza', 'steak', 'sushi'])
3. 获取预训练模型¶
好了,有趣的部分来了!
在过去的几个笔记本中,我们一直在从头开始构建 PyTorch 神经网络。
虽然这是一项很好的技能,但我们的模型并没有表现得如我们所愿。
这时候就轮到 迁移学习 登场了。
迁移学习的整个思想是 采用在一个与你相似的问题空间上已经表现良好的模型,然后根据你的用例对其进行定制。
由于我们正在处理一个计算机视觉问题(使用 FoodVision Mini 进行图像分类),我们可以在 torchvision.models
中找到预训练的分类模型。
探索文档,你会发现许多常见的计算机视觉架构骨干,例如:
架构骨干 | 代码 |
---|---|
ResNet | torchvision.models.resnet18() , torchvision.models.resnet50() ... |
VGG(类似于我们用于 TinyVGG 的模型) | torchvision.models.vgg16() |
EfficientNet | torchvision.models.efficientnet_b0() , torchvision.models.efficientnet_b1() ... |
VisionTransformer(ViT) | torchvision.models.vit_b_16() , torchvision.models.vit_b_32() ... |
ConvNeXt | torchvision.models.convnext_tiny() , torchvision.models.convnext_small() ... |
torchvision.models 中还有更多可用模型 |
torchvision.models... |
3.1 应该使用哪个预训练模型?¶
这取决于你的问题或你所使用的设备。
一般来说,模型名称中的数字越大(例如 efficientnet_b0()
-> efficientnet_b1()
-> efficientnet_b7()
)意味着性能更好,但模型也更大。
你可能会认为性能更好总是更好,对吧?
这是正确的,但一些性能更好的模型对于某些设备来说太大了。
例如,假设你想在移动设备上运行你的模型,你需要考虑设备上有限的计算资源,因此你会寻找一个更小的模型。
但如果你有无限的计算能力,正如The Bitter Lesson所述,你可能会选择你能找到的最大、最消耗计算资源的模型。
理解这种性能与速度与大小的权衡会随着时间和实践而来。
对我来说,我发现 efficientnet_bX
模型是一个不错的平衡点。
截至2022年5月,Nutrify(我正在开发的机器学习驱动的应用程序)由 efficientnet_b0
驱动。
Comma.ai(一家制作开源自动驾驶汽车软件的公司)使用 efficientnet_b2
来学习道路的表示。
注意: 尽管我们使用
efficientnet_bX
,但重要的是不要过于依赖任何一种架构,因为随着新研究的发布,它们总是在变化。最好进行实验、实验、实验,看看哪种方法对你的问题有效。
3.2 设置预训练模型¶
我们将使用的预训练模型是 torchvision.models.efficientnet_b0()
。
该架构来自论文 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks。
我们即将创建的示例:一个来自 torchvision.models
的预训练 EfficientNet_B0
模型,其输出层已调整,用于分类披萨、牛排和寿司图像。
我们可以使用与创建转换相同的代码来设置 EfficientNet_B0
预训练的 ImageNet 权重。
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = ImageNet 的最佳可用权重
这意味着该模型已经在数百万张图像上进行了训练,并且具有良好的图像数据基础表示。
该预训练模型的 PyTorch 版本能够在 ImageNet 的 1000 个类别中达到约 77.7% 的准确率。
我们还会将其发送到目标设备。
# OLD: Setup the model with pretrained weights and send it to the target device (this was prior to torchvision v0.13)
# model = torchvision.models.efficientnet_b0(pretrained=True).to(device) # OLD method (with pretrained=True)
# NEW: Setup the model with pretrained weights and send it to the target device (torchvision v0.13+)
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = best available weights
model = torchvision.models.efficientnet_b0(weights=weights).to(device)
#model # uncomment to output (it's very long)
注意: 在
torchvision
的早期版本中,您会使用以下代码创建一个预训练模型:
model = torchvision.models.efficientnet_b0(pretrained=True).to(device)
然而,在使用
torchvision
v0.13+ 运行此代码时,会导致如下错误:
UserWarning: 参数 'pretrained' 自 0.13 版本起已弃用,并将在 0.15 版本中移除,请改用 'weights'。
以及...
UserWarning: 自 0.13 版本起,除了权重枚举或 None 之外的其他参数已弃用,并将在 0.15 版本中移除。当前行为等同于传递 weights=EfficientNet_B0_Weights.IMAGENET1K_V1。您也可以使用 weights=EfficientNet_B0_Weights.DEFAULT 来获取最新的权重。
如果我们打印模型,会得到类似以下的内容:
非常非常多的层。
这是迁移学习的好处之一,利用世界上一些最优秀的工程师已经精心设计的现有模型,并将其应用于自己的问题。
我们的 efficientnet_b0
主要包含三个部分:
features
- 一系列卷积层和其他各种激活层,用于学习视觉数据的基线表示(这个基线表示/这些层通常被称为特征或特征提取器,"模型的基础层学习图像的不同特征")。avgpool
- 取features
层的输出并将其转换为特征向量。classifier
- 将特征向量转换为与所需输出类别数量相同维度的向量(由于efficientnet_b0
是在 ImageNet 上预训练的,而 ImageNet 有 1000 个类别,因此默认情况下out_features=1000
)。
3.3 使用 torchinfo.summary()
获取模型的概要信息¶
为了更深入地了解我们的模型,让我们使用 torchinfo
的 summary()
方法。
为此,我们将传入以下参数:
model
- 我们想要获取概要信息的模型。input_size
- 我们希望传递给模型的数据形状,对于efficientnet_b0
模型,输入大小是(batch_size, 3, 224, 224)
,尽管 其他efficientnet_bX
变体有不同的输入大小。- 注意: 许多现代模型由于使用了
torch.nn.AdaptiveAvgPool2d()
层,能够处理不同大小的输入图像。这个层会根据需要自适应地调整给定输入的output_size
。你可以通过向summary()
或你的模型传递不同大小的输入图像来尝试这一点。
- 注意: 许多现代模型由于使用了
col_names
- 我们希望看到的关于模型的各种信息列。col_width
- 概要信息中各列的宽度。row_settings
- 行中显示的特征。
# Print a summary using torchinfo (uncomment for actual output)
summary(model=model,
input_size=(32, 3, 224, 224), # make sure this is "input_size", not "input_shape"
# col_names=["input_size"], # uncomment for smaller output
col_names=["input_size", "output_size", "num_params", "trainable"],
col_width=20,
row_settings=["var_names"]
)
============================================================================================================================================ Layer (type (var_name)) Input Shape Output Shape Param # Trainable ============================================================================================================================================ EfficientNet (EfficientNet) [32, 3, 224, 224] [32, 1000] -- True ├─Sequential (features) [32, 3, 224, 224] [32, 1280, 7, 7] -- True │ └─Conv2dNormActivation (0) [32, 3, 224, 224] [32, 32, 112, 112] -- True │ │ └─Conv2d (0) [32, 3, 224, 224] [32, 32, 112, 112] 864 True │ │ └─BatchNorm2d (1) [32, 32, 112, 112] [32, 32, 112, 112] 64 True │ │ └─SiLU (2) [32, 32, 112, 112] [32, 32, 112, 112] -- -- │ └─Sequential (1) [32, 32, 112, 112] [32, 16, 112, 112] -- True │ │ └─MBConv (0) [32, 32, 112, 112] [32, 16, 112, 112] 1,448 True │ └─Sequential (2) [32, 16, 112, 112] [32, 24, 56, 56] -- True │ │ └─MBConv (0) [32, 16, 112, 112] [32, 24, 56, 56] 6,004 True │ │ └─MBConv (1) [32, 24, 56, 56] [32, 24, 56, 56] 10,710 True │ └─Sequential (3) [32, 24, 56, 56] [32, 40, 28, 28] -- True │ │ └─MBConv (0) [32, 24, 56, 56] [32, 40, 28, 28] 15,350 True │ │ └─MBConv (1) [32, 40, 28, 28] [32, 40, 28, 28] 31,290 True │ └─Sequential (4) [32, 40, 28, 28] [32, 80, 14, 14] -- True │ │ └─MBConv (0) [32, 40, 28, 28] [32, 80, 14, 14] 37,130 True │ │ └─MBConv (1) [32, 80, 14, 14] [32, 80, 14, 14] 102,900 True │ │ └─MBConv (2) [32, 80, 14, 14] [32, 80, 14, 14] 102,900 True │ └─Sequential (5) [32, 80, 14, 14] [32, 112, 14, 14] -- True │ │ └─MBConv (0) [32, 80, 14, 14] [32, 112, 14, 14] 126,004 True │ │ └─MBConv (1) [32, 112, 14, 14] [32, 112, 14, 14] 208,572 True │ │ └─MBConv (2) [32, 112, 14, 14] [32, 112, 14, 14] 208,572 True │ └─Sequential (6) [32, 112, 14, 14] [32, 192, 7, 7] -- True │ │ └─MBConv (0) [32, 112, 14, 14] [32, 192, 7, 7] 262,492 True │ │ └─MBConv (1) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True │ │ └─MBConv (2) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True │ │ └─MBConv (3) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True │ └─Sequential (7) [32, 192, 7, 7] [32, 320, 7, 7] -- True │ │ └─MBConv (0) [32, 192, 7, 7] [32, 320, 7, 7] 717,232 True │ └─Conv2dNormActivation (8) [32, 320, 7, 7] [32, 1280, 7, 7] -- True │ │ └─Conv2d (0) [32, 320, 7, 7] [32, 1280, 7, 7] 409,600 True │ │ └─BatchNorm2d (1) [32, 1280, 7, 7] [32, 1280, 7, 7] 2,560 True │ │ └─SiLU (2) [32, 1280, 7, 7] [32, 1280, 7, 7] -- -- ├─AdaptiveAvgPool2d (avgpool) [32, 1280, 7, 7] [32, 1280, 1, 1] -- -- ├─Sequential (classifier) [32, 1280] [32, 1000] -- True │ └─Dropout (0) [32, 1280] [32, 1280] -- -- │ └─Linear (1) [32, 1280] [32, 1000] 1,281,000 True ============================================================================================================================================ Total params: 5,288,548 Trainable params: 5,288,548 Non-trainable params: 0 Total mult-adds (G): 12.35 ============================================================================================================================================ Input size (MB): 19.27 Forward/backward pass size (MB): 3452.35 Params size (MB): 21.15 Estimated Total Size (MB): 3492.77 ============================================================================================================================================
哇哦!
这可真是个大模型!
从汇总输出来看,我们可以看到随着图像数据通过模型,各种输入和输出形状的变化。
而且还有更多的总参数(预训练权重)来识别我们数据中的不同模式。
作为参考,我们之前章节中的模型 TinyVGG 有 8,083 个参数,而 efficientnet_b0
有 5,288,548 个参数,增加了约 654 倍!
你觉得这意味着性能会更好吗?
3.4 冻结基础模型并调整输出层以适应我们的需求¶
迁移学习的过程通常是这样的:冻结预训练模型的一些基础层(通常是 features
部分),然后调整输出层(也称为头部/分类器层)以适应你的需求。
你可以通过更改输出层以适应你的问题来定制预训练模型的输出。原始的 torchvision.models.efficientnet_b0()
带有 out_features=1000
,因为在它训练的数据集 ImageNet 中有 1000 个类别。然而,对于我们的问题,即对披萨、牛排和寿司的图像进行分类,我们只需要 out_features=3
。
让我们冻结 efficientnet_b0
模型中 features
部分的所有层/参数。
注意: 冻结层意味着在训练过程中保持它们不变。例如,如果你的模型有预训练层,冻结它们就是说:“在训练过程中不要改变这些层中的任何模式,保持它们原来的样子。” 本质上,我们希望保持模型从 ImageNet 学到的预训练权重/模式作为主干,然后只改变输出层。
我们可以通过设置属性 requires_grad=False
来冻结 features
部分的所有层/参数。
对于 requires_grad=False
的参数,PyTorch 不会跟踪梯度更新,因此这些参数不会在训练过程中被我们的优化器改变。
本质上,具有 requires_grad=False
的参数是“不可训练”的或“冻结”在原地的。
# Freeze all base layers in the "features" section of the model (the feature extractor) by setting requires_grad=False
for param in model.features.parameters():
param.requires_grad = False
特征提取层已冻结!
现在让我们根据需要调整预训练模型的输出层或 classifier
部分。
目前我们的预训练模型的 out_features=1000
,因为 ImageNet 有 1000 个类别。
然而,我们并没有 1000 个类别,我们只有三个类别:披萨、牛排和寿司。
我们可以通过创建一系列新层来改变模型的 classifier
部分。
当前的 classifier
由以下部分组成:
(classifier): Sequential(
(0): Dropout(p=0.2, inplace=True)
(1): Linear(in_features=1280, out_features=1000, bias=True)
我们将保持 Dropout
层不变,使用 torch.nn.Dropout(p=0.2, inplace=True)
。
注意: Dropout 层 以概率
p
随机移除两层神经网络之间的连接。例如,如果p=0.2
,每次传递时会随机移除 20% 的连接。这种做法旨在通过确保剩余连接学习特征以补偿其他连接的移除(希望这些剩余特征更通用)来帮助正则化(防止过拟合)模型。
我们将保持 Linear
输出层的 in_features=1280
,但我们将 out_features
值更改为我们的 class_names
的长度(len(['pizza', 'steak', 'sushi']) = 3
)。
我们的新 classifier
层应与我们的 model
位于同一设备上。
# Set the manual seeds
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# Get the length of class_names (one output unit for each class)
output_shape = len(class_names)
# Recreate the classifier layer and seed it to the target device
model.classifier = torch.nn.Sequential(
torch.nn.Dropout(p=0.2, inplace=True),
torch.nn.Linear(in_features=1280,
out_features=output_shape, # same number of output units as our number of classes
bias=True)).to(device)
太好了!
输出层已更新,让我们再总结一下模型,看看有哪些变化。
# # Do a summary *after* freezing the features and changing the output classifier layer (uncomment for actual output)
summary(model,
input_size=(32, 3, 224, 224), # make sure this is "input_size", not "input_shape" (batch_size, color_channels, height, width)
verbose=0,
col_names=["input_size", "output_size", "num_params", "trainable"],
col_width=20,
row_settings=["var_names"]
)
============================================================================================================================================ Layer (type (var_name)) Input Shape Output Shape Param # Trainable ============================================================================================================================================ EfficientNet (EfficientNet) [32, 3, 224, 224] [32, 3] -- Partial ├─Sequential (features) [32, 3, 224, 224] [32, 1280, 7, 7] -- False │ └─Conv2dNormActivation (0) [32, 3, 224, 224] [32, 32, 112, 112] -- False │ │ └─Conv2d (0) [32, 3, 224, 224] [32, 32, 112, 112] (864) False │ │ └─BatchNorm2d (1) [32, 32, 112, 112] [32, 32, 112, 112] (64) False │ │ └─SiLU (2) [32, 32, 112, 112] [32, 32, 112, 112] -- -- │ └─Sequential (1) [32, 32, 112, 112] [32, 16, 112, 112] -- False │ │ └─MBConv (0) [32, 32, 112, 112] [32, 16, 112, 112] (1,448) False │ └─Sequential (2) [32, 16, 112, 112] [32, 24, 56, 56] -- False │ │ └─MBConv (0) [32, 16, 112, 112] [32, 24, 56, 56] (6,004) False │ │ └─MBConv (1) [32, 24, 56, 56] [32, 24, 56, 56] (10,710) False │ └─Sequential (3) [32, 24, 56, 56] [32, 40, 28, 28] -- False │ │ └─MBConv (0) [32, 24, 56, 56] [32, 40, 28, 28] (15,350) False │ │ └─MBConv (1) [32, 40, 28, 28] [32, 40, 28, 28] (31,290) False │ └─Sequential (4) [32, 40, 28, 28] [32, 80, 14, 14] -- False │ │ └─MBConv (0) [32, 40, 28, 28] [32, 80, 14, 14] (37,130) False │ │ └─MBConv (1) [32, 80, 14, 14] [32, 80, 14, 14] (102,900) False │ │ └─MBConv (2) [32, 80, 14, 14] [32, 80, 14, 14] (102,900) False │ └─Sequential (5) [32, 80, 14, 14] [32, 112, 14, 14] -- False │ │ └─MBConv (0) [32, 80, 14, 14] [32, 112, 14, 14] (126,004) False │ │ └─MBConv (1) [32, 112, 14, 14] [32, 112, 14, 14] (208,572) False │ │ └─MBConv (2) [32, 112, 14, 14] [32, 112, 14, 14] (208,572) False │ └─Sequential (6) [32, 112, 14, 14] [32, 192, 7, 7] -- False │ │ └─MBConv (0) [32, 112, 14, 14] [32, 192, 7, 7] (262,492) False │ │ └─MBConv (1) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False │ │ └─MBConv (2) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False │ │ └─MBConv (3) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False │ └─Sequential (7) [32, 192, 7, 7] [32, 320, 7, 7] -- False │ │ └─MBConv (0) [32, 192, 7, 7] [32, 320, 7, 7] (717,232) False │ └─Conv2dNormActivation (8) [32, 320, 7, 7] [32, 1280, 7, 7] -- False │ │ └─Conv2d (0) [32, 320, 7, 7] [32, 1280, 7, 7] (409,600) False │ │ └─BatchNorm2d (1) [32, 1280, 7, 7] [32, 1280, 7, 7] (2,560) False │ │ └─SiLU (2) [32, 1280, 7, 7] [32, 1280, 7, 7] -- -- ├─AdaptiveAvgPool2d (avgpool) [32, 1280, 7, 7] [32, 1280, 1, 1] -- -- ├─Sequential (classifier) [32, 1280] [32, 3] -- True │ └─Dropout (0) [32, 1280] [32, 1280] -- -- │ └─Linear (1) [32, 1280] [32, 3] 3,843 True ============================================================================================================================================ Total params: 4,011,391 Trainable params: 3,843 Non-trainable params: 4,007,548 Total mult-adds (G): 12.31 ============================================================================================================================================ Input size (MB): 19.27 Forward/backward pass size (MB): 3452.09 Params size (MB): 16.05 Estimated Total Size (MB): 3487.41 ============================================================================================================================================
嘿嘿!这里有很多变化呢!
让我们逐一来看:
- 可训练列 - 你会看到许多基础层(位于
features
部分的层)的可训练值为False
。这是因为我们设置了它们的requires_grad=False
属性。除非我们改变这一点,否则这些层在未来的训练中不会被更新。 classifier
的输出形状 - 模型的classifier
部分现在的输出形状值为[32, 3]
而不是[32, 1000]
。它的可训练值也是True
。这意味着它的参数将在训练过程中被更新。本质上,我们使用features
部分为我们的classifier
部分提供图像的基础表示,然后我们的classifier
层将学习如何将这种基础表示与我们面临的问题对齐。- 更少的可训练参数 - 之前有 5,288,548 个可训练参数。但由于我们冻结了模型中的许多层,只保留了
classifier
为可训练,现在只有 3,843 个可训练参数(甚至比我们的 TinyVGG 模型还要少)。虽然还有 4,007,548 个不可训练参数,但这些参数将为我们的输入图像创建一个基础表示,以便输入到我们的classifier
层。
注意: 模型的可训练参数越多,训练所需的计算资源和时间就越长。冻结模型的基础层并减少可训练参数意味着我们的模型应该能很快训练完成。这是迁移学习的一大好处,利用已经在类似问题上训练过的模型的已学习参数,只需稍微调整输出以适应你的问题。
4. 训练模型¶
现在我们有了一个半冻结的预训练模型,并且添加了自定义的 classifier
,让我们看看迁移学习的实际效果吧。
首先,我们需要创建一个损失函数和一个优化器。
由于我们仍在进行多类别分类,我们将使用 nn.CrossEntropyLoss()
作为损失函数。
并且我们将继续使用 torch.optim.Adam()
作为优化器,学习率设为 lr=0.001
。
# Define loss and optimizer
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
太棒了!
为了训练我们的模型,我们可以使用在05. PyTorch Going Modular 第 04 节中定义的 train()
函数。
train()
函数位于 going_modular
目录下的 engine.py
脚本中。
让我们看看训练我们的模型 5 个周期需要多长时间。
注意: 我们这里只训练
classifier
参数,因为模型中的其他参数已经被冻结。
# Set the random seeds
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# Start the timer
from timeit import default_timer as timer
start_time = timer()
# Setup training and save the results
results = engine.train(model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
optimizer=optimizer,
loss_fn=loss_fn,
epochs=5,
device=device)
# End the timer and print out how long it took
end_time = timer()
print(f"[INFO] Total training time: {end_time-start_time:.3f} seconds")
0%| | 0/5 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 1.0924 | train_acc: 0.3984 | test_loss: 0.9133 | test_acc: 0.5398 Epoch: 2 | train_loss: 0.8717 | train_acc: 0.7773 | test_loss: 0.7912 | test_acc: 0.8153 Epoch: 3 | train_loss: 0.7648 | train_acc: 0.7930 | test_loss: 0.7463 | test_acc: 0.8561 Epoch: 4 | train_loss: 0.7108 | train_acc: 0.7539 | test_loss: 0.6372 | test_acc: 0.8655 Epoch: 5 | train_loss: 0.6254 | train_acc: 0.7852 | test_loss: 0.6260 | test_acc: 0.8561 [INFO] Total training time: 8.977 seconds
哇!
我们的模型训练速度相当快(在我的本地机器上使用NVIDIA TITAN RTX GPU大约5秒,在Google Colab上使用NVIDIA P100 GPU大约15秒)。
而且看起来它完全超越了我们之前的模型结果!
使用efficientnet_b0
作为骨干,我们的模型在测试数据集上达到了近85%的准确率,几乎是我们使用TinyVGG所能达到的两倍。
对于一个我们用几行代码下载的模型来说,这已经相当不错了。
5. 通过绘制损失曲线评估模型¶
我们的模型看起来表现相当不错。
让我们绘制其损失曲线,看看训练过程随时间的变化情况。
我们可以使用在04. PyTorch 自定义数据集 第7.8节中创建的 plot_loss_curves()
函数来绘制损失曲线。
该函数存储在 helper_functions.py
脚本中,因此我们将尝试导入它并在没有该脚本时下载它。
# Get the plot_loss_curves() function from helper_functions.py, download the file if we don't have it
try:
from helper_functions import plot_loss_curves
except:
print("[INFO] Couldn't find helper_functions.py, downloading...")
with open("helper_functions.py", "wb") as f:
import requests
request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/helper_functions.py")
f.write(request.content)
from helper_functions import plot_loss_curves
# Plot the loss curves of our model
plot_loss_curves(results)
这些损失曲线看起来非常棒!
看起来两个数据集(训练集和测试集)的损失都在朝着正确的方向发展。
准确率值也是一样,呈上升趋势。
这展示了迁移学习的强大之处。使用预训练模型通常能在较短时间内用少量数据取得相当不错的结果。
我在想,如果你尝试更长时间地训练模型,或者增加更多数据,会发生什么呢?
问题: 观察损失曲线,我们的模型看起来是过拟合、欠拟合,还是两者都不是?提示:查看笔记本 04. PyTorch 自定义数据集 第8部分. 理想的损失曲线应该是什么样的? 获取思路。
6. 对测试集中的图像进行预测¶
我们的模型在定量评估上表现良好,但定性评估如何呢?
让我们通过对测试集中的图像(这些图像在训练过程中未被使用)进行预测并绘制结果来一探究竟。
可视化,可视化,可视化!
我们需要记住的一点是,为了让模型对图像进行预测,图像必须与模型训练时使用的图像格式相同。
这意味着我们需要确保图像具有:
- 相同的形状 - 如果图像的形状与模型训练时的形状不同,我们将遇到形状错误。
- 相同的类型 - 如果图像的数据类型不同(例如
torch.int8
与torch.float32
),我们将遇到类型错误。 - 相同的设备 - 如果图像位于与模型不同的设备上,我们将遇到设备错误。
- 相同的变换 - 如果模型是在经过特定方式变换(例如使用特定均值和标准差进行归一化)的图像上训练的,而我们尝试对以不同方式变换的图像进行预测,这些预测可能会不准确。
注意: 这些要求适用于所有类型的数据,如果你试图使用训练好的模型进行预测。你想要预测的数据应与模型训练时的数据格式相同。
为了实现这一切,我们将创建一个函数 pred_and_plot_image()
来:
- 接受一个训练好的模型、一个类别名称列表、一个目标图像的文件路径、图像大小、变换和一个目标设备。
- 使用
PIL.Image.open()
打开图像。 - 为图像创建一个变换(默认使用我们上面创建的
manual_transforms
,或者可以使用weights.transforms()
生成的变换)。 - 确保模型位于目标设备上。
- 使用
model.eval()
开启模型评估模式(这将关闭nn.Dropout()
等层,以便它们不用于推理)和推理模式上下文管理器。 - 使用步骤 3 中创建的变换对目标图像进行变换,并使用
torch.unsqueeze(dim=0)
添加额外的批次维度,以便我们的输入图像具有形状[batch_size, color_channels, height, width]
。 - 通过确保图像位于目标设备上,将图像传递给模型进行预测。
- 使用
torch.softmax()
将模型的输出对数转换为预测概率。 - 使用
torch.argmax()
将模型的预测概率转换为预测标签。 - 使用
matplotlib
绘制图像,并将标题设置为步骤 9 中的预测标签和步骤 8 中的预测概率。
注意: 这个函数与 04. PyTorch 自定义数据集章节 11.3 中的
pred_and_plot_image()
类似,但进行了一些调整。
from typing import List, Tuple
from PIL import Image
# 1. Take in a trained model, class names, image path, image size, a transform and target device
def pred_and_plot_image(model: torch.nn.Module,
image_path: str,
class_names: List[str],
image_size: Tuple[int, int] = (224, 224),
transform: torchvision.transforms = None,
device: torch.device=device):
# 2. Open image
img = Image.open(image_path)
# 3. Create transformation for image (if one doesn't exist)
if transform is not None:
image_transform = transform
else:
image_transform = transforms.Compose([
transforms.Resize(image_size),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
### Predict on image ###
# 4. Make sure the model is on the target device
model.to(device)
# 5. Turn on model evaluation mode and inference mode
model.eval()
with torch.inference_mode():
# 6. Transform and add an extra dimension to image (model requires samples in [batch_size, color_channels, height, width])
transformed_image = image_transform(img).unsqueeze(dim=0)
# 7. Make a prediction on image with an extra dimension and send it to the target device
target_image_pred = model(transformed_image.to(device))
# 8. Convert logits -> prediction probabilities (using torch.softmax() for multi-class classification)
target_image_pred_probs = torch.softmax(target_image_pred, dim=1)
# 9. Convert prediction probabilities -> prediction labels
target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)
# 10. Plot image with predicted label and probability
plt.figure()
plt.imshow(img)
plt.title(f"Pred: {class_names[target_image_pred_label]} | Prob: {target_image_pred_probs.max():.3f}")
plt.axis(False);
多么漂亮的函数啊!
让我们通过在测试集中随机选择几张图片进行预测来测试一下。
我们可以使用 list(Path(test_dir).glob("*/*.jpg"))
获取所有测试图片的路径,glob()
方法中的星号表示“任何匹配此模式的文件”,换句话说,任何以 .jpg
结尾的文件(我们所有的图片)。
然后,我们可以使用 Python 的 random.sample(population, k)
从这些路径中随机抽取一些样本,其中 population
是要抽样的序列,k
是要抽取的样本数量。
# Get a random list of image paths from test set
import random
num_images_to_plot = 3
test_image_path_list = list(Path(test_dir).glob("*/*.jpg")) # get list all image paths from test data
test_image_path_sample = random.sample(population=test_image_path_list, # go through all of the test image paths
k=num_images_to_plot) # randomly select 'k' image paths to pred and plot
# Make predictions on and plot the images
for image_path in test_image_path_sample:
pred_and_plot_image(model=model,
image_path=image_path,
class_names=class_names,
# transform=weights.transforms(), # optionally pass in a specified transform from our pretrained model weights
image_size=(224, 224))
哇哦!
这些预测看起来比我们之前的TinyVGG模型所做的预测要好得多。
6.1 对自定义图像进行预测¶
看起来我们的模型在测试集数据上定性表现良好。
但对于我们自己的自定义图像呢?
这才是机器学习真正有趣的地方!
对不属于任何训练集或测试集的自己的数据进行预测。
为了在自定义图像上测试我们的模型,让我们导入经典的 pizza-dad.jpeg
图像(一张我爸爸吃披萨的照片)。
然后我们将它传递给上面创建的 pred_and_plot_image()
函数,看看会发生什么。
# Download custom image
import requests
# Setup custom image path
custom_image_path = data_path / "04-pizza-dad.jpeg"
# Download the image if it doesn't already exist
if not custom_image_path.is_file():
with open(custom_image_path, "wb") as f:
# When downloading from GitHub, need to use the "raw" file link
request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/images/04-pizza-dad.jpeg")
print(f"Downloading {custom_image_path}...")
f.write(request.content)
else:
print(f"{custom_image_path} already exists, skipping download.")
# Predict on custom image
pred_and_plot_image(model=model,
image_path=custom_image_path,
class_names=class_names)
data/04-pizza-dad.jpeg already exists, skipping download.
双击点赞!
看来我们的模型又一次预测对了!
不过这次预测的概率比 TinyVGG 模型(0.373
)在04. PyTorch 自定义数据集 11.3 节中的要高。
这表明我们的 efficientnet_b0
模型对其预测更加自信,而我们的 TinyVGG 模型只是勉强猜对而已。
主要收获¶
- 迁移学习通常允许你用相对较少的数据获得良好的结果。
- 了解迁移学习的力量后,在每个问题的开始询问自己:“是否存在一个表现良好的现有模型可以解决我的问题?”是一个好主意。
- 使用预训练模型时,确保你的自定义数据格式/预处理方式与原始模型的训练数据一致,否则可能会导致性能下降。
- 同样,在预测自定义数据时,确保你的自定义数据格式与模型训练数据的格式相同。
- 可以从多个地方找到预训练模型,包括PyTorch领域库、HuggingFace Hub以及
timm
(PyTorch图像模型)等库。
练习¶
所有练习都专注于练习上述代码。
你应该能够通过参考每个部分或遵循链接的资源来完成它们。
所有练习都应该使用设备无关代码完成。
资源:
- 第06讲的练习模板笔记本
- 第06讲的示例解决方案笔记本(在查看这个之前尝试练习)
- 在YouTube上观看解决方案的视频讲解(包括所有错误)
- 对整个测试数据集进行预测,并绘制一个混淆矩阵,比较我们模型的结果与真实标签。可以参考03. PyTorch计算机视觉第10节获取思路。
- 获取测试数据集上预测“最错误”的样本,并绘制5个“最错误”的图像。你可以这样做:
- 对整个测试数据集进行预测,存储标签和预测概率。
- 按错误预测和预测概率降序排序,这将给你预测概率最高的错误预测,换句话说,就是“最错误”的预测。
- 绘制前5个“最错误”的图像,你认为模型为什么会出错?
- 对自己拍摄的披萨/牛排/寿司图像进行预测——模型表现如何?如果对非披萨/牛排/寿司的图像进行预测会发生什么?
- 将上述第4节的模型训练更长时间(10个周期应该足够),性能会发生什么变化?
- 使用更多数据训练上述第4节的模型,例如从Food101数据集中选取20%的披萨、牛排和寿司图像。
- 你可以在课程GitHub上找到20%的披萨、牛排、寿司数据集。它是通过笔记本
extras/04_custom_data_creation.ipynb
创建的。
- 你可以在课程GitHub上找到20%的披萨、牛排、寿司数据集。它是通过笔记本
- 尝试在披萨、牛排、寿司数据上使用
torchvision.models
中的不同模型,这个模型的表现如何?- 你需要调整分类器层的大小以适应我们的问题。
- 你可能想尝试一个比B0更高的EfficientNet,比如
torchvision.models.efficientnet_b2()
?
额外课程¶
- 查找什么是“模型微调”,并花30分钟研究使用PyTorch进行不同方法的微调。我们如何改变代码来进行微调?提示:微调通常在你有大量自定义数据时效果最好,而特征提取通常在你有较少自定义数据时效果更好。
- 查看新的/即将推出的PyTorch多权重API(在撰写本文时仍处于测试阶段,2022年5月),这是在PyTorch中进行迁移学习的新方法。我们的代码需要进行哪些更改才能使用新API?
- 尝试创建一个针对两类图像的分类器,例如,你可以收集10张你和你朋友的狗的照片,并训练一个模型来分类这两只狗。这将是一个很好的练习,既可以创建数据集,也可以在该数据集上构建模型。