之前我做过一个图像分类任务,当时手里的主力模型是ResNet50,验证精度能到89%左右。想提点,第一反应是换更深的网络,于是试了ResNet101和ResNet152,折腾了大半个月,精度涨了不到一个点,训练时间和显存倒是翻着倍地往上蹿。那会儿我才切切实实体会到,模型“变大”这件事远没有想象中那么简单。后来我把方案换成了EfficientNet,同样的数据、差不多的训练成本,精度反而跨过了一个台阶。这篇文章就把EfficientNet从头到尾掰开揉碎讲清楚:它到底解决了什么问题、网络内部是怎么设计的、那个看起来很唬人的“复合缩放”到底是怎么回事,以及你在真实项目里应该怎么用它、有哪些坑要躲。适合准备做图像分类或特征提取的开发者,也适合想让模型在算力受限环境下跑得更高效的算法工程师。
1. 从“缩放”说起:EfficientNet解决的到底是什么问题
在EfficientNet出现之前,大家提升CNN效果的主要手段非常朴素:哪个方向不行就加哪个。要么加深网络层数,要么加宽通道数,要么把输入分辨率调大。这几种做法各有各的道理,但也各有各的隐性成本。
1.1 深度、宽度、分辨率三个维度,都有一堵看不见的墙
先看深度。增加卷积层数能提升模型容量,让高层特征更抽象,但收益会快速衰减。一个很直观的例子是ResNet从50层加到152层,提升已经很有限;再往上加,如果没有残差连接和归一化技巧,网络甚至很难收敛,梯度在反向传播过程中会逐渐消失。也就是说,深度不是无限增益的,它存在一个“边际收益递减”的临界点。
再看宽度。把每一层的通道数翻倍,理论上能提取更丰富的特征,但FLOPs会按平方规模膨胀。通道数过多还容易带来过拟合,尤其在小数据集上,模型会把训练集的噪声当成有效特征记住。很多人在实践中都有类似体验:把某几层通道从256加到512,精度没涨多少,训练时间却肉眼可见地变长了。
分辨率也是同理。把输入从224x224调到300x300甚至380x380,能保留更多纹理细节,但计算量同样迅速上升,而且高分辨率输入意味着网络需要更深的结构去把语义信息重新组织起来。如果网络不够深,单纯换高分辨率图反而会让感受野和细节信息不匹配,模型看似看了更多像素,实际有效信息利用率并不高。
1.2 比“单一维度缩放开到最大”更麻烦的,是维度之间的耦合关系
这三个维度并不是独立工作,而是互相制约的。举个很容易理解的例子:分辨率提高之后,同一个目标在图像里占据的像素更多,网络得用更多卷积层才能把它的语义信息逐级抽象出来;如果此时网络宽度不够,高分辨率带来的额外细节就没有足够的通道去编码,资源被白白浪费。这就是一种“木桶效应”——你把分辨率这块板拉高了,深度和宽度如果跟不上,整体性能还是会被短板卡住。
过去的做法更像是拍脑袋:有人习惯只加深,有人习惯只加宽,也有人习惯把分辨率一口气调大。但这样做的结果通常是某个维度很快触到上限,其他维度却还闲着,整体效率并不高。EfficientNet的核心洞察就在于此:既然三个维度之间是耦合的,那就不该只动其中一项,而是应该按一定比例同时缩放,让石头往同一个方向使劲。
1.3 EfficientNet给出的答案:把“缩放”当成一个有约束的优化问题
谷歌大脑团队在这个思路上做了一个很关键的转变:不再由人凭经验去猜缩放比例,而是把“在指定FLOPs预算下寻找最优精确度”定义成一个搜索问题。具体方法是先用手头可用的神经架构搜索(NAS)跑出一个基线网络,也就是后来广为人知的EfficientNet-B0,然后再通过一个统一的缩放系数φ,把深度、宽度、分辨率绑在一起同时放大。越大的φ对应越大的模型,从B0到B7就是这么来的。
这种方法最直接的收益是:在同样的算力预算下,EfficientNet拿到的精度通常比单独放大某一维度的老方案更高。当时论文里那张对比图让很多人印象深刻:EfficientNet-B7在ImageNet上的准确率超过了当时一堆更大更贵的模型,而它自己的FLOPs还低了至少一个数量级。这其实不是一个魔法,而是把有限的计算资源用在了更合理的位置上。
2. 复合缩放不玄乎:参数公式和它背后的直觉
很多人第一次接触EfficientNet,都会被那套“d、w、r”的复合缩放公式劝退。其实它不复杂,本质上就是一个带约束的条件极值问题。
2.1 一组系数,帮你在FLOPs预算内同时放大三个维度
EfficientNet把网络的三项关键属性定义为:深度系数d、宽度系数w、分辨率系数r。缩放时并不是单独缩放其中一个,而是通过一个公共指数φ把它们绑定起来:
- d = α^φ
- w = β^φ
- r = γ^φ
这里的α、β、γ是通过在小型模型上做网格搜索得到的常数,φ则是控制整体体量的全局缩放系数。φ=0的时候就是基线模型EfficientNet-B0;φ越来越大,模型整体按一定节奏“变宽、变深、看得更远”。
但这三个参数不是随便定的,它们之间有一个约束条件:α · β² · γ² ≈ 2。如果这个式子看起来有点突兀,可以用FLOPs的变化来解释。卷积层的计算量与深度成正比,与宽度(通道数)成平方关系,与分辨率(宽高尺寸)也成平方关系。也就是说,深度变成α倍时FLOPs大约变成α倍;宽度变成β倍时FLOPs大约变成β²倍;分辨率变成γ倍时FLOPs大约变成γ²倍。要想让总体FLOPs翻倍,就得让这三个维度的乘积保持在2左右。把深度系数的权重设成1,宽度和分辨率设成2,是为了让三个方向的资源消耗和收益保持一个大体均衡的状态。
2.2 网格搜索到底在搜什么
很多人误以为复合缩放公式是先天拍下来的,其实不是。作者在较小的模型上先跑了一系列NAS,固定一个FLOPs预算,然后让搜索算法去试不同的α、β、γ组合,看哪个组合在这个预算内能拿到最高精度。搜出来的典型配置大致类似α=1.2左右、β=1.1左右、γ=1.15左右,但具体数值会随搜索空间设置产生变化,不同复现版本也会有细微差别。
这个搜索的巧妙之处在于,它不需要直接去搜索超大规模的模型。先在小的B0上搜明白缩放比例,再靠φ把整个模型族“放大”出去,成本要低得多。这个思路后来也被很多模型设计继承了下来——先搜一个小骨架,再研究如何高效放大。
2.3 复合缩放不是追求单个维度的极限,而是追求资源的均衡分配
如果只谈直觉,复合缩放其实可以类比成一个团队扩编的问题。你给团队加预算,不能只疯狂加程序员而不加测试和产品,也不能只管加人手却不动项目范围。真正合理的扩编是让各个角色按比例同步增长,才能让整体协作效率最大化。
EfficientNet告诉我们的是:模型设计的资源分配也是同理。在FLOPs预算有限时,死磕深度不如把一部分算力匀给分辨率和宽度。这也是为什么同样算力下,EfficientNet能比单纯加深的模型拿到更好效果。理解了这个直觉,后面看网络内部结构就会更顺。
3. 网络内部拆解:MBConv、SE注意力与B0骨架
复合缩放解决的是“怎么放大”的问题,但放大之前的那个基线网络本身也必须足够高效。EfficientNet-B0不是随便搭的,它是在一个搜索空间里由NAS选出的结构,但它的核心组件其实都能找到清晰的来路。
3.1 MBConv:MobileNetV2的倒残差结构,为什么EfficientNet还在用
EfficientNet-B0的主体由一系列MBConv模块堆叠而成。MBConv全称是Mobile Inverted Bottleneck Conv,最早因为在MobileNetV2里的优秀表现而出名。它的结构和经典残差块相反:普通ResNet的bottleneck是先压缩通道再扩张,而MBConv是先利用1x1卷积把通道扩张,再做深度可分离卷积(Depthwise Conv),再用1x1卷积把通道投影回去。如果输入和输出分辨率一致且通道数一致,还会加一条短接残差。
这种“先扩后缩”的设计初衷,是深度可分离卷积在通道数很低的低维空间里能提取的信息太有限。先把通道扩张开,让每个通道有足够的表征空间,再做逐通道卷积,才能让信息流动更充分。它把标准卷积替换成深度可分离卷积之后,参数量和计算量都大幅下降,这也是EfficientNet整体FLOPs能压得那么低的核心原因。
3.2 通道注意力:SE模块在EfficientNet里的作用
只看MBConv还不够,EfficientNet在每个MBConv的深度可分离卷积之后还嵌入了一个SE(Squeeze-and-Excitation)模块。SE做的事情可以拆成两步:第一步“压缩”,把每个通道的特征图做一个全局平均池化,得到一个表示全局信息的数;第二步“激发”,经过两个全连接层和激活函数,输出一组介于0到1之间的通道权重,再逐通道相乘,把重要的通道放大、不重要的通道压低。
很多第一次看的时候会觉得SE模块不过是一个“注意力”的变体,但它在EfficientNet里的位置非常讲究。每个MBConv里都可以插入SE,也就意味着几乎每一层都在做通道级别的特征重标定。这带来的好处是:模型在资源有限时能自发地把算力倾向给信息量更大的通道,而不是让所有通道平均用力。实验里也可以看到,去掉SE模块后EfficientNet的精度下降非常明显,它并不是可有可无的点缀。
3.3 Swish激活函数和DropConnect,小细节里的大收益
EfficientNet用的激活函数不是ReLU,而是Swish:x乘以sigmoid(x)。Swish的特点是平滑且非单调,在深层网络上往往比ReLU更容易优化,梯度更平滑,信息能更顺畅地回传。这个函数在EfficientNet之前就有人研究过,但EfficientNet把它带进了大规模分类模型的实用领域,之后不少模型都跟着用。
另外一个细节是,EfficientNet的全连接层之前用的是DropConnect而不是传统Dropout。DropConnect不是随机把整个神经元输出置零,而是随机把某些权重连接置零,相当于一种更细粒度的正则化。这个设计结合合适的dropout比例,能让大规模模型在ImageNet那样的大数据集上不那么容易过拟合。如果复现时把这个比例设得过高或加错位置,训练曲线会明显异常。
3.4 从B0到B7,一张表看懂模型族差异
EfficientNet之所以好用,很大程度上是因为它在“模型尺寸”和“精度”之间提供了一条平滑的谱系,方便不同资源条件按需取用。下面是我整理的一张参数对照表:
| 模型版本 | 输入分辨率 | 宽度缩放 | 深度缩放 | 约FLOPs |
|---|---|---|---|---|
| EfficientNet-B0 | 224x224 | 1.0 | 1.0 | 0.39B |
| EfficientNet-B1 | 240x240 | 1.0 | 1.1 | 0.70B |
| EfficientNet-B2 | 260x260 | 1.1 | 1.2 | 1.0B |
| EfficientNet-B3 | 300x300 | 1.2 | 1.4 | 1.8B |
| EfficientNet-B4 | 380x380 | 1.4 | 1.8 | 4.2B |
| EfficientNet-B5 | 456x456 | 1.6 | 2.2 | 9.9B |
| EfficientNet-B6 | 528x528 | 1.8 | 2.6 | 19B |
| EfficientNet-B7 | 600x600 | 2.0 | 3.1 | 37B |
从这张表能看出一个趋势:B0到B7的FLOPs跨度接近一百倍。这既是优势也是约束——大版本虽然精度高,但对显存和推理时延的要求也高。真实项目里很少有人会一上来就上B7,更多是从B0到B4之间选一个合适的起点。
4. 从0到1实战:迁移学习跑通自己的图像任务
理论讲完,最要紧的还是把模型用起来。EfficientNet在PyTorch生态里接入非常方便,绝大多数场景不需要自己从零手写模型结构。
4.1 用timm库快速获得预训练权重
PyTorch官方模型库和很多第三方实现都收录了EfficientNet,我自己用得最顺的是timm库,一行代码就能加载预训练模型:
import timm import torch.nn as nn num_classes = 10 model = timm.create_model( "efficientnet_b0", pretrained=True, num_classes=num_classes ) model.train()如果只是想拿来提取特征,不换分类头,就把num_classes设为0。timm会自己根据预训练权重去适配最后的全连接层,省去手动对参数名的功夫。timm里可选的EfficientNet版本非常多,包括带_v2的最新版本,训练脚本里建议先用一个轻量版本跑通流程,再换更大的版本做对比。
4.2 数据预处理:尺寸、均值方差、增强策略
EfficientNet各版本的输入分辨率在预训练时是固定的,例如B0是224x224、B1是240x240、B3是300x300。加载预训练权重后最好配合相同的预处理参数使用。timm提供了一套推荐预处理参数,包括mean和std,直接用timm.data.create_transform或者timm.data.resolve_data_config就能取到,不需要自己照着ImageNet的归一化常数手工重算。
数据增强方面,EfficientNet的训练流程里比较常用的是RandAugment、MixUp和CutMix。这些策略在中小规模数据集上能明显提升迁移效果,但幅度需要根据数据量调节。数据量小的时候增强也别开太狠,否则会让模型更难拟合真实分布。
4.3 训练配置:优化器、学习率、EMA和微调策略
迁移学习中最容易翻车的是学习率先设太高,直接把预训练参数打乱。我的习惯做法是分两个阶段:第一阶段冻结backbone,只训练末尾的全连接分类层,学习率设在1e-3量级;跑几个epoch后,再解冻全部参数做整体微调,学习率降到1e-4左右,配合余弦退火scheduler。这个策略在小数据集上尤其有效。
优化器方面,我用得比较多的是AdamW,在一些大规模数据集上SGD也可以。EfficientNet的深度可分离卷积参数尺度差异较大,AdamW这类带权重衰减解耦的优化器通常更容易调出稳定结果。设置EMA(指数移动平均)能在不增加训练成本的情况下提升最终精度,实测稳定上涨零点几个点是很正常的。
4.4 训练脚本里的关键片段参考
import torch from torch import nn, optim from torch.optim import lr_scheduler criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW( model.parameters(), lr=1e-3, weight_decay=1e-4 ) scheduler = lr_scheduler.CosineAnnealingLR( optimizer, T_max=total_epochs ) # EMA影子权重 ema_model = timm.utils.ModelEmaV2(model, decay=0.999)这样一套配置跑下来,大多数中等规模图像分类任务都能较快地拿到一个不错的基线。之后再根据训练集大小和类别难度,决定是否从B0一级一级往上升级模型。
5. 真实项目里的表现与踩坑记录
模型选型时,光看排行榜上的精度数字不够,实际工程里的显存占用、推理速度、训练收敛难度同样关键。这部分我整理了一些在真实任务里的体会。
5.1 精度、显存和速度:一次典型对比的实际感受
在一个中等规模的图像分类任务上,我同时用ResNet50和EfficientNet-B3做了对比。训练集大概有几十万张图,类别数在100出头。ResNet50验证精度约91.2%,输入224x224;EfficientNet-B3输入300x300,验证精度约93.5%,整体FLOPs只有ResNet50的一半左右。
不过速度并不完全由FLOPs决定。EfficientNet里的深度可分离卷积和SE模块在GPU上不一定能完全换来与FLOPs成比例的速度提升,尤其是小batch推理时,卷积算子的调度开销会吃掉一部分理论优势。所以工程里做选型时,不能只看FLOPs,最好用自己的数据和推理后端实测一次。
5.2 容易翻车的四个具体场景
第一个坑是输入分辨率与预训练设置不一致。有人觉得B3是高分辨率训练的,直接在224x224下用,效果往往不升反降。分辨率低了,全局平均池化的感受野会和预训练时期差很多。真想用更高分辨率,要么做充分微调,要么牺牲掉一部分性能退回到标准分辨率。
第二个坑是训练时的batch size太小。EfficientNet里BN层很多,batch太小会让统计量非常不稳定,精度波动很大。如果显存有限,我建议用梯度累积来模拟大batch,而不是硬着头皮用小batch微调。
第三个坑是DropConnect的正则化强度。有些库对drop_rate的默认设置不完全一致,如果在自己数据上明显欠拟合或者过拟合,需要单独把这个参数调出来。位置和比率都影响很大,直接沿用默认参数有时候会得到一个“看起来训练正常但验证集一直上不去”的模型。
第四个坑是加载预训练权重时分类头维度不一致。换数据集时num_classes变了,预训练模型的最后一层权重参数配不上,加载时一定要忽略不匹配的键,否则会直接报错。timm的create_model对这种情况处理得比较顺,但自己手写加载逻辑时要留意。
5.3 反向思考:什么时候别优先选EfficientNet
EfficientNet并不是所有场景下的最优解。如果项目对推理延迟极敏感,并且主要跑在高端GPU上,某些经过算子深度优化的标准卷积网络反而更快,因为深度可分离卷积在小batch下没法充分利用GPU算力。另一个情况是数据集非常小,比如只有一两万张图且类别差异明显,这时ResNet或MobileNet这类结构更简单的模型反而更稳,EfficientNet的SE模块和复杂结构容易在数据不足时变得更难收敛。
还有一个现实约束:EfficientNet的预训练权重来自ImageNet,如果目标域和ImageNet差距很大,比如医学影像、卫星遥感,迁移收益会打折扣,这个时候与其钻研EfficientNet的缩放参数,不如把精力花在数据清洗和领域预训练上。选择模型本质上是选择“先验”和“预算”的匹配,没有绝对好坏。
6. 站在现在看EfficientNet:它的位置与后续演进
距离EfficientNet提出已经好几年了,这期间模型设计的风向变了好几轮,但它依然是很多项目里的默认参考之一。
6.1 EfficientNetV2在修正什么
EfficientNet本身有一个明显的痛点:训练太慢,尤其在超大版本上极耗时间。后来提出的EfficientNetV2主要做了三件事:一是把早期阶段的部分MBConv替换成Fused-MBConv,用普通卷积替代展开后的深度可分离卷积,减少内存访问开销;二是把缩放策略从“只看FLOPs”改成“兼顾训练速度和显存占用”;三是引入渐进式训练,在训练过程中动态调整输入分辨率和增强强度,让大模型训练更高效。
这意味着EfficientNetV2并不是一个从零开始的重新设计,而是在原版思路上的工程化修正。如果你的项目是从EfficientNet迁移过来的,直接换V2往往能在相近精度下拿到更短的训练时间和更低的内存占用。
6.2 和RepVGG、ViT这些后来者放在一起怎么选
RepVGG代表的是一条相反的路:训练时用复杂的多分支结构,推理时通过重参数化合并成单路卷积,让推理速度极快。它在GPU推理时比深度可分离卷积更友好,适合需要极致低时延的场景。ViT这类Transformer模型则在超大数据集上表现出了更强上限,但中小数据集上的迁移能力不如同规模CNN稳定。EfficientNet处于一个非常独特的位置:它不追求某个极端,而是在精度、效率、迁移性三者之间做到了很好的平衡。我现在的习惯是把它当作“中高强度工程基线”:先训一个小版本确认数据可行,再根据资源和效果往上下调整。
6.3 我认为最值得带走的一个方法论
EfficientNet留下的并不是单纯一组网络结构,而是“用统一预算同时优化多个维度”的组合思路。这个思路已经被后来的很多网络设计吸收,比如很多新模型在设定宽度、深度、分辨率时都会做联合考虑,而不是单独拍一个参数。
如果真要我给一个落地的使用建议,我会说:先别急着上B7,也别一上来就自己重新搜索缩放系数。用预训练的B1或B2把整个数据链路跑通,确认没有预处理和数据分布的问题之后,再根据瓶颈决定往大版本提升精度、往小版本压缩在线服务成本。模型选型这件事,最怕的是在一个还没验证清楚的数据集上直接堆大模型。EfficientNet最聪明的地方,在于它给了你一条平滑的“资源-精度”刻度尺,让你能精确找到属于自己的那个档位。