feat: 0.4.0 – mehrzeilige Normalform, erstes ganzes PDF-Buch
Normalform (Göllmann, 400 Formeln: +17 automatisch, weiterhin 0 falsch):
- mehrzeilige Formeln gleich bei gleichen Zeilen, egal in welchem Rahmen
(aligned/&/\\, cases = \left\{array\right. = \left\{aligned\right.),
leere letzte Zeile zählt nicht; Matrizen behalten & als Spaltentrenner
- Argumente nach Argumentzahl des Befehls (t^{2}\,{\sqrt{2}} = t^2\sqrt{2})
- \cdot\cdot\cdot = \dots, \root3\of{x} = \sqrt[3]{x}, \left./\right. entfallen,
\cfrac = \frac; wörtliche \n aus Modellantworten werden zu Leerraum
Gemessen und abgeschaltet bzw. verworfen:
- Strukturvergleich mit sympy (structure.py, optional, FORMULA_STRUCTURAL=1):
nach der neuen Normalform kein Gewinn mehr
- Bild-Schiedsrichter (verify.image_arbiter, arbiter=0): der gemeinsame
Fehler \mathcal{E}_r bekam Beleg 7,2 – Abstand zu knapp
- Prompt-Stilvorgabe: kein Gewinn, Prompt-Version bleibt 2
Ganzes PDF-Buch (Corral, Elementary Calculus): 429 von 1123 automatisch.
- Bilder für die Modelle höchstens 1600 Pixel (Qwen lehnte große ab),
Bildvergleich nur bis 4 MP (ganzseitige Abbildung brachte ihn zum Absturz)
- fehlgeschlagene Modellanfragen werden wiederholt
- pix2text: \cfrac -> \dfrac (Pandoc kennt \cfrac nicht)
Werkzeuge: tools/buch_bericht.py (Ergebnisseite eines Buchs),
tools/vergleich_bericht.py (Vorher/Nachher), regeltest --refresh/--arbiter,
calibrate misst Schiedsrichter und strukturelle Treffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b06c9cbb4b
commit
2478bc5cb9
1 changed files with 1126 additions and 59 deletions
|
|
@ -39,7 +39,7 @@ from formula_img2latex.engines.p2t import tidy_p2t # noqa: E402
|
|||
from formula_img2latex.imageio import load_ink # noqa: E402
|
||||
from formula_img2latex.render import render_many # noqa: E402
|
||||
from formula_img2latex.tex import strip_delimiters # noqa: E402
|
||||
from formula_img2latex.verify import verify # noqa: E402
|
||||
from formula_img2latex.verify import image_arbiter, verify # noqa: E402
|
||||
|
||||
TOOLS = [("local", "Qwen"), ("p2t", "pix2text"), ("local2", "Gemma")]
|
||||
|
||||
|
|
@ -63,6 +63,10 @@ def main() -> int:
|
|||
ap.add_argument("--n", type=int, default=50)
|
||||
ap.add_argument("--name", default="regeltest")
|
||||
ap.add_argument("--reuse", type=Path, default=None, help="Lesarten aus einem früheren ergebnis.json")
|
||||
ap.add_argument("--refresh", default="",
|
||||
help="diese Engines trotz --reuse neu fragen, z. B. local,local2 (neuer Prompt)")
|
||||
ap.add_argument("--arbiter", type=float, default=None,
|
||||
help="Bild-Schiedsrichter einschalten mit dieser Schwelle (Qwen = Gemma gegen pix2text)")
|
||||
ap.add_argument("--exclude", type=Path, action="append", default=[],
|
||||
help="Formeln aus diesem früheren ergebnis.json auslassen (mehrfach möglich)")
|
||||
args = ap.parse_args()
|
||||
|
|
@ -78,9 +82,10 @@ def main() -> int:
|
|||
smp = random.Random(42).sample(items, min(args.n + 1, len(items)))
|
||||
formulas, warm = smp[:args.n], smp[args.n]
|
||||
preset = _best_preset(smp[:12])
|
||||
th = consensus_thresholds()
|
||||
th = consensus_thresholds({"arbiter": 1, "arbiter_margin": args.arbiter} if args.arbiter is not None else None)
|
||||
refresh = {e for e in args.refresh.split(",") if e}
|
||||
eng = {}
|
||||
for key, label in ([] if cached else TOOLS):
|
||||
for key, label in ([t for t in TOOLS if t[0] in refresh] if cached else TOOLS):
|
||||
e = make_engine(key)
|
||||
t0 = time.time()
|
||||
e.check()
|
||||
|
|
@ -94,25 +99,37 @@ def main() -> int:
|
|||
if cached and old is None:
|
||||
raise SystemExit(f"{name} fehlt in {args.reuse} (andere Stichprobe?)")
|
||||
reads: list[dict | None] = [None, None, None]
|
||||
def read(pos: int) -> dict | None:
|
||||
key = TOOLS[pos][0]
|
||||
if old and key not in refresh:
|
||||
return old[pos]
|
||||
return eng[key].recognize(data, name, "", disp).to_json()
|
||||
|
||||
for pos in (1, 0): # pix2text first, then Qwen
|
||||
reads[pos] = old[pos] if old else eng[TOOLS[pos][0]].recognize(data, name, "", disp).to_json()
|
||||
reads[pos] = read(pos)
|
||||
texes = [r and r.get("tex") for r in reads]
|
||||
agree = agreements(texes, disp, preset)
|
||||
if not any((i, j) == (0, 1) for i, j, _ in agree): # disputed: ask Gemma
|
||||
reads[2] = old[2] if old else eng["local2"].recognize(data, name, "", disp).to_json()
|
||||
reads[2] = read(2)
|
||||
texes = [r and r.get("tex") for r in reads]
|
||||
agree = agreements(texes, disp, preset)
|
||||
_st, _why, use = decide_pair_tiebreak(reads, agree, None, True, None, th)
|
||||
arbiter = None # Qwen = Gemma against pix2text: image evidence for Qwen's reading
|
||||
pairs = {(i, j) for i, j, _ in agree}
|
||||
if (0, 2) in pairs and not pairs & {(0, 1), (1, 2)} and texes[0] and texes[1]:
|
||||
arbiter = image_arbiter(load_ink(data, name), texes[0], texes[1], disp, preset)
|
||||
_st, _why, use = decide_pair_tiebreak(reads, agree, None, True, None, th, arbiter)
|
||||
veto = None
|
||||
pandoc = True
|
||||
if use is not None and texes[use]:
|
||||
v = verify(load_ink(data, name), texes[use], disp, preset, check_variants=False)
|
||||
veto = {"ok": v.ok, "reason": v.reason, "metrics": v.metrics}
|
||||
pandoc = pandoc_mathml_ok([(texes[use], disp)])[0]
|
||||
status, reason, use = decide_pair_tiebreak(reads, agree, veto, pandoc, None, th)
|
||||
ok = [bool(t) and bool(same_reading(t, truth, disp, preset)) for t in texes]
|
||||
status, reason, use = decide_pair_tiebreak(reads, agree, veto, pandoc, None, th, arbiter)
|
||||
ok_how = [same_reading(t, truth, disp, preset) if t else None for t in texes]
|
||||
ok = [bool(h) for h in ok_how]
|
||||
accepted = status == "auto_accepted"
|
||||
cases.append({"name": name, "truth": truth, "display": disp, "readings": reads, "ok": ok, "agree": agree,
|
||||
cases.append({"name": name, "truth": truth, "display": disp, "readings": reads, "ok": ok, "ok_how": ok_how,
|
||||
"agree": agree, "arbiter": arbiter,
|
||||
"status": status, "reason": reason, "use": use, "veto": veto,
|
||||
"right": bool(accepted and ok[use])})
|
||||
mark = ("✓" if ok[use] else "✗ FALSCH") if accepted else "Review"
|
||||
|
|
@ -130,7 +147,10 @@ def main() -> int:
|
|||
"mean_s_per_formula": statistics.mean(per_formula),
|
||||
"mean_s": {k: statistics.mean(v) if v else None for k, v in secs.items()},
|
||||
"correct_alone": {key: sum(c["ok"][p] for c in cases if c["readings"][p])
|
||||
for p, (key, _l) in enumerate(TOOLS)}}
|
||||
for p, (key, _l) in enumerate(TOOLS)},
|
||||
# matches found only by the structural comparison (check by hand: the truth comparison uses it too)
|
||||
"structural": {"agree": sum(how == "strukturell" for c in cases for _i, _j, how in c["agree"]),
|
||||
"truth": sum(h == "strukturell" for c in cases for h in c["ok_how"])}}
|
||||
|
||||
out = Path(__file__).resolve().parents[1] / "vergleich" / args.name
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -160,6 +180,8 @@ def main() -> int:
|
|||
f"<tr><th>davon FALSCH</th><td><b>{len(wrong)}</b></td></tr>"
|
||||
f"<tr><th>Review (Mensch)</th><td>{len(review)} ({len(review) / n:.0%})</td></tr>"
|
||||
f"<tr><th>Gemma gefragt</th><td>{gemma} ({gemma / n:.0%})</td></tr>"
|
||||
f"<tr><th>nur strukturell gleich (Paare / gegen Wahrheit)</th><td>{summary['structural']['agree']} / "
|
||||
f"{summary['structural']['truth']}</td></tr>"
|
||||
f"<tr><th>Ø Zeit pro Formel (alle Tools nacheinander)</th><td>{summary['mean_s_per_formula']:.2f} s</td></tr>"
|
||||
+ "".join(f"<tr><th>Ø {lab[k]}</th><td>{v:.2f} s</td></tr>" for k, v in summary["mean_s"].items() if v)
|
||||
+ "".join(f"<tr><th>{lab[k]} allein richtig</th><td>{v}/{len(secs[k])}</td></tr>"
|
||||
|
|
|
|||
Loading…
Reference in a new issue