花卉识别实战:数据集清洗与PyTorch迁移学习训练 简介面向花卉识别与图像分类学习者这套资料提供一份完整可用的花卉数据集及配套训练源码。数据集涵盖千屈菜、射干、旋覆花、曼陀罗、桔梗、棣棠等16种花卉共32000张224×224彩色图片每类约2000张可直接用于模型训练与评估。源码基于TensorFlow编写集成了23种主流图片分类模型训练时可根据任务自由选择网络结构便于横向对比算法效果。压缩包共110个文件主要包含Python源码py、配置说明txt及编译缓存pyc另附启动脚本、数据文件与示例图片整体约684MB。目前已有1499人学习下载适合深度学习入门及中高级开发者进行花卉分类实战、模型调优与实验对比也可作为课程设计或算法研究的参考基线。1. 花卉识别不是“先跑代码”数据、源码和图片集三件事的顺序第一次拿到“花卉识别-花卉数据集-花卉识别训练源码-花卉图片集02”这类组合包的开发者习惯是直接打开训练源码跑训练这个习惯我在早期项目里踩过几次。花卉识别任务本身并不复杂瓶颈通常出现在数据上图片集里有没有错图、类别怎么分、验证集怎么切这些决定训练结果的上限。这套组合包其实是四件套花卉识别是目标花卉数据集是标签与切分依据训练源码是加速手段花卉图片集02是原料。适合想用现成图片集快速验证迁移学习流程的人也适合入门图像分类的从业者——花卉类间差异小是练细粒度分类的稳妥对象。下文按我自己的落地顺序讲先把花卉识别图像数据整理成模型能吃的结构再跑通训练源码然后讲训练中高频的坑最后谈评估和怎么扩展。2. 整理花卉图片集目录规范、脏数据清洗与标签体系训练脚本跑不起来的最常见原因往往不是模型代码而是数据目录和标签对不上。图片集的原始目录通常按拍摄批次或下载来源组织不会天然符合训练框架的要求所以第一步是把它们改造成标准结构。这一步做扎实后面所有环节都会顺很多。2.1 先定目录结构用 ImageFolder 约定把图片集变成数据集PyTorch 的datasets.ImageFolder要求“类别名作为一级目录名图片放在对应类别目录内”目录名就是标签。后续数据加载、类别数统计、评估脚本都会读取这套目录所以一开始就要定死规则。针对“花卉图片集02”这种按批次整理的图我建议先建一个固定骨架# 建立 train / val / test_infer 三个目录 mkdir -p floral-dataset/train floral-dataset/val floral-dataset/test_infer # 把同一类别的图片归拢例如玫瑰花这一个类 mv ./02批/玫瑰花 ./floral-dataset/train/rose目录名即标签rose会出现在预测输出和混淆矩阵里。手工移动少量目录没问题批量操作建议用脚本避免手滑把两个类混在一起。下面是一个通用的归拢脚本按原始子目录名生成新文件名并复制到目标位置import shutil from pathlib import Path src_root Path(./02批原始图片) dst_root Path(./floral-dataset/train) for cls_dir in src_root.iterdir(): if not cls_dir.is_dir(): continue for i, img_file in enumerate(cls_dir.iterdir()): ext img_file.suffix.lower() # 只留常见图片格式过滤隐藏文件和缩略图缓存 if ext not in {.jpg, .jpeg, .png, .bmp}: continue # 用“类别名_序号”重命名防止不同子目录出现同名文件互相覆盖 new_name f{cls_dir.name}_{i:04d}{ext} dst dst_root / cls_dir.name / new_name dst.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(img_file, dst)脚本逻辑遍历原始图片目录下的每个子目录子目录名变成类别名“类别名_序号.ext”的命名方式保证全局唯一copy2保留文件修改时间等元数据方便后来排查同批次拍摄的连拍图。这里要注意如果同一种花在不同来源里叫法不一致比如rose和玫瑰花要先人工归并成统一类别名再跑脚本否则模型会把同一物种当两类来学混淆矩阵里会出现一组莫名其妙的“易混类”。2.2 清洗花卉识别图像数据去重、去水印、去错图这批花卉识别图像数据的价值首先不在数量而在标签质量。我处理过好几批图片集几乎每批都有四类典型脏数据重复图片连拍或同一张图存了两个文件名、错标签目录写着雏菊但内容是非洲菊、手机截图混入、花只占画面一小角且带大面积水印。这些脏图直接进训练轻则拖慢收敛重则让模型学到水印而不是花。先跑一个自动检查脚本把重复图和损坏图筛出来import numpy as np from PIL import Image, ImageChops from pathlib import Path def is_duplicate(img1, img2, threshold0.95): 把两张图缩到32x32灰度统计灰度差小于8的像素占比 a img1.convert(L).resize((32, 32)) b img2.convert(L).resize((32, 32)) diff ImageChops.difference(a, b) arr np.asarray(diff).astype(np.float32) similar_ratio (arr 8).mean() return similar_ratio threshold def is_valid_image(img_path): 用 PIL 的 verify 检查文件结构是否完整 try: with Image.open(img_path) as im: im.verify() return True except Exception: return Falsethreshold0.95表示 95% 的像素近似相同才判为重复太低会误伤不同拍摄角度的图缩到 32x32 是为了快速批量比较。verify()只检查文件头与编码结构不会整图解码能在不消耗大量内存的前提下识别下载中断产生的损坏文件。这个自动检查解决不了旋转裁剪后的重复图那种情况靠特征匹配成本高实战中更有效的是人工抽检。抽检方法很直接每个类别随机取 10 到 20 张拼成一张九宫格图或直接翻相册肉眼过一遍重点看“标签对不对、主体是不是花”。如果一个类 5% 以上的图都有问题说明这个类的标签不可信宁可暂时移出训练集也别带病训练。批量裁边去水印可以用 PIL 的crop统一裁掉四边但水印压在花瓣上的图基本无解只能换数据源。2.3 类别平衡与数量下限少样本类别的处理策略整理完目录后先统计每个类的图片数再决定训练策略。类别分布天然不均衡是常态月季、菊花这类常见花可能上千张某些地方性品种只有二三十张。交叉熵训练会把大头梯度贡献给大类别小类别很容易被忽略。下面是我常用的判断框架类别样本数主要风险常见做法少于 30 张训练必过拟合验证集不可信用数据增强补偿必要时同类合并30 到 100 张基本可用但指标波动大按 9:1 切分 train/val训练时加颜色抖动多于 100 张相对稳定按 8:1:1 切分可上 MixUp 等正则不要用“把少数类复制几份”来平衡模型看到的是同一份像素的重复等于人为提高权重对泛化没有帮助。如果某类只有 20 张且标注者自己都不确定是不是同一种我一般直接把这批图移出训练集放到test_infer里当负样本用。另外切分 train/val 时要遵循“同源图不进两边”的原则同一朵花的连拍、同一个拍摄者在同一时段拍的一组图必须整体划入同一个集合。这个原则在第 4 章有具体案例现在先埋个伏笔。3. 用 PyTorch 迁移学习跑通花卉识别训练源码数据整理好之后才是真正碰“花卉识别训练源码”的部分。我以 PyTorch 为例给出一套最小可运行方案因为 torchvision 生态对迁移学习支持最省事。花卉识别不需要从零训练一个卷积网络用 ImageNet 预训练模型做迁移学习在中型数据量上的效果远好于随机初始化收敛速度也快一个量级。3.1 数据加载与增强几行 torchvision 组合出可用数据流加载部分直接用ImageFolder配合DataLoader所有关键参数都集中在 transform 里。增强策略影响模型泛化需要按花卉场景单独调from torch.utils.data import DataLoader from torchvision import datasets, transforms TRAIN_DIR floral-dataset/train VAL_DIR floral-dataset/val IMG_SIZE 224 BATCH_SIZE 32 train_transforms transforms.Compose([ transforms.RandomResizedCrop(IMG_SIZE, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), 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_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(TRAIN_DIR, transformtrain_transforms) val_ds datasets.ImageFolder(VAL_DIR, transformval_transforms) train_loader DataLoader(train_ds, batch_sizeBATCH_SIZE, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_sizeBATCH_SIZE, shuffleFalse, num_workers4, pin_memoryTrue)这里几个参数值得单独说明。RandomResizedCrop的scale(0.7, 1.0)限定裁剪比例可以模拟“花在画面中占比不同”的真实场景但不会把花裁到只剩叶子。ColorJitter三个值都设 0.2 而不是 0.5是怕增强过头冲淡品种本身的颜色判别信息——这是做花卉识别时反复调过的经验值。均值方差用 ImageNet 默认的因为我们加载的预训练权重就是在这个分布上训出来的保持一致能让特征复用更充分。验证集不做随机增强只做固定Resize(256)加CenterCrop(224)确保每次评估指标可复现。num_workers在 Windows 上建议 4 以内Linux 可以到 8同时留意内存占用。3.2 模型构建ResNet18 换头冻结多少层看数据量模型部分用torchvision.models里的 ResNet18替换最后的全连接层。是否冻结骨干层取决于你的数据量import torch import torch.nn as nn from torchvision import models num_classes len(train_ds.classes) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 替换最后的全连接层输出类别数 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 每个类别不足100张时先冻结骨干只训练分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False用 ImageNet 预训练权重比随机初始化强很多尤其是花卉这种细粒度场景底层学到的纹理、边缘特征可以直接复用。只训fc层时10 到 15 个 epoch 就能到达瓶颈适合快速验证流程。如果每类图片超过 200 张建议把最后两个resnetstage 解冻骨干学习率设1e-4fc层保持1e-3通常还能再提几个点。如果每类只有十几张冻结全部骨干反而是防过拟合最稳妥的方案。提示脚本里加一行print(num_classes, train_ds.classes)确认类别数等于实际标签数。目录里多一个空文件夹就会让类别数加一这类错很隐蔽排查起来很费时间。3.3 训练主循环与模型保存用验证损失挑模型不靠最后一轮训练循环是整套源码的核心。优化器用 AdamW学习率调度用 cosine周期结束后同时保存两份权重from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR EPOCHS 30 DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(DEVICE) criterion nn.CrossEntropyLoss() optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_maxEPOCHS) best_val_loss float(inf) for epoch in range(EPOCHS): model.train() run_loss 0.0 train_total 0 for imgs, labels in train_loader: imgs, labels imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() run_loss loss.item() * imgs.size(0) train_total imgs.size(0) scheduler.step() model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(DEVICE), labels.to(DEVICE) logits model(imgs) loss criterion(logits, labels) val_loss loss.item() * imgs.size(0) correct (logits.argmax(1) labels).sum().item() total labels.size(0) val_loss / total val_acc correct / total # 用验证集loss选best比用acc更平稳 if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), flower_best.pth) torch.save(model.state_dict(), flower_last.pth) print(fepoch {epoch1:02d} | train_loss {run_loss / train_total:.3f} f| val_loss {val_loss:.3f} | val_acc {val_acc:.3f})优化器参数用filter(lambda p: p.requires_grad, ...)过滤掉冻结的骨干确保反传时只更新fc层。weight_decay1e-4是常用值太大反而让收敛变慢。选模型时用验证集loss而不是acc因为acc是 0/1 离散值在小型验证集上波动大loss更平滑。保存best和last两份权重的意思是flower_best.pth做推理演示flower_last.pth做断点续训。万一后面发现数据清洗有遗漏重新整理后还能从上次状态继续不用从头再来算是给自己留的后悔药。4. 花卉识别训练避坑指南五个高频翻车点与排查手记训练过程中的问题通常不在语法报错而在评估指标失真和看不见的数据污染。下面五条是从数据整理到训练完成这段链路里最常踩的坑每一条我都按“现象 → 原因 → 解决”的结构写方便对照排查。4.1 验证集准确率虚高同源图片泄漏一上线就掉点现象验证集 acc 接近 0.95拿着手机到实地拍几张准确率掉到 0.6 左右。原因切分 train/val 时同一场景下连拍的同类图片被随机分到了两边。模型记住了特定背景、光线和拍摄设备留下的纹理验证时相当于开卷考试自然分数虚高。解决切分前先按“拍摄批次”或“来源目录”分组整组划入 train 或 val不能逐文件随机切。如果图片集没有分组信息至少按文件名前缀和时间戳做初判同目录同时间段的图归到同一组。这个动作必须在数据整理阶段完成等训练完才发现要全部重跑。提示验证集的作用是模拟没见过的新数据。同源图片放进验证集等于自己骗自己。4.2 模型学到水印和拍摄边框而不是花现象训练 loss 收敛很快但拿干净的新图预测时准确率不高把带平台 logo 的截图误判成某个特定类别。原因这套“花卉图片集02”里如果某几类网图占比高平台水印、白边框会比花瓣纹理更容易区分模型于是走了捷径。解决先把训练集中统一出现的白边裁掉再对角落的 logo 区域做统计看是哪一类图片普遍带水印。如果某类图片水印风格高度统一最简单的做法是把整批水印图移入验证集而不是训练集让模型从干净图里学特征。批量裁边可以用 PIL 的crop但水印压在花朵主体上的图脚本救不了只能靠人工剔除。4.3 类别不平衡导致小类训练不足现象大类别月季、菊花的 val_acc 能到 95%某稀有品种只有 50% 到 60%混淆矩阵里小类几乎全部被推给大类。原因交叉熵按整体 loss 优化小类在一个 batch 里出现的次数太少梯度贡献被大类别稀释模型没机会认真学小类的判别特征。解决用加权采样器让每个类别在每个 epoch 里被抽到的次数大致均衡代替普通随机打乱from torch.utils.data import WeightedRandomSampler # 用训练集标签统计每个类的样本数再取倒数作为采样权重 targets torch.tensor(train_ds.targets) class_counts torch.bincount(targets) sample_weights 1.0 / class_counts[targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(targets), replacementTrue) train_loader DataLoader(train_ds, batch_size64, samplersampler, num_workers4, pin_memoryTrue)weight 1/class_count让样本数小的类权重更高replacementTrue允许重复采样同一张图稀有类才能凑够一个 batch 的量。代价是每个 epoch 会有重复样本训练轮数要适当缩短否则容易过拟合。batch size 从 32 提到 64 也是为了让小类更容易出现在同一个 batch 里。4.4 学习率过大导致 loss 震荡或 NaN现象第一个 epoch 的 loss 就在 3 到 7 之间剧烈波动甚至直接出现 NaN。原因常见有两种。一种是学习率对当前 batch size 来说太大梯度更新一步跨过最优点另一种是数据里混进了损坏图片解码出极端像素值导致反向传播出异常。解决先用第 2 章的is_valid_image全量过滤损坏图再把学习率从1e-3降到3e-4。学习率设置带点玄学我的经验是只训练fc层时lr1e-3解冻骨干后骨干1e-4、fc保持1e-3一般不会翻车。如果还没恢复正常检查数据加载链路里有没有哪一步跳过了归一化。4.5 相似品种混淆严重月季与玫瑰雏菊与非洲菊现象混淆矩阵里某两个类互相串而且错判的置信度很高模型不是“犹豫地错”而是“自信地错”。原因花卉分类天然是细粒度问题。部分品种差异在花瓣纹理、叶片形态ImageNet 预训练模型提取的特征对这种层级的差异不够敏感。解决最直接的办法是解冻 backbone 最后两个 stage 做全局微调同时给易混类补充不同拍摄环境下的大 Data。另一个常见的做法是把易混的两个类单独提出来训练一个二分类模型效果往往比在大模型里硬调更好。数据增强层面可以加随机旋转和微小尺度扰动但根本还是让模型看到更多“相似但不同种”的样本。5. 评估花卉识别模型混淆矩阵、错例回看与可信度判断训练结束不等于模型可用。我见过不少项目在验证集上数字很好看落到真实场景一测就不行区别就在于评估做得粗不粗。评估不是打印一串 acc而是要回答三个问题哪两类容易混、错的是数据还是模型、模型对自己的判断到底有多自信。5.1 用混淆矩阵定位易混类别混淆矩阵比 acc 值提供的信息多得多。它能把“哪两类互相串”直接钉在表格里方便针对性地补数据或微调import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay model.load_state_dict(torch.load(flower_best.pth)) model.to(DEVICE) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(DEVICE), labels.to(DEVICE) preds model(imgs).argmax(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(cm, display_labelstrain_ds.classes) disp.plot(xticks_rotation90, cmapBlues) plt.savefig(confusion_matrix.png, bbox_inchestight)类别多时全量热力图会密密麻麻看不清楚。更高效的做法是把矩阵非对角元素按值从大到小排序只打印前十对易混类。混淆矩阵能告诉你“哪两类混了”但不能告诉你“为什么混”所以下一步必须回看样本。5.2 错误样本回看区分“数据错”和“模型错”把验证集的错误预测样本按置信度从高到低导出图片人工看二三十张就能快速判断问题出在哪一环。我一般按下面的标准分类处理错误类型判断依据处理方式标签错图是 A 但目录标签是 B模型反而预测对了修正标签后重训类间特征太近两张图在人类眼里也确实高度接近解冻微调、补数据或单独建二分类模型图片质量差模糊、被手遮挡、花只占画面一小块从正式测试集中剔除单独做鲁棒性测试把错例导出成文件夹能反复查看也方便给标注人员复核。这一步能避免“盲目调模型参数但问题其实出在标签上”的尴尬。模型验证集 95% 还是 99%先搞清楚错在哪类、错得为什么比盲目堆数据更重要。5.3 黑匣子之外Top-2置信度与领域外测试一个分类模型如果只输出 top1对中型花卉数据集来说浪费了不少信息。Top-2 在实践中有意义如果真实标签经常出现在前两名里说明模型“大致知道是哪类只是细分差一点”距离可用只差一步。输出 Top-2 的代码很简单with torch.no_grad(): logits model(img.unsqueeze(0).to(DEVICE)) probs torch.softmax(logits, dim1) top2_idx probs.squeeze(0).topk(2).indices.cpu().numpy() top2_names [train_ds.classes[i] for i in top2_idx] print(top2_names, probs.squeeze(0).topk(2).values.tolist())领域外测试更关键找一些训练集里没出现过的拍摄角度、光照条件或手机型号拍的鲜花图放进test_infer目录跑一遍记录置信度分布。如果模型对这些图普遍给出 0.98 以上的置信度但预测是错的说明它把训练分布记得太死最可能的根源是第 4 章讲的数据泄漏而不是学习率没调好。注意高置信度不等于高正确性。验证集切得越“舒服”真实环境的落差可能越大。6. 从花卉数据集和源码继续扩展数据飞轮、轻量化与细粒度技巧训练跑通、评估通过这个花卉识别项目其实才刚起步。模型上线只是开始长期效果取决于能不能把现场拍到的新错例回流到数据集里再迭代训练。我建议按下面几个方向做增量投入。6.1 把现场错例变成数据飞轮在推理服务里加一条日志凡是置信度低于阈值的输入图都自动保存到一个待复核目录。每周人工复核一次错得最多的那一类往往对应新的拍摄环境或长尾品种。把复核过的图片按第 2 章的规范并入训练集就是一轮高质量的增量训练。这个数据回流动作的收益通常比继续调模型结构显著得多。新数据进来时不走清洗流程就直接训练数据飞轮会越转越脏最后又要返工。6.2 轻量化从 ResNet 换到 MobileNet 的取舍如果最终要部署在手机或边缘设备上建议把骨干换成 MobileNetV3-Large用同一套训练流程做全量微调。在花卉识别任务上这个替换通常只掉不到 1 个点的精度但推理速度快好几倍。换模型时注意分类头的结构model models.mobilenet_v3_large( weightsmodels.MobileNet_V3_Large_Weights.IMAGENET1K_V1) in_features model.classifier[-1].in_features model.classifier[-1] nn.Linear(in_features, num_classes)MobileNetV3 的classifier是一个包含 Dropout 和多个 Linear 的序列只需要替换最后一个线性层前面的 Dropout 结构保持不变。6.3 细粒度技巧先粗分再细分如果后面要覆盖上百种花我建议做两阶段分类先用一个模型粗分到“属”再在属内做细分类。这种分层结构比训练一个超大类模型更容易维护新增品种时只需重训对应属的局部模型不用整体重来。另一个技巧是把高混淆类单独抽出来做针对性微调常见做法是训练一个只包含这两类的二分类器效果往往比在全体类别里反复调参更直接。最早做花卉识别时我为了省事跳过了数据整理结果训练加排查花了双倍时间后来才养成“先清洗再训练”的习惯。现在每批新图片集进来我第一件事永远是建目录、跑查重、抽检错图模型反而是最快确定的环节。这套工作流放到今天的“花卉图片集02”上依然适用希望帮到你。本文还有配套的精品资源点击获取