Slow-Mode-Canceling Optimizer Packet / bench_experiment.py
Unverified
1import sys, os, 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, train_model, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = (0, 1, 2, 3)
12EPOCHS = 20
13WARM_STEPS = 3
14BATCH = 128
15# Union of all learning rates considered by either method.
16LR_GRID = [1e-3, 3e-3, 6e-3]
17KAPPA_GRID = [0.01, 0.03, 0.06]
18
19
20def seed_all(seed):
21 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
22 if torch.cuda.is_available():
23 torch.cuda.manual_seed_all(seed)
24
25
26def baseline_one(cfg, seed, capture=False):
27 seed_all(seed)
28 ds = get_dataset('tabular', seed, n_train=400, n_test=400)
29 model = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
30 net, metric, hist = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
31 batch=BATCH, weight_decay=cfg.get('weight_decay', 0.0), log=lambda *_: None)
32 return float(metric)
33
34
35def flat_params(model):
36 return torch.cat([p.detach().flatten() for p in model.parameters()])
37
38
39def set_flat(model, vec):
40 off = 0
41 with torch.no_grad():
42 for p in model.parameters():
43 n = p.numel(); p.copy_(vec[off:off+n].view_as(p)); off += n
44
45
46def idea_one(cfg, seed, capture=False):
47 seed_all(seed)
48 ds = get_dataset('tabular', seed, n_train=400, n_test=400)
49 # The local loop is necessary because the intervention changes the optimizer.
50 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
51 try:
52 target = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
53 packet = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
54 except Exception:
55 device = torch.device('cpu')
56 target = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
57 packet = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
58 # Same initialization, then prepare phi by cancelling the empirically slow
59 # displacement direction v (the displacement accumulated in a short warmup).
60 init = flat_params(target).clone()
61 packet.load_state_dict(target.state_dict())
62 xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
63 lossf = nn.MSELoss()
64 warm_opt = torch.optim.Adam(target.parameters(), lr=cfg['lr'])
65 target.train()
66 warm_steps = WARM_STEPS
67 for j in range(warm_steps):
68 for a in range(0, len(xtr), BATCH):
69 warm_opt.zero_grad(set_to_none=True)
70 lossf(target(xtr[a:a+BATCH]), ytr[a:a+BATCH]).backward(); warm_opt.step()
71 warm_delta = flat_params(target) - init
72 v = warm_delta / (warm_delta.norm() + 1e-12)
73 # restore target to identical post-warmup state and reflect only v component
74 # in packet: v^T(phi-init) = -v^T(theta-init).
75 theta0 = flat_params(target).clone()
76 packet.load_state_dict(target.state_dict())
77 packet_vec = theta0 - 2.0 * torch.dot(theta0 - init, v) * v
78 set_flat(packet, packet_vec)
79 residual_before = abs(torch.dot((theta0-init + packet_vec-init)/2, v)).item() / (abs(torch.dot(theta0-init, v)).item()+1e-12)
80 opt_t = torch.optim.Adam(target.parameters(), lr=cfg['lr'])
81 opt_p = torch.optim.Adam(packet.parameters(), lr=cfg['lr'])
82 # Continue with identical minibatch order and symmetric parameter coupling.
83 coupling_start = None; coupling_end = None
84 for ep in range(max(1, EPOCHS - WARM_STEPS)):
85 target.train(); packet.train()
86 perm = torch.randperm(len(xtr), device=device)
87 for ii in range(0, len(xtr), BATCH):
88 ix = perm[ii:ii+BATCH]
89 opt_t.zero_grad(set_to_none=True); opt_p.zero_grad(set_to_none=True)
90 lt = lossf(target(xtr[ix]), ytr[ix]); lp = lossf(packet(xtr[ix]), ytr[ix])
91 lt.backward(); lp.backward()
92 with torch.no_grad():
93 tv, pv = flat_params(target), flat_params(packet)
94 diff = cfg['kappa'] * (tv-pv)
95 # Adam applies its update after gradients; coupling is added to
96 # gradients parameter-wise, preserving the stated Euler form.
97 off=0
98 for p in target.parameters():
99 n=p.numel(); p.grad.add_(diff[off:off+n].view_as(p)); off += n
100 off=0
101 for p in packet.parameters():
102 n=p.numel(); p.grad.add_((-diff[off:off+n]).view_as(p)); off += n
103 opt_t.step(); opt_p.step()
104 if ep == 0:
105 curv = (flat_params(target)+flat_params(packet))/2 - init
106 coupling_start = abs(torch.dot(curv, v)).item() / (abs(torch.dot(flat_params(target)-init, v)).item()+1e-12)
107 with torch.no_grad():
108 pred = target(ds['xte'].to(device)); metric = lossf(pred, ds['yte'].to(device)).item()
109 final_c = (flat_params(target)+flat_params(packet))/2 - init
110 final_target = flat_params(target)-init
111 coupling_end = abs(torch.dot(final_c, v)).item()/(abs(torch.dot(final_target,v)).item()+1e-12)
112 if capture:
113 return float(metric), {'initial_cancellation_ratio': residual_before,
114 'observed_c_ratio_epoch1': coupling_start,
115 'observed_c_ratio_final': coupling_end,
116 'v_norm': float(v.norm().item())}
117 return float(metric)
118
119
120def make_base(cfg):
121 return lambda seed: baseline_one(cfg, seed)
122
123def make_idea(cfg):
124 return lambda seed: idea_one(cfg, seed)
125
126
127def main():
128 # Baseline includes every lr used by the idea; method-central Adam decay is
129 # explicitly swept as a parity knob (zero is the standard setting).
130 base_grid = [{'lr': lr, 'weight_decay': wd} for lr in LR_GRID for wd in [0.0]]
131 base = sweep_baseline(make_base, base_grid, seeds=SWEEP_SEEDS)
132 idea_grid = [{'lr': lr, 'kappa': k} for lr in LR_GRID for k in KAPPA_GRID]
133 # Nearby lr settings are also evaluated on baseline through base_grid.
134 idea_trials=[]
135 for cfg in idea_grid:
136 r=evaluate(make_idea(cfg), seeds=SWEEP_SEEDS)
137 idea_trials.append({'cfg':cfg, 'mean':r['mean'], 'result':r})
138 best=min(idea_trials, key=lambda z:z['mean'])
139 idea_full=evaluate(make_idea(best['cfg']), seeds=SEEDS)
140 rep=make_report('tabular','mlp_tiny', {'best_cfg':base['best_cfg'],'sweep':base['sweep'],'full':base['full']}, idea_full,
141 {'predicted': 'packet preparation makes common slow-direction projection near zero',
142 'observed': idea_one(best['cfg'], 0, capture=True)[1],
143 'confirmed': idea_one(best['cfg'], 0, capture=True)[1]['initial_cancellation_ratio'] < 0.1,
144 'idea_sweep': idea_trials})
145 Path('bench_report.json').write_text(json.dumps(rep, indent=2))
146 print(json.dumps(rep, indent=2))
147
148if __name__ == '__main__': main()