人工智能-python-深度学习-神经网络-MobileNet V1&V2

LeNet-5(详解)------ 从原理到 PyTorch 实现(含训练示例)

文章目录

标题

MobileNet V1 & V2(详解)------ 从原理到 PyTorch 实现(含训练示例与对比实验)


简介:为什么要学习 MobileNet 系列

MobileNet 系列是为移动端/嵌入式场景设计的轻量级卷积神经网络家族。它们通过结构设计(Depthwise Separable Convolution、宽度/分辨率缩放、倒置残差与线性瓶颈等)在"精度 ↔ 计算/内存"之间做出高效平衡,是实际工程中非常实用的模型。本文目标是:讲清核心思想、给出逐层计算举例、并提供可直接运行的 PyTorch 实现与训练/评估流程,便于你上手学习与改进。


核心思想(总结)

  1. Depthwise separable convolution(深度可分离卷积):把标准卷积分解为"depthwise" + "pointwise",从而大幅降低参数与计算量(论文给出 3×3 的情形通常能节省约 8--9 倍计算)。
  2. 模型缩放超参 (MobileNet V1):使用 width multiplier(宽度缩放 α)resolution multiplier(分辨率缩放 ρ) 来在线性权衡精度/速度/内存。
  3. 倒置残差 + 线性瓶颈(MobileNetV2):在瓶颈处使用扩展(先扩张通道再做深度卷积,最后线性投影回小通道),并在窄层做残差连接,从而保持信息流并节约内存,同时使用 ReLU6 来增强低精度场景的鲁棒性。

逐层结构(概要表)

MobileNet V1(简化版结构)

(以标准 224×224×3 输入,最后 FC 输出 1000 类为例)

  • conv3×3, stride=2, 32
  • dw-sep(3×3) → 64 (s=1)
  • dw-sep(3×3) → 128 (s=2)
  • dw-sep(3×3) → 128 (s=1)
  • dw-sep(3×3) → 256 (s=2)
  • dw-sep(3×3) → 256 (s=1)
  • dw-sep(3×3) → 512 (s=2)
  • dw-sep(3×3) → 512 (s=1) ×5(重复5次)
  • dw-sep(3×3) → 1024 (s=2)
  • dw-sep(3×3) → 1024 (s=1)
  • global avg pool → FC(1000)
    (详细表可查论文 Table 1)

MobileNet V2(关键层次表,paper Table 2)

MobileNetV2 用 bottleneck(倒置残差块) 做堆叠,每个 block 有扩张系数 t、输出通道 c、重复次数 n、stride s,示例(部分):

  • conv3×3, 32, s=2
  • bottleneck t=1, c=16, n=1, s=1
  • bottleneck t=6, c=24, n=2, s=2
  • bottleneck t=6, c=32, n=3, s=2
  • ...
  • 最后 1×1 conv → 1280 → avgpool → FC
    详表见论文 Table 2(本文后面给出 PyTorch 实现)。

逐层计算举例(含卷积输出尺寸与计算量公式)

1) 空间尺寸计算(单层示例)

标准二维卷积输出尺寸公式(以 0-based padding 表述):

复制代码
out_H = floor((in_H + 2*pad - kernel_size) / stride) + 1
out_W = floor((in_W + 2*pad - kernel_size) / stride) + 1

举例:输入 224×224,第一层 conv3×3, stride=2, padding=1

  • out_H = floor((224 + 2*1 - 3)/2) + 1 = floor(224/2) + 1 = 112(等价于常用"保持 same" 的设置)
    所以空间变为 112×112

2) 参数与计算量(FLOPs / Multiply-Adds)对比:标准卷积 vs 深度可分离卷积

标准卷积 (kernel K×K, 输入通道 M, 输出通道 N, 特征图 D_F × D_F)的计算量(multiply-adds):

复制代码
cost_standard = K*K * M * N * D_F * D_F

Depthwise separable = depthwise + pointwise:

  • depthwise cost = K*K * M * D_F * D_F

  • pointwise cost = 1*1 * M * N * D_F * D_F = M * N * D_F * D_F

  • 总和:

    cost_ds = K*K * M * D_F * D_F + M * N * D_F * D_F

节省比率(cost_ds / cost_standard)

复制代码
ratio = (K^2 * M + M*N) / (K^2 * M * N) = 1/N + 1/(K^2)

对于 K=3K^2=9,当 N 很大(常见情况)时,主要项约为 1/9 ≈ 0.111,也就是约 8~9 倍 的减少(与论文结论一致)。

数值示例(手算/演示)

K=3, M=128, N=256, D_F=56(比较常见的一层尺寸):

  • 标准卷积 MAdds = 3*3*128*256*56*56 = 924,844,032(约 9.25e8)
  • Depthwise separable MAdds = 3*3*128*56*56 + 128*256*56*56 = 106,373,120(约 1.06e8)
  • 比例 ≈ 106,373,120 / 924,844,032 ≈ 0.115约 8.7× 更少计算(论文中也得出 8--9 倍的结论)。

(上面数字为逐项展开后的乘法结果,展示了深度可分离卷积带来的实际计算节省。)


MobileNet V1:关键设计点细解

  • Depthwise + Pointwise:把"滤波(spatial)"和"通道融合"分离开。滤波放到 depthwise(每通道一个 filter),channel 则由 1×1 pointwise 完成。
  • 宽度/分辨率超参(α, ρ):通过 α 缩减通道数,通过 ρ 缩减输入分辨率,从而在推断速度/模型大小/精度之间平滑权衡。论文中说明了多组 α/ρ 的实验。
  • BN + ReLU:每个 depthwise 和 pointwise 后接 BatchNorm 与 ReLU(paper 中使用 ReLU)。

MobileNet V2:核心创新

  • 倒置残差(Inverted Residual):残差连接不再连接"宽"通道,而是连接"窄(bottleneck)"通道;基本单元是先用 1×1 扩展通道(ReLU6),再 depthwise 3×3(ReLU6),最后用 1×1 线性投影回窄通道(无激活)。该结构在表达能力与内存效率之间取得平衡。
  • 线性瓶颈(Linear Bottleneck):最后投影回低维时不使用非线性(no ReLU),以避免信息在低维空间被 ReLU 非线性"毁损"。
  • ReLU6:在窄通道使用 ReLU6(对低精度设备更鲁棒)。

MobileNetV2 在效率/准确率曲线上相比 V1 有明显改进(paper 给出在 ImageNet 上 MobileNetV2 (1.0) top-1 ≈ 72.0%,参数 ≈ 3.4M,MAdds ≈ 300M 的典型点)。


PyTorch 实现(完整代码块)

说明:下面给出可直接复制运行 的简洁实现(适合教学与小改动)。在工程中也可直接使用 torchvision.models.mobilenet_v2(可加载预训练权重)。([PyTorch][1])

MobileNet V1(精简实现)

python 复制代码
# mobilenet_v1.py
import torch
import torch.nn as nn
import torch.nn.functional as F

def _make_divisible(v, divisor=8, min_value=None):
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    # make sure that round down does not go down by more than 10%
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_ch, out_ch, stride=1):
        super().__init__()
        self.depthwise = nn.Conv2d(in_ch, in_ch, kernel_size=3, stride=stride,
                                   padding=1, groups=in_ch, bias=False)
        self.bn1 = nn.BatchNorm2d(in_ch)
        self.pointwise = nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_ch)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.pointwise(x)
        x = self.bn2(x)
        x = self.relu(x)
        return x

class MobileNetV1(nn.Module):
    def __init__(self, num_classes=1000, width_mult=1.0):
        super().__init__()
        # base channel settings from paper
        base_cfg = [
            # t: (out_ch, stride)
            (32, 2),
            (64, 1),
            (128, 2),
            (128, 1),
            (256, 2),
            (256, 1),
            (512, 2),
            (512, 1), (512, 1), (512, 1), (512, 1), (512, 1),
            (1024, 2),
            (1024, 1),
        ]
        input_channel = _make_divisible(32 * width_mult)
        self.features = []
        # first conv (not depthwise)
        self.features.append(nn.Conv2d(3, input_channel, kernel_size=3, stride=2, padding=1, bias=False))
        self.features.append(nn.BatchNorm2d(input_channel))
        self.features.append(nn.ReLU(inplace=True))

        # add depthwise separable blocks
        for out_ch, stride in base_cfg[1:]:
            out_ch = _make_divisible(out_ch * width_mult)
            self.features.append(DepthwiseSeparableConv(input_channel, out_ch, stride))
            input_channel = out_ch

        self.features = nn.Sequential(*self.features)

        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Linear(input_channel, num_classes)

        # weight init
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x).view(x.size(0), -1)
        x = self.classifier(x)
        return x

MobileNet V2(关键模块 + 简洁实现)

python 复制代码
# mobilenet_v2.py
import torch
import torch.nn as nn

def _make_divisible(v, divisor=8, min_value=None):
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

class InvertedResidual(nn.Module):
    def __init__(self, inp, oup, stride, expand_ratio):
        super().__init__()
        self.stride = stride
        self.use_res_connect = (self.stride == 1 and inp == oup)
        hidden_dim = int(round(inp * expand_ratio))
        layers = []
        if expand_ratio != 1:
            # pointwise
            layers.append(nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False))
            layers.append(nn.BatchNorm2d(hidden_dim))
            layers.append(nn.ReLU6(inplace=True))
        # depthwise
        layers.append(nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False))
        layers.append(nn.BatchNorm2d(hidden_dim))
        layers.append(nn.ReLU6(inplace=True))
        # linear projection
        layers.append(nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False))
        layers.append(nn.BatchNorm2d(oup))
        self.conv = nn.Sequential(*layers)

    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)

class MobileNetV2(nn.Module):
    def __init__(self, num_classes=1000, width_mult=1.0):
        super().__init__()
        # setting as paper's table
        inverted_residual_setting = [
            # t, c, n, s
            (1, 16, 1, 1),
            (6, 24, 2, 2),
            (6, 32, 3, 2),
            (6, 64, 4, 2),
            (6, 96, 3, 1),
            (6, 160, 3, 2),
            (6, 320, 1, 1),
        ]
        input_channel = _make_divisible(32 * width_mult)
        last_channel = _make_divisible(1280 * max(1.0, width_mult))
        features = []
        # first layer
        features.append(nn.Conv2d(3, input_channel, kernel_size=3, stride=2, padding=1, bias=False))
        features.append(nn.BatchNorm2d(input_channel))
        features.append(nn.ReLU6(inplace=True))

        # bottlenecks
        for t, c, n, s in inverted_residual_setting:
            output_channel = _make_divisible(c * width_mult)
            for i in range(n):
                stride = s if i == 0 else 1
                features.append(InvertedResidual(input_channel, output_channel, stride, expand_ratio=t))
                input_channel = output_channel

        # last layers
        features.append(nn.Conv2d(input_channel, last_channel, kernel_size=1, bias=False))
        features.append(nn.BatchNorm2d(last_channel))
        features.append(nn.ReLU6(inplace=True))

        self.features = nn.Sequential(*features)
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Linear(last_channel, num_classes)

        # weight init
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x).view(x.size(0), -1)
        x = self.classifier(x)
        return x

实现注释

  • groups=in_channels 的 Conv2d 即为 depthwise 卷积(PyTorch 标准实现)。关于如何用 groups 做 depthwise,请参见 PyTorch 讨论与示例。([PyTorch Forums][2])
  • MobileNetV2 的实现与 torchvision 的实现思想一致;在工程中可以直接用 torchvision.models.mobilenet_v2(pretrained=True) 加载预训练模型并微调。([PyTorch][1])

训练与评估(示例流程 + 超参建议)

数据与预处理(以 ImageNet/或 CIFAR-10 为例)

  • ImageNet 推荐 Resize(256) -> CenterCrop(224)(或训练时 RandomResizedCrop(224)),标准 ImageNet 归一化。
  • CIFAR-10 推荐 RandomCrop(32, padding=4), RandomHorizontalFlip()、Normalize。

超参(示例)

  • Optimizer:SGD(momentum=0.9, weight_decay=1e-4)
  • LR schedule:初始 LR=0.1(ImageNet)或 0.01(小数据集),使用 StepLR 或 CosineAnnealing,训练 90 epoch(ImageNet)或 200 epoch(CIFAR-10)
  • Batch size:根据显存设定(常见 128/256 for ImageNet)
  • loss:CrossEntropyLoss
  • 预训练:对小数据集用 ImageNet 预训练权重做 fine-tune 可以大幅加速与提高最终精度(若可用)。

简单训练循环框架(伪代码)

python 复制代码
# 假设 model, train_loader, val_loader 已准备
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 x,y in train_loader:
        x,y = x.to(device), y.to(device)
        pred = model(x)
        loss = criterion(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    scheduler.step()

    # 验证
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for x,y in val_loader:
            x,y = x.to(device), y.to(device)
            pred = model(x)
            correct += (pred.argmax(1) == y).sum().item()
            total += y.size(0)
    print(f"Epoch {epoch}: val_acc = {correct/total:.4f}")

训练曲线与结果(参考)

  • 论文给出的 ImageNet 对比(paper 中的表格)显示:

    • MobileNetV1 (1.0): Top-1 ≈ 70.6% , params ≈ 4.2M , MAdds ≈ 575M
    • MobileNetV2 (1.0): Top-1 ≈ 72.0% , params ≈ 3.4M , MAdds ≈ 300M(更高效)。
  • 你的实际训练结果会根据数据增强、优化器与训练时长而变化;上面是论文在 ImageNet 上的基准值,可作为对照。


实验扩展(可以做的对比实验)

  1. Width multiplier α 的影响:α ∈ {1.4, 1.0, 0.75, 0.5, 0.35},对比参数量 / MAdds / Top-1 精度的 trade-off(论文有完整曲线)。
  2. 输入分辨率 ρ 的影响:224 vs 192 vs 160 的效果对比(影响计算量与精度)。
  3. V1 vs V2 基于相同 MAdds 的精度对比:论文显示 V2 在相同预算下通常更优。
  4. 激活函数对比:ReLU6(V2) vs ReLU(V1),在低精度量化(8-bit)场景下通常 ReLU6 更稳健。
  5. 数据增强 / AutoAugment / MixUp / CutMix:尝试这些增广能在给定模型上提升 1~3% 精度。
  6. 替换 Depthwise 的实现或优化:不同后端(e.g., cuDNN, TensorRT, TF-Lite)对 depthwise 卷积支持差异会导致实际运行时间差异,注意工程部署时的算子支持。

部署注意与工程实践

  • 虽然 depthwise separable 在理论 FLOPs 大幅减少,但实际 延迟/吞吐 受后端对 groups 支持、内存访问与并发实现影响。不同设备上需要做针对性 benchmark(TF-Lite / ONNX / TensorRT / CoreML)。
  • 若目标是极致压缩,可结合量化(8-bit)、剪枝、知识蒸馏等手段进一步减小模型与延迟。

总结

  • MobileNet 系列通过结构设计(深度可分离卷积、宽度/分辨率缩放、倒置残差与线性瓶颈)实现了在资源受限场景下优异的"精度 ↔ 计算/内存"折中。MobileNetV2 在 V1 的基础上引入了倒置残差和线性瓶颈,进一步提高了性能与效率。论文中给出的基准(ImageNet 上)可作为工程选择模型规模的参考。
相关推荐
njxiejing7 小时前
Pandas数据结构(DataFrame,字典赋值)
数据结构·人工智能·pandas
盼小辉丶7 小时前
TensorFlow深度学习实战(37)——深度学习的数学原理
人工智能·深度学习·tensorflow
GEO_YScsn7 小时前
计算机视觉 (CV) 基础:图像处理、特征提取与识别
图像处理·人工智能·计算机视觉
金井PRATHAMA7 小时前
超越模仿,探寻智能的本源:从人类认知机制到下一代自然语言处理
人工智能·自然语言处理·知识图谱
eleqi7 小时前
Python+DRVT 从外部调用 Revit:批量创建楼板
python·系统集成·revit·外部调用·drvt·自动化生产流水线
l1t7 小时前
我改写的二分法XML转CSV文件程序速度追上了张泽鹏先生的
xml·c语言·人工智能·算法·expat
roshy7 小时前
MCP(模型上下文协议)入门教程1
人工智能·大模型·agent
一碗白开水一7 小时前
【论文阅读】Far3D: Expanding the Horizon for Surround-view 3D Object Detection
论文阅读·人工智能·深度学习·算法·目标检测·计算机视觉·3d
nju_spy7 小时前
李沐深度学习论文精读(二)Transformer + GAN
人工智能·深度学习·机器学习·transformer·gan·注意力机制·南京大学