583 lines
27 KiB
ReStructuredText
583 lines
27 KiB
ReStructuredText
.. _env-to-module-pipeline-docs:
|
|
|
|
.. grid:: 1 2 3 4
|
|
:gutter: 1
|
|
:class-container: container pb-3
|
|
|
|
.. grid-item-card::
|
|
:img-top: /rllib/images/connector_v2/connector_generic.svg
|
|
:class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img
|
|
|
|
.. button-ref:: connector-v2-docs
|
|
|
|
ConnectorV2 overview
|
|
|
|
.. grid-item-card::
|
|
:img-top: /rllib/images/connector_v2/env_to_module_connector.svg
|
|
:class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img
|
|
|
|
.. button-ref:: env-to-module-pipeline-docs
|
|
|
|
Env-to-module pipelines (this page)
|
|
|
|
.. grid-item-card::
|
|
:img-top: /rllib/images/connector_v2/learner_connector.svg
|
|
:class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img
|
|
|
|
.. button-ref:: learner-pipeline-docs
|
|
|
|
Learner pipelines
|
|
|
|
Env-to-module pipelines
|
|
=======================
|
|
|
|
.. include:: /_includes/rllib/new_api_stack.rst
|
|
|
|
On each :py:class:`~ray.rllib.env.env_runner.EnvRunner` resides one env-to-module pipeline
|
|
responsible for handling the data flow from the `gymnasium.Env <https://gymnasium.farama.org/api/env/>`__ to the :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule`.
|
|
|
|
.. figure:: images/connector_v2/env_runner_connector_pipelines.svg
|
|
:width: 1000
|
|
:align: left
|
|
|
|
**EnvRunner ConnectorV2 Pipelines**: Both env-to-module and module-to-env pipelines are located on the :py:class:`~ray.rllib.env.env_runner.EnvRunner`
|
|
workers. The env-to-module pipeline sits between the RL environment, a `gymnasium.Env <https://gymnasium.farama.org/api/env/>`__, and the
|
|
:py:class:`~ray.rllib.core.rl_module.rl_module.RLModule`, and translates ongoing episodes into batches for the model's `forward_...()` methods.
|
|
|
|
.. The module-to-env pipeline serves the other direction, converting the output of the :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule`, such as action logits and action distribution parameters, to actual actions understandable by the `gymnasium.Env <https://gymnasium.farama.org/api/env/>`__ and used in the env's next `step()` call.
|
|
|
|
The env-to-module pipeline, when called, performs transformations from a list of ongoing :ref:`Episode objects <single-agent-episode-docs>` to an
|
|
``RLModule``-readable tensor batch and RLlib passes this generated batch as the first argument into the
|
|
:py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.forward_inference` or :py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.forward_exploration`
|
|
methods of the :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule`, depending on your exploration settings.
|
|
|
|
.. hint::
|
|
|
|
Set `config.exploration(explore=True)` in your :py:class:`~ray.rllib.algorithms.algorithm_config.AlgorithmConfig` to have RLlib call the
|
|
:py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.forward_exploration` method with the connector's output.
|
|
Otherwise, RLlib calls :py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.forward_inference`.
|
|
Note also that normally these two methods only differ in that actions are sampled when ``explore=True`` and
|
|
greedily picked when ``explore=False``. However, the exact behavior in each case depends on your :ref:`RLModule's implementation <rlmodule-guide>`.
|
|
|
|
|
|
.. _default-env-to-module-pipeline:
|
|
|
|
Default env-to-module behavior
|
|
------------------------------
|
|
|
|
By default RLlib populates every env-to-module pipeline with the following built-in connector pieces.
|
|
|
|
* :py:class:`~ray.rllib.connectors.common.add_observations_from_episodes_to_batch.AddObservationsFromEpisodesToBatch`: Places the most recent observation from each ongoing episode into the batch. The column name is ``obs``. Note that if you have a vector of ``N`` environments per :py:class:`~ray.rllib.env.env_runner.EnvRunner`, your batch size is also ``N``.
|
|
* *Relevant for stateful models only:* :py:class:`~ray.rllib.connectors.common.add_time_dim_to_batch_and_zero_pad.AddTimeDimToBatchAndZeroPad`: If the :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` is stateful, adds a single timestep, second axis to all data to make it sequential.
|
|
* *Relevant for stateful models only:* :py:class:`~ray.rllib.connectors.common.add_states_from_episodes_to_batch.AddStatesFromEpisodesToBatch`: If the :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` is stateful, places the most recent state outputs of the module as new state inputs into the batch. The column name is ``state_in`` and the values don't have a time-dimension.
|
|
* *For multi-agent only:* :py:class:`~ray.rllib.connectors.common.agent_to_module_mapping.AgentToModuleMapping`: Maps per-agent data to the respective per-module data depending on your defined agent-to-module mapping function.
|
|
* :py:class:`~ray.rllib.connectors.common.batch_individual_items.BatchIndividualItems`: Converts all data in the batch, which thus far are lists of individual items, into batched structures meaning NumPy arrays, whose 0th axis is the batch axis.
|
|
* :py:class:`~ray.rllib.connectors.common.numpy_to_tensor.NumpyToTensor`: Converts all NumPy arrays in the batch into framework specific tensors and moves these to the GPU, if required.
|
|
|
|
You can disable all the preceding default connector pieces by setting `config.env_runners(add_default_connectors_to_env_to_module_pipeline=False)`
|
|
in your :ref:`algorithm config <rllib-algo-configuration-docs>`.
|
|
|
|
Note that the order of these transforms is very relevant for the functionality of the pipeline.
|
|
See :ref:`here on how to write and add your own connector pieces <writing_custom_env_to_module_connectors>` to the pipeline.
|
|
|
|
|
|
Constructing an env-to-module connector
|
|
---------------------------------------
|
|
|
|
Normally, you wouldn't have to construct the env-to-module connector pipeline yourself. RLlib's :py:class:`~ray.rllib.env.env_runner.EnvRunner`
|
|
actors initially perform this operation. However, if you would like to test or debug either the default pipeline or a custom one,
|
|
use the following code snippet as a starting point:
|
|
|
|
.. testcode::
|
|
|
|
import gymnasium as gym
|
|
|
|
from ray.rllib.algorithms.ppo import PPOConfig
|
|
from ray.rllib.env.single_agent_episode import SingleAgentEpisode
|
|
|
|
# Start with an algorithm config.
|
|
config = (
|
|
PPOConfig()
|
|
.environment("CartPole-v1")
|
|
)
|
|
# Create an env to generate some episode data.
|
|
env = gym.make("CartPole-v1")
|
|
|
|
# Build the env-to-module connector through the config object.
|
|
env_to_module = config.build_env_to_module_connector(env=env, spaces=None)
|
|
|
|
|
|
Alternatively, in case there is no ``env`` object available, you should pass in the ``spaces`` argument instead.
|
|
RLlib requires either of these pieces of information to compute the correct output observation space of the pipeline, so that the
|
|
:py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` can receive the correct input space for its own setup procedure.
|
|
The structure of the `spaces` argument should ideally be:
|
|
|
|
.. code-block:: python
|
|
|
|
spaces = {
|
|
"__env__": ([env observation space], [env action space]), # <- may be vectorized
|
|
"__env_single__": ([env observation space], [env action space]), # <- never vectorized!
|
|
"[module ID, e.g. 'default_policy']": ([module observation space], [module action space]),
|
|
... # <- more modules in multi-agent case
|
|
}
|
|
|
|
However, for single-agent cases, it may be enough to provide the non-vectorized, single observation-
|
|
and action spaces only:
|
|
|
|
.. testcode::
|
|
|
|
# No `env` available? Use `spaces` instead:
|
|
env_to_module = config.build_env_to_module_connector(
|
|
env=None,
|
|
spaces={
|
|
# At minimum, pass in a 2-tuple of the single, non-vectorized
|
|
# observation- and action spaces:
|
|
"__env_single__": (env.observation_space, env.action_space),
|
|
},
|
|
)
|
|
|
|
To test the actual behavior or the created pipeline, look at these code snippets
|
|
for stateless- and stateful :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` cases:
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: Stateless RLModule
|
|
|
|
.. testcode::
|
|
|
|
from ray.rllib.env.single_agent_episode import SingleAgentEpisode
|
|
|
|
# Create two SingleAgentEpisode instances. You pass these to the connector pipeline
|
|
# as input.
|
|
episode1 = SingleAgentEpisode()
|
|
episode2 = SingleAgentEpisode()
|
|
|
|
# Fill episodes with some data, as if we were currently stepping through them
|
|
# to collect samples.
|
|
# - episode 1 (two timesteps)
|
|
obs, _ = env.reset()
|
|
episode1.add_env_reset(observation=obs)
|
|
action = 0
|
|
obs, _, _, _, _ = env.step(action)
|
|
episode1.add_env_step(observation=obs, action=action, reward=1.0)
|
|
# - episode 2 (just one timestep)
|
|
obs, _ = env.reset()
|
|
episode2.add_env_reset(observation=obs)
|
|
|
|
# Call the connector on the two running episodes.
|
|
batch = {}
|
|
batch = env_to_module(
|
|
episodes=[episode1, episode2],
|
|
batch=batch,
|
|
rl_module=None, # in stateless case, RLModule is not strictly required
|
|
explore=True,
|
|
)
|
|
# Print out the resulting batch.
|
|
print(batch)
|
|
|
|
|
|
.. tab-item:: Stateful RLModule (RNN)
|
|
|
|
.. testcode::
|
|
|
|
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
|
|
from ray.rllib.env.single_agent_episode import SingleAgentEpisode
|
|
|
|
# Alter the config to use the default LSTM model of RLlib.
|
|
config.rl_module(model_config=DefaultModelConfig(use_lstm=True))
|
|
|
|
# For stateful RLModules, we do need to pass in the RLModule to every call to the
|
|
# connector. so construct an instance here.
|
|
rl_module_spec = config.get_rl_module_spec(env=env)
|
|
rl_module = rl_module_spec.build()
|
|
|
|
# Create a SingleAgentEpisode instance. You pass this to the connector pipeline
|
|
# as input.
|
|
episode = SingleAgentEpisode()
|
|
|
|
# Initialize episode with first (reset) observation.
|
|
obs, _ = env.reset()
|
|
episode.add_env_reset(observation=obs)
|
|
|
|
# Call the connector on the running episode.
|
|
batch = {}
|
|
batch = env_to_module(
|
|
episodes=[episode],
|
|
batch=batch,
|
|
rl_module=rl_module, # in stateful case, RLModule is required
|
|
explore=True,
|
|
)
|
|
# Print out the resulting batch.
|
|
print(batch)
|
|
|
|
|
|
|
|
You can see that the pipeline extracted the current observations from the two
|
|
running episodes and placed them under the ``obs`` column into the forward batch.
|
|
The batch has a size of two, because we had two episodes, and should look similar to this:
|
|
|
|
.. code-block:: text
|
|
|
|
{'obs': tensor([[ 0.0212, -0.1996, -0.0414, 0.2848],
|
|
[ 0.0292, 0.0259, -0.0322, -0.0004]])}
|
|
|
|
In the stateful case, you can also expect the ``STATE_IN`` columns to be present.
|
|
Note that because of the LSTM layer, the internal state of the module consists of two components, ``c`` and ``h``:
|
|
|
|
.. code-block:: text
|
|
|
|
{
|
|
'obs': tensor(
|
|
[[ 0.0212, -0.1996, -0.0414, 0.2848],
|
|
[ 0.0292, 0.0259, -0.0322, -0.0004]]
|
|
),
|
|
'state_in': {
|
|
# Note: The shape of each state tensor here is
|
|
# (B=2, [num LSTM-layers=1], [LSTM cell size]).
|
|
'h': tensor([[[0., 0., .., 0.]]]),
|
|
'c': tensor([[[0., 0., ... 0.]]]),
|
|
},
|
|
}
|
|
|
|
.. hint::
|
|
|
|
You are free to design the internal states of your custom :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` classes
|
|
however you like. You only need to override the :py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.get_initial_state` method and make sure
|
|
you return a new state of any nested structure and shape from your `forward_..()` methods under the fixed ``state_out`` key.
|
|
See `here for an example <https://github.com/ray-project/ray/blob/master/rllib/examples/rl_modules/classes/lstm_containing_rlm.py>`__
|
|
of an RLModule class with a custom LSTM layer in it.
|
|
|
|
|
|
.. _writing_custom_env_to_module_connectors:
|
|
|
|
Writing custom env-to-module connectors
|
|
---------------------------------------
|
|
|
|
You can customize the default env-to-module pipeline that RLlib creates through specifying a function in your
|
|
:py:class:`~ray.rllib.algorithms.algorithm_config.AlgorithmConfig`, which takes an optional RL environment object (`env`) and an optional `spaces`
|
|
dictionary as input arguments and returns a single :py:class:`~ray.rllib.connectors.connector_v2.ConnectorV2` piece or a list thereof.
|
|
RLlib prepends these :py:class:`~ray.rllib.connectors.connector_v2.ConnectorV2` instances to the
|
|
:ref:`default env-to-module pipeline <default-env-to-module-pipeline>` in the order returned,
|
|
unless you set `add_default_connectors_to_env_to_module_pipeline=False` in your config, in which case RLlib exclusively uses the provided
|
|
:py:class:`~ray.rllib.connectors.connector_v2.ConnectorV2` pieces without any automatically added default behavior.
|
|
|
|
For example, to prepend a custom ConnectorV2 piece to the env-to-module pipeline, you can do this in your config:
|
|
|
|
.. testcode::
|
|
:skipif: True
|
|
|
|
# Your builder function must accept an optional `gymnasium.Env` and an optional `spaces` dict
|
|
# as arguments.
|
|
config.env_runners(
|
|
env_to_module_connector=lambda env, spaces, device: MyEnvToModuleConnector(..),
|
|
)
|
|
|
|
|
|
If you want to add multiple custom pieces to the pipeline, return them as a list:
|
|
|
|
.. testcode::
|
|
:skipif: True
|
|
|
|
# Return a list of connector pieces to make RLlib add all of them to your
|
|
# env-to-module pipeline.
|
|
config.env_runners(
|
|
env_to_module_connector=lambda env, spaces, device: [
|
|
MyEnvToModuleConnector(..),
|
|
MyOtherEnvToModuleConnector(..),
|
|
AndOneMoreConnector(..),
|
|
],
|
|
)
|
|
|
|
RLlib adds the connector pieces returned by your function to the beginning of the env-to-module pipeline,
|
|
before the previously described default connector pieces that RLlib provides automatically:
|
|
|
|
|
|
.. figure:: images/connector_v2/custom_pieces_in_env_to_module_pipeline.svg
|
|
:width: 1000
|
|
:align: left
|
|
|
|
**Inserting custom ConnectorV2 pieces into the env-to-module pipeline**: RLlib inserts custom connector pieces, such
|
|
as observation preprocessors, before the default pieces. This way, if your custom connectors alter the input episodes
|
|
in any way, for example by changing the observations as in an :ref:`ObservationPreprocessor <observation-preprocessors>`,
|
|
the tailing default pieces automatically add these changed observations to the batch.
|
|
|
|
|
|
.. _observation-preprocessors:
|
|
|
|
Observation preprocessors
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The simplest way of customizing an env-to-module pipeline is to write your own
|
|
:py:class:`~ray.rllib.connectors.env_to_module.observation_preprocessor.SingleAgentObservationPreprocessor` subclass, implement two methods,
|
|
and point your config to the new class:
|
|
|
|
.. testcode::
|
|
|
|
import gymnasium as gym
|
|
import numpy as np
|
|
|
|
from ray.rllib.connectors.env_to_module.observation_preprocessor import SingleAgentObservationPreprocessor
|
|
|
|
|
|
class IntObservationToOneHotTensor(SingleAgentObservationPreprocessor):
|
|
"""Converts int observations (Discrete) into one-hot tensors (Box)."""
|
|
|
|
def recompute_output_observation_space(self, in_obs_space, in_act_space):
|
|
# Based on the input observation space, either from the preceding connector piece or
|
|
# directly from the environment, return the output observation space of this connector
|
|
# piece.
|
|
# Implementing this method is crucial for the pipeline to know its output
|
|
# spaces, which are an important piece of information to construct the succeeding
|
|
# RLModule.
|
|
return gym.spaces.Box(0.0, 1.0, (in_obs_space.n,), np.float32)
|
|
|
|
def preprocess(self, observation, episode):
|
|
# Convert an input observation (int) into a one-hot (float) tensor.
|
|
# Note that 99% of all connectors in RLlib operate on NumPy arrays.
|
|
new_obs = np.zeros(shape=self.observation_space.shape, dtype=np.float32)
|
|
new_obs[observation] = 1.0
|
|
return new_obs
|
|
|
|
|
|
Note that any observation preprocessor actually changes the underlying episodes object in place, but and doesn't contribute anything to
|
|
the batch under construction. Because RLlib always inserts any user defined preprocessor (and other custom
|
|
:py:class:`~ray.rllib.connectors.connector_v2.ConnectorV2`
|
|
pieces) before the default pieces, the :py:class:`~ray.rllib.connectors.common.add_observations_from_episodes_to_batch.AddObservationsFromEpisodesToBatch`
|
|
default piece then automatically takes care of adding the preprocessed and updated observation from the episode to the batch:
|
|
|
|
Now you can use the custom preprocessor in environments with integer observations, for example the
|
|
`FrozenLake <https://gymnasium.farama.org/environments/toy_text/frozen_lake/>`__ RL environment:
|
|
|
|
|
|
.. testcode::
|
|
|
|
from ray.rllib.algorithms.ppo import PPOConfig
|
|
|
|
config = (
|
|
PPOConfig()
|
|
|
|
# Configure a simple 2x2 grid-world.
|
|
# ____
|
|
# |S | <- S=start position
|
|
# | G| <- G=goal position
|
|
# ----
|
|
.environment("FrozenLake-v1", env_config={"desc": ["SF", "FG"]})
|
|
|
|
# Plug your custom connector piece into the env-to-module pipeline.
|
|
.env_runners(
|
|
env_to_module_connector=(
|
|
lambda env, spaces, device: IntObservationToOneHotTensor()
|
|
),
|
|
)
|
|
)
|
|
algo = config.build()
|
|
# Train one iteration.
|
|
print(algo.train())
|
|
|
|
|
|
.. _observation-preprocessors-adding-rewards-to-obs:
|
|
|
|
Example: Adding recent rewards to the batch
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Assume you wrote a custom :ref:`RLModule <rlmodule-guide>` that requires the last three received
|
|
rewards as input in the calls to any of its `forward_..()` methods.
|
|
|
|
You can use the same :py:class:`~ray.rllib.connectors.env_to_module.observation_preprocessor.SingleAgentObservationPreprocessor`
|
|
API to achieve this.
|
|
|
|
In the following example, you extract the last three rewards from the ongoing episode and concatenate
|
|
them with the observation to form a new observation tensor.
|
|
Note that you also have to change the observation space returned by the connector, since
|
|
there are now three more values in each observation:
|
|
|
|
|
|
.. testcode::
|
|
|
|
import gymnasium as gym
|
|
import numpy as np
|
|
|
|
from ray.rllib.connectors.env_to_module.observation_preprocessor import SingleAgentObservationPreprocessor
|
|
|
|
|
|
class AddPastThreeRewards(SingleAgentObservationPreprocessor):
|
|
"""Extracts last three rewards from episode and concatenates them to the observation tensor."""
|
|
|
|
def recompute_output_observation_space(self, in_obs_space, in_act_space):
|
|
# Based on the input observation space (), return the output observation
|
|
# space. Implementing this method is crucial for the pipeline to know its output
|
|
# spaces, which are an important piece of information to construct the succeeding
|
|
# RLModule.
|
|
|
|
assert isinstance(in_obs_space, gym.spaces.Box) and len(in_obs_space.shape) == 1
|
|
return gym.spaces.Box(-100.0, 100.0, (in_obs_space.shape[0] + 3,), np.float32)
|
|
|
|
def preprocess(self, observation, episode):
|
|
# Extract the last 3 rewards from the ongoing episode using a python `slice` object.
|
|
# Alternatively, you can also pass in a list of indices, [-3, -2, -1].
|
|
past_3_rewards = episode.get_rewards(indices=slice(-3, None))
|
|
|
|
# Concatenate the rewards to the actual observation.
|
|
new_observation = np.concatenate([
|
|
observation, np.array(past_3_rewards, np.float32)
|
|
])
|
|
|
|
# Return the new observation.
|
|
return new_observation
|
|
|
|
|
|
.. note::
|
|
|
|
Note that the preceding example should work without any further action required on your model,
|
|
whether it's a custom one or a default one provided by RLlib, as long as the model determines its input layer's
|
|
size through its own ``self.observation_space`` attribute. The connector pipeline correctly captures the observation
|
|
space changes, from the environment's 1D-Box to the reward-enhanced, larger 1D-Box and
|
|
passes this new observation space to your RLModule's :py:meth:`~ray.rllib.core.rl_module.rl_module.RLModule.setup`
|
|
method.
|
|
|
|
|
|
Example: Preprocessing observations in multi-agent setups
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
In multi-agent setups, you have two options for preprocessing your agents' individual observations
|
|
through customizing your env-to-module pipeline:
|
|
|
|
1) Agent-by-agent: Using the same API as in the previous examples,
|
|
:py:class:`~ray.rllib.connectors.env_to_module.observation_preprocessor.SingleAgentObservationPreprocessor`,
|
|
you can apply a single preprocessing logic across all agents. However, in case you need one distinct preprocessing
|
|
logic per ``AgentID``, lookup the agent information from the provided ``episode`` argument in the
|
|
:py:meth:`~ray.rllib.connectors.env_to_module.observation_preprocessor.SingleAgentObservationPreprocessor.preprocess` method:
|
|
|
|
.. testcode::
|
|
:skipif: True
|
|
|
|
def recompute_output_observation_space(self, in_obs_space, in_act_space):
|
|
# `in_obs_space` is a `Dict` space, mapping agent IDs to individual agents' spaces.
|
|
# Alter this dict according to which agents you want to preprocess observations for
|
|
# and return the new `Dict` space.
|
|
|
|
# For example:
|
|
return gym.spaces.Dict({
|
|
"some_agent_id": [obs space],
|
|
"other_agent_id": [another obs space],
|
|
...
|
|
})
|
|
|
|
def preprocess(self, observation, episode):
|
|
|
|
# Skip preprocessing for certain agent ID(s).
|
|
if episode.agent_id != "some_agent_id":
|
|
return observation
|
|
|
|
# Preprocess other agents' observations.
|
|
...
|
|
|
|
1) Multi-agent preprocessor with access to the entire multi-agent observation dict: Alternatively, you can subclass the
|
|
:py:class:`~ray.rllib.connectors.env_to_module.observation_preprocessor.MultiAgentObservationPreprocessor` API and
|
|
override the same two methods, ``recompute_output_observation_space`` and ``preprocess``.
|
|
|
|
See here for a `2-agent observation preprocessor example <https://github.com/ray-project/ray/blob/master/rllib/examples/connectors/multi_agent_observation_preprocessor.py>`__
|
|
showing how to enhance each agents' observations through adding information from the respective other agent to the observations.
|
|
|
|
Use :py:class:`~ray.rllib.connectors.env_to_module.observation_preprocessor.MultiAgentObservationPreprocessor` whenever you need to
|
|
preprocess observations of an agent by lookup information from other agents, for example their own observations, but also rewards and
|
|
previous actions.
|
|
|
|
|
|
Example: Adding new columns to the batch
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
So far, you have altered the observations in the input episodes, either by
|
|
:ref:`manipulating them directly <observation-preprocessors>` or
|
|
:ref:`adding additional information like rewards to them <observation-preprocessors-adding-rewards-to-obs>`.
|
|
|
|
RLlib's default env-to-module connectors add the observations found in the episodes to the batch under the ``obs`` column.
|
|
If you would like to create a new column in the batch, you can subclass :py:class:`~ray.rllib.connectors.connector_v2.ConnectorV2` directly
|
|
and implement its :py:meth:`~ray.rllib.connectors.connector_v2.ConnectorV2.__call__` method. This way, if you have an
|
|
:py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` that requires certain custom columns to be present in the input batch,
|
|
write a custom connector piece following this example here:
|
|
|
|
.. testcode::
|
|
|
|
import numpy as np
|
|
from ray.rllib.connectors.connector_v2 import ConnectorV2
|
|
|
|
class AddNewColumnToBatch(ConnectorV2):
|
|
|
|
def __init__(
|
|
self,
|
|
input_observation_space=None,
|
|
input_action_space=None,
|
|
*,
|
|
col_name: str = "last_3_rewards_mean",
|
|
):
|
|
super().__init__(input_observation_space, input_action_space)
|
|
|
|
self.col_name = col_name
|
|
|
|
def __call__(self, *, episodes, batch, rl_module, explore, shared_data, **kwargs):
|
|
|
|
# Use the convenience `single_agent_episode_iterator` to loop through given episodes.
|
|
# Even if `episodes` are a list of MultiAgentEpisodes, RLlib splits them up into
|
|
# their single-agent subcomponents.
|
|
|
|
for sa_episode in self.single_agent_episode_iterator(episodes):
|
|
|
|
# Compute some example new-data item for your `batch` (to be added
|
|
# under a new column).
|
|
# Here, we compile the average over the last 3 rewards.
|
|
last_3_rewards = sa_episode.get_rewards(
|
|
indices=[-3, -2, -1],
|
|
fill=0.0, # at beginning of episode, fill with 0s
|
|
)
|
|
new_data_item = np.mean(last_3_rewards)
|
|
# Use the convenience utility: `add_item_to_batch` to add a new value to
|
|
# a new or existing column.
|
|
self.add_batch_item(
|
|
batch=batch,
|
|
column=self.col_name,
|
|
item_to_add=new_data_item,
|
|
single_agent_episode=sa_episode,
|
|
)
|
|
|
|
# Return the altered batch (with the new column in it).
|
|
return batch
|
|
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
config = (
|
|
PPOConfig()
|
|
.environment("CartPole-v1")
|
|
.env_runners(
|
|
env_to_module_connector=lambda env, spaces, device: AddNewColumnToBatch()
|
|
)
|
|
)
|
|
env = gym.make("CartPole-v1")
|
|
env_to_module = config.build_env_to_module_connector(env=env, spaces=None)
|
|
episode = SingleAgentEpisode()
|
|
obs, _ = env.reset()
|
|
episode.add_env_reset(observation=obs)
|
|
action = 0
|
|
obs, _, _, _, _ = env.step(action)
|
|
episode.add_env_step(observation=obs, action=action, reward=1.0)
|
|
batch = {}
|
|
batch = env_to_module(
|
|
episodes=[episode],
|
|
batch=batch,
|
|
rl_module=None, # in stateless case, RLModule is not strictly required
|
|
explore=True,
|
|
)
|
|
# Print out the resulting batch.
|
|
print(batch)
|
|
|
|
|
|
You should see the new column in the batch, after running through this connector piece.
|
|
|
|
Note, though, that if your :py:class:`~ray.rllib.core.rl_module.rl_module.RLModule` also requires the new information
|
|
in the train batch, you would also need to add the same custom connector piece to your Algorithm's
|
|
:py:class:`~ray.rllib.connectors.learner.learner_connector_pipeline.LearnerConnectorPipeline`.
|
|
|
|
See :ref:`the Learner connector pipeline documentation <learner-pipeline-docs>` for more details on how to customize it.
|