来详解一下计算机视觉中的语义分割任务 (5)—转置卷积

这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战

在 FCN 里提到过转置卷积,不过到底什么是转置卷积,如何通过转置卷积实现上采样,可能会或多或少感到困惑,为此写了这篇文章。

可以一篇文章篇幅不够将转置卷积分享清楚,准备用 2 篇或者更多篇幅来说这件事

在做图像分类我们通常都是将图像通过步长(stride)大于 1 或者池化层不断缩小特征图的宽高,提升通道,来进行下采样

为什么需要上采样

在语义分割网络或者图像生成网络如 GAN 中,神经网络来生成图像时,就需要从低分辨率到高分辨率的上采样。

有各种方法来进行上采样操作。

  • 近邻插值
  • 双线性插值
  • 双三次插值

所有这些方法都是基于些插值来实现上采样,选择了插值方法后采样方法也就确定,并不是可以通过学习而确定的,融入了人为因素。

为什么是转置卷积(Transposed Convolution)

一种理想的上采样方式,可以使用转置卷积。这样一来我们就无需事先将插值方法确定,不是人为设定的,而是通过网络学出来的。

转置卷积方法还被用于其他应用

  • DCGAN 图像生成器接受一些随机数,就可以输出一张完整的图像

  • 在语义分割中,编码器中使用卷积层来提取特征,接下里通过上采样将特征图回复到原有尺寸,并且输出图像对每一个像素进行分类。转置卷积有时候也被叫做

  • Fractionally-strided convolution

  • 反卷积(Deconvolution)

不过个人更喜欢将其叫做转置卷积,所以在此分享中随后都会将其称为转置卷积。

卷积操作

还是通过一个简单例子来解释一下卷积操作(运算)是如何工作的吧。假设有一个 4 × 4 4 \times 4 大小矩阵,然后使用 3 × 3 3 \times 3 大小卷积核对其进行卷积操作,没有添加任何 padding 也就是边缘填充。卷积核在图像每次移动 1 stride。经过卷积会得到 2 × 2 2 \times 2 ,下面是输入矩阵(浅绿色)和卷积核(浅黄色)表示。

transpose_convolution_001.PNG

然后用卷积核在

transpose_convolution_002.PNG

如何用卷积核从左上开始每次移动一个位置,先从左向右移动,然后再从上向下移动,也就是从上至下逐行扫描,每当来到一个位置,就用卷积核中每一个元素和其覆盖的元素对应相乘后再相加的值就是输出该位置上的值。

因为滑动计算带来成本过高,也没有发挥 GPU 的作用,这里用一些小技巧,我们还是以图解方式来说明。这里利用到等效矩阵。

通过上面内容,我们知道卷积核通过移动,而每一次移动会覆盖输入矩阵一部分元素,这样我们可以生产其移动次数个矩阵,矩阵大小和原有矩阵相同,然后将卷积核放置在每一次其在矩阵上出现对应的位置。然后其他位置保持为 0。这样一来将移动动作分解成一定次数两个矩阵逐点相乘的动作,如下图。

transpose_convolution_003.PNG

这里用代码演示一下

import numpy as np
img = np.array([
                [1,2,3,4],
                [2,3,4,5],
                [5,6,7,8],
                [1,2,5,6]
])
kernel = np.array([
                   [1,0,0],
                   [0,1,0],
                   [0,0,1]
])
res = convolve2D(img, kernel, padding=0, strides=1)
复制代码

array([[11., 14.], [13., 16.]])
复制代码

我们继续将几步完成任务合并为一步完成,我们将输入矩阵展平,然后再将上面的每一个步等效矩阵也进行展开,成为两个向量,这两个向量通过点乘可以得到一个数值。

transpose_convolution_005.PNG

然后我们可以将每一步等效矩阵进行堆叠后再和向量相乘就可以得到我们想要一个向量,然后在将向量进行 reshape 就得到等同的结果。

transpose_convolution_006.PNG

卷积运算代码如下

def convolve2D(image, kernel, padding=0, strides=1):
    # Cross Correlation
    kernel = np.flipud(np.fliplr(kernel))

    # Gather Shapes of Kernel + Image + Padding
    xKernShape = kernel.shape[0]
    yKernShape = kernel.shape[1]
    xImgShape = image.shape[0]
    yImgShape = image.shape[1]

    # Shape of Output Convolution
    xOutput = int(((xImgShape - xKernShape + 2 * padding) / strides) + 1)
    yOutput = int(((yImgShape - yKernShape + 2 * padding) / strides) + 1)
    output = np.zeros((xOutput, yOutput))

    # Apply Equal Padding to All Sides
    if padding != 0:
        imagePadded = np.zeros((image.shape[0] + padding*2, image.shape[1] + padding*2))
        imagePadded[int(padding):int(-1 * padding), int(padding):int(-1 * padding)] = image
        print(imagePadded)
    else:
        imagePadded = image

    # Iterate through image
    for y in range(image.shape[1]):
        # Exit Convolution
        if y > image.shape[1] - yKernShape:
            break
        # Only Convolve if y has gone down by the specified Strides
        if y % strides == 0:
            for x in range(image.shape[0]):
                # Go to next row once kernel is out of bounds
                if x > image.shape[0] - xKernShape:
                    break
                try:
                    # Only Convolve if x has moved by the specified Strides
                    if x % strides == 0:
                        output[x, y] = (kernel * imagePadded[x: x + xKernShape, y: y + yKernShape]).sum()
                except:
                    break

    return output
复制代码

Guess you like

Origin juejin.im/post/7067368840992129054