SabaPivot's picture
download
raw
15.1 kB
"""
Exact Gaussian-moment machinery for ULMC / RMD / LMC / composite-LMC on
diagonal quadratic targets.
Target: V(x) = 1/2 sum_i a_i x_i^2 , a_i in [alpha, beta].
=> grad^2 V = A = diag(a), tightest Hessian upper bound H = A,
tr(H) = sum_i a_i, kappa = beta/alpha.
pi(x,p) propto exp(-V(x) - ||p||^2/2) => per coordinate
pi_i = N(0, diag(1/a_i, 1)).
Because V is quadratic and diagonal, EVERY discretization below is a linear
Gaussian recursion that factorises across coordinates. So if mu_0 factorises
and is Gaussian, mu_N is Gaussian (ULMC/LMC/composite) and
KL(mu_N || pi) = sum_i KL_2d(mu_N^i || pi_i) is available in CLOSED FORM --
no Monte-Carlo noise anywhere.
For RMD the per-step midpoints u_n, v_n are random, so mu_N is a *mixture* of
Gaussians. We propagate the exact first and second moments (exact, by
integrating the linear recursion over the laws of u,v with Gauss-Legendre
quadrature) and report
KL_G := KL( N(m_N, Sigma_N) || pi ),
which by the maximum-entropy property of the Gaussian is a CERTIFIED LOWER
BOUND on the true KL(mu_N||pi). `rmd_frozen_upper` gives the matching
convexity upper bound E_{u,v-seq}[ KL(component || pi) ] >= KL(mu_N||pi).
n-step propagation uses repeated squaring of the augmented affine map, so N up
to 1e10 costs O(log N).
All quantities are exact to float64; no seeds are needed for the exact paths.
"""
import numpy as np
# ----------------------------------------------------------------------------
# Brownian functionals of one ULD step (per coordinate, scalar BM on [0,h])
# xi1(s) = sqrt(2g) int_0^s (1-e^{-g(s-u)})/g dB_u
# xi2(h) = sqrt(2g) int_0^h e^{-g(h-u)} dB_u
# ----------------------------------------------------------------------------
def _em1(x):
"""1 - e^{-x}, stable for small x."""
return -np.expm1(-np.asarray(x, dtype=float))
def var_xi1(s, g):
"""Var(xi1(s)) = (2/g^2) * G(g s), G(z) = z - 3/2 + 2e^{-z} - e^{-2z}/2."""
z = np.asarray(g * s, dtype=float)
big = z > 5e-2
G = np.empty_like(z)
zb = z[big] if z.ndim else z
if z.ndim == 0:
if big:
G = z - 1.5 + 2 * np.exp(-z) - 0.5 * np.exp(-2 * z)
else:
G = z**3 / 3 - z**4 / 4 + 7 * z**5 / 60 - z**6 / 24
return 2.0 * G / g**2
G[big] = z[big] - 1.5 + 2 * np.exp(-z[big]) - 0.5 * np.exp(-2 * z[big])
zs = z[~big]
G[~big] = zs**3 / 3 - zs**4 / 4 + 7 * zs**5 / 60 - zs**6 / 24
return 2.0 * G / g**2
def cov_xi1_xi1(s, t, g):
"""Cov(xi1(s), xi1(t)) for arbitrary s,t>0 (symmetric)."""
s = np.asarray(s, dtype=float)
t = np.asarray(t, dtype=float)
lo = np.minimum(s, t)
hi = np.maximum(s, t)
tau = hi - lo
e = np.exp(-g * tau)
return (2.0 / g) * (
(g * lo - _em1(g * lo)) / g
- e * _em1(g * lo) / g
+ e * _em1(2 * g * lo) / (2 * g)
)
def cov_xi1_xi2(s, h, g):
"""Cov(xi1(s), xi2(h)) for 0 < s <= h."""
s = np.asarray(s, dtype=float)
tp = h - s
return 2.0 * np.exp(-g * tp) * (_em1(g * s) / g - _em1(2 * g * s) / (2 * g))
def var_xi2(h, g):
return _em1(2 * g * h)
# ----------------------------------------------------------------------------
# vec(S) = (S11, S12, S22) helpers
# ----------------------------------------------------------------------------
def T_from_M(M):
"""3x3 matrix implementing vec(S) -> vec(M S M^T). M: (...,2,2)."""
m11, m12 = M[..., 0, 0], M[..., 0, 1]
m21, m22 = M[..., 1, 0], M[..., 1, 1]
T = np.empty(M.shape[:-2] + (3, 3))
T[..., 0, 0] = m11 * m11
T[..., 0, 1] = 2 * m11 * m12
T[..., 0, 2] = m12 * m12
T[..., 1, 0] = m11 * m21
T[..., 1, 1] = m11 * m22 + m12 * m21
T[..., 1, 2] = m12 * m22
T[..., 2, 0] = m21 * m21
T[..., 2, 1] = 2 * m21 * m22
T[..., 2, 2] = m22 * m22
return T
def _augment(T, q):
"""(...,3,3),(...,3) -> (...,4,4) affine map [[T,q],[0,1]]."""
sh = T.shape[:-2]
Aug = np.zeros(sh + (4, 4))
Aug[..., :3, :3] = T
Aug[..., :3, 3] = q
Aug[..., 3, 3] = 1.0
return Aug
def propagate(T, q, A, S0, m0, n):
"""Apply the affine second-moment map n times (repeated squaring).
T,q : second-moment map S -> T S + q (vec form)
A : mean map m -> A m
returns S_n (…,3), m_n (…,2)
"""
n = int(n)
Aug = _augment(T, q)
Augn = np.linalg.matrix_power(Aug, n)
S0a = np.concatenate([S0, np.ones(S0.shape[:-1] + (1,))], axis=-1)
Sn = np.einsum("...ij,...j->...i", Augn, S0a)[..., :3]
An = np.linalg.matrix_power(A, n)
mn = np.einsum("...ij,...j->...i", An, m0)
return Sn, mn
def compose_step(T, q, A, S, m):
"""One extra step of the (T,q,A) map."""
Sn = np.einsum("...ij,...j->...i", T, S) + q
mn = np.einsum("...ij,...j->...i", A, m)
return Sn, mn
# ----------------------------------------------------------------------------
# KL of the phase-space Gaussian against pi
# ----------------------------------------------------------------------------
def kl_per_coord(S, m, a):
"""KL( N(m,Sigma) || N(0, diag(1/a,1)) ) with S = Sigma + m m^T (vec form).
KL = 1/2 [ a*S11 + S22 - 2 - log(a) - log det Sigma ]
"""
S11, S12, S22 = S[..., 0], S[..., 1], S[..., 2]
m1, m2 = m[..., 0], m[..., 1]
c11 = S11 - m1 * m1
c12 = S12 - m1 * m2
c22 = S22 - m2 * m2
det = c11 * c22 - c12 * c12
det = np.maximum(det, 1e-300)
return 0.5 * (a * S11 + S22 - 2.0 - np.log(a) - np.log(det))
# ----------------------------------------------------------------------------
# ULMC (Eq. 3.2)
# ----------------------------------------------------------------------------
def ulmc_maps(a, h, g):
"""Return A (mean map), T, q for standard ULMC on coordinate stiffness a."""
a = np.asarray(a, dtype=float)
e = np.exp(-g * h)
c1 = _em1(g * h) / g # (1-e^{-gh})/g
G = (h - c1) / g # (1/g)(h - (1-e^{-gh})/g)
M = np.empty(a.shape + (2, 2))
M[..., 0, 0] = 1.0 - a * G
M[..., 0, 1] = c1
M[..., 1, 0] = -a * c1
M[..., 1, 1] = e
q = np.empty(a.shape + (3,))
q[..., 0] = var_xi1(h, g)
q[..., 1] = cov_xi1_xi2(h, h, g)
q[..., 2] = var_xi2(h, g)
return M, T_from_M(M), q
# ----------------------------------------------------------------------------
# Randomized midpoint discretization (Eq. 3.4), doubly randomized
# ----------------------------------------------------------------------------
def _rmd_nodes(h, g, nq=48):
"""Gauss-Legendre nodes/weights for the laws (3.3) of u and v on [0,1]."""
x, w = np.polynomial.legendre.leggauss(nq)
t = 0.5 * (x + 1.0)
w = 0.5 * w
c1 = _em1(g * h) / g
pu = h * _em1(g * (1 - t) * h) / (h - c1)
pv = h * g * np.exp(-g * (1 - t) * h) / _em1(g * h)
wu = w * pu
wv = w * pv
return t, wu / wu.sum(), wv / wv.sum()
def rmd_maps(a, h, g, nq=48, literal_typo=False):
"""Return A (mean map), T, q for the doubly-randomized midpoint scheme.
literal_typo=True reproduces the coefficient (1-e^{-gh})/g printed in front
of P in the two midpoint lines of Eq. (3.4); the surrounding text says the
midpoints are obtained by *standard ULMC of step u_n h / v_n h*, which gives
(1-e^{-g u_n h})/g. Default False = the text-consistent version.
"""
a = np.asarray(a, dtype=float)[..., None] # broadcast over quad nodes
t, wu, wv = _rmd_nodes(h, g, nq)
e = np.exp(-g * h)
c1 = _em1(g * h) / g
G = (h - c1) / g
su, sv = t * h, t * h
gu = (su - _em1(g * su) / g) / g
gv = (sv - _em1(g * sv) / g) / g
cu = c1 * np.ones_like(su) if literal_typo else _em1(g * su) / g
cv = c1 * np.ones_like(sv) if literal_typo else _em1(g * sv) / g
# X' = [1 - aG(1 - a gu)] X + [c1 - aG cu] P + xi1(h) - aG xi1(u h)
# P' = [-a c1 (1 - a gv)] X + [e - a c1 cv] P + xi2(h) - a c1 xi1(v h)
r11 = 1.0 - a * G * (1.0 - a * gu)
r12 = c1 - a * G * cu
r21 = -a * c1 * (1.0 - a * gv)
r22 = e - a * c1 * cv
E11 = (r11 * wu).sum(-1)
E12 = (r12 * wu).sum(-1)
E21 = (r21 * wv).sum(-1)
E22 = (r22 * wv).sum(-1)
sh = a.shape[:-1]
A = np.empty(sh + (2, 2))
A[..., 0, 0], A[..., 0, 1] = E11, E12
A[..., 1, 0], A[..., 1, 1] = E21, E22
# E[M S M^T]: row1 depends on u only, row2 on v only, u indep v.
T = np.empty(sh + (3, 3))
T[..., 0, 0] = (r11 * r11 * wu).sum(-1)
T[..., 0, 1] = 2 * (r11 * r12 * wu).sum(-1)
T[..., 0, 2] = (r12 * r12 * wu).sum(-1)
T[..., 1, 0] = E11 * E21
T[..., 1, 1] = E11 * E22 + E12 * E21
T[..., 1, 2] = E12 * E22
T[..., 2, 0] = (r21 * r21 * wv).sum(-1)
T[..., 2, 1] = 2 * (r21 * r22 * wv).sum(-1)
T[..., 2, 2] = (r22 * r22 * wv).sum(-1)
# noise covariance, averaged over u,v
V1h = var_xi1(h, g)
V2h = var_xi2(h, g)
C12h = cov_xi1_xi2(h, h, g)
Cu = cov_xi1_xi1(su, h, g) # Cov(xi1(uh), xi1(h))
Cv = cov_xi1_xi1(sv, h, g)
V1u = var_xi1(su, g)
V1v = var_xi1(sv, g)
C2u = cov_xi1_xi2(su, h, g) # Cov(xi1(uh), xi2(h))
C2v = cov_xi1_xi2(sv, h, g)
aG = a * G
ac1 = a * c1
q = np.empty(sh + (3,))
q[..., 0] = ((V1h - 2 * aG * Cu + aG**2 * V1u) * wu).sum(-1)
q[..., 2] = ((V2h - 2 * ac1 * C2v + ac1**2 * V1v) * wv).sum(-1)
# cross term needs E_{u,v}[Cov(xi1(uh), xi1(vh))]
Cuv = cov_xi1_xi1(su[:, None], sv[None, :], g) # (nq,nq)
Ecuv = wu @ Cuv @ wv
q[..., 1] = (
C12h
- (ac1[..., 0]) * (Cv * wv).sum(-1)
- (aG[..., 0]) * (C2u * wu).sum(-1)
+ (aG[..., 0]) * (ac1[..., 0]) * Ecuv
)
return A, T, q
# ----------------------------------------------------------------------------
# Overdamped baselines
# ----------------------------------------------------------------------------
def lmc_maps(a, h):
"""Euler-Maruyama LMC: x' = (1-h a) x + sqrt(2h) N(0,1). 1-d state, but we
embed in the same 2-d (x,p) formalism with a dummy p ~ N(0,1) exactly at
stationarity, so KL is comparable."""
a = np.asarray(a, dtype=float)
M = np.zeros(a.shape + (2, 2))
M[..., 0, 0] = 1.0 - h * a
M[..., 1, 1] = 0.0 # p redrawn from N(0,1) each step
q = np.zeros(a.shape + (3,))
q[..., 0] = 2.0 * h
q[..., 2] = 1.0
return M, T_from_M(M), q
def composite_lmc_maps(a, h, alpha):
"""Composite / exponential-integrator LMC in the sense of Freund et al.
(2022): V = (alpha/2)||x||^2 + R(x) with grad R = (A - alpha I)x; the
strongly-convex quadratic part is integrated EXACTLY (OU), R explicitly.
x' = e^{-alpha h} x - ((1-e^{-alpha h})/alpha) (a-alpha) x
+ N(0, (1-e^{-2 alpha h})/alpha)
"""
a = np.asarray(a, dtype=float)
ea = np.exp(-alpha * h)
c = _em1(alpha * h) / alpha
M = np.zeros(a.shape + (2, 2))
M[..., 0, 0] = ea - c * (a - alpha)
M[..., 1, 1] = 0.0
q = np.zeros(a.shape + (3,))
q[..., 0] = _em1(2 * alpha * h) / alpha
q[..., 2] = 1.0
return M, T_from_M(M), q
# ----------------------------------------------------------------------------
# driver: KL(mu_N || pi) as a function of N
# ----------------------------------------------------------------------------
def kl_at(N, maps, S0, m0, a, mult, final_ulmc=None):
"""KL after N steps of `maps`=(A,T,q) (plus one final ULMC step if given)."""
A, T, q = maps
if final_ulmc is not None:
if N < 1:
return np.inf
S, m = propagate(T, q, A, S0, m0, N - 1)
S, m = compose_step(final_ulmc[1], final_ulmc[2], final_ulmc[0], S, m)
else:
S, m = propagate(T, q, A, S0, m0, N)
return float(np.sum(mult * kl_per_coord(S, m, a)))
def n_grid(nmax=int(3e9), ratio=1.03):
ns, n = [1], 1
while n < nmax:
nn = max(n + 1, int(round(n * ratio)))
ns.append(nn)
n = nn
return np.array(ns, dtype=np.int64)
def iters_to_eps(
maps, S0, m0, a, mult, eps2, nmax=int(3e9), ratio=1.03, final_ulmc=None
):
"""Smallest N on a geometric grid with KL(mu_N||pi) <= eps2; np.inf if none.
Also returns the KL floor (KL at the largest grid point)."""
grid = n_grid(nmax, ratio)
lo, hi = 0, len(grid) - 1
floor = kl_at(int(grid[hi]), maps, S0, m0, a, mult, final_ulmc)
if not np.isfinite(floor) or floor > eps2:
return np.inf, floor
# KL decreases then plateaus; bisect for first grid index under eps2
while lo < hi:
mid = (lo + hi) // 2
v = kl_at(int(grid[mid]), maps, S0, m0, a, mult, final_ulmc)
if np.isfinite(v) and v <= eps2:
hi = mid
else:
lo = mid + 1
return int(grid[lo]), floor
def kl_floor(maps, S0, m0, a, mult, nbig=int(2e9), final_ulmc=None):
return kl_at(nbig, maps, S0, m0, a, mult, final_ulmc)
def best_over_h(
hs, make_maps, S0f, m0f, a, mult, eps2, final_ulmc_f=None, nmax=int(3e9)
):
"""min over step sizes h of the number of steps to reach KL <= eps2.
Returns (Nbest, hbest, table)."""
table = []
best = (np.inf, None)
for h in hs:
maps = make_maps(h)
fin = final_ulmc_f(h) if final_ulmc_f is not None else None
N, fl = iters_to_eps(
maps, S0f(h), m0f(h), a, mult, eps2, nmax=nmax, final_ulmc=fin
)
table.append((float(h), float(N) if np.isfinite(N) else None, float(fl)))
if N < best[0]:
best = (N, h)
return best[0], best[1], table
def kl_curve(maps, S0, m0, a, mult, grid, final_ulmc=None):
"""KL(mu_n||pi) for every n in `grid` (ascending ints). O(len(grid)*log N)."""
A, T, q = maps
Aug = _augment(T, q)
K = Aug.shape[0]
accS = np.broadcast_to(np.eye(4), (K, 4, 4)).copy()
accM = np.broadcast_to(np.eye(2), (K, 2, 2)).copy()
S0a = np.concatenate([S0, np.ones((K, 1))], axis=-1)
out = np.empty(len(grid))
prev = 0
for i, n in enumerate(grid):
d = int(n) - prev
if d > 0:
accS = accS @ np.linalg.matrix_power(Aug, d)
accM = accM @ np.linalg.matrix_power(A, d)
prev = int(n)
S = np.einsum('kij,kj->ki', accS, S0a)[:, :3]
m = np.einsum('kij,kj->ki', accM, m0)
if final_ulmc is not None:
S, m = compose_step(final_ulmc[1], final_ulmc[2], final_ulmc[0], S, m)
out[i] = float(np.sum(mult * kl_per_coord(S, m, a)))
return out
def first_below(grid, vals, thr):
idx = np.nonzero(np.isfinite(vals) & (vals <= thr))[0]
return int(grid[idx[0]]) if len(idx) else None
def n_eps_over_h(hs, make_maps, S0, m0, a, mult, eps2, grid,
final_ulmc_f=None):
"""min over h of #steps to reach KL<=eps2. Returns dict."""
best_N, best_h, rows = None, None, []
for h in hs:
mp = make_maps(h)
fin = final_ulmc_f(h) if final_ulmc_f is not None else None
vals = kl_curve(mp, S0, m0, a, mult, grid, final_ulmc=fin)
N = first_below(grid, vals, eps2)
rows.append({"h": float(h), "N": N, "floor": float(vals[-1])})
if N is not None and (best_N is None or N < best_N):
best_N, best_h = N, float(h)
return {"N_eps": best_N, "h_star": best_h, "table": rows}

Xet Storage Details

Size:
15.1 kB
·
Xet hash:
0cc498f88c90999e100346330db689fd4758e31c7abdde46bedea68a3bc5b6db

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