先说我为什么盯上这篇论文。最近在折腾移动端模型部署手头需要把图像分类模型塞到一台低配嵌入式设备上跑。一开始走的是先训大模型再压缩的路线按倍率剪枝、量化折腾一圈精度掉得厉害速度提升也不理想。后来换个思路从网络结构设计上直接控制模型体积才认真地、一页一页地把这篇MobileNets啃完。圈子里提起轻量级网络这篇几乎绕不开——它用深度可分离卷积替换标准卷积配合两个可调超参数把模型参数量和计算量压到极低同时精度保持在可用水平。这篇论文标题直译过来就是《MobileNets面向移动视觉应用的高效卷积神经网络》适合谁看想把手头模型部署到手机、嵌入式设备、MCU这类环境的同学或者做模型轻量化但还没有系统读过论文的工程师都值得花一下午认真读一遍。1. 这篇论文到底解决了什么问题1.1 移动端部署的三个死结2017年前后图像深度学习领域的路子基本上是越深越准——VGG动辄上亿参数ResNet往几百层走。这套思路在云端GPU上没问题但放到手机、摄像头、可穿戴设备上就全卡住了。卡住的原因主要有三个。第一是算力。移动端的CPU、GPU、NPU跟服务器端差着好几个数量级云端一次标准3x3卷积的耗时在高端GPU上是几毫秒放在手机上可能就变成几十毫秒甚至几百毫秒而且这还只是单层卷积。第二是内存。一个VGG16模型权重文件就要五百多兆光把权重加载进内存就已经快把移动应用的内存预算吃光了更不要提推理过程中还要保存每层的中间特征图内存占用会进一步放大。第三是功耗。模型计算量大的时候设备发热严重、电池掉电快用户根本没法接受。这三个问题不是靠堆硬件能解决的因为移动设备的芯片面积、散热条件、电池容量都有限制。所以正确的思路只有一个从模型本身下手设计出天生就小、天生就快的网络结构。1.2 为什么不能靠直接缩小模型解决有人可能会说既然模型太大把每层通道数调小一些或者把层数剪掉几层不就行了吗听起来挺合理实际效果却很糟糕。直接缩小宽度本质上还是在用标准卷积做空间和通道的联合计算。通道数一旦被压缩卷积在空间维度上提取特征的能力、在通道维度上组合信息的能力会同时被削弱。你削的只是一个数字但损失的是两方面的表达能力精度掉得非常难看。更麻烦的是标准卷积的计算量公式里输入通道数、输出通道数、卷积核大小、特征图尺寸是相乘关系。你就算把通道数减半计算量也只是降到原来的四分之一左右还是远不够用。所以单纯缩小不是一个好办法。MobileNets的思路不一样它保留网络的深度和表达的丰富性但改变最基本的计算单元把空间特征提取和跨通道组合两种操作彻底分开分别用更小的代价去做。这两个操作一旦解耦整个模型从设计层面就比标准卷积少了一个数量级的计算量这才是它真正解决的问题。1.3 适合谁读、读的时候关注什么如果你在做服务端AIGPU管够那MobileNets不是你的第一选择它的精度跟同期的大模型相比还是有差距。但如果你跟我一样日常工作要跟嵌入式设备打交道——手机端图像分类、实时视频流分析、智能门锁、工业质检终端这些场景——这篇论文就是必读。读的时候建议重点盯三件事深度可分离卷积的计算量推导、两个超参数的调节规律、论文里的网络结构配置表。把这三样吃透你就能在自己的项目里直接套用结构也能看懂后面一系列轻量网络的变体是怎么设计出来的。2. 核心思想深度可分离卷积2.1 先看标准卷积到底消耗在哪里任何一个写过卷积层的人都清楚卷积的大致运作方式但很多人写完之后从来没仔细算过计算量到底烧在哪个环节。假设输入特征图尺寸是D_f x D_f通道数是M输出通道数是N卷积核大小是D_k x D_k。标准卷积对每个输出位置都要做D_k x D_k x M次乘法整张输出特征图有D_f x D_f x N个位置所以总计算量可以用这个式子表达标准卷积计算量 D_k × D_k × M × N × D_f × D_f注意这个式子里几个因子是连乘关系。M个输入通道全参与N个输出通道全输出中间没有解耦。通道数一旦上到几百上千计算量直接乘积爆炸。这也是为什么标准卷积在移动端这么贵——它不是某一个环节贵而是在所有维度上都在翻倍。2.2 深度可分离卷积的两步操作深度可分离卷积把上面这一大坨乘积操作拆成了两个环节论文原文的描述是A depthwise separable convolution is made up of two layers: depthwise convolutions and pointwise convolutions.翻译过来就是深度可分离卷积由两层组成深度卷积和逐点卷积。第一步是深度卷积。它对输入特征的每一个通道单独做一个D_k x D_k的空间卷积通道之间不互相融合这一步负责的是空间特征提取。打个比方每个通道请了一个专属的空间过滤器各管各的事。第二步是逐点卷积。用1x1卷积核把M个通道的信息两两组合输出为N个通道这一步负责的是跨通道信息交换。用1x1卷积做跨通道融合几乎不引入额外的空间计算。算一下总账。深度卷积的计算量是D_k x D_k x M x D_f x D_f逐点卷积的计算量是M x N x D_f x D_f两项相加就是深度可分离卷积的总计算量。生活化类比是这样的标准卷积像全科医生一个人同时看空间结构和跨通道信息能力很强但收费高深度可分离卷积像专科医生团队一个人只负责一个通道的空间处理另一个人专门把各通道信息汇总。团队人数多了但每个成员做的事简单很多总开销反而少得多。2.3 省了多少一个公式算清楚把两种计算量做一个比率结论非常干净总计算量比率 (D_k × D_k × M × D_f × D_f M × N × D_f × D_f) / (D_k × D_k × M × N × D_f × D_f) 1/N 1/D_k²这个公式是整篇论文最值得记住的部分。它说明计算量省多少取决于输出通道数N和卷积核大小D_k。在常见的3x3卷积场景里1/D_k²等于1/9而N在神经网络里通常远大于9像是输出128、256通道所以1/N那一项几乎可以忽略。整体计算量大概只剩标准卷积的九分之一左右。我自己复现时用PyTorch搭了一个MobileNet风格的block然后把深度可分离卷积换成普通标准卷积保持输入输出完全一致FLOPs差距就在8到9倍之间。这个差距不是某一个参数凑出来的而是结构上天然决定的。所以哪怕你换了数据集、换了输入分辨率它省计算量的核心优势也不会消失。有一点要说清楚深度可分离卷积不是完全没有代价。它把一层卷积拆成了两个卷积层kernel发起的次数变多了内存访问频率也更高。这在后面的实操章节我会再展开讲这也是很多人看完论文之后以为FLOPs降九倍速度就快九倍结果实测完全不是那么回事的原因。3. 两个关键超参数怎么调3.1 宽度乘子把通道数整体打折MobileNets提出两个超参数第一个叫宽度乘子α取值一般在0到1之间论文里给出的常用档位是1.0、0.75、0.5、0.25。加了α之后每层的输入通道数从M变成α×M输出通道数也从N变成α×N。逐点卷积的计算量变成αM × αN深度卷积变成αM整体计算量大概会变成原来的α²倍。注意这里是α的平方不是α本身。这意味着把α从1.0降到0.5计算量不是减半而是变成原来的四分之一左右。论文里的实验数据也能印证MobileNet-224在α1.0时top-1精度大约70.6%α0.5时降到了63.7%α0.25时只有50.6%。看这个趋势α每缩小一档精度掉的幅度相对平缓但计算量是大台阶式下降。所以α0.5是很多嵌入式项目里最常用的档位性价比最好。3.2 分辨率乘子把输入图整体缩小第二个超参数叫分辨率乘子ρ范围同样是0到1。它通过控制输入图像分辨率等比缩小每一层特征图的空间尺寸。比如输入从224×224变成192×192ρ就是192/224约等于0.857。因为空间尺寸是二维的计算量的变化会跟着变成ρ²。也就是说输入分辨率缩小20%计算量不是降20%而是降36%左右。这跟宽度乘子的道理类似都是平方级别的杠杆。实际项目里分辨率乘子对精度的影响和任务类型有很强的关系。图像分类这类任务224缩到192对精度影响通常不大但如果是目标检测或者语义分割这类对细节敏感的任务分辨率缩太多会导致小目标直接消失。我亲自做过检测项目检测任务的输入分辨率一般要320到416起步ρ不能低于0.7再往下漏检率就很难看了。3.3 两个超参数怎么搭配更合理α和ρ是独立可调的参数理论上可以自由组合。但实操中我不建议两个都往低了拉那样精度会双倍受损。我的做法是先固定α0.5跑一版看精度是否达标。如果精度够用就尝试把ρ从1.0降到0.857再测一遍。如果精度不够就反过来把α升到0.75重新测试。这种先调α、后调ρ的顺序比两个参数一起乱试好控制得多。为什么先调α而不是先调ρ因为α对参数量的影响更大参数量直接决定模型文件大小和内存占用ρ只影响计算量对模型文件大小没有贡献。如果你的设备存储紧张优先保住α如果目标是降低单次推理时延优先调ρ。这个决策逻辑想清楚了后面选参就不会纠结。4. 网络结构和训练细节4.1 卷积块顺序Conv BN ReLUMobileNets的基本卷积块顺序是卷积 - 批归一化 - ReLU在深度卷积和逐点卷积后面都各接一组这样的操作。这里有个细节值得注意论文里用的是ReLU6而不是普通ReLU。ReLU6把激活值截断在6以内看起来只是增加了一点非线性限制但在低精度推理场景中非常关键——激活值范围受限后量化时不容易出现极端的数值分布定点实现也更容易。这个设计在当年的论文里不算显眼后来做量化部署时才体会到它的价值。BN层放在卷积和激活之间现在大家都觉得这是理所当然的了但放在2017年很多模型还没这么规整。BN的作用是稳定训练、加速收敛而且在推理阶段可以合并到卷积参数里这也是MobileNets部署时很省事的原因之一。部署时记得做BN合并别在推理流程里傻傻地保留BN算子否则会白白增加计算和内存开销。4.2 论文里的完整结构表论文给出的MobileNet结构可以这样概括第一层是普通的3x3标准卷积后面每一层基本都是3x3深度卷积 1x1逐点卷积的组合最后一层接全局平均池化再通过全连接输出分类结果。整个模型由1层标准卷积加上若干个深度可分离卷积模块组成每个模块包含深度卷积和逐点卷积各一个。在α1.0时模型参数量约420万计算量约5.69亿次乘加操作。对比VGG16参数量约1.38亿、计算量约155亿次差距是两个数量级的。我复现的时候严格按照论文结构表逐层搭搭完跟标准实现对齐输出参数数量确实是四百二十万上下跟论文报告一致。放到今天来看这个规模也算小模型但在2017年这属于非常激进的轻量设计。4.3 训练技巧和蒸馏的作用论文提到训练时使用RMSProp优化器、异步梯度下降并且使用了与标准大模型相同的数据增强和正则化策略。这里有一个值得单独拿出来说的技巧是蒸馏。所谓蒸馏是用一个已经训练好的大模型在目标数据集上生成软标签再拿这些软标签去训练MobileNets。你没有看错先训练一个大模型然后把大模型的知识教给小模型。蒸馏对轻量网络的收益相当明显论文里把MobileNet-224 α1.0的top-1精度从70.6%提到了75.1%左右。我自己在做移动端分类模型时也试过蒸馏确实比直接在小型模型上硬训高出两三个点。尤其是在训练数据不那么充足的情况下软标签提供的监督信号更平滑小模型不容易过拟合。如果你要复现MobileNets的效果强烈建议把蒸馏纳入训练流程这是论文里一颗容易被忽略的彩蛋。5. 实验效果复盘5.1 图像分类数据集上的表现论文把MobileNet-224在α1.0时放到了大型图像分类数据集上测试top-1精度大约70.6%加上蒸馏之后可以到75.1%。跟VGG16相比它用大约三十二分之一的参数量、二十七分之一的计算量拿到了可用的精度。这个精度放到今天肯定不算高但关键要放进2017年的移动端硬件环境里去评价。当时在手机上能跑起来的快速模型精度普遍比这个水平要低不少。MobileNets的意义在于它第一次系统性地证明了模型可以又小又快又准这件事是可以同时成立的。从工程角度看还有一个点值得关注MobileNets的精度下降曲线非常平滑。从α1.0到α0.25随着计算量从高到低精度是缓慢递降的没有出现断崖式崩溃。这种梯度式的性能-精度权衡对实际项目选型非常友好你可以按精度预算反推该选哪个档位的模型。5.2 目标检测和细粒度识别里的用法MobileNets不只是用来做图像分类的论文里还做了把它作为骨干网络接入目标检测框架的实验验证了它在实时检测场景下的潜力。结论是MobileNets可以在保持接近的mAP水平的同时把整体计算量压到很低。后续大量工程实践证明很多嵌入式检测方案直接使用MobileNet作为特征提取器配合轻量检测头跑起来。细粒度识别任务也是一样MobileNets在细粒度分类上的表现跟当时较大的模型相比精度差距在可接受范围内但计算量少得多。这里真正想说的是MobileNets作为骨干网络的潜力意义可能比它自己单独做分类的表现更重要。它是第一个把轻量骨干证明得这么充分的模型后面很多移动端网络都是在这个框架下做改进。5.3 论文的消融实验为什么值得看论文对两个超参数做了非常完整的消融实验用一组一组的数据验证了宽度乘子对计算量的二次方影响、分辨率乘子对计算量的二次方影响并给出了不同超参数组合下的精度对标。很多论文的消融实验做得敷衍参数随便改两档就完事了。MobileNets在这方面非常扎实每个档位都有对应的精度、计算量和参数量数据这也是后来很多轻量工作把它作为baseline的原因。你如果想快速评估一个新轻量结构是否值得用建议也跑一个类似的消融把α和ρ作为变量做网格搜索绘制精度对计算量的曲线。有了这条曲线你拍板选择任何中间档位都有数据支撑而不是靠感觉。6. 实操复现的细节与避坑6.1 PyTorch里实现深度可分离卷积深度可分离卷积在PyTorch里写起来很直接核心就是用groups参数让普通卷积退化为深度卷积import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1): super().__init__() self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingkernel_size // 2, groupsin_channels, biasFalse ) self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0, biasFalse ) def forward(self, x): x self.depthwise(x) x self.pointwise(x) return x两个注意点第一groups参数必须等于in_channels而且in_channels必须能被groups整除否则直接报错或者结果错乱。第二bias要设成False因为后续接BN层BN自带偏置可以吸收卷积偏置保留bias只会引入冗余参数。6.2 TensorFlow里的等价写法TensorFlow / Keras里提供了更语义化的DepthwiseConv2D接口from tensorflow.keras import layers x layers.DepthwiseConv2D( kernel_size3, strides1, paddingsame, use_biasFalse )(x) x layers.Conv2D( filtersout_channels, kernel_size1, use_biasFalse )(x)逻辑一样先深度卷积提取空间特征再1x1逐点卷积做通道融合。TensorFlow里同样要注意use_biasFalse同时关注通道维度的排列方式channels_first和channels_last会影响卷积算子的实际行为。6.3 复现过程中最常踩的深坑第一深度卷积层之后一定要接BN和激活不能省。省掉BN训练会非常不稳定尤其在模型本身比较小的情况下梯度噪声会被放大精度容易飘。第二FLOPs降了九倍不代表推理速度就快九倍。我实测在GPU上跑深度可分离卷积的kernel发起次数更多、内存访问更频繁实际提速往往只有两到四倍。但在CPU和嵌入式处理器上效果会好很多因为在那种硬件上计算单元弱省下的乘法量是实打实的主导因素。所以选型的时候不能只看FLOPs要结合自己的目标硬件实测决定。第三量化部署时ReLU6的数值截断上限一定要设置好。如果使用普通ReLU激活值可能分布在很大的范围内量化会引入较大误差ReLU6把数值限制在0到6之间整体分布更紧凑量化误差小得多。如果你改用了ReLU部署时量化参数需要重新校准不能沿用ReLU6的配置。6.4 一个高效的最小验证方法当你接到一个移动端项目不确定某个网络在目标设备上到底跑多快最快的办法不是读论文也不是数FLOPs而是把网络导出成ONNX再用目标环境的运行时跑一次真实推理。我自己从MobileNets的复现中学到的教训是FLOPs只是理论指标实测延迟才是唯一真理。建议你至少准备一张真实的输入图设好batch_size1测三十次取平均看吞吐和时延再做选型决定。不做这一步你以为的快和目标设备上的快很可能是两回事。还有一个小技巧复现这类论文时不要直接抄结构代码。先从计算量公式自己推一遍再动手实现理解深度完全不一样。推公式的过程会让你明白为什么深度可分离卷积能把计算量压下来也能让你在遇到效果不如预期时更快定位是结构问题还是训练问题。这篇论文值得反复读每次读侧重不同收获也不同。
阅读完成 · 觉得有帮助?