Files
2026-07-13 13:17:40 +08:00

167 lines
6.6 KiB
Python

import abc
from typing import Any, Dict, List, Tuple, Union
from ray.rllib.core.learner.utils import make_target_network
from ray.rllib.core.models.base import Encoder, Model
from ray.rllib.core.rl_module.apis import InferenceOnlyAPI, QNetAPI, TargetNetworkAPI
from ray.rllib.core.rl_module.rl_module import RLModule
from ray.rllib.utils.annotations import (
OverrideToImplementCustomLogic,
override,
)
from ray.rllib.utils.schedules.scheduler import Scheduler
from ray.rllib.utils.typing import NetworkType, TensorType
from ray.util.annotations import DeveloperAPI
QF_PREDS = "qf_preds"
ATOMS = "atoms"
QF_LOGITS = "qf_logits"
QF_NEXT_PREDS = "qf_next_preds"
QF_PROBS = "qf_probs"
QF_TARGET_NEXT_PREDS = "qf_target_next_preds"
QF_TARGET_NEXT_PROBS = "qf_target_next_probs"
@DeveloperAPI
class DefaultDQNRLModule(RLModule, InferenceOnlyAPI, TargetNetworkAPI, QNetAPI):
@override(RLModule)
def setup(self):
# If a dueling architecture is used.
self.uses_dueling: bool = self.model_config.get("dueling")
# If double Q learning is used.
self.uses_double_q: bool = self.model_config.get("double_q")
# The number of atoms for a distribution support.
self.num_atoms: int = self.model_config.get("num_atoms")
# If distributional learning is requested configure the support.
if self.num_atoms > 1:
self.v_min: float = self.model_config.get("v_min")
self.v_max: float = self.model_config.get("v_max")
# The epsilon scheduler for epsilon greedy exploration.
self.epsilon_schedule = Scheduler(
fixed_value_or_schedule=self.model_config["epsilon"],
framework=self.framework,
)
# Build the encoder for the advantage and value streams. Note,
# the same encoder is used.
# Note further, by using the base encoder the correct encoder
# is chosen for the observation space used.
self.encoder = self.catalog.build_encoder(framework=self.framework)
# Build heads.
self.af = self.catalog.build_af_head(framework=self.framework)
if self.uses_dueling:
# If in a dueling setting setup the value function head.
self.vf = self.catalog.build_vf_head(framework=self.framework)
@override(InferenceOnlyAPI)
def get_non_inference_attributes(self) -> List[str]:
return ["_target_encoder", "_target_af"] + (
["_target_vf"] if self.uses_dueling else []
)
@override(TargetNetworkAPI)
def make_target_networks(self) -> None:
self._target_encoder = make_target_network(self.encoder)
self._target_af = make_target_network(self.af)
if self.uses_dueling:
self._target_vf = make_target_network(self.vf)
@override(TargetNetworkAPI)
def get_target_network_pairs(self) -> List[Tuple[NetworkType, NetworkType]]:
return [(self.encoder, self._target_encoder), (self.af, self._target_af)] + (
# If we have a dueling architecture we need to update the value stream
# target, too.
[
(self.vf, self._target_vf),
]
if self.uses_dueling
else []
)
@override(TargetNetworkAPI)
def forward_target(self, batch: Dict[str, Any]) -> Dict[str, Any]:
"""Computes Q-values from the target network.
Note, these can be accompanied by logits and probabilities
in case of distributional Q-learning, i.e. `self.num_atoms > 1`.
Args:
batch: The batch received in the forward pass.
Results:
A dictionary containing the target Q-value predictions ("qf_preds")
and in case of distributional Q-learning in addition to the target
Q-value predictions ("qf_preds") the support atoms ("atoms"), the target
Q-logits ("qf_logits"), and the probabilities ("qf_probs").
"""
# If we have a dueling architecture we have to add the value stream.
return self._qf_forward_helper(
batch,
self._target_encoder,
(
{"af": self._target_af, "vf": self._target_vf}
if self.uses_dueling
else self._target_af
),
)
@override(QNetAPI)
def compute_q_values(self, batch: Dict[str, TensorType]) -> Dict[str, TensorType]:
"""Computes Q-values, given encoder, q-net and (optionally), advantage net.
Note, these can be accompanied by logits and probabilities
in case of distributional Q-learning, i.e. `self.num_atoms > 1`.
Args:
batch: The batch received in the forward pass.
Results:
A dictionary containing the Q-value predictions ("qf_preds")
and in case of distributional Q-learning - in addition to the Q-value
predictions ("qf_preds") - the support atoms ("atoms"), the Q-logits
("qf_logits"), and the probabilities ("qf_probs").
"""
# If we have a dueling architecture we have to add the value stream.
return self._qf_forward_helper(
batch,
self.encoder,
{"af": self.af, "vf": self.vf} if self.uses_dueling else self.af,
)
@override(RLModule)
def get_initial_state(self) -> dict:
if hasattr(self.encoder, "get_initial_state"):
return self.encoder.get_initial_state()
else:
return {}
@abc.abstractmethod
@OverrideToImplementCustomLogic
def _qf_forward_helper(
self,
batch: Dict[str, TensorType],
encoder: Encoder,
head: Union[Model, Dict[str, Model]],
) -> Dict[str, TensorType]:
"""Computes Q-values.
This is a helper function that takes care of all different cases,
i.e. if we use a dueling architecture or not and if we use distributional
Q-learning or not.
Args:
batch: The batch received in the forward pass.
encoder: The encoder network to use. Here we have a single encoder
for all heads (Q or advantages and value in case of a dueling
architecture).
head: Either a head model or a dictionary of head model (dueling
architecture) containing advantage and value stream heads.
Returns:
In case of expectation learning the Q-value predictions ("qf_preds")
and in case of distributional Q-learning in addition to the predictions
the atoms ("atoms"), the Q-value predictions ("qf_preds"), the Q-logits
("qf_logits") and the probabilities for the support atoms ("qf_probs").
"""