Cross-Degree Certificate Against Recurrent Oscillation / bench_cross_degree.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, 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, make_model, evaluate, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11EPOCHS = 12
 12BATCH = 128
 13# Baseline and idea share the complete learning-rate union.
 14GRID = [
 15    {'lr': 1e-3, 'weight_decay': 0.0},
 16    {'lr': 3e-3, 'weight_decay': 0.0},
 17    {'lr': 6e-3, 'weight_decay': 0.0},
 18]
 19IDEA_GRID = [
 20    {'lr': 1e-3, 'weight_decay': 0.0, 'lambda_cycle': 0.01},
 21    {'lr': 3e-3, 'weight_decay': 0.0, 'lambda_cycle': 0.03},
 22    {'lr': 6e-3, 'weight_decay': 0.0, 'lambda_cycle': 0.10},
 23]
 24
 25
 26def seed_all(seed):
 27    random.seed(seed)
 28    np.random.seed(seed)
 29    torch.manual_seed(seed)
 30    if torch.cuda.is_available():
 31        torch.cuda.manual_seed_all(seed)
 32
 33
 34def train_one(seed, cfg, intervention=False, return_signature=False):
 35    seed_all(seed)
 36    ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
 37    net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 38    lam = float(cfg.get('lambda_cycle', 0.0)) if intervention else 0.0
 39    seq_len = int(np.prod(ds['input_shape']) // 3)
 40    last_h = None
 41    for device in (['cuda', 'cpu'] if torch.cuda.is_available() else ['cpu']):
 42        try:
 43            net = net.to(device)
 44            x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 45            opt = torch.optim.Adam(net.parameters(), lr=float(cfg['lr']),
 46                                   weight_decay=float(cfg.get('weight_decay', 0.0)))
 47            mse = nn.MSELoss()
 48            for _ in range(EPOCHS):
 49                net.train()
 50                perm = torch.randperm(len(x), device=device)
 51                for start in range(0, len(x), BATCH):
 52                    idx = perm[start:start+BATCH]
 53                    xb, yb = x[idx], y[idx]
 54                    inp = xb.view(len(xb), seq_len, 3)
 55                    _, hseq = net.rnn(inp)
 56                    pred = net.head(hseq[-1])
 57                    loss = mse(pred, yb)
 58                    if lam:
 59                        # Re-run the GRU one prefix at a time to expose h_t.
 60                        # Penalize the exact period-two signature: adjacent
 61                        # states differ but two-step states are close.
 62                        hs = []
 63                        h = torch.zeros(1, len(xb), net.rnn.hidden_size,
 64                                         device=device, dtype=xb.dtype)
 65                        for t in range(seq_len):
 66                            _, h = net.rnn(inp[:, t:t+1], h)
 67                            hs.append(h[-1])
 68                        H = torch.stack(hs, dim=1)
 69                        if seq_len >= 3:
 70                            two = (H[:, 2:] - H[:, :-2]).pow(2).mean()
 71                            adjacent = (H[:, 1:] - H[:, :-1]).pow(2).mean()
 72                            cycle_pen = two / (adjacent.detach() + 1e-4)
 73                            loss = loss + lam * cycle_pen
 74                    opt.zero_grad(set_to_none=True)
 75                    loss.backward()
 76                    opt.step()
 77            net.eval()
 78            with torch.no_grad():
 79                pred = net(ds['xte'].to(device))
 80                metric = float(((pred - ds['yte'].to(device)) ** 2).mean())
 81                # Signature is measured from the trained model, not an analytic toy.
 82                inp = ds['xte'].to(device).view(len(ds['xte']), seq_len, 3)
 83                h = torch.zeros(1, len(inp), net.rnn.hidden_size, device=device)
 84                hs = []
 85                for t in range(seq_len):
 86                    _, h = net.rnn(inp[:, t:t+1], h); hs.append(h[-1])
 87                H = torch.stack(hs, 1)
 88                if seq_len >= 3:
 89                    two = float((H[:, 2:] - H[:, :-2]).pow(2).mean())
 90                    one = float((H[:, 1:] - H[:, :-1]).pow(2).mean())
 91                    ratio = two / (one + 1e-8)
 92                else:
 93                    two, one, ratio = 0.0, 0.0, 0.0
 94            if return_signature:
 95                return metric, {'observed_two_step_mse': two,
 96                                'observed_adjacent_mse': one,
 97                                'observed_two_to_one_ratio': ratio}
 98            return metric
 99        except RuntimeError:
100            if device == 'cpu':
101                raise
102            net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
103    raise RuntimeError('training failed')
104
105
106def base_fn(cfg):
107    return lambda seed: train_one(seed, cfg, False)
108
109
110def idea_fn(cfg):
111    return lambda seed: train_one(seed, cfg, True)
112
113
114def main():
115    baseline = sweep_baseline(base_fn, GRID, seeds=(0, 1, 2, 3))
116    # Evaluate all three idea settings on the same full paired seeds; report best.
117    idea_trials = []
118    for cfg in IDEA_GRID:
119        r = evaluate(idea_fn(cfg), seeds=SEEDS)
120        idea_trials.append({'cfg': cfg, 'result': r})
121    best = min(idea_trials, key=lambda z: z['result']['mean'])
122    sig_rows = []
123    for s in SEEDS:
124        _, sig = train_one(s, best['cfg'], True, True)
125        _, bsig = train_one(s, baseline['best_cfg'], False, True)
126        sig_rows.append({'seed': s, 'baseline': bsig, 'idea': sig})
127    b2 = np.mean([r['baseline']['observed_two_to_one_ratio'] for r in sig_rows])
128    i2 = np.mean([r['idea']['observed_two_to_one_ratio'] for r in sig_rows])
129    signature = {
130        'prediction': 'anti-oscillation penalty should reduce trained recurrent two-step similarity relative to adjacent change',
131        'baseline_mean_two_to_one_ratio': float(b2),
132        'idea_mean_two_to_one_ratio': float(i2),
133        'predicted_direction': 'idea lower than baseline',
134        'confirmed': bool(i2 < b2),
135        'per_seed': sig_rows,
136    }
137    rep = make_report('dynamics', 'rnn_small', baseline, best['result'], extra=signature)
138    rep['idea_trials'] = idea_trials
139    rep['structural_match'] = 'Dynamics track: recurrent GRU predicts actuated pendulum rollout; anti-oscillation targets recurrent hidden-state period-two behaviour.'
140    rep['protocol'] = {'paired_seeds': list(SEEDS), 'epochs': EPOCHS, 'batch': BATCH,
141                       'baseline_grid': GRID, 'idea_grid': IDEA_GRID}
142    Path('bench_report.json').write_text(json.dumps(rep, indent=2))
143    print(json.dumps(rep, indent=2))
144
145
146if __name__ == '__main__':
147    main()