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=(insize−1)×stride+kernelsize
- 完整版:
Hout=(Hin−1)×stride[0]−H_{\\text{out}} = (H_{\\text{in}}-1) \\times \\mathrm{stride}[0] - Hout=(Hin−1)×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+e−x1
梯度公式:
y′=y∗(1−y)y^{\\prime}=y *(1-y) y′=y∗(1−y)
特性:
- 输出值在 (0,1)(0,1)(0,1), 符合概率性质。
- 导数范围是 [0,0.25][0,0.25][0,0.25], 容易导致梯度消失。
- 输出为非 0 均值, 会破坏数据分布。
4.3.2.nn.tanh
计算公式:
y=sinxcosx=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+e−xex−e−x=1+e−2x2+1
梯度公式:
y′=1−y2y^{\\prime}=1-y^2 y′=1−y2
特性:
- 输出值在 (−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。下节中,我们将学习网络层权值的初始化。