| """Phase G.3 β AdaptiveDangerPolicy. |
| |
| Wraps the v3 pipeline so that OBSERVE has functional meaning: |
| BELIEF (mid window) |
| β DangerHead [perception_summary, per_frame, hazard_logits] |
| β PolicyHead anchor pi_t on mid window |
| β AdaptiveWindowModule (pi_t, hazard_logits, belief_summary) β window choice w* |
| β PolicyHead final action on the chosen window |
| |
| Three forward modes for 3-stage curriculum: |
| forward_chosen_window(beliefs_3w, valid_3w, prev_action, window_idx) |
| Stage 1 (oracle) + Stage 2 (mixed) β gather a single window per sample. |
| forward_softmix_window(beliefs_3w, valid_3w, prev_action) |
| Stage 3 β differentiable window selection via straight-through. |
| predict(beliefs_3w, valid_3w, prev_action, decode_window="learned") |
| Inference β uses AdaptiveWindow's argmax; returns (policy_logits, |
| window_choice, hazard_logits, policy_pi). |
| |
| Args: |
| danger_ckpt: path to DangerHead ckpt (with n_hazards=8 hazard head) |
| policy_ckpt: path to warm-start PolicyHeadV2 ckpt |
| n_hazards: 8 (matches taxonomy from adaptive_window.py) |
| |
| The danger_head is frozen; policy_head + adaptive_window are trainable. |
| """ |
| from __future__ import annotations |
|
|
| import sys |
| from pathlib import Path |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| ROOT = Path(__file__).resolve().parents[2] |
| sys.path.insert(0, str(ROOT)) |
|
|
| from lkalert.models.danger_head import DangerHead |
| from lkalert.models.policy_head_v2 import PolicyHeadV2 |
| from lkalert.models.adaptive_window import ( |
| AdaptiveWindowModule, |
| straight_through_window_select, |
| WINDOW_NARROW, WINDOW_MID, WINDOW_WIDE, |
| N_HAZARDS, |
| ) |
|
|
|
|
| class AdaptiveDangerPolicy(nn.Module): |
| """Composite model: frozen DangerHead + trainable PolicyHead + trainable |
| AdaptiveWindow. Always anchors on mid window first to derive pi_t for |
| window selection. |
| """ |
|
|
| def __init__( |
| self, |
| danger_ckpt: Path | str, |
| policy_ckpt: Path | str | None = None, |
| in_dim: int = 10240, |
| policy_dim: int = 2560, |
| perception_dim_per_query: int = 512, |
| k_queries: int = 4, |
| adaptive_belief_dim: int = 2560, |
| adaptive_hidden: int = 128, |
| adaptive_dropout: float = 0.1, |
| use_hazard_bias: bool = True, |
| freeze_danger: bool = True, |
| ): |
| super().__init__() |
|
|
| |
| ck_d = torch.load(danger_ckpt, weights_only=False, map_location="cpu") |
| dh_kwargs = dict( |
| in_dim=ck_d.get("in_dim", in_dim), |
| hidden=ck_d.get("hidden", 512), |
| k_queries=ck_d.get("k_queries", k_queries), |
| dropout=ck_d.get("dropout", 0.2), |
| n_hazards=ck_d.get("n_hazards", N_HAZARDS), |
| ) |
| self.danger_head = DangerHead(**dh_kwargs) |
| self.danger_head.load_state_dict(ck_d["model"]) |
| if freeze_danger: |
| for p in self.danger_head.parameters(): |
| p.requires_grad_(False) |
| self.danger_head.eval() |
|
|
| |
| ph_kwargs = dict( |
| policy_dim=policy_dim, |
| perception_dim_per_query=perception_dim_per_query, |
| k_queries=k_queries, |
| ) |
| if policy_ckpt is not None: |
| ck_p = torch.load(policy_ckpt, weights_only=False, map_location="cpu") |
| for k in ("policy_dim", "perception_dim_per_query", "k_queries"): |
| if k in ck_p: |
| ph_kwargs[k] = ck_p[k] |
| self.policy_head = PolicyHeadV2(**ph_kwargs) |
| if policy_ckpt is not None: |
| self.policy_head.load_state_dict(ck_p["model"]) |
|
|
| |
| self.adaptive_window = AdaptiveWindowModule( |
| belief_dim=adaptive_belief_dim, |
| hidden=adaptive_hidden, |
| dropout=adaptive_dropout, |
| use_hazard_bias=use_hazard_bias, |
| ) |
|
|
| |
| self.in_dim = in_dim |
| self.policy_dim = policy_dim |
| self.adaptive_belief_dim = adaptive_belief_dim |
|
|
| |
| |
| |
| def _danger_forward(self, belief: torch.Tensor, |
| valid: torch.Tensor | None) -> dict: |
| """Forward DangerHead (always frozen-eval).""" |
| with torch.no_grad(): |
| return self.danger_head(belief, valid_frames=valid) |
|
|
| def _policy_forward(self, policy_pos: torch.Tensor, |
| perception_summary: torch.Tensor, |
| per_frame: torch.Tensor, |
| prev_action: torch.Tensor, |
| valid: torch.Tensor | None) -> torch.Tensor: |
| return self.policy_head(policy_pos, perception_summary, per_frame, |
| prev_action, valid_frames=valid) |
|
|
| def _belief_summary(self, policy_pos: torch.Tensor, |
| valid: torch.Tensor | None) -> torch.Tensor: |
| """Mean-pool valid frames of policy_pos to get a [B, D] summary.""" |
| if valid is None: |
| return policy_pos.mean(dim=1) |
| mask = valid.float().unsqueeze(-1) |
| s = (policy_pos * mask).sum(dim=1) |
| n = mask.sum(dim=1).clamp(min=1) |
| return s / n |
|
|
| |
| |
| |
| def forward_chosen_window( |
| self, |
| belief_3w: torch.Tensor, |
| policy_pos_3w: torch.Tensor, |
| valid_3w: torch.Tensor, |
| prev_action: torch.Tensor, |
| window_idx: torch.Tensor, |
| ) -> dict: |
| """Stage 1/2 β single-window forward chosen by `window_idx`. |
| |
| Also runs AdaptiveWindow on mid-window anchor for window-CE loss. |
| """ |
| B = belief_3w.shape[0] |
| ar = torch.arange(B, device=belief_3w.device) |
|
|
| |
| b_mid = belief_3w[:, WINDOW_MID] |
| pp_mid = policy_pos_3w[:, WINDOW_MID] |
| v_mid = valid_3w[:, WINDOW_MID] |
| dh_mid = self._danger_forward(b_mid, v_mid) |
| logits_mid = self._policy_forward( |
| pp_mid, dh_mid["perception_summary"], dh_mid["per_frame"], |
| prev_action, v_mid) |
| pi_mid = F.softmax(logits_mid, dim=-1) |
|
|
| hazard_logits = dh_mid.get("hazard_logits", |
| torch.zeros((B, N_HAZARDS), |
| device=belief_3w.device)) |
| belief_summary = self._belief_summary(pp_mid, v_mid) |
| window_logits = self.adaptive_window( |
| pi_mid, hazard_logits, belief_summary) |
|
|
| |
| b_c = belief_3w[ar, window_idx] |
| pp_c = policy_pos_3w[ar, window_idx] |
| v_c = valid_3w[ar, window_idx] |
| dh_c = self._danger_forward(b_c, v_c) |
| policy_logits = self._policy_forward( |
| pp_c, dh_c["perception_summary"], dh_c["per_frame"], |
| prev_action, v_c) |
|
|
| return { |
| "policy_logits": policy_logits, |
| "window_logits": window_logits, |
| "hazard_logits": hazard_logits, |
| "policy_pi_mid": pi_mid, |
| "policy_logits_mid": logits_mid, |
| } |
|
|
| def forward_softmix_window( |
| self, |
| belief_3w: torch.Tensor, |
| policy_pos_3w: torch.Tensor, |
| valid_3w: torch.Tensor, |
| prev_action: torch.Tensor, |
| ) -> dict: |
| """Stage 3 β differentiable window mix via straight-through. |
| |
| AdaptiveWindow's argmax determines the forward path; gradients flow |
| through softmax(window_logits). |
| """ |
| B, _, F_, D_in = belief_3w.shape |
| _, _, _, D_pp = policy_pos_3w.shape |
|
|
| b_mid = belief_3w[:, WINDOW_MID] |
| pp_mid = policy_pos_3w[:, WINDOW_MID] |
| v_mid = valid_3w[:, WINDOW_MID] |
| dh_mid = self._danger_forward(b_mid, v_mid) |
| logits_mid = self._policy_forward( |
| pp_mid, dh_mid["perception_summary"], dh_mid["per_frame"], |
| prev_action, v_mid) |
| pi_mid = F.softmax(logits_mid, dim=-1) |
|
|
| hazard_logits = dh_mid.get("hazard_logits", |
| torch.zeros((B, N_HAZARDS), |
| device=belief_3w.device)) |
| belief_summary = self._belief_summary(pp_mid, v_mid) |
| window_logits = self.adaptive_window( |
| pi_mid, hazard_logits, belief_summary) |
|
|
| |
| |
| |
| |
| win_choice = window_logits.argmax(dim=-1) |
| ar = torch.arange(B, device=belief_3w.device) |
| b_c = belief_3w[ar, win_choice] |
| v_c = valid_3w[ar, win_choice] |
| dh_c = self._danger_forward(b_c, v_c) |
|
|
| |
| |
| pp_soft = straight_through_window_select(window_logits, policy_pos_3w) |
| |
| policy_logits = self._policy_forward( |
| pp_soft, dh_c["perception_summary"], dh_c["per_frame"], |
| prev_action, v_c) |
|
|
| return { |
| "policy_logits": policy_logits, |
| "window_logits": window_logits, |
| "window_choice": win_choice, |
| "hazard_logits": hazard_logits, |
| "policy_pi_mid": pi_mid, |
| "policy_logits_mid": logits_mid, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| PREV_ACTION_TO_WINDOW_V4 = (0, 1, 2, 1) |
|
|
| def forward_with_prev_action( |
| self, |
| belief_3w: torch.Tensor, |
| policy_pos_3w: torch.Tensor, |
| valid_3w: torch.Tensor, |
| prev_action: torch.Tensor, |
| ) -> dict: |
| """v4 forward: window is fully determined by `prev_action`. |
| |
| prev_action β {0:SIL, 1:OBS, 2:ALR, 3:BOS}. |
| Window index β {0:sil_wide, 1:obs_mid, 2:alr_narrow}. |
| Mapping: SILβsil_wide, OBSβobs_mid, ALRβalr_narrow, BOSβobs_mid. |
| |
| No learned window selector, no AdaptiveWindow forward, no mid anchor. |
| This is the production path for v4. |
| """ |
| B = belief_3w.shape[0] |
| ar = torch.arange(B, device=belief_3w.device) |
|
|
| lookup = torch.tensor(self.PREV_ACTION_TO_WINDOW_V4, |
| dtype=torch.long, device=belief_3w.device) |
| window_idx = lookup[prev_action.clamp(min=0, max=3)] |
|
|
| b_c = belief_3w[ar, window_idx] |
| pp_c = policy_pos_3w[ar, window_idx] |
| v_c = valid_3w[ar, window_idx] |
| dh_c = self._danger_forward(b_c, v_c) |
| policy_logits = self._policy_forward( |
| pp_c, dh_c["perception_summary"], dh_c["per_frame"], |
| prev_action, v_c) |
| hazard_logits = dh_c.get( |
| "hazard_logits", |
| torch.zeros((B, N_HAZARDS), device=belief_3w.device)) |
|
|
| return { |
| "policy_logits": policy_logits, |
| "window_idx": window_idx, |
| "hazard_logits": hazard_logits, |
| "policy_pi": F.softmax(policy_logits, dim=-1), |
| } |
|
|
| @torch.no_grad() |
| def predict_v4( |
| self, |
| belief_3w: torch.Tensor, |
| policy_pos_3w: torch.Tensor, |
| valid_3w: torch.Tensor, |
| prev_action: torch.Tensor, |
| ) -> dict: |
| """Inference convenience β same as forward_with_prev_action but in eval mode.""" |
| self.eval() |
| return self.forward_with_prev_action( |
| belief_3w, policy_pos_3w, valid_3w, prev_action) |
|
|
| @torch.no_grad() |
| def predict( |
| self, |
| belief_3w: torch.Tensor, |
| policy_pos_3w: torch.Tensor, |
| valid_3w: torch.Tensor, |
| prev_action: torch.Tensor, |
| decode_window: str = "learned", |
| oracle_window: torch.Tensor | None = None, |
| ) -> dict: |
| """Inference β supports several decoding strategies for Phase H ablation.""" |
| self.eval() |
| B = belief_3w.shape[0] |
| ar = torch.arange(B, device=belief_3w.device) |
|
|
| |
| b_mid = belief_3w[:, WINDOW_MID] |
| pp_mid = policy_pos_3w[:, WINDOW_MID] |
| v_mid = valid_3w[:, WINDOW_MID] |
| dh_mid = self._danger_forward(b_mid, v_mid) |
| logits_mid = self._policy_forward( |
| pp_mid, dh_mid["perception_summary"], dh_mid["per_frame"], |
| prev_action, v_mid) |
| pi_mid = F.softmax(logits_mid, dim=-1) |
| hazard_logits = dh_mid.get("hazard_logits", |
| torch.zeros((B, N_HAZARDS), |
| device=belief_3w.device)) |
| belief_summary = self._belief_summary(pp_mid, v_mid) |
| window_logits = self.adaptive_window( |
| pi_mid, hazard_logits, belief_summary) |
|
|
| |
| if decode_window == "learned": |
| win_choice = window_logits.argmax(dim=-1) |
| elif decode_window == "fixed_narrow": |
| win_choice = torch.full((B,), WINDOW_NARROW, dtype=torch.long, |
| device=belief_3w.device) |
| elif decode_window == "fixed_mid": |
| win_choice = torch.full((B,), WINDOW_MID, dtype=torch.long, |
| device=belief_3w.device) |
| elif decode_window == "fixed_wide": |
| win_choice = torch.full((B,), WINDOW_WIDE, dtype=torch.long, |
| device=belief_3w.device) |
| elif decode_window == "oracle": |
| assert oracle_window is not None |
| win_choice = oracle_window.to(belief_3w.device) |
| else: |
| raise ValueError(f"unknown decode_window: {decode_window}") |
|
|
| |
| b_c = belief_3w[ar, win_choice] |
| pp_c = policy_pos_3w[ar, win_choice] |
| v_c = valid_3w[ar, win_choice] |
| dh_c = self._danger_forward(b_c, v_c) |
| policy_logits = self._policy_forward( |
| pp_c, dh_c["perception_summary"], dh_c["per_frame"], |
| prev_action, v_c) |
|
|
| return { |
| "policy_logits": policy_logits, |
| "window_logits": window_logits, |
| "window_choice": win_choice, |
| "hazard_logits": hazard_logits, |
| "policy_pi_mid": pi_mid, |
| } |
|
|