目录
参考资料:
论文:
SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size
博客:
一、前言
(1)模型轻量化优势
近年来,深度卷积神经网络(CNNs)的研究主要集中在 提高准确性
方面 ;对于 同等精度级别
,通常存在着多种 CNN 网络结构 ;但是在同样的精度下,更小的 CNN 至少有三方面的 优势
:
①
模型训练
:更小的 CNN 在分布式训练中需要更少的跨服务器通信 ;
②模型导出
:更小的 CNN 需要更少的带宽来将新模型从云导出到自动驾驶汽车 ;
③模型部署
:较小的 CNN 更适合部署在FPGA等内存有限的硬件上 ;
(2)模型压缩方法
①奇异值分解(singular value decomposition (SVD));
②网络剪枝(Network Pruning);
③深度压缩(Deep Compression);
从LeNet5到DenseNet,反应卷积网络的一个发展方向:提高精度。这里我们开始另外一个方向的介绍:在不大幅降低模型精度的前提下,最大程度的提高运算速度。
提高运算所读有两个可以调整的方向:
- 1.减少可学习参数的数量;
- 2.减少整个网络的计算量。
这个方向带来的效果是非常明显的:
- 1.减少模型训练和测试时候的计算量,单个step的速度更快;
- 2.减小模型文件的大小,更利于模型的保存和传输;
- 3.可学习参数更少,网络占用的显存更小。
SqueezeNet正是诞生在这个环境下的一个精度的网络,它能够在ImageNet数据集上达到AlexNet近似的效果,但是参数比AlexNet少50倍,结合他们的模型压缩技术Deep Compression,模型文件可比AlexNet小510倍。
二、网络结构详解
2.1 压缩策略
SqueezeNet的模型压缩使用了3个策略:
-
(1)将 3×3 卷积替换成 1×1 卷积:通过这一步,一个卷积操作的参数数量减少了9倍;
-
(2)减少 3×3 卷积的通道数:一个 3×3 卷积的计算量是 3×3×M×N (其中 M , N 分别是输入Feature Map和输出Feature Map的通道数),作者认为这样一个计算量过于庞大,因此希望将 M , N 减小以减少参数数量,利用
squeeze layers
实现 ; -
(3)将降采样后置:作者认为较大的Feature Map含有更多的信息,因此将降采样往分类层移动。注意这样的操作虽然会提升网络的精度,但是它有一个非常严重的缺点:即会增加网络的计算量。
策略(1)(2)用于减少CNN参数量,策略(3)用于在有限的参数量下最大化CNN准确率;
2.2 Fire Module
Fire Module 组成: 主要包括 挤压层(squeeze) 和 拓展层(expand) ;
squeeze
:只有 1 × 1 1 \times 1 1×1 卷积滤波器 ;expand
:混合有 1 × 1 1 \times 1 1×1 和 3 × 3 3 \times 3 3×3 卷积滤波器 ;
并引入了三个调节维度的 超参数
:
-
s
1
×
1
s_{1 \times 1}
s1×1:
squeeze
中 1 x 1 卷积滤波器个数 ; -
e
1
×
1
e_{1 \times 1}
e1×1 :
expand
中 1 x 1 卷积滤波器个数 ; -
e
3
×
3
e_{3 \times 3}
e3×3 :
expand
中 3 x 3 卷积滤波器个数 ;
使用Fire module的过程中,令s1x1s1x1 < e1x1e1x1 + e3x3e3x3,这样squeeze layer可以限制输入通道数量,即以上提到的策略(2)
2.3 SqueezeNet的网络架构
下图是SqueezeNet的几个实现,左侧是不加short-cut的SqueezeNet,中间是加了short-cut的,右侧是short-cut跨有不同Feature Map个数的卷积的。
其他细节:
- (1)为使 1 x 1 和 3 x 3 卷积滤波器的输出激活具有相同的高度和宽度,在 3 x 3 卷积滤波器操作中添加
padding=1
; - (2)使用 ReLU 函数作为挤压层(squeeze) 和 拓展层(expand)的激活函数 ;
- (3)在 Fire9 模块后应用了 50%的
Dropout
; - (4)在 SqueezeNet 中没有
全连接层
,设计灵感来源于 NiN 网络 ; - (5)训练 SqueezeNet 时,学习率设置为 0.04 开始,整个训练过程使用线性降低学习率 ;
2.4 SqueezeNet 结构探索
(1)微观结构探索
微观结构探索
( 每个模块层的维度和配置 ):
使用了以下超参数进行实验分析:
-
Squeeze Ratio(挤压比,SR)
: s 1 × 1 = S R ∗ ( e 1 × 1 + e 3 × 3 ) s_{1 \times 1} = SR * ( e_{1 \times 1} + e_{3 \times 3} ) s1×1=SR∗(e1×1+e3×3); -
Percentage of 3x3 filters
:在expand layer有1x1和3x3两种卷积,这里定义的参数是3x3卷积个占卷积总个数的比例;
(2)宏观结构探索
宏观结构探索
( 模块与其他层之间的高层次端到端结构 ):
作者通过前面所说到的三个结构对 SqueezeNet 的宏观结构进行了实验探索 ;
( SqueezeNet - 带简单旁路的 SqueezeNet - 带复杂旁路的 SqueezeNet )
作者使用了三种宏观架构对 SqueezeNet
进行了训练,并对它们的准确率和模型大小进行了对比 ;
有趣的是,简单旁路比复杂旁路具有更高的精度提高 ;
三、总结
SqueezeNet的压缩策略是依靠将 3×3 卷积替换成 1×1 卷积来达到的,其参数数量是等性能的AlexNet的2.14%。从参数数量上来看,SqueezeNet的目的达到了。SqueezeNet的最大贡献在于其开拓了模型压缩这一方向,之后的一系列文章也就此打开。
这里我们着重说一下SqueezeNet的缺点:
- SqueezeNet的侧重的应用方向是嵌入式环境,目前嵌入式环境主要问题是实时性。SqueezeNet的通过更深的深度置换更少的参数数量虽然能减少网络的参数,但是其丧失了网络的并行能力,测试时间反而会更长,这与目前的主要挑战是背道而驰的;
为什么会丧失网络的并行性?
SqueezeNet的Fire模块的两个分支的计算方式不同,在GPU并行计算两个分支时,运算量较小的分支会等待运算量较大的分支,于是丧失了网络的并行性,因为小分支的计算量小的优点无法体现出来。
- 论文的题目非常标题党,虽然纸面上是减少了50倍的参数,但是问题的主要症结在于AlexNet本身全连接节点过于庞大,50倍参数的减少和SqueezeNet的设计并没有关系,考虑去掉全连接之后3倍参数的减少更为合适。
- SqueezeNet得到的模型是5MB左右,0.5MB的模型还要得益于Deep Compression。虽然Deep Compression也是这个团队的文章,但是将0.5这个数列在文章的题目中显然不是很合适。
四、论文复现
参考:
(1)Fire模块:
class Fire(nn.Module):
def __init__(self, in_channels, squeeze_channels, expand1x1_channels, expand3x3_channels):
super(Fire, self).__init__()
self.in_channels = in_channels
# squeeze layers
self.squeeze = nn.Conv2d(in_channels, squeeze_channels, kernel_size=1)
self.squeeze_activation = nn.ReLU(inplace=True)
# expand layers - 1x1
self.expand1x1 = nn.Conv2d(squeeze_channels, expand1x1_channels,
kernel_size=1)
self.expand1x1_activation = nn.ReLU(inplace=True)
# expand layers -3x3
self.expand3x3 = nn.Conv2d(squeeze_channels, expand3x3_channels,
kernel_size=3, padding=1)
self.expand3x3_activation = nn.ReLU(inplace=True)
def forward(self, x):
x = self.squeeze_activation(self.squeeze(x))
e1 = self.expand1x1_activation(self.expand1x1(x))
e2 = self.expand3x3_activation(self.expand3x3(x))
out = torch.cat([e1, e2], 1)
return out
(2)主体网络结构:
class SqueezeNet(nn.Module):
def __init__(self, version='1_0', num_classes=1000):
super(SqueezeNet, self).__init__()
self.num_classes = num_classes
if version == '1_0':
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=7, stride=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(96, 16, 64, 64),
Fire(128, 16, 64, 64),
Fire(128, 32, 128, 128),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(256, 32, 128, 128),
Fire(256, 48, 192, 192),
Fire(384, 48, 192, 192),
Fire(384, 64, 256, 256),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(512, 64, 256, 256),
)
elif version == '1_1':
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(64, 16, 64, 64),
Fire(128, 16, 64, 64),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(128, 32, 128, 128),
Fire(256, 32, 128, 128),
nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
Fire(256, 48, 192, 192),
Fire(384, 48, 192, 192),
Fire(384, 64, 256, 256),
Fire(512, 64, 256, 256),
)
final_conv = nn.Conv2d(512, self.num_classes, kernel_size=1)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
final_conv,
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1))
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
out = torch.flatten(x, 1)
return out
def _squeezenet(version, **kwargs):
model = SqueezeNet(version, **kwargs)
return model
def squeezenet1_0(**kwargs):
return _squeezenet('1_0', **kwargs)
def squeezenet1_1(**kwargs):
return _squeezenet('1_1', **kwargs)
def test():
net = squeezenet1_0()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
if __name__ == '__main__':
test()