深度学习入门(九):卷积层和池化层的实现

本文为《深度学习入门 基于Python的理论与实现》的部分读书笔记,也参考吴恩达深度学习视频
代码以及图片均参考此书

卷积神经网络(CNN: Convolutional Neural Network)整体结构

在这里插入图片描述
CNN 的层的连接顺序是“Convolution - ReLU - (Pooling)”(Pooling 层有时会被省略)。还需要注意的是,在上的CNN中,靠近输出的层中使用了之前的“Affine - ReLU”组合。此外,最后的输出层中使用了之前的“Affine - Softmax”组合。这些都是一般的CNN中比较常见的结构。

卷积层负责对图像进行特征提取,池化层负责降采样:保留显著特征、降低特征维度的同时增大感受野。最后的全连接层负责对特征图进行降维后进行激活分类。

  • 最后的全连接层参数多,易造成过拟合。因此在很多场景中,可以使用全局平均池化层(Global Average Pooling, GAP)来取代最后的全连接层进行降维。因为GAP是利用池化实现降维,因此减少了参数量,防止过拟合;同时可以实现任意图像尺度的输入

卷积层

  • 将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)

全连接层存在的问题

  • 数据的形状被“忽视”了。比如,输入数据是图像时,图像通常是高、长、通道方向上的3维形状。但是,向全连接层输入时,需要将3维数据拉平为1维数据。图像是3维形状,这个形状中应该含有重要的空间信息。比如,空间上邻近的像素为相似的值、RBG的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等,3维形状中可能隐藏有值得提取的本质模式。但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。
  • 在输入输出的数据形状相同的情况下,全连接层所需要训练的参数远远多于卷积层。例如,输入为32×32×332\times 32\times 3, 输出为28×28×628\times 28\times 6,如果使用全连接层,则需要32×32×3×28×28×614m(million)32\times 32\times 3 \times 28\times 28\times 6 \approx 14m(million)个参数,而如果使用卷积层(6个5×55 \times 5的滤波器)则只需要150个参数,加上偏置也只需要156个参数。

总结:

  • 相比于全连接层,卷积层可以用较少的参数提取有效特征

Parameter sharing: A feature detector (such as a vertical edge detector) that’s useful in one part of the image is probably useful in another part of the image.

卷积运算

乘积累加运算

对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。如下图所示,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和,将这个结果保存到输出的对应位置。把这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。

在这里插入图片描述

偏置

在这里插入图片描述

填充(padding)

在这里插入图片描述

  • 使用填充主要是为了调整输出的大小。比如,对大小为(4, 4) 的输入数据应用(3, 3) 的滤波器时,输出大小变为(2, 2),相当于输出大小比输入大小缩小了2 个元素。这在反复进行多次卷积运算的深度网络会成为问题。为什么呢?因为如果每次进行卷积运算都会缩小空间,那么在某个时刻输出大小就有可能变为1,导致无法再应用卷积运算。为了避免出现这样的情况,就要使用填充。在刚才的例子中,将填充的幅度设为1,那么相对于输入大小(4, 4),输出大小也保持为原来的(4, 4)。因此,卷积运算就可以在保持空间大小不变的情况下将数据传给下一层。

步幅(stride)

  • 应用滤波器的位置间隔称为步幅
    在这里插入图片描述

小结:卷积层的输出特征图的大小

假设输入大小为(H,W)(H,W),滤波器大小为(FH,FW)(FH, FW),输出大小为(OH,OW)(OH,OW),填充为PP,步幅为SS。此时,输出大小为:
OH=H+2PFHS+1OH = \frac{H+2P-FH}{S} + 1OW=W+2PFWS+1OW = \frac{W+2P-FW}{S} + 1
设定的卷积核参数必须使上面两式都能除尽,当输出大小无法除尽时(结果是小数时),需要采取报错等对策。顺便说一下,根据深度学习的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行报错而继续运行。

3维数据的卷积运算

  • 图像是3维数据,除了高、长方向之外,还需要处理通道方向。通道方向上有多个特征图时,按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。
    在这里插入图片描述
    在3维数据的卷积运算中,输入数据和滤波器的通道数要设为相同的值。每个通道的滤波器大小要全部相同

结合方块思考卷积运算

在这里插入图片描述
在这个例子中,数据输出是1张特征图,换句话说,就是通道数为1的特征图。如果要在通道方向上也拥有多个卷积运算的输出,就需要用到多个滤波器(权重)。

在这里插入图片描述
因此,滤波器的形状可以写为(output_channel,input_channel,height,width)(output\_channel, input\_channel, height, width)

如果再加上偏置运算,则结果如下图所示:
在这里插入图片描述

卷积运算的批处理

在这里插入图片描述

1 x 1卷积层

参考:《DIVE INTO DEEP LEARNING》,吴恩达深度学习视频

与全连接层的相似性

1×11×1 卷积层即为卷积窗口形状为 1×11×1的多通道卷积层。因为使用了最小窗口,1×11×1卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式的功能。实际上, 1×11×1 卷积的主要计算发生在通道维上。
在这里插入图片描述
如上图所示,输出中的每个元素来自输入中在高和宽上相同位置的元素在不同通道之间的按权重累加,这就类似于全连接层。
在这里插入图片描述
假设我们将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么 1×11×1 卷积层的作用与全连接层等价,而且还使空间信息自然地传递到后面的层。即1×11×1 卷积层中可以把通道当作特征,高和宽上的每个元素相当于样本。将输出的通道数设置为类别数之后,再接上后面要讲的全局平均池化层,就可以做到用卷积层代替全连接层了!

可以这么理解:把每个通道上的二维数据都看作是全连接层上的一个神经元,那么上图就可以看作是一个3输入2输出的全连接层,(2×3)(2\times3)个卷积核就是全连接层的权重

减小通道数(bottleneck layer)

另外,1×11×1 卷积层还可以达到减小通道数的作用。虽然其他大小的卷积核在配置适当的padding和srtide之后也可以在保持宽高不变的情况下减小通道数,但是1×11×1 卷积层所需的乘法运算次数要少一个数量级。

在这里插入图片描述
如上图所示,用5×55\times5卷积核如果要保持宽高不变,则需要120M次乘法运算

在这里插入图片描述
中间的那一层也常被称为 bottleneck layer(瓶颈层),由上图所示,使用1×11\times 1卷积层明显的降低了计算成本

空洞卷积(Dilated Convolution)

参考:https://www.zhihu.com/question/54149221

空洞卷积最初是为解决图像分割而提出的。常见的图像分割算法通常使用池化层来增大感受野,同时也缩小了特征图尺寸,然后再利用上采样还原图像尺寸。特征图缩小再放大的过程造成了精度损失。而空洞卷积则可以在增大感受野的同时保持特征图的尺寸不变,解决了这个问题。

空洞卷积就是卷积核中间带有一些洞,跳过一些元素进行卷积

标准的卷积过程:
在这里插入图片描述
空洞数为2的空洞卷积:
在这里插入图片描述
可以看出,空洞卷积在不增加参数量的前提下,增大了感受野

缺陷:

  • 网格效应(Gridding Effect):由于空洞卷积是一种稀疏的采样方式,当多个空洞卷积叠加时,有些像素根本没有被利用,会损失信息的连续性与相关性
  • 远距离的信息没有相关性
  • 大的空洞数(dilation rate)对于大物体分割与检测有利,但是对于小物体则有弊无利,如何处理好多尺度问题的检测,是空洞卷积设计的重点

Hybrid Dilated Convolution (HDC)

参考:https://www.zhihu.com/question/54149221

为了弥补空洞卷积的缺陷,HDC被设计了出来

HDC的设计准则

  • 叠加卷积的 dilation rate 不能有大于1的公约数
  • 将 dilation rate 设计成锯齿状结构,例如 [1, 2, 5, 1, 2, 5] 的循环结构
  • 满足下列式子:
    Mi=maxMi+12ri,Mi+12(Mi+1ri),riM_i = max \lceil M_{i+1} - 2r_i, M_{i+1} - 2(M_{i+1} - r_i), r_i \rceil其中rir_iii层的 dilation rate ,MiM_i是在 ii 层的最大dilation rate,那么假设总共有nn层的话,默认 Mn=rnM_n=r_n

一个简单的例子: dilation rate [1, 2, 5] with 3 x 3 kernel (可行的方案)
在这里插入图片描述

池化层

Max池化

池化是缩小高、长方向上的空间的运算。
在这里插入图片描述
一般来说,池化的窗口大小会和步幅设定成相同的值

全局平均池化层(Global Average Pooling, GAP)

参考:https://www.jianshu.com/p/510072fc9c62

在常见的卷积神经网络中,全连接层之前的卷积层负责对图像进行特征提取,在获取特征后,传统的方法是接上全连接层之后再进行激活分类,而GAP的思路是使用GAP来替代该全连接层(即使用池化层的方式来降维)(在NiN网络中,它的前面还接了一个NiN块,其中使用了1×11\times 1卷积层),更重要的一点是保留了前面各个卷积层和池化层提取到的空间信息\语义信息,所以在实际应用中效果提升也较为明显。另外,GAP去除了对输入大小的限制,而且在卷积可视化Grad-CAM中也有重要的应用.

GAP直接从 feature map 的通道信息下手,比如我们现在的分类有N种,那么最后一层的卷积输出的 feature map 就只有N个通道,然后对这个 feature map 进行全局池化操作,获得长度为N的向量,这就相当于直接赋予了每个通道类别的意义

在这里插入图片描述

GAP优点:

  • 由于有GAP,特征图的各个通道可以更直观的被解读为图片属于每个类别的概率
  • 利用池化实现了降维,减少了参数量,防止过拟合
  • 保留了前面各个卷积层和池化层提取到的空间信息\语义信息,更具有鲁棒性
  • 可以实现任意图像尺度的输入

池化层的特征

  • 没有要学习的参数
  • 通道数不发生变化
    在这里插入图片描述
  • 对微小的位置变化具有鲁棒性(健壮)。输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。

因此,池化层可以降低特征图的参数量,提升计算速度,增加感受野,是一种降采样的操作。可是模型更关注全局特征而非局部出现的位置,可提升容错能力,一定程度上防止过拟合

卷积层和池化层的实现

基于im2col的展开

im2col是一个函数,将输入数据展开以适合滤波器(权重)。如下图所示,对3维的输入数据应用im2col后,数据转换为2维矩阵(正确地讲,是把包含批数量的4维数据转换成了2维数据)
在这里插入图片描述
im2col对于输入数据,将应用滤波器的区域(3 维方块)横向展开为1 列。im2col会在所有应用滤波器的地方进行这个展开处理。
在这里插入图片描述
在上图中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处

在这里插入图片描述

  • 下面写一下变换后的各个矩阵的形状以便理解:
    (N,C,H,W)输入数据:(N, C, H, W)im2col(N×OH×OW,C×FH×FW)im2col转换之后的输入数据:(N \times OH\times OW, C\times FH\times FW) (FN,C,FH,FW)滤波器:(FN, C, FH, FW) FN(C×FH×FW,FN)FN个滤波器转换之后的矩阵:(C\times FH\times FW, FN) b(FN,)偏置b:(FN, ) (2)(N×OH×OW,FN)输出数据(2维):(N \times OH\times OW, FN) (N,FN,OH,OW)输出数据:(N, FN, OH, OW)
    可以看出,在输出数据(2维)中,矩阵的一列即为输入特征图经过一个滤波器之后的结果

  • 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

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, out_h, out_w, filter_h, filter_w))

    # x_min 和 y_min 用于界定一个滤波器作用的方块区域
    for y in range(out_h):
        y_min = y * stride
        for x in range(out_w):
            x_min = x * stride
            col[:, :, y, x, :, :] = img[:, :, y_min:y_min+filter_h, x_min:x_min+filter_w]
    col = col.transpose(0, 2, 3, 1, 4, 5).reshape(N*out_h*out_w, -1)

    return col

当然,在反向传播的时候还需要im2col函数的逆处理col2im函数。由于在卷积运算的过程中,滤波器的作用区域可能是重合的,因此img中的一个元素可能会多次出现在col中,根据链式法则,img中元素的偏导即为col中所有该元素所在位置的偏导之和。

  • col2im的代码实现:
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_shape # padding之前的图像大小
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 1, 2, 4, 5)

    img = np.zeros((N, C, H + 2 * pad, W + 2 * pad))
    for y in range(out_h):
        y_min = y * stride
        for x in range(out_w):
            x_min = x * stride
            # 要注意这里是 += 而非 = ,原因就是上面的那段话
            img[:, :, y_min:y_min+filter_h, x_min:x_min+filter_w] += col[:, :, y, x, :, :]

    return img[:, :, pad:H+pad, pad:W+pad]

卷积层的实现

正向传播

利用前面讲的im2col进行快速矩阵运算

反向传播

反向传播类似于全连接层,唯一的区别就是在求得dxdx之后还要通过col2im函数变换形状

简单推导一下反向传播的计算(与之前全连接层的推导是一模一样的,但还是写一下)
为了推理上方便书写,先引入克罗内克符号:
δi,j=1             if i=j\delta_{i,j} = 1 \ \ \ \ \ \ \ \ \ \ \ \ \ if \ i = j δi,j=0             if ij\delta_{i,j} = 0 \ \ \ \ \ \ \ \ \ \ \ \ \ if \ i \neq j
下面正式进行推导:
Lwab=i,jLyijyijwab=i,jLyij(k(xikwkj)+bj)wab=i,jLyijxikδakδbj=iLyibxiaLW=XTLY\begin{aligned} \frac{\partial L}{\partial w_{ab}} &= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial w_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial w_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} x_{ik } \delta_{ak} \delta_{bj} \\&= \sum_{i} \frac{\partial L}{\partial y_{ib}} x_{ia} \\ \therefore \frac{\partial L}{\partial W} &= X^T \cdot \frac{\partial L}{\partial Y} \end{aligned}
Lxab=i,jLyijyijxab=i,jLyij(k(xikwkj)+bj)xab=i,jLyijwkjδaiδbk=jLyajwbjLX=LYWT\begin{aligned} \frac{\partial L}{\partial x_{ab}} &= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial x_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial x_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} w_{kj } \delta_{ai} \delta_{bk} \\&= \sum_{j} \frac{\partial L}{\partial y_{aj}} w_{bj} \\ \therefore \frac{\partial L}{\partial X} &= \frac{\partial L}{\partial Y} \cdot W^T \end{aligned}
Lba=i,jLyijyijba=i,jLyij(k(xikwkj)+bj)ba=i,jLyijδaj=iLyiaLB=LY0\begin{aligned} \frac{\partial L}{\partial b_{a}} &=\sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial b_{a}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial b_{a}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \delta_{aj}\\&= \sum_{i} \frac{\partial L}{\partial y_{ia}} \\ \therefore \frac{\partial L}{\partial B} &= \frac{\partial L}{\partial Y} 的第0轴上的和 \end{aligned}

代码实现

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad

        self.x = None
        self.col = None
        self.col_W = None

        self.db = None
        self.dW = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = (H + 2 * self.pad - FH) // self.stride + 1
        out_w = (W + 2 * self.pad - FW) // self.stride + 1        

        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).reshape(N, out_h, out_w, FN).transpose(0, 3, 1, 2)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
        
        self.db = dout.sum(axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.T.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

Max池化层的实现

正向传播

池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。池化的应用区域应该按通道单独展开。
在这里插入图片描述
也就是说,(N,C,H,W)(N, C, H, W)的输入数据在im2col之后形状变为(N×OH×OW,C×FH×FW)(N \times OH\times OW, C\times FH\times FW),因此还要多加一步,将其reshape为(N×C×OH×OW,FH×FW)(N \times C \times OH \times OW, FH \times FW)

像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的形状即可。转换为最大值之后的矩阵形状为(N×C×OH×OW,1)(N \times C \times OH \times OW, 1),最后将其reshape为(N,C,OH,OW)(N , C, OH, OW)即可
在这里插入图片描述

反向传播

反向传播类似于Relu的反向传播,只要在正向传播时保存好各个滤波器中最大值的索引位置即可

代码实现

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.mask = None

    def forward(self, x):
        N, C, H, W = x.shape

        out_h = (H + 2 * self.pad - self.pool_h) // self.stride + 1
        out_w = (W + 2 * self.pad - self.pool_w) // self.stride + 1

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(N, out_h * out_w, C, self.pool_h * self.pool_w).transpose(0, 2, 1, 3).reshape(N * C * out_h * out_w, self.pool_h * self.pool_w)

        mask = np.argmax(col, axis=1)
        out = col[np.arange(mask.size), mask]
        out = out.reshape(N, C, out_h, out_w)

        self.mask = mask
        self.input_shape = x.shape

        return out

    def backward(self, dout):
        N, C, H, W = self.input_shape

        out_h = (H + 2 * self.pad - self.pool_h) // self.stride + 1
        out_w = (W + 2 * self.pad - self.pool_w) // self.stride + 1

        dout = dout.reshape(N * C * out_h * out_w)
        dcol = np.zeros((N * C * out_h * out_w, self.pool_h * self.pool_w))
        dcol[np.arange(self.mask.size), self.mask] = dout

        dcol = dcol.reshape(N, C, out_h * out_w, self.pool_h * self.pool_w).transpose(0, 2, 1, 3).reshape(N * out_h * out_w, C * self.pool_h * self.pool_w)
        dx = col2im(dcol, self.input_shape, self.pool_h, self.pool_w, self.stride, self.pad)

        return dx
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章