本文主要是介绍Ceres-Solver 官网教程翻译与学习,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Ceres-Sovler
- 仿函数(预备知识)
- Ceres
- 最小二乘问题
- Hello World
- 导数(derivatives)
- 数值微分(Numerical Derivatives)
- 解析法求导(Analytic Derivative)
- Powell's Function
- Curve Fitting
- Rubust Curve Fitting 鲁棒曲线拟合
- Bundle Adjustment
- 让我们来求解[BAL](http://grail.cs.washington.edu/projects/bal/)数据集 此处未完待续
- On Derivatives
- Spivak Notation (斯皮瓦克标记法)
- Analytic Derivatives
- Numeric Derivatives
- Forward Differences
- Central Differences
- Ridders' Methord
- Automatic Derivatives
- Interfacing With Automatic Differentiation
- A function that return its value(返回值的非模板函数)
ceres-solver 参考1这是一个系列教程
ceres-solver 参考2这是一个入门教程
官网教程 推荐
仿函数(预备知识)
仿函数 参考
ceres当中代价函数(cost function)的构建当中用到了仿函数(functor),使用方法是在costfunction函数中重载了()运算符(operator)
示例程序:
class Functor
{
private:char* ss;int count;
public:explicit Functor(char* str) :ss(str),count(0){std::cout << "Functor: " << ss << "\n"; }void operator() (std::string str) {count++;std::cout << "The Functor is called " << count << " times " <<str<< "\n";}
};
int main()
{Functor functor("construct");functor("first");functor("second");Functor functor1("construct1");functor1("first1");functor1("second1");
}
结果:
Functor: construct
The Functor is called 1 times first
The Functor is called 2 times second
Functor: construct1
The Functor is called 1 times first1
The Functor is called 2 times second1
Ceres
ceres用来解决边界约束鲁棒非线性最小二乘 问题 (bounds constrained robustified non-linear least squares problems)
最小二乘问题
边界约束非线性最小二成问题
min x 1 2 ∑ i ρ i ( ∥ f i ( x i 1 , … , x i k ) ∥ 2 ) s . t . l j ≤ x j ≤ u j (1) \begin{aligned} \underset{x}{\min}&\enspace\frac{1}{2}{\sum\limits_i}\rho_i\Big(\|f_i(x_{i1},\dots,x_{ik})\|^2\Big) \\\mathrm{s.t.}&\enspace{}l_j\le{}x_j\le{}u_j \end{aligned}\tag{1} xmins.t.21i∑ρi(∥fi(xi1,…,xik)∥2)lj≤xj≤uj(1)
- 目标函数 min x f ( x ) \min\limits_x\thinspace{}f(x) xminf(x)表示使 f ( x ) f(x) f(x)最小的 x x x取值
- 约束条件 s . t . \mathrm{s.t.} s.t.是subject to 的简写
- ρ i ( ∥ f i ( x i 1 , … , x i k ) ∥ 2 ) \rho_i\Big(\|f_i(x_{i1},\dots,x_{ik})\|^2\Big) ρi(∥fi(xi1,…,xik)∥2) 称为残差块(ResidualBlock)
- f i ( ⋅ ) f_i(\cdot) fi(⋅)称为代价函数(CostFuction)
- [ x i 1 , … , x i k ] [x_{i1},\dots,x_{ik}] [xi1,…,xik]称为参数块(ParameterBlock) 其中 x i k x_{ik} xik是标量(Scalar),参数块可以是一个标量群,也可以是单个标量
- l j l_j lj, u j u_j uj是参数块(Parameter Block) x j x_j xj的边界条件
- ρ i \rho_i ρi称为损失函数(LossFunction) 是一个标量函数(Scalar Function),用来减小异常值(outliers)对结果的非线性最小二乘求解结果的影响。当 ρ i ( f i ( x i ) ) = f i ( x i ) \rho_i\big(f_i(x_i)\big)=f_i(x_i) ρi(fi(xi))=fi(xi)且 l i = − ∞ l_i=-\infty li=−∞, u i = ∞ u_i=\infty ui=∞时,我们就得到通常意义上的非线性最小二乘问题
1 2 ∑ i ∥ f ( x i 1 , … , x i k ) ∥ 2 (2) \frac{1}{2}\thinspace\sum\limits_i\|f(x_{i1},\dots,x_{ik})\|^2\tag{2} 21i∑∥f(xi1,…,xik)∥2(2)
Hello World
首先从一个查找函数最小值的问题起步
1 2 ( 10 − x ) 2 \dfrac{1}{2}(10-x)^2 21(10−x)2
这是一个很简单的问题,很容易得到最小值在 x = 10 x=10 x=10处,但是这是一个很好的起点来说明使用Ceres来解决问题的基本用法。
首先写一个仿函数(functor)来评价函数
f ( x ) = 10 − x f(x)=10-x f(x)=10−x
struct CostFunctor
{template <typename T>bool operator() (const T* const x, T* residual) const{residual[0] = 10.0 - x[0];return true;}
};
operator()是模板方法(Template Method),这种方法假设输入与输出都是某个类型T,模板的使用允许ceres按照下面两种方法调用CostFunctor::operator():
- 当只需要计算残差值时T=double
- 当需要Jacobian矩阵时T=Jet,Jet是Ceres定义的一个特殊类型
一旦我们有办法计算残差方程(Residual Function),是时候构建一个非线性最小二乘问题(non-linear least squares problem)并利用Ceres来求解
void TestCeres()
{ceres::Problem problem;double initial_x = 5.0;double x = initial_x;//建立唯一的代价函数(cost function)也可以称为 残差(residual)//Auto-Differentiation用来获得微分(derivative )或者 Jacobian矩阵ceres::CostFunction* cost_function =new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);problem.AddResidualBlock(cost_function, nullptr, &x);//run the solver!ceres::Solver::Options options;options.linear_solver_type = ceres::DENSE_QR;options.minimizer_progress_to_stdout = true;ceres::Solver::Summary summary;ceres::Solve(options, &problem, &summary);std::cout << summary.BriefReport() << "\n";std::cout << "x : " << initial_x << " -> " << x << "\n";
}
AutoDiffCostFucntion
将 CostFunctor
的一个实例地址作为输入,自动的对其进行微分,并赋予其一个 CostFunction
接口
结果: 我的结果跟官网的结果稍有不同,我的目标值(cost)比官网小,不知道为什么
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time0 1.250000e+01 0.00e+00 5.00e+00 0.00e+00 0.00e+00 1.00e+04 0 8.90e-04 1.96e-031 1.249750e-07 1.25e+01 5.00e-04 5.00e+00 1.00e+00 3.00e+04 1 1.18e-03 7.61e-032 1.388518e-16 1.25e-07 1.67e-08 5.00e-04 1.00e+00 9.00e+04 1 3.60e-04 8.09e-03
Ceres Solver Report: Iterations: 3, Initial cost: 1.250000e+01, Final cost: 1.388518e-16, Termination: CONVERGENCE
x : 5 -> 10
从 x = 5 x=5 x=5开始,求解器(solver)经过两次迭代得到最终结果10,仔细的读者应该发现这是一个线性问题,采用线性求解器应该就会求得优化结果。求解器的默认配置是面向非线性问题的,为了简化过程例子中并没有修改配置。使用Ceres-Solver确实有可能通过一次迭代得到这个问题的结果,在讲到Ceres收敛与参数设置时我们会详细的讨论这个问题。
导数(derivatives)
参考
导数与微分
Ceres-Solver与大多数优化包一样,依赖于能够在任意参数下评估(evaluate)目标函数中每一项的函数值与导数,能够正确并有效的做到这一点对获得好的结果至关重要。Ceres Solver提供了几种不同方法,在上面的例子中你已经见到了其中的一种-Automatic Differentiation(自动微分),我们现在考虑其他两种可能性(possibilities):解析求导(Analytic Derivatives)与数值求导(Numerical Derivatives)
数值微分(Numerical Derivatives)
在某些情况下,我们无法定义一个模板代价函数,比如计算残差(residual)时调用了一个无法控制的库函数,这种情况下就可以使用数值求导。用户定义一个仿函数(functor)来计算残差(residual),并构建一个NumericDiffCostFunction
,以f(x)=10-x
为例,对应的仿函数(functor)为:
struct NumericalDiffCostFunctor
{bool operator()(const double* const x, double* residual) const{residual[0] = 10.0 - x[0];return true;}
};
按照如下方法加入Problem
中
void TestCeresNumerical()
{ceres::Problem problem;double initial_x = 5;double x = initial_x;ceres::CostFunction* cost_function =new ceres::NumericDiffCostFunction<NumericalDiffCostFunctor, ceres::CENTRAL, 1, 1>(new NumericalDiffCostFunctor);problem.AddResidualBlock(cost_function, nullptr, &x);ceres::Solver::Options option;option.linear_solver_type = ceres::DENSE_QR;option.minimizer_progress_to_stdout = true;ceres::Solver::Summary summary;ceres::Solve(option, &problem, &summary);std::cout << summary.BriefReport() << "\n";std::cout << "x: " << initial_x << " -> " << x << "\n";
}
与Automatic Differentiation中我们使用的代码做对比
ceres::CostFunction* cost_function =new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);problem.AddResidualBlock(cost_function, nullptr, &x);
与 Automatic Differentiation中的代码结构几乎一样(identical),但是增加了一个额外的参数ceres::CENTRAL
用来表明采用哪种有限差分方案来计算数值导数。ceres::CENTRAL
定义如下:NumericDiffCostFunction
enum NumericDiffMethodType {// Compute central finite difference: f'(x) ~ (f(x+h) - f(x-h)) / 2h.CENTRAL,// Compute forward finite difference: f'(x) ~ (f(x+h) - f(x)) / h.FORWARD,// Adaptive numerical differentiation using Ridders' method. Provides more// accurate and robust derivatives at the expense of additional cost// function evaluations.RIDDERS
};
总的来说我们推荐自动微分(Automatic Differentiation)来取代数值微分(Numerical Differentiation)。C++的模板类让自动微分十分高效,而数值微分计算量大,容易产生数值误差(numeric error),并且收敛(convergence)更慢。
解析法求导(Analytic Derivative)
有些情况下可能无法使用自动微分。比如,在某些情况下使用封闭解比自动微分中使用的链式法则(chain rule)的计算导数更有效。
在这种情况下,需要提供你自己的残差(residual)与 雅可比(Jacobian)计算代码。 定义一个CostFunction
的子类(subclass),如果你知道参数(parameter)与残差(residual)在编译时的大小也可以定义SizedCostFunction
的子类。下面以实现 f ( x ) = 10 − x f(x)=10-x f(x)=10−x的SimpleCostFunction
为例
class QuadraticCostFunction : public ceres::SizedCostFunction<1, 1>
{
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 forif (jacobians != nullptr && jacobians[0] != nullptr){jacobians[0][0] = -1;}return true;}
};
下面是我自己写的,教程中没有
void testQuadraticCostFunction()
{ceres::Problem problem;double initial_x = 5;double x = initial_x;ceres::CostFunction* cost_function = new QuadraticCostFunction;problem.AddResidualBlock(cost_function,nullptr,&x);ceres::Solver::Options option;option.linear_solver_type = ceres::DENSE_QR;option.minimizer_progress_to_stdout = true;ceres::Solver::Summary summary;ceres::Solve(option, &problem, &summary);std::cout << summary.BriefReport() << "\n";std::cout << "x: " << initial_x << " -> " << x << '\n';
}
这里代码应该是写错了,将SimpleCOstFunction写成了QuadraticCostFunction
QuadraticCostFunction::Evaluate
有一个参数(parameter)数组,一个输出的残差(residual)数组,和一个输出的雅可比(Jacobian)数组,雅可比数组是可选的,evaluate
函数会检查jacobians是否为非空(non-null),如果是非空的就填入残差方程的导数值(value of the derivative of residual function)。在本例当中残差方程是线性的,所以雅可比矩阵是常数(constant)
从上面的例子中我们可以看出,实现CostFunction
对象的过程有些枯燥,除非你有非常好的理由自己实现Jacobian矩阵,否则我们还是推荐你使用自动微分或者数值微分来构建残差块(residual blocks)
计算导数目前为止是使用Ceres最复杂的部分,根据情况使用者可能需要更复杂的导数计算方法。这一部分只是浅显的介绍了如何向Ceres提供导数,一旦你适应了使用NumericDiffCostFunction
andAutoDiffCostFunction
,我们建议看一看DynamicAutoDiffCostFunction
,CostFunctionToFunctor
,NumericDiffFunctor
andConditionedCostFunction
来获取更多构建与计算代价函数的方法。
Powell’s Function
鲍威尔方程
现在考虑一个稍微复杂点的例子—鲍威尔方程的最小化,令 x = [ x 1 , x 2 , x 3 , x 4 ] x=[x_1,x_2,x_3,x_4] x=[x1,x2,x3,x4]
f 1 ( x ) = x 1 + 10 x 2 f 2 ( x ) = 5 ( x 3 − x 4 ) f 3 ( x ) = ( x 2 − 2 x 3 ) 2 f 4 ( x ) = 10 ( x 1 − x 4 ) 2 F ( x ) = [ f 1 ( x ) , f 2 ( x ) , f 3 ( x ) , f 4 ( x ) ] \begin{aligned} f_1(x)&=x_1+10x_2\\ f_2(x)&=\sqrt{5}(x_3-x_4)\\ f_3(x)&=(x_2-2x_3)^2\\ f_4(x)&=\sqrt{10}(x_1-x_4)^2\\ F(x)&=[f_1(x),f_2(x),f_3(x),f_4(x)] \end{aligned} f1(x)f2(x)f3(x)f4(x)F(x)=x1+10x2=5(x3−x4)=(x2−2x3)2=10(x1−x4)2=[f1(x),f2(x),f3(x),f4(x)]
F ( x ) F(x) F(x)是一个拥有四个参数,四组残差的方程,我们希望找到一个令 1 2 ∥ F ( x ) ∥ 2 \dfrac{1}{2}\|F(x)\|^2 21∥F(x)∥2最小化的 x x x
第一步,目标函数中的每一项都定义仿函数,下面是 f 1 ( x 1 , x 2 ) f_1(x_1,x_2) f1(x1,x2), f 2 ( x 3 , x 4 ) f_2(x_3,x_4) f2(x3,x4), f 3 ( x 2 , x 3 ) f_3(x_2,x_3) f3(x2,x3), f 4 ( x 1 , x 4 ) f_4(x_1,x_4) f4(x1,x4)的代码
struct F1
{template <typename T>bool operator()(const T* const x1, const T* const x2, T* residual) const{residual[0] = x1[0] + 10 * x2[0];return true;}
};
struct F2
{template <typename T>bool operator()(const T* const x3, const T* const x4, T* residual) const{residual[0] = sqrt(5) * (x3[0] - x4[0]);}
};
struct F3
{template <typename T>bool operator()(const T* const x2, const T* const x3, T* residual) const{residual[0] = (x2[0] - 2 * x3[0]) * (x2[0] - 2 * x3[0]);}
};
struct F4
{template <typename T>bool operator()(const T* const x1, const T* const x4, T* residual)const{residual = sqrt(10.0) * (x1[0] - x4[0]) * (x1[0] - x4[0]);return true;}
};
利用上面定义的结构体按如下方法构建problem:
double x1 = 3.0; double x2 = -1.0; double x3 = 0.0; double x4 = 1.0;ceres::Problem problem;problem.AddResidualBlock(new ceres::AutoDiffCostFunction<F1, 1, 1, 1>(new F1), nullptr, &x1, &x2);problem.AddResidualBlock(new ceres::AutoDiffCostFunction<F2, 1, 1, 1>(new F2), nullptr, &x3, &x4);problem.AddResidualBlock(new ceres::AutoDiffCostFunction<F3, 1, 1, 1>(new F3), nullptr, &x2, &x3);problem.AddResidualBlock(new ceres::AutoDiffCostFunction<F4, 1, 1, 1>(new F4), nullptr, &x1, &x4);ceres::Solver::Summary summary;ceres::Solver::Options option;option.max_num_iterations = 100;option.linear_solver_type = ceres::DENSE_QR;option.minimizer_progress_to_stdout = true;ceres::Solve(option, &problem, &summary);std::cout << "Initial: " << "x1 =" << x1 << ", x2= " << x2 << ", x3= " << x3 << ", x4= " << x4 << '\n';std::cout << summary.FullReport() << '\n';std::cout << "Final: " << "x1 =" << x1 << ", x2= " << x2 << ", x3= " << x3 << ", x4= " << x4 << '\n';
注意到每一个ResidualBlock
只依赖两个参数,相应的残差对象也不依赖所有的四个参数。结果略。
Curve Fitting
到目前为止我们所见到的例子都是无数据的简单优化问题。最小方差与非线性最小方差分析的最初目的是对数据进行曲线拟合。现在正适合我们考虑这个问题。问题包含从曲线 e 0.3 x + 0.1 e^{0.3x+0.1} e0.3x+0.1采样的数据,并向数据添加带有标准差(standard deviation) σ = 2 \sigma=2 σ=2的高斯(Gaussian)噪声。现在我们用数据拟合曲线 y = e m x + c y=e^{mx+c} y=emx+c
我们先来定义一个模板类来计算残差。对每一个观测(observation)都有一个残差。
struct ExponentialResidual
{ExponentialResidual(double x, double y) :x_(x), y_(y) {}template <typename T>bool operator()(const T* const m, const T* const c, T* residual) const{residual[0] = y_ - exp(m[0] * x_ + c[0]);return ture;}
private:const double x_;const double y_;
};
假设观测数据存在一个 2 n 2n 2n大小的数组data
当中, problem 的构建就是简单的对每一个观测数据创建一个CostFunction
。
const int kNumObservations = 67;// clang-format offconst double data[] = { 0.000000e+00, 1.133898e+00,
... };//这里有67组数 一组有一个x,一个ydouble m = 0.0;double c = 0.0;ceres::Problem problem;for (int i = 0; i <kNumObservations; i++){ceres::CostFunction* cost_function =new ceres::AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(new ExponentialResidual(data[2 * i], data[2 * i + 1]));problem.AddResidualBlock(cost_function, nullptr, &m, &c);}ceres::Solver::Options option;option.max_num_iterations = 100;option.linear_solver_type = ceres::DENSE_QR;option.minimizer_progress_to_stdout = true;ceres::Solver::Summary summary;ceres::Solve(option, &problem, &summary);std::cout << summary.BriefReport() << '\n';std::cout << "Initial m: " << 0.0 << " c: " << 0.0 << '\n';std::cout << "Final m: " << m << " c: " << c << '\n';
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time0 1.211734e+02 0.00e+00 3.61e+02 0.00e+00 0.00e+00 1.00e+04 0 1.80e-03 2.13e-031 2.334822e+03 -2.21e+03 3.61e+02 7.52e-01 -1.87e+01 5.00e+03 1 8.37e-04 3.48e-032 2.331438e+03 -2.21e+03 3.61e+02 7.51e-01 -1.86e+01 1.25e+03 1 4.32e-04 4.01e-033 2.311313e+03 -2.19e+03 3.61e+02 7.48e-01 -1.85e+01 1.56e+02 1 4.24e-04 4.52e-034 2.137268e+03 -2.02e+03 3.61e+02 7.22e-01 -1.70e+01 9.77e+00 1 4.26e-04 5.02e-035 8.553131e+02 -7.34e+02 3.61e+02 5.78e-01 -6.32e+00 3.05e-01 1 4.28e-04 5.54e-036 3.306595e+01 8.81e+01 4.10e+02 3.18e-01 1.37e+00 9.16e-01 1 2.11e-03 7.77e-037 6.426770e+00 2.66e+01 1.81e+02 1.29e-01 1.10e+00 2.75e+00 1 2.01e-03 9.90e-038 3.344546e+00 3.08e+00 5.51e+01 3.05e-02 1.03e+00 8.24e+00 1 2.24e-03 1.22e-029 1.987485e+00 1.36e+00 2.33e+01 8.87e-02 9.94e-01 2.47e+01 1 2.01e-03 1.44e-0210 1.211585e+00 7.76e-01 8.22e+00 1.05e-01 9.89e-01 7.42e+01 1 2.38e-03 1.69e-0211 1.063265e+00 1.48e-01 1.44e+00 6.06e-02 9.97e-01 2.22e+02 1 2.05e-03 1.91e-0212 1.056795e+00 6.47e-03 1.18e-01 1.47e-02 1.00e+00 6.67e+02 1 2.02e-03 2.12e-0213 1.056751e+00 4.39e-05 3.79e-03 1.28e-03 1.00e+00 2.00e+03 1 2.02e-03 2.34e-02
Ceres Solver Report: Iterations: 14, Initial cost: 1.211734e+02, Final cost: 1.056751e+00, Termination: CONVERGENCE
Initial m: 0 c: 0
Final m:0.291861 c: 0.131439
从参数值 m = 0 m=0 m=0, c = 0 c=0 c=0开始,初始的目标函数值是121.173。Ceres找到了一组结果 m = 0.291861 , c = 0.131439 m=0.291861, c=0.131439 m=0.291861,c=0.131439对应的目标函数值是1.05675。这些值与原始的模型参数 y = e 0.3 x + 0.1 y=e^{0.3x+0.1} y=e0.3x+0.1 稍有不同,但是这个是可以预料到的。当从有噪声的数据中拟合曲线时,我们希望看到这个偏差(deviation)
Rubust Curve Fitting 鲁棒曲线拟合
如果假设数据中有一些离群值(outlier),也就是说有一些点不符合噪声模式,如果我们使用上面的代码来拟合这样的数据就会得到下面的曲线(摘自官网),注意拟合的曲线如何偏离真值。
为了处理离群值outlier
,标准的方法是利用损失函数LossFunction
, 损失函数减小了交大残差对残差块的影响,通常较大残差是由离群值影响的。为了使一个损失函数与残差块关联起来,我们将
problem.AddResidualBlock(Cost_function, nullptr, &m, &c)
替换为
problem.AddResidualBlock(const_function, new CauchyLoss(0.5), &m, &c);
CauchyLoss
是Ceres Solver的的一个损失函数。参数0.5制定了损失函数的幅度。这样就可以得到下面的较好结果:略
Bundle Adjustment
光束平差法1
光束平差法2 推荐
李群和李代数
自然常数 e e e,工程中的自然数 1 1 1
编写Ceres的一个重要原因是我们需要解决大规模的光束平差问题。
给出一组测量的图像特征坐标和其他相应数据,光束平差法的目标是找到使重投影误差最小的3D点坐标和相机参数。这个优化问题通常被规划为非线性最小方差问题,其中误差是观测道德特征坐标与相应3D点在相机相平面投影之间的 L 2 L_2 L2范数normal
的平方square
。Ceres 广泛的支持光束平差问题。
Reference
L 0 L_0 L0范数:向量中非0元素的个数
L 1 L_1 L1范数:向量中各个元素绝对值之和 ∥ x ∥ 1 = ∣ x 1 ∣ + ∣ x 2 ∣ + ⋯ + ∣ x n ∣ \|x\|_1=|x_1|+|x_2|+\dots+|x_n| ∥x∥1=∣x1∣+∣x2∣+⋯+∣xn∣
L 2 L_2 L2范数:向量各元素平方和后求平方根 ∥ x 2 ∥ 2 = ∣ x 1 ∣ 2 + ∣ x 2 ∣ 2 + ⋯ + ∣ x n ∣ 2 \|x_2\|_2=\sqrt{|x_1|^2+|x_2|^2+\dots+|x_n|^2} ∥x2∥2=∣x1∣2+∣x2∣2+⋯+∣xn∣2
让我们来求解BAL数据集
此处未完待续
On Derivatives
Spivak Notation (斯皮瓦克标记法)
Michael Spivak – 微分几何的数学家
为了保持集体理智,我们使用Spivak关于导数的解释。这是一个实用的解释,使包含导数的表达式理解起来变得简单易懂
对于一个单变量的函数 f f f, f ( a ) f(a) f(a)代表它再 a a a点的值, D f Df Df代表它的一次导数, D f ( a ) Df(a) Df(a)是在 a a a点的导数,也就是: D f ( a ) = d d x f ( x ) ∣ x = a Df(a)=\frac{d}{dx}f(x)\Big|_{x=a} Df(a)=dxdf(x)∣∣∣x=a
D k D^k Dk代表 f f f的第 k t h k^{th} kth次导数。
对于一个二元函数 g ( x , y ) g(x,y) g(x,y). D 1 g D_{1g} D1g与 D 2 g D_{2g} D2g分别代表 g g g的第一变量与第二变量的偏微分. 与经典标记法等效:
D 1 g = ∂ ∂ x g ( x , y ) D 2 g = ∂ ∂ y g ( x , y ) D_{1g}=\frac{\partial}{\partial{x}}g(x,y)\\ D_{2g}=\frac{\partial}{\partial{y}}g(x,y) D1g=∂x∂g(x,y)D2g=∂y∂g(x,y)
D g D_g Dg代表 g g g的Jacobian矩阵:
D g = [ D 1 g , D 2 g ] D_g=[D_{1g},D_{2g}] Dg=[D1g,D2g]
对于一个多变量函数 g : R n → R m g:\mathbb{R}^{n}\to\mathbb{R}^{m} g:Rn→Rm, D g D_g Dg 代表 m × n m\times{n} m×n的Jacobian矩阵. D i g D_{ig} Dig是 g g g的第 i i i个坐标的偏微分,是 D g D_g Dg的第 i i i列。
最后, D 1 2 g D_{1}^{2}g D12g, D 1 D 2 g D_{1}D_{2}g D1D2g代表高阶偏微分。
For more see Michael Spivak’s book Calculus on Manifolds
Analytic Derivatives
考虑下面这个将曲线拟合到数据上的问题:
b 1 ( 1 + e b 2 − b 3 x 1 ) 1 / b 4 \frac{b_{1}}{(1+e^{b_{2}-b_{3}x_{1}})^{1/b_{4}}} (1+eb2−b3x1)1/b4b1
也就是说给定一些数据 { x i , y i } , ∀ i = 1 , 2 , … , n \{x_i,y_i\},\forall{i}=1,2,\dots,n {xi,yi},∀i=1,2,…,n, 确定最适合数据的参数 b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4。
这个问题也就是找到使下面目标函数最小的参数 b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4
E ( b 1 , b 2 , b 3 , b 4 ) = ∑ i = 1 n f 2 ( b 1 , b 2 , b 3 , b 4 ; x i , y j ) = ∑ i = 1 n ( b 1 ( 1 + E b 2 − b 3 x 1 ) − y i ) 2 \begin{aligned}E(b_1,b_2,b_3,b_4)&=\sum_{i=1}^{n}{f^2(b_1,b_2,b_3,b_4;x_i,y_j)}\\ &=\sum_{i=1}^{n}\bigg(\frac{b_1}{(1+E^{b_2-b_3x_1})}-y_i\bigg)^2 \end{aligned} E(b1,b2,b3,b4)=i=1∑nf2(b1,b2,b3,b4;xi,yj)=i=1∑n((1+Eb2−b3x1)b1−yi)2
利用Ceres解决这个问题,我们需要定义一个代价函数来计算给定 x x x与 y y y关于 b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4的残差Residual
f f f和导数.
使用初级微积分,我们可以得到:
D 1 f ( b 1 , b 2 , b 3 , b 4 ; x , y ) = 1 ( 1 + e b 2 − b 3 x ) 1 / b 4 D 2 f ( b 1 , b 2 , b 3 , b 4 ; x , y ) = − b 1 e b 2 − b 3 x b 4 ( 1 + e b 2 − b 3 x ) 1 / b 4 + 1 D 3 f ( b 1 , b 2 , b 3 , b 4 ; x , y ) = b 1 x e b 2 − b 3 x b 4 ( 1 + e b 2 − b 3 x ) 1 / b 4 + 1 D 4 f ( b 1 , b 2 , b 3 , b 4 ; x , y ) = b 1 log ( 1 + e b 2 − b 3 x ) b 4 2 ( 1 + e b 2 − b 3 x ) 1 / b 4 \begin{aligned} D_1f(b_1,b_2,b_3,b_4;x,y)&=\frac{1}{(1+e^{b_2-b_3x})^{1/b_4}}\\ D_2f(b_1,b_2,b_3,b_4;x,y)&=\frac{-b_1e^{b2-b_3x}}{b_4(1+e^{b_2-b_3x})^{1/b_4+1}}\\ D_3f(b_1,b_2,b_3,b_4;x,y)&=\frac{b_1xe^{b_2-b_3x}}{b_4(1+e^{b_2-b_3x})^{1/b_4+1}}\\ D_4f(b_1,b_2,b_3,b_4;x,y)&=\frac{b_1\log(1+e^{b_2-b_3x})}{b_4^2(1+e^{b_2-b_3x})^{1/b_4}} \end{aligned} D1f(b1,b2,b3,b4;x,y)D2f(b1,b2,b3,b4;x,y)D3f(b1,b2,b3,b4;x,y)D4f(b1,b2,b3,b4;x,y)=(1+eb2−b3x)1/b41=b4(1+eb2−b3x)1/b4+1−b1eb2−b3x=b4(1+eb2−b3x)1/b4+1b1xeb2−b3x=b42(1+eb2−b3x)1/b4b1log(1+eb2−b3x)
Tat43AnalyticOptimized
比Rat43Analytic
快了2.8倍。这种运行时的差异十分普遍。为了获得解析导数的最佳性能,需要对通用子表达式进行优化。
什么时候需要使用解析微分?
- 1、表达式简
- 单,例如线性的
- 2、一个计算机代数软件像Maple, Mathematica, SymPy可以用来对目标函数计算符号微分,并生成C++代码。
- 3、对效率要求极高,可以利用一些代数结构来获得比自动微分更好的性能。
也就是说为了获得更好的性能,需要做大量的复杂工作。在走这条路之前, 评估计算Jacobian矩阵在整个求解过程中所占的时间非常有必要。记住Amdahl’s Law对你很有帮助。 - 4、没有其他方法计算导数,例如要计算下面这个多项式
Polyomial
根关于 x x x, y y y的导数:
a 3 ( x , y ) z 3 + a 2 ( x , y ) z 2 + a 1 ( x , y ) z + a 0 ( x , y ) = 0 a_3(x,y)z^3+a_2(x,y)z^2+a_1(x,y)z+a_0(x,y)=0 a3(x,y)z3+a2(x,y)z2+a1(x,y)z+a0(x,y)=0
这个方程的导数求解需要使用Inverse Function Theorem - 5、你就是喜欢手动计算导数。
附注Footnotes
最佳拟合的概念依赖用于评价拟合质量目标函数的选择,目标函数反过来也依赖于产生观测的基本噪声处理。当噪声是高斯噪声时,最小化方差和是正确的方法。在这种情况下,参数的优化值是极大似然估计
Numeric Derivatives
Forward Differences
Central Differences
Ridders’ Methord
Automatic Derivatives
Interfacing With Automatic Differentiation
Reference
当代价函数是一个明确的表达式时,可以直接使用自动微分。但是并不总是这样,有时代价函数需要与外部程序或数据进行交互。在这一章中我们考虑几种不同的方法。
我们考虑下面这个问题,通过查找参数 θ \theta θ与 t t t来优化下面这个优化问题:
min ∑ ∥ y i − f ( ∥ q i ∥ 2 ) q i ∥ 2 s u c h t h a t q i = R ( θ ) x i + t \begin{aligned} \min\quad&\sum{\big\|y_i-f(\|q_i\|^2)q_i\big\|^2}\\ \mathrm{such\ {}that}\quad&q_i=R(\theta)x_i+t \end{aligned} minsuch that∑∥∥yi−f(∥qi∥2)qi∥∥2qi=R(θ)xi+t
这里 R R R是一个二维的旋转矩阵,使用 θ \theta θ进行参数化。 t t t是一个二维向量。 f f f是一个外部的畸变函数distortion function
先考虑这个情况,首先有一个模板函数TemplatedComputeDistortion
来计算函数 f f f。那么对应的残差仿函数就很简单,如下所示:
template <typename T> T TemplatedComputeDistortion(const T r2) {const double k1 = 0.0082;const double k2 = 0.000023;return 1.0 + k1 * y2 + k2 * r2 * r2;//此处y2 应为 r2
}struct Affine2DWithDistortion {Affine2DWithDistortion(const double x_in[2], const double y_in[2]) {x[0] = x_in[0];x[1] = x_in[1];y[0] = y_in[0];y[1] = y_in[1];}template <typename T>bool operator()(const T* theta,const T* t,T* residuals) const {const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];const T f = TemplatedComputeDistortion(q_0 * q_0 + q_1 * q_1); // !!!residuals[0] = y[0] - f * q_0;residuals[1] = y[1] - f * q_1;return true;}double x[2];double y[2];
};
现在我们考虑三种不能直接应用自动微分的 f f f:
- 1.非模板函数
- 2.计算自身值与导数的函数
- 3.函数被定义成一个插值的函数表
下面依次考虑这三种情况.
A function that return its value(返回值的非模板函数)
假设给定一个方程ComputeDistortionValue
声明signature
如下
double ComputeDistortionValue(double r2)
这个函数计算了 f f f的值,函数内部的实现不重要,将这个函数与Affine2DWithDistortion
对接总共分三步:
- 1、将
ComputeDistortionValue
封装warp成仿函数ComputeDistortionValueFunctor
- 2、对
ComputeDistortionValueFunctor
使用’NumericDiffCostFunction’进行数值微分 - 3、使用
CostFunctionToFunctor
对CostFunction
对象进行封装,得到一个含有模板函数Operator()
的仿函数对象,这个仿函数对象将NumericDiffCostFunction
计算的Jacobian矩阵传送到Jet对象中
struct ComputeDistortionValueFunctor {bool operator()(const double* r2, double* value) const {*value = ComputeDistortionValue(r2[0]);return true;}
};struct Affine2DWithDistortion {Affine2DWithDistortion(const double x_in[2], const double y_in[2]) {x[0] = x_in[0];x[1] = x_in[1];y[0] = y_in[0];y[1] = y_in[1];compute_distortion.reset(new ceres::CostFunctionToFunctor<1, 1>(new ceres::NumericDiffCostFunction<ComputeDistortionValueFunctor,ceres::CENTRAL,1,1>(new ComputeDistortionValueFunctor)));}template <typename T>bool operator()(const T* theta, const T* t, T* residuals) const {const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];const T r2 = q_0 * q_0 + q_1 * q_1;T f;(*compute_distortion)(&r2, &f);residuals[0] = y[0] - f * q_0;residuals[1] = y[1] - f * q_1;return true;}double x[2];double y[2];std::unique_ptr<ceres::CostFunctionToFunctor<1, 1> > compute_distortion;
};
未完待续
这篇关于Ceres-Solver 官网教程翻译与学习的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!