遷移學習應用

 

轉移學習:微調和特徵提取

在*微調*中,我們從預訓練模型開始,更新我們新任務的所有模型參數,實質上是重新訓練整個模
型。
在*特徵提取*中,我們從預訓練模型開始,僅更新從中導出預測的最終圖層權重。它被稱爲特徵提
取,因爲我們使用預訓練的CNN作爲固定 的特徵提取器,並且僅改變輸出層。

這兩種遷移學習方法都遵循以下幾個步驟:
初始化預訓練模型
重組最後一層,使其具有與新數據集類別數相同的輸出數
爲優化算法定義我們想要在訓練期間更新的參數
運行訓練步驟

 

導包:

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

數據集:https://download.pytorch.org/tutorial/hymenoptera_data.zip

以下爲運行時需要更改的所有參數。我們將使用的數據集 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 = 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

設置模型參數的 .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

初始化和重塑網絡
現在來到最有趣的部分。在這裏我們對每個網絡進行重塑。請注意,這不是一個自動過程,並且
對每個模型都是唯一的。 回想一下,CNN模型的最後一層(通常是FC層)與數據集中的輸出類的
數量具有相同的節點數。由於所有模型都已在 Imagenet 上預先訓練, 因此它們都具有大小爲
1000的輸出層,每個類一個節點。這裏的目標是將最後一層重塑爲與之前具有相同數量的輸入,
並且具有與數據集 中的類別數相同的輸出數。在以下部分中,我們將討論如何更改每個模型的體
繫結構。但首先,有一個關於微調和特徵提取之間差異的重要細節。
當進行特徵提取時,我們只想更新最後一層的參數,換句話說,我們只想更新我們正在重塑層的
參數。因此,我們不需要計算不需要改變 的參數的梯度,因此爲了提高效率,我們將其它層的
.requires_grad 屬性設置爲False。這很重要,因爲默認情況下,此屬性設置爲True。 然後,當
我們初始化新層時,默認情況下新參數 .requires_grad = True ,因此只更新新層的參數。當我
們進行微調時,我們可以將所有 .required_grad 設置爲默認值True

 

4.1 Resnet
論文Deep Residual Learning for Image Recognition介紹了Resnet模型。有幾種不同尺寸的變體,
包括Resnet18、Resnet34、Resnet50、Resnet101和Resnet152,所有這些模型都可以從
torchvision 模型中獲得。因爲我們的數據集很小, 只有兩個類,所以我們使用Resnet18。 當我們
打印這個模型時,我們看到最後一層是全連接層,如下所示:
因此,我們必須將 model.fc 重新初始化爲具有512個輸入特徵和2個輸出特徵的線性層:
4.2 Alexnet
Alexnet在論文ImageNet Classification with Deep Convolutional Neural Networks 中被介紹,是
ImageNet數據集上第一個非常成功的CNN。當我們打印模型架構時,我們看到模型輸出爲分類器
的第6層:
要在我們的數據集中使用這個模型,我們將此圖層重新初始化爲:
4.3 VGG
VGG在論文Very Deep Convolutional Networks for Large-Scale Image Recognition 中被引入。
Torchvision 提供了8種不同長度的VGG版本,其中一些版本具有批標準化層。這裏我們使用
VGG-11進行批標準化。 輸出層與Alexnet類似,即
(fc): Linear(in_features=512, out_features=1000, bias=True)
model.fc = nn.Linear(512, num_classes)
(classifier): Sequential(
...
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
model.classifier[6] = nn.Linear(4096,num_classes)
(classifier): Sequential(
...
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
© 2018 Tangramor 4.1 Resnet 頁:6
因此,我們使用相同的方法來修改輸出層
4.4 Squeezenet
論文SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size 描述
了 Squeeznet 架構,使用了與此處顯示的任何其他模型不同的輸出結構。Torchvision 的
Squeezenet 有兩個版本,我們使用1.0版本。 輸出來自1x1卷積層,它是分類器的第一層:
爲了修改網絡,我們重新初始化Conv2d層,使輸出特徵圖深度爲2
4.5 Densenet
論文Densely Connected Convolutional Networks引入了Densenet模型。Torchvision 有四種
Densenet 變型,但在這裏我們只使用 Densenet-121。輸出層是一個具有1024個輸入特徵的線性
層:
爲了重塑這個網絡,我們將分類器的線性層重新初始化爲
4.6 Inception v3
Inception v3首先在論文Rethinking the Inception Architecture for Computer Vision 中描述。該網絡
的獨特之處在於它在訓練時有兩個輸出層。第二個輸出稱爲輔助輸出,包含在網絡的 AuxLogits
部分中。主輸出是網絡末端的線性層。 注意,測試時我們只考慮主輸出。加載模型的輔助輸出和
主輸出打印爲:
model.classifier[6] = nn.Linear(4096,num_classes)
(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)
)
model.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
(classifier): Linear(in_features=1024, out_features=1000, bias=True)
model.classifier = nn.Linear(1024, num_classes)
© 2018 Tangramor 4.1 Resnet 頁:7
要微調這個模型,我們必須重塑這兩個層。可以通過以下方式完成
請注意,許多模型具有相似的輸出結構,但每個模型的處理方式略有不同。另外,請查看重塑網
絡的模型體系結構,並確保輸出特徵數與 數據集中的類別數相同。

 

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_b"""
		 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)

數據加載

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

# 數據擴充和訓練規範化
# 只需驗證標準化
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")

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

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

結果:

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

。。。。。。

9.總結展望 嘗試運行其他模型,看看可以得到多好的正確率。另外,請注意特徵提取花費的時間較少,因爲 在後向傳播中我們不需要計算大部分的梯度。 還有很多地方可以嘗試。例如: 在更難的數據集上運行此代碼,查看遷移學習的更多好處。 在新的領域(比如NLP,音頻等)中,使用此處描述的方法,使用遷移學習更新不同的模型。 一旦您對一個模型感到滿意,可以將其導出爲 ONNX 模型,或使用混合前端跟蹤它以獲得更 快的速度和優化的機會

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章