# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from collections.abc import Sequence from typing import TYPE_CHECKING import paddle from paddle.distribution import distribution from paddle.utils.decorator_utils import param_one_alias if TYPE_CHECKING: from paddle import Tensor, dtype class ContinuousBernoulli(distribution.Distribution): r"""The Continuous Bernoulli distribution with parameter: `probs` characterizing the shape of the density function. The Continuous Bernoulli distribution is defined on [0, 1], and it can be viewed as a continuous version of the Bernoulli distribution. `The continuous Bernoulli: fixing a pervasive error in variational autoencoders. `_ Mathematical details The probability density function (pdf) is .. math:: p(x;\lambda) = C(\lambda)\lambda^x (1-\lambda)^{1-x} In the above equation: * :math:`x`: is continuous between 0 and 1 * :math:`probs = \lambda`: is the probability. * :math:`C(\lambda)`: is the normalizing constant factor .. math:: C(\lambda) = \left\{ \begin{aligned} &2 & \text{ if $\lambda = \frac{1}{2}$} \\ &\frac{2\tanh^{-1}(1-2\lambda)}{1 - 2\lambda} & \text{ otherwise} \end{aligned} \right. Args: probs(int|float|Tensor): The probability of Continuous Bernoulli distribution between [0, 1], which characterize the shape of the pdf. If the input data type is int or float, the data type of `probs` will be convert to a 1-D Tensor the paddle global default dtype. lims(tuple): Specify the unstable calculation region near 0.5, where the calculation is approximated by talyor expansion. The default value is (0.499, 0.501). Examples: .. code-block:: pycon >>> import paddle >>> from paddle.distribution import ContinuousBernoulli >>> paddle.set_device("cpu") >>> paddle.seed(100) >>> rv = ContinuousBernoulli(paddle.to_tensor([0.2, 0.5])) >>> print(rv.sample([2])) Tensor(shape=[2, 2], dtype=float32, place=Place(cpu), stop_gradient=True, [[0.38694882, 0.20714243], [0.00631948, 0.51577556]]) >>> print(rv.mean) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [0.38801414, 0.50000000]) >>> print(rv.variance) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [0.07589778, 0.08333334]) >>> print(rv.entropy()) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [-0.07641457, 0. ]) >>> print(rv.cdf(paddle.to_tensor(0.1))) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [0.17259926, 0.10000000]) >>> print(rv.icdf(paddle.to_tensor(0.1))) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [0.05623737, 0.10000000]) >>> rv1 = ContinuousBernoulli(paddle.to_tensor([0.2, 0.8])) >>> rv2 = ContinuousBernoulli(paddle.to_tensor([0.7, 0.5])) >>> print(rv1.kl_divergence(rv2)) Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True, [0.20103608, 0.07641447]) """ probs: Tensor lims: Tensor dtype: dtype def __init__( self, probs: float | Tensor, lims: tuple[float] = (0.499, 0.501) ) -> None: self.dtype = paddle.get_default_dtype() self.probs = self._to_tensor(probs) self.lims = paddle.to_tensor(lims, dtype=self.dtype) # eps_prob is used to clip the input `probs` in the range of [eps_prob, 1-eps_prob] eps_prob = paddle.finfo(self.probs.dtype).eps self.probs = paddle.clip(self.probs, min=eps_prob, max=1 - eps_prob) batch_shape = self.probs.shape super().__init__(batch_shape) def _to_tensor(self, probs: float | Tensor) -> Tensor: """Convert the input parameters into tensors Returns: Tensor: converted probability. """ # convert type if isinstance(probs, (float, int)): probs = paddle.to_tensor([probs], dtype=self.dtype) else: self.dtype = probs.dtype return probs def _cut_support_region(self) -> Tensor: """Generate stable support region indicator (prob < self.lims[0] && prob >= self.lims[1] ) Returns: Tensor: the element of the returned indicator tensor corresponding to stable region is True, and False otherwise """ return paddle.logical_or( paddle.less_equal(self.probs, self.lims[0]), paddle.greater_than(self.probs, self.lims[1]), ) def _cut_probs(self) -> Tensor: """Cut the probability parameter with stable support region Returns: Tensor: the element of the returned probability tensor corresponding to unstable region is set to be self.lims[0], and unchanged otherwise """ return paddle.where( self._cut_support_region(), self.probs, self.lims[0] * paddle.ones_like(self.probs), ) def _tanh_inverse(self, value: Tensor) -> Tensor: """Calculate the tanh inverse of value Args: value (Tensor) Returns: Tensor: tanh inverse of value """ return 0.5 * (paddle.log1p(value) - paddle.log1p(-value)) def _log_constant(self) -> Tensor: """Calculate the logarithm of the constant factor :math:`C(lambda)` in the pdf of the Continuous Bernoulli distribution Returns: Tensor: logarithm of the constant factor """ cut_probs = self._cut_probs() half = paddle.to_tensor(0.5, dtype=self.dtype) cut_probs_below_half = paddle.where( paddle.less_equal(cut_probs, half), cut_probs, paddle.zeros_like(cut_probs), ) cut_probs_above_half = paddle.where( paddle.greater_equal(cut_probs, half), cut_probs, paddle.ones_like(cut_probs), ) log_constant_propose = paddle.log( 2.0 * paddle.abs(self._tanh_inverse(1.0 - 2.0 * cut_probs)) ) - paddle.where( paddle.less_equal(cut_probs, half), paddle.log1p(-2.0 * cut_probs_below_half), paddle.log(2.0 * cut_probs_above_half - 1.0), ) x = paddle.square(self.probs - 0.5) taylor_expansion = ( paddle.log(paddle.to_tensor(2.0, dtype=self.dtype)) + (4.0 / 3.0 + 104.0 / 45.0 * x) * x ) return paddle.where( self._cut_support_region(), log_constant_propose, taylor_expansion ) @property def mean(self) -> Tensor: """Mean of Continuous Bernoulli distribution. Returns: Tensor: mean value. """ cut_probs = self._cut_probs() tmp = paddle.divide(cut_probs, 2.0 * cut_probs - 1.0) propose = tmp + paddle.divide( paddle.to_tensor(1.0, dtype=self.dtype), 2.0 * self._tanh_inverse(1.0 - 2.0 * cut_probs), ) x = self.probs - 0.5 taylor_expansion = ( 0.5 + (1.0 / 3.0 + 16.0 / 45.0 * paddle.square(x)) * x ) return paddle.where( self._cut_support_region(), propose, taylor_expansion ) @property def variance(self) -> Tensor: """Variance of Continuous Bernoulli distribution. Returns: Tensor: variance value. """ cut_probs = self._cut_probs() tmp = paddle.divide( cut_probs * (cut_probs - 1.0), paddle.square(1.0 - 2.0 * cut_probs), ) propose = tmp + paddle.divide( paddle.to_tensor(1.0, dtype=self.dtype), paddle.square(paddle.log1p(-cut_probs) - paddle.log(cut_probs)), ) x = paddle.square(self.probs - 0.5) taylor_expansion = 1.0 / 12.0 - (1.0 / 15.0 - 128.0 / 945.0 * x) * x return paddle.where( self._cut_support_region(), propose, taylor_expansion ) @param_one_alias(["shape", "sample_shape"]) def sample(self, shape: Sequence[int] = []) -> Tensor: """Generate Continuous Bernoulli samples of the specified shape. The final shape would be ``sample_shape + batch_shape``. Args: shape (Sequence[int], optional): Prepended shape of the generated samples. Returns: Tensor, Sampled data with shape `sample_shape` + `batch_shape`. """ with paddle.no_grad(): return self.rsample(shape) @param_one_alias(["shape", "sample_shape"]) def rsample(self, shape: Sequence[int] = []) -> Tensor: """Generate Continuous Bernoulli samples of the specified shape. The final shape would be ``sample_shape + batch_shape``. Args: shape (Sequence[int], optional): Prepended shape of the generated samples. Returns: Tensor, Sampled data with shape `sample_shape` + `batch_shape`. """ if not isinstance(shape, Sequence): raise TypeError('sample shape must be Sequence object.') shape = tuple(shape) batch_shape = tuple(self.batch_shape) output_shape = tuple(shape + batch_shape) u = paddle.uniform(shape=output_shape, dtype=self.dtype, min=0, max=1) return self.icdf(u) def log_prob(self, value: Tensor) -> Tensor: """Log probability density function. Args: value (Tensor): The input tensor. Returns: Tensor: log probability. The data type is the same as `self.probs`. """ value = paddle.cast(value, dtype=self.dtype) eps = paddle.finfo(self.probs.dtype).eps cross_entropy = paddle.nan_to_num( value * paddle.log(self.probs) + (1.0 - value) * paddle.log(1 - self.probs), neginf=-eps, ) return self._log_constant() + cross_entropy def prob(self, value: Tensor) -> Tensor: """Probability density function. Args: value (Tensor): The input tensor. Returns: Tensor: probability. The data type is the same as `self.probs`. """ return paddle.exp(self.log_prob(value)) def entropy(self) -> Tensor: r"""Shannon entropy in nats. The entropy is .. math:: \mathcal{H}(X) = -\log C + \left[ \log (1 - \lambda) -\log \lambda \right] \mathbb{E}(X) - \log(1 - \lambda) In the above equation: * :math:`\Omega`: is the support of the distribution. Returns: Tensor, Shannon entropy of Continuous Bernoulli distribution. """ log_p = paddle.log(self.probs) log_1_minus_p = paddle.log1p(-self.probs) return paddle.where( paddle.equal(self.probs, paddle.to_tensor(0.5, dtype=self.dtype)), paddle.full_like(self.probs, 0.0), ( -self._log_constant() + self.mean * (log_1_minus_p - log_p) - log_1_minus_p ), ) def cdf(self, value: Tensor) -> Tensor: r"""Cumulative distribution function .. math:: { P(X \le t; \lambda) = F(t;\lambda) = \left\{ \begin{aligned} &t & \text{ if $\lambda = \frac{1}{2}$} \\ &\frac{\lambda^t (1 - \lambda)^{1 - t} + \lambda - 1}{2\lambda - 1} & \text{ otherwise} \end{aligned} \right. } Args: value (Tensor): The input tensor. Returns: Tensor: quantile of :attr:`value`. The data type is the same as `self.probs`. """ value = paddle.cast(value, dtype=self.dtype) cut_probs = self._cut_probs() cdfs = ( paddle.pow(cut_probs, value) * paddle.pow(1.0 - cut_probs, 1.0 - value) + cut_probs - 1.0 ) / (2.0 * cut_probs - 1.0) unbounded_cdfs = paddle.where(self._cut_support_region(), cdfs, value) return paddle.where( paddle.less_equal(value, paddle.to_tensor(0.0, dtype=self.dtype)), paddle.zeros_like(value), paddle.where( paddle.greater_equal( value, paddle.to_tensor(1.0, dtype=self.dtype) ), paddle.ones_like(value), unbounded_cdfs, ), ) def icdf(self, value: Tensor) -> Tensor: r"""Inverse cumulative distribution function .. math:: { F^{-1}(x;\lambda) = \left\{ \begin{aligned} &x & \text{ if $\lambda = \frac{1}{2}$} \\ &\frac{\log(1+(\frac{2\lambda - 1}{1 - \lambda})x)}{\log(\frac{\lambda}{1-\lambda})} & \text{ otherwise} \end{aligned} \right. } Args: value (Tensor): The input tensor, meaning the quantile. Returns: Tensor: the value of the r.v. corresponding to the quantile. The data type is the same as `self.probs`. """ value = paddle.cast(value, dtype=self.dtype) cut_probs = self._cut_probs() return paddle.where( self._cut_support_region(), ( paddle.log1p(-cut_probs + value * (2.0 * cut_probs - 1.0)) - paddle.log1p(-cut_probs) ) / (paddle.log(cut_probs) - paddle.log1p(-cut_probs)), value, ) def kl_divergence(self, other: ContinuousBernoulli) -> Tensor: r"""The KL-divergence between two Continuous Bernoulli distributions with the same `batch_shape`. The probability density function (pdf) is .. math:: KL\_divergence(\lambda_1, \lambda_2) = - H - \{\log C_2 + [\log \lambda_2 - \log (1-\lambda_2)] \mathbb{E}_1(X) + \log (1-\lambda_2) \} Args: other (ContinuousBernoulli): instance of Continuous Bernoulli. Returns: Tensor, kl-divergence between two Continuous Bernoulli distributions. """ if self.batch_shape != other.batch_shape: raise ValueError( "KL divergence of two Continuous Bernoulli distributions should share the same `batch_shape`." ) part1 = -self.entropy() log_q = paddle.log(other.probs) log_1_minus_q = paddle.log1p(-other.probs) part2 = -( other._log_constant() + self.mean * (log_q - log_1_minus_q) + log_1_minus_q ) return part1 + part2