介紹
【資料圖】
在本系列的上一部分中,我們使用了CIFAR-10數(shù)據(jù)集,并介紹了PyTorch的基礎(chǔ)知識:
張量及其相關(guān)操作
數(shù)據(jù)集和數(shù)據(jù)加載器
構(gòu)建基本的神經(jīng)網(wǎng)絡(luò)
基本模型的訓(xùn)練和評估
我們?yōu)镃IFAR-10數(shù)據(jù)集中的圖像分類開發(fā)的模型只能在驗證集上達到53%的準(zhǔn)確率,并且在一些類別(如鳥類和貓類)的圖像分類上表現(xiàn)非常困難(約33-35%的準(zhǔn)確率)。這是預(yù)期的,因為我們通常會使用卷積神經(jīng)網(wǎng)絡(luò)進行圖像分類。在本教程系列的這一部分,我們將專注于卷積神經(jīng)網(wǎng)絡(luò)(CNN)并改善在CIFAR-10上的圖像分類性能。
CNN基礎(chǔ)知識
在我們深入代碼之前,讓我們討論卷積神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)知識,以便更好地理解我們的代碼在做什么。如果你已經(jīng)對CNN的工作原理感到熟悉,可以跳過本節(jié)。
與前一部分中開發(fā)的前饋網(wǎng)絡(luò)相比,卷積神經(jīng)網(wǎng)絡(luò)具有不同的架構(gòu),并由不同類型的層組成。在下圖中,我們可以看到典型CNN的一般架構(gòu),包括它可能包含的不同類型的層。
卷積網(wǎng)絡(luò)中通常包含的三種類型的層是:
卷積層(紅色虛線框)
池化層(藍色虛線框)
全連接層(紅色和紫色實線框)
卷積層
CNN的定義組件和第一層是卷積層,它由以下部分組成:
輸入數(shù)據(jù)(在本例中為圖像)
濾波器
特征圖
將卷積層與全連接層區(qū)分開來的關(guān)鍵是卷積運算。我們不會詳細討論卷積的定義,但如果你真的感興趣并想深入了解其數(shù)學(xué)定義以及一些具體的示例,我強烈推薦閱讀這篇文章,它在解釋數(shù)學(xué)定義方面做得非常好
https://betterexplained.com/articles/intuitive-convolution/#Part_3_Mathematical_Properties_of_Convolution
卷積相對于密集連接層(全連接層)在圖像數(shù)據(jù)中的優(yōu)勢何在?簡而言之,密集連接層會學(xué)習(xí)輸入中的全局模式,而卷積層具有學(xué)習(xí)局部和空間模式的優(yōu)勢。這可能聽起來有些模糊或抽象,所以讓我們看一個例子來說明這是什么意思。
在圖片的左側(cè),我們可以看到一個基本的2D黑白圖像的4是如何在卷積層中表示的。
紅色方框是濾波器/特征檢測器/卷積核,在圖像上進行卷積操作。在右側(cè)是相同圖像在一個密集連接層中的表示。你可以看到相同的9個圖像像素被紅色的卷積核框起來。請注意,在左側(cè),像素在空間上是分組的,與相鄰的像素相鄰。然而,在右側(cè),這相同的9個像素不再是相鄰的。
通過這個例子,我們可以看到當(dāng)圖像被壓平并表示為完全連接/線性層時,空間/位置信息是如何丟失的。這就是為什么卷積神經(jīng)網(wǎng)絡(luò)在處理圖像數(shù)據(jù)時更強大的原因。輸入數(shù)據(jù)的空間結(jié)構(gòu)得到保留,圖像中的模式(邊緣、紋理、形狀等)可以被學(xué)習(xí)。
這基本上是為什么在圖像上使用卷積神經(jīng)網(wǎng)絡(luò)的原因,但現(xiàn)在讓我們討論一下如何實現(xiàn)。讓我們來看看我們的輸入數(shù)據(jù)的結(jié)構(gòu),我們一直在談?wù)摰哪切┙凶觥盀V波器”的東西,以及當(dāng)我們將它們放在一起時卷積是什么樣子。
輸入數(shù)據(jù)
CIFAR-10數(shù)據(jù)集包含60,000個32x32的彩色圖像,每個圖像都表示為一個3D張量。每個圖像將是一個(32,32,3)的張量,其中的維度是32(高度)x 32(寬度)x 3(R-G-B顏色通道)。下圖展示了從數(shù)據(jù)集中分離出來的飛機全彩色圖像的3個不同的顏色通道(RGB)。
通常將圖像視為二維的,所以很容易忘記它們實際上是以三維表示的,因為它們有3個顏色通道!
濾波器
在卷積層中,濾波器(也稱為卷積核或特征檢測器)是一組權(quán)重數(shù)組,它以滑動窗口的方式在圖像上進行掃描,計算每一步的點積,并將該點積輸出到一個稱為特征圖的新數(shù)組中。這種滑動窗口的掃描稱為卷積。讓我們看一下這個過程的示例,以幫助理解正在發(fā)生的事情。
一個3x3的濾波器(藍色)對輸入(紅色)進行卷積,生成一個特征圖(紫色):
在每個卷積步驟中計算點積的示意圖:
需要注意的是,濾波器的權(quán)重在每個步驟中保持不變。就像在全連接層中的權(quán)重一樣,這些值在訓(xùn)練過程中進行學(xué)習(xí),并通過反向傳播在每個訓(xùn)練迭代后進行調(diào)整。
這些示意圖并不能完全展示所有情況。當(dāng)訓(xùn)練一個卷積神經(jīng)網(wǎng)絡(luò)時,模型不僅在卷積層中使用一個濾波器是很常見的。通常在一個卷積層中會有32或64個濾波器,實際上,在本教程中,我們將在一個層中使用多達96個濾波器來構(gòu)建我們的模型。
最后,雖然濾波器的權(quán)重是需要訓(xùn)練的主要參數(shù),但卷積神經(jīng)網(wǎng)絡(luò)也有一些可以調(diào)整的超參數(shù):
層中的濾波器數(shù)量
濾波器的維度
步幅(每一步濾波器移動的像素數(shù))
填充(濾波器如何處理圖像邊界)
我們不會詳細討論這些超參數(shù),因為本文不旨在全面介紹卷積神經(jīng)網(wǎng)絡(luò),但這些是需要注意的重要因素。
池化層
池化層與卷積層類似,都是通過濾波器對輸入數(shù)據(jù)(通常是從卷積層輸出的特征圖)進行卷積運算。
然而,池化層的功能不是特征檢測,而是降低維度或降采樣。最常用的兩種池化方法是最大池化和平均池化。在最大池化中,濾波器在輸入上滑動,并在每一步選擇具有最大值的像素作為輸出。在平均池化中,濾波器輸出濾波器所經(jīng)過像素的平均值。
全連接層
最后,在卷積和池化層之后,卷積神經(jīng)網(wǎng)絡(luò)通常會有全連接層,這些層將在圖像分類任務(wù)中執(zhí)行分類,就像本教程中的任務(wù)一樣。
現(xiàn)在,我們已經(jīng)了解了卷積神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)和操作方式,讓我們開始進行有趣的部分,在PyTorch中訓(xùn)練我們自己的CNN模型!
設(shè)置
與本教程的第一部分一樣,我建議使用Google Colab進行跟隨,因為你的Python環(huán)境已經(jīng)安裝了PyTorch和其他庫,并且有一個GPU可以用于訓(xùn)練模型。
因此,如果你使用的是Colab,請確保使用GPU,方法是轉(zhuǎn)到“運行時”(Runtime)并點擊“更改運行時類型”。
在對話框中選擇GPU并保存。
現(xiàn)在你可以在Colab中使用GPU了,并且我們可以使用PyTorch驗證你的設(shè)備。
因此,首先,讓我們處理導(dǎo)入部分:
importtorch
fromtorchimportnn
fromtorch.utils.dataimportDataLoader
fromtorchvision.utilsimportmake_grid
fromtorchvision.datasetsimportCIFAR10
fromtorchvisionimporttransforms
fromtorchvisionimportutils
fromtorchvision.utilsimportmake_grid
importmatplotlib.pyplotasplt
importnumpyasnp
importseabornassns
importpandasaspd
如果你想檢查你可以訪問的GPU是什么,請鍵入并執(zhí)行torch.cuda.get_device_name(0),你應(yīng)該會看到設(shè)備輸出。Colab有幾種不同的GPU選項可供選擇,因此你的輸出將根據(jù)你所能訪問的內(nèi)容而有所不同,但只要你在運行此代碼時沒有看到“RuntimeError: No CUDA GPUs are available”錯誤,那么你正在使用GPU!
我們可以將GPU設(shè)備設(shè)置為device,以便在開發(fā)模型時將其分配給GPU,如果沒有CUDA GPU設(shè)備可用,我們也可以使用CPU。
device="cuda"iftorch.cuda.is_available()else"cpu"
print(device)
#cuda
接下來,讓我們設(shè)置一個隨機種子,以便我們的結(jié)果是可重現(xiàn)的,并下載我們的訓(xùn)練數(shù)據(jù)并設(shè)置一個轉(zhuǎn)換,將圖像轉(zhuǎn)換為張量并對數(shù)據(jù)進行歸一化。
torch.manual_seed(42)
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))]
)
training_data=CIFAR10(root="cifar",
train=True,
download=True,
transform=transform)
test_data=CIFAR10(root="cifar",
train=False,
download=True,
transform=transform)
一旦下載完成,讓我們查看數(shù)據(jù)集中的類別:
classes=training_data.classes
classes
#["airplane",
#"automobile",
#"bird",
#"cat",
#"deer",
#"dog",
#"frog",
#"horse",
#"ship",
#"truck"]
最后,讓我們設(shè)置訓(xùn)練和測試數(shù)據(jù)加載器:
batch_size=24
train_dataloader=DataLoader(training_data,batch_size=batch_size,shuffle=True,num_workers=0)
test_dataloader=DataLoader(test_data,batch_size=batch_size,shuffle=True,num_workers=0)
forX,yintrain_dataloader:
print(f"ShapeofX[N,C,H,W]:{X.shape}")
print(f"Shapeofy:{y.shape}{y.dtype}")
break
#ShapeofX[N,C,H,W]:torch.Size([24,3,32,32])
#Shapeofy:torch.Size([24])torch.int64
現(xiàn)在我們準(zhǔn)備構(gòu)建我們的模型!
構(gòu)建CNN
在PyTorch中,nn.Conv2d是用于圖像輸入數(shù)據(jù)的卷積層。Conv2d的第一個參數(shù)是輸入中的通道數(shù),在我們的第一層卷積層中,我們將使用3,因為彩色圖像將有3個顏色通道。
在第一個卷積層之后,該參數(shù)將取決于前一層輸出的通道數(shù)。第二個參數(shù)是在該層中卷積操作輸出的通道數(shù)。這些通道是卷積層介紹中討論的特征圖。最后,第三個參數(shù)將是卷積核或濾波器的大小。這可以是一個整數(shù)值,如3表示3x3的卷積核,或者是一個元組,如(3,3)。因此,我們的卷積層將采用nn.Conv2d(in_channels, out_channels, kernel_size)的形式。還可以添加其他可選參數(shù),包括(但不限于)步幅(stride)、填充(padding)和膨脹(dilation)。在我們的卷積層conv4中,我們將使用stride=2。
在一系列卷積層之后,我們將使用一個扁平化層將特征圖扁平化,以便能夠輸入到線性層中。為此,我們將使用nn.Flatten()。我們可以使用nn.BatchNorm1d()應(yīng)用批量歸一化,并需要將特征數(shù)作為參數(shù)傳遞。
最后,我們使用nn.Linear()構(gòu)建線性的全連接層,第一個參數(shù)是特征數(shù),第二個參數(shù)是指定輸出特征數(shù)。
因此,要開始定義我們模型的基本架構(gòu),我們將定義一個ConvNet類,該類繼承自PyTorch的nn.Module類。然后,我們可以將每個層定義為類的屬性,并根據(jù)需要構(gòu)建它們。
一旦我們指定了層的架構(gòu),我們可以通過創(chuàng)建一個forward()方法來定義模型的流程。我們可以使用激活函數(shù)包裝每個層,在我們的情況下,我們將使用relu。我們可以通過傳遞前一層和p(元素被丟棄的概率,缺省值為0.5)在層之間應(yīng)用dropout。
最后,我們創(chuàng)建模型對象并將其附加到設(shè)備上,以便可以在GPU上訓(xùn)練。
classConvNet(nn.Module):
def__init__(self):
super().__init__()
self.d1=0.1
self.conv1=nn.Conv2d(3,48,3)
self.conv2=nn.Conv2d(48,48,3)
self.conv3=nn.Conv2d(48,96,3)
self.conv4=nn.Conv2d(96,96,3,stride=2)
self.flat=nn.Flatten()
self.batch_norm=nn.BatchNorm1d(96*12*12)
self.fc1=nn.Linear(96*12*12,256)
self.fc2=nn.Linear(256,10)
defforward(self,x):
x=nn.functional.relu(self.conv1(x))
x=nn.functional.relu(self.conv2(x))
x=nn.functional.dropout(x,self.d1)
x=nn.functional.relu(self.conv3(x))
x=nn.functional.relu(self.conv4(x))
x=nn.functional.dropout(x,0.5)
x=self.flat(x)
x=nn.functional.relu(self.batch_norm(x))
x=nn.functional.relu(self.fc1(x))
x=self.fc2(x)
returnx
model=ConvNet().to(device)
訓(xùn)練和測試函數(shù)
如果你完成了本教程的第一部分,我們的訓(xùn)練和測試函數(shù)將與之前創(chuàng)建的函數(shù)相同,只是在訓(xùn)練方法中返回損失,而在測試方法中返回損失和正確數(shù)量,以便在調(diào)整超參數(shù)時使用。
#TrainMethod
deftrain(dataloader,model,loss_fn,optimizer,verbose=True):
size=len(dataloader.dataset)
model.train()
forbatch,(X,y)inenumerate(dataloader):
X,y=X.to(device),y.to(device)
#Computepredictionerror
pred=model(X)
loss=loss_fn(pred,y)
#Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
ifverbose==True:
ifbatch%50==0:
loss,current=loss.item(),batch*len(X)
print(f"loss:{loss:>7f}[{current:>5d}/{size:>5d}]")
returnloss
#TestMethod
deftest(dataloader,model,loss_fn,verbose=True):
size=len(dataloader.dataset)
num_batches=len(dataloader)
model.eval()
test_loss,correct=0,0
withtorch.no_grad():
forX,yindataloader:
X,y=X.to(device),y.to(device)
pred=model(X)
test_loss+=loss_fn(pred,y).item()
correct+=(pred.argmax(1)==y).type(torch.float).sum().item()
test_loss/=num_batches
correct/=size
ifverbose==True:
print(f"TestError:Accuracy:{(100*correct):>0.1f}%,Avgloss:{test_loss:>8f}")
returntest_loss,correct#Forreportingtuningresults/earlystopping
最后,在基本模型訓(xùn)練之前,我們定義損失函數(shù)和優(yōu)化器。
loss_fn=nn.CrossEntropyLoss()
optimizer=torch.optim.Adam(model.parameters(),lr=0.001)
讓我們訓(xùn)練模型。
epochs=10
fortinrange(epochs):
print(f"Epoch{t+1}-------------------------------")
train(train_dataloader,model,loss_fn,optimizer)
test(test_dataloader,model,loss_fn)
print("Done!")
僅經(jīng)過10個epochs,61.7%的性能比我們訓(xùn)練的全連接模型要好得多!很明顯,CNN更適合用于圖像分類,但我們可以通過延長訓(xùn)練時間和調(diào)整超參數(shù)來進一步提高性能。
在進行這些之前,讓我們快速看看模型內(nèi)部是什么樣子。請記住,濾波器的像素是我們模型中可訓(xùn)練的參數(shù)。這不是訓(xùn)練圖像分類模型的必要步驟,也不會得到太多有用的信息,但是了解模型內(nèi)部的情況還是挺有意思的。
可視化濾波器
我們可以編寫一個函數(shù)來繪制模型中指定層的濾波器。我們只需要指定要查看的層,并將其傳遞給我們的函數(shù)。
defvisualizeTensor(tensor,ch=0,all_kernels=False,nrow=8,padding=1):
n,c,w,h=tensor.shape
ifall_kernels:
tensor=tensor.view(n*c,-1,w,h)
elifc!=3:
tensor=tensor[:,ch,:,:].unsqueeze(dim=1)
rows=np.min((tensor.shape[0]//nrow+1,64))
grid=utils.make_grid(tensor,
nrow=nrow,
normalize=True,
padding=padding)
grid=grid.cpu()#backtocpufornumpyandplotting
plt.figure(figsize=(nrow,rows))
plt.imshow(grid.numpy().transpose((1,2,0)))
讓我們來看看第一個卷積層(conv1)中的濾波器是什么樣子,因為這些濾波器直接應(yīng)用于圖像。
filter=model.conv1.weight.data.clone()
visualizeTensor(filter)
plt.axis("off")
plt.ioff()
plt.show
下面是輸出,包含了我們的conv1卷積層中48個濾波器的可視化。我們可以看到每個濾波器都是一個不同值或顏色的3x3張量。
如果我們的濾波器是5x5的,我們會在繪圖中看到以下差異。請記住,使用nn.Conv2d我們可以使用第三個參數(shù)更改濾波器的大小,因此如果我們想要一個5x5的濾波器,conv1將如下所示:
self.conv1=nn.Conv2d(3,48,5)#NewKernelSize
如果我們用新的5x5濾波器重新訓(xùn)練模型,輸出將如下所示:
如我之前提到的,這里并沒有太多有用的信息,但還是很有趣可以看到這些。
超參數(shù)優(yōu)化
在本教程中,我們將調(diào)整的超參數(shù)是卷積層中的濾波器數(shù)量以及線性層中的神經(jīng)元數(shù)量。當(dāng)前這些值在我們的模型中是硬編碼的,所以為了使它們可調(diào)整,我們需要使我們的模型可配置。
我們可以在模型的__init__方法中使用參數(shù)(c1、c2和l1),并使用這些值創(chuàng)建模型的層,在調(diào)整過程中將動態(tài)傳遞這些值。
classConfigNet(nn.Module):
def__init__(self,l1=256,c1=48,c2=96,d1=0.1):
super().__init__()
self.d1=d1
self.conv1=nn.Conv2d(3,c1,3)
self.conv2=nn.Conv2d(c1,c1,3)
self.conv3=nn.Conv2d(c1,c2,3)
self.conv4=nn.Conv2d(c2,c2,3,stride=2)
self.flat=nn.Flatten()
self.batch_norm=nn.BatchNorm1d(c2*144)
self.fc1=nn.Linear(c2*144,l1)
self.fc2=nn.Linear(l1,10)
defforward(self,x):
x=nn.functional.relu(self.conv1(x))
x=nn.functional.relu(self.conv2(x))
x=nn.functional.dropout(x,self.d1)
x=nn.functional.relu(self.conv3(x))
x=nn.functional.relu(self.conv4(x))
x=nn.functional.dropout(x,0.5)
x=self.flat(x)
x=nn.functional.relu(self.batch_norm(x))
x=nn.functional.relu(self.fc1(x))
x=self.fc2(x)
returnx
model=ConfigNet().to(device)
當(dāng)然,我們不僅限于調(diào)整這些超參數(shù)。事實上,學(xué)習(xí)率和批量大小通常也包括在要調(diào)整的超參數(shù)列表中,但由于我們將使用網(wǎng)格搜索,為了保持訓(xùn)練時間合理,我們必須大大減少可調(diào)整的變量數(shù)量。
接下來,讓我們?yōu)樗阉骺臻g定義一個字典,并保存給我們最佳結(jié)果的參數(shù)。由于我們使用網(wǎng)格搜索進行優(yōu)化,將使用每個超參數(shù)組合的所有組合。
你可以輕松地向每個超參數(shù)的列表中添加更多值,但每個額外的值都會大大增加運行時間,因此建議從以下值開始以節(jié)省時間。
search_space={
"c1":[48,96],
"c2":[96,192],
"l1":[256,512],
}
best_results={
"c1":None,
"c2":None,
"l1":None,
"loss":None,
"acc":0
}
提前停止
優(yōu)化過程中一個重要的組成部分是使用提前停止。由于我們將進行多次訓(xùn)練運行,每次訓(xùn)練運行時間都很長,如果訓(xùn)練性能沒有改善,我們將希望提前結(jié)束訓(xùn)練。繼續(xù)訓(xùn)練一個沒有改善的模型是沒有意義的。
實質(zhì)上,我們將在每個時期之后跟蹤模型產(chǎn)生的最低損失。然后,我們定義一個容差,指定模型必須在多少個時期內(nèi)達到更好的損失。如果在指定的容差內(nèi)沒有實現(xiàn)更低的損失,將終止該運行的訓(xùn)練,并繼續(xù)下一個超參數(shù)組合。
如果你像我一樣,喜歡檢查訓(xùn)練過程,可以設(shè)置self.verbose = True來記錄控制臺上的更新,并查看提前停止計數(shù)器增加的情況。你可以在此處硬編碼到EarlyStopping類中,也可以在優(yōu)化過程中實例化EarlyStopping對象時更改verbose值。
classEarlyStopping():
def__init__(self,tolerance=5,verbose=False,path="cifar-tune.pth"):
self.tolerance=tolerance
self.counter=0
self.early_stop=False
self.lowest_loss=None
self.verbose=verbose
self.path=path
defstep(self,val_loss):
if(self.lowest_loss==None):
self.lowest_loss=val_loss
torch.save(model.state_dict(),self.path)
elif(val_loss self.lowest_loss=val_loss self.counter=0 torch.save(model.state_dict(),self.path) else: ifself.verbose: print("Earlystopcounter:{}".format(self.counter+1)) self.counter+=1 ifself.counter>=self.tolerance: self.early_stop=True ifself.verbose: print("Earlystoppingexecuted.") 圖像增強 在設(shè)置超參數(shù)優(yōu)化方法之前,我們還有最后一件事要做,以提取出一些額外的性能并避免在訓(xùn)練數(shù)據(jù)上過度擬合。圖像增強是一種將隨機變換應(yīng)用于圖像的技術(shù),從本質(zhì)上講,它會創(chuàng)建“新的”人工數(shù)據(jù)。這些變換可以是以下幾種: 旋轉(zhuǎn)圖像幾度 水平/垂直翻轉(zhuǎn)圖像 裁剪 輕微的亮度/色調(diào)變化 隨機縮放 包含這些隨機變換將提高模型的泛化能力,因為增強后的圖像將與原始圖像類似,但不同。內(nèi)容和模式將保持不變,但數(shù)組表示將有所不同。 PyTorch通過torchvision.transforms模塊使圖像增強變得很容易。如果我們想要應(yīng)用多個變換,可以使用Compose將它們鏈接在一起。 需要記住的一點是,圖像增強對每個變換需要一點計算量,并且這些計算量應(yīng)用于數(shù)據(jù)集中的每個圖像。將許多不同的隨機變換應(yīng)用于我們的數(shù)據(jù)集將增加訓(xùn)練時間。 因此,現(xiàn)在讓我們限制變換的數(shù)量,以便訓(xùn)練時間不會太長。如果你想添加更多變換,請查看PyTorch關(guān)于轉(zhuǎn)換和增強圖像的文檔,然后將它們添加到Compose列表中。 選擇了增強變換之后,我們可以像應(yīng)用規(guī)范化和將圖像轉(zhuǎn)換為張量一樣將它們應(yīng)用于數(shù)據(jù)集。 #AugmentImagesforthetrainset augmented=transforms.Compose([ transforms.RandomRotation(20), transforms.ColorJitter(brightness=0.2,hue=0.1), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)) ]) #Standardtransformationforvalidationset transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)) ]) training_data=CIFAR10(root="cifar", train=True, download=True, transform=augmented) test_data=CIFAR10(root="cifar", train=False, download=True, transform=transform) 現(xiàn)在我們已經(jīng)在訓(xùn)練數(shù)據(jù)上設(shè)置了圖像增強,我們準(zhǔn)備設(shè)置我們的超參數(shù)優(yōu)化方法。 定義優(yōu)化方法 我們可以創(chuàng)建一個類(HyperSearch),其中包含超參數(shù)值配置、詳細報告設(shè)置、報告列表(以便在優(yōu)化完成后查看每個配置的表現(xiàn))的屬性,以及一個變量來存儲具有最佳性能的配置。 classHyperSearch(): def__init__(self,config,verbose=True): self.config=config self.verbose=verbose self.report_list=[] self.best_results={"c1":None, "c2":None, "l1":None, "loss":None, "acc":0 } 接下來,我們可以創(chuàng)建一個方法(仍在HyperSearch類中),以執(zhí)行網(wǎng)格搜索,并對每個超參數(shù)組合進行訓(xùn)練運行。首先,我們將使用tolerance=3配置EarlyStopping,并設(shè)置它保存每個超參數(shù)組合的權(quán)重。如果我們將self.verbose設(shè)置為True,我們可以在控制臺中看到當(dāng)前正在訓(xùn)練的超參數(shù)組合。 之后,我們使用我們設(shè)計的CoinfigNet模型定義我們的模型,并傳遞l1、c1和c2的值,同時選擇損失函數(shù)和優(yōu)化器,并設(shè)置我們的訓(xùn)練和驗證DataLoader。由于我們沒有時間也沒有意愿完全訓(xùn)練每個組合,所以我們將保持較低的時期數(shù)。目標(biāo)是了解哪種組合在對數(shù)據(jù)集進行分類時效果最好,然后我們可以將該模型完全訓(xùn)練,以查看它在完整的訓(xùn)練周期中的性能。 #OptimizationMethod defoptimize(self): forl1inself.config["l1"]: forc1inself.config["c1"]: forc2inself.config["c2"]: early_stopping=EarlyStopping(tolerance=3,verbose=False,path="{}-{}-{}.pth".format(c1,c2,l1)) ifself.verbose==True: print("Conv1:{}|Conv2:{}|Lin1:{}".format(str(c1),str(c2),str(l1))) model=ConfigNet(l1=l1,c1=c1,c2=c2).to(device) loss_fn=nn.CrossEntropyLoss() optimizer=torch.optim.Adam(model.parameters(),lr=lrate) train_dataloader=DataLoader(training_data,batch_size=batch_sz,shuffle=True,num_workers=0) test_dataloader=DataLoader(test_data,batch_size=batch_sz,shuffle=True,num_workers=0) 現(xiàn)在,我們定義訓(xùn)練循環(huán),大部分與之前相同,只是現(xiàn)在我們將保存train和test方法的損失,以便early_stopping可以跟蹤訓(xùn)練進展(或缺乏進展)。最后,在每個時期之后,將結(jié)果保存到報告中,并更新最佳損失的值。 epochs=10 fortinrange(epochs): ifself.verbose==True: print(f"Epoch{t+1}-------------------------------") train_loss=train(train_dataloader,model,loss_fn,optimizer,verbose=self.verbose) test_loss,test_acc=test(test_dataloader,model,loss_fn,verbose=self.verbose) #EarlyStopping early_stopping.step(test_loss) ifearly_stopping.early_stop: break print("Done!") self.append_to_report(test_acc,test_loss,c1,c2,l1) ifself.best_results["loss"]==Noneortest_loss ifself.verbose==True: print("UPDATE:Bestlosschangedfrom{}to{}".format(self.best_results["loss"],test_loss)) self.best_results.update({ "c1":c1, "c2":c2, "loss":test_loss, "l1":l1, "acc":test_acc }) self.report() 我們可以將整個超參數(shù)優(yōu)化周期的結(jié)果輸出到一個漂亮的表格中,在表格中,我們可以看到每次運行的超參數(shù)配置,以及相應(yīng)的損失和準(zhǔn)確率。 defreport(self): print(""" |-----------------------------------------------------------------------------------------------------| || |Reportforhyperparameteroptimization| || |-----------------------------------------------------------------------------------------------------| |RUN|PERFORMANCE|CONFIGURATION| |------------|--------------------------------------|-------------------------------------------------|""") foridx,iteminenumerate(self.report_list): print("|Run{:02d}|Accuracy:{:.2f}%|Loss:{:.2f}|Conv-1:{}|Conv-2:{:3}|Linear-1:{:>4}|".format(idx, item[0]*100, item[1], item[2], item[3], item[4])) print("|------------|---------------------|----------------|--------------|---------------|------------------|") print("BestResults|Accuracy:{:.2f}%|Loss:{:.2f}|Conv-1:{}|Conv-2:{}|Linear-1:{:>4}|".format(self.best_results["acc"]*100, self.best_results["loss"], self.best_results["c1"], self.best_results["c2"], self.best_results["l1"])) defappend_to_report(self,acc,loss,c1,c2,l1): list_set=(acc,loss,c1,c2,l1) self.report_list.append(list_set) 因此,將所有這些代碼放在一起,我們的HyperSearch類應(yīng)該如下所示: classHyperSearch(): def__init__(self,config,verbose=True): self.config=config self.verbose=verbose self.report_list=[] self.best_results={"c1":None, "c2":None, "l1":None, "loss":None, "acc":0 #"d1":None, #"lr":None, #"bsz":None, } #OptimizationMethod defoptimize(self): forl1inself.config["l1"]: forc1inself.config["c1"]: forc2inself.config["c2"]: early_stopping=EarlyStopping(tolerance=3,verbose=False,path="{}-{}-{}.pth".format(c1,c2,l1)) ifself.verbose==True: print("Conv1:{}|Conv2:{}|Lin1:{}".format(str(c1),str(c2),str(l1))) model=ConfigNet(l1=l1,c1=c1,c2=c2).to(device) loss_fn=nn.CrossEntropyLoss() optimizer=torch.optim.Adam(model.parameters(),lr=lrate) train_dataloader=DataLoader(training_data,batch_size=batch_sz,shuffle=True,num_workers=0) test_dataloader=DataLoader(test_data,batch_size=batch_sz,shuffle=True,num_workers=0) epochs=10 fortinrange(epochs): ifself.verbose==True: print(f"Epoch{t+1}-------------------------------") train_loss=train(train_dataloader,model,loss_fn,optimizer,verbose=self.verbose) test_loss,test_acc=test(test_dataloader,model,loss_fn,verbose=self.verbose) #EarlyStopping early_stopping.step(test_loss) ifearly_stopping.early_stop: break print("Done!") self.append_to_report(test_acc,test_loss,c1,c2,l1) ifself.best_results["loss"]==Noneortest_loss ifself.verbose==True: print("UPDATE:Bestlosschangedfrom{}to{}".format(self.best_results["loss"],test_loss)) self.best_results.update({ "c1":c1, "c2":c2, "loss":test_loss, "l1":l1, "acc":test_acc }) self.report() defreport(self): print(""" |-----------------------------------------------------------------------------------------------------| || |Reportforhyperparameteroptimization| || |-----------------------------------------------------------------------------------------------------| |RUN|PERFORMANCE|CONFIGURATION| |------------|--------------------------------------|-------------------------------------------------|""") foridx,iteminenumerate(self.report_list): print("|Run{:02d}|Accuracy:{:.2f}%|Loss:{:.2f}|Conv-1:{}|Conv-2:{:3}|Linear-1:{:>4}|".format(idx, item[0]*100, item[1], item[2], item[3], item[4])) print("|------------|---------------------|----------------|--------------|---------------|------------------|") print("BestResults|Accuracy:{:.2f}%|Loss:{:.2f}|Conv-1:{}|Conv-2:{}|Linear-1:{:>4}|".format(self.best_results["acc"]*100, self.best_results["loss"], self.best_results["c1"], self.best_results["c2"], self.best_results["l1"])) defappend_to_report(self,acc,loss,c1,c2,l1): list_set=(acc,loss,c1,c2,l1) self.report_list.append(list_set) 調(diào)整 現(xiàn)在我們可以調(diào)整超參數(shù)了!通過使用%%time,在整個調(diào)整過程執(zhí)行完成后,我們可以看到整個過程花費的時間。讓我們保持學(xué)習(xí)率lrate=0.001和批量大小batch_sz=512,用我們之前定義的search_space實例化HyperSearch類,將verbose設(shè)置為True或False(根據(jù)你的喜好),然后調(diào)用optimize()方法開始調(diào)優(yōu)。 注意:在我的機器上(NVIDIA RTX 3070),完成這個過程大約需要50分鐘,所以如果你使用的是Colab上提供的GPU,可能需要大致相同的時間。 %%time lrate=0.001 batch_sz=512 hyper_search=HyperSearch(search_space,verbose=True) hyper_search.optimize() 完成整個優(yōu)化周期后,你應(yīng)該得到一個如下所示的表格: 結(jié)果 從表格中可以看出,最佳結(jié)果來自于Run 00,它具有c1=48、c2=96和l1=256。損失為0.84,準(zhǔn)確率為71.24%,這是一個不錯的改進,尤其是考慮到只有10個時期! 因此,現(xiàn)在我們已經(jīng)找到了在10個時期內(nèi)性能最佳的超參數(shù),讓我們對這個模型進行微調(diào)!我們可以在更多的時期內(nèi)訓(xùn)練它,并稍微降低學(xué)習(xí)率,以嘗試獲得更好的性能。 所以首先,讓我們定義我們想要使用的模型,并設(shè)置批量大小和學(xué)習(xí)率: classConfigNet(nn.Module): def__init__(self,l1=256,c1=48,c2=96,d1=0.1): super().__init__() self.d1=d1 self.conv1=nn.Conv2d(3,c1,3) self.conv2=nn.Conv2d(c1,c1,3) self.conv3=nn.Conv2d(c1,c2,3) self.conv4=nn.Conv2d(c2,c2,3,stride=2) self.flat=nn.Flatten() self.batch_norm=nn.BatchNorm1d(c2*144) self.fc1=nn.Linear(c2*144,l1) self.fc2=nn.Linear(l1,10) defforward(self,x): x=nn.functional.relu(self.conv1(x)) x=nn.functional.relu(self.conv2(x)) x=nn.functional.dropout(x,self.d1) x=nn.functional.relu(self.conv3(x)) x=nn.functional.relu(self.conv4(x)) x=nn.functional.dropout(x,0.5) x=self.flat(x) x=nn.functional.relu(self.batch_norm(x)) x=nn.functional.relu(self.fc1(x)) x=self.fc2(x) returnx model=ConfigNet().to(device) model=ConfigNet(l1=256,c1=48,c2=96,d1=0.1).to(device) batch_sz=512 lrate=0.0008 最后,我們可以將時期數(shù)設(shè)置為50,并更改保存權(quán)重的路徑。讓訓(xùn)練周期運行起來,如果進展停滯,early stopping將終止訓(xùn)練。 %%time early_stopping=EarlyStopping(tolerance=6,verbose=True,path="cifar-optimized-test.pth") loss_fn=nn.CrossEntropyLoss() optimizer=torch.optim.Adam(model.parameters(),lr=lrate) train_dataloader=DataLoader(training_data,batch_size=batch_sz,shuffle=True,num_workers=0) test_dataloader=DataLoader(test_data,batch_size=batch_sz,shuffle=True,num_workers=0) epochs=50 fortinrange(epochs): print(f"Epoch{t+1}-------------------------------") train_loss=train(train_dataloader,model,loss_fn,optimizer) test_loss,test_acc=test(test_dataloader,model,loss_fn) #EarlyStopping early_stopping.step(test_loss) ifearly_stopping.early_stop: break print("Done!") Early stopping應(yīng)該在達到50個時期之前終止訓(xùn)練,并且應(yīng)該達到約77%的準(zhǔn)確率。 現(xiàn)在,我們已經(jīng)調(diào)整了超參數(shù),找到了最佳配置,并對該模型進行了微調(diào),現(xiàn)在是對模型的性能進行更深入評估的時候了。 模型評估 在這種情況下,我們的測試數(shù)據(jù)集實際上是我們的驗證數(shù)據(jù)。我們將重復(fù)使用我們的驗證數(shù)據(jù)來評估模型,但通常在超參數(shù)調(diào)整之后,你將希望使用真實的測試數(shù)據(jù)進行模型評估。 讓我們加載優(yōu)化后的模型,準(zhǔn)備沒有應(yīng)用任何圖像增強的test_dataloader,并運行test()來進行評估。 model=ConfigNet(l1=256,c1=48,c2=96,d1=0.1).to(device) model.load_state_dict(torch.load("cifar-optimized-test.pth")) loss_fn=nn.CrossEntropyLoss() batch_sz=512 test_dataloader=DataLoader(test_data,batch_size=batch_sz,shuffle=False,num_workers=0) classes=test_data.classes test_loss,test_acc=test(test_dataloader,model,loss_fn) 這應(yīng)該會輸出準(zhǔn)確率和損失: 總體性能不錯,但每個類別的性能對我們更有用。以下代碼將輸出數(shù)據(jù)集中每個類別的模型準(zhǔn)確率: correct_pred={classname:0forclassnameinclasses} total_pred={classname:0forclassnameinclasses} withtorch.no_grad(): fordataintest_dataloader: images,labels=data outputs=model(images.to(device)) _,predictions=torch.max(outputs,1) forlabel,predictioninzip(labels,predictions): iflabel==prediction: correct_pred[classes[label]]+=1 total_pred[classes[label]]+=1 forclassname,correct_countincorrect_pred.items(): accuracy=100*float(correct_count)/total_pred[classname] print(f"Accuracyforclass{classname:5s}:{accuracy:.1f}%") 執(zhí)行此代碼塊將給出以下輸出: 我們的模型在飛機、汽車、青蛙、船和卡車類別上表現(xiàn)非常好。有趣的是,它在狗和貓這兩個類別上遇到了最大的困難,這也是前面這個系列中完全連接模型面臨的最棘手的類別。 混淆矩陣 我們可以通過混淆矩陣進一步了解模型的性能。讓我們設(shè)置一個混淆矩陣,并進行可視化。 num_classes=10 confusion_matrix=torch.zeros(num_classes,num_classes) withtorch.no_grad(): fori,(inputs,classes)inenumerate(test_dataloader): inputs=inputs.to(device) classes=classes.to(device) outputs=model(inputs) _,preds=torch.max(outputs,1) fort,pinzip(classes.view(-1),preds.view(-1)): confusion_matrix[t.long(),p.long()]+=1 通過定義混淆矩陣,我們可以使用Seaborn庫來幫助我們可視化它。 plt.figure(figsize=(15,10)) cf_dataframe=pd.DataFrame(np.array(confusion_matrix,dtype="int"),index=test_data.classes,columns=test_data.classes) heatmap=sns.heatmap(cf_dataframe,annot=True,fmt="g") 這個表格的兩個維度是“實際”和“預(yù)測”值。我們希望大部分數(shù)據(jù)都在中心對角線上對齊,即實際和預(yù)測屬于同一類別。從錯誤的預(yù)測中,我們可以看到模型經(jīng)?;煜埡凸?,這兩個類別的準(zhǔn)確率最低。 總數(shù)看起來不錯,但每個類別的精確度和召回率將為我們提供更有意義的數(shù)據(jù)。讓我們首先看一下每個類別的召回率。 每個類別的召回率cf=np.array(confusion_matrix) norm_cf=cf/cf.astype(float).sum(axis=1) plt.figure(figsize=(15,10)) cf_dataframe=pd.DataFrame(np.array(norm_cf,dtype="float64"),index=test_data.classes,columns=test_data.classes).astype(float) heatmap=sns.heatmap(cf_dataframe,annot=True) 每個類別的精確度cf=np.array(confusion_matrix) norm_cf=cf/cf.astype(float).sum(axis=0) plt.figure(figsize=(15,10)) cf_dataframe=pd.DataFrame(np.array(norm_cf,dtype="float64"),index=test_data.classes,columns=test_data.classes).astype(float) heatmap=sns.heatmap(cf_dataframe,annot=True) 樣本模型預(yù)測 最后,讓我們給模型提供幾張圖像,并檢查它的預(yù)測結(jié)果。讓我們創(chuàng)建一個函數(shù)來準(zhǔn)備我們的圖像數(shù)據(jù)以供查看: defimshow(img): img=img/2+.05#revertnormalizationforviewing npimg=img.numpy() plt.imshow(np.transpose(npimg,(1,2,0))) plt.show() 現(xiàn)在,我們可以準(zhǔn)備我們的測試數(shù)據(jù),并創(chuàng)建另一個函數(shù)來獲取n個樣本預(yù)測。 test_data=CIFAR10(root="cifar", train=False, transform=transforms.ToTensor()) classes=test_data.classes defsample_predictions(n=4): test_dataloader=DataLoader(test_data,batch_size=n,shuffle=True,num_workers=0) dataiter=iter(test_dataloader) images,labels=dataiter.next() outputs=model(images.to(device)) _,predicted=torch.max(outputs,1) imshow(make_grid(images)) print("[GroundTruth|Predicted]:","".join(f"[{classes[labels[j]]:5s}|{classes[predicted[j]]:5s}]"forjinrange(n))) 調(diào)用該函數(shù),傳遞你想要采樣的圖像數(shù)量。輸出將給出每個圖像的實際類別和預(yù)測類別,從左到右。 利用經(jīng)過超參數(shù)調(diào)優(yōu)和圖像增強的卷積網(wǎng)絡(luò),我們成功提高了在CIFAR-10數(shù)據(jù)集上的性能!感謝你的閱讀,希望你對PyTorch和用于圖像分類的卷積神經(jīng)網(wǎng)絡(luò)有所了解。這里提供了包含所有代碼的完整筆記本在GitHub上可用。 https://github.com/florestony54/intro-to-pytorch-2/blob/main/pytorch2_2.ipynb
標(biāo)簽: