Skip to content

Commit 382134b

Browse files
committed
get_values
1 parent f533cf0 commit 382134b

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

amago/agent.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,18 @@ def edit_critic_mask(
470470
"""
471471
return pad_mask
472472

473+
def on_checkpoint_loaded(self, is_resume: bool = False):
474+
"""Called after checkpoint weights have been loaded into this agent.
475+
476+
Override to snapshot parameters, build frozen copies, etc.
477+
478+
Args:
479+
is_resume: True when restoring a full accelerate training state
480+
(i.e. continuing an in-progress run). False when loading raw
481+
policy weights for a new run or inference.
482+
"""
483+
pass
484+
473485

474486
@gin.configurable
475487
@register_agent("agent")
@@ -751,6 +763,75 @@ def get_actions(
751763
dtype = torch.uint8 if (self.discrete or self.multibinary) else torch.float32
752764
return actions.to(dtype=dtype), hidden_state
753765

766+
@torch.no_grad()
767+
def get_values(self, batch: Batch) -> dict[str, torch.Tensor]:
768+
"""Inference-time computation of Q, V, advantage, and filter weights.
769+
770+
Mirrors the advantage computation in `forward` but without computing
771+
any losses or updating any state. Useful for offline analysis of
772+
what the critic thinks about a batch of data.
773+
774+
Args:
775+
batch: A collated Batch of trajectories.
776+
777+
Returns:
778+
Dict with keys:
779+
- q_sa: Q(s, a_observed), mean over critic ensemble. (B, L-1, G, 1)
780+
- v_s: V(s) = E_pi[Q(s, a~pi)], mean over samples and ensemble. (B, L-1, G, 1)
781+
- advantage: A(s,a) = Q(s,a) - V(s). (B, L-1, G, 1)
782+
- filter_weights: f(A(s,a)) from fbc_filter_func. (B, L-1, G, 1)
783+
- state_mask: Valid timestep mask. (B, L-1, 1)
784+
"""
785+
o = self.tstep_encoder(obs=batch.obs, rl2s=batch.rl2s)
786+
straight_from_obs = {k: batch.obs[k] for k in self.pass_obs_keys_to_actor}
787+
B, L, D_o = o.shape
788+
a = batch.actions
789+
a = a.clamp(0, 1.0) if self.discrete else a.clamp(-1.0, 1.0)
790+
_B, _L, D_action = a.shape
791+
G = len(self.gammas)
792+
K_a = self.num_actions_for_value_in_actor_loss if not self.discrete else 1
793+
a_buffer = F.pad(a, (0, 0, 0, 1), "replicate")
794+
a_buffer = repeat(a_buffer, f"b l a -> b l {G} a")
795+
state_mask = (~((batch.rl2s == self.pad_val).all(-1, keepdim=True))).bool()[
796+
:, 1:, ...
797+
]
798+
799+
s_rep, _ = self.traj_encoder(
800+
seq=o, time_idxs=batch.time_idxs, hidden_state=None
801+
)
802+
803+
a_dist = self.actor(s_rep, straight_from_obs=straight_from_obs)
804+
805+
# Q(s, a_observed) — raw critic output, (B, L, C, G, 1)
806+
q_sa_raw = self.critics(s_rep, a_buffer.unsqueeze(0)).mean(0)
807+
# V(s) = E_pi[Q(s, a~pi)] — raw critic output
808+
a_agent = self._sample_k_actions(a_dist, k=K_a)
809+
v_s_raw = self.critics(s_rep.detach(), a_agent).mean(0)
810+
811+
# Filter in popart-normalized space (stable, scale-invariant to reward shifts)
812+
q_sa_norm = self.popart(q_sa_raw)[:, :-1, ...].mean(2)
813+
v_s_norm = self.popart(v_s_raw)[:, :-1, ...].mean(2)
814+
filter_weights = self.fbc_filter_func(q_sa_norm - v_s_norm).float()
815+
816+
# Q, V, advantage in denormalized reward scale for interpretability
817+
q_denorm = self.popart(q_sa_raw, normalized=False)[:, :-1, ...]
818+
v_denorm = self.popart(v_s_raw, normalized=False)[:, :-1, ...]
819+
q_sa = q_denorm.mean(2)
820+
q_sa_std = q_denorm.std(2)
821+
v_s = v_denorm.mean(2)
822+
v_s_std = v_denorm.std(2)
823+
advantage = q_sa - v_s
824+
825+
return {
826+
"q_sa": q_sa,
827+
"v_s": v_s,
828+
"advantage": advantage,
829+
"filter_weights": filter_weights,
830+
"state_mask": state_mask,
831+
"q_sa_std": q_sa_std,
832+
"v_s_std": v_s_std,
833+
}
834+
754835
def _reduce_critic_ensemble(self, q_ensemble: torch.Tensor) -> torch.Tensor:
755836
B, L, C, G, _ = q_ensemble.shape
756837
# random subset of critic ensemble
@@ -1187,6 +1268,75 @@ def __init__(
11871268
def _sample_k_actions(self, dist, k: int):
11881269
raise NotImplementedError
11891270

1271+
@torch.no_grad()
1272+
def get_values(self, batch: Batch) -> dict[str, torch.Tensor]:
1273+
"""Inference-time computation of Q, V, advantage, and filter weights.
1274+
1275+
MultiTaskAgent override that uses bin-distribution critics
1276+
(NCriticsTwoHot) instead of scalar critics.
1277+
1278+
Args:
1279+
batch: A collated Batch of trajectories.
1280+
1281+
Returns:
1282+
Dict with keys:
1283+
- q_sa: Q(s, a_observed), mean over critic ensemble. (B, L-1, G, 1)
1284+
- v_s: V(s) = E_pi[Q(s, a~pi)], mean over samples and ensemble. (B, L-1, G, 1)
1285+
- advantage: A(s,a) = Q(s,a) - V(s). (B, L-1, G, 1)
1286+
- filter_weights: f(A(s,a)) from fbc_filter_func. (B, L-1, G, 1)
1287+
- state_mask: Valid timestep mask. (B, L-1, 1)
1288+
"""
1289+
o = self.tstep_encoder(obs=batch.obs, rl2s=batch.rl2s)
1290+
straight_from_obs = {k: batch.obs[k] for k in self.pass_obs_keys_to_actor}
1291+
B, L, D_o = o.shape
1292+
a = batch.actions
1293+
a = a.clamp(0, 1.0) if self.discrete else a.clamp(-1.0, 1.0)
1294+
_B, _L, D_action = a.shape
1295+
G = len(self.gammas)
1296+
K_a = self.num_actions_for_value_in_actor_loss
1297+
a_buffer = F.pad(a, (0, 0, 0, 1), "replicate")
1298+
a_buffer = repeat(a_buffer, f"b l a -> b l {G} a")
1299+
state_mask = (~((batch.rl2s == self.pad_val).all(-1, keepdim=True))).bool()[
1300+
:, 1:, ...
1301+
]
1302+
1303+
s_rep, _ = self.traj_encoder(
1304+
seq=o, time_idxs=batch.time_idxs, hidden_state=None
1305+
)
1306+
1307+
a_dist = self.actor(s_rep, straight_from_obs=straight_from_obs)
1308+
if self.discrete:
1309+
a_dist = DiscreteLikeContinuous(a_dist)
1310+
1311+
# Q(s, a_observed) via bin distribution critics
1312+
q_dist = self.critics(s_rep, a_buffer.unsqueeze(0))
1313+
scalar_q = self.critics.bin_dist_to_raw_vals(q_dist).squeeze(0)
1314+
# scalar_q: (B, L, C, G, 1) — C is critic ensemble dim
1315+
q_sa = scalar_q[:, :-1, ...].mean(2)
1316+
q_sa_std = scalar_q[:, :-1, ...].std(2)
1317+
1318+
# V(s) = E_pi[Q(s, a~pi)] via K_a sampled actions
1319+
a_agent = a_dist.sample((K_a,))
1320+
q_v_dist = self.critics(s_rep.detach(), a_agent)
1321+
val_s = self.critics.bin_dist_to_raw_vals(q_v_dist)
1322+
# val_s: (K_a, B, L, C, G, 1) — mean over samples (0), keep ensemble (3)
1323+
val_s_per_critic = val_s[:, :, :-1, ...].mean(0) # (B, L-1, C, G, 1)
1324+
v_s = val_s_per_critic.mean(2)
1325+
v_s_std = val_s_per_critic.std(2)
1326+
1327+
advantage = q_sa - v_s
1328+
filter_weights = self.fbc_filter_func(advantage).float()
1329+
1330+
return {
1331+
"q_sa": q_sa,
1332+
"v_s": v_s,
1333+
"advantage": advantage,
1334+
"filter_weights": filter_weights,
1335+
"state_mask": state_mask,
1336+
"q_sa_std": q_sa_std,
1337+
"v_s_std": v_s_std,
1338+
}
1339+
11901340
def forward(self, batch: Batch, log_step: bool):
11911341
# fmt: off
11921342
self.update_info = {} # holds wandb stats

amago/experiment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ def load_checkpoint_from_path(
380380
self.policy.load_state_dict(ckpt)
381381
else:
382382
self.accelerator.load_state(path)
383+
self.policy.on_checkpoint_loaded(is_resume=is_accelerate_state)
383384

384385
def load_checkpoint(self, epoch: int, resume_training_state: bool = True) -> None:
385386
"""Load a historical checkpoint from the `ckpts` directory of this experiment.

0 commit comments

Comments
 (0)