无法证明(或找到反例)对于浮点数值,tan(x)等于无穷大。

6
C语言中的tan函数族(tan,tanf,tanl)的POSIX页面指出:
如果正确的值会导致溢出,将发生范围错误,并且tan(),tanf()和tanl()将返回±HUGE_VAL,±HUGE_VALF和±HUGE_VALL,其符号与函数的正确值相同。
然而,在实践中,要在这种情况下实际获得+∞或−∞作为计算结果是非常困难的,因为浮点数需要非常接近奇数倍的π/2,其切线大于该类型可表示的最大浮点数值。由于可表示值的间隔,我们无法总是接近所需的特定实数。
经验上,我无法通过标准的glibc获得这样的结果,即使使用"nextafter"函数来获取最接近π/2的值(使用"atan(1) * 2"作为"半π",然后从左边或右边进行计算)。
对所有(32位)浮点数进行详尽测试证实了在32位中这是正确的,至少在我的库中是如此。对于双精度和长双精度数的π/2附近进行测试也表明它们也是如此。然而,详尽测试需要太长时间:我需要测试(2k+1)•π/2的附近,其中k ∈ ℤ,使得(2k+1)•π/2在浮点有限范围内。
那么,是否有一些数学或实际的论据可以得出这样的结论:至少在“相当准确”的库实现中(例如,使用一些测量的ULP边界,就像GNU C库数学函数所做的那样),浮点数值的精度总是能够使得tan(x)适应这些值的有限表示?换句话说,tan是否不会比我们接近π/2更快地趋向无穷大?
请注意,我在讨论中排除了tan(NAN)tan(INFINITY),因为这些都是已记录的特殊情况,它们返回NaN。此外,可能可以通过非规范化数获得不同的结果,但由于它们只在接近零而不是接近π/2时发生,我认为我们可以将其排除在外。
因此,我正在寻找一些数学论证/证明/详尽测试,以显示这种情况不会发生,或者只是一个反例,其中包括任何标准的C库实现,包括并调用tan函数;但是排除具有非标准tan函数的特定库。

请问您为什么需要这样的证明? - n. m.
1
它允许指定 tan 函数的简化合同,例如:“如果 x 是有限的,则 tan(x) 也是有限的”。 - anol
2
请参阅2005年Damien Stehlé、Vincent Lefèvre和Paul Zimmerman的论文《使用格点约简搜索单变量函数的最坏情况》(https://www.vinc17.net/research/papers/ieeetc2005-wclr.pdf)或他们2002年的论文《最坏情况和格点约简》(https://hal.inria.fr/inria-00071999/document)。我在实现三角函数的过程中进行了分析。从记忆中得知,对于IEEE-754二进制64位,没有任何`tan`值足够大到会溢出。 - Eric Postpischil
这很简单,但它也不提供实现的自由,并且难以验证超过32位的任何内容。 - n. m.
此外,Jean-Michel Muller在《初等函数:算法与实现》(目前在Amazon.com上作为电子书出售)中展示了用于搜索的Maple代码。他指出,在IEEE-754 binary64中,π/2的倍数的最坏情况是6381956970095103•2^797。 - Eric Postpischil
确实,@Eric,我修正了公式,谢谢。 - anol
3个回答

5
这里有一个实际演示,证明对于任何有限的IEEE 754 binary64 double类型的x,正确舍入的 tan(x) 永远不会是无穷大,事实上它永远不会超过 2.2e+18。相同的方法也适用于64位精度的long double类型,但是搜索需要更长时间。甚至可以对IEEE 754 binary128和binary256浮点格式进行处理。超出这个范围后,启发式论据表明对于任何IEEE 754二进制浮点格式,tan(x)都不可能溢出,但我不知道是否有任何证明,而且我认为不存在已知的证明。
显然,只考虑正有限浮点数就足够了(此处和以下中,“float”指的是IEEE 754 binary64浮点值)。每个这样的 float 都具有形式 m * 2^e ,其中m和e是一些整数,满足 0
因此,我们要找到整数n,m和e,使绝对值 |nπ/2-m2^e| 最小化,同时满足约束条件 0
对于任何特定固定值的 e,我们的问题等价于找到最小化绝对值 |n-m2^(e+1)/π| 的正整数 m 和 n ,其中 m<2^53。如果我们能高效地解决这个问题,我们就可以在1025个不同的 e 值上迭代,提取出总体最佳解决方案。
但是,对于固定的指数e的子问题可以通过找到2^(e+1) / π的连分数拓展来解决:标准的数论结果表明,|n - m 2^(e+1) / π|的最小值保证来自于收敛或半收敛的n / m2^(e+1) / π。我们所要做的就是找到最佳的收敛或半收敛,其分母严格小于2^53。实际上,连分数拓展允许我们找到既是最佳收敛或半收敛(受制于分母小于2^53的限制),而且比|2^(e+1) / π|严格更大,也找到了最佳收敛或半收敛(具有类似限制的分母),其比|2^(e+1) / π|严格更小,然后需要进行比较以确定哪一个收敛更接近。

下面是一些Python代码,它实现了上述内容,并最终找到了评论中已经提到的相同最佳逼近。首先,我们需要一些稍后会用到的导入:

from fractions import Fraction
from math import floor, ldexp

我们需要一个近似值来表示π/2。实际上,我们需要一对近似值:一个下限和一个上限。我们将使用前1000位π的数字,因为它们有良好的记录,并且在网上易于找到(尽管实际上我们只需要约330个数字)。使用这些数字,我们创建Fraction实例HALF_PI_LOWERHALF_PI_UPPER,分别代表π/2的紧密下限和上限:

# Approximation to π, given by the first 1000 digits after the point. (source:
# https://mathshistory.st-andrews.ac.uk/HistTopics/1000_places/)
PI_APPROX = Fraction("".join("""
3.14159265358979323846264338327950288419716939937510
  58209749445923078164062862089986280348253421170679
  82148086513282306647093844609550582231725359408128
  48111745028410270193852110555964462294895493038196
  44288109756659334461284756482337867831652712019091
  45648566923460348610454326648213393607260249141273
  72458700660631558817488152092096282925409171536436
  78925903600113305305488204665213841469519415116094
  33057270365759591953092186117381932611793105118548
  07446237996274956735188575272489122793818301194912
  98336733624406566430860213949463952247371907021798
  60943702770539217176293176752384674818467669405132
  00056812714526356082778577134275778960917363717872
  14684409012249534301465495853710507922796892589235
  42019956112129021960864034418159813629774771309960
  51870721134999999837297804995105973173281609631859
  50244594553469083026425223082533446850352619311881
  71010003137838752886587533208381420617177669147303
  59825349042875546873115956286388235378759375195778
  18577805321712268066130019278766111959092164201989
""".split()))

# True value of π is within ERROR of the value given above.
ERROR = Fraction("1e-1000")

HALF_PI_LOWER = (PI_APPROX - ERROR) / 2
HALF_PI_UPPER = (PI_APPROX + ERROR) / 2

现在我们来讲一下连分数机制。首先,我们提供了一个生成器函数,当提供该值的下限和上限时,它会生成该值的连分数系数序列。基本上,这个函数会分别计算下限和上限的系数,而只要这些系数匹配,它们就会被返回给调用者。一旦它们不匹配,我们就无法确定下一个系数的值,此时该函数将引发一个异常(可能是RuntimeErrorZeroDivisionError)。我们并不太关心引发哪种异常:我们依赖于我们最初的近似值足够准确,从而避免了遇到该异常的情况。

def continued_fraction_coefficients(lower: Fraction, upper: Fraction):
    """
    Generate continued fraction coefficients for a positive number.

    Given rational lower and upper bounds for a postive rational or irrational
    number, generate the known coefficients of the continued fraction
    representation of that number. The tighter the lower and upper bounds, the
    more coefficients will be known.

    Raises RuntimeError or ZeroDivisionError when there's insufficient
    information to determine the next coefficient.
    """
    while (q := floor(lower)) == floor(upper):
        yield q
        lower, upper = 1/(upper - q), 1/(lower - q)
    raise RuntimeError("Insufficient resolution")

接下来是一个函数,它使用这个连分数系数序列来产生对应值的最佳下限和最佳上限近似值(对于给定的分母限制)。它从连分数序列中生成连分数逼近,超过分母限制后回溯寻找符合分母限制的最佳边界。
def best_bounds(coefficients, max_denominator):
    """
    Find best lower and upper bounds under a given denominator bound.

    *coefficients* is the sequence of continued fraction coefficients
    for the value we're approximating.

    *max_denominator* is the limit on the denominator.

    Returns the best lower approximation and the best upper approximation
    to the value being approximated, for the given limit on the denominator.
    """
    # a/b and c/d are the closest convergents so far
    # sign is 1 if a/b < value < c/d, and -1 otherwise.

    a, b, c, d, sign = 0, 1, 1, 0, 1
    for q in coefficients:
        a, b, c, d, sign = c, d, a + q * c, b + q * d, -sign
        if max_denominator < d:
            correction = (max_denominator - d) // b
            c, d = c + correction * a, d + correction * b
            break

    if sign == 1:
        return Fraction(a, b), Fraction(c, d)
    else:
        return Fraction(c, d), Fraction(a, b)

现在我们编写一个专用函数,对于给定的指数e,在所有nmm < 2^53的情况下,最小化|n π/2 - m 2^e|。该函数返回两个三元组(x, n, error),其中x是浮点值m 2^e,表示为Python中的floaterror给出了近似值的误差,也被转换为浮点型。(因此,error值并不完全准确,但它们足够接近,以便我们找到最佳的整体近似值。)这两个三元组分别表示最佳下限和上限。
def best_for_given_exponent(e):
    """
    Find best lower and upper approximations for 2^e / (π/2)
    with denominator smaller than 2**53.

    Returns two triples of the form (x, n, error), where:

    - x is a float close to an integer multiple of π/2
    - n is the coefficient of that integer multiple of π/2
    - error is the absolute error |x - n π/2|, rounded to the nearest float.

    The first triple gives a best lower approximation for a multiple of π/2,
    while the second triple gives a best upper approximation.
    """
    twopow = Fraction(2)**e
    coefficients = continued_fraction_coefficients(twopow/HALF_PI_UPPER, twopow/HALF_PI_LOWER)
    best_lower, best_upper = best_bounds(coefficients, max_denominator=2**53 - 1)

    n, d = best_lower.as_integer_ratio()
    lower_result = ldexp(d, e), n, float(d * twopow - n * HALF_PI_LOWER)

    n, d = best_upper.as_integer_ratio()
    upper_result = ldexp(d, e), n, float(n * HALF_PI_UPPER - d * twopow)

    return lower_result, upper_result

最后,我们将所有内容综合起来,找到一个最佳的binary64逼近值,使其为奇数个π/2的倍数。
all_results = [
    result for e in range(-53, 972)
    for result in best_for_given_exponent(e)
]
x, n, error = min(all_results, key=lambda result: result[2])

print(f"Best approximation: {x.hex()} is an approximation to {n}*π/2 with approximate error {error}")

当我在我的电脑上运行这个程序时,大约经过5秒钟的计算,它会输出以下内容:
Best approximation: 0x1.6ac5b262ca1ffp+849 is an approximation to 3386417804515981120643892082331156599120239393299838035242121518428537554064774221620930267583474709602068045686026362989271814411863708499869721322715946622634302011697632972907922558892710830616034038541342154669787134871905353772776431251615694251273653*π/2 with approximate error 4.687165924254628e-19

因此,最糟糕的情况是浮点数 0x1.6ac5b262ca1ffp+849,它匹配了埃里克•波斯特皮斯希尔在原问题的评论中提到的值6381956970095103•2^797

>>> float.fromhex('0x1.6ac5b262ca1ffp+849') == ldexp(6381956970095103, 797)
True

该值的正切约为±1/4.687165924254628e-19,或2.133485385753704e+18,事实上,这就是我计算机上数学库所产生的值:

>>> from math import tan
>>> tan(float.fromhex('0x1.6ac5b262ca1ffp+849'))
-2.133485385753704e+18

因此,没有任何有限的二进制64位浮点数其正切值超过2.133485385753704e+18

4

是否有数学或实际的论据可以得出结论,即浮点值的精度将始终使 tan(x) 适合这些值的有限表示形式?

π是一个无理数,some_odd_integer*π/2也是 - 这是数学正切的极点。

所有有限的浮点值都是有理数。因此,没有任何一个double是π/2的整数倍,tan(some_finite_double)从不是极点。

some_odd_n*π/2可能非常接近像6381956970095103 ∗ 2797这样的double a。当这是真的时,tan(a)就很大。

这与K.C. Ng的“巨大参数的参数缩减:最后一位很好”第2.3章非常接近。 这大致意味着对于所有double,在some_odd_n * π / 2上偏移约2-62的最接近情况差异。 在some_odd_n * π / 2上偏移2-62的值a会导致tan()约为262,远远在double范围内的21023之内。
这个关键点是,对于某些FP类型而言,要使tan(a)超过FP_MAX,a必须非常接近于some_odd_n*π/2,大约是log2(FP_MAX)的数量级。随着位宽度的增加,精度线性增加,范围指数增加,更宽的FP类型不太可能发生这种情况。只有比binary16更窄的编造的FP类型才有可能出现这种情况。
注意:tan() 的第一步是将参数缩减到主范围[-π/2...π/2]。在我看来,这比主要的tan()评估更难做好。因此,tan()实现的质量(或不足)可能会导致接近some_odd_n*π/2的值产生明显更大(且不正确)的结果。
我正在寻找数学证明/证据/详尽测试,以表明这种情况不会发生。
参见上述文章中与π相关的连分数(参考文献6),了解关于如何确定最坏情况的想法。

1
另请参见:Roger Alan Smith,“三角函数参数约减的连分数分析”,《IEEE计算机学报》,1995年11月,第44卷,第1348-1351页。 “我们提出了一种使用连分数确定所有以IEEE浮点格式表示的数字x的简单方法,该方法可以确定出在最高位无关紧要的数字x。” - njuffa
1
还基于连分数的使用,W. Kahan通过S. McDonald的方式提出了这个链接中的“Nearpi”,一个C程序,用来展示非常接近$\pi/2$的整数倍的大型浮点数$Z = m \cdot 2^L$。 - njuffa

1

浮点数的精度由IEEE规定,并且只要使用IEEE浮点数,它将始终保持不变。特别地,最接近π/2的32位IEEE数字为1.57079637050628662109375(上方)和1.5707962512969970703125(下方)。这些值是精确的。在这两个数字处的正切的数学正确值并不特别大,约为-2.2E7和1.3E7,远离32位二进制数的IEEE-754极限约为3E38。

然而,这并不能说明tan在大参数下的行为。首先,绝对没有保证库函数将返回最接近数学正确值的结果(尤其是对于大参数)。其次,对于某个奇数N,N*π/2的表示可能足够接近真实的数学值,以产生一个超出可表示范围的tan值。这两个观察结果的结合意味着需要详尽测试tan的特定实现,以涵盖所有感兴趣的值。


“exhaustively test… for all values of interest”这句话有点不清楚。当然,没有必要对所有可表示的值进行详尽测试;有数论技巧可以确定每个二进制区间(指数区间)中最接近的值,因此只需要测试这些值。另一方面,确实需要测试或排除所有大于1的值(对于较低指数,参数小于π/2)。这给出了2045个“感兴趣”的值(对于IEEE-754 二进制64,指数为2到2046)。我不会将对近2^64个值中的2045个值进行测试描述为“详尽”。 - Eric Postpischil
关于“浮点数的精度由IEEE规定”的问题:IEEE-754规定了某些基本格式,并允许无限多的其他格式。 - Eric Postpischil
通常,对于使用这些函数实现的人来说,一些实现的属性是已知的,例如它具有某些已知的错误界限,因此不是任何参数都可能溢出。如果误差界限是这样的,即最靠近奇数倍 π/2 的可表示值即使具有最大误差也不会溢出,那么没有其他可表示值会溢出。当然,这需要白盒测试而不是黑盒测试。 - Eric Postpischil
@n. m. 可能是一个人工智能,"绝对没有保证库函数会返回最接近数学正确值的结果" 是一个非常好的观点。对于某些 x 值,不太理想的 tan(x) 可能会返回无穷大。OP 的 <至少在 "相当正确" 的库实现中> 排除了这种情况,但你的观点在一般情况下是有效的,因为在实际应用中,tan(x) 可能会返回 巨大 的值。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接