
简介花卉识别方向的数据集与训练代码组合包面向计算机视觉初学者、算法工程师及高校相关课题人员解决花朵分类任务中训练数据不足与模型选型困难的问题。包内包含16种花卉共32000张224×224彩色图片涵盖千屈菜、射干、曼陀罗、桔梗、狗尾草、秋英、粉黛乱子草、红花酢浆草、黄金菊等常见与观赏植物每类约2000张分布均衡可直接用于训练、验证与测试。配套训练源码基于TensorFlow编写集合了23种主流图片分类模型支持根据场景灵活选择网络结构开展对比实验也便于二次改造。全部文件共110个以txt标注/说明、py训练脚本、pyc编译文件为主另有bat启动脚本与jpg示例图压缩包整体684.45MB目录层级清晰。已有1499人学习下载适合需要现成花卉数据集和多种模型基线、快速搭建分类项目或撰写实验报告的开发者。1. 花卉识别训练源码从一张花图到可部署模型的最小闭环做花卉识别的时候大部分人第一步不是选模型而是去整理一堆名字乱七八糟的花卉图片集。标题里的“训练源码”和“花卉数据集”其实是同一个项目里的两座大山数据决定了模型的上限源码和参数决定了这个上限能不能够到。我见过太多训练日志漂亮、一到户外拍几张真花就翻车的案例问题往往不在网络结构而在图片集的标签噪声和训练脚本里的细节。这篇就围绕“花卉识别-花卉数据集-花卉识别训练源码-花卉图片集02”这个标题把数据整理、迁移学习训练、日志判读、避坑和推理部署串成一个可复现的完整闭环适合准备用自己图片集做花卉分类的工程师和学生照着重跑一遍。2. 把花卉图片集整理成训练集目录划分、去重与标签映射2.1 先用公开花卉数据集验证再决定是否自制数据集市面上的公开花卉数据集不算少常见的有 Oxford 102 Flower 这类按类别组织好的图片集也有一批按花期、颜色、病虫害细分标注的数据集。这些公开集的好处是标签已经清洗过有相对标准的类别划分和拍摄场景适合拿来当“标准答案”跑通训练源码、验证模型有没有选对。坏处是公开集的类别往往和实际业务对不上你想识别的是自己园区里的十几种花公开集却按植物学分类给你一百多个类别视角也不一致。我一般这么定位先用公开花卉数据集把训练脚本和超参跑一遍确认那一套源码在这个任务上能收敛、不翻车再切换到自己的花卉图片集。自制的图片集优势是贴合业务场景劣势也很明显——采集来的图片命名混乱、重复度高、同一种花在不同光照和角度下差异悬殊标签错一张就是一颗老鼠屎。标题里既然出现了“花卉数据集”就别轻视这一步训练前花两小时整理目录能省下训练后两周的排错时间。2.2 目录划分脚本按类别拆出 train/val/testPyTorch 的torchvision.datasets.ImageFolder要求数据按类别目录/图片文件组织并且会把每个子目录名当成一个类别。常见做法是在数据集根目录下再分train/val/test三个子目录每个子目录里再放类别文件夹。下面这段脚本能把一堆散乱的类别图片按比例拆开并保证每个类别都均匀分到三个集合里。import os import random import shutil from collections import defaultdict # 配置 src_dir raw_flowers # 原始图片根目录每个子目录一个类别 dst_dir flowers_dataset # 输出目录 train_ratio 0.7 # 训练集比例 val_ratio 0.2 # 验证集比例 seed 42 random.seed(seed) # 收集每个类别的所有图片 class_images defaultdict(list) for class_name in os.listdir(src_dir): class_path os.path.join(src_dir, class_name) if not os.path.isdir(class_path): continue for fname in os.listdir(class_path): if fname.lower().endswith((.jpg, .jpeg, .png)): class_images[class_name].append(fname) # 按类别独立划分并复制 for class_name, fnames in class_images.items(): random.shuffle(fnames) n len(fnames) n_train int(n * train_ratio) n_val int(n * val_ratio) splits { train: fnames[:n_train], val: fnames[n_train:n_train n_val], test: fnames[n_train n_val:], } for split_name, split_files in splits.items(): out_class_dir os.path.join(dst_dir, split_name, class_name) os.makedirs(out_class_dir, exist_okTrue) for fname in split_files: src os.path.join(src_dir, class_name, fname) dst os.path.join(out_class_dir, fname) shutil.copy2(src, dst)这段脚本的核心逻辑是“按类别洗牌后再切分”。如果先给所有文件全局洗牌再按比例切某些类的图片可能会全部落到训练集验证集里缺了这个类训练日志上的 acc 就会虚高模型实际泛化能力完全看不见。另一个细节是用了copy2而不是move这样即使后续发现某个类需要重新划分原始图片还在不吃后悔药。比例参数上train 占 0.7、val 占 0.2、test 留 0.1 是常见默认值。类别越多、每类图片越少train 的比例不妨降到 0.8保证验证集每类至少有 5 到 10 张。数据量特别少的时候每类不到 20 张我会只分 train 和 val 两部分test 直接复用真实场景拍摄的几张照片而不从这份小数据集里再抠。2.3 标签映射与不平衡检查训练前最后一关ImageFolder会自动生成class_to_idx映射规则是按子目录名的字母序从 0 编号。这个“自动”既是方便也是隐患如果你在训练中途改过文件夹名或者某个子目录里混进了别类的图映射就悄悄变了。训练前必须花两分钟检查各类别数量分布脚本如下import os from torchvision import datasets data_root flowers_dataset for split in [train, val, test]: ds datasets.ImageFolder(rootos.path.join(data_root, split)) # 类别名与数字索引的映射 print(f {split} class_to_idx: {ds.class_to_idx}) # 按类别统计图片数量 class_count {} for path, idx in ds.samples: class_name ds.classes[idx] class_count[class_name] class_count.get(class_name, 0) 1 for name, cnt in sorted(class_count.items(), keylambda x: x[1]): print(f {name}: {cnt})这段代码做两件事一是把class_to_idx打出来人工核对类别名和索引是否和预期一致二是统计每个类的图片数。如果某个类明显少于其他类训练时它的 loss 贡献会被大类别淹没。处理方法有几种给少样本类别做简单过采样重复读图、在WeightedRandomSampler里按类别数量反比采样或者干脆补拍一批该类的图片。数据不平衡在花卉识别里经常被忽略因为表面上看每一类都有几十张图但实际拍摄条件下某些花就是难拍到这类“尾巴类”往往才是上线后误判的重灾区。3. 选 backbone 和训练框架迁移学习是花卉识别最快的落地路径3.1 为什么从 ResNet18 而不是从零训练开始从零训练一个 CNN 分类网络需要百万张图片作为支撑花卉图片集的规模通常在每类几十到几百张从零训练几乎必然过拟合。ImageNet 上预训练过的模型已经学会边缘、纹理、形状这些通用特征花卉识别需要做的只是在这些特征之上“微调”出一个新的分类头。这个过程在工程上叫迁移学习也是当前从业者做小数据集图像分类最稳妥的默认方案代码简单、收敛快、泛化也远比从零训练好。backbone 选 ResNet18 是基于成本和收益的权衡。ResNet18 只有约 1100 万参数单张消费级显卡能轻松跑起来推理速度也快在 10 到 20 类花卉的常见任务里精度已经够用。只有当你发现模型在验证集上仍然欠拟合、且所有调参手段用尽时才考虑升级到 ResNet50 或 EfficientNet-B0。输入分辨率也可以从 224 提到 384这比盲目加大网络层数更直接有效但显存占用会翻倍。如果目标是识别月季和玫瑰这种细粒度品种224 分辨率往往不够捕捉花瓣细节那从一开始就应该用 ResNet50 高分辨率输入。3.2 最小可运行的 PyTorch 训练脚本下面是一个可复现的花卉识别训练脚本从读取数据集到保存最优权重一步到位。配合上一章整理好的目录结构换数据集时只需要改data_root和num_epochs。import os import json import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, models, transforms # 超参数 data_root flowers_dataset batch_size 32 num_epochs 30 lr 1e-3 device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据增强与归一化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_transform 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(os.path.join(data_root, train), transformtrain_transform) val_ds datasets.ImageFolder(os.path.join(data_root, val), transformval_transform) num_classes len(train_ds.classes) print(类别数:, num_classes, 类别:, train_ds.classes) train_loader DataLoader(train_ds, batch_sizebatch_size, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_sizebatch_size, shuffleFalse, num_workers4) # 加载预训练模型并替换分类头 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lrlr, weight_decay1e-4) # 训练循环 best_val_acc 0.0 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_ds) # 每个 epoch 结束做一次验证 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) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total print(fepoch {epoch1}/{num_epochs} loss{epoch_loss:.4f} val_acc{val_acc:.4f}) # 只保存验证集上表现最好的权重 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_flower_model.pth) # 保存类别名供推理时使用 with open(class_names.json, w) as f: json.dump(train_ds.classes, f, indent2)几个关键逻辑点需要展开说明。第一train_transform和val_transform必须分开训练集用随机裁剪和颜色扰动验证集只用固定缩放和中心裁剪否则验证集的评估会被随机增强干扰每次跑的 acc 都不一样。第二model.fc被替换成新的全连接层这一层的参数从零开始随机初始化需要更大的学习率去适应而预训练部分的 backbone 已经收敛学习率过大会把学到的通用特征摧毁。第三保存权重时只认验证集上的最优表现不在测试集上反复挑模型否则测试集就失去了真实评估的意义。num_workers4在 Windows 上偶尔会遇到多进程数据加载报错如果遇到就直接改成num_workers0。损失函数CrossEntropyLoss自带 softmax所以模型的输出不需要再做一次 softmax直接用 logits 算 loss 更数值稳定。3.3 训练参数怎么设lr、batch_size、epoch 与设备的匹配训练参数不能脱离硬件和数据量单独谈。下面是不同设备上我用过的参考配置表格里的组合至少能保证“训练跑得起来”训练场景backbonebatch_size学习率epochCPU仅功能验证ResNet1881e-410-156-8GB 显存显卡ResNet18321e-330-5012GB 以上显卡ResNet50642e-350细粒度品种识别ResNet50 384输入161e-340-60batch_size 直接影响学习率的合理取值。32 是 ResNet18 在常见消费级显卡上的甜点值显存不够就降到 16 或 8但不要只降 batch_size 不降学习率否则梯度噪声偏大、训练不稳定。AdamW 配合weight_decay1e-4是我在分类任务上的默认组合比纯 Adam 更好控制过拟合。epoch 的多少要看验证集曲线花卉小数据集上通常 30 个 epoch 内就能看到明显收敛超过 50 个 epoch 后边际收益很低更多是在做无用功。学习率的设定还有一个误区很多人微调时直接沿用 ImageNet 训练的初始学习率例如从头训练用的 0.1结果第一个 epoch loss 直接飞掉。迁移学习的正确姿势是从1e-3附近开始如果发现 loss 震荡就降到1e-4。你可以简单地把学习率理解成“模型信任新数据的程度”数据越脏、类别越细学习率就要越保守。4. 跑通训练之后数据增强、warmup 与训练日志判读4.1 数据增强花卉图片最有效的几个操作花卉识别的图片天然存在三个问题拍摄尺度差别大、光照条件各不相同、背景复杂多变。数据增强就是为了让模型不依赖这些与花无关的表面特征。以我的经验最有效的三个操作是RandomResizedCrop、RandomHorizontalFlip和ColorJitter。RandomResizedCrop随机截取图片的一部分并缩放到目标尺寸模拟远近不同的拍摄距离。对花卉来说这特别重要因为同一个品种的特写和远景照片差别非常大。RandomHorizontalFlip几乎不改变花的语义但能成倍扩充训练样本。ColorJitter模拟一天内不同时段的光照变化尤其在室外拍摄的图片集里非常有效。要注意别做过头。见过有人给花卉图片加RandomRotation(45)结果训练时频繁出现倒挂的花模型被迫学习“转正”而不是学习“这是什么花”。还见过把病害叶片的图片和健康花朵混在一个类别里训练数据增强反而会把病斑特征放大让模型学到捷径——这个问题在涉及花卉病虫害数据集时尤其容易踩后面避坑章还会展开。4.2 学习率 warmup 与余弦退火让训练过程更稳训练初期模型权重刚从预训练迁移过来直接上大学习率会让 loss 飙升。warmup 的思路是先用小学习率跑几个 epoch等 loss 稳定下来再把学习率升到设定值。余弦退火让学习率随训练进度从高到低平滑衰减帮助模型在后期做精细收敛。两者搭配是当前图像分类训练里的常见做法代码量不大收益却明显。from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR warmup_epochs 3 total_epochs 30 scheduler SequentialLR( optimizer, schedulers[ LinearLR(optimizer, start_factor0.1, total_iterswarmup_epochs), CosineAnnealingLR(optimizer, T_maxtotal_epochs - warmup_epochs) ], milestones[warmup_epochs] )这段代码配合上一章的训练循环即可。SequentialLR把两个调度器串起来前 3 个 epoch 用LinearLR从 10% 的学习率线性升到全量后面 27 个 epoch 走余弦退火。milestones是切换调度器的节点必须和warmup_epochs保持一致。注意T_max填的是余弦退火覆盖的 epoch 数而不是总 epoch 数写错了学习率曲线会不完整。4.3 训练日志怎么读loss、top-1 acc 和过拟合信号训练脚本每个 epoch 打印一条日志新手往往只盯 val_acc忽略了 loss 和 acc 之间的关系。下面是一个典型的小数据集训练过程epoch 1/30 loss1.8563 val_acc0.6214 epoch 5/30 loss0.5248 val_acc0.8027 epoch 10/30 loss0.2179 val_acc0.8936 epoch 15/30 loss0.1205 val_acc0.9062 epoch 25/30 loss0.0680 val_acc0.8849 epoch 30/30 loss0.0431 val_acc0.8613前 15 个 epoch 是正常的训练 loss 从 1.8 降到 0.12val_acc 同步从 62% 涨到 90%说明模型在学习。但从第 15 个 epoch 开始训练 loss 还在继续下降val_acc 却不升反降这是典型的过拟合信号。对花卉小数据集来说这个转折点通常出现在 15 到 25 个 epoch 之间不用等到 30 个 epoch 跑完才能判断。发现过拟合后优先做三件事一是给数据增强加码比如提高ColorJitter的幅度或加入RandomAffine的轻微平移二是给分类头加一点 Dropout三是把weight_decay从1e-4增大到5e-4。所有手段都试过仍然过拟合才考虑减少 backbone 的参数量或提前停止训练。val_acc 和 train_acc 相差 5 个百分点以内是正常区间差超过 10 个点就要警惕。训练日志本身就是黑匣子的唯一窗口多记录、多对比比反复改网络结构更能定位问题。5. 花卉识别训练避坑5 个让我重跑数据的真实问题5.1 同一张图同时出现在训练集和验证集val_acc 虚高不真实现象训练日志上 val_acc 一路升到 95% 以上模型上线后拿真实场景的图片一测准确率掉到 60% 附近完全不是训练时的水平。原因数据集里存在重复图片。最常见的是从网上采集的花卉图片同一个文件被改名为不同文件名或同一朵花的照片被不同来源重复收录。如果这些重复图恰好分别落在 train 和 val 里模型相当于提前看到了“答案”val_acc 自然虚高。解决划分数据集之前必须先做去重。按文件 MD5 建索引找出内容完全相同的图片import hashlib from pathlib import Path def file_md5(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() md5_to_path {} for img_path in Path(raw_flowers).rglob(*): if img_path.suffix.lower() not in {.jpg, .jpeg, .png}: continue md5 file_md5(img_path) if md5 in md5_to_path: print(f重复图片: {img_path} 与 {md5_to_path[md5]}) else: md5_to_path[md5] img_path这段脚本会打印所有内容相同的图片对。处理方式不是简单地删一张而是要人工看一眼如果是同一朵花的重复照片删掉其中一份如果是同一类别下的相似但不同照片MD5 不会判重说明不在这个问题的范围内。MD5 去重只解决完全相同的文件无法识别“同一素材被重新压缩或加了水印”的变体这类情况就得靠感知哈希了但作为第一道防线已经够用。5.2 类别标签错乱class_to_idx 与文件夹名对不上现象训练了十几个 epoch训练 loss 不降或者降得极慢val_acc 一直在类别数分之一附近徘徊例如 10 分类时 acc 稳定在 20% 上下。原因某个类别文件夹里混入了其他类的图片或者手工改过文件夹名但没重新生成标签文件。ImageFolder 按字母序自动生成索引你以为“玫瑰”是 0 号类别实际 0 号可能是“牡丹”。解决训练脚本里加上 2.3 小节的类别检查代码把class_to_idx和每个类的样例路径打印出来。训练前人工抽查 10 到 20 张图片确认目录名和内容一致。这个检查 30 秒能完成但能省下几个小时的无效训练。如果你从网上爬图片时把“花名-来源网站”作为目录名更要检查一遍来源网站名混进类别名这种事我见过不止一次。5.3 推理 transform 与训练不一致单张图片预测精度骤降现象训练脚本里 val_acc 90% 以上自己写了个单张图片推理脚本随便拿一张验证集里的图去测模型给出的预测居然错了而且置信度很低。原因推理脚本里可能只用了Resize(224)或干脆没有做归一化而训练和验证时用的是Resize(256) - CenterCrop(224) - Normalize。输入分布不一致模型在验证集上学的“经验”完全用不上。解决推理时必须复用训练时的val_transform特别是Resize到CenterCrop的顺序和Normalize的均值方差。最好的做法是把val_transform单独抽成一个函数训练脚本和推理脚本都从同一个地方导入而不是在推理脚本里抄一遍。这个坑是最隐蔽的因为报错不会出现模型默默给你一个错误的高置信度结果。5.4 细粒度品种混淆月季、玫瑰和蔷薇分不清现象大类识别不错比如能把菊花和玫瑰分清但月季、玫瑰、蔷薇这几个近缘品种互相混常用的增强和调参手段都救不回来。原因这三个品种在图像上差异极小224x224 分辨率下花瓣结构和叶子形状的特征可能只有几个像素的差别。预训练模型的特征是面向 1000 类通用任务的在细粒度区分上先天不足。解决两条路。一条是提高输入分辨率把输入从 224 提到 384 或 448让模型“看得更细”同时配合更大 batch 或更小学习率另一条是在工程上调整分类体系先用粗分类模型定大类再在类内训练一个细粒度模型而不是让一个模型做所有事。如果数据量本身就少提高分辨率的意义也有限因为模型没有足够的样本去学习那些细微特征。这时候先补数据比调模型更现实。5.5 病虫害图片混入模型学到的是病斑而不是花现象训练 loss 正常下降val_acc 也升高但模型对正常健康花卉的误判率很高反而对带有病虫害特征的图片“格外有把握”。原因采集图片时把带有病斑、黄叶的样本也塞进了对应花类的文件夹。模型在训练中发现“有褐色斑点就是玫瑰”这种捷径特征比花瓣结构更容易区分于是学到的不是玫瑰的特征而是病斑的特征。这类问题在整理花卉病虫害数据集时尤其突出——病虫害样本往往具有明显的视觉共性模型很容易抓住这个共性忽略真正的花朵结构。解决回看数据源把带明显病害特征的样本单独拿出来。如果业务目标本身就是做病虫害预警那就应该单独设置“病害玫瑰”这样的类别而不是混进健康玫瑰里如果目标只是花种识别这些样本应直接剔除宁可少一些训练数据也不要让标签变得不纯净。判断方法很简单单独挑一批完全健康的花图做验证看看模型在这批图上的表现是不是远低于训练时的 val_acc如果是就去数据里找“捷径特征”的来源。6. 从训练到可用推理脚本、top-k 输出与置信度校准6.1 单张图片推理脚本top-3 与置信度一起返回训练好模型后下一步是写一个能处理单张图片的推理脚本。注意这里不能直接拿训练脚本改因为训练脚本里的模型是train模式会计算 dropout 和 batch norm 的滚动统计量推理必须切到eval模式。import torch import json from PIL import Image from torchvision import models, transforms device torch.device(cuda if torch.cuda.is_available() else cpu) # 读取训练时保存的类别名 with open(class_names.json) as f: class_names json.load(f) num_classes len(class_names) # 重新构建模型结构并加载权重 model models.resnet18() model.fc torch.nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_flower_model.pth, map_locationdevice)) model.to(device).eval() # 与训练时 val_transform 保持一致 transform 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, topk3): img Image.open(image_path).convert(RGB) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1).squeeze(0) topk_indices probs.topk(topk).indices.tolist() return [(class_names[i], probs[i].item()) for i in topk_indices] # 示例 for name, conf in predict(test.jpg, topk3): print(f{name}: {conf:.4f})这个脚本的class_names必须来自训练时保存的 json 文件不能重新定义否则索引对不上。返回 top-3 而不是只返回 top-1是因为花卉识别里近缘品种本来就难分把三个候选都交给调用方让业务侧做最终判断比模型硬给一个答案更可靠。Image.open(...).convert(RGB)这一步很重要有些手机拍出的照片是 RGBA 模式或带 EXIF 方向信息不转 RGB 或不做归一化预测结果会被这些无关因素干扰。6.2 置信度校准与阈值模型说 0.97 也不一定可靠很多刚跑通训练的人看到softmax输出 0.97 就觉得模型很确定但实际统计下来这个 0.97 的预测准确率可能只有 80%。这种“过度自信”在花卉数据集上很常见尤其是在细粒度品种分类里。原因是训练数据的类别分布和真实场景差异很大模型拟合的是训练集的分布而不是真实世界的分布。把置信度校准回真实概率的常用做法是温度缩放。具体操作是在验证集上收集模型的 logits 和真实标签搜索一个最优的温度参数 T让logits / T经 softmax 后的交叉熵损失最小然后推理时用调整后的概率作为置信度。T 如果小于 1说明模型过于保守大于 1说明模型过度自信。这个原理不复杂但很多人不知道——拿到模型后第一件事就是看它输出的概率分布而不是看提不提供。校准之后还有一个必须做的动作设定拒识阈值。常见做法是把 top-1 置信度低于某个值的图片直接判为“无法识别”而不是硬分到某一个类。阈值的取值可以在验证集上扫一遍比如分别试 0.5、0.6、0.7看被拒绝样本的准确率和拒绝率哪个组合最符合业务诉求。做花卉识别的真实场景里很多误判其实是“不确定但硬猜”导致的加了拒识之后整体精度反而更容易看。我做这类项目养成的习惯是训练完不急着看训练日志先把手机里自己拍的几张户外花卉照片放进推理脚本里跑一遍再对照验证集 acc 判断模型到底行不行。图片集的干净程度永远比网络结构更能决定上线效果。这套流程你已经跟着走了一遍剩下的就是拿自己的花卉数据集试一次——训练脚本、数据划分、避坑清单都齐了试一次就会有自己的手感。希望帮到你。本文还有配套的精品资源点击获取