Normal-Cone Certified Priority Weighting / experiment.py
Failed on benchmark
1import json, math, random
2from pathlib import Path
3import numpy as np
4from scipy.optimize import linprog
5
6SEED = 2189
7np.random.seed(SEED)
8random.seed(SEED)
9
10
11def certificate(q, S, epsilon, wmax=100.0):
12 """Maximize delta subject to w_i >= delta and ||q+S w||inf <= epsilon."""
13 q = np.asarray(q, dtype=float)
14 S = np.asarray(S, dtype=float)
15 d, L = len(q), S.shape[1]
16 A, b = [], []
17 for i in range(L):
18 row = np.zeros(L + 1); row[i] = -1; row[-1] = 1
19 A.append(row); b.append(0.)
20 for k in range(d):
21 row = np.zeros(L + 1); row[:L] = S[k]
22 A.append(row); b.append(epsilon - q[k])
23 row = np.zeros(L + 1); row[:L] = -S[k]
24 A.append(row); b.append(epsilon + q[k])
25 result = linprog(np.r_[np.zeros(L), -1.], A_ub=np.array(A), b_ub=np.array(b),
26 bounds=[(0., wmax)] * L + [(0., wmax)], method="highs")
27 if not result.success:
28 return np.zeros(L), 0., float("inf"), False
29 w = result.x[:L]
30 return w, float(result.x[-1]), float(np.max(np.abs(q + S @ w))), True
31
32
33def mechanism_checks():
34 # Prediction 1: for S=lambda I, q=(-3,-1), exact weights are (3/lambda,1/lambda),
35 # hence delta=1/lambda until the wmax ceiling is active.
36 lambdas = np.array([.25, .5, 1., 2., 4.])
37 rows1 = []
38 for lam in lambdas:
39 w, delta, residual, ok = certificate([-3., -1.], lam*np.eye(2), 1e-9, 100.)
40 rows1.append(dict(lambda_=float(lam), predicted_delta=float(1/lam),
41 observed_delta=delta, weights=w.tolist(), residual=residual))
42
43 # Prediction 2: feasibility transition for scalar q=-3, S=1: delta can be positive
44 # iff epsilon >= 3-wmax; for wmax=2 this predicts epsilon*=1.
45 epsilons = np.array([.25, .75, 1., 1.25, 2.])
46 rows2 = []
47 for eps in epsilons:
48 w, delta, residual, ok = certificate([-3.], [[1.]], eps, 2.)
49 rows2.append(dict(epsilon=float(eps), predicted_positive=bool(eps >= 1.),
50 observed_positive=bool(delta > 1e-8), delta=delta, weight=w.tolist()))
51
52 # Prediction 3: increasing slack epsilon permits a larger common margin for
53 # q=(-2,-1), S=I; delta=1+eps (the upper residual bound on the second coordinate) in this unconstrained-wmax sweep.
54 eps3 = np.array([0., .25, .5, .75, 1.])
55 rows3 = []
56 for eps in eps3:
57 w, delta, residual, ok = certificate([-2., -1.], np.eye(2), eps, 100.)
58 predicted = 1. + eps
59 rows3.append(dict(epsilon=float(eps), predicted_delta=predicted,
60 observed_delta=delta, residual=residual))
61 return {"scaling_lambda": rows1, "feasibility_transition": rows2,
62 "slack_margin": rows3}
63
64
65def hinge(v):
66 return np.maximum(v, 0.)
67
68
69def objective(z, target, thresholds, weights):
70 # Convex 2-D last-layer problem: performance J=1/2||z-target||2,
71 # ordered constraints V_i=[z_i-threshold_i]_+ (each tier acts on one coordinate).
72 J = .5 * np.sum((z-target)**2)
73 V = hinge(z-np.asarray(thresholds))
74 return float(J + np.dot(weights, V)), float(J), V
75
76
77def train_controller(mode, seed=SEED, steps=160, refresh=8):
78 rng=np.random.default_rng(seed)
79 target=np.array([1.0, 1.0, 1.0])
80 thresholds=np.array([.2, .4, .6])
81 z=np.zeros(3)
82 weights=np.ones(3)
83 history=[]
84 for t in range(steps):
85 if mode == "fixed": weights=np.array([100.,10.,1.])
86 elif mode == "equal": weights=np.ones(3)
87 elif mode == "certificate" and t % refresh == 0:
88 # q = grad J and columns are subgradients of hinge tiers at current z.
89 q=z-target
90 S=np.eye(3) * (z-thresholds > 0.)
91 neww, delta, residual, ok=certificate(q, S, epsilon=.05, wmax=100.)
92 if ok and delta > 1e-5: weights=neww
93 history.append((t, delta, residual, weights.copy()))
94 # subgradient descent on weighted objective
95 grad=z-target + weights*(z-thresholds > 0.)
96 z -= .08*grad
97 J=.5*np.sum((z-target)**2); V=hinge(z-thresholds)
98 history.append((t, J, float(np.sum(V)), float(V[0]), float(np.dot(weights,V))))
99 return z, history
100
101
102def summarize():
103 checks=mechanism_checks()
104 comparison={}
105 for mode in ["equal", "fixed", "certificate"]:
106 z,h=train_controller(mode)
107 comparison[mode]={"final_z":z.tolist(), "final_J":float(.5*np.sum((z-np.array([1.,1.,1.]))**2)),
108 "final_total_violation":float(np.sum(hinge(z-np.array([.2,.4,.6])))),
109 "final_high_priority_violation":float(hinge(z[0]-.2))}
110 # Relative numerical errors for the predicted equations.
111 e1=max(abs(r["observed_delta"]-r["predicted_delta"])/max(r["predicted_delta"],1e-12)
112 for r in checks["scaling_lambda"])
113 e3=max(abs(r["observed_delta"]-r["predicted_delta"]) for r in checks["slack_margin"])
114 transition=all(r["observed_positive"] == r["predicted_positive"] for r in checks["feasibility_transition"])
115 out={"seed":SEED, "mechanism_checks":checks,
116 "check_summary":{"lambda_scaling_max_relative_error":e1,
117 "transition_all_correct":transition,
118 "slack_margin_max_absolute_error":e3},
119 "training_comparison":comparison}
120 Path("results.json").write_text(json.dumps(out, indent=2))
121 print(json.dumps(out, indent=2))
122
123if __name__ == "__main__":
124 summarize()