简介:这是一份基于 PyTorch 的 EfficientNet 图像分类实战资源,定位清晰,适合有一定深度学习基础、希望快速上手图像分类项目的开发者和学习者。整包共 8 个文件,包括 5 个 Python 脚本、2 个 pyc 缓存文件及 1 个已训练模型权重文件,压缩包大小约 38.27MB。脚本覆盖数据集读取、模型搭建、训练、测试与结果验证等环节,权重文件可直接加载用于推理或继续微调;配合清晰的工程目录,读者能对照从数据准备到模型评估的完整流程,理解 EfficientNet 在真实数据上的应用方式。资源目前已有 1205 人学习下载,适合课程设计、算法实验或个人项目起步;借助这份代码,可减少环境搭建和调参的重复工作,快速获得一套可复用的图像分类方案,并重点关注数据预处理、训练迭代和模型保存等落地细节。
1. 图像分类实战:为什么EfficientNet是性价比最高的选择
做图像分类落地的人大概率都经历过这个阶段:用 ResNet 堆深度,精度卡在某个值上不去;换成 ViT,又没有那么多数据喂饱它。我见过某开发者做一个工业零件表面缺陷分类,ResNet50 加数据、调学习率折腾两周,准确率停在 93% 上下,后来把主干换成 EfficientNet-B3,只改了几行代码,同样的训练配置直接到 96.8%。这个反差不是玄学,而是 EfficientNet 的结构本身就占了便宜——它不靠单纯加深加宽网络,而是用神经架构搜索找到的 MBConv 模块和一套叫复合缩放的规则,把算力花在真正有效的位置上。
标题里这个「图像分类EfficientNet实战.zip」指向的,正是一个从数据整理、模型训练到推理导出的完整工程。它不是只丢给你一个模型类,而是把整个训练链路、参数配置和部署脚本打包好,解压就能跑。这篇文章会把它背后的结构原理、训练流程、调参细节和常见坑全部拆开讲清楚。适合两类人:一类是刚入门分类任务、想找个可靠基线的新手,另一类是已经在用 ResNet/ConvNeXt 但觉得精度或成本不理想、想换个主干的熟手。
2. EfficientNet原理与选型:MBConv、复合缩放与B0/B1的取舍
2.1 MBConv与复合缩放:EfficientNet的精度来源
EfficientNet 的核心单元是 MBConv(Mobile Inverted Bottleneck Convolution),这套结构最早在 MobileNetV2 里被验证过:先用 1x1 卷积升维,再做深度可分离卷积,最后用 1x1 卷积降维,形成一个两头窄、中间宽的「倒瓶颈」形态。EfficientNet 在这个基础之上加了两样东西——SE 注意力模块和 Swish 激活函数。
SE 模块会对特征图的每个通道做全局平均池化,过两个全连接层算出每个通道的重要程度,再乘回原特征图。这个机制在分类任务里特别实用,因为某些通道对「有没有缺陷区域」这类判别信息贡献更大,SE 能自动把这些通道放大。Swish 激活则解决了 ReLU 在负半轴直接截断导致的信息丢失问题,计算量增加不多,精度却有稳定的小幅提升。
复合缩放是另一个关键设计。传统做法是单独调深度、宽度或输入分辨率,但 EfficientNet 用一组系数把三者绑定:深度按 α^φ、宽度按 β^φ、分辨率按 γ^φ 缩放,其中 α、β、γ 是基准结构上的常数,φ 是用户控制的复合系数。约束条件是 α·β²·γ² ≈ 2,保证缩放后计算量大致按 2^φ 增长。φ=0 就是 B0,φ 每增加 1,模型规模按比例放大一个档次,对应 B1、B2、B3 直到 B7。
实际做工程时不用太纠结这个公式,但要记住一个结论:EfficientNet 的每个档位都不是简单把层数乘二,而是深度、宽度、分辨率一起变。这也是为什么同一张图,B0 和 B3 的推荐输入尺寸不一样,后面处理预处理时必须注意。
2.2 B0到B3怎么选:参数量、推理速度与精度的平衡表
实战包里最常见的型号是 B0 到 B3,再往上就是 B4/B5,基本只有显存充足的 GPU 服务器才用得上。下面是几个常用档位在 ImageNet-1K 上的大致对比,参数和计算量是公开信息:
| 模型版本 | 输入尺寸 | Top-1 精度(约) | 参数量(约) | FLOPs(约) |
|---|---|---|---|---|
| EfficientNet-B0 | 224x224 | 77.1% | 5.3M | 0.39B |
| EfficientNet-B1 | 240x240 | 79.1% | 7.8M | 0.70B |
| EfficientNet-B2 | 260x260 | 80.1% | 9.2M | 1.00B |
| EfficientNet-B3 | 300x300 | 81.1% | 12.0M | 1.80B |
| EfficientNet-B4 | 380x380 | 82.6% | 19.0M | 4.20B |
选型我一直遵循「先定推理预算,再选档位」的原则。如果模型要跑在 CPU 或者嵌入式设备上,选 B0 并把输入压到 224,一次前向控制在 20-40ms 内的可接受范围;如果跑在普通办公级 GPU 上(如 8GB 显存),B2 或 B3 是甜点区,精度比 B0 高 3-4 个点,显存占用还在可控范围。B4 以上我只在离线训练、不要求实时性的场景用。
EfficientNetV2 也值得知道。V2 改进了训练速度和显存效率,引入了 Fused-MBConv 和渐进式缩放,训练比 V1 快不少。但很多实战包、预训练权重和部署工具链还是优先兼容 V1,如果你是拿现成工程起步,先跑 V1 更稳妥,模型训完再考虑是否迁移到 V2。
2.3 预处理差异:输入尺寸、归一化参数与训练测试一致性
EfficientNet 的预处理细节比 ResNet 更讲究。归一化均值方差和 ImageNet 预训练权重是配套的:均值取 (0.485, 0.456, 0.406),方差取 (0.229, 0.224, 0.225),这些值来自 ImageNet 数据集的统计结果,加载官方预训练权重时就该用这对参数,改掉会直接影响权重初始化的匹配度。
输入尺寸方面,B0 推荐 224,B1 是 240,B2 是 260,B3 是 300。不少人在这一步翻车:训练时用 300,验证时为了省时间把图 resize 成 224,然后精度掉两三个点,还怪模型不行。正确做法是训练和测试保持同一个尺寸。
缩放方式也要统一。常见做法是先用短边等比缩小、再中心裁剪,而不是直接把图拉成正方形,避免宽高比失真。这里可以注意一个细节:如果目标是训练 B3 这类大输入模型,使用 0.8-1.2 倍随机缩放配合随机裁剪做数据增强,会比固定尺寸训练有更好的尺寸鲁棒性。
3. 跑通实战包:环境准备、数据整理与训练启动全流程
3.1 环境与依赖:PyTorch版本与CUDA对齐检查
拿到实战工程,第一步不是着急跑训练,而是先确认环境对齐。EfficientNet 项目基本都基于 PyTorch 和 timm 库实现,timm 提供了直接加载预训练权重的接口,省去自己实现 MBConv 的麻烦。版本上我习惯的搭配是 Python 3.8+、PyTorch 1.13 或 2.0、timm 0.6 以上,CUDA 11.7 或 11.8。先跑一段检查脚本:
python -c "import torch, timm; print('torch:', torch.__version__); print('cuda:', torch.cuda.is_available()); print('timm:', timm.__version__)"这段命令一次性确认三件事:torch 是否装好、CUDA 是否可用、timm 版本是否过老。如果cuda: False,先检查nvidia-smi输出的驱动版本和 PyTorch 的 CUDA 版本号是否匹配,常见问题是驱动支持 CUDA 12,但 PyTorch 编译用的 CUDA 11.8,这种不匹配会导致 GPU 完全不可用。另一种常见错误是ModuleNotFoundError: timm,只要用pip install timm补上即可。
3.2 数据目录与标签映射:ImageFolder的整理方式
实战包里的数据加载器绝大多数基于torchvision.datasets.ImageFolder,它要求数据按类别分目录存放。目录结构长这样:
data/ ├── train/ │ ├── 缺陷A/ │ │ ├── 0001.jpg │ │ └── 0002.jpg │ └── 缺陷B/ │ └── 0003.jpg └── val/ ├── 缺陷A/ │ └── 0004.jpg └── 缺陷B/ └── 0005.jpgImageFolder 会按文件夹名字典序自动生成标签,缺陷A就是类别 0、缺陷B就是类别 1。如果你的原始数据是 CSV 标注,必须先把图片复制到对应目录下,我一般写个一段式脚本:
import os, shutil, csv with open('labels.csv') as f: for row in csv.DictReader(f): src = os.path.join('raw_images', row['filename']) dst = os.path.join('data/train', row['label'], row['filename']) os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy(src, dst)这里重点是exist_ok=True,避免第一次运行报目录不存在;同时把原始图片复制而不是移动,保留一份备份对排查标签错误很有帮助。类别的划分比例按任务需求,常见默认是训练集和验证集 8:2,但如果某一类只有几十张图,需要先对类别数量做统计,避免某个类别在验证集里少到无法评估。
3.3 训练启动命令:单卡与多卡的最小可跑配置
环境检查通过、目录结构就位后,就可以启动训练了。实战包里 train.py 的常用启动命令是这样:
python train.py \ --model efficientnet-b0 \ --data-dir ./data \ --epochs 50 \ --batch-size 64 \ --lr 1e-3 \ --weight-decay 1e-5 \ --output-dir ./runs/exp001各参数含义:--model指定主干,注意这个名称要和 timm 里注册的名字完全一致;--batch-size 64在 8GB 显存的卡上跑 B0 比较稳,B3 则要降到 32;--lr 1e-3是 AdamW 在 64 这个 batch 下的经验起点;--weight-decay 1e-5控制权重衰减强度,太大容易欠拟合,太小则模型变复杂后出现过拟合。
多卡训练不需要改模型代码,用 torchrun 启动就好:
torchrun --nproc_per_node=2 train.py \ --model efficientnet-b0 \ --data-dir ./data \ --epochs 50 \ --batch-size 64 \ --lr 1e-3这里有个坑,很多人会忽略:多卡时如果代码没有做学习率缩放,等同于用 2 倍的 batch 但学习率没变,收敛速度会明显变慢。常见做法是 batch 翻倍、学习率也翻倍,或者用公式lr_new = lr_base * sqrt(batch_new / batch_base)做调整。先用这个起点跑一个 10 epoch 的短训练,确认 loss 在下降,再放开完整训练,能省很多排查时间。
3.4 训练日志判读:loss、accuracy、lr曲线的正常形态
训练跑起来后,要能看懂日志里的三个信号:loss、top-1 acc 和 lr。一个健康的训练过程,train loss 应该在前 5 个 epoch 内快速下降,从初始的 4-6 掉到 1 以下,随后进入缓慢下降阶段;验证集 acc 会在波动中逐步爬升,波动幅度通常在 1 个百分点以内,如果某个 epoch 突然暴跌 5 个点以上,大概率是验证集数据没打乱、或者验证代码里误用了训练模式。
学习率曲线如果是 cosine 形式,日志里应该能看到 lr 从峰值平滑降到接近 0。如果 lr 曲线出现阶跃或平台期,说明 scheduler 配置和 epoch 数不匹配,比如总 epoch 是 50 但 warmup 设了 20,整个训练的前半段都在热身中浪费了。另外提醒一句,如果训练日志没有任何验证集输出,说明工程里可能只做了训练 loop 没有验证 loop,这种包跑出来的模型精度没有参考价值,建议优先换一个带完整验证流程的工程。
4. 把精度练上去:学习率、数据增强与训练技巧的实战调参
4.1 学习率与优化器:warmup、cosine与AdamW的峰值设置
EfficientNet 训练时优化器首选 AdamW,它对权重衰减的处理比 Adam 更规范——Adam 里权重衰减被合并到带一阶动量项里,AdamW 则把它独立出来,正则化更干净。学习率策略我固定用 warmup + cosine 的组合,前 5 个 epoch 线性从 0 升到峰值,之后按余弦曲线衰减到峰值的一小部分。下面是一个可对照实现的配置:
optimizer: name: adamw base_lr: 1.0e-3 weight_decay: 1.0e-5 scheduler: name: cosine warmup_epochs: 5 min_lr: 1.0e-6峰值学习率这里给的是 batch size 64 下的默认值,换 batch 时按线性比率缩放即可。如果训练曲线表现为早期 loss 震荡不下降,大概率是峰值设大了,降到 3e-4 或者把 warmup 延长到 8 个 epoch;如果 loss 下降平稳但验证 acc 长期上不去,可能是峰值太小,提到 2e-3 试一轮对比。学习率调参是经验活,但在这里可以遵循一个大致规律:EfficientNet 比 ResNet 对学习率更敏感,因为 SE 模块的缩放参数直接控制特征响应强度,学习率过大时模型会先从「遗忘注意力」开始崩。
4.2 数据增强组合:MixUp与CutMix的比例和概率
数据增强是分类精度提升空间最大的板块。EfficientNet 在 ImageNet 上的官方训练就用了 AutoAugment,但落到小数据集实战时,AutoAugment 有时会增强过头,导致模型学到的特征偏离真实分布。我更推荐可控的 MixUp 和 CutMix 组合。MixUp 把两张图的像素按比例混合,标签也按相同比例混合;CutMix 则是裁剪一张图的一部分贴到另一张图上,标签按面积比例混合。
augmentation: mixup_alpha: 0.2 cutmix_alpha: 1.0 prob: 0.5这两个参数的含义分别是混合强度 α 和启用概率。MixUp 的 α=0.2 表示混合比例接近极端(0 或 1)的概率更大,对原始图破坏较小;CutMix 的 α=1.0 则是标准默认值,裁剪区域面积分布比较均匀。两者共用同一个概率开关,0.5 表示每个 batch 有一半概率触发混合增强。如果数据集本身只有几百张图,可以把 prob 提到 0.7;如果数据已经过采样或类别均衡,维持 0.5 就好。这里有一个明显的收益规律:MixUp 加 1-2 个点,CutMix 加 1 个点左右,两个叠加往往有 2-3 个点提升,但任何增强都不会修复标签本身存在的错误——标注质量低时增强只会放大错误。
4.3 稳定训练的三个开关:EMA、标签平滑与梯度累积
EMA(指数移动平均)是分类任务里性价比最高的技巧之一。训练时维护一份模型参数的 shadow 版本,每个 step 用当前参数和 shadow 参数做加权平均,验证和推理都用 shadow 版本。它对精度提升约 0.3-0.5 个点,有时更多,几乎不增加训练耗时。decay 系数默认取 0.999,意思是每个 step shadow 参数只向当前参数靠近千分之一,模型在训练后期小步迭代时,EMA 能起到平滑作用。
ema_decay = 0.999 ema_model = copy.deepcopy(model) for current_params, ema_params in zip(model.parameters(), ema_model.parameters()): ema_params.data.mul_(ema_decay).add_(current_params.data, alpha=1 - ema_decay)这段代码在 model 里维护一份 ema_model 的副本,训练每步更新后执行。注意这里用的是mul_和add_原地更新,避免重复创建张量导致显存持续增长。标签平滑也很推荐:把 one-hot 标签从 1 和 0 改成 0.9 和 0.1 的混合,防止模型对训练样本过饱和。平滑系数 0.1 是常用值,类别数很多时可以降到 0.05。梯度累积则是显存不够时的补救手段:batch 64 装不进显存,就每次装 16,累积 4 次梯度后再更新一次参数,数学上等价于 batch 64。不要忘记在累积到第 4 步时做一次逆缩放,即把 loss 除以累积步数再反传,否则梯度会偏大,等效学习率被放大 4 倍。
5. EfficientNet实战避坑清单:5个让人反复翻车的典型问题
5.1 显存直接OOM:SE模块的中间激活比想象中大
现象是训练跑了几个 step 后报CUDA out of memory,不管怎么调小 batch 都只撑多一阵子。原因是 EfficientNet 的 SE 模块会为每个 block 保留一条全局池化后的分支,中间特征在反向传播时全部驻留显存,整体激活内存高于同参数量的 ResNet。解决方法是三个同时做:batch 降到 16 或 8;打开混精度训练,fp16 就能让激活内存减半;再配合梯度累积补回 batch 大小的语义。混精度的做法是在训练脚本里加一句:
python train.py --model efficientnet-b0 --batch-size 16 --accumulation-steps 4 --amp这是我在 6GB 显存卡上跑 B0 的常用组合:batch 16、累积 4 步、混精度开启,训练效果接近 batch 64,内存占用可以控制在 5GB 以内。
5.2 预训练权重加载失败:分类头数量和任务类别对不上
换到自己数据集时,最后一层全连接把 ImageNet 的 1000 类映射到你的 N 类,直接load_state_dict会报size mismatch。新手容易直接把整个文件跳过,结果模型用随机初始化从头学,收敛慢且精度低。正确做法是先加载 backbone 权重,再单独替换分类头:
state = torch.load('efficientnet_b0.pth', map_location='cpu') state.pop('classifier.weight') state.pop('classifier.bias') model.load_state_dict(state, strict=False)这里strict=False允许缺少分类头参数,但注意它会同时忽略所有名字不匹配的键,所以载入后最好打印一下模型输出层的 shape 确认已经变成[N, 784]而不是[1000, 1280]。如果是用 timm 加载,直接timm.create_model('efficientnet_b0', pretrained=True, num_classes=N)一行搞定,timm 会帮你处理分类头替换,这也是我推荐用 timm 的核心原因。
5.3 验证集精度比想象低一大截:训练和测试预处理不一致
这类坑我翻车过不止一次,最典型的是训练时用随机裁剪加翻转,验证时只做了 resize 但忘了中心裁剪,或者训练输入 300 而验证输入 224。现象是训练集 acc 已经 98%,验证集只有 80%,看起来像严重过拟合,但实际是预处理不一致导致的分布偏移。解决方法很机械:把训练和验证的预处理代码放在同一个函数里统一维护,验证分支严格用相同 resize 尺寸 + 中心裁剪,且所有分支都用模型的model.default_cfg里的 mean/std 做归一化,不手动硬编码。
5.4 训练loss下降但acc长期卡住:标签噪声或类别不平衡
loss 从 2.0 降到 0.3,但验证 acc 卡在某个固定值,比如 85%,再训练也不动。这种「loss 和 acc 脱钩」的情况,首先要怀疑标签错误——某个类里混入了大量错误标注,模型拟合出的是噪声分布。排查方法是随机抽 20 张预测置信度最高的验证集图片人工看一遍,如果预测对但标签错,那就是标注问题,直接修标签比调模型有效。类别不平衡也会造成类似现象,少数类从头到尾学不会,此时用类别加权 CrossEntropy 比盲目调增强更有效。
5.5 推理速度比预期慢很多:模型档位选大且没有导出优化
把训练好的 B3 直接接到生产接口,发现单张推理要一两百毫秒。原因有两层:一是 B3 的 FLOPs 有 1.8B,本身就比 B0 慢近 5 倍;二是 PyTorch 的 eager 模式推理没有做算子融合和量化。解决思路是先用 ONNX 导出再走 TensorRT 或用 ONNX Runtime,这个放到下一章展开。这里只提醒一点,如果上线机器是 CPU,选 B0 并量化到 int8,往往比用 B3 的 fp32 更快且精度足够。
6. 推理落地与验证:导出模型、统计指标与TTA精度提升
6.1 导出TorchScript/ONNX与正确性核对
训练完成后,把权重从 PyTorch 格式导出成 ONNX 是生产部署的标准动作。导出本身很简单,但最容易出错的是动态输入尺寸和输出名。下面是一个可用的导出脚本片段:
model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "efficientnet_b0.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=13 )dynamic_axes把 batch 维声明成动态,保证生产环境可以一次喂任意数量的图片;opset_version=13兼顾了足够新的算子支持和部署端兼容性。导出后务必做一次数值一致性核对,用同一张图分别跑 PyTorch 模型和 ONNX Runtime,输出概率差的绝对值不应超过 1e-3,超过这个量级说明导出过程丢失了算子精度,要检查是否有不支持的 op。
6.2 验证指标与最后一公里的精度技巧
部署前还要跑完整的评估指标:top-1 acc、top-5 acc,以及每个类别的精确率和召回率。类别数少的时候直接打印分类报告,一眼就能看出哪些类混淆。如果目标是再榨一点精度,TTA(测试时增强)是投入最小、见效最直接的手段:推理时把输入原图、水平翻转图、0.8/1.0/1.2 倍缩放图各过一遍模型,对输出的 softmax 做平均,再取 argmax。
preds = [] for scale in [0.8, 1.0, 1.2]: for flip in [False, True]: img = transform(img, scale=scale, hflip=flip) preds.append(model(img.unsqueeze(0)).softmax(dim=1)) final = torch.stack(preds).mean(dim=0).argmax(dim=1)这段代码在验证集上通常能带来 0.3-0.5 个点的提升,代价是推理时间翻了 6 倍。所以生产环境一般只保留水平翻转这一项,多尺度留给离线评测或比赛场景。我个人的习惯是先定好线上延迟预算,在预算内把模型档位和 TTA 开关定死,再去做其他调优——曾经为了追求精度上了 B4 加多尺度 TTA,结果接口响应慢了 8 倍,被上游服务投诉后又全部回退。希望这个教训能帮你少走一次弯路,希望帮到你。
本文还有配套的精品资源,点击获取