问题动机:
在我知道的标准数值语言中(例如Matlab,Python numpy等),例如,如果您采用适度大的指数,则由于数值溢出而导致输出为无穷大。如果将其乘以0,将得到NaN。另外,这些步骤足够合理,但是它们揭示了数学实现中的逻辑错误。已知由溢出产生的第一个数字是有限的,我们显然希望将这个大的有限数字乘以0的结果为0。
明确地:
>>> import numpy as np
>>> np.exp(709)*0
0.0
>>> np.exp(710)*0
nan
我想我们可以在这里引入“最大有限值”(LFV)的概念,该概念具有以下特性:
向上舍入到无穷大
另一方面,不能简单地按照为LFV所述的方式重新定义无穷大。适本地,对于0 * infinity = 0 ...没有意义,当前的标准无穷大实现在此设置下会产生NaN。另外,有时需要将数字初始化为无穷大,并且您想要任何数值运算的结果,即使是产生LFV的结果严格小于初始化值(这对于某些逻辑语句也很方便)。我确定存在其他情况,其中需要适当的无穷大-我的观点只是无穷大,应该简单地将而不是重新定义为具有上面的某些LFV属性。
问题:
我想知道是否有使用这种方案的语言,以及这种方案是否存在问题。在适当的数学中不会出现此问题,因为对数字大小没有这些数值限制,但是我认为在用编程语言实现一致的数学时这是一个真正的问题。本质上,通过LFV,我想我想为最大显式值和无穷大之间的开放时间间隔简写LFV =(LEV,infinity),但也许这种直觉是错误的。
更新:在评论中,人们似乎有点反对我提出的问题的实用性。出现我的问题并不是因为发生了许多相关的问题,而是因为同一问题经常在许多不同的环境中出现。从与进行数据分析的人员交谈开始,这通常足以在训练/拟合模型时导致运行时错误。基本上,问题在于为什么数字语言无法处理此问题。从评论中,我实际上是在收集那些写语言的人没有看到以这种方式处理事物的实用性的信息。以我的观点,当某些特定问题频繁发生,对于使用某种语言的人来说,以一种有原则的方式处理这些异常可能是有意义的,因此每个用户都不必这样做。
最佳答案
所以...我好奇了一下。
正如我在评论中已经提到的,如果考虑到exception status flags,则在IEEE 754中存在“最大有限值”类型。设置了溢出标志的无穷大值对应于您建议的LFV,不同之处在于该标志仅可在操作后读取,而不是作为值本身的一部分存储。这意味着您必须手动检查该标志并在发生溢出时采取措施,而不仅仅是内置LFV * 0 = 0。
关于异常处理及其在编程语言中的支持,有一个非常有趣的paper。引用:
这篇文章还哀叹对浮点异常处理的支持不佳,尤其是在C99和Java中(我相信大多数其他语言都不会更好)。鉴于此,尽管没有做出任何努力来解决此问题或创建更好的标准,但在我看来,IEEE 754及其支持在某种意义上说是“足够好”(稍后会详细介绍)。
让我为您的示例问题提供解决方案以进行演示。我正在使用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
捕获溢出,如果第二个参数是0
,则返回0
,否则与朴素的版本相同(请注意inf * 0 == nan
而inf * positive_value == inf
)。如果存在LFV常数,这是您可以做的最好的事情。 exp_then_mult_scaling
使用有关问题的信息来获取其他两个无法处理的输入的结果:如果b
很小,我们可以在将a
递减的同时将其乘以e,而不会更改结果。因此,如果np.exp(a) < np.inf
在b >= 1
之前,则结果合适。 (我知道我可以检查它是否适合一步而不是使用循环,但这现在更容易编写。)因此,现在您遇到的情况是,不需要LFV的解决方案就可以为更多的输入对提供正确的结果,而不是提供更多的输入对。 LFV在此具有的唯一优势是使用更少的代码行,同时在那种特定情况下仍能给出正确的结果。
顺便说一下,我不确定
seterr
的线程安全性。因此,如果您要在多个线程中使用它,并且每个线程中的设置都不同,请先进行测试,以免日后头痛。奖励事实:original standard实际上规定,您应该能够注册一个陷阱处理程序,该处理器在溢出时将被赋予操作结果除以大数的结果(请参见7.3节)。只要记住该值实际上要大得多,就可以进行计算。尽管我猜想它可能会在多线程环境中成为WTF的雷区,但不要介意我并没有真正找到对此的支持。
回到上面的“足够好”的观点:据我了解,IEEE 754被设计为通用格式,几乎可用于任何应用。当您说“同一问题经常发生在许多不同的环境中”时,显然(或者至少是)没有足够的时间来证明夸大标准是合理的。
让我引用Wikipedia article:
在我看来,抛开这一点,即使将NaN作为特殊值也是一个可疑的决定,添加LFV并不能真正使“数字简单”变得更容易或更安全,并且不允许专家做他们做不到的任何事情。
我想说的底线是很难表示有理数。 IEEE 754在简化许多应用程序方面做得很好。如果您不是其中之一,那么最后您只需要解决其中一个难题就可以了
关于python - 数值编程语言可以区分 "largest finite number"和 "infinity"吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/29859509/