多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于CNN的花卉识别项目实战:从源码到部署的完整指南

基于CNN的花卉识别项目实战:从源码到部署的完整指南 简介这份资源面向Python与深度学习入门者、计算机视觉方向的学生及需要完成毕业设计或课程大作业的开发者提供一套基于卷积神经网络CNN的花卉识别完整项目帮助读者理解图像分类从数据准备到模型推理的全流程。压缩包共48个文件约186.15MB以17个py源码、11个png图表、7个txt训练记录、5个xml配置、4个h5模型为主另含md说明与jpg示例图覆盖数据划分、训练、测试与推理等环节。项目包含CNN与MobileNet两类模型权重配套训练过程记录、结果对比图与热力图便于分析模型表现源码带注释新手也能看懂部署后即可运行。目前已有425人学习下载适合作为高分项目参考也可用于掌握卷积层、池化层与全连接层等核心概念。1. 花卉识别项目拆包一份能跑通的 CNN 源码到底长什么样做图像分类的同行大概都有过这种经历网上搜「卷积神经网络花卉识别代码」下回来的压缩包要么缺数据集、要么路径写死、要么训练脚本和推理脚本对不上号跑起来直接报FileNotFoundError。这份 Python 基于卷积神经网络 CNN 实现的花卉识别项目把源码、数据集、训练好的模型权重打包在一起属于那种「解压完改个路径就能跑」的完整工程。它解决的核心问题不是教你 CNN 是什么而是给你一套可复现的端到端流程数据怎么组织、模型怎么搭、训练怎么调、推理怎么接。适合正在做课程设计、毕设或者想拿一个干净 baseline 快速验证自己想法的从业者。下面我按实际拆包顺序把这份资源从目录结构到训练推理全流程讲透顺带把几个容易翻车的地方标出来。2. 环境与目录先把 Python、依赖和数据结构对齐2.1 环境选型为什么建议 Python 3.8 PyTorch 而不是 TensorFlow拿到这份源码第一件事不是急着python train.py而是确认环境。花卉识别这类中小规模图像分类任务框架选择直接影响你后面调参的顺手程度。这份项目用的是 PyTorch 生态常见做法是配 Python 3.8 到 3.10 之间的版本太新的 Python 3.12 有时会在装老版本 torchvision 时卡在编译环节。为什么优先 PyTorch 而不是 TensorFlow一是这份源码的模型定义、数据加载、训练循环都是按 PyTorch 的nn.Module和DataLoader写的换框架等于重写二是花卉数据集规模不大PyTorch 的动态图调试起来更直观打印中间张量形状不用先建静态图。如果你机器上有 GPU装对应 CUDA 版本的 torch 能省不少训练时间纯 CPU 也能跑只是训练轮次要拉长。安装依赖我一般直接走 pip把版本钉死避免玄学问题# 建议在虚拟环境里操作避免污染全局包 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖版本按你 CUDA 情况调整 pip install torch1.13.1 torchvision0.14.1 pip install numpy pillow matplotlib scikit-learn tqdm这里torch和torchvision版本要匹配1.13.1 配 0.14.1 是官方对应关系乱配会出现ImportError。tqdm用来显示训练进度条scikit-learn一般在画混淆矩阵时用到。装完跑一句python -c import torch; print(torch.cuda.is_available())返回 True 说明 GPU 可用False 就老老实实 CPU 跑。2.2 目录结构数据集、源码、模型权重三件套怎么摆解压后典型结构长这样不同打包方式可能略有出入但核心就三块flower_cnn/ ├── dataset/ │ ├── train/ │ │ ├── daisy/ │ │ ├── dandelion/ │ │ ├── rose/ │ │ ├── sunflower/ │ │ └── tulip/ │ └── val/ │ ├── daisy/ │ └── ...同上五类 ├── model/ │ └── flower_cnn.pth # 训练好的权重 ├── train.py ├── predict.py ├── dataset.py # 自定义 Dataset 类 ├── model.py # CNN 网络定义 └── requirements.txt数据集按类别分文件夹这是torchvision.datasets.ImageFolder的标准吃法——文件夹名就是类别标签不用额外写 label 映射文件。五类花分别是 daisy雏菊、dandelion蒲公英、rose玫瑰、sunflower向日葵、tulip郁金香每类几百张不等。train 和 val 分开是为了验证集不参与梯度更新能真实反映泛化能力。提示如果你拿到的数据集只有一个大文件夹没分 train/val别直接开跑。先按 8:2 切分否则验证集和训练集混在一起准确率虚高到 99% 都是假的。2.3 路径配置相对路径和绝对路径的取舍源码里最容易翻车的就是路径。常见写法是在train.py顶部定义import os # 用脚本所在目录做基准避免从别的目录调用时找不到文件 BASE_DIR os.path.dirname(os.path.abspath(__file__)) TRAIN_DIR os.path.join(BASE_DIR, dataset, train) VAL_DIR os.path.join(BASE_DIR, dataset, val) MODEL_SAVE_PATH os.path.join(BASE_DIR, model, flower_cnn.pth)用__file__推绝对路径的好处是不管你在哪个目录下执行python train.py路径都不会错。很多人图省事写dataset/train这种纯相对路径结果在 IDE 里跑没事换到终端就报找不到目录。参数上BASE_DIR是动态算出来的你换机器、换解压位置都不用改代码这是我认为这份源码里比较省心的一处设计。3. 模型与数据CNN 结构怎么搭、Dataset 怎么写3.1 CNN 网络定义三层卷积够不够用花卉识别属于细粒度分类的入门场景五类花之间颜色和形状差异还算明显不需要上 ResNet 这种深网。这份源码的model.py一般是一个轻量 CNN结构大致是「卷积-池化-卷积-池化-卷积-池化-全连接」import torch.nn as nn import torch.nn.functional as F class FlowerCNN(nn.Module): def __init__(self, num_classes5): super(FlowerCNN, self).__init__() # 输入 3 通道 RGB第一层卷出 32 个特征图 self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) # 假设输入 224x224三次下采样后是 28x28 self.fc1 nn.Linear(128 * 28 * 28, 256) self.fc2 nn.Linear(256, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): x self.pool(F.relu(self.conv1(x))) # 224 - 112 x self.pool(F.relu(self.conv2(x))) # 112 - 56 x self.pool(F.relu(self.conv3(x))) # 56 - 28 x x.view(x.size(0), -1) # 展平 x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x逐层看conv1把 3 通道 RGB 卷成 32 个特征图padding1保证卷积后尺寸不变每次MaxPool2d(2,2)把长宽各砍一半三次池化后 224 变成 28。fc1的输入维度128*28*28必须和前面特征图尺寸严格对应这是新手最容易算错的地方——如果你把输入改成 128x128那这里就得改成128*16*16否则view那一步直接报维度不匹配。Dropout(0.5)是防过拟合的常规手段花卉数据量不大时尤其有用。3.2 数据加载与增强Dataset 和 transforms 怎么配数据管道这块源码通常用ImageFolder加transforms组合省去自己写 Dataset 类from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集做增强验证集只做缩放和归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ColorJitter(brightness0.2), # 亮度扰动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) 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]) ]) train_dataset datasets.ImageFolder(TRAIN_DIR, transformtrain_transform) val_dataset datasets.ImageFolder(VAL_DIR, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2)参数说明几个关键点。Normalize里的 mean 和 std 是 ImageNet 的统计值迁移学习惯用自己从零训练其实用数据集自身的均值方差更准但差别不大直接用这套省事。batch_size32是显存和收敛速度的折中显存不够就降到 16 或 8。num_workers2在 Windows 上有时会出多进程报错遇到就改成 0。训练集shuffleTrue打乱顺序防止模型记住样本次序验证集必须False否则评估结果没法复现。注意RandomRotation和ColorJitter这类增强只加在训练集上。验证集加了增强评估指标会抖动你根本分不清是模型变好还是增强带来的随机性。3.3 训练循环损失函数、优化器和学习率怎么设训练脚本的核心就三样损失函数、优化器、轮次。花卉五分类用交叉熵损失优化器用 Adam 起步最稳import torch import torch.nn as nn import torch.optim as optim from model import FlowerCNN device torch.device(cuda if torch.cuda.is_available() else cpu) model FlowerCNN(num_classes5).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) EPOCHS 30 best_acc 0.0 for epoch in range(EPOCHS): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() # 梯度清零别漏 outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 每个 epoch 结束做一次验证 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total print(fEpoch {epoch1}/{EPOCHS}, Loss: {running_loss:.4f}, Val Acc: {acc:.4f}) # 只保存验证集上最好的权重 if acc best_acc: best_acc acc torch.save(model.state_dict(), MODEL_SAVE_PATH)lr1e-3是 Adam 的常用起点训练几轮后如果 loss 不降可以降到 1e-4。optimizer.zero_grad()这行千万别漏漏了梯度会累加模型直接训崩。保存权重时用state_dict()而不是整个模型对象这样加载时只要网络结构一致就能恢复文件也更小。model.eval()和torch.no_grad()在验证阶段必须开前者关掉 dropout 和 batchnorm 的训练行为后者省显存。4. 推理与评估模型训完怎么用、指标怎么看4.1 单张图片推理predict.py 怎么接训练完拿到flower_cnn.pth下一步是拿新图片试。推理脚本要保证和训练时用同一套预处理否则输入分布对不上预测结果全是错的import torch from PIL import Image from torchvision import transforms from model import FlowerCNN CLASSES [daisy, dandelion, rose, sunflower, tulip] device torch.device(cuda if torch.cuda.is_available() else cpu) model FlowerCNN(num_classes5).to(device) model.load_state_dict(torch.load(model/flower_cnn.pth, map_locationdevice)) model.eval() # 预处理必须和验证集完全一致 preprocess transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(test.jpg).convert(RGB) input_tensor preprocess(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): output model(input_tensor) prob torch.softmax(output, dim1) conf, pred torch.max(prob, 1) print(f预测类别: {CLASSES[pred.item()]}, 置信度: {conf.item():.4f})unsqueeze(0)是给单张图补一个 batch 维度因为模型 forward 期望输入是[N, C, H, W]。map_locationdevice保证在 CPU 机器上也能加载 GPU 训出来的权重。softmax把 logits 转成概率置信度低于 0.6 的基本可以认为模型没把握实际用的时候要设个阈值兜底。4.2 评估指标准确率之外还要看什么光看准确率不够五分类如果某类样本特别少准确率会被多数类带偏。常见做法是画混淆矩阵看每一类的召回率指标含义关注点Accuracy整体预测正确比例类别均衡时参考Precision预测为该类中真正是该类的比例误报多不多Recall该类样本被正确找出的比例漏报多不多F1-scorePrecision 和 Recall 的调和平均综合衡量from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesCLASSES)) print(confusion_matrix(all_labels, all_preds))classification_report会逐类打印 precision、recall、f1哪类花识别差一眼就能看出来。如果 rose 和 tulip 互相混淆严重说明这两类特征太接近要么加数据要么在网络里加注意力机制。4.3 训练曲线loss 不降、acc 不涨怎么判断训练过程中最常见的两个现象训练 loss 一直降但验证 acc 卡住这是过拟合加 dropout 或数据增强训练 loss 都不降这是欠拟合或学习率问题先检查数据标签有没有错再把 lr 调大或换 SGD。我一般会在训练脚本里把每个 epoch 的 loss 和 acc 存下来用 matplotlib 画曲线import matplotlib.pyplot as plt # 假设 train_losses 和 val_accs 是训练时记录的列表 plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, labeltrain loss) plt.xlabel(epoch); plt.ylabel(loss); plt.legend() plt.subplot(1, 2, 2) plt.plot(val_accs, labelval acc) plt.xlabel(epoch); plt.ylabel(accuracy); plt.legend() plt.savefig(training_curve.png)正常曲线是 loss 平滑下降、acc 平滑上升然后趋于平稳。如果 loss 剧烈震荡多半是 batch_size 太小或 lr 太大如果 acc 在某个值反复横跳可能是数据里有脏样本。5. 避坑与排查五个真实翻车记录5.1 报错 FileNotFoundError路径写死或数据集没解压对现象运行train.py直接抛FileNotFoundError: [Errno 2] No such file or directory: dataset/train。 原因源码里用了相对路径而你在别的目录下执行或者数据集压缩包解压后多套了一层文件夹实际路径是dataset/train/train。 解决把路径改成基于__file__的绝对路径然后ls dataset/train确认下面直接是五个类别文件夹不是再套一层。5.2 报错 RuntimeError: size mismatch全连接层维度算错现象RuntimeError: mat1 and mat2 shapes cannot be multiplied。 原因改了输入图片尺寸但fc1的输入维度还是按 224 算的128*28*28。 解决输入尺寸每池化一次除以 2三次池化就是除以 8。128 输入对应128*16*16224 对应128*28*28改尺寸时同步改全连接层。5.3 验证准确率虚高到 99%数据泄漏现象第一个 epoch 验证集准确率就 0.99高得不正常。 原因train 和 val 里有重复图片或者切分时没打乱同一张图既在训练又在验证。 解决检查两个文件夹有没有同名文件用哈希去重切分时先整体打乱再按比例分。5.4 显存爆了 CUDA out of memorybatch_size 太大现象RuntimeError: CUDA out of memory。 原因batch_size32加上 224 输入显存小的卡扛不住。 解决先把 batch_size 降到 16 或 8再不行就把输入降到 128。降 batch_size 后学习率可以适当调小保持训练稳定。5.5 推理结果全是同一类预处理不一致现象predict.py对任何图片都预测成 rose。 原因推理时的 transforms 和训练时不一样比如漏了 Normalize或者 Resize 尺寸不同。 解决把训练验证集的预处理代码复制过来保证 Resize、ToTensor、Normalize 三步完全一致一个参数都不能差。6. 进阶技巧把这份 baseline 再往上抬几个点跑通只是起点真要用在课程设计或小项目里准确率得拿得出手。我一般从三个方向动手。第一是迁移学习把model.py里的 CNN 换成预训练的 ResNet18只训最后的全连接层花卉这种小数据集上通常能从 80% 出头提到 90% 以上import torchvision.models as models import torch.nn as nn # 加载预训练 ResNet18替换最后一层适配 5 分类 model models.resnet18(pretrainedTrue) for param in model.parameters(): param.requires_grad False # 冻结主干 model.fc nn.Linear(model.fc.in_features, 5) # 只训这一层 model model.to(device)冻结主干后只更新fc层训练快、显存省等 fc 收敛了再解冻最后几个 block 做微调效果更好。第二是学习率调度加个StepLR每 10 个 epoch 降一次避免后期在最优解附近震荡scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) # 在每个 epoch 的 optimizer.step() 之后调用 scheduler.step()第三是 TTA测试时增强推理时把同一张图翻转、微调亮度各预测一次取平均概率一般能再涨 1 到 2 个点。验证方法很简单固定随机种子跑三次取平均对比加 TTA 前后的准确率别只看单次结果。血泪经验是别一上来就堆复杂模型。我见过太多人直接上 EfficientNet结果数据量不够过拟合到验证集惨不忍睹。先把这份 CNN baseline 跑稳把数据增强和迁移学习吃透再考虑换 backbone。从那以后我每次拿到新的分类项目都强制先跑一遍最简 CNN 确认数据管道没问题再往上加东西。希望这份拆解帮到你少走几个我踩过的坑。本文还有配套的精品资源点击获取
返回列表