4. MobileViT核心思想:CNN局部建模 + Transformer全局建模

各位同学,今天我们来聊聊MobileViT最核心的设计思路。说白了,它就是把CNN和Transformer的优点给揉在一起了。

你可能会问:CNN不是挺好的吗?干嘛非要加Transformer?

嗯,这个问题我刚开始接触MobileViT时也想过。CNN擅长捕捉局部特征,比如纹理、边缘这些细节。但它的感受野有限,想看到全局信息就得堆很多层。Transformer呢?自注意力机制一上来就能看到全局,但计算量太大,不适合移动端。

MobileViT的做法很聪明——让CNN负责局部,让Transformer负责全局。两者互补,效果出奇的好。

核心思想一句话总结:用CNN提取局部特征,用Transformer建模全局关系,最后再融合回去。

4.1 为什么需要这种结合?

我在做目标检测项目时遇到过这样一个问题:用纯CNN模型检测小物体,经常漏检。后来分析发现,小物体在图像中占的像素少,CNN的局部卷积很难捕捉到它的上下文信息。

举个例子,你要检测一个远处的交通标志。只看它周围的几个像素,你根本不知道那是个标志。但如果你能看到它旁边有马路、有路灯杆,那判断起来就容易多了。

这就是全局信息的重要性。

但Transformer也不是万能的。它虽然能看到全局,但对局部细节的建模能力不如CNN。而且你想想看,一张512x512的图片,直接做自注意力计算,那复杂度是O(n²),根本跑不动。

所以MobileViT的解决方案是:先局部,再全局,最后融合。

4.2 MobileViT Block结构详解

好,我们来看具体的结构。MobileViT Block是整个模型的核心组件,它的设计非常精巧。

我习惯把它的工作流程分成三个阶段:

  1. 局部特征提取 — 用标准卷积处理
  2. 全局特征建模 — 用Transformer处理
  3. 特征融合 — 把局部和全局信息合并

下面这张图展示了MobileViT Block的整体流程:

MobileViT Block 结构图 输入特征图 3x3 卷积 (局部特征) Transformer (全局特征) BN + SiLU 激活 1x1 卷积 (调整通道) Unfold (展成序列) Transformer × N 层 Fold (还原为特征图) 特征拼接 + 融合 输出特征图

4.3 局部特征提取阶段

首先,输入特征图会经过一个3x3的卷积层。这一步的作用是提取局部特征,比如边缘、纹理、角点这些。

我个人习惯把这个阶段叫做"打底"。因为后续的Transformer需要处理的是序列数据,而卷积层输出的特征图质量,直接决定了Transformer能学到什么。

小技巧:这里的3x3卷积后面通常会接BatchNorm和SiLU激活函数。SiLU相比ReLU,在深层网络中梯度流动更顺畅,训练更稳定。

4.4 全局特征建模阶段

这是MobileViT最巧妙的地方。它把特征图转换成序列,然后用Transformer处理。

具体怎么做呢?

  • Unfold操作:把特征图切成一个个patch,每个patch展平成一个
  • Transformer编码:对这些做自注意力计算,捕捉全局关系
  • Fold操作:把处理后的还原回特征图的形状

你想想看,这样做的好处是什么?

每个patch内部的信息由CNN负责提取,patch之间的全局关系由Transformer负责建模。两者各司其职,互不干扰。

注意:Transformer的层数不能太多。MobileViT通常只用2-4层Transformer。层数多了,计算量上去了,而且在小数据集上容易过拟合。我曾经试过堆8层,结果在VOC数据集上反而掉点了。

4.5 特征融合阶段

最后一步,把局部特征和全局特征拼接起来,再经过一个1x1卷积融合。

这里有个细节:拼接是在通道维度上做的。也就是说,局部特征有C个通道,全局特征也有C个通道,拼接后变成2C个通道。然后1x1卷积把通道数压缩回C。

这样做的好处是,模型可以学习到局部和全局信息的最佳组合方式。有些位置可能局部信息更重要,有些位置全局信息更重要,让网络自己学去吧。

4.6 代码实现要点

下面我给出MobileViT Block的核心代码框架。注意,这不是完整实现,而是让你理解关键步骤:

class MobileViTBlock(nn.Module):
    def __init__(self, in_channels, transformer_dim, num_heads, num_layers):
        super().__init__()
        
        # 局部特征提取
        self.local_conv = nn.Conv2d(in_channels, in_channels, 3, padding=1)
        self.bn = nn.BatchNorm2d(in_channels)
        self.act = nn.SiLU()
        
        # 调整通道,准备进入Transformer
        self.conv1x1_in = nn.Conv2d(in_channels, transformer_dim, 1)
        
        # Transformer编码器
        self.transformer = nn.TransformerEncoder(
            nn.TransformerEncoderLayer(
                d_model=transformer_dim,
                nhead=num_heads,
                dim_feedforward=transformer_dim * 2
            ),
            num_layers=num_layers
        )
        
        # 还原通道
        self.conv1x1_out = nn.Conv2d(transformer_dim, in_channels, 1)
        
    def forward(self, x):
        # 局部路径
        local = self.local_conv(x)
        local = self.act(self.bn(local))
        
        # 全局路径
        global_feat = self.conv1x1_in(x)
        # Unfold: [B, C, H, W] -> [B, C, H*W] -> [H*W, B, C]
        B, C, H, W = global_feat.shape
        global_feat = global_feat.flatten(2).permute(2, 0, 1)
        global_feat = self.transformer(global_feat)
        # Fold: [H*W, B, C] -> [B, C, H, W]
        global_feat = global_feat.permute(1, 2, 0).reshape(B, C, H, W)
        global_feat = self.conv1x1_out(global_feat)
        
        # 特征融合
        out = torch.cat([local, global_feat], dim=1)
        # 这里还需要一个1x1卷积融合,省略...
        return out

关键点:代码中Unfold和Fold操作是MobileViT的核心。它把2D特征图转换成1D序列,让Transformer能够处理。这个转换过程不涉及任何可学习参数,只是张量形状的变换。

4.7 实际项目中的经验

我在用MobileViT做目标检测时,总结了几条经验:

  • patch大小很关键:一般设为2x2或4x4。patch太小,Transformer感受野不够;patch太大,局部信息丢失严重
  • Transformer维度不要太大:MobileViT-S的transformer_dim是96,MobileViT-XS是64。太大了移动端跑不动
  • 训练时先用大学习率预热:Transformer部分收敛慢,我习惯前5个epoch用warmup

嗯,以上就是MobileViT Block的核心内容。说白了就是三件事:局部卷积、全局Transformer、特征融合。理解了这个结构,你就能明白为什么MobileViT能在移动端达到不错的精度了。

一句话总结:CNN负责看细节,Transformer负责看全局,两者合作,效果翻倍。

可可精品资料库