Lecture 12 nn 网络层:卷积层
在上节课中,我们学习了如何在 PyTorch 中搭建神经网络模型,以及在搭建网络的过程中常用的容器: Sequential
、ModuleList
和 ModuleDict
。本节课开始,我们将学习 PyTorch 中常见的网络层,现在我们先重点学习卷积层。
1. 一维、二维和三维卷积
卷积运算 (Convolution):卷积核在输入信号 (图像) 上滑动,相应位置上进行 乘加。 卷积核 (Kernel):又称为滤波器/过滤器,可认为是某种模式/某种特征。
卷积过程类似于用一个模版去图像上寻找与它相似的区域,与卷积核模式越相似,激活值越高,从而实现特征提取。所以在深度学习中,我们可以将卷积核视为特征提取器。
下图是 AlexNet 卷积核的可视化,我们发现卷积核实际上学习到的是 边缘、条纹、色彩 这些细节模式:
这进一步验证了卷积核是图像的某种特征提取器,而具体的特征模式则完全由模型学习得到。
卷积维度 (Dimension):一般情况下,一个卷积核在一个信号上沿几个维度上滑动,就是几维卷积。
1d 卷积:
2d 卷积:
3d 卷积:
可以看到,一个卷积核在一个信号上沿几个维度滑动,就是几维卷积。注意这里我们强调 一个卷积核 和 一个信号,因为通常我们会涉及包含多个卷积核和多个信号的卷积操作,这种情况下怎么去判断卷积的维度呢,这里我们可以先思考一下。
2. 二维卷积
nn.Conv2d
功能:对多个二维平面信号进行二维卷积。
1
2
3
4
5
6
7
8
9
10
11
nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros'
)
主要参数:
in_channels
:输入通道数。out_channels
:输出通道数,等价于卷积核个数。kernel_size
:卷积核尺寸。-
stride
:步长。下面是一个步长为 2 的卷积: -
padding
:填充个数。常用于保持输入输出图像尺寸匹配,可以用于提高输出图像的分辨率: -
dilation
:空洞卷积大小。常用于图像分割任务,目的是提高感受野,即输出图像的一个像素对应输入图像上更大的一块区域: -
groups
:分组卷积的组数。常用于模型的轻量化。例如,Alexnet 当时由于硬件限制采用了两组卷积操作: bias
:偏置。最终输出响应值时需加上偏置项。
尺寸计算:
-
简化版 (不带
\[\mathrm{out}_{\text{size}} = \dfrac{\mathrm{in}_{\text{size}} - \mathrm{kernel}_{\text{size}}}{\mathrm{stride}} + 1\]padding
和dilation
): -
完整版:
\[H_{\text{out}} = \left\lfloor \dfrac{H_{\text{in}} + 2\times \mathrm{padding}[0] - \mathrm{dilation}[0] \times (\text{kernel_size}[0]-1)-1}{\mathrm{stride}[0]} + 1 \right\rfloor\]
代码示例:
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
31
32
import os
import torch.nn as nn
from PIL import Image
from torchvision import transforms
from matplotlib import pyplot as plt
from tools.common_tools import transform_invert, set_seed
set_seed(3) # 设置随机种子,用于调整卷积核权值的状态。
# ================================= 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 convolution layer ==========================
conv_layer = nn.Conv2d(3, 1, 3) # input:(i, o, size) weights:(o, i , h, w)
nn.init.xavier_normal_(conv_layer.weight.data)
# calculation
img_conv = conv_layer(img_tensor)
# =========================== visualization ==================================
print("卷积前尺寸:{}\n卷积后尺寸:{}".format(img_tensor.shape, img_conv.shape))
img_conv = transform_invert(img_conv[0, 0:1, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_conv, cmap='gray')
plt.subplot(121).imshow(img_raw)
plt.show()
输出结果:
1
2
卷积前尺寸:torch.Size([1, 3, 512, 512])
卷积后尺寸:torch.Size([1, 1, 510, 510])
-
set.seed(1)
时的输出: -
set.seed(2)
时的输出: -
set.seed(3)
时的输出:
可以看到,不同的卷积核权值对应的输出是不相同的。通常,我们会在卷积层中设置多个卷积核,以提取不同的特征。
在上面的例子中,我们使用一个 3 维的卷积核实现了一个 2d 卷积:
我们的输入是一个 RGB 的二维图像,它包含 3 个色彩通道。然后,我们将创建 3 个二维卷积核,不同通道对应不同的卷积核。我们将三个通道的卷积结果相加,然后再加上偏置项,得到最终的卷积结果。
3. 转置卷积
转置卷积 (Transpose Convolution) 又称为 反卷积 (Deconvolution)注 1 或者 部分跨越卷积 (Fractionally strided Convolution),常见于图像分割任务中,主要用于对图像进行 上采样 (UpSample)。
(注 1:这里我们说的反卷积不同于信号系统中的反卷积)。
为什么称为转置卷积?
-
正常卷积:
假设图像尺寸为 $4 \times 4$,卷积核为 $3\times 3$,$\mathrm{padding} = 0$,$\mathrm{stride} = 1$。
- 图像:$I_{16\times 1}$,这里 $16$ 是输入图像的像素总数,$1$ 表示图片张数。
- 卷积核:$K_{4\times 16}$,这里 $4$ 是输出图像的像素总数,$16$ 是由卷积核中的 $9$ 个元素另外补零后得到。
- 输出:$O_{4\times 1} = K_{\color{orange}{4\times 16}} \times I_{16\times 1}$
-
转置卷积:上采样,输出图像比输入图像尺寸更大。
假设图像尺寸为 $2 \times 2$,卷积核为 $3\times 3$,$\mathrm{padding} = 0$,$\mathrm{stride} = 1$。
- 图像:$I_{4\times 1}$,这里 $4$ 是输入图像的像素总数,$1$ 表示图片张数。
- 卷积核:$K_{16\times 4}$,这里 $16$ 是输出图像的像素总数,$4$ 是由卷积核中的 $9$ 个元素剔除一部分后得到。
- 输出:$O_{16\times 1} = K_{\color{orange}{16\times 4}} \times I_{4\times 1}$
可以看到,转置卷积与正常卷积的卷积核尺寸在形状上是转置关系,这也是我们将其称为转置卷积的原因。注意,二者只是在形状上是转置关系,但它们的权值是完全不同的。也就是说,该卷积过程是不可逆的,即卷积后再转置卷积,得到的图像和初始图像是完全不同的。
nn.ConvTranspose2d
功能:转置卷积实现上采样。
1
2
3
4
5
6
7
8
9
10
11
12
nn.ConvTranspose2d(
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode='zeros'
)
主要参数:
in_channels
:输入通道数。out_channels
:输出通道数。kernel_size
:卷积核尺寸。stride
:步长。padding
:填充个数。dilation
:空洞卷积大小。groups
:分组卷积设置。bias
:偏置。
尺寸计算:
-
简化版 (不带
\[\mathrm{out}_{\text{size}} = (\mathrm{in}_{\text{size}} -1)\times \mathrm{stride} + \mathrm{kernel}_{\text{size}}\]padding
和dilation
): -
完整版:
\[H_{\text{out}} = (H_{\text{in}}-1) \times \mathrm{stride}[0] - 2 \times \mathrm{padding}[0] + \mathrm{dilation}[0] \times (\text{kernel_size}[0]-1) + \mathrm{output\_padding}[0]+ 1\]
代码示例:
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
31
32
import os
import torch.nn as nn
from PIL import Image
from torchvision import transforms
from matplotlib import pyplot as plt
from tools.common_tools import transform_invert, set_seed
set_seed(3) # 设置随机种子,用于调整卷积核权值的状态。
# ================================= 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 convolution layer ==========================
conv_layer = nn.ConvTranspose2d(3, 1, 3, stride=2) # input:(i, o, size)
nn.init.xavier_normal_(conv_layer.weight.data)
# calculation
img_conv = conv_layer(img_tensor)
# =========================== visualization ==================================
print("卷积前尺寸:{}\n卷积后尺寸:{}".format(img_tensor.shape, img_conv.shape))
img_conv = transform_invert(img_conv[0, 0:1, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_conv, cmap='gray')
plt.subplot(121).imshow(img_raw)
plt.show()
输出结果:
1
2
卷积前尺寸:torch.Size([1, 3, 512, 512])
卷积后尺寸:torch.Size([1, 1, 1025, 1025])
可以看到,在经过转置卷积上采样后,图像出现了一个奇怪的现象:输出的图像上有许多网格。这被称为 棋盘效应 (Checkerboard Artifacts),是由于转置卷积中的不均匀重叠造成的。关于棋盘效应的解释以及解决方法请参考论文 Deconvolution and Checkerboard Artifacts。
4. 总结
本节课中,我们学习了 nn
模块中卷积层。在下次课程中,我们将学习 nn
模块中的其他常用网络层。
下节内容:nn 网络层:池化层、线性层和激活函数层
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载,并请注明来自:YEY 的博客 同时保持文章内容的完整和以上声明信息!