
简介面向花卉识别与图像分类学习者这套资料提供了16类花卉图片数据集及配套训练源码可用于花朵分类模型训练、算法验证与数据集扩展。数据涵盖千屈菜、射干、旋覆花、曼陀罗、桔梗、棣棠、狗尾草、狼尾草、石竹、秋英、粉黛乱子草、红花酢浆草、芒草、蒲苇、马鞭草、黄金菊等16种常见花卉共约32000张224×224彩色图片每类近2000张。配套源码基于TensorFlow编写聚合23种主流图片分类模型可灵活选择进行训练与对比包内以文本说明、Python脚本及编译文件为主其中文本文件可查看标签与说明脚本构成训练主体另有启动脚本与数据文件便于直接搭建实验环境。整个压缩包共110个文件大小约684.45MB已有1499人学习下载适合希望在花卉数据集上快速开展分类实践的初学者与研究者。1. 花卉识别从“图片集”到可交付模型这套资源到底怎么用才不浪费接到公园智慧导览的需求客户丢过来一个压缩包几千张花图、一份现成训练源码、一个写着“花卉图片集02”的增量批次。大部分人拿到这种包的第一反应是直接开训但实际上一周内真正卡住你的往往不是模型而是图片集里的标签混乱、类别不均衡和混入的无效图片。花卉识别这个方向的门槛并不在算法上迁移学习几分钟就能把基线跑起来难的是把“图片集”变成“数据集”再把模型送进真实场景。这篇笔记面向要自己动手训练花类识别模型的工程师和学生按清洗图片集、选训练源码、定参数、避坑、验证交付的顺序把一套能复现的落地路径写清楚。2. 把花卉图片集清洗成可训练的数据集格式、标签与划分的完整处理2.1 先盘一遍图片集的“家底”文件格式、尺寸与损坏文件检测标题里的“02”一般表示这是同一批数据的第二批增补这类增量批次最容易出现标签风格和第一批不一致的问题所以第一步不是急着训练而是先做全量扫描。拿到图片集先统计扩展名、尺寸范围和损坏文件这一步能避免后面训练到一半才被坏图打断的尴尬。我一般会写一个短脚本把整个目录过一遍收集基本信息import os from collections import Counter from PIL import Image IMG_DIR ./flower_images exts Counter() sizes [] broken [] for root, _, files in os.walk(IMG_DIR): for name in files: if name.startswith(._): # 跳过 macOS 元数据文件 continue ext os.path.splitext(name)[1].lower() exts[ext] 1 path os.path.join(root, name) try: with Image.open(path) as im: im.load() # 真正解码verify() 只查文件头 sizes.append(im.size) except Exception: broken.append(path) print(扩展名分布:, exts) print(破损/无法解码文件数:, len(broken)) print(宽度范围:, min(s[0] for s in sizes), -, max(s[0] for s in sizes)) print(高度范围:, min(s[1] for s in sizes), -, max(s[1] for s in sizes))这段代码的作用是把图片集的“底账”盘出来。im.load()比verify()更严格很多截断的 JPEG 文件头正常但要等真正解码才会抛异常._开头的是 macOS 自动生成的元数据文件不跳过会污染统计。还有一个容易被忽略的点尺寸范围。如果一批是 4000 像素的手机原图一批是 300 像素的网页缩略图两种图混在一起训练会让模型对分辨率非常敏感建议先把所有图统一缩到短边 512 再存能显著加快后续 DataLoader 的读取速度。格式方面如果原图里有 HEIC 这类格式torchvision 的ImageFolder默认读不了需要先用 PIL 或其它工具统一转成 JPEG。扩展名分布统计出来之后建议把非 JPEG/PNG 的文件单独列出来宁可当场筛掉也不要冒险带进训练集。2.2 类别目录与标签清单中文名、拉丁名与编号的取舍图片集最常见的组织方式是一个子文件夹一个类别文件名往往是“月季”“玫瑰”“蔷薇”这类中文名。中文目录在跨平台拷贝、压缩包解压时容易出编码问题我不建议直接用中文目录名当标签。常见做法是先按业务口径整理一份类别清单把“业务中文名”和“目录代号”分开管理。比如在根目录放一个classes.csvclass_id,cn_name,latin_name 01,月季,Rosa chinensis 02,玫瑰,Rosa rugosa 03,蔷薇,Rosa multiflora目录代号用两位数字或英文短名训练时代码里只认class_id展示层再映射回中文名。这一步看似多余实际能省掉无数麻烦后期无论是新增类别还是改名都只需要改这一份 CSV不用碰数据目录。如果图片集里的标签本身比较乱比如“月季”和“玫瑰”在中文语境里经常混用务必先和需求方确认这两个类在业务上要不要区分这个决定做晚了清洗和训练都要推倒重来。2.3 训练/验证/测试划分脚本分层划分与固定随机种子划分数据集的常用做法是按类别做分层抽样保证每个类别在训练、验证、测试三份里都有样本避免样本量小的花类被随机切光。固定随机种子同样重要否则每次跑出来的结果不可比复现和调参都无从谈起。import random from pathlib import Path from sklearn.model_selection import train_test_split random.seed(42) data_root Path(./flower_images) samples [] labels [] # 每个子目录名作为一个类别标签 for cls_dir in sorted([p for p in data_root.iterdir() if p.is_dir()]): for img_path in sorted(cls_dir.glob(**/*.jpg)): if img_path.name.startswith(._): continue samples.append(str(img_path)) labels.append(cls_dir.name) # 第一次划分分出测试集 15% X_train_val, X_test, y_train_val, y_test train_test_split( samples, labels, test_size0.15, stratifylabels, random_state42 ) # 第二次划分从剩余部分再分出验证集 12% X_train, X_val, y_train, y_val train_test_split( X_train_val, y_train_val, test_size0.12, stratifyy_train_val, random_state42 )stratifylabels是关键它保证每个类别在三份数据里的占比和全集一致。random_state42固定后多次运行划分结果完全一致后续训练代码里任何用到随机数的地方也建议统一固定。另一个容易忽略的隐患是“同源图片泄漏”同一株花连续拍了十几张如果这些高度相似的图片同时落进训练集和验证集验证准确率会异常高但一到真实场景立刻打回原形。更稳妥的做法是先按文件名前缀或拍摄时间把图片聚类成组再按组划分而不是按单张图片划分。具体操作是把上面的samples从图片路径改成“拍摄组ID”级别再在每组内取代表图片。首次跑通可以直接复制文件到train_imgs/、val_imgs/、test_imgs/三个目录后面省得反复排查路径问题。2.4 数据增广策略光照抖动与随机裁剪的参数组合图片集里的图通常是“干净”的背景单一、光照均匀但真实场景里花有遮挡、有风吹模糊、有顺光和逆光。数据增广是缩小这个差距最便宜的手段但参数不能拍脑袋。from torchvision import transforms MEAN [0.485, 0.456, 0.406] STD [0.229, 0.224, 0.225] train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.05), transforms.ToTensor(), transforms.Normalize(MEAN, STD), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(MEAN, STD), ])RandomResizedCrop的scale(0.6, 1.0)表示裁剪区域占原图的 60% 到 100%。如果花在图中占比很大scale 下限调到 0.5 甚至 0.4 问题不大但如果图里花束只占画面一小块裁得太狠会把花主体切掉。ColorJitter的hue参数我一般控制在 0.05 以内花卉识别对颜色敏感色相抖动太大会把黄花和红花之间的边界打乱。验证集只做缩放和中心裁剪任何随机增广都不应该出现在验证集里否则验证指标会不稳定。提示以上 steps 里训练集和验证集的预处理要完全分开写一个加了随机一个不加这两个 transform 对象千万不能混用。3. 训练源码怎么选与怎么改迁移学习跑通花卉识别的最小方案3.1 为什么优先选迁移学习而不是从零训练花卉数据集一般只有几千到几万张图几十个类别这点数据量从零训练一个深度卷积网络很难收敛。花的类间差异有时候非常细微月季、玫瑰、蔷薇的区别集中在花萼形态和叶片纹理上这种精细特征需要大量同类样本才能让网络自己学出来。ImageNet 预训练模型已经具备成熟的边缘、纹理、形状特征提取能力花卉图片集要做的是在通用特征之上微调出花类特有的判别边界。选哪个骨干网络要看部署环境和数据规模。数据量小的时候不是网络越大越好ResNet50 虽然有更强的表达能力但同样更容易过拟合。我常用的选择逻辑是先跑通 ResNet18 拿到一个可信的基线准确率如果类目多、数据量大再换 ResNet50如果目标是手机端部署再换 MobileNetV3。网络参数量特点适用场景ResNet18约 11M轻量、收敛快、不易过拟合几十类几千张图的训练基线ResNet50约 25M表达能力强、精度上限高类目多、数据量足够、GPU 训练MobileNetV3约 2.5M体积小、推理快手机端或边缘设备部署3.2 基于 PyTorch 与 ResNet 的花卉识别训练脚本训练源码的核心就一块加载预训练权重、替换分类头、分组设置学习率。下面这个脚本是能直接跑通的最小形态逻辑上没有多余的东西。import torch import torch.nn as nn from torch.optim import AdamW from torch.utils.data import DataLoader from torchvision import datasets, models, transforms device cuda if torch.cuda.is_available() else cpu MEAN [0.485, 0.456, 0.406] STD [0.229, 0.224, 0.225] train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3, 0.05), transforms.ToTensor(), transforms.Normalize(MEAN, STD), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(MEAN, STD), ]) train_ds datasets.ImageFolder(./train_imgs, transformtrain_transform) val_ds datasets.ImageFolder(./val_imgs, transformval_transform) num_classes len(train_ds.classes) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, num_classes) model.to(device) # 骨干网络与新增分类头分开设置学习率 backbone_params [p for n, p in model.named_parameters() if not n.startswith(fc.)] fc_params [p for n, p in model.named_parameters() if n.startswith(fc.)] optimizer AdamW([ {params: backbone_params, lr: 1e-5}, {params: fc_params, lr: 1e-3}, ], weight_decay1e-4) criterion nn.CrossEntropyLoss() train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) best_acc 0.0 for epoch in range(30): model.train() train_loss, train_correct, train_total 0.0, 0, 0 for xs, ys in train_loader: xs, ys xs.to(device), ys.to(device) optimizer.zero_grad() logits model(xs) loss criterion(logits, ys) loss.backward() optimizer.step() train_loss loss.item() * xs.size(0) train_correct (logits.argmax(1) ys).sum().item() train_total ys.size(0) model.eval() val_correct, val_total 0, 0 with torch.no_grad(): for xs, ys in val_loader: xs, ys xs.to(device), ys.to(device) logits model(xs) val_correct (logits.argmax(1) ys).sum().item() val_total ys.size(0) val_acc val_correct / val_total avg_loss train_loss / train_total print(fepoch {epoch:02d} | loss {avg_loss:.4f} | val_acc {val_acc:.3f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_flower_model.pth)这段脚本里最值得琢磨的是分组学习率。backbone_params只匹配不含fc.开头的参数也就是把预训练骨干和新分类头分开给不同的学习率。骨干部分用1e-5做轻微微调避免破坏 ImageNet 上已经学好的通用特征新加的fc层是随机初始化的需要1e-3这样稍大的步长才能快速收敛。如果只在最后加了一层全连接这个配置足够如果后面还加了自定义的卷积块或注意力模块同样要把它们单独列一个组给偏大的学习率。weightsmodels.ResNet18_Weights.IMAGENET1K_V1会自动下载预训练权重并做标准化比手动下载再load_state_dict省事。注意ImageFolder的类别顺序是按目录名排序的训练前要确认train_ds.classes和业务类目顺序一致否则推理阶段输出 logits 对应的类别会错位。3.3 数据加载器参数num_workers、pin_memory 与内存的平衡DataLoader 的默认参数能跑但性能通常不是最优。上面代码里num_workers4、pin_memoryTrue是 GPU 训练的标准组合。num_workers决定用几个子进程做图片解码和预处理这个值不是越大越好普通台式机设 4 到 8 即可服务器可以到 16。设太大时进程调度的开销会超过并行收益反而变慢。pin_memoryTrue让数据张量锁页存放GPU 拷贝数据时更快但只对 GPU 训练有意义纯 CPU 训练设不设差别不大。还有一个容易踩的坑Windows 下num_workers大于 0 时训练入口必须放在if __name__ __main__:里否则多进程会递归地重新执行整个脚本直接报 RuntimeError 甚至把机器卡死。Linux 和 macOS 没有这个限制但写脚本时统一加上这个保护最稳妥。如果内存吃紧可以调低num_workers或给 DataLoader 加prefetch_factor2限制预载批次数。显存不够就先调小batch_size不要去动num_workers因为解码后的批数据在内存里跟显存是两回事。3.4 类别权重与损失函数让少数类不被淹没如果图片集里“月季”有 3000 张“某种珍稀兰”只有 80 张默认的CrossEntropyLoss会把绝大多数梯度贡献给月季模型的准确率看起来很高但珍稀兰的召回率可能接近 0。处理类别不平衡最直接的办法是给损失函数加类别权重。import numpy as np from sklearn.utils.class_weight import compute_class_weight # labels 是全体样本的类别标签列表 class_weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) # 限制权重比避免少数类权重过大导致训练震荡 class_weights np.clip(class_weights, 1.0, 10.0) weights torch.tensor(class_weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightweights)compute_class_weight(balanced)算出来的是样本数的倒数再归一化样本越少的类权重越大。直接使用原始权重有时会让少数类在 loss 里占比过高训练初期梯度震荡得很厉害所以我习惯把权重裁剪到 1 到 10 之间。如果加了权重之后少数类上来了、多数类反而掉了这是正常的评估时同时看 macro F1 而不是只看准确率。Focal Loss 是另一个思路核心是压低“已经分对的简单样本”的梯度贡献让模型更关注难样本。类别数很多且少数类特别难分的时候可以试但先别急着上普通类别权重对大多数花卉识别场景够用了。4. 花卉识别训练参数怎么定学习率、早停与曲线判断4.1 学习率与 Batch Size 的搭配从基线参数开始调训练参数里最容易翻车的是学习率其次是 Batch Size 与它的比例关系。迁移学习的经验法则是新分类头学习率是骨干的 10 到 100 倍骨干学习率尽量不超过1e-4。Batch Size 变大时梯度估计更稳定可以适当调大学习率但迁移学习场景里不要机械地按线性缩放骨干部分的学习率保持低调更安全。参数基线值调整方向骨干学习率1e-5只降不升超过 1e-4 容易把预训练特征冲乱分类头学习率1e-3分类头是随机初始化需要快点学Batch Size32显存不足时降到 16学习率同步乘 0.5优化器AdamW比 SGD 收敛快适合作为第一版基线weight_decay1e-4数据量小时调大到 1e-3 能压过拟合学习率调度CosineAnnealing训练后期平滑收敛比 StepLR 稳换了更大的骨干网络时分类头学习率可以保持但骨干学习率建议再降一半。Batch Size 和 GPU 显存强相关32 是多数情况不会出错的起点。如果显存只够跑 16先把分类头学习率降低到5e-4不要硬撑。4.2 Epoch、早停与模型保存策略花卉识别一般 20 到 30 个 epoch 就能收敛继续硬训只会开始过拟合。保存模型时有个常见误区只看最后一个 epoch但最后一个 epoch 往往不是验证集上最好的状态。早停判断建议用验证 loss 而不是验证准确率因为小验证集上的准确率跳跃很大loss 更平滑。class EarlyStopping: def __init__(self, patience6, min_delta1e-4): self.patience patience self.min_delta min_delta self.best_val_loss float(inf) self.wait 0 def check(self, val_loss, epoch): if val_loss self.best_val_loss - self.min_delta: self.best_val_loss val_loss self.wait 0 return False self.wait 1 if self.wait self.patience: print(fepoch {epoch}: early stop) return True return False # 训练循环里每个 epoch 结束后 # if early_stopping.check(val_loss, epoch): # breakpatience6表示连续 6 个 epoch 验证 loss 没有明显下降就停。min_delta1e-4是下降多少才算“有效改进”避免每轮都有微小波动重置等待计数。早停触发之后需要重新加载验证 loss 最小的那一份权重而不是用当前 epoch 的权重。这个“兜底保存”逻辑我习惯在每个 epoch 结束时同时保存两份一份是last_epoch.pth一份是验证指标最优时的best_flower_model.pth后面排查问题可以对比。4.3 训练曲线形态怎么看正常收敛、震荡与过拟合的干预训练过程中盯 TensorBoard 的 loss 曲线比盯准确率有用得多。正常收敛的曲线是训练 loss 与验证 loss 同步下降最后都趋于平坦训练 loss 略低于验证 loss 是正常的。如果训练 loss 在降验证 loss 先降后升这是过拟合的标准信号可以做的有三件事把weight_decay调到1e-3、增强数据增广、或者换回规模更小的骨干网络。如果训练 loss 和验证 loss 都降不下去卡在某个高位震荡常见原因是学习率太大了分类头学习率从1e-3降到3e-4试试同时确认类别权重没有把梯度搞得过于极端。还有一种情况训练 loss 下降但验证 loss 一路不降这往往不是参数问题而是标签噪声或验证集和训练集分布不一致。这时候先别急着调参去检查数据划分和样本质量调参救不了数据问题。5. 花卉识别训练避坑五个高频翻车点与排查方案5.1 类别不均衡导致准确率虚高现象训练结束打印出 85% 的验证准确率模型上线后某个稀有花类一张都认不对。展开混淆矩阵发现准确率主要来自几个样本量大的类样本少的类预测结果几乎全是其它类。原因CrossEntropyLoss默认对所有样本一视同仁多数类的梯度压过了少数类网络学到的是“无脑猜多数类”也能拿到不错的 loss。解决先统计每个类的样本数确认是不是有超过 10 倍差距。处理方式按优先级排给 loss 加类别权重见 3.4对少数类做过采样最后才考虑 Focal Loss。评估指标必须同时看 macro F1 和类别召回率不要只看 overall accuracy。这个坑在“花卉数据集”里尤其常见因为野生花卉样本采集难度差异极大越稀有的花图片越少。5.2 验证集准确率高、现场识别崩掉域差异比想象中严重现象验证集上 90%拿手机到公园实拍识别结果乱七八糟。初看像模型没训好但验证集确实没问题问题出在训练数据和真实场景之间隔着一道“域差异”。原因图片集里的图大多是网页下载或专业拍摄背景干净、光照均匀、花朵居正真实场景有背后的枝叶、有逆光、有风吹模糊甚至同一朵花在不同角度下看起来像两个物种。解决最有效的办法是从现场采集 200 到 500 张图组成一个小型实拍验证集单独用来评估不参与训练。训练侧把数据增广做得更“脏”一点RandomPerspective加随机透视、RandomAffine加随机仿射、ColorJitter的亮度抖动加大到 0.5让模型见过各种不完美的花。这个坑只有做过真实交付才能体会数据集再“纯净”都不如几十张现场图管用。5.3 图片集混入病虫害样本损失下降变慢、预测混乱现象训练 loss 比预期难降验证准确率上下跳得厉害。查看具体预测错误时发现模型把“有病害的花”和“健康的花”混在一起分不清楚。原因同一类花的图片里一部分是健康花一部分是带病斑、卷叶、枯黄的花。模型试图同时拟合“花形结构”与“病斑纹理”两种差异巨大的特征一个类里塞了两种视觉模式网络只能二选一。解决先把带病样本从主训练集里剔出来或者单独建一个“带病XX”类别。如果业务目标本身就是做花卉病虫害识别那就不要去跟花种类别混训应该按“病害名 寄主植物”重新设计标签体系建立一个独立的花卉病虫害数据集分支。混合训练只会让两个任务互相干扰分开建模再在部署时串行推理效果更可控。5.4 标签噪声同花异名、相似品种之间的标注混乱现象训练曲线能收敛但准确率卡在一个不高不低的位置怎么调参都上不去。抽查样本时发现验证集里“月季”类目下混着几张玫瑰标签本身就是错的。原因网络下载的图片集标签不一定可靠同一种花在不同来源里可能叫不同名字近缘种如雏菊和茼蒿菊在人眼都难分的情况下标注错误率更高。解决训练前抽查每个类别 20 张图人工过一遍这比任何算法都直接。训练之后用模型预测置信度辅助找错对验证集做预测挑出“预测类别与标签不同但置信度极高”和“置信度极低”的样本优先怀疑它们是错标或难例。把可疑样本重新归类或直接移除比加正则化更根本。记住模型给错样本强行建模等于替错误标签找规律。5.5 训练中断与断点续训别让一次断电毁掉整个实验现象训练到第 23 个 epoch机器断电或显存溢出之前的权重没有保存从头再来。原因训练脚本只保存最终模型没有保存中间 checkpoint也没有把优化器状态带进去。断点续训不只是加载模型权重还要恢复优化器的动量或自适应状态否则恢复后的训练会有一段明显的“适应期”。# 每个 epoch 结束后保存完整状态 checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, } torch.save(checkpoint, fcheckpoints/epoch_{epoch:03d}.pth) # 恢复训练 ckpt torch.load(checkpoints/epoch_022.pth, map_locationdevice) model.load_state_dict(ckpt[model_state_dict]) optimizer.load_state_dict(ckpt[optimizer_state_dict]) start_epoch ckpt[epoch] 1 best_acc ckpt[best_acc]恢复时注意如果中途改了模型结构比如分类头输出类别数变了load_state_dict会报 size mismatch这时候不要直接strictFalse蒙混过去要分清是骨干层不匹配还是分类头不匹配。骨干变了说明模型结构本身改了旧权重本来就不该硬套只有分类头变了才应该用strictFalse加载骨干权重再重新初始化分类头。训练中断这事看着概率低真碰上就是白跑几十个小时代价太大了。6. 让模型在真实场景可交付混淆矩阵、类激活图与 ONNX 导出6.1 用混淆矩阵找出最容易混淆的花类对训练结束先别急着交差跑一遍测试集画混淆矩阵。混淆矩阵能直接暴露模型的系统性错误哪两个类互相混淆、哪个类经常被漏掉比单看准确率直观得多。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt cm confusion_matrix(y_true, y_pred, labelsclass_names) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.show() print(classification_report(y_true, y_pred, target_namesclass_names))发现混淆最严重的花类对之后优先做一件事回看这两类的训练图确认是不是标签本身就不干净。如果标签没问题再决定是给这两类补充训练数据还是在业务侧把这两个类合并成一个展示类。花类之间的相似是客观存在的模型分不开也许不是模型的问题是业务对“区分度”的预期过高了。6.2 用梯度加权类激活图看模型“看”哪里准确率只能告诉你“对不对”类激活图能告诉你“看哪里”。Grad-CAM 的核心思想是把目标类回传梯度与最后一个卷积层的特征图做加权平均权重高的区域就是模型做出判断的主要依据。常见做法是直接用 torchcam 库或者手动注册 forward hook 抓特征图、注册 backward hook 抓梯度两层 hook 一合就得到热力图。给识别错误的花看图通常能看到两种典型问题模型在盯背景里的叶子而不是花或者把花盆、标签牌当成判别特征。这种错误通过调参解决不了正确做法是回到数据侧把含花盆、标签牌的样本剔除或增加遮挡花朵主体的增广逼模型去学花本身。类激活图也是向需求方解释“模型为什么错”的最好素材。6.3 ONNX 导出与推理一致性检查模型验证通过后导成 ONNX 是最通用的交付方式。PyTorch 训练好的权重导出时要注意输入尺寸和预处理必须和训练时一致否则服务端推理结果会直接跑偏。model.eval() dummy_input torch.randn(1, 3, 224, 224, devicedevice) torch.onnx.export( model, dummy_input, flower_model.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version13 )dynamic_axes把 batch 维度设成动态服务端一次推理多张图时不用重新导出。导出之后用 onnxruntime 加载同一张测试图跑一遍和 PyTorch 的输出对比误差在 1e-4 量级才算一致。不一致通常出在预处理差异或模型中包含训练模式才有的 dropout 层检查model.eval()是否在导出前执行。这个最后一步我自己吃过亏PyTorch 里跑得好好的服务端换了个预处理少做了一步归一化线上准确率直接掉了二十个点。所以从训练到部署把所有预处理逻辑固化成一份代码前后端共用比啥都可靠。这几年做下来的习惯是数据清洗和标签核对花的时间永远比训练多中途 checkpoint 每 3 个 epoch 存一份验证集里永远保留一部分现场实拍图。这三个习惯帮我避开了大多数重训的坑希望帮到你。本文还有配套的精品资源点击获取