| """Claim 4 -- Corollary 3.7 of arXiv:2601.20180. | |
| "Corollary 3.7 establishes an unconditional information-theoretic lower bound: any | |
| algorithm requires 2^Omega(d) expected-risk-minimization (ERM) queries to find an | |
| eps-performatively stable point even when rho <= 1 + O_eps(eps), and even for | |
| constant eps." | |
| The corollary is Theorem 3.6 (Hirsch, Papadimitriou & Vavasis 1989) pushed through | |
| the reduction of Proposition 3.2. Executable content: | |
| A. Proposition 3.2's reduction g(x) = (1-lambda)x + lambda T(x), lambda = eps/eps': | |
| the fixed-point-gap identity, the Lipschitz/rho bound, and -- the crux of the | |
| corollary -- that one ERM query reveals T(x) exactly, so the query lower bound | |
| transfers one-for-one. | |
| B. the arithmetic of Theorem 3.6: c((1/eps - 10)L)^{d-2} is 2^{Omega(d)} for every | |
| constant eps <= 1/12, so "even when eps is a constant" is justified; and the | |
| regime in which the base exceeds 1 at all. | |
| C. TOY: a hidden-path Brouwer family in d dimensions (the HPV mechanism at reduced | |
| scale) where the query cost of every strategy we implement grows like m^{d/2}; | |
| the exponent is fitted. This is our own construction, not the HPV instance. | |
| """ | |
| import numpy as np | |
| import sympy as sp | |
| from common import FIGS, dump | |
| SEED = 20260725 | |
| rng = np.random.default_rng(SEED) | |
| res = {"seed": SEED, "claim": "Corollary 3.7 (2^Omega(d) ERM queries)"} | |
| EPS_PRIME = 0.088 / 6 | |
| # ===================================================================== | |
| # A. Proposition 3.2 | |
| # ===================================================================== | |
| def make_T(d, rng, L=1.0): | |
| """A random L-Lipschitz self-map of the unit ball.""" | |
| A = rng.normal(size=(d, d)) | |
| A = L * A / np.linalg.norm(A, 2) | |
| c = rng.uniform(-0.2, 0.2, size=d) | |
| def T(x): | |
| y = A @ np.tanh(x) + c # tanh is 1-Lipschitz, so Lip(T) <= L | |
| n = np.linalg.norm(y) | |
| return y if n <= 1 else y / n | |
| return T, A | |
| # In Proposition 3.2, eps' is the target fixed-point accuracy for T and eps the | |
| # performative accuracy, with lambda = eps/eps'. Corollary 3.7 instantiates eps' | |
| # at a constant (Theorem 3.6 needs eps' <= 1/12 for a 2^(d-2) bound), so lambda is | |
| # the free knob and lambda <= 1 is the meaningful regime. | |
| EPS_PRIME_TARGET = 1.0 / 12 | |
| checks = [] | |
| for d in [2, 5, 20]: | |
| for lam in [1.0, 0.5, 0.1, 0.01]: | |
| eps = lam * EPS_PRIME_TARGET | |
| T, A = make_T(d, rng, L=1.0) | |
| def g(x, T=T, lam=lam): | |
| return (1 - lam) * x + lam * T(x) | |
| gap_id, lip, recov = [], 0.0, [] | |
| for _ in range(400): | |
| x = rng.uniform(-1, 1, size=d) | |
| x = x / max(1.0, np.linalg.norm(x)) | |
| y = rng.uniform(-1, 1, size=d) | |
| y = y / max(1.0, np.linalg.norm(y)) | |
| # (i) fixed-point-gap identity ||x - G(x)|| = lambda ||x - T(x)|| | |
| gap_id.append( | |
| abs(np.linalg.norm(x - g(x)) - lam * np.linalg.norm(x - T(x))) | |
| ) | |
| # (ii) empirical Lipschitz constant of g (= rho, since alpha = beta = 1) | |
| if np.linalg.norm(x - y) > 1e-9: | |
| lip = max( | |
| lip, float(np.linalg.norm(g(x) - g(y)) / np.linalg.norm(x - y)) | |
| ) | |
| # (iii) one ERM query G(x) reveals T(x) exactly | |
| recov.append(float(np.abs((g(x) - (1 - lam) * x) / lam - T(x)).max())) | |
| checks.append( | |
| { | |
| "d": d, | |
| "eps": eps, | |
| "lambda": lam, | |
| "max_fixed_point_gap_identity_error": float(max(gap_id)), | |
| "empirical_rho": lip, | |
| "rho_bound_1_plus_lambda_L": 1 + lam * 1.0, | |
| "rho_within_bound": bool(lip <= 1 + lam + 1e-9), | |
| "max_T_recovery_error_from_one_ERM_query": float(max(recov)), | |
| } | |
| ) | |
| res["proposition_3_2"] = checks | |
| print( | |
| "A. Prop 3.2: gap identity max error %.2e | rho <= 1+lambda*L in %d/%d | " | |
| "T(x) recovered from a single ERM query to %.2e" | |
| % ( | |
| max(c["max_fixed_point_gap_identity_error"] for c in checks), | |
| sum(c["rho_within_bound"] for c in checks), | |
| len(checks), | |
| max(c["max_T_recovery_error_from_one_ERM_query"] for c in checks), | |
| ) | |
| ) | |
| res["query_equivalence"] = { | |
| "statement": "G(x) = (1-lambda)x + lambda T(x) with lambda and x known, so " | |
| "T(x) = (G(x) - (1-lambda)x)/lambda: the ERM oracle and the " | |
| "T-oracle are information-theoretically equivalent, and the " | |
| "Hirsch et al. bound transfers with no loss.", | |
| "max_recovery_error": max( | |
| c["max_T_recovery_error_from_one_ERM_query"] for c in checks | |
| ), | |
| } | |
| # ===================================================================== | |
| # B. the arithmetic of Theorem 3.6 -> 2^Omega(d) | |
| # ===================================================================== | |
| epsm, Lm, dm = sp.symbols("epsilon L d", positive=True) | |
| base = (1 / epsm - 10) * Lm | |
| rows = [] | |
| for e_val in [ | |
| sp.Rational(1, 11), | |
| sp.Rational(1, 12), | |
| sp.Rational(1, 20), | |
| sp.Rational(1, 100), | |
| sp.Rational(1, 10), | |
| ]: | |
| b = sp.simplify(base.subs({epsm: e_val, Lm: 1})) | |
| rows.append( | |
| { | |
| "eps": str(e_val), | |
| "base_at_L_1": str(b), | |
| "base_float": float(b), | |
| "base_gt_1_nontrivial": bool(b > 1), | |
| "base_ge_2_gives_2_to_the_d": bool(b >= 2), | |
| } | |
| ) | |
| res["theorem_3_6_arithmetic"] = { | |
| "bound": "c * ((1/eps - 10) L)^(d-2)", | |
| "rows": rows, | |
| "eps_threshold_for_base_2": str(sp.solve(sp.Eq(1 / epsm - 10, 2), epsm)), | |
| "eps_threshold_for_base_1": str(sp.solve(sp.Eq(1 / epsm - 10, 1), epsm)), | |
| "conclusion": "for any constant eps <= 1/12 the bound is at least c*2^(d-2) = " | |
| "2^Omega(d); for eps > 1/11 the base drops below 1 and the bound " | |
| "is vacuous, so 'even when eps is a constant' means a constant " | |
| "below 1/11.", | |
| } | |
| print( | |
| "B. Theorem 3.6 base (1/eps - 10)L at L=1: " | |
| + ", ".join("eps=%s -> %.3f" % (r["eps"], r["base_float"]) for r in rows) | |
| ) | |
| print("B. base >= 2 (i.e. 2^Omega(d)) iff eps <= 1/12; base > 1 iff eps < 1/11") | |
| # combined with Prop 3.2: rho <= 1 + lambda L = 1 + eps/eps' L | |
| res["combined_regime"] = { | |
| "rho_bound": "1 + (eps/eps') L", | |
| "eps_prime": EPS_PRIME, | |
| "rho_at_eps_1_over_12_L_1": 1 + (1 / 12) / EPS_PRIME, | |
| "note": "Corollary 3.7 as stated says rho <= 1 + O_eps(eps); the reduction " | |
| "gives rho <= 1 + eps/eps' * L, which is O(eps) for fixed eps' and L.", | |
| } | |
| # ===================================================================== | |
| # C. TOY: hidden-path Brouwer family, ERM-query cost vs dimension | |
| # ===================================================================== | |
| def make_hidden_path(d, m, length, rng): | |
| """A self-avoiding walk on the grid {0..m-1}^d; only its end is a fixed point. | |
| Off the path the direction field is the default +e1, so an off-path query is | |
| consistent with every instance whose path misses that cell: the query reveals | |
| only "not here". This is the mechanism of Hirsch et al. (1989) at toy scale. | |
| """ | |
| cell = np.zeros(d, dtype=int) | |
| path = [tuple(cell)] | |
| seen = {tuple(cell)} | |
| for _ in range(length - 1): | |
| opts = [] | |
| for i in range(d): | |
| for s in (-1, 1): | |
| nxt = cell.copy() | |
| nxt[i] += s | |
| if 0 <= nxt[i] < m and tuple(nxt) not in seen: | |
| opts.append(nxt) | |
| if not opts: | |
| break | |
| cell = opts[rng.integers(len(opts))] | |
| path.append(tuple(cell)) | |
| seen.add(tuple(cell)) | |
| return path, set(path) | |
| def solve_by_probing(path, pathset, d, m, rng, max_queries=10_000_000): | |
| """Query random cells until one lies on the path, then follow it to the end. | |
| Following the path is free of search: each on-path query reveals the successor. | |
| """ | |
| end = path[-1] | |
| q = 0 | |
| while q < max_queries: | |
| c = tuple(int(rng.integers(m)) for _ in range(d)) | |
| q += 1 | |
| if c in pathset: | |
| i = path.index(c) | |
| q += len(path) - 1 - i # walk to the end, one ERM query per step | |
| return q, True | |
| if c == end: | |
| return q, True | |
| return q, False | |
| toy = [] | |
| m = 3 | |
| for d in range(2, 10): | |
| N = m**d | |
| length = max(2, int(round(np.sqrt(N)))) | |
| qs = [] | |
| for trial in range(40): | |
| path, pathset = make_hidden_path(d, m, length, rng) | |
| q, ok = solve_by_probing(path, pathset, d, m, rng) | |
| qs.append(q) | |
| toy.append( | |
| { | |
| "d": d, | |
| "grid_side": m, | |
| "cells": N, | |
| "path_length": length, | |
| "median_queries": float(np.median(qs)), | |
| "mean_queries": float(np.mean(qs)), | |
| "trials": len(qs), | |
| } | |
| ) | |
| print( | |
| "C. d=%d cells=%6d path=%4d median ERM queries = %.1f" | |
| % (d, N, length, np.median(qs)) | |
| ) | |
| ds = np.array([t["d"] for t in toy], dtype=float) | |
| lq = np.log(np.array([t["median_queries"] for t in toy])) | |
| slope, intercept = np.polyfit(ds, lq, 1) | |
| r2 = 1 - ((lq - (slope * ds + intercept)) ** 2).sum() / ((lq - lq.mean()) ** 2).sum() | |
| res["toy_hidden_path"] = { | |
| "label": "toy", | |
| "blocker": "the true Hirsch-Papadimitriou-Vavasis instance is a continuous " | |
| "Brouwer map on [0,1]^d whose construction we do not implement; this " | |
| "is a discrete hidden-path family with the same information-hiding " | |
| "mechanism, run at m=3 and d<=9.", | |
| "measurements": toy, | |
| "log_queries_slope_per_dimension": float(slope), | |
| "predicted_slope_half_log_m": float(0.5 * np.log(m)), | |
| "R2": float(r2), | |
| "implied_base": float(np.exp(slope)), | |
| "predicted_base_sqrt_m": float(np.sqrt(m)), | |
| } | |
| print( | |
| "C. fit: log(median queries) = %.4f*d + %.3f (R^2=%.4f) -> queries ~ %.3f^d " | |
| "(predicted sqrt(m) = %.3f, i.e. 2^Omega(d))" | |
| % (slope, intercept, r2, np.exp(slope), np.sqrt(m)) | |
| ) | |
| # the same search driven purely by ERM queries of the performative instance | |
| lam = 0.05 | |
| recover_err = [] | |
| for t in toy[:4]: | |
| d = t["d"] | |
| for _ in range(200): | |
| x = rng.uniform(-1, 1, size=d) | |
| Tx = rng.uniform(-1, 1, size=d) | |
| G = (1 - lam) * x + lam * Tx | |
| recover_err.append(float(np.abs((G - (1 - lam) * x) / lam - Tx).max())) | |
| res["erm_query_drives_the_search"] = { | |
| "max_error_recovering_T_from_ERM": float(max(recover_err)), | |
| "conclusion": "every ERM query of the constructed performative instance yields " | |
| "T(x) exactly, so the query counts above are simultaneously ERM " | |
| "query counts.", | |
| } | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| plt.figure(figsize=(5.4, 4)) | |
| plt.semilogy( | |
| ds, [t["median_queries"] for t in toy], "o-", label="measured (median of 40)" | |
| ) | |
| plt.semilogy( | |
| ds, | |
| np.exp(intercept) * np.exp(slope * ds), | |
| "k--", | |
| label="fit %.2f^d" % np.exp(slope), | |
| ) | |
| plt.xlabel("dimension d") | |
| plt.ylabel("ERM queries to find the fixed point") | |
| plt.title("hidden-path family: queries grow exponentially in d") | |
| plt.legend(fontsize=8) | |
| plt.grid(alpha=0.3, which="both") | |
| plt.tight_layout() | |
| p = FIGS + "/claim4_query_lb.png" | |
| plt.savefig(p, dpi=130) | |
| print("wrote", p) | |
| res["verdict"] = { | |
| "reduction_and_query_equivalence_verified": bool( | |
| max(c["max_T_recovery_error_from_one_ERM_query"] for c in checks) < 1e-9 | |
| ), | |
| "exponential_arithmetic_verified": True, | |
| "empirical_family": "toy", | |
| } | |
| dump("claim4_erm_query_lb.json", res) | |
Xet Storage Details
- Size:
- 11.4 kB
- Xet hash:
- 406cc8a9a8bee1e58a737e9f8368f327a1d6d0f1c30bacd54048698d70db7ae2
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.