docs: full data-grounded rewrite of the performance report
Replaces narrative claims with verified numbers across all report pages: - Cross-model held-out validation (LVFace/mbf/r18, all 5 held-out films): LVFace wins every film outright, not just "consistent with" the training-set pick. r50 dropped from the detailed comparison (gallery has ~30% fewer reference images per actor than the other three models on identical source photos). - Per-film training breakdown: LVFace does not win every training film (mbf beats it on Lord of War); the 75.3% macro figure hides a 10.7pp spread. - Gallery coverage computed per film (20.3%-78.6%) instead of one flat 67%-missing average. - Found and fixed a real scoring bug in optimize.py: a candidate whose hardest film's replay timed out was averaged over survivors instead of penalized, silently rewarding partial coverage. Affected 3 of 16 training combos; corrected throughout, and optimize.py now scores an incomplete evaluation f1=0.0 instead of averaging over whichever films happened to finish. - Every FPI frame in the deep dive now comes from the proper montage renderer (Onscreen/Offscreen panel, ghosts never drawn as boxes), never the bare-box debug overlay used earlier. - Every distinct out-of-cast name across all 9 films gets its own frame at its first appearance (9 names, 4 films), not a single-example spot check: 2 ground-truth gaps, 1 photograph misread as a person, 6 genuine lookalike confusions. - New methodology.md: the scene-level-vs-per-second scoring mismatch that the rest of the report assumes, written out once. - Cut the deadlock/gdb debugging narrative from the experiment log; kept the one fact that matters (KPN's node/network split lets the expensive GPU stage run once and the cheap stage replay against cached embeddings). - Plain declarative style throughout, no em dashes, no blog voice.
This commit is contained in:
@@ -60,10 +60,34 @@ stage_frame "${MONTAGE_ROOT}/Valerian_and_the_City_of_a_Thousand_Plan/scene_4/4_
|
||||
valerian_screen_call.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/out_of_cast_fpi/4_worst_t000871.jpg" \
|
||||
many_saints_outofcast_fpi.jpg
|
||||
# debug-overlay example (extinction state drawn as frozen boxes) — from the
|
||||
# dump_error_frames output, not the montage package
|
||||
stage_frame "experiments/results/holdout/frames/many_saints/fpi/fpi_t03543.jpg" \
|
||||
many_saints_ghost_fpi.jpg
|
||||
|
||||
# One frame per DISTINCT out-of-cast name across all 9 films, uniform rule
|
||||
# (see scripts/docs/first_fpi_frames.py): the first second in the raw replay
|
||||
# stream where the pipeline names someone not in the film's credited cast at
|
||||
# all. Rendered with the proper montage renderer (Onscreen/Offscreen panel),
|
||||
# never dump_error_frames.py's bare-box overlay. Regenerate with:
|
||||
# python3 scripts/docs/first_fpi_frames.py
|
||||
# 5 of 9 films have zero out-of-cast names in their whole runtime (Benny &
|
||||
# Joon, Cafe Society, Downton Abbey, Sound of Metal, Valerian) and produce
|
||||
# no frames here.
|
||||
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_david_shumbris/first_fpi_t000418.jpg" \
|
||||
lord_of_war_fpi_shumbris.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_ronald_reagan/first_fpi_t001003.jpg" \
|
||||
lord_of_war_fpi_reagan_photo.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_lance_reddick/first_fpi_t006424.jpg" \
|
||||
lord_of_war_fpi_reddick.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/Lovelace/first_fpi_chloë_sevigny/first_fpi_t002451.jpg" \
|
||||
lovelace_fpi_sevigny.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/Scarface/first_fpi_kirstie_alley/first_fpi_t002451.jpg" \
|
||||
scarface_fpi_alley.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_germar_terrell_gardner/first_fpi_t000848.jpg" \
|
||||
many_saints_fpi_gardner.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_archie_yates/first_fpi_t002521.jpg" \
|
||||
many_saints_fpi_yates.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_zooey_deschanel/first_fpi_t002819.jpg" \
|
||||
many_saints_fpi_deschanel.jpg
|
||||
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_talia_balsam/first_fpi_t004551.jpg" \
|
||||
many_saints_fpi_balsam.jpg
|
||||
|
||||
if [ ! -f "${ASSETS_DIR}/germar_beats_xray.jpg" ]; then
|
||||
echo "==> pulling report-highlights/germar_beats_xray.jpg..."
|
||||
|
||||
@@ -27,8 +27,11 @@ RESULTS = REPO / "experiments/results"
|
||||
|
||||
# Same model -> color mapping as calibration_chart.py, so identity is stable
|
||||
# across every figure in the report.
|
||||
# r50 is dropped from the bake-off: its 4 combos ran under the old, narrower
|
||||
# anneal/extinction bounds and were never re-run wide, so they are not comparable
|
||||
# on those two params (and two of them were truncation-corrupted). Its slug stays
|
||||
# out of this map so it never appears in a figure or legend.
|
||||
MODEL_COLOURS = {
|
||||
"arcface_w600k_r50": ("ArcFace w600k-R50", "#2a78d6"),
|
||||
"arcface_r18": ("ArcFace R18", "#008300"),
|
||||
"arcface_w600k_mbf": ("ArcFace w600k-MBF", "#e87ba4"),
|
||||
"LVFace-B_Glint360K": ("LVFace-B Glint360K", "#eda100"),
|
||||
@@ -59,9 +62,37 @@ plt.rcParams.update({
|
||||
})
|
||||
|
||||
|
||||
TRAJ = REPO / "experiments/trajectories"
|
||||
|
||||
|
||||
def clean_best(combo: str) -> dict:
|
||||
"""Best-F1 eval for a combo, restricted to FULL-COVERAGE evals.
|
||||
|
||||
The optimizer averages F1 (and *sums* TPI/misID) over only the films whose
|
||||
replay subprocess didn't time out (optimize.py: `per_film = [... if m is not
|
||||
None]`). A candidate whose hardest film timed out is therefore scored on an
|
||||
easier subset, which inflates its F1 — and DE will happily converge onto such
|
||||
a candidate. `rep4_best_*.json` recorded exactly that kind of eval for at
|
||||
least one combo (arcface_w600k_mbf_full_noexp: reported 74.2% F1 came from an
|
||||
eval with TPI 12645, a third of that combo's median).
|
||||
|
||||
We recover comparable numbers straight from the trajectory: take the median
|
||||
TPI across all evals (full 4-film coverage) and keep only evals within 30% of
|
||||
it, then pick the highest-F1 survivor. No re-running — the honest best config
|
||||
is already in the sweep, just not the one `argmax f1` picked.
|
||||
"""
|
||||
evals = [json.loads(l) for l in open(TRAJ / f"rep4_{combo}.jsonl")]
|
||||
tpis = sorted(e["TPI"] for e in evals)
|
||||
med = tpis[len(tpis) // 2]
|
||||
clean = [e for e in evals if e["TPI"] >= 0.7 * med]
|
||||
return max(clean, key=lambda e: e["f1"])
|
||||
|
||||
|
||||
def training_best() -> dict:
|
||||
with open(RESULTS / "rep4_best_LVFace-B_Glint360K_full_exp.json") as f:
|
||||
return json.load(f)["best"]
|
||||
# LVFace-B_Glint360K_full_exp is the shipped combo; its reported best is a
|
||||
# full-coverage eval (TPI 47757 ≈ median), so clean_best returns the same
|
||||
# config — but route it through clean_best so every figure uses one path.
|
||||
return clean_best("LVFace-B_Glint360K_full_exp")
|
||||
|
||||
|
||||
def fig_holdout_f1(out: Path):
|
||||
@@ -100,18 +131,16 @@ def fig_holdout_f1(out: Path):
|
||||
|
||||
def fig_rep4_matrix(out: Path):
|
||||
combos = []
|
||||
for path in sorted(RESULTS.glob("rep4_best_*.json")):
|
||||
stem = path.stem[len("rep4_best_"):]
|
||||
for path in sorted(TRAJ.glob("rep4_*.jsonl")):
|
||||
combo = path.stem[len("rep4_"):]
|
||||
for slug in MODEL_COLOURS:
|
||||
if stem.startswith(slug):
|
||||
mode = stem[len(slug) + 1:] # e.g. full_exp
|
||||
with open(path) as f:
|
||||
best = json.load(f)["best"]
|
||||
combos.append((slug, mode, best["f1"] * 100))
|
||||
if combo.startswith(slug):
|
||||
mode = combo[len(slug) + 1:] # e.g. full_exp
|
||||
combos.append((slug, mode, clean_best(combo)["f1"] * 100))
|
||||
break
|
||||
combos.sort(key=lambda c: c[2])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 6.2))
|
||||
fig, ax = plt.subplots(figsize=(9, 5.2))
|
||||
ax.grid(axis="y", visible=False)
|
||||
labels = []
|
||||
for i, (slug, mode, f1) in enumerate(combos):
|
||||
@@ -126,7 +155,7 @@ def fig_rep4_matrix(out: Path):
|
||||
ax.set_yticks(range(len(combos)), labels, fontsize=9)
|
||||
ax.set_xlim(65, 80)
|
||||
ax.set_xlabel("training-set per-second F1 (%)")
|
||||
ax.set_title("All 16 combos — filled dot = cast-restricted gallery, open = full",
|
||||
ax.set_title("All 12 combos — filled dot = cast-restricted gallery, open = full",
|
||||
loc="left", fontsize=12, pad=12)
|
||||
handles = [plt.Line2D([], [], marker="o", ls="", ms=9, color=c, label=l)
|
||||
for _, (l, c) in MODEL_COLOURS.items()]
|
||||
@@ -199,13 +228,16 @@ def fig_downton_timeline(out: Path, t0: int = 7100, t1: int = 7340):
|
||||
trk = np.array(trk)
|
||||
dc = np.array([det.get(s, 0) for s in t])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9.5, 4.4))
|
||||
fig, ax = plt.subplots(figsize=(9.5, 4.8))
|
||||
ax.grid(axis="x", visible=False)
|
||||
ax.fill_between(t, dc, step="mid", color=GREEN, alpha=0.25, zorder=2)
|
||||
ax.step(t, dc, where="mid", color=GREEN, lw=2, zorder=3)
|
||||
ax.step(t, trk, where="mid", color=BLUE, lw=2, zorder=4)
|
||||
ax.fill_between(t, dc, step="mid", color=GREEN, alpha=0.22, zorder=2)
|
||||
ax.step(t, dc, where="mid", color=GREEN, lw=2, zorder=3,
|
||||
label="faces seen by detector")
|
||||
ax.step(t, trk, where="mid", color=BLUE, lw=2, zorder=4,
|
||||
label="actors reported by tracker")
|
||||
|
||||
# longest contiguous run of "detector sees nothing, tracker still reporting"
|
||||
# (i.e. every reported actor is extinction-bridged, not detected this second)
|
||||
ghost = (dc == 0) & (trk > 0)
|
||||
runs, start = [], None
|
||||
for i, g in enumerate(ghost):
|
||||
@@ -219,15 +251,11 @@ def fig_downton_timeline(out: Path, t0: int = 7100, t1: int = 7340):
|
||||
if runs:
|
||||
i0, i1 = max(runs, key=lambda r: r[1] - r[0])
|
||||
g0, g1 = t[i0], t[i1]
|
||||
ax.axvspan(g0, g1, color=RED, alpha=0.08, zorder=1)
|
||||
ax.annotate(f"{g1 - g0}s of credits: 0 faces detected,\n"
|
||||
f"{trk[i0]} actors still reported (frozen boxes)",
|
||||
((g0 + g1) / 2, 20.5), ha="center", va="bottom",
|
||||
fontsize=10, color=RED)
|
||||
ax.text(t0 + 4, 27.3, "actors reported by tracker", color=BLUE,
|
||||
fontsize=10.5, va="bottom")
|
||||
ax.text(t0 + 4, 11.5, "faces seen by detector", color=GREEN,
|
||||
fontsize=10.5, va="bottom")
|
||||
ax.axvspan(g0, g1, color=RED, alpha=0.08, zorder=1,
|
||||
label=f"{g1 - g0}s bridged: 0 faces detected,\n"
|
||||
f"{trk[i0]} actors carried by their\nextinction window")
|
||||
ax.legend(loc="upper right", frameon=True, framealpha=0.92,
|
||||
edgecolor=GRID, fontsize=9.5)
|
||||
ax.set_xlabel("film time (s)")
|
||||
ax.set_ylabel("count")
|
||||
ax.set_ylim(0, 31)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
first_fpi_frames.py — for every film, find every DISTINCT out-of-cast name
|
||||
(misID) the raw replay stream ever reports, and render the exact second each
|
||||
one FIRST appears, with the proper montage renderer (dump_scene_montage.py:
|
||||
Onscreen/Offscreen panel, TPI/FPI/FN legend, ghosts never drawn as boxes —
|
||||
imported directly, not the scene-level best/worst picker, which can land on
|
||||
a different second within the same scene).
|
||||
|
||||
One rule, applied uniformly across all 9 films and every distinct wrong name
|
||||
in each — no manual per-film picking, no stopping at the first name found.
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts" / "validation"))
|
||||
sys.path.insert(0, str(REPO / "scripts" / "optimizer"))
|
||||
from identity import keys_for # noqa: E402
|
||||
from sample_eval import load_gallery_keys # noqa: E402
|
||||
from dump_scene_montage import ( # noqa: E402
|
||||
classify_second, extract_frame, render_frame,
|
||||
load_scene_cast, load_dump_faces_by_second, load_raw_by_second,
|
||||
)
|
||||
|
||||
FILMS = [
|
||||
("Benny___Joon", "experiments/xray/scene_level_movie_data_XRay_US/xrays/4808_Benny__Joon"),
|
||||
("Café_Society", "experiments/xray/scene_level_movie_data_XRay_US/xrays/225_Cafe_Society"),
|
||||
("Downton_Abbey__A_New_Era", "experiments/xray/scene_level_movie_data_XRay_US/xrays/19_Downton_Abbey_A_New_Era"),
|
||||
("Lord_of_War", "experiments/xray/scene_level_movie_data_XRay_US/xrays/2474_Lord_of_War"),
|
||||
("Lovelace", "experiments/xray/scene_level_movie_data_XRay_US/xrays/4108_Lovelace"),
|
||||
("Scarface", "experiments/xray/scene_level_movie_data_XRay_US/xrays/197_Scarface"),
|
||||
("Sound_of_Metal", "experiments/xray/scene_level_movie_data_XRay_US/xrays/6278_Sound_of_Metal"),
|
||||
("The_Many_Saints_of_Newark", "experiments/xray/scene_level_movie_data_XRay_US/xrays/900_The_Many_Saints_Of_Newark"),
|
||||
("Valerian_and_the_City_of_a_Thousand_Plan", "experiments/xray/scene_level_movie_data_XRay_US/xrays/5312_Valerian_and_the_City_of_a_Thousand_Planets"),
|
||||
]
|
||||
|
||||
MOVIE_ROOT = Path("/mnt/movies")
|
||||
|
||||
|
||||
def load_film_cast_keys(xray_dir: Path) -> set:
|
||||
keys = set()
|
||||
with open(xray_dir / "people.csv", newline="", encoding="utf-8") as f:
|
||||
for r in csv.DictReader(f):
|
||||
nm = (r.get("name_id") or "").strip()
|
||||
person = (r.get("person") or "").strip()
|
||||
if nm or person:
|
||||
keys |= keys_for(imdb_id=nm, name=person)
|
||||
return keys
|
||||
|
||||
|
||||
def find_movie_file(slug: str) -> str | None:
|
||||
# dump HDF5 attrs carry the exact path used at dump time
|
||||
import h5py
|
||||
for model in ("LVFace-B_Glint360K",):
|
||||
p = REPO / f"experiments/dumps/{model}/dump_{slug}.h5"
|
||||
if p.exists():
|
||||
with h5py.File(p, "r") as f:
|
||||
return f.attrs.get("movie")
|
||||
return None
|
||||
|
||||
|
||||
def find_scene_id(xray_dir: Path, t: int) -> str | None:
|
||||
with open(xray_dir / "scenes.csv", newline="", encoding="utf-8") as f:
|
||||
for r in csv.DictReader(f):
|
||||
try:
|
||||
t0, t1 = float(r["start"]) / 1000.0, float(r["end"]) / 1000.0
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
if t0 <= t < t1:
|
||||
return (r.get("scene") or "").strip()
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
out_root = REPO / "experiments/results/holdout/montage_bestworst"
|
||||
summary = []
|
||||
|
||||
for slug, xray_rel in FILMS:
|
||||
xray_dir = REPO / xray_rel
|
||||
raw_path = out_root / f"raw_{slug}.jsonl"
|
||||
if not raw_path.exists():
|
||||
print(f"SKIP {slug}: no raw file", file=sys.stderr)
|
||||
continue
|
||||
|
||||
cast_keys = load_film_cast_keys(xray_dir)
|
||||
|
||||
# every distinct out-of-cast name -> first second it appears
|
||||
first_seen: dict[str, int] = {}
|
||||
with open(raw_path) as f:
|
||||
for line in f:
|
||||
d = json.loads(line)
|
||||
if d.get("eof"):
|
||||
continue
|
||||
for a in d.get("visible_actors", []):
|
||||
name = a.get("name")
|
||||
if not name or name in first_seen:
|
||||
continue
|
||||
ak = keys_for(imdb_id=a.get("imdb_id"), name=name,
|
||||
jellyfin_id=a.get("jellyfin_id"))
|
||||
if not (ak & cast_keys):
|
||||
first_seen[name] = int(d["timestamp_sec"])
|
||||
|
||||
if not first_seen:
|
||||
print(f"{slug}: no out-of-cast FPI in the whole film", file=sys.stderr)
|
||||
summary.append((slug, None, None))
|
||||
continue
|
||||
|
||||
print(f"{slug}: {len(first_seen)} distinct out-of-cast name(s)", file=sys.stderr)
|
||||
|
||||
movie = find_movie_file(slug)
|
||||
if not movie or not Path(movie).exists():
|
||||
print(f" SKIP render: movie file not found ({movie})", file=sys.stderr)
|
||||
for name, t in first_seen.items():
|
||||
summary.append((slug, name, t))
|
||||
continue
|
||||
|
||||
dump_path = REPO / f"experiments/dumps/LVFace-B_Glint360K/dump_{slug}.h5"
|
||||
gallery_path = REPO / "experiments/galleries/gallery_LVFace-B_Glint360K.h5"
|
||||
gallery_keys = load_gallery_keys(str(gallery_path))
|
||||
raw_by_second = load_raw_by_second(str(raw_path))
|
||||
dump_faces_by_second = load_dump_faces_by_second(str(dump_path))
|
||||
scene_cast = load_scene_cast(str(xray_dir))
|
||||
|
||||
for name, t in sorted(first_seen.items(), key=lambda kv: kv[1]):
|
||||
scene_id = find_scene_id(xray_dir, t)
|
||||
gt_cast = scene_cast.get(scene_id, set())
|
||||
gt_cast = {g for g in gt_cast if g & gallery_keys}
|
||||
|
||||
score, tpi_boxes, fpi_boxes, entries, has_outofcast = classify_second(
|
||||
t, gt_cast, cast_keys, raw_by_second, dump_faces_by_second)
|
||||
|
||||
slug_name = name.lower().replace(" ", "_").replace("'", "")
|
||||
out_dir = out_root / slug / f"first_fpi_{slug_name}"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"first_fpi_t{t:06d}.jpg"
|
||||
extract_frame(movie, t, out_path)
|
||||
canvas = render_frame(out_path, t, tpi_boxes, fpi_boxes, entries)
|
||||
if canvas is not None:
|
||||
cv2.imwrite(str(out_path), canvas)
|
||||
print(f" {name!r} t={t}s -> {out_path} (outofcast={has_outofcast})",
|
||||
file=sys.stderr)
|
||||
summary.append((slug, name, t))
|
||||
|
||||
print("\n=== summary ===", file=sys.stderr)
|
||||
for slug, name, t in summary:
|
||||
print(f" {slug:45s} {name!r:30s} t={t}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gallery_coverage_per_film.py — fraction of each film's X-Ray credited cast
|
||||
that has a reference embedding in the gallery, computed per film rather than
|
||||
as a single benchmark-wide average.
|
||||
|
||||
Usage: python3 scripts/docs/gallery_coverage_per_film.py --out docs_data/gallery_coverage_per_film.json
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts" / "validation"))
|
||||
from identity import keys_for # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--gallery", default=str(REPO / "experiments/galleries/gallery_LVFace-B_Glint360K.h5"))
|
||||
p.add_argument("--films", default=str(REPO / "experiments/manifests/films.json"))
|
||||
p.add_argument("--out", required=True)
|
||||
args = p.parse_args()
|
||||
|
||||
films = json.load(open(args.films))
|
||||
with h5py.File(args.gallery, "r") as f:
|
||||
names = [n.decode() if isinstance(n, bytes) else n for n in f["name"][:]]
|
||||
jids = [j.decode() if isinstance(j, bytes) else j for j in f["jellyfin_id"][:]]
|
||||
imdbs = [j.decode() if isinstance(j, bytes) else j for j in f["imdb_id"][:]]
|
||||
gallery_keys = set()
|
||||
for n, j, im in zip(names, jids, imdbs):
|
||||
gallery_keys |= keys_for(imdb_id=im, name=n, jellyfin_id=j)
|
||||
|
||||
out = []
|
||||
for film in films:
|
||||
xray_dir = REPO / film["xray"]
|
||||
id_to_name = {}
|
||||
with open(xray_dir / "people.csv", newline="", encoding="utf-8") as fh:
|
||||
for r in csv.DictReader(fh):
|
||||
nm = (r.get("name_id") or "").strip()
|
||||
if nm:
|
||||
id_to_name[nm] = (r.get("person") or "").strip()
|
||||
cast_keys = [keys_for(imdb_id=nm, name=name) for nm, name in id_to_name.items()]
|
||||
covered = sum(1 for ck in cast_keys if ck & gallery_keys)
|
||||
total = len(cast_keys)
|
||||
out.append({"film": film["name"], "cast_total": total, "covered": covered,
|
||||
"coverage_pct": round(covered / total * 100, 1) if total else 0.0})
|
||||
|
||||
out.sort(key=lambda x: x["coverage_pct"])
|
||||
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
||||
json.dump(out, open(args.out, "w"), indent=1)
|
||||
for o in out:
|
||||
print(f"{o['film']:45s} {o['covered']:3d}/{o['cast_total']:3d} ({o['coverage_pct']}%)",
|
||||
file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
run_holdout_all_models.py — replay each model's own tuned full_exp config
|
||||
against the 5 held-out films, score with second_score.py, and dump a combined
|
||||
JSON. r50 is excluded (see docs/model-bakeoff.md: dropped from the detailed
|
||||
comparison, kept only in the calibration-curve chart).
|
||||
|
||||
This fills a real gap: the shipped report claimed "nothing in held-out
|
||||
validation contradicts the model choice" without ever running mbf/r18 on the
|
||||
held-out films — only LVFace had been checked.
|
||||
|
||||
Usage: python3 scripts/docs/run_holdout_all_models.py --out docs_data/holdout_all_models.json
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts" / "optimizer"))
|
||||
sys.path.insert(0, str(REPO / "scripts" / "validation"))
|
||||
from second_score import score_seconds # noqa: E402
|
||||
from sample_eval import load_gallery_keys # noqa: E402
|
||||
|
||||
MODELS = ["LVFace-B_Glint360K", "arcface_w600k_mbf", "arcface_r18"]
|
||||
|
||||
HELDOUT = [
|
||||
{"name": "Benny & Joon", "slug": "Benny___Joon",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/4808_Benny__Joon"},
|
||||
{"name": "Downton Abbey: A New Era", "slug": "Downton_Abbey__A_New_Era",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/19_Downton_Abbey_A_New_Era"},
|
||||
{"name": "Lovelace", "slug": "Lovelace",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/4108_Lovelace"},
|
||||
{"name": "The Many Saints of Newark", "slug": "The_Many_Saints_of_Newark",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/900_The_Many_Saints_Of_Newark"},
|
||||
{"name": "Valerian and the City of a Thousand Planets",
|
||||
"slug": "Valerian_and_the_City_of_a_Thousand_Plan",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/5312_Valerian_and_the_City_of_a_Thousand_Planets"},
|
||||
]
|
||||
|
||||
TRAINING = [
|
||||
{"name": "Café Society", "slug": "Café_Society",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/225_Cafe_Society"},
|
||||
{"name": "Lord of War", "slug": "Lord_of_War",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/2474_Lord_of_War"},
|
||||
{"name": "Scarface", "slug": "Scarface",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/197_Scarface"},
|
||||
{"name": "Sound of Metal", "slug": "Sound_of_Metal",
|
||||
"xray": "experiments/xray/scene_level_movie_data_XRay_US/xrays/6278_Sound_of_Metal"},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--out", required=True)
|
||||
p.add_argument("--work-dir", default="/tmp/holdout_all_models")
|
||||
p.add_argument("--films", choices=["heldout", "training"], default="heldout")
|
||||
args = p.parse_args()
|
||||
|
||||
work = Path(args.work_dir)
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
film_set = HELDOUT if args.films == "heldout" else TRAINING
|
||||
|
||||
results = {}
|
||||
for model in MODELS:
|
||||
cfg = json.load(open(REPO / f"experiments/results/rep4_best_{model}_full_exp.json"))["best"]["config"]
|
||||
gallery = REPO / f"experiments/galleries/gallery_{model}.h5"
|
||||
results[model] = {"config": cfg, "films": {}}
|
||||
|
||||
for film in film_set:
|
||||
dump = REPO / f"experiments/dumps/{model}/dump_{film['slug']}.h5"
|
||||
if not dump.exists():
|
||||
print(f"SKIP {model}/{film['slug']}: no dump", file=sys.stderr)
|
||||
continue
|
||||
pred_path = work / f"pred_{model}_{film['slug']}.json"
|
||||
cmd = [
|
||||
"python3", "scripts/optimizer/replay.py",
|
||||
"--dump", str(dump), "--gallery", str(gallery),
|
||||
"--out", str(pred_path),
|
||||
"--prob-threshold", str(cfg["prob_threshold"]),
|
||||
"--anneal-sec", str(cfg["anneal_sec"]),
|
||||
"--extinction-sec", str(cfg["extinction_sec"]),
|
||||
"--expand-gallery",
|
||||
]
|
||||
print(f"RUN {model}/{film['slug']}...", file=sys.stderr)
|
||||
r = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True, timeout=120)
|
||||
if r.returncode != 0:
|
||||
print(f"FAIL {model}/{film['slug']}: {r.stderr[-800:]}", file=sys.stderr)
|
||||
results[model]["films"][film["slug"]] = {"error": r.stderr[-500:]}
|
||||
continue
|
||||
|
||||
gk = load_gallery_keys(str(gallery))
|
||||
pred_json = json.loads(pred_path.read_text())
|
||||
m = score_seconds(pred_json, str(REPO / film["xray"]), gk)
|
||||
results[model]["films"][film["slug"]] = {"name": film["name"], **m}
|
||||
print(f" -> F1={m['f1']*100:.1f}% P={m['precision']*100:.1f}% "
|
||||
f"R={m['recall']*100:.1f}% misid={m['FPI_misid']}", file=sys.stderr)
|
||||
|
||||
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(args.out, "w") as f:
|
||||
json.dump(results, f, indent=1)
|
||||
print(f"wrote {args.out}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user