AlexNet von Scratch in PyTorch schreiben

Einführung

Dieser Beitrag setzt eine Reihe fort, in der wir die beliebtesten Konvolutionsneuronennetze von Grund auf in PyTorch aufbauen. Du kannst den vorherigen Beitrag hier lesen, in dem wir LeNet5 aufgebaut haben. In diesem Beitrag werden wir AlexNet bauen, eines der bedeutendsten Durchbrüche in der Computervision ist.

Wir werden beginnen, indem wir die Architektur von AlexNet untersuchen und verstehen. Dann werden wir direkt in den Code gehen, indem wir unsere Datenbank, CIFAR-10, laden, bevor wir die Daten vorverarbeiten. Anschließend werden wir AlexNet von Grund auf mit PyTorch bauen und es auf unserer vorverarbeiteten Daten trainieren. Schließlich werden wir das trainierte Modell auf unbekannten (test) Daten zur Bewertung ziehen.

Voraussetzungen

Knowledge of neural networks will be helpful in understanding this article. This would encompass being familiar with the different layers of neural networks (input layer, hidden layers, output layer), activation functions, optimization algorithms (variants of gradient descent), loss functions, etc. Additionally, familiarity with Python syntax and the PyTorch library is essential for understanding the code snippets presented in this article.

Ein Verständnis von CNNs ist unerlässlich. Dies beinhaltet das Wissen über konvolutionale Schichten, Pooling-Schichten und ihre Rolle beim Extrahieren von Merkmalen aus Eingabedaten. Der Verständnis von Konzepten wie Stride, Padding und der Auswirkung der Kernel/Filtergröße ist ebenfalls hilfreich.

AlexNet

AlexNet ist eine tiefe konvolutionelle neuronale Netz, die ursprünglich 2012 von Alex Krizhevsky und seinen Kollegen entwickelt wurde. Es wurde entwickelt, um Bilder für den ImageNet LSVRC-2010 Wettbewerb zu klassifizieren, wo es die neueste Methoden erzielte. Du kannst detailliert Informationen über das Modell im Originalforschungsbericht hier lesen.

Lass uns die wichtigsten Schlussfolgerungen aus dem AlexNet-Papier anschauen. Erstens arbeitete AlexNet mit 3-Kanalbildern, die (224x224x3) groß waren. Es verwendete Max-Pooling zusammen mit ReLU-Aktivierungen beim Subsampling. Die Kernel für Konvolutionen waren entweder 11×11, 5×5 oder 3x3groß, während die Kernel für Max-Pooling 3x3groß waren. Es klassifizierte Bilder in 1000 Klassen. Es verwendete auch mehrere GPUs.

Datensatz

Wir beginnen mit der Laden und Vorverarbeitung der Daten. Für unsere Zwecke verwenden wir die CIFAR-10-Datenmenge. Die Datenmenge besteht aus 60000 32×32 Farbabbildern in 10 Klassen, je 6000 Abbilder pro Klasse. Es gibt 50000 Trainingsabbilder und 10000 Testabbilder.

Hier sind die Klassen in der Datenmenge sowie 10 zufällige Beispielabbilder von jedem:

Quelle: source

Die Klassen sind vollständig gegenseitig ausschließlich. Es gibt keine Überschneidung zwischen Automobilen und Lastwagen. „Automobil“ umfasst Limousinen, SUVs und solche Dinge. „Lastwagen“ umfasst nur große Lastwagen. Keiner umfasst den Pickup.

Bibliotheken importieren

Lassen Sie uns beginnen, indem wir die notwendigen Bibliotheken importieren und eine Variable device definieren, sodass das Notizbuch weiß, einen GPU zum Trainieren des Modells zu verwenden, wenn er verfügbar ist.

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

# Geräte-Konfiguration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Datenmenge laden

Mit torchvision, einer Bibliothek zur Unterstützung von Computervision-Aufgaben, laden wir unser Datenset ein. Diese Methode beinhaltet einige Hilfsfunktionen, die die Vorverarbeitung recht einfach und direkt erleichtern. Definierten wir die Funktionen get_train_valid_loader und get_test_loader, und rufen sie dann auf, um unsere CIFAR-10-Daten zu laden und zu verarbeiten:

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

    # definiere Transformationen
    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,
        ])

    # lade das Datenset
    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],
    )

    # definiere Transformation
    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-Datensatz 
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)

Lass uns den Code aufteilen:

  • Wir definieren zwei Funktionen get_train_valid_loader und get_test_loader, um die Trainings-/Validierungs- und Test-Sets zu laden
  • Wir beginnen mit der Definition der Variable normalize mit den Mittelwerten und Standardabweichungen für jede der Kanäle (rot, grün und blau) im Datensatz. Diese können manuell berechnet werden, sind aber wegen der Beliebtheit von CIFAR-10 auch online verfügbar
  • Für unsere Trainingsdatenbank haben wir die Option hinzugefügt, das Datenset auch zu vergrößern, indem wir es verändern, um eine robustere Trainings- und Erhöhung der Anzahl der Bilder zu erreichen. Anmerkung: Die Vergrößerung wird nur auf das Trainings-Subset angewendet und nicht auf das Validierungs- und Test-Subset, da sie nur für Evaluierungszwecke verwendet werden
  • Wir teilen das Trainingsdatensatz in ein Trainings- und ein Validierungsdatensatz (90:10-Verhältnis) auf und nehmen ihn random aus dem gesamten Trainingsset
  • Wir legen die Batchgröße fest und mischen das Datenset beim Laden, sodass jeder Batch eine Variation in den Typen der Labels aufweist. Dies wird die Effektivität unseres erhaltenen Modells erhöhen.
  • Schließlich nutzen wir Datenloader. Dies kann die Leistung bei kleinen Datensätzen wie CIFAR-10 nicht beeinträchtigen, aber bei großen Datensätzen kann dies die Leistung wesentlich behindern und es wird allgemein als gute Praxis angesehen. Datenloader ermöglichen es uns, durch die Daten in Batches zu iterieren, und die Daten werden während der Iteration geladen und nicht alle gleichzeitig beim Start in Ihre RAM

AlexNet von Scratch

Beginnen wir mit dem 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

Definieren des AlexNet-Modells

Lassen Sie uns anschauen, wie der obige Code funktioniert:

  • Der erste Schritt zur Definition jeder neuronalen Netzwerks (egal ob es ein CNN ist oder nicht) in PyTorch besteht darin, eine Klasse zu definieren, die von nn.Module erbt, da es viele der Methoden enthält, die wir verwenden werden müssen
  • Es gibt zwei HauptSchritte danach. Der erste ist das Initialisieren der Schichten, die wir in unserem CNN verwenden werden, innerhalb des __init__ -Methoden und der andere ist die Definition der Sequenz, in der diese Schichten das Bild verarbeiten werden. Dies wird innerhalb der forward -Funktion definiert.
  • Für die Architektur selbst definieren wir zunächst die Konvolutionsschichten mithilfe der Funktion nn.Conv2D mit der passenden Kernelgröße und den Eingangs-/Ausgangskanälen. Wir setzen außerdem Max-Pooling mithilfe der Funktion nn.MaxPool2D ein. Der Vorteil von PyTorch besteht darin, dass wir die Konvolutionsschicht, die Aktivierungsfunktion und das Max-Pooling in einer einzigen Schicht (sie werden separat angewendet, aber es hilft bei der Organisation) kombinieren können, indem wir die nn.Sequential Funktion verwenden
  • . Dann definieren wir die vollständig verknüpften Schichten mit linearer (nn.Linear), Dropout (nn.Dropout) und ReLU-Aktivierungsfunktion (nn.ReLU) und kombinieren diese mit der nn.Sequential Funktion
  • . Schließlich erzeugt unsere letzte Schicht 10 Neuronen, die unsere letzten Voraussagen für die 10 Klassen von Objekten sind

Hyperparameter Einstellungen

. Vor dem Training müssen wir einige Hyperparameter einstellen, wie z.B. die zu verwendende Verlustfunktion und der Optimizer zusammen mit Batchgröße, Lernrate und Anzahl der epochs.

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

model = AlexNet(num_classes).to(device)

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

# Trainiere das Modell
total_step = len(train_loader)

Wir beginnen mit der Definition einfacher Hyperparameter (Epochen, Batch-Größe und Lernrate) und initialisieren unser Modell mit der Anzahl der Klassen als Argument, die in diesem Fall 10 ist, und wechseln das Modell auf die passende Hardware (CPU oder GPU). Dann definieren wir unsere Kostenfunktion als Verteilungskostenverlust und den Optimizer als Adam. Es gibt viele Möglichkeiten für diese, aber diese tendieren dazu, gute Ergebnisse mit dem Modell und den gegebenen Daten zu liefern. Schließlich definieren wir total_step, um bessere Schritte bei der Trainingszeit zu verfolgen

Training

Wir sind bereit, unser Modell zu trainieren:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # Tensoren auf die konfigurierte Hardware verschieben
        images = images.to(device)
        labels = labels.to(device)

        # Forward-Durchlauf
        outputs = model(images)
        loss = criterion(outputs, labels)

        # Rückwärtspass und Optimierung
        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)) 

Lassen Sie uns sehen, was der Code tut:

  • Wir beginnen, indem wir durch die Anzahl der Epochen iterieren und dann die Batchs in unseren Trainingsdaten
  • Wir wandeln die Bilder und die Labels je nach der Hardware, die wir verwenden, also GPU oder CPU
  • Im Forward-Durchlauf treffen wir Vorhersagen mit unserem Modell und berechnen den Verlust auf der Basis dieser Vorhersagen und unserer tatsächlichen Labeln
  • Nächstes führen wir den Rückwärtspass durch, in dem wir tatsächlich die Gewichte unseres Modells verbessern
  • Wir setzen dann die Gradienten auf Null vor jedem Update mit der Funktion optimizer.zero_grad()
  • Dann berechnen wir die neuen Gradienten mit der Funktion loss.backward()
  • Und schließlich aktualisieren wir die Gewichte mit der Funktion optimizer.step()
  • Außerdem berechnen wir am Ende jedes Epochens mit Hilfe des Validierungsdatensatzes die Genauigkeit des Modells. In diesem Fall brauchen wir keine Gradienten, daher verwenden wir with torch.no_grad() für eine schnellere Evaluierung

Wir können die Ausgabe wie folgt sehen:

Training Verlust und Validierungs Genauigkeit

Wie wir sehen können, nimmt der Verlust mit jeder Epoche ab, was anzeigt, dass unser Modell tatsächlich lernt. Zu beachten ist, dass dieser Verlust sich auf den Trainingsdatensatz bezieht und falls der Verlust zu klein ist, kann das Überfitten anzeigen. Deshalb verwenden wir auch den Validierungsdatensatz. Die Genauigkeit scheint im Validierungsdatensatz zu steigen, was anzeigt, dass ein starkes Überfitten unwahrscheinlich ist. Jetzt testen wir also unser Modell, um zu sehen, wie es performt.

Testen

Jetzt gucken wir, wie unser Modell auf unbekannten Daten performt:

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

Beachten Sie, dass der Code exakt das gleiche für unsere Validierungszwecke ist.

Mit dem Modell und nur sechs Epochen Training erreichen wir etwa 78,8% Genauigkeit auf dem Validierungsdatensatz.

Testgenauigkeit

Fazit

Lassen Sie uns nun abschließend zusammenfassen, was wir in diesem Artikel getan haben:

  • Wir begannen damit, die Architektur und verschiedene Schichten des AlexNet-Modells zu verstehen
  • Als Nächstes luden und verarbeiteten wir das CIFAR-10-Dataset mit torchvision
  • Dann verwendeten wir PyTorch, um unser AlexNet-Modell von Grund auf zu erstellen
  • Schließlich trainierten und testeten wir unser Modell mit dem CIFAR-10-Dataset, und das Modell schien gut auf dem Test-Dataset abzuschneiden, selbst mit minimalem Training (6 Epochen)

Zukünftige Arbeiten

Dieser Artikel bietet eine solide Einführung und praktische Erfahrung, aber Sie werden noch mehr Wissen gewinnen, wenn Sie weiter erkunden und herausfinden, was Sie sonst noch erreichen können

  • Sie können versuchen, verschiedene Datensätze zu verwenden. Einer dieser Datensätze ist CIFAR-100, eine Erweiterung des CIFAR-10-Datensatzes mit 100 Klassen
  • Sie können mit verschiedenen Hyperparametern experimentieren und die beste Kombination für das Modell herausfinden
  • Schließlich können Sie versuchen, Schichten zum Dataset hinzuzufügen oder zu entfernen, um deren Einfluss auf die Leistungsfähigkeit des Modells zu sehen

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