ProCreations's picture
Claim 4: run the 400x800 zero-sum comparison independently; second-order reaches gap 8.2e-14 in 1.10s vs first-order plateauing at 1.1e-5 (1.4e8x worse)
13e208e verified
Raw
History Blame Contribute Delete
4.14 kB
"""Claim 4: on 400x800 random zero-sum games, second-order (PSSN-style) methods
reach a duality gap of ~1e-12 in a few seconds.
The logbook checked this only as a source-reported figure. The claim has two
measurable parts -- the ACHIEVABLE GAP and the TIME -- and a second-order method
should separate sharply from a first-order one on the first of these.
Zero-sum game: min_x max_y x^T A y over the simplices. Duality gap at (x,y):
gap = max_j (A^T x)_j - min_i (A y)_i .
Compared: extragradient / mirror-prox (first order) against an interior-point
solve of the equivalent LP (second order, Newton steps on the KKT system).
"""
import json, time, numpy as np
from scipy.optimize import linprog
RES = {}
def game(m, n, seed):
return np.random.default_rng(seed).normal(size=(m, n))
def gap(A, x, y):
return float((A.T @ x).max()-(A @ y).min())
def simplex_proj(v):
u = np.sort(v)[::-1]; c = np.cumsum(u)-1
r = np.arange(1, len(v)+1)
rho = np.nonzero(u-c/r > 0)[0][-1]
return np.maximum(v-c[rho]/(rho+1), 0)
def first_order(A, iters=20000):
m, n = A.shape
x = np.ones(m)/m; y = np.ones(n)/n
L = np.linalg.norm(A, 2); eta = 0.9/max(L, 1e-12)
t0 = time.perf_counter()
for _ in range(iters):
xh = simplex_proj(x-eta*(A @ y)); yh = simplex_proj(y+eta*(A.T @ x))
x = simplex_proj(x-eta*(A @ yh)); y = simplex_proj(y+eta*(A.T @ xh))
return gap(A, x, y), time.perf_counter()-t0
def second_order(A):
"""LP form solved by interior point (Newton on the KKT system)."""
m, n = A.shape
t0 = time.perf_counter()
# min_x max_j (A^T x)_j -> min t s.t. A^T x <= t 1, sum x = 1, x >= 0
c = np.zeros(m+1); c[-1] = 1.0
Aub = np.hstack([A.T, -np.ones((n, 1))])
Aeq = np.zeros((1, m+1)); Aeq[0, :m] = 1.0
r = linprog(c, A_ub=Aub, b_ub=np.zeros(n), A_eq=Aeq, b_eq=[1.0],
bounds=[(0, None)]*m+[(None, None)], method="highs-ipm")
x = r.x[:m]
c2 = np.zeros(n+1); c2[-1] = -1.0
Aub2 = np.hstack([-A, np.ones((m, 1))])
Aeq2 = np.zeros((1, n+1)); Aeq2[0, :n] = 1.0
r2 = linprog(c2, A_ub=Aub2, b_ub=np.zeros(m), A_eq=Aeq2, b_eq=[1.0],
bounds=[(0, None)]*n+[(None, None)], method="highs-ipm")
y = r2.x[:n]
el = time.perf_counter()-t0
return gap(A, x, y), el
def main():
rows = []
for (m, n) in ((100, 200), (200, 400), (400, 800)):
for s in range(3):
A = game(m, n, seed=s)
g1, t1 = first_order(A)
g2, t2 = second_order(A)
rows.append({"m": m, "n": n, "seed": s,
"first_order_gap": g1, "first_order_sec": round(t1, 3),
"second_order_gap": g2, "second_order_sec": round(t2, 3)})
sub = [r for r in rows if r["m"] == m]
print(" %dx%d first-order: gap=%.3e in %.2fs | second-order: gap=%.3e in %.2fs"
% (m, n, np.mean([r["first_order_gap"] for r in sub]), np.mean([r["first_order_sec"] for r in sub]),
np.mean([r["second_order_gap"] for r in sub]), np.mean([r["second_order_sec"] for r in sub])), flush=True)
big = [r for r in rows if r["m"] == 400]
RES["claim4_pssn"] = {"rows": rows, "seeds": 3,
"target_size": "400x800",
"second_order_gap_400x800": float(np.mean([r["second_order_gap"] for r in big])),
"second_order_sec_400x800": round(float(np.mean([r["second_order_sec"] for r in big])), 3),
"first_order_gap_400x800": float(np.mean([r["first_order_gap"] for r in big])),
"gap_ratio": float(np.mean([r["first_order_gap"] for r in big])/max(np.mean([r["second_order_gap"] for r in big]), 1e-300)),
"paper_reported": {"gap": 1e-12, "seconds": "4.3-4.4"}}
R = RES["claim4_pssn"]
print(" 400x800: second-order gap %.3e in %.2fs (paper: 1e-12 in 4.3-4.4s); first-order gap %.3e -> %.1e x worse"
% (R["second_order_gap_400x800"], R["second_order_sec_400x800"],
R["first_order_gap_400x800"], R["gap_ratio"]), flush=True)
json.dump(RES, open("pssn_results.json", "w"), indent=1)
if __name__ == "__main__":
main(); print("DONE")