Chapter 6 卷积神经网络(CNN)(二)

&sect 6.2 图像卷积

前文提及了卷积的相关定义,也提及了在 DL 中的卷积实际上是互相关(corss-correlation)。那么在卷积层中,输入张量和核张量通过互相关运算产生输出张量

比如:

考虑输入张量 $X = \begin{bmatrix}1&2&3\\4&5&6\\7&8&9\end{bmatrix}$,核张量 $K = \begin{bmatrix}1&2\\3&4\end{bmatrix}$那么依据卷积核 $K$ 的形状, $X$ 内存在四个卷积窗口:

$$x_1=\begin{bmatrix}1&2\\4&5\end{bmatrix},x_2=\begin{bmatrix}2&3\\5&6\end{bmatrix}\\x_3=\begin{bmatrix}3&4\\7&8\end{bmatrix},x_4=\begin{bmatrix}5&6\\8&9\end{bmatrix}$$

以窗口 $x_1$ 为例,卷积运算得到输出张量中的元素 $y_1$:

$$\begin{align}y_1&=K\odot x_1 =\begin{bmatrix}1&2\\3&4\end{bmatrix}\odot \begin{bmatrix}1&2\\4&5\end{bmatrix}\\&=1\times 1+2\times 2 +3\times 4+4\times 5\\&=1+4+12+20\\&=37\end{align}$$

所以最后的输出张量 $y\in\mathbb{R}^{2\times2}$

不难总结出对于输入张量 $X\in\mathbb{R}^{a\times b}$,卷积核 $K\in\mathbb{R}^{m\times n}$,输出张量 $Y$ 的规模为:

$$Y\in\mathbb{R}^{(a-m+1)\times(b-n+1)}$$

这里注意到直接使用原始图像时因为窗口大小需要,丢失了边缘像素,那么为了解决相关问题,最简单的方法是引入填充(Padding)

那么在引入填充 $\vec{p}=(p_h, p_w)$ 的情况下,此时的输出张量规模公式变更为:

$$Y\in\mathbb{R}^{(a+2\times p_h-m+1)\times(b+2\times p_w-n+1)}$$

[注]:此处的padding类似于 HTML ,在不特别指明哪条边界的情况下默认各边界均扩展 $p_w$ px

除了扩展原始图像,我们同样也可以控制卷积核的移动速率,也就是卷积核移动的步幅(Stride),它表示当前窗口滑动元素的数量,记为 $\vec{s}=(s_h,s_w)$

此时的输出张量规模计算公式变更为:

$$Y\in\mathbb{R}^{(\lfloor\frac{a+2\times p_h-m}{s_h}\rfloor + 1)\times(\lfloor\frac{b+2\times p_w-n}{s_w}\rfloor+1)}$$

由于引入了整数除法,所以建议在设置超参数的时候注意使填充后的图像元素能够整除步幅,即 $ (p+x)\parallel s$

那么基于上述分析,图像卷积的基本流程也就不难实现了。

import torch

def corr2d(X, K, padding=[0,0], stride=[1,1]):
    """ 二维互相关运算 """
    h, w = K.shape
    # padding 非零时,对X进行零填充
    if padding[0] > 0 or padding[1] > 0:
        X_padded = torch.zeros((X.shape[0] + 2 * padding[0], X.shape[1] + 2 * padding[1]))
        X_padded[padding[0]:padding[0] + X.shape[0], padding[1]:padding[1] + X.shape[1]] = X
        X = X_padded
    # 计算输出形状
    output_height = (X.shape[0] - h) // stride[0] + 1
    output_width = (X.shape[1] - w) // stride[1] + 1
    Y = torch.zeros((output_height, output_width))
    for i in range(output_height):
        for j in range(output_width):
            Y[i, j] = (X[i*stride[0]:i*stride[0]+h, j*stride[1]:j*stride[1]+w] * K).sum()
    return Y

if __name__ == '__main__':
    # 构建对角线边缘图像
    X = torch.ones((6, 6))
    for i in range(6):
        X[i, i] = 0

    # 构建卷积核并计算特征
    print(f'X=\n{X}')
    K = torch.tensor([[1, -1]])
    # 水平边缘检测
    Y = corr2d(X, K)
    print(f'Y=\n{Y}')
    # 竖直边缘检测
    Y_transport_k = corr2d(X, K.t())
    print(f'Y_transport_k=\n{Y_transport_k}')

优化:卷积窗口的 $\odot$ 运算

前面我们提及,对于卷积窗口i,卷积核 $K$ 和 窗口矩阵 $x_i$需要进行逐元素乘法,也就是 $\odot$ ,代码中我们使用的是暴力循环实现,这在面对规模较大的运算时会带来可怕的性能瓶颈,事实上, 在 pytorch 中,这一过程是由 im2col() 进行优化的,这个函数是将输入数据(通常是图像)转换成列矩阵的形式,以便于进行卷积运算,也就是将逐元素乘法转化为了矩阵乘法以提高性能,仍然采取上例中的输入张量 $X$ 和卷积核 $K$ ,那么具体的数学过程包括:

1. 将卷积核按行展平为行向量:

$$\vec{K}=[1,\ 2,\ 3,\ 4]$$

2. 将每个卷积窗口矩阵同样按行展平,但是展平为列向量(以 $x_1$ 为例):

$$\vec{x_1}=[1,\ 2,\ 4,\ 5]^T$$

3. 合并所有展平的窗口向量得到新的输入张量$X’$:

$$X’=[\vec{x_1},\vec{x_2},\dots,\vec{x_n}]$$

4. 矩阵乘法得到输出向量$\vec{y}$:

$$\vec{y}=\vec{K}\cdot X’=[y_1,\ \dots,\ y_n]$$

5. 按窗口位置 reshape $\vec{y}$ 得到输出张量 $Y$

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注