基于经典网络架构训练图像分类模型 代码学习
引用的库
import os
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import torch
from torch import nn
import torch.optim as optim
import torchvision
#pip install torchvision
from torchvision import transforms, models, datasets
#https://pytorch.org/docs/stable/torchvision/index.html
import imageio
import time
import warnings
warnings.filterwarnings("ignore")
import random
import sys
import copy
import json
from PIL import Image
-
os
:用于与操作系统交互,例如读取或写入文件、目录操作等。 -
sys
:提供对 Python 解释器相关的变量和函数的访问。例如可以用来退出程序或获取命令行参数。 -
random
:生成随机数或随机选择元素,常用于数据打乱、初始化等。 -
copy
:用于复制对象。例如深拷贝(copy.deepcopy()
)可以在不修改原始对象的情况下复制复杂结构。 -
json
:用于处理 JSON 数据格式。可以将数据转换为 JSON 字符串,或者从 JSON 字符串解析数据。 -
numpy as np
:提供高效的多维数组(ndarray
)以及各种数学运算支持,是机器学习和科学计算的基础库。 -
matplotlib.pyplot as plt
:用于绘制图表(如折线图、柱状图、热力图等)。 -
imageio
:用于读取和写入图像文件,支持多种格式(如 PNG、JPG 等)。 -
time
:用于获取当前时间、计时等。 -
torch
:PyTorch 是一个流行的深度学习框架,提供张量操作、自动求导、神经网络模块等功能。 -
torch.nn as nn
:包含构建神经网络所需的类和函数,如卷积层、全连接层、激活函数等。 -
torch.optim
:提供优化算法,如 SGD、Adam 等,用于训练模型时更新权重。 -
torchvision
:PyTorch 的视觉工具包,包含:常用数据集(如 CIFAR-10、ImageNet);预训练模型(如 ResNet、VGG);图像变换工具(如缩放、裁剪)。 -
torchvision.transforms
:提供图像预处理和增强的方法,如归一化、随机翻转、裁剪等。 -
torchvision.models
:提供预训练的深度学习模型(如 ResNet、AlexNet、VGG 等),可以直接使用或进行迁移学习。 -
torchvision.datasets
:提供常用视觉数据集的加载方法,简化数据读取过程。 -
PIL.Image
:PIL(Python Imaging Library)用于处理图像文件,支持多种图像格式和操作。
主要分类:
- 数据处理:
os
,PIL.Image
,imageio
,torchvision.datasets
,transforms
- 模型构建:
torch
,torch.nn
,torchvision.models
- 训练和优化:
torch.optim
,time
- 可视化:
matplotlib
,numpy
- 辅助功能:
random
,copy
,json
,warnings
设置目录
data_dir = './flower_data/'
train_dir = data_dir + '/train'
valid_dir = data_dir + '/valid'
定义变量,+号主要用于字符串拼接。
valid_dir变量就是./flower_data//valid
制作数据源
data_transforms = {
'train':
transforms.Compose([
transforms.Resize([96, 96]),
transforms.RandomRotation(45),#随机旋转,-45到45度之间随机选
transforms.CenterCrop(64),#从中心开始裁剪
transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转 选择一个概率概率
transforms.RandomVerticalFlip(p=0.5),#随机垂直翻转
transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),#参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
transforms.RandomGrayscale(p=0.025),#概率转换成灰度率,3通道就是R=G=B
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#均值,标准差
]),
'valid':
transforms.Compose([
transforms.Resize([64, 64]),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
Python中的一些基本概念
1.字典
-
Python 中的一种数据结构,用
{}
表示。 -
存储键值对(key-value pairs),例如:
my_dict = {'name': 'Tom', 'age': 20}
-
你可以通过键来访问对应的值:
print(my_dict['name']) # 输出: Tom
2. 列表(List)
-
使用
[]
包裹多个元素,如:my_list = [1, 2, 3]
-
列表是有序的,可以通过索引访问元素。
3. 函数调用
- 如
transforms.RandomRotation(45)
是调用一个函数(或类)并传入参数。 -
transforms.Compose([...])
是将多个变换组合成一个流水线。
PyTorch 图像变换详解(transforms)
✅ 'train'
数据增强与预处理
这个集合用于训练数据,目的是增加数据多样性,提高模型泛化能力。
transforms.Compose([
transforms.Resize([96, 96]), # 缩放图像为 96x96
transforms.RandomRotation(45), # 随机旋转 -45~+45 度
transforms.CenterCrop(64), # 从中心裁剪出 64x64 大小
transforms.RandomHorizontalFlip(p=0.5),# 以50%概率水平翻转
transforms.RandomVerticalFlip(p=0.5), # 以50%概率垂直翻转
transforms.ColorJitter(...), # 随机改变亮度、对比度等
transforms.RandomGrayscale(p=0.025), # 以2.5%概率转灰度图
transforms.ToTensor(), # 转换为张量 (C x H x W),范围[0,1]
transforms.Normalize(...) # 标准化(均值、标准差)
])
🔁 顺序执行逻辑(非常重要!)
每张图像都会按照列表中的顺序依次经过这些变换。比如:
- 先缩放 →
- 再旋转 →
- 接着裁剪 →
- 然后可能随机翻转 →
- …直到最后标准化成模型可接受的数据格式。
✅ 'valid'
预处理(无增强)
验证集不需要做数据增强,所以变换更简单:
transforms.Compose([
transforms.Resize([64, 64]), # 缩放至 64x64
transforms.ToTensor(), # 转张量
transforms.Normalize(...) # 同样标准化
])
注意这里的尺寸是 64x64
,而训练集中间经历了 96x96 -> CenterCrop -> 64x64
。这样设计是为了保证训练和验证图像有相同的输入尺寸。
变换 | 目的 |
---|---|
Resize | 统一图像大小,便于批量处理 |
RandomRotation , RandomFlip , ColorJitter , RandomGrayscale | 数据增强,防止过拟合 |
CenterCrop | 提取图像主体部分,减少背景干扰 |
ToTensor() | 将 PIL 图像转为 PyTorch 张量 |
Normalize | 归一化处理,加速模型收敛(使用 ImageNet 的均值/标准差) |
batch_size = 128
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'valid']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in ['train', 'valid']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}
class_names = image_datasets['train'].classes
batch_size = 128
- 定义每次训练或验证时输入模型的样本数量(即 batch size)。
- 每次处理 128 张图像。
image_datasets = {
x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x])
for x in ['train', 'valid']
}
-
使用
ImageFolder
加载数据集:- 自动从目录结构中读取类别;
- 对每个图像应用之前定义的变换(如 resize、normalize 等);
-
x
是'train'
和'valid'
,表示训练和验证集;
-
返回一个字典:
{'train': Dataset对象, 'valid': Dataset对象}
dataloaders = {
x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True)
for x in ['train', 'valid']
}
-
创建 DataLoader:
- 将 Dataset 包装成可迭代的对象;
-
shuffle=True
表示在每个 epoch 开始前打乱训练数据,有助于提升泛化能力; - 每次返回一个 batch 的数据(图像 + 标签);
-
同样返回一个字典:
{'train': Dataloader, 'valid': Dataloader}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}
- 获取训练集和验证集的样本数量;
- 可用于计算训练进度、准确率比例等。
class_names = image_datasets['train'].classes
- 获取所有类别名称;
-
ImageFolder
会自动根据文件夹名排序并映射为数字标签; -
class_names
是一个列表,例如:['daisy', 'dandelion', 'roses', ...]
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False
迁移学习
🔍 param.requires_grad = False
是什么意思?
在 PyTorch 中,每个模型参数(权重、偏置等)都有一个属性:requires_grad
,它决定了:
- 是否需要计算该参数的梯度;
- 是否会在反向传播中更新该参数。
默认情况下是 True
,即会计算梯度并更新。
当你设置为 False
后:
- 不会计算梯度;
- 不会在训练过程中更新;
- 相当于“冻结”了这部分网络。
迁移学习做法
在迁移学习中,我们通常有两种做法:
✅ 方法 1:特征提取(Feature Extraction)
- 使用预训练模型(如 ImageNet 上训练好的 ResNet);
- 冻结前面所有的卷积层(不更新它们的参数);
- 只训练最后几层(通常是全连接层)来适配新任务;
- 适用于小数据集,节省时间和显存。
set_parameter_requires_grad(model, feature_extracting=True)
✅ 方法 2:微调(Fine-tuning)
- 解冻所有层(或部分层);
- 用较小的学习率重新训练整个模型;
- 适用于大数据集,能更好地适应目标任务。
set_parameter_requires_grad(model, feature_extracting=False)
🛠️ 使用示例(配合预训练模型)
# 加载预训练的 ResNet18 模型
model = models.resnet18(pretrained=True)
# 设置冻结参数
set_parameter_requires_grad(model, feature_extracting=True)
# 替换最后一层全连接层
num_ftrs = model.fc.in_features
num_classes = len(class_names) # 根据你的数据集类别数修改
model.fc = nn.Linear(num_ftrs, num_classes)
# 将模型移动到 GPU(如果可用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
📊 查看哪些参数可训练(调试用)
如果你想看看当前模型中哪些参数是可以训练的,可以用下面这段代码:
for name, param in model.named_parameters():
print(f"{name} requires_grad: {param.requires_grad}")
输出示例:
conv1.weight requires_grad: False
bn1.weight requires_grad: False
...
fc.weight requires_grad: True
fc.bias requires_grad: True
✅ 进阶用法:只冻结部分层
有时你想冻结前几层,但保留后面的层进行训练。例如:
for name, param in model.named_parameters():
if 'layer4' not in name and 'fc' not in name:
param.requires_grad = False
这样就只训练 layer4
和 fc
层。
🧠 总结
操作 | 说明 |
---|---|
requires_grad = False | 冻结参数,不参与训练 |
requires_grad = True | 参与训练 |
set_parameter_requires_grad(model, True) | 冻结整个模型参数 |
set_parameter_requires_grad(model, False) | 所有参数都可训练 |
关于params_to_update
🎯 场景设定
假设你使用的是 ResNet18 模型,并做了以下操作:
- 冻结前面所有卷积层(不更新它们);
- 只替换最后一层全连接层(
fc
),并设置它为可训练; - 然后通过遍历模型参数,收集所有
requires_grad == True
的参数到params_to_update
列表中。
✅ 示例代码
import torch
import torch.nn as nn
import torchvision.models as models
# 1. 加载预训练 ResNet18 模型
model = models.resnet18(pretrained=True)
# 2. 冻结所有参数
for param in model.parameters():
param.requires_grad = False
# 3. 替换最后的全连接层(输出类别数改为 10)
model.fc = nn.Linear(512, 10)
model.fc.requires_grad = True # 设置为可训练
# 4. 收集所有需要更新的参数
params_to_update = []
for name, param in model.named_parameters():
if param.requires_grad:
params_to_update.append(param)
print(f"参数名: {name} | 参数形状: {param.shape}")
📋 输出结果示例(模拟)
运行上面这段代码,你可能会看到类似如下输出:
参数名: fc.weight | 参数形状: torch.Size([10, 512])
参数名: fc.bias | 参数形状: torch.Size([10])
🧠 解释这个输出
-
params_to_update
是一个 Python 列表,里面包含两个元素:- 第一个元素是
fc.weight
:全连接层的权重矩阵,形状是[10, 512]
,表示输出 10 类,输入来自 512 个特征; - 第二个元素是
fc.bias
:偏置项,长度是10
,对应每个类别的偏置。
- 第一个元素是
这两个参数就是你希望在训练过程中被优化器更新的部分。
🔍 打印 params_to_update
列表内容(模拟)
如果你打印 params_to_update
,你会看到类似这样的内容(不是字符串,而是 tensor 对象):
[
Parameter containing:
tensor([[ 0.01, -0.02, ...], ..., requires_grad=True), # fc.weight
Parameter containing:
tensor([0.1, -0.1, ...], requires_grad=True) # fc.bias
]
它们都是 torch.nn.Parameter
类型,是 PyTorch 中专门用于自动求导和优化的张量类型。
✅ 传给优化器的样子
你可以这样使用它:
optimizer = torch.optim.Adam(params_to_update, lr=0.001)
这时优化器只会更新 fc.weight
和 fc.bias
,而不会动前面的所有卷积层。
🧩 总结一下
内容 | 说明 |
---|---|
params_to_update 是什么? | 一个包含所有“可训练参数”的列表 |
它里面的元素是什么? | torch.nn.Parameter 类型,也就是模型中的权重和偏置 |
举例说明? | 包含 fc.weight 和 fc.bias ,即最后的全连接层参数 |
有什么用? | 告诉优化器哪些参数要更新,在迁移学习中非常有用 |
训练模型
def train_model(model, dataloaders, criterion, optimizer, num_epochs=25, filename='best.pt'):
参数说明:
参数名 | 类型 | 作用 |
---|---|---|
model | PyTorch 模型 | 要训练的神经网络模型(如 ResNet) |
dataloaders | 字典 { 'train': DataLoader, 'valid': DataLoader } | 数据加载器 |
criterion | 损失函数 | 如 nn.CrossEntropyLoss() |
optimizer | 优化器 | 如 optim.Adam() 或 optim.SGD() |
num_epochs | 整数 | 训练多少轮 |
filename | 字符串 | 保存最佳模型的路径 |
🧠 初始化变量
since = time.time()
best_acc = 0
model.to(device)
-
since
: 开始计时,记录训练总时间; -
best_acc
: 记录验证集上最好的准确率; -
model.to(device)
: 把模型放到 GPU 或 CPU 上运行。
📊 训练过程中的指标记录
val_acc_history = []
train_acc_history = []
train_losses = []
valid_losses = []
LRs = [optimizer.param_groups[0]['lr']]
best_model_wts = copy.deepcopy(model.state_dict())
这些列表用于记录训练过程中的关键信息:
变量 | 作用 |
---|---|
val_acc_history | 验证集准确率历史 |
train_acc_history | 训练集准确率历史 |
train_losses | 训练损失历史 |
valid_losses | 验证损失历史 |
LRs | 学习率变化历史 |
best_model_wts | 最佳模型参数快照(用于最后恢复) |
🔁 主训练循环:按 Epoch 运行
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
- 控制训练的总轮数;
- 每个 epoch 打印当前进度。
🔄 训练 + 验证阶段
for phase in ['train', 'valid']:
if phase == 'train':
model.train()
else:
model.eval()
- 每个 epoch 中都会先训练一遍,再验证一遍;
-
model.train()
:启用 BatchNorm 和 Dropout; -
model.eval()
:关闭这些层,用于评估。
🚀 数据遍历 + 前向传播 + 反向传播
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
loss = criterion(outputs, labels)
_, preds = torch.max(outputs, 1)
if phase == 'train':
loss.backward()
optimizer.step()
-
inputs
和labels
是一批图像和标签; -
zero_grad()
:清空梯度; -
with torch.set_grad_enabled(...)
:控制是否计算梯度; -
loss.backward()
和optimizer.step()
:只在训练阶段进行反向传播; -
preds
:预测结果(最大概率对应的类别);
📈 统计本轮 Loss 和 Accuracy
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(dataloaders[phase].dataset)
epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)
-
loss.item()
:取出标量值; -
inputs.size(0)
:batch size; -
torch.sum(preds == labels.data)
:统计正确预测的数量; -
epoch_loss
和epoch_acc
:本轮平均损失和准确率。
💾 保存最佳模型
if phase == 'valid' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
state = {
'state_dict': model.state_dict(),
'best_acc': best_acc,
'optimizer': optimizer.state_dict(),
}
torch.save(state, filename)
- 如果验证准确率比之前高,就更新最佳模型;
- 使用
copy.deepcopy()
防止引用被修改; -
torch.save(state, filename)
:保存为.pt
文件,包含模型权重、最优准确率、优化器状态。
📤 记录训练过程数据
if phase == 'valid':
val_acc_history.append(epoch_acc)
valid_losses.append(epoch_loss)
if phase == 'train':
train_acc_history.append(epoch_acc)
train_losses.append(epoch_loss)
- 把每个 epoch 的训练和验证指标保存下来,方便后续可视化。
📉 学习率调度器(可选)
print('Optimizer learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))
LRs.append(optimizer.param_groups[0]['lr'])
scheduler.step() # 学习率衰减
- 输出当前学习率;
- 添加到
LRs
列表中; -
scheduler.step()
:使用学习率调度器(如StepLR
,ReduceLROnPlateau
)进行学习率衰减。
⏱️ 打印训练耗时
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
- 显示训练总共花了多少时间;
- 输出验证集上的最好准确率。
🔄 返回最终模型和训练记录
model.load_state_dict(best_model_wts)
return model, val_acc_history, train_acc_history, valid_losses, train_losses, LRs
- 加载最佳模型参数;
- 返回模型对象和所有训练记录(可用于画图、分析等)。
🧪 示例调用方式
model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(
model=model_ft,
dataloaders=dataloaders,
criterion=criterion,
optimizer=optimizer_ft,
num_epochs=10,
filename='best_model.pth'
)