卷积层和池化层的实现

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维数据)

CNN 卷积 池化 膨胀率 卷积池化过程是什么_python


im2col会把输入数据展开以适合滤波器(权重)。具体地说,如图7-18所示,对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。im2col会在所有应用滤波器的地方进行这个展开处理。

CNN 卷积 池化 膨胀率 卷积池化过程是什么_ide_02


在图7-18中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算上,可以有效地利用线性代数库

  使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可(参照图7-19)。这和全连接层的Affine层进行的处理基本相同。

  基于im2col方式的输出结果是2维矩阵。因为CNN中数据会保存为4维数组,所以要将2维输出数据转换为合适的形状。

CNN 卷积 池化 膨胀率 卷积池化过程是什么_CNN 卷积 池化 膨胀率_03

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开始的索引(编号)序列,就可以更改轴的顺序。

CNN 卷积 池化 膨胀率 卷积池化过程是什么_python_04

4.池化层的实现

池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。具体地讲,如图7-21所示,池化的应用区域按通道单独展开

CNN 卷积 池化 膨胀率 卷积池化过程是什么_ide_05


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

CNN 卷积 池化 膨胀率 卷积池化过程是什么_深度学习_06

#池化层的实现
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个阶段进行:

  1. 展开输入数据。
  2. 求各行的最大值。
  3. 转换为合适的输出大小。
      最大值的计算可以使用 NumPy 的 np.max方法。np.max可以指定axis参数,并在这个参数指定的各个轴方向上求最大值。比如,如果写成np.max(x, axis=1),就可以在输入x的第1维的各个轴方向上求最大值
    另外,池化层的backward处理可以参考ReLU层的实现中使用的max的反向
    传播