-
Notifications
You must be signed in to change notification settings - Fork 575
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
Allow Observables to return objects and still be differentiable #1291
Allow Observables to return objects and still be differentiable #1291
Conversation
@cvjjm this is great! I am in favour of this solution. The current code is written deliberately using duck typing, with the blocker here solely to distinguish between standard NumPy arrays and ragged arrays -- I'm not sure it makes sense to force objects to overwrite My only concern is more one of maintenance. Best to leave a detailed comment here to make sure that future development adheres to this duck typing, and doesn't inadvertently introduce any code that limits the returned object type. |
Very much in favour of having a user's custom operations remain differentiable |
Great! I added some comments. Let me know if you think this is sufficient and please approve the running of tests. |
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.
Thanks @cvjjm! This is a nice small modification that opens up a very nice feature. Before this can be merged in, we'll need to:
- Work out why the tests are failing
- Add a new test to ensure the new behaviour works (and catch it from breaking in the future).
if hasattr(g, "flatten"): | ||
# flatten only if g supports flattening to allow for | ||
# objects other than numpy ndarrays | ||
return g.flatten() | ||
return g |
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.
Not sure if this is too obfuscating, but we could avoid the if statement by doing this?
getattr(g, "flatten", lambda: g)()
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.
Too obfuscating for my taste :-)
pennylane/tape/jacobian_tape.py
Outdated
dtype = float if isinstance(g, float) else np.object | ||
jac = np.zeros((self._output_dim, len(params)), dtype=dtype) |
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.
@cvjjm I noticed that quite a few of the tests are failing, specifically the np.allclose
calls within the tests. I'm not 100% sure why, but I think the issue might be the logic here?
Should it instead be tied to the existence of a __len__
attribute?
dtype = float if hasattr(g, "__len__") else np.object
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 wasn't sure what I need to put here to preserve the default behavior. I will look into this.
Co-authored-by: Josh Izaac <josh146@gmail.com>
For me all the existing tests pass now and I have added a test of the newly supported behavior. One last thing I am not totally happy with is that if I declare my QNode to accept an array as parameter like this: # force diff_method='parameter-shift' because otherwise
# PennyLane swaps out dev for default.qubit.autograd
@qml.qnode(dev, diff_method='parameter-shift')
def qnode(x):
qml.RY(x[0], wires=0)
qml.RY(x[1], wires=0)
return qml.expval(SpecialObservable(wires=0))
@qml.qnode(dev, diff_method='parameter-shift')
def reference_qnode(x):
qml.RY(x[0], wires=0)
qml.RY(x[1], wires=0)
return qml.expval(qml.PauliZ(wires=0))
params = np.array([0.2, 0.1])
assert np.isclose(qnode(params).item().val, reference_qnode(params))
assert np.isclose(qml.jacobian(qnode)(params).item().val, qml.jacobian(reference_qnode)(params)) Differentiation does no longer work because of the following error:
I suspect there must be a way to teach autograd to construct proper VSpaces for my SpecialObject which would fix this, but I am unsure how. This however is not a problem of PennyLane. Just in case you have an idea how also this could be made to work it would be very nice if you told me how. |
Thanks for the update @cvjjm!
Unfortunately, while I come across this error many times, I am not 100% sure how to validate new type mappings for Autograd. I can show you how I managed to add the from autograd.tracer import Box
from autograd.numpy.numpy_boxes import ArrayBox
from autograd.numpy.numpy_vspaces import ComplexArrayVSpace, ArrayVSpace
from autograd.core import VSpace
def tensor_to_arraybox(x, *args):
"""Convert a :class:`~.tensor` to an Autograd ``ArrayBox``.
Args:
x (array_like): Any data structure in any form that can be converted to
an array. This includes lists, lists of tuples, tuples, tuples of tuples,
tuples of lists and ndarrays.
Returns:
autograd.numpy.numpy_boxes.ArrayBox: Autograd ArrayBox instance of the array
"""
if isinstance(x, tensor):
if x.requires_grad:
return ArrayBox(x, *args)
raise NonDifferentiableError(
"{} is non-differentiable. Set the requires_grad attribute to True.".format(x)
)
return ArrayBox(x, *args)
Box.type_mappings[tensor] = tensor_to_arraybox
VSpace.mappings[tensor] = lambda x: ComplexArrayVSpace(x) if onp.iscomplexobj(x) else ArrayVSpace(x) |
Codecov Report
@@ Coverage Diff @@
## master #1291 +/- ##
=======================================
Coverage 98.27% 98.27%
=======================================
Files 150 150
Lines 11256 11262 +6
=======================================
+ Hits 11062 11068 +6
Misses 194 194
Continue to review full report at Codecov.
|
Great. I didn't know about the |
Ups. Forgot the -l 100 flag when calling black... Now all should be well. |
# force diff_method='parameter-shift' because otherwise | ||
# PennyLane swaps out dev for default.qubit.autograd | ||
@qml.qnode(dev, diff_method="parameter-shift") |
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.
Hopefully this will be fixed soon via #1334!
return qml.expval(qml.PauliZ(wires=0)) | ||
|
||
assert np.isclose(qnode(0.2).item().val, reference_qnode(0.2)) | ||
assert np.isclose(qml.jacobian(qnode)(0.2).item().val, qml.jacobian(reference_qnode)(0.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.
A very thorough text example! This test will help ensure this (originally accidental) functionality remains working.
@cvjjm, before merging, could I get you to:
Thanks 🙂 |
Before making this into a proper RP I wanted to quickly get your opinion whether this change has a chance of making it into upstream.
Background: I realized that the only thing that is preventing users from constructing custom
Observables
that return an object (in my case anOpenFermion.QubitOperator
is super handy) is a very small portion of code injacobian_tape.py
that makes assumptions about return types having certain properties.With the rather minimal change below, I can have a device support a custom
Observable
that returns anobject
and and for which it supports the device gradient and use that inreturn qml.expval(CustomObservable)
of aQNode
and even differentiated thatQNode
withqml.grad()
.An alternative would be to implement
.dtype
,__len__()
, and.flatten()
in the class of the returned object and only change line 585 to take thedtype
from g rather than a hardcodedfloat
. Would that be preferred?