Bregman-Projection Polyak Optimizer / experiment.py
Failed on benchmark
1import json
2import numpy as np
3from pathlib import Path
4
5np.set_printoptions(precision=6, suppress=True)
6
7
8def entropy_point(p, g, lam):
9 """Simplex entropy mirror map: normalize p*exp(-lam*g), stably."""
10 z = np.log(p) - lam * g
11 z -= z.max()
12 w = np.exp(z)
13 return w / w.sum()
14
15
16def entropy_root(p, g, delta, max_expand=80, bisect=80):
17 """Solve g.(p-x(lam))=delta, or return failure if outside reachable range."""
18 if delta <= 0:
19 return 0.0, p.copy(), True
20 gmin = g.min()
21 reachable = float(np.dot(g, p) - gmin)
22 if delta > reachable + 1e-12:
23 return np.nan, p.copy(), False
24 def phi(lam):
25 x = entropy_point(p, g, lam)
26 return float(np.dot(g, p-x) - delta)
27 lo, hi = 0.0, 1.0
28 while phi(hi) < 0 and hi < 1e12:
29 hi *= 2
30 if phi(hi) < 0:
31 return np.nan, p.copy(), False
32 for _ in range(bisect):
33 mid = (lo + hi) / 2
34 if phi(mid) >= 0:
35 hi = mid
36 else:
37 lo = mid
38 lam = (lo + hi) / 2
39 x = entropy_point(p, g, lam)
40 return lam, x, abs(float(np.dot(g, p-x)-delta)) < 1e-10
41
42
43def simplex_project(v):
44 u = np.sort(v)[::-1]
45 cssv = np.cumsum(u) - 1
46 ind = np.arange(1, len(v)+1)
47 rho = np.nonzero(u - cssv / ind > 0)[0][-1]
48 theta = cssv[rho] / (rho + 1)
49 return np.maximum(v-theta, 0)
50
51
52def core_sweeps():
53 # Fixed non-symmetric point and gradient makes the predictions identifiable.
54 p = np.array([0.55, 0.30, 0.15])
55 g = np.array([0.8, -0.3, 1.4])
56 mean = np.dot(p, g)
57 variance = np.dot(p, (g-mean)**2)
58 reachable = np.dot(p, g) - g.min()
59
60 # Prediction 1: as delta -> 0, lambda/delta -> 1/Var_p(g).
61 deltas = np.logspace(-7, -2, 8)
62 small = []
63 for d in deltas:
64 lam, x, ok = entropy_root(p, g, d)
65 small.append((float(d), float(lam), float(lam/d), bool(ok)))
66 asymptotic = 1.0 / variance
67 observed_small = small[0][2]
68
69 # Prediction 2: simultaneous g, delta scaling leaves x unchanged and lambda -> lambda/a.
70 # This is the exact homogeneity law; fixed absolute delta is not invariant.
71 d0 = 0.23 * reachable
72 l1, x1, ok1 = entropy_root(p, g, d0)
73 scaling = []
74 for a in [0.25, 0.5, 1.0, 2.0, 4.0]:
75 la, xa, oka = entropy_root(p, a*g, a*d0)
76 scaling.append({"a": a, "lambda": float(la), "predicted_lambda": float(l1/a),
77 "lambda_ratio": float(la/(l1/a)), "x_error": float(np.max(abs(xa-x1))), "ok": bool(oka)})
78
79 # A local fixed-delta prediction also follows from the Taylor expansion:
80 # lambda/delta ~ 1/(a^2 Var_p(g)) as delta -> 0.
81 fixed_delta_scaling = []
82 tiny_delta = 1e-7
83 for a in [0.25, 0.5, 1.0, 2.0, 4.0]:
84 la, xa, oka = entropy_root(p, a*g, tiny_delta)
85 predicted = tiny_delta / (a*a*variance)
86 fixed_delta_scaling.append({"a": a, "lambda": float(la),
87 "predicted_lambda": float(predicted),
88 "ratio": float(la/predicted), "ok": bool(oka)})
89 # Prediction 3: root exists iff delta <= limiting displacement, and fails above it.
90 boundary = []
91 for frac in [0.0, 0.25, 0.5, 0.9, 0.999999, 1.001, 1.5]:
92 d = frac * reachable
93 lam, x, ok = entropy_root(p, g, d)
94 boundary.append({"fraction": frac, "delta": float(d), "success": bool(ok),
95 "lambda": None if not ok else float(lam)})
96 return {"p": p.tolist(), "g": g.tolist(), "weighted_variance": float(variance),
97 "reachable_delta": float(reachable), "small_gap": small,
98 "small_gap_prediction_lambda_over_delta": float(asymptotic),
99 "small_gap_observed_first": float(observed_small),
100 "scaling": scaling, "fixed_delta_local_scaling": fixed_delta_scaling, "boundary": boundary}
101
102
103def optimization_comparison(seed=7, n=8, steps=120):
104 rng = np.random.default_rng(seed)
105 q = rng.dirichlet(np.ones(n)*1.5)
106 p0 = np.ones(n)/n
107 # f=1/2||p-q||², f*=0, so the exact Polyak gap is known.
108 pe, pu = p0.copy(), p0.copy()
109 ent_losses, euc_losses = [], []
110 ent_resid, euc_resid = [], []
111 for _ in range(steps):
112 for p, kind, losses, residuals in [(pe, 'entropy', ent_losses, ent_resid), (pu, 'euclidean', euc_losses, euc_resid)]:
113 g = p-q
114 f = 0.5*np.dot(g,g)
115 delta = f
116 if kind == 'entropy':
117 lam, x, ok = entropy_root(p, g, delta)
118 if not ok: x = p
119 else:
120 # Euclidean Polyak step, with simplex projection for a fair feasible baseline.
121 alpha = delta / max(np.dot(g,g), 1e-30)
122 x = simplex_project(p-alpha*g)
123 residuals.append(abs(np.dot(g, p-x)-delta))
124 losses.append(f)
125 if kind == 'entropy': pe = x
126 else: pu = x
127 return {"target_q": q.tolist(), "steps": steps,
128 "entropy_loss_start": ent_losses[0], "entropy_loss_final": ent_losses[-1],
129 "euclidean_loss_start": euc_losses[0], "euclidean_loss_final": euc_losses[-1],
130 "entropy_loss_at_10": ent_losses[9], "euclidean_loss_at_10": euc_losses[9],
131 "entropy_max_halfspace_residual": max(ent_resid),
132 "euclidean_max_halfspace_residual_after_projection": max(euc_resid)}
133
134
135def main():
136 out = {"core": core_sweeps(), "optimization": optimization_comparison()}
137 Path("results.json").write_text(json.dumps(out, indent=2))
138 print(json.dumps(out, indent=2))
139
140if __name__ == '__main__':
141 main()