Flow-Efficiency Drift Scheduler / scheduler_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4from scipy.special import logsumexp
  5
  6SEED = 3134
  7rng = np.random.default_rng(SEED)
  8D, MODES, M = 16, 8, 1200
  9SIGMA = 0.55
 10LOG2PI = math.log(2.0 * math.pi)
 11
 12# Eight separated modes, with a common translation changing over annealing levels.
 13base_means = np.zeros((MODES, D))
 14for k in range(MODES):
 15    a = 3.0 if k < 4 else -3.0
 16    b = 3.0 if (k % 4) < 2 else -3.0
 17    base_means[k, :4] = [a, b, 2.2 * (1 if k % 2 else -1), 1.5 * (1 if k in (0, 3, 4, 7) else -1)]
 18# A shared target shift gives a clean, controllable nonstationarity signal.
 19shifts = []
 20for i in range(28):
 21    t = i / 27.0
 22    speed = 0.15 if i < 7 or i >= 21 else 0.95
 23    # piecewise rapid motion, plus a return, so consecutive q drift is observable
 24    x = (0.0 if i == 0 else sum(0.15 if j < 7 or j >= 21 else 0.95 for j in range(1, i+1)))
 25    shifts.append(np.array([x, -0.7*x, 0.35*x] + [0.] * (D-3)))
 26target_means = np.array([base_means + s for s in shifts])
 27
 28def log_mix(x, means):
 29    # equal-weight spherical Gaussian mixture
 30    z = -0.5 * (((x[:, None, :] - means[None, :, :]) / SIGMA) ** 2).sum(axis=2)
 31    z -= D * (math.log(SIGMA) + 0.5 * LOG2PI)
 32    return logsumexp(z, axis=1) - math.log(MODES)
 33
 34def sample_mix(means, n, rg):
 35    idx = rg.integers(MODES, size=n)
 36    return means[idx] + SIGMA * rg.normal(size=(n, D))
 37
 38def ess_from_logw(logw):
 39    a = logw - logsumexp(logw)
 40    return float(np.exp(-logsumexp(2*a)) / len(logw))
 41
 42def sym_kl_mc(a, b, n=5000):
 43    xa, xb = sample_mix(a, n, rng), sample_mix(b, n, rng)
 44    return float(0.5 * (np.mean(log_mix(xa, a)-log_mix(xa, b)) +
 45                        np.mean(log_mix(xb, b)-log_mix(xb, a))))
 46
 47def importance_stats(target, proposal, n=M):
 48    x = sample_mix(proposal, n, rng)
 49    lw = log_mix(x, target) - log_mix(x, proposal)
 50    return ess_from_logw(lw), float(abs(np.log(np.mean(np.exp(lw)))))
 51
 52def verify_math():
 53    a = base_means.copy()
 54    b = a + np.array([1.4, -0.5, 0.2] + [0.]*(D-3))
 55    # ESS is invariant to a common log-weight shift and lies in [1/M,1].
 56    lw = rng.normal(size=1000)
 57    e1 = ess_from_logw(lw)
 58    e2 = ess_from_logw(lw + 37.0)
 59    kl = sym_kl_mc(a, b, 12000)
 60    kl_rev = sym_kl_mc(b, a, 12000)
 61    # For this symmetric definition, swapping distributions should agree up to MC noise.
 62    return {"ess_in_range": bool(1/1000 - 1e-12 <= e1 <= 1.0),
 63            "ess_shift_invariance_abs_error": abs(e1-e2),
 64            "symmetric_kl_forward_reverse_abs_error": abs(kl-kl_rev),
 65            "symmetric_kl_positive": kl > 0, "symmetric_kl": kl}
 66
 67def run(adaptive):
 68    q = base_means.copy()
 69    previous_q = q.copy()
 70    K, R = 8, 1
 71    records = []
 72    # persistent FIFO of target means emulates stale flow-training data.
 73    history = []
 74    for i in range(len(target_means)):
 75        history.append(target_means[i].copy())
 76        if len(history) > K: history.pop(0)
 77        # A gradient-like flow update toward the FIFO training distribution.
 78        train_target = np.mean(history, axis=0)
 79        for _ in range(int(R)):
 80            q += 0.30 * (train_target - q)
 81        drift = sym_kl_mc(q, previous_q, 1800) if i else 0.0
 82        eta, evidence_error = importance_stats(target_means[i], q)
 83        # Small local Langevin correction is represented by a conservative mode-center
 84        # pull only when ESS is poor and drift is not high; it is counted separately.
 85        correction = 0
 86        if adaptive and eta < 0.10 and drift <= 0.45:
 87            correction = 6
 88            q += 0.35 * (target_means[i] - q)
 89            eta, evidence_error = importance_stats(target_means[i], q)
 90        records.append({"level": i, "eta": eta, "drift": drift, "K": K, "R": R,
 91                        "correction_steps": correction, "log_evidence_abs_error": evidence_error})
 92        if adaptive:
 93            if drift > 0.45:
 94                K = max(2, K // 2)
 95            elif drift < 0.08 and eta > 0.45:
 96                K = min(8, K + 1)
 97            if eta < 0.10:
 98                R = min(8, max(1, int(math.ceil(R * (1 + 2.0*(0.10-eta))))))
 99            elif eta > 0.55:
100                R = max(1, R-1)
101        previous_q = q.copy()
102    return records
103
104def summarize(rs):
105    eta = np.array([r["eta"] for r in rs])
106    return {"mean_eta": float(eta.mean()), "min_eta": float(eta.min()),
107            "levels_eta_below_0.1": int((eta < .1).sum()),
108            "levels_eta_below_0.02": int((eta < .02).sum()),
109            "mean_drift": float(np.mean([r["drift"] for r in rs])),
110            "total_R": int(sum(r["R"] for r in rs)),
111            "total_correction_steps": int(sum(r["correction_steps"] for r in rs)),
112            "mean_log_evidence_abs_error": float(np.mean([r["log_evidence_abs_error"] for r in rs])),
113            "max_log_evidence_abs_error": float(max(r["log_evidence_abs_error"] for r in rs))}
114
115def main():
116    mathcheck = verify_math()
117    fixed, adaptive = run(False), run(True)
118    out = {"seed": SEED, "math_check": mathcheck,
119           "fixed_summary": summarize(fixed), "adaptive_summary": summarize(adaptive),
120           "fixed_levels": fixed, "adaptive_levels": adaptive}
121    Path("results.json").write_text(json.dumps(out, indent=2))
122    print(json.dumps({"math_check": mathcheck, "fixed": summarize(fixed), "adaptive": summarize(adaptive)}, indent=2))
123
124if __name__ == "__main__": main()