Skip to content

Aufgabe 3 - Neuronale Netze & EA

Implementierung eines Multi-Layer Perceptrons (MLP) mit einer verdeckten Schicht zur Approximation der XOR-Funktion. Das Training der Netzparameter erfolgt mittels eines evolutionären Algorithmus (EA), der die Gewichtsmatrizen des Netzes optimiert.

Teilaufgaben:

  1. Genom definieren: Festlegen, wie die Netzparameter (z. B. W1 und W2) als Genom eines Individuums repräsentiert werden.
  2. Fitness-Funktion: Implementieren einer Fitness-Funktion zur Bewertung der Individuen, z. B. basierend auf dem Mean Squared Error (MSE) zwischen Netzoutput und Zielwerten.
  3. EA-Implementierung: Implementieren des evolutionären Trainings.
  4. Auswertung: Dokumentieren des besten Individuums nach 10, 20, 50 und 100 Evolutionsrunden.

In der Biologie enthält das Genom die vollständige Bauanleitung eines Organismus. In unserem ANN entspricht das Genom den trainierbaren Parametern, also den Gewichten der Verbindungen.

Das Netz hat zwei Gewichtsmatrizen:

  1. W1 (Input -> Hidden): Form 2x3 (6 Gewichte).
  2. W2 (Hidden -> Output): Form 3x1 (3 Gewichte).

Das Genom ist also die flache Liste aller 9 Gewichte.

Das MLP ist bereits gegegebn mit den hilfsfunktionen und kann getestet werden

import numpy as np
class ANN:
def __init__(self):
self.W1 = np.random.rand(2, 3)
self.W2 = np.random.rand(3, 1)
@staticmethod # korrigiert von @classmethod
def sigmoid(x):
# Aktivierungsfunktion: macht aus beliebigen Zahlen Werte zwischen 0 und 1
return (1/(1+np.exp(-x)))
def forward(self, x):
# 1) Hidden-Schicht berechnen: x (n,2) @ W1 (2,3) -> (n,3)
z = sigmoid(np.dot(x, self.W1))
# 2) Output-Schicht berechnen: z (n,3) @ W2 (3,1) -> (n,1)
y = sigmoid(np.dot(z, self.W2))
# Ausgabe zurückgeben (n,1)
return y
# 4 XOR-Eingaben als Trainings-/Testdaten (4 Beispiele, je 2 Inputs)
xor_inputs = np.vstack(([0, 0], [0, 1], [1, 0], [1, 1]))
# Ein Netzwerk mit zufälligen Gewichten erzeugen
ann = ANN()
# Vorwärtsdurchlauf: Ausgabe des Netzes für alle 4 Inputs berechnen
xor = ann.forward(xor_inputs)
PYTHON - Neural Network Output
print(xor)
Output > ...

Schritte:

  1. Die Zielwerte werden in y_true festgelegt:
    y_true = np.array([0, 1, 1, 0]).reshape(-1, 1)[[0],[1],[1],[0]]
  2. Netzausgabe für ein Individuum berechnen:
    y_pred = ann.forward(xor_inputs)
  3. Fehler mit MSE (Mean Squared Error) berechnen:
    mse = np.mean((y_true - y_pred) ** 2)
  4. MSE in Fitness umwandeln:
    fitness = 1.0 / (1.0 + mse)
    Je kleiner der MSE, desto näher liegt die Fitness an 1 (bestmöglicher Wert) und desto besser ist das Individuum.

Die fertige Funktion sieht dann so aus:

def fitness_function(ann, xor_inputs):
y_true = np.array([0, 1, 1, 0]).reshape(-1, 1)
y_pred = ann.forward(xor_inputs)
mse = np.mean((y_true - y_pred) ** 2)
return 1.0 / (1.0 + mse)
PYTHON - Neural Network Output
print(f"Fitness Score: {fitness_function(ann, xor_inputs):.4f}")
Output > ...

Ein Individuum = ein Satz Gewichte (W1, W2). Wir speichern es als Genom-Vektor mit 9 Zahlen (6 aus W1 + 3 aus W2).

Dafür werden 2 Hilfsfunktionen benötigt:

Individuum erstellen (pack)

def pack(W1, W2):
return np.concatenate([W1.flatten(), W2.flatten()]) # Länge 9

Wieder aufteilen (unpack)

def unpack(genome):
W1 = genome[:6].reshape(2,3)
W2 = genome[6:].reshape(3,1)
return W1, W2

2. Population erstellen (Pool von Individuen)

Section titled “2. Population erstellen (Pool von Individuen)”

Wir nehmen 30 Zufällige Individuen:

pop_size = 30
population = []
for _ in range(pop_size):
ann = ANN() # erzeugt zufällige W1/W2
genome = pack(ann.W1, ann.W2) # macht daraus den 9er-Vektor
population.append(genome)

Wir haben in der population Genom-Vektoren (Länge 9). Die Fitness-Funktion braucht aber ein ANN-Object dafür erstellen wir diese Funktion

def fitness_of_genome(genome):
W1, W2 = unpack(genome) # 9er-Vektor -> zwei Matrizen
ann = ANN() # Objekt erzeugen
ann.W1 = W1 # Gewichte überschreiben
ann.W2 = W2
return fitness_function(ann, xor_inputs) # deine Fitness aus Aufgabe 2

und dann können wir die Fitness-Liste für die ganze Population erstellen:

fitnesses = [fitness_of_genome(g) for g in population]

Bestes Individuum finden:

best_idx = int(np.argmax(fitnesses))
parent1 = population[best_idx]

Zweites Elternteil zufällig (aber nicht das beste)

indices = [i for i in range(len(population)) if i != best_idx]
rand_idx = int(np.random.choice(indices))
parent2 = population[rand_idx]

Damit haben wir jetzt zwei Eltern-Genome (parent1, parent2), beide sind Vektoren der Länge 9.

Der Crossover mischt die Gewichte (Gene) von zwei Eltern-Individuen zu einem neuen Kind-Individuum. Wir nehmen den Unform Crossover also zufällig aus einem der Elternteil gewählt.

# crossover funktion
def crossover(parent1, parent2):
mask = np.random.rand(parent1.shape[0]) < 0.5 # True/False pro Gen
child = np.where(mask, parent1, parent2) # pro Gen wählen
return child
# Kind erzeugen
child = crossover(parent1, parent2)

Nach dem Crossover ist das Kind nur eine Mischung der Eltern. Mit Mutation sorgen wir dafür, dass kleine Zufallsänderungen an einzelnen Genen durchgeführt werden damit man nicht stecken bleibt.

Mutations-Funktion (pro Gewicht mit Wahrscheinlichkeit p)

  • Mit Wahrscheinlichkeit p wird ein Gewicht verändert.
  • Änderung ist ein kleiner Zufallswert (Normalverteilung mit Std sigma).
def mutate(genome, p=0.1, sigma=0.1):
genome = genome.copy()
mask = np.random.rand(genome.shape[0]) < p # welche Gene mutieren?
genome[mask] += np.random.normal(0.0, sigma, size=np.sum(mask))
return genome
# Mutation anwenden
child = mutate(child, p=0.1, sigma=0.1)
# Fitness des Kindes berehcne
child_fit = fitness_of_genome(child)
# Schlechtestes Individuum in der Population finden
worst_idx = int(np.argmin(fitnesses))
# Ersetzen, wenn das Kind besser ist
if child_fit > fitnesses[worst_idx]:
population[worst_idx] = child
# Fitness-Wert aktualisieren:
fitnesses[worst_idx] = child_fit

Damit ist eine Runde des EA fertig: Eltern wählen → Crossover → Mutation → Kind bewerten → ggf. ersetzen.

Hier nochmal der Gesamte Code als Übersicht mit Kommentaren:

import numpy as np
# ------------------------------------------------------------
# Gegeben: XOR-Inputs + Zielwerte
# ------------------------------------------------------------
xor_inputs = np.vstack(([0, 0], [0, 1], [1, 0], [1, 1]))
y_true = np.array([0, 1, 1, 0]).reshape(-1, 1)
# ------------------------------------------------------------
# Gegeben/MLP
# ------------------------------------------------------------
class ANN:
def __init__(self):
self.W1 = np.random.rand(2, 3)
self.W2 = np.random.rand(3, 1)
@staticmethod
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
z = self.sigmoid(np.dot(x, self.W1))
y = self.sigmoid(np.dot(z, self.W2))
return y
# ------------------------------------------------------------
# Aufgabe 2: Fitness-Funktion
# ------------------------------------------------------------
def fitness_function(ann, xor_inputs):
y_pred = ann.forward(xor_inputs)
mse = np.mean((y_true - y_pred) ** 2)
fitness = 1.0 / (1.0 + mse) # größer = besser
return fitness
# ------------------------------------------------------------
# Hilfsfunktionen: Genom packen/entpacken
# ------------------------------------------------------------
def pack(W1, W2):
return np.concatenate([W1.flatten(), W2.flatten()]) # Länge 9
def unpack(genome):
W1 = genome[:6].reshape(2, 3)
W2 = genome[6:].reshape(3, 1)
return W1, W2
# ------------------------------------------------------------
# Fitness eines Genoms berechnen
# ------------------------------------------------------------
def fitness_of_genome(genome):
W1, W2 = unpack(genome)
ann = ANN()
ann.W1 = W1
ann.W2 = W2
return fitness_function(ann, xor_inputs)
# ------------------------------------------------------------
# EA-Bausteine: Crossover + Mutation
# ------------------------------------------------------------
def crossover(parent1, parent2):
# Uniform crossover: pro Gen zufällig Parent1 oder Parent2
mask = np.random.rand(parent1.shape[0]) < 0.5
child = np.where(mask, parent1, parent2)
return child
def mutate(genome, p=0.1, sigma=0.1):
genome = genome.copy()
mask = np.random.rand(genome.shape[0]) < p
genome[mask] += np.random.normal(0.0, sigma, size=np.sum(mask))
return genome
# ------------------------------------------------------------
# EA-Training
# ------------------------------------------------------------
pop_size = 30
rounds = 100
report_points = [10, 20, 50, 100]
# Population erstellen
population = []
for _ in range(pop_size):
ann = ANN()
genome = pack(ann.W1, ann.W2)
population.append(genome)
# Fitness initial berechnen
fitnesses = [fitness_of_genome(g) for g in population]
for r in range(1, rounds + 1):
# Eltern wählen: bestes + zufälliges anderes
best_idx = int(np.argmax(fitnesses))
parent1 = population[best_idx]
indices = [i for i in range(len(population)) if i != best_idx]
parent2 = population[int(np.random.choice(indices))]
# Kind erzeugen
child = crossover(parent1, parent2)
child = mutate(child, p=0.1, sigma=0.1)
# Kind bewerten
child_fit = fitness_of_genome(child)
# schlechtestes ersetzen, wenn Kind besser
worst_idx = int(np.argmin(fitnesses))
if child_fit > fitnesses[worst_idx]:
population[worst_idx] = child
fitnesses[worst_idx] = child_fit
# Reporting nach 10/20/50/100
if r in report_points:
best_idx = int(np.argmax(fitnesses))
best_genome = population[best_idx]
W1, W2 = unpack(best_genome)
ann = ANN()
ann.W1 = W1
ann.W2 = W2
y_pred = ann.forward(xor_inputs)
y_pred_bin = (y_pred >= 0.5).astype(int)
print(f"\n--- Runde {r} ---")
print("Beste Fitness:", fitnesses[best_idx])
print("Output (roh):\n", np.round(y_pred, 4))
print("Output (>=0.5):\n", y_pred_bin)
PYTHON - Neural Network Output
# Starte Evolution...
Output > ...