Source code for zea.models.generative
"""Generative models for zea."""
import abc
from keras import ops
from zea.models.base import BaseModel
[docs]
class GenerativeModel(abc.ABC):
"""Abstract base class for generative models."""
[docs]
def fit(self, data, **kwargs):
"""Fit the model to the data.
Args:
data: The data to fit the model to.
**kwargs: Additional arguments to pass to the fitting procedure.
"""
raise NotImplementedError("fit() must be implemented in subclasses.")
[docs]
def sample(self, n_samples=1, **kwargs):
r"""Draw samples $x \sim p(x)$ from the model.
Args:
n_samples: Number of samples to generate.
**kwargs: Additional arguments to pass to the sampling procedure.
Returns:
Samples $x$ from the model distribution $p(x)$.
"""
raise NotImplementedError("sample() must be implemented in subclasses.")
[docs]
def posterior_sample(self, measurements, n_samples=1, **kwargs):
r"""Draw samples $z \sim p(z \mid x)$ from the posterior given measurements.
Args:
measurements: The measurements $x$ to condition the posterior on, of
shape `(*input_shape)`. May also be given as `(n_samples,
*input_shape)` to condition each sample on a different
measurement. Use :func:`zea.func.vmap` to sample from a batch of
measurements.
n_samples: Number of posterior samples to generate. This is the
leading dimension of the output.
**kwargs: Additional arguments to pass to the sampling procedure.
Returns:
Samples $z$ from the posterior $p(z \mid x)$, with `n_samples` as the
leading dimension. The remaining dimensions are model-specific.
"""
raise NotImplementedError("posterior_sample() must be implemented in subclasses.")
@staticmethod
def _as_measurement_batch(measurements, n_samples, event_ndim):
"""Give `measurements` a leading axis, and say whether it was already there.
Posterior sampling conditions on a single measurement: either one shared by
every sample, or exactly one measurement per requested sample. A leading axis
of any other size is almost always a batch of measurements, which should be
mapped over with :func:`zea.func.vmap` rather than silently misread here.
Args:
measurements: A single measurement of shape `(*event_shape)`, or of
shape `(1 | n_samples, *event_shape)`.
n_samples: Number of posterior samples requested.
event_ndim: Rank of a single measurement, i.e. `len(event_shape)`.
Returns:
tuple: `(measurements, per_sample)`, where `measurements` has a leading
axis and `per_sample` is True when the caller supplied one measurement
per sample.
Raises:
ValueError: If `measurements` has neither the rank of a single
measurement nor that of a batch of them, or if a leading axis of
known size conditions neither every sample (size 1) nor one sample
each (size `n_samples`).
"""
measurements = ops.convert_to_tensor(measurements)
# Rank is static on every Keras backend, so it is safe to branch on.
ndim = ops.ndim(measurements)
if ndim == event_ndim:
return measurements[None], False
if ndim != event_ndim + 1:
raise ValueError(
f"Expected measurements of rank {event_ndim} (a single measurement) or "
f"{event_ndim + 1} (a leading axis of n_samples={n_samples}, or of 1 to "
f"share one measurement with every sample), but got shape "
f"{tuple(measurements.shape)}."
)
# Read the leading size off the static shape rather than `ops.shape`, which
# returns a dynamic tensor for unknown dimensions under `tf.function` that
# cannot be compared here. An unknown size is simply left unvalidated.
batch_size = measurements.shape[0]
if batch_size is None:
return measurements, True
# A singleton leading axis is shared by every sample, just like no axis.
if batch_size == 1:
return measurements, False
if batch_size != n_samples:
raise ValueError(
f"Expected a leading axis of n_samples={n_samples} (one measurement per "
f"sample) or of 1 (shared by every sample), but got {batch_size}. "
"Use zea.func.vmap to sample from a batch of measurements."
)
return measurements, True
[docs]
def log_density(self, data, **kwargs):
r"""Compute the log-density $\log p(x)$ of the data under the model.
Args:
data: The data $x$ to compute the log-density for.
**kwargs: Additional arguments.
Returns:
Log-density $\log p(x)$ of the data.
"""
raise NotImplementedError("log_density() must be implemented in subclasses.")
[docs]
class DeepGenerativeModel(BaseModel, GenerativeModel):
"""Base class for deep generative models.
Inherits from both GenerativeModel and BaseModel to combine
generative capabilities with Keras model functionality.
"""
def __init__(self, name="deep_generative_model", **kwargs):
"""Initialize a deep generative model.
Args:
name: Name of the model.
**kwargs: Additional arguments to pass to BaseModel.
"""
BaseModel.__init__(self, name=name, **kwargs)