ceres库使用说明
内容大部分来自ceres官网
[TOC]
1. 非线性最小二乘
1.1 介绍
Ceres可以解决形式的边界受约束的非线性最小二乘问题:
这种形式的问题在科学和工程领域广泛应用 :从统计学中的拟合曲线到从计算机视觉中的照片构建3D模型。在本节中,我们将学习如何解决使用Ceres Solver。
表达式$ \rho_{i}\left(\left|f_{i}\left(x_{i_{1}}, \ldots, x_{i_{k}}\right)\right|^{2}\right) $被称为ResidualBlock
,其中$ f_{i}(\cdot) $是一个CostFunction
,它取决于参数块$\left[x_{i_{1}}, \dots, x_{i_{k}}\right]$。在大多数优化问题中,一组标量一起出现。例如,平移向量的三个分量和四元数的四个分量定义摄像机的姿势。我们将这样一组小标量称为ParameterBlock
。当然,ParameterBlock
可以只是一个参数。$l_{j}$和$u_{j}$是参数块$x_{j}$的边界。
$\rho_{i}$是一个LossFunction
。LossFunction
是一个标量函数(即我们通常所说的核函数——见slam14讲),用于减少异常值对非线性最小二乘问题解的影响。作为一种特殊情况,当$\rho_{i}(x)=x$,即同一性函数,并且$l_{j}=-\infty$和$u_{j}=-\infty$时,我们得到更熟悉的非线性最小二乘问题。
1.2 Hello World!
最开始,先考虑找到函数最小值的问题:
这是一个很简单的问题,其最小值位于$x=10$,它可以比较直观的用ceres进行求解。
第一步是编写一个仿函数(functor),对函数$f(x)=10-x$设为初始残差:
1 | struct CostFunctor { |
这里需要注意的重要一点是operator()
是一个模板化方法,它假定它的所有输入和输出都是某种类型T
。这里使用模板允许Ceres调用CostFunctor :: operator <T>()
,当需要残差值时,T = double
,当需要雅可比行列时,使用特殊类型T = Jet
。在 Derivatives章节中,我们将更详细地讨论向Ceres供应派生的各种方法。一旦我们有了计算残差函数的方法,就可以用它来构造一个非线性最小二乘问题并让Ceres解决它。
1 | int main(int argc, char** argv) { |
AutoDiffCostFunction
将CostFunctor
作为输入,自动区分它并为其提供CostFunction
接口。编译和运行该函数得到以下结果:
1 | iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time |
从$x=5$开始,两次迭代中的求解器变为10。细心的读者会注意到这是一个线性问题,一个线性求解应该足以得到最优值。求解器的默认配置针对非线性问题,为简单起见,我们在此示例中未对其进行更改。确实可以在一次迭代中使用Ceres获得该问题的解决方案。还要注意,求解器在第一次迭代中确实得到误差接近0的最佳函数值。当我们讨论Ceres的收敛和参数设置时,我们将更详细地讨论这些问题。
1.3 衍生
像大多数优化包一样,Ceres Solver依赖于在任意参数值下评估目标函数中每个项的值和导数。正确有效地这样做对于获得好结果至关重要。Ceres Solver提供了许多方法。我们现在考虑另外两种可能性:解析解和数值解。
1.4 数值解
在某些情况下,无法定义模板cost functor
,例如,当残差的评估涉及对您无法控制的库函数的调用时。在这种情况下,可以使用数值微分。用户定义了一个算子,它计算残差值并使用它构造NumericDiffCostFunction
。例如,对于$f(x)=10-x $,相应的仿函数将是
1 | struct NumericDiffCostFunctor { |
将其添加到Problem
中:
1 | CostFunction* cost_function = |
注意与我们使用AutoDiffCostFunction
时的差异:
1 | CostFunction* cost_function = |
除了额外的模板参数指示用于计算数值导数的有限差分方案的类型外,该构造看起来几乎与用AutoDiffCostFunction
的构造相同。
一般来说,我们建议用AutoDiffCostFunction
而不是NumericDiffCostFunction
。C ++模板的使用使AutoDiffCostFunction
很高效,而NumericDiffCostFunction
很费时,而且容易出现数值错误,并导致收敛速度变慢。
1.5 解析解
在某些情况下,使用AutoDiffCostFunction
是不可行的。例如,解析解计算封闭形式导数比依赖于自动微分代码计算的更有效。在这种情况下,可以提供自己的残差和雅可比计算代码。为此,如果在编译时知道参数和残差的大小,请定义CostFunction
或SizedCostFunction
的子类。例如,下面是实现$ f(x)=10-x $的SimpleCostFunction
。
1 | class QuadraticCostFunction : public ceres::SizedCostFunction<1, 1> { |
QuadraticCostFunction :: Evaluate
提供了输入数组parameters
,输出数组residuals
和jacobians``。jacobians
数组是可选的,Evaluate
应该检查它何时为非null,如果为null,则用剩余函数的导数值填充它。在这种情况下,由于残余函数是线性的,雅可比矩阵是常数。从上面的代码片段可以看出,实现CostFunction
对象有点冗长。我们建议除非您有充分的理由自己管理雅可比计算,否则使用AutoDiffCostFunction
或NumericDiffCostFunction
来构造残差块。
计算数值解和解析解是迄今为止使用Ceres最复杂的部分,并且根据环境,用户可能需要更复杂的计算方法。一旦熟悉使用NumericDiffCostFunction
和AutoDiffCostFunction
,就可以继续查看DynamicAutoDiffCostFunction
,CostFunctionToFunctor
,NumericDiffFunctor
和ConditionedCostFunction
,以获得构建和计算成本函数的更高级方法。
1.6 Powell’s Function
现在考虑一个稍微复杂的例子 : Powell’s Function的最小化。设$x=\left[x_{1}, x_{2}, x_{3}, x_{4}\right]$和
$F(x)$是有四个参数的函数,有四个残差,我们希望找到$x$,使得$\frac{1}{2}|F(x)|^{2}$最小化。
同样,第一步是定义用于仿函数。以下是评估$f_{4}\left(x_{1}, x_{4}\right)$的代码:
1 | struct F4 { |
类似地,我们可以定义类F1
,F2
和F3
来分别评估$ f_1(x1,x2)$,$ f_2(x3,x4)$和$ f_3(x2,x3)$ 。这样,问题可以构建如下:
1 | double x1 = 3.0; double x2 = -1.0; double x3 = 0.0; double x4 = 1.0; |
请注意,每个ResidualBlock
仅取决于相应残留对象所依赖的两个参数,而不取决于所有四个参数。编译和运行程序后得到:
1 | Initial x1 = 3, x2 = -1, x3 = 0, x4 = 1 |
很容易看出这个问题的最优解是$x_1 = 0,x_2 = 0,x_3 = 0,x_4 = 0$,目标函数值为0。在10次迭代中,Ceres找到了一个目标函数值为$4×10^{-12}$的解。
上述问题所有代码如下:
1 | // Author: sameeragarwal@google.com (Sameer Agarwal) |
1.7 曲线拟合
我们到目前为止看到的例子是没有数据的简单优化问题。最小二乘和非线性最小二乘分析的最初目的是拟合数据曲线。我们现在考虑这样一个问题,它对曲线$y=e^{0.3 x+0.1}$进行采样并添加标准偏差$σ=0.2$的高斯噪声而生成的数据。让我们将一些数据拟合到曲线中
我们首先定义一个模板化对象来评估残差。每次观察都会有残差。
1 | struct ExponentialResidual { |
假设观察结果是一个称为data
的$2n$大小的数组,该问题的构造是为每个观察创建一个CostFunction
的简单问题。
1 | double m = 0.0; |
编译运行得到:
1 | iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time |
从参数值$m = 0,c = 0$开始,初始目标函数值为$121.173121.173$ 。Ceres找到一个解$m = 0.291861,c = 0.131439$,此时目标函数值$1.056751.05675$。这些值与原始模型$m = 0.3,c = 0.1$的参数略有不同,但误差并不大,是符合预期的。当从噪声数据重建曲线时,我们期望看到这种偏差。实际上,如果你要评估$m = 0.3,c = 0.1$的目标函数,其实拟合值更差,目标函数值为1.0824251.082425。下图说明了拟合度:
下面为曲线拟合的完整代码
1 |
|
1.8 鲁棒曲线拟合
现在假设我们给出的数据有一些异常值,即我们有一些不遵守噪声模型的点。如果我们使用上面的代码来拟合这些数据,我们会得到如下所示的拟合。注意拟合曲线如何偏离真实值。
为了处理异常值,常用方法是使用LossFunction
。损失函数减少具有高残差的残差块(即异常值)的影响,通常是与异常值相对应的残差。为了将损失函数与残差块相关联,我们把
1 | problem.AddResidualBlock(cost_function, NULL , &m, &c); |
改为了
1 | problem.AddResidualBlock(cost_function, new CauchyLoss(0.5) , &m, &c); |
CauchyLoss
是Ceres Solver附带的损失函数之一。参数$0.5$指定损失函数的比例。结果,我们得到了下面的拟合。注意拟合曲线如何向后靠近真实值曲线移动。
1.9 光束平差法
作者写Ceres库的主要原因之一是解决大规模的光束平差问题。
给定一组图像特征的位置和对应关系,光束平差的目标是找到最小化重投影误差的3D点位置和相机参数。该优化问题通常被表述为非线性最小二乘问题,其中误差是观察到的特征位置与相机图像平面上的相应3D点的投影之间的差的平方$L2$范数。Ceres比较好的支持解决光束平差问题。
让我们使用BAL数据集中解决问题。
通常的第一步是定义一个计算重投影误差/残差的模板化仿函数(templated functor)。仿函数的结构类似于ExponentialResidual
,因为该对象的实例负责每个图像。
BAL问题中的每个残差取决于三维点和有9个参数的相机。定义摄像机的9个参数是:三个用于旋转(作为Rodriques的轴角矢量),三个用于平移,一个用于焦距,两个用于径向畸变。可以在Bundler主页和BAL主页上找到此相机型号的详细信息。
1 | struct SnavelyReprojectionError { |
请注意,与之前的示例不同,这是一个不平凡的函数,计算其解析雅可比行列式有点痛苦。自动求导会使程序编写更加简单。函数AngleAxisRotatePoint()
和其他用于操作旋转的函数可以在include / ceres / rotation.h中找到。
给定这个仿函数,光束平差问题可以构造如下:
1 | ceres::Problem problem; |
请注意,光束平差问题的构造与曲线拟合示例非常相似 , 每观察一次就把一项误差添加到目标函数中。由于这是一个很大的稀疏问题(对于DENSE_QR
方法是很大啦),解决这个问题的一种方法是将Solver :: Options :: linear_solver_type
设置为SPARSE_NORMAL_CHOLESKY
并调用Solve()
。虽然这是一个合理的事情,但是光束平差问题有一个特殊的稀疏结构,可以利用它来更有效地解决它们。Ceres为此任务提供了三种专用求解器(统称为基于Schur的求解器)。示例代码使用最简单的DENSE_SCHUR
。
1 | ceres::Solver::Options options; |
下面代码是一个更复杂的光束平差示例,它演示了如何使用Ceres的更高级功能,包括各种线性求解器,强大的损失函数和本地参数化。
1 | // An example of solving a dynamically sized problem with various |