天天看点

卷积神经网络:卷积层和池化层代码实现,im2col代码实现-python(convolution,pooling,im2col)

目录

    • 1. im2col
    • 2.卷积层
    • 3.池化层

讲前注意:

理解卷积层和池化层的代码主要有两个稍困难的点,一个是关于transpose轴变换与reshape的使用,一个是反向传播的思想。关于反向传播的思想大家可以看这。Affine层和RuLe层的实现思想会用在卷积层和池化层的实现上。

本文不讲卷积层和池化层的原理,是代码实践,请理解原理后看代码。代码中有巨量注释,不多阐述

注意前向传播和反向传播代码在相反处理数据,轴变换不改变数组的元素,只改变元素的结构,只要对应索引值不变,那么元素是不会变的。大家可以先把im2col当作黑盒,看完卷积层和池化层的实现之后再看会很清晰。大家看代码一个要注意每个矩阵的transpose和shape。

1. im2col

im2col是讲多维数据放在二维矩阵中方便计算。

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
    filter_h : 滤波器的高
    filter_w : 滤波器的长
    stride : 步幅
    pad : 填充

    Returns
    -------
    col : 2维数组
    """
    N, C, H, W = input_data.shape#N 是数据个数 C 是通道数 H 是高 W 是长
    out_h = (H + 2*pad - filter_h)//stride + 1 #计算输出数据 即输出特征图 out_h 输出特征图高
    out_w = (W + 2*pad - filter_w)//stride + 1#计算输入数据 即输入特征特 out_w 输出特征图长
    # // 整除

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    #np.pad 填充数组input_data
    #constant_values没有赋值,默认填充0
    #第一维度(shape[0])前面填充0个后面填充0个
    #第二维度同理(0,0),第三第四维度同理(pad,pad)
    #数据量和通道默认(0,0),  咩咩咩咩咩咩咩咩,,长和高根据pad参数填充
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
    # col 6维数组
    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]
            # 一次操作取了N批C个通道上
            # col 的第三维y,第四维x赋值 img第三维度y导y_max隔stride是步长,隔stride取一位,第四维度同理
            # 数据批量和通道数不变的
            #  filter_h也会简写为FH或fh

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    # transpose轴变换 轴变换的目的是改变索引顺序方便reshape成想要的模样
    #  filter_h也会简写为FH或fh
    # col.shape = (N*out_h*out_w,C*FH*FW)

    return col
           

这里有一张图,可以方便你理解im2col的循环到底做了什么

卷积神经网络:卷积层和池化层代码实现,im2col代码实现-python(convolution,pooling,im2col)

2.卷积层

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 滤波器长 C 通道数(和滤波器通道数一样才能进行通道方向的卷积)
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = int((H + 2 * self.pad - FH) / self.stride + 1)
        out_w = int((W + 2 * self.pad - FW) / self.stride + 1)

        #im2col四维变二维,实际上把N放在了一维度,把C放在了二维度
        #im2col作为黑盒,详细见im2col讲解
        #实际上im2col是把元素相乘变为矩阵相乘
        col = im2col(x, FH, FW, self.stride, self.pad)
        #col.shape =(N*out_h*out_w,FH*FW*C)
        col_W = self.W.reshape(FN,-1).T
        #col_W.shape=(FN,FH*FW*C) 转置后(FH*FW*C,FN)
        #其实col_W.shape=(FN,C*FH*FW),这里提醒大家reshape是按索引顺序来的所以不能直接col_W=reshape(-1,FN)
        #不过值相同所以我没有按顺序写
        #矩阵乘法的规定了 滤波器通道数和输入数据通道数必须相同,否则无法相乘

        out = np.dot(col,col_W) + self.b
        #out.shape = (N*out_h*out_w,FN)
        #out的一个元素每个通道上 一个滤波器与输入数据对应元素乘积和 的和
        #行就是输出数据的一个通道上每批数据的每个元素,列就是输出数据的每个通道上的一批数据的一个元素 (一批指批处理)

        out = out.reshape(N, out_h, out_w, -1).transpose(0,3,1,2)
        #二维变成思维,然后再进行轴变换,把通道索引变到输出数据长索引和输出数据宽索引前面
        #其实就是变成了下一层输入数据的模样,要知道本层输入数据也是(批,通道,高,长)
        #此时 out.shape = (N,FN,out_h,out_w)
        #本层滤波器的个数就是下一层输入数据的通道数
        self.x = x
        self.col = col
        self.col_W = col_W

        return out


    def backward(self, dout):
        #根据误差反向传播思想,详细见误差反向传播章节
        #与Affine层不同的此处会注释一下

        FN, C, FH, FW = self.W.shape

        # 与forward相反 把通道索引变在最后
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)
        #transpose后 dout.shape=(批,out_h,out_w,通道)
        #reshape后dout.shape=(批*out_h*out_w,滤波器个数(输出通道数))
        self.db = np.sum(dout, axis=0)
        #在forward中加了N*out_h*out_w个b,反向传播就要加这些个dout

        self.dW = np.dot(self.col.T, dout)
        #反向传播矩阵求导操作 dW.shape = (FH*FW*C,FN) 这四个参数熟悉吧,就是滤波器的参数也就是W该有的参数
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
        #dw换成标准形式 也就是w的输入形式

        dcol = np.dot(dout, self.col_W.T)
        #反向传播矩阵求导操作 dcol.shape=(批*out_h*out_w,C*FH*FW)

        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
        #反向操作im2col
        return dx
  
           

3.池化层

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)
        #一行即是一个池化的应用区域内的值
        # col.shape=(N*C*out_h*out_w,pool_h*pool_w)

        arg_max = np.argmax(col, axis=1)
        #最大值索引数组
        out = np.max(col,axis=1)
        #每行只留下最大值 max池化
        out = out.reshape(N,out_h,out_w,C).transpose(0,3,1,2)
        #重新reshape为四维后做轴变换调整为输入数据标准形式

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        #forward的反向操作 此时dout.shape=(批,out_h,out_w,通道数)
        pool_size = self.pool_h * self.pool_w
        #pool_size 为im2col展开的列数
        dmax = np.zeros((dout.size, pool_size))
        #池化层输出是所有im2col展开行的最大值,元素的个数即为原展开行的行数
        #dout.size = arg_max.size
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        #在最大值索引处赋予最大值

        dmax = dmax.reshape(dout.shape + (pool_size,))
        #dout.shape+pool_size是在dout的维度上又加了一个维度pool_size
        #dmax.shape = (批,out_h,out_w,C,pool_size)


        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        #dcol.shape = (N*out_h*out_w,C*pool_size)而pool_size = pool_h*pool_w 正式im2col的输出
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)

        return dx
           

继续阅读