Tutorial sobre bandidos armados múltiples en TF-Agents

Empezar

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Configuración

Si no ha instalado las siguientes dependencias, ejecute:

pip install tf-agents

Importaciones

import abc
import numpy as np
import tensorflow as tf

from tf_agents.agents import tf_agent
from tf_agents.drivers import driver
from tf_agents.environments import py_environment
from tf_agents.environments import tf_environment
from tf_agents.environments import tf_py_environment
from tf_agents.policies import tf_policy
from tf_agents.specs import array_spec
from tf_agents.specs import tensor_spec
from tf_agents.trajectories import time_step as ts
from tf_agents.trajectories import trajectory
from tf_agents.trajectories import policy_step

nest = tf.nest

Introducción

El problema del bandido de armas múltiples (MAB) es un caso especial de aprendizaje por refuerzo: un agente recolecta recompensas en un entorno tomando algunas acciones después de observar algún estado del entorno. La principal diferencia entre RL general y MAB es que en MAB, asumimos que la acción tomada por el agente no influye en el siguiente estado del medio ambiente. Por lo tanto, los agentes no modelan las transiciones de estado, no atribuyen recompensas a acciones pasadas ni "planifican con anticipación" para llegar a estados ricos en recompensas.

Al igual que en otros dominios RL, el objetivo de un agente de MAB es encontrar una política que se acumula tanta recompensa como sea posible. Sin embargo, sería un error intentar siempre explotar la acción que promete la mayor recompensa, porque entonces existe la posibilidad de que perdamos mejores acciones si no exploramos lo suficiente. Este es el principal problema a resolver en (MAB), a menudo llamado el dilema de exploración de la explotación.

Ambientes Bandit, políticas, y agentes para MAB se pueden encontrar en los subdirectorios de tf_agents / bandidos .

Ambientes

En TF-agentes, la clase de entorno sirve la función de dar información sobre el estado actual (esto se llama observación o contexto), la recepción de una acción como entrada, la realización de una transición de estado, y la salida de una recompensa. Esta clase también se encarga de reiniciar cuando termina un episodio, para que pueda comenzar un nuevo episodio. Esto se realiza llamando a un reset función cuando un estado es etiquetado como "último" del episodio.

Para más detalles, ver los TF-Agentes entornos tutorial .

Como se mencionó anteriormente, MAB se diferencia del RL general en que las acciones no influyen en la siguiente observación. Otra diferencia es que en Bandits no hay "episodios": cada paso de tiempo comienza con una nueva observación, independientemente de los pasos de tiempo anteriores.

Para asegurarse de que las observaciones son independientes y a distancia abstracta del concepto de episodios RL, introducimos subclases de PyEnvironment y TFEnvironment : BanditPyEnvironment y BanditTFEnvironment . Estas clases exponen dos funciones miembro privadas que quedan por implementar por parte del usuario:

@abc.abstractmethod
def _observe(self):

y

@abc.abstractmethod
def _apply_action(self, action):

El _observe función devuelve una observación. Luego, la política elige una acción basada en esta observación. El _apply_action recibe esa acción como una entrada, y devuelve la recompensa correspondiente. Estas funciones miembro privadas son llamados por las funciones reset y step , respectivamente.

class BanditPyEnvironment(py_environment.PyEnvironment):

  def __init__(self, observation_spec, action_spec):
    self._observation_spec = observation_spec
    self._action_spec = action_spec
    super(BanditPyEnvironment, self).__init__()

  # Helper functions.
  def action_spec(self):
    return self._action_spec

  def observation_spec(self):
    return self._observation_spec

  def _empty_observation(self):
    return tf.nest.map_structure(lambda x: np.zeros(x.shape, x.dtype),
                                 self.observation_spec())

  # These two functions below should not be overridden by subclasses.
  def _reset(self):
    """Returns a time step containing an observation."""
    return ts.restart(self._observe(), batch_size=self.batch_size)

  def _step(self, action):
    """Returns a time step containing the reward for the action taken."""
    reward = self._apply_action(action)
    return ts.termination(self._observe(), reward)

  # These two functions below are to be implemented in subclasses.
  @abc.abstractmethod
  def _observe(self):
    """Returns an observation."""

  @abc.abstractmethod
  def _apply_action(self, action):
    """Applies `action` to the Environment and returns the corresponding reward.
    """

Los anteriores instrumentos provisionales clase abstracta PyEnvironment 's _reset y _step funciones y expone las funciones abstractas _observe y _apply_action a ser implementadas por las subclases.

Una clase de entorno de ejemplo simple

La siguiente clase da un entorno muy simple para el cual la observación es un número entero aleatorio entre -2 y 2, hay 3 acciones posibles (0, 1, 2) y la recompensa es el producto de la acción y la observación.

class SimplePyEnvironment(BanditPyEnvironment):

  def __init__(self):
    action_spec = array_spec.BoundedArraySpec(
        shape=(), dtype=np.int32, minimum=0, maximum=2, name='action')
    observation_spec = array_spec.BoundedArraySpec(
        shape=(1,), dtype=np.int32, minimum=-2, maximum=2, name='observation')
    super(SimplePyEnvironment, self).__init__(observation_spec, action_spec)

  def _observe(self):
    self._observation = np.random.randint(-2, 3, (1,), dtype='int32')
    return self._observation

  def _apply_action(self, action):
    return action * self._observation

Ahora podemos usar este entorno para obtener observaciones y recibir recompensas por nuestras acciones.

environment = SimplePyEnvironment()
observation = environment.reset().observation
print("observation: %d" % observation)

action = 2

print("action: %d" % action)
reward = environment.step(action).reward
print("reward: %f" % reward)
observation: -1
action: 2
reward: -2.000000

Entornos TF

Se puede definir un entorno de bandido subclasificando BanditTFEnvironment , o, de manera similar a los entornos de RL, se puede definir un BanditPyEnvironment y se envuelve con TFPyEnvironment . En aras de la simplicidad, elegimos la última opción en este tutorial.

tf_environment = tf_py_environment.TFPyEnvironment(environment)

Políticas

Una política en un problema bandido funciona de la misma manera que en un problema de RL: proporciona una acción (o una distribución de acciones), dada una observación como entrada.

Para más detalles, ver el tutorial TF-Agentes Política .

Al igual que con los entornos, hay dos maneras de construir una política: se puede crear un PyPolicy y se envuelve con TFPyPolicy , o crear directamente una TFPolicy . Aquí elegimos ir con el método directo.

Dado que este ejemplo es bastante simple, podemos definir la política óptima manualmente. La acción solo depende del signo de la observación, 0 cuando es negativo y 2 cuando es positivo.

class SignPolicy(tf_policy.TFPolicy):
  def __init__(self):
    observation_spec = tensor_spec.BoundedTensorSpec(
        shape=(1,), dtype=tf.int32, minimum=-2, maximum=2)
    time_step_spec = ts.time_step_spec(observation_spec)

    action_spec = tensor_spec.BoundedTensorSpec(
        shape=(), dtype=tf.int32, minimum=0, maximum=2)

    super(SignPolicy, self).__init__(time_step_spec=time_step_spec,
                                     action_spec=action_spec)
  def _distribution(self, time_step):
    pass

  def _variables(self):
    return ()

  def _action(self, time_step, policy_state, seed):
    observation_sign = tf.cast(tf.sign(time_step.observation[0]), dtype=tf.int32)
    action = observation_sign + 1
    return policy_step.PolicyStep(action, policy_state)

Ahora podemos solicitar una observación del entorno, llamar a la política para elegir una acción, luego el entorno generará la recompensa:

sign_policy = SignPolicy()

current_time_step = tf_environment.reset()
print('Observation:')
print (current_time_step.observation)
action = sign_policy.action(current_time_step).action
print('Action:')
print (action)
reward = tf_environment.step(action).reward
print('Reward:')
print(reward)
Observation:
tf.Tensor([[2]], shape=(1, 1), dtype=int32)
Action:
tf.Tensor([2], shape=(1,), dtype=int32)
Reward:
tf.Tensor([[4.]], shape=(1, 1), dtype=float32)

La forma en que se implementan los entornos de bandidos garantiza que cada vez que damos un paso, no solo recibimos la recompensa por la acción que tomamos, sino también la siguiente observación.

step = tf_environment.reset()
action = 1
next_step = tf_environment.step(action)
reward = next_step.reward
next_observation = next_step.observation
print("Reward: ")
print(reward)
print("Next observation:")
print(next_observation)
Reward: 
tf.Tensor([[-2.]], shape=(1, 1), dtype=float32)
Next observation:
tf.Tensor([[0]], shape=(1, 1), dtype=int32)

Agentes

Ahora que tenemos entornos de bandidos y políticas de bandidos, es hora de definir también agentes bandidos, que se encargan de cambiar la política en base a muestras de entrenamiento.

El API para los agentes de bandidos no difiere del de los agentes RL: el agente sólo necesita para implementar el _initialize y _train métodos, y definir una policy y un collect_policy .

Un entorno más complicado

Antes de escribir nuestro agente bandido, necesitamos tener un entorno que sea un poco más difícil de entender. Para condimentar las cosas un poco, el siguiente entorno sea siempre dará reward = observation * action o reward = -observation * action . Esto se decidirá cuando se inicialice el entorno.

class TwoWayPyEnvironment(BanditPyEnvironment):

  def __init__(self):
    action_spec = array_spec.BoundedArraySpec(
        shape=(), dtype=np.int32, minimum=0, maximum=2, name='action')
    observation_spec = array_spec.BoundedArraySpec(
        shape=(1,), dtype=np.int32, minimum=-2, maximum=2, name='observation')

    # Flipping the sign with probability 1/2.
    self._reward_sign = 2 * np.random.randint(2) - 1
    print("reward sign:")
    print(self._reward_sign)

    super(TwoWayPyEnvironment, self).__init__(observation_spec, action_spec)

  def _observe(self):
    self._observation = np.random.randint(-2, 3, (1,), dtype='int32')
    return self._observation

  def _apply_action(self, action):
    return self._reward_sign * action * self._observation[0]

two_way_tf_environment = tf_py_environment.TFPyEnvironment(TwoWayPyEnvironment())
reward sign:
-1

Una política más complicada

Un entorno más complicado requiere una política más complicada. Necesitamos una política que detecte el comportamiento del entorno subyacente. Hay tres situaciones que la política debe manejar:

  1. El agente no ha detectado saber todavía qué versión del entorno se está ejecutando.
  2. El agente detectó que se está ejecutando la versión original del entorno.
  3. El agente detectó que se está ejecutando la versión invertida del entorno.

Definimos una tf_variable llamado _situation para almacenar esta información codificada como valores en [0, 2] , a continuación, hacer que la política se comportan en consecuencia.

class TwoWaySignPolicy(tf_policy.TFPolicy):
  def __init__(self, situation):
    observation_spec = tensor_spec.BoundedTensorSpec(
        shape=(1,), dtype=tf.int32, minimum=-2, maximum=2)
    action_spec = tensor_spec.BoundedTensorSpec(
        shape=(), dtype=tf.int32, minimum=0, maximum=2)
    time_step_spec = ts.time_step_spec(observation_spec)
    self._situation = situation
    super(TwoWaySignPolicy, self).__init__(time_step_spec=time_step_spec,
                                           action_spec=action_spec)
  def _distribution(self, time_step):
    pass

  def _variables(self):
    return [self._situation]

  def _action(self, time_step, policy_state, seed):
    sign = tf.cast(tf.sign(time_step.observation[0, 0]), dtype=tf.int32)
    def case_unknown_fn():
      # Choose 1 so that we get information on the sign.
      return tf.constant(1, shape=(1,))

    # Choose 0 or 2, depending on the situation and the sign of the observation.
    def case_normal_fn():
      return tf.constant(sign + 1, shape=(1,))
    def case_flipped_fn():
      return tf.constant(1 - sign, shape=(1,))

    cases = [(tf.equal(self._situation, 0), case_unknown_fn),
             (tf.equal(self._situation, 1), case_normal_fn),
             (tf.equal(self._situation, 2), case_flipped_fn)]
    action = tf.case(cases, exclusive=True)
    return policy_step.PolicyStep(action, policy_state)

El agente

Ahora es el momento de definir el agente que detecta la señal del entorno y establece la política de forma adecuada.

class SignAgent(tf_agent.TFAgent):
  def __init__(self):
    self._situation = tf.Variable(0, dtype=tf.int32)
    policy = TwoWaySignPolicy(self._situation)
    time_step_spec = policy.time_step_spec
    action_spec = policy.action_spec
    super(SignAgent, self).__init__(time_step_spec=time_step_spec,
                                    action_spec=action_spec,
                                    policy=policy,
                                    collect_policy=policy,
                                    train_sequence_length=None)

  def _initialize(self):
    return tf.compat.v1.variables_initializer(self.variables)

  def _train(self, experience, weights=None):
    observation = experience.observation
    action = experience.action
    reward = experience.reward

    # We only need to change the value of the situation variable if it is
    # unknown (0) right now, and we can infer the situation only if the
    # observation is not 0.
    needs_action = tf.logical_and(tf.equal(self._situation, 0),
                                  tf.not_equal(reward, 0))


    def new_situation_fn():
      """This returns either 1 or 2, depending on the signs."""
      return (3 - tf.sign(tf.cast(observation[0, 0, 0], dtype=tf.int32) *
                          tf.cast(action[0, 0], dtype=tf.int32) *
                          tf.cast(reward[0, 0], dtype=tf.int32))) / 2

    new_situation = tf.cond(needs_action,
                            new_situation_fn,
                            lambda: self._situation)
    new_situation = tf.cast(new_situation, tf.int32)
    tf.compat.v1.assign(self._situation, new_situation)
    return tf_agent.LossInfo((), ())

sign_agent = SignAgent()

En el código anterior, el agente define la política, y la variable situation es compartida por el agente y la política.

Además, el parámetro de experience de la _train función es una trayectoria:

Trayectorias

En la carretera TF-agentes, trajectories se nombran tuplas que contienen muestras de los pasos anteriores tomadas. Luego, el agente utiliza estas muestras para entrenar y actualizar la política. En RL, las trayectorias deben contener información sobre el estado actual, el siguiente estado y si el episodio actual ha terminado. Dado que en el mundo Bandit no necesitamos estas cosas, configuramos una función auxiliar para crear una trayectoria:

# We need to add another dimension here because the agent expects the
# trajectory of shape [batch_size, time, ...], but in this tutorial we assume
# that both batch size and time are 1. Hence all the expand_dims.

def trajectory_for_bandit(initial_step, action_step, final_step):
  return trajectory.Trajectory(observation=tf.expand_dims(initial_step.observation, 0),
                               action=tf.expand_dims(action_step.action, 0),
                               policy_info=action_step.info,
                               reward=tf.expand_dims(final_step.reward, 0),
                               discount=tf.expand_dims(final_step.discount, 0),
                               step_type=tf.expand_dims(initial_step.step_type, 0),
                               next_step_type=tf.expand_dims(final_step.step_type, 0))

Capacitación de un agente

Ahora todas las piezas están listas para entrenar a nuestro agente bandido.

step = two_way_tf_environment.reset()
for _ in range(10):
  action_step = sign_agent.collect_policy.action(step)
  next_step = two_way_tf_environment.step(action_step.action)
  experience = trajectory_for_bandit(step, action_step, next_step)
  print(experience)
  sign_agent.train(experience)
  step = next_step
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[1]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[0]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[1]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[-1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[1.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[-1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[2.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[-2]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[4.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[2]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})
Trajectory(
{'action': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[0]], dtype=int32)>,
 'discount': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'next_step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>,
 'observation': <tf.Tensor: shape=(1, 1, 1), dtype=int32, numpy=array([[[1]]], dtype=int32)>,
 'policy_info': (),
 'reward': <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.]], dtype=float32)>,
 'step_type': <tf.Tensor: shape=(1, 1), dtype=int32, numpy=array([[2]], dtype=int32)>})

A partir del resultado, se puede ver que después del segundo paso (a menos que la observación fuera 0 en el primer paso), la política elige la acción de la manera correcta y, por lo tanto, la recompensa recolectada siempre es no negativa.

Un verdadero ejemplo de bandido contextual

En el resto de este tutorial, usamos los pre-implementado ambientes y agentes de la biblioteca TF-Agentes Bandits.

# Imports for example.
from tf_agents.bandits.agents import lin_ucb_agent
from tf_agents.bandits.environments import stationary_stochastic_py_environment as sspe
from tf_agents.bandits.metrics import tf_metrics
from tf_agents.drivers import dynamic_step_driver
from tf_agents.replay_buffers import tf_uniform_replay_buffer

import matplotlib.pyplot as plt

Entorno estocástico estacionario con funciones de pago lineal

El entorno utilizado en este ejemplo es la StationaryStochasticPyEnvironment . Este entorno toma como parámetro una función (generalmente ruidosa) para dar observaciones (contexto), y para cada brazo toma una función (también ruidosa) que calcula la recompensa basada en la observación dada. En nuestro ejemplo, tomamos muestras del contexto de manera uniforme a partir de un cubo de dimensión d, y las funciones de recompensa son funciones lineales del contexto, más algo de ruido gaussiano.

batch_size = 2 # @param
arm0_param = [-3, 0, 1, -2] # @param
arm1_param = [1, -2, 3, 0] # @param
arm2_param = [0, 0, 1, 1] # @param
def context_sampling_fn(batch_size):
  """Contexts from [-10, 10]^4."""
  def _context_sampling_fn():
    return np.random.randint(-10, 10, [batch_size, 4]).astype(np.float32)
  return _context_sampling_fn

class LinearNormalReward(object):
  """A class that acts as linear reward function when called."""
  def __init__(self, theta, sigma):
    self.theta = theta
    self.sigma = sigma
  def __call__(self, x):
    mu = np.dot(x, self.theta)
    return np.random.normal(mu, self.sigma)

arm0_reward_fn = LinearNormalReward(arm0_param, 1)
arm1_reward_fn = LinearNormalReward(arm1_param, 1)
arm2_reward_fn = LinearNormalReward(arm2_param, 1)

environment = tf_py_environment.TFPyEnvironment(
    sspe.StationaryStochasticPyEnvironment(
        context_sampling_fn(batch_size),
        [arm0_reward_fn, arm1_reward_fn, arm2_reward_fn],
        batch_size=batch_size))

El agente de LinUCB

El agente de abajo implementa el LinUCB algoritmo.

observation_spec = tensor_spec.TensorSpec([4], tf.float32)
time_step_spec = ts.time_step_spec(observation_spec)
action_spec = tensor_spec.BoundedTensorSpec(
    dtype=tf.int32, shape=(), minimum=0, maximum=2)

agent = lin_ucb_agent.LinearUCBAgent(time_step_spec=time_step_spec,
                                     action_spec=action_spec)

Métrica de arrepentimiento

Métrica más importante bandidos es pesar, calculado como la diferencia entre el premio recogido por el agente y la recompensa esperada de una política de Oracle que tiene acceso a las funciones de recompensa del medio ambiente. El RegretMetric por lo tanto necesita una función baseline_reward_fn que calcula la mejor recompensa esperada alcanzable dada una observación. Para nuestro ejemplo, necesitamos tomar el máximo de los equivalentes sin ruido de las funciones de recompensa que ya definimos para el entorno.

def compute_optimal_reward(observation):
  expected_reward_for_arms = [
      tf.linalg.matvec(observation, tf.cast(arm0_param, dtype=tf.float32)),
      tf.linalg.matvec(observation, tf.cast(arm1_param, dtype=tf.float32)),
      tf.linalg.matvec(observation, tf.cast(arm2_param, dtype=tf.float32))]
  optimal_action_reward = tf.reduce_max(expected_reward_for_arms, axis=0)
  return optimal_action_reward

regret_metric = tf_metrics.RegretMetric(compute_optimal_reward)

Capacitación

Ahora reunimos todos los componentes que presentamos anteriormente: el medio ambiente, la política y el agente. Llevamos a cabo la política de los datos del entorno y de formación de salida con la ayuda de un conductor, y capacitar a los agentes sobre los datos.

Tenga en cuenta que hay dos parámetros que juntos especifican el número de pasos realizados. num_iterations especifica cuántas veces se corre el bucle de entrenador, mientras que el conductor se llevará a steps_per_loop pasos por iteración. La razón principal para mantener ambos parámetros es que algunas operaciones se realizan por iteración, mientras que otras las realiza el controlador en cada paso. Por ejemplo, el agente de train función sólo se llama una vez por iteración. La compensación aquí es que si entrenamos con más frecuencia, entonces nuestra política es "más fresca", por otro lado, la capacitación en lotes más grandes podría ser más eficiente en el tiempo.

num_iterations = 90 # @param
steps_per_loop = 1 # @param

replay_buffer = tf_uniform_replay_buffer.TFUniformReplayBuffer(
    data_spec=agent.policy.trajectory_spec,
    batch_size=batch_size,
    max_length=steps_per_loop)

observers = [replay_buffer.add_batch, regret_metric]

driver = dynamic_step_driver.DynamicStepDriver(
    env=environment,
    policy=agent.collect_policy,
    num_steps=steps_per_loop * batch_size,
    observers=observers)

regret_values = []

for _ in range(num_iterations):
  driver.run()
  loss_info = agent.train(replay_buffer.gather_all())
  replay_buffer.clear()
  regret_values.append(regret_metric.result())

plt.plot(regret_values)
plt.ylabel('Average Regret')
plt.xlabel('Number of Iterations')
WARNING:tensorflow:From /tmp/ipykernel_11392/3138849230.py:21: ReplayBuffer.gather_all (from tf_agents.replay_buffers.replay_buffer) is deprecated and will be removed in a future version.
Instructions for updating:
Use `as_dataset(..., single_deterministic_pass=True)` instead.
Text(0.5, 0, 'Number of Iterations')

png

Después de ejecutar el último fragmento de código, la trama resultante (con suerte) muestra que el arrepentimiento promedio está disminuyendo a medida que se entrena al agente y la política mejora para determinar cuál es la acción correcta, dada la observación.

¿Que sigue?

Para ver ejemplos más trabajo, por favor ver el bandidos / agentes / Ejemplos directorio que tiene ejemplos listos para funcionar para diferentes agentes y entornos.

La biblioteca TF-Agents también es capaz de manejar bandidos de armas múltiples con funciones por brazo. A tal efecto, nos referimos al lector al bandido por el brazo tutorial .