深度学习——卷积神经网络(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) (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像素。如此一来,原始图像的边界丢失了许多有用信息。而填充是解决此问题最有效的方法;
有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅则可以在这类情况下提供帮助。
如上所述,在应用多层卷积时,我们常常丢失边缘像素。解决这个问题的简单方法即为填充:在输入图像的边界填充元素(通常填充元素是 0 0 0)。
我们将 3 × 3 3 \times 3 3×3输入填充到 5 × 5 5 \times 5 5×5,那么它的输出就增加为 4 × 4 4 \times 4 4×4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
通常,如果我们添加 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,使输入和输出具有相同的高度和宽度。
这样可以在构建网络时更容易地预测每个图层的输出形状。
卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。
选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X
,当满足:
Y[i, j]
是通过以输入X[i, j]
为中心,与卷积核进行互相关计算得到的。比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并(在所有侧边填充1个像素)。给定高度和宽度为8的输入,则输出的高度和宽度也是8。
#定义一个计算卷积的函数(初始化卷积层权重,并对输入和输出扩大和缩减相应的维数)
def comp_conv2d(conv2d,x):
x = x.reshape((1,1)+x.shape)# 这里的(1,1)表示批量样本大小和通道数都为1
y = conv2d(x)
return y.reshape(y.shape[2:]) #省去前两个维度
#这里每侧边都填充了1行1列,因此共填充了2行2列
conv2d = nn.Conv2d(1,1,kernel_size=3,padding=1)
x = torch.rand(size=(8,8))
print(comp_conv2d(conv2d,x).shape)
#当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度
conv2d = nn.Conv2d(1,1,kernel_size=(5,3),padding=(2,1))
print(comp_conv2d(conv2d,x).shape)
#结果:
torch.Size([8, 8])
torch.Size([8, 8])
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 之前我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素, 而每次滑动元素的数量称为步幅(stride)
如图是垂直步幅为 3 3 3,水平步幅为 2 2 2的二维互相关运算:
通常,当垂直步幅为 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. ⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋.
如果我们设置了 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,则输出形状将简化为 ⌊ ( 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+sh−1)/sh⌋×⌊(nw+sw−1)/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)
print(comp_conv2d(conv2d,x).shape)
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
print(comp_conv2d(conv2d, x).shape)
#结果:
torch.Size([4, 4])
torch.Size([2, 2])
默认情况下,填充为0,步幅为1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有ph=pw和sh=sw。
当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有3×h×w的形状。我们将这个大小为3的轴称为通道(channel)维度。
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算
假设输入的通道数为 c i c_i ci,那么卷积核的输入通道数也需要为 c i c_i ci。如果卷积核的窗口形状是 k h × k w k_h\times k_w kh×kw,那么当 c i = 1 c_i=1 ci=1时,我们可以把卷积核看作形状为 k h × k w k_h\times k_w kh×kw的二维张量。
当 c i > 1 c_i>1 ci>1时,我们卷积核的每个输入通道将包含形状为 k h × k w k_h\times k_w kh×kw的张量。将这些张量 c i c_i ci连结在一起可以得到形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的卷积核。由于输入和卷积核都有 c i c_i ci个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 c i c_i ci的结果相加)得到二维张量。
对图中所示用代码实现一下:
def corr2d_multi_in(x,k):
return sum(d2l.corr2d(x,k) for x,k in zip(x,k))
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
print(corr2d_multi_in(X, K))
#结果:
tensor([[ 56., 72.],
[104., 120.]])
每一层有多个输出通道是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。
我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
K = torch.stack((K, K + 1, K + 2), 0)
print(K.shape) #卷积核的数量是由K的第一个维度决定的,故这里是有三个卷积核(卷积核的输出通道数与卷积核的数量对应)
print(corr2d_multi_in_out(X,K))
#结果:
torch.Size([3, 2, 2, 2])
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
现在的输出包含3个通道,第一个通道的结果与先前输入张量X和多输入单输出通道的结果一致。
1×1卷积,即kh=kw=1,看起来似乎没有多大意义。 毕竟,卷积的本质是有效提取相邻像素间的相关特征,而1×1卷积显然没有此作用。 尽管如此,1×1仍然十分流行,经常包含在复杂深层网络的设计中。
因为使用了最小窗口, 1 × 1 1\times 1 1×1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。
其实 1 × 1 1\times 1 1×1卷积的唯一计算发生在通道上。
下图展示了使用 1 × 1 1\times 1 1×1卷积核与 3 3 3个输入通道和 2 2 2个输出通道的互相关计算。
这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。
我们可以将 1 × 1 1\times 1 1×1卷积层看作在每个像素位置应用的全连接层,以 c i c_i ci个输入值转换为 c o c_o co个输出值。
因为这仍然是一个卷积层,所以跨像素的权重是一致的。
同时, 1 × 1 1\times 1 1×1卷积层需要的权重维度为 c o × c i c_o\times c_i co×ci,再额外加上一个偏置。
下面,我们使用全连接层实现 1 × 1 1 \times 1 1×1卷积。请注意,我们需要对输入和输出的数据形状进行调整。
# 1x1卷积层
def corr2d_multi_in_out_1x1(X,K):
c_i,h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i,h*w)) #将每个通道的空间维度展平为一个列向量
K = K.reshape((c_o,c_i))
#全连接层中的矩阵乘法
y = torch.matmul(K,X)
return y.reshape(c_o,h,w) #恢复输出矩阵的通道数和空间维度
#当执行1×1卷积运算时,上述函数相当于先前实现的互相关函数corr2d_multi_in_out。
X= torch.normal(0,1,(3,3,3))
K = torch.normal(0,1,(2,3,1,1))
y1 = corr2d_multi_in_out_1x1(X,K)
y2 = corr2d_multi_in_out(X,K)
print(y1);print(y2)
assert float(torch.abs(y1-y2).sum()) < 1e-6
#结果:
tensor([[[ 0.5941, -0.5362, -1.0132],
[ 1.8299, -1.2045, 1.1347],
[-0.4486, -2.2969, -0.8045]],
[[ 0.3476, 1.7180, 1.9936],
[ 1.3062, 0.8710, 2.2610],
[ 1.6681, -1.8917, -0.0728]]])
tensor([[[ 0.5941, -0.5362, -1.0132],
[ 1.8299, -1.2045, 1.1347],
[-0.4486, -2.2969, -0.8045]],
[[ 0.3476, 1.7180, 1.9936],
[ 1.3062, 0.8710, 2.2610],
[ 1.6681, -1.8917, -0.0728]]])
本章学习了填充和步幅这两个调整数据维度的应用原理,再就是学习了多输入与多输出通道的扩展卷积层模型的概念,以及1×1卷积层在调整通道数量上的运用。
下士闻道,大笑之──不笑,不足以为道。
–2023-10-12 进阶篇