Residual-Scenario Safety Training / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4
  5SEED = 535
  6np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  7try:
  8    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  9except Exception:
 10    device = torch.device('cpu')
 11
 12# Scalar box-constrained output: y in [-1, 1]. The feature map is frozen;
 13# only the affine output head is trained, so the nominal objective is convex.
 14def make_data(n, noise, rng):
 15    x = rng.uniform(-1.0, 1.0, size=(n, 1)).astype(np.float32)
 16    y_clean = (0.82*x[:, 0] + 0.03*np.sin(3*x[:, 0])).astype(np.float32)
 17    y_obs = y_clean + rng.normal(0, noise, size=n).astype(np.float32)
 18    return x, y_obs, y_clean
 19
 20def fit_nominal(x, y, steps=1000, lr=0.04):
 21    X = torch.tensor(np.c_[x, np.ones(len(x), dtype=np.float32)].astype(np.float32), device=device)
 22    Y = torch.tensor(y[:, None], device=device)
 23    w = torch.zeros(2, 1, device=device, requires_grad=True)
 24    opt = torch.optim.Adam([w], lr=lr)
 25    for _ in range(steps):
 26        opt.zero_grad(); pred = X @ w
 27        loss = ((pred-Y)**2).mean() + 1e-4*(w[0]**2)
 28        loss.backward(); opt.step()
 29    return w.detach()
 30
 31def fit_scenario(x, y, residuals, mu=3.0, scen=48, steps=1200, lr=0.025):
 32    X = torch.tensor(np.c_[x, np.ones(len(x), dtype=np.float32)].astype(np.float32), device=device)
 33    Y = torch.tensor(y[:, None], device=device)
 34    R = torch.tensor(residuals[:, None], device=device)
 35    w = torch.zeros(2, 1, device=device, requires_grad=True)
 36    opt = torch.optim.Adam([w], lr=lr)
 37    g = torch.Generator(device=device); g.manual_seed(SEED+7)
 38    for _ in range(steps):
 39        opt.zero_grad(); pred = X @ w
 40        # Bootstrap residual scenarios independently for each minibatch item.
 41        idx = torch.randint(R.shape[0], (scen, X.shape[0]), generator=g, device=device)
 42        s = R[idx].squeeze(-1).T  # [batch, scenarios]
 43        ys = pred + s
 44        h = torch.relu(-1.0-ys) + torch.relu(ys-1.0)
 45        loss = ((pred-Y)**2).mean() + mu*h.mean() + 1e-4*(w[0]**2)
 46        loss.backward(); opt.step()
 47    return w.detach()
 48
 49def predict(w, x):
 50    X = torch.tensor(np.c_[x, np.ones(len(x), dtype=np.float32)].astype(np.float32), device=device)
 51    return (X @ w).detach().cpu().numpy()[:, 0]
 52
 53def violation_metrics(pred, clean, noise, rng):
 54    # Independent held-out disturbance distribution, with repeated draws to
 55    # estimate rare violations without conflating them with training labels.
 56    draws = clean[:, None] + rng.normal(0, noise, size=(len(clean), 80))
 57    # Predictor is nominal; each actual output is prediction error scenario.
 58    errors = draws - clean[:, None]
 59    actual = pred[:, None] + errors
 60    viol = np.maximum(0, -1-actual) + np.maximum(0, actual-1)
 61    return {
 62        'nominal_mse_to_clean': float(np.mean((pred-clean)**2)),
 63        'p95_abs_error': float(np.quantile(np.abs(pred[:, None]-draws), .95)),
 64        'trajectory_sample_violation_rate': float(np.mean(viol > 0)),
 65        'mean_slack': float(np.mean(viol)),
 66        'p99_slack': float(np.quantile(viol, .99)),
 67    }
 68
 69def exact_slack_check():
 70    # For each scenario, min_{h>=0} mu*h subject to y+s <= 1+h and
 71    # -1-h <= y+s is attained at h=max(0, |y+s|-1).
 72    vals = np.array([-1.4, -1.0, -0.2, 0.7, 1.0, 1.25])
 73    mu = 2.7
 74    eliminated = mu*np.maximum(0, np.abs(vals)-1)
 75    # brute force grid minimization of the defining one-dimensional problem
 76    brute = []
 77    for v in vals:
 78        hs = np.linspace(0, 1.5, 30001)
 79        feasible = (v <= 1+hs) & (v >= -1-hs)
 80        brute.append(np.min(np.where(feasible, mu*hs, np.inf)))
 81    brute = np.array(brute)
 82    return {'max_abs_error': float(np.max(np.abs(brute-eliminated))),
 83            'slacks': eliminated.tolist(), 'passed': bool(np.max(np.abs(brute-eliminated)) < 1e-4)}
 84
 85def main():
 86    rng = np.random.default_rng(SEED)
 87    xtr, ytr, cleantr = make_data(700, .10, rng)
 88    xte, yte, cleante = make_data(5000, .10, rng)
 89    # Residual buffer from observed prediction errors of a nominal fitted head.
 90    w0 = fit_nominal(xtr, ytr)
 91    residual_buffer = ytr - predict(w0, xtr)
 92    wb = fit_nominal(xtr, ytr)
 93    ws = fit_scenario(xtr, ytr, residual_buffer)
 94    result = {
 95      'device': str(device), 'exact_slack_check': exact_slack_check(),
 96      'baseline': violation_metrics(predict(wb,xte), cleante, .10, np.random.default_rng(SEED+2)),
 97      'residual_scenario': violation_metrics(predict(ws,xte), cleante, .10, np.random.default_rng(SEED+2)),
 98      'weights': {'baseline': wb.cpu().numpy().ravel().tolist(), 'scenario': ws.cpu().numpy().ravel().tolist()},
 99      'buffer': {'n': int(len(residual_buffer)), 'mean': float(residual_buffer.mean()), 'std': float(residual_buffer.std())}
100    }
101    print(json.dumps(result, indent=2))
102
103if __name__ == '__main__': main()