Skip to content

Commit ae56673

Browse files
authored
Merge pull request #96 from UT-Austin-RPL/mt_edit
draft hlgauss + remove clamp in mt
2 parents 951b3a8 + 2657ff1 commit ae56673

2 files changed

Lines changed: 94 additions & 14 deletions

File tree

amago/agent.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,9 @@ def __init__(
10641064
actor_type: Type[actor_critic.BaseActorHead] = actor_critic.Actor,
10651065
critic_type: Type[actor_critic.BaseCriticHead] = actor_critic.NCriticsTwoHot,
10661066
pass_obs_keys_to_actor: Optional[Iterable[str]] = None,
1067+
dpg_temperature: float = 1.0,
10671068
):
1069+
self.dpg_temperature = dpg_temperature
10681070
super().__init__(
10691071
obs_space=obs_space,
10701072
rl2_space=rl2_space,
@@ -1260,8 +1262,9 @@ def forward(self, batch: Batch, log_step: bool):
12601262
a_agent_dpg = torch.stack([a_dist.rsample() for _ in range(K_a)], dim=0)
12611263
q_s_a_agent = self.maximized_critics(s_rep.detach(), a_agent_dpg)
12621264
q_s_a_agent = self.popart.normalize_values(
1263-
# mean over K actions, min over critic ensemble
1264-
self.maximized_critics.bin_dist_to_raw_vals(q_s_a_agent).mean(0).min(2).values
1265+
self.maximized_critics.bin_dist_to_raw_vals(
1266+
q_s_a_agent, temperature=self.dpg_temperature
1267+
).mean(0).min(2).values
12651268
)
12661269
actor_loss += self.online_coeff * -(q_s_a_agent[:, :-1, ...])
12671270

amago/nets/actor_critic.py

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import contextlib
66
from typing import Optional, Type
7+
import math
78
import random
89
from functools import lru_cache
910
from 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"])
614691
class PopArtLayer(nn.Module):

0 commit comments

Comments
 (0)