KL Mirror-Prox for coupled routing / kl_mirror_prox_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 2078
  6np.set_printoptions(precision=6, suppress=True)
  7
  8
  9def prox(p, c, eta):
 10    """KL prox: argmin_q <c,q> + KL(q||p)/eta."""
 11    logw = np.log(np.maximum(p, 1e-300)) - eta * c
 12    logw -= np.max(logw)
 13    q = np.exp(logw)
 14    return q / q.sum()
 15
 16
 17def make_operator(lam):
 18    # On the tangent plane, A is skew-symmetric with eigenvalues +/- i*lam.
 19    return lam * np.array([[0., -1., 1.], [1., 0., -1.], [-1., 1., 0.]]) / math.sqrt(3.)
 20
 21
 22def md_step(p, A, eta):
 23    return prox(p, A @ p, eta)
 24
 25
 26def mp_step(p, A, eta):
 27    z = prox(p, A @ p, eta)
 28    return prox(p, A @ z, eta)
 29
 30
 31def radius(p):
 32    return float(np.linalg.norm(p - np.ones(3) / 3))
 33
 34
 35def jacobian_factor(step, u, A, eta, eps=1e-7):
 36    # Two tangent directions; both have the same theoretical factor here.
 37    basis = np.array([[1., -1., 0.], [1., 1., -2.]])
 38    basis /= np.linalg.norm(basis, axis=1)[:, None]
 39    vals = []
 40    for v in basis:
 41        p = u + eps * v
 42        vals.append(radius(step(p, A, eta)) / radius(p))
 43    return float(np.mean(vals))
 44
 45
 46def trajectory(step, p0, A, eta, steps=60):
 47    p = p0.copy(); rs = [radius(p)]
 48    for _ in range(steps):
 49        p = step(p, A, eta); rs.append(radius(p))
 50    return np.asarray(rs)
 51
 52
 53def main():
 54    u = np.ones(3) / 3
 55    p0 = u + np.array([0.035, -0.021, -0.014])
 56    rows = []
 57
 58    # Prediction 1: zero coupling gives exactly identity updates.
 59    A = make_operator(0.)
 60    zero_effect = max(float(np.max(np.abs(md_step(p0, A, .7) - p0))),
 61                      float(np.max(np.abs(mp_step(p0, A, .7) - p0))))
 62
 63    # At uniform p, linearized KL prox is I-(eta/3)A. Thus x=eta*Lambda/3,
 64    # MD factor=sqrt(1+x^2), MP factor=sqrt(1-x^2+x^4), neutral at x=1.
 65    etas = [0.30, 1.00, 2.00]
 66    lambdas = [0.50, 1.00, 2.00, 3.00, 5.00]
 67    for eta in etas:
 68        for lam in lambdas:
 69            x = eta * lam / 3.0
 70            A = make_operator(lam)
 71            measured_md = jacobian_factor(md_step, u, A, eta)
 72            measured_mp = jacobian_factor(mp_step, u, A, eta)
 73            rows.append({
 74                'eta': eta, 'Lambda': lam, 'x': x,
 75                'md_measured': measured_md, 'md_predicted': math.sqrt(1 + x*x),
 76                'mp_measured': measured_mp, 'mp_predicted': math.sqrt(1 - x*x + x**4)
 77            })
 78
 79    # Equal-step coupled-routing proxy at x=2/3: MP should damp the cycle while
 80    # standard one-stage entropic mirror descent amplifies it.
 81    eta, lam = 1.00, 2.00
 82    A = make_operator(lam)
 83    md_r = trajectory(md_step, p0, A, eta)
 84    mp_r = trajectory(mp_step, p0, A, eta)
 85
 86    def vi_residual(p):
 87        c = A @ p
 88        return float(np.max(c - c @ p))
 89
 90    pmd, pmp = p0.copy(), p0.copy(); md_vi, mp_vi = [], []
 91    for _ in range(60):
 92        md_vi.append(vi_residual(pmd)); mp_vi.append(vi_residual(pmp))
 93        pmd = md_step(pmd, A, eta); pmp = mp_step(pmp, A, eta)
 94
 95    tr = [r for r in rows if abs(r['eta'] - 2.0) < 1e-12]
 96    below = [r['x'] for r in tr if r['mp_measured'] < 1.0]
 97    above = [r['x'] for r in tr if r['mp_measured'] > 1.0]
 98    md_err = max(abs(r['md_measured'] - r['md_predicted']) for r in rows)
 99    mp_err = max(abs(r['mp_measured'] - r['mp_predicted']) for r in rows)
100    result = {
101        'seed': SEED,
102        'zero_lambda_max_change': zero_effect,
103        'zero_lambda_prediction_confirmed': bool(zero_effect < 1e-14),
104        'local_sweep': rows,
105        'max_abs_local_prediction_error_md': float(md_err),
106        'max_abs_local_prediction_error_mp': float(mp_err),
107        'transition': {
108            'predicted_x': 1.0,
109            'observed_below_max_x': float(max(below)) if below else None,
110            'observed_above_min_x': float(min(above)) if above else None,
111            'note': 'MP factor is below 1 for 0<x<1 and above 1 for x>1.'
112        },
113        'mini_experiment': {
114            'eta': eta, 'Lambda': lam, 'x': eta * lam / 3.0,
115            'md_radius_initial': float(md_r[0]), 'md_radius_final': float(md_r[-1]),
116            'mp_radius_initial': float(mp_r[0]), 'mp_radius_final': float(mp_r[-1]),
117            'md_vi_initial': float(md_vi[0]), 'md_vi_final': float(md_vi[-1]),
118            'mp_vi_initial': float(mp_vi[0]), 'mp_vi_final': float(mp_vi[-1]),
119            'md_radius_max': float(md_r.max()), 'mp_radius_max': float(mp_r.max())
120        }
121    }
122    Path('results.json').write_text(json.dumps(result, indent=2))
123    print(json.dumps(result, indent=2))
124
125
126if __name__ == '__main__':
127    main()