资讯动态

104种花分类实战:EfficientNet迁移学习从选型到踩坑

发布时间:2026/10/5 5:28:18 来源:尧图企业网站定制
简介一份基于EfficientNet轻量级网络的104种花卉图像分类实战项目面向需要系统学习迁移学习与图像识别流程的开发者。项目内置约1.6万张花卉样本及标签支持b0至b7共8种模型权重可自由选择是否加载预训练参数、是否冻结特征层并集成Adam、SGD、AdamW优化器与余弦退火学习率同时采用多类别交叉熵损失在训练集上精度可达0.9左右。压缩包共2000个文件主体为1993张JPG图像数据另有3个Python脚本、2个JSON指标文件以及TXT和readme说明文档整体约514MB。项目不仅输出训练集与验证集上的loss和准确率曲线还会生成混淆矩阵、recall、precision、F1 score及特异度等详细指标便于深入分析模型表现。目前已有124人学习下载适合希望快速上手迁移学习或更换数据集进行二次实验的读者。1. 104种花分类为什么绕不开EfficientNet和迁移学习同样一套图像分类代码在10类数据集上能到97%换到104种花的任务上直接掉到74%。这不是数据量的问题而是104类花之间的类间差异比想象中小得多——雏菊和蒲公英、玫瑰和月季人眼都经常分不清。做这种细粒度图像分类EfficientNet轻量级网络加迁移学习是当前最稳妥的组合预训练权重让模型不用从零学纹理和边缘轻量级结构又把训练成本和显存压到单卡可跑。这篇文章就从选型、数据处理、两阶段训练到踩坑清单把整个方案的落地路径讲透。适合手里有几千张花图、想做出一个能用的分类器而不是只跑通一个demo的读者。2. EfficientNet选型B0还是B4轻量级网络的边界在哪2.1 复合缩放到底在缩放什么从基线网络到B0-B7EfficientNet不是某个单一网络而是一族网络。先用神经架构搜索得到一个基线网络B0再通过复合缩放系数统一拉伸深度、宽度和分辨率。这里的重点在于“统一”不是单独把网络加深或加宽而是让三个维度按固定比例一起涨。论文里给出了基准系数alpha1.2控制网络深度beta1.1控制通道宽度gamma1.15控制输入分辨率。每多一个phi档位三个维度依次乘方放大。这个设计思路的价值在于它把“分辨率翻倍”和“网络加深”这两个原本互相打架的操作协调起来了。图像变大了感受野需要跟着变大通道变宽了深度也得跟上以免欠拟合。单独调任何一个维度都会在某个阶段出现收益骤降。复合缩放解决了这个问题所以B7比B0参数多得多但同样参数量下准确率始终压过ResNet系。迁移学习场景下更要明白一件事你用的预训练权重是在ImageNet上训出来的ImageNet的输入分布和花的细粒度特征有重叠但不完全一致。B0在ImageNet上Top-1约77%B4约83%但这几个点的差距在花分类任务上会被数据质量稀释。不要盲目上大模型后面会讲怎么反推。2.2 迁移学习里选哪个版本按数据量和显存反推选B几核心看两个变量每类花有多少张图以及你手里是什么显卡。每类花原始图少于50张时B0和B4的准确率差距可能只有1-2个点但训练时间和显存占用差3倍以上每类超过150张图B3或B4才有动力去追那2-3个点。参考这个粗略对照表版本参数量输入尺寸ImageNet Top-1单卡1080Ti可跑建议适用场景B05.3M22477.1%是每类80张快速验证B17.8M24079.1%是中等数据量均衡选择B29.2M26080.1%是数据量足够追求精度B312M30081.6%勉强每类100张接受慢训练B419M38082.9%否数据充足且有较好GPU还有一个容易忽略的点输入尺寸。B0是224B4是380从B2到B3分辨率从260跳到300。高分辨率对花的判别非常有帮助因为很多花品种的差异在花瓣边缘纹理上分辨率不够的时候这些纹理被下采样抹掉了。但这不是免费的——B0用224输入能在batch size 64下跑B4用380输入显存立刻吃紧。我一般做法是先用B0跑通全流程确认数据划分和训练策略没问题再在B2上做精度冲刺这样既不被显存卡住又能拿到分辨率红利。2.3 最小可跑代码timm加载预训练权重并替换分类头确定选B0之后第一步是加载预训练权重并把最后的全连接层换成104类的分类头。这里强烈建议用timm而不是torchvision因为timm里EfficientNet的实现更规范预训练权重的预处理配置也更完整import timm import torch # 加载预训练模型替换分类头为104类 model timm.create_model( efficientnet_b0, pretrainedTrue, num_classes104, ) # 查看模型预期的输入尺寸和归一化参数 print(model.default_cfg[input_size]) # (3, 224, 224) print(model.default_cfg[mean]) # 归一化均值 print(model.default_cfg[std]) # 归一化标准差这段代码的逻辑是num_classes104传入时timm会自动丢弃原ImageNet分类头换成新的线性层原分类头因为随机初始化反向传播时梯度会非常大这也是后面必须先冻结backbone训练头的原因之一。default_cfg里存着预训练权重对应的输入尺寸和归一化参数你的数据增强管线必须和它对齐不能拿一个416输入或自定义归一化去硬套。注意这里有个版本差异新版本timm里pretrainedTrue会有DeprecationWarning改用pretrained_cfgTrue或直接传weights参数。代码和注释里都要让读者清楚这一点避免跟着老教程抄了半天发现权重没加载上。3. 104类花数据集的处理按品种分组划分训练集验证集3.1 划分数据集的第一个坑不要随机切文件很多图像分类项目的数据划分是一行train_test_split按文件打散这在花分类上几乎必然翻车。原因很实际同一个品种的花往往来自同一个拍摄批次背景、光照、相机参数都高度相似。按文件随机切分时同一朵花的照片会同时出现在训练集和验证集里模型记住了这张图的背景纹理就能答对验证集指标虚高到96%换到真实场景直接崩到70%以下。这就是做104类花分类最常见的“假收敛”。正确做法是按花的品种分组划分保证同一个品种的所有图片只进入训练集或验证集不能两边都出现。用GroupShuffleSplit可以按点实现from sklearn.model_selection import GroupShuffleSplit # images: 所有图片路径 # labels: 每张图的品种标签 # species_ids: 每张图所属的品种组ID当同一个品种有多个子目录时 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(images, labels, groupsspecies_ids)) train_images [images[i] for i in train_idx] val_images [images[i] for i in val_idx]整个划分逻辑的关键是groups参数它告诉划分器“哪些样本属于同一个组”划分器保证同一组的样本不会跨到两个集合。在花分类里groups可以是按拍摄地点或花朵个体编号生成的ID而不只是品种标签。只有按个体分组才能测试模型是否学到了“花的品种特征”而不是“这朵花本身长这样”。3.2 数据增强的取舍ImageNet系增强够用高级玩法要克制104种花的细粒度分类增强策略需要在“多样性”和“保真度”之间找平衡。水平翻转、随机旋转10度、随机裁剪缩放这老三样是基础直接在transforms里组合from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(meantimm_mean, stdtimm_std), ])几个需要说明的点。scale(0.6, 1.0)比默认的(0.08, 1.0)保守很多因为花的主体通常占画面比例不小裁太小会把花茎或背景学成特征。RandomRotation只转了10度花朵朝向偏移超过15度会引入不自然的花瓣形变。ColorJitter用来模拟不同天气和光线条件下的花但对饱和度不要调太狠很多花的品种差异恰恰体现在颜色上把颜色搅乱了等于把关键判别信息破坏了。至于CutMix、MixUp这类高级增强在104类花上要非常谨慎。它们会把两朵不同品种的花拼在一起生成一个混合标签模型在细粒度特征上本身就容易混淆这种增强会让决策边界更模糊。我做过对比实验CutMix在102类花任务上反而掉点1.5%。如果要用只在训练后期以低概率使用。3.3 归一化参数用timm还是torchvision的标准归一化参数不对预训练权重等于白加载。EfficientNet的预训练权重在ImageNet上用均值(0.485, 0.456, 0.406)和标准差(0.229, 0.224, 0.225)归一化torchvision的EfficientNet实现也沿用这个标准但问题出在细节上——有些轮子库或自定义代码里写的是(0.5, 0.5, 0.5)的归一化这在一般分类任务里能跑但用在迁移学习上会让权重假设的输入分布完全错位收敛速度显著变慢。正确的做法是从模型配置里读取不要手写# 直接复用模型default_cfg里的归一化参数 mean model.default_cfg[mean] std model.default_cfg[std] normalize transforms.Normalize(meanmean, stdstd)这段代码的价值在于通用性不管在B0还是B4不管换到什么预训练权重归一化参数永远和模型匹配。另一个容易忽略的细节是transforms.ToTensor()会先把像素值缩放到0到1然后再做归一化。如果你的输入图像是灰度图或RGBA四通道需要在前面手动转换否则通道数不匹配会在网络第一层直接报错。我在实际项目里遇到过输入图片里有带透明通道的PNG第一层卷积直接shape mismatch排查了半天。4. 迁移学习的两个阶段先冻结backbone训头再解冻全量微调4.1 为什么先训头再解冻防止分类头扰动特征空间104类花的分类头是随机初始化的而backbone带着ImageNet上百万张图的先验。如果一开始就全量微调随机分类头会回传巨大的梯度直接冲垮backbone里学好的纹理和边缘特征整个网络要花很长时间重新适应还可能陷进一个差的局部最优。这段血泪经验我重复过太多遍了。正确做法分两个阶段。第一阶段冻结backbone所有参数只训练分类头让新的分类头在预训练特征空间表面找到一个靠谱的起点这个阶段通常只需要5-10个epoch就能收敛。第二阶段再解冻backbone小学习率微调让backbone的特征适配花的细粒度特性。冻结参数的代码实现其实就一行# 冻结backbone只留分类头可训练 for param in model.parameters(): param.requires_grad False for param in model.classifier.parameters(): param.requires_grad True # 优化器只看requires_grad为True的参数 optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4, )这里有一个细节模型里有些模块比如BatchNorm的running_mean不是参数但也要保证在冻结阶段不更新。PyTorch里设置requires_gradFalse不会阻止BatchNorm的统计量更新所以要么把模型设为eval()要么在训练循环里手动跳过。不过EfficientNet用的是BatchNorm如果你换成用了LayerNorm的版本不存在这个问题也算一个选择理由。4.2 直推式迁移学习在花分类场景里怎么落地热词里提到的直推式迁移学习很多人以为是换个预训练权重的事其实它指的是训练阶段让未标注的目标域数据也参与进来。在104种花分类里最常见的落地形式就是伪标签先用第一阶段训好的模型对一批未标注的花图片做预测把置信度高的样本连同预测标签一起混进训练集。做法是训练完第二阶段后额外跑一次推理把预测概率大于0.9的图片加入训练集再继续微调5个epoch。这个操作对花分类很有效因为花的品种是封闭集合104类全知道伪标签错误率不高。但要注意过滤条件花卉数据里有很多是不同角度的同一株花伪标签可能重复强化同一株花的背景特征导致模型在真实场景上过拟合到拍摄环境。我一般会同时限制每个品种最多加入的伪标签数量比如每类最多加100张防止类别不平衡被放大。直推式迁移学习不是必须的它是在你手上还有大量未标注数据时锦上添花的手段。如果104类的数据都是标注完的做好两阶段迁移学习就足够了不要为这个词硬上。4.3 解冻深度从stage4开始还是全部解冻第二阶段的“解冻”不是无脑全量解冻。EfficientNet的backbone分多个stage浅层stage学的是通用纹理和颜色边缘深层stage学的是抽象语义特征。花的品种特征更多落在深层语义上浅层特征的通用性已经很强改动反而可能破坏它。常见做法是解冻最后两个stage加上分类头其余保持冻结。具体到EfficientNet-B0网络结构里blocks是一个序列解冻从后往前数第3个block开始比较稳。代码上做一个分层学习率控制# 分三段设置学习率分类头最大backbone后端次之backbone前端最小 stage4_params [] backbone_params [] head_params list(model.classifier.parameters()) for name, param in model.named_parameters(): if not param.requires_grad: continue if blocks.9 in name or blocks.8 in name: stage4_params.append(param) elif features in name: backbone_params.append(param) optimizer torch.optim.AdamW([ {params: head_params, lr: 1e-3}, {params: stage4_params, lr: 1e-4}, {params: backbone_params, lr: 1e-5}, ], weight_decay1e-4)这个设置的核心逻辑是分类头从随机开始需要较大学习率快速收敛backbone后端只做微调用中等学习率前端特征太通用用小学习率防止灾难性遗忘。按经验三者的学习率比例1:0.1:0.01是稳定开局。如果你解冻后发现loss直接爆炸大概率是backbone的学习率给大了把比例降到1:0.05:0.005再试。5. 训练参数与三种典型翻车104类花分类的踩坑清单5.1 参数表lr、weight decay、batch size、epoch数的组合直接给一套经过验证的参数组合按两阶段分别设置参数第一阶段训头第二阶段微调backbone优化器AdamWAdamW学习率1e-3head 1e-3 / stage4 1e-4 / front 1e-5weight decay1e-41e-4batch size3232epoch820-30label smoothing0.10.1学习率策略CosineAnnealingCosineAnnealingwarmup epochs13label smoothing0.1在细粒度分类里几乎是必选项。104类花的训练集不可能完全干净标注时很容易把相似品种搞混硬标签会让模型对错误样本过拟合平滑后损失函数对错标样本的惩罚温和很多。warmup第二个阶段必开解冻backbone瞬间梯度方向变化大前几个step用小学习率过渡能避免震荡。5.2 翻车一验证集指标虚高因为同株花跨集合这是我做102类花分类时踩过最深的一个坑。第一次跑通时验证集准确率96.4%和当时论文里的SOTA持平心情很好。结果把模型接到自己拍的花图上预测准确率只有61%。检查半天才发现训练集和验证集里出现了同一朵花的不同照片——它们来自同一个原始目录按文件随机划分时这个目录被切成了两半。原因写在数据划分上只按文件随机分没有按拍摄对象分组。解决方式就是第3章里的GroupShuffleSplit但这里再多说一步划分完之后一定要检查验证集和训练集的品种分布是否均衡最好打印出训练集和验证集每类的图片数量按品种看有没有哪一类被完全排除在验证集外。104类里有一两类只有两三天照片随机划分很容易整类掉进训练集验证集直接缺少那一类的评测样本指标虚高且不可复现。5.3 翻车二loss下降但验证集不动分类头学习率过大训练到第10个epoch训练loss从2.0降到0.6验证集准确率卡在79%不动。很多人的第一反应是过拟合开始调dropout和weight decay但问题其实出在第一阶段训头时学习率给的太大了。分类头是随机初始化的线性层1e-3的AdamW学习率会让它大步幅乱撞虽然loss在下滑但它学到的是“答案在训练集某处”的索引式决策而不是花的语义特征。解决办法是回到第一阶段重训把学习率降到3e-4同时把训练epoch从8加到12。第一阶段训好后第二阶段解冻backbone时观察前5个epoch的验证集指标正常情况是缓慢爬升或先小幅下降再回升如果直接大幅波动说明前面的分类头没训稳。这类问题最折磨人的点在于它不报错指标也不是完全不动就是上不去很容易让人把时间浪费在调增强或换模型上。一阶段训头速度很快不要舍不得重跑。5.4 翻车三混淆集中在易混类不能只靠换模型验证集92%看混淆矩阵时发现分类错误集中分布在几个固定组合上雏菊和蒲公英、金鱼草和香彩雀。这些错误不是随机的它们在颜色、花瓣形状、甚至花径长度上都高度相似人眼都容易认错。这时候换更大的模型不一定有帮助问题可能出在训练数据的类别代表性上。解决的路径是三层递进。第一层从训练集里抽出被混淆类的图片统计每类的照片张数和拍摄场景数。如果某类只有三个拍摄地的照片模型学到的就是那三个拍摄地的背景而不是品种特征。第二层单独对比这几个易混类的样本差异找它们之间区别最大的特征是什么然后在增强里强化这一特征——比如金鱼草和香彩雀主要差在花瓣边缘那就在裁剪时减少旋转角度防止边缘形变过度。第三层如果以上都不够对这三个类单独提高采样权重用WeightedRandomSampler让它们在每个batch里出现次数更多。全部做下来通常能拿回3-5个点的准确率。6. 用类激活图验证模型是否真的在看花瓣而不是背景6.1 Grad-CAM可视化torchcam一行接入验证集指标过了90%不等于在真实场景可靠。花分类最容易出现的问题就是模型学会了看图里的“拍摄地点”而不是花的特征——比如某个品种的照片全部在红色墙前拍的模型直接学成“红色墙面这个品种”。要验证这个用类激活图Grad-CAM把模型决策时关注的区域可视化出来。import torch from torchcam.methods import GradCAM cam_extractor GradCAM(model) # 假设已经有一个预处理好的输入张量input_tensor model.eval() with torch.no_grad(): out model(input_tensor.unsqueeze(0)) # 取top-1预测的索引 pred_idx out.argmax(dim1).item() # 生成类激活图并叠加到原图上 activation_map cam_extractor(pred_idx, out)这段代码在model.eval()模式下对单张图推理得到预测类别后生成注意力热力图叠加到原图上就能看到模型主要根据图像的哪个区域做判断。需要注意GradCAM需要指定目标层EfficientNet一般取最后一个block的输出层效果最好。对104类花的每一类抽两三张测试图去跑能快速暴露模型的分类依据是否合理。6.2 三个值得检查的典型case第一个case模型关注点在整朵花上但花瓣边缘的高亮区域和人类判断依据一致说明学到了有效的品种特征这是理想情况。第二个case高亮区域集中在花蕊或花心而花瓣被忽略这在很多花品种里是危险的。比如两类花的花蕊颜色几乎一样模型关注点会偏向花蕊真正区分品种的花瓣纹理被压制。看到这种情况可以调整增强策略——增加对花蕊区域的遮挡增强或者手动裁剪掉花心区域强制模型学花瓣。第三个case高亮区域跑到背景绿叶或杂物上不用说又是数据划分或拍摄场景相关性问题回到第3章和第5.2节排查数据来源。我的一个习惯是训练结束后不只看准确率和F1还要跑一批Grad-CAM图统一检查大约花半小时但能帮你省下后面调试部署模型的大量时间。第一次做花分类时验证集指标好看结果部署后错的一塌糊涂就是没做这一步。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑