Un jugador tiene $2, puede jugar un juego de azar 4 veces y su objetivo es maximizar su probabilidad de terminar con al menos $6. Si el jugador apuesta $ en una jugada del juego, entonces con una probabilidad de 0,4 gana el juego, recupera la apuesta inicial y aumenta su posición de capital en $ ; con una probabilidad de 0,6, pierde el monto apostado $ ; todas las jugadas son independientes por pares . En cualquier jugada del juego, el jugador no puede apostar más dinero del que tiene disponible al comienzo de esa jugada. [1]
Se puede emplear la programación dinámica estocástica para modelar este problema y determinar una estrategia de apuestas que, por ejemplo, maximice la probabilidad del jugador de alcanzar una riqueza de al menos $6 al final del horizonte de apuestas.
Téngase en cuenta que si no hay límite en el número de juegos que se pueden jugar, el problema se convierte en una variante de la conocida paradoja de San Petersburgo .
Antecedentes formales
Consideremos un sistema discreto definido en etapas en el que cada etapa se caracteriza por
un estado inicial , donde es el conjunto de estados factibles al comienzo de la etapa ;
una variable de decisión , donde es el conjunto de acciones factibles en la etapa – tenga en cuenta que puede ser una función del estado inicial ;
una función de costo/recompensa inmediata , que representa el costo/recompensa en la etapa si es el estado inicial y la acción seleccionada;
una función de transición de estado que conduce al sistema hacia el estado .
Sea la ecuación costo/recompensa óptima que se obtiene al seguir una política óptima a lo largo de las etapas . Sin perder generalidad, en lo que sigue consideraremos una configuración de maximización de recompensa. En la programación dinámica determinista, normalmente se trabaja con ecuaciones funcionales que adoptan la siguiente estructura:
donde y la condición de contorno del sistema es
El objetivo es determinar el conjunto de acciones óptimas que maximizan . Dado el estado actual y la acción actual , sabemos con certeza la recompensa asegurada durante la etapa actual y, gracias a la función de transición de estado , el estado futuro hacia el que transita el sistema.
En la práctica, sin embargo, incluso si conocemos el estado del sistema al comienzo de la etapa actual, así como la decisión tomada, el estado del sistema al comienzo de la etapa siguiente y la recompensa del período actual son a menudo variables aleatorias que solo se pueden observar al final de la etapa actual.
La programación dinámica estocástica se ocupa de problemas en los que la recompensa del período actual y/o el estado del período siguiente son aleatorios, es decir, con sistemas estocásticos de múltiples etapas. El objetivo del responsable de la toma de decisiones es maximizar la recompensa esperada (descontada) durante un horizonte de planificación determinado.
En su forma más general, los programas dinámicos estocásticos tratan con ecuaciones funcionales que toman la siguiente estructura
dónde
es la recompensa máxima esperada que se puede obtener durante las etapas , dado el estado al inicio de la etapa ;
pertenece al conjunto de acciones factibles en una etapa dada del estado inicial ;
El juego de azar como programa dinámico estocástico
El juego de apuestas se puede formular como un programa dinámico estocástico de la siguiente manera: hay juegos (es decir, etapas ) en el horizonte de planificación
el estado en el periodo representa la riqueza inicial al comienzo del periodo ;
La acción dada en el estado del período es el monto de la apuesta ;
La probabilidad de transición de un estado a otro cuando se realiza una acción en el estado se deriva fácilmente de la probabilidad de ganar (0,4) o perder (0,6) un juego.
Sea la probabilidad de que, al final del juego 4, el jugador tenga al menos $6, dado que tenía $ al comienzo del juego .
El beneficio inmediato que se obtiene si se toma una acción en el estado viene dado por el valor esperado .
Para derivar la ecuación funcional , defina como una apuesta que alcanza , entonces al comienzo del juego
si es imposible alcanzar el objetivo, es decir para ;
si se alcanza el objetivo, es decir para ;
si el jugador debe apostar lo suficiente para alcanzar el objetivo, es decir , .
Para la ecuación funcional es , donde varía en ; el objetivo es encontrar .
Dada la ecuación funcional, se puede obtener una política de apuestas óptima mediante algoritmos de recursión hacia adelante o hacia atrás, como se describe a continuación.
Métodos de solución
Los programas dinámicos estocásticos se pueden resolver de manera óptima mediante algoritmos de recursión hacia atrás o hacia adelante. La memorización se emplea normalmente para mejorar el rendimiento. Sin embargo, al igual que la programación dinámica determinista, su variante estocástica también sufre la maldición de la dimensionalidad . Por este motivo, los métodos de solución aproximada se emplean normalmente en aplicaciones prácticas.
Recursión hacia atrás
Dado un espacio de estados acotado, la recursión hacia atrás (Bertsekas 2000) comienza tabulando para cada estado posible perteneciente a la etapa final . Una vez que estos valores se tabulan, junto con las acciones dependientes del estado óptimas asociadas , es posible pasar a la etapa y tabular para todos los estados posibles pertenecientes a la etapa . El proceso continúa considerando de manera regresiva todas las etapas restantes hasta la primera. Una vez que se completa este proceso de tabulación, el valor de una política óptima dado el estado inicial , así como la acción óptima asociada , se pueden recuperar fácilmente de la tabla. Dado que el cálculo se realiza de manera regresiva, está claro que la recursión hacia atrás puede conducir al cálculo de una gran cantidad de estados que no son necesarios para el cálculo de .
Ejemplo: Juego de apuestas
Recursión hacia adelante
Dado el estado inicial del sistema al comienzo del período 1, la recursión hacia adelante (Bertsekas 2000) calcula expandiendo progresivamente la ecuación funcional ( paso hacia adelante ). Esto implica llamadas recursivas para todo lo que es necesario para calcular un determinado . El valor de una política óptima y su estructura se recuperan luego a través de un ( paso hacia atrás ) en el que se resuelven estas llamadas recursivas suspendidas. Una diferencia clave con la recursión hacia atrás es el hecho de que se calcula solo para los estados que son relevantes para el cálculo de . La memorización se emplea para evitar el recálculo de estados que ya se han considerado.
Ejemplo: Juego de apuestas
Ilustraremos la recursión hacia adelante en el contexto de la instancia del juego de apuestas discutida anteriormente. Comenzamos el pase hacia adelante considerando
En este punto, aún no hemos calculado los elementos necesarios para calcularlos ; procedemos a calcularlos. Tenga en cuenta que , por lo tanto, se puede aprovechar la memorización y realizar los cálculos necesarios solo una vez.
Cálculo de
Ya hemos calculado todo lo necesario para calcular . Sin embargo, esto ha dado lugar a recursiones suspendidas adicionales que involucran . Procedemos y calculamos estos valores.
Cálculo de
Dado que la etapa 4 es la última etapa de nuestro sistema, representamos condiciones de contorno que se calculan fácilmente de la siguiente manera.
Condiciones de contorno
En este punto es posible proceder y recuperar la política óptima y su valor a través de un pase hacia atrás que involucra, en primer lugar, la etapa 3.
Pase hacia atrás que involucra
y luego, la etapa 2.
Pase hacia atrás que involucra
Por fin recuperamos el valor de una política óptima
Esta es la política óptima que se ha ilustrado anteriormente. Nótese que existen múltiples políticas óptimas que conducen al mismo valor óptimo ; por ejemplo, en el primer juego se puede apostar $1 o $2.
Implementación en Python. La siguiente es una implementación completa en Python de este ejemplo.
desde escribir import List , Tuple import functoolsclase memoize : def __init __ ( self , func ) : self.func = func self.memoized = { } self.metodo_cache = { }def __call __ ( self , * args ) : return self.cache_get ( self.memoized , args , lambda : self.func ( * args ) )def __get__ ( self , obj , objtype ): return self . cache_get ( self . method_cache , obj , lambda : self . __class__ ( functools . partial ( self . func , obj )), )def cache_get ( self , cache , clave , func ): try : devolver cache [ clave ] excepto KeyError : cache [ clave ] = func () devolver cache [ clave ]def reset ( self ) : self.memoized = { } self.method_cache = { }Clase Estado : """el estado del problema de la ruina del jugador"""def __init__ ( self , t : int , wealth : float ): """constructor de estado Argumentos: t {int} -- riqueza del período de tiempo {float} -- riqueza inicial """ self . t , self . wealth = t , wealthdef __eq__ ( self , other ): devuelve self . __dict__ == other . __dict__def __str __ ( self ) : devuelve str ( self.t ) + " " + str ( self.riqueza )def __hash__ ( self ): devuelve hash ( str ( self ))clase GamblersRuin : def __init__ ( self , bettingHorizon : int , targetWealth : float , pmf : List [ List [ Tuple [ int , float ]]], ): """El problema de la ruina del jugador Argumentos: bettingHorizon {int} -- horizonte de apuestas targetWealth {float} -- función de masa de probabilidad de riqueza objetivo {List[List[Tuple[int, float]]]} """# inicializar las variables de instancia self . bettingHorizon , self . targetWealth , self . pmf = ( bettingHorizon , targetWealth , pmf , )# lambdas self.ag = lambda s : [ i para i en rango ( 0 , min ( self.targetWealth // 2 , s.wealth ) + 1 ) ] # generador de acciones self.st = lambda s , a , r : State ( s.t + 1 , s.wealth - a + a * r ) # transición de estado self.iv = ( lambda s , a , r : 1 si s.wealth - a + a * r > = self.targetWealth de lo contrario 0 ) # función de valor inmediatoself . cache_actions = {} # caché con pares estado/acción óptimosdef f ( self , riqueza : float ) -> float : s = Estado ( 0 , riqueza ) return self . _f ( s )def q ( self , t : int , riqueza : float ) - > float : s = Estado ( t , riqueza ) return self.cache_actions [ str ( s ) ]@memoize def _f ( self , s : State ) -> float : # Recursión hacia adelante values = [ suma ( [ p [ 1 ] * ( self._f ( self.st ( s , a , p [ 0 ] ) ) if s.t < self.bettingHorizon - 1 else self.iv ( s , a , p [ 0 ] ) ) # función de valor para p en self.pmf [ s.t ] ] ) # realizaciones de apuestas para a en self.ag ( s ) ] # accionesv = max ( values ) try : self.cache_actions [ str ( s )] = self.ag ( s ) [ values.index ( v ) ] # almacena la mejor acción excepto ValueError : self.cache_actions [ str ( s )] = None print ( "Error al recuperar la mejor acción" ) return v # devuelve el costo total esperadoinstancia = { "bettingHorizon" : 4 , "targetWealth" : 6 , "pmf" : [[( 0 , 0.6 ), ( 2 , 0.4 )] para i en el rango ( 0 , 4 )], } gr , riqueza_inicial = GamblersRuin ( ** instancia ), 2# f_1(x) es la probabilidad del jugador de alcanzar $targetWealth al final de bettingHorizon print ( "f_1(" + str ( initial_wealth ) + "): " + str ( gr . f ( initial_wealth )))# Recupere la acción óptima para el período 2 cuando la riqueza inicial al comienzo del período 2 es $1. t , initial_wealth = 1 , 1 print ( "b_" + str ( t + 1 ) + "(" + str ( initial_wealth ) + "): " + str ( gr . q ( t , initial_wealth )) )
Implementación de Java. GamblersRuin.java es una implementación independiente de Java 8 del ejemplo anterior.
Programación estocástica : marco para modelar problemas de optimización que involucran incertidumbre
Referencias
^ Este problema está adaptado de WL Winston, Investigación de operaciones: aplicaciones y algoritmos (7.ª edición), Duxbury Press, 2003, capítulo 19, ejemplo 3.