Schrijven van AlexNet vanaf de grond in PyTorch

Inleiding

Dit bericht is een vervolg op de reeks artikelen over het vanaf de grond aanbouwen van de meest populaire convolutionele neurale netwerken in PyTorch. U kunt de vorige post hier bekijken, waarin we LeNet5 bouwden. In dit artikel zullen we AlexNet bouwen, een van de meest cruciale doorbraakalgoritmen in de computervisualisatie.

We zullen beginnen met het onderzoeken en begrijpen van de architectuur van AlexNet. Daarna zal het directegaande codegaande zijn door onze dataset, CIFAR-10, te laden, vooraleer we enige voorbehandeling aan de gegevens toepassen. Vervolgens zullen we ons AlexNet vanaf de grond opbouwen met behulp van PyTorch en het trainen op onze voorbehandelde gegevens. Tot slot zal de getrainde model getest worden op onbekende (test) gegevens voor evaluatiedoeleinden.

Vereisten

Kennis van neurale netwerken zal helpen om dit artikel te begrijpen. Dit omvatte het gemakkelijk zijn met de verschillende lagen van neurale netwerken (inputlaag, verborgen lagen, uitgaanswering). Activatiefuncties, optimalisatiesalgoritmen (varianten van de gradiëntafname) en verliesfuncties enzovoort. Bovendien is bekendheid met de Python-syntaxis en de PyTorch-bibliotheek noodzakelijk om de in dit artikel getoonde codefragmenten te kunnen begrijpen.

Een begrip van CNN’s is essentieel. Dit omvat kennis van convolutieven, poolingslagen en hun rol in het ontleden van kenmerken uit invoerdata. begrip van concepten als stride, padding en de invloed van kernel/filtergrootte is ook nuttig.

AlexNet

AlexNet is een diepgaande convolutieele netwerk, dat voor het eerst werd ontwikkeld door Alex Krizhevsky en zijn medewerkers in 2012. Het was ontworpen om afbeeldingen te classificeren voor de ImageNet LSVRC-2010 competitie, waarin het de beste resultaten behaalde. U kunt over het model in detail lezen in het originele wetenschappelijke artikel hier.

Laten we de belangrijkste conclusies uit het AlexNet-artikel overzien. eerstens, had AlexNet operationeel met 3-kanaal afbeeldingen die (224x224x3) in grootte waren. Het gebruikte max pooling gekoppeld aan ReLU-activaties bij het onderverdelen. De kernen die voor convoluties werden gebruikt waren 11×11, 5×5 of 3×3 terwijl de kernen voor max pooling een grootte hadden van 3×3. Het classificeerde afbeeldingen in 1000 klassen. Het maakte ook gebruik van meerdere GPU’s.

Dataset

We beginnen met het laden en voorbereiden van de gegevens. Voor onze doeleinden gebruiken we de CIFAR-10 dataset. Deze dataset bestaat uit 60000 32×32 kleurafbeeldingen in 10 klassen, met 6000 afbeeldingen per klasse. Er zijn 50000 trainingsafbeeldingen en 10000 testafbeeldingen.

Hier zijn de klassen in de dataset, evenals 10 gelukkige voorbeelden van elke klasse:

Bron: source

De klassen zijn volledig uitsluitend van elkaar verschillend. Er is geen overlap tussen auto’s en vrachtwagens. “Auto” omvat sedanen, SUV’s en dergelijke. “Vrachtwagen” omvat alleen grote vrachtwagens. Geen van beide omvat pick-up vrachtwagens.

Bibliotheken Importeren

Laten we beginnen met het importeren van de vereiste bibliotheken en het definiëren van een variabele device, zodat de notebook weet om een GPU te gebruiken voor het trainen van het model, 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')

Dataset laden

We gaan gebruik maken van torchvision (een hulpprogramma voor computervisuele taken), om onze dataset te laden. Deze methode beschikt over enkele helperfuncties die de voorbereiding erg gemakkelijk en eenvoudig maken. Laat ons de functies get_train_valid_loader en get_test_loader definiëren, en dan deze aanroepen om ons CIFAR-10-gegevensbestand in te laden en te verwerken:

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

    # definieer transformaties
    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,
        ])

    # laad het gegevensbestand
    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],
    )

    # definieer transformatie
    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-gegevensbestand 
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)

Laat ons de code uitleggen:

  • We definiëren twee functies get_train_valid_loader en get_test_loader om respectievelijk train/validatie- en testsets te laden
  • We beginnen door de variabele normalize te definiëren met de gemiddelden en standaardafwijkingen van elke kanaal (rood, groen en blauw) in het gegevensbestand. Deze kunnen handmatig berekend worden, maar zijn ook online beschikbaar, omdat CIFAR-10 erg populair is
  • Voor ons trainingsgegevensbestand voegen we de optie toe om het gegevensbestand ook te veranderen om de training robust te maken en het aantal afbeeldingen te vergroten. Noteer: verandering wordt enkel toegepast op de trainingssubgroep en niet op de validatiesubgroep en testsubgroep, omdat deze alleen worden gebruikt voor evaluatiedoeleinden
  • We splitsen het trainingsgegevensbestand in een trainings- en validatiesubgroep (90:10-verhouding) en halen een willekeurige subgroep eruit uit het gehele trainingsbestand
  • We specificeren de batchgrootte en schudden het gegevensbestand bij het laden, zodat elke batch een beetje variantie in de labels bevat. Dit zal de effectiviteit van onze resulterende model verhogen.
  • Ten slotte maakten we gebruik van data loaders. Dit zou de prestatie niet beïnvloeden voor een klein dataset zoals CIFAR-10, maar het kan de prestatie behoorlijk belemmeren voor grote datasets en wordt algemeen beschouwd als een goede praktijk. Data loaders laten ons de data in batchveringen iteraten, en de data wordt geladen tijdens de iteratie en niet allemaal tegelijk in het begin naar uw RAM

AlexNet van Scratch

Beginnen we met het code:

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

Definiëren van het AlexNet Model

Laten we kijken hoe het bovenstaande code werkt:

  • Het eerste stap om Definiëren van elke neurale netwerk (of een CNN of niet) in PyTorch is om een klasse te definiëren die nn.Module erft want het bevat veel van de methodes die we zullen moeten gebruiken
  • Er zijn twee hoofdstappen erna. Eerst is het initialiseren van de laag die we ga gebruiken in onze CNN binnen __init__ en de andere is het definiëren van de sequentie in welke deze laag de afbeelding zal verwerken. Dit wordt binnen de forward functie gedefiniëerd.
  • Voor de architectuur zelf definiëren we eerst de convolutionele lagen met de functie nn.Conv2D met de juiste kernelgrootte en de input/output kanalen. We brengen ook max pooling toe met de functie nn.MaxPool2D. Het mooie aan PyTorch is dat we de convolutionele laag, activeringsfunctie en max pooling kunnen combineren in één enkele laag (ze zullen apart toegepast worden, maar het helpt met de organisatie) met de functie nn.Sequential
  • Dan definiëren we de volledig verbonden lagen met lineaire (nn.Linear) en dropout (nn.Dropout) functies samen met de ReLu activeringsfunctie (nn.ReLU) en combineren deze met de functie nn.Sequential
  • Uiteindelijk produceert onze laatste laag 10 neuronen, die de uiteindelijke voorspellingen zijn voor de 10 objectklassen

Instellingen van Hyperparameters

Voordat we trainen, moeten we enkele hyperparameters instellen, zoals de verliesfunctie en de te gebruiken optimizer samen met batchgrootte, leeringssnelheid en aantal epochs.

num_classes = 10
num_epochs = 20
batch_size = 64
learning_rate = 0.005

model = AlexNet(num_classes).to(device)

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

# Trainer ons model
total_step = len(train_loader)

We beginnen door eenvoudige hyperparameters te definiëren (epochs, batchgrootte en leeringssnelheid) en ons model in te initialiseren met het aantal klassen als argument, wat in dit geval 10 is, en het model over te brengen naar de juiste apparaat (CPU of GPU). Daarna definiëren we onze kostenfunctie als kruisfunctieverlies en ons optimeringsalgoritme als Adam. Er zijn veel keuzes voor deze, maar deze geven meestal goede resultaten met het model en de gegeven gegevens. tenslotte definiëren we total_step om beter de stappen bij het trainen te volgen

Training

We zijn nu klaar om ons model te trainen:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # Verplaats tensoren naar de ingestelde apparaat
        images = images.to(device)
        labels = labels.to(device)

        # Voer de voorwaartse pass af
        outputs = model(images)
        loss = criterion(outputs, labels)

        # Voer de achterwaartse pass af 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()))

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

Laat ons zien wat de code doet:

  • We beginnen door door het aantal epochs heen te itereren, en dan de batchs in ons trainingsdata
  • We converteren de afbeeldingen en labels naar de apparaat die we gebruiken, d.w.z. GPU of CPU
  • In de voorwaartse pass, voorspelden we gebruik makend van ons model en berekenen we verlies op basis van deze voorspellingen en onze ware labels
  • Vervolgens doen we de achterwaartse pass, waarin we effectief onze gewichten aanpassen om ons model te verbetern
  • We zetten vervolgens de gradiënten op nul voordat we elke update uitvoeren door middel van de functie optimizer.zero_grad()
  • Dan berekenen we de nieuwe gradiënten met behulp van de functie loss.backward()
  • En uiteindelijk updateren we de gewichten met de functie optimizer.step()
  • Ook aan het einde van elke epoch gebruiken we ons validatieset om de accuracy van het model te berekenen. In dit geval zijn we niet aan gradiënten nodig dus gebruiken we with torch.no_grad() voor snellere evaluatie

We kunnen de uitvoer zien als volgt:

Training Loss en Validation Accuracy

Zoals we kunnen zien, is de verliezen elke epoch minder en dat laat zien dat ons model echt leert. Merk op dat deze verliezen op de training集 zijn, en als de verliezen erg klein zijn, kan dat overfitting aangeven. Dat is waarom we ook een validatieset gebruiken. De accuracy lijkt te stijgen op het validatieset, wat aangeeft dat er geen kans is op overfitting. Laten we nu ons model testen om te zien hoe het presteert.

Testen

Nu zien we hoe ons model presteert op ongeziene gegevens:

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

Merk op dat het code exact hetzelfde is als voor onze validatie doeleinden.

Met behulp van het model, en door slechts 6 epochs te trainen, lijkt het ons ongeveer 78,8% accuracy op het validatieset te geven.

Testen Accuracy

Conclusie

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

  • We begonnen met het begrijpen van de architectuur en de verschillende soorten lagen in het AlexNet-model.
  • Vervolgens laden we de CIFAR-10 dataset en voerden we voorbereidingen uit met behulp van torchvision.
  • Dan bouwden we ons AlexNet-model vanaf de grond af met behulp van PyTorch.
  • Ten slotte trainen we ons model op de CIFAR-10 dataset en het model lijkt goed te presteren op de testdataset met minimaal trainen (6 epochs).

Toekomstige werk

Dit artikel biedt een solide introductie en praktische ervaring, maar je zult nog meer kennis opdoen door verder te bladeren en te ontdekken wat je nog meer kunt bereiken.

  • U kunt proberen met verschillende datasets. Een dergelijk dataset is CIFAR-100, die een uitbreiding is van de CIFAR-10 dataset met 100 klassen.
  • U kunt experimenteren met verschillende hyperparameters en zo de beste combinatie ervan voor het model vinden.
  • Ten slotte kunt u proberen lagen toe te voegen of weg te halen uit het dataset om hun impact op de capaciteit van het model te zien.

Source:
https://www.digitalocean.com/community/tutorials/alexnet-pytorch