| """Where does the entropy ceiling H(p) <= log|S| sit, per stage? |
| |
| Two things are printed. |
| |
| 1. The ceiling itself, log|S| averaged over the |S| >= 2 cells of the eval set, |
| read straight from the candidate masks. This is the number the model's |
| entropy has to stay under, and it shrinks as the curriculum deepens. |
| |
| 2. Whether a model with a given (mass*, spread*) can satisfy the ceiling at |
| all. Writing p as mass mu on S with conditional q, and leak 1-mu spread as r |
| off S, |
| |
| H(p) = mu*H(q) + (1-mu)*H(r) + Hbin(mu), H(q) = spread * log|S| |
| |
| so the margin is |
| |
| log|S| - H(p) = log|S| * (1 - mu*spread) - Hbin(mu) - (1-mu)*H(r). |
| |
| H(r) is the only term not pinned by the reported metrics, and it is bounded: |
| 0 if the leak sits on one digit, log(9-|S|) if it spreads over all illegal |
| digits. That brackets the margin, which is enough to tell whether the gate is |
| reachable without running the model. |
| |
| The point this makes: Uniform(S) sits exactly ON the ceiling, so at spread* = 1 |
| any leak at all breaks it. The ceiling and a high spread* gate are in tension, |
| not independent. |
| |
| Run: python wavecurriculum_run/probe_entropy_ceiling.py |
| """ |
| import os |
| import sys |
|
|
| import numpy as np |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| CAND = os.path.join(os.path.dirname(HERE), "datasets_multicandidate_s12", |
| "test_cand_masks.npy") |
|
|
| |
| REPORTED = [ |
| ("w12_multistage", 3, 0.9512, 0.9227), |
| ] |
|
|
|
|
| def hbin(mu): |
| mu = np.clip(mu, 1e-12, 1 - 1e-12) |
| return -mu * np.log(mu) - (1 - mu) * np.log(1 - mu) |
|
|
|
|
| def main(): |
| if not os.path.exists(CAND): |
| sys.exit(f"candidate masks not found at {CAND}") |
| |
| masks = np.load(CAND, mmap_mode="r") |
| print(f"{CAND}\nshape {masks.shape}\n") |
| sub = np.asarray(masks[:2000]) |
| n_stages = sub.shape[1] |
|
|
| sizes = np.zeros(sub.shape, dtype=np.int16) |
| for d in range(9): |
| sizes += ((sub >> d) & 1).astype(np.int16) |
|
|
| print("stage cells|S|>=2 mean|S| mean log|S| %|S|=2 %|S|>=5") |
| ceil = {} |
| for s in range(n_stages): |
| sz = sizes[:, s, :].ravel() |
| multi = sz[sz >= 2] |
| if multi.size == 0: |
| print(f"{s + 1:>5} {'no |S|>=2 cells':>38}") |
| continue |
| ceil[s + 1] = float(np.log(multi).mean()) |
| print(f"{s + 1:>5} {multi.size:>10} {multi.mean():>8.3f} " |
| f"{ceil[s + 1]:>10.4f} {100 * (multi == 2).mean():>7.1f} " |
| f"{100 * (multi >= 5).mean():>7.1f}") |
|
|
| print("\nIs the ceiling reachable at the (mass*, spread*) each run reports?") |
| print("margin = log|S|(1 - mass*spread) - Hbin(mass) - (1-mass)H(leak)") |
| print("best = leak on ONE digit (H(r)=0); worst = leak spread over all " |
| "illegal digits\n") |
| for run, stage, mass, spread in REPORTED: |
| lm = ceil.get(stage) |
| if lm is None: |
| continue |
| sz = sizes[:, stage - 1, :].ravel() |
| multi = sz[sz >= 2] |
| best = lm * (1 - mass * spread) - hbin(mass) |
| |
| h_r = np.log(np.maximum(9 - multi, 1)).mean() |
| worst = best - (1 - mass) * h_r |
| print(f"{run} stage {stage}: mass*={mass:.4f} spread*={spread:.4f} " |
| f"log|S|={lm:.4f}") |
| print(f" H(p) is between {lm - best:.4f} and {lm - worst:.4f} nats") |
| print(f" margin best {best:+.4f} worst {worst:+.4f} " |
| f"-> ceiling {'HOLDS' if worst >= 0 else ('borderline' if best >= 0 else 'VIOLATED')}") |
| need = (hbin(mass)) / max(1 - mass * spread, 1e-9) |
| print(f" to hold in the best case this stage needs log|S| >= " |
| f"{need:.4f}, i.e. |S| >= {np.exp(need):.2f}\n") |
|
|
| print("At spread*=1 the margin is log|S|(1-mass) - Hbin(mass) - ..., which " |
| "is < 0 for every mass < 1:") |
| print(" mass log|S|=log2 log4 log6 (spread*=1, leak on one digit)") |
| for mass in (1.0, 0.99, 0.95, 0.90): |
| row = [f"{np.log(m) * (1 - mass) - hbin(mass):+.4f}" for m in (2, 4, 6)] |
| print(f" {mass:.2f} " + " ".join(f"{v:>10}" for v in row)) |
|
|
| |
| |
| |
| |
| |
| print("\nWorst-case ceiling margin at the corner of each candidate gate set") |
| print("(mass* bar, spread* bar); needs to be >= 0 at every stage to be " |
| "satisfiable:\n") |
| trials = [(0.95, 0.88), (0.95, 0.75), (0.95, 0.60), (0.98, 0.60), |
| (0.95, 0.50)] |
| hdr = " ".join(f"m{int(100 * a)}/s{int(100 * b)}" for a, b in trials) |
| print(f"stage log|S| {hdr}") |
| for s in sorted(ceil): |
| sz = sizes[:, s - 1, :].ravel() |
| multi = sz[sz >= 2] |
| h_r = np.log(np.maximum(9 - multi, 1)).mean() |
| cells = [] |
| for mass, spread in trials: |
| mg = (ceil[s] * (1 - mass * spread) - hbin(mass) |
| - (1 - mass) * h_r) |
| cells.append(f"{mg:+.3f}" if mg >= 0 else f"[{mg:+.3f}]") |
| print(f"{s:>5} {ceil[s]:.4f} " + " ".join(f"{c:>9}" for c in cells)) |
| print("\n[bracketed] = infeasible: no model can satisfy both that spread " |
| "bar and the ceiling at that stage.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|