Module gabenet.calibration
Expand source code
from typing import Any, Callable, Optional
import jax
from jax import random
from jax.tree_util import tree_leaves, tree_map
from matplotlib import pyplot as plt # type: ignore
import numpy as np
from numpy.typing import NDArray
from scipy.stats import binom # type: ignore
def simulation_based_calibration(
forward_fn,
posterior_fn: Callable[[Any, NDArray, int], NDArray],
key,
state_fn: Optional[Callable[[NDArray], NDArray]] = None,
n_replicates: int = 200,
n_posterior_samples: int = 10,
) -> NDArray:
"""Simulation based calibration to validate model implementation.
Verify posterior sampler by comparing prior with posterior distribution.
Relies on the identity: p(w) = integral dy dw' p(w | y) p(y| w') p(w'),
where p(w') is the prior and p(w|y) the posterior distribution.
Args:
forward_fn: Haiku function that generates an ancestral sample.
posterior_fn: Function that samples from the posterior. Takes as arguments
a PRNG key, the observed data, and number of posterior samples. Returns a
pytree where the two leading axes refer to the number of chains and
corresponding samples, respectively.
state_fn: Arbitrary function that maps a posterior `state` sample
(first argument) to a pytree. When `None`, directly compute rank statistics
based on `state`.
n_replicates: Number of replicates of the data. Controls the histogram's bin
variance.
n_posterior_samples: Number of posterior samples per replicate. Controls the
number of histogram bins.
Returns:
A pytree histogram of rank statistics (of size `n_posterior_samples + 1`) per
element returned by `state_fn`. Is uniformly distributed for a correctly
implemented prior and posterior sampler.
"""
if state_fn is None:
state_fn = lambda x: x # No-op
key, rng_key = random.split(key)
param, state_init = forward_fn.init(rng_key)
# Jit and peel off the `param` parameter.
_forward_fn = jax.jit(lambda s, k: forward_fn.apply(param, s, k)) # type: ignore
rank_statistics = []
for _ in range(n_replicates):
# 1) Generate a forward sample.
key, rng_key = random.split(key)
X_generative, state = _forward_fn(state_init, rng_key)
# Convert unobserved state to a real number.
f_generative = state_fn(state)
# 2) Sample from posterior using observations.
key, rng_key = random.split(key)
posterior_states = posterior_fn(rng_key, X_generative, n_posterior_samples)
# 3) Pool samples from different chains and apply `state_fn`.
posterior_states = tree_map(
lambda x: x.reshape(-1, *x.shape[2:]), posterior_states
)
f_posterior = state_fn(posterior_states)
# 4) Compute rank statistic equation in Sec. 4.1 Ref. [1].
rank_stat = tree_map(
lambda a, b: (a < b).sum(axis=0), f_posterior, f_generative
)
rank_statistics.append(rank_stat)
# 4) Build histogram of rank statistics.
# For each ranking element r(f(theta_1, ..., f(theta_L)|theta')) returned by
# `state_fn`, increment its histogram.
# (Count the ranks using a flattened representation of the `state_fn` tensor.)
tree_histogram = tree_map(
lambda leave: np.zeros([n_posterior_samples + 1, leave.size]), rank_stat
)
hist_leaves = tree_leaves(tree_histogram)
for rank_stat in rank_statistics:
rank_leaves = tree_leaves(rank_stat)
for r_leaf, h_leaf in zip(rank_leaves, hist_leaves):
# In the flat representation, the elements of the tensor are indexed as 0, 1, ...
h_leaf[r_leaf.flatten(), np.arange(r_leaf.size)] += 1
# Reshape histogram consistent with the tensor shape returned by `state_fn`.
histogram = tree_map(
lambda h, r: h.reshape((-1,) + r.shape), tree_histogram, rank_stat
)
return histogram
def plot_calibration_histogram(
histogram: NDArray, n_replicates: Optional[int] = None, alpha: float = 0.01
):
"""Analyse the calibration rank histogram for uniformity.
Args:
histogram: Rank statistics generated by simulation-based calibration.
n_replicates: Number of replicates used to compute rank histogram, used to
compute expected variation.
alpha: Plot expected variation corresponding to quantile range
`[alpha/2, 1 - alpha/2]`, i.e., a false positive rate of `alpha`.
"""
x_coords = np.arange(len(histogram))
if n_replicates is not None:
q_lower = alpha / 2
q_upper = 1 - alpha / 2
kwargs = {"n": n_replicates, "p": 1 / len(histogram)}
y_lower = binom.ppf(q=q_lower, **kwargs)
y_median = binom.ppf(q=0.5, **kwargs)
y_upper = binom.ppf(q=q_upper, **kwargs)
plt.fill_between(
x_coords,
np.full_like(x_coords, y_upper),
np.full_like(x_coords, y_lower),
alpha=0.5,
)
plt.plot(x_coords, np.full_like(x_coords, y_median), "-")
return plt.bar(x=x_coords, height=histogram, label="posterior", alpha=0.3)
Functions
def plot_calibration_histogram(histogram: numpy.ndarray[typing.Any, numpy.dtype[+ScalarType]], n_replicates: Optional[int] = None, alpha: float = 0.01)-
Analyse the calibration rank histogram for uniformity.
Args
histogram- Rank statistics generated by simulation-based calibration.
n_replicates- Number of replicates used to compute rank histogram, used to compute expected variation.
alpha- Plot expected variation corresponding to quantile range
[alpha/2, 1 - alpha/2], i.e., a false positive rate ofalpha.
Expand source code
def plot_calibration_histogram( histogram: NDArray, n_replicates: Optional[int] = None, alpha: float = 0.01 ): """Analyse the calibration rank histogram for uniformity. Args: histogram: Rank statistics generated by simulation-based calibration. n_replicates: Number of replicates used to compute rank histogram, used to compute expected variation. alpha: Plot expected variation corresponding to quantile range `[alpha/2, 1 - alpha/2]`, i.e., a false positive rate of `alpha`. """ x_coords = np.arange(len(histogram)) if n_replicates is not None: q_lower = alpha / 2 q_upper = 1 - alpha / 2 kwargs = {"n": n_replicates, "p": 1 / len(histogram)} y_lower = binom.ppf(q=q_lower, **kwargs) y_median = binom.ppf(q=0.5, **kwargs) y_upper = binom.ppf(q=q_upper, **kwargs) plt.fill_between( x_coords, np.full_like(x_coords, y_upper), np.full_like(x_coords, y_lower), alpha=0.5, ) plt.plot(x_coords, np.full_like(x_coords, y_median), "-") return plt.bar(x=x_coords, height=histogram, label="posterior", alpha=0.3) def simulation_based_calibration(forward_fn, posterior_fn: Callable[[Any, numpy.ndarray[Any, numpy.dtype[+ScalarType]], int], numpy.ndarray[Any, numpy.dtype[+ScalarType]]], key, state_fn: Optional[Callable[[numpy.ndarray[Any, numpy.dtype[+ScalarType]]], numpy.ndarray[Any, numpy.dtype[+ScalarType]]]] = None, n_replicates: int = 200, n_posterior_samples: int = 10) ‑> numpy.ndarray[typing.Any, numpy.dtype[+ScalarType]]-
Simulation based calibration to validate model implementation.
Verify posterior sampler by comparing prior with posterior distribution. Relies on the identity: p(w) = integral dy dw' p(w | y) p(y| w') p(w'),
where p(w') is the prior and p(w|y) the posterior distribution.
Args
forward_fn- Haiku function that generates an ancestral sample.
posterior_fn- Function that samples from the posterior. Takes as arguments a PRNG key, the observed data, and number of posterior samples. Returns a pytree where the two leading axes refer to the number of chains and corresponding samples, respectively.
state_fn- Arbitrary function that maps a posterior
statesample (first argument) to a pytree. WhenNone, directly compute rank statistics based onstate. n_replicates- Number of replicates of the data. Controls the histogram's bin variance.
n_posterior_samples- Number of posterior samples per replicate. Controls the number of histogram bins.
Returns
A pytree histogram of rank statistics (of size
n_posterior_samples + 1) per element returned bystate_fn. Is uniformly distributed for a correctly implemented prior and posterior sampler.Expand source code
def simulation_based_calibration( forward_fn, posterior_fn: Callable[[Any, NDArray, int], NDArray], key, state_fn: Optional[Callable[[NDArray], NDArray]] = None, n_replicates: int = 200, n_posterior_samples: int = 10, ) -> NDArray: """Simulation based calibration to validate model implementation. Verify posterior sampler by comparing prior with posterior distribution. Relies on the identity: p(w) = integral dy dw' p(w | y) p(y| w') p(w'), where p(w') is the prior and p(w|y) the posterior distribution. Args: forward_fn: Haiku function that generates an ancestral sample. posterior_fn: Function that samples from the posterior. Takes as arguments a PRNG key, the observed data, and number of posterior samples. Returns a pytree where the two leading axes refer to the number of chains and corresponding samples, respectively. state_fn: Arbitrary function that maps a posterior `state` sample (first argument) to a pytree. When `None`, directly compute rank statistics based on `state`. n_replicates: Number of replicates of the data. Controls the histogram's bin variance. n_posterior_samples: Number of posterior samples per replicate. Controls the number of histogram bins. Returns: A pytree histogram of rank statistics (of size `n_posterior_samples + 1`) per element returned by `state_fn`. Is uniformly distributed for a correctly implemented prior and posterior sampler. """ if state_fn is None: state_fn = lambda x: x # No-op key, rng_key = random.split(key) param, state_init = forward_fn.init(rng_key) # Jit and peel off the `param` parameter. _forward_fn = jax.jit(lambda s, k: forward_fn.apply(param, s, k)) # type: ignore rank_statistics = [] for _ in range(n_replicates): # 1) Generate a forward sample. key, rng_key = random.split(key) X_generative, state = _forward_fn(state_init, rng_key) # Convert unobserved state to a real number. f_generative = state_fn(state) # 2) Sample from posterior using observations. key, rng_key = random.split(key) posterior_states = posterior_fn(rng_key, X_generative, n_posterior_samples) # 3) Pool samples from different chains and apply `state_fn`. posterior_states = tree_map( lambda x: x.reshape(-1, *x.shape[2:]), posterior_states ) f_posterior = state_fn(posterior_states) # 4) Compute rank statistic equation in Sec. 4.1 Ref. [1]. rank_stat = tree_map( lambda a, b: (a < b).sum(axis=0), f_posterior, f_generative ) rank_statistics.append(rank_stat) # 4) Build histogram of rank statistics. # For each ranking element r(f(theta_1, ..., f(theta_L)|theta')) returned by # `state_fn`, increment its histogram. # (Count the ranks using a flattened representation of the `state_fn` tensor.) tree_histogram = tree_map( lambda leave: np.zeros([n_posterior_samples + 1, leave.size]), rank_stat ) hist_leaves = tree_leaves(tree_histogram) for rank_stat in rank_statistics: rank_leaves = tree_leaves(rank_stat) for r_leaf, h_leaf in zip(rank_leaves, hist_leaves): # In the flat representation, the elements of the tensor are indexed as 0, 1, ... h_leaf[r_leaf.flatten(), np.arange(r_leaf.size)] += 1 # Reshape histogram consistent with the tensor shape returned by `state_fn`. histogram = tree_map( lambda h, r: h.reshape((-1,) + r.shape), tree_histogram, rank_stat ) return histogram