VLAlert / training /Policy /policy_dataset.py
AsianPlayer's picture
Add VLAlert code
1e05592 verified
Raw
History Blame Contribute Delete
11.9 kB
#!/usr/bin/env python3
"""
PolicyDataset β€” loads policy label manifests for Stage 1 supervised warm-start.
Two modes:
Image mode (default): loads raw frames; requires full VLM forward at each step.
Used for: make_belief_cache.py, evaluate_policy.py.
Cache mode (--belief_cache_dir): loads pre-computed belief vectors from
data/belief_cache/{split}.pt produced by make_belief_cache.py.
Used for: warm_start_trainer.py (fast, ~1000Γ— speed-up).
In cache mode __getitem__ returns belief/tta tensors instead of images.
"""
from __future__ import annotations
import json
import logging
from collections import Counter
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
import numpy as np
import torch
from PIL import Image
from torch.utils.data import Dataset
logger = logging.getLogger("Policy.dataset")
MAX_FRAMES = 8
ACTION_NAMES = {0: "SILENT", 1: "OBSERVE", 2: "ALERT"}
SAMPLING_SCHEMES = ("original", "uniform", "last_biased", "last_2s")
SOURCE_FILTERS = ("all", "nexar", "multisrc", "dada", "dad")
# ── frame loading (mirrors DPO/SFT dataset) ───────────────────────────────────
def _load_frame(src_dir: Path, frame_idx: int) -> Optional[Image.Image]:
for fmt in ["{:03d}", "{:04d}", "{:05d}", "{:06d}", "{}"]:
for ext in [".jpg", ".jpeg", ".png"]:
p = src_dir / (fmt.format(frame_idx) + ext)
if p.exists():
try:
return Image.open(p).convert("RGB")
except Exception:
pass
return None
def _resample_indices(
base: Sequence[int],
n_frames: int,
scheme: str = "original",
) -> List[int]:
"""Resample frame indices within the window [base[0], base[-1]].
`base` is the manifest's baked frame_indices (typically length 8, event-window
biased). We treat its min/max as the sampling window and redraw `n_frames`
indices inside that window, rounded to int.
Schemes:
original β€” return `base[:n_frames]` (classic behavior)
uniform β€” evenly spaced indices across [min, max]
last_biased β€” 25% of frames from first half, 75% from second half
last_2s β€” all frames crammed into the last 2 s (~60 frames @ 30 fps)
assumed at the tail of the window
"""
if not base:
return []
if scheme == "original" or n_frames == len(base):
return list(base[:n_frames])
lo, hi = int(base[0]), int(base[-1])
if hi <= lo:
return [lo] * n_frames
if scheme == "uniform":
idx = np.linspace(lo, hi, n_frames)
elif scheme == "last_biased":
n_head = max(1, n_frames // 4)
n_tail = n_frames - n_head
mid = (lo + hi) // 2
head = np.linspace(lo, mid, n_head, endpoint=False)
tail = np.linspace(mid, hi, n_tail)
idx = np.concatenate([head, tail])
elif scheme == "last_2s":
# assume 30 fps β†’ last 2 s = last 60 frames, clamped to available window
two_s = min(hi - lo, 60)
start = hi - two_s
idx = np.linspace(start, hi, n_frames)
else:
raise ValueError(f"unknown sampling scheme: {scheme}")
return [int(round(x)) for x in idx]
def _load_frames(
source_dir: str,
frame_indices: List[int],
n_frames: int = MAX_FRAMES,
) -> List[Image.Image]:
src = Path(source_dir)
imgs = []
for idx in frame_indices[:n_frames]:
img = _load_frame(src, idx)
if img is not None:
imgs.append(img)
if not imgs:
imgs = [Image.new("RGB", (384, 384), (64, 64, 64))]
return imgs
# ── dataset ───────────────────────────────────────────────────────────────────
class PolicyDataset(Dataset):
"""
Args:
manifests : list of paths to JSON files from make_policy_labels.py
split : "train" or "val" (for logging)
belief_cache_path : optional path to .pt file from make_belief_cache.py;
when supplied, __getitem__ returns cached tensors
instead of PIL images (fast training mode)
debug : if True, truncate to first debug_samples
debug_samples : cap on samples in debug mode
"""
def __init__(
self,
manifests: List[Any],
split: str = "train",
belief_cache_path: Optional[Any] = None,
debug: bool = False,
debug_samples: int = 64,
n_frames: int = MAX_FRAMES,
sampling: str = "original",
source_filter: str = "all",
):
self.split = split
self.n_frames = int(n_frames)
self.sampling = sampling
self.source_filter = source_filter
assert sampling in SAMPLING_SCHEMES, (
f"sampling must be one of {SAMPLING_SCHEMES}, got {sampling}")
assert source_filter in SOURCE_FILTERS, (
f"source_filter must be one of {SOURCE_FILTERS}, got {source_filter}")
self.samples: List[dict] = []
for m in manifests:
m = Path(m)
if not m.exists():
logger.warning(f"Policy label manifest not found: {m}")
continue
with open(m) as f:
data = json.load(f)
chunk = data.get("samples", data if isinstance(data, list) else [])
self.samples.extend(chunk)
logger.info(
f"Loaded {len(chunk)} samples from {m.name} "
f"labels={data.get('label_counts', {}) if isinstance(data, dict) else 'n/a'} "
f"excluded={data.get('excluded', {}) if isinstance(data, dict) else 'n/a'}"
)
# Source filter (applied after manifest load so filter obeys the
# naming convention in the Stage K/L plan).
if source_filter != "all":
keep = {
"nexar": {"nexar"},
"multisrc": {"nexar", "dada"}, # balanced63k already controls ratio
"dada": {"dada"},
"dad": {"dad"},
}[source_filter]
before = len(self.samples)
self.samples = [s for s in self.samples if s.get("source") in keep]
logger.info(
f"source_filter={source_filter}: {before} β†’ {len(self.samples)} samples"
)
if debug:
self.samples = self.samples[:debug_samples]
# ── optional belief cache ─────────────────────────────────────────────
self._cache: Optional[dict] = None
if belief_cache_path is not None:
p = Path(belief_cache_path)
if not p.exists():
raise FileNotFoundError(f"Belief cache not found: {p}")
cache = torch.load(p, map_location="cpu", weights_only=True)
# Trim cache to match current sample count (debug mode may shrink samples)
n = len(self.samples)
# Clip caches ship key "beliefs" [N, D]; per-frame caches from
# make_belief_cache_v2 ship "beliefs_frame" [N, T, D] + "valid_frames" [N, T].
# Accept either: mean-pool across frames if given a per-frame cache so
# downstream v3/v5/v6/v7 heads (which read _cache["beliefs"]) all work.
if "beliefs" in cache:
beliefs = cache["beliefs"][:n]
if beliefs.dim() == 3:
dtype = beliefs.dtype
beliefs = beliefs.float().mean(dim=1).to(dtype)
elif "beliefs_frame" in cache:
raw = cache["beliefs_frame"][:n] # [N, T, D]
dtype = raw.dtype
vf = cache.get("valid_frames")
if vf is not None:
vmask = vf[:n].float().unsqueeze(-1) # [N, T, 1]
denom = vmask.sum(dim=1).clamp(min=1.0) # [N, 1]
beliefs = (raw.float() * vmask).sum(dim=1) / denom
else:
beliefs = raw.float().mean(dim=1)
beliefs = beliefs.to(dtype)
else:
raise KeyError(
f"Belief cache {p.name} has neither 'beliefs' nor 'beliefs_frame' key"
)
self._cache = {
"beliefs": beliefs,
"tta_means": cache["tta_means"][:n],
"tta_vars": cache["tta_vars"][:n],
}
logger.info(
f"Loaded belief cache from {p.name} ({n} entries, "
f"belief_dim={self._cache['beliefs'].shape[-1]})"
)
label_dist = Counter(ACTION_NAMES[s["action_label"]] for s in self.samples)
cat_dist = Counter(s["category"] for s in self.samples)
mode = "cached" if self._cache is not None else "image"
logger.info(
f"PolicyDataset [{split}, {mode}]: {len(self.samples)} samples. "
f"Labels: {dict(label_dist)}. Categories: {dict(cat_dist)}"
)
def __len__(self) -> int:
return len(self.samples)
def __getitem__(self, idx: int) -> Dict[str, Any]:
s = self.samples[idx]
base = {
"action_label": int(s["action_label"]),
"ce_weight": float(s["ce_weight"]),
"category": s["category"],
"tta_raw": float(s["tta_raw"]), # -1.0 for non_ego / safe_neg
"video_id": s["video_id"],
}
if self._cache is not None:
# Fast path: return pre-computed belief tensors
base["belief"] = self._cache["beliefs"][idx] # [hidden_dim]
base["tta_mean"] = self._cache["tta_means"][idx] # scalar
base["tta_var"] = self._cache["tta_vars"][idx] # scalar
else:
# Slow path: return raw images for VLM encoding.
# Resample frame_indices if a non-default sampling scheme is requested.
base_idx = s["frame_indices"]
if self.sampling != "original" or self.n_frames != MAX_FRAMES:
frame_idx = _resample_indices(base_idx, self.n_frames, self.sampling)
else:
frame_idx = base_idx
base["images"] = _load_frames(s["source_dir"], frame_idx,
n_frames=self.n_frames)
base["metadata"] = s.get("metadata", {})
base["frame_indices_used"] = frame_idx
return base
def policy_collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Works for both image-mode and cache-mode batches."""
out: Dict[str, Any] = {
"action_labels": torch.tensor([b["action_label"] for b in batch], dtype=torch.long),
"ce_weights": torch.tensor([b["ce_weight"] for b in batch], dtype=torch.float32),
"categories": [b["category"] for b in batch],
"tta_raws": torch.tensor([b["tta_raw"] for b in batch], dtype=torch.float32),
"video_ids": [b["video_id"] for b in batch],
}
if "belief" in batch[0]:
# Cache mode
out["beliefs"] = torch.stack([b["belief"] for b in batch]) # [B, H]
out["tta_means"] = torch.stack([b["tta_mean"] for b in batch]) # [B]
out["tta_vars"] = torch.stack([b["tta_var"] for b in batch]) # [B]
else:
# Image mode
out["images"] = [b["images"] for b in batch]
out["metadata"] = [b["metadata"] for b in batch]
return out