
简介一套基于 Python 与卷积神经网络CNN的花卉图像识别完整项目面向计算机视觉课程设计、毕业设计及期末大作业场景尤其适合计算机相关专业学生和需要项目实战的初学者。项目以 CNN 为算法核心覆盖数据预处理、特征提取、模型构建、训练调参、测试评估与 GUI 图形界面识别等完整流程并提供命令行与可视化两种使用方式训练好的模型与源码均已打包确保可直接运行帮助使用者快速理解图像分类任务从数据集到模型部署的各个环节。包体共 13 个文件包含 6 个 Python 脚本分别负责模型构建、训练、推理测试、数据记录与图形界面等、1 份 Word 设计报告、1 份 PPT 答辩演示以及环境配置文件、说明文档和模型压缩包整体大小约 10.82MB结构清晰便于分类查阅。现有 148 人学习使用适合按步骤复现并迁移到相似任务中。通过该资源用户可获得接近满分的项目范例包括完整可运行代码、已训练模型、设计报告与答辩演示材料无需从零调参即可体验深度学习图像识别项目也可在此基础上针对不同花卉数据集进行扩展改造直接支撑课程答辩或文档撰写。1. 计算机视觉大作业从数据集到设计报告的一条龙方案期末前两周接到这门课的大作业要求基于 CNN 的花卉图像识别要交源码、模型和设计报告。这类题目在“计算机视觉大作业”里出现频率极高——任务本身不难但坑全藏在数据集处理、模型选型和报告组织里。你以为难点在“识别”真正卡住人的往往是标签错位、训练不收敛、显存不够以及报告写得像流水账。这篇文章讲的是我做过的一套完整路径用什么数据集、怎么搭 CNN、参数怎么设、哪里最容易翻车以及设计报告怎么写得让老师愿意给高分。适合正在做课程设计、需要交源码和报告的本硕学生也适合想快速拿一个图像分类项目练手的入门开发者。2. 数据集与预处理花卉识别项目的起点也是决定结果上限的地方2.1 选数据集的标准类别数、每类样本量和标注质量花卉图像识别最常用的公开数据集有两个一个是 Oxford 102 Flower102 个类别、每类 40 到 258 张图不等类别多、相似度高适合做课程设计撑起报告篇幅另一个是 TensorFlow 示例数据集 flower_photos5 个类别雏菊、蒲公英、玫瑰、向日葵、郁金香每类约 700 张适合快速验证流程。我的建议是课程设计选 Oxford 102时间紧就选 5 类的 flower_photos。判断一个数据集适不适合你的作业看三点类别数是否和你报告的实验设计匹配、每类样本是否均衡、是否带官方划分。Oxford 102 不带官方 train/test 划分需要自己拆这反而是好事——你可以在报告里写“数据划分策略”这一节老师很吃这一套。注意 102 类的相似度很高比如某些菊科花卉之间只差花瓣形态这会让准确率上不去所以报告里模型选择部分要提前铺垫“类别间相似度高”这个理由。2.2 目录组织让 ImageFolder 直接吃你的数据PyTorch 的 torchvision.datasets.ImageFolder 是这类任务的标准入口但它对目录结构有硬性要求每个类别一个子文件夹文件夹名就是类别名。常见做法是先手动建好目录再用脚本按类别复制图片# 处理 flower_photos 的经典目录结构 # 原始结构flower_photos/daisy/*.jpg, flower_photos/dandelion/*.jpg # 目标结构data/train/daisy/*.jpg, data/val/daisy/*.jpg, data/test/daisy/*.jpg mkdir -p data/train data/val data/test for class in daisy dandelion rose sunflower tulip; do mkdir -p data/train/$class data/val/$class data/test/$class done这段脚本的作用是先把目标目录骨架建出来等会儿用 Python 脚本做随机划分。这里有个容易踩的坑文件夹名必须用英文用中文名会导致某些环境下的路径编码报错尤其是 Windows 上跑 PyTorch 的同学中文路径会让部分 PIL 版本读图失败。2.3 划分脚本随机种子是唯一能救你的东西import os import random import shutil from collections import defaultdict random.seed(42) # 固定种子保证报告里的数字可复现 src_root flower_photos train_root data/train val_root data/val test_root data/test train_ratio, val_ratio 0.7, 0.15 # 剩下 0.15 给测试集 for class_name in os.listdir(src_root): class_path os.path.join(src_root, class_name) if not os.path.isdir(class_path): continue imgs [f for f in os.listdir(class_path) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) n_train int(len(imgs) * train_ratio) n_val int(len(imgs) * val_ratio) for i, img in enumerate(imgs): if i n_train: dst os.path.join(train_root, class_name, img) elif i n_train n_val: dst os.path.join(val_root, class_name, img) else: dst os.path.join(test_root, class_name, img) shutil.copyfile(os.path.join(class_path, img), dst) print(train:, sum(len(os.listdir(os.path.join(train_root, c))) for c in os.listdir(train_root)))这段脚本的逻辑是按比例随机抽样然后复制到对应目录。关键点是 random.seed(42) 必须放在 shuffle 之前否则每次跑结果都不一样。划分比例我一般用 7:1.5:1.5不要用 8:1:1因为花卉数据集类别多、相似度高验证集太小会让 early stopping 的判断不稳定。复制而不是移动原文件这样原始数据还在出问题随时能回来重分。2.4 数据增强策略翻转变换加归一化别做过度裁剪from torchvision import transforms # 训练集增强 归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证/测试集只做 resize 和归一化不做任何随机增强 val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])transforms 里每个操作的顺序是固定的先 Resize 统一尺寸再做随机增强再转 Tensor最后归一化。RandomHorizontalFlip 和 RandomRotation 是保真增强不会改变花的类别语义裁剪类增强比如 RandomResizedCrop建议谨慎用因为花卉图像可能出现花在边缘、裁剪后主体丢失的情况。归一化的 mean 和 std 是 ImageNet 的标准值用预训练模型时不能改——后面讲模型迁移时会再强调一次。3. CNN 模型选型与训练为什么迁移学习是大作业的最优解3.1 从零搭 CNN 的诱惑和代价很多同学觉得“自己搭的 CNN 才是真本事”于是写了五六层卷积加全连接的网络然后训练两小时准确率卡在 60% 上不去。问题不是你代码写错了而是数据量不够——Oxford 102 每类平均才几十张图从零训练一个深层 CNN 必然过拟合。这不是能力问题是统计规律。如果你确实想展示对 CNN 结构的理解可以搭一个迷你 CNN 作为 baseline和迁移学习做对比实验。这比只用一个模型更有报告价值import torch.nn as nn class MiniCNN(nn.Module): def __init__(self, num_classes102): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), # 224x224 - 224x224 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # - 112x112 nn.Conv2d(32, 64, kernel_size3, padding1), # - 112x112 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # - 56x56 nn.Conv2d(64, 128, kernel_size3, padding1), # - 56x56 nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # - 28x28 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))这个迷你 CNN 有 5 层带参数的层结构上覆盖了卷积、池化、ReLU、Dropout 这些基础组件。padding1 让卷积不改变尺寸MaxPool2d 每次减半1282828 是最后特征图的展平长度——改输入尺寸时必须同步重算这个数这是新手最容易搞错的地方。Dropout(0.5) 是防止过拟合的关键但要注意推理时 Dropout 会被自动关闭PyTorch 的 model.eval() 会处理这件事。3.2 迁移学习用预训练 ResNet18 替换分类头真正的重头戏是迁移学习。用 torchvision 里预训练好的 ResNet18 或 ResNet34把最后一层全连接换成你自己的分类器。对花卉识别这种细粒度分类任务ResNet18 比 ResNet50 更合适——不是因为 50 不好是因为数据集太小50 的参数更多、更容易过拟合训练时间也更长。import torchvision.models as models # 加载预训练模型pretrainedTrue 会下载 ImageNet 权重 model models.resnet18(pretrainedTrue) # 冻结所有层参数只训练最后一层 for param in model.parameters(): param.requires_grad False # 替换最后一层全连接 num_ftrs model.fc.in_features # ResNet18 是 512 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(num_ftrs, 102) # 102 类花卉 )这段代码的做法是先加载在 ImageNet 上预训练好的 ResNet18冻结前面所有卷积层requires_gradFalse再把最后一层换成 Dropout 加 Linear。冻结的原因很直接——前面层学到的是通用特征边缘、纹理、形状对花卉同样适用没必要重新学只有最后分类层需要适应你的 102 类。这个方案在小数据集上通常能到 85% 以上的测试准确率而从头训同样数据大概只有 60% 多一点。注意这里的 102 是类别数如果你的数据集是 5 类就要改成 5否则最后一层维度不匹配会直接报错。3.3 训练循环的关键参数学习率、batch size 和超参数怎么调import torch import torch.optim as optim from torch.optim.lr_scheduler import StepLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() # 只优化分类头因为前面的层被冻结了 optimizer optim.Adam(model.fc.parameters(), lr0.001) # 每 5 个 epoch 学习率乘以 0.5 scheduler StepLR(optimizer, step_size5, gamma0.5) num_epochs 30 best_acc 0.0 for epoch in range(num_epochs): model.train() running_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) scheduler.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for val_inputs, val_labels in val_loader: val_inputs, val_labels val_inputs.to(device), val_labels.to(device) val_outputs model(val_inputs) _, predicted torch.max(val_outputs, 1) total val_labels.size(0) correct (predicted val_labels).sum().item() val_acc correct / total print(fEpoch {epoch1}: loss{running_loss/len(train_loader.dataset):.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(Best val acc:, best_acc)这个训练循环是标准骨架。几个关键参数的解释optimizer 用 Adamlr0.001 是默认值也是最稳的起点。如果发现 loss 震荡厉害把 lr 降到 0.0003如果收敛太慢先试 0.003。学习率是深度学习里最“玄学”的参数但按 3 倍递减法找一般不会翻车。StepLR 每 5 个 epoch 把学习率乘 0.5作用是后期收敛更细。你可以在报告里画 lr 变化曲线这比在正文里写“我调了参”有说服力得多。batch size 初始设 32 或 64。显存不够就降到 16但注意 batch size 变了学习率也要跟着变——后面避坑章节会专门讲。best_model.pth 只在验证集准确率创新高时保存防止最后几个 epoch 过拟合把好模型覆盖掉。冻结前面层意味着梯度只在 model.fc 里流动反向传播仍然会过完整网络但前面层不更新所以训练速度不会比全量微调快太多。3.4 评估top-1 准确率不够用混淆矩阵才是重点花卉识别大作业里最常见的评估错误是只看 top-1 准确率。但 102 类花卉相似度极高模型真正答对但排名第二的情况很常见。很多课程的老师会追问“你的模型为什么把这种菊花认成那种菊花”这时候你有混淆矩阵就能直接回应。import matplotlib.pyplot as plt import numpy as np from sklearn.metrics import confusion_matrix all_preds, all_labels [], [] model.eval() with torch.no_grad(): for inputs, labels in test_loader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) # 打印每一类的准确率找到最容易被混淆的类别 class_acc cm.diagonal() / cm.sum(axis1) worst_idx np.argsort(class_acc)[:5] print(最易出错的 5 个类别索引:, worst_idx) print(对应准确率:, class_acc[worst_idx])这段代码会告诉你模型在哪几个类别上表现最差。通常会发现是外观相似的菊科或蔷薇科花卉。报告里放一张混淆矩阵热力图再解释“这两类花瓣颜色分布近似导致特征混叠”立刻就把一个普通作业拉升到“有实验分析”的档次。如果你用了 top-5 准确率可以顺便算一下一般会比 top-1 高出 8 到 12 个百分点这也是值得写进报告的数据。4. 花卉识别项目避坑指南现象、原因和根治办法4.1 训练 loss 是 nan 或者不下降现象第一个 epoch 打印的 loss 就是 nan或者连续十个 epoch loss 卡在同一个值附近不动。原因分两种loss 为 nan 十有八九是输入数据没有归一化或者学习率太大loss 不降则往往是归一化参数和预训练模型不匹配。很多同学从网上抄了一个训练代码但 mean 和 std 用的是别人数据集的值自己的数据分布对不上。解决检查 transforms 里有没有 ToTensor 和 Normalize顺序必须是先 ToTensor 再 Normalize。预训练 ResNet 的归一化必须用 ImageNet 的 mean/std也就是 0.485/0.456/0.406 和 0.229/0.224/0.225。如果确认归一化没问题把 lr 从 0.001 降到 0.0003 试一次。4.2 验证集准确率远低于训练集现象训练集准确率到了 95%验证集却只有 70%而且随着 epoch 增加差距还在拉大。原因这就是典型的过拟合。你的模型把训练集的背景、拍摄角度等无关特征也学进去了。冻结核层只训分类头的方案能缓解但如果你把整个网络都解冻了requires_gradTrue过拟合马上回来。解决三种手段叠加使用。第一确认数据增强只加在训练集上验证和测试集不要加随机翻转旋转第二把 Dropout 加到 0.5 或 0.6第三提前停止——当验证集准确率连续 7 到 10 个 epoch 不再上升就保存模型退出而不是跑满预设的 30 轮。早停的代码很简单记录 best_acc连续 N 轮没更新就 break。4.3 文件夹里混入了损坏图片导致 DataLoader 报错现象训练到一半突然报错说某个图片无法加载或者 ImageFolder 初始化时直接抛 RuntimeError。原因数据集中存在损坏的图片文件或者扩展名是 .jpg 但实际编码格式不是 JPEG。这在从网上下载的数据集里非常常见尤其是用爬虫凑数据的情况。解决在划分数据集之前先扫一遍所有文件用 PIL 验证能不能正常打开from PIL import Image import os bad_files [] for root, dirs, files in os.walk(flower_photos): for f in files: if f.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(root, f) try: img Image.open(path) img.load() except Exception: bad_files.append(path) print(损坏文件数:, len(bad_files)) for p in bad_files[:10]: print(p)这段代码会列出所有打不开的图片。处理方式很简单删除或者移动到 backup 文件夹。这个检查必须在划分数据集之前做否则损坏文件被分到了训练集模型又训练了半小时才发现浪费时间。4.4 显存溢出调小 batch size 后效果变差现象batch size 从 32 降到 16 以后模型训练到同样的 epoch 数验证集准确率明显下降。原因batch size 变小意味着每个 batch 的梯度估计噪声变大。很多同学只降了 batch size 没降学习率Adam 对 batch size 变化没那么敏感但它依然是个暗坑——batch size 大的时候可以用大步长batch size 小了步长太大反而震荡。解决如果显存不够建议的调整顺序是换更小的输入尺寸224 降到 160 或 128但会牺牲一点准确率用梯度累积模拟更大的 batch或者换 ResNet18 而不是 ResNet34。调整 batch size 后观察 loss 曲线如果震荡明显把 lr 同步往下降。如果用的是只训分类头的方案batch size 对这个坑的影响会小很多因为可训练参数少。4.5 保存的模型文件加载后结果对不上现象训练时准确率 90%重新加载 best_model.pth 做测试时准确率只有 40%。原因保存模型时没有同时保存类别映射。ImageFolder 的类别顺序是按文件夹名字典序排的比如 daisy 在第 0 位、dandelion 在第 1 位。加载模型的机器上如果目录遍历顺序不同预测结果和真实标签就对不上了。解决训练结束后把 class_to_idx 存成 json 文件import json from torchvision.datasets import ImageFolder dataset ImageFolder(data/train) class_to_idx dataset.class_to_idx with open(class_to_idx.json, w) as f: json.dump(class_to_idx, f, indent2)注意 class_to_idx 是基于训练集的目录。验证和测试集目录里的类别名必须和训练集完全一致否则映射会错位。加载模型做推理时先读这个 json 文件再用同一个映射做预测就能保证结果可复现。这是很多人做作业时忽略的细节但在课程的验收演示环节非常容易出问题。5. 从跑通到能交设计报告的写法与答辩的四个高频追问代码跑通只是完成了这个大作业的一半另一半是把设计报告写到让老师相信你“真的理解了这个项目的每一行代码”。课程设计报告的常规结构是问题定义、数据说明、模型设计、实验与结果、总结与展望。其中数据说明里放两张图——类别分布图和增强前后的对比图就能撑起一页半的篇幅。模型设计部分不要只贴一行 ResNet18 的调用代码。对比实验是拉开分差的关键一个 MiniCNN baseline 加一个迁移学习主模型两张训练曲线放在一起标注出各自的最佳验证准确率然后写一段“为什么迁移学习在小数据集上占优”的分析。这段分析里提 ImageNet 预训练特征对通用边缘纹理的抽象能力再提 Fine-tuning 和特征冻结的差别——你理解了这个老师就不会在答辩时卡你。答辩时被问得最多的四个问题提前准备好答案。第一“为什么选 ResNet18 不选 ResNet50”——数据集小、ResNet50 参数多更容易过拟合、显存和学生卡训练时间都有限制。第二“数据增强为什么有效”——增加了训练样本的表征多样性降低了模型对翻转角度和拍摄位置的过拟合。第三“log loss 曲线怎么解释”——用你训练过程里保存的 loss 值画曲线指出三个阶段快速下降、平台期、微调期。第四“你的模型哪些类容易混”——把混淆矩阵里最差的 5 个类别拿出来解释说你观察了这些类的花形相近、颜色分布统计相似。收尾说一个我的习惯做完这个项目后我养成了固定随机种子加记录所有超参数的习惯包括每次实验的 lr、batch size、epoch、数据增强参数都记在一个 markdown 文件里。后面不管做课程作业还是实验室项目回去翻这些记录都比翻聊天记录管用。报错时先查是不是数据的问题再查是不是模型的问题这条排查顺序帮我省过不止一个下午。希望帮到你。本文还有配套的精品资源点击获取