Run this notebook online:Binder or Colab: Colab

6.5. 池化

通常,当我们处理图像时,我们希望逐渐 降低隐藏表示的空间分辨率, 聚合信息以便 我们在网络中的地位越高, 感受野越大(在输入端) 每个隐藏节点都对其敏感。

通常,我们的最终任务会问一些关于形象的全球性问题, e.g.,里面有猫吗 所以最后一层的节点通常是敏感的 到整个输入。 通过逐渐聚合信息,生成越来越粗糙的地图, 我们最终实现了学习全球代表性的目标, 同时在处理的中间层保留卷积层的所有优点。

此外,在检测较低级别的特征时,例如边缘 (如 Section 6.2)中所述, 我们经常希望我们的表达在某种程度上对翻译保持不变。 例如,如果我们拍摄图像 X 黑与白之间有着清晰的界限 将整个图像向右移动一个像素, 例如,Z[i, j] = X[i, j+1] , 那么新图像 Z 的输出可能会有很大的不同。 边缘将移动一个像素,并随之进行所有激活。 实际上,物体几乎不会完全出现在同一个地方。 事实上,即使有三脚架和固定物体, 快门移动导致相机振动 可能会让所有东西都偏移一个像素左右 (高端摄像头配备了特殊功能来解决这个问题)。

本节介绍池层, 有两个目的 降低卷积层对位置的敏感性 以及空间下采样表示。

6.5.1. 最大池和平均池

像卷积层,池操作符 由一个固定形状的滑动窗口组成 根据步幅输入的所有区域, 为每个经过的位置计算单个输出 通过固定形状窗口(有时称为池窗口)。 然而,与互相关计算不同 卷积层中的输入和内核, 池层不包含任何参数(没有过滤器)。 相反,池运算符是确定性的, 通常计算最大值或平均值 池窗口中的元素的。 这些操作称为最大池max pooling简称) 和平均池

在这两种情况下,与互相关算子一样, 我们可以考虑池窗口 从输入数组的左上角开始 从左到右,从上到下滑动输入数组。 在池窗口命中的每个位置, 它计算最大值或平均值 窗口中输入子数组的值 (取决于使用的是最大还是平均池)。

https://raw.githubusercontent.com/d2l-ai/d2l-en/master/img/pooling.svg

Fig. 6.5.1 Maximum pooling with a pooling window shape of \(2\times 2\). The shaded portions represent the first output element and the input element used for its computation: \(\max(0, 1, 3, 4)=4\)

上面的:numref:fig_pooling中的输出数组的高度为2,宽度为2。 这四个元素来自 \(\text{max}\)的最大值:

(6.5.1)\[\begin{split}\max(0, 1, 3, 4)=4,\\ \max(1, 2, 4, 5)=5,\\ \max(3, 4, 6, 7)=7,\\ \max(4, 5, 7, 8)=8.\\\end{split}\]

池窗口形状为 \(p \times q\) 的池层 被称为\(p \times q\) 池层。 池操作称为 \(p \times q\) 池。

让我们回到物体边缘检测的例子 在本节开头提到。 现在我们将使用卷积层的输出 作为 \(2\times 2\) 最大池的输入。 将卷积层输入设置为 X ,池层输出设置为 Y。无论 X[i, j]X[i, j+1] 的值是否不同, 或者 X[i, j+1]X[i, j+2] 是不同的, 池层输出都包括 Y[i, j]=1。 也就是说,使用 \(2\times 2\) 最大池层, 我们仍然可以检测出卷积层是否识别出模式 在高度和宽度上最多移动一个元素。

在下面的代码中,我们实现了正向计算 pool2d 函数中池层的。 此函数类似于 corr2d 函数 in:numref:sec_conv_layer。 然而,在这里,我们没有计算输出的内核 作为输入中每个区域的最大值或平均值。

%load ../utils/djl-imports
NDManager manager = NDManager.newBaseManager();

public NDArray pool2d(NDArray X, Shape poolShape, String mode){

    long poolHeight = poolShape.get(0);
    long poolWidth = poolShape.get(1);

    NDArray Y = manager.zeros(new Shape(X.getShape().get(0) - poolHeight + 1,
                                        X.getShape().get(1) - poolWidth + 1));
    for(int i=0; i < Y.getShape().get(0); i++){
        for(int j=0; j < Y.getShape().get(1); j++){

            if("max".equals(mode)){
                Y.set(new NDIndex(i+","+j),
                            X.get(new NDIndex(i + ":" + (i + poolHeight) + ", " + j + ":" + (j + poolWidth))).max());
            }
            else if("avg".equals(mode)){
                Y.set(new NDIndex(i+","+j),
                            X.get(new NDIndex(i + ":" + (i + poolHeight) + ", " + j + ":" + (j + poolWidth))).mean());
            }

        }
    }

    return Y;
}

我们可以在上图中构造输入数组 X ,以验证二维最大池层的输出。

NDArray X = manager.arange(9f).reshape(3,3);
pool2d(X, new Shape(2,2), "max");
ND: (2, 2) gpu(0) float32
[[4., 5.],
 [7., 8.],
]

同时,我们对平均池层进行了实验。

pool2d(X, new Shape(2,2), "avg");
ND: (2, 2) gpu(0) float32
[[2., 3.],
 [5., 6.],
]

6.5.2. 填充和跨步

与卷积层一样,池层 还可以更改输出形状。 和以前一样,我们可以改变操作以获得所需的输出形状 通过填充输入和调整步幅。 我们可以演示填充和跨步的使用 通过二维最大池层 maxPool2dBlock在池层中 在DJL的 Pool 模块中发货。 我们首先构造一个形状为 (1, 1, 4, 4)的输入数据, 其中前两个维度是批次和通道。

X = manager.arange(16f).reshape(1, 1, 4, 4);
X
ND: (1, 1, 4, 4) gpu(0) float32
[[[[ 0.,  1.,  2.,  3.],
   [ 4.,  5.,  6.,  7.],
   [ 8.,  9., 10., 11.],
   [12., 13., 14., 15.],
  ],
 ],
]

下面,我们使用一个形状为 (3, 3) 的池窗口, 步幅形状为 (3, 3)

// 定义块指定内核和步幅
Block block = Pool.maxPool2dBlock(new Shape(3, 3), new Shape(3, 3));
block.initialize(manager, DataType.FLOAT32, new Shape(1,1,4,4));

ParameterStore parameterStore = new ParameterStore(manager, false);
// 因为池层中没有模型参数,所以我们不需要
// 调用参数初始化函数
block.forward(parameterStore, new NDList(X), true).singletonOrThrow();
ND: (1, 1, 1, 1) gpu(0) float32
[[[[10.],
  ],
 ],
]

步幅和填充可以手动指定。

// 重新定义内核形状、跨步形状和垫块形状
block = Pool.maxPool2dBlock(new Shape(3,3), new Shape(2,2), new Shape(1,1));
// block forward 方法
block.forward(parameterStore, new NDList(X), true).singletonOrThrow();
ND: (1, 1, 2, 2) gpu(0) float32
[[[[ 5.,  7.],
   [13., 15.],
  ],
 ],
]

当然,我们可以指定一个任意的矩形池窗口 并分别为高度和宽度指定填充和跨步。

// 重新定义内核形状、跨步形状和垫块形状
block = Pool.maxPool2dBlock(new Shape(2,3), new Shape(2,3), new Shape(1,2));
block.forward(parameterStore, new NDList(X), true).singletonOrThrow();
ND: (1, 1, 3, 2) gpu(0) float32
[[[[ 0.,  3.],
   [ 8., 11.],
   [12., 15.],
  ],
 ],
]

6.5.3. 多通道

在处理多通道输入数据时, 池化层分别池化每个输入通道, 而不是一个通道一个通道地添加每个通道的输入 就像在卷积层中一样。 这意味着池层的输出通道数 与输入通道的数量相同。 下面,我们将把数组 XX+1连接起来 在通道维度上,使用2个通道构造输入。

X = X.concat(X.add(1), 1);
X
ND: (1, 2, 4, 4) gpu(0) float32
[[[[ 0.,  1.,  2.,  3.],
   [ 4.,  5.,  6.,  7.],
   [ 8.,  9., 10., 11.],
   [12., 13., 14., 15.],
  ],
  [[ 1.,  2.,  3.,  4.],
   [ 5.,  6.,  7.,  8.],
   [ 9., 10., 11., 12.],
   [13., 14., 15., 16.],
  ],
 ],
]

正如我们所看到的,在合并后,输出通道的数量仍然是2。

block = Pool.maxPool2dBlock(new Shape(3,3), new Shape(2,2), new Shape(1,1));
block.forward(parameterStore, new NDList(X), true).singletonOrThrow();
ND: (1, 2, 2, 2) gpu(0) float32
[[[[ 5.,  7.],
   [13., 15.],
  ],
  [[ 6.,  8.],
   [14., 16.],
  ],
 ],
]

6.5.4. 总结

  • 对于池窗口中的输入元素,最大池操作将最大值指定为输出,平均池操作将平均值指定为输出。

  • 池层的主要功能之一是减轻卷积层对位置的过度敏感性。

  • 我们可以为池层指定填充和跨步。

  • 最大池,再加上大于1的步幅,可以用来降低分辨率。

  • 池层的输出通道数与输入通道数相同。

6.5.5. 练习

  1. 你能把平均池作为卷积层的一个特例来实现吗?如果是这样,那就去做。

  2. 你能把最大池作为卷积层的特例来实现吗?如果是这样,那就去做。

  3. 池层的计算成本是多少?假设池层的输入大小为 \(c\times h\times w\),池窗口的形状为 \(p_h\times p_w\) ,填充为 \((p_h, p_w)\) ,跨步为\((s_h, s_w)\)

  4. 为什么您希望最大池和平均池的工作方式有所不同?

  5. 我们需要一个单独的最小池层吗?你能换个运算吗?

  6. 是否可以考虑平均和最大池之间的另一种操作(提示:recall the softmax)?为什么不那么受欢迎?