经典网络复现之SqueezeNet

Fire-Module

摘要: 本文记录利用ImageNet数据集复现经典网络SqueezeNet的过程,并记录在大型数据集训练过程中需要考虑的问题。

为了增强对state of the art深度神经网络模型的认知,准备进行一系列模型的复现工作,使用ImageNet的大规模图像分类数据集,从头训练各个经典的神经网络模型,同时结合作者论文对训练过程中的技巧进行归纳总结,主要安排如下几部分内容:

  • ImageNet数据集及数据准备
  • AlexNet网络
  • VGG网络
  • GoogLeNet网络
  • ResNet网络
  • SqueezeNet网络

概述

SqueezeNet正如作者在论文题目中特别强调的那样,主要针对模型大小进行了深度优化,在保证模型精度没有太多损失的情况下,极大的缩小了所需模型的尺寸,为在嵌入式设备部署提供了强力支撑。SqueezeNet为我们裁剪模型和设计可在资源受限设备上运行的高性能模型提供了指导,特别是Fire Module的设计原理值得思考。

  • 论文:SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size,2016.
  • 代码链接:https://github.com/DeepScale/SqueezeNet
  • 数据集: ImageNet 2012 ILSRVC数据集(数据集的获取及准备详见ImageNet-DataSet
  • 计算框架: MxNet
  • 算力资源:AWS云主机p2.8xlarge:8个Tesla K80 GPU ($7.20/hour)
    • 总计训练时间1天左右
    • 每轮迭代时间:~1060秒
    • 在ImageNet数据集上的效果:

SqueezeNet

  • microarchitecture and macro architecture
    • 在设计深度网络架构的过程中,如果手动选择每一层的滤波器显得过于繁复。通常先构建由几个卷积层组成的小模块,再将模块堆叠形成完整的网络。定义这种模块的网络为CNN microarchitecture。
    • 与模块相对应,定义完整的网络架构为CNN macroarchitecture。在完整的网络架构中,深度是一个重要的参数。
  • 比如FPGA中只有10MB的片上内存空间,没有片下存储,对模型大小要求较高;ASICs也会有相同的需求;

设计原则

作者在论文中给出SqueezeNet的三条设计原则:

  • 尽量用1x1卷积替代3x3卷积,目的自然是降低参数量;
  • 降低输入到3x3卷积对象的channel数目,在SqueezeNet中是使用squeeze模块来实现的;
  • 尽量在网络后面层次中进行降采样,这样可以是卷积层获得更大的激活地图,从而获得更高的精度;

Fire Module

Fire-Module

Fire Module是本文的核心构件,思想非常简单,就是将原来简单的一层conv层变成两层:squeeze层+expand层,各自带上Relu激活层。Fire Moduel主要包括squeeze模块和expand模块,其中squeeze模块全部由1x1卷积组成,而expand模块由1x1卷积和3x3卷积组成:

  • squeeze模块中卷积核个数一定要小于expand模块中卷积核的个数,只有这样才能达到降参和压缩的目的;
  • squeeze中的1x1卷积起到了降维的效果;
  • 为了实现expand模块中1x1卷积和3x3卷积之后的数据能够concatenate在一起,对于输入3x3的对象需要增加1的zero-padding操作;
  • 在squeeze模块和expand模块之后通过ReLU进行激活;

模型架构及参数

architechtureofSqueezeNet

parameters-in-SqueezeNet

如图所示,

  • SqueezeNet中彻底放弃了全连接网络;
  • 作者论文中初始学习率很大,为0.04,实际实验中,发现这么大的学习率震荡太厉害,降为0.01;
  • 降维的操作尽量放在了模型后半段,为了满足第三条设计原则;
  • 参数方面:使用了8个Fire Module,每个Fire Module中,squeeze的卷积核数目是expand的1/8;
  • fire9之后采用了dropout,其中keep_prob=0.5;

分析结果

lab_results

如图所示,SqueezeNet模型本身是AlexNet的1/50,却可以获得相近的准确率,通过利用Deep Compression技术可以进一步压缩模型,实现1/510模型大小情况下的相同性能(当然实际中,由于Deep Compression采取了编码策略,需要编码本,会带来一定的额外开销)。

[扩展阅读]Deep Compression

  • Deep compression: Compressing DNNs with pruning, trained quantization and huffman coding, 2015
  • 常见的模型压缩技术
    • 奇异值分解(singular value decomposition (SVD))1
    • 网络剪枝(Network Pruning)2:使用网络剪枝和稀疏矩阵
    • 深度压缩(Deep compression)3:使用网络剪枝,数字化和huffman编码
    • 硬件加速器(hardware accelerator)4

重头训练一个SqueezeNet

  1. 使用MxNet构建AlexNet代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# import the necessary packages
import mxnet as mx

class MxSqueezeNet:
@staticmethod
def squeeze(input, numFilter):
# the first part of a FIRE module consists of a number of 1x1
# filter squeezes on the input data followed by an activation
squeeze_1x1 = mx.sym.Convolution(data=input, kernel=(1, 1),
stride=(1, 1), num_filter=numFilter)
act_1x1 = mx.sym.LeakyReLU(data=squeeze_1x1,
act_type="elu")

# return the activation for the squeeze
return act_1x1

@staticmethod
def fire(input, numSqueezeFilter, numExpandFilter):
# construct the 1x1 squeeze followed by the 1x1 expand
squeeze_1x1 = MxSqueezeNet.squeeze(input, numSqueezeFilter)
expand_1x1 = mx.sym.Convolution(data=squeeze_1x1,
kernel=(1, 1), stride=(1, 1), num_filter=numExpandFilter)
relu_expand_1x1 = mx.sym.LeakyReLU(data=expand_1x1,
act_type="elu")

# construct the 3x3 expand
expand_3x3 = mx.sym.Convolution(data=squeeze_1x1, pad=(1, 1),
kernel=(3, 3), stride=(1, 1), num_filter=numExpandFilter)
relu_expand_3x3 = mx.sym.LeakyReLU(data=expand_3x3,
act_type="elu")

# the output of the FIRE module is the concatenation of the
# activation for the 1x1 and 3x3 expands along the channel
# dimension
output = mx.sym.Concat(relu_expand_1x1, relu_expand_3x3,
dim=1)

# return the output of the FIRE module
return output

@staticmethod
def build(classes):
# data input
data = mx.sym.Variable("data")

# Block #1: CONV => RELU => POOL
conv_1 = mx.sym.Convolution(data=data, kernel=(7, 7),
stride=(2, 2), num_filter=96)
relu_1 = mx.sym.LeakyReLU(data=conv_1, act_type="elu")
pool_1 = mx.sym.Pooling(data=relu_1, kernel=(3, 3),
stride=(2, 2), pool_type="max")

# Block #2-4: (FIRE * 3) => POOL
fire_2 = MxSqueezeNet.fire(pool_1, numSqueezeFilter=16,
numExpandFilter=64)
fire_3 = MxSqueezeNet.fire(fire_2, numSqueezeFilter=16,
numExpandFilter=64)
fire_4 = MxSqueezeNet.fire(fire_3, numSqueezeFilter=32,
numExpandFilter=128)
pool_4 = mx.sym.Pooling(data=fire_4, kernel=(3, 3),
stride=(2, 2), pool_type="max")

# Block #5-8: (FIRE * 4) => POOL
fire_5 = MxSqueezeNet.fire(pool_4, numSqueezeFilter=32,
numExpandFilter=128)
fire_6 = MxSqueezeNet.fire(fire_5, numSqueezeFilter=48,
numExpandFilter=192)
fire_7 = MxSqueezeNet.fire(fire_6, numSqueezeFilter=48,
numExpandFilter=192)
fire_8 = MxSqueezeNet.fire(fire_7, numSqueezeFilter=64,
numExpandFilter=256)
pool_8 = mx.sym.Pooling(data=fire_8, kernel=(3, 3),
stride=(2, 2), pool_type="max")

# Block #9-10: FIRE => DROPOUT => CONV => RELU => POOL
fire_9 = MxSqueezeNet.fire(pool_8, numSqueezeFilter=64,
numExpandFilter=256)
do_9 = mx.sym.Dropout(data=fire_9, p=0.5)
conv_10 = mx.sym.Convolution(data=do_9, num_filter=classes,
kernel=(1, 1), stride=(1, 1))
relu_10 = mx.sym.LeakyReLU(data=conv_10, act_type="elu")
pool_10 = mx.sym.Pooling(data=relu_10, kernel=(13, 13),
pool_type="avg")

# softmax classifier
flatten = mx.sym.Flatten(data=pool_10)
model = mx.sym.SoftmaxOutput(data=flatten, name="softmax")

# return the network architecture
return model

几点注意点:

  1. 使用ELU替代了ReLU激活函数;在ImageNet数据集中,ELU表现效果由于ReLU;
  2. 3x3 expand中有个pad=(1,1)
  3. 最有使用的全局平均池化层,kernel=(13,13)
  4. 全局池化输出被flatten之后直接丢给softmax

SqueezeNet的TensorFLow实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class SqueezeNet(object):
def __init__(self, inputs, nb_classes=1000, is_training=True):
# conv1
net = tf.layers.conv2d(inputs, 96, [7, 7], strides=[2, 2],
padding="SAME", activation=tf.nn.relu,
name="conv1")
# maxpool1
net = tf.layers.max_pooling2d(net, [3, 3], strides=[2, 2],
name="maxpool1")
# fire2
net = self._fire(net, 16, 64, "fire2")
# fire3
net = self._fire(net, 16, 64, "fire3")
# fire4
net = self._fire(net, 32, 128, "fire4")
# maxpool4
net = tf.layers.max_pooling2d(net, [3, 3], strides=[2, 2],
name="maxpool4")
# fire5
net = self._fire(net, 32, 128, "fire5")
# fire6
net = self._fire(net, 48, 192, "fire6")
# fire7
net = self._fire(net, 48, 192, "fire7")
# fire8
net = self._fire(net, 64, 256, "fire8")
# maxpool8
net = tf.layers.max_pooling2d(net, [3, 3], strides=[2, 2],
name="maxpool8")
# fire9
net = self._fire(net, 64, 256, "fire9")
# dropout
net = tf.layers.dropout(net, 0.5, training=is_training)
# conv10
net = tf.layers.conv2d(net, 1000, [1, 1], strides=[1, 1],
padding="SAME", activation=tf.nn.relu,
name="conv10")
# avgpool10
net = tf.layers.average_pooling2d(net, [13, 13], strides=[1, 1],
name="avgpool10")
# squeeze the axis
net = tf.squeeze(net, axis=[1, 2])

self.logits = net
self.prediction = tf.nn.softmax(net)


def _fire(self, inputs, squeeze_depth, expand_depth, scope):
with tf.variable_scope(scope):
squeeze = tf.layers.conv2d(inputs, squeeze_depth, [1, 1],
strides=[1, 1], padding="SAME",
activation=tf.nn.relu, name="squeeze")
# squeeze
expand_1x1 = tf.layers.conv2d(squeeze, expand_depth, [1, 1],
strides=[1, 1], padding="SAME",
activation=tf.nn.relu, name="expand_1x1")
expand_3x3 = tf.layers.conv2d(squeeze, expand_depth, [3, 3],
strides=[1, 1], padding="SAME",
activation=tf.nn.relu, name="expand_3x3")
return tf.concat([expand_1x1, expand_3x3], axis=3)

训练SqueezeNet

训练用的脚本和前面几个网络一致。

几个注意点:

  1. batchSize = config.BATCH_SIZE * config.NUM_DEVICES; 使用k80 12GB先存,选择batchSzie=128;
  2. 优化算法使用SGD,初始学习率为1e-2,动量0.9,L2权重正则化参数0.0002;rescale参数尤为关键,根据批的大小放大梯度:rescale_grad=1.0 / batchSize;
  3. model中的ctx参数用于指定用于训练的GPU;
  4. 使用了Xavier进行参数初始化,与原模型略有不同,Xavier是目前CNN网络常采用的参数初始化方式;initializer=mx.initializer.Xavier();
  5. In Keras, the ELU activation uses a default α value of 1.0. • But in mxnet, the ELU α value defaults to 0.25.

学习率的控制

Epoch 学习率
1-64 1e-2
65-80 1e-3
81-89 1e-4
90-100 1e-5
  1. 控制每轮学习率修改的观察窗口要在10-15个epoch之后再下结论,确定该阶段验证集准确率饱和了再行降低学习率;
  2. 调整学习率 python train_alexnet.py --checkpoints checkpoints --prefix alexnet \ --start-epoch 50
  3. 在8个GPU上,每轮用时1000多秒;
  1. 第一遍训练采用1e-2的学习率训练75轮,发现65轮以后,验证集准确率已经不再增加;为此在65轮之后调整学习率为1e-3;

    0-86-accuracy

    0-86-loss

  2. 将学习率调整到1e-3之后,验证集准确率有大幅提升,代表调整有效,80轮之后再度饱和,降低学习率到1e-4;到89轮验证集结果如下:

    0-89-accuracy

    0-89-loss

  1. 进一步降低学习率到1e-5,发现整个网络没有性能提升,结束该批次参数训练过程。

实验

  1. 初始学习率的选择

lr=0.04

  1. BN的效果

    在SqueezeNet中没有效果

  2. ReLU vs ELU ?

    用ELU替代ReLU,提升1-2%的性能;

  3. 选取90轮的训练结果,在测试集数据上进行验证,结果如下:

    1
    2
    [INFO] rank-1: 55.44%
    [INFO] rank-5: 78.10%

结论

今天终于完成了所有预定神经网络的训练过程,虽然有些网络由于本身对网络结构的认知或者是实际训练过程中没有很好的把握节奏,导致训练效果不是十分理想,但从这次的训练过程中还是学到了很多东西。整个过程大概持续了半个月左右的时间,得益于AWS上8 GPU的资源,使得很多网络的训练比原想进度要快很多,但费用也是惊人,看了下账单,发生在虚拟机上的费用大概有1万3千多,主要由于8 GPU服务器要85元人民币每个小时的费用不是所有人都可以承受的起的。所以我一直认为短期内,深度学习还是很难真正产业化的一个泡沫,很难有应用业务可以抵消这个昂贵的训练成本。我这只是复现网络,真实训练一个产品级的网络、所需要测试的参数十倍百倍于此。

cost_AWS

付了昂贵的学费,学习的动力也更加充足,将几种网络的训练过程进行一下简单的横向比较,同时总结一下大型网络训练过程中注意的主要内容。

几种网络在训练过程的简单比较

网络名称 模型大小 每轮训练时间(8GPU Tesla K80) 总迭代数+训练时间 初始化参数选择 初始学习率 激活函数 最终测试集准确率(Top-1/Top-5)
AlexNet 239MB ~670s ~1.5天 Xavier 1e-2,动量0.9,L2 0.0005 ELU 60.20%/81.99%
VGG-16 529MB ~ ~10天 MSRA 1e-2,动量0.9,L2 0.0005 PReLU
GoogLeNet 28MB ~1200s ~2天 Xavier 1e-3,Adam,L2 0.0002 ReLU 69.58%/89.01%
ResNet 98MB ~3500s ~3天 MSRA 1e-1,动量0.9,L2 0.0001 ReLU 71.49%/89.96%
SqueezeNet 5MB ~1060s ~1.5天 Xavier 1e-2,动量0.9,L2 0.0002 ELU 55.44%/78.10%

训练心得

  1. 训练一个大型数据集的深度神经网络是个费时费力的活,为了获得最优的参数,一般需要进行10-100次参数实验,需要极大的耐心和计算资源;
  2. 原则:训练深度神经网络的目的不是找寻全局最优解,因为一般很难找到这个解,我们只是在探寻一个比上次效果更好的模型;
  3. 网络参数如何入手?
    1. 逆机器学习流程的思考:在机器学习算法的应用逻辑里,我们总是强调要快速的开始搭建第一个模型,设定测试的基准,然后再迭代更新模型。而训练大型神经网络,由于训练过程对时间和资源消耗十分巨大,为充分利用资源,切不可盲目起步;
    2. 由于可调参数终端,所以必须要想办法缩小可调参数范围,优先选择重要参数进行调试,同时通过查阅相似数据集文献中使用的参数来进一步缩小自己调试参数的范围;
  4. 初始学习率选择:
    1. 学习率参数是整个参数空间中最重要的一个,如果你只想调试一个参数,那么请选择它;
    2. 初始学习率需要根据网络模型的特点来决定,一般而言1e-2可能是一个合适的学习率,但不是对所有的网络都是最佳的学习率,比如ResNet支持更大的学习率,比如1e-1,GoogLeNet而言,1e-2有点太大,验证集准确率会发生比较大的震荡;
  5. 学习率控制:
    1. 初始学习率选择好以后,需要在第一次迭代中让网络多运行一段时间,观察验证集上的表现决定是否变更学习率,一般当验证集误差出现阻塞或者出现明显过拟合现象时,需要停止训练,调整学习率,从打断位置或前面位置继续训练;
    2. 学习率手段衰减一般采取对数衰减的策略,比如每次衰减十倍;
  6. 优化算法选择:
    1. 关于优化算法的选择,一般先从尝试经典的SGD算法、配合动量设置开始训练看网络是否能够有效学习参数空间信息;
    2. 进一步的可以调整为Adam来加快梯度的更新;
    3. 在大型网络训练中还经常使用梯度的rescale操作,根据batchsize大小,放大梯度:rescale_grad=1.0 / batchSize;
  7. 激活函数选择:先使用ReLU作为激活函数获得baseline,再选择提花为ELU来获得提升;
  8. 初始化参数选择:训练深层网络时,考虑使用Xavier,MSRA/HE的初始化参数,并配合PReLU一起使用,效果更好。
  9. 训练过程的控制:
    1. 训练过程要利用callback机制,随时记录训练参数、checkpoints;
    2. 通过Tensorboard等工具观察训练过程的精度、误差、损失函数变化情况;
    3. 有些网络需要预训练过程,通过预训练可以加速网络学习;
  10. 参数调优的其它技巧:
    1. BN
    2. DropOut
    3. Data Augmentation
    4. 计算框架:TensorFlow or MxNet
    5. 论文阅读与讨论

参考

  1. ImageNet Classification with Deep Convolutional Neural Networks

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×