-
Notifications
You must be signed in to change notification settings - Fork 105
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
ENH: add decorator to fix adjoint weightings #1177
base: master
Are you sure you want to change the base?
Conversation
In my opinion, adding a utility is the only option that is not outrageously surprising to users. This could be, as you suggest, in the form of a decorator, but I'd simply make it a free function that is either called on the adjoint: @property
def adjoint(self):
...
return auto_weighting(adjoint) or in the operator itself class AdjointyOperator:
def __call__(self, x):
return auto_weighting(adjoint) |
I think the same. Only for the sake of not introducing bias in the discussion I left it kind of open. Note that the option of decorating the adjoint, which is equivalent to your first code box, would still require some monkey-patching. However, in this case I would find it more acceptable since it happens "almost" during object creation. The second option is cleaner in that respect, but has the disadvantage that it would make it harder to use existing (unweighted) operators off the shelf. Things like So I'm leaning towards the operator decoration option. And BTW, this has interesting implications for I'll think a bit more about this. |
roughly :-) |
9dbb725
to
2a92199
Compare
Here's a more serious suggestion. Obviously needs more tests, but does the job. |
Also obviously, the implementation does not make use of the |
e25fa7d
to
c211b66
Compare
Checking updated PR...
Comment last updated on February 15, 2018 at 22:06 Hours UTC |
64e16b2
to
85c64fb
Compare
So this is now a serious PR. I've added tests and an example, and I think this concept would actually work. Comments welcome. |
Once we settle on a solution, I think this PR should also go through the "standard library" and fix all instances of operators that need fixing. I'll make a TODO list at the top. |
Open question: what to do with spaces that have no |
Ping @matthiasje this may be interesting to you, too. You also had trouble with unscaled adjoints, right? |
Yes, in the most elegant definition of TGV I would like to use weighted spaces for the second derivative. It then needs proper adjoints and prox operators. This should be used in |
Sure, you can do
|
@kohr-h, I tried the TGV example again with weighted spaces. It does not work as weighting is not implemented for product spaces? |
What happens if you remove that check and add the |
I tried the following but it did not help significantly. It does then result in other errors about things not being well-defined (see below).
|
Maybe it would be the best if you have a look yourself. I tried to run
|
Without actually looking, I believe this has to do with #1062 in some sense. What happens here is that the code tries to multiply the result, a product space element, with a Numpy array that has the weights, one for each product space component. That doesn't work, unfortunately, which is part of a bigger issue.
>>> pspace = odl.rn(2) * odl.rn(3)
>>> z = pspace.one()
>>> z * [1, 2] # should work, doesn't currently
ProductSpace(rn(2), rn(3)).element([
[1.0, 1.0],
[2.0, 2.0, 2.0]
])
>>> z * np.array([1, 2]) # same as above
ProductSpace(rn(2), rn(3)).element([
[1.0, 1.0],
[2.0, 2.0, 2.0]
])
>>> pspace = odl.rn(2) ** 2
>>> z = pspace.one()
>>> z * np.array([1, 2])[None, :] # broadcast along pspace axis, now produces Numpy array
ProductSpace(rn(2), rn(3)).element([
[1.0, 2.0],
[1.0, 2.0]
])
>>> pspace = odl.rn(2) ** 2
>>> z = pspace.one()
>>> z * [1, 2] # along axis 0 or axis 1??
|
In view of the current issue with product spaces, I think I'll implement a workaround for that case to move things forward. |
85c64fb
to
e7c7da4
Compare
Will do that now. |
Now the decorator works also for operators on product spaces. Needs a proper review now. @mehrhardt if you like, you can test your code again. Anyway, I think this is a rather important issue since many adjoints are wrong as of now, we should give it some priority. |
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.
Some overall questions:
Does this commute with @property
?
How does this handle adjoints that are defined in terms of other operators?
Shouldn't this be a operator util instead of a space util?
examples/operator/auto_weighting.py
Outdated
@@ -0,0 +1,58 @@ | |||
"""Example demonstrating the usage of the ``auto_weighting`` decorator.""" |
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 needs to be somewhat fleshed out, especially given that new users come 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.
Can do
odl/space/space_utils.py
Outdated
@@ -281,6 +284,303 @@ def rn(shape, dtype=None, impl='numpy', **kwargs): | |||
return rn | |||
|
|||
|
|||
class auto_weighting(OptionalArgDecorator): |
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.
Suggest rename to auto_adjoint_weighting
to make this more clear
odl/space/space_utils.py
Outdated
``M: R^n -> R^m``, and its unweighted adjoint is defined by the | ||
transposed matrix ``M^T: R^m -> R^n``. | ||
|
||
Similarly, when an integration operator ``A: L^2(I) -> R`` on an |
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.
Wouldnt always using the R^n -> R style be better?
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.
What do you mean by "using"? In this example?
odl/space/space_utils.py
Outdated
|
||
Parameters | ||
---------- | ||
unweighted_adjoint : `Operator` |
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.
Shouldn't the type of this be "method" or something like that?
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.
Technically yes. I'll change it.
odl/space/space_utils.py
Outdated
elif dom_w_type == 'const' and ran_w_type == 'const': | ||
if unweighted_adjoint.domain.size < unweighted_adjoint.range.size: | ||
new_dom_w = 1.0 | ||
new_ran_w = ran_w / dom_w |
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.
Are we sure this always makes sense? What about operators C -> R
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.
Yes, because weightings can never be complex. (Note to self: make sure that this is checked.)
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.
Some further reviewing and a question.
Could this somehow become buggy if adjoint
is an attribute instead of a method?
odl/test/space/space_utils_test.py
Outdated
return ScalingOp(self.range, self.domain, self.c) | ||
|
||
op1 = ScalingOp(rn, rn, 1.5) | ||
op2 = ScalingOp(rn_w, rn_w, 1.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.
Prefer not using 1.5 everywhere
odl/test/space/space_utils_test.py
Outdated
def test_auto_weighting_cached_adjoint(): | ||
"""Check if auto_weighting plays well with adjoint caching.""" | ||
rn = odl.rn(2) | ||
rn_w = odl.rn(2, weighting=2) |
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.
Use a "random" value here
odl/test/space/space_utils_test.py
Outdated
def __init__(self, dom, ran, c): | ||
super(InvalidScalingOp, self).__init__(dom, ran, linear=True) | ||
self.c = c | ||
self._adjoint = None |
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.
What does this line do?
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.
That must be copy-paste from the caching test, should be removed.
That cannot happen since >>> class A(odl.Operator):
... def __init__(self):
... super(A, self).__init__(odl.rn(2), odl.rn(2))
... self.adjoint = None
>>>
>>> a = A()
Traceback (most recent call last):
[...]
self.adjoint = None
AttributeError: can't set attribute |
No. I tried to hack it in, but the wrapping does not seem to work as expected. I'll leave it as it is, but I can improve the error message to give users a hint. |
Not sure what you mean. Like compositions and such? |
Yes I guess so. |
8a39fc6
to
77e1fba
Compare
3fe7cec
to
962a8bc
Compare
f67f8cf
to
9a0a073
Compare
Current status here is that there's some mysterious issue when I'll think a bit about better ways to do this, without doing brain surgery on the operator returned by |
Update: The general logic is settled, details are in discussion.
This is a discussion PR.I think we could go somewhere along these lines to make adjoints "automatically correct" with respect to weighted spaces, in particular when the weights differ.This solution should always be correct where it applies, however it wouldn't yet help in special cases like
RayTransform
where the weights need to be computed per axis.The larger question is what to do instead of the monkey-patching here. I can imagine a bunch of scenarios:
Operator
. This means that users implement the unweighted adjoint, and the weighted one is implemented with a helper function similar to this.This would require us to communicate this "magic" clearly and prominently.
_auto_weighting
or so. Don't do anything by default.This option would still need good communication but leads to less unexpected behavior on the user side.
adjoint
, like@auto_weighting
that users can choose to use.This option has the least surprise and the simplest migration path, we would just have to use it a lot ourselves so users pick it up.
Opinions?
For the records: What I don't want is to let
adjoint
return an operator composition. That would be ugly and a usability nightmare, since operator properties will be buried deep down.TODO:
weighting
at all?We could treat no weighting as 1.0, but need to be careful with implementation since the current variant uses in-place arithmetic. It should be fine, though, since the weighting factors that are 1.0 are just skipped.
Operator.norm
method (should be way simpler though)Addnot in this PRdomain
orrange
where missingOperatorComp
to avoid dumb "multiplications by 1"adjoint
uses another property, e.g.inverse
, needs investigationrepr
changes that are now in DONTMERGE: improve repr by a lot #1276