别再只调参了!手把手教你用EfficientNet-B0的MBConv和SENet模块,在PyTorch里复现一个轻量级分类网络
从零构建EfficientNet-B0核心模块:MBConv与SENet的PyTorch实战指南
当你第一次看到EfficientNet论文中那些复杂的结构图时,是否感到无从下手?作为计算机视觉领域的重要里程碑,EfficientNet系列模型以其出色的性能与效率平衡著称。但大多数教程止步于理论介绍或简单调用预训练模型,很少深入探讨如何从零实现其核心架构。本文将带你用PyTorch亲手构建EfficientNet-B0的两个关键模块——MBConv和SENet,最终组装成一个完整的轻量级分类网络。
1. 环境准备与基础架构
在开始编码前,我们需要搭建好开发环境并理解EfficientNet-B0的整体架构。不同于直接调用torchvision.models.efficientnet,我们将从最基础的卷积层开始构建。
首先确保已安装最新版PyTorch和torchvision:
pip install torch torchvision matplotlibEfficientNet-B0由以下几个主要部分组成:
- 初始卷积层(Stem Convolution)
- 16个MBConv模块(核心部分)
- 顶部卷积层
- 全局平均池化和全连接分类层
让我们先定义网络的基本骨架:
import torch import torch.nn as nn class EfficientNetB0(nn.Module): def __init__(self, num_classes=10): super(EfficientNetB0, self).__init__() # Stem卷积层 self.stem = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1, bias=False), nn.BatchNorm2d(32), nn.SiLU() # Swish激活函数 ) # MBConv模块将在这里添加 self.blocks = nn.Sequential() # 顶部卷积层 self.top = nn.Sequential( nn.Conv2d(320, 1280, kernel_size=1, bias=False), nn.BatchNorm2d(1280), nn.SiLU() ) # 分类器 self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(1280, num_classes) ) def forward(self, x): x = self.stem(x) x = self.blocks(x) x = self.top(x) x = self.classifier(x) return x提示:Swish激活函数(SiLU)是EfficientNet中的重要组件,定义为x * sigmoid(x),在PyTorch中可直接使用
nn.SiLU()
2. MBConv模块的深度解析与实现
MBConv(Mobile Inverted Bottleneck Convolution)是EfficientNet的核心构建块,它结合了深度可分离卷积和残差连接。与MobileNetV2的MBConv不同,EfficientNet的版本还加入了SENet注意力机制。
2.1 MBConv的结构分解
一个标准的MBConv模块包含以下层次结构:
- 1×1扩展卷积(当expand_ratio>1时)
- 深度可分离卷积(Depthwise Convolution)
- SENet注意力模块
- 1×1投影卷积
- 残差连接(当满足条件时)
让我们先实现深度可分离卷积,这是MBConv的关键部分:
class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1): super().__init__() padding = (kernel_size - 1) // 2 self.depthwise = nn.Conv2d( in_channels, in_channels, kernel_size, stride=stride, padding=padding, groups=in_channels, bias=False ) self.bn1 = nn.BatchNorm2d(in_channels) self.pointwise = nn.Conv2d( in_channels, out_channels, kernel_size=1, bias=False ) self.bn2 = nn.BatchNorm2d(out_channels) self.act = nn.SiLU() def forward(self, x): x = self.depthwise(x) x = self.bn1(x) x = self.act(x) x = self.pointwise(x) x = self.bn2(x) return x2.2 完整MBConv的实现
现在我们可以构建完整的MBConv模块,注意处理expand_ratio和残差连接的条件:
class MBConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, expand_ratio=1, se_ratio=0.25): super().__init__() self.use_residual = (in_channels == out_channels) and (stride == 1) hidden_dim = in_channels * expand_ratio layers = [] # 扩展阶段 if expand_ratio != 1: layers.extend([ nn.Conv2d(in_channels, hidden_dim, 1, bias=False), nn.BatchNorm2d(hidden_dim), nn.SiLU() ]) # 深度可分离卷积 layers.extend([ nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride=stride, padding=(kernel_size-1)//2, groups=hidden_dim, bias=False), nn.BatchNorm2d(hidden_dim), nn.SiLU() ]) # 添加SENet模块 layers.append(SEModule(hidden_dim, se_ratio)) # 投影阶段 layers.extend([ nn.Conv2d(hidden_dim, out_channels, 1, bias=False), nn.BatchNorm2d(out_channels) ]) self.conv = nn.Sequential(*layers) def forward(self, x): if self.use_residual: return x + self.conv(x) return self.conv(x)注意:expand_ratio控制着通道扩展的程度,当expand_ratio=1时表示不进行通道扩展。se_ratio控制SENet模块中压缩的比例。
3. SENet注意力机制的实现与集成
SENet(Squeeze-and-Excitation Network)是MBConv中的重要组成部分,它通过学习通道间的关系来自适应地调整各通道的权重。
3.1 SENet的工作原理
SENet包含两个主要操作:
- Squeeze:全局平均池化,将空间维度压缩为1×1
- Excitation:两个全连接层形成瓶颈结构,学习通道间的相关性
实现代码如下:
class SEModule(nn.Module): def __init__(self, channels, se_ratio=0.25): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) reduced_channels = max(1, int(channels * se_ratio)) self.fc = nn.Sequential( nn.Conv2d(channels, reduced_channels, 1, bias=True), nn.SiLU(), nn.Conv2d(reduced_channels, channels, 1, bias=True), nn.Sigmoid() ) def forward(self, x): y = self.avg_pool(x) y = self.fc(y) return x * y3.2 SENet在MBConv中的位置
在MBConv中,SENet模块位于深度可分离卷积之后、投影层之前。这种位置安排使得网络可以先提取空间特征,然后通过注意力机制重新校准通道重要性,最后再进行降维。
为了验证我们的实现是否正确,可以对比有无SENet模块的性能差异:
# 测试MBConv模块 mbconv_with_se = MBConv(32, 16, expand_ratio=6, se_ratio=0.25) mbconv_without_se = MBConv(32, 16, expand_ratio=6, se_ratio=None) x = torch.randn(1, 32, 224, 224) print("Output with SENet:", mbconv_with_se(x).shape) print("Output without SENet:", mbconv_without_se(x).shape)4. 完整网络组装与训练技巧
现在我们已经实现了所有关键组件,接下来需要按照EfficientNet-B0的架构将它们组装起来。
4.1 构建完整的网络块
EfficientNet-B0包含7个阶段,每个阶段有特定的配置:
| 阶段 | 操作 | 重复次数 | 输入通道 | 输出通道 | 扩展比例 | 核大小 | 步长 | SE比例 |
|---|---|---|---|---|---|---|---|---|
| 1 | MBConv | 1 | 32 | 16 | 1 | 3x3 | 1 | 0.25 |
| 2 | MBConv | 2 | 16 | 24 | 6 | 3x3 | 2 | 0.25 |
| 3 | MBConv | 2 | 24 | 40 | 6 | 5x5 | 2 | 0.25 |
| 4 | MBConv | 3 | 40 | 80 | 6 | 3x3 | 2 | 0.25 |
| 5 | MBConv | 3 | 80 | 112 | 6 | 5x5 | 1 | 0.25 |
| 6 | MBConv | 4 | 112 | 192 | 6 | 5x5 | 2 | 0.25 |
| 7 | MBConv | 1 | 192 | 320 | 6 | 3x3 | 1 | 0.25 |
根据上表配置网络:
def make_blocks(): block_configs = [ # (num_repeat, in_channels, out_channels, expand_ratio, kernel_size, stride, se_ratio) (1, 32, 16, 1, 3, 1, 0.25), (2, 16, 24, 6, 3, 2, 0.25), (2, 24, 40, 6, 5, 2, 0.25), (3, 40, 80, 6, 3, 2, 0.25), (3, 80, 112, 6, 5, 1, 0.25), (4, 112, 192, 6, 5, 2, 0.25), (1, 192, 320, 6, 3, 1, 0.25) ] blocks = [] for config in block_configs: num_repeat, in_c, out_c, expand_ratio, kernel_size, stride, se_ratio = config # 第一个块可能有不同的stride blocks.append(MBConv(in_c, out_c, kernel_size, stride, expand_ratio, se_ratio)) # 重复的块保持通道数不变,stride=1 for _ in range(1, num_repeat): blocks.append(MBConv(out_c, out_c, kernel_size, 1, expand_ratio, se_ratio)) return nn.Sequential(*blocks)4.2 训练技巧与超参数设置
在CIFAR-10这样的小数据集上训练EfficientNet时,需要注意以下几点:
- 学习率调度:使用余弦退火学习率
- 数据增强:RandAugment或AutoAugment效果很好
- 优化器选择:使用带有权重衰减的AdamW
- 标签平滑:有助于防止过拟合
示例训练代码片段:
from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model = EfficientNetB0(num_classes=10) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=100) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 训练循环 for epoch in range(100): for inputs, targets in train_loader: outputs = model(inputs) loss = criterion(outputs, targets) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()4.3 模型压缩与部署优化
虽然EfficientNet已经是轻量级模型,但在边缘设备上部署时还可以进一步优化:
- 量化:使用PyTorch的量化工具减少模型大小
- 剪枝:移除不重要的连接
- TensorRT加速:转换模型以获得更好的推理性能
量化示例:
model = EfficientNetB0().eval() quantized_model = torch.quantization.quantize_dynamic( model, {nn.Linear, nn.Conv2d}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "efficientnet_quantized.pth")在实际项目中,我发现MBConv模块中的深度可分离卷积对计算效率提升最大,而SENet虽然增加了少量计算量,但带来的精度提升通常值得这些额外开销。特别是在处理细粒度分类任务时,通道注意力机制能显著改善模型对细微特征的识别能力。
