> 文章列表 > ch03-PyTorch模型搭建

ch03-PyTorch模型搭建

ch03-PyTorch模型搭建

ch03-PyTorch模型搭建

    • 0.引言
    • 1.模型创建步骤与 nn.Module
      • 1.1. 网络模型的创建步骤
      • 1.2. nn.Module
      • 1.3. 总结
    • 2.模型容器与 AlexNet 构建
      • 2.1. 模型容器
        • 2.1.1.nn.Sequential
        • 2.1.2.nn.ModuleList
        • 2.1.3.nn.ModuleDict
      • 2.2. AlexNet 构建
      • 2.3. 总结
    • 3.nn 网络层:卷积
      • 3.1. 一维、二维和三维卷积
        • 3.1.1.1d 卷积
        • 3.1.2.2d 卷积
        • 3.1.3.3d 卷积
      • 3.2. 二维卷积
      • 3.3. 转置卷积
      • 3.4. 总结
    • 4.nn 网络层:池化层、全连接层和激活函数层
      • 4.1. 池化层
        • 4.1.1.nn.MaxPool2d
        • 4.1.2.nn.AvgPool2d
        • 4.1.3.divisor_override 的使用
        • 4.1.4.nn.MaxUnpool2d
      • 4.2. 线性层
      • 4.3. 激活函数层
        • 4.3.1.nn.Sigmoid
        • 4.3.2.nn.tanh
        • 4.3.3.nn.ReLU
        • 4.3.4.nn.LeakyReLU
        • 4.3.5.nn.PReLU
        • 4.3.6.nn.RReLU
      • 4.4. 总结

0.引言

1.模型创建步骤与 nn.Module

前几节中,我们学习了 PyTorch 的数据模块,并了解了 PyTorch 如何从硬盘中读取数据,然后对数据进行预处理、数据增强,最后转换为张量的形式输入到我们的模型中。在深度模型中,会对张量进行一系列复杂的数学运算,最终得到用于分类、分割、目标检测等任务的输入。本节中,我们将学习 PyTorch 中模型的创建以及 nn.Module 的相关概念。

1.1. 网络模型的创建步骤

在学习创建模型之前,我们先回顾一下之前提到的机器学习模型训练的 5 个步骤:

在这里插入图片描述

我们已经在前几节课中完成了对数据模块的学习,接下来我们开始学习模型模块。

在这里插入图片描述

回顾一下之前在人民币分类的例子中我们使用过的 LeNet 网络:

LeNet 模型结构图:

在这里插入图片描述

可以看到,LeNet 网络由 7 个层构成:卷积层 1、池化层 1、卷积层 2、池化层 2,以及 3 个全连接层。在创建 LeNet 时,需要先构建这些子模块,在构建完成这 7 个子网络层后,我们会采用一定的顺序对其进行连接。最后,将它们包装起来就得到我们的 LeNet 网络。

在 PyTorch 中,LeNet 是一个 Module 的概念,而它的子网络层也是一个 Module 的概念,它们都属于 nn.Module 类。所以,一个 nn.Module (例如:LeNet) 可以包含很多个子 Module (例如:卷积层、池化层等)。

下面我们从计算图的角度来观察模型的创建过程:

在这里插入图片描述

计算图中有两个主要的概念:结点和边。其中,结点代表张量 (数据),边代表运算。LeNet 整体上可以视为一组张量运算:它接收一个 32*32*3 的张量,经过一系列复杂运算之后,输出一个长度为 10 的向量作为分类概率。而在 LeNet 内部,则由一系列子网络层构成,例如:卷积层 1 对一个 32*32*3 的张量进行卷积操作得到一个 28*28*6 的张量,并将其作为下一层子网络的输入,经过这种不断的前向传播,最终计算得到输出概率。在深度学习中,该过程被称为 前向传播

我们从网络结构和计算图的角度分析了 LeNet 网络模型,并且知道了构建模型的两个要素:构建子模块和拼接子模块。

在这里插入图片描述

接下来,我们还是通过之前人民币二分类的例子来学习如何构建模型。

构建模型:

# ============================ step 2/5 模型 ============================
net = LeNet(classes=2)
net.initialize_weights()

LeNet 类:

class LeNet(nn.Module):# 构建子模块def __init__(self, classes):super(LeNet, self).__init__()self.conv1 = nn.Conv2d(3, 6, 5)self.conv2 = nn.Conv2d(6, 16, 5)self.fc1 = nn.Linear(16*5*5, 120)self.fc2 = nn.Linear(120, 84)self.fc3 = nn.Linear(84, classes)# 拼接子模块def forward(self, x):out = F.relu(self.conv1(x))out = F.max_pool2d(out, 2)out = F.relu(self.conv2(out))out = F.max_pool2d(out, 2)out = out.view(out.size(0), -1)out = F.relu(self.fc1(out))out = F.relu(self.fc2(out))out = self.fc3(out)return outdef initialize_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.xavier_normal_(m.weight.data)if m.bias is not None:m.bias.data.zero_()elif isinstance(m, nn.BatchNorm2d):m.weight.data.fill_(1)m.bias.data.zero_()elif isinstance(m, nn.Linear):nn.init.normal_(m.weight.data, 0, 0.1)m.bias.data.zero_()

1.2. nn.Module

在模型模块中,我们有一个非常重要的概念 —— nn.Module。我们所有的模型和网络层都是继承自 nn.Module 这个类的,所以我们有必要了解它。在学习 nn.Module 之前,我们先来看一下与其相关的几个模块:

在这里插入图片描述

首先是 torch.nn,它是 PyTorch 的一个神经网络模块,其中又有很多子模块,这里我们需要了解其中的 4 个模块:nn.Parameter、nn.Module、nn.functional 和 nn.init。本节课我们先重点关注 nn.Module。

  • nn.Module

在 nn.Module 中有 8 个重要的属性,用于管理整个模型:

self._parameters = OrderedDict()
self._buffers = OrderedDict()
self._backward_hooks = OrderedDict()
self._forward_hooks = OrderedDict()
self._forward_pre_hooks = OrderedDict()
self._state_dict_hooks = OrderedDict()
self._load_state_dict_pre_hooks = OrderedDict()
self._modules = OrderedDict()

主要属性:

  • parameters:存储管理 nn.Parameter 类。
  • modules:存储管理 nn.Module 类。
  • buffers:存储管理缓冲属性,如 BN 层中的 running_mean。
  • *_hooks:存储管理钩子函数。

这里,我们重点关注其中的两个属性:parameters 和 modules。

nn.Module 的属性构建机制:

在 module 类里面进行属性赋值时会先被 setattr 函数拦截,该函数对即将赋值的数据类型进行判断:如果赋值是 nn.Parameter 类,则将其存入 parameters 字典中进行管理;如果赋值是 nn.Module 类,则将其存入 modules 字典中进行管理。

nn.Module 总结:

  • 一个 module 可以包含多个子 module。
    • 例如:在 LeNet 这个 module 下会包含一些卷积层、池化层等子 module。
  • 一个 module 相当于一个运算,必须实现 forward() 函数。
    • 从计算图的角度来看,一个 module 接收一个张量,经过一系列复杂运算,输出概率或者其他数据。因此,我们需要在其中实现一个前向传播的函数。
  • 每个 module 都有 8 个 有序字典 (OrderedDict) 管理它的属性。
    • 这里,最常用的是 parameters 字典和 modules 字典。

1.3. 总结

本节中,我们学习了 nn.Module 的概念以及模型创建的两个要素。下节中,我们将学习容器 Containers 以及 AlexNet 的搭建。

2.模型容器与 AlexNet 构建

上节中,我们学习了如何搭建一个模型,搭建模型的过程中有两个要素:构建子模块和拼接子模块。另外,搭建模型时还有一个非常重要的概念:模型容器 (Containers)。本节课我们将学习模型容器以及 AlexNet 的构建。

2.1. 模型容器

在 PyTorch 模型容器中有三个常用模块:nn.Sequetial、nn.ModuleList 和 nn.ModuleDict。

在这里插入图片描述

2.1.1.nn.Sequential

nn.Sequential 是 nn.Module 的容器,用于 按顺序 包装一组网络层。

在这里插入图片描述

nn.Sequential 将一组网络层按顺序包装为一个整体,可以视为模型的一个子模块。在传统的机器学习中有一个步骤被称为特征工程:我们需要人为地设计特征,并将特征输入到分类器当中进行分类。在深度学习时代,特征工程这一概念已经被弱化,尤其是在卷积神经网络中,我们不需要人为设计图像特征,相反,我们可以让卷积神经网络去自动学习特征,并在最后加上几个全连接层用于输出分类结果。在早期的神经网络当中,用于分类的分类器是由全连接构成的,所以在深度学习时代,通常也习惯以全连接层为界限,将网络模型划分为特征提取模块和分类模块。对一个大的模型进行划分可以方便按照模块进行管理:例如在上面的 LeNet 模型中,我们可以将多个卷积层和池化层包装为一个特征提取器,并且将后面的几个全连接层包装为一个分类器,最后再将这两个模块包装为一个完整的 LeNet 神经网络。在 PyTorch 中,我们可以使用 nn.Sequential 完成这些包装过程。

代码示例:

class LeNetSequential(nn.Module):def __init__(self, classes):super(LeNetSequential, self).__init__()self.features = nn.Sequential(nn.Conv2d(3, 6, 5),nn.ReLU(),nn.MaxPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, 5),nn.ReLU(),nn.MaxPool2d(kernel_size=2, stride=2),)self.classifier = nn.Sequential(nn.Linear(16*5*5, 120),nn.ReLU(),nn.Linear(120, 84),nn.ReLU(),nn.Linear(84, classes),)def forward(self, x):x = self.features(x)x = x.view(x.size()[0], -1)x = self.classifier(x)return xclass LeNetSequentialOrderDict(nn.Module):def __init__(self, classes):super(LeNetSequentialOrderDict, self).__init__()self.features = nn.Sequential(OrderedDict({'conv1': nn.Conv2d(3, 6, 5),'relu1': nn.ReLU(inplace=True),'pool1': nn.MaxPool2d(kernel_size=2, stride=2),'conv2': nn.Conv2d(6, 16, 5),'relu2': nn.ReLU(inplace=True),'pool2': nn.MaxPool2d(kernel_size=2, stride=2),}))self.classifier = nn.Sequential(OrderedDict({'fc1': nn.Linear(16*5*5, 120),'relu3': nn.ReLU(),'fc2': nn.Linear(120, 84),'relu4': nn.ReLU(inplace=True),'fc3': nn.Linear(84, classes),}))def forward(self, x):x = self.features(x)x = x.view(x.size()[0], -1)x = self.classifier(x)return xnet = LeNetSequential(classes=2)
net = LeNetSequentialOrderDict(classes=2)
fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)
output = net(fake_img)
print(net)
print(output)

nn.Sequential 的两个特性:

  • 顺序性:各网络层之间严格按照顺序构建。
  • 自带 forward():自带的 forward 里,通过 for 循环依次执行前向传播运算。

2.1.2.nn.ModuleList

nn.ModuleList 是 nn.Module 的容器,用于包装一组网络层,以 迭代 方式调用网络层。

主要方法:

  • append():在 ModuleList 后面 添加 网络层。
  • extend():拼接 两个 ModuleList。
  • insert():指定在 ModuleList 中位置 插入 网络层。

代码示例:

class ModuleList(nn.Module):def __init__(self):super(ModuleList, self).__init__()# 构建 20 个全连接层self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(20)])def forward(self, x):for i, linear in enumerate(self.linears):x = linear(x)return xnet = ModuleList()
print(net)
fake_data = torch.ones((10, 10))
output = net(fake_data)
print(output)

2.1.3.nn.ModuleDict

nn.ModuleDict 是 nn.Module 的容器,用于包装一组网络层,以 索引 方式调用网络层。

主要方法:

  • clear():清空 ModuleDict。
  • items():返回可迭代的键值对 (key - value pairs)。
  • keys():返回字典的键 (key)。
  • values():返回字典的值 (value)。
  • pop():返回一对键值,并从字典中删除。

代码示例:

class ModuleDict(nn.Module):def __init__(self):super(ModuleDict, self).__init__()self.choices = nn.ModuleDict({'conv': nn.Conv2d(10, 10, 3),'pool': nn.MaxPool2d(3)})self.activations = nn.ModuleDict({'relu': nn.ReLU(),'prelu': nn.PReLU()})def forward(self, x, choice, act):x = self.choices[choice](x)x = self.activations[act](x)return xnet = ModuleDict()
fake_img = torch.randn((4, 10, 32, 32))
output = net(fake_img, 'conv', 'relu')
print(output)

容器总结

  • nn.Sequential:顺序性,各网络层之间严格按顺序执行,常用于 block 构建。
  • nn.ModuleList:迭代性,常用于大量重复网络层构建,通过 for 循环实现重复构建。
  • nn.ModuleDict:索引性,常用于可选择的网络层。

2.2. AlexNet 构建

AlexNet:2012 年以高出第二名 10 多个百分点的准确率获得 ImageNet 分类任务冠军,开创了卷积神经网络的新时代。

AlexNet 特点如下:

  • 采用 ReLU:替换饱和激活函数 (例如:Sigmoid),减轻梯度消失。
  • 采用 LRN (Local Response Normalization):对数据归一化,减轻梯度消失。
  • Dropout:提高全连接层的鲁棒性,增加网络的泛化能力。
  • Data Augmentation:TenCrop,色彩修改。
  • 参考文献:ImageNet Classification with Deep Convolutional Neural Networks

在这里插入图片描述

AlexNet 采用了卷积、池化、卷积、池化的堆叠方式来提取数据特征,后面再接上三个全连接层进行分类。这里,我们可以应用 nn.Sequential 中的概念,将前面的卷积池化部分包装成一个 features 模块,将后面的全连接部分包装成一个 classifier 模块,从而将一个复杂网络分解成一个特征提取模块和一个分类模块。

PyTorch 在 torchvision.models 中内置了 AlexNet 的实现:

class AlexNet(nn.Module):def __init__(self, num_classes=1000):super(AlexNet, self).__init__()self.features = nn.Sequential(nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=3, stride=2),nn.Conv2d(64, 192, kernel_size=5, padding=2),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=3, stride=2),nn.Conv2d(192, 384, kernel_size=3, padding=1),nn.ReLU(inplace=True),nn.Conv2d(384, 256, kernel_size=3, padding=1),nn.ReLU(inplace=True),nn.Conv2d(256, 256, kernel_size=3, padding=1),nn.ReLU(inplace=True),nn.MaxPool2d(kernel_size=3, stride=2),)self.avgpool = nn.AdaptiveAvgPool2d((6, 6))self.classifier = nn.Sequential(nn.Dropout(),nn.Linear(256 * 6 * 6, 4096),nn.ReLU(inplace=True),nn.Dropout(),nn.Linear(4096, 4096),nn.ReLU(inplace=True),nn.Linear(4096, num_classes),)def forward(self, x):x = self.features(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.classifier(x)return x

代码示例:

alexnet = torchvision.models.AlexNet()

2.3. 总结

本节中,我们学习了 3 种不同的模型容器:Sequential、ModuleList、ModuleDict,以及 AlexNet 的搭建。下节课中,我们将学习 nn 中网络层的具体使用。

3.nn 网络层:卷积层

在上节课中,我们学习了如何在 PyTorch 中搭建神经网络模型,以及在搭建网络的过程中常用的容器: Sequential、ModuleList 和 ModuleDict。本节课开始,我们将学习 PyTorch 中常见的网络层,现在我们先重点学习卷积层。

3.1. 一维、二维和三维卷积

卷积运算 (Convolution):卷积核在输入信号 (图像) 上滑动,相应位置上进行 乘加。 卷积核 (Kernel):又称为滤波器/过滤器,可认为是某种模式/某种特征。

卷积过程类似于用一个模版去图像上寻找与它相似的区域,与卷积核模式越相似,激活值越高,从而实现特征提取。所以在深度学习中,我们可以将卷积核视为特征提取器。

请添加图片描述

下图是 AlexNet 卷积核的可视化,我们发现卷积核实际上学习到的是 边缘、条纹、色彩 这些细节模式:

在这里插入图片描述

这进一步验证了卷积核是图像的某种特征提取器,而具体的特征模式则完全由模型学习得到。

卷积维度 (Dimension):一般情况下,一个卷积核在一个信号上沿几个维度上滑动,就是几维卷积。

3.1.1.1d 卷积

请添加图片描述

3.1.2.2d 卷积

请添加图片描述

3.1.3.3d 卷积

请添加图片描述

可以看到,一个卷积核在一个信号上沿几个维度滑动,就是几维卷积。注意这里我们强调 一个卷积核 和 一个信号,因为通常我们会涉及包含多个卷积核和多个信号的卷积操作,这种情况下怎么去判断卷积的维度呢,这里我们可以先思考一下。

3.2. 二维卷积

nn.Conv2d
功能:对多个二维平面信号进行二维卷积。

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:偏置。最终输出响应值时需加上偏置项。

尺寸计算:

在这里插入图片描述

代码示例:

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_seedset_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()

输出结果:

卷积前尺寸: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.3. 转置卷积

转置卷积 (Transpose Convolution) 又称为 反卷积 (Deconvolution)注 1 或者 部分跨越卷积 (Fractionally strided Convolution),常见于图像分割任务中,主要用于对图像进行 上采样 (UpSample)。

(注 1:这里我们说的反卷积不同于信号系统中的反卷积)。

为什么称为转置卷积?

正常卷积:
请添加图片描述

假设图像尺寸为 4×44 \\times 44×4, 卷积核为 3×33 \\times 33×3, padding =0=0=0, stride =1=1=1

  • 图像: I16×1I_{16 \\times 1}I16×1, 这里 16 是输入图像的像素总数, 1 表示图片张数。
    。 卷积核: K4×16K_{4 \\times 16}K4×16, 这里 4 是输出图像的像素总数, 16 是由卷积核中的 9 个元素另外补零 后得到。
  • 输出: O4×1=K1×16×I16×1O_{4 \\times 1}=K_{1 \\times 16} \\times I_{16 \\times 1}O4×1=K1×16×I16×1

  • 转置卷积: 上采样,输出图像比输入图像尺寸更大。

请添加图片描述

假设图像尺寸为 2×22 \\times 22×2, 卷积核为 3×33 \\times 33×3, padding =0=0=0, stride =1=1=1

  • 图像: I4×1I_{4 \\times 1}I4×1, 这里 4 是输入图像的像素总数, 1 表示图片张数。
    。 卷积核: K16×4K_{16 \\times 4}K16×4, 这里 16 是输出图像的像素总数, 4 是由卷积核中的 9 个元素剔除一部 分后得到。
    。输出: O16×1=K16×4×I4×1O_{16 \\times 1}=K_{16 \\times 4} \\times I_{4 \\times 1}O16×1=K16×4×I4×1

可以看到,转置卷积与正常卷积的卷积核尺寸在形状上是转置关系,这也是我们将其称为转置卷积的原因。注意,二者只是在形状上是转置关系,但它们的权值是完全不同的。也就是说,该卷积过程是不可逆的,即卷积后再转置卷积,得到的图像和初始图像是完全不同的。

nn.ConvTranspose2d

功能:转置卷积实现上采样。

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

尺寸计算:

  • 简化版 (不带 padding 和 dilation):

outsize=(insize−1)×stride+kernelsize\\mathrm{out}_{\\text{size}} = (\\mathrm{in}_{\\text{size}} -1)\\times \\mathrm{stride} + \\mathrm{kernel}_{\\text{size}}outsize=(insize1)×stride+kernelsize

  • 完整版:

Hout=(Hin−1)×stride[0]−H_{\\text{out}} = (H_{\\text{in}}-1) \\times \\mathrm{stride}[0] - Hout=(Hin1)×stride[0]2×padding[0]+dilation[0]×(kernelsize[0]−1)+output_padding[0]+12 \\times \\mathrm{padding}[0] + \\mathrm{dilation}[0] \\times( \\text{kernelsize}[0]-1) + \\mathrm{output\\_padding}[0]+ 12×padding[0]+dilation[0]×(kernelsize[0]1)+output_padding[0]+1
代码示例:

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_seedset_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()

输出结果:

卷积前尺寸:torch.Size([1, 3, 512, 512])
卷积后尺寸:torch.Size([1, 1, 1025, 1025])

在这里插入图片描述

可以看到,在经过转置卷积上采样后,图像出现了一个奇怪的现象:输出的图像上有许多网格。这被称为 棋盘效应 (Checkerboard Artifacts),是由于转置卷积中的不均匀重叠造成的。关于棋盘效应的解释以及解决方法请参考论文 Deconvolution and Checkerboard Artifacts。

3.4. 总结

本节课中,我们学习了 nn 模块中卷积层。在下次课程中,我们将学习 nn 模块中的其他常用网络层。

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

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

4.1. 池化层

请添加图片描述

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

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

最大池化 vs. 平均池化:

在这里插入图片描述

4.1.1.nn.MaxPool2d

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

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:记录池化像素索引。通常在最大值反池化上采样时使用。

代码示例:

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_seedset_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()

输出结果:

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

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

4.1.2.nn.AvgPool2d

在这里插入图片描述

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

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:除法因子。计算平均值时代替像素个数作为分母。

代码示例:

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_seedset_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()

输出结果:

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

在这里插入图片描述

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

4.1.3.divisor_override 的使用

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

正常的平均池化:

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))

输出结果:

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.]]]])

计算池化后的像素值:

1+1+1+14=1\\frac{1+1+1+1}{4}=141+1+1+1=1

divisor_override=3 的平均池化:

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))

输出结果:

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]]]])

计算池化后的像素值:

1+1+1+13=1.3333\\frac{1+1+1+1}{3}=1.333331+1+1+1=1.3333

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

4.1.4.nn.MaxUnpool2d

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

nn.MaxUnpool2d(kernel_size,stride=None,padding=0
)forward(self, input, indices, output_size=None)

主要参数:

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

最大值反池化:

在这里插入图片描述

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

代码示例:

# 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))

输出结果:

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

4.2. 线性层

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

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

在这里插入图片描述

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

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

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

主要参数:

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

计算公式:

y=xWT+by = xW^T + by=xWT+b

代码示例:

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)

输出结果:

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])

4.3. 激活函数层

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

在这里插入图片描述

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

4.3.1.nn.Sigmoid

在这里插入图片描述

计算公式:
y=11+e−xy=\\frac{1}{1+e^{-x}} y=1+ex1
梯度公式:
y′=y∗(1−y)y^{\\prime}=y *(1-y) y=y(1y)
特性:

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

4.3.2.nn.tanh

在这里插入图片描述
计算公式:
y=sin⁡xcos⁡x=ex−e−xex+e−x=21+e−2x+1y=\\frac{\\sin x}{\\cos x}=\\frac{e^x-e^{-x}}{e^x+e^{-x}}=\\frac{2}{1+e^{-2 x}}+1 y=cosxsinx=ex+exexex=1+e2x2+1
梯度公式:
y′=1−y2y^{\\prime}=1-y^2 y=1y2
特性:

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

4.3.3.nn.ReLU

在这里插入图片描述

计算公式:
y=max⁡(0,x)y=\\max (0, x) y=max(0,x)
梯度公式:
y′={1,x>0undefined, ,x=00,x<0y^{\\prime}= \\begin{cases}1, & x>0 \\\\ \\text { undefined, }, & x=0 \\\\ 0, & x<0\\end{cases} y=1, undefined, ,0,x>0x=0x<0
特性:

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

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

在这里插入图片描述

4.3.4.nn.LeakyReLU

  • negative_slope:负半轴斜率。

4.3.5.nn.PReLU

  • init:可学习斜率。

4.3.6.nn.RReLU

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

4.4. 总结

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