Ein Junction-Tree ist eine
Tree-
oder auch
Hypertree-Decomposition
eines Graphen
1).
Um
eine solche Tree-Decomposition erstellen zu können muss
der Graph
trianguliert sein bzw. in einen triangulierten Graphen
überführt werden.
Definition
trianguliert:
Ein Graph ist trianguliert wenn er
keinen abkürzungsfreien zyklischen Pfad mit
mehr als drei Knoten
(entsprechend >3 Kanten) beinhaltet.
|
Definition zyklisch:
Ein Pfad ist zyklisch wenn er von einem
Knoten A
über weitere
Knoten (z.B. B,
C, D) zurück
zum Knoten A
führt. Wobei jeder
Zwischenknoten nur einmal in einem Zyklus vorkommen
darf.
|
Definition
abkürzungsfrei:
Abkürzungsfrei
ist ein solcher zyklischer Pfad, wenn keine zwei
Knoten dieses Pfades
durch eine
Kante verbunden sind, die nicht schon Teil des
Pfades ist.
|
Ist ein
Graph nicht trianguliert so wird er durch zusätzliche
Kanten, die
sogenannten
Fill-Ins
in einen
triangulierten Graphen überführt. Wie in der
Einleitung
bereits beschrieben gibt es verschiedene Kriterien nach denen
die
Güte einer Decomposition bestimmt werden kann. Das
allgemeinste
Kriterium ist die
Tree-Width, welche durch die
Anzahl der Knoten der größten (=knotenreichsten)
Clique minus
1 bestimmt ist.
Ein offensichtliches, aber - insbesondere während
eines Suchlaufs nach einer möglichst guten Triangulierung
-
normalerweise aufwändiger zu
berechnendes Kriterium ist die Anzahl der zusätzlich
benötigten
Fill-Ins.
Bei
der Erstellung eines Junction-Trees ist die Größe
der
benötigten Wahrscheinlichkeitstafeln das eigentliche
Zielkriterium
- da dieses aber noch aufwändiger zu berechnen ist, wird
oft auf
einfachere Kriterien hin optimiert, mit der (zumeist durchaus
berechtigten)
Hoffnung, dass der entstehende Junction-Tree auch im Hinblick
auf das
zuletzt genannte Kriterium relativ gut ist.
Triangulierte
und nicht triangulierte
Graphen
Ganz links: Nicht
triangulierter
Graph, da der Zyclus A-B-C-D(-A)
nicht abkürzbar ist. Mitte links:
Triangulierter Graph - A-B-C-D(-A)
hat eine Abkürzung A-C. Der
Graph hätte z.B. durch die Eliminierungsreihenfolge
B, D,
A, C aus dem ersten Graphen
entstanden sein können. Mitte
rechts: Nicht triangulierter
Graph: A-B-C-D(-A)
ist nicht (direkt)
abkürzbar, genausowenig wie A-B-C-E(-A)
und A-E-C-D(-A). Ganz rechts:
Triangulierter Graph, A-C
stellt für alle zuvor
genannten Zyklen eine Abkürzung dar. Eine Kante B-D alleine
hätte dagegen aus dem vorletzten Graphen keinen
triangulierten
Graphen gemacht. Der Graph hätte z.B. durch die
Eliminierungsreihenfolge B, D, A, C,
E aus dem vorletzten Graphen
entstanden sein können, während z.B. die
Reihenfolge C, A, B,
D, E zu einer sehr viel schlechteren
Triangulierung mit drei Fill-Ins geführt hätte.
Um einen triangulierten Graphen und gleichzeitig die Cliquen
zu
bestimmen wird zumeist der
Eliminierungs-Algorithmus benutzt.
Dieser
wird hier nochmals kurz dargestellt:
Eliminierungs-Algorithmus
(ohne
Optimierungen)
- Wähle einen Knoten X
- Verbinde paarweise alle
Nachbarn
von X durch eine Kante (wenn noch
nicht verbunden)
- Sofern X und
alle
Nachbarn von X nicht schon Teil einer
zuvor gefundenen
Clique
sind bilden sie eine neue Clique
- Entferne (eliminiere) X (und
damit auch alle Kanten an X)
- Wiederhole die Schritte 1-4 bis kein Knoten mehr
übrig ist
Beliebig schlecht wird die
Triangulierung
und damit der entstehende
Junction-Tree, wenn im Schritt 1. tatsächlich ein
beliebiger
Knoten zufällig ausgewählt wird. Die einfachste
Heuristik um
eine möglichst gute Triangulierung
zu erreichen ist die
One-Step-Look-Ahead-Heuristik
(auch
Greedy-Search
genannt)
. Hierbei wird vor der
eigentlichen Eliminierung jeder verbliebene
Knoten "ausprobiert" (d.h. virtuell eliminiert) und dann
der beste hinsichtlich des
gewählten Kriteriums (z.B. wenigste
Fill-Ins, kleinste
resultierende
Cliquen-Größe, kleinste
Tafel-Größe,...
2))
zur Eliminierung ausgewählt. Ist der Graph bereits
trianguliert,
so ist sichergestellt, dass dieses Verfahren keine
zusätzlichen
Fill-Ins
einfügt (zumindest
bei
Verwendung des Kriteriums "wenigste
Fill-Ins").
Triangulierungen
Ganz
links: Nicht triangulierter
Ausgangs-Graph (der Zyklus A-B-C-D(-A)
ist nicht abkürzbar). Mitte links:
Triangulierter Graph, wie er entsteht wenn man
zunächst Knoten B oder D
eliminiert. Mitte rechts:
Triangulierter
Graph, wie er entsteht wenn man zunächst Knoten A
oder C eliminiert Ganz
rechts: Triangulierter Graph, der nicht durch
eine
Eliminierungsreihenfolge gewonnen werden kann,
allerdings ist auch nur
eine der beiden Fill-In-Kanten zur Triangulierung
notwendig (wie in den
beiden mittleren Graphen ersichtlich wird).
Umgekehrt ist es allerdings nicht
möglich alle zulässigen Triangulierungen durch eine
Eliminierungsreihenfolge abzubilden. Man kann ja z.B. beliebig
viele
überflüssige Kanten einfügen - so ist z.B. ein
vollständig verbundener Graph immer eine korrekte
Triangulierung
eines Ausgangsgraphen, selbst wenn dieser aus N Knoten ohne
eine
einzige Kanten
bestünde (also vollständig unverbunden wäre).
Es gibt
Autoren die darauf verweisen, dass die optimale Triangulierung
teilweise nicht durch eine Eliminierungsreihenfolge abgebildet
werden
könne. Ich habe dafür allerdings noch kein Beispiel
gesehen...
Es ist jedoch nicht besonders schwierig Beispiele zu
konstruieren bei
denen der oben beschriebe 1-Step-Lookahead Algorithmus nicht
die
optimale Triangulierung findet (siehe. Abbildung unten).
Suboptimale
Lösungen mit der
One-Step-Lookahead-Heuristik
Oben:
In diesem Graphen ist kein
Knoten mit weniger als einem Fill-In
eliminierbar. Für die Heuristik (mit Anzahl Fill-Ins als
Optimierungskriterium)
scheinen die Knoten A,
C, D, I, J, K und X daher
zunächst alle als
gleich gute Kandidaten. Wird (zufällig) X gewählt
führt das aber
zum Einfügen der völlig
überflüssigen Fill-In-Kante B-L. Würde
stattdessen mit D
begonnen ergäbe sich im
Folgenden zwangsläufig eine optimale Reihenfolge
ohne
überflüssige Fill-Ins
(2 Fill-Ins
sind ja
tatsächlich notwendig). Unten:
In diesem Graphen wird auf jeden Fall zuerst Knoten X
ausgewählt, da alle anderen
Knoten mindestens 3 Fill-Ins
zur Folge hätten (z.B. für Knoten D: A-H, A-C, H-C). Nach wie vor
führt dies
jedoch zu der überflüssigen Kante B-L und stellt
damit offensichtlich
eine suboptimale Lösung dar.
Betrachten wir noch einmal etwas
allgemeiner den Eliminierungsansatz: Man kann einen Graphen
G immer in zwei
Teilgraphen G
1
und
G2
zerlegen
indem man
G an einer
trennenden Menge von Knoten
X1...Xn
aufspaltet. Die trennenden Knoten
X1...Xn
sind dabei Teil beider Subgraphen - also sowohl in G
1
als
auch in
G2
enthalten. Die Knoten
X1...Xn
müssen dabei vollständig verbunden werden,
d.h. jedes noch nicht durch eine Kante verbundene Knotenpaar
Xi,Xj aus
X1...Xn
muss
durch eine
Fill-In-Kante
verbunden
werden. Führt man dieses Verfahren rekursiv auf allen
entstehenden Sub-Graphen durch, bis keine weiteren
Aufspaltungen mehr
möglich sind, ist garantiert, dass der Ausgangsgraph
G durch alle
eingefügten
Fill-Ins
in einen triangulierten
Graph
Gt überführt
werden kann.
Definition trennende
Knotenmenge:
Eine Knotenmenge ist dann trennend
wenn es keinen Pfad von einem Knoten aus G1
zu einem Knoten
aus G2
gibt der
nicht über (mind.) einen der trennenden
Knoten X1...Xn
führt
|
Definition
vollständig verbundene
Knotenmenge:
Eine Menge von Knoten X1...Xn
ist
vollständig
verbunden,
wenn
alle
Paare Xi,Xj aus X1...Xn
durch eine Kante Xi-Xj
verbunden
sind.
|
Der Eliminierungsansatz ist damit ein Spezialfall
dieses
Verfahrens: Es
wird dabei nämlich jeweils ein Sub-Graph abgespalten der
nur aus
genau einem Knoten
X0
plus der trennenden Knotenmenge
X1...Xn
besteht, wobei
X1...Xn
natürlich genau die Nachbarn von
X0
in
G
sind.Da der entstehende Sub-Graph für
X0 durch die
Fill-Ins
bereits vollständig verbunden ist (
X0 ist
logischer Weise
bereits mit all seinen Nachbarn
verbunden und diese werden durch
Fill-Ins
vollständig verbunden, soweit dies noch nicht der Fall
ist),
braucht er nicht weiter aufgespalten zu werden. Im oben
gezeigten
Beispielgraphen könnten jeweils durch Aufspaltung am
trennenden
Knoten
X (oder auch
B oder
L) zwei Teilgraphen
entstehen.
Solche trennenden Knotenmengen, die nur aus einem einzigen
Knoten
bestehen sind noch relativ leicht zu identifizieren und
können als
eine Optimierung in das Verfahren eingebaut werden. Leider ist
dies
für komplexere Knotenmengen so nicht durchführbar
(jedenfalls
ist mir kein effektives Verfahren bekannt).
Aufspalten
eines Graphen an einer
trennenden Knotenmenge
Dies
ist ein Beispiel für eine
(allerdings suboptimale) Aufspaltung des oben gezeigten
Beispielgraphen
in zwei Teilgraphen. Da der eine Teilgraph wesentlich
kleiner ist
könnte man auch sagen, dass die Knoten D und H (zusammen mit der
trennenden
Knotenmenge {A, E, G,
C}
abgespalten wurden. Diese Aufspaltung ist suboptimal, da
4 Fill-Ins (A-G, A-C, E-G und E-C) benötigt
werden (sind in
beiden Teilgraphen enthalten), während eine
Aufspaltung an den
trennenden Knoten B,
X oder L überhaupt
keine Fill-Ins
benötigt hätte.
Auch der Knoten D
alleine
hätte sich an den trennenden Knoten {A, H, C} abspalten
lassen, wodurch
nur 3 Fill-Ins
(A-H, A-C und H-C) benötigt
worden
wären (das entspricht einer normalen Eliminierung
des Knotens D)
Die beiden oben gezeigten Beispielgraphen
legen noch eine
weitere relativ einfache Optimierungsvariante nahe:
Re-compiliere einzelne
Cliquen, wobei jeder Separator zu einer Nachbar-Clique im
Hypertree
jeweils in mindestens einer der entstehenden neuen Cliquen
vollständig erhalten sein muss, damit der entstehende
Sub-Hypertree anstelle der ursprünglichen Clique
eingefügt
werden kann. Dazu nimmt man den Sub-Graphen der nur die Knoten
der
gerade bearbeiteten Clique enthält und verbindet
paarweise alle
Knoten die einem gemeinsamen Separator angehören und
führt
auf diesem Sub-Graphen das oben beschriebene
One-Step-Lookahead-Verfahren durch. Im oben gezeigten Beispiel
würde die Clique
{B,
X, L}
so in die Cliquen
{B, X}
und
{X, L}
aufgespalten
3).
Leider ist der
oben gezeigte Graph da offenbar eine seltene Ausnahme, denn in
allgemeinen, komplexeren Graphen führt das Verfahren
praktisch nie
zu einer
Aufspaltung bestehender Cliquen. Normalerweise verbinden
suboptimale
Fill-Ins auch selten Knoten die in keinem gemeinsamen Zyklus
enthalten
sind. Meiner Beobachtung nach scheitert das Verfahren zumeist
daran,
dass durch die ein Clique verbindenden Separatoren bereits ein
vollständig verbundener
Sub-Graph erzwungen wird, der dann nur in einer (nämlich
der
bereits gefundenen) Clique aufgehen kann...
Suboptimaler
vs. optimaler Hypertree
Ein möglicher
suboptimale Hypertree, wie er mit der
One-Step-Lookahead-Heuristik, für den zweiten oben
gezeigten
Graphen entsteht. Angedeutet ist eine optimale
Lösung, bei der
statt
einer Clique {B,X,L} zwei Cliquen {B,X} und {X,L}
entstehen. Zu sehen
ist auch, dass die Baumstruktur für die beiden
spiegelsymmetrischen linken und rechten Teilgraphen bei
gleicher
Qualität durchaus unterschiedlich ausfallen kann.
Aufwändigere Optimierungsverfahren
benutzen z.B.
Simulated-Annealing
[3],
Evolutionäre Algorithmen
[2],
Branch-and-Bound-Verfahren
oder
Ant-Colony-Optimization
[1].
Hierbei ist zumeist die Eliminierungsreihenfolge (
A, B, C, D, ...) der
Knoten
Gegenstand der Optimierung (und nicht z.B. die Menge der
einzufügenden
Fill-Ins).
Einige der genannten Verfahren werden im Folgenden ganz kurz
vorgestellt.
Weitere Verfahren zielen darauf ab den Graphen zunächst
zu
vereinfachen (
[4]), oder mithilfe
"sicherer
Separatoren" in Teilgraphen zu zerlegen (
[5]),
jeweils
ohne
die
maximal
entstehende
Cliquen-Größe
zu
beeinflussen.
Um
tatsächlich
einen
Hypertree
(z.B. Junction-Tree)
zu erzeugen (und nicht z.B. nur die maximale
Tree-Width
abzuschätzen)
müssen die angewendeten Vorverarbeitungsschritte
natürlich
reversibel sein.
Wieder andere Verfahren versuchen bereits bestehende Cliquen
eines
Hypertrees nachträglich in kleinere Cliquen aufzuspalten.
Zumindest theoretisch lässt sich mit solchen Verfahren
ein
Hypertree auch komplett konstruieren, wenn man mit einem
Hypertree
beginnt, welcher nur eine Clique umfasst, die alle Knoten des
Ausgangsgraphen enthält
4).
Ant-Colony-Optimization (ACO):
Die Grundidee ist, dass sich Individuen (
Ameisen) auf einer
Topologie von
Spots und
jeweils zwei
Spots
verbindenden
Trails
bewegen
5).
Die
Trails haben
einen
A-Priori-Wert,
der anfänglich
der einzige Wert
ist der die Attraktivität eines
Trails bestimmt. Ausgehend von
einer Startposition (einem Start-
Spot)
darf
dann
jede
Ameise
zufällig
einen
Pfad über
die
Spots suchen,
wobei die
Entscheidung für einen
Trail
zwar
zufällig ist, jedoch mit einer durch die
A-Priori-Werte
bestimmten Wahrscheinlichkeit vorgenommen wird.
Sind alle
Ameisen an
ihrem
Ziel (wie immer das auch definiert ist)
angekommen wird für jede
Ameise die Güte ihrer Lösung
bestimmt (wobei die Lösung dem gelaufenen
Pfad entspricht).
Ensprechend dieser Güte darf dann jede
Ameise Pheromon auf den
Trails ihres
Pfades ablegen. Dieser
Pheromonwert jedes
Trails summiert
sich auf, unterliegt jedoch auch einer "
Verdunstung", die
dafür
sorgt, dass sich zwischen zwei Läufen der Pheromonwert
jedes
Trails jeweils um einen bestimmten Prozentsatz verringert.
Für die
weiteren Läufe, bei denen alle
Ameisen erneut Lösungen
suchen, indem sie von ihrer Startposition ausgehend einen
Pfad zu
der/einer gültigen Zielposition suchen, bestimmt sich die
Attraktivität eines
Trails
neben
dem
A-Priori-Wert
auch durch die
Menge des dort abgelegten (und noch nicht verdunsteten)
Pheromons. Die
Wahrscheinlichkeit
am
Spot S einen (noch)
möglichen
Trail Trailx aus
der Menge
der von
S abgehenden
Trails zu nehmen ist
bestimmt durch:
P(Trailx|S)
= |
Prior(Trailx|S)Beta
* Pheromon(Trailx|S)Alpha
|
|
Prior(Traili|S)Beta
* Pheromon(Traili|S)Alpha |
i |
|
|
Der Nenner ist dabei eine Normierungskonstante, die durch die
Summe der
Werte aller noch möglichen von S abgehenden Trails
bestimmt wird.
Alpha und
Beta sind
zwei Werte, die die Wichtigkeit des
Pheromons bzw.
der
A-Priori-Werte
bestimmen.
Diese Formel kann auch für den
ersten Lauf verwendet werden, wenn ein initialer
Pheromon-Level
für all
Trails vorgegeben
wird.
Es existieren verschiedene Abwandlungen und Erweiterungen, die
z.B.
den
Pheromonwert der
Trails auf
einen minimalen und
einen maximalen Wert begrenzen,
oder nur der/den beste(n)
Ameise(n)
des letzten Laufs/aller vorhergehenden Läufe erlauben
Pheromon abzulegen. Das
Verfahren
zeigt zum Beispiel für das Traveling-Salesman-Problem
gute
Ergebnisse.
In unserem Fall soll die Ameise ja eine
Eliminierungsreihenfolge der Knoten finden, d.h. die
Spots sind die (zu
eliminierenden) Knoten des moralischen Graphen.
Zusätzlich wird
ein Start-
Spot eingeführt,
von
dem aus alle Ameisen starten und von dem aus jeder andere Spot
erreichbar ist, der jedoch selbst keinem
Knoten des moralischen Graphen entspricht. Eine Pfadsuche
einer Ameise
ist beendet sobald sie alle
Spots
einmal
besucht hat, wobei sie keinen
Spot zweimal besuchen
darf (
Trails die
dazu
führen würden dürfen nicht benutzt werden). Der
Pfad ist
dann eine
Eliminierungsreihenfolge.
Nun ist das Problem, dass es keinen Sinn
macht die
Ameisen zunächst
rein
zufällig loslaufen zu lassen, da die Wahrscheinlichkeit
hierbei
irgendwelche brauchbaren Lösungen zu finden im
Allgemeinen so gering ist, dass man nahezu
unendlich viele
Ameisen (Lösungen)
generieren
müsste. Andererseits versagt die Möglichkeit
die
Ameisen durch
einen fest an jeden
Trail gehängten
A-Priori-Wert zu
führen, da
die Qualität /
Güte eines nächsten
Spots (zu
eliminierenden Knotens) von allen zuvor eliminierten Knoten
abhängt.
So kann der Pfad
A-> B
-> C -> D -> E sehr gut sein, während
A -> E -> C
-> D -> B sehr schlecht ist. Im zweiten Fall
kann gerade
die
Eliminierung von
D
nach
C zu einer
besonders schlechten
Decomposion
führen, während dieselbe Teilreihenfolge im ersten
Fall noch
besonders gut war. Daher bestimmen die Ameisen den
A-Priori-Wert aller
von einem
Spot S abgehenden
Trails selbst durch
einen
One-Step-Lookahead-Algorithmus
sobald sie an diesem
Spot S angelangt
sind. Leider scheint genau diese Herangehensweise
problematisch zu
sein, wie die unten gezeigten Grafiken verdeutlichen. Die
initiale
Lösungsmenge nach dem ersten Lauf entspricht
erwartungsgemäß genau dem Spektrum, welches auch
ein
N
mal wiederholter
One-Step-Lookahead-Algorithmus
liefern würde. Danach
folgt eine Phase in der zwar systematisch bessere
Lösungen
gefunden werden, jedoch treten gleichzeitig auch besonders
schlechte
Lösungen auf, die sehr viel schlechter sind als die
schlechtesten
Lösungen der initialen
Lösungsmenge des ersten Laufs. Da das
Pheromon naturgemäß
nicht
die Historie des bereits zurückgelegten Pfads
berücksichtigt,
sondern (so wie normalerweise auch der
A-Prior-Wert)
fest bestimmten
Trails
zugeordnet
ist, kann es
Ameisen
offenbar
auch schwer in die Irre führen. Danach folgt oft eine
Phase in
der die Lösungen aller
Ameisen gegen ein und dieselbe Lösung konvergieren, also
alle
Ameisen
demselben
Pfad folgen, diese Lösung aber relativ schlecht
ist. Diese Lösung wird besser wenn nur einem bestimmten
Prozentsatz
der besten Ameisen erlaubt wird Pheromon abzulegen und zwar
umso
besser, je kleiner dieser Prozentsatz ist (z.B. nur die besten
5% der
Ameisen).
Allerdings
vergrößert sich damit auch die Gefahr das Optimum
zu
verpassen und in einem lokalen Minimum hängen zu bleiben.
Andererseits lässt sich auch das Zusammenfallen der
unterschiedlichen Lösungen, zu nur noch einer einzigen
Lösung,
mit geeigneten Parametereinstellungen oft verhindert. Das
Problem, dass
nach der Anfangsphase keine besseren Lösungen mehr
gefunden werden
bleibt jedoch bestehen. So wird die beste Lösung oft nach
wenigen
Läufen erreicht, diese ist jedoch bei komplexen Graphen
oft noch
weit weg vom globalen Optimum...
.
Decomposition mit
Ant-Colony-Optimization
Jeweils oben ist die Güteverteilung
der Lösungen über alle Ameisen (Individuals)
und Läufe
(Genarations) zu sehen. Dunkelblau sind dabei die besten
Lösungen, rot die schlechtesten. Darunter (Mitte)
ist der Verlauf
der besten Lösung, sowie die gemittelte Güte
der besten 5%
bzw. besten 10% der Lösungen über die
Läufe
(Generations) aufgetragen. Ganz unten sind in derselben
Weise die
beste, die schlechteste, sowie die besten 25%, die
schlechtesten 25%
und der Mittelwert aller Lösungen aufgetragen. Links: die
Lösungen aller Ameisen
kollabieren zwischen der 20. und 25. Iteration zu einer
einzigen
Lösung, welche nicht besonders gut ist -
insbesondere auch
deutlich schlechter als das zwischenzeitlich erreichte
Optimum von hier
586 Fill-Ins,
welches bereits
nach 4 Iterationen gefunden wurde.Rechts: das
Kollabieren der Lösungen
konnte zumindest bis zur 100. Iteration verhindert
werden, jedoch
konvergiert das Verfahren nicht, das (lokale) Optimum
(hier bei 584 Fill-Ins)
wird weiterhin eher am
Anfang erreicht (hier nach 4 Iterationen).
Evolutionäre Algorithmen
(EA):
Bei evolutionären Algorithmen werden (mehr oder
weniger)
zufällig eine initiale Menge von Lösungen (Individuen)
erzeugt. Durch bestimmte Rekombinations-Operatoren
können jeweils
zwei Individuen einen
oder
mehrere Nachkommen erzeugen, welche
zusätzlich noch durch Mutations-Operatoren
verändert werden
können. Die Menge der Individuen einer Generation sowie die Menge aller neu
erzeugten
Nachkommen bildet die nächste Generation,
wobei die (gemäß einer Bewertungsfunktion)
schlechtesten Individuen
ausgesondert
werden, so dass jeweils nur eine vorgegebene
Anzahl von Individuen "überlebt".
Auf
diese Art können gute Individuen viele Generationen lang
überleben, während schlechte Nachkommen
unmittelbar wieder aus der Population herausfallen.
Evolutionärer
Optimierungs-Algorithmus (in der hier verwendeten
Form)
Folgende Parameter werden definiert:
- Population:
eine initiale Menge von Individuen (also
den zu optimierenden Objekten)
- Recombinator:
ein Operator, der aus zwei Individuen einen
Nachkommen erzeugen kann.
- Fitness:
ein
Operator, der jedem Individuum einen
Fitness-Wert
zuordnet,
der die Güte des Individuums (der Lösung)
beschreibt.
- maxGenerations:
die maximale Anzahl von Generationen die erzeugt wird
- maxPopulation:
die maximale Anzahl von Individuen die
gleichzeitig in der Population "leben" dürfen;
sind mehr enthalten werden die schlechtesten
verworfen ("sterben")
- maxChildren:
die maximale Anzahl von neuen Individuen,
die durch den Recombinator
in
einer Generation
erzeugt
werden (sind dann mehr als maxPopulation Individuen enthalten
greift der gerade beschriebene
Filter-Mechanisimus)
- aging:
ein
Offset der in jeder Generation die
ein Individuum
bereits
überlebt hat, auf die Fitness-Bewertungen
dieses Individuums
addiert
wird.
- satisfyingFitness:
wird ein Individuum erzeugt und mit ein Fitness-Wert
bewertet, der mindestens diesen Wert hat, dann wird
die Suche abgebrochen, da ein
ausreichen gutes Individuum gefunden wurde.
Algorithmus:
- Solange noch nicht maxGenerations erzeugt wurden und
noch kein Individuum mit
mindestens der vorgegebenen satisfyingFitness
gefunden wurde:
- Erhöhe die Bewertung aller Individuen, die
derzeit in der Poulation sind um den Wert aging
- Über alle IndividuenI1
der aktuellen Population
(beste zuerst):
- Über alle IndividuenI2
der aktuellen Population
(beste zuerst):
- benutze den Recombinator
um einen Nachkommen I12 aus I1
und I2 zu erzeugen, bewerte ihn mit dem Fitness-Operator
und füge I12
der Population hinzu
- Wenn der Recombinator
nicht symmetrisch ist, dann erzeuge auch
noch einen Nachkommen I21 aus I2
und I1, bewerte
auch diesen und füge I21 ebenfalls
der Population hinzu
- Wenn bereits maxChildren
Nachkommen erzeugt wurden breche ab (ggf. auch schon
vor dem zuletzt
beschriebenen
Schritt)
Anmerkungen:
- Der Mutations-Operator,
der
erzeugte Individuen noch zufälligen
Veränderungen unterwirft, wird
hier als Teil des Recombinators
gesehen,
auch wenn in diesem nicht "fest-verdrahtet" sondern
austauschbar implementiert werden sollte.
- Man kann sich z.B. auch
Meta-Recombinator-Operatoren
vorstellen die selbst nur bei jeder "Paarung" unter
mehreren
verfügbaren Rekombinations-Operatoren einen
auswählen und
anwenden, um so evtl. eine noch größere
Vielfalt an
Individuen zu erzeugen.
Es gibt viele Standard-Rekombinations-
und Mutations-Operatoren
wobei
einige auch speziell für Reihenfolgeprobleme geeignet
sind. Die
Ergebnisse für eine kleine Auswahl dieser Operatoren
sind unten
abgebildet. Die genaue Beschreibung würde den Rahmen
dieser kurzen
Einführung sprengen, kann aber in [2]
nachgelesen werden.
Grundsätzlich zeigen die evolutionären Algorithmen
eine klare
Konvergenz hin zu immer besseren Lösungen, wobei die
Lösungen
auch ganz am Ende i.d.R. noch alle unterschiedlich, wenn
auch alle
(fast)
gleich gut sind. Für hinreichend komplexe Netze sind
die
Lösungen praktisch immer denen mit ACO gefundenen
überlegen.
Die Konvergenzgeschwindigkeit
kann durch stärker spezialisierte
Operatoren z.T. noch erheblich gesteigert werden. Der EOT-Recombinator (siehe
Grafik
unten), der
zwar erheblich mehr Rechenzeit pro Generation erfordert,
dies aber
durch die
schnellere Konvergenz wieder mehr als wettmacht, ist ein
Beispiel
für einen solchen stark spezialisierten Operator. Er
kombiniert
dabei nicht direkt zwei Eliminierungsreihenfolgen, wie die
anderen
erprobten Rekombinations-Operatoren. Stattdessen kombiniert
er die aus
Eliminierungsreihenfolgen resultierenden Strukturen (die
sich ja durch
verschiedene Triangulierungen, also durch ihre
Fill-In-Kanten
unterscheiden), um aus einer "rekombinierten" Struktur
wieder eine neue
Eliminierungsreihenfolge abzuleiten (z.B. mithilfe eines 1-Step-Lookahead-Verfahrens).
Der folgende Kasten erläutert den Algorithmus im
Detail:
EOT-Recombinator
Folgende Parameter werden definiert:
- pSuccess:
die Wahrscheinlichkeit, dass zwei Eltern-Individuen
in einer Generation
überhaupt ein Kind generieren (ein allgemeiner
Parameter, den alle
Recombinator-Operatoren haben)
- pTakeFrom1:
die Wahrscheinlichkeit, dass eine Fill-In-Kante
übernommen wird,
die nur in genau einem Eltern-Individuum vorkommt
(der optimale Wert
hierfür scheint zumeist bei 0 zu liegen, so
dass dieser Parameter
auch weggelassen werden kann und entsprechend
niemals Fill-Ins
übernommen werden, die nur in einem
Eltern-Inidividuum vorkommen)
- pTakeFrom2:
die Wahrscheinlichkeit, dass eine Fill-In-Kante
übernommen wird,
die in beiden Eltern-Individuen vorkommt (der
optimale Wert
hierfür scheint zumeist bei 1, oder zumindest
sehr nahe an 1 zu
liegen, so dass auch dieser Wert nicht zwingend
parametrisierbar
gehalten werden muss und solche Fill-Ins immer
übernommen werden
können)
- Mutator:
der
Mutations-Operator, der auf der erzeugten
Elimination-Order noch
Veränderungen machen darf6)
Weitere Parameter, die für einen intern
benötigten
1-Step-Lookahead-Algorithmus
sinnvoll sind:
- Ein Rauschen,
welches
die Bewertung eines Knotens bei der Frage, welcher
Knoten als nächstes
eliminiert werden soll beeinflusst.
- maxFillIns:
eine maximal erlaubte Anzahl von Fill-Ins bei deren
Überschreitung
abgebrochen und letztlich kein Nachkomme erzeugt
wird.
Algorithmus:
- Erzeuge einen zufälligen Wert zwischen 0 und
1 -
wenn dieser kleiner pSuccess ist,
brich ab und liefere keinen Nachkommen zurück
(wenn man sicherstellen
will, dass tatsächlich mit genau dieser
Wahrscheinlichkeit ein Nachkommen-Individuum erzeugt
wird, kann man
diese Prüfung nicht bereits hier am Anfang,
sondern - in
modifizierter
Form - erst am Ende machen, wenn man sicher ist,
dass der Algorithmus
nicht aus anderen Gründen abgebrochen wurde -
z.B. wegen zu vieler
Fill-Ins)
- Erzeuge ein Netz mit denselben Kanten wie sie im
Ursprungsnetz enthalten waren (ohne Fill-Ins)7)
- Übernehme in dieses Netz von beiden
Eltern-Netzen die Fill-In-Kanten entsprechend den
Wahrscheinlichkeiten pTakeFrom1 und pTakeFrom2
- Benutze einen 1-Step-Lookahead-Algorithmus (ggf.
mit
verrauschten Knoten-Bewertungen, s.o.) um eine
Eliminierungsreihenfolge
und zusätzlich nötige Fill-Ins für
das erzeugte
Nachkommen-Netz zu bestimmen (theoretisch kan hier
auch ein anderer
Optimierungs-Algorithmus als 1-SLAT benutzt werden,
nur sollte er
möglichst performant sein)
- Wende den Mutator
auf
diese Eliminierungsreihenfolge an.
- Wenn der Mutator
die
Eliminierungsreihenfolge noch verändert hat,
dann verwerfe das Nachkommen-Netz
und benutze erneut einen
1-Step-Lookahead-Algorithmus
um ausgehend vom Originalnetz (ohne Fill-Ins!), die
Triangulierung und
damit die entsprechenden Fill-Ins, gemäß
der mutierten
Eliminierungsreihenfolge, für das
Nachkommen-Netz zu bestimmen
- Gib das erzeugte Netz als Nachkommen der beiden
Ausgangsnetze (Individuen) zurück.
.
Decomposition mit
Evolutionären
Algorithmen
Jeweils oben ist die
Güteverteilung
der Lösungen über alle Individuen und
Genarationen zu
sehen. Dunkelblau sind dabei die besten Lösungen,
rot die
schlechtesten. Darunter (Mitte) ist der Verlauf der
besten Lösung,
sowie die gemittelte Güte der besten 5% bzw. besten
10% der
Lösungen
über die Läufe (Generations) aufgetragen. Ganz
unten sind in
derselben
Weise die beste, die schlechteste, sowie die besten 25%,
die
schlechtesten 25% und der Mittelwert aller Lösungen
aufgetragen. Anders als
bei den Beispielen für den ACO-Ansatz wurde hier
die Tree-Width
als
Optimierungskriterium verwendet, was aber keinen
grundsätzlichen
Unterschied der Ansätze darstellt. Ganz links:
Recombinator = POS, Mutator = ISM. Mitte links:
Recombinator = OX1, Mutator = ISM. Mitte rechts:
Recombinator = OX2, Mutator = ISM. Ganz rechts:
Recombinator = EOT, Mutator = ISM.
Simulated Annealing:
Dieser Ansatz wurde von mir nicht selbst getestet. Es werden
aber in
der Literatur gute Ergebnisse berichtet (
[3]). Beim Simulated
Annealing
(simuliertem Auskühlen) werden zufällige
Schwankungen auf
einer Menge initialer Lösungen implementiert. Diese
Schwankungen
dürfen abhängig von einer Temperatur
T eine Lösung auch
verschlechtern. Wird dieses zulässige Maß
überschritten, dann wird die Veränderung der
Lösung
verworfen und die unveränderte Lösung beibehalten.
Über
die Zeit (entsprechend den Generationen beim
evolutionären
Algorithmen) wird die Temperatur immer kleiner, womit die
Lösungen immer weniger stark schwanken dürfen. Der
Ansatz
zeigt einige Ähnlichkeiten zu evolutionären
Algorithmen, so
dass zu erwarten ist, dass
Simulated Annealing nicht grundsätzlich geeigneter ist
als
evolutionäre Algorithmen. Für andere
Problemstellungen wird
sogar berichtet, dass evolutionäre Algorithmen solchen
die auf
Simulated Annealing basieren i.d.R. überlegen sind.
1) |
|
Wobei für Bayes'sche Netze der
zugrundeliegende Graph nicht direkt der
Directec Acyclig Graph (DAG) des Bayes'schen Netzes
ist, sondern der
Moralische Graph, welcher entsteht, indem alle
gerichteten Kanten durch
ungerichtete ersetzt und alle Eltern-Knoten
gemeinsamer Kind-Knoten
durch moralische Kanten verbunden werden. |
2) |
|
Wobei hier das
eigentliche Zielkriterium etwas abgewandelt werden
muss: so wird der
Knoten als nächstes eliminiert, mit dem die
kleinste Clique
entsteht, um am Ende die kleinste Tree-Width
zu erhalten, oder es wird der Knoten eliminiert mit
dem die wenigsten Fill-Ins
entstehen in der Hoffnung
damit insgesamt die wenigsten Fill-Ins
im gesamten Graphen zu erzeugen... |
3) |
|
Man beachte,
dass
das paarweise Verbinden alle Separator-Knoten hier
trivial ist, da beide Separatoren nur jeweils einen
Knoten enthalten,
also nichts zu tun ist... |
4) |
|
Ein solches
Gebilde
ist bereits ein formal korrekter Hypertree, der so
allerdings in praktisch allen nicht trivialen
Fällen vollkommen
nutzlos ist... |
6) |
|
Diese
Terminologie soll den Unterschied zu den zu
verarbeitenden
Graphen verdeutlichen, eine Topologie in diesem
Sinne ist jedoch selbst
nichts anderes als ein Graph, wobei die Spots die
Knoten und die Trails
die Kanten sind. |
5) |
|
Dieser ist in
meiner Implementierung generell ein Teil des
Recombinator-Operators, weil das manchmal
Optimierungen zulässt,
die
sonst schwierig zu erzielen wären, generell
kann der
Mutations-Operator
aber auch unabhängig vom Recombination-Operator
gehalten werden |
7)
|
|
Hier wird davon
ausgegangen, dass das Original-Netz von jedem
Individuum (also irgendeinem der beiden
Eltern-Individuen) bezogen
werden kann. |