Slow-Mode-Canceling Optimizer Packet / bench_experiment.py

Unverified

Raw ⬇ ZIP
  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()