Ein neuronales Netz von Grund auf mit Python & NumPy bauen
27.08.2024
Zusammenfassung
Ein einsteigerfreundlicher Guide zu neuronalen Netzen: vom einzelnen Neuron bis zum kompletten Python-Modell, das mit NumPy Hauspreise vorhersagt.
Einleitung
Künstliche Intelligenz verändert unsere Welt, und neuronale Netze sind der Grundbaustein hinter dem meisten davon. In diesem Artikel schauen wir uns an, wie diese Systeme funktionieren, von einem einzelnen Neuron bis zum kompletten Netzwerk, und bauen ein kleines Modell in Python von Grund auf.
Ein einzelnes Neuron
Wenn du dich schon mit KI beschäftigt hast, ist dir wahrscheinlich eine Abbildung wie die unten begegnet. Sie zeigt ein einfaches neuronales Netz, das wir uns heute genauer ansehen und in Python implementieren. Das neuronale Netz ist das fundamentale Konzept hinter moderner KI und lohnt einen genaueren Blick. Bevor wir zum Code kommen, schauen wir uns die wichtigsten Ideen an.
Um zu verstehen, wie ein neuronales Netz funktioniert, zoomen wir auf einen einzelnen Knoten (Neuron) heran.
Stell dir einen Bewegungsmelder in deinem Garten vor, der das Licht einschaltet, wenn er Bewegung erkennt und es draußen dunkel ist. Zusätzlich gibt es einen Drehregler, mit dem du einstellen kannst, wie leicht das Licht angeht. Dieser Bewegungsmelder ist eine gute Analogie für ein einzelnes Neuron.
In einem neuronalen Netz sind die erkannte Bewegung und die Helligkeit der Umgebung die Eingaben (x1 und x2). Diese Eingaben sind über Gewichte mit dem Neuron verbunden, die bestimmen, wie stark jede Eingabe das Ergebnis beeinflusst. Der Drehregler in unserem Beispiel entspricht dem Bias, einem zusätzlichen unabhängigen Wert, der den Einfluss der Eingaben auf das Endergebnis anpasst.
Um die Ausgabe des Neurons zu berechnen, multiplizieren wir jede Eingabe (x1, x2) mit ihrem zugehörigen Gewicht (w1, w2). Dann summieren wir diese Produkte und addieren den Bias. Wir erhalten so einen numerischen Wert, der die Antwort des Neurons darstellt. In älteren neuronalen Modellen zeigte dieser Wert an, ob das Neuron „feuert" (Licht an) oder ruhig bleibt (Licht aus). Da dieser Ansatz nur zwei Zustände erlaubte (1 oder 0), wurde später eine Aktivierungsfunktion eingeführt, die eine Wahrscheinlichkeit liefert, wie stark das Neuron feuert, normalerweise einen Wert zwischen 0 und 1. In unserem Fall verwenden wir die Sigmoid-Funktion, eine gängige Wahl für solche Netze.
Jetzt, wo wir wissen, wie ein einzelnes Neuron funktioniert, wird das Bauen eines Netzes überschaubar: Wir fassen mehrere Neuronen zu Schichten zusammen und verbinden sie so, dass die Ausgabe einer Schicht zur Eingabe der nächsten wird.
Unser einfaches neuronales Netz besteht aus drei Schichten:
Input-Layer: Nimmt die Eingangsdaten entgegen (z. B. Größe und Alter eines Hauses). Die Neuronen hier sind Platzhalter für normalisierte Zahlen, die die Eingabe repräsentieren.
Hidden-Layer: Verarbeitet die Informationen.
Output-Layer: Erzeugt das Endergebnis (z. B. den Hauspreis).
So sieht der Datenfluss im Netz aus, an einem Beispiel erklärt:
Die Eingabewerte gelangen über den Input-Layer ins Netz (z. B. ist die normalisierte Hausgröße 0,8 und das normalisierte Alter 0,4, also hat ein Input-Neuron den Wert 0,8 und das andere 0,4).
Jedes Hidden-Neuron erhält alle Eingabewerte (z. B. 0,8 und 0,4).
Die Hidden-Neuronen berechnen ihre Ausgaben und geben sie an den Output-Layer weiter.
Der Output-Layer berechnet auf Basis der Hidden-Layer-Ausgaben das Endergebnis.
Wir schauen uns das später noch genauer an und setzen es im Code um. Zuerst legen wir eine Klasse mit der Struktur unseres neuronalen Netzes in Python an:
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
# Initialize random weights and biases
# W1 and b1 sit between the input and hidden layer
self.W1 = np.random.randn(self.input_size, self.hidden_size)
self.b1 = np.ones((1, self.hidden_size))
# W2 and b2 sit between the hidden and output layer
self.W2 = np.random.randn(self.hidden_size, self.output_size)
self.b2 = np.ones((1, self.output_size))
# Create the network
nn = NeuralNetwork(input_size=2, hidden_size=3, output_size=1)
Einführung ins Training
Mit der Struktur des Netzes können wir zum Training übergehen. Die folgende Übersicht beschreibt einen einzelnen Trainingsschritt. In der Praxis trainieren wir über mehrere Batches und viele Durchläufe. Der Ablauf:
Initialisierung: Wir haben ein Netz, das mit zufälligen Gewichten und Biases initialisiert wurde.
Forward-Pass: Wir nehmen die Trainingsdaten, die aus Eingabe und korrekter Ausgabe bestehen. Die Eingabe wird ins Netz gespeist und die vorhergesagte Ausgabe berechnet.
Kostenfunktion: Wir vergleichen die vorhergesagte Ausgabe mit der korrekten Ausgabe über eine Kostenfunktion, die die Größe des Fehlers quantifiziert. Es gibt viele Kostenfunktionen; in unserem Beispiel nutzen wir den Mean Squared Error (MSE).
Backward-Pass: Mit dieser Kosten gehen wir rückwärts durch das Netz und berechnen über partielle Ableitungen, wie jedes Gewicht und jeder Bias angepasst werden muss.
Update: Zum Schluss passen wir jedes Gewicht und jeden Bias ein Stück in die richtige Richtung an. Die Lernrate steuert, wie groß diese Schritte sind.
Forward-Pass
Wie im vorigen Abschnitt angerissen, nimmt der Forward-Pass die Werte aus den bereitgestellten Daten, speist sie ins Netz und berechnet die Ausgabe. Ein Beispiel zeigt das am besten.
1. Die Situation: Wir haben zwei Eingabeparameter: die Größe eines Hauses (i1) und sein Alter (i2). Das Ziel ist es, ein neuronales Netz zu trainieren, das auf Basis dieser beiden Parameter den Preis des Hauses (o1) vorhersagt. Für das Training nutzen wir diesen kleinen Datensatz:
2. Initialisierung: Wir initialisieren das Netz mit zufälligen Gewichten und Biases. In der Abbildung habe ich der Einfachheit halber 1 als Bias verwendet.
3. Normalisierung: Als Nächstes normalisieren wir die Daten, damit jedes Merkmal einen vergleichbaren Einfluss auf das Netz hat. Ich gehe hier nicht auf die Details ein; eine kurze Suche liefert gute Erklärungen. Für unser Beispiel wird die erste Eingabe [100, 5] normalisiert und gerundet zu -0,3 (i1) und -0,8 (i2).
4. Berechnung: Mit diesen Eingaben und unseren Gewichten und Biases können wir eine Ausgabe berechnen.
a) Wir berechnen das Ergebnis für jedes Hidden-Neuron mit der Summe von vorher:
b) Auf Basis der Hidden-Layer-Ergebnisse berechnen wir die Ausgabe:
o1 = σ(h1·0,5 + h2·0,4 + h3·0,6 + 1)
o1 = σ(0,65·0,5 + 0,61·0,4 + 0,59·0,6 + 1)
o1 = σ(1,923) = 0,87
Mit der Übersicht zum Forward-Pass kommen wir zum Code:
# Sigmoid activation function
def sigmoid(x):
return 1 / (1 + np.exp(-x))
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.W1 = np.random.randn(self.input_size, self.hidden_size)
self.b1 = np.ones((1, self.hidden_size))
self.W2 = np.random.randn(self.hidden_size, self.output_size)
self.b2 = np.ones((1, self.output_size))
def forward(self, X):
# Compute the hidden layer output
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = sigmoid(self.z1)
# Compute the output layer (no activation, since this is a regression task)
self.z2 = np.dot(self.a1, self.W2) + self.b2
self.a2 = self.z2
return self.a2
Backward-Pass
Sobald wir die Ausgabe für eine Eingabe berechnet haben, wird sie beim ersten Versuch selten dem Zielwert entsprechen. Der Backpropagation-Algorithmus hilft uns, das zu verbessern. Er arbeitet rückwärts durch das Netz, ausgehend vom Fehler, und berechnet, wie stark jedes Neuron zu diesem Fehler beigetragen hat. Mit dieser Information können wir Gewichte und Biases Schritt für Schritt anpassen, um den Fehler zu reduzieren.
Backpropagation nutzt partielle Ableitungen, um zu bestimmen, wie jede Eingabe den Endfehler beeinflusst. Die Idee ist die gleiche wie bei der Analyse, wie sich die Eingaben x₁ und x₂ in einem einzelnen Neuron auf den Fehler auswirken. Der Algorithmus läuft in mehreren Schritten ab: Ein Forward-Pass berechnet die Ausgabe, der Fehler wird durch Vergleich von Ausgabe und erwartetem Ergebnis berechnet, der Fehler wird rückwärts durch das Netz propagiert, dabei werden die Gradienten berechnet, und die Gewichte und Biases werden aktualisiert, um den Fehler zu reduzieren.
Die mathematischen Details von Backpropagation sind für diesen Artikel zu weit gefasst. Wer tiefer einsteigen möchte, findet im YouTube-Video von 3Blue1Brown zum Thema einen guten Einstieg. Für ein praktisches Gefühl, wie es funktioniert, hier eine vereinfachte Python-Implementierung des Backward-Pass:
# Derivative of the sigmoid function
def sigmoid_derivative(x):
return x * (1 - x)
class NeuralNetwork:
# ... (previous code stays the same) ...
def backward(self, X, y, output, learning_rate):
m = X.shape[0] # number of training samples
# Error of the output layer
self.error = y - output
self.delta_output = self.error
# Gradients for W2 and b2
self.W2_grad = np.dot(self.a1.T, self.delta_output) / m
self.b2_grad = np.sum(self.delta_output, axis=0, keepdims=True) / m
# Error of the hidden layer
self.error_hidden = np.dot(self.delta_output, self.W2.T)
self.delta_hidden = self.error_hidden * sigmoid_derivative(self.a1)
# Gradients for W1 and b1
self.W1_grad = np.dot(X.T, self.delta_hidden) / m
self.b1_grad = np.sum(self.delta_hidden, axis=0, keepdims=True) / m
# Update weights and biases via gradient descent
self.W2 += learning_rate * self.W2_grad
self.b2 += learning_rate * self.b2_grad
self.W1 += learning_rate * self.W1_grad
self.b1 += learning_rate * self.b1_grad
Training und Test
Nachdem die Architektur des Netzes steht, trainieren und testen wir es auf einem Datensatz mit Hauspreisen. Der Ablauf besteht aus mehreren Schritten:
Netz initialisieren: Wir erzeugen ein NeuralNetworkObjekt mit 2 Input-Neuronen, 4 Hidden-Neuronen und 1 Output-Neuron.
Datensatz: Wir definieren unseren Datensatz, wobei X die Eingabemerkmale (Größe und Alter) und y die Zielwerte (Hauspreise in Tausend Euro) repräsentiert.
Normalisierung: Wir normalisieren die Daten, damit alle Merkmale auf einer ähnlichen Skala liegen, was dem Netz beim Lernen hilft.
Training: Das Training läuft über 2.000 Epochen mit einer Lernrate von 0,05. In jeder Epoche führen wir einen Forward-Pass und anschließend einen Backward-Pass aus, um die Gewichte und Biases zu aktualisieren. Wir berechnen den Mean Squared Error (MSE) als Loss-Funktion und geben ihn alle 100 Epochen aus, um den Fortschritt zu beobachten.
Vorhersagefunktion: Wir definieren eine predictFunktion, die Größe und Alter eines Hauses entgegennimmt, sie mit denselben Parametern wie die Trainingsdaten normalisiert, durchs Netz schickt und die Ausgabe denormalisiert, um den vorhergesagten Preis in Tausend Euro zu erhalten.
Test: Wir testen das trainierte Netz, indem wir Preise für einige hypothetische Häuser vorhersagen.
Das Beispiel zeigt den vollständigen Ablauf, ein kleines neuronales Netz für eine Regressionsaufgabe zu trainieren, und bringt den vorher beschriebenen Backpropagation-Algorithmus in die Praxis.
Die Grafik unten zeigt, wie sich das Modell während des Trainings bei der Vorhersage der Hauspreise verbessert. Am Anfang ist der Loss hoch. Mit jeder weiteren Epoche fällt er schnell ab und flacht dann ab, eine typische Lernkurve.
Das Modell hat den Zusammenhang zwischen Größe, Alter und Preis gut genug erfasst, um auf den Trainingsdaten plausible Vorhersagen zu treffen. Der niedrige finale Loss ist ein gutes Zeichen, eine saubere Bewertung würde aber einen separaten Testdatensatz mit mehr Beispielen erfordern. Es besteht auch das Risiko, dass das Modell die Trainingsdaten teilweise auswendig gelernt hat, ein robusterer Aufbau würde also mehr Daten und Techniken wie einen Train/Validation-Split oder Regularisierung einschließen.
Fazit
Dieser Artikel hat die Grundlagen neuronaler Netze vorgestellt und Schritt für Schritt eine kleine Implementierung in Python mit NumPy durchgegangen. Sobald man die Mechanik selbst nachbaut (Gewichte, Biases, Forward-Pass, Backpropagation), wird sie deutlich greifbarer. Gute nächste Schritte sind, andere Aktivierungsfunktionen auszuprobieren, weitere Hidden-Layer hinzuzufügen oder auf ein Framework wie PyTorch umzusteigen.