介紹
在本系列的上一部分中,我們使用了CIFAR-10數據集,並介紹了PyTorch的基礎知識:
張量及其相關操作
數據集和數據加載器
構建基本的神經網絡
基本模型的訓練和評估
我們爲CIFAR-10數據集中的圖像分類开發的模型只能在驗證集上達到53%的准確率,並且在一些類別(如鳥類和貓類)的圖像分類上表現非常困難(約33-35%的准確率)。這是預期的,因爲我們通常會使用卷積神經網絡進行圖像分類。在本教程系列的這一部分,我們將專注於卷積神經網絡(CNN)並改善在CIFAR-10上的圖像分類性能。
CNN基礎知識
在我們深入代碼之前,讓我們討論卷積神經網絡的基礎知識,以便更好地理解我們的代碼在做什么。如果你已經對CNN的工作原理感到熟悉,可以跳過本節。
與前一部分中开發的前饋網絡相比,卷積神經網絡具有不同的架構,並由不同類型的層組成。在下圖中,我們可以看到典型CNN的一般架構,包括它可能包含的不同類型的層。
卷積網絡中通常包含的三種類型的層是:
卷積層(紅色虛线框)
池化層(藍色虛线框)
全連接層(紅色和紫色實线框)
卷積層
CNN的定義組件和第一層是卷積層,它由以下部分組成:
輸入數據(在本例中爲圖像)
濾波器
特徵圖
將卷積層與全連接層區分开來的關鍵是卷積運算。我們不會詳細討論卷積的定義,但如果你真的感興趣並想深入了解其數學定義以及一些具體的示例,我強烈推薦閱讀這篇文章,它在解釋數學定義方面做得非常好
https://betterexplained.com/articles/intuitive-convolution/#Part_3_Mathematical_Properties_of_Convolution
卷積相對於密集連接層(全連接層)在圖像數據中的優勢何在?簡而言之,密集連接層會學習輸入中的全局模式,而卷積層具有學習局部和空間模式的優勢。這可能聽起來有些模糊或抽象,所以讓我們看一個例子來說明這是什么意思。
在圖片的左側,我們可以看到一個基本的2D黑白圖像的4是如何在卷積層中表示的。
紅色方框是濾波器/特徵檢測器/卷積核,在圖像上進行卷積操作。在右側是相同圖像在一個密集連接層中的表示。你可以看到相同的9個圖像像素被紅色的卷積核框起來。請注意,在左側,像素在空間上是分組的,與相鄰的像素相鄰。然而,在右側,這相同的9個像素不再是相鄰的。
通過這個例子,我們可以看到當圖像被壓平並表示爲完全連接/线性層時,空間/位置信息是如何丟失的。這就是爲什么卷積神經網絡在處理圖像數據時更強大的原因。輸入數據的空間結構得到保留,圖像中的模式(邊緣、紋理、形狀等)可以被學習。
這基本上是爲什么在圖像上使用卷積神經網絡的原因,但現在讓我們討論一下如何實現。讓我們來看看我們的輸入數據的結構,我們一直在談論的那些叫做“濾波器”的東西,以及當我們將它們放在一起時卷積是什么樣子。
輸入數據
CIFAR-10數據集包含60,000個32x32的彩色圖像,每個圖像都表示爲一個3D張量。每個圖像將是一個(32,32,3)的張量,其中的維度是32(高度)x 32(寬度)x 3(R-G-B顏色通道)。下圖展示了從數據集中分離出來的飛機全彩色圖像的3個不同的顏色通道(RGB)。
通常將圖像視爲二維的,所以很容易忘記它們實際上是以三維表示的,因爲它們有3個顏色通道!
濾波器
在卷積層中,濾波器(也稱爲卷積核或特徵檢測器)是一組權重數組,它以滑動窗口的方式在圖像上進行掃描,計算每一步的點積,並將該點積輸出到一個稱爲特徵圖的新數組中。這種滑動窗口的掃描稱爲卷積。讓我們看一下這個過程的示例,以幫助理解正在發生的事情。
一個3x3的濾波器(藍色)對輸入(紅色)進行卷積,生成一個特徵圖(紫色):
在每個卷積步驟中計算點積的示意圖:
需要注意的是,濾波器的權重在每個步驟中保持不變。就像在全連接層中的權重一樣,這些值在訓練過程中進行學習,並通過反向傳播在每個訓練迭代後進行調整。
這些示意圖並不能完全展示所有情況。當訓練一個卷積神經網絡時,模型不僅在卷積層中使用一個濾波器是很常見的。通常在一個卷積層中會有32或64個濾波器,實際上,在本教程中,我們將在一個層中使用多達96個濾波器來構建我們的模型。
最後,雖然濾波器的權重是需要訓練的主要參數,但卷積神經網絡也有一些可以調整的超參數:
層中的濾波器數量
濾波器的維度
步幅(每一步濾波器移動的像素數)
填充(濾波器如何處理圖像邊界)
我們不會詳細討論這些超參數,因爲本文不旨在全面介紹卷積神經網絡,但這些是需要注意的重要因素。
池化層
池化層與卷積層類似,都是通過濾波器對輸入數據(通常是從卷積層輸出的特徵圖)進行卷積運算。
然而,池化層的功能不是特徵檢測,而是降低維度或降採樣。最常用的兩種池化方法是最大池化和平均池化。在最大池化中,濾波器在輸入上滑動,並在每一步選擇具有最大值的像素作爲輸出。在平均池化中,濾波器輸出濾波器所經過像素的平均值。
全連接層
最後,在卷積和池化層之後,卷積神經網絡通常會有全連接層,這些層將在圖像分類任務中執行分類,就像本教程中的任務一樣。
現在,我們已經了解了卷積神經網絡的結構和操作方式,讓我們开始進行有趣的部分,在PyTorch中訓練我們自己的CNN模型!
設置
與本教程的第一部分一樣,我建議使用Google Colab進行跟隨,因爲你的Python環境已經安裝了PyTorch和其他庫,並且有一個GPU可以用於訓練模型。
因此,如果你使用的是Colab,請確保使用GPU,方法是轉到“運行時”(Runtime)並點擊“更改運行時類型”。
在對話框中選擇GPU並保存。
現在你可以在Colab中使用GPU了,並且我們可以使用PyTorch驗證你的設備。
因此,首先,讓我們處理導入部分:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision.utils import make_grid
from torchvision.datasets import CIFAR10
from torchvision import transforms
from torchvision import utils
from torchvision.utils import make_grid
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import pandas as pd
如果你想檢查你可以訪問的GPU是什么,請鍵入並執行torch.cuda.get_device_name(0),你應該會看到設備輸出。Colab有幾種不同的GPU選項可供選擇,因此你的輸出將根據你所能訪問的內容而有所不同,但只要你在運行此代碼時沒有看到“RuntimeError: No CUDA GPUs are available”錯誤,那么你正在使用GPU!
我們可以將GPU設備設置爲device,以便在开發模型時將其分配給GPU,如果沒有CUDA GPU設備可用,我們也可以使用CPU。
device = "cuda" if torch.cuda.is_available() else "cpu"
print(device)
# cuda
接下來,讓我們設置一個隨機種子,以便我們的結果是可重現的,並下載我們的訓練數據並設置一個轉換,將圖像轉換爲張量並對數據進行歸一化。
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)
一旦下載完成,讓我們查看數據集中的類別:
classes = training_data.classes
classes
#['airplane',
# 'automobile',
# 'bird',
# 'cat',
# 'deer',
# 'dog',
# 'frog',
# 'horse',
# 'ship',
# 'truck']
最後,讓我們設置訓練和測試數據加載器:
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)
for X, y in train_dataloader:
print(f"Shape of X [N, C, H, W]: {X.shape}")
print(f"Shape of y: {y.shape} {y.dtype}")
break
#Shape of X [N, C, H, W]: torch.Size([24, 3, 32, 32])
#Shape of y: torch.Size([24]) torch.int64
現在我們准備構建我們的模型!
構建CNN
在PyTorch中,nn.Conv2d是用於圖像輸入數據的卷積層。Conv2d的第一個參數是輸入中的通道數,在我們的第一層卷積層中,我們將使用3,因爲彩色圖像將有3個顏色通道。
在第一個卷積層之後,該參數將取決於前一層輸出的通道數。第二個參數是在該層中卷積操作輸出的通道數。這些通道是卷積層介紹中討論的特徵圖。最後,第三個參數將是卷積核或濾波器的大小。這可以是一個整數值,如3表示3x3的卷積核,或者是一個元組,如(3,3)。因此,我們的卷積層將採用nn.Conv2d(in_channels, out_channels, kernel_size)的形式。還可以添加其他可選參數,包括(但不限於)步幅(stride)、填充(padding)和膨脹(dilation)。在我們的卷積層conv4中,我們將使用stride=2。
在一系列卷積層之後,我們將使用一個扁平化層將特徵圖扁平化,以便能夠輸入到线性層中。爲此,我們將使用nn.Flatten()。我們可以使用nn.BatchNorm1d()應用批量歸一化,並需要將特徵數作爲參數傳遞。
最後,我們使用nn.Linear()構建线性的全連接層,第一個參數是特徵數,第二個參數是指定輸出特徵數。
因此,要开始定義我們模型的基本架構,我們將定義一個ConvNet類,該類繼承自PyTorch的nn.Module類。然後,我們可以將每個層定義爲類的屬性,並根據需要構建它們。
一旦我們指定了層的架構,我們可以通過創建一個forward()方法來定義模型的流程。我們可以使用激活函數包裝每個層,在我們的情況下,我們將使用relu。我們可以通過傳遞前一層和p(元素被丟棄的概率,缺省值爲0.5)在層之間應用dropout。
最後,我們創建模型對象並將其附加到設備上,以便可以在GPU上訓練。
class ConvNet(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)
def forward(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)
return x
model = ConvNet().to(device)
訓練和測試函數
如果你完成了本教程的第一部分,我們的訓練和測試函數將與之前創建的函數相同,只是在訓練方法中返回損失,而在測試方法中返回損失和正確數量,以便在調整超參數時使用。
# Train Method
def train(dataloader, model, loss_fn, optimizer, verbose=True):
size = len(dataloader.dataset)
model.train()
for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
if verbose == True:
if batch % 50 == 0:
loss, current = loss.item(), batch * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
return loss
# Test Method
def test(dataloader, model, loss_fn, verbose=True):
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
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
if verbose == True:
print(f"Test Error: Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} ")
return test_loss, correct # For reporting tuning results/ early stopping
最後,在基本模型訓練之前,我們定義損失函數和優化器。
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
讓我們訓練模型。
epochs = 10
for t in range(epochs):
print(f"Epoch {t+1}-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)
print("Done!")
僅經過10個epochs,61.7%的性能比我們訓練的全連接模型要好得多!很明顯,CNN更適合用於圖像分類,但我們可以通過延長訓練時間和調整超參數來進一步提高性能。
在進行這些之前,讓我們快速看看模型內部是什么樣子。請記住,濾波器的像素是我們模型中可訓練的參數。這不是訓練圖像分類模型的必要步驟,也不會得到太多有用的信息,但是了解模型內部的情況還是挺有意思的。
可視化濾波器
我們可以編寫一個函數來繪制模型中指定層的濾波器。我們只需要指定要查看的層,並將其傳遞給我們的函數。
def visualizeTensor(tensor, ch=0, all_kernels=False, nrow=8, padding=1):
n,c,w,h = tensor.shape
if all_kernels:
tensor = tensor.view(n*c, -1, w, h)
elif c != 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() # back to cpu for numpy and plotting
plt.figure( figsize=(nrow,rows) )
plt.imshow(grid.numpy().transpose((1, 2, 0)))
讓我們來看看第一個卷積層(conv1)中的濾波器是什么樣子,因爲這些濾波器直接應用於圖像。
filter = model.conv1.weight.data.clone()
visualizeTensor(filter)
plt.axis('off')
plt.ioff()
plt.show
下面是輸出,包含了我們的conv1卷積層中48個濾波器的可視化。我們可以看到每個濾波器都是一個不同值或顏色的3x3張量。
如果我們的濾波器是5x5的,我們會在繪圖中看到以下差異。請記住,使用nn.Conv2d我們可以使用第三個參數更改濾波器的大小,因此如果我們想要一個5x5的濾波器,conv1將如下所示:
self.conv1 = nn.Conv2d(3, 48, 5) # New Kernel Size
如果我們用新的5x5濾波器重新訓練模型,輸出將如下所示:
如我之前提到的,這裏並沒有太多有用的信息,但還是很有趣可以看到這些。
超參數優化
在本教程中,我們將調整的超參數是卷積層中的濾波器數量以及线性層中的神經元數量。當前這些值在我們的模型中是硬編碼的,所以爲了使它們可調整,我們需要使我們的模型可配置。
我們可以在模型的__init__方法中使用參數(c1、c2和l1),並使用這些值創建模型的層,在調整過程中將動態傳遞這些值。
class ConfigNet(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)
def forward(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)
return x
model = ConfigNet().to(device)
當然,我們不僅限於調整這些超參數。事實上,學習率和批量大小通常也包括在要調整的超參數列表中,但由於我們將使用網格搜索,爲了保持訓練時間合理,我們必須大大減少可調整的變量數量。
接下來,讓我們爲搜索空間定義一個字典,並保存給我們最佳結果的參數。由於我們使用網格搜索進行優化,將使用每個超參數組合的所有組合。
你可以輕松地向每個超參數的列表中添加更多值,但每個額外的值都會大大增加運行時間,因此建議從以下值开始以節省時間。
search_space = {
'c1': [48, 96],
'c2': [96, 192],
'l1': [256, 512],
}
best_results = {
'c1': None,
'c2': None,
'l1': None,
'loss': None,
'acc': 0
}
提前停止
優化過程中一個重要的組成部分是使用提前停止。由於我們將進行多次訓練運行,每次訓練運行時間都很長,如果訓練性能沒有改善,我們將希望提前結束訓練。繼續訓練一個沒有改善的模型是沒有意義的。
實質上,我們將在每個時期之後跟蹤模型產生的最低損失。然後,我們定義一個容差,指定模型必須在多少個時期內達到更好的損失。如果在指定的容差內沒有實現更低的損失,將終止該運行的訓練,並繼續下一個超參數組合。
如果你像我一樣,喜歡檢查訓練過程,可以設置self.verbose = True來記錄控制台上的更新,並查看提前停止計數器增加的情況。你可以在此處硬編碼到EarlyStopping類中,也可以在優化過程中實例化EarlyStopping對象時更改verbose值。
class EarlyStopping():
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
def step(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):
self.lowest_loss = val_loss
self.counter = 0
torch.save(model.state_dict(), self.path)
else:
if self.verbose:
print("Early stop counter: {}".format(self.counter+1))
self.counter +=1
if self.counter >= self.tolerance:
self.early_stop = True
if self.verbose:
print('Early stopping executed.')
圖像增強
在設置超參數優化方法之前,我們還有最後一件事要做,以提取出一些額外的性能並避免在訓練數據上過度擬合。圖像增強是一種將隨機變換應用於圖像的技術,從本質上講,它會創建“新的”人工數據。這些變換可以是以下幾種:
旋轉圖像幾度
水平/垂直翻轉圖像
裁剪
輕微的亮度/色調變化
隨機縮放
包含這些隨機變換將提高模型的泛化能力,因爲增強後的圖像將與原始圖像類似,但不同。內容和模式將保持不變,但數組表示將有所不同。
PyTorch通過torchvision.transforms模塊使圖像增強變得很容易。如果我們想要應用多個變換,可以使用Compose將它們鏈接在一起。
需要記住的一點是,圖像增強對每個變換需要一點計算量,並且這些計算量應用於數據集中的每個圖像。將許多不同的隨機變換應用於我們的數據集將增加訓練時間。
因此,現在讓我們限制變換的數量,以便訓練時間不會太長。如果你想添加更多變換,請查看PyTorch關於轉換和增強圖像的文檔,然後將它們添加到Compose列表中。
選擇了增強變換之後,我們可以像應用規範化和將圖像轉換爲張量一樣將它們應用於數據集。
# Augment Images for the train set
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))
])
# Standard transformation for validation set
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)
現在我們已經在訓練數據上設置了圖像增強,我們准備設置我們的超參數優化方法。
定義優化方法
我們可以創建一個類(HyperSearch),其中包含超參數值配置、詳細報告設置、報告列表(以便在優化完成後查看每個配置的表現)的屬性,以及一個變量來存儲具有最佳性能的配置。
class HyperSearch():
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
}
接下來,我們可以創建一個方法(仍在HyperSearch類中),以執行網格搜索,並對每個超參數組合進行訓練運行。首先,我們將使用tolerance=3配置EarlyStopping,並設置它保存每個超參數組合的權重。如果我們將self.verbose設置爲True,我們可以在控制台中看到當前正在訓練的超參數組合。
之後,我們使用我們設計的CoinfigNet模型定義我們的模型,並傳遞l1、c1和c2的值,同時選擇損失函數和優化器,並設置我們的訓練和驗證DataLoader。由於我們沒有時間也沒有意愿完全訓練每個組合,所以我們將保持較低的時期數。目標是了解哪種組合在對數據集進行分類時效果最好,然後我們可以將該模型完全訓練,以查看它在完整的訓練周期中的性能。
# Optimization Method
def optimize(self):
for l1 in self.config['l1']:
for c1 in self.config['c1']:
for c2 in self.config['c2']:
early_stopping = EarlyStopping(tolerance=3, verbose=False, path="{}-{}-{}.pth".format(c1, c2, l1))
if self.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)
現在,我們定義訓練循環,大部分與之前相同,只是現在我們將保存train和test方法的損失,以便early_stopping可以跟蹤訓練進展(或缺乏進展)。最後,在每個時期之後,將結果保存到報告中,並更新最佳損失的值。
epochs = 10
for t in range(epochs):
if self.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)
# Early Stopping
early_stopping.step(test_loss)
if early_stopping.early_stop:
break
print("Done!")
self.append_to_report(test_acc, test_loss, c1, c2, l1)
if self.best_results['loss'] == None or test_loss < self.best_results['loss']:
if self.verbose == True:
print("UPDATE: Best loss changed from {} 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()
我們可以將整個超參數優化周期的結果輸出到一個漂亮的表格中,在表格中,我們可以看到每次運行的超參數配置,以及相應的損失和准確率。
def report(self):
print("""
|-----------------------------------------------------------------------------------------------------|
| |
| Report for hyperparameter optimization |
| |
|-----------------------------------------------------------------------------------------------------|
| RUN | PERFORMANCE | CONFIGURATION |
|------------|--------------------------------------|-------------------------------------------------|""")
for idx, item in enumerate(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("Best Results | 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']))
def append_to_report(self, acc, loss, c1, c2, l1):
list_set = (acc, loss, c1, c2, l1)
self.report_list.append(list_set)
因此,將所有這些代碼放在一起,我們的HyperSearch類應該如下所示:
class HyperSearch():
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,
}
# Optimization Method
def optimize(self):
for l1 in self.config['l1']:
for c1 in self.config['c1']:
for c2 in self.config['c2']:
early_stopping = EarlyStopping(tolerance=3, verbose=False, path="{}-{}-{}.pth".format(c1, c2, l1))
if self.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
for t in range(epochs):
if self.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)
# Early Stopping
early_stopping.step(test_loss)
if early_stopping.early_stop:
break
print("Done!")
self.append_to_report(test_acc, test_loss, c1, c2, l1)
if self.best_results['loss'] == None or test_loss < self.best_results['loss']:
if self.verbose == True:
print("UPDATE: Best loss changed from {} 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()
def report(self):
print("""
|-----------------------------------------------------------------------------------------------------|
| |
| Report for hyperparameter optimization |
| |
|-----------------------------------------------------------------------------------------------------|
| RUN | PERFORMANCE | CONFIGURATION |
|------------|--------------------------------------|-------------------------------------------------|""")
for idx, item in enumerate(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("Best Results | 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']))
def append_to_report(self, acc, loss, c1, c2, l1):
list_set = (acc, loss, c1, c2, l1)
self.report_list.append(list_set)
調整
現在我們可以調整超參數了!通過使用%%time,在整個調整過程執行完成後,我們可以看到整個過程花費的時間。讓我們保持學習率lrate=0.001和批量大小batch_sz=512,用我們之前定義的search_space實例化HyperSearch類,將verbose設置爲True或False(根據你的喜好),然後調用optimize()方法开始調優。
注意:在我的機器上(NVIDIA RTX 3070),完成這個過程大約需要50分鐘,所以如果你使用的是Colab上提供的GPU,可能需要大致相同的時間。
%%time
lrate=0.001
batch_sz=512
hyper_search = HyperSearch(search_space, verbose=True)
hyper_search.optimize()
完成整個優化周期後,你應該得到一個如下所示的表格:
結果
從表格中可以看出,最佳結果來自於Run 00,它具有c1=48、c2=96和l1=256。損失爲0.84,准確率爲71.24%,這是一個不錯的改進,尤其是考慮到只有10個時期!
因此,現在我們已經找到了在10個時期內性能最佳的超參數,讓我們對這個模型進行微調!我們可以在更多的時期內訓練它,並稍微降低學習率,以嘗試獲得更好的性能。
所以首先,讓我們定義我們想要使用的模型,並設置批量大小和學習率:
class ConfigNet(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)
def forward(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)
return x
model = ConfigNet().to(device)
model = ConfigNet(l1=256, c1=48, c2=96, d1=0.1).to(device)
batch_sz = 512
lrate = 0.0008
最後,我們可以將時期數設置爲50,並更改保存權重的路徑。讓訓練周期運行起來,如果進展停滯,early stopping將終止訓練。
%%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
for t in range(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)
# Early Stopping
early_stopping.step(test_loss)
if early_stopping.early_stop:
break
print("Done!")
Early stopping應該在達到50個時期之前終止訓練,並且應該達到約77%的准確率。
現在,我們已經調整了超參數,找到了最佳配置,並對該模型進行了微調,現在是對模型的性能進行更深入評估的時候了。
模型評估
在這種情況下,我們的測試數據集實際上是我們的驗證數據。我們將重復使用我們的驗證數據來評估模型,但通常在超參數調整之後,你將希望使用真實的測試數據進行模型評估。
讓我們加載優化後的模型,准備沒有應用任何圖像增強的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)
這應該會輸出准確率和損失:
總體性能不錯,但每個類別的性能對我們更有用。以下代碼將輸出數據集中每個類別的模型准確率:
correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}
with torch.no_grad():
for data in test_dataloader:
images, labels = data
outputs = model(images.to(device))
_, predictions = torch.max(outputs, 1)
for label,prediction in zip(labels, predictions):
if label == prediction:
correct_pred[classes[label]] += 1
total_pred[classes[label]] += 1
for classname, correct_count in correct_pred.items():
accuracy = 100 * float(correct_count) / total_pred[classname]
print(f'Accuracy for class {classname:5s}: {accuracy:.1f}%')
執行此代碼塊將給出以下輸出:
我們的模型在飛機、汽車、青蛙、船和卡車類別上表現非常好。有趣的是,它在狗和貓這兩個類別上遇到了最大的困難,這也是前面這個系列中完全連接模型面臨的最棘手的類別。
混淆矩陣
我們可以通過混淆矩陣進一步了解模型的性能。讓我們設置一個混淆矩陣,並進行可視化。
num_classes = 10
confusion_matrix = torch.zeros(num_classes, num_classes)
with torch.no_grad():
for i, (inputs, classes) in enumerate(test_dataloader):
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for t, p in zip(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')
這個表格的兩個維度是“實際”和“預測”值。我們希望大部分數據都在中心對角线上對齊,即實際和預測屬於同一類別。從錯誤的預測中,我們可以看到模型經常混淆貓和狗,這兩個類別的准確率最低。
總數看起來不錯,但每個類別的精確度和召回率將爲我們提供更有意義的數據。讓我們首先看一下每個類別的召回率。
每個類別的召回率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)
樣本模型預測
最後,讓我們給模型提供幾張圖像,並檢查它的預測結果。讓我們創建一個函數來准備我們的圖像數據以供查看:
def imshow(img):
img = img / 2 + .05 # revert normalization for viewing
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1,2,0)))
plt.show()
現在,我們可以准備我們的測試數據,並創建另一個函數來獲取n個樣本預測。
test_data = CIFAR10(root = "cifar",
train = False,
transform = transforms.ToTensor())
classes = test_data.classes
def sample_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('[Ground Truth | Predicted]:', ' '.join(f'[{classes[labels[j]]:5s} | {classes[predicted[j]]:5s}]' for j in range(n)))
調用該函數,傳遞你想要採樣的圖像數量。輸出將給出每個圖像的實際類別和預測類別,從左到右。
利用經過超參數調優和圖像增強的卷積網絡,我們成功提高了在CIFAR-10數據集上的性能!感謝你的閱讀,希望你對PyTorch和用於圖像分類的卷積神經網絡有所了解。這裏提供了包含所有代碼的完整筆記本在GitHub上可用。
https://github.com/florestony54/intro-to-pytorch-2/blob/main/pytorch2_2.ipynb
原文標題 : PyTorch 2簡介:卷積神經網絡
標題:PyTorch 2簡介:卷積神經網絡
地址:https://www.utechfun.com/post/230082.html