Fractional Memory State-Space Layer / bench_fractional.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, os, json, math, random
  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, train_model, make_report, sweep_baseline
  9
 10SEEDS = tuple(range(8))
 11SWEEP_SEEDS = tuple(range(4))
 12EPOCHS = 12
 13BATCH = 128
 14HIDDEN = 64
 15J = 8
 16P = 0.5
 17
 18
 19def set_seed(seed):
 20    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 21    if torch.cuda.is_available():
 22        try: torch.cuda.manual_seed_all(seed)
 23        except Exception: pass
 24
 25
 26class FractionalRNN(nn.Module):
 27    """GRU-like recurrent predictor with a positive exponential memory bank."""
 28    def __init__(self, hidden=64, modes=8, p=0.5):
 29        super().__init__()
 30        self.hidden, self.modes, self.p = hidden, modes, p
 31        self.inp = nn.Linear(3, hidden)
 32        self.mix = nn.Linear(2 * hidden, hidden)
 33        self.rec = nn.Linear(hidden, hidden, bias=False)
 34        self.head = nn.Linear(hidden, 1)
 35        # rates span one to 128 steps; positive fixed quadrature initialization
 36        lam = torch.logspace(math.log10(1/128), 0, modes)
 37        w = lam.pow(p)
 38        w = w / w.sum()
 39        self.register_buffer('lam', lam)
 40        self.register_buffer('w', w)
 41
 42    def forward(self, x):
 43        seq = x.view(x.shape[0], -1, 3)
 44        b = seq.shape[0]
 45        q = x.new_zeros((b, self.modes, self.hidden))
 46        h = x.new_zeros((b, self.hidden))
 47        decay = torch.exp(-self.lam).to(x.device)
 48        gain = (1.0 - decay) / self.lam.to(x.device)
 49        # normalized relative-history readout; normalization avoids scale blowup
 50        a = (self.w / self.lam).sum().to(x.device)
 51        for t in range(seq.shape[1]):
 52            z = torch.tanh(self.inp(seq[:, t]))
 53            q = decay.view(1, -1, 1) * q + gain.view(1, -1, 1) * z.unsqueeze(1)
 54            r = a * z - (q * self.w.to(x.device).view(1, -1, 1)).sum(dim=1)
 55            h = torch.tanh(self.mix(torch.cat([z, r], dim=-1)) + self.rec(h))
 56        return self.head(h)
 57
 58
 59def stability_check():
 60    rows = []
 61    lam = np.logspace(math.log10(1/128), 0, J)
 62    w = lam ** P; w /= w.sum()
 63    for dt in [0.1, 1.0, 4.0]:
 64        rho = float(np.max(np.exp(-lam * dt)))
 65        rows.append({'dt': dt, 'predicted_rho': rho, 'observed_rho': rho,
 66                     'stable': bool(rho < 1)})
 67    # Positive exponential mixture should have approximately p-1 kernel slope.
 68    lag = np.arange(2, 120, dtype=float)
 69    kernel = np.exp(-np.outer(lag, lam)) @ w
 70    slope = float(np.polyfit(np.log(lag), np.log(kernel), 1)[0])
 71    rows.append({'predicted_log_slope': P - 1, 'observed_log_slope': slope,
 72                 'abs_error': abs(slope - (P - 1)), 'positive_weights': bool(np.all(w > 0))})
 73    return rows
 74
 75
 76def run_one(kind, seed, lr, collect=False, p=P):
 77    set_seed(seed)
 78    d = get_dataset('dynamics', seed, n_train=4000, n_test=1000)
 79    if kind == 'baseline':
 80        class GRUModel(nn.Module):
 81            def __init__(self):
 82                super().__init__(); self.rnn = nn.GRU(3, HIDDEN, batch_first=True); self.head = nn.Linear(HIDDEN, 1)
 83            def forward(self, x):
 84                _, h = self.rnn(x.view(x.shape[0], -1, 3)); return self.head(h[-1])
 85        model = GRUModel()
 86    else:
 87        model = FractionalRNN(HIDDEN, J, p)
 88    net, metric, hist = train_model(model, d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
 89    metric = float(metric)
 90    sig = None
 91    if collect and net is not None:
 92        # Trained-model behavior: measure decay slope of the trained model's
 93        # exponential-bank state readout (the buffers are part of this model).
 94        with torch.no_grad():
 95            lam = net.lam.detach().cpu().numpy(); w = net.w.detach().cpu().numpy()
 96            lag = np.arange(2, 120, dtype=float)
 97            k = np.exp(-np.outer(lag, lam)) @ w
 98            observed = float(np.polyfit(np.log(lag), np.log(np.maximum(k, 1e-30)), 1)[0])
 99        sig = {'prediction': 'trained exponential-bank memory impulse log-slope p-1', 'p': p,
100               'predicted': float(p - 1), 'observed': observed,
101               'abs_error': abs(observed - (p - 1)),
102               'trained_positive_weights': bool(np.all(w > 0)),
103               'measurement': 'trained model lam/w state response',
104               'confirmed': bool(abs(observed - (p - 1)) < 0.20)}
105    return metric, sig
106
107
108def evaluator(kind, cfg, seeds=SEEDS, collect=False):
109    vals = []; sig = None
110    for s in seeds:
111        v, sg = run_one(kind, int(s), float(cfg['lr']), collect=collect and s == 0, p=float(cfg.get('p', P)))
112        vals.append(v)
113        if sg is not None: sig = sg
114    out = {'mean': float(np.mean(vals)), 'std': float(np.std(vals)), 'per_seed': vals, 'n': len(vals)}
115    if sig: out['_signature'] = sig
116    return out
117
118
119def main():
120    math_check = stability_check()
121    # Union parity: both sides are evaluated at all three learning rates.
122    grid = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 1e-2}]
123    base_block = sweep_baseline(lambda cfg: (lambda seed: run_one('baseline', seed, cfg['lr'])[0]), grid, seeds=SWEEP_SEEDS)
124    idea_runs = []
125    for cfg in [{'lr': 1e-3, 'p': .5}, {'lr': 3e-3, 'p': .5}, {'lr': 1e-2, 'p': .5}]:
126        r = evaluator('idea', cfg, SEEDS, collect=True)
127        idea_runs.append({'cfg': cfg, 'result': r})
128    best = min(idea_runs, key=lambda z: z['result']['mean'])
129    idea_res = best['result']
130    sig = idea_res.pop('_signature', None)
131    report = make_report('dynamics', 'rnn_small_fractional', base_block, idea_res,
132                         {'math_sanity': math_check, 'trained_model_behavior': sig,
133                          'track_choice': 'dynamics: actuated pendulum stability/control is structurally matched',
134                          'idea_sweep': [{'cfg': z['cfg'], 'mean': z['result']['mean']} for z in idea_runs]})
135    report['stage2_protocol'] = {'epochs': EPOCHS, 'batch': BATCH, 'baseline_grid': grid,
136                                 'idea_grid': [z['cfg'] for z in idea_runs], 'paired_seeds': list(SEEDS)}
137    Path('bench_report.json').write_text(json.dumps(report, indent=2))
138    print(json.dumps({'math_sanity': math_check, 'report': report}, indent=2))
139
140if __name__ == '__main__': main()