pytorch-模型微調

9.2 微調

在前面的一些章節中,我們介紹瞭如何在只有6萬張圖像的Fashion-MNIST訓練數據集上訓練模型。我們還描述了學術界當下使用最廣泛的大規模圖像數據集ImageNet,它有超過1,000萬的圖像和1,000類的物體。然而,我們平常接觸到數據集的規模通常在這兩者之間。

假設我們想從圖像中識別出不同種類的椅子,然後將購買鏈接推薦給用戶。一種可能的方法是先找出100種常見的椅子,爲每種椅子拍攝1,000張不同角度的圖像,然後在收集到的圖像數據集上訓練一個分類模型。這個椅子數據集雖然可能比Fashion-MNIST數據集要龐大,但樣本數仍然不及ImageNet數據集中樣本數的十分之一。這可能會導致適用於ImageNet數據集的複雜模型在這個椅子數據集上過擬合。同時,因爲數據量有限,最終訓練得到的模型的精度也可能達不到實用的要求。

爲了應對上述問題,一個顯而易見的解決辦法是收集更多的數據。然而,收集和標註數據會花費大量的時間和資金。例如,爲了收集ImageNet數據集,研究人員花費了數百萬美元的研究經費。雖然目前的數據採集成本已降低了不少,但其成本仍然不可忽略。

另外一種解決辦法是應用遷移學習(transfer learning),將從源數據集學到的知識遷移到目標數據集上。例如,雖然ImageNet數據集的圖像大多跟椅子無關,但在該數據集上訓練的模型可以抽取較通用的圖像特徵,從而能夠幫助識別邊緣、紋理、形狀和物體組成等。這些類似的特徵對於識別椅子也可能同樣有效。

本節我們介紹遷移學習中的一種常用技術:微調(fine tuning)。如圖9.1所示,微調由以下4步構成。

  1. 在源數據集(如ImageNet數據集)上預訓練一個神經網絡模型,即源模型。
  2. 創建一個新的神經網絡模型,即目標模型。它複製了源模型上除了輸出層外的所有模型設計及其參數。我們假設這些模型參數包含了源數據集上學習到的知識,且這些知識同樣適用於目標數據集。我們還假設源模型的輸出層跟源數據集的標籤緊密相關,因此在目標模型中不予採用。
  3. 爲目標模型添加一個輸出大小爲目標數據集類別個數的輸出層,並隨機初始化該層的模型參數。
  4. 在目標數據集(如椅子數據集)上訓練目標模型。我們將從頭訓練輸出層,而其餘層的參數都是基於源模型的參數微調得到的。

Image Name

當目標數據集遠小於源數據集時,微調有助於提升模型的泛化能力。

9.2.1 熱狗識別

接下來我們來實踐一個具體的例子:熱狗識別。我們將基於一個小數據集對在ImageNet數據集上訓練好的ResNet模型進行微調。該小數據集含有數千張包含熱狗和不包含熱狗的圖像。我們將使用微調得到的模型來識別一張圖像中是否包含熱狗。

首先,導入實驗所需的包或模塊。torchvision的models包提供了常用的預訓練模型。如果希望獲取更多的預訓練模型,可以使用使用pretrained-models.pytorch倉庫。

%matplotlib inline
# 導入包
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision.datasets import ImageFolder
from torchvision import transforms
from torchvision import models#預訓練模型
import os

import sys

sys.path.append("/home/kesci/input/")
import d2lzh1981 as d2l

os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

9.2.1.1 獲取數據集

我們使用的熱狗數據集(點擊下載)是從網上抓取的,它含有1400張包含熱狗的正類圖像,和同樣多包含其他食品的負類圖像。各類的1000張圖像被用於訓練,其餘則用於測試。

我們首先將壓縮後的數據集下載到路徑data_dir之下,然後在該路徑將下載好的數據集解壓,得到兩個文件夾hotdog/trainhotdog/test。這兩個文件夾下面均有hotdognot-hotdog兩個類別文件夾,每個類別文件夾裏面是圖像文件。

import os
os.listdir('/home/kesci/input/resnet185352')
['resnet18-5c106cde.pth']
data_dir = '/home/kesci/input/hotdog4014'
os.listdir(os.path.join(data_dir, "hotdog"))
['test', 'train']

我們創建兩個ImageFolder實例來分別讀取訓練數據集和測試數據集中的所有圖像文件。

train_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/train'))
test_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/test'))

下面畫出前8張正類圖像和最後8張負類圖像。可以看到,它們的大小和高寬比各不相同。

hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4)
print(transforms.ToTensor()(train_imgs[0][0]))
tensor([[[0.5961, 0.2941, 0.5294,  ..., 0.6706, 0.6667, 0.6667],
         [0.4510, 0.4706, 0.4314,  ..., 0.6588, 0.6667, 0.6784],
         [0.2667, 0.2471, 0.3529,  ..., 0.6588, 0.6588, 0.6706],
         ...,
         [0.1608, 0.5294, 0.7059,  ..., 0.5569, 0.5333, 0.6157],
         [0.6667, 0.4706, 0.4353,  ..., 0.6706, 0.5137, 0.4863],
         [0.6314, 0.5451, 0.6392,  ..., 0.5647, 0.6314, 0.5843]],

        [[0.5529, 0.2510, 0.4784,  ..., 0.6667, 0.6627, 0.6627],
         [0.4078, 0.4275, 0.4000,  ..., 0.6588, 0.6667, 0.6784],
         [0.2314, 0.2118, 0.3255,  ..., 0.6588, 0.6588, 0.6667],
         ...,
         [0.1529, 0.5098, 0.6588,  ..., 0.2941, 0.2784, 0.3725],
         [0.3961, 0.2314, 0.2000,  ..., 0.4157, 0.2667, 0.2392],
         [0.3529, 0.2784, 0.3882,  ..., 0.3255, 0.3922, 0.3333]],

        [[0.4667, 0.1647, 0.4039,  ..., 0.6863, 0.6824, 0.6824],
         [0.3373, 0.3569, 0.3255,  ..., 0.6902, 0.6980, 0.7098],
         [0.1725, 0.1529, 0.2627,  ..., 0.6902, 0.6902, 0.6980],
         ...,
         [0.3373, 0.6353, 0.7373,  ..., 0.0784, 0.0588, 0.1490],
         [0.2745, 0.1176, 0.1608,  ..., 0.1961, 0.0588, 0.0314],
         [0.1333, 0.0235, 0.1059,  ..., 0.0706, 0.1373, 0.0824]]])

在訓練時,我們先從圖像中裁剪出隨機大小和隨機高寬比的一塊隨機區域,然後將該區域縮放爲高和寬均爲224像素的輸入。測試時,我們將圖像的高和寬均縮放爲256像素,然後從中裁剪出高和寬均爲224像素的中心區域作爲輸入。此外,我們對RGB(紅、綠、藍)三個顏色通道的數值做標準化:每個數值減去該通道所有數值的平均值,再除以該通道所有數值的標準差作爲輸出。

注: 在使用預訓練模型時,一定要和預訓練時作同樣的預處理。
如果你使用的是torchvisionmodels,那就要求:
All pre-trained models expect input images normalized in the same way, i.e. mini-batches of 3-channel RGB images of shape (3 x H x W), where H and W are expected to be at least 224. The images have to be loaded in to a range of [0, 1] and then normalized using mean = [0.485, 0.456, 0.406] and std = [0.229, 0.224, 0.225].
所有經過預訓練的模型都希望輸入圖像以相同的方式歸一化,即形狀爲(3 x H x W)的3通道RGB圖像的迷你批,其中H和W至少應爲224。 加載到[0,1]的範圍內,然後使用均值= [0.485,0.456,0.406]和std = [0.229,0.224,0.225]進行歸一化。

normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])# 歸一化
train_augs = transforms.Compose([
        transforms.RandomResizedCrop(size=224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        normalize
    ])
 
test_augs = transforms.Compose([
        transforms.Resize(size=256),
        transforms.CenterCrop(size=224),
        transforms.ToTensor(),
        normalize
    ])
# 訓練集合和測試集合進行歸一化

9.2.1.2 定義和初始化模型

我們使用在ImageNet數據集上預訓練的ResNet-18作爲源模型。這裏指定pretrained=True來自動下載並加載預訓練的模型參數。在第一次使用時需要聯網下載模型參數。

pretrained_net = models.resnet18(pretrained=False)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/resnet185352/resnet18-5c106cde.pth'))
# 這裏已經有了,就不用下載了
<All keys matched successfully>

下面打印源模型的成員變量fc。作爲一個全連接層,它將ResNet最終的全局平均池化層輸出變換成ImageNet數據集上1000類的輸出。

print(pretrained_net.fc)
Linear(in_features=512, out_features=1000, bias=True)

注: 如果你使用的是其他模型,那可能沒有成員變量fc(比如models中的VGG預訓練模型),所以正確做法是查看對應模型源碼中其定義部分,這樣既不會出錯也能加深我們對模型的理解。pretrained-models.pytorch倉庫貌似統一了接口,但是我還是建議使用時查看一下對應模型的源碼。

可見此時pretrained_net最後的輸出個數等於目標數據集的類別數1000。所以我們應該將最後的fc成修改我們需要的輸出類別數:

pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)
# fc層,全連接層,輸出層
Linear(in_features=512, out_features=2, bias=True)

此時,pretrained_netfc層就被隨機初始化了,但是其他層依然保存着預訓練得到的參數。由於是在很大的ImageNet數據集上預訓練的,所以參數已經足夠好,因此一般只需使用較小的學習率來微調這些參數,而fc中的隨機初始化參數一般需要更大的學習率從頭訓練。PyTorch可以方便的對模型的不同部分設置不同的學習參數,我們在下面代碼中將fc的學習率設爲已經預訓練過的部分的10倍。

output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id(p) not in output_params, pretrained_net.parameters())

lr = 0.01
optimizer = optim.SGD([{'params': feature_params},
                       {'params': pretrained_net.fc.parameters(), 'lr': lr * 10}],
                       lr=lr, weight_decay=0.001)
                       # 最後一層fc的訓練步長要更長

9.2.1.3 微調模型

def train_fine_tuning(net, optimizer, batch_size=128, num_epochs=5):
    train_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/train'), transform=train_augs),
                            batch_size, shuffle=True)
    test_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/test'), transform=test_augs),
                           batch_size)
    loss = torch.nn.CrossEntropyLoss()
    d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
train_fine_tuning(pretrained_net, optimizer)
training on  cpu
epoch 1, loss 3.4516, train acc 0.687, test acc 0.884, time 298.2 sec
epoch 2, loss 0.1550, train acc 0.924, test acc 0.895, time 296.2 sec
epoch 3, loss 0.1028, train acc 0.903, test acc 0.950, time 295.0 sec
epoch 4, loss 0.0495, train acc 0.931, test acc 0.897, time 294.0 sec
epoch 5, loss 0.1454, train acc 0.878, test acc 0.939, time 291.0 sec

作爲對比,我們定義一個相同的模型,但將它的所有模型參數都初始化爲隨機值。由於整個模型都需要從頭訓練,我們可以使用較大的學習率。

scratch_net = models.resnet18(pretrained=False, num_classes=2)
lr = 0.1
optimizer = optim.SGD(scratch_net.parameters(), lr=lr, weight_decay=0.001)
train_fine_tuning(scratch_net, optimizer)
training on  cpu
epoch 1, loss 2.6391, train acc 0.598, test acc 0.734, time 292.4 sec
epoch 2, loss 0.2703, train acc 0.790, test acc 0.632, time 289.7 sec
epoch 3, loss 0.1584, train acc 0.810, test acc 0.825, time 290.2 sec
epoch 4, loss 0.1177, train acc 0.805, test acc 0.787, time 288.6 sec
epoch 5, loss 0.0782, train acc 0.829, test acc 0.828, time 289.8 sec

輸出:

training on  cuda
epoch 1, loss 2.6686, train acc 0.582, test acc 0.556, time 25.3 sec
epoch 2, loss 0.2434, train acc 0.797, test acc 0.776, time 25.3 sec
epoch 3, loss 0.1251, train acc 0.845, test acc 0.802, time 24.9 sec
epoch 4, loss 0.0958, train acc 0.833, test acc 0.810, time 25.0 sec
epoch 5, loss 0.0757, train acc 0.836, test acc 0.780, time 24.9 sec
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章