很多人第一次看到EfficientNet这个名字,第一反应大概率是“又一个刷榜的网络?”。但它能火这么多年,被大量分类、检测、分割任务当作骨干网络来用,靠的可不只是精度数字。EfficientNet真正厉害的地方在于:它把网络设计的三个关键维度——深度、宽度、分辨率——用一个数学约束统一了起来,让“调参定网络”变成“定一个计算预算就行”。这篇文章我不打算照搬论文,而是从我自己使用和复现的角度,把EfficientNet的核心思路、模块结构、代码实操和踩坑经历完整讲一遍。如果你正在做视觉任务,想找一个算力与精度平衡的基线模型,或者只是想知道为什么EfficientNet能这么“省参数”,这篇文章应该能给你答案。
1. 设计思路拆解:为什么深度、宽度、分辨率必须联合缩放
1.1 以前的调参困局:三个旋钮各调各的
在EfficientNet出现之前,提升网络精度的手段基本就三类:把网络做得更深(更多层)、做得更宽(更多通道)、或者把输入图片分辨率放大。这三件事听起来简单,实际操作起来却很容易翻车。
举个例子,你单纯加深网络到50层、100层,感受野变大了,特征抽象层次更多,但深层如果通道不够,信息瓶颈就会卡住,甚至出现梯度传播困难。单纯加宽通道,短时间确实涨点,但参数量和计算量跟着暴涨,训练开销直线上升。分辨率也是一样,你把输入从224放大到512,模型能看到的纹理细节是多了,但浅层卷积核如果cover不过来,反而引入大量冗余计算,精度收益也不一定划算。
更麻烦的是,这三个旋钮之间是耦合的。加深网络的时候,应当同步放大输入分辨率,让深层卷积的感受野覆盖到更完整的空间信息;加宽通道之后,又得加深几层去组合更抽象的特征。手动调这三个维度,等于在一个高维空间里做黑盒搜索,经验再丰富的人也只能靠试错。EfficientNet要解决的,正是这个“既要又要还要”的调参难题。
1.2 一个公式搞定缩放:复合缩放模型
EfficientNet的做法很直接:设定一个基线网络(EfficientNet-B0),然后通过三个缩放系数去控制深度、宽度和分辨率,并且强制它们满足一个约束关系。
具体来说,深度d按α的φ次方缩放,宽度w按β的φ次方缩放,分辨率r按γ的φ次方缩放:
- 深度:d = α^φ
- 宽度:w = β^φ
- 分辨率:r = γ^φ
约束条件是:α · β² · γ² ≈ 2。
这里面的关键在于,卷积网络的计算量(FLOPs)近似正比于深度 × 宽度² × 分辨率²。为什么是平方?因为卷积运算是在空间维度和通道维度同时消耗算力,你把特征图的宽高放大一倍,flops变成四倍;你把通道数放大一倍,同样变成四倍。所以要让总计算量可控,就必须让宽度和分辨率的平方项参与约束。
这样一来,每增加一个φ,目标总计算量就大约翻一倍。用户不需要再纠结“到底加深好还是加宽好”,只需要想一个问题:我手里有多少算力预算,能接受多大的计算量翻倍,然后直接把φ定下来就行。
我当时第一次看到这个设计的时候,最大的感触是它把“玄学调参”收敛成了“选参数”。论文作者团队在B0上用网格搜索找到了最优的α、β、γ组合(大概在1.2、1.1、1.15附近),这几个系数一旦固定下来,后续从B1到B7全部沿用同一套比例,只是φ在递增。这相当于只留了一个自由度给使用者,其余全部自动推导,工程上非常友好。
提示:复合缩放解决的是“怎么从基线放大到更大模型”的问题,但前提是你得有一个足够好的基线网络。所以EfficientNet的另一半功劳,在NAS搜索出的B0结构——这就引出了下面的MBConv和NAS。
2. 架构核心:NAS搜索出来的B0与MBConv模块
2.1 NAS搜索策略:先搜小网络,再统一放大
EfficientNet-B0并不是人工设计的网络,而是通过神经架构搜索在固定计算预算约束下搜出来的。因为直接在大网络上做结构搜索的代价太高,作者团队先在一个小的计算约束(约400M FLOPs,B0)下搜索基线结构,搜到一个精度和参数量的平衡解,再通过复合缩放把它放大成B1到B7。
这种做法在逻辑上非常合理:NAS在搜索空间里探索各种可能的结构组合,如果空间太大,搜索根本跑不动。先固定在一个小规模上搜索,至少保证了搜索到的模块是“性价比高”的;后面做复合缩放,本质上是在同样的模块结构上等比放大,而不改变模块本身的设计。
B0的整体结构其实并不复杂,大致可以概括为:开头一个普通卷积做stem,中间串联多个stage,每个stage由若干个MBConv模块堆叠而成,最后接一个全局池化和全连接分类层。各个stage之间的通道数和层数,在B0里已经定好,后面放大时按复合缩放公式调整即可。
2.2 MBConv模块:轻量又强效的积木
B0里用得最多的基础模块是MBConv。它源自MobileNetV2的倒残差结构,但在此基础上做了一些改进。一个典型的MBConv流程如下:
- 先用1×1卷积把通道数扩张到原来的若干倍(通常是6倍,这时记作MBConv6);
- 然后做深度可分离卷积,也就是每个通道独立卷一次,用较小的kernel size把空间信息处理掉;
- 再接SE模块对通道做注意力加权;
- 最后用1×1卷积把通道数压回目标值,和输入的跳连相加。
整个过程的关键在于,参数和计算量主要集中在内层的高维空间,而跳连连接在低维空间,信息流可以更顺畅地传递。深度可分离卷积把空间卷积和通道变换拆开,极大减少了计算量。我实测下来,一个标准的MBConv6模块在同样计算预算下,表达能力确实比传统残差块更高,这也是EfficientNet参数消耗少、精度却不低的直接原因。
放大之后的EfficientNet,每个stage里的MBConv层数会变多,通道数也会变宽,但模块的基本套路不变。你在读代码的时候只要认准MBConv这个抽象,整张网络就能拆成一张“积木清单”。
2.3 SE模块与Swish激活函数:细节里的精度增量
EfficientNet里的SE模块也很值得单独说。SE的全称是Squeeze-and-Excitation,它先对特征图做全局平均池化,把每个通道的全局信息压成一个标量,再经过两个全连接层,最后通过sigmoid输出0到1的通道权重,把这些权重乘回原来的特征图。
这个操作本质上是在教网络“动态选择哪些通道更重要”。有些通道在某个样本上贡献大,权重就高;贡献小的,权重被压低。它在原网络基础上增加的参数量微不足道,但对精度的提升是实打实的。我当时在某个小型分类任务上把SE模块去掉做消融,top-1准确率掉了将近1个百分点,这个收益完全对得起那一点点额外开销。
激活函数方面,EfficientNet没有沿用ReLU,而是用了Swish,公式是x·sigmoid(x)。Swish的好处是函数曲线平滑,不存在ReLU在负区间直接“死掉”的问题,梯度流动更稳定。代价是sigmoid的计算比ReLU稍微费一点,但对整体精度的影响非常正面。实操中发现,如果在老设备上做端侧推理,Swish对算子库的优化要求更高,个别硬件上可能要单独做手工算子融合,否则延迟会偏高。
3. 代码实操:30分钟跑通EfficientNet的训练与迁移
3.1 PyTorch调用与自定义分类层修改
现在安装和使用EfficientNet已经非常方便了,尤其是PyTorch环境,直接一行代码就能加载带预训练权重的模型:
import torchvision.models as models # 加载B0预训练权重 model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)如果你的任务是二分类或者多分类,而ImageNet是1000类,那就需要把最后的分类层换掉。EfficientNet的最终分类层通常叫classifier,里面是一个Dropout加一个线性层。修改方式如下:
import torch.nn as nn num_classes = 10 model.classifier[1] = nn.Linear(in_features=1280, out_features=num_classes)注意,B0最后输出的特征维度是1280,B7也基本保持这个数字,但如果用了不同的变体,最好先打印model结构确认一下。如果你做的是一个规模较小的数据集,可以直接冻结前面的backbone只训练分类层,我记得第一次实验时,只训分类层用很小的学习率也能拿到不错的基线,后面再逐步解冻更深的段。
3.2 版本选择逻辑:B0到B7的算力与精度权衡
EfficientNet最常用的几个版本是B0、B1、B3、B4、B7,它们之间的差距主要体现在计算预算上。因为复合缩放的系数是固定的,所以版本间的关系很清晰,但每个版本的推荐场景差异很大:
| 版本 | 约参数规模 | 约FLOPs | 典型使用场景 |
|---|---|---|---|
| B0 | 5.3M | 0.39B | 移动端、边缘设备、快速基线 |
| B1 | 7.8M | 0.7B | 算力稍强的设备,精度和速度均衡 |
| B3 | 12M | 1.8B | 单卡服务器训练,日常视觉任务首选 |
| B4 | 19M | 4.2B | 对精度要求较高的场景 |
| B5 | 30M | 9.9B | 高精度离线任务,对延迟不太敏感 |
| B7 | 66M | 37B | 大算力离线任务,精度天花板较高 |
我个人的习惯是:先看计算预算,再看数据量。数据量小(几千张级别),优先B0或B1,因为大数据集才喂得起大网络;数据量大且训练资源充足,B3或B4是一个性价比极高的区间,再往上到B5/B7,精度提升开始放缓,但训练成本和推理成本翻倍涨。B6和B7之间的区别在实测中经常小于0.1个百分点,所以如果你没有严格的SOTA需求,B4到B5之间就够用了。
3.3 训练配置细节:优化器、学习率与数据增强
EfficientNet对训练配置比较敏感,尤其是大版本,一个容易踩坑的点是——盲目拉到很大输入分辨率,同时batch size没跟上,最终精度反而下降。我的经验是把下面几个配置作为起步参考:
# 典型的分类训练配置参考 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) # 输入分辨率建议 # B0-B1: 224 或 256 # B3-B4: 288 或 320 # B5-B7: 384 或 456数据增强方面,预训练权重默认是在ImageNet上训练的,迁移到自己的数据集时,只需要常规的RandomResizedCrop和RandomHorizontalFlip即可。但如果你的数据集非常接近ImageNet分布(比如自然图像分类),可以考虑用更强一点的AutoAugment或RandAugment。我在一个工业瑕疵数据集上试过,强增强对B3是有正面效果的,但对B0这种小网络,强增强有时反而拉低精度,因为它本身容量有限,学不过来。这个反差值得留意。
训练时推荐使用混合精度(AMP),EfficientNet的Swish和SE模块在FP16下数值稳定性没问题,显存压力也能小一半。如果batch size很小(比如只有8或16),建议初始化BN时适当调低momentum,比如从默认的0.1改成0.01,避免小batch下batch统计量抖动太厉害。
4. 项目实战中的硬坑:推理速度、精度复现与问题速查
4.1 FLOPs低不等于跑得快:延迟实测颠覆直觉
EfficientNet在论文里以“低FLOPs”著称,但真到了实际部署阶段,你可能会发现它的推理延迟并没有想象中那么好看。原因在于FLOPs只是一个理论计算量指标,它不考虑内存访问开销、算子调度和硬件优化程度。
我在CPU上做过一个简单的延迟测试,用ONNX Runtime跑同一批图片:ResNet50大概需要85毫秒,EfficientNet-B3理论FLOPs虽然只有ResNet50的1/2左右,实际延迟却在100毫秒上下,反而更慢。原因很好理解:MBConv里的depthwise卷积,在底层硬件上并没有像普通卷积那样被深度优化过;SE模块里的两个全连接层也增加了几次矩阵运算,这些小算子反复调用,内存读写层面吃掉了很多时间。
所以我的建议是:立项阶段就做真实设备benchmark,不要拿paper里的FLOPs去估算延迟。GPU场景下,新一点的显卡对depthwise卷积优化明显更好,EfficientNet的优势能体现出来;但CPU或者老旧深度学习加速卡上,ResNet这类常规卷积网络可能有更稳定的表现。
注意:如果部署目标是嵌入式设备或老款推理卡,强烈建议先用ONNX导出你的模型,跑一次目标硬件的实测再决定要不要选EfficientNet。
4.2 输入分辨率不一致导致的精度崩坏
EfficientNet对分辨率很敏感,因为复合缩放本身就包含了分辨率维度,B5和B7对应的输入尺寸是384和456,不是默认的224。我见过不少人拿预训练的EfficientNet-B4(训练分辨率大概是320)直接迁移到自己的224分辨率数据集上微调,结果发现收敛速度慢,最终精度也不理想。
原因在于:预训练权重学习到的特征统计是在320分辨率下形成的,小分辨率下卷积核的感受野覆盖模式变了,浅层特征分布也跟着变化。解决方案有两种:一是把训练输入分辨率调整到和预训练接近,再配合几轮warmup;二是如果任务本身要求小输入,最好从一开始就在224分辨率下重新预训练或做一个较长的微调。根据我自己做过的一组对比实验,在B4上从256分辨率切到224,不做适配地硬训,top-1准确率掉了将近2%,但加了几轮低分辨率适应训练后差距缩小到0.5%以内。
4.3 大模型过拟合与BN参数调整
B0是5M参数,B7是66M参数,模型容量差了十几倍,对应的正则化强度必须跟着变。如果你只有几千张图的训练集,却直接上B5,过拟合几乎是必然的。这时候可以留意两个点:一是预训练权重中自带Dropout系数,B0大约0.2,B5以上接近0.5,自己搭建模型时要按比例调整;二是如果资源允许,UseStochastic Depth(随机深度)或者DropPath,对深层大模型极有帮助。
还有一个高频问题是BN在大batch和小batch之间的表现差异。我用单卡训练B3时batch设为64,没有任何问题;但换成B6时显存吃紧,batch只能压到24,结果loss异常波动。后来把BN的momentum从0.1改成0.01,并把训练轮数适当延长,波动才缓解。小batch下,不要小看这个参数,它对稳定训练的影响比其他层大得多。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 微调精度一直上不去 | 输入分辨率与预训练不一致 | 调整输入尺寸或加长低分辨率适应训练 |
| 大模型过拟合严重 | 数据量小、正则不足 | 换小版本模型、调大Dropout、加数据增强 |
| Batch小导致loss震荡 | BN统计量不稳定 | 调低momentum、增大有效batch、使用SyncBN |
| 推理延迟远高预期 | depthwise和SE算子未优化 | 导出ONNX做算子融合、在目标硬件测试 |
| B6到B7提升不明显 | 精度已进入饱和区 | 优先选B6,省下的算力做ensemble或TTA |
| 混合精度训练时精度损失 | Swish/SE在FP16下精度不足 | 使用AMP的dynamic loss scaling机制 |
5. 延伸应用:EfficientNet在不同视觉任务中的变形与我的使用体会
5.1 从分类到检测、分割的骨干迁移
EfficientNet并不仅限于图像分类。在做目标检测时,很多方案会直接把它替换为骨干网络,比如Faster R-CNN或FCOS的backbone。因为EfficientNet的特征图本身有丰富的多尺度表达能力,浅层有纹理细节,深层有语义信息,拿来做FPN输入非常合适。不过这里有一个容易忽略的点:检测任务的输入分辨率通常是固定尺寸(如512×512或640×640),而EfficientNet的预训练分类分辨率未必匹配,因此需要专门在检测数据上做长时间finetune,否则backbone的初始特征分布会让检测头非常难受。
图像分割任务也是同理。EfficientNet的stacked stages天然适合U-Net这类编码器-解码器结构,编码阶段直接使用不同stage的输出作为特征金字塔。我自己的使用体验是,EfficientNet作为分割任务backbone,在内存占用上比同等精度的ResNet系列友好很多,尤其适合高分辨率输入场景。但要注意的是,SE模块会引入更多通道间的依赖,对解码阶段的特征融合有一定帮助,但也不可避免地增加了一点延迟。
5.2 模型的适用边界与选型建议
沿用EfficientNet几年做下来,我的体会是它非常适合做“性价比基线”:在计算资源有限、又想快速迭代idea的时候,EfficientNet-B0/B1永远是第一选择——参数量小、显存占用低、精度不差,不管是做特征提取还是直接接分类头,都足够灵活。
但如果你追求极致的推理吞吐量,或者目标部署设备的计算生态比较老旧,我建议先把ResNet和RepVGG这类普通卷积网络放进对比池,实测里这些结构的算子优化更成熟,延迟更稳定。EfficientNet在学术领域极度光亮,在工业落地时则要额外小心,它省的很多理论FLOPs可能在实际硬件上“省不到位”。
5.3 相比后辈模型,EfficientNet还值得用吗
现在EfficientNetV2、ConvNeXt、Transformer-based的分类模型已经层出不穷,EfficientNet作为早期NAS+缩放的产物,在架构上肯定不是最前沿了。但我的判断是:它依然值得作为基线模型保留在工具箱里。原因主要有三点:第一,它的源码和预训练权重覆盖范围广,几乎所有深度学习框架里都能找到现成实现,调试成本极低;第二,它的性能可预测性强,B0到B7的性能曲线平滑,你可以很直观地估算不同预算下的效果;第三,NAS搜出来的结构本身就带有很强的“性价比倾向”,在小数据集上两三个epoch就能看出模型的收敛趋势,这一点对基础研究非常重要。
如果你之前一直用ResNet做一切任务的基线,下次遇到新任务,不妨直接把EfficientNet-B0或B3丢进去对比一轮。在很多情况下,它可能会让你的下一个项目有一个更好的起点。
最后再分享一个小经验:EfficientNet的学习率设置不宜过大。我训练时用AdamW常规的1e-3起步,配合warmup在5个epoch内线性升到设定学习率,整体稳定性和最终精度都比直接恒定1e-3要好。网络越深,对lr越敏感,B4以上的模型如果lrn设为3e-4或更低,反而更容易复现出理想的结果。砸几次坑之后你会慢慢发现,这个模型在“温和训练”下表现最好,也算是一种意外但实用的调参心得。