Pytorch图像分类:06使用PyTorch搭建MobileNet模型

简介】:基于flower_data使用PyTorch搭建ResNet模型进行图片分类
【参考】:7.1 MobileNet网络详解_哔哩哔哩_bilibili
【代码完整版】:06MobileNet(github.com)

注:本人还在学习初期,此文是为了梳理自己所学整理的,有些说法是自己的理解,不一定对,如有差错,请批评指正!


一、MobileNet V1基础知识

1.背景

MobileNet 模型是 google 在 2017 年针对手机或者嵌入式设备提出轻量级模型,它的提出是为了解决传统神经网络内存需求量大、运算量大的问题。
在这里插入图片描述

2.亮点

网络中的亮点为:

  • Depthwise Convolution(深度可分离卷积的提出,大大减少了运算量和参数数量)
  • 增加超参数α、β

🚀2.1 深度可分离卷积(Depthwise Separable Conv)

深度可分离卷积就是将普通卷积拆分成为一个深度卷积和一个逐点卷积

对于传统的卷积,它的卷积核的深度(channel)就等于输入特征矩阵的深度(channel) N,可看成是N个二维卷积的叠加,因为它要保证输入特征矩阵每一个channel都被运算到;而输出特征矩阵的深度则取决于卷积核的个数。传统的卷积运算是 算一个个卷积核得到的特征矩阵

1)深度卷积(Depthwise Convolution,简称DW卷积)
在这里插入图片描述

对于DW卷积,它是对每个输入通道都单独使用一个卷积核进行卷积处理,这样它的每个卷积核的channel为1,而卷积核的个数为输入特征矩阵的channel,这就像是传统卷积的第一步——计算单个卷积核的特征图,这一步,也可以看成是将这个三维卷积核拆分成是N个二维卷积叠加来进行的运算,结果是只改变了特征图的宽高,而channel不变。
在这里插入图片描述

2)逐点卷积(Pointwise Conv,简称PW卷积)
逐点卷积卷积核大小为1的普通卷积,主要作用就是对特征图进行升维和降维。
在这里插入图片描述

  • 深度卷积:改变特征图的宽高,深度/channel不变
  • 逐点卷积:保持特征图的宽高,深度/channel改变

🤔深度可分离卷积为什么有用?它相比于普通卷积是怎么减少了参数和运算量的?
在这里插入图片描述
标准卷积的参数量:
卷积核的尺寸是 D K × D K × M D_K×D_K×M DK×DK×M,一共有N个,所以标准卷积的参数量是: D K × D K × M × N D_K×D_K×M×N DK×DK×M×N

标准卷积的计算量:
卷积核的尺寸是 D K × D K × M D_K×D_K×M DK×DK×M,一共有N个,每一个都要进行 D F × D F D_F×D_F DF×DF次运算,所以标准卷积的计算量是: D K × D K × M × N × D F × D F D_K×D_K×M×N×D_F×D_F DK×DK×M×N×DF×DF

标准卷积算完了,我们接下来计算深度可分离卷积的参数量和计算量:

深度可分离卷积的参数量:
深度可分离卷积的参数量由深度卷积和逐点卷积两部分组成:

深度卷积的卷积核尺寸 D K × D K × M D_K×D_K×M DK×DK×M;逐点卷积的卷积核尺寸为 1 × 1 × M 1×1×M 1×1×M,一共有N个,所以深度可分离卷积的参数量是: D K × D K × M + M × N D_K×D_K×M+M×N DK×DK×M+M×N

深度可分离卷积的计算量:

深度卷积的卷积核尺寸 D K × D K × M D_K×D_K×M DK×DK×M,一共要做 D F × D F D_F×D_F DF×DF次乘加运算;逐点卷积的卷积核尺寸为 1 × 1 × M 1×1×M 1×1×M,有N个,一共要做 D F × D F D_F×D_F DF×DF次乘加运算,所以深度可分离卷积的计算量是:
D K × D K × M × D F × D F + M × N × D F × D F D_K×D_K×M×D_F×D_F+M×N×D_F×D_F DK×DK×M×DF×DF+M×N×DF×DF

见上图可知,深度可分离卷积的计算量比传统卷积下降了8~9倍
更详细的内容请见:轻量级神经网络“巡礼”(二)—— MobileNet,从V1到V3 - 知乎 (zhihu.com)

2.2超参数α、β

下图表格是mobileNetv1的网络结构,表中标Conv的表示普通卷积,Conv dw代表刚刚说的DW卷积,s表示步距,根据表格信息就能很容易的搭建出mobileNet v1网络。在mobilenetv1原论文中,还提出了两个超参数,一个是α一个是β,α参数是一个倍率因子,用来调整卷积核的个数,β是控制输入网络的图像尺寸参数,下图右侧给出了使用不同α和β网络的分类准确率,计算量以及模型参数:
在这里插入图片描述

  • 上表第1行中Conv/s2表示普通卷积且步长为2,filter shape为3×3×3×32表示卷积核height=3,width=3,channel=3(rgb图片),#filters=32。
  • 第2行Conv dw/s1表示采用DW卷积操作,且步长为1。由于DW卷积的卷积核深度为1,则filter shape为3×3×32 dw表示卷积核height=3,width=3,#filters=32。其中channel=1.

通过实验结果我们可以看出,MoblieNet相比于GoogLeNet、VGG准确率只降低一点点,但计算量和参数量大幅下降;适当地调整α和β,准确率下降得不太多的同时,计算量和参数量可以下降很多。

二、MobileNet V2基础知识

1.背景

在MobileNet v1的实际训练过程中,深度卷积时卷积核特别容易废掉,即训练完成后卷积核参数是大部分为0。

在 2018 年 googleNet 提出了 v2 版本的 mobileNet,v2认为是Relu函数造成这样的原因:在输入维度是2,3时,输出和输入相比丢失了较多信息;但是在输入维度是15到30时,输出则保留了输入的较多信息。如下图所示,

image-20240114093338901

所以在使用Relu函数时,当输入的维度较低时,会丢失较多信息,因此我们这里可以想到两种思路,一是把Relu激活函数替换成别的,二是通过升维将输入的维度变高。

相比MobileNet V1网络,准确率更高,模型更小。

在这里插入图片描述

2.亮点

网络中的亮点为:

  • Inverted Residuals(倒残差结构) 通过升维将输入的维度变高
  • Linear Bottlenecks 把Relu激活函数替换成别的

🚀2.1Inverted Residuals(倒残差结构)

通过下图可以看出,左侧为ResNet中的残差结构,其结构为1x1卷积降维—>3x3卷积—>1x1卷积升维;右侧为MobileNetV2中的倒残差结构,其结构为1x1卷积升维—>3x3DW卷积—>1x1卷积降维。

二者的区别在于:

  • 一个是先降维再升维,一个是先升再降维;
  • 一个使用的是普通卷积+relu激活函数,一个使用的是DW卷积+relu6激活函数
    在这里插入图片描述
    现在看一下relu6激活函数:
    y = R e L U 6 ( x ) = m i n ( m a x ( x , 0 ) , 6 ) y = ReLU6 (x) = min (max (x, 0) , 6) y=ReLU6(x)=min(max(x,0),6)

relu6
❗❗❗注意:倒残差结构的最后两层是DW卷积+1x1的卷积,这就相当于DW+PW–> 深度可分离卷积

2.2 Linear Bottlenecks

使用倒残差结构,将最后一层的非线性激活换成线性激活

由上面的背景可知,既然是ReLU导致的信息损耗将ReLU替换成线性激活函数。但是需要注意的是,并不是将所有的Relu激活都换成了线性激活,而是将最后一个ReLU6变成线性激活函数,如下图。变换后的模块称为Linear Bottlenecks
在这里插入图片描述
stride=1输入特征矩阵与输出特征矩阵的shape相同时才有shortcut连接。

3.网络结构

下图左侧是Mobilenet V2的网络结构:
在这里插入图片描述
这里需要解释几个参数:

t:扩展因子,即上面使用卷积层的扩展倍率
c:输出特征矩阵的深度/channel
n:bottleneck的重复次数/个数
s:步距,需要注意的是,这里的s只针对第一层,其他的通通为1

结果:
在这里插入图片描述
通过性能对比实验结果可以看出,mobilenet v2的高效。

三、MobileNet V3基础知识

1.背景

MobileNet V3比MobileNet V2准确率更高,速度也更快
在这里插入图片描述

2.亮点

网络中的亮点包括:

  • 更新Block(bneck)
  • 使用NAS搜索参数(Neural Architecture Search)
  • 重新设计耗时层结构

🚀2.1更新Block(SE+h-switch)

在MobileNet(v3)中,更新了Block,其中主要体现在1.加入了SE模块(注意力机制)。2.更新了激活函数
在这里插入图片描述
1)SE模块
这是一种轻量级的注意力机制,注意力机制是一种模仿人类视觉注意力机制的神经网络结构,用于增强模型对重要特征的关注度,通过引入注意力机制,可以提高模型的性能和准确率。SE 模块由两个步骤组成:

①Squeeze(压缩)步骤: 将特征图的全局信息进行压缩,通过全局平均池化操作将特征图的通道维度压缩成一个数值,以获得特征图的全局统计信息。

②Excitation(激发)步骤: 根据压缩后的全局信息,学习生成特征图的通道注意力权重,以增强重要特征的表示能力。

​ SE 模块通过学习生成特征图的通道注意力权重,使模型能够更加有效地关注重要的特征,从而提高模型的性能和准确率。

​ 注意力机制S,其结构见图 3.7,通过 Squeeze 操作(H×W×C 的特征图被压缩为 1×1×C)将特征图的全局空间信息压缩为通道描述信息,再通过Excitation 操作(对1×1×C 的通道信息进行重要程度的预测)得到每个通道的重要性信息,最后将其作用于原始特征上。

引入SE模块,主要为了利用结合特征通道的关系来加强网络的学习能力它首先是将特征图的每个通道都进行平均池化,然后进行两个全连接层得到一个输出结果,这个结果会和原始的特征图进行相乘,得到新的特征图

img

U 是一个 W×H×C 的Feature Map, (W,H) 是图像的尺寸, C是图像的通道数。

首先是Fsq(⋅) (Squeeze操作),顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个 1×1×C 的特征向量,特征向量的值由U确定。这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野,这一点在很多任务中都是非常有用的。

Squeeze部分的作用是获得Feature Map U的每个通道的全局信息嵌入(特征向量)。在SE block中,这一步通过VGG中引入的Global Average Pooling(GAP)实现的,即通过求每个通道C Feature Map的平均值。
其次是 Excitation 操作,它是一个类似于循环神经网络中门的机制。通过参数 w 来为每个特征通道生成权重,其中参数 w 被学习用来显式地建模特征通道间的相关性。

最后是一个 Reweight 的操作,将 Excitation 的输出的权重看做是经过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。

以在Mobilenetv3中的应用为例进行理解
img

首先下图左上角表示为四个通道的特质图,经平均池化后得到左下角的图;再次经过两次全连接层后,转化成了右下角的图,最后用右下角的0.5、0.3,0.1,0.2(Conv1每个channel的权值系数)分别乘原始的特质图,则得到最终的右上角的图。

2)重新设计激活函数

在v1和v2版本中用到的Relu激活函数是Relu6激活函数,

R e L U ( x ) = m a x ( x , 0 ) ReLU(x) = max (x, 0) ReLU(x)=max(x,0)

R e L U 6 ( x ) = m i n ( m a x ( x , 0 ) , 6 ) ReLU6 (x) = min (max (x, 0) , 6) ReLU6(x)=min(max(x,0),6)

s w i s h ( x ) = x ⋅ σ ( x ) , σ ( x ) = 1 / ( 1 + e − x ) swish (x) = x · σ(x),σ(x)=1/(1+e^{-x}) swish(x)=xσ(x)σ(x)=1/(1+ex)

作者在文中提到,将sigmoid激活函数替换为h-sigmoid激活函数,将swish激活函数替换为h-swish激活函数:

h − s i g m o i d = R e L u ( x + 3 ) / 6 h-sigmoid=ReLu(x+3)/6 hsigmoid=ReLu(x+3)/6

h − s w i s h [ x ] = x × h − s i g m o i d = x R e L u ( x + 3 ) / 6 h-swish[x]=x×h-sigmoid=xReLu(x+3)/6 hswish[x]=x×hsigmoid=xReLu(x+3)/6

image-20240114161549752

🤔为什么要替换呢?

因为switch函数计算、求导过于复杂,对量化过程不友好,而h-switch与switch函数相似,看起来简单,计算也简单,使用h-switch后,对网络的推理过程有帮助,且对量化过程友好。

🤔是如何设计出h-sigmoid和h-switch激活函数的?

swish的作者认为,该函数具有无上界、有下界、平滑、非单调的特点,在深层模型上优于ReLU。但是,由于sigmoid函数计算复杂(sigmoid(x) = (1 + exp(-x))^(-1)),所以V3改用近似函数来逼近swish,这使其变得更硬(hard)。作者选择了ReLU6作为这个近似函数,有两个原因:1、在几乎所有的软件和硬件框架上都可以使用ReLU6的优化实现;2、ReLU6能在特定模式下消除由于近似sigmoid的不同实现而带来的潜在的数值精度损失。img

作者认为,随着网络的深入,应用非线性激活函数的成本会降低,能够更好的减少参数量。作者发现swish的大多数好处都是通过在更深的层中使用它们实现的。因此,在V3的架构中,只在模型的第一层和后半部分使用h-swish(HS)。

参考:MobileNet V3激活函数之h-swish_h-swish函数优点-CSDN博客

2.2 神经结构搜索(NAS)

相较于之前的网络,不管是VGG、ResNet、MobileNetV1、MobileNetV2,网络结构都是我们自己手动去设计的,如网络的层数、卷积核大小、步长等等参数都需要自己设置。而NAS通过计算机来实现最优的参数设定,通过比较不同参数的网络模型效果,从而选择最优的参数设置。但是这也对计算机的性能要求也特别的高。

2.3重新设计耗时层结构

耗时层指的是在深度学习网络模型中计算复杂度较高、耗费大量时间的层或模块,MobileNet V3主要是针对第一层卷积层和最后的阶段进行了精简和优化。

主要改变如下:
1.减少第一个卷积层的卷积核个数 (32->16)
在MobileNet v1,v2中,第一个卷积层的卷积核个数(即#filters or c)都是32,论文作者研究发现,将卷积核个数变为16个后,准确率和32个差不多,但是可以节省2ms的时间。

2.精简Last Stage

Original Last Stage是通过NAS算出来的,但最后实际测试发现Efficient Last Stage结构可以在不损失精度情况下去除一些多余的层,(移除之前的瓶颈层连接,进一步降低网络参数。可以有效降低11%的推理耗时,而性能几乎没有损失)如下图所示:

image-20240114160014579

3.网络结构

bottleneck:
在这里插入图片描述
总体:
img

上图为MobileNetV3的网络结构图,large和small的整体结构一致,区别就是基本单元bneck的个数以及内部参数上,主要是通道数目。

详细:

small和large版本参数

上表为具体的参数设置,其中bneck是网络的基本结构。SE代表是否使用通道注意力机制。NL代表激活函数的类型,包括HS(h-swish),RE(ReLU)。NBN 代表没有BN操作。 s 是stride的意思,网络使用卷积stride操作进行降采样,没有使用pooling操作。

*注意:
1-对于何时使用shortcut需要注意;
2-对于large网络,第一个bneck的输入channel和输出channel相同,这里就没用到第一个1x1的卷积,因为它的作用是升维的,这里的维度没变。

四、搭建模型

1-1.搭建MobileNet V2模型

1)搭建ConvBNReLU模块

class ConvBNReLU(nn.Sequential):
    def __init__(self,in_channel,out_channel,kernel_size=1,stride=1,groups=1):
        padding=(kernel_size-1)//2 #为了确保输出前后特征图的尺寸大小不变
        super(ConvBNReLU,self).__init__(
            nn.Conv2d(in_channel,out_channel,kernel_size,stride,padding,groups=groups,bias=False),#groups参数为1表示普通卷积,否则表示分组卷积
            nn.BatchNorm2d(out_channel),
            nn.ReLU6(inplace=True)
        )

🤔为什么padding要设置为(kernel_size-1)//2?
为了确保输出前后特征图的尺寸大小不变,这个模块是构成Residual的基本模块,而Residual的基本结构为1x1升维卷积-3x3DW卷积+PW卷积,对于其中1x1升维卷积,只需要改变其维度,而其尺寸大小不变。

nn.Conv2d中的groups参数解释?
group=1表示普通卷积,group=n表示将卷积核分为n个,即分组卷积
group=1 vs groups=2:
在这里插入图片描述
参考:https://blog.csdn.net/qq_44166630/article/details/127802567

2)搭建Inverted Residual模块

在这里插入图片描述

class InvertedResidual(nn.Module):
    def __init__(self,in_channel,out_channel,stride,expand_ratio):
        super(InvertedResidual, self).__init__()
        hidden_channel=in_channel*expand_ratio #expand_ratio:扩展因子t
        self.use_shortcut=stride ==1 and in_channel==out_channel

        layers=[]
        if expand_ratio!=1:
        	# 1x1 conv
            layers.append(ConvBNReLU(in_channel,hidden_channel,kernel_size=1))
            layers.extend([
            # 3x3 DW
            ConvBNReLU(hidden_channel,hidden_channel,stride=stride,groups=hidden_channel),#DW卷积的卷积数为输入特征矩阵的深度
            # 1x1 PW conv(linear)
            nn.Conv2d(hidden_channel,out_channel,kernel_size=1,bias=False),
            nn.BatchNorm2d(out_channel)
        ])
        self.conv=nn.Sequential(*layers)#将所有层打包到一起
    def forward(self,x):
        if self.use_shortcut:
            return x+self.conv(x)
        else:
            return self.conv(x)

🤔为什么这里要判断expand_ratio!=1?
对于第一个bottleneck,t=1,说明并没有升维,不需要第一个卷积层
🤔为什么在构建inverted residual时第一层用append,后面两层用extend?
因为append() 方法用于在列表的末尾添加单个元素,而 extend() 方法用于在列表的末尾一次性追加另一个列表中的多个值。
在这里插入图片描述

3)搭建MobileNet V2模块

def _make_divisible(ch, divisor=8, min_ch=None):
    """
    该函数的作用是确保所有的层的通道数都能被8整除。它会将输入的通道数调整为最接近的可以被8整除的数,并且保证调整后的通道数不会比原来的数减少超过10%
    This function is taken from the original tf repo.
    It ensures that all layers have a channel number that is divisible by 8
    It can be seen here:
    https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
    """
    if min_ch is None:
        min_ch = divisor
    new_ch = max(min_ch, int(ch + divisor / 2) // divisor * divisor)
    # Make sure that round down does not go down by more than 10%.
    if new_ch < 0.9 * ch:
        new_ch += divisor
    return new_ch
    
class MobileNetV2(nn.Module):
    def __init__(self,num_classes=1000,alpha=1.0,round_nearest=8):
        super(MobileNetV2, self).__init__()
        block=InvertedResidual
        input_channel=_make_divisible(32*alpha,round_nearest)
        last_channel=_make_divisible(1280*alpha,round_nearest)
        inverted_residual_setting=[
            #t,c,n,s(倍率因子,channels,num_classes,stride)
            [1, 16, 1, 1],
            [6, 24, 2, 2],
            [6, 32, 3, 2],
            [6, 64, 4, 2],
            [6, 96, 3, 1],
            [6, 160, 3, 2],
            [6, 320, 1, 1],
        ]

        features=[]
        # conv1 layer
        features.append(ConvBNReLU(3,input_channel,stride=2))
        # building inverted residual residual blockes
        for t,c,n,s in inverted_residual_setting:
            output_channel=_make_divisible(c*alpha,round_nearest)
            for i in range(n):
                stride=s if i==0 else 1
                features.append(block(input_channel,output_channel,stride,expand_ratio=t))
                input_channel=output_channel
        # last
        features.append(ConvBNReLU(input_channel,last_channel,1))
        self.features=nn.Sequential(*features)

        # building classifier
        self.avgpool=nn.AdaptiveAvgPool2d((1,1))
        self.classifier=nn.Sequential(
            nn.Dropout(0.2),
            nn.Linear(last_channel,num_classes)
        )
        # weight initialization
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out')
                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)
    def forward(self,x):
        x=self.features(x)
        x=self.avgpool(x)
        x=torch.flatten(x,1)
        x=self.classifier(x)
        return x

_make_divisible是官方实现的,作用是确保所有的层的通道数都能被8整除,可能会提高运算效率

1-2.搭建MobileNet V3模型

1)搭建ConvBNActivation模块

这里直接复制的官方的代码:from torchvision.models import mobilenetv3,点击mobilenetv3查看


def _make_divisible(ch, divisor=8, min_ch=None):
    """
    This function is taken from the original tf repo.
    It ensures that all layers have a channel number that is divisible by 8
    It can be seen here:
    https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
    """
    if min_ch is None:
        min_ch = divisor
    new_ch = max(min_ch, int(ch + divisor / 2) // divisor * divisor)
    # Make sure that round down does not go down by more than 10%.
    if new_ch < 0.9 * ch:
        new_ch += divisor
    return new_ch


class ConvBNActivation(nn.Sequential):
    def __init__(self,
                 in_planes: int,
                 out_planes: int,
                 kernel_size: int = 3,
                 stride: int = 1,
                 groups: int = 1,
                 norm_layer: Optional[Callable[..., nn.Module]] = None,
                 activation_layer: Optional[Callable[..., nn.Module]] = None):
        padding = (kernel_size - 1) // 2
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        if activation_layer is None:
            activation_layer = nn.ReLU6
        super(ConvBNActivation, self).__init__(nn.Conv2d(in_channels=in_planes,
                                                         out_channels=out_planes,
                                                         kernel_size=kernel_size,
                                                         stride=stride,
                                                         padding=padding,
                                                         groups=groups,
                                                         bias=False),
                                               norm_layer(out_planes),
                                               activation_layer(inplace=True))


2)搭建SE模块

重点:
①结构:avg-fc1-fc2-*
②两个全连接层:fc1的通道数是输入特征矩阵的四分之一,fc2的通道数要与输入特征矩阵的通道数保持一致

class SqueezeExcitation(nn.Module):
    def __init__(self, input_c: int, squeeze_factor: int = 4):
        super(SqueezeExcitation, self).__init__()
        squeeze_c = _make_divisible(input_c // squeeze_factor, 8)
        self.fc1 = nn.Conv2d(input_c, squeeze_c, 1)
        self.fc2 = nn.Conv2d(squeeze_c, input_c, 1)

    def forward(self, x: Tensor) -> Tensor:
        scale = F.adaptive_avg_pool2d(x, output_size=(1, 1))
        scale = self.fc1(scale)
        scale = F.relu(scale, inplace=True)
        scale = self.fc2(scale)
        scale = F.hardsigmoid(scale, inplace=True)
        return scale * x

3)配置MobileNetV3网络模型参数

class InvertedResidualConfig:
    def __init__(self,
                 input_c: int,
                 kernel: int,
                 expanded_c: int,
                 out_c: int,
                 use_se: bool,
                 activation: str,
                 stride: int,
                 width_multi: float):
        self.input_c = self.adjust_channels(input_c, width_multi)
        self.kernel = kernel
        self.expanded_c = self.adjust_channels(expanded_c, width_multi)
        self.out_c = self.adjust_channels(out_c, width_multi)
        self.use_se = use_se
        self.use_hs = activation == "HS"  # whether using h-swish activation
        self.stride = stride

    @staticmethod
    def adjust_channels(channels: int, width_multi: float):
        return _make_divisible(channels * width_multi, 8)

4)搭建Inverted Residual模型

class InvertedResidual(nn.Module):
    def __init__(self,
                 cnf: InvertedResidualConfig,
                 norm_layer: Callable[..., nn.Module]):
        super(InvertedResidual, self).__init__()

        if cnf.stride not in [1, 2]:
            raise ValueError("illegal stride value.")

        self.use_res_connect = (cnf.stride == 1 and cnf.input_c == cnf.out_c)

        layers: List[nn.Module] = []
        activation_layer = nn.Hardswish if cnf.use_hs else nn.ReLU

        # expand
        if cnf.expanded_c != cnf.input_c:
            layers.append(ConvBNActivation(cnf.input_c,
                                           cnf.expanded_c,
                                           kernel_size=1,
                                           norm_layer=norm_layer,
                                           activation_layer=activation_layer))

        # depthwise
        layers.append(ConvBNActivation(cnf.expanded_c,
                                       cnf.expanded_c,
                                       kernel_size=cnf.kernel,
                                       stride=cnf.stride,
                                       groups=cnf.expanded_c,
                                       norm_layer=norm_layer,
                                       activation_layer=activation_layer))

        if cnf.use_se:
            layers.append(SqueezeExcitation(cnf.expanded_c))

        # project
        layers.append(ConvBNActivation(cnf.expanded_c,
                                       cnf.out_c,
                                       kernel_size=1,
                                       norm_layer=norm_layer,
                                       activation_layer=nn.Identity))

        self.block = nn.Sequential(*layers)
        self.out_channels = cnf.out_c
        self.is_strided = cnf.stride > 1

    def forward(self, x: Tensor) -> Tensor:
        result = self.block(x)
        if self.use_res_connect:
            result += x

        return result

🤔这里为什么要有cnf.expanded_c != cnf.input_c:这个判断呢?
在解释MobileNet V3的网络结构时说过,对于large网络,第一个bneck的输入channel和输出channel相同,这里就没用到第一个1x1的卷积,因为它的作用是升维的,这里的维度没变,也就是它们的深度不相等时才会有这一层。

5)搭建MobileNet V3模型

class MobileNetV3(nn.Module):
    def __init__(self,
                 inverted_residual_setting: List[InvertedResidualConfig],
                 last_channel: int,
                 num_classes: int = 1000,
                 block: Optional[Callable[..., nn.Module]] = None,
                 norm_layer: Optional[Callable[..., nn.Module]] = None):
        super(MobileNetV3, self).__init__()

        if not inverted_residual_setting:
            raise ValueError("The inverted_residual_setting should not be empty.")
        elif not (isinstance(inverted_residual_setting, List) and
                  all([isinstance(s, InvertedResidualConfig) for s in inverted_residual_setting])):
            raise TypeError("The inverted_residual_setting should be List[InvertedResidualConfig]")

        if block is None:
            block = InvertedResidual

        if norm_layer is None:
            norm_layer = partial(nn.BatchNorm2d, eps=0.001, momentum=0.01)

        layers: List[nn.Module] = []

        # building first layer
        firstconv_output_c = inverted_residual_setting[0].input_c
        layers.append(ConvBNActivation(3,
                                       firstconv_output_c,
                                       kernel_size=3,
                                       stride=2,
                                       norm_layer=norm_layer,
                                       activation_layer=nn.Hardswish))
        # building inverted residual blocks
        for cnf in inverted_residual_setting:
            layers.append(block(cnf, norm_layer))

        # building last several layers
        lastconv_input_c = inverted_residual_setting[-1].out_c
        lastconv_output_c = 6 * lastconv_input_c
        layers.append(ConvBNActivation(lastconv_input_c,
                                       lastconv_output_c,
                                       kernel_size=1,
                                       norm_layer=norm_layer,
                                       activation_layer=nn.Hardswish))
        self.features = nn.Sequential(*layers)
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Sequential(nn.Linear(lastconv_output_c, last_channel),
                                        nn.Hardswish(inplace=True),
                                        nn.Dropout(p=0.2, inplace=True),
                                        nn.Linear(last_channel, num_classes))

        # initial weights
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out")
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                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)

    def _forward_impl(self, x: Tensor) -> Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)

        return x

    def forward(self, x: Tensor) -> Tensor:
        return self._forward_impl(x)

6)传入参数构建mobilenet_v3_largemobilenet_v3_small

上面只是把moblienetv3的模型架构搭建出来了,现在才是传入具体的参数进行构建。

def mobilenet_v3_large(num_classes: int = 1000,
                       reduced_tail: bool = False) -> MobileNetV3:
    """
    Constructs a large MobileNetV3 architecture from
    "Searching for MobileNetV3" <https://arxiv.org/abs/1905.02244>.

    weights_link:
    https://download.pytorch.org/models/mobilenet_v3_large-8738ca79.pth

    Args:
        num_classes (int): number of classes
        reduced_tail (bool): If True, reduces the channel counts of all feature layers
            between C4 and C5 by 2. It is used to reduce the channel redundancy in the
            backbone for Detection and Segmentation.
    """
    width_multi = 1.0
    bneck_conf = partial(InvertedResidualConfig, width_multi=width_multi)
    adjust_channels = partial(InvertedResidualConfig.adjust_channels, width_multi=width_multi)

    reduce_divider = 2 if reduced_tail else 1

    inverted_residual_setting = [
        # input_c, kernel, expanded_c, out_c, use_se, activation, stride
        bneck_conf(16, 3, 16, 16, False, "RE", 1),
        bneck_conf(16, 3, 64, 24, False, "RE", 2),  # C1
        bneck_conf(24, 3, 72, 24, False, "RE", 1),
        bneck_conf(24, 5, 72, 40, True, "RE", 2),  # C2
        bneck_conf(40, 5, 120, 40, True, "RE", 1),
        bneck_conf(40, 5, 120, 40, True, "RE", 1),
        bneck_conf(40, 3, 240, 80, False, "HS", 2),  # C3
        bneck_conf(80, 3, 200, 80, False, "HS", 1),
        bneck_conf(80, 3, 184, 80, False, "HS", 1),
        bneck_conf(80, 3, 184, 80, False, "HS", 1),
        bneck_conf(80, 3, 480, 112, True, "HS", 1),
        bneck_conf(112, 3, 672, 112, True, "HS", 1),
        bneck_conf(112, 5, 672, 160 // reduce_divider, True, "HS", 2),  # C4
        bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1),
        bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1),
    ]
    last_channel = adjust_channels(1280 // reduce_divider)  # C5

    return MobileNetV3(inverted_residual_setting=inverted_residual_setting,
                       last_channel=last_channel,
                       num_classes=num_classes)


def mobilenet_v3_small(num_classes: int = 1000,
                       reduced_tail: bool = False) -> MobileNetV3:
    """
    Constructs a large MobileNetV3 architecture from
    "Searching for MobileNetV3" <https://arxiv.org/abs/1905.02244>.

    weights_link:
    https://download.pytorch.org/models/mobilenet_v3_small-047dcff4.pth

    Args:
        num_classes (int): number of classes
        reduced_tail (bool): If True, reduces the channel counts of all feature layers
            between C4 and C5 by 2. It is used to reduce the channel redundancy in the
            backbone for Detection and Segmentation.
    """
    width_multi = 1.0
    bneck_conf = partial(InvertedResidualConfig, width_multi=width_multi)
    adjust_channels = partial(InvertedResidualConfig.adjust_channels, width_multi=width_multi)

    reduce_divider = 2 if reduced_tail else 1

    inverted_residual_setting = [
        # input_c, kernel, expanded_c, out_c, use_se, activation, stride
        bneck_conf(16, 3, 16, 16, True, "RE", 2),  # C1
        bneck_conf(16, 3, 72, 24, False, "RE", 2),  # C2
        bneck_conf(24, 3, 88, 24, False, "RE", 1),
        bneck_conf(24, 5, 96, 40, True, "HS", 2),  # C3
        bneck_conf(40, 5, 240, 40, True, "HS", 1),
        bneck_conf(40, 5, 240, 40, True, "HS", 1),
        bneck_conf(40, 5, 120, 48, True, "HS", 1),
        bneck_conf(48, 5, 144, 48, True, "HS", 1),
        bneck_conf(48, 5, 288, 96 // reduce_divider, True, "HS", 2),  # C4
        bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1),
        bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1)
    ]
    last_channel = adjust_channels(1024 // reduce_divider)  # C5

    return MobileNetV3(inverted_residual_setting=inverted_residual_setting,
                       last_channel=last_channel,
                       num_classes=num_classes)

2.训练

和之前训练AlexNet、VGG、GoogLeNet、ResNet的步骤差不多,只需要修改少部分内容,如下:

1)下载预训练权重
去torchvision.models里面找

from torchvision.models import mobilenetv2

鼠标点击mobilenetv2进入,找到MobileNet_V2,这里给出了下载链接:
https://download.pytorch.org/models/mobilenet_v2-b0353104.pth
下载之后改名为mobilenet_v2.pth

2)定义模型并载入权重

net=MobileNetV2(num_classes=5)
net=net.to(device)
model_weight_path="./mobilenet_v2.pth"
assert os.path.exists(model_weight_path),"file {} does not exists".format(model_weight_path)
net.load_state_dict(torch.load(model_weight_path,map_location=device))#载入模型权重

3)冻结特征层权重
不会对它们求导,也不会对它们进行参数更新了

#freeze features weights
for param in net.features.parameters():
    param.requires_grad=False

然后就可以进行训练了。
预测就是修改下训练出来的模型的路径就好

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值