Source code for bayesflow.utils.jacobian.jacobian_trace

import warnings
from collections.abc import Callable

import keras

from bayesflow.types import Tensor
from .jacobian import jacobian
from .vjp import vjp


[docs] def jacobian_trace( f: Callable[[Tensor], Tensor], x: Tensor, max_steps: int = None, return_output: bool = False, seed: int | keras.random.SeedGenerator = None, projectors: Tensor = None, ): """Compute or estimate the trace of the Jacobian matrix of f. Parameters ---------- f : Callable The function to be differentiated. x : Tensor of shape (n, ..., d) The input tensor to f. max_steps : int, optional The maximum number of steps to use for the estimate. If this does not exceed the dimensionality of f(x), use Hutchinson's algorithm to return an unbiased estimate of the Jacobian trace. Otherwise, perform an exact computation. Default: None return_output : bool, optional Whether to return the output of f(x) along with the trace of the Jacobian. Default: False seed : int or keras SeedGenerator, optional The seed to use for Hutchinson trace estimation. Only has an effect when max_steps < d. projectors : Tensor of shape (steps, n, ..., d), optional Fixed probe vectors for Hutchinson's algorithm. Pass the same projectors to every function evaluation of an ODE solve to keep the augmented dynamics deterministic. Returns ------- 2-tuple of tensors: 1. The output of f(x) (if return_output is True) 2. Tensor of shape (n,) An unbiased estimate or the exact trace of the Jacobian of f. """ warnings.warn( "jacobian_trace is deprecated; we are working on moving these utilities upstream or into their own " "module with improved signatures.", DeprecationWarning, stacklevel=2, ) dims = keras.ops.shape(x)[-1] if projectors is None and (max_steps is None or dims <= max_steps): fx, jac = jacobian(f, x, return_output=True) trace = keras.ops.trace(jac, axis1=-2, axis2=-1) else: fx, trace = _hutchinson(f, x, steps=max_steps, return_output=True, projectors=projectors, seed=seed) if return_output: return fx, trace return trace
def hutchinson_projectors(x: Tensor, steps: int | None, seed: int | keras.random.SeedGenerator = None) -> Tensor | None: """Sample Rademacher probe vectors of shape (steps, *x.shape) for Hutchinson's algorithm. Returns None if steps is None or not smaller than the dimensionality of x, in which case the exact trace is cheaper and has no variance. """ shape = tuple(keras.ops.shape(x)) if steps is None or shape[-1] <= steps: return None # Rademacher probes have lower variance than Gaussian probes signs = keras.random.randint((steps,) + shape, 0, 2, seed=seed) return keras.ops.cast(2 * signs - 1, keras.ops.dtype(x)) def _hutchinson( f: Callable, x: Tensor, steps: int = 1, return_output: bool = False, seed: int | keras.random.SeedGenerator = None, projectors: Tensor = None, ): """Estimate the trace of the Jacobian matrix of f using Hutchinson's algorithm. :param f: The function to be differentiated. :param x: Tensor of shape (n,..., d) The input tensor to f. :param steps: The number of steps to use for the estimate. Higher values yield better precision. Default: 1 :return: 2-tuple of tensors: 1. The output of f(x) 2. Tensor of shape (n,) An unbiased estimate of the trace of the Jacobian matrix of f. """ shape = keras.ops.shape(x) trace = keras.ops.zeros(shape[:-1]) fx, vjp_fn = vjp(f, x, return_output=True) if projectors is None: projectors = hutchinson_projectors(x, steps, seed=seed) steps = keras.ops.shape(projectors)[0] for step in range(steps): projector = projectors[step] trace += keras.ops.sum(vjp_fn(projector) * projector, axis=-1) trace = trace / steps if return_output: return fx, trace return trace