非线性最小二乘-高斯牛顿法

前言

 在SLAM领域,状态估计问题多采用基于非线性优化的方法,其中高斯牛顿法的使用频率很高。

求解状态估计问题可以转化成非线性最小二乘问题,例如ICP的求解。下面先考虑一个简单的最小二乘问题:

其中F(x)是目标函数,f(x)是目标函数中的非线性函数。如果f(x)为简单的线性函数,那么F(x)的最小值求解就可以通过令目标函数F(x)的导数为零,然后求解x的最优值即可,如下公式所示:

但是f(x)并不一定是简单的线性函数,所以其导函数形式比较复杂,使得该方程不易求解。对于不方便直接求解的最小二乘问题,可以采用迭代的方式,从一个初始值出发,不断地更新当前的优化变量,使目标函数下降

因此求解F(x)最小值问题就变成了,每次迭代寻找\Delta x值来最小化F(x+\Delta x)的问题。

现在考虑第 k次迭代,假设我们在 X_{k}处,想要寻找增量 Δx,那么最直观的方式就是将目标函数在X_{k}附近进行泰勒展开:(泰勒展开其实就是用多项式函数来近似原函数的操作)

高斯牛顿法推理

由于H矩阵求解比较困难,因此我们引入了高斯牛顿法。

高斯牛顿法是通过对f(x)在X_{k}附近进行一阶泰勒展开,即高斯牛顿是分解目标函数F为f*f后对f的一次偏导的迭代,最够效果等同于目标函数的二次偏导。

将高斯牛顿中的目标函数展开后为:

对于上式,求关于 Δx的导数,并令其为零,得到: 

从上面公式可以看出,高斯牛顿法是每一次迭代后,都会在新的迭代点X_{k}附近重新进行一阶泰勒展开进行线性化近似,并根据最新迭代点求解新的雅可比矩阵的值。

求解高斯牛顿法的核心公式:

其中 f 是误差函数,J是误差关于待优化变量的雅可比矩阵。 

其中H为海森矩阵(error相对于变量的二阶导数的近似 ),海森矩阵的特征向量表示了优化问题中的不同方向,海森矩阵的特征值可以提供关于优化问题在不同方向上的曲率大小的信息,在高斯-牛顿算法中,特征值可以用作步长(步幅)的参考。

但是在实际的计算中,JTJ却只有半正定性,因此可能会出现奇异矩阵或者病态的情况。在SLAM的非线性优化中使用高斯牛顿法求解时,会通过近似的H矩阵的特征值大小判断是否发生退化情况。

特征值和特征向量的几何意义:

矩阵特征值,奇异值分解_奇异值分解的特征矩阵-CSDN博客

LIO-SAM中scan-to-map的非线性优化问题采用了高斯牛顿法:

    /**
     * scan-to-map优化
     * 对匹配特征点计算Jacobian矩阵,观测值为特征点到直线、平面的距离,构建高斯牛顿方程,迭代优化当前位姿,存transformTobeMapped
     * 公式推导:todo
    */
    bool LMOptimization(int iterCount)
    {
        // This optimization is from the original loam_velodyne by Ji Zhang, need to cope with coordinate transformation
        // lidar <- camera      ---     camera <- lidar
        // x = z                ---     x = y
        // y = x                ---     y = z
        // z = y                ---     z = x
        // roll = yaw           ---     roll = pitch
        // pitch = roll         ---     pitch = yaw
        // yaw = pitch          ---     yaw = roll

        // lidar -> camera
        float srx = sin(transformTobeMapped[1]);
        float crx = cos(transformTobeMapped[1]);
        float sry = sin(transformTobeMapped[2]);
        float cry = cos(transformTobeMapped[2]);
        float srz = sin(transformTobeMapped[0]);
        float crz = cos(transformTobeMapped[0]);

        // 当前帧匹配特征点数太少
        int laserCloudSelNum = laserCloudOri->size();
        if (laserCloudSelNum < 50) {
            return false;
        }

        cv::Mat matA(laserCloudSelNum, 6, CV_32F, cv::Scalar::all(0));
        cv::Mat matAt(6, laserCloudSelNum, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtA(6, 6, CV_32F, cv::Scalar::all(0));
        cv::Mat matB(laserCloudSelNum, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtB(6, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matX(6, 1, CV_32F, cv::Scalar::all(0));

        PointType pointOri, coeff;

        // 遍历匹配特征点,构建Jacobian矩阵
        for (int i = 0; i < laserCloudSelNum; i++) {
            // lidar -> camera todo
            // 首先将当前点以及点到线(面)的单位向量转到相机系
            pointOri.x = laserCloudOri->points[i].y;
            pointOri.y = laserCloudOri->points[i].z;
            pointOri.z = laserCloudOri->points[i].x;
            // lidar -> camera
            coeff.x = coeffSel->points[i].y;
            coeff.y = coeffSel->points[i].z;
            coeff.z = coeffSel->points[i].x;
            coeff.intensity = coeffSel->points[i].intensity;
            // in camera
            // 对Roll角的求导
            float arx = (crx*sry*srz*pointOri.x + crx*crz*sry*pointOri.y - srx*sry*pointOri.z) * coeff.x
                      + (-srx*srz*pointOri.x - crz*srx*pointOri.y - crx*pointOri.z) * coeff.y
                      + (crx*cry*srz*pointOri.x + crx*cry*crz*pointOri.y - cry*srx*pointOri.z) * coeff.z;

            float ary = ((cry*srx*srz - crz*sry)*pointOri.x 
                      + (sry*srz + cry*crz*srx)*pointOri.y + crx*cry*pointOri.z) * coeff.x
                      + ((-cry*crz - srx*sry*srz)*pointOri.x 
                      + (cry*srz - crz*srx*sry)*pointOri.y - crx*sry*pointOri.z) * coeff.z;

            float arz = ((crz*srx*sry - cry*srz)*pointOri.x + (-cry*crz-srx*sry*srz)*pointOri.y)*coeff.x
                      + (crx*crz*pointOri.x - crx*srz*pointOri.y) * coeff.y
                      + ((sry*srz + cry*crz*srx)*pointOri.x + (crz*sry-cry*srx*srz)*pointOri.y)*coeff.z;
            // lidar -> camera
            // 雅可比矩阵,【6,1】的矩阵,前3个是误差关于旋转的导数,后3个是误差关于平移的导数。
            matA.at<float>(i, 0) = arz;
            matA.at<float>(i, 1) = arx;
            matA.at<float>(i, 2) = ary;
            matA.at<float>(i, 3) = coeff.z;
            matA.at<float>(i, 4) = coeff.x;
            matA.at<float>(i, 5) = coeff.y;
            // 点到直线距离、平面距离,作为观测值
            matB.at<float>(i, 0) = -coeff.intensity;
        }

        // 构造JTJ(H)以及-JTe(b)矩阵,matAt是转置
        cv::transpose(matA, matAt);
        matAtA = matAt * matA;
        matAtB = matAt * matB;
        // J^T·J·delta_x = -J^T·f 高斯牛顿,matX是增量待求解量。
        cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);

        // 首次迭代,检查近似Hessian矩阵(J^T·J)是否退化,或者称为奇异,行列式值=0 todo
        if (iterCount == 0) {

            cv::Mat matE(1, 6, CV_32F, cv::Scalar::all(0));
            cv::Mat matV(6, 6, CV_32F, cv::Scalar::all(0));
            cv::Mat matV2(6, 6, CV_32F, cv::Scalar::all(0));

            // 对H进行特征值分解
            cv::eigen(matAtA, matE, matV); 
            matV.copyTo(matV2);

            isDegenerate = false;
            float eignThre[6] = {100, 100, 100, 100, 100, 100};
            for (int i = 5; i >= 0; i--) {
                // 特征值从小到大遍历,如果小于阈值就认为是退化。(特征值比较小,可能是一个零空间,无法得到最优估计?属于一个退化环境)
                if (matE.at<float>(0, i) < eignThre[i]) {
                    for (int j = 0; j < 6; j++) {
                        matV2.at<float>(i, j) = 0;
                    }
                    isDegenerate = true;
                } else {
                    break;
                }
            }
            matP = matV.inv() * matV2;
        }

        // 如果发生退化,就对增量进行修改,退化方向不更新
        if (isDegenerate)
        {
            cv::Mat matX2(6, 1, CV_32F, cv::Scalar::all(0));
            matX.copyTo(matX2);
            matX = matP * matX2;
        }

        // 更新当前位姿 x = x + delta_x
        transformTobeMapped[0] += matX.at<float>(0, 0);
        transformTobeMapped[1] += matX.at<float>(1, 0);
        transformTobeMapped[2] += matX.at<float>(2, 0);
        transformTobeMapped[3] += matX.at<float>(3, 0);
        transformTobeMapped[4] += matX.at<float>(4, 0);
        transformTobeMapped[5] += matX.at<float>(5, 0);

        float deltaR = sqrt(
                            pow(pcl::rad2deg(matX.at<float>(0, 0)), 2) +
                            pow(pcl::rad2deg(matX.at<float>(1, 0)), 2) +
                            pow(pcl::rad2deg(matX.at<float>(2, 0)), 2));
        float deltaT = sqrt(
                            pow(matX.at<float>(3, 0) * 100, 2) +
                            pow(matX.at<float>(4, 0) * 100, 2) +
                            pow(matX.at<float>(5, 0) * 100, 2));

        // delta_x很小,认为收敛
        if (deltaR < 0.05 && deltaT < 0.05) {
            return true; 
        }
        // 否则继续优化
        return false; 
    }

其中一步对H矩阵做了特征值分解,若特征值太小,则说明优化方向的曲率过小,在SLAM中认为这种情况下为退化环境,非线性优化无法得到一个最优的结果。

例如长廊下的退化环境,进行scan-to-map的非线性优化时,此时求解H的特征值就会出现很小的情况(由于长廊中每个位置的差异性很小,雷达帧点云和长廊的局部地图的点云配准的残差都比较小,其非线性优化的曲率就会很小),就容易无法得到一个很好的优化结果。

Ceres是一款开源的C++库,用于实现非线性最小二乘问题的求解。它支持许多常见的最优化算法,包括牛顿法高斯牛顿法和阻尼最小二乘法。 牛顿法是一种迭代法,用于寻找非线性函数的最小值。它使用函数的一阶和二阶导数来逼近函数,并使用这些信息来更新当前解的位置。Ceres实现了一种变形的牛顿法,称为LM(Levenberg-Marquardt)算法,它使用一个参数来平衡牛顿法和梯度下降法之间的权衡。 高斯牛顿法是一种迭代法,用于寻找非线性函数的最小值。它使用函数的一阶导数和雅克比矩阵来逼近函数,并使用这些信息来更新当前解的位置。Ceres实现了一种变形的高斯牛顿法,称为Schur-Jacobi算法,它使用一个参数来平衡高斯牛顿法和梯度下降法之间的权衡。 阻尼最小二乘法是一种迭代法,用于寻找非线性函数的最小值。它使用函数的一阶导数和雅克比矩阵来逼近函数,并使用这些信息来更新当前解的位置。不同于牛顿法高斯牛顿法,阻尼最小二乘法在更新步长时引入了一个阻尼因子,以避免步长过大,从而导致求解过程不稳定。 下面是一个使用Ceres实现最小二乘问题的示例代码: ```c++ #include <iostream> #include "ceres/ceres.h" using namespace std; // 定义残差函数 struct CostFunctor { template <typename T> bool operator()(const T* const x, T* residual) const { residual[0] = T(10.0) - x[0]; return true; } }; int main(int argc, char** argv) { // 初始化问题 ceres::Problem problem; // 添加变量 double x = 0.5; problem.AddParameterBlock(&x, 1); // 添加残差项 ceres::CostFunction* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor); problem.AddResidualBlock(cost_function, nullptr, &x); // 设置求解选项 ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = true; // 求解问题 ceres::Solver::Summary summary; ceres::Solve(options, &problem, &summary); // 输出结果 cout << summary.BriefReport() << endl; cout << "x = " << x << endl; return 0; } ``` 在这个例子中,我们定义了一个简单的残差函数,它的目标是让变量x的值接近10。然后,我们使用Ceres来求解这个最小二乘问题。我们首先将变量x添加到问题中,并添加一个残差项。然后,我们设置求解选项并调用ceres::Solve函数来求解问题。最后,我们输出求解结果。 这只是一个简单的例子,Ceres还支持更复杂的问题和算法。如果你想深入了解Ceres的使用,请参考官方文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jiqiang_z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值