假设输入形状是$n_h\times n_w$,卷积核窗口形状是$k_h\times k_w$,那么输出形状将会是

$$
(n_h-k_h+1) \times (n_w-k_w+1).
$$

所以卷积层的输出形状由输入形状和卷积核窗口形状决定。卷积层有两个超参数,即填充和步幅。它们可以对给定形状的输入和卷积核改变输出形状。

填充

填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。下图里在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽由2增加到4。图中的阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times0+0\times1+0\times2+0\times3=0$。

一般来说,如果在高的两侧一共填充$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_w-1$来使输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。假设这里$k_h$是奇数,会在高的两侧分别填充$p_h/2$行。如果$k_h$是偶数,一种可能是在输入的顶端一侧填充$\lceil p_h/2\rceil$行,而在底端一侧填充$\lfloor p_h/2\rfloor$行。在宽的两侧填充同理。

卷积神经网络经常使用奇数高宽的卷积核,如1、3、5和7,所以两端上的填充个数相等。对任意的二维数组X,设它的第i行第j列的元素为X[i,j]。当两端上的填充个数相等,并使输入和输出具有相同的高和宽时,就知道输出Y[i,j]是由输入以X[i,j]为中心的窗口同卷积核进行互相关计算得到的。

下面的例子里创建一个高和宽为3的二维卷积层,然后设输入高和宽两侧的填充数分别为1。给定一个高和宽为8的输入,发现输出的高和宽也是8。

import torch
from torch import nn

# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
    # (1, 1)代表批量大小和通道数均为1
    X = X.view((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.view(Y.shape[2:])  # 排除不关心的前两维:批量和通道

# 注意这里是两侧分别填充1行或列,所以在两侧一共填充2行或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)

X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape

升维操作:X.shape 假设为 (8, 8),即输入张量是一个二维数组。view 方法用于重塑张量的形状。(1, 1) + X.shape 表示在张量前面添加两个维度:

  • 第一个 1 表示批量大小(batch size),即一次处理一个样本。
  • 第二个 1 表示通道数(channel number),即输入数据只有一个通道(灰度图像)。

最终,X 的形状从 (8, 8) 变为 (1, 1, 8, 8),符合卷积层的输入要求([batch_size, channels, height, width])。

降维操作: 卷积层的输出 Y 的 形状通常为 [batch_size, channels, height, width]。这里通过 Y.view(Y.shape[2:]) 去掉了前两维(批量大小和通道数),只保留空间维 度(高度和宽度)。

定义二维卷积层: nn.Conv2d,其参数如下:

  • in_channels=1: 输入张量的通道数为 1(例如灰度图像)。
  • out_channels=1: 输出张量的通道数为 1。
  • kernel_size=3: 卷积核的大小为 3×3。
  • padding=1: 在输入张量的每一边填充 1 行/列,使得输出的空间尺寸与输入相同(假设步幅为 1)。

输出:

torch.Size([8, 8])

当卷积核的高和宽不同时,也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。

# 使用高为5、宽为3的卷积核。在高和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

输出:

torch.Size([8, 8])

步幅

在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。将每次滑动的行数和列数称为步幅(stride)。

目前看到的例子里,在高和宽两个方向上步幅均为1。下图展示了在高上步幅为3、在宽上步幅为2的二维互相关运算。图中的阴影部分为输出元素及其计算所使用的输入和核数组元素:$0\times0+0\times1+1\times2+2\times3=8$、$0\times0+6\times1+0\times2+0\times3=6$。

一般来说,当高上步幅为$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(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape

输出:

torch.Size([4, 4])

接下来是一个稍微复杂点儿的例子。

conv2d = nn.Conv2d(1, 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。


循之际,如星夜般的幻想。