diff options
| author | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 21:37:55 -0500 |
|---|---|---|
| committer | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 21:37:55 -0500 |
| commit | 22acd2899958def0d103f11da49c2c4a499be773 (patch) | |
| tree | 892b422c1e04cd9854420eda0c965e21df81c8db /logs | |
| parent | ef104fe4f07713bf11f266d2954b7446f176f8ae (diff) | |
The missing term was unary: omit-size solved at its ceiling in 1.4s
Adversarial review of the artifacts found three of my numbers to be
artifacts of my own code. All three reproduced here before acceptance:
- anchor_bound presented probe rows in the same index order on both
sides, so exact twins had their tie broken onto the diagonal. 0.997 ->
0.920 on omit-size. Fixed by scrambling the T-side presentation.
- The truth is not a strict local minimum: 51 transpositions have
exactly zero energy delta. fast_pair_descent only looked stationary
because its break test treats zero as no-improvement.
- scipy's FAQ takes no n_init, so it was swallowed into unknown_options
and 'FAQ x30 restarts' computed bit-identically to plain FAQ. Replaced
with a real restart loop over P0='randomized'.
The blind ceiling for omit-size is 0.836, not 1.0: the text field has 51
exact transposition automorphisms, so T[s,s] is bitwise identical to T
and no objective f(V, P T P^T) can separate an orbit at any order. Every
synthetic accuracy was being divided by the wrong denominator.
The fifteen failed solvers share one property -- all purely quadratic or
purely spectral, none with a node-level term. Eight moments of each
node's own field row, blended with the quadratic term through
Frank-Wolfe, reach 0.837 with an energy gap of exactly zero. The term
must stay in the loop: as a seed for pure-quadratic descent it scores
0.21, pinned through the iterations it scores 0.84 -- which is also why
amplification plateaued, being itself pure-quadratic.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'logs')
| -rw-r--r-- | logs/amp_probe.log | 10 | ||||
| -rw-r--r-- | logs/amp_probe.py | 166 | ||||
| -rw-r--r-- | logs/amplify.log | 6 | ||||
| -rw-r--r-- | logs/amplify160.log | 7 | ||||
| -rw-r--r-- | logs/amplify640.log | 6 | ||||
| -rw-r--r-- | logs/battery2.log | 3 |
6 files changed, 198 insertions, 0 deletions
diff --git a/logs/amp_probe.log b/logs/amp_probe.log new file mode 100644 index 0000000..6ee836b --- /dev/null +++ b/logs/amp_probe.log @@ -0,0 +1,10 @@ +REF grampa 0.0195 umeyama 0.0078 chance 0.0039 +trial0 mf scale=0 raw mean 0.0205 max 0.0234 | refined mean 0.0220 max 0.0273 | sharp 0.851 (79.4s) +trial0 iid scale=0.0001 raw mean 0.0200 max 0.0234 | refined mean 0.0190 max 0.0234 | sharp 0.851 (27.0s) +trial0 iid scale=0.01 raw mean 0.0200 max 0.0234 | refined mean 0.0215 max 0.0312 | sharp 0.852 (95.5s) +trial0 iid scale=0.1 raw mean 0.0205 max 0.0234 | refined mean 0.0200 max 0.0273 | sharp 0.851 (277.2s) +trial0 iid scale=0.3 raw mean 0.0127 max 0.0195 | refined mean 0.0151 max 0.0234 | sharp 0.783 (116.4s) +trial0 iid scale=1 raw mean 0.0151 max 0.0195 | refined mean 0.0142 max 0.0195 | sharp 0.857 (141.2s) +trial0 iid scale=3 raw mean 0.0083 max 0.0195 | refined mean 0.0088 max 0.0156 | sharp 0.810 (130.5s) +trial0 iid scale=10 raw mean 0.0093 max 0.0156 | refined mean 0.0107 max 0.0234 | sharp 0.840 (331.4s) +trial0 iid scale=100 raw mean 0.0107 max 0.0117 | refined mean 0.0117 max 0.0195 | sharp 0.859 (202.1s) diff --git a/logs/amp_probe.py b/logs/amp_probe.py new file mode 100644 index 0000000..2969348 --- /dev/null +++ b/logs/amp_probe.py @@ -0,0 +1,166 @@ +"""Adversarial probe of the Onsager-corrected Sinkhorn (adaptive-TAP) proposal. + +Iteration under test: h = beta * V mu T - b_t * mu_prev ; mu = Sinkhorn(exp(h)) +b_t = 0 recovers naive mean field, which is algebraically identical to the PGD +form of entropic Gromov-Wasserstein already measured at 0.034 on omit-size. + +We sweep the Onsager coefficient over ten orders of magnitude so the verdict +does not depend on getting the R-transform prescription exactly right: if no +scalar b works, no principled derivation of b can rescue it. +""" +import json, sys, time +import numpy as np +import torch + +sys.path.insert(0, "/home/yurenh2/emm") +from worldalign.synth_fast_gate import fast_pair_descent +from worldalign.spectral_match import grampa, umeyama + +DEV = torch.device("cuda:0") +DT = torch.float64 + + +def standardise(M): + m = ~np.eye(len(M), dtype=bool) + v = M[m] + out = (M - v.mean()) / v.std() + np.fill_diagonal(out, 0.0) + return out + + +def sinkhorn_log(h, iters=60): + """Doubly stochastic (row and col sums 1) projection of exp(h), batched.""" + lg = h - h.amax(dim=(-2, -1), keepdim=True) + for _ in range(iters): + lg = lg - torch.logsumexp(lg, dim=-1, keepdim=True) + lg = lg - torch.logsumexp(lg, dim=-2, keepdim=True) + return torch.exp(lg) + + +def hutch_chi(h, mu, eps=1e-3, iters=60): + """(1/N^2) tr(d Sinkhorn / d h) by a random probe.""" + Z = torch.randn_like(h) + mu2 = sinkhorn_log(h + eps * Z, iters) + return ((Z * (mu2 - mu)).sum(dim=(-2, -1)) / (eps * h.shape[-1] ** 2)) + + +def r_transform(spec_prod, chi_grid): + """Numerical R-transform of an empirical spectrum: invert Stieltjes.""" + # G(z) = mean 1/(z - l); R(g) = G^{-1}(g) - 1/g + lo, hi = spec_prod.max() * 1.0001, spec_prod.max() * 1e6 + out = [] + for g in chi_grid: + a, b = lo, hi + for _ in range(200): + m = 0.5 * (a + b) + if np.mean(1.0 / (m - spec_prod)) > g: + a = m + else: + b = m + z = 0.5 * (a + b) + out.append(z - 1.0 / g) + return np.array(out) + + +def run_amp(V, T, betas, b_scale, mode, restarts, iters_per_beta, seed, + spec_prod=None, damping=0.0): + N = V.shape[-1] + g = torch.Generator(device=DEV).manual_seed(seed) + mu = torch.rand(restarts, N, N, generator=g, device=DEV, dtype=DT) * 0.01 + mu = sinkhorn_log(mu) + mu_prev = torch.full_like(mu, 1.0 / N) + traj = [] + for beta in betas: + for _ in range(iters_per_beta): + drive = beta * torch.matmul(torch.matmul(V, mu), T) + if mode == "mf": + b = torch.zeros(restarts, device=DEV, dtype=DT) + else: + chi = hutch_chi(drive, mu) + if mode == "iid": + coef = beta * beta * N # sum_j V_ij^2 <T^2> ~ N + elif mode == "ri": + cg = np.clip(chi.abs().cpu().numpy(), 1e-12, None) + coef = torch.tensor( + r_transform(spec_prod * beta, cg), device=DEV, dtype=DT) + b = b_scale * coef * chi + h = drive - b.view(-1, 1, 1) * mu_prev + new = sinkhorn_log(h) + if damping: + new = (1 - damping) * new + damping * mu + mu_prev, mu = mu, new + if not torch.isfinite(mu).all(): + return None, traj + traj.append(float(mu.amax(dim=-1).mean())) + return mu, traj + + +def score(mu, hidden, V_gpu, Tsh_gpu, N, refine=True): + from scipy.optimize import linear_sum_assignment + accs, refs = [], [] + for b in range(mu.shape[0]): + _, col = linear_sum_assignment(-mu[b].cpu().numpy()) + a = float((hidden[col] == np.arange(N)).mean()) + accs.append(a) + if refine: + fin = fast_pair_descent( + Tsh_gpu, V_gpu, + torch.from_numpy(np.ascontiguousarray(col)).to(DEV), 600) + refs.append(float((hidden[fin.cpu().numpy()] == np.arange(N)).mean())) + return accs, refs + + +def main(): + d = torch.load("/home/yurenh2/emm/artifacts/synth_v1/omit_size.pt", + map_location="cpu", weights_only=False) + V = standardise(d["visual_field"].double().numpy()) + T = standardise(d["text_field"].double().numpy()) + N = len(V) + lv = np.linalg.eigvalsh(V) + results = [] + + for trial in range(2): + rng = np.random.default_rng(trial) + hidden = rng.permutation(N) + Tsh = standardise(T[np.ix_(hidden, hidden)]) + lt = np.linalg.eigvalsh(Tsh) + spec_prod = np.outer(lv, lt).ravel() / N # operator scale of V (x) T / N + Vg = torch.from_numpy(V).to(DEV).to(DT) + Tg = torch.from_numpy(Tsh).to(DEV).to(DT) + + # sanity: GRAMPA / Umeyama reference on this exact instance + if trial == 0: + gp = grampa(V, Tsh, 1.0) + um = umeyama(V, Tsh) + print("REF grampa %.4f umeyama %.4f chance %.4f" + % (float((hidden[gp] == np.arange(N)).mean()), + float((hidden[um] == np.arange(N)).mean()), 1 / N), flush=True) + + betas_ladder = list(np.geomspace(0.02, 2.0, 20)) + for mode in ("mf", "iid", "ri"): + scales = [0.0] if mode == "mf" else [1e-4, 1e-2, 0.1, 0.3, 1.0, 3.0, 10.0, 100.0] + for sc in scales: + t0 = time.time() + mu, traj = run_amp(Vg, Tg, betas_ladder, sc, mode, 8, 25, 100 + trial, + spec_prod=spec_prod) + if mu is None: + print("trial%d %-4s scale=%-8g DIVERGED after %d beta steps (%.1fs)" + % (trial, mode, sc, len(traj), time.time() - t0), flush=True) + results.append(dict(trial=trial, mode=mode, scale=sc, + diverged=True, betas_done=len(traj))) + continue + accs, refs = score(mu, hidden, Vg, Tg, N) + print("trial%d %-4s scale=%-8g raw mean %.4f max %.4f | refined mean %.4f max %.4f | sharp %.3f (%.1fs)" + % (trial, mode, sc, np.mean(accs), np.max(accs), + np.mean(refs), np.max(refs), traj[-1], time.time() - t0), + flush=True) + results.append(dict(trial=trial, mode=mode, scale=sc, diverged=False, + raw=accs, refined=refs, sharpness=traj[-1])) + + with open("/home/yurenh2/emm/logs/amp_probe.json", "w") as f: + json.dump(results, f, indent=1) + print("DONE") + + +if __name__ == "__main__": + main() diff --git a/logs/amplify.log b/logs/amplify.log index f397d0a..f775312 100644 --- a/logs/amplify.log +++ b/logs/amplify.log @@ -1,2 +1,8 @@ === omit-size (N=256, chance=0.0039) + trial 0: ladders=[0.242, 0.152, 0.207] picked-by-energy=0.2070 + trial 1: ladders=[0.02, 0.059, 0.035] picked-by-energy=0.0195 + trial 2: ladders=[0.277, 0.273, 0.016] picked-by-energy=0.2734 + MEAN selected-by-energy=0.1667 (oracle ladder choice would give 0.1927) + +=== synth-full (N=256, chance=0.0039) diff --git a/logs/amplify160.log b/logs/amplify160.log new file mode 100644 index 0000000..c51c9f0 --- /dev/null +++ b/logs/amplify160.log @@ -0,0 +1,7 @@ + +=== omit-size-pool160 (N=256, chance=0.0039) + trial 0: ladders=[0.383, 0.395] picked-by-energy=0.3945 + trial 1: ladders=[0.641, 0.68] picked-by-energy=0.6797 + trial 2: ladders=[0.012, 0.449] picked-by-energy=0.4492 + MEAN selected-by-energy=0.5078 (oracle ladder choice would give 0.5078) +{"done": true} diff --git a/logs/amplify640.log b/logs/amplify640.log new file mode 100644 index 0000000..f2e8354 --- /dev/null +++ b/logs/amplify640.log @@ -0,0 +1,6 @@ + +=== omit-size-pool640 (N=256, chance=0.0039) + trial 0: ladders=[0.562, 0.293] picked-by-energy=0.5625 + trial 1: ladders=[0.727, 0.461] picked-by-energy=0.7266 + MEAN selected-by-energy=0.6445 (oracle ladder choice would give 0.6445) +{"done": true} diff --git a/logs/battery2.log b/logs/battery2.log index a0b3a2a..baae871 100644 --- a/logs/battery2.log +++ b/logs/battery2.log @@ -28,3 +28,6 @@ === synth-full-BOUND0.99 (N=256, chance=0.0039) GW x12 restarts raw=0.3698 refined=0.3971 (12.3s) GW kl-loss raw=0.0846 refined=0.0885 (13.9s) + entropic GW deep anneal raw=0.0078 refined=0.0443 (824.6s) + semirelaxed GW raw=0.0221 refined=0.0365 (60.7s) +{"done": true} |
