news 2026/9/13 2:04:05

PyTorch CNN花卉图像识别实战:从数据增强到模型调优全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch CNN花卉图像识别实战:从数据增强到模型调优全流程

简介:面向计算机视觉课程设计与期末大作业的基于卷积神经网络的花卉图像识别项目,已通过导师指导并获得九十九分评价,适合计算机相关专业学生用于毕业设计、课程设计或期末项目,也适合希望快速上手图像分类实战的学习者参考。整套共包含十三个文件,压缩包约十点八二兆字节,主要包含六个源代码文件、一份设计报告文档、一份答辩演示文稿、环境配置文件、模型压缩包及说明文档等,覆盖数据预处理、模型构建、训练测试、界面识别和环境复现等完整流程,源码中还对数据读取和参数配置进行了封装,便于修改和扩展。目前已有一百四十七人学习下载。代码结构清晰且可运行,关键模块均提供源码,可灵活修改网络结构或数据路径;设计报告和演示文稿能辅助理解卷积神经网络原理、框架实现细节,并用于答辩汇报;模型包及说明文档支持快速加载权重复现结果,非常适合作为图像分类任务的起点模板,迁移到其他花卉或物体识别场景。即使编程基础较为薄弱,也能按照说明逐步完成项目复现。

1. 计算机视觉大作业选 CNN 花卉图像识别,需要先解决三件事

花卉图像识别是计算机视觉课程里最容易被低估的题目。看起来只是告诉模型“这张是菊花,那张是玫瑰”,实际跑一轮数据就会发现,不同品种的花在花瓣形状、颜色和纹理上差异很大,同一类花又受光照、角度、遮挡影响;用传统特征提取方法很难划清边界,CNN 却能端到端地学习感知特征。做这个大作业的典型路径是:Python 写全套训练与评估脚本,用公开花卉数据集,先搭一个基础 CNN 跑通流程,再换成预训练模型刷指标,最后用验证曲线和混淆矩阵支撑设计报告。正文按“数据准备、CNN 结构、训练调参、排错、报告输出”的顺序展开,提交时源码、模型文件和设计报告也按同一套逻辑整理,验收时不用来回翻文件。

2. 花卉图像数据集与 DataLoader:先把 python 环境和目录结构搭对

训练数据放不对,后面 CNN 结构再复杂都白搭。常见数据集像 Oxford 102 Flower 这类,图片量在几千到上万张,类别多、单类样本少,正好能暴露图像识别大作业最常见的过拟合问题。也有不少作业使用自采照片,画质会更参差。无论哪种来源,第一步都是把图片按类别放进独立目录,再配置 DataLoader,否则标注信息很容易在迭代中错位。

2.1 公开数据集的类别不均衡,和自采照片的清洗问题

公开数据集的主要坑是类别不均衡。以大作业里常见的 102 类花卉集为例,热门类别可能有上百张,冷门类别有时候只有三十张左右。直接按原始分布训练,模型会偏向样本多的类,整体准确率看着还行,但低频类别的 recall 会低得难看。自采照片的坑更直接:要先做人工清洗,把模糊、重复、带大面积水印、一图包含多株不同花的照片删掉。我一般会在清洗后按类目随机抽 10% 再人工复核一次,避免“玫瑰”和“月季”混在同一个文件夹。数据清洗阶段不解决,后面所有实验指标都会被带偏。

2.2 train/val/test 划分和标签映射,建议固定成文件

图像识别大作业里,train/val/test 的划分必须固定,不能每次跑脚本都换随机种子。目录结构建议按 torchvision 的 ImageFolder 规则组织:

data/train/class_001/xxx.jpg data/train/class_002/xxx.jpg data/val/class_001/xxx.jpg data/test/class_001/xxx.jpg

datasets.ImageFolder会按文件夹名字符串排序生成类别索引。一个很容易忽略的细节是:如果类名是 "1"、"10"、"2" 这种纯数字,排序结果不是你直觉里的自然顺序。所以我会把dataset.classes保存成label_map.json,训练前打印一遍,确认标注表里的类名和类别 ID 一一对应。手动划分数据集时,建议对每个类别按 8:1:1 分层抽取,而不是全量随机切。全量随机切在少数类上很容易出现某个类在验证集里只剩一两个样本,val_acc 会剧烈抖动。

2.3 用 ImageFolder 和 DataLoader 把图片预处理并装进模型

数据准备好后,用一段 Python 脚本把预处理、增强和 batch 加载一次完成。这里特别要注意 train 和 val 的 transform 必须分开:

from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((224, 224)), # 与预训练模型输入对齐 transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(12), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # ImageNet 统计量 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder('data/train', transform=train_tf) val_ds = datasets.ImageFolder('data/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)

这段代码的核心在train_tfval_tf的差异。训练集做水平翻转、随机旋转这些数据增强,验证集只做 Resize 和归一化,这样才能看到模型在真实图片上的泛化能力;如果验证集也做随机增强,val_acc 会一直上下跳,过拟合信号根本看不出来。shuffle=True只在训练集打开,用于打乱每个 epoch 的样本顺序;验证集不打乱,保证评估结果与样本顺序无关。pin_memory=True在 GPU 训练时能减少 CPU 到 GPU 的数据拷贝耗时,但 CPU 内存太小的时候不要开。num_workers在 Windows 下容易触发 worker 重启,设 0 更稳定,Linux 下可以设 2 或 4。常用参数建议如下:

参数建议值说明
resize224x224与预训练模型输入保持一致
batch_size32显存不足时降到 16
num_workers2 或 4Windows 下优先设 0
pin_memoryTrue只在 GPU 训练时开启
shuffletrain=True, val=False保证验证结果稳定

数据加载到这里,后面所有模型和训练脚本都只依赖train_loaderval_loader,不会再有图片路径问题。

3. CNN 结构设计:从三层卷积到预训练模型

网络结构是设计报告里占比最大的一节。很多大作业只写“使用 ResNet”,但说不清为什么替换全连接层、为什么调学习率。这里先给一个能跑通的基础 CNN,再给预训练模型的改法,两种结构共用同一套数据加载和训练代码。

3.1 一个能跑通的基础 CNN 网络,应该怎么搭

CNN 在花卉图像识别里有效,是因为卷积核逐层抽象特征:底层看到边缘和颜色块,中层看到花瓣纹理,高层拼出完整花型。先写一个不依赖预训练权重的浅层 CNN,结构透明,容易在报告里画图,也能尽早验证训练脚本本身有没有问题。

import torch.nn as nn class FlowerCNN(nn.Module): def __init__(self, num_classes=102): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), # 3x3, padding=1 不改变分辨率 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 224 -> 112 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112 -> 56 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56 -> 28 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 28 -> 14 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(256 * 14 * 14, 512), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

这里特意让每个卷积层都使用 3x3 卷积、padding=1,特征图分辨率不降,尺寸只由 MaxPool2d 负责减半,这样每层输出好算,出问题也好定位。四组卷积池化后,224x224 输入变成 14x14 特征图,展平后是 2561414,再接两层全连接。BatchNorm 稳定中间层分布,允许用更大学习率;inplace=True只省显存,不改计算结果。分类头的Dropout(0.5)是基础 CNN 控制过拟合最直接的手段,适配几千张的中等规模数据集。

基础 CNN 各层输出关系可以总结成表:

阶段输出尺寸对应语义
Conv2d 3→32 + BN + ReLU224x224x32边缘、颜色块
MaxPool2d112x112x32降低分辨率
Conv2d 32→64 + BN + ReLU112x112x64花瓣纹理
MaxPool2d56x56x64局部纹理聚合
Conv2d 64→128 + BN + ReLU56x56x128花型局部结构
MaxPool2d28x28x128下采样
Conv2d 128→256 + BN + ReLU28x28x256花型语义特征
MaxPool2d14x14x256交给分类器

3.2 换成 ResNet18 预训练模型,只需改输出层

基础 CNN 能跑通,但准确率常常不够;更常规的做法是换成 ImageNet 预训练模型。以 ResNet18 为例,修改量很小:

import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_features = model.fc.in_features # 读原 fc 输入维度 model.fc = nn.Linear(num_features, 102)

到这里只改了最后一层fcnum_features从原模型读出来,避免不同版本 torchvision 之间把维度写死。替换后,前面所有卷积层仍使用预训练权重,只有新分类头随机初始化。这时不能把整体学习率设成同一个值,预训练参数和随机初始化头的最佳学习率不同;常见做法是分类头 lr=1e-3、特征提取层 lr=1e-4,下一章训练循环里会体现。硬件条件允许时,也可以用timm库加载 EfficientNet 或 ConvNeXt,但大作业验收重点通常不是模型多新,而是“模型结构、训练参数、结果指标”三者能对上。

3.3 源码文件怎么拆分,模型结构才能跟报告对应起来

提交材料里除了模型权重,还要有源码和设计报告。建议把代码拆成四个文件,而不是把几百行写进一个 train.py:

src/models.py # CNN 定义和预训练模型加载 src/dataset.py # 数据集划分、DataLoader 构建 src/train.py # 训练入口与模型保存 src/eval.py # 验证、混淆矩阵、曲线绘制

模型定义单独放,训练脚本只调用模型名;换网络时只改 models.py 里的一个函数,不用动训练逻辑。这样设计报告里写“使用了 ResNet18,输出 102 维”,源码里能读到同一句话,不会出现报告结构图和实际代码不一致的情况。

4. 训练与超参数设置:让 CNN 在花卉集上稳定收敛

网络定义好后,训练参数决定能不能收敛。大作业里最常见的失败不是模型太差,而是学习率、优化器、batch_size 三者不匹配,导致 loss 震荡。这一章给出一套能直接复制的最小训练闭环,再说明每个参数为什么这样设。

4.1 交叉熵、优化器和学习率调度怎么选

花卉识别是标准多分类任务,损失函数用nn.CrossEntropyLoss()。PyTorch 的这个接口已经包含 softmax 和负对数损失,所以模型最后一层不需要再额外接 softmax,训练时直接拿 logits 输入 loss 即可。优化器建议用 AdamW,比 SGD 带动量少调一个动量参数,对课程作业足够稳定。学习率调度用CosineAnnealingLR,让学习率按周期逐渐降到接近零,比固定学习率更容易在训练后期把 val_acc 再往上顶一点。

4.2 标准训练循环模板:训练、验证、保存最优模型

把下面这段代码放进train.py,配合前面的train_loaderval_loader,就能跑通完整流程。

import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = FlowerCNN(num_classes=102).to(device) criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2) scheduler = CosineAnnealingLR(optimizer, T_max=20) best_acc = 0.0 for epoch in range(30): model.train() # 切到训练模式,BN 和 Dropout 生效 train_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() * images.size(0) model.eval() # 切到验证模式 correct = 0 total = 0 with torch.no_grad(): # 验证不计算梯度 for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) val_acc = correct / total scheduler.step() if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f'epoch {epoch + 1} | loss {train_loss / len(train_loader.dataset):.4f} | val_acc {val_acc:.4f}')

这段代码有三个容易漏的点。第一,model.train()model.eval()必须成对出现,否则 BatchNorm 和 Dropout 在验证阶段仍按训练状态运行,结果会虚高或抖动。第二,验证循环用torch.no_grad()包住,避免计算梯度浪费显存。第三,outputs.argmax(dim=1)取的是得分最高的类别,不需要再做一次 softmax,softmax 是单调函数,不影响 argmax 结果。保存模型时只在验证集提升时覆盖best_model.pth,不要每轮都存,否则连续跑 30 轮会写满几十个权重文件。

4.3 数据增强参数和正则化参数怎么配

花卉集的类间差异小、类内差异大,数据增强直接扩充训练分布。常用参数可以按下表起步:

参数建议值使用说明
RandomHorizontalFlipp=0.5水平翻转,对花这类近似对称物体有效
RandomRotation12 度旋转太大容易产生黑边
ColorJitterbrightness=0.2, contrast=0.2, saturation=0.2模拟不同光照环境
weight_decay1e-2减缓权重过大导致的过拟合
Dropout0.5只加在分类头
label smoothing0.05如果验证阶段过于自信可调整

ColorJitter时不要把饱和度和色相调太重,因为部分花的类别标签就是靠颜色区分的,增强过度会破坏关键信息。数据集本身只有几千张时,不建议一上来就用 MixUp 或 CutMix。先用基础增强把 train_acc 跑到 90% 以上,再观察 train 和 val 的差距,决定要不要加强正则化。

5. 训练排错与调优:从欠拟合、过拟合到类别不平衡

训练脚本跑通只代表流程没问题,不代表结果能用。这一章按“先判断现象、再改参数”的顺序处理常见问题,其中类别不平衡是花卉数据集最容易忽略的一环。

5.1 看 train/val 的差距,先判断问题在哪一层

训练过程中会出现各种不收敛现象。常用排查表如下:

现象可能原因优先检查项
train_loss 不下降学习率过大或过小打印每轮 lr,检查梯度范数
train_acc 高,val_acc 低过拟合,数据增强不足看 val_loss 是否在升高
val_acc 始终等于 1/类别数标签映射错或输出维度错打印预测结果与真实标签
train/val 都低,提高很慢模型容量不足先换预训练模型
loss 来回抖动batch_size 太小或样本不均衡增大 batch,调整类别权重

“train_acc 高、val_acc 低”在几千张的花卉集上最常出现。遇到它不要急着加网络层数,先把数据增强强度提上来,再降学习率,然后看是否需要增加 Dropout。反过来,如果 train_loss 一直不降,多一半是学习率或数据预处理的问题,可以先用很小的学习率跑 5 个 epoch 观察 loss 是否缓慢下降,再逐步调大。

5.2 类别不平衡会导致平均准确率虚高

花卉数据集普遍存在长尾分布,少数类别样本数量只有多数的三分之一。直接训练时模型会偏向样本多的类,平均准确率不低,但打印出混淆矩阵后会发现稀有花类的 recall 很低。针对这种情况,常用做法是给CrossEntropyLoss传入类别权重。权重计算在训练开始前完成:

from collections import Counter import torch counts = Counter(all_train_labels) # 记录每个类别的样本数 num_classes = max(all_train_labels) + 1 weights = torch.zeros(num_classes) for c in range(num_classes): n = counts.get(c, 1) weights[c] = 1.0 / (n ** 0.5) # sqrt 加权,防止稀有类权重过大 weights = weights / weights.mean() # 权重均值回到 1 criterion = nn.CrossEntropyLoss(weight=weights.to(device))

计算all_train_labels要在构造训练集时同步收集;如果直接使用train_ds.targets,要确认train_ds是完整训练集而不是已经切出去的 subset。权重归一化后,整体梯度不会被放大太多。需要强调,类别权重不是越大越好。如果稀有类权重是常见类的几十倍,训练会反复忽略常见类,整体准确率反而下降。若加权后稀有类仍低,再考虑 Focal Loss,但大作业一般先确认加权方案有效。

5.3 用类别权重和采样器时,验证集该不该改

部分同学会把过采样用到验证集上,这是常见错误。验证集和测试集必须保持真实分布,否则报告的召回率与线上效果不一致。过采样或欠采样只能用在训练阶段,比如使用WeightedRandomSampler提高稀有类在每个 epoch 的采样概率。类别权重和过采样效果有重叠,通常选一个即可。大作业评分往往更关注你对方案的判断力,在报告里说明“为什么用类别权重而不是过采样”,比直接堆模型更有说服力。

5.4 调优时的实验顺序

模型能跑但指标不理想时,我一般按下述顺序调整,避免一次改多个变量导致无法定位问题:

  1. 固定随机种子,保持 train/val 划分不变。
  2. 先用基础 CNN 或预训练模型跑一版,记录 baseline。
  3. 数据增强强度逐步提升,每次只改一个参数。
  4. 检查混淆矩阵,调整稀有类别权重。
  5. 用验证曲线判断是否继续训练。

每次修改保存对应的 val_acc,写报告时直接引用实验记录,不需要重新跑旧配置。

6. 最后一遍自检:训练曲线、混淆矩阵与可复现报告

6.1 训练曲线是判断模型是否欠拟合的第一张图

提交前先把 loss 曲线画出来,存到报告目录。matplotlib 代码通常只需要十行:

import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.plot(train_loss_history, label='train loss') plt.plot(val_loss_history, label='val loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.grid(True) plt.savefig('report/loss_curve.png', dpi=150)

看这张图时,先确认 val_loss 在训练后期没有持续反弹。如果 val_loss 已经脱离最低点向上走,就不要再加 epoch,直接把之前保存的最优模型固定下来。报告里记录“最佳 epoch=17”这类信息,其他人复现时不需要重新搜参数。

6.2 用混淆矩阵找低频类和混淆类

训练结束后,用验证集完整跑一遍,保存所有预测结果,再输出分类报告:

from sklearn.metrics import classification_report print(classification_report(all_labels, all_preds, digits=3))

报告会给出每个类别的 precision、recall、f1-score。从输出中找出 recall 较低的三五个类别,回到原始图片看是样本太少、遮挡严重还是标注错误。这部分分析放到设计报告的实验章节,比只贴 val_acc 更能体现对图像识别问题的理解。找到具体问题后,再回到第五章的类别权重或数据增强去调,而不是随机试参数。

6.3 提交前固定环境版本,写一份可运行的 README

大作业资料里不仅要放训练好的模型,还要保证别人能重新跑出同一份结果。常见做法是在 README 中写清楚 Python 版本、torch 和 torchvision 版本、数据集目录结构、训练命令和测试命令。torch 与 torchvision 的版本尤其要固定,因为不同版本对预训练模型加载接口有差异。模型文件名建议带指标和时间,例如resnet18_flower_0.913_20250501.pth,避免和多轮训练产生的其他权重混淆。运行eval.py打印出的数字、报告表格、模型文件名里的准确率,三者保持一致,这份大作业在验收时就不容易失分。所以提交前最后做的一件事,是用 README 里的同一条命令重新跑一遍 eval,而不是只信任之前打印的 val_acc 日志。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 1:56:59

2023年AIGC论文助手TOP10评测与学术写作指南

1. AIGC论文助手市场现状与需求分析2023年被称为AIGC(AI Generated Content)元年,随着大语言模型的爆发式发展,学术写作领域正在经历前所未有的变革。根据最新调研数据显示,全球已有超过67%的研究生尝试使用AI工具辅助…

作者头像 李华