C++手写高斯牛顿法

因为毕设要用Bundle Adjustment 进行数据优化,涉及到优化方法的实现,所以想训练一下编程实现的能力。这篇文章也是参考SLAM14讲,通过高斯牛顿法来完成曲线拟合,从而熟悉高斯牛顿法流程和编程

一、只用eigen库实现

1.1 头文件

#include <iostream>
#include <chrono>	//计时模块,非核心代码
#include <opencv2/opencv.hpp>
#include <Eigen/Core>
#include <Eigen/Dense>

1.2 高斯牛顿法原理

设曲线为

y

=

e

x

p

(

a

x

2

+

b

x

+

c

)

y=exp(ax^2+bx+c)

y=exp(ax2+bx+c)
加噪声后为

y

=

e

x

p

(

a

x

2

+

b

x

+

c

)

+

W

,

W

y=exp(ax^2+bx+c)+W ,W为噪声

y=exp(ax2+bx+c)+W,W
获得多个曲线数据点

(

x

i

,

y

i

)

(x_i,y_i)

(xi,yi)
目标函数为

F

=

a

r

g

(

a

,

b

,

c

)

m

i

n

i

=

1

N

e

i

2

=

(

y

i

e

x

p

(

a

x

i

2

+

b

x

i

+

c

)

F=_{arg(a,b,c)}min\sum_{i=1}^Ne_i^2=(y_i-exp(ax_i^2+bx_i+c)

F=arg(a,b,c)mini=1Nei2=(yiexp(axi2+bxi+c)
那么对每项误差

e

i

e_i

ei求雅可比得到

J

i

J_i

Ji,得到增量方程

(

i

=

1

N

J

i

J

i

T

)

d

x

=

i

=

1

N

(

J

i

e

i

)

(\sum_{i=1}^NJ_iJ_i^T)dx=\sum_{i=1}^N(-J_ie_i)

(i=1NJiJiT)dx=i=1N(Jiei)
求解线性方程,得到增量,更新变量,检查cost是否下降,是的话继续迭代,直到收敛为止

1.3 代码

曲线数据生成

  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;      // 数据
  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));
  }

迭代优化

// 开始Gauss-Newton迭代
  chrono::steady_clock::time_point t1 = chrono::steady_clock::now(); //计时器
  int iterations = 100;    // 迭代次数
  double cost = 0, lastCost = 0;  // 本次迭代的cost和上一次迭代的cost
  double err;  //每个误差项误差
  bool switch1=true;
//    bool switch1= false;
  if(switch1) {
//      iterations=10;
      for (int i = 0; i < iterations; ++i) {
          //****每次迭代前,把这几个变量置0很重要,之前就是因为没有置0导致数值累计,数值越来越大,优化结果错误了。
          Vector3d J; //误差函数雅克比
          Matrix<double, 3, 3> H = Matrix3d::Zero();   //高斯牛顿矩阵
          Vector3d g = Vector3d::Zero();     //增量方程右边向量
          cost=0;
          //加载高斯牛顿矩阵、增量方程右边向量、误差平方和
          for (int j = 0; j < N; j++) {
              J[0] = -x_data[j] * x_data[j] * exp(ae * x_data[j] * x_data[j] + be * x_data[j] + ce);  //de/da
              J[1] = -x_data[j] * exp(ae * x_data[j] * x_data[j] + be * x_data[j] + ce);    //de/db
              J[2] = -exp(ae * x_data[j] * x_data[j] + be * x_data[j] + ce);      //de/dc
              err = y_data[j] - exp(ae * x_data[j] * x_data[j] + be * x_data[j] + ce);  //每一项的误差
              H += J * J.transpose();
              g -= J * err; 		//这里是负号,一定要注意,之前忘了,debug了很久
              cost += err * err;  //误差和
          }
          cout<<"第"<<i<<"迭代时J100为\t"<<J.transpose()<<endl;	//debug用的,用来检查Jacob计算是否正确
          //求解方程Hdx=g Vector3d dx = H.ldlt().solve(b);
          Vector3d dx;
          dx = H.ldlt().solve(g);	//似乎是用cholesky分解解线性方程组,没有研究
          if (isnan(dx[0])) {
              cout << "result is nan!" << endl;
              break;
          }
          //更新变量
          ae += dx[0];
          be += dx[1];
          ce += dx[2];

          //跳出循环条件
          if (i > 0 && cost >= lastCost) {		//i>0很重要,否则第一次迭代就跳出循环
              cout << "当前误差:" << cost << ">=上次误差:" << lastCost << "迭代结束" << endl;
              break;
          }

          lastCost = cost;
          cout << "迭代次数N=" << i << "\t" << "误差平方和为:" << cost << "\t" << "变量增量为:" << dx.transpose() << "\t估计的参数a,b,c:"
               << 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 << "solve time cost = " << time_used.count() << " seconds. " << endl;

  cout << "estimated abc = " << ae << ", " << be << ", " << ce << endl;
  return 0;
}

最终输出结果

0迭代时J100为	-383.791 -387.668 -391.584
迭代次数N=0	误差平方和为:3.19575e+06	变量增量为:0.0455771  0.078164 -0.985329	估计的参数a,b,c:2.04558,-0.921836,4.014671迭代时J100为	-161.875  -163.51 -165.161
迭代次数N=1	误差平方和为:376785	变量增量为: 0.065762  0.224972 -0.962521	估计的参数a,b,c:2.11134,-0.696864,3.052152迭代时J100为	-82.3911 -83.2233  -84.064
迭代次数N=2	误差平方和为:35673.6	变量增量为:-0.0670241   0.617616  -0.907497	估计的参数a,b,c:2.04432,-0.0792484,2.144653迭代时J100为	 -57.382 -57.9616 -58.5471
迭代次数N=3	误差平方和为:2195.01	变量增量为:-0.522767   1.19192 -0.756452	估计的参数a,b,c:1.52155,1.11267,1.38824迭代时J100为	-52.5053 -53.0356 -53.5714
迭代次数N=4	误差平方和为:174.853	变量增量为:-0.537502  0.909933 -0.386395	估计的参数a,b,c:0.984045,2.0226,1.001815迭代时J100为	-51.8599 -52.3838 -52.9129
迭代次数N=5	误差平方和为:102.78	变量增量为:-0.0919666   0.147331 -0.0573675	估计的参数a,b,c:0.892079,2.16994,0.9444386迭代时J100为	-51.7746 -52.2976 -52.8259
迭代次数N=6	误差平方和为:101.937	变量增量为:-0.00117081  0.00196749 -0.00081055	估计的参数a,b,c:0.890908,2.1719,0.9436287迭代时J100为	-51.7741 -52.2971 -52.8253
迭代次数N=7	误差平方和为:101.937	变量增量为:  3.4312e-06 -4.28555e-06  1.08348e-06	估计的参数a,b,c:0.890912,2.1719,0.9436298迭代时J100为	-51.7741 -52.2971 -52.8254
迭代次数N=8	误差平方和为:101.937	变量增量为:-2.01204e-08  2.68928e-08 -7.86602e-09	估计的参数a,b,c:0.890912,2.1719,0.9436299迭代时J100为	-51.7741 -52.2971 -52.8254
迭代次数N=9	误差平方和为:101.937	变量增量为:1.07059e-10 -1.4233e-10 4.12042e-11	估计的参数a,b,c:0.890912,2.1719,0.943629
solve time cost = 0.000600477 seconds. 
estimated abc = 0.890912, 2.1719, 0.943629

Process finished with exit code 0

1.4 结果可视化

由于对曲线进行了加噪,所以曲线参数a,b,c没有真值,想到通过计算的a,b,c来绘制曲线,与真实曲线对比,可以大概看出迭代过程收敛趋势
下图绘制了真实带噪声曲线,以后迭代过程中1,2,3,4及收敛次曲线
在这里插入图片描述

二、手写高斯牛顿法代码框架总结

用伪代码表示一下核心流程

for(迭代次数)
{
	//三个参数置零,否则会累加
	H=0;
	g=0;
	cost=0;
	for(遍历所有误差项ei)
	{
		计算Ji、每项的误差err;
		H+=ji*ji^t;
		g-=Ji*ei;
		cost+=err^2;
	}
	跳出循环判断
	if(cost>=lastcost)	//本例中使用cost不下降进行判断,也可以用增量的模是否小于某个阈值进行判断
	break;
	解增量方程  H*dx=g(参考高翔代码,使用的cholesky分解)
	dx=H.ldlt().solve(g); 	//需要去深入理解
	更新变量
	X=X+dx;
	输出当前cost,增量,变量
	更新lastcost
	lastcost=cost;
}

只用eigen库来实现Gauss-Newton优化总结到这,下一章再写下通过Ceres、G2o来实现优化


版权声明:本文为wenixang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>