Conditioned PSD sensing bottleneck / stage2_psd_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, sweep_baseline, evaluate, make_report
  7
  8# Sequence track is the structurally matched built-in task: the representation is
  9# a multi-token feature map and the PSD feature is a token Gram/covariance.
 10DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
 11EPOCHS = 12
 12NTR, NTE = 400, 200
 13BATCH = 64
 14D, M, WIN = 32, 16, 32
 15LAMBDA = 0.03
 16EPS = 1e-6
 17
 18class BottleneckNet(nn.Module):
 19    def __init__(self, kind):
 20        super().__init__()
 21        self.kind = kind
 22        self.inp = nn.Linear(1, D)
 23        self.pos = nn.Parameter(torch.zeros(1, WIN, D))
 24        nn.init.normal_(self.pos, std=.02)
 25        layer = nn.TransformerEncoderLayer(D, nhead=2, dim_feedforward=64,
 26                                            batch_first=True, dropout=0.0)
 27        self.enc = nn.TransformerEncoder(layer, 1)
 28        if kind == 'dense':
 29            self.proj = nn.Linear(WIN * D, M)
 30        else:
 31            self.a = nn.Parameter(torch.randn(M, D) / np.sqrt(D))
 32        self.head = nn.Linear(M, 1)
 33
 34    def features(self, x):
 35        h = self.inp(x.unsqueeze(-1)) + self.pos[:, :x.shape[1]]
 36        return self.enc(h)
 37
 38    def latent(self, x):
 39        h = self.features(x)
 40        if self.kind == 'dense':
 41            return self.proj(h.reshape(x.shape[0], -1))
 42        # z_i = <a_i a_i^T, HH^T/s> = mean_tokens (a_i^T h_t)^2
 43        return (torch.einsum('btd,md->btm', h, self.a).square().mean(1))
 44
 45    def forward(self, x):
 46        return self.head(self.latent(x))
 47
 48def seed_all(seed):
 49    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 50    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 51
 52def cond_loss(net, x):
 53    # Two independently perturbed views provide X,Y without changing labels.
 54    x1 = x + 0.025 * torch.randn_like(x)
 55    x2 = x + 0.025 * torch.randn_like(x)
 56    h1, h2 = net.features(x1), net.features(x2)
 57    if net.kind == 'dense':
 58        z1 = net.proj(h1.reshape(x.shape[0], -1)); z2 = net.proj(h2.reshape(x.shape[0], -1))
 59    else:
 60        z1 = torch.einsum('btd,md->btm', h1, net.a).square().mean(1)
 61        z2 = torch.einsum('btd,md->btm', h2, net.a).square().mean(1)
 62    # Frobenius norm of normalized Gram differences, vectorized over batch.
 63    X = torch.bmm(h1.transpose(1,2), h1) / WIN
 64    Y = torch.bmm(h2.transpose(1,2), h2) / WIN
 65    den = (X-Y).square().sum((1,2)).sqrt() + EPS
 66    q = (z1-z2).abs().sum(1) / den
 67    lo, hi = torch.quantile(q, .05), torch.quantile(q, .95)
 68    return torch.log(hi + EPS) - torch.log(lo + EPS)
 69
 70def train_one(seed, lr, kind, lam=0.0, return_net=False):
 71    seed_all(seed)
 72    ds = get_dataset('sequence', seed, NTR, NTE)
 73    net = BottleneckNet(kind)
 74    try:
 75        net = net.to(DEVICE); dev = DEVICE
 76        opt = torch.optim.Adam(net.parameters(), lr=lr)
 77        lossf = nn.MSELoss()
 78        xtr, ytr = ds['xtr'].to(dev), ds['ytr'].to(dev)
 79        for _ in range(EPOCHS):
 80            net.train(); p = torch.randperm(len(xtr), device=dev)
 81            for i in range(0, len(xtr), BATCH):
 82                ix = p[i:i+BATCH]
 83                pred = net(xtr[ix]); loss = lossf(pred, ytr[ix])
 84                if lam:
 85                    loss = loss + lam * cond_loss(net, xtr[ix])
 86                opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0); opt.step()
 87        net.eval()
 88        with torch.no_grad():
 89            metric = float(lossf(net(ds['xte'].to(dev)), ds['yte'].to(dev)))
 90        return (metric, net, ds) if return_net else metric
 91    except RuntimeError:
 92        # Explicit CPU fallback for constrained/shared CUDA environments.
 93        seed_all(seed); net = BottleneckNet(kind).cpu(); opt = torch.optim.Adam(net.parameters(), lr=lr)
 94        xtr, ytr = ds['xtr'], ds['ytr']
 95        for _ in range(EPOCHS):
 96            p = torch.randperm(len(xtr))
 97            for i in range(0,len(xtr),BATCH):
 98                ix=p[i:i+BATCH]; loss=nn.functional.mse_loss(net(xtr[ix]),ytr[ix])
 99                if lam: loss=loss+lam*cond_loss(net,xtr[ix])
100                opt.zero_grad(); loss.backward(); opt.step()
101        with torch.no_grad(): metric=float(nn.functional.mse_loss(net(ds['xte']),ds['yte']))
102        return (metric,net,ds) if return_net else metric
103
104def stats_for_model(net, ds):
105    net.eval(); dev=next(net.parameters()).device; x=ds['xte'].to(dev)
106    with torch.no_grad():
107        x1=x+0.025*torch.randn_like(x); x2=x+0.025*torch.randn_like(x)
108        h1,h2=net.features(x1),net.features(x2)
109        if net.kind=='dense': z1=net.proj(h1.reshape(len(x),-1)); z2=net.proj(h2.reshape(len(x),-1))
110        else: z1=torch.einsum('btd,md->btm',h1,net.a).square().mean(1); z2=torch.einsum('btd,md->btm',h2,net.a).square().mean(1)
111        den=(torch.bmm(h1.transpose(1,2),h1)/WIN-torch.bmm(h2.transpose(1,2),h2)/WIN).square().sum((1,2)).sqrt()+EPS
112        q=(z1-z2).abs().sum(1)/den
113    q=q.cpu().numpy(); lo,hi=np.quantile(q,[.05,.95])
114    return {'L_q05':float(lo),'U_q95':float(hi),'beta':float(hi/max(lo,EPS)),'mean_q':float(q.mean())}
115
116def main():
117    # Search-space parity: both methods are evaluated at every lr.
118    grid=[{'lr':v} for v in (0.001,0.003,0.006)]
119    base=sweep_baseline(lambda cfg: lambda s: train_one(s,cfg['lr'],'dense'), grid)
120    best_lr=base['best_cfg']['lr']
121    idea_grid=[best_lr]+[v for v in (0.001,0.003,0.006) if v!=best_lr]
122    idea_cfg=min(idea_grid, key=lambda lr: evaluate(lambda s: train_one(s,lr,'psd',LAMBDA), seeds=(0,1,2,3))['mean'])
123    idea=evaluate(lambda s: train_one(s,idea_cfg,'psd',LAMBDA))
124    # Signature is measured from trained models, not an analytical identity.
125    bmet,bnet,bds=train_one(0,best_lr,'dense',return_net=True)
126    imet,inet,ids=train_one(0,idea_cfg,'psd',LAMBDA,return_net=True)
127    bs,ins=stats_for_model(bnet,bds),stats_for_model(inet,ids)
128    sig={'prediction':'conditioning should narrow q spread (lower beta) and raise lower distortion',
129         'baseline_seed0':bs,'idea_seed0':ins,
130         'predicted_beta_direction':ins['beta'] < bs['beta'],
131         'predicted_L_direction':ins['L_q05'] > bs['L_q05'],
132         'confirmed':bool(ins['beta'] < bs['beta'] and ins['L_q05'] > bs['L_q05'])}
133    rep=make_report('sequence','transformer_tiny',base,idea,{'track_match':'multi-token sequence correlations','signature':sig,'hyperparameters':{'epochs':EPOCHS,'m':M,'d':D,'lambda':LAMBDA,'idea_lr':idea_cfg}})
134    rep['idea_sweep']=[{'lr':lr,'mean_4seed':evaluate(lambda s,lr=lr:train_one(s,lr,'psd',LAMBDA),seeds=(0,1,2,3))['mean']} for lr in idea_grid]
135    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
136    print(json.dumps(rep,indent=2))
137if __name__=='__main__': main()