资讯动态

花卉识别模型训练实战:数据划分、调参避坑与ONNX部署

发布时间:2026/9/28 2:06:49 来源:尧图企业网站定制
简介面向图像分类入门与实战的花卉识别资料包适合深度学习学习者与算法调试人员使用。内含16种花卉共32000张224×224彩色图片每类约2000张覆盖千屈菜、射干、旋覆花、曼陀罗、桔梗、秋英、粉黛乱子草等常见花卉配套基于TensorFlow编写的训练源码集成了23种主流图片分类模型可直接选用ResNet、Inception等结构开展训练与对比实验。资源共110个文件包含33个Python源码、对应pyc编译文件、TXT说明文档、bat启动脚本以及数据封装模块压缩包总大小684.45MB。源码将卷积层、残差结构、图像预处理、模型搭建等模块拆分配置便于二次开发图片统一尺寸且覆盖不同生长形态与背景可直接用于分类训练。此批数据集为第一批后续批次花卉种类不同如需更多类别可另下其他批次。目前已有1499人学习下载适合需要完整花卉数据集与多模型训练框架的读者可用于课程设计、算法预研或模型效果验证作为图像分类项目的基线参考。1. 花卉识别数据集别把精度当回事先看你的训练集和测试集分没分对做花卉分类踩过最大的坑不是模型选错而是数据集本身“看起来很美”。这个花卉识别数据集02包含数千张花朵图片和配套训练源码类别覆盖常见园林花卉适合做花朵分类、识别算法的训练与验证。我一开始直接拿默认划分跑 ResNet50训练集准确率冲到 96%测试集却只有 71%翻车翻得莫名其妙——后来发现是数据集里同一株花的照片被硬生生拆进了 train 和 val模型等于提前见了答案。这篇就围绕这个数据集讲清楚怎么组织数据、怎么选模型、怎么把训练跑稳、怎么避坑以及最后怎么把模型导出成能落地的格式。新手可以按步骤走熟手重点看参数边界和踩坑记录。2. 把图片集变成训练集目录结构、标签映射与 PyTorch 加载2.1 先搞清楚数据集目录里装的是什么拿到这个花卉图片集第一步不是急着训练而是把目录结构摸清楚。常见做法是dataset/下按类别分文件夹或者统一放在images/里配一个labels.csv。这套花卉数据集的常规组织方式是前者——每个类别一个文件夹文件夹名即类别名。这样做的好处是 torchvision 的ImageFolder可以直接吃不需要手写 label 映射。我一般会先用一段脚本把实际情况打出来确认类别数、每类图片数、图片尺寸范围和格式。这一步能提前发现三个问题类别文件夹命名是否规范、图片是否损坏、不同类别数量是否悬殊。批量下载或采集的数据集最容易翻车的地方就是图片格式五花八门.jpg、.png、.bmp、.jpeg混着来有些甚至是.webp后缀但实际编码是 JPEG。import os from collections import Counter from PIL import Image dataset_root ./flower_dataset extensions {.jpg, .jpeg, .png, .bmp, .webp} class_counter Counter() bad_images [] size_counter Counter() for class_name in sorted(os.listdir(dataset_root)): class_path os.path.join(dataset_root, class_name) if not os.path.isdir(class_path): continue for fname in os.listdir(class_path): ext os.path.splitext(fname)[1].lower() if ext not in extensions: continue fpath os.path.join(class_path, fname) class_counter[class_name] 1 try: with Image.open(fpath) as img: size_counter[img.size] 1 except Exception: bad_images.append(fpath) print(类别统计:, class_counter) print(图片尺寸分布:, size_counter.most_common(5)) print(f损坏图片数量: {len(bad_images)}) for p in bad_images[:10]: print(BAD:, p)这段脚本做的三件事分别对应三个风险点class_counter检查每类样本量防止某个类别只有 20 张、另一个类别有 800 张这种极端不均衡size_counter统计尺寸分布如果图片既有 224x224 又有 600x800后面做 Resize 和 CenterCrop 时就要统一策略bad_images提前揪出打不开的文件训练时遇到损坏文件报错中断浪费的时间远多于提前扫描这几分钟。2.2 数据集划分必须按“个体”切不能按“文件”切这里说的坑很隐蔽但直接影响模型真实水平。花卉数据集很多来自同一株植物连续拍摄的多张照片或者同一个公园同一片花丛的多角度照片。如果你直接random_split(0.8, 0.2)那同一株花的多张照片大概率同时落在训练集和验证集里。模型在验证集上的表现会虚高因为它“见过”这些花的其他角度照片本质上是记住了这株花的纹理特征而不是学到了“玫瑰”和“月季”的判别边界。正确的做法是先按文件名的前缀或元数据把同一来源的图片聚成一组再按组划分。但这个数据集不一定提供了明确的个体 ID所以退而求其次的做法是先做一次聚类或按目录子文件夹归组再划分。import os import random import shutil from collections import defaultdict dataset_root ./flower_dataset train_dir ./split/train val_dir ./split/val # 先按文件名前缀归组假设同一株花的图片以相同前缀命名 groups defaultdict(list) for class_name in os.listdir(dataset_root): class_path os.path.join(dataset_root, class_name) if not os.path.isdir(class_path): continue for fname in os.listdir(class_path): prefix fname.split(_)[0] # 例如 IMG_1234_01.jpg - IMG_1234 groups[f{class_name}/{prefix}].append((class_name, fname)) group_list list(groups.keys()) random.seed(42) random.shuffle(group_list) split_idx int(len(group_list) * 0.8) train_groups set(group_list[:split_idx]) val_groups set(group_list[split_idx:]) for class_name in os.listdir(dataset_root): os.makedirs(os.path.join(train_dir, class_name), exist_okTrue) os.makedirs(os.path.join(val_dir, class_name), exist_okTrue) for class_name, fname_list in groups.items(): pass # 注意上面是归组这里重新遍历逻辑略 # 实际上更稳妥的做法是按组名遍历 for group_name, files in groups.items(): class_name, _ group_name.split(/, 1) dest_root train_dir if group_name in train_groups else val_dir for cls, fname in files: src os.path.join(dataset_root, cls, fname) dst os.path.join(dest_root, cls, fname) shutil.copy(src, dst)这里有个关键理解划分的单位是“组”而不是“张”。如果数据集图片命名没有前缀规律那至少要做到按时间戳或拍摄批次划分。实在没有这类信息也要在训练后用按类别抽样的方式单独检查验证集里有没有和训练集高度相似的图。用random.seed(42)固定随机种子保证每次复现的划分一致这是训练可复现性的底线。2.3 用 ImageFolder 和 DataLoader 把训练管道跑通划分完成后的标准动作是用torchvision.datasets.ImageFolder加载。这个类要求目录层级是root/class/image.jpg前面划分脚本的输出正好满足这个结构。ImageFolder会自动按文件夹名的字母序生成类别索引这意味着class_to_idx的映射是确定的但也提示你要在训练前把这个映射打印出来存档后面推理时要保证同样的映射顺序。from torch.utils.data import DataLoader from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.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(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(./split/train, transformtrain_transform) val_dataset datasets.ImageFolder(./split/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别映射:, train_dataset.class_to_idx) print(训练集样本数:, len(train_dataset)) print(验证集样本数:, len(val_dataset))训练集和验证集的 transform 策略是两套完全不同的逻辑训练集要“折腾”数据用RandomResizedCrop模拟不同距离、不同构图的拍摄效果RandomRotation(15)模拟花朵朝向偏移ColorJitter模拟不同光照条件验证集要“稳定”数据只做Resize CenterCrop保证每张图都以固定尺度和位置进入模型。这里Normalize用的均值和标准差是 ImageNet 的统计值不是在当前数据集上重新算的。如果换用自己算的均值方差迁移学习的预训练权重效果会打折因为预训练模型已经习惯了输入数据的分布范围。3. 模型选型与训练配置ResNet50 还是 MobileNetV33.1 预训练权重是底线不是可选项花卉识别这种细粒度分类任务类别之间差异小——“桃花”和“樱花”都是粉色小花花瓣形状、花蕊颜色才是判别点。从头训练一个 CNN 在这个规模的数据集上很难收敛到理想效果因为可学习的判别特征太多而样本量不足以支撑模型学到鲁棒的边界。使用 ImageNet 预训练权重做迁移学习相当于让模型从“认识 1000 类物体”的底座出发只需要微调来适应花卉的细粒度差异。这个数据集的源码包通常默认支持 ResNet 系列部分版本也带了 MobileNet 的配置入口。选 ResNet50 的理由是它在 ImageNet 分类任务上的特征表达足够强模型规模适中在单卡 GPU 上训练 30~50 个 epoch 的时间可控。MobileNetV3 的优势是参数量小适合之后部署到移动端或边缘设备。我的做法是先用 ResNet50 跑通流程、验证数据没问题再切到 MobileNetV3 做正式训练。import torch import torch.nn as nn import torch.optim as optim from torchvision import models num_classes len(train_dataset.classes) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 冻结除最后一层外的所有参数先只训练分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3)先冻结 backbone 只训练 fc 层这一步是让新初始化的分类头先在当前数据集分布上站稳——因为新头的权重是随机初始化的如果一开始就用大梯度去更新整个网络预训练特征很快就被破坏模型会退化成一个从头训练的 CNN。跑 5 个 epoch 左右、验证集准确率不再明显上升后再解冻全部参数、把学习率调低一个数量级做全量微调。3.2 学习率、batch size、epoch 的搭配逻辑花卉分类训练最容易出现的问题不是不收敛而是收敛得太快然后过拟合。预训练权重让模型起点很高第一轮 epoch 就可能到 80% 以上这时候容易误判为“模型很好”继续训练则会发现验证集准确率停滞甚至下降。学习率策略我一般用OneCycleLR。它的核心逻辑是先热身后降温——前 20% 的步数把学习率从小到大线性升到峰值后面逐步降到接近 0。这种方式比固定学习率更能逃离局部极小点在微调场景下尤其有效。峰值学习率可以参考lr_max 1e-4这个量级如果 backbone 被冻结则可以用 1e-3全量微调时必须降到 1e-4 以下。from torch.optim.lr_scheduler import OneCycleLR total_steps len(train_loader) * 20 # 假设训练 20 个 epoch # 解冻 backbone切换优化器 for param in model.parameters(): param.requires_grad True optimizer optim.Adam(model.parameters(), lr1e-4) scheduler OneCycleLR( optimizer, max_lr1e-4, total_stepstotal_steps, pct_start0.2, anneal_strategycos ) for epoch in range(20): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() running_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) train_acc correct / total val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1}/20 | Loss: {running_loss/total:.4f} f| Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f})pct_start0.2的含义是整个训练过程前 20% 的步数用于升温之后 80% 用于余弦退火。batch size 32 时梯度噪声适中类别不均衡时还可以调CrossEntropyLoss(weightclass_weights)但会增加调参复杂度。我建议先不平权跑一轮看混淆矩阵里哪些类别互相混淆再决定是否加权。4. 训练源码跑通训练脚本、断点续训与评估指标4.1 完整训练入口的参数怎么传这个数据集的训练源码通常提供一个train.py入口核心参数包括--data、--model、--epochs、--batch-size、--lr、--pretrained。第一次跑通不建议用默认参数直接开跑先把--epochs 3 --batch-size 16这种小配置跑一轮确认数据加载、前向传播、反向传播、验证评估全链路无异常再上完整配置。python train.py \ --data ./split \ --model resnet50 \ --epochs 30 \ --batch-size 32 \ --lr 1e-4 \ --pretrained \ --num-classes 10 \ --output-dir ./runs/exp1 \ --device cuda:0--data指向的是包含 train 和 val 两个子目录的根目录不是指向 train 本身。--num-classes建议显式指定虽然代码里可能能从目录结构自动推断但显式传参可以避免“自动推断拿到的类别数和模型 fc 层输出不匹配”这类低级错误。--output-dir设置独立的输出目录每个实验一组参数一个目录后面对比实验时才不会互相覆盖。4.2 训练日志和 checkpoint 保存是后悔药训练到第 20 个 epoch 时发现验证集准确率开始下降这时候最需要的是能回退到第 15 个 epoch 的 checkpoint。很多训练脚本只保存最后一个 epoch 的模型这在迁移学习场景下不够。正确做法是保存两类文件一是“当前最优”模型——用验证集准确率做判据一旦刷新最高值就覆盖保存二是周期性 checkpoint——每 5 个 epoch 存一份完整状态包括模型权重、优化器状态、学习率调度器状态、当前 epoch 数。best_val_acc 0.0 save_path ./runs/exp1 for epoch in range(30): train_loss, train_acc train_one_epoch(...) val_acc evaluate(...) if val_acc best_val_acc: best_val_acc val_acc torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), val_acc: val_acc, }, f{save_path}/best_checkpoint.pth) print(fBest model saved at epoch {epoch1}, val_acc{val_acc:.4f}) if (epoch 1) % 5 0: torch.save(model.state_dict(), f{save_path}/epoch_{epoch1}.pth)best_checkpoint.pth保存完整状态而不是只保存model.state_dict()原因是断点续训时需要恢复优化器和调度器的状态。如果只有模型权重恢复训练后学习率会从初始状态重新开始导致训练曲线断裂。周期性保存则用于事后分析——如果最后一轮过拟合了可以回到第 20 轮的权重继续评估不用重新训练。磁盘占用方面ResNet50 一个 checkpoint 约 100MB30 个 epoch 存 6 个也就 600MB这点成本换来的后悔药非常值。4.3 别用准确率当唯一指标花卉数据集的类别分布很难做到完全均衡特别是采集自野外的图片集某些罕见品种可能只有训练集里几十张。这种情况下验证集准确率会被高频类别主导模型可能对低频类别完全失效但整体准确率看着还不错。我每次训练结束后会强制打印每个类别的精确率、召回率和 F1这个工具 sklearn 的classification_report可以直接给不需要自己写循环。from sklearn.metrics import classification_report import numpy as np all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) class_names list(train_dataset.class_to_idx.keys()) print(classification_report( all_labels, all_preds, target_namesclass_names, digits3 ))classification_report会按类别给出 precision、recall、f1-score 和 support样本数。看这个报告的习惯是先看 support——哪些类别样本少于 50、哪些类别超过 200这是判断结果可信度的前提再看每个类别的 recall——recall 低的类别说明模型大量漏检典型情况是“月季”被误判为“玫瑰”因为两者外观相似度极高最后看 macro avg 和 weighted avg 的差值差得越大说明模型在低频类别上的表现越差。5. 避坑与调参花卉识别训练中的常见问题排查5.1 验证集准确率虚高换真实场景就崩现象训练时验证集达到 95%拿到几张自己拍的花朵照片测试识别结果完全不准。原因训练集和验证集划分不够严格同一株花、同一个拍摄场景的照片被同时分进两边也可能数据集本身是从视频帧或连拍中提取的相邻帧差异极小。解决按文件名前缀或拍摄时间归组后重新划分保证同一来源图片只出现在一边如果数据集没有提供这些信息只能手动人工抽检验证集图片把明显来自同一场景的图像整组剔除。从那以后我拿到数据集的第一件事就是跑一遍相似度检查用感知哈希算图片两两相似度相似度高于阈值的只留一张而不是直接开训。5.2 训练 loss 下降到 0.1 以下验证集准确率却不高现象训练 loss 一路降到 0.1 以下训练准确率几乎 100%但验证集准确率只有 70% 左右。原因这是典型的过拟合信号。花卉数据集图片数量通常在几千张级别ResNet50 参数量 2500 万以上如果没有强数据增强和正则化模型很容易把训练集的背景、光照等无关特征记下来。解决强化数据增强RandomResizedCrop的 scale 参数可以设到(0.5, 1.0)强制模型关注花朵主体同时加入RandomHorizontalFlip和RandomRotation(15)之外的RandomErasing——随机遮挡图片的一部分让模型不能依赖单一局部特征。Dropout 层加在 fc 层之前概率取 0.3。5.3 训练速度极慢GPU 利用率只有 20%现象显卡显示利用率很低训练一个 epoch 要 20 分钟但实际计算量不大。原因数据加载成为瓶颈。图片尺寸大、解码慢num_workers设置太低或者磁盘本身是机械硬盘。解决num_workers调成 CPU 核心数的一半或三分之二pin_memoryTrue让 GPU 直接读取锁页内存。如果图片分辨率很大在 transform 里先Resize(256)再RandomResizedCrop(224)这样可以减少解码后的内存占用和缩放计算量。还有一个玄学问题——Windows 系统上num_workers必须放在if __name__ __main__保护的代码块里否则会无限递归报错。5.4 微调阶段损失函数震荡不收敛现象解冻 backbone 后 loss 不但没下降反而出现周期性震荡验证集准确率上下跳动。原因学习率过大Adam 默认lr1e-3在预训练模型上偏大微调阶段权重更新幅度过猛也可能是 batch size 太小导致梯度估计噪声太大。解决微调阶段学习率调到1e-4到3e-4之间batch size 小于 16 时先调大。另外确认 backbone 的 BatchNorm 层行为PyTorch 的 model.train() 下 BN 会使用当前 batch 的统计量如果 batch size 太小BN 的均值方差估计不稳定也会导致 loss 震荡。如果必须用小 batch改用SyncBatchNorm或干脆冻结前几层的 BN。5.5 推理时输入尺寸和 transform 不一致现象训练时验证集准确率正常部署到 API 接口后准确率明显下降。原因推理代码里没有做Normalize或者 Resize/Crop 策略与训练时不一致。训练时的RandomResizedCrop相当于数据增强如果推理时也用了这个等于给模型喂了“随机裁剪”后的图片输出自然不稳定。解决推理管线和验证集完全一致——先Resize(256)再CenterCrop(224)最后Normalize并且 Normalize 用的 mean 和 std 必须和训练时完全相同。我把 transform 封装成一个单独的函数训练和推理都从这里导入杜绝手写两遍导致不一致的问题。6. 模型导出的工程化验证从 PyTorch 到 ONNX 的完整流程训练收尾后模型最终要走出 Jupyter Notebook进入真实的应用环境。PyTorch 的.pth文件只能在 Python 环境里跑如果要部署到推理框架或嵌入到移动端需要导出成 ONNX 或者 TorchScript。我这里以 ONNX 为例因为 OnnxRuntime 的跨平台性最好CPU 推理速度也比纯 PyTorch 快不少。import torch import onnx import onnxruntime as ort import numpy as np from PIL import Image from torchvision import transforms model.eval() dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, flower_model.onnx, export_paramsTrue, opset_version13, do_constant_foldingTrue, input_names[input], output_names[logits], dynamic_axes{input: {0: batch_size}, logits: {0: batch_size}} ) # 验证导出的 ONNX 模型输出与 PyTorch 一致 ort_session ort.InferenceSession(flower_model.onnx) sample_img Image.open(test_flower.jpg).convert(RGB) transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) input_tensor transform(sample_img).unsqueeze(0).numpy() onnx_output ort_session.run([logits], {input: input_tensor})[0] with torch.no_grad(): torch_output model(torch.from_numpy(input_tensor)).cpu().numpy() diff np.abs(onnx_output - torch_output).max() print(f最大输出差异: {diff:.6f}) assert diff 1e-3, ONNX 输出与 PyTorch 差异过大dynamic_axes设置 batch 维度为动态这样导出后既支持单张推理也支持 batch 推理不需要为不同 batch size 导多份模型。opset_version13兼容性较好OnnxRuntime 和大多数推理框架都能跑。差异验证是关键一步——diff通常应该在1e-4量级如果超过1e-2说明模型里有某些算子导出质量不行常见的坑是torch.nn.functional.interpolate的坐标变换在 ONNX 里的行为与 PyTorch 不完全一致。导出完成后还要做一次全流程验证随机从验证集抽 100 张图全部用 OnnxRuntime 跑一遍推理再和 PyTorch 的预测结果对比统计预测不一致的数量。理论上 ONNX 和 PyTorch 推理结果应该完全一致因为只是计算图的格式变了权重没有改动。如果出现不一致优先检查推理时的预处理——transform 是否和训练时一模一样尤其是 Normalize 的 mean/std 是否照抄这里抄错一个数字模型输出就完全不同。从那以后我每次训练完强制自己走一遍“导出 ONNX → 跑差异对比 → 抽检 100 张图”的流程把 PyTorch 模型和 ONNX 模型当成两个独立的实现来交叉验证。这步做完模型才算真正从“训练能跑”变成“能用”。拿到这份花卉识别训练源码和数据集的同行建议先花 20 分钟跑一遍第 2 章的检查脚本再决定要不要调模型——数据这一关过了后面训练踩的坑能少一半。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑