ProCreations's picture
Publish validated ICML reproduction
515b676 verified
Raw
History Blame Contribute Delete
6.63 kB
"""Correctness gates: re-derive quantities the paper (or classical theory)
already establishes, and assert them before interpreting anything else."""
import json
import numpy as np
import rdpg
OUT = {}
def gate_eig_residual(rng):
"""LAPACK eigendecomposition of A is exact to machine precision."""
n, r = 800, 5
A, X = rdpg.weighted_rdpg(n, r, rng)
s, U = rdpg.full_spectrum(A)
res = float(np.max(np.abs(A - (U * s) @ U.T)))
orth = float(np.max(np.abs(U.T @ U - np.eye(n))))
return {"max_abs_recon_residual": res, "max_abs_orthonormality_residual": orth,
"n": n, "pass": res < 1e-9 and orth < 1e-9}
def gate_dirichlet_moments(rng):
"""For X_i ~ Dir(1,1,1,1,1), M = E[x x^T] = (I + J)/30 exactly, so the
non-zero eigenvalues of P = X X^T obey s_1/n -> 1/5, s_2..s_5/n -> 1/30.
This pins the constant in Assumption A4 (s_j asymp rho_n n) and shows that
the paper's own simulation design has a 4-fold degenerate population
eigenvalue (Assumptions A3/A6 hold only through O(sqrt(n)) fluctuations)."""
r = 5
M_theory = (np.eye(r) + np.ones((r, r))) / 30.0
ev_theory = np.sort(np.linalg.eigvalsh(M_theory))[::-1]
rows = []
for n in [2000, 20000, 200000]:
X = rdpg.dirichlet_latent(n, r, rng)
M_emp = X.T @ X / n
ev_emp = np.sort(np.linalg.eigvalsh(M_emp))[::-1]
rows.append({"n": n,
"max_abs_moment_err": float(np.max(np.abs(M_emp - M_theory))),
"eigs_emp": ev_emp.tolist()})
return {"M_theory_eigs": ev_theory.tolist(), "rows": rows,
"degenerate_multiplicity": 4,
"pass": rows[-1]["max_abs_moment_err"] < 5e-3}
def gate_eckart_young(rng):
"""Lemma 2.1 (k<0 branch) is an Eckart-Young statement. With E = 0 the
d-dimensional ASE IS the rank-d truncated eigendecomposition of P, so
min_W ||Xhat^o W - rho^{1/2}X||_F must EQUAL sqrt(sum_{j>d} s_j) exactly,
and the (2,inf) lower bound of Lemma 2.1 must hold."""
n, r, rho = 1500, 5, 1.0
X = rdpg.dirichlet_latent(n, r, rng)
P = rho * (X @ X.T)
s, U = rdpg.full_spectrum(P)
s_nz = np.sort(np.linalg.eigvalsh(rho * (X.T @ X)))[::-1] # exact non-zero eigs
rows = []
for d in [1, 2, 3, 4]:
Xh = rdpg.ase_from_spectrum(s, U, d)
e2i, D = rdpg.err_2inf(Xh, np.sqrt(rho) * X)
fro = float(np.linalg.norm(D))
fro_theory = float(np.sqrt(s_nz[d:].sum()))
bound = float(np.sqrt(s_nz[d:].sum() / n)) # Lemma 2.1
rows.append({"d": d, "k": d - r, "frob_measured": fro,
"frob_eckart_young": fro_theory,
"rel_frob_residual": abs(fro - fro_theory) / fro_theory,
"err_2inf": e2i, "lemma21_lower_bound": bound,
"ratio_2inf_over_bound": e2i / bound})
return {"n": n, "rows": rows,
"max_rel_frob_residual": max(x["rel_frob_residual"] for x in rows),
"min_ratio": min(x["ratio_2inf_over_bound"] for x in rows),
"pass": max(x["rel_frob_residual"] for x in rows) < 1e-10
and min(x["ratio_2inf_over_bound"] for x in rows) >= 1.0}
def gate_procrustes(rng):
"""W = U V^T from SVD(Xhat^T X) solves min_{W in O} ||Xhat W - X||_F.
Verified by (a) first-order optimality: W^T Xhat^T X is symmetric PSD,
(b) beating 20000 random rotations."""
n, r, d = 400, 5, 7
A, X = rdpg.weighted_rdpg(n, r, rng)
s, U = rdpg.full_spectrum(A)
Xh = rdpg.ase_from_spectrum(s, U, d)
Xp = np.hstack([X, np.zeros((n, d - r))])
Uu, sv, Vt = np.linalg.svd(Xh.T @ Xp)
W = Uu @ Vt
best = float(np.linalg.norm(Xh @ W - Xp))
Msym = W.T @ (Xh.T @ Xp)
sym_err = float(np.max(np.abs(Msym - Msym.T)))
psd_min = float(np.min(np.linalg.eigvalsh((Msym + Msym.T) / 2)))
worst = 0.0
for _ in range(20000):
Q, _ = np.linalg.qr(rng.standard_normal((d, d)))
worst = max(worst, best - float(np.linalg.norm(Xh @ Q - Xp)))
return {"frob_at_procrustes": best, "symmetry_residual": sym_err,
"min_eig_of_WtXhatTX": psd_min,
"best_random_rotation_improvement": worst,
"pass": sym_err < 1e-9 and psd_min > -1e-9 and worst <= 1e-9}
def gate_semicircle_edge(rng):
"""||E|| / (2 sigma sqrt(n)) -> 1 (Wigner edge). The paper bounds the
trailing ASE eigenvalues by ||E|| (Sec. 2.1), so this constant matters."""
rows = []
for n in [500, 1000, 2000, 4000]:
vals = []
for _ in range(3):
E = rdpg.sym_noise(n, rng, "normal", 1.0)
vals.append(float(np.linalg.norm(E, 2)))
rows.append({"n": n, "opnorm": float(np.mean(vals)),
"ratio_to_2sqrtn": float(np.mean(vals)) / (2 * np.sqrt(n))})
return {"rows": rows, "final_ratio": rows[-1]["ratio_to_2sqrtn"],
"pass": abs(rows[-1]["ratio_to_2sqrtn"] - 1.0) < 0.05}
def gate_signal_incoherence(rng):
"""Corollary B.3.1 / Assumption A1: ||U_{1:r}||_{2,inf} <~ 1/sqrt(n).
We report the constant sqrt(n)*||U_{1:r}||_{2,inf}, which must not grow."""
rows = []
for n in [500, 1000, 2000, 4000, 8000]:
X = rdpg.dirichlet_latent(n, 5, rng)
P = X @ X.T
s, U = rdpg.full_spectrum(P)
c = rdpg.two_inf(U[:, :5]) * np.sqrt(n)
rows.append({"n": n, "sqrtn_times_2inf": float(c)})
return {"rows": rows,
"drift": rows[-1]["sqrtn_times_2inf"] / rows[0]["sqrtn_times_2inf"],
"pass": rows[-1]["sqrtn_times_2inf"] < 3 * rows[0]["sqrtn_times_2inf"]}
def jsonable(o):
if isinstance(o, dict):
return {k: jsonable(v) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [jsonable(v) for v in o]
if isinstance(o, (np.bool_, bool)):
return bool(o)
if isinstance(o, (np.integer,)):
return int(o)
if isinstance(o, (np.floating,)):
return float(o)
return o
if __name__ == "__main__":
rng = np.random.default_rng(20260728)
OUT["matmul_sanity"] = rdpg.matmul_sanity()
OUT["eig_residual"] = gate_eig_residual(rng)
OUT["dirichlet_moments"] = gate_dirichlet_moments(rng)
OUT["eckart_young"] = gate_eckart_young(rng)
OUT["procrustes"] = gate_procrustes(rng)
OUT["semicircle_edge"] = gate_semicircle_edge(rng)
OUT["signal_incoherence"] = gate_signal_incoherence(rng)
OUT["all_pass"] = bool(all(v["pass"] for v in OUT.values() if isinstance(v, dict)))
OUT = jsonable(OUT)
with open("outputs/gates.json", "w") as f:
json.dump(OUT, f, indent=1)
print(json.dumps(OUT, indent=1))