PyTorch 12:nn 网络层:卷积层

PyTorch 学习笔记

Posted by YEY on December 16, 2020

Lecture 12 nn 网络层:卷积层

在上节课中,我们学习了如何在 PyTorch 中搭建神经网络模型,以及在搭建网络的过程中常用的容器: SequentialModuleListModuleDict。本节课开始,我们将学习 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:偏置。最终输出响应值时需加上偏置项。

尺寸计算

  • 简化版 (不带 paddingdilation):

    \[\mathrm{out}_{\text{size}} = \dfrac{\mathrm{in}_{\text{size}} - \mathrm{kernel}_{\text{size}}}{\mathrm{stride}} + 1\]
  • 完整版:

    \[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:偏置。

尺寸计算

  • 简化版 (不带 paddingdilation):

    \[\mathrm{out}_{\text{size}} = (\mathrm{in}_{\text{size}} -1)\times \mathrm{stride} + \mathrm{kernel}_{\text{size}}\]
  • 完整版:

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