CNN中的卷积操作
卷积层是CNNs网络中可以说是最重要的层了,卷积层的主要作用是对输入图像求卷积运算。如下图所示,输入图片的维数为$[c_0,h_0,w_0]$ ;卷积核的维数为$[c_1,c_0,h_k,w_k]$,其中$c_0$在图中没有表示出来,一个卷积核可以看成由$c_1$个维数为$[c_0,h_k,w_k]$的三维滤波器组成;除了这些参数通常在计算卷积运算的时候还有一些超参数比如:stride(步长):$S$,padding(填充):$P$。
根据上面所说的参数就可以求出输出特征的维数为$[c_1,h_1,w_1]$,其中$h_1 = (h_0-h_k+2P)/S+1$,$w_1 = (w_0-w_k+2P)/S+1$。
卷积的计算过程其实很简单,但不是很容易说清楚,下面通过代码来说明。
基本环境设置:
1 | %load_ext cython #代码运行在jupyter-notebook中 |
卷积层计算的代码如下,想象一副图像尺寸为MxM,卷积核mxm。在计算时,卷积核与图像中每个mxm大小的图像块做element-wise相乘,然后得到的结果相加得到一个值,然后再移动一个stride,做同样的运算,直到整副输入图像遍历完,上述过程得到的值就组成了输出特征,具体运算过程还是看代码。
1 | def conv_forward_naive(x, w, b, conv_param): |
下面来检测下上面的卷积计算代码,我们人为的设置两个卷积核(分别为求灰度特征,和边缘特征),然后对两幅输入图像求卷积,观察输出的结果:
1 | from scipy.misc import imread, imresize |
图像经过卷积后,输入结果如下所示:
im2col
运行上面代码的时候,我们发现对这两张图片计算卷积还是比较慢的,而在CNN中是存在大量的卷积运算的,所以我们需要一个更加快速的计算卷积的方法。如下图所示为Caffe中计算卷积的示意图,通过上面普通卷积运算的实现我们可以发现,卷积操作实际上是在对输入特征的一定范围内和卷积核滤波器做点乘,如下图我们可以利用这一特性把卷积操作转换成两个大矩阵相乘。
把输入图像要经行卷积操作的这一区域展成列向量的操作通常称为im2col
,具体过程如下图所示。
下图为一个具体的例子,看懂下面这个图应该就会清楚上面的做法。
下面的im2col_cython
是使用Cython代码来实现im2col
功能,有关Cython在Python中的具体使用可参考:Python速度优化-Cython中numpy以及多线程的使用。
1 | %%cython |
调用上面的im2col_cython
函数来实现卷积操作: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
27def conv_forward_im2col(x, w, b, conv_param):
"""
A fast implementation of the forward pass for a convolutional layer
based on im2col and col2im.
"""
N, C, H, W = x.shape
num_filters, _, filter_height, filter_width = w.shape
stride, pad = conv_param['stride'], conv_param['pad']
# Check dimensions
assert (W + 2 * pad - filter_width) % stride == 0, 'width does not work'
assert (H + 2 * pad - filter_height) % stride == 0, 'height does not work'
# Create output
out_height = (H + 2 * pad - filter_height) / stride + 1
out_width = (W + 2 * pad - filter_width) / stride + 1
out = np.zeros((N, num_filters, out_height, out_width), dtype=x.dtype)
# x_cols = im2col_indices(x, w.shape[2], w.shape[3], pad, stride)
x_cols = im2col_cython(x, w.shape[2], w.shape[3], pad, stride)
res = w.reshape((w.shape[0], -1)).dot(x_cols) + b.reshape(-1, 1)
out = res.reshape(w.shape[0], out.shape[2], out.shape[3], x.shape[0])
out = out.transpose(3, 0, 1, 2)
cache = (x, w, b, conv_param, x_cols)
return out, cache
测试使用im2col
方法的卷积操作,从输出的图片可以看出和原始卷积方法一样。
1 | out, _ = conv_forward_im2col(x, w, b, {'stride': 1, 'pad': 1}) |
下面来测试一下使用两种方法的时间,使用原始的卷积操作每次循环需要2.19s,而使用im2col
方法则只需要28.3ms,时间大概缩短了77倍,当然这其中也包括了使用Cython所降低的时间,但总体上来说还是大大加快了卷积的计算速度。
虽然使用im2col
方法加快了计算速度,但也会使用更多的内存,因为把输入图像转换为col的时候,会有很多重复的元素。
1 | %timeit conv_forward_naive(x, w, b, {'stride': 1, 'pad': 1}) |
1 | 1 loop, best of 3: 2.19 s per loop |