简介:本资源为基于Python深度学习的花卉识别系统设计源码,面向具备一定Python基础、希望实践图像分类项目的开发者与高校学生,可用于课程设计、毕业设计或花卉识别类应用的原型搭建。压缩包共1048个文件,约37.71MB,以456个py源码文件为核心,辅以450个pyc字节码、54个dll动态库、25个pyd扩展及17个exe可执行文件,另含txt说明、jpg样本图、xml配置与虚拟环境相关文件,整体结构清晰,便于理解工程组织与依赖管理。系统借助TensorFlow或PyTorch等框架构建深度神经网络,完成花卉图像的特征提取与分类,并包含数据预处理与模型训练代码,可直接运行或二次开发。目前已有665人学习下载,适合作为深度学习入门到实战的参考案例,帮助读者掌握从数据到模型部署的完整流程。
1. 花卉识别系统到底难在哪:从一份 Python 深度学习源码说起
很多人第一次接触图像分类,都是从花卉识别开始的。原因很直接:数据集干净、类别直观、效果肉眼可见,跑通一个 demo 只要几十行代码。但真正把「基于 Python 深度学习的花卉识别系统设计源码」做成能交付、能演示、能换数据继续用的东西,坑远比想象中多。我见过太多人卡在三个地方:数据集划分乱导致验证集泄漏、模型训练完不会导出推理、GUI 和模型两张皮各跑各的。这篇笔记就按一线落地的顺序,把数据准备、模型选型、训练调参、推理封装、界面集成这条链路拆开讲清楚。适合正在做课程设计、想复现一个完整图像分类项目、或者手里有花卉数据想快速验证方案的人。读完你应该能自己搭出一套可复现、可替换数据集、可打包演示的花卉识别系统,而不是只会跑一个 notebook。
2. 数据集准备与目录结构:花卉识别系统的地基怎么打
2.1 为什么花卉数据集最容易在划分上翻车
花卉识别常用的公开数据集是 Oxford Flower 系列,常见的是 102 类版本,每类大约 40 到 258 张不等,类别间样本数差异很大。这个不均衡是第一个隐患。很多人拿到数据直接train_test_split随机切,结果同一张图的不同增强版本或者高度相似的近景图同时进了训练集和验证集,验证准确率虚高到 95% 以上,一换真实照片就崩。这就是典型的验证集泄漏,属于血泪经验级别的坑。
正确做法是按类别分层抽样,并且尽量保证同一植株、同一拍摄批次的图只出现在一个集合里。如果数据来源本身没有分组信息,至少要做到按类别等比例划分,并且固定随机种子。我一般会留出 70% 训练、15% 验证、15% 测试,测试集从头到尾不参与任何调参,只在最后评估一次。
目录结构建议直接按ImageFolder的约定来组织,这样后面换数据集不用改代码:
dataset/ ├── train/ │ ├── rose/ │ ├── tulip/ │ └── sunflower/ ├── val/ │ ├── rose/ │ ├── tulip/ │ └── sunflower/ └── test/ ├── rose/ ├── tulip/ └── sunflower/每个类别一个文件夹,文件夹名就是类别标签。这个约定是 PyTorchImageFolder和 TensorFlowimage_dataset_from_directory都认的通用格式,省去自己写标签映射的麻烦。
2.2 用 Python 做分层划分与数据校验
下面这段脚本做两件事:按类别分层划分,并检查每个集合的类别是否齐全、数量是否合理。直接抄改路径就能用。
import os import shutil import random from collections import defaultdict random.seed(42) # 固定种子,保证划分可复现 SRC_DIR = "raw_flowers" # 原始数据,每个类别一个子文件夹 DST_DIR = "dataset" # 输出目录 RATIOS = {"train": 0.7, "val": 0.15, "test": 0.15} def split_dataset(): for split in RATIOS: os.makedirs(os.path.join(DST_DIR, split), exist_ok=True) for cls in os.listdir(SRC_DIR): cls_dir = os.path.join(SRC_DIR, cls) if not os.path.isdir(cls_dir): continue imgs = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(imgs) # 打乱后再切,避免按文件名顺序造成的偏差 n = len(imgs) n_train = int(n * RATIOS["train"]) n_val = int(n * RATIOS["val"]) buckets = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:] } for split, files in buckets.items(): out_dir = os.path.join(DST_DIR, split, cls) os.makedirs(out_dir, exist_ok=True) for f in files: shutil.copy(os.path.join(cls_dir, f), os.path.join(out_dir, f)) # 校验:打印每个集合的类别数和样本数 for split in RATIOS: split_dir = os.path.join(DST_DIR, split) total = 0 for cls in os.listdir(split_dir): cnt = len(os.listdir(os.path.join(split_dir, cls))) total += cnt print(f"{split}: {len(os.listdir(split_dir))} classes, {total} images") if __name__ == "__main__": split_dataset()逻辑说明:先对每个类别的文件列表打乱,再按比例切片,保证每个集合里都有全部类别。random.seed(42)是关键,没有它每次划分结果不同,实验无法复现。参数上RATIOS可以按数据量调整,样本少的时候验证集比例可以降到 0.1,但测试集不建议低于 0.1,否则评估结果波动太大。
校验部分打印类别数和样本数,如果某个集合类别数少于总类别数,说明有类别样本太少被切没了,需要回头处理长尾类别。常见做法是对样本数少于 20 的类别做数据增强或者过采样,而不是直接丢弃。
提示:划分完成后不要再手动往 val 或 test 里加图,一旦加了,之前所有实验的对比就失效了。
3. 模型选型与训练:从 ResNet 到迁移学习的参数怎么定
3.1 为什么花卉识别不建议从零训练 CNN
花卉识别的类间差异往往很细,比如不同品种的玫瑰,颜色和花瓣形状非常接近。从零训练一个小 CNN,在几千张图上很容易过拟合,验证准确率卡在 60% 上下上不去。常见做法是用在 ImageNet 上预训练过的骨干网络做迁移学习,把卷积层当特征提取器,只替换最后的全连接分类头。ResNet50、EfficientNet-B0、MobileNetV3 都是常用选择。如果部署在边缘设备或者要打包成桌面程序,MobileNetV3 更合适,参数量小、推理快;如果追求精度且算力够,ResNet50 是稳妥的基线。
选型理由很实际:预训练权重已经学到了边缘、纹理、颜色分布这些通用特征,花卉识别只需要在此基础上微调高层语义。数据量在每类几百张的量级时,冻结骨干只训练分类头就能拿到不错的效果;数据量上千后再解冻最后几个 stage 做微调,学习率要调小一个量级。
3.2 训练脚本的关键参数与代码骨架
下面是一个基于 PyTorch 的训练骨架,包含数据增强、迁移学习、学习率调度和验证循环。代码里标注了每个关键参数的作用。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") BATCH_SIZE = 32 # 显存不够就降到 16,不要硬撑 EPOCHS = 30 LR_HEAD = 1e-3 # 只训练分类头时的学习率 LR_FINETUNE = 1e-4 # 解冻骨干后的学习率,必须更小 NUM_CLASSES = 102 # 训练集增强,验证集只做 resize 和归一化 train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("dataset/train", train_tf) val_ds = datasets.ImageFolder("dataset/val", val_tf) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=4) # 加载预训练 ResNet50,替换分类头 model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) for p in model.parameters(): p.requires_grad = False # 先冻结全部骨干 model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) model = model.to(DEVICE) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.fc.parameters(), lr=LR_HEAD, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS) def run_epoch(loader, train=True): model.train() if train else model.eval() total_loss, correct, total = 0.0, 0, 0 with torch.set_grad_enabled(train): for imgs, labels in loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) outputs = model(imgs) loss = criterion(outputs, labels) if train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total for epoch in range(EPOCHS): # 第 10 个 epoch 解冻最后两个 stage 做微调 if epoch == 10: for name, p in model.named_parameters(): if name.startswith(("layer3", "layer4", "fc")): p.requires_grad = True optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=LR_FINETUNE, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=EPOCHS - epoch) tr_loss, tr_acc = run_epoch(train_loader, train=True) va_loss, va_acc = run_epoch(val_loader, train=False) scheduler.step() print(f"Epoch {epoch:02d} | train {tr_loss:.4f} {tr_acc:.3f} " f"| val {va_loss:.4f} {va_acc:.3f}")逻辑说明:前 10 个 epoch 只训练分类头,因为随机初始化的分类头梯度很大会破坏预训练特征。第 10 个 epoch 解冻layer3、layer4和fc,用更小的学习率微调。CosineAnnealingLR让学习率按余弦曲线下降,比固定学习率更容易收敛到好的局部最优。weight_decay用 1e-4 做正则,防止微调阶段过拟合。
参数说明:BATCH_SIZE和显存直接相关,224 输入下 ResNet50 大约每张图占 1.5GB 显存(batch 32),不够就减半。num_workers在 Windows 上如果报错就设成 0。RandomResizedCrop的scale下限不要低于 0.5,否则花蕊被裁掉太多,模型学不到完整结构。
3.3 训练过程中该盯哪些指标
不要只看准确率。训练时同时记录训练损失、验证损失、验证准确率。如果训练损失持续下降但验证损失在某个 epoch 后开始上升,就是过拟合信号,应该提前停止或者加大增强。如果两者都下不去,先检查数据标签有没有错、学习率是不是太大。花卉识别里还有一个隐蔽问题:某些类别样本太少,模型会把它们全预测成样本多的类别。这时候要看混淆矩阵,而不是只看总体准确率。
注意:验证集准确率在 85% 到 92% 之间通常是正常水平,超过 95% 要怀疑验证集泄漏,低于 70% 先检查数据划分和归一化参数是否和预训练模型匹配。
4. 推理封装与界面集成:让源码变成能演示的系统
4.1 模型导出与推理脚本怎么写才不翻车
训练完的模型要保存成推理可用的格式。最省事的是保存state_dict,但加载时必须保证模型结构代码一致。更稳妥的做法是导出成 TorchScript 或者 ONNX,这样推理端不依赖训练代码。下面这段导出 TorchScript 并写一个独立推理函数。
import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image NUM_CLASSES = 102 CKPT = "best_resnet50.pth" TS_PATH = "flower_model.ts" # 重建结构并加载权重 model = models.resnet50(weights=None) model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) model.load_state_dict(torch.load(CKPT, map_location="cpu")) model.eval() # 导出 TorchScript,固定输入尺寸 example = torch.randn(1, 3, 224, 224) traced = torch.jit.trace(model, example) traced.save(TS_PATH) # 独立推理函数,只依赖 TorchScript 文件和类别列表 infer_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) def predict(image_path, class_names): model = torch.jit.load(TS_PATH) model.eval() img = Image.open(image_path).convert("RGB") tensor = infer_tf(img).unsqueeze(0) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1) conf, idx = probs.max(1) return class_names[idx.item()], conf.item()逻辑说明:torch.jit.trace把模型和计算图一起序列化,推理时不需要原始模型定义代码。map_location="cpu"保证在没有 GPU 的机器上也能加载。推理函数里convert("RGB")很重要,花卉图片可能有 RGBA 或者灰度图,不转换会在归一化时报错。softmax后的置信度可以用来做拒识,低于阈值就提示「无法确定」。
参数说明:Resize(256)加CenterCrop(224)必须和验证集预处理完全一致,差一个像素或者换一种插值方式都会导致精度下降。类别列表class_names的顺序必须和训练时ImageFolder的classes顺序一致,这个顺序是按文件夹名字母序排的,换数据集后要重新确认。
4.2 用 Tkinter 做一个最小可用的识别界面
课程设计通常要求有界面。Tkinter 是 Python 自带的,不用额外装依赖,打包也方便。下面是一个能选图、显示图片、输出类别和置信度的最小界面。
import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk from inference import predict # 上面写的推理函数 class_names = [...] # 从训练时的 dataset/train 类别顺序复制过来 def choose_and_predict(): path = filedialog.askopenfilename( filetypes=[("Images", "*.jpg *.jpeg *.png")]) if not path: return img = Image.open(path).convert("RGB").resize((300, 300)) tk_img = ImageTk.PhotoImage(img) panel.config(image=tk_img) panel.image = tk_img label, conf = predict(path, class_names) result_var.set(f"类别:{label} 置信度:{conf:.2%}") root = tk.Tk() root.title("花卉识别系统") root.geometry("420x420") btn = tk.Button(root, text="选择图片", command=choose_and_predict) btn.pack(pady=8) panel = tk.Label(root) panel.pack() result_var = tk.StringVar() tk.Label(root, textvariable=result_var, font=("Arial", 14)).pack(pady=8) root.mainloop()逻辑说明:界面只负责选图和展示,推理逻辑全部放在inference.py里,这样换模型或者换界面互不影响。panel.image = tk_img这行必须写,否则图片对象被垃圾回收,界面显示空白,这是 Tkinter 的经典坑。置信度用百分比显示,方便演示时解释。
参数说明:resize((300, 300))只影响显示,不影响推理,推理用的还是原图路径。如果要做批量识别,把choose_and_predict改成遍历文件夹即可,但要注意界面会卡住,需要放到线程里。
提示:打包成 exe 用 PyInstaller 时,TorchScript 文件和类别列表要一起打进去,用
--add-data指定,否则运行时报找不到模型。
5. 避坑与排查:花卉识别源码落地时最容易踩的五个坑
5.1 验证准确率异常高但实际识别全错
现象:训练日志里验证准确率 96%,但拿手机拍的花卉照片测试,几乎全错。原因:验证集和训练集来自同一批拍摄数据,背景、光照、角度高度相似,模型学到了背景而不是花卉特征。解决:划分数据时按拍摄批次分组,或者至少保证验证集里有不同背景的图。更彻底的做法是额外准备一批完全独立的真实场景图做最终测试。
5.2 训练损失不下降,准确率卡在随机水平
现象:训练几个 epoch 后损失一直在 4.6 左右(102 类随机猜是 ln(102)≈4.62),准确率 1% 上下。原因:最常见的是标签和图片没对上,ImageFolder按文件夹名排序,如果类别列表顺序搞错,标签全乱。另一个原因是归一化参数用错,比如用了 ImageNet 的均值方差但输入是 0 到 255 没除以 255。解决:先拿 10 张图过一遍模型,打印预测类别和真实类别,确认标签映射正确;再检查ToTensor()是否在Normalize之前。
5.3 推理时显存溢出或者速度极慢
现象:训练时正常,推理时单张图要好几秒,或者直接 OOM。原因:推理时没有用torch.no_grad(),计算图还在构建;或者 batch size 设成 1 但模型还在 GPU 上反复加载。解决:推理函数里加with torch.no_grad():,模型只加载一次并缓存,不要每次预测都torch.jit.load。如果部署在 CPU 上,用torch.set_num_threads(4)控制线程数。
5.4 换数据集后类别数对不上导致加载失败
现象:换了一个 50 类的花卉数据集,加载之前 102 类的权重时报size mismatch。原因:分类头fc的输出维度是 102,新数据集是 50。解决:加载权重时用strict=False跳过分类头,然后重新初始化fc并只训练分类头。代码上就是model.load_state_dict(ckpt, strict=False),再model.fc = nn.Linear(in_features, new_num_classes)。
5.5 界面打包后模型文件找不到
现象:PyInstaller 打包成 exe 后,双击运行提示找不到flower_model.ts。原因:打包后工作目录变了,相对路径失效。解决:用sys._MEIPASS获取临时解压目录,把模型路径拼成绝对路径。或者更简单,把模型文件放在 exe 同目录,用os.path.dirname(sys.executable)定位。
import sys, os def resource_path(relative): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.abspath("."), relative) TS_PATH = resource_path("flower_model.ts")6. 把花卉识别源码用起来:从演示到可复用系统的最后一步
走到这里,训练和推理都通了,但要让这套源码真正可复用,还差一个习惯:把配置抽出来。我一般会在项目根目录放一个config.yaml,把数据路径、类别数、输入尺寸、模型名称、学习率全部写进去,训练和推理都读同一份配置。这样换数据集时只改配置,不动代码。下面是一个最小配置示例和读取方式。
data: train_dir: dataset/train val_dir: dataset/val num_classes: 102 input_size: 224 model: backbone: resnet50 pretrained: true train: batch_size: 32 epochs: 30 lr_head: 0.001 lr_finetune: 0.0001 infer: model_path: flower_model.ts conf_threshold: 0.5import yaml with open("config.yaml", "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) NUM_CLASSES = cfg["data"]["num_classes"] INPUT_SIZE = cfg["data"]["input_size"] CONF_THRESHOLD = cfg["infer"]["conf_threshold"]验证这套系统是否真的可用,我通常做三步:第一步,用测试集跑一遍,看总体准确率和混淆矩阵,确认没有某个类别被完全忽略;第二步,拿 10 张训练时没见过的真实照片,人工核对预测结果,重点看置信度低于 0.5 的样本;第三步,把模型文件单独拷到另一台没装训练环境的机器上,只装推理依赖,跑一遍界面,确认能正常出结果。这三步过了,才算能交付。
还有一个实用技巧:在推理函数里加一个置信度阈值判断,低于阈值时返回「不确定」而不是硬猜一个类别。花卉识别里经常遇到不属于任何已知类别的花,硬猜会误导使用者。阈值设 0.5 到 0.6 之间比较稳妥,具体看验证集上的置信度分布。
我自己踩过最深的坑是早期做课程设计时,把训练集和验证集混在一起调参,最后答辩演示时换了一批图直接翻车。后来养成的习惯是:测试集从划分那天起就锁死,任何调参都不许碰它,直到最后评估才打开。这个习惯比任何模型技巧都值钱。希望帮到你。
本文还有配套的精品资源,点击获取