☰
MobileNetV3架构详解:从SE模块到PyTorch实现
2026/9/28 9:23:50 网站建设 项目流程

简介: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-Excitation(SE)模块。这是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到Small

MobileNetV3发布时提供了两个版本:MobileNetV3-Large和MobileNetV3-Small。

Large版本面向算力相对充足的设备,比如旗舰手机、边缘计算盒子;Small版本面向超低功耗设备,比如MCU、低端IPC芯片。两者在总体结构上一致,都是五个stage堆叠再加分类头,差异主要体现在每层的通道数和堆叠层数。Small版本的参数量大约只有Large的三分之一,在CIFAR-10这类小数据集上微调,Small版本也能达到不错的精度。

这里要特别提醒一点:在复现或改造时,不要想当然地把“Large配置里的某个层直接硬套到Small上”。两种版本的配置是各自独立搜索出来的,混合使用往往会导致精度下降而且推理速度没有改善。

提示:不管是Large还是Small,Bottleneck的核心实现完全一样,区别只在配置表。这也是为什么我们写代码时要把配置表和网络结构分离,后面我会详细展开。

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_in=C_out=256,K=3,H=W=56:

  • 标准卷积: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_size=1) self.fc2 = nn.Conv2d(reduced_dim, in_channels, kernel_size=1) 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 * (x+3)/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_ratio=0.25, use_hs=True, expansion=4): 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_size=1, bias=False)) layers.append(nn.BatchNorm2d(hidden_dim)) layers.append(HSwish() if use_hs else nn.ReLU(inplace=True)) # 第二层:3x3或5x5深度卷积 layers.append(nn.Conv2d(hidden_dim, hidden_dim, kernel_size=kernel_size, stride=stride, padding=kernel_size // 2, groups=hidden_dim, bias=False)) layers.append(nn.BatchNorm2d(hidden_dim)) layers.append(HSwish() if use_hs else nn.ReLU(inplace=True)) # 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_size=1, bias=False)) 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通常固定expansion=6,但V3中不同层配置不一样,有的层甚至不需要升维,expansion=1表示直接跳过第一层1x1卷积。这一点在标准实现中常被忽略,导致网络结构和原版不一致。

第二,SE模块放在深度卷积之后、降维1x1之前,这和原版论文一致。SE模块对深度卷积输出的每个通道计算权重,然后反馈到特征上,最后再降维。放在这个位置的原因是让注意力机制作用的特征更丰富,如果放在降维之后,通道数变少,注意力的表达能力会被削弱。

第三,残差连接的条件是stride=1且输入输出通道数相等。注意stride=2时,下采样导致空间尺寸减半,不能直接做元素相加,所以此时只走卷积分支。

3. 网络整体结构与配置表解析

Bottleneck模块搞定之后,搭建整个网络就水到渠成了。核心是配置表:每一层用哪些参数组合。这里直接给出MobileNetV3-Large的配置表(与论文Table 2对应),并用代码实现。

3.1 MobileNetV3-Large配置表详解

以图像输入尺寸224x224为例,网络结构如下:

输入尺寸算子扩展倍数输出通道SE激活函数步长
224x224Conv2d 3x3-16否ReLU2
112x112Bottleneck 3x3116否ReLU1
112x112Bottleneck 3x3424否ReLU2
56x56Bottleneck 3x3324否ReLU1
56x56Bottleneck 5x5340是ReLU2
28x28Bottleneck 5x5340是ReLU1
28x28Bottleneck 5x5340是ReLU1
28x28Bottleneck 3x3680否h-swish2
14x14Bottleneck 3x32.580否h-swish1
14x14Bottleneck 3x32.380否h-swish1
14x14Bottleneck 3x32.380否h-swish1
14x14Bottleneck 3x36112是h-swish1
14x14Bottleneck 3x36112是h-swish1
14x14Bottleneck 5x56160是h-swish2
7x7Bottleneck 5x56160是h-swish1
7x7Bottleneck 5x56160是h-swish1
7x7Conv2d 1x1-960否h-swish1
7x7AvgPool 7x7----1
1x1Conv2d 1x1(NBN)-1280否h-swish1
1x1Conv2d 1x1(NBN)-类别数--1

有没有注意到扩展倍数不是固定值?这正是MobileNetV3和V2的一大区别。V2每层都固定扩展6倍,V3是NAS搜索出来的,不同层扩展倍数不同,有的取整到整数,有的保留小数。复现时这些值不能随意四舍五入,否则参数量和精度都会跑偏。

在分类头部分,最后一层由一个1x1卷积升到1280维,再接一个1x1卷积输出类别数。这种“最后的特征提升层”是借鉴了MnasNet的设计,本质是用一次廉价计算增强特征表达能力。

3.2 整体网络代码:配置表驱动

把配置表直接写进Python,用list组织:

class MobileNetV3(nn.Module): def __init__(self, config, num_classes=1000): super().__init__() layers = [] # 第一层:普通卷积 layers.append(nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1, bias=False)) 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_ratio=0.25 if se else None, use_hs=use_hs, expansion=exp)) in_channels = out_ch # 最后的特征层 layers.append(nn.Conv2d(in_channels, 960, kernel_size=1, bias=False)) 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_size=1), HSwish(), nn.Conv2d(1280, num_classes, kernel_size=1) ) 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.5=200是整数,但802.3=184也是整数,因为in_channels恰好是能被整除的数。如果不想依赖这种巧合,可以在Bottleneck里加一行:

hidden_dim = max(1, int(in_channels * expansion + 0.5))

四舍五入取整,保证不会因为浮点数误差导致hidden_dim偏小。

3.3 为什么在小数据集上需要微调stride

还有一个我在实际项目中踩过的坑,值得拿出来分享。原版MobileNetV3假设输入是224x224,网络中间经历了几次stride=2的下采样,最终特征图是7x7。但很多实际任务输入只有96x96或者128x128,如果直接照搬原配置,最后特征图会变得特别小,分类头拿到的信息太少,精度明显下降。

解决思路有两种。第一种是在前几层把stride改为1,减少下采样次数,让特征图保持合适的分辨率。第二种是输入尺寸调整到接近原版的尺度,比如192或224,但这个在计算资源受限时不现实。

我当时做动物识别时输入是160x160,就在config第一层把stride从2改成1,并把后面某个stride=2的层改成stride=1,同时对应调整输出通道。这样模型计算量会有变化,但特征是足够的。改完之后在同样的验证集上,准确率从78.3%提升到84.1%,效果很明显。

注意:修改stride后,需要重新初始化BN层的统计量。如果你直接从ImageNet权重迁移,第一次训练前最好在训练集上跑几个warmup步,让BN的running_mean和running_var更新到新分布。

4. 训练技巧与迁移学习实战

MobileNetV3的代码实现只是万里长征第一步,真正用到项目里,训练和调参才是出效果的关键。这里分享一些我实测有效的经验。

4.1 数据增强与图像尺寸选择

MobileNetV3作为轻量模型,对数据增强比较敏感。直接套用ResNet那套标准增强(RandomCrop+RandomFlip),效果一般不错但不是最优。

我实际使用的增强组合包括:RandomResizedCrop、RandomHorizontalFlip、ColorJitter(brightness 0.2, contrast 0.2, saturation 0.2)、RandomRotation(10度以内)。如果想要更强一点的泛化,可以加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(pretrained=True)

但注意torchvision的实现和我上面写的代码在分类头结构上有细微差异。torchvision版本把最后一层改成了Linear,而原版论文是1x1卷积。为了保持兼容,加载预训练权重时要么用torchvision的自带结构,要么手动修改。

如果自己从头训练,建议不要随机初始化直接开跑,尤其是在数据量不大的情况下。正确的迁移流程是:

  1. 加载torchvision的预训练模型,跑一遍自己的数据集统计一下类别数。
  2. 替换分类头:model.classifier[-1] = nn.Linear(in_features, num_classes)
  3. 冻结backbone的前几层(或全部BN层),只训练分类头几个epoch,让分类头先稳定。
  4. 解冻部分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(), lr=1e-3, weight_decay=0.01) scheduler = OneCycleLR( optimizer, max_lr=5e-3, steps_per_epoch=len(train_loader), epochs=epochs, pct_start=0.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_dim=0。

我在刚开始复现时遇到过一种特殊情况:当config里某个层的in_channels乘以expansion后等于0,Conv2d会直接报错,但如果不是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才能让浅层特征适配新任务;二是增大数据增强的强度;三是在全连接层前多加一个Dropout(0.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%,而推理速度是教师的五倍以上。这类轻量化技术组合,在边缘端场景中远比单纯调参更有效。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询