Künstliche Intelligenz
Künstliche Neuronale Netze im Überblick 2: Schichten und Feed-Forward-Netzwerke
Neuronale Netze sind der Motor vieler Anwendungen in Künstlicher Intelligenz (KI) und GenAI. Diese Artikelserie gibt einen Einblick in die einzelnen Elemente. Nach der Vorstellung der Neuronen im ersten Teil widmet sich der zweite Teil den Schichten des Netzwerks.
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.
Wenn mehrere künstliche Neuronen so gruppiert sind, dass sie alle denselben Satz von Eingaben erhalten und ihre Ausgaben parallel erzeugen, bezeichnen wir diese Sammlung als Schicht. In einer einzelnen Schicht wendet jedes Neuron seinen eigenen Gewichtsvektor und seinen eigenen Bias-Term auf das eingehende Signal an, aber alle Neuronen teilen sich dieselbe Eingabe. Durch die Anordnung von Schichten in einer Reihenfolge erstellen wir ein Netzwerk, das einfache numerische Eingaben in beliebig reichhaltige Darstellungen umwandeln kann.
Mathematisch ausgedrückt: Wenn wir die Aktivierungen der Schicht ℓ−1 durch den Spaltenvektor aℓ−1 und die Gewichte der Schicht ℓ durch eine Matrix Wℓ bezeichnen, deren Zeilen die Gewichtsvektoren der einzelnen Neuronen sind, dann ergibt sich der Präaktivierungsvektor zℓ der Schicht ℓ aus dem Matrix-Vektor-Produkt:
zℓ = Wℓ · aℓ−1 + bℓ
Wobei bℓ der Bias-Vektor für die Schicht ℓ ist. Anschließend wenden wir eine elementweise nicht lineare Aktivierungsfunktion σ an, um die Ausgabe der Schicht zu erhalten:
aℓ = σ(zℓ)
Wenn wir einen Stapel von Eingaben einspeisen, stapeln wir einfach jede Eingabe als Spalte (oder Zeile, je nach Konvention) einer Matrix X und ersetzen die Vektoroperationen durch Matrixmultiplikationen auf dem Stapel, was zu einer hocheffizienten vektorisierten Berechnung führt.
Im Code lassen sich mit PyTorch auf einfache Weise Schichten und ihre Verbindungen ausdrücken. Die integrierte Klasse torch.nn.Linear
kapselt sowohl die Gewichtsmatrix als auch den Bias-Vektor und verbindet sie für die automatische Differenziation mit dem Berechnungsgrafen. Nachfolgend finden Sie ein minimales Beispiel für ein kleines Feed-Forward-Netzwerk mit einer versteckten Schicht. Jede Zeile wird ausführlich erklärt.
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(SimpleMLP, self).__init__()
# Definieren Sie eine vollständig verbundene Schicht, die input_dim auf hidden_dim abbildet
self.fc1 = nn.Linear(input_dim, hidden_dim)
# Wählen Sie eine nichtlineare Aktivierungsfunktion für die versteckte Schicht
self.relu = nn.ReLU()
# Definieren Sie eine zweite vollständig verbundene Schicht, die hidden_dim auf output_dim abbildet
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
# Wende die erste lineare Transformation an
x = self.fc1(x)
# Wende die nichtlineare Aktivierungsfunktion elementweise an
x = self.relu(x)
# Wende die zweite lineare Transformation an, um die Ausgabe zu erzeugen
x = self.fc2(x)
return x
Die Import-Anweisungen laden torch
für Tensoroperationen und torch.nn
als Namespace für Bausteine neuronaler Netze. Die Klasse SimpleMLP
erbt von nn.Module
, der Basisklasse von PyTorch für alle Komponenten neuronaler Netze. Der Aufruf von super(SimpleMLP
, self).init()
stellt sicher, dass die interne Maschinerie von Module
ordnungsgemäß initialisiert wird.
Innerhalb des Konstruktors sind self.fc1
und self.fc2
Instanzen von nn.Linear
. Jede Linearschicht weist eine Gewichtungsmatrix der Form (output_features, input_features)
und einen Bias-Vektor der Länge output_features
zu. Durch das Speichern dieser Schichten als Attribute des Moduls registriert PyTorch automatisch ihre Parameter, sodass wir beim Aufruf von model.parameters()
alle Gewichtungs- und Bias-Tensoren in einem einzigen iterierbaren Objekt zurückgeben können.
Die Wahl von ReLU
für self.relu
spiegelt dessen weitverbreitete Verwendung wider: Die rektifizierte lineare Einheit gibt für jede negative Eingabe Null zurück und für jede nicht negative Eingabe die Eingabe selbst. Diese einfache, nicht lineare Operation führt die Nichtlinearität ein, die das Netzwerk benötigt, um komplexe Funktionen zu approximieren.
Die Vorwärtsmethode definiert die Transformation des Eingabetensors x
, während er durch das Netzwerk fließt. Wenn x
die Form (batch_size, input_dim)
hat, hat er nach self.fc1(x)
die Form (batch_size, hidden_dim)
und behält nach Anwendung von ReLU die gleiche Form, wobei sich negative Werte auf Null abbilden. Der letzte Aufruf von self.fc2
erzeugt eine Ausgabe der Form (batch_size, output_dim)
. Durch die Rückgabe von x
am Ende von forward
ermöglichen wir es, das Netzwerk wie eine Funktion aufzurufen:
model = SimpleMLP(input_dim=10, hidden_dim=50, output_dim=1)
batch_of_inputs = torch.randn(32, 10)
outputs = model(batch_of_inputs)
In diesem Beispiel ist batch_of_inputs
ein Tensor der Form (32, 10), der zweiunddreißig Samples mit jeweils zehn Merkmalen darstellt. Der Aufruf model(batch_of_inputs)
ruft im Hintergrund forward
auf, und outputs
hat die Form (32, 1), sodass wir pro Sample eine Vorhersage erhalten.
Im Hintergrund erstellt PyTorch einen Berechnungsgraphen, der jede Operation – Matrixmultiplikationen, Additionen und Nichtlinearitäten – aufzeichnet, sodass bei der Berechnung eines Verlusts auf der Grundlage der Outputs und dem anschließenden Aufruf von loss.backward()
sich die Gradienten aller Parameter in fc1
und fc2
automatisch berechnen lassen. Diese Gradienten lassen sich dann von Optimierern verwenden, um die Gewichtungsmatrizen und Bias-Vektoren zu aktualisieren.
Durch das Stapeln weiterer Schichten – beispielsweise durch mehrmaliges Abwechseln von linearen und Aktivierungsschichten – und durch Variieren der versteckten Dimensionen lassen sich tiefere und breitere Netzwerke erstellen, die hochkomplexe Zuordnungen lernen können.
Der nächste Teil der Serie zeigt, wie die gebatchten, vektorisierten Operationen den Vorwärtslauf in seiner Allgemeinheit bilden und wie die Wahl der Aktivierungen mit der Netzwerktiefe zusammenwirkt.
(rme)