引言
? ? ? ? 在深度學習領域,卷積神經網絡(CNN)已經在計算機視覺任務中取得了巨大成功。然而,從頭開始訓練一個高性能的CNN模型需要大量標注數據和計算資源。遷移學習(Transfer Learning)技術為我們提供了一種高效解決方案,它能夠將預訓練模型的知識遷移到新任務中,顯著減少訓練時間和數據需求。本文將全面介紹CNN遷移學習的原理、優勢及實踐方法。
1、內容
? ? ? 遷移學習是指利用已經訓練好的模型,在新的任務上進行微調。遷移學習可以加快模型訓練速度,提高模型性能,并且在數據稀缺的情況下也能很好地工作
2、步驟
1、選擇預訓練的模型和適當的層:通常,我們會選擇在大規模圖像數據集(如ImageNet)上預訓練的模型,如VGG、ResNet等。然后,根據新數據集的特點,選擇需要微調的模型層。對于低級特征的任務(如邊緣檢測),最好使用淺層模型的層,而對于高級特征的任務(如分類),則應選擇更深層次的模型。
2、凍結預訓練模型的參數:保持預訓練模型的權重不變,只訓練新增加的層或者微調一些層,避免因為在數據集中過擬合導致預訓練模型過度擬合。
3、在新數據集上訓練新增加的層:在凍結預訓練模型的參數情況下,訓練新增加的層。這樣,可以使新模型適應新的任務,從而獲得更高的性能。
4、微調預訓練模型的層:在新層上進行訓練后,可以解凍一些已經訓練過的層,并且將它們作為微調的目標。這樣做可以提高模型在新數據集上的性能。
5、評估和測試:在訓練完成之后,使用測試集對模型進行評估。如果模型的性能仍然不夠好,可以嘗試調整超參數或者更改微調層。
Resnet網絡:
原理:
? ? ? ?卷積神經網絡都是通過卷積層和池化層的疊加組成的。 在實際的試驗中發現,隨著卷積層和池化層的疊加,學習效果不會逐漸變好,反而出現2個問題:
1、梯度消失和梯度爆炸
梯度消失:若每一層的誤差梯度小于1,反向傳播時,網絡越深,梯度越趨近于0
梯度爆炸:若每一層的誤差梯度大于1,反向傳播時,網絡越深,梯度越來越大
2、退化問題
? ? ? ?為了解決梯度消失或梯度爆炸問題,論文提出通過數據的預處理以及在網絡中使用 BN(Batch Normalization)層來解決。
? ? ? 為了解決深層網絡中的退化問題,可以人為地讓神經網絡某些層跳過下一層神經元的連接,隔層相連,弱化每層之間的強聯系。這種神經網絡被稱為 殘差網絡 (ResNets)。
1、18層resnet結構:
2、BN(Batch Normalization)
實例
1、導入相關的庫
import torch
from torch.utils.data import DataLoader,Dataset #數據包管理工具,打包數據,
from torchvision import transforms
from torch import nn
import torchvision.models as models
from PIL import Image
import numpy as np
2、調取模型并凍結參數
#不再需要自己來搭建模型了。預訓練的文件也加載進去了。
# 將resnet18模型遷移到食物分類項目中.#殘差網絡是固定的網絡結構,不需要你自己來類定義了。
resnet_model=models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
#weights=models.ResNet18_Weights.DEFAULT表示使用在 ImageNet 數據集上預先訓練好的權重
for param in resnet_model.parameters():print(param)param.requires_grad=False #凍結
#模型所有參數(即權重和偏差)的requires_grad屬性設置為False,從而凍結所有模型參數,
詳細說明:
-
models.resnet18()
加載ResNet18架構 -
weights=models.ResNet18_Weights.DEFAULT
指定使用官方預訓練權重 -
遍歷所有參數并凍結
3、對網絡模型進行微調
in_features=resnet_model.fc.in_features #獲取模型原輸入的特征個數
resnet_model.fc=nn.Linear(in_features,20) #創建一個全連接層
4、保存需要訓練的參數
params_to_update=[] #保存需要訓練的參數,僅僅包含全連接層的參數
for param in resnet_model.parameters():if param.requires_grad==True:params_to_update.append(param)
5、數據預處理
data_transforms={
'train':
transforms.Compose([transforms.Resize([300, 300]),transforms.RandomRotation(45), # 隨機旋轉,-45到45度之間隨機選transforms.CenterCrop(224), # 從中心開始裁剪[256,256]transforms.RandomHorizontalFlip(p=0.5), # 隨機水平翻轉 選擇一個概率概率transforms.RandomVerticalFlip(p=0.5), # 隨機垂直翻轉# transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),transforms.RandomGrayscale(p=0.1), # 概率轉換成灰度率,3通道就是R=G=Btransforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'valid':
transforms.Compose([transforms.Resize([224,224]),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
}
數據預處理包括:
-
訓練集使用多種數據增強(隨機旋轉、水平翻轉等)
-
驗證集只進行簡單的resize和歸一化
-
歸一化參數使用ImageNet的均值和標準差
6、自定義數據集的類
class food_dataset(Dataset):def __init__(self,file_path,transform=None):self.file_path=file_pathself.imgs=[]self.labels=[]self.transform=transformwith open(self.file_path) as f:samples=[x.strip().split(' ') for x in f.readlines()]for img_path,label in samples:self.imgs.append(img_path)self.labels.append(label)def __len__(self):return len(self.imgs)def __getitem__(self, idx):image=Image.open(self.imgs[idx])if self.transform:image=self.transform(image)label = self.labels[idx]label = torch.from_numpy(np.array(label,dtype=np.int64))return image,label
這個自定義Dataset類:
-
從文本文件讀取圖像路徑和標簽
-
實現
__len__
和__getitem__
方法,供DataLoader使用 -
應用指定的transform處理圖像
7、數據加載器準備
# 創建訓練集和測試集實例
training_data = food_dataset(file_path='trainda.txt', transform=data_transforms['train'])
test_data = food_dataset(file_path='testda.txt', transform=data_transforms['valid'])# 創建數據加載器
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
數據加載器提供:
-
批量加載功能(batch_size=64)
-
訓練數據隨機打亂(shuffle=True)
-
多線程數據預讀取
8、訓練和測試流程
def train(dataloader,model,loss_fn,optimizer):model.train() #告訴模型,我要開始訓練,模型中w進行隨機化操作,已經更新w。在訓練過程中,w會被修改的
#pytorch提供2種方式來切換訓練和測試的模式,分別是:model.train()和 model.eval()。
#一般用法是:在訓練開始之前寫上model.trian(),在測試時寫上 model.eval()for X,y in dataloader: #其中batch為每一個數據的編號X,y=X.to(device),y.to(device) #把訓練數據集和標簽傳入cpu或GPUpred=model.forward(X) #.forward可以被省略,父類中已經對次功能進行了設置。自動初始化loss=loss_fn(pred,y) #通過交叉熵損失函數計算損失值loss# Backpropagation 進來一個batch的數據,計算一次梯度,更新一次網絡optimizer.zero_grad() #梯度值清零loss.backward() #反向傳播計算得到每個參數的梯度值woptimizer.step() #根據梯度更新網絡w參數best_acc=0
def test(dataloader, model, loss_fn):global best_accsize = len(dataloader.dataset)num_batches = len(dataloader)model.eval()test_loss, correct = 0, 0with torch.no_grad():for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model.forward(X)test_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss /= num_batchescorrect /= size
9、循環訓練
# 定義損失函數和優化器
loss_fn = nn.CrossEntropyLoss() # 交叉熵損失
optimizer = torch.optim.Adam(params_to_update, lr=0.001) # Adam優化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) # 學習率調度# 訓練10個epoch
epoch = 10
for i in range(epoch):print(f"Epoch {i + 1}")train(train_dataloader, model, loss_fn, optimizer) # 訓練scheduler.step() # 更新學習率test(test_dataloader, model, loss_fn) # 測試print('Best accuracy:', best_acc) # 打印最佳準確率
主循環流程:
-
定義損失函數和優化器
-
設置學習率調度器(每5個epoch學習率減半)
-
進行10輪訓練和測試
-
打印最終最佳準確率
結果展示: