Spiele
Man kann Spiele auch als Suchproblem betrachten und als Ziel die Suche nach dem optimalen Zug definieren.
Man kann Spiele auch als Suchproblem betrachten und als Ziel die Suche nach dem optimalen Zug definieren.
Spiele können als Suchproblem betrachtet werden. Dabei sind in der Regel mehrere Spieler ("Agenten") beteiligt. Bei manchen Spielen ist die Umgebung (der Spielzustand) vollständig einsehbar, bei anderen nur teilweise (Kartenspiele). Bei manchen Spielen kommt eine Zufallskomponente zum Wirken.
Spiele sind in der KI deshalb so interessant, weil bei der Suche riesige Suchbäume entstehen (bzw. durchsucht werden müssten). Da die Ressourcen normalerweise begrenzt sind (denken Sie an die Reaktionszeit auf einen Zug des Gegners), muss man hier intelligente Lösungen finden. (Einige davon werden wir in den folgenden Sitzungen anschauen).
Quelle: "position-backgammon-decembre" by serialgamer_fr on Flickr.com (CC BY 2.0)
Zwei Spieler, ein Spielstand und ein Würfelergebnis: Was ist jetzt der beste Zug?!
=> Mehrere konkurrierende Agenten an Suche beteiligt!
=> (Re-) Aktion des Gegners unbekannt/nicht vorhersehbar.
Deterministisch | Zufallskomponente | |
---|---|---|
Voll beobachtbar | Schach, Go, ... | Backgammon, Monopoly |
Partiell beobachtbar | Schiffe-versenken | Bridge, Poker, Skat, ... |
=> Bis auf Roboterfußball in KI traditionell keine physischen Spiele!
Brettspiele gut abstrakt darstellbar:
Problem: Suchbäume werden in Praxis riesig
Beispiel Schach:
Quelle: [Russell2020, pp. 193-196]
Zeit begrenzt
Speicher begrenzt
Strategien nötig
Mit dem Minimax-Algorithmus können optimale Züge berechnet werden. Dabei wird von zwei
Spielern Max
und Min
ausgegangen, die abwechselnd ziehen und beide optimal spielen.
Wenn Max
gewonnen hat, wird der Spielausgang mit +1 bewertet, wenn Min
gewonnen hat
mit -1, und mit 0 sonst. Damit hat man ein sogenanntes "Nullsummenspiel" (der Gewinn des
einen Spielers ist der Verlust des anderen) und kann den Algorithmus so gestalten, dass
Max
stets den Zug wählt, der das Spielergebnis maximiert und Min
entsprechend den
Zug wählt, der das Spielergebnis minimiert (daher auch die Namen der Spieler).
Minimax baut den gesamten Spielbaum bis zu den Blättern auf. Die Blätter (Spielausgang)
werden mit einer Utility
-Funktion bewertet, und diese Bewertung wird dann im Spielbaum
nach oben gereicht.
Zwei abwechselnd spielende Spieler: MAX
und MIN
, wobei MAX
beginnt
MAX
bewertet:
MAX
gewinntMIN
gewinntMAX
versucht, das Spielergebnis zu maximierenMIN
versucht, das Spielergebnis zu minimierenStartzustand: Initialer Zustand des Spielbrettes
Aktionen: Legale Züge, abhängig vom Spielzustand
Zieltest: Ist das Spiel vorbei?
=> Startzustand und anwendbare Aktionen definieren den Zustandsraum.
Nutzenfunktion: $\operatorname{UTILITY}(s,p)$: Wert des Spiels für Spieler $p$ im Spielzustand $s$
Strategie: Spieler benötigen Strategie, um zu gewünschtem Endzustand zu kommen (unabhängig von den Entscheidungen des Gegenspielers) => einfacher Pfad von Start zu Ziel reicht nicht
Hinweis: Nullsummenspiel! (Der Gewinn des einen Spielers ist der Verlust des anderen Spielers.)
Eine mit dem Minimax-Algorithmus berechnete Strategie wird auch Minimax-Strategie genannt. Sie sichert dem betreffenden Spieler den höchstmöglichen Gewinn, der unabhängig von der Spielweise des Gegners erreichbar ist.
Bei Nicht-Nullsummenspielen, bei denen die Niederlage des Gegners nicht zwangsläufig mit dem eigenen Gewinn zusammenfällt (d.h. Gewinn des einen Spielers $\ne$ Verlust des anderen Spielers), liefert der Minimax-Algorithmus nicht unbedingt eine optimale Strategie.
Min
-Knoten:
Nutzen ist das Minimum der KindknotenMax
-Knoten:
Nutzen ist das Maximum der KindknotenMax
wählt den Zug, der zum Nachfolger mit der
höchsten Bewertung führtAnnahme: Beide spielen perfekt. Fehler verbessern das Resultat für den Gegner.
def Max-Value(state):
if Terminal-Test(state): return Utility(state)
v = -INF
for (a, s) in Successors(state):
v = MAX(v, Min-Value(s))
return v
def Min-Value(state):
if Terminal-Test(state): return Utility(state)
v = +INF
for (a, s) in Successors(state):
v = MIN(v, Max-Value(s))
return v
Hinweis I: Auf wikipedia.org/wiki/Minimax
finden Sie eine Variante mit einem zusätzlichen Tiefenparameter, um bei einer bestimmten
Suchtiefe abbrechen zu können. Dies ist bereits eine erweiterte Version, wo man beim
Abbruch durch das Erreichen der Suchtiefe statt Utility()
eine Eval()
-Funktion
braucht (vgl. Minimax: Heuristiken).
Wenn man ohne Suchtiefenbeschränkung arbeiten will, braucht man diesen Parameter nicht! Der Algorithmus terminiert auch ohne Suchtiefenbeschränkung!
Hinweis II: Im [Russell2020, S. 196, Abb. 6.3] findet sich eine Variante, die die
auf der nächsten Folien gezeigte Startfunktion mit den hier gezeigten Min-Value()
-
und Max-Value()
-Funktionen verschmilzt. Dabei wird in den beiden Hilfsfunktionen
nicht nur der min
- bzw. max
-Wert optimiert, sondern auch der jeweils beste Zug
gespeichert und als Rückgabe zurückgeliefert.
def Minimax(state):
(val, action) = (-INF, null)
for (a, s) in Successors(state):
v = Min-Value(s)
if (val <= v):
(val, action) = (v, a)
return action
Gedankenexperiment:
Max
und Min
Max
: +1, Gewinn von Min
: -1Max
wählt das Maximum der möglichen Züge von Min
Min
wählt das Minimum der möglichen Züge von Max
Utility
Optimale Spiele und MiniMax
Auf einem Tisch liegen nebeneinander 5 Streichhölzer. Es gibt zwei Spieler - Weiß und Schwarz - die abwechselnd ein oder zwei Streichhölzer wegnehmen dürfen (es muss mind. ein Streichholz genommen werden). Wer das letzte Streichholz nehmen muss, hat verloren. Zu Beginn ist Weiß am Zug.
Zeichnen Sie den kompletten Spielbaum auf. Geben Sie an den Kanten jeweils die Zahl der genommenen und der verbleibenden Hölzer an.
Beispiel: Wenn in einem Zug ein Holz genommen wird und 3 Hölzer verbleiben, steht an der entsprechenden Kante "1/3". Geben Sie jeweils an, welcher Spieler am Zug ist.
Geben Sie die Bewertung aller Spielzustände mit Hilfe des Minimax-Algorithmus an. Bewerten Sie die Endzustände mit +1, wenn Spieler Weiß gewonnen hat, mit -1, falls Schwarz gewonnen hat.
Mit welchem Zug muss Weiß beginnen, um das Spiel garantiert zu gewinnen (beide Spieler verhalten sich stets optimal)? Argumentieren Sie mit der Minimax-Bewertung.
Minimax entwickelt den gesamten Spielbaum. Wenn nicht genug Zeit dafür zur Verfügung steht, kann man die
Suchtiefe begrenzen. Für die Bewertung der Zustände benötigt man eine Eval
-Funktion, die die Knoten in
der selben Reihenfolge sortieren sollte wie es in der vollständigen Version über die Utility
-Funktion
geschieht. Die Eval
-Funktion sollte zudem schnell zu berechnen sein. Typische Varianten für die
Eval
-Funktion sind gewichtete Features oder ein Nachschlagen in Spieldatenbanken (Spielzustand plus
Bewertung).
Minimax kann auf Spiele mit mehr als zwei Spielern erweitert werden. Dabei versucht dann jeder Spieler für sich, das Ergebnis des Spiels (aus seiner Sicht) zu maximieren.
Bei Spielen mit Zufall (Würfelereignisse) kann man jedem Würfelereignis eine Wahrscheinlichkeit zuordnen
und damit den jeweils erreichbaren Max
- oder Min
-Wert gewichten. Die Summe dieser gewichteten Bewertungen
ist die Bewertung des entsprechenden "Chance"-Knotens, der dann in der darüberliegenden Ebene nach dem
Minimax-Prinzip ausgewertet wird (=> Expectimax).
Einführung neuer Funktionen:
Cutoff-Test
statt Terminal-Test
Beispielsweise bei erreichter Tiefe oder Zeitüberschreitung
Eval
statt Utility
Bewertung der erreichten Position (statt nur Bewertung des Endzustandes)
Bedingungen an Eval
:
Utility
Mögliche Evaluierungskriterien:
Nutzung gewichteter Features $f_i$: $\operatorname{Eval}(s) = w_1f_1(s) + w_2f_2(s) + \ldots$
Alternativ: Speicherung von Positionen plus Bewertung in Datenbanken => Lookup mit $\operatorname{Eval}(s)$ (statt Berechnung zur Laufzeit)
Hier maximiert jeder Spieler sein eigenes Ergebnis. Im Grunde müsste diese Variante dann besser "Maximax" heissen ...
Wenn es an einer Stelle im Suchbaum mehrere gleich gute (beste) Züge geben sollte, kann der Spieler Allianzen bilden: Er könnte dann einen Zug auswählen, der für einen der Mitspieler günstiger ist.
Quelle: "position-backgammon-decembre" by serialgamer_fr on Flickr.com (CC BY 2.0)
Backgammon: Was ist in dieser Situation der optimale Zug?
Zusätzlich zu den MIN- und MAX-Knoten führt man noch Zufalls-Knoten ein, um das Würfelergebnis repräsentieren zu können. Je möglichem Würfelergebnis $i$ gibt es einen Ausgang, an dem die Wahrscheinlichkeit $P(i)$ dieses Ausgangs annotiert wird.
=> Für Zufallsknoten erwarteten Minimax-Wert (Expectimax) nutzen
Expectimax-Wert für Zufallsknoten $C$:
$$ \operatorname{Expectimax}(C) = \sum_i P(i) \operatorname{Expectimax}(s_i) $$Für die normalen Min- und Max-Knoten liefert Expectimax()
die üblichen
Aufrufe von Min-Value()
bwz. Max-Value()
.
Auf wikipedia.org/wiki/Expectiminimax
finden Sie eine Variante mit einem zusätzlichen Tiefenparameter, um bei einer bestimmten
Suchtiefe abbrechen zu können. Dies ist bereits eine erweiterte Version, wo man beim
Abbruch durch das Erreichen der Suchtiefe statt Utility()
eine Eval()
-Funktion
braucht. Zusätzlich kombiniert der dort gezeigte Algorithmus die Funktionen
Expectimax()
, Min-Value()
und Max-Value()
in eine einzige Funktion.
Eine ähnliche geschlossene Darstellung finden Sie im [Russell2020, S. 212].
Hinweis: Üblicherweise sind die Nachfolger der Zufallsknoten gleich wahrscheinlich. Dann kann man einfach mit dem Mittelwert der Bewertung der Nachfolger arbeiten.
Eval
statt Utility
Minimax entwickelt den gesamten Spielbaum. Wenn man dabei die bisher besten Werte für MAX und MIN als $\alpha$ und $\beta$ mitführt, beobachtet man, dass ein $\alpha$-Wert nie kleiner wird und ein $\beta$-Wert nie größer wird. Dies kann man ausnutzen und das Entwickeln von Pfaden abbrechen, wenn in einem MIN-Knoten der $\beta$-Wert kleiner wird als der $\alpha$-Wert des MAX-Vorgängers: (a) kann der $\beta$-Wert bei der weiteren Untersuchung der verbleibenden Nachfolger im MIN-Knoten nur noch kleiner werden, und (b) würde der MAX-Vorgänger diesen MIN-Knoten nie als Nachfolger in Betracht ziehen, da er bereits einen besseren Zug gesehen hat (da sein $\alpha$ größer ist als das $\beta$ im Nachfolger). Deshalb kann man hier sofort die Untersuchung der verbleibenden Nachfolger im MIN-Knoten abbrechen ("Pruning"). Eine analoge Überlegung gilt für einen MAX-Nachfolger unter einem MIN-Knoten.
Dabei bleibt das Endergebnis erhalten. Man schneidet nur Suchpfade weg, die das Ergebnis von Minimax nicht verändern.
Die mögliche Effizienzsteigerung ist sehr stark abhängig von Sortierung der Nachfolger! Deshalb stellt man häufig Heuristiken zur "richtigen" Sortierung der Nachfolger auf ("Killer-Moves").
Zusätzlich kann man wie bei Minimax auch die Suchtiefe begrenzen und eine Bewertungsfunktion (statt der Nutzenfunktion) einsetzen. Auch hier kann die Bewertungsfunktion wieder gewichtete Features nutzen und/oder Positionen mit in Datenbanken gespeicherten Positionen und Bewertungen abgleichen.
=> Minimax-Baum: Verbesserungen möglich?
Minimax-Algorithmus mit zusätzlichen Informationen:
=> Beobachtungen:
Schneide (unter) MIN-Knoten ab, deren $\beta$ $\le$ dem $\alpha$ des MAX-Vorgängers ist.
Schneide (unter) MAX-Knoten ab, deren $\alpha$ $\ge$ dem $\beta$ des MIN-Vorgängers ist.
Abbruch, wenn kein Platz mehr zwischen Alpha und Beta
def Max-Value(state, alpha, beta):
if Terminal-Test(state): return Utility(state)
v = -INF
for (a, s) in Successors(state):
v = MAX(v, Min-Value(s, alpha, beta))
if (v >= beta): return v
alpha = MAX(alpha, v)
return v
def Min-Value(state, alpha, beta):
if Terminal-Test(state): return Utility(state)
v = +INF
for (a, s) in Successors(state):
v = MIN(v, Max-Value(s, alpha, beta))
if (v <= alpha): return v
beta = MIN(beta, v)
return v
Initialer Aufruf von Max-Value()
mit $\alpha = -\infty$ und $\beta = +\infty$
Achtung: Es kursieren Varianten von diesem Algorithmus, bei denen auf die
Hilfsvariable v
verzichtet wird und stattdessen alpha
bzw. beta
direkt
modifiziert werden und als Rückgabewert dienen. Das kann zu anderen oder falschen
Ergebnissen führen! Sie können das in der Aufgabe auf Blatt 03 gut beobachten.
Pruning beeinflusst nicht das Endergebnis!
Sortierung der Nachfolger spielt große Rolle
Perfekte Sortierung: $O(b^{d/2})$ => Verdopplung der Suchtiefe möglich
Für Schach immer noch zu aufwändig ...
"Killer-Move": Maximale Effizienz nur wenn optimaler Zug immer zuerst untersucht => Zu untersuchende Züge sortieren/priorisieren, zb. Schach: a) Figuren schlagen b) Drohen c) Vorwärts ziehen d) Rückwärts ziehen
Verändern der Suchtiefe nach Spielsituation
Bewertungsfunktion Eval
:
Eval
-FunktionEval
:
Quelle: [Russell2014, p. 185]
Beschränkung der Suchtiefe: Bewertung der Stellung durch "Value Network"
Beschränkung der Verzweigungsbreite: Bestimmung von Zugkandidaten durch "Policy Network"
Training dieser "Deep Neural Networks":
Quelle: [Silver2016], siehe auch deepmind.com/research/alphago/
Alpha-beta-Pruning:
Viele Verbesserungen denkbar:
Optimale Spiele und MiniMax
Auf einem Tisch liegen nebeneinander 5 Streichhölzer. Es gibt zwei Spieler - Weiß und Schwarz - die abwechselnd ein oder zwei Streichhölzer wegnehmen dürfen (es muss mind. ein Streichholz genommen werden). Wer das letzte Streichholz nehmen muss, hat verloren. Zu Beginn ist Weiß am Zug.
Zeichnen Sie den kompletten Spielbaum auf. Geben Sie an den Kanten jeweils die Zahl der genommenen und der verbleibenden Hölzer an.
Beispiel: Wenn in einem Zug ein Holz genommen wird und 3 Hölzer verbleiben, steht an der entsprechenden Kante "1/3". Geben Sie jeweils an, welcher Spieler am Zug ist.
Geben Sie die Bewertung aller Spielzustände mit Hilfe des Minimax-Algorithmus an. Bewerten Sie die Endzustände mit +1, wenn Spieler Weiß gewonnen hat, mit -1, falls Schwarz gewonnen hat.
Wenden Sie Alpha-Beta-Pruning auf den Spielbaum an. Werden damit mehr oder weniger oder gleich viele Spielzüge wie mit Minimax entwickelt? Begründen Sie Ihre Antwort.