Spaces:
Paused
Paused
Download sensorflow/seqeval/paired.py from whassan/SensorflowStudio: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/seqeval/paired.py
- Command line
-
hf download hf://spaces/whassan/SensorflowStudio/sensorflow/seqeval/paired.py
-
curl -L -o paired.py https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/seqeval/paired.py
10.2 kB
| """Paired evaluation harness with a fingerprint-keyed prediction cache. | |
| Core design: the SAME sampled units are run through both models. The paired | |
| difference d_i = c_i - b_i removes the between-unit difficulty variance that | |
| dominates two-independent-sample comparisons (Var(d) ~ discordance rate, an | |
| order of magnitude below p(1-p) for adjacent model versions), which is where | |
| most of the sample-size savings in this system come from. | |
| Models are simulated through the megaeval synthetic machinery's population | |
| (dimension codes + difficulty + container structure) with configurable | |
| per-stratum performance effects, so a "2pp pedestrian-night regression" | |
| candidate is constructible deterministically: | |
| SimulatedModel("cand-v2", effects={"pedestrian|night": -0.02, "__global__": 0.004}) | |
| Determinism/pairing: | |
| * a per-OBJECT pairing latent u_i drawn once per dataset (seeded by the | |
| population seed) is shared by every model -> outcomes are paired; | |
| * each model adds a small independent "flip" noise stream seeded by its own | |
| fingerprint -> realistic discordance even at Delta = 0; | |
| * a container-level scene effect (shared by both models) induces honest | |
| intra-cluster correlation for the clustering machinery to handle. | |
| Prediction cache: full-population success vectors keyed by | |
| (dataset fingerprint, model fingerprint) and persisted under | |
| runs/seqeval/cache/. Baseline predictions are computed once and reused across | |
| candidate updates; COMPUTE_COUNTS lets tests assert no recompute happened. | |
| Invalidation is by construction: a new dataset or new checkpoint changes the | |
| fingerprint and therefore the key; stale entries never collide. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import os | |
| import threading | |
| from typing import Dict, Optional, Tuple | |
| import numpy as np | |
| from sensorflow.megaeval import population as pop_mod | |
| from sensorflow.seqeval import ledger as ledger_mod | |
| # Simulation profile (baseline physics of the synthetic detector). | |
| BASE_DETECT = 0.945 | |
| DIFFICULTY_WEIGHT = 0.16 | |
| NIGHT_PENALTY = 0.030 | |
| VRU_PENALTY = 0.020 | |
| SCENE_EFFECT_SD = 0.030 | |
| FLIP_NOISE = 0.04 # per-model chance of drawing a fresh latent | |
| VRU_CLASSES = (1, 2, 3) # pedestrian, cyclist, motorcycle | |
| GLOBAL_EFFECT_KEY = "__global__" | |
| # fingerprint -> number of actual (non-cache) prediction computations | |
| COMPUTE_COUNTS: Dict[str, int] = {} | |
| _COUNT_LOCK = threading.Lock() | |
| class SimulatedModel: | |
| """A model version plus per-stratum performance effects. | |
| effects keys: "class|condition" (e.g. "pedestrian|night") in absolute | |
| detection-probability points, or "__global__" applied everywhere. | |
| """ | |
| def __init__(self, model_version: str, effects: Optional[Dict[str, float]] = None): | |
| self.model_version = model_version | |
| self.effects = dict(effects or {}) | |
| def fingerprint(self) -> str: | |
| blob = json.dumps({"v": self.model_version, | |
| "e": {k: round(float(v), 6) | |
| for k, v in sorted(self.effects.items())}}, | |
| sort_keys=True) | |
| return hashlib.sha256(blob.encode()).hexdigest()[:16] | |
| def to_dict(self) -> Dict: | |
| return {"model_version": self.model_version, "effects": self.effects, | |
| "fingerprint": self.fingerprint()} | |
| def dataset_fingerprint(meta: Dict) -> str: | |
| blob = json.dumps({"pop": meta["population_id"], "seed": meta["seed"], | |
| "n": meta["num_objects"]}, sort_keys=True) | |
| return hashlib.sha256(blob.encode()).hexdigest()[:16] | |
| # ------------------------------------------------------------------ frame | |
| _FRAME_CACHE: Dict[str, Dict[str, np.ndarray]] = {} | |
| _FRAME_LOCK = threading.Lock() | |
| def load_frame(meta: Dict) -> Dict[str, np.ndarray]: | |
| """Full-population sampling frame (dimension codes only), sorted by | |
| object_id so that object_id == row index. Cached in memory per population.""" | |
| pid = meta["population_id"] | |
| with _FRAME_LOCK: | |
| if pid in _FRAME_CACHE: | |
| return _FRAME_CACHE[pid] | |
| cols_needed = ["object_id", "container_id", "class", "lighting", "scenario", | |
| "weather", "difficulty", "safety_critical"] | |
| parts = {k: [] for k in cols_needed} | |
| for p in range(meta["num_partitions"]): | |
| part = pop_mod.load_partition(pid, p) | |
| for k in cols_needed: | |
| parts[k].append(part[k]) | |
| frame = {k: np.concatenate(v) for k, v in parts.items()} | |
| order = np.argsort(frame["object_id"], kind="stable") | |
| frame = {k: v[order] for k, v in frame.items()} | |
| with _FRAME_LOCK: | |
| if len(_FRAME_CACHE) > 4: | |
| _FRAME_CACHE.clear() | |
| _FRAME_CACHE[pid] = frame | |
| return frame | |
| # ------------------------------------------------------------------ latents | |
| def _pair_latents(meta: Dict) -> np.ndarray: | |
| """Per-object pairing latent, shared by all models on this dataset.""" | |
| rng = np.random.default_rng([int(meta["seed"]), 0x5E9, 424242]) | |
| return rng.random(int(meta["num_objects"])) | |
| def _scene_effects(meta: Dict) -> np.ndarray: | |
| """Container-level 'scene hardness' shared by all models (drives ICC).""" | |
| rng = np.random.default_rng([int(meta["seed"]), 777]) | |
| return rng.normal(0.0, SCENE_EFFECT_SD, size=int(meta["num_containers"])) | |
| _LATENT_CACHE: Dict[str, Tuple[np.ndarray, np.ndarray]] = {} | |
| def _latents(meta: Dict) -> Tuple[np.ndarray, np.ndarray]: | |
| pid = meta["population_id"] | |
| if pid not in _LATENT_CACHE: | |
| if len(_LATENT_CACHE) > 4: | |
| _LATENT_CACHE.clear() | |
| _LATENT_CACHE[pid] = (_pair_latents(meta), _scene_effects(meta)) | |
| return _LATENT_CACHE[pid] | |
| # ------------------------------------------------------------------ prediction | |
| def _detect_probability(frame: Dict[str, np.ndarray], meta: Dict, | |
| model: SimulatedModel, condition_dim: str) -> np.ndarray: | |
| difficulty = frame["difficulty"].astype(np.float64) | |
| p = np.full(difficulty.shape, BASE_DETECT) | |
| p -= DIFFICULTY_WEIGHT * difficulty | |
| p -= NIGHT_PENALTY * (frame["lighting"] == 1) | |
| p -= VRU_PENALTY * np.isin(frame["class"], VRU_CLASSES) | |
| _, scene = _latents(meta) | |
| p += scene[frame["container_id"]] | |
| p += float(model.effects.get(GLOBAL_EFFECT_KEY, 0.0)) | |
| cond_vocab = pop_mod.DIMENSIONS[condition_dim] | |
| cls_vocab = pop_mod.DIMENSIONS["class"] | |
| for key, eff in model.effects.items(): | |
| if key == GLOBAL_EFFECT_KEY or "|" not in key: | |
| continue | |
| cls_name, cond_name = key.split("|", 1) | |
| if cls_name not in cls_vocab or cond_name not in cond_vocab: | |
| continue | |
| mask = ((frame["class"] == cls_vocab.index(cls_name)) | |
| & (frame[condition_dim] == cond_vocab.index(cond_name))) | |
| p[mask] += float(eff) | |
| return np.clip(p, 0.01, 0.995) | |
| def _compute_predictions(meta: Dict, model: SimulatedModel, | |
| condition_dim: str) -> np.ndarray: | |
| frame = load_frame(meta) | |
| p = _detect_probability(frame, meta, model, condition_dim) | |
| u_pair, _ = _latents(meta) | |
| fp_int = int(model.fingerprint(), 16) | |
| rng = np.random.default_rng([fp_int % (2**32), int(meta["seed"]), 7]) | |
| n = u_pair.size | |
| swap = rng.random(n) < FLIP_NOISE | |
| fresh = rng.random(n) | |
| u = np.where(swap, fresh, u_pair) | |
| return (u < p).astype(np.uint8) | |
| class PredictionCache: | |
| """Disk + memory cache of full-population success vectors. | |
| Key = (dataset fingerprint, model fingerprint). Both fingerprints cover | |
| everything the predictions depend on, so invalidation is automatic: any | |
| change to the dataset (new population/seed/size) or the model (version or | |
| effects) produces a new key. | |
| """ | |
| def __init__(self): | |
| self._mem: Dict[str, np.ndarray] = {} | |
| self._lock = threading.Lock() | |
| def _path(self, key: str) -> str: | |
| return os.path.join(ledger_mod.seq_root(), "cache", f"{key}.npz") | |
| def get_or_compute(self, meta: Dict, model: SimulatedModel, | |
| condition_dim: str = "lighting") -> np.ndarray: | |
| key = f"{dataset_fingerprint(meta)}-{model.fingerprint()}" | |
| with self._lock: | |
| if key in self._mem: | |
| return self._mem[key] | |
| path = self._path(key) | |
| if os.path.exists(path): | |
| with np.load(path) as z: | |
| success = z["success"] | |
| else: | |
| success = _compute_predictions(meta, model, condition_dim) | |
| with _COUNT_LOCK: | |
| COMPUTE_COUNTS[key] = COMPUTE_COUNTS.get(key, 0) + 1 | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| np.savez_compressed(path, success=success, | |
| model_version=np.array([model.model_version])) | |
| with self._lock: | |
| if len(self._mem) > 16: | |
| self._mem.clear() | |
| self._mem[key] = success | |
| return success | |
| def clear_memory(self) -> None: | |
| with self._lock: | |
| self._mem.clear() | |
| _CACHE: Optional[PredictionCache] = None | |
| _CACHE_LOCK = threading.Lock() | |
| def get_prediction_cache() -> PredictionCache: | |
| global _CACHE | |
| with _CACHE_LOCK: | |
| if _CACHE is None: | |
| _CACHE = PredictionCache() | |
| return _CACHE | |
| def reset_prediction_cache() -> None: | |
| global _CACHE | |
| with _CACHE_LOCK: | |
| _CACHE = None | |
| with _FRAME_LOCK: | |
| _FRAME_CACHE.clear() | |
| _LATENT_CACHE.clear() | |
| with _COUNT_LOCK: | |
| COMPUTE_COUNTS.clear() | |
| def paired_outcomes(meta: Dict, baseline: SimulatedModel, candidate: SimulatedModel, | |
| object_ids: np.ndarray, condition_dim: str = "lighting" | |
| ) -> Dict[str, np.ndarray]: | |
| """Per-unit paired outcomes for the given objects (baseline served from | |
| cache; the same sampled units go through both models).""" | |
| cache = get_prediction_cache() | |
| b_full = cache.get_or_compute(meta, baseline, condition_dim) | |
| c_full = cache.get_or_compute(meta, candidate, condition_dim) | |
| idx = np.asarray(object_ids, dtype=np.int64) | |
| b = b_full[idx].astype(np.int8) | |
| c = c_full[idx].astype(np.int8) | |
| return {"baseline": b, "candidate": c, "d": (c - b).astype(np.float64)} | |