回忆一下图 7.2.1中的卷积示例。输入的高度和宽度均为 3,卷积核的高度和宽度均为 2,从而产生具有维度的输出表示2×2. 假设输入形状是 nh×nw卷积核形状为 kh×kw,输出形状将是 (nh−kh+1)×(nw−kw+1):我们只能将卷积核移动到它用完像素以应用卷积为止。
在下文中,我们将探索许多技术,包括填充和跨步卷积,它们可以更好地控制输出的大小。作为动机,请注意,由于内核的宽度和高度通常大于1,在应用许多连续的卷积之后,我们往往会得到比输入小得多的输出。如果我们从一个240×240像素图像,10层层5×5卷积将图像缩小为200×200像素,切片30%的图像,并用它抹掉原始图像边界上的任何有趣信息。填充是处理此问题的最流行的工具。在其他情况下,我们可能希望大幅降低维数,例如,如果我们发现原始输入分辨率很笨重。跨步卷积是一种流行的技术,可以在这些情况下提供帮助。
import torch from torch import nn
from mxnet import np, npx from mxnet.gluon import nn npx.set_np()
import jax from flax import linen as nn from jax import numpy as jnp from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
import tensorflow as tf
7.3.1. 填充
如上所述,应用卷积层时的一个棘手问题是我们往往会丢失图像周边的像素。考虑 图 7.3.1,该图将像素利用率描述为卷积核大小和图像内位置的函数。角落里的像素几乎没有被使用。
图 7.3.1尺寸卷积的像素利用1×1, 2×2, 和3×3分别。
由于我们通常使用小内核,对于任何给定的卷积,我们可能只会丢失几个像素,但是当我们应用许多连续的卷积层时,这可能会累加起来。这个问题的一个直接解决方案是在输入图像的边界周围添加额外的填充像素,从而增加图像的有效尺寸。通常,我们将额外像素的值设置为零。在 图 7.3.2中,我们填充一个3×3输入,将其大小增加到5×5. 相应的输出然后增加到4×4矩阵。阴影部分是第一个输出元素以及用于输出计算的输入和内核张量元素:0×0+0×1+0×2+0×3=0.
图 7.3.2带填充的二维互相关。
一般来说,如果我们总共添加ph填充行(大约一半在顶部,一半在底部)和总共pw填充列(大约一半在左边,一半在右边),输出形状将是
(7.3.1)(nh−kh+ph+1)×(nw−kw+pw+1).
这意味着输出的高度和宽度将增加 ph和pw, 分别。
在许多情况下,我们会想要设置ph=kh−1和 pw=kw−1给输入和输出相同的高度和宽度。这样在构建网络时更容易预测每一层的输出形状。假如说kh这里很奇怪,我们会垫ph/2高度两侧的行。如果 kh是偶数,一种可能是填充 ⌈ph/2⌉输入顶部的行和 ⌊ph/2⌋底部的行。我们将以相同的方式填充宽度的两侧。
CNN 通常使用具有奇数高度和宽度值的卷积核,例如 1、3、5 或 7。选择奇数核大小的好处是我们可以保留维度,同时在顶部和底部填充相同数量的行,并且左右的列数相同。
此外,这种使用奇数内核和填充来精确保持维度的做法提供了文书上的好处。对于任意一个二维张量X,当核的大小为奇数,且各边的padding行数和列数相同时,产生与输入等高等宽的输出,我们知道输出是通过cross计算的-输入和卷积核与以 为中心的窗口的相关性。Y[i, j]X[i, j]
在下面的示例中,我们创建了一个二维卷积层,其高度和宽度均为 3,并在所有边上应用 1 个像素的填充。给定一个高度和宽度为 8 的输入,我们发现输出的高度和宽度也为 8。
# We define a helper function to calculate convolutions. It initializes the # convolutional layer weights and performs corresponding dimensionality # elevations and reductions on the input and output def comp_conv2d(conv2d, X): # (1, 1) indicates that batch size and the number of channels are both 1 X = X.reshape((1, 1) + X.shape) Y = conv2d(X) # Strip the first two dimensions: examples and channels return Y.reshape(Y.shape[2:]) # 1 row and column is padded on either side, so a total of 2 rows or columns # are added conv2d = nn.LazyConv2d(1, kernel_size=3, padding=1) X = torch.rand(size=(8, 8)) comp_conv2d(conv2d, X).shape
torch.Size([8, 8])
# We define a helper function to calculate convolutions. It initializes # the convolutional layer weights and performs corresponding dimensionality # elevations and reductions on the input and output def comp_conv2d(conv2d, X): conv2d.initialize() # (1, 1) indicates that batch size and the number of channels are both 1 X = X.reshape((1, 1) + X.shape) Y = conv2d(X) # Strip the first two dimensions: examples and channels return Y.reshape(Y.shape[2:]) # 1 row and column is padded on either side, so a total of 2 rows or columns are added conv2d = nn.Conv2D(1, kernel_size=3, padding=1) X = np.random.uniform(size=(8, 8)) comp_conv2d(conv2d, X).shape
(8, 8)
# We define a helper function to calculate convolutions. It initializes # the convolutional layer weights and performs corresponding dimensionality # elevations and reductions on the input and output def comp_conv2d(conv2d, X): # (1, X.shape, 1) indicates that batch size and the number of channels are both 1 key = jax.random.PRNGKey(d2l.get_seed()) X = X.reshape((1,) + X.shape + (1,)) Y, _ = conv2d.init_with_output(key, X) # Strip the dimensions: examples and channels return Y.reshape(Y.shape[1:3]) # 1 row and column is padded on either side, so a total of 2 rows or columns are added conv2d = nn.Conv(1, kernel_size=(3, 3), padding='SAME') X = jax.random.uniform(jax.random.PRNGKey(d2l.get_seed()), shape=(8, 8)) comp_conv2d(conv2d, X).shape
(8, 8)
# We define a helper function to calculate convolutions. It initializes # the convolutional layer weights and performs corresponding dimensionality # elevations and reductions on the input and output def comp_conv2d(conv2d, X): # (1, 1) indicates that batch size and the number of channels are both 1 X = tf.reshape(X, (1, ) + X.shape + (1, )) Y = conv2d(X) # Strip the first two dimensions: examples and channels return tf.reshape(Y, Y.shape[1:3]) # 1 row and column is padded on either side, so a total of 2 rows or columns # are added conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same') X = tf.random.uniform(shape=(8, 8)) comp_conv2d(conv2d, X).shape
TensorShape([8, 8])
当卷积核的高和宽不同时,我们可以通过为高和宽设置不同的填充数,使输出和输入具有相同的高和宽。
# We use a convolution kernel with height 5 and width 3. The padding on either # side of the height and width are 2 and 1, respectively conv2d = nn.LazyConv2d(1, kernel_size=(5, 3), padding=(2, 1)) comp_conv2d(conv2d, X).shape
torch.Size([8, 8])
# We use a convolution kernel with height 5 and width 3. The padding on # either side of the height and width are 2 and 1, respectively conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1)) comp_conv2d(conv2d, X).shape
(8, 8)
# We use a convolution kernel with height 5 and width 3. The padding on # either side of the height and width are 2 and 1, respectively conv2d = nn.Conv(1, kernel_size=(5, 3), padding=(2, 1)) comp_conv2d(conv2d, X).shape
(8, 8)
# We use a convolution kernel with height 5 and width 3. The padding on # either side of the height and width are 2 and 1, respectively conv2d = tf.keras.layers.Conv2D(1, kernel_size=(5, 3), padding='same') comp_conv2d(conv2d, X).shape
TensorShape([8, 8])
7.3.2. 步幅
在计算互相关时,我们从输入张量左上角的卷积窗口开始,然后将其滑过所有位置,包括向下和向右。在前面的示例中,我们默认一次滑动一个元素。然而,有时,无论是为了提高计算效率还是因为我们希望下采样,我们一次将窗口移动一个以上的元素,跳过中间位置。如果卷积核很大,这是特别有用的,因为它捕获了大面积的底层图像。
我们将每张幻灯片遍历的行数和列数称为 步幅。到目前为止,我们对高度和宽度都使用了 1 的步幅。有时,我们可能想使用更大的步幅。 图 7.3.3显示了垂直步长为 3,水平步长为 2 的二维互相关运算。阴影部分是输出元素以及用于输出计算的输入和内核张量元素: 0×0+0×1+1×2+2×3=8, 0×0+6×1+0×2+0×3=6. 我们可以看到,当第一列的第二个元素生成时,卷积窗口向下滑动了三行。当生成第一行的第二个元素时,卷积窗口向右滑动两列。当卷积窗口在输入上继续向右滑动两列时,就没有输出了,因为输入元素无法填满窗口(除非我们再添加一列padding)。
图 7.3.3高度和宽度的步长分别为 3 和 2 的互相关。
一般来说,当高度的步幅为sh宽度的步幅是sw,输出形状为
(7.3.2)⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋.
如果我们设置ph=kh−1和pw=kw−1, 那么输出形状可以简化为 ⌊(nh+sh−1)/sh⌋×⌊(nw+sw−1)/sw⌋. 更进一步,如果输入的高度和宽度可以被高度和宽度的步幅整除,那么输出形状将是 (nh/sh)×(nw/sw).
下面,我们将高度和宽度的步幅都设置为 2,从而将输入的高度和宽度减半。
conv2d = nn.LazyConv2d(1, kernel_size=3, padding=1, stride=2) comp_conv2d(conv2d, X).shape
torch.Size([4, 4])
conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2) comp_conv2d(conv2d, X).shape
(4, 4)
conv2d = nn.Conv(1, kernel_size=(3, 3), padding=1, strides=2) comp_conv2d(conv2d, X).shape
(4, 4)
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2) comp_conv2d(conv2d, X).shape
TensorShape([4, 4])
让我们看一个稍微复杂一点的例子。
conv2d = nn.LazyConv2d(1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4)) comp_conv2d(conv2d, X).shape
torch.Size([2, 2])
conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4)) comp_conv2d(conv2d, X).shape
(2, 2)
conv2d = nn.Conv(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4)) comp_conv2d(conv2d, X).shape
(2, 2)
conv2d = tf.keras.layers.Conv2D(1, kernel_size=(3,5), padding='valid', strides=(3, 4)) comp_conv2d(conv2d, X).shape
TensorShape([2, 1])
7.3.3. 总结与讨论
填充可以增加输出的高度和宽度。这通常用于为输出提供与输入相同的高度和宽度,以避免不希望的输出收缩。此外,它确保所有像素的使用频率相同。通常我们在输入高度和宽度的两侧选择对称填充。在这种情况下,我们指的是 (ph,pw)填充。最常见的是我们设置ph=pw,在这种情况下,我们只是声明我们选择填充p.
类似的约定适用于步幅。横步时 sh和垂直步幅swmatch,我们简单说说strides. 步幅可以降低输出的分辨率,例如将输出的高度和宽度降低到仅 1/n输入的高度和宽度n>1. 默认情况下,填充为 0,步幅为 1。
到目前为止,我们讨论的所有填充都只是用零扩展图像。这具有显着的计算优势,因为它很容易实现。此外,可以将运算符设计为隐式利用此填充,而无需分配额外的内存。同时,它允许 CNN 对图像中的隐式位置信息进行编码,只需了解“空白”的位置即可。零填充有很多替代方法。 Alsallakh等人。( 2020 )提供了替代方案的广泛概述(尽管没有明确的案例使用非零填充,除非出现伪影)。
7.3.4. 练习
给定本节中最后一个具有内核大小的代码示例 (3,5), 填充(0,1), 和大步(3,4), 计算输出形状以检查它是否与实验结果一致。
实施镜像填充,即边界值被简单地镜像以扩展张量的填充。
步幅大于 1 的计算优势是什么?
大于 1 的步幅在统计上有什么好处?
你将如何实现一大步12?它对应什么?这什么时候有用?
-
pytorch
+关注
关注
2文章
802浏览量
13105
发布评论请先 登录
相关推荐
评论