在本章中,我们将首先简要回顾卷积神经网络(CNN)架构的发展历程,然后详细研究不同的CNN架构。我们将使用PyTorch实现这些CNN架构,并通过此过程深入探索PyTorch在构建深度CNN时提供的工具(模块和内置函数)。在PyTorch中获得强大的CNN专业知识将使我们能够解决涉及CNN的许多深度学习问题。这也将帮助我们构建更复杂的深度学习模型或应用程序,其中CNN是一个组成部分。
本章将涵盖以下主题:
- 为什么CNN如此强大?
- CNN架构的演变
- 从头开发LeNet
- 微调AlexNet模型
- 运行预训练的VGG模型
- 探索GoogLeNet和Inception v3
- 讨论ResNet和DenseNet架构
- 理解EfficientNets及CNN架构的未来
本章的所有代码文件可以在 github.com/arj7192/Mas… 找到。
让我们开始讨论CNN的关键特性。
为什么CNN如此强大?
卷积神经网络(CNN)在解决图像分类、目标检测、目标分割、视频处理、自然语言处理和语音识别等挑战性问题方面,表现出了强大的能力。其成功归因于以下几个因素:
- 权重共享:这使得CNN在参数上非常高效,即使用相同的一组权重或参数提取不同的特征。特征是模型通过其参数生成的输入数据的高级表示。
- 自动特征提取:多个特征提取阶段帮助CNN自动学习数据集中的特征表示。
- 层次学习:多层的CNN结构帮助CNN学习低级、中级和高级特征。
- 探索数据的空间和时间相关性:例如在视频处理任务中。
除了这些固有的基本特性外,CNN在以下领域的改进推动了其多年来的发展:
- 激活函数和损失函数的改进:例如,使用ReLU来克服梯度消失问题。
- 参数优化:例如,使用基于自适应动量的优化器(Adam)而不是简单的随机梯度下降。
- 正则化:除了L2正则化外,还应用了dropout和批量归一化。
常见问题解答 – 什么是梯度消失问题?
神经网络中的反向传播是基于链式法则的。根据链式法则,损失函数相对于输入层参数的梯度可以表示为每层梯度的乘积。如果这些梯度都小于1,甚至趋向于0,则这些梯度的乘积将会是一个非常小的值。梯度消失问题可能在优化过程中造成严重的麻烦,因为它阻止了网络参数的值发生变化,相当于学习受阻。
近年来,CNN的显著发展驱动因素之一是各种架构创新:
- 基于空间探索的CNN:空间探索的理念是使用不同的卷积核大小,以便在输入数据中探索不同层次的视觉特征。以下图示展示了一个空间探索型CNN模型的示例架构:
- 基于深度的CNN:这里的“深度”指的是神经网络的层数。因此,这里的理念是创建一个具有多个卷积层的CNN模型,以提取高度复杂的视觉特征。以下图示展示了这种模型架构的示例:
- 基于宽度的CNN:宽度指的是数据中的通道数量或从数据中提取的特征图的数量。因此,基于宽度的CNN主要是通过在从输入层到输出层的过程中增加特征图的数量来提高模型的能力,如下图所示:
- 基于多路径的CNN:到目前为止,前面提到的三种架构在层之间的连接上都有单调性,即只有相邻层之间存在直接连接。基于多路径的CNN引入了在非连续层之间建立快捷连接或跳跃连接的概念。下图展示了一个多路径CNN模型架构的示例:
基于多路径的架构的一个关键优势是信息在多个层之间的流动更为顺畅,这得益于跳跃连接。这反过来也使得梯度可以更有效地流回输入层,减少了过多的梯度消失。
在了解了CNN模型中不同的架构设置之后,我们将继续探讨CNN自首次使用以来的发展历程。
CNN架构的演变
CNN自1989年诞生以来,经过了显著的发展。当时,Yann LeCun开发了第一个多层CNN模型,该模型能够执行识别手写数字的视觉认知任务。1998年,LeCun开发了一种改进的卷积网络模型,称为LeNet。由于在光学识别任务中的高精度,LeNet在发明后不久便被应用于工业界。自那时以来,CNN不仅在学术研究中取得了成功,而且在实际工业应用中也表现出色。下图展示了从1989年到2020年,CNN架构发展历程的简要时间线:
正如我们所见,1998年和2012年之间存在显著的空白期。这主要有两个原因:
- 数据集不足:当时没有足够大且适合展示CNN,特别是深度CNN能力的数据集。
- 计算能力有限:可用的计算能力比较有限。
此外,关于第一个原因,现有的小数据集(如MNIST)上,传统的机器学习模型(如SVM)开始超越CNN的表现。
随着时间的推移,从1998年到2012年及以后的发展,这两大限制得到了缓解。首先,得益于互联网的兴起以及数字相机和智能手机等设备的普及,数字数据的增长呈指数级增长。其次,计算能力得到了巨大的提升,包括GPU的到来。
这些变化导致了几个CNN的发展。为了解决反向传播中的梯度爆炸和衰减问题,开发了ReLU激活函数。网络参数值的非随机初始化被证明至关重要。Max pooling被发明为一种有效的子采样方法。GPU在大规模训练神经网络,尤其是CNN时变得越来越流行。
最重要的是,斯坦福大学的一个研究小组创建了一个大规模的标注图像数据集,称为ImageNet [1]。这个数据集至今仍然是CNN模型的主要基准数据集之一。
随着这些发展积累,在2012年,一种不同的架构设计大大提升了CNN在ImageNet数据集上的性能。这个网络被称为AlexNet(以其创建者Alex Krizhevsky命名)。AlexNet引入了多种新颖的方面,如随机裁剪和预训练,并确立了均匀和模块化卷积层设计的趋势。这种均匀和模块化的层结构通过反复堆叠这样的模块(卷积层),形成了非常深的CNN,也被称为VGG。
另一种方法是将卷积层的块/模块分支,并将这些分支块堆叠在一起,这对定制的视觉任务非常有效。这个网络被称为GoogLeNet(因为它在Google开发)或Inception v1(其中inception是指这些分支块)。之后出现了VGG和Inception网络的多个变种,如VGG16、VGG19、Inception v2、Inception v3等。
开发的下一阶段始于跳跃连接。为了解决训练CNN时梯度衰减的问题,非连续层通过跳跃连接进行连接,以防信息在它们之间因梯度过小而消散。需要注意的是,跳跃连接本质上是前面讨论的多路径CNN的一种特例。出现了一种流行的网络类型,结合了这种技巧和其他新颖的特性(如批量归一化),称为ResNet。
ResNet的逻辑扩展是DenseNet,其中层之间密集连接,即每一层都从所有前面层的输出特征图中获取输入。此外,随后开发了混合架构,通过结合过去成功的架构,如Inception-ResNet和ResNeXt,其中块内的并行分支数量增加。
最近,通道增强技术被证明对提高CNN性能非常有用。其核心思想是通过迁移学习来学习新特征和利用预学习的特征。最近,自动设计新块和寻找最佳CNN架构已成为CNN研究中的一种增长趋势。这些CNN的例子包括MnasNets和EfficientNets。这些模型的基本思路是进行神经网络架构搜索,以推导出具有均匀模型缩放方法的最佳CNN架构。
在下一节中,我们将回顾其中一个最早的CNN模型,并详细了解自那时以来开发的各种CNN架构。我们将使用PyTorch构建这些架构,并在实际数据集上训练一些模型。我们还将探索PyTorch的预训练CNN模型库,通常称为model-zoo。我们将学习如何微调这些预训练模型以及如何对其进行预测。
从头开发LeNet
LeNet,最初称为LeNet-5,是最早的CNN模型之一,开发于1998年。LeNet-5中的数字5代表该模型的总层数,即两个卷积层和三个全连接层。该模型总共有约60,000个参数,并在1998年的手写数字图像识别任务中表现出了最先进的性能。正如预期的那样,LeNet展示了对旋转、位置和尺度的不变性以及对图像失真的鲁棒性。与当时的经典机器学习模型(如SVMs)不同,SVMs将图像的每个像素单独处理,而LeNet利用了邻近像素之间的相关性。
值得注意的是,尽管LeNet最初是为手写数字识别开发的,但它当然可以扩展到其他图像分类任务,如我们将在下一个练习中看到的。下图展示了LeNet模型的架构:
如前所述,LeNet包含两个卷积层,后跟三个全连接层(包括输出层)。这种将卷积层堆叠在一起,然后连接全连接层的方法,后来成为了CNN研究中的一种常见做法,并且仍然应用于最新的CNN模型。
这是因为,当我们到达最后一个卷积层的输出时,输出具有较小的空间维度(长度和宽度),但深度较高,这使得输出看起来像是输入图像的嵌入。这种嵌入类似于一个可以输入到全连接网络中的向量,而全连接网络本质上就是一系列全连接层。除了这些层之间,还有池化层。这些池化层实际上是降采样层,它们减少了图像表示的空间尺寸,从而减少了参数和计算量,并有效地压缩了输入信息。LeNet中使用的池化层是一个具有可训练权重的平均池化层。很快,最大池化成为了CNN中最常用的池化函数。
图中的每一层括号中的数字表示维度(对于输入、输出和全连接层)或窗口大小(对于卷积层和池化层)。对于灰度图像,期望的输入大小是32x32像素。这个图像首先经过5x5的卷积核处理,然后进行2x2的池化,依此类推。输出层的大小是10,代表10个类别。
在本节中,我们将使用PyTorch从头开始构建LeNet,并在图像数据集上进行训练和评估,以完成图像分类任务。我们将看到使用PyTorch构建网络架构(按照图2.6的轮廓)是多么简单和直观。
此外,我们还将展示LeNet在不同于其最初开发数据集(即MNIST)的数据集上也有多么有效,并且PyTorch如何使训练和测试模型变得简单,只需几行代码。
使用PyTorch构建LeNet
观察以下步骤以构建模型:
对于这个练习,我们需要导入一些依赖项。执行以下导入语句:
import numpy as np
import matplotlib.pyplot as plt
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
torch.use_deterministic_algorithms(True)
除了常规的导入外,我们还调用了use_deterministic_algorithms函数,以确保本练习的可重复性。
接下来,我们将根据图2.6中的轮廓定义模型架构:
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
# 3个输入图像通道,6个输出特征图,5x5卷积核
self.cn1 = nn.Conv2d(3, 6, 5)
# 6个输入图像通道,16个输出特征图,5x5卷积核
self.cn2 = nn.Conv2d(6, 16, 5)
# 全连接层,大小为120、84和10
# 5*5是该层的空间维度
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# 5x5卷积
x = F.relu(self.cn1(x))
# (2, 2)窗口的最大池化
x = F.max_pool2d(x, (2, 2))
# 5x5卷积
x = F.relu(self.cn2(x))
# (2, 2)窗口的最大池化
x = F.max_pool2d(x, (2, 2))
# 将空间和深度维度展平为一个向量
x = x.view(-1, self.flattened_features(x))
# 全连接操作
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def flattened_features(self, x):
# 除了第一个(批量)维度
size = x.size()[1:]
num_feats = 1
for s in size:
num_feats *= s
return num_feats
在最后两行中,我们实例化了模型并打印了网络架构。输出将如下所示:
LeNet(
(conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
模型包括通常的__init__和forward方法用于架构定义和前向传递。额外的flattened_features方法用于计算图像表示层中的特征总数(通常是卷积层或池化层的输出)。该方法将特征的空间表示展平为一个数字向量,然后作为全连接层的输入。
除了之前提到的架构细节外,网络中使用ReLU作为激活函数。此外,与原始LeNet网络(接收单通道图像)不同,当前模型被修改为接受RGB图像,即三通道输入。这是为了适应本练习所使用的数据集。
然后,我们定义了训练例程,即实际的反向传播步骤:
def train(net, trainloader, optim, epoch):
# 初始化损失
loss_total = 0.0
for i, data in enumerate(trainloader, 0):
# 获取输入;data是一个[inputs, labels]的列表
# ip指的是输入图像,ground_truth指的是图像所属的输出类别
ip, ground_truth = data
# 将参数梯度置零
optim.zero_grad()
# 前向传递 + 反向传递 + 优化步骤
op = net(ip)
loss = nn.CrossEntropyLoss()(op, ground_truth)
loss.backward()
optim.step()
# 更新损失
loss_total += loss.item()
# 打印损失统计信息
if (i+1) % 1000 == 0:
# 每1000个小批量打印一次
print('[Epoch number : %d, Mini-batches: %5d] \
loss: %.3f' % (epoch + 1, i + 1,
loss_total / 200))
loss_total = 0.0
对于每个epoch,此函数遍历整个训练数据集,通过网络执行前向传递,并使用反向传播更新模型的参数。经过每1000个训练数据集的小批量,这个方法还记录了计算出的损失。
类似于训练例程,我们将定义用于评估模型性能的测试例程:
def test(net, testloader):
success = 0
counter = 0
with torch.no_grad():
for data in testloader:
im, ground_truth = data
op = net(im)
_, pred = torch.max(op.data, 1)
counter += ground_truth.size(0)
success += (pred == ground_truth).sum().item()
print('LeNet accuracy on 10000 images from test dataset: %d %%'\
% (100 * success / counter))
该函数对每个测试集图像进行前向传递,计算正确的预测数量,并打印测试集上的正确预测百分比。
在训练模型之前,我们需要加载数据集。对于这个练习,我们将使用CIFAR-10数据集。
数据集引用
本节中的图像来自《Learning Multiple Layers of Features from Tiny Images》,Alex Krizhevsky, 2009: www.cs.toronto.edu/~kriz/learn…。它们是CIFAR-10数据集的一部分(toronto.edu):www.cs.toronto.edu/~kriz/cifar…
该数据集包含60,000张32x32的RGB图像,标记为10个类别,每个类别6,000张图像。60,000张图像分为50,000张训练图像和10,000张测试图像。更多细节可以在数据集网站找到 [2]。Torch通过torchvision.datasets模块提供了CIFAR10数据集。我们将使用该模块直接加载数据,并实例化训练和测试数据加载器,如下代码所示:
# 均值和标准差保持为0.5,用于像素值归一化
# 因为像素值最初在0到1范围内
train_transform = transforms.Compose(
[transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32, 4),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR10(root='./data',
train=True, download=True, transform=train_transform)
trainloader = torch.utils.data.DataLoader(trainset,
batch_size=8, shuffle=True)
test_transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))])
testset = torchvision.datasets.CIFAR10(root='./data',
train=False, download=True, transform=test_transform)
testloader = torch.utils.data.DataLoader(testset,
batch_size=10000, shuffle=False)
# 顺序很重要
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog',
'frog', 'horse', 'ship', 'truck')
在下一章中,我们将下载数据集并编写自定义数据集类和数据加载器函数。由于torchvision.datasets模块的存在,我们不需要在这里编写这些内容。
由于我们将下载标志设置为True,数据集将被下载到本地。然后,我们将看到以下输出:
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz
100%
170498071/170498071 [00:34<00:00, 5191345.41it/s]
Extracting ./data/cifar-10-python.tar.gz to ./data
Files already downloaded and verified
用于训练和测试数据集的转换是不同的,因为我们对训练数据集应用了一些数据增强操作,如翻转和裁剪,而这些在测试数据集上不适用。此外,在定义trainloader和testloader之后,我们声明了数据集中10个类别的预定义顺序。
加载数据集后,让我们检查数据的样子:
# 定义一个显示图像的函数
def imageshow(image):
# 反归一化图像
image = image/2 + 0.5
npimage = image.numpy()
plt.imshow(np.transpose(npimage, (1, 2, 0)))
plt.show()
# 从训练集中取样图像
dataiter = iter(trainloader)
images, labels = next(dataiter)
# 将图像显示为网格
num_images = 4
imageshow(torchvision.utils.make_grid(images[:num_images]))
# 打印标签
print(' '+' || '.join(classes[labels[j]]
for j in range(num_images)))
以上代码展示了来自训练数据集的四张示例图像及其对应的标签。输出将如下所示:
训练LeNet
让我们按照以下步骤来训练模型:
我们将定义优化器并开始训练循环,如下所示:
# 定义优化器
optim = torch.optim.Adam(lenet.parameters(), lr=0.001)
# 对数据集进行多次训练循环
for epoch in range(50):
train(lenet, trainloader, optim, epoch)
print()
test(lenet, testloader)
print()
print('Finished Training')
输出将如下所示:
[Epoch number : 1, Mini-batches: 1000] loss: 9.804
[Epoch number : 1, Mini-batches: 2000] loss: 8.783
[Epoch number : 1, Mini-batches: 3000] loss: 8.444
[Epoch number : 1, Mini-batches: 4000] loss: 8.118
[Epoch number : 1, Mini-batches: 5000] loss: 7.819
[Epoch number : 1, Mini-batches: 6000] loss: 7.672
LeNet accuracy on 10000 images from test dataset: 44 %
...
[Epoch number : 50, Mini-batches: 1000] loss: 5.022
[Epoch number : 50, Mini-batches: 2000] loss: 5.067
[Epoch number : 50, Mini-batches: 3000] loss: 5.137
[Epoch number : 50, Mini-batches: 4000] loss: 5.009
[Epoch number : 50, Mini-batches: 5000] loss: 5.107
[Epoch number : 50, Mini-batches: 6000] loss: 4.977
LeNet accuracy on 10000 images from test dataset: 67 %
Finished Training
训练完成后,我们可以将模型文件保存在本地:
model_path = './cifar_model.pth'
torch.save(lenet.state_dict(), model_path)
在训练了LeNet模型之后,我们将在下一节测试它在测试数据集上的性能。
测试LeNet
测试LeNet模型需要遵循以下步骤:
通过加载保存的模型并在测试数据集上运行它来进行预测:
# 加载测试数据集图像
d_iter = iter(testloader)
im, ground_truth = next(d_iter)
# 打印图像和真实标签
imageshow(torchvision.utils.make_grid(im[:4]))
print('Label: ', ' '.join('%5s' %
classes[ground_truth[j]]
for j in range(4)))
# 加载模型
lenet_cached = LeNet()
lenet_cached.load_state_dict(torch.load(model_path))
# 模型推理
op = lenet_cached(im)
# 打印预测结果
_, pred = torch.max(op, 1)
print('Prediction: ', ' '.join('%5s' % classes[pred[j]]
for j in range(4)))
输出将如下所示:
显然,四个预测中有三个是正确的。
最后,我们将检查该模型在测试数据集上的总体准确率以及每个类别的准确率:
success = 0
counter = 0
with torch.no_grad():
for data in testloader:
im, ground_truth = data
op = lenet_cached(im)
_, pred = torch.max(op.data, 1)
counter += ground_truth.size(0)
success += (pred == ground_truth).sum().item()
print('Model accuracy on 10000 images from test dataset: %d %%' % (100 * success / counter))
输出如下:
Model accuracy on 10000 images from test dataset: 67 %
要计算每个类别的准确率,代码如下:
class_success = list(0. for i in range(10))
class_counter = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
im, ground_truth = data
op = lenet_cached(im)
_, pred = torch.max(op, 1)
c = (pred == ground_truth).squeeze()
for i in range(10000):
ground_truth_curr = ground_truth[i]
class_success[ground_truth_curr] += c[i].item()
class_counter[ground_truth_curr] += 1
for i in range(10):
print('Model accuracy for class %5s : %2d %%' % (classes[i], 100 * class_success[i] / class_counter[i]))
输出如下:
Model accuracy for class plane : 70 %
Model accuracy for class car : 83 %
Model accuracy for class bird : 45 %
Model accuracy for class cat : 37 %
Model accuracy for class deer : 80 %
Model accuracy for class dog : 52 %
Model accuracy for class frog : 81 %
Model accuracy for class horse : 71 %
Model accuracy for class ship : 76 %
Model accuracy for class truck : 74 %
某些类别的表现优于其他类别。总体来说,模型的准确率离完美(即100%准确率)还有差距,但比随机预测的模型(准确率为10%,因为有10个类别)要好得多。
在用PyTorch从头构建LeNet模型并评估其性能之后,我们将继续研究LeNet的继任者——AlexNet。对于LeNet,我们从头构建了模型,进行了训练和测试。对于AlexNet,我们将使用一个预训练模型,在一个较小的数据集上进行微调,然后进行测试。
微调AlexNet模型
在本节中,我们将首先快速了解AlexNet的架构以及如何使用PyTorch构建一个AlexNet模型。接着,我们将探索PyTorch的预训练CNN模型库,最后,我们将使用预训练的AlexNet模型在图像分类任务上进行微调,并进行预测。
AlexNet是LeNet的继任者,在架构上进行了增量改进,例如将层数从5增加到8(包括5个卷积层和3个全连接层),模型参数从60,000增加到6,000,000,并使用MaxPool替代AvgPool。此外,AlexNet是在一个规模更大的数据集——ImageNet上进行训练和测试的,该数据集大小超过100 GB,而LeNet训练的数据集MNIST仅有几MB。AlexNet真正地革新了CNN,因为它在图像相关任务上比其他经典的机器学习模型,如支持向量机(SVM),表现出了显著更强的能力。下图展示了AlexNet的架构:
在本节中,我们将首先简要了解AlexNet的架构以及如何使用PyTorch构建一个AlexNet模型。接下来,我们将探索PyTorch的预训练CNN模型库,并最终使用预训练的AlexNet模型进行微调,以解决图像分类任务,并进行预测。
AlexNet是LeNet的继任者,在架构上进行了增量改进,例如将层数从5增加到8(包括5个卷积层和3个全连接层),模型参数从60,000增加到60,000,000,并使用MaxPool替代AvgPool。此外,AlexNet是在一个规模更大的数据集——ImageNet上进行训练和测试的,该数据集大小超过100 GB,而LeNet训练的数据集MNIST仅有几MB。AlexNet真正地革新了CNN,因为它在图像相关任务上比其他经典的机器学习模型,如支持向量机(SVM),表现出了显著更强的能力。下图展示了AlexNet的架构:
如图所示,AlexNet的架构沿用了LeNet的常见主题,即将卷积层按顺序堆叠,然后在输出端跟随一系列全连接层。PyTorch使得将这样的模型架构转化为实际代码变得非常简单。以下是AlexNet架构的PyTorch代码实现:
python
复制代码
class AlexNet(nn.Module):
def __init__(self, number_of_classes):
super(AlexNet, self).__init__()
self.feats = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64,
kernel_size=11, stride=4, padding=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=64, out_channels=192,
kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=192, out_channels=384,
kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=256,
kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=256, out_channels=256,
kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.clf = nn.Linear(in_features=256, out_features=number_of_classes)
def forward(self, inp):
op = self.feats(inp)
op = op.view(op.size(0), -1)
op = self.clf(op)
return op
代码解释如下:__init__函数包含了整个层次结构的初始化,包括卷积层、池化层和全连接层,以及ReLU激活函数。forward函数将数据点通过初始化的网络进行前向传播。请注意,forward方法中的第二行已经执行了展平操作,因此我们不需要像LeNet那样单独定义展平函数。
除了自己初始化模型架构和训练的选项外,PyTorch通过其torchvision包提供了一个models子包,其中包含了用于解决不同任务的CNN模型定义,如图像分类、语义分割、目标检测等。以下是图像分类任务中可用模型的一部分:
- AlexNet
- VGG
- ResNet
- SqueezeNet
- DenseNet
- Inception v3
- GoogLeNet
- ShuffleNet v2
- MobileNet v2
- ResNeXt
- Wide ResNet
- MnasNet
- EfficientNet
在下一节中,我们将以预训练的AlexNet模型为例,演示如何使用PyTorch对其进行微调。
使用PyTorch微调AlexNet
在以下练习中,我们将加载一个预训练的AlexNet模型,并在不同于ImageNet的数据集上进行微调,最后测试微调后的模型性能,看看它是否能够从新数据集中迁移学习。为了提高代码的可读性,练习中的部分代码经过了简化,但完整代码可以在我们的GitHub库中找到。
在进行练习之前,我们需要导入一些依赖项。执行以下导入语句:
import os
import time
import copy
import numpy as np
import matplotlib.pyplot as plt
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torchvision import datasets, models, transforms
torch.use_deterministic_algorithms(True)
接下来,我们将下载并转换数据集。对于本次微调练习,我们将使用一个小型的蜜蜂和蚂蚁图像数据集。数据集中共有240张训练图像和150张验证图像,均匀分布于两个类别(蜜蜂和蚂蚁)之间。
我们从Kaggle下载数据集,并将其存储在当前工作目录中。有关数据集的更多信息,可以在数据集网站上找到。
数据集引用
Elsik, C. G., Tayal, A., Diesh, C. M., Unni, D. R., Emery, M. L., Nguyen, H. N., Hagen, D. E. Hymenoptera Genome Database: integrating genome annotations in HymenopteraMine. Nucleic Acids Research, 2016, Jan. 4; 44(D1)
. DOI: 10.1093/nar/gkv1208. Epub 2015, Nov. 17. PubMed PMID: 26578564.
下载数据集之前,您需要登录Kaggle。如果您还没有Kaggle账号,需要进行注册。接下来,下载并转换数据集:
# 创建本地数据目录
ddir = 'hymenoptera_data'
# 数据标准化和增强变换
data_transformers = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.490, 0.449, 0.411], [0.231, 0.221, 0.230])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.490, 0.449, 0.411], [0.231, 0.221, 0.230])
])
}
img_data = {k: datasets.ImageFolder(os.path.join(ddir, k), data_transformers[k])
for k in ['train', 'val']}
dloaders = {k: torch.utils.data.DataLoader(img_data[k], batch_size=8, shuffle=True)
for k in ['train', 'val']}
dset_sizes = {x: len(img_data[x]) for x in ['train', 'val']}
classes = img_data['train'].classes
dvc = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
现在我们已经完成了前期准备工作,接下来开始:
让我们可视化一些训练数据集的样本图像:
def imageshow(img, text=None):
img = img.numpy().transpose((1, 2, 0))
avg = np.array([0.490, 0.449, 0.411])
stddev = np.array([0.231, 0.221, 0.230])
img = stddev * img + avg
img = np.clip(img, 0, 1)
plt.imshow(img)
if text is not None:
plt.title(text)
# 生成一个训练数据集批次
imgs, cls = next(iter(dloaders['train']))
# 从批次生成网格
grid = torchvision.utils.make_grid(imgs)
imageshow(grid, text=[classes[c] for c in cls])
我们使用了numpy的np.clip()方法来确保图像像素值限制在0到1之间,以便更清晰地进行可视化。输出将如下所示:
我们现在定义微调例程,这实际上是在预训练模型上进行的一种训练流程:
def finetune_model(pretrained_model, loss_func, optim, epochs=10):
...
for e in range(epochs):
for dset in ['train', 'val']:
if dset == 'train':
# 将模型设置为训练模式
# (即可训练权重)
pretrained_model.train()
else:
# 将模型设置为验证模式
pretrained_model.eval()
# 遍历(训练/验证)数据
for imgs, tgts in dloaders[dset]:
...
optim.zero_grad()
with torch.set_grad_enabled(dset == 'train'):
ops = pretrained_model(imgs)
_, preds = torch.max(ops, 1)
loss_curr = loss_func(ops, tgts)
# 仅在训练模式下进行反向传播
if dset == 'train':
loss_curr.backward()
optim.step()
loss += loss_curr.item() * imgs.size(0)
successes += torch.sum(preds == tgts.data)
loss_epoch = loss / dset_sizes[dset]
accuracy_epoch = successes.double() / dset_sizes[dset]
if dset == 'val' and accuracy_epoch > accuracy:
accuracy = accuracy_epoch
model_weights = copy.deepcopy(
pretrained_model.state_dict())
# 加载最佳模型版本(权重)
pretrained_model.load_state_dict(model_weights)
return pretrained_model
在这个函数中,我们需要传入预训练模型(即架构和权重)、损失函数、优化器以及训练的轮数。基本上,我们不是从随机初始化的权重开始,而是使用AlexNet的预训练权重。函数的其他部分与我们之前的练习相似。
在开始微调(训练)模型之前,我们将定义一个函数来可视化模型的预测结果:
def visualize_predictions(pretrained_model, max_num_imgs=4):
was_model_training = pretrained_model.training
pretrained_model.eval()
imgs_counter = 0
fig = plt.figure()
with torch.no_grad():
for i, (imgs, tgts) in enumerate(dloaders['val']):
imgs = imgs.to(dvc)
tgts = tgts.to(dvc)
ops = pretrained_model(imgs)
_, preds = torch.max(ops, 1)
for j in range(imgs.size()[0]):
imgs_counter += 1
ax = plt.subplot(max_num_imgs // 2, 2, imgs_counter)
ax.axis('off')
ax.set_title(f'Prediction: {classes[preds[j]]}, Ground Truth: {classes[tgts[j]]}')
imageshow(imgs.cpu().data[j])
if imgs_counter == max_num_imgs:
pretrained_model.train(mode=was_model_training)
return
pretrained_model.train(mode=was_model_training)
最后,我们进入有趣的部分。让我们使用PyTorch的torchvision.models子包来加载预训练的AlexNet模型:
model_finetune = models.alexnet(pretrained=True)
这个模型对象有两个主要组成部分:
features:特征提取部分,包含所有的卷积层和池化层。classifier:分类器块,包含所有的全连接层,通向输出层。
我们可以如下可视化这些组成部分:
print(model_finetune.features)
这应该输出如下内容:
Sequential(
(0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
(1): ReLU(inplace=True)
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU(inplace=True)
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU(inplace=True)
(8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU(inplace=True)
(10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
接下来,我们检查分类器块:
print(model_finetune.classifier)
这应该输出如下内容:
Sequential(
(0): Dropout(p=0.5, inplace=False)
(1): Linear(in_features=9216, out_features=4096, bias=True)
(2): ReLU(inplace=True)
(3): Dropout(p=0.5, inplace=False)
(4): Linear(in_features=4096, out_features=4096, bias=True)
(5): ReLU(inplace=True)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
正如你所注意到的,预训练模型的输出层大小为1000,但我们在微调数据集中只有2个类别。因此,我们需要更改输出层,如下所示:
# 将最后一层从1000类更改为2类
model_finetune.classifier[6] = nn.Linear(4096, len(classes))
现在,我们已经准备好定义优化器和损失函数,并运行训练例程:
loss_func = nn.CrossEntropyLoss()
optim_finetune = optim.SGD(model_finetune.parameters(), lr=0.0001)
# 训练(微调)和验证模型
model_finetune = finetune_model(model_finetune, loss_func, optim_finetune, epochs=10)
输出将如下所示:
Epoch number 0/9
====================
train loss in this epoch: 0.6528244360548551, accuracy in this epoch: 0.610655737704918
val loss in this epoch: 0.5563900120118085, accuracy in this epoch: 0.7320261437908496
Epoch number 1/9
====================
train loss in this epoch: 0.5144887796190919, accuracy in this epoch: 0.75
val loss in this epoch: 0.4758027388769038, accuracy in this epoch: 0.803921568627451
Epoch number 2/9
====================
train loss in this epoch: 0.4620713156754853, accuracy in this epoch: 0.7950819672131147
val loss in this epoch: 0.4326762077855129, accuracy in this epoch: 0.803921568627451
...
Epoch number 7/9
====================
train loss in this epoch: 0.3297723409582357, accuracy in this epoch: 0.860655737704918
val loss in this epoch: 0.3347476099441254, accuracy in this epoch: 0.869281045751634
Epoch number 8/9
====================
train loss in this epoch: 0.32671376110100353, accuracy in this epoch: 0.8524590163934426
val loss in this epoch: 0.32516936344258923, accuracy in this epoch: 0.8823529411764706
Epoch number 9/9
====================
train loss in this epoch: 0.3130935803055763, accuracy in this epoch: 0.8770491803278688
val loss in this epoch: 0.3200583465251268, accuracy in this epoch: 0.8888888888888888
Training finished in 5.0mins 50.6720712184906secs
Best validation set accuracy: 0.8888888888888888
让我们可视化一些模型预测,以查看模型是否确实从这个小数据集中学习到了相关特征:
visualize_predictions(model_finetune)
这将输出如下:
显然,预训练的AlexNet模型在这个相当小的图像分类数据集上成功进行了迁移学习。这既展示了迁移学习的强大能力,也表明了使用PyTorch微调知名模型的速度和简便性。
在下一节中,我们将讨论AlexNet的一个更深、更复杂的继任者——VGG网络。我们已经详细展示了LeNet和AlexNet的模型定义、数据集加载、模型训练(或微调)和评估步骤。在接下来的部分,我们将主要关注模型架构定义,因为其他方面(如数据加载和评估)的PyTorch代码将会类似。
运行预训练的VGG模型
我们已经讨论了LeNet和AlexNet,这两种是基础的卷积神经网络(CNN)架构。随着章节的进展,我们将探索越来越复杂的CNN模型。尽管如此,构建这些模型架构的关键原则将保持一致。我们将看到一种模块化的模型构建方法,将卷积层、池化层和全连接层组合成块/模块,然后顺序或分支地堆叠这些块。在本节中,我们将介绍AlexNet的继任者——VGGNet。
VGG的名字源自于其发源地——牛津大学的视觉几何组(Visual Geometry Group)。与拥有8层和6000万参数的AlexNet相比,VGG包含13层(10层卷积层和3层全连接层)和1.38亿参数。VGG基本上是在AlexNet架构的基础上堆叠了更多的层,使用了更小的卷积核(2x2或3x3)。
因此,VGG的创新之处在于其架构所带来的前所未有的深度。图2.12展示了VGG的架构:
前面的VGG架构称为VGG13,因为它包含13层。其他变体包括VGG16和VGG19,分别包含16层和19层。还有一组变体——VGG13_bn、VGG16_bn和VGG19_bn,其中“bn”表示这些模型还包含批量归一化(batch normalization)层。
PyTorch的torchvision.models子包提供了预训练的VGG模型(包括前述的六种变体),这些模型是在ImageNet数据集上训练的。在以下练习中,我们将使用预训练的VGG13模型对一个小型的蜜蜂和蚂蚁数据集(在前面的练习中使用过)进行预测。这里我们将重点关注代码的关键部分,因为其他部分的代码与之前的练习大多重叠。我们可以随时参考我们的笔记本以查看完整代码 [7]。
首先,我们需要导入依赖项,包括torchvision.models。
下载数据并设置蜜蜂和蚂蚁数据集及数据加载器,同时进行必要的图像转换。
为了对这些图像进行预测,我们需要下载ImageNet数据集的1000个标签 [8]。下载后,我们需要创建从类索引0到999与对应的类标签之间的映射,如下所示:
import ast
with open('./imagenet1000_clsidx_to_labels.txt') as f:
classes_data = f.read()
classes_dict = ast.literal_eval(classes_data)
print({k: classes_dict[k] for k in list(classes_dict)[:5]})
这应该输出前五个类的映射,如下所示:
{0: 'tench, Tinca tinca', 1: 'goldfish, Carassius auratus', 2: 'great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias', 3: 'tiger shark, Galeocerdo cuvieri', 4: 'hammerhead, hammerhead shark'}
定义模型预测可视化函数,该函数接受预训练的模型对象和要运行预测的图像数量。此函数应输出带有预测结果的图像。
加载预训练的VGG13模型:
model_finetune = models.vgg13(pretrained=True)
VGG13模型将在此步骤中下载。
常见问题解答:VGG13模型的磁盘大小是多少?
一个VGG13模型大约需要508 MB的硬盘空间。
最后,我们使用这个预训练模型对蜜蜂和蚂蚁数据集进行预测:
visualize_predictions(model_finetune)
这应该输出如下结果:
训练在完全不同数据集上的VGG13模型似乎能够正确预测蜜蜂和蚂蚁数据集中的所有测试样本。基本上,模型从1000个类别中选择最相似的两个动物,并在图像中找到它们。通过这次练习,我们看到模型仍能从图像中提取相关的视觉特征,这也展示了PyTorch开箱即用的推理功能的实用性。
在接下来的部分,我们将研究另一种类型的CNN架构——涉及多个并行卷积层的模块。这些模块被称为Inception模块,整个网络被称为Inception网络——以电影《盗梦空间》(Inception)命名——因为这个模型包含了多个分支模块,就像电影中的分支梦境一样。我们将探讨这个网络的各个部分及其成功的原因,并使用PyTorch构建Inception模块和Inception网络架构。
探索GoogLeNet和Inception v3
到目前为止,我们已经发现了CNN模型从LeNet到VGG的发展过程,我们观察到更多卷积层和全连接层的顺序堆叠。这导致了具有大量参数的深度网络。GoogLeNet作为一种截然不同的CNN架构出现,其由一个叫做inception模块的并行卷积层模块组成。因此,GoogLeNet也被称为Inception v1(v1标志着第一个版本,因为后来出现了更多版本)。GoogLeNet引入了一些显著的新元素,包括:
- Inception模块——一个由多个并行卷积层组成的模块
- 使用1x1卷积来减少模型参数的数量
- 全局平均池化代替全连接层——减少过拟合
- 使用辅助分类器进行训练——用于正则化和梯度稳定性
GoogLeNet有22层,比任何VGG模型变体的层数都多。然而,由于使用了一些优化技巧,GoogLeNet的参数数量为500万,远低于VGG的1.38亿个参数。接下来我们将详细扩展这个模型的一些关键特性。
Inception模块
也许这个模型最重要的贡献就是开发了一个卷积模块,该模块包含多个并行运行的卷积层,最终将这些卷积层的输出拼接成一个单一的输出向量。这些并行的卷积层使用不同的卷积核大小,从1x1到3x3,再到5x5。其目的是从图像中提取所有层次的视觉信息。除了这些卷积操作之外,3x3的最大池化层还增加了另一个特征提取层级。
图2.14展示了Inception块的示意图及GoogLeNet的整体架构:
使用此架构图,我们可以在PyTorch中构建Inception模块,如下所示:
class InceptionModule(nn.Module):
def __init__(self, input_planes, n_channels1x1, n_channels3x3red,
n_channels3x3, n_channels5x5red, n_channels5x5,
pooling_planes):
super(InceptionModule, self).__init__()
# 1x1 卷积分支
self.block1 = nn.Sequential(
nn.Conv2d(input_planes, n_channels1x1, kernel_size=1),
nn.BatchNorm2d(n_channels1x1),
nn.ReLU(True),
)
# 1x1 卷积 -> 3x3 卷积分支
self.block2 = nn.Sequential(
nn.Conv2d(input_planes, n_channels3x3red, kernel_size=1),
nn.BatchNorm2d(n_channels3x3red),
nn.ReLU(True),
nn.Conv2d(n_channels3x3red, n_channels3x3, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels3x3),
nn.ReLU(True),
)
# 1x1 卷积 -> 5x5 卷积分支
self.block3 = nn.Sequential(
nn.Conv2d(input_planes, n_channels5x5red, kernel_size=1),
nn.BatchNorm2d(n_channels5x5red),
nn.ReLU(True),
nn.Conv2d(n_channels5x5red, n_channels5x5, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels5x5),
nn.ReLU(True),
nn.Conv2d(n_channels5x5, n_channels5x5, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels5x5),
nn.ReLU(True),
)
# 3x3 池化 -> 1x1 卷积分支
self.block4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(input_planes, pooling_planes, kernel_size=1),
nn.BatchNorm2d(pooling_planes),
nn.ReLU(True),
)
def forward(self, ip):
op1 = self.block1(ip)
op2 = self.block2(ip)
op3 = self.block3(ip)
op4 = self.block4(ip)
return torch.cat([op1, op2, op3, op4], 1)
接下来,我们将关注GoogLeNet的另一个重要特性——1x1卷积。
1x1 卷积
除了Inception模块中的并行卷积层,每个并行层之前都有一个1x1卷积层。使用这些1x1卷积层的原因是为了进行维度缩减。1x1卷积不会改变图像表示的宽度和高度,但可以改变图像表示的深度。这一技巧用于在执行1x1、3x3和5x5卷积之前,减少输入视觉特征的深度。减少参数的数量不仅有助于构建更轻的模型,还有助于防止过拟合。
全局平均池化
如果我们查看图2.14中的GoogLeNet整体架构,模型的倒数第二个输出层前面是一个7x7的平均池化层。这个层再次帮助减少模型的参数数量,从而减少过拟合。如果没有这个层,由于全连接层的密集连接,模型将有数百万额外的参数。
辅助分类器
图2.14还展示了模型中的两个额外或辅助输出分支。这些辅助分类器旨在通过在反向传播过程中增加梯度的大小来应对梯度消失问题,特别是对于靠近输入端的层。由于这些模型有大量的层,梯度消失可能成为一个主要限制。因此,使用辅助分类器对这个22层深的模型证明是有用的。此外,辅助分支还有助于正则化。请注意,在进行预测时,这些辅助分支会被关闭或丢弃。
一旦我们定义了Inception模块,我们可以轻松实例化整个Inception v1模型,如下所示:
class GoogLeNet(nn.Module):
def __init__(self):
super(GoogLeNet, self).__init__()
self.stem = nn.Sequential(
nn.Conv2d(3, 192, kernel_size=3, padding=1),
nn.BatchNorm2d(192),
nn.ReLU(True),
)
self.im1 = InceptionModule(192, 64, 96, 128, 16, 32, 32)
self.im2 = InceptionModule(256, 128, 128, 192, 32, 96, 64)
self.max_pool = nn.MaxPool2d(3, stride=2, padding=1)
self.im3 = InceptionModule(480, 192, 96, 208, 16, 48, 64)
self.im4 = InceptionModule(512, 160, 112, 224, 24, 64, 64)
self.im5 = InceptionModule(512, 128, 128, 256, 24, 64, 64)
self.im6 = InceptionModule(512, 112, 144, 288, 32, 64, 64)
self.im7 = InceptionModule(528, 256, 160, 320, 32, 128, 128)
self.im8 = InceptionModule(832, 256, 160, 320, 32, 128, 128)
self.im9 = InceptionModule(832, 384, 192, 384, 48, 128, 128)
self.average_pool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(4096, 1000)
def forward(self, ip):
op = self.stem(ip)
out = self.im1(op)
out = self.im2(op)
op = self.max_pool(op)
op = self.im3(op)
op = self.im4(op)
op = self.im5(op)
op = self.im6(op)
op = self.im7(op)
op = self.im8(op)
op = self.im9(op)
op = self.average_pool(op)
op = op.view(op.size(0), -1)
op = self.fc(op)
return op
除了实例化自己的模型外,我们还可以用以下两行代码加载预训练的GoogLeNet模型:
import torchvision.models as models
model = models.googlenet(pretrained=True)
最后,如前所述,后来开发了多个版本的Inception模型。其中一个著名的版本是Inception v3,我们将简要讨论。
从架构上可以看出,这个模型是Inception v1模型的架构扩展。同样,除了手动构建模型外,我们还可以使用PyTorch库中的预训练模型,如下所示:
python
复制代码
import torchvision.models as models
model = models.inception_v3(pretrained=True)
在下一节中,我们将讨论在非常深的CNN中有效解决梯度消失问题的CNN模型类别——ResNet和DenseNet。我们将学习跳跃连接和密集连接的新技术,并使用PyTorch编写这些高级架构的基本模块代码。
讨论ResNet和DenseNet架构
在上一节中,我们探讨了Inception模型,这些模型在层数增加的情况下,通过1x1卷积和全局平均池化减少了模型参数的数量。此外,还使用了辅助分类器来解决梯度消失问题。在本节中,我们将讨论ResNet和DenseNet模型。
ResNet
ResNet引入了跳跃连接的概念。这一简单而有效的技巧克服了参数溢出和梯度消失的问题。其基本思想如以下图所示,非常简单。输入首先经过非线性变换(卷积后接非线性激活函数),然后将这种变换的输出(称为残差)加到原始输入上。
每个这样的计算块称为残差块,因此模型的名称为残差网络(Residual Network)或ResNet:
通过使用这些跳跃(或快捷)连接,ResNet-50的参数数量被限制在2600万,网络总层数为50层。由于参数数量有限,即使在层数增加到152层(ResNet-152)时,ResNet仍能够很好地进行泛化而不发生过拟合。下图展示了ResNet-50的架构:
有两种类型的残差块——卷积残差块和恒等残差块,它们都有跳跃连接。对于卷积残差块,添加了一个1x1卷积层,进一步有助于减少维度。一个用于ResNet的残差块可以在PyTorch中实现如下:
python
复制代码
class BasicBlock(nn.Module):
multiplier = 1
def __init__(self, input_num_planes, num_planes, stride=1):
super(BasicBlock, self).__init__()
self.conv_layer1 = nn.Conv2d(in_channels=input_num_planes,
out_channels=num_planes,
kernel_size=3,
stride=stride, padding=1,
bias=False)
self.batch_norm1 = nn.BatchNorm2d(num_planes)
self.conv_layer2 = nn.Conv2d(in_channels=num_planes,
out_channels=num_planes,
kernel_size=3, stride=1,
padding=1, bias=False)
self.batch_norm2 = nn.BatchNorm2d(num_planes)
self.res_connection = nn.Sequential()
if stride > 1 or input_num_planes != self.multiplier * num_planes:
self.res_connection = nn.Sequential(
nn.Conv2d(in_channels=input_num_planes,
out_channels=self.multiplier * num_planes,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.multiplier * num_planes))
def forward(self, inp):
op = F.relu(self.batch_norm1(self.conv_layer1(inp)))
op = self.batch_norm2(self.conv_layer2(op))
op += self.res_connection(inp)
op = F.relu(op)
return op
要快速开始使用ResNet,我们可以从PyTorch的模型库中加载预训练的ResNet模型:
python
复制代码
import torchvision.models as models
model = models.resnet50(pretrained=True)
ResNet使用恒等函数(通过直接连接输入和输出)来在反向传播过程中保持梯度(因为梯度为1)。然而,对于极深的网络,这一原则可能不足以保持从输出层到输入层的强梯度。
接下来我们讨论的CNN模型旨在确保强梯度流,同时进一步减少所需的参数数量。
DenseNet
ResNet的跳跃连接直接将残差块的输入连接到其输出。然而,残差块之间的连接仍然是顺序的;即,残差块3与块2有直接连接,但与块1没有直接连接。
DenseNet(密集网络)引入了将每个卷积层与所谓的密集块内的每个其他层连接的概念。每个密集块也与整体DenseNet中的其他密集块相连接。密集块实际上是由两个3x3的密集连接卷积层组成的模块。
这些密集连接确保每个层从网络中的所有前置层接收信息。这确保了从最后一层到第一层的强梯度流。与直觉相反,这种网络设置的参数数量也会较低。由于每一层接收来自所有前面层的特征图,所需的通道数(深度)可以减少。在早期模型中,增加的深度代表了从早期层积累的信息,但由于网络中处处都有密集连接,我们不再需要这些积累。
ResNet和DenseNet之间的一个关键区别在于,ResNet中输入通过跳跃连接添加到输出中。然而,在DenseNet中,前面的层的输出与当前层的输出进行拼接。拼接发生在深度维度上。
这可能会引发一个问题,即随着网络的进一步推进,输出大小的爆炸效应。为了解决这一问题,为这个网络设计了一种特殊的块,称为过渡块。该块由一个1x1卷积层和一个2x2池化层组成,用于标准化或重置深度维度的大小,以便可以将此块的输出馈送到后续的密集块中。
下图展示了DenseNet的架构:
正如前面提到的,DenseNet涉及两种类型的块——密集块和过渡块。这些块可以在PyTorch中用几行代码编写,如下所示:
class DenseBlock(nn.Module):
def __init__(self, input_num_planes, rate_inc):
super(DenseBlock, self).__init__()
self.batch_norm1 = nn.BatchNorm2d(input_num_planes)
self.conv_layer1 = nn.Conv2d(in_channels=input_num_planes,
out_channels=4*rate_inc,
kernel_size=1, bias=False)
self.batch_norm2 = nn.BatchNorm2d(4*rate_inc)
self.conv_layer2 = nn.Conv2d(in_channels=4*rate_inc,
out_channels=rate_inc,
kernel_size=3, padding=1,
bias=False)
def forward(self, inp):
op = self.conv_layer1(F.relu(self.batch_norm1(inp)))
op = self.conv_layer2(F.relu(self.batch_norm2(op)))
op = torch.cat([op, inp], 1)
return op
class TransBlock(nn.Module):
def __init__(self, input_num_planes, output_num_planes):
super(TransBlock, self).__init__()
self.batch_norm = nn.BatchNorm2d(input_num_planes)
self.conv_layer = nn.Conv2d(in_channels=input_num_planes,
out_channels=output_num_planes,
kernel_size=1, bias=False)
def forward(self, inp):
op = self.conv_layer(F.relu(self.batch_norm(inp)))
op = F.avg_pool2d(op, 2)
return op
这些块会被密集地堆叠在一起,形成整体的DenseNet架构。与ResNet类似,DenseNet也有多个变体,如DenseNet121、DenseNet161、DenseNet169和DenseNet201,其中的数字表示总层数。这样的大量层数是通过重复堆叠密集块和过渡块以及在输入端固定的7x7卷积层和输出端固定的全连接层获得的。PyTorch为所有这些变体提供了预训练模型:
import torchvision.models as models
densenet121 = models.densenet121(pretrained=True)
densenet161 = models.densenet161(pretrained=True)
densenet169 = models.densenet169(pretrained=True)
densenet201 = models.densenet201(pretrained=True)
DenseNet在ImageNet数据集上的表现优于目前讨论的所有模型。通过混合和匹配前面各节中提出的概念,已经开发出了各种混合模型。Inception-ResNet和ResNeXt模型就是这样的混合网络的例子。下图展示了ResNeXt架构:
从架构图中可以看出,这种模型看起来像是ResNet和Inception混合体的一个更宽的变体,因为在残差块中有大量并行的卷积分支,而并行的思想源自于Inception网络。
在本章的最后一部分,我们将介绍一种迄今为止表现最好的CNN架构——EfficientNets。我们还将讨论CNN架构的未来发展,同时触及CNN架构在图像分类之外任务中的应用。
理解EfficientNets及CNN架构的未来
在我们从LeNet到DenseNet的探索中,我们注意到CNN架构进展的一个潜在主题:通过以下任一方式扩展或缩放CNN模型:
- 增加层数
- 增加卷积层中的特征图或通道数量
- 增加空间维度,例如从LeNet中的32x32像素图像到AlexNet中的224x224像素图像等
这三种不同的缩放方面分别被称为深度、宽度和分辨率。EfficientNets通过神经架构搜索来计算每个方面的最佳缩放因子,而不是手动缩放这些属性,这往往会导致次优结果。
扩展深度被认为很重要,因为网络越深,模型越复杂,因此能够学习高度复杂的特征。然而,随着深度的增加,梯度消失问题和过拟合问题也会加剧。
同样,扩展宽度理论上应有帮助,因为更多的通道意味着网络可以学习更细粒度的特征。然而,对于极宽的模型,准确率往往会迅速饱和。
最后,理论上更高分辨率的图像应该效果更好,因为它们包含更多细粒度的信息。然而,实际上,分辨率的增加并不会线性地提高模型性能。这些都表明,在决定缩放因子时需要权衡,因此神经架构搜索帮助找到最佳的缩放因子。
EfficientNet提出了找到在深度、宽度和分辨率之间具有适当平衡的架构,这三个方面通过一个全局缩放因子一起缩放。EfficientNet架构分为两步构建。首先,通过将缩放因子固定为1来设计一个基本架构(称为基础网络)。在这个阶段,根据任务和数据集决定深度、宽度和分辨率的相对重要性。得到的基础网络与一个著名的CNN架构——MnasNet(即Mobile Neural Architecture Search Network)相似。
PyTorch提供了预训练的MnasNet模型,可以如下加载:
import torchvision.models as models
model = models.mnasnet1_0()
在获得基础网络后,接下来计算最佳的全局缩放因子,以最大化模型的准确性并最小化计算量(或flops)。基础网络称为EfficientNet B0,后续的网络根据不同的最佳缩放因子称为EfficientNet B1-B7。PyTorch为这些变体提供了预训练模型:
import torchvision.models as models
efficientnet_b0 = models.efficientnet_b0(pretrained=True)
efficientnet_b1 = models.efficientnet_b1(pretrained=True)
...
efficientnet_b7 = models.efficientnet_b7(pretrained=True)
未来,CNN架构的有效缩放将成为研究的一个重要方向,同时还会开发更多受Inception、Residual和Dense模块启发的复杂模块。CNN架构开发的另一个方面是最小化模型大小,同时保持性能。MobileNets是一个很好的例子,相关研究也在不断进行。
除了从现有模型的架构修改的自上而下方法,还会继续努力采用从根本上重新思考CNN单元(如卷积核、池化机制、扁平化方式等)的自下而上的方法。一个具体的例子是CapsuleNet,它对卷积单元进行了改造,以适应图像中的第三维(深度)。
CNN本身是一个巨大的研究主题。在本章中,我们主要讨论了CNN的架构发展,主要是在图像分类的背景下。然而,这些相同的架构也被广泛应用于各种任务。一个著名的例子是使用ResNets进行物体检测和分割,形式为RCNNs。
RCNN的改进变体包括Faster R-CNN、Mask-RCNN和Keypoint-RCNN。PyTorch提供了这三种变体的预训练模型:
faster_rcnn = models.detection.fasterrcnn_resnet50_fpn()
mask_rcnn = models.detection.maskrcnn_resnet50_fpn()
keypoint_rcnn = models.detection.keypointrcnn_resnet50_fpn()
PyTorch还提供了应用于视频相关任务的ResNet预训练模型,如视频分类。用于视频分类的两个ResNet基础模型是ResNet3D和ResNet Mixed Convolution:
resnet_3d = models.video.r3d_18()
resnet_mixed_conv = models.video.mc3_18()
尽管本章没有详细介绍这些不同的应用和相应的CNN模型,但我们鼓励你深入阅读。PyTorch的网站可以作为一个良好的起点。
总结
本章介绍了CNN架构。下一章,我们将介绍另一种类型的神经网络模型——递归神经网络。我们将通过将CNN与LSTM(递归神经网络的一种)结合,构建一个端到端的深度学习应用——图像标题生成器。