SabaPivot's picture
download
raw
11 kB
"""CLAIM 2 -- Theorem 3.1: expected static regret
E[R_T(u)] = O~( G eps + (d/kappa) E[ ||u|| sqrt(V_T log_+(d||u||Lambda_T/(G eps))) ] )
with kappa = sqrt(d) in the norm-oblivious setting and kappa = 1 in the norm-adaptive
setting, i.e. an effective dimension factor sqrt(d) resp. d (the paper's "sqrt(d)
separation"; note the paper writes d/kappa, so the SMALLER rate is the oblivious one).
Tests
A batched-vs-reference implementation consistency.
B scaling of the measured E[R_T(u)] in T, d and ||u|| on the hardest known loss family
(hypercube bias, Delta = G/sqrt(T), noise variance G^2/(2d)); fitted log-log exponents
vs the theorem's 1/2, 1/2, 1.
C the bound itself, evaluated with explicit constants dropped, for kappa=sqrt(d) and
kappa=1, on four environments including an adversarial sign-flipping one; also the
risk-control term R_T(0) = O(G eps).
D where kappa comes from: the two moment bounds of Corollary 2.2 measured along real
trajectories (a.s. 4d^2||l||^2 -> factor d; conditional expectation 2d||l||^2 ->
factor sqrt(d)), and the fitted d-exponent of the realised sum_t ||ltilde_t||^2.
E a norm-adaptive adversary that fixes ||u|| only after seeing the realised trajectory:
does the sqrt(d)-worse kappa=1 branch actually get realised?
"""
from __future__ import annotations
import json
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from batch import BatchAlg6, fit_exponent, run_pablo_batch
from pablo import Alg6, env_flip, env_hard, env_rademacher, run_pablo
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
SEED = 20260725
G = 1.0
EPSILON = 1.0 # user-specified risk parameter (the paper's eps; R_T(0) <= O~(G eps))
EPS_PERT = 1e-3 # the varepsilon of Eq. (4)
def make_env(kind, T, d, seed):
rng = np.random.default_rng(seed)
if kind == "hard":
return env_hard(T, d, G, rng)
if kind == "rademacher":
return env_rademacher(T, d, G, rng)
if kind == "flip":
return env_flip(T, d, G, rng)
raise ValueError(kind)
def measure(kind, T, d, S, seed):
losses = make_env(kind, T, d, seed)
L = 2 * d * G # a.s. bound on ||ltilde|| (Cor. 2.2)
olo = BatchAlg6(S, d, T, L, EPSILON / d) # "tuned with eps/d" (Theorem 3.1)
r = run_pablo_batch(losses, olo, d, EPS_PERT, np.random.default_rng(seed + 999), S)
r["clipped"] = olo.clipped
r["V_T"] = float(np.sum(losses * losses))
r["finite"] = bool(np.all(np.isfinite(r["cum_play"])))
return r
def regret_for(r, u):
return r["cum_play"] - float(np.dot(r["sum_losses"], u))
def worst_direction(r):
s = r["sum_losses"]
n = float(np.linalg.norm(s))
return -s / n if n > 0 else np.zeros_like(s)
def bound_thm31(u_norm, V_T, T, d, kappa):
"""Theorem 3.1 shape with unspecified constants set to 1:
G*eps + (d/kappa) ||u|| sqrt( V_T log_+( d ||u|| Lambda_T/(G eps) ) ),
Lambda_T = G sqrt(T) log^2(1+T)."""
Lam = G * np.sqrt(T) * np.log(1 + T) ** 2
lg = max(np.log(max(d * u_norm * Lam / (G * EPSILON), 1.0)), 1.0)
return G * EPSILON + (d / kappa) * u_norm * np.sqrt(V_T * lg)
def test_A_consistency():
d, T, S = 5, 300, 400
losses = make_env("hard", T, d, SEED)
L = 2 * d * G
rb = run_pablo_batch(
losses,
BatchAlg6(S, d, T, L, EPSILON / d),
d,
EPS_PERT,
np.random.default_rng(4242),
S,
)
ref = [
run_pablo(
losses,
Alg6(d, T, L, EPSILON / d),
d,
EPS_PERT,
np.random.default_rng(9000 + s),
)["regret"]
for s in range(400)
]
u = np.zeros(d)
b = regret_for(rb, u)
return dict(
note="independent estimates of E[R_T(0)] from the batched and the reference "
"single-seed code paths on the same loss sequence",
batch_mean=float(b.mean()),
batch_stderr=float(b.std(ddof=1) / np.sqrt(S)),
reference_mean=float(np.mean(ref)),
reference_stderr=float(np.std(ref, ddof=1) / np.sqrt(len(ref))),
)
def test_B_scalings():
out = {}
d, S = 8, 400
Ts = [250, 500, 1000, 2000, 4000, 8000]
means, sems, maxw = [], [], []
for T in Ts:
r = measure("hard", T, d, S, SEED + T)
R = regret_for(r, 1.0 * worst_direction(r))
means.append(float(R.mean()))
sems.append(float(R.std(ddof=1) / np.sqrt(S)))
maxw.append(float(r["max_w"].max()))
sl, _, se = fit_exponent(Ts, means)
out["T_sweep"] = dict(
env="hard",
d=d,
seeds=S,
u_norm=1.0,
T=Ts,
mean_regret=means,
sem=sems,
fitted_exponent=sl,
stderr=se,
predicted=0.5,
max_iterate_norm=maxw,
)
T, S = 2000, 400
ds = [2, 4, 8, 16, 32, 64]
means, sems, lt = [], [], []
for dd in ds:
r = measure("hard", T, dd, S, SEED + 31 * dd)
R = regret_for(r, 1.0 * worst_direction(r))
means.append(float(R.mean()))
sems.append(float(R.std(ddof=1) / np.sqrt(S)))
lt.append(float(np.mean(np.sqrt(r["sum_lt_sq"]))))
sl, _, se = fit_exponent(ds, means)
sl2, _, se2 = fit_exponent(ds, lt)
out["d_sweep"] = dict(
env="hard",
T=T,
seeds=S,
u_norm=1.0,
d=ds,
mean_regret=means,
sem=sems,
fitted_exponent=sl,
stderr=se,
predicted_norm_oblivious=0.5,
predicted_norm_adaptive=1.0,
mean_sqrt_sum_ltilde_sq=lt,
fitted_d_exponent_of_sqrt_sum_ltilde_sq=sl2,
stderr2=se2,
)
d, T, S = 8, 2000, 400
r = measure("hard", T, d, S, SEED + 5)
dirn = worst_direction(r)
norms = [0.25, 0.5, 1, 2, 4, 8, 16, 32, 64]
means, sems, over_log = [], [], []
for un in norms:
R = regret_for(r, un * dirn)
means.append(float(R.mean()))
sems.append(float(R.std(ddof=1) / np.sqrt(S)))
Lam = G * np.sqrt(T) * np.log(1 + T) ** 2
lg = max(np.log(max(d * un * Lam / (G * EPSILON), 1.0)), 1.0)
over_log.append(float(R.mean()) / np.sqrt(lg))
sl, _, se = fit_exponent(norms, means)
sl3, _, se3 = fit_exponent(norms, over_log)
out["u_sweep"] = dict(
env="hard",
d=d,
T=T,
seeds=S,
u_norm=norms,
mean_regret=means,
sem=sems,
fitted_exponent=sl,
stderr=se,
predicted=1.0,
fitted_exponent_after_dividing_by_sqrt_log=sl3,
stderr3=se3,
)
return out
def test_C_bound():
rows = []
grid = [0.25, 0.5, 1, 2, 4, 8, 16, 32, 64, 128]
for kind in ["hard", "rademacher", "flip"]:
for d in [2, 8, 32]:
T, S = 2000, 200
r = measure(kind, T, d, S, SEED + 101 * d + len(kind))
dirn = worst_direction(r)
best_ob, best_ad, arg = -np.inf, -np.inf, None
for un in grid:
m = float(np.mean(regret_for(r, un * dirn)))
ro = m / bound_thm31(un, r["V_T"], T, d, np.sqrt(d))
ra = m / bound_thm31(un, r["V_T"], T, d, 1.0)
if ro > best_ob:
best_ob, arg = ro, un
best_ad = max(best_ad, ra)
R0 = regret_for(r, np.zeros(d))
rows.append(
dict(
env=kind,
d=d,
T=T,
seeds=S,
finite=r["finite"],
clipped=r["clipped"],
max_ratio_to_kappa_sqrt_d_bound=best_ob,
argmax_u_norm=arg,
max_ratio_to_kappa_1_bound=best_ad,
mean_R_T_0=float(R0.mean()),
max_R_T_0=float(R0.max()),
risk_control_budget_G_eps=G * EPSILON,
max_iterate_norm=float(r["max_w"].max()),
)
)
return rows
def test_D_kappa_mechanism():
rows = []
for d in [2, 4, 8, 16, 32, 64]:
T, S = 1000, 200
r = measure("hard", T, d, S, SEED + 77 * d)
V = r["V_T"]
rows.append(
dict(
d=d,
mean_sum_ltilde_sq_over_V_T=float(np.mean(r["sum_lt_sq"])) / V,
max_seed_sum_ltilde_sq_over_V_T=float(np.max(r["sum_lt_sq"])) / V,
Cor22_as_bound_4d2=4.0 * d * d,
Cor22_expectation_bound_2d=2.0 * d,
)
)
sl, _, se = fit_exponent(
[x["d"] for x in rows],
[np.sqrt(x["mean_sum_ltilde_sq_over_V_T"]) for x in rows],
)
return dict(
rows=rows,
fitted_d_exponent_of_realised_dimension_factor=sl,
stderr=se,
predicted_if_kappa_sqrt_d=0.5,
predicted_if_kappa_1=1.0,
)
def test_E_norm_adaptive():
rows = []
grid = np.array([0.25, 0.5, 1, 2, 4, 8, 16, 32, 64, 128])
for d in [2, 4, 8, 16, 32]:
T, S = 2000, 400
r = measure("hard", T, d, S, SEED + 13 * d)
dirn = worst_direction(r)
V = r["V_T"]
bnd = np.array([bound_thm31(un, V, T, d, np.sqrt(d)) for un in grid])
ob = max(
float(np.mean(regret_for(r, un * dirn))) / b for un, b in zip(grid, bnd)
)
per_seed = np.stack([regret_for(r, un * dirn) for un in grid]) # (grid, S)
ratios = np.max(per_seed / bnd[:, None], axis=0)
rows.append(
dict(
d=d,
T=T,
seeds=S,
best_oblivious_norm_ratio=ob,
mean_norm_adaptive_ratio=float(ratios.mean()),
max_norm_adaptive_ratio=float(ratios.max()),
adaptive_over_oblivious=float(ratios.mean()) / ob,
predicted_adaptive_over_oblivious_if_gap_real=float(np.sqrt(d)),
frac_seeds_exceeding_oblivious_bound=float(np.mean(ratios > 1.0)),
)
)
return rows
if __name__ == "__main__":
res = dict(
claim="claim-2 Theorem 3.1 expected static regret",
seed=SEED,
settings=dict(
G=G,
epsilon=EPSILON,
varepsilon_eq4=EPS_PERT,
olo_subroutine="Algorithm 6 of the paper (Appendix E.2); its "
"static specialisation satisfies exactly the parameter-free OLO "
"guarantee that Theorem 3.1 requires of JC22 Algorithm 4",
perturbation="isotropic H_t of Eq. (4), equality case",
),
A_batch_consistency=test_A_consistency(),
B_scalings=test_B_scalings(),
C_bound_check=test_C_bound(),
D_kappa_mechanism=test_D_kappa_mechanism(),
E_norm_adaptive_adversary=test_E_norm_adaptive(),
)
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "claim2_static.json"), "w") as f:
json.dump(res, f, indent=1)
print(json.dumps(res, indent=1))

Xet Storage Details

Size:
11 kB
·
Xet hash:
d2cfd4c9b5ba7a8c352c2466524a17ebaeb67e71ba30f2cd3a52c88ff9cabd81

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