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.jpgdatasets.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(p0.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, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2, pin_memoryTrue)这段代码的核心在train_tf和val_tf的差异。训练集做水平翻转、随机旋转这些数据增强验证集只做 Resize 和归一化这样才能看到模型在真实图片上的泛化能力如果验证集也做随机增强val_acc 会一直上下跳过拟合信号根本看不出来。shuffleTrue只在训练集打开用于打乱每个 epoch 的样本顺序验证集不打乱保证评估结果与样本顺序无关。pin_memoryTrue在 GPU 训练时能减少 CPU 到 GPU 的数据拷贝耗时但 CPU 内存太小的时候不要开。num_workers在 Windows 下容易触发 worker 重启设 0 更稳定Linux 下可以设 2 或 4。常用参数建议如下参数建议值说明resize224x224与预训练模型输入保持一致batch_size32显存不足时降到 16num_workers2 或 4Windows 下优先设 0pin_memoryTrue只在 GPU 训练时开启shuffletrainTrue, valFalse保证验证结果稳定数据加载到这里后面所有模型和训练脚本都只依赖train_loader和val_loader不会再有图片路径问题。3. CNN 结构设计从三层卷积到预训练模型网络结构是设计报告里占比最大的一节。很多大作业只写“使用 ResNet”但说不清为什么替换全连接层、为什么调学习率。这里先给一个能跑通的基础 CNN再给预训练模型的改法两种结构共用同一套数据加载和训练代码。3.1 一个能跑通的基础 CNN 网络应该怎么搭CNN 在花卉图像识别里有效是因为卷积核逐层抽象特征底层看到边缘和颜色块中层看到花瓣纹理高层拼出完整花型。先写一个不依赖预训练权重的浅层 CNN结构透明容易在报告里画图也能尽早验证训练脚本本身有没有问题。import torch.nn as nn class FlowerCNN(nn.Module): def __init__(self, num_classes102): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), # 3x3, padding1 不改变分辨率 nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 224 - 112 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112 - 56 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56 - 28 nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 28 - 14 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(256 * 14 * 14, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这里特意让每个卷积层都使用 3x3 卷积、padding1特征图分辨率不降尺寸只由 MaxPool2d 负责减半这样每层输出好算出问题也好定位。四组卷积池化后224x224 输入变成 14x14 特征图展平后是 2561414再接两层全连接。BatchNorm 稳定中间层分布允许用更大学习率inplaceTrue只省显存不改计算结果。分类头的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(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features # 读原 fc 输入维度 model.fc nn.Linear(num_features, 102)到这里只改了最后一层fc。num_features从原模型读出来避免不同版本 torchvision 之间把维度写死。替换后前面所有卷积层仍使用预训练权重只有新分类头随机初始化。这时不能把整体学习率设成同一个值预训练参数和随机初始化头的最佳学习率不同常见做法是分类头 lr1e-3、特征提取层 lr1e-4下一章训练循环里会体现。硬件条件允许时也可以用timm库加载 EfficientNet 或 ConvNeXt但大作业验收重点通常不是模型多新而是“模型结构、训练参数、结果指标”三者能对上。3.3 源码文件怎么拆分模型结构才能跟报告对应起来提交材料里除了模型权重还要有源码和设计报告。建议把代码拆成四个文件而不是把几百行写进一个 train.pysrc/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_loader和val_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_classes102).to(device) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-2) scheduler CosineAnnealingLR(optimizer, T_max20) 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(dim1) 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(fepoch {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(dim1)取的是得分最高的类别不需要再做一次 softmaxsoftmax 是单调函数不影响 argmax 结果。保存模型时只在验证集提升时覆盖best_model.pth不要每轮都存否则连续跑 30 轮会写满几十个权重文件。4.3 数据增强参数和正则化参数怎么配花卉集的类间差异小、类内差异大数据增强直接扩充训练分布。常用参数可以按下表起步参数建议值使用说明RandomHorizontalFlipp0.5水平翻转对花这类近似对称物体有效RandomRotation12 度旋转太大容易产生黑边ColorJitterbrightness0.2, contrast0.2, saturation0.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(weightweights.to(device))计算all_train_labels要在构造训练集时同步收集如果直接使用train_ds.targets要确认train_ds是完整训练集而不是已经切出去的 subset。权重归一化后整体梯度不会被放大太多。需要强调类别权重不是越大越好。如果稀有类权重是常见类的几十倍训练会反复忽略常见类整体准确率反而下降。若加权后稀有类仍低再考虑 Focal Loss但大作业一般先确认加权方案有效。5.3 用类别权重和采样器时验证集该不该改部分同学会把过采样用到验证集上这是常见错误。验证集和测试集必须保持真实分布否则报告的召回率与线上效果不一致。过采样或欠采样只能用在训练阶段比如使用WeightedRandomSampler提高稀有类在每个 epoch 的采样概率。类别权重和过采样效果有重叠通常选一个即可。大作业评分往往更关注你对方案的判断力在报告里说明“为什么用类别权重而不是过采样”比直接堆模型更有说服力。5.4 调优时的实验顺序模型能跑但指标不理想时我一般按下述顺序调整避免一次改多个变量导致无法定位问题固定随机种子保持 train/val 划分不变。先用基础 CNN 或预训练模型跑一版记录 baseline。数据增强强度逐步提升每次只改一个参数。检查混淆矩阵调整稀有类别权重。用验证曲线判断是否继续训练。每次修改保存对应的 val_acc写报告时直接引用实验记录不需要重新跑旧配置。6. 最后一遍自检训练曲线、混淆矩阵与可复现报告6.1 训练曲线是判断模型是否欠拟合的第一张图提交前先把 loss 曲线画出来存到报告目录。matplotlib 代码通常只需要十行import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.plot(train_loss_history, labeltrain loss) plt.plot(val_loss_history, labelval loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True) plt.savefig(report/loss_curve.png, dpi150)看这张图时先确认 val_loss 在训练后期没有持续反弹。如果 val_loss 已经脱离最低点向上走就不要再加 epoch直接把之前保存的最优模型固定下来。报告里记录“最佳 epoch17”这类信息其他人复现时不需要重新搜参数。6.2 用混淆矩阵找低频类和混淆类训练结束后用验证集完整跑一遍保存所有预测结果再输出分类报告from sklearn.metrics import classification_report print(classification_report(all_labels, all_preds, digits3))报告会给出每个类别的 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 日志。本文还有配套的精品资源点击获取