Source code for leaspy.utils.distributions

"""Module defining useful distribution for ordinal noise model."""

from typing import ClassVar, Optional, Union

import numpy as np
import torch
from torch.distributions.constraints import unit_interval

__all__ = [
    "discrete_sf_from_pdf",
    "compute_ordinal_pdf_from_ordinal_sf",
    "MultinomialDistribution",
    "MultinomialCdfDistribution",
]


[docs] def discrete_sf_from_pdf(pdf: Union[torch.Tensor, np.ndarray]) -> torch.Tensor: """ Compute the discrete survival function values from a discrete probability density. For a discrete variable with levels ``l=0..L`` the survival function is :math:`P(X>l)=P(X\\ge l+1)` for ``l=0..L-1``. This function assumes the last dimension of ``pdf`` indexes the discrete levels. Parameters ---------- pdf : :class:`torch.Tensor` or :class:`np.ndarray` The discrete probability density. Returns ------- :class:`np.ndarray` : The discrete survival function values. """ return (1 - pdf.cumsum(-1))[..., :-1]
[docs] def compute_ordinal_pdf_from_ordinal_sf( ordinal_sf: torch.Tensor, dim_ordinal_levels: int = 3, ) -> torch.Tensor: """ Compute the probability density of an ordinal model from its survival function. Given the survival function probabilities :math:`P(X>l)=P(X\\ge l+1)` for ``l=0..L-1``, compute :math:`P(X=l)` for ``l=0..L``. Parameters ---------- ordinal_sf : :class:`torch.FloatTensor` Survival function values : ordinal_sf[..., l] is the proba to be superior or equal to l+1 Dimensions are: * 0=individual * 1=visit * 2=feature * 3=ordinal_level [l=0..L-1] * [4=individual_parameter_dim_when_gradient] dim_ordinal_levels : int, default = 3 The dimension of the tensor where the ordinal levels are. Returns ------- ordinal_pdf : :class:`torch.FloatTensor` (same shape as input, except for dimension 3 which has one more element) ordinal_pdf[..., l] is the proba to be equal to l (l=0..L) """ # nota: torch.diff was introduced in v1.8 but would not highly improve performance of this routine anyway s = list(ordinal_sf.shape) s[dim_ordinal_levels] = 1 last_row = torch.zeros(size=tuple(s)) if len(s) == 5: # in the case of gradient we added a dimension first_row = last_row # gradient(P>=0) = 0 else: first_row = torch.ones(size=tuple(s)) # (P>=0) = 1 sf_sup = torch.cat([first_row, ordinal_sf], dim=dim_ordinal_levels) sf_inf = torch.cat([ordinal_sf, last_row], dim=dim_ordinal_levels) pdf = sf_sup - sf_inf return pdf
[docs] class MultinomialDistribution(torch.distributions.Multinomial): """ Class for a multinomial distribution with only one sample based on the Multinomial torch distrib. Parameters ---------- probs : :class:`torch.Tensor` The pdf of the multinomial distribution. Attributes ---------- """ arg_constraints: ClassVar = {} def __init__(self, probs, **kwargs): super().__init__(total_count=1, probs=probs, **kwargs)
[docs] @classmethod def from_sf(cls, sf: torch.Tensor, **kws): """ Generate a new MultinomialDistribution from its survival function instead of its probability density function. Parameters ---------- pdf : :class:`torch.Tensor` The input probability density function. **kws Additional keyword arguments to be passed for instance initialization. """ return cls( compute_ordinal_pdf_from_ordinal_sf(sf, dim_ordinal_levels=-1), **kws )
[docs] class MultinomialCdfDistribution(torch.distributions.Distribution): """ Class for a multinomial distribution with only sample method. Parameters ---------- sf : :class:`torch.Tensor` Values of the survival function [P(X > l) for l=0..L-1 where L is max_level] from which the distribution samples. Ordinal levels are assumed to be in the last dimension. Those values must be in [0, 1], and decreasing when ordinal level increases (not checked). validate_args : bool, optional (default True) Whether to validate the arguments or not (None for default behavior, which changed after torch >= 1.8 to True). Attributes ---------- cdf : :class:`torch.Tensor` The cumulative distribution function [P(X <= l) for l=0..L] from which the distribution samples. The shape of latest dimension is L+1 where L is max_level. We always have P(X <= L) = 1 arg_constraints : :obj:`dict` Contains the constraints on the arguments. """ arg_constraints: ClassVar = {} def __init__(self, sf: torch.Tensor, *, validate_args: Optional[bool] = True): super().__init__(validate_args=validate_args) if self._validate_args: assert unit_interval.check( sf ).all(), "Bad probabilities in MultinomialDistribution" # shape of the sample (we discard the last dimension, used to store the different ordinal levels) self._sample_shape = sf.shape[:-1] # store the cumulative distribution function with trailing P(X <= L) = 1 self.cdf = torch.cat((1.0 - sf, torch.ones((*self._sample_shape, 1))), dim=-1)
[docs] @classmethod def from_pdf(cls, pdf: torch.Tensor, **kws): """ Generate a new MultinomialDistribution from its probability density function instead of its survival function. Parameters ---------- pdf : :class:`torch.Tensor` The input probability density function. **kws Additional keyword arguments to be passed for instance initialization. """ return cls(discrete_sf_from_pdf(pdf), **kws)
[docs] def sample(self) -> torch.Tensor: """ Multinomial sampling. We sample uniformly on [0, 1( but for the latest dimension corresponding to ordinal levels this latest dimension will be broadcast when comparing with `cdf`. Returns ------- :class:`torch.Tensor` : Vector of integer values corresponding to the multinomial sampling. Result is in [[0, L]] """ r = torch.rand(self._sample_shape).unsqueeze(-1) return (r < self.cdf).int().argmax(dim=-1)