天天看點

Torchvision模型微調

Torchvision模型微調

本文将深入探讨如何對 torchvision 模型進行微調和特征提取,所有這些模型都已經預先在1000類的magenet資料集上訓練完成。将深入介紹如何使用幾個現代的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之後,列印訓練和驗證正确率。 ```buildoutcfg 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 =

                _, 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`,如果從頭開始訓練或微調,這種設定就沒問題。

但是,如果要運作特征提取,隻想為新初始化的層計算梯度,那麼希望所有其它參數不需要梯度變化。這将在稍後更能了解。

```buildoutcfg

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類似,即

是以,使用相同的方法來修改輸出層

4.4 Squeezenet

論文SqueezeNet:

AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size 描述了 Squeeznet 架構,使用了與此處顯示的任何其他模型不同的輸出結構。Torchvision

的 Squeezenet 有兩個版本,使用1.0版本。

輸出來自1x1卷積層,它是分類器的第一層:

    (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,

 ...

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

models.alexnet(pretrained=use_pretrained)

model_ft.classifier[6].in_features

        model_ft.classifier[6] =

nn.Linear(num_ftrs,num_classes)

    elif model_name == "vgg":

VGG11_bn

models.vgg11_bn(pretrained=use_pretrained)

    elif model_name == "squeezenet":

Squeezenet

        model_ft = models.squeezenet1_0(pretrained=use_pretrained)

        model_ft.classifier[1] = nn.Conv2d(512, num_classes,

kernel_size=(1,1), stride=(1,1))

        model_ft.num_classes =

num_classes

    elif model_name == "densenet":

Densenet

models.densenet121(pretrained=use_pretrained)

model_ft.classifier.in_features

        model_ft.classifier =

    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)

        # 處理輔助網絡

model_ft.AuxLogits.fc.in_features

        model_ft.AuxLogits.fc =

        # 處理主要網絡

        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):

      (expand3x3): Conv2d(16, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

      (expand3x3_activation):

    )

    (4): Fire(

      (squeeze): Conv2d(128, 16, kernel_size=(1, 1), stride=(1, 1))

    (5): Fire(

      (squeeze): Conv2d(128, 32, kernel_size=(1, 1), stride=(1, 1))

      (expand1x1): Conv2d(32, 128, kernel_size=(1, 1), stride=(1, 1))

      (expand3x3): Conv2d(32, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

    (6):

    (7): Fire(

      (squeeze): Conv2d(256, 32, kernel_size=(1, 1), stride=(1, 1))

      (squeeze_activation): ReLU(inplace)

    (8): Fire(

      (squeeze): Conv2d(256, 48, kernel_size=(1, 1), stride=(1, 1))

      (expand1x1): Conv2d(48, 192, kernel_size=(1, 1), stride=(1, 1))

      (expand3x3): Conv2d(48, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

    (9): Fire(

      (squeeze): Conv2d(384, 48, kernel_size=(1, 1), stride=(1, 1))

    (10): Fire(

      (squeeze): Conv2d(384, 64, kernel_size=(1, 1), stride=(1, 1))

      (expand1x1): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1))

      (expand3x3): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

    (11):

    (12): Fire(

      (squeeze): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1))

  )

  (classifier): Sequential(

    (1): Conv2d(512, 2, kernel_size=(1, 1), stride=(1, 1))

    (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),

}

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:

# 觀察所有參數都在優化

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()

  • 輸出結果 
  • Torchvision模型微調

train Loss: 0.7131 Acc: 0.4959

val Loss: 0.6931 Acc: 0.4575

train Loss: 0.6930 Acc: 0.5041

train Loss: 0.6932 Acc: 0.5041

train Loss: 0.6931 Acc: 0.5041

train Loss: 0.6929 Acc: 0.5041

train Loss: 0.6918 Acc: 0.5041

val Loss: 0.6934 Acc: 0.4575

train Loss: 0.6907 Acc: 0.5041

val Loss: 0.6932 Acc: 0.4575

train Loss: 0.6914 Acc: 0.5041

val Loss: 0.6927 Acc: 0.4575

train Loss: 0.6851 Acc: 0.5041

val Loss: 0.6946 Acc: 0.4575

train Loss: 0.6841 Acc: 0.5041

val Loss: 0.6942 Acc: 0.4575

train Loss: 0.6778 Acc: 0.5041

val Loss: 0.7228 Acc: 0.4575

train Loss: 0.6874 Acc: 0.5041

Training complete in 0m 30s

Best val Acc: 0.457516

  • 在更難的資料集上運作此代碼,檢視遷移學習的更多好處。
  • 在新的領域(比如NLP,音頻等)中,使用此處描述的方法,使用遷移學習更新不同的模型。
  • 一旦對一個模型感到滿意,可以将其導出為 ONNX 模型,或使用混合前端跟蹤它以獲得更快的速度和優化的機會。

人工智能晶片與自動駕駛