3. MobileNetV2回顾:深度可分离卷积,倒残差结构,线性瓶颈层

好,咱们正式开始搭建MobileViT之前,我觉得有必要先回头把它的“骨架”——MobileNetV2——彻底捋一遍。你可能会问:“都2025年了,还讲V2?” 嗯,我个人的经验是,很多同学直接上手ViT,结果调参调到怀疑人生,最后发现是底层卷积的“地基”没打牢。MobileNetV2里的几个核心设计,说白了就是MobileViT能跑得又快又好的秘密武器。

今天咱们就聚焦三个关键词:深度可分离卷积、倒残差结构、线性瓶颈层。把这三点吃透,你再看MobileViT的代码,会感觉豁然开朗。

3.1 深度可分离卷积:把标准卷积“拆开”算

先聊聊最基础的。标准卷积是怎么做的?一个3x3的卷积核,同时处理输入图像的通道信息和空间信息。这就像一个人既要管财务又要管人事,效率自然不高。

深度可分离卷积的思路很简单:把通道和空间分开处理。它分两步走:

  1. 深度卷积(Depthwise Convolution):每个输入通道单独用一个卷积核去卷积。说白了,就是每个通道“各扫门前雪”。
  2. 逐点卷积(Pointwise Convolution):用1x1的卷积核,把上一步的结果在通道维度上“融合”起来。

这样做的好处是什么?我算一笔账给你看。假设输入是 H x W x C,输出也是 H x W x C,卷积核大小是 K x K。

  • 标准卷积的计算量:H x W x C x C x K x K
  • 深度可分离卷积的计算量:H x W x C x K x K(深度卷积) + H x W x C x C(逐点卷积)

两者一除,你会发现计算量大约是标准卷积的 1/K² + 1/C。当 K=3,C 很大时,计算量直接降到原来的 九分之一左右。

核心结论:深度可分离卷积用极小的精度损失,换来了巨大的速度提升。这是MobileNet系列能在移动端跑起来的根本原因。

我在项目中遇到过一个问题:用深度可分离卷积替换标准卷积后,模型收敛变慢了。后来发现是BatchNorm的参数初始化没调好。嗯,这里要注意,替换后BN层的统计量会变化,建议适当调大学习率或者用warmup。

3.2 倒残差结构:先升维,再卷积,后降维

接下来是MobileNetV2最标志性的设计——倒残差结构。你想想看,ResNet的残差结构是“压缩-卷积-扩张”,像个沙漏。而MobileNetV2反其道而行之,是“扩张-卷积-压缩”,像个倒置的沙漏。

为什么这么设计?

我个人习惯这样理解:深度可分离卷积在低维空间里提取特征的能力有限。如果输入通道数很少(比如24维),你再用3x3深度卷积去提特征,信息量不够,效果会很差。所以,MobileNetV2先用一个1x1卷积把通道数扩张(通常是6倍),在高维空间里做深度卷积,最后再用1x1卷积把通道数压缩回去。

结构示意图如下:

输入 (低维) 1x1 扩张 (高维) 3x3 深度卷积 (高维) 1x1 压缩 输出 残差连接(当输入输出维度相同时)

避坑指南:我曾经在实现倒残差结构时,忘记在最后一个1x1卷积后加激活函数。结果模型训练时loss死活降不下去。后来才意识到,这就是下面要讲的线性瓶颈层——压缩后的低维空间不能再加ReLU,否则信息会丢失。

3.3 线性瓶颈层:低维空间不要ReLU

这是MobileNetV2里最容易被忽视,但也是最精妙的设计。为什么叫“线性瓶颈”?说白了,就是在瓶颈层(低维特征)后面,去掉ReLU激活函数,只保留线性变换。

为什么会这样?

我打个比方。ReLU会把负数全部置为0,这相当于一个“信息过滤器”。在高维空间里,信息冗余多,过滤掉一些没关系。但在低维空间里,每个通道的信息都很宝贵,ReLU一过滤,可能就把关键特征给滤没了。

里用了一个很直观的实验:把一个二维数据通过随机矩阵映射到高维空间,加ReLU,再映射回二维。你会发现,当映射到的维度足够高时,信息能保留下来;但维度低时,数据会严重变形,甚至变成一片空白。

所以,MobileNetV2的倒残差结构中,最后一个1x1卷积(降维层)后面没有ReLU,直接输出。这就是线性瓶颈层。

组件 输入通道 中间通道(扩张后) 输出通道 激活函数
1x1 扩张卷积 C_in C_in × expansion_factor - ReLU6
3x3 深度卷积 C_in × expansion_factor - - ReLU6
1x1 线性瓶颈 C_in × expansion_factor - C_out 无(线性)

重要提醒:当步长(stride)为2时,输入和输出的空间尺寸不同,此时没有残差连接。只有步长为1且输入输出通道相同时,才使用残差连接。这个细节我在第一次复现时踩过坑,特此强调。

3.4 代码实现:一个完整的倒残差块

光说不练假把式。我写一个PyTorch版本的倒残差块,你感受一下:

import torch
import torch.nn as nn

class InvertedResidual(nn.Module):
    def __init__(self, in_channels, out_channels, stride, expand_ratio):
        super().__init__()
        hidden_dim = in_channels * expand_ratio
        self.use_res_connect = (stride == 1 and in_channels == out_channels)

        layers = []
        # 1x1 扩张
        if expand_ratio != 1:
            layers.append(nn.Conv2d(in_channels, hidden_dim, 1, bias=False))
            layers.append(nn.BatchNorm2d(hidden_dim))
            layers.append(nn.ReLU6(inplace=True))
        
        # 3x3 深度卷积
        layers.extend([
            nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, 
                      groups=hidden_dim, bias=False),
            nn.BatchNorm2d(hidden_dim),
            nn.ReLU6(inplace=True),
            
            # 1x1 线性瓶颈(无激活函数)
            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_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)

你看,代码其实很简洁。关键点就两个:

  • groups=hidden_dim 实现了深度卷积
  • 最后一个卷积后面没有ReLU,这就是线性瓶颈层

我个人习惯把 expand_ratio 设为6,这是里验证过的最优值。当然,如果你在资源更受限的设备上跑,可以试试4或者3,效果也还行。

3.5 小结

好了,MobileNetV2的三个核心设计咱们都过了一遍。总结一下:

  • 深度可分离卷积:把标准卷积拆成深度卷积+逐点卷积,计算量降到原来的1/9左右
  • 倒残差结构:先升维再降维,让深度卷积在高维空间里提取特征,效果更好
  • 线性瓶颈层:低维空间去掉ReLU,保护信息不丢失

这三个设计环环相扣,缺一不可。你想想看,如果没有线性瓶颈层,倒残差结构的效果会大打折扣;如果没有深度可分离卷积,整个网络的计算量会爆炸。这就是MobileNetV2的精妙之处。

下一节,咱们就要把这些知识用到MobileViT里了。准备好了吗?

可可精品资料库