PyTorch - Convolutional Neural Networks

Im Jahr 1998 veröffentlichte Yann LeCun das erste Convolutional Neural Network (CNN) mit dem Namen LeNet-5.

Er war der erste, der die Architektur eines CNNs definierte und es erfolgreich auf die Erkennung von handgeschriebenen Ziffern anwandte.

Yann LeCun (Quelle: `https://de.wikipedia.org/wiki/Yann_LeCun`_)

In dieser Aufgabe werden Sie ein Convolutional Neural Network (CNN) mit PyTorch erstellen, das auf dem CIFAR-100-Datensatz trainiert wird.

CIFAR-100 ist ein Datensatz, der 100 verschiedene Klassen von Bildern enthält, darunter Tiere, Fahrzeuge und alltägliche Objekte.

CIFAR-100

In dieser Aufgabe arbeiten Sie in der Datei pytorch/cifar100.py.

Aufgabe 1: Data Augmentation Pipeline

In dieser Aufgabe werden Sie eine Data Augmentation Pipeline für den CIFAR-100-Datensatz erstellen. Data Augmentation ist eine Technik, die verwendet wird, um die Vielfalt der Trainingsdaten zu erhöhen, indem verschiedene Transformationen auf die Bilder angewendet werden. Dies kann helfen, die Generalisierungsfähigkeit des Modells zu verbessern und Overfitting zu reduzieren. Sie können verschiedene Transformationen wie zufällige Drehungen, Skalierungen, Spiegelungen und Farbänderungen anwenden. PyTorch bietet eine einfache Möglichkeit, Data Augmentation mit der torchvision.transforms-Bibliothek zu implementieren.

Dabei verwendet man in der Regel die Klasse torchvision.transforms.Compose, um mehrere Transformationen zu kombinieren. Sie sollten eine Pipeline erstellen, die mindestens folgende Transformationen enthält:

  • Konvertierung in Tensor: Die torchvision.transforms.ToTensor()-Klasse konvertiert die Bilder in PyTorch-Tensoren, die für das Training verwendet werden können.

  • Zufällige horizontale Spiegelung: Die torchvision.transforms.RandomHorizontalFlip()-Klasse spiegelt die Bilder zufällig horizontal, was bei vielen Objekten sinnvoll ist. Verwenden Sie p=0.5.

  • Zufällige Drehung: Die Klasse torchvision.transforms.RandomRotation() dreht die Bilder um einen zufälligen Winkel, um die Robustheit des Modells gegenüber verschiedenen Orientierungen zu erhöhen. Verwenden Sie degrees=15, um die Bilder um bis zu 15 Grad (plus oder minus) zu drehen.

  • Zufälliger Zuschnitt: Die torchvision.transforms.RandomCrop()-Klasse schneidet die Bilder zufällig aus, um die Robustheit des Modells gegenüber verschiedenen Bildausschnitten zu erhöhen. Verwenden Sie size=(32, 32) und padding=4, um die Bilder auf die Größe 32x32 zu beschneiden und einen Rand von 4 Pixeln hinzuzufügen.

  • Normalisierung: Die torchvision.transforms.Normalize()-Klasse normalisiert die Bilder, um die Pixelwerte in einen bestimmten Bereich zu bringen. Verwenden Sie mean = (0.5, ) und std = (0.5, ), um die Bilder zu normalisieren.

Achtung: Erstellen Sie auch eine zweite Pipeline für die Validierung, die nur die Konvertierung in Tensor und die Normalisierung enthält, ohne Data Augmentation.

Lösung anzeigen

training_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.RandomCrop(size=(32, 32), padding=4),
    transforms.Normalize((0.5,), (0.5,))
])

validation_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

Aufgabe 2: Der Datensatz laden

Nun müssen Sie den CIFAR-100-Datensatz laden. PyTorch bietet eine einfache Möglichkeit, diesen Datensatz zu laden und in Trainings- und Validierungssets zu unterteilen. Sie können den Datensatz mit der Klasse torchvision.datasets.CIFAR100 laden.

Instantieren Sie zwei torchvision.datasets.CIFAR100-Objekte: eines für das Training und eines für die Validierung. Verwenden Sie die root-Option, um den Speicherort des Datensatzes anzugeben, und die download-Option, um den Datensatz herunterzuladen, falls er nicht vorhanden ist. Verwenden Sie die train-Option, um anzugeben, ob es sich um das Trainings- oder Validierungsset handelt.

Lösung anzeigen

training_data = datasets.CIFAR100(
    root="data/cifar100",
    train=True,
    download=True,
    transform=training_transform
)

validation_data = datasets.CIFAR100(
    root="data/cifar100",
    train=False,
    download=True,
    transform=validation_transform
)

Wrappen Sie die Datensätze in torch.utils.data.DataLoader-Objekte, um sie in Batches laden zu können. Ein Batch ist dabei eine Gruppe von Bildern, die gleichzeitig verarbeitet werden. Verwenden Sie die batch_size-Option, um die Größe der Batches festzulegen, und die shuffle-Option, um die Daten zufällig zu mischen. Wählen Sie eine Batch-Größe zwischen 32 und 256, abhängig von Ihrer Hardware und den verfügbaren Ressourcen.

Lösung anzeigen

training_set = torch.utils.data.DataLoader(training_data, batch_size=256, shuffle=True)
validation_set = torch.utils.data.DataLoader(validation_data, batch_size=256, shuffle=False)

Aufgabe 3: Das Netzwerk definieren

Implementieren Sie nun die Klasse CNNNetwork, die ein einfaches Convolutional Neural Network (CNN) mit mehreren Convolutional-Schichten und voll verbundenen Schichten definiert.

class pytorch.cifar100.CNNNetwork[Quellcode]

Ein einfaches neuronales Netzwerk mit einer versteckten Schicht.

__init__()[Quellcode]

Initialisiert das Netzwerk mit mehreren Convolutional-Schichten und voll verbundenen Schichten.

TODO:

  • Rufen Sie die Methode super().__init__() auf, um die Basisklasse zu initialisieren.

  • Definieren Sie die Faltungs-Schichten conv1, conv2, conv3 mit den entsprechenden Eingangs- und Ausgangskanälen. Verwenden Sie nn.Conv2d(…). Setzen Sie kernel_size=3 und padding=“same“ für alle Schichten. Verwenden Sie jeweils 16, 32 und 64 Ausgänge für conv1, conv2 und conv3.

  • Definieren Sie die voll verbundenen Schichten fc1 und fc2 mit den entsprechenden Eingangs- und Ausgangsgrößen. Verwenden Sie nn.Linear(…). Setzen Sie fc1 auf 512 Ausgänge und fc2 auf 100 Ausgänge.

  • Fügen Sie eine Flatten-Schicht hinzu, um die Ausgabe der Convolutional-Schichten in einen Vektor umzuwandeln.

  • Fügen Sie eine Max-Pooling-Schicht pool mit kernel_size=2 und stride=2 hinzu, um die räumliche Dimension der Feature-Maps zu reduzieren.

  • Verwenden Sie torch.relu für die Aktivierung.

forward(x)[Quellcode]

Führt den Vorwärtsdurchlauf des Netzwerks aus.

TODO:

  • Wenden Sie abwechselnd immer die Faltungs-Schichten conv1, conv2, conv3 auf die Eingabe x an, gefolgt von einer ReLU-Aktivierung und einem Pooling-Layer.

  • Flatten Sie die Ausgabe der letzten Faltungs-Schicht mit .`self.flatten(x)`

  • Wenden Sie die voll verbundenen Schichten fc1 und fc2 auf die flachgelegte Ausgabe an, wobei Sie ReLU-Aktivierung auf die Ausgabe von fc1 anwenden.

  • Geben Sie die Ausgabe der letzten Schicht fc2 zurück.

Implementierung des Konstruktors anzeigen

def __init__(self):
  super().__init__()
  self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding="same")
  self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding="same")
  self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding="same")

  self.fc1 = nn.Linear(64 * 4 * 4, 512)
  self.fc2 = nn.Linear(512, 100)
  self.flatten = nn.Flatten()
  self.pool = nn.MaxPool2d(kernel_size=2, stride=2)

Implementierung des Forward-Passes anzeigen

def forward(self, x):
  x = self.pool(torch.relu(self.conv1(x)))
  x = self.pool(torch.relu(self.conv2(x)))
  x = self.pool(torch.relu(self.conv3(x)))
  x = self.flatten(x)
  x = torch.relu(self.fc1(x))
  x = self.fc2(x)
  return x

Aufgabe 4: Das Training des Netzwerks

Dieses mal möchten wir das Training etwas ausführlicher gestalten. Insbesondere möchten wir den Fortschritt des Trainings in Form von Metriken wie der Genauigkeit und dem Verlust verfolgen. Ausserdem möchten wir sowohl den Trainings- als auch den Validierungsprozess in separaten Funktionen implementieren, um den Code übersichtlicher zu gestalten.

Da einige Module sich während des Trainings anders verhalten als während der Validierung, wie z.B. die Dropout-Schichten, müssen wir das Netzwerk in den richtigen Modus versetzen. Rufen Sie dazu model.train() bzw. model.eval() auf, um das Netzwerk in den Trainings- bzw. Validierungsmodus zu versetzen.

Während des Trainings müssen die Gradienten berechnet und die Gewichte aktualisiert werden. Dazu müssen wie vorher mit optimizer.zero_grad() die Gradienten auf Null gesetzt werden, bevor der Vorwärtsdurchlauf durchgeführt wird. Mit loss.backward() und optimizer.step() werden die Gradienten berechnet, und mit optimizer.step() werden die Gewichte aktualisiert.

Während der Validierung müssen die Gradienten nicht berechnet werden, da wir nur die Vorhersagen des Modells benötigen. Sie können dies erreichen, indem Sie den Kontextmanager torch.set_grad_enabled(train) verwenden, der verhindert, dass PyTorch die Gradienten berechnet und speichert.

Um den durchschnittlichen Verlust und die Genauigkeit während des Trainings und der Validierung zu berechnen, müssen Sie während der Epoche Statistiken sammeln. Summieren sie den Verlust und die Anzahl der korrekten Vorhersagen für jedes Batch auf. Zählen Sie zusätzlich die Anzahl der Bilder in jedem Batch, um die Gesamtzahl der Bilder zu erhalten.

Nachdem das Netz eine Vorhesage für einen Batch gemacht hat stehen in

outputs = model(inputs)

die s.g. Logits. Mit torch.argmax(outputs, dim=1) können Sie die Klasse mit der höchsten Wahrscheinlichkeit auswählen. Der dim=1-Parameter gibt an, dass die Argmax-Operation entlang der zweiten Dimension (der Klassen) durchgeführt wird. Sie erhalten damit zu jedem Bild die Klasse mit der höchsten Wahrscheinlichkeit. Über einen Vergleich mit den wahren Labels können Sie die Anzahl der korrekten Vorhersagen zählen.

Implementieren Sie die Funktion pytorch.cifar100.epoch(), die das Netzwerk auf dem Trainingsset trainiert und den Fortschritt in Form von Metriken wie der Genauigkeit und dem Verlust verfolgt.

pytorch.cifar100.epoch(model, n, train, dataloader, criterion, optimizer)[Quellcode]

Führt eine einzelne Trainings- oder Evaluations-Epoche für das Modell aus.

Parameters:

model (nn.Module):

Das zu trainierende oder evaluierende Modell.

n (int):

Die aktuelle Epoche.

train (bool):

Gibt an, ob die Epoche im Trainingsmodus oder im Evaluationsmodus durchgeführt wird.

dataloader (DataLoader):

Der DataLoader, der die Daten für die Epoche bereitstellt.

criterion (nn.Module):

Das Loss-Kriterium, das zur Berechnung des Verlusts verwendet wird.

optimizer (torch.optim.Optimizer):

Der Optimierer, der zur Aktualisierung der Modellparameter verwendet wird.

TODO:

  • Setzen Sie das Modell in den Trainingsmodus, wenn train=True ist, und in den Evaluationsmodus, wenn train=False ist. Rufen Sie dazu model.train() bzw. model.eval() auf.

  • Initialisieren Sie total_loss, total_samples und total_correct auf 0.0, 0 und 0.

  • Verwenden Sie tqdm für den Fortschrittsbalken, um den Fortschritt der Epoche anzuzeigen. Speichern Sie den Iterator in einer eigenen Variable damit er innerhalb der Schleife verwendet werden kann.

  • Iterieren Sie über den dataloader und führen Sie die folgenden Schritte aus:

  • Verschieben Sie die Daten und Labels auf das Gerät (DEVICE).

  • Setzen Sie die Gradienten des Optimierers zurück, wenn train=True ist, indem Sie optimizer.zero_grad() aufrufen.

  • Führen Sie den Vorwärtsdurchlauf des Modells aus, indem Sie model(data) aufrufen. Verwenden Sie torch.set_grad_enabled(train), um den Gradientenfluss nur im Trainingsmodus zu aktivieren.

  • Berechnen Sie den Verlust mit criterion(outputs, labels).

  • Wenn train=True ist, führen Sie den Rückwärtsdurchlauf aus, indem Sie loss.backward() aufrufen und die Parameter mit optimizer.step() aktualisieren.

  • Aktualisieren Sie total_loss, total_samples und total_correct mit den entsprechenden Werten aus dem aktuellen Batch. Der total_loss sollte den Verlust des aktuellen Batches aufsummieren, total_samples die Anzahl der Samples im aktuellen Batch und total_correct die Anzahl der korrekt klassifizierten Samples. Die Anzahl der korrekt klassifizierten Samples kann mit (outputs.argmax(dim=1) == labels).sum() berechnet werden.

  • Aktualisieren Sie den Fortschrittsbalken mit dem aktuellen Verlust und der Genauigkeit. Zeigen Sie auch an ob das Netz im Trainings- oder Validationsmodus betrieben wird. Rufen Sie dazu tqdm.set_description() auf und formatieren Sie die Ausgabe entsprechend.

Implementierung der Funktion anzeigen

# Vorbereiten des Modells für Training oder Evaluation
if train:
    model.train()
else:
    model.eval()

# Training des Modells
total_loss = 0.0
total_samples = 0
total_correct = 0
bar = tqdm(dataloader)
for data, labels in bar:
    # Daten und Labels auf das Gerät verschieben
    data, labels = data.to(DEVICE), labels.to(DEVICE)

    # Gradienten zurücksetzen
    if train:
        optimizer.zero_grad()

    # Vorwärtsdurchlauf
    with torch.set_grad_enabled(train):
        outputs = model(data)

    # Verlust berechnen und Rückwärtsdurchlauf
    loss = criterion(outputs, labels)

    # Gradienten berechnen
    if train:
        loss.backward()
        optimizer.step()

    # Aktualisieren der Metriken
    total_loss += loss.item()
    total_samples += data.size(0)
    total_correct += (outputs.argmax(dim=1) == labels).sum().item()

    bar.set_description(f"Epoch {n} ({'T' if train else 'V'}), Loss: {total_loss / total_samples:.4f}, Accuracy: {total_correct / total_samples:.2%}")

Sobald Sie die Funktion pytorch.cifar100.epoch() implementiert haben, können Sie das Netzwerk auf dem Trainingsset trainieren und auf dem Validierungsset evaluieren. Starten Sie dazu das Skript

python pytorch/cifar100.py
Downloading https://www.cs.toronto.edu/~kriz/cifar-100-python.tar.gz to ./data\cifar-100-python.tar.gz
100%|█████████████████████████████████████████████████████████████████████| 169001437/169001437 [00:22<00:00, 7650435.65it/s]
Extracting ./data\cifar-100-python.tar.gz to ./data
Files already downloaded and verified
Epoch 1 (T), Loss: 0.0192, Accuracy: 1.87%:  12%|█████                                      | 23/196 [00:18<02:20,  1.23it/s]

Aufgabe 5: Batch-Normalisierung hinzufügen

Fügen Sie Batch-Normalisierungsschichten nach jeder Convolutional-Schicht hinzu, um die Trainingsgeschwindigkeit zu erhöhen und die Leistung des Modells zu verbessern. Batch-Normalisierung normalisiert die Ausgaben der Convolutional-Schichten, um die Verteilung der Aktivierungen zu stabilisieren und die Trainingsgeschwindigkeit zu erhöhen. Verwenden Sie Batch-Normalisierung dazu nach der Aktivierungsfunktion (z.B. ReLU) jeder Convolutional-Schicht.+

Sie können die Klasse torch.nn.BatchNorm2d verwenden, um Batch-Normalisierung für 2D-Bilder zu implementieren. Diese muss wissen wieviele Kanäle die Convolutional-Schicht hat, daher müssen Sie die Anzahl der Kanäle als Argument übergeben.

Implementierung der Batch-Normalisierung anzeigen

def __init__(self):
  super().__init__()
  self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding="same")
  self.bn1 = nn.BatchNorm2d(16)
  self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding="same")
  self.bn2 = nn.BatchNorm2d(32)
  self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding="same")
  self.bn3 = nn.BatchNorm2d(64)
  ...

def forward(self, x):
    x = self.pool(self.bn1(torch.relu(self.conv1(x))))
    x = self.pool(self.bn2(torch.relu(self.conv2(x))))
    x = self.pool(self.bn3(torch.relu(self.conv3(x))))
    x = self.flatten(x)
    x = torch.relu(self.fc1(x))
    x = self.fc2(x)
    return x

Beobachten sie den Effekt der Batch-Normalisierung auf die Trainingsgeschwindigkeit und die Leistung des Modells.

Aufgabe 6: Dropout hinzufügen

Fügen Sie eine Dropout-Schicht hinzu, um Overfitting zu reduzieren. Dropout ist eine Technik, bei der während des Trainings zufällig einige Neuronen deaktiviert. Dies hilft, Overfitting zu reduzieren, indem es das Modell zwingt, robuster zu werden und nicht von einzelnen Neuronen abhängig zu sein. Sie können die Klasse torch.nn.Dropout verwenden, um Dropout zu implementieren. Fügen Sie Dropout-Schichten nach den voll verbundenen Schichten hinzu, um Overfitting zu reduzieren.

Implementierung des Dropouts anzeigen

def __init__(self):
    super().__init__()
    ...
    self.dropout = nn.Dropout(p=0.5)

def forward(self, x):
    x = self.pool(self.bn1(torch.relu(self.conv1(x))))
    x = self.pool(self.bn2(torch.relu(self.conv2(x))))
    x = self.pool(self.bn3(torch.relu(self.conv3(x))))
    x = self.flatten(x)
    x = torch.relu(self.fc1(x))
    x = self.dropout(x)  # Dropout nach der voll verbundenen Schicht
    x = self.fc2(x)
    return x

Musterlösung

Convolutional Neural Network - Musterlösung