Gauss-Newton算法学习

Gauss-Newton算法是解决非线性最优问题的常见算法之一,最近研读开源项目代码,又碰到了,索性深入看下。本次讲解内容如下:

  • 基本数学名词识记
  • 牛顿法推导、算法步骤、计算实例
  • 高斯牛顿法推导(如何从牛顿法派生)、算法步骤、编程实例
  • 高斯牛顿法优劣总结

一、基本概念定义

1.非线性方程定义及最优化方法简述

   指因变量与自变量之间的关系不是线性的关系,比如平方关系、对数关系、指数关系、三角函数关系等等。对于此类方程,求解n元实函数f在整个n维向量空间Rn上的最优值点往往很难得到精确解,经常需要求近似解问题。

   求解该最优化问题的方法大多是逐次一维搜索的迭代算法,基本思想是在一个近似点处选定一个有利于搜索方向,沿这个方向进行一维搜索,得到新的近似点。如此反复迭代,知道满足预定的精度要求为止。根据搜索方向的取法不同,这类迭代算法可分为两类:

解析法:需要用目标函数的到函数,

梯度法:又称最速下降法,是早期的解析法,收敛速度较慢

牛顿法:收敛速度快,但不稳定,计算也较困难。高斯牛顿法基于其改进,但目标作用不同

共轭梯度法:收敛较快,效果好

变尺度法:效率较高,常用DFP法(Davidon Fletcher Powell)

 

直接法:不涉及导数,只用到函数值。有交替方向法(又称坐标轮换法)、模式搜索法、旋转方向法、鲍威尔共轭方向法和单纯形加速法等。

2.非线性最小二乘问题

   非线性最小二乘问题来自于非线性回归,即通过观察自变量和因变量数据,求非线性目标函数的系数参数,使得函数模型与观测量尽量相似。

   高斯牛顿法解决非线性最小二乘问题的最基本方法,并且它只能处理二次函数。(使用时必须将目标函数转化为二次的)

   Unlike Newton'smethod, the Gauss–Newton algorithm can only be used to minimize a sum ofsquared function values

3.基本数学表达

a.梯度gradient,由多元函数的各个偏导数组成的向量

以二元函数为例,其梯度为:

b.黑森矩阵Hessian matrix,由多元函数的二阶偏导数组成的方阵,描述函数的局部曲率,以二元函数为例,

c.雅可比矩阵 Jacobian matrix,是多元函数一阶偏导数以一定方式排列成的矩阵,体现了一个可微方程与给出点的最优线性逼近。以二元函数为例,

如果扩展多维的话F: Rn-> Rm,则雅可比矩阵是一个m行n列的矩阵:

雅可比矩阵作用,如果P是Rn中的一点,F在P点可微分,那么在这一点的导数由JF(P)给出,在此情况下,由F(P)描述的线性算子即接近点P的F的最优线性逼近:

d.残差 residual,表示实际观测值与估计值(拟合值)之间的差

二、牛顿法

牛顿法的基本思想是采用多项式函数来逼近给定的函数值,然后求出极小点的估计值,重复操作,直到达到一定精度为止。

1.考虑如下一维无约束的极小化问题:

因此,一维牛顿法的计算步骤如下:

需要注意的是,牛顿法在求极值的时候,如果初始点选取不好,则可能不收敛于极小点

2.下面给出多维无约束极值的情形:

若非线性目标函数f(x)具有二阶连续偏导,在x(k)为其极小点的某一近似,在这一点取f(x)的二阶泰勒展开,即:

  如果f(x)是二次函数,则其黑森矩阵H为常数,式(1)是精确的(等于号),在这种情况下,从任意一点处罚,用式(2)只要一步可求出f(x)的极小点(假设黑森矩阵正定,所有特征值大于0)

  如果f(x)不是二次函数,式(1)仅是一个近似表达式,此时,按式(2)求得的极小点,只是f(x)的近似极小点。在这种情况下,常按照下面选取搜索方向:

牛顿法收敛的速度很快,当f(x)的二阶导数及其黑森矩阵的逆矩阵便于计算时,这一方法非常有效。【但通常黑森矩阵很不好求】

3.下面给出一个实际计算例子。

例:试用牛顿法求的极小值

解:

【f(x)是二次函数,H矩阵为常数,只要任意点出发,只要一步即可求出极小点】

三、牛顿高斯法

1.      gauss-newton是如何由上述派生的

有时候为了拟合数据,比如根据重投影误差求相机位姿(R,T为方程系数),常常将求解模型转化为非线性最小二乘问题。高斯牛顿法正是用于解决非线性最小二乘问题,达到数据拟合、参数估计和函数估计的目的。

假设我们研究如下形式的非线性最小二乘问题:

这两个位置间残差(重投影误差):

如果有大量观测点(多维),我们可以通过选择合理的T使得残差的平方和最小求得两个相机之间的位姿。机器视觉这块暂时不扩展,接着说怎么求非线性最小二乘问题。

若用牛顿法求式3,则牛顿迭代公式为:

看到这里大家都明白高斯牛顿和牛顿法的差异了吧,就在这迭代项上。经典高斯牛顿算法迭代步长λ为1.

那回过头来,高斯牛顿法里为啥要舍弃黑森矩阵的二阶偏导数呢?主要问题是因为牛顿法中Hessian矩阵中的二阶信息项通常难以计算或者花费的工作量很大,而利用整个H的割线近似也不可取,因为在计算梯度时已经得到J(x),这样H中的一阶信息项JTJ几乎是现成的。鉴于此,为了简化计算,获得有效算法,我们可用一阶导数信息逼近二阶信息项。注意这么干的前提是,残差r接近于零或者接近线性函数从而接近与零时,二阶信息项才可以忽略。通常称为“小残量问题”,否则高斯牛顿法不收敛。


3.  举例

接下来的代码里并没有保证算法收敛的机制,在例子2的自嗨中可以看到劣势。关于自变量维数,代码可以支持多元,但两个例子都是一维的,比如例子1中只有年份t,其实可以增加其他因素的,不必在意。

 

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

// A simple demo of Gauss-Newton algorithm on a user defined function

#include <cstdio>
#include <vector>
#include <opencv2/core/core.hpp>

using namespace std;
using namespace cv;

const double DERIV_STEP = 1e-5;
const int MAX_ITER = 100;

void GaussNewton(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer
				 const Mat &inputs, const Mat &outputs, Mat ¶ms);

double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer
			 const Mat &input, const Mat ¶ms, int n);

// The user defines their function here
double Func(const Mat &input, const Mat ¶ms);

int main()
{
	// For this demo we're going to try and fit to the function
	// F = A*exp(t*B)
	// There are 2 parameters: A B
	int num_params = 2;

    // Generate random data using these parameters
    int total_data = 8;

    Mat inputs(total_data, 1, CV_64F);
    Mat outputs(total_data, 1, CV_64F);

	//load observation data
    for(int i=0; i < total_data; i++) {
        inputs.at<double>(i,0) = i+1;  //load year
    }
	//load America population
	outputs.at<double>(0,0)= 8.3;
	outputs.at<double>(1,0)= 11.0;
	outputs.at<double>(2,0)= 14.7;
	outputs.at<double>(3,0)= 19.7;
	outputs.at<double>(4,0)= 26.7;
	outputs.at<double>(5,0)= 35.2;
	outputs.at<double>(6,0)= 44.4;
	outputs.at<double>(7,0)= 55.9;

    // Guess the parameters, it should be close to the true value, else it can fail for very sensitive functions!
    Mat params(num_params, 1, CV_64F);

	//init guess
    params.at<double>(0,0) = 6;
	params.at<double>(1,0) = 0.3;

    GaussNewton(Func, inputs, outputs, params);

    printf("Parameters from GaussNewton: %f %f\n", params.at<double>(0,0), params.at<double>(1,0));

    return 0;
}

double Func(const Mat &input, const Mat ¶ms)
{
	// Assumes input is a single row matrix
	// Assumes params is a column matrix

	double A = params.at<double>(0,0);
	double B = params.at<double>(1,0);

	double x = input.at<double>(0,0);

    return A*exp(x*B);
}

//calc the n-th params' partial derivation , the params are our  final target
double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), const Mat &input, const Mat ¶ms, int n)
{
	// Assumes input is a single row matrix

	// Returns the derivative of the nth parameter
	Mat params1 = params.clone();
	Mat params2 = params.clone();

	// Use central difference  to get derivative
	params1.at<double>(n,0) -= DERIV_STEP;
	params2.at<double>(n,0) += DERIV_STEP;

	double p1 = Func(input, params1);
	double p2 = Func(input, params2);

	double d = (p2 - p1) / (2*DERIV_STEP);

	return d;
}

void GaussNewton(double(*Func)(const Mat &input, const Mat ¶ms),
				 const Mat &inputs, const Mat &outputs, Mat ¶ms)
{
    int m = inputs.rows;
    int n = inputs.cols;
    int num_params = params.rows;

    Mat r(m, 1, CV_64F); // residual matrix
    Mat Jf(m, num_params, CV_64F); // Jacobian of Func()
    Mat input(1, n, CV_64F); // single row input

    double last_mse = 0;

    for(int i=0; i < MAX_ITER; i++) {
        double mse = 0;

        for(int j=0; j < m; j++) {
        	for(int k=0; k < n; k++) {//copy Independent variable vector, the year
        		input.at<double>(0,k) = inputs.at<double>(j,k);
        	}

            r.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params);//diff between estimate and observation population

            mse += r.at<double>(j,0)*r.at<double>(j,0);

            for(int k=0; k < num_params; k++) {
            	Jf.at<double>(j,k) = Deriv(Func, input, params, k);
            }
        }

        mse /= m;

        // The difference in mse is very small, so quit
        if(fabs(mse - last_mse) < 1e-8) {
        	break;
        }

        Mat delta = ((Jf.t()*Jf)).inv() * Jf.t()*r;
        params += delta;

        //printf("%d: mse=%f\n", i, mse);
        printf("%d %f\n", i, mse);

        last_mse = mse;
    }
}

运行结果:

A=7.0,B=0.26  (初始值,A=6,B=0.3),100次迭代到第4次就收敛了。

若初始值A=1,B=1,则要迭代14次收敛。

下图为根据上面得到的A、B系数,利用matlab拟合的人口模型曲线

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

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

// A simple demo of Gauss-Newton algorithm on a user defined function

#include <cstdio>
#include <vector>
#include <opencv2/core/core.hpp>

using namespace std;
using namespace cv;

const double DERIV_STEP = 1e-5;
const int MAX_ITER = 100;

void GaussNewton(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer
				 const Mat &inputs, const Mat &outputs, Mat ¶ms);

double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), // function pointer
			 const Mat &input, const Mat ¶ms, int n);

// The user defines their function here
double Func(const Mat &input, const Mat ¶ms);

int main()
{
	// For this demo we're going to try and fit to the function
	// F = A*sin(Bx) + C*cos(Dx)
	// There are 4 parameters: A, B, C, D
	int num_params = 4;

    // Generate random data using these parameters
    int total_data = 100;

    double A = 5;
    double B = 1;
    double C = 10;
    double D = 2;

    Mat inputs(total_data, 1, CV_64F);
    Mat outputs(total_data, 1, CV_64F);

    for(int i=0; i < total_data; i++) {
        double x = -10.0 + 20.0* rand() / (1.0 + RAND_MAX); // random between [-10 and 10]
        double y = A*sin(B*x) + C*cos(D*x);

        // Add some noise
       // y += -1.0 + 2.0*rand() / (1.0 + RAND_MAX);

        inputs.at<double>(i,0) = x;
        outputs.at<double>(i,0) = y;
    }

    // Guess the parameters, it should be close to the true value, else it can fail for very sensitive functions!
    Mat params(num_params, 1, CV_64F);

    params.at<double>(0,0) = 1;
    params.at<double>(1,0) = 1;
    params.at<double>(2,0) = 8; // changing to 1 will cause it not to find the solution, too far away
    params.at<double>(3,0) = 1;

    GaussNewton(Func, inputs, outputs, params);

    printf("True parameters: %f %f %f %f\n", A, B, C, D);
    printf("Parameters from GaussNewton: %f %f %f %f\n", params.at<double>(0,0), params.at<double>(1,0),
    													params.at<double>(2,0), params.at<double>(3,0));

    return 0;
}

double Func(const Mat &input, const Mat ¶ms)
{
	// Assumes input is a single row matrix
	// Assumes params is a column matrix

	double A = params.at<double>(0,0);
	double B = params.at<double>(1,0);
	double C = params.at<double>(2,0);
	double D = params.at<double>(3,0);

	double x = input.at<double>(0,0);

    return A*sin(B*x) + C*cos(D*x);
}

double Deriv(double(*Func)(const Mat &input, const Mat ¶ms), const Mat &input, const Mat ¶ms, int n)
{
	// Assumes input is a single row matrix

	// Returns the derivative of the nth parameter
	Mat params1 = params.clone();
	Mat params2 = params.clone();

	// Use central difference  to get derivative
	params1.at<double>(n,0) -= DERIV_STEP;
	params2.at<double>(n,0) += DERIV_STEP;

	double p1 = Func(input, params1);
	double p2 = Func(input, params2);

	double d = (p2 - p1) / (2*DERIV_STEP);

	return d;
}

void GaussNewton(double(*Func)(const Mat &input, const Mat ¶ms),
				 const Mat &inputs, const Mat &outputs, Mat ¶ms)
{
    int m = inputs.rows;
    int n = inputs.cols;
    int num_params = params.rows;

    Mat r(m, 1, CV_64F); // residual matrix
    Mat Jf(m, num_params, CV_64F); // Jacobian of Func()
    Mat input(1, n, CV_64F); // single row input

    double last_mse = 0;

    for(int i=0; i < MAX_ITER; i++) {
        double mse = 0;

        for(int j=0; j < m; j++) {
        	for(int k=0; k < n; k++) {
        		input.at<double>(0,k) = inputs.at<double>(j,k);
        	}

            r.at<double>(j,0) = outputs.at<double>(j,0) - Func(input, params);

            mse += r.at<double>(j,0)*r.at<double>(j,0);

            for(int k=0; k < num_params; k++) {
            	Jf.at<double>(j,k) = Deriv(Func, input, params, k);
            }
        }

        mse /= m;

        // The difference in mse is very small, so quit
        if(fabs(mse - last_mse) < 1e-8) {
        	break;
        }

        Mat delta = ((Jf.t()*Jf)).inv() * Jf.t()*r;
        params += delta;

        //printf("%d: mse=%f\n", i, mse);
        printf("%f\n",mse);

        last_mse = mse;
    }
}

运行结果,得到的参数并不够理想,50次后收敛了

下图中,每次迭代残差并没有持续减少,有反复

4.优缺点分析

优点:

对于零残量问题,即r=0,有局部二阶收敛速度

对于小残量问题,即r较小或接近线性,有快的局部收敛速度

对于线性最小二乘问题,一步达到极小点

 

缺点:

对于不是很严重的大残量问题,有较慢的局部收敛速度

对于残量很大的问题或r的非线性程度很大的问题,不收敛

不一定总体收敛

如果J不满秩,则方法无定义

 

对于它的缺点,我们通过增加线性搜索策略,保证目标函数每一步下降,对于几乎所有非线性最小二乘问题,它都具有局部收敛性及总体收敛,即所谓的阻尼高斯牛顿法。

其中,ak为一维搜索因子

时间: 2024-09-21 06:09:58

Gauss-Newton算法学习的相关文章

bundle adjustment算法学习

  今天学习了稀疏的光束平差法,基于上一篇博文Levenberg–Marquardt算法学习,这里对学习内容做一个理论梳理.本次内容包括: BA简介 BA迭代步长的数学推导 稀疏BA迭代步长的算法求解过程 1.BA简介    摄像机在静态环境中移动,得到不同时刻拍摄的多幅图像.假设这些图像是同一刚性物体的投影,则可由图像特征对应关系估计出摄像机的运动参数.在计算机视觉中 ,这一过程称为运动分析或由运动重建物体结构(structure frommotion).    Bundle Adjustme

Levenberg–Marquardt算法学习

  本次是对Levenberg–Marquardt的学习总结,是为之后看懂sparse bundle ajdustment打基础.这篇笔记包含如下内容: 回顾高斯牛顿算法,引入LM算法 惩罚因子的计算(迭代步子的计算) 完整的算法流程及代码样例 1.      回顾高斯牛顿,引入LM算法  根据之前的博文:Gauss-Newton算法学习   假设我们研究如下形式的非线性最小二乘问题:   r(x)为某个问题的残差residual,是关于x的非线性函数.我们知道高斯牛顿法的迭代公式:   Lev

计划如期完成,老师也给我回邮件了,继续坚持,努力。(顺便求推荐数据结构和算法学习的两本书)

问题描述 国庆二号号出去玩了一天,陪女朋友看"心花路放"(很赞的电影,推荐),购物,吃饭,然后剩下的六天假期都是在半学习半休闲的态度下度过的,因为女朋友和我一起在自习室,再加上国庆实在是有些懈怠心理.一期java语法基础包括集合框架,jdbc,sql,IO流,线程都简单的认识了,只局限于简单的使用(线程真的是认识的简单的不能再简单了).因为这些资料都是培训班的资料,所以做一个总结吧,培训班还是的确有独到之处的,目的性强,实用性强,速成性强,如果只有半年时间的话,我相信选择培训班是个很好

python算法学习之计数排序实例_python

python算法学习之计数排序实例 复制代码 代码如下: # -*- coding: utf-8 -*- def _counting_sort(A, B, k):    """计数排序,伪码如下:    COUNTING-SORT(A, B, k)    1  for i ← 0 to k // 初始化存储区的值    2    do C[i] ← 0    3  for j ← 1 to length[A] // 为各值计数    4    do C[A[j]] ← C[A

OTSU算法及其改进算法学习

   这篇文章还是来自斯坦福课后作业hw2_3,主要是结合一个例子介绍otsu算法[亦称为大律算法,小日本]及其改进算法.    本文将先介绍老外的题目.解题思路及maltab解答,然后分析otsu算法步骤,末了给出opencv实现.     老外的题目:Binarization of Scanned Book Pages 题目大意: 网上图书服务,比如百度文库需要将大量藏书数字化.首先,书的每一页将被扫描.然后,这些扫描图片将被二值化,并通过字符识别引擎OCR处理,即图片转字符.对于传统书籍[

Efficient Color Boundary Detection with Color-opponent Mechanisms算法学习笔记

这是一篇基于视觉颜色机制的边缘检测论文,原文分中文和英文版 中文版链接:中文版PDF 英文版链接:英文版PDF 项目主页:http://www.neuro.uestc.edu.cn/vccl/projcvpr2013.html 以下是我个人学习该算法后的理解,希望各位看官批评指正! 整个算法可分为以下几步: 1.输入一张彩色图像 2. 分别提取R-G-B三种颜色信息,并计算Y颜色信息,进行高斯滤波得到 3.设置连接权重ω ,通过式(1)得到R+wG和wR+G两种连接权值的的结果 $$Srg.Sg

知其所以然(以算法学习为例)

原文发表于2008年 其实下文的绝大部分内容对所有学习都是同理的.只不过最近在正儿巴经地学算法,而后者又不是好啃的骨头,所以平时思考总结得就自然要比学其它东西要多一些. 问题:目前几乎所有的算法书的讲解方式都是欧几里德式的.瀑布式的.自上而下的.每一个推导步骤都是精准制导直接面向目标的.由因到果,定义.引理.定理.证明一样不少,井井有条一丝不乱毫无赘肉.而实际上,这完全把人类大脑创造发明的步骤给反过来了.看起来是阳关大道,实际上车马不通. 而对读者来说,这就等于直接告诉你答案&做法了,然后让你去

SPFA算法学习笔记

一.理论准备         为了学习网络流,先水一道spfa.         SPFA算法是1994年西南交通大学段凡丁提出,只要最短路径存在,SPFA算法必定能求出最小值,SPFA对Bellman-Ford算法优化的关键之处在于意识到:只有那些在前一遍松弛中改变了距离估计值的点,才可能引起他们的邻接点的距离估计值的改变.为什么队列为空就不改变了呢?就是因为要到下一点必须经过它的前一个邻接点..SPFA可以处理负权边.很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之

C5.0算法学习

  C5.0是决策树模型中的算法,79年由J R Quinlan发展,并提出了ID3算法,主要针对离散型属性数据,其后又不断的改进,形成C4.5,它在ID3基础上增加了队连续属性的离散化.C5.0是C4.5应用于大数据集上的分类算法,主要在执行效率和内存使用方面进行了改进. C4.5算法是ID3算法的修订版,采用GainRatio来加以改进方法,选取有最大GainRatio的分割变量作为准则,避免ID3算法过度配适的问题. C5.0算法则是C4.5算法的修订版,适用于处理大数据集,采用Boost