4. MobileViT块详解

好,咱们今天来啃一块硬骨头——MobileViT块。说实话,我第一次看这个结构的时候,脑子里就一个想法:这玩意儿怎么把CNN和Transformer揉到一起的?后来在项目里真正用上了,才明白它的精妙之处。

MobileViT块的核心思想其实很简单:用CNN提取局部特征,用Transformer捕捉全局关系,最后再把它们融合起来。说白了,就是让模型既能看到树木,又能看到森林。

4.1 局部表示:CNN的拿手好戏

我们先看局部表示这部分。MobileViT并没有抛弃CNN,而是保留了它的优势。

具体来说,输入特征图会先经过一个 3×3 的标准卷积。这个卷积的作用是什么?就是提取局部信息。你想想看,一个像素点周围的小区域里有什么纹理、边缘、颜色变化,这些细节CNN最擅长捕捉。

关键点:这个3×3卷积的步长通常是1,padding也是1,保证特征图尺寸不变。输出通道数我习惯设为输入通道数的一半,这样后续处理更灵活。

我在做移动端部署时遇到过一个问题:如果卷积核太大,计算量会爆炸。3×3是性价比最高的选择,既能捕捉局部信息,又不会太慢。

局部表示这部分,说白了就是让模型先“看局部”。就像你看一张照片,先注意到某个人的眼睛、鼻子这些细节。

4.2 全局表示:Transformer登场

接下来是重头戏——全局表示。这里用到了Transformer的编码器结构。

为什么要用Transformer?因为CNN的局限性很明显:感受野有限。你堆再多的卷积层,也很难让一个像素点看到图像另一端的像素。但Transformer的自注意力机制可以做到——它让每个位置都能关注到所有其他位置。

具体流程是这样的:

  1. 展开成序列:把特征图切成一个个小块(patch),每个小块拉平成一个向量。这就像把一张大图切成拼图碎片。
  2. 位置编码:给每个小块加上位置信息。Transformer本身没有顺序概念,所以得告诉它“这个块在左上角,那个块在右下角”。
  3. 自注意力计算:每个小块和其他所有小块做注意力计算。这一步就是全局交互的核心。
  4. 还原成特征图:把序列再拼回特征图的形状。

我的经验:在MobileViT中,Transformer的层数一般设2-4层就够了。层数太多,移动端跑不动;层数太少,全局信息又抓不够。我一般从2层开始试,效果不够再加。

嗯,这里要注意一个细节:MobileViT用的Transformer和标准的不太一样。它去掉了LayerNorm,换成了BatchNorm。为什么?因为BatchNorm在移动端推理时更快,而且可以和卷积层的BN合并,减少计算量。

4.3 融合策略:1+1 > 2

有了局部表示和全局表示,怎么把它们合在一起?这就是融合策略要解决的问题。

MobileViT的融合方式很巧妙,我画个图你就明白了:

输入特征图 3×3卷积(局部) Transformer(全局) 局部特征图 全局特征图 拼接 + 1×1卷积 输出特征图

融合的具体步骤:

  1. 通道拼接:把局部特征图和全局特征图在通道维度上拼起来。假设局部输出是 H×W×C,全局输出也是 H×W×C,拼接后就是 H×W×2C。
  2. 1×1卷积降维:用1×1卷积把通道数从 2C 压缩回 C。这一步有两个作用:一是减少计算量,二是让模型学习如何融合局部和全局信息。
  3. 残差连接:把融合后的特征图和原始输入相加。这能缓解梯度消失问题,让训练更稳定。

我曾经踩过的坑:一开始做融合时,我直接用了简单的相加(element-wise add),结果效果很差。后来发现,局部和全局特征的分布差异很大,直接相加会互相干扰。用拼接+1×1卷积让模型自己学怎么融合,效果好了很多。

4.4 完整流程:一个MobileViT块的前向过程

把上面三部分串起来,一个完整的MobileViT块长这样:

# 伪代码,展示MobileViT块的核心逻辑
def mobilevit_block(x):
    # 1. 局部表示
    local_feat = conv3x3(x)  # 3×3卷积提取局部特征
    
    # 2. 全局表示
    # 展开成序列
    patches = unfold_to_patches(local_feat, patch_size=2)
    # 加位置编码
    patches = patches + pos_embedding
    # Transformer编码
    global_feat = transformer_encoder(patches)
    # 还原成特征图
    global_feat = fold_to_feature_map(global_feat)
    
    # 3. 融合策略
    fused = torch.cat([local_feat, global_feat], dim=1)  # 拼接
    fused = conv1x1(fused)  # 降维融合
    output = fused + x  # 残差连接
    
    return output

这个流程看起来简单,但每个环节都有讲究。比如patch_size设多大?我试过2、4、8,发现2效果最好。patch太大,全局信息就变成了“半局部”,失去了Transformer的优势。

4.5 为什么MobileViT块这么设计?

你可能会问:为什么不直接用纯Transformer?或者纯CNN?

我个人的理解是:MobileViT块的设计是在效率和效果之间找平衡。

  • 纯CNN:计算快,但感受野有限,大目标容易漏掉。
  • 纯Transformer:全局关系抓得好,但计算量太大,移动端扛不住。
  • MobileViT块:CNN处理局部细节,Transformer处理全局关系,各司其职。而且Transformer只在特征图分辨率降低后才用,进一步控制了计算量。

我在一个图像分类项目里对比过:同样的参数量,MobileViT比纯CNN的准确率高了3-4个点,推理速度只慢了不到10%。这个性价比,我觉得很值。

4.6 实际部署时的注意事项

最后聊几个部署时容易踩的坑:

问题 原因 解决方案
Transformer推理慢 自注意力计算复杂度是O(n²) 减小patch数量,或者用窗口注意力
内存占用高 Transformer需要保存中间结果 用torch.jit.script优化,或者分块计算
量化后精度下降 Transformer对量化敏感 用QAT(量化感知训练)替代PTQ

我的建议:如果你要在手机上部署,先用NCNN或者TNN跑一下性能测试。MobileViT的Transformer部分在ARM CPU上可能不如预期快,这时候可以考虑用Winograd算法优化卷积部分来补偿。

好了,MobileViT块的核心内容就这些。局部表示、全局表示、融合策略,这三块搞明白了,整个MobileViT的骨架你就拿下了。