Source code for sbi.neural_nets.factory

# This file is part of sbi, a toolkit for simulation-based inference. sbi is licensed
# under the Apache License Version 2.0, see <https://www.apache.org/licenses/>

from dataclasses import replace
from enum import Enum
from typing import Any, Callable, Literal, Optional, Union

from torch import Tensor, nn

from sbi.neural_nets.net_builders.estimator_configs import (
    VF_MODELS,
    _CLASSIFIER_CONFIGS,
    _DENSITY_CONFIGS,
    MarginalFlowConfig,
    _config_from_factory_kwargs,
    _factory_defaults,
    _mixed_config_from_factory_kwargs,
)
from sbi.neural_nets.net_builders.flow import build_zuko_unconditional_flow
from sbi.neural_nets.net_builders.vector_field_nets import (
    FlowMatchingConfig,
    _score_config_from_sde_type,
    _vf_config_from_factory_kwargs,
)
from sbi.utils.nn_utils import check_net_device
from sbi.utils.vector_field_utils import VectorFieldNet


# TODO: currently only used for marginal_nn, adapt to use for all
class ZukoFlowType(Enum):
    """Enumeration of Zuko flow types."""

    BPF = "bpf"
    GF = "gf"
    MAF = "maf"
    NAF = "naf"
    NCSF = "ncsf"
    NICE = "nice"
    NSF = "nsf"
    SOSPF = "sospf"
    UNAF = "unaf"


embedding_net_warn_msg = """The passed embedding net will be moved to cpu for
                        constructing the net building function."""

# Maps a config field to the name the factory exposes it under, so that the
# factory's own defaults can be read back from its signature.
_CLASSIFIER_FACTORY_FIELDS: dict = {
    "z_score_input": "z_score_theta",
    "z_score_condition": "z_score_x",
    "hidden_features": "hidden_features",
    "embedding_net_theta": "embedding_net_theta",
    "embedding_net_x": "embedding_net_x",
}

_POSTERIOR_FACTORY_FIELDS: dict = {
    "z_score_input": "z_score_theta",
    "z_score_condition": "z_score_x",
    "hidden_features": "hidden_features",
    "num_transforms": "num_transforms",
    "num_bins": "num_bins",
    "embedding_net": "embedding_net",
    "num_components": "num_components",
}

# NLE models p(x|theta), so the two z-scoring arguments swap roles.
_LIKELIHOOD_FACTORY_FIELDS: dict = {
    **_POSTERIOR_FACTORY_FIELDS,
    "z_score_input": "z_score_x",
    "z_score_condition": "z_score_theta",
}

_VF_FACTORY_FIELDS: dict = {
    "z_score_input": "z_score_theta",
    "z_score_condition": "z_score_x",
    "embedding_net": "embedding_net",
    "hidden_features": "hidden_features",
    "num_layers": "num_layers",
    "time_embedding_dim": "t_embedding_dim",
    "time_emb_type": "time_emb_type",
    "compose_standardization": "compose_standardization",
}


_Z_SCORE_FIELDS: frozenset = frozenset({"z_score_input", "z_score_condition"})


def _normalize_z_scoring(family_args: dict) -> dict:
    """Map the z-scoring arguments left at ``None`` to ``"none"``.

    The factories document ``None`` as "do not z-score", and forwarded it to
    ``z_score_parser``, which reads it that way. The configs do not accept it,
    so it is translated here rather than reaching them.
    """
    return {
        k: ("none" if v is None and k in _Z_SCORE_FIELDS else v)
        for k, v in family_args.items()
    }


def _density_family_args(embedding_net: nn.Module, **family_args: Any) -> dict:
    """Return the density factories' family-wide arguments as config fields."""
    return _normalize_z_scoring(
        dict(
            family_args,
            embedding_net=check_net_device(
                embedding_net, "cpu", embedding_net_warn_msg
            ),
        )
    )


def _unknown_density_build_fn(model: str) -> Callable:
    """Preserve the factories' build-time error for unknown model names."""

    def build_fn(batch_theta, batch_x):
        raise NotImplementedError(f"Model {model} is not implemented")

    return build_fn


[docs] def classifier_nn( model: str, z_score_theta: Optional[ Literal["independent", "structured", "none"] ] = "independent", z_score_x: Optional[Literal["independent", "structured", "none"]] = "independent", hidden_features: int = 50, embedding_net_theta: nn.Module = nn.Identity(), embedding_net_x: nn.Module = nn.Identity(), **kwargs: Any, ) -> Callable: r""" Returns a function that builds a classifier for learning density ratios. This function will usually be used for SNRE. The returned function is to be passed to the inference class when using the flexible interface. Note that in the view of the SNRE classifier we build below, x=theta and y=x. Args: model: The type of classifier that will be created. One of [`linear`, `mlp`, `resnet`]. z_score_theta: Whether to z-score parameters $\theta$ before passing them into the network, can take one of the following: - `none`, or None: do not z-score. - `independent`: z-score each dimension independently. - `structured`: treat dimensions as related, therefore compute mean and std over the entire batch, instead of per-dimension. Should be used when each sample is, for example, a time series or an image. z_score_x: Whether to z-score simulation outputs $x$ before passing them into the network, with the same options as `z_score_theta`. hidden_features: Number of hidden features. embedding_net_theta: Optional embedding network for parameters $\theta$. embedding_net_x: Optional embedding network for simulation outputs $x$. This embedding net allows to learn features from potentially high-dimensional simulation outputs. **kwargs: Additional classifier arguments. Valid keys are the fields of the chosen model's config; a key the model does not use raises, and an unknown key triggers a warning and is forwarded to the builder. """ # Map user-facing parameter names to the config's field names. family_args = _normalize_z_scoring( dict( z_score_input=z_score_theta, z_score_condition=z_score_x, hidden_features=hidden_features, embedding_net_theta=check_net_device( embedding_net_theta, "cpu", embedding_net_warn_msg ), embedding_net_x=check_net_device( embedding_net_x, "cpu", embedding_net_warn_msg ), ) ) config = _config_from_factory_kwargs( model, _CLASSIFIER_CONFIGS, "classifier", family_args=family_args, factory_defaults=_factory_defaults(classifier_nn, _CLASSIFIER_FACTORY_FIELDS), extra=kwargs, ) def build_fn(batch_theta, batch_x): return config.build(batch_input=batch_theta, batch_condition=batch_x) return build_fn
[docs] def likelihood_nn( model: str, z_score_theta: Optional[ Literal["independent", "structured", "none"] ] = "independent", z_score_x: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", hidden_features: int = 50, num_transforms: int = 5, num_bins: int = 10, embedding_net: nn.Module = nn.Identity(), num_components: int = 10, **kwargs: Any, ) -> Callable: r""" Returns a function that builds a density estimator for learning the likelihood. This function will usually be used for SNLE. The returned function is to be passed to the inference class when using the flexible interface. Args: model: The type of density estimator that will be created. One of [`mdn`, `made`, `maf`, `maf_rqs`, `nsf`]. z_score_theta: Whether to z-score parameters $\theta$ before passing them into the network, can take one of the following: - `none`, or None: do not z-score. - `independent`: z-score each dimension independently. - `structured`: treat dimensions as related, therefore compute mean and std over the entire batch, instead of per-dimension. Should be used when each sample is, for example, a time series or an image. z_score_x: Whether to z-score simulation outputs $x$ before passing them into the network, with the same options as `z_score_theta`. Supported flow configs additionally accept `transform_to_unconstrained` for this modeled variable. hidden_features: Number of hidden features. num_transforms: Number of transforms when a flow is used. Only relevant if density estimator is a normalizing flow (i.e. currently either a `maf` or a `nsf`). A non-default value raises if the chosen model does not use it. num_bins: Number of bins used for spline models. A non-default value raises if the chosen model does not use it. embedding_net: Optional embedding network for parameters $\theta$. num_components: Number of mixture components for a mixture of Gaussians. A non-default value raises if the chosen model is not an MDN. **kwargs: Additional estimator arguments. Valid keys are the fields of the chosen model's config; a key the model does not use raises, and an unknown key triggers a warning and is forwarded to the builder. """ family_args = _density_family_args( z_score_input=z_score_x, z_score_condition=z_score_theta, hidden_features=hidden_features, num_transforms=num_transforms, num_bins=num_bins, embedding_net=embedding_net, num_components=num_components, ) if model in ("mnle", "mnpe"): config = _mixed_config_from_factory_kwargs( model=model, family_args=family_args, factory_defaults=_factory_defaults( likelihood_nn, _LIKELIHOOD_FACTORY_FIELDS ), extra=kwargs, ) elif model in _DENSITY_CONFIGS: config = _config_from_factory_kwargs( model, _DENSITY_CONFIGS, "density", family_args=family_args, factory_defaults=_factory_defaults( likelihood_nn, _LIKELIHOOD_FACTORY_FIELDS ), extra=kwargs, ) else: return _unknown_density_build_fn(model) def build_fn(batch_theta, batch_x): # NLE models p(x|theta), so the modeled variable is x. return config.build(batch_input=batch_x, batch_condition=batch_theta) return build_fn
[docs] def posterior_nn( model: str, z_score_theta: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", z_score_x: Optional[Literal["independent", "structured", "none"]] = "independent", hidden_features: int = 50, num_transforms: int = 5, num_bins: int = 10, embedding_net: nn.Module = nn.Identity(), num_components: int = 10, **kwargs: Any, ) -> Callable: r""" Returns a function that builds a density estimator for learning the posterior. This function will usually be used for SNPE. The returned function is to be passed to the inference class when using the flexible interface. Args: model: The type of density estimator that will be created. One of [`mdn`, `made`, `maf`, `maf_rqs`, `nsf`]. z_score_theta: Whether to z-score parameters $\theta$ before passing them into the network, can take one of the following: - `none`, or None: do not z-score. - `independent`: z-score each dimension independently. - `structured`: treat dimensions as related, therefore compute mean and std over the entire batch, instead of per-dimension. Should be used when each sample is, for example, a time series or an image. Supported flow configs additionally accept `transform_to_unconstrained` for this modeled variable. z_score_x: Whether to z-score simulation outputs $x$ before passing them into the network: `none`, `independent`, or `structured`. hidden_features: Number of hidden features. num_transforms: Number of transforms when a flow is used. Only relevant if density estimator is a normalizing flow (i.e. currently either a `maf` or a `nsf`). A non-default value raises if the chosen model does not use it. num_bins: Number of bins used for spline models. A non-default value raises if the chosen model does not use it. embedding_net: Optional embedding network for simulation outputs $x$. This embedding net allows to learn features from potentially high-dimensional simulation outputs. num_components: Number of mixture components for a mixture of Gaussians. A non-default value raises if the chosen model is not an MDN. **kwargs: Additional estimator arguments. Valid keys are the fields of the chosen model's config; a key the model does not use raises, and an unknown key triggers a warning and is forwarded to the builder. """ family_args = _density_family_args( z_score_input=z_score_theta, z_score_condition=z_score_x, hidden_features=hidden_features, num_transforms=num_transforms, num_bins=num_bins, embedding_net=embedding_net, num_components=num_components, ) if model == "mdn_snpe_a": if num_components != 10: raise ValueError( "You set `num_components`. For NPE-A, this has to be done at " "instantiation of the inference object, i.e. " "`inference = NPE_A(..., num_components=20)`" ) # NPE-A overrides the number of components per round, so it stays a # call-time argument rather than a field of the config. base = _config_from_factory_kwargs( "mdn", _DENSITY_CONFIGS, "density", family_args={k: v for k, v in family_args.items() if k != "num_components"}, factory_defaults=_factory_defaults(posterior_nn, _POSTERIOR_FACTORY_FIELDS), extra=kwargs, ) def build_fn_snpe_a(batch_theta, batch_x, num_components): """Build function for SNPE-A. ``num_components`` stays a call-time argument so that it can later be overridden with `functools.partial`. This is necessary in order to make sure that the MDN in SNPE-A only has one component when running the Algorithm 1 part. """ return replace(base, num_components=num_components).build( batch_input=batch_theta, batch_condition=batch_x ) return build_fn_snpe_a if model in ("mnle", "mnpe"): config = _mixed_config_from_factory_kwargs( model=model, family_args=family_args, factory_defaults=_factory_defaults(posterior_nn, _POSTERIOR_FACTORY_FIELDS), extra=kwargs, ) elif model in _DENSITY_CONFIGS: config = _config_from_factory_kwargs( model, _DENSITY_CONFIGS, "density", family_args=family_args, factory_defaults=_factory_defaults(posterior_nn, _POSTERIOR_FACTORY_FIELDS), extra=kwargs, ) else: return _unknown_density_build_fn(model) def build_fn(batch_theta, batch_x): # NPE models p(theta|x), so the modeled variable is theta. return config.build(batch_input=batch_theta, batch_condition=batch_x) return build_fn
[docs] def posterior_score_nn( model: Union[ VF_MODELS, VectorFieldNet, ] = "mlp", sde_type: str = "ve", z_score_theta: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", z_score_x: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", hidden_features: int = 100, num_layers: int = 5, embedding_net: nn.Module = nn.Identity(), time_emb_type: Literal["sinusoidal", "random_fourier"] = "sinusoidal", t_embedding_dim: int = 32, compose_standardization: bool = False, **kwargs: Any, ) -> Callable: """Build util function that builds a ScoreEstimator object for score-based posteriors. Args: sde_type: SDE type used, which defines the mean and std functions. One of: - 'vp': Variance preserving. - 'subvp': Sub-variance preserving. - 've': Variance exploding. Defaults to 've'. model: Type of regression network. One of: - 'mlp': Fully connected feed-forward network. - 'ada_mlp': Fully connected feed-forward with adaptive layer normalization for conditioning. - 'transformer': Transformer network. - 'transformer_cross_attn': Transformer with cross-attention. Requires sequence-shaped conditioning (3-D ``batch_y`` or an ``embedding_net`` returning ``(batch, seq_len, emb_dim)``). - `VectorFieldNet`: Custom network implementing `forward(theta, x, t)`. Defaults to 'mlp'. z_score_theta: Whether to z-score thetas passing into the network, can be one of: - `none`, or None: do not z-score. - `independent`: z-score each dimension independently. - `structured`: treat dimensions as related, therefore compute mean and std over the entire batch, instead of per-dimension. Should be used when each sample is, for example, a time series or an image. z_score_x: Whether to z-score xs passing into the network, same options as z_score_theta. hidden_features: Number of hidden units per layer. Defaults to 100. embedding_net: Embedding network for x (conditioning variable). Defaults to nn.Identity(). time_emb_type: Type of time embedding. Defaults to 'sinusoidal'. t_embedding_dim: Embedding dimension of diffusion time. Defaults to 32. compose_standardization: Opt-in per-dim affine standardization theta<->z for scale-equivariant calibration. Defaults to False. **kwargs: Additional estimator / network arguments. Valid keys are the fields of the chosen score config and of the network's config; an unknown key warns and is forwarded to the network builder. Returns: Constructor function for NPSE. """ if compose_standardization and z_score_theta != "independent": raise ValueError( "compose_standardization=True requires z_score_theta='independent'." ) config = _vf_config_from_factory_kwargs( _score_config_from_sde_type(sde_type), model, factory_defaults=_factory_defaults(posterior_score_nn, _VF_FACTORY_FIELDS), named_net=dict( hidden_features=hidden_features, num_layers=num_layers, time_embedding_dim=t_embedding_dim, time_emb_type=time_emb_type, ), named_estimator=_normalize_z_scoring( dict( z_score_input=z_score_theta, z_score_condition=z_score_x, embedding_net=check_net_device( embedding_net, "cpu", embedding_net_warn_msg ), compose_standardization=compose_standardization, ) ), extra=kwargs, ) def build_fn(batch_theta, batch_x): return config.build(batch_input=batch_theta, batch_condition=batch_x) return build_fn
[docs] def posterior_flow_nn( model: Union[ VF_MODELS, VectorFieldNet, ] = "mlp", z_score_theta: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", z_score_x: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", hidden_features: int = 100, num_layers: int = 5, embedding_net: nn.Module = nn.Identity(), time_emb_type: Literal["sinusoidal", "random_fourier"] = "sinusoidal", t_embedding_dim: int = 32, gaussian_baseline: bool = False, compose_standardization: bool = False, **kwargs: Any, ) -> Callable: """Build util function that builds a FlowMatchingEstimator object for flow-based posteriors. Args: model: Type of regression network. One of: - 'mlp': Fully connected feed-forward network. - 'ada_mlp': Fully connected feed-forward with adaptive layer normalization for conditioning. - 'transformer': Transformer network. - 'transformer_cross_attn': Transformer with cross-attention. Requires sequence-shaped conditioning (3-D ``batch_y`` or an ``embedding_net`` returning ``(batch, seq_len, emb_dim)``). - `VectorFieldNet`: Custom network implementing `forward(theta, x, t)`. Defaults to 'mlp'. z_score_theta: Whether to z-score theta for time-dependent normalization. This enables time-dependent z-scoring which helps FMPE learn when theta is far from N(0,1). Defaults to 'independent'. z_score_x: Whether to z-score observations (x) before passing to the embedding network. Defaults to 'independent'. hidden_features: Number of hidden units per layer. Defaults to 100. num_layers: Number of hidden layers. Defaults to 5. embedding_net: Embedding network for x (conditioning variable). Defaults to nn.Identity(). time_emb_type: Type of time embedding. Defaults to 'sinusoidal'. t_embedding_dim: Embedding dimension of diffusion time. Defaults to 32. gaussian_baseline: If True, use analytical Gaussian baseline velocity derived from Bayes' rule. The network then only learns the residual. Defaults to False. compose_standardization: Opt-in per-dim affine standardization theta<->z for scale-equivariant calibration. Defaults to False. **kwargs: Additional estimator / network arguments. Valid keys are the fields of ``FlowMatchingConfig`` and of the network's config; an unknown key warns and is forwarded to the network builder. Returns: Constructor function for FMPE. """ if compose_standardization and z_score_theta != "independent": raise ValueError( "compose_standardization=True requires z_score_theta='independent'." ) config = _vf_config_from_factory_kwargs( FlowMatchingConfig(), model, factory_defaults=_factory_defaults( posterior_flow_nn, {**_VF_FACTORY_FIELDS, "gaussian_baseline": "gaussian_baseline"}, ), named_net=dict( hidden_features=hidden_features, num_layers=num_layers, time_embedding_dim=t_embedding_dim, time_emb_type=time_emb_type, ), named_estimator=_normalize_z_scoring( dict( z_score_input=z_score_theta, z_score_condition=z_score_x, embedding_net=check_net_device( embedding_net, "cpu", embedding_net_warn_msg ), gaussian_baseline=gaussian_baseline, compose_standardization=compose_standardization, ) ), extra=kwargs, ) def build_fn(batch_theta, batch_x): return config.build(batch_input=batch_theta, batch_condition=batch_x) return build_fn
[docs] def marginal_nn( model: ZukoFlowType, z_score_x: Optional[ Literal["independent", "structured", "transform_to_unconstrained", "none"] ] = "independent", hidden_features: int = 50, num_transforms: int = 5, num_bins: int = 10, num_components: int = 10, **kwargs: Any, ) -> Callable: r""" Returns a function that builds a density estimator for learning the marginal. Args: model: The type of density estimator that will be created. z_score_x: Whether to z-score samples $x$ before passing them into the network. hidden_features: Number of hidden features. num_transforms: Number of transforms when a flow is used. num_bins: Number of bins used for the splines in `nsf`. num_components: Number of mixture components for a mixture of Gaussians. **kwargs: Additional estimator arguments. Valid keys are defined by ``MarginalFlowConfig``; unknown keys trigger a warning and are forwarded to the builder. """ # Map user-facing parameter names to internal names (no renaming needed here). mapped = dict( z_score_x=z_score_x, hidden_features=hidden_features, num_transforms=num_transforms, num_bins=num_bins, num_components=num_components, ) # Validate against known fields — warns on unknown kwargs (typos) # while still forwarding them to the underlying builder. config = MarginalFlowConfig.from_kwargs(**mapped, **kwargs) builder_kwargs = config.to_dict() def build_fn(batch_x: Tensor) -> Any: return build_zuko_unconditional_flow( which_nf=model.value.upper(), batch_x=batch_x, **builder_kwargs ) return build_fn