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 的博客 同时保持文章内容的完整和以上声明信息!