#!/usr/bin/env python3 """exp15 — fertility calibration B1.2 ($0, offline): src chars -> ru output-token coefficients. Regresses ru output tokens of the 25-chapter rerun finals on (cjk, other) source-char counts, giving the chunker's est_out = f_cjk*cjk + f_other*other (§B1.1/B1.2). Chunker-spec deliverable feeding the Q1 size-match. Offline tokenizer per §B1.2: deepseek-v4 tokenizer dir is EMPTY in repo, so we use the editor-family glm-4.6 tokenizer (present) and label it; Go needs only the COEFFICIENTS, not a tokenizer. Run: eval/.venv/bin/python eval/exp15/fertility_calib.py """ from __future__ import annotations import json from pathlib import Path import numpy as np from tokenizers import Tokenizer REC = Path("/home/ubuntu/books/gu-zhenren/rerun/records.json") TOK = Path("/home/ubuntu/projects/textmachine/eval/tokenizers/glm-4.6/tokenizer.json") OUT = Path("/home/ubuntu/books/gu-zhenren/exp15/fertility_calib.json") def is_cjk(ch: str) -> bool: o = ord(ch) return (0x4E00 <= o <= 0x9FFF or 0x3040 <= o <= 0x30FF or 0x3400 <= o <= 0x4DBF or 0xF900 <= o <= 0xFAFF or 0x20000 <= o <= 0x2A6DF) def char_classes(s: str): cjk = sum(1 for c in s if is_cjk(c)) other = sum(1 for c in s if not c.isspace() and not is_cjk(c)) return cjk, other def main(): tok = Tokenizer.from_file(str(TOK)) recs = json.load(open(REC)) rows, meta = [], [] for r in recs: src = r.get("source") or "" fin = r.get("final") or "" if not src.strip() or not fin.strip(): continue cjk, other = char_classes(src) out_tok = len(tok.encode(fin).ids) if cjk + other < 200: # skip tiny/metadata chunks (e.g. title block) continue rows.append((cjk, other, out_tok)) meta.append((r.get("chapter"), r.get("chunk_idx"))) A = np.array([[c, o] for c, o, _ in rows], dtype=float) y = np.array([t for _, _, t in rows], dtype=float) # non-negative-ish least squares, no intercept (est_out = f_cjk*cjk + f_other*other) coef, *_ = np.linalg.lstsq(A, y, rcond=None) f_cjk, f_other = float(coef[0]), float(coef[1]) pred = A @ coef ss_res = float(np.sum((y - pred) ** 2)) ss_tot = float(np.sum((y - y.mean()) ** 2)) r2 = 1 - ss_res / ss_tot if ss_tot else float("nan") mape = float(np.mean(np.abs((y - pred) / y))) * 100 result = { "n_records": len(rows), "tokenizer": "glm-4.6 (editor family; deepseek-v4 tokenizer dir empty in repo — §B1.2 caveat)", "model": "est_out = f_cjk*cjk_src + f_other*other_src (no intercept)", "f_cjk": round(f_cjk, 4), "f_other": round(f_other, 4), "r2": round(r2, 4), "mape_pct": round(mape, 2), "placeholder_B1_1": {"cjk_char": 0.75, "other_char": 0.25}, "note": "per-language-pair config (layer 2); recompute = loud resnapshot. Src-char est is the SECOND " "quantity (§B1.2); primary budget is ru-out tokens. Calibrated on 25-ch guzhenren rerun (in-pretrain).", } OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"fertility calibration (n={len(rows)} chunks, glm-4.6 ru tokens):") print(f" est_out = {f_cjk:.4f}*cjk + {f_other:.4f}*other R2={r2:.4f} MAPE={mape:.1f}%") print(f" vs §B1.1 placeholder cjk=0.75/other=0.25") print(f" sample src cjk/other -> out (first 6):") for (c, o, t), m in list(zip(rows, meta))[:6]: print(f" ch{m[0]}#{m[1]}: cjk={c} other={o} out_tok={t} pred={f_cjk*c+f_other*o:.0f}") print(f"-> {OUT}") # self-review: coefficients must be positive and R2 reasonable assert f_cjk > 0, f"non-positive cjk fertility {f_cjk}" assert r2 > 0.5, f"poor fit R2={r2} — investigate before trusting coefficients" print("OK: positive fertility coefficients, R2>0.5.") if __name__ == "__main__": main()