File size: 6,339 Bytes
1e05592 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | #!/usr/bin/env python3
"""
DPO Dataset β manifest-based preference pairs for HazardHead alignment.
Each sample is a (chosen, rejected) window pair where:
chosen = window where issuing an alert is CORRECT
(ego_pos, TTA β [1.5, 5.0]s β "timely_alert")
rejected = window where issuing an alert is WRONG
(too_early, too_late, safe_neg, non_ego)
The dataset returns raw PIL frames; the DPO trainer handles VLM tokenisation.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
import torch
from PIL import Image
from torch.utils.data import Dataset
logger = logging.getLogger(__name__)
MAX_FRAMES = 8
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Frame loader (mirrors 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 _load_frames(source_dir: str, frame_indices: List[int]) -> List[Image.Image]:
src = Path(source_dir)
imgs = []
for idx in frame_indices[:MAX_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
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# DPODataset
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class DPODataset(Dataset):
"""
Loads preference pairs from DPO pair manifests.
Args
----
manifests : list of paths to JSON pair manifests (as generated by make_dpo_pairs.py)
split : "train" or "val"
debug : if True, limit to debug_samples pairs
debug_samples : number of pairs to use in debug mode
"""
def __init__(
self,
manifests: List[Path],
split: str = "train",
debug: bool = False,
debug_samples: int = 64,
):
self.split = split
self.pairs: List[dict] = []
for m in manifests:
m = Path(m)
if not m.exists():
logger.warning(f"DPO manifest not found: {m}")
continue
with open(m) as f:
data = json.load(f)
p = data.get("pairs", [])
self.pairs.extend(p)
logger.info(f"Loaded {len(p)} pairs from {m.name}")
if debug:
self.pairs = self.pairs[:debug_samples]
logger.info(
f"DPODataset [{split}]: {len(self.pairs)} pairs "
f"({sum(1 for p in self.pairs if p['pair_type']=='timing')} timing, "
f"{sum(1 for p in self.pairs if p['pair_type']=='category')} category)"
)
def __len__(self) -> int:
return len(self.pairs)
def __getitem__(self, idx: int) -> Dict[str, Any]:
pair = self.pairs[idx]
c = pair["chosen"]
r = pair["rejected"]
chosen_images = _load_frames(c["source_dir"], c["frame_indices"])
rejected_images = _load_frames(r["source_dir"], r["frame_indices"])
return {
"pair_id": pair["pair_id"],
"video_id": pair["video_id"],
"source": pair["source"],
"pair_type": pair["pair_type"],
# chosen
"chosen_images": chosen_images,
"chosen_tta": float(c["tta_true"]),
"chosen_label": c["label"],
"chosen_metadata": c.get("metadata", {}),
# rejected
"rejected_images": rejected_images,
"rejected_tta": float(r["tta_true"]),
"rejected_label": r["label"],
"rejected_metadata":r.get("metadata", {}),
}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Collate
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def dpo_collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
return {
"pair_ids": [b["pair_id"] for b in batch],
"video_ids": [b["video_id"] for b in batch],
"sources": [b["source"] for b in batch],
"pair_types": [b["pair_type"] for b in batch],
# chosen
"chosen_images": [b["chosen_images"] for b in batch],
"chosen_ttas": torch.tensor([b["chosen_tta"] for b in batch], dtype=torch.float32),
"chosen_labels": [b["chosen_label"] for b in batch],
"chosen_metadata": [b["chosen_metadata"] for b in batch],
# rejected
"rejected_images": [b["rejected_images"] for b in batch],
"rejected_ttas": torch.tensor([b["rejected_tta"] for b in batch], dtype=torch.float32),
"rejected_labels": [b["rejected_label"] for b in batch],
"rejected_metadata": [b["rejected_metadata"] for b in batch],
}
|