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是整个模型的核心组件,它的设计非常精巧。
我习惯把它的工作流程分成三个阶段:
- 局部特征提取 — 用标准卷积处理
- 全局特征建模 — 用Transformer处理
- 特征融合 — 把局部和全局信息合并
下面这张图展示了MobileViT Block的整体流程:
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负责看全局,两者合作,效果翻倍。
可可精品资料库