Projection-Regularized Gradient Updates / projection_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4
  5SEED = 1515
  6
  7def invsqrt(A, eps=1e-12):
  8    w, V = np.linalg.eigh((A + A.T) * 0.5)
  9    w = np.maximum(w, eps)
 10    return (V * (1.0 / np.sqrt(w))) @ V.T
 11
 12def metric_from_history(G, beta, lam, rho, alpha):
 13    p, m = G.shape
 14    S = beta * np.zeros((p, p)) + (1-beta) * (G @ G.T) / m + lam * np.eye(p)
 15    R = invsqrt(S)
 16    if alpha == 1.0:
 17        # Projection is mathematically unused in this baseline branch.
 18        P = np.zeros((p, p))
 19        Q = R @ R
 20    else:
 21        K = G.T @ np.linalg.solve(S, G) + rho * np.eye(m)
 22        P = R @ G @ np.linalg.solve(K, G.T) @ R
 23        Q = R @ ((1-alpha) * P + alpha * np.eye(p)) @ R
 24    return S, P, Q
 25
 26def predicted_threshold(H, Q):
 27    # For general noncommuting H,Q this is the exact quadratic GD threshold.
 28    A = (Q @ H + H @ Q) * 0.5
 29    return 2.0 / np.max(np.linalg.eigvalsh(A))
 30
 31def run_fixed(H, G, eta, beta, lam, rho, alpha, steps=100):
 32    _, _, Q = metric_from_history(G, beta, lam, rho, alpha)
 33    theta = np.array([1.0, 1.0])
 34    losses = []
 35    for _ in range(steps):
 36        losses.append(0.5 * theta @ H @ theta)
 37        theta = theta - eta * Q @ (H @ theta)
 38        if not np.all(np.isfinite(theta)) or np.linalg.norm(theta) > 1e8:
 39            return False, losses
 40    return losses[-1] < losses[0] and np.linalg.norm(theta) < 1.0, losses
 41
 42def observed_threshold(H, G, beta, lam, rho, alpha):
 43    pred = predicted_threshold(H, metric_from_history(G,beta,lam,rho,alpha)[2])
 44    # binary search for largest tested stable eta; stability means monotone decay over 300 steps
 45    lo, hi = 0.0, pred * 1.8
 46    for _ in range(30):
 47        mid = (lo + hi) / 2
 48        ok, _ = run_fixed(H,G,mid,beta,lam,rho,alpha,steps=300)
 49        if ok: lo = mid
 50        else: hi = mid
 51    return lo, pred
 52
 53def quadratic_training(H, Gbase, eta, lam, rho, alpha, rng, steps=160, noise=0.25):
 54    p = H.shape[0]
 55    theta = np.array([1.0, 1.0])
 56    hist = []
 57    losses = []
 58    for t in range(steps):
 59        g = H @ theta + noise * rng.normal(size=p)
 60        hist.append(g.copy())
 61        G = np.column_stack(hist[-Gbase:])
 62        _, _, Q = metric_from_history(G, 0.0, lam, rho, alpha)
 63        theta -= eta * Q @ g
 64        losses.append(0.5 * theta @ H @ theta)
 65    return float(np.mean(losses[-30:])), float(np.mean(losses)), losses
 66
 67def main():
 68    H = np.diag([12.0, 1.0])
 69    # The history only identifies the stiff coordinate: this makes the weak-support claim explicit.
 70    G = np.array([[1.0, 0.8, 1.2, 0.9], [0.0, 0.0, 0.0, 0.0]])
 71    beta, rho, alpha = 0.0, 0.15, 0.08
 72
 73    # Prediction 1: increasing ridge shrinks the supported metric eigenvalue and raises the stability limit.
 74    ridge_rows = []
 75    for lam in [0.01, 0.03, 0.1, 0.3, 1.0]:
 76        obs, pred = observed_threshold(H,G,beta,lam,rho,alpha)
 77        Q = metric_from_history(G,beta,lam,rho,alpha)[2]
 78        ridge_rows.append({'lambda':lam, 'q_supported':float(Q[1,1]), 'pred_eta_c':pred, 'observed_eta_c':obs})
 79
 80    # Prediction 2: increasing rho weakens projection-supported directions and raises eta_c.
 81    rho_rows = []
 82    for r in [0.01, 0.03, 0.1, 0.3, 1.0]:
 83        obs, pred = observed_threshold(H,G,beta,0.1,r,alpha)
 84        Q = metric_from_history(G,beta,0.1,r,alpha)[2]
 85        rho_rows.append({'rho':r, 'q_supported':float(Q[0,0]), 'pred_eta_c':pred, 'observed_eta_c':obs})
 86
 87    # Prediction 3: alpha controls unsupported directions: at alpha=0 they receive zero metric.
 88    alpha_rows = []
 89    for a in [0.0, 0.02, 0.1, 0.3, 1.0]:
 90        Q = metric_from_history(G,beta,0.1,rho,a)[2]
 91        alpha_rows.append({'alpha':a, 'q_unsupported':float(Q[1,1]), 'q_supported':float(Q[0,0])})
 92
 93    # Secondary matched-step comparison on noisy quadratic, averaging fixed seeds.
 94    baseline, idea = [], []
 95    eta = 0.055
 96    for seed in range(8):
 97        rb = np.random.default_rng(SEED + seed)
 98        ri = np.random.default_rng(SEED + seed)
 99        baseline.append(quadratic_training(H, 4, eta, 1e-12, 0.0, 1.0, rb)[0])
100        idea.append(quadratic_training(H, 4, eta, 0.1, rho, alpha, ri)[0])
101    result = {
102        'settings': {'H_diag':[12.0,1.0], 'history_rank':1, 'beta':beta, 'rho':rho, 'alpha':alpha},
103        'ridge_sweep':ridge_rows, 'rho_sweep':rho_rows, 'alpha_sweep':alpha_rows,
104        'comparison': {'baseline_mean_final_loss':float(np.mean(baseline)), 'baseline_std':float(np.std(baseline)), 'idea_mean_final_loss':float(np.mean(idea)), 'idea_std':float(np.std(idea)), 'eta':eta},
105        'checks': {
106            'ridge_predicted_monotonic': bool(all(ridge_rows[i]['pred_eta_c'] < ridge_rows[i+1]['pred_eta_c'] for i in range(4))),
107            'ridge_observed_monotonic': bool(all(ridge_rows[i]['observed_eta_c'] < ridge_rows[i+1]['observed_eta_c'] for i in range(4))),
108            'rho_predicted_monotonic': bool(all(rho_rows[i]['pred_eta_c'] < rho_rows[i+1]['pred_eta_c'] for i in range(4))),
109            'rho_observed_monotonic': bool(all(rho_rows[i]['observed_eta_c'] < rho_rows[i+1]['observed_eta_c'] for i in range(4))),
110            'unsupported_zero_at_alpha0': bool(alpha_rows[0]['q_unsupported'] < 1e-10),
111            'unsupported_increases_with_alpha': bool(all(alpha_rows[i]['q_unsupported'] < alpha_rows[i+1]['q_unsupported'] for i in range(4)))
112        }
113    }
114    with open('results.json','w') as f: json.dump(result,f,indent=2)
115    print(json.dumps(result, indent=2))
116
117if __name__ == '__main__': main()