slam十四讲-ch6-非线性优化(包含手写高斯牛顿、使用g2o库、使用ceres库三种方法的源码详细注释)

一、自写高斯-牛顿法

该程序是要进行一个非线性优化,对非线性函数的系数进行优化
y=exp(ax2+bx+c)
给定初始的系数 ae,be,ce(估计的) ar,br,cr(真实的)
源码如下:

//
// Created by wenbo on 2020/10/28.
//
//该程序是手写高斯牛顿法,对一个简单的非线性函数进行优化
//y=exp(ax2+bx+c)
#include<iostream>
#include<opencv2/opencv.hpp>
#include <Eigen/Core>
#include <Eigen/Dense>
#include <chrono>
using namespace std;
using namespace Eigen;
int main(int argc ,char** argv)
{
    
    
    double ar=1.0,br=2.0,cr=1.0;//真实的参数
    double ae =2.0,be=-1.0,ce=5.0;//估计参数值,对这组数进行优化
    int N=100;//数据的个数
    double w_sigma=1.0;//噪声sigma的值
    double inv_sigma=1.0/w_sigma;//求个倒数,??
    //利用opencv随机数产生器
    cv::RNG rng;
    vector<double> x_data,y_data;//定义容器分别来接受x ,y的数据
    //得到真实值
    for(int i=0;i<N;i++)
    {
    
    
        double x=i/100.0;//得到数据点 0,,,,,,,
        x_data.push_back(x);//再把数据点(自变量)放回容器x_data
        y_data.push_back(exp(ar*x*x+br*x+cr)+rng.gaussian(w_sigma*w_sigma));//w_sigma*w_sigma是噪声方差
        //这里把每个自变量所对应的真实值(y_data)算了出来,用作观测值
    }
    //高斯牛顿迭代
    int iters=100;//迭代次数
    double cost=0,lastcost=0;//本次迭代和上次迭代,最终的目标函数,要将误差进行平方
    chrono::steady_clock::time_point t1=chrono::steady_clock::now();//
    //理一下思路
    //要算出J 矩阵 利用J 矩阵 算出H矩阵
    //之后解线性方程组 Hx=b
    //判断最后的cost满足调节没有,满足就不用迭代
    //要定义误差函数    yi=y_data[i] error=yi-exp(ae*x*x+be*x+ce)//用真实值减去估计值得到误差函数
    //J矩阵是误差函数对各个进行优化的系数的导数所组成的向量
    //H=(协方差矩阵转置)*J*J.transopose()//协方差转置来源于随机噪声
    //b=-(协方差矩阵转置)*error*J
    //cost=error*error
    for(int iter=0;iter<iters;iter++)
    {
    
    

        Matrix3d H=Matrix3d::Zero();
        Vector3d b=Vector3d::Zero();
        cost=0;
        for(int i=0;i<N;i++)
        {
    
    
            double xi=x_data[i], yi=y_data[i];//拿到对应的数据点
            double error =yi-exp(ae*xi*xi+be*xi+ce);//得到误差含糊是
            Eigen::Vector3d J; // 雅可比矩阵
            J[0] = -xi * xi * exp(ae * xi * xi + be * xi + ce);  // de/da
            J[1] = -xi * exp(ae * xi * xi + be * xi + ce);  // de/db
            J[2] = -exp(ae * xi * xi + be * xi + ce);  // de/dc

            H += inv_sigma * inv_sigma * J * J.transpose();
            b += -inv_sigma * inv_sigma * error * J;
            cost += error * error;
        }
        //求解线性方程组 Hx=b
        Vector3d dx=H.ldlt().solve(b);//利用ldlt分解
        //也可用QR分解
       //Vector3d dx=H.colPivHouseholderQr().solve(b);//利用Qr分解


        //isnan() 判断是不是NAN值(not a number非法数字)
        // 标准库中定义了一个宏:NAN来表示非法数字。
        // 比如负数开方、负数求对数、0.0/0.0、0.0* INFINITY(无穷大)、INFINITY/INFINITY、INFINITY-INFINITY
        // 以上操作都会得到NAN。
        // 注意:如果是整数0/0会产生操作异常
        //isinf()测试某个浮点数是否是无限大,其中INF表示无穷大

        if(isnan(dx[0]))
        {
    
    
            cout<<"result is nan"<<endl;
            break;
        }
       //如果优化结束,就应该退出迭代
        if(iter>0&&cost>=lastcost)//判断迭代结束
        {
    
    
            cout<<"cost="<<cost<<">="<<"lastcost="<<lastcost<<",break!"<<endl;
            break;
        }
        //否则继续优化系数
        ae+=dx[0];
        be+=dx[1];
        ce+=dx[2];
        lastcost=cost;
        cout<<"cost:"<<cost<<",\t\t update: "<<dx.transpose()<<
        "\t\t estimated params: "<<ae<<","<<be<<","<<ce<<endl;
    }
    chrono::steady_clock::time_point t2=chrono::steady_clock::now();
    chrono::duration<double> time_used=chrono::duration_cast<chrono::duration<double>>(t2-t1);
    cout<<"cost_time="<<time_used.count()<<endl;
    cout<<"estimated abc ="<<ae<<","<<be<<","<<ce<<endl;
    return 0;
}


运行结果如下:

cost:3.19575e+06,		 update: 0.0455771  0.078164 -0.985329		 estimated params: 2.04558,-0.921836,4.01467
cost:376785,		 update:  0.065762  0.224972 -0.962521		 estimated params: 2.11134,-0.696864,3.05215
cost:35673.6,		 update: -0.0670241   0.617616  -0.907497		 estimated params: 2.04432,-0.0792484,2.14465
cost:2195.01,		 update: -0.522767   1.19192 -0.756452		 estimated params: 1.52155,1.11267,1.3882
cost:174.853,		 update: -0.537502  0.909933 -0.386395		 estimated params: 0.984045,2.0226,1.00181
cost:102.78,		 update: -0.0919666   0.147331 -0.0573675		 estimated params: 0.892079,2.16994,0.944438
cost:101.937,		 update: -0.00117081  0.00196749 -0.00081055		 estimated params: 0.890908,2.1719,0.943628
cost:101.937,		 update:   3.4312e-06 -4.28555e-06  1.08348e-06		 estimated params: 0.890912,2.1719,0.943629
cost:101.937,		 update: -2.01204e-08  2.68928e-08 -7.86602e-09		 estimated params: 0.890912,2.1719,0.943629
cost=101.937>=lastcost=101.937,break!
cost_time=0.000228831
estimated abc =0.890912,2.1719,0.943629

2、利用g2o进行非线性优化

该程序是要进行一个非线性优化,对非线性函数的系数进行优化
y=exp(ax2+bx+c)
给定初始的系数 ae,be,ce(估计的) ar,br,cr(真实的)
源码如下:

//
//
//g2o
#include <iostream>
#include <g2o/core/g2o_core_api.h>
#include <g2o/core/base_vertex.h>
#include <g2o/core/base_unary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/core/optimization_algorithm_gauss_newton.h>
#include <g2o/core/optimization_algorithm_dogleg.h>
#include <g2o/solvers/dense/linear_solver_dense.h>
#include <Eigen/Core>
#include <opencv2/core/core.hpp>
#include <cmath>
#include <chrono>

using namespace std;
class CurveFittingVertex: public g2o::BaseVertex<3,Eigen::Vector3d>//定义顶点类。继承g2o的父类(基础顶点类)
{
    
    
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW//这个必须写,我也不知道为什么
    //重置,估计系数(待优化的系数)重写虚函数
    // override保留字表示当前函数重写了基类的虚函数
    virtual void setToOriginImpl() override
    {
    
    
        _estimate<<0,0,0;//设置估计的系数
    }
    //更新估计系数(待优化的系数)重写虚函数
    virtual void oplusImpl(const double *update) override
    {
    
    
        _estimate += Eigen::Vector3d(update);
    }
    //  存盘和读盘:留空
      virtual bool read(istream &in) {
    
    }
    //
      virtual bool write(ostream &out) const {
    
    }
};

//误差模板(边)  模板参数:观测值(真实值 y)维度,类型。链接顶点类型
class CurveFittingEdge: public g2o::BaseUnaryEdge<1,double,CurveFittingVertex>
{
    
    
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW//这个必须写,我也不知道为什么
    //有参构造
    CurveFittingEdge(double x): BaseUnaryEdge(), _x(x) {
    
    }//这里的构造函数里面还调用了BaseUnaryEdge的构造函数
    //计算曲线模型误差 重写虚函数
    virtual void computeError() override
    {
    
    
        const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]);
        const Eigen::Vector3d abc = v->estimate();//abc待优化的系数
        //误差函数 =y-exp(ax2+bx+c)  这里面的abc都是待优化的系数
        _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0));
    }
    // 计算雅可比矩阵 重写虚函数
    virtual void linearizeOplus() override
    {
    
    
        //定义顶点
        const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]);
        //拿出顶点模板里面的待优化系数
        const Eigen::Vector3d abc = v->estimate();
        //计算误差
        _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0));
    }
    // 存盘和读盘:留空
    virtual bool read(istream &in) {
    
    }

    virtual bool write(ostream &out) const {
    
    }
//属性
public:
    double _x;  // x 值, y 值为 _measurement
};
//至此顶点和边的模板就写完了
int main(int argc ,char** argv)
{
    
    
    double ar = 1.0, br = 2.0, cr = 1.0;         // 真实参数值
    double ae = 2.0, be = -1.0, ce = 5.0;        // 估计参数值
    int N = 100;                                 // 数据点
    double w_sigma = 1.0;                        // 噪声Sigma值
    double inv_sigma = 1.0 / w_sigma;
    cv::RNG rng;                                 // OpenCV随机数产生器

    vector<double> x_data, y_data;      // 数据
    //真实值,
    // 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(exp(ar * x * x + br * x + cr) + rng.gaussian(w_sigma * w_sigma));
    }
    // 构建图优化,先设定g2o
    //定义维度类型
    typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType;  // 每个误差项优化变量维度为3,误差值维度为1
    //线性求解器类型
    typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;
    // 梯度下降方法,可以从GN, LM, DogLeg 中选
    //把维度信息,线性求解器类型,算法放进去求解器
    auto solver = new g2o::OptimizationAlgorithmGaussNewton(
            g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
    g2o::SparseOptimizer optimizer;     // 图模型
    optimizer.setAlgorithm(solver);   // 设置求解器
    optimizer.setVerbose(true);       // 打开调试输出
    //上面把图优化构建好了
    //传入数据
    // 往图中增加顶点
    CurveFittingVertex *v = new CurveFittingVertex();//创建顶点
    v->setEstimate(Eigen::Vector3d(ae, be, ce));//设置估计值
    v->setId(0);//编号
    optimizer.addVertex(v);//加入顶点

    // 往图中增加边
    for (int i = 0; i < N; i++) {
    
    
        CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]);//   创建边
        edge->setId(i);  //编号
        edge->setVertex(0, v);                // 设置连接的顶点。顶点和对应编号的边连接
        edge->setMeasurement(y_data[i]);      // 观测数值  (真实值)
        edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity() * 1 / (w_sigma * w_sigma)); // 信息矩阵:  1 / (w_sigma * w_sigma)协方差矩阵之逆
        optimizer.addEdge(edge); //加入边
    }
     // 执行优化
      cout << "start optimization" << endl;

      chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
      optimizer.initializeOptimization();       //开始优化
      optimizer.optimize(10); //迭代次数10
      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;

      // 输出优化值
      Eigen::Vector3d abc_estimate = v->estimate();
      cout << "estimated model: " << abc_estimate.transpose() << endl;
    return 0;
}

运行结果如下:

start optimization
solve time cost = 0.00582179 seconds. 
estimated model:  2 -1  5
iteration= 0	 chi2= 3195746.523015	 time= 2.9264e-05	 cumTime= 2.9264e-05	 edges= 100	 schur= 0
iteration= 1	 chi2= 3195746.523015	 time= 2.7616e-05	 cumTime= 5.688e-05	 edges= 100	 schur= 0
iteration= 2	 chi2= 3195746.523015	 time= 2.8487e-05	 cumTime= 8.5367e-05	 edges= 100	 schur= 0
iteration= 3	 chi2= 3195746.523015	 time= 5.7382e-05	 cumTime= 0.000142749	 edges= 100	 schur= 0
iteration= 4	 chi2= 3195746.523015	 time= 3.061e-05	 cumTime= 0.000173359	 edges= 100	 schur= 0
iteration= 5	 chi2= 3195746.523015	 time= 2.9421e-05	 cumTime= 0.00020278	 edges= 100	 schur= 0
iteration= 6	 chi2= 3195746.523015	 time= 2.0262e-05	 cumTime= 0.000223042	 edges= 100	 schur= 0
iteration= 7	 chi2= 3195746.523015	 time= 2.1139e-05	 cumTime= 0.000244181	 edges= 100	 schur= 0
iteration= 8	 chi2= 3195746.523015	 time= 2.026e-05	 cumTime= 0.000264441	 edges= 100	 schur= 0
iteration= 9	 chi2= 3195746.523015	 time= 2.5783e-05	 cumTime= 0.000290224	 edges= 100	 schur= 0

3、利用ceres进行非线性优化

源码如下:

//
//
#include<iostream>
#include<opencv2/core/core.hpp>
#include <ceres/ceres.h>
#include <chrono>
using namespace std;
//利用ceres进行非线性优化(曲线拟合)
//步骤:
//1、定义内个参数块,位每个参数分配double数组来存储变量的值
//2、定义残差块的计算方式
//3、残差块往往也需要定义雅克比的计算方式
//4、把左右的参数块和残差块加入Ceres定义的Problem的对象中,调用solve函数求解即可

//代价函数的计算模型,这里面计算的是误差(残差)函数 Ceres应该会对这个误差函数进行平方,得到目标函数
struct CURVE_FITTING_COST
{
    
    
    CURVE_FITTING_COST(double x,double y):_x(x),_y(y) {
    
    }//初始化数据 这里面的_x _y 是真实的数据,_y也就是观测值
    //残差计算  y-exp(ax2+bx+c)  ,其中这个式子里面的a b c值咱们的估计值,也就是希望优化的系数
    //重载()运算符,()运算符能够利用其传入的参数abc,residual,算出残差,返回true
    //我个人理解是,在构建Problem的时候,这个重载被集成在Problem里了
    template<typename T>
    bool operator ()(const T *const abc, T *residual)
    const{
    
    
        // y-exp(ax^2+bx+c),其中这个式子里面的a b c值咱们的估计值,也就是希望优化的系数
        residual[0] = T(_y) - ceres::exp(abc[0] * T(_x) * T(_x) + abc[1] * T(_x) + abc[2]);
        return true;
    }
    const double _x,_y;
};

int main(int argc,char** argv)
{
    
    
    double ar=1.0,br=2.0,cr=1.0;//真实的参数
    double ae =2.0,be=-1.0,ce=5.0;//估计参数值,对这组数进行优化
    double abc[3]={
    
    ae,be,ce};
    int N=100;//数据的个数
    double w_sigma=1.0;//噪声sigma的值
    double inv_sigma=1.0/w_sigma;  //求个倒数,??
    //利用opencv随机数产生器
    cv::RNG rng;
    vector<double> x_data,y_data;//定义容器分别来接受x ,y的数据
    //得到真实值
    for(int i=0;i<N;i++)
    {
    
    
        double x=i/100.0;//得到数据点 0,,,,,,,
        x_data.push_back(x);//再把数据点(自变量)放回容器x_data
        y_data.push_back(exp(ar*x*x+br*x+cr)+rng.gaussian(w_sigma*w_sigma));//w_sigma*w_sigma是噪声方差
        //这里把每个自变量所对应的真实值(y_data)算了出来,用作观测值
    }
    //构建最小二乘问题,也就是非线性优化问题
    ceres::Problem problem;
    //把每个数据点都放进problem中
    for(int i=0;i<N;i++)
    {
    
    
        problem.AddResidualBlock(
                // 使用自动求导,模板参数:误差类型,输出维度,输入维度,维数要与前面struct中一致
                new ceres::AutoDiffCostFunction<CURVE_FITTING_COST,1,3>(
                        //传入真实数据
                        new CURVE_FITTING_COST(x_data[i],y_data[i])
                        ),
                nullptr,//核函数,这里不使用,空就完事了
                abc//待优化的系数,也就是咱们的估计值
                );
    }

    //配置求解器
    ceres::Solver::Options options;//options用来配置
    options.linear_solver_type=ceres::DENSE_NORMAL_CHOLESKY;//利用DENSE_NORMAL_CHOLESKY求解增量方差 Hx=b
    options.minimizer_progress_to_stdout=true;//允许 输出到cout
    ceres::Solver::Summary summary;//优化信息

    chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
    ceres::Solve(options, &problem, &summary);  // 开始优化  将配置信息和要求解的问题放进去,把优化的信息放在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 ";
    for(auto a:abc) cout<<a<<" ";//遍历abc
    cout<<endl;

    return 0;
}

运行结果如下:

iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  1.597873e+06    0.00e+00    3.52e+06   0.00e+00   0.00e+00  1.00e+04        0    6.29e-05    1.43e-04
   1  1.884440e+05    1.41e+06    4.86e+05   9.88e-01   8.82e-01  1.81e+04        1    1.48e-04    1.81e-03
   2  1.784821e+04    1.71e+05    6.78e+04   9.89e-01   9.06e-01  3.87e+04        1    6.89e-05    2.02e-03
   3  1.099631e+03    1.67e+04    8.58e+03   1.10e+00   9.41e-01  1.16e+05        1    8.80e-05    2.25e-03
   4  8.784938e+01    1.01e+03    6.53e+02   1.51e+00   9.67e-01  3.48e+05        1    4.10e-05    2.42e-03
   5  5.141230e+01    3.64e+01    2.72e+01   1.13e+00   9.90e-01  1.05e+06        1    7.10e-05    2.61e-03
   6  5.096862e+01    4.44e-01    4.27e-01   1.89e-01   9.98e-01  3.14e+06        1    6.60e-05    2.82e-03
   7  5.096851e+01    1.10e-04    9.53e-04   2.84e-03   9.99e-01  9.41e+06        1    6.51e-05    3.03e-03
solve time cost = 0.00320557 seconds. 
Ceres Solver Report: Iterations: 8, Initial cost: 1.597873e+06, Final cost: 5.096851e+01, Termination: CONVERGENCE
estimated a,b,c 0.890908 2.1719 0.943628 

猜你喜欢

转载自blog.csdn.net/joun772/article/details/109365046