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

195 lines
7.6 KiB
Python

from typing import Any, Dict, Optional
from ray.rllib.core.columns import Columns
from ray.rllib.core.learner.utils import make_target_network
from ray.rllib.core.rl_module.apis import (
TARGET_NETWORK_ACTION_DIST_INPUTS,
TargetNetworkAPI,
ValueFunctionAPI,
)
from ray.rllib.core.rl_module.torch import TorchRLModule
from ray.rllib.models.torch.misc import (
normc_initializer,
same_padding,
valid_padding,
)
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 TinyAtariCNN(TorchRLModule, ValueFunctionAPI, TargetNetworkAPI):
"""A tiny CNN stack for fast-learning of Atari envs.
The architecture here is the exact same as the one used by the old API stack as
CNN default ModelV2.
We stack 3 CNN layers based on the config, then a 4th one with linear activation
and n 1x1 filters, where n is the number of actions in the (discrete) action space.
Simple reshaping (no flattening or extra linear layers necessary) lead to the
action logits, which can directly be used inside a distribution or loss.
.. testcode::
import numpy as np
import gymnasium as gym
my_net = TinyAtariCNN(
observation_space=gym.spaces.Box(-1.0, 1.0, (42, 42, 4), np.float32),
action_space=gym.spaces.Discrete(4),
)
B = 10
w = 42
h = 42
c = 4
data = torch.from_numpy(
np.random.random_sample(size=(B, w, h, c)).astype(np.float32)
)
print(my_net.forward_inference({"obs": data}))
print(my_net.forward_exploration({"obs": data}))
print(my_net.forward_train({"obs": data}))
num_all_params = sum(int(np.prod(p.size())) for p in my_net.parameters())
print(f"num params = {num_all_params}")
"""
@override(TorchRLModule)
def setup(self):
"""Use this method to create all the model components that you require.
Feel free to access the following useful properties in this class:
- `self.model_config`: The config dict for this RLModule class,
which should contain flxeible settings, for example: {"hiddens": [256, 256]}.
- `self.observation|action_space`: The observation and action space that
this RLModule is subject to. Note that the observation space might not be the
exact space from your env, but that it might have already gone through
preprocessing through a connector pipeline (for example, flattening,
frame-stacking, mean/std-filtering, etc..).
"""
# Get the CNN stack config from our RLModuleConfig's (self.config)
# `model_config` property:
conv_filters = self.model_config.get("conv_filters")
# Default CNN stack with 3 layers:
if conv_filters is None:
conv_filters = [
[16, 4, 2, "same"], # num filters, kernel wxh, stride wxh, padding type
[32, 4, 2, "same"],
[256, 11, 1, "valid"],
]
# Build the CNN layers.
layers = []
# Add user-specified hidden convolutional layers first
width, height, in_depth = self.observation_space.shape
in_size = [width, height]
for filter_specs in conv_filters:
if len(filter_specs) == 4:
out_depth, kernel_size, strides, padding = filter_specs
else:
out_depth, kernel_size, strides = filter_specs
padding = "same"
# Pad like in tensorflow's SAME mode.
if padding == "same":
padding_size, out_size = same_padding(in_size, kernel_size, strides)
layers.append(nn.ZeroPad2d(padding_size))
# No actual padding is performed for "valid" mode, but we will still
# compute the output size (input for the next layer).
else:
out_size = valid_padding(in_size, kernel_size, strides)
layer = nn.Conv2d(in_depth, out_depth, kernel_size, strides, bias=True)
# Initialize CNN layer kernel and bias.
nn.init.xavier_uniform_(layer.weight)
nn.init.zeros_(layer.bias)
layers.append(layer)
# Activation.
layers.append(nn.ReLU())
in_size = out_size
in_depth = out_depth
self._base_cnn_stack = nn.Sequential(*layers)
# Add the final CNN 1x1 layer with num_filters == num_actions to be reshaped to
# yield the logits (no flattening, no additional linear layers required).
_final_conv = nn.Conv2d(in_depth, self.action_space.n, 1, 1, bias=True)
nn.init.xavier_uniform_(_final_conv.weight)
nn.init.zeros_(_final_conv.bias)
self._logits = nn.Sequential(
nn.ZeroPad2d(same_padding(in_size, 1, 1)[0]), _final_conv
)
self._values = nn.Linear(in_depth, 1)
# Mimick old API stack behavior of initializing the value function with `normc`
# std=0.01.
normc_initializer(0.01)(self._values.weight)
@override(TorchRLModule)
def _forward(self, batch, **kwargs):
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
_, logits = self._compute_embeddings_and_logits(batch)
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
return {
Columns.ACTION_DIST_INPUTS: logits,
}
@override(TorchRLModule)
def _forward_train(self, batch, **kwargs):
# Compute the basic 1D feature tensor (inputs to policy- and value-heads).
embeddings, logits = self._compute_embeddings_and_logits(batch)
# Return features and logits as ACTION_DIST_INPUTS (categorical distribution).
return {
Columns.ACTION_DIST_INPUTS: logits,
Columns.EMBEDDINGS: embeddings,
}
# We implement this RLModule as a TargetNetworkAPI RLModule, so it can be used
# by the APPO algorithm.
@override(TargetNetworkAPI)
def make_target_networks(self) -> None:
self._target_base_cnn_stack = make_target_network(self._base_cnn_stack)
self._target_logits = make_target_network(self._logits)
@override(TargetNetworkAPI)
def get_target_network_pairs(self):
return [
(self._base_cnn_stack, self._target_base_cnn_stack),
(self._logits, self._target_logits),
]
@override(TargetNetworkAPI)
def forward_target(self, batch, **kw):
obs = batch[Columns.OBS].permute(0, 3, 1, 2)
embeddings = self._target_base_cnn_stack(obs)
logits = self._target_logits(embeddings)
return {TARGET_NETWORK_ACTION_DIST_INPUTS: torch.squeeze(logits, dim=[-1, -2])}
# We implement this RLModule as a ValueFunctionAPI RLModule, so it can be used
# by value-based methods like PPO or IMPALA.
@override(ValueFunctionAPI)
def compute_values(
self,
batch: Dict[str, Any],
embeddings: Optional[Any] = None,
) -> TensorType:
# Features not provided -> We need to compute them first.
if embeddings is None:
obs = batch[Columns.OBS]
embeddings = self._base_cnn_stack(obs.permute(0, 3, 1, 2))
embeddings = torch.squeeze(embeddings, dim=[-1, -2])
return self._values(embeddings).squeeze(-1)
def _compute_embeddings_and_logits(self, batch):
obs = batch[Columns.OBS].permute(0, 3, 1, 2)
embeddings = self._base_cnn_stack(obs)
logits = self._logits(embeddings)
return (
torch.squeeze(embeddings, dim=[-1, -2]),
torch.squeeze(logits, dim=[-1, -2]),
)