目录
参考资料:
论文:
MobileNetV2: Inverted Residuals and Linear Bottlenecks
博客:
1. 简介
MobileNet-v1
存在以下两个主要问题:
- (1)结构问题:MobileNet-v1的结构非常简单,类似于VGGNet,是一个非常复古的直筒结构。这种结构的性价比其实不高,后续一系列的ResNet、DenseNet等结构已经证明通过复用图像特征,使用Concat/Elwise+等操作进行融合,能极大提升网络的性价比。
- (2)Depthwise convolution存在的问题:
Depthwise convolution
确实是大大降低了计算量,并且Depthwise+Pointwise
的结构在性能上也能接近普通卷积。但是在实际应用时我们发现Depthwsie部分的kernel比较容易训废掉,训练完之后发现Depthwise训出来的kernel有不少是空的。因为depthwise每个kernel_dim相对于普通卷积要小得多,过小的kernel_dim加上ReLU的激活影响下,使得输出神经元很容易变为0,所以就学废了。ReLU对于0的输出梯度为0,所以一旦陷入0输出,就没法恢复了。
MobileNet-v2的主要思想就是在v1的基础上引入了线性瓶颈 (Linear Bottleneck)
和逆残差 (Inverted Residual)
来提高网络的表征能力,同样也是一种轻量级的卷积神经网络。
2. 网络结构
2.1 Linear Bottleneck(线性瓶颈层)
- Original bottlenecks:Elwise + with ReLU at end of a bottleneck
- Linear bottlenecks:Elwise + with No ReLU at the end of a bottleneck
我们在设计网络结构的时候,想要减少运算量,就需要尽可能将网络维度设计的低一些,但是维度如果低的话,ReLU激活函数可能会滤除很多有用信息。如下图所示:
- 如果当前空间维度较低,经过ReLU,可能会让激活空间坍塌,不可避免的会丢失信息。
- 如果经过ReLU变换输出是非零的,那输入和输出之间是做了一个线性变换的,即将输入空间中的一部分映射到全维输出,换句话来说,ReLU的作用是线性分类器。
论文针对这个问题在Bottleneck末尾使用Linear Bottleneck(即不使用ReLU激活,做了线性变换)来代替原本的非线性激活变换。实验证明,使用Linear Bottleneck可以防止非线性破坏太多信息。
ReLU让负半轴为0。本来参数就不多,学习能力就有限,这一下再让一些参数为0了,就更学不着什么东西了,干脆在elwise+那里不要ReLU了。
2.2 Inverted Residual(逆残差)
(a)Original residual block:reduce – transfer – expand
(中间窄两头宽)
Residual block先用1x1卷积降通道过ReLU,再3x3卷积过ReLU,最后再用1x1卷积过ReLU恢复通道,并和输入相加。之所以要1x1卷积降通道,是为了减少计算量,不然中间的3x3卷积计算量太大。所以Residual block是中间窄两头宽。
(b)Inverted residual block:expand – transfer – reduce
(中间宽两头窄)
在Inverted Residual block中,3x3卷积变成Depthwise了,计算量很少了,所以通道数可以多一点,效果更好,所以通过1x1卷积先提升通道数,再Depthwise3x3卷积,最后用1x1卷积降低通道数。两端的通道数都很小,所以1x1卷积升通道和降通道计算量都并不大,而中间的通道数虽然多,但是Depthwise 的卷积计算量也不大。
(c)具体应用:
在V2的网络设计中,我们除了继续使用深度可分离(中间那个)结构之外,还使用了Expansion layer和 Projection layer。
- Projection layer 也是使用 1×1 的网络结构,他的目的是希望把高维特征映射到低维空间。
另外说一句,使用 1×1 的网络结构将高维空间映射到低纬空间的设计有的时候我们也称之为Bottleneck layer。
- Expansion layer的功能正相反,使用 1×1 的网络结构,目的是将低维空间映射到高维空间。
这里Expansion有一个超参数是维度扩展几倍。可以根据实际情况来做调整的,默认值是6,也就是扩展6倍。
此图更详细的展示了整个模块的结构。我们输入是24维,最后输出也是24维。但这个过程中,我们扩展了6倍,然后应用深度可分离卷积进行处理。整个网络是中间胖,两头窄。Bottleneck residual block(ResNet论文中的)是中间窄两头胖,在MobileNetV2中正好反了过来,所以,在MobileNetV2的论文中我们称这样的网络结构为Inverted residuals。
需要注意的是Residual connection是在输入和输出的部分进行连接。另外,我们之前已经花了很大篇幅来讲Linear Bottleneck,因为从高维向低维转换,使用ReLU激活函数可能会造成信息丢失或破坏(不使用非线性激活数数)。所以在Projection convolution这一部分,我们不再使用ReLU激活函数而是使用线性激活函数。
2.3 ReLU6
卷积之后通常会接一个 ReLU
非线性激活,在 MobileNet 中使用 ReLU6
。ReLU6
就是普通的 ReLU 但是限制最大输出为 6,这是为了在移动端设备 float16/int8 的低精度的时候也能有很好的数值分辨率。
如果对 ReLU 的激活范围不加限制,输出范围为 0 到正无穷,如果激活值非常大,分布在一个很大的范围内,则低精度的 float16/int8 无法很好地精确描述如此大范围的数值,带来精度损失。
2.4 主体网络模块
下面谈谈为什么要构造一个这样的网络结构:
如果tensor维度越低,卷积层的乘法计算量就越小。那么如果整个网络都是低维的tensor,那么整体计算速度就会很快。然而,如果只是使用低维的tensor效果并不会好。如果卷积层的过滤器都是使用低维的tensor来提取特征的话,那么就没有办法提取到整体的足够多的信息。所以,如果提取特征数据的话,我们可能更希望有高维的tensor来做这个事情,V2就设计这样一个结构来达到平衡。
先通过Expansion layer来扩展维度,之后在用深度可分离卷积来提取特征,之后使用Projection layer来压缩数据,让网络重新变小。
因为Expansion layer 和 Projection layer都是有可以学习的参数,所以整个网络结构可以学习到如何更好的扩展数据和重新压缩数据。
2.5 整体网络结构
MobileNetV2的模型如下图所示:
- t t t 为Bottleneck内部升维的倍数;
- c c c 为通道数;
- n n n 为该Bottleneck重复的次数;
- s s s 为sride。
- k k k 为宽度缩放因子,其作用是在整体上对网络的每一层维度(特征数量)进行瘦身。在MobileNetV2中,当k<1时,最后的1*1conv不进行宽度缩放,否则进行宽度缩放。
注意:
(1)当stride=1时,才会使用elementwise 的sum将输入和输出特征连接(如下图左侧);stride=2时,无short cut连接输入和输出特征(下图右侧)。
(2)当n>1时(即该瓶颈层重复的次数>1),只在第一个瓶颈层stride为对应的s,其他重复的瓶颈层stride均为1;
(3)当n>1时,只在第一个瓶颈层升维,其他时候channel不变。(针对整个瓶颈层的维度,而不是瓶颈层内部的维度,内部可能先升后降。)
例如,对于该图中56x56x24的那层,共有3个该瓶颈层,只在第一个瓶颈层使用stride=2,后两个瓶颈层stride=1;第一个瓶颈层由于输入和输出尺寸不一致,因而无short cut连接,后两个由于stride=1,输入输出特征尺寸一致,会使用short cut将输入和输出特征进行elementwise的sum;
另外,只在第一个瓶颈层最后的1x1conv对特征进行升维,后两个瓶颈层输出维度不变(不要和瓶颈层内部的升维弄混了)。该层输入特征为56x56x24,第一个瓶颈层输出为28x28x32(特征尺寸降低,特征维度增加,无short cut),第二个、第三个瓶颈层输入和输出均为28x28x32(此时c=32,s=1,有short cut)。
3.训练细节
部分 | 设置 |
---|---|
使用工具 | TensorFlow |
训练器 | RMSPropOptimizer, decay and momentum都设置0.9 |
标准的权重衰减 | 4e-5 |
学习率 | 初始学习率为0.045,每个epoch后衰减0.98 |
batch_size | 16GPU内设置96 |
其他细节 | 每层后使用BN层 |
4.论文复现
参考:
MobileNetV2网络结构详解并获取网络计算量与参数量
使用pytorch搭建MobileNetV2并基于迁移学习训练
(1)确保Channel个数能被8整除
# ------------------------------------------------------#
# 这个函数的目的是确保Channel个数能被8整除。
# 很多嵌入式设备做优化时都采用这个准则
# ------------------------------------------------------#
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
# int(v + divisor / 2) // divisor * divisor:四舍五入到8
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
(2)定义带BN的卷积:卷积+BN+ReLu6
-
注意groups=1表示构建的是普通的卷积,如果groups等于in_channel,那么它就是DW卷积。
-
由于要使用BN层,因此bias是不使用的,设置为False
# -------------------------------------------------------------#
# Conv+BN+ReLU经常会用到,组在一起
# 参数顺序:输入通道数,输出通道数...
# 最后的groups参数:groups=1时,普通卷积;
# groups=输入通道数时,DW卷积=深度可分离卷积
# pytorch官方继承自nn.sequential,想用它的预训练权重,就得听它的
# -------------------------------------------------------------#
class ConvBNReLU(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super(ConvBNReLU, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
self.bn = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU6(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
(3)倒残差结构:
# ------------------------------------------------------#
# InvertedResidual,先变胖后变瘦
# 参数顺序:输入通道数, 输出通道数,步长,变胖倍数(扩展因子)
# ------------------------------------------------------#
class InvertedResidual(nn.Module):
def __init__(self, in_channels, out_channels, stride, factor):
super(InvertedResidual, self).__init__()
self.stride = stride
# 确保stride=1或2
assert stride in [1, 2]
# 所谓的隐藏维度,其实就是输入通道数*变胖倍数
hidden_dim = int(round(in_channels * factor))
# 只有同时满足两个条件时,才使用短连接
self.use_res_connect = self.stride == 1 and in_channels == out_channels
layers = []
# 如果扩展因子等于1,就没有第一个1x1的卷积层
if factor != 1:
layers.append(ConvBNReLU(in_channels, hidden_dim, kernel_size=1, bias=False)) # pointwise
# extend()函数用于在列表末尾一次性追加另一个序列中的多个值,即用新序列扩展原来的列表。
layers.extend([
# 3x3 depthwise conv,因为使用了groups=hidden_dim
ConvBNReLU(hidden_dim, hidden_dim, kernel_size=3, padding=1,
stride=stride, groups=hidden_dim, bias=False),
# 1x1 pointwise conv(linear),不使用激活函数ReLU6
nn.Conv2d(hidden_dim, out_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(out_channels),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
(4)主体网络结构:
- t t t 为Bottleneck内部升维的倍数,Expansion模块内部的一个超参数;
- c c c 为该层输出通道数;
- n n n 为该Bottleneck重复的次数;
- s s s 为sride。
# MobileNetV2是一个类,继承自nn.module这个父类
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
# 保证通道数是 8 的倍数,原因是:适配于硬件优化加速
input_channel = _make_divisible(32 * width_mult, round_nearest)
last_channel = _make_divisible(1280 * width_mult, round_nearest)
if inverted_residual_setting is None:
# t表示扩展因子(变胖倍数);c是通道数;n是block重复几次;
# s:stride步长,只针对第一层,其它s都等于1
inverted_residual_setting = [
# t, c, n, s
# 112,112,32 -> 112,112,16
[1, 16, 1, 1],
# 112,112,16 -> 56,56,24
[6, 24, 2, 2],
# 56,56,24 -> 28,28,32
[6, 32, 3, 2],
# 28,28,32 -> 14,14,64
[6, 64, 4, 2],
# 14,14,64 -> 14,14,96
[6, 96, 3, 1],
# 14,14,96 -> 7,7,160
[6, 160, 3, 2],
# 7,7,160 -> 7,7,320
[6, 320, 1, 1],
]
# 检查传入的配置是否正确
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))
# conv1 layer
# 224,224,3 -> 112,112,32
features = [ConvBNReLU(3, input_channel, kernel_size=3, stride=2, padding=1)]
# building inverted residual blocks
# t 为Bottleneck内部升维的倍数,Expansion模块内部的一个超参数;
# c 为该层输出通道数;
# n 为该Bottleneck重复的次数;
# s 为sride。
for t, c, n, s in inverted_residual_setting:
output_channel = _make_divisible(c * width_mult, round_nearest)
for i in range(n):
# -----------------------------------#
# s为1或者2 只针对重复了n次的bottleneck 的第一个bottleneck,
# 重复n次的剩下几个bottleneck中s均为1。
# -----------------------------------#
stride = s if i == 0 else 1
# 这个block就是上面那个InvertedResidual函数
features.append(block(input_channel, output_channel, stride, factor=t))
# 这一层的输出通道数作为下一层的输入通道数
input_channel = output_channel
# building last several layers
features.append(ConvBNReLU(input_channel, last_channel, kernel_size=1))
# *features表示位置信息,将特征层利用nn.Sequential打包成一个整体
self.features = nn.Sequential(*features)
# building classifier
# 自适应平均池化下采样层,输出矩阵高和宽均为1
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(last_channel, num_classes),
)
# 权重初始化
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 = self.avgpool(x)
x = torch.flatten(x, 1) # 展平处理
x = self.classifier(x)
return x
from thop import profile
def test():
# ------------------------------------#
# 方法1 获取计算量与参数量
# ------------------------------------#
net = MobileNetV2()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
def test2():
# ------------------------------------#
# 方法2 获取计算量与参数量
# ------------------------------------#
net = MobileNetV2()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
input = torch.randn(1, 3, 224, 224).to(device) # 1张3通道尺寸为224x224的图片作为输入
flops, params = profile(net, (input,))
print(flops, params)
if __name__ == '__main__':
# test()
test2()