
简介面向深度学习从业者与研究人员MobileNetV3 完整 PyTorch 实现资源包聚焦轻量级网络在计算机视觉中的应用涵盖模型搭建、预训练权重、训练日志、推理测试与效率评估等环节适合用于学习 MobileNetV3 架构细节并快速开展图像分类实验。资源包为 zip 格式共 24 个文件、约 58.72MB包含 4 个 .pth 预训练模型文件、4 个 .log 训练过程日志、3 个 .py 脚本、3 张 .jpeg 测试图片、2 个 .txt 文本文件以及若干项目配置与说明文件。其中 mobilenetv3.py 与 run_pred_mobilenet.py 分别负责网络定义和推理入口flops.py 可辅助计算模型计算量imagenet_classes.txt 提供分类标签日志文件记录 300/450 轮训练曲线便于分析训练效果。资源附带猫、狗、鸟等真实测试图片下载后即可加载权重体验分类推理。目前已有 657 人浏览学习对于希望深入理解轻量级网络实现、需要可直接运行的基准代码的读者来说具备较高的参考价值。1. 从MobileNetV1到V3轻量网络的进化逻辑1.1 深度可分离卷积V1打的底子要真正理解MobileNetV3不能跳过它的前两代。2017年MobileNetV1提出时核心就一句话把标准卷积拆成两步走。标准卷积在提取特征时既要考虑通道间的关系又要考虑空间位置的关系计算量是 H×W×C_in×C_out×K×K。而深度可分离卷积把这个过程拆成了深度卷积Depthwise Convolution和逐点卷积Pointwise Convolution深度卷积只在每个通道内部做K×K的卷积核滑动逐点卷积再用1×1卷积把通道信息融合起来。直观感受一下标准卷积96个单位算完的事拆开后只需要十几个单位就能完成计算量大约下降到原来的八分之一到九分之一具体取决于卷积核大小和通道数。MobileNetV1用两个超参数控制网络的胖瘦和输入分辨率宽度乘子α和分辨率乘子ρ。α负责把每层的通道数按比例缩放典型值0.25、0.5、0.75、1.0ρ负责缩放输入图像尺寸。这套组合拳让MobileNetV1可以按照不同硬件算力快速裁剪出合适的子网络从高算力GPU一路覆盖到嵌入式MCU。1.2 V2的线性瓶颈与倒残差V2的改进点从MobileNetV1的实测结果中来深度卷积本身不改变通道数如果通道很窄经过ReLU激活后特征信息会被大量冲掉。研究者在实验中发现低维特征图经过ReLU之后很多通道直接变成了死区信息不可恢复。于是V2引入了倒残差结构Inverted Residual Block先通过1×1卷积把低维特征升维到高维一般是6倍在高维空间做深度卷积提取特征再通过线性变换降维回低维这个降维操作不再接ReLU而是纯线性映射。这就是著名的Linear Bottleneck。为什么升维因为ReLU在高维空间造成的特征损失相对小信息不容易被破坏。为什么最后不加ReLU因为降维后特征已经很紧凑了再经过非线性激活可能破坏已经提取好的信息。V2的瓶颈结构从视觉上看是一个两头小、中间大的纺锤形而传统的残差块是个两头大、中间小的沙漏形所以叫倒残差。这个结构直接奠定了V3的基本骨架V3的几个核心组件全都是围绕这个骨架做优化。1.3 V3做了什么NAS搜索、NetAdapt与手工微调V3的诞生背后是神经网络架构搜索NAS的成熟。V3的论文名字里就带着“Searching for MobileNetV3”它的block配置不是人手工拍脑袋排的而是通过NAS自动搜索出来的。具体分两个阶段先用平台感知的NAS在计算量约束下搜索网络的整体结构每个block用多少个通道、多少层、用不用SE模块再用NetAdapt算法对搜索出来的结构精细化调整每层的卷积核数量。最后作者在V2的代码框架上做了几处人工干预重点就是两个引入h-swish激活函数和SE通道注意力模块。需要强调一点V3并不是纯粹让NAS黑箱跑出来的作者做了大量统计分析比如发现搜索出来的网络在网络的浅层普遍倾向使用传统卷积而不是深度卷积因为浅层往往只有几个通道深度卷积收益不明显比如SE模块放在某些靠后的层收益更大。这些人工微调和分析才是V3在精度和速度上能够同时全面超越V2的关键。所以读V3时千万别只背配置表要理解每一层为什么放在那里这样你自己写代码、改结构的时候才有依据。2. MobileNetV3的核心组件逐模块拆解2.1 h-swish激活函数低开销版的SwishSwish激活函数是Google量子团队在2017年提出的公式是 x·sigmoid(x)。实测发现Swish在深层模型上精度优于ReLU但sigmoid的计算开销在移动端不可接受——它涉及指数、除法和加法等浮点运算在手机上跑要比ReLU贵一大截。MobileNetV3的解决办法是“硬”版本用ReLU6来近似sigmoid公式如下[ h\text{-}swish(x) x \cdot \frac{ReLU6(x3)}{6} ]因为ReLU6本身只是clip操作运算只有一次加法和一次乘法比sigmoid便宜得多。实测下来h-swish和原版Swish的精度差距在零点几个百分点以内但速度优势明显在骁龙系列等移动处理器上可以节省约20%的激活层耗时。作者的测试结果显示h-swish放在网络后半段效果更好放在浅层反而可能产生负面影响——这也是为什么V3只在网络的第7层开始才使用h-swish浅层仍然保持ReLU。这点在后续实现中要特别注意不是全网络无脑换激活函数。在PyTorch里h-swish实现起来非常简洁而且PyTorch的ReLU6本身有CUDA上的高效实现逐行代码如下class HSwish(nn.Module): h-swish激活函数: x * relu6(x3) / 6 相比原版swish, 用relu6近似sigmoid, 避免指数运算, 移动端友好 def __init__(self, inplace: bool True): super(HSwish, self).__init__() self.inplace inplace def forward(self, x: torch.Tensor) - torch.Tensor: return x * F.relu6(x 3.0, inplaceself.inplace) / 6.02.2 SE注意力模块让网络学会“挑重点”SE模块是SENet提出的通道注意力机制做法不复杂对特征图的每个通道做全局平均池化得到一个长度为C的向量然后经过两个全连接层第一层降维通常降到C的1/4第二层升维回C经过sigmoid得到一个0到1之间的逐通道权重最后把这个权重乘回原特征图。直觉上可以理解为网络自己判断每个通道的重要程度有用的通道放大没用的通道压小。在轻量网络中加SE作者一开始是有顾虑的——两个全连接层虽然参数量不大但运算开销是实打实的。特别是SE模块通常放在Bneck块内部会打断原有的流水线。不过V3的实验结果表明在合适的层加入SE之后精度提升明显大于那点速度损失。另外还有一个细节SE模块在V3中使用的降维比例不是统一的1/4作者在部分层用了1/8这部分在代码里要根据配置灵活处理。标准SE实现如下class SqueezeExcitation(nn.Module): SE通道注意力模块 参数: in_channels: 输入通道数 se_ratio: 隐藏层降维比例, 默认0.25 def __init__(self, in_channels: int, se_ratio: float 0.25): super().__init__() hidden_channels max(1, int(in_channels * se_ratio)) # 全局平均池化: 聚合空间信息 self.pool nn.AdaptiveAvgPool2d(1) # 两个1x1卷积等价于两个全连接层, 1x1卷积可以保持特征图的空间维度, 效率更高 self.fc1 nn.Conv2d(in_channels, hidden_channels, kernel_size1, biasTrue) self.fc2 nn.Conv2d(hidden_channels, in_channels, kernel_size1, biasTrue) def forward(self, x: torch.Tensor) - torch.Tensor: b, c, _, _ x.size() # 空间压缩 - 全连接降维 - ReLU - 全连接升维 - sigmoid加权 out self.pool(x).view(b, c, 1, 1) out self.fc1(out) out F.relu(out, inplaceTrue) out self.fc2(out) out F.sigmoid(out) # 权重范围[0,1] return x * out.expand_as(x)2.3 Bneck瓶颈残差块的完整结构Bneck是MobileNetV3的基本构成单元它把前面提到的所有组件缝合在一起。一个完整Bneck块的结构从输入到输出依次是1×1扩展卷积只有当扩展比例大于1时才存在把低维输入升到expanded_channel维接BN和激活函数3×3或5×5深度卷积接BN和激活函数步长可为1或2步长为2时用于下采样SE模块按配置决定是否加在深度卷积之后、逐点卷积之前插入1×1逐点卷积把高维特征压回输出通道数接BN不加激活函数残差连接步长为1且输入输出通道相等时把原始输入和输出相加。为什么要按这个顺序排列深度卷积之后紧接SE是因为SE是在空间特征已经充分提取之后重新校准通道权重效果最好。如果把SE放到扩展卷积之后输入特征被放大到高维空间SE需要处理的通道数多但空间信息还没经过深度卷积提取注意力校准的意义不大。V2的经验告诉我们在逐点卷积之后不加激活函数所以Bneck的最后一层保持线性输出。代码实现如下class Bneck(nn.Module): MobileNetV3的瓶颈残差块 参数: in_channels: 输入通道数 out_channels: 输出通道数 kernel_size: 深度卷积核大小, 3或5 stride: 深度卷积步长, 1或2 expand_ratio: 扩展比例, 决定了中间升维后的通道数 se_ratio: SE模块降维比例, 0表示不添加SE act_layer: 激活函数类型, relu或hs表示h-swish use_hs: 是否使用h-swish激活 def __init__(self, in_channels, out_channels, kernel_size, stride, expand_ratio, se_ratio0.25, use_hsFalse): super().__init__() self.stride stride hidden_channels in_channels * expand_ratio self.use_shortcut (stride 1 and in_channels out_channels) # 当expand_ratio为1时, 跳过扩展卷积, 直接进入深度卷积 if expand_ratio ! 1: self.conv_expand nn.Conv2d(in_channels, hidden_channels, kernel_size1, biasFalse) self.bn_expand nn.BatchNorm2d(hidden_channels) # 深度卷积 self.conv_depthwise nn.Conv2d(hidden_channels, hidden_channels, kernel_sizekernel_size, stridestride, paddingkernel_size // 2, groupshidden_channels, biasFalse) self.bn_depthwise nn.BatchNorm2d(hidden_channels) # SE模块, 当se_ratio不为None时启用 self.se None if se_ratio is not None and se_ratio 0: self.se SqueezeExcitation(hidden_channels, se_ratio) # 逐点压缩卷积 self.conv_project nn.Conv2d(hidden_channels, out_channels, kernel_size1, biasFalse) self.bn_project nn.BatchNorm2d(out_channels) # 激活函数选择 self.act_layer HSwish() if use_hs else nn.ReLU(inplaceTrue) def forward(self, x): identity x if hasattr(self, conv_expand): x self.bn_expand(self.conv_expand(x)) x self.act_layer(x) x self.bn_depthwise(self.conv_depthwise(x)) x self.act_layer(x) if self.se is not None: x self.se(x) x self.bn_project(self.conv_project(x)) if self.use_shortcut: x x identity return x3. PyTorch代码实现与逐行解析3.1 完整网络模型实现MobileNetV3在论文中给出了Large和Small两个版本配置分别面向高算力和低算力场景。我把配置文件直接用Python列表形式写出来每组参数对应一个Bneck块。相比用字典定义再解析的方式直接用列表更直观可读性更好也方便逐个层检查。配置格式定义为(扩展比例, 输出通道数, 注意力配置, 深度卷积核大小, 步长)用se0.25表示SE的降维比率None表示不添加SE激活函数单独用列表控制。网络前面必须加一个标准的3×3卷积层做初始下采样stride2中间所有下采样都通过Bneck块内部的stride2深度卷积完成最后接全局平均池化和分类头。核心配置和主干代码def _make_divisible(v, divisor8, min_valueNone): 将通道数向上取整到divisor的倍数, 保证硬件友好性 移动端处理器对8的倍数通道数有更好的内存对齐效率 if min_value is None: min_value divisor new_v max(min_value, int(v divisor / 2) // divisor * divisor) # 确保向下取整后不低于原值的90%, 防止通道数被过度缩减 if new_v 0.9 * v: new_v divisor return new_v class MobileNetV3(nn.Module): MobileNetV3网络主干 参数: cfg: 配置块列表, 见下方Large配置定义 num_classes: 分类类别数 width_mult: 全局宽度乘子, 用于通道数缩放 def __init__(self, cfg, num_classes1000, width_mult1.0): super().__init__() # 初始卷积层, 输入3通道 - 16通道, stride2下采样 in_channels _make_divisible(16 * width_mult) self.conv_stem nn.Conv2d(3, in_channels, kernel_size3, stride2, padding1, biasFalse) self.bn_stem nn.BatchNorm2d(in_channels) self.act_stem HSwish() # 根据配置逐层堆叠Bneck块 layers [] for layer_cfg in cfg: expand_ratio, out_channels, se_ratio, kernel_size, stride layer_cfg out_channels _make_divisible(out_channels * width_mult) layers.append(Bneck( in_channelsin_channels, out_channelsout_channels, kernel_sizekernel_size, stridestride, expand_ratioexpand_ratio, se_ratiose_ratio, use_hs(in_channels 40) # 论文: h-swish仅用于通道数40的层 )) in_channels out_channels self.blocks nn.Sequential(*layers) # 最后一层扩展卷积, 提升特征维度再分类 last_conv_out _make_divisible(960 * width_mult) self.last_conv nn.Conv2d(in_channels, last_conv_out, kernel_size1, biasFalse) self.last_conv_bn nn.BatchNorm2d(last_conv_out) self.last_conv_act HSwish() self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.2), nn.Linear(last_conv_out, num_classes), ) self._initialize_weights() def forward(self, x): x self.act_stem(self.bn_stem(self.conv_stem(x))) x self.blocks(x) x self.last_conv_act(self.last_conv_bn(self.last_conv(x))) x self.classifier(x) return x def _initialize_weights(self): 轻量网络对初始化敏感, 采用官方推荐的初始化策略 for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.zeros_(m.bias)3.2 Large与Small配置表论文中的配置表我这里直接保留原始数据但需要注意的是第一行16表示的是stem卷积的输出通道不是标准Bneck配置在代码里已经单独处理了权重初始化了。原始配置中的注意力模块列我用se0.25形式表示方便代码解析。def mobilenetv3_large(num_classes1000, width_mult1.0): MobileNetV3-Large, 适合算力相对充裕的场景, 比如旗舰手机或Jetson系列 cfg [ # expand_ratio, out_channels, se_ratio, kernel_size, stride (1, 16, None, 3, 1), (4, 24, None, 3, 2), (3, 24, None, 3, 1), (3, 40, se0.25, 5, 2), (3, 40, se0.25, 5, 1), (3, 40, se0.25, 5, 1), (6, 80, None, 3, 2), (2.5, 80, None, 3, 1), (2.3, 80, None, 3, 1), (2.3, 80, None, 3, 1), (6, 112, se0.25, 3, 1), (6, 112, se0.25, 3, 1), (6, 160, se0.25, 5, 2), # 注意: 最后的stride2下采样, 全局池化前的小分辨率 (6, 160, se0.25, 5, 1), (6, 160, se0.25, 5, 1), ] return MobileNetV3(cfg, num_classes, width_mult)有一个坑要提醒原始论文配置表中最后一层Bneck的stride是2意味着在网络很深的位置还有一个下采样这样到分类层时特征图大小为7×7输入224。印象中很多GitHub开源实现把这一层的stride改为1因为大部分分类任务在7×7全局池化和在14×14全局池化上的精度差异极小而后者参数更少。如果要和论文数据对齐建议保留stride2。def mobilenetv3_small(num_classes1000, width_mult1.0): MobileNetV3-Small, 适合低算力场景, 如MCU或低端嵌入式设备 cfg [ (1, 16, se0.25, 3, 2), (4.5, 24, None, 3, 2), (3.67, 24, None, 3, 1), (4, 40, se0.25, 5, 2), (6, 40, se0.25, 5, 1), (6, 40, se0.25, 5, 1), (3, 48, se0.25, 5, 1), (3, 48, se0.25, 5, 1), (6, 96, se0.25, 5, 2), (6, 96, se0.25, 5, 1), (6, 96, se0.25, 5, 1), ] return MobileNetV3(cfg, num_classes, width_mult)3.3 前向传播细节与参数量统计搭建完成后最好立刻做一次参数验证确认模型结构合理。用下面的代码可以快速算出参数量和理论FLOPs并打印每一层的输出张量尺寸方便排查维度问题import torch from torchinfo import summary model mobilenetv3_large(num_classes1000) model.eval() summary(model, input_size(1, 3, 224, 224))MobileNetV3-Large在width_mult1.0时参数量大约548万5.48MFLOPs约为2.2G。可以看到相比于ResNet50的25.5M参数和4.1G FLOPs计算量压缩得非常明显。实际测试中如果只跑分类且不要求Top-5精度到99%的级别这类轻量模型完全可以在树莓派4上实时运行。宽度乘子width_mult是调参利器想进一步压缩模型时可以直接把width_mult设为0.75或0.5。实测在不少小型分类任务上0.5倍宽度的V3-Small仍然有可用的精度参数量直接降到130万以下非常适合在STM32MP1之类的主控上跑。4. 训练、迁移学习与部署经验4.1 训练配置与迁移学习要点MNV3的PyTorch实现拿来即用但训练时有不少细节容易踩坑。官方在ImageNet上训练时采用RMSProp优化器初始学习率0.1权重衰减1e-5但在自己的小数据集上直接用AdamW配合余弦退火学习率更省心。如果你要迁移到自己的分类数据集建议加载PyTorch官方预训练权重然后把分类头替换成自己任务的类别数只微调分类头训练几个epoch等loss收敛后再解冻全部层用更小的学习率1e-4左右微调整个网络。import torch.nn as nn from torchvision.models import mobilenet_v3_large # 加载官方预训练权重 model mobilenet_v3_large(weightsMobileNet_V3_Large_Weights.DEFAULT) # 替换分类头为自己的任务, 例如生物分类5类 num_classes 5 model.classifier[3] nn.Linear(model.classifier[3].in_features, num_classes) # 冻结特征提取层, 先只训练分类器 for param in model.features.parameters(): param.requires_grad False # 只更新分类头和新增层 optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)有一个值得留意的现象MobileNetV3的BN层在迁移学习时默认统计的是当前batch的均值和方差不会受冻结影响。但如果冻结了特征层还套用BN的预训练running_mean实际batch size又特别小反而容易导致训练不稳定。建议冻结阶段把BN层的track_running_stats保持原样不做额外修改因为PyTorch在requires_gradFalse时仍然维护BN统计量不会引发问题。4.2 不同输入分辨率下SE模块的性能表现SE模块的性能跟输入分辨率强相关。在224×224输入下SE的全局平均池化把空间信息压缩到1×1各通道的注意力权重实际上是整张图的全局统计。当分辨率降低到96×96甚至64×64时SE在高分辨率下学到的通道重要性分布可能不再适用——因为空间信息变少通道间的语义差异也变化了。我的实测经验是如果训练和推理分辨率不一致建议重新微调SE模块所在的层单独设置较大的学习率。另一个有效做法是直接在低分辨率训练整个网络因为MNV3在低分辨率下训练收敛速度明显更快而精度损失在多数任务中只有1-3个百分点。5. 踩坑记录与调试技巧实录5.1 常见问题速查表下面是我在复现MobileNetV3过程中实际遇到并解决过的问题整理成表格方便对照排查问题现象可能原因解决方案训练时loss不下降分类头初始化不当分类头用正态分布(mean0, std0.01)初始化别直接用默认的均匀分布验证精度比训练低很多Dropout比例在超大模型上不合适低数据量任务把Dropout从0.2降到0.1或0.05推理速度比MobileNetV2还慢h-swish和SE在CPU上未做算子融合部署时改用C LibTorch或ONNX RuntimePyTorch动态图在移动端有额外开销显存占用比预期高BN层在推理模式下仍保留训练缓冲区model.eval()后调用torch.no_grad()并检查是否有model.train()残留微调loss不收敛学习率设置过高预训练权重微调时初始学习率不超过1e-4全局微调用1e-4~5e-4不同batch size下精度波动大BN对batch size敏感小batch训练时把BN默认的momentum从0.1减小到0.015.2 我最想提醒的几个细节第一_make_divisible函数一定不要省。MNV3配置表里的通道数看起来都是整齐的数字但在width_mult不是1的时候直接取整会导致硬件利用率下降。比如width_mult0.5时40×0.520而20不是8的倍数在有些边缘设备的NEON指令集中会产生额外的padding计算。加上这个约束函数模型可能多0.1%的参数量但推理速度能提升3-5%。第二MNV3的SE模块放在深度卷积之后和逐点卷积之前这个顺序非常讲究。踩过一次坑把SE放到了逐点卷积之后精度掉了一个多点参数量完全相同。原因在2.3小节说过逐点卷积是特征压缩过程压缩后的通道是“浓缩信息”此时再按通道加权就晚了。第三如果你要在自己的数据集上做蒸馏或者对比实验建议直接用PyTorch官方torchvision提供的版本因为它和timm等第三方实现的性能基本一致但接口更规范、依赖更少。而Google的官方老仓库对应的是TensorFlow实现两者的BN策略和dropout位置有细微差别。6. 模型扩展思路与个人实测心得MobileNetV3的基础分类版本是很好的特征提取骨干实际项目里我更常用它来做目标检测的Backbone。把classifier和last_conv层去掉把blocks输出的特征图接到YOLO或SSD的检测头上速度和精度综合表现都很可靠。SSD-Lite的检测头配合MNV3-Large作为骨干在Jetson Nano上跑640×640输入能到30FPS左右这个组合在轻量检测场景里非常经典。另一个思路是替换classifier为多层感知机做多标签分类。MNV3最后的特征图是7×7×160Large版全局池化后是160维向量可以在上面堆两个全连接层160→64→类别数效果比直接单层分类头好一点开销可忽略。最后分享一个我个人的经验MobileNetV3在ImageNet的Top-1精度比MobileNetV2高大约3-4个百分点官方数据是75.2% vs 72.0%均不加额外增强但在一些小型数据集上优势并没有这么大差距往往只有1-2个点。原因很简单小数据集本身类别和样本量有限注意力模块能学到的通道关系不够丰富。所以如果你的任务数据量特别小优先级也许不是直接上MNV3而是先考虑数据增强和正则化。反过来如果数据量大且类别多MNV3的SE模块优势就体现出来了。用不用SE、用多少层的SE可以结合你实际硬件的时间和功耗预算通过消融实验来决策。本文还有配套的精品资源点击获取