Ceres 翻譯為谷神星,是太陽系中的一顆矮行星,於1801年被意大利神父 Piazzi 首次觀測到,但隨后 Piazzi 因為生病,跟丟了它的運行軌跡。
幾個月后,德國數學家 Gauss,利用最小二乘法,僅僅依靠 Piazzi 之前觀測到的12個數據,便成功的預測了谷神星的運行軌跡。
兩百多年后,為了解決一些復雜的最優化問題 (如:帶邊界約束的非線性最小二乘、一般的無約束最優化等),谷歌開發了一個 C++ 庫 Ceres Solver
之所以取名 Ceres Solver,是為了紀念 Gauss 利用最小二乘法,成功的預測了谷神星軌跡,這個在最優化、統計學、天文學歷史上,都具有重要意義的事件
1 Ceres 簡介
1.1 非線性最小二乘
Ceres 可用來解決帶邊界約束的非線性最小二乘問題,如下:
$\quad \begin{split} \min_x &\quad \frac{1}{2}\sum_{i} \rho_i\left(\left\|f_i\left(x_{i_1}, x_{i_2}, ... ,x_{i_k}\right)\right\|^2\right) \\ &\quad l_j \le x_j \le u_j \end{split}$
其中,1) $ \rho_i\left(\left\|f_i\left(x_{i_1},x_{i_2},...,x_{i_k}\right)\right\|^2\right) $ 為殘差塊
2) $f_i(\cdot)$ 為代價函數,取決於參數塊 $\left[x_{i_1},x_{i_2},... , x_{i_k}\right]$
3) $\rho_i$ 為損失函數,是一個標量函數,主要用來消除異常點對求解過程的影響
令損失函數 $\rho_i = x$ 為恆等函數,並放寬約束條件 $[-\infty, \infty]$,則得到無約束的非線性最小二乘形式:
$\quad \begin{split}\frac{1}{2}\sum_{i} \left\|f_i\left(x_{i_1}, ... ,x_{i_k}\right)\right\|^2 \end{split}$
以上形式在科學和工程領域有着廣泛的應用,如,統計學中的曲線擬合,計算機視覺的三維重建等
1.2 Ceres 的特點
1) 模型接口簡潔
- 求導簡單;魯棒的損失函數;局部參數化
2) 求解方法多
- 信賴域法:Levenberg-Marquardt, Powell's Dogleg, Subspace dogleg
- 線搜索法:Non-linear Conjugate Gradients, BFGS, LBFGS
3) 求解質量高
- 在 NIST 數據集下,按照 Mondragon 和 Borchers 的測試標准,Ceres 的准確度最高
1.3 應用實例
在谷歌內部,Ceres 已經被應用於多個產品中,如:谷歌街景中汽車、飛行器的位姿估計;PhotoTours 中 3D 模型的建立;SLAM 算法 Cartographer 等
此外,一些公司和研究所也在使用 Ceres,像是 Southwest Research Institute 的機器人視覺系統標定,OpenMVG 的光束平差 (BA) 問題,Willow Garage 的 SLAM 問題等
2 編譯配置
Win10 64-bit ;VS 2019 社區版,下載地址 ;CMake 解壓版,下載地址
2.1 源文件
- Ceres Solver 源文件,下載地址,從 2.0 開始,需要支持 C++14 的編譯器
- eigen 源文件,必須,>=3.3,下載地址
- glog 源文件,推薦,>=0.3.1,下載地址,及其依賴庫 gflags,下載地址
2.2 配置生成
1) 將源文件 ceres、eigen、glog 和 gflags 解壓,運行 cmake-gui.exe,配置生成 eigen (先點 "Configure",再點 "Generate" 即可)
2) 編譯 gflags,先點 "Configure",再點 "Generate",然后點 "Open Project",在 VS 中打開工程,最后在 debug 和 release 模式下分別編譯
3) 編譯 glog,操作同 2),其中 cmake 配置不再贅述,如:CMAKE_CONFGURATION_TYPES 只保留 Debug 和 Release,gflags_DIR 指向含 gflags-config.cmake 的目錄等
4) 編譯 ceres,操作同 2),注意配置 eigen、glog 和 gflags 的 _DIR 目錄,並勾上 BUILD_SHARED_LIBS 以便生成 dll 庫
2.3 VS 配置
將生成的 .lib 和 .dll 文件放在特定目錄下,並新建 include 文件夾,匯總對應的 .h 文件
1)環境變量
計算機 -> 屬性 -> 高級系統設置 -> 環境變量,編輯系統變量里的 path 變量
D:\3rdparty\gflags\bin\Debug D:\3rdparty\glog\bin\Debug D:\3rdparty\ceres\bin\Debug
2) 頭文件和庫文件
在 VS 中配置 ceres, eigen, glog 和 gflags 的 頭文件目錄,以及 庫文件目錄
注:在編譯時,若出現頭文件缺失,則在源文件中找到對應的 .h,拷貝到 include 目錄中即可
3) 依賴庫
添加對應的 .lib 依賴庫,如下:
ceres-debug.lib gflags_debug.lib glogd.lib
4) 錯誤處理
運行程序,如出現如下錯誤,則在 "項目屬性 - C/C++ - 預處理器定義" 中,定義 _USE_MATH_DEFINES 宏可解決
3 代碼實例
給定一個目標函數 $\begin{split} \frac{1}{2}(10 -x)^2 \end{split}$,求使其取值最小時,對應的 $x$
3.1 求解步驟
1) 構建代價函數
Ceres 中利用仿函數,通過重載 operator() 運算符,來實現代價函數的定義,本例中的代價函數為 $f(x) = 10 -x $
// A templated cost functor that implements the residual r = x - 10 struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { residual[0] = x[0] - 10.0; return true; } };
2) 構建殘差塊
// Build the problem Problem problem; // Set up the cost function (also known as residual) CostFunction* cost_function = new AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor); problem.AddResidualBlock(cost_function, nullptr, &x);
3) 配置求解器
// The options structure, which controls how the solver operates Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = true;
4) 求解
// Run the solver Solver::Summary summary; Solve(options, &problem, &summary);
3.2 完整代碼
#include "ceres/ceres.h" using ceres::AutoDiffCostFunction; using ceres::CostFunction; using ceres::Problem; using ceres::Solver; using ceres::Solve; // A templated cost functor that implements the residual r = x - 10. struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { residual[0] = 10.0 - x[0]; return true; } }; int main() { // The variable to solve for with its initial value double x = 0.5; const double initial_x = x; // Build the problem. Problem problem; problem.AddResidualBlock(new AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor), nullptr, &x); // The options structure, which controls how the solver operates Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = true; // Run solver Solver::Summary summary; Solve(options, &problem, &summary); std::cout << summary.BriefReport() << "\n"; std::cout << "x : " << initial_x << " -> " << x << "\n"; }
運行結果如下:
參考資料