ProCreations's picture
Publish validated ICML reproduction
515b676 verified
Raw
History Blame Contribute Delete
12.4 kB
"""Targeted sweeps and negative controls.
ksweep - paper Fig. 2: error vs k at fixed n (predicted sqrt(k) growth)
sigma - sigma-dependence of the over-specification term (Theorem 3.2)
rho - rho-dependence of the under-specification floor (Theorem 3.2, k<0)
ktrue - |k|-dependence of the floor vs the EXACT Lemma 2.1 bound
rankctrl - negative control: same d, but the discarded direction carries no signal
sparse - sparse binary RDPG, rho_n = n^{-gamma} (Conjecture 1 / Sec. 4.2)
hubctrl - negative control for Conjecture 1: variance condition c <= E|E|^2 broken
"""
import json
import sys
import numpy as np
import rdpg
R = 5
def _ase_err(A, Xt, dims, r=R):
s, U = rdpg.full_spectrum(A)
out = {}
for d in dims:
Xh = rdpg.ase_from_spectrum(s, U, d)
e, _ = rdpg.err_2inf(Xh, Xt)
out[d] = {"err": e}
if d > r:
out[d]["trail"] = rdpg.two_inf(Xh[:, r:d])
out[d]["U_trail_2inf"] = rdpg.two_inf(U[:, r:d])
out[d]["s_hat_d"] = float(abs(s[d - 1]))
return out, s, U
def ksweep(rng, reps=8):
"""Paper Fig. 2: exponential noise, error as a function of embedding dim."""
dims = [3, 4, 5, 6, 7, 8, 10, 14, 20, 30, 40, 60, 80]
res = {}
for n in [1000, 2000, 4000]:
acc = {d: [] for d in dims}
tacc = {d: [] for d in dims if d > R}
for _ in range(reps):
A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="exponential", scale=0.1)
o, _, _ = _ase_err(A, Xt, dims)
for d in dims:
acc[d].append(o[d]["err"])
if d > R:
tacc[d].append(o[d]["trail"])
res[n] = {"dims": dims,
"err_mean": [float(np.mean(acc[d])) for d in dims],
"err_se": [float(np.std(acc[d], ddof=1) / np.sqrt(reps)) for d in dims],
"trail_mean": {str(d): float(np.mean(tacc[d])) for d in dims if d > R}}
ks = [d - R for d in dims if d > R]
res[n]["k_slope_err"] = rdpg.loglog_slope(
ks, [np.mean(acc[d]) for d in dims if d > R])
res[n]["k_slope_trail"] = rdpg.loglog_slope(
ks, [np.mean(tacc[d]) for d in dims if d > R])
res[n]["argmin_dim"] = int(dims[int(np.argmin([np.mean(acc[d]) for d in dims]))])
return res
def sigma(rng, reps=8):
"""Theorem 3.2 writes the over-specification term as sqrt(sigma^2 k)(...)/n^{1/4},
i.e. sigma^1. Lemma 2.1 builds that term as ||Uhat_{r+1:r+k}||_{2,inf}*||E||^{1/2},
and ||E|| ~ 2 sigma sqrt(n), which predicts sigma^{1/2}. We measure the exponent."""
n, d = 2000, 10
sigmas = [0.025, 0.05, 0.1, 0.2, 0.4, 0.8]
T, EV, SV, EX = [], [], [], []
for sg in sigmas:
t, ev, sv, ex = [], [], [], []
for _ in range(reps):
A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=sg)
o, s, U = _ase_err(A, Xt, [R, d])
t.append(o[d]["trail"])
ev.append(o[d]["U_trail_2inf"])
sv.append(np.sqrt(o[d]["s_hat_d"]))
ex.append(o[d]["err"] - o[R]["err"])
T.append(float(np.mean(t))); EV.append(float(np.mean(ev)))
SV.append(float(np.mean(sv))); EX.append(float(np.mean(ex)))
return {"n": n, "d": d, "k": d - R, "sigmas": sigmas,
"trail_mean": T, "U_trail_2inf_mean": EV, "sqrt_s_hat_mean": SV,
"excess_err_mean": EX,
"exp_trail": rdpg.loglog_slope(sigmas, T),
"exp_U": rdpg.loglog_slope(sigmas, EV),
"exp_sqrt_s": rdpg.loglog_slope(sigmas, SV),
"exp_excess": rdpg.loglog_slope(sigmas, EX),
"bbp_ratio": [float(n / 30 / (sg * np.sqrt(n))) for sg in sigmas]}
def rho(rng, reps=8):
"""Theorem 3.2 (k<0): floor >~ sqrt(|k| rho_n). Sweep rho at fixed n, |k|=2."""
n, d = 4000, 3
rhos = [1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125]
E, B = [], []
for rh in rhos:
e, b = [], []
for _ in range(reps):
X = rdpg.dirichlet_latent(n, R, rng)
A, Xt = rdpg.weighted_rdpg(n, R, rng, rho=rh, kind="normal",
scale=0.02, X=X)
o, _, _ = _ase_err(A, Xt, [d])
s_nz = np.sort(np.linalg.eigvalsh(rh * (X.T @ X)))[::-1]
e.append(o[d]["err"])
b.append(float(np.sqrt(s_nz[d:].sum() / n)))
E.append(float(np.mean(e))); B.append(float(np.mean(b)))
return {"n": n, "d": d, "k": d - R, "rhos": rhos, "floor_mean": E,
"lemma21_bound_mean": B,
"ratio": [float(a / b) for a, b in zip(E, B)],
"sqrt_k_rho": [float(np.sqrt(2 * rh)) for rh in rhos],
"exp_floor": rdpg.loglog_slope(rhos, E),
"exp_bound": rdpg.loglog_slope(rhos, B)}
def ktrue(rng, reps=8):
"""|k|-dependence of the under-specification floor vs the exact Lemma 2.1 bound."""
n = 4000
dims = [1, 2, 3, 4]
E = {d: [] for d in dims}
B = {d: [] for d in dims}
for _ in range(reps):
X = rdpg.dirichlet_latent(n, R, rng)
A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=0.1, X=X)
o, _, _ = _ase_err(A, Xt, dims)
s_nz = np.sort(np.linalg.eigvalsh(X.T @ X))[::-1]
for d in dims:
E[d].append(o[d]["err"])
B[d].append(float(np.sqrt(s_nz[d:].sum() / n)))
return {"n": n, "dims": dims,
"floor_mean": [float(np.mean(E[d])) for d in dims],
"lemma21_bound_mean": [float(np.mean(B[d])) for d in dims],
"ratio": [float(np.mean(E[d]) / np.mean(B[d])) for d in dims],
"sqrt_k_rho": [float(np.sqrt(R - d)) for d in dims]}
def rankctrl(rng, reps=8):
"""NEGATIVE CONTROL for the k<0 lower bound. At the SAME embedding
dimension d=4 we compare (i) a true rank-5 P (one signal direction is
discarded -> floor) with (ii) a true rank-4 P (nothing is discarded ->
no floor). If a floor appeared in (ii) as well, the effect would be an
artefact of the dimension count rather than of discarded signal."""
dims = [4]
out = {"n": [], "rank5_d4": [], "rank4_d4": [], "rank5_d5": []}
for n in [500, 1000, 2000, 4000]:
a, b, c = [], [], []
for _ in range(reps):
X5 = rdpg.dirichlet_latent(n, 5, rng)
A5, Xt5 = rdpg.weighted_rdpg(n, 5, rng, kind="normal", scale=0.1, X=X5)
o5, _, _ = _ase_err(A5, Xt5, [4, 5])
X4 = rdpg.dirichlet_latent(n, 4, rng)
A4, Xt4 = rdpg.weighted_rdpg(n, 4, rng, kind="normal", scale=0.1, X=X4)
o4, _, _ = _ase_err(A4, Xt4, [4], r=4)
a.append(o5[4]["err"]); b.append(o4[4]["err"]); c.append(o5[5]["err"])
out["n"].append(n)
out["rank5_d4"].append(float(np.mean(a)))
out["rank4_d4"].append(float(np.mean(b)))
out["rank5_d5"].append(float(np.mean(c)))
for key in ["rank5_d4", "rank4_d4", "rank5_d5"]:
out["slope_" + key] = rdpg.loglog_slope(out["n"], out[key])
return out
def sparse(rng, reps=6):
"""Sparse binary RDPG, rho_n = n^{-gamma} (paper Eq. 18). The k<0 floor is
predicted to be sqrt(|k| rho_n) ~ n^{-gamma/2}: a *decaying* floor whose
slope is set by gamma. This is a sharp, falsifiable prediction."""
dims = [3, 4, 5, 6, 7, 10, 20]
ngrid = [500, 1000, 2000, 4000, 8000]
res = {}
for gam in [0.0, 0.25, 0.5]:
per = {d: [] for d in dims}
bnd, dl = [], []
for n in ngrid:
acc = {d: [] for d in dims}
bb, dd = [], []
for _ in range(reps):
X = rdpg.dirichlet_latent(n, R, rng)
rh = n ** (-gam)
A, Xt = rdpg.binary_rdpg(n, R, rng, rho=rh, X=X)
o, s, U = _ase_err(A, Xt, dims)
for d in dims:
acc[d].append(o[d]["err"])
s_nz = np.sort(np.linalg.eigvalsh(rh * (X.T @ X)))[::-1]
bb.append(float(np.sqrt(s_nz[3:].sum() / n)))
dd.append(float(np.abs(U[:, R]).max()))
for d in dims:
per[d].append(float(np.mean(acc[d])))
bnd.append(float(np.mean(bb)))
dl.append(float(np.mean(dd)))
res[str(gam)] = {"ngrid": ngrid,
"err": {str(d): per[d] for d in dims},
"lemma21_bound_d3": bnd,
"deloc_rp1": dl,
"slopes": {str(d): rdpg.loglog_slope(ngrid, per[d]) for d in dims},
"slope_bound_d3": rdpg.loglog_slope(ngrid, bnd),
"slope_deloc": rdpg.loglog_slope(ngrid, dl),
"predicted_floor_slope": -gam / 2}
return res
def hubctrl(rng, reps=6):
"""NEGATIVE CONTROL for Conjecture 1. The conjecture relaxes Assumption A7
to c <= E|E_ij|^2 <= C, i.e. entry variances bounded AWAY FROM ZERO. A
bounded-degree binary graph (rho_n = c/n) violates the lower bound c, and
there eigenvector localisation is expected. We compare the delocalisation
statistic in the dense (conjecture-compliant) and bounded-degree regimes."""
ngrid = [1000, 2000, 4000]
out = {"ngrid": ngrid, "dense": [], "bounded_degree": [],
"dense_ipr": [], "bounded_degree_ipr": []}
for n in ngrid:
a, b, ai, bi = [], [], [], []
for _ in range(reps):
A, _ = rdpg.binary_rdpg(n, R, rng, rho=1.0)
s, U = rdpg.full_spectrum(A)
a.append(float(np.abs(U[:, R]).max())); ai.append(float((U[:, R] ** 4).sum()))
A, _ = rdpg.binary_rdpg(n, R, rng, rho=12.0 / n)
s, U = rdpg.full_spectrum(A)
b.append(float(np.abs(U[:, R]).max())); bi.append(float((U[:, R] ** 4).sum()))
out["dense"].append(float(np.mean(a)))
out["bounded_degree"].append(float(np.mean(b)))
out["dense_ipr"].append(float(np.mean(ai)))
out["bounded_degree_ipr"].append(float(np.mean(bi)))
out["slope_dense"] = rdpg.loglog_slope(ngrid, out["dense"])
out["slope_bounded_degree"] = rdpg.loglog_slope(ngrid, out["bounded_degree"])
out["slope_dense_ipr"] = rdpg.loglog_slope(ngrid, out["dense_ipr"])
out["slope_bd_ipr"] = rdpg.loglog_slope(ngrid, out["bounded_degree_ipr"])
return out
def decompctrl(rng, reps=8):
"""NEGATIVE CONTROL for the two-term decomposition of Lemma 2.1. We rebuild
the d-dimensional embedding with its trailing columns SET TO ZERO,
[Xhat_{1:r} | 0], and re-solve the same O_d Procrustes problem. If the
n^{-1/4} degradation really comes from the trailing block, this ablated
embedding must fall back exactly onto the n^{-1/2} base curve."""
dims = [10, 20]
out = {"ngrid": [], "base": [], "full": {str(d): [] for d in dims},
"ablated": {str(d): [] for d in dims}}
for n in [500, 1000, 2000, 4000]:
b, f, a = [], {d: [] for d in dims}, {d: [] for d in dims}
for _ in range(reps):
A, Xt = rdpg.weighted_rdpg(n, R, rng, kind="normal", scale=0.1)
s, U = rdpg.full_spectrum(A)
Xr = rdpg.ase_from_spectrum(s, U, R)
b.append(rdpg.err_2inf(Xr, Xt)[0])
for d in dims:
Xh = rdpg.ase_from_spectrum(s, U, d)
f[d].append(rdpg.err_2inf(Xh, Xt)[0])
Xz = Xh.copy()
Xz[:, R:] = 0.0
a[d].append(rdpg.err_2inf(Xz, Xt)[0])
out["ngrid"].append(n)
out["base"].append(float(np.mean(b)))
for d in dims:
out["full"][str(d)].append(float(np.mean(f[d])))
out["ablated"][str(d)].append(float(np.mean(a[d])))
out["slope_base"] = rdpg.loglog_slope(out["ngrid"], out["base"])
out["slope_full"] = {k: rdpg.loglog_slope(out["ngrid"], v) for k, v in out["full"].items()}
out["slope_ablated"] = {k: rdpg.loglog_slope(out["ngrid"], v) for k, v in out["ablated"].items()}
return out
if __name__ == "__main__":
which = sys.argv[1]
rng = np.random.default_rng(hash(which) % (2 ** 31))
fn = {"ksweep": ksweep, "sigma": sigma, "rho": rho, "ktrue": ktrue,
"rankctrl": rankctrl, "sparse": sparse, "hubctrl": hubctrl,
"decompctrl": decompctrl}[which]
res = fn(rng)
with open(f"outputs/extra_{which}.json", "w") as f:
json.dump(res, f, indent=1)
print(which, "done")