Source code for bayesflow.experimental.latent_inference_network.latent_inference_network

from functools import lru_cache

import keras

from bayesflow._backend import jacrev
from bayesflow.networks.inference import InferenceNetwork
from bayesflow.types import Tensor
from bayesflow.utils import filter_kwargs, issue_url
from bayesflow.utils.serialization import serializable, serialize
from ..autoencoder import AutoEncoder


[docs] @serializable("bayesflow.experimental") class LatentInferenceNetwork(InferenceNetwork): """Inference network wrapper that operates on a compressed latent representation. This class wraps an autoencoder as a feature extractor, projecting input data into a lower-dimensional latent space before passing it to the internal inference network. This approach can significantly reduce computational costs, especially for high-dimensional or image data where redundancy is present. The latent inference paradigm was originally introduced in [1] and is also known as "Stable Diffusion". To closely match this architecture, configure the network as follows: >>> from bayesflow.experimental import VariationalAutoEncoder >>> from bayesflow.networks import DiffusionModel >>> inference_network = LatentInferenceNetwork(VariationalAutoEncoder(...), DiffusionModel(...)) Parameters ---------- autoencoder : AutoEncoder The autoencoder serving as the feature extractor. inference_network : InferenceNetwork The inference network to be applied to the latent representation. References ---------- [1]: Rombach et al. (2021). High-Resolution Image Synthesis with Latent Diffusion Models. arXiv:2112.10752. """ def __init__( self, autoencoder: AutoEncoder, inference_network: InferenceNetwork, **kwargs, ): super().__init__(**kwargs) if isinstance(inference_network, LatentInferenceNetwork): raise TypeError("Cannot recurse latent inference networks.") self.autoencoder = autoencoder self.inference_network = inference_network
[docs] def build(self, xz_shape, conditions_shape=None): if self.built: return shape = xz_shape self.autoencoder.build(shape) shape = self.autoencoder.compute_output_shape(shape) self.inference_network.build(shape, conditions_shape) self.base_distribution.build(shape)
[docs] def get_config(self): base_config = super().get_config() config = { "autoencoder": self.autoencoder, "inference_network": self.inference_network, } return base_config | serialize(config)
def _forward( self, x: Tensor, conditions: Tensor | None = None, density: bool = False, training: bool = False, **kwargs ): if not density: y = self.autoencoder(x, training=training, **filter_kwargs(kwargs, self.autoencoder.call)) z = self.inference_network( y, conditions=conditions, training=training, **filter_kwargs(kwargs, self.inference_network.call) ) return z @lru_cache(1) def fn(_x): return self.autoencoder( _x, inverse=False, training=training, **filter_kwargs(kwargs, self.autoencoder.call) ) # the general case is that the encoder projects down, so we use jacrev to save on compute as compared to jacfwd jac_fn = jacrev(fn, argnums=0, has_aux=False) y = fn(x) jac = jac_fn(x) z, log_density = self.inference_network( y, inverse=False, density=True, training=training, **filter_kwargs(kwargs, self.inference_network.call) ) # modified change of variables; p(x) = p(f(x)) / sqrt(|J^T J|) log_density = log_density - 0.5 * keras.ops.logdet(keras.ops.matmul(keras.ops.transpose(jac), jac)) return z, log_density def _inverse( self, z: Tensor, conditions: Tensor | None = None, density: bool = False, training: bool = False, **kwargs ): if not self.built: raise RuntimeError("Must call build before calling inverse.") if density: raise NotImplementedError( f"Inverse mode density estimation is not supported; use forward-mode estimation instead. " f"If you need this feature, please open an issue at {issue_url}." ) y = self.inference_network( z, conditions=conditions, inverse=True, training=training, **filter_kwargs(kwargs, self.inference_network.call), ) x = self.autoencoder(y, training=training, inverse=True, **filter_kwargs(kwargs, self.autoencoder.call)) return x
[docs] def compute_metrics( self, x: Tensor, conditions: Tensor = None, sample_weight: Tensor = None, stage: str = "training", **kwargs ) -> dict[str, Tensor]: autoencoder_metrics = self.autoencoder.compute_metrics(x, sample_weight=sample_weight, stage=stage, **kwargs) try: y = autoencoder_metrics.pop("z") except KeyError: raise RuntimeError( f"The autoencoder for {self.__class__.__name__} must return a cached latent vector from its " f"`compute_metrics` method under the key 'z'." ) if not keras.ops.is_tensor(y): raise TypeError( f"Expected the autoencoder's cached latent vector (key 'z') to be a Tensor, got {type(y)!r} instead." ) # decouple the autoencoder and inference network to avoid representation drift issues y = keras.ops.stop_gradient(y) inference_metrics = self.inference_network.compute_metrics( y, conditions=conditions, sample_weight=sample_weight, stage=stage, **kwargs ) metrics = {"loss": inference_metrics["loss"] + autoencoder_metrics["loss"]} autoencoder_metrics = { f"{self.autoencoder.__class__.__name__}/{key}": value for key, value in autoencoder_metrics.items() } inference_metrics = { f"{self.inference_network.__class__.__name__}/{key}": value for key, value in inference_metrics.items() } metrics = metrics | autoencoder_metrics | inference_metrics return metrics