April 14, 2026

Neuronale Netzwerke auf GPU trainieren

Beim Trainieren von neuronalen Netzwerken haben GPUs gegenüber klassischen CPUs einen enormen Geschwindigkeitsvorteil. Mit entsprechender Grafikkarte ist es mittlerweile auch sehr einfach, solche Trainings lokal auszuführen. Dieser Beitrag zeigt das vorgehen mit NVIDIA Grafikkarte und PyTorch.

1. Voraussetzungen prüfen (NVIDIA Setup)

Damit PyTorch deine Grafikkarte erkennt, müssen der Treiber und das Toolkit passen.

  1. NVIDIA Treiber: Installiere den aktuellsten Treiber von der NVIDIA-Website.
  2. Check: Öffne dein Terminal oder PowerShell und tippe nvidia-smi Hier sollte oben rechts die unterstützte CUDA-Version stehen (z.B. CUDA Version: 12.x).

PyTorch liefert die benötigten CUDA-Bibliotheken meist direkt im Paket mit, du musst also nicht zwingend das komplette CUDA-Toolkit separat installieren, solange dein Treiber aktuell ist. Auf https://pytorch.org/get-started/ kann man sich den Installationsbefehl für sein jeweiliges Setup direkt anzeigen lassen.

pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cu128

Oder mit uv:

uv add torch torchvision --index-url https://download.pytorch.org/whl/cu128    

Ob die Installation geklappt hat kann man direkt im Python Code prüfen:

import torch

# Test, ob CUDA verfügbar ist
if torch.cuda.is_available():
    device = torch.device("cuda")
    print(f"✅ GPU gefunden: {torch.cuda.get_device_name(0)}")
    print(f"Speicher: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")
else:
    device = torch.device("cpu")
    print("❌ Keine GPU gefunden, nutze CPU.")

Beispielprojekt

Für das Beispielprojekt benötigt man zunächst ein paar Imports. Zudem ist es sinnvoll auch am Anfang des Codes nochmal zu prüfen, ob die GPU richtig initialisiert ist.

Projekt einrichten

Als Hyperparameter wählen wir eine Batch_Size von 64, learning rate von 0.001 und trainieren in 10 Epochen.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
from torch.amp import autocast, GradScaler # Für RTX 4060 Optimierung

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if device.type == 'cuda':
    print(f"🚀 Training auf: {torch.cuda.get_device_name(0)}")
    # Optimierung für Ampere/Ada-Lovelace Architekturen, abhängig von der GPU
    torch.backends.cudnn.benchmark = True 

BATCH_SIZE = 64  # abhängig von der GPU, hier eine 4060 (8GB VRAM), da passt 64 ganz gut
LEARNING_RATE = 0.001
EPOCHS = 10

Daten laden

Wir verwenden den Standarddatensatz „Flowers102“ aus der PyTorch Bibiothek. Bei pytorch kann man bereits beim laden verschiedene Datentransformationen durchführen. Dafür muss man zunächst einen transformer initialisieren. Dieser durchläuft folgende Schritte:

TransformationErklärungAnmerkung
Resize((224, 224))Das Bild wird auf eine feste quadratische Größe skaliert.Vortrainierte neuronale Netze (wie ResNet oder VGG) erwarten eine fixe Eingabegröße.
RandomHorizontalFlipDas Bild wird mit einer 50% Wahrscheinlichkeit horizontal gespiegelt.Data Augmentation: Dein Modell lernt, dass eine Blume immer noch eine Blume ist, egal ob sie nach links oder rechts geneigt ist. Das beugt Overfitting vor.
RandomRotation(15)Rotiert das Bild zufällig um bis zu 15 Grad.Data Augmentation: Simuliert verschiedene Kameraperspektiven und macht das Modell ebenfalls robuster gegen Overfitting.
ToTensor()Skaliert die Pixelwerte von [0,255] auf [0.0,1.0] und ändert das Layout von (H, W, C) zu (C, H, W).PyTorch rechnet intern ausschließlich mit Floating-Point-Tensoren in diesem Format.
Normalize(...)Z-Transformation in Standardnormalverteilung für jeden Kanal (R,G,B), subtrahiert den Mittelwert und teilt durch die Standardabweichung.Beschleunigt den Gradientenabstieg. Die konkreten Werte stammen aus dem ImageNet-Datensatz auf dem die meisten vortrainierten Modelle trainiert werden.
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(15),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# Datensatz laden
train_dataset = datasets.Flowers102(root='./data', split='train', download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4, pin_memory=True)

Das Ergebnis er Transformation kann man sich so anzeigen lassen:

def imshow(inp, title=None):
    """Imshow for Tensor."""
    # Rückgängig machen der Normalisierung für die Anzeige
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1) # Pixelwerte auf [0, 1] begrenzen
    
    plt.figure(figsize=(15, 5))
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.axis('off')
    plt.show()

# Ein Batch Bilder holen
inputs, classes = next(iter(train_loader))

# Ein Gitter aus den Bildern erstellen (z.B. die ersten 4)
out = make_grid(inputs[:4])

imshow(out, title=[f"Klasse: {c.item()}" for c in classes[:4]])

Model und Optimizer definieren

Wir verwenden in diesem Beispiel das vortrainierte Modell Restnet18 inklusive Gewichte. ResNet18 wurde ursprünglich für ImageNet trainiert, das 1000 Klassen hat. Der Flowers Datensatz hat nur 102 Klassen. Daher ersetzen wir den letzten Layermodel.fc und ersetzen ihn durch einen neuen Layer, der genau 102 Ausgänge hat. Die Eingangsfeatures aus dem vorherigen Layer lesen wir mit model.fc.in_features aus und legen sie als Eingangsfeatures für diese Layer fest.

Mit dem Befehl model.to(device) schieben wir das gesamte Modell auf die GPU.

model = models.resnet18(weights='IMAGENET1K_V1')
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 102) 
model = model.to(device)

Die Abweichung der Vorhersage von den Trainingsdaten (Loss) messen wir mit CrossEntropy. Als Optimizer zur Anpassung der Gewichte verwenden wir Adam, ein Standardverfahren das meist schneller konvergiert als klassisches Gradient Descent.

GradScaler dient zur Optimierung der Geschwindigkeit. Statt mit 32-Bit-Fließkommazahlen ermöglicht Automatic Mixed Precision (AMP) das ein Teil des Netzes mit 16-Bit berechnet erden. Das verdoppelt die Geschwindigkeit und halbiert den VRAM-Verbrauch. Der Scaler rechnet dabei sehr kleine Zahlen hoch, um beim Backpropagation keine Informationen zu verlieren, und rechnet sie später wieder zurück.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

# GradScaler für Mixed Precision (FP16) Initialisierung (einmalig vor der Loop)
scaler = torch.amp.GradScaler()

Modell trainieren

In jeder Epoche des trainings durchläuft das Modell folgende Schritte.

Zunächst werden die Daten geladen und mit image.to(device, non_blocking=True) auf die GPU geschoben. Der Zusatz non_blocking=True sorgt dafür, dass der Datentransfer zwischen CPU und GPU überlappend stattfinden kann. Das spart wertvolle Millisekunden pro Batch.

optimizer.zero_grad() löscht die alten Gradienten aus dem vorherigen Trainingsschritt.

with autocast(device_type='cuda'): Innerhalb dieses Blocks entscheidet PyTorch selbstständig, welche Rechenoperationen mit 16-Bit (FP16) durchgeführt werden können, ohne die Genauigkeit zu gefährden. Das spart massiv Speicherbandbreite und Rechenzeit.

Die folgenden Schritte für den Scaler sorgen dafür, dass sehr kleine Gradienten bei 16-bit nicht zu Null abgerundet werden. Stattdessen wird der Loss skaliert und später mit step(optimizer) wieder zurückgerechnet. update passt den Skalierungsfaktor für den nächsten Batch an (falls Gradienten zu groß wurden oder Inf/NaN-Werte auftraten).

for epoch in range(EPOCHS):
    model.train()
    running_loss = 0.0
    
    for images, labels in train_loader:
        # Daten auf GPU schieben
        images, labels = images.to(device, non_blocking=True), labels.to(device, non_blocking=True)
        
        optimizer.zero_grad()

        # Autocast für Mixed Precision
        with torch.amp.autocast(device_type=device.type if device.type != 'cpu' else 'cuda'):
            outputs = model(images)
            loss = criterion(outputs, labels)

        # Skalierte Backpropagation
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

        running_loss += loss.item()
    
    avg_loss = running_loss / len(train_loader)
    print(f"Epoch [{epoch+1}/{EPOCHS}] - Loss: {avg_loss:.4f}")

torch.save(model.state_dict(), "flower_resnet18.pth")

Learning Rate Scheduler

Weiterführend kann man noch einen LR Scheduler einbauen. Ein Scheduler reduziert die Lernrate während des Trainings automatisch. D.h. zu Beginn des Trainings werden große Schritte gemacht, gegen Ende kleine Schritte, um nicht über das Ziel hinauszuschießen. In modernen Deep-Learning-Pipelines (wie bei Fast.ai oder modernen PyTorch-Projekten) nutzt man oft den OneCycleLR-Scheduler. Er steigert die Lernrate erst leicht und senkt sie dann sehr fein ab.

# --- Vor der Epochen-Schleife ---
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

# Der Scheduler braucht die Info, wie viele Schritte insgesamt gemacht werden
total_steps = len(train_loader) * EPOCHS
scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=LEARNING_RATE, total_steps=total_steps)

for epoch in range(EPOCHS):
    model.train()
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()

        with torch.amp.autocast(device_type='cuda'):
            outputs = model(images)
            loss = criterion(outputs, labels)

        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()
        
        # WICHTIG: Der Scheduler wird bei OneCycleLR nach JEDEM Batch aufgerufen
        scheduler.step()

Modell testen

Fürs Testing kann man dieses Skript verwenden. Es funktioniert praktisch ebenso wie das Trainingsskript mit folgenden Änderungen:

  • Bei den Transformationen wird auf die Augmentation mit RandomHorizontalFlip und RandomRotation verzichtet. Es geht schließlich nur noch um die Vorhersage.
  • Das Model wird natürlich nicht trainiert, sondern geladen.
  • Mit model.eval() wird das Modell in den Evaluationsmodus versetzt (Dropouts, das zufällige ausschalten von Neuronen und Batchnormalisierung, das Statistiken pro Batch berechnet) werden in den Testmodus geschaltet.
  • Der Evaluationsloop wird dann schließlich ohne Gradienten gestartet.
  • Mit torch.max(outputs.data,1) erhalten wird aus allen Modelloutputs den maximalen Weg. Der erste Rückgabewert (den maximalen Wert selbst) wird ignoriert, gespeichert wird nur der Index (die Klasse).
import torch
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader

def test():
    # 1. Setup & Device
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Teste auf: {device}")

    # 2. Gleiche Transformationen wie beim Training (aber ohne Augmentation!)
    test_transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    # 3. Test-Datensatz laden (split='test')
    test_dataset = datasets.Flowers102(root='./data', split='test', download=True, transform=test_transform)
    test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

    # 4. Modell-Architektur definieren & Gewichte laden
    model = models.resnet18() 
    num_ftrs = model.fc.in_features
    model.fc = torch.nn.Linear(num_ftrs, 102) # Muss exakt wie im Training sein
    
    # Hier laden wir deine trainierten Parameter
    model.load_state_dict(torch.load("flower_resnet18.pth", map_location=device))
    model = model.to(device)
    model.eval() # WICHTIG: Setzt Dropout und Batch Normalization in Test-Modus

    # 5. Evaluation Loop
    correct = 0
    total = 0
    
    print("Starte Evaluation...")
    with torch.no_grad(): # Gradienten-Berechnung deaktivieren
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1) # Klasse mit höchstem Score
            
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    accuracy = 100 * correct / total
    print(f"\nTest-Ergebnis:")
    print(f"Genauigkeit auf {total} Testbildern: {accuracy:.2f}%")

if __name__ == '__main__':
    test()

Confusion Matrix

Eine Confusion-Matrix zeigt alle vorhergesagten Klassen vs. die echten Klassen. Da es sich um 102 Klassen handelt, werden in diesem Fall keine Zahlen angezeigt, sondern nur die Farben dargestellt. Wir nutzen logarithmische Normalisierung, damit auch kleinere Abweichungen besser erkennbar sind.

# Confusion Matrix anzeigen
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
import numpy as np
from matplotlib.colors import LogNorm

def plot_confusion_matrix(model, loader, device):
    all_preds = []
    all_labels = []
    
    model.eval()
    with torch.no_grad():
        for images, labels in loader:
            images = images.to(device)
            outputs = model(images)
            _, preds = torch.max(outputs, 1)
            
            all_preds.extend(preds.cpu().numpy())
            all_labels.extend(labels.cpu().numpy())

    # Matrix berechnen
    cm = confusion_matrix(all_labels, all_preds)
    
    # Plotten mit Log-Normalisierung
    plt.figure(figsize=(20, 15))
    sns.heatmap(cm, 
                annot=False, 
                cmap='Blues', 
                norm=LogNorm(vmin=1, vmax=cm.max()), # Vmin=1 vermeidet Weiß-Rauschen bei 0
                cbar_kws={'label': 'Anzahl Vorhersagen (log-skaliert)'})
    
    plt.xlabel('Vorhergesagte Klasse')
    plt.ylabel('Echte Klasse')
    plt.title('Confusion Matrix: Flower102 (Log-skaliert)')
    plt.show()

# Aufruf nach der Evaluation
plot_confusion_matrix(model, test_loader, device)

Weiterführende Links

Grundlegendes zu Neuronalen Netzen wir hier erklärt.

Das offizielle Tutorial zu PyTorch findet man hier: https://docs.pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert