CVaR-tail active residual correction / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, random, sys
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  8
  9SEEDS = tuple(range(8))
 10SWEEP_SEEDS = tuple(range(4))
 11EPOCHS = 24
 12BATCH = 128
 13TRACK = 'tabular'
 14MODEL = 'mlp_tiny'
 15LRS = [1e-3, 3e-3, 1e-2]
 16
 17
 18def seed_all(seed):
 19    random.seed(seed)
 20    np.random.seed(seed)
 21    torch.manual_seed(seed)
 22    if torch.cuda.is_available():
 23        try:
 24            torch.cuda.manual_seed_all(seed)
 25        except Exception:
 26            pass
 27
 28
 29def baseline_fn(cfg):
 30    def run(seed):
 31        seed_all(seed)
 32        ds = get_dataset(TRACK, seed, n_train=400, n_test=400)
 33        model = make_model(MODEL, ds['input_shape'], ds['out_dim'])
 34        _, metric, _ = train_model(model, ds, epochs=EPOCHS,
 35                                   lr=cfg['lr'], batch=BATCH,
 36                                   weight_decay=cfg.get('weight_decay', 0.0),
 37                                   log=lambda *_: None)
 38        return float(metric)
 39    return run
 40
 41
 42def idea_train(model, ds, epochs, lr, tail_weight, batch=BATCH):
 43    """Tail residual correction: first fit the cheap/global predictor, then
 44    upweight positive residuals above the empirical upper-tail boundary.
 45    The same MLP and optimizer family as the baseline are retained."""
 46    errs = []
 47    devices = []
 48    if torch.cuda.is_available():
 49        devices.append('cuda')
 50    devices.append('cpu')
 51    for device in devices:
 52        try:
 53            x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 54            net = model.to(device)
 55            opt = torch.optim.Adam(net.parameters(), lr=lr)
 56            n = x.shape[0]
 57            # Cheap surrogate phase: ordinary global MSE.
 58            warm = max(1, epochs // 2)
 59            gen = torch.Generator(device='cpu')
 60            gen.manual_seed(17000 + int(n) + int(round(lr * 1e6)))
 61            for ep in range(epochs):
 62                perm = torch.randperm(n, generator=gen)
 63                for start in range(0, n, batch):
 64                    ix = perm[start:start + batch].to(device)
 65                    pred = net(x[ix])
 66                    residual = pred - y[ix]
 67                    if ep < warm:
 68                        loss = (residual * residual).mean()
 69                    else:
 70                        # Estimated upper-tail boundary is the training beta quantile.
 71                        q = torch.quantile(y, 0.90)
 72                        # Smoothly emphasize samples in/above the tail, retaining
 73                        # ordinary MSE in the central region.
 74                        weights = 1.0 + (tail_weight - 1.0) * torch.sigmoid((y[ix] - q) / 0.15)
 75                        loss = (weights * residual * residual).mean()
 76                    opt.zero_grad(set_to_none=True)
 77                    loss.backward()
 78                    opt.step()
 79            with torch.no_grad():
 80                pred = net(ds['xte'].to(device))
 81                metric = ((pred - ds['yte'].to(device)) ** 2).mean().item()
 82            return net, float(metric), device
 83        except Exception as exc:
 84            errs.append(f'{device}:{str(exc)[:100]}')
 85            try:
 86                torch.cuda.empty_cache()
 87            except Exception:
 88                pass
 89    raise RuntimeError('idea training failed: ' + ' | '.join(errs))
 90
 91
 92def idea_fn(cfg):
 93    def run(seed):
 94        seed_all(seed)
 95        ds = get_dataset(TRACK, seed, n_train=400, n_test=400)
 96        model = make_model(MODEL, ds['input_shape'], ds['out_dim'])
 97        _, metric, _ = idea_train(model, ds, EPOCHS, cfg['lr'], cfg['tail_weight'])
 98        return float(metric)
 99    return run
100
101
102def behavior(cfg, idea=False):
103    global_vals, tail_vals, cvar_vals = [], [], []
104    for seed in SEEDS:
105        seed_all(seed)
106        ds = get_dataset(TRACK, seed, n_train=400, n_test=400)
107        model = make_model(MODEL, ds['input_shape'], ds['out_dim'])
108        if idea:
109            net, _, dev = idea_train(model, ds, EPOCHS, cfg['lr'], cfg['tail_weight'])
110        else:
111            net, _, hist = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
112                                       batch=BATCH, weight_decay=cfg.get('weight_decay', 0),
113                                       log=lambda *_: None)
114            dev = next(net.parameters()).device
115        with torch.no_grad():
116            p = net(ds['xte'].to(dev)).detach().cpu().numpy().ravel()
117        y = ds['yte'].numpy().ravel()
118        threshold = np.quantile(y, .90)
119        tail = y >= threshold
120        global_vals.append(float(np.mean((p-y)**2)))
121        tail_vals.append(float(np.mean((p[tail]-y[tail])**2)))
122        # Rockafellar empirical CVaR prediction versus observed test CVaR.
123        eta = np.quantile(p, .90)
124        pc = eta + np.maximum(p-eta, 0).mean()/.10
125        et = np.quantile(y, .90)
126        tc = et + np.maximum(y-et, 0).mean()/.10
127        cvar_vals.append(float(abs(pc-tc)))
128    return {'global_mse': float(np.mean(global_vals)),
129            'tail_mse': float(np.mean(tail_vals)),
130            'tail_to_global': float(np.mean(tail_vals)/max(np.mean(global_vals), 1e-12)),
131            'cvar_abs_error': float(np.mean(cvar_vals))}
132
133
134def main():
135    # Baseline is swept on the same learning-rate union used by the idea.
136    grid = [{'lr': lr, 'weight_decay': 0.0} for lr in LRS]
137    base = sweep_baseline(baseline_fn, grid, seeds=SWEEP_SEEDS)
138    idea_grid = [{'lr': lr, 'tail_weight': 4.0} for lr in LRS]
139    idea_sweep = []
140    for cfg in idea_grid:
141        r = evaluate(idea_fn(cfg), seeds=SWEEP_SEEDS)
142        idea_sweep.append({'cfg': cfg, 'result': r})
143    best = min(idea_sweep, key=lambda z: z['result']['mean'])
144    idea_full = evaluate(idea_fn(best['cfg']), seeds=SEEDS)
145    bcfg = base['best_cfg']
146    sig_b = behavior(bcfg, idea=False)
147    sig_i = behavior(best['cfg'], idea=True)
148    signature = {
149        'prediction': 'tail residual correction should preferentially reduce upper-tail prediction error relative to global error',
150        'baseline_behavior': sig_b,
151        'idea_behavior': sig_i,
152        'predicted_tail_focus': True,
153        'observed_tail_to_global_ratio_change': sig_i['tail_to_global'] - sig_b['tail_to_global'],
154        'confirmed': bool(sig_i['tail_to_global'] < sig_b['tail_to_global'])
155    }
156    report = make_report(TRACK, MODEL, base, idea_full,
157                         {'idea_config': best['cfg'], 'idea_sweep': idea_sweep,
158                          'mechanism_signature': signature})
159    report['protocol_notes'] = {'epochs': EPOCHS, 'batch': BATCH,
160        'baseline_grid': grid, 'idea_grid': idea_grid,
161        'intervention': 'same mlp_tiny trained with global-MSE warmup followed by upper-tail weighted residual correction'}
162    with open('bench_report.json', 'w') as f:
163        json.dump(report, f, indent=2)
164    print(json.dumps(report, indent=2))
165
166if __name__ == '__main__':
167    main()