Download analysis/dataset_level_analysis/analyze_dataset_level.py from LLDDSS/Stereo_Depth: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/LLDDSS/Stereo_Depth/resolve/main/analysis/dataset_level_analysis/analyze_dataset_level.py
- Command line
-
hf download hf://LLDDSS/Stereo_Depth/analysis/dataset_level_analysis/analyze_dataset_level.py
-
curl -L -o analyze_dataset_level.py https://huggingface.co/LLDDSS/Stereo_Depth/resolve/main/analysis/dataset_level_analysis/analyze_dataset_level.py
12.2 kB
| #!/usr/bin/env python3 | |
| """Compare every model across the four evaluation splits. | |
| python analysis/dataset_level_analysis/analyze_dataset_level.py | |
| Reads result/<model>/<task>/baseline_<b>/{metrics.json,predictions.jsonl} and | |
| writes the CSVs next to this script. | |
| Raw accuracy is not comparable across the four splits: VSR is a two-way | |
| true/false question while the three Youtube levels are four-way, and level 2 | |
| carries a structural floor of its own (see FLOORS). Everything is therefore | |
| also reported as *headroom captured*, (acc - floor) / (1 - floor): the share of | |
| the distance between guessing and perfect that the model actually covers. It | |
| is negative when a model does worse than the floor, which happens here. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import collections | |
| import csv | |
| import json | |
| import math | |
| import os | |
| import statistics | |
| BASELINES = ["0.00", "0.05", "0.10", "0.15", "0.20", "0.25"] | |
| TASKS = ["vsr", "youtube_level1", "youtube_level2", "youtube_level3"] | |
| # What a model scores on each split without looking at the image: pure guessing | |
| # among the choices. Level 2's three distractors are drawn uniformly from the | |
| # 23 wrong orderings, so the four options are exchangeable and every text-only | |
| # heuristic sits at chance -- measured on the split: prefix vote 0.252, medoid | |
| # 0.256, outlier 0.254, positional majority 0.245. Earlier structured variants | |
| # did NOT have this property; see disparity_qa/level2_choices.py. | |
| FLOORS = {"vsr": 0.500, "youtube_level1": 0.250, | |
| "youtube_level2": 0.250, "youtube_level3": 0.250} | |
| CHANCE = dict(FLOORS) | |
| def load_metrics(root: str, model: str, task: str, baseline: str) -> dict: | |
| with open(os.path.join(root, "result", model, task, | |
| f"baseline_{baseline}", "metrics.json")) as fh: | |
| return json.load(fh) | |
| def load_predictions(root: str, model: str, task: str, baseline: str): | |
| path = os.path.join(root, "result", model, task, | |
| f"baseline_{baseline}", "predictions.jsonl") | |
| with open(path) as fh: | |
| for line in fh: | |
| line = line.strip() | |
| if line: | |
| yield json.loads(line) | |
| def entropy(counter: collections.Counter) -> float: | |
| n = sum(counter.values()) | |
| if not n: | |
| return 0.0 | |
| return -sum((v / n) * math.log2(v / n) for v in counter.values() if v) | |
| def slope(xs, ys) -> float: | |
| """Least-squares slope of accuracy against baseline (per unit of baseline).""" | |
| mx, my = statistics.mean(xs), statistics.mean(ys) | |
| den = sum((x - mx) ** 2 for x in xs) | |
| return sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den if den else 0.0 | |
| def main() -> int: | |
| here = os.path.dirname(os.path.abspath(__file__)) | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--root", default=os.path.abspath(os.path.join(here, "..", ".."))) | |
| p.add_argument("--out_dir", default=here) | |
| args = p.parse_args() | |
| models = sorted(m for m in os.listdir(os.path.join(args.root, "result")) | |
| if not m.startswith("_") | |
| and os.path.isdir(os.path.join(args.root, "result", m))) | |
| acc = {} # (model, task, baseline) -> metrics dict | |
| for m in models: | |
| for t in TASKS: | |
| for b in BASELINES: | |
| acc[(m, t, b)] = load_metrics(args.root, m, t, b) | |
| # ---------------------------------------------------------- per baseline | |
| rows = [] | |
| for m in models: | |
| for t in TASKS: | |
| for b in BASELINES: | |
| d = acc[(m, t, b)] | |
| rows.append({ | |
| "model": m, "task": t, "baseline": b, | |
| "n": d["num_samples"], "accuracy": round(d["accuracy"], 4), | |
| "num_correct": d["num_correct"], | |
| "unparsed_rate": round(d["unparsed_rate"], 4), | |
| "scoring": d["scoring"], | |
| "chance": CHANCE[t], "floor": FLOORS[t], | |
| "headroom_captured": round( | |
| (d["accuracy"] - FLOORS[t]) / (1 - FLOORS[t]), 4), | |
| }) | |
| write_csv(os.path.join(args.out_dir, "accuracy_by_baseline.csv"), rows) | |
| # ------------------------------------------------------- per model/task | |
| summary = [] | |
| for m in models: | |
| for t in TASKS: | |
| a = [acc[(m, t, b)]["accuracy"] for b in BASELINES] | |
| xs = [float(b) for b in BASELINES] | |
| summary.append({ | |
| "model": m, "task": t, "n": acc[(m, t, "0.00")]["num_samples"], | |
| "scoring": acc[(m, t, "0.00")]["scoring"], | |
| "chance": CHANCE[t], "floor": FLOORS[t], | |
| "acc_b0": round(a[0], 4), "acc_mean": round(statistics.mean(a), 4), | |
| "acc_min": round(min(a), 4), "acc_max": round(max(a), 4), | |
| "acc_range": round(max(a) - min(a), 4), | |
| "acc_std": round(statistics.pstdev(a), 4), | |
| "slope_per_baseline": round(slope(xs, a), 4), | |
| "delta_b025_minus_b0": round(a[-1] - a[0], 4), | |
| "headroom_captured_mean": round( | |
| (statistics.mean(a) - FLOORS[t]) / (1 - FLOORS[t]), 4), | |
| "above_floor": statistics.mean(a) > FLOORS[t], | |
| }) | |
| write_csv(os.path.join(args.out_dir, "summary_by_model_task.csv"), summary) | |
| # --------------------------------------------------------------- biases | |
| bias = [] | |
| for m in models: | |
| for t in TASKS: | |
| for b in BASELINES: | |
| pred = collections.Counter() | |
| ref = collections.Counter() | |
| n = 0 | |
| for r in load_predictions(args.root, m, t, b): | |
| pred[r["prediction_norm"]] += 1 | |
| ref[r["reference_norm"]] += 1 | |
| n += 1 | |
| top_label, top_n = pred.most_common(1)[0] | |
| # what a model would score by always naming its own modal answer | |
| majority_acc = ref[top_label] / n if n else 0.0 | |
| bias.append({ | |
| "model": m, "task": t, "baseline": b, "n": n, | |
| "n_distinct_predictions": len(pred), | |
| "modal_prediction": top_label, | |
| "modal_share": round(top_n / n, 4), | |
| "prediction_entropy_bits": round(entropy(pred), 4), | |
| "reference_entropy_bits": round(entropy(ref), 4), | |
| "always_modal_accuracy": round(majority_acc, 4), | |
| "accuracy": round(acc[(m, t, b)]["accuracy"], 4), | |
| "prediction_counts": json.dumps(dict(sorted(pred.items()))), | |
| }) | |
| write_csv(os.path.join(args.out_dir, "prediction_bias.csv"), bias) | |
| ladder = level2_ladder(args.root, models) | |
| if ladder: | |
| write_csv(os.path.join(args.out_dir, "level2_structure_use.csv"), ladder) | |
| report(models, acc, summary, bias, ladder) | |
| print(f"\nwrote {3 + bool(ladder)} CSVs to {args.out_dir}") | |
| return 0 | |
| # the split lives in the repo, one level above this experiment directory | |
| LEVEL2_TEST = os.path.join("..", "02_data", "Youtube_self_depth_QA", | |
| "level2", "test.jsonl") | |
| def _prefix_survivors(options): | |
| """The options a majority vote on successive positions cannot rule out. | |
| A regression guard. With random distractors this should leave most items | |
| undecided and score at chance; if it ever climbs toward 0.5 again, the | |
| option construction has reacquired a structural leak. | |
| """ | |
| alive = list(options) | |
| seqs = {lab: txt.split(" > ") for lab, txt in options.items()} | |
| for k in range(4): | |
| if len(alive) <= 1: | |
| break | |
| counts = collections.Counter(seqs[a][k] for a in alive) | |
| top = counts.most_common(1)[0][1] | |
| winners = [l for l, v in counts.items() if v == top] | |
| if len(winners) == 1: | |
| alive = [a for a in alive if seqs[a][k] == winners[0]] | |
| return alive | |
| def level2_ladder(root: str, models): | |
| """How often each model's answer lands inside the structure-only survivor set.""" | |
| path = os.path.abspath(os.path.join(root, LEVEL2_TEST)) | |
| if not os.path.exists(path): | |
| print(f"[warn] level-2 split not found at {path}; skipping ladder analysis") | |
| return [] | |
| test = {} | |
| with open(path) as fh: | |
| for line in fh: | |
| rec = json.loads(line) | |
| if rec.get("options"): | |
| test[rec["sample_id"]] = rec["options"] | |
| if not test: | |
| return [] | |
| out = [] | |
| for m in models: | |
| for b in BASELINES: | |
| inside = hit = n = 0 | |
| for r in load_predictions(root, m, "youtube_level2", b): | |
| opts = test.get(r["sample_id"]) | |
| if not opts: | |
| continue | |
| alive = _prefix_survivors(opts) | |
| n += 1 | |
| inside += int(r["prediction_norm"] in alive) | |
| hit += int(r["correct"]) | |
| out.append({ | |
| "model": m, "baseline": b, "n": n, | |
| "answer_in_survivor_set": round(inside / n, 4) if n else 0.0, | |
| "accuracy": round(hit / n, 4) if n else 0.0, | |
| # of the answers that did land in the survivor set, how many were | |
| # right: this is the coin flip the structure cannot decide | |
| "accuracy_within_survivors": round(hit / inside, 4) if inside else 0.0, | |
| }) | |
| return out | |
| def write_csv(path: str, rows) -> None: | |
| with open(path, "w", newline="") as fh: | |
| w = csv.DictWriter(fh, fieldnames=list(rows[0])) | |
| w.writeheader() | |
| w.writerows(rows) | |
| def report(models, acc, summary, bias, ladder=()) -> None: | |
| short = {m: m.replace("Qwen_Qwen", "").replace("-Instruct", "") for m in models} | |
| idx = {(r["model"], r["task"]): r for r in summary} | |
| print("=" * 96) | |
| print("ACCURACY (mean over the six baselines; floor = what the split gives " | |
| "away for free)") | |
| print("=" * 96) | |
| print(f'{"model":22s}' + "".join(f"{t.replace('youtube_','yt_'):>16s}" for t in TASKS)) | |
| print(f'{"floor":22s}' + "".join(f"{FLOORS[t]:>16.3f}" for t in TASKS)) | |
| print("-" * 96) | |
| for m in models: | |
| print(f"{short[m]:22s}" + "".join( | |
| f'{idx[(m,t)]["acc_mean"]:>16.3f}' for t in TASKS)) | |
| print() | |
| print("HEADROOM CAPTURED (acc - floor) / (1 - floor); negative = below floor") | |
| print("-" * 96) | |
| for m in models: | |
| cells = [] | |
| for t in TASKS: | |
| v = idx[(m, t)]["headroom_captured_mean"] | |
| cells.append(f"{v:>+15.3f} " if v < 0 else f"{v:>16.3f}") | |
| print(f"{short[m]:22s}" + "".join(cells)) | |
| print() | |
| print("BASELINE SENSITIVITY range over 0.00-0.25 / slope per unit baseline") | |
| print("-" * 96) | |
| for m in models: | |
| print(f"{short[m]:22s}" + "".join( | |
| f'{idx[(m,t)]["acc_range"]:>7.3f}/{idx[(m,t)]["slope_per_baseline"]:>+8.3f}' | |
| for t in TASKS)) | |
| print() | |
| print("ANSWER COLLAPSE modal predicted label, its share, and what always " | |
| "answering it would score (baseline 0.00)") | |
| print("-" * 96) | |
| bidx = {(r["model"], r["task"], r["baseline"]): r for r in bias} | |
| for m in models: | |
| cells = [] | |
| for t in TASKS: | |
| r = bidx[(m, t, "0.00")] | |
| cells.append(f'{r["modal_prediction"]:>7s}{r["modal_share"]:>6.2f}' | |
| f'{r["always_modal_accuracy"]:>7.3f}') | |
| print(f"{short[m]:22s}" + "".join(cells)) | |
| print(" (reference labels are near-uniform, so 'always modal' sits at chance)") | |
| if ladder: | |
| print() | |
| print("LEVEL 2 REGRESSION GUARD -- can a text-only majority vote still help?") | |
| print("-" * 96) | |
| lidx = {(r["model"], r["baseline"]): r for r in ladder} | |
| for m in models: | |
| r = lidx[(m, "0.00")] | |
| print(f'{short[m]:22s} answer inside the vote survivors ' | |
| f'{r["answer_in_survivor_set"]:.3f} accuracy {r["accuracy"]:.3f}') | |
| print(" distractors are random, so the vote rarely decides anything; a") | |
| print(" survivor rate near 1.0 means it eliminated nothing.") | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |