final implementation of shc
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",))
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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", [])}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user