NOTE / 2020/4/28
Givens Rotation解Ax=b
工程中的很多问题都会归结为求解方程 。 是一个 矩阵, 是一个 的列向量。 求解这个问题有很多中方法,如QR分解,LLT分解等等(可以看Eigen的一个教程: http:// eigen.tuxfamily.org/dox /group__TutorialLinearAlgebra.html )。本文主要介绍使用Givens Rotation解这个问题。 很多SLAM算法中都有使用Givens Rotation,比如 MSCKF,iSAM 等。
1. QR分解求解Ax=b
在引入Givens Rotation之前,先介绍QR分解的方法。
矩阵 可以分解为一个正交方阵 和一个上三角矩阵
我们要求解的问题就变为
两边同时乘
取上半部分:
是一个上三角矩阵,所以求解方程(3)只需要从最后一行开始,先计算 的最后一个元素,然后逐行回带,就可以可把 求解出来。
使用上面的过程求解,首先需要把 进行QR分解,而且要存储 。这个过程费时,费内存。 而Givens Rotation告诉我们,不需要把 求出来,也可以得到(3)式。
2. Givens Rotation
为了获取(3)式子,我们的目的是要把矩阵 的下半部分的元素(除了上三角的元素)变成0。
2.1 先看简单的二维方阵的情况
我们要把 那里变成0,可以对 左乘一个旋转矩阵/Givens矩阵 :
之所以用旋转矩阵,因为它是正交的,它的连乘也是正交的,也就是它的乘积可以组成
旋转的角度 的选取要能够把左下角的元素 变为0,也就是要满足:
很显然,我们是知道 ,所以 也可以解出来,那自然的givens矩阵 也就有了。所以,就可以把 变成上三角矩阵了。
实际上我们并不需要把 解出来,只需要把 和 解出来就可以了。有一种数值稳定的解法是:
对于要解的问题,两边同时乘上Givens矩阵就得到了和(3)式一样的情况。
从下往上计算 的元素,先计算 ,再计算 ,就解完了。
2.2 上面这个二维的方阵,比较简单,只需要做一次Givens旋转就搞定了。下面再看,对于超定矩阵的情况。
我们要做的是把 的地方变为0。
变为0的顺序,为从最左侧一列开始往右,每一列从最下面一个元素开始。
第一步:把 变为0,我们需要左乘Givens矩阵
可以看到这个Givens矩阵是由 和 确定的。变化的元素都有 来表示。
第二步:把 变为0,左乘Givens矩阵
这次的Givens矩阵由 和 确定。变化的元素用 来标识。
第三步:把 变为0,左乘Givens矩阵
至此,我们就把矩阵 变成了上三角矩阵。
实际上三个Givens矩阵的连乘就是 矩阵的转置:
2.3 总结
总结一下Givens Rotation的过程:
- 从左到右逐列执行。
- 每一列,从下往上执行。
- 每一次的Givens矩阵由要消为0 的元素和对应对角线的元素确定。
如果我们把用Givens Rotation计算的上三角矩阵与Matlab,Eigen等其他算法计算的结果对比,有时候会发现不一样。不要以为自己算错了,这是因为QR分解并不是唯一的。 例如,定义一个对角矩阵 ,每个 都是+1或者-1,那么 , 和 就组成了新的QR分解。
3. 实现代码
// Note: i is col, j is row.
void GivensCosSin(const double aii, const double aji, double* c, double* s) {
if (std::abs(aji) < 1e-12) {
*c = 1.;
*s = 0.;
} else if (std::abs(aji) > std::abs(aii)) {
const double aii_over_aji = aii / aji;
const double one_over_sqrt = 1. / std::sqrt(1. + aii_over_aji * aii_over_aji);
*c = - aii_over_aji * one_over_sqrt;
*s = one_over_sqrt;
} else {
const double aji_over_aii = aji / aii;
const double one_over_sqrt = 1. / std::sqrt(1 + aji_over_aii * aji_over_aii);
*c = one_over_sqrt;
*s = - aji_over_aii * one_over_sqrt;
}
}
// A * x = b to R * x = d.
void GivensRotationQRInPlace(Eigen::MatrixXd* A, Eigen::VectorXd* b) {
Eigen::MatrixXd& AA = *A;
Eigen::VectorXd& bb = *b;
const int rows = AA.rows();
const int cols = AA.cols();
assert(rows > cols);
assert(rows == bb.rows());
// Traverse all columns
for (int col = 0; col < cols; ++col) {
// Bottom to up.
for (int row = rows - 1; row > col; --row) {
// Compute cos_theta, sin_theta
double c, s;
GivensCosSin(AA(col, col), AA(row, col), &c, &s);
// Porcess A.
// For all elements at the ith, and jth rows.
for (int n = col; n < cols; ++n) {
const double a = AA(col, n);
const double b = AA(row, n);
AA(col, n) = c * a - s * b;
AA(row, n) = s * a + c * b;
}
// Process b.
{
const double a = bb(col);
const double b = bb(row);
bb(col) = c * a - s * b;
bb(row) = s * a + c * b;
}
}
}
}
bool BackSubstitution(const Eigen::MatrixXd& R, const Eigen::VectorXd& d, Eigen::VectorXd* x) {
x->resize(R.rows());
// From bottom to up.
for (int row = R.rows() - 1; row >= 0; --row) {
if (std::abs(R(row, row)) < 1e-12) {
return false;
}
double tmp = 0.;
for (size_t col = row + 1; col < R.cols(); ++col) {
tmp += R(row, col) * (*x)(col);
}
(*x)(row) = (d(row) - tmp) / R(row, row);
}
return true;
}
bool SolveLinearSystemInPlace(Eigen::MatrixXd* A, Eigen::VectorXd* b, Eigen::VectorXd* x) {
// Ax=b to Rx=d.
GivensRotationQRInPlace(A, b);
// Solve Rx = d.
return BackSubstitution(A->topRows(A->cols()), b->topRows(A->cols()), x);
}
4. 参考资料
- Joan Sola. Course on SLAM.
- Timothy Sauer. Numerical Analysis.
- Michael Kaess. iSAM: Incremental Smoothing and Mapping
- G. Golub and C. V. Loan, Matrix Computations, 3rd