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 x2 x3 x1 x2 x3 BDDs für logische Operatoren x 1 ∧x 2 x 1 ∨x 2 x1 x1 0 x2 x2 1 0 x1 ⇔ x2 ¬x 1 1 x1 0 x1 1 x2 x2 0 1 ROBDDs (Beispiele) columnx rowx diagonalx Ü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: image ( s ) =∃ x . ( T ( x , x ' ) ∧s ( x ) ) Finden von Vorgängern (pre-image) analog: preimage ( s ) =∃ x ' . ( T ( x , x ' ) ∧s ( x ' ) ) zusätzlich: nach jedem (pre-)image: Verschieben der Variablen BDD-basierte Suche Partitionierte Berechnung: T = VaTa für alle Aktionen a ∃ und ∨ kommutieren image ( s ) =¿ a ∃ x . ( T a ( x , x ' ) ∧s ( x ) ) (entsprechend auch für pre-image) Vorteil: Berechnung monolithischer Transitionsrelation teuer (Zeit und Speicher) BDD-basierte Suche Finden der Vorgänger, deren Nachfolger alle in s liegen (strong pre-image): strong preimage ( s )=∀ x ' . ( T ( x , x ' ) ⇒ s ( x ' ) ) strong pre-image auf pre-image zurückführbar → Übungsaufgabe BDD-basierte Suche strong image pre-image 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 v: Anzahl Variablen für einen Zustand mittels SBFS n: 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) 1E+14 1E+12 1E+10 1E+08 1E+06 1E+04 Knoten (BDD) Zustände (BDD) Zustände (Allis-Schätzung) 1E+02 1E+00 0 5 10 15 20 25 30 35 40 Bidirektionale Breitensuche (SBBFS) Schnitt gefunden I G 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: f ( v )=g ( v ) +h ( v ) Verwendung einer Heuristik z.B. aus Musterdatenbank (pattern database (PDB)) Heuristik h darf nicht überschätzen (zulässig) h = 0 → Dijkstra 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 100 100 100 99 100 75 100 90 80 90 80 90 80 80 75 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 ZweipersonenNullsummenspielen 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 ZweipersonenNullsummenspielen 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 ZweipersonenNullsummenspielen 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 preimage) 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 (und gegnerischen minimieren)? Gewinn maximieren? Hier: 2. Fall 0 own … 100 … 100 0 opponent oder Differenz zum gegnerischen 100 0 opponent eigenen Gewinn maximieren 0 own … … 100 Beispiel player 0 0 1 2 0/1 3 player 1 0 0/1 0/3 0/1 1 2 0/1 3/1 0/3 0/1 2/0 3 0/1 0/1 3/1 2/0 3/1 0/1 0/3 0/1 2/0 3/1 player 0‘s turn player 1‘s turn 0/1 3/1 2/0 3/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 t0-sum tnew Clobber 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 1Nachfolger 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) 30 abgedeckte Zustände: 14 4 2 1 000001 16 000111 001011 001101 010011 010100 2010101 5 010110 1 010111 2 011011 1 0 1 011100 011101 011110 011111 100011 100100 3100101 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) 30 v 14 v 4 2 0 s ← 011101 16 v 1 rank(G,s) ← [()2 + 1] * lc(G,s,v0) - 1 2 lc(G,s,v0) ← ()2 * sc(v1) + lc(G,s,v1) v3 lc(G,s,v1) ← 23-2-1 * sc(v3) + (1)2 * sc(v6) + lc(G,s,v6) 2 v4 v5 5 v6 3 lc(G,s,v6) ← 25-4-1 * sc(v9) + (01)2 * sc(v13) + lc(G,s,v13) vv 7 13 ist 1-Senke → lc(G,s,v13) ← 1 lc(G,s,v6) ← 20 * sc(v9) + 1 * sc(v13) + lc(G,s,v13) 1 v8 1 1 v11 0 v12 v9 2 v10 =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 1 v13 rank(G,s) ← 1 * lc(G,s,v0) - 1 = 11 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) 30 v 14 v 0 r ← 19 16 v 1 s ← 101011 1 101 1010 10101 i ← 1; r ≥ 22-1-1 * sc(v1) = 1 * 14 = 14 2 s1 ← 1; r ← r - 22-1-1 * sc(v1) = 19 - 1 * 14 = 5 4 2 r ← r mod sc(v2) = 5 mod 16 = 5 v3 i ← 2; r < 24-2-1 * sc(v6) = 2 * 5 = 10 2 v4 1 v8 1 v5 1 5 v9 v6 2 3 v10 v7 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 v11 s5 ← 1; r ← r - 27-5-1 * sc(v12) = 0 - 2 * 0 = 0 r ← r mod sc(v11) = 0 mod 1 = 0 0 v12 1 v13 i ← 6; r ≥ 27-6-1 * sc(v12) = 1 * 0 = 0 s6 ← 1; r ← r - 27-6-1 * sc(v12) = 0 - 1 * 0 = 0 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 Vorgänger- und Nachfolgerberechnungen (image und pre- image) liefern direkt SBFS und SBBFS Symbolische Formen von Dijkstra und A* Lösen von Spielen mittels symbolischer Suche BDDs als perfekte Hash-Funktion