Ceres优化库—自动求导与解析求导的实现

Ceres提供了三种求导方式:自动求导、解析求导、数值求导。

1.自动求导

(1) 自动求导是通过定义一个仿函数,然后传给AutoDiffCostFunction, 只要给出残差和待优化变量的表达式,Ceres就能自动地求出导数。

(2) 所谓仿函数,其实是一个类,只不过这个类的作用像函数,所以叫仿函数。原理就是类实现了operator()函数。

(3) operator()函数必须是模板函数,因为Ceres内部求导要用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自动求导 构造仿函数
struct MyAutoCostFunction {
MyAutoCostFunction(double x, double y) : _x(x), _y(y) {}

// 残差的计算
template<typename T>
bool operator()(const T *const abcd, T *residual) const
{
residual[0] = T(_y) - (pow(abcd[0] , T(_x)) + abcd[1] * T(_x) * T(_x) + abcd[2] * T(_x) + abcd[3]); // y-(a^x+b*x^2+c*x+d)
return true;
}

const double _x, _y; // x,y数据
};

2.解析求导

解析求导的含义就是直接给出导数的解析形式,而不是ceres去推导 。解析求导也兼容自动求导,给出雅可比时, ceres会直接使用该雅可比,不给出雅可比时, ceres就会自动去求导。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class AnalyticCostFunction : public ceres::SizedCostFunction<1, 4> {
public:
AnalyticCostFunction(const double x, const double y) : _x(x), _y(y) {}
virtual ~AnalyticCostFunction() {}
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
const double a = parameters[0][0];
const double b = parameters[0][1];
const double c = parameters[0][2];
const double d = parameters[0][3];
residuals[0] = double(_y) - (pow(a, double(_x)) + b * double(_x) * double(_x) + c * double(_x) + d); // y-(a^x+b*x^2+c*x+d)

if (jacobians != NULL && jacobians[0] != NULL) {
jacobians[0][0] = -1 * pow(a, double(_x - 1)) * double(_x); // dy/da
jacobians[0][1] = -1 * double(_x) * double(_x); // dy/db
jacobians[0][2] = -1 * double(_x); // dy/dc
jacobians[0][3] = -1; // dy/dd
}
return true;
}

private:
double _x, _y;
};

3.自动求导与解析求导的对比

  • 自动求导实现方便,但效率会比解析求导低 (比较 A-LOAM 和 F-LOAM );

  • 实际使用中,能够自动求导且效率没有形成障碍的,优先使用自动求导;

  • 除这两种方法外,还有数值求导,相比于自动求导只是没有使用模板函数,SLAM问题中不常见,不过多介绍。

4.完整代码

ceres::Solve(options, &problem, &summary);有三个参数,option是求解器设置,包括方程的求解方式,最大迭代次数等设置;problem就是设置优化问题,添加残差块;summary是结果报告,输出最终优化次数,cost等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include<cmath>
#include <iostream>
#include <opencv2/core/core.hpp>
#include <ceres/ceres.h>
#include <chrono>

using namespace std;

// 自动求导 构造仿函数
struct MyAutoCostFunction {
MyAutoCostFunction(double x, double y) : _x(x), _y(y) {}

// 残差的计算
template<typename T>
bool operator()(const T *const abcd, T *residual) const
{
residual[0] = T(_y) - (pow(abcd[0] , T(_x)) + abcd[1] * T(_x) * T(_x) + abcd[2] * T(_x) + abcd[3]); // y-(a^x+b*x^2+c*x+d)
return true;
}

const double _x, _y; // x,y数据
};

class AnalyticCostFunction : public ceres::SizedCostFunction<1, 4> {
public:
AnalyticCostFunction(const double x, const double y) : _x(x), _y(y) {}
virtual ~AnalyticCostFunction() {}
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
const double a = parameters[0][0];
const double b = parameters[0][1];
const double c = parameters[0][2];
const double d = parameters[0][3];
residuals[0] = double(_y) - (pow(a, double(_x)) + b * double(_x) * double(_x) + c * double(_x) + d); // y-(a^x+b*x^2+c*x+d)

if (jacobians != NULL && jacobians[0] != NULL) {
jacobians[0][0] = -1 * pow(a, double(_x - 1)) * double(_x); // dy/da
jacobians[0][1] = -1 * double(_x) * double(_x); // dy/db
jacobians[0][2] = -1 * double(_x); // dy/dc
jacobians[0][3] = -1; // dy/dd
}
return true;
}

private:
double _x, _y;
};


int main(int argc, char **argv) {
double ar = 2.0, br = 2.0, cr = 1.0, dr = 2.0; // 真实参数值
double ae = 9.0, be = -3.0, ce = -1.0, de = 4.0; // 估计参数值
int N = 100; // 数据点
double w_sigma = 0.0; // 噪声Sigma值
double inv_sigma = 1.0 / w_sigma;
cv::RNG rng; // OpenCV随机数产生器

vector<double> x_data, y_data; // 数据
for (int i = 0; i < N; i++) {
double x = i / 100.0;
x_data.push_back(x);
y_data.push_back((pow(ar , x) + br * x * x + cr * x + dr) + rng.gaussian(w_sigma * w_sigma));
}

double abcd[4] = {ae, be, ce, de}; // 待优化参数

// 构建最小二乘问题
ceres::Problem problem;
for (int i = 0; i < N; i++) {

// 自动求导
auto costFunction = new ceres::AutoDiffCostFunction<MyAutoCostFunction, 1, 4>(new MyAutoCostFunction(x_data[i], y_data[i]));
// ^ ^
// | |
// 残差的维度 ____| |
// 优化变量abcd[]的维度 _______|

// 解析求导
// auto costFunction = new AnalyticCostFunction(x_data[i], y_data[i]);

problem.AddResidualBlock(
costFunction, //代价函数
nullptr, // 核函数,这里不使用,为空
abcd // 待估计参数
);

}

// 配置求解器
ceres::Solver::Options options; // 这里有很多配置项可以填
options.linear_solver_type = ceres::DENSE_NORMAL_CHOLESKY; // 增量方程求解方式
// options.linear_solver_type = ceres::DENSE_QR; // 增量方程求解方式
options.minimizer_progress_to_stdout = true; // 优化过程输出到cout
options.max_num_iterations = 100; // 最大迭代次数

ceres::Solver::Summary summary; // 结果报告
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
ceres::Solve(options, &problem, &summary); // 开始优化
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
cout << "solve time cost = " << time_used.count() << " seconds. " << endl;

// 输出结果
cout << summary.BriefReport() << endl;
cout << "estimated a,b,c,d = ";

for (auto a:abcd) cout << a << " ";
cout << endl;

return 0;
}

Cmake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_minimum_required(VERSION 2.8)
project(learnCeres)

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

# OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

# Ceres
find_package(Ceres REQUIRED)
include_directories(${CERES_INCLUDE_DIRS})

add_executable(learnCeres learnCeres.cpp)
target_link_libraries(learnCeres ${OpenCV_LIBS} ${CERES_LIBRARIES})