nano-proofread / eval_nano_proofread.py
vukrosic's picture
Upload eval_nano_proofread.py with huggingface_hub
0faa578 verified
Raw
History Blame Contribute Delete
3.71 kB
"""Standalone nano-proofread benchmark: the published weights vs the best context-free
script, overall and on the context-dependent slice the script cannot do.
The naive script (`naive_fix` in data_proofread.py) is the strongest *context-free*
fixer a developer would write: remove adjacent doubled words, and normalise every
homophone to the most common member of its family. It fixes doubled words and
`could of`->`could have` perfectly, but it cannot know whether `their`/`there`/
`they're` is right — that needs the surrounding words. The hard slice = examples where
the correct homophone is NOT the script's default, i.e. exactly where context decides.
python eval_nano_proofread.py
"""
from __future__ import annotations
import torch
from data_proofread import OOD_CASES, naive_fix, proofread_pairs
from modeling_nano_proofread import load
_HOLDOUT_SEED = 987_654_321
@torch.no_grad()
def _greedy_batch(model, prompts, max_new=48, device="cpu"):
"""Greedy-decode `max_new` tokens per prompt, then cut at the newline EOS.
Prompts are bucketed by EXACT byte length so each batch is a dense, padding-free
(B, L) tensor — bucketing (not padding) keeps the result bit-identical to decoding
one prompt at a time."""
enc = [list(p.encode("utf-8")) for p in prompts]
order = sorted(range(len(prompts)), key=lambda i: len(enc[i]))
max_seq = model.cfg["max_seq_len"]
out = [None] * len(prompts)
i = 0
while i < len(order):
L = len(enc[order[i]])
j = i
while j < len(order) and len(enc[order[j]]) == L:
j += 1
rows = order[i:j]
toks = torch.tensor([enc[k] for k in rows], dtype=torch.long, device=device)
for _ in range(max_new):
nxt = model(toks[:, -max_seq:])[:, -1, :].argmax(-1, keepdim=True)
toks = torch.cat([toks, nxt], dim=1)
tail = toks[:, L:L + max_new]
for r, k in enumerate(rows):
s = bytes(int(b) & 0xFF for b in tail[r].tolist()).decode("utf-8", "replace")
out[k] = s.split("\n", 1)[0]
i = j
return out
def main():
m = load()
n_params = sum(p.numel() for p in m.parameters())
pairs = proofread_pairs(_HOLDOUT_SEED, 4000)
preds = _greedy_batch(m, [p for p, _ in pairs])
mo = nv = 0
hn = hm = hnv = 0
for (prompt, target), pred in zip(pairs, preds):
inp = prompt[:-4]
mo += pred == target
nv += naive_fix(inp) == target
if naive_fix(target) != target: # correct answer isn't the script default
hn += 1
hm += pred == target
hnv += naive_fix(inp) == target
N = len(pairs)
print(f"params : {n_params:,}")
print(f"held-out: N={N} (seed {_HOLDOUT_SEED})\n")
print(f" {'overall':<28}model {mo/N:>6.1%} naive-script {nv/N:>6.1%}")
print(f" context slice (N={hn}): model {hm/hn:>6.1%} naive-script {hnv/hn:>6.1%}")
print(" (context slice = correct homophone is NOT the script's default —")
print(" exactly where the surrounding words decide the answer)")
# ---- the honest test: hand-written natural phrases NOT drawn from the frames ----
ood_preds = _greedy_batch(m, [f"{i} => " for i, _ in OOD_CASES])
om = onv = 0
print(f"\nOUT-OF-DISTRIBUTION (hand-written, N={len(OOD_CASES)}):")
for (inp, gold), pred in zip(OOD_CASES, ood_preds):
om += pred == gold
onv += naive_fix(inp) == gold
flag = "ok " if pred == gold else "XX "
print(f" {flag}{inp:<28}-> {pred:<28} (gold: {gold})")
O = len(OOD_CASES)
print(f"\n OOD exact-match: model {om/O:>6.1%} naive-script {onv/O:>6.1%}")
if __name__ == "__main__":
main()