Cross-Degree Certificate Against Recurrent Oscillation / bench_cross_degree.py
Failed on benchmark
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()