chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from ray.rllib.examples.rl_modules.classes.rock_paper_scissors_heuristic_rlm import (
|
||||
AlwaysSameHeuristicRLM,
|
||||
BeatLastHeuristicRLM,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AlwaysSameHeuristicRLM",
|
||||
"BeatLastHeuristicRLM",
|
||||
]
|
||||
@@ -0,0 +1,211 @@
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
|
||||
import gymnasium as gym
|
||||
|
||||
from ray.rllib.algorithms.ppo.torch.ppo_torch_rl_module import PPOTorchRLModule
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI
|
||||
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
|
||||
from ray.rllib.core.rl_module.rl_module import RLModule
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.torch_utils import FLOAT_MIN
|
||||
from ray.rllib.utils.typing import TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class ActionMaskingRLModule(RLModule):
|
||||
"""An RLModule that implements an action masking for safe RL.
|
||||
|
||||
This RLModule implements action masking to avoid unsafe/unwanted actions
|
||||
dependent on the current state (observations). It does so by using an
|
||||
environment generated action mask defining which actions are allowed and
|
||||
which should be avoided. The action mask is extracted from the
|
||||
environment's `gymnasium.spaces.dict.Dict` observation and applied after
|
||||
the module's `forward`-pass to the action logits. The resulting action
|
||||
logits prevent unsafe/unwanted actions to be sampled from the corresponding
|
||||
action distribution.
|
||||
|
||||
Note, this RLModule is implemented for the `PPO` algorithm only. It is not
|
||||
guaranteed to work with other algorithms. Furthermore, not that for this
|
||||
module to work it requires an environment with a `gymnasium.spaces.dict.Dict`
|
||||
observation space containing tow key, `"action_mask"` and `"observations"`.
|
||||
"""
|
||||
|
||||
@override(RLModule)
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
observation_space: Optional[gym.Space] = None,
|
||||
action_space: Optional[gym.Space] = None,
|
||||
inference_only: Optional[bool] = None,
|
||||
learner_only: bool = False,
|
||||
model_config: Optional[Union[dict, DefaultModelConfig]] = None,
|
||||
catalog_class=None,
|
||||
**kwargs,
|
||||
):
|
||||
# If observation space is not of type `Dict` raise an error.
|
||||
if not isinstance(observation_space, gym.spaces.dict.Dict):
|
||||
raise ValueError(
|
||||
"This RLModule requires the environment to provide a "
|
||||
"`gym.spaces.Dict` observation space of the form: \n"
|
||||
" {'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
||||
" 'observation_space': self.observation_space}"
|
||||
)
|
||||
|
||||
# While the environment holds an observation space that contains, both,
|
||||
# the action mask and the original observation space, the 'RLModule'
|
||||
# receives only the `"observation"` element of the space, but not the
|
||||
# action mask.
|
||||
self.observation_space_with_mask = observation_space
|
||||
self.observation_space = observation_space["observations"]
|
||||
|
||||
# Keeps track if observation specs have been checked already.
|
||||
self._checked_observations = False
|
||||
|
||||
# The DefaultPPORLModule, in its constructor will build networks for the
|
||||
# original observation space (i.e. without the action mask).
|
||||
super().__init__(
|
||||
observation_space=self.observation_space,
|
||||
action_space=action_space,
|
||||
inference_only=inference_only,
|
||||
learner_only=learner_only,
|
||||
model_config=model_config,
|
||||
catalog_class=catalog_class,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ActionMaskingTorchRLModule(ActionMaskingRLModule, PPOTorchRLModule):
|
||||
@override(PPOTorchRLModule)
|
||||
def setup(self):
|
||||
super().setup()
|
||||
# We need to reset here the observation space such that the
|
||||
# super`s (`PPOTorchRLModule`) observation space is the
|
||||
# original space (i.e. without the action mask) and `self`'s
|
||||
# observation space contains the action mask.
|
||||
self.observation_space = self.observation_space_with_mask
|
||||
|
||||
@override(PPOTorchRLModule)
|
||||
def _forward_inference(
|
||||
self, batch: Dict[str, TensorType], **kwargs
|
||||
) -> Dict[str, TensorType]:
|
||||
# Preprocess the original batch to extract the action mask.
|
||||
action_mask, batch = self._preprocess_batch(batch)
|
||||
# Run the forward pass.
|
||||
outs = super()._forward_inference(batch, **kwargs)
|
||||
# Mask the action logits and return.
|
||||
return self._mask_action_logits(outs, action_mask)
|
||||
|
||||
@override(PPOTorchRLModule)
|
||||
def _forward_exploration(
|
||||
self, batch: Dict[str, TensorType], **kwargs
|
||||
) -> Dict[str, TensorType]:
|
||||
# Preprocess the original batch to extract the action mask.
|
||||
action_mask, batch = self._preprocess_batch(batch)
|
||||
# Run the forward pass.
|
||||
outs = super()._forward_exploration(batch, **kwargs)
|
||||
# Mask the action logits and return.
|
||||
return self._mask_action_logits(outs, action_mask)
|
||||
|
||||
@override(PPOTorchRLModule)
|
||||
def _forward_train(
|
||||
self, batch: Dict[str, TensorType], **kwargs
|
||||
) -> Dict[str, TensorType]:
|
||||
# Run the forward pass.
|
||||
outs = super()._forward_train(batch, **kwargs)
|
||||
# Mask the action logits and return.
|
||||
return self._mask_action_logits(outs, batch["action_mask"])
|
||||
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(self, batch: Dict[str, TensorType], embeddings=None):
|
||||
# Check, if the observations are still in `dict` form.
|
||||
if isinstance(batch[Columns.OBS], dict):
|
||||
# Preprocess the batch to extract the `observations` to `Columns.OBS`.
|
||||
action_mask, batch = self._preprocess_batch(batch)
|
||||
# NOTE: Because we manipulate the batch we need to add the `action_mask`
|
||||
# to the batch to access them in `_forward_train`.
|
||||
batch["action_mask"] = action_mask
|
||||
# Call the super's method to compute values for GAE.
|
||||
return super().compute_values(batch, embeddings)
|
||||
|
||||
def _preprocess_batch(
|
||||
self, batch: Dict[str, TensorType], **kwargs
|
||||
) -> Tuple[TensorType, Dict[str, TensorType]]:
|
||||
"""Extracts observations and action mask from the batch
|
||||
|
||||
Args:
|
||||
batch: A dictionary containing tensors (at least `Columns.OBS`)
|
||||
|
||||
Returns:
|
||||
A tuple with the action mask tensor and the modified batch containing
|
||||
the original observations.
|
||||
"""
|
||||
# Check observation specs for action mask and observation keys.
|
||||
self._check_batch(batch)
|
||||
|
||||
# Extract the available actions tensor from the observation.
|
||||
action_mask = batch[Columns.OBS].pop("action_mask")
|
||||
|
||||
# Modify the batch for the `DefaultPPORLModule`'s `forward` method, i.e.
|
||||
# pass only `"obs"` into the `forward` method.
|
||||
batch[Columns.OBS] = batch[Columns.OBS].pop("observations")
|
||||
|
||||
# Return the extracted action mask and the modified batch.
|
||||
return action_mask, batch
|
||||
|
||||
def _mask_action_logits(
|
||||
self, batch: Dict[str, TensorType], action_mask: TensorType
|
||||
) -> Dict[str, TensorType]:
|
||||
"""Masks the action logits for the output of `forward` methods
|
||||
|
||||
Args:
|
||||
batch: A dictionary containing tensors (at least action logits).
|
||||
action_mask: A tensor containing the action mask for the current
|
||||
observations.
|
||||
|
||||
Returns:
|
||||
A modified batch with masked action logits for the action distribution
|
||||
inputs.
|
||||
"""
|
||||
# Convert action mask into an `[0.0][-inf]`-type mask.
|
||||
inf_mask = torch.clamp(torch.log(action_mask), min=FLOAT_MIN)
|
||||
|
||||
# Mask the logits.
|
||||
batch[Columns.ACTION_DIST_INPUTS] += inf_mask
|
||||
|
||||
# Return the batch with the masked action logits.
|
||||
return batch
|
||||
|
||||
def _check_batch(self, batch: Dict[str, TensorType]) -> Optional[ValueError]:
|
||||
"""Assert that the batch includes action mask and observations.
|
||||
|
||||
Args:
|
||||
batch: A dicitonary containing tensors (at least `Columns.OBS`) to be
|
||||
checked.
|
||||
|
||||
Raises:
|
||||
`ValueError` if the column `Columns.OBS` does not contain observations
|
||||
and action mask.
|
||||
"""
|
||||
if not self._checked_observations:
|
||||
if "action_mask" not in batch[Columns.OBS]:
|
||||
raise ValueError(
|
||||
"No action mask found in observation. This `RLModule` requires "
|
||||
"the environment to provide observations that include an "
|
||||
"action mask (i.e. an observation space of the Dict space "
|
||||
"type that looks as follows: \n"
|
||||
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
||||
"'observations': self.observation_space}"
|
||||
)
|
||||
if "observations" not in batch[Columns.OBS]:
|
||||
raise ValueError(
|
||||
"No observations found in observation. This 'RLModule` requires "
|
||||
"the environment to provide observations that include the original "
|
||||
"observations under a key `'observations'` in a dict (i.e. an "
|
||||
"observation space of the Dict space type that looks as follows: \n"
|
||||
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
||||
"'observations': <observation_space>}"
|
||||
)
|
||||
self._checked_observations = True
|
||||
@@ -0,0 +1,129 @@
|
||||
from typing import Dict
|
||||
|
||||
import gymnasium as gym
|
||||
|
||||
from ray.rllib.core import Columns
|
||||
from ray.rllib.core.distribution.torch.torch_distribution import (
|
||||
TorchCategorical,
|
||||
TorchDiagGaussian,
|
||||
TorchMultiDistribution,
|
||||
)
|
||||
from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI
|
||||
from ray.rllib.core.rl_module.rl_module import RLModule
|
||||
from ray.rllib.core.rl_module.torch.torch_rl_module import TorchRLModule
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.torch_utils import one_hot
|
||||
from ray.rllib.utils.typing import TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class AutoregressiveActionsRLM(TorchRLModule, ValueFunctionAPI):
|
||||
"""An RLModule that uses an autoregressive action distribution.
|
||||
|
||||
Actions are sampled in two steps. The first (prior) action component is sampled from
|
||||
a categorical distribution. Then, the second (posterior) action component is sampled
|
||||
from a posterior distribution that depends on the first action component and the
|
||||
other input data (observations).
|
||||
|
||||
Note, this RLModule works in combination with any algorithm, whose Learners require
|
||||
the `ValueFunctionAPI`.
|
||||
"""
|
||||
|
||||
@override(RLModule)
|
||||
def setup(self):
|
||||
super().setup()
|
||||
|
||||
# Assert the action space is correct.
|
||||
assert isinstance(self.action_space, gym.spaces.Tuple)
|
||||
assert isinstance(self.action_space[0], gym.spaces.Discrete)
|
||||
assert self.action_space[0].n == 3
|
||||
assert isinstance(self.action_space[1], gym.spaces.Box)
|
||||
|
||||
self._prior_net = nn.Sequential(
|
||||
nn.Linear(
|
||||
in_features=self.observation_space.shape[0],
|
||||
out_features=256,
|
||||
),
|
||||
nn.Tanh(),
|
||||
nn.Linear(in_features=256, out_features=self.action_space[0].n),
|
||||
)
|
||||
|
||||
self._posterior_net = nn.Sequential(
|
||||
nn.Linear(
|
||||
in_features=self.observation_space.shape[0] + self.action_space[0].n,
|
||||
out_features=256,
|
||||
),
|
||||
nn.Tanh(),
|
||||
nn.Linear(in_features=256, out_features=self.action_space[1].shape[0] * 2),
|
||||
)
|
||||
|
||||
# Build the value function head.
|
||||
self._value_net = nn.Sequential(
|
||||
nn.Linear(
|
||||
in_features=self.observation_space.shape[0],
|
||||
out_features=256,
|
||||
),
|
||||
nn.Tanh(),
|
||||
nn.Linear(in_features=256, out_features=1),
|
||||
)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_inference(self, batch: Dict[str, TensorType]) -> Dict[str, TensorType]:
|
||||
return self._pi(batch[Columns.OBS], inference=True)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_exploration(
|
||||
self, batch: Dict[str, TensorType], **kwargs
|
||||
) -> Dict[str, TensorType]:
|
||||
return self._pi(batch[Columns.OBS], inference=False)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch: Dict[str, TensorType]) -> Dict[str, TensorType]:
|
||||
return self._forward_exploration(batch)
|
||||
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(self, batch: Dict[str, TensorType], embeddings=None):
|
||||
# Value function forward pass.
|
||||
vf_out = self._value_net(batch[Columns.OBS])
|
||||
# Squeeze out last dimension (single node value head).
|
||||
return vf_out.squeeze(-1)
|
||||
|
||||
# __sphinx_begin__
|
||||
def _pi(self, obs, inference: bool):
|
||||
# Prior forward pass and sample a1.
|
||||
prior_out = self._prior_net(obs)
|
||||
dist_a1 = TorchCategorical.from_logits(prior_out)
|
||||
if inference:
|
||||
dist_a1 = dist_a1.to_deterministic()
|
||||
a1 = dist_a1.sample()
|
||||
|
||||
# Posterior forward pass and sample a2.
|
||||
posterior_batch = torch.cat(
|
||||
[obs, one_hot(a1, self.action_space[0])],
|
||||
dim=-1,
|
||||
)
|
||||
posterior_out = self._posterior_net(posterior_batch)
|
||||
dist_a2 = TorchDiagGaussian.from_logits(posterior_out)
|
||||
if inference:
|
||||
dist_a2 = dist_a2.to_deterministic()
|
||||
a2 = dist_a2.sample()
|
||||
actions = (a1, a2)
|
||||
|
||||
# We need logp and distribution parameters for the loss.
|
||||
return {
|
||||
Columns.ACTION_LOGP: (
|
||||
TorchMultiDistribution((dist_a1, dist_a2)).logp(actions)
|
||||
),
|
||||
Columns.ACTION_DIST_INPUTS: torch.cat([prior_out, posterior_out], dim=-1),
|
||||
Columns.ACTIONS: actions,
|
||||
}
|
||||
# __sphinx_end__
|
||||
|
||||
@override(TorchRLModule)
|
||||
def get_inference_action_dist_cls(self):
|
||||
return TorchMultiDistribution.get_partial_dist_cls(
|
||||
child_distribution_cls_struct=(TorchCategorical, TorchDiagGaussian),
|
||||
input_lens=(3, 2),
|
||||
)
|
||||
@@ -0,0 +1,153 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.distribution.torch.torch_distribution import TorchCategorical
|
||||
from ray.rllib.core.rl_module.apis import ValueFunctionAPI
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.typing import TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
def _make_categorical_with_temperature(temp):
|
||||
"""Helper function to create a new action distribution class.
|
||||
|
||||
The returned class takes a temperature parameter in its constructor with the default
|
||||
value `temp`.
|
||||
|
||||
Args:
|
||||
temp: The default temperature to use for the generated distribution class.
|
||||
"""
|
||||
|
||||
class TorchCategoricalWithTemp(TorchCategorical):
|
||||
def __init__(self, logits=None, probs=None, temperature: float = temp):
|
||||
"""Initializes a TorchCategoricalWithTemp instance.
|
||||
|
||||
Args:
|
||||
logits: Event log probabilities (non-normalized).
|
||||
probs: The probabilities of each event.
|
||||
temperature: In case of using logits, this parameter can be used to
|
||||
determine the sharpness of the distribution. i.e.
|
||||
``probs = softmax(logits / temperature)``. The temperature must be
|
||||
strictly positive. A low value (e.g. 1e-10) will result in argmax
|
||||
sampling while a larger value will result in uniform sampling.
|
||||
"""
|
||||
# Either divide logits or probs by the temperature.
|
||||
assert (
|
||||
temperature > 0.0
|
||||
), f"Temperature ({temperature}) must be strictly positive!"
|
||||
if logits is not None:
|
||||
logits /= temperature
|
||||
else:
|
||||
probs = torch.nn.functional.softmax(probs / temperature)
|
||||
super().__init__(logits, probs)
|
||||
|
||||
return TorchCategoricalWithTemp
|
||||
|
||||
|
||||
class CustomActionDistributionRLModule(TorchRLModule, ValueFunctionAPI):
|
||||
"""A simple TorchRLModule with its own custom action distribution.
|
||||
|
||||
The distribution differs from the default one by an additional temperature
|
||||
parameter applied on top of the Categorical base distribution. See the above
|
||||
`TorchCategoricalWithTemp` class for details.
|
||||
|
||||
.. testcode::
|
||||
|
||||
import numpy as np
|
||||
import gymnasium as gym
|
||||
|
||||
my_net = CustomActionDistributionRLModule(
|
||||
observation_space=gym.spaces.Box(-1.0, 1.0, (4,), np.float32),
|
||||
action_space=gym.spaces.Discrete(4),
|
||||
model_config={"action_dist_temperature": 5.0},
|
||||
)
|
||||
|
||||
B = 10
|
||||
data = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, 4)).astype(np.float32)
|
||||
)
|
||||
# Expect a relatively high-temperature distribution.
|
||||
# Set "action_dist_temperature" to small values << 1.0 to approximate greedy
|
||||
# behavior (even when stochastically sampling from the distribution).
|
||||
print(my_net.forward_exploration({"obs": data}))
|
||||
"""
|
||||
|
||||
@override(TorchRLModule)
|
||||
def setup(self):
|
||||
"""Use this method to create all the model components that you require.
|
||||
|
||||
Feel free to access the following useful properties in this class:
|
||||
- `self.model_config`: The config dict for this RLModule class,
|
||||
which should contain flexible settings, for example: {"hiddens": [256, 256]}.
|
||||
- `self.observation|action_space`: The observation and action space that
|
||||
this RLModule is subject to. Note that the observation space might not be the
|
||||
exact space from your env, but that it might have already gone through
|
||||
preprocessing through a connector pipeline (for example, flattening,
|
||||
frame-stacking, mean/std-filtering, etc..).
|
||||
- `self.inference_only`: If True, this model should be built only for inference
|
||||
purposes, in which case you may want to exclude any components that are not used
|
||||
for computing actions, for example a value function branch.
|
||||
"""
|
||||
input_dim = self.observation_space.shape[0]
|
||||
hidden_dim = self.model_config.get("hidden_dim", 256)
|
||||
output_dim = self.action_space.n
|
||||
|
||||
# Define simple encoder, and policy- and vf heads.
|
||||
self._encoder = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_dim, hidden_dim),
|
||||
torch.nn.ReLU(),
|
||||
)
|
||||
self._policy_net = torch.nn.Linear(hidden_dim, output_dim)
|
||||
self._vf = nn.Linear(hidden_dim, 1)
|
||||
|
||||
# Plug in a custom action dist class.
|
||||
# NOTE: If you need more granularity as to which distribution class is used by
|
||||
# which forward method (`forward_inference`, `forward_exploration`,
|
||||
# `forward_train`), override the RLModule methods
|
||||
# `get_inference_action_dist_cls`, `get_exploration_action_dist_cls`, and
|
||||
# `get_train_action_dist_cls`, and return
|
||||
# your custom class(es) from these. In this case, leave `self.action_dist_cls`
|
||||
# set to None, its default value.
|
||||
self.action_dist_cls = _make_categorical_with_temperature(
|
||||
self.model_config["action_dist_temperature"]
|
||||
)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward(self, batch, **kwargs):
|
||||
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
|
||||
_, logits = self._compute_embeddings_and_logits(batch)
|
||||
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
}
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
|
||||
embeddings, logits = self._compute_embeddings_and_logits(batch)
|
||||
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
Columns.EMBEDDINGS: embeddings,
|
||||
}
|
||||
|
||||
# We implement this RLModule as a ValueFunctionAPI RLModule, so it can be used
|
||||
# by value-based methods like PPO or IMPALA.
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(
|
||||
self,
|
||||
batch: Dict[str, Any],
|
||||
embeddings: Optional[Any] = None,
|
||||
) -> TensorType:
|
||||
# Features not provided -> We need to compute them first.
|
||||
if embeddings is None:
|
||||
embeddings = self._encoder(batch[Columns.OBS])
|
||||
return self._vf(embeddings).squeeze(-1)
|
||||
|
||||
def _compute_embeddings_and_logits(self, batch):
|
||||
embeddings = self._encoder(batch[Columns.OBS])
|
||||
logits = self._policy_net(embeddings)
|
||||
return embeddings, logits
|
||||
@@ -0,0 +1,240 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
import tree # pip install dm_tree
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module.apis import SelfSupervisedLossAPI
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
from ray.rllib.models.utils import get_activation_fn
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.torch_utils import one_hot
|
||||
from ray.rllib.utils.typing import ModuleID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.core.learner.torch.torch_learner import TorchLearner
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class IntrinsicCuriosityModel(TorchRLModule, SelfSupervisedLossAPI):
|
||||
"""An intrinsic curiosity model (ICM) as TorchRLModule for better exploration.
|
||||
|
||||
For more details, see:
|
||||
[1] Curiosity-driven Exploration by Self-supervised Prediction
|
||||
Pathak, Agrawal, Efros, and Darrell - UC Berkeley - ICML 2017.
|
||||
https://arxiv.org/pdf/1705.05363.pdf
|
||||
|
||||
Learns a simplified model of the environment based on three networks:
|
||||
1) Embedding observations into latent space ("feature" network).
|
||||
2) Predicting the action, given two consecutive embedded observations
|
||||
("inverse" network).
|
||||
3) Predicting the next embedded obs, given an obs and action
|
||||
("forward" network).
|
||||
|
||||
The less the agent is able to predict the actually observed next feature
|
||||
vector, given obs and action (through the forwards network), the larger the
|
||||
"intrinsic reward", which will be added to the extrinsic reward.
|
||||
Therefore, if a state transition was unexpected, the agent becomes
|
||||
"curious" and will further explore this transition leading to better
|
||||
exploration in sparse rewards environments.
|
||||
|
||||
.. testcode::
|
||||
|
||||
import numpy as np
|
||||
import gymnasium as gym
|
||||
import torch
|
||||
|
||||
from ray.rllib.core import Columns
|
||||
from ray.rllib.examples.rl_modules.classes.intrinsic_curiosity_model_rlm import ( # noqa
|
||||
IntrinsicCuriosityModel
|
||||
)
|
||||
|
||||
B = 10 # batch size
|
||||
O = 4 # obs (1D) dim
|
||||
A = 2 # num actions
|
||||
f = 25 # feature dim
|
||||
|
||||
# Construct the RLModule.
|
||||
icm_net = IntrinsicCuriosityModel(
|
||||
observation_space=gym.spaces.Box(-1.0, 1.0, (O,), np.float32),
|
||||
action_space=gym.spaces.Discrete(A),
|
||||
)
|
||||
|
||||
# Create some dummy input.
|
||||
obs = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, O)).astype(np.float32)
|
||||
)
|
||||
next_obs = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, O)).astype(np.float32)
|
||||
)
|
||||
actions = torch.from_numpy(
|
||||
np.random.random_integers(0, A - 1, size=(B,))
|
||||
)
|
||||
input_dict = {
|
||||
Columns.OBS: obs,
|
||||
Columns.NEXT_OBS: next_obs,
|
||||
Columns.ACTIONS: actions,
|
||||
}
|
||||
|
||||
# Call `forward_train()` to get phi (feature vector from obs), next-phi
|
||||
# (feature vector from next obs), and the intrinsic rewards (individual, per
|
||||
# batch-item forward loss values).
|
||||
print(icm_net.forward_train(input_dict))
|
||||
|
||||
# Print out the number of parameters.
|
||||
num_all_params = sum(int(np.prod(p.size())) for p in icm_net.parameters())
|
||||
print(f"num params = {num_all_params}")
|
||||
"""
|
||||
|
||||
@override(TorchRLModule)
|
||||
def setup(self):
|
||||
# Get the ICM achitecture settings from the `model_config` attribute:
|
||||
cfg = self.model_config
|
||||
|
||||
feature_dim = cfg.get("feature_dim", 288)
|
||||
|
||||
# Build the feature model (encoder of observations to feature space).
|
||||
layers = []
|
||||
dense_layers = cfg.get("feature_net_hiddens", (256, 256))
|
||||
# `in_size` is the observation space (assume a simple Box(1D)).
|
||||
in_size = self.observation_space.shape[0]
|
||||
for out_size in dense_layers:
|
||||
layers.append(nn.Linear(in_size, out_size))
|
||||
if cfg.get("feature_net_activation") not in [None, "linear"]:
|
||||
layers.append(
|
||||
get_activation_fn(cfg["feature_net_activation"], "torch")()
|
||||
)
|
||||
in_size = out_size
|
||||
# Last feature layer of n nodes (feature dimension).
|
||||
layers.append(nn.Linear(in_size, feature_dim))
|
||||
self._feature_net = nn.Sequential(*layers)
|
||||
|
||||
# Build the inverse model (predicting the action between two observations).
|
||||
layers = []
|
||||
dense_layers = cfg.get("inverse_net_hiddens", (256,))
|
||||
# `in_size` is 2x the feature dim.
|
||||
in_size = feature_dim * 2
|
||||
for out_size in dense_layers:
|
||||
layers.append(nn.Linear(in_size, out_size))
|
||||
if cfg.get("inverse_net_activation") not in [None, "linear"]:
|
||||
layers.append(
|
||||
get_activation_fn(cfg["inverse_net_activation"], "torch")()
|
||||
)
|
||||
in_size = out_size
|
||||
# Last feature layer of n nodes (action space).
|
||||
layers.append(nn.Linear(in_size, self.action_space.n))
|
||||
self._inverse_net = nn.Sequential(*layers)
|
||||
|
||||
# Build the forward model (predicting the next observation from current one and
|
||||
# action).
|
||||
layers = []
|
||||
dense_layers = cfg.get("forward_net_hiddens", (256,))
|
||||
# `in_size` is the feature dim + action space (one-hot).
|
||||
in_size = feature_dim + self.action_space.n
|
||||
for out_size in dense_layers:
|
||||
layers.append(nn.Linear(in_size, out_size))
|
||||
if cfg.get("forward_net_activation") not in [None, "linear"]:
|
||||
layers.append(
|
||||
get_activation_fn(cfg["forward_net_activation"], "torch")()
|
||||
)
|
||||
in_size = out_size
|
||||
# Last feature layer of n nodes (feature dimension).
|
||||
layers.append(nn.Linear(in_size, feature_dim))
|
||||
self._forward_net = nn.Sequential(*layers)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
# Push both observations through feature net to get feature vectors (phis).
|
||||
# We cat/batch them here for efficiency reasons (save one forward pass).
|
||||
obs = tree.map_structure(
|
||||
lambda obs, next_obs: torch.cat([obs, next_obs], dim=0),
|
||||
batch[Columns.OBS],
|
||||
batch[Columns.NEXT_OBS],
|
||||
)
|
||||
phis = self._feature_net(obs)
|
||||
# Split again to yield 2 individual phi tensors.
|
||||
phi, next_phi = torch.chunk(phis, 2)
|
||||
|
||||
# Predict next feature vector (next_phi) with forward model (using obs and
|
||||
# actions).
|
||||
predicted_next_phi = self._forward_net(
|
||||
torch.cat(
|
||||
[
|
||||
phi,
|
||||
one_hot(batch[Columns.ACTIONS].long(), self.action_space).float(),
|
||||
],
|
||||
dim=-1,
|
||||
)
|
||||
)
|
||||
|
||||
# Forward loss term: Predicted phi - given phi and action - vs actually observed
|
||||
# phi (square-root of L2 norm). Note that this is the intrinsic reward that
|
||||
# will be used and the mean of this is the forward net loss.
|
||||
forward_l2_norm_sqrt = 0.5 * torch.sum(
|
||||
torch.pow(predicted_next_phi - next_phi, 2.0), dim=-1
|
||||
)
|
||||
|
||||
output = {
|
||||
Columns.INTRINSIC_REWARDS: forward_l2_norm_sqrt,
|
||||
# Computed feature vectors (used to compute the losses later).
|
||||
"phi": phi,
|
||||
"next_phi": next_phi,
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
@override(SelfSupervisedLossAPI)
|
||||
def compute_self_supervised_loss(
|
||||
self,
|
||||
*,
|
||||
learner: "TorchLearner",
|
||||
module_id: ModuleID,
|
||||
config: "AlgorithmConfig",
|
||||
batch: Dict[str, Any],
|
||||
fwd_out: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
module = learner.module[module_id].unwrapped()
|
||||
|
||||
# Forward net loss.
|
||||
forward_loss = torch.mean(fwd_out[Columns.INTRINSIC_REWARDS])
|
||||
|
||||
# Inverse loss term (predicted action that led from phi to phi' vs
|
||||
# actual action taken).
|
||||
dist_inputs = module._inverse_net(
|
||||
torch.cat([fwd_out["phi"], fwd_out["next_phi"]], dim=-1)
|
||||
)
|
||||
action_dist = module.get_train_action_dist_cls().from_logits(dist_inputs)
|
||||
|
||||
# Neg log(p); p=probability of observed action given the inverse-NN
|
||||
# predicted action distribution.
|
||||
inverse_loss = -action_dist.logp(batch[Columns.ACTIONS])
|
||||
inverse_loss = torch.mean(inverse_loss)
|
||||
|
||||
# Calculate the ICM loss.
|
||||
total_loss = (
|
||||
config.learner_config_dict["forward_loss_weight"] * forward_loss
|
||||
+ (1.0 - config.learner_config_dict["forward_loss_weight"]) * inverse_loss
|
||||
)
|
||||
|
||||
learner.metrics.log_dict(
|
||||
{
|
||||
"mean_intrinsic_rewards": forward_loss,
|
||||
"forward_loss": forward_loss,
|
||||
"inverse_loss": inverse_loss,
|
||||
},
|
||||
key=module_id,
|
||||
window=1,
|
||||
)
|
||||
|
||||
return total_loss
|
||||
|
||||
# Inference and exploration not supported (this is a world-model that should only
|
||||
# be used for training).
|
||||
@override(TorchRLModule)
|
||||
def _forward(self, batch, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"`IntrinsicCuriosityModel` should only be used for training! "
|
||||
"Only calls to `forward_train()` supported."
|
||||
)
|
||||
@@ -0,0 +1,281 @@
|
||||
import abc
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.learner.utils import make_target_network
|
||||
from ray.rllib.core.rl_module.apis import (
|
||||
TARGET_NETWORK_ACTION_DIST_INPUTS,
|
||||
InferenceOnlyAPI,
|
||||
TargetNetworkAPI,
|
||||
)
|
||||
from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
from ray.rllib.utils.annotations import (
|
||||
override,
|
||||
)
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.typing import NetworkType, TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class LSTMContainingRLModule(TorchRLModule, ValueFunctionAPI):
|
||||
"""An example TorchRLModule that contains an LSTM layer.
|
||||
|
||||
.. testcode::
|
||||
|
||||
import numpy as np
|
||||
import gymnasium as gym
|
||||
|
||||
B = 10 # batch size
|
||||
T = 5 # seq len
|
||||
e = 25 # embedding dim
|
||||
CELL = 32 # LSTM cell size
|
||||
|
||||
# Construct the RLModule.
|
||||
my_net = LSTMContainingRLModule(
|
||||
observation_space=gym.spaces.Box(-1.0, 1.0, (e,), np.float32),
|
||||
action_space=gym.spaces.Discrete(4),
|
||||
model_config={"lstm_cell_size": CELL}
|
||||
)
|
||||
|
||||
# Create some dummy input.
|
||||
obs = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, T, e)
|
||||
).astype(np.float32))
|
||||
state_in = my_net.get_initial_state()
|
||||
# Repeat state_in across batch.
|
||||
state_in = tree.map_structure(
|
||||
lambda s: torch.from_numpy(s).unsqueeze(0).repeat(B, 1), state_in
|
||||
)
|
||||
input_dict = {
|
||||
Columns.OBS: obs,
|
||||
Columns.STATE_IN: state_in,
|
||||
}
|
||||
|
||||
# Run through all 3 forward passes.
|
||||
print(my_net.forward_inference(input_dict))
|
||||
print(my_net.forward_exploration(input_dict))
|
||||
print(my_net.forward_train(input_dict))
|
||||
|
||||
# Print out the number of parameters.
|
||||
num_all_params = sum(int(np.prod(p.size())) for p in my_net.parameters())
|
||||
print(f"num params = {num_all_params}")
|
||||
"""
|
||||
|
||||
@override(TorchRLModule)
|
||||
def setup(self):
|
||||
"""Use this method to create all the model components that you require.
|
||||
|
||||
Feel free to access the following useful properties in this class:
|
||||
- `self.model_config`: The config dict for this RLModule class,
|
||||
which should contain flxeible settings, for example: {"hiddens": [256, 256]}.
|
||||
- `self.observation|action_space`: The observation and action space that
|
||||
this RLModule is subject to. Note that the observation space might not be the
|
||||
exact space from your env, but that it might have already gone through
|
||||
preprocessing through a connector pipeline (for example, flattening,
|
||||
frame-stacking, mean/std-filtering, etc..).
|
||||
"""
|
||||
# Assume a simple Box(1D) tensor as input shape.
|
||||
in_size = self.observation_space.shape[0]
|
||||
|
||||
# Get the LSTM cell size from the `model_config` attribute:
|
||||
self._lstm_cell_size = self.model_config.get("lstm_cell_size", 256)
|
||||
self._lstm = nn.LSTM(in_size, self._lstm_cell_size, batch_first=True)
|
||||
in_size = self._lstm_cell_size
|
||||
|
||||
# Build a sequential stack.
|
||||
layers = []
|
||||
# Get the dense layer pre-stack configuration from the same config dict.
|
||||
dense_layers = self.model_config.get("dense_layers", [128, 128])
|
||||
for out_size in dense_layers:
|
||||
# Dense layer.
|
||||
layers.append(nn.Linear(in_size, out_size))
|
||||
# ReLU activation.
|
||||
layers.append(nn.ReLU())
|
||||
in_size = out_size
|
||||
|
||||
self._fc_net = nn.Sequential(*layers)
|
||||
|
||||
# Logits layer (no bias, no activation).
|
||||
self._pi_head = nn.Linear(in_size, self.action_space.n)
|
||||
# Single-node value layer.
|
||||
self._values = nn.Linear(in_size, 1)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def get_initial_state(self) -> Any:
|
||||
return {
|
||||
"h": np.zeros(shape=(self._lstm_cell_size,), dtype=np.float32),
|
||||
"c": np.zeros(shape=(self._lstm_cell_size,), dtype=np.float32),
|
||||
}
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward(self, batch, **kwargs):
|
||||
# Compute the basic 1D embedding tensor (inputs to policy- and value-heads).
|
||||
embeddings, state_outs = self._compute_embeddings_and_state_outs(batch)
|
||||
logits = self._pi_head(embeddings)
|
||||
|
||||
# Return logits as ACTION_DIST_INPUTS (categorical distribution).
|
||||
# Note that the default `GetActions` connector piece (in the EnvRunner) will
|
||||
# take care of argmax-"sampling" from the logits to yield the inference (greedy)
|
||||
# action.
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
Columns.STATE_OUT: state_outs,
|
||||
}
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
# Same logic as _forward, but also return embeddings to be used by value
|
||||
# function branch during training.
|
||||
embeddings, state_outs = self._compute_embeddings_and_state_outs(batch)
|
||||
logits = self._pi_head(embeddings)
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
Columns.STATE_OUT: state_outs,
|
||||
Columns.EMBEDDINGS: embeddings,
|
||||
}
|
||||
|
||||
# We implement this RLModule as a ValueFunctionAPI RLModule, so it can be used
|
||||
# by value-based methods like PPO or IMPALA.
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(
|
||||
self, batch: Dict[str, Any], embeddings: Optional[Any] = None
|
||||
) -> TensorType:
|
||||
if embeddings is None:
|
||||
embeddings, _ = self._compute_embeddings_and_state_outs(batch)
|
||||
values = self._values(embeddings).squeeze(-1)
|
||||
return values
|
||||
|
||||
def _compute_embeddings_and_state_outs(self, batch):
|
||||
obs = batch[Columns.OBS]
|
||||
state_in = batch[Columns.STATE_IN]
|
||||
h, c = state_in["h"], state_in["c"]
|
||||
# Unsqueeze the layer dim (we only have 1 LSTM layer).
|
||||
embeddings, (h, c) = self._lstm(obs, (h.unsqueeze(0), c.unsqueeze(0)))
|
||||
# Push through our FC net.
|
||||
embeddings = self._fc_net(embeddings)
|
||||
# Squeeze the layer dim (we only have 1 LSTM layer).
|
||||
return embeddings, {"h": h.squeeze(0), "c": c.squeeze(0)}
|
||||
|
||||
|
||||
class LSTMContainingRLModuleWithTargetNetwork(
|
||||
LSTMContainingRLModule, TargetNetworkAPI, InferenceOnlyAPI, abc.ABC
|
||||
):
|
||||
"""LSTMContainingRLModule with TargetNetworkAPI support for use with APPO.
|
||||
|
||||
This class extends LSTMContainingRLModule to add target network functionality,
|
||||
which is required by algorithms like APPO that use target networks for
|
||||
importance sampling and policy updates.
|
||||
|
||||
.. testcode::
|
||||
|
||||
import numpy as np
|
||||
import gymnasium as gym
|
||||
import tree
|
||||
import torch
|
||||
from ray.rllib.core.columns import Columns
|
||||
|
||||
B = 10 # batch size
|
||||
T = 5 # seq len
|
||||
e = 25 # embedding dim
|
||||
CELL = 32 # LSTM cell size
|
||||
|
||||
# Construct the RLModule with target network support.
|
||||
my_net = LSTMContainingRLModuleWithTargetNetwork(
|
||||
observation_space=gym.spaces.Box(-1.0, 1.0, (e,), np.float32),
|
||||
action_space=gym.spaces.Discrete(4),
|
||||
model_config={"lstm_cell_size": CELL}
|
||||
)
|
||||
|
||||
# Create target networks (required for TargetNetworkAPI).
|
||||
my_net.make_target_networks()
|
||||
|
||||
# Create some dummy input.
|
||||
obs = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, T, e)
|
||||
).astype(np.float32))
|
||||
state_in = my_net.get_initial_state()
|
||||
# Repeat state_in across batch.
|
||||
state_in = tree.map_structure(
|
||||
lambda s: torch.from_numpy(s).unsqueeze(0).repeat(B, 1), state_in
|
||||
)
|
||||
input_dict = {
|
||||
Columns.OBS: obs,
|
||||
Columns.STATE_IN: state_in,
|
||||
}
|
||||
|
||||
# Run through all forward passes including target network forward.
|
||||
print("Forward inference:", my_net.forward_inference(input_dict))
|
||||
print("Forward exploration:", my_net.forward_exploration(input_dict))
|
||||
print("Forward train:", my_net.forward_train(input_dict))
|
||||
print("Forward target:", my_net.forward_target(input_dict))
|
||||
|
||||
# Get target network pairs for synchronization.
|
||||
target_pairs = my_net.get_target_network_pairs()
|
||||
print(f"Number of target network pairs: {len(target_pairs)}")
|
||||
|
||||
# Print out the number of parameters.
|
||||
num_all_params = sum(int(np.prod(p.size())) for p in my_net.parameters())
|
||||
print(f"num params = {num_all_params}")
|
||||
|
||||
Example usage with APPO:
|
||||
|
||||
.. testcode::
|
||||
|
||||
from ray.rllib.algorithms.appo import APPOConfig
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.examples.rl_modules.classes.lstm_containing_rlm import (
|
||||
LSTMContainingRLModuleWithTargetNetwork,
|
||||
)
|
||||
|
||||
config = (
|
||||
APPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.rl_module(
|
||||
rl_module_spec=RLModuleSpec(
|
||||
module_class=LSTMContainingRLModuleWithTargetNetwork,
|
||||
model_config={"lstm_cell_size": 256, "dense_layers": [128, 128]},
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
@override(TargetNetworkAPI)
|
||||
def make_target_networks(self):
|
||||
"""Creates target networks for LSTM, FC net, and policy head."""
|
||||
self._old_lstm = make_target_network(self._lstm)
|
||||
self._old_fc_net = make_target_network(self._fc_net)
|
||||
self._old_pi_head = make_target_network(self._pi_head)
|
||||
|
||||
@override(TargetNetworkAPI)
|
||||
def get_target_network_pairs(self) -> List[Tuple[NetworkType, NetworkType]]:
|
||||
"""Returns pairs of (main_net, target_net) for target network updates."""
|
||||
return [
|
||||
(self._lstm, self._old_lstm),
|
||||
(self._fc_net, self._old_fc_net),
|
||||
(self._pi_head, self._old_pi_head),
|
||||
]
|
||||
|
||||
@override(TargetNetworkAPI)
|
||||
def forward_target(self, batch: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Forward pass through target networks to get action distribution inputs."""
|
||||
# Compute embeddings using target networks (similar to _compute_embeddings_and_state_outs)
|
||||
obs = batch[Columns.OBS]
|
||||
state_in = batch[Columns.STATE_IN]
|
||||
h, c = state_in["h"], state_in["c"]
|
||||
# Unsqueeze the layer dim (we only have 1 LSTM layer) and forward through target LSTM
|
||||
embeddings, (h, c) = self._old_lstm(obs, (h.unsqueeze(0), c.unsqueeze(0)))
|
||||
# Push through target FC net
|
||||
embeddings = self._old_fc_net(embeddings)
|
||||
# Forward through target policy head to get action distribution inputs
|
||||
old_action_dist_logits = self._old_pi_head(embeddings)
|
||||
|
||||
return {TARGET_NETWORK_ACTION_DIST_INPUTS: old_action_dist_logits}
|
||||
|
||||
@override(InferenceOnlyAPI)
|
||||
def get_non_inference_attributes(self) -> List[str]:
|
||||
"""Returns attributes that should not be included in inference-only mode."""
|
||||
return ["_old_lstm", "_old_fc_net", "_old_pi_head", "_values"]
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
This example shows how to take full control over what models and action distribution
|
||||
are being built inside an RL Module. With this pattern, we can bypass a Catalog and
|
||||
explicitly define our own models within a given RL Module.
|
||||
|
||||
Here, we plug a pre-trained MobileNet V3 (small) image encoder from `torchvision` into
|
||||
a PPO RLModule and use it to encode image observations before the policy- and
|
||||
value-heads. You can modify this example to accommodate your own encoder network or
|
||||
other pre-trained networks.
|
||||
"""
|
||||
# __sphinx_doc_begin__
|
||||
from dataclasses import dataclass
|
||||
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
|
||||
from ray.rllib.algorithms.ppo.ppo import PPOConfig
|
||||
from ray.rllib.algorithms.ppo.torch.default_ppo_torch_rl_module import (
|
||||
DefaultPPOTorchRLModule,
|
||||
)
|
||||
from ray.rllib.core.models.base import ENCODER_OUT, Encoder
|
||||
from ray.rllib.core.models.configs import (
|
||||
ActorCriticEncoderConfig,
|
||||
MLPHeadConfig,
|
||||
ModelConfig,
|
||||
)
|
||||
from ray.rllib.core.models.torch.base import TorchModel
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.examples.envs.classes.random_env import RandomEnv
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
# torchvision's pre-trained image classifiers expect (3, 224, 224) inputs.
|
||||
MOBILENET_INPUT_SHAPE = (3, 224, 224)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MobileNetV3EncoderConfig(ModelConfig):
|
||||
freeze: bool = True
|
||||
|
||||
@property
|
||||
def output_dims(self):
|
||||
# MobileNet V3 (small) has a flat output of length 1000 (its ImageNet logits).
|
||||
return (1000,)
|
||||
|
||||
def build(self, framework):
|
||||
assert framework == "torch", "Unsupported framework `{}`!".format(framework)
|
||||
return MobileNetV3Encoder(self)
|
||||
|
||||
|
||||
class MobileNetV3Encoder(TorchModel, Encoder):
|
||||
"""A MobileNet V3 (small) encoder for RLlib."""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
# Load MobileNet V3 (small) with its default pre-trained ImageNet weights from
|
||||
# the installed torchvision. We use torchvision directly (rather than
|
||||
# `torch.hub.load`) so the model code always matches the installed torch.
|
||||
from torchvision.models import MobileNet_V3_Small_Weights, mobilenet_v3_small
|
||||
|
||||
self.net = mobilenet_v3_small(weights=MobileNet_V3_Small_Weights.DEFAULT)
|
||||
if config.freeze:
|
||||
# We don't want to train this encoder, so freeze its parameters!
|
||||
for p in self.net.parameters():
|
||||
p.requires_grad = False
|
||||
|
||||
def _forward(self, input_dict, **kwargs):
|
||||
return {ENCODER_OUT: (self.net(input_dict["obs"]))}
|
||||
|
||||
|
||||
class MobileNetTorchPPORLModule(DefaultPPOTorchRLModule):
|
||||
"""A DefaultPPORLModule with MobileNet V3 (small) as an encoder.
|
||||
|
||||
The idea behind this model is to demonstrate how we can bypass catalog to
|
||||
take full control over what models and action distribution are being built.
|
||||
In this example, we do this to modify an existing RLModule with a custom encoder.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
mobilenet_config = MobileNetV3EncoderConfig()
|
||||
# Since we want to use PPO, which is an actor-critic algorithm, we need to
|
||||
# use an ActorCriticEncoderConfig to wrap the base encoder config.
|
||||
actor_critic_encoder_config = ActorCriticEncoderConfig(
|
||||
base_encoder_config=mobilenet_config
|
||||
)
|
||||
|
||||
self.encoder = actor_critic_encoder_config.build(framework="torch")
|
||||
mobilenet_output_dims = mobilenet_config.output_dims
|
||||
|
||||
pi_config = MLPHeadConfig(
|
||||
input_dims=mobilenet_output_dims,
|
||||
output_layer_dim=2,
|
||||
)
|
||||
|
||||
vf_config = MLPHeadConfig(input_dims=mobilenet_output_dims, output_layer_dim=1)
|
||||
|
||||
self.pi = pi_config.build(framework="torch")
|
||||
self.vf = vf_config.build(framework="torch")
|
||||
|
||||
|
||||
config = (
|
||||
PPOConfig()
|
||||
.environment(
|
||||
RandomEnv,
|
||||
env_config={
|
||||
"action_space": gym.spaces.Discrete(2),
|
||||
# Test a simple Image observation space.
|
||||
"observation_space": gym.spaces.Box(
|
||||
0.0,
|
||||
1.0,
|
||||
shape=MOBILENET_INPUT_SHAPE,
|
||||
dtype=np.float32,
|
||||
),
|
||||
},
|
||||
)
|
||||
.env_runners(num_env_runners=0)
|
||||
# The following training settings make it so that a training iteration is very
|
||||
# quick. This is just for the sake of this example. PPO will not learn properly
|
||||
# with these settings!
|
||||
.training(
|
||||
train_batch_size_per_learner=32,
|
||||
minibatch_size=16,
|
||||
num_epochs=1,
|
||||
)
|
||||
.rl_module(rl_module_spec=RLModuleSpec(module_class=MobileNetTorchPPORLModule))
|
||||
)
|
||||
|
||||
config.build().train()
|
||||
# __sphinx_doc_end__
|
||||
@@ -0,0 +1,207 @@
|
||||
import pathlib
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import tree
|
||||
|
||||
from ray.rllib.core import DEFAULT_POLICY_ID, Columns
|
||||
from ray.rllib.core.distribution.torch.torch_distribution import (
|
||||
TorchCategorical,
|
||||
TorchDiagGaussian,
|
||||
TorchMultiCategorical,
|
||||
TorchMultiDistribution,
|
||||
TorchSquashedGaussian,
|
||||
)
|
||||
from ray.rllib.core.rl_module.apis import ValueFunctionAPI
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
from ray.rllib.models.torch.torch_action_dist import (
|
||||
TorchCategorical as OldTorchCategorical,
|
||||
TorchDiagGaussian as OldTorchDiagGaussian,
|
||||
TorchMultiActionDistribution as OldTorchMultiActionDistribution,
|
||||
TorchMultiCategorical as OldTorchMultiCategorical,
|
||||
TorchSquashedGaussian as OldTorchSquashedGaussian,
|
||||
)
|
||||
from ray.rllib.policy.torch_policy_v2 import TorchPolicyV2
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
|
||||
torch, _ = try_import_torch()
|
||||
|
||||
|
||||
class ModelV2ToRLModule(TorchRLModule, ValueFunctionAPI):
|
||||
"""An RLModule containing a (old stack) ModelV2.
|
||||
|
||||
The `ModelV2` may be define either through
|
||||
- an existing Policy checkpoint
|
||||
- an existing Algorithm checkpoint (and a policy ID or "default_policy")
|
||||
- or through an AlgorithmConfig object
|
||||
|
||||
The ModelV2 is created in the `setup` and contines to live through the lifetime
|
||||
of the RLModule.
|
||||
"""
|
||||
|
||||
@override(TorchRLModule)
|
||||
def setup(self):
|
||||
# Try extracting the policy ID from this RLModule's config dict.
|
||||
policy_id = self.model_config.get("policy_id", DEFAULT_POLICY_ID)
|
||||
|
||||
# Try getting the algorithm checkpoint from the `model_config`.
|
||||
algo_checkpoint_dir = self.model_config.get("algo_checkpoint_dir")
|
||||
if algo_checkpoint_dir:
|
||||
algo_checkpoint_dir = pathlib.Path(algo_checkpoint_dir)
|
||||
if not algo_checkpoint_dir.is_dir():
|
||||
raise ValueError(
|
||||
"The `model_config` of your RLModule must contain a "
|
||||
"`algo_checkpoint_dir` key pointing to the algo checkpoint "
|
||||
"directory! You can find this dir inside the results dir of your "
|
||||
"experiment. You can then add this path "
|
||||
"through `config.rl_module(model_config={"
|
||||
"'algo_checkpoint_dir': [your algo checkpoint dir]})`."
|
||||
)
|
||||
policy_checkpoint_dir = algo_checkpoint_dir / "policies" / policy_id
|
||||
# Try getting the policy checkpoint from the `model_config`.
|
||||
else:
|
||||
policy_checkpoint_dir = self.model_config.get("policy_checkpoint_dir")
|
||||
|
||||
# Create the ModelV2 from the Policy.
|
||||
if policy_checkpoint_dir:
|
||||
policy_checkpoint_dir = pathlib.Path(policy_checkpoint_dir)
|
||||
if not policy_checkpoint_dir.is_dir():
|
||||
raise ValueError(
|
||||
"The `model_config` of your RLModule must contain a "
|
||||
"`policy_checkpoint_dir` key pointing to the policy checkpoint "
|
||||
"directory! You can find this dir under the Algorithm's checkpoint "
|
||||
"dir in subdirectory: [algo checkpoint dir]/policies/[policy ID "
|
||||
"ex. `default_policy`]. You can then add this path through `config"
|
||||
".rl_module(model_config={'policy_checkpoint_dir': "
|
||||
"[your policy checkpoint dir]})`."
|
||||
)
|
||||
# Create a temporary policy object.
|
||||
policy = TorchPolicyV2.from_checkpoint(policy_checkpoint_dir)
|
||||
# Create the ModelV2 from scratch using the config.
|
||||
else:
|
||||
config = self.model_config.get("old_api_stack_algo_config")
|
||||
if not config:
|
||||
raise ValueError(
|
||||
"The `model_config` of your RLModule must contain a "
|
||||
"`algo_config` key with a AlgorithmConfig object in it that "
|
||||
"contains all the settings that would be necessary to construct a "
|
||||
"old API stack Algorithm/Policy/ModelV2! You can add this setting "
|
||||
"through `config.rl_module(model_config={'algo_config': "
|
||||
"[your old config]})`."
|
||||
)
|
||||
# Get the multi-agent policies dict.
|
||||
policy_dict, _ = config.get_multi_agent_setup(
|
||||
spaces={
|
||||
policy_id: (self.observation_space, self.action_space),
|
||||
},
|
||||
default_policy_class=config.algo_class.get_default_policy_class(config),
|
||||
)
|
||||
config = config.to_dict()
|
||||
config["__policy_id"] = policy_id
|
||||
policy = policy_dict[policy_id].policy_class(
|
||||
self.observation_space,
|
||||
self.action_space,
|
||||
config,
|
||||
)
|
||||
|
||||
self._model_v2 = policy.model
|
||||
|
||||
# Translate the action dist classes from the old API stack to the new.
|
||||
self.action_dist_class = self._translate_dist_class(policy.dist_class)
|
||||
|
||||
# Erase the torch policy from memory, so it can be garbage collected.
|
||||
del policy
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_inference(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
||||
return self._forward_pass(batch, inference=True)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_exploration(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
||||
return self._forward_inference(batch, **kwargs)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
||||
out = self._forward_pass(batch, inference=False)
|
||||
out[Columns.ACTION_LOGP] = self.get_train_action_dist_cls()(
|
||||
out[Columns.ACTION_DIST_INPUTS]
|
||||
).logp(batch[Columns.ACTIONS])
|
||||
out[Columns.VF_PREDS] = self._model_v2.value_function()
|
||||
if Columns.STATE_IN in batch and Columns.SEQ_LENS in batch:
|
||||
out[Columns.VF_PREDS] = torch.reshape(
|
||||
out[Columns.VF_PREDS], [len(batch[Columns.SEQ_LENS]), -1]
|
||||
)
|
||||
return out
|
||||
|
||||
def _forward_pass(self, batch, inference=True):
|
||||
# Translate states and seq_lens into old API stack formats.
|
||||
batch = batch.copy()
|
||||
state_in = batch.pop(Columns.STATE_IN, {})
|
||||
state_in = [s for i, s in sorted(state_in.items())]
|
||||
seq_lens = batch.pop(Columns.SEQ_LENS, None)
|
||||
|
||||
if state_in:
|
||||
if inference and seq_lens is None:
|
||||
seq_lens = torch.tensor(
|
||||
[1.0] * state_in[0].shape[0], device=state_in[0].device
|
||||
)
|
||||
elif not inference:
|
||||
assert seq_lens is not None
|
||||
# Perform the actual ModelV2 forward pass.
|
||||
# A recurrent ModelV2 adds and removes the time-rank itself (whereas in the
|
||||
# new API stack, the connector pipelines are responsible for doing this) ->
|
||||
# We have to remove, then re-add the time rank here to make ModelV2 work.
|
||||
batch = tree.map_structure(
|
||||
lambda s: torch.reshape(s, [-1] + list(s.shape[2:])), batch
|
||||
)
|
||||
nn_output, state_out = self._model_v2(batch, state_in, seq_lens)
|
||||
# Put back 1ts time rank into nn-output (inference).
|
||||
if state_in:
|
||||
if inference:
|
||||
nn_output = tree.map_structure(
|
||||
lambda s: torch.unsqueeze(s, axis=1), nn_output
|
||||
)
|
||||
else:
|
||||
nn_output = tree.map_structure(
|
||||
lambda s: torch.reshape(s, [len(seq_lens), -1] + list(s.shape[1:])),
|
||||
nn_output,
|
||||
)
|
||||
# Interpret the NN output as action logits.
|
||||
output = {Columns.ACTION_DIST_INPUTS: nn_output}
|
||||
# Add the `state_out` to the `output`, new API stack style.
|
||||
if state_out:
|
||||
output[Columns.STATE_OUT] = {}
|
||||
for i, o in enumerate(state_out):
|
||||
output[Columns.STATE_OUT][i] = o
|
||||
|
||||
return output
|
||||
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(self, batch: Dict[str, Any], embeddings: Optional[Any] = None):
|
||||
self._forward_pass(batch, inference=False)
|
||||
v_preds = self._model_v2.value_function()
|
||||
if Columns.STATE_IN in batch and Columns.SEQ_LENS in batch:
|
||||
v_preds = torch.reshape(v_preds, [len(batch[Columns.SEQ_LENS]), -1])
|
||||
return v_preds
|
||||
|
||||
@override(TorchRLModule)
|
||||
def get_initial_state(self):
|
||||
"""Converts the initial state list of ModelV2 into a dict (new API stack)."""
|
||||
init_state_list = self._model_v2.get_initial_state()
|
||||
return dict(enumerate(init_state_list))
|
||||
|
||||
def _translate_dist_class(self, old_dist_class):
|
||||
map_ = {
|
||||
OldTorchCategorical: TorchCategorical,
|
||||
OldTorchDiagGaussian: TorchDiagGaussian,
|
||||
OldTorchMultiActionDistribution: TorchMultiDistribution,
|
||||
OldTorchMultiCategorical: TorchMultiCategorical,
|
||||
OldTorchSquashedGaussian: TorchSquashedGaussian,
|
||||
}
|
||||
if old_dist_class not in map_:
|
||||
raise ValueError(
|
||||
f"ModelV2ToRLModule does NOT support {old_dist_class} action "
|
||||
f"distributions yet!"
|
||||
)
|
||||
|
||||
return map_[old_dist_class]
|
||||
@@ -0,0 +1,63 @@
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import tree # pip install dm_tree
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module import RLModule
|
||||
from ray.rllib.policy.sample_batch import SampleBatch
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.spaces.space_utils import batch as batch_func
|
||||
|
||||
|
||||
class RandomRLModule(RLModule):
|
||||
@override(RLModule)
|
||||
def _forward(self, batch, **kwargs):
|
||||
obs_batch_size = len(tree.flatten(batch[SampleBatch.OBS])[0])
|
||||
actions = batch_func(
|
||||
[self.action_space.sample() for _ in range(obs_batch_size)]
|
||||
)
|
||||
return {SampleBatch.ACTIONS: actions}
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_train(self, *args, **kwargs):
|
||||
# RandomRLModule should always be configured as non-trainable.
|
||||
# To do so, set in your config:
|
||||
# `config.multi_agent(policies_to_train=[list of ModuleIDs to be trained,
|
||||
# NOT including the ModuleID of this RLModule])`
|
||||
raise NotImplementedError("Random RLModule: Should not be trained!")
|
||||
|
||||
def compile(self, *args, **kwargs):
|
||||
"""Dummy method for compatibility with TorchRLModule.
|
||||
|
||||
This is hit when RolloutWorker tries to compile TorchRLModule."""
|
||||
pass
|
||||
|
||||
|
||||
class StatefulRandomRLModule(RandomRLModule):
|
||||
"""A stateful RLModule that returns STATE_OUT from its forward methods.
|
||||
|
||||
- Implements the `get_initial_state` method (returning a all-zeros dummy state).
|
||||
- Returns a dummy state under the `Columns.STATE_OUT` from its forward methods.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._internal_state_space = gym.spaces.Box(-1.0, 1.0, (1,))
|
||||
|
||||
@override(RLModule)
|
||||
def get_initial_state(self):
|
||||
return {
|
||||
"state": np.zeros_like([self._internal_state_space.sample()]),
|
||||
}
|
||||
|
||||
def _random_forward(self, batch, **kwargs):
|
||||
batch = super()._random_forward(batch, **kwargs)
|
||||
batch[Columns.STATE_OUT] = {
|
||||
"state": batch_func(
|
||||
[
|
||||
self._internal_state_space.sample()
|
||||
for _ in range(len(batch[Columns.ACTIONS]))
|
||||
]
|
||||
),
|
||||
}
|
||||
return batch
|
||||
@@ -0,0 +1,92 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module.rl_module import RLModule
|
||||
from ray.rllib.utils.annotations import override
|
||||
|
||||
|
||||
class AlwaysSameHeuristicRLM(RLModule):
|
||||
"""In rock-paper-scissors, always chooses the same action within an episode.
|
||||
|
||||
The first move is random, all the following moves are the same as the first one.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._actions_per_vector_idx = defaultdict(int)
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_inference(self, batch, **kwargs):
|
||||
ret = []
|
||||
# Note that the obs is the previous move of the opponens (0-2). If it's 3, it
|
||||
# means that there was no previous move and thus, the episode just started.
|
||||
for i, obs in enumerate(batch[Columns.OBS]):
|
||||
if obs == 3:
|
||||
self._actions_per_vector_idx[i] = np.random.choice([0, 1, 2])
|
||||
ret.append(self._actions_per_vector_idx[i])
|
||||
return {Columns.ACTIONS: np.array(ret)}
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_exploration(self, batch, **kwargs):
|
||||
return self._forward_inference(batch, **kwargs)
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"AlwaysSameHeuristicRLM is not trainable! Make sure you do NOT include it "
|
||||
"in your `config.multi_agent(policies_to_train={...})` set."
|
||||
)
|
||||
|
||||
|
||||
class BeatLastHeuristicRLM(RLModule):
|
||||
"""In rock-paper-scissors, always acts such that it beats prev. move of opponent.
|
||||
|
||||
The first move is random.
|
||||
|
||||
For example, after opponent played `rock` (and this policy made a random
|
||||
move), the next move would be `paper`(to beat `rock`).
|
||||
"""
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_inference(self, batch, **kwargs):
|
||||
"""Returns the exact action that would beat the previous action of the opponent.
|
||||
|
||||
The opponent's previous action is the current observation for this agent.
|
||||
|
||||
Both action- and observation spaces are discrete. There are 3 actions available.
|
||||
(0-2) and 4 observations (0-2 plus 3, where 3 is the observation after the env
|
||||
reset, when no action has been taken yet). Thereby:
|
||||
0=Rock
|
||||
1=Paper
|
||||
2=Scissors
|
||||
3=[after reset] (observation space only)
|
||||
"""
|
||||
return {
|
||||
Columns.ACTIONS: np.array(
|
||||
[self._pick_single_action(obs) for obs in batch[Columns.OBS]]
|
||||
),
|
||||
}
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_exploration(self, batch, **kwargs):
|
||||
return self._forward_inference(batch, **kwargs)
|
||||
|
||||
@override(RLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"BeatLastHeuristicRLM is not trainable! Make sure you do NOT include it in "
|
||||
"your `config.multi_agent(policies_to_train={...})` set."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _pick_single_action(prev_opponent_obs):
|
||||
if prev_opponent_obs == 0:
|
||||
return 1
|
||||
elif prev_opponent_obs == 1:
|
||||
return 2
|
||||
elif prev_opponent_obs == 2:
|
||||
return 0
|
||||
else:
|
||||
return np.random.choice([0, 1, 2])
|
||||
@@ -0,0 +1,194 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.learner.utils import make_target_network
|
||||
from ray.rllib.core.rl_module.apis import (
|
||||
TARGET_NETWORK_ACTION_DIST_INPUTS,
|
||||
TargetNetworkAPI,
|
||||
ValueFunctionAPI,
|
||||
)
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
from ray.rllib.models.torch.misc import (
|
||||
normc_initializer,
|
||||
same_padding,
|
||||
valid_padding,
|
||||
)
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.typing import TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class TinyAtariCNN(TorchRLModule, ValueFunctionAPI, TargetNetworkAPI):
|
||||
"""A tiny CNN stack for fast-learning of Atari envs.
|
||||
|
||||
The architecture here is the exact same as the one used by the old API stack as
|
||||
CNN default ModelV2.
|
||||
|
||||
We stack 3 CNN layers based on the config, then a 4th one with linear activation
|
||||
and n 1x1 filters, where n is the number of actions in the (discrete) action space.
|
||||
Simple reshaping (no flattening or extra linear layers necessary) lead to the
|
||||
action logits, which can directly be used inside a distribution or loss.
|
||||
|
||||
.. testcode::
|
||||
|
||||
import numpy as np
|
||||
import gymnasium as gym
|
||||
|
||||
my_net = TinyAtariCNN(
|
||||
observation_space=gym.spaces.Box(-1.0, 1.0, (42, 42, 4), np.float32),
|
||||
action_space=gym.spaces.Discrete(4),
|
||||
)
|
||||
|
||||
B = 10
|
||||
w = 42
|
||||
h = 42
|
||||
c = 4
|
||||
data = torch.from_numpy(
|
||||
np.random.random_sample(size=(B, w, h, c)).astype(np.float32)
|
||||
)
|
||||
print(my_net.forward_inference({"obs": data}))
|
||||
print(my_net.forward_exploration({"obs": data}))
|
||||
print(my_net.forward_train({"obs": data}))
|
||||
|
||||
num_all_params = sum(int(np.prod(p.size())) for p in my_net.parameters())
|
||||
print(f"num params = {num_all_params}")
|
||||
"""
|
||||
|
||||
@override(TorchRLModule)
|
||||
def setup(self):
|
||||
"""Use this method to create all the model components that you require.
|
||||
|
||||
Feel free to access the following useful properties in this class:
|
||||
- `self.model_config`: The config dict for this RLModule class,
|
||||
which should contain flxeible settings, for example: {"hiddens": [256, 256]}.
|
||||
- `self.observation|action_space`: The observation and action space that
|
||||
this RLModule is subject to. Note that the observation space might not be the
|
||||
exact space from your env, but that it might have already gone through
|
||||
preprocessing through a connector pipeline (for example, flattening,
|
||||
frame-stacking, mean/std-filtering, etc..).
|
||||
"""
|
||||
# Get the CNN stack config from our RLModuleConfig's (self.config)
|
||||
# `model_config` property:
|
||||
conv_filters = self.model_config.get("conv_filters")
|
||||
# Default CNN stack with 3 layers:
|
||||
if conv_filters is None:
|
||||
conv_filters = [
|
||||
[16, 4, 2, "same"], # num filters, kernel wxh, stride wxh, padding type
|
||||
[32, 4, 2, "same"],
|
||||
[256, 11, 1, "valid"],
|
||||
]
|
||||
|
||||
# Build the CNN layers.
|
||||
layers = []
|
||||
|
||||
# Add user-specified hidden convolutional layers first
|
||||
width, height, in_depth = self.observation_space.shape
|
||||
in_size = [width, height]
|
||||
for filter_specs in conv_filters:
|
||||
if len(filter_specs) == 4:
|
||||
out_depth, kernel_size, strides, padding = filter_specs
|
||||
else:
|
||||
out_depth, kernel_size, strides = filter_specs
|
||||
padding = "same"
|
||||
|
||||
# Pad like in tensorflow's SAME mode.
|
||||
if padding == "same":
|
||||
padding_size, out_size = same_padding(in_size, kernel_size, strides)
|
||||
layers.append(nn.ZeroPad2d(padding_size))
|
||||
# No actual padding is performed for "valid" mode, but we will still
|
||||
# compute the output size (input for the next layer).
|
||||
else:
|
||||
out_size = valid_padding(in_size, kernel_size, strides)
|
||||
|
||||
layer = nn.Conv2d(in_depth, out_depth, kernel_size, strides, bias=True)
|
||||
# Initialize CNN layer kernel and bias.
|
||||
nn.init.xavier_uniform_(layer.weight)
|
||||
nn.init.zeros_(layer.bias)
|
||||
layers.append(layer)
|
||||
# Activation.
|
||||
layers.append(nn.ReLU())
|
||||
|
||||
in_size = out_size
|
||||
in_depth = out_depth
|
||||
|
||||
self._base_cnn_stack = nn.Sequential(*layers)
|
||||
|
||||
# Add the final CNN 1x1 layer with num_filters == num_actions to be reshaped to
|
||||
# yield the logits (no flattening, no additional linear layers required).
|
||||
_final_conv = nn.Conv2d(in_depth, self.action_space.n, 1, 1, bias=True)
|
||||
nn.init.xavier_uniform_(_final_conv.weight)
|
||||
nn.init.zeros_(_final_conv.bias)
|
||||
self._logits = nn.Sequential(
|
||||
nn.ZeroPad2d(same_padding(in_size, 1, 1)[0]), _final_conv
|
||||
)
|
||||
|
||||
self._values = nn.Linear(in_depth, 1)
|
||||
# Mimick old API stack behavior of initializing the value function with `normc`
|
||||
# std=0.01.
|
||||
normc_initializer(0.01)(self._values.weight)
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward(self, batch, **kwargs):
|
||||
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
|
||||
_, logits = self._compute_embeddings_and_logits(batch)
|
||||
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
}
|
||||
|
||||
@override(TorchRLModule)
|
||||
def _forward_train(self, batch, **kwargs):
|
||||
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
|
||||
embeddings, logits = self._compute_embeddings_and_logits(batch)
|
||||
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
|
||||
return {
|
||||
Columns.ACTION_DIST_INPUTS: logits,
|
||||
Columns.EMBEDDINGS: embeddings,
|
||||
}
|
||||
|
||||
# We implement this RLModule as a TargetNetworkAPI RLModule, so it can be used
|
||||
# by the APPO algorithm.
|
||||
@override(TargetNetworkAPI)
|
||||
def make_target_networks(self) -> None:
|
||||
self._target_base_cnn_stack = make_target_network(self._base_cnn_stack)
|
||||
self._target_logits = make_target_network(self._logits)
|
||||
|
||||
@override(TargetNetworkAPI)
|
||||
def get_target_network_pairs(self):
|
||||
return [
|
||||
(self._base_cnn_stack, self._target_base_cnn_stack),
|
||||
(self._logits, self._target_logits),
|
||||
]
|
||||
|
||||
@override(TargetNetworkAPI)
|
||||
def forward_target(self, batch, **kw):
|
||||
obs = batch[Columns.OBS].permute(0, 3, 1, 2)
|
||||
embeddings = self._target_base_cnn_stack(obs)
|
||||
logits = self._target_logits(embeddings)
|
||||
return {TARGET_NETWORK_ACTION_DIST_INPUTS: torch.squeeze(logits, dim=[-1, -2])}
|
||||
|
||||
# We implement this RLModule as a ValueFunctionAPI RLModule, so it can be used
|
||||
# by value-based methods like PPO or IMPALA.
|
||||
@override(ValueFunctionAPI)
|
||||
def compute_values(
|
||||
self,
|
||||
batch: Dict[str, Any],
|
||||
embeddings: Optional[Any] = None,
|
||||
) -> TensorType:
|
||||
# Features not provided -> We need to compute them first.
|
||||
if embeddings is None:
|
||||
obs = batch[Columns.OBS]
|
||||
embeddings = self._base_cnn_stack(obs.permute(0, 3, 1, 2))
|
||||
embeddings = torch.squeeze(embeddings, dim=[-1, -2])
|
||||
return self._values(embeddings).squeeze(-1)
|
||||
|
||||
def _compute_embeddings_and_logits(self, batch):
|
||||
obs = batch[Columns.OBS].permute(0, 3, 1, 2)
|
||||
embeddings = self._base_cnn_stack(obs)
|
||||
logits = self._logits(embeddings)
|
||||
return (
|
||||
torch.squeeze(embeddings, dim=[-1, -2]),
|
||||
torch.squeeze(logits, dim=[-1, -2]),
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
import torch
|
||||
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module.torch import TorchRLModule
|
||||
|
||||
|
||||
class VPGTorchRLModule(TorchRLModule):
|
||||
"""A simple VPG (vanilla policy gradient)-style RLModule for testing purposes.
|
||||
|
||||
Use this as a minimum, bare-bones example implementation of a custom TorchRLModule.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
"""Use this method to create all the model components that you require.
|
||||
|
||||
Feel free to access the following useful properties in this class:
|
||||
- `self.model_config`: The config dict for this RLModule class,
|
||||
which should contain flexible settings, for example: {"hiddens": [256, 256]}.
|
||||
- `self.observation|action_space`: The observation and action space that
|
||||
this RLModule is subject to. Note that the observation space might not be the
|
||||
exact space from your env, but that it might have already gone through
|
||||
preprocessing through a connector pipeline (for example, flattening,
|
||||
frame-stacking, mean/std-filtering, etc..).
|
||||
- `self.inference_only`: If True, this model should be built only for inference
|
||||
purposes, in which case you may want to exclude any components that are not used
|
||||
for computing actions, for example a value function branch.
|
||||
"""
|
||||
input_dim = self.observation_space.shape[0]
|
||||
hidden_dim = self.model_config["hidden_dim"]
|
||||
output_dim = self.action_space.n
|
||||
|
||||
self._policy_net = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_dim, hidden_dim),
|
||||
torch.nn.ReLU(),
|
||||
torch.nn.Linear(hidden_dim, output_dim),
|
||||
)
|
||||
|
||||
def _forward(self, batch, **kwargs):
|
||||
# Push the observations from the batch through our `self._policy_net`.
|
||||
action_logits = self._policy_net(batch[Columns.OBS])
|
||||
# Return parameters for the (default) action distribution, which is
|
||||
# `TorchCategorical` (due to our action space being `gym.spaces.Discrete`).
|
||||
return {Columns.ACTION_DIST_INPUTS: action_logits}
|
||||
|
||||
# If you need more granularity between the different forward behaviors during
|
||||
# the different phases of the module's lifecycle, implement three different
|
||||
# forward methods. Thereby, it is recommended to put the inference and
|
||||
# exploration versions inside a `with torch.no_grad()` context for better
|
||||
# performance.
|
||||
# def _forward_train(self, batch):
|
||||
# ...
|
||||
#
|
||||
# def _forward_inference(self, batch):
|
||||
# with torch.no_grad():
|
||||
# return self._forward_train(batch)
|
||||
#
|
||||
# def _forward_exploration(self, batch):
|
||||
# with torch.no_grad():
|
||||
# return self._forward_train(batch)
|
||||
@@ -0,0 +1,254 @@
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Union,
|
||||
)
|
||||
|
||||
import torch
|
||||
|
||||
from ray.rllib.core import Columns
|
||||
from ray.rllib.core.models.base import ENCODER_OUT
|
||||
from ray.rllib.core.rl_module.multi_rl_module import MultiRLModule
|
||||
from ray.rllib.core.rl_module.torch.torch_rl_module import TorchRLModule
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.typing import ModuleID
|
||||
|
||||
SHARED_ENCODER_ID = "shared_encoder"
|
||||
|
||||
|
||||
# __sphinx_doc_policy_begin__
|
||||
class VPGPolicyAfterSharedEncoder(TorchRLModule):
|
||||
"""A VPG (vanilla pol. gradient)-style RLModule using a shared encoder.
|
||||
# __sphinx_doc_policy_end__
|
||||
|
||||
The shared encoder RLModule must be held by the same MultiRLModule, under which
|
||||
this RLModule resides. The shared encoder's forward is called before this
|
||||
RLModule's forward and returns the embeddings under the "encoder_embeddings"
|
||||
key.
|
||||
"""
|
||||
|
||||
# __sphinx_doc_policy_2_begin__
|
||||
|
||||
def setup(self):
|
||||
super().setup()
|
||||
|
||||
# Incoming feature dim from the shared encoder.
|
||||
embedding_dim = self.model_config["embedding_dim"]
|
||||
hidden_dim = self.model_config["hidden_dim"]
|
||||
|
||||
self._pi_head = torch.nn.Sequential(
|
||||
torch.nn.Linear(embedding_dim, hidden_dim),
|
||||
torch.nn.ReLU(),
|
||||
torch.nn.Linear(hidden_dim, self.action_space.n),
|
||||
)
|
||||
|
||||
def _forward(self, batch, **kwargs):
|
||||
embeddings = batch[ENCODER_OUT] # Get the output of the encoder
|
||||
logits = self._pi_head(embeddings)
|
||||
return {Columns.ACTION_DIST_INPUTS: logits}
|
||||
|
||||
|
||||
# __sphinx_doc_policy_2_end__
|
||||
|
||||
|
||||
# __sphinx_doc_mrlm_begin__
|
||||
class VPGMultiRLModuleWithSharedEncoder(MultiRLModule):
|
||||
"""VPG (vanilla pol. gradient)-style MultiRLModule handling a shared encoder.
|
||||
# __sphinx_doc_mrlm_end__
|
||||
|
||||
This MultiRLModule needs to be configured appropriately as below.
|
||||
|
||||
.. testcode::
|
||||
|
||||
# __sphinx_doc_how_to_run_begin__
|
||||
import gymnasium as gym
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.core.rl_module.multi_rl_module import MultiRLModuleSpec
|
||||
|
||||
from ray.rllib.examples.algorithms.classes.vpg import VPGConfig
|
||||
from ray.rllib.examples.learners.classes.vpg_torch_learner_shared_optimizer import VPGTorchLearnerSharedOptimizer
|
||||
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
|
||||
from ray.rllib.examples.rl_modules.classes.vpg_using_shared_encoder_rlm import (
|
||||
SHARED_ENCODER_ID,
|
||||
SharedEncoder,
|
||||
VPGPolicyAfterSharedEncoder,
|
||||
VPGMultiRLModuleWithSharedEncoder,
|
||||
)
|
||||
|
||||
single_agent_env = gym.make("CartPole-v1")
|
||||
|
||||
EMBEDDING_DIM = 64 # encoder output dim
|
||||
|
||||
config = (
|
||||
VPGConfig()
|
||||
.environment(MultiAgentCartPole, env_config={"num_agents": 2})
|
||||
.training(
|
||||
learner_class=VPGTorchLearnerSharedOptimizer,
|
||||
)
|
||||
.multi_agent(
|
||||
# Declare the two policies trained.
|
||||
policies={"p0", "p1"},
|
||||
# Agent IDs of `MultiAgentCartPole` are 0 and 1. They are mapped to
|
||||
# the two policies with ModuleIDs "p0" and "p1", respectively.
|
||||
policy_mapping_fn=lambda agent_id, episode, **kw: f"p{agent_id}"
|
||||
)
|
||||
.rl_module(
|
||||
rl_module_spec=MultiRLModuleSpec(
|
||||
multi_rl_module_class=VPGMultiRLModuleWithSharedEncoder,
|
||||
rl_module_specs={
|
||||
# Shared encoder.
|
||||
SHARED_ENCODER_ID: RLModuleSpec(
|
||||
module_class=SharedEncoder,
|
||||
model_config={"embedding_dim": EMBEDDING_DIM},
|
||||
observation_space=single_agent_env.observation_space,
|
||||
action_space=single_agent_env.action_space,
|
||||
),
|
||||
# Large policy net.
|
||||
"p0": RLModuleSpec(
|
||||
module_class=VPGPolicyAfterSharedEncoder,
|
||||
model_config={
|
||||
"embedding_dim": EMBEDDING_DIM,
|
||||
"hidden_dim": 1024,
|
||||
},
|
||||
),
|
||||
# Small policy net.
|
||||
"p1": RLModuleSpec(
|
||||
module_class=VPGPolicyAfterSharedEncoder,
|
||||
model_config={
|
||||
"embedding_dim": EMBEDDING_DIM,
|
||||
"hidden_dim": 64,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
algo = config.build_algo()
|
||||
print(algo.train())
|
||||
# __sphinx_doc_how_to_run_end__
|
||||
"""
|
||||
|
||||
# __sphinx_doc_mrlm_2_begin__
|
||||
|
||||
def setup(self):
|
||||
# Call the super's setup().
|
||||
super().setup()
|
||||
# Assert, we have the shared encoder submodule.
|
||||
assert SHARED_ENCODER_ID in self._rl_modules and len(self._rl_modules) > 1
|
||||
# Assign the encoder to a convenience attribute.
|
||||
self.encoder = self._rl_modules[SHARED_ENCODER_ID]
|
||||
|
||||
def _forward(self, batch, forward_type, **kwargs):
|
||||
# Collect our policies' outputs in this dict.
|
||||
fwd_out = {}
|
||||
# Loop through the policy nets (through the given batch's keys).
|
||||
for policy_id, policy_batch in batch.items():
|
||||
# Feed this policy's observation into the shared encoder
|
||||
encoder_output = self.encoder._forward(batch[policy_id])
|
||||
policy_batch[ENCODER_OUT] = encoder_output[ENCODER_OUT]
|
||||
# Get the desired module
|
||||
m = getattr(self._rl_modules[policy_id], forward_type)
|
||||
# Pass the policy's embeddings through the policy net.
|
||||
fwd_out[policy_id] = m(batch[policy_id], **kwargs)
|
||||
return fwd_out
|
||||
|
||||
# These methods could probably stand to be adjusted in MultiRLModule using something like this, so that subclasses that tweak _forward don't need to rewrite all of them. The prior implementation errored out because of this issue.
|
||||
@override(MultiRLModule)
|
||||
def _forward_inference(
|
||||
self, batch: Dict[str, Any], **kwargs
|
||||
) -> Union[Dict[str, Any], Dict[ModuleID, Dict[str, Any]]]:
|
||||
return self._forward(batch, "_forward_inference", **kwargs)
|
||||
|
||||
@override(MultiRLModule)
|
||||
def _forward_exploration(
|
||||
self, batch: Dict[str, Any], **kwargs
|
||||
) -> Union[Dict[str, Any], Dict[ModuleID, Dict[str, Any]]]:
|
||||
return self._forward(batch, "_forward_exploration", **kwargs)
|
||||
|
||||
@override(MultiRLModule)
|
||||
def _forward_train(
|
||||
self, batch: Dict[str, Any], **kwargs
|
||||
) -> Union[Dict[str, Any], Dict[ModuleID, Dict[str, Any]]]:
|
||||
return self._forward(batch, "_forward_train", **kwargs)
|
||||
|
||||
|
||||
# __sphinx_doc_mrlm_2_end__
|
||||
|
||||
|
||||
# __sphinx_doc_encoder_begin__
|
||||
class SharedEncoder(TorchRLModule):
|
||||
"""A shared encoder that can be used with `VPGMultiRLModuleWithSharedEncoder`."""
|
||||
|
||||
def setup(self):
|
||||
super().setup()
|
||||
|
||||
input_dim = self.observation_space.shape[0]
|
||||
embedding_dim = self.model_config["embedding_dim"]
|
||||
|
||||
# A very simple encoder network.
|
||||
self._net = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_dim, embedding_dim),
|
||||
)
|
||||
|
||||
def _forward(self, batch, **kwargs):
|
||||
# Pass observations through the net and return outputs.
|
||||
return {ENCODER_OUT: self._net(batch[Columns.OBS])}
|
||||
|
||||
|
||||
# __sphinx_doc_encoder_end__
|
||||
|
||||
|
||||
# __sphinx_doc_ns_encoder_begin__
|
||||
class VPGIndividualEncoder(torch.nn.Module):
|
||||
def __init__(self, observation_space, embedding_dim):
|
||||
"""
|
||||
An individual version of SharedEncoder, supporting direct comparison between
|
||||
the two architectures.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
input_dim = observation_space.shape[0]
|
||||
|
||||
# A very simple encoder network.
|
||||
self._net = torch.nn.Sequential(
|
||||
torch.nn.Linear(input_dim, embedding_dim),
|
||||
)
|
||||
|
||||
def forward(self, batch, **kwargs):
|
||||
# Pass observations through the net and return outputs.
|
||||
return {ENCODER_OUT: self._net(batch[Columns.OBS])}
|
||||
|
||||
|
||||
# __sphinx_doc_ns_encoder_end__
|
||||
|
||||
|
||||
# __sphinx_doc_ns_policy_begin__
|
||||
class VPGPolicyNoSharedEncoder(TorchRLModule):
|
||||
"""
|
||||
A VPG (vanilla pol. gradient)-style RLModule that doesn't use a shared encoder.
|
||||
Facilitates experiments comparing shared and individual encoder architectures.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
super().setup()
|
||||
|
||||
# Incoming feature dim from the encoder.
|
||||
embedding_dim = self.model_config["embedding_dim"]
|
||||
hidden_dim = self.model_config["hidden_dim"]
|
||||
|
||||
self._pi_head = torch.nn.Sequential(
|
||||
torch.nn.Linear(embedding_dim, hidden_dim),
|
||||
torch.nn.ReLU(),
|
||||
torch.nn.Linear(hidden_dim, self.action_space.n),
|
||||
)
|
||||
self.encoder = VPGIndividualEncoder(self.observation_space, embedding_dim)
|
||||
|
||||
def _forward(self, batch, **kwargs):
|
||||
if ENCODER_OUT not in batch:
|
||||
batch = self.encoder(batch)
|
||||
embeddings = batch[ENCODER_OUT]
|
||||
logits = self._pi_head(embeddings)
|
||||
return {Columns.ACTION_DIST_INPUTS: logits}
|
||||
|
||||
|
||||
# __sphinx_doc_ns_policy_end__
|
||||
Reference in New Issue
Block a user