在PyTorch中從頭開始撰寫AlexNet

引言

本篇文章是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_loaderget_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_loaderget_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