计算机视觉101:7-PyTorch 实验跟踪

在这里插入图片描述

计算机视觉101,计算机视觉(Computer Vision, CV)是人工智能的核心领域之一,它赋予机器“看”和“理解”世界的能力,广泛应用于自动驾驶、医疗影像、工业检测、增强现实等前沿场景。然而,从理论到落地,CV工程师不仅需要掌握算法原理,更要具备工程化思维,才能让模型真正服务于现实需求。本课程以​​“学以致用”​​为核心理念,采用​​“理论+代码+部署”​​三位一体的教学方式,带您系统掌握计算机视觉的核心技术栈:

  1. 从基础开始​​:理解图像处理(OpenCV)、特征工程(SIFT/SURF)等传统方法,夯实CV基本功
  2. 进阶深度学习​​:掌握CNN、Transformer等现代模型(PyTorch),学会数据增强、模型调优技巧
  3. 实战部署落地​​:学习模型量化、ONNX转换、边缘计算(TensorRT/RKNN)

让算法在真实场景中高效运行通过​​Python代码示范+工业级案例(如缺陷检测、人脸识别)​​,您将逐步构建从​​算法开发到嵌入式部署​​的完整能力链,最终独立完成一个可落地的CV项目。

无论您是希望转行AI的开发者、在校学生,还是寻求技术突破的工程师,本课程都将助您跨越“纸上谈兵”的瓶颈,成为兼具​​算法能力与工程思维​​的CV实战派。

​​🚀 现在开始,用代码让机器真正“看见”世界!​

什么是实验跟踪?

机器学习和深度学习非常依赖实验。

你需要戴上艺术家的贝雷帽或厨师帽,尝试构建各种不同的模型。

同时,你还需披上科学家的外套,记录数据、模型架构和训练策略的各种组合结果。

这就是实验跟踪的作用所在。

如果你在运行大量不同的实验,实验跟踪能够帮助你明确哪些方法有效,哪些无效

为什么要跟踪实验?

如果你只运行少量模型(就像我们目前所做的那样),可能仅仅通过打印输出和一些字典来跟踪它们的结果就足够了。

然而,随着你运行的实验数量开始增加,这种简单的跟踪方式可能会变得难以管理。
在这里插入图片描述

因此,如果你遵循机器学习实践者的座右铭:实验、实验、再实验!,那么你需要一种方法来跟踪这些实验。

在构建了一些模型并跟踪其结果后,你会开始意识到事情会多么迅速地变得复杂。

跟踪机器学习实验的不同方法

跟踪机器学习实验的方法与可以运行的实验数量一样多。

下表涵盖了一些常见的方法:

方法设置优点缺点成本
Python字典、CSV文件、打印输出无需额外设置易于设置,纯Python环境即可运行难以跟踪大量实验免费
TensorBoard最小化设置,安装tensorboardPyTorch内置扩展,广泛使用且易于扩展用户体验不如其他选项友好免费
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.pyengine.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),以正确准备我们的图像。

为了将图像转换为张量,我们可以使用以下两种方法:

  1. 使用torchvision.transforms手动创建转换。
  2. 使用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.001torch.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.tensorboardTensorBoard 入门

在 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 的信息,请参考以下内容:

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%以上),但训练/推理时间不会太长。

我们有很多选择,但为什么不保持简单呢?

让我们尝试以下组合:

  1. 不同数量的数据(披萨、牛排、寿司的10% vs. 20%)
  2. 不同的模型(torchvision.models.efficientnet_b0 vs. torchvision.models.efficientnet_b2
  3. 不同的训练时间(5个epoch vs. 10个epoch)

分解后我们得到以下表格:

实验编号训练数据集预训练于ImageNet的模型epoch数量
1披萨、牛排、寿司 10%EfficientNetB05
2披萨、牛排、寿司 10%EfficientNetB25
3披萨、牛排、寿司 10%EfficientNetB010
4披萨、牛排、寿司 10%EfficientNetB210
5披萨、牛排、寿司 20%EfficientNetB05
6披萨、牛排、寿司 20%EfficientNetB25
7披萨、牛排、寿司 20%EfficientNetB010
8披萨、牛排、寿司 20%EfficientNetB210

注意我们是如何逐步扩展的。

每次实验我们都慢慢增加数据量、模型大小和训练时间。

最终,实验8将使用比实验1多一倍的数据、两倍的模型大小和两倍的训练时间。

注意:我想明确指出,你可以运行的实验数量没有限制。我们设计的这些只是选项的一小部分。然而,你无法测试所有内容,所以最好先尝试一些,然后继续那些效果最好的。

作为提醒,我们使用的数据集是Food101数据集的一个子集(3类,披萨、牛排、寿司,而不是101类),并且是10%和20%的图像,而不是100%。如果我们的实验成功,我们可以开始在更多数据上运行更多实验(尽管这需要更长时间计算)。你可以通过04_custom_data_creation.ipynb笔记本查看数据集是如何创建的。

7.3 下载不同的数据集

在我们开始运行一系列实验之前,我们需要确保数据集已准备好。

我们需要两种形式的训练集:

  1. 包含Food101披萨、牛排、寿司图像10%数据的训练集(我们已经在上面创建了这个,但为了完整性我们将再次创建)。
  2. 包含Food101披萨、牛排、寿司图像20%数据的训练集。

出于一致性考虑,所有实验都将使用相同的测试数据集(来自10%数据分割的测试数据集)。

我们将使用之前创建的download_data()函数下载所需的各个数据集。

两个数据集都可以从课程GitHub获取:

  1. 披萨、牛排、寿司10%训练数据。
  2. 披萨、牛排、寿司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

接下来我们将创建一系列转换来准备我们的图像供模型使用。

为了保持一致性,我们将手动创建一个转换(就像我们之前做的那样),并在所有数据集上使用相同的转换。

转换将执行以下操作:

  1. 调整所有图像大小(我们从224x224开始,但可以更改)。
  2. 将它们转换为值在0到1之间的张量。
  3. 标准化它们,使其分布与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 创建特征提取模型

是时候开始构建我们的模型了。

我们将创建两个特征提取模型:

  1. torchvision.models.efficientnet_b0() 预训练主干 + 自定义分类头(简称EffNetB0)。
  2. 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最后一层所需的输入形状,让我们:

  1. 创建一个torchvision.models.efficientnet_b2(pretrained=True)实例。
  2. 运行torchinfo.summary()查看各种输入和输出形状。
  3. 打印出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 跟踪机器学习实验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值