Ceres Solver 自學
介紹
Ceres可以用來解受邊界約束的非線性最小二乘問題,如
這一章我們將學習如何使用Ceres Solver解(1)問題。表達式是ResidualBock
,其中是CostFunction
,它依賴參數塊。在大多數優化問題中,一小組標量出現在一起(例如參數塊)。例如,相機的位姿由3個標量組成的平移向量和4個標量組成的旋轉(用四元數表示旋轉)組成,其中涉及到額一小組標量叫做ParameterBlock
,當然一個parameterBlock
可以僅僅是一個參數,和是參數塊的邊界約束。
是一個LossFunction
,一個LossFunction
是一個標量函數,它用來減少非線性最小二乘中outliners的影響。
在特性的情況下,當,也就是identity function, 同時,,我們得到更熟悉的非線性最小二乘問題。
Hello Word
一開始,我們考慮下面的一個問題,找該函數的最小值
這是一個簡單額問題,它的最小值是當的時候取得最小值,但是這是使用Ceres解這個問題一個好的開始。
第一步寫一個仿函數(functor),它用來估計函數
struct CostFunctor{
template <typename T>
bool operator() (const T* const x, T* residual) const {
residual[0] = T(10.0) - x[0];
return true;
}
};
注意這裏重要的是operator()
是一個模板方法,它假設所有的輸入和輸出是T
類型,模板的使用允許Ceres調用CostFunctor::operator<T>()
,對於T=double
時僅僅需要殘差(residual)值,對於T=Jet
時需要雅克比。在導數那一節我們將要討論Ceres中提供的多種導數類型。
一旦我們有了計算參差函數(residual function)的方法,就可以使用它構造一個最小二乘問題,然後使用Ceres 求解。
int main(int argc, char** argv) {
google::InitGoogleLogging(argv[0]);
// 1. 設置變量初始值
double initial_x = 5.0;
double x = initial_x;
// 2. 構建一個問題.
Problem problem;
// 3. 配置cost function並使用ceres提供的自動求導的方式求導
CostFunction* cost_function =
new AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
// 4. 啓動求解器
Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR;
options.minimizer_progress_to_stdout = true;
Solver::Summary summary;
Solve(options, &problem, &summary);
std::cout << summary.BriefReport() << "\n";
std::cout << "x : " << initial_x
<< " -> " << x << "\n";
return 0;
}
AutoDiffCostFunction
將CostFunctor
作爲輸入,自動求導它並使用CostFunction
作爲接口。
編譯和運行 examples/helloworld.cc將得到
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time
0 4.512500e+01 0.00e+00 9.50e+00 0.00e+00 0.00e+00 1.00e+04 0 5.33e-04 3.46e-03
1 4.511598e-07 4.51e+01 9.50e-04 9.50e+00 1.00e+00 3.00e+04 1 5.00e-04 4.05e-03
2 5.012552e-16 4.51e-07 3.17e-08 9.50e-04 1.00e+00 9.00e+04 1 1.60e-05 4.09e-03
Ceres Solver Report: Iterations: 2, Initial cost: 4.512500e+01, Final cost: 5.012552e-16, Termination: CONVERGENCE
x : 5 -> 10
從開始,求解器在兩次迭代後到達10。仔細的讀者注意到這是一個線性問題,線性求解應該能到到達最佳的值。求解器的默認配置意在非線性問題,爲了簡單性,在這個例子中沒有改變它。的確,使用Ceres可以在一次迭代後獲取它的解。另外我們還注意在一次迭代後求解器就非常接近最佳值0。當我們談到Ceres的收斂和參數配置時將深入探討這些細節。
導數
像大多數優化包一樣,Ceres Solver依賴於能夠在任意參數值下估計目標函數中每個項的值和導數。正確有效地做法對於獲得好結果至關重要。 Ceres Solver提供了許多方法。 你已經看到其中一個 - 自動求導的例子examples/helloworld.cc
我們將要考慮其他的兩種:分析和數值導數(Analytic and numeric derivatives)。
數值導數(Numeric Derivatives)
在一些情況下,不可能定義一個模板cost functor,例如,當殘差的估計涉及到調用一個你不能控制的庫函數時。在那種情況下可以使用數值求導。用戶定義一個仿函數來計算殘差值,然後構造一個NumericDiffCostFunction
使用它。例如,對於對應的仿函數是
struct NumericDiffCostFunctor {
bool operator()(const double* const x, double* residual) const {
residual[0] = 10.0 - x[0];
return true;
}
};
它被添加到Problem
zhong
CostFunction* cost_function =
new NumericDiffCostFunction<NumericDiffCostFunctor, ceres::CENTRAL, 1, 1>(
new NumericDiffCostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
注意到當我們使用自動求導時
CostFunction* cost_function =
new AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor);
problem.AddResidualBlock(cost_function, NULL, &x);
對比可知道,除了額外的模板參數指示用於計算數值導數的有限求導方案的類型外,該構造看起來幾乎與用於自動微分的構造相同[3]。 有關更多詳細信息,請參閱NumericDiffCostFunction
的文檔。
一般來說,我們推薦自動求導而不是數值求導。 C ++模板的使得自動求導更加有效,而數字求導很昂貴,容易出現數值錯誤,並導致收斂速度變慢。
分析導數(Analytic Derivatives)
在一些情況下,使用自動求導是不可行的。例如,在某些情況下計算導數的閉合解比靠自動求導的鏈式規則更加有效。
在那種情況,提供你自己的殘差和牙可以計算的代碼是可能的。這樣做需要定義CostFucntion
的子類或者如果你在編譯時間知道參數和殘差的大小,可以定義SizedCostFunction
的子類。這裏是一個實現的一個SimpleConstFunction
的例子
class QuadraticCostFunction : public ceres::SizedCostFunction<1, 1> {
public:
virtual ~QuadraticCostFunction() {}
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
const double x = parameters[0][0];
residuals[0] = 10 - x;
// Compute the Jacobian if asked for.
if (jacobians != NULL && jacobians[0] != NULL) {
jacobians[0][0] = -1;
}
return true;
}
};
SimpleCostFunction :: Evaluate
提供了parameters
的輸入數組,殘差的輸出數組residuals
和雅可比的輸出數組jacobians
。 jacobians
數組是可選的,Evaluate
應該檢查它何時爲非null
,如果是非null
,則用殘差函數的導數值填充它。 在這裏,由於殘差函數是線性的,雅可比矩陣是常數。
從上面的代碼片段可以看出,實現CostFunction
對象有點單調乏味。 我們建議除非您有充分的理由自己管理雅可比計算,否則使用AutoDiffCostFunction
或NumericDiffCostFunction
來構造殘差塊。
關於更多導數(More About Derivatives)
計算導數是迄今爲止使用Ceres最複雜的部分,並且根據環境,用戶可能需要更復雜的計算導數的方法。本節僅涉及Ceres如何求導的表面。 一旦您熟悉使用NumericDiffCostFunction
和AutoDiffCostFunction
,我們建議您查看DynamicAutoDiffCostFunction
,CostFunctionToFunctor
,NumericDiffFunctor
和ConditionedCostFunction
,以獲得構建和計算成本函數的更高級方法。
Powell’s Function
現在考慮一個稍微複雜的例子 - Powell函數的最小化。讓,
是4個參數的函數,有4個殘差,我們想找到一個使得最小。
再一次,第一步要定義目標函數每一項的估計的仿函數,這裏是估計的代碼:
struct F4 {
template <typename T>
bool operator()(const T* const x1, const T* const x4, T* residual) const {
residual[0] = T(sqrt(10.0)) * (x1[0] - x4[0]) * (x1[0] - x4[0]);
return true;
}
};
類似的,我們可以定義分別估計。使用這些仿函數,Problem
構造如下:
double x1 = 3.0; double x2 = -1.0; double x3 = 0.0; double x4 = 1.0;
Problem problem;
// Add residual terms to the problem using the using the autodiff
// wrapper to get the derivatives automatically.
problem.AddResidualBlock(
new AutoDiffCostFunction<F1, 1, 1, 1>(new F1), NULL, &x1, &x2);
problem.AddResidualBlock(
new AutoDiffCostFunction<F2, 1, 1, 1>(new F2), NULL, &x3, &x4);
problem.AddResidualBlock(
new AutoDiffCostFunction<F3, 1, 1, 1>(new F3), NULL, &x2, &x3)
problem.AddResidualBlock(
new AutoDiffCostFunction<F4, 1, 1, 1>(new F4), NULL, &x1, &x4);
請注意,每個ResidualBlock
僅取決於相應殘差對象所依賴的兩個參數,而不取決於所有四個參數。編譯和運行**examples/powell.cc **將得到
Initial x1 = 3, x2 = -1, x3 = 0, x4 = 1
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time
0 1.075000e+02 0.00e+00 1.55e+02 0.00e+00 0.00e+00 1.00e+04 0 4.95e-04 2.30e-03
1 5.036190e+00 1.02e+02 2.00e+01 2.16e+00 9.53e-01 3.00e+04 1 4.39e-05 2.40e-03
2 3.148168e-01 4.72e+00 2.50e+00 6.23e-01 9.37e-01 9.00e+04 1 9.06e-06 2.43e-03
3 1.967760e-02 2.95e-01 3.13e-01 3.08e-01 9.37e-01 2.70e+05 1 8.11e-06 2.45e-03
4 1.229900e-03 1.84e-02 3.91e-02 1.54e-01 9.37e-01 8.10e+05 1 6.91e-06 2.48e-03
5 7.687123e-05 1.15e-03 4.89e-03 7.69e-02 9.37e-01 2.43e+06 1 7.87e-06 2.50e-03
6 4.804625e-06 7.21e-05 6.11e-04 3.85e-02 9.37e-01 7.29e+06 1 5.96e-06 2.52e-03
7 3.003028e-07 4.50e-06 7.64e-05 1.92e-02 9.37e-01 2.19e+07 1 5.96e-06 2.55e-03
8 1.877006e-08 2.82e-07 9.54e-06 9.62e-03 9.37e-01 6.56e+07 1 5.96e-06 2.57e-03
9 1.173223e-09 1.76e-08 1.19e-06 4.81e-03 9.37e-01 1.97e+08 1 7.87e-06 2.60e-03
10 7.333425e-11 1.10e-09 1.49e-07 2.40e-03 9.37e-01 5.90e+08 1 6.20e-06 2.63e-03
11 4.584044e-12 6.88e-11 1.86e-08 1.20e-03 9.37e-01 1.77e+09 1 6.91e-06 2.65e-03
12 2.865573e-13 4.30e-12 2.33e-09 6.02e-04 9.37e-01 5.31e+09 1 5.96e-06 2.67e-03
13 1.791438e-14 2.69e-13 2.91e-10 3.01e-04 9.37e-01 1.59e+10 1 7.15e-06 2.69e-03
Ceres Solver v1.12.0 Solve Report
----------------------------------
Original Reduced
Parameter blocks 4 4
Parameters 4 4
Residual blocks 4 4
Residual 4 4
Minimizer TRUST_REGION
Dense linear algebra library EIGEN
Trust region strategy LEVENBERG_MARQUARDT
Given Used
Linear solver DENSE_QR DENSE_QR
Threads 1 1
Linear solver threads 1 1
Cost:
Initial 1.075000e+02
Final 1.791438e-14
Change 1.075000e+02
Minimizer iterations 14
Successful steps 14
Unsuccessful steps 0
Time (in seconds):
Preprocessor 0.002
Residual evaluation 0.000
Jacobian evaluation 0.000
Linear solver 0.000
Minimizer 0.001
Postprocessor 0.000
Total 0.005
Termination: CONVERGENCE (Gradient tolerance reached. Gradient max norm: 3.642190e-11 <= 1.000000e-10)
Final x1 = 0.000292189, x2 = -2.92189e-05, x3 = 4.79511e-05, x4 = 4.79511e-05
很容易看出這個問題的最優解是在時目標函數值爲0。在10次迭代中,Ceres找到一個具有目標函數值的解 。
曲線擬合
直到現在我們看到的例子都是簡單的沒有數據的優化問題。最小二乘與非線性最小二乘分析的最初目的是對數據擬合。現在讓我們考慮這樣的一個例子,它是樣本曲線加上標準差爲高斯噪聲生成的數據。讓我們擬合該數據曲線
首先我們定義一個模板對象估計殘差,這樣每一次觀察將要有一個殘差
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:
// Observations for a sample.
const double x_;
const double y_;
};
假設觀察數據的大小是2n數組,叫做data
,對於每一次觀察,創建一個CostFunction
增加到Problem
中
//設置m,c的初值
double m = 0.0;
double c = 0.0;
//kNumObservations = 67
Problem problem;
for (int i = 0; i < kNumObservations; ++i) {
CostFunction* cost_function =
new AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(
new ExponentialResidual(data[2 * i], data[2 * i + 1]));
problem.AddResidualBlock(cost_function, NULL, &m, &c);
}
計算和編譯**examples/curve_fitting.cc **
iter cost cost_change |gradient| |step| tr_ratio tr_radius ls_iter iter_time total_time
0 1.211734e+02 0.00e+00 3.61e+02 0.00e+00 0.00e+00 1.00e+04 0 4.26e-05 8.59e-05
1 1.211734e+02 -2.21e+03 0.00e+00 7.52e-01 -1.87e+01 5.00e+03 1 3.89e-05 1.74e-04
2 1.211734e+02 -2.21e+03 0.00e+00 7.51e-01 -1.86e+01 1.25e+03 1 1.36e-05 1.97e-04
3 1.211734e+02 -2.19e+03 0.00e+00 7.48e-01 -1.85e+01 1.56e+02 1 1.18e-05 2.15e-04
4 1.211734e+02 -2.02e+03 0.00e+00 7.22e-01 -1.70e+01 9.77e+00 1 1.11e-05 2.31e-04
5 1.211734e+02 -7.34e+02 0.00e+00 5.78e-01 -6.32e+00 3.05e-01 1 1.14e-05 2.48e-04
6 3.306595e+01 8.81e+01 4.10e+02 3.18e-01 1.37e+00 9.16e-01 1 2.95e-05 2.83e-04
7 6.426770e+00 2.66e+01 1.81e+02 1.29e-01 1.10e+00 2.75e+00 1 2.48e-05 3.14e-04
8 3.344546e+00 3.08e+00 5.51e+01 3.05e-02 1.03e+00 8.24e+00 1 2.45e-05 3.44e-04
9 1.987485e+00 1.36e+00 2.33e+01 8.87e-02 9.94e-01 2.47e+01 1 2.69e-05 3.76e-04
10 1.211585e+00 7.76e-01 8.22e+00 1.05e-01 9.89e-01 7.42e+01 1 2.46e-05 4.06e-04
11 1.063265e+00 1.48e-01 1.44e+00 6.06e-02 9.97e-01 2.22e+02 1 2.40e-05 4.34e-04
12 1.056795e+00 6.47e-03 1.18e-01 1.47e-02 1.00e+00 6.67e+02 1 2.41e-05 4.64e-04
13 1.056751e+00 4.39e-05 3.79e-03 1.28e-03 1.00e+00 2.00e+03 1 2.43e-05 4.93e-04
Solver Summary (v 1.11.0-eigen-(3.2.10)-lapack-suitesparse-(4.4.6)-openmp)
Original Reduced
Parameter blocks 2 2
Parameters 2 2
Residual blocks 67 67
Residual 67 67
Minimizer TRUST_REGION
Dense linear algebra library EIGEN
Trust region strategy LEVENBERG_MARQUARDT
Given Used
Linear solver DENSE_QR DENSE_QR
Threads 1 1
Linear solver threads 1 1
Cost:
Initial 1.211734e+02
Final 1.056751e+00
Change 1.201167e+02
Minimizer iterations 13
Successful steps 8
Unsuccessful steps 5
Time (in seconds):
Preprocessor 0.0000
Residual evaluation 0.0001
Jacobian evaluation 0.0001
Linear solver 0.0000
Minimizer 0.0005
Postprocessor 0.0000
Total 0.0005
Termination: CONVERGENCE (Function tolerance reached. |cost_change|/cost: 3.541695e-08 <= 1.000000e-06)
Initial m: 0 c: 0
Final m: 0.291861 c: 0.131439
以作爲開始的初始目標函數值爲1.211734e+02,Ceres找到一個解使得目標函數的值爲1.05675,這些值與原始的參數值有些不同,但是是希望的,因爲我們構造的曲線來自帶有噪聲的數據,我們希望有些偏差。實際上,如果要估計m = 0.3,c = 0.1的目標函數,則擬合值會更差,目標函數值爲1.082425。 下圖說明了適合度。
魯棒性曲線擬合
現在假設我們給定的數據有一些outliers,也就是有一些點不符合噪聲模型。我們仍然使用上面的代碼擬合這樣的數據,得到的擬合曲線如下圖,注意看擬合的曲線如何偏離真實的曲線
爲了解決outliers問題,一個標準的技術是使用LossFunction
。損失函數能夠減少帶有很高殘差數據的影響,這些數據通常是outliers,應該減少其影響。爲了在殘差塊中使用損失函數,我們改變
problem.AddResidualBlock(cost_function, NULL , &m, &c);
到
problem.AddResidualBlock(cost_function, new CauchyLoss(0.5) , &m, &c);
CauchyLoss
是Ceres Solver附帶的損失函數之一。 參數0.5指定損失函數的比例。 結果,我們得到了下面的擬合。 注意擬合曲線如何向後靠近實際曲線移動。