6.6 池化层

🏷️sec0606_Convolutional_Pooling

6.6.1 池化层的功能

获取图像的全局语义是机器学习任务最常见的目标,例如图像中是否存在一艘宇宙飞船?因此,当我们进行图像处理时,总是希望能够逐渐降低隐层神经元的空间分辨率,并进行信息聚集。随着模型层叠数量的上升,每个神经元所接受的感受野会逐渐增大,最后的输出应该对整个输入的全局都敏感。通过对信息的逐渐聚合,层间的映射会变得越来越“粗糙”,细节信息将会被逐渐丢弃,模型最终会实现全局表示的学习。由于信息的汇聚是逐渐进行的,因此,细节信息仍然会保留在模型不同深度的中间层中。换句话说,卷积层的所有优势依然会被存储在模型之中。当我们需要去探测一些较为底层的特征时,依然可以从这些中间层中去将它们给读取出来,例如第 6.3.5 节中所讨论的边缘检测。

在第 6.2 节中,我们介绍了卷积神经网络的典型的三级结构 [卷积层-激活层-池化层]。在这组术语中,卷积网络会被视作少量相对复杂层的组合,每层都具有很多级,例如典型的三级组合。在这种三级结构中,第一级是特征提取级,它可以并行或串行地计算一个或多个卷积,并产生一组线性激活响应。在第二级中,线性激活会通过一个非线性激活函数实现整型,并产生非线性激活响应,例如限制线性单元激活函数ReLU,该级有时被称为探测级。在第三级中,一个称之为池化函数(pooling function)的功能函数会被用来实现邻域信息的汇集。在另一种术语中,卷积网络会被视为大量简单层的组合,即每个处理步骤都会被认为是一个独立的层。这意味着并非每一层都有参数,例如池化层和ReLU激活函数层都属于无参数的层。

严格地说,池化操作并非一种运算,而是一类运算,它将相邻位置的总体统计特征作为网络在该位置的输出。最大池化(max pooling)(Zhou, 1988)是最常用的一种池化函数,它能给出相邻矩形区域内的最大值。其他常用的池化函数(Boureau, 2010)包括求平均值的平均池化(average pooling)、求 L2L^2 范数的池化、聚类池化(Boureau, 2011)、可学习核的池化(Tolstikhin, 2021)以及求基于距中心像素距离的加权平均函数池化等。

当然,无论采用哪一种池化函数,我们都希望它能够具有保持输入平移不变性的能力。也就是说,如果输入因为某种原因发生了少量的平移,经过池化函数后的大多数输出仍然保持不变。局部平移不变性是一种很有用的性质,尤其是当我们只关心某个特征是否出现而不关心它出现的具体位置时。例如,在判断监控视频中是否有行人存在时,我们并不需要知道行人位于视频中的精确像素位置,只确定位于视频中的目标符合人的基本结构就可以了。具体而言,假设存在一幅边缘轮廓十分清洗的图像 II,如果整幅图像或者局部区域向右偏移了一个像素生成了新的图像 ZZ,则有 Z[i,j]=I[i,j+1]Z[i,j]=I[i,j+1]。虽然只有细微的差异,但新生成的图像 ZZ 的输出可能会与原始图像 II 大不相同。然而在现实中,任何拍摄的图片都不可能总是发生在同一个像素上,即使使用三脚架拍摄静止的物体,也可能会因为快门的移动、相机的振动或光照的扰动而产生局部细微的偏移。在一些特定的领域中,保存特征的具体位置是极为重要的,但即使是在高清样本中,保持细微的平移不变特性依然很有意义。

池化可以被看作是一种无限强的先验,它必须要能够学习到目标少量平移的不变性。当这个假设成立时,池化可以极大地提高网络的统计效率。此外,由于池化通常反映的是一个局部区域的综合反馈,因此池化生成的单元数量可能会少于它所探测的单元数量。此时的池化,相当于综合了一个区域 kk 个像素的统计特征,而不仅仅只是单个像素的信息。这种信息汇聚方法使得输出的网格数相比输入少了 kk 倍,因此大大提高了网格的计算效率,可以有效地实现空间的降采样。更进一步,也使得参数存储的需求得到了降低。

在本小节中,我们将介绍两种最常见的池化层,它们都具有双重目的,降低卷积层对位置的敏感性以及降低模型对空间降采样的敏感性。

6.6.2 最大池化层和平均池化层

与卷积层类似,池化运算符也是由一个固定尺寸的窗口组成。该窗口在输入特征图上从左上角开始,按照从左到右、从上至下的顺序进行遍历滑动,然后在每个位置都输出一个标量值。与卷积层不同的是,池化层并不存在一个可以用来与输入特征图进行互相关运算的核(基于核方法的池化例外)。它仅仅只存在一个窗口,所有的输出都由这个窗口所框选范围内的输入产生,因此池化层不包含参数。池化运算是一种确定性的运算,它可以被看作是输入信息的汇聚过程。近年来,我们常常将池化层称之为汇聚层,池化窗口称为汇聚窗口,池化运算也称为汇聚运算。这是因为池化的过程使用特征汇聚来描述会更加贴切一些,并且也能更直接地从字面去理解它的功能和意义。通常我们会计算汇聚窗口内所有元素的最大值或平均值,这两种池化分别称为最大池化层(max-pooling)和平均池化层(average-pooling)。图6-28 给出了这两种操作的示例。

图6-28 最大池化和平均池化

图6-28 最大池化和平均池化

以左边的最大池化为例。作为输入的池化窗口的形状为 2×22×2,红色框内的输出是第一个输出元素,以及用于生成这个输出的输入元素。不难看出,最大池化会将窗口所框定范围内所有像素中最大的那个值给取出来,这也是它为什么称之为最大池化的原因。这个过程可以公式表示为:o=max{xii=[1,N+]}o=\max \{x_i|i=[1,N_+] \}。例如,上图中的第一个输出元素值为:max(1,1,5,6)=6max(1,1,5,6)=6。平均池化是另一种常用的池化,不过它计算的是池化窗口中所有元素的平均值。例如右图中第一个输出元素值为:avg(1,1,5,6)=3.25avg(1,1,5,6)=3.25。在池化过程中,我们将池化窗口形状为 m×nm×n 的池化层称为 m×nm×n 池化层,其对应的池化操作称为 m×nm×n 池化。例如将 图6-28 中左图的池化层称为 2×22×2 最大池化层,其操作称为 2×22×2 最大池化(max-pooling)。

下面,我们再来看看池化是如何实现少量平移不变性的。或者说它为什么能够降低卷积层对位置的敏感性。图6-29 的输入图是一幅关于站在栏杆前远望城市的人群的黑白图,这张图包含大量的黑白交替的垂直边缘。假设我们放大图像中的一个边缘,并将其数值化后作为输入层输入到模型中。如下图所示,此时可以得到一个 6×66×6 的二维矩阵。

图6-29 具有少量平移不变性的池化层

图6-29 具有少量平移不变性的池化层

从输入矩阵中不难看出,该局部区域是一个从白到黑的垂直边缘,也就是说它左边是白色,右边是黑色。因为像素值 11 代表的是白色,而 00 代表的是黑色。如果我们使用一个左边是 11,右边是 1-12×22×2 的卷积核去对它进行卷积。就可以得到一个 5×55×5 的输出矩阵。在这个矩阵中,第三列的值全为 22。不难想到,这个 22 就是图像中黑色区域和白色区域的交界线,也就是我们说的边缘。在这条边缘的左边和右边都是 00,这代表着左右都没有边缘信息。可以说 2×22×2 的卷积核进行的边缘检测完成得非常完美,也非常的显著。但是反过来讲,通过卷积检测出来的边缘信息也是非常敏感的。正如我们前面所描述的,当我们想要输出这个边缘信息时,如果不小心偏移了一个像素,无论是往左还是往右,最终的输出都会变成 00。这意味着边缘检测失效了。此时,如果我们使用一个 2×22×2 的窗口对卷积层的输出进行最大池化操作,那么我们都将获得一个宽度为 22 的边缘。也就是说,无论 I[i,j]I[i,j]I[i,j+1]I[i,j+1] 的值是否相同,或 I[i,j+1]I[i,j+1]I[i,j+2]I[i,j+2] 的值是否相同,池化层始终能输出相同的 Y[i,j]Y[i,j]。换句话说,在 2×22×2 的最大池化的作用下,即使像素在高度或者宽度上移动了一个像素,后面的卷积层仍然能够识别到边缘模式的存在。相似的,平均池化也具有相似的性质,不同的只是它检测到的边缘的强度有所不同。

下面我们使用python代码来实现 图6-29 给出的实例。首先是导入必要的库。

程序清单6-21 导入必备库和自定义库

# codes06021_print_pooling
# 0. 导入必备库和自定义库
import numpy as np
import sys
import paddle
sys.path.append(r'D:\WorkSpace\DeepLearning\WebsiteV2')   # 定义自定义模块保存位置
from codes.paddle import common

接下来,我们定义一个简易版的池化函数pool2D,该函数可以根据输入矩阵和池化窗口的尺度来执行池化运算的前向传输。该函数可以在卷积层的输出上同时实现最大池化和平均池化。

程序清单6-22 定义二维池化函数

# @save common.pool2D
def pool2D(X, pool_size, mode='max'):
    """在池化窗口内进行池化/汇聚运算"""
    "X: 输入矩阵"
    "pool_size:池化窗口的尺度,形态为 [height, width]"
    "mode:max最大池化,avg均值池化"
    h, w = pool_size
    Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - 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+h, j:j+w].max()
            elif mode == 'avg':
                Y[i,j] = X[i:i+h, j:j+w].mean()
    return Y

我们可以构建图 6-29 中的输入张量X,然后验证二维最大池化的输出。

程序清单6-23 定义输入矩阵并输出最大池化的结果

# 1. 生成6×6的卷积层条纹图
X = paddle.zeros((6, 6))
X[:, 0:3] = 1
print('输出图像矩阵:\n{}'.format(X)) # 输出图像矩阵

# 2. 定义2×2的垂直边缘检测器(卷积核),并输出卷积运算的结果
W = paddle.to_tensor([[1,-1], [1,-1]])
out_conv = common.cross_correlation(X, W)
print('卷积后的输出:\n{}'.format(out_conv))

# 3. 执行池化函数输出最大池化结果
out_maxpooling = common.pool2D(out_conv, (2,2), 'max')
print('最大池化后的输出:\n{}'.format(out_maxpooling))

输出图像矩阵:
Tensor(shape=[6, 6], dtype=float32, place=Place(gpu:0), stop_gradient=True,
       [[1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0., 0.]])

卷积后的输出:
Tensor(shape=[5, 5], dtype=float32, place=Place(cpu), stop_gradient=True,
       [[0., 0., 2., 0., 0.],
        [0., 0., 2., 0., 0.],
        [0., 0., 2., 0., 0.],
        [0., 0., 2., 0., 0.],
        [0., 0., 2., 0., 0.]])

最大池化后的输出:
[[0. 2. 2. 0.]
 [0. 2. 2. 0.]
 [0. 2. 2. 0.]
 [0. 2. 2. 0.]]

此外,我们也可以验证平均池化层的输出。

程序清单6-24 输出平均池化的结果

# 4. 执行池化函数输出均值池化结果
out_avgpooling = common.pool2D(out_conv, (2,2), 'avg')
print('平均池化后的输出:\n{}'.format(out_avgpooling))

均值池化后的输出:
[[0. 1. 1. 0.]
 [0. 1. 1. 0.]
 [0. 1. 1. 0.]
 [0. 1. 1. 0.]]

最后,我们使用Paddle内置的API接口来实现池化操作。需要注意的是,进入到API的输入需要转换为四维张量。在本例中,除特征图的高度和宽度外,样本数和通道数的维度都是1。可以看到API接口和自定义函数的输出是相同的。

程序清单6-25 使用内置API接口输出最大池化和平均池化的结果

# 5. 使用Paddle内置接口定义最大池化和平均池化函数,并输出结果
# 5.1 定义池化函数
max_pool = paddle.nn.MaxPool2D(kernel_size=2, stride=1)
mean_pool = paddle.nn.AvgPool2D(kernel_size=2, stride=1)
# 5.2 将卷积输入转换为Paddle规定的张量形态
out_conv = out_conv.reshape((1,1,out_conv.shape[0],out_conv.shape[1]))
# 5.3 执行池化运算
print('最大池化后的输出(API):{}'.format(max_pool(out_conv)))
print('平均池化后的输出(API):{}'.format(mean_pool(out_conv)))

最大池化后的输出(API):Tensor(shape=[1, 1, 4, 4], dtype=float32, place=Place(cpu), stop_gradient=True,
       [[[[0., 2., 2., 0.],
          [0., 2., 2., 0.],
          [0., 2., 2., 0.],
          [0., 2., 2., 0.]]]])
均值池化后的输出(API):Tensor(shape=[1, 1, 4, 4], dtype=float32, place=Place(cpu), stop_gradient=True,
       [[[[0., 1., 1., 0.],
          [0., 1., 1., 0.],
          [0., 1., 1., 0.],
          [0., 1., 1., 0.]]]])

6.6.3 填充和步幅

与卷积层相似,池化层也可以通过填充和步幅来实现输出形状的调节。假设输入的高度和宽度是 nh×nwn_h×n_w,若池化窗口的高度和宽度为 kh×kwk_h×k_w,则输出特征图的尺度可以表示为:
(nhkh+2ph)/sh+1×(nwkw+2pw)/sw+1(6.18)\lfloor (n_h-k_h+2p_h)/s_h + 1 \rfloor × \lfloor (n_w-k_w+2p_w)/s_w + 1 \rfloor \tag{6.18}

其中,ph,pwp_h,p_wsh,sws_h,s_w 仍然表示为填充和步幅。在实际应用中,通常会设置填充为零,并使用步幅来实现特征图尺度的成倍缩小。此时,输出形状的计算可以简化为:
(nh/sh)×(nw/sw)(6.19)\lfloor(n_h/s_h)\rfloor × \lfloor (n_w/s_w)\rfloor \tag{6.19}

也就是说,输出特征图的尺度等于输入特征图的尺度除以步幅。默认情况下,大多数深度学习框架都会将步幅与池化窗口设置成相同的值,以满足该需求。因此,如果我们使用 2×22×2 的池化窗口,那么默认情况下,步幅为22(2,2)

程序清单6-26 输出步幅为2、填充为0的池化运算的结果

# codes06022_print_pooling_stride_and_padding
# 1. 步幅Stride=2,填充Padding=0
max_pool_stride2 = paddle.nn.MaxPool2D(2)
print('最大池化后的输出(stride=2):{}'.format(max_pool_stride2(out_conv)))

最大池化后的输出(stride=2:Tensor(shape=[1, 1, 2, 2], dtype=float32, place=Place(cpu), stop_gradient=True,
       [[[[0., 2.],
          [0., 2.]]]])

以上代码实现了在 5×55×5 的特征图上使用步长为 222×22×2 的最大池化对特征进行汇聚。当然,我们也可以手动设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度。

程序清单6-27 输出步幅和填充为矩形的池化运算的结果

# 3. 步幅Stride=(1,2),填充Padding=(0,1)
max_pool_stride12_padding01 = paddle.nn.MaxPool2D(kernel_size=2, stride=(1,2), padding=(0,1))
print('最大池化后的输出(stride=2,padding=1):{}'.format(max_pool_stride12_padding01(out_conv)))

最大池化后的输出(stride=2,padding=1:Tensor(shape=[1, 1, 4, 3], dtype=float32, place=Place(cpu), stop_gradient=True,
[[[[0., 2., 0.],
   [0., 2., 0.],
   [0., 2., 0.],
   [0., 2., 0.]]]])

增加填充后,代码的输出依然符合计算 公式6.19。细心的读者可能会发现,在上面的例子中,无论是否使用填充,输入矩阵的最后一行和最后一列都并没有加入到池化运算中,而是被直接丢弃了。这并不是一种比较好的设置。因此,为了不产生不必要的信息损失,通常会通过控制输入和卷积的尺度来实现输入刚好能够被池化窗口整除。

6.6.4 多通道池化

在处理多通道输入数据时,池化层与卷积层的方法略有不同。它不像卷积层一样在通道的维度上将各个通道的运算结果进行汇总输出,而是对各个通道上的数据都单独进行处理。这意味着,池化层的输入通道数与输出通道数是相同的。下面,我们对卷积的输出进行扩展,在通道维度上连接张量 out_convout_conv+1,以构建具有两个通道的输入。

程序清单6-28 输出多通道池化的结果

# codes06023_print_pooling_multichannels
out_conv_3D = paddle.concat((out_conv, out_conv+1), 1)
print('最大池化后的输出(3D):{}'.format(max_pool_stride2_padding1(out_conv_3D)))

最大池化后的输出(3D):Tensor(shape=[1, 2, 3, 3], dtype=float32, place=Place(cpu), stop_gradient=True,
[[[[0., 2., 0.],
   [0., 2., 0.],
   [0., 2., 0.]],

   [[1., 3., 1.],
   [1., 3., 1.],
   [1., 3., 1.]]]])

从以上代码的输出不难看出,池化运算保留了输入特征图的通道维度,也即输出通道数等于输入通道数。

6.6.5 小结

  1. 池化能缓解卷积层对位置的过度敏感,同时成倍地减少计算量。
  2. 常见的池化操作包括最大池化和平均池化,前者输出窗口内的最大值,后者输出窗口内的平均值。
  3. 池化层通常没有可学习的参数,所以它也不需要占用额外的存储空间。
  4. 池化层与卷积层类似,都以窗口大小、填充和步长作为超参数。假设输入为 nh×nw×cinn_h×n_w×c_{in},池化窗口为 kh×kwk_h×k_w,步长为 sh,sws_h,s_w,填充为 ph,pwp_h,p_w,则输出为 mh×mw×coutm_h×m_w×c_{out}。其中 nh=nhkh+2phsh+1n_h=\frac{n_h−k_h+2p_h}{s_h}+1nw=nwkw+2pwsw+1n_w=\frac{n_w−k_w+2p_w}{s_w}+1。若填充为零,且步幅与窗口尺度相同,则输出尺度可以简化为 nh/sh×nw/shn_h/s_h × n_w/s_h
  5. 池化不做通道融合,通道 cc 始终不变,即 cin=coutc_{in} = c_{out}

6.6.6 练习

  1. 在卷积网络中是否需要最小池化层?

  2. 以下对池化层描述正确的包括()。
    A. 池化层能够成倍地减少计算量
    B. 增加池化层不需要增加参数
    C. 池化层可以增加模型非线性特性,从而提高模型的拟合能力
    D. 池化层可以为模型增加平移不变性的特性

  3. 给定一个3×3的矩阵 A=[[2,2,2],[2,2,2],[2,2,2]],若存在一个 2×22×2 的avg-pooling核,其步长stride=1,则经过池化后的输出结果正确的一项是()。
    A. [[1,1,1],[1,1,1],[1,1,1]]
    B. [[2,2],[2,2]]
    C. [[1,1],[1,1]]
    D. [[1]]

  4. 给定一个3×3的矩阵 A=[[1,2,3],[2,3,4],[3,4,5]],若存在一个 2×22×2 的max-pooling核,其步长stride=1,则经过池化后的输出结果正确的一项是()。
    A. [[1,2,3],[2,3,4],[3,4,5]]
    B. [[2,3],[3,4]]
    C. [[3,4],[4,5]]
    D. [[5]]

  5. 若存在一个 117×117117×117 的特征图,后面紧跟一个步长为2,尺度为 3×33×3 的池化核。试求经过池化层后,特征图新的维度是多少?
    A. 117
    B. 114
    C. 58
    D. 57

  6. 观察下图给出的网络拓扑结构图,试计算Conv3在执行均值池化后的特征图的维度。
    Interaction06002LeNet
    A. 64
    B. 21
    C. 2
    D. 1

  7. 池化层最主要的作用是()。
    A. 压缩图像
    B. 提取图像特征
    C. 将多维数据一维化
    D. 连接卷积层与全连接层

6.6 池化层