Entwicklung & Code
Künstliche Neuronale Netze im Überblick 4: Verlustfunktionen
Neuronale Netze sind der Motor vieler Anwendungen in KI und GenAI. Diese Artikelserie gibt einen Einblick in die einzelnen Elemente. Der vierte Teil der Serie stellt die gängigsten Verlustfunktionen vor, leitet ihre Gradienten ab und zeigt dann, wie man die grundlegende Gradientenabstiegs-Aktualisierungsregel und ihre komplexeren Varianten entwickelt.
Prof. Dr. Michael Stal arbeitet seit 1991 bei Siemens Technology. Seine Forschungsschwerpunkte umfassen Softwarearchitekturen für große komplexe Systeme (Verteilte Systeme, Cloud Computing, IIoT), Eingebettte Systeme und Künstliche Intelligenz.
Er berät Geschäftsbereiche in Softwarearchitekturfragen und ist für die Architekturausbildung der Senior-Software-Architekten bei Siemens verantwortlich.
Um einem neuronalen Netzwerk nützliche Aufgaben beizubringen, müssen wir quantifizieren, wie gut seine Vorhersagen mit den Zielwerten übereinstimmen. Eine Verlustfunktion ist ein skalares Maß für den Fehler, den der Trainingsprozess durch Anpassung der Netzwerkparameter minimieren soll.
Mittlerer quadratischer Fehler für Regression
Wenn die Aufgabe des Netzwerks darin besteht, kontinuierliche Größen vorherzusagen, ist der mittlere quadratische Fehler eine naheliegende Wahl. Angenommen, wir haben einen Datensatz mit N Beispielen, wobei jedes Beispiel i einen Zielwert yᵢ hat und das Netzwerk eine Vorhersage ŷᵢ liefert. Der mittlere quadratische Fehler L ist definiert durch
L(ŷ, y) = (1/N) Σᵢ (ŷᵢ − yᵢ)²
Dieser Ausdruck summiert die quadrierten Differenzen zwischen Vorhersage und Zielwert über alle Beispiele und dividiert sie durch N, um einen Durchschnitt zu erhalten. Durch die Quadrierung des Fehlers werden große Abweichungen stärker bestraft als kleine, und durch den Durchschnitt wird der Verlust unabhängig von der Größe des Datensatzes.
Um zu sehen, wie dieser Verlust die Parameteraktualisierungen beeinflusst, berechnen wir seine Ableitung in Bezug auf eine einzelne Vorhersage ŷⱼ:
∂L/∂ŷⱼ = (2/N) (ŷⱼ − yⱼ)
Während der Rückpropagation wird dieser Gradient durch das Netzwerk weitergeleitet und steuert jede Gewichtsaktualisierung in Richtung einer Verringerung des quadratischen Fehlers.
In PyTorch schreibt man:
import torch
import torch.nn as nn
# Angenommen, `model` ordnet Eingaben den Vorhersagen ŷ der Form (batch_size, 1) zu
loss_fn = nn.MSELoss() # erstellt ein Modul für den mittleren quadratischen Fehler
predictions = model(inputs) # Vorwärtsdurchlauf erzeugt ŷ
loss = loss_fn(predictions, targets)
Hier kapselt nn.MSELoss()
die obige Formel. Wenn wir später loss.backward()
aufrufen, berechnet PyTorch ∂L/∂parameters automatisch, indem es die partiellen Ableitungen durch den Berechnungsgraphen verkettet.
Kreuzentropie für die Klassifizierung
Wenn die Aufgabe die Klassifizierung in eine von C Klassen ist, gibt das Netzwerk typischerweise einen Vektor von Logits z ∈ ℝᶜ für jedes Beispiel aus. Um diese Logits in eine Wahrscheinlichkeitsverteilung p umzuwandeln, wenden wir die Softmax-Funktion an:
softmax(z)ᵢ = exp(zᵢ) / Σⱼ exp(zⱼ)
Wenn die wahre Klassenbezeichnung für Beispiel i als One-Hot-Vektor y codiert ist, wobei yᵢ = 1 und yⱼ = 0 für j ≠ i, dann ist der Kreuzentropieverlust
L(z, y) = − Σᵢ yᵢ · log( softmax(z)ᵢ )
Da nur ein Eintrag von y ungleich Null ist, vereinfacht sich dies zu der negativen Log-Wahrscheinlichkeit, die der richtigen Klasse zugewiesen wird. Bei Verwendung von PyTorch’s nn.CrossEntropyLoss
fusioniert die Implementierung die Softmax- und Log-Schritte auf numerisch stabile Weise und erwartet rohe Logits und ganzzahlige Klassenindizes:
import torch.nn as nn
loss_fn = nn.CrossEntropyLoss() # erstellt einen kombinierten Log-Softmax + NLL-Verlust
logits = model(inputs) # Form (batch_size, num_classes)
loss = loss_fn(logits, class_indices)
Im Hintergrund ist der Gradient von L in Bezug auf jedes Logit zₖ
∂L/∂zₖ = softmax(z)ₖ − yₖ,
was genau der Differenz zwischen der vorhergesagten Wahrscheinlichkeit und der tatsächlichen Beschriftung für jede Klasse entspricht.
Grundlegender Gradientenabstieg
Sobald ein Verlust festgelegt wurde, ist die einfachste Regel zur Parameteraktualisierung der Gradientenabstieg. Bezeichnen wir mit θ einen einzelnen Skalarparameter (einen Eintrag einer Gewichtungsmatrix oder eines Bias-Vektors) und mit L(θ) den Verlust als Funktion von θ. Die Gradientenabstiegsregel aktualisiert θ in Richtung des steilsten Abstiegs:
θ ← θ − η · ∂L/∂θ
Hier ist η > 0 die Lernrate, ein Hyperparameter, der die Schrittgröße steuert. Ein kleiner η-Wert führt zu einer langsamen, aber stabilen Konvergenz, während ein großer η-Wert dazu führen kann, dass der Verlust schwankt oder divergiert.
In der Praxis unterscheidet man drei Varianten: Wenn der Gradient vor jeder Aktualisierung über den gesamten Datensatz berechnet wird, spricht man von Batch-Gradientenabstieg; wenn für jede Aktualisierung nur ein einziges Beispiel verwendet wird, spricht man von stochastischem Gradientenabstieg; und wenn kleine Teilmengen von Beispielen (Mini-Batches) verwendet werden, spricht man von Mini-Batch-Gradientenabstieg. Mini-Batch-Aktualisierungen stellen einen Kompromiss zwischen verrauschten, aber schnellen stochastischen Aktualisierungen und stabilen, aber kostspieligen Batch-Aktualisierungen dar.
PyTorch bietet über das Paket torch.optim
eine einfache Möglichkeit, Gradientenabstieg mit Mini-Batches durchzuführen. Um beispielsweise den einfachen stochastischen Gradientenabstieg mit Momentum zu verwenden, schreibt man:
import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# für Eingaben, Ziele in data_loader: data_loader liefert Mini-Batches
optimizer.zero_grad() # alle akkumulierten Gradienten löschen
outputs = model(inputs) # Vorwärtsdurchlauf
loss = loss_fn(outputs, targets) # Verlust berechnen
loss.backward() # Rückwärtspropagierung zur Berechnung der Gradienten
optimizer.step() # Parameter an Ort und Stelle aktualisieren
Der Aufruf von optimizer.zero_grad()
löscht die Gradientenpuffer aller Parameter, sodass sich Gradienten aus früheren Iterationen nicht ansammeln. Der Aufruf von loss.backward()
füllt das Attribut .grad
jedes Parameters mit dem berechneten ∂L/∂θ, und optimizer.step()
verwendet diese Gradienten, um die Parameter gemäß der gewählten Aktualisierungsregel zu aktualisieren.
Fortgeschrittene Optimierer: RMSProp und Adam
Während einfaches Momentum die Konvergenz in Tälern der Verlustfunktion beschleunigen kann, passen adaptive Methoden die Lernrate jedes Parameters individuell auf der Grundlage der Historie seiner Gradienten an. RMSProp behält einen exponentiell gewichteten gleitenden Durchschnitt der vergangenen quadrierten Gradienten bei:
sₜ = γ·sₜ₋₁ + (1−γ)·gₜ²
θ ← θ − (η / sqrt(sₜ + ε)) · gₜ
wobei gₜ = ∂L/∂θ zum Zeitpunkt t, γ typischerweise um 0,9 liegt und ε eine kleine Konstante für die numerische Stabilität ist. In PyTorch wird dies wie folgt konstruiert:
optimizer = optim.RMSprop(model.parameters(),
lr=0.001,
alpha=0.99,
eps=1e-8)
Adam kombiniert Momentum und RMSProp, indem es sowohl einen gleitenden Durchschnitt der Gradienten mₜ als auch der quadrierten Gradienten vₜ beibehält und eine Bias-Korrektur anwendet:
mₜ = β₁·mₜ₋₁ + (1−β₁)·gₜ
vₜ = β₂·vₜ₋₁ + (1−β₂)·gₜ²
m̂ₜ = mₜ / (1−β₁ᵗ)
v̂ₜ = vₜ / (1−β₂ᵗ)
θ ← θ − η · ( m̂ₜ / ( sqrt(v̂ₜ) + ε ) )
mit den Standardwerten β₁=0,9, β₂=0,999 und ε=1e-8.
Im Code:
optimizer = optim.Adam(model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
eps=1e-8)
Jeder dieser Optimierer erfordert die Abstimmung seiner Hyperparameter – Lernrate, Abklingraten und Epsilon –, um die beste Leistung für ein bestimmtes Problem zu erzielen.
Der nächste Teil der Serie zeigt, wie diese Komponenten zu einer vollständigen Trainingsschleife zusammengesetzt werden. Anschließend untersucht er die Unterschiede zwischen dem Training mit und ohne explizite Minibatches und stellt Techniken wie Dropout und Gewichtsabnahme zur Verbesserung der Generalisierung vor.
(rme)