jcm/likelihood.py (86 lines of code) (raw):
# coding=utf-8
# Copyright 2020 The Google Research 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.
# pylint: skip-file
# pytype: skip-file
"""Various sampling methods."""
import jax
import flax
import jax.numpy as jnp
import numpy as np
from scipy import integrate
import haiku as hk
from jcm.utils import T
from .models import utils as mutils
import diffrax
def get_div_fn(fn):
"""Create the divergence function of `fn` using the Hutchinson-Skilling trace estimator."""
## Reverse-mode differentiation (slower)
# def div_fn(x, t, eps):
# grad_fn = lambda data: jnp.sum(fn(data, t) * eps)
# grad_fn_eps = jax.grad(grad_fn)(x)
# return jnp.sum(grad_fn_eps * eps, axis=tuple(range(1, len(x.shape))))
## Forward-mode differentiation (faster)
def div_fn(x, t, eps):
jvp = jax.jvp(lambda x: fn(x, t), (x,), (eps,))[1]
return jnp.sum(jvp * eps, axis=tuple(range(1, len(x.shape))))
return div_fn
def get_likelihood_fn(
sde,
model,
hutchinson_type="Rademacher",
rtol=1e-5,
atol=1e-5,
eps=1e-5,
num_repeats=1,
):
"""Create a function to compute the unbiased log-likelihood estimate of a given data point.
Args:
sde: A `sde_lib.SDE` object that represents the forward SDE.
model: A `flax.linen.Module` object that represents the architecture of the score-based model.
hutchinson_type: "Rademacher" or "Gaussian". The type of noise for Hutchinson-Skilling trace estimator.
rtol: A `float` number. The relative tolerance level of the black-box ODE solver.
atol: A `float` number. The absolute tolerance level of the black-box ODE solver.
eps: A `float` number. The probability flow ODE is integrated to `eps` for numerical stability.
num_repeats: The number of times to repeat the black-box ODE solver for reduced variance.
Returns:
A function that takes random states, replicated training states, and a batch of data points
and returns the log-likelihoods in bits/dim, the latent code, and the number of function
evaluations cost by computation.
"""
def drift_fn(state, x, t):
"""The drift function of the reverse-time SDE."""
score_fn = mutils.get_score_fn(
sde,
model,
state.params_ema,
state.model_state,
train=False,
)
# Probability flow ODE is a special case of Reverse SDE
rsde = sde.reverse(score_fn, probability_flow=True)
return rsde.sde(x, t)[0]
def likelihood_fn(rng, state, data):
"""Compute an unbiased estimate to the log-likelihood in bits/dim.
Args:
rng: An array of random states.
state: Replicated training state for running on multiple devices.
data: A JAX array of shape [batch size, ...].
Returns:
bpd: A JAX array of shape [batch size]. The log-likelihoods on `data` in bits/dim.
z: A JAX array of the same shape as `data`. The latent representation of `data` under the
probability flow ODE.
nfe: An integer. The number of function evaluations used for running the black-box ODE solver.
"""
div_fn = get_div_fn(lambda x, t: drift_fn(state, x, t))
rng = hk.PRNGSequence(rng)
shape = data.shape
if hutchinson_type == "Gaussian":
epsilon = jax.random.normal(next(rng), shape)
elif hutchinson_type == "Rademacher":
epsilon = jax.random.rademacher(next(rng), shape, dtype=data.dtype)
else:
raise NotImplementedError(f"Hutchinson type {hutchinson_type} unknown.")
## ODE function for diffrax ODE solver
def ode_func(t, x, args):
sample = x[..., :-1]
vec_t = jnp.ones((sample.shape[0],)) * t
drift = drift_fn(sample, vec_t)
logp_grad = div_fn(sample, vec_t, epsilon)
return jnp.stack([drift, logp_grad], axis=-1)
term = diffrax.ODETerm(ode_func)
solver = diffrax.Tsit5()
stepsize_controller = diffrax.PIDController(rtol=rtol, atol=atol)
solution = diffrax.diffeqsolve(
term,
solver,
t0=sde.T,
t1=eps,
dt0=eps - sde.T,
y0=jnp.stack([data, jnp.zeros_like((data.shape[0],))], axis=-1),
stepsize_controller=stepsize_controller,
)
nfe = solution.stats["num_steps"]
z = solution.ys[-1, ..., :-1]
delta_logp = solution.ys[-1, ..., -1]
prior_logp = sde.prior_logp(z)
bpd = -(prior_logp + delta_logp) / np.log(2)
N = np.prod(shape[1:])
bpd = bpd / N
offset = 7.0
bpd += offset
return bpd, z, nfe
def likelihood_fn_repeated(rng, state, data):
def loop_fn(i, carry):
bpd, nfe, rng = carry
rng, step_rng = jax.random.split(rng)
bpd_i, z_i, nfe_i = likelihood_fn(step_rng, state, data)
bpd = bpd + bpd_i
nfe = nfe + nfe_i
return bpd, nfe, rng
bpd, nfe, rng = jax.lax.fori_loop(
0, num_repeats, loop_fn, (jnp.zeros(data.shape[0]), 0, rng)
)
bpd = bpd / num_repeats
nfe = nfe / num_repeats
return bpd, nfe
return jax.pmap(likelihood_fn_repeated, axis_name="batch")