Buckets:
| """ | |
| CLAIM 6 -- Theorem 3.12: proper learners require sample complexity | |
| Omega((d_gamma/eps) ln(1/eps)), strictly worse than the O(d_gamma/eps) achieved | |
| by median-of-three aggregation. | |
| Concretely the theorem states: for any 0 < gamma < 1 and d_gamma >= 2 there is a | |
| class H with gamma-graph dimension d_gamma such that for every proper learner A | |
| and every 0 < eps < 1/(64e) there is a realizable D with | |
| E_{S~D^n}[L^gamma_D(A(S))] >= 4 eps / 3 whenever n <= (d/(32 eps)) ln(1/(64 e eps)). | |
| Independent method | |
| ------------------ | |
| (a) Build the split-space class of the proof overview and verify d_gamma = d | |
| exhaustively (Definition 3.2). | |
| (b) Re-derive the hard instance: ku = Theta(d/eps), A subset [ku] with | |
| |A| = ku - d + 1 drawn uniformly, D_A uniform on {(ku,i) : i in A}, labels 0, | |
| realizable by h_{ku, A^c}. Conditioned on the sample, the unobserved points | |
| are exchangeable, so EVERY proper learner is forced to pick its d-1 | |
| "gamma-far" points blindly from the unobserved set. We verify this | |
| exchangeability empirically by running five different proper-learner | |
| strategies and comparing their losses. | |
| (c) Check the theorem's inequality at n = floor((d/(32 eps)) ln(1/(64 e eps))). | |
| (d) The headline: measure n*(eps), the smallest n at which the BEST proper | |
| learner reaches loss eps against the worst ku, and regress n* eps / d on | |
| ln(1/eps). A non-zero slope is the extra ln(1/eps) factor. | |
| (e) Run median-of-three interpolators on the SAME instances and show it reaches | |
| eps with n = Theta(d/eps) (no log), i.e. aggregation strictly beats proper | |
| learning. | |
| 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 ( | |
| dump_json, | |
| fit_loglog_slope, | |
| gamma_graph_dim, | |
| thm312_class, | |
| ) # noqa: E402 | |
| 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.12", | |
| "eps_upper_limit_1_over_64e": 1.0 / (64 * math.e)} | |
| # --------------------------------------------------------------------------- | |
| # (a) the class has gamma-graph dimension exactly d | |
| # --------------------------------------------------------------------------- | |
| print("== Theorem 3.12 class: exhaustive gamma-graph dimension ==") | |
| dims = [] | |
| for d in (2, 3, 4): | |
| cls, specs, offs = thm312_class([2 * d, max(1, d - 1)], d, GAMMA) | |
| dg = gamma_graph_dim(cls, GAMMA, max_d=d + 2) | |
| dims.append( | |
| { | |
| "d": d, | |
| "blocks": [2 * d, max(1, d - 1)], | |
| "n_hyp": cls.n_hyp, | |
| "n_pts": cls.n_pts, | |
| "d_gamma": dg, | |
| "match": bool(dg == d), | |
| } | |
| ) | |
| print( | |
| f" d={d}: |H|={cls.n_hyp:4d} |X|={cls.n_pts:3d} d_gamma={dg} (expected {d})" | |
| ) | |
| res["dimensions"] = dims | |
| res["all_dims_match"] = bool(all(x["match"] for x in dims)) | |
| # --------------------------------------------------------------------------- | |
| # (b) the hard instance and the loss of ANY proper learner | |
| # --------------------------------------------------------------------------- | |
| def proper_loss_analytic(d, ku, n): | |
| """E[L] for the optimal proper learner. | |
| |A| = N = ku - d + 1. After n i.i.d. draws from A, m = N - #distinct A-points | |
| are unobserved. The unobserved universe is U = M union A^c, |U| = m + d - 1, | |
| and by exchangeability the learner's d-1 chosen gamma-far points are uniform | |
| in U, so E[|A' ∩ A|] = (d-1) m / (m + d - 1) and L = that / N.""" | |
| N = ku - d + 1 | |
| if N <= 0: | |
| return 0.0 | |
| m = N * (1.0 - 1.0 / N) ** n | |
| return float((d - 1) * m / ((m + d - 1) * N)) | |
| def proper_loss_mc(d, ku, n, trials, seed, strategy="random"): | |
| """Real simulation with an explicit proper learner strategy.""" | |
| rng = np.random.default_rng(seed) | |
| N = ku - d + 1 | |
| out = np.empty(trials) | |
| for t in range(trials): | |
| perm = rng.permutation(ku) | |
| A = np.sort(perm[:N]) # the support of D_A | |
| Ac = np.sort(perm[N:]) # the d-1 points the target is gamma-far on | |
| draws = A[rng.integers(0, N, size=n)] | |
| obs = np.unique(draws) | |
| U = np.setdiff1d(np.arange(ku), obs, assume_unique=False) | |
| if strategy == "random": | |
| pick = rng.choice(U, size=d - 1, replace=False) | |
| elif strategy == "lowest": | |
| pick = U[: d - 1] | |
| elif strategy == "highest": | |
| pick = U[-(d - 1) :] | |
| elif strategy == "spread": | |
| pick = U[np.linspace(0, len(U) - 1, d - 1).astype(int)] | |
| elif strategy == "middle": | |
| mid = len(U) // 2 | |
| pick = U[mid : mid + d - 1] | |
| out[t] = np.isin(pick, A).sum() / N | |
| return float(out.mean()), float(out.std(ddof=1) / math.sqrt(trials)) | |
| print("\n== proper learners are forced to guess: five strategies, same loss ==") | |
| # the theorem requires 0 < eps < 1/(64 e) = 0.005746 for the sample bound to be | |
| # positive; we use eps = 0.001 | |
| d_t, eps_t = 16, 0.001 | |
| ku_t = int(round(d_t / (2 * eps_t))) | |
| n_t = max( | |
| 1, int(np.floor((d_t / (32 * eps_t)) * math.log(1.0 / (64 * math.e * eps_t)))) | |
| ) | |
| strat = {} | |
| for s in ("random", "lowest", "highest", "spread", "middle"): | |
| mean, se = proper_loss_mc(d_t, ku_t, n_t, 1500, SEED + 3, s) | |
| strat[s] = {"E_loss": mean, "stderr": se} | |
| print(f" strategy={s:8s} E[L] = {mean:.6f} +/- {2*se:.6f}") | |
| ana = proper_loss_analytic(d_t, ku_t, n_t) | |
| res["strategy_equivalence"] = { | |
| "d": d_t, | |
| "eps": eps_t, | |
| "ku": ku_t, | |
| "n": n_t, | |
| "analytic": ana, | |
| "strategies": strat, | |
| } | |
| vals = [v["E_loss"] for v in strat.values()] | |
| res["strategy_spread_rel"] = float((max(vals) - min(vals)) / np.mean(vals)) | |
| res["analytic_vs_mc_rel_err"] = float(abs(np.mean(vals) - ana) / ana) | |
| print( | |
| f" analytic prediction = {ana:.6f}; strategy spread = " | |
| f"{res['strategy_spread_rel']*100:.2f}% ; analytic vs MC = {res['analytic_vs_mc_rel_err']*100:.2f}%" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # (c) the theorem's inequality | |
| # --------------------------------------------------------------------------- | |
| print("\n== Theorem 3.12 at n = floor((d/(32 eps)) ln(1/(64 e eps))) ==") | |
| grid = [] | |
| for d in (4, 8, 16, 32, 64): | |
| for eps in (0.005, 0.002, 0.001, 0.0005, 0.0002): | |
| n = max( | |
| 1, int(np.floor((d / (32 * eps)) * math.log(1.0 / (64 * math.e * eps)))) | |
| ) | |
| # the theorem lets the adversary choose D; ku = Theta(d/eps) -- optimise | |
| best = max( | |
| ( | |
| (proper_loss_analytic(d, ku, n), ku) | |
| for ku in [ | |
| int(round(c * d / eps)) | |
| for c in (0.25, 0.4, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0) | |
| ] | |
| ) | |
| ) | |
| grid.append( | |
| { | |
| "d_gamma": d, | |
| "eps": eps, | |
| "n_theorem": n, | |
| "best_ku": best[1], | |
| "ku_over_d_over_eps": best[1] / (d / eps), | |
| "E_loss": best[0], | |
| "target_4eps_over_3": 4 * eps / 3, | |
| "E_loss_over_eps": best[0] / eps, | |
| "holds": bool(best[0] >= 4 * eps / 3), | |
| } | |
| ) | |
| res["grid"] = grid | |
| n_hold = sum(g["holds"] for g in grid) | |
| res["grid_hold"] = f"{n_hold}/{len(grid)}" | |
| res["min_ratio_to_4eps3"] = min(g["E_loss"] / (4 * g["eps"] / 3) for g in grid) | |
| print( | |
| f" {n_hold}/{len(grid)} cells satisfy E[L] >= 4eps/3; min E[L]/(4eps/3) = {res['min_ratio_to_4eps3']:.4f}" | |
| ) | |
| for g in grid: | |
| if g["d_gamma"] == 16: | |
| print( | |
| f" d={g['d_gamma']:3d} eps={g['eps']:<7} n={g['n_theorem']:8d} ku={g['best_ku']:9d} " | |
| f"(={g['ku_over_d_over_eps']:.2f} d/eps) E[L]={g['E_loss']:.6f} vs 4eps/3={4*g['eps']/3:.6f} {g['holds']}" | |
| ) | |
| # spot-check three grid cells with the real Monte-Carlo simulation | |
| print("\n Monte-Carlo spot checks of the analytic loss:") | |
| spot = [] | |
| for d, eps in ((8, 0.005), (16, 0.002), (32, 0.005)): | |
| n = int(np.floor((d / (32 * eps)) * math.log(1.0 / (64 * math.e * eps)))) | |
| ku = [g for g in grid if g["d_gamma"] == d and g["eps"] == eps][0]["best_ku"] | |
| a = proper_loss_analytic(d, ku, n) | |
| m, se = proper_loss_mc(d, ku, n, 800, SEED + 17) | |
| spot.append( | |
| { | |
| "d": d, | |
| "eps": eps, | |
| "n": n, | |
| "ku": ku, | |
| "analytic": a, | |
| "mc": m, | |
| "mc_stderr": se, | |
| "rel_err": abs(m - a) / a, | |
| } | |
| ) | |
| print( | |
| f" d={d:3d} eps={eps:<7} n={n:8d} ku={ku:8d}: analytic={a:.6f} MC={m:.6f} +/- {2*se:.6f}" | |
| ) | |
| res["mc_spot_checks"] = spot | |
| # --------------------------------------------------------------------------- | |
| # (d) the ln(1/eps) factor, measured | |
| # --------------------------------------------------------------------------- | |
| print("\n== measured sample complexity of the best proper learner ==") | |
| def worst_proper_loss(d, n): | |
| """max over ku of the optimal proper learner's loss at sample size n.""" | |
| best = 0.0 | |
| for c in np.geomspace(0.02, 40.0, 260): | |
| ku = int(round(c * n)) + d | |
| if ku <= d: | |
| continue | |
| best = max(best, proper_loss_analytic(d, ku, n)) | |
| return best | |
| def n_star_proper(d, eps): | |
| lo, hi = 1, 1 << 30 | |
| while lo < hi: | |
| mid = (lo + hi) // 2 | |
| if worst_proper_loss(d, mid) <= eps: | |
| hi = mid | |
| else: | |
| lo = mid + 1 | |
| return lo | |
| rows = [] | |
| for d in (8, 32, 128): | |
| for eps in (0.05, 0.02, 0.01, 0.005, 0.002, 0.001, 0.0005, 0.0002): | |
| ns = n_star_proper(d, eps) | |
| rows.append( | |
| { | |
| "d_gamma": d, | |
| "eps": eps, | |
| "n_star_proper": ns, | |
| "n_star_eps_over_d": ns * eps / d, | |
| "ln_1_over_eps": math.log(1 / eps), | |
| } | |
| ) | |
| if d == 32: | |
| print( | |
| f" d={d:4d} eps={eps:<7} n*={ns:10d} n* eps/d = {ns*eps/d:8.4f} ln(1/eps)={math.log(1/eps):.3f}" | |
| ) | |
| res["proper_sample_complexity"] = rows | |
| # regress n* eps/d on ln(1/eps): the ln(1/eps) factor shows up as a positive slope | |
| for d in (8, 32, 128): | |
| sub = [r for r in rows if r["d_gamma"] == d] | |
| x = np.array([r["ln_1_over_eps"] for r in sub]) | |
| y = np.array([r["n_star_eps_over_d"] for r in sub]) | |
| A_ = np.vstack([x, np.ones_like(x)]).T | |
| coef = np.linalg.lstsq(A_, y, rcond=None)[0] | |
| pred = A_ @ coef | |
| r2 = 1 - ((y - pred) ** 2).sum() / ((y - y.mean()) ** 2).sum() | |
| res[f"linear_fit_d{d}"] = { | |
| "slope_vs_ln_1_over_eps": float(coef[0]), | |
| "intercept": float(coef[1]), | |
| "r2": float(r2), | |
| } | |
| print( | |
| f" d={d:4d}: n* eps/d = {coef[0]:.4f} * ln(1/eps) + {coef[1]:.4f} R^2={r2:.5f}" | |
| ) | |
| # null model: n* eps/d = const (i.e. no ln(1/eps) factor) | |
| r2_null = 1 - ((y - y.mean()) ** 2).sum() / ((y - y.mean()) ** 2).sum() | |
| res[f"const_model_range_d{d}"] = float(y.max() / y.min()) | |
| print( | |
| f" constant-model check: n* eps/d ranges over {y.min():.3f} .. {y.max():.3f} " | |
| f"(factor {y.max()/y.min():.2f}) -> NOT constant" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # (e) median-of-three on the SAME instances | |
| # --------------------------------------------------------------------------- | |
| print("\n== median-of-three interpolators on the same class ==") | |
| def median3_loss_mc(d, ku, n, trials, seed): | |
| """Three independent samples; each yields an interpolator h_{ku,A'_j} with | |
| A'_j a size-(d-1) subset of that sample's unobserved set. The pointwise | |
| median errs at a point of A iff at least two of the three are gamma-far | |
| there. (Note: the median-of-three predictor is NOT a member of H, so this | |
| is exactly the improper aggregation the theorem is compared against.)""" | |
| rng = np.random.default_rng(seed) | |
| N = ku - d + 1 | |
| out = np.empty(trials) | |
| for t in range(trials): | |
| perm = rng.permutation(ku) | |
| A = np.sort(perm[:N]) | |
| far_counts = np.zeros(ku, dtype=np.int8) | |
| for _ in range(3): | |
| obs = np.unique(A[rng.integers(0, N, size=n)]) | |
| U = np.setdiff1d(np.arange(ku), obs) | |
| pick = rng.choice(U, size=d - 1, replace=False) | |
| far_counts[pick] += 1 | |
| out[t] = (far_counts[A] >= 2).sum() / N | |
| return float(out.mean()), float(out.std(ddof=1) / math.sqrt(trials)) | |
| comp = [] | |
| for d in (8, 16, 32): | |
| for eps in (0.005, 0.002, 0.001): | |
| n = int(np.floor((d / (32 * eps)) * math.log(1.0 / (64 * math.e * eps)))) | |
| ku = [g for g in grid if g["d_gamma"] == d and g["eps"] == eps] | |
| ku = ku[0]["best_ku"] if ku else int(round(0.5 * d / eps)) | |
| pl = proper_loss_analytic(d, ku, n) | |
| m3, se3 = median3_loss_mc(d, ku, n, 400, SEED + 41) | |
| comp.append( | |
| { | |
| "d_gamma": d, | |
| "eps": eps, | |
| "n": n, | |
| "ku": ku, | |
| "proper_E_loss": pl, | |
| "median3_E_loss": m3, | |
| "median3_stderr": se3, | |
| "ratio_proper_over_median3": pl / max(m3, 1e-12), | |
| } | |
| ) | |
| print( | |
| f" d={d:3d} eps={eps:<7} n={n:8d} ku={ku:8d}: proper={pl:.6f} " | |
| f"median-of-3={m3:.6f} +/- {2*se3:.6f} ratio={pl/max(m3,1e-12):8.1f}x" | |
| ) | |
| res["proper_vs_median3"] = comp | |
| res["median3_beats_proper_everywhere"] = bool( | |
| all(c["median3_E_loss"] < c["proper_E_loss"] for c in comp) | |
| ) | |
| slopes = [res[f"linear_fit_d{d}"]["slope_vs_ln_1_over_eps"] for d in (8, 32, 128)] | |
| ok = ( | |
| res["all_dims_match"] | |
| and n_hold == len(grid) | |
| and all(s > 0.01 for s in slopes) | |
| and all(res[f"linear_fit_d{d}"]["r2"] > 0.98 for d in (8, 32, 128)) | |
| and res["median3_beats_proper_everywhere"] | |
| ) | |
| res["verdict"] = "verified" if ok else "partial" | |
| print(f"\nverdict = {res['verdict']}") | |
| dump_json(os.path.join(OUT, "claim6_thm312.json"), res) | |
Xet Storage Details
- Size:
- 14 kB
- Xet hash:
- 5271edbb3fd4153eca0778074ca703469ed7cbd9c6b8770fa317c43abbd006b3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.