是否可以使此表达式更准确并且不返回负值?

Can this expression be made more accurate and not return negative values?

提问人:aquaticapetheory 提问时间:2/25/2023 最后编辑:aquaticapetheory 更新时间:3/9/2023 访问量:88

问:

我正在使用 Julia,但我相信解决方案将主要跨不同语言进行翻译。

我想精确计算以下表达式,以获得 $T$ 的正值。Float64

enter image description here

a是一个正常数。平方根内的表达式应始终为正数,但当接近零时,由于数值精度问题,结果偶尔会变为负数。T

julia> α=1/4.0; map( T -> ( 4 * exp(-α * T) - 3 - exp( -2α * T ) + 2α * T ), 10.0.^(-10:0) )
11-element Vector{Float64}:
 -4.137018548132909e-18
 -4.137018546840439e-17
  3.038735495922355e-17
 -2.512379594116203e-16
  4.113329634720811e-17
  1.8928899382176026e-16
  1.0552627836227929e-14
  1.041483522687403e-11
  1.0397158019953556e-8
  1.022361261644733e-5
  0.00867247257298609

使用 s 进行计算时不会遇到相同的问题(对于相同的值)。BigFloat

julia> α=BigFloat(1/4.0); map( T -> ( 4 * exp(-α * T) - 3 - exp( -2α * T ) + 2α * T ), 10.0.^(-10:0) )
11-element Vector{BigFloat}:
 1.041666666647135530517511139334179132983501942809441564565600382453160881163658e-32
 1.041666666471354361319426381902535606415006422809427598976663299435084708258933e-29
 1.041666664713541734328314928659467024540737963331449636847040682408936614887116e-26
 1.041666647135417166709396893449432138811473447225776037995767356684771923939405e-23
 1.041666471354189311710850303868877685043857627660615702953528423064833434922657e-20
 1.04166471354394556609940068924547895770393434711169898595204995967864381931136e-17
 1.041647135644529365261489400892467181239989440055364005317784883808499181692089e-14
 1.041471376951090709983860760560291162481306334994613272150860749219930444115473e-11
 1.039715818279496102769801850953898783936741961589734116598630369857383947507474e-08
 1.022361261666541821235659358423414459600515733306470783618814653113646037198644e-05
 0.008672472572986049376881532922102135745171026217378941282377306337925928800328869

有没有办法修改这个计算,以提高精度,并在仍然使用s的同时防止这个问题?Float64

精密 数值方法

评论

1赞 Raymond Chen 2/25/2023
这是数值分析。将根式下的表达式展开为零附近的泰勒级数,丢弃二次项,并进行化简。如果它崩溃了,则保留二次项并丢弃三次。

答:

2赞 Lutz Lehmann 2/25/2023 #1

4*z - 3 - z^2 = (3-z)*(z-1)

和使用

e^(-aT)-1 = expm1(-aT)

它以完全相对精度计算此因子为 。-aT*(1+O(aT))

使用 gnuplot 可视化差异:

enter image description here

评论

0赞 Lutz Lehmann 2/25/2023
是的,这是因为IEEE实现不能保证数值二阶导数,只能保证零的一阶导数。也可以将表达式分解为,实现中的二次(也是高阶)多项式系数与泰勒系数不同,因为优化是针对区间内的小误差,而不是零。因此,从这个意义上说,切换到项或整体泰勒展开是必要的,以保持尽可能充分的精度接近于零。-(exp(-x)-1)^2+2(exp(-x)-1+x)exp
2赞 njuffa 2/25/2023 #2

原始函数出现问题的根本原因是减法抵消,即对两个大小相似的量进行有效减法。关于如何处理这种情况,有一些一般的启发式方法:(1)对于涉及接近零的计算,改用。(2) 将减法转换为除法或乘法 (3) 在发生减法抵消的地方使用融合乘加法,前提是所涉及的两个操作数之一是一个乘积。这之所以有效,是因为 FMA 将双倍宽度未舍入乘积合并到总和中。exp()expm1()

在提出合适的缓解重新排列后,我注意到精度损失持续存在,小于 2-6 小时。对于非常接近于零的参数,此类问题的标准方法是尝试泰勒级数展开。我使用配置为 130 位十进制数字的多精度库,以数值方式计算了泰勒展开的前六项。更快的替代方案是使用 Wolfram Alpha。a * T

我应该指出,我以行人的方式处理了减少错误的替换。Lutz Lehmann 找到了一种更优雅、更高效且不需要 FMA 的重新排列;看他的回答

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define USE_NJ (1)
#define FUNC   accurate_func

double accurate_func (double a, double T)
{
    double r, u = a * T;
    // use Taylor series expansion near zero
    if (u <= 0.015625) {  // 1/64
        double u3 = u * u * u;
        double p =  -1.0 / 160.0;
        p = p * u + 31.0 / 1260.0; 
        p = p * u -  1.0 / 12.0;
        p = p * u +  7.0 / 30.0;
        p = p * u -  1.0 / 2.0;
        p = p * u +  2.0 / 3.0;
        r = p * u3;
    } else {
#if USE_NJ // use exmp1() and fma() to mitigate subtractive cancellation
        double v = expm1 (u);
        double w = exp (-2 * u);
        r = fma (-3 * v * v, w, 2 * fma (-v, w, u));
#else // Lutz Lehmann's elegant arrangement
        double s = expm1 (-u);
        r = (2.0 - s) * s + 2 * u;
#endif
    }
    return sqrt (r);
}

double original_func (double a, double T)
{
    double u = a * T;
    return sqrt (4* exp (-u) - 3 - exp (-2 * u) + 2*u);
}

int main (void)
{
    double mpref [13] = {
        5.7981245368580744e-1, // 2^0
        2.4133627509989329e-1, // 2^-1
        9.3126111123497739e-2, // 2^-2
        3.4450359460776940e-2, // 2^-3
        1.2463894628395316e-2, // 2^-4
        4.4581489320727356e-3, // 2^-5
        1.5854164404162396e-3, // 2^-6
        5.6217040623145288e-4, // 2^-7
        1.9904830179668834e-4, // 2^-8
        7.0425736778377448e-5, // 2^-9
        2.4908375624050728e-5, // 2^-10
        8.8080530945851612e-6, // 2^-11
        3.1144021359546032e-6, // 2^-12
    };

    double mpref2 [13] = {
        5.7981245368580744e-1, // 2^0
        1.2341091904923634e+0, // 2^1
        2.2523159398554711e+0, // 2^2
        3.6057373362429543e+0, // 2^3
        5.3851648489290173e+0, // 2^4
        7.8102496759066576e+0, // 2^5
        1.1180339887498948e+1, // 2^6
        1.5905973720586866e+1, // 2^7
    };

    int i = 0;
    double a = 1.0;
    do {
        double T = 1.0 / (1 << i);
        printf ("i=%2d a*T=%23.16e  func=%23.16e  relerrfunc=% 15.8e\n", 
                i, a*T, FUNC(a,T), (FUNC(a,T) - mpref[i]) / mpref[i]);
        i++;
    } while (i < 13);
    printf ("\n");
    i = 0;
    do {
        double T = 1.0 * (1 << i);
        printf ("i=%2d a*T=%23.16e  func=%23.16e  relerrfunc=% 15.8e\n", 
                i, a*T, FUNC(a,T), (FUNC(a,T) - mpref2[i]) / mpref2[i]);
        i++;
    } while (i < 8);

    return EXIT_SUCCESS;
}
0赞 phipsgabler 3/9/2023 #3

我只是一个随机的 Julia 人,不是数字学家,但我想贡献 Herbie,一个自动提高浮点精度的工具,给你的东西:

function f(a, T)
    t1 = Float64((T ^ 4.0) * -0.5)
    t2 = a ^ 4.0
    t3 = Float64(Float64(T * -2.0) + Float64(T * 2.0))
    t4 = a ^ 3.0
    t5 = Float64((T ^ 3.0) * 0.6666666666666666)
    t6 = log(exp(Float64(0.23333333333333334 * (Float64(T * a) ^ 5.0))))

    u1 = fma(t4, t5, t6)
    u2 = fma(t3, a, u1)
    return fma(t1, t2, u2)
end

(赫比可以直接生成茱莉亚;我只是将输出从单行重新格式化为更可分析的内容。

它甚至可以为你生成一个证明,证明为什么转换是正确的。