0
$\begingroup$

I'm trying to figure out how action masking works and the closest workaround i get is following the hanabi environment example and then write my own custom version to learn more:

it is my very basic choosing numbers game working with action_mask

""" based on RL environment for Hanabi, using an API similar to OpenAI Gym.
when obs return [0,0] available actions are [0,1,2]
when obs return [1,0] available actions are [0,2,3]
when obs return [0,1] available actions are [0,1,4]
when obs return [1,1] available actions are [0,3,4]
"""
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 
import numpy as np
import gin.tf
from tf_agents.environments import py_environment
from tf_agents.specs import array_spec
from tf_agents.trajectories import time_step as ts

class CustomMaskedEnvironment(py_environment.PyEnvironment):
def __init__(self, gamma=0.95):
    super().__init__()       
    self.gamma = gamma
        
    self.one = 0
    self.two = 0
        
    self.invalid_action = 0
        
    self._action_spec = array_spec.BoundedArraySpec(shape=(), dtype=np.int_, minimum=0, maximum=1)
    self._observation_spec = {
        'observation': array_spec.ArraySpec(shape=(2,), dtype=np.float32),
        'legal_moves': array_spec.ArraySpec(shape=(), dtype=np.bool_),
}
        
def observation_spec(self):
    return self._observation_spec
    
def action_spec(self):
    return self._action_spec

def _reset(self):
    self.state = self._make_observation()

    obs = self.state
    legal_moves = self.num_moves()
        
        
    observations_and_legal_moves = {
        'observation': obs,
        'legal_moves':np.logical_not(legal_moves),
    }
    return ts.restart(observations_and_legal_moves)

def _step(self, action):
    if self._current_time_step.is_last():
        return self.reset()
        
    # Apply the action to the state.
    self.apply_move(action)

    obs = self._make_observation()
    legal_moves = self.num_moves()
        
    done = self.state.is_terminal()
    reward = self.invalid_action
    self.invalid_action = 0

    observations_and_legal_moves = {
        'observation': obs,
        'legal_moves': np.logical_not(legal_moves),
    }

    if done:
        return ts.termination(observations_and_legal_moves, reward)
    else:
        return ts.transition(observations_and_legal_moves, reward, self.gamma)
    
def apply_move(self, action):
    if action == 0:
        pass 
    elif action == 1:
        if self.one == 1:
            self.invalid_action = -1
        else:
            self.one = 1
            self.invalid_action = 1
        elif action == 2:
            if self.two == 1:
                self.invalid_action = -1
            else:
                self.two = 1
                self.invalid_action = 1
        elif action == 3:
            if self.one == 1:
                self.one = 0
                self.invalid_action = 1
            else:
                self.invalid_action = -1
        elif action == 4:
            if self.two == 1:
                self.two = 0
                self.invalid_action = 1
            else:
                self.invalid_action = -1

def num_moves(self):
    action_mask = np.ones(5, dtype=np.uint8)
    # IF one and two are present in the observation
    # make available three and four
    if self.one == 1 and self.two == 1:
        action_mask[0] = 0 # wait
        action_mask[3] = 0 # make three available
        action_mask[4] = 0 # make four available
        
        # IF one is present in the observation make three available
    elif self.one == 1:
        action_mask[0] = 0 # wait
        action_mask[2] = 0 # two is available
        action_mask[3] = 0 # three is available
        
    # IF two is present in the observation make four available
    elif self.one == 1:
        action_mask[0] = 0 # wait
        action_mask[1] = 0 # one is available
        action_mask[4] = 0 # make four available
    
    # IF isnt one or two present in the observation
    else:
        action_mask[0] = 0 # wait
        action_mask[1] = 0 # one is available
        action_mask[2] = 0 # two is available
    
    return action_mask

def _make_observation(self):
    one  = self.one
    two  = self.two
    obs = np.array([one, two], dtype=np.float32)
    
    return obs
    
from tf_agents.agents.reinforce import reinforce_agent
from tf_agents.agents.dqn import dqn_agent
from tf_agents.agents.categorical_dqn import categorical_dqn_agent
from tf_agents.networks import categorical_q_network
from tf_agents.networks import q_network
from tf_agents.replay_buffers import tf_uniform_replay_buffer
from tf_agents.utils import common
import tensorflow as tf
from tf_agents.environments import tf_py_environment
import reverb
from tf_agents.networks.mask_splitter_network import MaskSplitterNetwork
from tf_agents.networks.actor_distribution_network import ActorDistributionNetwork 
from tf_agents.specs import tensor_spec
from tf_agents.replay_buffers import reverb_replay_buffer
from tf_agents.replay_buffers import reverb_utils

optimizer = tf.keras.optimizers.Adam(learning_rate=0.00025)
train_step_counter = tf.Variable(0)
fc_layer_params = (256, 256)

def filter_fun(observation):
    return observation['observation'], observation['legal_moves']

train_py_env = CustomMaskedEnvironment()
train_env = tf_py_environment.TFPyEnvironment(train_py_env)

eval_py_env = CustomMaskedEnvironment()
eval_env = tf_py_environment.TFPyEnvironment(eval_py_env)

num_eval_episodes = 10

masked_actor_network = MaskSplitterNetwork(
    splitter_fn=filter_fun,
    wrapped_network= ActorDistributionNetwork(
       train_env.observation_spec(['observation'],
   train_env.action_spec(),
   fc_layer_params=fc_layer_params
   ),
   passthrough_mask=True
)

agent = reinforce_agent.ReinforceAgent(
    train_env.time_step_spec(),
    train_env.action_spec(),
    actor_network=masked_actor_network,
    optimizer=optimizer,
    normalize_returns=True,
    train_step_counter=train_step_counter,
)
agent.initialize()

eval_policy = agent.policy
collect_policy = agent.collect_policy

def compute_avg_return(environment, policy, num_episodes=10):
    total_return = 0.0
    for _ in range(num_episodes):
        time_step = environment.reset()
        episode_return = 0.0

    while not time_step.is_last():
        action_step = policy.action(time_step)
        time_step = environment.step(action_step.action)
        episode_return += time_step.reward
    total_return += episode_return

    avg_return = total_return / num_episodes
    return avg_return.numpy()[0]

replay_buffer_capacity = 90000

table_name = 'uniform_table'
replay_buffer_signature = tensor_spec.from_spec(
      agent.collect_data_spec)
replay_buffer_signature = tensor_spec.add_outer_dim(
      replay_buffer_signature)
table = reverb.Table(
    table_name,
    max_size=replay_buffer_capacity,
    sampler=reverb.selectors.Uniform(),
    remover=reverb.selectors.Fifo(),
    rate_limiter=reverb.rate_limiters.MinSize(1),
    signature=replay_buffer_signature)

reverb_server = reverb.Server([table])

replay_buffer = reverb_replay_buffer.ReverbReplayBuffer(
    agent.collect_data_spec,
    table_name=table_name,
    sequence_length=None,
    local_server=reverb_server)

rb_observer = reverb_utils.ReverbAddEpisodeObserver(
    replay_buffer.py_client,
    table_name,
    replay_buffer_capacity
)

def collect_episode(environment, policy, num_episodes):
    driver = py_driver.PyDriver(
    environment,
    py_tf_eager_policy.PyTFEagerPolicy(
        policy, use_tf_function=True),
    [rb_observer],
    max_episodes=num_episodes)
    initial_time_step = environment.reset()
    driver.run(initial_time_step)


# (Optional) Optimize by wrapping some of the code in a graph using TF function.
agent.train = common.function(agent.train)

# Reset the train step
agent.train_step_counter.assign(0)

# Evaluate the agent's policy once before training.
avg_return = compute_avg_return(eval_env, agent.policy, num_eval_episodes)
returns = [avg_return]

for _ in range(num_iterations):

  # Collect a few episodes using collect_policy and save to the replay buffer.
  collect_episode(
      train_py_env, agent.collect_policy, collect_episodes_per_iteration)

  # Use data from the buffer and update the agent's network.
  iterator = iter(replay_buffer.as_dataset(sample_batch_size=1))
  trajectories, _ = next(iterator)
  train_loss = agent.train(experience=trajectories)  

  replay_buffer.clear()

  step = tf_agent.train_step_counter.numpy()

  if step % log_interval == 0:
    print('step = {0}: loss = {1}'.format(step, train_loss.loss))

  if step % eval_interval == 0:
    avg_return = compute_avg_return(eval_env, agent.policy, num_eval_episodes)
    print('step = {0}: Average Return = {1}'.format(step, avg_return))
    returns.append(avg_return)

everything seems to works fine but i get the following error messages:

logits = tf.compat.v2.where( tensorflow.python.framework.errors_impl.InvalidArgumentError: Exception encountered when calling layer "CategoricalProjectionNetwork" (type CategoricalProjectionNetwork).

condition [1,5], then [1,2], and else [] must be broadcastable [Op:SelectV2]

Call arguments received by layer "CategoricalProjectionNetwork" (type CategoricalProjectionNetwork): • inputs=tf.Tensor(shape=(1, 256), dtype=float32) • outer_rank=1 • training=False • mask=tf.Tensor(shape=(1, 5), dtype=bool) [reverb/cc/platform/default/server.cc:84] Shutting down replay server

but acctually without a graphic card maybe reverb is not in my "usable options" just a workaround, i would migrate my code to pytorch, rllib, whatever that gives me a usable solution

$\endgroup$

0

You must log in to answer this question.

Browse other questions tagged .