44
55import contextlib
66from typing import Optional , Type
7+ import math
78import random
89from functools import lru_cache
910from abc import ABC , abstractmethod
@@ -458,6 +459,14 @@ class NCriticsTwoHot(BaseCriticHead):
458459 max_return: Maximum return value. If not set, defaults to a very positive value (100_000).
459460 output_bins: Number of bins in the categorical distribution. Defaults to 128.
460461 use_symlog: Whether to use a symlog transformation on the value estimates. Defaults to True.
462+ label_type: How to construct target distributions from scalar values. Options:
463+ - "twohot": Two-hot encoding (Dreamer-V3). Puts mass on the two bins flanking the
464+ target value, weighted by proximity. (Default)
465+ - "hlgauss": Histogram Loss with Gaussian targets (Imani & White, 2018;
466+ Farebrother et al., 2024 "Stop Regressing"). Spreads mass across multiple bins
467+ using a Gaussian kernel centered at the target.
468+ sigma: Ratio of Gaussian standard deviation to bin width for HL-Gauss targets.
469+ init_value: If set, initializes the network to predict this value at the start of training.
461470
462471 Note:
463472 The default bin settings (wide range, lots of bins, symlog transformation) follow
@@ -485,6 +494,9 @@ def __init__(
485494 max_return : Optional [float ] = None ,
486495 output_bins : int = 128 ,
487496 use_symlog : bool = True ,
497+ label_type : str = "twohot" ,
498+ sigma : float = 0.75 ,
499+ init_value : Optional [float ] = None ,
488500 ):
489501 super ().__init__ (
490502 state_dim = state_dim ,
@@ -510,6 +522,9 @@ def __init__(
510522 self .transform_values (max_return ),
511523 output_bins ,
512524 )
525+ assert label_type in ("twohot" , "hlgauss" ), f"Unknown label_type: { label_type } "
526+ self .label_type = label_type
527+ self .sigma = sigma
513528 self .net = _EinMixEnsemble (
514529 ensemble_size = self .num_critics ,
515530 inp_dim = inp_dim ,
@@ -520,9 +535,43 @@ def __init__(
520535 dropout_p = dropout_p ,
521536 )
522537
538+ if init_value is not None :
539+ if not (min_return <= init_value <= max_return ):
540+ raise ValueError (
541+ f"init_value={ init_value } is outside the bin support "
542+ f"[{ min_return } , { max_return } ]"
543+ )
544+ init_bin_idx = (
545+ (self .bin_vals - self .transform_values (init_value ))
546+ .abs ()
547+ .argmin ()
548+ .item ()
549+ )
550+ edge_margin = min (init_bin_idx , output_bins - 1 - init_bin_idx )
551+ if edge_margin < 12 :
552+ amago_warning (
553+ f"init_value={ init_value } is only { edge_margin } bins from the edge "
554+ f"of the support. The initialization Gaussian may be truncated, "
555+ f"slightly biasing the initial prediction inward. Consider widening "
556+ f"[min_return, max_return] or adjusting init_value."
557+ )
558+ self ._initialize_to_value (init_value )
559+
523560 def __len__ (self ):
524561 return self .num_critics
525562
563+ def _initialize_to_value (self , init_value : float ):
564+ """Initialize the output layer bias so the network's initial prediction
565+ is centered near init_value.
566+ """
567+ init_transformed = self .transform_values (init_value )
568+ bin_width = self .bin_vals [1 ] - self .bin_vals [0 ]
569+ sigma_init = 4.0 * bin_width
570+ logits = - ((self .bin_vals - init_transformed ) ** 2 ) / (2.0 * sigma_init ** 2 )
571+ output_layer = self .net .output_layer
572+ with torch .no_grad ():
573+ output_layer .bias .data [..., :] = logits
574+
526575 @torch .compile
527576 def critic_network_forward (
528577 self , state : torch .Tensor , action : torch .Tensor , log_dict : Optional [dict ] = None
@@ -549,40 +598,50 @@ def critic_network_forward(
549598 outputs = rearrange (
550599 outputs , "(k b g) l c o -> k b l c g o" , k = K , g = self .num_gammas
551600 )
552- val_dist = pyd .Categorical (logits = outputs )
553- clip_probs = val_dist .probs .clamp (1e-6 , 0.999 )
554- safe_probs = clip_probs / clip_probs .sum (- 1 , keepdims = True ).detach ()
555- safe_dist = pyd .Categorical (probs = safe_probs )
556- return safe_dist
601+ return pyd .Categorical (logits = outputs )
557602
558- def bin_dist_to_raw_vals (self , bin_dist : pyd .Categorical ) -> torch .Tensor :
603+ def bin_dist_to_raw_vals (
604+ self , bin_dist : pyd .Categorical , temperature : float = 1.0
605+ ) -> torch .Tensor :
559606 """Convert a categorical distribution over bins to a scalar.
560607
561608 Args:
562609 bin_dist: The categorical distribution over bins (output of `forward`).
610+ temperature: Temperature for softening the distribution.
563611
564612 Returns:
565613 The scalar value.
566614 """
567615 assert isinstance (bin_dist , pyd .Categorical )
568- probs = bin_dist .probs
616+ if temperature == 1.0 :
617+ probs = bin_dist .probs
618+ else :
619+ probs = torch .softmax (bin_dist .logits / temperature , dim = - 1 )
569620 bin_vals = self .bin_vals .to (probs .device , dtype = probs .dtype )
570621 exp_val = (probs * bin_vals ).sum (- 1 , keepdims = True )
571622 return self .invert_bins (exp_val )
572623
573624 def raw_vals_to_labels (self , raw_td_target : torch .Tensor ) -> torch .Tensor :
574- """Convert a scalar to a categorical distribution over bins.
625+ """Convert a scalar to a categorical target distribution over bins.
575626
576- Just a torch port of the `dreamerv3/jaxutils.py` implementation .
627+ Dispatches to two-hot or HL-Gauss encoding based on ``self.label_type`` .
577628
578629 Args:
579630 raw_td_target: The scalar value.
580631
581632 Returns:
582- A two-hot encoded categorical distribution over bins.
633+ A categorical distribution over bins (sums to 1 along the last dim).
634+ """
635+ if self .label_type == "hlgauss" :
636+ return self ._hlgauss_labels (raw_td_target )
637+ return self ._twohot_labels (raw_td_target )
638+
639+ def _twohot_labels (self , raw_td_target : torch .Tensor ) -> torch .Tensor :
640+ """Two-hot encoding (Dreamer-V3).
641+
642+ Puts probability mass on exactly two bins flanking the target value,
643+ weighted by proximity. Torch port of `dreamerv3/jaxutils.py`.
583644 """
584- # raw scalar --> symlog --> two hot encoding
585- # (github: danijar/dreamerv3/jaxutils.py)
586645 symlog_td_target = self .transform_values (raw_td_target )
587646 bin_vals = self .bin_vals .to (symlog_td_target .device )
588647 # below and above are indices of the bins directly above and below the scaled value
@@ -609,6 +668,24 @@ def raw_vals_to_labels(self, raw_td_target: torch.Tensor) -> torch.Tensor:
609668 )
610669 return target
611670
671+ def _hlgauss_labels (self , raw_td_target : torch .Tensor ) -> torch .Tensor :
672+ """HL-Gauss: Histogram Loss with Gaussian targets.
673+
674+ Reference: Imani & White (2018); Farebrother et al. (2024)
675+ "Stop Regressing: Training Value Functions via Classification for Scalable Deep RL"
676+ (https://arxiv.org/abs/2403.03950)
677+ """
678+ symlog_target = self .transform_values (raw_td_target )
679+ bin_vals = self .bin_vals .to (symlog_target .device )
680+ bin_width = bin_vals [1 ] - bin_vals [0 ]
681+ sigma_abs = self .sigma * bin_width
682+ edges = torch .cat ([bin_vals [:1 ] - bin_width / 2 , bin_vals + bin_width / 2 ])
683+ z = (edges - symlog_target ) / sigma_abs
684+ cdf_vals = 0.5 * (1.0 + torch .erf (z * (1.0 / math .sqrt (2.0 ))))
685+ bin_probs = cdf_vals [..., 1 :] - cdf_vals [..., :- 1 ]
686+ bin_probs = bin_probs / bin_probs .sum (- 1 , keepdim = True ).clamp (min = 1e-8 )
687+ return bin_probs
688+
612689
613690@gin .configurable (denylist = ["enabled" ])
614691class PopArtLayer (nn .Module ):
0 commit comments