经典轻量级神经网络(2)MobileNet V2及其在Fashion-MNIST数据集上的应用
1 MobileNet V2的简述
MobileNet V2
创新性的提出了具有线性bottleneck
的Inverted
残差块。- 这种块特别适用于移动设备和嵌入式设备,因为它用到的张量都较小,因此减少了推断期间的内存需求。
- MobileNetV2 提供了一个非常高效的面向移动设备的模型,可以用作许多视觉识别任务的基础。
- 论文下载地址: https://arxiv.org/abs/1801.04381
1.1 回顾一下MobileNet V1
MobileNet V1核心思想是采用 深度可分离卷积 操作。在相同的权值参数数量的情况下,相较标准卷积操作,可以减少数倍的计算量,从而达到提升网络运算速度的目的。
-
如上图,MobileNet V1首先利用3×3的深度可分离卷积提取特征,然后利用1×1的卷积来扩张通道。用这样的block堆叠起来的MobileNetV1既能较少不小的参数量、计算量,提高网络运算速度,又能的得到一个接近于标准卷积的还不错的结果,看起来是很美好的。
-
但是,在实际使用的时候, 发现深度卷积部分的卷积核比较容易训废掉:训完之后发现深度卷积训出来的卷积核有不少是空的。
-
作者认为,这是ReLU激活函数造成的,
ReLU
的非线性导致信息的不可逆丢失。一直以来,人们认为与任务相关的信息是嵌入到feature map
中的一个低维子空间。因此feature map
事实上有一定的信息冗余。如果缩减feature map
的通道数量(相当于降维),则可以降低计算量。MobileNet V1
就是采用宽度乘子,从而在计算复杂度和准确率之间平衡。由于ReLU
非线性激活的存在,缩减输入feature map
的通道数量可能会产生不良的影响。
1.2 MobileNet V2主要的创新点
1.2.1 ReLU 的非线性导致信息的不可逆丢失
下图就是对一个n维空间中的一个“东西”做ReLU运算,然后(利用T的逆矩阵T-1恢复)对比ReLU之后的结果与Input的结果相差有多大。
-
可以发现当n = 2、3时,与Input相比有很大一部分的信息已经丢失了。而当n = 15到30,还是有相当多的地方被保留了下来。
- 也就是说,对低维度做ReLU运算,很容易造成信息的丢失。而在高维度进行ReLU运算的话,信息的丢失则会很少。
-
这就解释了为什么深度卷积的卷积核有不少是空。
-
针对这个问题,可以这样解决:既然是ReLU导致的信息损耗,将ReLU替换成线性激活函数。
1.2.2 bottleneck block
-
虽然引入非线性会提升模型的表达能力,但是引入非线性会破坏太多信息,会引起准确率的下降。因此
bootleneck
中使用线性是非常重要的。 -
我们当然不能把所有的激活层都换成线性的,所以我们就把最后的那个ReLU6换成Linear。
-
作者将这个部分称之为linear bottleneck。
现在还有个问题是,深度卷积本身没有改变通道的能力,来的是多少通道输出就是多少通道。如果来的通道很少的话,DW深度卷积只能在低维度上工作,这样效果并不会很好,所以我们要【扩张】通道。既然我们已经知道PW逐点卷积也就是1×1卷积可以用来升维和降维,那就可以在DW深度卷积之前使用PW卷积进行升维(升维倍数为t,t=6),再在一个更高维的空间中进行卷积操作来提取特征。
也就是说,不管输入通道数是多少,经过第一个PW逐点卷积升维之后,深度卷积都是在相对的更高6倍维度上进行工作。
bottleneck block
:输入feature map
首先经过线性 bottleneck
来扩张通道数,然后经过深度可分离卷积,最后通过线性bottleneck
来缩小通道数。
输入bootleneck
输出通道数与输入通道数的比例称作膨胀比。
- 通常较小的网络使用略小的膨胀比效果更好,较大的网络使用略大的膨胀比效果更好。
- 如果膨胀比小于 1 ,这就是一个典型的
resnet
残差块。
1.2.3 Inverted residuals
MobileNet V1很像是一个直筒型的VGG网络。我们想像Resnet一样复用我们的特征,所以我们引入了shortcut结构,这样V2的block就是如下图形式:
现在,我们再来比较一下ResNet和MobileNetV2:
可以发现,都采用了 1×1 -> 3 ×3 -> 1 × 1 的模式,以及都使用Shortcut结构。但是不同点呢:
- ResNet 先降维 (0.25倍)、卷积、再升维。
- MobileNetV2 则是 先升维 (6倍)、卷积、再降维。
刚好V2的block刚好与Resnet的block相反,作者将其命名为Inverted residuals。就是论文名中的Inverted residuals。
1.3 两个版本block的对比及网络结构
1.3.1 两个版本block的对比
-
左边是v1的block,没有Shortcut并且带最后的ReLU6。
-
右边是v2的加入了1×1升维,引入Shortcut并且去掉了最后的ReLU,改为Linear。步长为1时,先进行1×1卷积升维,再进行深度卷积提取特征,再通过Linear的逐点卷积降维。将input与output相加,形成残差结构。步长为2时,因为input与output的尺寸不符,因此不添加shortcut结构,其余均一致。
-
事实上旁路连接有两个插入的位置:在两个
1x1
卷积的前后,或者在Dwise
卷积的前后。通过实验证明:在两个1x1
卷积的前后使用旁路连接的效果最好。 -
bottleneck block
可以看作是对信息的两阶段处理过程:- 阶段一:对输入
feature map
进行降维,这一部分代表了信息的容量。 - 阶段二:对信息进行非线性处理,这一部分代表了信息的表达。
在
MobileNet v2
中这二者是独立的,而传统网络中这二者是相互纠缠的。 - 阶段一:对输入
1.3.2 网络结构
MobileNet V2
的设计基于 MobileNet v1
,其结构如下:
- 卷积块+不断堆叠的倒置残差结构+卷积块+平均池化+卷积
- 每一行代表一个或者一组相同结构的层,层的数量由 n 给定。
- 相同结构指的是:
- 同一行内的层的类型相同,由
Operator
指定。其中bottleneck
指的是bottleneck block
。 - 同一行内的层的膨胀比相同,由
t
指定。 - 同一行内的层的输出通道数相同,由
c
指定。 - 同一行内的层:第一层采用步幅
s
,其它层采用步幅1
。
- 同一行内的层的类型相同,由
- 采用
ReLU6
激活函数,因为它在低精度浮点运算的环境下表现较好。 - 训练过程中采用
dropout
和BN
。 - 与
MobileNet V1
类似,MobileNet V2
也可以引入宽度乘子、分辨率乘子这两个超参数。
网络在ImageNet 测试集上的表现: 最后一列给出了预测单张图片的推断时间。
网络 | Top 1 | Params(百万) | 乘-加 数量(百万) | CPU |
---|---|---|---|---|
MobileNet V1 | 70.6 | 4.2 | 575 | 113ms |
ShuffleNet (1.5) | 71.5 | 3.4 | 292 | - |
ShuffleNet (x2) | 73.7 | 5.4 | 524 | - |
NasNet-A | 74.0 | 5.3 | 564 | 183ms |
MobileNet V2 | 72.0 | 3.4 | 300 | 75ms |
MobileNet V2(1.4) | 74.7 | 6.9 | 585 | 143ms |
2 MobileNet V2在Fashion-MNIST数据集上的应用示例
2.1 创建MobileNet V2网络模型
import torch
import torch.nn as nn
'''
定义卷积块,conv+bn+relu6
'''
def conv_block(in_channel, out_channel, kernel_size=3, stride=1, groups=1):
# 1x1卷积,padding=0
# 3x3卷积,padding=1
padding = 0 if kernel_size == 1 else 1
return nn.Sequential(
# conv
nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding=padding, groups=groups, bias=False),
# bn
nn.BatchNorm2d(out_channel),
# relu6
nn.ReLU6(inplace=True)
)
'''
定义倒置残差结构,Inverted Residual
'''
class InvertedResidual(nn.Module):
def __init__(self, in_channel, out_channel, stride, t=6):
super(InvertedResidual, self).__init__()
# 输入通道数
self.in_channel = in_channel
# 输出通道数
self.out_channel = out_channel
# 步长
self.stride = stride
# 中间层通道扩大倍数,对应原文expansion ratio
self.t = t
# 计算中间层通道数
self.hidden_channel = in_channel * t
# 存放模型结构
layers = []
# 如果expansion ratio不为1,就利用1x1卷积进行升维
if self.t != 1:
# 添加conv+bn+relu6
layers += [conv_block(self.in_channel, self.hidden_channel, kernel_size=1)]
layers += [
# 添加conv+bn+relu6,此处使用组数等于输入通道数的 分组卷积实现 【depthwise conv】
conv_block(self.hidden_channel, self.hidden_channel, stride=self.stride, groups=self.hidden_channel),
# 添加1x1conv+bn,此处不再进行relu6
conv_block(self.hidden_channel, self.out_channel, kernel_size=1)[:-1]
]
# 倒置残差结构块
self.residul_block = nn.Sequential(*layers)
def forward(self, x):
# 如果卷积步长为1且前后通道数一致,则连接残差边
if self.stride == 1 and self.in_channel == self.out_channel:
# x + F(x)
return x + self.residul_block(x)
# 否则不进行残差连接
else:
# F(x)
return self.residul_block(x)
'''
定义MobileNet v2网络
'''
class MobileNetV2(nn.Module):
def __init__(self, num_classes):
super(MobileNetV2, self).__init__()
# 类别数量
self.num_classes = num_classes
# 特征提取部分
self.feature = nn.Sequential(
# conv_block(3, 32, strid=2), # conv+bn+relu6,(n,3,224,224)-->(n,32,112,112)
conv_block(1, 32, stride=2), # conv+bn+relu6,(n,1,224,224)-->(n,32,112,112)
InvertedResidual(32, 16, stride=1, t=1), # inverted residual block,(n,32,112,112)-->(n,16,112,112)
InvertedResidual(16, 24, stride=2), # inverted residual block,(n,16,112,112)-->(n,24,56,56)
InvertedResidual(24, 24, stride=1), # inverted residual block,(n,24,56,56)-->(n,24,56,56)
InvertedResidual(24, 32, stride=2), # inverted residual block,(n,24,56,56)-->(n,32,28,28)
InvertedResidual(32, 32, stride=1), # inverted residual block,(n,32,28,28)-->(n,32,28,28)
InvertedResidual(32, 32, stride=1), # inverted residual block,(n,32,28,28)-->(n,32,28,28)
InvertedResidual(32, 64, stride=2), # inverted residual block,(n,32,28,28)-->(n,64,14,14)
InvertedResidual(64, 64, stride=1), # inverted residual block,(n,64,14,14)-->(n,64,14,14)
InvertedResidual(64, 64, stride=1), # inverted residual block,(n,64,14,14)-->(n,64,14,14)
InvertedResidual(64, 64, stride=1), # inverted residual block,(n,64,14,14)-->(n,64,14,14)
InvertedResidual(64, 96, stride=1), # inverted residual block,(n,64,14,14)-->(n,96,14,14)
InvertedResidual(96, 96, stride=1), # inverted residual block,(n,96,14,14)-->(n,96,14,14)
InvertedResidual(96, 96, stride=1), # inverted residual block,(n,96,14,14)-->(n,96,14,14)
InvertedResidual(96, 160, stride=2), # inverted residual block,(n,96,14,14)-->(n,160,7,7)
InvertedResidual(160, 160, stride=1), # inverted residual block,(n,160,7,7)-->(n,160,7,7)
InvertedResidual(160, 160, stride=1), # inverted residual block,(n,160,7,7)-->(n,160,7,7)
InvertedResidual(160, 320, stride=1), # inverted residual block,(n,160,7,7)-->(n,320,7,7)
conv_block(320, 1280, kernel_size=1) # conv+bn+relu6,(n,320,7,7)-->(n,1280,7,7)
)
# 分类部分
self.classifier = nn.Sequential(
# avgpool,(n,1280,7,7)-->(n,1280,1,1)
nn.AdaptiveAvgPool2d(1),
# 1x1conv,(n,1280,1,1)-->(n,num_classes,1,1),等同于linear
nn.Conv2d(1280, self.num_classes, 1, 1, 0)
)
def forward(self, x):
# 提取特征
x = self.feature(x)
# 分类
x = self.classifier(x)
return x.view(-1, self.num_classes) # 压缩不需要的维度,返回分类结果,(n,num_classes,1,1)-->(n,num_classes)
if __name__ == '__main__':
net = MobileNetV2(num_classes=10)
X = torch.rand(size=(1, 1, 224, 224), dtype=torch.float32)
for layer in net.feature:
X = layer(X)
print(layer.__class__.__name__, 'output shape:', X.shape)
print()
for layer in net.classifier:
X = layer(X)
print(layer.__class__.__name__, 'output shape:', X.shape)
2.2 读取Fashion-MNIST数据集
其他所有的函数,与经典神经网络(1)LeNet及其在Fashion-MNIST数据集上的应用完全一致。
# 我们将图片大小设置224×224
# 训练机器内存有限,将批量大小设置为64
batch_size = 64
train_iter,test_iter = get_mnist_data(batch_size,resize=224)
2.3 在GPU上进行模型训练
from _09_MobileNetV2 import MobileNetV2
# 初始化模型,并设置为10分类
net = MobileNetV2(num_classes=10)
lr, num_epochs = 0.1, 10
train_ch(net, train_iter, test_iter, num_epochs, lr, try_gpu())