Aufgabe 3 - Neuronale Netze & EA
Aufgabenstellung
Section titled “Aufgabenstellung”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:
- Genom definieren: Festlegen, wie die Netzparameter (z. B. W1 und W2) als Genom eines Individuums repräsentiert werden.
- Fitness-Funktion: Implementieren einer Fitness-Funktion zur Bewertung der Individuen, z. B. basierend auf dem Mean Squared Error (MSE) zwischen Netzoutput und Zielwerten.
- EA-Implementierung: Implementieren des evolutionären Trainings.
- Auswertung: Dokumentieren des besten Individuums nach 10, 20, 50 und 100 Evolutionsrunden.
1. Was ist das “Genom” des ANN?
Section titled “1. Was ist das “Genom” des ANN?”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:
- W1 (Input -> Hidden): Form 2x3 (6 Gewichte).
- W2 (Hidden -> Output): Form 3x1 (3 Gewichte).
Das Genom ist also die flache Liste aller 9 Gewichte.
2. Fitness-Funktion
Section titled “2. Fitness-Funktion”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 erzeugenann = ANN()
# Vorwärtsdurchlauf: Ausgabe des Netzes für alle 4 Inputs berechnenxor = ann.forward(xor_inputs)print(xor) Implementierung
Section titled “Implementierung”Schritte:
- Die Zielwerte werden in
y_truefestgelegt:
y_true = np.array([0, 1, 1, 0]).reshape(-1, 1)→[[0],[1],[1],[0]] - Netzausgabe für ein Individuum berechnen:
y_pred = ann.forward(xor_inputs) - Fehler mit MSE (Mean Squared Error) berechnen:
mse = np.mean((y_true - y_pred) ** 2) - 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)print(f"Fitness Score: {fitness_function(ann, xor_inputs):.4f}") 3. EA implementierung und Training
Section titled “3. EA implementierung und Training”1. Individuum speichern
Section titled “1. Individuum speichern”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 9Wieder aufteilen (unpack)
def unpack(genome): W1 = genome[:6].reshape(2,3) W2 = genome[6:].reshape(3,1) return W1, W22. Population erstellen (Pool von Individuen)
Section titled “2. Population erstellen (Pool von Individuen)”Wir nehmen 30 Zufällige Individuen:
pop_size = 30population = []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)3. Fitness für Individuum
Section titled “3. Fitness für Individuum”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 2und dann können wir die Fitness-Liste für die ganze Population erstellen:
fitnesses = [fitness_of_genome(g) for g in population]4. Eltern auswählen
Section titled “4. Eltern auswählen”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.
5. Crossover (Kind aus zwei Eltern bauen)
Section titled “5. Crossover (Kind aus zwei Eltern bauen)”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 funktiondef 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 erzeugenchild = crossover(parent1, parent2)5. Mutation
Section titled “5. Mutation”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
pwird 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 anwendenchild = mutate(child, p=0.1, sigma=0.1)6. Kind bewerten und Population updaten
Section titled “6. Kind bewerten und Population updaten”# Fitness des Kindes berehcnechild_fit = fitness_of_genome(child)
# Schlechtestes Individuum in der Population findenworst_idx = int(np.argmin(fitnesses))
# Ersetzen, wenn das Kind besser istif child_fit > fitnesses[worst_idx]: population[worst_idx] = child
# Fitness-Wert aktualisieren:fitnesses[worst_idx] = child_fitDamit ist eine Runde des EA fertig: Eltern wählen → Crossover → Mutation → Kind bewerten → ggf. ersetzen.
7. Das Training
Section titled “7. Das Training”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 = 30rounds = 100report_points = [10, 20, 50, 100]
# Population erstellenpopulation = []for _ in range(pop_size): ann = ANN() genome = pack(ann.W1, ann.W2) population.append(genome)
# Fitness initial berechnenfitnesses = [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)# Starte Evolution...