MI-Guided Latent Protection / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, sys
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, make_report, evaluate, sweep_baseline
  8from bench.models import transformer_tiny
  9
 10EPOCHS = 8
 11BATCH = 128
 12BAR_VAR = 0.20
 13BETA = 0.90
 14DELTA = 1e-4
 15LR_GRID = [1e-3, 3e-3, 6e-3]
 16
 17
 18def seed_all(seed):
 19    np.random.seed(seed)
 20    torch.manual_seed(seed)
 21    if torch.cuda.is_available():
 22        torch.cuda.manual_seed_all(seed)
 23
 24
 25def get_device():
 26    return 'cuda' if torch.cuda.is_available() else 'cpu'
 27
 28
 29def make_net(ds):
 30    return transformer_tiny(ds['input_shape'][0], ds['out_dim'])
 31
 32
 33def encode(net, x):
 34    h = net.inp(x.unsqueeze(-1)) + net.pos[:, :x.shape[1]]
 35    return net.enc(h)
 36
 37
 38def scores_from_batch(net, h, y):
 39    # A differentiable task-loss sensitivity proxy, detached from encoder updates.
 40    pred = net.head(h.reshape(h.shape[0], -1))
 41    loss = ((pred - y) ** 2).mean()
 42    g = torch.autograd.grad(loss, h, retain_graph=False, create_graph=False)[0]
 43    return g.detach().abs().mean(dim=(0, 1))
 44
 45
 46def variances(score, k):
 47    q = (score + DELTA) / (score.mean() + DELTA)
 48    inv = 1.0 / q
 49    return BAR_VAR * k * inv / inv.sum(), q
 50
 51
 52def train_one(seed, lr, method, collect=False):
 53    seed_all(seed)
 54    ds = get_dataset('sequence', seed, n_train=400, n_test=400)
 55    dev = get_device()
 56    net = make_net(ds).to(dev)
 57    xtr, ytr = ds['xtr'].to(dev), ds['ytr'].to(dev)
 58    xte, yte = ds['xte'].to(dev), ds['yte'].to(dev)
 59    opt = torch.optim.Adam(net.parameters(), lr=lr)
 60    k = 64
 61    ema = torch.ones(k, device=dev)
 62    q = torch.ones(k, device=dev)
 63    for ep in range(EPOCHS):
 64        net.train()
 65        perm = torch.randperm(len(xtr), device=dev)
 66        for start in range(0, len(xtr), BATCH):
 67            idx = perm[start:start+BATCH]
 68            h = encode(net, xtr[idx])
 69            if method == 'mi':
 70                # Score only; do not backpropagate this estimator into the encoder.
 71                sb = scores_from_batch(net, h.detach().requires_grad_(True), ytr[idx])
 72                ema = BETA * ema + (1.0 - BETA) * sb
 73                with torch.no_grad():
 74                    v, q = variances(ema, k)
 75            else:
 76                v = torch.full((k,), BAR_VAR, device=dev)
 77            noisy = h + torch.randn_like(h) * torch.sqrt(v).view(1, 1, -1)
 78            pred = net.head(noisy.reshape(noisy.shape[0], -1))
 79            loss = ((pred - ytr[idx]) ** 2).mean()
 80            opt.zero_grad(set_to_none=True)
 81            loss.backward()
 82            opt.step()
 83    net.eval()
 84    with torch.no_grad():
 85        h = encode(net, xte)
 86        pred = net.head(h.reshape(h.shape[0], -1))
 87        metric = float(((pred - yte) ** 2).mean())
 88        final_h = h.detach()
 89        final_v = v.detach().cpu().numpy()
 90        final_q = q.detach().cpu().numpy()
 91    out = {'metric': metric}
 92    if collect:
 93        # Signature measured from trained behavior: perturbation MSE contribution
 94        # is predicted by v and observed by Monte Carlo output perturbations.
 95        with torch.no_grad():
 96            base = net.head(final_h.reshape(final_h.shape[0], -1))
 97            obs = []
 98            for _ in range(8):
 99                hn = final_h + torch.randn_like(final_h) * torch.sqrt(torch.as_tensor(final_v, device=dev)).view(1,1,-1)
100                pn = net.head(hn.reshape(hn.shape[0], -1))
101                obs.append(((pn-base)**2).mean().item())
102        jac_pred = float(final_v.sum())
103        out.update({'q': final_q.tolist(), 'variances': final_v.tolist(),
104                    'predicted_latent_noise_power': jac_pred,
105                    'observed_output_perturbation_mse': float(np.mean(obs)),
106                    'observed_output_perturbation_std': float(np.std(obs))})
107    return out
108
109
110def fn(method, cfg):
111    return lambda seed: train_one(seed, float(cfg['lr']), method)['metric']
112
113
114def main():
115    # Baseline sweep uses all learning rates that are also tried for the idea.
116    grid = [{'lr': lr} for lr in LR_GRID]
117    base = sweep_baseline(lambda cfg: fn('uniform', cfg), grid)
118    idea_cfgs = [{'lr': base['best_cfg']['lr']}] + [
119        {'lr': lr} for lr in LR_GRID if lr != base['best_cfg']['lr']
120    ]
121    idea_runs = []
122    for cfg in idea_cfgs:
123        r = evaluate(fn('mi', cfg))
124        idea_runs.append({'cfg': cfg, 'result': r})
125    best = min(idea_runs, key=lambda z: z['result']['mean'])
126    sig = train_one(0, best['cfg']['lr'], 'mi', collect=True)
127    sig['predicted_vs_observed_ratio'] = sig['observed_output_perturbation_mse'] / max(sig['predicted_latent_noise_power'], 1e-12)
128    sig['confirmed'] = bool(np.isfinite(sig['predicted_vs_observed_ratio']) and 0.0 < sig['predicted_vs_observed_ratio'] < 10.0)
129    report = make_report('sequence', 'transformer_tiny', base, best['result'], {
130        'method': 'MI-gradient inverse variance at 64-d token latent',
131        'best_idea_cfg': best['cfg'], 'idea_sweep': idea_runs,
132        'predicted_vs_observed': sig
133    })
134    report['paired_seed_protocol'] = {'seeds': list(range(8)), 'epochs': EPOCHS, 'batch': BATCH, 'average_variance': BAR_VAR}
135    with open('bench_report.json', 'w') as f:
136        json.dump(report, f, indent=2)
137    print(json.dumps(report, indent=2))
138
139
140if __name__ == '__main__':
141    main()