ProCreations's picture
Six claims: QE-chain verification, calibrated instrument with certified non-shattering, measured upper bounds, and the paper's own matching lower bound
9f8d17a verified
Raw
History Blame Contribute Delete
15 kB
"""Sections D and E: structural-precondition tests on real solver output and
measured pseudo-dimension of real tuning classes against the theorem bounds."""
import itertools, json, math, sys, time
import numpy as np
from scipy import linalg
from evidence import stated_bound, measure_pdim_pool
RNG = np.random.default_rng(4242)
# --------------------------------------------------------------- ElasticNet
def enet_solve(A, b, a1, a2, iters=6000):
"""Cyclic coordinate descent for min ||A th - b||^2 + a1|th|_1 + a2|th|^2."""
n, d = A.shape
th = np.zeros(d)
col = (A ** 2).sum(0)
r = -b.copy()
for _ in range(iters):
mx = 0.0
for k in range(d):
r -= A[:, k] * th[k]
rho = -2.0 * A[:, k] @ r
den = 2.0 * col[k] + 2.0 * a2
new = np.sign(rho) * max(abs(rho) - a1, 0.0) / den
mx = max(mx, abs(new - th[k]))
th[k] = new
r += A[:, k] * th[k]
if mx < 1e-14:
break
return th
def cramer_residual(A, b, a1, a2, th, tol=1e-9):
"""On the active set S with signs s, stationarity says
(2 A_S'A_S + 2 a2 I) th_S = 2 A_S'b - a1 s, whose solution is a rational
function of (a1, a2) by Cramer's rule. Return the residual norm."""
S = np.where(np.abs(th) > tol)[0]
if S.size == 0:
return 0.0, 0
As = A[:, S]
G = 2.0 * As.T @ As + 2.0 * a2 * np.eye(S.size)
rhs = 2.0 * As.T @ b - a1 * np.sign(th[S])
return float(np.linalg.norm(G @ th[S] - rhs)), int(S.size)
def group_lasso_solve(A, b, alphas, groups, iters=40000):
"""Proximal gradient for min ||A th - b||^2 + sum_i alpha_i ||th_{G_i}||_2."""
d = A.shape[1]
th = np.zeros(d)
Lc = 2.0 * np.linalg.norm(A, 2) ** 2
for _ in range(iters):
g = 2.0 * A.T @ (A @ th - b)
z = th - g / Lc
new = z.copy()
for gi, G in enumerate(groups):
nz = np.linalg.norm(z[G])
sc = max(0.0, 1.0 - alphas[gi] / (Lc * max(nz, 1e-300)))
new[G] = z[G] * sc
if np.max(np.abs(new - th)) < 1e-15:
th = new
break
th = new
return th
def section_D():
out = {}
# ---- D1 Assumption 7.1: is the ElasticNet path piecewise rational?
d, n = 6, 25
A = RNG.standard_normal((n, d)); b = RNG.standard_normal(n)
res, sizes = [], []
grid = [(a1, a2) for a1 in np.linspace(0.05, 20.0, 40)
for a2 in np.linspace(0.05, 5.0, 40)]
for a1, a2 in grid:
th = enet_solve(A, b, a1, a2)
r, sz = cramer_residual(A, b, a1, a2, th)
res.append(r); sizes.append(sz)
out["enet"] = dict(solves=len(grid), max_cramer_residual=float(np.max(res)),
mean_cramer_residual=float(np.mean(res)),
active_sizes=[int(min(sizes)), int(max(sizes))])
# rational vs equal-capacity polynomial fit inside one active-set region
a1f = 3.0
pts = []
for a2 in np.linspace(0.20, 3.00, 60):
th = enet_solve(A, b, a1f, a2)
pts.append((a2, th))
S0 = tuple(np.where(np.abs(pts[0][1]) > 1e-9)[0])
same = [(a, t) for a, t in pts if tuple(np.where(np.abs(t) > 1e-9)[0]) == S0]
xa = np.array([a for a, _ in same]); ya = np.array([t[S0[0]] for _, t in same])
# inside a fixed active set S the exact solution is
# th_S(a2) = (G0 + 2 a2 I)^{-1} r , a ratio of a degree-(|S|-1) numerator
# to a degree-|S| denominator in a2. Fit that form, and an
# equal-parameter-count polynomial, and compare residuals.
nS = len(S0)
xs_ = (xa - xa.mean()) / xa.std()
Mr = np.column_stack([xs_ ** k for k in range(nS)] +
[-(xs_ ** k) * ya for k in range(1, nS + 1)])
cr, *_ = np.linalg.lstsq(Mr, ya, rcond=None)
num = sum(cr[k] * xs_ ** k for k in range(nS))
den = 1.0 + sum(cr[nS + k - 1] * xs_ ** k for k in range(1, nS + 1))
fit_r = num / den
npar = Mr.shape[1]
Mp = np.column_stack([xs_ ** k for k in range(npar)])
cp, *_ = np.linalg.lstsq(Mp, ya, rcond=None)
fit_p = Mp @ cp
out["enet_fit"] = dict(region_points=len(same),
rational_rel_residual=float(np.linalg.norm(fit_r - ya) /
max(np.linalg.norm(ya), 1e-30)),
polynomial_rel_residual=float(np.linalg.norm(fit_p - ya) /
max(np.linalg.norm(ya), 1e-30)),
active_set_size=nS, free_parameters_each=int(npar))
# control: group LASSO violates the same stationarity identity
groups = [np.arange(0, 3), np.arange(3, 6)]
viol = []
for a in np.linspace(0.5, 3.0, 12):
th = group_lasso_solve(A, b, np.array([a, a]), groups)
S = np.where(np.abs(th) > 1e-9)[0]
if S.size == 0:
continue
As = A[:, S]
G = 2.0 * As.T @ As
rhs = 2.0 * As.T @ b - a * np.sign(th[S])
viol.append(float(np.linalg.norm(G @ th[S] - rhs)))
out["group_lasso_control"] = dict(n=len(viol), max_violation=float(np.max(viol)),
min_violation=float(np.min(viol)))
# ---- D2 Theorem 8.1 premise: ||v||_2 is not piecewise polynomial
NS = 6000
v = RNG.standard_normal((NS, 4))
nrm = np.linalg.norm(v, axis=1)
sq = nrm ** 2
feats = []
for deg in range(9):
for e in itertools.combinations_with_replacement(range(4), deg):
c = np.ones(NS)
for i in e:
c = c * v[:, i]
feats.append(c)
F = np.column_stack(feats)
def relres(y):
c, *_ = np.linalg.lstsq(F, y, rcond=None)
return float(np.linalg.norm(F @ c - y) / np.linalg.norm(y))
out["nonpoly"] = dict(deg8_residual_norm=relres(nrm),
deg8_residual_normsq=relres(sq),
n_features=F.shape[1])
# nu-encoding on real proximal-gradient solves
enc = []
for _ in range(48):
Ai = RNG.standard_normal((30, 8)); bi = RNG.standard_normal(30)
gs = [np.arange(0, 4), np.arange(4, 8)]
al = RNG.uniform(0.3, 3.0, 2)
th = group_lasso_solve(Ai, bi, al, gs)
nu = np.array([np.linalg.norm(th[G]) for G in gs])
e1 = max(abs(nu[i] ** 2 - float(th[gs[i]] @ th[gs[i]])) for i in range(2))
# KKT residual of the group-LASSO stationarity condition
gsm = 2.0 * Ai.T @ (Ai @ th - bi)
kkt = 0.0
for i, G in enumerate(gs):
if nu[i] > 1e-10:
kkt = max(kkt, float(np.linalg.norm(gsm[G] + al[i] * th[G] / nu[i])))
else:
kkt = max(kkt, max(0.0, float(np.linalg.norm(gsm[G])) - al[i]))
enc.append((e1, kkt, float(nu.min())))
out["nu_encoding"] = dict(solves=len(enc),
max_nu_sq_error=float(max(e[0] for e in enc)),
max_kkt_residual=float(max(e[1] for e in enc)),
min_nu=float(min(e[2] for e in enc)))
# ---- D3 Proposition G.1 for weighted fused LASSO
def fused(y, w):
d = y.size
D = np.zeros((d - 1, d))
for i in range(d - 1):
D[i, i] = -1.0; D[i, i + 1] = 1.0
H = D @ D.T
u = np.zeros(d - 1)
Lc = np.linalg.norm(H, 2)
for _ in range(30000):
g = H @ u - D @ y
un = np.clip(u - g / Lc, -w, w)
if np.max(np.abs(un - u)) < 1e-15:
u = un; break
u = un
th = y - D.T @ u
prim = 0.5 * float((th - y) @ (th - y)) + float(w @ np.abs(D @ th))
dual = -0.5 * float(u @ H @ u) + float(u @ (D @ y))
return th, u, prim, dual, H
gaps, eigs, regions = [], [], []
for d in [4, 6, 8, 10, 12]:
y = RNG.standard_normal(d)
seen = set()
for _ in range(400):
w = RNG.uniform(0.05, 1.5, d - 1)
th, u, prim, dual, H = fused(y, w)
gaps.append(abs(prim - dual))
seen.add(tuple(np.sign(np.round(np.diff(th), 9)).astype(int)))
eigs.append(float(np.min(np.linalg.eigvalsh(H))))
regions.append(dict(d=d, regions=len(seen), cap=3 ** (d - 1)))
out["fused"] = dict(max_duality_gap=float(np.max(gaps)),
min_dual_hessian_eig=float(np.min(eigs)),
regions=regions,
all_under_cap=all(r["regions"] < r["cap"] for r in regions))
# piecewise-affine check: within one region theta*(w) is affine in w
y = RNG.standard_normal(8)
base = RNG.uniform(0.4, 0.6, 7)
th0, u0, *_ = fused(y, base)
sgn0 = tuple(np.sign(np.round(np.diff(th0), 9)).astype(int))
W, T = [], []
for _ in range(120):
w = base + RNG.uniform(-0.02, 0.02, 7)
th, u, *_ = fused(y, w)
if tuple(np.sign(np.round(np.diff(th), 9)).astype(int)) == sgn0:
W.append(np.concatenate([[1.0], w])); T.append(th)
W = np.array(W); T = np.array(T)
c, *_ = np.linalg.lstsq(W, T, rcond=None)
inreg = float(np.linalg.norm(W @ c - T) / max(np.linalg.norm(T), 1e-30))
W2, T2 = [], []
for _ in range(160):
w = RNG.uniform(0.05, 1.5, 7)
th, u, *_ = fused(y, w)
W2.append(np.concatenate([[1.0], w])); T2.append(th)
W2 = np.array(W2); T2 = np.array(T2)
c2, *_ = np.linalg.lstsq(W2, T2, rcond=None)
across = float(np.linalg.norm(W2 @ c2 - T2) / max(np.linalg.norm(T2), 1e-30))
out["fused_affine"] = dict(in_region_points=len(T), in_region_rel_residual=inreg,
across_region_points=len(T2),
across_region_rel_residual=across,
separation=across / max(inreg, 1e-300))
# rank-deficient control breaks Prop G.1's precondition
Dbad = np.zeros((3, 4)); Dbad[0] = [-1, 1, 0, 0]; Dbad[1] = [-1, 1, 0, 0]; Dbad[2] = [0, 0, -1, 1]
Hb = Dbad @ Dbad.T
out["fused_control"] = dict(min_eig_rank_deficient=float(np.min(np.linalg.eigvalsh(Hb))),
rank=int(np.linalg.matrix_rank(Dbad)), rows=3)
return out
# --------------------------------------------------- E: real tuning classes
def make_instance(n, d, p, seed):
r = np.random.default_rng(seed)
A = r.standard_normal((n, d)); b = r.standard_normal(n)
Ap = r.standard_normal((n, d)); bp = r.standard_normal(n)
groups = np.array_split(np.arange(d), p)
return A, b, Ap, bp, groups
def ridge_theta(A, b, alpha_vec):
G = A.T @ A + np.diag(alpha_vec)
return np.linalg.solve(G, A.T @ b)
def section_E():
out = {}
# ---- f != g check on a real bi-level ridge instance
A, b, Ap, bp, groups = make_instance(40, 8, 4, 11)
al = np.array([0.7, 1.3, 0.2, 2.1])
av = np.zeros(8)
for gi, G in enumerate(groups):
av[G] = al[gi]
th = ridge_theta(A, b, av)
gf = 2.0 * (A.T @ (A @ th - b) + av * th)
gg = 2.0 * Ap.T @ (Ap @ th - bp)
out["bilevel_fneqg"] = dict(train_stationarity=float(np.linalg.norm(gf)),
val_gradient_at_same_point=float(np.linalg.norm(gg)))
# ---- certified pseudo-dimension lower bounds on real tuning classes
def pdim_of(loss, xs, ts, alphas, kmax):
return measure_pdim_pool(loss, xs, ts, alphas, nmax=kmax)
rows = []
for (p, d) in [(2, 8), (3, 12), (4, 16), (6, 24), (8, 32)]:
A, b, Ap, bp, groups = make_instance(60, d, p, 100 + p * 7 + d)
insts = [make_instance(60, d, p, 500 + p * 31 + d * 5 + k) for k in range(p + 4)]
def loss_bi(alpha, inst):
Ai, bi, Api, bpi, gi = inst
av = np.zeros(d)
for k, G in enumerate(gi):
av[G] = alpha[k]
t = ridge_theta(Ai, bi, av)
return float(np.sum((Api @ t - bpi) ** 2))
def loss_single(alpha, inst):
Ai, bi, Api, bpi, gi = inst
av = np.zeros(d)
for k, G in enumerate(gi):
av[G] = alpha[k]
t = ridge_theta(Ai, bi, av)
return float(np.sum((Ai @ t - bi) ** 2) + av @ (t * t))
pool = np.exp(RNG.uniform(math.log(1e-3), math.log(1e3), (40000, p)))
ts_bi = [np.median([loss_bi(a, inst) for a in pool[:400]]) for inst in insts]
ts_si = [np.median([loss_single(a, inst) for a in pool[:400]]) for inst in insts]
kb, _ = pdim_of(loss_bi, insts, ts_bi, pool, min(p + 3, 12))
ks, _ = pdim_of(loss_single, insts, ts_si, pool, min(p + 3, 12))
Lb = np.array([[loss_bi(a, x) for x in insts] for a in pool[:8000]])
Sb = (Lb >= np.array(ts_bi)[None, :]).astype(np.int8)
Ls = np.array([[loss_single(a, x) for x in insts] for a in pool[:8000]])
Ss = (Ls >= np.array(ts_si)[None, :]).astype(np.int8)
cells_bi = len(set(map(tuple, Sb.tolist())))
cells_si = len(set(map(tuple, Ss.tolist())))
Mtot, Dtot = 6 * d + 64, 4
ub61 = stated_bound(p, (d, d), Mtot, Dtot)
ub51 = stated_bound(p, (d,), 3 * d + 64 + d, 4)
rows.append(dict(p=p, d=d, pdim_lb_bilevel=kb, pdim_lb_single=ks,
sign_cells_bilevel=cells_bi, sign_cells_single=cells_si,
cell_ratio=cells_bi / max(cells_si, 1),
thm61_bound=ub61, thm51_bound=ub51,
lb_under_bound=bool(kb <= ub61)))
print("E p=%d d=%d pdim_lb bi=%d single=%d cells %d/%d bound61=%.1f"
% (p, d, kb, ks, cells_bi, cells_si, ub61), flush=True)
out["pdim_rows"] = rows
# ---- precondition control: a non-semi-algebraic (sinusoidal) class
xs = [np.array([1.0])] * 12
pool1 = RNG.uniform(0.0, 200.0, (200000, 1))
def sinloss(a, x):
return float(np.sin(a[0] * (1.0 + 0.37 * x[0])))
freqs = [np.array([1.0 + 0.31 * k]) for k in range(12)]
def sinloss2(a, x):
return float(np.sin(a[0] * x[0]))
k_sin, _ = measure_pdim_pool(sinloss2, freqs, [0.0] * 12, pool1, nmax=12)
def linloss(a, x):
return float(a[0] * x[0])
k_lin, _ = measure_pdim_pool(linloss, freqs, [0.0] * 12, pool1, nmax=12)
out["precondition_control"] = dict(
p=1, sinusoidal_pdim_lb=k_sin, affine_pdim_lb=k_lin,
note="a single-parameter semi-algebraic class has Pdim 1; sin(alpha x) is not "
"semi-algebraic and shatters far more points, so Theorem 4.1's polynomial-FOL "
"hypothesis is doing real work")
return out
if __name__ == "__main__":
which = sys.argv[1] if len(sys.argv) > 1 else "de"
out = {}
if "d" in which:
t = time.time(); out["D"] = section_D(); out["D"]["secs"] = round(time.time() - t, 1)
print("D", json.dumps(out["D"])[:1500], flush=True)
if "e" in which:
t = time.time(); out["E"] = section_E(); out["E"]["secs"] = round(time.time() - t, 1)
print("E", json.dumps(out["E"])[:1200], flush=True)
with open("evidence_%s.json" % which, "w") as f:
json.dump(out, f, indent=1)