Skip to content
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

Merged
merged 22 commits into from
Sep 17, 2020
Merged

Conversation

josh146
Copy link
Member

@josh146 josh146 commented Sep 16, 2020

Context: Adds an autograd interface to the quantum tape.

Description of the Change:

The quantum tape can now be interfaced with autograd as follows:

import pennylane as qml
from pennylane import numpy as np

from pennylane.beta.tapes import QuantumTape#, qnode
from pennylane.beta.queuing import expval, var, sample, probs, MeasurementProcess
from pennylane.beta.interfaces.autograd import AutogradInterface

dev = qml.device("default.qubit", wires=2)

tape = AutogradInterface.apply(QuantumTape())

with tape:
    qml.Rot(0, 0, 0, wires=0)
    expval(qml.PauliX(0))

x = np.array(0.1, requires_grad=False)
y = np.array(0.2, requires_grad=True)
z = np.array(0.3, requires_grad=True)
>>> tape.execute(dev, [x, y, z])
[0.18979606]
>>> grad_fn = qml.grad(lambda params: tape.execute(dev, params))
>>> grad_fn([x, y, z])
[array(-1.11022302e-09), array(0.93629335), array(-0.05871081)]

We can wrap the tape execution in a cost function if we want to perform classical pre-processing on the tape parameters:

def cost_fn(x, y, z):
    tape.set_parameters([x, y ** 2, y * np.sin(z)], trainable_only=False)
    return tape.execute(dev)
>>> 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 parameter x is automatically skipped.

Benefits: n/a

Possible Drawbacks: n/a

Related GitHub Issues: n/a

@codecov
Copy link

codecov bot commented Sep 16, 2020

Codecov Report

Merging #803 into master will decrease coverage by 0.11%.
The diff coverage is 96.29%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
pennylane/__init__.py 62.65% <0.00%> (-1.55%) ⬇️
pennylane/beta/interfaces/autograd.py 100.00% <100.00%> (ø)
pennylane/interfaces/autograd.py 71.05% <0.00%> (-28.95%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 843ece7...c51b4e4. Read the comment docs.

@josh146 josh146 added the review-ready 👌 PRs which are ready for review by someone from the core team. label Sep 16, 2020
@josh146 josh146 requested a review from thisac September 16, 2020 15:00
Copy link
Member

@co9olguy co9olguy left a 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!

pennylane/beta/interfaces/autograd.py Outdated Show resolved Hide resolved
return "autograd"

def _update_trainable_params(self):
params = []
Copy link
Member

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)

Copy link
Member Author

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 differentiable
  • get_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 😆

Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than renaming the method (which is shared across all interfaces), I both (a) updated the docstring, and (b) changed it to a private attribute:

image

pennylane/beta/interfaces/autograd.py Show resolved Hide resolved
pennylane/beta/interfaces/autograd.py Outdated Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Outdated Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Outdated Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Outdated Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Show resolved Hide resolved
Copy link
Contributor

@thisac thisac left a 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):
Copy link
Contributor

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?

Copy link
Member Author

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.

pennylane/beta/interfaces/autograd.py Outdated Show resolved Hide resolved
pennylane/beta/interfaces/autograd.py Outdated Show resolved Hide resolved
Comment on lines +114 to +115
if res.dtype == np.dtype("object"):
return np.hstack(res)
Copy link
Contributor

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?

Copy link
Member Author

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:

image

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.

pennylane/beta/interfaces/autograd.py Show resolved Hide resolved
Comment on lines +157 to +161
tape_class = getattr(tape, "__bare__", tape.__class__)
tape.__bare__ = tape_class
tape.__class__ = type("AutogradQuantumTape", (cls, tape_class), {})
tape._update_trainable_params()
return tape
Copy link
Contributor

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)? 🤔

Copy link
Member Author

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.)

tests/beta/interfaces/test_tape_autograd.py Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Outdated Show resolved Hide resolved
tests/beta/interfaces/test_tape_autograd.py Show resolved Hide resolved
Base automatically changed from tape-pr-3 to master September 17, 2020 04:38
@josh146 josh146 merged commit d8fb17b into master Sep 17, 2020
@josh146 josh146 deleted the tape-pr-4a branch September 17, 2020 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review-ready 👌 PRs which are ready for review by someone from the core team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants