数值编程语言是否区分 "largest finite number" 和 "infinity"?

Do numerical programming languages distinguish between a "largest finite number" and "infinity"?

提问动机:

在我所知道的标准数字语言中(例如 Matlab、Pythonnumpy 等),例如,如果您采用适度大数的指数,则输出为无穷大数值溢出的结果。如果将其乘以 0,则会得到 NaN。单独来看,这些步骤是足够合理的,但它们揭示了数学实现中的逻辑错误。已知溢出产生的第一个数是有限的,我们显然希望这个大的有限数乘以 0 的结果为 0。

明确:

>>> import numpy as np
>>> np.exp(709)*0
0.0
>>> np.exp(710)*0
nan

我想我们可以在这里引入一个 "largest finite value" (LFV) 的概念,它具有以下属性:

另一方面,无穷大不应该简单地按照 LFV 描述的方式重新定义。对于 0 *infinity = 0 没有意义...适当地,无穷大的当前标准实现在此设置中产生 NaN。此外,有时需要将数字初始化为无穷大,并且您希望任何数值运算的结果,即使产生 LFV 的结果严格小于初始化值(这对于某些逻辑语句来说很方便)。我确信在其他情况下需要适当的无穷大——我的观点很简单,无穷大应该 而不是 简单地重新定义为具有上面的一些 LFV 属性。

问题:

我想知道是否有任何语言使用这样的方案,这样的方案是否有任何问题。这个问题在适当的数学中不会出现,因为数字的大小没有这些数字限制,但我认为在用编程语言实现一致的数学时这是一个真正的问题。本质上,通过 LFV,我想我想要一个 shorthand 作为最大显式值和无穷大 LFV = (LEV,infinity) 之间的开区间,但也许这种直觉是错误的。

更新:在评论中,人们似乎有点反对我提出的问题的实用性。出现我的问题不是因为发生了许多相关问题,而是因为同一问题经常出现在许多不同的设置中。通过与做数据分析的人交谈,在 training/fitting 建模时,这常常足以导致运行时错误。问题基本上是为什么这不是由数字语言处理的。从评论中,我基本上收集到编写语言的人看不到以这种方式处理事情的效用。在我看来,当某些特定问题对于使用某种语言的人来说发生得足够频繁时,以一种有原则的方式处理这些异常可能是有意义的,这样每个用户都不必这样做。

所以...我很好奇并四处挖掘了一下。

正如我在评论中提到的那样,如果您考虑 exception status flags,IEEE 754 中就存在 "largest finite value" 类型。设置溢出标志的无穷大值对应于您建议的 LFV,不同之处在于该标志仅可在操作后读出,而不是作为值本身的一部分存储。这意味着您必须手动检查标志并在发生溢出时采取行动,而不是仅仅内置 LFV*0 = 0。

关于异常处理及其在编程语言中的支持,有一个非常有趣的 paper。引用:

The IEEE 754 model of setting a flag and returning an infinity or a quiet NaN assumes that the user tests the status frequently (or at least appropriately.) Diagnosing what the original problem was requires the user to check all results for exceptional values, which in turn assumes that they are percolated through all operations, so that erroneous data can be flagged. Given these assumptions, everything should work, but unfortunately they are not very realistic.

本文还哀叹对浮点异常处理的支持不佳,尤其是在 C99 和 Java 中(我相信大多数其他语言也不会更好)。鉴于尽管如此,没有重大努力来解决这个问题或创建更好的标准,对我来说似乎表明 IEEE 754 及其支持在某种意义上是 "good enough"(稍后会详细介绍)。


让我给出您的示例问题的解决方案来演示一些东西。我正在使用 numpy 的 seterr 使其在溢出时引发异常:

import numpy as np

def exp_then_mult_naive(a, b):
    err = np.seterr(all='ignore')
    x = np.exp(a) * b
    np.seterr(**err)
    return x

def exp_then_mult_check_zero(a, b):
    err = np.seterr(all='ignore', over='raise')
    try:
        x = np.exp(a)
        return x * b
    except FloatingPointError:
        if b == 0:
            return 0
        else:
            return exp_then_mult_naive(a, b)
    finally:
        np.seterr(**err)

def exp_then_mult_scaling(a, b):
    err = np.seterr(all='ignore', over='raise')
    e = np.exp(1)
    while abs(b) < 1:
        try:
            x = np.exp(a) * b
            break
        except FloatingPointError:
            a -= 1
            b *= e
    else:
        x = exp_then_mult_naive(a, b)
    np.seterr(**err)
    return x

large = np.float_(710)
tiny = np.float_(0.01)
zero = np.float_(0.0)

print('naive: e**710 * 0 = {}'.format(exp_then_mult_naive(large, zero)))
print('check zero: e**710 * 0 = {}'
    .format(exp_then_mult_check_zero(large, zero)))
print('check zero: e**710 * 0.01 = {}'
    .format(exp_then_mult_check_zero(large, tiny)))
print('scaling: e**710 * 0.01 = {}'.format(exp_then_mult_scaling(large, tiny)))

# output:
# naive: e**710 * 0 = nan
# check zero: e**710 * 0 = 0
# check zero: e**710 * 0.01 = inf
# scaling: e**710 * 0.01 = 2.233994766161711e+306
  • exp_then_mult_naive 做你所做的:将溢出的表达式乘以 0 得到 nan.
  • exp_then_mult_check_zero 捕获溢出并且 returns 0 如果第二个参数是 0,否则与原始版本相同(注意 inf * 0 == naninf * positive_value == inf)。如果有 LFV 常量,这是你能做的最好的事情。
  • exp_then_mult_scaling 使用有关问题的信息来获取其他两个无法处理的输入的结果:如果 b 很小,我们可以在递减 [=22= 的同时将其乘以 e ] 而不改变结果。因此,如果 np.exp(a) < np.infb >= 1 之前,则结果适合。 (我知道我可以一步检查它是否适合而不是使用循环,但现在写起来更容易。)

现在您遇到这样一种情况,即不需要 LFV 的解决方案能够为更多的输入对提供正确的结果。 LFV 在这里的唯一优势是使用更少的代码行,同时在特定情况下仍能给出正确的结果。

顺便说一下,我不确定 seterr 的线程安全性。因此,如果您在多个线程中使用它,每个线程中的设置都不同,请先测试一下,以免以后头疼。


红利事实:original standard 实际上规定您应该能够注册一个陷阱处理程序,该处理程序将在溢出时给出除以一个大数的操作结果(请参阅第 7.3 节)。这将允许您继续计算,只要您记住该值实际上要大得多。虽然我猜它在多线程环境下可能会成为 WTF 的雷区,但没关系,我并没有真正找到对它的支持。


回到上面的 "good enough" 点:在我看来,IEEE 754 被设计为通用格式,几乎可用于任何应用程序。当你说 "the same issue frequently arises in many different settings" 时,它(或至少是)显然不够频繁以证明夸大标准是合理的。

让我引用 Wikipedia article:

[...] the more esoteric features of the IEEE 754 standard discussed here, such as extended formats, NaN, infinities, subnormals etc. [...] are designed to give safe robust defaults for numerically unsophisticated programmers, in addition to supporting sophisticated numerical libraries by experts.

撇开这一点,在我看来,即使将 NaN 作为一个特殊值也是一个有点可疑的决定,添加 LFV 并不会让 "numerically unsophisticated" 变得更容易或更安全,并且不允许专家做他们不能做的任何事情。

我想底线是表示有理数很难。 IEEE 754 在简化许多应用程序方面做得非常好。如果你不是其中之一,最后你只能通过

来处理困难的事情
  • 如果可用,使用更高精度的浮点数(好吧,这个很简单),
  • 仔细选择执行顺序,以免一开始就溢出,
  • 如果您知道所有值都将非常大,请为所有值添加偏移量,
  • 使用不会溢出的任意精度表示(除非你 运行 内存不足),或者
  • 其他我现在想不起来的。