引言
本篇文章是PyTorch中從頭開始建立最受欢迎的卷積神經網絡系列的繼續。您可以在這裡查看前一篇文章,我們在那裡建立了LeNet5。在本文中,我們將建立AlexNet,這是计算机視覺中最关键的突破算法之一。
我們將從調查和理解AlexNet的架構開始。然後,我們將直接通過載入我們的數據集CIFAR-10進入程式碼,并对数据進行一些预处理。然後,我們將使用PyTorch
從頭開始建立我們的AlexNet,並在我們預處理过的數據上對其進行訓練。最後,將對未見過(測試)數據進行測試,以進行評估。
前提知識
了解神經網絡對理解本文有益。這將包括熟悉神經網絡的不同層(輸入層,隱藏層,輸出層),激活函數,優化算法(梯度下降的變體),損失函數等。此外,熟悉Python語法和PyTorch庫對於理解本文中展示的程式碼片段是必要的。
理解卷積 neural network(CNN)是必要的,這包括卷積層、池化層以及它們從輸入數據中提取特徵的角色。理解像步長、填充以及核/過濾器大小對的影響也是有益的。
AlexNet
AlexNet 是一個深層卷積 neural network,最初是由 Alex Krizhevsky 和他的同事在 2012 年開發的。它是為了classify 圖像而設計的,用於 ImageNet LSVRC-2010 比賽,在這場比賽中取得了最好的成績。您可以在原始研究論文這裡详盡地閱讀關於該模型的內容。
讓我們來看看 AlexNet 論文的主要收获。首先,AlexNet 操作3-channel 圖像,大小為 (224x224x3),在 subsample 時使用 max pooling 以及 ReLU 激活。用於卷積的核是 11×11、5×5 或 3×3,而用於最大池化的核大小為 3×3。它將圖像classify 成 1000個類別。它還使用了多個 GPU。
數據集
我們先載入並進行資料的前處理。為了本課程的需要,我們將使用CIFAR-10數據集。這個數據集包括10個類別的60000張32×32彩色圖片,每個類別有6000張圖片。其中包含50000張訓練圖片和10000張測試圖片。
以下是數據集中的類別,以及每個類別的10張隨機樣本圖片:
來源:source
類別之間是完全独立的。汽車和卡車之間沒有重疊。”汽車”包括轎車、SUV等類型的車輛。”卡車”只包括大型卡車。 neither 包括皮卡。
導入庫存
首先,我們需要導入必要的庫,並定義一個變量device
,使 notebook 知道如果可用,則使用 GPU 訓練模型。
import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler
# 設備配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
載入數據集
使用 torchvision
(計算機視覺任務的輔助函式庫),我們將加載我們的數據集。這種方法有一些助手函數,使得預處理變得非常簡單直接。讓我們定義函數get_train_valid_loader
和get_test_loader
,然後調用它們以加載和處理我們的CIFAR-10數據:
def get_train_valid_loader(data_dir,
batch_size,
augment,
random_seed,
valid_size=0.1,
shuffle=True):
normalize = transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2023, 0.1994, 0.2010],
)
# 定義轉換
valid_transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
if augment:
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize,
])
else:
train_transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
# 載入數據集
train_dataset = datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=train_transform,
)
valid_dataset = datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=valid_transform,
)
num_train = len(train_dataset)
indices = list(range(num_train))
split = int(np.floor(valid_size * num_train))
if shuffle:
np.random.seed(random_seed)
np.random.shuffle(indices)
train_idx, valid_idx = indices[split:], indices[:split]
train_sampler = SubsetRandomSampler(train_idx)
valid_sampler = SubsetRandomSampler(valid_idx)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, sampler=train_sampler)
valid_loader = torch.utils.data.DataLoader(
valid_dataset, batch_size=batch_size, sampler=valid_sampler)
return (train_loader, valid_loader)
def get_test_loader(data_dir,
batch_size,
shuffle=True):
normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
)
# 定義轉換
transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
dataset = datasets.CIFAR10(
root=data_dir, train=False,
download=True, transform=transform,
)
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=shuffle
)
return data_loader
# CIFAR10 數據集
train_loader, valid_loader = get_train_valid_loader(data_dir = './data', batch_size = 64,
augment = False, random_seed = 1)
test_loader = get_test_loader(data_dir = './data',
batch_size = 64)
讓我們來解析代碼:
- 我們定義兩個函數
get_train_valid_loader
和get_test_loader
分別用於加載訓練/驗證和測試集 - 我們首先定義變量
normalize
,包含數據集中每個通道(紅色、綠色和藍色)的平均值和標準差。這些可以手動計算,但因為CIFAR-10非常流行,也可以在線上找到 - 對於我們的訓練數據集,我們還添加了增加數據集增強的選項,以進行更健壯的訓練並增加圖像數量。注意:增強僅適用於訓練子集,不適用於驗證和測試子集,因為它們僅用於評估目的
- 我們將訓練數據集分成訓練和驗證集(90:10比率),並從整個訓練集中隨機取樣
- 我們指定批量大小,並在加載時打亂數據集,這樣每個批次在標籤類型上都會有一些變化。這將提高我們結果模型的效能
- 最後,我們使用了數據加載器。對於像CIFAR-10這樣的小型數據集,這可能不會影響性能,但對於大型數據集來說,這可能會严重影响性能,並且一般被認為是一個好做法。數據加載器讓我們可以批量迭代數據,而在迭代過程中load數據,而不是一次性全部start載入RAM中
從零開始的AlexNet
讓我們先從代碼開始:
class AlexNet(nn.Module):
def __init__(self, num_classes=10):
super(AlexNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
nn.BatchNorm2d(96),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.layer2 = nn.Sequential(
nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.layer3 = nn.Sequential(
nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(384),
nn.ReLU())
self.layer4 = nn.Sequential(
nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(384),
nn.ReLU())
self.layer5 = nn.Sequential(
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 3, stride = 2))
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(9216, 4096),
nn.ReLU())
self.fc1 = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU())
self.fc2= nn.Sequential(
nn.Linear(4096, num_classes))
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
out = self.fc1(out)
out = self.fc2(out)
return out
定義AlexNet模型
讓我們深入了解上面代碼的運作方式:
- 在PyTorch中定義任何神經網絡(無論是CNN還是其他)的第一步是定義一個繼承自
nn.Module
的類,因為它包含我們將要用到的許多方法 - 然後有兩個主要步驟。第一個是初始化我們要在CNN中使用的層在
__init__
內,另一個是定義這些層處理圖像的順序。這在forward
函數內定義。 - 對於架构本身,我們首先使用
nn.Conv2D
函數定義卷積層,並設定適當的核大小以及輸入/輸出通道。我們也使用nn.MaxPool2D
函數應用最大池化。PyTorch 的一個好处是,我們可以把卷積層、激活函數和最大池化組合在一個單一的層中(它們將分別應用,但這有助於組織)使用nn.Sequential
函數 - 然後我們使用線性(
nn.Linear
)和dropout(nn.Dropout
)以及ReLu激活函數(nn.ReLU
)來定義完全連接到層,並使用nn.Sequential
函數將這些結合在一起 - 最後,我們的最後一層派出10個神经元,這是我們對於10個類別的對象的最後預測
設定超參數
在訓練之前,我們需要設定一些超參數,如要用到的損失函數和優化器,以及批次大小、學習率與迭代次數。
num_classes = 10
num_epochs = 20
batch_size = 64
learning_rate = 0.005
model = AlexNet(num_classes).to(device)
# 損失函數和優化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)
# 訓練模型
total_step = len(train_loader)
我們先定義簡單的超參數(批次大小、學習率與迭代的回合數)並使用類別數目作為參數來初始化我們的模型,在這個案例中類別數目是10,並將模型移转到適當的設備(CPU或GPU)。然後我們定義我們的損失函數為交叉熵損失,並將優化器設定為Adam。這些選擇很多,但這些通常在模型和給定的數據上給出良好的結果。最後,我們定義total_step
來更好地追蹤訓練步驟
訓練
在這個點上我們已經準備好訓練我們的模型了:
total_step = len(train_loader)
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
# 將張量移到配置設備
images = images.to(device)
labels = labels.to(device)
# 前向傳播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向傳播和優化
optimizer.zero_grad()
loss.backward()
optimizer.step()
print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
.format(epoch+1, num_epochs, i+1, total_step, loss.item()))
# Validation
with torch.no_grad():
correct = 0
total = 0
for images, labels in valid_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
del images, labels, outputs
print('Accuracy of the network on the {} validation images: {} %'.format(5000, 100 * correct / total))
讓我們看看代碼做了什麼:
- 我們從迭代回合數開始,然後是訓練數據中的批次
- 我們根據所用設備(即是GPU或CPU)將圖像和標籤進行轉換
- 在正向傳播過程中,我們使用模型進行預測並根據預測和實際標籤計算損失
- 接下來,我們進行反向傳播,在這裡我們實際上更新我們的權重以改善我們的模型
- 然後,在每次更新之前,我們使用
optimizer.zero_grad()
功能將梯度置零 - 然後,我們使用
loss.backward()
功能計算新的梯度。 - 最後,我們使用 `
optimizer.step()
` 函數來更新權重 - 此外,在每個epoch的末尾,我們使用我們的驗證集來計算模型的準確性。在這個情況下,我們不需要梯度,因此我們使用 `
with torch.no_grad()
` 以進行更快的評估
我們可以看到如下輸出:
訓練損失和驗證準確性
如我們所見,損失隨著每個epoch的減少,這表明我們的模型的確在學習。注意,這個損失是在訓練集上,如果損失太小,可能會指向過擬合。這就是我們也使用驗證集的原因。驗證集中的準確性似乎在增加,這表明過擬合的機率不大。現在讓我們來測試我們的模型,看看它的表現如何。
測試
現在,我們來看看我們的模型在未見过的數據上的表現:
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
del images, labels, outputs
print('Accuracy of the network on the {} test images: {} %'.format(10000, 100 * correct / total))
注意,用於我們的驗證目的的代碼是完全一樣的。
使用這個模型,並且只訓練了6個epochs,我們在驗證集上似乎得到了約78.8%的準確性。
測試準確性
結論
現在讓我們來結論本篇文章我們做了什麼:
- 我們首先了解AlexNet模型的結構及不同的層
- 然後,我們使用
torchvision
載入和預處理CIFAR-10數據集 - 接下來,我們從頭開始用
PyTorch
建立我們的AlexNet模型 - 最後,我們在CIFAR-10數據集上訓練和測試了我們的模型,模型的表現似乎在測試數據上很好,僅僅通過很少的訓練(6個時期)
未來工作
這篇文章提供了一个扎实的介紹和实践經驗,但如果你進一步探索並發現你能完成什麼,你將獲得更多的知識。
- 你可以嘗試使用不同的數據集。一個這樣的數據集是CIFAR-100,它有100個類別,是CIFAR-10數據集的擴展
- 你可以嘗試不同的超參數,並查看它們對於模型的最佳組合
- 最後,你可以嘗試從數據集中添加或移除層,以查看它們對於模型的能力的影響。
Source:
https://www.digitalocean.com/community/tutorials/alexnet-pytorch