PEP 485 – 用于测试近似相等的函数
- 作者:
- Christopher Barker <PythonCHB at gmail.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2015年1月20日
- Python 版本:
- 3.5
- 历史记录:
- 决议:
- Python-Dev 消息
摘要
本 PEP 建议向标准库 math 模块添加一个 isclose() 函数,该函数用于确定一个值是否近似等于或“接近”另一个值。
基本原理
浮点数包含有限的精度,这导致它们无法精确表示某些值,并且在重复计算时会累积误差。因此,通常建议仅在非常特定的情况下使用相等比较。通常不等式比较可以满足要求,但在某些情况下(通常在测试中),程序员希望确定计算出的值是否“接近”预期值,而无需要求它们完全相等。这种情况很常见,尤其是在测试中,并且并非总是显而易见如何做到这一点,因此将其添加到标准库中将非常有用。
现有实现
标准库包含 unittest.TestCase.assertAlmostEqual
方法,但它
- 隐藏在 unittest.TestCase 类中
- 是一个断言,因此您无法将其用作命令行等处的通用测试(很容易)。
- 是绝对差测试。通常,差异的度量需要,特别是对于浮点数,相对误差,即“这两个值是否彼此的 x% 之内?”,而不是绝对误差。尤其是在值的幅度事先未知的情况下。
numpy 包具有 allclose()
和 isclose()
函数,但它们仅在使用 numpy 时可用。
statistics 包测试包含一个实现,用于其单元测试。
您还可以在 Stack Overflow 和其他帮助站点上找到讨论和示例实现。
许多其他非 Python 系统提供了这样的测试,包括 Boost C++ 库和 APL 语言 [4]。
这些现有的实现表明这是一个普遍的需求,并且编写起来并不简单,这使得它成为标准库的候选者。
建议的实现
注意:本 PEP 是在 python-ideas 列表上进行广泛讨论的结果 [1]。
新函数将进入 math 模块,并具有以下签名
isclose(a, b, rel_tol=1e-9, abs_tol=0.0)
a
和 b
:是要测试其相对接近度的两个值
rel_tol
:是相对容差——它是允许的误差量,相对于 a 或 b 的较大绝对值。例如,要设置 5% 的容差,请传递 tol=0.05。默认容差为 1e-9,这确保这两个值在约 9 位小数内相同。rel_tol
必须大于 0.0
abs_tol
:是最低绝对容差水平——在接近零时很有用。
除了错误检查等之外,该函数将返回以下结果
abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )
名称 isclose
是为了与现有的 isnan
和 isinf
保持一致而选择的。
非有限数的处理
NaN、inf 和 -inf 的 IEEE 754 特殊值将根据 IEEE 规则处理。具体来说,NaN 不被认为接近任何其他值,包括 NaN。inf 和 -inf 仅被认为接近自身。
非浮点数类型
主要用例预计是浮点数。但是,用户可能希望以类似的方式比较其他数值类型。理论上,它应该适用于任何支持 abs()
、乘法、比较和减法的类型。但是,math 模块中的实现是用 C 编写的,因此不能(容易地)使用 Python 的鸭子类型。相反,传递给函数的值将在执行计算之前转换为 float 类型。传递无法转换为浮点数的类型(或值)将引发相应的异常(TypeError、ValueError 或 OverflowError)。
代码将经过测试以至少适应这些类型的一些值
Decimal
int
Fraction
complex
:对于复数,将向cmath
模块添加一个配套函数。在cmath.isclose()
中,容差指定为浮点数,复数的绝对值将用于缩放和比较。如果传递了复数容差,则其绝对值将用作容差。
注意:添加一个 Decimal.isclose()
来正确且完整地处理十进制类型可能是有意义的,但这不包含在本 PEP 中。
接近零时的行为
如果任一值为零,则相对比较会出现问题。根据定义,没有值相对于零很小。并且从计算上讲,如果任一值为零,则差异为另一值的绝对值,并且计算出的绝对容差将为 rel_tol
乘以该值。当 rel_tol
小于一时,差异将永远不会小于容差。
但是,虽然在数学上是正确的,但许多用例都需要用户知道计算出的值是否“接近”零。这需要绝对容差测试。如果用户需要在循环或推导式内调用此函数,其中一些(但不是全部)预期值可能为零,那么重要的是,可以使用单个函数和一组参数同时测试相对容差和绝对容差。
如果要比较的两个值跨越零,则存在类似的问题:如果 a 近似等于 -b,则 a 和 b 将永远不会被计算为“接近”。
为了处理这种情况,可以使用可选参数 abs_tol
来设置在计算出的相对容差非常小或为零的情况下使用的最小容差。也就是说,如果它们之间的差异小于 abs_tol
,则始终将这些值视为接近。
默认绝对容差值设置为零,因为对于一般情况没有合适的值。在不知道给定用例的预期可能值的情况下,不可能知道合适的值。如果所有测试值都按 1 的顺序排列,那么约 1e-9 的值可能合适,但如果预期值按 1e-9 或更小的顺序排列,则该值将过大。
任何非零默认值都可能导致用户的测试完全不适当地通过。另一方面,如果第一次使用默认值对零的测试失败,则会提示用户为手头的问题选择一个合适的值以使测试通过。
注意:本 PEP 的作者已决定重新检查他许多使用 numpy allclose()
函数(提供默认绝对容差)的测试,并确保默认值是合适的。
如果用户将 rel_tol 参数设置为 0.0,则只有绝对容差会影响结果。虽然这不是该函数的目标,但它也允许将其用作纯粹的绝对容差检查。
实现
Python 中的示例实现可在 (截至 2015 年 1 月 22 日) gitHub 上找到
https://github.com/PythonCHB/close_pep/blob/master/is_close.py
此实现有一个标志,允许用户选择要应用的相对容差测试——本 PEP 没有建议保留该标志,而是建议选择弱测试。
此处还有本 PEP 和测试代码等的草稿
相对差
基本上有两种方法可以考虑两个数字彼此之间的接近程度
绝对差:简单地 abs(a-b)
相对差:abs(a-b)/scale_factor
[2]。
绝对差非常简单,因此本提案侧重于相对差。
通常,比例因子是所考虑值的某个函数,例如
- 输入值之一的绝对值
- 两者中的最大绝对值
- 两者中的最小绝对值。
- 这两个值的算术平均值的绝对值
这些导致了以下确定两个值 a 和 b 是否彼此接近的可能性。
abs(a-b) <= tol*abs(a)
abs(a-b) <= tol * max( abs(a), abs(b) )
abs(a-b) <= tol * min( abs(a), abs(b) )
abs(a-b) <= tol * abs(a + b)/2
注意:(2) 和 (3) 也可以写成
(abs(a-b) <= abs(tol*a)) or (abs(a-b) <= abs(tol*b))
(abs(a-b) <= abs(tol*a)) and (abs(a-b) <= abs(tol*b))
(Boost 将这些称为“弱”和“强”公式 [3]) 这些在计算上可能稍微有效一点,因此在示例代码中使用。
这些公式中的每一个都可能导致略微不同的结果。但是,如果容差值很小,则差异非常小。事实上,通常小于可用的浮点数精度。
差异有多大?
在选择确定接近度的方法时,可能需要了解使用一个测试或另一个测试会产生多少差异——即有多少个值(或值的范围)会通过一个测试,但不会通过另一个测试。
最大的差异在于选项 (2) 和 (3),其中允许的绝对差值按较大值或较小值中的一个进行缩放。
定义delta
为由较大值定义的允许绝对容差与由较小值定义的允许绝对容差之间的差值。也就是说,两个输入值需要有多大差异才能从两个测试中获得不同的结果。tol
是相对容差值。
假设a
是较大的值,并且a
和b
都是正数,以使分析更容易。delta
因此为
delta = tol * (a-b)
或
delta / tol = (a-b)
通过测试的最大绝对差值:(a-b)
,等于容差乘以较大值
(a-b) = tol * a
代入delta的表达式
delta / tol = tol * a
所以
delta = tol**2 * a
例如,对于a = 10
,b = 9
,tol = 0.1
(10%)
最大容差 tol * a == 0.1 * 10 == 1.0
最小容差 tol * b == 0.1 * 9.0 == 0.9
delta = (1.0 - 0.9) = 0.1
或 tol**2 * a = 0.1**2 * 10 = .1
在这种情况下,最大和最小容差测试之间的绝对差异可能很大。但是,所提议函数的主要用例是测试计算结果。在这种情况下,可能会选择更小数量级的相对容差。
例如,相对容差为1e-8
大约是python浮点数可用精度的二分之一。在这种情况下,两个测试之间的差异为1e-8**2 * a
或1e-16 * a
,这接近于python浮点数的精度极限。如果将相对容差设置为建议的默认值1e-9(或更小),则两个测试之间的差异将丢失到浮点数精度的限制。也就是说,对于a和b的所有值,这四种方法都将产生完全相同的结果。
此外,在常见用法中,容差定义为1个有效数字——即,1e-9指定了大约9位十进制精度。因此,各种可能的测试之间的差异远低于指定容差的精度。
对称性
相对比较可以是对称的或非对称的。对于对称算法
isclose(a,b)
始终与isclose(b,a)
相同
如果相对接近度测试仅使用其中一个值(例如上面的 (1)),则结果是非对称的,即isclose(a,b)不一定与isclose(b,a)相同。
哪种方法最合适取决于提出的问题。如果问题是:“这两个数字彼此接近吗?”,则没有明显的顺序,对称测试最合适。
但是,如果问题是:“计算值是否在该已知值的x%范围内?”,则适应该将容差缩放到已知值,非对称测试最合适。
从上一节可以清楚地看出,在常见用例中,这两种方法都会产生相同或相似的结果。在这种情况下,本提案的目标是提供一个最不可能产生意外结果的函数。
对称方法提供了一种有吸引力的一致性——它反映了相等的对称性,并且不太可能让人困惑。对称测试还可以免除用户考虑设置参数的顺序的需要。还指出,在某些情况下,评估顺序可能没有明确定义,例如在将一组值全部相互比较的情况下。
在某些情况下,用户确实需要知道某个值是否在已知值的特定范围内。在这种情况下,只需直接编写测试即可
if a-b <= tol*a:
(假设在这种情况下 a > b)。几乎不需要为此特定情况提供函数。
本提案使用对称测试。
哪种对称测试?
考虑了三个对称测试
使用两个值的算术平均数的情况要求在除以 2 之前将值加在一起,这可能会导致非常大的数字出现额外的溢出到无穷大,或者要求在加在一起之前将每个值除以 2,这可能会导致非常小的数字下溢到零。这种影响只会发生在浮点值的极限处,但我们决定该方法没有任何好处值得减少功能范围或增加检查值的复杂性以确定计算顺序。
这使得boost“弱”测试 (2) 成为选择——或者使用较大的值来缩放容差,或者Boost“强” (3) 测试,它使用较小的值来缩放容差。对于较小的容差,它们会产生相同的结果,但本提案使用boost“弱”测试用例:它是对称的,并且对于非常大的容差提供了更有用的结果。
大容差
最常见的用例预计是较小的容差——大约为默认值1e-9。但是,可能存在用户想要知道两个相当不同的值是否在彼此的特定范围内的情况:“a是否在b的200%(rel_tol = 2.0)以内?”。在这种情况下,如果其中一个值为零,则强测试永远不会表明两个值在彼此的范围内。而弱测试将使用较大的(非零)值进行测试,因此如果一个值为零,则返回真。例如:0是否在10的200%以内?10的200%是20,因此10的200%范围是-10到+30。零落在此范围内,因此它将返回真。
默认值
相对和绝对容差需要默认值。
相对容差默认值
将两个值视为“接近”所需的相对容差完全取决于用例。然而,相对容差需要大于1e-16(python浮点数的近似精度)。选择1e-9的值是因为它是各种可能方法将产生相同结果的最大相对容差,并且它也约为python浮点数可用精度的二分之一。在一般情况下,不期望良好的数值算法丢失超过可用精度数字的大约一半,如果可以接受更大的容差,则用户应在这种情况下考虑正确的值。因此,预计1e-9在许多情况下“都能正常工作”。
绝对容差默认值
绝对容差值主要用于与零进行比较。确定某个值是否“接近”零所需的绝对容差完全取决于用例。本质上,有用范围也没有界限——预期值可能在python浮点数的限制范围内。因此,选择0.0作为默认值。
如果对于给定的用例,用户需要与零进行比较,则测试将保证第一次失败,用户可以选择适当的值。
有人建议,事实上,与零进行比较是一个常见的用例(证据表明numpy函数通常与零一起使用)。在这种情况下,最好有一个“有用”的默认值。建议使用大约1e-8的值,大约是值为1的值的浮点精度的二分之一。
但是,引用禅宗:“面对模糊不清,拒绝猜测的诱惑。”猜测用户最常关注的值接近1.0会导致在使用较小值时出现虚假通过测试——这可能比要求用户仔细选择适当的值更有害。
预期用途
主要预期用例是各种形式的测试——“计算结果是否接近我期望的结果?”。这种测试可能是正式单元测试套件的一部分,也可能不是。此类测试可以在命令行中一次性使用,在IPython笔记本中使用,作为doctest的一部分,或者在if __name__ == "__main__"
块中使用简单的断言。
它也适合用作隐式函数的简单迭代解的终止条件。
guess = something
while True:
new_guess = implicit_function(guess, *args)
if isclose(new_guess, guess):
break
guess = new_guess
不恰当的用途
浮点比较的一个用例是测试数值算法的准确性。但是,在这种情况下,数值分析师理想情况下会进行仔细的误差传播分析,并且应该确切地了解要测试的内容。也可能需要调用ULP(最后一位的单位)比较。虽然此函数可能在这些情况下很有用,但它并非旨在在没有仔细考虑的情况下以这种方式使用。
其他方法
unittest.TestCase.assertAlmostEqual
(https://docs.pythonlang.cn/3/library/unittest.html#unittest.TestCase.assertAlmostEqual)
通过计算差异,四舍五入到给定的十进制位数(默认为7),并与零进行比较,来测试值是否近似(或不近似)相等。
此方法纯粹是绝对容差测试,没有解决相对容差测试的需求。
numpy isclose()
https://docs.scipy.org.cn/doc/numpy-dev/reference/generated/numpy.isclose.html
numpy包提供了向量化函数isclose()和allclose(),用于与本提案类似的用例
isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)
返回一个布尔数组,其中两个数组在容差范围内按元素相等。容差值是正数,通常是非常小的数字。相对差值 (rtol * abs(b)) 和绝对差值 atol 加在一起以与a和b之间的绝对差值进行比较
在这种方法中,绝对和相对容差加在一起,而不是本提案中使用的or
方法。这在计算上更简单,如果相对容差大于绝对容差,则加法将不起作用。但是,如果绝对和相对容差的数量级相似,则允许的差异将大约是预期值的兩倍。
这使得函数更难理解,并且在这种情况下没有计算优势。
更重要的是,如果传递的值与绝对容差相比很小,则相对容差将完全被淹没,这可能是出乎意料的。
这就是为什么在本提案中,绝对容差默认为零——用户需要选择一个适合当前值的数值。
Boost 浮点数比较
Boost 项目([3])提供了一个浮点数比较函数。它采用了一种对称的方法,既有“弱”(两个相对误差中较大的)选项,也有“强”(两个相对误差中较小的)选项。本提案使用 Boost 的“弱”方法。当结果在大多数情况下相似时,没有必要通过提供选择不同方法的选项来复杂化 API,而且用户在任何情况下都不太可能知道选择哪种方法。
备选方案
一个方案
主要替代方案是不提供标准库函数,而是为用户提供一个参考方案。这样做的好处是,该方案可以提供并解释各种选项,并让用户选择最合适的选项。但是,这将要求任何需要此类测试的人至少将函数复制到他们的代码库中,并选择要使用的比较方法。
zero_tol
一种可能性是提供一个零容差参数,而不是绝对容差参数。这将是一个仅在其中一个参数恰好为零时应用的绝对容差。这样做的好处是,对于所有非零值,它都保留了完整的相对容差行为,同时允许对零进行测试。但是,它也会导致一个可能令人惊讶的结果,即一个小值可能“接近”零,但“不接近”一个更小的值。例如,1e-10 “接近”零,但不“接近”1e-11。
没有绝对容差
鉴于与零比较的问题,另一种可能性是只提供相对容差,并让与零的比较失败。在这种情况下,用户需要进行一个简单的绝对测试:abs(val) < zero_tol
,在比较涉及零的情况下。
但是,这将不允许对一系列值(例如在循环或推导式中)使用相同的调用。这使得函数的实用性大大降低。需要注意的是,如果未覆盖默认值,则默认的 abs_tol=0.0 会达到相同的效果。
其他测试
其他考虑的测试都在上面“相对误差”部分进行了讨论。
参考文献
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0485.rst
上次修改时间:2023-09-09 17:39:29 GMT