Algorithmische Intelligenz
„Symbolische Suche“
Peter Kissmann
Spiele
Einpersonenspiele
(n² - 1)-Puzzle
Solitär
Zweipersonenspiele
Tic-Tac-Toe
Clobber
Vier Gewinnt
Motivation
Zustandsraumexplosion
#erreichbare Zustände:
(n²-1)-Puzzle: (n²)!/2
• 15-Puzzle: ≈ 1013
• 24-Puzzle: ≈ 7,8 x 1024
• 35-Puzzle: ≈ 1,9 x 1041
Solitär: 375 110 246
Clobber (4x5): 26 787 440
4 Gewinnt: ≤ 70 728 639 995 483 (≈ 7 x 1013) (Allis, 1988) (tatsächlich: 4 531 985 219 092 (≈ 4,5 x 1012))
Motivation
Speicher sparen z.B. mittels Binären Entscheidungsdiagrammen (BDDs)
verwalten Zustandsmengen
sparen unnötige Knoten ein → teils exponentiell viele
Beispiel: vollständiges Lösen von allgemeinen Spielen
(General Game Playing)
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BFS, Dijkstra, A*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BFS, Dijkstra, A*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
BDDs (Binary Decision Diagrams)
Repräsentieren Zustandsmenge
gerichteter azyklischer Graph von Wurzel zu 0- oder 1- Senke
Knoten für (binäre) Variablen
Zwei Ausgänge: low und high (auch 0 und 1)
Pfad von Wurzel bis 1-Senke
Zustand entsprechender Variablenbelegung in repräsentierter Menge enthalten
OBDDs (Ordered BDDs)
Feste Variablenordnung π
Gute Variablenordnung → exponentiell weniger Knoten (möglicherweise)
Finden guter Variablenordnung NP-schwer
Graphisch: Schichten gleicher Variablen
ROBDDs (Reduced OBDDs)
Zwei Vereinfachungsregeln:
ROBDDs eindeutig
Im Folgenden nur ROBDDs
x1 x1 x1
x2 x3 x2 x3
BDDs für logische Operatoren
x1
x2
0 1
x
1∧ x
2x1
x2
0 1
x
1∨ x
2x1
0 1
¬x
1x1
x2 x2
0 1
x
1⇔ x
2ROBDDs (Beispiele)
column- x
row- x
diagonal- x
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BFS, Dijkstra, A*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
BDD-basierte Suche (Voraussetzungen)
S Menge aller Zustände
Initialzustand I ∈ S
Menge von Zielzuständen G ⊆ S
Transitionsrelation T ⊆ S x S
beschreibt Zustandsübergänge durch Vorgänger und Nachfolger
mögliche Ziele:
finde kürzesten Pfad von I nach g ∈ G
berechne alle erreichbaren Zustände
2 Variablensätze:
x für Vorgängervariablen
x‘ für Nachfolgervariablen
in Variablenordnung xi und xi‘ abwechselnd (interleaved)
BDD-basierte Suche
Finden von Nachfolgern (image)
Relationales Produkt:
Finden von Vorgängern (pre-image) analog:
zusätzlich: nach jedem (pre-)image:
Verschieben der Variablen
image ( s ) =∃ x . ( T ( x , x ' ) ∧s ( x ) )
preimage ( s ) =∃ x ' . ( T ( x , x ' ) ∧s ( x ' ) )
BDD-basierte Suche
Partitionierte Berechnung:
T = VaTa für alle Aktionen a
∃ und ∨ kommutieren
(entsprechend auch für pre-image)
Vorteil: Berechnung monolithischer Transitionsrelation teuer (Zeit und Speicher)
image ( s ) =¿
a∃ x . ( T
a( x , x ' ) ∧ s ( x ) )
BDD-basierte Suche
Finden der Vorgänger, deren Nachfolger alle in s liegen (strong pre-image):
strong pre-image auf pre-image zurückführbar → Übungsaufgabe
strong
preimage( s ) =∀ x ' . ( T ( x , x ' ) ⇒ s ( x ' ) )
BDD-basierte Suche
image
pre-image
strong pre-image
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BFS, Dijkstra, A*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
Breitensuche (SBFS)
iterativ images berechnen
reach ← I
wiederhole
newBDD ← image(reach) ∧ ⌐reach
reach ← reach ∨ newBDD
solange Abbruchkriterium nicht erfüllt
mögliche Abbruchkriterien:
newBDD = ⊥ (alle Zustände bestimmt)
reach ∧ G ≠ ⊥ (kürzester Weg zum Ziel gefunden)
Mögliche Verbesserung
Jeden Zustand nur einmal expandieren (Duplikatserkennung)
Dazu: Closed-BDD
front ← I
wiederhole
closed ← closed ∨ front
front ← image(front) ∧ ⌐closed
solange Abbruchkriterium nicht erfüllt
Bestimmung erreichbarer Zustände mittels SBFS
v: Anzahl Variablen für einen Zustandn: Anzahl BDD-Knoten zur Repräsentation aller Zustände
s: Anzahl aller erreichbarer Zustände
Bestimmung erreichbarer Zustände in „Vier
Gewinnt“ (SBFS)
Bestimmung erreichbarer Zustände in „Vier Gewinnt“ (SBFS)
0 5 10 15 20 25 30 35 40
1E+00 1E+02 1E+04 1E+06 1E+08 1E+10 1E+12 1E+14
Knoten (BDD) Zustände (BDD)
Zustände (Allis-Schätzung)
Bidirektionale Breitensuche (SBBFS)
I
G
Schnitt gefunden
Bidirektionale Breitensuche (SBBFS)
BFS von Start und Ziel „gleichzeitig“
Ende, wenn Suchfronten überschneiden
ffront ← I, bfront ← G
wiederhole
• falls vorwärts
ffront ← image(ffront)
• sonst
bfront ← pre-image(bfront)
solange ffront ∧ bfront = ⊥
Auswahlkriterium etwa Zeit der letzten Iteration
Verwendung von closed-BDDs möglich
Symbolischer Dijkstra
BFS nur bei uniformen Kosten
Gewichtete Transitionsrelation → „Single Source Shortest Path“→ Dijkstra
Kosten c ∈ {1, …, C}
T = VcTc
Symbolischer Dijkstra
open0 ← I, closed ← ⊥, g ← 0
wiederhole
falls (openg ∧ G ≠ ⊥) STOPP
openg ← openg ∧ ⌐closed
für c ← 1, …, C
• openg+c ← openg+c ∨ imagec(openg)
closed ← closed ∨ openg
g ← g + 1
Symbolisches A* (BDDA*)
Ähnlich Dijkstra; Expansion nach f-Wert:
Verwendung einer Heuristik
z.B. aus Musterdatenbank (pattern database (PDB))
Heuristik h darf nicht überschätzen (zulässig)
h = 0 → Dijkstra
f ( v ) = g ( v ) +h ( v )
Symbolisches A* (BDDA*)
h
g
Symbolisches A* (BDDA*)
open(0,h(I)) ← I, closed(0, …, |h|) ← ⊥, f ← h(I)
wiederhole
für g ← 0, …, f
h ← f - g
falls (h = 0 & open(g, h) ∧ G ≠ ⊥) STOPP
open(g, h) ← open(g, h) ∧ ⌐ closed(h)
für c ← 1, …, C
• succc ← imagec(open(g, h))
• für hsucc ← 0, …, |h|
open(g + c, hsucc) ← open(g + c, hsucc) ∨ (succc ∧ hsucc)
closed(h) ← closed(h) ∨ open(g, h)
f← f + 1
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BDD-BFS, BDD-Dijkstra, BDDA*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
Überblick 2 (Lösen allgemeiner Spiele)
General Game Playing
Einpersonenspiele
Zweipersonenspiele
Zweipersonen-Nullsummenspiele
Zweipersonenspiele mit allgemeinen Gewinnen
Überblick 2 (Lösen allgemeiner Spiele)
General Game Playing
Einpersonenspiele
Zweipersonenspiele
Zweipersonen-Nullsummenspiele
Zweipersonenspiele mit allgemeinen Gewinnen
General Game Playing
Beschreibung für Spiele mit folgenden Eigenschaften:
endlich
diskret
deterministisch
vollständige Information
Spiele können
Ein- oder Mehr-Personenspiele sein
gleichzeitige oder abwechselnde Züge ermöglichen
General Game Playing
„Game Description Language“ (GDL)
Gegeben:
Initialzustand
Bestimmung legaler Züge
Effekt eines Zuges
Terminierungsbedingungen
Verteilung der Gewinne {0, …, 100} darin
Gesucht:
Lösung erreichbarer Zustände
Bestimmung optimaler Gewinn-Verteilung
General Game Playing
Beispiele:
Blocksworld
Original GDL-Datei: .kif
Tic-Tac-Toe
Original GDL-Datei: .kif
Mehr Informationen:
http://games.stanford.edu (dort entwickelt; leider veraltet)
http://www.general-game-playing.de
http://euklid.inf.tu-dresden.de:8180/ggpserver (aktuelle Spiele etc.)
Überblick 2 (Lösen allgemeiner Spiele)
General Game Playing
Einpersonenspiele
Zweipersonenspiele
Zweipersonen-Nullsummenspiele
Zweipersonenspiele mit allgemeinen Gewinnen
Lösen von Einpersonenspielen
Erst: Erreichbare Zustände finden (BFS)
Dann: Rückwärtssuche
Start: Zielzustände mit Gewinn 100
BFS (rückwärts)
Weiter: Zielzustände mit Gewinn 99
BFS (rückwärts)
dabei: bereits gelöste Zustände auslassen
Weiter bis Gewinn 0
Lösen von Einpersonenspielen
75 100 90 80
99
75 80
100 100
100
100
90
90
80
80
75
Ergebnisse für Solitär
Erreichbar: 375 110 246 Zustände
Überblick 2 (Lösen allgemeiner Spiele)
General Game Playing
Einpersonenspiele
Zweipersonenspiele
Zweipersonen-Nullsummenspiele
Zweipersonenspiele mit allgemeinen Gewinnen
Lösen von Zweipersonen- Nullsummenspielen
Mögliche Gewinne: 0, 50, 100
Jeder Spieler versucht, möglichst hohen Gewinn zu erreichen
Lösung liefert Verteilung der Gewinne (bei optimaler
Spielweise)
Lösen von Zweipersonen- Nullsummenspielen
BFS für Finden erreichbarer Zustände
Zwei Rückwärtssuchen (eine pro Spieler):
Start bei verlorenen Zielzuständen
Bestimmung verlorener Vorgänger (2 Schritte)
für alle Züge, die Spieler durchführen kann, kann Gegenspieler Zug zu verlorenem Zustand wählen (pre-image und strong pre-image)
Iterieren, solange neue Zustände gefunden
player 0‘s turn player 1‘s turn
lost for player 0 lost for player 1
Lösen von Zweipersonen- Nullsummenspielen
reach ← berechneErreichbareZustände()
für jeden Spieler p ∈ {0, 1}
front ← verlorenp ← reach ∧ gewinn(p, 0) ∧ G ∧ zugp
gewonnen1-p ← reach ∧ gewinn(p, 0) ∧ G ∧ zug1-p
wiederhole
• pred ← pre-image(front) ∧ reach
• gewonnen1-p ← gewonnen1-p ∨ pred
• front ← strong-pre-image(gewonnen1-p) ∧ reach ∧ ⌐verlorenp
• verlorenp ← verlorenp ∨ front
solange front ≠ ⊥
Überblick 2 (Lösen allgemeiner Spiele)
General Game Playing
Einpersonenspiele
Zweipersonenspiele
Zweipersonen-Nullsummenspiele
Zweipersonenspiele mit allgemeinen Gewinnen
Lösen allgemeiner Zweipersonenspiele
Mögliche Gewinne ∈ {0, …, 100}
Verwendung von (101 x 101)-Matrix
Zustand an Position (i, j):
i Punkte für Spieler 0
j Punkte für Spieler 1
falls unvollständig, Verwendung als Endspieldatenbank
Lösen allgemeiner Zweipersonenspiele
Eine Vorwärts- und eine Rückwärtssuche
finde alle Vorgänger, deren Nachfolger alle gelöst sind (strong pre- image)
finde optimales Bucket für diese (pre-image)
füge sie ein
iteriere, bis alle Zustände gelöst
Einschub: Reihenfolge beim Lösen
schwierig im allgemeinen Fall
eigenen Gewinn maximieren
(und gegnerischen minimieren)?
oder Differenz zum gegnerischen Gewinn maximieren?
Hier: 2. Fall
own
100 opponent 0
0 100
…
…
own
100 opponent 0
0 100
…
…
Beispiel
player 0
player 1
0 0
1 1
2
2
3
3
player 0‘s turn
player 1‘s turn 0/1
0/1
0/3 2/0
3/1
2/0
2/0 3/1
3/1
0/1 3/1 3/1
0/1 0/1
0/3 0/3
0/1 3/1 0/1 2/0
0/1 0/1
0/1
Lösen allgemeiner Zweipersonenspiele
reach ← berechneErreichbareZustände()
init matrix; solved ← alle Zustände in Matrix
unsolved ← reach ∧ ⌐solved
solange unsolved ≠ ⊥
für jeden Spieler p ∈ {0, 1}
• solvable ← strong-pre-image(solved) ∧ unsolved ∧ zugp
• falls solvable ≠ ⊥
matrix ← fügeZuständeEin(solvable, p, matrix)
solved ← solved ∨ solvable
unsolved ← unsolved ∧ ⌐solvable
Ergebnisse
Game t
0-sumt
newClobber 3x4 - 1.1s
Clobber 3x4 0-sum 1.0s 1.4s Clobber 4x5 - 2:14:20 Clobber 4x5 0-sum 0:54:35 1:22:09
Minichess 1.0s 0.7s
TicTacToe 0.1s 0.2s
Nim 40 0.0s 0.1s
Überblick
Wiederholung: BDDs
BDD-basierte Suche
BDD-BFS, BDD-Dijkstra, BDDA*
Anwendung auf allgemeine Spiele („General Game Playing“)
BDDs als perfekte Hash-Funktion
Hashing
Gegeben: Menge von Zuständen S
Gesucht: Abbildung S → R ⊆ ℕ
Hashfunktion ordnet jedem Zustand einen Wert zu
perfektes Hashing: Hashwert jedes Zustandes eindeutig
minimales perfektes Hashing: |R| = |S|
Sat-Count
Anzahl gespeicherter Zustände in BDD G
mögliche Berechnung:
sat-count(0-Senke) ← 0, sat-count(1-Senke) ← 1
für Knoten v aus Schicht i mit 0-Nachfolger u in Schicht j > i und 1- Nachfolger w in Schicht k > i
sat-count(v) ← 2j-i-1 * sat-count(u) + 2k-i-1 * sat-count(w)
falls Wurzel in Schicht i:
sat-count(G) ← 2i-1 * sat-count(Wurzel)
Laufzeit- und Speicherbedarf: ≤ O(|G|)
Sat-Count (Beispiel)
0 1
1 1
1
30
16 14
4
3 5
2 2
2
abgedeckte Zustände:
000001
000111
001011
001101
010011
010100
010101
010110
010111
011011
011100
011101
011110
011111
100011
100100
100101
100110
100111
101011
101100
101101
101110
101111
110010
110011
110111
111010
111011
111111
Ranking
Gegeben: BDD G, Zustand s
Gesucht: Hash-Wert von s (in {0, …, sat-count(G) - 1})
Vorverarbeitung:
Berechne Sat-Count aller Knoten
speichere diese Sat-Counts
Ranking
rank(G,s)
falls Wurzel in Schicht i
d ← Binärwert von (s1, …, si-1)
gib (d+1) * lexicographic-count(G,s,Wurzel) - 1 zurück
Ranking
lexicographic-count(G,s,v)
falls v 0-Senke, gib 0 zurück; falls v 1-Senke, gib 1 zurück
falls v in Schicht i mit 0-Nachf. u in j und 1-Nachf. w in k
falls si = 0
• r0 ← lexicographic-count(G,s,u)
• d0 ← Binärwert von (si+1, …, sj-1)
• gib d0 * sat-count(u) + r0 zurück
falls si = 1
• r1 ← lexicographic-count(G,s,w)
• d1 ← Binärwert von (si+1, …, sk-1)
• gib 2j-i-1 * sat-count(u) + d1 * sat-count(w) + r1 zurück
Ranking (Beispiel)
s ← 011101
rank(G,s) ← [()2 + 1] * lc(G,s,v0) - 1
lc(G,s,v0) ← ()2 * sc(v1) + lc(G,s,v1)
lc(G,s,v1) ← 23-2-1 * sc(v3) + (1)2 * sc(v6) + lc(G,s,v6)
lc(G,s,v6) ← 25-4-1 * sc(v9) + (01)2 * sc(v13) + lc(G,s,v13)
v13 ist 1-Senke → lc(G,s,v13) ← 1
lc(G,s,v6) ← 20 * sc(v9) + 1 * sc(v13) + lc(G,s,v13)
= 1 * 1 + 1 * 1 + 1 = 3
lc(G,s,v1) ← 20 * sc(v3) + 1 * sc(v6) + lc(G,s,v6)
= 1 * 4 + 1 * 5 + 3 = 12
lc(G,s,v0) ← 0 * sc(v1) + lc(G,s,v1) = 12
rank(G,s) ← 1 * lc(G,s,v0) - 1 = 11
0 1
1 1
1
30
16 14
4
3 5
2 2
2
v0
v1 v2
v3
v4 v5 v6 v7
v8 v9 v10
v11
v12 v13
Unranking
Gegeben: BDD G, Hash-Wert r
Gesucht: zugehöriger Zustand
Unranking
unrank(G,r)
starte an der Wurzel
falls Wurzel in Schicht l
(s1, …, sl-1) ← Binärrepräsentation von r div sat-count(Wurzel)
r ← r mod sat-count(Wurzel)
v ← Wurzel; i ← l
wiederhole, bis v 0- oder 1-Senke
falls v Knoten in Schicht i mit 0-Nachf. u in j 1-Nachf. w in k
• falls r < 2j-i-1 * sat-count(u)
si ← 0; (si+1, …, sj-1) ← Binärrepräsentation von r div sat-count(u)
r ← r mod sat-count(u)
v ← u; i ← j
• falls r ≥ 2j-i-1 * sat-count(u)
si ← 1; r ← r - 2j-i-1 * sat-count(u)
(si+1, …, sk-1) ← Binärrepräsentation von r div sat-count(w)
r ← r mod sat-count(w)
v ← w; i ← k
Unranking (Beispiel)
r ← 19
i ← 1; r ≥ 22-1-1 * sc(v1) = 1 * 14 = 14
s1 ← 1; r ← r - 22-1-1 * sc(v1) = 19 - 1 * 14 = 5
r ← r mod sc(v2) = 5 mod 16 = 5
i ← 2; r < 24-2-1 * sc(v6) = 2 * 5 = 10
s2 ← 0; (s3) ← (r div sc(v6))2 = (5 div 5)2 = 12 = 1
r ← r mod sc(v6) = 5 mod 5 = 0
i ← 4; r < 25-4-1 * sc(v9) = 1 * 1 = 1
s4 ← 0; r ← r mod sc(v9) = 0 mod 1 = 0
i ← 5; r ≥ 26-5-1 * sc(v12) = 2 * 0 = 0
s5 ← 1; r ← r - 27-5-1 * sc(v12) = 0 - 2 * 0 = 0
r ← r mod sc(v11) = 0 mod 1 = 0
i ← 6; r ≥ 27-6-1 * sc(v12) = 1 * 0 = 0
s6 ← 1; r ← r - 27-6-1 * sc(v12) = 0 - 1 * 0 = 0
r ← r mod sc(v13) = 0 mod 1 = 0
0 1
1 1
1
30
16 14
4
3 5
2 2
2
v0
v1 v2
v3
v4 v5 v6 v7
v8 v9 v10
v11
v12 v13
s ←
s ← 1
s ← 101
s ← 1010
s ← 10101
s ← 101011
Ranking und Unranking (Analyse)
Vorverarbeitung: O(|G|)
Ranking pro Zustand: O(n)
Unranking pro Zustand: O(n)
Vorverarbeitung beschriftet jeden Knoten mit n-bit Zahl →
O(n|G|) extra Bits nötig
Zusammenfassung
Symbolische Suche zur Verringerung der Speicherlast
speichern von Zustandsmengen (als BDDs) statt einzelner Zustände