非线性优化问题以及在视觉SLAM中的应用
1.0 最小二乘基础概念
- 定义
这里要着重注意一下,这里的
和 都是每一个残差项的雅可比堆叠(计算)而来,实际上对于初学者来说并不直观,后面我们会以一个曲线拟合和 问题来详细分析一下如何通过连加来推算到 和
- 损失函数泰勒展开的性质
- 如果在点
处有导数为 ,则称这个点为稳定点。 - 在点
处对应的 Hessian 为 :- 如果是正定矩阵,即它的特征值都大于
,则在 处有 为局部最小值。 - 如果是复定矩阵,即它的特征值都小于
,则在 处有 为局部最大值。 - 如果是不定矩阵,即它的特征值大于
也有小于 的,则 处为鞍点。
- 如果是正定矩阵,即它的特征值都大于
- 求解方法主要有以下两种:
- 直接求解:线性最小二乘(这里不再赘述,为线性代数的内容,超定方程的通解为
) - 迭代下降法:适用于线性和非线性最小二乘
- 直接求解:线性最小二乘(这里不再赘述,为线性代数的内容,超定方程的通解为
2.0 迭代下降求解方式
-
迭代法初衷:
找到一个下降方向使得损失函数随着
的迭代逐渐减少直到 。分两个步骤;第一,找到下降方向单位向量
,第二,确定下降的步长 。假设
足够的小,又因为 为单位向量,因此可以将 看作是一个微小的变化量 ,我们可以对损失函数进行一阶泰勒展开:只需要寻找下降方向,满足:
通过 line search 的方法找到下降的步长:
2.1 最速下降法: 适用于迭代的开始阶段
适用于迭代的开始阶段
这里为什么能写成向量的内积运算,笔者在刚开始看起来还认为是两个矩阵相乘法,却直接写成了内积运算,仔细思索发现
其实上是一个和 相同维度的单位向量,其纬度为 ,而雅可比矩阵
2.2 牛顿法: 适用于最优值附近
在局部最优点
得到:
这里我们其实既可以看作是多个残差的分量相加后组成的
,也可以看作是每个残差单独的 。
2.3 阻尼法:防止 的模过大
将损失函数的二阶泰勒展开记作:
求以下函数的最小化:
其中,
2.4 Gauss-Newton 和 LM
残差函数
带入损失函数:
这里我们假设暂时只关注其中的一项(其实也可以是所有损失项的叠加,最终形式是一样的)。在
上面这个形式就是我们在论文或者各种SLAM问题中经常见到的形式了,
曲线拟合理解
现在我们通过非线性最小二乘来进行线性拟合实验,将理论应用于实际中去。假设曲线方程为:
其中设
现在加入噪声项生成带有高斯分布的噪声数据,当然不是高斯分布的数据也是可以的,但是在自己实验的时候尽量不要出现外点数据,因为我们并没有处理外点数据的策略。则生成数据的方程为:
其中
通过如下程序生成观测数据:
double ar = 1.0, br = 2.0, cr = 1.0; // 真实参数值
int N = 100; // 数据点
double w_sigma = 1.0; // 噪声Sigma值
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));
}
接下来我们关心雅可比如何计算,误差项
我们知道这两项相减是绝对不可能相等的,因为在生成数据的时候加入了高斯噪声。我们这里有
该式中右边就是残差项的具体形式,我们对其进行平方,防止负的残差和正的残差抵消的情况,前面我们已经说过可以将残差项通过一阶泰勒展开进行近似,然后进行平方得到每一个残差项的具体形式:
此时由于某时刻的观测已知,因此误差项是一个关于
这里我们简单的记:
即我们常见的形式:
读者要注意到这里的
其实就是上面的
这里我们假设残差项记为
整个
这里的
再将该式子变形,将关于
其实也就是:
写成连加的形式:
这里我们就通过每一项的一个具体形式来推倒出最后的 H 和 b 是怎么来的了。也就是我们经常在程序中见到的 +=
操作的原理:
H += J * J.transpose();
b += -J * error;
我们再次回到曲线拟合的题目中去,待优化的变量就三个
得到了雅可比,那么剩下的就是迭代求解即可,完整代码如下,来自14讲配套代码:
#include
#include
#include
#include
#include
using namespace std;
using namespace Eigen;
int main(int argc, char **argv) {
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迭代
int iterations = 100; // 迭代次数
double cost = 0, lastCost = 0; // 本次迭代的cost和上一次迭代的cost
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
for (int iter = 0; iter < iterations; iter++) {
Matrix3d H = Matrix3d::Zero(); // Hessian = J^T W^{-1} J in Gauss-Newton
Vector3d b = Vector3d::Zero(); // bias
cost = 0;
for (int i = 0; i < N; i++) {
double xi = x_data[i], yi = y_data[i]; // 第i个数据点
double error = yi - exp(ae * xi * xi + be * xi + ce);
Vector3d J; // 雅可比矩阵
J[0] = -xi * xi * exp(ae * xi * xi + be * xi + ce); // de/da
J[1] = -xi * exp(ae * xi * xi + be * xi + ce); // de/db
J[2] = -exp(ae * xi * xi + be * xi + ce); // de/dc
H += J * J.transpose();
b += -J * error;
cost += error * error;
}
// 求解线性方程 Hx=b
Vector3d dx = H.ldlt().solve(b);
if (isnan(dx[0])) {
cout << "result is nan!" << endl;
break;
}
if (iter > 0 && cost >= lastCost) {
cout << "cost: " << cost << ">= last cost: " << lastCost << ", break." << endl;
break;
}
ae += dx[0];
be += dx[1];
ce += dx[2];
lastCost = cost;
cout << "total cost: " << cost << ", \t\tupdate: " << dx.transpose() <<
"\t\testimated params: " << ae << "," << be << "," << ce << endl;
}
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
chrono::duration<double> time_used = chrono::duration_castdouble>>(t2 - t1);
cout << "solve time cost = " << time_used.count() << " seconds. " << endl;
cout << "estimated abc = " << ae << ", " << be << ", " << ce << endl;
return 0;
}
- 运行结果如下:
total cost: 3.19575e+06, update: 0.0455771 0.078164 -0.985329 estimated params: 2.04558,-0.921836,4.01467
total cost: 376785, update: 0.065762 0.224972 -0.962521 estimated params: 2.11134,-0.696864,3.05215
total cost: 35673.6, update: -0.0670241 0.617616 -0.907497 estimated params: 2.04432,-0.0792484,2.14465
total cost: 2195.01, update: -0.522767 1.19192 -0.756452 estimated params: 1.52155,1.11267,1.3882
total cost: 174.853, update: -0.537502 0.909933 -0.386395 estimated params: 0.984045,2.0226,1.00181
total cost: 102.78, update: -0.0919666 0.147331 -0.0573675 estimated params: 0.892079,2.16994,0.944438
total cost: 101.937, update: -0.00117081 0.00196749 -0.00081055 estimated params: 0.890908,2.1719,0.943628
total cost: 101.937, update: 3.4312e-06 -4.28555e-06 1.08348e-06 estimated params: 0.890912,2.1719,0.943629
total cost: 101.937, update: -2.01204e-08 2.68928e-08 -7.86602e-09 estimated params: 0.890912,2.1719,0.943629
cost: 101.937>= last cost: 101.937, break.
solve time cost = 0.00440302 seconds.
estimated abc = 0.890912, 2.1719, 0.943629
- 和真实结果对比,这里的准确度取决于我们噪声方差的大小
Estimate | |||
Real |
下一节我们来讨论一下视觉SLAM中的非线性优化问题的具体形式,以及其