Ceres是什么?

学SLAM的同学可能都听说过Ceres,但是大多数可能都和现在的我一样,对其仅仅停留在一知半解的程度。现在一起从0开始去搞定Ceres吧!没有安装的同学先安装CeresCeres可以解决如下形式的“带边界约束的鲁棒的非线性最小二乘问题”(bounds constrained robustified non-linear least squares problems)
ceres 常用 ceres 1_linux
其中ceres 常用 ceres 1_linux_02是需要最小化的损失。
ceres 常用 ceres 1_main函数_03为是基于输入ceres 常用 ceres 1_ceres 常用_04的代价函数也可以叫残差块,ceres 常用 ceres 1_main函数_05是损失函数也可以叫核函数——用于减少异常值对求解非线性最小二乘问题的影响,ceres 常用 ceres 1_最小二乘_06ceres 常用 ceres 1_linux_07ceres 常用 ceres 1_ceres 常用_08的边界。

解决个简单的问题吧!

我们用一个简单的例子,求解ceres 常用 ceres 1_最小二乘_09的最小值,来迈出学习Ceres的第一步。
首先,我们需要给出函数ceres 常用 ceres 1_ceres 常用_10的实现:

struct CostFunctor
{
    template <typename T>
    bool operator()(const T* const x, T* residual) const{
        residual[0] = 10.0 - x[0];
        return true;
    }
};

不了解tmplate模板的同学可以看看这篇C++ Template 教學 结构体CostFunctor的主要作用是,通过重写 "()"运算符 使其表现为一个对 类型T的输入x 求 类型为T的残差residual 的函数,一旦我们有了这个函数,就可以使用Ceres构建一个非线性最小二乘问题求解原问题啦,主函数的内容如下

int main(int argc, char** argv){
    double initial_x=5.0;
    double x=initial_x;

    ceres::Problem problem;

    ceres::CostFunction* cost_function = 
        new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);

    problem.AddResidualBlock(cost_function, nullptr, &x);

    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);

    cout << summary.BriefReport() << endl;
    cout << "x: " << initial_x << " -> " << x << endl;
    return 0;
}

这里用到ceres的几个类。

ceres::Problem
用来表示非线性最小二乘问题的一个类,并专门被用来求解稀疏的最小二乘问题(了解SLAM的同学可能知道求解后端图优化问题过程中的H矩阵是稀疏的)。一个非线性最小二乘问题最重要的两个部分就是参数和残差的设置,分别对应成员方法AddParameterBlock、AddResidualBlock。
main函数中用到了AddResidualBlock设定了非线性最小二乘问题的代价函数、损失函数、参数块指针(CostFunction、nullptr、x的地址)

ceres::CostFunction
用来表示代价计算函数的一个类,是用户将自己描述的最小二乘问题用于Ceres的接口

ceres::AutoDiffCostFunction
用来表示利用自动求导框架计算导数(雅克比矩阵)的Cost函数类,SizedCostFunction的子类,而SizedCostFunction是CostFunction的子类。模板参数列表为

// 对应main函数中的<CostFunctor, 1, 1>
template <typename CostFunctor,	// 自己定义的CostFunctor结构体
          int kNumResiduals,  					// 残差的维度
          int... Ns>											// 参数块每个成员的维度

ceres::Solver
用来表示非线性最小二乘问题的求解器的类,内有成员变量Options、Summary和成员方法Solve,在之后都有用到
ceres::Solver::Options
用来表示如何求解最小二乘问题的一些设置的结构体,main函数中用到的
linear_solver_type:求解线性方程的求解器
minimizer_progress_to_stdout:日志输出格式
ceres::Solver::Summary
用来记录求解最小二乘问题过程中的参数变化的结构体,BriefReport()表示求解完成后用一行简短地描述状态。

ceres::Solve
同ceres::Slover类中的Solve方法应该相同,都在solver.h文件中定义,只不过不是成员函数而是直接的一个函数,表示执行最小二乘问题的求解

结果

程序运行的结果如下图所示,本篇文章的源程序可以去我的github下,CeresLearning中tag名字为Ceres_Turotial_01的那文件

ceres 常用 ceres 1_slam_11

疑问

1、残差residual和损失loss到底是什么?怎么区分?

个人理解,残差对应的是代价/残差函数ceres 常用 ceres 1_main函数_03计算的结果,其往往是二次的形式;而损失对应的是残差函数的结果再经过损失/鲁棒核函数ceres 常用 ceres 1_main函数_13计算的结果,其对残差的形状做出了改变。
物理意义上残差很好理解,其直观地表示了我们当前的误差大小。而由于数据问题或者误差的存在,用二次形式表示的残差有时会受到极大的影响导致我们的求解无法顺利进行(比如在SLAM后端优化的BA问题求解中,错误的匹配会使得残差变得很大导致这对匹配话语权很大,而这明显是错误的)。此时核函数的引入可以有效改善这种情况。
当不设置损失函数时,残差和损失可以看做一个东西(大概???)

错误

LearnCeres: ../nptl/pthread_mutex_lock.c:81: __pthread_mutex_lock: Assertion `mutex->__data.__owner == 0' failed.
已放弃 (核心已转储

一开始运行上面的代码会报和线程有关的错误,仔细排查后不知道为何。
此时我用的老版本ceres_solver1.14.0,取github下载并安装最新版后就没有这个线程问题了。Ceres安装

装完还有一个版本问题,在cmakelist里面把

set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )

改为

set( CMAKE_CXX_FLAGS "-std=c++14 -O3" )

即可。