Buckets:
| """ | |
| CLAIM 3 -- Theorem 3.8: there exist hypothesis classes with gamma-graph | |
| dimension d_gamma and gamma-OIG dimension at most 3 for which EVERY finite | |
| aggregation algorithm (Def. 3.6) using an interpolating aggregation rule | |
| (Def. 3.7) still needs n = Omega(d_gamma/eps), i.e. E[L] > eps for | |
| n <= d_gamma/(128 eps) -- separating them from general learners that get | |
| Otilde(1/eps). | |
| Independent method | |
| ------------------ | |
| (a) Build the class explicitly: H = {h_A : A subset N, |A| = d}, h_A = 0 on A and | |
| a unique gamma_A in (gamma,1] elsewhere. Compute d_gamma EXHAUSTIVELY and | |
| the gamma-OIG dimension EXHAUSTIVELY (all point sets of size k, all | |
| orientations of the induced one-inclusion hypergraph, plus vertex-induced | |
| subgraphs) -- both from Definitions 3.2 and 3.9. | |
| (b) Simulate the hard distribution family and a *best-effort* finite aggregation | |
| algorithm (it puts every observed point in the zero-set of a selected | |
| hypothesis and spends the rest of its budget guessing), for a grid of | |
| (d, eps, m) and several interpolating rules. Check E[L] > eps at | |
| n = floor(d/(128 eps)). | |
| (c) Verify the separation numerically: run an explicit *general* (non-finite- | |
| aggregation) improper learner on the same class and measure its loss. | |
| Seeds: numpy default_rng(20260725 + offset). CPU only. | |
| """ | |
| import math | |
| import os | |
| import sys | |
| import numpy as np | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from core import ( # noqa: E402 | |
| dump_json, | |
| gamma_graph_dim, | |
| gamma_oig_dim, | |
| is_interpolating_rule, | |
| rule_max, | |
| rule_mean, | |
| rule_median, | |
| rule_midrange, | |
| rule_min, | |
| thm38_class, | |
| ) | |
| OUT = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs" | |
| ) | |
| GAMMA = 0.1 | |
| SEED = 20260725 | |
| res = {"gamma": GAMMA, "seed": SEED, "theorem": "3.8"} | |
| # --------------------------------------------------------------------------- | |
| # (a) the two dimensions of the construction, computed exhaustively | |
| # --------------------------------------------------------------------------- | |
| print("== dimensions of the Theorem 3.8 class ==") | |
| dims = [] | |
| for ku, d in ((4, 2), (5, 2), (6, 2), (6, 3), (8, 4)): | |
| cls, subsets = thm38_class(ku, d, GAMMA) | |
| dg = gamma_graph_dim(cls, GAMMA, max_d=min(ku, d + 2)) | |
| max_k = min(5, ku) | |
| oig, oig_detail = gamma_oig_dim(cls, GAMMA, max_k=max_k) | |
| dims.append( | |
| { | |
| "ku": ku, | |
| "d": d, | |
| "n_hyp": cls.n_hyp, | |
| "d_gamma": dg, | |
| "d_gamma_expected": min(d, ku - d), | |
| "oig_dim": oig, | |
| "oig_le_3": bool(oig <= 3), | |
| "oig_detail": oig_detail, | |
| "oig_scanned_up_to_k": max_k, | |
| } | |
| ) | |
| print( | |
| f" ku={ku} d={d} |H|={cls.n_hyp:3d}: d_gamma={dg} (expect {min(d,ku-d)}) " | |
| f"gamma-OIG dim={oig} (<=3: {oig<=3}) " | |
| f"min-max-outdeg per k={[ (k,v['min_max_outdegree']) for k,v in oig_detail.items() ]}" | |
| ) | |
| res["dimensions"] = dims | |
| res["all_oig_le_3"] = bool(all(x["oig_le_3"] for x in dims)) | |
| res["all_dgamma_match"] = bool(all(x["d_gamma"] == x["d_gamma_expected"] for x in dims)) | |
| # rules used below really are interpolating (Definition 3.7) | |
| rng = np.random.default_rng(SEED) | |
| IRULES = { | |
| "mean": rule_mean, | |
| "median": rule_median, | |
| "midrange": rule_midrange, | |
| "min": rule_min, | |
| "max": rule_max, | |
| } | |
| res["rule_interpolating"] = { | |
| k: bool(is_interpolating_rule(v, rng)) for k, v in IRULES.items() | |
| } | |
| print(" Definition 3.7 check (interpolating?):", res["rule_interpolating"]) | |
| # an interpolating rule cannot rescue a point where every selected hypothesis | |
| # is gamma-far: check explicitly on the actual value range of the construction | |
| vals = np.array([GAMMA + 0.05 * GAMMA, GAMMA + 0.55 * GAMMA]) | |
| res["interpolating_rule_cannot_rescue"] = { | |
| k: bool(abs(f(vals) - 0.0) > GAMMA) for k, f in IRULES.items() | |
| } | |
| print( | |
| " every interpolating rule still errs when all inputs are in (gamma,1]:", | |
| res["interpolating_rule_cannot_rescue"], | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # (b) the hard distribution and the best-effort finite aggregation algorithm | |
| # --------------------------------------------------------------------------- | |
| def simulate_finite_aggregation(d, eps, n, m, ku, trials, seed, a_const=2.0): | |
| """A ~ uniform: A_1 = 1, A_2..A_d without replacement from {2,...,ku}. | |
| D_A: mass 1 - a*eps on A_1 and a*eps/(d-1) on each A_j, all labelled 0. | |
| The finite aggregation algorithm selects m hypotheses h_{B_1},...,h_{B_m} | |
| (|B_j| = d) -- best effort: cover every observed point, then spend the | |
| remaining m*d - |obs| slots on uniformly random unobserved points. | |
| Every point outside Z = union B_j is mispredicted by any interpolating rule. | |
| """ | |
| rng = np.random.default_rng(seed) | |
| p = np.empty(d) | |
| p[0] = 1.0 - a_const * eps | |
| p[1:] = a_const * eps / (d - 1) | |
| losses = np.empty(trials) | |
| for t in range(trials): | |
| A = np.empty(d, dtype=np.int64) | |
| A[0] = 1 | |
| rest = rng.integers(2, ku + 1, size=d - 1) # ku >> d: collisions negligible | |
| while len(np.unique(rest)) != d - 1: | |
| rest = rng.integers(2, ku + 1, size=d - 1) | |
| A[1:] = rest | |
| draws = rng.choice(d, size=n, p=p) | |
| obs = np.unique(A[draws]) | |
| budget = m * d | |
| n_guess = max(0, budget - len(obs)) | |
| # guesses: uniform over the universe minus the observed points | |
| guesses = ( | |
| rng.integers(1, ku + 1, size=n_guess) if n_guess else np.empty(0, np.int64) | |
| ) | |
| Z = np.union1d(obs, guesses) | |
| missed = ~np.isin(A, Z) | |
| losses[t] = float(p[missed].sum()) | |
| return float(losses.mean()), float(losses.std(ddof=1) / math.sqrt(trials)) | |
| print( | |
| "\n== Theorem 3.8: finite aggregation with an interpolating rule, n = floor(d/(128 eps)) ==" | |
| ) | |
| grid = [] | |
| TRIALS = 3000 | |
| for d in (32, 64, 128, 256): | |
| for eps in (0.1, 0.05, 0.02, 0.01): | |
| n = int(np.floor(d / (128 * eps))) | |
| for m in (1, 3, 10, 100): | |
| ku = max(1000, 1000 * d * m) # "ku sufficiently large relative to d*m(n)" | |
| mean, se = simulate_finite_aggregation( | |
| d, eps, n, m, ku, TRIALS, SEED + d * 100 + m | |
| ) | |
| grid.append( | |
| { | |
| "d_gamma": d, | |
| "eps": eps, | |
| "n": n, | |
| "m_hypotheses": m, | |
| "ku": ku, | |
| "E_loss": mean, | |
| "stderr": se, | |
| "E_loss_over_eps": mean / eps, | |
| "holds": bool(mean - 2 * se > eps), | |
| } | |
| ) | |
| res["grid"] = grid | |
| n_hold = sum(g["holds"] for g in grid) | |
| res["grid_hold"] = f"{n_hold}/{len(grid)}" | |
| res["min_ratio"] = min(g["E_loss_over_eps"] for g in grid) | |
| print( | |
| f" {n_hold}/{len(grid)} cells satisfy E[L] > eps (2-sigma); min E[L]/eps = {res['min_ratio']:.3f}" | |
| ) | |
| for g in grid: | |
| if g["eps"] == 0.01: | |
| print( | |
| f" d={g['d_gamma']:3d} eps={g['eps']} n={g['n']:4d} m={g['m_hypotheses']:4d} ku={g['ku']:8d}" | |
| f" E[L]={g['E_loss']:.5f} +/- {2*g['stderr']:.5f} ({g['E_loss_over_eps']:.2f} x eps)" | |
| ) | |
| # how large must m be before the bound breaks? (the theorem lets ku grow with m) | |
| print( | |
| "\n m-sweep at fixed ku (shows the bound is about ku >> d*m, not about m alone):" | |
| ) | |
| msweep = [] | |
| d, eps = 128, 0.01 | |
| n = int(np.floor(d / (128 * eps))) | |
| ku_fixed = 20000 | |
| for m in (1, 10, 100, 300, 1000): | |
| mean, se = simulate_finite_aggregation(d, eps, n, m, ku_fixed, 2000, SEED + 5 + m) | |
| msweep.append( | |
| { | |
| "m": m, | |
| "ku": ku_fixed, | |
| "d_times_m_over_ku": d * m / ku_fixed, | |
| "E_loss": mean, | |
| "E_loss_over_eps": mean / eps, | |
| "holds": bool(mean > eps), | |
| } | |
| ) | |
| print( | |
| f" m={m:5d} d*m/ku={d*m/ku_fixed:7.3f} E[L]/eps={mean/eps:6.3f} {'(holds)' if mean>eps else '(broken: universe too small)'}" | |
| ) | |
| res["m_sweep_fixed_ku"] = msweep | |
| # --------------------------------------------------------------------------- | |
| # (b2) where does the loss actually cross eps? n*(eps) should be Theta(d/eps). | |
| # --------------------------------------------------------------------------- | |
| print("\n== empirical threshold n*(eps): smallest n with E[L] <= eps (m=10) ==") | |
| thr = [] | |
| for d in (32, 64, 128): | |
| for eps in (0.05, 0.02, 0.01): | |
| lo, hi = 1, 1 << 17 | |
| while lo < hi: | |
| mid = (lo + hi) // 2 | |
| mean, _ = simulate_finite_aggregation( | |
| d, eps, mid, 10, 1000 * d * 10, 800, SEED + 77 | |
| ) | |
| if mean <= eps: | |
| hi = mid | |
| else: | |
| lo = mid + 1 | |
| thr.append( | |
| {"d_gamma": d, "eps": eps, "n_star": lo, "n_star_over_d_over_eps": lo / (d / eps)} | |
| ) | |
| print(f" d={d:4d} eps={eps:<5} n*={lo:7d} n*/(d/eps)={lo/(d/eps):.4f}") | |
| res["empirical_threshold"] = thr | |
| rat = [t["n_star_over_d_over_eps"] for t in thr] | |
| res["n_star_ratio_mean"] = float(np.mean(rat)) | |
| res["n_star_ratio_cv"] = float(np.std(rat) / np.mean(rat)) | |
| print(f" n*/(d/eps): mean={np.mean(rat):.4f} coeff-of-variation={np.std(rat)/np.mean(rat):.4f} " | |
| f"(constant => n* = Theta(d_gamma/eps))") | |
| # --------------------------------------------------------------------------- | |
| # (c) the separation: a general improper learner on the SAME class | |
| # --------------------------------------------------------------------------- | |
| print("\n== separation: a general (non-finite-aggregation) improper learner ==") | |
| def improper_learner_loss(d, ku, n, trials, seed, p_nonzero): | |
| """Learner: if the sample ever shows a non-zero label it reveals the unique | |
| gamma_A, which identifies the target hypothesis exactly -> zero loss. | |
| Otherwise predict the constant 0 function (NOT in H, and not reachable by | |
| any interpolating aggregation of members of H). | |
| Distribution: mass p_nonzero spread on points where the target is non-zero, | |
| the rest on its zero set. Loss = p_nonzero * 1{no non-zero label seen}.""" | |
| rng = np.random.default_rng(seed) | |
| seen = rng.random((trials, n)) < p_nonzero | |
| identified = seen.any(axis=1) | |
| return float(np.mean(np.where(identified, 0.0, p_nonzero))) | |
| sep = [] | |
| for d in (32, 64, 128, 256): | |
| eps = 0.01 | |
| n = max(1, int(np.floor(d / (128 * eps)))) | |
| # worst case over the free parameter p_nonzero | |
| worst = max( | |
| (improper_learner_loss(d, 10**6, n, 4000, SEED + 11, p), p) | |
| for p in np.linspace(0.005, 0.9, 60) | |
| ) | |
| fa = [ | |
| g | |
| for g in grid | |
| if g["d_gamma"] == d and g["eps"] == eps and g["m_hypotheses"] == 100 | |
| ][0] | |
| sep.append( | |
| { | |
| "d_gamma": d, | |
| "eps": eps, | |
| "n": n, | |
| "finite_aggregation_E_loss": fa["E_loss"], | |
| "general_learner_worst_E_loss": worst[0], | |
| "general_learner_worst_p": float(worst[1]), | |
| "theory_bound_1_over_en": 1.0 / (math.e * n), | |
| "gap_factor": fa["E_loss"] / max(worst[0], 1e-12), | |
| } | |
| ) | |
| print( | |
| f" d_gamma={d:3d} n={n:4d}: finite aggregation E[L]={fa['E_loss']:.5f} " | |
| f"general learner worst-case E[L]={worst[0]:.5f} (1/(en)={1/(math.e*n):.5f}) " | |
| f"gap = {sep[-1]['gap_factor']:.1f}x" | |
| ) | |
| res["separation"] = sep | |
| res["gap_grows_with_dgamma"] = bool(sep[-1]["gap_factor"] > sep[0]["gap_factor"]) | |
| ok = ( | |
| res["all_oig_le_3"] | |
| and res["all_dgamma_match"] | |
| and n_hold == len(grid) | |
| and res["gap_grows_with_dgamma"] | |
| ) | |
| res["verdict"] = "verified" if ok else "partial" | |
| print(f"\nverdict = {res['verdict']}") | |
| dump_json(os.path.join(OUT, "claim3_thm38.json"), res) | |
Xet Storage Details
- Size:
- 11.7 kB
- Xet hash:
- 484bce2e0d98c0dcee5913929b0e9095e28f623d4a0f9505b4579bfbaed205cf
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.