Moment-Controlled Mutation / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, copy, random
  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
  6import torch
  7
  8TRACK = 'tabular'
  9MODEL = 'mlp_tiny'
 10N = 8
 11GENERATIONS = 16
 12SEEDS = tuple(range(8))
 13SWEEP_SEEDS = tuple(range(4))
 14# Shared union: every idea learning rate is also evaluated by baseline.
 15GRID = [
 16    {'lr': 1e-3, 'weight_decay': 0.0},
 17    {'lr': 3e-3, 'weight_decay': 0.0},
 18    {'lr': 1e-2, 'weight_decay': 0.0},
 19]
 20
 21
 22def seed_all(seed):
 23    random.seed(seed)
 24    np.random.seed(seed)
 25    torch.manual_seed(seed)
 26    if torch.cuda.is_available():
 27        torch.cuda.manual_seed_all(seed)
 28
 29
 30def baseline_one(cfg, seed):
 31    seed_all(seed)
 32    d = get_dataset(TRACK, seed, n_train=400, n_test=400)
 33    net = make_model(MODEL, d['input_shape'], d['out_dim'])
 34    net, metric, history = train_model(
 35        net, d, epochs=GENERATIONS, lr=cfg['lr'], batch=128,
 36        weight_decay=cfg['weight_decay'], log=lambda *_: None)
 37    return float(metric) if metric is not None else float('inf')
 38
 39
 40def flat(net):
 41    return torch.cat([p.detach().reshape(-1) for p in net.parameters()])
 42
 43
 44def put(net, v):
 45    pos = 0
 46    with torch.no_grad():
 47        for p in net.parameters():
 48            n = p.numel()
 49            p.copy_(v[pos:pos+n].reshape_as(p))
 50            pos += n
 51
 52
 53def train_candidate(net, vec, x, y, lr):
 54    """One identical SGD update for each population member."""
 55    put(net, vec)
 56    net.zero_grad(set_to_none=True)
 57    loss = torch.mean((net(x) - y) ** 2)
 58    loss.backward()
 59    with torch.no_grad():
 60        out = torch.cat([(p - lr * p.grad).reshape(-1) for p in net.parameters()])
 61    return out, float(loss.detach().cpu())
 62
 63
 64def idea_one(cfg, seed, return_trace=False):
 65    seed_all(seed)
 66    d = get_dataset(TRACK, seed, n_train=400, n_test=400)
 67    net = make_model(MODEL, d['input_shape'], d['out_dim'])
 68    x, y = d['xtr'], d['ytr']
 69    center = flat(net).clone()
 70    # A priori fixed controls from the proposal.
 71    dmin, dmax, gamma, target = 1e-7, 3e-4, 0.20, 2e-4
 72    kappa = 1.5
 73    rng = np.random.default_rng(seed + 991)
 74    population = center[None, :] + 0.02 * torch.randn((N, center.numel()))
 75    records = []
 76    for step in range(GENERATIONS):
 77        rewards = []
 78        updated = []
 79        for i in range(N):
 80            # Candidate training is the population optimizer's training step.
 81            v, train_loss = train_candidate(net, population[i], x, y, cfg['lr'])
 82            updated.append(v)
 83            rewards.append(-train_loss)
 84        population = torch.stack(updated)
 85        r = np.asarray(rewards, dtype=np.float64)
 86        # Stable normalized selection weights.
 87        w = np.exp(kappa * (r - r.max()))
 88        w /= w.sum()
 89        z = population.detach().cpu().numpy()
 90        mu = np.sum(w[:, None] * z, axis=0)
 91        c = np.sum(w[:, None] * (z - mu) ** 2, axis=0)
 92        q = (z - mu) ** 2 - c
 93        rbar = float(np.sum(w * r))
 94        r2 = np.sum(w[:, None] * (r[:, None] - rbar) * q, axis=0) / (np.sum(w[:, None] * q*q, axis=0) + 1e-8)
 95        r2 = np.clip(r2, -8, 8)
 96        rv = float(np.sum(w * (r-rbar)**2))
 97        D = np.clip(dmin + gamma * rv / (1 + np.abs(r2)), dmin, dmax)
 98        ess = float(1 / np.sum(w*w))
 99        # Diversity safeguard, reflecting +2D in the variance equation.
100        D = np.maximum(D, np.maximum(0, (target-c) / 2.0))
101        D = np.clip(D, dmin, dmax)
102        # Resample selected candidates, then apply coordinatewise diffusion.
103        idx = rng.choice(N, size=N, p=w)
104        noise = torch.as_tensor(rng.normal(size=(N, center.numel())), dtype=population.dtype)
105        population = population[idx] + noise * torch.as_tensor(np.sqrt(2*D), dtype=population.dtype)[None, :]
106        if step == GENERATIONS-1 or step % 4 == 0:
107            records.append({'step': step, 'cov_trace': float(np.mean(c)), 'ess': ess,
108                            'reward_var': rv, 'mutation_D': float(np.mean(D)),
109                            'curvature_median': float(np.median(r2))})
110    # Evaluate the trained selected system, not a readout of baseline weights.
111    final_w = np.exp(kappa * (r-r.max())); final_w /= final_w.sum()
112    best = population[int(np.argmax(final_w))]
113    put(net, best)
114    with torch.no_grad():
115        metric = float(torch.mean((net(d['xte'])-d['yte'])**2).item())
116    return (metric, records) if return_trace else metric
117
118
119def mechanism_signature(cfg, seed=0):
120    metric, trace = idea_one(cfg, seed, True)
121    # Re-test the claimed additive diffusion relation on trained NN parameters:
122    # measured coordinate variance increment from mutation versus predicted 2D.
123    if len(trace) >= 2:
124        a, b = trace[-2], trace[-1]
125        observed = b['cov_trace'] - a['cov_trace']
126        predicted = 2.0 * b['mutation_D']
127    else:
128        observed = predicted = float('nan')
129    rel = abs(observed-predicted)/(abs(predicted)+1e-12)
130    return {'metric': metric, 'observed_cov_increment': observed,
131            'predicted_2D_increment': predicted, 'relative_error': rel,
132            'curvature_median_final': trace[-1]['curvature_median'],
133            'confirmed': bool(np.isfinite(rel) and rel < 0.50)}
134
135
136def main():
137    # Baseline sweep uses the same three lr values and a final eight-seed rerun.
138    base = sweep_baseline(
139        lambda cfg: (lambda seed: baseline_one(cfg, seed)), GRID, seeds=SWEEP_SEEDS)
140    idea_trials = []
141    for cfg in GRID:
142        res = evaluate(lambda seed, cfg=cfg: idea_one(cfg, seed), seeds=SEEDS)
143        idea_trials.append({'cfg': cfg, 'result': res})
144    best_trial = min(idea_trials, key=lambda x: x['result']['mean'])
145    sig = mechanism_signature(best_trial['cfg'], seed=0)
146    report = make_report(TRACK, MODEL, base, best_trial['result'], {
147        'mechanism_signature': sig,
148        'track_choice': 'tabular is the built-in optimizer/training-dynamics match; Friedman regression keeps architecture and task fixed.',
149        'idea_sweep': idea_trials,
150        'budget': {'baseline_epochs': GENERATIONS, 'idea_generations': GENERATIONS, 'population': N}
151    })
152    report['idea']['best_cfg'] = best_trial['cfg']
153    report['mechanism_signature']['trained_model_metric'] = sig.pop('metric')
154    Path('bench_report.json').write_text(json.dumps(report, indent=2))
155    print(json.dumps(report, indent=2))
156
157
158if __name__ == '__main__':
159    main()