VGG von Grund auf in PyTorch schreiben

Fortsetzung meiner Reihe über die Erstellung klassischer Konvolutionalen Neuronalen Netze, die den Bereich der Bildverarbeitung in den letzten 1-2 Jahrzehnten revolutioniert haben, werden wir nun das sehr tiefe Konvolutionale Neuronale Netz VGG aus dem Nichts mit PyTorch aufbauen. Du kannst die vorherigen Artikel in der Reihe auf meinem Profil sehen, hauptsächlich LeNet5 und AlexNet.

Wie zuvor werden wir die Architektur und die Intuition hinter VGG untersuchen und wie die Ergebnisse zu der Zeit waren. Danach werden wir unser Datensatz CIFAR100 untersuchen und mit memory-effizientem Code in unser Programm laden. Anschließend werden wir VGG16 (die Zahl bezieht sich auf die Anzahl der Schichten, es gibt grundsätzlich zwei Versionen: VGG16 und VGG19) von Grund auf mit PyTorch implementieren und es mit unserem Datensatz trainieren, sowie es auf unserem Testdatensatz evaluieren, um zu sehen, wie es auf unbekannten Daten performt


VGG

Aufbauend auf die Arbeit von AlexNet konzentriert sich VGG auf einen weiteren wichtigen Aspekt von Convolutional Neural Networks (CNNs), nämlich Tiefe. Es wurde von Simonyan und Zisserman entwickelt. Es besteht normalerweise aus 16 Konvolutionsschichten, kann aber auch auf 19 Schichten erweitert werden (daher die beiden Versionen, VGG-16 und VGG-19). Alle Konvolutionsschichten bestehen aus 3×3-Filtern. Mehr über die Netzwerkarchitektur kannst du in der offiziellen Arbeit hier

VGG16-Architektur lesen. Quelle


Daten Laden

Datensatz

vor dem Modellbau ist es eine der wichtigsten Dinge in jedem Machine Learning Projekt, Daten zu laden, zu analysieren und zu vorverarbeiten. In diesem Artikel verwenden wir das CIFAR-100 Datenset. Dieses Datenset ähnelt dem CIFAR-10, unterscheidet sich jedoch durch 100 Klassen, die jeweils 600 Bilder enthalten. Es gibt 500 Trainingsbilder und 100 Testbilder pro Klasse. Die 100 Klassen des CIFAR-100 sind in 20 Superklassen gruppiert. Jeder Bildschirm ist mit einer „fine“ Label (der Klasse, der er angehört) und einem „coarse“ Label (der Superklasse, der er angehört) versehen. Wir verwenden hier das „fine“ Label. Hier ist die Liste der Klassen im CIFAR-100:


Klassenliste für das CIFAR-100 Datenset

Bibliotheken importieren

Wir arbeiten hauptsächlich mit torch (für das Erstellen und die Trainings des Modells), torchvision (für die Datenladung/Verarbeitung, enthält Datensets und Methoden zur Verarbeitung dieser Datensets in der Computervision) und numpy (für mathematische Manipulation). Wir definieren auch eine Variable device, sodass das Programm die GPU verwenden kann, wenn verfügbar

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

Daten laden

torchvision ist eine Bibliothek, die einfache Zugriff auf viele Computerbildverarbeitungsdatenbanken bietet und Methoden zur Vorverarbeitung dieser Datenbanken in einer einfach und intuitiv handhabbaren Weise bereitstellt

  • Wir definieren eine Funktion data_loader, die entweder Trainings-/Validierungsdaten oder Testdaten abhängig von den Argumenten liefert
  • Wir beginnen mit der Definition der Variable normalize, die die Mittelwerte und Standardabweichungen jedes Kanals (rot, grün und blau) im Datensatz enthält. Diese können manuell berechnet werden, sind aber auch online verfügbar. Dies wird in der Variable transform verwendet, wo wir die Daten skalieren, sie in Tensoren umwandeln und anschließend normalisieren
  • Wenn das Argument test wahr ist, laden wir einfach den Testteil des Datensatzes und liefern ihn mit Hilfe von Datenladern (unten erklärt) aus
  • Wenn test nicht wahr ist (Standardverhalten), laden wir den Trainingsteil des Datensatzes und teilen ihn zufällig in einen Trainings- und Validierungsteil (0,9:0,1) auf
  • Schließlich verwenden wir Datenladen. Dies kann die Leistung bei kleinen Datensätzen wie CIFAR100 nicht beeinträchtigen, aber es kann die Leistung bei großen Datensätzen erheblich beeinträchtigen und ist generell eine gute Praxis. Datenladen ermöglichen es uns, die Daten in Batches durchzugehen, und die Daten werden während der Iteration geladen und nicht alle gleichzeitig in den Arbeitsspeicher 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],
    )

    # Definition von Umwandlungen
    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

    # Laden des Datensatzes
    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 Datensatz
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 von Grund auf

Um das Modell von Grund auf zu erstellen, müssen wir zunächst verstehen, wie Modelldefinitionen in torch funktionieren und die verschiedenen Arten von Schichten, die wir hier verwenden werden:

  • Jeder benutzerdefinierte Modell muss von der Klasse nn.Module erben, da sie einige grundlegende Funktionen bereitstellt, die dem Modell helfen, zu trainieren.
  • Zweitens gibt es zwei Hauptaufgaben, die wir erfüllen müssen. Erstens Definieren wir die verschiedenen Schichten unseres Modells innerhalb der Funktion __init__ und die Reihenfolge, in der diese Schichten auf das Eingabedaten aufgerufen werden, innerhalb der Funktion forward

Lassen Sie uns nun die verschiedenen Arten von Schichten definieren, die wir hier verwenden:

  • nn.Conv2d: Diese sind die Konvolutionsschichten, die die Anzahl der Eingangs- und Ausgangskanäle als Argumente akzeptieren, zusammen mit der Kernelgröße für das Filter. Es können auch Strides oder Padding angewendet werden, wenn Sie das verlangen
  • nn.BatchNorm2d: Dies führt Batch-Normalisierung auf dem Ausgang der Konvolutionsschicht an
  • nn.ReLU: Dies ist die Aktivierung, die auf verschiedenen Netzwerkausgaben angewendet wird.
  • nn.MaxPool2d : Dies führt max pooling auf der Ausgabe mit der angegebenen Kernelgröße aus
  • nn.Dropout: Dies wird verwendet, um dropout mit einer gegebenen Wahrscheinlichkeit auf der Ausgabe anzuwenden
  • nn.Linear: Dies ist grundsätzlich eine vollständig verknpfte Schicht
  • nn.Sequential: Technisch gesehen ist dies kein Typ von Schicht, aber es hilft dabei, unterschiedliche Operationen, die zum gleichen Schritt gehören, zu kombinieren

Mit diesen Kenntnissen können wir nun unseren VGG16-Modell auf der Architektur aus dem Paper aufbauen:

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


Hyperparameter

Ein wichtiger Teil jedes maschinellen oder deep learning Projekts besteht darin, die Hyperparameter zu optimieren. Hier werden wir nicht verschiedene Werte für diese experimentieren, sondern wir werden sie vorher definieren müssen. Dies包括但不限于 die Definition der Anzahl der Epochen, der Batchgröße, der Lernrate, der Verlustfunktion sowie des Optimizers

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

model = VGG16(num_classes).to(device)


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


# Modell trainieren
total_step = len(train_loader)

Hyperparameter einstellen


Training

Wir sind nun bereit, unser Modell zu trainieren. Wir werden zunächst anschauen, wie wir unser Modell in torch trainieren und dann auf den Code eingehen:

  • Für jede Epoche gehen wir durch die Bilder und Label innerhalb unseres train_loader und verschieben diese Bilder und Label an den GPU, wenn verfügbar. Dies geschieht automatisch
  • Wir verwenden unser Modell, um auf den Label zu predigen (model(images)) und berechnen dann den Verlust zwischen den Vorhersagen und den wahren Labeln mit unserer Verlustfunktion (criterion(outputs, labels))
  • Dann verwenden wir diesen Verlust, um backpropagation durchzuführen (loss.backward) und die Gewichte zu aktualisieren (optimizer.step()). Denken Sie jedoch daran, die Gradienten vor jedem Update auf Null zu setzen. Dies wird mit optimizer.zero_grad() erledigt
  • Außerdem berechnen wir am Ende jeder Epoche auch die Genauigkeit des Modells auf unserem Validierungsdatensatz. In diesem Fall brauchen wir keine Gradienten, also verwenden wir with torch.no_grad() für eine schnellere Evaluierung

Nun kombinieren wir all das in folgendem Code:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # Bilder und Label auf die konfigurierte Hardware verschieben
        images = images.to(device)
        labels = labels.to(device)
        
        # Vorwärtspass
        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()))
            
    # Validierung
    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)) 

Training

Wir können die Ausgabe des obigen Codes wie folgt sehen, die zeigt, dass das Modell tatsächlich lernt, da der Verlust mit jeder Epoche abnimmt:


Training Verluste


Testing

Für die Tests verwenden wir exakt den gleichen Code wie beim Validieren, allerdings mit dem 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))   

Testing

Durch die Verwendung des oben stehenden Codes und die Trainings des Modells für 20 Epochen konnten wir eine Genauigkeit von 75% auf dem Testset erreichen.


Schlussfolgerung

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

  • Wir haben zunächst die Architektur und verschiedene Arten von Schichten im VGG-16-Modell verstanden
  • Danach haben wir das CIFAR100-Datensatz mit torchvision geladen und vorverarbeitet
  • Dann haben wir mit PyTorch unser VGG-16-Modell von Grund auf aufgebaut und verschiedene Arten von Schichten in torch verstanden
  • Schließlich haben wir unser Modell auf dem CIFAR100-Datensatz trainiert und getestet, und das Modell hat auf dem Testdatensatz mit 75% Genauigkeit gut abgeschnitten

Weitere Arbeit

Mit diesem Artikel erhalten Sie eine gute Einführung und praktische Lernform, aber Sie werden noch mehr lernen, wenn Sie dies erweitern und sehen, was Sie sonst tun können:

  • Sie können verschiedene Datensets verwenden. Ein solches Datenset ist CIFAR10 oder ein Subset der ImageNet-Datenbank.
  • Sie können mit verschiedenen Hyperparameter-Kombinationen experimentieren und die beste finden, die für das Modell geeignet ist.
  • Schließlich können Sie versuchen, Schichten hinzuzufügen oder zu entfernen, um deren Auswirkung auf die Fähigkeiten des Modells zu testen. Noch besser wäre es, die VGG-19-Version dieses Modells zu entwickeln.

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