Note
Go to the end to download the full example code.
Fit with optimistix BFGS and Levenberg-Marquardt¶
optimistix solves minimization and least-squares problems on JAX pytrees.
BFGS minimizes potential from flatppl.Module.logdensity() in
unconstrained coordinates. Levenberg-Marquardt needs residuals rather than a
density, so a FlatPPL query supplies them. The model is a Normal likelihood
with unknown mean and precision under a conjugate Normal-Gamma prior; every
fitted point has a closed form.
Runtime budget: 120 s.
The model and its closed forms¶
The likelihood L peaks at the sample mean and tau = n / S with
S = sum((y - mean)^2). The posterior peaks at m_n and
(a_n - 1/2) / b_n.
data = np.array([1.2, 0.4, 2.1, 1.7, 0.9, 1.5, 0.3, 1.1])
model = flatppl(
"""
tau ~ Gamma(shape = 3.0, rate = 2.0)
mu ~ Normal(mu = 0.0, sigma = 1.0 / sqrt(2.0 * tau))
y ~ iid(Normal(mu = mu, sigma = 1.0 / sqrt(tau)), 8)
observed = external(cartpow(reals, 8))
prior = lawof(record(mu = mu, tau = tau))
L = likelihoodof(kernelof(record(y = y), mu = mu, tau = tau), record(y = observed))
posterior = bayesupdate(L, prior)
"""
).set(observed=data)
a0, b0, k0, m0, n = 3.0, 2.0, 2.0, 0.0, data.size
mean = data.mean()
squares = ((data - mean) ** 2).sum()
kn, an = k0 + n, a0 + n / 2
mn = (k0 * m0 + n * mean) / kn
bn = b0 + 0.5 * squares + k0 * n * (mean - m0) ** 2 / (2 * kn)
mle = {"mu": mean, "tau": n / squares}
mode = {"mu": mn, "tau": (an - 0.5) / bn}
BFGS in unconstrained coordinates¶
jacobian=False keeps each density itself in the free coordinates
(mu, log tau), so its minimizer maps through constrain to the mode in
model coordinates. optimistix passes args to the function; here it is
unused.
Float64 is used here because BFGS stops when the function stops changing,
which in float32 happens about 1e-4 short of the optimum. FlatPPL itself does
not require it. jax.enable_x64 scopes it to these fits.
def potential_of(fit: Any) -> Any:
"""Wrap ``fit.potential`` in optimistix's ``fn(y, args)`` form."""
def potential(position: Any, args: None) -> jax.Array:
return fit.potential(position)
return potential
solver = optx.BFGS(rtol=1e-9, atol=1e-9)
for name, measure, expected in [("MAP", "posterior", mode), ("MLE", "L", mle)]:
with jax.enable_x64(True):
fit = model.logdensity(measure, jacobian=False, dtype="float64")
solution = optx.minimise(potential_of(fit), solver, fit.init)
point = {
key: float(value) for key, value in fit.constrain(solution.value).items()
}
print(name, {key: round(value, 8) for key, value in point.items()})
for key, value in expected.items():
assert abs(point[key] - value) < 1e-8 * abs(value), (name, key)
MAP {'mu': 0.92, 'tau': 1.47794452}
MLE {'mu': 1.15, 'tau': 2.98507463}
Levenberg-Marquardt on residuals¶
A least-squares solver wants the residual vector, not its summed square. This
query returns observed - mu for a given mu; its least-squares
solution is the sample mean, the maximum likelihood mu for any tau.
It runs in float64 for the same stopping reason.
with jax.enable_x64(True):
residuals = (
flatppl(
"""
observed = external(cartpow(reals, 8))
mu = elementof(reals)
inputs = mu
outputs = observed .- mu
"""
)
.set(observed=data)
.compile(dtype="float64")
)
least_squares = optx.least_squares(
lambda mu, args: residuals(mu),
optx.LevenbergMarquardt(rtol=1e-9, atol=1e-9),
jnp.float64(0.0),
)
fitted_mean = float(least_squares.value)
print("Levenberg-Marquardt mu:", round(fitted_mean, 8))
assert abs(fitted_mean - mean) < 1e-8 * mean
Levenberg-Marquardt mu: 1.15
The posterior’s normalization¶
The posterior in free coordinates, with its Jacobian, integrates to the closed-form Normal-Gamma evidence.
log_evidence = (
lgamma(an) - lgamma(a0) + a0 * log(b0) - an * log(bn)
+ 0.5 * log(k0 / kn) - n / 2 * log(2 * pi)
) # fmt: skip
density = model.logdensity("posterior")
mu_axis = jnp.linspace(-3.0, 5.0, 401)
log_tau_axis = jnp.linspace(-4.0, 3.0, 401)
grid = jax.vmap(
lambda m: jax.vmap(lambda t: density({"mu": m, "tau": t}))(log_tau_axis)
)(mu_axis)
total = np.trapezoid(
np.trapezoid(np.exp(np.asarray(grid)), np.asarray(log_tau_axis), axis=1),
np.asarray(mu_axis),
)
print(f"log evidence: grid {np.log(total):.6f}, closed form {log_evidence:.6f}")
assert abs(np.log(total) - log_evidence) < 1e-6
log evidence: grid -10.558731, closed form -10.558731
Total running time of the script: (0 minutes 1.250 seconds)