简介:面向深度学习和计算机视觉研究者,这份 MobileNetV3 完整实现包以 PyTorch 代码为主线,配套预训练权重、训练日志和推理脚本,解决了从网络结构理解到实际部署验证的关键环节。压缩包共24个文件,体积约58.72MB,涵盖模型定义代码、4个预训练权重文件、4个训练日志、3张测试图片、ImageNet类别标签、运行环境说明及项目配置文件,类型涉及py、pth、log、jpeg、txt、xml等,目录结构清晰。资源包中不同模块分层存放,方便按需查阅。目前已有657人学习下载。借助该资源,读者可以快速搭建 MobileNetV3-Small/Large 网络,加载权重对猫、鸟、车等图片进行推理测试;通过训练日志观察不同迭代阶段的收敛情况,使用FLOPs脚本评估模型计算量;也可结合源码逐行理解bneck结构、SE模块和激活函数等关键设计,适合作为论文复现、课程设计或移动端轻量化模型的参考实现。 在端侧部署任务里,我经常被问到一个问题:模型体积卡死在10MB以内,精度还不能太难看,到底该选哪个网络。我的默认答案基本是MobileNetV3。MobileNetV3-Large以约5.4M参数量,换来ImageNet上接近75%的top-1,计算量大约在两百多M MACs,树莓派、手机、边缘盒子上都能跑得动。但能把MobileNetV3真正讲清楚的人其实不多,尤其是它比V2到底改了什么、H-Swish为什么要这样近似、SE注意力到底插在哪一层,很多人拿着torchvision现成模型调用就结束了。如果你也想做到结构可改、训练可调、部署可控,这篇就沿着PyTorch实现逐行拆一遍MobileNetV3,从架构设计动机一直讲到代码落地。
1. MobileNet三代在解决什么:理解V3之前,先理解V1和V2
1.1 深度可分离卷积和线性瓶颈,打下的两个基础
MobileNet的历史,本质上是移动端CNN不断压缩计算量的历史。V1的核心贡献是证明了一个反直觉的事:把标准卷积拆成深度可分离卷积,精度损失没有想象中大,但计算量能降一个量级。
标准卷积一层要同时做两件事:跨通道混合信息和跨空间提取特征。深度可分离卷积把这两件事拆开,先用depthwise卷积逐通道做空间采样,再用1x1 pointwise卷积把通道信息混合起来。假设输入输出都是C个通道、卷积核是3x3,标准卷积计算量是KKCinCoutHW,而深度可分离卷积是KKCinHW + CinCoutHW,整体差不多只有原来的九分之一。换个生活化的说法,标准卷积相当于一个全能员工同时干五个岗位,深度可分离卷积则是一个岗位配一个专员,每个人只做自己的事,链路短了,整体效率反而高。
V2在此基础上补了一个关键短板:V1的深度可分离结构在低维空间直接接ReLU,会丢信息。ReLU会把负值区域直接压成0,低维空间里信息本来就密,再经ReLU一截断,很多通道的信息就永久丢失了。V2的解决办法是线性瓶颈结构,在bottleneck的输出端去掉激活函数,保住低维特征的信息完整性,同时引入倒残差:先1x1扩展通道,再做depthwise卷积,最后1x1投影回低维。这个“窄-宽-窄”的结构让网络在高维空间里提取特征,又在低维空间里传信息,计算量可控,梯度流动也顺畅。
V3的大量结构延续的就是这条路线,所以如果不理解V1和V2的这两个设计,后面看bneck模块会一头雾水。
1.2 V3的“搜索+手工”组合拳:NAS、NetAdapt和最后阶段重设计
MobileNetV3核心突破不是某一个算子,而是把网络结构搜索和人工调优缝合在了一起。搜索部分用了两套工具:Platform-Aware NAS负责搜索每个block的宏观结构,比如卷积核大小、通道数、是否使用SE;NetAdapt再在已搜出的网络上做微调,逐层去试每个通道数能不能再压缩一点,直到满足延迟约束。
但搜出来的结构并不是直接可用,作者还做了一系列人工修正,其中最重要的一处是最后阶段的优化。V2的尾部有几个在7x7分辨率上做1x1高维扩展的层,计算量很大。V3把这一大块砍掉,把高维扩展挪到特征图分辨率更低的时候再做,甚至放到全局平均池化之后,用一个等价的全连接层来完成。省下来的计算量非常可观,这也是为什么MobileNetV3能在参数量不大幅增加的情况下,把延迟压下来。
理解这个背景很有用,因为很多复现代码看起来结构差不多,但最后几个层放的位置不同,数值和计算量差异就是从这里来的。
2. 核心算子拆解:H-Swish、SE和bneck的PyTorch表达
2.1 H-Swish为什么比Swish更值得在端侧用
V3另一个标志性改动是引入H-Swish激活函数。Swish函数公式是 x * sigmoid(x),效果在深层网络上确实比ReLU好,但sigmoid里有指数运算,在移动端CPU上非常不友好,误差反向传播时计算也贵。H-Swish的思路是用一个硬近似替换sigmoid:把sigmoid换成 relu6(x+3) / 6。
这个近似的妙处在于:relu6本身是一次函数和截断的组合,没有任何指数运算,计算代价极低,而且relu6在量化推理时是标准算子,INT8部署非常友好。表达式也不难写,先看代码:
class Hsigmoid(nn.Module): def __init__(self, inplace=True): super().__init__() self.relu6 = nn.ReLU6(inplace=inplace) def forward(self, x): # relu6(x + 3) / 6,近似 sigmoid(x) return self.relu6(x + 3.0) / 6.0 class Hswish(nn.Module): def __init__(self, inplace=True): super().__init__() self.sigmoid = Hsigmoid(inplace=inplace) def forward(self, x): return x * self.sigmoid(x)使用H-Swish时要注意一个细节:V3并不是所有层都用H-Swish。前面靠近输入的几层仍然用ReLU,因为作者在实验中发现H-Swish在浅层收益很小,反而增加计算和延迟。所以配置表里会有一列专门记录每个bneck用哪种激活,代码里也是按层配置的,不是全局统一替换。
PyTorch其实也内置了nn.Hardswish,实现的就是 x * relu6(x+3) / 6。自己实现Hsigmoid的好处是,在后续做量化导出时,可以更加清楚地控制relu6的图结构,避免某些部署框架对内置Hardswish算子的支持差异。
2.2 SE模块:全局平均池化加两层全连接,注意力到底加在哪
SE模块是V3里一个特别容易被忽略、又特别影响精度的组件。SE的核心思想是给每个通道算一个权重,让网络自己决定哪些通道值得保留,哪些通道可以抑制。计算流程不复杂:先对特征图做全局平均池化,把每个通道压成一个标量,得到的是全局的通道统计信息;然后经过两层1x1卷积(等价于全连接)先降维再升维;最后通过sigmoid(V3里用H-Sigmoid近似)输出0到1之间的权重,和原始特征图逐通道相乘。
class SqueezeExcitation(nn.Module): def __init__(self, in_channels, reduced_dim): super().__init__() self.fc1 = nn.Conv2d(in_channels, reduced_dim, 1) self.fc2 = nn.Conv2d(reduced_dim, in_channels, 1) self.relu = nn.ReLU(inplace=True) self.hsigmoid = Hsigmoid() def forward(self, x): # squeeze:全局平均池化,得到通道统计 y = F.adaptive_avg_pool2d(x, 1) # excitation:降维->ReLU->升维->H-Sigmoid y = self.fc1(y) y = self.relu(y) y = self.fc2(y) y = self.hsigmoid(y) return x * ySE里比较有争议的细节是降维系数。论文里使用的是reduction=4,也就是说SE内部先压缩到输入通道的四分之一,再恢复回来。但很多第三方复现为了省参数写了reduction=16,这个差异在大型数据集上会掉不少点。后文踩坑部分我会单独展开。SE在V3中也不是所有bneck都用,只有配置表里
本文还有配套的精品资源,点击获取