卷积层和池化层的实现
1. 4维数组
CNN中各层间传递的数据是4维数据。所谓4维数据,比如数据的形状是(10, 1, 28, 28),则它对应10个高为28、长为28、通道为1的数据。
用python是实现的话,如下图所示:
>>> x = np.random.rand(10,1,28,28)
>>> x.shape
(10,1,28,28)
在这里,要访问第1个数据,只需要写x[0]就可以了,同样的,用x[1]可以访问第2个数据
>>> x[0].shape
>>> x[1].shape
(1,28,28)
如果要访问第1个数据的第1个通道的空间数据,可以写成下面这样。
>>> x[0,0] # x[0][0]
像这样,CNN中处理的是4维数据,因此卷积运算的实现看上去会很复杂,但是通过使用下面要介绍的im2col这个技巧,问题就会变得很简单
1.2 基于im2col的展开
如果老老实实地实现卷积运算,估计要重复好几层的for语句。这样的
实现有点麻烦,而且,NumPy中存在使用for语句后处理变慢的缺点(NumPy中,访问元素时最好不要用for语句).
这里,我们不使用for语句,而是使用im2col这个便利的函数进行简单的实现
im2col函数(image to column) ,翻译过来就是“从图像到矩阵”的意思。将输入数据展开以适应滤波器(权重),
im2col详细介绍 例如 对于 3维的输入数据应用im2col后,数据转化为2维矩阵,(正确的来说,是吧包含批数量的4维数据转换成了2维数据)
im2col会把输入数据展开以适合滤波器(权重)。具体地说,如图7-18所示,对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。im2col会在所有应用滤波器的地方进行这个展开处理。
在图7-18中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算上,可以有效地利用线性代数库
使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可(参照图7-19)。这和全连接层的Affine层进行的处理基本相同。
基于im2col方式的输出结果是2维矩阵。因为CNN中数据会保存为4维数组,所以要将2维输出数据转换为合适的形状。
1.3 卷积层的实现
im2col (input_data, filter_h, filter_w, stride=1, pad=0)
• input_data―由(数据量,通道,高,长)的4维数组构成的输入数据
• filter_h―滤波器的高
• filter_w―滤波器的长
• stride―步幅
• pad―填充
•returns
col : 2维数组
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1 #输出图的计算公式
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
#在通道的之后维度,长和宽的纬度进行填充
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
#确定下来了后4个纬度,col的前两个冒号和img的前两个冒号对应,
#y:y_max:stride, x:x_max:stride是赋值给y, x, :, :后面的两个冒号
#这里可以理解为卷积核的每个元素(filter_h, filter_w确定一个卷积核元素)能在图中滑动的范围,
#y_max-y/stride就是out_h,也就是输出图的高,也就是滑动范围
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
#-1表示第二个维度需要程序进行推理,即总个数除以N*out_h*out_w
return col
im2col会考虑滤波器大小、步幅、填充,将输入数据展开为2维数组。现在,
我们来实际使用一下这个im2col。
批大小为1、通道为3的7 × 7的数据
>>> x1 = np.random.rand(1,3,7,7)
>>> col1 = im2col(x1,5,5,stride = 1,pad = 0)
>>> print(col1.shape)
(9,75)
>>> x2 = np.random.rand(10,3,7,7)
>>> col2 = im2col(x2,5,5,stride = 1,pad = 0)
>>> print(col2.shape)
(90,75)
对其应用im2col函数,在这两种情形下,第2维的元素个数均为75。这是滤波器(通道为3、大小为5 × 5)的元素个数的总和[3x5x5 = 75]。第二个数据的批大小是10 即保存了10倍的数据得到(90,75)
利用im2col 来实现卷积层(Convolution).
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # 滤波器的展开
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
卷积层的初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收。
滤波器是 (FN, C, FH, FW)的 4 维形状。
另外,FN、C、FH、FW分别是 Filter
Number(滤波器数量)、Channel、Filter Height、Filter Width的缩写。
这里用粗体字表示Convolution层的实现中的重要部分。在这些粗体字
部分,用im2col展开输入数据,并用reshape将滤波器展开为2维数组。然后,
计算展开后的矩阵的乘积。
展开滤波器的部分,将各个滤波器的方块纵向展开为1列。这里通过reshape(FN,-1)将参数指定为-1,这是reshape的一个便利的功能。通过在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致。比如,(10, 3, 5, 5)形状的数组的元素个数共有750个,指定reshape(10,-1)后,就
会转换成(10, 75)形状的数组。
forward的实现中,最后会将输出大小转换为合适的形状。转换时使用了
NumPy的transpose函数。transpose会更改多维数组的轴的顺序。如图7-20
所示,通过指定从0开始的索引(编号)序列,就可以更改轴的顺序。
4.池化层的实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。具体地讲,如图7-21所示,池化的应用区域按通道单独展开
像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的形状
#池化层的实现
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
def forward(self,x):
N,C,H,W = x.shape
out_h = int(1+(H+2*pad-FH)/self.stride)
out_w = int(1+(W+2*pad-FW)/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)
#最大值
out = np.max(col,axis=1)
#转化
out = out.reshape(N,out_h,out_w,C).transpose(0,3,1,2)
return out
池化层的实现按下面3个阶段进行:
- 展开输入数据。
- 求各行的最大值。
- 转换为合适的输出大小。
最大值的计算可以使用 NumPy 的 np.max方法。np.max可以指定axis参数,并在这个参数指定的各个轴方向上求最大值。比如,如果写成np.max(x, axis=1),就可以在输入x的第1维的各个轴方向上求最大值
另外,池化层的backward处理可以参考ReLU层的实现中使用的max的反向
传播