近期实习学习到了不少CNN的黑科技(其实也不能称为黑科技,只是一些常见的trick甚至是必须掌握的知识以前偷懒没掌握)。 利用本文一点点把学到的黑科技记录起来,方便自己以后回顾。
分类任务
CNN对于常见的分类任务,基本是一个鲁棒且有效的方法。例如,做物体分类的话,入门级别的做法就是利用caffe提供的alexnet的模型,然后把输出的全连接层稍稍修改称为自己想要的类别数,然后再根据实际需要修改网络模型(通常是瘦身)。下面记录几个point。
关于crop
一般在训练的时候会利用两个手段做data augmentation,分别是mirror和crop。其中,mirror没什么特别,但是crop有一些东西我们需要了解。
- 在训练的时候,crop操作会在大图上随机切小图,然后小图才是进入CNN的数据;而在测试的时候,crop操作是直接在大图中间取小图;
- 我们做前向的时候,可以考虑模拟训练时候的crop机制,多crop几份,然后每一个crop都前向一遍,然后综合考虑多个crop的前向结果;
- 上一点提到的多个crop类似于多次试验求平均的感觉;
- 多crop求平均的手法可以构成一个batch来一起走一次前向;
以上是比较普通的trick,仔细思考第四点的计算性能,我们会发现,这种做法对于一张完整的大图而言,有很多像素都被重复计算了。那么,下面介绍一种较为巧妙的思路(需要对卷积层以及对特征有比较深刻的理理解)。
- 依然用普通的方式训练一个CNN
- 一般我们的网络在最后面会加入全连接层把feature map压成一个一维向量,然后我们需要先理解一个说法:全连接层实际上是n个1*1的卷积核对上层feature进行卷积,然后在对卷积后的feature做一次均值pooling;
下面用两个简单的例子说明:
例1
<fc1> output: 9
<fc2> output: 1
[1 2 3 4 5 6 7 8 9]
[a b c d e f g h i]
得到
[1*a+2*b+3*c+...+9*i]
则需要的9个权值,每个权值一一对应fc1的值。那么假如,fc2换成卷积层,那么
<fc1> output:9
<conv2> output:1, kernel:(1,1)
[1 2 3 4 5 6 7 8 9]
[a b c d e f g h i]
得到
[1*a 2*b 3*c ... 9*i]
刚好也是需要9个权值,一一对应fc1。但是呢,这时候经过conv2的卷积,目前的feature实际上就保持了和fc1一致的形状。那么怎么把它变成我们想要的output=1呢?这时候就是一个均值加权的过程,即卷积后的九个值求加权平均得到真正的输出。
例2
理解了例1后,再来理解例2
<conv1> outputsize:(1,2,2) # 1通道,宽高各为2
<fc2> output: 2
1 2
3 4
a b e f
c d , g h
得到
[a*1+b*2+c*3+d*4, e*1+f*2+g*3+h*4]
这个网络需要的同样是4*2=8个权值,每4个一组分别对应展开conv1后的4个像素,共两组,故可以得到两个计算值。这时候,把网络变成
<conv1> shape:(1,2,2) # 1通道,宽高各为2
<conv2> kernel:(1,1), output:2
1 2
3 4
a b e f
c d , g h
得到两个特征矩阵
1a 2b 1e 2f
3c 4d , 3g 4h
实际上只需要分别把两个矩阵所有元素求和就可以得到与全连接一样的值。
回到正题,花了很多笔墨提出这个全连接层等同与1*1卷积核,是为了在前向时把全连接层替换掉。为什么呢?假设没有全连接层,实际上我们对于输入图片的大小是没有任何限制的,大图小图一样都经过卷积池化激活。这种网络有人称为全卷积网络(FCN)。
好,那么现在替换全连接层为卷积层之后,输入图片大小是任意的,那么意味着最后一层出来的feature不再是1*1,而可能是m*n。所以为了映射到分类任务的结果,把最后的featuremap做一下求和,然后送入softmax层,就得到了每个类别的可能性。
那么,为什么我将这部分内容放在“关于crop”这个标签下面呢?思考一下,假如用普通的crop策略,那么是不是相当于全卷积到最后一层只取出crop区域对应的特征图的点区域?全卷积是不是就相当于crop了全图的所有能crop的区域并融合在一起?(有点拗口)核心的思想是,CNN训练得到的是滤波器,本质上是对于某种特定的模式有响应,反之无响应,所以全卷积一直到最后一层,响应最强的特征图,就是对应的分类。
关于卷积
卷积计算在CPU上一般是需要采取预定的加速策略。目前见到的方法有以下几款。
- 采用blas库进行一定程度的加速,例如OpenBlas,Atlas,等
- 利用一些特定的系统的指令集进行加速。
- 把N*N卷积核拆分成1*N卷积核以及N*1卷积核
这里重点讲方法1的相关内容,方法2和方法3这里简单带过一下。方法2在移动端可以利用NEON指令集进行优化,NEON指令集一般是用于ARM芯片上,大体上是一个协同处理器,类似于能够多线程计算的感觉。方法3是一种有趣的理论,认为一次卷积使用N*N的核实际上完全等同于两次卷积分别使用1*N核和N*1核,这种方法减少了30%的计算量和参数储存量,但是基本不影响CNN的性能。没记错的话,具体可以看一篇《Rethinking the Inception Architecture for Computer Vision》
回到方法1。一次卷积过程如果完全不考虑优化速度的话,就是一个五层循环,时间复杂度为C * H * W * KH * KW
,其中C和H和W表示特征图的通道高宽,KH和KW则是卷积核的高宽。很可怕对吧。
那么哪个地方能加速呢?
考虑卷积这个运算,如下:
原图
a b c
d e f
g h i
卷积核
1 2 3
4 5 6
7 8 9
目标像素 = a1+b2+c3+d4+e5+f6+g7+h8+i9
是不是就相当于矩阵运算
矩阵A 矩阵B
a b c d e f g h i 1
2
3
4
5
6
7
8
9
到了这里,补充一个背景知识,Blas库可以通过并行计算以及指令集优化等等各种各样的方式加速矩阵乘法(具体原理暂时不懂,以后有机会补充)。那么,如果我们能够把所有卷积计算都转化成矩阵乘法,这里就可以利用blas库加速了。接下来思考的问题就是,如何把一张特征图转化成为矩阵? 回到例子
原图
a b c d
e f g h
i j k l
m n o p
卷积核
1 2 3
4 5 6
7 8 9
这里卷积核需要计算四次卷积,结果为一个2*2的矩阵。这里引入一个称为im2col的矩阵展开方法
原图
a b c d
e f g h
i j k l
m n o p
展开
a b c e f g i j k
b c d f g h j k l
e f g i j k m n o
f g h j k l n o p
卷积核也展开
1 2 3 4 5 6 7 8 9
那么让展开后的矩阵与卷积核展开的矩阵相乘,就可以把卷积运算转化为矩阵运算,然后用blas库加速。这里的im2col运算也非常简单,就是原图里面会被卷积核覆盖的区域拉成一个一维向量,然后下一行就是下一个卷积区域,如此类推。虽然这个展开会带来内存损耗(显然有大量重复元素),但是如果能够达到加速目的,我们还是会需要用上,说到底还是空间换时间的思想。
此处留空,以后上一段openblas计算卷积的代码。
以上。