Skip to content

Commit 94de6ef

Browse files
yashb98claude
andcommitted
Publish the 7 evidence files the paper's manifest cites
The public evidence_manifest.json referenced research/rlvr/plan.md, the ledger, two run notes, two decontam reports, and eval_math_acc.py — but they were never committed (research/ is gitignored; the paper package was force-added, these were missed), so 7 of the site's 61 provenance links 404'd. Found by the site's capstone link audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fe18745 commit 94de6ef

7 files changed

Lines changed: 2182 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"prepared_for": "research/rlvr/plan.md P1 (GRPO prompt set) \u2014 unlocked by Phase-1 GO 2026-07-02",
3+
"extractor": "math-acc-v1",
4+
"ngram_n": 13,
5+
"flag_threshold": 0.5,
6+
"eval_decontam": "vs math-eval-v1 (gsm8k_test+math500) \u2014 overlaps DROPPED from *_clean",
7+
"sft_flag_status": "done",
8+
"gsm8k_train": {
9+
"n": 7473,
10+
"eval_dropped": 2,
11+
"clean": 7471,
12+
"sft_flagged": 0
13+
},
14+
"math_l13_train": {
15+
"n": 3502,
16+
"eval_dropped": 12,
17+
"clean": 3490,
18+
"sft_flagged": 7,
19+
"load_error": null
20+
},
21+
"smoke_limit": null
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"prepared_for": "research/rlvr/plan.md Phase-1 pass@k go/no-go",
3+
"consumer": "research.eval_math_acc.run_math_acc (extractor math-acc-v1)",
4+
"ngram_n": 13,
5+
"flag_threshold": 0.5,
6+
"decontam_target": "35924 OpenR1 SFT problems (of 35231 used uuids)",
7+
"sft_decontam_status": "done",
8+
"rerun_vs_sft_online": "HF_HUB_OFFLINE=0 python3 research/datasets/math-eval-v1/prepare_math_eval.py (fetches OpenR1 problems online for the vs-SFT check)",
9+
"index_13grams": 1138279,
10+
"gsm8k_test": {
11+
"n": 1319,
12+
"contaminated": 0,
13+
"clean": 1319
14+
},
15+
"math500": {
16+
"n": 500,
17+
"contaminated": 0,
18+
"clean": 500
19+
},
20+
"max_overlap_gsm8k": 0.0,
21+
"max_overlap_math500": 0.0588,
22+
"smoke_limit": null
23+
}

‎research/eval_math_acc.py‎

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""Math-accuracy scorer — the `math-acc-v1` capability axis (the decision metric the
2+
rlvr stage needs, per research/rlvr/plan.md §C27). The MISSING piece: eval-harness has
3+
PPL + multiple-choice but NO exact-match / pass@k, and the SFT cohort proved PPL cannot
4+
separate reasoning arms — so held-out math EXACT-MATCH pass@1 + pass@k is the only valid
5+
win metric for SFT/distillation/GRPO reasoning runs.
6+
7+
DESIGN — model-agnostic & CPU-testable, mirroring research/eval_downstream.py. The scorer
8+
takes a `generate_fn` callable (`(prompt:str, n:int) -> list[str]` of n sampled
9+
completions), NOT a model, so all of extraction / equivalence / pass@k is unit-tested here
10+
with a deterministic stub and the SAME code runs on the real checkpoint under eval-harness.
11+
This module NEVER trains, NEVER loads a model, NEVER allocates CUDA — the CALLER
12+
(eval-harness template) loads the checkpoint behind `safe_cuda.guard(...)` and passes
13+
`generate_fn`.
14+
15+
The three §C25 `rlvr` battery items this module supplies:
16+
- `pass1_wilson_ci` — accuracy_wilson_ci over all (item×sample) trials.
17+
- `passk_chen2021` — the unbiased HumanEval/Chen-2021 pass@k estimator, mean over items.
18+
- `extractor_pinned` — EXTRACTOR_VERSION is stamped into every result; the extractor +
19+
verifier are frozen here and version-bumped on any change, so a
20+
number is comparable only within one extractor version.
21+
Plus `verifier_honesty_ipt` support: `verifier_false_positive_rate()` runs the verifier on
22+
KNOWN-wrong (question, answer) pairs — it MUST stay near 0, else the reward is gameable.
23+
24+
HONEST SCALE CAVEAT (carried into every result): at 596M params / ~1.19B training tokens
25+
the SFT'd model very likely sits near pass@k≈0 on any non-trivial math band. A pass@k of 0
26+
is a REAL, decisive result (RL is null-by-construction — nothing to sharpen), not a bug.
27+
Report the number; do not over-read a near-zero accuracy as a failure of the harness.
28+
"""
29+
from __future__ import annotations
30+
import re
31+
from typing import Callable, Sequence
32+
33+
from research.eval_metrics import accuracy_wilson_ci
34+
35+
# Pinned extractor+verifier identity. BUMP on ANY change to _extract/_normalize/is_equiv —
36+
# numbers are only comparable within one version (the `extractor_pinned` §C25 item).
37+
EXTRACTOR_VERSION = "math-acc-v1"
38+
39+
GenerateFn = Callable[[str, int], Sequence[str]]
40+
41+
# --------------------------------------------------------------- pass@k (Chen 2021)
42+
43+
def pass_at_k(n: int, c: int, k: int) -> float:
44+
"""Unbiased pass@k estimator (Chen et al. 2021, the HumanEval formula):
45+
the probability that at least one of k samples drawn WITHOUT replacement from n
46+
total samples (of which c are correct) is correct = 1 - C(n-c,k)/C(n,k), computed
47+
in the numerically-stable product form. k=1 ⇒ c/n (plain pass@1)."""
48+
if k <= 0:
49+
raise ValueError("k must be >= 1")
50+
if n <= 0 or k > n:
51+
raise ValueError(f"need 1 <= k <= n; got n={n} k={k}")
52+
if c <= 0:
53+
return 0.0
54+
if n - c < k: # not enough wrong samples to fill k slots ⇒ guaranteed a hit
55+
return 1.0
56+
prod = 1.0
57+
for i in range(n - c + 1, n + 1):
58+
prod *= 1.0 - k / i
59+
return 1.0 - prod
60+
61+
# --------------------------------------------------------------- answer extraction
62+
63+
def _last_boxed(text: str) -> str | None:
64+
"""Return the content of the LAST \\boxed{...} (balanced braces), or None."""
65+
idx = text.rfind(r"\boxed")
66+
if idx < 0:
67+
return None
68+
i = idx + len(r"\boxed")
69+
while i < len(text) and text[i] in " \t":
70+
i += 1
71+
if i >= len(text) or text[i] != "{":
72+
return None
73+
depth, start = 0, i
74+
for j in range(i, len(text)):
75+
if text[j] == "{":
76+
depth += 1
77+
elif text[j] == "}":
78+
depth -= 1
79+
if depth == 0:
80+
return text[start + 1:j]
81+
return None
82+
83+
_ANS_IS = re.compile(r"(?:final answer|answer)\s*(?:is|:|=)\s*\$?([^\n.$]+)", re.IGNORECASE)
84+
_NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?")
85+
86+
def extract_answer(text: str) -> str | None:
87+
"""Pinned+versioned answer extractor. Priority: \\boxed{} → GSM8K '#### X' →
88+
'the answer is X' → last number in the text. Returns the raw span (normalized at
89+
compare time), or None if nothing parseable. Deterministic, no model, no network."""
90+
if not text:
91+
return None
92+
boxed = _last_boxed(text)
93+
if boxed is not None:
94+
return boxed.strip()
95+
if "####" in text: # GSM8K gold/solution convention
96+
tail = text.rsplit("####", 1)[1]
97+
m = _NUM.search(tail)
98+
if m:
99+
return m.group(0).strip()
100+
return tail.strip().splitlines()[0].strip() if tail.strip() else None
101+
m = _ANS_IS.search(text)
102+
if m:
103+
return m.group(1).strip()
104+
nums = _NUM.findall(text) # last-number fallback
105+
return nums[-1].strip() if nums else None
106+
107+
# --------------------------------------------------------------- normalization + verifier
108+
109+
_TEXT_CMD = re.compile(r"\\(?:text|mathrm|mbox|mathbf|mathit)\s*\{([^}]*)\}")
110+
_FRAC = re.compile(r"\\d?frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}")
111+
112+
def _normalize(s: str) -> str:
113+
"""Canonicalize a math answer string for a fast string-equality path (the classic
114+
MATH/Minerva cleanup): strip $, \\left/\\right, \\text{}, units, spaces, thousands
115+
commas, trailing punctuation; \\frac{a}{b} → (a)/(b); ^ stays for the sympy path."""
116+
if s is None:
117+
return ""
118+
s = s.strip()
119+
s = _TEXT_CMD.sub(r"\1", s)
120+
s = s.replace(r"\boxed", "").replace(r"\left", "").replace(r"\right", "")
121+
s = _FRAC.sub(r"(\1)/(\2)", s) # \frac{a}{b} → (a)/(b) BEFORE braces are stripped
122+
for a, b in ((r"\!", ""), (r"\,", ""), (r"\ ", " "), ("$", ""), (r"\%", ""), ("%", ""),
123+
(r"^\circ", ""), (r"\circ", ""), (r"\cdot", "*"), (r"\times", "*"),
124+
("{", ""), ("}", "")):
125+
s = s.replace(a, b)
126+
s = s.replace("\\", "").replace(" ", "")
127+
s = s.rstrip(".")
128+
if re.fullmatch(r"-?\d{1,3}(?:,\d{3})+(?:\.\d+)?", s): # 1,234 → 1234 (only true separators)
129+
s = s.replace(",", "")
130+
return s.lower()
131+
132+
def _as_float(s: str):
133+
try:
134+
return float(s)
135+
except (ValueError, TypeError):
136+
return None
137+
138+
def _sympy_equal(a: str, b: str) -> bool:
139+
"""Symbolic equivalence via sympy (1/2 == 0.5, (1)/(2) == 0.5, 2^3 == 8). Fully
140+
guarded: any parse/timeout/exception → False (a verifier must never crash training).
141+
Length-capped to avoid pathological sympify blow-ups."""
142+
if len(a) > 100 or len(b) > 100:
143+
return False
144+
try:
145+
from sympy import simplify
146+
from sympy.parsing.sympy_parser import parse_expr
147+
expr = f"({a.replace('^', '**')})-({b.replace('^', '**')})"
148+
return simplify(parse_expr(expr, evaluate=True)) == 0
149+
except Exception:
150+
return False
151+
152+
def is_equiv(pred: str | None, gold: str | None) -> bool:
153+
"""The VERIFIER (verifiable-reward, no neural RM). True iff the extracted prediction
154+
is mathematically equivalent to the gold answer. Order: normalized string equality →
155+
numeric equality (tol 1e-6) → sympy symbolic equality. Conservative: unparseable ⇒ False."""
156+
if pred is None or gold is None:
157+
return False
158+
np_, ng = _normalize(pred), _normalize(gold)
159+
if np_ == "" or ng == "":
160+
return False
161+
if np_ == ng:
162+
return True
163+
fp, fg = _as_float(np_), _as_float(ng)
164+
if fp is not None and fg is not None:
165+
return abs(fp - fg) <= 1e-6 * max(1.0, abs(fg))
166+
return _sympy_equal(np_, ng)
167+
168+
def verifier_false_positive_rate(wrong_pairs: Sequence[tuple[str, str]]) -> float:
169+
"""`verifier_honesty_ipt` support: fraction of KNOWN-WRONG (pred, gold) pairs the
170+
verifier wrongly accepts. MUST be ~0 — a nonzero rate means the reward is gameable."""
171+
if not wrong_pairs:
172+
return 0.0
173+
fp = sum(1 for pred, gold in wrong_pairs if is_equiv(pred, gold))
174+
return fp / len(wrong_pairs)
175+
176+
# --------------------------------------------------------------- scoring harness
177+
178+
def score_item(generate_fn: GenerateFn, prompt: str, gold: str, n_samples: int) -> dict:
179+
"""Generate n_samples completions for one prompt, extract+verify each. Returns
180+
{n, c, correct: [0/1...]} — c = number of verifier-correct samples."""
181+
comps = list(generate_fn(prompt, n_samples))
182+
correct = [1 if is_equiv(extract_answer(c), gold) else 0 for c in comps]
183+
return {"n": len(correct), "c": sum(correct), "correct": correct}
184+
185+
def run_math_acc(generate_fn: GenerateFn, items: Sequence[dict],
186+
n_samples: int = 16, k_list: Sequence[int] = (1, 8, 16)) -> dict:
187+
"""Score a held-out math set. `items` = [{"prompt", "gold"}...]. For each item sample
188+
n_samples completions; aggregate pass@1 (Wilson CI over all item×sample trials) and
189+
pass@k (Chen-2021, mean over items). Returns a flat metrics dict stamped with
190+
EXTRACTOR_VERSION — the §C25 rlvr items pass1_wilson_ci / passk_chen2021 / extractor_pinned."""
191+
if not items:
192+
raise ValueError("no items to score")
193+
k_list = [k for k in k_list if k <= n_samples]
194+
per_item, flat = [], []
195+
for it in items:
196+
r = score_item(generate_fn, it["prompt"], it["gold"], n_samples)
197+
per_item.append(r)
198+
flat.extend(r["correct"])
199+
acc, lo, hi = accuracy_wilson_ci(flat)
200+
passk = {k: sum(pass_at_k(r["n"], r["c"], k) for r in per_item) / len(per_item)
201+
for k in k_list}
202+
return {
203+
"extractor_version": EXTRACTOR_VERSION, # → extractor_pinned
204+
"n_items": len(items),
205+
"n_samples": n_samples,
206+
"pass1_wilson_ci": {"acc": acc, "ci_low": lo, "ci_high": hi},
207+
"passk_chen2021": passk,
208+
"solved_items": sum(1 for r in per_item if r["c"] > 0),
209+
}
210+
211+
def _self_test() -> None:
212+
"""Deterministic CPU smoke — no model, no network. Run: python3 -m research.eval_math_acc"""
213+
assert extract_answer(r"so the answer is \boxed{72}.") == "72"
214+
assert extract_answer("...\n#### 18") == "18"
215+
assert is_equiv(r"\boxed{1/2}", "0.5") and is_equiv("72", "72") and not is_equiv("72", "73")
216+
assert abs(pass_at_k(4, 1, 1) - 0.25) < 1e-9 and pass_at_k(4, 0, 2) == 0.0 and pass_at_k(4, 4, 2) == 1.0
217+
# a stub policy that always emits the gold answer ⇒ pass@1 == 1
218+
items = [{"prompt": "2+2?", "gold": "4"}, {"prompt": "3+3?", "gold": "6"}]
219+
gen = lambda p, n: [f"the answer is \\boxed{{{ '4' if '2+2' in p else '6' }}}"] * n
220+
out = run_math_acc(gen, items, n_samples=4, k_list=(1, 4))
221+
assert out["pass1_wilson_ci"]["acc"] == 1.0 and out["passk_chen2021"][4] == 1.0
222+
print("SELF-TEST OK", out["extractor_version"], out["passk_chen2021"])
223+
224+
if __name__ == "__main__":
225+
_self_test()

0 commit comments

Comments
 (0)