Rate this Page

Source code for torchrl.objectives.rnd

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

import torch
import torch.nn as nn
from tensordict import TensorDict, TensorDictBase
from tensordict.utils import NestedKey

from torchrl.envs.transforms.rnd import RunningMeanStd
from torchrl.objectives.common import LossModule


[docs] class RNDLoss(LossModule): """Loss module for training the predictor network in Random Network Distillation. Presented in: Burda et al., "Exploration by Random Network Distillation" (2018). https://arxiv.org/abs/1810.12894 Computes the MSE between the *predictor* and the frozen *target* network on next observations sampled from a replay buffer. Call this loss alongside your main policy objective; its gradients update the predictor so that familiar observations gradually yield lower intrinsic rewards. The :attr:`predictor_network` and :attr:`target_network` should be the **same objects** passed to :class:`~torchrl.envs.transforms.RNDTransform` so that reducing the predictor error here also reduces the intrinsic reward produced during collection. Observation normalization is optionally applied using the running statistics maintained by :class:`~torchrl.envs.transforms.RNDTransform`. Pass ``obs_rms=transform.obs_rms`` after collecting initial data to keep the normalization consistent between collection and training. Args: predictor_network (torch.nn.Module): trainable network. target_network (torch.nn.Module): frozen random network. Its parameters are frozen on construction. Keyword Args: obs_rms (RunningMeanStd, optional): running observation statistics shared with :class:`~torchrl.envs.transforms.RNDTransform`. When provided, observations are normalized before being passed to the networks, matching the normalization done during collection. Defaults to ``None`` (no normalization). obs_clip (float, optional): clip normalized observations to ``[-obs_clip, obs_clip]``. Only used when ``obs_rms`` is not ``None``. Default: ``5.0``. reduction (str, optional): reduction over the per-sample losses: ``"mean"`` | ``"sum"`` | ``"none"``. Default: ``"mean"``. update_fraction (float, optional): fraction of each batch used to compute the predictor loss, following the original paper (default 25 %). A random mask selects which samples contribute so the operation is ``torch.compile``-friendly. Default: ``0.25``. Examples: >>> import torch >>> import torch.nn as nn >>> from tensordict import TensorDict >>> from torchrl.objectives.rnd import RNDLoss >>> predictor = nn.Sequential(nn.Linear(4, 64), nn.ReLU(), nn.Linear(64, 64)) >>> target = nn.Sequential(nn.Linear(4, 64), nn.ReLU(), nn.Linear(64, 64)) >>> loss_fn = RNDLoss(predictor, target) >>> batch = TensorDict({"next": {"observation": torch.randn(32, 4)}}, [32]) >>> loss_td = loss_fn(batch) >>> loss_td["loss_predictor"].backward() """ @dataclass class _AcceptedKeys: """Maintains default values for all configurable tensordict keys. Attributes: observation (NestedKey): key where the next observation is read from. Defaults to ``("next", "observation")``, matching the nested format produced by replay buffers. """ observation: NestedKey = ("next", "observation") default_keys = _AcceptedKeys tensor_keys: _AcceptedKeys out_keys = ["loss_predictor"] def __init__( self, predictor_network: nn.Module, target_network: nn.Module, *, obs_rms: RunningMeanStd | None = None, obs_clip: float = 5.0, reduction: Literal["mean", "sum", "none"] = "mean", update_fraction: float = 0.25, ): super().__init__() if not 0.0 <= update_fraction <= 1.0: raise ValueError( f"update_fraction must be in the [0, 1] interval, got {update_fraction}." ) self.predictor_network = predictor_network self.target_network = target_network self.target_network.requires_grad_(False) self.obs_rms = obs_rms self.obs_clip = obs_clip self.reduction = reduction self.update_fraction = update_fraction def _forward_value_estimator_keys(self, **kwargs) -> None: pass
[docs] def forward(self, tensordict: TensorDictBase) -> TensorDictBase: obs = tensordict.get(self.tensor_keys.observation).float() if self.obs_rms is not None: obs = self.obs_rms.normalize(obs).clamp(-self.obs_clip, self.obs_clip) with torch.no_grad(): target_feat = self.target_network(obs) pred_feat = self.predictor_network(obs) per_sample_loss = (pred_feat - target_feat).pow(2).mean(dim=-1) mask = None if self.update_fraction < 1.0: mask = ( torch.rand(per_sample_loss.shape, device=per_sample_loss.device) < self.update_fraction ) per_sample_loss = torch.where( mask, per_sample_loss, torch.zeros_like(per_sample_loss) ) if self.reduction == "none": mask = None loss = self._reduce_loss(per_sample_loss, tensordict=tensordict, mask=mask) return TensorDict({"loss_predictor": loss}, batch_size=[])