news 2026/9/26 3:51:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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 torch==1.13.1 torchvision==0.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_classes=5): super(FlowerCNN, self).__init__() # 输入 3 通道 RGB,第一层卷出 32 个特征图 self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) 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 个特征图,padding=1保证卷积后尺寸不变;每次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(brightness=0.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, transform=train_transform) val_dataset = datasets.ImageFolder(VAL_DIR, transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)

参数说明几个关键点。Normalize里的 mean 和 std 是 ImageNet 的统计值,迁移学习惯用,自己从零训练其实用数据集自身的均值方差更准,但差别不大,直接用这套省事。batch_size=32是显存和收敛速度的折中,显存不够就降到 16 或 8。num_workers=2在 Windows 上有时会出多进程报错,遇到就改成 0。训练集shuffle=True打乱顺序防止模型记住样本次序,验证集必须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_classes=5).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-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(f"Epoch {epoch+1}/{EPOCHS}, Loss: {running_loss:.4f}, Val Acc: {acc:.4f}") # 只保存验证集上最好的权重 if acc > best_acc: best_acc = acc torch.save(model.state_dict(), MODEL_SAVE_PATH)

lr=1e-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_classes=5).to(device) model.load_state_dict(torch.load("model/flower_cnn.pth", map_location=device)) 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, dim=1) conf, pred = torch.max(prob, 1) print(f"预测类别: {CLASSES[pred.item()]}, 置信度: {conf.item():.4f}")

unsqueeze(0)是给单张图补一个 batch 维度,因为模型 forward 期望输入是[N, C, H, W]。map_location=device保证在 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_names=CLASSES)) 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, label="train loss") plt.xlabel("epoch"); plt.ylabel("loss"); plt.legend() plt.subplot(1, 2, 2) plt.plot(val_accs, label="val 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*16,224 对应128*28*28,改尺寸时同步改全连接层。

5.3 验证准确率虚高到 99%:数据泄漏

现象:第一个 epoch 验证集准确率就 0.99,高得不正常。 原因:train 和 val 里有重复图片,或者切分时没打乱,同一张图既在训练又在验证。 解决:检查两个文件夹有没有同名文件,用哈希去重;切分时先整体打乱再按比例分。

5.4 显存爆了 CUDA out of memory:batch_size 太大

现象:RuntimeError: CUDA out of memory。 原因:batch_size=32加上 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(pretrained=True) 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_size=10, gamma=0.1) # 在每个 epoch 的 optimizer.step() 之后调用 scheduler.step()

第三是 TTA(测试时增强),推理时把同一张图翻转、微调亮度各预测一次,取平均概率,一般能再涨 1 到 2 个点。验证方法很简单:固定随机种子,跑三次取平均,对比加 TTA 前后的准确率,别只看单次结果。

血泪经验是,别一上来就堆复杂模型。我见过太多人直接上 EfficientNet,结果数据量不够,过拟合到验证集惨不忍睹。先把这份 CNN baseline 跑稳,把数据增强和迁移学习吃透,再考虑换 backbone。从那以后我每次拿到新的分类项目,都强制先跑一遍最简 CNN 确认数据管道没问题,再往上加东西。希望这份拆解帮到你,少走几个我踩过的坑。

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

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

RK3588/RK3576为何砍掉原生LVDS?从带宽和PHY解析显示接口取舍

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 3:49:58

基于Spring Boot的医院预约挂号系统:从选题到答辩全攻略

每年到了毕业设计选题季,总有不少计算机专业的同学来找我参谋:有的想选个容易过审的题目,有的想找个能写进简历的项目,还有的干脆就是想找个附源码的现成项目改一改。说实话,如果你在Java技术栈上有点基础,…

作者头像 李华
网站建设 2026/9/26 3:49:53

区域综合能源系统双层优化调度:计及需求响应的Matlab完整复现

做核心期刊论文复现这件事,老读者都知道我的习惯:不把代码丢出来就完事,一定要把模型顶层设计、数学原理、代码架构和踩坑过程全部讲清楚。这篇要拆解的,是“计及需求响应的区域综合能源系统双层优化调度策略研究”,Ma…

作者头像 李华