这篇文章翻译自官方教程Numeric derivatives并且参考了少年的此间的博客文章Ceres-Solver学习笔记(5)
利用analytic derivatives的另一个极端形式是 numeric derivatives,即数值微分法。数值微分法的关键是,目标函数 f(x) f ( x ) 的微分方程可以被写成一个极限形式:
当然,在计算机上 h h 使不可能无限逼近 0 0 的,那就是选择一个非常小的 h h 的值并近似导数
那么,如何在Ceres中实际构建一个数值微分算法过程的呢?主要可以分为两个步骤:
1. 定义Functor
给定参数值,Ceres将通过它对给定的 (x,y) ( x , y ) 的进行残值计算。
2. 用 NumericDiffCostFunction
来构造一个CostFunction
来封装整个实例。
这里我们仍然延用上一节解析微分算法中的例子。
y=b1(1+eb2−b3x)1/b4 y = b 1 ( 1 + e b 2 − b 3 x ) 1 / b 4
E(b1,b2,b3,b4)=∑if2(b1,b2,b3,b4;xi,yi)=∑i(b1(1+eb2−b3xi)1/b4−yi)2(1)(2) (1) E ( b 1 , b 2 , b 3 , b 4 ) = ∑ i f 2 ( b 1 , b 2 , b 3 , b 4 ; x i , y i ) (2) = ∑ i ( b 1 ( 1 + e b 2 − b 3 x i ) 1 / b 4 − y i ) 2
D1f(b1,b2,b3,b4;x,y)=1(1+eb2−b3x)1/b4(3) (3) D 1 f ( b 1 , b 2 , b 3 , b 4 ; x , y ) = 1 ( 1 + e b 2 − b 3 x ) 1 / b 4
具体代码如下:
struct Rat43CostFunctor {
Rat43CostFunctor(const double x, const double y) : x_(x), y_(y) {}
bool operator()(const double* parameters, double* residuals) const {
const double b1 = parameters[0];
const double b2 = parameters[1];
const double b3 = parameters[2];
const double b4 = parameters[3];
residuals[0] = b1 * pow(1.0 + exp(b2 - b3 * x_), -1.0 / b4) - y_;
return true;
}
const double x_;
const double y_;
//Analytic算法中手动求解Jacobians的部分被拿掉了。
}
CostFunction* cost_function =
new NumericDiffCostFunction1, 4>(
new Rat43CostFunctor(x, y));
这个生成微分的方法对于用户来说工作量最小。用户只需要关注残差计算是否正确和有效。这是第一步。
在进一步深入之前,我们需要先探讨一下前向查分的误差。我们在 x x 点附近对 f f 进行泰勒展开。
在上面我们构建了一个Functor
。现在我们用NumericDiffCostFunction
实现一种通用的方法(generic algorithm),对我们给定的Functor
进行数值微分,这是第二步。虽然 NumericDiffCostFunction
的实际实现是非常复杂的,但最终的结果是一个CostFunction
,大致代码如下:
class Rat43NumericDiffForward : public SizedCostFunction<1,4> {
public:
Rat43NumericDiffForward(const Rat43Functor* functor) : functor_(functor) {} //构造函数
virtual ~Rat43NumericDiffForward(){} //析构函数
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
functor_(parameters[0], residuals);
//Functor中的()操作符重载使之成为了一个函数。在Evaluate函数中parameters是个二级指针,对应二维数组。但是在Functor中parameters是个指向double的指针数组。
if (!jacobians) return true;
double* jacobian = jacobians[0];
if (!jacobian) return true;
const double f = residuals[0];
//这里的residuals[0]来自于上面的functor_()。这里的f对应上面表达式的分子的第二项f(x)。
double parameters_plus_h[4];
for (int i = 0; i < 4; ++i) {
std::copy(parameters, parameters + 4, parameters_plus_h);
//把目前的Parameters[4]中的元素复制到parameters_plus_h[4]中。
const double kRelativeStepSize = 1e-6;
//相对步长系数。
const double h = std::abs(parameters[i]) * kRelativeStepSize;
//实际步长 = 参数值 * 相对步长系数。
parameters_plus_h[i] += h;
//加入h之后的参数
double f_plus;
functor_(parameters_plus_h, &f_plus);
//用functor_()计算f_plus。f_plus即上面表达式的分子的第一项f(x+h)。
jacobian[i] = (f_plus - f) / h;
//计算上面表达式,求得近似的微分值Df(x)。
}
return true;
}
private:
scoped_ptr functor_; //作用域指针
};
std::copy(start, end, std::back_inserter(container));
用于容器内容的拷贝。
例如:
int a[3]={1,2,3}; int b[3]; std::copy(a,a+3,b);
注意在上面的代码中选择步骤大小的 h h ,不是一个绝对的步长大小,对于所有的参数都是相同的。我们使用相对步长大小 kRelativeStepSize=10−6 kRelativeStepSize = 10 − 6 ,这比绝对步长给出了更好的导数估计。这个步长大小的选择只适用于不接近于零的参数值。因此,数字扩散函数的实际实现,使用一个更复杂的步长选择逻辑,在接近于零的地方,它切换到一个固定的步长。上述是一个简化的版本。
O(h) O ( h ) 误差在前向差分公式中是可以的,但不是很好。一个更好的方法是使用中心差分公式:
中心差分公式的误差是 O(h2) O ( h 2 ) 。这个误差是平方的,而前向差分公式中的误差只会呈线性下降。
在Ceres中,将前向差分转换为中心差分很简单:只需要将NumericDiffCostFunction
模板参数中的FORWARD
更改为CENTRAL
。具体如下:
CostFunction* cost_function =
new NumericDiffCostFunction
new Rat43CostFunctor(x, y));
但是,这一点变化在实际中到底意味着什么呢?要搞清楚这个问题,让我们来考虑下面这个函数在 x=1.0 x = 1.0 处的导数的问题
从右到左阅读图表,显而易见的:
这里补充博客文章Ridders求导算法。文章对两种查分方式误差的对比分析写得更清晰。但是这篇文章对于Ridders算法 的介绍不太易懂。
在上面两种差分方法,精度受到计算机浮点数精度的限制,也就是 h h 不能无限小。那么,我们能否得到更好的对 Df D f 的估计,而不需要如此小的 h h ,以至于我们开始碰到浮点数的精度极限?
一种可能的方法是找到一种比 O(h2) O ( h 2 ) 快得多的方法。这可以通过运用 Richardson Extrapolation来解决微分问题。这也被称为Ridders的方法。
让我们回忆一下,中心差分公式中的误差。
这里要注意的是 K2 K 2 K4 K 4 ……独立于 h h ,只依赖于 x x 。
我们定义:
A(5,1) A ( 5 , 1 ) 需要 A(4,2) A ( 4 , 2 ) , A(4,2) A ( 4 , 2 ) 需要 A(3,3) A ( 3 , 3 ) , A(3,3) A ( 3 , 3 ) 需要 A(2,4) A ( 2 , 4 ) , A(2,4) A ( 2 , 4 ) 需要 A(1,5) A ( 1 , 5 ) ,所以 A(5,1) A ( 5 , 1 ) 最终对应的中心差分步长是 h25−1 h 2 5 − 1
上述内容是针对“数值微分法”的Ridders方法基础。具体的实现是一种自适应模式,它跟踪自己的评估误差,并在达到所需的精度时自动停止。很明晰那,它比前向差分和中心差分公式需要更多计算,但也更加鲁棒和精确。在Ceres使用Ridders法很方便,只需要修改一下NumericDiffCostFunction
的模板参数而已。
CostFunction* cost_function =
new NumericDiffCostFunction1, 4>(
new Rat43CostFunctor(x, y));
下面的图表显示了使用这三种差分方法时,绝对步长大小与相对误差的关系。对于Ridders的方法,我们假设评估 A(n,1) A ( n , 1 ) 的对应步长为 21−nh 2 1 − n h 。
使用Ridders方法计算 A(5,1) A ( 5 , 1 ) 需要计算十次评估函数,对于 Df(1.0) D f ( 1.0 ) 我们的估算结果比中心差分估算好1000倍(不明白怎么比较的)。为了准确地计算这些数字,计算机的双精度浮点类型≈2.22×10−16。
回到Rat43(上面的曲线拟合问题),让我们对比使用数值微分算法的各种方法的运行效率。
CostFunction | Time (ns) |
---|---|
Rat43Analytic | 255 |
Rat43AnalyticOptimized | 92 |
Rat43NumericDiffForward | 262 |
Rat43NumericDiffCentral | 517 |
Rat43NumericDiffRidders | 3760 |
正如预期的那样,中心差分的运行时间大约是前向差分的两倍,而Ridders方法的运行时间是前两者的好多倍,虽然它的精度也随之显著提高。
当你不能通过分析微分法或自动微分法来计算微分时,就应该使用数值微分算法。通常情况下,这种情况出现在调用一个外部库或函数时。用户不知道它的解析形式,或者即使知道,也无法用 Automatic Derivatives的方式重写它。
当使用数字微分时,尽量使用中心差分法。如果求算时间无关紧要,或者无法为目标函数确定一个适宜的静态相对步长,那么建议Ridders方法。