Source code for blackjax.mcmc.laplace_dynamic_hmc

# Copyright 2020- The Blackjax Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Dynamic HMC on the Laplace-approximated marginal log-density.

Combines the warm-started Laplace marginalisation of
:mod:`~blackjax.mcmc.laplace_hmc` with the quasi-random integration-step
schedule of :mod:`~blackjax.mcmc.dynamic_hmc`.

The state carries both extra fields:

- ``theta_star``: MAP of latent variables at the current ``phi``, used to
  warm-start L-BFGS at every leapfrog step.
- ``random_generator_arg``: Halton index (or PRNG key) used by
  ``integration_steps_fn`` to draw the number of leapfrog steps each
  transition.

Two variants are available at the top level:

+---------------------------+------------------+------------------------------+
| Alias                     | Proposal         | Notes                        |
+===========================+==================+==============================+
| ``blackjax.laplace_dhmc`` | endpoint + M-H   | default                      |
| ``blackjax.laplace_dmhmc``| full trajectory  | multinomial, no rejection    |
+---------------------------+------------------+------------------------------+

Typical usage::

    sampler = blackjax.laplace_dhmc(
        log_joint, theta_init=jnp.zeros(n),
        step_size=0.1, inverse_mass_matrix=jnp.ones(d),
    )
    state = sampler.init(phi_init, rng_key)
    new_state, info = jax.jit(sampler.step)(rng_key, state)
    # new_state.theta_star  — MAP latent at accepted phi
    # new_state.random_generator_arg  — advanced Halton index
"""

from collections.abc import Callable
from typing import NamedTuple, cast

import jax

import blackjax.mcmc.dynamic_hmc as dynamic_hmc
import blackjax.mcmc.hmc as hmc
import blackjax.mcmc.integrators as integrators
import blackjax.mcmc.metrics as metrics
from blackjax.base import SamplingAlgorithm, build_sampling_algorithm
from blackjax.mcmc.dynamic_hmc import DynamicHMCState
from blackjax.mcmc.laplace_marginal import (
    LaplaceHMCInfo,
    LaplaceMarginal,
    laplace_marginal_factory,
)
from blackjax.types import Array, ArrayLikeTree, ArrayTree, PRNGKey

__all__ = [
    "LaplaceDynamicHMCState",
    "LaplaceHMCInfo",
    "init",
    "build_kernel",
    "as_top_level_api",
]


[docs] class LaplaceDynamicHMCState(NamedTuple): """State of the Laplace dynamic HMC sampler. position Current hyperparameter position ``phi``. logdensity Current value of the Laplace log-marginal ``log p̂(phi | y)``. logdensity_grad Gradient of ``log p̂(phi | y)`` w.r.t. ``phi``. theta_star MAP of the latent variables at the current ``phi``. Warm-starts L-BFGS at every leapfrog step. random_generator_arg Halton index or PRNG key consumed by ``integration_steps_fn`` to draw the number of leapfrog steps for the next transition. """
[docs] position: ArrayTree
[docs] logdensity: float
[docs] logdensity_grad: ArrayTree
[docs] theta_star: ArrayTree
[docs] random_generator_arg: Array
[docs] def init( position: ArrayLikeTree, laplace: LaplaceMarginal, random_generator_arg: Array, ) -> LaplaceDynamicHMCState: """Create an initial :class:`LaplaceDynamicHMCState`. Parameters ---------- position Initial hyperparameter value ``phi``. laplace A :class:`~blackjax.mcmc.laplace_marginal.LaplaceMarginal` instance. random_generator_arg Initial value for the quasi-random step-count generator (e.g. a PRNG key or Halton index). When called via the top-level API this is seeded automatically from the ``rng_key`` passed to ``.init``. """ (logdensity, theta_star), logdensity_grad = jax.value_and_grad( laplace, has_aux=True )(position) # position is stored unconverted, like hmc.HMCState: build_kernel below # re-feeds LaplaceDynamicHMCState.position into a DynamicHMCState. return LaplaceDynamicHMCState( cast(ArrayTree, position), logdensity, logdensity_grad, theta_star, random_generator_arg, )
[docs] def build_kernel( integrator: Callable = integrators.velocity_verlet, divergence_threshold: float = 1000, next_random_arg_fn: Callable = lambda key: jax.random.split(key)[1], integration_steps_fn: Callable = lambda key: jax.random.randint(key, (), 1, 10), build_proposal: Callable = hmc.hmc_proposal, ) -> Callable: """Build the Laplace dynamic HMC kernel. Parameters ---------- integrator Symplectic integrator for the leapfrog trajectory. divergence_threshold Energy difference above which a transition is declared divergent. next_random_arg_fn Advances ``random_generator_arg`` each step. integration_steps_fn Callable with signature ``(random_generator_arg, *integration_steps_params) -> int`` that draws the number of leapfrog steps for a single transition. Extra positional arguments are supplied at call time via ``integration_steps_params`` on the inner kernel. build_proposal Proposal builder. Defaults to :func:`~blackjax.mcmc.hmc.hmc_proposal` (endpoint + M-H). Pass :func:`~blackjax.mcmc.hmc.multinomial_hmc_proposal` for multinomial trajectory sampling (``blackjax.laplace_dmhmc``). Returns ------- A kernel ``(rng_key, state, laplace, step_size, inverse_mass_matrix) -> (LaplaceDynamicHMCState, LaplaceHMCInfo)``. """ dynamic_kernel = dynamic_hmc.build_kernel( integrator, divergence_threshold, next_random_arg_fn, integration_steps_fn, build_proposal, ) def kernel( rng_key: PRNGKey, state: LaplaceDynamicHMCState, laplace: LaplaceMarginal, step_size: float, inverse_mass_matrix: metrics.MetricTypes, integration_steps_params: tuple = (), ) -> tuple[LaplaceDynamicHMCState, LaplaceHMCInfo]: """One Laplace dynamic-HMC transition. L-BFGS diagnostics from the post-accept ``theta*`` refresh are surfaced in the returned :class:`~blackjax.mcmc.laplace_marginal.LaplaceHMCInfo`. """ theta_prev = state.theta_star def logdensity_fn(phi): lp, _ = laplace(phi, theta_prev) return lp dynamic_state = DynamicHMCState( state.position, state.logdensity, state.logdensity_grad, state.random_generator_arg, ) new_dynamic_state, hmc_info = dynamic_kernel( rng_key, dynamic_state, logdensity_fn, step_size, inverse_mass_matrix, integration_steps_params, ) new_theta_star, lbfgs_diag = laplace.solve_theta_with_info( new_dynamic_state.position, theta_prev ) new_state = LaplaceDynamicHMCState( new_dynamic_state.position, new_dynamic_state.logdensity, new_dynamic_state.logdensity_grad, new_theta_star, new_dynamic_state.random_generator_arg, ) new_info = LaplaceHMCInfo( momentum=hmc_info.momentum, acceptance_rate=hmc_info.acceptance_rate, is_accepted=hmc_info.is_accepted, is_divergent=hmc_info.is_divergent, energy=hmc_info.energy, proposal=hmc_info.proposal, num_integration_steps=hmc_info.num_integration_steps, lbfgs_iter_num=lbfgs_diag.iter_num, lbfgs_error=lbfgs_diag.error, lbfgs_converged=lbfgs_diag.converged, lbfgs_hit_maxiter=lbfgs_diag.hit_maxiter, ) return new_state, new_info return kernel
[docs] def as_top_level_api( log_joint_fn: Callable, theta_init: ArrayLikeTree, step_size: float, inverse_mass_matrix: metrics.MetricTypes, *, divergence_threshold: int = 1000, integrator: Callable = integrators.velocity_verlet, next_random_arg_fn: Callable = lambda key: jax.random.split(key)[1], integration_steps_fn: Callable = lambda key: jax.random.randint(key, (), 1, 10), integration_steps_params: tuple = (), build_proposal: Callable = hmc.hmc_proposal, **optimizer_kwargs, ) -> SamplingAlgorithm: """Dynamic HMC on the Laplace-approximated marginal log-density. Combines Laplace marginalisation over latent variables with a quasi-random number of leapfrog steps per transition, reducing periodic-orbit sensitivity while retaining the computational benefits of operating on the low-dimensional hyperparameter marginal. Parameters ---------- log_joint_fn ``(theta, phi) -> float``. Full log joint ``log p(theta, phi, y)``. theta_init Initial guess for theta; fixes the latent PyTree structure. step_size Leapfrog step size. inverse_mass_matrix Inverse mass matrix (1-D array for diagonal, scalar for isotropic). divergence_threshold Absolute energy difference above which a transition is divergent. integrator Symplectic integrator. Default: velocity Verlet. next_random_arg_fn Advances ``random_generator_arg`` each step. integration_steps_fn Callable with signature ``(random_generator_arg, *integration_steps_params) -> int`` that draws the number of leapfrog steps for a single transition. integration_steps_params Extra positional arguments unpacked into ``integration_steps_fn`` after ``random_generator_arg`` on every step. Defaults to ``()`` so that a plain 1-arg ``integration_steps_fn`` works unchanged. build_proposal Proposal builder. Defaults to :func:`~blackjax.mcmc.hmc.hmc_proposal` (``blackjax.laplace_dhmc``). Pass :func:`~blackjax.mcmc.hmc.multinomial_hmc_proposal` for ``blackjax.laplace_dmhmc``. **optimizer_kwargs Forwarded to :func:`~blackjax.optimizers.lbfgs.minimize_lbfgs`. Useful keys: ``maxiter`` (default 30), ``gtol``, ``ftol``. Returns ------- A :class:`~blackjax.base.SamplingAlgorithm` whose ``step`` returns a :class:`LaplaceDynamicHMCState` and :class:`~blackjax.mcmc.laplace_marginal.LaplaceHMCInfo` (all standard HMCInfo fields plus L-BFGS diagnostics ``lbfgs_hit_maxiter``, etc.). Examples -------- .. code:: sampler = blackjax.laplace_dhmc( log_joint, theta_init=jnp.zeros(n_latent), step_size=0.1, inverse_mass_matrix=jnp.ones(d_phi), maxiter=100, ) state = sampler.init(phi_init, rng_key) new_state, info = jax.jit(sampler.step)(rng_key, state) print(new_state.theta_star) # MAP latent at new phi print(new_state.random_generator_arg) # advanced Halton index """ laplace = laplace_marginal_factory(log_joint_fn, theta_init, **optimizer_kwargs) kernel = build_kernel( integrator, divergence_threshold, next_random_arg_fn, integration_steps_fn, build_proposal, ) return build_sampling_algorithm( kernel, init, laplace, kernel_args=(step_size, inverse_mass_matrix, integration_steps_params), pass_rng_key_to_init=True, )