PyTorch深度学习笔记3:VGG-16模型微调
一、模型微调概述
1. 什么是模型微调
模型微调(Fine-tuning)是迁移学习的一种具体技术,指在预训练模型的基础上,使用目标领域的数据对模型进行进一步训练,使其适应特定任务的过程。
通过模型微调,可以在有限的数据和计算资源下,获得接近甚至超过从头训练的性能,这是现代深度学习实践中不可或缺的技术。
2. 微调的核心原理
# 微调的基本流程示意
预训练模型 → 修改输出层 → 冻结部分层 → 小学习率训练 → 评估调整
3. 微调的三种主要策略
|
策略类型 |
训练层数 |
适用场景 |
优点 |
缺点 |
|---|---|---|---|---|
|
特征提取 |
仅新层 |
小数据集 |
训练快、防过拟合 |
性能有限 |
|
部分微调 |
后几层+新层 |
中等数据集 |
平衡速度与性能 |
需要调参 |
|
完全微调 |
所有层 |
大数据集 |
最佳性能 |
易过拟合、耗时 |
4. 微调的成功因素分析
"数据相似度": "源域与目标域的相似程度",
"数据量": "目标领域数据数量",
"模型容量": "预训练模型的表达能力",
"微调策略": "冻结层数、学习率等",
"正则化": "Dropout、权重衰减等",
"数据增强": "增强策略的合理性"
二、模型微调与模型迁移的关系
1. 包含关系
模型迁移是方法论,模型微调是具体技术,微调是迁移学习中最常用、最有效的方法之一。
迁移学习 (Transfer Learning)
├── 特征提取 (Feature Extraction)
├── 模型微调 (Fine-tuning) ← 代码中使用的方法
├── 领域自适应 (Domain Adaptation)
└── 多任务学习 (Multi-task Learning)
2. 技术演进关系
传统机器学习 → 深度学习 → 预训练模型 → 迁移学习 → 模型微调
↓ ↓ ↓ ↓ ↓
手工特征工程 端到端学习 通用特征学习 知识迁移 特定任务优化
3.策略区别
两者采用了不同的参数训练策略,以下是详细的对比分析:


|
对比维度 |
笔记2 (模型迁移/特征提取) |
笔记3 (模型微调) |
|---|---|---|
|
核心策略 |
特征提取 (Feature Extraction) 在训练过程中,保持预训练模型(包括它的特征提取器)的参数固定不变,仅训练新添加的分类层。 |
精细化微调 (Fine-tuning) 在训练过程中,不仅训练为适应新任务而新增的分类层,同时解冻预训练模型的部分或全部原有层(包括其“特征提取器”),并以较小的学习率更新这些解冻层的权重,使整个模型(从特征提取到分类决策)的参数都根据新数据集进行专业化调整。 |
|
参数更新范围 |
极窄。仅训练新添加的输出层。 |
很广。训练特征提取器、分类器前层、输出层。 |
|
具体参数 |
只更新 |
分为三组更新: |
|
|
除了输出层,其余所有参数 |
只有少数与当前任务无关的层(如分类器中的ReLU和Dropout)被冻结,大部分关键层的参数 |
|
学习率策略 |
单一学习率 (0.001)。 |
分层学习率: |
|
代码位置 |
保守的“特征提取”,它几乎不修改预训练模型的原始参数,只训练一个全新的、轻量级的输出头。训练速度快,所需数据少,适合作为基线或数据量极少的场景。 | 积极的“模型微调”,它修改了大量预训练模型的参数(特征层和分类器前层),并结合分层学习率进行精细优化。这种方法通常能获得更高的最终精度,但需要更多的数据以防过拟合,训练时间也更长,调参更复杂。 |
本文采用的是一种更高级、更灵活的迁移学习策略,其动机和优势在于:
-
适应数据差异:当目标数据集(蚂蚁/蜜蜂)与源数据集(ImageNet)存在一定分布差异时,仅调整最后几层可能不够。适度调整特征提取器中的深层卷积层,可以让模型学习到更适合新任务的“细节特征”。
-
释放模型容量:VGG-16的预训练权重包含了丰富的通用视觉特征。通过微调(而不仅是冻结)这些特征,可以更好地利用预训练模型的表征能力,使其在特定任务上达到更高的性能上限。
-
分层优化:不同层的参数在训练过程中的“成熟度”和敏感性不同。对新添加的输出层使用较大的学习率,可以使其快速适应新任务;对预训练的特征层使用较小的学习率,可以对其进行“精雕细琢”,避免破坏已学到的有用特征。这种策略被称为差分学习率或分层学习率,是微调的最佳实践之一。
三、程序
1.Python实现
# 导入必要的Python库
import glob
import os.path as osp
import random
import numpy as np
import json
from PIL import Image
from tqdm import tqdm
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision
from torchvision import models, transforms
# 导入VGG16权重枚举
from torchvision.models import vgg16, VGG16_Weights
# 设置随机数种子以确保结果可重现
# 在机器学习中,固定随机种子可以使每次运行得到相同的结果
torch.manual_seed(1234) # 设置PyTorch的随机种子
np.random.seed(1234) # 设置NumPy的随机种子
random.seed(1234) # 设置Python内置random模块的随机种子
# 定义预处理参数
size = 224 # VGG-16的标准输入尺寸
mean = (0.485, 0.456, 0.406) # ImageNet数据集的RGB均值
std = (0.229, 0.224, 0.225) # ImageNet数据集的RGB标准差
# 输入图像的预处理类
# 训练时和验证/测试时采用不同的处理方式
class ImageTransform():
"""
图像的预处理类。训练时和验证/测试时采用不同的处理方式
对图像的大小进行调整,并将颜色信息标准化
训练时采用RandomResizedCrop和RandomHorizontalFlip进行数据增强处理
Attributes
----------
resize : int
指定调整后图像的尺寸
mean : (R, G, B)
各个颜色通道的平均值
std : (R, G, B)
各个颜色通道的标准偏差
"""
def __init__(self, resize, mean, std):
# 定义训练和验证时的不同预处理流程
self.data_transform = {
'train': transforms.Compose([
# 1. 随机缩放裁剪:增强模型对不同尺度和位置的适应性
transforms.RandomResizedCrop(
resize, scale=(0.5, 1.0)), # 数据增强处理
# 2. 随机水平翻转:增加数据多样性,防止过拟合
transforms.RandomHorizontalFlip(), # 数据增强处理
# 3. 转换为张量:将PIL图像或numpy数组转换为torch.Tensor
transforms.ToTensor(),
# 4. 归一化:用均值和标准差对每个通道进行归一化
transforms.Normalize(mean, std)
]),
'val': transforms.Compose([
# 1. 调整大小:将图像缩放到指定大小
transforms.Resize(resize), # 调整大小
# 2. 中心裁剪:从图像中央截取resize×resize大小的区域
transforms.CenterCrop(resize),
# 3. 转换为张量
transforms.ToTensor(),
# 4. 归一化
transforms.Normalize(mean, std)
])
}
def __call__(self, img, phase='train'):
"""
使类的实例可以像函数一样调用
Parameters
----------
phase : 'train' or 'val'
指定预处理所使用的模式
Returns
-------
预处理后的图像张量
"""
return self.data_transform[phase](img)
class HymenopteraDataset(data.Dataset):
"""
蚂蚁和蜜蜂图片的Dataset类,继承自PyTorch的Dataset类
这是一个自定义数据集类,用于加载和预处理蚂蚁和蜜蜂的图片
Attributes
----------
file_list : 列表
列表中保存了图片路径
transform : object
预处理类的实例
phase : 'train' or 'val'
指定是训练还是验证
"""
def __init__(self, file_list, transform=None, phase='train'):
self.file_list = file_list # 文件路径列表
self.transform = transform # 预处理类的实例
self.phase = phase # 指定是train还是val
def __len__(self):
'''返回数据集中的图片数量'''
return len(self.file_list)
def __getitem__(self, index):
'''
获取预处理完毕的图片的张量数据和标签
Parameters
----------
index : int
要获取的数据的索引
Returns
-------
img_transformed : torch.Tensor
预处理后的图片张量
label : int
图片的标签(0:蚂蚁, 1:蜜蜂)
'''
# 加载第index张图片
img_path = self.file_list[index]
img = Image.open(img_path) # 打开图片,格式为[高度][宽度][颜色RGB]
# 对图片进行预处理
img_transformed = self.transform(img, self.phase) # 输出形状: torch.Size([3, 224, 224])
# 从文件名中提取图片的标签
# 注意:这里的切片位置依赖于特定的文件路径结构
# 假设文件路径格式为: "./data/hymenoptera_data/train/ants/xxx.jpg"
if self.phase == "train":
# 提取"ants"或"bees"
label = img_path[30:34] # 从第30个字符开始,取4个字符
elif self.phase == "val":
label = img_path[28:32] # 从第28个字符开始,取4个字符
# 将标签转换为数字
if label == "ants":
label = 0
elif label == "bees":
label = 1
return img_transformed, label
def make_datapath_list(phase="train"):
"""
创建用于保存数据路径的列表
Parameters
----------
phase : 'train' or 'val'
指定是训练数据还是验证数据
Returns
-------
path_list : list
保存了数据路径的列表
"""
# 数据根目录
rootpath = "./data/hymenoptera_data/"
# 构建搜索路径模式
target_path = osp.join(rootpath + phase + '/**/*.jpg')
print(f"搜索路径: {target_path}")
path_list = [] # 用于保存找到的文件路径
# 使用glob模块查找所有匹配的.jpg文件
for path in glob.glob(target_path):
path_list.append(path)
return path_list
##############################################################################
# 上面和迁移学习采用相同的处理方式
##############################################################################
def train_model_gpu(net, dataloaders_dict, criterion, optimizer, num_epochs):
"""
训练和验证模型的函数(GPU版本)
Parameters
----------
net : nn.Module
要训练的神经网络模型
dataloaders_dict : dict
包含'train'和'val'两个DataLoader的字典
criterion : nn.Module
损失函数
optimizer : optim.Optimizer
优化器
num_epochs : int
训练的轮数
"""
#初始化设置
#确认GPU是否能使用
print(f"PyTorch 版本:{torch.__version__}")
print(f"CUDA 可用:{torch.cuda.is_available()}")
print(f"PyTorch对应CUDA版本:{torch.version.cuda}")
import subprocess, re
nvcc_version=subprocess.check_output("nvcc --version 2>&1 | grep release | sed 's/.*release //; s/,.*//'", shell=True).decode().strip() or "未安装CUDA"
print(f"实际CUDA版本:{nvcc_version}")
if torch.cuda.is_available():
print(f"GPU 名称:{torch.cuda.get_device_name(0)}")
print(f"GPU 计算能力:{torch.cuda.get_device_capability(0)}")
print(f"CUDA设备数量: {torch.cuda.device_count()}")
# 选择设备:优先使用GPU,如果没有则使用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("使用设备:", device)
#将网络输入GPU
net.to(device)
#如果网络达到较稳定的程度,则开启加速
# benchmark模式会让cudnn在运行时自动寻找当前硬件和输入尺寸下最快的算法。
# 首次运行时进行基准测试,找出并缓存最快算法,后续调用该算法。
# 在输入/网络稳定时,能获得显著的性能提升(通常可达10%-30%)。
# 首次运行有开销。在输入/网络不稳定时,性能会严重下降。
torch.backends.cudnn.benchmark = True
# epoch循环
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch+1, num_epochs))
print('-' * 44) # 分隔线
#每个epoch中的训练和验证循环
for phase in ['train', 'val']:
if phase == 'train':
net.train() #将模型设置为训练模式
else:
net.eval() #将模型设置为验证模式
epoch_loss = 0.0 # epoch的损失总和
epoch_corrects = 0 # epoch的正确答案数量
#为了对未学习时的验证性能进行确认,省略epoch=0的训练
if (epoch == 0) and (phase == 'train'):
continue
#从数据加载器中取出小批次数据的循环
for inputs, labels in tqdm(dataloaders_dict[phase]):
#如果GPU可用,则将数据装载到GPU中
inputs = inputs.to(device)
labels = labels.to(device)
#初始化optimizer
optimizer.zero_grad()
#计算正向传播(forward)
with torch.set_grad_enabled(phase == 'train'):
# 模型前向传播
outputs = net(inputs)
#计算损失值
loss = criterion(outputs, labels)
#对标签进行预测,返回最大值和对应的索引
_, preds = torch.max(outputs, 1)
#训练时的反向传播
if phase == 'train':
# 反向传播计算梯度
loss.backward()
# 更新模型参数
optimizer.step()
#结果的计算
# 累加损失(乘以批次大小,因为损失是批次平均损失)
epoch_loss += loss.item() * inputs.size(0)
#更新正确答案的合计数量
epoch_corrects += torch.sum(preds == labels.data)
#显示每轮epoch的loss和准确率
epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
# 主程序开始
if __name__ == "__main__":
# 1. 创建用于保存蚂蚁和蜜蜂图片的文件路径列表
print("##########步骤1: 创建数据路径列表##########")
print("=" * 50)
train_list = make_datapath_list(phase="train")
val_list = make_datapath_list(phase="val")
print(f"训练集大小: {len(train_list)} 张图片")
print(f"验证集大小: {len(val_list)} 张图片")
print("=" * 50)
# 2. 创建由蚂蚁和蜜蜂图片组成的Dataset
print("\n##########步骤2: 创建Dataset##########")
print("=" * 50)
# 创建训练数据集
train_dataset = HymenopteraDataset(
file_list=train_list,
transform=ImageTransform(size, mean, std),
phase='train'
)
# 创建验证数据集
val_dataset = HymenopteraDataset(
file_list=val_list,
transform=ImageTransform(size, mean, std),
phase='val'
)
print(f"训练数据集大小: {len(train_dataset)}")
print(f"验证数据集大小: {len(val_dataset)}")
print("=" * 50)
# 3. 遍历打印整个数据集(调试用)
print("\n##########步骤3: 遍历数据集并显示统计信息##########")
print("=" * 50)
print("训练数据集遍历")
print("=" * 50)
ants_count = 0 # 蚂蚁计数
bees_count = 0 # 蜜蜂计数
for index in range(len(train_dataset)):
# 获取样本
# img_tensor: 预处理后的图片张量
# label: 图片标签 (0:蚂蚁, 1:蜜蜂)
img_tensor, label = train_dataset.__getitem__(index)
print(f'索引 {index}: 图像形状 {img_tensor.size()}, 标签 {label}')
# 统计类别
if label == 0:
ants_count += 1
else:
bees_count += 1
print("=" * 50)
print(f'总计:{len(train_dataset)} 张图片')
print(f'蚂蚁 (0): {ants_count} 张')
print(f'蜜蜂 (1): {bees_count} 张')
print("=" * 50)
# 4. 创建DataLoader
print("\n##########步骤4: 创建DataLoader##########")
print("=" * 50)
# 指定小批次尺寸
batch_size = 32
# 创建训练DataLoader
# shuffle=True: 每次迭代都会打乱数据,增加随机性
train_dataloader = torch.utils.data.DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True
)
# 创建验证DataLoader
# shuffle=False: 验证时不需要打乱数据
val_dataloader = torch.utils.data.DataLoader(
val_dataset,
batch_size=batch_size,
shuffle=False
)
# 集中到字典变量中
dataloaders_dict = {"train": train_dataloader, "val": val_dataloader}
# 确认执行结果
print(f"训练DataLoader批次数量: {len(train_dataloader)}")
print(f"验证DataLoader批次数量: {len(val_dataloader)}")
# 获取一个批次的数据进行检查
batch_iterator = iter(dataloaders_dict["train"]) # 转换成迭代器
inputs, labels = next(batch_iterator) # 取出第一个元素
print(f"输入张量形状: {inputs.size()}") # 应为 [batch_size, 3, 224, 224]
print(f"标签形状: {labels.size()}") # 应为 [batch_size]
print(f"第一批次标签: {labels}")
print("=" * 50)
# 5. 创建网络模型
print("\n##########步骤5: 创建和修改VGG-16网络模型##########")
print("=" * 50)
# 载入已经学习完毕的VGG-16模型
# 创建VGG-16模型的实例
weights = VGG16_Weights.DEFAULT
net = vgg16(weights=weights)
print("原始VGG-16模型结构:")
print("=" * 30)
print(net.classifier) # 显示分类器部分的结构
# 修改模型:将最后一层全连接层改为2个输出(蚂蚁/蜜蜂)
# 原始VGG-16的classifier结构:
# 0: Linear(25088, 4096)
# 1: ReLU
# 2: Dropout(p=0.5)
# 3: Linear(4096, 4096)
# 4: ReLU
# 5: Dropout(p=0.5)
# 6: Linear(4096, 1000) # 原始输出层,对应ImageNet的1000个类别
# 将第6层替换为新的输出层(2个输出:蚂蚁/蜜蜂)
net.classifier[6] = nn.Linear(in_features=4096, out_features=2)
print("\n修改后的VGG-16模型分类器部分:")
print("=" * 30)
print(net.classifier)
# 设定为训练模式
net.train()
print('\n网络设置完毕:载入已经学习完毕的权重,并设置为训练模式')
print("=" * 50)
# 6. 设置损失函数
print("\n##########步骤6: 设置损失函数##########")
print("=" * 50)
# nn.CrossEntropyLoss()是PyTorch中的交叉熵损失函数
# 它结合了LogSoftmax和NLLLoss,专门用于多分类任务
# 对于二分类问题(蚂蚁/蜜蜂)同样适用
criterion = nn.CrossEntropyLoss()
print(f"损失函数: {criterion}")
print("=" * 50)
##############################################################################
# 上面和迁移学习采用相同的处理方式
##############################################################################
# 7. 设定最优化算法
print("\n##########步骤7: 设置优化器##########")
print("=" * 50)
#将微调中需要学习的参数保存到变量params_to_update的1~3中
params_to_update_1 = []# 特征提取器(卷积层)参数
params_to_update_2 = []# 分类器的前两层全连接层参数
params_to_update_3 = []# 分类器的最后一层(新添加的输出层)参数
#指定需要学习的网络层的名称
update_param_names_1 = ["features"]# 特征提取器中的所有卷积层
update_param_names_2 = ["classifier.0.weight","classifier.0.bias", # 第一个全连接层
"classifier.3.weight", "classifier.3.bias"]# 第二个全连接层
update_param_names_3 = ["classifier.6.weight", "classifier.6.bias"]# 最后一个全连接层(新添加的)
print("需要更新的参数:")
#将各个参数分别保存到各个列表中
for name, param in net.named_parameters():
if update_param_names_1[0] in name:# 检查是否包含"features"
param.requires_grad = True
params_to_update_1.append(param)
print("保存到params_to_update_1中 :", name)
elif name in update_param_names_2:
param.requires_grad = True
params_to_update_2.append(param)
print("保存到params_to_update_2中", name)
elif name in update_param_names_3:
param.requires_grad = True
params_to_update_3.append(param)
print("保存到params_to_update_3中 :", name)
else:
param.requires_grad = False
print("不计算梯度。不进行学习 :", name)
print("\n特征提取features模块需要优化的参数数量:", len(params_to_update_1))
print("\n全连接层classifier模块需要优化的参数数量:", len(params_to_update_1))
print("\n特征提取features模块需要优化的参数数量:", len(params_to_update_1))
print("=" * 50)
# 创建随机梯度下降(SGD)优化器
# 使用分层学习率策略:
# 1. 特征提取器(卷积层):较小学习率(1e-4),因为它们是预训练的
# 2. 分类器前两层:中等学习率(5e-4)
# 3. 新添加的输出层:较大学习率(1e-3),因为没有经过预训练
optimizer = optim.SGD([
{'params': params_to_update_1, 'lr': 1e-4},
{'params': params_to_update_2, 'lr': 5e-4},
{'params': params_to_update_3, 'lr': 1e-3}
], momentum=0.9)
print(f"优化器: {optimizer}")
print("=" * 50)
# 8. 执行学习和验证
print("\n##########步骤8: 开始训练模型##########")
print("=" * 50)
num_epochs = 2
print(f"训练轮数: {num_epochs}")
print("开始训练...")
print("=" * 50)
# 调用训练函数(GPU版本)
train_model_gpu(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)
print("=" * 50)
print("训练完成!")
print("=" * 50)
# 9. 保存、加载和验证模型
print("\n##########步骤9: 保存、加载和验证模型##########")
print("=" * 50)
# 9.1 保存PyTorch的网络参数
print("\n9.1 保存模型参数...")
save_path = './weights_fine_tuning.pth'
torch.save(net.state_dict(), save_path)
print(f"模型已保存到: {save_path}")
# 9.2 重新创建模型结构
print("\n9.2 重新创建模型结构...")
# 使用与训练时相同的权重加载方式
loaded_net = vgg16(weights=VGG16_Weights.DEFAULT)
# 修改最后一层,与训练时保持一致
loaded_net.classifier[6] = nn.Linear(in_features=4096, out_features=2)
print("模型结构重新创建完成")
# 9.3 加载保存的权重
print("\n9.3 加载保存的权重...")
try:
# 根据当前设备决定加载方式
if torch.cuda.is_available():
# GPU环境加载
device = torch.device("cuda:0")
# 使用map_location指定加载设备
load_weights = torch.load(save_path, map_location=device, weights_only=True)
else:
# CPU环境加载
device = torch.device("cpu")
# 从GPU保存的模型加载到CPU
load_weights = torch.load(save_path, map_location='cpu', weights_only=True)
# 加载权重到模型
loaded_net.load_state_dict(load_weights)
# 将模型移动到对应的设备
loaded_net = loaded_net.to(device)
print(f"模型权重已成功从 {save_path} 加载")
print(f"模型已移动到设备: {device}")
except Exception as e:
print(f"加载模型时出错: {e}")
# 如果加载失败,可以尝试不使用weights_only参数(旧版PyTorch兼容)
try:
if torch.cuda.is_available():
load_weights = torch.load(save_path, map_location=device)
else:
load_weights = torch.load(save_path, map_location='cpu')
loaded_net.load_state_dict(load_weights)
loaded_net = loaded_net.to(device)
print(f"模型权重已成功加载 (使用旧版加载方式)")
except Exception as e2:
print(f"旧版加载方式也失败: {e2}")
# 如果还是失败,可以尝试直接加载整个模型
try:
if torch.cuda.is_available():
loaded_net = torch.load(save_path, map_location=device)
else:
loaded_net = torch.load(save_path, map_location='cpu')
print(f"整个模型已成功加载")
except Exception as e3:
print(f"所有加载方式都失败: {e3}")
# 跳过验证步骤
loaded_net = None
print("无法加载模型,跳过验证步骤")
exit(1)
# 9.4 验证加载的模型
print("\n9.4 验证加载的模型...")
if loaded_net is not None:
# 设置模型为评估模式
loaded_net.eval()
# 验证模型结构
print("\n验证模型结构:")
print("=" * 30)
print(f"模型类型: {type(loaded_net)}")
print(f"模型是否在训练模式: {loaded_net.training}")
# 检查模型参数
total_params = sum(p.numel() for p in loaded_net.parameters())
trainable_params = sum(p.numel() for p in loaded_net.parameters() if p.requires_grad)
print(f"模型总参数量: {total_params:,}")
print(f"可训练参数量: {trainable_params:,}")
# 检查最后一层
print(f"\n模型最后一层: {loaded_net.classifier[6]}")
if hasattr(loaded_net.classifier[6], 'out_features'):
print(f"输出类别数: {loaded_net.classifier[6].out_features}")
# 9.5 在验证集上测试加载的模型
print("\n9.5 在验证集上测试加载的模型...")
print("=" * 50)
# 确保有验证数据加载器
if 'val' in dataloaders_dict and len(dataloaders_dict['val']) > 0:
# 重新获取设备(确保与模型在同一设备)
device = next(loaded_net.parameters()).device
print(f"模型当前设备: {device}")
# 设置损失函数
criterion = nn.CrossEntropyLoss()
# 初始化统计变量
val_loss = 0.0
val_corrects = 0
total_samples = 0
# 禁用梯度计算,提高效率
with torch.no_grad():
# 使用tqdm显示进度
for batch_idx, (inputs, labels) in enumerate(tqdm(dataloaders_dict['val'], desc="验证中")):
# 将数据移动到与模型相同的设备
inputs = inputs.to(device)
labels = labels.to(device)
# 前向传播
outputs = loaded_net(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 累加损失
val_loss += loss.item() * inputs.size(0)
# 获取预测结果
_, preds = torch.max(outputs, 1)
# 统计正确预测的数量
val_corrects += torch.sum(preds == labels.data).item()
total_samples += inputs.size(0)
# 可选:显示前几个批次的预测示例
if batch_idx == 0:
print(f"\n第一个验证批次的预测示例:")
print(f" 输入形状: {inputs.shape}")
print(f" 输出形状: {outputs.shape}")
print(f" 前5个预测: {preds[:5].cpu().numpy()}")
print(f" 前5个真实标签: {labels[:5].cpu().numpy()}")
# 计算平均损失和准确率
avg_loss = val_loss / total_samples
accuracy = val_corrects / total_samples
print("\n" + "=" * 50)
print("验证结果:")
print("=" * 50)
print(f"验证集大小: {total_samples} 张图片")
print(f"平均损失: {avg_loss:.4f}")
print(f"准确率: {accuracy:.4f} ({accuracy*100:.2f}%)")
print("=" * 50)
# 9.6 详细类别分析
print("\n9.6 类别详细分析...")
print("=" * 50)
# 重置统计
class_correct = {0: 0, 1: 0} # 0:蚂蚁, 1:蜜蜂
class_total = {0: 0, 1: 0}
class_predictions = {0: [], 1: []} # 存储预测结果
with torch.no_grad():
for inputs, labels in dataloaders_dict['val']:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = loaded_net(inputs)
_, preds = torch.max(outputs, 1)
# 统计每个类别的结果
for i in range(labels.size(0)):
label = labels[i].item()
pred = preds[i].item()
class_total[label] += 1
if label == pred:
class_correct[label] += 1
# 存储预测结果
class_predictions[label].append(pred)
# 计算每个类别的准确率
print("\n类别详细统计:")
for class_id in [0, 1]:
if class_total[class_id] > 0:
class_acc = class_correct[class_id] / class_total[class_id]
class_name = "蚂蚁" if class_id == 0 else "蜜蜂"
print(f" {class_name} (类别 {class_id}):")
print(f" 数量: {class_total[class_id]} 张")
print(f" 正确: {class_correct[class_id]} 张")
print(f" 准确率: {class_acc:.4f} ({class_acc*100:.2f}%)")
# 9.7 混淆矩阵
print("\n9.7 混淆矩阵...")
print("=" * 50)
# 收集所有预测和标签
all_preds = []
all_labels = []
with torch.no_grad():
for inputs, labels in dataloaders_dict['val']:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = loaded_net(inputs)
_, preds = torch.max(outputs, 1)
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
# 计算混淆矩阵
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(all_labels, all_preds)
print(f"\n混淆矩阵:")
print(f" 预测蚂蚁 预测蜜蜂")
print(f"真实蚂蚁 {cm[0, 0]:4d} {cm[0, 1]:4d}")
print(f"真实蜜蜂 {cm[1, 0]:4d} {cm[1, 1]:4d}")
# 计算评价指标
from sklearn.metrics import accuracy_score,precision_score, recall_score, f1_score
accuracy = accuracy_score(all_labels, all_preds)
precision = precision_score(all_labels, all_preds, average='macro')
recall = recall_score(all_labels, all_preds, average='macro')
f1 = f1_score(all_labels, all_preds, average='macro')
print(f"\n综合评估指标:")
print(f" 准确率 (Accuracy): {accuracy:.4f}")
print(f" 精确率 (Precision): {precision:.4f}")
print(f" 召回率 (Recall): {recall:.4f}")
print(f" F1分数: {f1:.4f}")
# 9.8 预测单张图片示例
print("\n9.8 单张图片预测示例...")
print("=" * 50)
# 从验证集中取一张图片
if len(val_dataset) > 0:
# 随机选择一张图片
import random
idx = random.randint(0, len(val_dataset) - 1)
img_tensor, true_label = val_dataset[idx]
# 添加批次维度
img_batch = img_tensor.unsqueeze(0).to(device)
# 预测
with torch.no_grad():
output = loaded_net(img_batch)
probabilities = torch.nn.functional.softmax(output, dim=1)
predicted_prob, predicted_class = torch.max(probabilities, 1)
# 转换为类别名
class_names = ["蚂蚁", "蜜蜂"]
true_class_name = class_names[true_label]
predicted_class_name = class_names[predicted_class.item()]
print(f"示例图片索引: {idx}")
print(f"真实类别: {true_class_name} (标签: {true_label})")
print(f"预测类别: {predicted_class_name} (标签: {predicted_class.item()})")
print(f"预测概率: {predicted_prob.item():.4f}")
# 显示所有类别的概率
print(f"\n所有类别概率:")
for i, prob in enumerate(probabilities[0]):
print(f" {class_names[i]}: {prob.item():.4f}")
# 判断预测是否正确
if predicted_class.item() == true_label:
print(f"\n✅ 预测正确!")
else:
print(f"\n❌ 预测错误!")
# 9.9 模型推理速度测试
print("\n9.9 模型推理速度测试...")
print("=" * 50)
# 测试推理时间
import time
# 创建测试数据
test_input = torch.randn(1, 3, 224, 224).to(device)
# 预热(第一次推理通常较慢)
with torch.no_grad():
_ = loaded_net(test_input)
# 多次推理取平均时间
num_tests = 100
start_time = time.time()
with torch.no_grad():
for _ in range(num_tests):
_ = loaded_net(test_input)
end_time = time.time()
avg_time = (end_time - start_time) / num_tests
fps = 1.0 / avg_time
print(f"测试 {num_tests} 次推理的平均时间:")
print(f" 平均推理时间: {avg_time*1000:.2f} ms")
print(f" 推理速度: {fps:.2f} FPS")
# 9.10 保存验证结果
print("\n9.10 保存验证结果...")
print("=" * 50)
# 创建结果字典
results = {
'validation_loss': float(avg_loss),
'validation_accuracy': float(accuracy),
'class_accuracy': {
'ants': float(class_correct[0]/class_total[0]) if class_total[0] > 0 else 0.0,
'bees': float(class_correct[1]/class_total[1]) if class_total[1] > 0 else 0.0
},
'confusion_matrix': cm.tolist(),
'metrics': {
'precision': float(precision),
'recall': float(recall),
'f1_score': float(f1)
},
'inference_speed': {
'avg_time_ms': float(avg_time * 1000),
'fps': float(fps)
},
'model_info': {
'total_params': int(total_params),
'trainable_params': int(trainable_params),
'device': str(device)
}
}
# 保存结果到JSON文件
import json
results_path = './validation_results.json'
with open(results_path, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"验证结果已保存到: {results_path}")
# 9.11 生成验证报告
print("\n9.11 生成验证报告...")
print("=" * 50)
report = f"""
========================================
VGG-16迁移学习模型验证报告
========================================
1. 模型信息
- 模型类型: VGG-16
- 输出类别: 2 (蚂蚁/蜜蜂)
- 总参数量: {total_params:,}
- 可训练参数: {trainable_params:,}
- 运行设备: {device}
2. 验证结果
- 验证集大小: {total_samples} 张图片
- 平均损失: {avg_loss:.4f}
- 总体准确率: {accuracy:.4f} ({accuracy*100:.2f}%)
3. 类别详细结果
- 蚂蚁准确率: {class_correct[0]}/{class_total[0]} = {class_correct[0]/class_total[0]:.4f} ({class_correct[0]/class_total[0]*100:.2f}%)
- 蜜蜂准确率: {class_correct[1]}/{class_total[1]} = {class_correct[1]/class_total[1]:.4f} ({class_correct[1]/class_total[1]*100:.2f}%)
4. 混淆矩阵
预测蚂蚁 预测蜜蜂
真实蚂蚁 {cm[0, 0]:4d} {cm[0, 1]:4d}
真实蜜蜂 {cm[1, 0]:4d} {cm[1, 1]:4d}
5. 评估指标
- 精确率: {precision:.4f}
- 召回率: {recall:.4f}
- F1分数: {f1:.4f}
6. 性能测试
- 平均推理时间: {avg_time*1000:.2f} ms
- 推理速度: {fps:.2f} FPS
7. 文件保存
- 模型文件: {save_path}
- 验证结果: {results_path}
========================================
验证完成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}
========================================
"""
print(report)
# 保存报告到文本文件
report_path = './validation_report.txt'
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"详细验证报告已保存到: {report_path}")
else:
print("错误: 验证数据加载器不存在或为空")
else:
print("错误: 模型加载失败,无法进行验证")
print("\n" + "=" * 50)
print("模型验证步骤完成!")
print("=" * 50)
2.程序输出
##########步骤1: 创建数据路径列表##########
==================================================
搜索路径: ./data/hymenoptera_data/train/**/*.jpg
搜索路径: ./data/hymenoptera_data/val/**/*.jpg
训练集大小: 243 张图片
验证集大小: 153 张图片
==================================================
##########步骤2: 创建Dataset##########
==================================================
训练数据集大小: 243
验证数据集大小: 153
==================================================
##########步骤3: 遍历数据集并显示统计信息##########
==================================================
训练数据集遍历
==================================================
索引 0: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 1: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 2: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 3: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 4: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 5: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 6: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 7: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 8: 图像形状 torch.Size([3, 224, 224]), 标签 0
索引 9: 图像形状 torch.Size([3, 224, 224]), 标签 0
==================================================
总计:243 张图片
蚂蚁 (0): 122 张
蜜蜂 (1): 121 张
==================================================
##########步骤4: 创建DataLoader##########
==================================================
训练DataLoader批次数量: 8
验证DataLoader批次数量: 5
输入张量形状: torch.Size([32, 3, 224, 224])
标签形状: torch.Size([32])
第一批次标签: tensor([1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1,
1, 0, 0, 0, 1, 0, 1, 0])
==================================================
##########步骤5: 创建和修改VGG-16网络模型##########
==================================================
原始VGG-16模型结构:
==============================
Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
修改后的VGG-16模型分类器部分:
==============================
Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=2, bias=True)
)
网络设置完毕:载入已经学习完毕的权重,并设置为训练模式
==================================================
##########步骤6: 设置损失函数##########
==================================================
损失函数: CrossEntropyLoss()
==================================================
##########步骤7: 设置优化器##########
==================================================
需要更新的参数:
保存到params_to_update_1中 : features.0.weight
保存到params_to_update_1中 : features.0.bias
保存到params_to_update_1中 : features.2.weight
保存到params_to_update_1中 : features.2.bias
保存到params_to_update_1中 : features.5.weight
保存到params_to_update_1中 : features.5.bias
保存到params_to_update_1中 : features.7.weight
保存到params_to_update_1中 : features.7.bias
保存到params_to_update_1中 : features.10.weight
保存到params_to_update_1中 : features.10.bias
保存到params_to_update_1中 : features.12.weight
保存到params_to_update_1中 : features.12.bias
保存到params_to_update_1中 : features.14.weight
保存到params_to_update_1中 : features.14.bias
保存到params_to_update_1中 : features.17.weight
保存到params_to_update_1中 : features.17.bias
保存到params_to_update_1中 : features.19.weight
保存到params_to_update_1中 : features.19.bias
保存到params_to_update_1中 : features.21.weight
保存到params_to_update_1中 : features.21.bias
保存到params_to_update_1中 : features.24.weight
保存到params_to_update_1中 : features.24.bias
保存到params_to_update_1中 : features.26.weight
保存到params_to_update_1中 : features.26.bias
保存到params_to_update_1中 : features.28.weight
保存到params_to_update_1中 : features.28.bias
保存到params_to_update_2中 classifier.0.weight
保存到params_to_update_2中 classifier.0.bias
保存到params_to_update_2中 classifier.3.weight
保存到params_to_update_2中 classifier.3.bias
保存到params_to_update_3中 : classifier.6.weight
保存到params_to_update_3中 : classifier.6.bias
特征提取features模块需要优化的参数数量: 26
全连接层classifier模块需要优化的参数数量: 26
特征提取features模块需要优化的参数数量: 26
==================================================
优化器: SGD (
Parameter Group 0
dampening: 0
differentiable: False
foreach: None
fused: None
lr: 0.0001
maximize: False
momentum: 0.9
nesterov: False
weight_decay: 0
Parameter Group 1
dampening: 0
differentiable: False
foreach: None
fused: None
lr: 0.0005
maximize: False
momentum: 0.9
nesterov: False
weight_decay: 0
Parameter Group 2
dampening: 0
differentiable: False
foreach: None
fused: None
lr: 0.001
maximize: False
momentum: 0.9
nesterov: False
weight_decay: 0
)
==================================================
##########步骤8: 开始训练模型##########
==================================================
训练轮数: 2
开始训练...
==================================================
PyTorch 版本:2.4.1+cu121
CUDA 可用:True
PyTorch对应CUDA版本:12.1
实际CUDA版本:12.8
GPU 名称:NVIDIA GeForce RTX 3070 Laptop GPU
GPU 计算能力:(8, 6)
CUDA设备数量: 1
使用设备: cuda:0
Epoch 1/2
--------------------------------------------
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:03<00:00, 1.62it/s]
val Loss: 0.6393 Acc: 0.6405
Epoch 2/2
--------------------------------------------
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:08<00:00, 1.03s/it]
train Loss: 0.4424 Acc: 0.7901
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 5.67it/s]
val Loss: 0.1803 Acc: 0.9542
==================================================
训练完成!
==================================================
##########步骤9: 保存、加载和验证模型##########
==================================================
9.1 保存模型参数...
模型已保存到: ./weights_fine_tuning.pth
9.2 重新创建模型结构...
模型结构重新创建完成
9.3 加载保存的权重...
模型权重已成功从 ./weights_fine_tuning.pth 加载
模型已移动到设备: cuda:0
9.4 验证加载的模型...
验证模型结构:
==============================
模型类型: <class 'torchvision.models.vgg.VGG'>
模型是否在训练模式: False
模型总参数量: 134,268,738
可训练参数量: 134,268,738
模型最后一层: Linear(in_features=4096, out_features=2, bias=True)
输出类别数: 2
9.5 在验证集上测试加载的模型...
==================================================
模型当前设备: cuda:0
验证中: 0%| | 0/5 [00:00<?, ?it/s]
第一个验证批次的预测示例:
输入形状: torch.Size([32, 3, 224, 224])
输出形状: torch.Size([32, 2])
前5个预测: [0 0 1 0 0]
前5个真实标签: [0 0 0 0 0]
验证中: 100%|█████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 5.54it/s]
==================================================
验证结果:
==================================================
验证集大小: 153 张图片
平均损失: 0.1803
准确率: 0.9542 (95.42%)
==================================================
9.6 类别详细分析...
==================================================
类别详细统计:
蚂蚁 (类别 0):
数量: 70 张
正确: 65 张
准确率: 0.9286 (92.86%)
蜜蜂 (类别 1):
数量: 83 张
正确: 81 张
准确率: 0.9759 (97.59%)
9.7 混淆矩阵...
==================================================
混淆矩阵:
预测蚂蚁 预测蜜蜂
真实蚂蚁 65 5
真实蜜蜂 2 81
综合评估指标:
准确率 (Accuracy): 0.9542
精确率 (Precision): 0.9560
召回率 (Recall): 0.9522
F1分数: 0.9537
9.8 单张图片预测示例...
==================================================
示例图片索引: 112
真实类别: 蜜蜂 (标签: 1)
预测类别: 蜜蜂 (标签: 1)
预测概率: 0.9202
所有类别概率:
蚂蚁: 0.0798
蜜蜂: 0.9202
✅ 预测正确!
9.9 模型推理速度测试...
==================================================
测试 100 次推理的平均时间:
平均推理时间: 4.19 ms
推理速度: 238.49 FPS
9.10 保存验证结果...
==================================================
验证结果已保存到: ./validation_results.json
9.11 生成验证报告...
==================================================
========================================
VGG-16迁移学习模型验证报告
========================================
1. 模型信息
- 模型类型: VGG-16
- 输出类别: 2 (蚂蚁/蜜蜂)
- 总参数量: 134,268,738
- 可训练参数: 134,268,738
- 运行设备: cuda:0
2. 验证结果
- 验证集大小: 153 张图片
- 平均损失: 0.1803
- 总体准确率: 0.9542 (95.42%)
3. 类别详细结果
- 蚂蚁准确率: 65/70 = 0.9286 (92.86%)
- 蜜蜂准确率: 81/83 = 0.9759 (97.59%)
4. 混淆矩阵
预测蚂蚁 预测蜜蜂
真实蚂蚁 65 5
真实蜜蜂 2 81
5. 评估指标
- 精确率: 0.9560
- 召回率: 0.9522
- F1分数: 0.9537
6. 性能测试
- 平均推理时间: 4.19 ms
- 推理速度: 238.49 FPS
7. 文件保存
- 模型文件: ./weights_fine_tuning.pth
- 验证结果: ./validation_results.json
========================================
验证完成时间: 2026-03-07 14:11:29
========================================
详细验证报告已保存到: ./validation_report.txt
==================================================
模型验证步骤完成!
==================================================
3.混淆矩阵说明

4.报错处理
net.load_state_dict(load_weights)
#报错
# FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.
#原因
这是 PyTorch 新版本的安全警告。从 PyTorch 2.4+ 开始,torch.load 函数默认会提示 weights_only 参数,目的是防止恶意 pickle 数据执行任意代码。
pickle模块是Python中用于对象序列化和反序列化的标准库,但它也容易被利用来执行任意代码,特别是当反序列化的数据来自不可信的来源时。
# 解决办法显式设置 weights_only=False
load_weights = torch.load(load_path, weights_only=True)
net = models.vgg16(pretrained=True)
#报错
UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.
warnings.warn(
UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=VGG16_Weights.IMAGENET1K_V1`. You can also use `weights=VGG16_Weights.DEFAULT` to get the most up-to-date weights.
#原因
这是 torchvision 新版本(0.13+)的弃用警告。pretrained 参数已被 weights 参数替代。
# 解决办法
# VGG16_Weights.IMAGENET1K_V1:ImageNet数据集第1版权重,固定版本,稳定,不会变化(与原 pretrained=True 等效)
weights = VGG16_Weights.IMAGENET1K_V1
# VGG16_Weights.DEFAULT:当前最新推荐权重,动态更新,可能随版本升级变化
weights = VGG16_Weights.DEFAULT
net = vgg16(weights=weights)更多推荐
所有评论(0)