Module gabenet.mcmc
Expand source code
from typing import Any, Callable, Optional
import warnings
import haiku as hk
import jax
from jax import random
from gabenet.sugar import scannable
def sample_markov_chain(
key,
kernel: Callable[[tuple], tuple[Any, dict]] | hk.TransformedWithState,
n_samples: int,
n_burnin_steps: int,
n_chains: Optional[int] = None,
params: Optional[hk.Params] = None,
initial_state: Optional[hk.State] = None,
n_leap_size: int = 1,
):
"""Take samples using Markov chain Monte Carlo.
Args:
key: Pseudo random number generator key.
kernel: The transition kernel that advances the Markov chain by one step. The
kernel is either a haiku `TransformedWithState` or a function compatible
with `TransformedWithState.apply`.
n_samples: Number of samples to generate.
n_burnin_steps: Advance the chain by this many steps before taking a sample.
n_chains: Number of (independent) Markov chains to run.
params: The chain-specific but state-independent `params` (a pytree) of
the Markov chain. When `None`, kernel is assumed to be a
`TransformedWithState` to initialise using `kernel.init`.
initial_state: Starting `state` (a pytree) of the Markov chain. Leading
dimension corresponds to a chain.
n_leap_size: Number of successive steps between samples.
Returns:
A pair of `(params, state)`, each a pytree. The leading dimension of
`params` and `state` refers to the chain. The second dimension of `state`
corresponds to the samples in per chain.
"""
key_seq = hk.PRNGSequence(key)
_scannable_kernel_fn = scannable(kernel)
if initial_state is None or params is None:
if not isinstance(kernel, hk.TransformedWithState):
raise ValueError(
"Kernel must be `TransformedWithState` to be able to initialise"
"`params` and `state` when either is None."
)
if n_chains is None:
raise ValueError("Number of chains not specified!")
init_key_per_chain = random.split(next(key_seq), num=n_chains)
params, initial_state = jax.vmap(kernel.init)(init_key_per_chain)
else:
states_leaves = jax.tree_util.tree_leaves(initial_state)
# Leading axis of states corresponds to chains.
n_chains_expected = states_leaves[0].shape[0]
if n_chains is None:
n_chains = n_chains_expected
elif n_chains != n_chains_expected:
raise ValueError(
f"The leading axis of `initial_state` suggests "
f"{n_chains_expected} chains, but the argument is set to "
f"`n_chains={n_chains}`."
)
def _leapfrog(carry, n_steps: int):
"""A for-loop (from 0,..,`n_steps-1`) that runs `step`."""
carry_out, _ = jax.lax.scan(_scannable_kernel_fn, carry, None, length=n_steps)
state = carry_out[1]
return carry_out, state
def _sampler(params_init, state_init, key, n_sample_size: int):
"""Samper for a single Markov chain."""
carry = (params_init, state_init, key)
# 1) Take first sample after `(n_burnin - n_leap_size) + n_leap_size` steps.
# 2) Take subsequent samples after `n_leap_size` steps.
carry, _ = _leapfrog(carry, n_steps=n_burnin_steps - n_leap_size)
carry, stacked_states = jax.lax.scan(
lambda c, _: _leapfrog(c, n_leap_size),
carry,
xs=None,
length=n_sample_size,
)
return stacked_states
n_devices = jax.local_device_count()
if n_devices == 1:
warnings.warn("Only one visible device in JAX. Reconfigure XLA_FLAGS.")
if n_devices == n_chains:
# Run each chain on a separate device.
_vectorised_kernel_fn = jax.pmap(
_sampler, in_axes=(0, 0, 0, None), static_broadcasted_argnums=3
)
else:
# Vectorise with vmap instead of sharding across devices.
warnings.warn(
f"Chains (n={n_chains}) not divisible across devices. Falling back to vmap."
)
_vectorised_kernel_fn = jax.vmap(_sampler, in_axes=(0, 0, 0, None))
if n_samples % n_chains > 0:
raise ValueError(
"Number of samples {n_samples} not divisible by number of chains {n_chains}."
)
# Generate an initial state for each chain.
keys = random.split(next(key_seq), num=n_chains)
n_samples_per_chain = n_samples // n_chains
state = _vectorised_kernel_fn(params, initial_state, keys, n_samples_per_chain)
return params, state
Functions
def sample_markov_chain(key, kernel: Union[Callable[[tuple], tuple[Any, dict]], haiku._src.transform.TransformedWithState], n_samples: int, n_burnin_steps: int, n_chains: Optional[int] = None, params: Optional[Mapping[str, Mapping[str, jax.Array]]] = None, initial_state: Optional[Mapping[str, Mapping[str, jax.Array]]] = None, n_leap_size: int = 1)-
Take samples using Markov chain Monte Carlo.
Args
key- Pseudo random number generator key.
kernel- The transition kernel that advances the Markov chain by one step. The
kernel is either a haiku
TransformedWithStateor a function compatible withTransformedWithState.apply. n_samples- Number of samples to generate.
n_burnin_steps- Advance the chain by this many steps before taking a sample.
n_chains- Number of (independent) Markov chains to run.
params- The chain-specific but state-independent
params(a pytree) of the Markov chain. WhenNone, kernel is assumed to be aTransformedWithStateto initialise usingkernel.init. initial_state- Starting
state(a pytree) of the Markov chain. Leading dimension corresponds to a chain. n_leap_size- Number of successive steps between samples.
Returns
A pair of
(params, state), each a pytree. The leading dimension ofparamsandstaterefers to the chain. The second dimension ofstatecorresponds to the samples in per chain.Expand source code
def sample_markov_chain( key, kernel: Callable[[tuple], tuple[Any, dict]] | hk.TransformedWithState, n_samples: int, n_burnin_steps: int, n_chains: Optional[int] = None, params: Optional[hk.Params] = None, initial_state: Optional[hk.State] = None, n_leap_size: int = 1, ): """Take samples using Markov chain Monte Carlo. Args: key: Pseudo random number generator key. kernel: The transition kernel that advances the Markov chain by one step. The kernel is either a haiku `TransformedWithState` or a function compatible with `TransformedWithState.apply`. n_samples: Number of samples to generate. n_burnin_steps: Advance the chain by this many steps before taking a sample. n_chains: Number of (independent) Markov chains to run. params: The chain-specific but state-independent `params` (a pytree) of the Markov chain. When `None`, kernel is assumed to be a `TransformedWithState` to initialise using `kernel.init`. initial_state: Starting `state` (a pytree) of the Markov chain. Leading dimension corresponds to a chain. n_leap_size: Number of successive steps between samples. Returns: A pair of `(params, state)`, each a pytree. The leading dimension of `params` and `state` refers to the chain. The second dimension of `state` corresponds to the samples in per chain. """ key_seq = hk.PRNGSequence(key) _scannable_kernel_fn = scannable(kernel) if initial_state is None or params is None: if not isinstance(kernel, hk.TransformedWithState): raise ValueError( "Kernel must be `TransformedWithState` to be able to initialise" "`params` and `state` when either is None." ) if n_chains is None: raise ValueError("Number of chains not specified!") init_key_per_chain = random.split(next(key_seq), num=n_chains) params, initial_state = jax.vmap(kernel.init)(init_key_per_chain) else: states_leaves = jax.tree_util.tree_leaves(initial_state) # Leading axis of states corresponds to chains. n_chains_expected = states_leaves[0].shape[0] if n_chains is None: n_chains = n_chains_expected elif n_chains != n_chains_expected: raise ValueError( f"The leading axis of `initial_state` suggests " f"{n_chains_expected} chains, but the argument is set to " f"`n_chains={n_chains}`." ) def _leapfrog(carry, n_steps: int): """A for-loop (from 0,..,`n_steps-1`) that runs `step`.""" carry_out, _ = jax.lax.scan(_scannable_kernel_fn, carry, None, length=n_steps) state = carry_out[1] return carry_out, state def _sampler(params_init, state_init, key, n_sample_size: int): """Samper for a single Markov chain.""" carry = (params_init, state_init, key) # 1) Take first sample after `(n_burnin - n_leap_size) + n_leap_size` steps. # 2) Take subsequent samples after `n_leap_size` steps. carry, _ = _leapfrog(carry, n_steps=n_burnin_steps - n_leap_size) carry, stacked_states = jax.lax.scan( lambda c, _: _leapfrog(c, n_leap_size), carry, xs=None, length=n_sample_size, ) return stacked_states n_devices = jax.local_device_count() if n_devices == 1: warnings.warn("Only one visible device in JAX. Reconfigure XLA_FLAGS.") if n_devices == n_chains: # Run each chain on a separate device. _vectorised_kernel_fn = jax.pmap( _sampler, in_axes=(0, 0, 0, None), static_broadcasted_argnums=3 ) else: # Vectorise with vmap instead of sharding across devices. warnings.warn( f"Chains (n={n_chains}) not divisible across devices. Falling back to vmap." ) _vectorised_kernel_fn = jax.vmap(_sampler, in_axes=(0, 0, 0, None)) if n_samples % n_chains > 0: raise ValueError( "Number of samples {n_samples} not divisible by number of chains {n_chains}." ) # Generate an initial state for each chain. keys = random.split(next(key_seq), num=n_chains) n_samples_per_chain = n_samples // n_chains state = _vectorised_kernel_fn(params, initial_state, keys, n_samples_per_chain) return params, state