Conservative Parallel-Edge Decomposition / bench_stage2.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, random, sys
  2from pathlib import Path
  3import numpy as np
  4import torch
  5from torch import nn
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import get_dataset, train_model, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11# Union of all learning rates is used by both baseline and idea.
 12GRID = [
 13    {'lr': 0.0015, 'epochs': 24},
 14    {'lr': 0.0030, 'epochs': 24},
 15    {'lr': 0.0060, 'epochs': 24},
 16]
 17B = torch.tensor([[-1.0], [1.0]], dtype=torch.float32)
 18
 19class EdgeTemporalBase(nn.Module):
 20    """Matched small dynamics forecaster; only edge exchange differs."""
 21    def __init__(self, idea=False, channels=2, hidden=32):
 22        super().__init__()
 23        self.idea, self.channels = idea, channels
 24        self.temporal = nn.GRU(3, hidden, batch_first=True)
 25        # Shared post-message state readout on both systems.
 26        self.mix = nn.Linear(hidden + 2, hidden)
 27        if idea:
 28            self.channel_mlps = nn.ModuleList([
 29                nn.Sequential(nn.Linear(3, 12), nn.Tanh(), nn.Linear(12, 1))
 30                for _ in range(channels)
 31            ])
 32        else:
 33            self.edge = nn.Sequential(nn.Linear(3, 24), nn.Tanh(), nn.Linear(24, 2))
 34        self.head = nn.Linear(hidden, 1)
 35        self._last_exchange = None
 36
 37    def forward(self, x):
 38        seq = x.view(x.shape[0], 8, 3)
 39        _, h = self.temporal(seq)
 40        z = seq[:, -1, :]  # endpoint/driver features of the final observed step
 41        if self.idea:
 42            flows = torch.cat([m(z) for m in self.channel_mlps], dim=1)
 43            flow = flows.sum(dim=1, keepdim=True)
 44            exchange = torch.einsum('nm,bm->bn', B.to(x.device), flow)
 45        else:
 46            exchange = self.edge(z)
 47        self._last_exchange = exchange
 48        fused = torch.tanh(self.mix(torch.cat([h[-1], exchange], dim=1)))
 49        return self.head(fused)
 50
 51def seed_all(seed):
 52    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 53    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 54
 55def make_ds(seed):
 56    return get_dataset('dynamics', seed, n_train=400, n_test=200)
 57
 58def train_one(seed, cfg, idea):
 59    seed_all(seed)
 60    ds = make_ds(seed)
 61    model = EdgeTemporalBase(idea=idea)
 62    _, metric, _ = train_model(model, ds, epochs=int(cfg['epochs']), lr=float(cfg['lr']), batch=128, log=lambda *_: None)
 63    return float(metric) if metric is not None else float('nan')
 64
 65def make_fn(idea, cfg):
 66    return lambda seed: train_one(seed, cfg, idea)
 67
 68def signature(cfg):
 69    # Re-test the predicted identity on trained models, not an analytic toy.
 70    rows = []
 71    for seed in SEEDS:
 72        seed_all(seed); ds = make_ds(seed)
 73        model = EdgeTemporalBase(idea=True)
 74        model, _, _ = train_model(model, ds, epochs=int(cfg['epochs']), lr=float(cfg['lr']), batch=128, log=lambda *_: None)
 75        with torch.no_grad():
 76            # Signature is evaluated on CPU to avoid shared-GPU/cuDNN allocation failures.
 77            model = model.cpu()
 78            x = ds['xte'][:128].cpu()
 79            q = model(x)
 80            ex = model._last_exchange
 81            residual = ex.sum(dim=1).abs().mean().item()
 82            rows.append({'seed': seed, 'mean_abs_internal_residual': residual,
 83                         'mean_abs_predicted_output': q.abs().mean().item()})
 84    vals = [r['mean_abs_internal_residual'] for r in rows]
 85    max_res = max(vals)
 86    # In float32, <=1e-6 is an honest machine-scale conservation tolerance.
 87    return {'quantity': 'trained-model mean absolute 1^T B P',
 88            'predicted': 'zero internal net exchange', 'observed_max': max_res,
 89            'observed_mean': float(np.mean(vals)), 'per_seed': rows,
 90            'confirmed': bool(max_res <= 1e-6)}
 91
 92def main():
 93    print('baseline sweep')
 94    base = sweep_baseline(lambda cfg: make_fn(False, cfg), GRID, seeds=SEEDS)
 95    best = base['best_cfg']
 96    # Idea is evaluated at best baseline config and two nearby settings; the
 97    # union is exactly GRID, and baseline sweep covered every setting.
 98    idea = {'per_seed': [], 'configs': []}
 99    for cfg in GRID:
100        r = __import__('bench').evaluate(make_fn(True, cfg), SEEDS)
101        idea['configs'].append({'cfg': cfg, 'result': r})
102    best_idea = min(idea['configs'], key=lambda z: z['result']['mean'])
103    idea_res = best_idea['result']; idea_res['selected_cfg'] = best_idea['cfg']
104    rep = make_report('dynamics', 'rnn_small', base, idea_res,
105                      extra=signature(best_idea['cfg']))
106    rep['protocol'] = {'paired_seeds': list(SEEDS), 'grid': GRID,
107                       'baseline_best_cfg': best, 'idea_grid_results': idea['configs']}
108    Path('bench_report.json').write_text(json.dumps(rep, indent=2))
109    print(json.dumps(rep, indent=2))
110
111if __name__ == '__main__': main()