From 4e24c805eeffda096ae5f2485c9a2b4d8e143456 Mon Sep 17 00:00:00 2001 From: Victor Le Date: Sat, 7 Dec 2019 21:08:40 +0100 Subject: [PATCH] AlphaZero and Ranked reward implementation (#6385) --- ci/jenkins_tests/run_rllib_tests.sh | 3 + doc/source/rllib-algorithms.rst | 14 ++ doc/source/rllib-toc.rst | 2 + rllib/contrib/alpha_zero/README.md | 24 +++ .../alpha_zero/core/alpha_zero_policy.py | 126 +++++++++++++ .../alpha_zero/core/alpha_zero_trainer.py | 175 ++++++++++++++++++ rllib/contrib/alpha_zero/core/mcts.py | 152 +++++++++++++++ .../contrib/alpha_zero/core/ranked_rewards.py | 79 ++++++++ .../contrib/alpha_zero/doc/cartpole_plot.png | Bin 0 -> 78446 bytes .../alpha_zero/environments/cartpole.py | 40 ++++ .../alpha_zero/examples/train_cartpole.py | 51 +++++ .../alpha_zero/models/custom_torch_models.py | 108 +++++++++++ .../sync_batches_replay_optimizer.py | 34 ++++ rllib/contrib/registry.py | 7 + 14 files changed, 815 insertions(+) create mode 100644 rllib/contrib/alpha_zero/README.md create mode 100644 rllib/contrib/alpha_zero/core/alpha_zero_policy.py create mode 100644 rllib/contrib/alpha_zero/core/alpha_zero_trainer.py create mode 100644 rllib/contrib/alpha_zero/core/mcts.py create mode 100644 rllib/contrib/alpha_zero/core/ranked_rewards.py create mode 100644 rllib/contrib/alpha_zero/doc/cartpole_plot.png create mode 100644 rllib/contrib/alpha_zero/environments/cartpole.py create mode 100644 rllib/contrib/alpha_zero/examples/train_cartpole.py create mode 100644 rllib/contrib/alpha_zero/models/custom_torch_models.py create mode 100644 rllib/contrib/alpha_zero/optimizer/sync_batches_replay_optimizer.py diff --git a/ci/jenkins_tests/run_rllib_tests.sh b/ci/jenkins_tests/run_rllib_tests.sh index 07f34b11e..05db3d3ab 100755 --- a/ci/jenkins_tests/run_rllib_tests.sh +++ b/ci/jenkins_tests/run_rllib_tests.sh @@ -426,6 +426,9 @@ docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ /ray/ci/suppress_output --force-direct python /ray/rllib/contrib/random_agent/random_agent.py +docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ + /ray/ci/suppress_output --force-direct python /ray/rllib/contrib/alpha_zero/examples/train_cartpole.py --training-iteration=1 + docker run --rm --shm-size=${SHM_SIZE} --memory=${MEMORY_SIZE} $DOCKER_SHA \ /ray/ci/suppress_output --force-direct python /ray/rllib/examples/centralized_critic.py --stop=2000 diff --git a/doc/source/rllib-algorithms.rst b/doc/source/rllib-algorithms.rst index 90826674e..8ce7e66ff 100644 --- a/doc/source/rllib-algorithms.rst +++ b/doc/source/rllib-algorithms.rst @@ -364,3 +364,17 @@ Tuned examples: `CartPole-v0 `__ `[implementation] `__ AlphaZero is an RL agent originally designed for two-player games. This version adapts it to handle single player games. The code can be used with the SyncSamplesOptimizer as well as with a modified version of the SyncReplayOptimizer, and it scales to any number of workers. It also implements the ranked rewards `(R2) `__ strategy to enable self-play even in the one-player setting. The code is mainly purposed to be used for combinatorial optimization. + +Tuned examples: `CartPole-v0 `__ + +**AlphaZero-specific configs** (see also `common configs `__): + +.. literalinclude:: ../../rllib/contrib/alpha_zero/core/alpha_zero_trainer.py + :language: python + :start-after: __sphinx_doc_begin__ + :end-before: __sphinx_doc_end__ diff --git a/doc/source/rllib-toc.rst b/doc/source/rllib-toc.rst index fca6a7623..86f74dd3c 100644 --- a/doc/source/rllib-toc.rst +++ b/doc/source/rllib-toc.rst @@ -78,6 +78,8 @@ Algorithms - `Asynchronous Proximal Policy Optimization (APPO) `__ + - `Single-Player AlphaZero (contrib/AlphaZero) `__ + * Gradient-based - `Advantage Actor-Critic (A2C, A3C) `__ diff --git a/rllib/contrib/alpha_zero/README.md b/rllib/contrib/alpha_zero/README.md new file mode 100644 index 000000000..2b9807e29 --- /dev/null +++ b/rllib/contrib/alpha_zero/README.md @@ -0,0 +1,24 @@ +# AlphaZero implementation for Ray/RLlib +## Notes + +This code implements a one-player AlphaZero agent. It includes the "ranked rewards" (R2) strategy which simulates the self-play in the two-player AlphaZero in forcing the agent to be better than its previous self. R2 is also very helpful to normalize dynamically the rewards. + +The code is Pytorch based. It assumes that the environment is a gym environment, has a discrete action space and returns an observation as a dictionary with two keys: + + - `obs` that contains an observation under either the form of a state vector or an image + - `action_mask` that contains a mask over the legal actions + + It should also implement a `get_state`and a `set_state` function. + + The model used in AlphaZero trainer should extend `ActorCriticModel` and implement the method `compute_priors_and_value`. + +## Example on Cartpole + +Note that both mean and max rewards are obtained with the MCTS in exploration mode: dirichlet noise is added to priors and actions are sampled from the tree policy vectors. We will add later the display of the MCTS in exploitation mode: no dirichlet noise and actions are chosen as tree policy vectors argmax. +![cartpole_plot](doc/cartpole_plot.png) + +## References + +- AlphaZero: https://arxiv.org/abs/1712.01815 +- Ranked rewards: https://arxiv.org/abs/1807.01672 + \ No newline at end of file diff --git a/rllib/contrib/alpha_zero/core/alpha_zero_policy.py b/rllib/contrib/alpha_zero/core/alpha_zero_policy.py new file mode 100644 index 000000000..2b1a29059 --- /dev/null +++ b/rllib/contrib/alpha_zero/core/alpha_zero_policy.py @@ -0,0 +1,126 @@ +import numpy as np +import torch +from ray.rllib.policy.policy import Policy, LEARNER_STATS_KEY +from ray.rllib.policy.torch_policy import TorchPolicy +from ray.rllib.utils.annotations import override + +from ray.rllib.contrib.alpha_zero.core.mcts import Node, RootParentNode + + +class AlphaZeroPolicy(TorchPolicy): + def __init__(self, observation_space, action_space, model, loss, + action_distribution_class, mcts_creator, env_creator, + **kwargs): + super().__init__(observation_space, action_space, model, loss, + action_distribution_class) + # we maintain an env copy in the policy that is used during mcts + # simulations + self.env_creator = env_creator + self.mcts = mcts_creator() + self.env = self.env_creator() + self.env.reset() + self.obs_space = observation_space + + @override(TorchPolicy) + def compute_actions(self, + obs_batch, + state_batches=None, + prev_action_batch=None, + prev_reward_batch=None, + info_batch=None, + episodes=None, + **kwargs): + + with torch.no_grad(): + input_dict = {"obs": obs_batch} + if prev_action_batch: + input_dict["prev_actions"] = prev_action_batch + if prev_reward_batch: + input_dict["prev_rewards"] = prev_reward_batch + + actions = [] + + for i, episode in enumerate(episodes): + if episode.length == 0: + # if first time step of episode, get initial env state + env_state = episode.user_data["initial_state"] + # verify if env has been wrapped for ranked rewards + if self.env.__class__.__name__ == \ + "RankedRewardsEnvWrapper": + # r2 env state contains also the rewards buffer state + env_state = { + "env_state": env_state, + "buffer_state": None + } + # create tree root node + obs = self.env.set_state(env_state) + tree_node = Node( + state=env_state, + obs=obs, + reward=0, + done=False, + action=None, + parent=RootParentNode(env=self.env), + mcts=self.mcts) + else: + # otherwise get last root node from previous time step + tree_node = episode.user_data["tree_node"] + + # run monte carlo simulations to compute the actions + # and record the tree + mcts_policy, action, tree_node = self.mcts.compute_action( + tree_node) + # record action + actions.append(action) + # store new node + episode.user_data["tree_node"] = tree_node + + # store mcts policies vectors and current tree root node + if episode.length == 0: + episode.user_data["mcts_policies"] = [mcts_policy] + else: + episode.user_data["mcts_policies"].append(mcts_policy) + + return np.array(actions), [], self.extra_action_out( + input_dict, state_batches, self.model) + + @override(Policy) + def postprocess_trajectory(self, + sample_batch, + other_agent_batches=None, + episode=None): + # add mcts policies to sample batch + sample_batch["mcts_policies"] = np.array( + episode.user_data["mcts_policies"])[sample_batch["t"]] + # final episode reward corresponds to the value (if not discounted) + # for all transitions in episode + final_reward = sample_batch["rewards"][-1] + # if r2 is enabled, then add the reward to the buffer and normalize it + if self.env.__class__.__name__ == "RankedRewardsEnvWrapper": + self.env.r2_buffer.add_reward(final_reward) + final_reward = self.env.r2_buffer.normalize(final_reward) + sample_batch["value_label"] = final_reward * np.ones_like( + sample_batch["t"]) + return sample_batch + + @override(Policy) + def learn_on_batch(self, postprocessed_batch): + train_batch = self._lazy_tensor_dict(postprocessed_batch) + + loss_out, policy_loss, value_loss = self._loss( + self, self.model, self.dist_class, train_batch) + self._optimizer.zero_grad() + loss_out.backward() + + grad_process_info = self.extra_grad_process() + self._optimizer.step() + + grad_info = self.extra_grad_info(train_batch) + grad_info.update(grad_process_info) + grad_info.update({ + "total_loss": loss_out.detach().numpy(), + "policy_loss": policy_loss.detach().numpy(), + "value_loss": value_loss.detach().numpy() + }) + + return {LEARNER_STATS_KEY: grad_info} diff --git a/rllib/contrib/alpha_zero/core/alpha_zero_trainer.py b/rllib/contrib/alpha_zero/core/alpha_zero_trainer.py new file mode 100644 index 000000000..0b3d9a9e9 --- /dev/null +++ b/rllib/contrib/alpha_zero/core/alpha_zero_trainer.py @@ -0,0 +1,175 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging + +import torch +import torch.nn as nn +from ray.rllib.agents import with_common_config +from ray.rllib.agents.trainer_template import build_trainer +from ray.rllib.models.catalog import ModelCatalog +from ray.rllib.models.model import restore_original_dimensions +from ray.rllib.models.torch.torch_action_dist import TorchCategorical +from ray.rllib.optimizers import SyncSamplesOptimizer +from ray.rllib.utils import try_import_tf +from ray.tune.registry import ENV_CREATOR, _global_registry + +from ray.rllib.contrib.alpha_zero.core.alpha_zero_policy import AlphaZeroPolicy +from ray.rllib.contrib.alpha_zero.core.mcts import MCTS +from ray.rllib.contrib.alpha_zero.core.ranked_rewards import get_r2_env_wrapper +from ray.rllib.contrib.alpha_zero.optimizer.sync_batches_replay_optimizer \ + import SyncBatchesReplayOptimizer + +tf = try_import_tf() +logger = logging.getLogger(__name__) + + +def on_episode_start(info): + # save env state when an episode starts + env = info["env"].get_unwrapped()[0] + state = env.get_state() + episode = info["episode"] + episode.user_data["initial_state"] = state + + +# yapf: disable +# __sphinx_doc_begin__ +DEFAULT_CONFIG = with_common_config({ + # Size of batches collected from each worker + "sample_batch_size": 200, + # Number of timesteps collected for each SGD round + "train_batch_size": 4000, + # Total SGD batch size across all devices for SGD + "sgd_minibatch_size": 128, + # Whether to shuffle sequences in the batch when training (recommended) + "shuffle_sequences": True, + # Number of SGD iterations in each outer loop + "num_sgd_iter": 30, + # IN case a buffer optimizer is used + "learning_starts": 1000, + "buffer_size": 10000, + # Stepsize of SGD + "lr": 5e-5, + # Learning rate schedule + "lr_schedule": None, + # Share layers for value function. If you set this to True, it"s important + # to tune vf_loss_coeff. + "vf_share_layers": False, + # Whether to rollout "complete_episodes" or "truncate_episodes" + "batch_mode": "complete_episodes", + # Which observation filter to apply to the observation + "observation_filter": "NoFilter", + # Uses the sync samples optimizer instead of the multi-gpu one. This does + # not support minibatches. + "simple_optimizer": True, + + # === MCTS === + "mcts_config": { + "puct_coefficient": 1.0, + "num_simulations": 30, + "temperature": 1.5, + "dirichlet_epsilon": 0.25, + "dirichlet_noise": 0.03, + "argmax_tree_policy": False, + "add_dirichlet_noise": True, + }, + + # === Ranked Rewards === + # implement the ranked reward (r2) algorithm + # from: https://arxiv.org/pdf/1807.01672.pdf + "ranked_rewards": { + "enable": True, + "percentile": 75, + "buffer_max_length": 1000, + # add rewards obtained from random policy to + # "warm start" the buffer + "initialize_buffer": True, + "num_init_rewards": 100, + }, + + # === Evaluation === + # Extra configuration that disables exploration. + "evaluation_config": { + "mcts_config": { + "argmax_tree_policy": True, + "add_dirichlet_noise": False, + }, + }, + + # === Callbacks === + "callbacks": { + "on_episode_start": on_episode_start, + } +}) + + +# __sphinx_doc_end__ +# yapf: enable + + +def choose_policy_optimizer(workers, config): + if config["simple_optimizer"]: + return SyncSamplesOptimizer( + workers, + num_sgd_iter=config["num_sgd_iter"], + train_batch_size=config["train_batch_size"]) + else: + return SyncBatchesReplayOptimizer( + workers, + num_gradient_descents=config["num_sgd_iter"], + learning_starts=config["learning_starts"], + train_batch_size=config["train_batch_size"], + buffer_size=config["buffer_size"]) + + +def alpha_zero_loss(policy, model, dist_class, train_batch): + # get inputs unflattened inputs + input_dict = restore_original_dimensions(train_batch["obs"], + policy.observation_space, "torch") + # forward pass in model + model_out = model.forward(input_dict, None, [1]) + logits, _ = model_out + values = model.value_function() + logits, values = torch.squeeze(logits), torch.squeeze(values) + priors = nn.Softmax(dim=-1)(logits) + # compute actor and critic losses + policy_loss = torch.mean( + -torch.sum(train_batch["mcts_policies"] * torch.log(priors), dim=-1)) + value_loss = torch.mean(torch.pow(values - train_batch["value_label"], 2)) + # compute total loss + total_loss = (policy_loss + value_loss) / 2 + return total_loss, policy_loss, value_loss + + +class AlphaZeroPolicyWrapperClass(AlphaZeroPolicy): + def __init__(self, obs_space, action_space, config): + model = ModelCatalog.get_model_v2( + obs_space, action_space, action_space.n, config["model"], "torch") + env_creator = _global_registry.get(ENV_CREATOR, config["env"]) + if config["ranked_rewards"]["enable"]: + # if r2 is enabled, tne env is wrapped to include a rewards buffer + # used to normalize rewards + env_cls = get_r2_env_wrapper(env_creator, config["ranked_rewards"]) + + # the wrapped env is used only in the mcts, not in the + # rollout workers + def _env_creator(): + return env_cls(config["env_config"]) + else: + + def _env_creator(): + return env_creator(config["env_config"]) + + def mcts_creator(): + return MCTS(model, config["mcts_config"]) + + super().__init__(obs_space, action_space, model, alpha_zero_loss, + TorchCategorical, mcts_creator, _env_creator) + + +AlphaZeroTrainer = build_trainer( + name="AlphaZero", + default_config=DEFAULT_CONFIG, + default_policy=AlphaZeroPolicyWrapperClass, + make_policy_optimizer=choose_policy_optimizer) diff --git a/rllib/contrib/alpha_zero/core/mcts.py b/rllib/contrib/alpha_zero/core/mcts.py new file mode 100644 index 000000000..c31538236 --- /dev/null +++ b/rllib/contrib/alpha_zero/core/mcts.py @@ -0,0 +1,152 @@ +""" +Mcts implementation modified from +https://github.com/brilee/python_uct/blob/master/numpy_impl.py +""" +import collections +import math + +import numpy as np + + +class Node: + def __init__(self, action, obs, done, reward, state, mcts, parent=None): + self.env = parent.env + self.action = action # Action used to go to this state + + self.is_expanded = False + self.parent = parent + self.children = {} + + self.action_space_size = self.env.action_space.n + self.child_total_value = np.zeros( + [self.action_space_size], dtype=np.float32) # Q + self.child_priors = np.zeros( + [self.action_space_size], dtype=np.float32) # P + self.child_number_visits = np.zeros( + [self.action_space_size], dtype=np.float32) # N + self.valid_actions = obs["action_mask"].astype(np.bool) + + self.reward = reward + self.done = done + self.state = state + self.obs = obs + + self.mcts = mcts + + @property + def number_visits(self): + return self.parent.child_number_visits[self.action] + + @number_visits.setter + def number_visits(self, value): + self.parent.child_number_visits[self.action] = value + + @property + def total_value(self): + return self.parent.child_total_value[self.action] + + @total_value.setter + def total_value(self, value): + self.parent.child_total_value[self.action] = value + + def child_Q(self): + # TODO (weak todo) add "softmax" version of the Q-value + return self.child_total_value / (1 + self.child_number_visits) + + def child_U(self): + return math.sqrt(self.number_visits) * self.child_priors / ( + 1 + self.child_number_visits) + + def best_action(self): + """ + :return: action + """ + child_score = self.child_Q() + self.mcts.c_puct * self.child_U() + masked_child_score = child_score + masked_child_score[~self.valid_actions] = -np.inf + return np.argmax(masked_child_score) + + def select(self): + current_node = self + while current_node.is_expanded: + best_action = current_node.best_action() + current_node = current_node.get_child(best_action) + return current_node + + def expand(self, child_priors): + self.is_expanded = True + self.child_priors = child_priors + + def get_child(self, action): + if action not in self.children: + self.env.set_state(self.state) + obs, reward, done, _ = self.env.step(action) + next_state = self.env.get_state() + self.children[action] = Node( + state=next_state, + action=action, + parent=self, + reward=reward, + done=done, + obs=obs, + mcts=self.mcts) + return self.children[action] + + def backup(self, value): + current = self + while current.parent is not None: + current.number_visits += 1 + current.total_value += value + current = current.parent + + +class RootParentNode(object): + def __init__(self, env): + self.parent = None + self.child_total_value = collections.defaultdict(float) + self.child_number_visits = collections.defaultdict(float) + self.env = env + + +class MCTS: + def __init__(self, model, mcts_param): + self.model = model + self.temperature = mcts_param["temperature"] + self.dir_epsilon = mcts_param["dirichlet_epsilon"] + self.dir_noise = mcts_param["dirichlet_noise"] + self.num_sims = mcts_param["num_simulations"] + self.exploit = mcts_param["argmax_tree_policy"] + self.add_dirichlet_noise = mcts_param["add_dirichlet_noise"] + self.c_puct = mcts_param["puct_coefficient"] + + def compute_action(self, node): + for _ in range(self.num_sims): + leaf = node.select() + if leaf.done: + value = leaf.reward + else: + child_priors, value = self.model.compute_priors_and_value( + leaf.obs) + if self.add_dirichlet_noise: + child_priors = (1 - self.dir_epsilon) * child_priors + child_priors += self.dir_epsilon * np.random.dirichlet( + [self.dir_noise] * child_priors.size) + + leaf.expand(child_priors) + leaf.backup(value) + + # Tree policy target (TPT) + tree_policy = node.child_number_visits / node.number_visits + tree_policy = tree_policy / np.max( + tree_policy) # to avoid overflows when computing softmax + tree_policy = np.power(tree_policy, self.temperature) + tree_policy = tree_policy / np.sum(tree_policy) + if self.exploit: + # if exploit then choose action that has the maximum + # tree policy probability + action = np.argmax(tree_policy) + else: + # otherwise sample an action according to tree policy probabilities + action = np.random.choice( + np.arange(node.action_space_size), p=tree_policy) + return tree_policy, action, node.children[action] diff --git a/rllib/contrib/alpha_zero/core/ranked_rewards.py b/rllib/contrib/alpha_zero/core/ranked_rewards.py new file mode 100644 index 000000000..67ff3853a --- /dev/null +++ b/rllib/contrib/alpha_zero/core/ranked_rewards.py @@ -0,0 +1,79 @@ +from copy import deepcopy + +import numpy as np + + +class RankedRewardsBuffer: + def __init__(self, buffer_max_length, percentile): + self.buffer_max_length = buffer_max_length + self.percentile = percentile + self.buffer = [] + + def add_reward(self, reward): + if len(self.buffer) < self.buffer_max_length: + self.buffer.append(reward) + else: + self.buffer = self.buffer[1:] + [reward] + + def normalize(self, reward): + reward_threshold = np.percentile(self.buffer, self.percentile) + if reward < reward_threshold: + return -1.0 + else: + return 1.0 + + def get_state(self): + return np.array(self.buffer) + + def set_state(self, state): + if state is not None: + self.buffer = list(state) + + +def get_r2_env_wrapper(env_creator, r2_config): + class RankedRewardsEnvWrapper: + def __init__(self, env_config): + self.env = env_creator(env_config) + self.action_space = self.env.action_space + self.observation_space = self.env.observation_space + max_buffer_length = r2_config["buffer_max_length"] + percentile = r2_config["percentile"] + self.r2_buffer = RankedRewardsBuffer(max_buffer_length, percentile) + if r2_config["initialize_buffer"]: + self._initialize_buffer(r2_config["num_init_rewards"]) + + def _initialize_buffer(self, num_init_rewards=100): + # initialize buffer with random policy + for _ in range(num_init_rewards): + obs = self.env.reset() + done = False + while not done: + mask = obs["action_mask"] + probs = mask / mask.sum() + action = np.random.choice( + np.arange(mask.shape[0]), p=probs) + obs, reward, done, _ = self.env.step(action) + self.r2_buffer.add_reward(reward) + + def step(self, action): + obs, reward, done, info = self.env.step(action) + if done: + reward = self.r2_buffer.normalize(reward) + return obs, reward, done, info + + def get_state(self): + state = { + "env_state": self.env.get_state(), + "buffer_state": self.r2_buffer.get_state() + } + return deepcopy(state) + + def reset(self): + return self.env.reset() + + def set_state(self, state): + obs = self.env.set_state(state["env_state"]) + self.r2_buffer.set_state(state["buffer_state"]) + return obs + + return RankedRewardsEnvWrapper diff --git a/rllib/contrib/alpha_zero/doc/cartpole_plot.png b/rllib/contrib/alpha_zero/doc/cartpole_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..75cbf2146cf73ecc3432d47a04a5f84beb4b9c55 GIT binary patch literal 78446 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ℑ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 literal 0 HcmV?d00001 diff --git a/rllib/contrib/alpha_zero/environments/cartpole.py b/rllib/contrib/alpha_zero/environments/cartpole.py new file mode 100644 index 000000000..961d33689 --- /dev/null +++ b/rllib/contrib/alpha_zero/environments/cartpole.py @@ -0,0 +1,40 @@ +from copy import deepcopy + +import gym +import numpy as np +from gym.spaces import Discrete, Dict, Box + + +class CartPole: + """ + Wrapper for gym CartPole environment where the reward + is accumulated to the end + """ + + def __init__(self, config=None): + self.env = gym.make("CartPole-v0") + self.action_space = Discrete(2) + self.observation_space = Dict({ + "obs": self.env.observation_space, + "action_mask": Box(low=0, high=1, shape=(self.action_space.n, )) + }) + self.running_reward = 0 + + def reset(self): + self.running_reward = 0 + return {"obs": self.env.reset(), "action_mask": np.array([1, 1])} + + def step(self, action): + obs, rew, done, info = self.env.step(action) + self.running_reward += rew + score = self.running_reward if done else 0 + return {"obs": obs, "action_mask": np.array([1, 1])}, score, done, info + + def set_state(self, state): + self.running_reward = state[1] + self.env = deepcopy(state[0]) + obs = np.array(list(self.env.unwrapped.state)) + return {"obs": obs, "action_mask": np.array([1, 1])} + + def get_state(self): + return deepcopy(self.env), self.running_reward diff --git a/rllib/contrib/alpha_zero/examples/train_cartpole.py b/rllib/contrib/alpha_zero/examples/train_cartpole.py new file mode 100644 index 000000000..8b9cbeecd --- /dev/null +++ b/rllib/contrib/alpha_zero/examples/train_cartpole.py @@ -0,0 +1,51 @@ +"""Example of using training on CartPole.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse + +from ray import tune + +from ray.rllib.contrib.alpha_zero.models.custom_torch_models import DenseModel +from ray.rllib.contrib.alpha_zero.environments.cartpole import CartPole +from ray.rllib.models.catalog import ModelCatalog + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--num-workers", default=6, type=int) + parser.add_argument("--training-iteration", default=10000, type=int) + args = parser.parse_args() + + ModelCatalog.register_custom_model("dense_model", DenseModel) + + tune.run( + "contrib/AlphaZero", + stop={"training_iteration": args.training_iteration}, + max_failures=0, + config={ + "env": CartPole, + "num_workers": args.num_workers, + "sample_batch_size": 50, + "train_batch_size": 500, + "sgd_minibatch_size": 64, + "lr": 1e-4, + "num_sgd_iter": 1, + "mcts_config": { + "puct_coefficient": 1.5, + "num_simulations": 100, + "temperature": 1.0, + "dirichlet_epsilon": 0.20, + "dirichlet_noise": 0.03, + "argmax_tree_policy": False, + "add_dirichlet_noise": True, + }, + "ranked_rewards": { + "enable": True, + }, + "model": { + "custom_model": "dense_model", + }, + }, + ) diff --git a/rllib/contrib/alpha_zero/models/custom_torch_models.py b/rllib/contrib/alpha_zero/models/custom_torch_models.py new file mode 100644 index 000000000..bf0291ee4 --- /dev/null +++ b/rllib/contrib/alpha_zero/models/custom_torch_models.py @@ -0,0 +1,108 @@ +from abc import ABC + +import numpy as np +import torch +import torch.nn as nn +from ray.rllib.models.model import restore_original_dimensions +from ray.rllib.models.preprocessors import get_preprocessor +from ray.rllib.models.torch.torch_modelv2 import TorchModelV2 + + +def convert_to_tensor(arr): + tensor = torch.from_numpy(np.asarray(arr)) + if tensor.dtype == torch.double: + tensor = tensor.float() + return tensor + + +class ActorCriticModel(TorchModelV2, nn.Module, ABC): + def __init__(self, obs_space, action_space, num_outputs, model_config, + name): + TorchModelV2.__init__(self, obs_space, action_space, num_outputs, + model_config, name) + nn.Module.__init__(self) + + self.preprocessor = get_preprocessor(obs_space.original_space)( + obs_space.original_space) + + self.shared_layers = None + self.actor_layers = None + self.critic_layers = None + + self._value_out = None + + def forward(self, input_dict, state, seq_lens): + x = input_dict["obs"] + x = self.shared_layers(x) + # actor outputs + logits = self.actor_layers(x) + + # compute value + self._value_out = self.critic_layers(x) + return logits, None + + def value_function(self): + return self._value_out + + def compute_priors_and_value(self, obs): + obs = convert_to_tensor([self.preprocessor.transform(obs)]) + input_dict = restore_original_dimensions(obs, self.obs_space, "torch") + + with torch.no_grad(): + model_out = self.forward(input_dict, None, [1]) + logits, _ = model_out + value = self.value_function() + logits, value = torch.squeeze(logits), torch.squeeze(value) + priors = nn.Softmax(dim=-1)(logits) + + priors = priors.cpu().numpy() + value = value.cpu().numpy() + + return priors, value + + +class Flatten(nn.Module): + def forward(self, input): + return input.view(input.size(0), -1) + + +class ConvNetModel(ActorCriticModel): + def __init__(self, obs_space, action_space, num_outputs, model_config, + name): + ActorCriticModel.__init__(self, obs_space, action_space, num_outputs, + model_config, name) + + in_channels = model_config["custom_options"]["in_channels"] + feature_dim = model_config["custom_options"]["feature_dim"] + + self.shared_layers = nn.Sequential( + nn.Conv2d(in_channels, 32, kernel_size=4, stride=2), + nn.Conv2d(32, 64, kernel_size=2, stride=1), + nn.Conv2d(64, 64, kernel_size=2, stride=1), Flatten(), + nn.Linear(1024, feature_dim)) + + self.actor_layers = nn.Sequential( + nn.Linear(in_features=feature_dim, out_features=action_space.n)) + + self.critic_layers = nn.Sequential( + nn.Linear(in_features=feature_dim, out_features=1)) + + self._value_out = None + + +class DenseModel(ActorCriticModel): + def __init__(self, obs_space, action_space, num_outputs, model_config, + name): + ActorCriticModel.__init__(self, obs_space, action_space, num_outputs, + model_config, name) + + self.shared_layers = nn.Sequential( + nn.Linear( + in_features=obs_space.original_space["obs"].shape[0], + out_features=256), nn.Linear( + in_features=256, out_features=256)) + self.actor_layers = nn.Sequential( + nn.Linear(in_features=256, out_features=action_space.n)) + self.critic_layers = nn.Sequential( + nn.Linear(in_features=256, out_features=1)) + self._value_out = None diff --git a/rllib/contrib/alpha_zero/optimizer/sync_batches_replay_optimizer.py b/rllib/contrib/alpha_zero/optimizer/sync_batches_replay_optimizer.py new file mode 100644 index 000000000..839808c5b --- /dev/null +++ b/rllib/contrib/alpha_zero/optimizer/sync_batches_replay_optimizer.py @@ -0,0 +1,34 @@ +import random + +from ray.rllib.evaluation.metrics import get_learner_stats +from ray.rllib.optimizers.sync_batch_replay_optimizer import \ + SyncBatchReplayOptimizer +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.utils.annotations import override + + +class SyncBatchesReplayOptimizer(SyncBatchReplayOptimizer): + def __init__(self, + workers, + learning_starts=1000, + buffer_size=10000, + train_batch_size=32, + num_gradient_descents=10): + super(SyncBatchesReplayOptimizer, self).__init__( + workers, learning_starts, buffer_size, train_batch_size) + self.num_sgds = num_gradient_descents + + @override(SyncBatchReplayOptimizer) + def _optimize(self): + for _ in range(self.num_sgds): + samples = [random.choice(self.replay_buffer)] + while sum(s.count for s in samples) < self.train_batch_size: + samples.append(random.choice(self.replay_buffer)) + samples = SampleBatch.concat_samples(samples) + with self.grad_timer: + info_dict = self.workers.local_worker().learn_on_batch(samples) + for policy_id, info in info_dict.items(): + self.learner_stats[policy_id] = get_learner_stats(info) + self.grad_timer.push_units_processed(samples.count) + self.num_steps_trained += samples.count + return info_dict diff --git a/rllib/contrib/registry.py b/rllib/contrib/registry.py index 340cd701c..05c456d23 100644 --- a/rllib/contrib/registry.py +++ b/rllib/contrib/registry.py @@ -15,7 +15,14 @@ def _import_maddpg(): return maddpg.MADDPGTrainer +def _import_alphazero(): + from ray.rllib.contrib.alpha_zero.core.alpha_zero_trainer import\ + AlphaZeroTrainer + return AlphaZeroTrainer + + CONTRIBUTED_ALGORITHMS = { "contrib/RandomAgent": _import_random_agent, "contrib/MADDPG": _import_maddpg, + "contrib/AlphaZero": _import_alphazero, }