赞
踩
本次开发是由飞桨黑客马拉松中的题目进行进一步详细的分析
此任务的目标是在 Paddle 框架中,基于现有概率分布方案进行扩展,新增 Cauchy API, API调用 paddle.distribution.Cauchy
。
柯西分布是一个数学期望不存在的连续型概率分布。当随机变量X满足它的概率密度函数时,称X服从柯西分布.
柯西分布也叫作柯西一洛伦兹分布,它是以奥古斯丁-路易-柯西与亨德里克-洛伦兹名字命名的连续概率分布,如概述图所示。其概率密度函数为
式中:
x
0
{x_0}
x0为定义分布峰值位置的位置参数;
λ
{\lambda}
λ 为最大值一半处的一半宽度的尺度参数。
柯西分布的标注表达式为:
柯西函数的特点:
下面是使用python画出的一个柯西分布图:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
x = np.random.standard_cauchy(size=1000)
plt.hist(x,100,range=(-10,10))
plt.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9T6z2AA-1679471461906)(main_files/main_1_0.png)]
本项目的目的是为Paddle
贡献柯西分布的算子,因此,需要实现分布算子的常用功能,Cauchy 用于 Cauchy 分布的概率统计与随机采样。API具体包含如下方法:
功能:Creates a Laplace distribution parameterized by loc and scale.
mean
计算均值;mode
众数;variance
计算方差 ;stddev
计算标准偏差sample
随机采样;rsample
重参数化采样;prob
概率密度;log_prob
对数概率密度;entropy
熵计算;cdf
累积分布函数(Cumulative Distribution Function)icdf
逆累积分布函数kl_divergence
返回两个分布之间的kl散度本项目基于paddle.distribution API
基类进行开发。 class API 中的具体实现(部分方法已完成开发,故直接使用源代码),该api有两个参数:位置参数self.loc, 尺度参数self.scale
mean
计算均值;paddle.full(shape, nan, dtype=self.loc.dtype)
,由于柯西分布没有均值,故返回nan
mode
众数 ;self.loc
variance
计算方差 ;paddle.full(shape, inf, dtype=self.loc.dtype)
,由于柯西分布没有方差,故返回nan
stddev
计算标准偏差 (2 ** 0.5) * self.scale
sample
随机采样; with paddle.no_grad(): return self.rsample(shape)
rsample
重参数化采样;def rsample(self, sample_shape):
shape = sample_shape or (1,)
eps = paddle.to_tensor(numpy.random.standard_cauchy(shape), dtype=self.loc.dtype)
return self.loc + eps * self.scale
prob
概率密度;-math.log(math.pi) - self.scale.log() - (1 + ((value - self.loc) / self.scale)**2).log()
log_prob
对数概率密度;loc, scale, value = self._validate_value(value)
tmp = 1 + ((value - loc) / scale)**2
tmp = paddle.cast(paddle.to_tensor(tmp), value.dtype)
scale = paddle.cast(paddle.to_tensor(scale), value.dtype)
loc = paddle.cast(paddle.to_tensor(loc), value.dtype)
return -math.log(math.pi) - paddle.log(scale) - paddle.log(tmp)
entropy
熵计算;math.log(4 * math.pi) + self.scale.log()
cdf
累积分布函数(Cumulative Distribution Function)paddle.atan((value - self.loc) / self.scale) / math.pi + 0.5)
icdf
逆累积分布函数paddle.tan(math.pi * (value - 0.5)) * self.scale + self.loc
同时在paddle/distribution/kl.py
中注册_kl_cauchy_cauchy
函数,使用时可直接调用kl_divergence
计算cauchy分布之间的kl散度。
测试考虑的 case 如下:
根据api类各个方法及特性传参的不同,把单测分成三个部分:测试分布的特性(无需额外参数)、测试分布的概率密度函数(需要传值)以及测试KL散度(需要传入一个实例)。
测试Cauchy分布的特性
测试方法:该部分主要测试分布的均值、方差、熵等特征。类TestCauchy
继承unittest.TestCase
,分别实现方法setUp
(初始化),test_mean
(mean单测),test_variance
(variance单测),test_stddev
(stddev单测),test_entropy
(entropy单测),test_sample
(sample单测)。
均值、方差、标准差通过Numpy计算相应值,对比Cauchy类中相应property的返回值,若一致即正确;
采样方法除验证其返回的数据类型及数据形状是否合法外,还需证明采样结果符合Cauchy分布。验证策略如下:随机采样30000个Cauchy分布下的样本值,计算采样样本的均值和方差,并比较同分布下scipy.stats.Cauchy返回的均值与方差,检查是否在合理误差范围内;同时通过Kolmogorov-Smirnov test
进一步验证采样是否属于Cauchy分布,若计算所得ks值小于0.02,则拒绝不一致假设,两者属于同一分布;
熵计算通过对比scipy.stats.Cauchy.entropy
的值是否与类方法返回值一致验证结果的正确性。
测试用例:单测需要覆盖单一维度的Cauchy分布和多维度分布情况,因此使用两种初始化参数
‘one-dim’: loc=parameterize.xrand((2, )), scale=parameterize.xrand((2, ))
;
‘multi-dim’: loc=parameterize.xrand((5, 5)), scale=parameterize.xrand((5, 5))
。
测试Cauchy分布的概率密度函数
测试方法:该部分主要测试分布各种概率密度函数。类TestCauchyPDF继承unittest.TestCase,分别实现方法setUp(初始化),test_prob(prob单测),test_log_prob(log_prob单测),test_cdf(cdf单测),test_icdf(icdf)。以上分布在scipy.stats.Cauchy中均有实现,因此给定某个输入value,对比相同参数下Cauchy分布的scipy实现以及paddle实现的结果,若误差在容忍度范围内则证明实现正确。
测试用例:为不失一般性,测试使用多维位置参数和尺度参数初始化Cauchy类,并覆盖int型输入及float型输入。
‘value-float’: loc=np.array([0.2, 0.3]), scale=np.array([2, 3]), value=np.array([2., 5.]); * 'value-int': loc=np.array([0.2, 0.3]), scale=np.array([2, 3]), value=np.array([2, 5])
;
‘value-multi-dim’: loc=np.array([0.2, 0.3]), scale=np.array([2, 3]), value=np.array([[4., 6], [8, 2]])
。
3. 测试Cauchy分布之间的KL散度
测试方法:该部分测试两个Cauchy分布之间的KL散度。类TestCauchyAndCauchyKL
继承unittest.TestCase
,分别实现setUp
(初始化),test_kl_divergence
(kl_divergence)。在scipy中scipy.stats.entropy
可用来计算两个分布之间的散度。因此对比两个Cauchy分布在paddle.distribution.kl_divergence下和在scipy.stats.Cauchy下计算的散度,若结果在误差范围内,则证明该方法实现正确。
测试用例:分布1:loc=np.array([0.0]), scale=np.array([1.0]), 分布2: loc=np.array([1.0]), scale=np.array([0.5])
本项目参考了Pytorch的柯西分布算子实现方式
import math from numpy import inf, nan import numbers import numpy import paddle from paddle.distribution import constraint from paddle.distribution import Distribution from paddle.fluid import framework as framework # __all__ = ['Cauchy'] class Cauchy(Distribution): r""" Creates a Cauchy distribution parameterized by :attr:`loc` and :attr:`scale`. Mathematical details The probability density function (pdf) is .. math:: pdf(x; \mu, \sigma) = \frac{1}{2 * \sigma} * e^{\frac{-|x - \mu|}{\sigma}} In the above equation: * :math:`loc = \mu`: is the location parameter. * :math:`scale = \sigma`: is the scale parameter. Args: loc (scalar|Tensor): The mean of the distribution. scale (scalar|Tensor): The scale of the distribution. Examples: .. code-block:: python import paddle m = paddle.distribution.Cauchy(paddle.to_tensor([0.0]), paddle.to_tensor([1.0])) m.sample() # Cauchy distributed with loc=0, scale=1 # Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=True,[4.26052284]) """ def __init__(self, loc, scale): arg_constraint = {'loc': constraint.real, 'scale': constraint.positive} support = constraint.real if (len(scale.shape) > 0 or len(loc.shape) > 0) and ( loc.dtype == scale.dtype ): self.loc, self.scale = paddle.broadcast_tensors([loc, scale]) else: self.loc, self.scale = loc, scale super().__init__(self.loc.shape) @property def mean(self): """Mean of distribution. Returns: Tensor: The mean value. """ return paddle.full(shape=(), fill_value=nan, dtype=self.loc.dtype) @property def mode(self): """Mode of distribution. Returns: Tensor: The mode value. """ return self.loc @property def variance(self): return paddle.full(shape=(), fill_value=inf, dtype=self.loc.dtype) def sample(self, shape=()): r"""Generate samples of the specified shape. Args: shape(tuple[int]): The shape of generated samples. Returns: Tensor: A sample tensor that fits the Laplace distribution. Examples: .. code-block:: python import paddle m = paddle.distribution.Laplace(paddle.to_tensor([0.0]), paddle.to_tensor([1.0])) m.sample() # Laplace distributed with loc=0, scale=1 #Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=True,[4.26052284]) """ if not isinstance(shape, tuple): raise TypeError( f'Expected shape should be tuple[int], but got {type(shape)}' ) with paddle.no_grad(): return self.rsample(shape) def rsample(self, sample_shape): shape = sample_shape or (1,) eps = paddle.to_tensor(numpy.random.standard_cauchy(shape), dtype=self.loc.dtype) return self.loc + eps * self.scale def log_prob(self, value): loc, scale, value = self._validate_value(value) tmp = 1 + ((value - loc) / scale)**2 tmp = paddle.cast(paddle.to_tensor(tmp), value.dtype) scale = paddle.cast(paddle.to_tensor(scale), value.dtype) loc = paddle.cast(paddle.to_tensor(loc), value.dtype) return -math.log(math.pi) - paddle.log(scale) - paddle.log(tmp) def cdf(self, value): loc, scale, value = self._validate_value(value) return paddle.atan((value - loc) / scale) / math.pi + 0.5 def icdf(self, value): # return paddle.tan(math.pi * (value - 0.5)) * self.scale + self.loc return paddle.full(shape=(), fill_value=nan, dtype=self.loc.dtype) # np.tan(np.pi*q-np.pi/2.0) def entropy(self): return math.log(4 * math.pi) + self.scale.log() def _validate_value(self, value): """Argument dimension check for distribution methods such as `log_prob`, `cdf` and `icdf`. Args: value (Tensor|Scalar): The input value, which can be a scalar or a tensor. Returns: loc, scale, value: The broadcasted loc, scale and value, with the same dimension and data type. """ if isinstance(value, numbers.Real): value = paddle.full(shape=(), fill_value=value) if value.dtype != self.loc.dtype: value = paddle.cast(value, self.loc.dtype) if self.scale.dtype != self.loc.dtype: scale = paddle.cast(self.scale, self.loc.dtype) else: scale = self.loc if ( len(self.scale.shape) > 0 or len(self.loc.shape) > 0 or len(value.shape) > 0 ): loc, scale, value = paddle.broadcast_tensors( [self.loc, scale, value] ) else: loc, scale = self.loc, self.scale return loc, scale, value
测试代码必须放置在/mnt/cfs/algorithm/yunji.feng/code/Paddle/python/paddle/fluid/tests/unittests/distribution
目录下,它会引用在paddle源码中实现的parameterize
文件,并使用unittest
模块完成整个测试任务。
Unittest
是Python内部自带的一个单元测试的模块,具备完整的测试结构,支持自动化测试的执行,对测试用例集进行组织,并且提供了丰富的断言方法,最后生成测试报告。Unittest
框架的初衷是用于单元测试,但也不限于此,在实际工作中,由于它强大的功能,提供的完整的测试流程,我们往往将其用于自动化测试的各个方面。
np.testing.assert_allclose
是numpy
的一个测试功能,能够对比两个函数的输出是否一致,其输入参数为:
参数 | 类型 | 说明 |
---|---|---|
actual | array_like | 实际数组 |
desired | array_like | 期望数组 |
rtol | float | 相对 tolerance |
atol | float | 绝对 tolerance |
equal_nan | bool | True 时认为 NaN 值相等 |
err_msg | str | 断言失败时的错误信息 |
verbose | bool | 在错误信息中是否附加冲突值 |
通过np.testing.assert_allclose
,能够对本项目实现的Cauchy
算子运算的正确性进行检验,这里使用了scipy
的Cauchy
函数作为期望值。
import unittest import numpy as np import paddle import scipy.stats import config import parameterize SEED = 2023 @parameterize.place(config.DEVICES) @parameterize.parameterize_cls( (parameterize.TEST_CASE_NAME, 'loc', 'scale'), [ ('one-dim', parameterize.xrand((2, )),\ parameterize.xrand((2, ))), ('multi-dim', parameterize.xrand((5, 5)),\ parameterize.xrand((5, 5))), ]) class TestCauchy(unittest.TestCase): def setUp(self): self._dist = paddle.distribution.Cauchy(loc=paddle.to_tensor(self.loc), scale=paddle.to_tensor(\ self.scale)) def test_entropy(self): entropy = self._dist.entropy() self.assertEqual(entropy.numpy().dtype, self.scale.dtype) def _np_entropy(self): return scipy.stats.cauchy.entropy(loc=self.loc, scale=self.scale) def _kstest(self, loc, scale, samples): # Uses the Kolmogorov-Smirnov test for goodness of fit. ks, p_value = scipy.stats.kstest( samples, scipy.stats.cauchy(loc, scale=scale).cdf) return ks < 0.02 @parameterize.place(config.DEVICES) @parameterize.parameterize_cls( (parameterize.TEST_CASE_NAME, 'loc', 'scale', 'value'), [ ('value-float', np.array([0.2, 0.3]),\ np.array([2, 3]), np.array([2., 5.])), ('value-int', np.array([0.2, 0.3]),\ np.array([2, 3]), np.array([2, 5])), ('value-multi-dim', np.array([0.2, 0.3]), np.array([2, 3]),\ np.array([[4., 6], [8, 2]])), ]) class TestCauchyPDF(unittest.TestCase): def setUp(self): self._dist = paddle.distribution.Cauchy(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.cauchy.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.cauchy.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.cauchy.cdf( self.value, self.loc, self.scale), rtol=config.RTOL.get(str(self.loc.dtype)), atol=config.ATOL.get(str(self.loc.dtype))) def test_icdf(self): np.testing.assert_allclose( self._dist.icdf(paddle.to_tensor(self.value)), scipy.stats.cauchy.ppf(self.value, self.loc, self.scale), rtol=config.RTOL.get(str(self.loc.dtype)), atol=config.ATOL.get(str(self.loc.dtype))) if __name__ == '__main__': , atol=config.ATOL.get(str(self.loc.dtype))) if __name__ == '__main__': unittest.main()
在实现过程中,学习Pytorch的实现方式能够很快地写出柯西分布的核心代码,但是在测试过程中会出现各种问题,比如数据类型不同、Nan返回错误…等。
本项目是笔者第一次接触Paddle框架的底层实现方式,比想象中要容易很多,但是算子的测试是十分严谨和复杂的,中途遇到的大多数问题在网上都没有解答,只能够根据算法的实现方式去排除bug,进一步加深了对Paddle框架的理解
本项目是AI特训营的项目之一,很开心能够在这样的活动中学到新的东西,Paddle YYDS!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。