Download src/scripts/fixtrain_gates.py from voraseth/klon8-base: direct link, hf CLI and curl.
- Browser
- Download file 8.35 kB
-
https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fixtrain_gates.py
- Command line
-
hf download hf://voraseth/klon8-base/src/scripts/fixtrain_gates.py
-
curl -L -o fixtrain_gates.py https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fixtrain_gates.py
8.35 kB
| """Pre-judge gates for FIX 5 (mask schedule) and FIX 7 (PAD attention mask), runs/CSEL/FIX_TRAINING_PREREG.md. | |
| Compares one fix arm with the shared control runs/CANVAS_FIXU_CTL (job 152: HEAD trainer, WangchanBERTa body, fixed | |
| 12-epoch OneCycle, last epoch) on: | |
| 1. LOSS-CURVE gate (pre-registered). fix5 ("trajectory"): the average valid loss over mask ratios | |
| {1.0, 0.9, ..., 0.1, 0.05} must drop >= 0.03 nats vs CTL AND the standard valid loss must be no worse than | |
| CTL + 0.03. fix7 ("standard"): the trainer's standard masking must drop >= 0.03 nats. | |
| Both conditions are computed here on the trainer's 1,000 validation poems (same split: seed 0 shuffle of | |
| load_poems), identical masks for both models (seed 1 for standard). | |
| 2. raw candidates on the dev49 pilot (the shared CTL decodes runs/DEV49_CTL vs the arm's), paired by seed: form >= 98% and not below CTL by more | |
| than 1pp; line-final syllable entropy within 0.3 bits; distinct-2 over syllables not below CTL by more than 0.03; | |
| immediate syllable repeats (stutters) per poem not above CTL + 0.05; line-final word (newmm) share unseen in the train | |
| poems not above CTL + 0.02, and mean line-final word log-frequency within 0.3 of CTL (strange or bland). | |
| Writes RUN/gates_ARM.json; exits 2 if any gate fails (the arm is then not judged). | |
| Usage: fixtrain_gates.py RUN_DIR CTL_MODEL ARM_MODEL ARM_NAME {trajectory|standard} CTL_CANDS ARM_CANDS [SEEDS A-B] | |
| Amendment 2026-09-29 (fixes re-run on the bug-fixed base, CTL_MODEL = runs/CANVAS_FIXBASE): the validation poems are | |
| loaded exactly as the control model's trainer loaded them (segmenter, title cleaning and ๆ handling read from its | |
| vocab.json; needs 170's patched canvas_bot.py for such models), both models must record the same data flags, and | |
| SEEDS restricts the candidate files to c{A..B}.jsonl (170's control directory holds seeds 0-63). | |
| """ | |
| import os, sys, json, glob, math, random, collections | |
| sys.path.insert(0, "."); sys.path.insert(0, "scripts") | |
| import torch, torch.nn.functional as F | |
| import canvas_bot as B | |
| from syl_align import syls_aligned as SA | |
| RATIOS = (1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05) | |
| def valid_losses(model_dir, VY_cache={}): | |
| """standard: the trainer's validation masking (seed 1); r=X: every poem cell masked with probability X (seed 2), | |
| the trajectory an iterative decoder visits; identical masks for every model.""" | |
| model, V = B.load_model(model_dir) | |
| meta = json.load(open(f"{model_dir}/vocab.json", encoding="utf-8")) | |
| kw = {"seg": meta.get("seg", "aligned")} | |
| kw.update({k: True for k in ("clean_titles", "yamok") if meta.get(k)}) # the fixed base's data flags | |
| if "VY" not in VY_cache: | |
| random.seed(0); torch.manual_seed(0) # the trainer's split: seed 0 shuffle | |
| poems = B.load_poems("data/modern_bot_train.jsonl", **kw); random.shuffle(poems) | |
| VY_cache["VY"] = B.build_tok(poems[:1000], V); VY_cache["itos"] = V.itos; VY_cache["kw"] = kw | |
| assert V.itos == VY_cache["itos"], "control and arm use different vocabularies" | |
| assert kw == VY_cache["kw"], f"control and arm load their data differently: {VY_cache['kw']} vs {kw}" | |
| VY = VY_cache["VY"]; seg = model.seg_id.cpu(); poem = (seg[None, :] == 1) & (VY != B.PAD) | |
| conds = [("standard",) + B.mask_batch(VY, seg, torch.Generator().manual_seed(1))] | |
| g = torch.Generator().manual_seed(2); u = torch.rand(VY.shape, generator=g) | |
| for r in RATIOS: | |
| m = poem & (u < r) | |
| if r == 1.0: m = poem.clone() | |
| conds.append((f"r={r}", torch.where(m, torch.full_like(VY, B.MASK), VY), m)) | |
| out = {} | |
| with torch.no_grad(): | |
| for name, x, m in conds: | |
| tot = cnt = 0 | |
| for i in range(0, len(VY), 50): | |
| mi = m[i:i + 50] | |
| if not mi.any(): continue | |
| lg = model(x[i:i + 50].to(B.DEV)).float().cpu() | |
| tot += float(F.cross_entropy(lg[mi], VY[i:i + 50][mi], reduction="sum")); cnt += int(mi.sum()) | |
| out[name] = tot / max(cnt, 1) | |
| out["trajectory"] = sum(out[f"r={r}"] for r in RATIOS) / len(RATIOS) | |
| del model | |
| if str(B.DEV) == "mps": torch.mps.empty_cache() | |
| return out | |
| WF = None | |
| def word_logfreq(): | |
| """log frequency of words in the TRAIN poems (newmm), for the line-final strange-word check""" | |
| global WF | |
| if WF is None: | |
| from pythainlp.tokenize import word_tokenize | |
| c = collections.Counter() | |
| for l in open("data/modern_bot_train.jsonl", encoding="utf-8"): | |
| for ln in json.loads(l)["poem"].split("\n"): c.update(w for w in word_tokenize(ln.replace(" ", ""), engine="newmm") if w.strip()) | |
| tot = sum(c.values()); WF = (c, tot) | |
| return WF | |
| def stats(rows): | |
| from pythainlp.tokenize import word_tokenize | |
| c_, tot_ = word_logfreq() | |
| finals = collections.Counter(); bi = set(); nbi = 0; stut = 0; lf = [] | |
| for r in rows: | |
| for ln in r["poem"].split("\n"): | |
| sy = SA(ln, 12) | |
| if sy: finals[sy[-1]] += 1 | |
| pairs = list(zip(sy, sy[1:])); nbi += len(pairs); bi.update(pairs); stut += sum(a == b for a, b in pairs) | |
| ws = [w for w in word_tokenize(ln, engine="newmm") if w.strip()] | |
| if ws: lf.append(math.log((c_[ws[-1]] + 1) / (tot_ + len(c_)))) | |
| tot = sum(finals.values()) | |
| return {"n": len(rows), "form": 100 * sum(bool(r["all_pass"]) for r in rows) / len(rows), | |
| "final_bits": -sum(c / tot * math.log2(c / tot) for c in finals.values()), | |
| "distinct2": len(bi) / max(nbi, 1), "stutters": stut / len(rows), | |
| "final_word_logfreq": sum(lf) / max(len(lf), 1), "final_word_unseen": sum(x < math.log(1.5 / (tot_ + len(c_))) for x in lf) / max(len(lf), 1)} | |
| def seed_files(d, spec=None): | |
| fs = sorted(glob.glob(f"{d}/c*.jsonl")) | |
| if spec: | |
| a, b = map(int, spec.split("-")) | |
| fs = [f"{d}/c{s}.jsonl" for s in range(a, b + 1)] | |
| assert all(os.path.exists(f) for f in fs), f"{d}: missing seed files in {spec}" | |
| return fs | |
| def main(run, ctl_model, arm_model, arm, cond, ctl_cands, arm_cands, seeds=None): | |
| assert cond in ("trajectory", "standard") | |
| vc, va = valid_losses(ctl_model), valid_losses(arm_model) | |
| S = {} | |
| for a, d in (("CTL", ctl_cands), (arm, arm_cands)): | |
| rows = [json.loads(l) for f in seed_files(d, seeds) for l in open(f, encoding="utf-8")] | |
| assert rows, f"no candidates in {d}" | |
| S[a] = stats(rows) | |
| c, f = S["CTL"], S[arm] | |
| dead = [] | |
| if cond == "trajectory": | |
| if va["trajectory"] > vc["trajectory"] - 0.03: dead.append("loss-curve (trajectory-average valid loss not >= 0.03 below CTL)") | |
| if va["standard"] > vc["standard"] + 0.03: dead.append("loss-curve (standard valid loss worse than CTL + 0.03)") | |
| elif va[cond] > vc[cond] - 0.03: dead.append(f"loss-curve ({cond} valid loss not >= 0.03 below CTL)") | |
| if f["form"] < c["form"] - 1: dead.append("form (below CTL - 1pp)") | |
| if abs(f["final_bits"] - c["final_bits"]) > 0.3: dead.append("rhyme variety") | |
| if f["distinct2"] < c["distinct2"] - 0.03: dead.append("distinct-2") | |
| if f["stutters"] > c["stutters"] + 0.05: dead.append("stutters") | |
| # line-final strange-word check (both directions): rarer = strange words at line ends; more frequent = blander | |
| if f["final_word_unseen"] > c["final_word_unseen"] + 0.02: dead.append("line-final unseen words") | |
| if abs(f["final_word_logfreq"] - c["final_word_logfreq"]) > 0.3: dead.append("line-final word frequency (strange or bland)") | |
| print(f" valid loss CTL standard {vc['standard']:.3f} trajectory {vc['trajectory']:.3f} | {arm} standard {va['standard']:.3f} trajectory {va['trajectory']:.3f} (gate: {cond})") | |
| for a, s in S.items(): | |
| print(f" {a}: n {s['n']}, form {s['form']:.1f}%, final-syllable bits {s['final_bits']:.2f}, distinct-2 {s['distinct2']:.3f}, stutters/poem {s['stutters']:.3f}, line-final word log-freq {s['final_word_logfreq']:.2f}, unseen {s['final_word_unseen']:.3f}") | |
| json.dump({"valid": {"CTL": vc, arm: va}, "arms": S, "dead": dead}, open(f"{run}/gates_{arm}.json", "w"), indent=1) | |
| print(f" {arm} KILLED before judging: " + ", ".join(dead) if dead else f" {arm} pre-judge gates PASS") | |
| if dead: sys.exit(2) | |
| if __name__ == "__main__": | |
| main(*sys.argv[1:9]) | |