计算机视觉101:7-PyTorch 实验跟踪
计算机视觉101
,计算机视觉(Computer Vision, CV)是人工智能的核心领域之一,它赋予机器“看”和“理解”世界的能力,广泛应用于自动驾驶、医疗影像、工业检测、增强现实等前沿场景。然而,从理论到落地,CV工程师不仅需要掌握算法原理,更要具备工程化思维,才能让模型真正服务于现实需求。本课程以“学以致用”为核心理念,采用“理论+代码+部署”三位一体的教学方式,带您系统掌握计算机视觉的核心技术栈:
- 从基础开始:理解图像处理(OpenCV)、特征工程(SIFT/SURF)等传统方法,夯实CV基本功
- 进阶深度学习:掌握CNN、Transformer等现代模型(PyTorch),学会数据增强、模型调优技巧
- 实战部署落地:学习模型量化、ONNX转换、边缘计算(TensorRT/RKNN)
让算法在真实场景中高效运行通过Python代码示范+工业级案例(如缺陷检测、人脸识别),您将逐步构建从算法开发到嵌入式部署的完整能力链,最终独立完成一个可落地的CV项目。
无论您是希望转行AI的开发者、在校学生,还是寻求技术突破的工程师,本课程都将助您跨越“纸上谈兵”的瓶颈,成为兼具算法能力与工程思维的CV实战派。
🚀 现在开始,用代码让机器真正“看见”世界!
什么是实验跟踪?
机器学习和深度学习非常依赖实验。
你需要戴上艺术家的贝雷帽或厨师帽,尝试构建各种不同的模型。
同时,你还需披上科学家的外套,记录数据、模型架构和训练策略的各种组合结果。
这就是实验跟踪的作用所在。
如果你在运行大量不同的实验,实验跟踪能够帮助你明确哪些方法有效,哪些无效。
为什么要跟踪实验?
如果你只运行少量模型(就像我们目前所做的那样),可能仅仅通过打印输出和一些字典来跟踪它们的结果就足够了。
然而,随着你运行的实验数量开始增加,这种简单的跟踪方式可能会变得难以管理。
因此,如果你遵循机器学习实践者的座右铭:实验、实验、再实验!,那么你需要一种方法来跟踪这些实验。
在构建了一些模型并跟踪其结果后,你会开始意识到事情会多么迅速地变得复杂。
跟踪机器学习实验的不同方法
跟踪机器学习实验的方法与可以运行的实验数量一样多。
下表涵盖了一些常见的方法:
方法 | 设置 | 优点 | 缺点 | 成本 |
---|---|---|---|---|
Python字典、CSV文件、打印输出 | 无需额外设置 | 易于设置,纯Python环境即可运行 | 难以跟踪大量实验 | 免费 |
TensorBoard | 最小化设置,安装tensorboard | PyTorch内置扩展,广泛使用且易于扩展 | 用户体验不如其他选项友好 | 免费 |
Weights & Biases 实验跟踪 | 最小化设置,安装wandb 并创建账户 | 极佳的用户体验,可公开实验结果,几乎可以跟踪所有内容 | 需要依赖PyTorch之外的外部资源 | 个人使用免费 |
MLFlow | 最小化设置,安装mlflow 并开始跟踪 | 完全开源的MLOps生命周期管理工具,支持多种集成 | 设置远程跟踪服务器比其他服务稍复杂 | 免费 |
您可以使用多种位置和技术来跟踪机器学习实验。注意: 还有许多类似于Weights & Biases的商业选项和类似于MLFlow的开源选项,但为了简洁起见,这里未列出。您可以通过搜索“机器学习实验跟踪”找到更多相关内容。
我们将要涵盖的内容
我们将运行多个不同的建模实验,这些实验涉及不同级别的数据量、模型大小和训练时间,目标是改进 FoodVision Mini 模型。
由于其与 PyTorch 的紧密集成以及广泛的使用,本笔记本专注于使用 TensorBoard 来跟踪我们的实验。
然而,我们将讨论的原则适用于所有其他实验跟踪工具。
主题 | 内容 |
---|---|
0. 准备环境 | 在过去的几个部分中,我们编写了不少有用的代码。让我们下载这些代码并确保可以再次使用它们。 |
1. 获取数据 | 让我们获取用于改进 FoodVision Mini 模型结果的披萨、牛排和寿司图像分类数据集。 |
2. 创建数据集和数据加载器 | 我们将在第 05 章(PyTorch 模块化)中编写的 data_setup.py 脚本来设置我们的 DataLoaders。 |
3. 获取并定制预训练模型 | 与上一节(06. PyTorch 迁移学习)类似,我们将从 torchvision.models 下载一个预训练模型,并根据自己的问题进行定制。 |
4. 训练模型并跟踪结果 | 让我们看看使用 TensorBoard 训练单个模型并跟踪其训练结果的过程。 |
5. 在 TensorBoard 中查看模型结果 | 之前我们使用辅助函数可视化了模型的损失曲线,现在让我们看看它们在 TensorBoard 中的表现。 |
6. 创建一个用于跟踪实验的辅助函数 | 如果我们要遵循机器学习实践者的座右铭——实验、实验、再实验!,那么最好创建一个函数来帮助我们保存建模实验的结果。 |
7. 设置一系列建模实验 | 不是一次运行一个实验,而是编写一些代码一次性运行多个实验,涉及不同的模型、不同的数据量和不同的训练时间。 |
8. 在 TensorBoard 中查看建模实验 | 到这一阶段,我们将一次性运行八个建模实验,需要跟踪的内容较多,让我们看看它们在 TensorBoard 中的结果如何。 |
9. 加载最佳模型并用它进行预测 | 实验跟踪的目的是找出性能最佳的模型。让我们加载表现最佳的模型,并用它进行预测以 可视化、可视化、再可视化! |
以上是本教程的主要内容结构,旨在通过系统化的实验设计和结果跟踪,提升模型性能并优化开发流程。
0. 环境准备
让我们从下载本节所需的所有模块开始。
为了节省我们编写额外代码的时间,我们将利用在第 05 节《PyTorch 模块化》中创建的一些 Python 脚本(例如 data_setup.py
和 engine.py
)。
具体来说,我们将从 pytorch-deep-learning
仓库中下载 going_modular
目录(如果尚未下载的话)。
我们还将获取 torchinfo
包(如果尚未安装的话)。
torchinfo
将帮助我们在后续部分中生成模型的可视化摘要。
由于我们使用的是较新的 torchvision
包版本(截至 2022 年 6 月为 v0.13),我们将确保已安装最新版本。
确保 PyTorch 和 torchvision 的版本要求
# 为了运行此笔记本并使用更新的 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__}")
注意: 如果您使用的是 Google Colab,在运行上述单元格后可能需要重启运行时。重启后,您可以再次运行该单元格以验证是否已正确安装 torch
(1.12+)和 torchvision
(0.13+)。
导入必要的库
# 继续导入常规库
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)
device = "cuda" if torch.cuda.is_available() else "cpu"
device
输出:
'cuda'
注意: 如果您使用的是 Google Colab,并且尚未启用 GPU,请通过 Runtime -> Change runtime type -> Hardware accelerator -> GPU
启用 GPU。
创建一个辅助函数以设置随机种子
由于我们在之前的章节中多次设置了随机种子,为什么不将其封装成一个函数呢?
让我们创建一个名为 set_seeds()
的函数来“设置种子”。
注意: 回忆随机种子是一种控制计算机生成随机性的方法。虽然在运行机器学习代码时并不总是需要设置随机种子,但它有助于确保结果的可重复性(即我运行代码得到的结果与您运行代码得到的结果相似)。在教育或实验环境中,随机种子通常是必需的,但在实际生产环境中一般不需要。
# 设置随机种子
def set_seeds(seed: int = 42):
"""
为 PyTorch 操作设置随机种子。
参数:
seed (int, 可选): 要设置的随机种子,默认为 42。
"""
# 为通用 PyTorch 操作设置种子
torch.manual_seed(seed)
# 为 CUDA PyTorch 操作(GPU 上的操作)设置种子
torch.cuda.manual_seed(seed)
至此,环境准备已完成!接下来可以继续进行模型训练和其他操作。
1. 获取数据
和往常一样,在我们运行机器学习实验之前,我们需要一个数据集。
我们将继续尝试改进在 FoodVision Mini 上获得的结果。
在上一节《06. PyTorch 迁移学习》中,我们看到了使用预训练模型和迁移学习在分类披萨、牛排和寿司图像时的强大能力。
那么,我们是否可以运行一些实验并进一步改进我们的结果呢?
为了实现这一目标,我们将使用与上一节类似的代码来下载 pizza_steak_sushi.zip
(如果数据尚不存在),不过这次它已经被功能化了。
这将允许我们在稍后再次使用它。
下载和解压数据
以下是用于下载和解压数据的函数:
import os
import zipfile
from pathlib import Path
import requests
def download_data(source: str,
destination: str,
remove_source: bool = True) -> Path:
"""
从源地址下载压缩数据集并解压到目标目录。
参数:
source (str): 包含数据的压缩文件链接。
destination (str): 解压数据的目标目录。
remove_source (bool): 下载并解压完成后是否删除源文件。
返回:
pathlib.Path: 下载数据的路径。
示例用法:
download_data(
source="https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip",
destination="pizza_steak_sushi"
)
"""
# 设置数据文件夹路径
data_path = Path("data/")
image_path = data_path / destination
# 如果图像文件夹不存在,则下载并准备数据...
if image_path.is_dir():
print(f"[INFO] {image_path} 目录已存在,跳过下载。")
else:
print(f"[INFO] 未找到 {image_path} 目录,正在创建...")
image_path.mkdir(parents=True, exist_ok=True)
# 下载披萨、牛排、寿司数据
target_file = Path(source).name
with open(data_path / target_file, "wb") as f:
request = requests.get(source)
print(f"[INFO] 正在从 {source} 下载 {target_file}...")
f.write(request.content)
# 解压披萨、牛排、寿司数据
with zipfile.ZipFile(data_path / target_file, "r") as zip_ref:
print(f"[INFO] 正在解压 {target_file} 数据...")
zip_ref.extractall(image_path)
# 删除 .zip 文件
if remove_source:
os.remove(data_path / target_file)
return image_path
# 调用函数下载数据
image_path = download_data(
source="https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip",
destination="pizza_steak_sushi"
)
image_path
输出结果
[INFO] data/pizza_steak_sushi 目录已存在,跳过下载。
结果
PosixPath('data/pizza_steak_sushi')
太棒了!看起来我们已经准备好了一个包含披萨、牛排和寿司图像的标准图像分类格式数据集,可以开始下一步了。
2. 创建数据集和DataLoaders
现在我们已经获取了一些数据,接下来将其转换为PyTorch的DataLoaders。
我们可以通过在05. PyTorch模块化部分2中创建的create_dataloaders()
函数来实现这一点。
由于我们将使用迁移学习,特别是来自torchvision.models
的预训练模型,因此我们需要创建一个转换(transform),以正确准备我们的图像。
为了将图像转换为张量,我们可以使用以下两种方法:
- 使用
torchvision.transforms
手动创建转换。 - 使用
torchvision.models.MODEL_NAME.MODEL_WEIGHTS.DEFAULT.transforms()
自动创建转换。- 其中
MODEL_NAME
是特定的torchvision.models
架构,MODEL_WEIGHTS
是一组特定的预训练权重,而DEFAULT
表示“最佳可用权重”。
- 其中
我们在06. PyTorch迁移学习第2部分中看到了这两种方法的示例。
首先,让我们看看如何手动创建一个torchvision.transforms
流水线(这种方法提供了最大的自定义能力,但如果转换与预训练模型不匹配,可能会导致性能下降)。
我们需要确保的主要手动转换是:所有图像都以ImageNet格式进行归一化(这是因为torchvision.models
的所有预训练模型都是在ImageNet上预训练的)。
我们可以通过以下代码实现:
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
2.1 使用手动创建的转换生成DataLoaders
在[6]中:
# 设置目录
train_dir = image_path / "train"
test_dir = image_path / "test"
# 设置ImageNet归一化级别(将所有图像转换为与ImageNet相似的分布)
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
# 手动创建转换流水线
manual_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
normalize
])
print(f"手动创建的转换: {manual_transforms}")
# 创建数据加载器
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir,
test_dir=test_dir,
transform=manual_transforms, # 使用手动创建的转换
batch_size=32
)
train_dataloader, test_dataloader, class_names
输出结果如下:
手动创建的转换: Compose(
Resize(size=(224, 224), interpolation=bilinear, max_size=None, antialias=None)
ToTensor()
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
Out[6]:
(<torch.utils.data.dataloader.DataLoader at 0x7febf1d218e0>,
<torch.utils.data.dataloader.DataLoader at 0x7febf1d216a0>,
['pizza', 'steak', 'sushi'])
2.2 使用自动创建的转换生成DataLoaders
数据已转换,DataLoaders已创建!
现在让我们看看相同的转换流水线,但这次使用自动转换。
我们可以通过实例化一组预训练权重(例如weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
)并调用其transforms()
方法来实现这一点。
在[7]中:
# 设置目录
train_dir = image_path / "train"
test_dir = image_path / "test"
# 设置预训练权重(torchvision.models中有许多可用的权重)
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
# 从权重中获取转换(这些是用于获得权重的转换)
automatic_transforms = weights.transforms()
print(f"自动创建的转换: {automatic_transforms}")
# 创建数据加载器
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir,
test_dir=test_dir,
transform=automatic_transforms, # 使用自动创建的转换
batch_size=32
)
train_dataloader, test_dataloader, class_names
输出结果如下:
自动创建的转换: ImageClassification(
crop_size=[224]
resize_size=[256]
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BICUBIC
)
Out[7]:
(<torch.utils.data.dataloader.DataLoader at 0x7febf1d213a0>,
<torch.utils.data.dataloader.DataLoader at 0x7febf1d21490>,
['pizza', 'steak', 'sushi'])
以上展示了如何通过手动和自动方式创建图像转换,并生成相应的DataLoaders。
3. 获取预训练模型、冻结基础层并修改分类头
在我们运行和跟踪多个建模实验之前,先来看看运行和跟踪单个实验是什么样的。
由于我们的数据已经准备就绪,接下来我们需要的是一个模型。
让我们下载 torchvision.models.efficientnet_b0()
模型的预训练权重,并将其调整为适用于我们自己的数据。
下载预训练模型
# 注意:这是在 torchvision > 0.13 中创建预训练模型的方式,未来版本中将被弃用。
# model = torchvision.models.efficientnet_b0(pretrained=True).to(device) # 已废弃
# 下载 EfficientNet_B0 的预训练权重
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # 在 torchvision 0.13 中新增,"DEFAULT" 表示 "最佳可用权重"
# 使用预训练权重设置模型,并将其发送到目标设备
model = torchvision.models.efficientnet_b0(weights=weights).to(device)
# 查看模型输出
# model
太棒了!
现在我们已经获取了一个预训练模型,接下来将其转换为特征提取器模型。
本质上,我们将冻结模型的基础层(我们将使用这些层从输入图像中提取特征),并修改分类头(输出层)以适应我们正在处理的类别数量(我们有 3 个类别:披萨、牛排、寿司)。
注意:创建特征提取器模型(即我们在此处所做的)的概念在 06. PyTorch 迁移学习部分 3.2:设置预训练模型中有更深入的讨论。
冻结基础层并修改分类头
# 冻结所有基础层,通过将 requires_grad 属性设置为 False
for param in model.features.parameters():
param.requires_grad = False
# 由于我们将创建一个带有随机权重的新层 (torch.nn.Linear),因此设置随机种子
set_seeds()
# 更新分类头以适应我们的问题
model.classifier = torch.nn.Sequential(
nn.Dropout(p=0.2, inplace=True),
nn.Linear(in_features=1280,
out_features=len(class_names), # 根据类别数量调整输出维度
bias=True).to(device)
)
基础层已冻结,分类头已修改,现在我们使用 torchinfo.summary()
获取模型的摘要。
查看模型摘要
from torchinfo import summary
# 获取模型的摘要(取消注释以查看完整输出)
# 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"]
# )
torchinfo.summary()
的输出显示了我们的特征提取器 EffNetB0 模型的结构。请注意,基础层已被冻结(不可训练),而输出层已根据我们的问题进行了自定义。
4. 训练模型并跟踪结果
模型已准备就绪!
让我们通过创建损失函数和优化器来准备好训练它。
由于我们正在处理多类别问题,我们将使用 torch.nn.CrossEntropyLoss()
作为损失函数。
并且我们将坚持使用学习率为 0.001
的 torch.optim.Adam()
作为优化器。
在 [11] 中:
# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
调整 train()
函数以使用 SummaryWriter()
跟踪结果
太棒了!
我们的训练代码的所有部分开始整合在一起。
现在让我们添加最后一个部分来跟踪我们的实验。
之前,我们使用多个 Python 字典(每个模型一个)来跟踪我们的建模实验。
但你可以想象,如果我们要运行更多的实验,这种方法可能会失控。
不用担心,有更好的选择!
我们可以使用 PyTorch 的 torch.utils.tensorboard.SummaryWriter()
类,将模型的训练进度的各个部分保存到文件中。
默认情况下,SummaryWriter()
类会将有关模型的各种信息保存到由 log_dir
参数设置的文件中。
log_dir
的默认位置是 runs/CURRENT_DATETIME_HOSTNAME
,其中 HOSTNAME
是你的计算机名称。
当然,你可以更改实验的跟踪位置(文件名可以根据你的需求自定义)。
SummaryWriter()
的输出以 TensorBoard 格式保存。
TensorBoard 是 TensorFlow 深度学习库的一部分,是可视化模型不同部分的绝佳工具。
为了开始跟踪我们的建模实验,让我们创建一个默认的 SummaryWriter()
实例。
在 [12] 中:
try:
from torch.utils.tensorboard import SummaryWriter
except:
print("[INFO] 找不到 tensorboard... 正在安装它。")
!pip install -q tensorboard
from torch.utils.tensorboard import SummaryWriter
# 创建具有所有默认设置的 writer
writer = SummaryWriter()
现在要使用 writer,我们可以编写一个新的训练循环,或者可以调整现有的 train()
函数(我们在 05. PyTorch Going Modular 第 4 节中创建的)。
我们选择后者。
我们将从 engine.py
获取 train()
函数,并调整它以使用 writer
。
具体来说,我们将为 train()
函数添加记录模型训练和测试损失及准确率值的功能。
我们可以通过 writer.add_scalars(main_tag, tag_scalar_dict)
来实现这一点,其中:
main_tag
(字符串) - 被跟踪标量的名称(例如 “Accuracy”)tag_scalar_dict
(字典) - 被跟踪值的字典(例如{"train_loss": 0.3454}
)
注意: 该方法名为
add_scalars()
,因为我们的损失和准确率值通常是标量(单个值)。
完成值的跟踪后,我们将调用 writer.close()
告诉 writer
停止寻找需要跟踪的值。
为了开始修改 train()
,我们还将从 engine.py
导入 train_step()
和 test_step()
。
注意: 你几乎可以在代码的任何地方跟踪模型的信息。但通常实验会在模型训练时被跟踪(在训练/测试循环内)。
torch.utils.tensorboard.SummaryWriter()
类还有许多不同的方法来跟踪模型/数据的不同方面,例如add_graph()
可以跟踪模型的计算图。更多选项请查阅SummaryWriter()
文档。
在 [13] 中:
from typing import Dict, List
from tqdm.auto import tqdm
from going_modular.going_modular.engine import train_step, test_step
def train(
model: torch.nn.Module,
train_dataloader: torch.utils.data.DataLoader,
test_dataloader: torch.utils.data.DataLoader,
optimizer: torch.optim.Optimizer,
loss_fn: torch.nn.Module,
epochs: int,
device: torch.device
) -> Dict[str, List]:
"""
训练和测试一个 PyTorch 模型。
将目标 PyTorch 模型通过 train_step() 和 test_step() 函数进行多次 epoch 的训练和测试,
在同一个 epoch 循环中训练和测试模型。
计算、打印并存储评估指标。
参数:
model: 要训练和测试的 PyTorch 模型。
train_dataloader: 用于训练模型的 DataLoader 实例。
test_dataloader: 用于测试模型的 DataLoader 实例。
optimizer: 用于帮助最小化损失函数的 PyTorch 优化器。
loss_fn: 用于在两个数据集上计算损失的 PyTorch 损失函数。
epochs: 表示训练多少个 epoch 的整数。
device: 目标设备(例如 "cuda" 或 "cpu")。
返回:
包含训练和测试损失以及训练和测试准确率指标的字典。
每个指标都有一个列表形式的值,对应每个 epoch。
格式为:{train_loss: [...], train_acc: [...], test_loss: [...], test_acc: [...]}
例如,如果训练 epochs=2:
{train_loss: [2.0616, 1.0537],
train_acc: [0.3945, 0.3945],
test_loss: [1.2641, 1.5706],
test_acc: [0.3400, 0.2973]}
"""
# 创建空的结果字典
results = {
"train_loss": [],
"train_acc": [],
"test_loss": [],
"test_acc": []
}
# 循环执行训练和测试步骤多个 epoch
for epoch in tqdm(range(epochs)):
train_loss, train_acc = train_step(
model=model,
dataloader=train_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
device=device
)
test_loss, test_acc = test_step(
model=model,
dataloader=test_dataloader,
loss_fn=loss_fn,
device=device
)
# 打印当前情况
print(
f"Epoch: {epoch+1} | "
f"train_loss: {train_loss:.4f} | "
f"train_acc: {train_acc:.4f} | "
f"test_loss: {test_loss:.4f} | "
f"test_acc: {test_acc:.4f}"
)
# 更新结果字典
results["train_loss"].append(train_loss)
results["train_acc"].append(train_acc)
results["test_loss"].append(test_loss)
results["test_acc"].append(test_acc)
### 新增:实验跟踪 ###
# 将损失结果添加到 SummaryWriter
writer.add_scalars(
main_tag="Loss",
tag_scalar_dict={"train_loss": train_loss, "test_loss": test_loss},
global_step=epoch
)
# 将准确率结果添加到 SummaryWriter
writer.add_scalars(
main_tag="Accuracy",
tag_scalar_dict={"train_acc": train_acc, "test_acc": test_acc},
global_step=epoch
)
# 跟踪 PyTorch 模型架构
writer.add_graph(
model=model,
input_to_model=torch.randn(32, 3, 224, 224).to(device)
)
# 关闭 writer
writer.close()
### 结束新增 ###
# 返回最终结果字典
return results
哇哦!
我们的 train()
函数现在已更新为使用 SummaryWriter()
实例来跟踪模型的结果。
我们为什么不尝试运行 5 个 epoch?
在 [14] 中:
# 训练模型
# 注意:不使用 engine.train(),因为原始脚本尚未更新为使用 writer
set_seeds()
results = train(
model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
optimizer=optimizer,
loss_fn=loss_fn,
epochs=5,
device=device
)
输出如下:
Epoch: 1 | train_loss: 1.0924 | train_acc: 0.3984 | test_loss: 0.9133 | test_acc: 0.5398
Epoch: 2 | train_loss: 0.8975 | train_acc: 0.6562 | test_loss: 0.7838 | test_acc: 0.8561
Epoch: 3 | train_loss: 0.8037 | train_acc: 0.7461 | test_loss: 0.6723 | test_acc: 0.8864
Epoch: 4 | train_loss: 0.6769 | train_acc: 0.8516 | test_loss: 0.6698 | test_acc: 0.8049
Epoch: 5 | train_loss: 0.7065 | train_acc: 0.7188 | test_loss: 0.6746 | test_acc: 0.7737
注意: 你可能会注意到这里的结果与我们在 06. PyTorch Transfer Learning 中得到的模型结果略有不同。差异来自于使用
engine.train()
和我们修改后的train()
函数。你能猜到为什么吗?PyTorch 关于随机性的文档可能会提供更多帮助。
运行上述单元格后,我们得到了与 06. PyTorch Transfer Learning 第 4 节:训练模型类似的输出,但不同之处在于,我们的 writer
实例在后台创建了一个 runs/
目录来存储模型的结果。
例如,保存位置可能如下所示:
runs/Jun21_00-46-03_daniels_macbook_pro
默认格式为 runs/CURRENT_DATETIME_HOSTNAME
。
我们稍后会检查这些内容,但作为提醒,我们之前是通过字典来跟踪模型的结果。
在 [15] 中:
# 查看模型结果
results
输出如下:
{
'train_loss': [1.0923754647374153, 0.8974628075957298, 0.803724929690361, 0.6769256368279457, 0.7064960040152073],
'train_acc': [0.3984375, 0.65625, 0.74609375, 0.8515625, 0.71875],
'test_loss': [0.9132757981618246, 0.7837507526079813, 0.6722926497459412, 0.6698453426361084, 0.6746167540550232],
'test_acc': [0.5397727272727273, 0.8560606060606061, 0.8863636363636364, 0.8049242424242425, 0.7736742424242425]
}
嗯,我们可以将其格式化为漂亮的图表,但你能想象同时跟踪多个这样的字典吗?
一定有更好的方法…
5. 在 TensorBoard 中查看模型的结果
SummaryWriter()
类默认以 TensorBoard 格式将模型的结果存储在名为 runs/
的目录中。
TensorBoard 是由 TensorFlow 团队创建的一个可视化程序,用于查看和检查模型及数据的相关信息。
你知道这意味着什么吗?
是时候遵循数据可视化者的座右铭:可视化、可视化、再可视化!
你可以通过多种方式查看 TensorBoard:
代码环境 | 如何查看 TensorBoard | 资源 |
---|---|---|
VS Code(笔记本或 Python 脚本) | 按下 SHIFT + CMD + P 打开命令面板,并搜索命令 “Python: Launch TensorBoard”。 | VS Code 关于 TensorBoard 和 PyTorch 的指南 |
Jupyter 和 Colab 笔记本 | 确保已安装 TensorBoard,使用 %load_ext tensorboard 加载它,然后通过 %tensorboard --logdir DIR_WITH_LOGS 查看结果。 | torch.utils.tensorboard 和 TensorBoard 入门 |
在 Google Colab 或 Jupyter Notebook 中运行以下代码将启动一个交互式的 TensorBoard 会话,用于查看 runs/
目录中的 TensorBoard 文件。
%load_ext tensorboard # 加载 TensorBoard 的行魔法命令
%tensorboard --logdir runs # 使用 "runs/" 目录运行 TensorBoard 会话
在单元格 [16] 中:
复制完成!
# 示例代码,适用于 Jupyter 或 Google Colab Notebook(取消注释即可运行)
# %load_ext tensorboard
# %tensorboard --logdir runs
示例代码(取消注释后可在 Jupyter 或 Google Colab Notebook 中运行)
# %load_ext tensorboard
# %tensorboard --logdir runs
如果一切正常,你应该会看到类似以下的内容:
在 TensorBoard 中查看单个建模实验的准确率和损失结果。
注意: 如果想了解更多关于在笔记本或其他位置运行 TensorBoard 的信息,请参考以下内容:
- TensorFlow 提供的 在笔记本中使用 TensorBoard 的指南
6. 创建一个辅助函数来构建 SummaryWriter()
实例
SummaryWriter()
类将各种信息记录到由 log_dir
参数指定的目录中。
我们是否可以创建一个辅助函数,为每次实验生成自定义目录?
本质上,每个实验都有自己的日志目录。
例如,我们可能希望跟踪以下内容:
- 实验日期/时间戳 - 实验是在何时进行的?
- 实验名称 - 我们是否希望给实验起个名字?
- 模型名称 - 使用了哪个模型?
- 其他信息 - 是否需要跟踪其他内容?
在这里,您可以几乎跟踪任何内容,并发挥您的创造力,但这些应该足够作为起点。
接下来,我们将创建一个名为 create_writer()
的辅助函数,该函数生成一个 SummaryWriter()
实例,并将其记录到自定义的 log_dir
中。
理想情况下,我们希望 log_dir
的结构如下:
runs/YYYY-MM-DD/experiment_name/model_name/extra
其中 YYYY-MM-DD
是实验运行的日期(如果需要,也可以添加时间)。
创建 create_writer()
函数
def create_writer(experiment_name: str,
model_name: str,
extra: str = None) -> torch.utils.tensorboard.writer.SummaryWriter:
"""
创建一个 torch.utils.tensorboard.writer.SummaryWriter() 实例,
并将其保存到特定的 log_dir 中。
log_dir 是 runs/timestamp/experiment_name/model_name/extra 的组合。
其中 timestamp 是当前日期,格式为 YYYY-MM-DD。
参数:
experiment_name (str): 实验名称。
model_name (str): 模型名称。
extra (str, 可选): 要添加到目录中的其他信息。默认为 None。
返回:
torch.utils.tensorboard.writer.SummaryWriter(): 保存到 log_dir 的 writer 实例。
示例用法:
# 创建一个保存到 "runs/2022-06-04/data_10_percent/effnetb2/5_epochs/" 的 writer
writer = create_writer(experiment_name="data_10_percent",
model_name="effnetb2",
extra="5_epochs")
# 上述等同于:
writer = SummaryWriter(log_dir="runs/2022-06-04/data_10_percent/effnetb2/5_epochs/")
"""
from datetime import datetime
import os
# 获取当前日期的时间戳(同一天的所有实验都存储在同一个文件夹中)
timestamp = datetime.now().strftime("%Y-%m-%d") # 返回当前日期,格式为 YYYY-MM-DD
if extra:
# 创建日志目录路径
log_dir = os.path.join("runs", timestamp, experiment_name, model_name, extra)
else:
log_dir = os.path.join("runs", timestamp, experiment_name, model_name)
print(f"[INFO] 已创建 SummaryWriter,保存路径为: {log_dir}...")
return SummaryWriter(log_dir=log_dir)
非常棒!
现在我们有了 create_writer()
函数,让我们尝试一下。
# 创建一个示例 writer
example_writer = create_writer(experiment_name="data_10_percent",
model_name="effnetb0",
extra="5_epochs")
# 输出结果
[INFO] 已创建 SummaryWriter,保存路径为: runs/2022-06-23/data_10_percent/effnetb0/5_epochs...
看起来不错!现在我们有了一个方法来记录和追溯我们的各种实验。
6.1 更新 train()
函数以包含 writer
参数
我们的 create_writer()
函数工作得很好。
我们是否可以让 train()
函数接受一个 writer
参数,以便我们在每次调用 train()
时都能主动更新正在使用的 SummaryWriter()
实例?
例如,如果我们正在运行一系列实验,并多次调用 train()
来训练多个不同的模型,那么让每个实验使用不同的 writer
将会很有帮助。
一个 writer
对应一个实验 = 每个实验对应一个日志目录。
为了调整 train()
函数,我们将向函数添加一个 writer
参数,并添加一些代码来检查是否存在 writer
,如果存在,则在其中记录我们的信息。
from typing import Dict, List
from tqdm.auto import tqdm
# 向 train() 添加 writer 参数
def train(model: torch.nn.Module,
train_dataloader: torch.utils.data.DataLoader,
test_dataloader: torch.utils.data.DataLoader,
optimizer: torch.optim.Optimizer,
loss_fn: torch.nn.Module,
epochs: int,
device: torch.device,
writer: torch.utils.tensorboard.writer.SummaryWriter = None # 新参数,用于接收 writer
) -> Dict[str, List]:
"""
训练并测试一个 PyTorch 模型。
将目标 PyTorch 模型通过 train_step() 和 test_step() 函数运行多个 epoch,
在同一 epoch 循环中同时训练和测试模型。
计算、打印并存储评估指标。
如果存在 writer,则将指标存储到指定的 writer log_dir 中。
参数:
model: 要训练和测试的 PyTorch 模型。
train_dataloader: 用于训练模型的 DataLoader 实例。
test_dataloader: 用于测试模型的 DataLoader 实例。
optimizer: 帮助最小化损失函数的 PyTorch 优化器。
loss_fn: 用于计算两个数据集上损失的 PyTorch 损失函数。
epochs: 表示要训练多少个 epoch 的整数。
device: 目标设备(例如 "cuda" 或 "cpu")。
writer: 用于记录模型结果的 SummaryWriter() 实例。
返回:
包含训练和测试损失以及训练和测试准确率指标的字典。
每个指标在列表中都有一个值,对应每个 epoch。
格式为:{train_loss: [...], train_acc: [...], test_loss: [...], test_acc: [...]}
例如,如果训练 epochs=2:
{train_loss: [2.0616, 1.0537],
train_acc: [0.3945, 0.3945],
test_loss: [1.2641, 1.5706],
test_acc: [0.3400, 0.2973]}
"""
# 创建空的结果字典
results = {"train_loss": [],
"train_acc": [],
"test_loss": [],
"test_acc": []}
# 在多个 epoch 中循环执行训练和测试步骤
for epoch in tqdm(range(epochs)):
train_loss, train_acc = train_step(model=model,
dataloader=train_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
device=device)
test_loss, test_acc = test_step(model=model,
dataloader=test_dataloader,
loss_fn=loss_fn,
device=device)
# 打印当前情况
print(
f"Epoch: {epoch+1} | "
f"train_loss: {train_loss:.4f} | "
f"train_acc: {train_acc:.4f} | "
f"test_loss: {test_loss:.4f} | "
f"test_acc: {test_acc:.4f}"
)
# 更新结果字典
results["train_loss"].append(train_loss)
results["train_acc"].append(train_acc)
results["test_loss"].append(test_loss)
results["test_acc"].append(test_acc)
### 新增:使用 writer 参数跟踪实验 ###
# 检查是否存在 writer,如果存在,则记录到它
if writer:
# 将结果添加到 SummaryWriter
writer.add_scalars(main_tag="Loss",
tag_scalar_dict={"train_loss": train_loss,
"test_loss": test_loss},
global_step=epoch)
writer.add_scalars(main_tag="Accuracy",
tag_scalar_dict={"train_acc": train_acc,
"test_acc": test_acc},
global_step=epoch)
# 关闭 writer
writer.close()
else:
pass
### 结束新增 ###
# 在所有 epoch 结束后返回填充的结果
return results
这样,我们就完成了对 train()
函数的更新,使其支持 TensorBoard 日志记录功能。
7. 设置一系列建模实验
让我们将实验提升一个档次。
之前我们运行了各种实验并逐一检查结果。
但如果我们可以运行多个实验,然后一起检查结果呢?
你有兴趣吗?
来吧,我们一起开始!
7.1 应该运行哪些实验?
这是机器学习中的百万美元问题。
因为你几乎可以运行无限数量的实验。
这种自由正是机器学习既令人兴奋又令人畏惧的原因。
这时你需要穿上科学家的外套,并记住机器学习实践者的座右铭:实验、实验、实验!
每个超参数都可以作为一个不同实验的起点:
- 改变epoch的数量。
- 改变层数/隐藏单元的数量。
- 改变数据的数量。
- 改变学习率。
- 尝试不同的数据增强方法。
- 选择不同的模型架构。
通过实践和运行许多不同的实验,你会逐渐建立起对可能帮助你的模型的直觉。
我特意说“可能”,因为没有任何保证。
但一般来说,根据The Bitter Lesson(我已经提到过两次,因为它是一篇在AI领域中非常重要的文章),通常模型越大(更多可学习参数)并且数据越多(更多学习机会),性能就越好。
然而,当你第一次接触一个机器学习问题时:从小规模开始,如果某个方法有效,再逐步扩大规模。
你的第一批实验应该在几秒到几分钟内完成。
你越快进行实验,就越快找出哪些不起作用,从而更快找到哪些起作用。
7.2 我们将运行哪些实验?
我们的目标是改进为FoodVision Mini提供支持的模型,同时不让它变得太大。
本质上,我们理想的模型能够在测试集上达到高准确率(90%以上),但训练/推理时间不会太长。
我们有很多选择,但为什么不保持简单呢?
让我们尝试以下组合:
- 不同数量的数据(披萨、牛排、寿司的10% vs. 20%)
- 不同的模型(
torchvision.models.efficientnet_b0
vs.torchvision.models.efficientnet_b2
) - 不同的训练时间(5个epoch vs. 10个epoch)
分解后我们得到以下表格:
实验编号 | 训练数据集 | 预训练于ImageNet的模型 | epoch数量 |
---|---|---|---|
1 | 披萨、牛排、寿司 10% | EfficientNetB0 | 5 |
2 | 披萨、牛排、寿司 10% | EfficientNetB2 | 5 |
3 | 披萨、牛排、寿司 10% | EfficientNetB0 | 10 |
4 | 披萨、牛排、寿司 10% | EfficientNetB2 | 10 |
5 | 披萨、牛排、寿司 20% | EfficientNetB0 | 5 |
6 | 披萨、牛排、寿司 20% | EfficientNetB2 | 5 |
7 | 披萨、牛排、寿司 20% | EfficientNetB0 | 10 |
8 | 披萨、牛排、寿司 20% | EfficientNetB2 | 10 |
注意我们是如何逐步扩展的。
每次实验我们都慢慢增加数据量、模型大小和训练时间。
最终,实验8将使用比实验1多一倍的数据、两倍的模型大小和两倍的训练时间。
注意:我想明确指出,你可以运行的实验数量没有限制。我们设计的这些只是选项的一小部分。然而,你无法测试所有内容,所以最好先尝试一些,然后继续那些效果最好的。
作为提醒,我们使用的数据集是Food101数据集的一个子集(3类,披萨、牛排、寿司,而不是101类),并且是10%和20%的图像,而不是100%。如果我们的实验成功,我们可以开始在更多数据上运行更多实验(尽管这需要更长时间计算)。你可以通过04_custom_data_creation.ipynb
笔记本查看数据集是如何创建的。
7.3 下载不同的数据集
在我们开始运行一系列实验之前,我们需要确保数据集已准备好。
我们需要两种形式的训练集:
- 包含Food101披萨、牛排、寿司图像10%数据的训练集(我们已经在上面创建了这个,但为了完整性我们将再次创建)。
- 包含Food101披萨、牛排、寿司图像20%数据的训练集。
出于一致性考虑,所有实验都将使用相同的测试数据集(来自10%数据分割的测试数据集)。
我们将使用之前创建的download_data()
函数下载所需的各个数据集。
两个数据集都可以从课程GitHub获取:
- 披萨、牛排、寿司10%训练数据。
- 披萨、牛排、寿司20%训练数据。
# 下载10%和20%的训练数据(如需)
data_10_percent_path = download_data(source="https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip",
destination="pizza_steak_sushi")
data_20_percent_path = download_data(source="https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi_20_percent.zip",
destination="pizza_steak_sushi_20_percent")
[INFO] data/pizza_steak_sushi目录已存在,跳过下载。
[INFO] data/pizza_steak_sushi_20_percent目录已存在,跳过下载。
数据已下载!
现在让我们设置用于不同实验的数据路径。
我们将创建不同的训练目录路径,但只需要一个测试目录路径,因为所有实验都将使用相同的测试数据集(来自披萨、牛排、寿司10%的测试数据集)。
# 设置训练目录路径
train_dir_10_percent = data_10_percent_path / "train"
train_dir_20_percent = data_20_percent_path / "train"
# 设置测试目录路径(注意:使用相同的测试数据集以比较结果)
test_dir = data_10_percent_path / "test"
# 检查目录
print(f"训练目录10%: {train_dir_10_percent}")
print(f"训练目录20%: {train_dir_20_percent}")
print(f"测试目录: {test_dir}")
训练目录10%: data/pizza_steak_sushi/train
训练目录20%: data/pizza_steak_sushi_20_percent/train
测试目录: data/pizza_steak_sushi/test
7.4 转换数据集并创建DataLoaders
接下来我们将创建一系列转换来准备我们的图像供模型使用。
为了保持一致性,我们将手动创建一个转换(就像我们之前做的那样),并在所有数据集上使用相同的转换。
转换将执行以下操作:
- 调整所有图像大小(我们从224x224开始,但可以更改)。
- 将它们转换为值在0到1之间的张量。
- 标准化它们,使其分布与ImageNet数据集一致(我们这样做是因为我们的模型来自
torchvision.models
,它们已在ImageNet上预训练)。
from torchvision import transforms
# 创建一个转换以使数据分布与ImageNet一致
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], # 每个颜色通道的均值 [红, 绿, 蓝]
std=[0.229, 0.224, 0.225]) # 每个颜色通道的标准差 [红, 绿, 蓝]
# 将转换组合成管道
simple_transform = transforms.Compose([
transforms.Resize((224, 224)), # 1. 调整图像大小
transforms.ToTensor(), # 2. 将图像转换为张量,值在0到1之间
normalize # 3. 标准化图像,使其分布与ImageNet数据集一致
])
转换已准备好!
现在让我们使用我们在05. PyTorch模块化部分2中创建的data_setup.py
中的create_dataloaders()
函数创建DataLoaders。
我们将使用批量大小为32创建DataLoaders。
对于所有实验,我们将使用相同的test_dataloader
(以保持比较一致性)。
BATCH_SIZE = 32
# 创建10%训练和测试DataLoaders
train_dataloader_10_percent, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir_10_percent,
test_dir=test_dir,
transform=simple_transform,
batch_size=BATCH_SIZE
)
# 创建20%训练和测试数据DataLoaders
train_dataloader_20_percent, test_dataloader, class_names = data_setup.create_dataloaders(
train_dir=train_dir_20_percent,
test_dir=test_dir,
transform=simple_transform,
batch_size=BATCH_SIZE
)
# 查找每个DataLoader中的样本/批次数量(使用相同的test_dataloader进行两个实验)
print(f"10%训练数据中大小为{BATCH_SIZE}的批次数量: {len(train_dataloader_10_percent)}")
print(f"20%训练数据中大小为{BATCH_SIZE}的批次数量: {len(train_dataloader_20_percent)}")
print(f"测试数据中大小为{BATCH_SIZE}的批次数量: {len(test_dataloader)} (所有实验将使用相同的测试集)")
print(f"类别数量: {len(class_names)}, 类别名称: {class_names}")
10%训练数据中大小为32的批次数量: 8
20%训练数据中大小为32的批次数量: 15
测试数据中大小为32的批次数量: 8 (所有实验将使用相同的测试集)
类别数量: 3, 类别名称: ['pizza', 'steak', 'sushi']
7.5 创建特征提取模型
是时候开始构建我们的模型了。
我们将创建两个特征提取模型:
torchvision.models.efficientnet_b0()
预训练主干 + 自定义分类头(简称EffNetB0)。torchvision.models.efficientnet_b2()
预训练主干 + 自定义分类头(简称EffNetB2)。
为此,我们将冻结基础层(特征层)并更新模型的分类头(输出层)以适应我们的问题,就像我们在06. PyTorch迁移学习部分3.4中所做的那样。
我们在前一章看到EffNetB0分类头的in_features
参数是1280
(主干将输入图像转换为大小为1280
的特征向量)。
由于EffNetB2具有不同的层数和参数,我们需要相应地调整它。
注意:无论你使用哪种模型,首先应该检查的是输入和输出形状。这样你就会知道如何准备输入数据/更新模型以获得正确的输出形状。
我们可以使用torchinfo.summary()
并传递input_size=(32, 3, 224, 224)
参数来查找EffNetB2的输入和输出形状((32, 3, 224, 224)
等价于(batch_size, color_channels, height, width)
,即我们向模型传递一个单批数据的示例)。
注意:许多现代模型可以通过
torch.nn.AdaptiveAvgPool2d()
层处理不同大小的输入图像,该层会根据需要自适应调整output_size
。你可以通过向torchinfo.summary()
或使用该层的模型传递不同大小的输入图像来尝试这一点。
为了找到EffNetB2最后一层所需的输入形状,让我们:
- 创建一个
torchvision.models.efficientnet_b2(pretrained=True)
实例。 - 运行
torchinfo.summary()
查看各种输入和输出形状。 - 打印出EffNetB2分类部分的
state_dict()
中权重矩阵的长度以获取in_features
的数量。- 注意:你也可以直接检查
effnetb2.classifier
的输出。
- 注意:你也可以直接检查
import torchvision
from torchinfo import summary
# 1. 创建一个EffNetB2实例,带有预训练权重
effnetb2_weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT # "DEFAULT"表示最佳可用权重
effnetb2 = torchvision.models.efficientnet_b2(weights=effnetb2_weights)
# 2. 获取EffNetB2的标准摘要(取消注释以查看完整输出)
# summary(model=effnetb2,
# 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"]
# )
# 3. 获取EffNetB2分类层的in_features数量
print(f"EfficientNetB2最后一层的in_features数量: {len(effnetb2.classifier.state_dict()['1.weight'][0])}")
EfficientNetB2最后一层的in_features数量: 140
8. 在 TensorBoard 中查看实验结果
呵,呵!
看看我们进展得多快!
一次训练八个模型?
这才叫真正践行座右铭!
实验、实验、再实验!
那么,我们何不通过 TensorBoard 查看结果呢?
在代码块 [30] 中:
# 在 Jupyter 和 Google Colab 笔记本中查看 TensorBoard(取消注释以查看完整的 TensorBoard 实例)
# %load_ext tensorboard
# %tensorboard --logdir runs
# 在 Jupyter 和 Google Colab 笔记本中查看 TensorBoard(取消注释以查看完整的 TensorBoard 实例)
# %load_ext tensorboard
# %tensorboard --logdir runs
运行上述单元格后,我们应该会得到类似于以下的输出。
注意: 根据你使用的随机种子或硬件,你的数值可能与这里的不完全相同。这是正常的,因为深度学习本身具有一定的随机性。最重要的是趋势,即你的数值变化的方向。如果数值偏差较大,可能是某些地方出了问题,建议回过头检查代码。但如果只是小幅度差异(比如几个小数位),那是可以接受的。
通过在 TensorBoard 中可视化不同建模实验的测试损失值,可以看到 EffNetB0 模型在使用 20% 的数据并训练 10 个 epoch 后取得了最低的损失值。这与实验的整体趋势一致:更多的数据、更大的模型以及更长的训练时间通常会带来更好的效果。
9. 加载最佳模型并进行预测
通过查看我们八个实验的 TensorBoard 日志,实验八似乎取得了整体最佳结果(最高的测试准确率和第二低的测试损失)。
这个实验使用了以下配置:
- EffNetB2(EffNetB0 参数量的两倍)
- 20% 的披萨、牛排、寿司训练数据(原始训练数据的两倍)
- 10 个 epoch(原始训练时间的两倍)
简而言之,我们的最大模型取得了最佳结果。
尽管这些结果并没有比其他模型好太多。
相同的模型在相同的数据上以一半的训练时间取得了相似的结果(实验六)。
这表明我们实验中最具影响力的可能是参数数量和数据量。
进一步检查结果后发现,通常情况下,参数更多(EffNetB2)和数据更多(20% 的披萨、牛排、寿司训练数据)的模型表现更好(测试损失更低,测试准确率更高)。
可以进行更多实验来进一步验证这一点,但目前,让我们加载实验八中的最佳模型(保存路径为:models/07_effnetb2_data_20_percent_10_epochs.pth
,您可以通过课程的 GitHub 下载该模型),并进行一些定性评估。
换句话说,让我们可视化、可视化、再可视化!
我们可以通过创建一个 EffNetB2 的新实例(用于加载保存的 state_dict()
),然后使用 torch.load()
加载保存的 state_dict()
来导入最佳保存的模型。
# 设置最佳模型文件路径
best_model_path = "models/07_effnetb2_data_20_percent_10_epochs.pth"
# 创建 EffNetB2 的新实例(用于加载保存的 state_dict())
best_model = create_effnetb2()
# 加载保存的最佳模型 state_dict()
best_model.load_state_dict(torch.load(best_model_path))
[INFO] 已创建新的 effnetb2 模型。
最佳模型已加载!
既然我们在这里,让我们检查一下它的文件大小。
这是稍后部署模型时需要考虑的重要因素(将其集成到应用程序中)。
如果模型太大,可能会难以部署。
# 检查模型文件大小
from pathlib import Path
# 获取模型大小(以字节为单位),然后转换为兆字节
effnetb2_model_size = Path(best_model_path).stat().st_size // (1024 * 1024)
print(f"EfficientNetB2 特征提取器模型大小: {effnetb2_model_size} MB")
EfficientNetB2 特征提取器模型大小: 29 MB
看起来我们目前的最佳模型大小为 29 MB。如果我们稍后想要部署它,我们会记住这一点。
现在开始进行预测并可视化结果。
我们在 06. PyTorch 迁移学习部分第 6 节中创建了一个 pred_and_plot_image()
函数,用于使用训练好的模型对图像进行预测并绘制结果。
我们可以通过从 going_modular.going_modular.predictions.py
导入该函数来重用它(我将 pred_and_plot_image()
函数放在脚本中以便重用)。
因此,为了对模型未见过的各种图像进行预测,我们将首先从 20% 的披萨、牛排、寿司测试数据集中获取所有图像文件路径,然后随机选择其中的一部分路径传递给我们的 pred_and_plot_image()
函数。
# 导入用于对图像进行预测并绘制结果的函数
from going_modular.going_modular.predictions import pred_and_plot_image
# 随机获取 20% 测试集中的 3 张图像
import random
num_images_to_plot = 3
test_image_path_list = list(Path(data_20_percent_path / "test").glob("*/*.jpg")) # 获取 20% 数据集中所有测试图像路径
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=best_model,
image_path=image_path,
class_names=class_names,
image_size=(224, 224)
)
很好!
多次运行上述单元格后,我们可以看到我们的模型表现相当不错,并且通常比我们之前构建的模型具有更高的预测概率。
这表明模型对其所做的决策更加自信。
9.1 使用最佳模型对自定义图像进行预测
在测试数据集上进行预测很酷,但机器学习真正的魔力在于对您自己的自定义图像进行预测。
因此,让我们导入我们过去几个部分中一直使用的披萨爸爸图像(一张我爸爸站在披萨前的照片),看看我们的模型在它上面的表现如何。
# 下载自定义图像
import requests
# 设置自定义图像路径
custom_image_path = Path("data/04-pizza-dad.jpeg")
# 如果图像不存在,则下载
if not custom_image_path.is_file():
with open(custom_image_path, "wb") as f:
# 从 GitHub 下载时,需要使用“raw”文件链接
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=best_model,
image_path=custom_image_path,
class_names=class_names
)
data/04-pizza-dad.jpeg 已存在,跳过下载。
哇!
再次得到了两个大拇指!
我们的最佳模型正确预测了“披萨”,并且这次的预测概率(0.978)甚至比我们在 06. PyTorch 迁移学习部分第 6.1 节中训练和使用的第一个特征提取模型更高。
这再次表明我们当前的最佳模型(在 20% 的披萨、牛排、寿司训练数据上训练了 10 个 epoch 的 EffNetB2 特征提取器)已经学到了模式,使其对预测披萨的决策更加自信。
我想知道还有什么可以进一步提高我们模型的性能?
我会把这个作为留给您的挑战去研究。
优质资源
- 阅读 Richard Sutton 的博客文章《The Bitter Lesson》,了解许多最新的 AI 进步如何源于规模的增加(更大的数据集和更大的模型)以及更通用(而非精心设计)的方法。
- 如果您希望以 DataFrame 的形式查看和重新排列模型的 TensorBoard 日志(以便按最低损失或最高准确率对结果进行排序),可以在 TensorBoard 文档中找到相关指南。
- 如果您喜欢使用 VSCode 开发脚本或笔记本(VSCode 现在可以原生支持 Jupyter Notebook),您可以按照 PyTorch 在 VSCode 中开发的指南,在 VSCode 内直接设置 TensorBoard。
- 如果您想进一步跟踪实验,并从速度角度了解 PyTorch 模型的表现(是否存在可以改进以加速训练的瓶颈?),请参阅 PyTorch 文档中的 PyTorch Profiler 部分。
- Made With ML是 Goku Mohandas 提供的一个出色的机器学习资源网站,其中关于实验跟踪的指南包含了一个非常棒的介绍,讲解如何使用 MLflow 跟踪机器学习实验。