|从零搭建网络| Mnasnet网络详解及pytorch搭建
文章目录
前言
在前段时间完成MobileNet系列网络的博客后觉得应该在加上一个Mnasnet的网络,原因是在学完MobileNetV2的情况下直接去接触MobileNetV3系列的网络也没那么容易理解V3系列网络中的部分创新点,但是在Mnasnet的网络中,对于那部分难以理解的创新点做了更为详尽的描述,并且在多个领域进行实践来证明了创新点的可行性,以下将从原论文及代码实现等方面详尽叙述Mnasnet网络。
Mnasnet详解
MnasNet和MobileNet系列网络一样都属于是一种针对移动和边缘设备设计的轻量级卷积神经网络(CNN)架构,由Google团队开发。该网络采用基于神经架构搜索(NAS)的方法,通过自动化搜索最优化的网络结构,以达到在移动设备上高效运行的目的。
首先我们从原论文摘要部分进行简单了解:
大致意思就是说谷歌团队采用基于神经架构搜索(NAS),通过搜索最优化的网络结构,创建了又一可在移动设备使用的轻量级网络,也就是Mnasnet。并且使用Mnasnet对多个视觉任务进行处理时,在准确率、延迟时间、计算量等多个性能评估中都取得了良好的成绩,最后经过试验证明,在创建Mnasnet网络的阶段,此网络是最为先进的移动CNN模型。
Mnasnet网络创新点
本篇博客的创新点只针对原论文中着重叙述的两个(多目标优化函数和神经架构搜索)进行解释,对于网络结构中涉及MobileNet系列网络的创新点(深度可分离卷积、倒残差结构、注意力机制等)不做详细解释,这方面有兴趣的话可以跳转到之前的博客:|从零搭建网络| MobileNet系列网络详解及搭建
在Mnasnet的网络中有两个比较重要的创新点,第一个是运用了多目标的优化函数(multi-objective function),这个优化函数兼顾了模型的准确率以及模型拟合的速度;第二个比较重要的创新点是上文中提到的神经架构搜索,旨在通过NAS搜索,找到一个强调模型延迟(推理时间)和准确性的平衡的网络结构。
多目标优化函数(multi-objective function)
在原论文中提到了一种多目标优化函数,他以模型最终的准确率以及速度作为基准,其中这里的速度为在移动设备上真实的推理时间。如下图:
下面我们来看一下这个优化函数:
在上述图片中,公式(2)为论文中提到的目标函数,其中‘ACC(m)’为准确率,‘T’则是一项硬指标,也就是一个我们事先设定好的一个常数;分子‘LAT(m)’是模型的预测时间,同时通过和我们事先设定好的T进行比较,从而定义‘w’的取值。
下图我们假设准确率(ACC(m)) = 0.5,硬指标(T) = 80ms来看一下分别采用软硬延迟的目标值。
其中上图α = 0,β = -1,表示在满足硬指标(LAT(m)≤T)时候,α = 0,即等于准确率本身。
所以当横轴LAT(m)小于硬指标的时候,此时目标函数只与准确率有关,所以为一个常数;而当LAT(m)大于硬指标时,w = β = -1,此时便会给目标函数一个显著的惩罚,目标函数急剧减小并且与LAT(m)的关系逐渐增大。
而在下图中α = -0.7,β = -0.7,则表示无论LAT是否超过硬指标,都不会改变w的大小,最后也会拟合成一个比较平滑的曲线,并不会对某些LAT的取值造成显著的惩罚。
并且这两种不同的取值也会分别对应着两种不同的结果,如下图:
从图中我们可以看出,当我们通过指定α和β的值,给超过硬指标的LAT(m)一个惩罚的话,模型的大部分目标都会仅存在与一部分舒适区;然而在惩罚并不那么显著的情况下,模型的目标分部则会更加的平均以及广泛,也就是说能够搜索到更多样的、不同组的帕雷托最优解。
分层的NAS搜索空间
在Mnasnet的网络的NAS搜索中,他更加地强调模型推理时间以及准确性的平衡,尤其是关注对于实际移动设备上的推理性能。
具体的作用可从下图中体现:
在原文中,他把一个完整的神经网络分为了七个Block,并且每一个Block的结构都是一样的,但是中间的连接都有所区别,所以我们可以具体地去设计每一个连接的结构。在每一个具体的layer中,都有经过NAS搜索的参数,图片中蓝色标注的参数都为AI搜索得到的最优解,例如图片右下方显示的卷积方式、卷积核大小、是否使用注意力机制、倒残差结构扩充的大小等等。
下面我们看一下NAS搜索的实验结果:
在上表中,‘Model’列的第一部分是人工设计的网络,下面则是通过搜索空间搜索参数,由AI自动设计的网络。从中我们可以看到,经过搜索空间搜索参数得到的网络模型,无论是在参数量、准确率还是计算量上面相较于人工设计的绝大多数模型都有着较好的性能。
上图展示的是文中两个创新点加持下的神经网络评估,我们可以从上表中看到,加入两个创新点后的Mnasnet网络在准确性以及推理时间上面都得到了很大的优化。
最后对于这个创新点我想表达的是NAS搜索无论是在算力还是经济成本上面消耗都是巨大的,也就是谷歌公司或者是一些资源比较多的的财大气粗的企业可以玩得起,所以说想在神经架构搜索领域有所作为或者是发表论文之类的,还是有那么一点其他方面的难度。
基于Pytorch的Mnasnet复现
在本部分中,我将从一维Mnasnet模型和二维Mnasnet模型两部分一一展现复现思路及代码,并且着重在二维复现中展现相关结构以及复现思路,一维复现更多是在二维复现的基础上进行某些方面的更改。下文中出现的MobileNet网络系列的创新点(深度可分离卷积、倒残差结构、注意力机制等)不会详细讲述,有兴趣可以移步我之前的博客:|从零搭建网络| MobileNet系列网络详解及搭建
二维Mnasnet复现
首先我们先来看下原论文中Mnasnet的结构图。
从上图左半部分的‘MnasNet-A1’结构图中可以看到,网络主要由七个Block进行构成,首先SepConv代表的是深度可分离卷积,MBConv6则是代表使用扩大因子为6的倒残差结构,SE代表使用注意力机制,(k3×3)则是使用核大小为3×3的卷积核进行卷积,每一个Block后面‘× ’代表这个Block需要重复几次;右半部分则是分别介绍了深度可分离卷积以及倒残差结构和注意力机制的详细结构。
首先由于每一个Block中的卷积都会带有一个批量归一化(BN)层,所以我们首先定义一种卷积规则,以避免后续出现代码冗余。
import torch
#定义卷积规则
class conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,
stride = 1,groups = 1,activation = True):
super().__init__()
self.activation = activation
self.padding = kernal_size // 2
self.con1 = torch.nn.Conv2d(input_channel,output_channel,kernal_size,
stride,padding=self.padding,groups=groups)
self.BN = torch.nn.BatchNorm2d(output_channel)
self.ReLu = torch.nn.ReLU()
def forward(self,x):
x = self.con1(x)
x = self.BN(x)
if self.activation == True:
x = self.ReLu(x)
return x
上述代码中定义的卷积规则会根据卷积核的大小适当调整填充,这样卷积层中具体的数据大小就仅有步长决定,有助于我们更好的借助结构图复现网络。
下面我们定义第一个Block,也就是深度可分离卷积层,它是由一个卷积核大小3×3的DW层和一个PW层组成。
#深度可分离卷积层
class Sepconv(torch.nn.Module):
def __init__(self,input_channel,output_channel,stride = 1):
super().__init__()
self.conv1 = conv(input_channel,3,input_channel,stride,groups=input_channel)
self.conv2 = conv(input_channel,1,output_channel,stride,activation=False)
def forward(self,x):
x = self.conv1(x)
x = self.conv2(x)
return x
接下来是MBconv的创建,但是由于它涉及到注意力机制,所以我们首先来定义注意力机制SE_block.
#注意力机制
class SE_block(torch.nn.Module):
def __init__(self,input_channel,ratio = 1):
super().__init__()
self.pool = torch.nn.AdaptiveAvgPool2d(1)
self.conv2 = torch.nn.Sequential(
torch.nn.Linear(input_channel,input_channel * ratio),
torch.nn.ReLU(inplace=True),
torch.nn.Linear(input_channel * ratio,input_channel),
torch.nn.Hardsigmoid(inplace=True)
)
def forward(self,x):
b,c,_,_ = x.shape
weight = self.pool(x)
weight = weight.view(b,c)
weight = self.conv2(weight)
weight = weight.view(b,c,1,1)
return x * weight
在注意力机制中,ratio是扩充的大小,并且在前向传播中,实际上求得是各个参数的权重,所以‘return’部分使用权重乘以原来的参数。
下面开始定义MBconv.
#倒残差结构
class MB_conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,
stride = 1,use_attention = False,t = 3):
super().__init__()
self.attention = use_attention
self.stride = stride
self.input_channel = input_channel
self.output_channel = output_channel
self.conv1 = conv(input_channel,1,input_channel * t)
self.conv2 = conv(input_channel * t,kernal_size,input_channel * t,
stride=stride,groups=input_channel)
self.conv3 = conv(input_channel * t,1,output_channel,activation=False)
self.SE = SE_block(input_channel * t)
def forward(self,x):
input = self.conv1(x)
input = self.conv2(input)
if self.attention == True:
input = self.SE(input)
input = self.conv3(input)
if self.stride == 1 and self.input_channel == self.output_channel:
input += x
return input
初始化中的t为扩充因子,目的是倒残差结构中扩充每层的大小。
最后开始搭建网络:
#Mnasnet
class Mnasnet(torch.nn.Module):
def __init__(self,input_channel,classes):
super().__init__()
self.feature = torch.nn.Sequential(
conv(input_channel,3,32,stride=2,activation=False),
Sepconv(32,16),
MB_conv(16,3,24,2,False,6),
MB_conv(24,3,24,1,False,6),
MB_conv(24,5,40,2,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,3,80,2,False,6),
MB_conv(80,3,80,1,False,6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80,3,112,1,True,6),
MB_conv(112,3,112,1,True,6),
MB_conv(112,5,160,2,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,3,320,1,False,6),
torch.nn.AdaptiveAvgPool2d(1)
)
self.classifier = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(320,160),
torch.nn.ReLU(),
torch.nn.Linear(160,classes)
)
def forward(self,x):
x = self.feature(x)
x = self.classifier(x)
return x
在倒残差结构中,每一个Block都有进行重复,但是重复时注意步长要变为‘1’,并且输入输出通道数不变。
下面是总体代码:
import torch
#定义卷积规则
class conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,
stride = 1,groups = 1,activation = True):
super().__init__()
self.activation = activation
self.padding = kernal_size // 2
self.con1 = torch.nn.Conv2d(input_channel,output_channel,kernal_size,
stride,padding=self.padding,groups=groups)
self.BN = torch.nn.BatchNorm2d(output_channel)
self.ReLu = torch.nn.ReLU()
def forward(self,x):
x = self.con1(x)
x = self.BN(x)
if self.activation == True:
x = self.ReLu(x)
return x
#深度可分离卷积层
class Sepconv(torch.nn.Module):
def __init__(self,input_channel,output_channel,stride = 1):
super().__init__()
self.conv1 = conv(input_channel,3,input_channel,stride,groups=input_channel)
self.conv2 = conv(input_channel,1,output_channel,stride,activation=False)
def forward(self,x):
x = self.conv1(x)
x = self.conv2(x)
return x
#注意力机制
class SE_block(torch.nn.Module):
def __init__(self,input_channel,ratio = 1):
super().__init__()
self.pool = torch.nn.AdaptiveAvgPool2d(1)
self.conv2 = torch.nn.Sequential(
torch.nn.Linear(input_channel,input_channel * ratio),
torch.nn.ReLU(inplace=True),
torch.nn.Linear(input_channel * ratio,input_channel),
torch.nn.Hardsigmoid(inplace=True)
)
def forward(self,x):
b,c,_,_ = x.shape
weight = self.pool(x)
weight = weight.view(b,c)
weight = self.conv2(weight)
weight = weight.view(b,c,1,1)
return x * weight
#倒残差结构
class MB_conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,
stride = 1,use_attention = False,t = 3):
super().__init__()
self.attention = use_attention
self.stride = stride
self.input_channel = input_channel
self.output_channel = output_channel
self.conv1 = conv(input_channel,1,input_channel * t)
self.conv2 = conv(input_channel * t,kernal_size,input_channel * t,
stride=stride,groups=input_channel)
self.conv3 = conv(input_channel * t,1,output_channel,activation=False)
self.SE = SE_block(input_channel * t)
def forward(self,x):
input = self.conv1(x)
input = self.conv2(input)
if self.attention == True:
input = self.SE(input)
input = self.conv3(input)
if self.stride == 1 and self.input_channel == self.output_channel:
input += x
return input
#Mnasnet
class Mnasnet(torch.nn.Module):
def __init__(self,input_channel,classes):
super().__init__()
self.feature = torch.nn.Sequential(
conv(input_channel,3,32,stride=2,activation=False),
Sepconv(32,16),
MB_conv(16,3,24,2,False,6),
MB_conv(24,3,24,1,False,6),
MB_conv(24,5,40,2,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,3,80,2,False,6),
MB_conv(80,3,80,1,False,6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80,3,112,1,True,6),
MB_conv(112,3,112,1,True,6),
MB_conv(112,5,160,2,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,3,320,1,False,6),
torch.nn.AdaptiveAvgPool2d(1)
)
self.classifier = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(320,160),
torch.nn.ReLU(),
torch.nn.Linear(160,classes)
)
def forward(self,x):
x = self.feature(x)
x = self.classifier(x)
return x
if __name__ == '__main__':
x = torch.randn(1,3,224,224)
model = Mnasnet(3,10)
y = model(x)
print(y.size())
一维Mnasnet复现
一维Mnasnet复现相较于二维的仅仅需要对网络中仅使用于二维的网络变成一维,同时注意力机制也需要相对变化。
首先来看注意力机制变化的地方:
#注意力机制
class SE_block(torch.nn.Module):
def __init__(self,input_channel,ratio = 1):
super().__init__()
self.pool = torch.nn.AdaptiveAvgPool1d(1)
self.conv2 = torch.nn.Sequential(
torch.nn.Linear(input_channel,input_channel * ratio),
torch.nn.ReLU(inplace=True),
torch.nn.Linear(input_channel * ratio,input_channel),
torch.nn.Hardsigmoid(inplace=True)
)
def forward(self,x):
b,c,_ = x.shape
weight = self.pool(x)
weight = weight.view(b,c)
weight = self.conv2(weight)
weight = weight.view(b,c,1)
return x * weight
在注意力机制中,因为涉及到数据的映射,所以需要把关于二维的映射变成一维。
下面是完整的一维复现:
import torch
#定义卷积规则
class conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,
stride = 1,groups = 1,activation = True):
super().__init__()
self.activation = activation
self.padding = kernal_size // 2
self.con1 = torch.nn.Conv1d(input_channel,output_channel,kernal_size,
stride,padding=self.padding,groups=groups)
self.BN = torch.nn.BatchNorm1d(output_channel)
self.ReLu = torch.nn.ReLU()
def forward(self,x):
x = self.con1(x)
x = self.BN(x)
if self.activation == True:
x = self.ReLu(x)
return x
#深度可分离卷积层
class Sepconv(torch.nn.Module):
def __init__(self,input_channel,output_channel,stride = 1):
super().__init__()
self.conv1 = conv(input_channel,3,input_channel,stride,groups=input_channel)
self.conv2 = conv(input_channel,1,output_channel,stride,activation=False)
def forward(self,x):
x = self.conv1(x)
x = self.conv2(x)
return x
#注意力机制
class SE_block(torch.nn.Module):
def __init__(self,input_channel,ratio = 1):
super().__init__()
self.pool = torch.nn.AdaptiveAvgPool1d(1)
self.conv2 = torch.nn.Sequential(
torch.nn.Linear(input_channel,input_channel * ratio),
torch.nn.ReLU(inplace=True),
torch.nn.Linear(input_channel * ratio,input_channel),
torch.nn.Hardsigmoid(inplace=True)
)
def forward(self,x):
b,c,_ = x.shape
weight = self.pool(x)
weight = weight.view(b,c)
weight = self.conv2(weight)
weight = weight.view(b,c,1)
return x * weight
#倒残差结构
class MB_conv(torch.nn.Module):
def __init__(self,input_channel,kernal_size,output_channel,stride = 1,use_attention = False,t = 3):
super().__init__()
self.attention = use_attention
self.stride = stride
self.input_channel = input_channel
self.output_channel = output_channel
self.conv1 = conv(input_channel,1,input_channel * t)
self.conv2 = conv(input_channel * t,kernal_size,input_channel * t,stride=stride,groups=input_channel)
self.conv3 = conv(input_channel * t,1,output_channel,activation=False)
self.SE = SE_block(input_channel * t)
def forward(self,x):
input = self.conv1(x)
input = self.conv2(input)
if self.attention == True:
input = self.SE(input)
input = self.conv3(input)
if self.stride == 1 and self.input_channel == self.output_channel:
input += x
return input
#Mnasnet
class Mnasnet(torch.nn.Module):
def __init__(self,input_channel,classes):
super().__init__()
self.feature = torch.nn.Sequential(
conv(input_channel,3,32,stride=2,activation=False),
Sepconv(32,16),
MB_conv(16,3,24,2,False,6),
MB_conv(24,3,24,1,False,6),
MB_conv(24,5,40,2,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,5,40,1,True,3),
MB_conv(40,3,80,2,False,6),
MB_conv(80,3,80,1,False,6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80, 3, 80, 1, False, 6),
MB_conv(80,3,112,1,True,6),
MB_conv(112,3,112,1,True,6),
MB_conv(112,5,160,2,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,5,160,1,True,6),
MB_conv(160,3,320,1,False,6),
torch.nn.AdaptiveAvgPool1d(1)
)
self.classifier = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(320,160),
torch.nn.ReLU(),
torch.nn.Linear(160,classes)
)
def forward(self,x):
x = self.feature(x)
x = self.classifier(x)
return x
if __name__ == '__main__':
x = torch.randn(1,1,224)
model = Mnasnet(1,10)
y = model(x)
print(y.size())
总结
以上就是个人对于Mnasnet网络的一些见解以及相关复现,本来是想贴上生成虚拟数据集、调用模型、初始化以及实例化等代码,但是本着不跑题的本质决定还是不在这里出现,需要这方面泛用模板的话可以到学长 @浩浩的科研笔记 Pytroch 自写训练模板适合入门版 包含十五种经典的自己复现的一维模型 1D CNN自取。
最后就是如果有不当的地方很欢迎指出。