CV with PyTorch 04 - Use a pre-trained network with transfer learning

274 阅读17分钟

使用预训练网络来进行转移学习

一、预训练网络与转移学习

训练 CNN 需要大量时间,并且该任务需要大量的数据。 然而,大部分时间都花在从图像中提取模式的最佳低级过滤器上。那么问题就来了,我们能否使用在一个数据集上训练后的神经网络并使其适应于对不同图像进行分类而无需完整的训练过程?

答案是肯定的,这种方法称为迁移学习,因为我们将一些“认知”从一个神经网络模型转移到另一个。 在迁移学习中,我们通常从一个预训练模型开始(该模型已经在一些大型图像数据集,例如 ImageNet)上进行了训练。 这些模型已经可以很好地从通用图像中提取不同的特征,并且在多数情况下,只需在这些提取的特征之上构建分类器就可以产生良好的结果。

!wget https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
--2022-05-17 17:19:58--  https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 199.232.68.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|199.232.68.133|:443... failed: Unknown error.
Retrying.

--2022-05-17 17:20:21--  (try: 2)  https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|199.232.68.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6371 (6.2K) [text/plain]
Saving to: 'pytorchcv.py.1'

     0K ......                                                100% 1.01M=0.006s

2022-05-17 17:20:23 (1.01 MB/s) - 'pytorchcv.py.1' saved [6371/6371]

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

1. 猫狗数据集

在本单元中,我们将解决一个现实生活中对猫和狗图像进行分类的问题。 出于这个原因,我们将使用 Kaggle Cats vs. Dogs Dataset,也可以从 Microsoft 下载。

not os.path.exists('data/kagglecatsanddogs_5340.zip')
True

不存在就下载,让我们下载这个数据集并将其解压缩到数据目录中(这个过程可能需要一些时间!) :

if not os.path.exists('data/kagglecatsanddogs_3367a.zip'):
    !wget -P data -q https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.zip

下面的下载链接失效了,可以直接在www.microsoft.com/en-us/downl… 搜索下载

!wget -P data -q https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.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


F:\anaconda3\lib\site-packages\PIL\TiffImagePlugin.py:822: UserWarning: Truncated File Read
  warnings.warn(str(msg))

接下来,让我们将图像加载到 PyTorch 数据集中,将它们转换为张量并进行一些归一化。 我们通过使用 Compose模块 组合几个原始转换来定义图像转换管道:

  1. resize:将我们的图像重塑大小为 256 * 256
  2. CenterCrop:获取大小为 224x224 的图像的中心部分。 预训练的 VGG 网络已经在 224x224 图像上进行了训练,因此我们需要将我们的数据集带变为此大小。
  3. ToTensor:将图像的所有灰度值归一化到 [0,1] 区间,且将图像转化为张量
  4. std_normalize:transform 是针对 VGG 网络的额外归一化步骤。 在训练 VGG 网络时,来自 ImageNet 的原始图像通过按颜色减去数据集平均强度并除以标准差(也按颜色)进行转换。 因此,我们需要对数据集应用相同的转换,以便正确处理所有图像。

我们将图像大小调整为 256,然后裁剪为 224 像素的原因有几个:

  1. 我们想展示更多可能的转换。
  2. 宠物通常位于图像的中心部分,因此我们可以通过更多地关注中心部分来改进分类
  3. 由于某些图像不是正方形的,我们最终会得到不包含任何有用图片数据的图像填充部分 ,并且稍微裁剪图像会减少填充部分。
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)

output_19_0.png

2. 预训练模型

torchvision 模块中提供了许多不同的预训练模型,甚至可以在 Internet 上找到更多模型。 让我们看看如何加载和使用最简单的 VGG-16 模型:

vgg = torchvision.models.vgg16(pretrained=True)
sample_image = dataset[0][0].unsqueeze(0)
res = vgg(sample_image)
print(res[0].argmax())
Downloading: "https://download.pytorch.org/models/vgg16-397923af.pth" to C:\Users\yzx20/.cache\torch\hub\checkpoints\vgg16-397923af.pth



  0%|          | 0.00/528M [00:00<?, ?B/s]


tensor(281)

我们收到的结果是一个 ImageNet 类的编号,可以在此处查找gist.github.com/yrevar/942d… 我们可以使用下面的代码来自动加载这个类表并返回结果:

报错解决:重新载入一次就行

import json, requests
class_map = json.loads(requests.get("https://mslearntensorflowlp.blob.core.windows.net/metadata/imagenet_class_index.json").text)
class_map = { int(k) : v for k,v in class_map.items() }

class_map[res[0].argmax().item()]
['n02123045', 'tabby']
# 可以读取到数据
requests.get("https://mslearntensorflowlp.blob.core.windows.net/metadata/imagenet_class_index.json").text
'{"0": ["n01440764", "tench"], "1": ["n01443537", "goldfish"], "2": ["n01484850", "great_white_shark"], "3": ["n01491361", "tiger_shark"], "4": ["n01494475", "hammerhead"], "5": ..."yellow_lady\'s_slipper"], "987": ["n12144580", "corn"], "988": ["n12267677", "acorn"], "989": ["n12620546", "hip"], "990": ["n12768682", "buckeye"], "991": ["n12985857", "coral_fungus"], "992": ["n12998815", "agaric"], "993": ["n13037406", "gyromitra"], "994": ["n13040303", "stinkhorn"], "995": ["n13044778", "earthstar"], "996": ["n13052670", "hen-of-the-woods"], "997": ["n13054560", "bolete"], "998": ["n13133613", "ear"], "999": ["n15075141", "toilet_tissue"]}'

接下来我们查看以下VGG网络的结构

summary(vgg,input_size=(1,3,224,224))
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
VGG                                      --                        --
├─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。 这些层充当正则化技术。 正则化对学习算法进行了轻微修改,因此模型可以更好地泛化。 在训练过程中,dropout 层丢弃了前一层中的一部分(大约 30%)神经元,并且在没有它们的情况下进行训练。 这有助于使优化过程摆脱局部最小值,并在不同的神经路径之间分配决定性的力量,从而提高网络的整体稳定性。

二、GPU计算

深度神经网络,例如 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()
Doing computations on device = cuda





tensor(281, device='cuda:0')

三、提取VGG特征

如果我们想使用 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])



output_36_1.png

特征张量的维度是 512x7x7,但为了将其可视化,我们必须将其重塑为 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)
F:\anaconda3\lib\site-packages\torch\nn\modules\container.py:141: UserWarning: Implicit dimension choice for log_softmax has been deprecated. Change the call to include dim=X as an argument.
  input = module(input)


Epoch  0, Train acc=0.940, Val acc=1.000, Train loss=0.039, Val loss=0.000
Epoch  1, Train acc=0.986, Val acc=1.000, Train loss=0.011, Val loss=0.000
Epoch  2, Train acc=0.983, Val acc=1.000, Train loss=0.012, Val loss=0.000
Epoch  3, Train acc=0.994, Val acc=0.990, Train loss=0.002, Val loss=0.014
Epoch  4, Train acc=0.996, Val acc=1.000, Train loss=0.004, Val loss=0.000
Epoch  5, Train acc=1.000, Val acc=1.000, Train loss=0.000, Val loss=0.000
Epoch  6, Train acc=1.000, Val acc=1.000, Train loss=0.000, Val loss=0.000
Epoch  7, Train acc=1.000, Val acc=1.000, Train loss=0.000, Val loss=0.000
Epoch  8, Train acc=1.000, Val acc=1.000, Train loss=0.000, Val loss=0.000
Epoch  9, Train acc=1.000, Val acc=1.000, Train loss=0.000, Val loss=0.000

结果很好,我们几乎可以 100% 的概率区分猫和狗! 然而,我们只在所有图像的一小部分上测试了这种方法,因为手动特征提取似乎需要很多时间。

四、使用VGG网络进行转移学习

我们还可以在训练期间通过使用原始 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)
  )
)

可以看到网络包含:

  1. 特征提取器(features),由多个卷积层和池化层组成,
  2. 平均池化层(avgpool)
  3. 最终分类器,由几个密集层组成,将25088个输入特征变成1000个类(即ImageNet中的类数)

为了训练对我们的数据集进行分类的端到端模型,我们需要:

  1. 将最终分类器替换为将产生所需数量的类的分类器。 在我们的例子中,我们可以使用一个具有 25088 个输入和 2 个输出神经元的线性层。
  2. 冻结卷积特征提取器的权重,这样它们就不会被训练。 建议一开始就进行这种冻结,否则未经训练的分类器层会破坏卷积提取器的原始预训练权重。 冻结权重可以通过将所有参数的 requires_grad 属性设置为 False 来完成
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                                      --                        --
├─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
==========================================================================================

从总结中可以看出,这个模型总共包含大约 1500 万个参数,但其中只有 50k 是可训练的——这些是分类层的权重。 这很好,因为我们能够用更少的示例微调更少的参数。

现在让我们使用原始数据集训练模型。 这个过程需要很长时间,所以我们将使用 train_long 函数,该函数将打印一些中间结果,而无需等待 epoch 结束。 强烈建议在支持 GPU 的计算上运行此训练!

  • 注意:如果您对 train_long 函数的实现感兴趣,请参考 pytorchcv.py 文件。
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.25, train loss = 0.08703116327524185
Epoch 0, minibatch 90: train acc = 0.9601648351648352, train loss = 0.0797352895631895
Epoch 0, minibatch 180: train acc = 0.9613259668508287, train loss = 0.09862042000280559
Epoch 0, minibatch 270: train acc = 0.9624077490774908, train loss = 0.11259729307955921
Epoch 0, minibatch 360: train acc = 0.965893351800554, train loss = 0.10862904176157268
Epoch 0, minibatch 450: train acc = 0.9675720620842572, train loss = 0.11064581014628949
Epoch 0, minibatch 540: train acc = 0.9689232902033271, train loss = 0.11547197211471812
Epoch 0, minibatch 630: train acc = 0.9700871632329635, train loss = 0.11887597811014262
Epoch 0, minibatch 720: train acc = 0.97000693481276, train loss = 0.13252536599083847
Epoch 0, minibatch 810: train acc = 0.9714858199753391, train loss = 0.12969087349060285
Epoch 0, minibatch 900: train acc = 0.9721143174250833, train loss = 0.1346065847246549
Epoch 0, minibatch 990: train acc = 0.9721871846619576, train loss = 0.14882451277808872
Epoch 0, minibatch 1080: train acc = 0.9725948196114709, train loss = 0.15638207815842536
Epoch 0, minibatch 1170: train acc = 0.9732600341588385, train loss = 0.15407317932806497
Epoch 0 done, validation acc = 0.9805922368947579, validation loss = 0.15342186679359243

可以看出我们的猫狗分类器十分精准,那就把它保存下来吧

torch.save(vgg,'data/cats_dogs.pth')

然后我们可以随时从文件中加载模型。 如果下一个实验破坏了模型,您可能会发现模型备份很有用 - 您不必从头开始重新开始。

vgg = torch.load('data/cats_dogs.pth')

五、微调迁移学习

在上一节中,我们已经训练了最终的分类器层来对我们自己的数据集中的图像进行分类。 但是,我们没有重新训练特征提取器,我们的模型依赖于模型在 ImageNet 数据上学习到的特征。 如果您的对象在视觉上与普通 ImageNet 图像不同,则这种特征组合可能效果不佳。 因此,开始训练卷积层也是有意义的。

要训练卷积层的参数,我们可以解冻卷积滤波器参数。

  • 注意: 为了稳定分类层中的权重,首先冻结参数并执行几个Epoch的训练是很重要的。如果您立即开始训练端到端网络的解冻参数,大错误可能会破坏在卷积层的预先训练的权重。
for x in vgg.features.parameters():
    x.requires_grad = True

解冻后,我们可以再做几个 epochs 的训练。 您还可以选择较低的学习率,以尽量减少对预训练权重的影响。 然而,即使学习率很低,你也可以预期在训练开始时准确率会下降,直到最终达到比固定权重略高的水平。

  • 注意:这种训练发生的速度要慢得多,因为我们需要将梯度传播回网络的许多层! 您可能想观察前几个小批量以了解趋势,然后停止计算
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,print_freq=90,lr=0.0001)
Epoch 0, minibatch 0: train acc = 1.0, train loss = 0.0
Epoch 0, minibatch 90: train acc = 0.929945054945055, train loss = 0.1727896679888715
Epoch 0, minibatch 180: train acc = 0.9299033149171271, train loss = 0.11025271758190176
Epoch 0, minibatch 270: train acc = 0.9328874538745388, train loss = 0.08081236272720393
Epoch 0, minibatch 360: train acc = 0.9392313019390581, train loss = 0.06374165225887563
Epoch 0, minibatch 450: train acc = 0.9440133037694013, train loss = 0.054383449173819466
Epoch 0, minibatch 540: train acc = 0.9462800369685767, train loss = 0.04723344757023668
Epoch 0, minibatch 630: train acc = 0.9497820919175911, train loss = 0.04178125166855221
Epoch 0, minibatch 720: train acc = 0.951369625520111, train loss = 0.03808653271975365
Epoch 0, minibatch 810: train acc = 0.9485203452527744, train loss = 0.03571697757217652
Epoch 0, minibatch 900: train acc = 0.9489456159822419, train loss = 0.03312074489784029
Epoch 0, minibatch 990: train acc = 0.9498612512613521, train loss = 0.030831443794319777
Epoch 0, minibatch 1080: train acc = 0.9506244218316374, train loss = 0.028792687415194446
Epoch 0, minibatch 1170: train acc = 0.9519641332194705, train loss = 0.02702361024617131
Epoch 0 done, validation acc = 0.9629851940776311, validation loss = 0.006640538448045233

六、其他的CV模型

VGG-16 是最简单的计算机视觉架构之一。 torchvision 包提供了更多的预训练网络。 其中最常用的是微软开发的 ResNet 架构和谷歌的 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 网络经过预训练以识别猫和狗,因此我们只是重用了网络中已经存在的大部分模式。 您可以预期在更奇特的特定领域对象上的准确性会降低,例如工厂生产线的细节或不同的树叶。

可以看到,我们现在解决的更复杂的任务需要更高的计算能力,在 CPU 上不容易解决。 在下一个单元中,我们将尝试使用更轻量级的实现来使用更少的计算资源来训练相同的模型,这会导致精度略低。

九、错误小结

1. 对于迁移学习,我们使用在 1000 个类上预先训练过的 VGG-16 网络。 我们在网络中可以拥有多少类?

由于我们正在训练自己的最终分类器,所以预先训练的网络中的类数并不重要,所以任何类数都可以