7.5. 池化¶ 在 SageMaker Studio Lab 中打开 Notebook
在很多情况下,我们的最终任务是回答有关图像的一些全局性问题,例如,它是否包含一只猫? 因此,我们最后一层的单元应该对整个输入敏感。通过逐渐聚合信息,生成越来越粗糙的映射,我们实现了这一目标:最终学习一个全局表示,同时在中间层处理过程中保持卷积层的所有优点。当网络越深时,每个隐藏节点对其敏感的感受野(相对于输入)就越大。降低空间分辨率会加速这个过程,因为卷积核会覆盖更大的有效区域。
此外,在检测较低级别的特征时,例如边(如 7.2节 中讨论的),我们通常希望我们的表示在某种程度上具有平移不变性。例如,如果我们对图像 X
(在黑白之间有清晰的界限)进行处理,并将整个图像向右移动一个像素,即 Z[i, j] = X[i, j + 1]
,那么新图像 Z
的输出可能会有很大不同。边缘会移动一个像素。在现实中,物体几乎不可能出现在完全相同的位置。事实上,即使使用三脚架和静止的物体,由于快门移动引起的相机振动也可能会使所有东西移动一个像素左右(高端相机配备了特殊功能来解决这个问题)。
本节介绍池化(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,并对表示进行空间降采样。
import torch
from torch import nn
from d2l import torch as d2l
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
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.)
7.5.1. 最大池化和平均池化¶
与卷积层类似,池化算子由一个固定形状的窗口组成,该窗口根据其步幅在输入的所有区域上滑动,为固定形状窗口(有时称为池化窗口)遍历的每个位置计算一个输出。然而,与卷积层中输入和核的互相关计算不同,池化层不包含参数(没有核)。相反,池化算子是确定性的,通常计算池化窗口中元素的最大值或平均值。这些操作分别称为最大池化(max-pooling)和平均池化(average pooling)。
平均池化的历史几乎和CNN一样悠久。这个想法类似于对图像进行下采样。与其仅仅为低分辨率图像取每隔一个(或第三个)像素的值,我们可以对相邻像素进行平均,从而获得信噪比更好的图像,因为我们结合了多个相邻像素的信息。最大池化由 Riesenhuber and Poggio (1999) 在认知神经科学的背景下引入,用于描述信息如何为物体识别的目的进行分层聚合;在语音识别中已经有一个更早的版本 (Yamaguchi et al., 1990)。在几乎所有情况下,最大池化(max-pooling)都优于平均池化。
在这两种情况下,与互相关算子一样,我们可以认为池化窗口从输入张量的左上角开始,从左到右、从上到下滑动。在池化窗口滑过的每个位置,它都会计算窗口中输入子张量的最大值或平均值,具体取决于采用的是最大池化还是平均池化。
图 7.5.1 池化窗口形状为 \(2\times 2\) 的最大池化。阴影部分是第一个输出元素以及用于输出计算的输入张量元素:\(\max(0, 1, 3, 4)=4\)。¶
图 7.5.1中的输出张量的高度为2,宽度为2。这四个元素由每个池化窗口中的最大值导出:
更一般地,我们可以通过对一个给定大小的区域进行聚合来定义一个 \(p \times q\) 的池化层。回到边缘检测的问题,我们使用卷积层的输出作为 \(2\times 2\) 最大池化的输入。用 X
表示卷积层的输入,用 Y
表示池化层的输出。无论 X[i, j]
、X[i, j + 1]
、X[i+1, j]
和 X[i+1, j + 1]
的值是否不同,池化层总是输出 Y[i, j] = 1
。也就是说,使用 \(2\times 2\) 最大池化层,我们仍然可以检测到由卷积层识别的模式在高度或宽度上移动不超过一个元素。
在下面的代码中,我们在 pool2d
函数中实现了池化层的前向传播。这个函数类似于 7.2节 中的 corr2d
函数。但是,这里不需要核,而是将输出计算为输入中每个区域的最大值或平均值。
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = np.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = jnp.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y = Y.at[i, j].set(X[i: i + p_h, j: j + p_w].max())
elif mode == 'avg':
Y = Y.at[i, j].set(X[i: i + p_h, j: j + p_w].mean())
return Y
import tensorflow as tf
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = tf.Variable(tf.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w +1)))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j].assign(tf.reduce_max(X[i: i + p_h, j: j + p_w]))
elif mode =='avg':
Y[i, j].assign(tf.reduce_mean(X[i: i + p_h, j: j + p_w]))
return Y
我们可以构建 图 7.5.1 中的输入张量 X
来验证二维最大池化层的输出。
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
tensor([[4., 5.],
[7., 8.]])
X = np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
[22:02:56] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
array([[4., 5.],
[7., 8.]])
X = jnp.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
Array([[4., 5.],
[7., 8.]], dtype=float32)
X = tf.constant([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[4., 5.],
[7., 8.]], dtype=float32)>
此外,我们也可以实验一下平均池化层。
pool2d(X, (2, 2), 'avg')
tensor([[2., 3.],
[5., 6.]])
pool2d(X, (2, 2), 'avg')
array([[2., 3.],
[5., 6.]])
pool2d(X, (2, 2), 'avg')
Array([[2., 3.],
[5., 6.]], dtype=float32)
pool2d(X, (2, 2), 'avg')
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[2., 3.],
[5., 6.]], dtype=float32)>
7.5.2. 填充和步幅¶
与卷积层一样,池化层也会改变输出形状。和之前一样,我们可以通过填充输入和调整步幅来调整操作以获得期望的输出形状。我们可以通过深度学习框架中内置的二维最大池化层来演示在池化层中使用填充和步幅。我们首先构建一个输入张量 X
,其形状有四个维度,其中样本数(批量大小)和通道数都为1。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
X = np.arange(16, dtype=np.float32).reshape((1, 1, 4, 4))
X
array([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
X = jnp.arange(16, dtype=jnp.float32).reshape((1, 4, 4, 1))
X
Array([[[[ 0.],
[ 1.],
[ 2.],
[ 3.]],
[[ 4.],
[ 5.],
[ 6.],
[ 7.]],
[[ 8.],
[ 9.],
[10.],
[11.]],
[[12.],
[13.],
[14.],
[15.]]]], dtype=float32)
请注意,与其他框架不同,TensorFlow 更倾向于并针对*通道最后*的输入进行了优化。
X = tf.reshape(tf.range(16, dtype=tf.float32), (1, 4, 4, 1))
X
<tf.Tensor: shape=(1, 4, 4, 1), dtype=float32, numpy=
array([[[[ 0.],
[ 1.],
[ 2.],
[ 3.]],
[[ 4.],
[ 5.],
[ 6.],
[ 7.]],
[[ 8.],
[ 9.],
[10.],
[11.]],
[[12.],
[13.],
[14.],
[15.]]]], dtype=float32)>
由于池化聚合了来自一个区域的信息,深度学习框架默认将池化窗口大小和步幅相匹配。例如,如果我们使用形状为 (3, 3)
的池化窗口,默认情况下我们会得到一个形状为 (3, 3)
的步幅。
pool2d = nn.MaxPool2d(3)
# Pooling has no model parameters, hence it needs no initialization
pool2d(X)
tensor([[[[10.]]]])
pool2d = nn.MaxPool2D(3)
# Pooling has no model parameters, hence it needs no initialization
pool2d(X)
array([[[[10.]]]])
# Pooling has no model parameters, hence it needs no initialization
nn.max_pool(X, window_shape=(3, 3), strides=(3, 3))
Array([[[[10.]]]], dtype=float32)
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3])
# Pooling has no model parameters, hence it needs no initialization
pool2d(X)
<tf.Tensor: shape=(1, 1, 1, 1), dtype=float32, numpy=array([[[[10.]]]], dtype=float32)>
当然,如果需要,可以手动指定步幅和填充来覆盖框架的默认值。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]]]])
pool2d = nn.MaxPool2D(3, padding=1, strides=2)
pool2d(X)
array([[[[ 5., 7.],
[13., 15.]]]])
X_padded = jnp.pad(X, ((0, 0), (1, 0), (1, 0), (0, 0)), mode='constant')
nn.max_pool(X_padded, window_shape=(3, 3), padding='VALID', strides=(2, 2))
Array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)
paddings = tf.constant([[0, 0], [1,0], [1,0], [0,0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid',
strides=2)
pool2d(X_padded)
<tf.Tensor: shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)>
当然,我们可以指定一个任意的矩形池化窗口,其高度和宽度可以任意指定,如下例所示。
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]]]])
pool2d = nn.MaxPool2D((2, 3), padding=(0, 1), strides=(2, 3))
pool2d(X)
array([[[[ 5., 7.],
[13., 15.]]]])
X_padded = jnp.pad(X, ((0, 0), (0, 0), (1, 1), (0, 0)), mode='constant')
nn.max_pool(X_padded, window_shape=(2, 3), strides=(2, 3), padding='VALID')
Array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)
paddings = tf.constant([[0, 0], [0, 0], [1, 1], [0, 0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[2, 3], padding='valid',
strides=(2, 3))
pool2d(X_padded)
<tf.Tensor: shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)>
7.5.3. 多通道¶
在处理多通道输入数据时,池化层对每个输入通道分别进行池化,而不是像卷积层那样将各通道的输入相加。这意味着池化层的输出通道数与输入通道数相同。下面,我们将在通道维度上连接张量 X
和 X + 1
,以构建一个具有两个通道的输入。
X = torch.cat((X, X + 1), 1)
X
tensor([[[[ 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.]]]])
X = np.concatenate((X, X + 1), 1)
X
array([[[[ 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.]]]])
# Concatenate along `dim=3` due to channels-last syntax
X = jnp.concatenate([X, X + 1], 3)
X
Array([[[[ 0., 1.],
[ 1., 2.],
[ 2., 3.],
[ 3., 4.]],
[[ 4., 5.],
[ 5., 6.],
[ 6., 7.],
[ 7., 8.]],
[[ 8., 9.],
[ 9., 10.],
[10., 11.],
[11., 12.]],
[[12., 13.],
[13., 14.],
[14., 15.],
[15., 16.]]]], dtype=float32)
请注意,由于TensorFlow的通道最后(channels-last)语法,这需要在最后一个维度上进行连接。
# Concatenate along `dim=3` due to channels-last syntax
X = tf.concat([X, X + 1], 3)
X
<tf.Tensor: shape=(1, 4, 4, 2), dtype=float32, numpy=
array([[[[ 0., 1.],
[ 1., 2.],
[ 2., 3.],
[ 3., 4.]],
[[ 4., 5.],
[ 5., 6.],
[ 6., 7.],
[ 7., 8.]],
[[ 8., 9.],
[ 9., 10.],
[10., 11.],
[11., 12.]],
[[12., 13.],
[13., 14.],
[14., 15.],
[15., 16.]]]], dtype=float32)>
正如我们所看到的,池化后输出通道的数量仍然是2。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
pool2d = nn.MaxPool2D(3, padding=1, strides=2)
pool2d(X)
array([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
X_padded = jnp.pad(X, ((0, 0), (1, 0), (1, 0), (0, 0)), mode='constant')
nn.max_pool(X_padded, window_shape=(3, 3), padding='VALID', strides=(2, 2))
Array([[[[ 5., 6.],
[ 7., 8.]],
[[13., 14.],
[15., 16.]]]], dtype=float32)
paddings = tf.constant([[0, 0], [1,0], [1,0], [0,0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid',
strides=2)
pool2d(X_padded)
<tf.Tensor: shape=(1, 2, 2, 2), dtype=float32, numpy=
array([[[[ 5., 6.],
[ 7., 8.]],
[[13., 14.],
[15., 16.]]]], dtype=float32)>
请注意,TensorFlow池化的输出乍一看似乎不同,但数值上与MXNet和PyTorch的结果相同。差异在于维度,垂直读取输出会得到与其他实现相同的输出。
7.5.4. 总结¶
池化是一个极其简单的操作。它正如其名,在一个值窗口上聚合结果。所有的卷积语义,如步幅和填充,都以与之前相同的方式应用。请注意,池化与通道无关,即它不改变通道数量,并且它分别应用于每个通道。最后,在两种流行的池化选择中,最大池化优于平均池化,因为它赋予了输出一定程度的不变性。一个流行的选择是选择一个 \(2 \times 2\) 的池化窗口大小,将输出的空间分辨率减少四分之一。
请注意,除了池化之外,还有许多其他降低分辨率的方法。例如,在随机池化 (Zeiler and Fergus, 2013) 和分数最大池化 (Graham, 2014) 中,聚合与随机化相结合。这在某些情况下可以略微提高准确性。最后,正如我们稍后将在注意力机制中看到的,还有更精细的方法来聚合输出,例如,通过使用查询和表示向量之间的对齐。
7.5.5. 练习¶
通过卷积实现平均池化。
证明最大池化不能仅通过卷积来实现。
最大池化可以使用ReLU操作来完成,即 \(\textrm{ReLU}(x) = \max(0, x)\)。
仅使用ReLU操作来表示 \(\max (a, b)\)。
利用这一点,通过卷积和ReLU层来实现最大池化。
对于 \(2 \times 2\) 卷积,你需要多少个通道和层?对于 \(3 \times 3\) 卷积呢?
池化层的计算成本是多少?假设池化层的输入大小为 \(c\times h\times w\),池化窗口的形状为 \(p_\textrm{h}\times p_\textrm{w}\),填充为 \((p_\textrm{h}, p_\textrm{w})\),步幅为 \((s_\textrm{h}, s_\textrm{w})\)。
为什么你期望最大池化和平均池化的工作方式不同?
我们是否需要一个单独的最小池化层?你能用其他操作来替换它吗?
我们可以使用softmax操作进行池化。为什么它可能不那么流行?