PyTorch 13:nn 网络层:池化层、线性层和激活函数层

PyTorch 学习笔记

Posted by YEY on December 16, 2020

Lecture 13 nn 网络层:池化层、全连接层和激活函数层

上节课中,我们学习了网络层中的卷积层。本节课中,我们将继续学习其他几种网络层:池化层、线性层和激活函数层。

1. 池化层

池化运算 (Pooling):对信号进行 “收集”“总结”,类似水池收集水资源,因而得名池化层。

  • “收集”:多变少。
  • “总结”:最大值/平均值。

最大池化 vs. 平均池化

nn.MaxPool2d

功能:对二维信号(图像)进行最大值池化。

1
2
3
4
5
6
7
8
nn.MaxPool2d(
    kernel_size,
    stride=None,
    padding=0,
    dilation=1,
    return_indices=False,
    ceil_mode=False
)

主要参数

  • kernel_size:池化核尺寸。
  • stride:步长。
  • padding:填充个数。
  • dilation:池化核间隔大小。
  • ceil_mode:尺寸是否向上取整。用于计算输出特征图尺寸,默认设置为向下取整。
  • return_indices:记录池化像素索引。通常在最大值反池化上采样时使用。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
import torch
import torch.nn as nn
from torchvision import transforms
from matplotlib import pyplot as plt
from PIL import Image
from tools.common_tools import transform_invert, set_seed

set_seed(1)  # 设置随机种子

# ================================= load img ==================================
path_img = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lena.png")
img = Image.open(path_img).convert('RGB')  # 0~255

# convert to tensor
img_transform = transforms.Compose([transforms.ToTensor()])
img_tensor = img_transform(img)
img_tensor.unsqueeze_(dim=0)    # C*H*W to B*C*H*W

# ========================== create maxpool layer =============================
maxpool_layer = nn.MaxPool2d((2, 2), stride=(2, 2))   # input:(i, o, size) weights:(o, i , h, w)
img_pool = maxpool_layer(img_tensor)

# ================================= visualization =============================
print("池化前尺寸:{}\n池化后尺寸:{}".format(img_tensor.shape, img_pool.shape))
img_pool = transform_invert(img_pool[0, 0:3, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_pool)
plt.subplot(121).imshow(img_raw)
plt.show()

输出结果:

1
2
池化前尺寸:torch.Size([1, 3, 512, 512])
池化后尺寸:torch.Size([1, 3, 256, 256])

可以看到,经过最大池化后的图像尺寸减小了一半,而图像质量并没有明显降低。因此,池化操作可以剔除图像中的冗余信息,以及减小后续的计算量。

nn.AvgPool2d

功能:对二维信号(图像)进行平均值池化。

1
2
3
4
5
6
7
8
nn.AvgPool2d(
    kernel_size,
    stride=None,
    padding=0,
    ceil_mode=False,
    count_include_pad=True,
    divisor_override=None
)

主要参数

  • kernel_size:池化核尺寸。
  • stride:步长。
  • padding:填充个数。
  • ceil_mode:尺寸向上取整。
  • count_include_pad:是否将填充值用于平均值的计算。
  • divisor_override:除法因子。计算平均值时代替像素个数作为分母。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
import torch
import torch.nn as nn
from torchvision import transforms
from matplotlib import pyplot as plt
from PIL import Image
from tools.common_tools import transform_invert, set_seed

set_seed(1)  # 设置随机种子

# ================================= load img ==================================
path_img = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lena.png")
img = Image.open(path_img).convert('RGB')  # 0~255

# convert to tensor
img_transform = transforms.Compose([transforms.ToTensor()])
img_tensor = img_transform(img)
img_tensor.unsqueeze_(dim=0)    # C*H*W to B*C*H*W

# ========================== create avgpool layer =============================
avgpoollayer = nn.AvgPool2d((2, 2), stride=(2, 2))   # input:(i, o, size) weights:(o, i , h, w)
img_pool = avgpoollayer(img_tensor)

# =============================== visualization ===============================
print("池化前尺寸:{}\n池化后尺寸:{}".format(img_tensor.shape, img_pool.shape))
img_pool = transform_invert(img_pool[0, 0:3, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_pool)
plt.subplot(121).imshow(img_raw)
plt.show()

输出结果:

1
2
池化前尺寸:torch.Size([1, 3, 512, 512])
池化后尺寸:torch.Size([1, 3, 256, 256])

同样,图像尺寸减小了一半,而质量并没有明显降低。另外,如果我们仔细对比最大池化与平均池化的结果,可以发现最大池化后的图像会偏亮一些,而平均池化后的图像会偏暗一些,这是由于两种池化操作采用不同的计算方式造成的 (像素值越大,图像亮度越高)。

divisor_override 的使用

现在,我们来看一下除法因子的使用。这里,我们初始化一个 $4\times 4$ 的图像,并且采用一个 $2\times 2$ 的窗口,步长设置为 $2$。

正常的平均池化

1
2
3
4
5
img_tensor = torch.ones((1, 1, 4, 4))
avgpool_layer = nn.AvgPool2d((2, 2), stride=(2, 2))
img_pool = avgpool_layer(img_tensor)

print("raw_img:\n{}\npooling_img:\n{}".format(img_tensor, img_pool))

输出结果:

1
2
3
4
5
6
7
8
raw_img:
tensor([[[[1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.]]]])
pooling_img:
tensor([[[[1., 1.],
          [1., 1.]]]])

计算池化后的像素值:

\[\dfrac{1+1+1+1}{4} = 1\]

divisor_override=3 的平均池化

1
2
3
4
5
img_tensor = torch.ones((1, 1, 4, 4))
avgpool_layer = nn.AvgPool2d((2, 2), stride=(2, 2), divisor_override=3)
img_pool = avgpool_layer(img_tensor)

print("raw_img:\n{}\npooling_img:\n{}".format(img_tensor, img_pool))

输出结果:

1
2
3
4
5
6
7
8
raw_img:
tensor([[[[1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.]]]])
pooling_img:
tensor([[[[1.3333, 1.3333],
          [1.3333, 1.3333]]]])

计算池化后的像素值:

\[\dfrac{1+1+1+1}{3} = 1.3333\]

目前为止,我们学习了最大池化和平均池化,它们都是对图像实现下采样的过程,即输入尺寸较大的图像,输出尺寸较小的图像。下面我们将学习反池化,即将小尺寸图像变为大尺寸图像。

nn.MaxUnpool2d

功能:对二维信号(图像)进行最大值反池化上采样。

1
2
3
4
5
6
7
8
nn.MaxUnpool2d(
    kernel_size,
    stride=None,
    padding=0
)

forward(self, input, indices, output_size=None)

主要参数

  • kernel_size:池化核尺寸。
  • stride:步长。
  • padding:填充个数。

最大值反池化:

早期的自编码器和图像分割任务中都会涉及一个上采样的操作,当时普遍采用的方法是最大值反池化上采样。上图左半部分是最大池化过程,原始 $4\times 4$ 的图像经过最大池化后得到一个 $2\times 2$ 的下采样图像,然后经过一系列的网络层之后,进入上图右半部分的上采样解码器,即将一个尺寸较小的图像经过上采样得到一个尺寸较大的图像。此时,涉及到的一个问题是:我们应该将像素值放到什么位置。例如:右边 $2\times 2$ 图像中的左上角的 $3$ 应当放入最终 $4\times 4$ 图像中的左上部分的 $4$ 个像素中的哪一个?这时,我们就可以利用之前最大池化过程中记录的池化像素索引,将 $3$ 放入之前原始 $4\times 4$ 图像中左上角的 $4$ 个像素中最大值对应的位置。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
# pooling
img_tensor = torch.randint(high=5, size=(1, 1, 4, 4), dtype=torch.float)
maxpool_layer = nn.MaxPool2d((2, 2), stride=(2, 2), return_indices=True)
img_pool, indices = maxpool_layer(img_tensor)

# unpooling
img_reconstruct = torch.randn_like(img_pool, dtype=torch.float)
maxunpool_layer = nn.MaxUnpool2d((2, 2), stride=(2, 2))
img_unpool = maxunpool_layer(img_reconstruct, indices)

print("raw_img:\n{}\nimg_pool:\n{}".format(img_tensor, img_pool))
print("img_reconstruct:\n{}\nimg_unpool:\n{}".format(img_reconstruct, img_unpool))

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
raw_img:
tensor([[[[0., 4., 4., 3.],
          [3., 3., 1., 1.],
          [4., 2., 3., 4.],
          [1., 3., 3., 0.]]]])
img_pool:
tensor([[[[4., 4.],
          [4., 4.]]]])
img_reconstruct:
tensor([[[[-1.0276, -0.5631],
          [-0.8923, -0.0583]]]])
img_unpool:
tensor([[[[ 0.0000, -1.0276, -0.5631,  0.0000],
          [ 0.0000,  0.0000,  0.0000,  0.0000],
          [-0.8923,  0.0000,  0.0000, -0.0583],
          [ 0.0000,  0.0000,  0.0000,  0.0000]]]])

这里,我们初始化一个 $4\times 4$ 的图像,并且采用一个 $2\times 2$ 的窗口,步长设置为 $2$。首先,我们对其进行最大值池化,并记录其中的最大值像素的索引。然后,我们进行反池化,这里反池化的输入和之前最大池化后得到的图像尺寸是一样的,并且反池化层的窗口和步长与之前最大池化层是一致的。最后,我们将输入和索引传入反池化层,得到与原始图像尺寸相同的图像。

2. 线性层

线性层 (Linear Layer) 又称 全连接层 (Full-connected Layer),其每个神经元与上一层所有神经元相连,实现对前一层的 线性组合/线性变换

在卷积神经网络进行分类的时候,在输出之前,我们通常会采用一个全连接层对特征进行处理,在 PyTorch 中,全连接层又称为线性层,因为如果不考虑激活函数的非线性性质,那么全连接层就是对输入数据进行一个线性组合。

每个神经元都和前一层中的所有神经元相连,每个神经元的计算方式是对上一层的加权求和的过程。因此,线性层可以采用矩阵乘法来实现。注意,上图中我们暂时忽略了偏置项。

nn.Linear

功能:对一维信号(向量)进行线性组合。

1
nn.Linear(in_features, out_features, bias=True)

主要参数

  • in_features:输入结点数。
  • out_features:输出结点数。
  • bias:是否需要偏置。

计算公式

\[y = x W^{\mathrm T} + \text{bias}\]

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
inputs = torch.tensor([[1., 2, 3]])
linear_layer = nn.Linear(3, 4)
linear_layer.weight.data = torch.tensor([[1., 1., 1.],
                                         [2., 2., 2.],
                                         [3., 3., 3.],
                                         [4., 4., 4.]])
linear_layer.bias.data.fill_(0.5)
output = linear_layer(inputs)

print(inputs, inputs.shape)
print(linear_layer.weight.data, linear_layer.weight.data.shape)
print(output, output.shape)

输出结果:

1
2
3
4
5
6
tensor([[1., 2., 3.]]) torch.Size([1, 3])
tensor([[1., 1., 1.],
        [2., 2., 2.],
        [3., 3., 3.],
        [4., 4., 4.]]) torch.Size([4, 3])
tensor([[ 6.5000, 12.5000, 18.5000, 24.5000]], grad_fn=<AddmmBackward>) torch.Size([1, 4])

3. 激活函数层

激活函数 (Activation Function) 是对特征进行非线性变换,赋予多层神经网络具有 深度 的意义。

在上面最后一步中,由于矩阵乘法的结合性,我们可以把右边三个权重矩阵先结合相乘,可以得到一个大的权重矩阵 $W$。这样我们可以看到,我们的 $\textit{Output}$ 实际上就是输入 $X$ 乘以一个大的权重矩阵 $W$。因此,这里的三层线性全连接层实际上等价于一个一层的全连接层,这是由于线性运算当中矩阵乘法的结合性导致的,并且这里我们没有引入非线性激活函数。如果加上 非线性激活函数,这一结论将不再成立,因此我们说,激活函数赋予了多层神经网络具有 深度 的意义。

nn.Sigmoid

计算公式

\[y = \dfrac{1}{1+e^{-x}}\]

梯度公式

\[y' = y*(1-y)\]

特性

  • 输出值在 $(0,1)$,符合概率性质。
  • 导数范围是 $[0, 0.25]$,容易导致梯度消失。
  • 输出为非 $0$ 均值,会破坏数据分布。

nn.tanh

计算公式

\[y= \dfrac{\sin x}{\cos x}=\dfrac{e^x - e^{-x}}{e^x + e^{-x}} = \dfrac{2}{1+e^{-2x}}+1\]

梯度公式

\[y' = 1- y^2\]

特性

  • 输出值在 $(- 1,1)$,数据符合 $0$ 均值。
  • 导数范围是 $(0, 1)$,容易导致梯度消失。

nn.ReLU

计算公式

\[y= \max(0,x)\]

梯度公式

\[y'=\begin{cases}1\,, & x > 0 \\[2ex] \text{undefined}\,, & x=0 \\[2ex]0\,, & x < 0\end{cases}\]

特性

  • 输出值均为正数,负半轴导致死神经元。
  • 导数是 $1$,可以缓解梯度消失,但容易引发梯度爆炸。

针对 ReLU 激活函数负半轴死神经元的问题,有以下几种改进方式:

nn.LeakyReLU

  • negative_slope:负半轴斜率。

nn.PReLU

  • init:可学习斜率。

nn.RReLU

  • lower:均匀分布下限。
  • upper:均匀分布上限。

4. 总结

本节课中,我们学习了 nn 模块中池化层、线性层和激活函数层。在池化层中有正常的最大值池化、均值池化,还有图像分割任务中常用的反池化 —— MaxUnpool;在激活函数中我们学习了 Sigmoid、Tanh 和 Relu,以及 Relu 的各种变体,如 LeakyReLU、PReLU、RReLU。下节课中,我们将学习网络层权值的初始化。

下节内容:权值初始化

知识共享许可协议本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载,并请注明来自:YEY 的博客 同时保持文章内容的完整和以上声明信息!