From 90e67652ab715e72368c61f994de97a628ada731 Mon Sep 17 00:00:00 2001 From: Morten Rohgalf Date: Fri, 26 Sep 2025 14:18:00 +0200 Subject: [PATCH] final implementation of shc --- .../__pycache__/exoself.cpython-312.pyc | Bin 16058 -> 17924 bytes .../__pycache__/genotype.cpython-312.pyc | Bin 8474 -> 8420 bytes .../actors/__pycache__/neuron.cpython-312.pyc | Bin 6806 -> 6830 bytes .../stochastic_hillclimber/actors/best.json | 130 ++++++++++-------- .../stochastic_hillclimber/actors/cortex.py | 28 +--- .../stochastic_hillclimber/actors/exoself.py | 108 +++++---------- .../actors/experimental.json | 96 ------------- .../stochastic_hillclimber/actors/genotype.py | 122 +++------------- .../actors/morphology.py | 32 +---- .../stochastic_hillclimber/actors/neuron.py | 21 +-- .../stochastic_hillclimber/actors/scape.py | 16 +-- .../stochastic_hillclimber/actors/trainer.py | 95 ++++--------- 12 files changed, 186 insertions(+), 462 deletions(-) delete mode 100644 experiments/stochastic_hillclimber/actors/experimental.json diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/exoself.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/exoself.cpython-312.pyc index bfb5e11a74e2d83fc73bad3c43b9f12ac849d978..492570adc41353e383d1d8055a309c63c2c4521f 100644 GIT binary patch delta 1782 zcmai!YfxK76vuayd&3POfh6SSrVt(_A-PFPc@;`{wDduZBly7Sv`||iH&mcJn%vsT zgjOvx@&O&>*b3U2QfC}->VppI2mN3zEmECPkSY}0+7CMFhjwCHm3GF_-H^1MesMGV z+y6Ox_UvwQHlOzP4Pf2ZT5TdCEv@l~XZDqy(rv~3H^h7iy^aE?0ZXVsA>jt4WP%<% znAe9I3{rByD5V5aC4IoyqLK`KOoK@>k{Mn~B{cV=1Gqt`M=w(AC@i}xLk?`J}3PM>B9TU{a94h1?h2vB^0F;sT3vx(F{)ghC}O#Tuf{YwRVQ& zrcQrgk&#;caxjyIWl_{p&_f1SQshun6Nu^{IUovdq}W8UnWBbb3&leeK7vI}N7Hl7 zfn8(+ITTV8PgcAIA9m4z6gk+`+PQ0Ar`)=SgmrcI2%8Udg#vq8@*bSIMdCw;$lZTK zOV6XLT+%#epXKZqI0yWq-=Vy!NroCjA-rG^;5ALQWOL5js%LH0rybKJGxZH~w#NC4 z#zD=`M(c3y&mb-~Qn~Q5In6r-RDq>98aZMF7m20B90#jB| z!bOK2Ux$Ak7g>qxWx~+SnG+>-nCT%odsw*cG~+~2i6YB}S@INjFE| zg8}+Hj~jNseC^}Wzo*8lb}!#JvJSrYJeDaki%HFaki4s}NV7u+VLr7xt^x^6EdIquxx!J2PARNz~K&lF)_w(3mrI-)0uwy7qItL?-%XTd%P zb8dy1>H_R@G3N@a*AnAPSGv!`OeZju8aMGwmtbFkYP!@$^!YUGD`L*4tEjeNN=KBg zC_Pa>*=G&*PfX-zver@Zg#@c8)=_vVEEGox;IB2O@Cg_zvEmIAQzhjL9tA~tBbH%j z`4wz|%8DX94tpznrV+X%`hOJtD_T#@6Yy2VTKoklE05;f$jW5%Bb%_u7LErZhzzIe tqr*)7wwu(vu?;?{T&a4CE_@rVR1V_v&{gHtx3f5{Kkz4_W6(`z{{u08#1Q}h delta 826 zcmYk0Ur5tY6vyxR?f!o1+@{;yOs2-n<>q9%&E^096DK5EM#Unc_FHC>#x}HMBMBiN zR?Q9+XwhSk1d2} z_&=P}xfe7?!I)(B8o5J)#1As@3o`f>avqLD!Wz`cIYGUg8`PbZWbGvN=gGQB;y1{8 zM&lH@1pfSfVlyn(3J)yU=di#8jygUGQNY$ZqjAVfNT@CfQF2P+pYkJT;`K_KNTc+; z9)FtkkcDq52X=WS_Ez<(+o5bvlV@o((D3q{zqYUec$Vg;exNtF?!5k_)ZfTQ=FkqwSj5=GF zIP%e8duOn{&$i)`=d-w`(*2h3>46(pDv8$lFQp_}7pJ;>YfV+Qy`dte!;q4Q+Ofr$ z<=PHUuyI%0ZN6q$wu8?uEmyQKy~4CZTB&HWFte%!pOdcY^0~Hw&rMg$+my^auP*d? z=nF07G}u`tNx)YxC84x}8w>avX>z~BwF5YvX}Z(&r1OnTFaE7Di={JclTb593r9Ui z0mHg-Fg!RMj+`D21kuy90JHeLsR&GqVsit98Ek7m0X=xH{VVLjXonjf;(SMU?q1&B z!_mqyiyCh&Ji#vS?V?o4KG8KFOc55I^{2zz{^P&6-8guwvqZYfJMN>kGXX2O)LF0f PieQ=wZW10tc2@llQoq)h diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/genotype.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/genotype.cpython-312.pyc index 41d8f81bf65cf3dcf443d700dc4d71b1c0082fcb..39e3d3f9e47a0bd54bd85f1a0621819d21edd550 100644 GIT binary patch delta 311 zcmbQ`^u&?(G%qg~0}!l?y_T_gBaq9*3*<2a@n@dNk9f@(FHe@_yUge``7NKRAy8!u z69Yr7K&@bnK#gDu+Z?8eOg%g-49U#3g0%uQJSprc>{1LV9CO%eg=%=SxF_fH%d@+% zFx0RD*;D!58Lw`>%fFbBk!^CLpyA|0f(ID=CN~S66pTLTb|CNsLZ)5h2REhu^kq*Q_@W5V^xyT2|C=vw`+(1H; qwa9n#0x?rY#*od|#NFA&qJV<8Sd#MdbBgRiiVQ(S%w}Wx=ZpZbzg8ds delta 407 zcmaFjILnFmG%qg~0}w<2@V zI|45&*upN>Sb>S2`WW)|64{P(e z@J~K0yn``(a*c=-dyx-NZIRF96(Yurew*)z03Z*n6wH9B|2vK|+HHLWbk*L_L>{1q&7H1bpObs4u zBnnPI6FuNfV`4NM^yF2eiHZ^>6HPp6;t6~mJULTRbPoSN^ZnoV?d(4@Q=1-as(D#m z?E`F#^t|ui9eh;t1j0Y$8N4>207EE(td|%0D55=}=O_TDJOEsjF3Ls8Kxf+kz;$kD ziNQFsA!=-KA1)#P>&N0nFi=xr{>uYlvQrMFrHTduH&Uq<pkQWV*jmc8A0MQ?bE@D?x>H$n~xahM?6rKno5WCb6yQ^}N&?5yg(Aa|R? zxRB3anaRIaEGRl-=?-Q;4fdr3Dvy{}uHSX2kSn-UIIr7eQD}leqN2@F9AGRe_Sps9 zrIJ20WabTuY|C-US+q^6XH^^_4^TT)$tTnT!`DR({vuIv@8|+Tihscm!YfK>Oqoxl zzXMS@1!w(@rDNmA=L6xJ^=||5kFmt`s=J9htKY>k<7iU(tk&J|P53^jiCHx<-8!eH zK5p3jaAYR)Tzqrv#BA%yade|@OApOBFE7h`Y&nwu|Sg=M^?&626w4kgC`K6a3~ z+TL2x86Kp<1#1X5uq9r>13w1b%mPIYI|5v8e1d~9 z9{Ba5qUD-{4sPNAzc09x2Z@)Ea}K-IGh|T_*X#M5NhPf3jlv*BSx0wMVb+H$p)_nK zk3$%yXI9rCF&5_X9xkuWW_xYjae7VL&SqH;+sHw!RcaKEz~5cuymr{Tp3zbQ>+~y4 z^Bv&Aav+P^#?JlRLezWR0>->W<=cq;%OtMF{Kb|V_s6?+uwV?^W*6=wr=y980b!(c P^e5o?N4EfBN16N=m4NHv delta 950 zcmY*YO-vI(6rS1H-BK*=mQsFND5Z$85-LIQAEQQ11k?D3ic!~PmA zrr;l6M>^2OJBXY?)kHxq&vp?w!4i+`EDNsCC0(9k)!8Cp6mHi+0jPg_6eM=?mH5DE zTK05T0p13BnG+z#Q6p?7kG&yfxhV^{*+px~J=DCZJUQx0riO6dYQrv;zBi7jYb0f) zGmcGBI`2?4teNRj;n|NRX9xL(BCwJ~#0@P})&>VtmQF>}upKI6&C>HZ>Kd|)fjrJp z@vNcQ#1gw?{$;qDOp6KVCGW&oxQYRzzCZmFs%ltb zNg~P?m>_-1+7-(Z0PH0b%HeQ=F{v<;rNVI5M(hxcMzNdixSjk|VsJf)`dgVo`~3+R zAs74~TKLqGae*Q;WfdFYHindPFaMj{OCDBkmaw0N$*0QZOZ-+8X{L$mxL0o2hGjdN z)t|!QV#prKQ8a1{Qfa`J5v<}vju24uW hf*s^ks1~NlaHy%~1cWtXy-R@a-$n%pH)H;%{szE6=Li4* diff --git a/experiments/stochastic_hillclimber/actors/best.json b/experiments/stochastic_hillclimber/actors/best.json index fd0196d..f4b0250 100644 --- a/experiments/stochastic_hillclimber/actors/best.json +++ b/experiments/stochastic_hillclimber/actors/best.json @@ -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 - ] - } - ], - "output_ids": [ - 0.24149710385676537 - ] - }, - { - "id": 0.7030052650887313, - "layer_index": 0, - "cx_id": 0.5107239887106383, - "activation_function": "tanh", - "input_weights": [ - { - "input_id": 5.685637947817566e-10, - "weights": [ - 1.507492385500833, - -1.5181033637128052 - ] - } - ], - "output_ids": [ - 0.24149710385676537 - ] - }, - { - "id": 0.9936497204289092, - "layer_index": 1, - "cx_id": 0.5107239887106383, - "activation_function": "tanh", - "input_weights": [ - { - "input_id": 0.3776999353275148, - "weights": [ - 0.9998252528454215 + 6.283185307179586, + 6.283185307179586 ] }, { - "input_id": 0.7030052650887313, + "input_id": "bias", "weights": [ - -1.7243886895741118 + 6.280908700445529 ] } ], "output_ids": [ - 5.685637947817563e-10 + 0.43659818311553367 + ] + }, + { + "id": 0.06722681140391684, + "layer_index": 0, + "cx_id": 0.17818153832773553, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685438203051634e-10, + "weights": [ + 6.283185307179586, + 6.283185307179586 + ] + }, + { + "input_id": "bias", + "weights": [ + -6.283185307179586 + ] + } + ], + "output_ids": [ + 0.43659818311553367 + ] + }, + { + "id": 0.646276420829124, + "layer_index": 1, + "cx_id": 0.17818153832773553, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 0.2035391483142075, + "weights": [ + 6.283185307179586 + ] + }, + { + "input_id": 0.06722681140391684, + "weights": [ + -6.283185307179586 + ] + }, + { + "input_id": "bias", + "weights": [ + -6.283185307179586 + ] + } + ], + "output_ids": [ + 5.685438203051628e-10 ] } ] diff --git a/experiments/stochastic_hillclimber/actors/cortex.py b/experiments/stochastic_hillclimber/actors/cortex.py index 9197530..6cf2af5 100644 --- a/experiments/stochastic_hillclimber/actors/cortex.py +++ b/experiments/stochastic_hillclimber/actors/cortex.py @@ -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) diff --git a/experiments/stochastic_hillclimber/actors/exoself.py b/experiments/stochastic_hillclimber/actors/exoself.py index fd97b98..e53fdaa 100644 --- a/experiments/stochastic_hillclimber/actors/exoself.py +++ b/experiments/stochastic_hillclimber/actors/exoself.py @@ -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",)) diff --git a/experiments/stochastic_hillclimber/actors/experimental.json b/experiments/stochastic_hillclimber/actors/experimental.json deleted file mode 100644 index 286fe5e..0000000 --- a/experiments/stochastic_hillclimber/actors/experimental.json +++ /dev/null @@ -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 - ] - } - ] -} \ No newline at end of file diff --git a/experiments/stochastic_hillclimber/actors/genotype.py b/experiments/stochastic_hillclimber/actors/genotype.py index 188b81f..f6a5600 100644 --- a/experiments/stochastic_hillclimber/actors/genotype.py +++ b/experiments/stochastic_hillclimber/actors/genotype.py @@ -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,71 +14,25 @@ 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": , - "name": , - "vector_length": , - # optional: "scape": , - } - -- get_InitActuator(morphology) -> dict mit Feldern: - { - "id": , - "name": , - "vector_length": , - # optional: "scape": , - } - -Du kannst z.B. ein kleines Modul/Objekt übergeben, das diese Funktionen bereitstellt. -""" - - -# ----------------------------- -# Genotype-Erzeugung -# ----------------------------- def construct( - morphology_module, - hidden_layer_densities: List[int], - file_name: Optional[str] = None, - *, - add_bias: bool = False, + morphology_module, + hidden_layer_densities: List[int], + file_name: Optional[str] = None, + *, + 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"], "vector_length": int(S["vector_length"]), - "cx_id": None, # wird später gesetzt - "fanout_ids": [], # wird später gesetzt + "cx_id": None, # wird später gesetzt + "fanout_ids": [], # wird später gesetzt # optional: # "scape": S.get("scape") } @@ -90,20 +41,17 @@ def construct( "id": A.get("id", generate_id()), "name": A["name"], "vector_length": int(A["vector_length"]), - "cx_id": None, # wird später gesetzt - "fanin_ids": [], # wird später gesetzt + "cx_id": None, # wird später gesetzt + "fanin_ids": [], # wird später gesetzt # optional: # "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, @@ -147,39 +93,26 @@ def construct( def _create_neuro_layers( - cx_id: float, - sensor: Dict[str, Any], - actuator: Dict[str, Any], - layer_densities: List[int], - *, - add_bias: bool, + cx_id: float, + sensor: Dict[str, Any], + actuator: Dict[str, Any], + layer_densities: List[int], + *, + 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,40 +121,27 @@ 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" def _create_neural_input( - input_idps: List[Tuple[float, int]], - *, - add_bias: bool, + input_idps: List[Tuple[float, int]], + *, + 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", [])} diff --git a/experiments/stochastic_hillclimber/actors/morphology.py b/experiments/stochastic_hillclimber/actors/morphology.py index 688f409..3f9feae 100644 --- a/experiments/stochastic_hillclimber/actors/morphology.py +++ b/experiments/stochastic_hillclimber/actors/morphology.py @@ -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: diff --git a/experiments/stochastic_hillclimber/actors/neuron.py b/experiments/stochastic_hillclimber/actors/neuron.py index 14c488c..2b9aa5b 100644 --- a/experiments/stochastic_hillclimber/actors/neuron.py +++ b/experiments/stochastic_hillclimber/actors/neuron.py @@ -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) diff --git a/experiments/stochastic_hillclimber/actors/scape.py b/experiments/stochastic_hillclimber/actors/scape.py index 95afe57..9e435f0 100644 --- a/experiments/stochastic_hillclimber/actors/scape.py +++ b/experiments/stochastic_hillclimber/actors/scape.py @@ -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": diff --git a/experiments/stochastic_hillclimber/actors/trainer.py b/experiments/stochastic_hillclimber/actors/trainer.py index 24aeaca..65e51cc 100644 --- a/experiments/stochastic_hillclimber/actors/trainer.py +++ b/experiments/stochastic_hillclimber/actors/trainer.py @@ -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())