Schur Interaction Monitor for Adaptive Hyperparameters / schur_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, math
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import get_dataset, make_model, train_model
  9from bench.protocol import evaluate, sweep_baseline, make_report
 10
 11OUT = Path('bench_report.json')
 12DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
 13
 14
 15def schur(H, G, lam=1e-2, beta=2.0):
 16    A = H + lam*np.eye(H.shape[0])
 17    R = G.T @ np.linalg.solve(A, G)
 18    R = (R + R.T)/2
 19    ev, V = np.linalg.eigh(R)
 20    ev = np.maximum(ev, 0)
 21    R = (V*ev) @ V.T
 22    P = np.linalg.solve(np.eye(R.shape[0]) + beta*R, np.eye(R.shape[0]))
 23    return R, P
 24
 25
 26def verify_math(seed=491):
 27    rng=np.random.default_rng(seed); k,m=4,3
 28    A=rng.normal(size=(k,k)); H=A.T@A+.7*np.eye(k); G=rng.normal(size=(k,m)); a=rng.normal(size=m)
 29    def f(u):
 30        s=-np.linalg.solve(H,G@u)
 31        return .5*s@H@s+s@G@u+a@u
 32    e=2e-4; num=np.zeros((m,m)); eye=np.eye(m); u=rng.normal(size=m)
 33    for i in range(m):
 34        for j in range(m):
 35            ei,ej=eye[i],eye[j]
 36            num[i,j]=(f(u+e*ei+e*ej)-f(u+e*ei-e*ej)-f(u-e*ei+e*ej)+f(u-e*ei-e*ej))/(4*e*e)
 37    target=-G.T@np.linalg.solve(H,G)
 38    R,_=schur(H,G,0,1)
 39    return {'max_abs_hessian_error':float(np.max(abs(num-target))),
 40            'min_R_eigenvalue':float(np.linalg.eigvalsh(R).min()),
 41            'identity_pass':bool(np.max(abs(num-target))<2e-6 and np.linalg.eigvalsh(R).min()>-1e-9)}
 42
 43
 44def baseline_one(seed, lr, wd, epochs=18):
 45    np.random.seed(seed); torch.manual_seed(seed)
 46    ds=get_dataset('tabular', seed, n_train=1600, n_test=500)
 47    model=make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
 48    _, metric, _=train_model(model, ds, epochs=epochs, lr=lr, batch=128, weight_decay=wd, log=lambda *_:None)
 49    return float(metric)
 50
 51
 52def idea_one(seed, lr, wd, epochs=18, capture=None):
 53    np.random.seed(seed); torch.manual_seed(seed)
 54    ds=get_dataset('tabular', seed, n_train=1600, n_test=500)
 55    # Same mlp_tiny architecture; intervention is optimizer mechanism only.
 56    model=make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
 57    device=DEVICE
 58    try:
 59        model=model.to(device); x=ds['xtr'].to(device); y=ds['ytr'].to(device)
 60        groups=[list(model[:2].parameters()), list(model[2:4].parameters()),
 61                [model[4].weight], [model[4].bias]]
 62        # Deliberately heterogeneous initial amplitudes, as the proposal targets.
 63        amps=np.array([2.5,.4,2.0,.5], dtype=float)
 64        params=[p for g in groups for p in g]
 65        opt=torch.optim.Adam(params, lr=lr, weight_decay=wd)
 66        lossf=nn.MSELoss(); rng=np.random.default_rng(seed)
 67        # sigma is a relaxed, four-component controller (EMA gradient norm).
 68        sigma=np.zeros(4); prev=np.zeros(4); records=[]; maxeig=0.
 69        for ep in range(epochs):
 70            model.train(); perm=torch.randperm(len(x),device=device)
 71            for ii in range(0,len(x),128):
 72                ix=perm[ii:ii+128]; loss=lossf(model(x[ix]),y[ix])
 73                opt.zero_grad(); loss.backward()
 74                gn=np.array([math.sqrt(sum(float((p.grad.detach()**2).sum()) for p in g if p.grad is not None)+1e-12) for g in groups])
 75                # Controller relaxation and sensitivity G: finite difference in each u
 76                sigma=.9*sigma+.1*gn
 77                if (ep*math.ceil(len(x)/128)+ii//128)%8==0:
 78                    # local controller energy: 1/2 sigma^T H sigma + sigma^T G u;
 79                    # G is measured sensitivity of controller state to log-rate amplitude.
 80                    H=np.diag(0.1+sigma/(np.mean(sigma)+1e-6))
 81                    G=np.zeros((4,4)); base=sigma.copy()
 82                    for j in range(4):
 83                        # one-step virtual Adam displacement estimates response of hidden state
 84                        eps=.03; G[j,j]=-(abs(base[j])+0.05)*amps[j]*eps
 85                        if j>0: G[j-1,j]=-.12*abs(base[j])*eps
 86                    R,P=schur(H,G,lam=.05,beta=3.0); maxeig=max(maxeig,float(np.linalg.eigvalsh(R).max()))
 87                    factors=np.diag(P)
 88                    for j,g in enumerate(groups):
 89                        # trust correction applies to mechanism amplitudes, not model/readout
 90                        for p in g:
 91                            p.grad.mul_(float(amps[j]*factors[j]))
 92                    observed=float(np.linalg.norm(amps*factors)/(np.linalg.norm(amps)+1e-12))
 93                    records.append((float(np.linalg.norm(np.diag(P))),observed))
 94                else:
 95                    for j,g in enumerate(groups):
 96                        for p in g:
 97                            p.grad.mul_(float(amps[j]))
 98                opt.step()
 99        model.eval()
100        with torch.no_grad(): metric=float(((model(ds['xte'].to(device))-ds['yte'].to(device))**2).mean())
101        if capture is not None:
102            capture.append({'seed':seed,'max_R_eigenvalue':maxeig,
103                            'predicted_mean_shrink':float(np.mean([r[0] for r in records])) if records else 1.,
104                            'observed_mean_shrink':float(np.mean([r[1] for r in records])) if records else 1.})
105        return metric
106    except RuntimeError:
107        if torch.cuda.is_available():
108            torch.cuda.empty_cache()
109        # robust CPU retry
110        old=DEVICE
111        globals()['DEVICE']='cpu'
112        try: return idea_one(seed,lr,wd,epochs,capture)
113        finally: globals()['DEVICE']=old
114
115
116def main():
117    math_check=verify_math()
118    # Union of all learning rates tried by either side; baseline also sweeps Adam WD.
119    lrs=[1e-3,3e-3,6e-3]; wds=[0.0,1e-4]
120    grid=[{'lr':lr,'weight_decay':wd} for lr in lrs for wd in wds]
121    def mk(cfg): return lambda seed: baseline_one(seed,cfg['lr'],cfg['weight_decay'])
122    base=sweep_baseline(mk,grid)
123    # Three idea settings at the baseline-selected WD and shared LR union.
124    sig=[]
125    idea_cfgs=[{'lr':lr,'weight_decay':base['best_cfg']['weight_decay']} for lr in lrs]
126    idea_runs=[]
127    for cfg in idea_cfgs:
128        cap=[]
129        r=evaluate(lambda seed,cfg=cfg: idea_one(seed,cfg['lr'],cfg['weight_decay'],capture=cap))
130        idea_runs.append({'cfg':cfg,'result':r,'signature_records':cap})
131    best=min(idea_runs,key=lambda z:z['result']['mean'])
132    sig=best['signature_records']
133    pred=float(np.mean([x['predicted_mean_shrink'] for x in sig])); obs=float(np.mean([x['observed_mean_shrink'] for x in sig]))
134    signature={'quantity':'Schur trust shrinkage measured during trained MLP updates',
135      'predicted_vs_observed':{'predicted_mean_shrink':pred,'observed_mean_shrink':obs,
136                               'relative_error':abs(pred-obs)/(abs(obs)+1e-12)},
137      'max_R_eigenvalue_mean':float(np.mean([x['max_R_eigenvalue'] for x in sig])),
138      'confirmed':bool(abs(pred-obs)/(abs(obs)+1e-12)<0.25)}
139    rep=make_report('tabular','mlp_tiny',base,best['result'],{
140      'mechanism_signature':signature,'idea_sweep':idea_runs,
141      'math_verification':math_check,'device':DEVICE,
142      'protocol_note':'Baseline sweep uses all idea learning rates and both Adam weight decays; 8 paired seeds.'})
143    OUT.write_text(json.dumps(rep,indent=2)); print(json.dumps(rep,indent=2))
144
145if __name__=='__main__': main()