@iStarLee
2019-12-07T03:54:15.000000Z
字数 8803
阅读 1762
Optimization
8.9 关于添加残差块,参数块,设置参数化的区别和调用关系【重要】
这一节相当重要,尤其是写代码的时候。
在旧版本的ceres里面,参数块最大是10个,但是新版本的ceres使用了可变长模板参数,所以参数快数量不限制。
CostFunction的存在是为了计算残差和雅克比矩阵。
主要接口。
class CostFunction {public:virtual bool Evaluate(double const* const* parameters,double* residuals,double** jacobians) = 0;const vector<int32>& parameter_block_sizes();int num_residuals() const;protected:vector<int32>* mutable_parameter_block_sizes();void set_num_residuals(int num_residuals);};
class AnalyticCostFunction: public ceres::SizedCostFunction<1 /* number of residuals */,2 /* size of first parameter */>{...}
class AnalyticCostFunction2: public ceres::CostFunction {public:AnalyticCostFunction2(const int& num_residuals, const int& block_sizes,double x, double y) : x_(x), y_(y) {set_num_residuals(num_residuals); // 设置残差维度std::vector<int>* param_block_sizes = mutable_parameter_block_sizes(); // 返回的引用param_block_sizes->push_back(block_sizes); // 设置参数块维度,有几个参数块,就pubsh_back几次}...}// 使用CostFunction *cost_function = new AnalyticCostFunction2(1,2,data[2*i], data[2*i + 1]);problem.AddResidualBlock(cost_function, NULL, x);
其实通过查看SizedCostFunction也是继承了CostFunction的, 通过查看其构造函数可以发现我们刚才直接继承CostFunction构建functor的方法其实和SizedCostFunction是一样的。
只不过Ceres已经帮我们做好了,以后我们只使用SizedCostFunction就好了,很方便。
SizedCostFunction() {set_num_residuals(kNumResiduals);*mutable_parameter_block_sizes() = std::vector<int32_t>{Ns...};}
//-------------------------------------------// 解析求导方式1//-------------------------------------------class AnalyticCostFunction: public ceres::SizedCostFunction<1 /* number of residuals */,2 /* size of first parameter */> {public:AnalyticCostFunction(double x, double y) : x_(x), y_(y) {}virtual ~AnalyticCostFunction() {}/*** @brief 重载Evaluate函数,完成jacobian和residuals的计算* @param parameters* @param residuals* @param jacobians* @return*/virtual bool Evaluate(double const *const *parameters,double *residuals,double **jacobians) const {double m = parameters[0][0]; // parameters[0]表示取出第一组参数double c = parameters[0][1];// 计算残差residuals[0] = y_ - exp(m*x_ + c);// 计算雅克比if (jacobians != NULL && jacobians[0] != NULL) {jacobians[0][0] = -x_*exp(m*x_ + c);jacobians[0][2] = -exp(m*x_ + c);}return true;}private:const double x_;const double y_;};//-------------------------------------------// 解析求导方式2//-------------------------------------------class AnalyticCostFunction2: public ceres::CostFunction {public:AnalyticCostFunction2(const int& num_residuals, const int& block_sizes,double x, double y) : x_(x), y_(y) {set_num_residuals(num_residuals); // 设置残差维度std::vector<int>* param_block_sizes = mutable_parameter_block_sizes(); // 返回的引用param_block_sizes->push_back(block_sizes); // 设置参数块维度,有几个参数块,就pubsh_back几次}virtual ~AnalyticCostFunction2() {}/*** @brief 重载Evaluate函数,完成jacobian和residuals的计算* @param parameters* @param residuals* @param jacobians* @return*/virtual bool Evaluate(double const *const *parameters,double *residuals,double **jacobians) const {double m = parameters[0][0]; // parameters[0]表示取出第一组参数double c = parameters[0][3];// 计算残差residuals[0] = y_ - exp(m*x_ + c);// 计算雅克比if (jacobians != NULL && jacobians[0] != NULL) {jacobians[0][0] = -x_*exp(m*x_ + c);jacobians[0][4] = -exp(m*x_ + c);}return true;}private:const double x_;const double y_;};
使用自动求导应该注意的地方,比如下面的例子。

自动求导设置参数维度的时候,一定要注意。有几个参数就必须写几个参数块,对于非线性的参数不能合并到一起。还是看代码比较清晰。
当然,如果你使用解析求导,随便你怎么写啦,因为求导是你自己定义的呢。这块具体为什么,参看自动求导的原理。
// 定义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 true;}// ------------【错误写法】------------// template<typename T>// bool operator()(const T *const x,// T *residual) const {// residual[0] = y_ - exp(x[0]*x_ + x[1]);// return true;// }private:const double x_;const double y_;};// 使用// ------------【正确写法】------------problem.AddResidualBlock(new AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(new ExponentialResidual(data[2*i], data[2*i + 1])),NULL,&m, &c);// ------------【错误写法】------------// problem.AddResidualBlock(// new AutoDiffCostFunction<ExponentialResidual, 1, 2>(// new ExponentialResidual(data[2*i], data[2*i + 1])),// NULL,
这些不常用,用的时候,具体参看官方文档。
设置核函数,抑制outliers影响。

Problem problem;// Add parameter blocksCostFunction* cost_function =new AutoDiffCostFunction < UW_Camera_Mapper, 2, 9, 3>(new UW_Camera_Mapper(feature_x, feature_y));LossFunctionWrapper* loss_function(new HuberLoss(1.0), TAKE_OWNERSHIP);problem.AddResidualBlock(cost_function, loss_function, parameters);Solver::Options options;Solver::Summary summary;Solve(options, &problem, &summary);loss_function->Reset(new HuberLoss(1.0), TAKE_OWNERSHIP);Solve(options, &problem, &summary);
使用情况:
该类的内部主要接口
class LocalParameterization {public:virtual ~LocalParameterization() {}// override Plus()函数进行更新virtual bool Plus(const double* x,const double* delta,double* x_plus_delta) const = 0;virtual bool ComputeJacobian(const double* x, double* jacobian) const = 0;virtual bool MultiplyByJacobian(const double* x,const int num_rows,const double* global_matrix,double* local_matrix) const;// GlobalSize()返回参数块大小,eg:四元数返回4virtual int GlobalSize() const = 0;// LocalSize()返回参数块在对应空间的实际大小,eg,四元数返回3virtual int LocalSize() const = 0;};
重新写一个类,继承LocalParameterization
ceres也为我们写好了一部分例子,可以直接拿来用,参考
对应的自动求导里面也有过参数过的处理形式,自定义更新形式,参考
ceres最核心的模块,用于构建最小二乘问题的关键。
Problem::AddResidualBlock() 意思如同他的名字一样,向最小二乘问题添加一个参数块。具体包括
该函数会检查传入的CostFunction中size和实际列表中的参数size是否一致。

用户可以使用以下选项显式添加参数块,Problem::AddParameterBlock()。
实际上,Problem::AddResidualBlock()隐式地添加不存在的参数块,所以不需要显式地调用Problem::AddParameterBlock()。
AddParameterBlock()还允许用户将LocalParameterization对象与参数块关联。具有相同参数的重复调用将被忽略(忽略默认的那个调用)。使用相同的双指针但大小不同的重复调用将导致未定义的行为。Problem::SetParameterBlockConstant()将任何参数块设置为常量,然后使用SetParameterBlockVariable()撤消此操作。
第一个可以添加局部参数化的更新方法,用于过参数或者manifold space参数更新。
第二个则是使用默认的参数更新plus方法。
void Problem::AddParameterBlock(double *values, int size)void Problem::RemoveResidualBlock(ResidualBlockId residual_block)
删除残差或参数块将破坏隐式排序,导致从求解器返回的雅可比矩阵或残差无法解释。如果依赖于求值的雅可比矩阵,不要使用remove!在将来的版本中可能会有所改变。在优化过程中保持指定的参数块不变。
在优化过程中保持指定的参数块不变。
或者
允许指定的参数在优化期间发生变化。

当然也可以获取参数的参数化方式
LocalParameterization *Problem::GetParameterization(double *values) const
获取与此参数块关联的本地参数化对象。 如果没有关联的参数化对象,则返回NULL
void Problem::SetParameterLowerBound(double *values, int index, double lower_bound)void Problem::SetParameterUpperBound(double *values, int index, double upper_bound)
默认上下边界为无穷。

通过查看源代码,分析出了这三个模块的调用关系。之所以会关注这个问题,起初是因为在VINS-MONO中优化的函数中关于添加各种残差和添加参数块,有的残差块添加了对应的参数块,有的没有,不知所以然。所以研究一下ceres源代码。哈哈,套用侯捷老师一句话
我们这里探讨的主要是应用在解析求导的时候,过参数,manifold space情况下(eg: 四元数),针对我们的参数更新,应该使用自定义的方法。主要通过AddParameterBlock和SetParameterization来实现。
源码面前,了无秘密! —— 侯捷
AddResidualBlockAddParameterBlockSetParameterization经过前面的介绍,做优化的第一步就是构建对应的CostFunction,完事后,调用AddResidualBlock函数添加残差块,这个函数里面事实上会调用AddParameterBlock函数,而AddParameterBlock函数里面实际上会调用SetParameterization函数。
也就是说如果我们的参数属于正常的plus更新的话,也就是没有过参数,没有manifold space,那么就完全不需要调用AddParameterBlock或者SetParameterization函数
如果我们的参数需要自定义更新方式,我们可以调用AddParameterBlock或者SetParameterization函数任何一个都可以,调用方式如下
// 方法1void AddParameterBlock(double* values,int size,LocalParameterization* local_parameterization);// 方法2void SetParameterization(double* values,LocalParameterization* local_parameterization);
这里提一下既然程序中给了默认的参数化方法,我们自己添加的话,程序就会调用我们的自定义方法。
还有一个比较有意思的地方是程序虽然反复调用了AddParameterBlock,但是参数并不会添加重复,因为内部使用map管理,每次添加的时候,都会保证地址不重复。
总结一下