深度学习之图像分类实战
一、写在前面
本项目用于深入了解深度学习关于图像分类项目工作的大致流程和基本原理十分合适。主要代码附在最后。
二、以小见大
1.依赖库导入
python
import random # 生成随机数(配合固定种子)
import torch # PyTorch核心库(模型构建、张量计算)
import torch.nn as nn # PyTorch神经网络模块(层、损失函数)
import numpy as np # 数值计算(数组存储图片、标签)
import os # 文件路径操作(遍历文件夹、拼接路径)
from PIL import Image # 图片读取与处理(打开、缩放图片)
from torch.utils.data import Dataset, DataLoader # 数据加载核心接口
from tqdm import tqdm # 进度条显示(数据加载、训练过程可视化)
from torchvision import transforms # 图像预处理(格式转换、增强)
import time # 计时(统计每个epoch的训练时长)
import matplotlib.pyplot as plt # 可视化(绘制损失、准确率曲线)
from model import initialize_model # 导入预训练模型初始化函数
其中torch库和torch.nn库没有相互包含的关系,torch库负责张量构建,设备管理等等,而torch.nn库负责构建神经网络结构,定义损失函数,激活函数等。
2.固定随机种子(项目可复现的核心)
python
def seed_everything(seed):
# 1. 固定PyTorch CPU的随机种子(张量初始化、数据打乱等随机操作)
torch.manual_seed(seed)
# 2. 固定当前GPU的随机种子(单GPU场景:避免GPU计算的随机性)
torch.cuda.manual_seed(seed)
# 3. 固定所有GPU的随机种子(多GPU场景:确保多卡训练结果一致)
torch.cuda.manual_seed_all(seed)
# 4. 禁用cuDNN的自动优化算法(cuDNN会自动选高效算法,但可能导致结果不一致)
torch.backends.cudnn.benchmark = False
# 5. 强制cuDNN使用确定性算法(保证GPU每次计算结果相同)
torch.backends.cudnn.deterministic = True
# 6. 固定Python原生随机种子(避免Python自带随机操作的影响)
random.seed(seed)
# 7. 固定NumPy随机种子(数组生成、索引打乱等操作的随机性)
np.random.seed(seed)
# 8. 固定Python哈希种子(字典、集合等哈希结构的随机性,避免影响数据顺序)
os.environ['PYTHONHASHSEED'] = str(seed)
# 调用函数,固定种子为0(任意整数均可,只要每次运行相同)
seed_everything(0)
计算机的随机数本身就是伪随机,实际上还是通过算法计算得到的,固定随机种子可以让每次生成的随机数相同,让实验变得可以复现。
3.图像预处理(增强本质特征的识别)
python
# 定义图片统一尺寸:224×224(适配VGG、ResNet等预训练模型的输入要求)
HW = 224
# 训练集预处理:数据增强+格式转换(核心目的:避免过拟合)
train_transform = transforms.Compose(
[
transforms.ToPILImage(), # 步骤1:将NumPy数组(H×W×3)转为PIL图像(后续增强需此
格式)
transforms.RandomResizedCrop(224), # 步骤2:随机裁剪+缩放(生成不同尺寸的图片,模
拟不同拍摄角度)
transforms.RandomRotation(50), # 步骤3:随机旋转(-50°~50°,适应图片拍摄时的角度偏
差)
transforms.ToTensor() # 步骤4:转为PyTorch张量(格式:3×224×224,数值归一化到0~1
)
]
)
# 验证集预处理:仅格式转换(核心目的:保证验证结果客观)
val_transform = transforms.Compose(
[
transforms.ToPILImage(), # 步骤1:NumPy数组→PIL图像
transforms.ToTensor() # 步骤2:PIL图像→张量(无随机操作,避免干扰验证结果)
]
)
图像预处理的意义是减少过拟合的可能性,对图片进行裁剪,放缩和旋转等操作,让模型训练时可以识别更多训练集数据,增强对特征的提取能力。
对图像进行预处理分为三步:
图片数组numpy转为PIL图像
PIL图像进行接口调用对图片进行操作
PIL图像转为torch张量传入模型进行训练
所有图片的存储形式都是数组,但是图像增强的调用接口RandomResizedCrop和RandomRotation都是只有PIL图像才能进行的,神经网络模型只有维度不同于数组的torch张量才能在上面进行计算,所以最终还要进行torch张量的维度转换
4.自定义数据集(数据加载的核心)
python
class food_Dataset(Dataset):
# 初始化函数:传入数据路径和模式(train/val),完成数据读取和预处理绑定
def __init__(self, path, mode="train"):
self.mode = mode # 记录模式(区分训练/验证,后续绑定不同预处理)
self.X, self.Y = self.read_file(path) # 调用read_file读取图片和标签
self.Y = torch.LongTensor(self.Y) # 标签转为LongTensor(PyTorch分类任务要求标签格式)
# 绑定预处理函数:训练集用增强,验证集用普通转换
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
# 核心方法:读取文件夹中的图片和标签(图像分类数据加载的标准逻辑)
def read_file(self, path):
X = None # 存储所有图片:shape=(样本数, 224, 224, 3)(NumPy数组)
Y = None # 存储所有标签:shape=(样本数,)(每个元素是0~10的类别)
# 遍历11个类别(food-11数据集共11类食物,文件夹名是00~10,对应类别0~10)
for i in tqdm(range(11)):
file_dir = path + "/%02d" % i # 拼接每个类别的文件夹路径(如path/00、path/01)
file_list = os.listdir(file_dir) # 获取该类别下所有图片的文件名(如xxx.png)
# 初始化当前类别的图片和标签数组(dtype=np.uint8:像素值0~255,节省内存)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) # 单类别图片数组
yi = np.zeros(len(file_list), dtype=np.uint8) # 单类别标签数组(所有标签都是i)
# 遍历当前类别下的每张图片
for j, img_name in enumerate(file_list):
img_path = os.path.join(file_dir, img_name) # 拼接图片完整路径(如path/00/123.png
)
img = Image.open(img_path) # 打开图片(PIL格式)
img = img.resize((HW, HW)) # 缩放为224×224(统一尺寸,避免模型报错)
xi[j, ...] = img # 将图片存入数组(...表示自动匹配剩余维度,即224×224×3)
yi[j] = i # 给当前图片分配标签(文件夹名=类别,无需手动标注)
# 合并所有类别的数据(第一次循环初始化,后续循环拼接)
if i == 0:
X = xi # 第一次循环:直接赋值(避免拼接时维度不匹配)
Y = yi
else:
# 按样本维度拼接(axis=0:在"样本数"维度增加,比如100张+200张=300张)
X = np.concatenate((X, xi), axis=0)
Y = np.concatenate((Y, yi), axis=0)
print("读到了%d个数据" % len(Y)) # 打印总样本数(验证数据是否加载成功)
return X, Y
# 核心方法:按索引返回单个样本(DataLoader会自动调用此方法批量取数据)
def __getitem__(self, item):
img = self.X[item] # 取出第item张图片(shape=(224,224,3),NumPy数组)
label = self.Y[item] # 取出第item张图片的标签(0~10的整数)
img_transformed = self.transform(img) # 对图片做预处理(训练集增强,验证集不增强)
return img_transformed, label # 返回(预处理后的张量,标签),供模型训练
# 核心方法:返回数据集总长度(DataLoader需要知道总样本数,才能计算批次数量)
def __len__(self):
return len(self.X)
将图片数组转为模型输入的关键步骤,分为以下几步:
直接将所在文件夹转为标签
将维度转换为固定的224*224可以转为numpy存储
对dataset类的函数进行重写,配置好pytorch
5.自定义CNN模型
python
class myModel(nn.Module):
# 初始化函数:定义网络层结构(输入:类别数num_class,如11类食物)
def __init__(self, num_class):
super(myModel, self).__init__() # 调用父类nn.Module的构造函数(必须写)
# 网络结构设计逻辑:从"低级特征"到"高级特征"逐步提取
# 第一层卷积:3通道(RGB图片)→64通道(提取64种低级特征,如边缘、线条)
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 卷积核3×3,步长1,填充1(输出尺寸=输入尺寸)
self.bn1 = nn.BatchNorm2d(64) # 批量归一化:加速训练收敛,避免梯度消失(训练深层网
络必备)
self.relu = nn.ReLU() # 激活函数:引入非线性(让模型能学习复杂特征,否则和线性回归一样
)
self.pool1 = nn.MaxPool2d(2) # 最大池化:尺寸减半(224→112),保留关键特征,减少计
算量
# 卷积块1:64通道→128通道(提取更复杂的纹理特征)
self.layer1 = nn.Sequential( # Sequential:将多个层封装为一个模块,简化代码
nn.Conv2d(64, 128, 3, 1, 1), # 128×112×112
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2) # 尺寸减半→56×56
)
# 卷积块2:128通道→256通道(提取更高级的形状特征)
self.layer2 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1), # 256×56×56
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2) # 尺寸减半→28×28
)
# 卷积块3:256通道→512通道(提取最高级的语义特征,如食物的整体轮廓)
self.layer3 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1), # 512×28×28
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2) # 尺寸减半→14×14
)
self.pool2 = nn.MaxPool2d(2) # 最终池化:尺寸减半→7×7(最终特征图尺寸:512×7×7)
self.fc1 = nn.Linear(25088, 1000) # 全连接层1:512×7×7=25088(拉平后的特征维度)→1
000维
self.relu2 = nn.ReLU()
self.fc2 = nn.Linear(1000, num_class) # 全连接层2:1000维→num_class维(输出每个类别
的得分)
# 前向传播函数:定义数据在网络中的流动路径(必须写,模型的核心)
def forward(self, x):
# 输入x:shape=(batch_size, 3, 224, 224)(批次大小×通道×高度×宽度)
x = self.conv1(x) # 卷积:提取低级特征
x = self.bn1(x)
# 批量归一化:稳定训练
x = self.relu(x)
# 激活:引入非线性
x = self.pool1(x) # 池化:缩小尺寸
x = self.layer1(x) # 卷积块1:提取纹理特征
x = self.layer2(x) # 卷积块2:提取形状特征
x = self.layer3(x) # 卷积块3:提取语义特征
x = self.pool2(x) # 最终池化:得到512×7×7的特征图
x = x.view(x.size()[0], -1) # 拉平:(batch_size, 512×7×7) → (batch_size, 25088)
x = self.fc1(x)
# 全连接层1:压缩特征维度
x = self.relu2(x) # 激活:引入非线性
x = self.fc2(x)
# 全连接层2:输出类别得分(shape=(batch_size, 11))
return x
其中layer1,layer2,layer3这三个是特征提取的核心,分别对应低级,中级和高级提取 。整体对应下来是layer1看的是细节,像素,layer2看的是形状,layer3看的是整体语义,进行完整的判断。
最前面包括有self.conv1用来对刚传进来的图片进行维度转化,让其可以对接上layer1,最后面有self.fc1/fc2等全连接层,用于将提取出来的特征转换为最终的分类结果。
6.训练验证函数
python
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss_fn, save_path):
# 步骤1:将模型移到指定设备(GPU/CPU)------GPU训练速度是CPU的10~100倍
model = model.to(device)
# 初始化存储训练/验证结果的列表(用于后续可视化)
plt_train_loss = [] # 存储每轮训练损失
plt_val_loss = [] # 存储每轮验证损失
plt_train_acc = [] # 存储每轮训练准确率
plt_val_acc = []
# 存储每轮验证准确率
max_acc = 0.0
# 记录最高验证准确率(用于保存最优模型)
# 步骤2:迭代训练epochs轮(每一轮=训练+验证)
for epoch in range(epochs):
# 初始化本轮的损失和准确率计数器(每次轮次重置为0)
train_loss = 0.0
val_loss = 0.0
train_acc = 0.0
val_acc = 0.0
start_time = time.time() # 记录本轮训练开始时间(统计耗时)
# -------------------------- 训练阶段(核心:更新模型参数)-------------------------
model.train() # 模型设为训练模式:启用BatchNorm、Dropout等训练专用层
# 遍历训练集的每个批次(DataLoader自动批量返回数据,batch_size=16)
for batch_x, batch_y in train_loader:
# 将批次数据移到指定设备(GPU/CPU)------数据和模型必须在同一设备
x, target = batch_x.to(device), batch_y.to(device)
# 前向传播:模型预测(输入批次图片,输出每个类别的得分)
pred = model(x) # pred.shape=(16, 11)(16个样本,每个样本11类得分)
# 计算损失:预测结果与真实标签的差异(损失越小,预测越准)
train_bat_loss = loss_fn(pred, target)
# 反向传播:计算模型参数的梯度(告诉模型"哪些参数需要调整")
train_bat_loss.backward()
# 优化器更新参数:根据梯度调整网络权重(模型"学习"的核心步骤)
optimizer.step()
# 梯度清零:避免下一个批次的梯度累积(否则梯度会越来越大,导致训练不稳定)
optimizer.zero_grad()
# 累加本轮训练损失(.cpu().item():将GPU张量转为Python数值,避免计算图占用显存)
train_loss += train_bat_loss.cpu().item()
# 计算本批次训练准确率:
# argmax(pred.detach().cpu().numpy(), axis=1):取得分最高的类别(shape=(16,))
# 与真实标签对比,统计匹配的数量(正确预测的样本数)
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().
numpy())
# 计算本轮平均训练损失和准确率(平均损失=总损失/批次数量;准确率=正确样本数/总样本
数)
avg_train_loss = train_loss / len(train_loader)
avg_train_acc = train_acc / len(train_loader.dataset)
plt_train_loss.append(avg_train_loss)
plt_train_acc.append(avg_train_acc)
# -------------------------- 验证阶段(核心:评估模型泛化能力,不更新参数)-------------------------
model.eval() # 模型设为验证模式:禁用BatchNorm、Dropout(避免影响验证结果)
with torch.no_grad(): # 禁用梯度计算(节省显存,加速验证,且避免参数被误更新)
# 遍历验证集的每个批次
for batch_x, batch_y in val_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x) # 前向传播预测
val_bat_loss = loss_fn(pred, target) # 计算验证损失
val_loss += val_bat_loss.cpu().item() # 累加验证损失
# 计算本批次验证准确率(逻辑和训练集一致)
val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().
numpy())
# 计算本轮平均验证损失和准确率
avg_val_loss = val_loss / len(val_loader)
avg_val_acc = val_acc / len(val_loader.dataset)
plt_val_loss.append(avg_val_loss)
plt_val_acc.append(avg_val_acc)
# -------------------------- 模型保存(核心:只保存最优模型)-------------------------
# 只有当当前验证准确率高于历史最高时,才保存模型(避免保存过拟合的模型)
if avg_val_acc > max_acc:
torch.save(model, save_path) # 保存完整模型(可直接加载复用)
max_acc = avg_val_acc # 更新最高准确率记录
# -------------------------- 打印训练日志(核心:监控训练状态)-------------------------
print('[%03d/%03d] 耗时: %2.2f秒 | 训练损失: %.6f | 验证损失: %.6f | 训练准确率: %.6f | 验证
准确率: %.6f' % \
(epoch+1, epochs, time.time() - start_time, avg_train_loss, avg_val_loss, avg_train_acc
, avg_val_acc))
# -------------------------- 可视化训练结果(核心:直观判断训练效果)-------------------------
plt.figure(figsize=(12, 4)) # 创建画布(宽度12,高度4)
# 子图1:损失曲线
plt.subplot(1, 2, 1) # 1行2列,第1个子图
plt.plot(plt_train_loss, label='训练损失')
plt.plot(plt_val_loss, label='验证损失')
plt.title('损失曲线')
plt.xlabel('训练轮次')
plt.ylabel('损失值')
plt.legend() # 显示图例
# 子图2:准确率曲线
plt.subplot(1, 2, 2) # 1行2列,第2个子图
plt.plot(plt_train_acc, label='训练准确率')
plt.plot(plt_val_acc, label='验证准确率')
plt.title('准确率曲线')
plt.xlabel('训练轮次')
plt.ylabel('准确率')
plt.legend()
plt.show() # 显示图片
训练验证主要分为四个主要的模块
- 训练函数核心:model.train()+前向传播+反向传播+更新参数+梯度清零,目的就是让模型学习。
- 验证函数核心:model.eval()+torch.no_grad()+前向传播,在只进行前向传播的同时关闭梯度传播等,就可以计算出在验证集上的偏差情况。
- 模型保存逻辑:准确率对比+torch.save(model, save_path),避免保存过拟合的模型。
- 模拟情况可视化:通过日志和曲线判断模型状态。
7.参数配置和训练启动
python
# -------------------------- 1-------------------------
train_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\
labeled"
val_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\validation
"
# -------------------------- 2 -------------------------
train_set = food_Dataset(train_path, mode="train") # 训练集:启用训练预处理
val_set = food_Dataset(val_path, mode="val")
# 验证集:启用验证预处理
# 训练集加载器:batch_size=16(每次喂16张图片),shuffle=True(训练前打乱数据,避免顺
序影响)
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
# 验证集加载器:batch_size=16,shuffle=False(验证时不打乱,结果稳定)
val_loader = DataLoader(val_set, batch_size=16, shuffle=False)
# -------------------------- 3-------------------------
# 加载预训练模型:model_name="vgg"(VGG16),num_classes=11(11类食物),use_
pretrained=True(用预训练权重)
model, _ = initialize_model("vgg", num_classes=11, use_pretrained=True)
# 若不用预训练模型,可注释上一行,启用下面这行(使用自定义CNN)
# model = myModel(num_class=11)
lr = 0.001 # 学习率:控制参数更新的步长(太大→震荡不收敛,太小→训练太慢)
loss_fn = nn.CrossEntropyLoss() # 分类任务专用损失函数(自动结合Softmax,直接接收类别得
分)
# 优化器:AdamW(带权重衰减的Adam,抑制过拟合),优化模型所有可训练参数
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
device = "cuda" if torch.cuda.is_available() else "cpu" # 自动选择GPU/CPU
save_path = "model_save/best_model.pth" # 最优模型保存路径
epochs = 15 # 训练轮数:模型遍历整个训练集15次(轮数太多可能过拟合,太少可能未收敛)
# -------------------------- 4. -------------------------
train_val(model, train_loader, val_loader, device, epochs, optimizer, loss_fn, save_path)
这段是模型训练的开始:
- 数据路径配置:train_path和val_path存放数据的路径
- 数据集和数据加载器的初始化:对food_Dataset和DataLoader进行初始化定义,进行后续的数据读取。
- 模型和超参数的配置:控制模型训练尺度方向,包含学习率,损失函数等。
- 启动训练
三、全程逻辑
- 对dataset重写,数据进行初始化,按"文件夹名=标签名"读取有标签数据
- 对图片进行裁剪旋转等预处理进行数据增强
- 用dataloader进行成批的加载数据
- 模型学习从图片特征到标签的映射关系
- 多轮训练优化模型
- 用验证集检测泛化能力
- 保存最优模型结果并且以可视化图像表示