上篇文章【神经网络】卷积神经网络(一)总览以及卷积层、池化层我们已经介绍了卷积神经网络中关于卷积层和池化层的运作原理,本文我们将详细说明卷积层和池化层的具体实现方式。
一.im2col函数
由于CNN中处理的是多维数据,如果老老实实地一步一步实现卷积运算,估计要重复好几层的for语句,而且NumPy中存在使用for语句后处理变慢的缺点,所以我们使用im2col这个函数来进行数据预处理。
im2col(Image to Column)是一个常用于神经网络以及计算机视觉领域的一种数据重组方法,主要用于将卷积运算转化为矩阵乘法来加速计算。以下通过一个简单的例子来说明im2col函数的用法,假设我们有如下条件:
- **输入图像:**假设是一张单通道的2D图片(3像素高 × 3像素宽)。
\[1, 2, 3,
4, 5, 6,
7, 8, 9\]
- **滤波器:**大小为 2×2。
\[W1, W2,
W3, W4\]
- **步幅:**设为 1。
- **填充:**设为 0。
首先是滑动滤波器操作,2×2的滤波器在3×3的图片上以步幅1滑动。它会像盖章一样,从左到右、从上到下扫描,一共能停靠 4 个位置。
我们分别用颜色标记这四个位置的区域:
位置1(左上角):数字 1, 2, 4, 5
位置2(右上角):数字 2, 3, 5, 6
位置3(左下角):数字 4, 5, 7, 8
位置4(右下角):数字 5, 6, 8, 9
紧接着**将每个3D方块"横向展开为1列",**把每一个位置抠出来的 2×2 小块拉直,变成一列。列的长度是 2×2=4。
位置1的区域 \[1,2,4,5] 拉直后变成列1:
[1,
2,
4,
5]
位置2的区域 \[2,3,5,6] 拉直后变成列2:
[2,
3,
5,
6]
位置3的区域 \[4,5,7,8] 拉直后变成列3:
[4,
5,
7,
8]
位置4的区域 \[5,6,8,9] 拉直后变成列4:
[5,
6,
8,
9]
把所有拉直后的列横向拼接起来,就得到了 im2col 的最终输出矩阵:
\[1, 2, 4, 5, <- 来自位置1
2, 3, 5, 6, <- 来自位置2
4, 5, 7, 8, <- 来自位置3
5, 6, 8, 9] <- 来自位置4
最后,我们把滤波器的权重也拉直成一行向量 W1, W2, W3, W4。用这个向量去乘以刚刚生成的 im2col 矩阵:
第1列的结果 = W1×1 + W2×2 + W3×4 + W4×5 (对应位置1的卷积运算结果)
第2列的结果 = W1×2 + W2×3 + W3×5 + W4×6 (对应位置2)
第3列的结果 = W1×4 + W2×5 + W3×7 + W4×8 (对应位置3)
第4列的结果 = W1×5 + W2×6 + W3×8 + W4×9 (对应位置4)
以上4个计算结果完全等于滤波器在原始图片上依次滑动做乘加得到的结果,但过程变成了一次简单的矩阵乘法。这就是im2col如何将应用滤波器的每一个3维区域变成1列,并最终拼成矩阵的原理。
im2col函数的传参如下:
cpp
im2col (input_data, filter_h, filter_w, stride=1, pad=0)
• input_data―由(数据量,通道,高,长)的4维数组构成的输入数据
• filter_h―滤波器的高
• filter_w―滤波器的长
• stride―步幅
• pad―填充
二.卷积层实现
现在使用im2col来实现卷积层。这里我们将卷积层实现为名为Convolution的类:
python
class Convolution:
def __init__(self, W, b, stride=1, pad=0): //滤波器(权重)、偏置、步幅、填充作为参数接收
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 中间数据(backward时使用)
self.x = None
self.col = None
self.col_W = None
# 权重和偏置参数的梯度
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape //滤波器是(FN, C, FH, FW) 的4 维形状
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T //将各个滤波器的方块纵向展开为1 列,在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0,2,3,1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
forward的实现中,最后会将输出大小转换为合适的形状,转换时使用了NumPy的transpose函数,transpose会更改多维数组的轴的顺序:

在进行卷积层的反向传播时,则使用im2col的逆处理,也就是col2im函数,除了使用col2im这一点,卷积层的反向传播和Affi ne 层的实现方式都一样。
三.池化层实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。不同的是在池化的情况下,通道方向上是独立的:

像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的形状即可:

代码如下:
python
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
池化层的实现按下面3 个阶段进行:
-
- 展开输入数据。
-
- 求各行的最大值。
-
- 转换为合适的输出大小。