Regret-Balanced Adaptive Context / adaptive_context_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json
  2import time
  3import numpy as np
  4
  5
  6def sigmoid(x):
  7    x = np.clip(x, -30.0, 30.0)
  8    return 1.0 / (1.0 + np.exp(-x))
  9
 10
 11def spectrum(T, r):
 12    j = np.arange(1, len(r) + 1)
 13    n = np.maximum(T - j + 1, 0)
 14    return float(np.sum(np.log1p(n * np.asarray(r) ** 2)))
 15
 16
 17def selection_objective(theta, rhat, t, lam):
 18    """Return C(h), h=0..H, using n_{t,j}=max(t-j+1,0)."""
 19    H = len(theta)
 20    j = np.arange(1, H + 1)
 21    n = np.maximum(t - j + 1, 0.0)
 22    price = lam * np.log1p(n * np.asarray(rhat) ** 2)
 23    tail = n * np.asarray(theta) ** 2
 24    # C(h)=sum_{j<=h} price_j + sum_{j>h} tail_j
 25    return np.r_[np.sum(tail), np.cumsum(price - tail) + np.sum(tail)]
 26
 27
 28def math_check(seed=7):
 29    rng = np.random.default_rng(seed)
 30    H, T = 18, 80
 31    theta = rng.normal(0, .12, H)
 32    rhat = np.abs(theta) + .2 / np.sqrt(np.arange(1, H + 1))
 33    lam, t = .35, 63
 34    C = selection_objective(theta, rhat, t, lam)
 35    j = np.arange(1, H + 1)
 36    n = np.maximum(t - j + 1, 0.)
 37    marginal = lam * np.log1p(n * rhat ** 2) - n * theta ** 2
 38    algebra_error = float(np.max(np.abs(np.diff(C) - marginal)))
 39
 40    # Directly verify the paper's weighted tail scale against logistic KL.
 41    # For independent Rademacher inputs, compare source and truncated logits.
 42    B = .75
 43    th = rng.normal(0, .035, 7)
 44    th *= min(1., B / max(np.sum(np.abs(th)), 1e-12))
 45    hh, TT = 2, 120
 46    V = sum((TT-j+1) * th[j-1] ** 2 for j in range(hh + 1, len(th) + 1))
 47    kl_sum = 0.0
 48    reps = 4000
 49    for _ in range(reps):
 50        u = rng.choice([-1., 1.], size=len(th))
 51        a = float(np.dot(th, u))
 52        b = float(np.dot(th[:hh], u[:hh]))
 53        p, q = sigmoid(a), sigmoid(b)
 54        kl = p * np.log(p / q) + (1-p) * np.log((1-p) / (1-q))
 55        kl_sum += kl
 56    # Same expected KL is incurred on each affected round, hence V is its scale.
 57    empirical_ratio = (TT-hh) * kl_sum / reps / max(V, 1e-12)
 58    return {"objective_marginal_max_error": algebra_error,
 59            "tail_energy": float(V), "empirical_logistic_KL_over_tail_energy": float(empirical_ratio),
 60            "math_check_pass": bool(algebra_error < 1e-10 and .05 < empirical_ratio < .2)}
 61
 62
 63def make_data(T, H, seed):
 64    rng = np.random.default_rng(seed)
 65    u = rng.choice([-1., 1.], size=T + H + 1)
 66    theta = np.zeros((T, H))
 67    # Abruptly switch from short exponential memory to a longer polynomial-like tail.
 68    a = .40 * (.55 ** np.arange(H))
 69    b = .17 / (np.arange(1, H + 1) ** .72)
 70    theta[:T//2] = a
 71    theta[T//2:] = b
 72    y = np.zeros(T, dtype=np.int8)
 73    for t in range(T):
 74        x = u[H + t - np.arange(1, H + 1)]
 75        y[t] = rng.random() < sigmoid(np.dot(theta[t], x))
 76    return u, y, theta
 77
 78
 79def run_model(u, y, true_theta, mode, H, lam=.012, k=.08, lr=.035, warmup=100):
 80    T = len(y)
 81    est = np.zeros(H)
 82    count = np.zeros(H)
 83    active_h = H if mode == "fixed_long" else (4 if mode == "fixed_short" else 1)
 84    losses, hs, ops = [], [], []
 85    t0 = time.perf_counter()
 86    for t in range(T):
 87        x = u[H + t - np.arange(1, H + 1)]
 88        h_used = H if mode == "fixed_long" else (4 if mode == "fixed_short" else active_h)
 89        logit = float(np.dot(est[:h_used], x[:h_used]))
 90        p = float(sigmoid(logit))
 91        losses.append(-(y[t] * np.log(p + 1e-12) + (1-y[t]) * np.log(1-p + 1e-12)))
 92        err = p - y[t]
 93        # Full estimates are maintained as a transparent online pilot for unseen lags;
 94        # only the active prefix contributes to prediction and its measured inference ops.
 95        est -= lr * err * x
 96        count += 1
 97        rhat = np.abs(est) + k / np.sqrt(count)
 98        if mode == "adaptive" and t >= warmup and t % 5 == 0:
 99            C = selection_objective(est, rhat, t + 1, lam)
100            best = np.min(C)
101            candidates = np.flatnonzero(C <= best * 1.02 + 1e-9)
102            active_h = int(candidates[0]) if len(candidates) else int(np.argmin(C))
103            active_h = max(1, min(H, active_h))
104        hs.append(h_used)
105        ops.append(h_used)
106    elapsed = time.perf_counter() - t0
107    losses = np.asarray(losses)
108    return {"cum_logloss": float(np.sum(losses)),
109            "first_half_logloss": float(np.sum(losses[:T//2])),
110            "second_half_logloss": float(np.sum(losses[T//2:])),
111            "mean_active_h": float(np.mean(hs)),
112            "second_half_active_h": float(np.mean(hs[T//2:])),
113            "inference_ops_relative": float(np.sum(ops) / (T * H)),
114            "wall_seconds": float(elapsed)}
115
116
117def main():
118    checks = math_check()
119    T, H = 7000, 24
120    u, y, truth = make_data(T, H, seed=19)
121    results = {}
122    for mode in ["fixed_long", "fixed_short", "adaptive"]:
123        results[mode] = run_model(u, y, truth, mode, H)
124    # Report the idea against the conventional fixed maximum context and a short fixed window.
125    out = {"math": checks, "experiment": results,
126           "settings": {"T": T, "H_max": H, "lambda": .012, "confidence_k": .08,
127                        "seed": 19, "data": "short exponential -> long polynomial abrupt switch"}}
128    with open("results.json", "w") as f:
129        json.dump(out, f, indent=2)
130    print(json.dumps(out, indent=2))
131
132
133if __name__ == "__main__":
134    main()