为什么这个结果比等效函数的另一个结果更准确

Why is this result more accurate than another result by equivalent functions

我有以下问题,为什么 myf(x) 给出的结果不如 myf2(x) 准确。这是我的 python 代码:

from math import e, log

def Q1():
    n = 15
    for i in range(1, n):
        #print(myf(10**(-i)))
        #print(myf2(10**(-i)))
    return

def myf(x):
    return ((e**x - 1)/x)

def myf2(x):
    return ((e**x - 1)/(log(e**x)))

这是 myf(x) 的输出:

1.0517091807564771
1.005016708416795
1.0005001667083846
1.000050001667141
1.000005000006965
1.0000004999621837
1.0000000494336803
0.999999993922529
1.000000082740371
1.000000082740371
1.000000082740371
1.000088900582341
0.9992007221626409
0.9992007221626409

myf2(x):

1.0517091807564762
1.0050167084168058
1.0005001667083415
1.0000500016667082
1.0000050000166667
1.0000005000001666
1.0000000500000017
1.000000005
1.0000000005
1.00000000005
1.000000000005
1.0000000000005
1.00000000000005
1.000000000000005

我相信这与python中的浮点数系统以及我的机器有关。欧拉数的自然对数生成的数字比作为整数的等效数字 x 具有更多位数的精度。

Why is this result more accurate than another result by equivalent functions

运气不好。这两个函数都不能可靠地传递 (17-n)/2 个数字。

结果的差异取决于 explog 的通用实现,但未指定语言。


对于给定的 x 作为 10 的负 n 次方,扩展数学结果为 1.(n zeroes)5(n-1 zeros)166666....

对于exp(x) - 1,由于exp(small_value)1.

的严格计算,n变得更大,这两个函数都失去了大约一半的意义

exp(x)-1 保留与典型浮点数一样长的数学值及其典型的 17 位精度数字。

n == 7, exp(x) = 1.00000010000000494...exp(x)-11.0000000494...e-07.

n = 8: 轮子掉了

n == 8, exp(x) = 1.00000000999999994...exp(x)-19.99999993922529029...e-09.

9.99999993922529029...e-091.000000005...e-08.

不够近

此时,两个函数都在 1.00000000x 处失去了精度。


n上升到16左右,然后一切都崩溃了

先从xlog(exp(x))的区别说起,因为其余的计算都是一样的

>>> for i in range(10):
...     x = 10**-i
...     y = exp(x)
...     print(x, log(y))
... 
1 1.0
0.1 0.10000000000000007
0.01 0.009999999999999893
0.001 0.001000000000000043
0.0001 0.00010000000000004326
9.999999999999999e-06 9.999999999902983e-06
1e-06 9.99999999962017e-07
1e-07 9.999999994336786e-08
1e-08 9.999999889225291e-09
1e-09 1.000000082240371e-09

如果您仔细观察,您可能会发现其中有错误。 = 0 时,没有错误。 = 1 时,它打印 0.1 for 和 0.10000000000000007 for log(y),仅在 16 位后错误。 到时间 = 9 时,log(y) 错误了 log().

中的一半数字

因为我们知道真正的答案是 (),我们可以很容易地计算出近似的相对误差是多少:

>>> for i in range(10):
...     x = 10**-i
...     y = exp(x)
...     z = log(y)
...     print(i, abs((x - z)/z))
... 
0 0.0
1 6.938893903907223e-16
2 1.0755285551056319e-14
3 4.293440603042413e-14
4 4.325966668215291e-13
5 9.701576564765975e-12
6 3.798286318045685e-11
7 5.663213319457187e-10
8 1.1077471033430869e-08
9 8.224036409872509e-08

每一步都会让我们失去一个数字的准确性! 为什么?

每个操作10**-iexp(x)log(y)只在结果中引入一个微小的相对误差,小于10−15.

假设 exp(x) 引入相对误差 ,返回数字 ⋅(1 + ) 而不是 (其中,毕竟,是一个无法用有限的数字串表示的超越数)。 我们知道 || < 10−15,但是当我们尝试计算 log(⋅(1 + )) 作为 log() = ?

我们可能希望得到 ⋅(1 + ) 其中 非常小。 但是 log(⋅(1 + )) = log() + log(1 + ) = + log(1 + ) = ⋅(1 + log(1 + )/), 所以 = log(1 + )/。 即使 很小,≈ 10 随着增加越来越接近零,所以误差 log(1 + )/ ≈ / 会随着增加而变得越来越糟,因为 1/ → ∞.

我们说对数函数在 1 附近是 病态的:如果你以接近 1 的输入的近似值来计算它,它可以将一个非常小的输入错误变成任意大的输出错误。 事实上,在 exp(x) 舍入为 1 并因此 log(y) returns 为零之前,您只能再执行几步正是。

这并不是因为浮点数有什么特别之处——任何一种近似值都会对 log 产生相同的效果! 函数的条件数是数学函数本身的属性,而不是浮点运算系统的。 如果输入来自物理测量,您可以 运行 解决同样的问题。


这与函数expm1log1p存在的原因有关。 虽然函数 log() 在 1 附近是病态的,但函数 log(1 + ) 不是,所以 log1p(y)log(1 + y) 计算它更准确。 类似地,exp(x) - 1 中的减法在 ≈ 1 时服从 catastrophic cancellation,因此 expm1(x) 计算 - 1 比评估 exp(x) - 1 更准确。

当然,

expm1log1pexplog 是不同的函数,但有时您可以根据它们重写子表达式以避免错误-条件域。 在这种情况下,例如,如果将 log() 重写为 log(1 + [ − 1]),并使用 expm1log1p 来计算它,往返通常是精确计算的:

>>> for i in range(10):
...     x = 10**-i
...     y = expm1(x)
...     z = log1p(y)
...     print(i, x, z, abs((x - z)/z))
... 
0 1 1.0 0.0
1 0.1 0.1 0.0
2 0.01 0.01 0.0
3 0.001 0.001 0.0
4 0.0001 0.0001 0.0
5 9.999999999999999e-06 9.999999999999999e-06 0.0
6 1e-06 1e-06 0.0
7 1e-07 1e-07 0.0
8 1e-08 1e-08 0.0
9 1e-09 1e-09 0.0

出于类似的原因,您可能希望将 (exp(x) - 1)/x 重写为 expm1(x)/x 如果你不这样做,那么当 exp(x) returns ⋅(1 + ) 而不是 时,你将结束与 (⋅(1 + ) − 1)/ = ( − 1 + )/ = ( − 1)⋅[1 + /( − 1)]/,这可能再次爆炸因为错误是 /( − 1) ≈ /.


但是,不是 只是运气好,第二个定义似乎产生了正确的结果! 发生这种情况是因为复合误差——/来自分子中的 exp(x) - 1,/ 来自分母中的 log(exp(x))——相互抵消。 第一个定义错误地计算了分子和准确地计算了分母,但第二个定义计算了它们 大致同样糟糕!

特别地,当 ≈ 0 时,我们有

log(⋅(1 + )) = + log(1 + ) ≈ +

⋅(1 + ) − 1 = + − 1 ≈ 1 + + − 1 = + .

注意在这两种情况下相同,因为使用exp(x)来近似是错误的。

您可以通过与 expm1(x)/x(这是一个保证相对误差较低的表达式,因为除法永远不会使错误变得更糟)进行比较来进行实验测试:

>>> for i in range(10):
...     x = 10**-i
...     u = (exp(x) - 1)/log(exp(x))
...     v = expm1(x)/x
...     print(u, v, abs((u - v)/v))
... 
1.718281828459045 1.718281828459045 0.0
1.0517091807564762 1.0517091807564762 0.0
1.0050167084168058 1.0050167084168058 0.0
1.0005001667083415 1.0005001667083417 2.2193360112628554e-16
1.0000500016667082 1.0000500016667084 2.220335028798222e-16
1.0000050000166667 1.0000050000166667 0.0
1.0000005000001666 1.0000005000001666 0.0
1.0000000500000017 1.0000000500000017 0.0
1.000000005 1.0000000050000002 2.2204460381480824e-16
1.0000000005 1.0000000005 0.0

分子和分母的这种近似 + 对于最接近零的情况最好,对于距离零最远的情况最差——但是随着离零越来越远,exp(x) - 1 中的误差放大(来自灾难性抵消)和log(exp(x))(来自 1 附近的对数病态)无论如何都会减少,所以答案仍然准确。


然而,第二个定义中的良性抵消仅在非常接近于零以致 exp(x) 被简单地四舍五入为 1 时才有效——此时,exp(x) - 1log(exp(x)) 都给出零,最终会尝试计算 0/0,这会产生 NaN 和浮点异常。

所以你应该在实践中使用expm1(x)/x,但是之外exp(x)的边缘情况是一个幸运的意外1,即使 (exp(x) - 1)/x 和(出于类似原因)expm1(x)/log(exp(x)) 都给出了错误的准确度,(exp(x) - 1)/log(exp(x)) 中的两个错误也给出了正确的准确度。