资讯动态

MobileNetV3架构详解:从SE模块到PyTorch实现

发布时间:2026/9/1 18:06:16 来源:尧图企业网站定制
简介MobileNetV3 完整 PyTorch 实现与配套资料包面向深度学习从业者和研究人员适合需要掌握轻量级网络设计、并进行模型复现或性能验证的开发者。资源包共24个文件压缩后58.72MB涵盖.pth预训练模型、.log训练日志、.py推理脚本、.jpeg测试图片以及.txt标签文件等提供从加载权重到图像分类推理的完整链路。目前已有657人浏览学习适合轻量级网络入门与进阶。包内附有MobileNetV3 large/small两种尺寸的预训练权重与对应训练日志另有FLOPs计算脚本和imagenet类别文件结合深度可分离卷积、倒残差结构、SE模块和h-swish激活等架构要点可帮助读者快速掌握模型设计思路并通过实际运行验证模型精度与计算复杂度。无论课程设计、论文复现还是工程部署都能从中获得直接可用的模型文件与脚本。1. 核心思路拆解MobileNetV3到底“改”了什么MobileNetV3这个名字做轻量模型的人应该都不陌生。它是Google在2019年提出的第三代MobileNet核心目标就一句话在算力有限的移动端和嵌入式设备上把精度和速度的平衡做到极致。我最初接触MobileNetV3是因为一个实际项目需要在树莓派上跑一个实时动物识别模型帧率不能低于15FPS精度不能低于85%。试一试直接用ResNet18推理一张图耗时接近800ms完全不行。后来换到MobileNetV3-Large同样的输入分辨率下推理时间压到180ms左右精度靠迁移学习还有提升空间这才算真正体会到这个架构的价值。很多人看MobileNetV3的论文和开源代码第一反应往往是“结构图太复杂了”“看不懂NAS搜索出来的到底是什么”。确实MobileNetV3不像VGG那样清一色堆卷积也不像ResNet那样用标准残差连接就完事它综合了当时很多前沿技巧深度可分离卷积、Squeeze-and-Excitation注意力、h-swish激活函数、NAS搜索出来的通道数配置以及不同版本在深层和浅层的差异化设计。这篇文章我就按自己的理解把整个架构从设计动机到PyTorch代码逐层拆开讲清楚。1.1 三大核心改进SE模块、h-swish与NAS搜索先来理一下MobileNetV3相对前两代做了哪些关键改动。第一是引入了Squeeze-and-ExcitationSE模块。这是2018年SeNet的经典设计通过全局平均池化获取每个通道的全局信息再用两个全连接层生成通道权重简单说就是“让网络知道哪些通道更重要”。MobileNetV3把SE模块以残差分支的形式嵌入每个Bottleneck中用很小的计算开销换来了明显的精度提升。第二是用h-swish替代swish。Swish激活函数x * sigmoid(x)效果不错但sigmoid在移动端算起来不友好。MobileNetV3提出用ReLU6来近似sigmoid得到h-swish。用hard版本不是随便凑合实测下来在量化场景下精度损失更小而且计算速度更快因为ReLU6在大多数推理框架中都有高效的底层实现。第三是NAS搜索。论文用了一个叫platform-aware NAS的方法在目标硬件平台的实际延迟约束下搜索网络的通道数和结构。搜索出来的结果直接体现在网络的stage配置里不同stage的通道数、堆叠层数以及该层是否使用SE模块、是否使用h-swish都不相同。这个配置表在论文里写得清清楚楚开源代码里的配置也和论文完全对应。1.2 为什么叫“兼容性设计”从Large到SmallMobileNetV3发布时提供了两个版本MobileNetV3-Large和MobileNetV3-Small。Large版本面向算力相对充足的设备比如旗舰手机、边缘计算盒子Small版本面向超低功耗设备比如MCU、低端IPC芯片。两者在总体结构上一致都是五个stage堆叠再加分类头差异主要体现在每层的通道数和堆叠层数。Small版本的参数量大约只有Large的三分之一在CIFAR-10这类小数据集上微调Small版本也能达到不错的精度。这里要特别提醒一点在复现或改造时不要想当然地把“Large配置里的某个层直接硬套到Small上”。两种版本的配置是各自独立搜索出来的混合使用往往会导致精度下降而且推理速度没有改善。提示不管是Large还是SmallBottleneck的核心实现完全一样区别只在配置表。这也是为什么我们写代码时要把配置表和网络结构分离后面我会详细展开。2. 基础模块解读从零开始搭建Bottleneck在写完整网络前一定要先把基础模块吃透。MobileNetV3的Bottleneck不是MobileNetV2那个简单的“1x1升维-3x3深度卷积-1x1降维”三层结构而是在中间加入了SE分支并按配置决定是否使用残差连接和h-swish。2.1 深度可分离卷积为啥能省这么多算力先回顾一下深度可分离卷积。普通3x3卷积输入通道数是C_in输出通道数是C_out卷积核尺寸是K x K那么计算量是 C_in * C_out * K * K * H * W。但当C_out和C_in都很大时这计算量是爆炸级别的。深度可分离卷积把标准卷积拆成两步先用一个3x3的卷积但每个输入通道只用一个卷积核去处理通道之间互不干扰输出通道数和输入通道数一致这一步叫depthwise convolution再用一个1x1卷积把通道数变换到C_out这一步叫pointwise convolution。计算量对比一下就很直观了。假设C_inC_out256K3HW56标准卷积256 * 256 * 9 * 56 * 56 约1.85亿次乘加深度可分离卷积256 * 9 * 56 * 56 256 * 256 * 1 * 56 * 56 约0.21亿次乘加差了大概8到9倍。MobileNet全系列都靠这个“省算力”的设计吃饭V3自然也保留了这个基本盘。2.2 SE模块的PyTorch实现细节SE模块在MobileNetV3中不是单独拿出来用的而是嵌在Bottleneck里。标准实现如下import torch import torch.nn as nn import torch.nn.functional as F class SqueezeExcitation(nn.Module): Squeeze-and-Excitation模块输入输出通道数不变。 def __init__(self, in_channels, reduced_dim): super().__init__() self.fc1 nn.Conv2d(in_channels, reduced_dim, kernel_size1) self.fc2 nn.Conv2d(reduced_dim, in_channels, kernel_size1) def forward(self, x): # Squeeze: 全局平均池化拿到每个通道的全局统计量 squeeze F.adaptive_avg_pool2d(x, 1) # Excitation: 两层全连接激活生成通道权重 excitation F.relu(self.fc1(squeeze)) excitation F.hardsigmoid(self.fc2(excitation)) # 通道权重与原始特征相乘 return x * excitation关键细节有两个。第一是reduced_dim怎么取。论文里用的是一个缩放系数通常是隐藏层神经元数量 输入通道数除以4但也有人用除以16。实际上这个值是复现时比较敏感的超参数不同任务对它的偏好不同。我在一个细粒度分类任务上做过对比通道数512时reduced_dim取128比取32高了约1.2%的准确率但推理时间多了大概4%。两者之间要找平衡不要盲目照搬。第二个细节是激活函数的选择。原始SeNet用的是sigmoid但MobileNetV3里SE模块最后一步用的是hardsigmoid。这是因为整个网络已经全面向h-swish系列靠拢了在量化到int8时hardsigmoid比sigmoid友好得多实现也简单。2.3 h-swish激活函数的实现与数值稳定性h-swish的原始定义是 x * relu6(x 3) / 6。这个写法在数值上很稳定relu6会把输入限制在0到6之间所以当x在-3到3之间时h-swish近似于x * (x3)/6当x小于-3时输出为0当x大于3时输出为x。PyTorch里实现很简单但要注意使用inplace时的陷阱class HSwish(nn.Module): def forward(self, x): return x * F.relu6(x 3.0) / 6.0如果你用的是PyTorch 1.10以上可以直接用官方的torch.nn.Hardsigmoid和torch.nn.Hardswish官方实现有更细致的梯度处理。当然在MobileNetV3里用h-swish还有一个地方容易踩坑h-swish在浅层使用不如深层收益大。论文中提到只在网络后半部分使用h-swish浅层仍然用ReLU。原因在于h-swish的计算开销在浅层大特征图上更明显而精度提升却不大。我们的代码里配置表会体现这个选择。2.4 Bottleneck完整代码把前面的模块组合起来就是MobileNetV3的Bottleneck块class Bottleneck(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride, se_ratio0.25, use_hsTrue, expansion4): super().__init__() hidden_dim in_channels * expansion self.use_residual (stride 1 and in_channels out_channels) self.use_se se_ratio is not None layers [] # 第一层1x1升维 if expansion ! 1: layers.append(nn.Conv2d(in_channels, hidden_dim, kernel_size1, biasFalse)) layers.append(nn.BatchNorm2d(hidden_dim)) layers.append(HSwish() if use_hs else nn.ReLU(inplaceTrue)) # 第二层3x3或5x5深度卷积 layers.append(nn.Conv2d(hidden_dim, hidden_dim, kernel_sizekernel_size, stridestride, paddingkernel_size // 2, groupshidden_dim, biasFalse)) layers.append(nn.BatchNorm2d(hidden_dim)) layers.append(HSwish() if use_hs else nn.ReLU(inplaceTrue)) # SE模块 if self.use_se: reduced_dim max(1, int(hidden_dim * se_ratio)) layers.append(SqueezeExcitation(hidden_dim, reduced_dim)) # 第三层1x1降维 layers.append(nn.Conv2d(hidden_dim, out_channels, kernel_size1, biasFalse)) layers.append(nn.BatchNorm2d(out_channels)) self.block nn.Sequential(*layers) def forward(self, x): if self.use_residual: return x self.block(x) return self.block(x)这个代码有几个地方需要解释清楚。第一expension参数。MobileNetV2的Bottleneck通常固定expansion6但V3中不同层配置不一样有的层甚至不需要升维expansion1表示直接跳过第一层1x1卷积。这一点在标准实现中常被忽略导致网络结构和原版不一致。第二SE模块放在深度卷积之后、降维1x1之前这和原版论文一致。SE模块对深度卷积输出的每个通道计算权重然后反馈到特征上最后再降维。放在这个位置的原因是让注意力机制作用的特征更丰富如果放在降维之后通道数变少注意力的表达能力会被削弱。第三残差连接的条件是stride1且输入输出通道数相等。注意stride2时下采样导致空间尺寸减半不能直接做元素相加所以此时只走卷积分支。3. 网络整体结构与配置表解析Bottleneck模块搞定之后搭建整个网络就水到渠成了。核心是配置表每一层用哪些参数组合。这里直接给出MobileNetV3-Large的配置表与论文Table 2对应并用代码实现。3.1 MobileNetV3-Large配置表详解以图像输入尺寸224x224为例网络结构如下输入尺寸算子扩展倍数输出通道SE激活函数步长224x224Conv2d 3x3-16否ReLU2112x112Bottleneck 3x3116否ReLU1112x112Bottleneck 3x3424否ReLU256x56Bottleneck 3x3324否ReLU156x56Bottleneck 5x5340是ReLU228x28Bottleneck 5x5340是ReLU128x28Bottleneck 5x5340是ReLU128x28Bottleneck 3x3680否h-swish214x14Bottleneck 3x32.580否h-swish114x14Bottleneck 3x32.380否h-swish114x14Bottleneck 3x32.380否h-swish114x14Bottleneck 3x36112是h-swish114x14Bottleneck 3x36112是h-swish114x14Bottleneck 5x56160是h-swish27x7Bottleneck 5x56160是h-swish17x7Bottleneck 5x56160是h-swish17x7Conv2d 1x1-960否h-swish17x7AvgPool 7x7----11x1Conv2d 1x1NBN-1280否h-swish11x1Conv2d 1x1NBN-类别数--1有没有注意到扩展倍数不是固定值这正是MobileNetV3和V2的一大区别。V2每层都固定扩展6倍V3是NAS搜索出来的不同层扩展倍数不同有的取整到整数有的保留小数。复现时这些值不能随意四舍五入否则参数量和精度都会跑偏。在分类头部分最后一层由一个1x1卷积升到1280维再接一个1x1卷积输出类别数。这种“最后的特征提升层”是借鉴了MnasNet的设计本质是用一次廉价计算增强特征表达能力。3.2 整体网络代码配置表驱动把配置表直接写进Python用list组织class MobileNetV3(nn.Module): def __init__(self, config, num_classes1000): super().__init__() layers [] # 第一层普通卷积 layers.append(nn.Conv2d(3, 16, kernel_size3, stride2, padding1, biasFalse)) layers.append(nn.BatchNorm2d(16)) layers.append(HSwish()) # 中间Bottleneck层 in_channels 16 for c in config: kernel_size, exp, out_ch, se, use_hs, stride c layers.append(Bottleneck(in_channels, out_ch, kernel_size, stride, se_ratio0.25 if se else None, use_hsuse_hs, expansionexp)) in_channels out_ch # 最后的特征层 layers.append(nn.Conv2d(in_channels, 960, kernel_size1, biasFalse)) layers.append(nn.BatchNorm2d(960)) layers.append(HSwish()) self.features nn.Sequential(*layers) self.avgpool nn.AdaptiveAvgPool2d(1) self.classifier nn.Sequential( nn.Conv2d(960, 1280, kernel_size1), HSwish(), nn.Conv2d(1280, num_classes, kernel_size1) ) def forward(self, x): x self.features(x) x self.avgpool(x) x self.classifier(x) return x.flatten(1)Large版的config定义如下mobilev3_large_config [ # kernel_size, expansion, out_channels, use_se, use_hs, stride (3, 1, 16, False, False, 1), (3, 4, 24, False, False, 2), (3, 3, 24, False, False, 1), (5, 3, 40, True, False, 2), (5, 3, 40, True, False, 1), (5, 3, 40, True, False, 1), (3, 6, 80, False, True, 2), (3, 2.5, 80, False, True, 1), (3, 2.3, 80, False, True, 1), (3, 2.3, 80, False, True, 1), (3, 6, 112, True, True, 1), (3, 6, 112, True, True, 1), (5, 6, 160, True, True, 2), (5, 6, 160, True, True, 1), (5, 6, 160, True, True, 1), ]这里有一个细节值得单独提一下expansion为2.5或者2.3这种非整数PyTorch的Conv2d要求整数通道数所以代码里hidden_dim int(in_channels * expansion)之后还要保证不为0。实际计算时802.5200是整数但802.3184也是整数因为in_channels恰好是能被整除的数。如果不想依赖这种巧合可以在Bottleneck里加一行hidden_dim max(1, int(in_channels * expansion 0.5))四舍五入取整保证不会因为浮点数误差导致hidden_dim偏小。3.3 为什么在小数据集上需要微调stride还有一个我在实际项目中踩过的坑值得拿出来分享。原版MobileNetV3假设输入是224x224网络中间经历了几次stride2的下采样最终特征图是7x7。但很多实际任务输入只有96x96或者128x128如果直接照搬原配置最后特征图会变得特别小分类头拿到的信息太少精度明显下降。解决思路有两种。第一种是在前几层把stride改为1减少下采样次数让特征图保持合适的分辨率。第二种是输入尺寸调整到接近原版的尺度比如192或224但这个在计算资源受限时不现实。我当时做动物识别时输入是160x160就在config第一层把stride从2改成1并把后面某个stride2的层改成stride1同时对应调整输出通道。这样模型计算量会有变化但特征是足够的。改完之后在同样的验证集上准确率从78.3%提升到84.1%效果很明显。注意修改stride后需要重新初始化BN层的统计量。如果你直接从ImageNet权重迁移第一次训练前最好在训练集上跑几个warmup步让BN的running_mean和running_var更新到新分布。4. 训练技巧与迁移学习实战MobileNetV3的代码实现只是万里长征第一步真正用到项目里训练和调参才是出效果的关键。这里分享一些我实测有效的经验。4.1 数据增强与图像尺寸选择MobileNetV3作为轻量模型对数据增强比较敏感。直接套用ResNet那套标准增强RandomCropRandomFlip效果一般不错但不是最优。我实际使用的增强组合包括RandomResizedCrop、RandomHorizontalFlip、ColorJitterbrightness 0.2, contrast 0.2, saturation 0.2、RandomRotation10度以内。如果想要更强一点的泛化可以加RandAugment但要注意RandAugment在低分辨率输入下可能过度裁剪损伤细粒度特征。图像尺寸上224x224是原版标准但真实场景里要看数据集的特性。如果物体在图像中占比大160x160或者128x128就足够了如果物体小或者需要细粒度分类224是底线甚至可以用到256再resize到224相当于增加一个尺度扰动。4.2 迁移学习如何加载官方预训练权重MobileNetV3的官方权重是在ImageNet上预训练的。使用torchvision可以直接加载from torchvision.models import mobilenet_v3_large model mobilenet_v3_large(pretrainedTrue)但注意torchvision的实现和我上面写的代码在分类头结构上有细微差异。torchvision版本把最后一层改成了Linear而原版论文是1x1卷积。为了保持兼容加载预训练权重时要么用torchvision的自带结构要么手动修改。如果自己从头训练建议不要随机初始化直接开跑尤其是在数据量不大的情况下。正确的迁移流程是加载torchvision的预训练模型跑一遍自己的数据集统计一下类别数。替换分类头model.classifier[-1] nn.Linear(in_features, num_classes)冻结backbone的前几层或全部BN层只训练分类头几个epoch让分类头先稳定。解冻部分backbone用较小的学习率比如1e-4微调全部参数。4.3 优化器与学习率策略MobileNetV3在ImageNet上的训练策略是RMSProp优化器batch size 4096初始学习率0.1。但普通玩家的显存没那么大我在实际项目中用AdamW OneCycleLR效果更稳定收敛速度也更快。一个小批次训练的推荐配置from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR optimizer AdamW(model.parameters(), lr1e-3, weight_decay0.01) scheduler OneCycleLR( optimizer, max_lr5e-3, steps_per_epochlen(train_loader), epochsepochs, pct_start0.3, )OneCycleLR先用30%的轮次把学习率从低升到max_lr再逐步衰减这种学习率策略对轻量模型的收敛特别友好。浅层特征比深层特征训练得更慢如果用均匀学习率浅层会欠拟合所以也可以给backbone和classifier设置不同的学习率倍数比如classifier用完整学习率backbone用0.1倍。4.4 BatchSize与BN的坑BN层在轻量模型里是性能敏感组件。BatchSize太小比如4或8会导致BN统计量不稳定训练集和验证集准确率波动很大。如果只能用小batch最好把BN换成GroupNorm或LayerNorm或者用SyncBatchNorm多卡时。我在单卡显存只有8G的情况下用batch size 16训练MobileNetV3-Large输入224x224结果验证集上loss一直抖动。后来把输入降到192并把batch size提到32问题立刻缓解。轻量模型欠拟合和过拟合的边界很细多试几个batch size很值得。5. 常见问题与排查技巧5.1 输出NaN或Loss不下降这个问题的排查顺序建议是先检查学习率是不是过大再检查数据归一化是否错误最后检查expasion计算是否出现hidden_dim0。我在刚开始复现时遇到过一种特殊情况当config里某个层的in_channels乘以expansion后等于0Conv2d会直接报错但如果不是0而只是1那么1x1卷积实际上没起到升维作用整个网络表达能力受限Loss会一直卡在高位不动。检查方式是把每一层的输出shape打出来对照论文的Table 2逐层看。5.2 推理速度比预期慢很多人复现MobileNetV3后发现在PyTorch CPU上跑推理并没有宣传的那么快。原因一般有两个。第一是PyTorch默认的eager模式对深度卷积的支持不够高效。深度可分离卷积的depthwise层在底层会被拆分成逐通道操作在CPU上开销不小。解决方法是换用C扩展库如oneDNN或者直接转ONNX用ONNX Runtime推理速度可以提高一大截。第二是h-swish的计算。用x * relu6(x 3) / 6这个写法每次都会创建一个中间张量。改成F.hardswish(x)在推理时可能合并算子减少内存访问。torch.jit.script或torch.compile也能自动做这类融合。5.3 迁移学习时老模型效果反而更好这种情况很常见。MobileNetV3在ImageNet上表现优秀但迁移到特定小数据集时由于特征提取器的归纳偏置不如ResNet强如果没有充分微调效果可能反而不如老模型。解决手段有几种一是加长微调时间MobileNetV3需要更多epoch才能让浅层特征适配新任务二是增大数据增强的强度三是在全连接层前多加一个Dropout0.2到0.5范围加强正则化。我自己在多个数据集上试过Dropout对MobileNetV3的效果提升比其他轻量网络更明显。6. 我的实操总结与扩展方向6.1 踩过几次坑后的心得体会MobileNetV3的代码实现不算复杂难的是真正理解每一步设计背后的动机。我刚开始实现时最喜欢的过程是debug每一层输出的shape打印参数量对比论文表格。经历过了之后发现以下几个维度最值得深入研究SE模块的reduced_dim对精度和计算量的影响曲线h-swish与ReLU在浅层和深层的混合策略NAS搜索出来的配置在不同数据集上是否需要手动调整这些维度没有标准答案只有结合自己的任务实测才能选出最优组合。6.2 MobileNetV4与后续模型的趋势MobileNetV3发布后后续又出现了MobileNetV4核心思路是引入fused convolution与NAS搜索的结合进一步在移动设备上压榨性能。但MobileNetV3的架构思想和工程化实现依然是理解和部署轻量模型的最佳教材。如果你接下来要做目标检测或语义分割MobileNetV3可以作为非常好的backbone替换标准ResNet系列。比如在SSD或DeepLabV3中把backbone换成MobileNetV3-Large在保持精度基本不下降的前提下FPS往往能翻一倍。这也是我推荐每个做边缘端视觉的人把MobileNetV3吃透的原因。6.3 后续代码扩展建议最后分享两个我目前在做的小方向供你参考。第一个是量化感知训练QAT。MobileNetV3在int8量化下如果直接后量化精度损失明显因为SE模块的通道权重分布比较敏感。用torch.ao.quantization做QAT把伪量化节点插入训练过程可以让量化后的模型精度损失控制在1%以内。第二个是知识蒸馏。MobileNetV3-Small作为学生用ResNet50作为教师蒸馏温度设置为4软标签权重设为0.7。我在一个十类花分类数据集上试过学生模型的准确率从86.2%提升到89.8%接近教师模型的91.5%而推理速度是教师的五倍以上。这类轻量化技术组合在边缘端场景中远比单纯调参更有效。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价