(pytorch-深度学习系列)卷积神经网络中的填充(padding)和步幅(stride)

卷积神经网络中的填充(padding)和步幅(stride)

之前写过一篇blog,描述CNN网络层的输入和输入尺寸的计算关系,但是并没有描述的很全面,这里全面描述了影响输出尺寸的两个超参数padding和stride,查阅了相关资料,编码理解了pytorch中CNN网络的输入输出关系。

对于CNN网络,一般来说,假设输入形状是 n h × n w n_h\times n_w nh×nw,卷积核窗口形状是 k h × k w k_h\times k_w kh×kw,那么输出形状将会是

( n h − k h + 1 ) × ( n w − k w + 1 ) . (n_h-k_h+1) \times (n_w-k_w+1). (nhkh+1)×(nwkw+1).
所以卷积层的输出形状由输入形状和卷积核窗口形状决定。卷积层还有两个超参数,即填充和步幅。它们可以对给定形状的输入和卷积核改变输出形状。

填充(padding)

填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。

对于输入:
i n p u t = [ 0 1 2 3 4 5 6 7 8 ] input = \begin{bmatrix} 0 & 1 & 2 \\ 3 & 4 &5 \\ 6 & 7 & 8 \end{bmatrix} input=036147258

我们在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽由2增加到4:

[ 0 1 2 3 4 5 6 7 8 ] → [ 0 0 0 0 0 0 0 1 2 0 0 3 4 5 0 0 6 7 8 0 0 0 0 0 0 ] \begin{bmatrix} 0 & 1 & 2 \\ 3 & 4 &5 \\ 6 & 7 & 8 \end{bmatrix} \rightarrow \begin{bmatrix} 0 & 0&0&0&0\\ 0&0&1&2&0 \\ 0&3&4&5&0\\0&6&7&8&0\\0&0&0&0&0\end{bmatrix} 0361472580000000360014700258000000

一般来说,如果在高的两侧一共填充 p h p_h ph行,在宽的两侧一共填充 p w p_w pw列,那么输出形状将会是

( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) , (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1), (nhkh+ph+1)×(nwkw+pw+1),

也就是说,输出的高和宽会分别增加 p h p_h ph p w p_w pw

[ 0 0 0 0 0 0 0 1 2 0 0 3 4 5 0 0 6 7 8 0 0 0 0 0 0 ] ∗ [ 0 1 2 3 ] = [ 0 3 8 4 9 19 25 10 21 37 43 16 6 7 8 0 ] \begin{bmatrix} 0 & 0&0&0&0\\ 0&0&1&2&0 \\ 0&3&4&5&0\\0&6&7&8&0\\0&0&0&0&0\end{bmatrix} * \begin{bmatrix} 0&1\\2&3 \end{bmatrix} = \begin{bmatrix} 0&3&8&4\\9 &19& 25& 10\\21 &37& 43& 16\\6 &7 &8 &0 \end{bmatrix} 0000000360014700258000000[0213]=09216319377825438410160

在很多情况下,我们会设置
p h = k h − 1 p w = k w − 1 p_h=k_h-1 \\ p_w=k_w-1 ph=kh1pw=kw1

使输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。假设这里 k h k_h kh是奇数,我们会在高的两侧分别填充 p h / 2 p_h/2 ph/2行。如果 k h k_h kh是偶数,一种可能是在输入的顶端一侧填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ph/2行,而在底端一侧填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ph/2行。在宽的两侧填充同理。

卷积神经网络经常使用奇数高宽的卷积核,如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):
    X = X.view((1, 1) + X.shape)# (1, 1)代表批量大小和通道数,均为1
    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

输出:

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

输出不变

步幅(stride)

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

目前我们看到的例子里,在高和宽两个方向上步幅均为1, 我们也可以使用更大的步幅。

在下面的问题中:
在高上步幅为3、在宽上步幅为2。可以看到,输出第一列第二个元素时,卷积窗口向下滑动了3行,而在输出第一行第二个元素时卷积窗口向右滑动了2列。当卷积窗口在输入上再向右滑动2列时,由于输入元素无法填满窗口,无结果输出。

[ 0 0 0 0 0 0 0 1 2 0 0 3 4 5 0 0 6 7 8 0 0 0 0 0 0 ] ∗ [ 0 1 2 3 ] = [ [ 0 1 2 3 ] ∗ [ 0 0 0 0 ] [ 0 1 2 3 ] ∗ [ 0 0 1 2 ] [ 0 1 2 3 ] ∗ [ 0 6 0 0 ] [ 0 1 2 3 ] ∗ [ 7 7 0 0 ] ] = [ 0 8 6 8 ] \begin{bmatrix} 0 & 0&0&0&0\\ 0&0&1&2&0 \\ 0&3&4&5&0\\0&6&7&8&0\\0&0&0&0&0\end{bmatrix} * \begin{bmatrix} 0&1\\2&3 \end{bmatrix} = \begin{bmatrix} {\begin{bmatrix} 0&1\\2&3 \end{bmatrix}* \begin{bmatrix} 0&0\\0&0 \end{bmatrix} \quad \begin{bmatrix} 0&1\\2&3 \end{bmatrix}* \begin{bmatrix} 0&0\\1&2 \end{bmatrix}}\\ \\ \\ {\begin{bmatrix} 0&1\\2&3 \end{bmatrix}* \begin{bmatrix} 0&6\\0&0 \end{bmatrix} \quad \begin{bmatrix} 0&1\\2&3 \end{bmatrix}* \begin{bmatrix} 7&7\\0&0 \end{bmatrix}} \end{bmatrix} = \begin{bmatrix} 0&8 \\ 6&8\end{bmatrix} 0000000360014700258000000[0213]=[0213][0000][0213][0102][0213][0060][0213][7070]=[0688]

一般来说,当高上步幅为 s h s_h sh,宽上步幅为 s w s_w sw时,输出形状为

⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / 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. (nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw.

如果设置
p h = k h − 1 p w = k w − 1 p_h=k_h-1\\p_w=k_w-1 ph=kh1pw=kw1
那么输出形状将简化为
⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor (nh+sh1)/sh×(nw+sw1)/sw
更进一步,如果输入的高和宽能分别被高和宽上的步幅整除,那么输出形状将是
( n h / s h ) × ( n w / s w ) ( 因 为 上 式 中 是 向 下 取 整 ) (n_h/s_h) \times (n_w/s_w) (因为上式中是向下取整) (nh/sh)×(nw/sw)()
·
·
我们令高和宽上的步幅均为2,从而使输出的高和宽减半。

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

输出:

torch.Size([4, 4])

代入公式,这里应该这么算:
⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ = ⌊ ( 8 − 3 + 1 × 2 + 2 ) / 2 ⌋ × ⌊ ( 8 − 3 + 1 × 2 + 2 ) / 2 ⌋ = 4 × 4 \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 = \\\lfloor(8-3+1\times2+2)/2\rfloor \times \lfloor(8-3+1\times2+2)/2\rfloor = 4 \times 4 (nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw=(83+1×2+2)/2×(83+1×2+2)/2=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])

代入公式,这里应该这么算:
⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ = ⌊ ( 8 − 3 + 0 × 2 + 3 ) / 3 ⌋ × ⌊ ( 8 − 5 + 1 × 2 + 4 ) / 4 ⌋ = 2 × 2 \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 = \\\lfloor(8-3+0\times2+3)/3\rfloor \times \lfloor(8-5+1\times2+4)/4\rfloor = 2 \times 2 (nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw=(83+0×2+3)/3×(85+1×2+4)/4=2×2