【Pytorch神经网络实战案例】25 (带数据增强)基于迁移学习识别多种鸟类(CUB-200数据集)
1 數(shù)據(jù)增強(qiáng)
在目前分類(lèi)效果最好的EficientNet系列模型中,EfficientNet-B7版本的模型就是使用隨機(jī)數(shù)據(jù)增強(qiáng)方法訓(xùn)練而成的。
RandAugment方法也是目前主流的數(shù)據(jù)增強(qiáng)方法,用RandAugment方法進(jìn)行訓(xùn)練,會(huì)使模型的精度得到提升。
2 RandAugment
2.1 RandAugment方法簡(jiǎn)介
RandAugment方法是一種新的數(shù)據(jù)增強(qiáng)方法,它比自動(dòng)數(shù)據(jù)增強(qiáng)(AutOAugment)方法更簡(jiǎn)單、更好用。它可以在原有的訓(xùn)練框架中,直接對(duì)AutoAugment方法進(jìn)行替換。
2.1.1 Tip
AuoAugment方法包含30多個(gè)參數(shù),可以對(duì)圖片數(shù)據(jù)進(jìn)行各種變換(參見(jiàn)arXiv網(wǎng)站上編號(hào)為1805.09501的論文)。
2.2 RandAugment方法的構(gòu)成
RandAugment方法是在AutoAugment方法的基礎(chǔ)之上,將30多個(gè)參數(shù)進(jìn)行策略級(jí)的優(yōu)化管理,使這30多個(gè)參數(shù)被簡(jiǎn)化成兩個(gè)參數(shù):圖片的N次變換和每次變換的強(qiáng)度M。其中每次變換的強(qiáng)度M,取值為0~10(只取整數(shù)),表示使原有圖片增強(qiáng)失真的大小。
RandAugment方法以結(jié)果為導(dǎo)向,使數(shù)據(jù)增強(qiáng)過(guò)程更加面向用。在減少AutoAugment的運(yùn)算消耗的同時(shí),又使增強(qiáng)的效果變得可控。詳細(xì)內(nèi)容可以參考相關(guān)論文(參見(jiàn)arXⅳ網(wǎng)站上編號(hào)為1909.13719的論文)。
2.2 代碼獲取
https://github.com/heartInsert/randaugment # 只有一個(gè)代碼文件Rand_Augment,py,將其下載后,直接引入代碼即可使用。3 本節(jié)案例(帶有數(shù)據(jù)增強(qiáng)的識(shí)別)
3.1 案例簡(jiǎn)介
使用遷移學(xué)習(xí)對(duì)預(yù)訓(xùn)練模型進(jìn)行微調(diào)的基礎(chǔ)上實(shí)現(xiàn)數(shù)據(jù)增強(qiáng),讓其學(xué)習(xí)鳥(niǎo)類(lèi)數(shù)據(jù)集,實(shí)現(xiàn)對(duì)多種鳥(niǎo)類(lèi)進(jìn)行識(shí)別。
3.2 代碼實(shí)現(xiàn):load_data函數(shù)加載圖片名稱(chēng)與標(biāo)簽的加載----Transfer_bird2_Augmentation.py(第1部分)
import glob import numpy as np from PIL import Image import matplotlib.pyplot as plt #plt 用于顯示圖片 import torch import torch.nn as nn import torch.optim as optim from torch.optim import lr_scheduler from torch.utils.data import Dataset,DataLoader import torchvision import torchvision.models as model from torchvision.transforms import ToPILImage import torchvision.transforms as transforms import os os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'# 1.1 實(shí)現(xiàn)load_data函數(shù)加載圖片名稱(chēng)與標(biāo)簽的加載,并使用torch.utils.data接口將其封裝成程序可用的數(shù)據(jù)集類(lèi)OwnDataset。 def load_dir(directory,labstart=0): # 獲取所有directory中的所有圖與標(biāo)簽# 返回path指定的文件夾所包含的文件或文件名的名稱(chēng)列表strlabels = os.listdir(directory)# 對(duì)標(biāo)簽進(jìn)行排序,以便訓(xùn)練和驗(yàn)證按照相同的順序進(jìn)行:在不同的操作系統(tǒng)中,加載文件夾的順序可能不同。目錄不同的情況會(huì)導(dǎo)致在不同的操作系統(tǒng)中,模型的標(biāo)簽出現(xiàn)串位的現(xiàn)象。所以需要對(duì)文件夾進(jìn)行排序,保證其順序的一致性。strlabels.sort()# 創(chuàng)建文件標(biāo)簽列表file_labels = []for i,label in enumerate(strlabels):print(label)jpg_names = glob.glob(os.path.join(directory,label,"*.jpg"))print(jpg_names)# 加入列表file_labels.extend(zip(jpg_names, [i + labstart] * len(jpg_names)))return file_labels,strlabelsdef load_data(dataset_path): # 定義函數(shù)load_data函數(shù)完成對(duì)數(shù)據(jù)集中圖片文件名稱(chēng)和標(biāo)簽的加載。# 該函數(shù)可以實(shí)現(xiàn)兩層文件夾的嵌套結(jié)構(gòu)。其中,外層結(jié)構(gòu)使用load_data函數(shù)進(jìn)行遍歷,內(nèi)層結(jié)構(gòu)使用load_dir函進(jìn)行遍歷。sub_dir = sorted(os.listdir(dataset_path)) # 跳過(guò)子文件夾:在不同的操作系統(tǒng)中,加載文件夾的順序可能不同。目錄不同的情況會(huì)導(dǎo)致在不同的操作系統(tǒng)中,模型的標(biāo)簽出現(xiàn)串位的現(xiàn)象。所以需要對(duì)文件夾進(jìn)行排序,保證其順序的一致性。start = 1 # 第0類(lèi)是nonetfile_lables,tstrlabels = [],['none'] # 在制作標(biāo)簽時(shí),人為地在前面添加了一個(gè)序號(hào)為0的none類(lèi)。這是一個(gè)訓(xùn)練圖文類(lèi)模型的技巧,為了區(qū)分模型輸出值是0和預(yù)測(cè)值是0這兩種情況。for i in sub_dir:directory = os.path.join(dataset_path,i)if os.path.isdir(directory) == False: # 只處理文件夾中的數(shù)據(jù)print(directory)continuefile_labels,strlables = load_dir(directory,labstart=start)tfile_lables.extend(file_labels)tstrlabels.extend(strlables)start = len(strlables)# 將數(shù)據(jù)路徑與標(biāo)簽解壓縮,把數(shù)據(jù)路徑和標(biāo)簽解壓縮出來(lái)filenames,labels = zip(*tfile_lables)return filenames, labels, tstrlabels3.3 代碼實(shí)現(xiàn):自定義數(shù)據(jù)集類(lèi)OwnDataset----Transfer_bird2_Augmentation.py(第2部分)
# 1.2 實(shí)現(xiàn)自定義數(shù)據(jù)集OwnDataset def default_loader(path) : # 定義函數(shù)加載圖片return Image.open(path).convert('RGB')class OwnDataset(Dataset): # 復(fù)用性較強(qiáng),可根據(jù)自己的數(shù)據(jù)集略加修改使用# 在PyTorch中,提供了一個(gè)torch.utis.data接口,可以用來(lái)對(duì)數(shù)據(jù)集進(jìn)行封裝。在實(shí)現(xiàn)時(shí),只需要繼承torch.utis.data.Dataset類(lèi),并重載其__gettem__方法。# 在使用時(shí),框架會(huì)向__gettem__方法傳入索引index,在__gettem__方法內(nèi)部根據(jù)指定index加載數(shù)據(jù),并返回。def __init__(self,img_dir,labels,indexlist=None,transform=transforms.ToTensor(),loader=default_loader,cache=True): # 初始化self.labels = labels # 存放標(biāo)簽self.img_dir = img_dir # 樣本圖片文件名self.transform = transform # 預(yù)處理方法self.loader = loader # 加載方法self.cache = cache # 緩存標(biāo)志if indexlist is None: # 要加載的數(shù)據(jù)序列self.indexlist = list(range(len(self.img_dir)))else:self.indexlist = indexlistself.data = [None] * len(self.indexlist) # 存放樣本圖片def __getitem__(self, idx): # 加載指定索引數(shù)據(jù)if self.data[idx] is None: # 第一次加載data = self.loader(self.img_dir[self.indexlist[idx]])if self.transform:data = self.transform(data)else:data = self.data[idx]if self.cache: # 保存到緩存里self.data[idx] = datareturn data,self.labels[self.indexlist[idx]]def __len__(self): # 計(jì)算數(shù)據(jù)集長(zhǎng)度return len(self.indexlist)3.4 代碼實(shí)戰(zhàn):測(cè)試數(shù)據(jù)集----Transfer_bird2_Augmentation.py(第3部分)【數(shù)據(jù)增強(qiáng)模塊】
# 1.3 測(cè)試數(shù)據(jù)集:在完成數(shù)據(jù)集的制作之后,編寫(xiě)代碼對(duì)其進(jìn)行測(cè)試。 # 數(shù)據(jù)增強(qiáng)模塊 from Rand_Augment import Rand_Augment data_transform = { #定義數(shù)據(jù)的預(yù)處理方法'train':transforms.Compose([Rand_Augment(), # 數(shù)據(jù)增強(qiáng)的方法帶入 僅此一處修改transforms.RandomResizedCrop(224),transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),'val':transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), } def Reduction_img(tensor,mean,std): #還原圖片,實(shí)現(xiàn)了圖片歸一化的逆操作,顯示數(shù)據(jù)集中的原始圖片。dtype = tensor.dtypemean = torch.as_tensor(mean,dtype=dtype,device=tensor.device)std = torch.as_tensor(std,dtype=dtype,device=tensor.device)tensor.mul_(std[:,None,None]).add_(mean[:,None,None]) # 還原操作dataset_path = r'./data/cub200/' # 加載數(shù)據(jù)集路徑 filenames,labels,classes = load_data(dataset_path) # 調(diào)用load_data函數(shù)對(duì)數(shù)據(jù)集中圖片文件名稱(chēng)和標(biāo)簽進(jìn)行加載,其返回對(duì)象classes中包含全部的類(lèi)名。 # 打亂數(shù)據(jù)順序 # 110-115行對(duì)數(shù)據(jù)文件列表的序號(hào)進(jìn)行亂序劃分,分為測(cè)試數(shù)據(jù)集和訓(xùn)練數(shù)集兩個(gè)索引列表。該索引列表會(huì)傳入OwnDataset類(lèi)做成指定的數(shù)據(jù)集。 np.random.seed(0) label_shuffle_index = np.random.permutation(len(labels)) label_train_num = (len(labels)//10) * 8 # 劃分訓(xùn)練數(shù)據(jù)集和測(cè)試數(shù)據(jù)集 train_list = label_shuffle_index[0:label_train_num] test_list = label_shuffle_index[label_train_num:] # 沒(méi)帶:train_dataset = OwnDataset(filenames,labels,train_list,data_transform['train'])# 實(shí)例化訓(xùn)練數(shù)據(jù)集 val_dataset = OwnDataset(filenames,labels,test_list,data_transform['val']) # 實(shí)例化測(cè)試數(shù)據(jù)集 # 實(shí)例化批次數(shù)據(jù)集:OwnDataset類(lèi)所定義的數(shù)據(jù)集,其使用方法與PyTorch中的內(nèi)置數(shù)據(jù)集的使用方法完全一致,配合DataLoader接口即可生成可以進(jìn)行訓(xùn)練或測(cè)試的批次數(shù)據(jù)。具體代碼如下。 train_loader = DataLoader(dataset=train_dataset,batch_size=32,shuffle=True) val_loader = DataLoader(dataset=val_dataset,batch_size=32,shuffle=True)sample = iter(train_loader) # 獲取一批次數(shù)據(jù),進(jìn)行測(cè)試 images,labels = sample.next() print("樣本形狀",np.shape(images)) print("標(biāo)簽個(gè)數(shù)",len(classes)) mulimgs = torchvision.utils.make_grid(images[:10],nrow=10) # 拼接多張圖片 Reduction_img(mulimgs,[0.485,0.456,0.406],[0.229,0.224,0.225]) _img = ToPILImage()(mulimgs) # 將張量轉(zhuǎn)化為圖片 plt.axis('off') plt.imshow(_img) # 顯示 plt.show() print(','.join('%5s' % classes[labels[j]] for j in range(len(images[:10]))))輸出:
樣本形狀 torch.Size([32, 3, 224, 224])
標(biāo)簽個(gè)數(shù) 6
輸出數(shù)據(jù)集中的10個(gè)圖片
3.5 代碼實(shí)戰(zhàn):獲取并改造ResNet模型----Transfer_bird2_Augmentation.py(第4部分)
# 1.4 獲取并改造ResNet模型:獲取ResNet模型,并加載預(yù)訓(xùn)練模型的權(quán)重。將其最后一層(輸出層)去掉,換成一個(gè)全新的全連接層,該全連接層的輸出節(jié)點(diǎn)數(shù)與本例分類(lèi)數(shù)相同。 # 指定設(shè)備 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print(device) # get_ResNet函數(shù),獲取預(yù)訓(xùn)練模型,可指定pretrained=True來(lái)實(shí)現(xiàn)自動(dòng)下載預(yù)訓(xùn)練模型,也可指定loadfile來(lái)從本地路徑加載預(yù)訓(xùn)練模型。 def get_ResNet(classes,pretrained=True,loadfile=None):ResNet = model.resnet101(pretrained) # 自動(dòng)下載官方的預(yù)訓(xùn)練模型if loadfile != None:ResNet.load_state_dict(torch.load(loadfile)) # 加載本地模型# 將所有的參數(shù)層進(jìn)行凍結(jié):設(shè)置模型僅最后一層可以進(jìn)行訓(xùn)練,使模型只針對(duì)最后一層進(jìn)行微調(diào)。for param in ResNet.parameters():param.requires_grad = False# 輸出全連接層的信息print(ResNet.fc)x = ResNet.fc.in_features # 獲取全連接層的輸入ResNet.fc = nn.Linear(x,len(classes)) # 定義一個(gè)新的全連接層print(ResNet.fc) # 最后輸出新的模型return ResNet ResNet = get_ResNet(classes) # 實(shí)例化模型 ResNet.to(device=device)3.6 代碼實(shí)戰(zhàn):定義損失函數(shù)、訓(xùn)練函數(shù)及測(cè)試函數(shù),對(duì)模型的最后一層進(jìn)行微調(diào)----Transfer_bird2_Augmentation.py(第5部分)
# 1.5 定義損失函數(shù)、訓(xùn)練函數(shù)及測(cè)試函數(shù),對(duì)模型的最后一層進(jìn)行微調(diào)。 criterion = nn.CrossEntropyLoss() # 指定新加的全連接層的學(xué)習(xí)率 optimizer = torch.optim.Adam([{'params':ResNet.fc.parameters()}],lr=0.01) def train(model,device,train_loader,epoch,optimizer): # 定義訓(xùn)練函數(shù)model.train()allloss = []for batch_idx,data in enumerate(train_loader):x,y = datax = x.to(device)y = y.to(device)optimizer.zero_grad()y_hat = model(x)loss = criterion(y_hat,y)loss.backward()allloss.append(loss.item())optimizer.step()print('Train Epoch:{}\t Loss:{:.6f}'.format(epoch,np.mean(allloss))) # 輸出訓(xùn)練結(jié)果def test(model,device,val_loader): # 定義測(cè)試函數(shù)model.eval()test_loss = []correct = []with torch.no_grad(): # 使模型在運(yùn)行時(shí)不進(jìn)行梯度跟蹤,可以減少模型運(yùn)行時(shí)對(duì)內(nèi)存的占用。for i,data in enumerate(val_loader):x, y = datax = x.to(device)y = y.to(device)y_hat = model(x)test_loss.append(criterion(y_hat,y).item()) # 收集損失函數(shù)pred = y_hat.max(1,keepdim=True)[1] # 獲取預(yù)測(cè)結(jié)果correct.append(pred.eq(y.view_as(pred)).sum().item()/pred.shape[0]) # 收集精確度print('\nTest:Average loss:{:,.4f},Accuracy:({:,.0f}%)\n'.format(np.mean(test_loss),np.mean(correct)*100)) # 輸出測(cè)試結(jié)果# 遷移學(xué)習(xí)的兩個(gè)步驟如下 if __name__ == '__main__': # 遷移學(xué)習(xí)步驟①:固定預(yù)訓(xùn)練模型的特征提取部分,只對(duì)最后一層進(jìn)行訓(xùn)練,使其快速收斂。firstmodepth = './data/cub200/firstmodepth_1.pth' # 定義模型文件的地址if os.path.exists(firstmodepth) == False:print("—————————固定預(yù)訓(xùn)練模型的特征提取部分,只對(duì)最后一層進(jìn)行訓(xùn)練,使其快速收斂—————————")for epoch in range(1,2): # 迭代兩次train(ResNet,device,train_loader,epoch,optimizer)test(ResNet,device,val_loader)# 保存模型torch.save(ResNet.state_dict(),firstmodepth)3.7 代碼實(shí)戰(zhàn):使用退化學(xué)習(xí)率對(duì)模型進(jìn)行全局微調(diào)----Transfer_bird2_Augmentation.py(第6部分)
# 1.6 使用退化學(xué)習(xí)率對(duì)模型進(jìn)行全局微調(diào) #遷移學(xué)習(xí)步驟②:使用較小的學(xué)習(xí)率,對(duì)全部模型進(jìn)行訓(xùn)練,并對(duì)每層的權(quán)重進(jìn)行細(xì)微的調(diào)節(jié),即將模型的每層權(quán)重都設(shè)為可訓(xùn)練,并定義帶有退化學(xué)習(xí)率的優(yōu)化器。(1.6部分)secondmodepth = './data/cub200/firstmodepth_2.pth'optimizer2 = optim.SGD(ResNet.parameters(),lr=0.001,momentum=0.9) # 第198行代碼定義帶有退化學(xué)習(xí)率的SGD優(yōu)化器。該優(yōu)化器常用來(lái)對(duì)模型進(jìn)行手動(dòng)微調(diào)。有實(shí)驗(yàn)表明,使用經(jīng)過(guò)手動(dòng)調(diào)節(jié)的SGD優(yōu)化器,在訓(xùn)練模型的后期效果優(yōu)于Adam優(yōu)化器。exp_lr_scheduler = lr_scheduler.StepLR(optimizer2,step_size=2,gamma=0.9) # 由于退化學(xué)習(xí)率會(huì)在訓(xùn)練過(guò)程中不斷地變小,為了防止學(xué)習(xí)率過(guò)小,最終無(wú)法進(jìn)行權(quán)重需要對(duì)其設(shè)置最小值。當(dāng)學(xué)習(xí)率低于該值時(shí),停止對(duì)退化學(xué)習(xí)率的操作。for param in ResNet.parameters(): # 所有參數(shù)設(shè)計(jì)為可訓(xùn)練param.requires_grad = Trueif os.path.exists(secondmodepth):ResNet.load_state_dict(torch.load(secondmodepth)) # 加載本地模型else:ResNet.load_state_dict(torch.load(firstmodepth)) # 加載本地模型print("____使用較小的學(xué)習(xí)率,對(duì)全部模型進(jìn)行訓(xùn)練,定義帶有退化學(xué)習(xí)率的優(yōu)化器______")for epoch in range(1,100):train(ResNet,device,train_loader,epoch,optimizer2)if optimizer2.state_dict()['param_groups'][0]['lr'] > 0.00001:exp_lr_scheduler.step()print("___lr:",optimizer2.state_dict()['param_groups'][0]['lr'])test(ResNet,device,val_loader)# 保存模型torch.save(ResNet.state_dict(),secondmodepth)4 代碼總覽Transfer_bird2_Augmentation.py
import glob import numpy as np from PIL import Image import matplotlib.pyplot as plt #plt 用于顯示圖片 import torch import torch.nn as nn import torch.optim as optim from torch.optim import lr_scheduler from torch.utils.data import Dataset,DataLoader import torchvision import torchvision.models as model from torchvision.transforms import ToPILImage import torchvision.transforms as transforms import os os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'# 1.1 實(shí)現(xiàn)load_data函數(shù)加載圖片名稱(chēng)與標(biāo)簽的加載,并使用torch.utils.data接口將其封裝成程序可用的數(shù)據(jù)集類(lèi)OwnDataset。 def load_dir(directory,labstart=0): # 獲取所有directory中的所有圖與標(biāo)簽# 返回path指定的文件夾所包含的文件或文件名的名稱(chēng)列表strlabels = os.listdir(directory)# 對(duì)標(biāo)簽進(jìn)行排序,以便訓(xùn)練和驗(yàn)證按照相同的順序進(jìn)行:在不同的操作系統(tǒng)中,加載文件夾的順序可能不同。目錄不同的情況會(huì)導(dǎo)致在不同的操作系統(tǒng)中,模型的標(biāo)簽出現(xiàn)串位的現(xiàn)象。所以需要對(duì)文件夾進(jìn)行排序,保證其順序的一致性。strlabels.sort()# 創(chuàng)建文件標(biāo)簽列表file_labels = []for i,label in enumerate(strlabels):print(label)jpg_names = glob.glob(os.path.join(directory,label,"*.jpg"))print(jpg_names)# 加入列表file_labels.extend(zip(jpg_names, [i + labstart] * len(jpg_names)))return file_labels,strlabelsdef load_data(dataset_path): # 定義函數(shù)load_data函數(shù)完成對(duì)數(shù)據(jù)集中圖片文件名稱(chēng)和標(biāo)簽的加載。# 該函數(shù)可以實(shí)現(xiàn)兩層文件夾的嵌套結(jié)構(gòu)。其中,外層結(jié)構(gòu)使用load_data函數(shù)進(jìn)行遍歷,內(nèi)層結(jié)構(gòu)使用load_dir函進(jìn)行遍歷。sub_dir = sorted(os.listdir(dataset_path)) # 跳過(guò)子文件夾:在不同的操作系統(tǒng)中,加載文件夾的順序可能不同。目錄不同的情況會(huì)導(dǎo)致在不同的操作系統(tǒng)中,模型的標(biāo)簽出現(xiàn)串位的現(xiàn)象。所以需要對(duì)文件夾進(jìn)行排序,保證其順序的一致性。start = 1 # 第0類(lèi)是nonetfile_lables,tstrlabels = [],['none'] # 在制作標(biāo)簽時(shí),人為地在前面添加了一個(gè)序號(hào)為0的none類(lèi)。這是一個(gè)訓(xùn)練圖文類(lèi)模型的技巧,為了區(qū)分模型輸出值是0和預(yù)測(cè)值是0這兩種情況。for i in sub_dir:directory = os.path.join(dataset_path,i)if os.path.isdir(directory) == False: # 只處理文件夾中的數(shù)據(jù)print(directory)continuefile_labels,strlables = load_dir(directory,labstart=start)tfile_lables.extend(file_labels)tstrlabels.extend(strlables)start = len(strlables)# 將數(shù)據(jù)路徑與標(biāo)簽解壓縮,把數(shù)據(jù)路徑和標(biāo)簽解壓縮出來(lái)filenames,labels = zip(*tfile_lables)return filenames, labels, tstrlabels# 1.2 實(shí)現(xiàn)自定義數(shù)據(jù)集OwnDataset def default_loader(path) : # 定義函數(shù)加載圖片return Image.open(path).convert('RGB')class OwnDataset(Dataset): # 復(fù)用性較強(qiáng),可根據(jù)自己的數(shù)據(jù)集略加修改使用# 在PyTorch中,提供了一個(gè)torch.utis.data接口,可以用來(lái)對(duì)數(shù)據(jù)集進(jìn)行封裝。在實(shí)現(xiàn)時(shí),只需要繼承torch.utis.data.Dataset類(lèi),并重載其__gettem__方法。# 在使用時(shí),框架會(huì)向__gettem__方法傳入索引index,在__gettem__方法內(nèi)部根據(jù)指定index加載數(shù)據(jù),并返回。def __init__(self,img_dir,labels,indexlist=None,transform=transforms.ToTensor(),loader=default_loader,cache=True): # 初始化self.labels = labels # 存放標(biāo)簽self.img_dir = img_dir # 樣本圖片文件名self.transform = transform # 預(yù)處理方法self.loader = loader # 加載方法self.cache = cache # 緩存標(biāo)志if indexlist is None: # 要加載的數(shù)據(jù)序列self.indexlist = list(range(len(self.img_dir)))else:self.indexlist = indexlistself.data = [None] * len(self.indexlist) # 存放樣本圖片def __getitem__(self, idx): # 加載指定索引數(shù)據(jù)if self.data[idx] is None: # 第一次加載data = self.loader(self.img_dir[self.indexlist[idx]])if self.transform:data = self.transform(data)else:data = self.data[idx]if self.cache: # 保存到緩存里self.data[idx] = datareturn data,self.labels[self.indexlist[idx]]def __len__(self): # 計(jì)算數(shù)據(jù)集長(zhǎng)度return len(self.indexlist)# 1.3 測(cè)試數(shù)據(jù)集:在完成數(shù)據(jù)集的制作之后,編寫(xiě)代碼對(duì)其進(jìn)行測(cè)試。 # 數(shù)據(jù)增強(qiáng)模塊 from Rand_Augment import Rand_Augment data_transform = { #定義數(shù)據(jù)的預(yù)處理方法'train':transforms.Compose([Rand_Augment(), # 數(shù)據(jù)增強(qiáng)的方法帶入 僅此一處修改transforms.RandomResizedCrop(224),transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),'val':transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), } def Reduction_img(tensor,mean,std): #還原圖片,實(shí)現(xiàn)了圖片歸一化的逆操作,顯示數(shù)據(jù)集中的原始圖片。dtype = tensor.dtypemean = torch.as_tensor(mean,dtype=dtype,device=tensor.device)std = torch.as_tensor(std,dtype=dtype,device=tensor.device)tensor.mul_(std[:,None,None]).add_(mean[:,None,None]) # 還原操作dataset_path = r'./data/cub200/' # 加載數(shù)據(jù)集路徑 filenames,labels,classes = load_data(dataset_path) # 調(diào)用load_data函數(shù)對(duì)數(shù)據(jù)集中圖片文件名稱(chēng)和標(biāo)簽進(jìn)行加載,其返回對(duì)象classes中包含全部的類(lèi)名。 # 打亂數(shù)據(jù)順序 # 110-115行對(duì)數(shù)據(jù)文件列表的序號(hào)進(jìn)行亂序劃分,分為測(cè)試數(shù)據(jù)集和訓(xùn)練數(shù)集兩個(gè)索引列表。該索引列表會(huì)傳入OwnDataset類(lèi)做成指定的數(shù)據(jù)集。 np.random.seed(0) label_shuffle_index = np.random.permutation(len(labels)) label_train_num = (len(labels)//10) * 8 # 劃分訓(xùn)練數(shù)據(jù)集和測(cè)試數(shù)據(jù)集 train_list = label_shuffle_index[0:label_train_num] test_list = label_shuffle_index[label_train_num:] # 沒(méi)帶:train_dataset = OwnDataset(filenames,labels,train_list,data_transform['train'])# 實(shí)例化訓(xùn)練數(shù)據(jù)集 val_dataset = OwnDataset(filenames,labels,test_list,data_transform['val']) # 實(shí)例化測(cè)試數(shù)據(jù)集 # 實(shí)例化批次數(shù)據(jù)集:OwnDataset類(lèi)所定義的數(shù)據(jù)集,其使用方法與PyTorch中的內(nèi)置數(shù)據(jù)集的使用方法完全一致,配合DataLoader接口即可生成可以進(jìn)行訓(xùn)練或測(cè)試的批次數(shù)據(jù)。具體代碼如下。 train_loader = DataLoader(dataset=train_dataset,batch_size=32,shuffle=True) val_loader = DataLoader(dataset=val_dataset,batch_size=32,shuffle=True)sample = iter(train_loader) # 獲取一批次數(shù)據(jù),進(jìn)行測(cè)試 images,labels = sample.next() print("樣本形狀",np.shape(images)) print("標(biāo)簽個(gè)數(shù)",len(classes)) mulimgs = torchvision.utils.make_grid(images[:10],nrow=10) # 拼接多張圖片 Reduction_img(mulimgs,[0.485,0.456,0.406],[0.229,0.224,0.225]) _img = ToPILImage()(mulimgs) # 將張量轉(zhuǎn)化為圖片 plt.axis('off') plt.imshow(_img) # 顯示 plt.show() print(','.join('%5s' % classes[labels[j]] for j in range(len(images[:10]))))# 1.4 獲取并改造ResNet模型:獲取ResNet模型,并加載預(yù)訓(xùn)練模型的權(quán)重。將其最后一層(輸出層)去掉,換成一個(gè)全新的全連接層,該全連接層的輸出節(jié)點(diǎn)數(shù)與本例分類(lèi)數(shù)相同。 # 指定設(shè)備 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print(device) # get_ResNet函數(shù),獲取預(yù)訓(xùn)練模型,可指定pretrained=True來(lái)實(shí)現(xiàn)自動(dòng)下載預(yù)訓(xùn)練模型,也可指定loadfile來(lái)從本地路徑加載預(yù)訓(xùn)練模型。 def get_ResNet(classes,pretrained=True,loadfile=None):ResNet = model.resnet101(pretrained) # 自動(dòng)下載官方的預(yù)訓(xùn)練模型if loadfile != None:ResNet.load_state_dict(torch.load(loadfile)) # 加載本地模型# 將所有的參數(shù)層進(jìn)行凍結(jié):設(shè)置模型僅最后一層可以進(jìn)行訓(xùn)練,使模型只針對(duì)最后一層進(jìn)行微調(diào)。for param in ResNet.parameters():param.requires_grad = False# 輸出全連接層的信息print(ResNet.fc)x = ResNet.fc.in_features # 獲取全連接層的輸入ResNet.fc = nn.Linear(x,len(classes)) # 定義一個(gè)新的全連接層print(ResNet.fc) # 最后輸出新的模型return ResNet ResNet = get_ResNet(classes) # 實(shí)例化模型 ResNet.to(device=device)# 1.5 定義損失函數(shù)、訓(xùn)練函數(shù)及測(cè)試函數(shù),對(duì)模型的最后一層進(jìn)行微調(diào)。 criterion = nn.CrossEntropyLoss() # 指定新加的全連接層的學(xué)習(xí)率 optimizer = torch.optim.Adam([{'params':ResNet.fc.parameters()}],lr=0.01) def train(model,device,train_loader,epoch,optimizer): # 定義訓(xùn)練函數(shù)model.train()allloss = []for batch_idx,data in enumerate(train_loader):x,y = datax = x.to(device)y = y.to(device)optimizer.zero_grad()y_hat = model(x)loss = criterion(y_hat,y)loss.backward()allloss.append(loss.item())optimizer.step()print('Train Epoch:{}\t Loss:{:.6f}'.format(epoch,np.mean(allloss))) # 輸出訓(xùn)練結(jié)果def test(model,device,val_loader): # 定義測(cè)試函數(shù)model.eval()test_loss = []correct = []with torch.no_grad(): # 使模型在運(yùn)行時(shí)不進(jìn)行梯度跟蹤,可以減少模型運(yùn)行時(shí)對(duì)內(nèi)存的占用。for i,data in enumerate(val_loader):x, y = datax = x.to(device)y = y.to(device)y_hat = model(x)test_loss.append(criterion(y_hat,y).item()) # 收集損失函數(shù)pred = y_hat.max(1,keepdim=True)[1] # 獲取預(yù)測(cè)結(jié)果correct.append(pred.eq(y.view_as(pred)).sum().item()/pred.shape[0]) # 收集精確度print('\nTest:Average loss:{:,.4f},Accuracy:({:,.0f}%)\n'.format(np.mean(test_loss),np.mean(correct)*100)) # 輸出測(cè)試結(jié)果# 遷移學(xué)習(xí)的兩個(gè)步驟如下 if __name__ == '__main__': # 遷移學(xué)習(xí)步驟①:固定預(yù)訓(xùn)練模型的特征提取部分,只對(duì)最后一層進(jìn)行訓(xùn)練,使其快速收斂。firstmodepth = './data/cub200/firstmodepth_1.pth' # 定義模型文件的地址if os.path.exists(firstmodepth) == False:print("—————————固定預(yù)訓(xùn)練模型的特征提取部分,只對(duì)最后一層進(jìn)行訓(xùn)練,使其快速收斂—————————")for epoch in range(1,2): # 迭代兩次train(ResNet,device,train_loader,epoch,optimizer)test(ResNet,device,val_loader)# 保存模型torch.save(ResNet.state_dict(),firstmodepth) # 1.6 使用退化學(xué)習(xí)率對(duì)模型進(jìn)行全局微調(diào) #遷移學(xué)習(xí)步驟②:使用較小的學(xué)習(xí)率,對(duì)全部模型進(jìn)行訓(xùn)練,并對(duì)每層的權(quán)重進(jìn)行細(xì)微的調(diào)節(jié),即將模型的每層權(quán)重都設(shè)為可訓(xùn)練,并定義帶有退化學(xué)習(xí)率的優(yōu)化器。(1.6部分)secondmodepth = './data/cub200/firstmodepth_2.pth'optimizer2 = optim.SGD(ResNet.parameters(),lr=0.001,momentum=0.9) # 第198行代碼定義帶有退化學(xué)習(xí)率的SGD優(yōu)化器。該優(yōu)化器常用來(lái)對(duì)模型進(jìn)行手動(dòng)微調(diào)。有實(shí)驗(yàn)表明,使用經(jīng)過(guò)手動(dòng)調(diào)節(jié)的SGD優(yōu)化器,在訓(xùn)練模型的后期效果優(yōu)于Adam優(yōu)化器。exp_lr_scheduler = lr_scheduler.StepLR(optimizer2,step_size=2,gamma=0.9) # 由于退化學(xué)習(xí)率會(huì)在訓(xùn)練過(guò)程中不斷地變小,為了防止學(xué)習(xí)率過(guò)小,最終無(wú)法進(jìn)行權(quán)重需要對(duì)其設(shè)置最小值。當(dāng)學(xué)習(xí)率低于該值時(shí),停止對(duì)退化學(xué)習(xí)率的操作。for param in ResNet.parameters(): # 所有參數(shù)設(shè)計(jì)為可訓(xùn)練param.requires_grad = Trueif os.path.exists(secondmodepth):ResNet.load_state_dict(torch.load(secondmodepth)) # 加載本地模型else:ResNet.load_state_dict(torch.load(firstmodepth)) # 加載本地模型print("____使用較小的學(xué)習(xí)率,對(duì)全部模型進(jìn)行訓(xùn)練,定義帶有退化學(xué)習(xí)率的優(yōu)化器______")for epoch in range(1,100):train(ResNet,device,train_loader,epoch,optimizer2)if optimizer2.state_dict()['param_groups'][0]['lr'] > 0.00001:exp_lr_scheduler.step()print("___lr:",optimizer2.state_dict()['param_groups'][0]['lr'])test(ResNet,device,val_loader)# 保存模型torch.save(ResNet.state_dict(),secondmodepth)總結(jié)
以上是生活随笔為你收集整理的【Pytorch神经网络实战案例】25 (带数据增强)基于迁移学习识别多种鸟类(CUB-200数据集)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 爬虫实战学习笔记_6 网络请求reque
- 下一篇: 图像特征提取与描述_角点特征01:Har