-
Notifications
You must be signed in to change notification settings - Fork 767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added accelerated power method to compute eigenpair fast #533
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @jingwuOUO,
Looks like you have all of the necessary pieces here, but this implementation looks to me like it's waaaaay more complicated than it needs to be.
I think it would be much better to refactor this into two simple standalone functions (i.e. not structs, which in my opinion are way overkill for such simple functions):
powerMethod()
, which implements only the basic (optionally-accelerated) power methodcomputeMinimumEigenPair()
, which implements Alg 6 in the paper (usingpowerMethod()
)
You should be able to implement a basic version of PowerMethod()
in ~30 lines of code, and similarly for computeMinimumEigenPair()
Importantly: everything involving initialization, etc. should be done outside of the powerMethod()
function -- this function should take as input the starting vector v0 (perhaps with an optional default random initialization of appropriate dimension, in case the user doesn't have, or doesn't want to supply, an explicit initial estimate).
@david-m-rosen i will take over from @jingwuOUO for a bit but will take your comments into account. |
@jingwuOUO back to you. However, I'd like to speak to you first before you start editing, so let's schedule a meeting. |
Given our conversation, please:
|
8f3fbd2
to
86255f6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, This is starting to look good. Still, a large number of cleanup/documentation needed before this can go into GTSAM. I'd also like you to split the header into two files (and the tests) and move them to the linear sub-directory, as they are actually general and have nothing to do with sfm per se.
415bdef
to
b7f29a0
Compare
Just a comment: please do not force-push a public branch, ever. It breaks the review process, and a number of other things. If this is about having the latest develop, justmerge develop into your branch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a number of things. Don't force-push
@dellaert I'd also be happy to give this a close look whenever you have a version you're happy with, if you'd like a second set of eyes on it [I've not been tracking this closely for the past few weeks, as I got the sense that you and Jing wanted to take the lead on this for a bit :-) ]. |
Yeah, my latest comments are just nits, so maybe you can take a look at the algorithmic aspects and comments? Would like to land this soon. Note, it's in linear subdirectory now and the intent is it is generic, not specific to Shonan. All Shonan-specific stuff should be in ShonanAveraging... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dellaert @jingwuOUO OK I finished giving this another close look.
Overall the Shonan Averaging code looks much cleaner -- there are a few small changes to the minimum eigenvalue computation that should be done to speed up the method, but after having restructured the code, these amount to changing 1 or 2 lines. This part looks good to me.
The power method and accelerated power method code still needs a bit of work -- there's still weird stuff being done in a few places. The unit tests also need to be fixed -- right now the logic in these tests is actually completely broken (see comments).
Yes. The test previous is not well-written. There are mistakes that I didn't fix. Now I have corrected the input with right value. For powermethod, the initial vector is a random one. And for accelerated, the initial is the row0 of aim matrix. Also revised the code according to your comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, still a few things that need to be fixed, but I think this is getting pretty close now :-)
|
||
// initialize beta_ | ||
if (!initialBeta) { | ||
estimateBeta(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't agree with having estimateBeta()
here -- I really don't think it's a good idea to have a heavy-duty operation like estimateBeta()
be a part of the constructor.
Notice, for example, that it would be impossible to construct an instance of this class for later use without that code immediately running. In contrast, if you remove that one function, the constructor becomes basically a no-op.
I think it would be much better to have beta_
set directly using initialBeta
, and leave estimateBeta()
as a completely separate function that can be called by the user, if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: I still think estimateBeta()
should not be called here ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jingwuOUO is there a reason estimateBeta
can't be moved out as a static function??
gtsam/sfm/ShonanAveraging.cpp
Outdated
// (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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK this looks pretty good now, but I think it might be a bit cleaner to use double &
instead of double*
for minEigenValue
and size_t&
instead of size_t*
for numIterations
(I like to avoid pointers whenever I can -- lots of creative ways to cause horrible segfaults with these :-P)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this part. I'm not 100% sure if there is further consideration to use double*
than double &
and size_t
because this form follows previous code by @dellaert . Could you take a look?
A.coeffRef(0, 0) = 2; | ||
A.coeffRef(0, 0) = 1; | ||
Vector initial = Vector6::Zero(); | ||
const Vector6 x1 = (Vector(6) << 1.0, 0.0, 0.0, 0.0, 0.0, 0.0).finished(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better here to make initial
a hard-coded vector instead of a random sample -- this would make it easier to reproduce failing tests at different times, different machines, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few final edits (all minor at this point), and I think this will be ready to go.
// Update the ritzVector with beta and previous two ritzVector, and return | ||
// x_{k+1} = A * x_k - \beta * x_{k-1} / || A * x_k - \beta * x_{k-1} || | ||
Vector powerIteration(const Vector &x1, const Vector &x0, | ||
// Run accelerated power iteration on the ritzVector with beta and previous |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should change this description so that the docstring matches the function arguments -- use x_1
, x_0
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also added parenthesis in numerator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still see a mismatch between doc and argument names. Also, should be a doxygen comment, /** */
Vector x0 = Vector::Random(this->dim_); | ||
x0.normalize(); | ||
Vector x00 = Vector::Zero(this->dim_); | ||
size_t T = 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might consider making the parameter T
an argument of this function (maybe with the default value 10)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do?
} | ||
|
||
/* ************************************************************************* */ | ||
TEST(AcceleratedPowerMethod, useFactorGraph) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test still looks excessively complicated to me to be a good unit test ...
// Check that we get zero eigenvalue and "constant" eigenvector | ||
EXPECT_DOUBLES_EQUAL(0.0, solver.eigenvalues()[0].real(), 1e-9); | ||
auto v0 = solver.eigenvectors().col(0); | ||
for (size_t j = 0; j < 3; j++) EXPECT_DOUBLES_EQUAL(-0.5, v0[j].real(), 1e-9); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, you should never enforce getting a specific eigenvector
// Check that we get zero eigenvalue and "constant" eigenvector | ||
EXPECT_DOUBLES_EQUAL(0.0, solver.eigenvalues()[0].real(), 1e-9); | ||
auto v0 = solver.eigenvectors().col(0); | ||
for (size_t j = 0; j < 3; j++) EXPECT_DOUBLES_EQUAL(-0.5, v0[j].real(), 1e-9); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before: you should never enforce getting a specific eigenvector
pf.compute(50, 1e-4); | ||
EXPECT_DOUBLES_EQUAL(ev1, pf.eigenvalue(), 1e-8); | ||
auto actual2 = pf.eigenvector(); | ||
EXPECT(assert_equal(ev2, actual2, 3e-5)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, you should never enforce getting a specific eigenvector
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About why using this unittest, the idea comes from that we want to use the graph.hessian() later to extract the A matrix. There are some tests using the EigenSolver from Eigen to see whether the eigenpair result is always "constant" and also to compare the results of power/accelerated method at initial (but now more clear about the correctness of power method).
But I'm still a little confused here, for the specific matrix A in this unittest, there is no two eigenvectors for one eigenvalue, and the eigenvector is normalized, the only uncertainty here is the sign of the eigenvector, whether +/-. So I guess it's fine to keep this. I'm not sure. I'm ok with deleting the checking specific eigenvector part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with:
EXPECT(assert_equal(ev2, actual2, 3e-5));
is that you are trying to enforce the condition that the (unit) eigenvector actual2
returned by the power method is exactly equal to the eigenvector ev2
returned by Eigen. But there are always at least 2 unit eigenvectors associated with any given eigenvalue, and in general there will be infinitely many (if that eigenvalue happens to have multiplicity > 1).
So the question here is: why would we ever expect the power method to return that specific eigenvector?
The answer is: we wouldn't, because eigenvectors are never unique. And if they're not unique, we shouldn't be trying to enforce uniqueness in a unit test. This doesn't make mathematical sense.
The correct test of "eigenvector-ness" is the eigenvector equation A*x = lambda*x
. Specifically, we should be checking something like a normalized Ritz residual:
eps := ||A*x - lambda*x|| / ||x||
Note that this error measure does not depend upon which specific eigenvector we get, since any eigenvector x
for eigenvalue lambda
would have eps = 0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it!
gtsam/sfm/ShonanAveraging.cpp
Outdated
// 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. | ||
Vector perturb(const Vector &initialVector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok got it -- I missed this earlier.
But this construction looks a bit weird to me: you have perturb()
as a function in ShonanAveraging.cpp, but not in ShonanAveraging.h -- this means that this function is only "visible" to functions in ShonanAveraging.cpp that occur lower down in this file.
I could easily see a case in which you might want to use this functionality outside of ShonanAveraging.cpp -- for example, it looks like you do this kind of perturbation in some of the unit tests?
I would suggest exposing this function externally by including its declaration in ShonanAveraging.h
Updated according to last review by David. Something I'm not sure about. I see previous |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still some comments by David that have not been addressed. Also, please read for 5 minutes about doxygen and then please apply that to entire PR.
|
||
// initialize beta_ | ||
if (!initialBeta) { | ||
estimateBeta(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jingwuOUO is there a reason estimateBeta
can't be moved out as a static function??
// Update the ritzVector with beta and previous two ritzVector, and return | ||
// x_{k+1} = A * x_k - \beta * x_{k-1} / || A * x_k - \beta * x_{k-1} || | ||
Vector powerIteration(const Vector &x1, const Vector &x0, | ||
// Run accelerated power iteration on the ritzVector with beta and previous |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still see a mismatch between doc and argument names. Also, should be a doxygen comment, /** */
} | ||
|
||
/* ************************************************************************* */ | ||
TEST(AcceleratedPowerMethod, useFactorGraph) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a test for factor graph operator, at least that part should be tested somewhere. We should have tests for a sparse and dense matrix, as well. Simplicity would be good.
So we need to add unittest for sparse and dense matrix, can I create some sparse and dense matrix myself? And then add it in these two test file. |
Addressed in meeting. Please re-request review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will approve but trust you to address the final few comments I have.
gtsam/linear/PowerMethod.h
Outdated
* \brief Compute maximum Eigenpair with power method | ||
* | ||
* References : | ||
* 1) Rosen, D. and Carlone, L., 2017, September. Computational |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note sure this reference is appropriate for power method. More for accelerated. Power method itself is as old as the street.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added Matrix Computation as reference and noted pages, which is also cited by Yunlun_arxiv paper
} | ||
|
||
/// Tuning the momentum beta using the Best Heavy Ball algorithm in Ref(3) | ||
double estimateBeta() const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explain again why this is still a method? I thought David had asked to make this a free function or at least a static method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dellaert @jingwuOUO I think it's fine to have this as a method (in fact, probably necessary, since we need access to the matrix-vector multiplication operator that's passed in during construction). What I don't want is this function being called inside the constructor (as currently happens in line 71), since this is a heavy-duty operation that is not actually required in order to construct an instance of this object -- permitting a default value of beta = 0.0 is perfectly fine, and I think arguably the "right" default behavior, since that will exactly correspond to the usual (simple) power iteration
gtsam/sfm/ShonanAveraging.cpp
Outdated
// 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. | ||
Vector perturb(const Vector &initialVector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, if it's a local function this can just be made static in this file.
Hi @jingwuOUO, the only changes I see that remain to be done here are:
|
@jingwuOUO pls fix last two issues so we can finally merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, looks good to me 😎
No description provided.