在前面的例子 图6.2.1中,输入的高度和宽度都为
3
3
3,卷积核的高度和宽度都为
2
2
2,生成的输出表征的维数为
2
×
2
2\times 2
2×2。 正如我们在 6-2节中所概括的那样,假设输入形状为
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)
(nh−kh+1)×(nw−kw+1)。 因此,卷积的输出形状取决于输入形状和卷积核的形状。
还有什么因素会影响输出的大小呢?本节我们将介绍填充(padding)和步幅(stride)。假设以下情景:
- 有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 1 1 1所导致的。比如,一个 240 × 240 240\times 240 240×240像素的图像,经过 10 10 10层 5 × 5 5\times 5 5×5的卷积后,将减少到 200 × 200 200\times 200 200×200像素。如此一来,原始图像的边界丢失了许多有用信息。而填充是解决此问题最有效的方法;
- 有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅则可以在这类情况下提供帮助。
填充
如上所述,在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。
解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是
0
0
0)。 例如,在 图6.3.1中,我们将
3
×
3
3\times 3
3×3输入填充到
5
×
5
5\times 5
5×5,那么它的输出就增加为
4
×
4
4\times 4
4×4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
0
×
0
+
0
×
1
+
0
×
2
+
0
×
3
=
0
0\times 0+0\times 1+0\times 2+ 0\times 3=0
0×0+0×1+0×2+0×3=0。
通常,如果我们添加
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)
(nh−kh+ph+1)×(nw−kw+pw+1)
这意味着输出的高度和宽度将分别增加
p
h
p_{h}
ph和
p
w
p_{w}
pw。
在许多情况下,我们需要设置
p
h
=
k
h
−
1
p_{h}=k_{h}-1
ph=kh−1和
p
w
=
k
w
−
1
p_{w}=k_{w}-1
pw=kw−1,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。假设
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
X
X,当满足:
- 卷积核的大小是奇数;
- 所有边的填充行数和列数相同;
- 输出与输入具有相同高度和宽度
则可以得出:输出 Y [ i , j ] Y[i, j] Y[i,j]是通过以输入 X [ i , j ] X[i, j] X[i,j]为中心,与卷积核进行互相关计算得到的。
比如,在下面的例子中,我们创建一个高度和宽度为 3 3 3的二维卷积层,并在所有侧边填充 1 1 1个像素。给定高度和宽度为 8 8 8的输入,则输出的高度和宽度也是 8 8 8。
在卷积神经网络中,如果你创建了一个高度和宽度都为 3 3 3的二维卷积层,并在所有侧边填充 1 1 1个像素,然后用这个卷积层处理一个高度和宽度都为 8 8 8的输入,输出的高度和宽度仍然是 8 8 8。下面是为什么这样设置可以达到这个效果的解释:
卷积核尺寸:在这个例子中,卷积核(filter)的高度和宽度 k h = k w = 3 k_h = k_w = 3 kh=kw=3。这意味着每个卷积核覆盖了输入数据的 3 × 3 3 \times 3 3×3区域。
填充大小:为了确保输出尺寸与输入尺寸相同,我们使用了填充。填充是在输入张量的边界添加额外的行和列,这里填充的尺寸 p h = p w = 2 p_h = p_w = 2 ph=pw=2。这意味着在输入的每一边(顶部、底部、左侧和右侧)都加上了一个像素的填充。
输出尺寸的计算: 通常输出尺寸的公式是 ( 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) (nh−kh+ph+1)×(nw−kw+pw+1)。 在这个例子中,输入尺寸 n h = n w = 8 n_h = n_w = 8 nh=nw=8,卷积核尺寸 k h = k w = 3 k_h = k_w = 3 kh=kw=3,填充 p h = p w = 2 p_h = p_w = 2 ph=pw=2。 代入公式得到输出尺寸为 $(8 - 3 + 2 + 1) \times (8 - 3 + 2 +
- = 8 \times 8$。
保持尺寸相同:通过添加等量的填充,卷积操作可以在不改变数据的高度和宽度的情况下进行。这样做的好处是简化了网络结构的设计,因为你可以确保无论添加多少这种类型的层,输入与输出的尺寸都保持一致,这使得每层的输出都可以明确计算和预见。因此,通过在高度和宽度为 3 3 3的卷积层周围加上 1 1 1像素的填充(这里说的填充,是加在输入张量上的,周围加1,即为共加了 2 2 2行,加了 2 2 2列),并处理高度和宽度为 8 8 8的输入,可以确保输出仍然保持 8 × 8 8 \times 8 8×8的尺寸,这有助于在设计深层网络时维持结构的一致性。
import torch
from torch import nn
# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
# 接受两个参数:conv2d(一个卷积层对象)和 X(输入数据张量)
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
# 将输入张量 X 的形状重塑为 (1, 1, height, width)
# 这里的 (1, 1) 分别表示批量大小和通道数都是 1
# PyTorch 的卷积层通常期望输入的形状为 (batch_size, channels, height, width)。
Y = conv2d(X)
# conv2d 对象被应用于已调整形状的输入 X
# conv2d 是一个卷积层,它执行卷积运算并输出结果 Y。
return Y.reshape(Y.shape[2:]) # 省略前两个维度:批量大小和通道
# 去除 Y 的前两个维度(批量大小和通道数),只保留高度和宽度,这样输出就是卷积结果的空间维度。
# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
# 创建了一个 Conv2d 卷积层,具有 1 个输入通道和 1 个输出通道,卷积核大小为 3(3x3),并且在所有边界上填充了 1 个像素(这样做可以保持输入和输出的空间尺寸相同)。
# padding=1意思是在上下左右各填充一行!
X = torch.rand(size=(8, 8))
# 生成一个形状为 (8, 8) 的张量,填充了 0 到 1 之间的随机数,代表一个 8x8 的图像数据。
comp_conv2d(conv2d, X).shape
# 使用定义好的 comp_conv2d 函数来计算卷积,并输出处理后的数据的形状。
# 由于卷积层使用了填充,输入和输出的形状应该是相同的,即 (8, 8)。
当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
这个例子其实也说明了,
p
h
p_{h}
ph和
p
w
p_{w}
pw在实际使用的时候可以分别设置不一样的。
巧记
-
p h = k h − 1 p_{h}=k_{h}-1 ph=kh−1和 p w = k w − 1 p_{w}=k_{w}-1 pw=kw−1
p h p_{h} ph是指在高度上,在原始输入的上部和下部,一共添加了多少行
p w p_{w} pw是指在宽度上,在原始输入的左边和右边,一共添加了多少行 -
nn.Conv2d
中的padding=(x,y)
,这里的(x,y)
指的是,在上部和下部分别添加x
行,在左边和右边分别添加y
列,padding
参数的值是 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) (nh−kh+ph+1)×(nw−kw+pw+1)
-
卷积神经网络中卷积核的高度和宽度通常为奇数
步幅
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
我们将每次滑动元素的数量称为步幅(stride)。到目前为止,我们只使用过高度或宽度为 1 1 1的步幅,那么如何使用较大的步幅呢? 图6.3.2是垂直步幅为 3 3 3,水平步幅为 2 2 2的二维互相关运算。 着色部分是输出元素以及用于输出计算的输入和内核张量元素: 0 × 0 + 0 × 1 + 1 × 2 + 2 × 3 = 8 0\times 0+0\times 1+1\times2+2\times3=8 0×0+0×1+1×2+2×3=8、 0 × 0 + 6 × 1 + 0 × 2 + 0 × 3 = 6 0\times0+6\times1+0\times2+0\times3=6 0×0+6×1+0×2+0×3=6。
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。
通常,当垂直步幅为
s
h
s_{h}
sh、水平步幅为
s
w
s_{w}
sw时,输出形状为:
下面,我们将高度和宽度的步幅设置为
2
2
2,从而将输出的高度和宽度减半。
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
接下来,看一个稍微复杂的例子。
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
为了简洁起见,当输入高度和宽度两侧的填充数量分别为
p
h
p_{h}
ph和
p
w
p_{w}
pw时,我们称之为填充
(
p
h
,
p
w
)
(p_{h},p_{w})
(ph,pw)。当
p
h
=
p
w
=
p
p_{h}=p_{w}=p
ph=pw=p时,填充是
p
p
p。
同理,当高度和宽度上的步幅分别为
s
h
s_{h}
sh和
s
w
s_{w}
sw时,我们称之为步幅
(
s
h
,
s
w
)
(s_{h},s_{w})
(sh,sw)。特别地,当
s
h
=
s
w
=
s
s_{h}=s_{w}=s
sh=sw=s时,我们称步幅为
s
s
s。
默认情况下,填充为
0
0
0,步幅为
1
1
1。
在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有
p
h
=
p
w
p_{h}=p_{w}
ph=pw和
s
h
=
s
w
s_{h}=s_{w}
sh=sw。
小结
-
填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
-
步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1 / n 1/n 1/n
( n n n是一个大于 1 1 1的整数)。 -
填充和步幅可用于有效地调整数据的维度。
可以通过统筹选择设置填充和步幅,来将矩形的输入张量,转换为指定高宽比的输出张量。
小trick
- 一般来说,填充的目的是让输入张量和输出张量的形状一致。
- 通常来讲,步幅等于 1 1 1是最好的,如果不选步幅为 1 1 1,一般是我们觉得计算量太大了,需要很多很多层才会使得我这个图片变得越来越小,我不想真的用很多层去做这个事情。通常来说步幅是取 2 2 2,每一次减半。步幅取多少,一般看我想把模型复杂度控制在什么程度。
- 步幅、填充、核大小和通道数,都是一个神经网络架构的一部分,是我整个网络怎么设计的一部分。
- 在实际情况中,我们一定是参照经典的网络结构做设计。