是否有任何“标准”方法来计算数值梯度?

时间:2016-08-09 15:05:57

标签: c++ math precision numerical-methods

我试图在c ++中计算平滑函数的数值梯度。并且参数值可以从零变化到非常大的数字(可能是1e10到1e20?)

我使用函数f(x,y)= 10 * x ^ 3 + y ^ 3作为测试平台,但我发现如果x或y太大,我就无法得到正确的梯度。

这是我计算graidient的代码:

#include <iostream>
#include <cmath>
#include <cassert>
using namespace std;
double f(double x, double y)
{
    // black box expensive function
    return 10 * pow(x, 3) + pow(y, 3);
}
int main()
{
    // double x = -5897182590.8347721;
    // double y = 269857217.0017581;
    double x = 1.13041e+19;
    double y = -5.49756e+14;
    const double epsi = 1e-4;

    double f1 = f(x, y);
    double f2 = f(x, y+epsi);
    double f3 = f(x, y-epsi);
    cout << f1 << endl;
    cout << f2 << endl;
    cout << f3 << endl;
    cout << f1 - f2 << endl; // 0
    cout << f2 - f3 << endl; // 0
    return 0;
}

如果我使用上面的代码计算渐变,渐变将为零!

testbench函数,10 * x ^ 3 + y ^ 3,只是一个演示,我需要解决的实际问题实际上是一个黑盒函数。

那么,有没有“标准”的方法来计算数值梯度?

6 个答案:

答案 0 :(得分:3)

首先,你应该使用中心差异方案,这更准确(通过取消泰勒发展的另一个术语)。

(f(x + h) - f(x - h)) / 2h

而不是

(f(x + h) - f(x)) / h

然后选择h是至关重要的,使用固定常数是你能做的最糟糕的事情。因为对于较小的xh会过大,因此近似公式不再适用,而对于较大的xh将会过小,导致严重截断错误。

更好的选择是取一个相对值h = x√ε,其中ε是机器epsilon(1 ulp),这给出了一个很好的权衡。

(f(x(1 + √ε)) - f(x(1 - √ε))) / 2x√ε

请注意,当x = 0时,相对值无法工作,您需要回退到常数。但是,没有什么可以告诉你使用哪个!

答案 1 :(得分:2)

您需要考虑所需的精确度。

乍一看,自|y| = 5.49756e14epsi = 1e-4以来,您需要至少⌈log2(5.49756e14)-log2(1e-4)⌉ = 63位有效精度(即用于编码数字位数的位数,也是被称为尾数的yy+epsi被认为是不同的。

双精度浮点格式只有53位有效位精度(假设它是8个字节)。因此,目前f1f2f3完全相同,因为yy+epsiy-epsi相等。

现在,让我们考虑一下限制:y = 1e20以及您的函数10x^3 + y^3的结果。我们暂时忽略x,所以让我们f = y^3。现在,我们可以计算f(y)f(y+epsi)所需的精度:f(y) = 1e60f(epsi) = 1e-12。这给出了⌈log2(1e60)-log2(1e-12)⌉ = 240位的最小有效位精度。

即使您使用long double类型,假设它是16字节,您的结果也不会有所不同:f1f2f3仍然相同,即使yy+epsi不会。

如果我们考虑x,则f的最大值为11e60x = y = 1e20)。因此,精度的上限是⌈log2(11e60)-log2(1e-12)⌉ = 243位,或至少31个字节。

解决问题的一种方法是使用另一种类型,可能是用作定点的bignum。

另一种方法是重新思考你的问题并以不同的方式处理它。最终,你想要的是f1 - f2。您可以尝试分解f(y+epsi)。同样,如果您忽略xf(y+epsi) = (y+epsi)^3 = y^3 + 3*y^2*epsi + 3*y*epsi^2 + epsi^3。所以f(y+epsi) - f(y) = 3*y^2*epsi + 3*y*epsi^2 + epsi^3

答案 2 :(得分:0)

唯一计算渐变的方法是微积分。

渐变是一个向量:

g(x, y) = Df/Dx i + Df/Dy j

其中(i,j)分别是x和y方向上的单位矢量。

近似导数的一种方法是一阶差分:

Df/Dx ~ (f(x2, y)-f(x1, y))/(x2-x1)

Df/Dy ~ (f(x, y2)-f(x, y1))/(y2-y1)

这看起来不像你在做什么。

您有一个封闭的表单表达式:

g(x, y) = 30*x^2 i + 3*y^2 j

您可以插入(x,y)的值,并在任何点精确计算渐变。将其与您的差异进行比较,看看您的近似效果如何。

如何以数字方式实施它是您的责任。 (10 ^ 19)^ 3 = 10 ^ 57,对吗?

您机器上的双倍大小是多少?它是64位IEEE双精度浮点数吗?

答案 3 :(得分:0)

使用

dx = (1+abs(x))*eps,   dfdx = (f(x+dx,y) - f(x,y)) / dx
dy = (1+abs(y))*eps,   dfdy = (f(x,y+dy) - f(x,y)) / dy

为大型参数获取有意义的步长。

使用eps = 1e-8表示单边差异公式,eps = 1e-5表示中心差异商。

探索自动差异化(参见autodiff.org),了解没有差异商数的衍生工具,因此数值误差要小得多。

答案 4 :(得分:0)

我们可以使用以下程序检查导数中误差的行为 - 它使用变化的步长计算单侧导数和基于中心差的导数。这里我使用的是x和y~10 ^ 10,它比你使用的要小,但是应该说明相同的观点。

#include <iostream>
#include <cmath>
#include <cassert>
using namespace std;
double f(double x, double y) {
    return 10 * pow(x, 3) + pow(y, 3);
}

double f_x(double x, double y) {
    return 3 * 10 * pow(x,2);
}

double f_y(double x, double y) {
    return 3 * pow(y,2);
}

int main()
{
    // double x = -5897182590.8347721;
    // double y = 269857217.0017581;
    double x = 1.13041e+10;
    double y = -5.49756e+10;
    //double x = 10.1;
    //double y = -5.2;

    double epsi = 1e8;
    for(int i=0; i<60; ++i) {
    double dfx_n = (f(x+epsi,y) - f(x,y))/epsi;
    double dfx_cd = (f(x+epsi,y) - f(x-epsi,y))/(2*epsi);
    double dfx = f_x(x,y);
    cout<<epsi<<" "<<fabs(dfx-dfx_n)<<" "<<fabs(dfx - dfx_cd)<<std::endl;
    epsi/=1.5;
    }
    return 0;
}

输出显示单侧差异使我们在步长约100.0时获得约1.37034e+13的最佳误差。请注意,虽然此错误看起来很大,但相对错误为3.5746632302764072e-09(因为确切的值为3.833e+21

相比之下,双边差异得到约1.89493e+10的最佳误差,步长约为45109.3。这比三个数量级更好(步长更大)。

我们如何计算步长? Yves Daosts评论中的链接给了我们一个大概的价值:

h=x_c sqrt(eps)表示单面,h=x_c cbrt(eps)表示双面。

但无论如何,如果在x~10 ^ 10处所需的步长为100.0,则x~10 ^ 20所需的步长也将大10 ^ 10。所以问题只是你的步长方式太小了。

这可以通过增加上面代码中的起始步长并将x / y值重置为原始值来验证。

然后预期导数为O(1e39),在O(1e31)的步长附近发生约5.9e10的最佳单侧误差,出现约O(1e29)的最佳双侧误差步长为6.1e13

答案 5 :(得分:0)

由于数值差异是病态的(这意味着一个小错误可能会显着改变你的结果),你应该考虑使用Cauchy's integral formula。这样你就可以用积分计算n次导数。考虑到准确性和稳定性,这将导致更少的问题。