Files
ray-project--ray/rllib/algorithms/marwil/tests/test_marwil.py
T
2026-07-13 13:17:40 +08:00

295 lines
10 KiB
Python

import unittest
from pathlib import Path
import gymnasium as gym
import numpy as np
import ray
import ray.rllib.algorithms.marwil as marwil
from ray.rllib.core import COMPONENT_RL_MODULE, DEFAULT_MODULE_ID
from ray.rllib.core.columns import Columns
from ray.rllib.core.learner.learner import POLICY_LOSS_KEY, VF_LOSS_KEY
from ray.rllib.env import INPUT_ENV_SPACES
from ray.rllib.offline.offline_prelearner import OfflinePreLearner
from ray.rllib.policy.sample_batch import DEFAULT_POLICY_ID
from ray.rllib.utils import unflatten_dict
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.metrics import LEARNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME
from ray.rllib.utils.test_utils import check
torch, _ = try_import_torch()
class TestMARWIL(unittest.TestCase):
@classmethod
def setUpClass(cls):
ray.init()
@classmethod
def tearDownClass(cls):
ray.shutdown()
def test_marwil_compilation_discrete_actions(self):
"""Test whether a MARWILAlgorithm can be built with all frameworks.
Learns from a historic-data file.
To generate this data, first run:
$ ./train.py --run=PPO --env=CartPole-v1 \
--stop='{"timesteps_total": 50000}' \
--config='{"output": "/tmp/out", "batch_mode": "complete_episodes"}'
"""
data_path = "offline/tests/data/cartpole/cartpole-v1_large"
base_path = Path(__file__).parents[3]
print(f"base_path={base_path}")
data_path = "local://" / base_path / data_path
print(f"data_path={data_path}")
config = (
marwil.MARWILConfig()
.environment(env="CartPole-v1")
.api_stack(
enable_rl_module_and_learner=True,
enable_env_runner_and_connector_v2=True,
)
.offline_data(
input_=[data_path.as_posix()],
dataset_num_iters_per_learner=1,
input_read_method_kwargs={"override_num_blocks": 2},
map_batches_kwargs={"concurrency": 2, "num_cpus": 2},
iter_batches_kwargs={"prefetch_batches": 1},
)
.training(
lr=0.0008,
train_batch_size_per_learner=2000,
beta=0.5,
)
.evaluation(
evaluation_interval=3,
evaluation_num_env_runners=1,
evaluation_duration=5,
evaluation_parallel_to_training=True,
)
)
num_iterations = 3
algo = config.build()
for i in range(num_iterations):
print(algo.train())
algo.stop()
def test_marwil_compilation_cont_actions(self):
"""Test whether MARWIL runs with cont. actions.
Learns from a historic-data file.
"""
data_path = "offline/tests/data/pendulum/pendulum-v1_large"
base_path = Path(__file__).parents[3]
print(f"base_path={base_path}")
data_path = "local://" + base_path.joinpath(data_path).as_posix()
print(f"data_path={data_path}")
config = (
marwil.MARWILConfig()
.api_stack(
enable_rl_module_and_learner=True,
enable_env_runner_and_connector_v2=True,
)
.environment(env="Pendulum-v1")
.env_runners(num_env_runners=1)
.training(
train_batch_size_per_learner=2000,
)
.offline_data(
# Learn from offline data.
input_=[data_path],
dataset_num_iters_per_learner=1,
input_read_method_kwargs={"override_num_blocks": 2},
map_batches_kwargs={"concurrency": 2, "num_cpus": 2},
iter_batches_kwargs={"prefetch_batches": 1},
)
# Evaluate on actual environment.
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=3,
evaluation_duration=5,
evaluation_parallel_to_training=True,
)
)
num_iterations = 3
algo = config.build()
for i in range(num_iterations):
print(algo.train())
algo.stop()
def test_marwil_loss_function(self):
"""Test MARWIL's loss function."""
data_path = "offline/tests/data/cartpole/cartpole-v1_large"
base_path = Path(__file__).parents[3]
print(f"base_path={base_path}")
data_path = "local://" + base_path.joinpath(data_path).as_posix()
print(f"data_path={data_path}")
config = (
marwil.MARWILConfig()
.environment(
observation_space=gym.spaces.Box(
np.array([-4.8, -np.inf, -0.41887903, -np.inf]),
np.array([4.8, np.inf, 0.41887903, np.inf]),
(4,),
np.float32,
),
action_space=gym.spaces.Discrete(2),
)
.api_stack(
enable_rl_module_and_learner=True,
enable_env_runner_and_connector_v2=True,
)
.offline_data(
input_=[data_path],
dataset_num_iters_per_learner=1,
)
.training(
train_batch_size_per_learner=2000,
)
) # Learn from offline data.
algo = config.build(env="CartPole-v1")
# Sample a batch from the offline data.
batch = algo.offline_data.data.take_batch(2000)
# Get the module state.
module_state = algo.offline_data.learner_handles[0].get_state(
component=COMPONENT_RL_MODULE,
)[COMPONENT_RL_MODULE]
# Create the prelearner and compute advantages and values.
offline_prelearner = OfflinePreLearner(
config=config,
module_spec=algo.offline_data.module_spec,
module_state=module_state,
spaces=algo.offline_data.spaces[INPUT_ENV_SPACES],
)
# Note, for `ray.data`'s pipeline everything has to be a dictionary
# therefore the batch is embedded into another dictionary.
batch = unflatten_dict(offline_prelearner(batch))
if Columns.LOSS_MASK in batch[DEFAULT_MODULE_ID]:
loss_mask = (
batch[DEFAULT_MODULE_ID][Columns.LOSS_MASK].detach().cpu().numpy()
)
num_valid = np.sum(loss_mask)
def possibly_masked_mean(data_):
return np.sum(data_[loss_mask]) / num_valid
else:
possibly_masked_mean = np.mean
# Calculate our own expected values (to then compare against the
# agent's loss output).
module = algo.learner_group._learner.module[DEFAULT_MODULE_ID].unwrapped()
fwd_out = module.forward_train(dict(batch[DEFAULT_MODULE_ID]))
advantages = (
batch[DEFAULT_MODULE_ID][Columns.VALUE_TARGETS].detach().cpu().numpy()
- module.compute_values(batch[DEFAULT_MODULE_ID]).detach().cpu().numpy()
)
advantages_squared = possibly_masked_mean(np.square(advantages))
c_2 = 100.0 + 1e-8 * (advantages_squared - 100.0)
c = np.sqrt(c_2)
exp_advantages = np.exp(config.beta * (advantages / c))
action_dist_cls = (
algo.learner_group._learner.module[DEFAULT_MODULE_ID]
.unwrapped()
.get_train_action_dist_cls()
)
# Note we need the actual model's logits not the ones from the data set
# stored in `batch[Columns.ACTION_DIST_INPUTS]`.
action_dist = action_dist_cls.from_logits(fwd_out[Columns.ACTION_DIST_INPUTS])
logp = action_dist.logp(batch[DEFAULT_MODULE_ID][Columns.ACTIONS])
logp = logp.detach().cpu().numpy()
# Calculate all expected loss components.
expected_vf_loss = 0.5 * advantages_squared
expected_pol_loss = -1.0 * possibly_masked_mean(exp_advantages * logp)
expected_loss = expected_pol_loss + config.vf_coeff * expected_vf_loss
# Calculate the algorithm's loss (to check against our own
# calculation above).
total_loss = algo.learner_group._learner.compute_loss_for_module(
module_id=DEFAULT_MODULE_ID,
batch=dict(batch[DEFAULT_MODULE_ID]),
fwd_out=fwd_out,
config=config,
)
learner_results = algo.learner_group._learner.metrics.peek(DEFAULT_MODULE_ID)
# Check all components.
check(learner_results[VF_LOSS_KEY], expected_vf_loss, decimals=4)
check(learner_results[POLICY_LOSS_KEY], expected_pol_loss, decimals=4)
# Check the total loss.
check(total_loss, expected_loss, decimals=3)
def test_marwil_lr_schedule(self):
# Define the data paths.
data_path = "offline/tests/data/cartpole/cartpole-v1_large"
base_path = Path(__file__).parents[3]
data_path = "local://" / base_path / data_path
config = (
marwil.MARWILConfig()
.environment(env="CartPole-v1")
.learners(
num_learners=0,
)
.evaluation(
evaluation_interval=3,
evaluation_num_env_runners=1,
evaluation_duration=5,
evaluation_parallel_to_training=True,
)
# Note, the `input_` argument is the major argument for the
# new offline API.
.offline_data(
input_=[data_path.as_posix()],
dataset_num_iters_per_learner=1,
)
.training(
lr=[
[0, 0.001],
[3000, 0.01],
],
train_batch_size_per_learner=2000,
)
)
algo = config.build()
done = False
while not done:
results = algo.train()
ts = results[NUM_ENV_STEPS_SAMPLED_LIFETIME]
assert ts > 0
lr = results[LEARNER_RESULTS][DEFAULT_POLICY_ID][
"default_optimizer_learning_rate"
]
if ts < 3000:
# The learning rate should be linearly interpolated.
expected_lr = 0.001 + (ts / 3000) * (0.01 - 0.001)
self.assertAlmostEqual(lr, expected_lr, places=6)
else:
self.assertEqual(lr, 0.01)
done = True
algo.stop()
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))