Pick-to-Learn Safety Fine-Tuning / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5
  6SEED = 1217
  7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  8torch.set_num_threads(4)
  9
 10# One-step stochastic point-mass abstraction. z is a severity/initial-state feature.
 11# The policy is u=theta*z; task optimum theta=1, while safety requires theta*z <= c.
 12# Thus g(theta,z)=theta*z-c and v=[g]_+ (scale s=1).
 13C = 0.62
 14B = 32
 15ROUNDS = 18
 16UPDATES = 12
 17BETA = 10.0
 18LR = 0.045
 19T = 0.035
 20
 21def soft_penalty(g):
 22    return torch.nn.functional.softplus(g / T).pow(2) * T*T
 23
 24def update(theta, z, picked=False):
 25    # z is a tensor of trajectories in the current uniform batch or constraint buffer.
 26    theta = theta.detach().clone().requires_grad_(True)
 27    g = theta * z - C
 28    task = (theta - 1.0).pow(2)
 29    loss = task + BETA * soft_penalty(g).mean()
 30    loss.backward()
 31    with torch.no_grad():
 32        theta -= LR * theta.grad
 33    return theta.detach(), float(loss.detach())
 34
 35def run(method, seed=SEED, record=False):
 36    rng = np.random.default_rng(seed)
 37    theta = torch.tensor(1.0)
 38    buffer = []
 39    history = []
 40    probe = torch.linspace(0, 1, 4001)
 41    for m in range(ROUNDS):
 42        z_np = rng.random(B)
 43        z = torch.tensor(z_np, dtype=torch.float32)
 44        v_np = np.maximum(theta.item() * z_np - C, 0)
 45        if method == 'pick':
 46            # The exact argmax trajectory is retained, as prescribed by C_{m+1}.
 47            j = int(np.argmax(v_np))
 48            buffer.append(float(z_np[j]))
 49            train_z = torch.tensor(buffer, dtype=torch.float32)
 50        else:
 51            train_z = z
 52        for _ in range(UPDATES):
 53            theta, _ = update(theta, train_z)
 54        probe_v = torch.relu(theta * probe - C)
 55        history.append({'round': m+1, 'theta': float(theta),
 56                        'batch_max_v': float(v_np.max()),
 57                        'probe_max_v': float(probe_v.max())})
 58    return theta.item(), history
 59
 60def certify(theta, seed=991):
 61    rng = np.random.default_rng(seed)
 62    out = []
 63    delta = .05
 64    for n in [50, 100, 200, 400, 600, 1200, 2400]:
 65        z = rng.random(n)
 66        v = np.maximum(theta*z-C, 0)
 67        phat = float(np.mean(v > 0))
 68        eps = math.sqrt(math.log(1/delta)/(2*n))
 69        out.append({'N': n, 'phat': phat, 'upper': min(1., phat+eps), 'epsilon': eps})
 70    # The correction itself is the claimed O(N^-1/2) object.
 71    x = np.log([r['N'] for r in out]); y = np.log([r['epsilon'] for r in out])
 72    slope = float(np.polyfit(x, y, 1)[0])
 73    return out, slope
 74
 75def main():
 76    # Prediction 1: for z~U[0,1], E[max z]=B/(B+1), hence selected max
 77    # should approach 1 and exceed the random-batch mean ~1/2.
 78    order_rows = []
 79    for b in [4, 8, 16, 32, 64, 128]:
 80        rng = np.random.default_rng(700+b)
 81        reps = 20000
 82        maxima = rng.random((reps,b)).max(axis=1)
 83        means = rng.random((reps,b)).mean(axis=1)
 84        order_rows.append({'B': b, 'observed_max': float(maxima.mean()),
 85                           'predicted_max': b/(b+1),
 86                           'observed_batch_mean': float(means.mean()),
 87                           'predicted_batch_mean': .5,
 88                           'max_minus_mean': float((maxima-means).mean())})
 89    # Prediction 2: the adaptive penalty should lower the worst-case probe margin
 90    # more than uniform empirical-risk updates at equal rollout budget.
 91    pick_theta, pick_hist = run('pick')
 92    uni_theta, uni_hist = run('uniform')
 93    ztest = np.linspace(0,1,200001)
 94    def eval_theta(th):
 95        v = np.maximum(th*ztest-C,0)
 96        return {'theta': th, 'violation_rate': float(np.mean(v>0)),
 97                'max_violation': float(v.max()), 'mean_violation': float(v.mean())}
 98    # Prediction 3: fixed-policy certificate correction has log-log slope -1/2.
 99    cert, cert_slope = certify(pick_theta)
100    # Independent held-out certification requested by the idea.
101    rng = np.random.default_rng(4242)
102    zh = rng.random(600); vh = np.maximum(pick_theta*zh-C,0)
103    heldout = {'N': 600, 'violations': int(np.sum(vh>0)),
104               'phat': float(np.mean(vh>0)),
105               'upper_95': float(min(1., np.mean(vh>0)+math.sqrt(math.log(20)/(1200))))}
106    result = {
107      'config': {'C': C, 'batch': B, 'rounds': ROUNDS, 'updates': UPDATES,
108                 'beta': BETA, 'lr': LR, 'temperature': T, 'seed': SEED},
109      'prediction_1_order_statistics': order_rows,
110      'prediction_2_equal_budget': {'pick': eval_theta(pick_theta),
111                                     'uniform': eval_theta(uni_theta),
112                                     'pick_history': pick_hist,
113                                     'uniform_history': uni_hist,
114                                     'rollouts_each': B*ROUNDS},
115      'prediction_3_certificate': {'rows': cert, 'observed_loglog_slope': cert_slope,
116                                   'predicted_slope': -0.5, 'heldout_600': heldout},
117      'notes': 'Violation is g=[theta*z-C], with U[0,1] severity and fixed policy during certification.'
118    }
119    Path('results.json').write_text(json.dumps(result, indent=2))
120    print(json.dumps(result, indent=2))
121
122if __name__ == '__main__': main()