-
Notifications
You must be signed in to change notification settings - Fork 18
feat: Add Dykstra's algorithms and tests. #229
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
Conversation
- DykstrasProjection - (Parallel) Dykstra's projection algorithm computing convex projection to the intersection of convex sets - DykstrasProjectionProx - The corresponding indicator function (prox) - DykstraLikeProximal - (Parallel) Dykstra-like proximal algorithm computing prox of the sum of convex functions Add tests in a separate test file. - test_dykstra.py
- minor fix in test_dykstras_projection() - change np to ncp with get_array_module() - improve docstring and comments in DykstraLikeProximal.py - refactoring DykstraLikeProximal.__call()__ for readability - refactoring DykstraLikeProximal._parallel_dykstra_like_proximal_algorithm() for reducing the cyclomatic complexity to pass the CI during PR
- to pass the CI during PR
|
@tttamaki this is awesome! Give me a week or so to review them 😄 in the meantime, give that this is a completely new feature, would you mind adding an example or tutorial (where you see more fit - our rule of thumb is, if the script just applies the operator to some dummy data - a bit like a test - it’s more an example, if the script takes an interesting problem and solves it, then it’s a tutorial) so users will be able to see how to use the operators in action |
|
Ok I spoke too fast without reading the entire message… your gist would be a perfect example 😀 |
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.
@tttamaki I started reviewing your PR and I think these Dykstra algorithms look like a great addition to pyproximal 😄
Regarding the Github issue you mention, I wouldn't worry about too much as, like you say, it is 2 years old so I doubt anything will ever happen there (once we merge this PR I may close it).
To the code: I think in general it is already very well written, I let some comments here and there (mostly typos and unclear sentences in docstrings). However, the more I look at it, the more I think some changes may be needed to make sure that this feels more like a natural addition to PyProximal that follows the various standards that we established with our operators and solvers. In general, I feel you have kind of mixed up the purspose of these operators (which to me is Intersection of two or more projections or sum or two or more proximable function) with the algorithm used to find a solution of the underlying optimization problems. I would prefer we call those operators something like Intersection for the projections/indicator functions and maybe Sum for the generalization to any proximal function. This way the operator would give a clear idea to what they are meant to do in terms of defining an optimization problem (and its objective function) and the fact that the Dykstra algorithm is used to solve such problems is to me an algorithmic choice that we adopt but should not be exposed so explicitly to the user.
So in other terms I would like to see someone do:
Op =
l2 = L2(Op, d)
circle = EuclideanBallProj(np.array([0.0, 3.5]), 5)
box = BoxProj(np.array([-5.0, -2.5]), np.array([5.0, 2.5]))
constr = Intersection([circle, box])
ximv = ADMM(l2, constr, ...)
more than constr = DykstrasProjectionProx([circle, box])
Another thing that I am not so convinced is why we need DykstraLikeProximal and DykstrasProjectionProx. Unless I do not understand something, _dykstra_projection in DykstrasProjectionProx is nearly identical to _dykstra_like_proximal_algorithm in DykstraLikeProximal with the only change that instead of calling self.projections[0] we call self.ops[0].prox.... could it not be possible to create a single solver (this could be in a private file called _dykstra or something) and have a flag that says if we pass proximals or projections (or even discover it from the class type as all prox operators are subclassed of ProxOperator), based on which we can do something likes this (this is just a pseudocode to give an idea):
if prox:
self.proj_or_prox = projections
else:
self.proj_or_prox = [partial(projection.prox, tau) for projection in projections]
Finally, one last point that we could test later, once the code is in place: what about if we add __add__ to ProxOperator to allow one doing something like this:
circle = EuclideanBall(np.array([0.0, 3.5]), 5)
circle1 = EuclideanBall(np.array([2.0, 3.5]), 3)
box = Box(np.array([-5.0, -2.5]), np.array([5.0, 2.5]))
constr = circle + circle1 + box
and internally the __add__ method will just form and return a DykstraLikeProximal/Sum operator.
What do you think?
|
Thank you for your review, I’m afraid that it might have taken a lot of time.
|
|
@tttamaki oh yeah I forgot about Intersection already existed. Well it is an intersection but a special one as it is only for a given type of convex set. It comes form Appendix A in https://cvg.cit.tum.de/_media/spezial/bib/chambolle_et_al_siims12.pdf and I had implemented when I was working on a segmentation project as it was suggested to be a good constraint for segmentation. It runs the Djkstra algorithm under the hood but I think it’s a bit of a special version of it tailored to the specific problem - the paper explains this in more details if interested. So, based on the above, and the fact we can’t really change the name of the current operator called Intersection, what about calling your one GenericIntersection, where Generic is used to indicate that it can work with any convex set whose projection is implemented in one of our Projection operator? To the second point about why both ‘need DykstraLikeProximal and DykstrasProjectionProx exist’ (note that what I asked doesn’t match with what you replied)… yes I am keen to keep consistency so to still have a projection operator and then a proximal for its indicator function that just calls the projection but what I’m not sure is about DUPLICATING the actual implementation of the Djkstra algorithm. So in other words I am fine to have GenericIntersection and Sum in the proximal submodule but internally they should both call the same algorithms (as I said about, it seem to me their algorithms are identical other than one is calling the call method of a projection and the other the prox method of a proximal). The actual Djkstra algorithm could but put into a _Djkstra file in optimization if we don’t want to expose it directly or in primal if we think it may be also worth to allow one to just run the algorithm passing some projection/proximal ops, and then internally we call this solver… Ok, good to hear that I understood correctly. For m>=2 the algorithms are slightly different. Fine, if this is what is in the literature we can definitely do that, but I don’t see an issue as we can always implement all variations in _djkstra and the call the appropriate one in the different operators. For the add, I see your point and I had initially the same thought. But, although I will need to play a bit with it to see if I’m right, my idea was that in the add we can easily check if one of the operators is already of type sum, extract its terms from self.ops and re-initialize a new Sum with all previous ops plus the new one… so ultimately what is returned is a single Sum and the prox operator will run a single Djkstra algorithm 😀 Hope this makes sense and you can start making the suggested changes? Ping me when you want me to do a second review 😉 |
|
@mrava87 Thank you for the clarification. I'll try to modify the PR. But the |
|
@tttamaki no worries, there is no rush 😀 for the add, feel free to have a go at it, but if you prefer I can do it once the rest of the PR is ready |
- rename classes - DykstrasProjectionProj --> GenericIntersectionProj - DykstrasProjectionProx --> GenericIntersectionProx - DykstraLikeProximal --> Sum - separate core algorithms to _dykstra_core.py to reduce duplication - docstring improved - add examples to dykstra.py
|
@mrava87 Thank you. I have finished to refine the PR except the add, so please check if I understand your suggestions correctly. |
|
@tttamaki this looks great! I just made a few minor changes here and there to be more consistent with the rest of the codebase (nothing in the code itself, mostly docstrings and examples). For the |
|
@tttamaki I had a look at wrapping |
I added the following features. The large part is in three files, and a separate test file.
DykstrasProjectioninprojection/DykstrasProjection.pyDykstrasProjectionProxinproximal/DykstraLikeProximal.pyDykstraLikeProximalinproximal/DykstrasProjectionProx.pypytests/test_dykstra.pyThese are a bit large files, so I would appreciate it if you have time to review the features.
Notes
self.use_original_tauas a hidden feature to align the code with the known equation in the literature, which doesn't pass the tests. I need more time to understand the proofs in the literature. Anyway, the modified implementation passes all tests and performs as expected. (I have only found one other library pylearn-parsimony which uses the known equation, but it lacks tests)