Pick-to-Learn Scenario Compression for Safe NN Calibration / bench_p2l.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, math
  2from pathlib import Path
  3import numpy as np
  4sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  5from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report, permutation_pvalue
  6
  7SEEDS = tuple(range(8))
  8# Shared union: every idea lr is also evaluated by baseline.
  9GRID = [
 10    {'lr': 0.0015, 'epochs': 12},
 11    {'lr': 0.0030, 'epochs': 12},
 12    {'lr': 0.0060, 'epochs': 12},
 13]
 14K = 20
 15BATCH_SELECT = 10
 16NTRAIN = 400
 17NTEST = 400
 18
 19
 20def epsilon_scenario(N, k, beta):
 21    # Cheap finite-sample-style sanity proxy: binomial tail/union bound.
 22    # It is not substituted for the paper theorem in the NN experiment.
 23    return min(1.0, (k + math.log(1.0 / beta)) / N)
 24
 25
 26def train_on_indices(ds, cfg, indices=None):
 27    if indices is None:
 28        sub = ds
 29    else:
 30        sub = dict(ds)
 31        ix = np.asarray(indices, dtype=np.int64)
 32        sub['xtr'] = ds['xtr'][ix]
 33        sub['ytr'] = ds['ytr'][ix]
 34    net = make_model('mlp_tiny', tuple(sub['xtr'].shape[1:]), sub['out_dim'])
 35    return train_model(net, sub, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, weight_decay=0.0, log=lambda *_: None)
 36
 37
 38def greedy_compress(ds, cfg, k=K, batch_size=BATCH_SELECT):
 39    n = len(ds['xtr'])
 40    chosen = []
 41    remaining = np.arange(n)
 42    while len(chosen) < k:
 43        # Retrain on the current active set before scoring all scenarios.
 44        model, _, _ = train_on_indices(ds, cfg, chosen if chosen else remaining[:1])
 45        model.eval()
 46        import torch
 47        dev = next(model.parameters()).device
 48        with torch.no_grad():
 49            pred = model(ds['xtr'].to(dev)).reshape(-1).cpu().numpy()
 50        y = ds['ytr'].reshape(-1).cpu().numpy()
 51        residual = np.abs(y - pred)
 52        mask = np.ones(n, dtype=bool); mask[np.asarray(chosen, dtype=int)] = False
 53        cand = remaining[mask[remaining]]
 54        take = min(batch_size, k-len(chosen))
 55        # Select the most informative/high-loss scenarios, a practical active-set rule.
 56        picked = cand[np.argsort(residual[cand])[-take:]]
 57        chosen.extend([int(x) for x in picked])
 58    model, metric, hist = train_on_indices(ds, cfg, chosen)
 59    return float(metric), chosen, model
 60
 61
 62def run_baseline(cfg):
 63    def one(seed):
 64        ds = get_dataset('tabular', seed, n_train=NTRAIN, n_test=NTEST)
 65        _, metric, _ = train_on_indices(ds, cfg)
 66        return metric
 67    return one
 68
 69
 70def run_idea(cfg):
 71    def one(seed):
 72        ds = get_dataset('tabular', seed, n_train=NTRAIN, n_test=NTEST)
 73        metric, _, _ = greedy_compress(ds, cfg)
 74        return metric
 75    return one
 76
 77
 78def behavior_signature(cfg, seed=0):
 79    ds = get_dataset('tabular', seed, n_train=NTRAIN, n_test=NTEST)
 80    # Retain models to measure predicted-vs-observed behavior, not an analytic identity.
 81    base_net, base_metric, _ = train_on_indices(ds, cfg)
 82    idea_m, chosen, idea_net = greedy_compress(ds, cfg)
 83    import torch
 84    with torch.no_grad():
 85        db = next(base_net.parameters()).device
 86        di = next(idea_net.parameters()).device
 87        pb = base_net(ds['xtr'].to(db)).reshape(-1).cpu().numpy()
 88        pi = idea_net(ds['xtr'].to(di)).reshape(-1).cpu().numpy()
 89    y = ds['ytr'].reshape(-1).cpu().numpy()
 90    rb, ri = np.abs(y-pb), np.abs(y-pi)
 91    # Stage-1 prediction: active compression should cover the high-residual tail.
 92    q = float(np.quantile(ri, 1-K/NTRAIN))
 93    selected_tail = float(np.mean(ri[np.asarray(chosen)] >= q))
 94    baseline_tail = float(np.mean(np.sort(rb)[-K:] >= np.quantile(rb, 1-K/NTRAIN)))
 95    return {'prediction': 'greedy selected scenarios cover the high-loss tail',
 96            'observed_selected_tail_fraction': selected_tail,
 97            'observed_baseline_top_tail_fraction': baseline_tail,
 98            'selected_count': len(chosen), 'idea_train_mse': float(np.mean(ri**2)),
 99            'baseline_train_mse': float(np.mean(rb**2)),
100            'confirmed': bool(selected_tail > 0.55)}
101
102
103def main():
104    # Core numerical check first: increasing N contracts the finite-sample proxy.
105    check = [epsilon_scenario(n, 2, 1e-5) for n in (100, 200, 400, 800)]
106    assert all(check[i] > check[i+1] for i in range(3))
107    baseline = sweep_baseline(run_baseline, GRID, seeds=SEEDS)
108    best = baseline['best_cfg']
109    # Three idea settings are exactly the shared three-point grid; report best.
110    idea_trials = []
111    for cfg in GRID:
112        r = evaluate(run_idea(cfg), seeds=SEEDS)
113        idea_trials.append({'cfg': cfg, **r})
114    idea_best = min(idea_trials, key=lambda x: x['mean'])
115    idea_res = {k: idea_best[k] for k in ('mean','std','per_seed','n')}
116    diffs = [a-b for a,b in zip(idea_res['per_seed'], baseline['full']['per_seed'])]
117    cmp = make_report('tabular', 'mlp_tiny', baseline, idea_res,
118                      extra=behavior_signature(idea_best['cfg']))
119    cmp['idea']['sweep'] = idea_trials
120    cmp['core_math_check'] = {'proxy_epsilon_N_100_200_400_800': check,
121                              'monotone_decrease': True,
122                              'N': 400, 'k': K, 'beta': 1e-5}
123    cmp['comparison']['paired_delta_mean'] = float(np.mean(diffs))
124    cmp['comparison']['permutation_pvalue'] = float(permutation_pvalue(diffs, n_perm=20000))
125    cmp['bench_report'] = {'track_justification': 'tabular is the harness-matched track for optimizer/training-dynamics/calibration interventions',
126                           'custom_track': None,
127                           'protocol': '8 paired seeds; baseline sweep and idea sweep share the exact lr/epoch union',
128                           'budget_note': 'small 400-sample, 12-epoch MLP; compression uses active-set retraining and final retraining'}
129    Path('bench_report.json').write_text(json.dumps(cmp, indent=2))
130    print(json.dumps(cmp, indent=2))
131
132if __name__ == '__main__': main()