训练 CNN 可能需要大量时间,并且该任务需要大量数据。但是,大部分时间都花在学习网络用来从图像中提取模式的最佳低级过滤器上。一个自然的问题出现了——我们是否可以使用在一个数据集上训练的神经网络,并在没有完整训练过程的情况下对其进行分类以对不同的图像进行分类?
这种方法称为迁移学习,因为我们将一些知识从一个神经网络模型转移到另一个神经网络模型。在迁移学习中,我们通常从一个预先训练的模型开始,该模型已经在一些大型图像数据集(如 ImageNet)上进行了训练。这些模型已经可以从通用图像中提取不同的特征,在许多情况下,只需在这些提取的特征之上构建分类器就可以产生良好的结果。
!wget https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
!pip install -r https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/requirements.txt
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
import os
from pytorchcv import train, plot_results, display_dataset, train_long, check_image_dir
在本单元中,我们将解决对猫和狗的图像进行分类的现实问题。出于这个原因,我们将使用Kaggle Cats vs. Dogs数据集。让我们下载此数据集并将其提取到数据目录中:
if not os.path.exists('data/kagglecatsanddogs_5340.zip'):
!wget -P data -q https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip
import zipfile
if not os.path.exists('data/PetImages'):
with zipfile.ZipFile('data/kagglecatsanddogs_5340.zip', 'r') as zip_ref:
zip_ref.extractall('data')
不幸的是,数据集中有一些损坏的图像文件。我们需要进行快速清理以检查损坏的文件。为了不破坏这个笔记本,我们将验证数据集的代码移到了一个模块中,我们在这里调用它。check_image_dir逐个图像遍历整个数据集,尝试加载图像并检查是否可以正确加载。所有损坏的图像都将被删除。
check_image_dir('data/PetImages/Cat/*.jpg')
check_image_dir('data/PetImages/Dog/*.jpg')
Corrupt image: data/PetImages/Cat/666.jpg
Corrupt image: data/PetImages/Dog/11702.jpg
/anaconda/envs/py38_default/lib/python3.8/site-packages/PIL/TiffImagePlugin.py:845: UserWarning: Truncated File Read
warnings.warn(str(msg))
接下来,让我们将图像加载到 PyTorch 数据集中,将它们转换为张量并进行一些规范化。我们通过使用 Compose 组合几个基元转换来定义图像转换管道:
我们将图像大小调整为 256 大小,然后裁剪为 224 像素有几个原因:
std_normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
trans = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
std_normalize])
dataset = torchvision.datasets.ImageFolder('data/PetImages',transform=trans)
trainset, testset = torch.utils.data.random_split(dataset,[20000,len(dataset)-20000])
display_dataset(dataset)
火炬视觉模块中有许多不同的预训练模型,甚至可以在互联网上找到更多模型。让我们看看如何加载和使用最简单的 VGG-16 模型。首先,我们将下载存储在本地存储库中的 VGG-16 模型的权重。
# Download model weights in the sandbox environment
!mkdir -p models
!wget -P models https://github.com/MicrosoftDocs/pytorchfundamentals/raw/main/computer-vision-pytorch/vgg16-397923af.pth
接下来,我们将使用 load_state_dict 方法将权重加载到预训练的 VGG-16 模型中。然后,使用 eval 方法将模型设置为推理模式。
file_path = 'models/vgg16-397923af.pth'
vgg = torchvision.models.vgg16()
vgg.load_state_dict(torch.load(file_path))
vgg.eval()
sample_image = dataset[0][0].unsqueeze(0)
res = vgg(sample_image)
print(res[0].argmax())
tensor(282)
我们收到的结果是 ImageNet 类的编号,可以在此处查找。我们可以使用下面的代码来自动加载这个类表并返回结果:
import json, requests
class_map = json.loads(requests.get("https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/imagenet_class_index.json").text)
class_map = { int(k) : v for k,v in class_map.items() }
class_map[res[0].argmax().item()]
['n02123159', 'tiger_cat']
我们还可以看看 VGG-16 网络的架构:
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
VGG [1, 1000] --
├─Sequential: 1-1 [1, 512, 7, 7] --
│ └─Conv2d: 2-1 [1, 64, 224, 224] 1,792
│ └─ReLU: 2-2 [1, 64, 224, 224] --
│ └─Conv2d: 2-3 [1, 64, 224, 224] 36,928
│ └─ReLU: 2-4 [1, 64, 224, 224] --
│ └─MaxPool2d: 2-5 [1, 64, 112, 112] --
│ └─Conv2d: 2-6 [1, 128, 112, 112] 73,856
│ └─ReLU: 2-7 [1, 128, 112, 112] --
│ └─Conv2d: 2-8 [1, 128, 112, 112] 147,584
│ └─ReLU: 2-9 [1, 128, 112, 112] --
│ └─MaxPool2d: 2-10 [1, 128, 56, 56] --
│ └─Conv2d: 2-11 [1, 256, 56, 56] 295,168
│ └─ReLU: 2-12 [1, 256, 56, 56] --
│ └─Conv2d: 2-13 [1, 256, 56, 56] 590,080
│ └─ReLU: 2-14 [1, 256, 56, 56] --
│ └─Conv2d: 2-15 [1, 256, 56, 56] 590,080
│ └─ReLU: 2-16 [1, 256, 56, 56] --
│ └─MaxPool2d: 2-17 [1, 256, 28, 28] --
│ └─Conv2d: 2-18 [1, 512, 28, 28] 1,180,160
│ └─ReLU: 2-19 [1, 512, 28, 28] --
│ └─Conv2d: 2-20 [1, 512, 28, 28] 2,359,808
│ └─ReLU: 2-21 [1, 512, 28, 28] --
│ └─Conv2d: 2-22 [1, 512, 28, 28] 2,359,808
│ └─ReLU: 2-23 [1, 512, 28, 28] --
│ └─MaxPool2d: 2-24 [1, 512, 14, 14] --
│ └─Conv2d: 2-25 [1, 512, 14, 14] 2,359,808
│ └─ReLU: 2-26 [1, 512, 14, 14] --
│ └─Conv2d: 2-27 [1, 512, 14, 14] 2,359,808
│ └─ReLU: 2-28 [1, 512, 14, 14] --
│ └─Conv2d: 2-29 [1, 512, 14, 14] 2,359,808
│ └─ReLU: 2-30 [1, 512, 14, 14] --
│ └─MaxPool2d: 2-31 [1, 512, 7, 7] --
├─AdaptiveAvgPool2d: 1-2 [1, 512, 7, 7] --
├─Sequential: 1-3 [1, 1000] --
│ └─Linear: 2-32 [1, 4096] 102,764,544
│ └─ReLU: 2-33 [1, 4096] --
│ └─Dropout: 2-34 [1, 4096] --
│ └─Linear: 2-35 [1, 4096] 16,781,312
│ └─ReLU: 2-36 [1, 4096] --
│ └─Dropout: 2-37 [1, 4096] --
│ └─Linear: 2-38 [1, 1000] 4,097,000
==========================================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
Total mult-adds (G): 15.48
==========================================================================================
Input size (MB): 0.60
Forward/backward pass size (MB): 108.45
Params size (MB): 553.43
Estimated Total Size (MB): 662.49
==========================================================================================
除了我们已经知道的图层之外,还有另一种称为 Dropout 的图层类型。这些层充当正则化技术。正则化对学习算法进行了轻微的修改,以便模型更好地泛化。在训练期间,辍学层丢弃前一层中一定比例(约30%)的神经元,并且在没有它们的情况下进行训练。这有助于使优化过程摆脱局部最小值,并在不同的神经路径之间分配决定性的力量,从而提高网络的整体稳定性。
深度神经网络,如VGG-16和其他更现代的架构,需要相当多的计算能力才能运行。如果可用,使用 GPU 加速是有意义的。为此,我们需要显式地将计算中涉及的所有张量移动到 GPU。
通常的做法是检查代码中 GPU 的可用性,并定义指向计算设备的设备变量 - GPU 或 CPU。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Doing computations on device = {}'.format(device))
vgg.to(device)
sample_image = sample_image.to(device)
vgg(sample_image).argmax()
如果我们想使用 VGG-16 从图像中提取特征,我们需要没有最终分类层的模型。事实上,这个“特征提取器”可以使用 vgg.features 方法获得:
res = vgg.features(sample_image).cpu()
plt.figure(figsize=(15,3))
plt.imshow(res.detach().view(-1,512))
print(res.size())
torch.Size([1, 512, 7, 7])
特征张量的维度是 512 × 7 × 7,但为了可视化它,我们必须将其重塑为 2D 形式。
现在让我们尝试看看这些功能是否可用于对图像进行分类。
让我们手动获取一部分图像(在我们的例子中为 800),并预先计算它们的特征向量。我们将结果存储在一个名为 feature_tensor 的大张量中,并将标签也存储在label_tensor中:
bs = 8
dl = torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True)
num = bs*100
feature_tensor = torch.zeros(num,512*7*7).to(device)
label_tensor = torch.zeros(num).to(device)
i = 0
for x,l in dl:
with torch.no_grad():
f = vgg.features(x.to(device))
feature_tensor[i:i+bs] = f.view(bs,-1)
label_tensor[i:i+bs] = l
i+=bs
print('.',end='')
if i>=num:
break
现在我们可以定义从这个张量中获取数据vgg_dataset,使用 random_split 函数将其拆分为训练集和测试集,并在提取的特征之上训练一个小型的单层密集分类器网络:
vgg_dataset = torch.utils.data.TensorDataset(feature_tensor,label_tensor.to(torch.long))
train_ds, test_ds = torch.utils.data.random_split(vgg_dataset,[700,100])
train_loader = torch.utils.data.DataLoader(train_ds,batch_size=32)
test_loader = torch.utils.data.DataLoader(test_ds,batch_size=32)
net = torch.nn.Sequential(torch.nn.Linear(512*7*7,2),torch.nn.LogSoftmax()).to(device)
history = train(net,train_loader,test_loader)
Epoch 0, Train acc=0.899, Val acc=0.960, Train loss=0.099, Val loss=0.050
Epoch 1, Train acc=0.989, Val acc=0.970, Train loss=0.016, Val loss=0.037
Epoch 2, Train acc=0.996, Val acc=0.970, Train loss=0.003, Val loss=0.155
Epoch 3, Train acc=0.999, Val acc=0.960, Train loss=0.001, Val loss=0.024
Epoch 4, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Epoch 5, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Epoch 6, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Epoch 7, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Epoch 8, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
Epoch 9, Train acc=1.000, Val acc=0.970, Train loss=0.000, Val loss=0.026
结果很棒,我们可以以几乎 98% 的概率区分猫和狗!但是,我们只在所有图像的一小部分上测试了这种方法,因为手动特征提取似乎需要花费大量时间。
我们还可以通过在训练期间使用原始 VGG-16 网络作为一个整体来避免手动预计算特征。让我们看一下 VGG-16 对象结构:
print(vgg)
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
您可以看到网络包含:
要训练将对数据集进行分类的端到端模型,我们需要:
vgg.classifier = torch.nn.Linear(25088,2).to(device)
for x in vgg.features.parameters():
x.requires_grad = False
summary(vgg,(1, 3,244,244))
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
VGG [1, 2] --
├─Sequential: 1-1 [1, 512, 7, 7] --
│ └─Conv2d: 2-1 [1, 64, 244, 244] (1,792)
│ └─ReLU: 2-2 [1, 64, 244, 244] --
│ └─Conv2d: 2-3 [1, 64, 244, 244] (36,928)
│ └─ReLU: 2-4 [1, 64, 244, 244] --
│ └─MaxPool2d: 2-5 [1, 64, 122, 122] --
│ └─Conv2d: 2-6 [1, 128, 122, 122] (73,856)
│ └─ReLU: 2-7 [1, 128, 122, 122] --
│ └─Conv2d: 2-8 [1, 128, 122, 122] (147,584)
│ └─ReLU: 2-9 [1, 128, 122, 122] --
│ └─MaxPool2d: 2-10 [1, 128, 61, 61] --
│ └─Conv2d: 2-11 [1, 256, 61, 61] (295,168)
│ └─ReLU: 2-12 [1, 256, 61, 61] --
│ └─Conv2d: 2-13 [1, 256, 61, 61] (590,080)
│ └─ReLU: 2-14 [1, 256, 61, 61] --
│ └─Conv2d: 2-15 [1, 256, 61, 61] (590,080)
│ └─ReLU: 2-16 [1, 256, 61, 61] --
│ └─MaxPool2d: 2-17 [1, 256, 30, 30] --
│ └─Conv2d: 2-18 [1, 512, 30, 30] (1,180,160)
│ └─ReLU: 2-19 [1, 512, 30, 30] --
│ └─Conv2d: 2-20 [1, 512, 30, 30] (2,359,808)
│ └─ReLU: 2-21 [1, 512, 30, 30] --
│ └─Conv2d: 2-22 [1, 512, 30, 30] (2,359,808)
│ └─ReLU: 2-23 [1, 512, 30, 30] --
│ └─MaxPool2d: 2-24 [1, 512, 15, 15] --
│ └─Conv2d: 2-25 [1, 512, 15, 15] (2,359,808)
│ └─ReLU: 2-26 [1, 512, 15, 15] --
│ └─Conv2d: 2-27 [1, 512, 15, 15] (2,359,808)
│ └─ReLU: 2-28 [1, 512, 15, 15] --
│ └─Conv2d: 2-29 [1, 512, 15, 15] (2,359,808)
│ └─ReLU: 2-30 [1, 512, 15, 15] --
│ └─MaxPool2d: 2-31 [1, 512, 7, 7] --
├─AdaptiveAvgPool2d: 1-2 [1, 512, 7, 7] --
├─Linear: 1-3 [1, 2] 50,178
==========================================================================================
Total params: 14,764,866
Trainable params: 50,178
Non-trainable params: 14,714,688
Total mult-adds (G): 17.99
==========================================================================================
Input size (MB): 0.71
Forward/backward pass size (MB): 128.13
Params size (MB): 59.06
Estimated Total Size (MB): 187.91
============================================================
从摘要中可以看出,该模型总共包含大约 15 万个参数,但其中只有 50k 个参数是可训练的——这些是分类层的权重。这很好,因为我们能够用更少的示例微调较少数量的参数。
现在,让我们使用原始数据集训练模型。这个过程需要很长时间,因此我们将使用 train_long 函数,该函数将打印一些中间结果,而无需等待纪元结束。强烈建议在启用 GPU 的计算上运行此培训!
trainset, testset = torch.utils.data.random_split(dataset,[20000,len(dataset)-20000])
train_loader = torch.utils.data.DataLoader(trainset,batch_size=16)
test_loader = torch.utils.data.DataLoader(testset,batch_size=16)
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,print_freq=90)
Epoch 0, minibatch 0: train acc = 0.375, train loss = 0.049225371330976486
Epoch 0, minibatch 90: train acc = 0.945054945054945, train loss = 0.13971448206639553
Epoch 0, minibatch 180: train acc = 0.9537292817679558, train loss = 0.13658707971730943
Epoch 0, minibatch 270: train acc = 0.959409594095941, train loss = 0.12419342378848593
Epoch 0, minibatch 360: train acc = 0.9643351800554016, train loss = 0.1132235328906791
Epoch 0, minibatch 450: train acc = 0.9657705099778271, train loss = 0.1185816914966524
Epoch 0, minibatch 540: train acc = 0.9660351201478743, train loss = 0.1288116652511625
Epoch 0, minibatch 630: train acc = 0.9667194928684627, train loss = 0.12467173048886936
Epoch 0, minibatch 720: train acc = 0.9690533980582524, train loss = 0.11865157128703081
Epoch 0, minibatch 810: train acc = 0.9700215782983971, train loss = 0.12006495766222257
Epoch 0, minibatch 900: train acc = 0.9701720310765816, train loss = 0.13083069491201182
Epoch 0, minibatch 990: train acc = 0.9709258324924319, train loss = 0.13124172237638748
Epoch 0, minibatch 1080: train acc = 0.9714962997224792, train loss = 0.13153546215978407
Epoch 0, minibatch 1170: train acc = 0.9718723313407344, train loss = 0.13723152870810204
Epoch 0 done, validation acc = 0.9795918367346939, validation loss = 0.12322273880255227
看起来我们已经获得了相当准确的猫与狗分类器!让我们保存它以备将来使用!
torch.save(vgg,'data/cats_dogs.pth')
然后,我们可以随时从文件加载模型。您可能会发现它在下一个实验破坏模型的情况下很有用。
vgg = torch.load('data/cats_dogs.pth')
在上一节中,我们训练了最终分类器层来对自己数据集中的图像进行分类。但是,我们没有重新训练特征提取器,我们的模型依赖于模型在 ImageNet 数据上学习的特征。如果您的对象在视觉上与普通 ImageNet 图像不同,则这种功能组合可能无法发挥最佳效果。因此,开始训练卷积层也是有意义的。
为此,我们可以解冻之前冻结的卷积过滤器参数。
注意:请务必先冻结参数并执行多个训练周期,以稳定分类层中的权重。如果您立即开始使用未冻结的参数训练端到端网络,则较大的错误可能会破坏卷积层中预先训练的权重。
for x in vgg.features.parameters():
x.requires_grad = True
解冻后,我们可以再做几个时期的训练。您还可以选择较低的学习率,以尽量减少对预先训练的权重的影响。但是,即使学习率较低,您也可以预期在训练开始时准确性会下降,直到最终达到比固定权重略高的水平。
注意:这种训练发生得要慢得多,因为我们需要将梯度传播回网络的许多层!您可能需要观察前几个小批量以查看趋势,然后停止计算。
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,print_freq=90,lr=0.0001)
VGG-16 是最简单的计算机视觉架构之一。
火炬视觉软件包提供了更多预先训练的网络。其中最常用的是Microsoft开发的ResNet架构和 Google开发的Inception。例如,让我们探索最简单的 ResNet-18 模型的架构(ResNet 是具有不同深度的模型家族,如果你想看看真正深度的模型是什么样子,可以尝试使用 ResNet-151 进行实验):
resnet = torchvision.models.resnet18()
print(resnet)
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)
)
如您所见,该模型包含相同的构建块:特征提取器和最终分类器 (fc)。这使我们能够以与使用 VGG-16 进行迁移学习完全相同的方式使用此模型。您可以尝试使用上面的代码进行试验,使用不同的 ResNet 模型作为基本模型,并查看准确性如何变化。
此网络包含另一种类型的层:批量规范化。批量归一化的想法是将流经神经网络的值带到正确的间隔。通常,当所有值都在 [-1, 1] 或 [0, 1] 范围内时,神经网络效果最好,这就是我们相应地缩放/规范化输入数据的原因。
但是,在深度网络的训练过程中,可能会发生值明显超出此范围的情况,这使得训练成为问题。批量归一化层计算当前小批量所有值的平均值和标准偏差,并在信号通过神经网络层之前使用它们对信号进行归一化。这显著提高了深度网络的稳定性。
使用迁移学习,我们能够快速为自定义对象分类任务组合分类器,并实现高精度。然而,这个例子并不完全公平,因为最初的VGG-16网络是预先训练来识别猫和狗的,因此我们只是重用了网络中已经存在的大多数模式。对于更奇特的领域特定对象,例如植物生产线的详细信息或不同的树叶,您可能会期望精度较低。V笔记本