yxc20098 commited on
Commit
03e4efa
·
1 Parent(s): 7a25eb3

Generalization-gap metric: held-out split in run_eval + leaderboard

Browse files

The stated goal is 'generalize on reasoning'; the literature
(Procgen, SMACv2, lmgame-Bench) is emphatic that the public-vs-held-
out generalization gap — not aggregate score — is THE anti-
memorization metric. evaluate() now takes held_out_seeds: those run
as split='held_out'; report adds overall_held_out +
generalization_gap (public − held-out composite). run_eval gains
--held-out-seeds; leaderboard ingest records held_out_composite +
generalization_gap; app.py surfaces both columns. Backward compatible
(no held-out → unchanged shape). tests: gap math, split tagging,
leaderboard capture, back-compat. Full bench suite 269 passed.

app.py CHANGED
@@ -173,6 +173,7 @@ def load_capability_leaderboard() -> pd.DataFrame:
173
  cols = [
174
  "rank", "model", "episodes", "win_rate", "composite",
175
  "perception", "reasoning", "action", "weakest_link",
 
176
  ]
177
  if not rows:
178
  return pd.DataFrame(columns=cols)
 
173
  cols = [
174
  "rank", "model", "episodes", "win_rate", "composite",
175
  "perception", "reasoning", "action", "weakest_link",
176
+ "held_out_composite", "generalization_gap",
177
  ]
178
  if not rows:
179
  return pd.DataFrame(columns=cols)
openra_bench/leaderboard.py CHANGED
@@ -63,6 +63,12 @@ def ingest_run(
63
  "reasoning": overall.get("reasoning_mean", 0.0),
64
  "action": overall.get("action_mean", 0.0),
65
  "weakest_link_hist": overall.get("weakest_link_hist", {}),
 
 
 
 
 
 
66
  "by_capability": _capability_breakdown(eps),
67
  "cells": sorted(stats.get("summary", {}).keys()),
68
  }
 
63
  "reasoning": overall.get("reasoning_mean", 0.0),
64
  "action": overall.get("action_mean", 0.0),
65
  "weakest_link_hist": overall.get("weakest_link_hist", {}),
66
+ # Anti-memorization: held-out composite + generalization gap
67
+ # (public − held-out). None when the run had no held-out split.
68
+ "held_out_composite": stats.get("overall_held_out", {}).get(
69
+ "composite_mean"
70
+ ),
71
+ "generalization_gap": stats.get("generalization_gap"),
72
  "by_capability": _capability_breakdown(eps),
73
  "cells": sorted(stats.get("summary", {}).keys()),
74
  }
openra_bench/run_eval.py CHANGED
@@ -70,11 +70,21 @@ def evaluate(
70
  seeds: list[int],
71
  provider_cfg=None,
72
  agent_factory: AgentFactory | None = None,
 
73
  ) -> dict:
 
 
 
 
 
 
74
  factory = agent_factory or _default_agent_factory(provider_cfg)
75
  by_cell: dict[str, list] = {}
76
  episodes: list[dict] = []
77
  skipped: list[str] = []
 
 
 
78
 
79
  for pack_path in packs:
80
  pack = load_pack(pack_path)
@@ -84,32 +94,47 @@ def evaluate(
84
  skipped.append(f"{pack.meta.id}:{level} (map not Rust-loadable)")
85
  continue
86
  cell = f"{pack.meta.id}:{level}"
87
- for seed in seeds:
88
- res = run_level(compiled, factory(compiled), seed=seed)
89
- sc = score_episode(compiled, res)
90
- by_cell.setdefault(cell, []).append(sc)
91
- episodes.append(
92
- {
93
- "cell": cell,
94
- "capability": compiled.meta.capability,
95
- "seed": seed,
96
- "outcome": sc.outcome,
97
- "composite": sc.composite,
98
- "perception": sc.perception,
99
- "reasoning": sc.reasoning,
100
- "action": sc.action,
101
- "weakest_link": sc.weakest_link,
102
- "turns": res.turns,
103
- "notes": sc.notes,
104
- }
105
- )
106
-
107
- return {
 
 
 
 
 
 
108
  "summary": {cell: _agg(scs) for cell, scs in by_cell.items()},
109
- "overall": _agg([s for scs in by_cell.values() for s in scs]),
110
  "episodes": episodes,
111
  "skipped": skipped,
112
  }
 
 
 
 
 
 
 
 
 
113
 
114
 
115
  def write_report(stats: dict, path: str | Path) -> None:
@@ -132,6 +157,12 @@ def main(argv: list[str]) -> int:
132
  ap.add_argument("--packs", help="pack file or dir (default: bundled packs/)")
133
  ap.add_argument("--levels", default="easy,medium,hard")
134
  ap.add_argument("--seeds", default="1,2,3")
 
 
 
 
 
 
135
  ap.add_argument("--provider", help="openrouter|vllm|openai (omit = scripted baseline)")
136
  ap.add_argument("--model", default="anthropic/claude-3.5-sonnet")
137
  ap.add_argument("--base-url")
@@ -162,6 +193,7 @@ def main(argv: list[str]) -> int:
162
  a.levels.split(","),
163
  [int(s) for s in a.seeds.split(",")],
164
  provider_cfg=cfg,
 
165
  )
166
  write_report(stats, a.out)
167
  o = stats["overall"]
 
70
  seeds: list[int],
71
  provider_cfg=None,
72
  agent_factory: AgentFactory | None = None,
73
+ held_out_seeds: list[int] | None = None,
74
  ) -> dict:
75
+ """Run packs×levels×seeds. If `held_out_seeds` is given, those are
76
+ run too and tagged split='held_out'; the report adds
77
+ `overall_held_out` and `generalization_gap` (public composite −
78
+ held-out composite) — the anti-memorization metric the
79
+ generalization literature (Procgen/SMACv2/lmgame-Bench) requires.
80
+ """
81
  factory = agent_factory or _default_agent_factory(provider_cfg)
82
  by_cell: dict[str, list] = {}
83
  episodes: list[dict] = []
84
  skipped: list[str] = []
85
+ public_scores: list = []
86
+ held_scores: list = []
87
+ held_out_seeds = held_out_seeds or []
88
 
89
  for pack_path in packs:
90
  pack = load_pack(pack_path)
 
94
  skipped.append(f"{pack.meta.id}:{level} (map not Rust-loadable)")
95
  continue
96
  cell = f"{pack.meta.id}:{level}"
97
+ for split, slist in (("public", seeds), ("held_out", held_out_seeds)):
98
+ for seed in slist:
99
+ res = run_level(compiled, factory(compiled), seed=seed)
100
+ sc = score_episode(compiled, res)
101
+ if split == "public":
102
+ by_cell.setdefault(cell, []).append(sc)
103
+ public_scores.append(sc)
104
+ else:
105
+ held_scores.append(sc)
106
+ episodes.append(
107
+ {
108
+ "cell": cell,
109
+ "capability": compiled.meta.capability,
110
+ "split": split,
111
+ "seed": seed,
112
+ "outcome": sc.outcome,
113
+ "composite": sc.composite,
114
+ "perception": sc.perception,
115
+ "reasoning": sc.reasoning,
116
+ "action": sc.action,
117
+ "weakest_link": sc.weakest_link,
118
+ "turns": res.turns,
119
+ "notes": sc.notes,
120
+ }
121
+ )
122
+
123
+ out = {
124
  "summary": {cell: _agg(scs) for cell, scs in by_cell.items()},
125
+ "overall": _agg(public_scores),
126
  "episodes": episodes,
127
  "skipped": skipped,
128
  }
129
+ if held_scores:
130
+ ho = _agg(held_scores)
131
+ out["overall_held_out"] = ho
132
+ out["generalization_gap"] = round(
133
+ out["overall"].get("composite_mean", 0.0)
134
+ - ho.get("composite_mean", 0.0),
135
+ 4,
136
+ )
137
+ return out
138
 
139
 
140
  def write_report(stats: dict, path: str | Path) -> None:
 
157
  ap.add_argument("--packs", help="pack file or dir (default: bundled packs/)")
158
  ap.add_argument("--levels", default="easy,medium,hard")
159
  ap.add_argument("--seeds", default="1,2,3")
160
+ ap.add_argument(
161
+ "--held-out-seeds",
162
+ default="",
163
+ help="comma seeds run as a held-out split; reports the "
164
+ "generalization gap (anti-memorization metric)",
165
+ )
166
  ap.add_argument("--provider", help="openrouter|vllm|openai (omit = scripted baseline)")
167
  ap.add_argument("--model", default="anthropic/claude-3.5-sonnet")
168
  ap.add_argument("--base-url")
 
193
  a.levels.split(","),
194
  [int(s) for s in a.seeds.split(",")],
195
  provider_cfg=cfg,
196
+ held_out_seeds=[int(s) for s in a.held_out_seeds.split(",") if s.strip()],
197
  )
198
  write_report(stats, a.out)
199
  o = stats["overall"]
tests/test_run_eval.py CHANGED
@@ -44,6 +44,47 @@ def test_evaluate_aggregates_and_reports(tmp_path):
44
  assert loaded["overall"]["n"] == 2
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def test_unsupported_map_is_skipped_not_crashed(tmp_path):
48
  """A pack on a non-Rust map must be reported as skipped, not raise."""
49
  pack = (PACKS / "perception-frontier-reading.yaml").read_text()
 
44
  assert loaded["overall"]["n"] == 2
45
 
46
 
47
+ def test_held_out_split_reports_generalization_gap(tmp_path):
48
+ stats = evaluate(
49
+ packs=[PACKS / "perception-frontier-reading.yaml"],
50
+ levels=["easy"],
51
+ seeds=[1, 2],
52
+ held_out_seeds=[7, 8],
53
+ )
54
+ assert "overall_held_out" in stats
55
+ assert stats["overall_held_out"]["n"] == 2
56
+ assert isinstance(stats["generalization_gap"], float)
57
+ splits = {e["split"] for e in stats["episodes"]}
58
+ assert splits == {"public", "held_out"}
59
+ assert sum(e["split"] == "held_out" for e in stats["episodes"]) == 2
60
+ # gap == public composite − held-out composite (sign can be ±).
61
+ g = round(
62
+ stats["overall"]["composite_mean"]
63
+ - stats["overall_held_out"]["composite_mean"],
64
+ 4,
65
+ )
66
+ assert stats["generalization_gap"] == g
67
+
68
+ # Leaderboard captures the gap.
69
+ from openra_bench.leaderboard import build_table, ingest_run
70
+
71
+ s = tmp_path / "lb.jsonl"
72
+ ingest_run(stats, "m", s)
73
+ row = build_table(s, min_episodes=1)[0]
74
+ assert row["generalization_gap"] == stats["generalization_gap"]
75
+ assert row["held_out_composite"] == stats["overall_held_out"]["composite_mean"]
76
+
77
+
78
+ def test_no_held_out_keeps_backward_compatible_shape():
79
+ stats = evaluate(
80
+ packs=[PACKS / "perception-frontier-reading.yaml"],
81
+ levels=["easy"],
82
+ seeds=[1],
83
+ )
84
+ assert "overall_held_out" not in stats and "generalization_gap" not in stats
85
+ assert all(e["split"] == "public" for e in stats["episodes"])
86
+
87
+
88
  def test_unsupported_map_is_skipped_not_crashed(tmp_path):
89
  """A pack on a non-Rust map must be reported as skipped, not raise."""
90
  pack = (PACKS / "perception-frontier-reading.yaml").read_text()