193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
import itertools
|
|
import unittest
|
|
|
|
import gymnasium as gym
|
|
import numpy as np
|
|
import tree
|
|
|
|
import ray
|
|
from ray.rllib.algorithms.ppo.ppo_catalog import PPOCatalog
|
|
from ray.rllib.algorithms.ppo.torch.default_ppo_torch_rl_module import (
|
|
DefaultPPOTorchRLModule,
|
|
)
|
|
from ray.rllib.core.columns import Columns
|
|
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
|
|
from ray.rllib.models.preprocessors import get_preprocessor
|
|
from ray.rllib.utils.framework import try_import_torch
|
|
from ray.rllib.utils.numpy import convert_to_numpy
|
|
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
|
|
|
|
torch, nn = try_import_torch()
|
|
|
|
|
|
def dummy_torch_ppo_loss(module, batch, fwd_out):
|
|
adv = batch[Columns.REWARDS] - module.compute_values(batch)
|
|
action_dist_class = module.get_train_action_dist_cls()
|
|
action_probs = action_dist_class.from_logits(
|
|
fwd_out[Columns.ACTION_DIST_INPUTS]
|
|
).logp(batch[Columns.ACTIONS])
|
|
actor_loss = -(action_probs * adv).mean()
|
|
critic_loss = (adv**2).mean()
|
|
loss = actor_loss + critic_loss
|
|
|
|
return loss
|
|
|
|
|
|
def _get_input_batch_from_obs(obs, lstm):
|
|
batch = {
|
|
Columns.OBS: convert_to_torch_tensor(obs)[None],
|
|
}
|
|
if lstm:
|
|
batch[Columns.OBS] = batch[Columns.OBS][None]
|
|
return batch
|
|
|
|
|
|
class TestPPO(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
ray.init()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
ray.shutdown()
|
|
|
|
def test_rollouts(self):
|
|
# TODO: Add FrozenLake-v1 to cover LSTM case.
|
|
env_names = ["CartPole-v1", "Pendulum-v1", "ale_py:ALE/Breakout-v5"]
|
|
fwd_fns = ["forward_exploration", "forward_inference"]
|
|
lstm = [True, False]
|
|
config_combinations = [env_names, fwd_fns, lstm]
|
|
for config in itertools.product(*config_combinations):
|
|
env_name, fwd_fn, lstm = config
|
|
print(f"ENV={env_name}; FWD={fwd_fn}; LSTM={lstm}")
|
|
env = gym.make(env_name)
|
|
|
|
preprocessor_cls = get_preprocessor(env.observation_space)
|
|
preprocessor = preprocessor_cls(env.observation_space)
|
|
|
|
module = DefaultPPOTorchRLModule(
|
|
observation_space=preprocessor.observation_space,
|
|
action_space=env.action_space,
|
|
model_config=DefaultModelConfig(use_lstm=lstm),
|
|
catalog_class=PPOCatalog,
|
|
)
|
|
|
|
obs, _ = env.reset()
|
|
obs = preprocessor.transform(obs)
|
|
|
|
batch = _get_input_batch_from_obs(obs, lstm)
|
|
|
|
if lstm:
|
|
state_in = module.get_initial_state()
|
|
state_in = convert_to_torch_tensor(state_in)
|
|
state_in = tree.map_structure(lambda x: x[None], state_in)
|
|
batch[Columns.STATE_IN] = state_in
|
|
|
|
if fwd_fn == "forward_exploration":
|
|
module.forward_exploration(batch)
|
|
else:
|
|
module.forward_inference(batch)
|
|
|
|
def test_forward_train(self):
|
|
# TODO: Add FrozenLake-v1 to cover LSTM case.
|
|
env_names = ["CartPole-v1", "Pendulum-v1", "ale_py:ALE/Breakout-v5"]
|
|
lstm = [False, True]
|
|
config_combinations = [env_names, lstm]
|
|
for config in itertools.product(*config_combinations):
|
|
env_name, lstm = config
|
|
print(f"ENV={env_name}; LSTM={lstm}")
|
|
env = gym.make(env_name)
|
|
|
|
preprocessor_cls = get_preprocessor(env.observation_space)
|
|
preprocessor = preprocessor_cls(env.observation_space)
|
|
|
|
module = DefaultPPOTorchRLModule(
|
|
observation_space=preprocessor.observation_space,
|
|
action_space=env.action_space,
|
|
model_config=DefaultModelConfig(use_lstm=lstm),
|
|
catalog_class=PPOCatalog,
|
|
)
|
|
|
|
# collect a batch of data
|
|
batches = []
|
|
obs, _ = env.reset()
|
|
obs = preprocessor.transform(obs)
|
|
tstep = 0
|
|
|
|
if lstm:
|
|
state_in = module.get_initial_state()
|
|
state_in = tree.map_structure(
|
|
lambda x: x[None], convert_to_torch_tensor(state_in)
|
|
)
|
|
initial_state = state_in
|
|
|
|
while tstep < 10:
|
|
input_batch = _get_input_batch_from_obs(obs, lstm=lstm)
|
|
if lstm:
|
|
input_batch[Columns.STATE_IN] = state_in
|
|
|
|
fwd_out = module.forward_exploration(input_batch)
|
|
action_dist_cls = module.get_exploration_action_dist_cls()
|
|
action_dist = action_dist_cls.from_logits(
|
|
fwd_out[Columns.ACTION_DIST_INPUTS]
|
|
)
|
|
_action = action_dist.sample()
|
|
action = convert_to_numpy(_action[0])
|
|
action_logp = convert_to_numpy(action_dist.logp(_action)[0])
|
|
if lstm:
|
|
# Since this is inference, fwd out should only contain one action
|
|
assert len(action) == 1
|
|
action = action[0]
|
|
new_obs, reward, terminated, truncated, _ = env.step(action)
|
|
new_obs = preprocessor.transform(new_obs)
|
|
output_batch = {
|
|
Columns.OBS: obs,
|
|
Columns.NEXT_OBS: new_obs,
|
|
Columns.ACTIONS: action,
|
|
Columns.ACTION_LOGP: action_logp,
|
|
Columns.REWARDS: np.array(reward),
|
|
Columns.TERMINATEDS: np.array(terminated),
|
|
Columns.TRUNCATEDS: np.array(truncated),
|
|
Columns.STATE_IN: None,
|
|
}
|
|
|
|
if lstm:
|
|
assert Columns.STATE_OUT in fwd_out
|
|
state_in = fwd_out[Columns.STATE_OUT]
|
|
batches.append(output_batch)
|
|
obs = new_obs
|
|
tstep += 1
|
|
|
|
# convert the list of dicts to dict of lists
|
|
batch = tree.map_structure(lambda *x: np.array(x), *batches)
|
|
# convert dict of lists to dict of tensors
|
|
fwd_in = {k: convert_to_torch_tensor(np.array(v)) for k, v in batch.items()}
|
|
if lstm:
|
|
fwd_in[Columns.STATE_IN] = initial_state
|
|
# If we test lstm, the collected timesteps make up only one batch
|
|
fwd_in = {
|
|
k: torch.unsqueeze(v, 0) if k != Columns.STATE_IN else v
|
|
for k, v in fwd_in.items()
|
|
}
|
|
|
|
# forward train
|
|
# before training make sure module is on the right device
|
|
# and in training mode
|
|
module.to("cpu")
|
|
module.train()
|
|
fwd_out = module.forward_train(fwd_in)
|
|
loss = dummy_torch_ppo_loss(module, fwd_in, fwd_out)
|
|
loss.backward()
|
|
|
|
# check that all neural net parameters have gradients
|
|
for param in module.parameters():
|
|
self.assertIsNotNone(param.grad)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|