卷积与池化的探究

Python实现

Posted by Welt Xing on August 22, 2021

这里我们想用Python实现CNN中的卷积和池化操作,同时观察效果(代码在最下方)。

前篇:Understanding of a Convolutional Neural - Welt Xing’s Blog (welts.xyz)

卷积

不同卷积核下的效果展示

我们接下来会用这张图片作为原始图片进行卷积操作:

origin

在论文《Understanding of a Convolutional Neural Network 》中,作者列举了几种常见卷积核,用于不同目的:

  • “Identity”,这里是指卷积后图片不发生变化:

    \[\begin{bmatrix} 0&0&0\\ 0&1&0\\ 0&0&0\\ \end{bmatrix}\]

    图片变换前和变换后看起来一模一样;

  • 边缘检测,用于凸显边缘的三种卷积核:

    \[\begin{bmatrix} 1&0&-1\\ 0&0&0\\ -1&0&1\\ \end{bmatrix},\begin{bmatrix} 0&1&0\\ 1&-4&1\\ 0&1&0\\ \end{bmatrix},\begin{bmatrix} -1&-1&-1\\ -1&8&-1\\ -1&-1&-1\\ \end{bmatrix}\]

    对图片进行卷积后的结果从左到右依次如下:

    edge

  • 锐化:

    \[\begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0\\ \end{bmatrix}\]

    锐化操作使图片边缘更加清晰,可以看到图中的边沿明显加粗:

    sharpen

  • 模糊,有两种,分别是Box blur和Gaussian blur:

    \[\frac19\begin{bmatrix} 1&1&1\\ 1&1&1\\ 1&1&1\\ \end{bmatrix},\frac1{16}\begin{bmatrix} 1&2&1\\ 2&4&2\\ 1&2&1\\ \end{bmatrix}\]

    第一种卷积核中元素均匀分布,而第二种类似一个高斯分布,效果如下:

    blur

    看起来Box blur模糊的效果更加明显,Gaussian模糊仍保留了一些原图特性。

Stride和Padding

默认Stride是1,如果将Stride设为2、3这样大于1的值,那么图片会缩小,我们设stride=3,观察卷积效果:

stride

上面三张图片对应三种边缘检测矩阵,下面三张图片从左到右分别使用的是Identity卷积核,锐化卷积核和高斯模糊卷积核。可以发现,卷积后图片的特征被提取了出来,但变得更加模糊。

Padding通过在图片周围加0,缓解了多层卷积时图片尺寸的急剧减少,我们设stride=3,padding=25(使效果更明显):

padding

大部分卷积核下,padding相当于给图片加了黑边,有一个卷积核对应的图片多了白色边框,无伤大雅。此外,我们发现,和上面不加padding相比,图片会更大一点。

池化

池化层旨在降低参数数量,有两种方法:平均池化和最大池化。我们先用边缘检测卷积核对图片进行stride=1的卷积,然后分别进行最大池化和平均池化,其中步长和池化尺寸都是3:

pool

尺寸减小的同时,图片的分辨率下降(更抽象);此外,最大池化下的图片的亮度更高,和预期相符。

多层卷积与池化

当前流行的卷积神经网络框架,常常会用到多层卷积,我们尝试对图片进行多层卷积和池化,观察效果。AlexNet有5个卷积层和2个全连接层。在第一、第二和第五卷积层之后进行最大池化。

alexnet

仿照AlexNet的处理步骤,我们分别用Identity,边缘检测,锐化,模糊卷积核进行卷积,第1,2,5层进行池化,效果从左到右如上所示。此时图片尺寸已经从(608,482)锐减到(72,56),而仍可以看原图的一些细节。

代码参考

我们在这里列出上面进行实验的代码:

# 论文中列举出的7中卷积核
kernels = [
    np.array([
        [0, 0, 0],
        [0, 1, 0],
        [0, 0, 0],
    ]),
    np.array([
        [1, 0, -1],
        [0, 0, 0],
        [-1, 0, 1],
    ]),
    np.array([
        [0, 1, 0],
        [1, -4, 1],
        [0, 1, 0],
    ]),
    np.array([
        [-1, -1, -1],
        [-1, 8, -1],
        [-1, -1, -1],
    ]),
    np.array([
        [0, -1, 0],
        [-1, 5, -1],
        [0, -1, 0],
    ]),
    np.ones((3, 3)) / 9,
    np.array([
        [1, 2, 1],
        [2, 4, 2],
        [1, 2, 1],
    ]) / 16,
]

def conv2d(array, stride=1, padding=0, kernel=0):
    '''
    卷积操作
    
    parameters
    ----------
    array : 输入的图片三维张量
    stride: 卷积的步长
    padding: 对图片进行0填充
    kernel: 上面卷积核的index,比如为0则选用kernels[0]
    
    return
    ------
    ret : 卷积后的三维张量
    '''
    kernel = np.expand_dims(kernels[kernel], -1)
    kernel_size = kernel.shape[0]

    im_height, im_width, tunnel = array.shape

    # padding
    if padding != 0:
        col = np.zeros((im_height, padding, tunnel))
        raw = np.zeros((padding, im_width + 2 * padding, tunnel))
        array = np.hstack((col, array, col))
        array = np.vstack((raw, array, raw))

    height = 1 + (im_height + 2 * padding - kernel_size) // stride
    width = 1 + (im_width + 2 * padding - kernel_size) // stride

    ret = np.empty((height, width, tunnel))

    for i in range(0, height):
        for j in range(0, width):
            ret[i, j] = np.sum(
                kernel * array[i * stride:i * stride + kernel_size,
                               j * stride:j * stride + kernel_size],
                axis=(0, 1),
            )
	
    return ret

def pooling(array, pool_size=2, stride=2, padding=0, method="max"):
    '''
    池化操作
    
    Parameters
    ----------
    array : 输入的图片三维张量
    pool_size : 池化计算的尺寸
    stride: 池化的步长
    padding: 对图片进行0填充
    method: 池化方法,"max"则是最大池化,"mean"则是平均池化
    
    return
    ------
    ret : 卷积后的三维张量
    '''
    im_height, im_width, tunnel = array.shape

    if padding != 0:
        col = np.zeros((im_height, padding, tunnel))
        raw = np.zeros((padding, im_width + 2 * padding, tunnel))
        array = np.hstack((col, array, col))
        array = np.vstack((raw, array, raw))

    height = 1 + (im_height + 2 * padding - kernel_size) // stride
    width = 1 + (im_width + 2 * padding - kernel_size) // stride

    ret = np.empty((height, width, tunnel))

    pool_method = {"max": np.max, "mean": np.mean}[method]

    for i in range(0, height):
        for j in range(0, width):
            ret[i, j] = pool_method(
                array[i * stride:i * stride + kernel_size,
                      j * stride:j * stride + kernel_size],
                axis=(0, 1),
            )

    return ret