diff --git a/python/paddle/distribution/__init__.py b/python/paddle/distribution/__init__.py index 0e77febe5519100b3a4a8e1185adc10d77f17127..805675a0f2373bc3da8fa1c72c83667d3c3a6cfd 100644 --- a/python/paddle/distribution/__init__.py +++ b/python/paddle/distribution/__init__.py @@ -17,6 +17,7 @@ from paddle.distribution.beta import Beta from paddle.distribution.categorical import Categorical from paddle.distribution.dirichlet import Dirichlet from paddle.distribution.distribution import Distribution +from paddle.distribution.gumbel import Gumbel from paddle.distribution.exponential_family import ExponentialFamily from paddle.distribution.independent import Independent from paddle.distribution.kl import kl_divergence, register_kl @@ -32,7 +33,7 @@ from paddle.distribution.laplace import Laplace __all__ = [ # noqa 'Beta', 'Categorical', 'Dirichlet', 'Distribution', 'ExponentialFamily', 'Multinomial', 'Normal', 'Uniform', 'kl_divergence', 'register_kl', - 'Independent', 'TransformedDistribution', 'Laplace', 'LogNormal' + 'Independent', 'TransformedDistribution', 'Laplace', 'LogNormal', 'Gumbel' ] __all__.extend(transform.__all__) diff --git a/python/paddle/distribution/gumbel.py b/python/paddle/distribution/gumbel.py new file mode 100644 index 0000000000000000000000000000000000000000..7c9aebc652022cd519f115fcc12f172564660ba5 --- /dev/null +++ b/python/paddle/distribution/gumbel.py @@ -0,0 +1,242 @@ +# Copyright (c) 2022 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. + +import paddle +import numbers +import math +import numpy as np + +from paddle.distribution.transformed_distribution import TransformedDistribution +from paddle.fluid import framework as framework + + +class Gumbel(TransformedDistribution): + r"""The Gumbel distribution with location `loc` and `scale` parameters. + + Mathematical details + + The probability density function (pdf) is + + .. math:: + + pdf(x; mu, sigma) = exp(-(x - mu) / sigma - exp(-(x - mu) / sigma)) / sigma + + + In the above equation: + + * :math:`loc = \mu`: is the mean. + * :math:`scale = \sigma`: is the std. + + Args: + loc(int|float|tensor): The mean of gumbel distribution.The data type is int, float, tensor. + scale(int|float|tensor): The std of gumbel distribution.The data type is int, float, tensor. + + Examples: + .. code-block:: python + + import paddle + from paddle.distribution.gumbel import Gumbel + + # Gumbel distributed with loc=0, scale=1 + dist = Gumbel(paddle.full([1], 0.0), paddle.full([1], 1.0)) + dist.sample([2]) + # Tensor(shape=[2, 1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [[-0.27544352], [-0.64499271]]) + value = paddle.full([1], 0.5) + dist.prob(value) + # Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [0.33070430]) + dist.log_prob(value) + # Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [-1.10653067]) + dist.cdf(value) + # Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [0.54523915]) + dist.entropy() + # Tensor(shape=[1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [1.57721567]) + dist.rsample([2]) + # Tensor(shape=[2, 1], dtype=float32, place=Place(gpu:0), stop_gradient=True, [[0.80463481], [0.91893655]]) + + """ + + def __init__(self, loc, scale): + + if not isinstance(loc, (numbers.Real, framework.Variable)): + raise TypeError( + f"Expected type of loc is Real|Variable, but got {type(loc)}") + if not isinstance(scale, (numbers.Real, framework.Variable)): + raise TypeError( + f"Expected type of scale is Real|Variable, but got {type(scale)}" + ) + + if isinstance(loc, numbers.Real): + loc = paddle.full(shape=(), fill_value=loc) + + if isinstance(scale, numbers.Real): + scale = paddle.full(shape=(), fill_value=scale) + + if loc.shape != scale.shape: + self.loc, self.scale = paddle.broadcast_tensors([loc, scale]) + else: + self.loc, self.scale = loc, scale + + finfo = np.finfo(dtype='float32') + self.base_dist = paddle.distribution.Uniform( + paddle.full_like(self.loc, float(finfo.tiny)), + paddle.full_like(self.loc, float(1 - finfo.eps))) + + self.transforms = () + + super(Gumbel, self).__init__(self.base_dist, self.transforms) + + @property + def mean(self): + """Mean of distribution + + The mean is + + .. math:: + + mean = \mu + \sigma * γ + + In the above equation: + + * :math:`loc = \mu`: is the location parameter. + * :math:`scale = \sigma`: is the scale parameter. + * :math:`γ`: is the euler's constant. + + Returns: + Tensor: mean value. + + """ + return self.loc + self.scale * np.euler_gamma + + @property + def variance(self): + """Variance of distribution. + + The variance is + + .. math:: + + variance = \sigma^2 * \pi^2 / 6 + + In the above equation: + + * :math:`scale = \sigma`: is the scale parameter. + + Returns: + Tensor: The variance value. + + """ + temp = paddle.full(shape=self.loc.shape, + fill_value=math.pi * math.pi, + dtype=self.scale.dtype) + + return paddle.pow(self.scale, 2) * temp / 6 + + @property + def stddev(self): + """Standard deviation of distribution + + The standard deviation is + + .. math:: + + stddev = \sqrt{\sigma^2 * \pi^2 / 6} + + In the above equation: + * :math:`scale = \sigma`: is the scale parameter. + + Returns: + Tensor: std value + """ + return paddle.sqrt(self.variance) + + def prob(self, value): + """Probability density/mass function + + Args: + value (Tensor): The input tensor. + + Returns: + Tensor: probability.The data type is same with value. + + """ + y = (self.loc - value) / self.scale + + return paddle.exp(y - paddle.exp(y)) / self.scale + + def log_prob(self, value): + """Log probability density/mass function. + + Args: + value (Tensor): The input tensor. + + Returns: + Tensor: log probability.The data type is same with value. + + """ + return paddle.log(self.prob(value)) + + def cdf(self, value): + """Cumulative distribution function. + Args: + value (Tensor): value to be evaluated. + + Returns: + Tensor: cumulative probability of value. + + """ + return paddle.exp(-paddle.exp(-(value - self.loc) / self.scale)) + + def entropy(self): + """Entropy of Gumbel distribution. + + Returns: + Entropy of distribution. + + """ + return paddle.log(self.scale) + 1 + np.euler_gamma + + def sample(self, shape): + """Sample from ``Gumbel``. + + Args: + shape (Sequence[int], optional): The sample shape. Defaults to (). + + Returns: + Tensor: A tensor with prepended dimensions shape.The data type is float32. + + """ + with paddle.no_grad(): + return self.rsample(shape) + + def rsample(self, shape): + """reparameterized sample + Args: + shape (Sequence[int]): 1D `int32`. Shape of the generated samples. + + Returns: + Tensor: A tensor with prepended dimensions shape.The data type is float32. + + """ + exp_trans = paddle.distribution.ExpTransform() + affine_trans_1 = paddle.distribution.AffineTransform( + paddle.full(shape=self.scale.shape, + fill_value=0, + dtype=self.loc.dtype), -paddle.ones_like(self.scale)) + affine_trans_2 = paddle.distribution.AffineTransform( + self.loc, -self.scale) + + return affine_trans_2.forward( + exp_trans.inverse( + affine_trans_1.forward( + exp_trans.inverse(self._base.sample(shape))))) diff --git a/python/paddle/distribution/transformed_distribution.py b/python/paddle/distribution/transformed_distribution.py index da0e5908f0ce1bab37a8ab651de6095dee8d5924..8433ed0c910980f5533d93d91c62f588b948a759 100644 --- a/python/paddle/distribution/transformed_distribution.py +++ b/python/paddle/distribution/transformed_distribution.py @@ -62,15 +62,19 @@ class TransformedDistribution(distribution.Distribution): chain = transform.ChainTransform(transforms) base_shape = base.batch_shape + base.event_shape - if len(base_shape) < chain._domain.event_rank: + self._base = base + self._transforms = transforms + if not transforms: + super(TransformedDistribution, + self).__init__(base.batch_shape, base.event_shape) + return + if len(base.batch_shape + base.event_shape) < chain._domain.event_rank: raise ValueError( - f"'base' needs to have shape with size at least {chain._domain.event_rank}, but got {len(base_shape)}." + f"'base' needs to have shape with size at least {chain._domain.event_rank}, bug got {len(base_shape)}." ) if chain._domain.event_rank > len(base.event_shape): base = independent.Independent( (base, chain._domain.event_rank - len(base.event_shape))) - self._base = base - self._transforms = transforms transformed_shape = chain.forward_shape(base.batch_shape + base.event_shape) diff --git a/python/paddle/distribution/uniform.py b/python/paddle/distribution/uniform.py index 3c5655b27a4e62cf06dfe7f68200e8cb115efbee..5f9dc6c3d37dff1e48376ea59a1e34ae4f23a3b1 100644 --- a/python/paddle/distribution/uniform.py +++ b/python/paddle/distribution/uniform.py @@ -123,6 +123,8 @@ class Uniform(distribution.Distribution): self.low = tensor.cast(self.low, dtype=self.dtype) self.high = tensor.cast(self.high, dtype=self.dtype) + super(Uniform, self).__init__(self.low.shape) + def sample(self, shape, seed=0): """Generate samples of the specified shape. diff --git a/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel.py b/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel.py new file mode 100644 index 0000000000000000000000000000000000000000..3f3cfd8e8bb1a01a7455abf267aff7abff0da1b7 --- /dev/null +++ b/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel.py @@ -0,0 +1,157 @@ +# Copyright (c) 2022 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. + +import unittest +import numpy as np +import paddle +import scipy.stats + +import config +import parameterize + +from paddle.distribution.gumbel import Gumbel + + +@parameterize.place(config.DEVICES) +@parameterize.parameterize_cls((parameterize.TEST_CASE_NAME, 'loc', 'scale'), [ + ('one-dim', parameterize.xrand((4, )), parameterize.xrand((4, ))), + ('multi-dim', parameterize.xrand((5, 3)), parameterize.xrand((5, 3))), +]) +class TestGumbel(unittest.TestCase): + + def setUp(self): + self._dist = Gumbel(loc=paddle.to_tensor(self.loc), + scale=paddle.to_tensor(self.scale)) + + def test_mean(self): + mean = self._dist.mean + self.assertEqual(mean.numpy().dtype, self._np_mean().dtype) + np.testing.assert_allclose(mean, + self._np_mean(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_variance(self): + var = self._dist.variance + self.assertEqual(var.numpy().dtype, self._np_variance().dtype) + np.testing.assert_allclose(var, + self._np_variance(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_stddev(self): + stddev = self._dist.stddev + self.assertEqual(stddev.numpy().dtype, self._np_stddev().dtype) + np.testing.assert_allclose(stddev, + self._np_stddev(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_entropy(self): + entropy = self._dist.entropy() + self.assertEqual(entropy.numpy().dtype, self._np_entropy().dtype) + np.testing.assert_allclose(entropy, + self._np_entropy(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_sample(self): + + sample_shape = [10000] + samples = self._dist.sample(sample_shape) + sample_values = samples.numpy() + self.assertEqual(sample_values.dtype, self.scale.dtype) + + np.testing.assert_allclose(sample_values.mean(axis=0), + scipy.stats.gumbel_r.mean(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.loc.dtype))) + np.testing.assert_allclose(sample_values.var(axis=0), + scipy.stats.gumbel_r.var(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.loc.dtype))) + + def test_rsample(self): + + sample_shape = [10000] + samples = self._dist.rsample(sample_shape) + sample_values = samples.numpy() + self.assertEqual(sample_values.dtype, self.scale.dtype) + + np.testing.assert_allclose(sample_values.mean(axis=0), + scipy.stats.gumbel_r.mean(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.loc.dtype))) + np.testing.assert_allclose(sample_values.var(axis=0), + scipy.stats.gumbel_r.var(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.loc.dtype))) + + def _np_mean(self): + return self.loc + self.scale * np.euler_gamma + + def _np_stddev(self): + return np.sqrt(self._np_variance()) + + def _np_variance(self): + return np.divide( + np.multiply(np.power(self.scale, 2), np.power(np.pi, 2)), 6) + + def _np_entropy(self): + return np.log(self.scale) + 1 + np.euler_gamma + + +@parameterize.place(config.DEVICES) +@parameterize.parameterize_cls( + (parameterize.TEST_CASE_NAME, 'loc', 'scale', 'value'), [ + ('value-float', np.array([0.1, 0.4]), np.array([1., 4. + ]), np.array([3., 7.])), + ('value-int', np.array([0.1, 0.4]), np.array([1, 4]), np.array([3, 7])), + ('value-multi-dim', np.array([0.1, 0.4]), np.array( + [1, 4]), np.array([[5., 4], [6, 2]])), + ]) +class TestGumbelPDF(unittest.TestCase): + + def setUp(self): + self._dist = Gumbel(loc=paddle.to_tensor(self.loc), + scale=paddle.to_tensor(self.scale)) + + def test_prob(self): + np.testing.assert_allclose( + self._dist.prob(paddle.to_tensor(self.value)), + scipy.stats.gumbel_r.pdf(self.value, self.loc, self.scale), + rtol=config.RTOL.get(str(self.loc.dtype)), + atol=config.ATOL.get(str(self.loc.dtype))) + + def test_log_prob(self): + np.testing.assert_allclose( + self._dist.log_prob(paddle.to_tensor(self.value)), + scipy.stats.gumbel_r.logpdf(self.value, self.loc, self.scale), + rtol=config.RTOL.get(str(self.loc.dtype)), + atol=config.ATOL.get(str(self.loc.dtype))) + + def test_cdf(self): + np.testing.assert_allclose(self._dist.cdf(paddle.to_tensor(self.value)), + scipy.stats.gumbel_r.cdf( + self.value, self.loc, self.scale), + rtol=0.02, + atol=config.ATOL.get(str(self.loc.dtype))) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel_static.py b/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel_static.py new file mode 100644 index 0000000000000000000000000000000000000000..1c39da5fccf378eb19ad8f677e469591909c48ad --- /dev/null +++ b/python/paddle/fluid/tests/unittests/distribution/test_distribution_gumbel_static.py @@ -0,0 +1,172 @@ +# Copyright (c) 2022 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. + +import unittest + +import numpy as np +import scipy.stats + +import paddle +import config +import parameterize + +from paddle.distribution.gumbel import Gumbel + +paddle.enable_static() + + +@parameterize.place(config.DEVICES) +@parameterize.parameterize_cls((parameterize.TEST_CASE_NAME, 'loc', 'scale'), [ + ('one-dim', parameterize.xrand((4, )), parameterize.xrand((4, ))), + ('multi-dim', parameterize.xrand((5, 3)), parameterize.xrand((5, 3))), +]) +class TestGumbel(unittest.TestCase): + + def setUp(self): + startup_program = paddle.static.Program() + main_program = paddle.static.Program() + executor = paddle.static.Executor(self.place) + with paddle.static.program_guard(main_program, startup_program): + loc = paddle.static.data('loc', self.loc.shape, self.loc.dtype) + scale = paddle.static.data('scale', self.scale.shape, + self.scale.dtype) + self._dist = Gumbel(loc=loc, scale=scale) + self.sample_shape = [50000] + mean = self._dist.mean + var = self._dist.variance + stddev = self._dist.stddev + entropy = self._dist.entropy() + samples = self._dist.sample(self.sample_shape) + fetch_list = [mean, var, stddev, entropy, samples] + self.feeds = {'loc': self.loc, 'scale': self.scale} + + executor.run(startup_program) + [self.mean, self.var, self.stddev, self.entropy, + self.samples] = executor.run(main_program, + feed=self.feeds, + fetch_list=fetch_list) + + def test_mean(self): + self.assertEqual(str(self.mean.dtype).split('.')[-1], self.scale.dtype) + np.testing.assert_allclose(self.mean, + self._np_mean(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_variance(self): + self.assertEqual(str(self.var.dtype).split('.')[-1], self.scale.dtype) + np.testing.assert_allclose(self.var, + self._np_variance(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_stddev(self): + self.assertEqual( + str(self.stddev.dtype).split('.')[-1], self.scale.dtype) + np.testing.assert_allclose(self.stddev, + self._np_stddev(), + rtol=config.RTOL.get(str(self.scale.dtype)), + atol=config.ATOL.get(str(self.scale.dtype))) + + def test_entropy(self): + self.assertEqual( + str(self.entropy.dtype).split('.')[-1], self.scale.dtype) + + def test_sample(self): + self.assertEqual(self.samples.dtype, self.scale.dtype) + + np.testing.assert_allclose(self.samples.mean(axis=0), + scipy.stats.gumbel_r.mean(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.scale.dtype))) + np.testing.assert_allclose(self.samples.var(axis=0), + scipy.stats.gumbel_r.var(self.loc, + scale=self.scale), + rtol=0.1, + atol=config.ATOL.get(str(self.scale.dtype))) + + def _np_mean(self): + return self.loc + self.scale * np.euler_gamma + + def _np_stddev(self): + return np.sqrt(self._np_variance()) + + def _np_variance(self): + return np.divide( + np.multiply(np.power(self.scale, 2), np.power(np.pi, 2)), 6) + + def _np_entropy(self): + return np.log(self.scale) + 1 + np.euler_gamma + + +@parameterize.place(config.DEVICES) +@parameterize.parameterize_cls( + (parameterize.TEST_CASE_NAME, 'loc', 'scale', 'value'), [ + ('value-float', np.array([0.1, 0.4]), np.array([1., 4. + ]), np.array([3., 7.])), + ('value-int', np.array([0.1, 0.4]), np.array([1, 4]), np.array([3, 7])), + ('value-multi-dim', np.array([0.1, 0.4]), np.array( + [1, 4]), np.array([[5., 4], [6, 2]])), + ]) +class TestGumbelPDF(unittest.TestCase): + + def setUp(self): + startup_program = paddle.static.Program() + main_program = paddle.static.Program() + executor = paddle.static.Executor(self.place) + + with paddle.static.program_guard(main_program, startup_program): + loc = paddle.static.data('loc', self.loc.shape, self.loc.dtype) + scale = paddle.static.data('scale', self.scale.shape, + self.scale.dtype) + value = paddle.static.data('value', self.value.shape, + self.value.dtype) + self._dist = Gumbel(loc=loc, scale=scale) + prob = self._dist.prob(value) + log_prob = self._dist.log_prob(value) + cdf = self._dist.cdf(value) + fetch_list = [prob, log_prob, cdf] + self.feeds = {'loc': self.loc, 'scale': self.scale, 'value': self.value} + + executor.run(startup_program) + [self.prob, self.log_prob, + self.cdf] = executor.run(main_program, + feed=self.feeds, + fetch_list=fetch_list) + + def test_prob(self): + np.testing.assert_allclose(self.prob, + scipy.stats.gumbel_r.pdf( + self.value, self.loc, self.scale), + rtol=config.RTOL.get(str(self.loc.dtype)), + atol=config.ATOL.get(str(self.loc.dtype))) + + def test_log_prob(self): + np.testing.assert_allclose(self.log_prob, + scipy.stats.gumbel_r.logpdf( + self.value, self.loc, self.scale), + rtol=config.RTOL.get(str(self.loc.dtype)), + atol=config.ATOL.get(str(self.loc.dtype))) + + def test_cdf(self): + np.testing.assert_allclose(self.cdf, + scipy.stats.gumbel_r.cdf( + self.value, self.loc, self.scale), + rtol=0.3, + atol=config.ATOL.get(str(self.loc.dtype))) + + +if __name__ == '__main__': + unittest.main()