PyTorch深度学习笔记2:基于VGG-16的迁移学习
1. 迁移学习概述
1.1 什么是迁移学习
迁移学习是将在一个任务上训练好的模型知识应用到另一个相关任务的技术。在深度学习领域,通常使用在大型数据集(如ImageNet)上预训练的模型作为基础。
1.2 为什么使用迁移学习
优势:
1. 数据需求小: 只需要少量标注数据即可获得好效果
2. 训练速度快: 只需微调部分层,训练时间短
3. 性能优异: 预训练模型已学习通用特征表示
4. 防止过拟合: 预训练权重提供良好的初始化
适用场景:
1. 数据集小(几百到几千张图片)
2. 计算资源有限
3. 需要快速原型验证
4. 新任务与预训练任务相似
2. VGG-16迁移学习流程
2.1 整体流程图

2.2 训练策略对比
|
策略 |
冻结层数 |
训练参数 |
训练时间 |
适用场景 |
|---|---|---|---|---|
|
全冻结 |
所有层 |
只训练新层 |
最短 |
数据集很小 |
|
部分解冻 |
大部分层 |
训练最后几层 |
短 |
数据集中等 |
|
全解冻 |
不冻结 |
训练所有层 |
长 |
数据集大 |
|
分层解冻 |
逐步解冻 |
分阶段训练 |
中 |
精细调优 |
4. 关键技术要点
4.1 参数冻结机制
VGG-16参数分布:
1. 特征提取器(features): 13个卷积层
2. 分类器(classifier): 3个全连接层
冻结策略:
1. 特征提取器参数全部冻结
2. 分类器前2层冻结
3. 只解冻最后一层(输出层)
原因:
- 特征提取器学习的是通用视觉特征
- 前2个全连接层是高级语义特征
- 最后一层是任务特定特征
4.2 优化算法
普通SGD更新公式:
θ = θ - η * ∇J(θ)
其中:
θ: 模型参数
η: 学习率 (lr)
∇J(θ): 损失函数关于参数的梯度
动量SGD更新公式:
v_t = γ * v_{t-1} + η * ∇J(θ)
θ = θ - v_t
其中:
v_t: 当前时刻的速度
γ: 动量系数 (momentum)
v_{t-1}: 上一时刻的速度
学习率的作用:
- 控制参数更新的步长
- 影响模型收敛速度和最终性能
常用学习率范围:
1. 较大学习率: 0.1, 0.01 # 初始训练,快速下降
2. 中等学习率: 0.001, 0.0005 # 微调常用
3. 较小学习率: 0.0001 # 精细调整
VGG-16微调经验:
- 初始学习率: 0.001-0.0001
- 随着训练进展逐渐减小
动量系数范围: 0.0 - 1.0
- 0.0: 无动量,普通SGD
- 0.5-0.9: 常用范围
- 0.9: 默认值,适用于大多数情况
- 0.99: 高动量,更平滑
动量的物理意义:
想象一个小球在损失函数表面上滚动:
- 动量=0: 小球只受当前梯度影响
- 动量>0: 小球有惯性,能冲过局部最小值
- 动量接近1: 小球能冲过平坦区域
动量优化的优势:
1. 加速收敛: 在梯度方向一致时加速
2. 减少震荡: 平滑梯度更新方向
3. 逃离局部最小值: 有"冲力"跳出局部最优
4. 通过平坦区域: 在梯度接近0时仍有动量
普通SGD vs 动量SGD:
普通SGD: zig-zag路径,缓慢且震荡
动量SGD: → 更直接的路径,快速稳定
4.3 为什么验证集表现比训练集好?
这在迁移学习中是正常现象:
| 原因 | 说明 |
|---|---|
| 预训练模型 | 使用的是在 ImageNet 上预训练的模型,已经学到了通用特征 |
| 只微调输出层 | 本例只训练最后一层,底层特征提取器是冻结的 |
| 数据增强 | 训练时使用了 RandomResizedCrop、RandomHorizontalFlip 等增强,使训练数据更难 |
| 验证集无增强 | 验证时使用 CenterCrop,图像更规范,更容易分类 |
5. 迁移学习性能对比
|
方法 |
准确率 |
训练时间 |
数据需求 |
适用场景 |
|---|---|---|---|---|
|
从头训练 |
60-70% |
长 |
大量 |
大数据集 |
|
特征提取 |
85-90% |
短 |
中等 |
快速原型 |
|
微调所有层 |
92-95% |
中 |
中等 |
性能优先 |
|
渐进解冻 |
94-96% |
中长 |
中等 |
精细调优 |
6. python实现
6.1图片下载
如果下载成功可以看到如下图片,已经分好类

6.2程序内容
使用迁移学习来微调一个预训练的VGG-16模型,以解决一个二分类问题(蚂蚁和蜜蜂)。利用了预训练模型的特征提取能力,只重新训练了最后一层全连接层,这样即使在小数据集上也能获得较好的性能,并且训练速度较快。
导入必要的库:包括文件操作、图像处理、数值计算、进度条、绘图以及PyTorch相关库。
设置随机种子:确保实验的可重复性。
定义预处理参数:图像大小调整为224x224,使用ImageNet的均值和标准差进行归一化。
定义图像预处理类(ImageTransform):这个类根据训练或验证阶段应用不同的预处理。训练阶段使用随机缩放裁剪和随机水平翻转进行数据增强,然后转换为张量并归一化。验证阶段只进行缩放、中心裁剪、转换为张量和归一化。
定义数据集类(HymenopteraDataset):这是一个自定义的PyTorch数据集类,用于加载蚂蚁和蜜蜂的图片。它从文件路径中提取标签(蚂蚁为0,蜜蜂为1),并应用相应的预处理。
创建数据路径列表函数(make_datapath_list):根据给定的阶段(训练或验证),使用glob模块收集对应目录下的所有图片路径。
训练模型函数(train_model):这个函数执行训练和验证循环。在每个epoch中,它依次在训练集和验证集上进行迭代。训练阶段执行反向传播和参数更新,验证阶段只进行前向传播计算损失和准确率。损失和准确率在每个epoch结束后打印。
6.3主程序流程:
a. 创建训练和验证数据路径列表。
b. 创建训练和验证数据集实例。
c. 遍历并打印训练数据集的部分样本信息,统计各类别的数量。
d. 创建训练和验证数据加载器(DataLoader),设置批量大小为32,训练数据加载器会打乱数据。
e. 加载预训练的VGG-16模型,并将其最后的全连接层(分类器的第6层)替换为新的线性层,输出为2(对应蚂蚁和蜜蜂)。
f. 设置模型为训练模式。
g. 定义损失函数为交叉熵损失。
h. 确定需要更新的参数:只更新最后一层的权重和偏置,其他层的参数被冻结(不计算梯度,不更新)。
i. 定义优化器为随机梯度下降(SGD),学习率为0.001,动量为0.9,只传入需要更新的参数。
j. 调用train_model函数进行训练,训练2个epoch。
6.4程序
# 导入必要的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
# 设置随机数种子以确保结果可重现
# 在机器学习中,固定随机种子可以使每次运行得到相同的结果
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(net, dataloaders_dict, criterion, optimizer, num_epochs):
"""
训练和验证模型的函数
Parameters
----------
net : nn.Module
要训练的神经网络模型
dataloaders_dict : dict
包含'train'和'val'两个DataLoader的字典
criterion : nn.Module
损失函数
optimizer : optim.Optimizer
优化器
num_epochs : int
训练的轮数
"""
# 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
# 载入数据并切取出小批次的循环
# 使用tqdm显示进度条
for inputs, labels in tqdm(dataloaders_dict[phase]):
# 初始化优化器梯度
optimizer.zero_grad()
# 计算正向传播
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的平均损失和准确率
epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
# 显示每个epoch的loss和准确率
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模型的实例
use_pretrained = True # 指定使用已经训练好的参数
net = models.vgg16(pretrained=use_pretrained)
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变量中
params_to_update = []
# 需要学习的参数名称
# 在迁移学习中,通常只训练最后的分类层
update_param_names = ["classifier.6.weight", "classifier.6.bias"]
print("需要更新的参数:")
for name, param in net.named_parameters():
if name in update_param_names:
param.requires_grad = True # 只训练输出层
params_to_update.append(param)
print(f" {name}")
else:
param.requires_grad = False # 冻结预训练层
print("\n需要优化的参数数量:", len(params_to_update))
print("=" * 50)
# 创建随机梯度下降(SGD)优化器
optimizer = optim.SGD(
params=params_to_update, # 要优化的参数
lr=0.001, # 学习率
momentum=0.9 # 动量系数
)
print(f"优化器: {optimizer}")
print("=" * 50)
# 8. 执行学习和验证
print("\n##########步骤8: 开始训练模型##########")
print("=" * 50)
num_epochs = 2
print(f"训练轮数: {num_epochs}")
print("开始训练...")
print("=" * 50)
# 调用训练函数
train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)
print("=" * 50)
print("训练完成!")
print("=" * 50)
7.运行输出
##########步骤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
==================================================
总计: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: 设置优化器##########
==================================================
需要更新的参数:
classifier.6.weight
classifier.6.bias
需要优化的参数数量: 2
==================================================
优化器: SGD (
Parameter Group 0
dampening: 0
lr: 0.001
momentum: 0.9
nesterov: False
weight_decay: 0
)
==================================================
##########步骤8: 开始训练模型##########
==================================================
训练轮数: 2
开始训练...
==================================================
Epoch 1/2
--------------------------------------------
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:12<00:00, 2.55s/it]
val Loss: 0.6394 Acc: 0.6405
Epoch 2/2
--------------------------------------------
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:18<00:00, 2.34s/it]
train Loss: 0.4322 Acc: 0.8066
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:11<00:00, 2.32s/it]
val Loss: 0.1877 Acc: 0.9608
==================================================
训练完成!
==================================================
更多推荐
所有评论(0)