4、Ceres Solver入门:Ceres的安装与配置、自动求导与数值求导

说到图优化,就绕不开Ceres Solver。这个库在SLAM圈子里几乎是标配,Google出品,质量有保障。我最早接触Ceres是在做后端优化的时候,那时候还在手写高斯牛顿,后来发现Ceres把求导、迭代、收敛判断全包了,真香。

这一节,咱们就聊聊Ceres的安装配置,以及它最核心的两个求导方式——自动求导和数值求导。你想想看,优化问题的本质就是最小化误差,而求导是其中最关键的一步。Ceres帮你把这一步简化了很多。

4.1 Ceres的安装与配置

安装Ceres其实不复杂,但有几个坑需要注意。我踩过不少,给你总结一下。

4.1.1 依赖项准备

Ceres依赖几个基础库:Eigen(线性代数)、glog/gflags(日志和参数解析)、以及可选的SuiteSparse(稀疏矩阵求解)。我个人习惯用apt安装,省事。

# 安装依赖
sudo apt-get install libeigen3-dev libgoogle-glog-dev libgflags-dev
sudo apt-get install libatlas-base-dev libsuitesparse-dev

这里要注意,Eigen版本不能太低。Ceres 2.x要求Eigen 3.3以上。我曾经在Ubuntu 16.04上装过,默认的Eigen版本不够,编译报错,折腾了半天才发现是版本问题。

4.1.2 编译与安装

从GitHub拉源码,然后cmake三板斧:

git clone https://github.com/ceres-solver/ceres-solver.git
cd ceres-solver
mkdir build && cd build
cmake ..
make -j4
sudo make install

嗯,这里有个小技巧。cmake的时候可以加一些选项,比如关闭测试来加快编译:

cmake -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF ..

我一般还会加上 -DUSE_CUDA=OFF,因为SLAM里很少用到GPU加速的Ceres。

4.1.3 验证安装

装完之后,写个简单的CMakeLists.txt来验证:

cmake_minimum_required(VERSION 3.10)
project(ceres_test)

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

add_executable(test_ceres test_ceres.cpp)
target_link_libraries(test_ceres ${CERES_LIBRARIES})

如果编译通过,说明安装成功了。我习惯在终端里跑一下Ceres自带的示例程序,比如 simple_bundle_adjuster,看看输出是否正常。

避坑指南: 我曾经在macOS上装Ceres,发现SuiteSparse死活装不上。后来发现Homebrew的版本和Ceres不兼容。解决办法是用源码编译SuiteSparse,或者干脆关掉它:cmake -DSUITESPARSE=OFF ..。对于大多数SLAM问题,没有SuiteSparse也能跑,只是求解速度慢一点。

4.2 自动求导 vs 数值求导

Ceres最吸引我的地方,就是它提供了多种求导方式。说白了,你不需要手动推导雅可比矩阵了。这对我们这种懒人来说,简直是福音。

4.2.1 数值求导(NumericDiff)

数值求导的原理很简单——用差分近似代替导数。比如前向差分:

f'(x) ≈ (f(x + h) - f(x)) / h

在Ceres里,用数值求导需要这样写:

struct CostFunctor {
    template <typename T>
    bool operator()(const T* const x, T* residual) const {
        residual[0] = 10.0 - x[0];
        return true;
    }
};

// 使用数值求导
ceres::CostFunction* cost_function =
    new ceres::NumericDiffCostFunction<CostFunctor, ceres::CENTRAL, 1, 1>(new CostFunctor);

注意这里的 ceres::CENTRAL,它指定了差分方式。有FORWARD、CENTRAL、RIDDERS三种。我一般用CENTRAL,精度比FORWARD高,但计算量翻倍。RIDDERS精度最高,但慢,适合对精度要求极高的场景。

数值求导的优缺点:
  • 优点:实现简单,不需要推导导数公式
  • 缺点:精度受步长h影响,计算速度慢,对病态问题不稳定

我在项目中遇到过一个问题:用数值求导优化相机位姿,结果迭代了100多次还不收敛。后来换成自动求导,20次就搞定了。说白了,数值求导的精度问题在非线性优化里会被放大。

4.2.2 自动求导(AutoDiff)

自动求导是Ceres的杀手锏。它不像数值求导那样近似,也不像符号求导那样需要推导表达式。它利用链式法则,在计算过程中同时计算导数。

用自动求导的写法:

struct CostFunctor {
    template <typename T>
    bool operator()(const T* const x, T* residual) const {
        residual[0] = T(10.0) - x[0];
        return true;
    }
};

// 使用自动求导
ceres::CostFunction* cost_function =
    new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);

你看,代码几乎一样,只是把 NumericDiffCostFunction 换成了 AutoDiffCostFunction。但内部机制完全不同。自动求导会把 T 替换成 Jet 类型,这个 Jet 同时保存值和导数,通过重载运算符实现链式求导。

个人经验: 在SLAM中,我几乎只用自动求导。除非遇到自动求导不支持的情况(比如代码里有复杂的条件分支),我才会考虑数值求导。自动求导的精度和速度都优于数值求导,而且代码更简洁。

4.2.3 对比总结

对比项 自动求导 数值求导
精度 机器精度(无截断误差) 受步长h影响,有截断误差
速度 快(与手写解析求导接近) 慢(每次需要多次函数求值)
实现难度 中等(需要模板化代码) 简单(直接写残差函数)
适用场景 大多数优化问题 自动求导不支持的特殊情况

4.3 一个完整的例子

咱们用Ceres来解一个简单的曲线拟合问题。假设我们有观测数据点,想拟合一条曲线 y = exp(m*x + c)。这个例子在SLAM里很常见,比如拟合传感器的响应曲线。

#include <ceres/ceres.h>
#include <iostream>

// 残差结构体
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] = T(y_) - exp(m[0] * T(x_) + c[0]);
        return true;
    }

private:
    const double x_;
    const double y_;
};

int main() {
    // 模拟数据
    double m_true = 0.3, c_true = 0.1;
    double m_est = 0.0, c_est = 0.0;

    // 构建问题
    ceres::Problem problem;
    for (int i = 0; i < 100; ++i) {
        double x = i * 0.1;
        double y = exp(m_true * x + c_true);
        problem.AddResidualBlock(
            new ceres::AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(
                new ExponentialResidual(x, y)),
            nullptr, &m_est, &c_est);
    }

    // 配置求解器
    ceres::Solver::Options options;
    options.minimizer_progress_to_stdout = true;
    options.max_num_iterations = 100;

    // 求解
    ceres::Solver::Summary summary;
    ceres::Solve(options, &problem, &summary);

    std::cout << summary.FullReport() << std::endl;
    std::cout << "True m: " << m_true << ", c: " << c_true << std::endl;
    std::cout << "Est m: " << m_est << ", c: " << c_est << std::endl;

    return 0;
}

这个例子虽然简单,但包含了Ceres的核心流程:定义残差、构建问题、配置求解器、求解。在SLAM闭环检测中,我们也是用同样的流程来优化位姿图。

核心流程总结:
  1. 定义残差结构体,重载 operator()
  2. AutoDiffCostFunctionNumericDiffCostFunction 包装
  3. 添加到 Problem
  4. 配置 Solver::Options,调用 Solve

4.4 知识体系图

为了让你更直观地理解Ceres Solver在SLAM中的位置,我画了一张图:

Ceres Solver 在SLAM闭环检测中的角色 闭环检测结果 (位姿约束) Ceres Solver 自动求导 / 数值求导 LM / Dogleg 求解器 优化后位姿 构建图优化问题 迭代求解 收敛判断

这张图展示了Ceres在闭环检测中的位置。说白了,闭环检测给你的是约束,Ceres负责把这些约束变成优化问题,然后求解。自动求导和数值求导就是求解过程中的核心工具。

我的建议: 刚开始学Ceres,先用自动求导。等你对优化流程熟悉了,再考虑手写解析求导来提升性能。数值求导嘛,除非万不得已,否则别用。我在实际项目中,99%的情况都用自动求导,剩下的1%用手写解析求导。

好了,这一节的内容就到这里。Ceres的安装配置和求导方式,是后续图优化的基础。你先把这些基础打牢,后面讲位姿图优化的时候,就能直接上手了。


公众号:蓝海资料掘金营,微信deep3321