端侧模型部署这件事,真正上手做过的人都知道,最难的往往不是训练,而是把一个参数量看着还行的网络塞进手机里,还得让它跑得动、跑得稳、跑得不发烫。MobileNet 系列从 v1 到 v3 的演进,本质上就是一条“如何在算力、内存、精度三者之间反复找平衡”的工程路线。我这些年做过几个端侧视觉的小项目,从最早拿 v1 当基线,到后来 v2 的倒残差结构踩过通道数配置的坑,再到 v3 的 h-swish 在低端机上实测的取舍,积累了一些文档里不太会写的东西。这篇就把这条线从头捋一遍,重点放在深度可分离卷积到底省在哪、v2 的倒残差为什么这么设计、v3 的搜索与激活函数改造值不值得用,以及最后落到端侧部署时那些真正会卡住你的细节。适合刚接触轻量网络、准备把模型往端侧搬的开发者,也适合已经用过 MobileNet 但没细想过结构背后逻辑的朋友。
1. 从标准卷积的算力账说起
1.1 为什么端侧第一刀砍在卷积上
做端侧部署,第一件事永远是算账。一个普通的 3x3 标准卷积,输入特征图 H×W×C_in,输出 C_out 个通道,它的乘加次数是 H×W×C_in×C_out×3×3。这个式子里,C_in×C_out×9 是跟空间尺寸无关的固定开销,特征图稍微大一点、通道稍微宽一点,算力就爆炸。而卷积神经网络里 90% 以上的计算量都压在卷积上,所以轻量化网络的第一刀必然砍向卷积本身,这是所有轻量模型设计的共同起点。
MobileNet v1 的核心思路非常直接:把标准卷积拆成两步。第一步叫深度卷积(Depthwise Convolution),每个输入通道单独用一个卷积核去卷,通道之间不混合;第二步叫逐点卷积(Pointwise Convolution),用 1x1 卷积把通道信息融合起来。这两步合起来就是深度可分离卷积(Depthwise Separable Convolution)。听起来只是拆了一下,但算力账变化非常大。
标准卷积的计算量是 H×W×C_in×C_out×9。深度可分离卷积拆成两部分:深度卷积是 H×W×C_in×9,逐点卷积是 H×W×C_in×C_out×1。两者相加再和标准卷积一比,比值是:
$$\frac{1}{C_{out}} + \frac{1}{9}$$
当 C_out 取 256 这种常见值时,这个比值大约是 0.115,也就是算力降到原来的十分之一出头。这就是 MobileNet v1 能在同等精度下把计算量压一个数量级的根本原因。我在第一次算这个账的时候特意拿纸推了一遍,确认不是论文写错了——确实就是这么夸张,因为 1x1 卷积把通道融合的成本压到了极低。
1.2 深度可分离卷积的代价不只是精度
很多人以为深度可分离卷积的唯一代价是精度略降,其实工程上还有几个隐性成本。第一是内存访问模式变碎。深度卷积每个通道独立计算,GPU 上还好,但在一些移动端 DSP 或 NPU 上,通道独立的算子往往访存效率不如大卷积核的稠密计算,实际加速比可能达不到理论值。第二是算子融合的复杂度上升。深度卷积后面通常紧跟 BN 和激活,逐点卷积后面也是,部署时要保证这两段能被推理框架正确融合,否则中间特征图反复读写,带宽就成了瓶颈。
我踩过的一个坑是:早期在一个推理框架上直接导出 v1,结果深度卷积和逐点卷积没有被融合成一个 block,中间多了一次全特征图的写回,实测延迟比预期高了将近 40%。后来查下来是导出时算子顺序和框架的融合规则对不上,调整了 BN 的位置才解决。所以理论算力账只是起点,真正决定端侧表现的是算子融合和访存效率,这一点在后面 v2、v3 的部署里会反复出现。
1.3 v1 的两个可调旋钮:宽度与分辨率乘子
v1 论文里给了两个很实用的旋钮,我觉得这是它工程价值被低估的地方。宽度乘子 α用来整体缩放每层的通道数,分辨率乘子 ρ用来缩放输入分辨率。计算量大致按 α²×ρ² 缩放,也就是说你把宽度砍到 0.75、分辨率砍到 0.75,算力能降到原来的三成左右,精度损失通常在可接受范围内。
实际项目里我的经验是:优先调分辨率,再调宽度。因为分辨率对延迟的影响更直接(特征图面积直接变小),而宽度砍太狠会让浅层特征提取能力明显下降,尤其是小目标检测任务。有一次做一个人脸关键点的端侧模型,宽度从 1.0 砍到 0.5,关键点误差直接翻倍;但把输入从 224 降到 160,误差只涨了不到 10%,延迟却降了快一半。这个取舍顺序值得记住。
2. MobileNet v2 的倒残差:为什么先升维再降维
2.1 残差结构在轻量网络上的失效
v1 是个直筒结构,没有残差连接。到了 v2,作者想加残差来提升训练稳定性和精度,但直接套 ResNet 的残差结构会出问题。ResNet 的瓶颈结构是“先 1x1 降维、再 3x3 卷积、再 1x1 升维”,残差连接加在两端通道数相同的部分。可轻量网络本身通道就窄,再降维会把特征压得太狠,深度卷积在极低通道数下提取能力很弱。
所以 v2 反其道而行:先 1x1 升维(expansion),再做 3x3 深度卷积,最后 1x1 降维(projection),残差连接加在升维前和降维后的低维通道上。这就是所谓的倒残差(Inverted Residual)。这个设计的逻辑是:深度卷积在通道数较多时才能发挥效果,所以先把它“喂饱”,再压回去做残差相加。我第一次看这个结构时觉得有点绕,后来自己画了一遍通道变化图才明白——它其实是把“深度卷积需要足够通道”这个约束显式地放进了结构里。
2.2 扩展系数 t 的选择与踩坑
倒残差里有个关键超参叫扩展系数 t,控制 1x1 升维把通道放大多少倍,v2 默认 t=6。这个值不是随便定的,它直接决定了深度卷积的工作通道宽度。t 太小,深度卷积吃不饱,特征提取弱;t 太大,算力和内存又上去了,而且升维后的特征图在内存里占的空间会明显变大。
我在一个嵌入式板子上部署 v2 时遇到过内存峰值超标的问题,排查下来就是某个 t=6 的 block 在升维后特征图太大,加上前后 buffer 没复用,峰值内存超了。后来把那一层的 t 调到 4,精度掉了不到 0.5 个点,内存峰值降下来了。所以t 不是越大越好,要结合目标设备的内存预算来调,尤其是输入分辨率较高的时候。
还有一个细节:v2 里并不是所有 block 都用残差连接。当 stride=2 做下采样时,输入输出尺寸不一致,残差连接直接跳过不加。这个设计在部署时要注意,因为带残差和不带残差的 block 在算子融合策略上可能不同,导出时要确认框架对这两种情况都处理正确。
2.3 线性瓶颈:最后一层为什么不要激活
v2 还有一个容易被忽略但很重要的设计:倒残差的最后那个 1x1 降维卷积后面不接 ReLU,直接线性输出。原因是降维后的通道数已经很少,如果再接 ReLU,会把低维特征里的负值全部截断,信息损失很大。这个叫线性瓶颈(Linear Bottleneck)。
这个点在实际调模型时很有用。如果你自己改结构,比如把最后的降维通道数调得更小,一定要记得保持线性输出,否则精度会莫名其妙地掉。我见过有人为了省算力把最后降维后的通道压到 8,还接了 ReLU,结果模型几乎学不动,排查了半天才发现是激活函数的问题。低维空间里 ReLU 的破坏性比高维空间大得多,这是线性瓶颈想表达的核心。
3. MobileNet v3 的搜索与激活改造
3.1 网络结构搜索带来的非直觉设计
v3 最大的变化是引入了网络结构搜索(NAS)来定架构,再配合NetAdapt做层级的通道裁剪。搜索结果出来的结构有些地方看着很“反直觉”,比如某些层用了 5x5 卷积核,某些层的通道配置也不是均匀的。这些不是拍脑袋定的,是搜索在给定延迟约束下找出来的局部最优。
我的看法是:v3 的结构不要轻易手动改。因为它是搜索出来的,各层之间的通道和核大小是相互配合的,你单独改一层,很可能破坏整体的平衡。我在一个项目里试过把 v3 的某个 5x5 层换成 3x3 想省算力,结果精度掉了 2 个多点,延迟反而没降多少,因为那一层本来就不是瓶颈。所以用 v3 就老老实实用官方结构,要调就调宽度乘子和输入分辨率这两个宏观旋钮。
3.2 h-swish 的收益与低端机上的现实
v3 把 ReLU 换成了h-swish,公式是 x·ReLU6(x+3)/6。用这个近似而不是真正的 swish,是因为 swish 里的 sigmoid 在端侧计算代价高,h-swish 只用到了 ReLU6 和乘法,硬件友好。论文里说 h-swish 在深层能带来精度提升,但只在网络后半段用,前半段还是 ReLU,因为浅层特征图大,h-swish 的额外计算在这里不划算。
这里有个实测经验值得说:h-swish 在支持 ReLU6 硬件加速的芯片上确实很香,但在一些老的低端芯片上,它的实际收益没那么明显,甚至因为多了一次乘法和一次加法,延迟略有上升。我做过对比,同一台中低端手机上,v3 用 h-swish 比全 ReLU 版本精度高约 1 个点,但延迟只差 2~3ms,基本可以接受;可在一台更老的设备上,这个延迟差拉到了 8ms 以上,就得权衡了。所以h-swish 值不值得用,要看目标设备的算子支持情况,不能一概而论。
3.3 SE 模块的插入位置与代价
v3 还在部分 block 里加了SE(Squeeze-and-Excitation)模块,做通道注意力。SE 本身计算量不大,但它引入了全局池化和两次全连接,在端侧会增加一些访存和同步开销。v3 的做法是只在部分层加 SE,而且是搜索决定的。
部署时要注意,SE 模块里的全局池化在某些推理框架上可能不被融合,会单独成为一个算子,增加调度开销。如果你的目标框架对 SE 支持不好,可以考虑在导出时把 SE 折叠掉或者换成更轻的注意力,但这样会损失一点精度。我的建议是先用官方结构跑一遍,看 SE 部分的耗时占比,如果占比很小就别动它,占比大再考虑优化。
4. 端侧部署的实操流程与关键配置
4.1 从训练到导出的完整链路
端侧部署的链路大致是:训练框架里定义模型 → 训练收敛 → 导出为中间格式(如 ONNX)→ 转换到目标推理框架 → 在设备上跑 benchmark。这条链路上每一步都可能出问题,我按顺序说几个关键点。
训练阶段,BN 层的状态很关键。导出前一定要确认模型处于 eval 模式,BN 用的是滑动统计量而不是当前 batch 的统计量。我见过导出后精度暴跌的案例,查下来就是导出时模型还在 train 模式,BN 用了错误的统计量。另外,如果训练时用了混合精度,导出前最好把模型转回 FP32,避免转换工具对某些精度类型支持不好。
导出 ONNX 时,opset 版本要选对。深度可分离卷积在 ONNX 里会被拆成 DepthwiseConv 和 Conv(1x1),不同 opset 对这两个算子的支持程度不同。我一般用 opset 11 或 12,兼容性比较好。导出后一定要用 ONNX Runtime 跑一遍和原框架的数值对比,确认输出差异在 1e-4 以内,否则后面转换到设备上精度问题会更难查。
4.2 量化:端侧提速的最大杠杆
端侧部署绕不开量化。训练后量化(PTQ)是最省事的方案,把 FP32 权重和激活量化到 INT8,算力和带宽都能降一大截。但 PTQ 对 MobileNet 这种深度可分离卷积为主的网络不太友好,因为深度卷积每个通道的数值分布差异大,统一量化容易丢精度。
我的实操经验是:PTQ 一定要做逐通道量化(per-channel),尤其是权重量化。逐通道量化让每个输出通道有独立的 scale,能显著缓解深度卷积的精度损失。激活量化用逐张量(per-tensor)通常够用,但如果精度还是不够,可以考虑对激活也做更细的粒度。校准数据集要选有代表性的,一般 100~500 张就够,覆盖实际场景的分布。我试过用随机噪声做校准,结果量化后精度掉了十几个点,换成真实场景图片后只掉 1 个点以内,校准集的重要性怎么强调都不过分。
如果 PTQ 精度不达标,就得上量化感知训练(QAT)。QAT 在训练时模拟量化误差,让模型学会适应。代价是训练成本高,但精度通常能接近 FP32。MobileNet v2/v3 在 INT8 下用 QAT 一般能把精度损失控制在 1 个点以内。
4.3 算子融合与内存复用配置
推理框架的算子融合对 MobileNet 影响巨大。一个标准的 v2 block 是“1x1 卷积 + BN + ReLU6 → 深度卷积 + BN + ReLU6 → 1x1 卷积 + BN”,理想情况下这三段各自融合成一个算子。导出时要确认框架的融合规则能识别这种模式。有些框架需要你显式指定融合选项,有些则自动识别,具体看文档。
内存复用方面,端侧内存紧张,要尽量让中间特征图的 buffer 复用。大部分推理框架有内存规划功能,会自动分析生命周期做复用。但如果你发现内存峰值异常,可以手动检查是否有不必要的特征图被同时持有。我在一个内存只有几十 MB 的设备上部署时,就是靠调整 block 的执行顺序和 buffer 复用策略,把峰值内存压下来的。
5. 常见问题排查与实测数据
5.1 精度不达标的排查顺序
端侧精度不达标是最高频的问题,我总结了一个排查顺序,按这个走能覆盖大部分情况。
| 排查项 | 检查方法 | 典型原因 |
|---|---|---|
| 导出模式 | 确认 eval 模式、BN 统计量 | train 模式导出导致 BN 错误 |
| 数值对齐 | ONNX Runtime 对比原框架输出 | 算子实现差异、opset 不匹配 |
| 预处理 | 核对归一化均值方差、通道顺序 | RGB/BGR 搞反、归一化参数错 |
| 量化校准 | 检查校准集代表性、量化粒度 | 校准集分布偏离、未用逐通道 |
| 后处理 | 核对解码逻辑、阈值 | 坐标解码公式不一致 |
这个表里我特别想强调预处理这一项。端侧部署时预处理经常在设备上用另一套代码实现,很容易和训练时不一致。RGB 和 BGR 搞反是最常见的,表现是精度掉得不多但就是达不到预期。归一化的均值和方差也要逐位核对,差一点点都会影响结果。
5.2 延迟不达标的优化路径
延迟不达标时,先别急着改模型,先做逐层 profiling。大部分推理框架都能输出每层的耗时,先看瓶颈在哪。常见情况是某个深度卷积层耗时异常,可能是该层通道配置不友好,或者算子没被正确融合。
优化路径我一般按这个顺序试:先确认算子融合是否生效,再看是否能用更快的量化方案,然后考虑调低输入分辨率或宽度乘子,最后才考虑改结构。改结构是最后手段,因为会破坏原有的精度平衡。实测下来,算子融合和量化这两步往往能带来最大的延迟收益,比改结构划算得多。
5.3 几个实测对比数据
我在一台中端移动设备上做过一组对比,输入 224×224,分类任务,数据仅供参考,不同设备和框架差异会很大。
| 模型 | FP32 延迟 | INT8 延迟 | INT8 精度损失 |
|---|---|---|---|
| MobileNet v1 | 基准 | 约降 55% | 约 1.5 点 |
| MobileNet v2 | 基准 | 约降 60% | 约 1 点 |
| MobileNet v3 | 基准 | 约降 58% | 约 0.8 点 |
从这组数据能看出,v2 和 v3 在 INT8 下的精度保持比 v1 好,这跟它们的结构设计有关——倒残差和线性瓶颈让特征分布更集中,量化更友好。v1 因为直筒结构和 ReLU 的大量使用,激活分布更分散,量化损失相对大一些。这也解释了为什么现在端侧项目更倾向用 v2/v3 做基线。
最后分享一个我踩过好几次的坑:不同推理框架对同一模型的延迟表现可能差一倍以上。同一个 v2 模型,在框架 A 上跑 30ms,换到框架 B 上可能就 60ms,原因往往是算子融合策略和内存调度不同。所以选框架时一定要拿自己的模型实测,别只看 benchmark 榜单。我现在的习惯是,模型定下来后,至少在两三个候选框架上各跑一遍,选实测最优的那个,这一步花的时间绝对值得。