EfficientNet图像分类实战:从原理到训练调优与避坑指南
发布时间:2026/10/2 2:42:08来源:尧图网络
简介这是一份基于 PyTorch 的图像分类 EfficientNet 实战代码包面向有一定深度学习基础、希望快速掌握 EfficientNet 训练与推理流程的读者也适合用于课程设计、毕业设计或图像分类算法对比实验。压缩包共 8 个文件包含 5 个 Python 脚本、2 个编译生成的 pyc 缓存文件及 1 个已训练好的 pth 模型权重整体约 38.27 MB。其中 Python 脚本覆盖数据集封装、模型定义、训练、测试与单图预测等关键环节pth 权重则保存了完整训练结果可直接加载验证分类效果。资源已吸引 1205 人学习代码与权重搭配使用可帮助读者快速跑通 EfficientNet 实验理解从数据加载、训练调参到模型评估与推理部署的完整链路并在此基础上迁移到自己的分类任务中。整体结构清晰适合作为深度学习图像分类项目的入门参考与实践模板。1. 图像分类EfficientNet实战.zip这个压缩包解决什么问题同样是做图像分类换一个骨干网络参数量能少一半、精度反而更高——这是图像分类EfficientNet实战.zip这个项目包最有价值的点。EfficientNet是 Google 在 2019 年提出的卷积骨干网络靠网络结构搜索和一套复合缩放策略在 ImageNet 上以远小于 ResNet 的参数量拿到当时的领先精度。之后很多图像分类算法实现、迁移学习项目都把它当默认首选主网络直到 transformer 图像分类模型比如 ViT、Swin出来之前它一直是精算比最划算的 CNN 选择。这个 zip 里装的就是一套完整的图像分类落地代码数据集怎么组织、训练脚本怎么写、超参怎么设、模型怎么验证、踩了哪些坑怎么排查。它适合两类人刚接触图像分类、想跑通从数据到模型的完整链路的学生或转行者已经在做分类任务、想给自己的项目换个更高精度和算力比的骨干网络的工程师。你不需要重新发明什么照着项目里的流程把数据和脚本对上就能在本地 GPU 上跑出自己的分类模型。2. EfficientNet的底层逻辑三个关键设计决定它快且准很多人拿到 EfficientNet 就当成一个黑盒 backbone 直接用但它和 ResNet 的差异不是层更深、更宽这么简单。它有三个核心设计网络结构搜索得到的基线、移动倒残差模块MBConv、以及复合缩放策略。不理解这三个设计你就不知道换型号时该动哪个参数也不知道为什么别人调参能上 90 而你卡在 85。2.1 复合缩放同时调整深度、宽度、分辨率传统做法是把模型加宽或加深网络宽度、深度、输入分辨率是三个分开调的东西。EfficientNet 的做法是用一个全局系数 phi 同时缩放这三个维度深度按 alpha^phi 缩放宽度按 beta^phi 缩放分辨率按 gamma^phi 缩放并且约束 alpha、beta、gamma 的乘积关系让算力增长均匀分配到三个维度上。这个设计的意义很直接同样增加一倍计算量均匀分配三维得到的精度提升比单独加深或单独加宽要好。体现在代码里就是 timm 里 model_name 从 efficientnet_b0 换到 efficientnet_b4输入尺寸也要跟着变——B0 是 224B4 是 380B7 是 600。如果你只换了模型名还用 224 去喂 B7那就是把复合缩放的预设给废了精度和速度都会不对。这也是调参时最容易忽略的点finetune 的输入分辨率必须和模型预设对应。很多人抱怨训练 EfficientNet 精度不如预期打开代码一看Resize 写死了 224模型却是 B5——预设分辨率是 456等于强制缩图后训练信息已经丢了。2.2 MBConv与SE注意力为什么它比ResNet省参数EfficientNet 的重复单元叫 MBConvMobile Inverted Bottleneck Conv从 MobileNetV2 继承而来再加了 SE 注意力。它的计算方式是先用 1x1 卷积把通道数升上去再用 depthwise逐通道卷积做空间特征提取接着过 SE 注意力对通道加权最后 1x1 卷积把通道降回来并接残差连接。省参数的关键在 depthwise 卷积每个通道单独做卷积而不是像标准卷积那样每个输出通道要遍历全部输入通道。标准 3x3 卷积的计算量是输入通道数乘输出通道数乘 9depthwise 只有输入通道数乘 9。SE 注意力则在通道维度上做 squeeze 和 excitation用很小的中间层通常压缩到 1/4给每个通道算一个权重让模型自己学会该关注哪些特征图。这两个结构叠加起来就是 EfficientNet 在同样精度下参数量和计算量远小于 ResNet 的原因。实际使用时的感受是一个能跑到 90% 精度的 EfficientNet-B1 模型权重文件只有十几 MB推理单张图在普通消费级显卡上几毫秒到十几毫秒。如果你的部署端是 CPU 或边缘设备这个精算比优势比任何训练技巧都值钱。2.3 从B0到B7按显存和精度需求选型号timm 库里 EfficientNet 按参数量从小到大排了 B0 到 B7 八个标准型号外加一批带噪声训练版本_ns、_rs 等。我一般的选择原则是这样的型号参数量预设分辨率约Top1精度适用场景B0约530万224约77%快速验证、CPU推理、小数据集B1约780万240约79%通用分类、入门级GPUB2约920万260约80%精度要求中等的业务场景B3约1200万300约81%12GB显存可跑的较优选择B4约1900万380约83%精度优先、显存充足时使用B5-B73000万以上456/528/600约84%高精度竞赛、后台离线任务显存估算要按分辨率的平方算B0 输入 224B4 输入 380输入面积大约是 2.9 倍特征图内存也近似按这个倍数涨。12GB 显存的卡跑 B4batch_size 往往只能开到 16 到 24。如果你只有一张 8GB 卡老老实实选 B0 或 B1先跑通流程精度不够再加模型不要一上来就 B7——大概率直接 OOM浪费一下午。2.4 环境准备解压后先把这三样装齐这个项目包默认用 PyTorch 和 timm 运行。解压之后先做环境检查别急着跑代码。# 解压项目包假设压缩包是英文路径下的 zip unzip -q image_classification_efficientnet.zip -d efficientnet_project # 进入项目目录 cd efficientnet_project # 安装关键依赖torch、torchvision、timm缺了哪样都跑不起来 pip install torch torchvision timm提示解压后先看一眼目录里是否有 data 或 train/val 之类的数据文件夹。如果没有需要自己准备图像分类数据集格式见下一章。用 unzip 加 -q 是为了避免解压时刷屏也能更快发现路径异常。依赖装完后用下面这段验证环境是否就绪python -c import torch, timm; print(torch.__version__, timm.__version__) python -c print(torch.cuda.is_available())第二行输出 True 才表示能用 GPU 训练。如果你在 Windows 上遇到中文路径乱码导致数据集加载失败这是 zip 包文件名编码的问题我在第 4 章的避坑部分专门写了处理办法这里先跳过。3. 跑通图像分类全流程数据、训练脚本与参数调优环境就绪之后核心问题就是怎么把数据喂进去、怎么把训练跑起来。图像分类项目里常见做法是用 torchvision 的 ImageFolder 组织数据它按文件夹名自动生成类别标签省去自己写 dataset 的功夫。本章给出一套能直接用的完整训练流程。3.1 数据组织按ImageFolder的目录规则准备图片ImageFolder 要求数据按类别名/图片文件的层级摆放。无论你是自己收集的图像分类数据集还是从网上下载的数据集压缩包标准做法是整理成下面这个结构data/ ├── train/ │ ├── cat/ # 每类一个文件夹 │ │ ├── cat_001.jpg │ │ └── ... │ ├── dog/ │ └── ... └── val/ ├── cat/ ├── dog/ └── ...准备命令可以这样写mkdir -p data/train data/val # 把原始图片按类目移动到对应目录这里以 cat 为例 mkdir -p data/train/cat data/val/cat mv cat_sample_*.jpg data/train/cat/ # val 里放一部分和 train 不重叠的图片用于验证 mv cat_test_*.jpg data/val/cat/关键点是 train 和 val 的图片不能有交集否则验证集的精度会虚高。类别文件夹名会直接变成 ImageFolder 的 class_to_idx 映射类别名尽量用英文避免中文路径在部分系统上乱码。如果你的数据集下载下来是扁平结构、每张图带一个 label 文件那需要先按 label 批量移动到对应文件夹这步只执行一次。3.2 完整训练脚本代码结构、数据加载和训练循环下面的训练脚本可以直接落到项目里跑前提是你按照 3.1 的目录结构准备好了数据。默认用 EfficientNet-B0 预训练权重训练 30 个 epoch。import torch import torch.nn as nn import timm from torch.utils.data import DataLoader from torchvision import datasets, transforms device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据路径按项目包 data/train、data/val 的结构 train_dir data/train val_dir data/val # 训练集增强随机裁剪到 224x224B0 预设分辨率 train_tf transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), 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_set datasets.ImageFolder(train_dir, train_tf) val_set datasets.ImageFolder(val_dir, val_tf) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size64, shuffleFalse, num_workers4) # num_classes 从数据集自动读取换数据集不用改代码 model timm.create_model(efficientnet_b0, pretrainedTrue, num_classeslen(train_set.classes)) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) def evaluate(model, loader): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in 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) return correct / total best_acc 0.0 for epoch in range(30): model.train() total_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) scheduler.step() train_acc correct / total val_acc evaluate(model, val_loader) print(fEpoch {epoch1}: loss {total_loss/total:.4f}, ftrain_acc {train_acc:.4f}, val_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)代码逻辑分四块数据装载、模型构建、训练循环、验证评价。最关键的一行是 timm.create_model 里指定了 pretrainedTrue 和 num_classes前者加载 ImageNet 预训练权重后者把分类头替换成你自己的类别数。evaluate 函数里用了 torch.no_grad() 来关闭梯度计算否则验证阶段也会占用显存batch 大一点就会 OOM。loss 累加用了 loss.item() 乘 batch 大小而不是直接累加 loss 张量是为了避免梯度图一直积累导致内存泄漏这是个很多新手会踩的隐性坑。3.3 参数细读学习率、batch_size、权重衰减怎么设图像分类模型微调的参数设置其实比网络结构影响更大。项目的默认参数按经验如下参数推荐值说明学习率1e-4微调/ 1e-3从头训练预训练权重下用 1e-4 起步最稳batch_sizeB0: 64B4: 16~24跟显存走不够就减半权重衰减1e-4 ~ 1e-5防止过拟合不应不设Epoch30 ~ 50小数据集 30 够大数据集再加学习率调度CosineAnnealing比 step decay 更平滑精度更高一个常见分歧是学习率有人直接从 1e-3 开始微调EfficientNet 前几个 epoch loss 很容易震荡甚至 NaN因为分类头是新初始化的梯度差异大。我一般用 AdamW 1e-4然后前 5 个 epoch 做线性 warmup后面接 cosine 退火这也是大部分图像分类竞赛方案的标准路径。3.4 数据增强对付小数据集的首选手段如果你的图像分类数据集每类只有几百张光靠 EfficientNet 的预训练权重还不够过拟合会很快出现。训练脚本里已经加了 RandomResizedCrop、RandomHorizontalFlip 和 ColorJitter这三项是最便宜的增强基本没副作用。# 如果数据量特别少可以再加 RandAugmenttimm 里直接可用 from timm.data import RandAugment train_tf.transforms.insert(0, RandAugment(num_ops2, magnitude9))RandAugment 会自动组合旋转、翻转、颜色变换、锐化等策略比手动逐个加增强省事但要注意 magnitude 别超过 10否则图像被改得过于激进模型反而学不到真实特征。视觉上验证的方式是把增强后的图保存几张下来看一眼如果人眼看都认不出类别那就是增强过度了这个判断永远比任何论文参数都靠谱。4. EfficientNet训练避坑五个翻车现场与排查方案跑通一遍容易跑出理想精度难。这一章把最常见的几个故障现象、原因和排查路径直接列出来都是我实际训练时踩过、或者帮别人排查过的血泪经验。4.1 现象损失不降反升训练几轮后直接变成 NaN原因基本锁定两个学习率过大或者数据里有异常图片。EfficientNet 用的 SiLU 激活函数对梯度幅值比 ReLU 更敏感1e-3 的学习率即使有预训练权重也容易在前几个 epoch 把权重推出正常范围梯度爆炸后 loss 变成 NaN。另一个冷门原因是数据里混入了全黑、全白或者损坏的图片文件归一化之后出现极端值。解决方法是分两步排查先把学习率降到 1e-4加一个梯度裁剪 clip_grad_norm_(model.parameters(), max_norm1.0)如果还在 NaN就写脚本遍历数据集删除损坏图片或者用 PIL 打开失败的图片直接过滤。大多数情况是学习率问题改完就正常。4.2 现象选了个大模型一跑训练直接 CUDA out of memory原因很直接B4 的预设分辨率是 380对比 B0 的 224输入面积大了约 2.9 倍特征图内存也按这个比例涨。很多人把模型从 B0 换到 B4batch_size 还保持 6412GB 显存瞬间就炸了。解决思路按优先级排先把 batch_size 降到 16 或 8看是否跑得动不行就把输入分辨率手动缩到 320代价是精度损失但内存下降显著再不行开启梯度累积模拟大 batch。稳妥的组合是 B3 300 分辨率 batch 3212GB 显卡一般能跑。不要硬上大模型小模型调好参数往往比大模型调不明白得分更高。4.3 现象训练集精度接近满分验证集精度差一大截这是典型的过拟合。EfficientNet 参数量是少但预训练模型在 ImageNet 上学到的特征对你的任务不一定完全适用数据量不足时会很快记住训练集。解决做法有四个一是降低学习率让模型在预训练权重附近微调而不是大改二是加重数据增强RandAugment 的 magnitude 从 9 调到 14 这种激进设置反而能提升泛化三是把权重衰减提高到 1e-4四是减小模型B1 换 B0。如果这些还压不住检查一下 train 和 val 是否有重复图片数据集切分不合理造成的过拟合是数据问题不是模型问题。4.4 现象zip解压后数据集路径中文乱码ImageFolder 加载报错这是一个和图像分类模型本身无关、却最浪费时间的坑。Windows 上压缩的 zip 包默认文件名编码是 GBK在 Linux 上用 unzip 解压后中文目录名变成乱码ImageFolder 找不到类别目录直接报错。解决方法是解压时指定编码# 指定 GBK 编码解压中文文件名正常还原 unzip -O GBK image_classification_efficientnet.zip -d efficientnet_project如果你的 unzip 版本不支持 -O 参数用 7z 也可以7z x 压缩包名。还有更省事的做法解压后统一重命名成英文路径一劳永逸。批量重命名可以用 mv 配合循环脚本几行就能搞定。4.5 现象精度卡在某个值上不去怎么调都动弹不了这是最玄学也最常见的情况。先别急着调学习率按下面顺序逐个检查一是确认验证集预处理和训练时一致ResizeCenterCrop 是 256224 还是 300224两边不一致会导致输入分布对不上二是看类别是否均衡某个类只有几十张模型直接放弃学习这一类三是检查 scheduler 是否生效很多代码忘了 scheduler.step()学习率一直没降loss 自始至终在那波动。最后一个容易忽略的点混合精度的 scale 因子没处理好小 loss 长期被忽略导致梯度不更新看起来就是精度卡死。把 AMP 的 GradScaler 打印出来看如果 scale 一直在增大而不更新就该手动检查梯度或者去掉混合精度重跑一轮对比。5. 推理与验证把模型从训练搬到实际图片训练出 best_model.pth 只是第一步真正检验分类效果的是推理环节。这里的推理不单是拿模型跑一张图还包括能否稳定输出可解释的预测结果、能否科学评估模型在新数据上的表现。工程上这两件事通常要一起做。5.1 单张图片推理脚本加载权重、预处理、输出TopK训练时模型分类头是 len(train_set.classes) 个输出推理时类别顺序必须和训练时一致所以训练脚本里最好把类别列表保存下来。常见做法是存成 json# 训练结束后保存类别列表推理时加载 import json with open(classes.json, w) as f: json.dump(train_set.classes, f)推理脚本如下import json import torch import timm from PIL import Image from torchvision import transforms with open(classes.json, r) as f: classes json.load(f) model timm.create_model(efficientnet_b0, pretrainedTrue, num_classeslen(classes)) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() 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]) ]) img Image.open(test.jpg).convert(RGB) x val_tf(img).unsqueeze(0) with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1)[0] top3 torch.topk(probs, 3) for score, idx in zip(top3.values.tolist(), top3.indices.tolist()): print(f{classes[idx]}: {score:.4f})这段代码用 map_locationcpu 加载权重方便在没有 GPU 的机器上做推理验证也避免把显卡占用掉。预处理要和训练验证集完全一致Resize 256、CenterCrop 224、同一套 mean/std。只要这中间任何一个数字和训练时不一样结果都会偏差这是推理部署里最容易犯的低级错误。5.2 用混淆矩阵算真实的分类效果只看 top-1 准确率会掩盖类别间的问题某个类被另一个类系统性误判准确率数字完全看不出来。用混淆矩阵能直观看到对角线上哪些类亮、哪些类暗。下面的代码用 sklearn 生成矩阵并用 seaborn 画图import torch import numpy as np import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix def collect_predictions(model, loader, device): model.eval() y_true, y_pred [], [] with torch.no_grad(): for images, labels in loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) y_true.extend(labels.numpy()) y_pred.extend(preds.cpu().numpy()) return y_true, y_pred y_true, y_pred collect_predictions(model, val_loader, device) cm confusion_matrix(y_true, y_pred) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelstrain_set.classes, yticklabelstrain_set.classes) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png)看到矩阵后凡是某一列的其他类数值偏高说明模型把那些类都判成了同一个类优先检查那一个类的样本是不是和另一个特别相似或者那一类在训练集里数量过少。这一步能直接指出下一步该增强哪个类的数据比盲目加 epoch 有效得多。5.3 模型导出TorchScript与ONNX的可选操作如果模型要部署到服务端或移动端PyTorch 的 .pth 权重文件不够通用常见做法是导出成 TorchScript 或 ONNX。TorchScript 仍由 PyTorch 生态托管适合 Python 服务ONNX 适合跨框架和硬件加速。# 用 torch.jit.trace 导出 TorchScriptexample 需要一张和输入尺寸一致的张量 python -c import torch, timm model timm.create_model(efficientnet_b0, pretrainedTrue, num_classes10) model.load_state_dict(torch.load(best_model.pth)) model.eval() example torch.rand(1, 3, 224, 224) traced torch.jit.trace(model, example) traced.save(model.pt) ONNX 导出类似torch.onnx.export(model, example, model.onnx, opset_version12)。导出的模型最好用 onnxruntime 加载跑一遍和 PyTorch 输出对比一下数值差异差异在 1e-4 以内才算没导出坏。这一步别省因为优化器版本差异可能导致算子变换后结果偏离损失一点点精度还算小事输出结构变了才是大问题。6. 进阶混合精度与断点续训把训练效率再提一档模型精度稳定之后下一步就是缩短训练时间、避免意外中断造成返工。两个技巧效果最直接混合精度训练和带状态断点续训。混合精度用 PyTorch 自带的 amp 模块即可改动量很小scaler torch.cuda.amp.GradScaler() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()注意梯度裁剪如果用到了需要写成 scaler.unscale_ 之后再 clip顺序错了裁剪就不生效。这套改动在 30 系列及以上显卡上能提速约 30% 到 50%B4 以上的大模型收益更明显。不要关掉 GradScaler否则前几个 epoch 的梯度幅度波动会直接把你打回 NaN。断点续训要把 optimizer、scheduler、epoch 一起存下来只存 model.state_dict() 意味着中断后学习率状态全丢了。我习惯每个 epoch 结束存一次带后缀的 checkpoint只保留最后两个版本torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), epoch: epoch, best_acc: best_acc, }, fcheckpoint_{epoch}.pth)恢复时用 torch.load 后依次 load_state_dict 即可。这两个习惯组合起来一个周末训练一个大模型完全不虚。我之前有一次跑 B5 到第 22 轮断电重启后因为只有模型权重学习率和动量全丢了从头再训 22 轮才恢复到之前的精度那种回头路走一次就记住了。现在我的所有图像分类脚本都默认带 checkpoint 和 amp算是项目包里最值得保留的两个功能希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网