The ChEES criterion is 41E[(∥θ′−Eθ∥2−∥θ−Eθ∥2)2]: an ESJD of the centred, squared norm. It is shift/rotation invariant, dominated by high-variance directions and focused on second moments. Algorithm 1 ascends its gradient with Adam on logT using one jittered length per iteration shared by all chains, weights chains by acceptance probability, and tunes step size by dual averaging on the harmonic-mean acceptance probability (target 0.651).
Overview
Section 2.1 argued for minimising first-order autocorrelation of ((θ−Eθ)⊤v)2 with v the highest-variance direction. We cannot know v, so ChEES is a heuristic satisfying three desiderata: (i) invariance to shifts and rotations, (ii) insensitivity to low-variance directions, (iii) focus on second-moment estimation.
Main Content
ChEES (eq. 7)
With θ∼p, r∼N(0,I) at equilibrium and (θ′,r′)=leapfrogϵ,L(θ,r):
ChEES≜41E[(∥θ′−E[θ]∥2−∥θ−E[θ]∥2)2].
Centring gives shift invariance; dependence only on squared norms gives rotation invariance.
ChEES decomposition (eqs 8-9)
Take E[θ]=0, E[θiθj]=0 for i=j, and σ⋅2=∑dσd2. Then
The first term is constant in T. The second equals ∑i(E[θi4]−σi4)ρi+∑i,j=iE[((θi′)2−σi2)(θj2−σj2)] where ρi is the first-order autocorrelation of θi2. The cross terms are small if dimensions are approximately independent after rotation. The weights E[θi4]−σi4 (the variance of θi2) are generally proportional to σi4, so the criterion aggressively weights high-variance directions, as desired.
Accept with αn(m)=min{1,exp(L(θ~)−L(θn−1))N(r~;0,I)/N(r;0,I)}.
If n<Nwarmup: compute the harmonic-mean acceptance αˉ=((1/M)∑m1/α(m))−1 and update ϵn by dual averaging toward αˉ=0.651; estimate means θ~^=M1∑θ~(m), θ^=M1∑θn−1(m); compute per-chain gradient estimates
g^(m)=tn(∥θ~(m)−θ~^∥2−∥θn−1(m)−θ^∥2)(θ~(m)−θ~^)⊤r~(m),
combine g^=∑mα(m)g^(m)/∑mα(m), update logTn with Adam, and set ϵˉ←0.9ϵˉ+0.1ϵn, Tˉ←0.9Tˉ+0.1Tn.
At n=Nwarmup freeze ϵn:N=ϵˉ, Tn:N=Tˉ.
Design choices and their rationale
Adam on logT because the scale of g^ is problem dependent: step size α=0.025 (not sensitive), β1=0 (no momentum), β2=0.95.
One shared t for all chains so every chain does the same work per iteration (simpler, faster code on SIMD).
Weight by acceptance probability so obviously invalid states do not drive g^.
Estimate E[θ] from the initial and final integrator states across chains.
Harmonic-mean acceptance prevents a “straggler” chain stuck in a region requiring small steps from being ignored by an arithmetic average; if any chain has tiny α the harmonic mean pulls ϵ down for all.
Round upt to an integer multiple of ϵ; adapt Tonly during warmup to avoid bias from hyperparameters depending on recent states.
Halton sequence (Owen 2017) instead of i.i.d. uniform jitter for more evenly spread lengths. It is determined a priori so the stationary distribution is unaffected.
Examples
Numpy sketch of ChEES-HMC on a 3-D Gaussian (tested)
Target N(0,diag(1,3,0.3)2), 64 chains, 1500 iterations, 1000 warmup. In three seeds the adapted T ended at about 6.0, 6.2 and 6.5, close to the 2.25σmax=6.75 rule from Hoffman 2021 - Gaussian Autocorrelation Analysis and Trajectory Jitter (the match is approximate, not guaranteed; this is an illustration, not the paper’s code).
import numpy as npdef halton(i, b=2): f, r = 1.0, 0.0 while i > 0: f /= b; r += f*(i % b); i //= b return r# per iteration n, with theta (M,D), eps, T, adam state:# t = halton(n)*T ; L = ceil(t/eps) ; r ~ N(0,I)# th2, r2 = leapfrog(theta, r, eps, L, gradL)# a = min(1, exp(logp(th2)-logp(theta) - .5|r2|^2 + .5|r|^2))# g_m = t*(|th2-mean(th2)|^2 - |theta-mean(theta)|^2) * sum((th2-mean(th2))*r2, -1)# g = sum(a*g_m)/sum(a); Adam step on log T with lr .025, b1=0, b2=.95# eps via dual averaging on harmonic mean of a (target .651)# accept per chain with prob a; freeze eps,T at warmup end (use EMAs)
Examples (PyMC-style)
Motivating example E86 (examples page): ChEES-HMC is available in blackjax. Untested sketch:
import blackjax, jax, jax.numpy as jnp# logdensity_fn: a JAX log-density over a flat position vector (e.g. from pymc.sampling.jax or hand-written)warmup = blackjax.chees_adaptation(logdensity_fn, num_chains=128)key = jax.random.key(0)(last_states, parameters), _ = warmup.run(key, initial_positions, step_size=0.1, optim=None, num_steps=500)kernel = blackjax.dynamic_hmc(logdensity_fn, **parameters).step # then jax.vmap over chains
API names and arguments may differ across blackjax versions; check the installed docs before use.