PyTorch ImageNet 基於預訓練六大常用圖片分類模型的實戰


微調 Torchvision 模型

在本教程中,我們將深入探討如何對 torchvision 模型進行微調和特征提取,所有這些模型都已經預先在1000類的Imagenet數據集上訓練完成。本教程將深入介紹如何使用幾個現代的CNN架構,並將直觀展示如何微調任意的PyTorch模型。由於每個模型架構是有差異的,因此沒有可以在所有場景中使用的微調代碼樣板。然而,研究人員必須查看現有架構並對每個模型進行自定義調整。

在本文檔中,我們將執行兩種類型的轉移學習:微調和特征提取。在微調中,我們從預訓練模型開始,更新我們新任務的所有模型參數,實質上是重新訓練整個模型。在特征提取中,我們從預訓練模型開始,僅更新從中導出預測的最終圖層權重。它被稱為特征提取,因為我們使用預訓練的CNN作為固定的特征提取器,並且僅改變輸出層。有關遷移學習的更多技術信息,請參閱此處和這里。

通常,這兩種遷移學習方法都遵循以下幾個步驟:

  • 初始化預訓練模型

  • 重組最后一層,使其具有與新數據集類別數相同的輸出數

  • 為優化算法定義我們想要在訓練期間更新的參數

  • 運行訓練步驟

1.導入相關包並打印版本號

from __future__ import print_function
from __future__ import division
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
print("PyTorch Version: ",torch.__version__)
print("Torchvision Version: ",torchvision.__version__)

輸出結果:

PyTorch Version:  1.1.0
Torchvision Version:  0.3.0

2.輸入

以下為運行時需要更改的所有參數。我們將使用的數據集 hymenoptera_data 可在此處下載。該數據集包含兩類:蜜蜂螞蟻,其結構使得我們可以使用 ImageFolder 數據集,不需要編寫我們自己的自定義數據集。下載數據並設置data_dir為數據集的根目錄。model_name是您要使用的模型名稱,必須從此列表中選擇:

[resnet, alexnet, vgg, squeezenet, densenet, inception]

其他輸入如下:num_classes為數據集的類別數,batch_size是訓練的 batch 大小,可以根據您機器的計算能力進行調整,num_epochsis是我們想要運行的訓練 epoch 數,feature_extractis是定義我們選擇微調還是特征提取的布爾值。如果feature_extract = False,將微調模型,並更新所有模型參數。如果feature_extract = True,則僅更新最后一層的參數,其他參數保持不變。

# 頂級數據目錄。 這里我們假設目錄的格式符合ImageFolder結構
data_dir = "./data/hymenoptera_data"

# 從[resnet, alexnet, vgg, squeezenet, densenet, inception]中選擇模型
model_name = "squeezenet"

# 數據集中類別數量
num_classes = 2

# 訓練的批量大小(根據您的內存量而變化)
batch_size = 8

# 你要訓練的epoch數
num_epochs = 15

# 用於特征提取的標志。 當為False時,我們微調整個模型,
# 當True時我們只更新重新形成的圖層參數
feature_extract = True

3.輔助函數

在編寫調整模型的代碼之前,我們先定義一些輔助函數。

3.1 模型訓練和驗證代碼

train_model函數處理給定模型的訓練和驗證。作為輸入,它需要PyTorch模型、數據加載器字典、損失函數、優化器、用於訓練和驗 證epoch數,以及當模型是初始模型時的布爾標志。is_inception標志用於容納 Inception v3 模型,因為該體系結構使用輔助輸出, 並且整體模型損失涉及輔助輸出和最終輸出,如此處所述。 這個函數訓練指定數量的epoch,並且在每個epoch之后運行完整的驗證步驟。它還跟蹤最佳性能的模型(從驗證准確率方面),並在訓練 結束時返回性能最好的模型。在每個epoch之后,打印訓練和驗證正確率。

def train_model(model, dataloaders, criterion, optimizer, num_epochs=25, is_inception=False):
    since = time.time()

    val_acc_history = []

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 每個epoch都有一個訓練和驗證階段
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            # 迭代數據
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # 零參數梯度
                optimizer.zero_grad()

                # 前向
                # 如果只在訓練時則跟蹤軌跡
                with torch.set_grad_enabled(phase == 'train'):
                    # 獲取模型輸出並計算損失
                    # 開始的特殊情況,因為在訓練中它有一個輔助輸出。
                    # 在訓練模式下,我們通過將最終輸出和輔助輸出相加來計算損耗
                    # 但在測試中我們只考慮最終輸出。
                    if is_inception and phase == 'train':
                        # From https://discuss.pytorch.org/t/how-to-optimize-inception-model-with-auxiliary-classifiers/7958
                        outputs, aux_outputs = model(inputs)
                        loss1 = criterion(outputs, labels)
                        loss2 = criterion(aux_outputs, labels)
                        loss = loss1 + 0.4*loss2
                    else:
                        outputs = model(inputs)
                        loss = criterion(outputs, labels)

                    _, preds = torch.max(outputs, 1)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # 統計
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
            if phase == 'val':
                val_acc_history.append(epoch_acc)

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model, val_acc_history

3.2 設置模型參數的`.requires_grad`屬性

當我們進行特征提取時,此輔助函數將模型中參數的 .requires_grad 屬性設置為False。默認情況下,當我們加載一個預訓練模型時,所有參數都是 .requires_grad = True,如果我們從頭開始訓練或微調,這種設置就沒問題。但是,如果我們要運行特征提取並且只想為新初始化的層計算梯度,那么我們希望所有其他參數不需要梯度變化。這將在稍后更能理解。

def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

4.初始化和重塑網絡

現在來到最有趣的部分。在這里我們對每個網絡進行重塑。請注意,這不是一個自動過程,並且對每個模型都是唯一的。回想一下,CNN模型的最后一層(通常是FC層)與數據集中的輸出類的數量具有相同的節點數。由於所有模型都已在 Imagenet 上預先訓練,因此它們都具有大小為1000的輸出層,每個類一個節點。這里的目標是將最后一層重塑為與之前具有相同數量的輸入,並且具有與數據集中的類別數相同的輸出數。在以下部分中,我們將討論如何更改每個模型的體系結構。但首先,有一個關於微調和特征提取之間差異的重要細節。

當進行特征提取時,我們只想更新最后一層的參數,換句話說,我們只想更新我們正在重塑層的參數。因此,我們不需要計算不需要改變的參數的梯度,因此為了提高效率,我們將其它層的.requires_grad屬性設置為False。這很重要,因為默認情況下,此屬性設置為True。然后,當我們初始化新層時,默認情況下新參數.requires_grad = True,因此只更新新層的參數。當我們進行微調時,我們可以將所有.required_grad設置為默認值True。

最后,請注意inception_v3的輸入大小為(299,299),而所有其他模型都輸入為(224,224)。

4.1 Resnet

論文Deep Residual Learning for Image Recognition介紹了Resnet模型。有幾種不同尺寸的變體,包括Resnet18、Resnet34、Resnet50、Resnet101和Resnet152,所有這些模型都可以從 torchvision 模型中獲得。因為我們的數據集很小,只有兩個類,所以我們使用Resnet18。當我們打印這個模型時,我們看到最后一層是全連接層,如下所示:

(fc): Linear(in_features=512, out_features=1000, bias=True)

因此,我們必須將model.fc重新初始化為具有512個輸入特征和2個輸出特征的線性層:

model.fc = nn.Linear(512, num_classes)

4.2 Alexnet

Alexnet在論文ImageNet Classification with Deep Convolutional Neural Networks中被介紹,是ImageNet數據集上第一個非常成功的CNN。當我們打印模型架構時,我們看到模型輸出為分類器的第6層:

(classifier): Sequential(
    ...
    (6): Linear(in_features=4096, out_features=1000, bias=True)
 )

要在我們的數據集中使用這個模型,我們將此圖層重新初始化為:

model.classifier[6] = nn.Linear(4096,num_classes)

4.3 VGG

VGG在論文Very Deep Convolutional Networks for Large-Scale Image Recognition中被引入。Torchvision 提供了8種不同長度的VGG版本,其中一些版本具有批標准化層。這里我們使用VGG-11進行批標准化。輸出層與Alexnet類似,即

(classifier): Sequential(
    ...
    (6): Linear(in_features=4096, out_features=1000, bias=True)
 )

因此,我們使用相同的方法來修改輸出層

model.classifier[6] = nn.Linear(4096,num_classes)

4.4 Squeezenet

論文SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size描述了 Squeeznet 架構,使用了與此處顯示的任何其他模型不同的輸出結構。Torchvision 的 Squeezenet 有兩個版本,我們使用1.0版本。輸出來自1x1卷積層,它是分類器的第一層:

(classifier): Sequential(
    (0): Dropout(p=0.5)
    (1): Conv2d(512, 1000, kernel_size=(1, 1), stride=(1, 1))
    (2): ReLU(inplace)
    (3): AvgPool2d(kernel_size=13, stride=1, padding=0)
 )

為了修改網絡,我們重新初始化Conv2d層,使輸出特征圖深度為2

model.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))

4.5 Densenet

論文Densely Connected Convolutional Networks引入了Densenet模型。Torchvision 有四種Densenet 變型,但在這里我們只使用 Densenet-121。輸出層是一個具有1024個輸入特征的線性層:

(classifier): Linear(in_features=1024, out_features=1000, bias=True)

為了重塑這個網絡,我們將分類器的線性層重新初始化為

model.classifier = nn.Linear(1024, num_classes)

4.6 Inception v3

Inception v3首先在論文Rethinking the Inception Architecture for Computer Vision中描述。該網絡的獨特之處在於它在訓練時有兩個輸出層。第二個輸出稱為輔助輸出,包含在網絡的 AuxLogits 部分中。主輸出是網絡末端的線性層。注意,測試時我們只考慮主輸出。加載模型的輔助輸出和主輸出打印為:

(AuxLogits): InceptionAux(
    ...
    (fc): Linear(in_features=768, out_features=1000, bias=True)
 )
 ...
(fc): Linear(in_features=2048, out_features=1000, bias=True)

要微調這個模型,我們必須重塑這兩個層。可以通過以下方式完成

model.AuxLogits.fc = nn.Linear(768, num_classes)
model.fc = nn.Linear(2048, num_classes)

請注意,許多模型具有相似的輸出結構,但每個模型的處理方式略有不同。另外,請查看重塑網絡的模型體系結構,並確保輸出特征數與數據集中的類別數相同。

4.7 重塑代碼

def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):
    # 初始化將在此if語句中設置的這些變量。 
    # 每個變量都是模型特定的。
    model_ft = None
    input_size = 0

    if model_name == "resnet":
        """ Resnet18
 """
        model_ft = models.resnet18(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs, num_classes)
        input_size = 224

    elif model_name == "alexnet":
        """ Alexnet
 """
        model_ft = models.alexnet(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "vgg":
        """ VGG11_bn
 """
        model_ft = models.vgg11_bn(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "squeezenet":
        """ Squeezenet
 """
        model_ft = models.squeezenet1_0(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
        model_ft.num_classes = num_classes
        input_size = 224

    elif model_name == "densenet":
        """ Densenet
 """
        model_ft = models.densenet121(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier.in_features
        model_ft.classifier = nn.Linear(num_ftrs, num_classes)
        input_size = 224

    elif model_name == "inception":
        """ Inception v3
 Be careful, expects (299,299) sized images and has auxiliary output
 """
        model_ft = models.inception_v3(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        # 處理輔助網絡
        num_ftrs = model_ft.AuxLogits.fc.in_features
        model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
        # 處理主要網絡
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs,num_classes)
        input_size = 299

    else:
        print("Invalid model name, exiting...")
        exit()

    return model_ft, input_size

# 在這步中初始化模型
model_ft, input_size = initialize_model(model_name, num_classes, feature_extract, use_pretrained=True)

# 打印我們剛剛實例化的模型
print(model_ft)
  • 輸出結果

SqueezeNet(
  (features): Sequential(
    (0): Conv2d(3, 96, kernel_size=(7, 7), stride=(2, 2))
    (1): ReLU(inplace)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
    (3): Fire(
      (squeeze): Conv2d(96, 16, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(16, 64, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(16, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (4): Fire(
      (squeeze): Conv2d(128, 16, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(16, 64, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(16, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (5): Fire(
      (squeeze): Conv2d(128, 32, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(32, 128, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(32, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (6): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
    (7): Fire(
      (squeeze): Conv2d(256, 32, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(32, 128, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(32, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (8): Fire(
      (squeeze): Conv2d(256, 48, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(48, 192, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(48, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (9): Fire(
      (squeeze): Conv2d(384, 48, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(48, 192, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(48, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (10): Fire(
      (squeeze): Conv2d(384, 64, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
    (11): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
    (12): Fire(
      (squeeze): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1))
      (squeeze_activation): ReLU(inplace)
      (expand1x1): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1))
      (expand1x1_activation): ReLU(inplace)
      (expand3x3): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (expand3x3_activation): ReLU(inplace)
    )
  )
  (classifier): Sequential(
    (0): Dropout(p=0.5)
    (1): Conv2d(512, 2, kernel_size=(1, 1), stride=(1, 1))
    (2): ReLU(inplace)
    (3): AdaptiveAvgPool2d(output_size=(1, 1))
  )
)

5.加載數據

現在我們知道輸入尺寸大小必須是什么,我們可以初始化數據轉換,圖像數據集和數據加載器。請注意,模型是使用硬編碼標准化值進行預先訓練的,如這里所述。

# 數據擴充和訓練規范化
# 只需驗證標准化
data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(input_size),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(input_size),
        transforms.CenterCrop(input_size),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

print("Initializing Datasets and Dataloaders...")

# 創建訓練和驗證數據集
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'val']}
# 創建訓練和驗證數據加載器
dataloaders_dict = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True, num_workers=4) for x in ['train', 'val']}

# 檢測我們是否有可用的GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  • 輸出結果

Initializing Datasets and Dataloaders...

6.創建優化器

現在模型結構是正確的,微調和特征提取的最后一步是創建一個只更新所需參數的優化器。回想一下,在加載預訓練模型之后,但在重塑之前,如果feature_extract = True,我們手動將所有參數的.requires_grad屬性設置為False。然后重新初始化默認為.requires_grad = True的網絡層參數。所以現在我們知道應該優化所有具有.requires_grad = True的參數。接下來,我們列出這些參數並將此列表輸入到 SGD 算法構造器。

要驗證這一點,可以查看要學習的參數。微調時,此列表應該很長並包含所有模型參數。但是,當進行特征提取時,此列表應該很短並且僅包括重塑層的權重和偏差。

# 將模型發送到GPU
model_ft = model_ft.to(device)

# 在此運行中收集要優化/更新的參數。 
# 如果我們正在進行微調,我們將更新所有參數。 
# 但如果我們正在進行特征提取方法,我們只會更新剛剛初始化的參數,即`requires_grad`的參數為True。
params_to_update = model_ft.parameters()
print("Params to learn:")
if feature_extract:
    params_to_update = []
    for name,param in model_ft.named_parameters():
        if param.requires_grad == True:
            params_to_update.append(param)
            print("\t",name)
else:
    for name,param in model_ft.named_parameters():
        if param.requires_grad == True:
            print("\t",name)

# 觀察所有參數都在優化
optimizer_ft = optim.SGD(params_to_update, lr=0.001, momentum=0.9)

*輸出結果

Params to learn:
         classifier.1.weight
         classifier.1.bias

7.運行訓練和驗證

最后一步是為模型設置損失,然后對設定的epoch數運行訓練和驗證函數。請注意,取決於epoch的數量,此步驟在CPU上可能需要執行一段時間。此外,默認的學習率對所有模型都不是最佳的,因此為了獲得最大精度,有必要分別調整每個模型。

# 設置損失函數
criterion = nn.CrossEntropyLoss()

# Train and evaluate
model_ft, hist = train_model(model_ft, dataloaders_dict, criterion, optimizer_ft, num_epochs=num_epochs, is_inception=(model_name=="inception"))
  • 輸出結果

Epoch 0/14
----------
train Loss: 0.5066 Acc: 0.7336
val Loss: 0.3781 Acc: 0.8693

Epoch 1/14
----------
train Loss: 0.3227 Acc: 0.8893
val Loss: 0.3254 Acc: 0.8889

Epoch 2/14
----------
train Loss: 0.2080 Acc: 0.9057
val Loss: 0.3137 Acc: 0.9216

Epoch 3/14
----------
train Loss: 0.2211 Acc: 0.9262
val Loss: 0.3126 Acc: 0.9020

Epoch 4/14
----------
train Loss: 0.1523 Acc: 0.9426
val Loss: 0.3000 Acc: 0.9085

Epoch 5/14
----------
train Loss: 0.1480 Acc: 0.9262
val Loss: 0.3167 Acc: 0.9150

Epoch 6/14
----------
train Loss: 0.1943 Acc: 0.9221
val Loss: 0.3129 Acc: 0.9216

Epoch 7/14
----------
train Loss: 0.1247 Acc: 0.9549
val Loss: 0.3139 Acc: 0.9150

Epoch 8/14
----------
train Loss: 0.1825 Acc: 0.9098
val Loss: 0.3336 Acc: 0.9150

Epoch 9/14
----------
train Loss: 0.1436 Acc: 0.9303
val Loss: 0.3295 Acc: 0.9281

Epoch 10/14
----------
train Loss: 0.1419 Acc: 0.9303
val Loss: 0.3548 Acc: 0.8889

Epoch 11/14
----------
train Loss: 0.1407 Acc: 0.9549
val Loss: 0.2953 Acc: 0.9216

Epoch 12/14
----------
train Loss: 0.0900 Acc: 0.9713
val Loss: 0.3457 Acc: 0.9216

Epoch 13/14
----------
train Loss: 0.1283 Acc: 0.9467
val Loss: 0.3451 Acc: 0.9281

Epoch 14/14
----------
train Loss: 0.0975 Acc: 0.9508
val Loss: 0.3381 Acc: 0.9281

Training complete in 0m 20s
Best val Acc: 0.928105

8.對比從頭開始模型

這部分內容出於好奇心理,看看如果我們不使用遷移學習,模型將如何學習。微調與特征提取的性能在很大程度上取決於數據集,但一般而言,兩種遷移學習方法相對於從頭開始訓練模型,在訓練時間和總體准確性方面產生了良好的結果。

# 初始化用於此運行的模型的非預訓練版本
scratch_model,_ = initialize_model(model_name, num_classes, feature_extract=False, use_pretrained=False)
scratch_model = scratch_model.to(device)
scratch_optimizer = optim.SGD(scratch_model.parameters(), lr=0.001, momentum=0.9)
scratch_criterion = nn.CrossEntropyLoss()
_,scratch_hist = train_model(scratch_model, dataloaders_dict, scratch_criterion, scratch_optimizer, num_epochs=num_epochs, is_inception=(model_name=="inception"))

# 繪制驗證精度的訓練曲線與轉移學習方法
# 和從頭開始訓練的模型的訓練epochs的數量
ohist = []
shist = []

ohist = [h.cpu().numpy() for h in hist]
shist = [h.cpu().numpy() for h in scratch_hist]

plt.title("Validation Accuracy vs. Number of Training Epochs")
plt.xlabel("Training Epochs")
plt.ylabel("Validation Accuracy")
plt.plot(range(1,num_epochs+1),ohist,label="Pretrained")
plt.plot(range(1,num_epochs+1),shist,label="Scratch")
plt.ylim((0,1.))
plt.xticks(np.arange(1, num_epochs+1, 1.0))
plt.legend()
plt.show()
  • 輸出結果

    640?wx_fmt=png

Epoch 0/14
----------
train Loss: 0.7131 Acc: 0.4959
val Loss: 0.6931 Acc: 0.4575

Epoch 1/14
----------
train Loss: 0.6930 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 2/14
----------
train Loss: 0.6932 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 3/14
----------
train Loss: 0.6932 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 4/14
----------
train Loss: 0.6931 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 5/14
----------
train Loss: 0.6929 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 6/14
----------
train Loss: 0.6931 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 7/14
----------
train Loss: 0.6918 Acc: 0.5041
val Loss: 0.6934 Acc: 0.4575

Epoch 8/14
----------
train Loss: 0.6907 Acc: 0.5041
val Loss: 0.6932 Acc: 0.4575

Epoch 9/14
----------
train Loss: 0.6914 Acc: 0.5041
val Loss: 0.6927 Acc: 0.4575

Epoch 10/14
----------
train Loss: 0.6851 Acc: 0.5041
val Loss: 0.6946 Acc: 0.4575

Epoch 11/14
----------
train Loss: 0.6841 Acc: 0.5041
val Loss: 0.6942 Acc: 0.4575

Epoch 12/14
----------
train Loss: 0.6778 Acc: 0.5041
val Loss: 0.7228 Acc: 0.4575

Epoch 13/14
----------
train Loss: 0.6874 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Epoch 14/14
----------
train Loss: 0.6931 Acc: 0.5041
val Loss: 0.6931 Acc: 0.4575

Training complete in 0m 30s
Best val Acc: 0.457516

9.總結展望

嘗試運行其他模型,看看可以得到多好的正確率。另外,請注意特征提取花費的時間較少,因為在后向傳播中我們不需要計算大部分的梯度。還有很多地方可以嘗試。例如:

  • 在更難的數據集上運行此代碼,查看遷移學習的更多好處。

  • 在新的領域(比如NLP,音頻等)中,使用此處描述的方法,使用遷移學習更新不同的模型。

  • 一旦您對一個模型感到滿意,可以將其導出為 ONNX 模型,或使用混合前端跟蹤它以獲得更快的速度和優化的機會。

 

PyTorch 入門視頻教程,歡迎B站觀看,收藏,點贊:

https://www.bilibili.com/video/av66421076

 

歡迎關注磐創博客資源匯總站:
http://docs.panchuang.net/

歡迎關注PyTorch官方中文教程站:
http://pytorch.panchuang.net/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM