本篇主要分析CV领域的主要代表性CNN网络结构,这些网络结构一般是在图像识别的任务上开发设计的,并迁移应用到其他的计算机视觉任务上。
对于计算机视觉而言,从简单到复杂一般包括:
- 图像识别
- 目标检测
- 图像分割
- 图像翻译/描述
所谓图像识别,就是识别一张图像的类别,比如说猫狗大战,图像识别是最基础的计算机视觉任务。对于图像识别任务中,待识别的照片类似于大头贴的形式,即目标基本覆盖整张图像的大部分区域。对于机器学习任务,网络的搭建以及模型的训练过程,实际上就是学习输入数据到标签之间的映射的过程,假设输入的数据为X
,对应的标签为Y
,网络模型为f(.)
,训练的过程就是为了找到合适的参数使得满足关系式$Y=f(x)$。
图像识别原理及技术
原理
人类识别图像的过程可以描述为:当看到一张图片时,我们的大脑会迅速感应到是否见过此图片或与其相似的图片。其实在“看到”与“感应到”的中间经历了一个迅速识别过程,这个识别的过程和搜索有些类似。在这个过程中,我们的大脑会根据存储记忆中已经分好的类别进行识别,查看是否有与该图像具有相同或类似特征的存储记忆,从而识别出是否见过该图像。
机器的图像识别实际和人对图像的识别方法类似:通过分类并提取重要特征而排除多余的信息来识别图像。机器所提取出的这些特征有时会非常明显,有时又是很普通,这在很大的程度上影响了机器识别的速率。总之,在计算机的视觉识别中,图像的内容通常是用图像特征进行描述。
技术过程
图像识别技术的过程分以下几步:信息的获取、预处理、特征抽取和选择、分类器设计和分类决策。其中这几个过程具体化到深度学习中的属于可以为
- 信息的获取-数据集的准备及制作
- 预处理:图像预处理及增强,统一图像尺寸,去除噪声,图像数据归一化等处理
- 特征抽取:卷积网络进行图像的卷积计算
- 分类器设计:选择何种分类损失计算函数,对于神经网络一般使用的是
softmax
- 分类决策:选择何种标准作为类别输出,一般为
top1
之所以选用图像识别作为其他CV的基础任务,是因为在最大规模的CV数据集ImageNet是图片识别数据,并且图片识别任务最简单,可以很好的向其他任务迁移。
CNN网络框架
在图中非常清晰的说明了对于图像分类任务的pipeline,实际上对于相关从业者或者工程实践而言,整个模型的设计主要集中在输入与输出之间,即图中的绿色括号范围。
目前而言,对于图像分类任务,公认的是使用卷积神经网络进行模型设计。这些年以来,逐步发展起来了Alexnet
,Vggnet
,GoogleNet/Inception
,Resnet
。模型的深度不断增加,正确率已在不断提升
AlexNet-GPU加速
2012年的Imagenet冠军,这个网络结构的提出具有非常重要的意义,首先验证了cnn
在复杂模型下的有效性,此外,使用gpu
实现训练使得训练时间大幅度缩减,为以后不断提出的cnn
和gpu
计算都是极大的推动。
网络结构如下
图中显示的是两块gpu
显卡的训练过程,将输入图片经过第一步的卷积之后得到的feature map
分割成两部分进行两块gpu
训练,对于目前来讲,已经可以实现单块显卡进行整个模型的训练。输入数据为227x227x3
,整个结构中使用了11x11,5x5,3x3
的卷积核,最大池化,全连接后面添加了droupout
策略抑制过拟合,并且使用的是relu
激活函数(之前使用的激活函数为sigmoid
或tanh
),优化方法为SGD+momentum
(带动量的随机梯度下降)。
一篇特别好的博客链接 https://www.learnopencv.com/number-of-parameters-and-tensor-sizes-in-convolutional-neural-network/
Vgg-小卷积更有效
vgg
是相比于Alexnet
的改进,使用的是多个的3x3
的卷积核代替了原来的11x11,5x5
卷积核,增加了网络的深度的同时,减小了模型的参数量。
对于一个5x5
的卷积对应的参数量为$25c^{2}$的参数量,其中$c$为输出入和输出的通道数;如果使用2个3x3
步长为1的卷积核,参数量为$2 \times (9c^{2})=18c^{2}$,从而降低了参数量。
- 一个
7x7
卷积的感受野$\Leftrightarrow$3个3x3
卷积步长为1- 参数量为 $49c^{2}$ $\rightarrow$ $3 \times 9c^{2}=27c^2$
Vgg 网络包含多种不同层数的结构,常用的有vgg16和vgg19
从图中可以看出,在vgg进行分块设计,每一个块内,使用的都是相同的卷积核及卷积数量,这种块结构block/modules
之后经常使用。模型在ImageNet数据集上的top-5
正确率为92.3%
GoogleNet/Inception-模型变宽一些
一般认为featuremap的通道数代表着模型的宽度。
googlenet
证明了网络的深度比宽度更加有效,使用更深的网络结构,更多的卷积,可以得到更好的效果。
由于vgg
的结构设计,其在内存和时间上的计算要求较高,并且由于卷积层的通道数过多,vgg
并不是足够高效。加入对于一个conv_3-512
对应的计算量为3x3x512
,计算量很大,此外在vgg
的结构设计中多个conv_3-512
相连构成block
,会带来大量计算。
googlenet
基于的理念是:在深度网络中大部分的激活值是没有必要的,或者由于相关性是冗余的,因此最高效的神经网络架构应该是激活值之间是稀疏连接的,这就意味着在vgg
中的多个512的输出特征图之间是没有必要进行完全连接的。
基于此,**googlenet
提出了inception module
,使用了一种密集结构来近似一个稀疏连接,在整个模型中使用了多个inception module
进行连接从此构建了整个网络。只有很少一部分神经元真正有效,设计了1x1
卷积,在保持尺寸不变的同时对通道数进行改变,增加了特征的融合可能性,此外大幅度减少了参数量,可以使模型设计的更加深。**
此外,**googlenet
对原来的最后的全连接进行开了改进设计,使用了全局均值池化(在整个2D特征图上计算均值)代替了全连接层**,此举大幅度减少了模型的总参数量。相比vgg
准确率有所提升,但是速度相比更快,top-5
准确率为93.3%
ResNet-更深网络的可能性
随着模型的层数加深,模型的准确率应该是同步增加的,但是随着网络层数的增加,根据梯度反向传播的优化策略,在逐渐远离输出层时,会出现梯度逐渐变小甚至消失的情况,导致浅层处的网络无法进行参数更新导致学习失败,这就是梯度消失的问题;另一个问题就是,随着网络加深,意味着参数空间更大,优化问题更困难,因此如果简单的单纯增加网络深度有可能会出现更高的训练误差。
深度残差网络(deep residual network)正是针对深层次网络出现的梯度消失问题进行的网络结构改进设计。
对于传统的神经网络而言,图中左侧所示,输入x
经过两个网络层之后得到H(x)
,激活函数为relu
,在反向求倒时,在距离输入近的网络层要涉及多个网络层的导数进行交叉相乘,容易引起梯度消失。resnet
将网络结构进行了修改,既然距离输入更近,距离输出更远的层会容易出现梯度消失,何不将距离输出更远的层短接到距离输出更近的层上,如图中右侧所示,经过网络传输之后得到的输出变为$H(x)=F(x)+x$,如此以来$F(x)$被设计为拟合输入$x$与目标输出$H(x)$之间的残差$H(x)-x$,残差网络的名称也由此而来。设想如果某一层的输出已经可以很好的拟合期望的结果,那么在此之后再增加一层也不会是的网络变得更差,因为该层输出将直接被短接到两层之后,相当于把学习了一个恒等变换,而跳过的两层之间只需要拟合上层的输出和目标之间的残差即可,学习残差相比于直接拟合新的特征会更加容易。因此可以适应更深层次的网络的同时避免了梯度消失的情形。
resnet
主要使用的是3x3
卷积,与vgg
类似,在vgg
的基础上,使用短路连接插入进入形成残差网络,目前可以训练152深度的网络,并且相比传统网络结构正确率更好。
Densenet(2017)-参数再少一点
参考博客:CVPR 2017最佳论文作者解读:DenseNet 的“what”、“why”和“how”|CVPR 2017
强推tensorflow implemention : https://github.com/taki0112/Densenet-Tensorflow, https://github.com/xiaofengShi/Densenet-Tensorflow
Torch implementation: https://github.com/liuzhuang13/DenseNet/tree/master/models
PyTorch implementation: https://github.com/gpleiss/efficient_densenet_pytorch
MxNet implementation: https://github.com/taineleau/efficient_densenet_mxnet
Keras implemention: https://github.com/flyyufelix/DenseNet-Keras
非常棒的网络结构,论文中拿来和resnet
和inception
进行对比,设计了全新的结构,简单却有效。前面所述resnet
通过残差的设计实现了网络更深的可能性,解决了网络更深的时候的梯度消失问题;inception
探究了不同的inception module
使网络变得更宽。
在深度学习网络中,随着网络深度的加深,梯度消失问题会愈加明显,目前很多论文都针对这个问题提出了解决方案,比如ResNet
,Highway Networks
,Stochastic depth
,FractalNets
等,尽管这些算法的网络结构有差别,但是核心都在于:create short paths from early layers to later layers
。对于Densenet
延续这个思路,那就是在保证网络中层与层之间最大程度的信息传输的前提下,进行了极限连接,网络的每一层的输入都是前面所有层输出的并集,该层学习的特征图也会直接传给其他后面的所有层作为输入。
图中显示的是包含5个dense block
,假设第$l$层的变换函数为$H_{l}$(对应一组或两组的BN
,Relu
,conv
的操作),输出为$x_{l}$,对于第$l$层接受所有之前靠近输入端的特征图$x_{0},x_{1}…,x_{l-1}$作为输入,由此可以得到第$l$特征图的输出为
$$
x_{l}=H_{l}([x_{0},x_{1}…,x_{l-1}])
$$
其中$[x_{0},x_{1}…,x_{l-1}]$表示从层$0,1,…,l-1$之间的连接。
densenet
与resnet
和cnn
的对比如下所示
网络提出的启发
Stochastic depth
中训练过程中随机drop一些层,可以显著提高resnet的泛化性能,由此可以得出结论神经网络之间其实不一定是一个层级递进的结构,在整个模型结构中,某一层可以不仅仅依赖于近紧邻的上一层的特征,而是可以依赖更前面层学习到的特征。训练过程中,随机droup很多层也不会破坏算法的收敛性,这说明了深层的
resnet
有明显的冗余,残差只提取了层间的很少特征,既然每层学习的特征很少,能否降低它的计算量来减少冗余呢?
DenseNet
的设计正是基于此,
- 让每一层都与前面的层进行直接可连接,实现特征的重复利用
- 网络的每一层设计的很窄,只学习非常少的特征,以实现降低冗余。
第一点是第二点的前提,没有密集连接的话,不可能吧网络设计的很窄,不然网络无法收敛,造成欠拟合。
优点
- 参数减少,计算开销降低但是精度没变差
- 抗过拟合,
DenseNet
可以利用千层复杂度低的特征,更容易得到光滑的且具有更好泛华性能的决策函数
有可能的问题
- 密集连接带来冗余?:网库每层计算量的减少和特征的重复利用,
DenseNet
每层只学习很少的特征,使得参数量和计算量显著减少 - 耗费显存?:网络结构存在反复的拼接,将之前的输出和当前层拼接在一起传递给下一层,每次拼接都将会使用新的内存保存拼接够的特征,由此,一个
n
层的网络会消耗n(n+1)/2
层网络的内存
实现细节
- 每层开始的瓶颈层(1x1 卷积)对于减少参数量和计算量非常有用。
- 像
VGG
和ResNet
那样每做一次下采样(down-sampling
)之后都把层宽度(growth rate
) 增加一倍,可以提高DenseNet
的计算效率(FLOPS efficiency
)。 - 与其他网络一样,
DenseNet
的深度和宽度应该均衡的变化,当然DenseNet
每层的宽度要远小于其他模型。 - 每一层设计得较窄会降低
DenseNet
在GPU
上的运算效率,但可能会提高在CPU
上的运算效率
代码详解
参考链接
- densenet architecture
在每个densenet block
内部进行不同feature 的连接
1 | def Dense_net(self, input_x): |
Dense block
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# 模型核心代码
# 每个densenet block内部存在多个bottleneck层,并且多个在block内部
# 前面的bottleneck输出传递到后面的bottleneck中
def dense_block(self, input_x, nb_layers, layer_name):
with tf.name_scope(layer_name):
layers_concat = list()
layers_concat.append(input_x)
# 进入bottlebneck 层
# conv1-4n --> conv3-n
x = self.bottleneck_layer(input_x, scope=layer_name + '_bottleN_' + str(0))
layers_concat.append(x)
# layers_concat 包含
# 原始输入x
# bottleneck层计算之后的特征
# nb_layers 对应着denseblock内的包含多少个bottleneck
# 每增加一个bottleneck整个densenet block输出的层数增加
# 同时layers_concat保存当前bottleneck的输出并传递到下次计算中
for i in range(nb_layers - 1):
x = Concatenation(layers_concat)
x = self.bottleneck_layer(x, scope=layer_name + '_bottleN_' + str(i + 1))
layers_concat.append(x)
x = Concatenation(layers_concat)
return xBottleneck layer
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 瓶颈层
def bottleneck_layer(self, x, scope):
with tf.name_scope(scope):
x = Batch_Normalization(x, training=self.training, scope=scope+'_batch1')
x = Relu(x)
x = conv_layer(x, filter=4 * self.filters, kernel=[1,1], layer_name=scope+'_conv1')
x = Drop_out(x, rate=dropout_rate, training=self.training)
x = Batch_Normalization(x, training=self.training, scope=scope+'_batch2')
x = Relu(x)
x = conv_layer(x, filter=self.filters, kernel=[3,3], layer_name=scope+'_conv2')
x = Drop_out(x, rate=dropout_rate, training=self.training)
return xtransition layer
1
2
3
4
5
6
7
8
9
10# 两个densenet block之间的传递层,使用bn+relu+conv1+dropuout+avg_pool
def transition_layer(self, x, scope):
with tf.name_scope(scope):
x = Batch_Normalization(x, training=self.training, scope=scope+'_batch1')
x = Relu(x)
x = conv_layer(x, filter=self.filters, kernel=[1,1], layer_name=scope+'_conv1')
x = Drop_out(x, rate=dropout_rate, training=self.training)
x = Average_pooling(x, pool_size=[2,2], stride=2)
return x
应用
图像分割,图像翻译,目标检测任务,本人使用densenet实现过不定长字符的识别任务,效果相当不错,相比于传统的图像encode+RNN_decode的方式训练快速很多并且正确率一点不差。
SqueezeNet(2016) -移动平台运行
SqueezeNet
的设计初衷不是为了提高CNN
的精度,而是为了简化模型复杂度的同时能够达到可用的网络识别精度,在正确率和参数量之间进行平衡,方便能够在移动端运行。
设计原则
在模型设计时,主要从模型压缩,卷积核尺寸,下采样方面着手,并且设计了全新的模块Firemodel,这是squeezenet最显著的贡献。
模型压缩
主要采用的技术有
SVD
使用SVD奇异值分解,减少网络参数。
NetworkPruning
网络剪枝(network pruning
):在weight
中设置一个阈值,低于这个阈值就设为0,从而将weight
变成系数矩阵,可以采用比较高效的稀疏存储方式,进而降低模型大小
DeepCompression
DeepCompression,其包括网络剪枝,权重共享以及Huffman
编码技术。简单说一下权重共享,其实就是对一个weight
进行聚类,比如采用k-means
分为256
类,那么对这个weight
只需要存储256
个值就可以了,然后可以采用8 bit存储类别索引,其中用到了codebook
来实现。关于Deep Compression
详细技术可以参考文献[deep compression]。
Quantization
量化(quantization
): 对参数降低位数,比如从float32
变成int8
,这样是有道理,因为训练时采用高位浮点是为了梯度计算,而真正做inference
时也许并不需要这么高位的浮点,TensorFlow
中是提供了量化工具的,采用更低位的存储不仅降低模型大小,还可以结合特定硬件做inference
加速。
CNNKernel
替换$3 \times 3$卷积核为$1 \times 1$卷积核
延迟下采样
延迟下采样(downsample
),前面的layers
可以有更大的特征图,有利于提升模型准确度。目前下采样一般采用strides>1
的卷积层或者pool layer
FireModule
设计Fire Module
减少$3 \times 3$卷积的feature map
数量,卷积核的参数为(number of input channels) * (number of filters) * 3 * 3
示意图如下所示,也是squeezenet
的核心组件,设计思想是将原来的一层conv
变成两层
squeeze
层+relu
在
squeeze
中使用$1 \times 1$的卷积核,卷积核的数量为S11
,使用$1\times 1$的卷积核可以看做这一步是将当前输入的featuremap
在卷积核数量的维度上进行压缩,提取当前featuremap
的显著特征;expand
层+relu
在
expand
中存在$1 \times 1$和$3 \times 3$的卷积核,数量分别为E11
和E33
,将二者在维度的方向上进行拼接,并且保证S11<E11+E33
,这一步可以理解在featuremap上提取不同的感受野情况下的特征,并且如此设计减少了原来输入到下一层的feature map
的数量具体的示意图如下所示
举个🌰
假设输入为
H,W,C
,如果使用的是普通的卷积方式为
conv3-16
+conv3-128
,- 参数量为:
$$1633C+1283316=144*(128+C)=18432+144C$$
- 计算量为:
$$HW16*(33C)+HW128*(3316)=HW(18432+144C)$$
如果使用的是
FireModule
的卷积方式,conv1-16
+conv1-64
+conv3-64
- 参数量为:
$$1611C+641116+6433*16=10240+16C$$
- 计算量为(
expand
中的contact
计算为加法运算,可以忽略,只考虑乘法):
$$
\begin{align}
HW16*(11C)+HW64*(1116)&+HW64*(3316) \
&=HW(10240+16C)
\end{align}
$$
可以看到相比于普通卷积
FireModule
具有更少的参数和计算量
模型结构
整个模型实际上就是使用了FireModule
进行堆叠形成,网络结构如图所示。左侧是标准模式,最开始是一个卷积层,之后就是一堆FireModule
的堆叠,中间穿插着stride=2
的maxpool
进行下采样并采用了延迟策略,尽量使前面拥有较大的feature map
。中间和右侧结构是借鉴了resnet
引入了不同短路机制的squeezenet
。
下面说一下SqueezeNet
的一些具体的实现细节:
(1)在FireModule
模块中,expand
层采用了混合卷积核1x1
和3x3
,其stride
均为1,对于1x1
卷积核,其输出feature map
与原始一样大小,但是由于它要和3x3
得到的feature map
做concat
,所以3x3
卷积进行了padding=1
的操作,实现的话就设置padding=”same”;
(2)Fire
模块中所有卷积层的激活函数采用ReLU
;
(3)Fire9
层后采用了dropout
,其中keep_prob=0.5
;
(4)SqueezeNet
没有全连接层,而是采用了全局的avgpool
层,即pool size
与输入feature map
大小一致;
(5)训练采用线性递减的学习速率,初始学习速率为0.04。
模型效果
在论文中和
alexnet
进行了对比,squeezenet
在性能相同的情况下,模型的的参数量大为减少。为移动端部署提供了支持,本人在看到这个效果时,不禁对deep compression
的压缩方法产生了浓厚的兴趣,不自觉的爆了粗口:我靠,真的假的,接下来要试试自己的模型能否进行这种极限压缩。代码实现
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
60class 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)