final implementation of shc

This commit is contained in:
2025-09-26 14:18:00 +02:00
parent 0ace1cec3c
commit 90e67652ab
12 changed files with 186 additions and 462 deletions

View File

@@ -1,95 +1,113 @@
{
"cortex": {
"id": 0.5107239887106383,
"id": 0.17818153832773553,
"sensor_ids": [
5.685637947817566e-10
5.685438203051634e-10
],
"actuator_ids": [
5.685637947817563e-10
5.685438203051628e-10
],
"neuron_ids": [
0.3776999353275148,
0.7030052650887313,
0.9936497204289092
0.2035391483142075,
0.06722681140391684,
0.646276420829124
]
},
"sensor": {
"id": 5.685637947817566e-10,
"id": 5.685438203051634e-10,
"name": "xor_GetInput",
"vector_length": 2,
"cx_id": 0.5107239887106383,
"cx_id": 0.17818153832773553,
"fanout_ids": [
0.3776999353275148,
0.7030052650887313
0.2035391483142075,
0.06722681140391684
]
},
"actuator": {
"id": 5.685637947817563e-10,
"id": 5.685438203051628e-10,
"name": "xor_SendOutput",
"vector_length": 1,
"cx_id": 0.5107239887106383,
"cx_id": 0.17818153832773553,
"fanin_ids": [
0.9936497204289092
0.646276420829124
]
},
"neurons": [
{
"id": 0.3776999353275148,
"id": 0.2035391483142075,
"layer_index": 0,
"cx_id": 0.5107239887106383,
"cx_id": 0.17818153832773553,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 5.685637947817566e-10,
"input_id": 5.685438203051634e-10,
"weights": [
-1.7328567115118854,
0.31546591460152307
6.283185307179586,
6.283185307179586
]
},
{
"input_id": "bias",
"weights": [
6.280908700445529
]
}
],
"output_ids": [
0.24149710385676537
0.43659818311553367
]
},
{
"id": 0.7030052650887313,
"id": 0.06722681140391684,
"layer_index": 0,
"cx_id": 0.5107239887106383,
"cx_id": 0.17818153832773553,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 5.685637947817566e-10,
"input_id": 5.685438203051634e-10,
"weights": [
1.507492385500833,
-1.5181033637128052
6.283185307179586,
6.283185307179586
]
},
{
"input_id": "bias",
"weights": [
-6.283185307179586
]
}
],
"output_ids": [
0.24149710385676537
0.43659818311553367
]
},
{
"id": 0.9936497204289092,
"id": 0.646276420829124,
"layer_index": 1,
"cx_id": 0.5107239887106383,
"cx_id": 0.17818153832773553,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 0.3776999353275148,
"input_id": 0.2035391483142075,
"weights": [
0.9998252528454215
6.283185307179586
]
},
{
"input_id": 0.7030052650887313,
"input_id": 0.06722681140391684,
"weights": [
-1.7243886895741118
-6.283185307179586
]
},
{
"input_id": "bias",
"weights": [
-6.283185307179586
]
}
],
"output_ids": [
5.685637947817563e-10
5.685438203051628e-10
]
}
]

View File

@@ -1,4 +1,3 @@
# actors/cortex.py
import time
from actor import Actor
@@ -12,12 +11,12 @@ class Cortex(Actor):
self.actuators = actuator_pids
self.exoself_pid = exoself_pid
self.awaiting_sync = set() # AIDs, von denen wir im aktuellen Schritt noch Sync erwarten
self.fitness_acc = 0.0 # akkumulierte Fitness über die Episode
self.ef_acc = 0 # akkumulierte EndFlags im aktuellen Schritt/Episode
self.cycle_acc = 0 # Anzahl abgeschlossener Sense-Think-Act Zyklen
self.active = False # aktiv/inaktiv (wartet auf reactivate)
self._t0 = None # Startzeit der Episode
self.awaiting_sync = set()
self.fitness_acc = 0.0
self.ef_acc = 0
self.cycle_acc = 0
self.active = False
self._t0 = None
async def _kick_sensors(self):
for s in self.sensors:
@@ -35,7 +34,6 @@ class Cortex(Actor):
self.active = True
async def run(self):
# Initialisierung: falls Actuatoren schon gesetzt sind, Episode starten
if self.actuators:
self._reset_for_new_episode()
await self._kick_sensors()
@@ -44,10 +42,8 @@ class Cortex(Actor):
msg = await self.inbox.get()
tag = msg[0]
# Optional: Exoself kann AIDs registrieren, bevor wir starten
if tag == "register_actuators":
_, aids = msg
# Map aids auf echte Actuatoren falls nötig; hier übernehmen wir sie direkt
self.awaiting_sync = set(aids)
if not self.active:
self._reset_for_new_episode()
@@ -56,7 +52,6 @@ class Cortex(Actor):
if tag == "sync" and self.active:
print("CORTEX: got sync message: ", msg)
# Aktuator meldet Schrittabschluss
_t, aid, fitness, halt_flag = msg
print("----------------")
@@ -66,36 +61,28 @@ class Cortex(Actor):
print("halt_flag:", halt_flag)
print("----------------")
# akkumulieren
self.fitness_acc += float(fitness)
self.ef_acc += int(halt_flag)
# diesen Aktuator als "eingetroffen" markieren
if aid in self.awaiting_sync:
self.awaiting_sync.remove(aid)
print("CORTEX: awaiting sync: ", self.awaiting_sync)
# Wenn alle Aktuatoren gemeldet haben, ist ein Zyklus fertig
if not self.awaiting_sync:
print("CORTEX: cycle completed.")
self.cycle_acc += 1
# Episodenende, wenn irgendein Aktuator EndFlag setzt
if self.ef_acc > 0:
elapsed = time.perf_counter() - self._t0
# An Exoself berichten
await self.exoself_pid.send((
"evaluation_completed",
self.fitness_acc,
self.cycle_acc,
elapsed
))
# inaktiv werden warten auf reactivate
self.active = False
# Flags für nächste Episode werden erst bei reactivate gesetzt
else:
# Nächsten Zyklus starten
self.ef_acc = 0
self._reset_for_new_cycle()
await self._kick_sensors()
@@ -103,17 +90,14 @@ class Cortex(Actor):
continue
if tag == "reactivate":
# neue Episode starten
self._reset_for_new_episode()
await self._kick_sensors()
continue
if tag == "terminate":
# geordnet alle Kinder beenden
for a in (self.sensors + self.actuators + self.neurons):
await a.send(("terminate",))
return
elif tag == "backup_from_neuron":
# vom Neuron an Cortex -> an Exoself weiterreichen
await self.exoself_pid.send(msg)

View File

@@ -1,4 +1,3 @@
# exoself.py
import asyncio
import json
import math
@@ -15,10 +14,6 @@ from scape import XorScape
class Exoself(Actor):
"""
Exoself übersetzt den Genotyp (JSON) in einen laufenden Phenotyp (Actors) und
steuert das simple Neuroevolution-Training (Backup/Restore/Perturb + Reactivate).
"""
def __init__(self, genotype: Dict[str, Any], file_name: Optional[str] = None):
super().__init__("Exoself")
self.g = genotype
@@ -40,29 +35,22 @@ class Exoself(Actor):
self.MAX_ATTEMPTS = 50
self.actuator_scape = None
# zuletzt perturbierte Neuronen (für Restore)
self._perturbed: List[Neuron] = []
# ---------- Convenience ----------
@staticmethod
def from_file(path: str) -> "Exoself":
with open(path, "r") as f:
g = json.load(f)
return Exoself(g, file_name=path)
# ---------- Public API ----------
async def run(self):
# 1) Netzwerk bauen
self._build_pid_map_and_spawn()
# 2) Cortex verlinken + starten
self._link_cortex()
# 3) Actors starten (Sensoren/Neuronen/Aktuatoren)
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors + [self.actuator_scape]:
self.tasks.append(asyncio.create_task(a.run()))
# 4) Hauptloop: auf Cortex-Events hören
while True:
msg = await self.inbox.get()
tag = msg[0]
@@ -75,23 +63,12 @@ class Exoself(Actor):
await self._terminate_all()
return
# in exoself.py, innerhalb der Klasse Exoself
async def run_evaluation(self):
"""
Eine einzelne Episode/Evaluation:
- baut & verlinkt das Netz
- startet alle Actors
- wartet auf 'evaluation_completed' vom Cortex
- beendet alles und liefert (fitness, evals, cycles, elapsed)
"""
# 1) Netzwerk bauen & Cortex verlinken
print("build network and link...")
self._build_pid_map_and_spawn()
print("link cortex...")
self._link_cortex()
# 2) Sensor/Neuron/Aktuator-Tasks starten (Cortex startete _link_cortex bereits)
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
self.tasks.append(asyncio.create_task(a.run()))
@@ -100,20 +77,16 @@ class Exoself(Actor):
print("network actors are running...")
# 3) Auf Abschluss warten
while True:
msg = await self.inbox.get()
print("message in exsoself: ", msg)
tag = msg[0]
if tag == "evaluation_completed":
_, fitness, cycles, elapsed = msg
# 4) Sauber terminieren
await self._terminate_all()
# Evals = 1 (eine Episode)
return float(fitness), 1, int(cycles), float(elapsed)
elif tag == "terminate":
await self._terminate_all()
# Falls vorzeitig terminiert wurde
return float("-inf"), 0, 0, 0.0
# ---------- Build ----------
@@ -127,23 +100,20 @@ class Exoself(Actor):
self.cx_actor = Cortex(
cid=cx["id"],
exoself_pid=self,
sensor_pids=[], # werden gleich gesetzt
sensor_pids=[],
neuron_pids=[],
actuator_pids=[]
)
self.actuator_scape = XorScape()
# Neuronen nach Layer gruppieren (damit outputs korrekt gesetzt werden)
layers: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
for n in self.g["neurons"]:
layers[n["layer_index"]].append(n)
ordered_layers = [layers[i] for i in sorted(layers)]
# Platzhalter: wir benötigen später Referenzen nach ID
id2neuron_actor: Dict[Any, Neuron] = {}
# Zuerst alle Neuronen erzeugen (ohne Outputs), damit wir Referenzen haben
for layer in ordered_layers:
for n in layer:
input_idps = [(iw["input_id"], iw["weights"]) for iw in n["input_weights"]]
@@ -157,16 +127,11 @@ class Exoself(Actor):
id2neuron_actor[n["id"]] = neuron
self.neuron_actors.append(neuron)
# Jetzt Outputs pro Layer setzen:
# - für Nicht-Output-Layer: Outputs = Neuronen der nächsten Schicht
# - für Output-Layer: Outputs = Aktuator(en) (setzen wir nachdem Aktuatoren erzeugt sind)
for li in range(len(ordered_layers) - 1):
next_pids = [id2neuron_actor[nx["id"]] for nx in ordered_layers[li + 1]]
for n in ordered_layers[li]:
id2neuron_actor[n["id"]].outputs = next_pids
# Aktuatoren anlegen (brauchen cx_pid und fanin_ids)
# Genotyp kann "actuator" (ein Objekt) oder "actuators" (Liste) haben wir unterstützen beides.
actuators = self._get_actuators_block()
if not actuators:
raise ValueError("Genotype must include 'actuator' or 'actuators'.")
@@ -184,14 +149,12 @@ class Exoself(Actor):
)
self.actuator_actors.append(actuator)
# Output-Layer Neuronen → Outputs = Aktuatoren
if ordered_layers:
last_layer = ordered_layers[-1]
out_targets = self.actuator_actors # Liste
out_targets = self.actuator_actors
for n in last_layer:
id2neuron_actor[n["id"]].outputs = out_targets
# Sensor(en) anlegen (brauchen cx_pid und fanout auf erste Schicht)
sensors = self._get_sensors_block()
if not sensors:
raise ValueError("Genotype must include 'sensor' or 'sensors'.")
@@ -224,24 +187,46 @@ class Exoself(Actor):
return [self.g["actuator"]]
return []
# ---------- Link ----------
def _link_cortex(self):
"""
Übergibt dem Cortex die fertigen Listen und setzt awaiting_sync.
Startet dann den Cortex-Task.
"""
self.cx_actor.sensors = [a for a in self.sensor_actors if a]
self.cx_actor.neurons = [a for a in self.neuron_actors if a]
self.cx_actor.actuators = [a for a in self.actuator_actors if a]
# Wichtig: vor Start die erwarteten AIDs setzen,
# damit der erste Sensor-Trigger nicht in eine leere awaiting_sync läuft.
self.cx_actor.awaiting_sync = set(a.aid for a in self.cx_actor.actuators)
# Cortex starten
self.tasks.append(asyncio.create_task(self.cx_actor.run()))
# ---------- Training-Loop Reaction ----------
async def train_until_stop(self):
self._build_pid_map_and_spawn()
self._link_cortex()
# 2) Start tasks
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
self.tasks.append(asyncio.create_task(a.run()))
if self.actuator_scape:
self.tasks.append(asyncio.create_task(self.actuator_scape.run()))
while True:
msg = await self.inbox.get()
tag = msg[0]
if tag == "evaluation_completed":
_, fitness, cycles, elapsed = msg
maybe_stats = await self._on_evaluation_completed(fitness, cycles, elapsed)
# _on_evaluation_completed() ruft bei Stop bereits _backup_genotype() und _terminate_all()
if isinstance(maybe_stats, dict):
# Trainingsende Daten aus self.* zurückgeben (wie im Buch: Fitness/Evals/Cycles/Time)
return (
float(self.highest_fitness),
int(self.eval_acc),
int(self.cycle_acc),
float(self.time_acc),
)
elif tag == "terminate":
await self._terminate_all()
return float("-inf"), 0, 0, 0.0
async def _on_evaluation_completed(self, fitness: float, cycles: int, elapsed: float):
self.eval_acc += 1
self.cycle_acc += int(cycles)
@@ -249,21 +234,20 @@ class Exoself(Actor):
print(f"[Exoself] evaluation_completed: fitness={fitness:.6f} cycles={cycles} time={elapsed:.3f}s")
if fitness > self.highest_fitness:
REL = 1e-6
if fitness > self.highest_fitness * (1.0 + REL):
self.highest_fitness = fitness
self.attempt = 0
# Backup aller Neuronen
for n in self.neuron_actors:
await n.send(("weight_backup",))
else:
self.attempt += 1
# Restore nur der zuletzt perturbierten Neuronen
for n in self._perturbed:
await n.send(("weight_restore",))
# Stop-Kriterium?
if self.attempt >= self.MAX_ATTEMPTS:
print(f"[Exoself] STOP. Best fitness={self.highest_fitness:.6f} evals={self.eval_acc} cycles={self.cycle_acc}")
print(
f"[Exoself] STOP. Best fitness={self.highest_fitness:.6f} evals={self.eval_acc} cycles={self.cycle_acc}")
await self._backup_genotype()
await self._terminate_all()
return {
@@ -273,7 +257,6 @@ class Exoself(Actor):
"time_acc": self.time_acc,
}
# Perturbiere Teilmenge der Neuronen
tot = len(self.neuron_actors)
mp = 1.0 / math.sqrt(max(1, tot))
self._perturbed = [n for n in self.neuron_actors if random.random() < mp]
@@ -281,25 +264,13 @@ class Exoself(Actor):
for n in self._perturbed:
await n.send(("weight_perturb",))
# Nächste Episode starten
await self.cx_actor.send(("reactivate",))
# ---------- Backup Genotype ----------
async def _backup_genotype(self):
"""
Holt von allen Neuronen die aktuellen Weights und schreibt sie in self.g.
Speichert optional in self.file_name.
"""
# 1) Request
remaining = len(self.neuron_actors)
for n in self.neuron_actors:
await n.send(("get_backup",))
# 2) Collect vom Cortex-Postfach (Neuronen senden an cx_pid → cx leitet an Exoself weiter
# oder du hast sie direkt an Exoself schicken lassen; falls direkt an Cortex, dann
# lausche hier stattdessen auf self.cx_actor.inbox. In deinem Neuron-Code geht es an cx_pid,
# und in deiner bisherigen Implementierung hast du aus dem Cortex-Postfach gelesen.
# Hier vereinfachen wir: Neuronen senden direkt an EXOSELF (passe Neuron ggf. an).
backups: List[Tuple[Any, List[Tuple[Any, List[float]]]]] = []
while remaining > 0:
@@ -309,8 +280,6 @@ class Exoself(Actor):
backups.append((nid, idps))
remaining -= 1
# 3) Update JSON
# exoself.py -> in _backup_genotype()
id2n = {n["id"]: n for n in self.g["neurons"]}
for nid, idps in backups:
if nid not in id2n:
@@ -324,17 +293,14 @@ class Exoself(Actor):
input_id, weights = item
new_iw.append({"input_id": input_id, "weights": list(weights)})
id2n[nid]["input_weights"] = new_iw
# Bias mit abspeichern (Variante B):
if bias_val is not None:
id2n[nid].setdefault("input_weights", []).append({"input_id": "bias", "weights": [bias_val]})
# 4) Save
if self.file_name:
with open(self.file_name, "w") as f:
json.dump(self.g, f, indent=2)
print(f"[Exoself] Genotype updated → {self.file_name}")
# ---------- Termination ----------
async def _terminate_all(self):
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
await a.send(("terminate",))

View File

@@ -1,96 +0,0 @@
{
"cortex": {
"id": 0.0873421806271496,
"sensor_ids": [
5.6856379409509e-10
],
"actuator_ids": [
5.685637940950896e-10
],
"neuron_ids": [
0.6384794610574114,
0.6023131059017351,
0.39277072802910173
]
},
"sensor": {
"id": 5.6856379409509e-10,
"name": "xor_GetInput",
"vector_length": 2,
"cx_id": 0.0873421806271496,
"fanout_ids": [
0.6384794610574114,
0.6023131059017351
]
},
"actuator": {
"id": 5.685637940950896e-10,
"name": "xor_SendOutput",
"vector_length": 1,
"cx_id": 0.0873421806271496,
"fanin_ids": [
0.39277072802910173
]
},
"neurons": [
{
"id": 0.6384794610574114,
"layer_index": 0,
"cx_id": 0.0873421806271496,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 5.6856379409509e-10,
"weights": [
-1.241701053140722,
-0.9643293453192259
]
}
],
"output_ids": [
0.20086494757397733
]
},
{
"id": 0.6023131059017351,
"layer_index": 0,
"cx_id": 0.0873421806271496,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 5.6856379409509e-10,
"weights": [
-0.38795737506820904,
-0.5125123920689245
]
}
],
"output_ids": [
0.20086494757397733
]
},
{
"id": 0.39277072802910173,
"layer_index": 1,
"cx_id": 0.0873421806271496,
"activation_function": "tanh",
"input_weights": [
{
"input_id": 0.6384794610574114,
"weights": [
-0.7426574958298033
]
},
{
"input_id": 0.6023131059017351,
"weights": [
-0.9823211499227558
]
}
],
"output_ids": [
5.685637940950896e-10
]
}
]
}

View File

@@ -6,9 +6,6 @@ import time
from typing import Dict, List, Tuple, Any, Optional
# -----------------------------
# Utilities / ID & Weights
# -----------------------------
def generate_id() -> float:
return random.random()
@@ -17,35 +14,6 @@ def create_neural_weights(vector_length: int) -> List[float]:
return [random.uniform(-2.0, 2.0) for _ in range(vector_length)]
# -----------------------------
# Morphology "Interface"
# -----------------------------
"""
Erwartete Morphologie-API (duck-typed):
- get_InitSensor(morphology) -> dict mit Feldern:
{
"id": <beliebig>,
"name": <sensor-funktionsname, z.B. "rng" oder "xor_GetInput">,
"vector_length": <int>,
# optional: "scape": <frei nutzbar>,
}
- get_InitActuator(morphology) -> dict mit Feldern:
{
"id": <beliebig>,
"name": <aktor-funktionsname, z.B. "pts" oder "xor_SendOutput">,
"vector_length": <int>,
# optional: "scape": <frei nutzbar>,
}
Du kannst z.B. ein kleines Modul/Objekt übergeben, das diese Funktionen bereitstellt.
"""
# -----------------------------
# Genotype-Erzeugung
# -----------------------------
def construct(
morphology_module,
hidden_layer_densities: List[int],
@@ -53,29 +21,12 @@ def construct(
*,
add_bias: bool = False,
) -> Dict[str, Any]:
"""
Baut einen Genotyp (JSON-kompatibles Dict) analog zur Erlang-Version:
- wählt Initial-Sensor & -Aktuator aus Morphologie
- bestimmt Output-Layer-Dichte = actuator.vector_length
- erzeugt alle Neuronen-Schichten & Gewichte
- setzt fanout_ids/fanin_ids
- erstellt Cortex mit Listen der IDs
- speichert optional in file_name
add_bias: Wenn True, hängt pro Neuron einen Bias als ('bias', b) an die Input-Liste an.
Dein bisheriges JSON nutzt KEIN Bias-Element. Standard=False für Kompatibilität.
"""
# Seed wie im Buch
rnd_seed = time.time_ns() & 0xFFFFFFFF
random.seed(rnd_seed)
# 1) Sensor & Aktuator aus Morphologie
S = morphology_module.get_InitSensor(morphology_module)
A = morphology_module.get_InitActuator(morphology_module)
# Pflichtfelder normalisieren
sensor = {
"id": S.get("id", generate_id()),
"name": S["name"],
@@ -96,14 +47,11 @@ def construct(
# "scape": A.get("scape")
}
# 2) Output-Layer-Dichte = actuator.vector_length (wie im Buch)
output_vl = actuator["vector_length"]
layer_densities = list(hidden_layer_densities) + [output_vl]
# 3) Cortex-ID festlegen
cortex_id = generate_id()
# 4) Neuronen-Schichten erzeugen
layers = _create_neuro_layers(
cx_id=cortex_id,
sensor=sensor,
@@ -112,7 +60,6 @@ def construct(
add_bias=add_bias,
)
# 5) Sensor/Aktuator mit Cortex/Fanout/Fanin verbinden
input_layer = layers[0]
output_layer = layers[-1]
@@ -122,7 +69,6 @@ def construct(
actuator["cx_id"] = cortex_id
actuator["fanin_ids"] = [n["id"] for n in output_layer]
# 6) Cortex-Objekt
neuron_ids = [n["id"] for layer in layers for n in layer]
cortex = {
"id": cortex_id,
@@ -154,32 +100,19 @@ def _create_neuro_layers(
*,
add_bias: bool,
) -> List[List[Dict[str, Any]]]:
"""
Baut alle Neuronen-Schichten.
- Erste Schicht erhält als Input den Sensor (Vektor).
- Mittlere Schichten erhalten skalare Inputs von voriger Schicht.
- Letzte Schicht verbindet zum Aktuator (output_ids = [actuator.id]).
"""
layers: List[List[Dict[str, Any]]] = []
# Platzhalter-Input: (input_id, vector_length)
input_idps: List[Tuple[float, int]] = [(sensor["id"], sensor["vector_length"])]
for layer_index, layer_density in enumerate(layer_densities):
# Neuron-IDs generieren
neuron_ids = [generate_id() for _ in range(layer_density)]
# output_ids: Standardmäßig IDs der nächsten Schicht (werden nach dem Layer bekannt)
if layer_index < len(layer_densities) - 1:
# IDs der nächste Schicht vorab erzeugen (für output_ids)
next_ids = [generate_id() for _ in range(layer_densities[layer_index + 1])]
# Aber: wir erzeugen hier *nur* die IDs für output_ids, nicht die Neuronen der nächsten Schicht
output_ids = next_ids
else:
# letzte Schicht → zum Aktuator
output_ids = [actuator["id"]]
# Layer-Neuronen erzeugen
this_layer: List[Dict[str, Any]] = []
for _nid in neuron_ids:
proper_input = _create_neural_input(input_idps, add_bias=add_bias)
@@ -188,27 +121,18 @@ def _create_neuro_layers(
"layer_index": layer_index,
"cx_id": cx_id,
"activation_function": "tanh",
# JSON-Konvention: [{"input_id": id, "weights": [...]}, ...]
"input_weights": [{"input_id": i, "weights": w} for (i, w) in proper_input if not _is_bias_tuple((i, w))],
"input_weights": [{"input_id": i, "weights": w} for (i, w) in proper_input],
"output_ids": output_ids[:], # Kopie
}
this_layer.append(neuron)
layers.append(this_layer)
# Inputs für nächste Schicht: jetzt sind es skalare Outputs der aktuellen Neuronen
input_idps = [(n["id"], 1) for n in this_layer]
# WICHTIG: oben haben wir für mittlere Layer „künstliche“ next_ids erzeugt, damit output_ids befüllt waren.
# In deiner Laufzeit-Topologie ignorieren wir diese Platzhalter und verdrahten später im Exoself die
# tatsächlichen Actor-Referenzen schichtweise. Für den Genotyp sind die output_ids der Zwischenlayer
# nicht kritisch (du überschreibst sie ohnehin beim Phenotype-Mapping). Die letzte Schicht zeigt korrekt auf den Aktuator.
return layers
def _is_bias_tuple(t: Tuple[Any, Any]) -> bool:
"""Hilfsfunktion falls du später Bias im JSON aufnehmen willst."""
key, _ = t
return isinstance(key, str) and key == "bias"
@@ -218,10 +142,6 @@ def _create_neural_input(
*,
add_bias: bool,
) -> List[Tuple[Any, List[float]]]:
"""
Wandelt Platzhalter (input_id, vector_length) in (input_id, weights[]) um.
Optional einen Bias als ('bias', [b]) anhängen (Erlang-Variante).
"""
proper: List[Tuple[Any, List[float]]] = []
for input_id, vl in input_idps:
proper.append((input_id, create_neural_weights(vl)))
@@ -232,9 +152,6 @@ def _create_neural_input(
return proper
# -----------------------------
# File I/O & Print
# -----------------------------
def save_genotype(file_name: str, genotype: Dict[str, Any]) -> None:
with open(file_name, "w") as f:
json.dump(genotype, f, indent=2)
@@ -253,7 +170,6 @@ def print_genotype(file_name: str) -> None:
nids = cx.get("neuron_ids", [])
aids = cx.get("actuator_ids", [])
# Indexe für schnellen Zugriff
nid2n = {n["id"]: n for n in g.get("neurons", [])}
sid2s = {g["sensor"]["id"]: g["sensor"]} if "sensor" in g else {s["id"]: s for s in g.get("sensors", [])}
aid2a = {g["actuator"]["id"]: g["actuator"]} if "actuator" in g else {a["id"]: a for a in g.get("actuators", [])}

View File

@@ -6,16 +6,10 @@ MorphologyType = Union[str, Callable[[str], List[Dict[str, Any]]]]
def generate_id() -> float:
"""
Zeitbasierte float-ID (ähnlich wie im Buch).
"""
now = time.time()
return 1.0 / now
# ------------------------------------------------------------
# Morphologie-API (duck-typed, wie im Erlang-Original)
# ------------------------------------------------------------
def get_InitSensor(morphology: MorphologyType) -> Dict[str, Any]:
sensors = get_Sensors(morphology)
if not sensors:
@@ -41,13 +35,6 @@ def get_Actuators(morphology: MorphologyType) -> List[Dict[str, Any]]:
def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict[str, Any]]]:
"""
Akzeptiert:
- eine Callable Morphologie-Funktion (z.B. xor_mimic)
- einen String (z.B. "xor_mimic") -> per Registry auflösen
- ein Modul-Objekt (z.B. import morphology) -> Standard 'xor_mimic' aus dem Modul
"""
# 1) Bereits eine Callable-Funktion? (z.B. xor_mimic)
if callable(morphology):
return morphology
@@ -55,14 +42,11 @@ def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict
if isinstance(morphology, str):
reg = {
"xor_mimic": xor_mimic,
# weitere Morphologien hier registrieren...
}
if morphology in reg:
return reg[morphology]
raise ValueError(f"Unknown morphology name: {morphology}")
# 3) Modul-Objekt: versuche eine Standard-Morphologie-Funktion daraus zu nehmen
# Hier nehmen wir 'xor_mimic' als Default (du kannst das generalisieren).
try:
# Ist es ein Modul mit einer Funktion 'xor_mimic'?
if hasattr(morphology, "xor_mimic") and callable(getattr(morphology, "xor_mimic")):
@@ -73,31 +57,23 @@ def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict
raise TypeError("morphology must be a callable, a module with 'xor_mimic', or a registered string key")
# ------------------------------------------------------------
# Beispiel-Morphologie: XOR (wie im Buch)
# ------------------------------------------------------------
def xor_mimic(kind: str) -> List[Dict[str, Any]]:
"""
Liefert je nach 'kind' ('sensors' | 'actuators') die Definitionen.
Felder sind so benannt, dass sie direkt mit unserem Genotype/Phenotype-Code
kompatibel sind (vector_length statt 'vl', etc.).
"""
if kind == "sensors":
return [
{
"id": generate_id(),
"name": "xor_GetInput", # Sensorfunktion (muss in deinem Sensor-Actor implementiert sein)
"name": "xor_GetInput",
"vector_length": 2,
"scape": {"private": "xor_sim"} # optional, falls du Scapes nutzt
"scape": {"private": "xor_sim"}
}
]
elif kind == "actuators":
return [
{
"id": generate_id(),
"name": "xor_SendOutput", # Aktuatorfunktion (muss in deinem Actuator-Actor implementiert sein)
"name": "xor_SendOutput",
"vector_length": 1,
"scape": {"private": "xor_sim"} # optional
"scape": {"private": "xor_sim"}
}
]
else:

View File

@@ -17,12 +17,21 @@ class Neuron(Actor):
# input_idps: [(input_id, [w1, w2, ...])]
self.inputs = {}
self.order = []
"""
for (inp_id, weights) in input_idps:
self.order.append(inp_id)
self.inputs[inp_id] = {"weights": list(weights), "got": False, "val": None}
# WICHTIG: Bias lernbar machen
self.bias = random.uniform(-2.0, 2.0)
# Für Backup/Restore
"""
self.bias = 0.0
for (inp_id, weights) in input_idps:
if inp_id == "bias":
self.bias = float(weights[0])
else:
self.order.append(inp_id)
self.inputs[inp_id] = {"weights": list(weights), "got": False, "val": None}
self._backup_inputs = None
self._backup_bias = None
@@ -61,13 +70,11 @@ class Neuron(Actor):
print(f"Neuron {self.nid}: input_sum={acc + self.bias:.3f}, output={out:.3f}")
elif tag == "get_backup":
# Packe Gewichte + Bias zurück
idps = [(i, self.inputs[i]["weights"]) for i in self.order]
idps.append(("bias", self.bias))
await self.cx_pid.send(("backup_from_neuron", self.nid, idps))
elif tag == "weight_backup":
# Tiefkopie der Gewichte + Bias
print(f"Neuron {self.nid}: backing up weights")
self._backup_inputs = {k: {"weights": v["weights"][:]} for k, v in self.inputs.items()}
self._backup_bias = self.bias
@@ -79,21 +86,17 @@ class Neuron(Actor):
self.bias = self._backup_bias
elif tag == "weight_perturb":
# In neuron.py weight_perturb, add:
print(f"Neuron {self.nid}: perturbing {len([w for i in self.order for w in self.inputs[i]['weights']])} weights")
# MP wie im Buch: 1/sqrt(TotalWeightsIncludingBias)
tot_w = sum(len(self.inputs[i]["weights"]) for i in self.order) + 1
mp = 1 / math.sqrt(tot_w)
delta_mag = 2.0 * math.pi
sat_lim = 2.0 * math.pi
# Gewichte perturbieren
for i in self.order:
ws = self.inputs[i]["weights"]
for j in range(len(ws)):
if random.random() < mp:
ws[j] = _sat(ws[j] + (random.random() - 0.5) * delta_mag, -sat_lim, sat_lim)
# Bias perturbieren
if random.random() < mp:
self.bias = _sat(self.bias + (random.random() - 0.5) * delta_mag, -sat_lim, sat_lim)

View File

@@ -1,4 +1,3 @@
# actors/scape.py
from actor import Actor
import math
@@ -14,7 +13,7 @@ class XorScape(Actor):
]
self.index = 0
self.error_acc = 0.0
self.last_actuator = None # <-- wer uns zuletzt "action" geschickt hat
self.last_actuator = None
async def run(self):
while True:
@@ -23,9 +22,8 @@ class XorScape(Actor):
if tag == "sense":
print("SCAPE: got sensed by sensor...")
# Sensor fragt nach Input
_, sid, from_pid = msg
self.last_actuator = from_pid # merken, wer beteiligt ist
self.last_actuator = from_pid
inputs, correct = self.data[self.index]
print("SENSOR input /correct: ", inputs, correct)
await from_pid.send(("percept", inputs))
@@ -33,25 +31,23 @@ class XorScape(Actor):
elif tag == "action":
_, output, from_pid = msg
_, correct = self.data[self.index]
# Calculate RMSE like the Erlang version
step_error = sum((o - c) ** 2 for o, c in zip(output, correct))
step_rmse = math.sqrt(step_error) # This is the key change!
step_rmse = math.sqrt(step_error)
print(f"XOR PATTERN: Input={self.data[self.index][0]} → Network={output} → Expected={correct}")
self.index += 1
if self.index >= len(self.data):
# Episode finished - calculate final fitness
total_rmse = math.sqrt(self.error_acc + step_rmse) # Not step_error!
total_rmse = math.sqrt(self.error_acc + step_rmse)
fitness = 1.0 / (total_rmse + 1e-5)
await from_pid.send(("result", fitness, 1))
self.index = 0
self.error_acc = 0.0
else:
# Continue episode
self.error_acc += step_rmse # Add RMSE, not raw error
self.error_acc += step_rmse
await from_pid.send(("result", 0.0, 0))
elif tag == "terminate":

View File

@@ -1,22 +1,14 @@
# trainer.py
import asyncio
import os
import time
from typing import Any, Dict, List, Tuple, Optional
import morphology # dein morphology.py
import morphology
from genotype import construct, save_genotype, print_genotype
from exoself import Exoself # deine Actor-basierte Exoself-Implementierung
from exoself import Exoself
class Trainer:
"""
Stochastischer Hillclimber wie im Erlang-Original.
- morphology_spec: entweder das morphology-Modul, ein String-Key, oder eine Callable-Morphologie
- hidden_layer_densities: z.B. [4, 3]
- max_attempts / eval_limit / fitness_target: Stoppbedingungen
- experimental_file / best_file: Pfade, falls du wie im Buch speichern willst
"""
def __init__(
self,
morphology_spec=morphology,
@@ -36,12 +28,8 @@ class Trainer:
self.fitness_target = fitness_target
self.experimental_file = experimental_file
self.best_file = best_file
# Wenn deine Exoself/Cortex eine feste Anzahl Zyklen pro „Evaluation“ braucht,
# kannst du hier default 0 lassen (Cortex entscheidet über Halt-Flag),
# oder einen Wert setzen.
self.exoself_steps_per_eval = exoself_steps_per_eval
# Laufende Akkus (wie im Erlang-Code)
self.best_fitness = float("-inf")
self.best_genotype: Optional[Dict[str, Any]] = None
@@ -49,57 +37,38 @@ class Trainer:
self.cycle_acc = 0
self.time_acc = 0.0
async def _run_one_attempt(self) -> Tuple[float, int, int, float, Dict[str, Any]]:
"""
Ein Trainingsversuch:
- Genotyp konstruieren
- Exoself laufen lassen
- Fitness/Evals/Cycles/Time zurückgeben + den verwendeten Genotyp
"""
async def _run_one_attempt(self) -> Tuple[float, int, int, float]:
print("constructing genotype...")
geno = construct(self.morphology_spec, self.hds, file_name=self.experimental_file, add_bias=True)
# Exoself starten und bis zum evaluation_completed warten
geno = construct(
self.morphology_spec,
self.hds,
file_name=self.experimental_file, # <-- schreibt Startnetz nach experimental.json
add_bias=True
)
fitness, evals, cycles, elapsed = await self._evaluate_with_exoself(geno)
return fitness, evals, cycles, elapsed, geno
async def _evaluate_with_exoself(self, genotype: Dict[str, Any]) -> Tuple[float, int, int, float]:
"""
Startet Exoself (deine Actor-basierte Variante) und wartet,
bis der Cortex die Evaluation abgeschlossen hat.
Erwartete Rückgabe: fitness, evals, cycles, elapsed
"""
print("creating exoself...")
ex = Exoself(genotype)
# Exoself.run() sollte idealerweise einen Tuple (fitness, evals, cycles, time)
# liefern. Falls deine Version aktuell „backup“-Listen liefert, ersetze das hier
# mit der passenden Logik oder benutze das „evaluation_completed“-Signal aus dem Cortex.
fitness, evals, cycles, elapsed = await ex.run_evaluation()
return fitness, evals, cycles, elapsed
async def _evaluate_with_exoself(self, genotype: Dict[str, Any]) -> Tuple[float, int, int, float]:
print("creating exoself...")
ex = Exoself(genotype, file_name=self.experimental_file)
best_fitness, evals, cycles, elapsed = await ex.train_until_stop()
return best_fitness, evals, cycles, elapsed
async def go(self):
"""
Entspricht dem Erlang loop/…:
Wiederholt Versuche, bis Stoppbedingung erfüllt.
"""
attempt = 1
while True:
print(".........")
print("current attempt: ", attempt)
print(".........")
# Stoppbedingung vor Versuch?
if attempt > self.max_attempts or self.eval_acc >= self.eval_limit or self.best_fitness >= self.fitness_target:
# Ausgabe wie im Buch
if self.best_genotype and self.best_file:
# bestes Genotypfile ausgeben/„drucken“
save_genotype(self.best_file, self.best_genotype)
# Abschlussausgabe wie im Buch
if self.best_file and os.path.exists(self.best_file):
print_genotype(self.best_file)
print(
f" Morphology: {getattr(self.morphology_spec, '__name__', str(self.morphology_spec))} | "
f"Best Fitness: {self.best_fitness} | EvalAcc: {self.eval_acc}"
)
# Optional: an „Benchmarker“ melden bei dir vermutlich nicht nötig
return {
"best_fitness": self.best_fitness,
"eval_acc": self.eval_acc,
@@ -109,43 +78,35 @@ class Trainer:
}
print("RUN ONE ATTEMPT!")
# --- Ein Versuch ---
fitness, evals, cycles, elapsed, geno = await self._run_one_attempt()
fitness, evals, cycles, elapsed = await self._run_one_attempt()
print("update akkus...")
# Akkus updaten
self.eval_acc += evals
self.cycle_acc += cycles
self.time_acc += elapsed
# Besser als bisher?
if fitness > self.best_fitness:
# „experimental.json“ → „best.json“ (semantisch wie file:rename(...))
self.best_fitness = fitness
self.best_genotype = geno
if self.best_file:
save_genotype(self.best_file, geno)
# Reset Attempt-Zähler (wie Erlang: Attempt=0 nach Verbesserung)
if self.best_file and self.experimental_file and os.path.exists(self.experimental_file):
os.replace(self.experimental_file, self.best_file)
attempt = 1
else:
attempt += 1
# --------------------------
# Beispiel: ausführen
# --------------------------
if __name__ == "__main__":
# Beispielkonfiguration (XOR-Morphologie, kleine Topologie)
trainer = Trainer(
morphology_spec=morphology, # oder morphology.xor_mimic
hidden_layer_densities=[2], # wie im Buch-Beispiel
max_attempts=1000,
morphology_spec=morphology,
hidden_layer_densities=[2],
max_attempts=200,
eval_limit=float("inf"),
fitness_target=float("inf"),
fitness_target=99.9,
experimental_file="experimental.json",
best_file="best.json",
exoself_steps_per_eval=0, # 0 => Cortex/Scape steuern Halt-Flag
exoself_steps_per_eval=0,
)
asyncio.run(trainer.go())