2. 模型结构拆解
好,咱们直接进入正题。上一章我们聊了MobileViT的整体设计哲学,这一章我带你拆开它的“内脏”。
MobileViT之所以能在移动端跑得又快又准,核心秘密就藏在三个模块里:MV2(MobileNetV2的倒残差块)、MobileViT Block(轻量级Transformer),以及融合模块。这三个东西怎么配合的?我画了张图,你先看个全貌。
2.1 MV2 模块:MobileNetV2 的倒残差块
MV2 说白了就是 MobileNetV2 里的那个 Inverted Residual Block。为什么叫“倒”残差?因为传统残差是先降维再升维,它反过来——先升维再降维。我个人觉得这个设计非常巧妙,它让网络在低维空间做恒等映射,计算量直接砍半。
我刚开始接触这个结构时,总觉得升维再降维多此一举。后来在项目中调试一个实时分割模型,发现同样的参数量,用了 MV2 的版本推理速度快了 30%。嗯,实践出真知。
MV2 的核心流程就三步:
- 1×1 升维卷积:把通道数放大 4~6 倍,让特征更“富裕”。
- 3×3 深度可分离卷积:每个通道单独做空间卷积,计算量只有普通卷积的 1/9。
- 1×1 降维卷积:把通道数压回原始大小,顺便做残差连接。
这里有个关键点——激活函数的选择。升维后用 ReLU6,降维后不用激活。为什么?因为低维空间用 ReLU 会丢失信息,我踩过这个坑,后来老老实实按来。
MV2 代码片段(PyTorch 风格)
class MV2Block(nn.Module):
def __init__(self, in_channels, out_channels, stride, expansion_factor=4):
super().__init__()
hidden_dim = in_channels * expansion_factor
self.use_res_connect = (stride == 1 and in_channels == out_channels)
layers = []
# 1x1 升维
if expansion_factor != 1:
layers.append(ConvNormAct(in_channels, hidden_dim, kernel_size=1, act='relu6'))
# 3x3 深度可分离
layers.append(ConvNormAct(hidden_dim, hidden_dim, kernel_size=3, stride=stride, groups=hidden_dim, act='relu6'))
# 1x1 降维(无激活)
layers.append(ConvNormAct(hidden_dim, out_channels, kernel_size=1, act=None))
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
💡 我的经验:在移动端部署时,expansion_factor 不要超过 6。我试过 8,精度没涨多少,但模型大了 40%,得不偿失。
2.2 MobileViT Block:轻量级 Transformer
这是 MobileViT 最核心的模块。它把 Transformer 的全局建模能力塞进了轻量级框架里。怎么做到的?说白了就是把图像切成小块,每个小块内部用卷积处理局部信息,小块之间用 Transformer 做全局交互。
你想想看,传统 ViT 直接把整张图拉成序列,计算量是 O(n²),一张 256×256 的图就有 65536 个 ,根本跑不动。MobileViT 的做法是:先把特征图分成 n×n 的 patch,每个 patch 内部做局部编码,然后只在 patch 之间做自注意力。这样 数量直接降到原来的 1/n²。
我记得第一次在手机上跑 MobileViT 时,看到推理延迟只有 15ms,当时真的有点激动。Transformer 终于能在移动端落地了。
MobileViT Block 的内部结构分三步:
- 局部编码:用 3×3 卷积提取每个 patch 内的局部特征。
- 全局 Transformer:把 patch 展平,送入轻量级 Transformer(只有 2~4 层,4 个头)。
- 局部融合:把 Transformer 的输出 reshape 回特征图,再用 3×3 卷积融合边界。
MobileViT Block 核心逻辑
class MobileViTBlock(nn.Module):
def __init__(self, dim, depth=2, heads=4, patch_size=2):
super().__init__()
self.patch_size = patch_size
# 局部编码
self.local_conv = ConvNormAct(dim, dim, kernel_size=3)
# 全局 Transformer
self.transformer = Transformer(dim, depth, heads)
# 局部融合
self.fusion_conv = ConvNormAct(dim * 2, dim, kernel_size=3)
def forward(self, x):
# 局部编码
local_feat = self.local_conv(x)
# 展开成 patch 序列
patches = self.unfold(local_feat) # (B, C, H/p, W/p, p*p)
# 全局 Transformer
global_feat = self.transformer(patches)
# 折叠回特征图
global_feat = self.fold(global_feat)
# 拼接局部和全局特征
out = torch.cat([x, global_feat], dim=1)
return self.fusion_conv(out)
⚠️ 避坑指南:我曾经在 patch_size=4 时发现模型精度下降明显。后来排查发现,patch 太大导致局部细节丢失。建议在移动端用 patch_size=2,平衡效果和速度。
2.3 融合模块:让局部和全局“握手”
融合模块的作用,就是把 MV2 提取的局部特征和 MobileViT Block 提取的全局特征拼接起来,再用卷积做一次深度融合。为什么需要这个模块?因为局部和全局特征的空间尺度不一样,直接相加效果不好。
我习惯用 1×1 卷积 + 3×3 深度可分离卷积 + 1×1 卷积 这个组合来做融合。它有点像一个小型的 bottleneck,计算量不大,但能让两种特征充分交互。
这里有个细节:融合模块的输入是两个分支的输出,通道数可能不同。所以第一步一定是 1×1 卷积对齐通道。我见过有人直接拼接不管通道数,结果模型训练时 loss 死活降不下去。
融合模块代码示例
class FusionBlock(nn.Module):
def __init__(self, in_channels_local, in_channels_global, out_channels):
super().__init__()
# 对齐通道
self.align_local = ConvNormAct(in_channels_local, out_channels, kernel_size=1)
self.align_global = ConvNormAct(in_channels_global, out_channels, kernel_size=1)
# 深度融合
self.fusion = nn.Sequential(
ConvNormAct(out_channels * 2, out_channels, kernel_size=1),
ConvNormAct(out_channels, out_channels, kernel_size=3, groups=out_channels),
ConvNormAct(out_channels, out_channels, kernel_size=1, act=None)
)
def forward(self, local_feat, global_feat):
local = self.align_local(local_feat)
global = self.align_global(global_feat)
concat = torch.cat([local, global], dim=1)
return self.fusion(concat)
💡 我的建议:融合模块的激活函数尽量用 ReLU6 而不是 ReLU。移动端量化时 ReLU6 更友好,精度损失小。我踩过用 ReLU 的坑,量化后精度掉了 2 个点。
2.4 三个模块如何协同工作?
整个 MobileViT 网络就是MV2 → MobileViT Block → 融合模块 的反复堆叠。每一层都在做:先提取局部细节,再捕捉全局上下文,最后把两者融合。
我举个例子:假设输入是一张 256×256 的猫图。MV2 先提取出猫耳朵、胡须这些局部纹理。MobileViT Block 则看到“哦,这些纹理组合起来是一只猫”。融合模块把这两部分信息合并,传给下一层继续处理。
这种设计的好处是:局部不丢细节,全局不失语义。纯 CNN 容易“只见树木不见森林”,纯 Transformer 又太“粗枝大叶”。MobileViT 正好取了个折中。
| 模块 | 核心作用 | 计算量占比 | 移动端优化点 |
|---|---|---|---|
| MV2 | 局部特征提取 + 下采样 | 约 40% | 深度可分离卷积、低精度量化 |
| MobileViT Block | 全局上下文建模 | 约 35% | patch 大小、Transformer 层数 |
| 融合模块 | 局部与全局特征融合 | 约 25% | 1×1 卷积、分组卷积 |
⚠️ 再提醒一句:这三个模块的通道数设计一定要匹配。我见过有人把 MV2 的输出通道设为 64,MobileViT Block 却用了 128,结果融合时通道对不上,还得加额外的对齐层,白白增加延迟。
好了,这一章我们把 MobileViT 的三个核心模块拆了个底朝天。MV2 负责局部,MobileViT Block 负责全局,融合模块负责整合——三者缺一不可。下一章我会带你实战,用代码把整个网络搭起来,到时候你就知道这些模块怎么拼在一起了。
📌 本章要点回顾
- MV2 是倒残差结构:升维 → 深度可分离 → 降维
- MobileViT Block 用 patch 方式降低 Transformer 计算量
- 融合模块用 1×1 + 3×3 depthwise + 1×1 做特征融合
- 三个模块的通道数必须对齐,否则会引入额外开销
可可精品资料库