基于Ultralytics YOLO通用目标检测训练体系与PyTorch EfficientNet的图像分类体系实现
第一部分聚焦于目标检测任务,基于先进的Ultralytics YOLO框架,构建了一套高度自动化和工程化的训练体系。该体系始于
demo.py
脚本,通过对原始图像与标签文件进行智能匹配与随机划分,解决了数据不一致问题,并通过设置随机种子确保了实验的可复现性。划分后的数据集通过标准化的data.yaml
文件进行管理,清晰定义了训练、验证和测试集的路径以及类别信息。在train.py
中,采用YOLOv11架构从零开始训练,通过精细配置SGD优化器、学习率预热、早停机制、混合精度训练(AMP)和多种数据增强策略(如Mosaic、HSV变换),实现了对“Car”和“Person”类别的高效定位与识别。
第二部分则专注于图像分类任务,基于
PyTorch torchvision
,实践了高效的迁移学习范式。项目采用在 ImageNet 上预训练的 EfficientNet-B0 模型作为强大的特征提取器,通过冻结主干网络(features)并重构分类头(classifier),使其适应 “pizza” 、 “steak” 和 “sushi” 三类食物的分类需求。训练过程中,严格应用了与预训练模型一致的数据预处理流程(包括归一化参数),确保了输入数据的一致性。该方法仅用少量数据和极短的训练时间,便在测试集上取得了超过 85% 的高准确率,充分体现了迁移学习在小样本场景下的巨大优势。
1. 基于Ultralytics YOLO的通用目标检测训练体系
1.1 demo.py 划分数据集
import os
import shutil
import random# --- 随机性控制说明 ---
# random.seed(0) # 【已开启】设置随机种子为0。开启此行可确保每次运行程序时,数据划分的结果都完全相同,保证实验的可复现性。
# 这对于调试和对比不同模型或参数的实验结果至关重要。
# # random.seed(0) # 【已关闭】注释掉此行(如当前状态),则每次运行程序时,数据划分都是完全随机的,训练/验证/测试集的具体内容会不同。
# --- 随机性控制说明 ---def split_data(file_path, label_path, new_file_path, train_rate, val_rate, test_rate):# 获取文件列表images = os.listdir(file_path)labels = os.listdir(label_path)# 创建不包含扩展名的字典,用于匹配images_no_ext = {os.path.splitext(image)[0]: image for image in images}labels_no_ext = {os.path.splitext(label)[0]: label for label in labels}# 找到图片和标签都能匹配上的数据对matched_data = [(img, images_no_ext[img], labels_no_ext[img]) for img in images_no_ext if img in labels_no_ext]# 检查并打印未匹配的文件unmatched_images = [img for img in images_no_ext if img not in labels_no_ext]unmatched_labels = [label for label in labels_no_ext if label not in images_no_ext]if unmatched_images:print("未匹配的图片文件:")for img in unmatched_images:print(images_no_ext[img])if unmatched_labels:print("未匹配的标签文件:")for label in unmatched_labels:print(labels_no_ext[label])# 【关键步骤】对匹配好的数据进行随机打乱# 如果上面设置了 random.seed(0),这里的打乱顺序是固定的。# 如果没有设置,这里的打乱顺序是随机的。random.shuffle(matched_data)# 计算总数据量和各数据集的切分点total = len(matched_data)train_data = matched_data[:int(train_rate * total)]val_data = matched_data[int(train_rate * total):int((train_rate + val_rate) * total)]test_data = matched_data[int((train_rate + val_rate) * total):]# 处理训练集for img_name, img_file, label_file in train_data:old_img_path = os.path.join(file_path, img_file)old_label_path = os.path.join(label_path, label_file)new_img_dir = os.path.join(new_file_path, 'train', 'images')new_label_dir = os.path.join(new_file_path, 'train', 'labels')os.makedirs(new_img_dir, exist_ok=True)os.makedirs(new_label_dir, exist_ok=True)shutil.copy(old_img_path, os.path.join(new_img_dir, img_file))shutil.copy(old_label_path, os.path.join(new_label_dir, label_file))# 处理验证集for img_name, img_file, label_file in val_data:old_img_path = os.path.join(file_path, img_file)old_label_path = os.path.join(label_path, label_file)new_img_dir = os.path.join(new_file_path, 'val', 'images')new_label_dir = os.path.join(new_file_path, 'val', 'labels')os.makedirs(new_img_dir, exist_ok=True)os.makedirs(new_label_dir, exist_ok=True)shutil.copy(old_img_path, os.path.join(new_img_dir, img_file))shutil.copy(old_label_path, os.path.join(new_label_dir, label_file))# 处理测试集for img_name, img_file, label_file in test_data:old_img_path = os.path.join(file_path, img_file)old_label_path = os.path.join(label_path, label_file)new_img_dir = os.path.join(new_file_path, 'test', 'images')new_label_dir = os.path.join(new_file_path, 'test', 'labels')os.makedirs(new_img_dir, exist_ok=True)os.makedirs(new_label_dir, exist_ok=True)shutil.copy(old_img_path, os.path.join(new_img_dir, img_file))shutil.copy(old_label_path, os.path.join(new_label_dir, label_file))print("数据集已划分完成")if __name__ == '__main__':file_path = r"D:\yolo\train\data\JPEGImages" # 图片文件夹label_path = r'D:\yolo\train\data\labels' # 标签文件夹new_file_path = r"D:\yolo\train\VOCdevkit" # 新数据存放位置split_data(file_path, label_path, new_file_path, train_rate=0.8, val_rate=0.1, test_rate=0.1)
1.2 data.yaml配置文件
# 指定训练集、验证集、测试集的图片路径。
# YOLO 会自动在这些图片文件夹的同级目录下寻找对应的标签(labels)文件夹。
train: D:/yolo/train/VOCdevkit/train/images
val: D:/yolo/train/VOCdevkit/val/images
test: D:/yolo/train/VOCdevkit/test/images# nc (number of classes): 数据集中总共有多少个类别。
nc: 2# names: 类别的名称列表,顺序必须和你的标签文件中的类别索引(0, 1, ...)一一对应。
names: ['Car','Person']
1.3 train.py 训练
from ultralytics import YOLOif __name__ == '__main__':# --- 模型初始化 ---# 使用 YOLO 类加载一个全新的、未预训练的模型架构。# 这里加载的是 ultralytics 代码库中定义的 YOLOv11 模型结构配置文件。# 训练将从随机初始化的权重开始,适用于从零开始训练或使用完全自定义的架构。model = YOLO(r'ultralytics/cfg/models/11/yolo11.yaml') # 不使用预训练权重训练# 另一种初始化方式(被注释):# 1. 首先加载一个YOLOv8的模型结构。# 2. 然后使用官方发布的预训练权重文件 'yolov8n.pt' 来初始化模型。# 这种方式称为“迁移学习”,能极大地加速收敛并提升最终性能,是更常见的做法。# model = YOLO(r'yolov8.yaml').load("yolov8n.pt") # 使用预训练权重训练# --- 开始训练 ---# 调用 model.train() 方法启动训练流程,传入一系列配置参数。model.train(# 【数据配置】---------------------------------------------------------------------------------------# data (str): 指向数据集配置文件(data.yaml)的绝对路径。# 这是训练的基石,文件中定义了训练集、验证集的图像路径、类别数量(nc)和类别名称(names)。data=r'D:/Bob/PyProject/ultralytics-8.3.58/data.yaml',# epochs (int): 训练的总轮数。每一轮(epoch)代表模型完整地遍历一次整个训练数据集。# 设置为300意味着模型将学习300遍训练数据。值越大,训练时间越长,但可能过拟合。epochs=300,# patience (int): 早停机制的容忍周期数。如果在连续`patience`个epoch内,验证集上的关键指标(如mAP)没有提升,# 训练将自动停止,以防止过拟合和节省计算资源。设置为50表示耐心等待50轮无改善。patience=50,# batch (int): 每个批次(batch)处理的图像数量。较大的batch size能提供更稳定的梯度估计,但需要更多显存。# 设置为16表示每次用16张图片计算一次梯度更新。-1表示自动选择批处理大小(auto-batch)。batch=16,# imgsz (int): 输入到模型的图像尺寸。所有图像在送入网络前都会被调整到此尺寸(如640x640)。# 尺寸越大,模型可能看到更多细节,但计算量和显存消耗也越大。imgsz=640,# save (bool): 是否保存训练过程中的模型权重。True表示会保存最佳模型(best.pt)和最终模型(last.pt)。save=True,# save_period (int): 每隔多少个epoch保存一次模型检查点。如果设为-1,则只保存最佳和最终模型,不保存中间检查点。# 如果设为正整数(如10),则每10个epoch保存一次,可用于恢复中断的训练或分析训练过程。save_period=-1,# cache (bool or str): 是否将数据集缓存到内存("ram")或磁盘("disk")以加速训练。# True/"ram"会将所有图像加载到内存,速度最快但耗内存;"disk"会将处理后的数据存到磁盘;False则不缓存,每次读取原始文件。cache=False,# device (str, int, list): 指定训练所用的设备。可以是:# - '' 或 'auto': 自动选择(优先GPU)# - 'cpu': 强制使用CPU# - '0': 使用第一个GPU# - '0,1,2,3': 使用多个GPU进行分布式训练device='',# workers (int): 数据加载的并行工作线程数。这些线程负责从磁盘读取图像、进行数据增强等预处理操作。# 设置合适的值(通常等于CPU核心数)可以避免数据加载成为训练瓶颈。workers=8,# project (str): 训练结果的顶级项目文件夹名称。所有实验结果将保存在 'project' 目录下。project='runs/train',# name (str): 当前实验的名称。最终的训练结果将保存在 'project/name' 的子目录中(如 'runs/train/exp')。name='exp',# exist_ok (bool): 如果为True,则允许覆盖已存在的同名实验目录。如果为False,则会创建一个新目录(如 exp1, exp2)来避免覆盖。exist_ok=False,# pretrained (bool or str): 是否使用预训练权重。# - True: 使用在大型数据集(如ImageNet)上预训练的权重来初始化模型(迁移学习)。# - False: 从头开始随机初始化权重训练。# - str: 指定一个自定义的预训练权重文件路径。pretrained=True,# optimizer (str): 选择用于优化模型权重的优化器算法。常用选项包括:# - 'SGD': 随机梯度下降,经典稳定。# - 'Adam': 自适应矩估计,收敛快。# - 'AdamW': Adam的改进版,权重衰减更合理。# - 'auto': 让框架根据模型自动选择(通常是AdamW)。optimizer='SGD',# verbose (bool): 是否打印详细的训练日志信息。True会输出更多细节,False则更简洁。verbose=True,# seed (int): 随机种子。设置一个固定值(如0)可以确保每次运行代码时,数据打乱、权重初始化等随机过程都完全相同,# 从而保证实验结果的可复现性,这对于调试和对比实验至关重要。seed=0,# deterministic (bool): 是否启用PyTorch的确定性算法。如果为True,会强制使用那些结果可复现的算法,# 结合固定的seed,可以最大程度保证结果的可复现性。但可能会牺牲少量性能。deterministic=True,# single_cls (bool): 如果数据集本质上是多类的,但你想把它当作单类(如“物体”)检测问题来训练,设为True。# 这会忽略类别信息,只检测物体是否存在。single_cls=False,# rect (bool): 是否使用矩形训练。如果为True,会将同一批次(batch)内的图像调整为相同的矩形尺寸(宽高比保持不变),# 而不是统一的正方形,可以减少padding,提高训练效率。通常在训练和验证时都设为True。rect=False,# cos_lr (bool): 是否使用余弦退火学习率调度器。如果为True,学习率会从初始值lr0开始,按照余弦曲线平滑地衰减到最终值lrf。# 这种方式有时能获得更好的性能。cos_lr=False,# close_mosaic (int): 在训练的最后几个epoch禁用马赛克(Mosaic)数据增强。马赛克增强在训练初期非常有效,# 但在后期可能会干扰模型对单个物体的精细学习。设置为10表示在最后10个epoch关闭此增强。close_mosaic=0,# resume (bool): 是否从中断的地方恢复训练。如果为True,程序会自动寻找上次训练的'last.pt'检查点并从中继续。# 这对于意外中断的训练非常有用。resume=False,# amp (bool): 是否启用自动混合精度(Automatic Mixed Precision)训练。如果为True,计算会使用float16半精度,# 可以显著减少显存占用并加快训练速度,同时基本不损失精度。现代GPU对此支持良好。amp=True,# fraction (float): 训练时使用的数据集比例。1.0表示使用训练集中的所有图像。0.5表示只使用50%的数据。# 常用于快速测试或数据集过大时。fraction=1.0,# profile (bool): 是否在训练期间为日志记录器启用性能分析(如ONNX和TensorRT的推理速度测试)。# 用于分析模型瓶颈,通常在调试时开启。profile=False,# freeze (int or list): 冻结模型的前n层(int)或指定索引的层(list),使其在训练过程中权重不更新。# 通常用于微调,只训练最后几层,以加快速度并防止过拟合。freeze=None,# 【分割任务专用】-----------------------------------------------------------------------------------# overlap_mask (bool): 在训练实例分割模型时,是否允许分割掩码(mask)重叠。对于有重叠物体的场景(如人群),应设为True。overlap_mask=True,# mask_ratio (int): 分割掩码的降采样比例。原始掩码会被缩小到1/mask_ratio的尺寸进行计算,以减少计算量。# 例如4表示将640x640的掩码降采样到160x160。mask_ratio=4,# 【分类任务专用】-----------------------------------------------------------------------------------# dropout (float): 在分类头中使用的Dropout正则化率。在训练时随机将一部分神经元输出置零,以防止过拟合。# 0.0表示不使用Dropout。dropout=0.0,# 【超参数 - 优化器相关】---------------------------------------------------------------------------# lr0 (float): 优化器的初始学习率。对于SGD,1E-2 (0.01) 是一个常见起点;对于Adam,1E-3 (0.001) 更常见。lr0=0.01,# lrf (float): 最终学习率与初始学习率的比值。最终学习率 = lr0 * lrf。例如0.01表示学习率衰减到初始值的1%。lrf=0.01,# momentum (float): SGD优化器的动量系数,或Adam优化器的beta1参数。帮助加速收敛并越过局部最优。momentum=0.937,# weight_decay (float): 优化器的权重衰减(L2正则化)系数,用于防止过拟合。5e-4 (0.0005) 是一个标准值。weight_decay=0.0005,# warmup_epochs (float): 学习率预热(Warmup)的周期数。在训练开始的几个epoch,学习率会从一个很小的值线性增加到lr0,# 有助于稳定训练初期的梯度更新。可以是小数(如3.0)。warmup_epochs=3.0,# warmup_momentum (float): 预热期间动量的初始值。warmup_momentum=0.8,# warmup_bias_lr (float): 预热期间偏置(bias)参数的学习率。warmup_bias_lr=0.1,# 【超参数 - 损失函数相关】--------------------------------------------------------------------------# box (float): 边界框回归损失(如CIoU Loss)在总损失中的权重系数。值越大,模型越重视定位精度。box=7.5,# cls (float): 分类损失(如交叉熵)在总损失中的权重系数。值越大,模型越重视分类准确性。cls=0.5,# dfl (float): 分布式焦点损失(Distribution Focal Loss)的权重系数,用于优化边界框的分布。dfl=1.5,# pose (float): 姿态估计损失的权重系数(用于姿态估计任务)。pose=12.0,# kobj (float): 关键点对象损失的权重系数(用于关键点检测任务)。kobj=1.0,# 【超参数 - 数据增强相关】--------------------------------------------------------------------------# label_smoothing (float): 标签平滑的强度。将硬标签(如[0,1,0])变为软标签(如[0.05,0.9,0.05]),可以提高模型的泛化能力。label_smoothing=0.0,# nbs (int): 名义批量大小(Nominal Batch Size)。用于根据实际batch size动态调整学习率,以模拟在更大batch size下训练的效果。nbs=64,# hsv_h (float): 图像HSV颜色空间中色调(Hue)增强的强度(比例)。hsv_h=0.015,# hsv_s (float): 图像HSV颜色空间中饱和度(Saturation)增强的强度(比例)。hsv_s=0.7,# hsv_v (float): 图像HSV颜色空间中明度(Value)增强的强度(比例)。hsv_v=0.4,# degrees (float): 图像随机旋转的最大角度(+/-)。degrees=0.0,# translate (float): 图像在水平和垂直方向上随机平移的最大比例(+/-)。translate=0.1,# scale (float): 图像随机缩放的比例增益(+/-)。0.5表示尺寸可以在0.5到1.5倍之间变化。scale=0.5,# shear (float): 图像随机剪切的最大角度(+/-)。shear=0.0,# perspective (float): 图像透视变换的最大畸变比例(+/-)。值通常很小(0-0.001)。perspective=0.0,# flipud (float): 图像上下翻转的概率。flipud=0.0,# fliplr (float): 图像左右翻转的概率。0.5表示50%的概率进行翻转。fliplr=0.5,# mosaic (float): 马赛克(Mosaic)数据增强的概率。1.0表示100%的概率使用,这是YOLO训练的关键增强技术。mosaic=1.0,# mixup (float): MixUp数据增强的概率。将两张图像和标签按一定比例混合。mixup=0.0,# copy_paste (float): 复制粘贴(Copy-Paste)数据增强的概率。仅用于实例分割任务,将一个物体的掩码区域复制到另一张图像上。copy_paste=0.0,)# 训练结束
2. 基于PyTorch torchvision的迁移学习图像分类体系
使用 Jupyter Notebook
交互式编程,从 torchvision.models
中获取一个预训练模型,并对其进行定制,使其适用于并希望改进的 FoodVision Mini 问题。
主题 | 内容 |
---|---|
0. 设置环境 | 编写了相当多有用的代码,下载它并确保可以再次使用它。 |
1. 获取数据 | 获取一直在使用的比萨饼、牛排和寿司图像分类数据集,以尝试改进模型的结果。 |
2. 创建数据集和数据加载器 | 将使用在编写的data_setup.py 脚本来设置的数据加载器。 |
3. 获取并定制预训练模型 | 在这里,将从torchvision.models 中下载一个预训练模型,并对其进行定制以适应自己的问题。 |
4. 训练模型 | 看看新的预训练模型在比萨饼、牛排、寿司数据集上的表现如何。将使用在上一章中创建的训练函数。 |
5. 通过绘制损失曲线评估模型 | 第一个迁移学习模型表现如何?它是否出现了过拟合或欠拟合? |
6. 对测试集中的图像进行预测 | 查看模型的评估指标是一回事,但在测试样本上查看其预测是另一回事,让可视化、可视化、可视化! |
2.1 初始设置
# For this notebook to run with updated APIs, we need torch 1.12+ and torchvision 0.13+
try:import torchimport torchvisionassert int(torch.__version__.split(".")[1]) >= 12, "torch version should be 1.12+"assert int(torchvision.__version__.split(".")[1]) >= 13, "torchvision version should be 0.13+"print(f"torch version: {torch.__version__}")print(f"torchvision version: {torchvision.__version__}")
except:print(f"[INFO] torch/torchvision versions not as required, installing nightly versions.")!pip3 install -U torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113import torchimport torchvisionprint(f"torch version: {torch.__version__}")print(f"torchvision version: {torchvision.__version__}")
torch version: 1.13.0.dev20220620+cu113
torchvision version: 0.14.0.dev20220620+cu113
# Continue with regular imports
import matplotlib.pyplot as plt
import torch
import torchvisionfrom torch import nn
from torchvision import transforms# Try to get torchinfo, install it if it doesn't work
try:from torchinfo import summary
except:print("[INFO] Couldn't find torchinfo... installing it.")!pip install -q torchinfofrom torchinfo import summary# Try to import the going_modular directory, download it from GitHub if it doesn't work
try:from going_modular.going_modular import data_setup, engine
except:# Get the going_modular scriptsprint("[INFO] Couldn't find going_modular scripts... downloading them from GitHub.")!git clone https://github.com/mrdbourke/pytorch-deep-learning!mv pytorch-deep-learning/going_modular .!rm -rf pytorch-deep-learningfrom going_modular.going_modular import data_setup, engine
导入 GPU:
# Setup device agnostic code
device = "cuda" if torch.cuda.is_available() else "cpu"
device
2.2 获取数据
在运用迁移学习之前,需要准备一个数据集。为了对比迁移学习与传统建模方法的差异,将继续使用之前采用的 FoodVision Mini 数据集。
import os
import zipfilefrom pathlib import Pathimport requests# Setup path to data folder
data_path = Path("data/")
image_path = data_path / "pizza_steak_sushi"# If the image folder doesn't exist, download it and prepare it...
if image_path.is_dir():print(f"{image_path} directory exists.")
else:print(f"Did not find {image_path} directory, creating one...")image_path.mkdir(parents=True, exist_ok=True)# Download pizza, steak, sushi datawith open(data_path / "pizza_steak_sushi.zip", "wb") as f:request = requests.get("https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip")print("Downloading pizza, steak, sushi data...")f.write(request.content)# Unzip pizza, steak, sushi datawith zipfile.ZipFile(data_path / "pizza_steak_sushi.zip", "r") as zip_ref:print("Unzipping pizza, steak, sushi data...") zip_ref.extractall(image_path)# Remove .zip fileos.remove(data_path / "pizza_steak_sushi.zip")
data/pizza_steak_sushi directory exists
现在,已获取与之前相同的数据集,包含标准图像分类格式的披萨、牛排和寿司图片。接下来,将创建训练集和测试集的目录路径。
# Setup Dirs
train_dir = image_path / "train"
test_dir = image_path / "test"
2.3 创建数据集和数据加载器(Create Datasets and DataLoaders)
由于已下载 going_modular
目录,直接使用 data_setup.py
脚本来配置 DataLoaders
。
不过,考虑到要使用 torchvision.models
中的预训练模型,需要先为图像数据准备特定的转换操作。
2.3.1 为 torchvision.models 创建 transform
使用预训练模型时,重要的是将输入模型的自定义数据准备方式与输入模型的原始训练数据相同。
在 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] 的小批量,其中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, 240, 240]
。有关特定输入图像尺寸,请参阅文档。
问题: 这些平均值和标准差值是从哪里来的?为什么需要这样做?这些值是从数据中计算得出的。具体来说,是从图像数据集(如ImageNet数据集)中取得的一部分图像的平均值和标准差。也不是需要这样做。神经网络通常能够自行计算适当的数据分布(它们将自行计算平均值和标准差的位置),但在开始时设置它们可以帮助网络更快地实现更好的性能。
组合一系列 torchvision.transforms 来执行上述步骤
# Create a transforms pipeline manually (required for torchvision < 0.13)
manual_transforms = transforms.Compose([transforms.Resize((224, 224)), # 1. Reshape all images to 224x224 (though some models may require different sizes)transforms.ToTensor(), # 2. Turn image values to between 0 & 1 transforms.Normalize(mean=[0.485, 0.456, 0.406], # 3. A mean of [0.485, 0.456, 0.406] (across each colour channel)std=[0.229, 0.224, 0.225]) # 4. A standard deviation of [0.229, 0.224, 0.225] (across each colour channel),
])
现在已经准备好了一系列手动创建的转换来准备图像,创建训练和测试 DataLoaders
。
设置 batch_size=32
,这样我们的模型就可以一次看到 32 个样本的小批量。
可以使用上面创建的转换流水线来转换图像,方法是设置 transform=manual_transforms
。
# Create training and testing DataLoaders as well as get a list of class names
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(train_dir=train_dir,test_dir=test_dir,transform=manual_transforms, # resize, convert images to between 0 & 1 and normalize thembatch_size=32) # set mini-batch size to 32train_dataloader, test_dataloader, class_names
(<torch.utils.data.dataloader.DataLoader at 0x7fa9429a3a60>,
<torch.utils.data.dataloader.DataLoader at 0x7fa9429a37c0>,
[‘pizza’, ‘steak’, ‘sushi’])
2.3.2 为 torchvision.models
创建 transform(auto creation)
正如之前所述,当使用预训练模型时,重要的是您输入模型的自定义数据要与训练模型时使用的原始训练数据以相同的方式准备。
上面看到了如何为预训练模型手动创建转换。
但是从 torchvision
v0.13+ 开始,添加了一个自动转换创建功能。
当您从 torchvision.models
中设置一个模型并选择要使用的预训练模型权重时,例如,想使用:
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
其中,
EfficientNet_B0_Weights
是想使用的模型架构权重(torchvision.models
中有许多不同的模型架构选项)。DEFAULT
表示最佳可用权重(在 ImageNet 中性能最好)。- 注意: 根据您选择的模型架构,您可能还会看到其他选项,如
IMAGENET_V1
和IMAGENET_V2
,通常版本号越高,性能越好。虽然如果您想要最佳可用的版本,DEFAULT
是最简单的选项。有关更多信息,请参阅torchvision.models
文档。
- 注意: 根据您选择的模型架构,您可能还会看到其他选项,如
# Get a set of pretrained model weights
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = best available weights from pretraining on ImageNet
weights
EfficientNet_B0_Weights.IMAGENET1K_V1
现在,要访问与 weights
关联的转换,可以使用 transforms()
方法。
这实质上是在说“获取用于在 ImageNet 上训练 EfficientNet_B0_Weights
的数据转换”。
# Get the transforms used to create our pretrained weights
auto_transforms = weights.transforms()
auto_transforms
ImageClassification(crop_size=[224]resize_size=[256]mean=[0.485, 0.456, 0.406]std=[0.229, 0.224, 0.225]interpolation=InterpolationMode.BICUBIC
)
请注意,auto_transforms
与 manual_transforms
非常相似,唯一的区别在于 auto_transforms
是随选择的模型架构一起提供的,而 manual_transforms
则是手动创建的。
通过 weights.transforms()
自动创建转换的好处是,可以确保在模型训练时使用了与预训练模型相同的数据转换。
然而,使用自动生成的转换的代价是缺乏定制性。
可以像以前一样使用 auto_transforms
来使用 create_dataloaders()
创建 DataLoaders。
# Create training and testing DataLoaders as well as get a list of class names
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(train_dir=train_dir,test_dir=test_dir,transform=auto_transforms, # perform same data transforms on our own data as the pretrained modelbatch_size=32) # set mini-batch size to 32train_dataloader, test_dataloader, class_names
(<torch.utils.data.dataloader.DataLoader at 0x7fa942951460>,<torch.utils.data.dataloader.DataLoader at 0x7fa942951550>,['pizza', 'steak', 'sushi'])
2.4 获取预训练模型
好了,现在到了有趣的部分!过去一直在从头开始构建 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’s) | torchvision.models.vit_b_16() 、torchvision.models.vit_b_32() … |
ConvNeXt | torchvision.models.convnext_tiny() 、torchvision.models.convnext_small() … |
在 torchvision.models 中还有更多可用的模型 | torchvision.models... |
2.4.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
,也不要过分依赖任何一个架构,因为随着新的研究发布,它们总是在变化。最好进行实验,看看哪种对你的问题有效。
2.4.2 设置预训练模型
将要使用的预训练模型是 torchvision.models.efficientnet_b0()
。
该架构来自论文 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks。
要创建的示例,即来自 torchvision.models
的预训练 EfficientNet_B0
模型,其输出层已调整为用例,用于分类比萨、牛排和寿司图像。
可以使用与创建转换相同的代码来设置EfficientNet_B0
的预训练的ImageNet
权重。
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = ImageNet的最佳可用权重
这意味着该模型已经在数百万张图像上进行了训练,并且对图像数据有一个良好的基本表示。
此预训练模型的PyTorch版本能够在ImageNet的1000个类别中实现约77.7%的准确率。
还将把它发送到目标设备。
# OLD: Setup the model with pretrained weights and send it to the target device (this was prior to torchvision v0.13)
# model = torchvision.models.efficientnet_b0(pretrained=True).to(device) # OLD method (with pretrained=True)# NEW: Setup the model with pretrained weights and send it to the target device (torchvision v0.13+)
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT # .DEFAULT = best available weights
model = torchvision.models.efficientnet_b0(weights=weights).to(device)#model # uncomment to output (it's very long)
如果打印模型,会得到类似以下的输出:
大量大量的层,这是迁移学习的好处之一,将一个已经由一些世界上最优秀的工程师精心设计的现有模型应用到您自己的问题上。
efficientnet_b0
分为三个主要部分:
features
- 一系列卷积层和其他各种激活层,用于学习视觉数据的基本表示(这个基本表示/层的集合通常被称为特征或特征提取器,“模型的基本层学习图像的不同特征”)。avgpool
- 取features
层的输出的平均值,并将其转换为特征向量。classifier
- 将特征向量转换为与所需输出类别数量相同维度的向量(由于efficientnet_b0
预训练于 ImageNet 数据集,而 ImageNet 有 1000 个类别,所以out_features=1000
是默认值)。
2.4.3 用 torchinfo.summary()
打印模型
要了解更多关于模型的信息,使用 torchinfo
的 summary()
方法。
为此,将传入:
model
- 想要获取摘要的模型。input_size
- 想要传递给模型的数据的形状,对于efficientnet_b0
,输入大小为(batch_size, 3, 224, 224)
,不过其他变体的efficientnet_bX
有不同的输入大小。- 注意: 许多现代模型可以处理不同大小的输入图像,这得益于
torch.nn.AdaptiveAvgPool2d()
,这个层根据需要自适应地调整给定输入的output_size
。您可以通过将不同大小的输入图像传递给summary()
或您的模型来尝试此功能。
- 注意: 许多现代模型可以处理不同大小的输入图像,这得益于
col_names
- 想要查看关于模型的各种信息列。col_width
- 摘要中列的宽度应为多少。row_settings
- 在一行中显示哪些特性。
# Print a summary using torchinfo (uncomment for actual output)
summary(model=model, input_size=(32, 3, 224, 224), # make sure this is "input_size", not "input_shape"# col_names=["input_size"], # uncomment for smaller outputcol_names=["input_size", "output_size", "num_params", "trainable"],col_width=20,row_settings=["var_names"]
)
============================================================================================================================================
Layer (type (var_name)) Input Shape Output Shape Param # Trainable
============================================================================================================================================
EfficientNet (EfficientNet) [32, 3, 224, 224] [32, 1000] -- True
├─Sequential (features) [32, 3, 224, 224] [32, 1280, 7, 7] -- True
│ └─Conv2dNormActivation (0) [32, 3, 224, 224] [32, 32, 112, 112] -- True
│ │ └─Conv2d (0) [32, 3, 224, 224] [32, 32, 112, 112] 864 True
│ │ └─BatchNorm2d (1) [32, 32, 112, 112] [32, 32, 112, 112] 64 True
│ │ └─SiLU (2) [32, 32, 112, 112] [32, 32, 112, 112] -- --
│ └─Sequential (1) [32, 32, 112, 112] [32, 16, 112, 112] -- True
│ │ └─MBConv (0) [32, 32, 112, 112] [32, 16, 112, 112] 1,448 True
│ └─Sequential (2) [32, 16, 112, 112] [32, 24, 56, 56] -- True
│ │ └─MBConv (0) [32, 16, 112, 112] [32, 24, 56, 56] 6,004 True
│ │ └─MBConv (1) [32, 24, 56, 56] [32, 24, 56, 56] 10,710 True
│ └─Sequential (3) [32, 24, 56, 56] [32, 40, 28, 28] -- True
│ │ └─MBConv (0) [32, 24, 56, 56] [32, 40, 28, 28] 15,350 True
│ │ └─MBConv (1) [32, 40, 28, 28] [32, 40, 28, 28] 31,290 True
│ └─Sequential (4) [32, 40, 28, 28] [32, 80, 14, 14] -- True
│ │ └─MBConv (0) [32, 40, 28, 28] [32, 80, 14, 14] 37,130 True
│ │ └─MBConv (1) [32, 80, 14, 14] [32, 80, 14, 14] 102,900 True
│ │ └─MBConv (2) [32, 80, 14, 14] [32, 80, 14, 14] 102,900 True
│ └─Sequential (5) [32, 80, 14, 14] [32, 112, 14, 14] -- True
│ │ └─MBConv (0) [32, 80, 14, 14] [32, 112, 14, 14] 126,004 True
│ │ └─MBConv (1) [32, 112, 14, 14] [32, 112, 14, 14] 208,572 True
│ │ └─MBConv (2) [32, 112, 14, 14] [32, 112, 14, 14] 208,572 True
│ └─Sequential (6) [32, 112, 14, 14] [32, 192, 7, 7] -- True
│ │ └─MBConv (0) [32, 112, 14, 14] [32, 192, 7, 7] 262,492 True
│ │ └─MBConv (1) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True
│ │ └─MBConv (2) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True
│ │ └─MBConv (3) [32, 192, 7, 7] [32, 192, 7, 7] 587,952 True
│ └─Sequential (7) [32, 192, 7, 7] [32, 320, 7, 7] -- True
│ │ └─MBConv (0) [32, 192, 7, 7] [32, 320, 7, 7] 717,232 True
│ └─Conv2dNormActivation (8) [32, 320, 7, 7] [32, 1280, 7, 7] -- True
│ │ └─Conv2d (0) [32, 320, 7, 7] [32, 1280, 7, 7] 409,600 True
│ │ └─BatchNorm2d (1) [32, 1280, 7, 7] [32, 1280, 7, 7] 2,560 True
│ │ └─SiLU (2) [32, 1280, 7, 7] [32, 1280, 7, 7] -- --
├─AdaptiveAvgPool2d (avgpool) [32, 1280, 7, 7] [32, 1280, 1, 1] -- --
├─Sequential (classifier) [32, 1280] [32, 1000] -- True
│ └─Dropout (0) [32, 1280] [32, 1280] -- --
│ └─Linear (1) [32, 1280] [32, 1000] 1,281,000 True
============================================================================================================================================
Total params: 5,288,548
Trainable params: 5,288,548
Non-trainable params: 0
Total mult-adds (G): 12.35
============================================================================================================================================
Input size (MB): 19.27
Forward/backward pass size (MB): 3452.35
Params size (MB): 21.15
Estimated Total Size (MB): 3492.77
============================================================================================================================================
可以看到当图像数据经过模型时,各种输入和输出形状的变化。而且,有更多的总参数(预训练权重)来识别我们数据中的不同模式。
作为参考,之前的模型,TinyVGG 有 8,083 个参数,而 efficientnet_b0
则有 5,288,548 个参数,增加了大约 654 倍!
2.4.4 冻结基础模型并更改输出层以满足需求
迁移学习的过程通常是这样的:冻结预训练模型的一些基础层(通常是 features
部分),然后调整输出层(也称为头/分类器层)以适应你的需求。
你可以通过修改输出层来定制预训练模型的输出,以适应你的问题。原始的 torchvision.models.efficientnet_b0()
的输出特征数量是 out_features=1000
,因为它是在 ImageNet 数据集上训练的,而 ImageNet 数据集有 1000 个类别。然而,分类比萨、牛排和寿司的图像,我们只需要 out_features=3
。
冻结 efficientnet_b0
模型中 features
部分的所有层/参数。
注意: 冻结 层意味着在训练期间保持它们的状态不变。例如,如果你的模型有预训练层,那么 冻结 它们就是说,“在训练期间不要改变这些层中的任何模式,保持它们的状态不变。” 实质上,想要保留模型从 ImageNet 学到的预训练权重/模式作为骨干,然后只更改输出层。
可以通过设置属性 requires_grad=False
来冻结 features
部分中的所有层/参数。
对于具有 requires_grad=False
的参数,PyTorch 不会跟踪梯度更新,因此在训练期间这些参数不会被优化器更改。
实质上,具有 requires_grad=False
的参数是“不可训练的”或“冻结的”。
# Freeze all base layers in the "features" section of the model (the feature extractor) by setting requires_grad=False
for param in model.features.parameters():param.requires_grad = False
特征提取层已冻结!现在根据需求调整预训练模型的输出层或 classifier
部分。目前的预训练模型的 out_features=1000
,因为 ImageNet 有 1000 个类别。
然而,不需要 1000 个类别,只有三个,比萨、牛排和寿司。可以通过创建一系列新的层来改变模型的 classifier
部分。
当前的 classifier
包括:
(classifier): Sequential((0): Dropout(p=0.2, inplace=True)(1): Linear(in_features=1280, out_features=1000, bias=True)
将保持 Dropout
层不变,使用 torch.nn.Dropout(p=0.2, inplace=True)
。
注意: Dropout 层 随机删除两个神经网络层之间的连接,概率为
p
。例如,如果p=0.2
,每次随机删除神经网络层之间 20% 的连接。这种做法旨在通过确保保留的连接学习补偿其他连接的删除(希望这些保留的特征更加通用)来帮助正则化(防止过拟合)模型。
将保持 Linear
输出层的 in_features=1280
,但是将更改 out_features
的值为 class_names
的长度(len(['pizza', 'steak', 'sushi']) = 3
)。
新 classifier
层应该与 model
在同一个设备上。
# Set the manual seeds
torch.manual_seed(42)
torch.cuda.manual_seed(42)# Get the length of class_names (one output unit for each class)
output_shape = len(class_names)# Recreate the classifier layer and seed it to the target device
model.classifier = torch.nn.Sequential(torch.nn.Dropout(p=0.2, inplace=True), torch.nn.Linear(in_features=1280, out_features=output_shape, # same number of output units as our number of classesbias=True)).to(device)
输出层已更新,再次对模型进行,并查看发生了什么变化。
# # Do a summary *after* freezing the features and changing the output classifier layer (uncomment for actual output)
summary(model, input_size=(32, 3, 224, 224), # make sure this is "input_size", not "input_shape" (batch_size, color_channels, height, width)verbose=0,col_names=["input_size", "output_size", "num_params", "trainable"],col_width=20,row_settings=["var_names"]
)
============================================================================================================================================
Layer (type (var_name)) Input Shape Output Shape Param # Trainable
============================================================================================================================================
EfficientNet (EfficientNet) [32, 3, 224, 224] [32, 3] -- Partial
├─Sequential (features) [32, 3, 224, 224] [32, 1280, 7, 7] -- False
│ └─Conv2dNormActivation (0) [32, 3, 224, 224] [32, 32, 112, 112] -- False
│ │ └─Conv2d (0) [32, 3, 224, 224] [32, 32, 112, 112] (864) False
│ │ └─BatchNorm2d (1) [32, 32, 112, 112] [32, 32, 112, 112] (64) False
│ │ └─SiLU (2) [32, 32, 112, 112] [32, 32, 112, 112] -- --
│ └─Sequential (1) [32, 32, 112, 112] [32, 16, 112, 112] -- False
│ │ └─MBConv (0) [32, 32, 112, 112] [32, 16, 112, 112] (1,448) False
│ └─Sequential (2) [32, 16, 112, 112] [32, 24, 56, 56] -- False
│ │ └─MBConv (0) [32, 16, 112, 112] [32, 24, 56, 56] (6,004) False
│ │ └─MBConv (1) [32, 24, 56, 56] [32, 24, 56, 56] (10,710) False
│ └─Sequential (3) [32, 24, 56, 56] [32, 40, 28, 28] -- False
│ │ └─MBConv (0) [32, 24, 56, 56] [32, 40, 28, 28] (15,350) False
│ │ └─MBConv (1) [32, 40, 28, 28] [32, 40, 28, 28] (31,290) False
│ └─Sequential (4) [32, 40, 28, 28] [32, 80, 14, 14] -- False
│ │ └─MBConv (0) [32, 40, 28, 28] [32, 80, 14, 14] (37,130) False
│ │ └─MBConv (1) [32, 80, 14, 14] [32, 80, 14, 14] (102,900) False
│ │ └─MBConv (2) [32, 80, 14, 14] [32, 80, 14, 14] (102,900) False
│ └─Sequential (5) [32, 80, 14, 14] [32, 112, 14, 14] -- False
│ │ └─MBConv (0) [32, 80, 14, 14] [32, 112, 14, 14] (126,004) False
│ │ └─MBConv (1) [32, 112, 14, 14] [32, 112, 14, 14] (208,572) False
│ │ └─MBConv (2) [32, 112, 14, 14] [32, 112, 14, 14] (208,572) False
│ └─Sequential (6) [32, 112, 14, 14] [32, 192, 7, 7] -- False
│ │ └─MBConv (0) [32, 112, 14, 14] [32, 192, 7, 7] (262,492) False
│ │ └─MBConv (1) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False
│ │ └─MBConv (2) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False
│ │ └─MBConv (3) [32, 192, 7, 7] [32, 192, 7, 7] (587,952) False
│ └─Sequential (7) [32, 192, 7, 7] [32, 320, 7, 7] -- False
│ │ └─MBConv (0) [32, 192, 7, 7] [32, 320, 7, 7] (717,232) False
│ └─Conv2dNormActivation (8) [32, 320, 7, 7] [32, 1280, 7, 7] -- False
│ │ └─Conv2d (0) [32, 320, 7, 7] [32, 1280, 7, 7] (409,600) False
│ │ └─BatchNorm2d (1) [32, 1280, 7, 7] [32, 1280, 7, 7] (2,560) False
│ │ └─SiLU (2) [32, 1280, 7, 7] [32, 1280, 7, 7] -- --
├─AdaptiveAvgPool2d (avgpool) [32, 1280, 7, 7] [32, 1280, 1, 1] -- --
├─Sequential (classifier) [32, 1280] [32, 3] -- True
│ └─Dropout (0) [32, 1280] [32, 1280] -- --
│ └─Linear (1) [32, 1280] [32, 3] 3,843 True
============================================================================================================================================
Total params: 4,011,391
Trainable params: 3,843
Non-trainable params: 4,007,548
Total mult-adds (G): 12.31
============================================================================================================================================
Input size (MB): 19.27
Forward/backward pass size (MB): 3452.09
Params size (MB): 16.05
Estimated Total Size (MB): 3487.41
============================================================================================================================================
- 可训练列 - 你会发现许多基本层(
features
部分中的层)的可训练值为False
。这是因为将它们的属性requires_grad=False
。除非改变这一点,否则这些层在将来的训练中将不会被更新。 classifier
的输出形状 - 模型的classifier
部分现在具有输出形状值[32, 3]
,而不是[32, 1000]
。它的可训练值也为True
。这意味着它的参数将在训练过程中被更新。实质上,使用features
部分将图像的基本表示传递给classifier
部分,然后classifier
层将学习如何将基本表示与问题对齐。- 可训练参数减少 - 以前有 5,288,548 个可训练参数。但由于冻结了模型的许多层,并且只留下了
classifier
作为可训练的,现在只有 3,843 个可训练参数(甚至比 TinyVGG 模型还少)。尽管还有 4,007,548 个不可训练参数,但这些参数将为输入图像创建一个基本表示,以供classifier
层使用。
注意: 模型具有的可训练参数越多,需要的计算资源/训练时间就越多。冻结模型的基本层,并使其具有较少的可训练参数意味着模型应该会非常快地训练。这是迁移学习的一个巨大好处,它采用了已经在类似问题上训练过的模型的学习参数,并仅略微调整输出以适应您的问题。
2.5 训练模型
现在有了一个半冻结的预训练模型和一个定制的 classifier
,来验证迁移学习的实际效果。
要开始训练,首先需要创建一个损失函数和一个优化器。
因为仍然在进行多类别分类,所以将使用 nn.CrossEntropyLoss()
作为损失函数。
而且,将继续使用 torch.optim.Adam()
作为优化器,学习率为 0.001
。
# Define loss and optimizer
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
要训练模型,可以使用在 05. PyTorch Going Modular 第 04 节 中定义的 train()
函数。
train()
函数位于 engine.py
脚本中,位于 going_modular
目录 内。
看看将模型训练 5 个 epochs
需要多长时间。
注意: 这里我们只会训练
classifier
的参数,因为我们模型中的所有其他参数都已经被冻结了。
# Set the random seeds
torch.manual_seed(42)
torch.cuda.manual_seed(42)# Start the timer
from timeit import default_timer as timer
start_time = timer()# Setup training and save the results
results = engine.train(model=model,train_dataloader=train_dataloader,test_dataloader=test_dataloader,optimizer=optimizer,loss_fn=loss_fn,epochs=5,device=device)# End the timer and print out how long it took
end_time = timer()
print(f"[INFO] Total training time: {end_time-start_time:.3f} seconds")
0%| | 0/5 [00:00<?, ?it/s]Epoch: 1 | train_loss: 1.0924 | train_acc: 0.3984 | test_loss: 0.9133 | test_acc: 0.5398
Epoch: 2 | train_loss: 0.8717 | train_acc: 0.7773 | test_loss: 0.7912 | test_acc: 0.8153
Epoch: 3 | train_loss: 0.7648 | train_acc: 0.7930 | test_loss: 0.7463 | test_acc: 0.8561
Epoch: 4 | train_loss: 0.7108 | train_acc: 0.7539 | test_loss: 0.6372 | test_acc: 0.8655
Epoch: 5 | train_loss: 0.6254 | train_acc: 0.7852 | test_loss: 0.6260 | test_acc: 0.8561
[INFO] Total training time: 8.977 seconds
借助 efficientnet_b0
的骨干结构,模型在测试数据集上达到了近 85% 以上的准确率,几乎是在 TinyVGG 上所能达到的准确率的 两倍。对于只用几行代码下载的模型来说,这个表现还不错。
2.6 通过绘制损失函数曲线评估模型
绘制其损失曲线,以查看训练随时间的变化情况。
# Get the plot_loss_curves() function from helper_functions.py, download the file if we don't have it
try:from helper_functions import plot_loss_curves
except:print("[INFO] Couldn't find helper_functions.py, downloading...")with open("helper_functions.py", "wb") as f:import requestsrequest = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/helper_functions.py")f.write(request.content)from helper_functions import plot_loss_curves# Plot the loss curves of our model
plot_loss_curves(results)
看起来两个数据集(训练集和测试集)的损失都朝着正确的方向发展。准确率值也是如此,呈上升趋势。这显示了迁移学习的强大之处。使用预训练模型通常会在较短的时间内以较少的数据产生相当不错的结果。
2.7 从测试集做预测
看起来我们的模型在定量上表现良好,但在定性上呢?
为了让模型对图像进行预测,图像必须与模型在训练时看到的图像格式相同需要确保图像具有以下特点:
- 相同的形状 - 如果图像形状与模型训练时的图像形状不同,将收到形状错误。
- 相同的数据类型 - 如果图像是与模型训练时的数据类型不同(例如
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, Tuplefrom PIL import Image# 1. Take in a trained model, class names, image path, image size, a transform and target device
def pred_and_plot_image(model: torch.nn.Module,image_path: str, class_names: List[str],image_size: Tuple[int, int] = (224, 224),transform: torchvision.transforms = None,device: torch.device=device):# 2. Open imageimg = Image.open(image_path)# 3. Create transformation for image (if one doesn't exist)if transform is not None:image_transform = transformelse:image_transform = transforms.Compose([transforms.Resize(image_size),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]),])### Predict on image ### # 4. Make sure the model is on the target devicemodel.to(device)# 5. Turn on model evaluation mode and inference modemodel.eval()with torch.inference_mode():# 6. Transform and add an extra dimension to image (model requires samples in [batch_size, color_channels, height, width])transformed_image = image_transform(img).unsqueeze(dim=0)# 7. Make a prediction on image with an extra dimension and send it to the target devicetarget_image_pred = model(transformed_image.to(device))# 8. Convert logits -> prediction probabilities (using torch.softmax() for multi-class classification)target_image_pred_probs = torch.softmax(target_image_pred, dim=1)# 9. Convert prediction probabilities -> prediction labelstarget_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)# 10. Plot image with predicted label and probability plt.figure()plt.imshow(img)plt.title(f"Pred: {class_names[target_image_pred_label]} | Prob: {target_image_pred_probs.max():.3f}")plt.axis(False);
通过对测试集中几张随机图像进行预测来测试一下它。
可以使用 list(Path(test_dir).glob("*/*.jpg"))
获取所有测试图像路径的列表。
然后,可以使用 Python 的 random.sample(populuation, k)
随机采样其中的几张图像,其中 population
是要采样的序列,k
是要检索的样本数量。
# Get a random list of image paths from test set
import random
num_images_to_plot = 3
test_image_path_list = list(Path(test_dir).glob("*/*.jpg")) # get list all image paths from test data
test_image_path_sample = random.sample(population=test_image_path_list, # go through all of the test image pathsk=num_images_to_plot) # randomly select 'k' image paths to pred and plot# Make predictions on and plot the images
for image_path in test_image_path_sample:pred_and_plot_image(model=model, image_path=image_path,class_names=class_names,# transform=weights.transforms(), # optionally pass in a specified transform from our pretrained model weightsimage_size=(224, 224))
2.8 在自定义图像上进行预测
为了测试模型在自定义图像上的表现,导入老朋友 pizza-dad.jpeg
图像(一张我爸爸吃比萨的照片)。然后将它传递给上面创建的 pred_and_plot_image()
函数,
# Download custom image
import requests# Setup custom image path
custom_image_path = data_path / "04-pizza-dad.jpeg"# Download the image if it doesn't already exist
if not custom_image_path.is_file():with open(custom_image_path, "wb") as f:# When downloading from GitHub, need to use the "raw" file linkrequest = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/images/04-pizza-dad.jpeg")print(f"Downloading {custom_image_path}...")f.write(request.content)
else:print(f"{custom_image_path} already exists, skipping download.")# Predict on custom image
pred_and_plot_image(model=model,image_path=custom_image_path,class_names=class_names)
data/04-pizza-dad.jpeg already exists, skipping download.
这次预测的概率比 TinyVGG
中的概率更高(0.373
),在 04. PyTorch 自定义数据集部分 11.3 这表明 efficientnet_b0
模型对其预测更有信心,而 TinyVGG 模型与随机猜测不相上下。
本文系统阐述了现代深度学习的两大核心实践:一是基于Ultralytics YOLO构建工程化的目标检测流水线,通过
demo.py
实现数据智能匹配与可复现划分,利用data.yaml
进行配置管理,并在train.py
中整合YOLOv11、SGD优化器、混合精度和多种数据增强策略,打造了通用、可扩展的训练基础设施。二是基于PyTorch torchvision实践迁移学习,以预训练的EfficientNet-B0为骨干,通过冻结特征提取器并重构分类头,仅用5个epoch即在FoodVision Mini数据集上取得超85%的准确率,彰显了其高效性。