计算机视觉101:6-PyTorch 迁移学习
计算机视觉101
,计算机视觉(Computer Vision, CV)是人工智能的核心领域之一,它赋予机器“看”和“理解”世界的能力,广泛应用于自动驾驶、医疗影像、工业检测、增强现实等前沿场景。然而,从理论到落地,CV工程师不仅需要掌握算法原理,更要具备工程化思维,才能让模型真正服务于现实需求。本课程以“学以致用”为核心理念,采用“理论+代码+部署”三位一体的教学方式,带您系统掌握计算机视觉的核心技术栈:
- 从基础开始:理解图像处理(OpenCV)、特征工程(SIFT/SURF)等传统方法,夯实CV基本功
- 进阶深度学习:掌握CNN、Transformer等现代模型(PyTorch),学会数据增强、模型调优技巧
- 实战部署落地:学习模型量化、ONNX转换、边缘计算(TensorRT/RKNN)
让算法在真实场景中高效运行通过Python代码示范+工业级案例(如缺陷检测、人脸识别),您将逐步构建从算法开发到嵌入式部署的完整能力链,最终独立完成一个可落地的CV项目。
无论您是希望转行AI的开发者、在校学生,还是寻求技术突破的工程师,本课程都将助您跨越“纸上谈兵”的瓶颈,成为兼具算法能力与工程思维的CV实战派。
🚀 现在开始,用代码让机器真正“看见”世界!
什么是迁移学习?
迁移学习允许我们将另一个模型从其他问题中学到的模式(也称为权重)应用于我们自己的问题。
例如,我们可以利用一个计算机视觉模型从数据集(如ImageNet,包含数百万张不同物体的图像)中学到的模式,并将其用于驱动我们的FoodVision Mini模型。
或者,我们可以从一个语言模型(该模型通过大量文本学习了语言的表示)中提取模式,并将其作为分类不同文本样本模型的基础。
核心思想始终是:找到一个表现良好的现有模型,并将其应用于你自己的问题。
将迁移学习应用于计算机视觉和自然语言处理(NLP)的示例。在计算机视觉的情况下,一个计算机视觉模型可能在ImageNet的数百万张图像上学习模式,然后使用这些模式推断另一个问题。而在NLP中,一个语言模型可能通过阅读整个维基百科(甚至更多内容)来学习语言结构,然后将这些知识应用于另一个问题。
什么是迁移学习?
迁移学习允许我们将另一个模型从其他问题中学到的模式(也称为权重)应用于我们自己的问题。
例如,我们可以利用一个计算机视觉模型从数据集(如ImageNet,包含数百万张不同物体的图像)中学到的模式,并将其用于驱动我们的FoodVision Mini模型。
或者,我们可以从一个语言模型(该模型通过大量文本学习了语言的表示)中提取模式,并将其作为分类不同文本样本模型的基础。
核心思想始终是:找到一个表现良好的现有模型,并将其应用于你自己的问题。
将迁移学习应用于计算机视觉和自然语言处理(NLP)的示例。在计算机视觉的情况下,一个计算机视觉模型可能在ImageNet的数百万张图像上学习模式,然后使用这些模式推断另一个问题。而在NLP中,一个语言模型可能通过阅读整个维基百科(甚至更多内容)来学习语言结构,然后将这些知识应用于另一个问题。
为什么使用迁移学习?
使用迁移学习主要有两个好处:
- 可以利用现有的模型(通常是神经网络架构),这些模型已经被证明在与我们类似的问题上表现良好。
- 可以利用一个已经在类似数据上预先学习过模式的模型。这通常能够帮助我们在更少的自定义数据的情况下取得出色的结果。
我们将通过 FoodVision Mini 问题来验证这些优势,我们将采用一个在 ImageNet 上预训练的计算机视觉模型,并尝试利用其底层学习到的表示来对披萨、牛排和寿司的图像进行分类。
无论是研究还是实践都支持使用迁移学习。
最近的一篇机器学习研究论文建议从业者尽可能使用迁移学习。
一项关于从头开始训练与使用迁移学习的对比研究发现,从实践者的角度来看,迁移学习在成本和时间方面具有更大的优势。来源: 如何训练你的 ViT?数据、增强和正则化在视觉Transformer中的应用 第6节(结论)。
此外,Jeremy Howard(fastai 的创始人)也是迁移学习的坚定支持者。
那些真正能带来改变的技术(如迁移学习),如果我们能在迁移学习方面做得更好,那将是一件改变世界的事情。突然之间,更多的人可以用更少的资源和更少的数据完成世界级的工作。 — Jeremy Howard 在 Lex Fridman 播客中的发言
示例代码块
以下是一个简单的代码示例,展示如何加载预训练模型:
import torch
import torchvision.models as models
# 加载预训练的 ResNet-50 模型
model = models.resnet50(pretrained=True)
# 冻结所有层
for param in model.parameters():
param.requires_grad = False
# 替换最后一层以适配我们的分类任务
num_classes = 3 # 披萨、牛排、寿司
model.fc = torch.nn.Linear(model.fc.in_features, num_classes)
通过上述代码,我们可以快速调整预训练模型以适应特定任务的需求。
在哪里找到预训练模型
深度学习的世界是一个令人惊叹的地方。
它如此令人惊叹,以至于世界各地的许多人分享他们的工作成果。
通常情况下,最新的前沿研究代码和预训练模型会在论文发表后的几天内发布。
并且有多个地方可以找到可用于解决你自己问题的预训练模型。
预训练模型资源汇总
位置 | 内容 | 链接 |
---|---|---|
PyTorch 领域库 | 每个 PyTorch 领域库(如 torchvision 、torchtext )都包含某种形式的预训练模型。这些模型可以直接在 PyTorch 中使用。 | torchvision.models , torchtext.models , torchaudio.models , torchrec.models |
HuggingFace Hub | 来自全球组织的各种领域(视觉、文本、音频等)的预训练模型集合。此外,这里还有许多不同的数据集。 | HuggingFace 模型, HuggingFace 数据集 |
timm (PyTorch Image Models) 库 | 几乎所有最新的计算机视觉模型都以 PyTorch 代码形式提供,并且还包含许多其他有用的计算机视觉功能。 | GitHub - timm |
Paperswithcode | 收录了最新前沿机器学习论文及其代码实现的集合。你还可以在这里找到不同任务上模型性能的基准测试结果。 | Paperswithcode |
借助上述高质量资源,每当你开始一个新的深度学习问题时,应该养成一个习惯:先问自己,“是否有适用于我问题的预训练模型?”
练习: 花 5 分钟时间浏览
torchvision.models
和 HuggingFace Hub 的模型页面,看看你能发现什么?(这里没有标准答案,只是为了练习探索能力)
0. 环境准备
让我们开始导入/下载本节所需的模块。
为了节省我们编写额外代码的时间,我们将利用在上一节(05. PyTorch 模块化实践)中创建的一些 Python 脚本(例如 data_setup.py
和 engine.py
)。
具体来说,我们将从 pytorch-deep-learning
仓库中下载 going_modular
目录(如果尚未下载的话)。
同时,如果尚未安装 torchinfo
包,我们也会获取它。
torchinfo
将帮助我们在后续部分中以可视化方式展示模型结构。
注意: 截至2022年6月,此笔记本使用的是
torch
和torchvision
的夜间版本,因为需要torchvision
v0.13+ 才能使用更新的多权重 API。可以使用以下命令安装这些版本。
检查并安装必要的依赖
# 为了运行此笔记本中的更新版 API,我们需要 torch 1.12+ 和 torchvision 0.13+
try:
import torch
import torchvision
assert int(torch.__version__.split(".")[1]) >= 12, "torch 版本应为 1.12+"
assert int(torchvision.__version__.split(".")[1]) >= 13, "torchvision 版本应为 0.13+"
print(f"torch 版本: {torch.__version__}")
print(f"torchvision 版本: {torchvision.__version__}")
except:
print(f"[INFO] torch/torchvision 版本不符合要求,正在安装夜间版本。")
!pip3 install -U torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
import torch
import torchvision
print(f"torch 版本: {torch.__version__}")
print(f"torchvision 版本: {torchvision.__version__}")
输出结果如下:
torch 版本: 1.13.0.dev20220620+cu113
torchvision 版本: 0.14.0.dev20220620+cu113
导入常规库
接下来,我们将继续导入其他必要的库,并确保 torchinfo
和 going_modular
工具可用。
# 继续导入常规库
import matplotlib.pyplot as plt
import torch
import torchvision
from torch import nn
from torchvision import transforms
# 尝试导入 torchinfo,如果不可用则安装
try:
from torchinfo import summary
except:
print("[INFO] 未找到 torchinfo... 正在安装。")
!pip install -q torchinfo
from torchinfo import summary
# 尝试导入 going_modular 目录,如果不可用则从 GitHub 下载
try:
from going_modular.going_modular import data_setup, engine
except:
# 获取 going_modular 脚本
print("[INFO] 未找到 going_modular 脚本... 正在从 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
设置设备无关代码
最后,我们将设置设备无关代码,确保可以在 CPU 或 GPU 上运行。
注意: 如果您正在使用 Google Colab 并且尚未启用 GPU,请通过
Runtime -> Change runtime type -> Hardware accelerator -> GPU
启用 GPU。
# 设置设备无关代码
device = "cuda" if torch.cuda.is_available() else "cpu"
device
输出结果如下:
'cuda'
这意味着当前环境已成功检测到 GPU,并将使用 CUDA 进行加速。
1. 获取数据
在我们开始使用 迁移学习 之前,我们需要一个数据集。
为了对比迁移学习与我们之前尝试的模型构建方法,我们将下载与 FoodVision Mini 中使用的相同数据集。
让我们编写一些代码来从课程的 GitHub 下载 pizza_steak_sushi.zip
数据集并解压它。
同时,如果数据已经存在,我们可以确保不会重新下载。
import os
import zipfile
from pathlib import Path
import requests
# 设置数据文件夹路径
data_path = Path("data/")
image_path = data_path / "pizza_steak_sushi"
# 如果图像文件夹不存在,则下载并准备数据...
if image_path.is_dir():
print(f"{image_path} 目录已存在。")
else:
print(f"未找到 {image_path} 目录,正在创建...")
image_path.mkdir(parents=True, exist_ok=True)
# 下载披萨、牛排、寿司数据
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("正在下载披萨、牛排、寿司数据...")
f.write(request.content)
# 解压披萨、牛排、寿司数据
with zipfile.ZipFile(data_path / "pizza_steak_sushi.zip", "r") as zip_ref:
print("正在解压披萨、牛排、寿司数据...")
zip_ref.extractall(image_path)
# 删除 .zip 文件
os.remove(data_path / "pizza_steak_sushi.zip")
data/pizza_steak_sushi 目录已存在。
太好了!
现在我们已经有了之前使用过的相同数据集,这是一系列以标准图像分类格式存储的披萨、牛排和寿司图片。
接下来,我们为训练和测试目录创建路径。
# 设置目录
train_dir = image_path / "train"
test_dir = image_path / "test"
通过上述步骤,我们成功地获取了数据,并为后续的迁移学习任务做好了准备。
2. 创建数据集和DataLoaders
由于我们已经下载了 going_modular
目录,因此可以使用我们在第 05 节《PyTorch 模块化》中创建的 data_setup.py
脚本来准备和设置我们的 DataLoaders。
但是,由于我们将使用来自 torchvision.models
的预训练模型,因此需要先准备一个特定的变换来处理我们的图像。
2.1 为 torchvision.models
创建变换(手动创建)
注意: 从
torchvision
v0.13+ 开始,创建数据变换的方式有所更新。我将之前的方法称为“手动创建”,新的方法称为“自动创建”。本笔记本展示了两种方法。
在使用预训练模型时,非常重要的一点是:输入到模型中的自定义数据必须以与模型原始训练数据相同的方式进行准备。
在 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
来执行上述步骤。
# 手动创建变换流水线(适用于 torchvision < 0.13)
manual_transforms = transforms.Compose([
transforms.Resize((224, 224)), # 1. 将所有图像调整为 224x224(尽管某些模型可能需要不同的尺寸)
transforms.ToTensor(), # 2. 将图像值转换为 0 到 1 之间
transforms.Normalize(mean=[0.485, 0.456, 0.406], # 3. 设置平均值为 [0.485, 0.456, 0.406](每个颜色通道的平均值)
std=[0.229, 0.224, 0.225]) # 4. 设置标准差为 [0.229, 0.224, 0.225](每个颜色通道的标准差)
])
太棒了!
现在我们已经准备好了一个手动创建的变换序列来处理我们的图像,接下来让我们创建训练和测试的 DataLoaders。
我们可以使用我们在第 05 节《PyTorch 模块化 第二部分》中创建的 data_setup.py
脚本中的 create_dataloaders
函数来创建这些 DataLoaders。
我们将 batch_size
设置为 32,这样我们的模型每次可以看到 32 个样本的小批量。
并且我们可以通过设置 transform=manual_transforms
来使用上面创建的变换流水线对图像进行变换。
注意: 我在本笔记本中包含了这种手动创建变换的方式,因为您可能会遇到使用这种风格的资源。同时需要注意的是,由于这些变换是手动创建的,因此它们也可以无限定制。如果您想在变换流水线中包含数据增强技术,完全可以实现。
# 创建训练和测试的 DataLoaders,并获取类别名称列表
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir,
test_dir=test_dir,
transform=manual_transforms, # 调整大小、将图像值转换为 0 到 1 之间并归一化
batch_size=32 # 设置小批量大小为 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
文档。
- 注意: 根据您选择的模型架构,您还可能看到其他选项,如
让我们尝试一下。
# 获取一组预训练模型权重
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = 在 ImageNet 上预训练的最佳可用权重
weights
输出结果:
EfficientNet_B0_Weights.IMAGENET1K_V1
现在,为了访问与我们选择的 weights
相关的变换,我们可以使用 transforms()
方法。
这实际上是在说“获取用于在 ImageNet 上训练 EfficientNet_B0_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()
来创建 DataLoaders。
# 创建训练和测试的 DataLoaders,并获取类别名称列表
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir,
test_dir=test_dir,
transform=auto_transforms, # 对我们自己的数据执行与预训练模型相同的变换
batch_size=32 # 设置小批量大小为 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 所指出的,你可能会选择最大、最耗计算资源的模型。
理解这种性能 vs. 速度 vs. 大小的权衡需要时间和实践。
对我来说,我发现 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% 的准确率。
我们还将它发送到目标设备。
# 旧方法:设置带有预训练权重的模型并发送到目标设备(torchvision v0.13 之前)
# model = torchvision.models.efficientnet_b0(pretrained=True).to(device) # 旧方法 (pretrained=True)
# 新方法:设置带有预训练权重的模型并发送到目标设备(torchvision v0.13+)
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = 最佳可用权重
model = torchvision.models.efficientnet_b0(weights=weights).to(device)
# model # 取消注释以输出(非常长)
注意: 在以前版本的
torchvision
中,你可以通过以下代码创建预训练模型:model = torchvision.models.efficientnet_b0(pretrained=True).to(device)
然而,在
torchvision
v0.13+ 中运行此代码会导致错误,例如:UserWarning: The parameter 'pretrained' is deprecated since 0.13 and will be removed in 0.15, please use 'weights' instead.
和…
UserWarning: Arguments other than a weight enum or None for weights are deprecated since 0.13 and will be removed in 0.15. The current behavior is equivalent to passing weights=EfficientNet_B0_Weights.IMAGENET1K_V1. You can also use weights=EfficientNet_B0_Weights.DEFAULT to get the most up-to-date weights.
如果我们打印模型,会得到类似以下的内容:
大量的层。
这是迁移学习的一个好处,采用由世界上一些最佳工程师设计的现有模型,并将其应用于自己的问题。
我们的 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
- 行中显示的功能。
# 使用 torchinfo 打印摘要(取消注释以查看实际输出)
summary(
model=model,
input_size=(32, 3, 224, 224), # 确保这是 "input_size",而不是 "input_shape"
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] -- --
...
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
的参数是“不可训练的”或“冻结的”。
# 冻结模型“features”部分(特征提取器)的所有基础层,通过设置 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
相同的设备上。
# 设置手动种子
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 获取 class_names 的长度(每个类别一个输出单元)
output_shape = len(class_names)
# 重新创建分类器层并将其移动到目标设备
model.classifier = torch.nn.Sequential(
torch.nn.Dropout(p=0.2, inplace=True),
torch.nn.Linear(in_features=1280,
out_features=output_shape, # 输出单元数量与类别数量相同
bias=True)).to(device)
很好!
输出层已更新,让我们再次获取模型的摘要,看看发生了哪些变化。
# 在冻结特征和更改输出分类器层之后获取摘要(取消注释以查看实际输出)
summary(
model,
input_size=(32, 3, 224, 224), # 确保这是 "input_size",而不是 "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, 2
4. 训练模型
现在我们已经得到了一个部分冻结的预训练模型,并且该模型具有自定义的 classifier
,那么让我们看看迁移学习的实际效果吧。
为了开始训练,我们首先需要创建一个损失函数和一个优化器。
由于我们仍然在处理多类别分类问题,因此我们将使用 nn.CrossEntropyLoss()
作为损失函数。
同时,我们继续使用 torch.optim.Adam()
作为优化器,学习率设置为 lr=0.001
。
# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
太棒了!
为了训练我们的模型,我们可以使用我们在第 05 节《PyTorch 模块化实践》中定义的 train()
函数。
train()
函数位于 going_modular
目录下的 engine.py
脚本中。
接下来,我们来看看模型在 5 个 epoch 内完成训练需要多长时间。
注意: 我们只会训练
classifier
的参数,因为模型中的其他参数已经被冻结了。
# 设置随机种子
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 启动计时器
from timeit import default_timer as timer
start_time = timer()
# 开始训练并保存结果
results = engine.train(
model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
optimizer=optimizer,
loss_fn=loss_fn,
epochs=5,
device=device
)
# 停止计时器并打印总耗时
end_time = timer()
print(f"[INFO] 总训练时间: {end_time-start_time:.3f} 秒")
训练输出
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] 总训练时间: 8.977 秒
哇!
我们的模型训练速度非常快(在我的本地机器上使用 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
脚本中,因此我们将尝试导入它。如果没有该文件,我们会下载它。
# 从 helper_functions.py 获取 plot_loss_curves() 函数,如果不存在则下载
try:
from helper_functions import plot_loss_curves
except ImportError:
print("[INFO] 未找到 helper_functions.py,正在下载...")
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_loss_curves(results)
获取 plot_loss_curves()
函数
如果本地没有 helper_functions.py
文件,上述代码会自动从指定 URL 下载该文件,并从中导入 plot_loss_curves()
函数。
损失曲线分析
这些损失曲线看起来非常理想!
从图中可以看出,训练集和测试集的损失值都在朝着正确的方向发展(逐渐降低)。
同时,准确率值也在稳步上升。
这充分展示了 迁移学习 的强大之处。使用预训练模型通常可以在较短的时间内,利用少量数据获得不错的性能。
你是否想过,如果我们延长训练时间,或者增加更多数据,会发生什么?
问题: 根据损失曲线,我们的模型是否存在过拟合或欠拟合?还是两者都不是?
提示: 可参考《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()
将模型的输出 logits 转换为预测概率。 - 使用
torch.argmax()
将模型的预测概率转换为预测标签。 - 使用
matplotlib
绘制图像,并将标题设置为步骤 9 中的预测标签和步骤 8 中的预测概率。
注意: 这是一个类似于 04. PyTorch 自定义数据集部分 11.3 的
pred_and_plot_image()
函数,只是进行了几个步骤的调整。
实现代码
from typing import List, Tuple
from PIL import Image
import torch
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# 1. 接收一个已训练的模型、类别名称列表、图像路径、图像大小、变换以及目标设备
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 = torch.device("cpu")
):
# 2. 打开图像
img = Image.open(image_path)
# 3. 创建图像的变换(如果不存在则创建)
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]),
])
### 对图像进行预测 ###
# 4. 确保模型位于目标设备上
model.to(device)
# 5. 启用模型评估模式和推理模式
model.eval()
with torch.inference_mode():
# 6. 对图像进行变换并添加额外的维度
transformed_image = image_transform(img).unsqueeze(dim=0)
# 7. 在目标设备上对图像进行预测
target_image_pred = model(transformed_image.to(device))
# 8. 将 logits 转换为预测概率
target_image_pred_probs = torch.softmax(target_image_pred, dim=1)
# 9. 将预测概率转换为预测标签
target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)
# 10. 绘制带有预测标签和概率的图像
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
是要抽取的样本数量。
import random
# 获取测试集中随机的图像路径列表
num_images_to_plot = 3
test_image_path_list = list(Path(test_dir).glob("*/*.jpg")) # 获取测试数据中所有图像路径
test_image_path_sample = random.sample(population=test_image_path_list, k=num_images_to_plot) # 随机选择 'k' 个图像路径进行预测和绘制
# 对图像进行预测并绘制
for image_path in test_image_path_sample:
pred_and_plot_image(
model=model,
image_path=image_path,
class_names=class_names,
image_size=(224, 224)
)
太棒了!
这些预测看起来比我们之前的 TinyVGG 模型的预测好得多。
6.1 在自定义图像上进行预测
我们的模型在测试集数据上的定性表现很好。
但如果是在我们自己的自定义图像上呢?
这才是机器学习真正的乐趣所在!
在自定义数据上进行预测,完全不在任何训练或测试集范围内。
为了在自定义图像上测试我们的模型,让我们导入经典的 pizza-dad.jpeg
图像(一张我父亲吃披萨的照片)。
然后,我们将它传递给上面创建的 pred_and_plot_image()
函数,看看会发生什么。
import requests
# 设置自定义图像路径
custom_image_path = data_path / "04-pizza-dad.jpeg"
# 如果图像不存在,则下载
if not custom_image_path.is_file():
with open(custom_image_path, "wb") as f:
request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/images/04-pizza-dad.jpeg")
print(f"正在下载 {custom_image_path}...")
f.write(request.content)
else:
print(f"{custom_image_path} 已存在,跳过下载。")
# 对自定义图像进行预测
pred_and_plot_image(
model=model,
image_path=custom_image_path,
class_names=class_names,
image_size=(224, 224)
)
data/04-pizza-dad.jpeg 已存在,跳过下载。
两个大拇指!
看起来我们的模型再次正确预测了!
但这次的预测概率高于 TinyVGG 在 04. PyTorch 自定义数据集部分 11.3 中的预测概率 (0.373
)。
这表明我们的 efficientnet_b0
模型对其预测更加自信,而 TinyVGG 模型几乎只是在猜测。
优秀资源
1. 模型微调 (Model Fine-Tuning)
- 查阅资料:查找“模型微调”是什么,并花30分钟研究如何使用 PyTorch 实现不同的微调方法。
- 代码调整:我们如何修改现有代码以实现模型微调?
提示:微调通常在你拥有大量自定义数据时效果最佳,而特征提取(Feature Extraction)则更适合在自定义数据较少的情况下使用。
2. 自定义分类器实践
- 动手实践:尝试构建一个针对两类图像的分类器。例如,你可以收集10张你家狗狗的照片和你朋友家狗狗的照片,并训练一个模型来区分这两只狗。
- 练习目标:这将是一个很好的机会来练习创建数据集以及在该数据集上构建模型。