Pay attention to condition number in covariance estimation.
1. Sparse covariance estimation now uses cholmod_rcond to
detect singular Jacobians.
2. Dense covariance estimation now uses relative magnitude
of singular/eigen values to compute the pseudoinverse.
3. Truncation logic is now unified with Solver::Options::null_space_rank.
Change-Id: I095bd737510c836b4251255926190a7f31d64bce
diff --git a/include/ceres/covariance.h b/include/ceres/covariance.h
index 6533487..26f94f7 100644
--- a/include/ceres/covariance.h
+++ b/include/ceres/covariance.h
@@ -159,8 +159,12 @@
// reconstruction is ambiguous upto a similarity transform. This is
// known as a Gauge Ambiguity. Handling Gauges correctly requires the
// use of SVD or custom inversion algorithms. For small problems the
-// user can use the dense algorithm. For more details see Morris,
-// Kanatani & Kanade's work the subject.
+// user can use the dense algorithm. For more details see
+//
+// Ken-ichi Kanatani, Daniel D. Morris: Gauges and gauge
+// transformations for uncertainty description of geometric structure
+// with indeterminacy. IEEE Transactions on Information Theory 47(5):
+// 2017-2028 (2001)
//
// Example Usage
// =============
@@ -201,7 +205,7 @@
#else
use_dense_linear_algebra(true),
#endif
- min_singular_value_threshold(1e-8),
+ min_reciprocal_condition_number(1e-14),
null_space_rank(0),
apply_loss_function(true) {
}
@@ -210,18 +214,87 @@
// estimation of covariance.
int num_threads;
- // Use Eigen's JacobiSVD algorithm to compute the covariance. This
- // is a very accurate but slow algorithm. The up side is that it
- // can handle numerically rank deficient jacobians. This option
- // only makes sense for small to moderate sized problems.
+ // Use Eigen's JacobiSVD algorithm to compute the covariance
+ // instead of SuiteSparse. This is a very accurate but slow
+ // algorithm. The up side is that it can handle numerically rank
+ // deficient jacobians. This option only makes sense for small to
+ // moderate sized problems.
bool use_dense_linear_algebra;
- // When use_dense_linear_algebra is true, singular values less
- // than min_singular_value_threshold are set to zero.
- double min_singular_value_threshold;
+ // If the Jacobian matrix is near singular, then inverting J'J
+ // will result in unreliable results, e.g,
+ //
+ // J = [1.0 1.0 ]
+ // [1.0 1.0000001 ]
+ //
+ // Which is essentially a rank deficient matrix
+ //
+ // inv(J'J) = [ 2.0471e+14 -2.0471e+14]
+ // [-2.0471e+14 2.0471e+14]
+ //
+ // The reciprocal condition number of a matrix is a measure of
+ // ill-conditioning or how close the matrix is to being
+ // singular/rank deficient. It is defined as the ratio of the
+ // smallest eigenvalue of the matrix to the largest eigenvalue. In
+ // the above case the reciprocal condition number is about
+ // 1e-16. Which is close to machine precision and even though the
+ // inverse exists, it is meaningless, and care should be taken to
+ // interpet the results of such an inversion.
+ //
+ // Matrices with condition number lower than
+ // min_reciprocal_condition_number are considered rank deficient.
+ //
+ // Depending on the value of use_dense_linear_algebra this may
+ // have further consequences on the covariance estimation process.
+ //
+ // 1. use_dense_linear_algebra = false
+ //
+ // If the reciprocal_condition_number of J'J is less than
+ // min_reciprocal_condition_number, Covariance::Compute() will
+ // fail and return false.
+ //
+ // 2. use_dense_linear_algebra = true
+ //
+ // When dense covariance estimation is being used, then rank
+ // deficiency/singularity of the Jacobian can be handled in a
+ // more sophisticated manner.
+ //
+ // If null_space_rank = -1, then instead of computing the
+ // inverse of J'J, the Moore-Penrose Pseudoinverse is computed. If
+ // (lambda_i, e_i) are eigenvalue and eigenvector pairs of J'J.
+ //
+ // pseudoinverse[J'J] = sum_i e_i e_i' / lambda_i
+ //
+ // if lambda_i / lambda_max >= min_reciprocal_condition_number.
+ //
+ // If null_space_rank is non-negative, then the smallest
+ // null_space_rank eigenvalue/eigenvectors are dropped
+ // irrespective of the magnitude of lambda_i. If the ratio of
+ // the smallest non-zero eigenvalue to the largest eigenvalue
+ // in the truncated matrix is still below
+ // min_reciprocal_condition_number, then the
+ // Covariance::Compute() will fail and return false.
+ double min_reciprocal_condition_number;
- // When use_dense_linear_algebra is true, the bottom
- // null_space_rank singular values are set to zero.
+ // When use_dense_linear_algebra is true, null_space_rank
+ // determines how many of the smallest eigenvectors of J'J are
+ // dropped when computing the pseudoinverse.
+ //
+ // If null_space_rank = -1, then instead of computing the inverse
+ // of J'J, the Moore-Penrose Pseudoinverse is computed. If
+ // (lambda_i, e_i) are eigenvalue and eigenvector pairs of J'J.
+ //
+ // pseudoinverse[J'J] = sum_i e_i e_i' / lambda_i
+ //
+ // if lambda_i / lambda_max >= min_reciprocal_condition_number.
+ //
+ // If null_space_rank is non-negative, then the smallest
+ // null_space_rank eigenvalue/eigenvectors are dropped
+ // irrespective of the magnitude of lambda_i. If the ratio of the
+ // smallest non-zero eigenvalue to the largest eigenvalue in the
+ // truncated matrix is still below
+ // min_reciprocal_condition_number, then the Covariance::Compute()
+ // will fail and return false.
int null_space_rank;
// Even though the residual blocks in the problem may contain loss
@@ -254,20 +327,28 @@
// what parts of the covariance matrix are computed. The full
// Jacobian is used to do the computation, i.e. they do not have an
// impact on what part of the Jacobian is used for computation.
+ //
+ // The return value indicates the success or failure of the
+ // covariance computation. Please see the documentation for
+ // Covariance::Options for more on the conditions under which this
+ // function returns false.
bool Compute(
const vector<pair<const double*, const double*> >& covariance_blocks,
Problem* problem);
- // Compute must be called before the first call to
- // GetCovarianceBlock. covariance_block must point to a memory
- // location that can store a parameter_block1_size x
- // parameter_block2_size matrix. The returned covariance will be a
- // row-major matrix.
+ // Return the block of the covariance matrix corresponding to
+ // parameter_block1 and parameter_block2.
//
- // The pair <parameter_block1, parameter_block2> OR the pair
- // <parameter_block2, parameter_block1> must have been present in
- // the vector covariance_blocks when Compute was called. Otherwise
+ // Compute must be called before the first call to
+ // GetCovarianceBlock and the pair <parameter_block1,
+ // parameter_block2> OR the pair <parameter_block2,
+ // parameter_block1> must have been present in the vector
+ // covariance_blocks when Compute was called. Otherwise
// GetCovarianceBlock will return false.
+ //
+ // covariance_block must point to a memory location that can store a
+ // parameter_block1_size x parameter_block2_size matrix. The
+ // returned covariance will be a row-major matrix.
bool GetCovarianceBlock(const double* parameter_block1,
const double* parameter_block2,
double* covariance_block) const;
diff --git a/internal/ceres/covariance_impl.cc b/internal/ceres/covariance_impl.cc
index 1a96193..966ba35 100644
--- a/internal/ceres/covariance_impl.cc
+++ b/internal/ceres/covariance_impl.cc
@@ -397,9 +397,23 @@
cholmod_factor* factor = ss_.AnalyzeCholesky(&cholmod_jacobian_view);
event_logger.AddEvent("Symbolic Factorization");
- bool status = ss_.Cholesky(&cholmod_jacobian_view, factor);
+ bool factorization_succeeded = ss_.Cholesky(&cholmod_jacobian_view, factor);
+ if (factorization_succeeded) {
+ const double reciprocal_condition_number =
+ cholmod_rcond(factor, ss_.mutable_cc());
+ if (reciprocal_condition_number <
+ options_.min_reciprocal_condition_number) {
+ LOG(WARNING) << "Cholesky factorization of J'J is not reliable. "
+ << "Reciprocal condition number: "
+ << reciprocal_condition_number << " "
+ << "min_reciprocal_condition_number : "
+ << options_.min_reciprocal_condition_number;
+ factorization_succeeded = false;
+ }
+ }
+
event_logger.AddEvent("Numeric Factorization");
- if (!status) {
+ if (!factorization_succeeded) {
ss_.Free(factor);
LOG(WARNING) << "Cholesky factorization failed.";
return false;
@@ -520,22 +534,39 @@
Eigen::JacobiSVD<Matrix> svd(dense_jacobian,
Eigen::ComputeThinU | Eigen::ComputeThinV);
- Vector inverse_singular_values = svd.singularValues();
- event_logger.AddEvent("SVD");
+ event_logger.AddEvent("SingularValueDecomposition");
- for (int i = 0; i < inverse_singular_values.rows(); ++i) {
- if (inverse_singular_values[i] > options_.min_singular_value_threshold &&
- i < (inverse_singular_values.rows() - options_.null_space_rank)) {
- inverse_singular_values[i] =
- 1.0 / (inverse_singular_values[i] * inverse_singular_values[i]);
- } else {
- inverse_singular_values[i] = 0.0;
+ const Vector singular_values = svd.singularValues();
+ const int num_singular_values = singular_values.rows();
+ Vector inverse_squared_singular_values(num_singular_values);
+ inverse_squared_singular_values.setZero();
+
+ const double max_singular_value = singular_values[0];
+ const double min_singular_value_ratio =
+ sqrt(options_.min_reciprocal_condition_number);
+
+ const bool automatic_truncation = (options_.null_space_rank < 0);
+ const int max_rank = min(num_singular_values,
+ num_singular_values - options_.null_space_rank);
+
+ for (int i = 0; i < max_rank; ++i) {
+ const double singular_value_ratio = singular_values[i] / max_singular_value;
+ if (singular_value_ratio >= min_singular_value_ratio) {
+ inverse_squared_singular_values[i] =
+ 1.0 / (singular_values[i] * singular_values[i]);
+ } else if (!automatic_truncation) {
+ LOG(WARNING) << "Cholesky factorization of J'J is not reliable. "
+ << "Reciprocal condition number: "
+ << singular_value_ratio * singular_value_ratio << " "
+ << "min_reciprocal_condition_number : "
+ << options_.min_reciprocal_condition_number;
+ return false;
}
}
Matrix dense_covariance =
svd.matrixV() *
- inverse_singular_values.asDiagonal() *
+ inverse_squared_singular_values.asDiagonal() *
svd.matrixV().transpose();
event_logger.AddEvent("PseudoInverse");
diff --git a/internal/ceres/covariance_test.cc b/internal/ceres/covariance_test.cc
index b74efda..05fb234 100644
--- a/internal/ceres/covariance_test.cc
+++ b/internal/ceres/covariance_test.cc
@@ -532,15 +532,24 @@
-1.6076e-02, 1.2549e-02, -3.3329e-04, -6.6659e-04, -9.9988e-04, 3.9539e-02
};
- Covariance::Options options;
- options.use_dense_linear_algebra = true;
- options.null_space_rank = 1;
- ComputeAndCompareCovarianceBlocks(options, expected_covariance);
- options.use_dense_linear_algebra = true;
- options.null_space_rank = 0;
- options.min_singular_value_threshold = sqrt(3.5);
- ComputeAndCompareCovarianceBlocks(options, expected_covariance);
+ {
+ Covariance::Options options;
+ options.use_dense_linear_algebra = true;
+ // Force dropping of the smallest eigenvector.
+ options.null_space_rank = 1;
+ ComputeAndCompareCovarianceBlocks(options, expected_covariance);
+ }
+
+ {
+ Covariance::Options options;
+ options.use_dense_linear_algebra = true;
+ // Force dropping of the smallest eigenvector via the ratio but
+ // automatic truncation.
+ options.min_reciprocal_condition_number = 0.044494;
+ options.null_space_rank = -1;
+ ComputeAndCompareCovarianceBlocks(options, expected_covariance);
+ }
}
class RankDeficientCovarianceTest : public CovarianceTest {
@@ -598,7 +607,7 @@
}
};
-TEST_F(RankDeficientCovarianceTest, MinSingularValueTolerance) {
+TEST_F(RankDeficientCovarianceTest, AutomaticTruncation) {
// J
//
// 1 0 0 0 0 0
@@ -631,15 +640,7 @@
Covariance::Options options;
options.use_dense_linear_algebra = true;
- ComputeAndCompareCovarianceBlocks(options, expected_covariance);
-
- options.null_space_rank = 1;
- ComputeAndCompareCovarianceBlocks(options, expected_covariance);
-
- options.null_space_rank = 2;
- ComputeAndCompareCovarianceBlocks(options, expected_covariance);
-
- options.null_space_rank = 3;
+ options.null_space_rank = -1;
ComputeAndCompareCovarianceBlocks(options, expected_covariance);
}