SabaPivot's picture
download
raw
5.77 kB
"""Seed-batched implementations of Algorithm 6 and of the PABLO loop, so that hundreds of
independent replicates can be run in one vectorised pass. Mathematically identical to the
reference implementations in pablo.py (checked in scripts/claim2_static.py)."""
from __future__ import annotations
import numpy as np
class BatchAlg6:
"""Algorithm 6 replicated over S independent seeds. State shape (S, |S_eta|, d)."""
EXP_CLIP = 300.0
def __init__(self, S, d, T, L, epsilon, k=4.0):
n = int(np.ceil(np.log2(max(T, 2)))) + 1
self.etas = np.array([min(2.0**i / (T * L), 1.0 / L) for i in range(n)])
self.alpha = epsilon / T
self.gamma = L / T
self.k = k
self.W = np.zeros((S, n, d))
self.clipped = 0
def play(self):
return self.W.sum(axis=1)
def update(self, g): # g: (S, d)
k, alpha, gamma = self.k, self.alpha, self.gamma
etas = self.etas[None, :, None]
nw = np.linalg.norm(self.W, axis=2, keepdims=True)
theta = (
(k / etas) * np.log(nw / alpha + 1.0) * (self.W / np.where(nw > 0, nw, 1.0))
)
theta = theta - g[:, None, :]
nt = np.linalg.norm(theta, axis=2, keepdims=True)
gg = np.sum(g * g, axis=1)[:, None, None]
x = (etas / k) * (nt - 0.5 * etas * gg - gamma)
if np.any(x > self.EXP_CLIP):
self.clipped += int(np.sum(x > self.EXP_CLIP))
x = np.minimum(x, self.EXP_CLIP)
mag = alpha * np.maximum(np.expm1(x), 0.0)
self.W = mag * theta / np.where(nt > 0, nt, 1.0)
class BatchOGD:
def __init__(self, S, d, lr):
self.w = np.zeros((S, d))
self.lr = lr
def play(self):
return self.w
def update(self, g):
self.w = self.w - self.lr * g
def run_pablo_batch(losses, olo, d, eps, rng, S, comparators=None, track_w=False):
"""PABLO (Algorithm 1, isotropic H_t of Eq. (4)) over S independent seeds sharing one
oblivious loss sequence.
Returns per-seed arrays:
cum_play sum_t <l_t, wtilde_t> (so R_T(u) = cum_play - <sum_t l_t, u> for
any static comparator u)
dyn_play sum_t <l_t, wtilde_t - u_t> (dynamic regret, if comparators given)
olo_reg sum_t <ltilde_t, w_t - u_t> (OLO regret of the subroutine on estimates)
sum_lt_sq sum_t ||ltilde_t||^2
sum_w_sq sum_t ||w_t||^2
max_w max_t ||w_t||
"""
T = len(losses)
cum_play = np.zeros(S)
dyn_play = np.zeros(S)
olo_reg = np.zeros(S)
sum_lt_sq = np.zeros(S)
sum_w_sq = np.zeros(S)
max_w = np.zeros(S)
sum_dir = np.zeros(S) # sum_t <z_t, l_t> with z_t = wtilde_t/||wtilde_t|| (Eq. (7))
sqd = np.sqrt(d)
for t in range(T):
w = olo.play() # (S, d)
ell = losses[t] # (d,)
nw = np.linalg.norm(w, axis=1)
m = np.maximum(nw, eps) # (S,)
idx = rng.integers(d, size=S)
sgn = rng.integers(2, size=S) * 2.0 - 1.0
Smat = np.zeros((S, d))
Smat[np.arange(S), idx] = sgn
wt = w + (sqd * m)[:, None] * Smat
obs = wt @ ell # (S,) bandit feedback
lt = (sqd / m)[:, None] * Smat * obs[:, None]
cum_play += obs
sum_dir += obs / np.maximum(np.linalg.norm(wt, axis=1), 1e-300)
u = comparators[t] if comparators is not None else None
if u is not None:
dyn_play += obs - float(np.dot(ell, u))
olo_reg += np.sum(lt * (w - u[None, :]), axis=1)
else:
olo_reg += np.sum(lt * w, axis=1)
sum_lt_sq += np.sum(lt * lt, axis=1)
sum_w_sq += nw**2
max_w = np.maximum(max_w, nw)
olo.update(lt)
return dict(
cum_play=cum_play,
dyn_play=dyn_play,
olo_reg=olo_reg,
sum_lt_sq=sum_lt_sq,
sum_w_sq=sum_w_sq,
max_w=max_w,
sum_dir=sum_dir,
sum_losses=losses.sum(axis=0),
V_T=float(np.sum(losses * losses)),
)
def fit_exponent(xs, ys):
"""Least-squares slope of log y vs log x, with a standard error."""
lx, ly = np.log(np.asarray(xs, float)), np.log(np.asarray(ys, float))
A = np.vstack([lx, np.ones_like(lx)]).T
coef, res, *_ = np.linalg.lstsq(A, ly, rcond=None)
pred = A @ coef
n = len(lx)
if n > 2:
s2 = float(np.sum((ly - pred) ** 2) / (n - 2))
se = float(np.sqrt(s2 * np.linalg.inv(A.T @ A)[0, 0]))
else:
se = float("nan")
return float(coef[0]), float(coef[1]), se
class BatchAlg5:
"""A single Algorithm-5 instance (one fixed step size eta) replicated over S seeds.
Used as the *oracle-tuned* / *mistuned* baseline against Algorithm 6, which has to
aggregate over the whole eta-grid because it does not know P_T."""
EXP_CLIP = 300.0
def __init__(self, S, d, T, L, epsilon, eta, k=4.0):
self.eta, self.k = eta, k
self.alpha = epsilon / T
self.gamma = L / T
self.w = np.zeros((S, d))
self.clipped = 0
def play(self):
return self.w
def update(self, g):
k, eta, alpha, gamma = self.k, self.eta, self.alpha, self.gamma
nw = np.linalg.norm(self.w, axis=1, keepdims=True)
theta = (k / eta) * np.log(nw / alpha + 1.0) * (self.w / np.where(nw > 0, nw, 1.0))
theta = theta - g
nt = np.linalg.norm(theta, axis=1, keepdims=True)
gg = np.sum(g * g, axis=1)[:, None]
x = (eta / k) * (nt - 0.5 * eta * gg - gamma)
if np.any(x > self.EXP_CLIP):
self.clipped += int(np.sum(x > self.EXP_CLIP))
x = np.minimum(x, self.EXP_CLIP)
self.w = alpha * np.maximum(np.expm1(x), 0.0) * theta / np.where(nt > 0, nt, 1.0)

Xet Storage Details

Size:
5.77 kB
·
Xet hash:
0ceef4c397b6b3114dd15d9a8487b29a66770713d9830b9e85b34da10ae9fff5

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