Buckets:
| """Faithful implementation of Algorithm 1 (two-point BCO with OOGD) from | |
| 'Improved Dimension Dependence for Bandit Convex Optimization with Gradient | |
| Variations' (arXiv:2602.04761). No official code is released for this paper | |
| (it is a pure theory/COLT-style paper); this is an independent, from-scratch | |
| implementation used to empirically probe the claimed regret-bound scaling. | |
| Algorithm 1 (paper, Section 2.2 / restated from Chiang et al. 2013): | |
| Input: step sizes {eta_t}, exploration delta, shrinkage xi = delta/R | |
| x_1 = w_hat_1 = 0, g_tilde_1 = 0 | |
| for t = 1..T: | |
| i_t ~ Uniform([d]) | |
| query x_t = w_t + delta*e_{i_t}, x'_t = w_t - delta*e_{i_t} | |
| observe f_t(x_t), f_t(x'_t) | |
| v_t = (f_t(x_t) - f_t(x'_t)) / (2*delta) | |
| g_t = d*(v_t - g_tilde_t[i_t]) * e_{i_t} + g_tilde_t (Eq 2.2) | |
| g_tilde_{t+1} = (v_t - g_tilde_t[i_t]) * e_{i_t} + g_tilde_t (Eq 2.2) | |
| w_hat_{t+1} = Proj_{(1-xi)X}[w_hat_t - eta_t * g_t] | |
| w_{t+1} = Proj_{(1-xi)X}[w_hat_{t+1} - eta_{t+1} * g_tilde_{t+1}] | |
| The feasible set X is taken as the centered ball of radius R (satisfies | |
| Assumption 1 with r=R). Function families used across claims are drifting | |
| linear / quadratic-plus-linear functions, for which gradients (and gradient | |
| variation) are known in closed form, letting us compute exact regret, | |
| gradient variation V_T, gradient variance W_T, and small loss F_T. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| def project_to_ball(x: np.ndarray, radius: float) -> np.ndarray: | |
| norm = np.linalg.norm(x) | |
| if norm <= radius or norm == 0: | |
| return x | |
| return x * (radius / norm) | |
| class DriftingQuadratic: | |
| """f_t(x) = 0.5*lam*||x||^2 + <c_t, x> (lam=0 -> linear; lam>0 -> lam-strongly convex, L=lam-smooth on the quadratic part). | |
| Two comparator-generation modes are supported: | |
| - mode="randomwalk" (legacy): c_t performs a random walk with a FIXED | |
| per-step norm `drift`. This is a BENIGN environment: because the | |
| target drifts slowly and smoothly, an adaptive online player can track | |
| it closely and often beats the single FIXED offline comparator in | |
| hindsight, producing negative "regret" that trivially satisfies any | |
| upper bound without stress-testing the algorithm. Kept only for | |
| backward compatibility / illustration of this failure mode. | |
| - mode="iid" (default): c_t is RESAMPLED i.i.d. every round from a fixed | |
| distribution (fixed norm `c_scale`, independent of d). Consecutive | |
| comparators are then uncorrelated, so gradient variation V_T is large | |
| and (approximately) dimension-independent (~ 2*T*c_scale^2), and the | |
| bandit algorithm -- which only observes one noisy coordinate probe per | |
| round -- cannot track the target at all; its best play converges to | |
| (approximately) the fixed mean of the c_t sequence, which is also the | |
| offline optimum. This yields genuine, positive regret driven by the | |
| bandit estimation/exploration cost, which is exactly the quantity the | |
| paper's bounds describe, giving a real (non-benign) stress test of the | |
| claimed dimension dependence. | |
| """ | |
| def __init__(self, d: int, T: int, lam: float = 0.0, drift: float = 0.05, | |
| c_scale: float = 1.0, seed: int = 0, mode: str = "iid"): | |
| rng = np.random.RandomState(seed) | |
| self.d = d | |
| self.T = T | |
| self.lam = lam | |
| self.mode = mode | |
| c = np.zeros((T + 1, d)) | |
| c[0] = rng.normal(size=d) | |
| c[0] *= c_scale / (np.linalg.norm(c[0]) + 1e-12) | |
| if mode == "iid": | |
| for t in range(1, T + 1): | |
| v = rng.normal(size=d) | |
| v *= c_scale / (np.linalg.norm(v) + 1e-12) | |
| c[t] = v | |
| elif mode == "randomwalk": | |
| for t in range(1, T + 1): | |
| step = rng.normal(size=d) | |
| step *= drift / (np.linalg.norm(step) + 1e-12) | |
| c[t] = c[t - 1] + step | |
| else: | |
| raise ValueError(f"unknown mode {mode!r}") | |
| self.c = c | |
| def grad(self, t: int, x: np.ndarray) -> np.ndarray: | |
| return self.lam * x + self.c[t] | |
| def value(self, t: int, x: np.ndarray) -> float: | |
| return 0.5 * self.lam * float(x @ x) + float(self.c[t] @ x) | |
| def gradient_variation(self) -> float: | |
| diffs = self.c[1:self.T + 1] - self.c[0:self.T] | |
| return float(np.sum(diffs ** 2)) | |
| def offline_optimum(self, radius: float) -> tuple[np.ndarray, float]: | |
| """min_x sum_{t=1}^T f_t(x) over ||x||<=radius, closed form (convex quadratic).""" | |
| csum = self.c[1:self.T + 1].sum(axis=0) | |
| if self.lam > 0: | |
| x_star = -csum / (self.lam * self.T) | |
| else: | |
| # Linear: optimum is at the boundary in direction -csum, i.e. at | |
| # distance `radius` from the origin (this used to normalize to a | |
| # UNIT vector and then no-op through project_to_ball since a unit | |
| # vector is already inside any radius>=1 ball, silently pinning | |
| # the "offline optimum" to the unit sphere regardless of the | |
| # requested radius -- a bug that made the small-loss quantity | |
| # F_T = offline_opt(R) - offline_opt(R+slack) degenerate to | |
| # exactly 0 for every d, since both radii produced the identical | |
| # point. Fixed: explicitly scale to the requested radius.) | |
| x_star = -csum / (np.linalg.norm(csum) + 1e-12) * radius | |
| x_star = project_to_ball(x_star, radius) | |
| total = sum(self.value(t, x_star) for t in range(1, self.T + 1)) | |
| return x_star, total | |
| def run_algorithm1(env: DriftingQuadratic, radius: float, eta_fn, delta: float | None = None): | |
| """Run Algorithm 1 (two-point OOGD) against env for T rounds. | |
| eta_fn(t, vbar_prev) -> eta_t (vbar_prev = running non-consecutive | |
| gradient variation estimate Vbar_{t-1}, Eq 2.1, needed for adaptive | |
| schedules such as Theorem 1's). | |
| Returns dict with per-round losses, cumulative regret, and Vbar_T. | |
| """ | |
| d, T = env.d, env.T | |
| if delta is None: | |
| # Paper's Algorithm 1 box: delta = 1/(2 d^2 L T R). We use a numerically | |
| # stable stand-in (small, dimension/T-scaled) since L, exact constants | |
| # only affect higher-order/bias terms, not the asymptotic regret shape | |
| # we are probing here. | |
| delta = radius / (2 * d ** 2 * max(T, 2)) | |
| xi = delta / radius | |
| w_hat = np.zeros(d) | |
| w = np.zeros(d) | |
| g_tilde = np.zeros(d) | |
| total_regret_loss = 0.0 | |
| vbar_running = 0.0 | |
| losses_at_w = [] | |
| for t in range(1, T + 1): | |
| i_t = np.random.randint(d) | |
| e_i = np.zeros(d) | |
| e_i[i_t] = 1.0 | |
| x_t = w + delta * e_i | |
| x_tp = w - delta * e_i | |
| f_plus = env.value(t, x_t) | |
| f_minus = env.value(t, x_tp) | |
| v_t = (f_plus - f_minus) / (2 * delta) | |
| g_t = d * (v_t - g_tilde[i_t]) * e_i + g_tilde | |
| g_tilde_next = (v_t - g_tilde[i_t]) * e_i + g_tilde | |
| vbar_running += float(np.sum((g_t - g_tilde) ** 2)) | |
| eta_t = eta_fn(t, vbar_running) | |
| w_hat_next = project_to_ball(w_hat - eta_t * g_t, (1 - xi) * radius) | |
| eta_next = eta_fn(t + 1, vbar_running) | |
| w_next = project_to_ball(w_hat_next - eta_next * g_tilde_next, (1 - xi) * radius) | |
| # Regret uses the average two-point loss 0.5*(f(x_t)+f(x'_t)) as in | |
| # the paper's REG_T definition (per-round midpoint surrogate). | |
| total_regret_loss += 0.5 * (f_plus + f_minus) | |
| losses_at_w.append(0.5 * (f_plus + f_minus)) | |
| w_hat, w, g_tilde = w_hat_next, w_next, g_tilde_next | |
| x_star, offline_opt = env.offline_optimum(radius) | |
| regret = total_regret_loss - offline_opt | |
| return { | |
| "regret": regret, | |
| "vbar_T": vbar_running, | |
| "V_T": env.gradient_variation(), | |
| "losses": np.array(losses_at_w), | |
| "offline_opt": offline_opt, | |
| } | |
Xet Storage Details
- Size:
- 7.89 kB
- Xet hash:
- 072fb33789bf1a13217060680c6afe5409bd545feac29a21fc55bbec2aa5ab73
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.