Source code for bayesflow.networks.inference.inference_network

from collections.abc import Callable, Mapping
from typing import Any

import keras

from bayesflow.masking import MaskHandler
from bayesflow.types import Shape, Tensor
from bayesflow.utils import layer_kwargs, find_distribution
from bayesflow.utils.decorators import allow_batch_size
from bayesflow.utils.keras_utils import resolve_seed
from bayesflow.utils.serialization import deserialize, serialize


[docs] class InferenceNetwork(keras.Layer): """Abstract base class for all inference networks in BayesFlow. An inference network learns a mapping between a data space and a latent space, optionally conditioned on external variables. Concrete subclasses power the different approximation strategies (normalizing flows, diffusion models, flow matching, consistency models, …). Subclassing guide ----------------- To implement a custom inference network, inherit from this class and override **at minimum** the following methods: ``_forward(x, conditions, density, training, **kwargs)`` Map data *x* -> latent *z*. When *density* is ``True`` the method must return a tuple ``(z, log_prob)``; otherwise just *z*. ``_inverse(z, conditions, density, training, **kwargs)`` Map latent *z* -> data *x*. Same density convention as ``_forward``. ``compute_metrics(x, conditions, sample_weight, stage)`` Compute and return a ``dict[str, Tensor]`` of training metrics. The dict **must** contain at least a ``"loss"`` key. This is where you implement the training objective for your custom inference network. Every inference network owns a ``self.seed_generator``, and :meth:`sample` resolves the user-provided ``seed`` against it via :func:`~bayesflow.utils.keras_utils.resolve_seed` before passing the result on to ``base_distribution`` and ``_inverse``. Subclasses that draw randomness of their own (e.g. stochastic solvers or training-time noise) must likewise resolve an omitted ``seed`` against ``self.seed_generator``, so that they never fall back to Keras' global generator (which cannot be traced under ``jax.jit``). Every inference network also owns a :class:`~bayesflow.masking.MaskHandler`. Implementations resolve masks once at the boundary of each top-level operation and reuse the returned immutable :class:`~bayesflow.masking.InferenceMasks` throughout solver steps or derivative closures. Ordinary padding and attention masks are routed separately to subnets which declare them explicitly. Custom implementations should follow the same boundary so mask sampling is neither repeated nor inserted into the backpropagation loop. Optionally override: ``sample(batch_shape, conditions, **kwargs)`` Draw samples from the learned distribution. The default implementation samples from ``base_distribution`` and passes the result through ``_inverse``. ``log_prob(samples, conditions, **kwargs)`` Evaluate the log-density of *samples* under the learned distribution. The default implementation calls ``_forward`` with ``density=True``. Parameters ---------- base_distribution : str or ~bayesflow.distributions.Distribution, optional The base (latent) distribution. Accepts the string shortcuts ``"normal"`` (:class:`~bayesflow.distributions.DiagonalNormal`) and ``"student_t"`` (:class:`~bayesflow.distributions.DiagonalStudentT`), or an explicit :class:`~bayesflow.distributions.Distribution` instance for full control. Default is ``"normal"``. mask_handler : ~bayesflow.masking.MaskHandler, optional Handler used to generate and route masks. If ``None``, a default :class:`~bayesflow.masking.MaskHandler` is created with the network's seed. A supplied handler retains its own random generator. seed : int, optional Initial seed for the network's local random generator. **kwargs Forwarded to ``keras.Layer`` after filtering with :func:`~bayesflow.utils.layer_kwargs`. """ def __init__( self, base_distribution: str | keras.Layer = "normal", *, mask_handler: MaskHandler | None = None, seed: int | None = None, **kwargs, ): super().__init__(**layer_kwargs(kwargs)) self.base_distribution = find_distribution(base_distribution) self.seed = seed self.seed_generator = keras.random.SeedGenerator(seed) if mask_handler is None: mask_handler = MaskHandler(seed=self.seed) self.mask_handler = mask_handler self._mask_seed_generator = mask_handler.seed_generator
[docs] def get_config(self): return super().get_config() | { "mask_handler": serialize(self.mask_handler), "seed": self.seed, }
[docs] def call( self, xz: Tensor, conditions: Tensor = None, inverse: bool = False, density: bool = False, training: bool = False, seed: int | keras.random.SeedGenerator | None = None, hutchinson_steps: int | None = None, guidance_kwargs: Mapping[str, Any] | None = None, **kwargs, ) -> Tensor | tuple[Tensor, Tensor]: if hutchinson_steps is not None: kwargs["hutchinson_steps"] = hutchinson_steps if guidance_kwargs is not None: kwargs["guidance_kwargs"] = guidance_kwargs if inverse: return self._inverse(xz, conditions=conditions, density=density, training=training, seed=seed, **kwargs) return self._forward(xz, conditions=conditions, density=density, training=training, seed=seed, **kwargs)
def _forward( self, x: Tensor, conditions: Tensor = None, density: bool = False, training: bool = False, **kwargs ) -> Tensor | tuple[Tensor, Tensor]: raise NotImplementedError def _inverse( self, z: Tensor, conditions: Tensor = None, density: bool = False, training: bool = False, **kwargs ) -> Tensor | tuple[Tensor, Tensor]: raise NotImplementedError def _build_subnet_and_projector( self, xz_shape: Shape, conditions_shape: Shape = None, units: int = None, name: str = "output_projector", has_time: bool = True, ) -> keras.Layer: """Build ``self.subnet`` and return a built projector to ``units`` outputs. The projector is the identity if the subnet already outputs ``units`` features (default: the target dimension), and a zero-bias dense layer named ``name`` otherwise. The subnet input is ``(target, time, conditions)`` when ``has_time`` is true and ``(target, conditions)`` otherwise. """ units = xz_shape[-1] if units is None else units if has_time: input_shape = (xz_shape, (xz_shape[0], 1), conditions_shape) else: input_shape = (xz_shape, conditions_shape) self.subnet.build(input_shape) return self._build_projector(self.subnet.compute_output_shape(input_shape), units, name) @staticmethod def _build_projector(out_shape: Shape, units: int, name: str = "output_projector") -> keras.Layer: """Return a built projector to ``units`` outputs: the identity if ``out_shape`` already has ``units`` features, and a zero-bias dense layer named ``name`` otherwise.""" if out_shape[-1] != units: projector = keras.layers.Dense(units=units, bias_initializer="zeros", name=name) else: projector = keras.layers.Identity() projector.build(out_shape) return projector def _inverse_compositional( self, z: Tensor, conditions: Tensor, compute_prior_score: Callable = None, density: bool = False, training: bool = False, seed: int | keras.random.SeedGenerator | None = None, **kwargs, ) -> Tensor | tuple[Tensor, Tensor]: raise NotImplementedError
[docs] @allow_batch_size def sample( self, batch_shape: Shape, conditions: Tensor = None, seed: int | keras.random.SeedGenerator | None = None, **kwargs, ) -> Tensor: # resolved here once instead of in every subclass seed = resolve_seed(seed, self.seed_generator) samples = self.base_distribution.sample(batch_shape, seed=seed) if "compute_prior_score" in kwargs: samples = self._inverse_compositional( samples, conditions=conditions, inverse=True, density=False, seed=seed, **kwargs ) else: samples = self(samples, conditions=conditions, inverse=True, density=False, seed=seed, **kwargs) return samples
[docs] def log_prob(self, samples: Tensor, conditions: Tensor = None, **kwargs) -> Tensor: _, log_density = self(samples, conditions=conditions, inverse=False, density=True, **kwargs) return log_density
[docs] def compute_metrics( self, x: Tensor, conditions: Tensor = None, sample_weight: Tensor = None, stage: str = "training", **kwargs ) -> dict[str, Tensor]: raise NotImplementedError
[docs] @classmethod def from_config(cls, config, custom_objects=None): return cls(**deserialize(config, custom_objects=custom_objects))