NOTE / 8/18/2018
[Optimization] Gauss–Newton for Nonlinear Least Squares
Many problems ultimately reduce to least-squares problems, including bundle adjustment and pose-graph optimization in SLAM. Gauss–Newton is one method for solving them.
Derivation
For a nonlinear least-squares problem,
Gauss–Newton approximates with the first-order term of its Taylor expansion:
Substituting (2) into (1) gives
Differentiate this expression and set the derivative to zero:
Let and . Equation (4) becomes
Solving (5) gives the increment . This requires to be invertible (positive definite), which is not always true; the method can consequently diverge. An overly large step can also cause divergence.
The Gauss–Newton procedure is therefore:
- Choose an initial value .
- At iteration , calculate , form and , then solve .
- Stop when is sufficiently small; otherwise set .
- Repeat steps 2–3 until the iteration limit or a stopping condition is reached.
Implementation
Problem. For the nonlinear equation , given observations , estimate .
Analysis. Let . The observations form one nonlinear system:
We can construct the least-squares objective
Following the derivation, we need its Jacobian:
The preceding steps can then solve the problem. Implementation:
/**
* This file is part of Gauss-Newton Solver.
*
* Copyright (C) 2018-2020 Dongsheng Yang <ydsf16@buaa.edu.cn> (Beihang University)
* For more information see <https://github.com/ydsf16/Gauss_Newton_solver>
*
* Gauss_Newton_solver is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Gauss_Newton_solver is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Gauss_Newton_solver. If not, see <http://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <eigen3/Eigen/Core>
#include <vector>
#include <opencv2/opencv.hpp>
#include <eigen3/Eigen/Cholesky>
#include <eigen3/Eigen/QR>
#include <eigen3/Eigen/SVD>
#include <chrono>
/* Timer */
class Runtimer{
public:
inline void start() { t_s_ = std::chrono::steady_clock::now(); }
inline void stop() { t_e_ = std::chrono::steady_clock::now(); }
inline double duration() {
return std::chrono::duration_cast<std::chrono::duration<double>>(t_e_ - t_s_).count() * 1000.0;
}
private:
std::chrono::steady_clock::time_point t_s_; // start time point
std::chrono::steady_clock::time_point t_e_; // end time point
};
/* Optimization equation */
class CostFunction{
public:
CostFunction(double* a, double* b, double* c, int max_iter, double min_step, bool is_out):
a_(a), b_(b), c_(c), max_iter_(max_iter), min_step_(min_step), is_out_(is_out) {}
void addObservation(double x, double y) { obs_.push_back({x, y}); }
void calcJ_fx() {
J_.resize(obs_.size(), 3);
fx_.resize(obs_.size(), 1);
for (size_t i = 0; i < obs_.size(); i++) {
std::vector<double>& ob = obs_.at(i);
double& x = ob.at(0);
double& y = ob.at(1);
double j1 = -x*x*exp(*a_ * x*x + *b_*x + *c_);
double j2 = -x*exp(*a_ * x*x + *b_*x + *c_);
double j3 = -exp(*a_ * x*x + *b_*x + *c_);
J_(i, 0) = j1; J_(i, 1) = j2; J_(i, 2) = j3;
fx_(i, 0) = y - exp(*a_ *x*x + *b_*x +*c_);
}
}
void calcH_b() { H_ = J_.transpose() * J_; B_ = -J_.transpose() * fx_; }
void calcDeltax() { deltax_ = H_.ldlt().solve(B_); }
void updateX() { *a_ += deltax_(0); *b_ += deltax_(1); *c_ += deltax_(2); }
double getCost() { Eigen::MatrixXd cost = fx_.transpose() * fx_; return cost(0, 0); }
void solveByGaussNewton() {
double sumt = 0;
bool is_conv = false;
for (size_t i = 0; i < max_iter_; i++) {
Runtimer t; t.start();
calcJ_fx(); calcH_b(); calcDeltax();
double delta = deltax_.transpose() * deltax_;
t.stop();
if (is_out_) {
std::cout << "Iter: " << std::left << std::setw(3) << i << " Result: "
<< std::left << std::setw(10) << *a_ << " " << std::left << std::setw(10) << *b_ << " "
<< std::left << std::setw(10) << *c_ << " step: " << std::left << std::setw(14) << delta
<< " cost: " << std::left << std::setw(14) << getCost() << " time: " << std::left
<< std::setw(14) << t.duration() << " total_time: " << std::left << std::setw(14)
<< (sumt += t.duration()) << std::endl;
}
if (delta < min_step_) { is_conv = true; break; }
updateX();
}
std::cout << (is_conv ? "\nConverged\n" : "\nDiverged\n\n");
}
Eigen::MatrixXd fx_;
Eigen::MatrixXd J_; // Jacobian matrix
Eigen::Matrix3d H_; // H matrix
Eigen::Vector3d B_, deltax_;
std::vector<std::vector<double>> obs_; // observations
double* a_; double* b_; double* c_;
int max_iter_; double min_step_; bool is_out_;
};
int main(int argc, char **argv) {
const double aa = 0.1, bb = 0.5, cc = 2; // parameters of the ground-truth equation
double a = 0.0, b = 0.0, c = 0.0; // initial value
CostFunction cost_func(&a, &b, &c, 50, 1e-10, true); // construct the problem
const size_t N = 100; // number of observations
cv::RNG rng(cv::getTickCount());
for (size_t i = 0; i < N; i++) {
double x = rng.uniform(0.0, 1.0);
double y = exp(aa*x*x + bb*x + cc) + rng.gaussian(0.05); // generate data with Gaussian noise
cost_func.addObservation(x, y);
}
cost_func.solveByGaussNewton(); // solve with Gauss–Newton
return 0;
}