在PyTorch中從頭開始撰寫VGG

繼續在我的系列文章中探讨如何构建曾在過去1-2十年的间革新電腦視覺領域的經典卷積神經網絡,我們接下來自行使用PyTorch建造一个非常深的卷積神經網絡VGG。你可以在我的個人檔案中看到這個系列文章的前一篇文章,主要是LeNet5AlexNet

與此同時,我們將探讨VGG的結構和背後理念以及當時的表現結果。然後,我們將研究我們的數據集CIFAR100,並使用記憶效率高的代碼將其載入我們的程式中。接下來,我們將使用PyTorch從頭開始實現VGG16(數字指的是層數,主要有兩個版本:VGG16和VGG19),然後在我們的測試集中的數據上訓練它,並對其進行評估,以查看它在未見过的數據上的表現如何。


VGG

建立在AlexNet的工作之上,VGG着眼的另一種卷積神經網絡(CNNs)的重要方面是深度。它由Simonyan和Zisserman開發。它通常由16個卷積層組成,但也可以擴展到19層(因此有两个版本,VGG-16和VGG-19)。所有的卷積層都由3×3過濾器組成。您可以在官方論文這裡

了解更多關於網絡的詳細信息。來源


數據載入

數據集

在建立模型之前,任何機器學習项目中最重要的其中之一就是載入、分析與預處理數據集。在本文中,我們將使用CIFAR-100數據集。這個數據集就像CIFAR-10一樣,只是它有100個類別,每個類別含有600張圖像。每個類別有500張訓練圖像和100張測試圖像。CIFAR-100中的100個類別被分為20個超大類別。每張圖像都伴隨著一個“細”標籤(它所属的類別)和一個“粗”標籤(它所属的超類別)。我們將在此使用“細”標籤。以下是CIFAR-100的類別列表:


CIFAR-100數據集的類別列表

導入庫

我們將主要使用torch(用於建立模型和訓練)、torchvision(用於數據載入/處理,含有用於计算机视觉中處理這些數據集的數據集和方法)和numpy(用於數值計算)。我們還將定義一個變量device,使程序能夠在有可用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 是一個庫,提供輕鬆存取大量電腦視覺數據集,以及容易且直觀的方式來前處理這些數據集

  • 我們定義了一個函數 data_loader,它根據參數返回訓練/驗證數據或測試數據
  • 我們首先定義了一個變量 normalize,它包含了數據集中每個通道(紅、綠、藍)的平均值和標準差。這些可以手動計算,但也可以在網路上找到。這在 transform 變量中使用,我們將數據縮放到正確的大小,然後將其轉為張量並進行標準化
  • 如果 test 參數為真,我們只是載入數據集的測試部分,並使用數據载入器(稍後解釋)返回它
  • 如果 test 為假(這是預設行為),我們則載入數據集的訓練部分,並隨機將其分為訓練和驗證集(9:1)
  • 最後,我們使用數據载入器。對於像 CIFAR100 這樣的小數據集,這可能不會影響性能,但在大數據集情況下,這可能會严重影响性能,且通常被認為是一種良好實踐。數據载入器讓我們可以批量迭代數據,而且在迭代過程中載入數據,而不是在開始時將所有數據一次載入內存中。
def data_loader(data_dir,
                batch_size,
                random_seed=42,
                valid_size=0.1,
                shuffle=True,
                test=False):
  
    normalize = transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010],
    )

    # 定義變換
    transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
    ])

    if test:
        dataset = datasets.CIFAR100(
          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

    # 載入數據集
    train_dataset = datasets.CIFAR100(
        root=data_dir, train=True,
        download=True, transform=transform,
    )

    valid_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=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)


# CIFAR100 數據集
train_loader, valid_loader = data_loader(data_dir='./data',
                                         batch_size=64)

test_loader = data_loader(data_dir='./data',
                              batch_size=64,
                              test=True)

從頭開始建立 VGG16

要从頭開始建立模型,我們首先需要了解在 torch 中模型的定義如何工作,以及我們將在此使用的不同類型的層:

  • 每個自訂模型都需要從 nn.Module 類繼承,因為它提供了一些基本功能,幫助模型進行訓練。
  • 其次,我們需要做兩件主要的事情。首先,在 __init__ 函數內定義我們模型的不同層,以及這些層在 forward 函數內對於輸入数据將執行的順序

現在讓我們定義我們在此使用的各種類型的層:

  • nn.Conv2d:這些是卷積層,它接受輸入和輸出通道的數量作為參數,以及過濾器的核大小。它還接受任何步長或填充,如果你想要應用這些功能
  • nn.BatchNorm2d:此函數將批次正規化應用於卷積層的輸出
  • nn.ReLU:此為應用於網絡中各種輸出的激活函數。
  • nn.MaxPool2d : 此函式將應用最大池化至給定核大小之輸出
  • nn.Dropout: 此函式用於在給定機率下將輸出進行dropout
  • nn.Linear: 此為基本上是一個全連接層
  • nn.Sequential: technically 這不是一種層,但它有助於結合屬於同一步的不同操作

倚靠此知識,我們現在可以建立我們的VGG16模型,使用论文中的架構:

class VGG16(nn.Module):
    def __init__(self, num_classes=10):
        super(VGG16, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU())
        self.layer2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(), 
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer5 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer6 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer7 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer8 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer9 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer10 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer11 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer12 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer13 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(7*7*512, 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 = self.layer6(out)
        out = self.layer7(out)
        out = self.layer8(out)
        out = self.layer9(out)
        out = self.layer10(out)
        out = self.layer11(out)
        out = self.layer12(out)
        out = self.layer13(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.fc1(out)
        out = self.fc2(out)
        return out

從頭開始的VGG16


超参数

任何機器或深度學習项目中重要的部分是優化超参数。在這裡,我們不會experiment 不同的值那些,但我們將必須在hand 之前定義它們。這些包括定義迭代的次數、批次大小、學習率、損失函數以及優化器

num_classes = 100
num_epochs = 20
batch_size = 16
learning_rate = 0.005

model = VGG16(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)

設置超参数


訓練

我們現在準備訓練我們的模型。我們先看一下在 torch 中如何訓練我們的模型,然後查看代碼:

  • 對於每個時代,我們遍历我們train_loader中的圖像和標籤,並將這些圖像和標籤移到可用的GPU上。這個過程是自動的
  • 我們使用我們的模型來在標籤上進行預測(model(images)),然後使用我們的損失函數(criterion(outputs, labels))計算預測值和真實標籤之間的損失
  • 然後我們使用這個損失進行反向傳播(loss.backward)並更新權重(optimizer.step())。但請記住,在每次更新之前將梯度置為零。這是用optimizer.zero_grad()完成的
  • 此外,在每個時代的尾声,我們使用我們的驗證集來計算模型的準確性。在這個情況下,我們不需要梯度,因此我們使用with torch.no_grad()以加快評估

現在,我們將所有這些內容結合到下面的代碼中:

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()))
            
    # 驗證
    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)) 

訓練

我們可以從上面代碼的輸出看到如下結果,這表明模型的實際學習情況,因為損失隨著每個時代的減少:


訓練損失


測試

為測試,我們使用與驗證完全相同的代碼,但與test_loader一起:

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))   

測試

使用上述代碼並對模型進行20個週期的訓練,我們能夠在測試集中的準確率达到75%。


結論

現在讓我們總結本文中所做的事情:

  • 我們首先了解VGG-16模型的結構以及不同類型的層
  • 接下來,我們使用torchvision載入和預處理CIFAR100數據集
  • 然後,我們使用PyTorch從頭開始建造我們的VGG-16模型,並了解torch中不同類型的層
  • 最後,我們的模型在CIFAR100數據集上進行了訓練和測試,並且模型在測試數據集上表現出色,準確率达到75%

未來工作

通過本文,您獲得了一個很好的介紹和實戰學習,但如果您延伸這個並看看您還能做什麼,您將學習到更多:

  • 您可以嘗試使用不同的數據集。一個這樣的數據集是CIFAR10或ImageNet數據集的子集。
  • 您可以试验区別性的超参数,並查看對模型最佳的組合。
  • 最後,您可以嘗試從數據集中添加或移除層,以查看它們對模型性能的影響。更棒的是,嘗試建造這個模型的VGG-19版本。

Source:
https://www.digitalocean.com/community/tutorials/vgg-from-scratch-pytorch