[关闭]
@iStarLee 2019-12-07T11:54:15.000000Z 字数 8803 阅读 1301

Ceres Tutotial(2) —— 最小二乘建模

Optimization


写在前面

8.9 关于添加残差块,参数块,设置参数化的区别和调用关系【重要】

这一节相当重要,尤其是写代码的时候。

1 CostFunction和SizedCostFunction

在旧版本的ceres里面,参数块最大是10个,但是新版本的ceres使用了可变长模板参数,所以参数快数量不限制。

1.1 细节对比

CostFunction的存在是为了计算残差和雅克比矩阵。
主要接口。

  1. class CostFunction {
  2. public:
  3. virtual bool Evaluate(double const* const* parameters,
  4. double* residuals,
  5. double** jacobians) = 0;
  6. const vector<int32>& parameter_block_sizes();
  7. int num_residuals() const;
  8. protected:
  9. vector<int32>* mutable_parameter_block_sizes();
  10. void set_num_residuals(int num_residuals);
  11. };
  1. class AnalyticCostFunction
  2. : public ceres::SizedCostFunction<1 /* number of residuals */,
  3. 2 /* size of first parameter */>
  4. {
  5. ...
  6. }
  1. class AnalyticCostFunction2
  2. : public ceres::CostFunction {
  3. public:
  4. AnalyticCostFunction2(const int& num_residuals, const int& block_sizes,
  5. double x, double y) : x_(x), y_(y) {
  6. set_num_residuals(num_residuals); // 设置残差维度
  7. std::vector<int>* param_block_sizes = mutable_parameter_block_sizes(); // 返回的引用
  8. param_block_sizes->push_back(block_sizes); // 设置参数块维度,有几个参数块,就pubsh_back几次
  9. }
  10. ...
  11. }
  12. // 使用
  13. CostFunction *cost_function = new AnalyticCostFunction2(1,2,data[2*i], data[2*i + 1]);
  14. problem.AddResidualBlock(cost_function, NULL, x);

其实通过查看SizedCostFunction也是继承了CostFunction的, 通过查看其构造函数可以发现我们刚才直接继承CostFunction构建functor的方法其实和SizedCostFunction是一样的。
只不过Ceres已经帮我们做好了,
以后我们只使用SizedCostFunction就好了,很方便。

  1. SizedCostFunction() {
  2. set_num_residuals(kNumResiduals);
  3. *mutable_parameter_block_sizes() = std::vector<int32_t>{Ns...};
  4. }

1.2 两种定义cost function的完整代码对比

  1. //-------------------------------------------
  2. // 解析求导方式1
  3. //-------------------------------------------
  4. class AnalyticCostFunction
  5. : public ceres::SizedCostFunction<1 /* number of residuals */,
  6. 2 /* size of first parameter */> {
  7. public:
  8. AnalyticCostFunction(double x, double y) : x_(x), y_(y) {}
  9. virtual ~AnalyticCostFunction() {}
  10. /**
  11. * @brief 重载Evaluate函数,完成jacobian和residuals的计算
  12. * @param parameters
  13. * @param residuals
  14. * @param jacobians
  15. * @return
  16. */
  17. virtual bool Evaluate(double const *const *parameters,
  18. double *residuals,
  19. double **jacobians) const {
  20. double m = parameters[0][0]; // parameters[0]表示取出第一组参数
  21. double c = parameters[0][1];
  22. // 计算残差
  23. residuals[0] = y_ - exp(m*x_ + c);
  24. // 计算雅克比
  25. if (jacobians != NULL && jacobians[0] != NULL) {
  26. jacobians[0][0] = -x_*exp(m*x_ + c);
  27. jacobians[0][2] = -exp(m*x_ + c);
  28. }
  29. return true;
  30. }
  31. private:
  32. const double x_;
  33. const double y_;
  34. };
  35. //-------------------------------------------
  36. // 解析求导方式2
  37. //-------------------------------------------
  38. class AnalyticCostFunction2
  39. : public ceres::CostFunction {
  40. public:
  41. AnalyticCostFunction2(const int& num_residuals, const int& block_sizes,
  42. double x, double y) : x_(x), y_(y) {
  43. set_num_residuals(num_residuals); // 设置残差维度
  44. std::vector<int>* param_block_sizes = mutable_parameter_block_sizes(); // 返回的引用
  45. param_block_sizes->push_back(block_sizes); // 设置参数块维度,有几个参数块,就pubsh_back几次
  46. }
  47. virtual ~AnalyticCostFunction2() {}
  48. /**
  49. * @brief 重载Evaluate函数,完成jacobian和residuals的计算
  50. * @param parameters
  51. * @param residuals
  52. * @param jacobians
  53. * @return
  54. */
  55. virtual bool Evaluate(double const *const *parameters,
  56. double *residuals,
  57. double **jacobians) const {
  58. double m = parameters[0][0]; // parameters[0]表示取出第一组参数
  59. double c = parameters[0][3];
  60. // 计算残差
  61. residuals[0] = y_ - exp(m*x_ + c);
  62. // 计算雅克比
  63. if (jacobians != NULL && jacobians[0] != NULL) {
  64. jacobians[0][0] = -x_*exp(m*x_ + c);
  65. jacobians[0][4] = -exp(m*x_ + c);
  66. }
  67. return true;
  68. }
  69. private:
  70. const double x_;
  71. const double y_;
  72. };

2 AutoDiffCostFunction

使用自动求导应该注意的地方,比如下面的例子。
此处输入图片的描述

自动求导设置参数维度的时候,一定要注意。有几个参数就必须写几个参数块,对于非线性的参数不能合并到一起。还是看代码比较清晰。

当然,如果你使用解析求导,随便你怎么写啦,因为求导是你自己定义的呢。这块具体为什么,参看自动求导的原理。

  1. // 定义
  2. struct ExponentialResidual {
  3. ExponentialResidual(double x, double y)
  4. : x_(x), y_(y) {}
  5. // ------------【正确写法】------------
  6. template<typename T>
  7. bool operator()(const T *const m,
  8. const T *const c,
  9. T *residual) const {
  10. residual[0] = y_ - exp(m[0]*x_ + c[0]);
  11. return true;
  12. }
  13. // ------------【错误写法】------------
  14. // template<typename T>
  15. // bool operator()(const T *const x,
  16. // T *residual) const {
  17. // residual[0] = y_ - exp(x[0]*x_ + x[1]);
  18. // return true;
  19. // }
  20. private:
  21. const double x_;
  22. const double y_;
  23. };
  24. // 使用
  25. // ------------【正确写法】------------
  26. problem.AddResidualBlock(
  27. new AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(
  28. new ExponentialResidual(data[2*i], data[2*i + 1])),
  29. NULL,
  30. &m, &c);
  31. // ------------【错误写法】------------
  32. // problem.AddResidualBlock(
  33. // new AutoDiffCostFunction<ExponentialResidual, 1, 2>(
  34. // new ExponentialResidual(data[2*i], data[2*i + 1])),
  35. // NULL,

3 其他cost function

这些不常用,用的时候,具体参看官方文档。

4 用法不清楚的模块

5 LossFunction

设置核函数,抑制outliers影响。

  1. Problem problem;
  2. // Add parameter blocks
  3. CostFunction* cost_function =
  4. new AutoDiffCostFunction < UW_Camera_Mapper, 2, 9, 3>(
  5. new UW_Camera_Mapper(feature_x, feature_y));
  6. LossFunctionWrapper* loss_function(new HuberLoss(1.0), TAKE_OWNERSHIP);
  7. problem.AddResidualBlock(cost_function, loss_function, parameters);
  8. Solver::Options options;
  9. Solver::Summary summary;
  10. Solve(options, &problem, &summary);
  11. loss_function->Reset(new HuberLoss(1.0), TAKE_OWNERSHIP);
  12. Solve(options, &problem, &summary);

6 LocalParameterization

使用情况:

该类的内部主要接口

  1. class LocalParameterization {
  2. public:
  3. virtual ~LocalParameterization() {}
  4. // override Plus()函数进行更新
  5. virtual bool Plus(const double* x,
  6. const double* delta,
  7. double* x_plus_delta) const = 0;
  8. virtual bool ComputeJacobian(const double* x, double* jacobian) const = 0;
  9. virtual bool MultiplyByJacobian(const double* x,
  10. const int num_rows,
  11. const double* global_matrix,
  12. double* local_matrix) const;
  13. // GlobalSize()返回参数块大小,eg:四元数返回4
  14. virtual int GlobalSize() const = 0;
  15. // LocalSize()返回参数块在对应空间的实际大小,eg,四元数返回3
  16. virtual int LocalSize() const = 0;
  17. };

重新写一个类,继承LocalParameterization

ceres也为我们写好了一部分例子,可以直接拿来用,参考

7 AutoDiffLocalParameterization

对应的自动求导里面也有过参数过的处理形式,自定义更新形式,参考

8 Problem

ceres最核心的模块,用于构建最小二乘问题的关键。

8.1 添加残差块

8.1.1 简介

Problem::AddResidualBlock() 意思如同他的名字一样,向最小二乘问题添加一个参数块。具体包括

该函数会检查传入的CostFunction中size和实际列表中的参数size是否一致。

8.1.2 两种调用接口

8.2 添加参数块

8.2.1 简介

用户可以使用以下选项显式添加参数块,Problem::AddParameterBlock()
实际上,Problem::AddResidualBlock()隐式地添加不存在的参数块,所以不需要显式地调用Problem::AddParameterBlock()

8.2.2 两种调用接口

第一个可以添加局部参数化的更新方法,用于过参数或者manifold space参数更新。
第二个则是使用默认的参数更新plus方法。

8.3 删除残差块

  1. void Problem::AddParameterBlock(double *values, int size)
  2. void Problem::RemoveResidualBlock(ResidualBlockId residual_block)

删除残差或参数块将破坏隐式排序,导致从求解器返回的雅可比矩阵或残差无法解释。如果依赖于求值的雅可比矩阵,不要使用remove!在将来的版本中可能会有所改变。在优化过程中保持指定的参数块不变。

8.4 设置参数块为常量或者变量


在优化过程中保持指定的参数块不变。
或者
允许指定的参数在优化期间发生变化。

8.5 设置参数块的参数化方式

当然也可以获取参数的参数化方式

  1. LocalParameterization *Problem::GetParameterization(double *values) const

获取与此参数块关联的本地参数化对象。 如果没有关联的参数化对象,则返回NULL

8.6 设置参数的上下边界

  1. void Problem::SetParameterLowerBound(double *values, int index, double lower_bound)
  2. void Problem::SetParameterUpperBound(double *values, int index, double upper_bound)

默认上下边界为无穷。

8.7 获取Problem内部情况的接口函数


8.8 Evaluate相关

参看文档

8.9 关于添加残差块,参数块,设置参数化的区别和调用关系【重要】

通过查看源代码,分析出了这三个模块的调用关系。之所以会关注这个问题,起初是因为在VINS-MONO中优化的函数中关于添加各种残差和添加参数块,有的残差块添加了对应的参数块,有的没有,不知所以然。所以研究一下ceres源代码。哈哈,套用侯捷老师一句话

我们这里探讨的主要是应用在解析求导的时候过参数,manifold space情况下(eg: 四元数),针对我们的参数更新,应该使用自定义的方法。主要通过AddParameterBlockSetParameterization来实现。

源码面前,了无秘密! —— 侯捷

经过前面的介绍,做优化的第一步就是构建对应的CostFunction,完事后,调用AddResidualBlock函数添加残差块,这个函数里面事实上会调用AddParameterBlock函数,而AddParameterBlock函数里面实际上会调用SetParameterization函数。

也就是说如果我们的参数属于正常的plus更新的话,也就是没有过参数,没有manifold space,那么就完全不需要调用AddParameterBlock或者SetParameterization函数

如果我们的参数需要自定义更新方式,我们可以调用AddParameterBlock或者SetParameterization函数任何一个都可以,调用方式如下

  1. // 方法1
  2. void AddParameterBlock(double* values,
  3. int size,
  4. LocalParameterization* local_parameterization);
  5. // 方法2
  6. void SetParameterization(double* values,
  7. LocalParameterization* local_parameterization);

这里提一下既然程序中给了默认的参数化方法,我们自己添加的话,程序就会调用我们的自定义方法。
还有一个比较有意思的地方是程序虽然反复调用了AddParameterBlock,但是参数并不会添加重复,因为内部使用map管理,每次添加的时候,都会保证地址不重复。

总结一下

9 旋转相关

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注