Permutation-Symmetric Quadratic Module / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4
  5SEED = 2235
  6np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  7torch.set_num_threads(min(12, torch.get_num_threads()))
  8try:
  9    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 10    if device.type == 'cuda':
 11        torch.cuda.set_device(0)
 12        torch.empty(1, device=device)
 13except Exception:
 14    device = torch.device('cpu')
 15
 16# Smooth exchangeable module: f(W)=sum_i v_i phi(u_i^T x), phi(0)=0.
 17# The quadratic term is phi'(0) sum_i v_i u_i^T x.
 18def phi(s):
 19    return s + 0.5*s*s + 0.1*s*s*s
 20
 21def branch_output(W, x):
 22    # W is p x d, last row is v and first k rows are u.
 23    u, v = W[:-1, :], W[-1, :]
 24    return (v * phi(u.T @ x)).sum()
 25
 26def structure_A(x):
 27    k = x.numel()
 28    A = torch.zeros((k+1, k+1), dtype=x.dtype, device=x.device)
 29    A[:-1, -1] = 0.5*x
 30    A[-1, :-1] = 0.5*x
 31    return A
 32
 33def quadratic_output(W, x):
 34    M = W @ W.T
 35    return torch.sum(M * structure_A(x))
 36
 37def fit_slope(xs, ys):
 38    return float(np.polyfit(np.log(xs), np.log(np.maximum(ys, 1e-30)), 1)[0])
 39
 40def math_checks():
 41    k, d = 5, 13
 42    x = torch.randn(k)
 43    base = torch.randn(k+1, d)
 44    eps = np.logspace(-3, -0.5, 8)
 45    residuals=[]; magnitudes=[]
 46    for e in eps:
 47        W=e*base
 48        residuals.append(abs(float(branch_output(W,x)-quadratic_output(W,x))))
 49        magnitudes.append(float(torch.linalg.norm(W)))
 50    slope=fit_slope(np.array(magnitudes), np.array(residuals))
 51
 52    # Exact exchangeability: arbitrary branch permutation leaves both outputs unchanged.
 53    perm=torch.randperm(d)
 54    perm_err=abs(float(branch_output(base[:,perm],x)-branch_output(base,x)))
 55    # Gram-only quadratic invariance: orthogonal mixing leaves WW^T exactly unchanged,
 56    # while the original module differs only by higher-order terms near zero.
 57    Q,_=torch.linalg.qr(torch.randn(d,d))
 58    gram_err=abs(float(quadratic_output(base@Q,x)-quadratic_output(base,x)))
 59    ortho_errs=[]
 60    for e in eps:
 61        ortho_errs.append(abs(float(branch_output(e*base@Q,x)-branch_output(e*base,x))))
 62    ortho_slope=fit_slope(np.array(magnitudes), np.array(ortho_errs))
 63
 64    # Width sweep: independent branch cubic contributions predict RMS residual ~ d^1/2
 65    # for random signs, while a fixed aligned construction predicts linear d. Measure both.
 66    widths=np.array([4,8,16,32,64])
 67    rms=[]; aligned=[]
 68    # Use nested prefixes for random widths and one fixed column/input for the
 69    # coherent construction; otherwise resampling would add unrelated noise.
 70    fixed_x=torch.randn(k)
 71    fixed_col=torch.randn(k+1,1)
 72    for dd in widths:
 73        vals=[]
 74        for rep in range(30):
 75            X=torch.randn(k+1,dd); xx=torch.randn(k)
 76            vals.append(abs(float(branch_output(0.03*X,xx)-quadratic_output(0.03*X,xx))))
 77        rms.append(np.mean(vals))
 78        X=fixed_col.repeat(1,dd)
 79        aligned.append(abs(float(branch_output(0.03*X,fixed_x)-quadratic_output(0.03*X,fixed_x))))
 80    width_slope_rms=fit_slope(widths, np.array(rms))
 81    width_slope_aligned=fit_slope(widths, np.array(aligned))
 82    return {
 83      'device':str(device), 'epsilon_values':eps.tolist(),
 84      'remainder_values':residuals, 'remainder_loglog_slope':slope,
 85      'predicted_remainder_slope':3.0,
 86      'permutation_error':perm_err, 'quadratic_orthogonal_error':gram_err,
 87      'original_orthogonal_difference_slope':ortho_slope,
 88      'predicted_orthogonal_difference_slope':3.0,
 89      'widths':widths.tolist(), 'width_mean_abs_remainder':rms,
 90      'width_slope_random':width_slope_rms, 'predicted_width_slope_random':0.5,
 91      'width_slope_aligned':width_slope_aligned, 'predicted_width_slope_aligned':1.0
 92    }
 93
 94class Branch(torch.nn.Module):
 95    def __init__(self,k,d,init=0.05):
 96        super().__init__(); self.W=torch.nn.Parameter(init*torch.randn(k+1,d))
 97    def forward(self,x):
 98        u,v=self.W[:-1],self.W[-1]
 99        return (phi(x @ u) * v).sum(1)
100class Quadratic(torch.nn.Module):
101    def __init__(self,k,d,init=0.05):
102        super().__init__(); self.W=torch.nn.Parameter(init*torch.randn(k+1,d))
103    def forward(self,x):
104        # Equivalent low-cost factorized trace: sum_i w_i^T A(x) w_i.
105        u,v=self.W[:-1],self.W[-1]
106        return (x @ u * v).sum(1)
107
108def train_model(cls, X, y, k, d, steps=500):
109    torch.manual_seed(SEED+7)
110    m=cls(k,d).to(device); opt=torch.optim.Adam(m.parameters(),lr=0.03)
111    Xt,yt=X.to(device),y.to(device)
112    losses=[]
113    for t in range(steps):
114        opt.zero_grad(); pred=m(Xt); loss=((pred-yt)**2).mean(); loss.backward(); opt.step()
115        if t in (0,99,249,499): losses.append(float(loss.detach().cpu()))
116    return losses
117
118def mini_experiment():
119    torch.manual_seed(SEED+99)
120    k,d,N=6,16,128
121    X=torch.randn(N,k)
122    teacher=torch.randn(k+1,d)*0.18
123    y=branch_output(teacher,X.T if False else X[0]) # overwritten vectorized below
124    u,v=teacher[:-1],teacher[-1]
125    y=(phi(X@u)*v).sum(1)
126    b=train_model(Branch,X,y,k,d)
127    q=train_model(Quadratic,X,y,k,d)
128    return {'dataset':'synthetic cubic exchangeable branch teacher','k':k,'width':d,
129            'steps':[0,99,249,499],'baseline_branch_mse':b,'idea_quadratic_mse':q,
130            'parameter_count_each':(k+1)*d}
131
132def main():
133    checks=math_checks(); comparison=mini_experiment()
134    # Mechanism passes only if all requested scaling predictions are numerically close.
135    checks['prediction_pass'] = (abs(checks['remainder_loglog_slope']-3)<0.15 and
136        abs(checks['original_orthogonal_difference_slope']-3)<0.2 and
137        abs(checks['width_slope_random']-0.5)<0.25 and
138        abs(checks['width_slope_aligned']-1)<0.2 and
139        checks['quadratic_orthogonal_error']<1e-5 and checks['permutation_error']<1e-5)
140    out={'math_checks':checks,'mini_experiment':comparison}
141    with open('results.json','w') as f: json.dump(out,f,indent=2)
142    print(json.dumps(out,indent=2))
143if __name__=='__main__': main()