填充和步幅(padding and stride)

我们都知道图像在经过卷积处理后,输出图像的尺寸一般会变小,假设输入图像尺寸为\(n_h \times n_w\),卷积核尺寸为\(k_h \times k_w\),经过卷积处理后,输出图像的尺寸为\((n_h-k_h+1)\times (n_w-k_w+1)\)。但是这个过程有个弊端,那就是在经过多个卷积层后,图像的尺寸会变得很小,这里我们做一个假设:假设我们有一张\(240 \times 240\)像素的图片,在经过10层$5\times5 $卷积核的卷积后,输出图像的尺寸为\(200\times 200\),如此一来,原始图片的边缘就丢失了很多有用的信息,因此,我们引入了填充的概念。除此之外,我们在卷积时,卷积核时每次滑动一步来处理图像,这样有时会显得效率过低,于是我们引入了步幅(stride)的概念。

填充(padding)

如上所述,在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充:在输入图像的边界填充元素(通常填充元素是0)。 例如,在下中,我们将\(3\times3\)输入填充到\(5 \times 5\),那么它的输出就增加为\(4 \times 4\)。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素: \(0\times 0 + 0 \times 1 + 0\times 2 + 0\times 3\)

假设我们添加\(p_h\)行填充,\(p_w\)列填充,则输出形状为:

\[(n_h - k_h + p_h + 1) \times (n_w - k_w + p_w + 1) \]

这意味着输出的高度和宽度将分别增加\(p_h\)\(p_w\)

在许多情况下,我们需要设置\(p_h=k_h-1\)\(p_w=k_h-1\),使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。

卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

对于任何二维张量X,当满足: 1. 卷积核的大小是奇数; 2. 所有边的填充行数和列数相同; 3. 输出与输入具有相同高度和宽度 则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。

比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素。给定高度和宽度为8的输入,则输出的高度和宽度也是8。

import torch
import torch.nn as nn

# 定义一个函数,用于计算卷积操作
def comp_conv2d(conv2d, X):
    # 将输入X的形状调整为(1, 1, X的形状)
    X = X.reshape((1, 1) + X.shape)
    # 对输入X进行卷积操作
    Y = conv2d(X)
    # 将输出Y的形状调整为(Y的形状[2:],即去掉前两个维度)
    return Y.reshape(Y.shape[2:])

# 定义一个卷积层,输入通道数为1,输出通道数为1,卷积核大小为3,padding为1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)
# 生成一个8x8的随机数矩阵作为输入
X = torch.rand(8, 8)
# 计算卷积操作的结果,并输出其形状
comp_conv2d(conv2d, X).shape

# 输出
# torch.Size([8, 8])
torch.Size([8, 8])

当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。

# 定义一个卷积层,输入通道数为1,输出通道数为1,卷积核大小为(5, 3),填充大小为(2, 1)
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
# 调用comp_conv2d函数,传入卷积层和输入数据X,输出卷积后的数据形状
comp_conv2d(conv2d, X).shape
torch.Size([8, 8])

步幅(Stride)

在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

我们将每次滑动元素的数量称为步幅。到目前为止,我们只使用过高度或宽度为\(1\)的步幅,那么如何使用较大的步幅呢? 下图垂直步幅为\(3\),水平步幅为\(3\)的二维互相关运算。 着色部分是输出元素以及用于输出计算的输入和内核张量元素:\(0\times0 + 0\times1+1\times2+2\times3\)\(0\times0+6\times1+0\times2+0\times3\)

可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。

通常,当垂直步幅为\(s_h\)、水平步幅为\(s_w\)时,输出形状为:

\[\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor. \]

如果我们设置了\(p_h=k_h-1\)\(p_w=k_w-1\),则输出形状将简化为\(\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor\)。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为\((n_h/s_h) \times (n_w/s_w)\)

下面,我们将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半。

conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape

# 输出
# torch.Size([4, 4])
torch.Size([4, 4])

接下来,再看一个稍微复杂点的例子。

conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
torch.Size([2, 2])

为了简洁起见,当输入高度和宽度两侧的填充数量分别为\(p_h\)\(p_w\)时,我们称之为填充\((p_h, p_w)\)。当\(p_h = p_w = p\)时,填充是\(p\)。同理,当高度和宽度上的步幅分别为\(s_h\)\(s_w\)时,我们称之为步幅\((s_h, s_w)\)。特别地,当\(s_h = s_w = s\)时,我们称步幅为\(s\)。默认情况下,填充为0,步幅为1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有\(p_h = p_w\)\(s_h = s_w\)

总结

填充的目的是为了减少图像边缘信息的损失,而采用合适步幅的目的是提升图像卷积运算的效率,并降低冗余像素的扫描次数。

posted @ 2024-08-02 10:02  codersgl  阅读(70)  评论(0编辑  收藏  举报