« Métodos combinados de… « || Inicio || » Programación Funciona… »

Minimax: Juegos con adversario

Última modificación: 29 de Julio de 2019, y ha tenido 6774 vistas

Etiquetas utilizadas: || ||

En entradas anteriores hemos visto estrategias para encontrar soluciones a problemas de búsqueda que se pueden expresar por medio de un espacio con estructura en forma de árbol o de grafo. Una de las fuentes más fructíferas de problemas que se pueden resolver por los procedimientos de búsqueda estudiados nos la proporciona la teoría de juegos unipersonales (o solitarios).

Sin embargo, es difícil aplicar directamente estos procedimientos a juegos en los que intervienen dos jugadores que compiten, lo que se conoce como juego con adversario. En este contexto, Minimax es un método de decisión para minimizar la pérdida máxima esperada en juegos con adversario y con información perfecta (para maximizar la ganancia mínima esperada). Su funcionamiento puede resumirse en: elegir el mejor movimiento para ti mismo suponiendo que tu contrincante escogerá el peor movimiento para ti.

John von Neumann dio la siguiente definición de lo que era un juego:

Un juego es una situación conflictiva en la que uno debe tomar una decisión sabiendo que los demás también toman decisiones, y que el resultado del conflicto se determina, de algún modo, a partir de todas las decisiones realizadas.

Y demostró en 1926 que siempre existe una forma racional (una estrategia óptima) de actuar en juegos de dos participantes, si los intereses que los gobiernan son completamente opuestos. A este resultado se le conoce como Teorema Minimax.

Este resultado establece que en los juegos bipersonales de suma cero (aquellos en los que los intereses que los gobiernan son completamente opuestos, o lo que es lo mismo, que lo que gana un jugador lo pierde el otro), donde además cada jugador conoce de antemano la estrategia de su oponente y sus consecuencias, existe una estrategia que permite a ambos jugadores minimizar la pérdida máxima esperada. En particular, cuando se examina cada posible estrategia, un jugador debe considerar todas las respuestas posibles del jugador adversario y la pérdida máxima que puede acarrear. El jugador juega, entonces, con la estrategia que resulta en la minimización de su máxima pérdida. Tal estrategia es llamada óptima para ambos jugadores sólo en caso de que sus minimaxes sean iguales (en valor absoluto) y contrarios (en signo). Si el valor común es cero el juego se convierte en un sinsentido. En los juegos de suma no nula, existe tanto la estrategia Minimax como la Maximin. La primera intenta minimizar la ganancia el rival, es decir, busca que el rival tenga el peor resultado, mientras que la segunda intenta maximizar la ganancia propia, es decir, busca que el jugador obtenga el mejor resultado.

El algoritmo Minimax

De forma más detallada, la idea consiste en comenzar en la posición actual del juego y usar el generador de movimientos legales para generar las posibles posiciones sucesivas hasta un cierto límite de niveles (si es posible porque el juego lo permita, se desarrolla el árbol completo de juego hasta las posiciones finales). A continuación, se aplica una función de evaluación estática a los últimos estados obtenidos, que es capaz de decir lo bueno o malo que es cada estado, y se elige la mejor posición para el jugador correspondiente, llevando los valores un nivel hacia atrás para continuar la evaluación en todos los niveles anteriores.

Habitualmente, se suele trabajar con una función de evaluación que devuelve valores positivos para indicar buenas situaciones para el jugador que hace uso del algoritmo, y valores negativos para indicar buenas situaciones para el adversario. Así planteado, el objetivo es maximizar el valor de esta función estática sobre las posibles jugadas que se pueden hacer desde la posición actual del juego.

De este mecanismo es de donde viene el nombre del algoritmo: dada la función evaluadora estática, el jugador que hace uso del algoritmo intenta maximizar su valor, mientras que el adversario intenta minimizarlo. En un árbol de juego donde los valores de la función evaluadora se calculan en relación al jugador maximizante, se maximiza y minimiza alternadamente de un nivel a otro hasta llegar al nivel actual de juego.

Formalmente, los pasos del algoritmo Minimax son:

  1. Generación del árbol de juego: A partir del nodo que representa el estado actual, se generan todos los nodos hasta llegar a un estado terminal (si no podemos afrontar la generación del árbol completo, es posible aplicar los pasos siguientes sobre una sección del mismo, aunque entonces no podremos asegurar la optimalidad de los resultados).
  2. Se calculan los valores de la función de evaluación para cada nodo terminal (o las hojas que hayamos conseguido si no hemos podido construirlo entero) del árbol construido.
  3. Se evalúan los nodos superiores a partir del valor de los inferiores. Según si estos nodos pertenecen a un nivel MAX o un nivel MIN, se elegirán los valores mínimos y máximos representando los movimientos del jugador y del oponente.
  4. Se repite el paso 3 hasta llegar al nodo superior (estado actual).
  5. Se selecciona la jugada-nodo directamente accesible desde el nodo actual que optimiza el valor de la evaluación.

Debe destacarse que, si no es posible realizar el árbol completo de jugadas, pero sí disponemos de una función que permite evaluar lo bueno que es un estado intermedio (por medio de una heurística), aún podemos hacer uso del algoritmo minimax para decidir cuál es el mejor movimiento posible a partir del estado actual. Por supuesto, dependerá de la calidad de la función heurística el que la estrategia que proporciona la ejecución del algoritmo sea ganadora o no. El problema de muchos juegos (como el ajedrez o el Go) es que no es posible trabajar con el árbol completo de jugadas posibles (ya que requiere un árbol con una cantidad de nodos que supera los \(10^{40}\)), y tampoco se conoce una heurística que asegure una estrategia ganadora.

Tal y como está definido, una de las formas más sencillas de implementar Minimax es por medio de un procedimiento recursivo de contrucción del árbol. Cuando no es posible realizar la construcción completa, hay que decidir cuándo se realiza el corte, que puede venir dado por alguna de las condiciones siguientes:

  • Gana algún jugador.
  • Se han explorado \(N\) niveles (con \(N\) un parámetro prefijado).
  • Se ha agotado el tiempo de exploración (parámetro prefijado).
  • Se ha llegado a una situación estática donde no hay grandes cambios de un nivel a otro.

Gracias al uso de agentes, implementar este algoritmo en NetLogo es bastante directo y sencillo.

Al igual que hemos hecho en las implementaciones de búsquedas en espacios de estados, modelaremos los estados como una especie de agentes con las propiedades que necesitemos durante la ejecución del algoritmo:

; Tipo de agente que representa los estados del juego
breed [estados estado]

estados-own [
evaluacion ; Almacena la evaluación del estado (solo válido para los estados finales,
; ya que el resto se calcula por minimax)
player ; Quién juega desde este estado (para saber si se aplica min o max)
gameover? ; Dice si el estado es final o no
nivel ; Indica el nivel dentro del árbol (distancia a la raíz)
]

Para hacerlo lo más general posible, supongamos que tenemos dos funciones: una que devuelve los estados sucesores a uno dado (es un report de tortuga), y otra que recibe como entrada uno de estos estados sucesores y devuelve el movimiento legal que nos lleva a él:

; Función general de sucesores legales (depende del problema que se está resolviendo,
; en este caso es simplemente el sucesor del grafo)
to-report legal-successors
report out-link-neighbors
end

; Función general de obtener el movimiento legal que nos lleva a un estado s'
; (depende del problema que se está resolviendo, en este caso es simplemente
; el link que los une)
to-report legal-move-to [a]
report out-link-to a
end

Supuesto que tenemos una evaluación en los estados finales (el valor de bondad que representa ese estado para el jugador principal), la siguiente función calcula de forma recursiva la evaluación de los estados intermedios, usando el máximo o el mínimo dependiendo de si es un estado en el que mueve el jugador principal o el oponente, hasta llegar al estado raíz sobre el que queremos decidir el mejor movimiento.

Realmente, la función de evaluación asocia a cada estado un par [ ev suc ] que indica no solo la evaluación en este estado (que será el máximo o mínimo de las evaluaciones de sus sucesores, dependiente del jugador), sino también en qué sucesor se consigue ese extremo:

; La función de evaluación es la que hace el cálculo Minimax.
; Lo que calcula en cada nodo es un par (evaluacion sucesor) que indica no solo
; la evaluación (min o max de la evaluacion de los sucesores), sino también en qué
; sucesor se consigue ese extremo (uno de ellos, si hay más de uno)
to-report eval
; Si es un estado final, se devuelve su evaluación directa
ifelse gameover?
[ set label evaluacion
report (list self evaluacion)
]
; Si no, se calcula el máximo o mínimo (dependiendo de si es un nodo en que
; debe jugar el jugador o el oponente) de las evaluaciones de sus sucesores
[ let selected nobody
ifelse player = "me"
[ set selected max-one-of legal-successors [last eval] ]
[ set selected min-one-of legal-successors [last eval] ]
ask legal-move-to selected [
set color blue
set thickness .2
]
set label [last eval] of selected
report (list selected [last eval] of selected)
]
end

En el procedimiento anterior no solo se hace el cálculo de las evaluaciones, sino que se muestran junto al estado correspondiente y se marca el movimiento (link) que suponen las mejores jugadas. En el modelo se representan los estados del jugador MAX en verde y con un triángulo hacia arriba, y los del jugador MIN en rojo y con el triángulo hacia abajo. El estado superior (o más a la izquierda) es el correspondiente a la jugada actual.

En el modelo que puedes encontrar aquí se muestra el algoritmo minimax aplicado sobre árboles aleatorios de partidas de un juego en el que los nodos finales tienen una valoración de 0 a 10. Adicionalmenrte, en el modelo se proporciona un sencillo método que permite representar árboles de forma equilibrada.

Poda alfa-beta de la búsqueda Minimax

El problema de la búsqueda Minimax es que el número de estados a explorar puede llegar a ser exponencial respecto al número de movimientos. Si $r$ es el factor de ramificación (cuántos hijos tiene cada nodo), y $m$ es el nivel de profundidad que vamos a alcanzar, entonces la complejidad en tiempo es del orden $O(r^m)$ y la complejidad en espacio del orden $O(rm)$.

La Poda alfa-beta es una técnica que reduce el número de nodos evaluados en el árbol de juego construido por el algoritmo Minimax. Para ello, trata de eliminar partes grandes del árbol que se va construyendo de forma que se devuelva el mismo movimiento que devolvería este, podando ramas que se sepa que no van a influir en la decisión final.

La ides de esta técnica es que cada nodo se analiza teniendo en cuenta el valor que por el momento tiene y el valor que por el momento tiene su padre, lo que determina en cada momento un intervalo $(\alpha,\beta)$ de posibles valores que podría tomar el nodo. El significado intuitivo de estos parámetros en cada momento es:

  • En los nodos MAX: $\alpha$ es el valor actual del nodo (que tendrá ese valor o superior), y $\beta$ es el valor actual del padre (que tendrá ese valor o inferior).
  • En los nodos MIN: $\beta$ es el valor actual del nodo (que tendrá ese valor o inferior), y $\alpha$ es el valor actual del padre (que tendrá ese valor o superior).
  • La poda se produce si en algún momento $\alpha \geq \beta$, y en ese momento no hace falta analizar los restantes sucesores del nodo. En nodos MIN, se denomina poda $\beta$, y en nodos MAX, poda $\alpha$.

El cambio que se introduce en el minimax habitual es que en esta nueva versión con poda se va actualizando el valor de los parámetros según se recorre el árbol. El método realizará la poda de las ramas restantes cuando el valor actual que se está examinando sea peor que el valor actual de \(\alpha\) o \(\beta\) para MAX o MIN, respectivamente:

function minimax(nodo, nivel, esJugadorMax, alpha, beta):

    Si nodo es una hoja:
        return valor del nodo
    
    Si esJugadorMAx :
        mejorVal = -INFINITY 
        Para cada hijo de nodo :
            valor = minimax(nodo, nivel+1, false, alpha, beta)
            mejorVal = max( mejorVal, valor) 
            alpha = max( alpha, mejorVal)
            Si beta <= alpha:
                parar
        return mejorVal

    Si no :
        mejorVal = +INFINITY 
        Para cada hijo de nodo :
            valor = minimax(nodo, nivel+1, true, alpha, beta)
            mejorVal = min( mejorVal, valor) 
            beta = min( beta, mejorVal)
            Si beta <= alpha:
                parar
        return mejorVal
// La llamada a la función por primera vez sería:
minimax(0, 0, true, -INFINITY, +INFINITY)

Es interesante estudiar cómo mejora el rendimiento del algoritmo en el mejor de los casos, para saber si realmente se ha mejorado de alguna forma el algoritmo original. Según Knuth y Moore, si los nodos están perfectamente ordenados, el número de nodos terminales considerados en una búsqueda de profundidad \(m\) con poda alfa-beta es el doble de los nodos terminales considerados en una búsqueda de profundidad \(m/2\) sin poda alfa-beta. Esto significa que en el caso perfecto, una búsqueda que haga uso de la poda alfa-beta proporciona una ganancia considerable, ya que permite explorar con igual costo la mitad de los nodos finales de un árbol con el doble de profundidad.

Hay otras opciones para realizar podas, como por ejemplo dejar de explorar un subárbol que ofrece pocas posibilidades de mejora sobre otros caminos que ya han sido explorados, es decir, que su evaluación sea solo ligeramente más favorable que otra rama ya explorada.

El siguiente conjunto de transparencias expone las ideas anteriores sobre el caso del 3 en raya:

Análisis y Simulación de Decisiones de José Enrique Alvarez Estrada

« Métodos combinados de… « || Inicio || » Programación Funciona… »