【论文笔记】轻量级网络MobileNet
文中代码可能是错误示范,如需代码及论文,移步gitee
1. 论文
V1
主要思想:深度可分离卷积
V2
论文第3节
3.1Depthwise Separable Convolutions,讲述了深度可分离卷积由两部分组成(1)3x3 depthwise convolution (2)1x1 convolution。如果输入为h*w*d的张量,卷积核为k*k*d*n,产生的输出为h*w*n。标准卷积层的计算量为h*w*k*k*d*n,而深度可分离卷积的计算量为h*w*k*k*d+h*w*1*1*d*n=h*w*d*(k2+n);两者相差k2倍,k通常取3,因此计算量约为之前的1/9。
3.2 Linear Bottlenecks,发现当维度n很小的时候,后面接ReLU非线性变换的话会导致很多信息的丢失。使用linear bottleneck(即不使用ReLU激活,做了线性变换)的来代替原本的非线性激活变换。
3.3 Inverted residuals,借鉴resnet,先用1x1卷积,然后3x3卷积,再1x1卷积,同时输入输出shortcut连接。不用的是,mobilenet的3x3卷积是dw卷积,且1x1卷积时,是升维的。如下图所以,经过1x1卷积后,维度是之前的t倍。1x1卷积和dw卷积后都接relu,最后的1x1降维时,接线性变换。
V3
5.1 Redesigning Expensive Layers
重新设计两个地方花费比较大的地方:
1)V2最后用了1x1的卷积来增加维度,为了拥有更多的特征,但是会导致额外的延迟。(说的是bottleneck中3x3之后的那个1x1)将其放在avg pooling的后面,首先利用avg pooling将特征图大小由7x7降到了1x1,然后再利用1x1提高维度,这样就减少了7x7=49倍的计算量。删掉3x3以及1x1后,精度并没有得到损失。
2)最开始的地方用了32个3x3的卷积核来做最初的边缘检测。我们改为用16个,并且用hard swish的非线性层。
5.2 修改了swish非线性层
swish提高了精度,但对于移动设备sigmoid带来的计算量多很多。对于这个问题, 采取了以下两个操作:
- 用ReLU6(x+3)/6替换了sigmoid:
1)ReLU6可以在任何软硬件平台进行计算,
2)量化的时候,它消除了由于近似sigmoid的不同实现而导致的潜在数值精度损失。
3)在实践中,h-swish可以作为一个分段函数来实现,以减少内存访问的数量,从而大幅降低延迟成本。
- 只在下半部分使用h-swish
随着我们深入网络,应用非线性的成本会降低,因为每层激活内存通常在分辨率下降时减半。我们发现swish的大部分好处都是通过在更深层次上使用它们来实现的。因此,在我们的架构中,我们只在下半部分使用h-swish。
5.3 引入squeeze-and-excite bottleneck
因为SE结构会消耗一定的时间,所以作者在含有SE的结构中,将expansion layer的channel变为原来的1/4,这样既提高了精度,同时还没有增加时间消耗。
5.4. MobileNetV3 Definitions
MobileNetV3被定义为两种型号:MobileNetV3-large和MobileNetV3-small,分别针对高资源和低资源用例。这些模型是通过应用支持平台的NAS和NetAdapt进行网络搜索并结合本节中定义的网络改进而创建的。
2. V1代码实现:
2.1 深度可分离卷积
class Block(nn.Module): '''Depthwise conv + Pointwise conv''' def __init__(self,input_channels,output_channels,stride=1): super(Block,self).__init__() self.conv1 = nn.Conv2d\ (input_channels,input_channels,kernel_size=3,stride=stride, padding=1,groups=input_channels,bias=False) self.bn1 = nn.BatchNorm2d(input_channels) self.conv2=nn.Conv2d(input_channels,output_channels,kernel_size=1, stride=1,padding=0,bias=False) self.bn2=nn.BatchNorm2d(output_channels) def forward(self,x): x = F.relu(self.bn1(self.conv1(x))) x = F.relu(self.bn2(self.conv2(x))) return x
- Depthwise conv不改变通道数,outout_channels = input_channels, groups = input_channels。相当于对卷积的通道数进行分组,然后对每组的特征图分别进行卷积,是组卷积(group convolution)的一种扩展,每组只有一个特征图。
2.2 13个blocks组合:
...//cfg存放(输出通道数,stride) cfg = [64, (128,2), 128, (256,2), 256, (512,2), 512, 512, 512, 512, 512, (1024,2), 1024] ... def _make_layers(self,input_channels): layers = [] for x in self.cfg: output_channels = x if isinstance(x,int) else x[0] stride = 1 if isinstance(x,int) else x[1] layers.append(Block(input_channels,output_channels,stride)) input_channels=output_channels return nn.Sequential(*layers)
- isinstance的用法:https://www.runoob.com/python/python-func-isinstance.html
- 对于使用深度可分离卷积后的计算速度:https://zhuanlan.zhihu.com/p/80177088
3. V2代码实现
3.1 bottleneck
class Bottleneck(nn.Module): def __init__(self,input_channels,output_channels,stride=1,expansion_ratio=1): super(Bottleneck,self).__init__() self.stride = stride temp_channels = input_channels*expansion_ratio self.conv1 = nn.Conv2d(input_channels,temp_channels,kernel_size=1,bias=False) self.bn1 = nn.BatchNorm2d(temp_channels) self.conv2 = nn.Conv2d(temp_channels,temp_channels,padding=1,groups=temp_channels,kernel_size=3,stride=stride,bias=False) self.conv3 = nn.Conv2d(temp_channels,output_channels,kernel_size=1,bias=False) self.bn2 = nn.BatchNorm2d(output_channels) if(stride==1 and input_channels!=output_channels): self.shortcut = nn.Conv2d(in_channels=input_channels,out_channels=output_channels,kernel_size=1) if(stride==1 and input_channels==output_channels): self.shortcut = nn.Sequential() def forward(self,x): output = F.relu(self.bn1(self.conv1(x))) output = F.relu(self.bn1(self.conv2(output))) output = self.bn2(self.conv3(output)) if(self.stride==1): output = output+self.shortcut(x) return output
Tips:
1. 在stride=2时,不需要将输入与输出shortcut
2. 输入和输出的维数不同,无法叠加,可以用一个卷积改变输入通道的维数。(这是公认的方法吗?
3. 上面每一个卷积后都要加BN层(BN层添加在激活函数前,对输入激活函数的输入进行归一化。这样解决了输入数据发生偏移和增大的影响。
4. 关于nn.Conv2d的bias参数是false还是true?(卷积之后,如果要接BN操作,最好是不设置偏置,因为不起作用,而且占显卡内存。
3.2 多个bottleneck叠加
Tips:表中的n是该bottleneck重复的次数,如果重复多次,且stride=2的话,只有第一次的stride为2,以后都为1。
class MobileNetV2(BasicModule): cfg = [(24,2),24, (32,2),32,32, (64,2),64,64,64, 96,96,96, (160,2),160,160, 320] def __init__(self,num_classes): super(MobileNetV2, self).__init__() self.model_name = "MobileNetV2" self.conv1 = nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3,stride=2,padding=1) self.bottle = Bottleneck(input_channels=32,output_channels=16) self.layer = self._make_layers(input_channels=16) self.conv2 = nn.Conv2d(in_channels=320,out_channels=1280,kernel_size=1) self.conv3 = nn.Conv2d(in_channels=1280,out_channels=256,kernel_size=1) self.linear = nn.Linear(256,num_classes) self.bn1 = nn.BatchNorm2d(32) def _make_layers(self,input_channels): layers = [] for x in self.cfg: output_channels = x if isinstance(x,int) else x[0] stride = 1 if isinstance(x,int) else x[1] layers.append(Bottleneck(input_channels,output_channels,stride=stride,expansion_ratio=6)) input_channels = output_channels return nn.Sequential(*layers) def forward(self,x): x = F.relu(self.bn1(self.conv1(x))) x = self.bottle(x) x = self.layer(x) x = F.relu(self.conv2(x)) x = F.avg_pool2d(x,7) x = self.conv3(x) x = x.view(x.size(0), -1) x = self.linear(x) return x
参考文献:
1.
2. 轻量级模型:MobileNet V2 详解MobileNetV2