Le来发:MobileNetV3(详解)—— 从原理到 PyTorch 实现(含训练示例)

摘要 / 简介
MobileNetV3 是 Google 团队在 MobileNet 系列基础上,结合硬件感知的神经结构搜索(NAS)与手工经验改进后得到的轻量级网络系列(包含 MobileNetV3-LargeMobileNetV3-Small),目标是在移动/嵌入式设备上获得更好的“延迟—准确率”折中。本文将从背景、核心思想、网络细节、逐层计算举例到 PyTorch 的可运行实现、训练/评估与扩展实验一并讲清楚,主要依据:MobileNetV3 原始论文与 PyTorch / torchvision 的实现与说明。([arXiv][1], [PyTorch Docs][2])


1) 背景与发展(快速回顾)

  • MobileNetV1:提出深度可分离卷积(depthwise separable conv)以显著减少参数与计算。([arXiv][3])
  • MobileNetV2:提出“倒残差(inverted residual) + 线性瓶颈”,通过在窄通道间保留线性投影来保护表达力。([CVF 开放获取][4])
  • MobileNetV3:在 MobileNetV2 的倒残差思想上,融合了硬件感知的 NAS(MnasNet 风格)+ NetAdapt 的后处理简化,并引入轻量化的 SE(Squeeze-and-Excitation)变体与新的高效激活(Hard-Swish / H-Sigmoid),得到 Large(高精度)与 Small(低资源)两个版本。MobileNetV3 同时提出了面向分割的轻量解码器 LR-ASPP。([arXiv][1])

2) MobileNetV3 的核心思想与创新点(要点)

  • 硬件感知的自动化搜索 + 人工设计结合:使用平台感知的 NAS(例如 MnasNet 风格的搜索)得到初始结构,再用 NetAdapt 等方法进一步在目标硬件上调优/简化,从而直接优化真实延迟(不是只看 FLOPs)。([arXiv][1])
  • 改进的基本块(MobileNetV3 block):基于 inverted residual(expansion 1×1 → depthwise K×K → project 1×1),在中间层兼容 SE(可选),激活函数在不同位置使用 ReLU 或 H-Swish。实现上保留了 MobileNetV2 的「窄输入/输出 + 宽中间」设计。([PyTorch Docs][2])
  • 高效激活:提出 Hard-Swish(近似 Swish)和 Hard-Sigmoid,计算更廉价,且在低精度/移动设备上更友好。h-swish(x) = x * relu6(x + 3) / 6。([arXiv][1])
  • 轻量化的 SE:把 Squeeze-and-Excitation 做得更轻(使用小的中间通道并用 Hard-Sigmoid 作缩放),兼顾效果与速度。([arXiv][1])
  • 两条目标线Large 面向高精度场景;Small 面向极低计算/内存场景(例如关键点检测或轻量分类)。([arXiv][1])

3) 基本模块详解(结构与公式)

3.1 Inverted Residual(倒残差)模块(核心)

结构顺序(若 expand ≠ 1):

Input (C_in, H, W)
→ 1x1 conv (expand) : C_in → C_mid (= C_in * t)  (BN + Activation)
→ Depthwise KxK conv (groups=C_mid) (BN + Activation)  stride 可为1或2
→ (optional) SE  (global pooling → fc-reduce → relu → fc-expand → hard-sigmoid) 
→ 1x1 conv (project) : C_mid → C_out  (BN, **no** activation)
→ + residual (if stride==1 and C_in==C_out)
  • t:扩展比(expansion factor),常见取 1/3/6 等。([PyTorch Docs][2])

3.2 Squeeze-and-Excitation(轻量化)

  • 作用:在通道维度上做自适应重标定(轻量的注意力)。
  • MobileNetV3 中 SE 通常把中间 squeeze 通道设为 C_mid // 4 再向上扩展,并用 Hard-Sigmoid 作为缩放激活以减少计算。([PyTorch Docs][2])

3.3 硬激活函数(公式)

  • Hard-Sigmoid: h_sigmoid(x) = relu6(x + 3) / 6
  • Hard-Swish: h_swish(x) = x * h_sigmoid(x) = x * relu6(x + 3) / 6
    相比原始 Swish(x * sigmoid(x)),硬近似更节省计算,且更易于量化。([arXiv][1])

4) 逐层结构(表格)和逐层计算举例

下面按原文与 torchvision 的实现给出 MobileNetV3-LargeMobileNetV3-Small 的核心骨干(表格为简化阅读版:(输入C, K, 扩展C, 输出C, SE?, 激活, stride))。来源:论文与 torchvision 源码。([arXiv][1], [PyTorch Docs][2])

MobileNetV3-Large(简化表)

Stage (in_C, k, exp_C, out_C, SE, NL, s)
C1 (16, 3, 16, 16, False, RE, 1)
C1 (16, 3, 64, 24, False, RE, 2)
C1 (24, 3, 72, 24, False, RE, 1)
C2 (24, 5, 72, 40, True, RE, 2)
C2 (40, 5,120, 40, True, RE, 1)
C2 (40, 5,120, 40, True, RE, 1)
C3 (40, 3,240, 80, False, HS, 2)
(更多 bneck,见源码/论文)
C4 (112,3,672,112,True, HS,1)
C5 (尾部) conv1x1 → 1280 → pool → classifier

(完整配置可参考 torchvision 的 inverted_residual_setting 列表,源码内有逐行注释)([PyTorch Docs][2])

MobileNetV3-Small(简化表)

Stage (in_C, k, exp_C, out_C, SE, NL, s)
C1 (16, 3, 16, 16, True, RE, 2)
C2 (16, 3, 72, 24, False, RE, 2)
C3 (24, 5, 96, 40, True, HS, 2)
C4 (40, 5,240, 40, True, HS, 1)
(更多 bneck,见源码/论文)
C5 (尾部) conv1x1 → 1024 → pool → classifier

注:RE 表示 ReLU,HS 表示 Hard-Swish。以上表格为帮助理解的精简版,请参考论文 Table 1/2 或 torchvision 源码以获全部精确配置。([arXiv][1], [PyTorch Docs][2])


逐层计算举例(形状 & 参数 & MAdds 演示)

问题: 假设某个倒残差块的输入为 16 x 112 x 112(C=16, H=W=112),expansion t=6,kernel=3,输出 C_out=24,stride=1。求形状变化、参数数目与近似 MAdds。

步骤:

  1. C_mid = C_in * t = 16 * 6 = 96

  2. Expand 1×1 conv: 16 → 96

    • 参数数 = 1*1*16*96 = 1536
    • MAdds ≈ H * W * 16 * 96 = 112 * 112 * 1536 = 19,626,624
  3. Depthwise 3×3 conv(groups=C_mid): 96 channels,kernel=3

    • 参数数 = 3*3*96 = 864
    • MAdds ≈ H * W * 96 * 3 * 3 = 112 * 112 * 864 = 10,737,408
  4. Project 1×1 conv: 96 → 24

    • 参数数 = 1*1*96*24 = 2304
    • MAdds ≈ H * W * 96 * 24 = 112 * 112 * 2304 = 28,642,944
  5. 总计(该块)

    • 参数 ≈ 1536 + 864 + 2304 = 4,704 参数(非常小)
    • MAdds ≈ 19,626,624 + 10,737,408 + 28,642,944 = 59,006,97659.0M MACs(仅供示意)

计算公式(通用):

  • 卷积输出尺寸:O = floor((I + 2P - K) / S) + 1
  • 普通 1x1 conv 参数:C_in * C_out * 1 * 1
  • depthwise conv 参数:C_in * K * K(groups = C_in)
  • MAdds(乘加次数)以特征图点数乘以核与通道规模来估算,上面演示逐项计算以避免常见算错。

上面数值用来说明“每个倒残差块参数很少,但在一些高分辨率层 MAdds 仍然占比大”。MobileNetV3 的设计通过合适的 expansion、SE 的选择、以及NAS 来在性能/速度上做折中。([PyTorch Docs][2])


5) PyTorch 实现(可直接复制使用)

下面给出一个 自包含、可跑的简化 MobileNetV3 构建代码(学习与小规模实验用)。注意:在实际生产或训 ImageNet 时建议直接使用 torchvision.models.mobilenet_v3_large/small(下方实现参考了 torchvision 的设计)。实现细节:包含 HardSigmoidHardSwish、SE 和倒残差模块,支持 Large/Small 配置列表。

该代码已尽量写清楚注释,直接复制到 .py 中即可使用(需要 torch>=1.7)。如需加载官方预训练权重,建议用 torchvision.models 中提供的接口。([PyTorch Docs][2])

# mobilenet_v3_simple.py
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List, Callable, Optional

# -------------------------
# 轻量激活与 SE
# -------------------------
class HardSigmoid(nn.Module):
    def forward(self, x):
        return F.relu6(x + 3.0) / 6.0

class HardSwish(nn.Module):
    def forward(self, x):
        return x * (F.relu6(x + 3.0) / 6.0)

class SqueezeExcite(nn.Module):
    def __init__(self, in_ch, squeeze_ch, scale_activation=HardSigmoid):
        super().__init__()
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.fc1 = nn.Conv2d(in_ch, squeeze_ch, 1, bias=True)
        self.relu = nn.ReLU(inplace=True)
        self.fc2 = nn.Conv2d(squeeze_ch, in_ch, 1, bias=True)
        self.scale_act = scale_activation()

    def forward(self, x):
        s = self.avgpool(x)
        s = self.fc1(s)
        s = self.relu(s)
        s = self.fc2(s)
        s = self.scale_act(s)
        return x * s

# -------------------------
# 基本构件:Conv + BN + Activation
# -------------------------
def conv_1x1_bn(in_c, out_c, activation=nn.ReLU):
    layers = [nn.Conv2d(in_c, out_c, 1, bias=False), nn.BatchNorm2d(out_c)]
    if activation is not None:
        layers.append(activation())
    return nn.Sequential(*layers)

def conv_nxn_bn(in_c, out_c, kernel, stride, groups=1, activation=nn.ReLU):
    padding = (kernel - 1) // 2
    layers = [nn.Conv2d(in_c, out_c, kernel, stride, padding=padding, groups=groups, bias=False),
              nn.BatchNorm2d(out_c)]
    if activation is not None:
        layers.append(activation())
    return nn.Sequential(*layers)

# -------------------------
# Inverted Residual Block(可选 SE / HS)
# -------------------------
class InvertedResidual(nn.Module):
    def __init__(self, in_c, out_c, kernel, stride, expand_ratio, use_se, nl='RE'):
        super().__init__()
        assert stride in [1, 2]
        self.use_res_connect = (stride == 1 and in_c == out_c)
        activation = HardSwish if nl == 'HS' else nn.ReLU
        mid_c = in_c * expand_ratio

        layers = []
        # expand
        if expand_ratio != 1:
            layers.append(conv_1x1_bn(in_c, mid_c, activation=activation))
        # depthwise
        layers.append(conv_nxn_bn(mid_c, mid_c, kernel=kernel, stride=stride, groups=mid_c, activation=activation))
        # SE
        if use_se:
            squeeze_channels = max(1, mid_c // 4)
            layers.append(SqueezeExcite(mid_c, squeeze_channels, scale_activation=HardSigmoid))
        # project
        layers.append(nn.Conv2d(mid_c, out_c, 1, bias=False))
        layers.append(nn.BatchNorm2d(out_c))

        self.block = nn.Sequential(*layers)

    def forward(self, x):
        out = self.block(x)
        if self.use_res_connect:
            return x + out
        else:
            return out

# -------------------------
# MobileNetV3(简易版)
# -------------------------
class MobileNetV3(nn.Module):
    def __init__(self, inverted_residual_setting: List, last_channel: int = 1280, num_classes: int = 1000):
        super().__init__()
        # first layer
        layers = [conv_nxn_bn(3, inverted_residual_setting[0][0], kernel=3, stride=2, activation=HardSwish)]
        # bneck settings: list of tuples (in_c, k, exp_c, out_c, se, nl, s)
        for (in_c, k, exp_c, out_c, se, nl, s) in inverted_residual_setting:
            layers.append(InvertedResidual(in_c, out_c, k, s, expand_ratio=exp_c // in_c, use_se=se, nl=nl))
        # last conv
        last_in = inverted_residual_setting[-1][3]
        last_out = last_channel
        layers.append(conv_1x1_bn(last_in, last_out, activation=HardSwish))
        self.features = nn.Sequential(*layers)
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Sequential(
            nn.Linear(last_out, 1024),
            HardSwish(),
            nn.Dropout(0.2),
            nn.Linear(1024, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.pool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

# -------------------------
# 简化的配置示例(与源码相似;实际使用建议直接调用 torchvision)
# tuple: (in_C, kernel, expanded_C, out_C, use_se, nl, stride)
mobilenet_v3_small_setting = [
    (16, 3, 16, 16, True, 'RE', 2),
    (16, 3, 72, 24, False, 'RE', 2),
    (24, 3, 88, 24, False, 'RE', 1),
    (24, 5, 96, 40, True, 'HS', 2),
    (40, 5,240, 40, True, 'HS', 1),
    (40, 5,240, 40, True, 'HS', 1),
    (40, 5,120, 48, True, 'HS', 1),
    (48, 5,144, 48, True, 'HS', 1),
    (48, 5,288, 96, True, 'HS', 2),
    (96, 5,576, 96, True, 'HS', 1),
    (96, 5,576, 96, True, 'HS', 1),
]

# Example usage:
if __name__ == "__main__":
    model = MobileNetV3(mobilenet_v3_small_setting, last_channel=1024, num_classes=1000)
    x = torch.randn(2, 3, 224, 224)
    y = model(x)
    print("Output shape:", y.shape)  # (2, 1000)

提示:上面实现为教育版(直观清楚),若要训练 ImageNet、使用预训练权重或追求最高性能,请直接使用 torchvision.models.mobilenet_v3_large() / mobilenet_v3_small(),它包含作者精确的 channel 调整、_make_divisible 规则、以及官方权重支持。([PyTorch Docs][2])


6) 训练与评估(实践步骤与示例超参)

6.1 小样本/教学训练(CIFAR-10)示例流程(伪代码与超参)

下面给出快速实验的建议。对于 ImageNet 则需要更强的机器与更长的训练时间(参考 torchvision 的训练 recipe)。

  • 数据:CIFAR-10(或 ImageNet);输入尺寸:CIFAR-10 推荐 32x32(若用 MobileNet,建议上采样到 224x224 或修改第一个 stride);ImageNet 使用 224x224

  • 常用超参(示例,CIFAR-10)

    • optimizer:SGD(momentum=0.9, weight_decay=1e-4)
    • lr:0.1(batch=128 时)或 0.01(batch 小)
    • lr schedule:StepLR 或 CosineAnnealing,epochs=100
    • batch_size:128(视显存)
    • 数据增强:随机裁剪、水平翻转、颜色抖动(ImageNet 上建议更强的增强)
    • loss:CrossEntropyLoss
  • 示例训练循环(伪代码)

model = MobileNetV3(...).to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
criterion = nn.CrossEntropyLoss()

for epoch in range(epochs):
    model.train()
    for images, targets in train_loader:
        images, targets = images.to(device), targets.to(device)
        outputs = model(images)
        loss = criterion(outputs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    scheduler.step()
    # eval
    model.eval()
    # compute val acc...

6.2 参考训练成绩(来自 torchvision 官方权重说明)

  • torchvision 给出的 MobileNetV3-Large 在 ImageNet 上的 top-1 准确率约 ~74.0%(某些训练 recipe 可到 ~75.3%),模型参数约 ~5.48M,算力指标约 0.217 GMac(注意:不同数据源统计方式不同)。若需精确对比请参考 torchvision 的权重元信息与原论文。([PyTorch Docs][2], [arXiv][1])

6.3 训练注意事项

  • 若使用自实现模型,请注意 BatchNorm 的 momentum/eps 设置与权重初始化(跟 torchvision 的实现保持一致可得更稳定结果)。([PyTorch Docs][2])
  • 对于 mobile 网络在真实设备上评估延迟时,要直接在目标设备上测量(paper 强调:用 FLOPs 作为延迟 proxy 会有偏差)。MobileNetV3 的设计流程中也集成了“实际延迟”的搜素目标。([arXiv][1])

7) 实验扩展(替换激活 / 池化 / BN / 数据增强 的对比实验建议)

下面给出若干可复制的对比实验方向与评估指标,便于写论文性或博客性对比段落。

  1. 激活函数对比:ReLU / ReLU6 / Hard-Swish / Swish

    • 目标:比较精度、训练收敛速度、量化后精度(8-bit)与实际移动端延迟。
  2. 池化策略:AveragePool(S)VS MaxPool(S)

    • 在 MobileNet 系列中一般用 AvgPool(全局池化做全连接前的聚合),注意对中间层做空间下采样时 stride 与 padding 的匹配对准确率/延迟影响。
  3. 是否使用 SE:在部分倒残差引入或去除 SE,观察精度/延迟权衡(paper 指出在某些层引入 SE 能有效提升精度但会增加少量延迟)。([arXiv][1])

  4. BatchNorm 与 GroupNorm 对比(针对少样本微调)

  5. 数据增强策略对比:基础增强 vs AutoAugment / RandAugment(大量提升 ImageNet 类任务性能)

  6. 量化感知训练(QAT):比较量化后精度(INT8)与浮点精度差距,MobileNetV3 的硬激活更易量化。([arXiv][1])


8) 总结

  • MobileNetV3 将自动化搜索(NAS)与手工设计结合,带来比 MobileNetV2 更优的移动端折中(更高准确率或更低延迟的选择),并通过 Hard-Swish、轻量 SE 与针对移动设备的搜素/简化策略来达到目标。([arXiv][1])
  • 若你要在实际工程中使用:优先使用 torchvision 官方实现与预训练权重,在部署前一定要在目标设备上做实际延迟测量并据此做进一步剪枝或量化。([PyTorch Docs][2], [arXiv][5])
  • 本文给出的教学实现与逐层计算示例适合学习、演示和小规模试验;大规模训练或部署请参考官方权重、训练 recipe 与硬件测量工具。

更多推荐