-
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
[TAPE] Adds an autograd interface to the quantum tape #803
Conversation
Codecov Report
@@ Coverage Diff @@
## master #803 +/- ##
==========================================
- Coverage 92.42% 92.30% -0.12%
==========================================
Files 121 122 +1
Lines 7892 7944 +52
==========================================
+ Hits 7294 7333 +39
- Misses 598 611 +13
Continue to review full report at Codecov.
|
Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com>
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.
Awesome work @josh146, I'm excited for all these new features to be available once the tape refactor is done!
return "autograd" | ||
|
||
def _update_trainable_params(self): | ||
params = [] |
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 function could use a docstring. I got confused as to its intention, since it both updates a class attribute, but also seems to return important data (which you might consider two distinct actions)
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.
Yep, I agree. The reason for the bad practice in this method is optimization.
To get autograd to work, it needs to update the trainable parameters when it accesses the tapes parameters (on call to get_parameters()
).
So we have:
_update_trainable_params()
extracts the tapes parameters, to determine which ones are differentiableget_parameters()
calls_update_trainable_params()
, and then proceeds to iterate through and extract the trainable parameters again.
So you can remove the additional redundant iteration by simply having the data already extract by _update_trainable_params()
provided to get_parameters()
.
I don't really like this either, and I tried to improve on it (for example, by changing the name and docstring of _update_trainable_params
to reflect this behaviour). But, Torch and TensorFlow don't work like this/want this behaviour, so they would then fail. Further, the base QuantumTape
doesn't need this behaviour either, so it now had a redundant iteration!
I couldn't really think of a better compromise. Perhaps another approach could be to have AutogradInterface._update_trainable_params()
save a private attribute that only Autograd.get_parameters()
accesses? This avoids the unexpected return, but trades it off for a hidden side effect. And all in the name of avoiding a redundant for loop 😆
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.
perhaps it's just a naming thing. If you called it function_which_does_x_and_y
I wouldn't be surprised when both x and y happen. Docstring would help as well
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.
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.
Looks great @josh146. Just had a small comment, and a few questions (mostly for my understanding). 🚀 🌔
from pennylane.beta.queuing import AnnotatedQueue | ||
|
||
|
||
class AutogradInterface(AnnotatedQueue): |
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.
Just to understand this. The user would never really create an AutogradInterface
by itself; but rather either create a subclass inheriting from both AutogradInterface
and QuantumTape
or use the AutogradInterface.apply
method to apply the AutogradInterface
to an existing QuantumTape
. Is this correct?
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.
Yep! Exactly.
This is because the interface might be applied to a subclass of the quantum tape --- we don't know what the inheritance structure of the class we're wrapping looks like, so easier to make the interface independent of the hierarchy.
if res.dtype == np.dtype("object"): | ||
return np.hstack(res) |
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 would this "object"
be? Is it for the case when the return type is an ArrayBox
?
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 NumPy object array simply refers to a NumPy array that stores a non-NumPy datatype :) In this case, it would likely be a list:
Any 'array' that is ragged will automatically be cast to an object array, since ragged arrays are not natively supported by NumPy. So you will still be able to use most NumPy functions on it, but NumPy will simply be storing it in memory as a Python nested list, rather than a more memory efficient fortran memory view.
tape_class = getattr(tape, "__bare__", tape.__class__) | ||
tape.__bare__ = tape_class | ||
tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {}) | ||
tape._update_trainable_params() | ||
return tape |
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.
At a glance it seems like this only updates the type name of the QuantumTape
to AutogradQuantumTape
and updates the trainable parameters, which would mean that the returned tape
would still use the functions from the QuantumTape
class. This seems strange though. 💭
Does the tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {})
line actually port over the QuantumTape
class (tape) to an AutogradInterface
class (cls) (i.e. overwrites all the methods in tape
with the ones from cls
)? 🤔
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.
Yep! One of the tricks of Python is it allows you to change the class of an object after instantiation (tape.__class__ =
), as long as the new class is compatible (it has the same __slots__
, method resolution order, etc.).
On the right hand side, we are simply using type
to generate a new class on-the-fly. It is equivalent to doing this:
class AutogradQuantumTape(AutogradInterface, tape.__class__):
pass
tape.__class__ = AutogradQuantumTape
we just save some space/indentation by using type
🙂
(Note: here, the AutogradInterface
class is a mixin class. The order of the inheritance is important; we want (AutogradInterface, tape.__class__)
. If we instead had (tape.__class__, AutogradInterface)
, Python would deal with methods with the same name by preferencing the one in the first class in the tuple.)
Co-authored-by: Nathan Killoran <co9olguy@users.noreply.github.com> Co-authored-by: Theodor <theodor@xanadu.ai>
Context: Adds an autograd interface to the quantum tape.
Description of the Change:
The quantum tape can now be interfaced with autograd as follows:
We can wrap the tape execution in a cost function if we want to perform classical pre-processing on the tape parameters:
>>> cost_fn(x, y, z) [0.03991951] >>> jac_fn = qml.jacobian(cost_fn) >>> jac_fn(x, y, z) [[ 0.39828408 -0.00045133]]
Note that the Jacobian has shape
(tape.output_dim, len(tape.trainable_params))
; the gradient of the non-differentiable parameterx
is automatically skipped.Benefits: n/a
Possible Drawbacks: n/a
Related GitHub Issues: n/a