import sys, json, random from pathlib import Path import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report SEEDS = tuple(range(8)) # Shared union: every lr and radius is evaluated by both methods. GRID = [ {'lr': 0.0015, 'radius': 0.05}, {'lr': 0.0030, 'radius': 0.05}, {'lr': 0.0060, 'radius': 0.05}, ] EPOCHS = 12 BATCH = 128 WEIGHT_DECAY = 0.0 def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def project_exact(g, R): n = torch.linalg.vector_norm(g) return g * torch.minimum(torch.ones_like(n), torch.as_tensor(R, device=g.device) / (n + 1e-12)) def project_smooth(g, R): # Differentiable radial approximation in the proposal. n = torch.linalg.vector_norm(g) return g / torch.sqrt(1.0 + (n / R) ** 2) def train_limited(seed, cfg, smooth, collect=False): seed_all(seed) ds = get_dataset('dynamics', seed, n_train=400, n_test=160) model = make_model('rnn_small', ds['input_shape'], ds['out_dim']) # Keep the intervention loop otherwise identical to bench.train_model's Adam path. try: device = 'cuda' if torch.cuda.is_available() else 'cpu' model = model.to(device) x, y = ds['xtr'].to(device), ds['ytr'].to(device) opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=WEIGHT_DECAY) lossf = nn.MSELoss() norms, clipped = [], [] for _ in range(EPOCHS): model.train(); perm = torch.randperm(len(x), device=device) for i in range(0, len(x), BATCH): idx = perm[i:i+BATCH] loss = lossf(model(x[idx]), y[idx]); opt.zero_grad(); loss.backward() grad = torch.cat([p.grad.reshape(-1) for p in model.parameters() if p.grad is not None]) gn = float(torch.linalg.vector_norm(grad).detach().cpu()); norms.append(gn) newg = project_smooth(grad, cfg['radius']) if smooth else project_exact(grad, cfg['radius']) scale = torch.linalg.vector_norm(newg) / (torch.linalg.vector_norm(grad) + 1e-30) for p in model.parameters(): if p.grad is not None: p.grad.mul_(scale) clipped.append(float(torch.linalg.vector_norm(newg).detach().cpu())) opt.step() model.eval() with torch.no_grad(): pred = model(ds['xte'].to(device)); metric = float(((pred-ds['yte'].to(device))**2).mean().cpu()) if collect: return metric, {'grad_norm_mean': float(np.mean(norms)), 'grad_norm_p95': float(np.percentile(norms,95)), 'applied_norm_mean': float(np.mean(clipped)), 'applied_norm_p95': float(np.percentile(clipped,95)), 'fraction_over_radius': float(np.mean(np.asarray(norms)>cfg['radius']))} return metric except RuntimeError: # Explicit CPU fallback, rebuilding the model and reproducing the same seed. if device == 'cuda': seed_all(seed) ds = get_dataset('dynamics', seed, n_train=400, n_test=160) model = make_model('rnn_small', ds['input_shape'], ds['out_dim']).cpu() old = torch.cuda.is_available torch.cuda.is_available = lambda: False try: return train_limited(seed, cfg, smooth, collect) finally: torch.cuda.is_available = old raise def make_fn(smooth, cfg): return lambda seed: train_limited(seed, cfg, smooth) def main(): # Core math check first: exact boundedness and smooth local gain are measured numerically. torch.manual_seed(11) checks = [] for n in [0.01, 0.05, 0.2, 1.0]: v = torch.randn(257, dtype=torch.float64); v *= n / torch.linalg.vector_norm(v) ex = project_exact(v, 0.05); sm = project_smooth(v, 0.05) checks.append({'input_norm': n, 'exact_norm': float(torch.linalg.vector_norm(ex)), 'smooth_norm': float(torch.linalg.vector_norm(sm)), 'exact_bound_ok': bool(torch.linalg.vector_norm(ex) <= 0.05 + 1e-12)}) # Baseline sweep over the full shared union, then final full-seed evaluation. base = sweep_baseline(lambda cfg: make_fn(False, cfg), GRID, seeds=(0,1,2,3)) idea_candidates = [] for cfg in GRID: r = evaluate(make_fn(True, cfg), seeds=SEEDS) idea_candidates.append((r, cfg)) idea, idea_cfg = min(idea_candidates, key=lambda z: z[0]['mean']) # Train one paired model at selected setting for behavior signature. bmet, bsig = train_limited(0, idea_cfg, False, True) imet, isig = train_limited(0, idea_cfg, True, True) signature = { 'prediction': 'smooth clipping can apply a different local gain and create larger gradients than exact projection near the boundary', 'observed': {'cfg': idea_cfg, 'baseline_seed0_metric': bmet, 'idea_seed0_metric': imet, 'baseline': bsig, 'idea': isig, 'smooth_applied_norm_exceeds_radius': bool(isig['applied_norm_p95'] > idea_cfg['radius'])}, 'confirmed': bool(isig['applied_norm_p95'] > idea_cfg['radius'] and bsig['applied_norm_p95'] <= idea_cfg['radius'] + 1e-7) } report = make_report('dynamics', 'rnn_small', base, idea, extra=signature) report['idea_grid'] = [{'cfg': c, 'full': r} for r,c in idea_candidates] report['math_sanity'] = checks report['protocol'] = {'epochs': EPOCHS, 'batch': BATCH, 'paired_seeds': list(SEEDS), 'track_reason': 'dynamics matches optimizer stability and bifurcation structure'} Path('bench_report.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2)) if __name__ == '__main__': main()