最小二乘法是工程学领域应用非常广泛和重要的一种方法,我们在大一的高数里面可能就已经学习了,然而我们还有很多工程师还没有对它进行深入的理解,这篇博客就是带领大家一起深入的学习和理解最小二乘法的美妙和怎么使用一个C++程序来实现它。

背景

首先我们学习最小二乘法当然得首先了解它的前世今生。最小二乘法是由勒让德在19世纪发现的,这个人很早就独立发现了最小二乘法,然而一直没有发表出来。这个方法的成名是我们伟大的数学家时年24岁的高斯在使用一堆数据计算预测出了谷神星的轨道,后被奥地利天文学家海因里希·奥尔伯斯的观测所证实,最小二乘法从此一站成名,高斯在后来提供了最小二乘法的优化效果强于其他方法的证明。如今,最小二乘在工程领域有着广泛的应用,比如机器学习的线性回归,无论是线性系统的状态估计还是非线性系统的估计,我们最终统一可以把问题转化为一个优化问题,而最常用的的优化方法就是最小二乘。

理论知识

最小二乘法的形式非常简单: 目标函数=

spark最小二乘法公式 最小二乘法程序实现_人工智能

(观测值−理论值)²

观察它的形式我们看到是有一个平方的,其实我们需要的是一个绝对值,但是为了数学上的计算方便,所以它是采取平方的形式。一般情况下,优化问题就是我们需要从一堆数据中找到和这些数据最匹配的一个函数作为这些数据描述的一个问题的一个解。

实际应用

下面我们用一个简单的例子给大家来讲解和使用最小二乘法,当我们理解清楚以后就能根据自己的实际应用和变化来灵活运用。比如说,现在有一辆小车在我们的一个二维平面地图上沿着直线行走,在一些相互独立的时刻我们使用测量装置测出来它在这些时刻在地图上的位置(xi, yi),因为我们知道,现实世界中小车走的肯定会有一定的误差,我们的测量手段也不可能百分之百的准确,所以我们实际得到的坐标值不会是像初中学习的数学那样严格符合一条直线,我们需要使用这些数据总结出它走的直线的坐标方程,并且预测它下一步走的位置的坐标。

我们知道直线可以描述为y = kx +b;所以我们现在要做的就是通过这些数据拟合出最像这一条直线的轨迹,约莫可以使用这张图来看:

spark最小二乘法公式 最小二乘法程序实现_算法_02

绿色的点是我们通过测量得到的小车的轨迹点,这些点分布在某条直线的两侧,那么我们现在就是用使用这些点求出误差最小的那条直线,也就是我们图中的红色直线。

好了,下面我们就来一步步求一下那条直线,根据我们前面说的最小二乘公式,我们可以构建出一个目标函数: 

spark最小二乘法公式 最小二乘法程序实现_机器学习_03

,现在我们就是需要通过各个

spark最小二乘法公式 最小二乘法程序实现_spark最小二乘法公式_04

点来获得k和b的值。

我们分别采用两种形式来给大家进行一些推导:

第一种是代数的形式,我们知道求取二次函数的导数的极值点就可以得到它的最低点的值,因为我们现在是求取k、b,我们就把k、b当做未知数,分别对k、b求偏导得到:

spark最小二乘法公式 最小二乘法程序实现_最小二乘法_05

                                                                                        (1)

spark最小二乘法公式 最小二乘法程序实现_人工智能_06

                                                                                               (2)令

spark最小二乘法公式 最小二乘法程序实现_人工智能_07

                            (3)得到: 

spark最小二乘法公式 最小二乘法程序实现_spark最小二乘法公式_08

这样的形式我们很好理解,也很方便的可以使用计算机程序求出最后的结果,只需要依次计算出A、B、C、D的值,带入公式我们就可以得到k和b了。

我们着重讲的是第二种形式,采用矩阵的形式,作为一个成熟的工程师也应该学会使用矩阵来求解各种问题。首先我们根据目标函数的公式构建出一个Ax=B的矩阵,在这里呢我们构建出来的矩阵形式如下:

spark最小二乘法公式 最小二乘法程序实现_算法_09

我们采用最小二乘的矩阵求解公式:

spark最小二乘法公式 最小二乘法程序实现_spark最小二乘法公式_10

就可以借助常用的矩阵库得出结果了。

当然以上公式的推导这里就不详细给出了,如果大家有诉求可以给我留言,我们在使用的过程中也需要考虑到矩阵退化的情况,这些都是你深入进去的后话了,好了,话不多说,我们上代码。

#include <iostream>
#include <vector>
#include <stdlib.h>
#include "Eigen/Core"
#include "Eigen/Geometry"

struct LinearEquationParameters_t
{
    double k;
    double b;
};

struct Point2D_t
{
    double x;
    double y;
};

/**
 * @brief: 求解Ax = B x = (At * A)逆*At *B
 * @param {vector<Point2D_t>} points
 * @retval:  
 */
LinearEquationParameters_t leastSquare(std::vector<Point2D_t> points)
{
    const int32_t dimension =static_cast<int>(points.size());
    LinearEquationParameters_t ret{0.0, 0.0};

    if (dimension< 2)
    {
        LOGE("bad points");
        return(ret);
    }

    Eigen::MatrixXd A(dimension, 2);
    Eigen::VectorXd B(dimension);
    Eigen::VectorXd X(2);

    for (int i = 0; i < dimension; ++i)
    {
        A.row(i) << points.at(i).x, 1.0;
        B.row(i) << points.at(i).y;
    }


    X = (A.transpose() * A).inverse() * A.transpose() * B;//效率更高
    // X = A.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(B);


    // std::cout << X <<std::endl;
    ret.k = X(0);
    ret.b = X(1);

    return(ret);
}

int main() 
{
    //模拟一些数据
    Point2D_t a{1.02,5.03}, b{1.97,6.99}, c{3.01,9.0}, d{10.05,23.001}, e{101,202};
    std::vector<Point2D_t> points;
    points.push_back(a);
    points.push_back(b);
    points.push_back(c);
    points.push_back(d);
    points.push_back(e);
    LinearEquationParameters_t val = leastSquare(points);
    printf("k = %lf, b = %lf\n", val.k, val.b);
}

最后的运行结果是:

k = 1.969391, b = 3.100752

以上是我个人抽空做的第一个博客专栏,有误的地方请大家多多批评指正,本人是从事于机器人导航研发工作的一名程序员,希望能在这个平台上多和各路大神进行交流,大家如果有什么想学的想知道也可以给我评论或者私信留言,谢谢。