Willkommen zurück! In Teil 1 haben wir gesehen, wie ein neuronales Netzwerk zwei Zahlen mit einer einfachen Feedforward-Struktur addieren kann. Am Ende des Artikels haben wir Ihnen sogar eine vollständige C++-Implementierung gezeigt, die den Vorwärtsdurchlauf, die Trainingsschleife, die Verlustberechnung und die Gewichtsanpassungen umfasst – alles im Code.
Das führt uns natürlich zur nächsten Frage: Wie genau lernt ein neuronales Netzwerk, diese Gewichte während des Trainings selbstständig anzupassen? Das werden wir hier in Teil 2 erkunden.
Bevor wir weitergehen, hier ist der vollständige C++-Code aus Teil 1, der ein einfaches neuronales Netzwerk implementiert, um zu lernen, wie man zwei Zahlen addiert. Er umfasst den Vorwärtsdurchlauf, die Verlustberechnung, Backpropagation und Gewichtsanpassungen:
#include <iostream>
#include <cmath>
using namespace std;
// Sigmoid-Aktivierungsfunktion
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
// Ableitung von Sigmoid (verwendet in Backpropagation)
float sigmoid_derivative(float y) {
return y * (1 - y); // wobei y = sigmoid(x)
}
int main() {
// Initialisiere Gewichte und Bias für 3 versteckte Knoten
// Diese Werte wurden manuell gewählt, um den Lernprozess klarer zu zeigen.
// Die Verwendung sowohl positiver als auch negativer Gewichte hilft, Symmetrie zu verhindern und bietet einen vielfältigen Ausgangspunkt.
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f; // Knoten 1 beginnt mit positiver Beeinflussung von beiden Eingaben
float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f; // Knoten 2 beginnt mit negativer Beeinflussung, was Gleichgewicht fördert
float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f; // Knoten 3 ist gemischt, nützlich zum Erfassen von Interaktionen
// Initialisiere Ausgabeschicht-Gewichte und Bias
float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000; // Anzahl der Durchläufe über die Trainingsdaten
// Trainingsschleife
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
float x1 = a / 100.0f;
float x2 = b / 100.0f;
float target = (a + b) / 200.0f; // Normalisiere Zielsumme auf [0, 1]
// ===== Vorwärtsdurchlauf =====
float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
float h1 = sigmoid(z1);
float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
float h2 = sigmoid(z2);
float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
float h3 = sigmoid(z3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float error = target - y_pred;
total_loss += error * error; // Quadratischer Fehler (Verlust)
// ===== Backpropagation - Ausgabeschicht =====
float delta_out = error; // Ableitung des Verlusts bezüglich y_pred
float grad_out_w1 = delta_out * h1; // Gradient für Ausgabewicht 1
float grad_out_w2 = delta_out * h2; // Gradient für Ausgabewicht 2
float grad_out_w3 = delta_out * h3; // Gradient für Ausgabewicht 3
float grad_out_b = delta_out; // Gradient für Ausgabebias
// ===== Backpropagation - Versteckte Schicht =====
// Berechne, wie jeder versteckte Knoten zum Fehler beigetragen hat
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1; // Gradient für w1 von Knoten 1
float grad_w2_node1 = delta_h1 * x2; // Gradient für w2 von Knoten 1
float grad_b_node1 = delta_h1; // Gradient für Bias von Knoten 1
float grad_w1_node2 = delta_h2 * x1;
float grad_w2_node2 = delta_h2 * x2;
float grad_b_node2 = delta_h2;
float grad_w1_node3 = delta_h3 * x1;
float grad_w2_node3 = delta_h3 * x2;
float grad_b_node3 = delta_h3;
// ===== Aktualisiere Ausgabeschicht-Gewichte =====
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
// ===== Aktualisiere versteckte Schicht-Gewichte =====
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
weight1_node2 += learning_rate * grad_w1_node2;
weight2_node2 += learning_rate * grad_w2_node2;
bias_node2 += learning_rate * grad_b_node2;
weight1_node3 += learning_rate * grad_w1_node3;
weight2_node3 += learning_rate * grad_w2_node3;
bias_node3 += learning_rate * grad_b_node3;
}
}
// Protokolliere Verlust alle 100 Epochen
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[Zusammenfassung] Epoche " << epoch + 1 << ": Verlust = " << total_loss << endl;
}
// ===== Teste das Modell mit Benutzereingaben =====
float a, b;
cout << "\nTeste Summenprognose (a + b)\nGeben Sie a ein: ";
cin >> a;
cout << "Geben Sie b ein: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// Erneuter Vorwärtsdurchlauf mit trainierten Gewichten
float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float predicted_sum = y_pred * 200.0f;
// Ausgabe des Ergebnisses
cout << "Vorhergesagte Summe: " << predicted_sum << "\nTatsächliche Summe: " << (a + b) << endl;
return 0;
}
In diesem Teil werden wir aufschlüsseln, wie ein neuronales Netzwerk lernt: von der Einrichtung von Schichten und Aktivierungsfunktionen bis zur Anpassung von Gewichten durch Backpropagation. Sie werden auch lernen, wie wir das Modell mit Lernraten und Epochen trainieren – und erkunden, ob ein Netzwerk über einfache Aufgaben wie Addition hinausgehen kann.
- Was Knoten und Schichten in einem neuronalen Netzwerk tatsächlich tun.
- Wie wir Anfangsgewichte setzen und sie während des Trainings anpassen.
- Die Rolle der Lernrate und der Epochen bei der Gestaltung des Lernprozesses.
- Was Aktivierungsfunktionen sind, wie sie funktionieren und wann man sigmoid, tanh oder ReLU verwenden sollte.
- Wie Backpropagation und Ableitungen es dem Netzwerk ermöglichen, aus Fehlern zu lernen.
- Ob ein neuronales Netzwerk über einfache Aufgaben wie Addition hinausgehen und mehrere Dinge lernen kann.
Bereit? Lass uns loslegen!
Knoten und Schichten: Was passiert wirklich?
Ein Neuron (oder Knoten) ist das grundlegende Bauelement eines neuronalen Netzwerks. Jedes Neuron erhält Eingabewerte, multipliziert sie mit Gewichten, addiert einen Bias und gibt das Ergebnis durch eine Aktivierungsfunktion weiter, um eine Ausgabe zu erzeugen. Es ist wie ein kleiner Entscheidungsträger, der Daten Schritt für Schritt transformiert.
Eine Schicht ist eine Sammlung von Neuronen, die auf derselben Ebene des Netzwerks arbeiten. Informationen fließen von einer Schicht zur nächsten. Die drei Haupttypen von Schichten sind:
- Eingabeschicht: empfängt die Rohdaten (z.B. die Zahlen, die Sie addieren möchten).
- Versteckte Schichten: führen Transformationen auf den Eingaben durch und lernen Muster durch gewichtete Verbindungen.
- Ausgabeschicht: produziert die endgültige Vorhersage oder das Ergebnis.

Jedes Neuron in einer Schicht ist mit den Neuronen in der nächsten Schicht verbunden, und jede dieser Verbindungen hat ein Gewicht, das das Netzwerk während des Trainings lernt und anpasst.
Betrachten Sie jeden Knoten (Neuron) in einem neuronalen Netzwerk als einen kleinen Entscheidungsträger. Er nimmt einige Eingaben, multipliziert sie mit seinen "Gewichten", addiert einen "Bias" und wendet dann eine Aktivierungsfunktion (wie sigmoid, tanh oder ReLU) an.
Mehr Knoten = mehr Gehirnleistung:
- Eine kleine Anzahl von Knoten könnte einfache Probleme schnell lösen.
- Mehr Knoten bedeuten, dass Ihr Netzwerk komplexere Muster verstehen kann, aber zu viele können es verlangsamen oder dazu führen, dass es überdenkt (überanpasst)!
Was passiert, wenn wir Schichten stapeln? Sie können mehrere versteckte Schichten stapeln – das ist ein "tiefes neuronales Netzwerk". Jede Schicht lernt etwas leicht Abstrakteres:
- Die erste Schicht lernt einfache Muster.
- Die zweite Schicht kombiniert diese Muster zu komplexeren Ideen.
- Dritte und weitere Schichten bauen ein zunehmend abstraktes Verständnis auf.
Gewichte und Bias in neuronalen Netzwerken
Was ist ein Gewicht in einem neuronalen Netzwerk?
In einem neuronalen Netzwerk ist ein Gewicht ein Wert, der die Stärke der Verbindung zwischen zwei Neuronen darstellt. Wenn Eingabedaten durch das Netzwerk fließen, werden sie mit diesen Gewichten multipliziert. Ein höheres Gewicht bedeutet, dass die Eingabe einen stärkeren Einfluss auf die Ausgabe des nächsten Neurons hat. Während des Trainings werden diese Gewichte angepasst, um dem Netzwerk zu helfen, bessere Vorhersagen zu treffen.
Was ist ein Bias in einem neuronalen Netzwerk?
Ein Bias ist ein zusätzlicher Parameter in einem neuronalen Netzwerk, der es ermöglicht, die Aktivierung eines Neurons zu verschieben. Er fungiert wie ein Offset oder Schwellenwert, der dem Modell hilft, die Daten besser anzupassen, indem er es ihm ermöglicht, Muster zu lernen, die nicht durch den Ursprung verlaufen. Genau wie Gewichte werden Biases während des Trainings angepasst, um die Vorhersagen zu verbessern.
Wie setzen wir die Startgewichte?
Wenn wir mit dem Training beginnen, müssen die Gewichte auf einige Werte initialisiert werden. In unserem Codebeispiel setzen wir sie zunächst einfach manuell:
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;
In komplexeren Systemen werden Gewichte typischerweise zufällig mit kleinen Werten (z.B. zwischen -0.5 und 0.5) initialisiert. Diese Zufälligkeit hilft, Symmetrie zu verhindern – wo alle Neuronen dasselbe lernen – und gibt dem Netzwerk eine bessere Chance, nützliche Muster zu entdecken. Häufige Initialisierungsstrategien sind Xavier (Glorot) und He Initialisierung, die darauf ausgelegt sind, ein stabiles Signal aufrechtzuerhalten, während es durch das Netzwerk fließt. Für einfache Experimente wie unsere funktionieren manuelle oder kleine Zufallswerte gut genug.
Wie passen wir die Gewichte an?
Während des Trainings funktioniert die Anpassung folgendermaßen:
- Das Netzwerk trifft eine Vorhersage mit den aktuellen Gewichten.
- Es vergleicht die Vorhersage mit der richtigen Antwort (Zielausgabe).
- Es berechnet, wie weit es daneben lag – das ist der Fehler oder "Verlust".
- Es verwendet Backpropagation, um zu berechnen, wie jedes Gewicht den Fehler beeinflusst hat.
- Jedes Gewicht wird leicht angepasst, um den Fehler beim nächsten Mal zu reduzieren – dieser Schritt erfolgt mithilfe von Gradienten (Ableitungen) und einer Lernrate.
In unserem C++-Codebeispiel geschieht dieser Prozess innerhalb der verschachtelten Trainingsschleifen. Hier ist eine kurze Referenz:
float error = target - y_pred;
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;
// Gewichte aktualisieren
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
Dieser Codeausschnitt zeigt, wie wir Gradienten berechnen und sie anwenden, um die Gewichte sowohl in der Ausgabeschicht als auch in der versteckten Schicht zu aktualisieren. Jede Iteration verwendet die Gradienten, die aus der Ableitung der Aktivierungsfunktion und dem Fehler berechnet werden, um die Gewichte in die Richtung zu schieben, die den Verlust reduziert. Über Tausende von Iterationen werden die Gewichte allmählich genauer.
Aktivierungsfunktionen: Geben Sie Ihren Knoten Persönlichkeit
Aktivierungsfunktionen entscheiden, ob ein Neuron "feuern" soll oder nicht. Sie werden auf das Ergebnis der gewichteten Summe und des Bias in jedem Knoten angewendet und führen Nichtlinearität in das Netzwerk ein – ohne die das Netzwerk nur lineare Beziehungen lernen könnte (was für komplexe Probleme nicht sehr nützlich ist).
Hier sind einige gängige Aktivierungsfunktionen:
1. Sigmoid
- Formel:
1 / (1 + e^(-x))
- Ausgabebereich: 0 bis 1
Anwendungsfall: Ideal für binäre Klassifikation oder Ausgabeschichten, wo Sie Werte zwischen 0 und 1 benötigen.
- Vorteile: Sanfte, begrenzte Ausgabe.
- Nachteile: Sättigt an den Extremen (nahe 0 oder 1), verlangsamt das Lernen aufgrund verschwindender Gradienten.

2. Tanh
- Formel:
(e^x - e^(-x)) / (e^x + e^(-x))
Ausgabebereich: -1 bis 1
- Anwendungsfall: Wenn Sie eine nullzentrierte Ausgabe wünschen.
- Vorteile: Stärkere Gradienten als sigmoid; schnelleres Lernen in versteckten Schichten.
- Nachteile: Leidet immer noch unter verschwindenden Gradienten an den Extremen.

3. ReLU (Rectified Linear Unit)
- Formel:
max(0, x)
- Ausgabebereich: 0 bis unendlich.
Anwendungsfall: Standard für die meisten versteckten Schichten im Deep Learning.
- Vorteile: Schnelle Berechnung, hilft bei spärlicher Aktivierung.
- Nachteile: Kann sterben, wenn Eingaben immer negativ sind ("sterbendes ReLU-Problem").

Wann sollte man welche Aktivierungsfunktion verwenden?
Aktivierung | Ausgabebereich | Gut für | Häufiger Schichttyp | Hinweise |
---|---|---|---|---|
Sigmoid | 0 bis 1 | Binäre Ausgabe | Ausgabeschicht | Kann verschwindenden Gradient verursachen |
Tanh | -1 bis 1 | Nullzentrierte versteckte Schichten | Versteckte Schicht | Stärkere Gradienten als sigmoid |
ReLU | 0 bis ∞ | Die meisten tiefen Netzwerke und CNNs | Versteckte Schicht | Schnell, effizient, kann aber bei Negativen "sterben" |
In unserem Beispielcode haben wir sigmoid für versteckte Schichten verwendet. Es funktioniert für kleine Demos, aber in größeren Netzwerken wird oft ReLU für bessere Leistung und schnellere Trainingszeiten bevorzugt.
Ableitungen und Backpropagation: Wie Netzwerke aus Fehlern lernen
Stellen Sie sich vor, Sie sind blind gefesselt, stehen auf einem Hügel und müssen Ihren Weg nach unten finden. Sie würden mit Ihrem Fuß herumfühlen, um zu spüren, in welche Richtung es bergab geht, und dann entsprechend bewegen.
Das ist es, was neuronale Netzwerke mit Hilfe von Ableitungen tun – sie überprüfen, welche kleinen Anpassungen die Vorhersagen genauer machen.
Nachdem das Netzwerk eine Vorhersage getroffen hat, berechnet es, wie falsch es war (wir nennen das "Verlust"). Dann verwendet es etwas, das Backpropagation genannt wird, um diesen Fehler rückwärts durch das Netzwerk zu verfolgen und herauszufinden, wie jedes Gewicht zu dem Fehler beigetragen hat.
Hier ist die vereinfachte Backpropagation-Schleife:
- Vorwärtsdurchlauf: Schätzen Sie die Antwort.
- Verlust berechnen: Wie falsch war diese Schätzung?
- Rückwärtsdurchlauf: Finden Sie heraus, wie jedes Gewicht diesen Fehler beeinflusst hat.
- Gewichte aktualisieren: Leicht die Gewichte anpassen, um zukünftige Fehler zu reduzieren.
Dieser Prozess wiederholt sich Tausende von Malen und macht das Netzwerk allmählich immer intelligenter.
Lassen Sie uns eine vereinfachte C++-Implementierung dieser Idee ansehen:
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
Das ist unsere Aktivierungsfunktion und ihre Ableitung. Sie werden sowohl im Vorwärts- als auch im Rückwärtsdurchlauf verwendet.
Die Trainingsschleife verwendet zwei Eingaben (a und b), normalisiert sie zwischen 0 und 1 und trainiert das Netzwerk, um ihre Summe vorherzusagen. Wir verwenden 3 versteckte Knoten und 1 Ausgabeknoten. Hier ist ein wichtiger Teil der Schleife:
float error = target - y_pred;
total_loss += error * error;
float delta_out = error;
...
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
Hier berechnen wir den Fehler und verwenden Ableitungen, um Anpassungen durch das Netzwerk zu schieben – Backpropagation in Aktion.
Am Ende des Trainings erlauben wir den Benutzern, zwei Zahlen einzugeben, und das Netzwerk sagt die Summe voraus. Es speichert nicht – es verallgemeinert aus dem, was es gelernt hat.
Lernrate und Epochen
Wie Sie vielleicht im Code bemerkt haben, verwenden wir zwei wichtige Hyperparameter, um den Trainingsprozess zu steuern: Lernrate und Epochen.
Was ist eine Lernrate?
Die Lernrate bestimmt, wie groß jeder Schritt bei der Aktualisierung der Gewichte ist. Eine kleine Lernrate (wie 0.01) bedeutet, dass das Netzwerk bei jedem Schritt winzige Anpassungen vornimmt, was sicherer, aber langsamer ist. Eine große Lernrate könnte die Dinge beschleunigen, aber wenn sie zu groß ist, könnte das Netzwerk über das Ziel hinausschießen und nicht richtig lernen.
In unserem Code verwenden wir:
float learning_rate = 0.01f;
Das gibt uns ein Gleichgewicht zwischen Geschwindigkeit und Stabilität.
Was ist eine Epoche?
Eine Epoche ist eine vollständige Durchlauf durch den gesamten Trainingsdatensatz. In unserem Beispiel trainieren wir das Netzwerk für jede Epoche mit jeder Kombination von a
und b
von 0 bis 99. Dann machen wir es für die nächste Epoche erneut.
Wir trainieren über mehrere Epochen, um dem Netzwerk zu ermöglichen, seine Gewichte immer wieder zu verfeinern.
- Wenn Sie die Anzahl der Epochen erhöhen, erhält das Netzwerk mehr Chancen zur Verbesserung, was möglicherweise den Verlust weiter reduziert. Zu viele Epochen können jedoch zu Überanpassung führen, bei der das Modell zu stark auf die Trainingsdaten zugeschnitten ist und bei neuen Eingaben schlecht abschneidet.
- Wenn Sie die Anzahl der Epochen verringern, wird das Training schneller, aber das Netzwerk lernt möglicherweise nicht genug und könnte unterperformen.
Im Code verwenden wir:
int epochs = 1000;
Das ist typischerweise ein guter Ausgangspunkt für einfache Probleme wie das Lernen von Addition, aber Sie können es anpassen, je nachdem, wie schnell oder langsam der Verlust sinkt.
Kann ein neuronales Netzwerk mehrere Dinge lernen?
Absolut. Neuronale Netzwerke sind nicht darauf beschränkt, nur eine Art von Aufgabe zu lernen. Tatsächlich kann dieselbe Netzwerkarchitektur trainiert werden, um eine Vielzahl von Dingen zu tun – wie das Erkennen handgeschriebener Ziffern, das Übersetzen von Sprachen oder sogar das Generieren von Musik – abhängig von den Daten, die ihm gegeben werden, und wie es trainiert wird.
In unserem Beispiel hat das Netzwerk gelernt, zwei Zahlen zu addieren, aber mit anderen Trainingsdaten und einer modifizierten Ausgabeschicht könnte es auch lernen:
- Zahlen subtrahieren
- Bilder klassifizieren
- Zukünftige Werte in einer Zeitreihe vorhersagen
Je mehr Aufgaben Sie möchten, dass das Netzwerk bewältigt – oder je komplexer die Aufgabe ist – desto mehr Neuronen und Schichten benötigen Sie möglicherweise. Ein einfaches Problem wie Addition benötigt möglicherweise nur wenige Knoten, während Aufgaben wie Bildklassifikation oder Sprachübersetzung viel tiefere und breitere Netzwerke erfordern können. Denken Sie nur daran: Mehr Komplexität ist nicht immer besser – zu viele Knoten können zu Überanpassung führen, bei der das Netzwerk speichert, anstatt zu lernen, zu verallgemeinern.
Dies ist die verbesserte Version unseres neuronalen Netzwerks, bei dem wir es trainieren, sowohl hinzuzufügen als auch zu subtrahieren von Eingaben mit einem einzigen gemeinsamen Netzwerk:
#include <iostream>
#include <cmath>
using namespace std;
// Sigmoid-Aktivierungsfunktion (drückt Eingaben in den Bereich [0, 1])
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
// Ableitung von Sigmoid (verwendet zur Berechnung von Gradienten während der Backpropagation)
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
int main() {
// === Gewichte und Bias der versteckten Schicht (3 versteckte Neuronen, jeweils 2 Eingaben) ===
float w1_n1 = 0.5f, w2_n1 = 0.5f, b1 = 0.0f;
float w1_n2 = -0.5f, w2_n2 = -0.5f, b2 = 0.0f;
float w1_n3 = 0.25f, w2_n3 = -0.25f, b3 = 0.0f;
// === Gewichte und Bias der Ausgabeschicht (2 Ausgabeneuronen: Summe und Subtraktion) ===
float out_w1_sum = 0.5f, out_w2_sum = 0.5f, out_w3_sum = 0.5f, out_b_sum = 0.0f;
float out_w1_sub = -0.5f, out_w2_sub = -0.5f, out_w3_sub = 0.5f, out_b_sub = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000;
// === Trainingsschleife ===
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
// === Normalisiere Eingabewerte ===
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// === Normalisiere Ziele ===
float target_sum = (a + b) / 200.0f; // Summe reicht von 0 bis 198 → [0, ~1]
float target_sub = (a - b + 100.0f) / 200.0f; // Subtraktion reicht von -99 bis +99 → [0, ~1]
// === Vorwärtsdurchlauf (Eingabe → Versteckte Schicht) ===
float z1 = x1 * w1_n1 + x2 * w2_n1 + b1;
float h1 = sigmoid(z1);
float z2 = x1 * w1_n2 + x2 * w2_n2 + b2;
float h2 = sigmoid(z2);
float z3 = x1 * w1_n3 + x2 * w2_n3 + b3;
float h3 = sigmoid(z3);
// === Vorwärtsdurchlauf (Versteckte → Ausgabeschicht) ===
float y_pred_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
float y_pred_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;
// === Verlust berechnen (Quadratischer Fehler) ===
float error_sum = target_sum - y_pred_sum;
float error_sub = target_sub - y_pred_sub;
total_loss += error_sum * error_sum + error_sub * error_sub;
// === Backpropagation - Ausgabeschicht (Gradienten für jeden Ausgabeknoten) ===
float delta_out_sum = error_sum;
float grad_out_w1_sum = delta_out_sum * h1;
float grad_out_w2_sum = delta_out_sum * h2;
float grad_out_w3_sum = delta_out_sum * h3;
float grad_out_b_sum = delta_out_sum;
float delta_out_sub = error_sub;
float grad_out_w1_sub = delta_out_sub * h1;
float grad_out_w2_sub = delta_out_sub * h2;
float grad_out_w3_sub = delta_out_sub * h3;
float grad_out_b_sub = delta_out_sub;
// === Backpropagation - Versteckte Schicht ===
float delta_h1 = (delta_out_sum * out_w1_sum + delta_out_sub * out_w1_sub) * sigmoid_derivative(h1);
float delta_h2 = (delta_out_sum * out_w2_sum + delta_out_sub * out_w2_sub) * sigmoid_derivative(h2);
float delta_h3 = (delta_out_sum * out_w3_sum + delta_out_sub * out_w3_sub) * sigmoid_derivative(h3);
float grad_w1_n1 = delta_h1 * x1;
float grad_w2_n1 = delta_h1 * x2;
float grad_b1 = delta_h1;
float grad_w1_n2 = delta_h2 * x1;
float grad_w2_n2 = delta_h2 * x2;
float grad_b2 = delta_h2;
float grad_w1_n3 = delta_h3 * x1;
float grad_w2_n3 = delta_h3 * x2;
float grad_b3 = delta_h3;
// === Aktualisiere Ausgabewichte ===
out_w1_sum += learning_rate * grad_out_w1_sum;
out_w2_sum += learning_rate * grad_out_w2_sum;
out_w3_sum += learning_rate * grad_out_w3_sum;
out_b_sum += learning_rate * grad_out_b_sum;
out_w1_sub += learning_rate * grad_out_w1_sub;
out_w2_sub += learning_rate * grad_out_w2_sub;
out_w3_sub += learning_rate * grad_out_w3_sub;
out_b_sub += learning_rate * grad_out_b_sub;
// === Aktualisiere versteckte Gewichte ===
w1_n1 += learning_rate * grad_w1_n1;
w2_n1 += learning_rate * grad_w2_n1;
b1 += learning_rate * grad_b1;
w1_n2 += learning_rate * grad_w1_n2;
w2_n2 += learning_rate * grad_w2_n2;
b2 += learning_rate * grad_b2;
w1_n3 += learning_rate * grad_w1_n3;
w2_n3 += learning_rate * grad_w2_n3;
b3 += learning_rate * grad_b3;
}
}
// === Verlust alle 100 Epochen ausgeben ===
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[Epoche " << epoch + 1 << "] Verlust: " << total_loss << endl;
}
// === Testphase ===
float a, b;
cout << "\nTestvorhersage\nGeben Sie a ein: ";
cin >> a;
cout << "Geben Sie b ein: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// Erneuter Vorwärtsdurchlauf für Benutzereingaben
float h1 = sigmoid(x1 * w1_n1 + x2 * w2_n1 + b1);
float h2 = sigmoid(x1 * w1_n2 + x2 * w2_n2 + b2);
float h3 = sigmoid(x1 * w1_n3 + x2 * w2_n3 + b3);
float y_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
float y_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;
// Denormalisiere Ausgaben
float predicted_sum = y_sum * 200.0f;
float predicted_sub = y_sub * 200.0f - 100.0f;
cout << "Vorhergesagte Summe: " << predicted_sum << endl;
cout << "Tatsächliche Summe: " << (a + b) << endl;
cout << "Vorhergesagte Differenz: " << predicted_sub << endl;
cout << "Tatsächliche Differenz: " << (a - b) << endl;
return 0;
}
Zusammenfassung
Das war eine Menge zu verarbeiten – aber irgendwie großartig, oder? Wir haben nicht nur über Theorie gesprochen; wir haben gesehen, wie ein neuronales Netzwerk lernt, etwas Reales zu tun: Zahlen zu addieren und zu subtrahieren. Keine Regeln hartkodiert – nur Gewichte, die sich durch das Training selbst anpassen. Auf dem Weg haben wir die Lernraten, Aktivierungsfunktionen und die Backpropagation-Magie kennengelernt, die alles zusammenbindet.
Im nächsten Teil werden wir einen Schritt zurück vom Code machen und die Geschichte hinter all dem erkunden. Woher kommen neuronale Netzwerke? Warum ist KI in den letzten Jahren explodiert? Was hat sich geändert? Wir werden erkunden, wie alles begann – und warum es gerade erst anfängt.