Adaptiver GA-Zyklus, Pool-Groessen-Panel, Konfig direkt aus cfg-Dateien
Zyklus/Farm: - neuer adaptiver Modus: grosse Startpopulation, die je Generation schrumpft (Start/Schrumpfung/MinPopulation) und bei Konvergenz stoppt (Geduld Generationen ohne Verbesserung >= Schwelle, max MaxGenerationen) - Zyklus.laden erkennt adaptives Profil am Feld 'Start' (Modus adaptiv), sonst klassisch (Abfolge/Anzahl); Farm._LaufStatisch/_LaufAdaptiv - neue Trunkierungs-Selektion Farm.PopulationBegrenzen - [Adaptiv]-Profil in zyklus.cfg (dokumentiert) Entwicklungsgrafik: - zweites Panel unter dem Verlauf zeigt die Anzahl erzeugter Verteilungen im Pool je Schritt (gemeinsame Zeitachse) Konfiguration: - platz.cfg entfernt; zyklus.cfg/strafen.cfg werden direkt aus $PLATZ_CFG geladen - neuer Schalter --zyklus-art (Default Adaptiv) waehlt das GA-Profil - web/main.py analog: direktes Laden, Profil via PLATZ_ZYKLUSART Doku (README.md, CLAUDE.md) und Tests entsprechend aktualisiert; Beispiele mit dem Adaptiv-Default neu berechnet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+181
-35
@@ -74,13 +74,30 @@ class Zyklus:
|
||||
wann sollen Objekte generiert, gekreuzt, mutiert und selektiert werden
|
||||
wieviele sind davon jeweils betroffen
|
||||
"""
|
||||
# adaptive Vorgaben, falls das Profil kein eigenes Feld setzt
|
||||
AdaptivDefaults = {
|
||||
'Start': 40, # Groesse der Startpopulation (Anfangswert)
|
||||
'MutationProLoesung': 2,# wie oft jede Loesung je Generation mutiert wird
|
||||
'Schrumpfung': 0.85, # Faktor, um den die Population je Generation faellt
|
||||
'MinPopulation': 5, # untere Schranke der Population
|
||||
'MaxGenerationen': 50, # harte Obergrenze der Generationen
|
||||
'Geduld': 3, # Generationen ohne Verbesserung bis zum Abbruch
|
||||
'Schwelle': 1.0 } # als Verbesserung zaehlende Mindestdifferenz
|
||||
|
||||
def __init__(self, Name='default',
|
||||
Abfolge='e,m,s',
|
||||
Anzahl='10,10,10'):
|
||||
self.Name = Name
|
||||
self.Abfolge = Abfolge.split(',')
|
||||
self.Anzahl = Anzahl.split(',')
|
||||
# 'statisch' = klassische Abfolge/Anzahl abspielen;
|
||||
# 'adaptiv' = Generationen-Schleife mit Abbruch bei Konvergenz.
|
||||
self.Modus = 'statisch'
|
||||
self.Adaptiv = {}
|
||||
def __repr__(self):
|
||||
if self.Modus == 'adaptiv':
|
||||
return( "Zyklus: %s (adaptiv)\n"
|
||||
" Parameter: %s\n" % ( self.Name, self.Adaptiv ) )
|
||||
return( "Zyklus: %s\n"
|
||||
" Zyklusabfolge: %s\n"
|
||||
" Anzahl : %s\n"
|
||||
@@ -89,15 +106,26 @@ class Zyklus:
|
||||
self.Anzahl ) )
|
||||
def laden(self, FileName, ZyklusName):
|
||||
config = configparser.ConfigParser()
|
||||
ZyklusPath= os.path.split( FileName )
|
||||
ZyklusPath= os.path.join( FileName )
|
||||
ZyklusPath= os.path.split( FileName )
|
||||
ZyklusPath= os.path.join( FileName )
|
||||
config.read( ZyklusPath )
|
||||
self.Name = ZyklusName
|
||||
# Adaptives Profil, sobald ein 'Start'-Feld vorhanden ist. Dann wird
|
||||
# die Aktionsabfolge nicht aus der Config gelesen, sondern von der
|
||||
# Farm generationenweise erzeugt (mit Abbruch bei Konvergenz).
|
||||
if config.has_option( ZyklusName, 'Start' ):
|
||||
self.Modus = 'adaptiv'
|
||||
self.Adaptiv = dict(self.AdaptivDefaults)
|
||||
for Feld in self.AdaptivDefaults:
|
||||
if config.has_option( ZyklusName, Feld ):
|
||||
self.Adaptiv[Feld] = ast.literal_eval(
|
||||
config.get( ZyklusName, Feld ) )
|
||||
return
|
||||
self.Modus = 'statisch'
|
||||
Abfolge = _StringAusdruck( config.get( ZyklusName, 'Abfolge' ))
|
||||
Anzahl = _StringAusdruck( config.get( ZyklusName, 'Anzahl' ))
|
||||
self.Abfolge = Abfolge.split(',')
|
||||
self.Anzahl = Anzahl.split(',')
|
||||
pass
|
||||
|
||||
class Entwicklung:
|
||||
"""Beobachtungsobjekt: zeichnet den Verlauf der Loesungsfindung auf.
|
||||
@@ -138,8 +166,14 @@ class Entwicklung:
|
||||
best, schnitt, schlecht, n))
|
||||
return r
|
||||
|
||||
def alsSVG(self, OutputSVG, Breite=900, Hoehe=500, Rand=60):
|
||||
"""zeichnet den aufgezeichneten Verlauf als SVG-Liniendiagramm"""
|
||||
def alsSVG(self, OutputSVG, Breite=900, Hoehe=650, Rand=60, PoolHoehe=150):
|
||||
"""zeichnet den aufgezeichneten Verlauf als SVG-Liniendiagramm
|
||||
|
||||
Oberes Panel: Wertigkeit (beste/durchschnittliche/schlechteste
|
||||
Loesung) ueber die Zyklus-Aktionen. Unteres Panel (PoolHoehe hoch):
|
||||
Anzahl der erzeugten Verteilungen im Pool je Schritt, auf derselben
|
||||
Zeitachse. Beide Panels teilen sich die X-Achse.
|
||||
"""
|
||||
if not self.Punkte:
|
||||
raise ValueError("keine Messpunkte aufgezeichnet")
|
||||
|
||||
@@ -150,9 +184,20 @@ class Entwicklung:
|
||||
if WMax == WMin:
|
||||
WMax = WMin + 1 # flache Linie nicht auf den Rand kleben
|
||||
|
||||
# Pool-Groessen (N je Messpunkt, Index 5)
|
||||
NMax = max( p[5] for p in self.Punkte )
|
||||
if NMax == 0:
|
||||
NMax = 1
|
||||
|
||||
N = len(self.Punkte)
|
||||
PlotBreite = Breite - 2 * Rand
|
||||
PlotHoehe = Hoehe - 2 * Rand
|
||||
# Oberes Panel (Wertigkeit) reicht von Rand bis kurz vor das
|
||||
# untere Panel; dazwischen ein kleiner Zwischenraum (Luecke).
|
||||
Luecke = 50
|
||||
OberUnten = Hoehe - Rand - PoolHoehe - Luecke # Unterkante oberes Panel
|
||||
OberHoehe = OberUnten - Rand
|
||||
UnterOben = OberUnten + Luecke # Oberkante unteres Panel
|
||||
UnterUnten = Hoehe - Rand # Unterkante unteres Panel
|
||||
|
||||
def X(i):
|
||||
if N == 1:
|
||||
@@ -161,7 +206,11 @@ class Entwicklung:
|
||||
|
||||
def Y(wert):
|
||||
# hoher (besserer) Wert oben, niedriger unten
|
||||
return Rand + PlotHoehe * (WMax - wert) / (WMax - WMin)
|
||||
return Rand + OberHoehe * (WMax - wert) / (WMax - WMin)
|
||||
|
||||
def YPool(n):
|
||||
# 0 an der Unterkante des unteren Panels, NMax an der Oberkante
|
||||
return UnterUnten - (UnterUnten - UnterOben) * n / NMax
|
||||
|
||||
def Polyline(Index, Farbe, Beschriftung):
|
||||
punkte = ' '.join( '%.1f,%.1f' % (X(i), Y(p[Index]))
|
||||
@@ -181,14 +230,15 @@ class Entwicklung:
|
||||
'fill="#333">Entwicklung der Loesungsfindung</text>\n'
|
||||
% (Breite // 2) )
|
||||
|
||||
# --- Oberes Panel: Wertigkeit ---
|
||||
# Achsen
|
||||
Teile.append(
|
||||
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#333" '
|
||||
'stroke-width="1.5"/>\n' % (Rand, Rand, Rand, Hoehe - Rand) )
|
||||
'<line x1="%d" y1="%d" x2="%d" y2="%.1f" stroke="#333" '
|
||||
'stroke-width="1.5"/>\n' % (Rand, Rand, Rand, OberUnten) )
|
||||
Teile.append(
|
||||
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#333" '
|
||||
'<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#333" '
|
||||
'stroke-width="1.5"/>\n'
|
||||
% (Rand, Hoehe - Rand, Breite - Rand, Hoehe - Rand) )
|
||||
% (Rand, OberUnten, Breite - Rand, OberUnten) )
|
||||
|
||||
# Y-Gitter + Beschriftung (5 Linien)
|
||||
for k in range(6):
|
||||
@@ -201,22 +251,10 @@ class Entwicklung:
|
||||
'<text x="%d" y="%.1f" text-anchor="end" font-size="11" '
|
||||
'fill="#666">%.0f</text>\n' % (Rand - 8, y + 4, wert) )
|
||||
Teile.append(
|
||||
'<text x="18" y="%d" text-anchor="middle" font-size="12" '
|
||||
'fill="#333" transform="rotate(-90 18 %d)">Wertigkeit '
|
||||
'(hoeher = besser)</text>\n' % (Hoehe // 2, Hoehe // 2) )
|
||||
Teile.append(
|
||||
'<text x="%d" y="%d" text-anchor="middle" font-size="12" '
|
||||
'fill="#333">Zyklus-Aktion (Zeit)</text>\n'
|
||||
% (Breite // 2, Hoehe - 18) )
|
||||
|
||||
# X-Beschriftung: Aktionsbuchstaben (bei wenigen Punkten alle,
|
||||
# sonst ausgeduennt, damit die Achse lesbar bleibt)
|
||||
Schritt = max(1, N // 30)
|
||||
for i, p in enumerate(self.Punkte):
|
||||
if i % Schritt == 0 or i == N - 1:
|
||||
Teile.append(
|
||||
'<text x="%.1f" y="%d" text-anchor="middle" font-size="10" '
|
||||
'fill="#999">%s</text>\n' % (X(i), Hoehe - Rand + 16, p[1]) )
|
||||
'<text x="18" y="%.1f" text-anchor="middle" font-size="12" '
|
||||
'fill="#333" transform="rotate(-90 18 %.1f)">Wertigkeit '
|
||||
'(hoeher = besser)</text>\n'
|
||||
% ((Rand + OberUnten) / 2, (Rand + OberUnten) / 2) )
|
||||
|
||||
# die drei Kurven
|
||||
Kurven = [
|
||||
@@ -238,6 +276,49 @@ class Entwicklung:
|
||||
'<text x="%d" y="%d" font-size="11" fill="#333">%s</text>\n'
|
||||
% (LegX + 30, y + 4, besch) )
|
||||
|
||||
# --- Unteres Panel: Anzahl erzeugter Verteilungen (Pool-Groesse) ---
|
||||
Teile.append(
|
||||
'<text x="%d" y="%.1f" text-anchor="middle" font-size="13" '
|
||||
'fill="#333">Anzahl erzeugter Verteilungen im Pool</text>\n'
|
||||
% (Breite // 2, UnterOben - 12) )
|
||||
# Achsen
|
||||
Teile.append(
|
||||
'<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#333" '
|
||||
'stroke-width="1.5"/>\n' % (Rand, UnterOben, Rand, UnterUnten) )
|
||||
Teile.append(
|
||||
'<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#333" '
|
||||
'stroke-width="1.5"/>\n'
|
||||
% (Rand, UnterUnten, Breite - Rand, UnterUnten) )
|
||||
# Y-Gitter + Beschriftung (0, Mitte, Max)
|
||||
for n in (0, NMax // 2, NMax):
|
||||
y = YPool(n)
|
||||
Teile.append(
|
||||
'<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#eee" '
|
||||
'stroke-width="1"/>\n' % (Rand, y, Breite - Rand, y) )
|
||||
Teile.append(
|
||||
'<text x="%d" y="%.1f" text-anchor="end" font-size="11" '
|
||||
'fill="#666">%d</text>\n' % (Rand - 8, y + 4, n) )
|
||||
# Poolgroessen-Kurve
|
||||
punkte = ' '.join( '%.1f,%.1f' % (X(i), YPool(p[5]))
|
||||
for i, p in enumerate(self.Punkte) )
|
||||
Teile.append(
|
||||
'<polyline points="%s" fill="none" stroke="#4060a0" '
|
||||
'stroke-width="2"/>\n' % punkte )
|
||||
|
||||
# X-Beschriftung: Aktionsbuchstaben unter dem unteren Panel (bei
|
||||
# wenigen Punkten alle, sonst ausgeduennt, damit die Achse lesbar
|
||||
# bleibt)
|
||||
Schritt = max(1, N // 30)
|
||||
for i, p in enumerate(self.Punkte):
|
||||
if i % Schritt == 0 or i == N - 1:
|
||||
Teile.append(
|
||||
'<text x="%.1f" y="%.1f" text-anchor="middle" font-size="10" '
|
||||
'fill="#999">%s</text>\n' % (X(i), UnterUnten + 16, p[1]) )
|
||||
Teile.append(
|
||||
'<text x="%d" y="%d" text-anchor="middle" font-size="12" '
|
||||
'fill="#333">Zyklus-Aktion (Zeit)</text>\n'
|
||||
% (Breite // 2, Hoehe - 12) )
|
||||
|
||||
Teile.append( '</svg>\n' )
|
||||
|
||||
fout = open( OutputSVG, 'w' )
|
||||
@@ -261,9 +342,22 @@ class Farm:
|
||||
self.Zyklus = Zyklus
|
||||
self.Klasse = Klasse
|
||||
self.Entwicklung = Entwicklung()
|
||||
for Aktion, Zahl in zip(Zyklus.Abfolge,Zyklus.Anzahl):
|
||||
#print 'Aktion:',Aktion
|
||||
#print 'Zahl:',Zahl
|
||||
if getattr(Zyklus, 'Modus', 'statisch') == 'adaptiv':
|
||||
self._LaufAdaptiv(Klasse, *Arguments, **keywords)
|
||||
else:
|
||||
self._LaufStatisch(Klasse, *Arguments, **keywords)
|
||||
|
||||
def _Aufzeichnen(self, Aktion):
|
||||
"""nach einer Aktion den aktuellen Pool-Zustand fuer die
|
||||
Entwicklungsgrafik erfassen. Der beobachtete Pool ist der, in dem
|
||||
gerade die Loesungen liegen: PoolNeu, sobald dort etwas gesammelt
|
||||
wurde, sonst Pool."""
|
||||
self.Entwicklung.erfassen(
|
||||
Aktion, self.PoolNeu if self.PoolNeu else self.Pool )
|
||||
|
||||
def _LaufStatisch(self, Klasse, *Arguments, **keywords):
|
||||
"""klassischer Ablauf: die Abfolge/Anzahl aus zyklus.cfg abspielen."""
|
||||
for Aktion, Zahl in zip(self.Zyklus.Abfolge, self.Zyklus.Anzahl):
|
||||
self.Position = self.Position + 1
|
||||
if Aktion == 'e':
|
||||
self.LoesungenErzeugen(int(Zahl), Klasse, *Arguments, **keywords)
|
||||
@@ -279,11 +373,54 @@ class Farm:
|
||||
self.NeueGeneration()
|
||||
else:
|
||||
pass
|
||||
# nach jeder Aktion den aktuellen Pool-Zustand aufzeichnen.
|
||||
# Der beobachtete Pool ist der, in dem gerade die Loesungen
|
||||
# liegen: PoolNeu, sobald dort etwas gesammelt wurde, sonst Pool.
|
||||
self.Entwicklung.erfassen(
|
||||
Aktion, self.PoolNeu if self.PoolNeu else self.Pool )
|
||||
self._Aufzeichnen(Aktion)
|
||||
|
||||
def _LaufAdaptiv(self, Klasse, *Arguments, **keywords):
|
||||
"""adaptiver Ablauf: viele Startloesungen, dann Generationen mit
|
||||
schrumpfender Population; Abbruch, sobald sich die beste Bewertung
|
||||
ueber 'Geduld' Generationen um weniger als 'Schwelle' verbessert."""
|
||||
P = self.Zyklus.Adaptiv
|
||||
Population = max( int(P['Start']), int(P['MinPopulation']) )
|
||||
|
||||
# Startpopulation erzeugen (der 'maximale Anfangswert')
|
||||
self.LoesungenErzeugen(Population, Klasse, *Arguments, **keywords)
|
||||
self._Aufzeichnen('e')
|
||||
|
||||
BesterWert = max(self.Pool).value
|
||||
OhneVerbesserung = 0
|
||||
for Gen in range(int(P['MaxGenerationen'])):
|
||||
self.Position = self.Position + 1
|
||||
|
||||
# Mutationskinder erzeugen (in PoolNeu) und die Eltern (Pool)
|
||||
# dazunehmen, so dass PoolNeu Eltern + Kinder als Kandidaten haelt.
|
||||
self.LoesungenMutieren( int(P['MutationProLoesung']) )
|
||||
self._Aufzeichnen('m')
|
||||
self.LoesungenBehalten() # PoolNeu += Pool
|
||||
self.NeueGeneration() # Pool = PoolNeu (Kandidaten), PoolNeu leer
|
||||
self._Aufzeichnen('j')
|
||||
|
||||
# Population fuer die naechste Generation schrumpfen (nie unter
|
||||
# MinPopulation) und den Kandidatenpool auf die besten K trunkieren.
|
||||
Population = max( int(P['MinPopulation']),
|
||||
int(Population * float(P['Schrumpfung'])) )
|
||||
self.PopulationBegrenzen( Population )
|
||||
self._Aufzeichnen('s')
|
||||
|
||||
# Konvergenz pruefen
|
||||
AktuellBest = max(self.Pool).value
|
||||
if AktuellBest - BesterWert >= float(P['Schwelle']):
|
||||
OhneVerbesserung = 0
|
||||
else:
|
||||
OhneVerbesserung = OhneVerbesserung + 1
|
||||
if AktuellBest > BesterWert:
|
||||
BesterWert = AktuellBest
|
||||
print("+ Generation %d: bester Wert %.1f, Population %d, "
|
||||
"ohne Verbesserung %d" % (Gen + 1, AktuellBest, len(self.Pool),
|
||||
OhneVerbesserung))
|
||||
if OhneVerbesserung >= int(P['Geduld']):
|
||||
print("+ Abbruch: %d Generationen ohne nennenswerte "
|
||||
"Verbesserung" % OhneVerbesserung)
|
||||
break
|
||||
def LoesungenErzeugen(self, N, Klasse, *Arguments, **keywords):
|
||||
""" Erzeuge Loesungen"""
|
||||
for i in range(N):
|
||||
@@ -324,6 +461,15 @@ class Farm:
|
||||
self.PoolNeu.append(L)
|
||||
self.Pool.remove(L)
|
||||
#print(self)
|
||||
def PopulationBegrenzen(self, K):
|
||||
"""Trunkierungs-Selektion: behalte im Pool nur die K besten
|
||||
Loesungen und verwirf den Rest. Anders als LoesungenAuswaehlen
|
||||
(das additiv nach PoolNeu verschiebt) veraendert dies den Pool an
|
||||
Ort und Stelle - fuer die adaptive Generationenschleife, damit die
|
||||
Population wirklich schrumpft."""
|
||||
K = max( 0, min( K, len(self.Pool) ) )
|
||||
self.Pool = sorted( self.Pool, reverse=True )[:K]
|
||||
print("+ Population auf %d beste begrenzt" % K)
|
||||
def LoesungenZufaelligAuswaehlen(self, N):
|
||||
"""Selektiere zufaellig Loesungen"""
|
||||
print("+ zufaellig auswaehlen -"+ repr(N) + "-")
|
||||
|
||||
+13
-13
@@ -11,7 +11,6 @@ __license__ = "Python"
|
||||
|
||||
from ga import *
|
||||
from Strukturdaten import *
|
||||
import configparser
|
||||
import optparse
|
||||
|
||||
|
||||
@@ -42,6 +41,9 @@ if __name__ == '__main__':
|
||||
help="zusaetzlich eine Entwicklung.svg im --indir Verzeichnis "
|
||||
"erzeugen: Verlauf der Loesungsfindung (Zeitachse nach rechts, "
|
||||
"beste/durchschnittliche/schlechteste Wertigkeit auf der Y-Achse)" )
|
||||
parser.add_option( "--zyklus-art", dest="zyklusart", default="Adaptiv",
|
||||
help="Name des GA-Profils in zyklus.cfg (Vorgabe: Adaptiv). "
|
||||
"Weitere mitgelieferte Profile: Easy, Simple" )
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.indir is None:
|
||||
@@ -50,18 +52,16 @@ if __name__ == '__main__':
|
||||
if not os.path.isdir( InDir ):
|
||||
parser.error( "--indir '%s' ist kein Verzeichnis" % InDir )
|
||||
|
||||
# lade Vorgaben fuer Optimierungszyklus und
|
||||
config = configparser.ConfigParser()
|
||||
ConfigPath= os.path.join( os.getenv('PLATZ_CFG'), 'platz.cfg')
|
||||
config.read( os.path.expandvars( ConfigPath ))
|
||||
|
||||
Zyklusconfig = os.path.normpath( config.get( 'Config', 'Zyklusdatei' ))
|
||||
Zyklusconfig = os.path.expandvars( Zyklusconfig )
|
||||
Zyklusart = config.get( 'Config', 'Zyklusart' )
|
||||
|
||||
Strafenconfig = os.path.normpath(config.get( 'Config', 'Strafendatei' ))
|
||||
Strafenconfig = os.path.expandvars( Strafenconfig )
|
||||
## print Strafenconfig
|
||||
# zyklus.cfg (GA-Profile) und strafen.cfg (Strafpunkte) direkt aus dem
|
||||
# per $PLATZ_CFG bestimmten cfg-Verzeichnis laden. Das GA-Profil waehlt
|
||||
# der Schalter --zyklus-art (Vorgabe: Adaptiv).
|
||||
CfgDir = os.getenv('PLATZ_CFG')
|
||||
if not CfgDir:
|
||||
parser.error( "Umgebungsvariable PLATZ_CFG ist nicht gesetzt "
|
||||
"(bin/setenv aufrufen)" )
|
||||
Zyklusconfig = os.path.join( CfgDir, 'zyklus.cfg' )
|
||||
Zyklusart = options.zyklusart
|
||||
Strafenconfig = os.path.join( CfgDir, 'strafen.cfg' )
|
||||
|
||||
# lade Eingabedateien
|
||||
Tischconfig = os.path.join( InDir, 'tische.ini' )
|
||||
|
||||
Reference in New Issue
Block a user