Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_convolutional-neural-networks/padding-and-strides.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_convolutional-neural-networks/padding-and-strides.ipynb .. _sec_padding: 填充和跨步 ========== 在上一个示例中,我们的输入的高度和宽度均为 :math:`3` 我们的卷积内核的高度和宽度都是 :math:`2`, 生成维度为 :math:`2\times2` 的输出表示。 通常,假设输入形状为 :math:`n_h\times n_w` 卷积核窗口的形状是 :math:`k_h\times k_w`\ , 然后输出形状将是 .. math:: (n_h-k_h+1) \times (n_w-k_w+1). 因此,卷积层的输出形状 由输入的形状决定 以及卷积核窗口的形状。 在一些情况下,我们结合了技术, 包括填充和跨步卷积, 这会影响输出的大小。 作为动机,请注意,由于内核 宽度和高度大于\ :math:`1`\ , 在应用多次连续卷积之后, 我们最终往往会得到 远小于我们的输入。 如果我们从\ :math:`240 \times 240`\ 的像素图像开始, :math:`10`\ 层的\ :math:`5 \times 5`\ 卷积 将图像减少到\ :math:`200 \times 200`\ 像素, 从图像中切下 :math:`30 \%`\ 并使用它 删除任何有趣的信息 在原始图像的边界上。 *Padding*\ 是处理此问题最常用的工具。 在其他情况下,我们可能希望大幅降低维度, 例如,如果我们发现原始输入分辨率很难处理。 *跨步卷积*\ 是一种流行的技术,可以在这些情况下有所帮助。 填充 ---- 如上所述,应用卷积层时有一个棘手的问题 我们往往会丢失图像周边的像素。 因为我们通常使用小内核, 对于任何给定的卷积, 我们可能只会损失几个像素, 但是,当我们申请时,这可以加起来 许多连续的卷积层。 这个问题的一个简单解决方案 就是在输入图像的边界周围添加额外的填充像素, 从而增加了图像的有效大小。 通常,我们将额外像素的值设置为 :math:`0`\ 。 在 :numref:`img_conv_pad`\ 中,我们输入 :math:`3 \times 3`\ , 将其大小增加到 :math:`5 \times 5`\ 。 相应的输出随后增加到 :math:`4 \times 4` 的矩阵。 .. _img_conv_pad: .. figure:: https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/conv-pad.svg Two-dimensional cross-correlation with padding. The shaded portions are the input and kernel array elements used by the first output element: :math:`0\times0+0\times1+0\times2+0\times3=0`. 一般来说,如果我们总共添加 :math:`p_h` 行填充 (大约一半在顶部,一半在底部) 以及总共 :math:`p_w` 列的填充 (大约一半在左边,一半在右边), 输出形状将为 .. math:: (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1). 这意味着输出的高度和宽度 将分别增加 :math:`p_h` 和 :math:`p_w`\ 。 在许多情况下,我们会希望设置 :math:`p_h=k_h-1` 和 :math:`p_w=k_w-1` 使输入和输出具有相同的高度和宽度。 这将更容易预测每层的输出形状 在构建网络时。 假设 :math:`k_h` 在这里, 我们将在高度的两侧垫 :math:`p_h/2` 行。 如果 :math:`k_h` 是奇数,一种可能性是 将 :math:`\lceil p_h/2\rceil` 行放在输入的顶部 和底部的 :math:`\lfloor p_h/2\rfloor` 行。 我们将以相同的方式填充宽度的两侧。 卷积神经网络通常使用卷积核 具有奇数的高度和宽度值,例如\ :math:`1`\ , :math:`3`\ , :math:`5`\ , 或 :math:`7`\ 。 选择奇数内核大小有好处 我们可以保持空间维度 在顶部和底部填充相同数量的行, 左右两侧的列数相同。 此外,这种使用奇数核的做法 和填充以精确地保留维度 提供文书福利。 对于任何二维数组 ``X``\ , 当果仁大小为奇数时 以及填充行和列的数量 各方面都是一样的, 产生与输入具有相同高度和宽度的输出, 我们知道输出 ``Y[i, j]`` 是经过计算的 通过输入和卷积核的互相关 窗口以 ``X[i, j]``\ 为中心。 在下面的示例中,我们创建一个二维卷积层 高度和宽度为 :math:`3` 并在所有侧面应用 :math:`1` 像素的填充物。 给定一个高度和宽度为 :math:`8` 的输入, 我们发现输出的高度和宽度也是 :math:`8`\ 。 .. code:: java %load ../utils/djl-imports %load ../utils/plot-utils %load ../utils/DataPoints.java %load ../utils/Training.java .. code:: java NDManager manager = NDManager.newBaseManager(); NDArray X = manager.randomUniform(0f, 1.0f, new Shape(1, 1, 8, 8)); .. code:: java // 请注意,这里每侧填充1行或1列,因此总共2行或1列 // 添加行或列 Block block = Conv2d.builder() .setKernelShape(new Shape(3, 3)) .optPadding(new Shape(1, 1)) .setFilters(1) .build(); TrainingConfig config = new DefaultTrainingConfig(Loss.l2Loss()); Model model = Model.newInstance("conv2D"); model.setBlock(block); Trainer trainer = model.newTrainer(config); trainer.initialize(X.getShape()); NDArray yHat = trainer.forward(new NDList(X)).singletonOrThrow(); // 排除我们不感兴趣的前两个维度:批次和 // 频道 System.out.println(yHat.getShape().slice(2)); .. parsed-literal:: :class: output (8, 8) When the height and width of the convolution kernel are different, we can make the output and input have the same height and width by setting different padding numbers for height and width. .. code:: java // 这里,我们使用一个高度为5、宽度为3的卷积核。这个 // 高度和宽度两侧的填充号分别为2和1, // 分别 block = Conv2d.builder() .setKernelShape(new Shape(5, 3)) .optPadding(new Shape(2, 1)) .setFilters(1) .build(); model.setBlock(block); trainer = model.newTrainer(config); trainer.initialize(X.getShape()); yHat = trainer.forward(new NDList(X)).singletonOrThrow(); System.out.println(yHat.getShape().slice(2)); .. parsed-literal:: :class: output (8, 8) 跨步 ---- 在计算互相关时, 我们从卷积窗口开始 在输入数组的左上角, 然后将其滑动到所有位置,包括向下和向右。 在前面的示例中,我们默认一次滑动一个像素。 然而,有时,无论是为了计算效率 或者因为我们想减少样本, 我们一次移动窗口超过一个像素, 跳过中间位置。 我们将每张幻灯片的行数和列数称为\ *跨步*\ 。 到目前为止,我们已经使用了1美元的跨步,包括高度和宽度。 有时,我们可能需要更大的步幅。 :numref:`img_conv_stride` 显示二维互相关运算 垂直跨步 :math:`3` ,水平跨步 :math:`2` 。 我们可以看到,当第一列的第二个元素被输出时, 卷积窗口向下滑动三行。 卷积窗口向右滑动两列 当第一行的第二个元素被输出时。 当卷积窗口在输入上向右滑动三列时, 没有输出,因为输入元素无法填充窗口 (除非我们添加另一列填充)。 .. _img_conv_stride: .. figure:: https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/conv-stride.svg Cross-correlation with strides of 3 and 2 for height and width respectively. The shaded portions are the output element and the input and core array elements used in its computation: :math:`0\times0+0\times1+1\times2+2\times3=8`, :math:`0\times0+6\times1+0\times2+0\times3=6`. 一般来说,当身高的步幅为\ :math:`s_h` 宽度的步幅为\ :math:`s_w`\ ,输出形状为 .. math:: \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. 如果我们设置 :math:`p_h=k_h-1` 和 :math:`p_w=k_w-1`\ , 然后将输出形状简化为 :math:`\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor`. 如果输入的高度和宽度 可以被高度和宽度上的跨步所分割, 然后输出形状将是 :math:`(n_h/s_h) \times (n_w/s_w)`. 下面,我们将高度和宽度的跨步设置为 :math:`2`\ , 从而将输入高度和宽度减半。 .. code:: java block = Conv2d.builder() .setKernelShape(new Shape(3, 3)) .optPadding(new Shape(1, 1)) .optStride(new Shape(2,2)) .setFilters(1) .build(); model.setBlock(block); trainer = model.newTrainer(config); trainer.initialize(X.getShape()); yHat = trainer.forward(new NDList(X)).singletonOrThrow(); System.out.println(yHat.getShape().slice(2)); .. parsed-literal:: :class: output (4, 4) 接下来,我们将看一个稍微复杂一点的例子。 .. code:: java block = Conv2d.builder() .setKernelShape(new Shape(3, 5)) .optPadding(new Shape(0, 1)) .optStride(new Shape(3,4)) .setFilters(1) .build(); model.setBlock(block); trainer = model.newTrainer(config); trainer.initialize(X.getShape()); yHat = trainer.forward(new NDList(X)).singletonOrThrow(); System.out.println(yHat.getShape().slice(2)); .. parsed-literal:: :class: output (2, 2) 为了简洁起见,当填充数字 在输入的两侧,高度和宽度分别是 :math:`p_h` 和 :math:`p_w`\ ,我们称之为padding :math:`(p_h, p_w)`\ 。 具体来说,当 :math:`p_h = p_w = p` 时,填充是 :math:`p`\ 。 当高度和宽度上的跨步分别为 :math:`s_h` 和 :math:`s_w` 时, 我们称步幅为 :math:`(s_h, s_w)`\ 。 具体来说,当 :math:`s_h = s_w = s` 时,步幅为 :math:`s`\ 。 默认情况下,填充为 :math:`0` ,步幅为 :math:`1`\ 。 实际上,我们很少使用不均匀的跨步或填充, 例如,我们通常有 :math:`p_h = p_w` 和 :math:`s_h = s_w`\ 。 总结 ---- - 填充可以增加输出的高度和宽度。这通常用于使输出与输入具有相同的高度和宽度。 - 步幅可以降低输出的分辨率,例如,将输出的高度和宽度降低到输入高度和宽度的 :math:`1/n`\ (\ :math:`n` 是大于 :math:`1` 的整数)。 - 填充和跨步可以有效地调整数据的维度。 练习 ---- 1. 对于本节的最后一个示例,使用形状计算公式计算输出形状,以查看其是否与实验结果一致。 2. 在本节的实验中尝试其他填充和跨步组合。 3. 对于音频信号,\ :math:`2` 的步幅对应什么? 4. 大于 :math:`1` 的步幅有什么计算优势。