stringtranslate.com

Programación dinámica estocástica

Introducida originalmente por Richard E. Bellman en (Bellman 1957), la programación dinámica estocástica es una técnica para modelar y resolver problemas de toma de decisiones en condiciones de incertidumbre . Estrechamente relacionada con la programación estocástica y la programación dinámica , la programación dinámica estocástica representa el problema en cuestión en forma de una ecuación de Bellman . El objetivo es calcular una política que prescriba cómo actuar de manera óptima ante la incertidumbre.

Un ejemplo motivador: el juego de azar

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 .

Estrategia de apuestas óptima.
Una estrategia de apuestas óptima que maximiza la probabilidad del jugador de obtener una riqueza de al menos $6 al final del horizonte de apuestas; representa el monto de la apuesta para el juego cuando el jugador tiene $ al comienzo de ese juego. Si el que toma la decisión sigue esta política, con una probabilidad de 0,1984 obtendrá una riqueza de al menos $6.

Antecedentes formales

Consideremos un sistema discreto definido en etapas en el que cada etapa se caracteriza por

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

Los procesos de decisión de Markov representan una clase especial de programas dinámicos estocásticos en los que el proceso estocástico subyacente es un proceso estacionario que presenta la propiedad de Markov .

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

Sea la probabilidad de que, al final del juego 4, el jugador tenga al menos $6, dado que tenía $ al comienzo del juego .

Para derivar la ecuación funcional , defina como una apuesta que alcanza , entonces al comienzo del juego

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 ,  wealth def  __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 inmediato                                                           self . cache_actions  =  {}  # caché con pares estado/acción óptimos def  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 ) ] # acciones                         v  =  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 esperado        instancia  =  {  "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 dinámica aproximada

Powell (2009) ofrece una introducción a la programación dinámica aproximada .

Lectura adicional

Véase también

Referencias

  1. ^ 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.