问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

卷积层和池化层的反向传播实现

创作时间:
作者:
@小白创作中心

卷积层和池化层的反向传播实现

引用
CSDN
1.
https://blog.csdn.net/absths/article/details/139708990

卷积神经网络(CNN)在图像识别、自然语言处理等领域有着广泛的应用。其中,卷积层和池化层是CNN的核心组成部分。本文将详细介绍卷积层和池化层的反向传播实现,帮助读者深入理解CNN的工作原理。

卷积层的反向传播

原理

在卷积层的前向传播中,我们通常会使用im2col操作将输入特征图转换成一个矩阵,其中矩阵的每一行对应于输入特征图在一个卷积窗口的所有通道内的数据。这样,卷积操作就被转化成了矩阵乘法。忽略掉偏置的计算(因为偏置的计算非常简单),卷积计算可以用下面的公式表示:

$$
\hat{y} = \hat{x} \times \hat{W}
$$

这里,$\hat{x}$是输入$x$经过im2col计算后的结果。因为卷积层的权重$W$的形状通常是$(C', C, K_h, K_w)$,为了匹配上面矩阵乘法的形式,需要通过reshapetranspose操作将$W$转化为$(C \cdot K_h \cdot K_w, C')$的形状,这里记这个矩阵为$\hat{W}$。得到的结果$\hat{y}$的形状是$(B \cdot H' \cdot W', C')$,通过reshapetranspose操作将其变换成$(B, C', H', W')$就是我们想要的结果了。

对于这个形式,你是否感觉有一点熟悉呢?没错,这与我们之前学习的全连接层的计算形式如出一辙,套用全连接层的反向传播公式:

$$
\frac{\partial \hat{W}}{\partial l} = \frac{\partial \hat{y}}{\partial l} \cdot \frac{\partial \hat{W}}{\partial \hat{y}} = \hat{x} \times \left( \frac{\partial \hat{y}}{\partial l} \right)^T
$$

$$
\frac{\partial \hat{x}}{\partial l} = \frac{\partial \hat{y}}{\partial l} \cdot \frac{\partial \hat{x}}{\partial \hat{y}} = \hat{W} \times \frac{\partial \hat{y}}{\partial l}
$$

最后,我们需要做的就是把$\frac{\partial l}{\partial \hat{x}}$变换为$\frac{\partial l}{\partial x}$,同时把$\frac{\partial l}{\partial \hat{W}}$变换为$\frac{\partial l}{\partial W}$。对于后者,只要通过transposereshape操作还原为原来的形状即可。对于前者,我们需要通过im2col的逆运算——col2im——来将其转换为$\frac{\partial l}{\partial x}$。

代码实现

实训拓展了在之前的实训定义的Convolution类,实训已经给出了forward(x)的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)dout是损失函数相对卷积层输出的梯度,即之前公式中的$\frac{\partial y}{\partial l}$,是一个形状为$(B, C', H', W')$的numpy.ndarray。实训已经提供了一个col2im的实现,其可以将一个$(B \times H' \times W', C \times K_h \times K_w)$的矩阵转化成$(B, C, H, W)$的特征图。

实训提供的col2im函数的定义如下:

col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0)

对应参数的含义分别为:

  1. col:变换后的矩阵;
  2. input_shape:输入特征图的形状;
  3. filter_hfilter_w:池化窗口的宽和高;
  4. stride:池化的步长;
  5. pad:池化的填充。

代码实现

import numpy as np
from utils import im2col, col2im

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        r'''
        卷积层的初始化
        Parameter:
- W: numpy.array, (C_out, C_in, K_h, K_w)
- b: numpy.array, (C_out)
- stride: int
- pad: int
        '''
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        self.x = None
        self.col = None
        self.col_W = None
        self.dW = None
        self.db = None

    def forward(self, x):
        r'''
        卷积层的前向传播
        Parameter:
- x: numpy.array, (B, C, H, W)
        Return:
- y: numpy.array, (B, C', H', W')
             H' = (H - Kh + 2P) / S + 1
             W' = (W - Kw + 2P) / S + 1
        '''
        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)
        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)
        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        r'''
        卷积层的反向传播
        Parameter:
- dout: numpy.array, (B, C', H', W')
        Return:
- dx: numpy.array, (B, C, H, W)
        另外,还需计算以下结果:
- self.dW: numpy.array, (C', C, Kh, Kw) 与self.W形状相同
- self.db: numpy.array, (C',) 与self.b形状相同
        '''
        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

测试说明

平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入$x$、权重$W$、偏置$b$和输出梯度$dout$,然后根据你的实现代码,创建一个Convolution类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过$10^{-5}$即可。

上述结果有四舍五入的误差,你可以忽略。

池化层的反向传播

原理

在之前的实训中,我们学习了池化层的前向传播,也知道无论在原理上还是在实现上池化层的前向传播与卷积层的前向传播都有很强的相似性。二者在反向传播上依然如此,所有的计算都是以窗口为处理单位。

对于平均值池化,反向传播非常简单,只要把对应输出位置的梯度平均放回窗口内的每个输入位置即可。对于最大值池化,如我们之前学习的max函数存在不可导点,因此我们按照之前ReLU的方法,使用次梯度,即:在每个窗口中,将输出位置的梯度放回前向传播时最大值对应的输入位置。下图展示了最大值池化反向传播的过程。

图1 最大值池化的前向和反向传播

池化层反向传播的实现

在实现池化层的前向传播时,我们采用了与实现卷积层时相似的技巧,即先用im2col操作,将输入特征图转化成一个矩阵,每个窗口内的数据都对应到矩阵的每一行,这样,窗口内的平均值操作或者最大值操作就变成矩阵的行操作,从而能够利用numpy进行高效的操作。因此,在进行反向传播时,也与卷积层相似,再对变换后的矩阵在平均值或最大值反向传播之后,再用col2im操作还原为与前向传播时输入特征图相同的形状。

实训拓展了在之前的实训定义的MaxPool类,实训已经给出了forward(x)的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)dout是损失函数相对于全连接层输出的梯度,是一个形状为$(B, C, H, W)$的numpy.ndarray。在前向传播时,输入特征图和最大值对应的索引分别记录在了self.xself.arg_max中。实训已经提供了一个col2im的实现,请参考上一关的介绍。

代码实现

代码实现

import numpy as np
from utils import im2col, col2im

class MaxPool:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        r'''
        池化层的初始化
        Parameter:
- pool_h: int
- pool_h: int
- stride: int
- pad: int
        '''
        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):
        r'''
        池化层的前向传播
        Parameter:
- x: numpy.array, (B, C, H, W)
        Return:
- y: numpy.array, (B, C, H', W')
             H' = (H - Kh + 2P) / S + 1
             W' = (W - Kw + 2P) / S + 1
        '''
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h + 2 * self.pad) / self.stride)
        out_w = int(1 + (W - self.pool_w + 2 * self.pad) / 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):
        r'''
        池化层的反向传播
        Parameter:
- dout: numpy.array, (B, C', H', W')
        Return:
- dx: numpy.array, (B, C, H, W)
        '''
        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

测试说明

平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入$x$和输出梯度$dout$,然后根据你的实现代码,创建一个MaxPool类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过$10^{-5}$即可。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号
卷积层和池化层的反向传播实现