| """VLAlert-X v2 Phase 5 β score a benchmark via cached features + heads. |
| |
| Load a dual-stream cache (from `tools/make_cache_x_v2.py`) and the trained |
| DangerHead + PolicyHeadV2 checkpoints. Forward to produce per-tick action |
| probabilities, then save in the standard `per_tick.pt` schema that the |
| existing `tools/compute_daus_*.py` utilities consume. |
| |
| Output schema: |
| { |
| "ids": list[str] (N,) |
| "indices": LongTensor [N] |
| "scores_binary": FloatTensor [N, 1] # P(ALERT) |
| "scores_3class": FloatTensor [N, 1, 3] # P(S), P(O), P(A) |
| "tta_per_tick": FloatTensor [N, 1] |
| "frame_indices": LongTensor [N, 8] |
| "category": list[str] |
| "source": list[str] |
| "tta_raw": FloatTensor [N] |
| "n_ticks": int = 1 |
| "method": "VLAlert-X-v2" |
| "danger_ckpt": str |
| "policy_ckpt": str |
| } |
| |
| Usage: |
| python tools/score_vlalert_x_v2.py \ |
| --cache data/belief_cache_v2/sft_x_v2__multisrc_val_full.pt \ |
| --manifest data/cot_corpus_v2/multisrc_val_full_perframe_v2.jsonl \ |
| --danger_ckpt checkpoints/danger_v2/seed2/best.pt \ |
| --policy_ckpt checkpoints/policy_v2/seed2/best.pt \ |
| --out eval_results/aus_metric/multisrc_per_tick/vlalert_x_v2.pt |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import logging |
| import sys |
| from pathlib import Path |
| from typing import Dict |
|
|
| import torch |
| import torch.nn.functional as F |
| from tqdm import tqdm |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT)) |
| from lkalert.models.danger_head import DangerHead |
| from lkalert.models.policy_head_v2 import PolicyHeadV2 |
|
|
| logging.basicConfig(level=logging.INFO, |
| format="%(asctime)s %(levelname)s %(message)s") |
| logger = logging.getLogger("score_vlalert_x_v2") |
|
|
|
|
| @torch.no_grad() |
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--cache", type=Path, required=True, |
| help="Dual-stream cache .pt from tools/make_cache_x_v2.py") |
| ap.add_argument("--manifest", type=Path, required=True, |
| help="Perframe-v2 jsonl that was used to build the cache " |
| "(needed for category/source/tta_raw metadata)") |
| ap.add_argument("--danger_ckpt", type=Path, required=True) |
| ap.add_argument("--policy_ckpt", type=Path, required=True) |
| ap.add_argument("--out", type=Path, required=True) |
| ap.add_argument("--batch_size", type=int, default=64) |
| ap.add_argument("--prev_action", type=int, default=3, |
| help="prev_action embedding index; 3=BOS (no temporal context)") |
| args = ap.parse_args() |
|
|
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| |
| logger.info(f"[load] cache: {args.cache}") |
| d = torch.load(args.cache, weights_only=False, map_location="cpu") |
| belief = d["belief_content"].float() |
| policy = d["policy_position"].float() |
| valid = d["valid_frames"] |
| ids_cache = list(d["ids"]) |
| N = belief.shape[0] |
| logger.info(f" N={N} belief={tuple(belief.shape)} policy={tuple(policy.shape)}") |
|
|
| |
| logger.info(f"[load] manifest: {args.manifest}") |
| meta_by_id: Dict[str, Dict] = {} |
| with open(args.manifest) as f: |
| for ln in f: |
| ln = ln.strip() |
| if not ln: continue |
| r = json.loads(ln) |
| mid = r.get("id") or r.get("video_id") |
| if mid: |
| meta_by_id[mid] = r |
| logger.info(f" manifest records: {len(meta_by_id)}") |
|
|
| |
| logger.info(f"[load] DangerHead: {args.danger_ckpt}") |
| ck_d = torch.load(args.danger_ckpt, weights_only=False, map_location="cpu") |
| danger = DangerHead(in_dim=ck_d["in_dim"]).to(device) |
| danger.load_state_dict(ck_d["model"]) |
| danger.eval() |
|
|
| logger.info(f"[load] PolicyHeadV2: {args.policy_ckpt}") |
| ck_p = torch.load(args.policy_ckpt, weights_only=False, map_location="cpu") |
| policy_head = PolicyHeadV2( |
| policy_dim=ck_p["policy_dim"], |
| perception_dim_per_query=ck_p["perception_dim_per_query"], |
| k_queries=ck_p["k_queries"], |
| ).to(device) |
| policy_head.load_state_dict(ck_p["model"]) |
| policy_head.eval() |
| logger.info(f" Phase 3 val: per_frame_auc={ck_d['val_metrics'].get('per_frame_auc',0):.4f}") |
| logger.info(f" Phase 4 val: bal_acc={ck_p['val_metrics']['balanced_acc']:.4f} " |
| f"per_class_recall={ck_p['val_metrics']['per_class_recall']}") |
|
|
| |
| scores_3class = torch.zeros(N, 1, 3, dtype=torch.float32) |
| n_failed = 0 |
| prev_act_tensor = torch.full((args.batch_size,), args.prev_action, dtype=torch.long, device=device) |
|
|
| bs = args.batch_size |
| for i in tqdm(range(0, N, bs), ncols=80, desc="infer"): |
| end = min(N, i + bs) |
| b_belief = belief[i:end].to(device, non_blocking=True) |
| b_policy = policy[i:end].to(device, non_blocking=True) |
| b_valid = valid[i:end].to(device, non_blocking=True) |
| cur_bs = end - i |
|
|
| |
| d_out = danger(b_belief, valid_frames=b_valid) |
| perc = d_out["perception_summary"] |
| danger_pf = d_out["per_frame"] |
|
|
| |
| prev = prev_act_tensor[:cur_bs] |
| logits = policy_head(b_policy, perc, danger_pf, prev, valid_frames=b_valid) |
| probs = F.softmax(logits, dim=-1).cpu() |
| scores_3class[i:end, 0] = probs |
|
|
| scores_binary = scores_3class[:, :, 2].clone() |
|
|
| |
| ids_out: list = [] |
| cat_out: list = [] |
| src_out: list = [] |
| tta_raw_out = torch.zeros(N, dtype=torch.float32) |
| tta_per_tick_out = torch.zeros(N, 1, dtype=torch.float32) |
| frame_indices_out = torch.zeros(N, 8, dtype=torch.long) |
| indices_out = torch.arange(N, dtype=torch.long) |
|
|
| |
| |
| |
| |
| cache_category = list(d.get("category", [""] * N)) |
| cache_source = list(d.get("source", [""] * N)) |
| cache_tick_tta = d.get("tick_tta_raw", torch.full((N,), -1.0)) |
|
|
| n_missing_meta = 0 |
| for i, vid in enumerate(ids_cache): |
| m = meta_by_id.get(vid, {}) |
| if not m: |
| n_missing_meta += 1 |
| ids_out.append(vid) |
| cat_out.append(cache_category[i] if i < len(cache_category) else "") |
| src_out.append(cache_source[i] if i < len(cache_source) else "") |
| tta_v = (cache_tick_tta[i].item() if hasattr(cache_tick_tta[i], "item") |
| else float(cache_tick_tta[i])) |
| tta_raw_out[i] = tta_v |
| tta_per_tick_out[i, 0] = tta_v |
| fi = m.get("frame_indices", [0]*8) |
| frame_indices_out[i] = torch.tensor(fi[:8], dtype=torch.long) |
| if n_missing_meta: |
| logger.warning(f" {n_missing_meta} cache ids had no matching manifest record " |
| f"(only frame_indices lost; category/source still correct from cache)") |
|
|
| out_dict = { |
| "ids": ids_out, |
| "indices": indices_out, |
| "scores_binary": scores_binary, |
| "scores_3class": scores_3class, |
| "tta_per_tick": tta_per_tick_out, |
| "frame_indices": frame_indices_out, |
| "category": cat_out, |
| "source": src_out, |
| "tta_raw": tta_raw_out, |
| "n_ticks": 1, |
| "method": "VLAlert-X-v2", |
| "danger_ckpt": str(args.danger_ckpt), |
| "policy_ckpt": str(args.policy_ckpt), |
| } |
| args.out.parent.mkdir(parents=True, exist_ok=True) |
| torch.save(out_dict, args.out) |
| logger.info(f"[save] {args.out}") |
| logger.info(f" N={len(ids_out)} " |
| f"P(ALERT) range=[{scores_binary.min():.4f}, {scores_binary.max():.4f}]") |
| |
| from collections import Counter |
| cc = Counter(cat_out) |
| logger.info(f" category dist: {dict(cc)}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|