ARTICLE DETAIL

资讯详情

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

花卉识别训练源码实战:从数据集准备到模型部署全流程

花卉识别训练源码实战:从数据集准备到模型部署全流程 简介面向花卉识别与图像分类学习者资料包提供十六种花卉、共三万二千张二百二十四乘二百二十四彩色图片的数据集每类约两千张涵盖一年蓬、三叶草、三角梅、蒲公英、油菜花等常见类别同时配套基于PyTorch的识别训练源码集成二十三种主流图片分类模型可根据需要自由选择训练。整个压缩包共一百一十个文件以txt标注文件、py训练脚本、pyc编译产物为主辅以bat启动脚本、dat数据集和jpg示例图整体大小五百八十四点七二MB。目前已有一千一百六十五人浏览学习适合用于深度学习图像分类实战、课程设计或模型对比研究。下载后可直接加载数据集进行训练与验证源码结构清晰也可参考其数据读取和模型调用方式扩展训练自己的花卉分类任务。1. 花卉识别项目到底在做什么从“认花”到训练源码的一整条链路上周有个朋友做植物科普App手里攒了2000张玫瑰照片用网上找的通用分类模型微调准确率卡在63%怎么都上不去。他缺的不是模型结构而是一套干净的数据集加上能直接跑的“训练源码”。这个标题给的就是完整闭环花卉图片集01负责喂数据训练源码负责把模型从随机权重训到能区分菊花、蒲公英、玫瑰、向日葵这几个类别。对做毕设、搭识花原型、跑教学实验的开发者来说它是最小可用单元——不需要自己爬图、清洗、写数据加载器拿到手改改路径就能开始训练。这篇笔记我从数据检查讲到训练调参再到导出部署把这条链路上的关键步骤和翻车点一次说清。2. 花卉图片集01怎么用从目录结构到数据划分的完整流程2.1 先搞清楚图片集01的目录结构不要一上来就跑训练拿到数据集的第一件事不是打开训练脚本而是先摸清目录长什么样。这个标题里的01通常代表第一批整理好的图片子集常见交付形式是一个按类别分文件夹的目录树直接兼容 torchvision 的ImageFolder接口。我一般会先执行一条tree命令看全貌。tree -L 3 flower_dataset_01/flower_dataset_01/ ├── train/ │ ├── daisy/ │ │ ├── daisy_001.jpg │ │ ├── daisy_002.jpg │ │ └── ... │ ├── dandelion/ │ ├── rose/ │ └── sunflower/ ├── val/ │ ├── daisy/ │ ├── dandelion/ │ ├── rose/ │ └── sunflower/ └── test/ ├── daisy/ ├── dandelion/ ├── rose/ └── sunflower/目录结构决定了数据加载方式train、val、test三个文件夹已经按类别分子目录PyTorch的datasets.ImageFolder能直接把子目录名当作标签读取不需要单独写 CSV 标签文件。这里要确认三个信息类别总数、每类图片数量、每张图的尺寸和格式。类别数决定模型最后全连接层的输出维度图片尺寸决定要不要在预处理里做 Resize格式JPG/PNG/BMP则影响解码兼容性。常见的情况是 train 每类几百张val 每类三五十张test 留给最终验证。如果发现 val 和 test 目录是空的那就需要手动从 train 里抽一部分出来这个操作放到 2.3 节说。目录结构确认没问题后下一步进入数据体检。2.2 数据检查脚本坏图和错标是训练翻车的第一源头数据集的“干净程度”直接决定训练源码能不能跑出正常指标。我在动手训练前必跑一遍数据检查脚本统计每类图片数量、找出损坏文件、捕捉尺寸异常。经验是大部分训练中断不是模型写得不对而是数据集里有几张截断的 JPG。import os import collections from PIL import Image data_root flower_dataset_01/train classes [d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))] counter collections.Counter() bad_images [] size_set set() for cls in classes: cls_dir os.path.join(data_root, cls) for name in os.listdir(cls_dir): path os.path.join(cls_dir, name) counter[cls] 1 try: with Image.open(path) as im: im.load() # 强制解码截断文件会在这里抛异常 size_set.add(im.size) except Exception as e: bad_images.append((path, str(e))) print(每类图片数量) for cls, cnt in counter.most_common(): print(f {cls}: {cnt}) print(f图片尺寸种类{size_set}) print(f损坏文件数{len(bad_images)}) for path, err in bad_images[:10]: print(f {path} - {err})脚本逻辑分三段先遍历类别目录对每张图用Image.open打开并调用load()强制解码这样能捕获文件头正常但数据截断的坏图同时把所有图片尺寸收集到集合里一眼看出是否有尺寸不统一的情况。最后打印类别分布检查有没有某类图片数特别少。参数说明这里用os.listdir而不是glob是为了拿到子目录名作为类别标签Image.open本身是惰性加载不加load()的话很多损坏图会漏检。如果发现某个类只有十几张而其他类有几百张那这个数据集天然存在类别不平衡后面训练时验证集准确率会虚高。如果发现尺寸集合里有多个不同分辨率比如有 1920x1080 也有 224x224就需要统一在预处理里Resize否则数据加载器会报维度不匹配错误。2.3 数据划分与验证策略train/val/test 怎么切才不浪费数据集没有现成验证集时手动划分是避不开的步骤。常见做法是按 70/15/15 的比例切成 train/val/test但要保证每个类别内的比例一致不能直接把整个 train 文件夹随机切——那样很可能某个类全进了训练集验证集里一个样本都没有准确率直接报 0。import os import shutil import random from collections import defaultdict source_root flower_dataset_01/all_images # 假设所有类别图片集中存放 target_root flower_dataset_01 split_ratio (0.7, 0.15, 0.15) random.seed(42) # 固定随机种子保证结果可复现 for cls in os.listdir(source_root): cls_dir os.path.join(source_root, cls) if not os.path.isdir(cls_dir): continue images os.listdir(cls_dir) random.shuffle(images) n_train int(len(images) * split_ratio[0]) n_val int(len(images) * split_ratio[1]) for split_name, split_images in zip( [train, val, test], [images[:n_train], images[n_train:n_train n_val], images[n_train n_val:]] ): out_dir os.path.join(target_root, split_name, cls) os.makedirs(out_dir, exist_okTrue) for img in split_images: shutil.copy2(os.path.join(cls_dir, img), os.path.join(out_dir, img))这段脚本按“每个类别内部先 shuffle 再切比例”的方式划分比全局 shuffle 好在每个类在三个集合中的占比一致验证结果不会被某些类别缺失带偏。参数说明random.seed(42)固定随机种子保证每次跑出来的划分一致后续换模型对比才有意义shutil.copy2保留文件元信息比copy更适合大批量文件。划分后建议再跑一遍 2.2 的统计脚本确认每类图片数在 train/val/test 中比例均衡。数据增强是另一个影响训练源码效果的关键点。对于几千张的中小规模花卉数据集训练时加上随机水平翻转、随机旋转、颜色抖动能显著提升模型泛化能力这部分我放到第 4 章结合训练参数一起说因为增强幅度和学习率是配套调的只增强不加正则照样过拟合。3. 花卉识别训练源码怎么跑通基于 PyTorch 的最小复现路径3.1 数据加载与模型实例化用 ImageFolder 和预训练 ResNet18 起步标题里的“训练源码”通常包含完整的训练脚本但很多源码拿下来跑不通问题多半出在数据加载器和模型输出维度对不上。这里给出一段我在类似花卉识别场景下验证过的最小训练代码逻辑和 torchvision 官方微调示例保持一致的风格方便后续替换成自己的数据路径。import torch from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 数据增强与归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), 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((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(flower_dataset_01/train, transformtrain_transform) val_dataset datasets.ImageFolder(flower_dataset_01/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) num_classes len(train_dataset.classes) print(f类别{train_dataset.classes}数量{num_classes})ImageFolder的标签顺序是按子目录名的字母序自动生成的所以daisy是 0、dandelion是 1 这种映射关系在每次运行中都是确定的不需要手动维护标签表。Resize((224, 224))把输入统一到固定尺寸因为这个尺寸和后面预训练模型的输入要求一致。Normalize里的均值和标准差是 ImageNet 预训练统计量如果换成随机初始化的模型这里就不能沿用。DataLoader参数里num_workers4表示用 4 个子进程加载数据能减少 GPU 等待时间shuffleTrue只在训练集开启验证集保持顺序保证评估结果稳定。如果数据集特别小每类少于 50 张batch_size32可以降到 16避免一个 batch 里几乎全是同一类让梯度更新失衡。模型部分我选择用预训练的 ResNet18 而不是从零搭 CNN。花卉识别场景下数据集规模通常只有几千到几万张从零训练的卷积网络很难学到足够泛化的特征而 ImageNet 预训练权重已经包含了大量纹理、边缘、颜色特征只需要把最后的全连接层替换成自己的类别数。model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc torch.nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)关键参数说明weightsmodels.ResNet18_Weights.IMAGENET1K_V1在 torchvision 新版本里取代了旧的pretrainedTrue写法加载的是 ImageNet-1K 训练好的权重model.fc.in_features动态读取原全连接层的输入维度ResNet18 是 512替换成我们自己类的输出数这样不用硬编码类别数。AdamW比原始 Adam 多了解耦权重衰减配合weight_decay5e-4相当于给所有参数加了 L2 正则对付小数据集过拟合很有效。3.2 训练循环与模型保存只在验证集最优时存盘训练循环本身不难但模型保存策略直接关系到后面部署时用的是哪个权重。很多训练源码里每轮都覆盖保存最近的模型结果训练 30 轮后拿到的是最后 1 轮的权重而最后 1 轮往往已经过拟合。我更习惯的做法是每轮在验证集上评估一次只有当验证准确率刷新历史最高时才执行torch.save。best_acc 0.0 num_epochs 30 for epoch in range(num_epochs): model.train() running_loss 0.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() running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_dataset) model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1:02d}/{num_epochs} | Loss: {epoch_loss:.4f} | Val Acc: {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f - 保存最佳模型验证准确率 {best_acc:.4f})训练循环里的每个环节有明确分工model.train()启用 Dropout 和 BatchNorm 的训练模式model.eval()切换回推理模式两者不能混用否则验证集指标会失真。optimizer.zero_grad()清空上一步的梯度缓冲loss.backward()反向传播计算梯度optimizer.step()更新参数这个顺序是 PyTorch 的固定流程少一步梯度就会累加导致 loss 异常波动。验证阶段用torch.no_grad()包裹告诉 PyTorch 不需要计算梯度能省掉大量显存和计算时间。torch.max(outputs, 1)返回预测类别索引与真实标签labels逐位比较后累加准确率。模型只保存state_dict而不是整个模型对象这样部署时只需要重新构建相同结构再加载权重文件更小也更灵活。3.3 学习率调度与训练轮数先让模型收敛再谈调优上面代码里使用了CosineAnnealingLR(optimizer, T_max30)这表示学习率会在 30 个 epoch 内从初始值余弦下降到接近 0。这样的曲线设计比固定学习率更适合小数据集训练前期学习率大模型快速进入正确参数区域后期学习率小参数在局部最优附近精细震荡。如果训练中验证损失在某个 epoch 后开始回升说明学习率下降速度跟不上过拟合速度可以把T_max调小到 20或提前设置早停。训练轮数 30 是预训练模型微调场景的经验值轮数太少模型只学了少量新增数据的特征轮数太多特征开始记忆训练集噪声。具体判断标准是验证准确率曲线如果验证准确率连续 5 个 epoch 不上升直接停止训练用已有的 best_model.pth 就行不用等满 30 轮。这类训练源码能跑通的标志不是 loss 降到 0而是验证集的准确率在 85% 以上且 loss 曲线没有剧烈振荡。4. 花卉识别训练参数怎么调让训练源码真正长出效果的三个核心变量4.1 学习率策略全层统一 vs 分层设置把训练源码跑通只是第一步要让模型指标从“能跑”变成“能用”学习率是最值得反复试的参数。迁移学习场景下有一个常见误区对预训练 backbone 和随机初始化的全连接层用同一个学习率。backbone 的特征提取能力已经很成熟学习率太高会把预训练权重冲毁全连接层是随机初始化的学习率太低又学不动。我一般把全连接层的学习率设为 backbone 的 10 倍用参数组的方式传给优化器。optimizer torch.optim.AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: [p for name, p in model.named_parameters() if not name.startswith(fc)], lr: 1e-4}, ], weight_decay5e-4)这里用参数组params列表替代直接传model.parameters()第一组只包含全连接层的参数第二组过滤掉以fc开头以外的所有参数。两个组的初始学习率相差 10 倍backbone 以较慢速度微调全连接层以较快速度从随机权重适配当前数据集。如果发现训练前期 loss 下降太慢可以检查是不是 backbone 学习率设得偏低如果训练后期验证准确率抖动剧烈可以先尝试把两组学习率同时缩小 3 倍。4.2 batch size、输入分辨率与显存的三角关系花卉识别数据集的图片尺寸五花八门统一 Resize 的尺寸和 batch size 共同决定显存占用。直接把 224 分辨率配 batch 64 在很多显卡上会 OOM而把分辨率降成 128 又可能损失细小视觉特征比如花瓣纹理这个取舍需要结合手里的硬件资源决定。输入分辨率batch size显存占用参考适用场景128x12864约 4 GB显存紧张快速验证想法224x22432约 6 GB常规训练平衡速度与精度224x22464约 11 GB显卡较富裕如 2080Ti 及以上320x32032约 14 GB追求精度小规模数据微调这个表格只是经验参考值实际占用还受num_workers、数据加载方式和模型复杂度影响。如果显存溢出最优先的调整不是换模型而是把batch_size减半并同步把学习率按比例缩小——因为 batch size 减半后每个 epoch 的梯度更新次数变多学习率不变容易震荡。常规做法是把学习率乘以sqrt(0.5)约 0.707比如从 1e-3 调到 7e-4。4.3 数据增强的幅度过了是毒药少了是欠拟合数据增强是训练源码里唯一一个“加多了翻车加少了也翻车”的玄学参数。花卉数据集比一般物体分类更依赖颜色和纹理增强太猛会把真正的类别特征削弱。比如超过 30 度的随机旋转会让“向日葵”和“菊花”的边界变得模糊而ColorJitter的亮度抖动太大时不同光照下拍摄的同一种花可能被模型当成不同类别。我常用的增强参数基准是水平翻转概率 0.5、随机旋转范围正负 15 度、ColorJitter 的 brightness/contrast/saturation 均设 0.2、RandomResizedCrop 的 scale 范围 0.8 到 1.0。如果训练集图片数量少每类低于 100再把旋转范围提高到 20 度并加上RandomAffine(translate(0.1, 0.1))增强平移的泛化能力。验证集和测试集不做任何随机增强只用 Resize 和标准化保证评估指标的稳定性和可比性。4.4 早停与最佳模型的“后悔药”训练过程里最常见的浪费是把 30 轮跑完后发现第 9 轮的权重最好后面 20 轮全部白跑。为了避免这种事后拍大腿我在 3.2 节里用了“只在验证准确率刷新时保存”的模式这相当于给自己留了后悔药。更进一步可以加上早停逻辑记录连续多少轮验证准确率没有上升达到阈值就自动终止训练。patience 7 no_improve 0 # 在验证评估之后追加这段逻辑 if val_acc best_acc: no_improve 1 if no_improve patience: print(f连续 {patience} 轮验证准确率未提升提前停止训练) break else: best_acc val_acc no_improve 0 torch.save(model.state_dict(), best_model.pth)参数patience7表示容忍连续 7 轮不提升超过就直接终止训练。这个值不是随便设定的数据集噪声越大、增强越强验证准确率波动越大patience 需要设得大一些如 10数据干净且任务简单patience 设 5 就够。早停的意义不仅是节省时间更是防止过拟合——训练后期模型在训练集上持续变好但验证集指标已经开始走下坡路这时继续训练只会记录更多训练集噪声。训练结束后用best_model.pth做测试集评估而不是用最后一轮的权重这是和本文描述的训练源码对齐的标准操作。5. 花卉识别训练避坑5 个让源码停摆和指标虚高的典型问题5.1 损坏图片导致训练中断OSError: image file is truncated现象训练跑到第 3 个 epoch 突然中断控制台报错OSError: image file is truncated继续运行还可能在另一批图片上复现。原因数据集中存在下载不完整或采集过程中被截断的 JPG 文件。PIL默认遇到这种文件会抛异常而数据加载器拿到异常直接终止整个训练循环。解决用 2.2 节的数据检查脚本预先扫一遍所有图片把异常文件过滤掉。还有一个常见补救办法是在Image.open前设置 PIL 的容错模式from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES True这个设置告诉 PIL 遇到截断图片时尝试跳过损坏部分继续解码而不是直接抛异常。但它只是一个无奈之举——图被强行解码后内容可能缺失部分像素模型能跑但精度会受到一定影响。真正稳妥的路径是检查阶段就把坏图片移到一个broken/目录重新统计类别数量后开始训练。我的经验是花 5 分钟检查能省掉 2 小时排查训练中断的功夫。5.2 验证准确率很高但真实场景一塌糊涂现象训练结束后验证集准确率显示 95%拿手机拍的路边野花喂给模型识别结果完全不对。原因经典的数据集分布偏差。验证集和训练集来自同一个图片源光照、角度、背景风格高度一致模型学到的其实是“特定拍摄风格下的花”而不是“花”这个概念。这是花卉识别项目中最坑的指标陷阱。解决重新审视验证集的来源——如果你手头的图片集01全部来自同一批网络爬图那它的 val 分区和 unseen 场景差距可能很大。我一般会在准备好训练集后额外挑几张完全没有见过的实拍图比如用手机在户外现场拍放进一个独立文件夹训练结束后专门测这个小样本集。测试集设计上要让背景、光照和训练集明显不同只有这样的验证结果才对真实部署有参考价值。5.3 训练集 loss 持续下降、验证集准确率从第 8 轮开始反弹现象训练过程看起来一切正常训练集 loss 从 1.2 一路降到 0.15验证准确率在第 8 轮达到 88% 后开始下降到第 20 轮只剩 76%。原因过拟合开始时间早于训练结束时间。模型在训练集上记忆了噪声和细节丧失了在未见数据上的泛化能力。没有早停机制的训练源码会持续记录越来越差的权重。解决先把早停逻辑加上patience 设 7。同时检查数据增强幅度特别是旋转角度和裁剪 scale 是否过强再确认weight_decay是否加了至少 5e-4 到 1e-3。如果增强和正则都没问题还有一招是减小模型容量——把 ResNet18 换成更轻的resnet18分支结构或MobileNetV3这类小型网络这是小数据集的兜底方案。5.4 显存不足RuntimeError: CUDA out of memory现象训练脚本在第一个 epoch 快结束时突然报CUDA out of memory前面所有 epoch 都正常运行。原因显存被临时张量占满。常见诱因有三个输入图片尺寸在某批数据里异常大比如 4000x3000 原始图直接进网络、batch size 和当前显存不匹配、验证阶段和训练阶段叠加导致显存峰值超出阈值。解决第一步把batch_size从 32 降到 16并同步按 4.2 节说明的方式调整学习率。第二步在数据预处理里增加一个强制 Resize确保任何输入最终都变成 224x224而不是靠RandomResizedCrop来兜底。第三步把验证评估放进with torch.no_grad():的上下文中独立执行验证结束后再做下一轮训练。如果还是不够在训练循环里每轮结束时调一次torch.cuda.empty_cache()清理缓存这也算是个有效的止血办法。5.5 训练刚开始 loss 就在 0 附近或准确率突然跳变现象第一个 epoch loss 显示 0.01验证准确率直接 100%或者训练到一半准确率突然从 80% 跳到 50%。原因loss 从 0 附近开始说明标签和网络输出可能错位了。检查是否在数据加载时把ImageFolder的类别顺序和你自己手写的标签映射搞混了。准确率跳变则常见于学习率设置过大优化过程越过了一个良好的参数区域模型输出分布被破坏。解决打印几个样本的映射关系确认输入图片对应的路径、train_dataset.classes、模型输出维度。我习惯在训练前加一段临时验证代码取出train_dataset[0]和train_dataset[1]分别打印标签索引和实际文件夹名对比。学习率跳变问题则把初始学习率降一个量级比如 1e-3 降到 1e-4同时检查scheduler的更新时机是否放在每个 optimizer 更新之后而不是 epoch 结束之后。这个小细节在训练源码里经常写错——学习率调度每轮更新一次是常规操作但如果把调度器放在optimizer.step()后面配合CosineAnnealingLR使用学习率实际下降速度会比你预期的快一个 epoch 倍率。6. 把训练好的模型放进真实应用ONNX 导出与单张图片推理验证训练脚本跑完拿到 best_model.pth 之后下一步是把模型从 PyTorch 环境里解放出来。我习惯导出 ONNX 格式好处是推理时不需要再依赖 PyTorch 的训练组件直接用 ONNX Runtime 就能跑部署到服务器或嵌入式环境都更轻量。导出代码很短但有一个参数必须留意。import torch from torchvision import models model models.resnet18(weightsNone) model.fc torch.nn.Linear(512, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, flower_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version12, )这里把dynamic_axes设成了 batch 维度动态变化调用方可以一次传 1 张或 32 张图都合法。如果不需要动态 batch把dynamic_axes整个删掉导出文件会更小且推理性能更好。opset_version 我选 12旧版本 ONNX Runtime 基本兼容新版本功能用不上反而可能引入算子兼容问题。推理验证我习惯写一个独立的脚本和训练源码完全分离避免互相污染。流程是把图片 Resize、归一化、过模型、取 Top-5 概率。import onnxruntime as ort import numpy as np from PIL import Image from torchvision import transforms ort_session ort.InferenceSession(flower_model.onnx, providers[CPUExecutionProvider]) img Image.open(test_rose.jpg).convert(RGB) tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) tensor tf(img).unsqueeze(0).numpy() outputs ort_session.run([output], {input: tensor})[0] probs np.exp(outputs) / np.sum(np.exp(outputs), axis1, keepdimsTrue) top5 np.argsort(probs[0])[::-1][:5] class_names [daisy, dandelion, rose, sunflower] for idx in top5: print(f{class_names[idx]}: {probs[0][idx]:.4f})推理脚本里有个值得注意的技术细节ToTensor()已经把像素值归一化到 0~1所以后面只做 ImageNet 标准化的减均值除标准差不需要再除以 255。ONNX Runtime 输入必须是 numpy 数组不能直接喂 torch tensor所以加了.numpy()。np.exp(outputs)把模型输出的 logits 转成概率分布再按总和归一化最后取 Top-5 看模型对不同花卉类别的置信度分布。我用手机拍了几张小区里不同角度、不同光线下的“路边野花”照片做真实场景验证发现对阴影覆盖的花朵识别准确率明显偏低。后来我在训练数据增强里额外加了RandomGrayscale(p0.1)和更强的ColorJitter让模型不再过度依赖色彩通道这个问题才好转。这个教训让我养成了一个习惯训练结束后不要急着部署先用真实场景照片跑一轮推理看看错误的样本集中在哪种光照、角度和背景条件下再回到训练源码补增强手段。希望帮到你。本文还有配套的精品资源点击获取
返回列表