File size: 4,733 Bytes
27038ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """Benchmark: generalization to unseen slots — char-level vs structural.
Measures the ONE thing the structural encoder is for: does the expert answer
*something well-formed* for a question whose slot was NEVER in training?
Train on capitals of {france, germany, italy, japan, egypt}.
Hold out: {spain, portugal, greece, brazil, norway, india, mexico, kenya}.
Metric: % of holdout questions returning a non-empty answer (the format-
generalization guarantee), char-level vs structural.
Honest framing: this does NOT measure factual correctness (without semantic
embeddings, the system can't know spain's capital is madrid). It measures
whether the system degrades gracefully (well-formed guess) vs silently
(emptiness) on the unknown — a real, meaningful axis of generalization.
Usage:
python bench/run_generalization.py --D 5000 --out results_gen.json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from ensemble import Expert, Brain
TRAIN = [
("what is the capital of france", "the capital of france is paris"),
("what is the capital of germany", "the capital of germany is berlin"),
("what is the capital of italy", "the capital of italy is rome"),
("what is the capital of japan", "the capital of japan is tokyo"),
("what is the capital of egypt", "the capital of egypt is cairo"),
]
# Ground-truth answers (for the factual-correctness metric, informational)
HOLDOUT = [
("what is the capital of spain", "madrid"),
("what is the capital of portugal", "lisbon"),
("what is the capital of greece", "athens"),
("what is the capital of brazil", "brasilia"),
("what is the capital of norway", "oslo"),
("what is the capital of india", "new delhi"),
("what is the capital of mexico", "mexico city"),
("what is the capital of kenya", "nairobi"),
]
PATTERN = "what is the capital of {country}"
@dataclass
class GenResult:
mode: str # "char" | "structural"
D: int
n_train: int
n_holdout: int
nonempty_rate: float # % holdout with non-empty answer (format guarantee)
avg_len: float # avg answer length (proxy for format richness)
answers: list[tuple[str, str, str]] # (question, truth, got)
def run_mode(mode: str, D: int, repeat: int) -> GenResult:
pairs = TRAIN * repeat
if mode == "structural":
e = Expert.from_qa_pairs(pairs, domain="geo", D=D, patterns=[PATTERN])
else:
e = Expert.from_qa_pairs(pairs, domain="geo", D=D)
brain = Brain()
brain.add_expert(e)
nonempty = 0
total_len = 0
answers = []
for q, truth in HOLDOUT:
got = brain.query(q, max_new_tokens=25).answer.strip()
if got:
nonempty += 1
total_len += len(got)
answers.append((q, truth, got))
n = len(HOLDOUT)
return GenResult(
mode=mode, D=D, n_train=len(pairs), n_holdout=n,
nonempty_rate=nonempty / n,
avg_len=total_len / max(nonempty, 1),
answers=answers,
)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--D", type=int, default=5000)
ap.add_argument("--repeat", type=int, default=4, help="training repeats")
ap.add_argument("--out", default="results_gen.json")
args = ap.parse_args()
results = []
for mode in ("char", "structural"):
print(f"\n=== {mode.upper()} (D={args.D}) ===", flush=True)
r = run_mode(mode, args.D, args.repeat)
results.append(r)
print(f" non-empty answers: {r.nonempty_rate:.1%} "
f"({int(r.nonempty_rate*r.n_holdout)}/{r.n_holdout})")
print(f" avg answer length: {r.avg_len:.1f} chars")
for q, truth, got in r.answers:
tag = "OK " if got else "EMPTY"
print(f" [{tag}] {q.split()[-1]:10} truth={truth:12} got={got!r}")
with open(args.out, "w", encoding="utf-8") as f:
json.dump([asdict(r) for r in results], f, indent=2, ensure_ascii=False)
# summary
char = next(r for r in results if r.mode == "char")
struct = next(r for r in results if r.mode == "structural")
print(f"\n{'='*60}")
print(f"GENERALIZATION (format guarantee on unseen slots)")
print(f" char-level non-empty: {char.nonempty_rate:.1%}")
print(f" structural non-empty: {struct.nonempty_rate:.1%}")
delta = struct.nonempty_rate - char.nonempty_rate
print(f" structural lift: +{delta*100:.0f} points")
print(f"{'='*60}")
print(f"\nwrote {args.out}", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())
|