txus/lsr1-artifacts / repro_bundle /code /eval_analytic.py
txus's picture
download
raw
11.5 kB
"""Evaluation for the analytic experiments.
Produces the evidence for:
* Claim 2 -- learned projection: secant residual + Newton alignment, on vs off.
* Claim 4 -- performance profiles over the 6-function x 5-dim suite, and
Newton-direction alignment vs L-BFGS on quadratics.
* Claim 5 -- (buffer L, wall-clock recorded by the trainer).
L-SR1 runs batched (dimension-invariant); baselines run per problem.
"""
from __future__ import annotations
import argparse
import json
import numpy as np
import torch
from lsr1 import (LSR1Config, LSR1Optimizer, Quadratic, Rastrigin, Rosenbrock,
make_quadratics, init_points, rollout)
from baselines import (run_adam, run_lbfgs, run_adahessian, iters_to_target,
performance_profile, auc)
def load_model(ckpt_path, device="cpu", buffer_L=None, K=None):
ck = torch.load(ckpt_path, map_location=device, weights_only=False)
cfg = LSR1Config(**ck["cfg"])
if buffer_L is not None:
cfg.buffer_L = buffer_L # larger test-time buffer (Table 6)
if K is not None:
cfg.K = K
opt = LSR1Optimizer(cfg).to(device)
opt.load_state_dict(ck["state_dict"])
opt.eval()
return opt, cfg
def f_star(problem, x0):
if isinstance(problem, Quadratic):
xs = problem.optimum()
return problem.value(xs).detach()
# Rosenbrock min 0 at ones, Rastrigin min 0 at zero
return torch.zeros(x0.shape[0], device=x0.device)
# ---------------------------------------------------------------------------
# Newton-direction alignment on quadratics (Claim 2 & 4)
# ---------------------------------------------------------------------------
def newton_alignment(opt, cfg, N, steps=20, n_problems=64, cond=1000.0,
device="cpu", seed=1):
gen = torch.Generator(device=device).manual_seed(seed)
prob = make_quadratics(n_problems, N, cond=cond, generator=gen,
device=device)
x0 = init_points(n_problems, N, "quadratic", generator=gen, device=device)
with torch.enable_grad():
out = rollout(opt, prob, x0, K=steps, create_graph=False, record=True)
traj = out["traj"] # [steps+1, B, N]
dirs = out["dirs"] # [steps, B, N] (actual update = -step)
cos = []
for k in range(steps):
xk = traj[k]
nd = prob.newton_dir(xk) # Newton *step* direction
d = dirs[k]
c = torch.nn.functional.cosine_similarity(d, nd, dim=-1)
cos.append(float(c.mean()))
return cos
def lbfgs_newton_alignment(N, steps=20, n_problems=64, cond=1000.0,
device="cpu", seed=1):
gen = torch.Generator(device=device).manual_seed(seed)
prob = make_quadratics(n_problems, N, cond=cond, generator=gen,
device=device)
x0 = init_points(n_problems, N, "quadratic", generator=gen, device=device)
cos_per_iter = [[] for _ in range(steps)]
for i in range(n_problems):
pi = Quadratic(prob.H[i:i+1], prob.b[i:i+1])
x = x0[i:i+1].clone().detach().requires_grad_(True)
opt = torch.optim.LBFGS([x], lr=1.0, max_iter=1, history_size=10,
line_search_fn="strong_wolfe")
for k in range(steps):
x_prev = x.detach().clone()
nd = pi.newton_dir(x_prev)
def closure():
opt.zero_grad(); loss = pi.value(x).sum(); loss.backward()
return loss
opt.step(closure)
d = (x.detach() - x_prev)
c = torch.nn.functional.cosine_similarity(d, nd, dim=-1)
cos_per_iter[k].append(float(c.mean()))
return [float(np.mean(c)) for c in cos_per_iter]
# ---------------------------------------------------------------------------
# Performance profiles over the 6-function x 5-dim suite (Claim 4)
# ---------------------------------------------------------------------------
def make_problem(family, N, cond, B, gen, device):
if family.startswith("quad"):
prob = make_quadratics(B, N, cond=cond, diagonal=True, generator=gen,
device=device)
x0 = init_points(B, N, "quadratic", generator=gen, device=device)
elif family == "rosenbrock":
prob = Rosenbrock()
x0 = init_points(B, N, "rosenbrock", generator=gen, device=device)
elif family == "rastrigin":
prob = Rastrigin()
x0 = init_points(B, N, "rastrigin", generator=gen, device=device)
return prob, x0
def lsr1_costs(opt, prob, x0, steps, rtol):
with torch.enable_grad():
out = rollout(opt, prob, x0, K=steps, create_graph=False, record=False)
f_traj = out["f_traj"].detach().cpu().numpy() # [steps, B]
f0 = prob.value(x0).detach().cpu().numpy()
fs = f_star(prob, x0).cpu().numpy()
full = np.concatenate([f0[None, :], f_traj], axis=0) # [steps+1, B]
return np.array([iters_to_target(full[:, i], fs[i], f0[i], rtol)
for i in range(full.shape[1])])
def baseline_costs(runner, prob, x0, steps, rtol, **kw):
costs = []
B = x0.shape[0]
for i in range(B):
pi = (Quadratic(prob.H[i:i+1], prob.b[i:i+1])
if isinstance(prob, Quadratic) else prob)
xi = x0[i:i+1]
traj = runner(pi, xi, steps, **kw)
f0 = float(pi.value(xi))
fs = float(f_star(pi, xi)[0]) if isinstance(pi, Quadratic) else 0.0
costs.append(iters_to_target(traj, fs, f0, rtol))
return np.array(costs)
def performance_suite(models, dims=(50, 100, 250, 500, 1000),
quad_conds=(1.0, 100.0, 1000.0, 10000.0),
instances=5, steps=60, rtol=1e-2, test_L=64,
device="cpu", seed=7, include_hard=True):
"""models: dict family-> (ckpt path). Returns cost dict + profile + per-type."""
gen = torch.Generator(device=device).manual_seed(seed)
problems = []
for c in quad_conds:
for N in dims:
problems.append(("quadratic", N, c))
if include_hard:
for fam in ("rosenbrock", "rastrigin"):
for N in dims:
problems.append((fam, N, None))
loaded = {}
for fam, path in models.items():
loaded[fam] = load_model(path, device=device, buffer_L=test_L)
solver_costs = {"L-SR1": [], "L-BFGS": [], "Adam": [], "AdaHessian": []}
per_type = []
for (family, N, cond) in problems:
prob, x0 = make_problem(family, N, cond, instances, gen, device)
key = "quadratic" if family.startswith("quad") else family
opt, cfg = loaded[key]
c_lsr1 = lsr1_costs(opt, prob, x0, steps, rtol)
c_lbfgs = baseline_costs(run_lbfgs, prob, x0, steps, rtol)
c_adam = baseline_costs(run_adam, prob, x0, steps, rtol, lr=1e-2)
c_adah = baseline_costs(run_adahessian, prob, x0, steps, rtol, lr=0.15)
for s, c in [("L-SR1", c_lsr1), ("L-BFGS", c_lbfgs),
("Adam", c_adam), ("AdaHessian", c_adah)]:
solver_costs[s].append(c)
per_type.append(dict(family=family, N=N, cond=cond,
solved={s: int(np.isfinite(v).sum())
for s, v in [("L-SR1", c_lsr1),
("L-BFGS", c_lbfgs),
("Adam", c_adam),
("AdaHessian", c_adah)]},
median={s: (float(np.median(v[np.isfinite(v)]))
if np.isfinite(v).any() else None)
for s, v in [("L-SR1", c_lsr1),
("L-BFGS", c_lbfgs),
("Adam", c_adam),
("AdaHessian", c_adah)]}))
cost_matrix = {s: np.concatenate(v) for s, v in solver_costs.items()}
taus, profiles = performance_profile(cost_matrix)
aucs = {s: auc(taus, profiles[s]) for s in profiles}
return dict(
taus=taus.tolist(),
profiles={s: profiles[s].tolist() for s in profiles},
aucs=aucs,
per_type=per_type,
problems=[dict(family=f, N=N, cond=c) for (f, N, c) in problems],
)
def convergence_curves(opt, N=10, steps=30, n=200, cond=1000.0, device="cpu",
seed=3):
"""Median f-gap vs iteration for L-SR1 vs L-BFGS/Adam on quadratics."""
gen = torch.Generator(device=device).manual_seed(seed)
prob = make_quadratics(n, N, cond=cond, generator=gen, device=device)
x0 = init_points(n, N, "quadratic", generator=gen, device=device)
fs = f_star(prob, x0).cpu().numpy()
with torch.enable_grad():
out = rollout(opt, prob, x0, K=steps, create_graph=False, record=False)
f_lsr1 = out["f_traj"].detach().cpu().numpy()
f0 = prob.value(x0).detach().cpu().numpy()
lsr1 = np.median(np.concatenate([f0[None], f_lsr1], 0) - fs[None], axis=1)
lb, ad = [], []
for i in range(n):
pi = Quadratic(prob.H[i:i+1], prob.b[i:i+1])
lb.append(run_lbfgs(pi, x0[i:i+1], steps) - fs[i])
ad.append(run_adam(pi, x0[i:i+1], steps, lr=1e-2) - fs[i])
return dict(iters=list(range(steps + 1)),
lsr1=lsr1.tolist(),
lbfgs=np.median(np.stack(lb), 0).tolist(),
adam=np.median(np.stack(ad), 0).tolist())
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--quad2", default="outputs/quad2_proj1.pt")
ap.add_argument("--quad2-noproj", default="outputs/quad2_proj0.pt")
ap.add_argument("--quad100", default="outputs/quad100_proj1.pt")
ap.add_argument("--rosen100", default="outputs/rosen100_proj1.pt")
ap.add_argument("--rastr100", default="outputs/rastr100_proj1.pt")
ap.add_argument("--device", default="cpu")
ap.add_argument("--out", default="outputs/eval.json")
ap.add_argument("--quick", type=int, default=0)
args = ap.parse_args()
dev = args.device
res = {}
# --- Claim 2: projection ablation via Newton alignment + secant ---------
opt_on, cfg_on = load_model(args.quad2, dev)
opt_off, cfg_off = load_model(args.quad2_noproj, dev)
steps = 20
res["newton_alignment"] = dict(
lsr1_proj=newton_alignment(opt_on, cfg_on, N=10, steps=steps, device=dev),
lsr1_noproj=newton_alignment(opt_off, cfg_off, N=10, steps=steps, device=dev),
lbfgs=lbfgs_newton_alignment(N=10, steps=steps, device=dev),
N=10, cond=1000.0,
)
res["convergence"] = convergence_curves(opt_on, N=10, device=dev)
# --- Claim 4: performance profiles --------------------------------------
models = {"quadratic": args.quad100, "rosenbrock": args.rosen100,
"rastrigin": args.rastr100}
dims = (50, 100) if args.quick else (50, 100, 250, 500, 1000)
res["performance"] = performance_suite(models, dims=dims, device=dev,
instances=3 if args.quick else 5,
steps=40 if args.quick else 60)
with open(args.out, "w") as f:
json.dump(res, f, indent=2)
print("wrote", args.out)
print("AUCs:", res["performance"]["aucs"])
na = res["newton_alignment"]
print("Newton align (final iter): proj=%.3f noproj=%.3f lbfgs=%.3f" %
(na["lsr1_proj"][-1], na["lsr1_noproj"][-1], na["lbfgs"][-1]))

Xet Storage Details

Size:
11.5 kB
·
Xet hash:
baeeade19be192daaa5ecbb91071d4c3021a082655d48290c6577fb8bbeaaee7

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.