分享

Levenberg–Marquardt算法学习(和matlab的LM算法对比)

 mscdj 2018-03-30
  • 回顾高斯牛顿算法,引入LM算法
  • 惩罚因子的计算(迭代步子的计算)
  • 完整的算法流程及代码样例

1.      回顾高斯牛顿,引入LM算法

 根据之前的博文:Gauss-Newton算法学习
  假设我们研究如下形式的非线性最小二乘问题:

  r(x)为某个问题的残差residual,是关于x的非线性函数。我们知道高斯牛顿法的迭代公式:


  Levenberg–Marquardt算法是对高斯牛顿的改进,在迭代步长上略有不同:


最速下降法对初始点没有特别要求具有整体收敛性,但是相邻两次的搜索方向是相互垂直的,所以收敛并不一定快。总而言之就是:当目标函数的等值线接近于圆(球)时,下降较快;等值线类似于扁长的椭球时,一开始快,后来很慢。This is good if the current iterate is far from the solution.

  c.   如果μ的值很小,那么hlm成了高斯牛顿法的方向(适合迭代的最后阶段,非常接近最优解,避免了最速下降的震荡)


由此可见,惩罚因子既下降的方向又影响下降步子的大小。

 2.    惩罚因子的计算[迭代步长计算]

  我们的目标是求f的最小值,我们希望迭代开始时,惩罚因子μ被设定为较小的值,若


    信赖域方法与线搜索技术一样,也是优化算法中的一种保证全局收敛的重要技术. 它们的功能都是在优化算法中求出每次迭代的位移, 从而确定新的迭代点.所不同的是: 线搜索技术是先产生位移方向(亦称为搜索方向), 然后确定位移的长度(亦称为搜索步长)。而信赖域技术则是直接确定位移, 产生新的迭代点。

    信赖域方法的基本思想是:首先给定一个所谓的“信赖域半径”作为位移长度的上界,并以当前迭代点为中心以此“上界”为半径确定一个称之为“信赖域”的闭球区域。然后,通过求解这个区域内的“信赖域子问题”(目标函数的二次近似模型) 的最优点来确定“候选位移”。若候选位移能使目标函数值有充分的下降量, 则接受该候选位移作为新的位移,并保持或扩大信赖域半径, 继续新的迭代。否则, 说明二次模型与目标函数的近似度不够理想,需要缩小信赖域半径,再通过求解新的信赖域内的子问题得到新的候选位移。如此重复下去,直到满足迭代终止条件。

  现在用信赖域方法解决之前的无约束线性规划:




  如果q很大,说明L(h)非常接近F(x+h),我们可以减少惩罚因子μ,以便于下次迭代此时算法更接近高斯牛顿算法。如果q很小或者是负的,说明是poor approximation,我们需要增大惩罚因子,减少步长,此时算法更接近最速下降法。具体来说,

a.当q大于0时,此次迭代有效:




b.当q小于等于0时,此次迭代无效:




3.完整的算法流程及代码距离

LM的算法流程和高斯牛顿几乎一样,只是迭代步长求法利用信赖域法

(1)给定初始点x(0),允许误差ε>0,置k=0

(2)当f(xk+1)-f(xk)小于阈值ε时,算法退出,否则(3)

(3)xk+1=xk+hlm,代入f,返回(1)



两个例子还是沿用之前的。

例子1,根据美国1815年至1885年数据,估计人口模型中的参数A和B。如下表所示,已知年份和人口总量,及人口模型方程,求方程中的参数。


  1. // A simple demo of Gauss-Newton algorithm on a user defined function  
  2.   
  3. #include <cstdio>  
  4. #include <vector>  
  5. #include <opencv2/core/core.hpp>  
  6.   
  7. using namespace std;  
  8. using namespace cv;  
  9.   
  10. const double DERIV_STEP = 1e-5;  
  11. const int MAX_ITER = 100;  
  12.   
  13.   
  14. void LM(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer  
  15.                  const Mat &inputs, const Mat &outputs, Mat ¶ms);  
  16.   
  17. double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer  
  18.              const Mat &input, const Mat ¶ms, int n);  
  19.   
  20. // The user defines their function here  
  21. double Func(const Mat &input, const Mat ¶ms);  
  22.   
  23. int main()  
  24. {  
  25.     // For this demo we're going to try and fit to the function  
  26.     // F = A*exp(t*B)  
  27.     // There are 2 parameters: A B  
  28.     int num_params = 2;  
  29.   
  30.     // Generate random data using these parameters  
  31.     int total_data = 8;  
  32.   
  33.     Mat inputs(total_data, 1, CV_64F);  
  34.     Mat outputs(total_data, 1, CV_64F);  
  35.   
  36.     //load observation data  
  37.     for(int i=0; i < total_data; i++) {  
  38.         inputs.at<double>(i,0) = i+1;  //load year  
  39.     }  
  40.     //load America population  
  41.     outputs.at<double>(0,0)= 8.3;  
  42.     outputs.at<double>(1,0)= 11.0;  
  43.     outputs.at<double>(2,0)= 14.7;  
  44.     outputs.at<double>(3,0)= 19.7;  
  45.     outputs.at<double>(4,0)= 26.7;  
  46.     outputs.at<double>(5,0)= 35.2;  
  47.     outputs.at<double>(6,0)= 44.4;  
  48.     outputs.at<double>(7,0)= 55.9;  
  49.   
  50.     // Guess the parameters, it should be close to the true value, else it can fail for very sensitive functions!  
  51.     Mat params(num_params, 1, CV_64F);  
  52.   
  53.     //init guess  
  54.     params.at<double>(0,0) = 6;  
  55.     params.at<double>(1,0) = 0.3;  
  56.   
  57.     LM(Func, inputs, outputs, params);  
  58.   
  59.     printf("Parameters from GaussNewton: %lf %lf\n", params.at<double>(0,0), params.at<double>(1,0));  
  60.   
  61.     return 0;  
  62. }  
  63.   
  64. double Func(const Mat &input, const Mat ¶ms)  
  65. {  
  66.     // Assumes input is a single row matrix  
  67.     // Assumes params is a column matrix  
  68.   
  69.     double A = params.at<double>(0,0);  
  70.     double B = params.at<double>(1,0);  
  71.   
  72.     double x = input.at<double>(0,0);  
  73.   
  74.     return A*exp(x*B);  
  75. }  
  76.   
  77. //calc the n-th params' partial derivation , the params are our  final target  
  78. double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), const Mat &input, const Mat ¶ms, int n)  
  79. {  
  80.     // Assumes input is a single row matrix  
  81.   
  82.     // Returns the derivative of the nth parameter  
  83.     Mat params1 = params.clone();  
  84.     Mat params2 = params.clone();  
  85.   
  86.     // Use central difference  to get derivative  
  87.     params1.at<double>(n,0) -= DERIV_STEP;  
  88.     params2.at<double>(n,0) += DERIV_STEP;  
  89.   
  90.     double p1 = Func(input, params1);  
  91.     double p2 = Func(input, params2);  
  92.   
  93.     double d = (p2 - p1) / (2*DERIV_STEP);  
  94.   
  95.     return d;  
  96. }  
  97.   
  98. void LM(double(*Func)(const Mat &input, const Mat ¶ms),  
  99.                  const Mat &inputs, const Mat &outputs, Mat ¶ms)  
  100. {  
  101.     int m = inputs.rows;  
  102.     int n = inputs.cols;  
  103.     int num_params = params.rows;  
  104.   
  105.     Mat r(m, 1, CV_64F); // residual matrix  
  106.     Mat r_tmp(m, 1, CV_64F);  
  107.     Mat Jf(m, num_params, CV_64F); // Jacobian of Func()  
  108.     Mat input(1, n, CV_64F); // single row input  
  109.     Mat params_tmp = params.clone();  
  110.   
  111.     double last_mse = 0;  
  112.     float u = 1, v = 2;  
  113.     Mat I = Mat::ones(num_params, num_params, CV_64F);//construct identity matrix  
  114.     int i =0;  
  115.     for(i=0; i < MAX_ITER; i++) {  
  116.         double mse = 0;  
  117.         double mse_temp = 0;  
  118.   
  119.         for(int j=0; j < m; j++) {  
  120.             for(int k=0; k < n; k++) {//copy Independent variable vector, the year  
  121.                 input.at<double>(0,k) = inputs.at<double>(j,k);  
  122.             }  
  123.   
  124.             r.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params);//diff between previous estimate and observation population  
  125.   
  126.             mse += r.at<double>(j,0)*r.at<double>(j,0);  
  127.   
  128.             for(int k=0; k < num_params; k++) {  
  129.                 Jf.at<double>(j,k) = Deriv(Func, input, params, k);  //construct jacobian matrix  
  130.             }  
  131.         }  
  132.   
  133.         mse /= m;  
  134.         params_tmp = params.clone();  
  135.           
  136.         Mat hlm = (Jf.t()*Jf + u*I).inv()*Jf.t()*r;  
  137.         params_tmp += hlm;   
  138.         for(int j=0; j < m; j++) {  
  139.             r_tmp.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params_tmp);//diff between current estimate and observation population  
  140.             mse_temp += r_tmp.at<double>(j,0)*r_tmp.at<double>(j,0);  
  141.         }  
  142.   
  143.         mse_temp /= m;  
  144.   
  145.         Mat q(1,1,CV_64F);  
  146.         q = (mse - mse_temp)/(0.5*hlm.t()*(u*hlm-Jf.t()*r));  
  147.         double q_value = q.at<double>(0,0);  
  148.         if(q_value>0)  
  149.         {  
  150.             double s = 1.0/3.0;  
  151.             v = 2;  
  152.             mse = mse_temp;  
  153.             params = params_tmp;  
  154.             double temp = 1 - pow(2*q_value-1,3);  
  155.             if(s>temp)  
  156.             {  
  157.                 u = u * s;  
  158.             }else  
  159.             {  
  160.                 u = u * temp;  
  161.             }  
  162.         }else  
  163.         {  
  164.             u = u*v;   
  165.             v = 2*v;  
  166.             params = params_tmp;  
  167.         }     
  168.           
  169.       
  170.         // The difference in mse is very small, so quit  
  171.         if(fabs(mse - last_mse) < 1e-8) {  
  172.             break;  
  173.         }  
  174.   
  175.         //printf("%d: mse=%f\n", i, mse);  
  176.         printf("%d %lf\n", i, mse);  
  177.         last_mse = mse;  
  178.     }  
  179. }  


A=7.0,B=0.26  (初始值,A=6,B=0.3),100次迭代到第7次收敛了。和之前差不多,但是LM对于初始的选择是非常敏感的,如果A=6,B=6,则拟合失败!


我调用了matlab的接口跑LM,结果也是一样错误的,图片上可以看到拟合失败
[plain] view plain copy
  1. clc;  
  2. clear;  
  3. a0=[6,6];  
  4. options=optimset('Algorithm','Levenberg-Marquardt','Display','iter');  
  5. data_1=[1 2 3 4 5 6 7 8];  
  6. obs_1=[8.3 11.0 14.7 19.7 26.7 35.2 44.4 55.9];  
  7. a=lsqnonlin(@myfun,a0,[],[],options,data_1,obs_1);  
  8. plot(data_1,obs_1,'o');  
  9. hold on  
  10. plot(data_1,a(1)*exp(a(2)*data_1),'b');  
  11. plot(data_1,7*exp(a(2)*data_1),'b');  
  12. %hold off  
  13. a(1)  
  14. a(2)  
  15.   
  16.   
  17.   
  18.   
  19. function E = myfun(a, x,y)  
  20. %这是一个测试文件用于测试 lsqnonlin  
  21. %   Detailed explanation goes here  
  22. x=x(:);  
  23. y=y(:);  
  24. Y=a(1)*exp(a(2)*x);  
  25. E=y-Y;  
  26. end  

最后一个点拟合失败的,所以函数不对的

   因此虽然莱文博格-马夸特迭代法能够自适应的在高斯牛顿和最速下降法之间调整,既可保证在收敛较慢时迭代过程总是下降的,又可保证迭代过程在解的邻域内迅速收敛。但是,LM对于初始点选择还是比较敏感的!


例子2:我想要拟合如下模型,


  由于缺乏观测量,就自导自演,假设4个参数已知A=5,B=1,C=10,D=2,构造100个随机数作为x的观测值,计算相应的函数观测值。然后,利用这些观测值,反推4个参数。

  1. // A simple demo of Gauss-Newton algorithm on a user defined function  
  2.   
  3. #include <cstdio>  
  4. #include <vector>  
  5. #include <opencv2/core/core.hpp>  
  6.   
  7. using namespace std;  
  8. using namespace cv;  
  9.   
  10. const double DERIV_STEP = 1e-5;  
  11. const int MAX_ITER = 100;  
  12.   
  13.   
  14. void LM(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer  
  15.                  const Mat &inputs, const Mat &outputs, Mat ¶ms);  
  16.   
  17. double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer  
  18.              const Mat &input, const Mat ¶ms, int n);  
  19.   
  20. // The user defines their function here  
  21. double Func(const Mat &input, const Mat ¶ms);  
  22.   
  23. int main()  
  24. {  
  25.     // For this demo we're going to try and fit to the function  
  26.     // F = A*sin(Bx) + C*cos(Dx)  
  27.     // There are 4 parameters: A, B, C, D  
  28.     int num_params = 4;  
  29.   
  30.     // Generate random data using these parameters  
  31.     int total_data = 100;  
  32.   
  33.     double A = 5;  
  34.     double B = 1;  
  35.     double C = 10;  
  36.     double D = 2;  
  37.   
  38.     Mat inputs(total_data, 1, CV_64F);  
  39.     Mat outputs(total_data, 1, CV_64F);  
  40.   
  41.     for(int i=0; i < total_data; i++) {  
  42.         double x = -10.0 + 20.0* rand() / (1.0 + RAND_MAX); // random between [-10 and 10]  
  43.         double y = A*sin(B*x) + C*cos(D*x);  
  44.   
  45.         // Add some noise  
  46.        // y += -1.0 + 2.0*rand() / (1.0 + RAND_MAX);  
  47.   
  48.         inputs.at<double>(i,0) = x;  
  49.         outputs.at<double>(i,0) = y;  
  50.     }  
  51.   
  52.     // Guess the parameters, it should be close to the true value, else it can fail for very sensitive functions!  
  53.     Mat params(num_params, 1, CV_64F);  
  54.   
  55.     params.at<double>(0,0) = 1;  
  56.     params.at<double>(1,0) = 1;  
  57.     params.at<double>(2,0) = 8; // changing to 1 will cause it not to find the solution, too far away  
  58.     params.at<double>(3,0) = 1;  
  59.   
  60.     LM(Func, inputs, outputs, params);  
  61.   
  62.     printf("True parameters: %f %f %f %f\n", A, B, C, D);  
  63.     printf("Parameters from GaussNewton: %f %f %f %f\n", params.at<double>(0,0), params.at<double>(1,0),  
  64.                                                         params.at<double>(2,0), params.at<double>(3,0));  
  65.   
  66.     return 0;  
  67. }  
  68.   
  69. double Func(const Mat &input, const Mat ¶ms)  
  70. {  
  71.     // Assumes input is a single row matrix  
  72.     // Assumes params is a column matrix  
  73.   
  74.     double A = params.at<double>(0,0);  
  75.     double B = params.at<double>(1,0);  
  76.     double C = params.at<double>(2,0);  
  77.     double D = params.at<double>(3,0);  
  78.   
  79.     double x = input.at<double>(0,0);  
  80.   
  81.     return A*sin(B*x) + C*cos(D*x);  
  82. }  
  83.   
  84. //calc the n-th params' partial derivation , the params are our  final target  
  85. double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), const Mat &input, const Mat ¶ms, int n)  
  86. {  
  87.     // Assumes input is a single row matrix  
  88.   
  89.     // Returns the derivative of the nth parameter  
  90.     Mat params1 = params.clone();  
  91.     Mat params2 = params.clone();  
  92.   
  93.     // Use central difference  to get derivative  
  94.     params1.at<double>(n,0) -= DERIV_STEP;  
  95.     params2.at<double>(n,0) += DERIV_STEP;  
  96.   
  97.     double p1 = Func(input, params1);  
  98.     double p2 = Func(input, params2);  
  99.   
  100.     double d = (p2 - p1) / (2*DERIV_STEP);  
  101.   
  102.     return d;  
  103. }  
  104.   
  105. void LM(double(*Func)(const Mat &input, const Mat ¶ms),  
  106.                  const Mat &inputs, const Mat &outputs, Mat ¶ms)  
  107. {  
  108.     int m = inputs.rows;  
  109.     int n = inputs.cols;  
  110.     int num_params = params.rows;  
  111.   
  112.     Mat r(m, 1, CV_64F); // residual matrix  
  113.     Mat r_tmp(m, 1, CV_64F);  
  114.     Mat Jf(m, num_params, CV_64F); // Jacobian of Func()  
  115.     Mat input(1, n, CV_64F); // single row input  
  116.     Mat params_tmp = params.clone();  
  117.   
  118.     double last_mse = 0;  
  119.     float u = 1, v = 2;  
  120.     Mat I = Mat::ones(num_params, num_params, CV_64F);//construct identity matrix  
  121.     int i =0;  
  122.     for(i=0; i < MAX_ITER; i++) {  
  123.         double mse = 0;  
  124.         double mse_temp = 0;  
  125.   
  126.         for(int j=0; j < m; j++) {  
  127.             for(int k=0; k < n; k++) {//copy Independent variable vector, the year  
  128.                 input.at<double>(0,k) = inputs.at<double>(j,k);  
  129.             }  
  130.   
  131.             r.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params);//diff between estimate and observation population  
  132.   
  133.             mse += r.at<double>(j,0)*r.at<double>(j,0);  
  134.   
  135.             for(int k=0; k < num_params; k++) {  
  136.                 Jf.at<double>(j,k) = Deriv(Func, input, params, k);  //construct jacobian matrix  
  137.             }  
  138.         }  
  139.   
  140.         mse /= m;  
  141.         params_tmp = params.clone();  
  142.           
  143.         Mat hlm = (Jf.t()*Jf + u*I).inv()*Jf.t()*r;  
  144.         params_tmp += hlm;   
  145.         for(int j=0; j < m; j++) {  
  146.             r_tmp.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params_tmp);//diff between estimate and observation population  
  147.             mse_temp += r_tmp.at<double>(j,0)*r_tmp.at<double>(j,0);  
  148.         }  
  149.   
  150.         mse_temp /= m;  
  151.   
  152.         Mat q(1,1,CV_64F);  
  153.         q = (mse - mse_temp)/(0.5*hlm.t()*(u*hlm-Jf.t()*r));  
  154.         double q_value = q.at<double>(0,0);  
  155.         if(q_value>0)  
  156.         {  
  157.             double s = 1.0/3.0;  
  158.             v = 2;  
  159.             mse = mse_temp;  
  160.             params = params_tmp;  
  161.             double temp = 1 - pow(2*q_value-1,3);  
  162.             if(s>temp)  
  163.             {  
  164.                 u = u * s;  
  165.             }else  
  166.             {  
  167.                 u = u * temp;  
  168.             }  
  169.         }else  
  170.         {  
  171.             u = u*v;   
  172.             v = 2*v;  
  173.             params = params_tmp;  
  174.         }     
  175.           
  176.       
  177.         // The difference in mse is very small, so quit  
  178.         if(fabs(mse - last_mse) < 1e-8) {  
  179.             break;  
  180.         }  
  181.   
  182.         //printf("%d: mse=%f\n", i, mse);  
  183.         printf("%d %lf\n", i, mse);  
  184.         last_mse = mse;  
  185.     }  
  186. }  


  我们看到迭代了100次,结果几何和高斯牛顿算出来是一样的。我们绘制LM和高斯牛顿的残差函数收敛过程,发现LM一直是总体下降的,没有太多反复。
高斯牛顿:

LM:

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多