EfficientNet是计算机视觉领域近年来最具影响力的卷积神经网络架构之一。作为一名长期从事深度学习模型优化的工程师,我第一次接触EfficientNet时就被它优雅的设计哲学所震撼——它不像大多数模型那样简单堆叠参数,而是通过系统化的方法实现了精度与效率的完美平衡。
这个项目将带您深入理解EfficientNet的核心思想,并手把手实现一个完整的图像分类模型。不同于简单的API调用教程,我们会从理论推导开始,逐步构建模型组件,最终在PyTorch中实现完整的训练流程。您将获得的不仅是一个可运行的代码库,更重要的是掌握模型缩放(Scaling)的底层方法论。
EfficientNet的核心突破在于提出了复合缩放(Compound Scaling)方法。传统模型缩放通常只调整深度(层数)或宽度(通道数),而EfficientNet的作者通过大量实验发现:
复合缩放的关键公式如下:
code复制depth = α^φ
width = β^φ
resolution = γ^φ
s.t. α·β²·γ²≈2, α≥1,β≥1,γ≥1
其中φ是用户定义的缩放系数,α,β,γ是通过网格搜索确定的基础系数。这种协同缩放方式使得模型在FLOPs增长有限的情况下,获得更显著的精度提升。
EfficientNet的基础构建块是MBConv(Mobile Inverted Bottleneck Conv),其结构包含:
这种设计在保持较大感受野的同时,极大减少了计算量。以EfficientNet-B0为例,其MBConv6模块的计算成本仅为标准卷积的1/9。
我们先实现最核心的MBConv模块:
python复制class MBConv(nn.Module):
def __init__(self, in_channels, out_channels, expansion=4, stride=1, se_ratio=0.25):
super().__init__()
expanded_channels = in_channels * expansion
self.use_residual = (in_channels == out_channels) and (stride == 1)
# 扩展层
self.expand = nn.Sequential(
nn.Conv2d(in_channels, expanded_channels, 1, bias=False),
nn.BatchNorm2d(expanded_channels),
nn.SiLU()
) if expansion != 1 else nn.Identity()
# 深度可分离卷积
self.dw_conv = nn.Sequential(
nn.Conv2d(expanded_channels, expanded_channels, 3,
stride=stride, padding=1, groups=expanded_channels, bias=False),
nn.BatchNorm2d(expanded_channels),
nn.SiLU()
)
# SE模块
squeeze_channels = max(1, int(in_channels * se_ratio))
self.se = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(expanded_channels, squeeze_channels, 1),
nn.SiLU(),
nn.Conv2d(squeeze_channels, expanded_channels, 1),
nn.Sigmoid()
)
# 投影层
self.project = nn.Sequential(
nn.Conv2d(expanded_channels, out_channels, 1, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
residual = x
x = self.expand(x)
x = self.dw_conv(x)
x = x * self.se(x)
x = self.project(x)
return x + residual if self.use_residual else x
基于MBConv构建完整的EfficientNet:
python复制class EfficientNet(nn.Module):
def __init__(self, width_coeff=1.0, depth_coeff=1.0, dropout=0.2):
super().__init__()
channels = [32, 16, 24, 40, 80, 112, 192, 320, 1280]
depths = [1, 2, 2, 3, 3, 4, 1]
strides = [1, 2, 2, 2, 1, 2, 1]
expansions = [1, 6, 6, 6, 6, 6, 6]
# 缩放通道数和深度
channels = [self._round_channels(c * width_coeff) for c in channels]
depths = [self._round_depth(d * depth_coeff) for d in depths]
# 主干网络
self.stem = nn.Sequential(
nn.Conv2d(3, channels[0], 3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(channels[0]),
nn.SiLU()
)
blocks = []
in_ch = channels[0]
for i in range(7):
out_ch = channels[i+1]
for j in range(depths[i]):
stride = strides[i] if j == 0 else 1
blocks.append(MBConv(in_ch, out_ch, expansions[i], stride))
in_ch = out_ch
self.blocks = nn.Sequential(*blocks)
# 分类头
self.head = nn.Sequential(
nn.Conv2d(in_ch, channels[-1], 1, bias=False),
nn.BatchNorm2d(channels[-1]),
nn.SiLU(),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Dropout(dropout),
nn.Linear(channels[-1], 1000)
)
def _round_channels(self, c, divisor=8):
return max(divisor, int(c + divisor/2) // divisor * divisor)
def _round_depth(self, d):
return int(math.ceil(d))
def forward(self, x):
x = self.stem(x)
x = self.blocks(x)
x = self.head(x)
return x
EfficientNet论文推荐使用RandAugment数据增强:
python复制from torchvision.transforms import autoaugment
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
autoaugment.RandAugment(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
RandAugment通过随机组合几何变换和颜色变换,在不引入额外超参数的情况下显著提升模型泛化能力。
使用余弦退火学习率配合线性warmup:
python复制def get_lr_scheduler(optimizer, warmup_epochs, total_epochs):
def lr_lambda(epoch):
if epoch < warmup_epochs:
return (epoch + 1) / warmup_epochs
return 0.5 * (1 + math.cos(math.pi * (epoch - warmup_epochs) / (total_epochs - warmup_epochs)))
return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
这种调度方式在训练初期稳定参数更新,后期逐步降低学习率以获得更好的收敛。
使用PyTorch的量化工具减小模型体积:
python复制model = EfficientNet().eval()
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear, nn.Conv2d}, dtype=torch.qint8
)
动态量化可将模型大小减少约4倍,推理速度提升2-3倍,而精度损失通常小于1%。
导出为ONNX格式实现跨平台部署:
python复制dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, dummy_input, "efficientnet.onnx",
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}
)
可能原因及解决方法:
nn.utils.clip_grad_norm_)优化策略:
python复制scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
加速方案:
EfficientNet架构可灵活迁移到其他视觉任务:
在实际项目中,我经常使用EfficientNet作为基础特征提取器。例如在医疗影像分析中,将B4版本的输出特征接入自定义头网络,在保持高精度的同时将推理速度控制在临床可接受的范围内。一个实用的技巧是在微调时冻结前三个阶段的参数,只训练高层网络,这样既能利用预训练特征又不会过度消耗计算资源。