[CAFFE笔记] Convolutional Layer I

神经网络

简介

在Convolutional Network(卷积神经网络)中,Convolutional Layer是最关键的层。Convolutional Layer可以被看作是一种滤波器。它通过一个设定大小的采集窗口采集某个像素点附近的像素的值,通过一个线性变换转换到输出矩阵的对应像素点上。

Convolutional Layer有两个重要的特征:

  • 稀疏连接
  • 共享参数

这两个特征也比较好理解,在对图像进行滤波操作(比如模糊操作)的时候,我们也是使用一个固定大小的窗口依次对图像的一个局部区域进行变换。当使用采样窗口时,输出像素点只取决于输入图像中其相应座标附近的像素点。也就是说,对任何一个输出点只需要连接到原图像的一个局部区域。这就是所谓的稀疏连接。另外,与滤波器类似,当我们对图像进行任何一种滤波变换时,我们理所当然地总是确保这个滤波变换函数对整个图像而言是相同的。否则,如果对部分区域进行锐化,而对另外的区域进行模糊,最终得到的图像肯定没有任何意义。要保证变换函数相同,也就是保证函数的参数相同。因此,对所有神经连接使用相同的权重和偏置也就变得理所当然了。

注意:本文只着重讲解卷积层的forwarding过程,不涉及backpropagate过程。主要的原因是我的研究任务没有涉及backpropagate,并且卷积层的backpropagate其实就只是forwarding的反向运算,并没有特别值得提及的地方。

参数

必要参数

  • num_output:滤波器的个数。在同一卷积层,我们可以设置多个不同的滤波器对同一输出图像进行特征提取。各个滤波器的参数相互独立,但连接方式相同。
  • kernel_size (或 kernel_h 和 kernel_w):窗口尺寸。这个参数定义了每个采集窗口的大小。如果使用kernel_size则定义的窗口为正方形。否则可以分别指定窗口的长宽。

推荐参数

  • weight_filter(默认类型:constant,默认值:0): 权重滤波器。

可选参数

  • bias_term(默认值:true):用于设定是否在滤波函数中加入偏置。
  • pad(或pad_h 和 pad_w,默认值:0):用于设定加在输入图像各方向上的额外像素
  • stride(或stride_h 和stride_w,默认值:1):窗口间距。用于设定两个相邻窗口的采样偏移量。如果stride=1,kernel_size=2,第一个采样窗口包含[00,01,10,11]四个点,而第二个窗口包含[01,02,11,12]四个点。
  • group(默认值:1):分组。如果这个值大于1,则第i组输入只会接入第i组输出。

Pad参数示例

Stride参数示例

CAFFE的卷积层forwarding实现

上面已经提到了,卷积层需要大量的连接,连接的数量随着输入图片尺寸和通道、输出图片通道、以及Kernel的尺寸的增大而急剧增加。即使使用稀疏连接和共享参数,当这些参数都比较大时,巨大的连接数量依然会导致整个卷积层耗费大量时间和资源进行计算。因此,为了减少计算时间,CAFFE使用了一个特别的方法来加速卷积层的计算。

首先,我们先不看CAFFE的实现过程,仅仅从卷积运算的概念来入手,我们自己就可以设计一个forwarding过程的实现。其实基本的算法并不复杂。我们知道,任何一个输出像素都取决于输入图像的一个局部区域,这个局部区域的尺寸由窗口(kernel)决定,而位置则由输出点的座标决定。对每个输出通道,我们在空白窗口上“涂上”各自的权重,然后根据需要计算的输出像素点的位置,我们使用这个窗口在输入图像的对应局部区域进行滤波操作(其实就是输入图像像素点乘以窗口上的对应权重,然后把结果统统加起来)。最后,再加上一个全局的偏置就得到输出点的值了。

我们以计算输出图像的[0,0]点为例,整个计算过程可以用下图表示:

输出像素点的计算过程

这个方法实现起来非常简单,但是它有一个致命的弱点:特别慢。为什么特别慢呢?如果你使用CPU这种并发度不高的处理器来运行,整个过程需要循环N = OutputChennel x OutputHeight x OutputWidth 次。而且整个过程没有任何可以加速的tricks。如果你使用GPU这种高度并发的处理器来运行,因为各个循环没有依赖关系,每次循环虽然都可以分配到一个thread进行运行,相比CPU的版本,速度会大大提升,但是每个thread会执行多次的读全局变量操作(获取局部输入图像像素点值),各个thread的读取的内存区域不满足形成WARP的条件,导致整个读取过程耗时大大增加。我在我自己的笔记本上做过实验,在GPU(Nvidia GForce GT630M)上执行一次GoogleNet的forwarding操作,上文提到的方法需要13秒左右,而原生的CAFFE则只需要3秒。

下面我们就来看看CAFFE到底怎么实现的。

CAFFE将整个forwarding过程分成了两部分。第一步,根据输入图像产生一个临时的矩阵(Col Matrix)。第二步,使用这个Col Matrix乘以weight矩阵,再加上bias矩阵就得到了输出图像。大家肯定想知道这个Col Matrix到底是个什么神奇的东西,为什么用它只需要再执行一个简单的矩阵就能得到输出。让我们先回忆一下前面提到的方法,我们是不是在一个局部区域执行了一个点乘操作,再把结果加到了一起。这个过程其实就跟矩阵乘法中计算一个点的操作一模一样。如果我们把输出图像看作一个矩阵,那么这个输出肯定是某个神秘矩阵乘以weight矩阵再整体加上bias矩阵得来的。而CAFFE的tricks就是将输入图像做了一个预处理(叫im2col)调整成这样一个矩阵Col Matrix。调整的过程其实也不难,其实就是用空白窗口在图像上做卷积操作,因为是空白窗口,所以不会有乘法操作,只是简单的将原始像素值复制一份而已。Col Matrix的每一个通道都对应一个输入图像通道下的一个窗口兴趣点。比如,如果输入图像由5个通道,而窗口是2x2的,那么这个Col Matrix就应该有20个通道。具体的过程可以参考下图:

CAFFE的forwarding实现过程

CAFFE这样做的好处显而易见。第一步操作,我们只需要执行赋值操作,不涉及任何复杂运算,因此运算很快。第二步操作,涉及到真正的复杂乘法和加法运算,但是因为只是矩阵乘法,因此我们可以直接调用高度优化的矩阵运算库(如blas库),这样运算速度会获得急速提升。

但是,CAFFE的方法就没有缺点吗?当然有。一个致命的缺点就是Col Matrix大得离谱。为了使其适合做矩阵乘法,Col Matrix里保存了大量冗余的输入图像信息。即使是中等尺寸的输入图像,Col Matrix也可能耗费上百MB的存储空间。这对于移动GPU来说往往是不可接受的。具体的改进方法,以后再慢慢讨论。

分享到 评论