SabaPivot's picture
download
raw
14.9 kB
"""CLAIM 3 -- Theorem 3.3: PABLO attains the optimal sqrt(P_T) dynamic-regret dependence on
the path length P_T *without prior knowledge of P_T*.
The decisive property is the "without prior knowledge" part, so the experiment is built
around it: comparator sequences with a CONTROLLED path length P_T are constructed, P_T is
withheld from the algorithm, and the measured dynamic regret is compared both to the
predicted sqrt(P_T) scaling and to an ORACLE-tuned single-step-size baseline that is given
P_T.
Tests
A Theorem E.6 (the guarantee of Algorithm 5, the base learner of Algorithm 6) checked as
an inequality on random full-information instances; the mis-parsed variant of the update
is also run to show the check is discriminative.
B Theorem E.7 (guarantee of Algorithm 6) checked as an inequality, full information.
C uBLO with K-phase hard environments and matching piecewise-constant comparators:
P_T sweep (fixed T) and T sweep (fixed K), fitted exponents vs the predicted 1/2.
D no-prior-knowledge test: Algorithm 6 (knows nothing) vs Algorithm 5 with the oracle
step size for the true P_T, vs Algorithm 5 with a step size tuned for P_T = 0.
E d-dependence of the dynamic-regret bound (the same d/kappa factor as Theorem 3.1).
"""
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 alg5 import Alg5, phi, run_alg5, thm_E6_bound
from batch import BatchAlg5, BatchAlg6, BatchOGD, fit_exponent, run_pablo_batch
from pablo import Alg6
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
SEED = 20260725
G = 1.0
EPSILON = 1.0
EPS_PERT = 1e-3
# ---------------------------------------------------------------------------------------
# environment: K phases, each an independent hard hypercube instance calibrated to its own
# length; the comparator sequence is piecewise constant and points at each phase's optimum.
# ---------------------------------------------------------------------------------------
def env_phases(T, d, K, M, rng, c=3.0):
losses = np.zeros((T, d))
comps = np.zeros((T, d))
for idx in np.array_split(np.arange(T), K):
Tk = len(idx)
theta = (rng.integers(2, size=d) * 2.0 - 1.0) * (c * G / np.sqrt(Tk))
losses[idx] = theta[None, :] + rng.standard_normal((Tk, d)) * (
G / np.sqrt(2 * d)
)
comps[idx] = -M * theta / np.linalg.norm(theta)
n = np.linalg.norm(losses, axis=1, keepdims=True)
losses = losses * np.minimum(1.0, G / np.maximum(n, 1e-12))
P_T = float(np.sum(np.linalg.norm(comps[1:] - comps[:-1], axis=1)))
return losses, comps, P_T
def test_A_thmE6():
rng = np.random.default_rng(SEED)
n, viol, slack = 0, 0, []
for trial in range(60):
d = int(rng.integers(1, 7))
T = int(rng.integers(50, 400))
L = float(rng.uniform(0.5, 2.0))
g = rng.standard_normal((T, d))
g = L * g / np.linalg.norm(g, axis=1, keepdims=True)
eps = float(rng.uniform(0.1, 2.0))
alpha, gamma = eps / T, L / T
eta = float(rng.uniform(1 / (L * T), 1 / L))
mode = trial % 3
if mode == 0:
u = np.repeat(rng.standard_normal((1, d)) * rng.uniform(0.1, 5), T, axis=0)
elif mode == 1:
u = np.cumsum(rng.standard_normal((T, d)) * 0.05, axis=0)
else:
u = np.zeros((T, d))
for i in np.array_split(np.arange(T), 5):
u[i] = rng.standard_normal(d) * rng.uniform(0.1, 3)
R, _ = run_alg5(g, u, alpha, eta, gamma)
B = thm_E6_bound(u, g, alpha, eta, gamma)
n += 1
viol += int(R > B)
slack.append(float(B - R))
return dict(
trials=n,
violations=viol,
min_slack=float(np.min(slack)),
median_slack=float(np.median(slack)),
)
def test_A2_parse_identifiability():
"""The paper's PDF flattens the fraction in the exponent of Algorithm 5's update. The
parsing used here, exp((eta/k)(...)), is the one implied by the mirror-descent optimality
condition for psi(w) = (k/eta) int log(x/alpha+1) dx. Running the alternative parsing,
exp((k/eta)(...)), on a sign-flipping instance shows the Theorem-E.6 check is
discriminative: the alternative violates the bound by many orders of magnitude."""
d, T, L = 4, 400, 1.0
g = np.zeros((T, d))
g[: T // 2, 0] = -L # first reward scaling up ...
g[T // 2:, 0] = +L # ... then reverse
u = np.zeros((T, d))
alpha, gamma, eta = 1.0 / T, L / T, 1.0 / L
R_ok, _ = run_alg5(g, u, alpha, eta, gamma)
B = thm_E6_bound(u, g, alpha, eta, gamma)
class Alg5Wrong(Alg5):
def update(self, gt):
k, eta_, alpha_, gamma_ = self.k, self.eta, self.alpha, self.gamma
z = self.w - self.w1
nz = float(np.linalg.norm(z))
gp = (k / eta_) * np.log(nz / alpha_ + 1.0) * (z / nz) if nz > 0 else 0.0 * z
th = gp - gt
nt = float(np.linalg.norm(th))
x = min((k / eta_) * (nt - 0.5 * eta_ * float(np.dot(gt, gt)) - gamma_), 300.0)
self.w = self.w1 + (alpha_ * max(np.expm1(x), 0.0) * th / nt if nt > 0 else 0 * th)
a = Alg5Wrong(d, alpha, eta, gamma)
Rw = 0.0
for t in range(T):
w = a.play()
Rw += float(np.dot(g[t], w - u[t]))
a.update(g[t])
return dict(instance="sign-flipping full-information instance, u = 0",
thm_E6_bound=B, regret_with_eta_over_k=R_ok, regret_with_k_over_eta=float(Rw),
eta_over_k_satisfies_E6=bool(R_ok <= B),
k_over_eta_satisfies_E6=bool(Rw <= B))
def test_B_thmE7():
rng = np.random.default_rng(SEED + 1)
rows = []
for T, d, K in [(500, 3, 1), (500, 3, 5), (1000, 5, 10), (2000, 4, 20)]:
L = 1.0
g = rng.standard_normal((T, d))
g = L * g / np.linalg.norm(g, axis=1, keepdims=True)
u = np.zeros((T, d))
for i in np.array_split(np.arange(T), K):
u[i] = rng.standard_normal(d)
a = Alg6(d, T, L, EPSILON)
R = 0.0
for t in range(T):
w = a.play()
R += float(np.dot(g[t], w - u[t]))
a.update(g[t])
nu = np.linalg.norm(u, axis=1)
path = np.linalg.norm(u[1:] - u[:-1], axis=1)
PhiT = phi(nu[-1], T / EPSILON)
PT = float(np.sum(phi(path, 4 * T**3 / EPSILON)))
B = 4 * L * len(a.etas) * (EPSILON + nu.max() + PhiT + PT) + 2 * np.sqrt(
2 * (PhiT + PT) * float(np.sum(np.sum(g * g, axis=1) * nu))
)
rows.append(
dict(T=T, d=d, K=K, regret=R, thm_E7_bound=float(B), holds=bool(R <= B))
)
return rows
def _run(kind, losses, comps, d, T, S, seed, eta=None):
L = 2 * d * G
if kind == "alg6":
olo = BatchAlg6(S, d, T, L, EPSILON / d)
elif kind == "ogd":
olo = BatchOGD(S, d, eta)
else:
olo = BatchAlg5(S, d, T, L, EPSILON / d, eta)
r = run_pablo_batch(losses, olo, d, EPS_PERT, np.random.default_rng(seed), S,
comparators=comps)
r["clipped"] = getattr(olo, "clipped", 0)
return r
def e6_tuned_bound(eta, d, T, P_T, M, L, eps_prime, k=4.0):
"""Theorem E.6 (untuned form) evaluated for a SINGLE step size eta, with
sum_t ||g_t||^2 ||u_t|| = 2 d G^2 T M (Corollary 2.2) and alpha = eps'/T, gamma = L/T."""
alpha, gamma = eps_prime / T, L / T
PhiT = phi(M, 1.0 / alpha)
PT_phi = phi(P_T, k / (eta * alpha * gamma)) if P_T > 0 else 0.0
return (2 * k * (PhiT + PT_phi) / (2 * eta)
+ 0.5 * eta * (2 * d * G * G * T) * M
+ gamma * T * M
+ eta * alpha * (2 * d * G * G * T))
def e7_bound(d, T, P_T, M, L, eps_prime):
"""Theorem E.7: the guarantee of Algorithm 6, which takes NO P_T and no M as input."""
nS = int(np.ceil(np.log2(max(T, 2)))) + 1
PhiT = phi(M, T / eps_prime)
PT_phi = phi(P_T, 4 * T ** 3 / eps_prime) if P_T > 0 else 0.0
return (4 * L * nS * (eps_prime + M + PhiT + PT_phi)
+ 2 * np.sqrt(2 * (PhiT + PT_phi) * (2 * d * G * G * T) * M))
def best_single_eta(d, T, P_T, M, L, eps_prime):
grid = np.exp(np.linspace(np.log(1.0 / (L * T)), np.log(1.0 / L), 400))
vals = [e6_tuned_bound(e, d, T, P_T, M, L, eps_prime) for e in grid]
i = int(np.argmin(vals))
return float(grid[i]), float(vals[i])
def test_CD_pathlength(fixed_T=8000, d=8, M=1.0, S=120):
"""Comparator sequences with a CONTROLLED path length P_T, withheld from the algorithm.
Measured: the uBLO dynamic regret of PABLO + Algorithm 6.
Bound-level: what a single-step-size learner would need to know. Theorem E.6's bound is
minimised over eta separately for each P_T (the oracle) and also evaluated at the eta
that is optimal for P_T = 0 (a learner that assumes a static comparator); Theorem E.7 --
the guarantee Algorithm 6 attains with no knowledge of P_T at all -- is compared to both.
"""
Ks = [1, 2, 4, 8, 16, 32, 64]
L = 2 * d * G
eps_prime = EPSILON / d
rows = []
for K in Ks:
rng = np.random.default_rng(SEED + 3 * K)
losses, comps, P_T = env_phases(fixed_T, d, K, M, rng)
r6 = _run("alg6", losses, comps, d, fixed_T, S, SEED + K)
eta_or, b_or = best_single_eta(d, fixed_T, P_T, M, L, eps_prime)
eta_0, _ = best_single_eta(d, fixed_T, 0.0, M, L, eps_prime)
b_static_tuning = e6_tuned_bound(eta_0, d, fixed_T, P_T, M, L, eps_prime)
b_alg6 = e7_bound(d, fixed_T, P_T, M, L, eps_prime)
# measured single-eta runs (oracle vs static tuning) for completeness
r_or = _run("alg5", losses, comps, d, fixed_T, S, SEED + K, eta=eta_or)
r_st = _run("alg5", losses, comps, d, fixed_T, S, SEED + K, eta=eta_0)
rows.append(dict(K=K, P_T=P_T,
measured_alg6_no_prior_knowledge=float(r6["dyn_play"].mean()),
sem=float(r6["dyn_play"].std(ddof=1) / np.sqrt(S)),
alg6_max_iterate=float(r6["max_w"].max()),
measured_alg5_oracle_eta=float(r_or["dyn_play"].mean()),
measured_alg5_eta_tuned_for_P_T_zero=float(r_st["dyn_play"].mean()),
oracle_eta=eta_or, static_eta=eta_0,
bound_E6_oracle_eta=b_or,
bound_E6_with_eta_tuned_for_P_T_zero=b_static_tuning,
bound_E7_alg6_no_prior_knowledge=b_alg6,
alg6_bound_over_oracle_bound=b_alg6 / b_or,
static_tuning_bound_over_oracle_bound=b_static_tuning / b_or,
measured_regret_below_E7_bound=bool(
float(r6["dyn_play"].mean()) <= b_alg6)))
P = [r["P_T"] for r in rows[1:]]
sl, _, se = fit_exponent(P, [r["measured_alg6_no_prior_knowledge"] for r in rows[1:]])
slb, _, seb = fit_exponent(P, [r["bound_E7_alg6_no_prior_knowledge"] for r in rows[1:]])
R2 = np.array([r["measured_alg6_no_prior_knowledge"] ** 2 for r in rows])
PT = np.array([r["P_T"] for r in rows])
A = np.vstack([PT, np.ones_like(PT)]).T
coef, *_ = np.linalg.lstsq(A, R2, rcond=None)
resid = R2 - A @ coef
r2score = 1.0 - float(np.sum(resid ** 2) / np.sum((R2 - R2.mean()) ** 2))
return dict(T=fixed_T, d=d, M=M, seeds=S, rows=rows,
fitted_P_T_exponent_of_measured_regret=sl, stderr=se,
fitted_P_T_exponent_of_E7_bound=slb, stderr_bound=seb,
predicted_exponent=0.5,
regression_of_squared_regret_on_P_T=dict(
slope=float(coef[0]), intercept=float(coef[1]), r_squared=r2score,
note="R^2 = a + b P_T <=> R = sqrt(a + b P_T), i.e. exactly a "
"sqrt(P_T) dependence once the P_T-independent part is accounted "
"for; a naive log-log slope is biased downwards by the intercept"),
max_alg6_bound_over_oracle_bound=float(max(
r["alg6_bound_over_oracle_bound"] for r in rows)),
max_static_tuning_bound_over_oracle_bound=float(max(
r["static_tuning_bound_over_oracle_bound"] for r in rows)),
note_on_measured_single_eta="on this family PABLO keeps ||w_t|| ~ 1e-3, i.e. "
"it correctly refuses to scale up, so all three measured curves "
"coincide: the realised regret is the information-theoretic floor and "
"no step-size tuning can change it. The separation between knowing and "
"not knowing P_T is therefore reported at the level of the guarantees.")
def test_T_sweep(K=8, d=8, M=1.0, S=120):
Ts = [500, 1000, 2000, 4000, 8000, 16000]
rows = []
for T in Ts:
rng = np.random.default_rng(SEED + 17 * T)
losses, comps, P_T = env_phases(T, d, K, M, rng)
r6 = _run("alg6", losses, comps, d, T, S, SEED + T)
rows.append(
dict(
T=T,
P_T=P_T,
regret=float(r6["dyn_play"].mean()),
sem=float(r6["dyn_play"].std(ddof=1) / np.sqrt(S)),
)
)
sl, _, se = fit_exponent(Ts, [r["regret"] for r in rows])
return dict(
K=K, d=d, seeds=S, rows=rows, fitted_T_exponent=sl, stderr=se, predicted=0.5
)
def test_E_dimension(K=8, T=4000, M=1.0, S=120):
ds = [2, 4, 8, 16, 32]
rows = []
for d in ds:
rng = np.random.default_rng(SEED + 23 * d)
losses, comps, P_T = env_phases(T, d, K, M, rng)
r6 = _run("alg6", losses, comps, d, T, S, SEED + d)
rows.append(
dict(
d=d,
P_T=P_T,
regret=float(r6["dyn_play"].mean()),
sem=float(r6["dyn_play"].std(ddof=1) / np.sqrt(S)),
)
)
sl, _, se = fit_exponent(ds, [r["regret"] for r in rows])
return dict(
K=K,
T=T,
seeds=S,
rows=rows,
fitted_d_exponent=sl,
stderr=se,
predicted_norm_oblivious=0.5,
predicted_norm_adaptive=1.0,
)
if __name__ == "__main__":
res = dict(
claim="claim-3 Theorem 3.3 sqrt(P_T) dynamic regret without prior knowledge",
seed=SEED,
A_theorem_E6_check=test_A_thmE6(),
A2_update_parse_identifiability=test_A2_parse_identifiability(),
B_theorem_E7_check=test_B_thmE7(),
C_D_path_length_sweep=test_CD_pathlength(),
T_sweep=test_T_sweep(),
E_dimension_sweep=test_E_dimension(),
)
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "claim3_dynamic.json"), "w") as f:
json.dump(res, f, indent=1)
print(json.dumps(res, indent=1))

Xet Storage Details

Size:
14.9 kB
·
Xet hash:
28c8b8ed81dc49fbd43c3fb151fc68af26138a92536f00f2309820a276342cf6

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