VGG vanaf de grond aanmaken in PyTorch

Houdend aan mijn reeks artikelen over het bouwen van klassieke convolutionele neurale netwerken die de computervisuele wereld in de laatste 1-2 decennia revolutioneerden, zullen we nu VGG, een zeer diep convolutioneel neurale netwerk, vanaf de grond af opbouwen met PyTorch. U kunt de vorige artikelen in de reeks op mijn profiel zien, voornamelijk LeNet5 en AlexNet.

Net als eerder zullen we kijken naar de architectuur en intuïtie achter VGG en hoe de resultaten destijds waren. We zullen vervolgens onze dataset, CIFAR100, verkennen en hem in ons programma laden met behulp van code die geheugengefficiëntie is. Daarna zullen we VGG16 (het getal verwijst naar het aantal lagen, er zijn eigenlijk twee versies: VGG16 en VGG19) vanaf de grond af implementeren met PyTorch en trainen we het op ons dataset, waarna we de prestaties evalueren op ons testset om te zien hoe het presteert op ongeziene data


VGG

Bij het bouwen op het werk van AlexNet focusert VGG op een ander cruciale aspect van Convolutionele Neurale Netwerken (CNN’s), diepte. Het werd ontwikkeld door Simonyan en Zisserman. Het bestaat gewoonlijk uit 16 convolutionele lagen, maar kan ook uitgebreid worden tot 19 lagen (dus de twee versies, VGG-16 en VGG-19). Alle convolutionele lagen bestaan uit 3×3 filters. U kunt meer over het netwerk lezen in het officiële paper hier

VGG16-architectuur. Bron


Data Laden

Dataset

Voordat we beginnen met het bouwen van het model, is het laden, analyseren en voorbereiden van de dataset een van de belangrijkste stappen in elk project op het gebied van Machine Learning. In dit artikel zullen we werken met de CIFAR-100 dataset. Deze dataset is net als de CIFAR-10, maar bevat 100 klassen met elk 600 afbeeldingen. Er zijn 500 trainingsbeelden en 100 testbeelden per klasse. De 100 klassen in de CIFAR-100 zijn onderverdeeld in 20 superklassen. Elke afbeelding wordt geassocieerd met een “fijne” label (de klasse waartoe het behoort) en een “coarse” label (de superklasse waartoe het behoort). We zullen in dit artikel gebruik maken van het “fijne” label. Hier is de lijst met klassen in de CIFAR-100:


Lijst met klassen voor de CIFAR-100 dataset

Libraries importeren

We zullen vooral werken met torch (gebruikt voor het bouwen van het model en het trainen), torchvision (voor data- Laden/verwerking, bevat datasets en methodes voor de verwerking van deze datasets in computer vision) en numpy (voor wiskundige manipulatie). We zullen ook een variabele device definiëren zodat het programma de GPU gebruikt indien beschikbaar

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


# Apparaatconfiguratie
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Gegevens worden geladen

torchvision is een bibliotheek die gemakkelijke toegang biedt tot veel computervisuele datasetten en methodes om deze datasetten gemakkelijk en intuitief te voorbereiden

  • We definiëren een functie data_loader die afhankelijk van de argumenten train/validatiegegevens of testgegevens teruggeeft
  • We beginnen met het definiëren van de variabele normalize met de gemiddelden en standaardafwijkingen van elke kanaal (rood, groen en blauw) in het dataset. Dit kan handmatig worden berekend, maar is ook online beschikbaar. Dit wordt gebruikt in de variabele transform waar we de gegevens opnieuw schalen, omzetten in tensor en vervolgens normaliseren
  • Als het argument test waar is, laden we simpelweg de testpartij van het dataset en geven we dit terug met hulp van data loaders (explandeerd hieronder)
  • In het geval dat test niet waar is (standaardgedrag ook), laden we de trainpartij van het dataset en splitsen we deze random in train- en validatieset (0.9:0.1)
  • Ten slotte maakten we gebruik van data loaders. Dit kan de prestatie niet beïnvloeden bij een klein dataset zoals CIFAR100, maar kan de prestatie behoorlijk belemmeren bij grote datasets en wordt algemeen gezien als een goed gedrag. Data loaders staan ons toe om de gegevens in batchformat door te itereren, en de gegevens worden geladen terwijl geiterated wordt en niet allemaal direct in het begin van uw RAM geladen.
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],
    )

    # definieer transformaties
    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

    # laad het dataset
    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 dataset
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 vanaf het begin

Om het model vanaf het begin te bouwen, moeten we eerst begrijpen hoe modeldefinities werken in torch en de verschillende typen lagen die we hier zullen gebruiken:

  • Elke aangepaste model moet erfelijk zijn aan de nn.Module klasse, aangezien deze enkele basisfunctionaliteit biedt die het model helpt te trainen.
  • Tweede, er zijn twee hoofdactiviteiten die we moeten uitvoeren. Eerst, definieer de verschillende lagen van ons model binnen de __init__ functie en de sequentie waarin deze lagen op de invoer worden uitgevoerd binnen de forward functie.

Laat ons nu de verschillende typen lagen definiëren die we hier gebruiken:

  • nn.Conv2d: Dit zijn de convolutie lagen die het aantal invoer- en uitgangskanalen aanvaarden als argumenten, samen met de filter kleine voor het filter. Het accepteert ook eventuele strides of padding als u die wilt toepassen.
  • nn.BatchNorm2d: Dit past batch normalisatie toe op de uitvoer van de convolutie laag.
  • nn.ReLU: Dit is de activatie die wordt toegepast op de verschillende uitvoer in het netwerk.
  • nn.MaxPool2d : Dit toepast max pooling op de uitvoer met de kernelgrootte gegeven
  • nn.Dropout: Dit wordt gebruikt om dropout toe te passen op de uitvoer met een gegeven kans
  • nn.Linear: Dit is eigenlijk een volledig verbonden laag
  • nn.Sequential: Dit is technisch gezien geen soort laag, maar helpt bij het combineren van verschillende bewerkingen die deel uitmaken van hetzelfde stap

Met deze kennis kunnen we nu onze VGG16-model bouwen met de architectuur uit het artikel:

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 vanaf Scratch


Hyperparameters

Een van de belangrijke delen van elk machine- of diepgeleerdenproject is het optimaliseren van de hyperparameters. Hier zal niet geexperimenteerd worden met verschillende waarden voor die, maar we zullen ze vooraf moeten definiëren. Dit omvat het definiëren van het aantal epochs, batchgrootte, learning rate, verliesfunctie samen met de optimizer

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

model = VGG16(num_classes).to(device)


# Verlies en optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)  


# Train het model
total_step = len(train_loader)

Hyperparameters instellen


Trainen

We zijn nu klaar om ons model te trainen. We zullen eerst kijken hoe we ons model trainen in torch en vervolgens naar de code kijken:

  • Voor elke epoch ga we door de afbeeldingen en labels binnen ons train_loader en verplaats die afbeeldingen en labels naar de GPU als beschikbaar. Dit gebeurt automatisch
  • We gebruiken ons model om voorspellingen te maken op de labels (model(images)) en bereken vervolgens de verliesfunctie tussen de voorspellingen en de ware labels met behulp van onze verliesfunctie (criterion(outputs, labels))
  • Vervolgens gebruiken we dat verlies om terug te propageren (loss.backward) en de gewichten bij te werken (optimizer.step()). Vergeet echter niet om de gradiënten op nul te zetten voor elke update. Dat doen we met behulp van optimizer.zero_grad()
  • Ook aan het eind van elke epoch gebruiken we ons validatieset om de accurateit van het model te berekenen. In dit geval hoeft geen gradiënten gebruikt te worden, dus gebruiken we with torch.no_grad() voor sneller evalueren

Nu combineren we al dit samen in het volgende code:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # Verplaats tensoren naar de geconfigureerde apparaat
        images = images.to(device)
        labels = labels.to(device)
        
        # Voorwaartse doorgang
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # Terugwaartse en optimaliseer
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
            
    # Validatie
    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)) 

Trainen

We kunnen de uitvoer van bovenstaande code zien als volgt, die aangeeft dat het model daadwerkelijk leert omdat het verlies afneemt met elke epoch:


Training Verliezen


Testen

Voor testen gebruiken we exact hetzelfde code als voor validatie, maar met de 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))   

Testen

Met het bovenstaande code en door het trainen van het model voor 20 epochs, zijn we in staat geweest om een nauwkeurigheid van 75% op de testset te behalen.


Conclusie

Laten we nu samenvatten wat we in dit artikel hebben gedaan:

  • We zijn begonnen met het begrijpen van de architectuur en de verschillende soorten lagen in het VGG-16 model
  • Volgend zijn we de CIFAR100 dataset geladen en voorbereidt door middel van torchvision
  • Dan hebben we PyTorch gebruikt om ons VGG-16 model van de grond af op te bouwen, inclusief het begrijpen van de verschillende typen lagen beschikbaar in torch
  • Ten slotte hebben we ons model getraind en getest op de CIFAR100 dataset, en het model leek goed te presteren op de testdataset met 75% nauwkeurigheid

Toekomstige Werk

Met dit artikel krijg je een goede introductie en praktische leer maar je zult veel meer leren als je dit uitbreidt en zie wat je anders kunt doen:

  • U kunt proberen met verschillende datasets. Een dergelijk dataset is CIFAR10 of een subset van de ImageNet dataset.
  • U kunt experimenteren met verschillende hyperparameters en kijk welke combinatie er het beste is voor het model
  • U kunt tenslotte proberen om lagen toe te voegen of weg te halen uit het dataset om hun impact te zien op de capaciteit van het model. Nog beter, probeer de VGG-19 versie van dit model aan te bouwen.

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