Merge pull request #533 from borglab/jing/powermethod
Added accelerated power method to compute eigenpair fastrelease/4.3a0
commit
d267368b28
|
@ -0,0 +1,176 @@
|
|||
/* ----------------------------------------------------------------------------
|
||||
|
||||
* GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||
* Atlanta, Georgia 30332-0415
|
||||
* All Rights Reserved
|
||||
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||
|
||||
* See LICENSE for the license information
|
||||
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @file AcceleratedPowerMethod.h
|
||||
* @date Sept 2020
|
||||
* @author Jing Wu
|
||||
* @brief accelerated power method for fast eigenvalue and eigenvector
|
||||
* computation
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtsam/linear/PowerMethod.h>
|
||||
|
||||
namespace gtsam {
|
||||
|
||||
using Sparse = Eigen::SparseMatrix<double>;
|
||||
|
||||
/**
|
||||
* \brief Compute maximum Eigenpair with accelerated power method
|
||||
*
|
||||
* References :
|
||||
* 1) G. Golub and C. V. Loan, Matrix Computations, 3rd ed. Baltimore, Johns
|
||||
* Hopkins University Press, 1996, pp.405-411
|
||||
* 2) Rosen, D. and Carlone, L., 2017, September. Computational
|
||||
* enhancements for certifiably correct SLAM. In Proceedings of the
|
||||
* International Conference on Intelligent Robots and Systems.
|
||||
* 3) Yulun Tian and Kasra Khosoussi and David M. Rosen and Jonathan P. How,
|
||||
* 2020, Aug, Distributed Certifiably Correct Pose-Graph Optimization, Arxiv
|
||||
* 4) C. de Sa, B. He, I. Mitliagkas, C. Ré, and P. Xu, “Accelerated
|
||||
* stochastic power iteration,” in Proc. Mach. Learn. Res., no. 84, 2018, pp.
|
||||
* 58–67
|
||||
*
|
||||
* It performs the following iteration: \f$ x_{k+1} = A * x_k - \beta *
|
||||
* x_{k-1} \f$ where A is the aim matrix we want to get eigenpair of, x is the
|
||||
* Ritz vector
|
||||
*
|
||||
* Template argument Operator just needs multiplication operator
|
||||
*
|
||||
*/
|
||||
template <class Operator>
|
||||
class AcceleratedPowerMethod : public PowerMethod<Operator> {
|
||||
|
||||
double beta_ = 0; // a Polyak momentum term
|
||||
|
||||
Vector previousVector_; // store previous vector
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor from aim matrix A (given as Matrix or Sparse), optional intial
|
||||
* vector as ritzVector
|
||||
*/
|
||||
explicit AcceleratedPowerMethod(
|
||||
const Operator &A, const boost::optional<Vector> initial = boost::none,
|
||||
double initialBeta = 0.0)
|
||||
: PowerMethod<Operator>(A, initial) {
|
||||
// initialize Ritz eigen vector and previous vector
|
||||
this->ritzVector_ = initial ? initial.get() : Vector::Random(this->dim_);
|
||||
this->ritzVector_.normalize();
|
||||
previousVector_ = Vector::Zero(this->dim_);
|
||||
|
||||
// initialize beta_
|
||||
beta_ = initialBeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run accelerated power iteration to get ritzVector with beta and previous
|
||||
* two ritzVector x0 and x00, and return y = (A * x0 - \beta * x00) / || A * x0
|
||||
* - \beta * x00 ||
|
||||
*/
|
||||
Vector acceleratedPowerIteration (const Vector &x1, const Vector &x0,
|
||||
const double beta) const {
|
||||
Vector y = this->A_ * x1 - beta * x0;
|
||||
y.normalize();
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run accelerated power iteration to get ritzVector with beta and previous
|
||||
* two ritzVector x0 and x00, and return y = (A * x0 - \beta * x00) / || A * x0
|
||||
* - \beta * x00 ||
|
||||
*/
|
||||
Vector acceleratedPowerIteration () const {
|
||||
Vector y = acceleratedPowerIteration(this->ritzVector_, previousVector_, beta_);
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tuning the momentum beta using the Best Heavy Ball algorithm in Ref(3), T
|
||||
* is the iteration time to find beta with largest Rayleigh quotient
|
||||
*/
|
||||
double estimateBeta(const size_t T = 10) const {
|
||||
// set initial estimation of maxBeta
|
||||
Vector initVector = this->ritzVector_;
|
||||
const double up = initVector.dot( this->A_ * initVector );
|
||||
const double down = initVector.dot(initVector);
|
||||
const double mu = up / down;
|
||||
double maxBeta = mu * mu / 4;
|
||||
size_t maxIndex;
|
||||
std::vector<double> betas;
|
||||
|
||||
Matrix R = Matrix::Zero(this->dim_, 10);
|
||||
// run T times of iteration to find the beta that has the largest Rayleigh quotient
|
||||
for (size_t t = 0; t < T; t++) {
|
||||
// after each t iteration, reset the betas with the current maxBeta
|
||||
betas = {2 / 3 * maxBeta, 0.99 * maxBeta, maxBeta, 1.01 * maxBeta,
|
||||
1.5 * maxBeta};
|
||||
// iterate through every beta value
|
||||
for (size_t k = 0; k < betas.size(); ++k) {
|
||||
// initialize x0 and x00 in each iteration of each beta
|
||||
Vector x0 = initVector;
|
||||
Vector x00 = Vector::Zero(this->dim_);
|
||||
// run 10 steps of accelerated power iteration with this beta
|
||||
for (size_t j = 1; j < 10; j++) {
|
||||
if (j < 2) {
|
||||
R.col(0) = acceleratedPowerIteration(x0, x00, betas[k]);
|
||||
R.col(1) = acceleratedPowerIteration(R.col(0), x0, betas[k]);
|
||||
} else {
|
||||
R.col(j) = acceleratedPowerIteration(R.col(j - 1), R.col(j - 2),
|
||||
betas[k]);
|
||||
}
|
||||
}
|
||||
// compute the Rayleigh quotient for the randomly sampled vector after
|
||||
// 10 steps of power accelerated iteration
|
||||
const Vector x = R.col(9);
|
||||
const double up = x.dot(this->A_ * x);
|
||||
const double down = x.dot(x);
|
||||
const double mu = up / down;
|
||||
// store the momentum with largest Rayleigh quotient and its according index of beta_
|
||||
if (mu * mu / 4 > maxBeta) {
|
||||
// save the max beta index
|
||||
maxIndex = k;
|
||||
maxBeta = mu * mu / 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
// set beta_ to momentum with largest Rayleigh quotient
|
||||
return betas[maxIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the accelerated iteration, after performing the
|
||||
* accelerated iteration, calculate the ritz error, repeat this
|
||||
* operation until the ritz error converge. If converged return true, else
|
||||
* false.
|
||||
*/
|
||||
bool compute(size_t maxIterations, double tol) {
|
||||
// Starting
|
||||
bool isConverged = false;
|
||||
|
||||
for (size_t i = 0; i < maxIterations && !isConverged; i++) {
|
||||
++(this->nrIterations_);
|
||||
Vector tmp = this->ritzVector_;
|
||||
// update the ritzVector after accelerated power iteration
|
||||
this->ritzVector_ = acceleratedPowerIteration();
|
||||
// update the previousVector with ritzVector
|
||||
previousVector_ = tmp;
|
||||
// update the ritzValue
|
||||
this->ritzValue_ = this->ritzVector_.dot(this->A_ * this->ritzVector_);
|
||||
isConverged = this->converged(tol);
|
||||
}
|
||||
|
||||
return isConverged;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gtsam
|
|
@ -0,0 +1,152 @@
|
|||
/* ----------------------------------------------------------------------------
|
||||
|
||||
* GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||
* Atlanta, Georgia 30332-0415
|
||||
* All Rights Reserved
|
||||
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||
|
||||
* See LICENSE for the license information
|
||||
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @file PowerMethod.h
|
||||
* @date Sept 2020
|
||||
* @author Jing Wu
|
||||
* @brief Power method for fast eigenvalue and eigenvector
|
||||
* computation
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtsam/base/Matrix.h>
|
||||
#include <gtsam/base/Vector.h>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Sparse>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace gtsam {
|
||||
|
||||
using Sparse = Eigen::SparseMatrix<double>;
|
||||
|
||||
/**
|
||||
* \brief Compute maximum Eigenpair with power method
|
||||
*
|
||||
* References :
|
||||
* 1) G. Golub and C. V. Loan, Matrix Computations, 3rd ed. Baltimore, Johns
|
||||
* Hopkins University Press, 1996, pp.405-411
|
||||
* 2) Rosen, D. and Carlone, L., 2017, September. Computational
|
||||
* enhancements for certifiably correct SLAM. In Proceedings of the
|
||||
* International Conference on Intelligent Robots and Systems.
|
||||
* 3) Yulun Tian and Kasra Khosoussi and David M. Rosen and Jonathan P. How,
|
||||
* 2020, Aug, Distributed Certifiably Correct Pose-Graph Optimization, Arxiv
|
||||
* 4) C. de Sa, B. He, I. Mitliagkas, C. Ré, and P. Xu, “Accelerated
|
||||
* stochastic power iteration,” in Proc. Mach. Learn. Res., no. 84, 2018, pp.
|
||||
* 58–67
|
||||
*
|
||||
* It performs the following iteration: \f$ x_{k+1} = A * x_k \f$
|
||||
* where A is the aim matrix we want to get eigenpair of, x is the
|
||||
* Ritz vector
|
||||
*
|
||||
* Template argument Operator just needs multiplication operator
|
||||
*
|
||||
*/
|
||||
template <class Operator>
|
||||
class PowerMethod {
|
||||
protected:
|
||||
/**
|
||||
* Const reference to an externally-held matrix whose minimum-eigenvalue we
|
||||
* want to compute
|
||||
*/
|
||||
const Operator &A_;
|
||||
|
||||
const int dim_; // dimension of Matrix A
|
||||
|
||||
size_t nrIterations_; // number of iterations
|
||||
|
||||
double ritzValue_; // Ritz eigenvalue
|
||||
Vector ritzVector_; // Ritz eigenvector
|
||||
|
||||
public:
|
||||
/// @name Standard Constructors
|
||||
/// @{
|
||||
|
||||
/// Construct from the aim matrix and intial ritz vector
|
||||
explicit PowerMethod(const Operator &A,
|
||||
const boost::optional<Vector> initial = boost::none)
|
||||
: A_(A), dim_(A.rows()), nrIterations_(0) {
|
||||
Vector x0;
|
||||
x0 = initial ? initial.get() : Vector::Random(dim_);
|
||||
x0.normalize();
|
||||
|
||||
// initialize Ritz eigen value
|
||||
ritzValue_ = 0.0;
|
||||
|
||||
// initialize Ritz eigen vector
|
||||
ritzVector_ = powerIteration(x0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run power iteration to get ritzVector with previous ritzVector x, and
|
||||
* return A * x / || A * x ||
|
||||
*/
|
||||
Vector powerIteration(const Vector &x) const {
|
||||
Vector y = A_ * x;
|
||||
y.normalize();
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run power iteration to get ritzVector with previous ritzVector x, and
|
||||
* return A * x / || A * x ||
|
||||
*/
|
||||
Vector powerIteration() const { return powerIteration(ritzVector_); }
|
||||
|
||||
/**
|
||||
* After Perform power iteration on a single Ritz value, check if the Ritz
|
||||
* residual for the current Ritz pair is less than the required convergence
|
||||
* tol, return true if yes, else false
|
||||
*/
|
||||
bool converged(double tol) const {
|
||||
const Vector x = ritzVector_;
|
||||
// store the Ritz eigen value
|
||||
const double ritzValue = x.dot(A_ * x);
|
||||
const double error = (A_ * x - ritzValue * x).norm();
|
||||
return error < tol;
|
||||
}
|
||||
|
||||
/// Return the number of iterations
|
||||
size_t nrIterations() const { return nrIterations_; }
|
||||
|
||||
/**
|
||||
* Start the power/accelerated iteration, after performing the
|
||||
* power/accelerated iteration, calculate the ritz error, repeat this
|
||||
* operation until the ritz error converge. If converged return true, else
|
||||
* false.
|
||||
*/
|
||||
bool compute(size_t maxIterations, double tol) {
|
||||
// Starting
|
||||
bool isConverged = false;
|
||||
|
||||
for (size_t i = 0; i < maxIterations && !isConverged; i++) {
|
||||
++nrIterations_;
|
||||
// update the ritzVector after power iteration
|
||||
ritzVector_ = powerIteration();
|
||||
// update the ritzValue
|
||||
ritzValue_ = ritzVector_.dot(A_ * ritzVector_);
|
||||
isConverged = converged(tol);
|
||||
}
|
||||
|
||||
return isConverged;
|
||||
}
|
||||
|
||||
/// Return the eigenvalue
|
||||
double eigenvalue() const { return ritzValue_; }
|
||||
|
||||
/// Return the eigenvector
|
||||
Vector eigenvector() const { return ritzVector_; }
|
||||
};
|
||||
|
||||
} // namespace gtsam
|
|
@ -0,0 +1,67 @@
|
|||
/* ----------------------------------------------------------------------------
|
||||
|
||||
* GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||
* Atlanta, Georgia 30332-0415
|
||||
* All Rights Reserved
|
||||
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||
|
||||
* See LICENSE for the license information
|
||||
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* powerMethodExample.h
|
||||
*
|
||||
* @file powerMethodExample.h
|
||||
* @date Nov 2020
|
||||
* @author Jing Wu
|
||||
* @brief Create sparse and dense factor graph for
|
||||
* PowerMethod/AcceleratedPowerMethod
|
||||
*/
|
||||
|
||||
#include <gtsam/inference/Symbol.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
|
||||
namespace gtsam {
|
||||
namespace linear {
|
||||
namespace test {
|
||||
namespace example {
|
||||
|
||||
/* ************************************************************************* */
|
||||
inline GaussianFactorGraph createSparseGraph() {
|
||||
using symbol_shorthand::X;
|
||||
// Let's make a scalar synchronization graph with 4 nodes
|
||||
GaussianFactorGraph fg;
|
||||
auto model = noiseModel::Unit::Create(1);
|
||||
for (size_t j = 0; j < 3; j++) {
|
||||
fg.add(X(j), -I_1x1, X(j + 1), I_1x1, Vector1::Zero(), model);
|
||||
}
|
||||
fg.add(X(3), -I_1x1, X(0), I_1x1, Vector1::Zero(), model); // extra row
|
||||
|
||||
return fg;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
inline GaussianFactorGraph createDenseGraph() {
|
||||
using symbol_shorthand::X;
|
||||
// Let's make a scalar synchronization graph with 10 nodes
|
||||
GaussianFactorGraph fg;
|
||||
auto model = noiseModel::Unit::Create(1);
|
||||
// Iterate over nodes
|
||||
for (size_t j = 0; j < 10; j++) {
|
||||
// Each node has an edge with all the others
|
||||
for (size_t i = 1; i < 10; i++)
|
||||
fg.add(X(j), -I_1x1, X((j + i) % 10), I_1x1, Vector1::Zero(), model);
|
||||
}
|
||||
|
||||
return fg;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
} // namespace example
|
||||
} // namespace test
|
||||
} // namespace linear
|
||||
} // namespace gtsam
|
|
@ -0,0 +1,140 @@
|
|||
/* ----------------------------------------------------------------------------
|
||||
|
||||
* GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||
* Atlanta, Georgia 30332-0415
|
||||
* All Rights Reserved
|
||||
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||
|
||||
* See LICENSE for the license information
|
||||
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* testPowerMethod.cpp
|
||||
*
|
||||
* @file testAcceleratedPowerMethod.cpp
|
||||
* @date Sept 2020
|
||||
* @author Jing Wu
|
||||
* @brief Check eigenvalue and eigenvector computed by accelerated power method
|
||||
*/
|
||||
|
||||
#include <CppUnitLite/TestHarness.h>
|
||||
#include <gtsam/base/Matrix.h>
|
||||
#include <gtsam/base/VectorSpace.h>
|
||||
#include <gtsam/inference/Symbol.h>
|
||||
#include <gtsam/linear/AcceleratedPowerMethod.h>
|
||||
#include <gtsam/linear/GaussianFactorGraph.h>
|
||||
#include <gtsam/linear/tests/powerMethodExample.h>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Dense>
|
||||
#include <Eigen/Eigenvalues>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
|
||||
using namespace std;
|
||||
using namespace gtsam;
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(AcceleratedPowerMethod, acceleratedPowerIteration) {
|
||||
// test power iteration, beta is set to 0
|
||||
Sparse A(6, 6);
|
||||
A.coeffRef(0, 0) = 6;
|
||||
A.coeffRef(1, 1) = 5;
|
||||
A.coeffRef(2, 2) = 4;
|
||||
A.coeffRef(3, 3) = 3;
|
||||
A.coeffRef(4, 4) = 2;
|
||||
A.coeffRef(5, 5) = 1;
|
||||
Vector initial = (Vector(6) << 0.24434602, 0.22829942, 0.70094486, 0.15463092, 0.55871359,
|
||||
0.2465342).finished();
|
||||
const double ev1 = 6.0;
|
||||
|
||||
// test accelerated power iteration
|
||||
AcceleratedPowerMethod<Sparse> apf(A, initial);
|
||||
apf.compute(100, 1e-5);
|
||||
EXPECT_LONGS_EQUAL(6, apf.eigenvector().rows());
|
||||
|
||||
Vector6 actual1 = apf.eigenvector();
|
||||
const double ritzValue = actual1.dot(A * actual1);
|
||||
const double ritzResidual = (A * actual1 - ritzValue * actual1).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
|
||||
EXPECT_DOUBLES_EQUAL(ev1, apf.eigenvalue(), 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(AcceleratedPowerMethod, useFactorGraphSparse) {
|
||||
// Let's make a scalar synchronization graph with 4 nodes
|
||||
GaussianFactorGraph fg = gtsam::linear::test::example::createSparseGraph();
|
||||
|
||||
// Get eigenvalues and eigenvectors with Eigen
|
||||
auto L = fg.hessian();
|
||||
Eigen::EigenSolver<Matrix> solver(L.first);
|
||||
|
||||
// find the index of the max eigenvalue
|
||||
size_t maxIdx = 0;
|
||||
for (auto i = 0; i < solver.eigenvalues().rows(); ++i) {
|
||||
if (solver.eigenvalues()(i).real() >= solver.eigenvalues()(maxIdx).real())
|
||||
maxIdx = i;
|
||||
}
|
||||
// Store the max eigenvalue and its according eigenvector
|
||||
const auto ev1 = solver.eigenvalues()(maxIdx).real();
|
||||
|
||||
Vector disturb = Vector4::Random();
|
||||
disturb.normalize();
|
||||
Vector initial = L.first.row(0);
|
||||
double magnitude = initial.norm();
|
||||
initial += 0.03 * magnitude * disturb;
|
||||
AcceleratedPowerMethod<Matrix> apf(L.first, initial);
|
||||
apf.compute(100, 1e-5);
|
||||
// Check if the eigenvalue is the maximum eigen value
|
||||
EXPECT_DOUBLES_EQUAL(ev1, apf.eigenvalue(), 1e-8);
|
||||
|
||||
// Check if the according ritz residual converged to the threshold
|
||||
Vector actual1 = apf.eigenvector();
|
||||
const double ritzValue = actual1.dot(L.first * actual1);
|
||||
const double ritzResidual = (L.first * actual1 - ritzValue * actual1).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(AcceleratedPowerMethod, useFactorGraphDense) {
|
||||
// Let's make a scalar synchronization graph with 10 nodes
|
||||
GaussianFactorGraph fg = gtsam::linear::test::example::createDenseGraph();
|
||||
|
||||
// Get eigenvalues and eigenvectors with Eigen
|
||||
auto L = fg.hessian();
|
||||
Eigen::EigenSolver<Matrix> solver(L.first);
|
||||
|
||||
// find the index of the max eigenvalue
|
||||
size_t maxIdx = 0;
|
||||
for (auto i = 0; i < solver.eigenvalues().rows(); ++i) {
|
||||
if (solver.eigenvalues()(i).real() >= solver.eigenvalues()(maxIdx).real())
|
||||
maxIdx = i;
|
||||
}
|
||||
// Store the max eigenvalue and its according eigenvector
|
||||
const auto ev1 = solver.eigenvalues()(maxIdx).real();
|
||||
|
||||
Vector disturb = Vector10::Random();
|
||||
disturb.normalize();
|
||||
Vector initial = L.first.row(0);
|
||||
double magnitude = initial.norm();
|
||||
initial += 0.03 * magnitude * disturb;
|
||||
AcceleratedPowerMethod<Matrix> apf(L.first, initial);
|
||||
apf.compute(100, 1e-5);
|
||||
// Check if the eigenvalue is the maximum eigen value
|
||||
EXPECT_DOUBLES_EQUAL(ev1, apf.eigenvalue(), 1e-8);
|
||||
|
||||
// Check if the according ritz residual converged to the threshold
|
||||
Vector actual1 = apf.eigenvector();
|
||||
const double ritzValue = actual1.dot(L.first * actual1);
|
||||
const double ritzResidual = (L.first * actual1 - ritzValue * actual1).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
int main() {
|
||||
TestResult tr;
|
||||
return TestRegistry::runAllTests(tr);
|
||||
}
|
||||
/* ************************************************************************* */
|
|
@ -0,0 +1,124 @@
|
|||
/* ----------------------------------------------------------------------------
|
||||
|
||||
* GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||
* Atlanta, Georgia 30332-0415
|
||||
* All Rights Reserved
|
||||
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
|
||||
|
||||
* See LICENSE for the license information
|
||||
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* testPowerMethod.cpp
|
||||
*
|
||||
* @file testPowerMethod.cpp
|
||||
* @date Sept 2020
|
||||
* @author Jing Wu
|
||||
* @brief Check eigenvalue and eigenvector computed by power method
|
||||
*/
|
||||
|
||||
#include <CppUnitLite/TestHarness.h>
|
||||
#include <gtsam/base/Matrix.h>
|
||||
#include <gtsam/base/VectorSpace.h>
|
||||
#include <gtsam/inference/Symbol.h>
|
||||
#include <gtsam/linear/GaussianFactorGraph.h>
|
||||
#include <gtsam/linear/PowerMethod.h>
|
||||
#include <gtsam/linear/tests/powerMethodExample.h>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Dense>
|
||||
#include <Eigen/Eigenvalues>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
|
||||
using namespace std;
|
||||
using namespace gtsam;
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(PowerMethod, powerIteration) {
|
||||
// test power iteration, beta is set to 0
|
||||
Sparse A(6, 6);
|
||||
A.coeffRef(0, 0) = 6;
|
||||
A.coeffRef(1, 1) = 5;
|
||||
A.coeffRef(2, 2) = 4;
|
||||
A.coeffRef(3, 3) = 3;
|
||||
A.coeffRef(4, 4) = 2;
|
||||
A.coeffRef(5, 5) = 1;
|
||||
Vector initial = (Vector(6) << 0.24434602, 0.22829942, 0.70094486, 0.15463092, 0.55871359,
|
||||
0.2465342).finished();
|
||||
PowerMethod<Sparse> pf(A, initial);
|
||||
pf.compute(100, 1e-5);
|
||||
EXPECT_LONGS_EQUAL(6, pf.eigenvector().rows());
|
||||
|
||||
Vector6 actual1 = pf.eigenvector();
|
||||
const double ritzValue = actual1.dot(A * actual1);
|
||||
const double ritzResidual = (A * actual1 - ritzValue * actual1).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
|
||||
const double ev1 = 6.0;
|
||||
EXPECT_DOUBLES_EQUAL(ev1, pf.eigenvalue(), 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(PowerMethod, useFactorGraphSparse) {
|
||||
// Let's make a scalar synchronization graph with 4 nodes
|
||||
GaussianFactorGraph fg = gtsam::linear::test::example::createSparseGraph();
|
||||
|
||||
// Get eigenvalues and eigenvectors with Eigen
|
||||
auto L = fg.hessian();
|
||||
Eigen::EigenSolver<Matrix> solver(L.first);
|
||||
|
||||
// find the index of the max eigenvalue
|
||||
size_t maxIdx = 0;
|
||||
for (auto i = 0; i < solver.eigenvalues().rows(); ++i) {
|
||||
if (solver.eigenvalues()(i).real() >= solver.eigenvalues()(maxIdx).real())
|
||||
maxIdx = i;
|
||||
}
|
||||
// Store the max eigenvalue and its according eigenvector
|
||||
const auto ev1 = solver.eigenvalues()(maxIdx).real();
|
||||
|
||||
Vector initial = Vector4::Random();
|
||||
PowerMethod<Matrix> pf(L.first, initial);
|
||||
pf.compute(100, 1e-5);
|
||||
EXPECT_DOUBLES_EQUAL(ev1, pf.eigenvalue(), 1e-8);
|
||||
auto actual2 = pf.eigenvector();
|
||||
const double ritzValue = actual2.dot(L.first * actual2);
|
||||
const double ritzResidual = (L.first * actual2 - ritzValue * actual2).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
TEST(PowerMethod, useFactorGraphDense) {
|
||||
// Let's make a scalar synchronization graph with 10 nodes
|
||||
GaussianFactorGraph fg = gtsam::linear::test::example::createDenseGraph();
|
||||
|
||||
// Get eigenvalues and eigenvectors with Eigen
|
||||
auto L = fg.hessian();
|
||||
Eigen::EigenSolver<Matrix> solver(L.first);
|
||||
|
||||
// find the index of the max eigenvalue
|
||||
size_t maxIdx = 0;
|
||||
for (auto i = 0; i < solver.eigenvalues().rows(); ++i) {
|
||||
if (solver.eigenvalues()(i).real() >= solver.eigenvalues()(maxIdx).real())
|
||||
maxIdx = i;
|
||||
}
|
||||
// Store the max eigenvalue and its according eigenvector
|
||||
const auto ev1 = solver.eigenvalues()(maxIdx).real();
|
||||
|
||||
Vector initial = Vector10::Random();
|
||||
PowerMethod<Matrix> pf(L.first, initial);
|
||||
pf.compute(100, 1e-5);
|
||||
EXPECT_DOUBLES_EQUAL(ev1, pf.eigenvalue(), 1e-8);
|
||||
auto actual2 = pf.eigenvector();
|
||||
const double ritzValue = actual2.dot(L.first * actual2);
|
||||
const double ritzResidual = (L.first * actual2 - ritzValue * actual2).norm();
|
||||
EXPECT_DOUBLES_EQUAL(0, ritzResidual, 1e-5);
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
int main() {
|
||||
TestResult tr;
|
||||
return TestRegistry::runAllTests(tr);
|
||||
}
|
||||
/* ************************************************************************* */
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
#include <SymEigsSolver.h>
|
||||
#include <cmath>
|
||||
#include <gtsam/linear/PCGSolver.h>
|
||||
#include <gtsam/linear/SubgraphPreconditioner.h>
|
||||
#include <gtsam/nonlinear/LevenbergMarquardtOptimizer.h>
|
||||
|
@ -487,8 +488,88 @@ Sparse ShonanAveraging<d>::computeA(const Values &values) const {
|
|||
return Lambda - Q_;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
template <size_t d>
|
||||
Sparse ShonanAveraging<d>::computeA(const Matrix &S) const {
|
||||
auto Lambda = computeLambda(S);
|
||||
return Lambda - Q_;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
// Perturb the initial initialVector by adding a spherically-uniformly
|
||||
// distributed random vector with 0.03*||initialVector||_2 magnitude to
|
||||
// initialVector
|
||||
// ref : Part III. C, Rosen, D. and Carlone, L., 2017, September. Computational
|
||||
// enhancements for certifiably correct SLAM. In Proceedings of the
|
||||
// International Conference on Intelligent Robots and Systems.
|
||||
static Vector perturb(const Vector &initialVector) {
|
||||
// generate a 0.03*||x_0||_2 as stated in David's paper
|
||||
int n = initialVector.rows();
|
||||
Vector disturb = Vector::Random(n);
|
||||
disturb.normalize();
|
||||
|
||||
double magnitude = initialVector.norm();
|
||||
Vector perturbedVector = initialVector + 0.03 * magnitude * disturb;
|
||||
perturbedVector.normalize();
|
||||
return perturbedVector;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
/// MINIMUM EIGENVALUE COMPUTATIONS
|
||||
// Alg.6 from paper Distributed Certifiably Correct Pose-Graph Optimization,
|
||||
// it takes in the certificate matrix A as input, the maxIterations and the
|
||||
// minEigenvalueNonnegativityTolerance is set to 1000 and 10e-4 ad default,
|
||||
// there are two parts
|
||||
// in this algorithm:
|
||||
// (1) compute the maximum eigenpair (\lamda_dom, \vect{v}_dom) of A by power
|
||||
// method. if \lamda_dom is less than zero, then return the eigenpair. (2)
|
||||
// compute the maximum eigenpair (\theta, \vect{v}) of C = \lamda_dom * I - A by
|
||||
// accelerated power method. Then return (\lamda_dom - \theta, \vect{v}).
|
||||
static bool PowerMinimumEigenValue(
|
||||
const Sparse &A, const Matrix &S, double &minEigenValue,
|
||||
Vector *minEigenVector = 0, size_t *numIterations = 0,
|
||||
size_t maxIterations = 1000,
|
||||
double minEigenvalueNonnegativityTolerance = 10e-4) {
|
||||
|
||||
// a. Compute dominant eigenpair of S using power method
|
||||
PowerMethod<Sparse> pmOperator(A);
|
||||
|
||||
const bool pmConverged = pmOperator.compute(
|
||||
maxIterations, 1e-5);
|
||||
|
||||
// Check convergence and bail out if necessary
|
||||
if (!pmConverged) return false;
|
||||
|
||||
const double pmEigenValue = pmOperator.eigenvalue();
|
||||
|
||||
if (pmEigenValue < 0) {
|
||||
// The largest-magnitude eigenvalue is negative, and therefore also the
|
||||
// minimum eigenvalue, so just return this solution
|
||||
minEigenValue = pmEigenValue;
|
||||
if (minEigenVector) {
|
||||
*minEigenVector = pmOperator.eigenvector();
|
||||
minEigenVector->normalize(); // Ensure that this is a unit vector
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const Sparse C = pmEigenValue * Matrix::Identity(A.rows(), A.cols()) - A;
|
||||
const boost::optional<Vector> initial = perturb(S.row(0));
|
||||
AcceleratedPowerMethod<Sparse> apmShiftedOperator(C, initial);
|
||||
|
||||
const bool minConverged = apmShiftedOperator.compute(
|
||||
maxIterations, minEigenvalueNonnegativityTolerance / pmEigenValue);
|
||||
|
||||
if (!minConverged) return false;
|
||||
|
||||
minEigenValue = pmEigenValue - apmShiftedOperator.eigenvalue();
|
||||
if (minEigenVector) {
|
||||
*minEigenVector = apmShiftedOperator.eigenvector();
|
||||
minEigenVector->normalize(); // Ensure that this is a unit vector
|
||||
}
|
||||
if (numIterations) *numIterations = apmShiftedOperator.nrIterations();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** This is a lightweight struct used in conjunction with Spectra to compute
|
||||
* the minimum eigenvalue and eigenvector of a sparse matrix A; it has a single
|
||||
|
@ -634,13 +715,6 @@ static bool SparseMinimumEigenValue(
|
|||
return true;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
template <size_t d>
|
||||
Sparse ShonanAveraging<d>::computeA(const Matrix &S) const {
|
||||
auto Lambda = computeLambda(S);
|
||||
return Lambda - Q_;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
template <size_t d>
|
||||
double ShonanAveraging<d>::computeMinEigenValue(const Values &values,
|
||||
|
@ -658,6 +732,23 @@ double ShonanAveraging<d>::computeMinEigenValue(const Values &values,
|
|||
return minEigenValue;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
template <size_t d>
|
||||
double ShonanAveraging<d>::computeMinEigenValueAP(const Values &values,
|
||||
Vector *minEigenVector) const {
|
||||
assert(values.size() == nrUnknowns());
|
||||
const Matrix S = StiefelElementMatrix(values);
|
||||
auto A = computeA(S);
|
||||
|
||||
double minEigenValue = 0;
|
||||
bool success = PowerMinimumEigenValue(A, S, minEigenValue, minEigenVector);
|
||||
if (!success) {
|
||||
throw std::runtime_error(
|
||||
"PowerMinimumEigenValue failed to compute minimum eigenvalue.");
|
||||
}
|
||||
return minEigenValue;
|
||||
}
|
||||
|
||||
/* ************************************************************************* */
|
||||
template <size_t d>
|
||||
std::pair<double, Vector> ShonanAveraging<d>::computeMinEigenVector(
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
#include <gtsam/linear/VectorValues.h>
|
||||
#include <gtsam/nonlinear/LevenbergMarquardtParams.h>
|
||||
#include <gtsam/sfm/BinaryMeasurement.h>
|
||||
#include <gtsam/linear/PowerMethod.h>
|
||||
#include <gtsam/linear/AcceleratedPowerMethod.h>
|
||||
#include <gtsam/slam/dataset.h>
|
||||
|
||||
#include <Eigen/Sparse>
|
||||
|
@ -250,6 +252,13 @@ class GTSAM_EXPORT ShonanAveraging {
|
|||
double computeMinEigenValue(const Values &values,
|
||||
Vector *minEigenVector = nullptr) const;
|
||||
|
||||
/**
|
||||
* Compute minimum eigenvalue with accelerated power method.
|
||||
* @param values: should be of type SOn
|
||||
*/
|
||||
double computeMinEigenValueAP(const Values &values,
|
||||
Vector *minEigenVector = nullptr) const;
|
||||
|
||||
/// Project pxdN Stiefel manifold matrix S to Rot3^N
|
||||
Values roundSolutionS(const Matrix &S) const;
|
||||
|
||||
|
|
|
@ -188,9 +188,14 @@ TEST(ShonanAveraging3, CheckWithEigen) {
|
|||
for (int i = 1; i < lambdas.size(); i++)
|
||||
minEigenValue = min(lambdas(i), minEigenValue);
|
||||
|
||||
// Compute Eigenvalue with Accelerated Power method
|
||||
double lambdaAP = kShonan.computeMinEigenValueAP(Qstar3);
|
||||
|
||||
// Actual check
|
||||
EXPECT_DOUBLES_EQUAL(0, lambda, 1e-11);
|
||||
EXPECT_DOUBLES_EQUAL(0, minEigenValue, 1e-11);
|
||||
EXPECT_DOUBLES_EQUAL(0, lambdaAP, 1e-11);
|
||||
|
||||
|
||||
// Construct test descent direction (as minEigenVector is not predictable
|
||||
// across platforms, being one from a basically flat 3d- subspace)
|
||||
|
|
Loading…
Reference in New Issue