我试图在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,只是一个演示,我需要解决的实际问题实际上是一个黑盒函数。
那么,有没有“标准”的方法来计算数值梯度?
答案 0 :(得分:3)
首先,你应该使用中心差异方案,这更准确(通过取消泰勒发展的另一个术语)。
(f(x + h) - f(x - h)) / 2h
而不是
(f(x + h) - f(x)) / h
然后选择h
是至关重要的,使用固定常数是你能做的最糟糕的事情。因为对于较小的x
,h
会过大,因此近似公式不再适用,而对于较大的x
,h
将会过小,导致严重截断错误。
更好的选择是取一个相对值h = x√ε
,其中ε
是机器epsilon(1 ulp),这给出了一个很好的权衡。
(f(x(1 + √ε)) - f(x(1 - √ε))) / 2x√ε
请注意,当x = 0
时,相对值无法工作,您需要回退到常数。但是,没有什么可以告诉你使用哪个!
答案 1 :(得分:2)
您需要考虑所需的精确度。
乍一看,自|y| = 5.49756e14
和epsi = 1e-4
以来,您需要至少⌈log2(5.49756e14)-log2(1e-4)⌉ = 63
位有效精度(即用于编码数字位数的位数,也是被称为尾数的y
和y+epsi
被认为是不同的。
双精度浮点格式只有53位有效位精度(假设它是8个字节)。因此,目前f1
,f2
和f3
完全相同,因为y
,y+epsi
和y-epsi
相等。
现在,让我们考虑一下限制:y = 1e20
以及您的函数10x^3 + y^3
的结果。我们暂时忽略x
,所以让我们f = y^3
。现在,我们可以计算f(y)
和f(y+epsi)
所需的精度:f(y) = 1e60
和f(epsi) = 1e-12
。这给出了⌈log2(1e60)-log2(1e-12)⌉ = 240
位的最小有效位精度。
即使您使用long double
类型,假设它是16字节,您的结果也不会有所不同:f1
,f2
和f3
仍然相同,即使y
和y+epsi
不会。
如果我们考虑x
,则f
的最大值为11e60
(x = y = 1e20
)。因此,精度的上限是⌈log2(11e60)-log2(1e-12)⌉ = 243
位,或至少31个字节。
解决问题的一种方法是使用另一种类型,可能是用作定点的bignum。
另一种方法是重新思考你的问题并以不同的方式处理它。最终,你想要的是f1 - f2
。您可以尝试分解f(y+epsi)
。同样,如果您忽略x
,f(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次导数。考虑到准确性和稳定性,这将导致更少的问题。