diff --git a/5_dpnn/alexnet.png b/5_dpnn/alexnet.png new file mode 100644 index 0000000000000000000000000000000000000000..c051d58d23958ed228bdbba68d21c5c4f5b93aa6 Binary files /dev/null and b/5_dpnn/alexnet.png differ diff --git a/5_dpnn/alexnet_torch.png b/5_dpnn/alexnet_torch.png new file mode 100644 index 0000000000000000000000000000000000000000..f8f409ec7e18e6338c8506bdee21d198a05a9dbc Binary files /dev/null and b/5_dpnn/alexnet_torch.png differ diff --git a/5_dpnn/cifar.png b/5_dpnn/cifar.png new file mode 100644 index 0000000000000000000000000000000000000000..9cea2f4704036fb940cca8499311fbfcc41c2cd2 Binary files /dev/null and b/5_dpnn/cifar.png differ diff --git a/5_dpnn/max_pooling.png b/5_dpnn/max_pooling.png new file mode 100644 index 0000000000000000000000000000000000000000..9d50afc6eef0ee21e7bb6d2fee159ee23e11144e Binary files /dev/null and b/5_dpnn/max_pooling.png differ diff --git a/5_dpnn/sheet_5.ipynb b/5_dpnn/sheet_5.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..9cab2f8528ba04bc3d82ebdfbdcdeb227163d0f0 --- /dev/null +++ b/5_dpnn/sheet_5.ipynb @@ -0,0 +1,1534 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Skalierbare Methoden der Künstlichen Intelligenz\n", + "Dr. Charlotte Debus (<charlotte.debus@kit.edu>) \n", + "Dr. Markus Götz (<markus.goetz@kit.edu>) \n", + "Dr. Marie Weiel (<marie.weiel@kit.edu>) \n", + "Dr. Kaleb Phipps (<kaleb.phipps@kit.edu>) \n", + "\n", + "## Übung 5 am 04.02.25: Datenparallele neuronale Netze in `PyTorch`\n", + "In der fünften Übung beschäftigen wir uns mit datenparallelen neuronalen Netzen am Beispiel von *AlexNet* in `PyTorch`. \n", + "*AlexNet* ist ein Convolutional Neural Network (CNN) zur Bildklassifizierung. \n", + "Im ersten Teil dieses Übungsblatts lernen Sie, wie Sie ein neuronales Netz in `PyTorch` trainieren. \n", + "Diese Aufgabe dient als Voraussetzung für das spätere Training des Netzes auf datenparallele Weise. \n", + "\n", + "### Convolutional Neural Networks\n", + "CNNs sind eine Klasse künstlicher neuronaler Netze, die häufig zur Analyse visueller Bilder eingesetzt werden. \n", + "Ein CNN besteht aus einer oder mehreren Faltungsschichten, auf die jeweils eine sogenannte Pooling-Schicht folgt. Diese Einheit kann beliebig oft wiederholt werden. \n", + "Im Vergleich zu voll vernetzten Schichten gibt es drei wesentliche Unterschiede:\n", + "- 2D- oder 3D-Anordnung der Neuronen\n", + "- Gemeinsame Gewichte\n", + "- Lokale Konnektivität\n", + "\n", + "Eine gute Einführung zu CNNs finden Sie [hier](https://www.youtube.com/watch?v=YRhxdVk_sIs&list=PLZbbT5o_s2xq7LwI2y8_QtvuXZedL6tQU&index=19).\n", + "\n", + "#### Was ist eine Faltungsschicht?\n", + "Normalerweise ist die Eingabe eine 2D- oder 3D-Matrix, die die Pixel eines einzelnen Graustufen- oder Farbbilds (Sample) repräsentiert. Die Neuronen in einer Faltungsschicht sind entsprechend angeordnet. \n", + "Die Eingabe eines jeden Neurons wird durch eine diskrete Faltung berechnet, indem eine kleine Matrix, der sogenannte Filterkernel, schrittweise über das Bild bewegt wird. Die Eingabe entspricht dem inneren Produkt des Filterkernels mit dem aktuell betrachteten Bildausschnitt. \n", + "Benachbarte Neuronen in der Faltungsschicht reagieren somit auf überlappende Bereiche in der lokalen Umgebung der Eingabe. \n", + "Ähnlich wie beim biologischen rezeptiven Feld reagiert ein Neuron nur auf Reize in einer lokalen Nachbarschaft der vorherigen Schicht. \n", + "Die Werte im Kernel entsprechen den Gewichten und werden unabhängig voneinander gelernt. \n", + "Sie sind für alle Neuronen einer Schicht gleich, weshalb CNNs translationsinvariant sind. Dies führt dazu, dass z. B. jedes Neuron in der ersten Faltungsschicht die Intensität der Kanten in einem bestimmten lokalen Bereich der Eingabe kodiert.\n", + "\n", + "Um die Randbereiche der Eingabe zu behandeln, gibt es verschiedene Padding-Methoden. \n", + "Eine Übersicht über das Zero Padding in CNNs finden Sie [hier](https://www.youtube.com/watch?v=qSTv_m-KFk0&list=PLZbbT5o_s2xq7LwI2y8_QtvuXZedL6tQU&index=23). \n", + "\n", + "Nachdem die Eingabe jedes Neurons wie oben beschrieben bestimmt wurde, wird sie von einer Aktivierungsfunktion zur Ausgabe weiterverarbeitet, bei CNNs üblicherweise **Re**ctified **L**inear **U**nit, $ReLU\\left(x\\right) = \\text{max}\\left(0,x\\right)$. \n", + "Da Backpropagation die Berechnung von Gradienten erfordert, wird in der Praxis eine differenzierbare Approximation von ReLU verwendet: $f\\left(x\\right)=\\text{ln}\\left(1+e^x\\right)$ \n", + "\n", + "#### Was ist eine Pooling-Schicht?\n", + "Der anschließende Schritt, das Pooling, dient dem Verwerfen unnötiger Informationen. \n", + "Für die Objekterkennung in Bildern ist beispielsweise die genaue Position einer Kante unwichtig. \n", + "Ihre ungefähre Lage ist ausreichend. \n", + "Die bei weitem häufigste Art des Poolings ist das Max-Pooling, bei dem nur die Aktivität des aktivsten Neurons aus jedem n x n-Quadrat von Neuronen in der Faltungsschicht behalten wird. \n", + "Die Aktivitäten der übrigen Neuronen werden verworfen. \n", + "Trotz der Datenreduzierung wird die Performance des Netzes im Allgemeinen nicht beeinträchtigt. \n", + "Tatsächlich bietet das Pooling sogar einige Vorteile:\n", + "\n", + "- Geringerer Speicherbedarf und höhere Rechengeschwindigkeit und damit die Möglichkeit, tiefere Netze zu trainieren, die komplexere Aufgaben lösen können\n", + "- Analog zum visuellen Kortex automatisches Wachstum der Größe der rezeptiven Felder (ohne explizite Vergrößerung der Filterkernels) und zunehmende Komplexität der erkannten Merkmale, z. B. Teile eines Gesichts, in tieferen Faltungsschichten\n", + "- Vermeidung von Overfitting\n", + "\n", + " \n", + "\n", + "Source: By Aphex34 - own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=45673581 \n", + "Einen praktischen Überblick zu Max-Pooling finden Sie [hier](https://www.youtube.com/watch?v=ZjM_XQa5s6s&list=PLZbbT5o_s2xq7LwI2y8_QtvuXZedL6tQU&index=24).\n", + "\n", + "### AlexNet\n", + "*AlexNet* ist eine CNN-Architektur, die von Alex Krizhevsky, Ilya Sutskever und Geoffrey Hinton entwickelt wurde. \n", + "Das Netzwerk löst das Problem der Bildklassifizierung, ursprünglich auf dem ImageNet-Datensatz. \n", + "Die Eingabe ist ein RGB-Bild der Größe 256 x 256 aus einer von 1000 Klassen (z. B. Katzen, Hunde) und die Ausgabe ist ein Vektor von 1000 Zahlen, die sich zu 1 aufsummieren. \n", + "Somit kann das $i$-te Element des Ausgabevektors als die Wahrscheinlichkeit interpretiert werden, dass das Eingabebild zur $i$-ten Klasse gehört. \n", + "\n", + "Bei der *ImageNet Large Scale Visual Recognition Challenge (ILSVRC)* 2012, einem Software-Wettbewerb zur Bildklassifizierung, erreichte *AlexNet* einen Top-5-Fehler von 15,3 % und damit mehr als 10,8 % weniger als der Zweitplatzierte. \n", + "Die wichtigste Erkenntnis war, dass die Tiefe des Modells entscheidend für seine hohe Leistung ist. Diese erfordert einen umfangreichen Einsatz von Rechenressourcen und wurde durch das Training des Netzes auf Grafikprozessoren (GPUs) ermöglicht. \n", + "\n", + "*AlexNet* besteht aus acht Schichten: die ersten fünf sind Faltungsschichten, einige gefolgt von Max-Pooling, die letzten drei sind voll vernetzte lineare Schichten. Es verwendet die nicht sättigende ReLU-Aktivierungsfunktion, die eine bessere Trainingsleistung bietet als tanh und Sigmoid.\n", + "*AlexNet* ist eine der einflussreichsten Arbeiten auf dem Gebiet der Computer Vision, da sie den Anstoß für viele andere Arbeiten gab, die CNNs und GPUs zur Beschleunigung von Deep Learning verwendeten. \n", + "Das Original-Paper wurde laut Google Scholar mehr als 124.000 Mal zitiert (Stand: 12. Januar 2024). \n", + "\n", + "**Paper:** Alex Krizhevsky, Ilya Sutskever, and Geoffrey E. Hinton. [**Imagenet Classification with Deep Convolutional Neural Networks**](https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf). *Advances in Neural Information Processing Systems* 25 (2012): 1097-1105 \n", + "\n", + "Die ursprünglich vorgeschlagene Architektur ist nachstehend skizziert.\n", + " \n", + "\n", + "Source: [https://learnopencv.com/understanding-alexnet/](https://learnopencv.com/understanding-alexnet/)\n", + "\n", + "In diesem Tutorial verwenden wir die leichter parallelisierbare (und nur geringfügig abweichende) Implementierung aus dem [**One weird trick for parallelizing convolutional neural networks**](https://arxiv.org/abs/1404.5997) Paper, die auch in `PyTorch` [doc](https://pytorch.org/vision/main/_modules/torchvision/models/alexnet.html#alexnet) implementiert ist. \n", + "Untenstehend finden Sie einen Überblick über diese Architektur einschließlich aller relevanten Implementierungsdetails. \n", + "\n", + "\n", + "\n", + "### ImageNet\n", + "[ImageNet](https://image-net.org/index.php) ist eine Bilddatenbank, die 2009 auf der *IEEE Conference on Computer Vision and Pattern Recognition (CVPR)* veröffentlicht wurde. \n", + "Jedes Bild ist mit einem Substantiv verknüpft. \n", + "Die Substantive sind durch das WordNet-Projekt hierarchisch geordnet. \n", + "Für jedes Substantiv gibt es im Durchschnitt mehr als 500 Bilder. \n", + "Für mehr als 14 Millionen Bilder wurden die abgebildeten Objekte von Hand dokumentiert. \n", + "Bei mindestens einer Million Bilder sind diese Objekte eingerahmt. \n", + "ImageNet enthält mehr als 20.000 Kategorien in englischer Sprache, mit typischen Kategorien wie \"balloon\" oder \"strawberry\". \n", + "Die Datenbank mit URL-Anmerkungen von Drittanbietern ist direkt über ImageNet frei zugänglich, obwohl die eigentlichen Bilder nicht im Besitz von ImageNet sind.\n", + "\n", + "Seit 2010 veranstaltet das ImageNet-Projekt jährlich einen Software-Wettbewerb, die *ImageNet Large Scale Visual Recognition Challenge (ILSVRC)*. \n", + "Hier treten Softwaresysteme aus den Bereichen Deep Learning und Objekterkennung gegeneinander an, um Objekte und Szenen korrekt zu klassifizieren und zu erkennen. \n", + "Bei diesem Wettbewerb wird eine reduzierte Liste von tausend sich nicht überschneidenden Klassen verwendet.\n", + "\n", + "### CIFAR-10\n", + "Da der ImageNet-Datensatz relativ groß ist, verwenden wir hier CIFAR-10. \n", + "Dieser Datensatz enthält 60.000 Farbbilder der Größe 32 x 32 aus 10 Klassen, wobei jede Klasse 6000 Bilder enthält. \n", + "Der Datensatz ist in fünf Trainings-Batches und ein Test-Batch unterteilt, die jeweils 10.000 Bilder enthalten. \n", + "Das Test-Batch enthält genau 1000 zufällig ausgewählte Bilder aus jeder Klasse. \n", + "Die Trainings-Batches enthalten die restlichen Bilder in zufälliger Reihenfolge, wobei einige Trainings-Batches mehr Bilder aus einer Klasse enthalten können als andere. \n", + "Insgesamt enthalten die Trainings-Batches genau 5000 Bilder aus jeder Klasse.\n", + "\n", + "Untenstehend sehen Sie die Klassen des Datensatzes und zehn Zufallsbilder aus jeder Klasse: \n", + "\n", + " \n", + "\n", + "Quelle: [https://www.cs.toronto.edu/~kriz/cifar.html](https://www.cs.toronto.edu/~kriz/cifar.html)\n", + "\n", + "### Aufgabe 1\n", + "Implementieren Sie *AlexNet* zusammen mit einer sequentiellen Trainingsschleife in `PyTorch`, um den CIFAR-10-Datensatz zu klassifizieren. \n", + "Diesen können Sie entweder selbst downloaden oder unter `/pfs/work7/workspace/scratch/ku4408-VL-ScalableAI/data` auf dem *bwUniCluster* abrufen. Das Tutorial ist wie folgt aufgebaut:\n", + "\n", + "-------------\n", + "- **Modell** \n", + " - Definieren Sie das Modell. \n", + "- **Daten**\n", + " - Definieren Sie die Dataloader. \n", + " - Laden Sie die Daten. \n", + "- **Training**\n", + " - Definieren Sie die Trainingsschleife.\n", + " - Setzen Sie das Modell auf und trainieren Sie es.\n", + "-------------\n", + "Untenstehend finden Sie \n", + "\n", + "- das Grundgerüst des neuronalen Netzes, \n", + "- eine Funktion zum Laden der Daten\n", + "- und das Grundgerüst der Trainingsschleife.\n", + "\n", + "Vervollständigen Sie den Code-Lückentext, um funktionalen `Python`-Code mit allen benötigten Klassen- und Funktionsdefinitionen sowie dem Mainteil zu erstellen. \n", + "**Normale Kommentare mit '#' beschreiben wie üblich den Code, in Zeilen mit '##' müssen Sie Code ergänzen.**\n", + "Führen Sie den Code im JupyterHub bzw. als Batch Job (siehe untenstehendes Submit-Skript) auf einer GPU des *bwUniClusters* aus." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "#!/bin/bash\n", + "\n", + "#SBATCH --job-name=alex\n", + "#SBATCH --partition=gpu_4\n", + "#SBATCH --gres=gpu:1 # number of requested GPUs (GPU nodes shared btwn multiple jobs)\n", + "#SBATCH --time=1:30:00 # wall-clock time limit†\n", + "#SBATCH --mem=128000\n", + "#SBATCH --nodes=1\n", + "#SBATCH --mail-type=ALL\n", + "\n", + "export VENVDIR=<path/to/your/venv> # Export path to your virtual environment.\n", + "export PYDIR=<path/to/your/python/scripts> # Export path to directory containing Python script.\n", + "\n", + "# Set up modules.\n", + "module purge # Unload all currently loaded modules.\n", + "module load compiler/gnu/13.3 # Load required modules.\n", + "module load mpi/openmpi/4.1\n", + "module load devel/cuda/12.4\n", + "\n", + "source ${VENVDIR}/bin/activate\n", + "\n", + "RESDIR=./job_${SLURM_JOB_ID}/\n", + "mkdir ${RESDIR}\n", + "cd ${RESDIR}\n", + "\n", + "python -u ${PYDIR}/alex.py # Execute main script." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Modell\n", + "### Definieren Sie das Modell\n", + "Neuronale Netze bestehen aus Schichten oder Modulen, die Operationen auf Daten durchführen. \n", + "Der `torch.nn` Namenspace von `PyTorch` bietet alle benötigten Bausteine, um ein neuronales Netz aufzusetzen [doc](https://pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html#). \n", + "Ein neuronales Netz ist selbst ein Modul, das aus anderen Modulen (Schichten) besteht. \n", + "Diese verschachtelte Struktur ermöglicht eine einfache Erstellung auch komplexer Architekturen. \n", + "Jedes Modul bzw. neuronale Netz in `PyTorch` sollte eine Unterklasse der Basisklasse `nn.Module` sein. \n", + "Wir beginnen also mit der Implementierung von *AlexNet* als einer benutzerdefinierten Unterklasse von `nn.Module`. \n", + "\n", + "Jede benutzerdefinierte Modellklasse in `PyTorch` überschreibt die `__init__` und die `forward` Methode. \n", + "In `__init__` wird die Architektur des Modells durch Angabe von Art und Reihenfolge der Schichten definiert. \n", + "Operationen auf den Eingabedaten werden in der Methode `forward` implementiert, die die bei jedem Aufruf durchgeführten Berechnungen im sogenannten Forward Pass definiert. \n", + "*Nebenbemerkung: Obwohl der Forward Pass in `forward` definiert ist, sollten Sie im eigentlichen Code stattdessen die Instanz `nn.Module` aufrufen. Dies sorgt dafür, dass die registrierten Hooks ausgeführt werden, während ein einfacher Aufruf von `model.forward()` diese stillschweigend ignoriert.*\n", + "\n", + "Für *AlexNet* benötigen Sie die folgenden Schichten als Bausteine:\n", + "- Faltungsschicht `torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)` [doc](https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html)\n", + "- ReLU-Aktivierungsfunktion `torch.nn.ReLU()` [doc](https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html)\n", + "- Max-Pooling-Schicht `torch.nn.MaxPool2d(kernel_size, stride)` [doc](https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html)\n", + "- Dropout `torch.nn.Dropout(p)` [doc](https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html)\n", + "- Voll vernetzte Schicht `torch.nn.Linear(in_features, out_features)` [doc](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import os\n", + "import time\n", + "from typing import Any, Callable, List, Tuple\n", + "\n", + "import numpy as np\n", + "import torch\n", + "import torchvision" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# MODEL\n", + "# Define neural network by subclassing PyTorch's nn.Module. \n", + "# Save to a separate Python module file `model.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "class AlexNet(torch.nn.Module):\n", + " \"\"\"\n", + " AlexNet architecture.\n", + "\n", + " Attributes\n", + " ----------\n", + " features : torch.nn.container.Sequential\n", + " The convolutional feature-extractor part.\n", + " avgpool : torch.nn.AdaptiveAvgPool2d\n", + " An adaptive pooling layer to handle different input sizes.\n", + " classifier : torch.nn.container.Sequential\n", + " The fully connected linear part.\n", + " \n", + " Methods\n", + " -------\n", + " __init__()\n", + " The constructor defining the network's architecture.\n", + " forward()\n", + " The forward pass.\n", + " \"\"\"\n", + " \n", + " # Initialize neural network layers in __init__. \n", + " def __init__(self, num_classes: int = 1000, dropout: float = 0.5) -> None:\n", + " \"\"\"\n", + " Initialize AlexNet architecture.\n", + "\n", + " Parameters\n", + " ----------\n", + " num_classes : int\n", + " The number of classes in the underlying classification problem.\n", + " dropout : float\n", + " The dropout probability.\n", + " \"\"\"\n", + " super().__init__()\n", + " self.features = torch.nn.Sequential(\n", + " # AlexNet has 8 layers: 5 convolutional layers, some followed by max-pooling (see figure),\n", + " # and 3 fully connected layers. In this model, we use nn.ReLU between our layers.\n", + " # nn.Sequential is an ordered container of modules. \n", + " # The data is passed through all the modules in the same order as defined. \n", + " # You can use sequential containers to put together a quick network.\n", + " #\n", + " # IMPLEMENT FEATURE-EXTRACTOR PART OF ALEXNET HERE!\n", + " # 1st convolutional layer (+ max-pooling)\n", + " torch.nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),\n", + " torch.nn.ReLU(inplace=True),\n", + " torch.nn.MaxPool2d(kernel_size=3, stride=2),\n", + " ## 2nd convolutional layer (+ max-pooling)\n", + " ## 3rd + 4th convolutional layer\n", + " ## 5th convolutional layer (+ max-pooling)\n", + " )\n", + " # Average pooling to downscale possibly larger input images.\n", + " self.avgpool = torch.nn.AdaptiveAvgPool2d((6, 6))\n", + " self.classifier = torch.nn.Sequential( \n", + " # IMPLEMENT FULLY CONNECTED PART HERE!\n", + " # 6th, 7th + 8th fully connected layer \n", + " # The linear layer is a module that applies a linear transformation \n", + " # on the input using its stored weights and biases.\n", + " # 6th fully connected layer (+ dropout)\n", + " torch.nn.Dropout(p=dropout),\n", + " torch.nn.Linear(256 * 6 * 6, 4096),\n", + " torch.nn.ReLU(inplace=True),\n", + " ## 7th fully connected layer (+ dropout)\n", + " # 8th (output) layer\n", + " torch.nn.Linear(4096, num_classes),\n", + " )\n", + " # Forward pass: Implement operations on the input data, i.e., apply model to input x.\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", + " \"\"\"\n", + " The forward pass.\n", + "\n", + " Parameters\n", + " ----------\n", + " x : torch.Tensor\n", + " The input data.\n", + "\n", + " Returns\n", + " -------\n", + " torch.Tensor\n", + " The model's output.\n", + " \"\"\"\n", + " # IMPLEMENT OPERATIONS ON INPUT DATA x HERE!\n", + " ## Apply feature-extractor part to input.\n", + " ## Apply average-pooling part.\n", + " x = torch.flatten(x, 1) # Flatten.\n", + " ## Apply fully connected part.\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Daten\n", + "### Definieren Sie die Dataloader\n", + "Als Nächstes müssen Sie die Daten laden. \n", + "Da der Code für die Vorverarbeitung der Daten schnell unübersichtlich werden kann, sollten Sie diesen idealerweise vom Code für das Training entkoppeln. Dies verbessert Lesbarkeit und Modularität. \n", + "`PyTorch` bietet zwei Datenprimitiven, die die Verwendung von sowohl vorgeladenen als auch eigenen Datensätzen ermöglichen [doc](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html):\n", + "- `torch.utils.data.Dataset`: Speichert die Samples und ihre zugehörigen Labels (Targets)\n", + "- `torch.utils.data.DataLoader`: Wrappt ein Iterable um das `Dataset` und ermöglicht so einfachen Zugriff auf die Samples\n", + "\n", + "Da die Daten meist nicht in der für das Training erforderlichen Form vorliegen, können Sie sie mittels Transformationen im `Dataset` manipulieren. \n", + "Über das `Dataset` Objekt lassen sich Features und Labels des Datensatzes dann sampleweise abfragen. \n", + "Beim Training wollen wir die Samples typischerweise in Form von Mini Batches übergeben und die Daten vor jeder Epoche neu durchmischen, um ein Overfitting des Modells zu vermeiden.\n", + "Der `DataLoader` ist ein Iterable, das diese Komplexität in einer einfachen API abstrahiert. \n", + "Vereinfacht gesagt kombiniert der `DataLoader` den Datensatz mit einer Sampling-Strategie. \n", + "Wir verwenden das CIFAR-10 `Dataset` aus dem `torchvision` Paket. \n", + "Letzteres stellt populäre Datensätze, Modellarchitekturen und gängige Bildtransformationen für Computer Vision bereit [doc](https://pytorch.org/vision/main/index.html). \n", + "Nachfolgend definieren wir die Dataloader inklusive einiger Hilfsfunktionen für unser Klassifizierungsproblem. \n", + "Da *AlexNet* ursprünglich für die Klassifizierung der 256 x 256 RGB-Bilder im ImageNet-Datensatz gedacht war, müssen wir einige Anpassungen vornehmen, damit die gleiche Architektur auch für die Klassifizierung der kleineren RGB-Bilder im CIFAR-10-Datensatz funktioniert. \n", + "Wenn Sie die ursprünglichen 32 x 32 CIFAR-10-Bilder als Eingaben verwenden würden, würden Ihre Samples in der letzten Faltungsschicht verschwinden. \n", + "Um dieses Problem zu beheben, können Sie die CIFAR-10-Bilder auf 64 x 64 Pixel upsamplen, bevor Sie sie als Eingaben an *AlexNet* weitergeben. \n", + "Dies wird unter anderem in den folgenden Transformationen gemacht. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# DATA\n", + "# Save to a separate Python module file `utils_data.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "def get_transforms_cifar10() -> (\n", + " Tuple[torchvision.transforms.Compose, torchvision.transforms.Compose]\n", + "):\n", + " \"\"\"\n", + " Get transforms applied to CIFAR-10 data for AlexNet training and inference.\n", + "\n", + " Returns\n", + " -------\n", + " torchvision.transforms.Compose\n", + " The transforms applied to CIFAR-10 for training AlexNet.\n", + " torchvision.transforms.Compose\n", + " The transforms applied to CIFAR-10 to run inference with AlexNet.\n", + " \"\"\"\n", + " # Transforms applied to training data (randomness to make network more robust against overfitting)\n", + " train_transforms = (\n", + " torchvision.transforms.Compose( # Compose several transforms together.\n", + " [\n", + " torchvision.transforms.Resize(\n", + " (70, 70)\n", + " ), # Upsample CIFAR-10 images to make them work with AlexNet.\n", + " torchvision.transforms.RandomCrop(\n", + " (64, 64)\n", + " ), # Randomly crop image to make NN more robust against overfitting.\n", + " torchvision.transforms.ToTensor(), # Convert image into torch tensor.\n", + " torchvision.transforms.Normalize(\n", + " (0.5, 0.5, 0.5), (0.5, 0.5, 0.5)\n", + " ), # Normalize to [-1,1] via (image-mean)/std.\n", + " ]\n", + " )\n", + " )\n", + "\n", + " test_transforms = torchvision.transforms.Compose(\n", + " [\n", + " torchvision.transforms.Resize((70, 70)),\n", + " torchvision.transforms.CenterCrop((64, 64)),\n", + " torchvision.transforms.ToTensor(),\n", + " torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),\n", + " ]\n", + " )\n", + " return train_transforms, test_transforms\n", + "\n", + "\n", + "def make_train_validation_split(\n", + " train_dataset: torchvision.datasets.CIFAR10,\n", + " seed: int = 123,\n", + " validation_fraction: float = 0.1,\n", + ") -> Tuple[np.ndarray, np.ndarray]:\n", + " \"\"\"\n", + " Split original CIFAR-10 training data into train and validation sets.\n", + "\n", + " Parameters\n", + " ----------\n", + " train_dataset : torchvision.datasets.CIFAR10\n", + " The original CIFAR-10 training dataset.\n", + " seed : int\n", + " The seed used to split the data.\n", + " validation_fraction : float\n", + " The fraction of samples used for validation.\n", + "\n", + " Returns\n", + " -------\n", + " numpy.ndarray\n", + " The sample indices for the training dataset.\n", + " numpy.ndarray\n", + " The sample indices for the validation dataset.\n", + " \"\"\"\n", + " num_samples = len(\n", + " train_dataset\n", + " ) # Get overall number of samples in original training data.\n", + " rng = np.random.default_rng(\n", + " seed=seed\n", + " ) # Set same seed over all ranks for consistent train-test split.\n", + " idx = np.arange(0, num_samples) # Construct array of all indices.\n", + " rng.shuffle(idx) # Shuffle them.\n", + " num_validate = int(\n", + " validation_fraction * num_samples\n", + " ) # Determine number of validation samples from validation split.\n", + " return (\n", + " idx[num_validate:],\n", + " idx[0:num_validate],\n", + " ) # Extract and return train and validation indices.\n", + "\n", + "\n", + "\n", + "def get_dataloaders_cifar10(\n", + " batch_size: int,\n", + " data_root: str = \"data\",\n", + " validation_fraction: float = 0.1,\n", + " train_transforms: Callable[[Any], Any] = None,\n", + " test_transforms: Callable[[Any], Any] = None,\n", + " seed: int = 123,\n", + ") -> Tuple[\n", + " torch.utils.data.DataLoader, torch.utils.data.DataLoader, torch.utils.data.DataLoader\n", + "]:\n", + " \"\"\"\n", + " Get dataloaders for training, validation, and testing on the CIFAR-10 dataset.\n", + "\n", + " Parameters\n", + " ----------\n", + " batch_size : int\n", + " The mini-batch size.\n", + " data_root : str\n", + " The path to the dataset.\n", + " validation_fraction : float\n", + " The fraction of the original training data used for validation.\n", + " train_transforms : Callable[[Any], Any]\n", + " The transform applied to the training data.\n", + " test_transforms : Callable[[Any], Any]\n", + " The transform applied to the validation/testing data (inference).\n", + " seed : int\n", + " The seed for the validation-train split.\n", + "\n", + " Returns\n", + " -------\n", + " torch.utils.data.DataLoader\n", + " The training dataloader.\n", + " torch.utils.data.DataLoader\n", + " The validation dataloader.\n", + " torch.utils.data.DataLoader\n", + " The testing dataloader.\n", + " \"\"\"\n", + " if train_transforms is None:\n", + " train_transforms = torchvision.transforms.ToTensor()\n", + "\n", + " if test_transforms is None:\n", + " test_transforms = torchvision.transforms.ToTensor()\n", + "\n", + " train_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=True, transform=train_transforms, download=True\n", + " )\n", + "\n", + " valid_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=True, transform=test_transforms\n", + " )\n", + "\n", + " test_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=False, transform=test_transforms\n", + " )\n", + "\n", + " # Perform index-based train-validation split of original training data.\n", + " train_indices, valid_indices = make_train_validation_split(\n", + " train_dataset, seed, validation_fraction\n", + " ) # Get train and validation indices.\n", + "\n", + " train_sampler = torch.utils.data.SubsetRandomSampler(train_indices)\n", + " valid_sampler = torch.utils.data.SubsetRandomSampler(valid_indices)\n", + "\n", + " valid_loader = torch.utils.data.DataLoader(\n", + " dataset=valid_dataset,\n", + " batch_size=batch_size,\n", + " sampler=valid_sampler,\n", + " )\n", + "\n", + " train_loader = torch.utils.data.DataLoader(\n", + " dataset=train_dataset,\n", + " batch_size=batch_size,\n", + " drop_last=True,\n", + " sampler=train_sampler,\n", + " )\n", + "\n", + " test_loader = torch.utils.data.DataLoader(\n", + " dataset=test_dataset,\n", + " batch_size=batch_size,\n", + " shuffle=False,\n", + " )\n", + "\n", + " return train_loader, valid_loader, test_loader" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Laden Sie die Daten\n", + "Wir erzeugen die Transformationen mit `get_transforms_cifar10` für die Datenvorverarbeitung und verwenden die obige Datenloader-Funktion `get_dataloaders_cifar10`, um die Daten zu laden. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# DATASET\n", + "# Include into your main script to be executed when running as a batch job later on.\n", + "\n", + "# Transforms on your data allow you to take it from its source state and transform it into ready-for-training data.\n", + "# Get transforms applied to CIFAR-10 data for training and inference.\n", + "## train_transforms, test_transforms = ...\n", + "\n", + "b = 256 # Set mini-batch size hyperparameter.\n", + "data_root = \"/pfs/work7/workspace/scratch/ku4408-VL_ScalableAI/data/cifar\" # Path to data dir.\n", + "\n", + "# GET PYTORCH DATALOADERS FOR TRAINING, TESTING, AND VALIDATION DATASET.\n", + "## train_loader, valid_loader, test_loader = get_dataloaders_cifar10(...)\n", + "\n", + "# Check loaded dataset.\n", + "for images, labels in train_loader: \n", + " print(\"Image batch dimensions:\", images.shape)\n", + " print(\"Image label dimensions:\", labels.shape)\n", + " print(\"Class labels of 10 examples:\", labels[:10])\n", + " break" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Training\n", + "### Definieren Sie die Trainingschleife\n", + "Nachdem wir unser Modell definiert und die Daten geladen haben, wollen wir das Modell nun trainieren, validieren und testen, indem wir seine Parameter anhand der Trainingsdaten optimieren. \n", + "Beim Training neuronaler Netze ist der am häufigsten verwendete Algorithmus die sogenannte Backpropagation. \n", + "Dabei werden die Parameter bzw. Modellgewichte entsprechend dem Gradienten der Lossfunktion bzgl. der jeweiligen Parameter angepasst. \n", + "Zur automatischen Berechnung dieser Gradienten hat `PyTorch` eine eingebaute Differenzierungsmaschinerie namens `torch.autograd`. \n", + "\n", + "Das Training eines Modells ist ein iterativer Prozess. In jeder Iteration sagt das Modell die Ausgabe für eine gegebene Eingabe voraus, berechnet über die Lossfunktion den Fehler in seiner Vorhersage bzgl. des Targets, sammelt die Ableitungen des Losses nach den Modellparametern ein und optimiert diese Parameter mit Hilfe des Gradientenabstiegs. \n", + "Eine detailliertere Darstellung dieses Prozesses finden Sie in diesem Video über Backpropagation von [3Blue1Brown](https://www.youtube.com/watch?v=tIeHLnjs5U8).\n", + "\n", + "Das Training wird von sogenannten Hyperparametern (HPs) beeinflusst, mit denen Sie den Modelloptimierungsprozess steuern können.\n", + "Bevor Sie Ihr Modell trainieren, müssen Sie die folgenden HPs einstellen:\n", + "- **Anzahl der Epochen:** Wie oft soll der gesamte Datensatz durchlaufen werden soll?\n", + "- **Batchgröße:** Wie viele Samples werden durch das Netzwerk propagiert, bevor die Parameter aktualisiert werden?\n", + "- **Lernrate (LR):** Wie stark werden die Modellparameter bei jedem Updateschritt angepasst? Kleinere Werte ergeben eine langsame Lerngeschwindigkeit, während große Werte zu unvorhersehbarem Verhalten während des Trainings führen können.\n", + "\n", + "Sobald Sie diese HPs festgelegt haben, können Sie Ihr Modell in einer Optimierungsschleife trainieren und optimieren.\n", + "Dabei entspricht jede Iteration einer Epoche, die aus zwei Hauptteilen besteht:\n", + "- **Trainingsschleife:** Iterationen über die Mini Batches im Trainingsdatensatz zur Anpassung der Parameter.\n", + "- **Validierungsschleife:** Iterationen über den Validierungsdatensatz, um zu prüfen, ob sich die Performance des Modells auf ungesehenen Daten weiter verbessert.\n", + "\n", + "Die wichtigsten Konzepte, die in der Trainingsschleife verwendet werden, werden im Folgenden näher erläutert. \n", + "Wenn Sie mit diesen Dingen in `PyTorch` bereits vertraut sind, können Sie diese Teile einfach überspringen.\n", + "\n", + "#### Lossfunktion\n", + "Erhält ein untrainiertes Netz Eingabedaten, so sagt es mit hoher Wahrscheinlichkeit nicht die richtige Antwort vorher.\n", + "Wir verwenden eine Lossfunktion, um den Grad der Unähnlichkeit einer Vorhersage mit dem tatsächlichen Target (Label, Ground Truth) zu messen. \n", + "Die Lossfunktion wird während des Trainings minimiert. \n", + "Um den Loss zu berechnen, machen wir eine Vorhersage basierend auf den Eingaben und vergleichen sie mit dem wahren Wert der zugehörigen Labels. \n", + "Eine gängige Lossfunktion für die Klassifizierung ist die Cross Entropy ([doc](https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html#torch.nn.CrossEntropyLoss) bzw. [doc](https://pytorch.org/docs/stable/generated/torch.nn.functional.cross_entropy.html)), die die Logits normalisiert und den Vorhersagefehler errechnet.\n", + "\n", + "#### Optimierer\n", + "Optimierung ist der Prozess der Anpassung der Modellparameter mit dem Ziel, den Modellfehler in jedem Trainingsschritt zu reduzieren. Optimierungsalgorithmen definieren, wie dieser Prozess genau durchgeführt wird. \n", + "Das Paket `torch.optim` bietet verschiedene Optimierungsalgorithmen [doc](https://pytorch.org/docs/stable/optim.html). \n", + "Um `torch.optim` zu verwenden, müssen Sie ein `optimizer`-Objekt konstruieren, das die gesamte Optimierungslogik kapselt. \n", + "Wir initialisieren den `optimizer`, indem wir die zu trainierenden Modellparameter registrieren und die Lernrate übergeben.\n", + "Während des Trainings hält der `optimizer` den aktuellen Zustand und aktualisiert die Parameter auf Grundlage der berechneten Gradienten. \n", + "\n", + "Hier verwenden wir den stochastischen Gradientenabstieg, `torch.optim.SGD`. Zusätzlich sind viele verschiedene Optimierer in `PyTorch` verfügbar, wie ADAM und RMSProp, die für verschiedene Arten von Modellen und Daten besser funktionieren.\n", + "\n", + "#### Trainingsschleife\n", + "Innerhalb der Trainingsschleife erfolgt die Optimierung technisch gesehen in drei Schritten:\n", + "- Rufen Sie `optimizer.zero_grad()` auf, um die Gradienten der Parameter Ihres Modells zurückzusetzen, bevor Sie einen neuen Mini Batch verarbeiten. Standardmäßig addieren sich die Gradienten auf. Um eine doppelte Berücksichtigung zu vermeiden, werden sie vor jeder Iteration explizit auf Null gesetzt.\n", + "- Backpropagieren Sie den Loss mit einem Aufruf von `loss.backward()`. `PyTorch` hinterlegt die Gradienten des Losses in Bezug auf jeden Parameter. \n", + "- Rufen Sie `optimizer.step()` auf, um die Parameter basierend auf den im Backward Pass gesammelten Gradienten anzupassen.\n", + "\n", + "#### Anpassung der Lernrate\n", + "Beim Training tiefer neuronaler Netze ist es oftmals sinnvoll, die Lernrate im Laufe des Trainings zu reduzieren. \n", + "Sogenannte Learning Rate Scheduler passen die Lernrate während des Trainings an, indem sie sie nach einem vordefinierten Zeitplan reduzieren. \n", + "`torch.optim.lr_scheduler` bietet mehrere Methoden, um dies auf Grundlage der Anzahl der Epochen zu tun. \n", + "Zum Beispiel erlaubt `torch.optim.lr_scheduler.ReduceLROnPlateau` eine dynamische LR-Reduktion basierend auf einigen Validierungsmessungen, wie beispielsweise der Accuracy des Modells auf dem Validierungsdatensatz.\n", + "LR Scheduling sollte nach dem Update durch den Optimierer am Ende jeder Epoche durchgeführt werden.\n", + "\n", + "Untenstehend definieren wir all dies in der Funktion `train_model` sowie einer weiteren Hilfsfunktion `compute_accuracy`. \n", + "Letztere berechnet die Genauigkeit der Vorhersagen Ihres Modells auf einem bestimmten Datensatz. \n", + "Sie benötigen diese Funktion, um Ihr Modell während des Trainings zu validieren und um es nach dem Training auf einem ungesehenen Testdatensatz zu testen. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# EVALUATION\n", + "# Save to a separate Python module file `utils_eval.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "def compute_accuracy(\n", + " model: torch.nn.Module,\n", + " data_loader: torch.utils.data.DataLoader,\n", + " device: torch.device,\n", + ") -> float: \n", + " \"\"\"\n", + " Compute the accuracy of the model's predictions on given labeled data.\n", + " \n", + " Parameters\n", + " ----------\n", + " model : torch.nn.Module\n", + " The model.\n", + " data_loader : torch.utils.data.DataLoader\n", + " The dataloader.\n", + " device : torch.device\n", + " The device to use.\n", + " \n", + " Returns\n", + " -------\n", + " float\n", + " The model's accuracy on the given dataset in percent.\n", + " \"\"\"\n", + " with torch.no_grad(): # Disable gradient calculation to reduce memory consumption.\n", + "\n", + " # Initialize number of correctly predicted samples + overall number of samples.\n", + " correct_pred, num_examples = (\n", + " 0,\n", + " 0,\n", + " ) # Initialize number of correctly predicted and overall samples, respectively.\n", + "\n", + " for i, (features, targets) in enumerate(data_loader):\n", + " # CONVERT DATASET TO USED DEVICE.\n", + " ## features = ...\n", + " ## targets = ...\n", + " #\n", + " # CALCULATE PREDICTIONS OF CURRENT MODEL ON FEATURES OF INPUT DATA.\n", + " ## logits = ...\n", + " ## Determine class with highest score.\n", + " ## Compare predictions to actual labels to determine number of correctly predicted samples.\n", + " ## Determine overall number of samples.\n", + " \n", + " # CALCULATE AND RETURN ACCURACY AS PERCENTAGE OF CORRECTLY PREDICTED SAMPLES.\n", + " ## return ... " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TRAINING\n", + "# Save to a separate Python module file `utils_train.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "# from utils_eval import compute_accuracy\n", + "\n", + "def train_model(\n", + " model: torch.nn.Module,\n", + " num_epochs: int,\n", + " train_loader: torch.utils.data.DataLoader,\n", + " valid_loader: torch.utils.data.DataLoader,\n", + " test_loader: torch.utils.data.DataLoader,\n", + " optimizer: torch.optim.Optimizer,\n", + " device: torch.device,\n", + " logging_interval: int = 50,\n", + " scheduler: torch.optim.lr_scheduler._LRScheduler = None,\n", + ") -> Tuple[List[float], List[float], List[float]]:\n", + " \"\"\"\n", + " Train your model.\n", + "\n", + " Parameters\n", + " ----------\n", + " model : torch.nn.Module\n", + " The model to train.\n", + " num_epochs : int\n", + " The number of epochs to train\n", + " train_loader : torch.utils.data.DataLoader\n", + " The training dataloader.\n", + " valid_loader : torch.utils.data.DataLoader\n", + " The validation dataloader.\n", + " test_loader : torch.utils.data.DataLoader\n", + " The testing dataloader.\n", + " optimizer : torch.optim.Optimizer\n", + " The optimizer to use.\n", + " device : torch.device\n", + " The device to train on.\n", + " logging_interval : int\n", + " The logging interval.\n", + " scheduler : torch.optim.lr_scheduler._LRScheduler\n", + " An optional learning rate scheduler.\n", + "\n", + " Returns\n", + " -------\n", + " List[float]\n", + " The loss history.\n", + " List[float]\n", + " The training accuracy history.\n", + " List[float]\n", + " The validation accuracy history.\n", + " \"\"\"\n", + " ## start = ... # Start timer to measure training time.\n", + "\n", + " # Initialize history lists for loss, training accuracy, and validation accuracy.\n", + " loss_history, train_acc_history, valid_acc_history = [], [], []\n", + "\n", + " # ACTUAL TRAINING STARTS HERE. \n", + " for epoch in range(num_epochs): # Loop over epochs.\n", + "\n", + " # IMPLEMENT TRAINING LOOP HERE.\n", + " #\n", + " ## Set model to training mode.\n", + " # Thus, layers like dropout which behave differently on train and \n", + " # test procedures know what is going on and can behave accordingly. \n", + " \n", + " for batch_idx, (features, targets) in enumerate(\n", + " train_loader\n", + " ): # Loop over mini batches.\n", + " # CONVERT DATASET TO USED DEVICE.\n", + " ## features = ... # Move features to used device.\n", + " ## targets = ... # Move targets to used device.\n", + " #\n", + " # FORWARD & BACKWARD PASS\n", + " ## logits = ... # Get predictions of model with current parameters.\n", + " ## loss = ... # Calculate cross-entropy loss on current mini-batch.\n", + " ## Zero out gradients.\n", + " ## Calculate gradients of loss w.r.t. model parameters in backward pass.\n", + " ## Perform single optimization step to update model parameters via optimizer.\n", + " #\n", + " # LOGGING\n", + " ## Append loss to history list.\n", + " \n", + " if not batch_idx % logging_interval:\n", + " print(\n", + " f\"Epoch: {epoch+1:03d}/{num_epochs:03d} \"\n", + " f\"| Batch {batch_idx:04d}/{len(train_loader):04d} \"\n", + " f\"| Loss: {loss:.4f}\"\n", + " )\n", + "\n", + " # VALIDATION STARTS HERE.\n", + " #\n", + " ## Set model to evaluation mode.\n", + " \n", + " with torch.no_grad(): # Disable gradient calculation to reduce memory consumption.\n", + " \n", + " # COMPUTE ACCURACY OF CURRENT MODEL PREDICTIONS ON TRAINING + VALIDATION DATASETS.\n", + " ## train_acc = compute_accuracy(...) # Compute accuracy on training data.\n", + " ## valid_acc = compute_accuracy(...) # Compute accuracy on validation data.\n", + " \n", + " print(\n", + " f\"Epoch: {epoch+1:03d}/{num_epochs:03d} \"\n", + " f\"| Train: {train_acc :.2f}% \"\n", + " f\"| Validation: {valid_acc :.2f}%\"\n", + " )\n", + " \n", + " ## APPEND ACCURACY VALUES TO CORRESPONDING HISTORY LISTS.\n", + " \n", + " ## elapsed = ... # Stop timer and calculate training time elapsed after epoch.\n", + " ## Print training time elapsed after epoch.\n", + " \n", + " if scheduler is not None: # Adapt learning rate.\n", + " scheduler.step(valid_acc_history[-1])\n", + " \n", + " ## elapsed = ... # Stop timer and calculate total training time.\n", + " ## Print overall training time.\n", + " \n", + " # FINAL TESTING STARTS HERE.\n", + " #\n", + " ## test_acc = compute_accuracy(...) # Compute accuracy on test data.\n", + " ## Print test accuracy.\n", + "\n", + " ## Return history lists for loss, training accuracy, and validation accuracy." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setzen Sie das Modell auf und trainieren Sie es\n", + "Bevor Sie Ihr Modell im Mainskript trainieren, müssen Sie einige allgemeine Einstellungen vornehmen, wie beispielsweise die Trainings-Hyperparameter. \n", + "Da wir unser Modell auf einem Hardware-Beschleuniger, d.h. einer GPU, trainieren wollen, prüfen wir zunächst, ob `torch.cuda` verfügbar ist, andernfalls verwenden wir die CPU. \n", + "\n", + "Dann erstellen wir eine Instanz von `AlexNet`, verschieben sie auf das `device` und geben die Struktur des Modells aus. \n", + "*Reminder: Um das Modell zu verwenden, übergeben wir diesem lediglich die Eingabedaten. Dies führt automatisch die Methode `forward` aus, zusammen mit einigen Hintergrundoperationen. Rufen Sie `model.forward()` nicht direkt auf!*\n", + "\n", + "Außerdem setzen wir wie oben beschrieben eine `optimizer`- und `scheduler`-Instanz auf.\n", + "Um mit dem Training zu beginnen, rufen wir unsere Funktion `train_model` auf und übergeben dieser das Modell, die Trainings-HPs, die Dataloader, den Optimierer und den Scheduler." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SETTINGS\n", + "# Include into your main script to be executed when running as a batch job later on.\n", + "\n", + "e = 100 # Number of epochs\n", + "lr = 0.1 # Learning rate\n", + "\n", + "# Get device used for training, e.g., check via torch.cuda.is_available().\n", + "## device = ...\n", + "## Print used device.\n", + "\n", + "## model = ... # Build an instance of AlexNet with 10 classes for CIFAR-10 and convert it to the used device.\n", + "## Print model.\n", + "\n", + "# Set up an SGD optimizer from the `torch.optim` package.\n", + "# Use a momentum of 0.9 and a learning rate of 0.1.\n", + "## optimizer = ... \n", + "\n", + "# Set up a LR scheduler. \n", + "scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.1, mode=\"max\", verbose=True)\n", + "\n", + "# TRAIN MODEL.\n", + "## loss_history, train_acc_history, valid_acc_history = train_model(...)\n", + "\n", + "# Save history lists for loss, training accuracy, and validation accuracy.S\n", + "torch.save(loss_list, \"loss.pt\")\n", + "torch.save(train_acc_list, \"train_acc.pt\")\n", + "torch.save(valid_acc_list, \"valid_acc.pt\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sie haben erfolgreich ein tiefes neuronales Netz in `PyTorch` trainiert. Um Ihre Ergebnisse visuell zu analysieren, können Sie nun die Entwicklung des Losses, der Trainingsgenauigkeit und der Validierungsgenauigkeit während des Trainings darstellen, z. B. mit `matplotlib.pyplot`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Datenparallele neuronale Netze (DPNNs) in `PyTorch`\n", + "Im Folgenden lernen Sie, wie man dasselbe Netz in einer verteilten datenparallelen Weise trainiert. Dazu werden Sie das Modul `DistributedDataParallel` von `PyTorch` verwenden. \n", + "\n", + "Beim datenparallelen Training neuronaler Netze werden die Trainingsdaten auf verschiedene Prozessoren aufgeteilt, welche die Daten parallel bearbeiten. So lässt sich das Training durch einen erhöhten Trainingsdurchsatz beschleunigen. \n", + "`PyTorch` bietet hierfür das Modul `DistributedDataParallel` (DDP), das einige der Komplexitäten der Implementierung von datenparallelem Training in einer verteilten Umgebung abstrahiert. \n", + "Aus der offiziellen [Dokumentation](https://pytorch.org/docs/master/generated/torch.nn.parallel.DistributedDataParallel.html): \n", + ">Distributed data-parallel training is a widely adopted single-program multiple-data training paradigm. The model is replicated on every process, and every model replica will be fed with a different set of input data samples. The `DistributedDataParallel` module takes care of gradient communication to keep model replicas synchronized and overlaps it with the gradient computations to speed up training. \n", + "It implements data parallelism at the module level which can run across multiple machines. Applications using `DDP` should spawn multiple processes and create a single `DDP` instance per process. `DDP` uses collective communications in the `torch.distributed` package to synchronize gradients and buffers. More specifically, `DDP` registers an autograd hook for each parameter given by `model.parameters()` and the hook will fire when the corresponding gradient is computed in the backward pass. Then `DDP` uses that signal to trigger gradient synchronization across processes. \n", + "*The recommended way to use `DDP` is to spawn one process for each model replica. `DDP` processes can be placed on the same machine or across machines, but GPU devices cannot be shared across processes.* \n", + "\n", + "Das Paket `torch.distributed` unterstützt drei eingebaute Backends für die Kommunikation zwischen Prozessoren. \n", + "Diese [Tabelle](https://pytorch.org/docs/stable/distributed.html#backends) zeigt, welche Funktionen für die Verwendung mit CPU- bzw. CUDA-Tensoren verfügbar sind. \n", + "Da der *bwUniCluster* die GPUs mit NVLink innerhalb eines Knotens und Mellanox Infiniband Interconnect zwischen den Knoten verbindet, verwenden wir das offiziell empfohlene NCCL-Backend. \n", + "Die [NVIDIA Collective Communication Library](https://developer.nvidia.com/nccl) (NCCL) implementiert Multi-GPU- und Multi-Node-Kommunikationsfunktionen, die für NVIDIA GPUs und -Netzwerke optimiert sind. \n", + "Sie bietet Routinen wie All-Gather, All-Reduce, Broadcast, Reduce, Reduce-Scatter und Point-to-Point Transmit und Receive. \n", + "\n", + "#### Wie man ein DPNN mit dem `DistributedDataParallel`-Modul von `PyTorch` trainiert\n", + "Im Folgenden finden Sie eine Anleitung für das Training eines DPNN mit `DDP` in `PyTorch`:\n", + "\n", + "1. **Initialisierung der verteilten Rechenumgebung:** Bevor Sie `DDP` verwenden, müssen Sie die verteilte Rechenumgebung definieren und initialisieren. Dazu gehört das Einrichten des Kommunikations-Backends (in unserem Fall NCCL), das Aufsetzen der sogenannten Prozessgruppe und das Zuweisen eines eindeutigen Rangs und der Weltgröße für jeden Prozess in der Prozessgruppe. Der Rang ist eine eindeutige Prozess-ID und die Weltgröße entspricht der Gesamtzahl der verwendeten Prozesse. \n", + "\n", + "2. **Laden Sie die Daten:** Datenparallelität bedeutet, dass die Eingabedaten auf die Prozesse in der Prozessgruppe aufgeteilt und die Forward und Backward Passes unabhängig für jeden Rang berechnet werden. Dies ermöglicht eine parallele Verarbeitung und reduziert die Trainingszeit. Wir laden die Trainings- und Validierungsdatensätze und verteilen sie gleichmäßig auf die Prozesse, sodass jeder Prozess eine andere, exklusive Teilmenge eines jeden Datensatzes erhält. `PyTorch` bietet hierfür den sogenannten `DistributedSampler`.\n", + "\n", + "3. **Modellinstanziierung und Replikation:** Danach muss das Modell auf die Prozesse repliziert werden. Während des Trainings verarbeitet jede Modellkopie eine Teilmenge der vom `DistributedSampler` bereitgestellten Eingabedaten. Dazu instanziieren wir das Modell wie im seriellen Fall und wrappen es mit `DDP`. So wird sichergestellt, dass die während des Backward Passes berechneten Gradienten über alle Modellkopien hinweg synchronisiert werden.\n", + "\n", + "4. **Trainingsschleife:** Wiederhole für eine bestimmte Anzahl von Iterationen oder bis zur Konvergenz:\n", + "\n", + " - *Forward Pass*: Jede Modellkopie verarbeitet unabhängig ihren Teil der Eingabedaten. \n", + " - *Backward Pass und Gradientensynchronisation*: Die Gradienten werden auf jeder Modellkopie unabhängig voneinander berechnet. Anschließend werden sie mit einer \"all-reduce\"-Operation über alle Kopien hinweg synchronisiert. Dieser Schritt stellt sicher, dass die Modellparameter über alle Prozesse hinweg konsistent aktualisiert werden.\n", + " - *Optimierungsschritt:* Sobald die Gradienten synchronisiert sind, führt der Optimierer einen Optimierungsschritt durch, um die Modellparameter zu aktualisieren. Dieser Schritt wird unabhängig und redundant für jede Kopie durchgeführt.\n", + " - *Validierung*: Nach der Aktualisierung der Modellparameter können Sie die Accuracy des aktuellen Modells auf den Trainings- und Validierungsdatensätzen berechnen. Da jeder Prozess nur über einen Teil eines jeden Datensatzes verfügt, müssen Sie weitere Kommunikationsschritte implementieren, um die Accuracy für den jeweiligen gesamten Datensatz zu erhalten. \n", + "\n", + "\n", + "5. **Evaluierung:** Nach dem Training können Sie die Performance des endgültigen Modells auf einem ungesehenen Testdatensatzes berechnen. Die Evaluierung wird in der Regel auf einem einzigen Prozess ohne Datenparallelität durchgeführt.\n", + "\n", + "### Aufgabe 2\n", + "\n", + "Aufbauend auf Aufgabe 1 lernen Sie, wie man eine datenparallele Version von *AlexNet* in `PyTorch` trainiert. Dieses Tutorial führt Sie durch die Schritte, die für die Parallelisierung des Trainings in einer datenparallelen Weise mit `DDP` erforderlich sind. \n", + "Dazu müssen Sie `Python`-Skripte aus den in diesem Notebook bereitgestellten Codeschnipseln erstellen und die Skripte als Batch-Job auf dem *bwUniCluster* ausführen.\n", + "\n", + "Das Tutorial ist wie folgt aufgebaut:\n", + "\n", + "-------------\n", + "1. Definieren Sie alle Bausteine.\n", + "- **Modell:** Definieren Sie Ihr Modell. \n", + "- **Daten:** Definieren Sie die Dataloader. \n", + "\n", + "- **Training:** Definieren Sie die Trainingsschleife.\n", + "\n", + "2. Stellen Sie das Main-Python-Skript aus diesen Bausteinen zusammen. \n", + "\n", + "3. Lassen Sie Ihren Code parallel als Batch-Job auf dem *bwUniCluster* laufen. \n", + "- Verwenden Sie vier GPUs, d.h. vier Prozesse im datenparallelen Trainingsprozess. \n", + "- Starten Sie Ihr `Python`-Skript parallel mit dem Befehl `srun` ([doc](https://slurm.schedmd.com/srun.html)) in einem Job-Bash-Skript. \n", + "- Übergeben Sie Ihr Job-Skript an den [SLURM](https://www.schedmd.com/) Workload-Manager mit `sbatch` ([doc](https://slurm.schedmd.com/sbatch.html)).\n", + "\n", + "-------------\n", + "\n", + "Nachfolgend finden Sie ein entsprechendes Codegerüst mit Dataloadern für das Training einschließlich Validierung und Testen, zusammen mit ausführlichen Erklärungen und Anweisungen für jeden Schritt. \n", + "**Normale Kommentare mit '#' beschreiben wie üblich den Code, in Zeilen mit '##' müssen Sie Code ergänzen.** " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Definieren Sie alle Bausteine\n", + "\n", + "### Modell: Definieren des Modells\n", + "Als ersten Schritt müssen Sie analog zum seriellen Fall in Aufgabe 1 die Modellarchitektur definieren. \n", + "Später werden Sie eine Instanz dieses Moduls mit `DDP` wrappen, um es in ein verteiltes, datenparalleles Modell zu konvertieren. \n", + "Speichern Sie den Modell-Code als separate Python-Moduldatei `model.py`, damit Sie die `AlexNet`-Modulklasse aus dieser Datei in Ihr Mainskript importieren können. \n", + "\n", + "### Daten: Definieren der Dataloader\n", + "Als Nächstes müssen Sie wieder die Daten einlesen. Sie haben bereits gelernt, dass zum Trainieren eines DPNN jeder Prozess eine exklusive Teilmenge des Datensatzes laden muss. \n", + "`PyTorch` bietet einen speziellen Sampler zum Verteilen und Laden von Daten in einer verteilten Trainingsumgebung, den sogenannten `DistributedSampler`.\n", + "Dieser ermöglicht das effiziente Laden von Daten über mehrere Prozesse hinweg, indem der Datensatz in kleinere Teilmengen partitioniert wird, die von jedem Prozess unabhängig voneinander verarbeitet werden.\n", + "Der `DistributedSampler` arbeitet in Verbindung mit `DDP`. \n", + "Er stellt sicher, dass jeder Prozess eine eigene Teilmenge der Daten bearbeitet, wodurch redundante Berechnungen vermieden und Parallelität ermöglicht werden. \n", + "Nachfolgend finden Sie einen Überblick über die Funktionsweise:\n", + "\n", + "1. **Datenpartitionierung:** Der `DistributedSampler` unterteilt den Datensatz in kleinere Teilmengen gemäß der Anzahl der am verteilten Training beteiligten Prozesse. Jeder Prozess ist für die Verarbeitung einer bestimmten Teilmenge der Daten verantwortlich.\n", + "2. **Shuffling und Sampling:** Optional kann der `DistributedSampler` den Datensatz vor der Partitionierung durchmischen, um Verzerrungen (Bias) zu vermeiden und die Generalisierung des Modells zu verbessern. Das Shuffling wird in der Regel von einem einzigen Prozess durchgeführt, welcher die Shuffle-Indizes dann an all anderen Prozesse weitergibt.\n", + "3. **Datenladen:** Jeder Prozess lädt die ihm zugewiesene Teilmenge des Datensatzes mit Hilfe des `DistributedSampler`. Der Sampler stellt Indizes bereit, die den Samples der exklusiven Partition des Datensatzes auf diesem Prozess entsprechen.\n", + "4. **Parallelverarbeitung:** Sobald die Daten geladen sind, arbeitet jeder Prozess unabhängig auf seinem Teil des Datensatzes. Forward und Backward Passes sowie der Optimierungsschritt werden für jeden Prozess separat durchgeführt.\n", + "5. **Synchronisation:** Nach jeder Trainingsiteration synchronisieren sich die Prozesse, um sicherzustellen, dass die Modellparameter und Gradienten in allen Prozessen konsistent sind. Diese Synchronisation wird von `DDP` durchgeführt.\n", + "6. **Iterations- und Epochenabschluss:** Der `DistributedSampler` verwaltet den Abschluss von Iterationen und Epochen. Er stellt sicher, dass jeder Prozess die Verarbeitung der ihm zugewiesenen Teilmenge der Daten abschließt, bevor er zur nächsten Iteration oder Epoche übergeht. Der `DistributedSampler` kann zusätzlich den Datensatz am Ende jeder Epoche neu durchmischen.\n", + "\n", + "Vervollständigen Sie den nachstehenden Code und speichern Sie ihn in einer separaten Python-Moduldatei `utils_data.py`, sodass Sie den Dataloader aus dieser Datei in Ihr Mainskript importieren können." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Training: Definieren Sie die Trainingsschleife\n", + "Nun möchten wir *AlexNet* auf den verteilten CIFAR-10-Daten trainieren, validieren und testen, indem wir seine Parameter mit `DDP` datenparallel optimieren. \n", + "Da `DDP` die Synchronisierung der Gradienten über alle Prozesse hinweg für Sie übernimmt, bleibt die Struktur der Trainingsschleife im Wesentlichen gleich. \n", + "Während jeder Prozessor seine Modellkopie auf seinem vom `DistributedSampler` bereitgestellten, lokalen Trainingsbatch trainiert, \n", + "kümmert sich `DDP` um die Gradientenkommunikation, um die Modellkopien synchron zu halten. \n", + "Sie möchten vielleicht auch den durchschnittlichen Loss über alle Prozesse während des Trainings verfolgen. \n", + "Da `DDP` nur die Synchronisation der Gradienten für Sie übernimmt, müssen Sie dies explizit mit den kollektiven Kommunikationsfunktionen von `torch.distributed` ([doc](https://pytorch.org/docs/stable/distributed.html#collective-functions)) implementieren. \n", + "\n", + "Ähnlich wie im seriellen Fall definieren wir einige Hilfsfunktionen zur Validierung des Modells während des Trainings und zum anschließenden Testen mit ungesehenen Daten:\n", + "- `get_right_ddp`: Ermittelt die Anzahl der korrekt vorhergesagten sowie der gesamten Samples in einem Datensatz. Sie benötigen diese Zahlen, um die Accuracy Ihres Modells während der Trainingsschleife auf den verteilten Trainings- und Validierungsdatensätzen zu berechnen.\n", + "- `compute_accuracy_ddp`: Berechnet die Accuracy der Vorhersagen Ihres Modells für einen bestimmten Datensatz. Sie benötigen diese Funktion, um Ihr fertig trainiertes Modell auf einem ungesehenen Testdatensatz zu testen. Ähnlich wie beim seriellen Fall mit einigen geringfügigen technischen Unterschieden.\n", + "\n", + "Die gesamte Funktionalität ist in der untenstehenden Funktion `train_model_ddp` definiert. \n", + "Vervollständigen Sie den Code und speichern Sie ihn als separate Python-Moduldatei `utils_train.py`, damit Sie die Trainingsfunktion aus dieser Datei in Ihr Mainskript importieren können. Die Hilfsfunktionen können Sie in der Python-Moduldatei `utils_eval.py` speichern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# DATA\n", + "# Save to a separate Python module file `utils_data.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "from typing import Any, Callable, Tuple\n", + "\n", + "import torch\n", + "import torchvision\n", + "import numpy as np\n", + "\n", + "\n", + "def get_dataloaders_cifar10_ddp(\n", + " batch_size: int,\n", + " data_root: str = \"data\",\n", + " validation_fraction: float = 0.1,\n", + " train_transforms: Callable[[Any], Any] = None,\n", + " test_transforms: Callable[[Any], Any] = None,\n", + " seed=123,\n", + ") -> Tuple[torch.utils.data.DataLoader, torch.utils.data.DataLoader]:\n", + " \"\"\"\n", + " Get distributed CIFAR-10 dataloaders for training and validation in a DDP setting.\n", + "\n", + " Parameters\n", + " ----------\n", + " batch_size : int\n", + " The batch size.\n", + " data_root : str\n", + " The path to the data directory.\n", + " validation_fraction : float\n", + " The fraction of training samples used for validation.\n", + " train_transforms : Callable[[Any], Any]\n", + " The transform applied to the training data.\n", + " test_transforms : Callable[[Any], Any]\n", + " The transform applied to the testing data (inference).\n", + " seed : int\n", + " Seed for train-validation split.\n", + "\n", + " Returns\n", + " -------\n", + " torch.utils.data.DataLoader\n", + " The training dataloader.\n", + " torch.utils.data.DataLoader\n", + " The validation dataloader.\n", + " \"\"\"\n", + " if train_transforms is None:\n", + " train_transforms = torchvision.transforms.ToTensor()\n", + " if test_transforms is None:\n", + " test_transforms = torchvision.transforms.ToTensor()\n", + "\n", + " if (\n", + " dist.get_rank() == 0\n", + " ): # Only root shall download dataset if data is not already there.\n", + " train_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=True, transform=train_transforms, download=True\n", + " )\n", + "\n", + " dist.barrier(device_ids=[torch.cuda.current_device()]) # Barrier\n", + "\n", + " if (\n", + " dist.get_rank() != 0\n", + " ): # Other ranks must not download dataset at the same time in parallel.\n", + " train_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=True, transform=train_transforms\n", + " )\n", + "\n", + " valid_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root, train=True, transform=test_transforms\n", + " )\n", + "\n", + " ## PERFORM INDEX-BASED TRAIN-VALIDATION SPLIT OF ORIGINAL TRAINING DATA.\n", + " ## train_indices, valid_indices = ... # Extract train and validation indices using helper function from task 1.\n", + "\n", + " # Split into training and validation dataset according to specified validation fraction.\n", + " train_dataset = torch.utils.data.Subset(train_dataset, train_indices)\n", + " valid_dataset = torch.utils.data.Subset(valid_dataset, valid_indices)\n", + "\n", + " # Sampler that restricts data loading to a subset of the dataset.\n", + " # Especially useful in conjunction with DistributedDataParallel. \n", + " # Each process can pass a DistributedSampler instance as a DataLoader sampler, \n", + " # and load a subset of the original dataset that is exclusive to it.\n", + "\n", + " # Get samplers.\n", + " train_sampler = torch.utils.data.distributed.DistributedSampler(\n", + " train_dataset,\n", + " num_replicas=torch.distributed.get_world_size(),\n", + " rank=torch.distributed.get_rank(),\n", + " shuffle=True,\n", + " drop_last=True\n", + " )\n", + "\n", + " valid_sampler = torch.utils.data.distributed.DistributedSampler(\n", + " valid_dataset,\n", + " num_replicas=torch.distributed.get_world_size(),\n", + " rank=torch.distributed.get_rank(),\n", + " shuffle=True,\n", + " drop_last=True\n", + " )\n", + "\n", + " # Get dataloaders.\n", + " train_loader = torch.utils.data.DataLoader(\n", + " dataset=train_dataset,\n", + " batch_size=batch_size,\n", + " drop_last=True,\n", + " sampler=train_sampler\n", + " )\n", + "\n", + " valid_loader = torch.utils.data.DataLoader(\n", + " dataset=valid_dataset,\n", + " batch_size=batch_size,\n", + " drop_last=True,\n", + " sampler=valid_sampler\n", + " )\n", + "\n", + " return train_loader, valid_loader" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# EVALUATION\n", + "# Save to a separate Python module file `utils_eval.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "import os\n", + "import random\n", + "from typing import Tuple\n", + "\n", + "import numpy as np\n", + "import torch\n", + "\n", + "\n", + "def get_right_ddp(\n", + " model: torch.nn.Module, data_loader: torch.utils.data.DataLoader\n", + ") -> Tuple[torch.Tensor, torch.Tensor]:\n", + " \"\"\"\n", + " Compute the number of correctly predicted samples and the overall number of samples in a given dataset.\n", + "\n", + " This function is needed to compute the accuracy over multiple processors in a distributed data-parallel setting.\n", + "\n", + " Parameters\n", + " ----------\n", + " model : torch.nn.Module\n", + " The model.\n", + " data_loader : torch.utils.data.DataLoader\n", + " The dataloader.\n", + "\n", + " Returns\n", + " -------\n", + " torch.Tensor\n", + " The number of correctly predicted samples.\n", + " torch.Tensor\n", + " The overall number of samples in the dataset.\n", + " \"\"\"\n", + " with torch.no_grad():\n", + " correct_pred, num_examples = 0, 0\n", + "\n", + " for i, (features, targets) in enumerate(data_loader):\n", + " features = features.cuda()\n", + " targets = targets.float().cuda()\n", + " # CALCULATE PREDICTIONS OF CURRENT MODEL ON FEATURES OF INPUT DATA.\n", + " ## logits = ...\n", + " ## Determine class with highest score.\n", + " _, predicted_labels = torch.max(logits, 1) # Get class with highest score.\n", + " ## Update overall number of samples.\n", + " ## Compare predictions to actual labels to determine number of correctly predicted samples.\n", + "\n", + " correct_pred = torch.Tensor([correct_pred]).cuda()\n", + " num_examples = torch.Tensor([num_examples]).cuda()\n", + " return correct_pred, num_examples\n", + " \n", + "\n", + "def compute_accuracy_ddp(\n", + " model: torch.nn.Module, data_loader: torch.utils.data.DataLoader\n", + ") -> float:\n", + " \"\"\"\n", + " Compute the accuracy of the model's predictions on given labeled data.\n", + "\n", + " Parameters\n", + " ----------\n", + " model : torch.nn.Module\n", + " The model.\n", + " data_loader : torch.utils.data.DataLoader\n", + " The dataloader.\n", + "\n", + " Returns\n", + " -------\n", + " float\n", + " The model's accuracy on the given dataset in percent.\n", + " \"\"\"\n", + " correct_pred, num_examples = get_right_ddp(model, data_loader)\n", + " return correct_pred.item() / num_examples.item() * 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TRAINING\n", + "# Save to a separate Python module file `utils_train.py` to import the functions from \n", + "# into your main script and run the training as a batch job later on. \n", + "# Add imports as needed.\n", + "\n", + "import time\n", + "from typing import List, Tuple\n", + "\n", + "import torch\n", + "\n", + "from utils_eval import get_right_ddp\n", + "\n", + "\n", + "def train_model_ddp(\n", + " model: torch.nn.Module,\n", + " num_epochs: int,\n", + " train_loader: torch.utils.data.DataLoader,\n", + " valid_loader: torch.utils.data.DataLoader,\n", + " optimizer: torch.optim.Optimizer,\n", + ") -> Tuple[List[float], List[float], List[float]]:\n", + " \"\"\"\n", + " Train the model in distributed data-parallel fashion.\n", + "\n", + " Parameters\n", + " ----------\n", + " model : torch.nn.Module\n", + " The model to train.\n", + " num_epochs : int\n", + " The number of epochs to train.\n", + " train_loader : torch.utils.data.DataLoader\n", + " The training dataloader.\n", + " valid_loader : torch.utils.data.DataLoader\n", + " The validation dataloader.\n", + " optimizer : torch.optim.Optimizer\n", + " The optimizer to use.\n", + "\n", + " Returns\n", + " -------\n", + " List[float]\n", + " The epoch-wise loss history.\n", + " List[float]\n", + " The epoch-wise training accuracy history.\n", + " List[float]\n", + " The epoch-wise validation accuracy history.\n", + " \"\"\"\n", + " ## start = ... # Start timer to measure training time. \n", + " rank = torch.distributed.get_rank() # Get local process ID (= rank).\n", + " world_size = torch.distributed.get_world_size() # Get overall number of processes.\n", + "\n", + " loss_history, train_acc_history, valid_acc_history = (\n", + " [],\n", + " [],\n", + " [],\n", + " ) # Initialize history lists.\n", + " \n", + " # Actual training starts here.\n", + " for epoch in range(num_epochs): # Loop over epochs.\n", + " train_loader.sampler.set_epoch(epoch) # Set current epoch for distributed dataloader.\n", + " ## Set model to training mode.\n", + "\n", + " for batch_idx, (features, targets) in enumerate(\n", + " train_loader\n", + " ): # Loop over mini batches.\n", + " # Convert dataset to GPU device.\n", + " features = features.cuda()\n", + " targets = targets.cuda()\n", + "\n", + " # FORWARD & BACKWARD PASS\n", + " ## logits = ... # Get predictions of current model from forward pass.\n", + " ## loss = ... # Use cross-entropy loss.\n", + " ## Zero out gradients (by default, gradients are accumulated in buffers in backward pass).\n", + " ## Backward pass.\n", + " ## Update model parameters in single optimization step.\n", + " #\n", + " # LOGGING\n", + " ## Calculate effective mini-batch loss as process-averaged mini-mini-batch loss.\n", + " ## Sum up mini-mini-batch losses from all processes and divide by number of processes.\n", + " ## Use collective communication functions from `torch.distributed` package.\n", + " # Note that `torch.distributed` collective communication functions will only\n", + " # work with `torch` tensors, i.e., floats, ints, etc. must be converted before!\n", + " ## Append globally averaged loss of this epoch to history list.\n", + "\n", + " if rank == 0:\n", + " print(\n", + " f\"Epoch: {epoch+1:03d}/{num_epochs:03d} \"\n", + " f\"| Batch {batch_idx:04d}/{len(train_loader):04d} \"\n", + " f\"| Averaged Loss: {loss:.4f}\"\n", + " )\n", + "\n", + " # Validation starts here.\n", + " \n", + " ## Set model to evaluation mode.\n", + "\n", + " with torch.no_grad(): # Disable gradient calculation.\n", + " # Validate model in data-parallel fashion.\n", + " # Determine number of correctly classified samples and overall number \n", + " # of samples in training and validation dataset.\n", + " #\n", + " ## right_train, num_train = get_right_ddp(...)\n", + " ## right_valid, num_valid = get_right_ddp(...)\n", + " #\n", + " ## Sum up number of correctly classified samples in training dataset,\n", + " ## overall number of considered samples in training dataset, \n", + " ## number of correctly classified samples in validation dataset,\n", + " ## and overall number of samples in validation dataset over all processes.\n", + " ## Use collective communication functions from `torch.distributed` package.\n", + " #\n", + " # Note that `torch.distributed` collective communication functions will only\n", + " # work with torch tensors, i.e., floats, ints, etc. must be converted before!\n", + " # From these values, calculate overall training + validation accuracy.\n", + " #\n", + " ## train_acc = ...\n", + " ## valid_acc = ...\n", + " ## Append accuracy values to corresponding history lists.\n", + "\n", + " if rank == 0:\n", + " print(\n", + " f\"Epoch: {epoch+1:03d}/{num_epochs:03d} \"\n", + " f\"| Train: {train_acc :.2f}% \"\n", + " f\"| Validation: {valid_acc :.2f}%\"\n", + " )\n", + "\n", + " elapsed = (\n", + " time.perf_counter() - start\n", + " ) / 60 # Measure training time per epoch.\n", + " elapsed = torch.Tensor([elapsed]).cuda()\n", + " torch.distributed.all_reduce(elapsed)\n", + " elapsed /= world_size\n", + " if rank == 0:\n", + " print(f\"Time elapsed: {elapsed.item()} min\")\n", + "\n", + " ## elapsed = ... # Stop timer and calculate training time elapsed after epoch. \n", + " elapsed = torch.Tensor([elapsed]).cuda()\n", + " ## Calculate average training time elapsed after each epoch over all processes,\n", + " ## i.e., sum up times from all processes and divide by overall number of processes.\n", + " ## Use collective communication functions from torch.distributed package.\n", + " # Note that torch.distributed collective communication functions will only\n", + " # work with torch tensors, i.e., floats, ints, etc. must be converted before!\n", + " \n", + " if rank == 0:\n", + " ## Print process-averaged training time after each epoch. \n", + " torch.save(loss_history, f\"loss_{world_size}_gpu.pt\")\n", + " torch.save(train_acc_history, f\"train_acc_{world_size}_gpu.pt\")\n", + " torch.save(valid_acc_history, f\"valid_acc_{world_size}_gpu.pt\")\n", + "\n", + " return loss_history, train_acc_history, valid_acc_history" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stellen Sie das Main-Python-Skript aus Ihren Bausteinen zusammen\n", + "Nachdem Sie alle benötigten Funktionen und Klassen implementiert haben, können Sie das Main-Python-Skript zusammenstellen, das parallel auf dem Supercomputer ausgeführt werden soll. \n", + "Wie oben erklärt, müssen Sie zunächst die sogenannte Prozessgruppe aufsetzen. \n", + "Anschließend können Sie Ihre Daten so laden, dass jeder Prozess eine exklusive Teilmenge erhält, Ihr Modul instanziieren, es mit `DDP` wrappen und es auf datenparallele Weise auf den prozesslokalen Daten trainieren. \n", + "Da `DDP` die Modellzustände vom Prozess mit Rang 0 (oft Root genannt) an alle anderen Prozesse im `DDP`-Konstruktor weitergibt, brauchen Sie sich keine Sorgen darüber zu machen, dass verschiedene `DDP`-Prozesse mit unterschiedlichen Modellparameterwerten initialisiert werden. \n", + "`DDP` wrappt Details der verteilten Kommunikation und bietet eine saubere API, so als ob Sie ein lokales Modell trainieren würden. \n", + "Die Kommunikation zur Gradientensynchronisation findet während des Backward Passes statt und überschneidet sich mit der Berechnung. \n", + "Wenn `backward()` returnt, enthält `param.grad` bereits den synchronisierten Gradiententensor. \n", + "\n", + "Vervollständigen Sie den nachstehenden Code und speichern Sie ihn als separates Python-Skript `alex_parallel.py` im selben Ordner wie alle Ihre Hilfsmoduldateien. Diese Datei ist diejenige, die tatsächlich parallel auf dem *bwUniCluster* ausgeführt werden soll." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import torch\n", + "import torch.distributed as dist\n", + "from torch.nn.parallel import DistributedDataParallel as DDP\n", + "import torchvision\n", + "\n", + "from utils_data import get_dataloaders_cifar10_ddp\n", + "from utils_train import train_model_ddp\n", + "from utils_eval import compute_accuracy_ddp\n", + "from model import AlexNet\n", + "\n", + "\n", + "def main():\n", + " \"\"\"\n", + " Distributed data-parallel training of AlexNet on the CIFAR-10 dataset.\n", + " \"\"\"\n", + " ## world_size = int(os.getenv(\"...\") # Get overall number of processes from SLURM environment variable.\n", + " ## rank = int(os.getenv(\"...\") # Get individual process ID from SLURM environment variable.\n", + " print(\n", + " f\"Rank, world size, device count: {rank}, {world_size}, {torch.cuda.device_count()}\"\n", + " )\n", + "\n", + " if rank == 0:\n", + " ## Check if distributed package available.\n", + " ## Check if NCCL backend available. \n", + "\n", + " # On each host with N GPUs, spawn up N processes, while ensuring that\n", + " # each process individually works on a single GPU from 0 to N-1.\n", + "\n", + " address = os.getenv(\"SLURM_LAUNCH_NODE_IPADDR\")\n", + " port = \"29500\"\n", + " os.environ[\"MASTER_ADDR\"] = address\n", + " os.environ[\"MASTER_PORT\"] = port\n", + " \n", + " # Initialize DDP.\n", + " ## torch.distributed.init_process_group(...)\n", + " ## Check if process group has been initialized successfully.\n", + " ## Check used backend.\n", + " \n", + " b = 256 # Set batch size.\n", + " e = 100 # Set number of epochs to be trained.\n", + " data_root = \"/pfs/work7/workspace/scratch/ku4408-VL_ScalableAI/data\" # Path to data dir\n", + " \n", + " # Get transforms for data preprocessing to make smaller CIFAR-10 images work with AlexNet using helper function from task 1.\n", + " ## train_transforms, test_transforms = ...\n", + "\n", + " # Get distributed dataloaders for training and validation data on all ranks.\n", + " ## train_loader, valid_loader = get_dataloaders_cifar10_ddp(...)\n", + "\n", + " ## model = ... # Create AlexNet model with 10 classes for CIFAR-10 and move it to GPU.\n", + " ## ddp_model = ... # Wrap model with DDP.\n", + " \n", + " # Set up stochastic gradient descent optimizer from torch.optim package.\n", + " # Use a momentum of 0.9 and a learning rate of 0.1.\n", + " # Use parameters of DDP model here!\n", + " ## optimizer = ... \n", + " \n", + " # Train DDP model.\n", + " ## train_model_ddp(...)\n", + " \n", + " # Test final model on root.\n", + " if dist.get_rank() == 0:\n", + " test_dataset = torchvision.datasets.CIFAR10(\n", + " root=data_root,\n", + " train=False,\n", + " transform=test_transforms,\n", + " ) # Get dataset for test data.\n", + " test_loader = torch.utils.data.DataLoader(\n", + " dataset=test_dataset, batch_size=b, shuffle=False\n", + " ) # Get dataloader for test data.\n", + " ## test_acc = compute_accuracy_ddp(...) # Compute accuracy on test data.\n", + " ## Print test accuracy. \n", + " \n", + " ## Destroy process group. \n", + "\n", + " \n", + "# MAIN STARTS HERE. \n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Führen Sie Ihren Code parallel als Batch-Job auf dem *bwUniCluster* aus\n", + "Nun können Sie Ihr Skript auf dem *bwUniCluster* ausführen. \n", + "Untenstehend finden Sie ein Job-Skript, das vier GPUs auf einem Knoten anfordert. \n", + "Verwenden Sie den Befehl `srun` ([doc](https://slurm.schedmd.com/srun.html)), um Ihr `Python`-Skript parallel auf den angeforderten vier GPUs auszuführen.\n", + "Passen Sie den Code an und speichern Sie ihn als separates Bash-Skript `submit_4.sh`. \n", + "Um Ihr Skript auf *bwUniCluster* auszuführen, submitten Sie es an den SLURM-Workload-Manager: `sbatch submit_4.sh`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#!/bin/bash\n", + "\n", + "#SBATCH --job-name=alex4\n", + "#SBATCH --partition=gpu_4\n", + "#SBATCH --gres=gpu:4 # number of requested GPUs (GPU nodes shared btwn multiple jobs)\n", + "#SBATCH --ntasks=4\n", + "#SBATCH --time=30:00 # wall-clock time limit\n", + "#SBATCH --mem=128000\n", + "#SBATCH --nodes=1\n", + "#SBATCH --mail-type=ALL\n", + "\n", + "export VENVDIR=<path/to/your/venv> # Export path to your virtual environment.\n", + "export PYDIR=<path/to/your/python/scripts> # Export path to directory containing Python script.\n", + "\n", + "# Set up modules.\n", + "module purge # Unload all currently loaded modules.\n", + "module load compiler/gnu/13.3 # Load required modules.\n", + "module load mpi/openmpi/4.1\n", + "module load devel/cuda/12.4\n", + "\n", + "source ${VENVDIR}/bin/activate\n", + "unset SLURM_NTASKS_PER_TRES\n", + "\n", + "RESDIR=./job_${SLURM_JOB_ID}/\n", + "mkdir ${RESDIR}\n", + "cd ${RESDIR}\n", + "\n", + "srun python -u ${PYDIR}/alex_parallel.py # Execute your main script in parallel using `srun`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Congratulations! \n", + "Sie haben erfolgreich ein verteiltes datenparalleles tiefes neuronales Netz in `PyTorch` trainiert. Um Ihre Ergebnisse visuell zu analysieren, können Sie die Entwicklung des Losses sowie der Accuracy auf Trainings- und Validierungsdatensatz über das Training darstellen, z.B. mit `matplotlib.pyplot`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + }, + "toc": { + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}