chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Conservative Q-Learning (CQL)
|
||||
|
||||
## Overview
|
||||
|
||||
[CQL](https://arxiv.org/abs/2006.04779) is an offline RL algorithm that mitigates the overestimation of Q-values outside the dataset distribution via convservative critic estimates. CQL does this by adding a simple Q regularizer loss to the standard Belman update loss. This ensures that the critic does not output overly-optimistic Q-values and can be added on top of any off-policy Q-learning algorithm (in this case, we use SAC).
|
||||
|
||||
## Documentation & Implementation:
|
||||
|
||||
Conservative Q-Learning (CQL).
|
||||
|
||||
**[Detailed Documentation](https://docs.ray.io/en/master/rllib-algorithms.html#cql)**
|
||||
|
||||
**[Implementation](https://github.com/ray-project/ray/blob/master/rllib/algorithms/cql/cql.py)**
|
||||
@@ -0,0 +1,9 @@
|
||||
from ray.rllib.algorithms.cql.cql import CQL, CQLConfig
|
||||
from ray.rllib.algorithms.cql.cql_torch_policy import CQLTorchPolicy
|
||||
|
||||
__all__ = [
|
||||
"CQL",
|
||||
"CQLConfig",
|
||||
# @OldAPIStack
|
||||
"CQLTorchPolicy",
|
||||
]
|
||||
@@ -0,0 +1,378 @@
|
||||
import logging
|
||||
from typing import Optional, Type, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from ray._common.deprecation import (
|
||||
DEPRECATED_VALUE,
|
||||
deprecation_warning,
|
||||
)
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided
|
||||
from ray.rllib.algorithms.cql.cql_tf_policy import CQLTFPolicy
|
||||
from ray.rllib.algorithms.cql.cql_torch_policy import CQLTorchPolicy
|
||||
from ray.rllib.algorithms.sac.sac import (
|
||||
SAC,
|
||||
SACConfig,
|
||||
)
|
||||
from ray.rllib.connectors.common.add_observations_from_episodes_to_batch import (
|
||||
AddObservationsFromEpisodesToBatch,
|
||||
)
|
||||
from ray.rllib.connectors.learner.add_next_observations_from_episodes_to_train_batch import ( # noqa
|
||||
AddNextObservationsFromEpisodesToTrainBatch,
|
||||
)
|
||||
from ray.rllib.core.learner.learner import Learner
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.execution.rollout_ops import (
|
||||
synchronous_parallel_sample,
|
||||
)
|
||||
from ray.rllib.execution.train_ops import (
|
||||
multi_gpu_train_one_step,
|
||||
train_one_step,
|
||||
)
|
||||
from ray.rllib.policy.policy import Policy
|
||||
from ray.rllib.utils.annotations import OldAPIStack, override
|
||||
from ray.rllib.utils.framework import try_import_tf, try_import_tfp
|
||||
from ray.rllib.utils.metrics import (
|
||||
LAST_TARGET_UPDATE_TS,
|
||||
LEARNER_RESULTS,
|
||||
LEARNER_UPDATE_TIMER,
|
||||
NUM_AGENT_STEPS_SAMPLED,
|
||||
NUM_AGENT_STEPS_TRAINED,
|
||||
NUM_ENV_STEPS_SAMPLED,
|
||||
NUM_ENV_STEPS_TRAINED,
|
||||
NUM_TARGET_UPDATES,
|
||||
OFFLINE_SAMPLING_TIMER,
|
||||
SAMPLE_TIMER,
|
||||
SYNCH_WORKER_WEIGHTS_TIMER,
|
||||
TARGET_NET_UPDATE_TIMER,
|
||||
TIMERS,
|
||||
)
|
||||
from ray.rllib.utils.typing import ResultDict, RLModuleSpecType
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
tfp = try_import_tfp()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CQLConfig(SACConfig):
|
||||
"""Defines a configuration class from which a CQL can be built.
|
||||
|
||||
.. testcode::
|
||||
:skipif: True
|
||||
|
||||
from ray.rllib.algorithms.cql import CQLConfig
|
||||
config = CQLConfig().training(gamma=0.9, lr=0.01)
|
||||
config = config.resources(num_gpus=0)
|
||||
config = config.env_runners(num_env_runners=4)
|
||||
print(config.to_dict())
|
||||
# Build a Algorithm object from the config and run 1 training iteration.
|
||||
algo = config.build(env="CartPole-v1")
|
||||
algo.train()
|
||||
"""
|
||||
|
||||
def __init__(self, algo_class=None):
|
||||
super().__init__(algo_class=algo_class or CQL)
|
||||
|
||||
# fmt: off
|
||||
# __sphinx_doc_begin__
|
||||
# CQL-specific config settings:
|
||||
self.bc_iters = 20000
|
||||
self.temperature = 1.0
|
||||
self.num_actions = 10
|
||||
self.lagrangian = False
|
||||
self.lagrangian_thresh = 5.0
|
||||
self.min_q_weight = 5.0
|
||||
self.deterministic_backup = True
|
||||
self.lr = 3e-4
|
||||
# Note, the new stack defines learning rates for each component.
|
||||
# The base learning rate `lr` has to be set to `None`, if using
|
||||
# the new stack.
|
||||
self.actor_lr = 1e-4
|
||||
self.critic_lr = 1e-3
|
||||
self.alpha_lr = 1e-3
|
||||
|
||||
self.replay_buffer_config = {
|
||||
"_enable_replay_buffer_api": True,
|
||||
"type": "MultiAgentPrioritizedReplayBuffer",
|
||||
"capacity": int(1e6),
|
||||
# If True prioritized replay buffer will be used.
|
||||
"prioritized_replay": False,
|
||||
"prioritized_replay_alpha": 0.6,
|
||||
"prioritized_replay_beta": 0.4,
|
||||
"prioritized_replay_eps": 1e-6,
|
||||
# Whether to compute priorities already on the remote worker side.
|
||||
"worker_side_prioritization": False,
|
||||
}
|
||||
|
||||
# Changes to Algorithm's/SACConfig's default:
|
||||
|
||||
# .reporting()
|
||||
self.min_sample_timesteps_per_iteration = 0
|
||||
self.min_train_timesteps_per_iteration = 100
|
||||
# fmt: on
|
||||
# __sphinx_doc_end__
|
||||
|
||||
self.timesteps_per_iteration = DEPRECATED_VALUE
|
||||
|
||||
@override(SACConfig)
|
||||
def training(
|
||||
self,
|
||||
*,
|
||||
bc_iters: Optional[int] = NotProvided,
|
||||
temperature: Optional[float] = NotProvided,
|
||||
num_actions: Optional[int] = NotProvided,
|
||||
lagrangian: Optional[bool] = NotProvided,
|
||||
lagrangian_thresh: Optional[float] = NotProvided,
|
||||
min_q_weight: Optional[float] = NotProvided,
|
||||
deterministic_backup: Optional[bool] = NotProvided,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""Sets the training-related configuration.
|
||||
|
||||
Args:
|
||||
bc_iters: Number of iterations with Behavior Cloning pretraining.
|
||||
temperature: CQL loss temperature.
|
||||
num_actions: Number of actions to sample for CQL loss
|
||||
lagrangian: Whether to use the Lagrangian for Alpha Prime (in CQL loss).
|
||||
lagrangian_thresh: Lagrangian threshold.
|
||||
min_q_weight: in Q weight multiplier.
|
||||
deterministic_backup: If the target in the Bellman update should have an
|
||||
entropy backup. Defaults to `True`.
|
||||
|
||||
Returns:
|
||||
This updated AlgorithmConfig object.
|
||||
"""
|
||||
# Pass kwargs onto super's `training()` method.
|
||||
super().training(**kwargs)
|
||||
|
||||
if bc_iters is not NotProvided:
|
||||
self.bc_iters = bc_iters
|
||||
if temperature is not NotProvided:
|
||||
self.temperature = temperature
|
||||
if num_actions is not NotProvided:
|
||||
self.num_actions = num_actions
|
||||
if lagrangian is not NotProvided:
|
||||
self.lagrangian = lagrangian
|
||||
if lagrangian_thresh is not NotProvided:
|
||||
self.lagrangian_thresh = lagrangian_thresh
|
||||
if min_q_weight is not NotProvided:
|
||||
self.min_q_weight = min_q_weight
|
||||
if deterministic_backup is not NotProvided:
|
||||
self.deterministic_backup = deterministic_backup
|
||||
|
||||
return self
|
||||
|
||||
@override(AlgorithmConfig)
|
||||
def offline_data(self, **kwargs) -> Self:
|
||||
|
||||
super().offline_data(**kwargs)
|
||||
|
||||
# Check, if the passed in class incorporates the `OfflinePreLearner`
|
||||
# interface.
|
||||
if "prelearner_class" in kwargs:
|
||||
from ray.rllib.offline.offline_data import OfflinePreLearner
|
||||
|
||||
if not issubclass(kwargs.get("prelearner_class"), OfflinePreLearner):
|
||||
raise ValueError(
|
||||
f"`prelearner_class` {kwargs.get('prelearner_class')} is not a "
|
||||
"subclass of `OfflinePreLearner`. Any class passed to "
|
||||
"`prelearner_class` needs to implement the interface given by "
|
||||
"`OfflinePreLearner`."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@override(SACConfig)
|
||||
def get_default_learner_class(self) -> Union[Type["Learner"], str]:
|
||||
if self.framework_str == "torch":
|
||||
from ray.rllib.algorithms.cql.torch.cql_torch_learner import CQLTorchLearner
|
||||
|
||||
return CQLTorchLearner
|
||||
else:
|
||||
raise ValueError(
|
||||
f"The framework {self.framework_str} is not supported. "
|
||||
"Use `'torch'` instead."
|
||||
)
|
||||
|
||||
@override(AlgorithmConfig)
|
||||
def build_learner_connector(
|
||||
self,
|
||||
input_observation_space,
|
||||
input_action_space,
|
||||
device=None,
|
||||
):
|
||||
pipeline = super().build_learner_connector(
|
||||
input_observation_space=input_observation_space,
|
||||
input_action_space=input_action_space,
|
||||
device=device,
|
||||
)
|
||||
|
||||
# Prepend the "add-NEXT_OBS-from-episodes-to-train-batch" connector piece (right
|
||||
# after the corresponding "add-OBS-..." default piece).
|
||||
pipeline.insert_after(
|
||||
AddObservationsFromEpisodesToBatch,
|
||||
AddNextObservationsFromEpisodesToTrainBatch(),
|
||||
)
|
||||
|
||||
return pipeline
|
||||
|
||||
@override(SACConfig)
|
||||
def validate(self) -> None:
|
||||
# First check, whether old `timesteps_per_iteration` is used.
|
||||
if self.timesteps_per_iteration != DEPRECATED_VALUE:
|
||||
deprecation_warning(
|
||||
old="timesteps_per_iteration",
|
||||
new="min_train_timesteps_per_iteration",
|
||||
error=True,
|
||||
)
|
||||
|
||||
# Call super's validation method.
|
||||
super().validate()
|
||||
|
||||
# CQL-torch performs the optimizer steps inside the loss function.
|
||||
# Using the multi-GPU optimizer will therefore not work (see multi-GPU
|
||||
# check above) and we must use the simple optimizer for now.
|
||||
if self.simple_optimizer is not True and self.framework_str == "torch":
|
||||
self.simple_optimizer = True
|
||||
|
||||
if self.framework_str in ["tf", "tf2"] and tfp is None:
|
||||
logger.warning(
|
||||
"You need `tensorflow_probability` in order to run CQL! "
|
||||
"Install it via `pip install tensorflow_probability`. Your "
|
||||
f"tf.__version__={tf.__version__ if tf else None}."
|
||||
"Trying to import tfp results in the following error:"
|
||||
)
|
||||
try_import_tfp(error=True)
|
||||
|
||||
# Assert that for a local learner the number of iterations is 1. Note,
|
||||
# this is needed because we have no iterators, but instead a single
|
||||
# batch returned directly from the `OfflineData.sample` method.
|
||||
if (
|
||||
self.num_learners == 0
|
||||
and not self.dataset_num_iters_per_learner
|
||||
and self.enable_rl_module_and_learner
|
||||
):
|
||||
self._value_error(
|
||||
"When using a single local learner the number of iterations "
|
||||
"per learner, `dataset_num_iters_per_learner` has to be defined. "
|
||||
"Set this hyperparameter in the `AlgorithmConfig.offline_data`."
|
||||
)
|
||||
|
||||
@override(SACConfig)
|
||||
def get_default_rl_module_spec(self) -> RLModuleSpecType:
|
||||
if self.framework_str == "torch":
|
||||
from ray.rllib.algorithms.cql.torch.default_cql_torch_rl_module import (
|
||||
DefaultCQLTorchRLModule,
|
||||
)
|
||||
|
||||
return RLModuleSpec(module_class=DefaultCQLTorchRLModule)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"The framework {self.framework_str} is not supported. Use `torch`."
|
||||
)
|
||||
|
||||
@property
|
||||
def _model_config_auto_includes(self):
|
||||
return super()._model_config_auto_includes | {
|
||||
"num_actions": self.num_actions,
|
||||
}
|
||||
|
||||
|
||||
class CQL(SAC):
|
||||
"""CQL (derived from SAC)."""
|
||||
|
||||
@classmethod
|
||||
@override(SAC)
|
||||
def get_default_config(cls) -> CQLConfig:
|
||||
return CQLConfig()
|
||||
|
||||
@classmethod
|
||||
@override(SAC)
|
||||
def get_default_policy_class(
|
||||
cls, config: AlgorithmConfig
|
||||
) -> Optional[Type[Policy]]:
|
||||
if config["framework"] == "torch":
|
||||
return CQLTorchPolicy
|
||||
else:
|
||||
return CQLTFPolicy
|
||||
|
||||
@override(SAC)
|
||||
def training_step(self) -> None:
|
||||
# Old API stack (Policy, RolloutWorker, Connector).
|
||||
if not self.config.enable_env_runner_and_connector_v2:
|
||||
return self._training_step_old_api_stack()
|
||||
|
||||
# Sampling from offline data.
|
||||
with self.metrics.log_time((TIMERS, OFFLINE_SAMPLING_TIMER)):
|
||||
# If we should use an iterator in the learner(s). Note, in case of
|
||||
# multiple learners we must always return a list of iterators.
|
||||
return_iterator = return_iterator = (
|
||||
self.config.num_learners > 0
|
||||
or self.config.dataset_num_iters_per_learner != 1
|
||||
)
|
||||
|
||||
# Return an iterator in case we are using remote learners.
|
||||
batch_or_iterator = self.offline_data.sample(
|
||||
num_samples=self.config.train_batch_size_per_learner,
|
||||
num_shards=self.config.num_learners,
|
||||
# Return an iterator, if a `Learner` should update
|
||||
# multiple times per RLlib iteration.
|
||||
return_iterator=return_iterator,
|
||||
)
|
||||
|
||||
# Updating the policy.
|
||||
with self.metrics.log_time((TIMERS, LEARNER_UPDATE_TIMER)):
|
||||
learner_results = self.learner_group.update(
|
||||
data_iterators=batch_or_iterator,
|
||||
minibatch_size=self.config.train_batch_size_per_learner,
|
||||
num_iters=self.config.dataset_num_iters_per_learner,
|
||||
)
|
||||
|
||||
# Log training results.
|
||||
self.metrics.aggregate(learner_results, key=LEARNER_RESULTS)
|
||||
|
||||
@OldAPIStack
|
||||
def _training_step_old_api_stack(self) -> ResultDict:
|
||||
# Collect SampleBatches from sample workers.
|
||||
with self._timers[SAMPLE_TIMER]:
|
||||
train_batch = synchronous_parallel_sample(worker_set=self.env_runner_group)
|
||||
train_batch = train_batch.as_multi_agent()
|
||||
self._counters[NUM_AGENT_STEPS_SAMPLED] += train_batch.agent_steps()
|
||||
self._counters[NUM_ENV_STEPS_SAMPLED] += train_batch.env_steps()
|
||||
|
||||
# Postprocess batch before we learn on it.
|
||||
post_fn = self.config.get("before_learn_on_batch") or (lambda b, *a: b)
|
||||
train_batch = post_fn(train_batch, self.env_runner_group, self.config)
|
||||
|
||||
# Learn on training batch.
|
||||
# Use simple optimizer (only for multi-agent or tf-eager; all other
|
||||
# cases should use the multi-GPU optimizer, even if only using 1 GPU)
|
||||
if self.config.get("simple_optimizer") is True:
|
||||
train_results = train_one_step(self, train_batch)
|
||||
else:
|
||||
train_results = multi_gpu_train_one_step(self, train_batch)
|
||||
|
||||
# Update target network every `target_network_update_freq` training steps.
|
||||
cur_ts = self._counters[
|
||||
NUM_AGENT_STEPS_TRAINED
|
||||
if self.config.count_steps_by == "agent_steps"
|
||||
else NUM_ENV_STEPS_TRAINED
|
||||
]
|
||||
last_update = self._counters[LAST_TARGET_UPDATE_TS]
|
||||
if cur_ts - last_update >= self.config.target_network_update_freq:
|
||||
with self._timers[TARGET_NET_UPDATE_TIMER]:
|
||||
to_update = self.env_runner.get_policies_to_train()
|
||||
self.env_runner.foreach_policy_to_train(
|
||||
lambda p, pid: pid in to_update and p.update_target()
|
||||
)
|
||||
self._counters[NUM_TARGET_UPDATES] += 1
|
||||
self._counters[LAST_TARGET_UPDATE_TS] = cur_ts
|
||||
|
||||
# Update remote workers's weights after learning on local worker
|
||||
# (only those policies that were actually trained).
|
||||
if self.env_runner_group.num_remote_workers() > 0:
|
||||
with self._timers[SYNCH_WORKER_WEIGHTS_TIMER]:
|
||||
self.env_runner_group.sync_weights(policies=list(train_results.keys()))
|
||||
|
||||
# Return all collected metrics for the iteration.
|
||||
return train_results
|
||||
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
TensorFlow policy class used for CQL.
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Dict, List, Type, Union
|
||||
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import tree
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.sac.sac_tf_policy import (
|
||||
ActorCriticOptimizerMixin as SACActorCriticOptimizerMixin,
|
||||
ComputeTDErrorMixin,
|
||||
_get_dist_class,
|
||||
apply_gradients as sac_apply_gradients,
|
||||
build_sac_model,
|
||||
compute_and_clip_gradients as sac_compute_and_clip_gradients,
|
||||
get_distribution_inputs_and_class,
|
||||
postprocess_trajectory,
|
||||
setup_late_mixins,
|
||||
stats,
|
||||
validate_spaces,
|
||||
)
|
||||
from ray.rllib.models.modelv2 import ModelV2
|
||||
from ray.rllib.models.tf.tf_action_dist import TFActionDistribution
|
||||
from ray.rllib.policy.policy import Policy
|
||||
from ray.rllib.policy.sample_batch import SampleBatch
|
||||
from ray.rllib.policy.tf_mixins import TargetNetworkMixin
|
||||
from ray.rllib.policy.tf_policy_template import build_tf_policy
|
||||
from ray.rllib.utils.exploration.random import Random
|
||||
from ray.rllib.utils.framework import get_variable, try_import_tf, try_import_tfp
|
||||
from ray.rllib.utils.typing import (
|
||||
AlgorithmConfigDict,
|
||||
LocalOptimizer,
|
||||
ModelGradients,
|
||||
TensorType,
|
||||
)
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
tfp = try_import_tfp()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MEAN_MIN = -9.0
|
||||
MEAN_MAX = 9.0
|
||||
|
||||
|
||||
def _repeat_tensor(t: TensorType, n: int):
|
||||
# Insert new axis at position 1 into tensor t
|
||||
t_rep = tf.expand_dims(t, 1)
|
||||
# Repeat tensor t_rep along new axis n times
|
||||
multiples = tf.concat([[1, n], tf.tile([1], tf.expand_dims(tf.rank(t) - 1, 0))], 0)
|
||||
t_rep = tf.tile(t_rep, multiples)
|
||||
# Merge new axis into batch axis
|
||||
t_rep = tf.reshape(t_rep, tf.concat([[-1], tf.shape(t)[1:]], 0))
|
||||
return t_rep
|
||||
|
||||
|
||||
# Returns policy tiled actions and log probabilities for CQL Loss
|
||||
def policy_actions_repeat(model, action_dist, obs, num_repeat=1):
|
||||
batch_size = tf.shape(tree.flatten(obs)[0])[0]
|
||||
obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs)
|
||||
logits, _ = model.get_action_model_outputs(obs_temp)
|
||||
policy_dist = action_dist(logits, model)
|
||||
actions, logp_ = policy_dist.sample_logp()
|
||||
logp = tf.expand_dims(logp_, -1)
|
||||
return actions, tf.reshape(logp, [batch_size, num_repeat, 1])
|
||||
|
||||
|
||||
def q_values_repeat(model, obs, actions, twin=False):
|
||||
action_shape = tf.shape(actions)[0]
|
||||
obs_shape = tf.shape(tree.flatten(obs)[0])[0]
|
||||
num_repeat = action_shape // obs_shape
|
||||
obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs)
|
||||
if not twin:
|
||||
preds_, _ = model.get_q_values(obs_temp, actions)
|
||||
else:
|
||||
preds_, _ = model.get_twin_q_values(obs_temp, actions)
|
||||
preds = tf.reshape(preds_, [obs_shape, num_repeat, 1])
|
||||
return preds
|
||||
|
||||
|
||||
def cql_loss(
|
||||
policy: Policy,
|
||||
model: ModelV2,
|
||||
dist_class: Type[TFActionDistribution],
|
||||
train_batch: SampleBatch,
|
||||
) -> Union[TensorType, List[TensorType]]:
|
||||
logger.info(f"Current iteration = {policy.cur_iter}")
|
||||
policy.cur_iter += 1
|
||||
|
||||
# For best performance, turn deterministic off
|
||||
deterministic = policy.config["_deterministic_loss"]
|
||||
assert not deterministic
|
||||
twin_q = policy.config["twin_q"]
|
||||
discount = policy.config["gamma"]
|
||||
|
||||
# CQL Parameters
|
||||
bc_iters = policy.config["bc_iters"]
|
||||
cql_temp = policy.config["temperature"]
|
||||
num_actions = policy.config["num_actions"]
|
||||
min_q_weight = policy.config["min_q_weight"]
|
||||
use_lagrange = policy.config["lagrangian"]
|
||||
target_action_gap = policy.config["lagrangian_thresh"]
|
||||
|
||||
obs = train_batch[SampleBatch.CUR_OBS]
|
||||
actions = tf.cast(train_batch[SampleBatch.ACTIONS], tf.float32)
|
||||
rewards = tf.cast(train_batch[SampleBatch.REWARDS], tf.float32)
|
||||
next_obs = train_batch[SampleBatch.NEXT_OBS]
|
||||
terminals = train_batch[SampleBatch.TERMINATEDS]
|
||||
|
||||
model_out_t, _ = model(SampleBatch(obs=obs, _is_training=True), [], None)
|
||||
|
||||
model_out_tp1, _ = model(SampleBatch(obs=next_obs, _is_training=True), [], None)
|
||||
|
||||
target_model_out_tp1, _ = policy.target_model(
|
||||
SampleBatch(obs=next_obs, _is_training=True), [], None
|
||||
)
|
||||
|
||||
action_dist_class = _get_dist_class(policy, policy.config, policy.action_space)
|
||||
action_dist_inputs_t, _ = model.get_action_model_outputs(model_out_t)
|
||||
action_dist_t = action_dist_class(action_dist_inputs_t, model)
|
||||
policy_t, log_pis_t = action_dist_t.sample_logp()
|
||||
log_pis_t = tf.expand_dims(log_pis_t, -1)
|
||||
|
||||
# Unlike original SAC, Alpha and Actor Loss are computed first.
|
||||
# Alpha Loss
|
||||
alpha_loss = -tf.reduce_mean(
|
||||
model.log_alpha * tf.stop_gradient(log_pis_t + model.target_entropy)
|
||||
)
|
||||
|
||||
# Policy Loss (Either Behavior Clone Loss or SAC Loss)
|
||||
alpha = tf.math.exp(model.log_alpha)
|
||||
if policy.cur_iter >= bc_iters:
|
||||
min_q, _ = model.get_q_values(model_out_t, policy_t)
|
||||
if twin_q:
|
||||
twin_q_, _ = model.get_twin_q_values(model_out_t, policy_t)
|
||||
min_q = tf.math.minimum(min_q, twin_q_)
|
||||
actor_loss = tf.reduce_mean(tf.stop_gradient(alpha) * log_pis_t - min_q)
|
||||
else:
|
||||
bc_logp = action_dist_t.logp(actions)
|
||||
actor_loss = tf.reduce_mean(tf.stop_gradient(alpha) * log_pis_t - bc_logp)
|
||||
# actor_loss = -tf.reduce_mean(bc_logp)
|
||||
|
||||
# Critic Loss (Standard SAC Critic L2 Loss + CQL Entropy Loss)
|
||||
# SAC Loss:
|
||||
# Q-values for the batched actions.
|
||||
action_dist_inputs_tp1, _ = model.get_action_model_outputs(model_out_tp1)
|
||||
action_dist_tp1 = action_dist_class(action_dist_inputs_tp1, model)
|
||||
policy_tp1, _ = action_dist_tp1.sample_logp()
|
||||
|
||||
q_t, _ = model.get_q_values(model_out_t, actions)
|
||||
q_t_selected = tf.squeeze(q_t, axis=-1)
|
||||
if twin_q:
|
||||
twin_q_t, _ = model.get_twin_q_values(model_out_t, actions)
|
||||
twin_q_t_selected = tf.squeeze(twin_q_t, axis=-1)
|
||||
|
||||
# Target q network evaluation.
|
||||
q_tp1, _ = policy.target_model.get_q_values(target_model_out_tp1, policy_tp1)
|
||||
if twin_q:
|
||||
twin_q_tp1, _ = policy.target_model.get_twin_q_values(
|
||||
target_model_out_tp1, policy_tp1
|
||||
)
|
||||
# Take min over both twin-NNs.
|
||||
q_tp1 = tf.math.minimum(q_tp1, twin_q_tp1)
|
||||
|
||||
q_tp1_best = tf.squeeze(input=q_tp1, axis=-1)
|
||||
q_tp1_best_masked = (1.0 - tf.cast(terminals, tf.float32)) * q_tp1_best
|
||||
|
||||
# compute RHS of bellman equation
|
||||
q_t_target = tf.stop_gradient(
|
||||
rewards + (discount ** policy.config["n_step"]) * q_tp1_best_masked
|
||||
)
|
||||
|
||||
# Compute the TD-error (potentially clipped), for priority replay buffer
|
||||
base_td_error = tf.math.abs(q_t_selected - q_t_target)
|
||||
if twin_q:
|
||||
twin_td_error = tf.math.abs(twin_q_t_selected - q_t_target)
|
||||
td_error = 0.5 * (base_td_error + twin_td_error)
|
||||
else:
|
||||
td_error = base_td_error
|
||||
|
||||
critic_loss_1 = tf.keras.losses.MSE(q_t_selected, q_t_target)
|
||||
if twin_q:
|
||||
critic_loss_2 = tf.keras.losses.MSE(twin_q_t_selected, q_t_target)
|
||||
|
||||
# CQL Loss (We are using Entropy version of CQL (the best version))
|
||||
rand_actions, _ = policy._random_action_generator.get_exploration_action(
|
||||
action_distribution=action_dist_class(
|
||||
tf.tile(action_dist_tp1.inputs, (num_actions, 1)), model
|
||||
),
|
||||
timestep=0,
|
||||
explore=True,
|
||||
)
|
||||
curr_actions, curr_logp = policy_actions_repeat(
|
||||
model, action_dist_class, model_out_t, num_actions
|
||||
)
|
||||
next_actions, next_logp = policy_actions_repeat(
|
||||
model, action_dist_class, model_out_tp1, num_actions
|
||||
)
|
||||
|
||||
q1_rand = q_values_repeat(model, model_out_t, rand_actions)
|
||||
q1_curr_actions = q_values_repeat(model, model_out_t, curr_actions)
|
||||
q1_next_actions = q_values_repeat(model, model_out_t, next_actions)
|
||||
|
||||
if twin_q:
|
||||
q2_rand = q_values_repeat(model, model_out_t, rand_actions, twin=True)
|
||||
q2_curr_actions = q_values_repeat(model, model_out_t, curr_actions, twin=True)
|
||||
q2_next_actions = q_values_repeat(model, model_out_t, next_actions, twin=True)
|
||||
|
||||
random_density = np.log(0.5 ** int(curr_actions.shape[-1]))
|
||||
cat_q1 = tf.concat(
|
||||
[
|
||||
q1_rand - random_density,
|
||||
q1_next_actions - tf.stop_gradient(next_logp),
|
||||
q1_curr_actions - tf.stop_gradient(curr_logp),
|
||||
],
|
||||
1,
|
||||
)
|
||||
if twin_q:
|
||||
cat_q2 = tf.concat(
|
||||
[
|
||||
q2_rand - random_density,
|
||||
q2_next_actions - tf.stop_gradient(next_logp),
|
||||
q2_curr_actions - tf.stop_gradient(curr_logp),
|
||||
],
|
||||
1,
|
||||
)
|
||||
|
||||
min_qf1_loss_ = (
|
||||
tf.reduce_mean(tf.reduce_logsumexp(cat_q1 / cql_temp, axis=1))
|
||||
* min_q_weight
|
||||
* cql_temp
|
||||
)
|
||||
min_qf1_loss = min_qf1_loss_ - (tf.reduce_mean(q_t) * min_q_weight)
|
||||
if twin_q:
|
||||
min_qf2_loss_ = (
|
||||
tf.reduce_mean(tf.reduce_logsumexp(cat_q2 / cql_temp, axis=1))
|
||||
* min_q_weight
|
||||
* cql_temp
|
||||
)
|
||||
min_qf2_loss = min_qf2_loss_ - (tf.reduce_mean(twin_q_t) * min_q_weight)
|
||||
|
||||
if use_lagrange:
|
||||
alpha_prime = tf.clip_by_value(model.log_alpha_prime.exp(), 0.0, 1000000.0)[0]
|
||||
min_qf1_loss = alpha_prime * (min_qf1_loss - target_action_gap)
|
||||
if twin_q:
|
||||
min_qf2_loss = alpha_prime * (min_qf2_loss - target_action_gap)
|
||||
alpha_prime_loss = 0.5 * (-min_qf1_loss - min_qf2_loss)
|
||||
else:
|
||||
alpha_prime_loss = -min_qf1_loss
|
||||
|
||||
cql_loss = [min_qf1_loss]
|
||||
if twin_q:
|
||||
cql_loss.append(min_qf2_loss)
|
||||
|
||||
critic_loss = [critic_loss_1 + min_qf1_loss]
|
||||
if twin_q:
|
||||
critic_loss.append(critic_loss_2 + min_qf2_loss)
|
||||
|
||||
# Save for stats function.
|
||||
policy.q_t = q_t_selected
|
||||
policy.policy_t = policy_t
|
||||
policy.log_pis_t = log_pis_t
|
||||
policy.td_error = td_error
|
||||
policy.actor_loss = actor_loss
|
||||
policy.critic_loss = critic_loss
|
||||
policy.alpha_loss = alpha_loss
|
||||
policy.log_alpha_value = model.log_alpha
|
||||
policy.alpha_value = alpha
|
||||
policy.target_entropy = model.target_entropy
|
||||
# CQL Stats
|
||||
policy.cql_loss = cql_loss
|
||||
if use_lagrange:
|
||||
policy.log_alpha_prime_value = model.log_alpha_prime[0]
|
||||
policy.alpha_prime_value = alpha_prime
|
||||
policy.alpha_prime_loss = alpha_prime_loss
|
||||
|
||||
# Return all loss terms corresponding to our optimizers.
|
||||
if use_lagrange:
|
||||
return actor_loss + tf.math.add_n(critic_loss) + alpha_loss + alpha_prime_loss
|
||||
return actor_loss + tf.math.add_n(critic_loss) + alpha_loss
|
||||
|
||||
|
||||
def cql_stats(policy: Policy, train_batch: SampleBatch) -> Dict[str, TensorType]:
|
||||
sac_dict = stats(policy, train_batch)
|
||||
sac_dict["cql_loss"] = tf.reduce_mean(tf.stack(policy.cql_loss))
|
||||
if policy.config["lagrangian"]:
|
||||
sac_dict["log_alpha_prime_value"] = policy.log_alpha_prime_value
|
||||
sac_dict["alpha_prime_value"] = policy.alpha_prime_value
|
||||
sac_dict["alpha_prime_loss"] = policy.alpha_prime_loss
|
||||
return sac_dict
|
||||
|
||||
|
||||
class ActorCriticOptimizerMixin(SACActorCriticOptimizerMixin):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
if config["lagrangian"]:
|
||||
# Eager mode.
|
||||
if config["framework"] == "tf2":
|
||||
self._alpha_prime_optimizer = tf.keras.optimizers.Adam(
|
||||
learning_rate=config["optimization"]["critic_learning_rate"]
|
||||
)
|
||||
# Static graph mode.
|
||||
else:
|
||||
self._alpha_prime_optimizer = tf1.train.AdamOptimizer(
|
||||
learning_rate=config["optimization"]["critic_learning_rate"]
|
||||
)
|
||||
|
||||
|
||||
def setup_early_mixins(
|
||||
policy: Policy,
|
||||
obs_space: gym.spaces.Space,
|
||||
action_space: gym.spaces.Space,
|
||||
config: AlgorithmConfigDict,
|
||||
) -> None:
|
||||
"""Call mixin classes' constructors before Policy's initialization.
|
||||
|
||||
Adds the necessary optimizers to the given Policy.
|
||||
|
||||
Args:
|
||||
policy: The Policy object.
|
||||
obs_space (gym.spaces.Space): The Policy's observation space.
|
||||
action_space (gym.spaces.Space): The Policy's action space.
|
||||
config: The Policy's config.
|
||||
"""
|
||||
policy.cur_iter = 0
|
||||
ActorCriticOptimizerMixin.__init__(policy, config)
|
||||
if config["lagrangian"]:
|
||||
policy.model.log_alpha_prime = get_variable(
|
||||
0.0, framework="tf", trainable=True, tf_name="log_alpha_prime"
|
||||
)
|
||||
policy.alpha_prime_optim = tf.keras.optimizers.Adam(
|
||||
learning_rate=config["optimization"]["critic_learning_rate"],
|
||||
)
|
||||
# Generic random action generator for calculating CQL-loss.
|
||||
policy._random_action_generator = Random(
|
||||
action_space,
|
||||
model=None,
|
||||
framework="tf2",
|
||||
policy_config=config,
|
||||
num_workers=0,
|
||||
worker_index=0,
|
||||
)
|
||||
|
||||
|
||||
def compute_gradients_fn(
|
||||
policy: Policy, optimizer: LocalOptimizer, loss: TensorType
|
||||
) -> ModelGradients:
|
||||
grads_and_vars = sac_compute_and_clip_gradients(policy, optimizer, loss)
|
||||
|
||||
if policy.config["lagrangian"]:
|
||||
# Eager: Use GradientTape (which is a property of the `optimizer`
|
||||
# object (an OptimizerWrapper): see rllib/policy/eager_tf_policy.py).
|
||||
if policy.config["framework"] == "tf2":
|
||||
tape = optimizer.tape
|
||||
log_alpha_prime = [policy.model.log_alpha_prime]
|
||||
alpha_prime_grads_and_vars = list(
|
||||
zip(
|
||||
tape.gradient(policy.alpha_prime_loss, log_alpha_prime),
|
||||
log_alpha_prime,
|
||||
)
|
||||
)
|
||||
# Tf1.x: Use optimizer.compute_gradients()
|
||||
else:
|
||||
alpha_prime_grads_and_vars = (
|
||||
policy._alpha_prime_optimizer.compute_gradients(
|
||||
policy.alpha_prime_loss, var_list=[policy.model.log_alpha_prime]
|
||||
)
|
||||
)
|
||||
|
||||
# Clip if necessary.
|
||||
if policy.config["grad_clip"]:
|
||||
clip_func = partial(tf.clip_by_norm, clip_norm=policy.config["grad_clip"])
|
||||
else:
|
||||
clip_func = tf.identity
|
||||
|
||||
# Save grads and vars for later use in `build_apply_op`.
|
||||
policy._alpha_prime_grads_and_vars = [
|
||||
(clip_func(g), v) for (g, v) in alpha_prime_grads_and_vars if g is not None
|
||||
]
|
||||
|
||||
grads_and_vars += policy._alpha_prime_grads_and_vars
|
||||
return grads_and_vars
|
||||
|
||||
|
||||
def apply_gradients_fn(policy, optimizer, grads_and_vars):
|
||||
sac_results = sac_apply_gradients(policy, optimizer, grads_and_vars)
|
||||
|
||||
if policy.config["lagrangian"]:
|
||||
# Eager mode -> Just apply and return None.
|
||||
if policy.config["framework"] == "tf2":
|
||||
policy._alpha_prime_optimizer.apply_gradients(
|
||||
policy._alpha_prime_grads_and_vars
|
||||
)
|
||||
return
|
||||
# Tf static graph -> Return grouped op.
|
||||
else:
|
||||
alpha_prime_apply_op = policy._alpha_prime_optimizer.apply_gradients(
|
||||
policy._alpha_prime_grads_and_vars,
|
||||
global_step=tf1.train.get_or_create_global_step(),
|
||||
)
|
||||
return tf.group([sac_results, alpha_prime_apply_op])
|
||||
return sac_results
|
||||
|
||||
|
||||
# Build a child class of `TFPolicy`, given the custom functions defined
|
||||
# above.
|
||||
CQLTFPolicy = build_tf_policy(
|
||||
name="CQLTFPolicy",
|
||||
loss_fn=cql_loss,
|
||||
get_default_config=lambda: ray.rllib.algorithms.cql.cql.CQLConfig(),
|
||||
validate_spaces=validate_spaces,
|
||||
stats_fn=cql_stats,
|
||||
postprocess_fn=postprocess_trajectory,
|
||||
before_init=setup_early_mixins,
|
||||
after_init=setup_late_mixins,
|
||||
make_model=build_sac_model,
|
||||
extra_learn_fetches_fn=lambda policy: {"td_error": policy.td_error},
|
||||
mixins=[ActorCriticOptimizerMixin, TargetNetworkMixin, ComputeTDErrorMixin],
|
||||
action_distribution_fn=get_distribution_inputs_and_class,
|
||||
compute_gradients_fn=compute_gradients_fn,
|
||||
apply_gradients_fn=apply_gradients_fn,
|
||||
)
|
||||
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
PyTorch policy class used for CQL.
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List, Tuple, Type, Union
|
||||
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import tree
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.sac.sac_tf_policy import (
|
||||
postprocess_trajectory,
|
||||
validate_spaces,
|
||||
)
|
||||
from ray.rllib.algorithms.sac.sac_torch_policy import (
|
||||
ComputeTDErrorMixin,
|
||||
_get_dist_class,
|
||||
action_distribution_fn,
|
||||
build_sac_model_and_action_dist,
|
||||
optimizer_fn,
|
||||
setup_late_mixins,
|
||||
stats,
|
||||
)
|
||||
from ray.rllib.models.modelv2 import ModelV2
|
||||
from ray.rllib.models.torch.torch_action_dist import TorchDistributionWrapper
|
||||
from ray.rllib.policy.policy import Policy
|
||||
from ray.rllib.policy.policy_template import build_policy_class
|
||||
from ray.rllib.policy.sample_batch import SampleBatch
|
||||
from ray.rllib.policy.torch_mixins import TargetNetworkMixin
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.metrics.learner_info import LEARNER_STATS_KEY
|
||||
from ray.rllib.utils.torch_utils import (
|
||||
apply_grad_clipping,
|
||||
concat_multi_gpu_td_errors,
|
||||
convert_to_torch_tensor,
|
||||
)
|
||||
from ray.rllib.utils.typing import AlgorithmConfigDict, LocalOptimizer, TensorType
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
F = nn.functional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MEAN_MIN = -9.0
|
||||
MEAN_MAX = 9.0
|
||||
|
||||
|
||||
def _repeat_tensor(t: TensorType, n: int):
|
||||
# Insert new dimension at posotion 1 into tensor t
|
||||
t_rep = t.unsqueeze(1)
|
||||
# Repeat tensor t_rep along new dimension n times
|
||||
t_rep = torch.repeat_interleave(t_rep, n, dim=1)
|
||||
# Merge new dimension into batch dimension
|
||||
t_rep = t_rep.view(-1, *t.shape[1:])
|
||||
return t_rep
|
||||
|
||||
|
||||
# Returns policy tiled actions and log probabilities for CQL Loss
|
||||
def policy_actions_repeat(model, action_dist, obs, num_repeat=1):
|
||||
batch_size = tree.flatten(obs)[0].shape[0]
|
||||
obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs)
|
||||
logits, _ = model.get_action_model_outputs(obs_temp)
|
||||
policy_dist = action_dist(logits, model)
|
||||
actions, logp_ = policy_dist.sample_logp()
|
||||
logp = logp_.unsqueeze(-1)
|
||||
return actions, logp.view(batch_size, num_repeat, 1)
|
||||
|
||||
|
||||
def q_values_repeat(model, obs, actions, twin=False):
|
||||
action_shape = actions.shape[0]
|
||||
obs_shape = tree.flatten(obs)[0].shape[0]
|
||||
num_repeat = int(action_shape / obs_shape)
|
||||
obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs)
|
||||
if not twin:
|
||||
preds_, _ = model.get_q_values(obs_temp, actions)
|
||||
else:
|
||||
preds_, _ = model.get_twin_q_values(obs_temp, actions)
|
||||
preds = preds_.view(obs_shape, num_repeat, 1)
|
||||
return preds
|
||||
|
||||
|
||||
def cql_loss(
|
||||
policy: Policy,
|
||||
model: ModelV2,
|
||||
dist_class: Type[TorchDistributionWrapper],
|
||||
train_batch: SampleBatch,
|
||||
) -> Union[TensorType, List[TensorType]]:
|
||||
logger.info(f"Current iteration = {policy.cur_iter}")
|
||||
policy.cur_iter += 1
|
||||
|
||||
# Look up the target model (tower) using the model tower.
|
||||
target_model = policy.target_models[model]
|
||||
|
||||
# For best performance, turn deterministic off
|
||||
deterministic = policy.config["_deterministic_loss"]
|
||||
assert not deterministic
|
||||
twin_q = policy.config["twin_q"]
|
||||
discount = policy.config["gamma"]
|
||||
action_low = model.action_space.low[0]
|
||||
action_high = model.action_space.high[0]
|
||||
|
||||
# CQL Parameters
|
||||
bc_iters = policy.config["bc_iters"]
|
||||
cql_temp = policy.config["temperature"]
|
||||
num_actions = policy.config["num_actions"]
|
||||
min_q_weight = policy.config["min_q_weight"]
|
||||
use_lagrange = policy.config["lagrangian"]
|
||||
target_action_gap = policy.config["lagrangian_thresh"]
|
||||
|
||||
obs = train_batch[SampleBatch.CUR_OBS]
|
||||
actions = train_batch[SampleBatch.ACTIONS]
|
||||
rewards = train_batch[SampleBatch.REWARDS].float()
|
||||
next_obs = train_batch[SampleBatch.NEXT_OBS]
|
||||
terminals = train_batch[SampleBatch.TERMINATEDS]
|
||||
|
||||
model_out_t, _ = model(SampleBatch(obs=obs, _is_training=True), [], None)
|
||||
|
||||
model_out_tp1, _ = model(SampleBatch(obs=next_obs, _is_training=True), [], None)
|
||||
|
||||
target_model_out_tp1, _ = target_model(
|
||||
SampleBatch(obs=next_obs, _is_training=True), [], None
|
||||
)
|
||||
|
||||
action_dist_class = _get_dist_class(policy, policy.config, policy.action_space)
|
||||
action_dist_inputs_t, _ = model.get_action_model_outputs(model_out_t)
|
||||
action_dist_t = action_dist_class(action_dist_inputs_t, model)
|
||||
policy_t, log_pis_t = action_dist_t.sample_logp()
|
||||
log_pis_t = torch.unsqueeze(log_pis_t, -1)
|
||||
|
||||
# Unlike original SAC, Alpha and Actor Loss are computed first.
|
||||
# Alpha Loss
|
||||
alpha_loss = -(model.log_alpha * (log_pis_t + model.target_entropy).detach()).mean()
|
||||
|
||||
batch_size = tree.flatten(obs)[0].shape[0]
|
||||
if batch_size == policy.config["train_batch_size"]:
|
||||
policy.alpha_optim.zero_grad()
|
||||
alpha_loss.backward()
|
||||
policy.alpha_optim.step()
|
||||
|
||||
# Policy Loss (Either Behavior Clone Loss or SAC Loss)
|
||||
alpha = torch.exp(model.log_alpha)
|
||||
if policy.cur_iter >= bc_iters:
|
||||
min_q, _ = model.get_q_values(model_out_t, policy_t)
|
||||
if twin_q:
|
||||
twin_q_, _ = model.get_twin_q_values(model_out_t, policy_t)
|
||||
min_q = torch.min(min_q, twin_q_)
|
||||
actor_loss = (alpha.detach() * log_pis_t - min_q).mean()
|
||||
else:
|
||||
bc_logp = action_dist_t.logp(actions)
|
||||
actor_loss = (alpha.detach() * log_pis_t - bc_logp).mean()
|
||||
# actor_loss = -bc_logp.mean()
|
||||
|
||||
if batch_size == policy.config["train_batch_size"]:
|
||||
policy.actor_optim.zero_grad()
|
||||
actor_loss.backward(retain_graph=True)
|
||||
policy.actor_optim.step()
|
||||
|
||||
# Critic Loss (Standard SAC Critic L2 Loss + CQL Entropy Loss)
|
||||
# SAC Loss:
|
||||
# Q-values for the batched actions.
|
||||
action_dist_inputs_tp1, _ = model.get_action_model_outputs(model_out_tp1)
|
||||
action_dist_tp1 = action_dist_class(action_dist_inputs_tp1, model)
|
||||
policy_tp1, _ = action_dist_tp1.sample_logp()
|
||||
|
||||
q_t, _ = model.get_q_values(model_out_t, train_batch[SampleBatch.ACTIONS])
|
||||
q_t_selected = torch.squeeze(q_t, dim=-1)
|
||||
if twin_q:
|
||||
twin_q_t, _ = model.get_twin_q_values(
|
||||
model_out_t, train_batch[SampleBatch.ACTIONS]
|
||||
)
|
||||
twin_q_t_selected = torch.squeeze(twin_q_t, dim=-1)
|
||||
|
||||
# Target q network evaluation.
|
||||
q_tp1, _ = target_model.get_q_values(target_model_out_tp1, policy_tp1)
|
||||
if twin_q:
|
||||
twin_q_tp1, _ = target_model.get_twin_q_values(target_model_out_tp1, policy_tp1)
|
||||
# Take min over both twin-NNs.
|
||||
q_tp1 = torch.min(q_tp1, twin_q_tp1)
|
||||
|
||||
q_tp1_best = torch.squeeze(input=q_tp1, dim=-1)
|
||||
q_tp1_best_masked = (1.0 - terminals.float()) * q_tp1_best
|
||||
|
||||
# compute RHS of bellman equation
|
||||
q_t_target = (
|
||||
rewards + (discount ** policy.config["n_step"]) * q_tp1_best_masked
|
||||
).detach()
|
||||
|
||||
# Compute the TD-error (potentially clipped), for priority replay buffer
|
||||
base_td_error = torch.abs(q_t_selected - q_t_target)
|
||||
if twin_q:
|
||||
twin_td_error = torch.abs(twin_q_t_selected - q_t_target)
|
||||
td_error = 0.5 * (base_td_error + twin_td_error)
|
||||
else:
|
||||
td_error = base_td_error
|
||||
|
||||
critic_loss_1 = nn.functional.mse_loss(q_t_selected, q_t_target)
|
||||
if twin_q:
|
||||
critic_loss_2 = nn.functional.mse_loss(twin_q_t_selected, q_t_target)
|
||||
|
||||
# CQL Loss (We are using Entropy version of CQL (the best version))
|
||||
rand_actions = convert_to_torch_tensor(
|
||||
torch.FloatTensor(actions.shape[0] * num_actions, actions.shape[-1]).uniform_(
|
||||
action_low, action_high
|
||||
),
|
||||
policy.device,
|
||||
)
|
||||
curr_actions, curr_logp = policy_actions_repeat(
|
||||
model, action_dist_class, model_out_t, num_actions
|
||||
)
|
||||
next_actions, next_logp = policy_actions_repeat(
|
||||
model, action_dist_class, model_out_tp1, num_actions
|
||||
)
|
||||
|
||||
q1_rand = q_values_repeat(model, model_out_t, rand_actions)
|
||||
q1_curr_actions = q_values_repeat(model, model_out_t, curr_actions)
|
||||
q1_next_actions = q_values_repeat(model, model_out_t, next_actions)
|
||||
|
||||
if twin_q:
|
||||
q2_rand = q_values_repeat(model, model_out_t, rand_actions, twin=True)
|
||||
q2_curr_actions = q_values_repeat(model, model_out_t, curr_actions, twin=True)
|
||||
q2_next_actions = q_values_repeat(model, model_out_t, next_actions, twin=True)
|
||||
|
||||
random_density = np.log(0.5 ** curr_actions.shape[-1])
|
||||
cat_q1 = torch.cat(
|
||||
[
|
||||
q1_rand - random_density,
|
||||
q1_next_actions - next_logp.detach(),
|
||||
q1_curr_actions - curr_logp.detach(),
|
||||
],
|
||||
1,
|
||||
)
|
||||
if twin_q:
|
||||
cat_q2 = torch.cat(
|
||||
[
|
||||
q2_rand - random_density,
|
||||
q2_next_actions - next_logp.detach(),
|
||||
q2_curr_actions - curr_logp.detach(),
|
||||
],
|
||||
1,
|
||||
)
|
||||
|
||||
min_qf1_loss_ = (
|
||||
torch.logsumexp(cat_q1 / cql_temp, dim=1).mean() * min_q_weight * cql_temp
|
||||
)
|
||||
min_qf1_loss = min_qf1_loss_ - (q_t.mean() * min_q_weight)
|
||||
if twin_q:
|
||||
min_qf2_loss_ = (
|
||||
torch.logsumexp(cat_q2 / cql_temp, dim=1).mean() * min_q_weight * cql_temp
|
||||
)
|
||||
min_qf2_loss = min_qf2_loss_ - (twin_q_t.mean() * min_q_weight)
|
||||
|
||||
if use_lagrange:
|
||||
alpha_prime = torch.clamp(model.log_alpha_prime.exp(), min=0.0, max=1000000.0)[
|
||||
0
|
||||
]
|
||||
min_qf1_loss = alpha_prime * (min_qf1_loss - target_action_gap)
|
||||
if twin_q:
|
||||
min_qf2_loss = alpha_prime * (min_qf2_loss - target_action_gap)
|
||||
alpha_prime_loss = 0.5 * (-min_qf1_loss - min_qf2_loss)
|
||||
else:
|
||||
alpha_prime_loss = -min_qf1_loss
|
||||
|
||||
cql_loss = [min_qf1_loss]
|
||||
if twin_q:
|
||||
cql_loss.append(min_qf2_loss)
|
||||
|
||||
critic_loss = [critic_loss_1 + min_qf1_loss]
|
||||
if twin_q:
|
||||
critic_loss.append(critic_loss_2 + min_qf2_loss)
|
||||
|
||||
if batch_size == policy.config["train_batch_size"]:
|
||||
policy.critic_optims[0].zero_grad()
|
||||
critic_loss[0].backward(retain_graph=True)
|
||||
policy.critic_optims[0].step()
|
||||
|
||||
if twin_q:
|
||||
policy.critic_optims[1].zero_grad()
|
||||
critic_loss[1].backward(retain_graph=False)
|
||||
policy.critic_optims[1].step()
|
||||
|
||||
# Store values for stats function in model (tower), such that for
|
||||
# multi-GPU, we do not override them during the parallel loss phase.
|
||||
# SAC stats.
|
||||
model.tower_stats["q_t"] = q_t_selected
|
||||
model.tower_stats["policy_t"] = policy_t
|
||||
model.tower_stats["log_pis_t"] = log_pis_t
|
||||
model.tower_stats["actor_loss"] = actor_loss
|
||||
model.tower_stats["critic_loss"] = critic_loss
|
||||
model.tower_stats["alpha_loss"] = alpha_loss
|
||||
model.tower_stats["log_alpha_value"] = model.log_alpha
|
||||
model.tower_stats["alpha_value"] = alpha
|
||||
model.tower_stats["target_entropy"] = model.target_entropy
|
||||
# CQL stats.
|
||||
model.tower_stats["cql_loss"] = cql_loss
|
||||
|
||||
# TD-error tensor in final stats
|
||||
# will be concatenated and retrieved for each individual batch item.
|
||||
model.tower_stats["td_error"] = td_error
|
||||
|
||||
if use_lagrange:
|
||||
model.tower_stats["log_alpha_prime_value"] = model.log_alpha_prime[0]
|
||||
model.tower_stats["alpha_prime_value"] = alpha_prime
|
||||
model.tower_stats["alpha_prime_loss"] = alpha_prime_loss
|
||||
|
||||
if batch_size == policy.config["train_batch_size"]:
|
||||
policy.alpha_prime_optim.zero_grad()
|
||||
alpha_prime_loss.backward()
|
||||
policy.alpha_prime_optim.step()
|
||||
|
||||
# Return all loss terms corresponding to our optimizers.
|
||||
return tuple(
|
||||
[actor_loss]
|
||||
+ critic_loss
|
||||
+ [alpha_loss]
|
||||
+ ([alpha_prime_loss] if use_lagrange else [])
|
||||
)
|
||||
|
||||
|
||||
def cql_stats(policy: Policy, train_batch: SampleBatch) -> Dict[str, TensorType]:
|
||||
# Get SAC loss stats.
|
||||
stats_dict = stats(policy, train_batch)
|
||||
|
||||
# Add CQL loss stats to the dict.
|
||||
stats_dict["cql_loss"] = torch.mean(
|
||||
torch.stack(*policy.get_tower_stats("cql_loss"))
|
||||
)
|
||||
|
||||
if policy.config["lagrangian"]:
|
||||
stats_dict["log_alpha_prime_value"] = torch.mean(
|
||||
torch.stack(policy.get_tower_stats("log_alpha_prime_value"))
|
||||
)
|
||||
stats_dict["alpha_prime_value"] = torch.mean(
|
||||
torch.stack(policy.get_tower_stats("alpha_prime_value"))
|
||||
)
|
||||
stats_dict["alpha_prime_loss"] = torch.mean(
|
||||
torch.stack(policy.get_tower_stats("alpha_prime_loss"))
|
||||
)
|
||||
return stats_dict
|
||||
|
||||
|
||||
def cql_optimizer_fn(
|
||||
policy: Policy, config: AlgorithmConfigDict
|
||||
) -> Tuple[LocalOptimizer]:
|
||||
policy.cur_iter = 0
|
||||
opt_list = optimizer_fn(policy, config)
|
||||
if config["lagrangian"]:
|
||||
log_alpha_prime = nn.Parameter(torch.zeros(1, requires_grad=True).float())
|
||||
policy.model.register_parameter("log_alpha_prime", log_alpha_prime)
|
||||
policy.alpha_prime_optim = torch.optim.Adam(
|
||||
params=[policy.model.log_alpha_prime],
|
||||
lr=config["optimization"]["critic_learning_rate"],
|
||||
eps=1e-7, # to match tf.keras.optimizers.Adam's epsilon default
|
||||
)
|
||||
return tuple(
|
||||
[policy.actor_optim]
|
||||
+ policy.critic_optims
|
||||
+ [policy.alpha_optim]
|
||||
+ [policy.alpha_prime_optim]
|
||||
)
|
||||
return opt_list
|
||||
|
||||
|
||||
def cql_setup_late_mixins(
|
||||
policy: Policy,
|
||||
obs_space: gym.spaces.Space,
|
||||
action_space: gym.spaces.Space,
|
||||
config: AlgorithmConfigDict,
|
||||
) -> None:
|
||||
setup_late_mixins(policy, obs_space, action_space, config)
|
||||
if config["lagrangian"]:
|
||||
policy.model.log_alpha_prime = policy.model.log_alpha_prime.to(policy.device)
|
||||
|
||||
|
||||
def compute_gradients_fn(policy, postprocessed_batch):
|
||||
batches = [policy._lazy_tensor_dict(postprocessed_batch)]
|
||||
model = policy.model
|
||||
policy._loss(policy, model, policy.dist_class, batches[0])
|
||||
stats = {LEARNER_STATS_KEY: policy._convert_to_numpy(cql_stats(policy, batches[0]))}
|
||||
return [None, stats]
|
||||
|
||||
|
||||
def apply_gradients_fn(policy, gradients):
|
||||
return
|
||||
|
||||
|
||||
# Build a child class of `TorchPolicy`, given the custom functions defined
|
||||
# above.
|
||||
CQLTorchPolicy = build_policy_class(
|
||||
name="CQLTorchPolicy",
|
||||
framework="torch",
|
||||
loss_fn=cql_loss,
|
||||
get_default_config=lambda: ray.rllib.algorithms.cql.cql.CQLConfig(),
|
||||
stats_fn=cql_stats,
|
||||
postprocess_fn=postprocess_trajectory,
|
||||
extra_grad_process_fn=apply_grad_clipping,
|
||||
optimizer_fn=cql_optimizer_fn,
|
||||
validate_spaces=validate_spaces,
|
||||
before_loss_init=cql_setup_late_mixins,
|
||||
make_model_and_action_dist=build_sac_model_and_action_dist,
|
||||
extra_learn_fetches_fn=concat_multi_gpu_td_errors,
|
||||
mixins=[TargetNetworkMixin, ComputeTDErrorMixin],
|
||||
action_distribution_fn=action_distribution_fn,
|
||||
compute_gradients_fn=compute_gradients_fn,
|
||||
apply_gradients_fn=apply_gradients_fn,
|
||||
)
|
||||
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms import cql
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.metrics import (
|
||||
ENV_RUNNER_RESULTS,
|
||||
EPISODE_RETURN_MEAN,
|
||||
EVALUATION_RESULTS,
|
||||
)
|
||||
from ray.rllib.utils.test_utils import check_compute_single_action, check_train_results
|
||||
|
||||
torch, _ = try_import_torch()
|
||||
|
||||
|
||||
class TestCQL(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_cql_compilation(self):
|
||||
"""Test whether CQL can be built with all frameworks."""
|
||||
|
||||
# Learns from a historic-data file.
|
||||
# To generate this data, first run:
|
||||
# $ ./train.py --run=SAC --env=Pendulum-v1 \
|
||||
# --stop='{"timesteps_total": 50000}' \
|
||||
# --config='{"output": "/tmp/out"}'
|
||||
rllib_dir = Path(__file__).parent.parent.parent.parent
|
||||
print("rllib dir={}".format(rllib_dir))
|
||||
data_file = os.path.join(rllib_dir, "offline/tests/data/pendulum/small.json")
|
||||
print("data_file={} exists={}".format(data_file, os.path.isfile(data_file)))
|
||||
|
||||
config = (
|
||||
cql.CQLConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=False,
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
)
|
||||
.environment(
|
||||
env="Pendulum-v1",
|
||||
)
|
||||
.offline_data(
|
||||
input_=data_file,
|
||||
# In the files, we use here for testing, actions have already
|
||||
# been normalized.
|
||||
# This is usually the case when the file was generated by another
|
||||
# RLlib algorithm (e.g. PPO or SAC).
|
||||
actions_in_input_normalized=False,
|
||||
)
|
||||
.training(
|
||||
clip_actions=False,
|
||||
train_batch_size=2000,
|
||||
twin_q=True,
|
||||
num_steps_sampled_before_learning_starts=0,
|
||||
bc_iters=2,
|
||||
)
|
||||
.evaluation(
|
||||
evaluation_interval=2,
|
||||
evaluation_duration=10,
|
||||
evaluation_config=cql.CQLConfig.overrides(input_="sampler"),
|
||||
evaluation_parallel_to_training=False,
|
||||
evaluation_num_env_runners=2,
|
||||
)
|
||||
.env_runners(num_env_runners=0)
|
||||
.reporting(min_time_s_per_iteration=0)
|
||||
)
|
||||
num_iterations = 4
|
||||
|
||||
algo = config.build()
|
||||
for i in range(num_iterations):
|
||||
results = algo.train()
|
||||
check_train_results(results)
|
||||
print(results)
|
||||
eval_results = results.get(EVALUATION_RESULTS)
|
||||
if eval_results:
|
||||
print(
|
||||
f"iter={algo.iteration} "
|
||||
f"R={eval_results[ENV_RUNNER_RESULTS][EPISODE_RETURN_MEAN]}"
|
||||
)
|
||||
check_compute_single_action(algo)
|
||||
|
||||
# Get policy and model.
|
||||
pol = algo.get_policy()
|
||||
cql_model = pol.model
|
||||
|
||||
# Example on how to do evaluation on the trained Algorithm
|
||||
# using the data from CQL's global replay buffer.
|
||||
# Get a sample (MultiAgentBatch).
|
||||
|
||||
batch = algo.env_runner.input_reader.next()
|
||||
multi_agent_batch = batch.as_multi_agent()
|
||||
# All experiences have been buffered for `default_policy`
|
||||
batch = multi_agent_batch.policy_batches["default_policy"]
|
||||
|
||||
obs = torch.from_numpy(batch["obs"])
|
||||
|
||||
# Pass the observations through our model to get the
|
||||
# features, which then to pass through the Q-head.
|
||||
model_out, _ = cql_model({"obs": obs})
|
||||
# The estimated Q-values from the (historic) actions in the batch.
|
||||
q_values_old = cql_model.get_q_values(
|
||||
model_out, torch.from_numpy(batch["actions"])
|
||||
)
|
||||
|
||||
# The estimated Q-values for the new actions computed
|
||||
# by our policy.
|
||||
actions_new = pol.compute_actions_from_input_dict({"obs": obs})[0]
|
||||
q_values_new = cql_model.get_q_values(model_out, torch.from_numpy(actions_new))
|
||||
|
||||
print(f"Q-val batch={q_values_old}")
|
||||
print(f"Q-val policy={q_values_new}")
|
||||
|
||||
algo.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,280 @@
|
||||
from typing import Dict
|
||||
|
||||
from ray.rllib.algorithms.cql.cql import CQLConfig
|
||||
from ray.rllib.algorithms.sac.sac_learner import (
|
||||
LOGPS_KEY,
|
||||
QF_LOSS_KEY,
|
||||
QF_MAX_KEY,
|
||||
QF_MEAN_KEY,
|
||||
QF_MIN_KEY,
|
||||
QF_PREDS,
|
||||
QF_TWIN_LOSS_KEY,
|
||||
QF_TWIN_PREDS,
|
||||
TD_ERROR_MEAN_KEY,
|
||||
)
|
||||
from ray.rllib.algorithms.sac.torch.sac_torch_learner import SACTorchLearner
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.learner.learner import (
|
||||
POLICY_LOSS_KEY,
|
||||
)
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.rllib.utils.metrics import ALL_MODULES
|
||||
from ray.rllib.utils.typing import ModuleID, ParamDict, TensorType
|
||||
from ray.tune.result import TRAINING_ITERATION
|
||||
|
||||
torch, nn = try_import_torch()
|
||||
|
||||
|
||||
class CQLTorchLearner(SACTorchLearner):
|
||||
@override(SACTorchLearner)
|
||||
def compute_loss_for_module(
|
||||
self,
|
||||
*,
|
||||
module_id: ModuleID,
|
||||
config: CQLConfig,
|
||||
batch: Dict,
|
||||
fwd_out: Dict[str, TensorType],
|
||||
) -> TensorType:
|
||||
|
||||
# TODO (simon, sven): Add upstream information pieces into this timesteps
|
||||
# call arg to Learner.update_...().
|
||||
self.metrics.log_value(
|
||||
(ALL_MODULES, TRAINING_ITERATION),
|
||||
1,
|
||||
reduce="sum",
|
||||
)
|
||||
# Get the train action distribution for the current policy and current state.
|
||||
# This is needed for the policy (actor) loss and the `alpha`` loss.
|
||||
action_dist_class = self.module[module_id].get_train_action_dist_cls()
|
||||
action_dist_curr = action_dist_class.from_logits(
|
||||
fwd_out[Columns.ACTION_DIST_INPUTS]
|
||||
)
|
||||
|
||||
# Optimize also the hyperparameter `alpha` by using the current policy
|
||||
# evaluated at the current state (from offline data). Note, in contrast
|
||||
# to the original SAC loss, here the `alpha` and actor losses are
|
||||
# calculated first.
|
||||
# TODO (simon): Check, why log(alpha) is used, prob. just better
|
||||
# to optimize and monotonic function. Original equation uses alpha.
|
||||
alpha_loss = -torch.mean(
|
||||
self.curr_log_alpha[module_id]
|
||||
* (fwd_out["logp_resampled"].detach() + self.target_entropy[module_id])
|
||||
)
|
||||
|
||||
# Get the current alpha.
|
||||
alpha = torch.exp(self.curr_log_alpha[module_id])
|
||||
# Start training with behavior cloning and turn to the classic Soft-Actor Critic
|
||||
# after `bc_iters` of training iterations.
|
||||
if (
|
||||
self.metrics.peek((ALL_MODULES, TRAINING_ITERATION), default=0)
|
||||
>= config.bc_iters
|
||||
):
|
||||
actor_loss = torch.mean(
|
||||
alpha.detach() * fwd_out["logp_resampled"] - fwd_out["q_curr"]
|
||||
)
|
||||
else:
|
||||
# Use log-probabilities of the current action distribution to clone
|
||||
# the behavior policy (selected actions in data) in the first `bc_iters`
|
||||
# training iterations.
|
||||
bc_logps_curr = action_dist_curr.logp(batch[Columns.ACTIONS])
|
||||
actor_loss = torch.mean(
|
||||
alpha.detach() * fwd_out["logp_resampled"] - bc_logps_curr
|
||||
)
|
||||
|
||||
# The critic loss is composed of the standard SAC Critic L2 loss and the
|
||||
# CQL entropy loss.
|
||||
|
||||
# Get the Q-values for the actually selected actions in the offline data.
|
||||
# In the critic loss we use these as predictions.
|
||||
q_selected = fwd_out[QF_PREDS]
|
||||
if config.twin_q:
|
||||
q_twin_selected = fwd_out[QF_TWIN_PREDS]
|
||||
|
||||
if not config.deterministic_backup:
|
||||
q_next = (
|
||||
fwd_out["q_target_next"]
|
||||
- alpha.detach() * fwd_out["logp_next_resampled"]
|
||||
)
|
||||
else:
|
||||
q_next = fwd_out["q_target_next"]
|
||||
|
||||
# Now mask all Q-values with terminating next states in the targets.
|
||||
q_next_masked = (1.0 - batch[Columns.TERMINATEDS].float()) * q_next
|
||||
|
||||
# Compute the right hand side of the Bellman equation. Detach this node
|
||||
# from the computation graph as we do not want to backpropagate through
|
||||
# the target network when optimizing the Q loss.
|
||||
# TODO (simon, sven): Kumar et al. (2020) use here also a reward scaler.
|
||||
q_selected_target = (
|
||||
# TODO (simon): Add an `n_step` option to the `AddNextObsToBatch` connector.
|
||||
batch[Columns.REWARDS]
|
||||
# TODO (simon): Implement n_step.
|
||||
+ (config.gamma) * q_next_masked
|
||||
).detach()
|
||||
|
||||
# Calculate the TD error.
|
||||
td_error = torch.abs(q_selected - q_selected_target)
|
||||
# Calculate a TD-error for twin-Q values, if needed.
|
||||
if config.twin_q:
|
||||
td_error += torch.abs(q_twin_selected - q_selected_target)
|
||||
# Rescale the TD error
|
||||
td_error *= 0.5
|
||||
|
||||
# MSBE loss for the critic(s) (i.e. Q, see eqs. (7-8) Haarnoja et al. (2018)).
|
||||
# Note, this needs a sample from the current policy given the next state.
|
||||
# Note further, we could also use here the Huber loss instead of the MSE.
|
||||
# TODO (simon): Add the huber loss as an alternative (SAC uses it).
|
||||
sac_critic_loss = torch.nn.MSELoss(reduction="mean")(
|
||||
q_selected,
|
||||
q_selected_target,
|
||||
)
|
||||
if config.twin_q:
|
||||
sac_critic_twin_loss = torch.nn.MSELoss(reduction="mean")(
|
||||
q_twin_selected,
|
||||
q_selected_target,
|
||||
)
|
||||
|
||||
# Now calculate the CQL loss (we use the entropy version of the CQL algorithm).
|
||||
# Note, the entropy version performs best in shown experiments.
|
||||
|
||||
# Compute the log-probabilities for the random actions (note, we generate random
|
||||
# actions (from the mu distribution as named in Kumar et al. (2020))).
|
||||
# Note, all actions, action log-probabilities and Q-values are already computed
|
||||
# by the module's `_forward_train` method.
|
||||
# TODO (simon): This is the density for a discrete uniform, however, actions
|
||||
# come from a continuous one. So actually this density should use (1/(high-low))
|
||||
# instead of (1/2).
|
||||
random_density = torch.log(
|
||||
torch.pow(
|
||||
0.5,
|
||||
torch.tensor(
|
||||
fwd_out["actions_curr_repeat"].shape[-1],
|
||||
device=fwd_out["actions_curr_repeat"].device,
|
||||
),
|
||||
)
|
||||
)
|
||||
# Merge all Q-values and subtract the log-probabilities (note, we use the
|
||||
# entropy version of CQL).
|
||||
q_repeat = torch.cat(
|
||||
[
|
||||
fwd_out["q_rand_repeat"] - random_density,
|
||||
fwd_out["q_next_repeat"] - fwd_out["logps_next_repeat"].detach(),
|
||||
fwd_out["q_curr_repeat"] - fwd_out["logps_curr_repeat"].detach(),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
cql_loss = (
|
||||
torch.logsumexp(q_repeat / config.temperature, dim=1).mean()
|
||||
* config.min_q_weight
|
||||
* config.temperature
|
||||
)
|
||||
cql_loss -= q_selected.mean() * config.min_q_weight
|
||||
# Add the CQL loss term to the SAC loss term.
|
||||
critic_loss = sac_critic_loss + cql_loss
|
||||
|
||||
# If a twin Q-value function is implemented calculated its CQL loss.
|
||||
if config.twin_q:
|
||||
q_twin_repeat = torch.cat(
|
||||
[
|
||||
fwd_out["q_twin_rand_repeat"] - random_density,
|
||||
fwd_out["q_twin_next_repeat"]
|
||||
- fwd_out["logps_next_repeat"].detach(),
|
||||
fwd_out["q_twin_curr_repeat"]
|
||||
- fwd_out["logps_curr_repeat"].detach(),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
cql_twin_loss = (
|
||||
torch.logsumexp(q_twin_repeat / config.temperature, dim=1).mean()
|
||||
* config.min_q_weight
|
||||
* config.temperature
|
||||
)
|
||||
cql_twin_loss -= q_twin_selected.mean() * config.min_q_weight
|
||||
# Add the CQL loss term to the SAC loss term.
|
||||
critic_twin_loss = sac_critic_twin_loss + cql_twin_loss
|
||||
|
||||
# TODO (simon): Check, if we need to implement here also a Lagrangian
|
||||
# loss.
|
||||
|
||||
total_loss = actor_loss + critic_loss + alpha_loss
|
||||
|
||||
# Add the twin critic loss to the total loss, if needed.
|
||||
if config.twin_q:
|
||||
# Reweigh the critic loss terms in the total loss.
|
||||
total_loss += 0.5 * critic_twin_loss - 0.5 * critic_loss
|
||||
|
||||
# Log important loss stats (reduce=mean (default), but with window=1
|
||||
# in order to keep them history free).
|
||||
self.metrics.log_dict(
|
||||
{
|
||||
POLICY_LOSS_KEY: actor_loss,
|
||||
QF_LOSS_KEY: critic_loss,
|
||||
# TODO (simon): Add these keys to SAC Learner.
|
||||
"cql_loss": cql_loss,
|
||||
"alpha_loss": alpha_loss,
|
||||
"alpha_value": alpha[0],
|
||||
"log_alpha_value": torch.log(alpha)[0],
|
||||
"target_entropy": self.target_entropy[module_id],
|
||||
LOGPS_KEY: torch.mean(
|
||||
fwd_out["logp_resampled"]
|
||||
), # torch.mean(logps_curr),
|
||||
QF_MEAN_KEY: torch.mean(fwd_out["q_curr_repeat"]),
|
||||
QF_MAX_KEY: torch.max(fwd_out["q_curr_repeat"]),
|
||||
QF_MIN_KEY: torch.min(fwd_out["q_curr_repeat"]),
|
||||
TD_ERROR_MEAN_KEY: torch.mean(td_error),
|
||||
},
|
||||
key=module_id,
|
||||
window=1, # <- single items (should not be mean/ema-reduced over time).
|
||||
)
|
||||
self._temp_losses[(module_id, POLICY_LOSS_KEY)] = actor_loss
|
||||
self._temp_losses[(module_id, QF_LOSS_KEY)] = critic_loss
|
||||
self._temp_losses[(module_id, "alpha_loss")] = alpha_loss
|
||||
|
||||
# TODO (simon): Add loss keys for langrangian, if needed.
|
||||
# TODO (simon): Add only here then the Langrange parameter optimization.
|
||||
if config.twin_q:
|
||||
self.metrics.log_value(
|
||||
key=(module_id, QF_TWIN_LOSS_KEY),
|
||||
value=critic_twin_loss,
|
||||
window=1, # <- single items (should not be mean/ema-reduced over time).
|
||||
)
|
||||
self._temp_losses[(module_id, QF_TWIN_LOSS_KEY)] = critic_twin_loss
|
||||
|
||||
# Return the total loss.
|
||||
return total_loss
|
||||
|
||||
@override(SACTorchLearner)
|
||||
def compute_gradients(
|
||||
self, loss_per_module: Dict[ModuleID, TensorType], **kwargs
|
||||
) -> ParamDict:
|
||||
|
||||
grads = {}
|
||||
for module_id in set(loss_per_module.keys()) - {ALL_MODULES}:
|
||||
# Loop through optimizers registered for this module.
|
||||
for optim_name, optim in self.get_optimizers_for_module(module_id):
|
||||
# Zero the gradients. Note, we need to reset the gradients b/c
|
||||
# each component for a module operates on the same graph.
|
||||
optim.zero_grad(set_to_none=True)
|
||||
|
||||
# Compute the gradients for the component and module.
|
||||
loss_tensor = self._temp_losses.pop((module_id, optim_name + "_loss"))
|
||||
loss_tensor.backward(
|
||||
retain_graph=False if optim_name in ["policy", "alpha"] else True
|
||||
)
|
||||
# Store the gradients for the component and module.
|
||||
# TODO (simon): Check another time the graph for overlapping
|
||||
# gradients.
|
||||
grads.update(
|
||||
{
|
||||
pid: grads[pid] + p.grad.clone()
|
||||
if pid in grads
|
||||
else p.grad.clone()
|
||||
for pid, p in self.filter_param_dict_for_optimizer(
|
||||
self._params, optim
|
||||
).items()
|
||||
}
|
||||
)
|
||||
|
||||
assert not self._temp_losses
|
||||
return grads
|
||||
@@ -0,0 +1,207 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import tree
|
||||
|
||||
from ray.rllib.algorithms.sac.sac_catalog import SACCatalog
|
||||
from ray.rllib.algorithms.sac.sac_learner import (
|
||||
QF_PREDS,
|
||||
QF_TWIN_PREDS,
|
||||
)
|
||||
from ray.rllib.algorithms.sac.torch.default_sac_torch_rl_module import (
|
||||
DefaultSACTorchRLModule,
|
||||
)
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.models.base import ENCODER_OUT
|
||||
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 DefaultCQLTorchRLModule(DefaultSACTorchRLModule):
|
||||
def __init__(self, *args, **kwargs):
|
||||
catalog_class = kwargs.pop("catalog_class", None)
|
||||
if catalog_class is None:
|
||||
catalog_class = SACCatalog
|
||||
super().__init__(*args, **kwargs, catalog_class=catalog_class)
|
||||
|
||||
@override(DefaultSACTorchRLModule)
|
||||
def _forward_train(self, batch: Dict) -> Dict[str, Any]:
|
||||
# Call the super method.
|
||||
fwd_out = super()._forward_train(batch)
|
||||
|
||||
# Make sure we perform a "straight-through gradient" pass here,
|
||||
# ignoring the gradients of the q-net, however, still recording
|
||||
# the gradients of the policy net (which was used to rsample the actions used
|
||||
# here). This is different from doing `.detach()` or `with torch.no_grads()`,
|
||||
# as these two methds would fully block all gradient recordings, including
|
||||
# the needed policy ones.
|
||||
all_params = list(self.pi_encoder.parameters()) + list(self.pi.parameters())
|
||||
# if self.twin_q:
|
||||
# all_params += list(self.qf_twin.parameters()) + list(
|
||||
# self.qf_twin_encoder.parameters()
|
||||
# )
|
||||
|
||||
for param in all_params:
|
||||
param.requires_grad = False
|
||||
|
||||
# Compute the repeated actions, action log-probabilites and Q-values for all
|
||||
# observations.
|
||||
# First for the random actions (from the mu-distribution as named by Kumar et
|
||||
# al. (2020)).
|
||||
low = torch.tensor(
|
||||
self.action_space.low,
|
||||
device=fwd_out[QF_PREDS].device,
|
||||
)
|
||||
high = torch.tensor(
|
||||
self.action_space.high,
|
||||
device=fwd_out[QF_PREDS].device,
|
||||
)
|
||||
num_samples = batch[Columns.ACTIONS].shape[0] * self.model_config["num_actions"]
|
||||
actions_rand_repeat = low + (high - low) * torch.rand(
|
||||
(num_samples, low.shape[0]), device=fwd_out[QF_PREDS].device
|
||||
)
|
||||
|
||||
# First for the random actions (from the mu-distribution as named in Kumar
|
||||
# et al. (2020)) using repeated observations.
|
||||
rand_repeat_out = self._repeat_actions(batch[Columns.OBS], actions_rand_repeat)
|
||||
(fwd_out["actions_rand_repeat"], fwd_out["q_rand_repeat"]) = (
|
||||
rand_repeat_out[Columns.ACTIONS],
|
||||
rand_repeat_out[QF_PREDS],
|
||||
)
|
||||
# Sample current and next actions (from the pi distribution as named in Kumar
|
||||
# et al. (2020)) using repeated observations
|
||||
# Second for the current observations and the current action distribution.
|
||||
curr_repeat_out = self._repeat_actions(batch[Columns.OBS])
|
||||
(
|
||||
fwd_out["actions_curr_repeat"],
|
||||
fwd_out["logps_curr_repeat"],
|
||||
fwd_out["q_curr_repeat"],
|
||||
) = (
|
||||
curr_repeat_out[Columns.ACTIONS],
|
||||
curr_repeat_out[Columns.ACTION_LOGP],
|
||||
curr_repeat_out[QF_PREDS],
|
||||
)
|
||||
# Then, for the next observations and the current action distribution.
|
||||
next_repeat_out = self._repeat_actions(batch[Columns.NEXT_OBS])
|
||||
(
|
||||
fwd_out["actions_next_repeat"],
|
||||
fwd_out["logps_next_repeat"],
|
||||
fwd_out["q_next_repeat"],
|
||||
) = (
|
||||
next_repeat_out[Columns.ACTIONS],
|
||||
next_repeat_out[Columns.ACTION_LOGP],
|
||||
next_repeat_out[QF_PREDS],
|
||||
)
|
||||
if self.twin_q:
|
||||
# First for the random actions from the mu-distribution.
|
||||
fwd_out["q_twin_rand_repeat"] = rand_repeat_out[QF_TWIN_PREDS]
|
||||
# Second for the current observations and the current action distribution.
|
||||
fwd_out["q_twin_curr_repeat"] = curr_repeat_out[QF_TWIN_PREDS]
|
||||
# Then, for the next observations and the current action distribution.
|
||||
fwd_out["q_twin_next_repeat"] = next_repeat_out[QF_TWIN_PREDS]
|
||||
# Reset the gradient requirements for all Q-function parameters.
|
||||
for param in all_params:
|
||||
param.requires_grad = True
|
||||
|
||||
return fwd_out
|
||||
|
||||
def _repeat_tensor(self, tensor: TensorType, repeat: int) -> TensorType:
|
||||
"""Generates a repeated version of a tensor.
|
||||
|
||||
The repetition is done similar `np.repeat` and repeats each value
|
||||
instead of the complete vector.
|
||||
|
||||
Args:
|
||||
tensor: The tensor to be repeated.
|
||||
repeat: How often each value in the tensor should be repeated.
|
||||
|
||||
Returns:
|
||||
A tensor holding `repeat` repeated values of the input `tensor`
|
||||
"""
|
||||
# Insert the new dimension at axis 1 into the tensor.
|
||||
t_repeat = tensor.unsqueeze(1)
|
||||
# Repeat the tensor along the new dimension.
|
||||
t_repeat = torch.repeat_interleave(t_repeat, repeat, dim=1)
|
||||
# Stack the repeated values into the batch dimension.
|
||||
t_repeat = t_repeat.view(-1, *tensor.shape[1:])
|
||||
# Return the repeated tensor.
|
||||
return t_repeat
|
||||
|
||||
def _repeat_actions(
|
||||
self, obs: TensorType, actions: Optional[TensorType] = None
|
||||
) -> Dict[str, TensorType]:
|
||||
"""Generated actions and Q-values for repeated observations.
|
||||
|
||||
The `self.model_config["num_actions"]` define a multiplier
|
||||
used for generating `num_actions` as many actions as the batch size.
|
||||
Observations are repeated and then a model forward pass is made.
|
||||
|
||||
Args:
|
||||
obs: A batched observation tensor.
|
||||
actions: An optional batched actions tensor.
|
||||
|
||||
Returns:
|
||||
A dictionary holding the (sampled or passed-in actions), the log
|
||||
probabilities (of sampled actions), the Q-values and if available
|
||||
the twin-Q values.
|
||||
"""
|
||||
output = {}
|
||||
# Receive the batch size.
|
||||
batch_size = obs.shape[0]
|
||||
# Receive the number of action to sample.
|
||||
num_actions = self.model_config["num_actions"]
|
||||
# Repeat the observations `num_actions` times.
|
||||
obs_repeat = tree.map_structure(
|
||||
lambda t: self._repeat_tensor(t, num_actions), obs
|
||||
)
|
||||
# Generate a batch for the forward pass.
|
||||
temp_batch = {Columns.OBS: obs_repeat}
|
||||
if actions is None:
|
||||
# TODO (simon): Run the forward pass in inference mode.
|
||||
# Compute the action logits.
|
||||
pi_encoder_outs = self.pi_encoder(temp_batch)
|
||||
action_logits = self.pi(pi_encoder_outs[ENCODER_OUT])
|
||||
# Generate the squashed Gaussian from the model's logits.
|
||||
action_dist = self.get_train_action_dist_cls().from_logits(action_logits)
|
||||
# Sample the actions. Note, we want to make a backward pass through
|
||||
# these actions.
|
||||
output[Columns.ACTIONS] = action_dist.rsample()
|
||||
# Compute the action log-probabilities.
|
||||
output[Columns.ACTION_LOGP] = action_dist.logp(
|
||||
output[Columns.ACTIONS]
|
||||
).view(batch_size, num_actions, 1)
|
||||
else:
|
||||
output[Columns.ACTIONS] = actions
|
||||
|
||||
# Compute all Q-values.
|
||||
temp_batch.update(
|
||||
{
|
||||
Columns.ACTIONS: output[Columns.ACTIONS],
|
||||
}
|
||||
)
|
||||
output.update(
|
||||
{
|
||||
QF_PREDS: self._qf_forward_train_helper(
|
||||
temp_batch,
|
||||
self.qf_encoder,
|
||||
self.qf,
|
||||
).view(batch_size, num_actions, 1)
|
||||
}
|
||||
)
|
||||
# If we have a twin-Q network, compute its Q-values, too.
|
||||
if self.twin_q:
|
||||
output.update(
|
||||
{
|
||||
QF_TWIN_PREDS: self._qf_forward_train_helper(
|
||||
temp_batch,
|
||||
self.qf_twin_encoder,
|
||||
self.qf_twin,
|
||||
).view(batch_size, num_actions, 1)
|
||||
}
|
||||
)
|
||||
del temp_batch
|
||||
|
||||
# Return
|
||||
return output
|
||||
Reference in New Issue
Block a user