ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python深度学习的花卉识别系统源码实战:从数据划分到界面集成

基于Python深度学习的花卉识别系统源码实战:从数据划分到界面集成 简介本资源为基于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_okTrue) 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_okTrue) 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_sizeBATCH_SIZE, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_sizeBATCH_SIZE, shuffleFalse, num_workers4) # 加载预训练 ResNet50替换分类头 model models.resnet50(weightsmodels.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(), lrLR_HEAD, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxEPOCHS) def run_epoch(loader, trainTrue): 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()), lrLR_FINETUNE, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxEPOCHS - epoch) tr_loss, tr_acc run_epoch(train_loader, trainTrue) va_loss, va_acc run_epoch(val_loader, trainFalse) scheduler.step() print(fEpoch {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(weightsNone) model.fc nn.Linear(model.fc.in_features, NUM_CLASSES) model.load_state_dict(torch.load(CKPT, map_locationcpu)) 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, dim1) conf, idx probs.max(1) return class_names[idx.item()], conf.item()逻辑说明torch.jit.trace把模型和计算图一起序列化推理时不需要原始模型定义代码。map_locationcpu保证在没有 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(imagetk_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选择图片, commandchoose_and_predict) btn.pack(pady8) panel tk.Label(root) panel.pack() result_var tk.StringVar() tk.Label(root, textvariableresult_var, font(Arial, 14)).pack(pady8) 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。解决加载权重时用strictFalse跳过分类头然后重新初始化fc并只训练分类头。代码上就是model.load_state_dict(ckpt, strictFalse)再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, encodingutf-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 之间比较稳妥具体看验证集上的置信度分布。我自己踩过最深的坑是早期做课程设计时把训练集和验证集混在一起调参最后答辩演示时换了一批图直接翻车。后来养成的习惯是测试集从划分那天起就锁死任何调参都不许碰它直到最后评估才打开。这个习惯比任何模型技巧都值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表