Ceres


Ceres是由Google开发的开源C++通用非线性优化库,与g2o并列为目前视觉SLAM中应用最广泛的优化算法库。Ceres可以解决如下形式的有限边界最小二乘问题:
min ⁡ x 1 2 ∑ i ρ i ( ∥ f i ( x i 1 , x i 2 , … , x i k ) ∥ 2 ) s . t . l j ≤ x j ≤ u j \begin{aligned} &\min _{x}\dfrac{1}{2}\sum _{i}\rho_{i}\left( \left\| f_{i}\left( x_{i_{1}},x_{i_{2}},\ldots ,x_{ik}\right) \right\| ^{2}\right) \\ &{\rm s.t.}\quad l_j\leq x_{j}\leq u_{j}\end{aligned} xmin21iρi(fi(xi1,xi2,,xik)2)s.t.ljxjuj
其中 ρ i ( ∥ f i ( x i 1 , x i 2 , … , x i k ) ∥ 2 ) \rho_{i}\left( \left\| f_{i}\left( x_{i_{1}},x_{i_{2}},\ldots ,x_{ik}\right) \right\| ^{2}\right) ρi(fi(xi1,xi2,,xik)2)称为残差模块(residual bolck), ρ i ( ⋅ ) \rho_i(\cdot) ρi()称为损失函数(loss function),损失函数是标量函数,用于减少异常值对非线性最小二乘问题求解的影响。; { x i 1 , x i 2 , … , x i k } \{x_{i_{1}},x_{i_{2}},\ldots ,x_{ik}\} {xi1,xi2,,xik}称为参数模块(parameter blocks), l j l_j lj u j u_j uj分别为参数模块的上界和下界, f i ( ⋅ ) f_i(\cdot) fi()为参数模块对应的代价函数(cost function)。

可以发现,Ceres计算cost的公式与g2o有所不同,因此同样的优化问题,最终的cost一般是g2o的一半。

Ceres中的优化需要四步:

  1. 构建优化问题Problem类
  2. 构建优化的残差函数CostFunction
  3. 最小二乘问题构建,在每次获取到数据后添加残差块
  4. 求解最小二乘问题

Problem类

Ceres的求解过程包括构:建立最小二乘和求解最小二乘问题两部分,其中构建最小二乘问题的相关方法均包含在Ceres::Problem类中,涉及的成员函数主要包括:

  1. Problem::AddResidualBlock()
  2. Problem::AddParameterBlock()

AddResidualBlock()

AddResidualBlock() 顾名思义主要用于向Problem类传递残差模块的信息,函数原型如下,传递的参数主要包括:代价函数模块损失函数模块参数模块

ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function,
                                          LossFunction *loss_function,
                                          const vector<double *> parameter_blocks)

ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function,
                                          LossFunction *loss_function,
                                          double *x0, double *x1, ...)
  • 代价函数:包含了参数模块的维度信息,内部使用仿函数定义误差函数的计算方式。AddResidualBlock() 函数会检测传入的参数模块是否和代价函数模块中定义的维数一致,维度不一致时程序会强制退出。代价函数模块的详解参见Ceres详解(二)CostFunction
  • 损失函数:用于处理参数中含有野值的情况,避免错误量测对估计的影响,常用参数包括 HuberLossCauchyLoss 等(完整的参数列表参见Ceres API文档);该参数可以取 NULL nullptr,此时损失函数为单位函数。
  • 参数模块:待优化的参数,可一次性传入所有参数的指针容器 vector<double*> 或 依次传入所有参数的指针double*。

AddParameterBlock()

用户在调用 AddResidualBlock() 时其实已经隐式地向 Problem 传递了参数模块,但在一些情况下,需要用户显式地向 Problem 传入参数模块(通常出现在需要对优化参数进行重新参数化的情况,因为这个时候,优化的参数已经变了)。

void Problem::AddParameterBlock(double *values, int size)
void Problem::AddParameterBlock(double *values, int size, LocalParameterization *local_parameterization)
  • 第一种 函数原型除了会增加一些额外的参数检查之外,功能上 和 隐式传递参数并没有太大区别。
  • 第二种 函数原型则会额外传入 LocalParameterization 参数,用于重构优化参数的维数,这里我们着重讲解一下 LocalParameterization 类。

// 设定对应的参数模块在优化过程中保持不变
void Problem::SetParameterBlockConstant(double *values)
// 设定对应的参数模块在优化过程中可变
void Problem::SetParameterBlockVariable(double *values)
// 设定优化下界
void Problem::SetParameterLowerBound(double *values, int index, double lower_bound)
// 设定优化上界
void Problem::SetParameterUpperBound(double *values, int index, double upper_bound)
// 该函数紧跟在参数赋值后,在给定的参数位置求解Problem,给出当前位置处的cost、梯度以及Jacobian矩阵;
bool Problem::Evaluate(const Problem::EvaluateOptions &options, 
					   double *cost, vector<double>* residuals, 
					   vector<double> *gradient, CRSMatrix *jacobian)

CostFunction类

与其他非线性优化工具包一样,ceres的性能很大程度上依赖于导数计算的精度和效率。这部分工作在ceres中称为 CostFunction,ceres提供了许多种 CostFunction模板,较为常用的包括以下三种:

  1. 自动导数(AutoDiffCostFunction):由ceres自行决定导数的计算方式,最常用的求导方式。
  2. 数值导数(NumericDiffCostFunction):由用户手动编写导数的数值求解形式,通常在残差函数的计算使用无法直接调用的库函数,导致调用AutoDiffCostFunction类构建时使用;但手动编写的精度和计算效率不如模板类,因此不到不得已,官方并不建议使用该方法。
  3. 解析导数(Analytic Derivatives):当导数存在闭合解析形式时使用,用于可基于CostFunciton基类自行编写;但由于需要自行管理残差和雅克比矩阵,除非闭合解具有具有明显的精度和效率优势,否则同样不建议使用。详细内容参考

手动推导,效率比AutoDiffCostFunction略高。

AutoDiffCostFunction

可以看出,ceres官方极力推荐用户使用自动求导方式AutoDiffCostFunction,这里也主要以AutoDiffCostFunction为例说明。AutoDiffCostFunction为模板类,构造函数如下:

// CostFunctor: 仿函数(functor)类型
// residualDim: 残差维数
// paramDim: 参数维数(可以有多个)
// functor: 仿函数指针
ceres::AutoDiffCostFunction<CostFunctor, int residualDim, int paramDim>(CostFunctor* functor);

CostFunctor仿函数

仿函数的本质为结构体struct或者类class,由于重载了()运算符,使得其能够具有和函数一样的调用行为,因此被称为仿函数。
由于是仿函数,operator()运算符必须被重载。

求解最小二乘问题

ceres::Solve函数是Ceres求解最小二乘问题的核心函数,函数原型如下:

void Solve(const Solver::Options& options, Problem* problem, Solver::Summary* summary)

函数接受的三个参数分别为:求解选项Solver::Options、求解问题Problem以及求解报告Solver::Summary。

  • Solver::Summary只用于存储求解过程中的相关信息,并不影响求解器性能;

  • Solver::Options则是Ceres求解的核心,包括消元顺序、分解方法、收敛精度等在内的求解器所有行为均由Solver::Options控制。

Solver::Summary

Solver::Summary包含了求解器本身和求解中各变量的信息,许多成员函数与Solver::Options一致,详细列表同样请参阅API文档,这里只给出另外两个常用的成员函数:

  • BriefReport():输出单行的简单总结;
  • FullReport():输出多行的完整总结。
    现在我们来看本例中的Solver::Summary的使用:
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.FullReport() << "\n";// 输出完整的报告

Solver::Options

Solver::Options含有的参数种类繁多,API文档中对于每个参数的作用和意义都给出了详细的说明。由于在大多数情况下,绝大多数参数我们都会使用Ceres的默认设置,这里只列出一些常用或较为重要的参数。

  • minimizer_type:迭代求解方法,可选线搜索方法(LINEAR_SEARCH)或信赖域方法(TRUST_REGION),默认为TRUST_REGION方法;由于大多数情况我们都会选择LM或DOGLEG方法,该选项一般直接采用默认值;
  • trust_region_strategy_type:信赖域策略,可选LEVENBERG_MARQUARDTDOGLEG,默认为LEVENBERG_MARQUARDT
  • linear_solver_type:信赖域方法中求解线性方程组所使用的求解器类型,默认为DENSE_QR
  • linear_solver_ordering:线性方程求解器的消元顺序,默认为NULL,即由Ceres自行决定消元顺序;在以BA为典型代表的,对消元顺序有特殊要求的应用中,可以通过成员函数reset设定消元顺序,稍后将详细说明;
  • min_linear_solver_iteration/max_linear_solver_iteration:线性求解器的最小/最大迭代次数,默认为0/500,一般不需要更改;
  • max_num_iterations:求解器的最大迭代次数;
  • max_solver_time_in_seconds:求解器的最大运行秒数;
  • num_threads:Ceres求解时使用的线程数,在老版本的Ceres中还有一个针对线性求解器的线程设置选项num_linear_solver_threads,最新版本的Ceres中该选项已被取消;虽然为了保证程序的兼容性,用户依旧可以设置该参数,但Ceres会自动忽略该参数,并没有实际意义;
  • minimizer_progress_to_stdout:是否向终端输出优化过程信息,具体内容稍后详细说明;

linear_solver_type

在实际应用中,上述参数中对最终求解性能最大的就是线性方程求解器类型linear_solver_type和线程数num_threads,如果发现最后的求解精度或求解效率不能满足要求,应首先尝试更换这两个参数。

ceres solver里面定义了7种线性求解器(默认为DENSE_QR),分别是:

  • DENSE_QR:对于有一百多个优化变量或不到1000个残差项的小优化问题,如果Jacobian是相对稠密的,那么使用QR分解;
  • DENSE_NORMAL_CHOLESKY & SPARSE_NORMAL_CHOLESKY:Cholesky分解,用于具有稀疏性的大规模非线性最小二乘问题求解:ceres之dense cholesky和sparse cholesky求解器-CSDN博客;
    这两种方法用来解决大型的优化问题,由于大型优化问题中Jacobian经常有一堆0,如果非常稀疏我们用SPARSE_NORMAL_CHOLESKY,否则用
  • DENSE_NORMAL_CHOLESKY。 对于BA问题,可以使用SPARSE_NORMAL_CHOLESKY来求解。
  • DENSE_SCHUR & SPARSE_SCHUR:SCHUR分解,用于BA问题求解;
    由于BA的特殊结构,我们可以使用这两种方法,其中SPARSE_SCHUR效率更高。
  • CGNR:使用共轭梯度法求解稀疏方程;
  • ITERATIVE_SCHUR:使用共轭梯度SCHUR求解BA问题;

linear_solver_ordering

Ceres消元顺序的设置由linear_solver_ordering的reset函数完成,该函数接受参数为ParameterBlockOrdering对象。

该对象将所有待优化参数存储为带标记(ID)的组(Group),ID小的Group在求解线性方程的过程中会被首先消去。因此,我们需要做的第一个工作是调用其成员函数AddElementToGroup将参数添加到对应ID的Group中,函数原型为:

bool ParameterBlockOrdering::AddElementToGroup(const double *element, const int group)

接收的元素为变量数组的指针;组ID为非负整数,最小为0,如果该Id对应的Group不存在,则Ceres会自动创建。
下面我们来看一个BA中的例子:

ceres::ParameterBlockOrdering* ordering = new ceres::ParameterBlockOrdering();

// set all points in ordering to 0
for(int i = 0; i < num_points; i++){
    ordering->AddElementToGroup(points + i * point_block_size, 0);
}
// set all cameras in ordering to 1
for(int i = 0; i < num_cameras; i++){
    ordering->AddElementToGroup(cameras + i * camera_block_size, 1);
}

该例子中,所有路标点被分到了ID = 0组,而所有相机位姿被分到了ID = 1组,因此在线性方程组的求解中,所有路标点会变首先SCHUR消元。

接下来,我们就可以使用reset函数制定线性求解器的消元顺序了:

// set ordering in options
options->linear_solver_ordering.reset(ordering);

QuaternionManifold

QuaternionManifoldManifold的扰动模型不同,QuaternionManifold为全局扰动模型
⊞ ( x , Δ ) = exp ⁡ ( Δ ) ⊗ x ⊟ ( y , x ) = log ⁡ ( y ⊗ x − 1 ) \begin{aligned}\boxplus(x,\Delta)&=\exp{(\Delta)}\otimes x\\\boxminus(y,x)&=\log{(y\otimes x^{-1})}\end{aligned} (x,Δ)(y,x)=exp(Δ)x=log(yx1)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shilong Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值