Source code for bayesflow.estimators.compositional_estimator

from collections.abc import Sequence, Callable, Mapping
from typing import Literal, Tuple

import keras
import numpy as np

from bayesflow.adapters import Adapter
from bayesflow.networks import InferenceNetwork, SummaryNetwork
from bayesflow.masking import MaskName
from bayesflow.types import Tensor
from bayesflow.utils import split_arrays
from bayesflow.utils.serialization import serializable

from .basic_estimator import BasicEstimator
from .helpers import build_prior_score_fn


[docs] @serializable("bayesflow.estimators") class CompositionalEstimator(BasicEstimator): """ Defines a wrapper for estimating arbitrary compositional distributions of the form: `p(inference_variables | [summary(summary_variables), ...], inference_conditions)` Parameters ---------- inference_network : InferenceNetwork The inference network used for posterior or likelihood approximation. adapter : bayesflow.adapters.Adapter, optional Adapter for data processing. You can use :py:meth:`build_adapter` to create it. If ``None`` (default), an identity adapter is used that makes a shallow copy and passes data through unchanged. summary_network : SummaryNetwork, optional The summary network used for data summarization (default is None). standardize : str | Sequence[str] | None The variables to standardize before passing to the networks. Can be either "all" or any subset of ["inference_variables", "summary_variables", "inference_conditions"]. (default is "inference_variables"). **kwargs : dict, optional Additional arguments passed to the :py:class:`bayesflow.estimators.Estimator` class. """ def __init__( self, *, inference_network: InferenceNetwork, adapter: Adapter | None = None, summary_network: SummaryNetwork | None = None, standardize: str | Sequence[str] | None = "inference_variables", **kwargs, ): super().__init__( adapter=adapter, inference_network=inference_network, summary_network=summary_network, standardize=standardize, **kwargs, )
[docs] def compositional_sample( self, *, num_samples: int, conditions: dict[str, np.ndarray] | None = None, compute_prior_score: Callable[[dict[str, Tensor], Tensor | None], dict[str, Tensor]] | None = None, split: bool = False, batch_size: int | None = None, sample_shape: Literal["infer"] | Tuple[int] | int = "infer", return_summaries: bool = False, summary_outputs: Tensor | np.ndarray | None = None, to_numpy: bool = True, **kwargs, ) -> dict[str, np.ndarray | Tensor]: """ Generates compositional samples from the estimator given input conditions. The `conditions` dictionary is preprocessed using the `adapter`. Samples are converted to NumPy arrays after inference. Expected shape of each condition variable is (n_datasets, n_compositional, ...), where n_compositional >= 2. Parameters ---------- num_samples : int Number of samples to generate. conditions : dict[str, np.ndarray], optional Dictionary of conditioning variables as NumPy arrays. compute_prior_score : Callable[[dict[str, Tensor], Tensor | None], dict[str, Tensor]], optional A function that computes the score of the log prior distribution in the original parameter space, using backend tensors. Optionally, the function can have a time argument, otherwise the prior score is multiplied with (1-t), where t is diffusion time. If none provided, the unconditional score is used. The score is mapped into the sampling space by a change of variables over the adapter, so adapters with non-zero log determinant are supported, provided their transforms are differentiable. split : bool, default=False Whether to split the output arrays along the last axis and return one sample array per target variable. batch_size : int or None, optional If provided, the conditions are split into batches of size `batch_size`, for which samples are generated sequentially. Can help with memory management for large sample sizes. sample_shape : str or tuple of int, optional Trailing structural dimensions of each generated sample, excluding the batch and target (intrinsic) dimension. For example, use `(time,)` for time series or `(height, width)` for images. If set to `"infer"` (default), the structural dimensions are inferred from the `inference_conditions`. In that case, all non-vector dimensions except the last (channel) dimension are treated as structural dimensions. For example, if the final `inference_conditions` have shape `(batch_size, time, channels)`, then `sample_shape` is inferred as `(time,)`, and the generated samples will have shape `(num_conditions, num_samples, time, target_dim)`. return_summaries: bool, optional If set to True and a summary network is present, will return the learned summary statistics for the provided conditions. summary_outputs : Tensor | np.ndarray | None, optional Precomputed summary outputs to be used as conditions for sampling. If provided, these will be used instead of the conditions. Should have shape (n_datasets, n_compositional_conditions, ...). to_numpy: bool, optional If True, the returned samples be converted to numpy arrays (as opposed to returned as keras backend tensors). **kwargs : dict Additional keyword arguments for the sampling process. Returns ------- dict[str, np.ndarray | Tensor] Dictionary containing generated samples with the same keys as `conditions`. """ resolved_conditions, adapted, summary_outputs = self._prepare_compositional_conditions( conditions=conditions, batch_size=batch_size, summary_outputs=summary_outputs ) if compute_prior_score is None: compute_prior_score_pre = None else: compute_prior_score_pre = build_prior_score_fn( compute_prior_score, adapter=self.adapter, standardizer=self.standardizer ) kwargs = self._maybe_inject_guidance_unstandardize(kwargs) inference_kwargs = self._prepare_inference_kwargs(kwargs, adapted=adapted) inference_kwargs["compute_prior_score"] = compute_prior_score_pre # NOTE: infer option of sample cannot handle the compositional dimensions if sample_shape == "infer": sample_shape = tuple(keras.ops.shape(resolved_conditions)[2:-1]) samples = self.sampler.sample( inference_network=self.inference_network, num_samples=num_samples, conditions=resolved_conditions, batch_size=batch_size, sample_shape=sample_shape, **inference_kwargs, ) # Unstandardize and inverse-adapt samples (tree-aware for nested dict outputs) samples = keras.tree.map_structure( lambda s: self.standardizer.maybe_standardize( s, key="inference_variables", stage="inference", forward=False ), samples, ) samples = keras.tree.map_structure( lambda s: self.adapter({"inference_variables": s}, inverse=True, strict=False), samples, ) if to_numpy: samples = keras.tree.map_structure(keras.ops.convert_to_numpy, samples) if return_summaries and summary_outputs is not None: samples["_summaries"] = summary_outputs if split: samples = split_arrays(samples, axis=-1) return samples
def _prepare_compositional_conditions( self, conditions: Mapping[str, np.ndarray] | None, batch_size: int | None = None, summary_outputs: Tensor | np.ndarray | None = None, **kwargs, ) -> tuple[Tensor | None, dict[str, Tensor], Tensor | None]: if summary_outputs is not None: num_datasets, num_items = keras.ops.shape(summary_outputs)[:2] summary_outputs = keras.ops.reshape( summary_outputs, (num_datasets * num_items,) + keras.ops.shape(summary_outputs)[2:] ) flattened_conditions = None elif conditions is not None: original_shapes = {} flattened_conditions = {} target_mask_names = { MaskName.FIXED_TARGET, MaskName.INFER_TARGET, MaskName.FIXED_TARGET_VALUE, } for key, value in conditions.items(): original_shapes[key] = value.shape if key in target_mask_names: flattened_conditions[key] = value continue num_datasets, num_items = value.shape[:2] flattened_shape = (num_datasets * num_items,) + value.shape[2:] flattened_conditions[key] = value.reshape(flattened_shape) first_compositional_key = next(key for key in original_shapes if key not in target_mask_names) num_datasets, num_items = original_shapes[first_compositional_key][:2] else: raise ValueError( "At least one of 'conditions' or 'summary_outputs' must be provided for compositional sampling." ) if num_items <= 1: raise ValueError( "At least two conditioning variables are required for compositional sampling, got " f"{num_items}. Use 'sample' instead." ) resolved_conditions, adapted, summary_outputs = self._prepare_conditions( data=flattened_conditions, summary_outputs=summary_outputs, batch_size=batch_size, **kwargs, ) # Reshape tensors back to (num_datasets, num_items, ...) restored_shape = (num_datasets, num_items) + keras.ops.shape(resolved_conditions)[1:] resolved_conditions = keras.ops.reshape(resolved_conditions, restored_shape) if summary_outputs is not None: summary_outputs = keras.ops.reshape(summary_outputs, restored_shape) observed_condition_mask = adapted.get(MaskName.OBSERVED_CONDITION) if observed_condition_mask is not None: adapted = dict(adapted) adapted[MaskName.OBSERVED_CONDITION] = keras.ops.reshape( observed_condition_mask, (num_datasets, num_items) + tuple(keras.ops.shape(observed_condition_mask)[1:]), ) return resolved_conditions, adapted, summary_outputs