Convolutional Neural Network - Musterlösung

  1import torch
  2import torch.nn as nn
  3from torchvision import transforms, datasets
  4from tqdm import tqdm
  5
  6LR = 0.01  # Lernrate
  7DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  8
  9# TODO: Data Augmentation Pipeline
 10# Hier definieren wir die Transformationspipeline für die Trainings- und Validierungsdaten.
 11# Die Trainingsdaten werden mit verschiedenen Transformationen augmentiert, um die Robustheit des Modells
 12# zu erhöhen. Folgend Sie den Anweisungen in der Aufgabenstellung, um die Pipeline zu vervollständigen.
 13training_transform = transforms.Compose(
 14    [
 15        transforms.ToTensor(),
 16        transforms.RandomHorizontalFlip(p=0.5),
 17        transforms.RandomRotation(degrees=15),
 18        transforms.RandomCrop(size=(32, 32), padding=4),
 19        transforms.Normalize((0.5,), (0.5,)),
 20    ]
 21)
 22
 23# TODO: Validation Pipeline
 24# Definieren Sie hier eine zweite Transformationspipeline für die Validierungsdaten.
 25# Folgend Sie den Anweisungen in der Aufgabenstellung, um die Pipeline zu vervollständigen.
 26validation_transform = transforms.Compose(
 27    [transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))]
 28)
 29
 30if __name__ == "__main__":
 31    # TODO: Laden der CIFAR-100-Daten
 32    training_data = datasets.CIFAR100(
 33        root="./data", train=True, download=True, transform=training_transform
 34    )
 35    validation_data = datasets.CIFAR100(
 36        root="./data", train=False, download=True, transform=validation_transform
 37    )
 38
 39    training_set = torch.utils.data.DataLoader(
 40        training_data, batch_size=256, shuffle=True
 41    )
 42    validation_set = torch.utils.data.DataLoader(
 43        validation_data, batch_size=256, shuffle=False
 44    )
 45
 46
 47class CNNNetwork(nn.Module):
 48    """Ein einfaches neuronales Netzwerk mit einer versteckten Schicht."""
 49
 50    def __init__(self):
 51        """Initialisiert das Netzwerk mit mehreren Convolutional-Schichten und voll verbundenen Schichten.
 52
 53        **TODO**:
 54
 55        - Rufen Sie die Methode `super().__init__()` auf, um die Basisklasse zu initialisieren.
 56
 57        - Definieren Sie die Faltungs-Schichten `conv1`, `conv2`, `conv3` mit den entsprechenden Eingangs- und Ausgangskanälen. Verwenden Sie `nn.Conv2d(...) <https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html>`_. Setzen Sie `kernel_size=3` und `padding="same"` für alle Schichten.
 58          Verwenden Sie jeweils 16, 32 und 64 Ausgänge für `conv1`, `conv2` und `conv3`.
 59
 60        - Definieren Sie die voll verbundenen Schichten `fc1` und `fc2` mit den entsprechenden Eingangs- und Ausgangsgrößen. Verwenden Sie `nn.Linear(...) <https://pytorch.org/docs/stable/generated/torch.nn.Linear.html>`_. Setzen Sie `fc1` auf 512 Ausgänge und `fc2` auf 100 Ausgänge.
 61
 62        - Fügen Sie eine `Flatten-Schicht <https://pytorch.org/docs/stable/generated/torch.nn.Flatten.html>`_ hinzu, um die Ausgabe der Convolutional-Schichten in einen Vektor umzuwandeln.
 63
 64        - Fügen Sie eine `Max-Pooling-Schicht <https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html>`_ `pool` mit `kernel_size=2` und `stride=2` hinzu, um die räumliche Dimension der Feature-Maps zu reduzieren.
 65
 66        - Verwenden Sie `torch.relu <https://pytorch.org/docs/stable/generated/torch.relu.html>`_ für die Aktivierung.
 67        """
 68        super().__init__()
 69        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding="same")
 70        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding="same")
 71        self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding="same")
 72
 73        self.bn1 = nn.BatchNorm2d(16)
 74        self.bn2 = nn.BatchNorm2d(32)
 75        self.bn3 = nn.BatchNorm2d(64)
 76
 77        self.fc1 = nn.Linear(64 * 4 * 4, 512)
 78        self.fc2 = nn.Linear(512, 100)
 79        self.flatten = nn.Flatten()
 80        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
 81        self.dropout = nn.Dropout(0.5)
 82
 83    def forward(self, x):
 84        """Führt den Vorwärtsdurchlauf des Netzwerks aus.
 85
 86        **TODO**:
 87
 88        - Wenden Sie abwechselnd immer die Faltungs-Schichten `conv1`, `conv2`, `conv3` auf die Eingabe `x` an, gefolgt von einer ReLU-Aktivierung und einem Pooling-Layer.
 89
 90        - Flatten Sie die Ausgabe der letzten Faltungs-Schicht mit .`self.flatten(x)`
 91
 92        - Wenden Sie die voll verbundenen Schichten `fc1` und `fc2` auf die flachgelegte Ausgabe an, wobei Sie ReLU-Aktivierung auf die Ausgabe von `fc1` anwenden.
 93
 94        - Geben Sie die Ausgabe der letzten Schicht `fc2` zurück.
 95        """
 96        x = self.pool(self.bn1(torch.relu(self.conv1(x))))
 97        x = self.pool(self.bn2(torch.relu(self.conv2(x))))
 98        x = self.pool(self.bn3(torch.relu(self.conv3(x))))
 99        x = self.flatten(x)
100        x = torch.relu(self.fc1(x))
101        x = self.dropout(x)
102        x = self.fc2(x)
103        return x
104
105
106def epoch(model, n, train, dataloader, criterion, optimizer):
107    """
108    Führt eine einzelne Trainings- oder Evaluations-Epoche für das Modell aus.
109
110    Parameters:
111    -----------
112
113    model (nn.Module):
114        Das zu trainierende oder evaluierende Modell.
115
116    n (int):
117        Die aktuelle Epoche.
118
119    train (bool):
120        Gibt an, ob die Epoche im Trainingsmodus oder im Evaluationsmodus durchgeführt wird.
121
122    dataloader (DataLoader):
123        Der DataLoader, der die Daten für die Epoche bereitstellt.
124
125    criterion (nn.Module):
126        Das Loss-Kriterium, das zur Berechnung des Verlusts verwendet wird.
127
128    optimizer (torch.optim.Optimizer):
129        Der Optimierer, der zur Aktualisierung der Modellparameter verwendet wird.
130
131    **TODO**:
132
133    - 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.
134
135    - Initialisieren Sie `total_loss`, `total_samples` und `total_correct` auf 0.0, 0 und 0.
136
137    - Verwenden Sie `tqdm` für den Fortschrittsbalken, um den Fortschritt der Epoche anzuzeigen.
138      Speichern Sie den Iterator in einer eigenen Variable damit er innerhalb der Schleife verwendet werden kann.
139
140    - Iterieren Sie über den `dataloader` und führen Sie die folgenden Schritte aus:
141
142    - Verschieben Sie die Daten und Labels auf das Gerät (`DEVICE`).
143
144    - Setzen Sie die Gradienten des Optimierers zurück, wenn `train=True` ist, indem Sie `optimizer.zero_grad()` aufrufen.
145
146    - 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.
147
148    - Berechnen Sie den Verlust mit `criterion(outputs, labels)`.
149
150    - Wenn `train=True` ist, führen Sie den Rückwärtsdurchlauf aus, indem Sie `loss.backward()` aufrufen und die Parameter mit `optimizer.step()` aktualisieren.
151
152    - Aktualisieren Sie `total_loss`, `total_samples` und `total_correct` mit den entsprechenden Werten aus dem aktuellen Batch.
153      Der `total_loss` sollte den Verlust des aktuellen Batches aufsummieren, `total_samples` die Anzahl der Samples im aktuellen Batch und
154      `total_correct` die Anzahl der korrekt klassifizierten Samples. Die Anzahl der korrekt klassifizierten Samples kann mit `(outputs.argmax(dim=1) == labels).sum()` berechnet werden.
155
156    - Aktualisieren Sie den Fortschrittsbalken mit dem aktuellen Verlust und der Genauigkeit. Zeigen Sie auch an ob das Netz im Trainings- oder Validationsmodus betrieben wird.
157      Rufen Sie dazu tqdm.set_description() auf und formatieren Sie die Ausgabe entsprechend.
158    """
159    # Vorbereiten des Modells für Training oder Evaluation
160    if train:
161        model.train()
162    else:
163        model.eval()
164
165    # Training des Modells
166    total_loss = 0.0
167    total_samples = 0
168    total_correct = 0
169    bar = tqdm(dataloader)
170    for data, labels in bar:
171        # Daten und Labels auf das Gerät verschieben
172        data, labels = data.to(DEVICE), labels.to(DEVICE)
173
174        # Gradienten zurücksetzen
175        if train:
176            optimizer.zero_grad()
177
178        # Vorwärtsdurchlauf
179        with torch.set_grad_enabled(train):
180            outputs = model(data)
181
182        # Verlust berechnen und Rückwärtsdurchlauf
183        loss = criterion(outputs, labels)
184
185        # Gradienten berechnen
186        if train:
187            loss.backward()
188            optimizer.step()
189
190        # Aktualisieren der Metriken
191        total_loss += loss.item()
192        total_samples += data.size(0)
193        total_correct += (outputs.argmax(dim=1) == labels).sum().item()
194
195        bar.set_description(
196            f"Epoch {n} ({'T' if train else 'V'}), Loss: {total_loss / total_samples:.4f}, Accuracy: {total_correct / total_samples:.2%}"
197        )
198
199
200if __name__ == "__main__":
201    # Initialisierung des Modells, Loss-Kriteriums und Optimierers
202    model = CNNNetwork().to(DEVICE)
203    criterion = nn.CrossEntropyLoss()
204    optimizer = torch.optim.Adam(model.parameters(), lr=LR)
205
206    # Das Modell trainieren
207    model.train()
208    for n in range(1, 30):
209        epoch(model, n, True, training_set, criterion, optimizer)
210        epoch(model, n, False, validation_set, criterion, optimizer)