mobilenet结构详解
这里写目录标题
mobilenet简介
mobilenet网路是一种轻量级网络,专门给嵌入式设备而设计,它可以稍微降低准确率的情况下大大缩减模型的参数量。因为现在很多网络的参数量巨大,比如resnet152模型参数量就有600多兆,无法使用到嵌入式设备。
mobilenetv1
模型亮点
mobilenetv1网络是由谷歌团队在2017年提出的,这个网络有两个亮点:
1、使用了depthwise conv(dw)和pointwise conv(pw)两个卷积结构
2、使用了两个超参数(α和β)α:控制卷积核个数 β:控制输入图像分辨率
dw和pw
1.什么是dw卷积呢?
看下图 此图片是 霹雳吧啦Wz up主视频中的图片,如果不让使用请联系删除
如图所示,其采用卷积核的通道数是1,卷积核的个数=输入图片的通道数=输出特征图的通道数
2.pw卷积是什么呢?
如图就是pw卷积,它和普通的卷积操作相同,只不过其卷积核的大小是11,
dw卷积核pw卷积就组合成了我们的深度可分离卷积,下面我们看看两个操作所的参数量对比。
大家都知道,参数和输入输出特征图的大小是没有关系的,只和其特征图的深度,和卷积核的大小和深度有关,比如一个普通卷积,
到这里,基础不是很好的同学要拿出你的笔,稍微记录画一下我下面叙述的内容,这样你就便于理解了,想象力非常强可以忽略哦,比如:
输入特征图是28283
卷积核个数:4
卷积核大小:333
则在此层的参数量是3334=108
如果采用深度可分离卷积:
首先dw卷积:输入图像28283
卷积核:331
卷积核个数:3
输出特征图:28283
首先dw卷积的参数量:1333=9
然后是pw卷积:
输入特征图:28283
卷积核:113
卷积核个数:4
pw卷积的参数量1134=12
dw+pw=9+12=21
大致是3-4倍左右,我这里是简单的举了一个例子,在真实情况中比这个要大,因为我这里数值太小,影响了
α和β
第二个亮点就是设置了α和β两个参数,第一个是卷积核个数,第二个是输入图片大小。
这里给出v1网络的性能对比,
模型整体结构
这个是mobilenetv1的网络结构图了,一个conv dw和一个conv组成了一个深度可分离卷积
在研究过程中呢?很多的dw卷积的卷积核很多的参数接近于0,直接就废掉了,为了解决这个问题,我们产生了mobilenetv2版本
mobilenetv2
它是谷歌团队在2018年提出的,
模型亮点
他的亮点主要有两个:1.提出了inverted residual block(倒残差结构)和bottleneck
inverted residual block
直接上图,如图所示,第一个是残差结构,两边大中间小的一种瓶颈结构,在resnet中提出,我们这里不做详细介绍,第二个是到残差结构,两边小中间大,其先使用11的卷积核对特征图进行升维,然后用33的卷积核对特征图进行卷积提取特征,这里用到的卷积是dw卷积,我们在上边已经提到过,最后再使用1*1的卷积核进行降维,其中升维和降维的意思就是特征图的深度。
激活函数
在一般的卷积中使用的是relu激活函数,但是在mobilenetv2中提出,relu激活函数会对低维特征造成巨大的信息损失,所以本文提出了使用relu6激活函数。
bottlenect
下面这三个图显示了mobilenetv2的网络结构,下面我们进行分析
上面这个图和下面这个图是配套的,首先我们先用11的卷积进行升维,卷积得到特征图的深度是由t 这个系数进行控制,在总的结构图中也给出了t在各个层的取值,然后再进行33的dw卷积操作来提取特征, 这个图片上标的s=s,s就是步长的意思,所以其将特征图大小压缩了S,因为是dw卷积,所以其特征图的维度没有发生改变,最后一个是1*1卷积,对特征图进行降维操作,最后使用的是线性激活,不是relu激活,这三步就组成了我们mobilenet网络的基础bottlenect,这个还不是完全的,还差残差连接。
下边这个图片就是一个bottlenect,bottlenect中是有残差结构的,其实残差结构就是将卷积结果和输入直接相加,而不是堆叠,如果是直接相加的话,需要满足一定的条件才能相加,你想:如果两个特征图相加,是不是特征图的大小和维度相同才能让两个特征图中的数据对位相加,如果大小,或者维度有一个不是相等的,计算机就无法运算了,这里就是这个道理,进行卷积后必须要特征图大小和通道数相等才能相加。
网络的整体架构
下表就是mobilenetv2的网络结构图,都是由很多个bottlenect组成,
这里就详细叙述一下这个表:表中的t:就是我们刚才那个bottlenect表中的
t,就是卷积核个数(输出特征图深度)的扩增倍数,
c:就是卷积出来的特征图的通道数,或者该层卷积核的个数,
n:bottlenect的重复次数,
s:string,卷积核移动的步长,
还有需要提醒的s步长只针对第一层,其他的均为1,因为只在第一层会改变特征图的大小
在pytorch的torchvision中实现了mobilenetv2版本,有兴趣的同学可以看一下,
import torchvision.models.mobilenet
我下载的是torchvision比较老的版本了,不知道那些版本对这个为了完善,有看了的小伙伴可以跟我说一下,顺便告诉我你下载的是torchvision的版本,我也该一下,哈h
最后贴出来mobilenetv2的性能对比图,第一个是在分类中的性能,第二个是在目标检测领域中的性能。
代码
from torch import nn
import torch
from torchsummary import summary
__all__ = ['MobileNetV2', 'mobilenet_v2']
model_urls = {
'mobilenet_v2': 'https://download.pytorch.org/models/mobilenet_v2-b0353104.pth',
}
def _make_divisible(v, divisor, min_value=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
:param v:
:param divisor:
:param min_value:
:return:
"""
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
class ConvBNReLU(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1):
padding = (kernel_size - 1) // 2
super(ConvBNReLU, self).__init__(
nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False),
nn.BatchNorm2d(out_planes),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# pw
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1))
layers.extend([
# dw
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV2(nn.Module):
def __init__(self, num_classes=1000, width_mult=1.0, inverted_residual_setting=None, round_nearest=8):
"""
MobileNet V2 main class
Args:
num_classes (int): Number of classes
width_mult (float): Width multiplier - adjusts number of channels in each layer by this amount
inverted_residual_setting: Network structure
round_nearest (int): Round the number of channels in each layer to be a multiple of this number
Set to 1 to turn off rounding
"""
super(MobileNetV2, self).__init__()
block = InvertedResidual
input_channel = 32
last_channel = 1280
if inverted_residual_setting is None:
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],
]
# only check the first element, assuming user knows t,c,n,s are required
if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 4:
raise ValueError("inverted_residual_setting should be non-empty "
"or a 4-element list, got {}".format(inverted_residual_setting))
# building first layer
input_channel = _make_divisible(input_channel * width_mult, round_nearest)
self.last_channel = _make_divisible(last_channel * max(1.0, width_mult), round_nearest)
features = [ConvBNReLU(3, input_channel, stride=2)]
# building inverted residual blocks
for t, c, n, s in inverted_residual_setting:
output_channel = _make_divisible(c * width_mult, 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
# building last several layers
features.append(ConvBNReLU(input_channel, self.last_channel, kernel_size=1))
# make it nn.Sequential
self.features = nn.Sequential(*features)
# building classifier
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(self.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 = x.mean([2, 3])
x = self.classifier(x)
return x
def mobilenet_v2(pretrained=False, progress=True, **kwargs):
"""
Constructs a MobileNetV2 architecture from
`"MobileNetV2: Inverted Residuals and Linear Bottlenecks" <https://arxiv.org/abs/1801.04381>`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
model = MobileNetV2(**kwargs)
return model
# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# m=mobilenet_v2().to(device)
# summary(m, input_size=(3, 416, 416))
mobilenetv3
文章亮点
1)互补搜索技术组合:由资源受限的NAS执行模块级搜索,NetAdapt执行局部搜索。
2)网络结构改进:将最后一步的平均池化层前移并移除最后一个卷积层,引入h-swish激活函数。
MobileNetV3的 block
和之前的mobilenetv2的区别是在v3中加入了注意力机制,并且激活函数也有所区别,综合一下v3就是使用了注意力机制,倒残差结构,深度可分离卷积三点相结合。
互补搜索组合技术
(1)资源受限的NAS(platform-aware NAS):计算和参数量受限的前提下搜索网络的各个模块,所以称之为模块级的搜索(Block-wise Search)。
(2)NetAdapt:用于对各个模块确定之后网络层的微调。
对于模型结构的探索和优化来说,网络搜索是强大的工具。研究人员首先使用了神经网络搜索功能来构建全局的网络结构,随后利用了NetAdapt算法来对每层的核数量进行优化。对于全局的网络结构搜索,研究人员使用了与Mnasnet中相同的,基于RNN的控制器和分级的搜索空间,并针对特定的硬件平台进行精度-延时平衡优化,在目标延时(~80ms)范围内进行搜索。随后利用NetAdapt方法来对每一层按照序列的方式进行调优。在尽量优化模型延时的同时保持精度,减小扩充层和每一层中瓶颈的大小。
很多博主都是这样写的,应该是翻译的论文内容具体什么意思我也不是很清楚,应该就像名字说的一样,他的整个模型的拼接不是人为强制定义的,而是通过一种技术手段,让这个算法搜索出模型的最优搭配。这篇博文中详细描述了过程mobilenet v3
网络结构的改进
一些论文中比较关键的图片
网络结构在开头和结尾进行了改进,并且引入了SE模块,上边那篇博文讲的很清楚了,我就不做赘述了,最后贴上代码
import torch
import torch.nn as nn
import torch.nn.functional as F
__all__ = ['MobileNetV3', 'mobilenetv3']
def conv_bn(inp, oup, stride, conv_layer=nn.Conv2d, norm_layer=nn.BatchNorm2d, nlin_layer=nn.ReLU):
return nn.Sequential(
conv_layer(inp, oup, 3, stride, 1, bias=False),
norm_layer(oup),
nlin_layer(inplace=True)
)
def conv_1x1_bn(inp, oup, conv_layer=nn.Conv2d, norm_layer=nn.BatchNorm2d, nlin_layer=nn.ReLU):
return nn.Sequential(
conv_layer(inp, oup, 1, 1, 0, bias=False),
norm_layer(oup),
nlin_layer(inplace=True)
)
class Hswish(nn.Module):
def __init__(self, inplace=True):
super(Hswish, self).__init__()
self.inplace = inplace
def forward(self, x):
return x * F.relu6(x + 3., inplace=self.inplace) / 6.
class Hsigmoid(nn.Module):
def __init__(self, inplace=True):
super(Hsigmoid, self).__init__()
self.inplace = inplace
def forward(self, x):
return F.relu6(x + 3., inplace=self.inplace) / 6.
class SEModule(nn.Module):
def __init__(self, channel, reduction=4):
super(SEModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
Hsigmoid()
# nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y.expand_as(x)
class Identity(nn.Module):
def __init__(self, channel):
super(Identity, self).__init__()
def forward(self, x):
return x
def make_divisible(x, divisible_by=8):
import numpy as np
return int(np.ceil(x * 1. / divisible_by) * divisible_by)
class MobileBottleneck(nn.Module):
def __init__(self, inp, oup, kernel, stride, exp, se=False, nl='RE'):
super(MobileBottleneck, self).__init__()
assert stride in [1, 2]
assert kernel in [3, 5]
padding = (kernel - 1) // 2
self.use_res_connect = stride == 1 and inp == oup
conv_layer = nn.Conv2d
norm_layer = nn.BatchNorm2d
if nl == 'RE':
nlin_layer = nn.ReLU6 # or ReLU6
elif nl == 'HS':
nlin_layer = Hswish
else:
raise NotImplementedError
if se:
SELayer = SEModule
else:
SELayer = Identity
self.conv = nn.Sequential(
# pw 升维
conv_layer(inp, exp, 1, 1, 0, bias=False),
norm_layer(exp),
nlin_layer(inplace=True),
# dw 提取特征
conv_layer(exp, exp, kernel, stride, padding, groups=exp, bias=False),
norm_layer(exp),
SELayer(exp),
nlin_layer(inplace=True),
# pw-linear 降维
conv_layer(exp, oup, 1, 1, 0, bias=False),
norm_layer(oup),
)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV3(nn.Module):
def __init__(self, n_class=1000, input_size=224, dropout=0.8, mode='small', width_mult=1.0):
super(MobileNetV3, self).__init__()
input_channel = 16
last_channel = 1280
if mode == 'large':
# refer to Table 1 in paper
mobile_setting = [
# k, exp, c, se, nl, s,
[3, 16, 16, False, 'RE', 1],
[3, 64, 24, False, 'RE', 2],
[3, 72, 24, False, 'RE', 1],
[5, 72, 40, True, 'RE', 2],
[5, 120, 40, True, 'RE', 1],
[5, 120, 40, True, 'RE', 1],
[3, 240, 80, False, 'HS', 2],
[3, 200, 80, False, 'HS', 1],
[3, 184, 80, False, 'HS', 1],
[3, 184, 80, False, 'HS', 1],
[3, 480, 112, True, 'HS', 1],
[3, 672, 112, True, 'HS', 1],
[5, 672, 160, True, 'HS', 2],
[5, 960, 160, True, 'HS', 1],
[5, 960, 160, True, 'HS', 1],
]
elif mode == 'small':
# refer to Table 2 in paper
mobile_setting = [
# k, exp, c, se, nl, s,
[3, 16, 16, True, 'RE', 2],
[3, 72, 24, False, 'RE', 2],
[3, 88, 24, False, 'RE', 1],
[5, 96, 40, True, 'HS', 2],
[5, 240, 40, True, 'HS', 1],
[5, 240, 40, True, 'HS', 1],
[5, 120, 48, True, 'HS', 1],
[5, 144, 48, True, 'HS', 1],
[5, 288, 96, True, 'HS', 2],
[5, 576, 96, True, 'HS', 1],
[5, 576, 96, True, 'HS', 1],
]
else:
raise NotImplementedError
# building first layer
assert input_size % 32 == 0
last_channel = make_divisible(last_channel * width_mult) if width_mult > 1.0 else last_channel
self.features = [conv_bn(3, input_channel, 2, nlin_layer=Hswish)]
self.classifier = []
# building mobile blocks
for k, exp, c, se, nl, s in mobile_setting:
output_channel = make_divisible(c * width_mult)
exp_channel = make_divisible(exp * width_mult)
self.features.append(MobileBottleneck(input_channel, output_channel, k, s, exp_channel, se, nl))
input_channel = output_channel
# building last several layers
if mode == 'large':
last_conv = make_divisible(960 * width_mult)
self.features.append(conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish))
self.features.append(nn.AdaptiveAvgPool2d(1))
self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0))
self.features.append(Hswish(inplace=True))
elif mode == 'small':
last_conv = make_divisible(576 * width_mult)
self.features.append(conv_1x1_bn(input_channel, last_conv, nlin_layer=Hswish))
# self.features.append(SEModule(last_conv)) # refer to paper Table2, but I think this is a mistake
self.features.append(nn.AdaptiveAvgPool2d(1))
self.features.append(nn.Conv2d(last_conv, last_channel, 1, 1, 0))
self.features.append(Hswish(inplace=True))
else:
raise NotImplementedError
# make it nn.Sequential
self.features = nn.Sequential(*self.features)
# building classifier
self.classifier = nn.Sequential(
nn.Dropout(p=dropout), # refer to paper section 6
nn.Linear(last_channel, n_class),
)
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = x.mean(3).mean(2)
x = self.classifier(x)
return x
def _initialize_weights(self):
# 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)
if m.bias is not None:
nn.init.zeros_(m.bias)
def mobilenetv3(pretrained=False, **kwargs):
model = MobileNetV3(**kwargs)
if pretrained:
state_dict = torch.load('mobilenetv3_small_67.4.pth.tar')
model.load_state_dict(state_dict, strict=True)
# raise NotImplementedError
return model
if __name__ == '__main__':
net = mobilenetv3()
print('mobilenetv3:\n', net)
print('Total params: %.2fM' % (sum(p.numel() for p in net.parameters())/1000000.0))
input_size=(1, 3, 224, 224)
# pip install --upgrade git+https://github.com/kuan-wang/pytorch-OpCounter.git
# from thop import profile
# flops, params = profile(net, input_size=input_size)
# # print(flops)
# # print(params)
# print('Total params: %.2fM' % (params/1000000.0))
# print('Total flops: %.2fM' % (flops/1000000.0))
# x = torch.randn(input_size)
# out = net(x)
次代码是别人的,我对立面的一点小东西做了稍微改动,下面我注释掉的,你需要安装一个包才能使用。上面代码出自这位大佬