手把手实现CNN的卷积层及池化层

参考《深度学习入门》

卷积层

在二维图像上,卷积操作一方面可以高效地按照我们的需求提取图像的领域信息,在全局上又有着非常好的平移特性。接下来我们看卷积层的实现,im2col实现参见论文High Performance Convolutional Neural Networks for Document Processing,这里还有一篇讲的比较通俗的文章,可供参考:im2col方法实现卷积算法

preview

下面我们来看如何使用纯python代码来实现卷积操作,这里我们使用im2col来进行优化,下面是im2col的实现(参考《深度学习入门》),代码不是很好理解,加了详细的注释😃

    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

        #在4维tensor(数据量,通道,高,长)的两个维度:高和长 上进行常量填充
        img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
        #col.shape=[N,C,filter_h,filter_w,out_h,out_w]
        col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

        for y in range(filter_h):
            #从y到y_max表示窗口在h方向上滑动可以出现的起始位置
            y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            #可以理解为处于初试位置(未滑动)卷积核对应img上的每个元素滑动时可以取到的值,shape=[out_h, out_w]
            #col中的y,x就是用来定位img上的元素,其shape=[filter_h, filter_w]
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
        
        #col变换前可以通过指定某个批数据、某个通道、某个元素来获取
        #shape=[out_h, out_w]形状的滑动时可取到的值
        #col.transpose(0,4,5,1,2,3)就是依次访问N,out_h,out_w,channel,y,x,也就是说,我们先指定一个批数据,
        #然后指定range(out_h),range(out_w)来指定某个滑动位置,基于该滑动位置,指定一个通道即可访问该位置的
        #待卷积元素,shape=[filter_h, filter_w]
        #.reshape(N*out*h*out_w, -1)即可使matrix每一行按照channel、y、x依次展开成一维
        col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
        return col

img2col的逆操作定义为函数col2img,其代码(参考《深度学习入门》)如下(这里有个问题就是第10行不知道为什么写,我重新改写了一下):

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    #col.shape=[N*out_h*out_w, C*filter_h*filter_w]
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
    #col.shape=[N, C, filter_h, filter_w, out_h, out_w]

    #注释掉了原来的语句,不懂为什么要这么写
    #img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    img = np.zeros((N, C, H + 2*pad, W + 2*pad))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            #img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
            img[:, :, y:y_max:stride, x:x_max:stride] = col[:, :, y, x, :, :]

    #得到填充之前的梯度
    return img[:, :, pad:H + pad, pad:W + pad]

最后我们以类的形式实现卷积层,包含初始化参数、前向传递以及误差反向传播部分:

#卷积层的实现
class Convolution:
    #卷积层我们需要指定卷积核、偏置、卷积步长以及填充大小
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        #根据不同的卷积模式指定pad大小
        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表示channel数,FH=filter_h, FW=filter_w
        FN, C, FH, FW = self.W.shape
        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)

        #将input_feature转成二维矩阵,shape=[N*out_h*out_w, channel*FH*FW]
        col = im2col(x, FH, FW, self.stride, self.pad)
        #将卷积核转成二维矩阵,self.W.reshape(FN, -1)将W转成FN * (C * FH * FW)的二维矩阵
        #col_W.shape=[C*FH*FW, FN]
        col_W = self.W.reshape(FN, -1).T
        #二维矩阵相乘得到shape=[N*out_h*out_w, FN]的矩阵(矩阵上每一个元素都是对应区域与卷积核的卷积结果,
        #多通道),对于每个卷积核都对应一个偏置,因此需要+self.b
        out = np.dot(col, col_W) + self.b
        #out.reshape(N,out_h,out_w,-1)得到一个四维张量(N,out_h,out_w,FN)
        #.transpose(0,3,1,2)得到四维张量(N,FN,out_h,out_w)即为卷积输出
        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

    #反向传播的实现,输入dout.shape=[N,FN, out_h,out_w]
    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        #forward的逆操作,很好理解
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        #参考通过计算图来计算Affline层的反向传播
        self.db = np.sum(dout, axis=0)
        #根据公式计算得到关于卷积核的梯度,二维矩阵,需要重整成卷积核原来的形状
        #col_W = self.W.reshape(FN, -1).T的逆操作
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        #dout.shape=[N*out_h*out_w, FN], self.col_W.shape=[C*FH*FW, FN]
        dcol = np.dot(dout, self.col_W.T)
        #需要将关于输入x展开后的二维矩阵的梯度重整为与x具有相同的形状
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx
池化层

池化层的特征如下:

  • 没有要学习的参数
  • 通道数不发生变化
  • 对微小的位置变化具有鲁棒性

池化层的实现和卷积层相同,都使用im2col展开输入数据,不过,池化时在通道方向是弗里德,这一点和卷积层不同,池化的应用区域按通道单独展开。

池化层的实现代码如下:

class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        #pool_h,pool_w为池化区域的大小
        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)
        #执行im2col后,col.shape=[N*out_h*out_w, C*self.pool_h*self.pool_w]
        #依次按照批大小,平移位置,通道展开
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        #out.shape=[N,C,out_h,out_w],即为各通道独立求max
        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)
        #dout.shape=[N,out_h,out_w,C]
        
        pool_size = self.pool_h * self.pool_w
        #dout.size=N*out_h*out_w*C,每一个池化后的元素对应原来的pool_size个池化区域中的元素
        dmax = np.zeros((dout.size, pool_size))
        #flatten将张量转成一维
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        #dmax.shape=[N,out_h,out_w,C,pool_size]
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        #dcol=[N*out_h*out_w, C*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
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值