Skip to content

Conjugate marginals

The closed-form marginals of the standard conjugate pairs.

conjugate

Closed-form marginals of the standard conjugate pairs.

A conjugate pair is a parent site whose value is the parameter of a child site: z ~ Beta(a, b); y ~ Bernoulli(z) and its kin. The child's marginal, with the parent integrated out, has a closed form for the four pairs documented in Bernardo and Smith (1994): Normal-Normal, Beta-Bernoulli, Gamma-Poisson, and Dirichlet-Categorical. The solvers here compute those marginals from the parent's distribution parameters and the child's value, and check that the child is the identity-link form the marginal is derived for.

CONJUGATE_SOLVERS module-attribute

CONJUGATE_SOLVERS: Mapping[tuple[str, str], Solver] = {('Normal', 'Normal'): normal_normal, ('Beta', 'Bernoulli'): beta_bernoulli, ('Gamma', 'Poisson'): gamma_poisson, ('Dirichlet', 'Categorical'): dirichlet_categorical}

The closed-form solvers, keyed by (prior family, likelihood family).

Solver

Solver = Callable[[Mapping[str, Tensor], Tensor, TorchSampleable, Tensor], Tensor]

A closed-form marginal: given the parent's parameters, the parent's drawn value, the child's sampleable at that value, and the child's value, the log marginal density of the child's value.

family_of

family_of(sampleable: TorchSampleable) -> str

The registry family a sampleable's morphism draws from.

PARAMETER DESCRIPTION
sampleable

The sampleable.

TYPE: TorchSampleable

RETURNS DESCRIPTION
str

The family's registry name.

RAISES DESCRIPTION
TypeError

If the morphism's class is registered for no family.

Source code in src/quivers/effects/conjugate.py
def family_of(sampleable: TorchSampleable) -> str:
    """The registry family a sampleable's morphism draws from.

    Parameters
    ----------
    sampleable : TorchSampleable
        The sampleable.

    Returns
    -------
    str
        The family's registry name.

    Raises
    ------
    TypeError
        If the morphism's class is registered for no family.
    """
    inner = sampleable.declared
    assert inner is not None
    for name, meta in FAMILY_META.items():
        if meta.quivers_class is not None and isinstance(inner, meta.quivers_class):
            return name
    get_dist = getattr(inner, "_get_dist", None)
    if callable(get_dist):
        distribution = get_dist(sampleable.input)
        for name, meta in FAMILY_META.items():
            if type(distribution) is meta.distribution_class:
                return name
    raise TypeError(
        f"{type(inner).__name__} draws from no registered family; a conjugate "
        "marginal needs the parent's and the child's families"
    )

normal_normal

normal_normal(parent: Mapping[str, Tensor], parent_value: Tensor, child: TorchSampleable, value: Tensor) -> Tensor

The marginal of y ~ Normal(z, tau) under z ~ Normal(mu, sigma).

PARAMETER DESCRIPTION
parent

The parent's loc and scale.

TYPE: Mapping[str, Tensor]

parent_value

The parent's drawn value.

TYPE: Tensor

child

The child at the parent's drawn value, whose scale is tau.

TYPE: TorchSampleable

value

The child's value.

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

log Normal(y; mu, sqrt(sigma^2 + tau^2)) summed over the event.

RAISES DESCRIPTION
ValueError

If the child's location is not the parent's value.

Source code in src/quivers/effects/conjugate.py
def normal_normal(
    parent: Mapping[str, torch.Tensor],
    parent_value: torch.Tensor,
    child: TorchSampleable,
    value: torch.Tensor,
) -> torch.Tensor:
    """The marginal of ``y ~ Normal(z, tau)`` under ``z ~ Normal(mu, sigma)``.

    Parameters
    ----------
    parent : Mapping[str, torch.Tensor]
        The parent's ``loc`` and ``scale``.
    parent_value : torch.Tensor
        The parent's drawn value.
    child : TorchSampleable
        The child at the parent's drawn value, whose ``scale`` is ``tau``.
    value : torch.Tensor
        The child's value.

    Returns
    -------
    torch.Tensor
        ``log Normal(y; mu, sqrt(sigma^2 + tau^2))`` summed over the event.

    Raises
    ------
    ValueError
        If the child's location is not the parent's value.
    """
    _identity_link(child, "loc", parent_value)
    tau = child.parameters()["scale"].reshape(parent["scale"].shape)
    scale = torch.sqrt(parent["scale"] ** 2 + tau**2)
    residual = (value - parent["loc"]) / scale
    log_density = -0.5 * residual**2 - scale.log() - 0.5 * math.log(2 * math.pi)
    return log_density.sum(dim=-1) if log_density.dim() > 1 else log_density

beta_bernoulli

beta_bernoulli(parent: Mapping[str, Tensor], parent_value: Tensor, child: TorchSampleable, value: Tensor) -> Tensor

The marginal of y ~ Bernoulli(z) under z ~ Beta(a, b).

PARAMETER DESCRIPTION
parent

The parent's concentration1 and concentration0.

TYPE: Mapping[str, Tensor]

parent_value

The parent's drawn value.

TYPE: Tensor

child

The child at the parent's drawn value.

TYPE: TorchSampleable

value

The child's value, zero or one.

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

log(a / (a + b)) for one and log(b / (a + b)) for zero.

RAISES DESCRIPTION
ValueError

If the child's probability is not the parent's value.

Source code in src/quivers/effects/conjugate.py
def beta_bernoulli(
    parent: Mapping[str, torch.Tensor],
    parent_value: torch.Tensor,
    child: TorchSampleable,
    value: torch.Tensor,
) -> torch.Tensor:
    """The marginal of ``y ~ Bernoulli(z)`` under ``z ~ Beta(a, b)``.

    Parameters
    ----------
    parent : Mapping[str, torch.Tensor]
        The parent's ``concentration1`` and ``concentration0``.
    parent_value : torch.Tensor
        The parent's drawn value.
    child : TorchSampleable
        The child at the parent's drawn value.
    value : torch.Tensor
        The child's value, zero or one.

    Returns
    -------
    torch.Tensor
        ``log(a / (a + b))`` for one and ``log(b / (a + b))`` for zero.

    Raises
    ------
    ValueError
        If the child's probability is not the parent's value.
    """
    _identity_link(child, "probs", parent_value)
    a = parent["concentration1"]
    b = parent["concentration0"]
    probability = a / (a + b)
    log_density = torch.where(
        value.to(probability.dtype) > 0.5, probability.log(), (1 - probability).log()
    )
    return log_density.sum(dim=-1) if log_density.dim() > 1 else log_density

gamma_poisson

gamma_poisson(parent: Mapping[str, Tensor], parent_value: Tensor, child: TorchSampleable, value: Tensor) -> Tensor

The marginal of y ~ Poisson(z) under z ~ Gamma(alpha, beta).

PARAMETER DESCRIPTION
parent

The parent's concentration and rate.

TYPE: Mapping[str, Tensor]

parent_value

The parent's drawn value.

TYPE: Tensor

child

The child at the parent's drawn value.

TYPE: TorchSampleable

value

The child's count.

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

The negative binomial log mass with alpha successes and success probability beta / (beta + 1).

RAISES DESCRIPTION
ValueError

If the child's rate is not the parent's value.

Source code in src/quivers/effects/conjugate.py
def gamma_poisson(
    parent: Mapping[str, torch.Tensor],
    parent_value: torch.Tensor,
    child: TorchSampleable,
    value: torch.Tensor,
) -> torch.Tensor:
    """The marginal of ``y ~ Poisson(z)`` under ``z ~ Gamma(alpha, beta)``.

    Parameters
    ----------
    parent : Mapping[str, torch.Tensor]
        The parent's ``concentration`` and ``rate``.
    parent_value : torch.Tensor
        The parent's drawn value.
    child : TorchSampleable
        The child at the parent's drawn value.
    value : torch.Tensor
        The child's count.

    Returns
    -------
    torch.Tensor
        The negative binomial log mass with ``alpha`` successes and
        success probability ``beta / (beta + 1)``.

    Raises
    ------
    ValueError
        If the child's rate is not the parent's value.
    """
    _identity_link(child, "rate", parent_value)
    alpha = parent["concentration"]
    beta = parent["rate"]
    count = value.to(alpha.dtype)
    log_density = (
        torch.lgamma(alpha + count)
        - torch.lgamma(alpha)
        - torch.lgamma(count + 1)
        + alpha * (beta.log() - torch.log1p(beta))
        - count * torch.log1p(beta)
    )
    return log_density.sum(dim=-1) if log_density.dim() > 1 else log_density

dirichlet_categorical

dirichlet_categorical(parent: Mapping[str, Tensor], parent_value: Tensor, child: TorchSampleable, value: Tensor) -> Tensor

The marginal of y ~ Categorical(z) under z ~ Dirichlet(alpha).

PARAMETER DESCRIPTION
parent

The parent's concentration.

TYPE: Mapping[str, Tensor]

parent_value

The parent's drawn value.

TYPE: Tensor

child

The child at the parent's drawn value.

TYPE: TorchSampleable

value

The child's category.

TYPE: Tensor

RETURNS DESCRIPTION
Tensor

log(alpha_y / sum(alpha)).

RAISES DESCRIPTION
ValueError

If the child's probabilities are not the parent's value.

Source code in src/quivers/effects/conjugate.py
def dirichlet_categorical(
    parent: Mapping[str, torch.Tensor],
    parent_value: torch.Tensor,
    child: TorchSampleable,
    value: torch.Tensor,
) -> torch.Tensor:
    """The marginal of ``y ~ Categorical(z)`` under ``z ~ Dirichlet(alpha)``.

    Parameters
    ----------
    parent : Mapping[str, torch.Tensor]
        The parent's ``concentration``.
    parent_value : torch.Tensor
        The parent's drawn value.
    child : TorchSampleable
        The child at the parent's drawn value.
    value : torch.Tensor
        The child's category.

    Returns
    -------
    torch.Tensor
        ``log(alpha_y / sum(alpha))``.

    Raises
    ------
    ValueError
        If the child's probabilities are not the parent's value.
    """
    _identity_link(child, "probs", parent_value)
    alpha = parent["concentration"]
    probabilities = alpha / alpha.sum(dim=-1, keepdim=True)
    index = value.long().unsqueeze(-1)
    return probabilities.gather(-1, index).squeeze(-1).log()

conjugate_solver

conjugate_solver(parent_family: str, child_family: str) -> Solver

The solver of a conjugate pair.

PARAMETER DESCRIPTION
parent_family

The parent's family.

TYPE: str

child_family

The child's family.

TYPE: str

RETURNS DESCRIPTION
Solver

The closed-form marginal.

RAISES DESCRIPTION
KeyError

If the pair has no registered marginal.

Source code in src/quivers/effects/conjugate.py
def conjugate_solver(parent_family: str, child_family: str) -> Solver:
    """The solver of a conjugate pair.

    Parameters
    ----------
    parent_family : str
        The parent's family.
    child_family : str
        The child's family.

    Returns
    -------
    Solver
        The closed-form marginal.

    Raises
    ------
    KeyError
        If the pair has no registered marginal.
    """
    try:
        return CONJUGATE_SOLVERS[(parent_family, child_family)]
    except KeyError as error:
        registered = ", ".join(f"{a}-{b}" for a, b in CONJUGATE_SOLVERS)
        raise KeyError(
            f"no conjugate marginal is registered for ({parent_family}, "
            f"{child_family}); the registered pairs are {registered}"
        ) from error