Commutator-Regularized Switched SSM / commutator_ssm_mvp.py

Running benchmark…

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4from scipy.linalg import expm, norm, eigvals
  5import torch
  6import torch.nn as nn
  7
  8SEED = 449
  9np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
 10torch.set_num_threads(4)
 11DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 12
 13
 14def bch_check():
 15    # For two generators, the residual after retaining the commutator term
 16    # should scale cubically with the step size.
 17    rng = np.random.default_rng(SEED)
 18    X = rng.normal(size=(3,3)); Y = rng.normal(size=(3,3))
 19    X -= 1.7*np.eye(3); Y -= 1.7*np.eye(3)
 20    C = X @ Y - Y @ X
 21    rows = []
 22    for s in [0.02, 0.04, 0.08, 0.16, 0.32]:
 23        exact = expm(s*Y) @ expm(s*X)
 24        avg = expm(s*(X+Y) - 0.5*s*s*C)
 25        first = expm(s*(X+Y))
 26        rows.append((s, norm(exact-first, 'fro'), norm(exact-avg, 'fro')))
 27    x = np.log([r[0] for r in rows[-3:]])
 28    y1 = np.log([r[1] for r in rows[-3:]])
 29    y2 = np.log([r[2] for r in rows[-3:]])
 30    return {"rows": [[float(v) for v in r] for r in rows],
 31            "first_order_loglog_slope": float(np.polyfit(x,y1,1)[0]),
 32            "commutator_corrected_loglog_slope": float(np.polyfit(x,y2,1)[0])}
 33
 34
 35def matrix_metrics(A, tau=0.7, alpha=(0.5,0.5)):
 36    B = [alpha[i]*tau*A[i] for i in range(2)]
 37    Phi = expm(B[1]) @ expm(B[0])
 38    avg = expm(B[0]+B[1])
 39    comm = norm(B[0]@B[1]-B[1]@B[0], 'fro')**2
 40    mismatch = norm(Phi-avg, 'fro') / max(norm(avg,'fro'), 1e-12)
 41    return float(comm), float(mismatch), Phi
 42
 43
 44def regularizer_optimization():
 45    # Directly optimize two matrices toward a fixed noncommuting task matrix,
 46    # with and without the proposed commutator penalty.
 47    torch.manual_seed(SEED+1)
 48    target = torch.tensor(np.array([[.72,.35],[-.15,.61]], dtype=np.float32))
 49    out = {}
 50    for name, lam in [("unregularized",0.0),("commutator_regularized",2.0)]:
 51        A = nn.Parameter(torch.randn(2,2)*.7)
 52        D = nn.Parameter(torch.randn(2,2)*.7)
 53        opt = torch.optim.Adam([A,D], lr=.04)
 54        for _ in range(500):
 55            B0=.35*A; B1=.35*D
 56            phi=torch.matrix_exp(B1)@torch.matrix_exp(B0)
 57            loss=(phi-target).pow(2).mean()+lam*(B0@B1-B1@B0).pow(2).sum()
 58            opt.zero_grad(); loss.backward(); opt.step()
 59        a=A.detach().numpy(); d=D.detach().numpy()
 60        comm, mismatch, _=matrix_metrics([a,d],tau=.7)
 61        fit=float(((expm(.35*d)@expm(.35*a))-target.numpy())**2 .mean()) if False else float(np.mean((expm(.35*d)@expm(.35*a)-target.numpy())**2))
 62        out[name]={"task_fit_mse":fit,"commutator":comm,"ordered_averaged_relative_error":mismatch}
 63    return out
 64
 65
 66class SwitchedSSM(nn.Module):
 67    def __init__(self, d=8, modes=2, regularized=False, single=False):
 68        super().__init__(); self.d=d; self.m=modes; self.regularized=regularized; self.single=single
 69        n=1 if single else modes
 70        self.A=nn.Parameter(torch.randn(n,d,d)*.08 - .35*torch.eye(d).unsqueeze(0).repeat(n,1,1))
 71        self.U=nn.Parameter(torch.randn(modes,d)*.12)
 72        self.v=nn.Parameter(torch.randn(d)*.12)
 73        self.readout=nn.Linear(d,1)
 74    def forward(self,x):
 75        h=torch.zeros(x.shape[0],self.d,device=x.device)
 76        alpha=1.0/self.m; tau=.7
 77        E=[]
 78        for i in range(self.m):
 79            A=self.A[0] if self.single else self.A[i]
 80            E.append(torch.matrix_exp(alpha*tau*A))
 81        for t in range(x.shape[1]):
 82            for i in range(self.m):
 83                h=(h.unsqueeze(1) @ E[i].T).squeeze(1)
 84                h=h+alpha*x[:,t:t+1]*self.U[i]
 85        return self.readout(h).squeeze(-1)
 86    def penalties(self):
 87        if self.single: return torch.tensor(0.,device=self.A.device)
 88        B=[.5*.7*a for a in self.A]
 89        return sum(((B[i]@B[j]-B[j]@B[i])**2).sum() for i in range(self.m) for j in range(i))
 90    def diagnostics(self):
 91        with torch.no_grad():
 92            As=self.A.detach().cpu().numpy(); As=As if not self.single else np.repeat(As,2,axis=0)
 93            comm, mismatch, phi=matrix_metrics(As,tau=.7)
 94            gain=max(abs(eigvals(phi)))
 95        return {"commutator":comm,"ordered_averaged_relative_error":mismatch,"cycle_spectral_gain":float(gain)}
 96
 97
 98def sequence_experiment():
 99    torch.manual_seed(SEED+2)
100    dev=DEVICE
101    ntrain, ntest, T=192,96,18
102    rng=np.random.default_rng(SEED+2)
103    X=rng.normal(size=(ntest+ntrain,T)).astype('float32')
104    # Stable long-memory target: exponentially discounted sequence sum.
105    weights=(.88**np.arange(T-1,-1,-1)).astype('float32')
106    y=(X*weights).sum(1).astype('float32')
107    xt=torch.tensor(X[:ntrain],device=dev); yt=torch.tensor(y[:ntrain],device=dev)
108    xe=torch.tensor(X[ntrain:],device=dev); ye=torch.tensor(y[ntrain:],device=dev)
109    results={}
110    for name, reg, single in [("single_generator",0.,True),("switched_unregularized",0.,False),("switched_commutator_regularized",.08,False)]:
111        torch.manual_seed(SEED+10+len(results)); model=SwitchedSSM(8,2,reg>0,single).to(dev)
112        opt=torch.optim.Adam(model.parameters(),lr=.025)
113        for step in range(100):
114            pred=model(xt); loss=((pred-yt)**2).mean()+reg*model.penalties()
115            opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(),5.0); opt.step()
116        with torch.no_grad(): val=float(((model(xe)-ye)**2).mean().cpu())
117        results[name]={"validation_mse":val,**model.diagnostics()}
118    return results
119
120
121def main():
122    global DEVICE
123    try:
124        b=bch_check(); ro=regularizer_optimization(); seq=sequence_experiment()
125    except Exception as e:
126        # CUDA failures are explicitly allowed to fall back to CPU.
127        if DEVICE=="cuda":
128            DEVICE="cpu"; torch.cuda.empty_cache(); b=bch_check(); ro=regularizer_optimization(); seq=sequence_experiment()
129        else: raise
130    result={"device":DEVICE,"bch":b,"regularizer_optimization":ro,"sequence_experiment":seq}
131    Path("results.json").write_text(json.dumps(result,indent=2))
132    print(json.dumps(result,indent=2))
133if __name__=="__main__": main()