Ceres solver 是谷歌开发的一款用于非线性优化的库,在谷歌的开源激光雷达slam项目cartographer中被大量使用。Ceres官网上的文档非常详细地介绍了其具体使用方法,相比于另外一个在slam中被广泛使用的图优化库G2O,ceres的文档可谓相当丰富详细(没有对比就没有伤害,主要是G2O资料太少了,对比起来就显得ceres的很多),下面我就介绍下如何使用ceres库进行简单的非线性优化,给各位ceres的初学者一个低门槛的入门教程,能比较直观地对使用ceres库求解优化问题的过程有一个清晰的了解。话不多说,开整。
使用Ceres求解非线性优化问题,一共分为三个部分:
1、 第一部分:构建cost fuction,即代价函数,也就是寻优的目标式。这个部分需要使用仿函数(functor)这一技巧来实现,做法是定义一个cost function的结构体,在结构体内重载()运算符,具体实现方法后续介绍。
2、 第二部分:通过代价函数构建待求解的优化问题。
3、 第三部分:配置求解器参数并求解问题,这个步骤就是设置方程怎么求解、求解过程是否输出等,然后调用一下Solve方法。
好了,此时你应该对ceres的大概使用流程有了一个基本的认识。下面我就基于ceres官网上的教程中的一个例程来详细介绍一下ceres的用法。
Ceres官网教程给出的例程中,求解的问题是求x使得 12(10−x)2 取到最小值。(很容易心算出x的解应该是10)
好,来看代码:
#include
#include
using namespace std;
using namespace ceres;
//第一部分:构建代价函数,重载()符号,仿函数的小技巧
struct CostFunctor {
template <typename T>
bool operator()(const T* const x, T* residual) const {
residual[0] = T(10.0) - x[0];
return true;
}
};
//主函数
int main(int argc, char** argv) {
google::InitGoogleLogging(argv[0]);
// 寻优参数x的初始值,为5
double initial_x = 5.0;
double x = initial_x;
// 第二部分:构建寻优问题
Problem problem;
CostFunction* cost_function =
new AutoDiffCostFunction1, 1>(new CostFunctor); //使用自动求导,将之前的代价函数结构体传入,第一个1是输出维度,即残差的维度,第二个1是输入维度,即待寻优参数x的维度。
problem.AddResidualBlock(cost_function, NULL, &x); //向问题中添加误差项,本问题比较简单,添加一个就行。
//第三部分: 配置并运行求解器
Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR; //配置增量方程的解法
options.minimizer_progress_to_stdout = true;//输出到cout
Solver::Summary summary;//优化信息
Solve(options, &problem, &summary);//求解!!!
std::cout << summary.BriefReport() << "\n";//输出优化的简要信息
//最终结果
std::cout << "x : " << initial_x
<< " -> " << x << "\n";
return 0;
}
这里的使用了仿函数的技巧,即在CostFunction结构体内,对()进行重载,这样的话,该结构体的一个实例就能具有类似一个函数的性质,在代码编写过程中就能当做一个函数一样来使用。
关于仿函数,这里再多说几句,对结构体、类的一个实例,比如Myclass类的一个实例Obj1,如果Myclass里对()进行了重载,那Obj1被创建之后,就可以将Obj1这个实例当做函数来用,比如Obj(x)这样,为了方便读者理解,下面随便编一段简单的示例代码,凑活看看吧。
//仿函数的示例代码
#include
using namespace std;
class Myclass
{
public:
Myclass(int x):_x(x){};
int operator()(const int n)const{
return n*_x;
}
private:
int _x;
};
int main()
{
Myclass Obj1(5);
cout<3)<"pause");
return 0;
}
在我随便写的示教代码中,可以看到我将Myclass的()符号的功能定义成了将括号内的数n乘以隐藏参数x倍,其中x是Obj1对象的一个私有成员变量,是是在构造Obj1时候赋予的。因为重载了()符号,所以在主函数中Obj1这个对象就可以当做一个函数来使用,使用方法为Obj1(n),如果Obj1的内部成员变量_x是5,则此函数功能就是将输入参数扩大5倍,如果这个成员变量是50,Obj1()函数的功能就是将输入n扩大50倍,这也是仿函数技巧的一个优点,它能利用对象的成员变量来储存更多的函数内部参数。
了解了仿函数技巧的使用方法后,再回过头来看看ceres使用中构造CostFuction 的具体方法:
CostFunction结构体中,对括号符号重载的函数中,传入参数有两个,一个是待优化的变量x,另一个是残差residual,也就是代价函数的输出。
重载了()符号之后,CostFunction就可以传入AutoDiffCostFunction方法来构建寻优问题了。
Problem problem;
CostFunction* cost_function =
new AutoDiffCostFunction1, 1>(new CostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
这一部分就是待求解的优化问题的构建过程,使用之前结构体创建一个实例,由于使用了仿函数技巧,该实例在使用上可以当做一个函数。基于该实例new了一个CostFunction结构体,这里使用的自动求导,将之前的代价函数结构体传入,第一个1是输出维度,即残差的维度,第二个1是输入维度,即待寻优参数x的维度。分别对应之前结构体中的residual和x。
向问题中添加误差项,本问题比较简单,添加一次就行(有的问题要不断多次添加ResidualBlock以构建最小二乘求解问题)。这里的参数NULL是指不使用核函数,&x表示x是待寻优参数。
Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR;
options.minimizer_progress_to_stdout = true;
Solver::Summary summary;
Solve(options, &problem, &summary);
std::cout << summary.BriefReport() << "\n";
std::cout << "x : " << initial_x
<< " -> " << x << "\n";
这一部分很好理解,创建一个Option,配置一下求解器的配置,创建一个Summary。最后调用Solve方法,求解。
最后输出结果:
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time
0 4.512500e+01 0.00e+00 9.50e+00 0.00e+00 0.00e+00 1.00e+04 0 5.33e-04 3.46e-03
1 4.511598e-07 4.51e+01 9.50e-04 9.50e+00 1.00e+00 3.00e+04 1 5.00e-04 4.05e-03
2 5.012552e-16 4.51e-07 3.17e-08 9.50e-04 1.00e+00 9.00e+04 1 1.60e-05 4.09e-03
Ceres Solver Report: Iterations: 2, Initial cost: 4.512500e+01, Final cost: 5.012552e-16, Termination: CONVERGENCE
x : 0.5 -> 10
读者们看到这里相信已经对Ceres库的使用已经有了一个大概的认识,现在可以试着将代码实际运行一下来感受一下,加深一下理解。
博主的使用环境为Ubuntu 16.04,所以在此附上CMakeList.txt,怎么样,是不是很贴心:)。
附:CMakeLists.txt代码:
//CMakeLists.txt:
cmake_minimum_required(VERSION 2.8)
project(ceres)
find_package(Ceres REQUIRED)
include_directories(${CERES_INCLUDE_DIRS})
add_executable(use_ceres use_ceres.cpp)
target_link_libraries(use_ceres ${CERES_LIBRARIES})
在上面的例子中,使用的是自动求导法(AutoDiffCostFunction),Ceres库中其实还有更多的求导方法可供选择(虽然自动求导的确是最省心的,而且一般情况下也是最快的。。。)。这里就简要介绍一下其他的求导方法:
数值求导法(一般比自动求导法收敛更慢,且更容易出现数值错误):
数值求导法的代价函数结构体构建和自动求导中的没有区别,只是在第二部分的构建求解问题中稍有区别,下面是官网给出的数值求导法的问题构建部分代码:
CostFunction* cost_function =
new NumericDiffCostFunction<NumericDiffCostFunctor, ceres::CENTRAL, 1, 1>(
new NumericDiffCostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
乍一看和自动求导法中的代码没区别,除了代价函数结构体的名字定义得稍有不同,使用的是NumericDiffCostFunction而非AutoDiffCostFunction,改动的地方只有在模板参数设置输入输出维度前面加了一个模板参数ceres::CENTRAL,表明使用的是数值求导法。
还有其他一些更多更复杂的求导法,不详述。
趁热打铁,阅读到这里想必读者们应该已经对Ceres库的使用已经比较了解了(如果前面认真看了的话),现在就来尝试解决一个更加复杂的问题来检验一下成果,顺便进阶一下。
问题:
拟合非线性函数的曲线(和官网上的例子不一样,稍微复杂一丢丢):
y=e3x2+2x+1
依然,先上代码:
代码之前先啰嗦几句,整个代码的思路还是先构建代价函数结构体,然后在[0,1]之间均匀生成待拟合曲线的1000个数据点,加上方差为1的白噪声,数据点用两个vector储存(x_data和y_data),然后构建待求解优化问题,最后求解,拟合曲线参数。
(PS. 本段代码中使用OpenCV的随机数产生器,要跑代码的同学可能要先装一下OpenCV)
#include
#include
#include
using namespace std;
using namespace cv;
//构建代价函数结构体,abc为待优化参数,residual为残差。
struct CURVE_FITTING_COST
{
CURVE_FITTING_COST(double x,double y):_x(x),_y(y){}
template <typename T>
bool operator()(const T* const abc,T* residual)const
{
residual[0]=_y-ceres::exp(abc[0]*_x*_x+abc[1]*_x+abc[2]);
return true;
}
const double _x,_y;
};
//主函数
int main()
{
//参数初始化设置,abc初始化为0,白噪声方差为1(使用OpenCV的随机数产生器)。
double a=3,b=2,c=1;
double w=1;
RNG rng;
double abc[3]={0,0,0};
//生成待拟合曲线的数据散点,储存在Vector里,x_data,y_data。
vector<double> x_data,y_data;
for(int i=0;i<1000;i++)
{
double x=i/1000.0;
x_data.push_back(x);
y_data.push_back(exp(a*x*x+b*x+c)+rng.gaussian(w));
}
//反复使用AddResidualBlock方法(逐个散点,反复1000次)
//将每个点的残差累计求和构建最小二乘优化式
//不使用核函数,待优化参数是abc
ceres::Problem problem;
for(int i=0;i<1000;i++)
{
problem.AddResidualBlock(
new ceres::AutoDiffCostFunction1,3>(
new CURVE_FITTING_COST(x_data[i],y_data[i])
),
nullptr,
abc
);
}
//配置求解器并求解,输出结果
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<<"a= "<0]<cout<<"b= "<1]<cout<<"c= "<2]<return 0;
}
}
代码解读:
代码的整体流程还是之前的流程,四个部分大致相同。比之前稍微复杂一点的地方就在于计算单个点的残差时需要输入该点的x,y坐标,而且需要反复多次累计单点的残差以构建总体的优化目标。
先看代价函数结构体的构建:
struct CURVE_FITTING_COST
{
CURVE_FITTING_COST(double x,double y):_x(x),_y(y){}
template <typename T>
bool operator()(const T* const abc,T* residual)const
{
residual[0]=_y-ceres::exp(abc[0]*_x*_x+abc[1]*_x+abc[2]);
return true;
}
const double _x,_y;
};
这里依然使用仿函数技巧,与之前不同的是结构体内部有_x,_y成员变量,用于储存散点的坐标。
再看优化问题的构建:
ceres::Problem problem;
for(int i=0;i<1000;i++)
{
//自动求导法,输出维度1,输入维度3,
problem.AddResidualBlock(
new ceres::AutoDiffCostFunction,1,3>(
new CURVE_FITTING_COST(x_data[i],y_data[i])
),
nullptr,
abc
);
}
这里由于有1000个点,所以需要对每个点计算一次残差,将所有残差累积在一起构成问题的总体优化目标,所以for循环1000次。
这里与前例不同的是需要输入散点的坐标x,y,由于_x,_y是结构体成员变量,所以可以通过构造函数直接对这两个值赋值。本代码里也是这么用的。
最终的运行结果是:
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time
0 5.277388e+06 0.00e+00 5.58e+04 0.00e+00 0.00e+00 1.00e+04 0 3.94e-04 7.06e-04
1 5.277388e+06 -4.29e+238 0.00e+00 7.39e+02 -8.79e+231 5.00e+03 1 3.32e-04 1.27e-03
2 5.277388e+06 -1.09e+238 0.00e+00 7.32e+02 -2.24e+231 1.25e+03 1 2.48e-04 1.60e-03
3 5.277388e+06 -5.13e+234 0.00e+00 6.96e+02 -1.05e+228 1.56e+02 1 2.18e-04 1.89e-03
4 5.277388e+06 -1.42e+215 0.00e+00 4.91e+02 -2.97e+208 9.77e+00 1 2.17e-04 2.18e-03
5 5.277388e+06 -9.61e+166 0.00e+00 1.85e+02 -2.23e+160 3.05e-01 1 2.41e-04 2.49e-03
6 5.277388e+06 -7.19e+60 0.00e+00 4.59e+01 -2.94e+54 4.77e-03 1 2.15e-04 2.77e-03
7 5.061060e+06 2.16e+05 2.68e+05 1.21e+00 2.52e+00 1.43e-02 1 5.03e-04 3.34e-03
8 4.342234e+06 7.19e+05 9.34e+05 8.84e-01 2.08e+00 4.29e-02 1 5.25e-04 4.47e-03
9 2.876001e+06 1.47e+06 2.06e+06 6.42e-01 1.66e+00 1.29e-01 1 5.01e-04 5.06e-03
10 1.018645e+06 1.86e+06 2.58e+06 4.76e-01 1.38e+00 3.86e-01 1 5.59e-04 5.69e-03
11 1.357731e+05 8.83e+05 1.30e+06 2.56e-01 1.13e+00 1.16e+00 1 5.51e-04 1.23e-02
12 2.142986e+04 1.14e+05 2.71e+05 8.60e-02 1.03e+00 3.48e+00 1 5.40e-04 1.34e-02
13 1.636436e+04 5.07e+03 5.94e+04 3.01e-02 1.01e+00 1.04e+01 1 5.14e-04 1.41e-02
14 1.270381e+04 3.66e+03 3.96e+04 6.21e-02 9.96e-01 3.13e+01 1 1.85e-03 1.60e-02
15 6.723500e+03 5.98e+03 2.68e+04 1.30e-01 9.89e-01 9.39e+01 1 5.37e-04 1.67e-02
16 1.900795e+03 4.82e+03 1.24e+04 1.76e-01 9.90e-01 2.82e+02 1 5.76e-04 1.74e-02
17 5.933860e+02 1.31e+03 3.45e+03 1.23e-01 9.96e-01 8.45e+02 1 6.41e-04 1.81e-02
18 5.089437e+02 8.44e+01 3.46e+02 3.77e-02 1.00e+00 2.53e+03 1 5.98e-04 1.88e-02
19 5.071157e+02 1.83e+00 4.47e+01 1.63e-02 1.00e+00 7.60e+03 1 7.68e-04 1.97e-02
20 5.056467e+02 1.47e+00 3.03e+01 3.13e-02 1.00e+00 2.28e+04 1 6.21e-04 2.05e-02
21 5.046313e+02 1.02e+00 1.23e+01 3.82e-02 1.00e+00 6.84e+04 1 6.08e-04 2.13e-02
22 5.044403e+02 1.91e-01 2.23e+00 2.11e-02 9.99e-01 2.05e+05 1 5.57e-04 2.21e-02
23 5.044338e+02 6.48e-03 1.38e-01 4.35e-03 9.98e-01 6.16e+05 1 6.23e-04 2.28e-02
a= 3.01325
b= 1.97599
c= 1.01113
可以看到,最终的拟合结果与真实值非常接近。
求解优化问题中(比如拟合曲线),数据中往往会有离群点、错误值什么的,最终得到的寻优结果很容易受到影响,此时就可以使用一些损失核函数来对离群点的影响加以消除。要使用核函数,只需要把上述代码中的NULL或nullptr换成损失核函数结构体的实例。
Ceres库中提供的核函数主要有:TrivialLoss 、HuberLoss、 SoftLOneLoss 、 CauchyLoss。
比如此时要使用CauchyLoss,只需要将nullptr换成new CauchyLoss(0.5)就行(0.5为参数)。
下面两图别为Ceres官网上的例程的结果,可以明显看出使用损失核函数之后的曲线收离群点的影响更小。