课程5. 迁移学习
课程计划:
- 流行的神经网络架构
- 迁移学习
卷积神经网络架构
在上一课中,我们学习了卷积神经网络的运行原理,它非常适合处理图像。在本课中,我们将熟悉流行的卷积神经网络架构。
首先,简单说一下为什么我们需要分析特定的卷积网络架构。
当研究人员想要使用神经网络解决问题时,他们通常不会从头编写自己的神经网络,而是采用标准架构之一,例如 ResNet 或 DenseNet,并对其进行训练以完成任务。这非常方便:这些架构已经证明它们可以有效地解决分类问题,并且您无需自己设置架构(层数、层中的过滤器数量、将池化放在何处等),而是可以采用现成的成功架构(例如 ResNet)并对其进行训练。
选择哪种架构取决于问题的类型、问题的难度以及您拥有的训练数据的数量。数据越少,任务越简单,需要使用的网络就越轻,深度越浅,以免过度拟合。例如,最简单的AlexNet网络适合对MNIST或CIFAR图像进行分类。如果您的任务很复杂,即您拥有复杂不同形状和许多不同类别的图像,例如在 ImageNet 中,那么您将需要一个具有大量层的神经网络。
此外,不同的架构使用不同的附加技巧来提高网络效率。了解它们对于为你的任务选择正确的架构很有用。
在我们开始讨论神经网络架构之前,让我们先讨论一下 ImageNet。
ImageNet
ImageNet 是一个分为大量类别的图像数据库。也就是说,这是一个可以解决图像分类问题的数据集。 ImageNet 有两个版本:
- 旧版本:1,000 个类别,1,431,167 张图像(1,281,167/50,000/100,000 个训练/验证/测试)
- 新版本:21,000 个类别,14,197,122 张图片
2010年启动了基于ImageNet-1k版本的图像分类竞赛。参与者必须在 ImageNet 的训练集上训练机器学习模型,并在测试集上获得预测。使用准确度指标来评估解决方案。
比赛时间表:
神经网络架构
让我们简要讨论一下时间线上的架构(以及一些不在时间线上的架构)
- NEC-UIUC、XRCE 是非神经模型。它们基于经典机器学习的思想;
- AlexNet 是第一个在 ImageNet 上表现优异的卷积神经网络。这一刻被认为是深度学习的一次革命:人们相信神经网络可以成功地用于处理图像。 AlexNet 的架构相当简单,由我们已知的元素组成:conv、relu、fc、bachnorm、dropout;
- ZFNet、VGG — 也是常规卷积神经网络,但比 AlexNet 更深;
VGG 优于 AlexNet,很大程度上归功于 VGG 拥有更多的卷积层。这是有道理的:更多的层数以及更多的网络参数使得模型能够更好地适应数据中的复杂依赖关系。而在VGG成功之后,人们开始尝试进一步增加网络的层数,希望神经网络能够发挥更好的作用。
但事实证明事情并非如此简单:如果神经网络的层数太多,网络的学习效果就会开始变差。出现梯度衰减问题。
我们不会详细讲解梯度衰减问题的本质,以及解决该问题的所有方法,但会提到一种方法:跳跃连接(skip connection)。
跳过连接是在网络的非连续层之间添加额外的连接。
在上图的示例中,从网络的第一层到第四层添加了额外的连接。第四层将第 1 层和第 3 层的激活图的总和或连接作为输入。
在网络中添加跳过连接可以创建具有大量层的神经网络:
- ResNet:卷积网络 + 跳过连接(激活图的总和);
- DenseNet:卷积网络 + 跳过连接(激活图的连接);
以下是 ResNet-34(具有 34 层的 ResNet 版本)的示意图:
除了跳过连接之外,还有许多技巧可以提高神经网络的效率。其中一些技巧是针对特定任务的,而其他技巧则旨在改善神经网络的整体图像处理过程。
其他架构的示例:
- Inception(GoogLeNet);
- MobileNet:专为移动设备使用而定制的轻量级架构;
- ResNeXt、ShuffleNet、…
torchvision 中可用的架构可以在这里找到。
许多架构都有版本。例如:
- VGG-13, -16, -19;
-ResNet-18, -34, -50, 154, …
版本之间的区别主要在于层数(以及参数)。
实践
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import datasets, models, transforms
从 torchvision 加载模型
# resnet-18 模型
model = models.resnet18(pretrained=True)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
为了最有效地使用预先训练的 resnet18 网络,我们需要以某种方式预处理作为输入的所有图像。 resnet 系列的所有网络都有自己固定的变换:
resnet_transforms = transforms.Compose([
transforms.Resize(256), # 每幅图像的尺寸将缩小到 256*256
transforms.CenterCrop(224), # 图片的中心部分将被剪掉,尺寸为 224*224
transforms.ToTensor(), # 图像从python数组转换为torch.Tensor格式
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 图像像素值归一化
])
# 你不必自己编写转换,你可以从模型中获取它们:
# resnet_transforms = models.ResNet18_Weights.IMAGENET1K_V1.transforms()
在一个图像上测试预先训练的网络
我们从互联网上下载一张狗的图片:
! wget "https://ichef.bbci.co.uk/news/640/cpsprodpb/475B/production/_98776281_gettyimages-521697453.jpg" -O "doggie.jpg"
让我们来看看图:
from PIL import Image
image = Image.open('doggie.jpg')
image
np.array(image).shape
输出:(360, 640, 3)
让我们对图像进行变换,看看变换之后它会是什么样子:
image_transformed = resnet_transforms(image)
print(image_transformed.shape)
输出:torch.Size([3, 224, 224])
plt.imshow(image_transformed.permute(1,2,0).data.cpu().numpy())
我们将图像输入到网络输入并得到答案:
model.eval()
with torch.no_grad():
# image_transformed -> [image_transformed]
model_output = model(image_transformed.reshape((1, 3, 224, 224)))
model_output
我们来获取一下网络对图片的响应。类别编号和名称之间的关系可以在这里找到:链接
np.argmax(model_output.data.cpu().numpy())
输出:np.int64(207)
我们根据上面的链接发现第207号是’golden retriever’,金毛猎犬,和我们图一致!
迁移学习
Transfer Learning!
我们现在将讨论迁移学习范式。 Transfer Learning 译为知识的转移。我们将了解它是什么以及如何使用迁移学习来训练神经网络。
迁移学习有助于解决的问题:
我们知道,当训练数据集中的数据很少时,神经网络经常会过度拟合。当任务复杂时尤其如此。此类问题的一个例子是肿瘤图像的分类。在医学领域,用于训练神经网络的数据总是很少:收集起来非常困难。但这项任务相当复杂:肿瘤的形状和大小完全不同。
另一个很难收集足够数据的复杂任务的例子是鲜为人知的语言的机器翻译。为了训练一个从一种语言到另一种语言的机器翻译模型,需要大量的平行句子。平行句是由一对句子组成的:一种语言的句子和其第二种语言的准确翻译。最常见的是,此类优惠都是从互联网上收集的。例如维基百科,同一篇文章经常被翻译成许多不同的语言。
但如果某种语言没有被广泛使用,那么互联网上用该语言写的文本就会很少。而且,不可能用这种语言输入足够数量的句子并将其翻译成另一种语言。
这是一个常见的问题。它无处不在,不仅仅是在医学领域和机器翻译中。机器学习问题变得越来越复杂,解决这些问题所需的神经网络也变得越来越深,这需要越来越多的数据进行训练。简而言之,在机器学习中,数据永远是稀缺的。
解决这个问题的一种方法就是迁移学习。将知识从一个机器学习模型转移到另一个机器学习模型。迁移学习最简单的方法之一就是再训练。
网络训练
再训练的工作原理非常简单:
- 从头开始在 ImageNet(或其他大型数据集)上训练网络。
- 我们进一步在所需的数据集上训练网络。
ImageNet 通常是一个很好的预训练数据集。它包含大量不同的类别和形式,因此在对其进行训练的过程中,神经网络会学习对图片中各种模式做出反应的过滤器。就好像她“了解周围世界的物体是如何构成的”。即使新数据集中没有太多图像,这也能帮助网络更好地适应对新数据集中的新对象进行分类。
然而,这里有一个警告:大多数情况下,额外训练的数据集包含的类别数量与 ImageNet 不同。数据集中的类别数量会影响网络最后一层应该有多少个神经元。
这个问题解决起来很简单:在进一步训练之前,我们扔掉网络的最后一层,用一个新的层替换它,它将具有所需数量的神经元。该层的权重在重新训练之前将是随机的;它们将在重新训练过程中与整个神经网络一起进行训练。
重新训练几乎总是比从头开始训练效果更好(前提是重新训练的数据集不比预训练的数据集丰富)。
即使对于语义上相距甚远的数据集,重新训练也有效。例如,在 ImageNet 上进行预训练并在建筑物数据集上进行再训练也效果很好。
有时,在重新训练卷积神经网络之前,所有全连接层都会被替换。这是因为卷积层是特征提取层,而全连接层是分类器层。在重新训练的过程中,特征提取层学会了从图像中提取有用的特征,这在新的数据集上重新训练时也会有用。分类器层是相当专业的层,经过训练可以解决狭义的任务:对特定数据集中的图像进行分类。因此,它们可以针对新任务进行重新训练,以便它们学会以新的方式匹配从卷积层和任务类获得的特征。
冻结层
有时,为了加快神经网络的训练,会冻结预先训练的神经网络的几个初始层。
为什么他们要冻结初始层?我们记得,在训练卷积神经网络时,初始层响应图像中更简单、低级的模式,而外层响应更高级、复合的模式。无论我们解决什么问题,几乎所有图片中都会出现简单的图案。因此,神经网络的第一层在 ImageNet 上训练时学会提取的几乎所有信息在我们进一步对野生动物进行神经网络训练时也将有用。但最后的卷积层学会了提取有关 ImageNet 图像中复杂模式存在的信息。并且它们更具体地针对网络训练的数据集。并非所有这些都与野生动物分类有关。因此,在新的数据集上进一步训练远距离卷积层是有意义的,这样它们就会学会识别那些有助于对野生动物进行分类的复杂模式。
我们可以说,第一个卷积层学习理解图片中物体的基础、基本结构。然后我们将有关物体基本结构的知识转移到另一项任务。现在需要对神经网络进行训练,以便对野生动物进行分类,而不必从头开始:它只需要从它所知道的形式中学习组装十种野生动物的模式:即第一个预训练的卷积层识别的形式。
实践
准备数据
# 此单元格下载包含数据的 zip 存档
! pip install wldhx.yadisk-direct
! curl -L $(yadisk-direct https://disk.yandex.com/d/eS6LL7bLmrlO7w) -o dogs.zip
输出:
# 此单元格解压档案。包含数据的文件夹 ./dogs 将出现在 colab 中。
! unzip -qq dogs.zip
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import datasets, models, transforms
resnet_transforms = transforms.Compose([
transforms.Resize(256), # 每幅图像的尺寸将缩小到 256*256
transforms.CenterCrop(224), # 图片的中心部分将被剪掉,尺寸为 224*224
transforms.RandomPerspective(distortion_scale=0.6, p=1.0),
transforms.ToTensor(), # 图像从python数组转换为torch.Tensor格式
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 图像像素值归一化
])
train_data = datasets.ImageFolder('./dogs/train', transform=resnet_transforms)
val_data = datasets.ImageFolder('./dogs/valid', transform=resnet_transforms)
test_data = datasets.ImageFolder('./dogs/test', transform=resnet_transforms)
会变成:
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=64, shuffle=False)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=64, shuffle=False)
替换网络的最后一层
让我们加载将进一步训练的模型:
model = models.resnet18(pretrained=True)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
model.fc
输出:Linear(in_features=512, out_features=1000, bias=True)
让我们用一个包含 70 个神经元的新层替换网络的最后一层(因为数据集中有 70 个类):
model.fc = nn.Linear(512, 70)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=70, bias=True)
)
我们发现只有最后的输出类别(也就是最后一层发生了改变,其余的不变)
冻结层
# model.children() 返回神经网络子模块列表
# 在我们的例子中,这些是 resnet 块
len(list(model.children()))
输出:10
让我们冻结所有卷积层:
# 我们通过神经网络块
for i, layer in enumerate(model.children()):
# 冻结前九个块
if i < 9:
# 我们遍历块的所有权重(参数)
for param in layer.parameters():
# 冻结参数
param.requires_grad = False
我们将所有准备代码放在一起:
def create_model(model, num_freeze_layers, num_out_classes):
# 替换网络的最后一层
model.fc = nn.Linear(512, num_out_classes)
# 冻结图层
for i, layer in enumerate(model.children()):
if i < num_freeze_layers:
for param in layer.parameters():
param.requires_grad = False
return model
model = create_model(models.resnet18(pretrained=True), 9, 70)
网络训练
如果 GPU 可用,我们就将神经网络转移到 GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
def evaluate(model, dataloader, loss_fn):
losses = []
num_correct = 0
num_elements = 0
for i, batch in enumerate(dataloader):
# 获取当前批次
X_batch, y_batch = batch
num_elements += len(y_batch)
# 此行禁用梯度计算
with torch.no_grad():
# 获取批量图像的网络响应
logits = model(X_batch.to(device))
# 计算当前批次的损失
loss = loss_fn(logits, y_batch.to(device))
losses.append(loss.item())
# 计算网络响应作为每幅图像的类别编号
y_pred = torch.argmax(logits, dim=1)
# 计算当前批次中正确的网络响应数量
num_correct += torch.sum(y_pred.cpu() == y_batch)
# 计算最终正确答案的百分比
accuracy = num_correct / num_elements
return accuracy.numpy(), np.mean(losses)
def train(model, loss_fn, optimizer, n_epoch=3):
# 网络训练周期
for epoch in range(n_epoch):
print("Epoch:", epoch+1)
model.train(True)
running_losses = []
running_accuracies = []
for i, batch in enumerate(train_loader):
# 获取当前批次
X_batch, y_batch = batch
# 前向传递(获取对一批图像的响应)
logits = model(X_batch.to(device))
# 计算网络给出的答案和批次的正确答案的损失
loss = loss_fn(logits, y_batch.to(device))
running_losses.append(loss.item())
loss.backward() # backpropagation(梯度计算)
optimizer.step() # 更新网络权重
optimizer.zero_grad() # 重置权重
# 计算当前训练批次的准确率
model_answers = torch.argmax(logits, dim=1)
train_accuracy = torch.sum(y_batch == model_answers.cpu()) / len(y_batch)
running_accuracies.append(train_accuracy)
# 记录结果
if (i+1) % 50 == 0:
print("Average train loss and accuracy over the last 50 iterations:",
np.mean(running_losses), np.mean(running_accuracies), end='\n')
# 每个时期之后,我们都会得到验证样本的质量指标
model.train(False)
val_accuracy, val_loss = evaluate(model, val_loader, loss_fn=loss_fn)
print("Epoch {}/{}: val loss and accuracy:".format(epoch+1, n_epoch,),
val_loss, val_accuracy, end='\n')
return model
# 再次声明模型
model = create_model(models.resnet18(pretrained=True), 9, 70)
model = model.to(device)
# 选择损失函数
loss_fn = torch.nn.CrossEntropyLoss()
# 选择优化算法和学习率。
# 你可以尝试不同的 learning_rate 值
learning_rate = 1e-3
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 让我们开始训练模型
# 参数 n_epoch 可以变化
model = train(model, loss_fn, optimizer, n_epoch=3)
输出:
获取测试样本的质量指标
test_accuracy, _ = evaluate(model, test_loader, loss_fn)
print('Accuracy on the test', test_accuracy)
输出:
Accuracy on the test 0.81857145