Entwicklung & Code
Künstliche Neuronale Netze im Überblick 3: Aktivierungsfunktionen
Neuronale Netze sind der Motor vieler Anwendungen in KI und GenAI. Diese Artikelserie gibt einen Einblick in die einzelnen Elemente.
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.
Der dritte Teil zeigt, wie Operationen den Vorwärtsdurchlauf in seiner Allgemeinheit bilden und wie die Wahl der Aktivierungen mit der Netzwerktiefe zusammenwirkt.
Vorwärtsausbreitung, auch Vorwärtsdurchlauf genannt, bezeichnet den Prozess der Berechnung der Ausgabe eines neuronalen Netzwerks durch sukzessive Anwendung linearer Transformationen und nicht linearer Aktivierungsfunktionen auf die Eingabedaten. Für eine einzelne Schicht ℓ gilt, wenn wir die Aktivierungen der vorherigen Schicht mit a^(ℓ–1), die Matrix der Gewichte der Schicht mit W^(ℓ) und den Bias-Vektor mit b^(ℓ) bezeichnen, dann berechnet die Schicht den Präaktivierungsvektor z^(ℓ) als
z^(ℓ) = W^(ℓ) · a^(ℓ–1) + b^(ℓ)
Nach der Berechnung von z^(ℓ) wendet die Schicht eine elementweise nicht lineare Aktivierungsfunktion σ an, um die Ausgangsaktivierungen zu erzeugen:
a^(ℓ) = σ(z^(ℓ)).
Durch Zusammensetzen dieser Operationen für jede Schicht vom Eingang bis zum Ausgang wandelt das Netzwerk einen anfänglichen Eingabevektor in einen endgültigen Ausgabevektor um, der eine Vorhersage oder eine Merkmals-Einbettung darstellt.
Um diese Berechnungen konkret zu veranschaulichen, betrachten wir eine Gruppe von Eingabevektoren, die wir als Matrix X
mit der Form (batch_size, input_dim)
darstellen. Eine vollständig verbundene Schicht in PyTorch lässt sich manuell wie folgt implementieren, wobei W
die Form (output_dim, input_dim)
und b
die Form (output_dim)
hat:
import torch
batch_size = 32
input_dim = 10
output_dim = 50
# Simulieren Sie einen Stapel von Eingaben
X = torch.randn(batch_size, input_dim)
# Initialisieren Sie die Gewichtungsmatrix und den Bias-Vektor
W = torch.randn(output_dim, input_dim)
b = torch.randn(output_dim)
# Berechnen Sie die Voraktivierung für den Stapel: Z = X @ W^T + b
Z = X.matmul(W.t()) + b
# Wende eine Nichtlinearität (z. B. ReLU) an, um Aktivierungen A zu erhalten
A = torch.relu(Z)
In diesem Ausschnitt enthält der Tensor X
zu Demonstrationszwecken zufällige Werte. Der Ausdruck W.t()
bezeichnet die Transponierung von W
, die die Form (input_dim, output_dim)
hat. Die Batch-Matrixmultiplikation X.matmul(W.t())
erzeugt einen neuen Tensor Z
mit der Form (batch_size, output_dim)
, da jede der batch_size
-Zeilen von X mit Wᵀ multipliziert wird. Durch Hinzufügen von b
zu Z
übertragen wir den Bias-Vektor auf alle Zeilen, wodurch wir die vollständigen Voraktivierungswerte erhalten. Zuletzt führt torch.relu
eine elementweise rektifizierte lineare Einheit auf Z
aus, um A
zu erzeugen.
Aktivierungsfunktionen führen die Nichtlinearität ein, die es neuronalen Netzen ermöglicht, komplexe Muster zu lernen. Die logistische Sigmoid-Funktion, definiert durch
σ(z) = 1 / (1 + exp(−z)),
bildet jede reelle Eingabe in den Bereich zwischen null und eins ab. Ihre Ableitung kann in Bezug auf ihre Ausgabe ausgedrückt werden als
σ′(z) = σ(z) · (1 − σ(z)).
Obwohl Sigmoid in der Vergangenheit sehr beliebt war, können seine Gradienten sehr klein werden, wenn |z| groß ist, was zu einem langsamen Lernen in der Tiefe eines Netzwerks führt.
In PyTorch können Sie eine Sigmoid-Aktivierung für einen Tensor Z mit folgendem Befehl berechnen:
A_sigmoid = torch.sigmoid(Z)
wobei torch.sigmoid
die elementweise logistische Funktion auf jeden Eintrag von Z anwendet und einen neuen Tensor mit Werten zwischen null und eins erzeugt.
Die hyperbolische Tangensfunktion, definiert durch
tanh(z) = (exp(z) − exp(−z)) / (exp(z) + exp(−z)),
bildet reelle Eingaben auf den Bereich zwischen minus eins und eins ab. Ihre Ableitung ist gegeben durch 1 − tanh(z)^2. Da tanh nullzentriert ist, führt sie oft zu einer schnelleren Konvergenz als Sigmoid, leidet jedoch bei großen positiven oder negativen Eingaben unter verschwindenden Gradienten.
In PyTorch können Sie eine tanh-Aktivierung mit folgendem Befehl berechnen:
A_tanh = torch.tanh(Z)
Die in modernen tiefen Netzwerken am häufigsten verwendete Aktivierung ist die rektifizierte lineare Einheit (ReLU), definiert als
ReLU(z) = max(0, z)
Ihre Ableitung ist immer null, wenn z negativ ist, und immer eins, wenn z positiv ist. Diese einfache, stückweise lineare Form vermeidet eine Sättigung bei positiven Eingaben, was das Problem der verschwindenden Gradienten erheblich mildert. Allerdings können Neuronen während des Trainings „sterben“, wenn ihre Eingaben negativ werden und negativ bleiben, was zu dauerhaft null-Gradienten führt.
Sie können eine ReLU-Aktivierung in PyTorch entweder mit torch.relu
oder durch Erstellen einer Instanz des Moduls
anwenden:
A_relu = torch.relu(Z)
import torch.nn as nn
relu_layer = nn.ReLU()
A_relu_mod = relu_layer(Z)
Um das Problem des „sterbenden ReLU“ zu beheben, führt Leaky ReLU eine kleine Steigung α für negative Eingaben ein, definiert als
LeakyReLU(z) = max(α·z, z).
Ihre Ableitung ist α, wenn z negativ ist, und eins, wenn z positiv ist. Eine typische Wahl für α ist 0,01. Im Code können Sie eine leaky ReLU-Schicht in PyTorch mit folgendem Code erstellen und anwenden:
leaky_relu = nn.LeakyReLU(negative_slope=0.01)
A_leaky = leaky_relu(Z)
Bei der Klassifizierung über mehrere Klassen wird die Softmax-Funktion verwendet, um einen Vektor mit beliebigen reellen Werten in eine Wahrscheinlichkeitsverteilung umzuwandeln. Für einen Vektor z mit Komponenten z_i gilt
softmax(z)_i = exp(z_i) / Σ_j exp(z_j).
Die Jacobi-Matrix von Softmax hat Einträge ∂σ_i/∂z_j = σ_i (δ_{ij} − σ_j). In der Praxis wird Softmax typischerweise mit dem Kreuzentropieverlust kombiniert, was zu einem numerisch stabilen und einfacheren kombinierten Gradienten führt. In PyTorch lässt sich Softmax entlang einer bestimmten Dimension anwenden. Für einen Batch von Logits mit dem Namen logits und der Form (batch_size, num_classes)
können Sie beispielsweise schreiben:
import torch.nn.functional as F
probabilities = F.softmax(logits, dim=1)
Das berechnet die Exponentialfunktionen jeder Zeile in logits, normalisiert sie anhand der Zeilensummen und gibt einen Tensor derselben Form zurück, der Werte zwischen null und eins enthält, die entlang jeder Zeile eins ergeben.
Über diese klassischen Aktivierungen hinaus haben neuere Funktionen wie Swish, definiert als z · sigmoid(z), und GELU, die Gaußsche Fehlerlineareinheit, für bestimmte Architekturen an Beliebtheit gewonnen, da sie glattere Gradienten und eine verbesserte Leistung bei Aufgaben wie der Sprachmodellierung bieten. Obwohl diese Funktionen in Bibliotheken wie PyTorch (z. B. über das Modul nn.GELU
) verfügbar sind, bleiben ReLU und seine Varianten aufgrund ihres zusätzlichen Rechenaufwands für viele Praktiker die Standardwahl.
Nachdem wir nun sowohl die linearen Transformationen, aus denen die Voraktivierungen jeder Schicht bestehen, als auch die darauffolgenden nicht linearen Aktivierungsfunktionen behandelt haben, sind wir in der Lage, tiefe neuronale Netze zu bauen, die Eingabedaten in reichhaltige interne Darstellungen umwandeln.
Der nächste Teil der Serie zeigt, wie gut die Vorhersagen eines Netzwerks mit den gewünschten Zielen übereinstimmen, indem man Verlustfunktionen einführt. Anschließend zeigt der Beitrag gradientenbasierten Optimierungsalgorithmen, mit denen sich die Parameter des Netzwerks anpassen lassen.
(rme)