Persistent Relational Memory / experiment.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json, time
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED = 17
  7np.random.seed(SEED); torch.manual_seed(SEED)
  8try:
  9    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 10    if device.type == 'cuda':
 11        torch.zeros(1, device=device)
 12except Exception:
 13    device = torch.device('cpu')
 14
 15# Core recurrence from the proposal, with a compact GRU-like parameterization.
 16class EdgeGRU(nn.Module):
 17    def __init__(self, inp=2, mem=8):
 18        super().__init__(); self.mem = mem
 19        self.z = nn.Linear(inp + mem, mem); self.q = nn.Linear(inp + mem, mem)
 20        self.h = nn.Linear(inp + mem, mem)
 21    def forward(self, m, u):
 22        v = torch.cat([u, m], -1)
 23        z = torch.sigmoid(self.z(v)); q = torch.sigmoid(self.q(v))
 24        cand = torch.tanh(self.h(torch.cat([u, q*m], -1)))
 25        return (1-z)*m + z*cand
 26
 27class PersistentEdge(nn.Module):
 28    def __init__(self, mem=8):
 29        super().__init__(); self.rnn=EdgeGRU(2,mem); self.out=nn.Linear(mem,1)
 30    def forward(self, vals, active):
 31        # vals/active: [batch,T], one stable pair per sequence; dictionary is per batch key.
 32        m=torch.zeros(vals.size(0), self.rnn.mem, device=vals.device); outputs=[]
 33        for t in range(vals.size(1)):
 34            u=torch.stack([active[:,t], vals[:,t]],-1)
 35            # Inactive pairs are not updated: this is dictionary retention.
 36            new=self.rnn(m,u); m=torch.where(active[:,t,None].bool(),new,m)
 37            outputs.append(self.out(m).squeeze(-1))
 38        return torch.stack(outputs,1)
 39
 40class ActiveMLP(nn.Module):
 41    def __init__(self):
 42        super().__init__(); self.net=nn.Sequential(nn.Linear(2,16),nn.Tanh(),nn.Linear(16,1))
 43    def forward(self, vals, active):
 44        return self.net(torch.stack([active,vals],-1)).squeeze(-1)
 45
 46def math_check():
 47    torch.manual_seed(3)
 48    g=EdgeGRU(2,16); m=torch.randn(1000,16)*20; u=torch.randn(1000,2)*50
 49    with torch.no_grad():
 50        mp=g(m,u)
 51        # Since candidate is in [-1,1], each coordinate is a convex combination.
 52        bound=(mp.abs() <= torch.maximum(m.abs(), torch.ones_like(m))).float().mean().item()
 53        inactive=torch.randn(4,16); update=g(inactive,torch.randn(4,2))
 54        retained=torch.where(torch.zeros(4,1).bool(),update,inactive)
 55        exact=(retained==inactive).all().item()
 56    return {'convex_coordinate_bound_fraction':bound,'inactive_retention_exact':bool(exact),
 57            'max_abs_before':float(m.abs().max()),'max_abs_after':float(mp.abs().max())}
 58
 59def data(n):
 60    # Pair-specific latent contact history. At t=0..2 the value is observable;
 61    # after a gap it reappears at t=9..11 with no informative current value.
 62    latent=torch.randint(0,2,(n,)).float()*2-1
 63    T=12; active=torch.zeros(n,T); vals=torch.zeros(n,T)
 64    active[:,0:3]=1; active[:,9:12]=1; vals[:,0:3]=latent[:,None]
 65    target=latent[:,None].expand(n,T)
 66    return vals.to(device),active.to(device),target.to(device)
 67
 68def train(model, vals, active, target, steps=700):
 69    opt=torch.optim.Adam(model.parameters(),lr=3e-3)
 70    model.train()
 71    for _ in range(steps):
 72        # fresh batch each step, avoiding memorization of sequence ordering
 73        v,a,y=data(128); pred=model(v,a)
 74        loss=((pred[:,9:12]-y[:,9:12])**2).mean()
 75        opt.zero_grad(); loss.backward(); opt.step()
 76    model.eval()
 77    with torch.no_grad():
 78        pred=model(vals,active); react=((pred[:,9:12]-target[:,9:12])**2).mean().item()
 79        one=((pred[:,0:3]-target[:,0:3])**2).mean().item()
 80        long=((pred-target)**2).mean().item()
 81    return react,one,long
 82
 83def timed(model, vals, active, reps=100):
 84    with torch.no_grad():
 85        for _ in range(10): model(vals,active)
 86        if device.type=='cuda': torch.cuda.synchronize()
 87        t=time.perf_counter()
 88        for _ in range(reps): model(vals,active)
 89        if device.type=='cuda': torch.cuda.synchronize()
 90    return (time.perf_counter()-t)/reps*1000
 91
 92def main():
 93    check=math_check(); vals,active,target=data(2048)
 94    torch.manual_seed(SEED); base=ActiveMLP().to(device)
 95    b=train(base,vals,active,target)
 96    torch.manual_seed(SEED); idea=PersistentEdge(8).to(device)
 97    e=train(idea,vals,active,target)
 98    base_ms=timed(base,vals[:128],active[:128]); edge_ms=timed(idea,vals[:128],active[:128])
 99    result={'device':str(device),'math_check':check,
100      'baseline':{'reactivation_mse':b[0],'early_mse':b[1],'all_timestep_mse':b[2], 'params':sum(p.numel() for p in base.parameters()),'ms_batch128':base_ms},
101      'persistent_edge_gru':{'reactivation_mse':e[0],'early_mse':e[1],'all_timestep_mse':e[2], 'params':sum(p.numel() for p in idea.parameters()),'ms_batch128':edge_ms},
102      'reactivation_mse_ratio_idea_over_baseline':e[0]/max(b[0],1e-12),
103      'latency_overhead_ratio':edge_ms/max(base_ms,1e-12)}
104    print(json.dumps(result,indent=2))
105if __name__=='__main__': main()