本节内容主要翻译自官方指南Derivatives部分
像大多数优化软件包一样,Ceres求解器的求解基于其能够在任意参数值下评估目标函数中每个项的值和导数。 正确而高效地做到这一点对于取得优秀的运算结果至关重要。Ceres提供了一系列解决方案,其中一个就是在Hello World中用到的Automatic Differentiation (自动微分算法)。这一部分我们将探讨另外两种可能性,即解析法(Analytic)和数值法(Numeric )求导。
在某些情况下,像在Hello World中一样定义一个代价函数是不可能的。比如在求解残差值(residual)的时候调用了一个库函数,而这个库函数的内部算法你根本无法干预。在这种情况下数值微分算法就派上用场了。用户定义一个CostFunctor来计算残差值,并且构建一个NumericDiffCostFunction
数值微分代价函数。比如对于 f(x)=10−x f ( x ) = 10 − x 对应函数体如下:
struct NumericDiffCostFunctor {
bool operator()(const double* const x, double* residual) const {
residual[0] = 10.0 - x[0];
return true;
}
};
这里不妨再插入一次在Hello world中的Functor的定义:
struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { residual[0] = T(10.0) - x[0]; return true; } };
可以发现,这次没用模板类。
然后继续添加Problem
CostFunction* cost_function =
new NumericDiffCostFunction<NumericDiffCostFunctor, ceres::CENTRAL, 1, 1>(
new NumericDiffCostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
同样的这里引入在Hello World中使用automatic算法时的代码以供对比:
CostFunction* cost_function = new AutoDiffCostFunction
1, 1>(new CostFunctor); problem.AddResidualBlock(cost_function, NULL, &x);
可以发现两种算法在构建Problem时候基本差不多。但是在用Nummeric算法时需要额外给定一个参数ceres::CENTRAL
。这个参数告诉计算机如何计算导数。更多具体介绍可以参看NumericDiffCostFunction的Doc文档。
Ceres官方更加推荐自动微分算法,因为C++模板类使自动算法有更高的效率。数值微分算法通常来说计算更复杂,收敛更缓慢。
有些时候,应用自动求解算法时不可能的。比如在某些情况下,计算导数的时候,使用闭合解(closed form,也被称为解析解)会比使用自动微分算法中的链式法则(chain rule)更有效率。这里辨析一下解析解和数值解:
在解组件特性相关的方程式时,大多数的时候都要去解偏微分或积分式,才能求得其正确的解。依照求解方法的不同,可以分成以下两类:解析解和数值解。
解析解(analytical solution):
就是一些严格的公式,给出任意的自变量就可以求出其因变量,也就是问题的解。他人可以利用这些公式计算各自的问题。所谓的解析解是一种包含:分式、三角函数、指数、对数甚至无限级数等基本函数的解的形式。用来求得解析解的方法称为解析法〈analytic techniques、analytic methods〉,解析法即是常见的微积分技巧,例如分离变量法等。解析解为一封闭形式〈closed-form〉的函数,因此对任一独立变量,我们皆可将其带入解析函数求得正确的相依变量。因此,解析解也被称为闭式解(closed-form solution)。
数值解(numerical solution):
是采用某种计算方法,如有限元的方法, 数值逼近,插值的方法, 得到的解。别人只能利用数值计算的结果, 而不能随意给出自变量并求出计算值。当无法藉由微积分技巧求得解析解时,这时便只能利用数值分析的方式来求得其数值解了。数值方法变成了求解过程重要的媒介。在数值分析的过程中,首先会将原方程式加以简化,以利后来的数值分析。例如,会先将微分符号(连续)改为差分符号(离散)等。然后再用传统的代数方法将原方程式改写成另一方便求解的形式。这时的求解步骤就是将一独立变量带入,求得相依变量的近似解。因此利用此方法所求得的相依变量为一个个分离的数值〈discrete values〉,不似解析解为一连续的分布,而且因为经过上述简化的动作,所以可以想见正确性将不如解析法来的好。
引自: closed-form solution (闭合解/解析解)和数值解的理解
至于链式法则可以参考维基百科的介绍以及例子。这里简要摘录如下。
链式法则或链锁定则(英语:chain rule),是求复合函数导数的一个法则。设 f f 和 g g 为两个关于 x x 可导函数,则复合函数 (f∘g)(x) ( f ∘ g ) ( x ) 的导数 (f∘g)′(x) ( f ∘ g ) ′ ( x ) 为:
(f∘g)′(x)=f′(g(x))g′(x). ( f ∘ g ) ′ ( x ) = f ′ ( g ( x ) ) g ′ ( x ) .
例如:
求函数 f(x)=(x2+1)3 f ( x ) = ( x 2 + 1 ) 3 的导数。设 g(x)=x2+1 g ( x ) = x 2 + 1 , h(g)=g3 h ( g ) = g 3 ,则
f(x)=h(g(x)) f ( x ) = h ( g ( x ) )
f′(x)=h′(g(x))g′(x)=3(g(x))2(2x)=3(x2+1)2(2x)=6x(x2+1)2. f ′ ( x ) = h ′ ( g ( x ) ) g ′ ( x ) = 3 ( g ( x ) ) 2 ( 2 x ) = 3 ( x 2 + 1 ) 2 ( 2 x ) = 6 x ( x 2 + 1 ) 2 .
在这种情况下,你就可以自己编写残差计算代码和雅可比行列式的计算代码了。还是用Hello world中的 f(x)=10−x f ( x ) = 10 − x 为例:
class QuadraticCostFunction : public ceres::SizedCostFunction<1, 1> {
// 定义一个CostFunction或 SizedCostFunction(如果参数和残差在编译时就已知了)的子类。
public:
virtual ~QuadraticCostFunction() {}
virtual bool Evaluate(double const* const* parameters,
//输入参数数组
double* residuals,
//输出残差数组
double** jacobians) const {
//输出雅可比行列式
const double x = parameters[0][0];
residuals[0] = 10 - x;
// Compute the Jacobian if asked for.
if (jacobians != NULL && jacobians[0] != NULL) {
jacobians[0][0] = -1;
}
return true;
}
};
Evaluate
函数用于检查jacobians
是否为非零值。如果是,那么就把残差方程的导数值赋值给它。
(这一句有点糊涂,可能翻译的不对:The jacobians array is optional, Evaluate is expected to check when it is non-null, and if it is the case then fill it with the values of the derivative of the residual function.)
因为这里的残差方程是线性方程,所以雅可比行列式是常数。
从上述代码片段可以看出,实现“CostFunction““其实有点乏味。所以除非有什么特殊原因需要自行构建雅可比的计算,否则最好还是直接使用自动微分法或者数值微分法来创建残差块。
到目前为止,计算导数其实是Ceres最复杂的部分了。根据需要,用户有时候还需要更复杂的导数计算算法。这一节仅仅是大体介绍了如何使用Ceres进行导数计算最浅显的部分。对Numeric和Auto方法都很熟悉之后,还可以深入研究一下DynamicAutoDiffCostFunction
, CostFunctionToFunctor
, NumericDiffFunctor
和ConditionedCostFunction
,从而实现更高级的代价函数的计算方法。