本文为学习记录和备忘录,对代码进行了详细注释,以供学习。
内容来源:
★github: https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
★b站:https://space.bilibili.com/18161609/channel/index
★CSDN:https://blog.csdn.net/qq_37541097
1. MobileNet网络详解
1.1 MobileNet(v1)
1.1.1 MoblieNet(v1)网络概述
MobileNet网络是由google团队在2017年提出的,专注于移动端或者嵌入式设备中的轻量级CNN网络。相比传统卷积神经网络,在准确率小幅降低的前提下大大减少模型参数与运算量。(相比VGG16准确率减少了0.9%,但模型参数只有VGG的1/32)
研究动机:传统卷积神经网络, 内存需求大、 运算量大,导致无法在移动设备以及嵌入式设备上运行
论文全称:MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
论文链接:MobileNet(v1):MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
网络中的亮点:
- Depthwise Convolution(简称DW卷积,大大减少运算量和参数数量)
- 增加超参数α、β
1.1.2 DW卷积(Depthwise Convolution)
传统卷积:
其中,卷积核channel=输入特征矩阵channel,输出特征矩阵channel = # filters。如上图,输入特征矩阵channel=3,则卷积核channel=3。共有4个filters,则输出特征矩阵channel=4。
DW卷积(Depthwise Convolution):
DW卷积中,每个卷积核的channel都为1,每个卷积核只负责与输入特征矩阵中的1个channel进行卷积运算,然后再得到相应的输出特征矩阵中的1个channel。则,所有卷积核的channel都等于1,且输入特征矩阵channel=# filters(即卷积核个数)=输出特征矩阵channel。
PW卷积(Pointwise Conv):
PW卷积和普通卷积一样,特殊在于卷积核大小为1。
深度可分卷积(Depthwise Separable Conv):
由两部分组成:DW和PW。理论上普通卷积计算量是DW+PW的8到9倍。
1.1.3 MobileNet(v1)网络详细参数
上表第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准确率只降低一点点,但是模型参数大概只有VGG网络的1/32。超参数α指卷积核个数的倍率,控制卷积过程中所采用的卷积核的个数。β指输入图像尺寸。
1.2 MobileNet(v2)
1.2.1 MoblieNet(v2)网络概述
MobileNet v2网络是由google团队在2018年提出的,相比MobileNet V1网络,准确率更高,模型更小。
论文全称:MobileNetV2: Inverted Residuals and Linear Bottlenecks
论文链接:MobileNet(v2):MobileNetV2: Inverted Residuals and Linear Bottlenecks
网络中的亮点:
- Inverted Residuals(倒残差结构)
- Linear Bottlenecks
1.2.2 Inverted Residuals(倒残差结构)
(1)残差结构与倒残差结构对比如下:
原始的残差结构先通过1×1卷积降维,然后经过3×3卷积,最后再经过1×1卷积升维。
而倒残差结构先通过1×1卷积升维,然后经过3×3卷积,最后再经过1×1卷积降维。与原始残差结构正好相反。
另外,普通的残差结构中采用的激活函数是relu激活函数,而在倒残差结构中采用的激活函数是relu6激活函数:
y = ReLU6 (x) = min (max (x, 0) , 6)
(2)Linear Bottlenecks
倒残差结构中最后一个1×1的卷积层,它使用了线性的激活函数而不是relu激活函数。因为relu函数对低维特征信息会产生大量损失。而在倒残差结构中最后经过1×1卷积降维,是一个低维特征向量,因此要用线性激活函数代替relu激活函数来避免信息的损失。
(3)原论文中倒残差结构的结构图为:
倒残差结构中的层信息为:
由上图,倒残差结构第1层为普通的卷积层,卷积核大小为1×1,激活函数为ReLU6,(1×1卷积升维),所采用的卷积核个数(# filters)为tk(t为倍率因子,用来扩大深度)。
第2层为DW卷积,卷积核大小为3,步距s为传入参数,使用ReLU6激活函数,输出特征矩阵深度与输入特征矩阵深度相同,为tk。但是高和宽缩减为1/s倍。
第3层为普通1×1卷积层,这里需要注意的是,激活函数使用的是线性激活函数,卷积核个数为k’(人为指定)。
需要注意的是:当stride=1且输入特征矩阵与输出特征矩阵shape相同时才有shortcut连接。(和图中表示的稍有不同)
1.2.3MobileNet(v2)网络详细参数
- 要点1:t是扩展因子,对应表格中第1层1×1卷积层的扩展倍率h×w×(tk);
c是输出特征矩阵深度channel,对应表格中的k’;
n是bottleneck的重复次数;
s是步距(针对第一层,其他为1),s只代表每一个block的第1层的bottleneck的步距,一个block由一系列bottleneck组成。如第3行n=2,s=2,bottleneck重复2次,第1层的bottleneck的步距为2,而第2层的bottleneck的步距仍为1。 - 要点2:
上面说过,当stride=1且输入特征矩阵与输出特征矩阵shape相同时才有shortcut连接。
以这个例子为例:
有3层bottleneck,对于第1层bottleneck,步距s=1,但是输入特征矩阵深度为64,输出特征矩阵深度为96,两者shape不相等,故在第1层bottleneck中不存在short cut分支。而对于第2层、第3层bottleneck而言,输入特征矩阵和输出特征矩阵深度都等于96,且满足步距s=1的条件,因此在后2层bottleneck中存在short cut分支。 - 要点3:最后一层为卷积层,但其输入特征矩阵为1×1×1280,相当于是一维向量,因此卷积的效果和全连接层相同。这里的输出矩阵深度为k,代表的就是分类的类别个数。
1.3 MobileNet(v3)
1.3.1 MoblieNet(v3)网络概述
在很多轻量级的网络中,MobileNet(v3)经常被使用到。MobileNet(v3)是Google在继MobileNet(v2)后提出的v3版本。
论文全称:Searching for MobileNetV3
论文链接:MobileNet(v3):Searching for MobileNetV3
网络中的亮点:
- 更新Block(bneck)
- 使用NAS搜索参数(Neural Architecture Search)
- 重新设计耗时层结构
1.3.2 更新Block(bneck)
MobileNet(v2)中倒残差结构如下所示:
其中,需要注意的是:当stride=1且 输入特征矩阵与输出特征矩阵shape 相同时才有shortcut连接。
在MobileNet(v3)中,更新了Block,其中主要体现在1.加入了SE模块(注意力机制)。2.更新了激活函数。
其结构如下:
- 要点1:SE模块,即注意力机制(上图中红色框部分)。对得到的特征矩阵,对其每一个channel进行池化处理,那么特征矩阵的channel为多少, 得到的一维向量就有多少个元素。接下来通过两个全连接层得到一个输出向量。
- 要点2:对于第1个全连接层,它的节点个数等于特征矩阵channel数的1/4,第2个全连接层的节点个数与特征矩阵的channel数相同。则经过两个全连接层的输出向量可以理解为对特征矩阵的每1个channel分析出了一个权重关系(比较重要的channel赋予一个大权重,不太重要的channel赋予小权重)。则得到的输出向量中每一个元素即为针对每一个channel的权重,将每一个channel中的数据与相应权重相乘,即可得到新的特征矩阵。(输出特征矩阵channel与输入特征矩阵channel相同)
- 要点3:第1个全连接层的激活函数是Relu,第2个全连接层的激活函数是Hard-sigmoid。
- 要点4:图中NL指非线性激活函数,因为每一个层中使用的激活函数类型不同,这里统一以NL指代。
- 要点5:最后1×1卷积降维层没有使用激活函数。(也可以说使用了线性激活y=x)
SE注意力机制过程可由下例展示:
1.3.3 重新设计耗时层结构
主要改变如下:
1.减少第一个卷积层的卷积核个数 (32->16)
在MobileNet v1,v2中,第一个卷积层的卷积核个数(即#filters or c)都是32,论文作者研究发现,将卷积核个数变为16个后,准确率和32个差不多,但是可以节省2ms的时间。
2.精简Last Stage
1.3.4 重新设计激活函数
在MobileNet(v2)中,常用relu6激活函数。 ReLU6 (x) = min (max (x, 0) , 6)
现在介绍一种新的激活函数:swish (x) = x × σ(x),其中σ(x)=1/(1+e(-x)),但是这种激活函数计算、求导复杂,对量化过程不友好。因此作者提出了h-swish激活函数。
在这之前介绍一下h-sigmoid激活函数,h-sigmoid=ReLu(x+3)/6
定义h-swish函数为:
h-swish[x]=x×h-sigmoid=xReLu(x+3)/6.
作者在文中提到,将sigmoid激活函数替换为h-sigmoid激活函数,将swish激活函数替换为h-swish激活函数,对网络的推理过程有帮助,且对量化过程友好。
1.3.5 MobileNet(v3)网络结构及详细参数
(1)MobileNet(v3)-Large
input表示当前层输入特征矩阵的shape,比如表中使用RGB彩色图片,它的高和宽都是244;
Operator表示相应的操作,其中①bneck表示V3中更新后的block,②其后紧跟的3×3表示DW卷积的卷积核大小③最后两层NBN表示不使用BN层;
exp size表示bneck结构中,第1个1×1升维卷积层要将输入特征矩阵升到的维度,即exp size给定多少,就将输入特征矩阵升到多少维;
#out表示输出特征矩阵的channel,上文强调过,为了减少耗时,第1层卷积层中使用的卷积核个数为16;
SE表示是否使用了SE注意力机制;
NL表示非线性激活函数,其中HS表示h-swish激活函数,RE表示使用relu激活函数;
s表示DW卷积的步距。
以下几点需注意:
- 第1个1×1升维卷积层根据exp size给定值的大小将输入特征矩阵升维至指定channel,然后DW卷积层不会改变channel大小,SE操作同样不改变channel大小,最后根据#out的给定值,通过1×1降维卷积层输出指定channel的特征矩阵。
- 在第1个bneck结构中,即详细参数的第2行。其输入特征矩阵channel为16,升维维度也为16,则在第1个bneck结构中,没有进行1×1升维卷积层操作,同时这层也没有SE结构。则直接对输入特征矩阵进行DW操作,然后直接通过1×1卷积降温处理得到输出特征矩阵。
- 与MoblieNetv2相似,当stride=1且输入特征矩阵与输出特征矩阵shape相同时才有shortcut连接。
(2)MobileNet(v3)-Small
MobileNet(v3)-Small与MobileNet(v3)-Large类似,详细参数如下:
2. Pytorch搭建
2.1 MobileNet(v2)
2.1.1 model.py
首先定义Conv+BN+ReLU这样的组合层,在MobileNet中所有的卷积层,包括DW卷积操作,基本上都是有卷积conv+BN+ReLU6激活函数共同组成,唯一不同的是在倒残差结构的第3层,使用1×1的普通卷积,将其进行降维处理时,使用的是线性激活函数。
- 要点1:始化函数传入参数groups:groups如果设置为1,则为普通卷积。如果groups设置为in_channel,则为DW卷积(pytroch中DW卷积也调用nn.Conv2d来实现)。
接下来定义倒残差结构,def InvertedResidual(nn.Module):
- 要点1:由上图,倒残差结构第1层为普通的卷积层,卷积核大小为1×1,激活函数为ReLU6,(1×1卷积升维),所采用的卷积核个数(# filters)为tk(t为倍率因子,用来扩大深度)。
第2层为DW卷积,卷积核大小为3,步距s为传入参数,使用ReLU6激活函数,输出特征矩阵深度与输入特征矩阵深度相同,为tk。但是高和宽缩减为1/s倍。
第3层为普通1×1卷积层,这里需要注意的是,激活函数使用的是线性激活函数,卷积核个数为k’(人为指定)。
且当stride=1且输入特征矩阵与输出特征矩阵shape相同时才有shortcut分支。 - 要点2:当倍率因子t=1时(对应详细参数表第2行),那么倒残差结构第1层1×1升维卷积层输出特征矩阵channel等于输入特征矩阵channel,即第1层1×1卷积层没有起作用,此时,舍去第1层1×1卷积层。当倍率因子t != 1时,不存在上述情况。
最后定义MobileNet(v2)网络结构,初始化函数中传入参数num_classes,即分类的类别个数。α是超参数,在v1中提到,控制卷积层所使用卷积核个数的倍率,round_nearest为基数,在定义的_make_divisible函数中起作用,_make_divisible的作用是将输入值调整为最接近基数值整数倍的数值。
input_channel = _make_divisible(32 * alpha, round_nearest) # _make_divisible将输入的卷积核个数调整为round_nearest的整数倍
input_channel = _make_divisible(32 * alpha, round_nearest)
即将32×alpha调整为最接近8的整数倍的数值(这里round_nearest值为8)。
模型部分全部代码如下:
from torch import nn
import torch
def _make_divisible(ch, divisor=8, min_ch=None): # ch指输入特征深度,divisor指基数
# 此函数的作用时讲ch调整为指定divisor这个数的整数倍,将ch调整为离8最近的整数倍的数值
"""
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
# 首先定义一个Conv+BN+ReLU这样的组合层,在MobileNet中所有的卷积层,包括DW卷积操作,基本上都是有卷积+BN+ReLU6激活函数共同组成。
class ConvBNReLU(nn.Sequential): # 继承来自于nn.Sequential,而不是nn.Module。与pytorch官方样例保持一致。
def __init__(self, in_channel, out_channel, kernel_size=3, stride=1, groups=1):
# groups如果设置为1,则为普通卷积。如果groups设置为in_channel,则为DW卷积(pytroch中DW卷积也调用nn.Conv2d来实现)
padding = (kernel_size - 1) // 2 # padding根据kernel_size来计算
super(ConvBNReLU, self).__init__( # 在super.__init__()中传入这3个层结构
nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding, groups=groups, bias=False),
# kernel_size默认3,stride默认1,padding计算得到,groups默认等于1,bias不使用(因为下面有BN层)
nn.BatchNorm2d(out_channel), # BN层输入特征矩阵深度为out_channel
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, in_channel, out_channel, stride, expand_ratio):
# expand_ratio为倍率因子,用来扩大深度
super(InvertedResidual, self).__init__()
hidden_channel = in_channel * expand_ratio # hidden_channel为第1层卷积层卷积核个数,即tk
self.use_shortcut = stride == 1 and in_channel == out_channel # 定义1个布尔变量判断是否使用short cut分支
layers = []
if expand_ratio != 1: # 如果倍率因子=1,只有参数表第2行情况,这时不需要残差结构中第1层1×1卷积层
# 1x1 pointwise conv
layers.append(ConvBNReLU(in_channel, hidden_channel, kernel_size=1)) # 第1层:1×1卷积
layers.extend([ # 通过extend函数添加一系列层结构,与append功能相同,但extend能一次性批量插入很多元素
# 3x3 depthwise conv
# 第2层:DW卷积。输入c与输出c相同,都是hidden_channel.groups=hidden_channel控制着DW卷积区别于普通卷积。
ConvBNReLU(hidden_channel, hidden_channel, stride=stride, groups=hidden_channel),
# 1x1 pointwise conv(linear)
# 注意这里是线性激活函数,就不可以用刚才定义的ConvBNReLU()函数,这里用Conv2d。
nn.Conv2d(hidden_channel, out_channel, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channel),
# 线性激活函数y=x,也就是不做处理。则不添加激活函数就相当于是线性激活函数。
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_shortcut: # 判断是否满足short cut分支连接条件
return x + self.conv(x) # 如果满足shortcut条件,返回shortcut分支结果与主分支结果的和
else:
return self.conv(x) # 如果不满足shortcut条件,只返回主分支结果
class MobileNetV2(nn.Module):
def __init__(self, num_classes=1000, alpha=1.0, round_nearest=8):
# num_classes分类的类别个数.α超参数,控制卷积层所使用卷积核个数的倍率,round_nearest为基数,在下面_make_divisible函数中
super(MobileNetV2, self).__init__()
block = InvertedResidual # 将上面定义的InvertedResidual类传给block
input_channel = _make_divisible(32 * alpha, round_nearest) # _make_divisible将输入的卷积核个数调整为round_nearest的整数倍
# input_channel表示表格中第1行Conv2d卷积层所使用的卷积核的个数,也等于下一层输入特征矩阵的深度
last_channel = _make_divisible(1280 * alpha, round_nearest)
# last_channel表示表格中倒数第3行1×1卷积层的卷积核个数
# 创建1个list列表,list列表中每一个元素就是表格中bottleneck对应每一行的参数t,c,n,s
inverted_residual_setting = [
# t, c, n, s
[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 = [