简介一套面向花卉识别任务的深度学习资源包含64类花卉图像数据集与配套卷积神经网络训练代码适合图像分类学习者、课程设计开发者以及需要快速验证分类模型的工程人员。数据集共计32000张224×224彩色图片全部为手机实地采集训练集25600张、测试集6400张划分明确且背景多样能贴近真实拍摄场景。训练代码覆盖ResNet、VGG、Inception、MobileNet、DenseNet、EfficientNet、SqueezeNet七大系列共37种主流分类模型可通过参数灵活切换网络结构便于对比不同架构的精度和效率。压缩包共2000个文件主体为1919张jpg图片另含39个txt说明文档、20个py训练脚本和22个pyc编译模块整体约195MBtxt内提供数据集说明与标签配置py文件可二次修改pyc便于直接调用。目前已有1646人学习下载可作为花卉识别项目、毕业设计或深度学习图像分类入门实验的可靠数据与代码支撑。1. 花卉识别数据集新手最容易低估的第一个深度学习实战项目一份能跑通的花卉识别项目本质上就三件事一个干净的花卉识别数据集、一段能改的深度学习训练代码、以及一台能跑 PyTorch 的电脑。很多做深度学习的人第一次接触图像分类不是手写数字而是花——因为花卉类目足够多、类间差异足够真实又不像人脸和车牌那样涉及隐私拿来练手几乎没有副作用。这篇文章要解决的是更实际的问题数据集怎么选、训练代码怎么写、参数怎么调、以及最容易被忽略的“模型看起来准但实际不能用”的坑。适合刚学完理论准备做第一个深度学习项目的学生也适合要快速验证分类方案的开发者。2. 选数据集与画像平衡花卉分类的第一个隐藏门槛2.1 公开花卉数据集怎么选类别数、单类样本量与场景分布公开的花卉识别数据集不算少常见的有 Oxford 102 Flowers、VGG Flower 17、以及 iNaturalist 里抽出的花卉子集。很多新手一看“102 类”就觉得更高级直接下载开训结果小网络欠拟合、大网络跑不动最后把责任推给“电脑不行”。我一般会先看三个指标类别数、单类最少样本量、以及拍摄场景是否单一。数据集类别数单类样本量大致区间场景特点适合做什么Flower 1717每类约 80 张姿态背景相对干净流程练手、验证模型结构Oxford 10210240 到 258 张不等景深、光线、姿态变化较大正经分类任务、写论文实验iNaturalist 花卉子集数千差异极大野外复杂背景类别层级嵌套大规模弱监督实验不适合入门表格看明白之后下一步是别再拿“总数”算账。Oxford 102 总共八千多张听着不少摊到 102 类里最少的类只有三四十张。深度学习图像分类里单类样本量不足 100 又没有什么预训练支撑的话很容易出现“那一类完全学不出来”的情况。所以入门第一个项目我建议要么用 Flower 17要么从大数据集里抽 10 到 20 个你认识的常见花类保证每类至少 150 张。动手之前先写个脚本把数据集翻个底朝天这是最有价值的五分钟。把训练目录的类别数、每类张数、总张数打出来你就不会对着一个黑匣子开始调参。import os from collections import Counter data_dir data/train # 改成你自己的训练目录 counter Counter() for cls_name in os.listdir(data_dir): cls_path os.path.join(data_dir, cls_name) if not os.path.isdir(cls_path): continue n len([f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png))]) counter[cls_name] n total sum(counter.values()) print(f类别数: {len(counter)}) for cls_name, n in counter.most_common(): print(f{cls_name}: {n} 张, 占比 {n / total * 100:.2f}%)这段脚本的逻辑很简单就是遍历data/train下的子目录每个子目录当作一个类统计图片文件数量。代码值得注意的地方有两个一是用os.listdir后必须配合os.path.isdir过滤防止数据集里混着说明文档之类的东西二是扩展名过滤不能只写.jpg很多爬下来或者转换过的数据是.png。看到输出后如果发现某类只有二三十张趁早删掉这个类别指望数据增强能凭空变出信息。这一步能帮你避开后续大半的翻车现场。2.2 自建花朵数据集拍摄采集、清洗与目录规范公开数据集用腻了或者你想识别自己阳台上的花自建数据集也完全可以。不只是拍照这么简单采集和清洗的规范直接决定训练效果。我见过有人每类只拍 30 张全在同一个角度同一个光线训练出来 val_acc 很高一换手机拍就原形毕露这就是数据多样性没做够。采集时尽量让每类覆盖 3 个以上场景不同光照、不同角度、不同背景。每类至少攒到 100 张能上 200 张更好。手机拍就行不需要微距镜头但要把主体拍清楚——花朵占比小于画面三分之一的基本可以删掉分类器学的可能不是花而是叶子纹理。目录结构按 PyTorch 的ImageFolder约定来组织最省事data/ train/ rose/0001.jpg tulip/0001.jpg ... val/ rose/0001.jpg ...采集完先做一轮清洗重中之重是检查损坏图片。PIL 打开图片时是惰性解码的只Image.open不load的话很多坏图不会立刻报错会一直拖到训练中途才把进程炸掉。from PIL import Image import os bad_images [] non_rgb [] for root, dirs, files in os.walk(data): for name in files: if not name.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(root, name) try: img Image.open(path) img.load() # 关键不 load 就不会真正解码文件 if img.mode ! RGB: non_rgb.append((path, img.mode)) except Exception as exc: bad_images.append((path, str(exc))) print(f损坏图片: {len(bad_images)}) for path, msg in bad_images: print(bad:, path, msg) print(f非 RGB 图片: {len(non_rgb)})这里的核心逻辑就是img.load()它才会真正读取整张图像的像素数据。见到非 RGB 的图片比如L灰度或RGBA不要直接删用img.convert(RGB)转一下再存回原路径更稳妥。清洗完的数据集后续训练时省下的时间远超现在这点折腾工夫。另外一个容易被忽略的点训练图片没必要追求超大尺寸手机原图动不动三四兆直接喂网络只会拖慢读取速度我一般先统一缩放到短边 512 像素左右再入库。3. 用深度学习训练代码在本地跑通花卉分类从预处理到调参3.1 数据加载与预处理千万不要直接喂原图拿到数据集后的第一件事不是写模型而是写数据加载。花卉识别这类细粒度分类任务模型对尺度、颜色偏移、位置都很敏感。直接Image.open后塞给网络会遇到几个很现实的问题图片尺寸不统一网络输入是固定张量、颜色分布不一致不同手机拍的同一朵花可能差出十万八千里、训练时没有随机扰动模型泛化极差。常见做法是用torchvision.transforms把训练集和验证集分开处理。训练集做随机增强验证集只做固定缩放绝不把增强用在验证集上否则你看到的指标是假的。from torchvision import datasets, transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf 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_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf)几个参数得说明白。RandomResizedCrop(224, scale(0.6, 1.0))表示每次随机裁剪原图面积的 60% 到 100%再缩放到 224×224这样模型看到的是“位置略有变化的花”RandomRotation(15)是正负 15 度内的随机旋转花卉这种中心对称的物体旋转增强非常有效但旋转角度别超过 30 度否则背景占比过大反而破坏语义ColorJitter的扰动幅度控制在 0.3 以内是对颜色过拟合的直接解药。验证集不随机裁剪所以用Resize(256)后CenterCrop(224)保证每张图裁剪到同一区域指标可复现。这里隐含一个关键点归一化的均值和标准差用的是 ImageNet 的常数因为后面如果要迁移学习预训练权重就是在这个分布上学出来的。自己从头炼丹的话这个归一化也照用没必要重新统计自己数据集的均值方差。3.2 搭建并训练一朵花分类模型最小可运行代码预处理写好后就能进入训练环节了。下面这段代码刻意做成“最小可运行”不碰任何花哨的 trick目的是先把流程跑通。模型用 ResNet18数据集假设你已经按上一章目录结构整理好了。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models from torch.optim import AdamW device cuda if torch.cuda.is_available() else cpu batch_size 32 lr 1e-4 epochs 30 num_classes len(train_ds.classes) model models.resnet18(weightsNone) # 先不用预训练纯看流程 model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) 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) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lrlr, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) for epoch in range(epochs): model.train() total_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() total_loss loss.item() * images.size(0) model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) scheduler.step() print(fepoch {epoch1:02d}/{epochs} floss {total_loss / len(train_ds):.4f} fval_acc {correct / total * 100:.2f}%)这段代码的逻辑主线是每个 epoch 先train()遍历训练集计算梯度更新参数再切到eval()模式遍历验证集统计准确率。注意model.eval()不能省它会关掉 BatchNorm 的统计更新和 Dropout否则验证集指标会偏低尤其容易误导你对模型真实水平的判断。参数上几个默认选择解释一下。用 AdamW 而不是 SGD是因为它对学习率不敏感新人用 SGD 常常因为一张大图就把 loss 震飞了weight_decay1e-4是 AdamW 自带的解耦权重衰减能有效压住过拟合。学习率 1e-4 是迁移学习场景的安全起步值。num_workers4在 Linux 和 Mac 上没问题Windows 上如果报错或者卡死改成 0后面避坑章节会细讲。3.3 三个必调参数学习率、batch size 与 epoch 策略训练脚本能跑只是第一步参数怎么调才是真正拉开差距的地方。花卉识别这种中小规模数据集最值得花时间的参数就三个。学习率1e-4 起步观察前几个 epoch 的 loss。如果 loss 在 1 到 3 之间反复横跳不下降降到 3e-5 试试如果 loss 一路正常下降但速度很慢可以提到 3e-4。从头训练weightsNone时学习率要更低因为梯度噪声比预训练微调更大。我踩过的坑是从头训练用 1e-3第一个 epoch 后 loss 直接 NaN然后开始怀疑代码写错了折腾半天其实是学习率太高。batch sizeGPU 显存决定上限32 起步。显存不够就把 batch size 降成 16 甚至 8同时学习率也降一点因为小 batch 的梯度方差更大大步长容易震荡。如果 batch size 升到 64学习率可以相应微调但没必要严格按线性缩放规则来花卉分类任务里 32 加 1e-4 的组合在绝大多数情况下都不是最差选择。epoch 和早停固定跑 30 或 50 个 epoch 不是好习惯。我一般每轮验证后记录最佳 val_acc连续 5 到 8 个 epoch 没有提升就停同时把最好的那份权重保存下来。保存完整检查点而不是只存模型权重因为你要留着老权重回去继续调学习率。best_ckpt { model: model.state_dict(), optimizer: optimizer.state_dict(), epoch: epoch 1, best_acc: best_acc, classes: train_ds.classes, } torch.save(best_ckpt, flower_best.pt)加载时记住一件事torch.load到的文件是个字典model.load_state_dict(ckpt[model])只恢复权重恢复 optimizer 对继续训练才有意义。存classes是给你推理时把索引转回花名用的这个字段不存的话你训练完根本不知道输出张量的第 3 维对应哪朵花。4. 从“能跑”到“好用”用验证集、混淆矩阵看清模型真实水平4.1 多分类报告准确率之外必须看的三个指标训练结束后如果只盯着 val_acc 一个数字你大概率会被骗。花卉数据集类别多102 类里随机猜测也有接近 1% 的准确率17 类里是 5.9%。acc 上了 90% 不代表每类都好可能只是常见的几类玫瑰、菊花、向日葵学得极好而少数类几乎全错。这时候要跑一个多分类报告逐类看 precision、recall 和 F1。from sklearn.metrics import classification_report import torch all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) preds model(images).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namestrain_ds.classes, digits3))这段代码做的事把整个验证集的所有预测和真实标签收进两个列表再交给 sklearn 生成逐类报告。输出里每一行会有 precision、recall、f1-score、support其中 support 是这一类的样本数。对花卉分类来说重点看 recall 低的那几类——recall 低意味着这类花被大量认成了别的类典型例子是月季和玫瑰这种外形极其接近的。看报告时要结合 support 数量一起读support 只有个位数的话指标波动不具备参考价值这类样本应该回数据集确认是否要补充。4.2 混淆矩阵定位“到底认错了谁”多分类报告告诉你哪类差混淆矩阵告诉你差在哪。生成一张矩阵图能直观看到哪些类别对在互相打架。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix import seaborn as sns cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(14, 12)) sns.heatmap(cm, cmapBlues, annotFalse, xticklabelstrain_ds.classes, yticklabelstrain_ds.classes) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(flower_confusion.png, dpi150)整个脚本只是把真实标签和预测标签喂给confusion_matrix然后画热力图。annotFalse是因为花卉分类类别多填数字反而看不清类别少于 30 时把annot改成 True 更直接。拿到这张图后找矩阵里非对角线的亮块比如 25 号真实类被大量预测成 31 号就去翻两类花的原始图片找规律。常见结论是颜色相近粉色系互相认错、花瓣结构相近菊科内部一团乱、或者拍摄背景相似草地上的花容易被一起错分。搞清楚错分原因解决方式就很明确了不是盲目堆模型深度而是补数据——对错分的那类补充不同角度和光线下的照片比换一个更大的 ResNet 有效得多。4.3 单独验证一个类别的鲁棒性val 集指标好看还不够模型最终要面对的是手机随手拍的照片。常见做法是训练完之后拿一类你数据集里没有的全新图片跑单张推理从实际结果反推数据覆盖度。我习惯做一个小脚本把 val 文件夹外的几十张真实拍摄图按顺序丢给模型人工逐张检查结果。发现连续几张把郁金香认成百合问题几乎都能追溯到训练数据里郁金香的拍摄场景太单一——比如全是俯拍而测试图全是侧拍。单张推理的代码很简单加载图片、预处理、过模型、取argmax然后用保存的classes列表映射回花名。这个步骤能发现 val_acc 完全暴露不出来的问题也是提前给“实际部署”打预防针因为我见过太多人 val_acc 刷到 95%交付给用户一拍照就掉到 60% 以下。5. 花卉识别训练避坑与排查数据、显存与过拟合的 4 个真实翻车现场5.1 训练好多个 epochval_acc 卡在个位数不动现象loss 在下降从 2.3 降到 1.8但 val_acc 一直卡在 4% 到 6%102 类数据集上甚至等于随机猜测。原因最常见的是从头训练小模型比如三四层的自定义 CNN去拟合 102 类细粒度分类。花卉之间的差异不像猫狗那样大网络太浅学不到区分性特征另一个可能原因是学习率低于 1e-5模型更新慢得像蜗牛loss 在降但精度还没跟上。解决先用预训练 ResNet18 或 ResNet50 试一轮如果 val_acc 能到 80% 以上说明网络容量不是瓶颈再回去调自己的小网络加深度如果预训练模型也卡那就检查类别数是否和num_classes对得上简单打印train_ds.classes看一眼多少类别多分类头直接变残废。这条经验我写过很多次花卉识别不是一个值得从零炼丹的方向预训练权重就是后悔药能少吃点苦就少吃点。5.2 训练 loss 降到接近 0val_acc 却死活上不去现象train loss 最后降到 0.01 以下val_loss 却在 1.0 附近val_acc 卡在 70% 左右上不去。原因过拟合。花卉数据集规模小模型容量大尤其是从头训练的深层网络会把训练集里每朵花的纹理背景全部背下来。这种“背题式学习”对细粒度分类特别致命因为花类之间的差异本来就细微背景和姿态又高度一致时模型学到的全是干扰项。解决先用强化数据增强试试把RandomRotation从 15 加到 25RandomResizedCrop的scale下限从 0.6 降到 0.4让模型被迫学习花的本体而不是固定构图同时检查weight_decay是否设置AdamW 下至少要 1e-4最好在 1e-4 到 1e-3 之间试着来最后一个手段是把BatchNorm换成Dropout但对 ResNet 系列来说直接换预训练模型再微调反而比在随机初始化模型上加 regularization 更高效。5.3 DataLoader 设置 num_workers4 后一跑就卡死或疯狂报错现象Windows 上python train.py一执行到第一个 batch 就卡住CPU 占用 100%程序不报错也不退出或者报一堆和 shared memory 相关的异常。原因Windows 下 PyTorch 的 DataLoader 多进程模型和 Linux 不太一样子进程经常在处理迭代器复制时卡住尤其当你把训练脚本直接写在脚本顶层、没有if __name__ __main__保护时Windows 会递归启动子进程直接把内存打死。解决Windows 上先把num_workers设成 0用主进程加载数据训练能跑通后再逐步加回 2 或者 4无论什么系统都建议把训练代码完整包进if __name__ __main__:里这是 PyTorch 官方示例一直在做而很多新手教程从不强调的事。5.4 训练中途突然报 OSError提示某张图片打不开现象训练跑到第 23 个 epoch突然弹出一个OSError: image file is truncated或cannot identify image file整个进程直接中断。原因数据集里混入了下载不完整的图片或图片格式被改后缀比如把.png直接改名为.jpg。这类问题在 2.2 节的清洗脚本检查时如果只做了Image.open没做load()坏图会被漏掉因为 PIL 是惰性解码的。解决第一道防线是回到清洗脚本对全部数据重新执行一遍包含img.load()的检查第二道防线更稳——在训练代码里注册 PIL 的容错处理让程序在遇到截断图时只跳过而不是崩溃from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES True这个设置让 PIL 对截断的 JPEG 不再抛异常而是载入已经解析的部分。注意这只是兜底策略那些被截断的图片即便能读内容也是残的训练效果会受影响所以正确做法是先用清洗脚本把坏图剔掉才轮到 LOAD_TRUNCATED_IMAGES 兜底。我还见过有人把所有坏图删掉后数据集失衡最后是靠补充拍摄数据解决的——所以坏图特别多时不要只想着删。6. 迁移学习再提速用预训练模型几分钟让花卉识别收敛如果你已经用随机初始化的 ResNet18 跑通了上面的代码下一个自然动作是把weightsNone换成 ImageNet 预训练权重并在训练时只微调最后几十层乃至只训练分类头。Flower 17 这种小数据集上预训练模型通常几十个 epoch 就能到 90% 以上而随机初始化可能永远停在 80% 附近。改造方式很直接model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, num_classes)冻结主干后优化器只更新最后一层全连接参数少显存和训练时间都大幅下降。想追求更高精度可以只解冻最后两个残差块用分组学习率训练。我常用的写法是给分类头 1e-3、给layer4用 1e-4主干保持冻结optimizer AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4}, ], weight_decay1e-4)这种分组学习率的好处是新初始化的分类头需要大步长快速收敛预训练特征层只做小幅调整避免破坏已经学好的特征表达。注意解冻和调整的学习率主次关系别搞反预训练层的学习率永远要比分类头低一个数量级甚至更多否则一个大 step 就可能把预训练几年的成果彻底冲垮。最后的验证和导出流程我现在的固定习惯是训练完画混淆矩阵、跑分类报告确认重点类别没问题后把权重保存为这张形式并在项目里留下一个infer.py单张推理脚本用真实拍摄图片验证过再交付。这套流程走下来换到其他类似场景鸟类、车辆、农产品分类也能直接复用大部分代码。希望这些踩过的坑和调参习惯能帮到你少走几步是我写这篇笔记最实在的期望。本文还有配套的精品资源点击获取