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

Add adjoint differentiation method #1032

Merged
merged 44 commits into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9fc4301
Move rewind diff method to device
josh146 Jan 24, 2021
994a732
Move rewind diff method to device
josh146 Jan 24, 2021
f402118
more
josh146 Jan 24, 2021
a426cca
Fix import
trbromley Jan 24, 2021
0c62766
Merge branch 'master' into rewind-on-device
trbromley Jan 25, 2021
ccd080b
Apply suggestions
trbromley Jan 25, 2021
d823c52
Add to changelog
trbromley Jan 25, 2021
8cccca8
Rename rewind to adjoint
trbromley Jan 25, 2021
89a78fa
Add tests
trbromley Jan 25, 2021
5b2e094
Add tests
trbromley Jan 25, 2021
902514f
Add tests
trbromley Jan 25, 2021
1ee5eeb
Remove spacing
trbromley Jan 25, 2021
fe35a8b
Add to tests
trbromley Jan 25, 2021
91c75ad
Add skips
trbromley Jan 25, 2021
ef67430
Fix CI
trbromley Jan 25, 2021
9ad2026
Fix CI
trbromley Jan 25, 2021
8982090
Add docstring
trbromley Jan 25, 2021
55afc93
Respond to comments
trbromley Jan 25, 2021
2f728bf
Fix test
trbromley Jan 25, 2021
a66ee08
Add docstring
trbromley Jan 25, 2021
ad2728b
Change order in docstring
trbromley Jan 25, 2021
1e6033c
Update sum
trbromley Jan 25, 2021
2073a85
Tidy
trbromley Jan 25, 2021
dfb6afa
Update pennylane/_qubit_device.py
trbromley Jan 25, 2021
e0a9d85
Move operation_derivative
trbromley Jan 25, 2021
6c0daa6
Apply black
trbromley Jan 25, 2021
e975cef
Tidy imports
trbromley Jan 25, 2021
21ba5c7
Reword
trbromley Jan 25, 2021
4cf10fc
Move position
trbromley Jan 25, 2021
523997e
Update docstring:
trbromley Jan 25, 2021
ad042a1
Apply suggestions from code review
trbromley Jan 26, 2021
ce7865f
Add test for coverage
trbromley Jan 26, 2021
d1e24b5
Merge branch 'master' into rewind-on-device
josh146 Jan 26, 2021
ff5123c
Merge branch 'master' into rewind-on-device
trbromley Jan 26, 2021
d098434
Add note
trbromley Jan 26, 2021
8235ca6
Update import
trbromley Jan 26, 2021
46daf2a
Update pennylane/_qubit_device.py
trbromley Jan 26, 2021
0501bf2
Move tests
trbromley Jan 26, 2021
561b473
Merge branch 'rewind-on-device' of github.com:XanaduAI/pennylane into…
trbromley Jan 26, 2021
6f9d1d8
tidy
trbromley Jan 26, 2021
83c47ae
Update
trbromley Jan 26, 2021
c431279
Fix test
trbromley Jan 26, 2021
f5f20b7
Update docstring
trbromley Jan 26, 2021
e585da5
Update docstring
trbromley Jan 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

<h3>New features since last release</h3>

* A new differentiation method has been added for use with simulators in tape mode. The `"adjoint"`
method operates after a forward pass by iteratively applying inverse gates to scan backwards
through the circuit. This method is similar to the reversible method, but has a lower time
overhead and a similar memory overhead. It follows the approach provided by
[Jones and Gacon](https://arxiv.org/abs/2009.02823). This method is only compatible with certain
statevector-based devices such as `default.qubit`.
trbromley marked this conversation as resolved.
Show resolved Hide resolved

Example use:

```python
import pennylane as qml

qml.enable_tape()
trbromley marked this conversation as resolved.
Show resolved Hide resolved

wires = 1
device = qml.device("default.qubit", wires=wires)

@qml.qnode(device, diff_method="adjoint")
def f(params):
qml.RX(0.1, wires=0)
qml.Rot(*params, wires=0)
qml.RX(-0.3, wires=0)
return qml.expval(qml.PauliZ(0))

params = [0.1, 0.2, 0.3]
qml.grad(f)(params)
```

* Added `qml.math.squeeze`.
[(#1011)](https://github.com/PennyLaneAI/pennylane/pull/1011)

Expand Down
104 changes: 103 additions & 1 deletion pennylane/_qubit_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@

import numpy as np

from pennylane.operation import Sample, Variance, Expectation, Probability, State
import pennylane as qml
from pennylane.operation import (
Sample,
Variance,
Expectation,
Probability,
State,
operation_derivative,
)
from pennylane.qnodes import QuantumFunctionError
from pennylane import Device
from pennylane.math import sum
trbromley marked this conversation as resolved.
Show resolved Hide resolved
from pennylane.wires import Wires


Expand Down Expand Up @@ -688,3 +697,96 @@ def sample(self, observable):
unraveled_indices = [2] * len(device_wires)
indices = np.ravel_multi_index(samples.T, unraveled_indices)
return observable.eigvals[indices]

def adjoint_jacobian(self, tape):
"""Implements the adjoint method outlined in
`Jones and Gacon <https://arxiv.org/abs/2009.02823>`__ to differentiate an input tape.

After a forward pass, the circuit is reversed by iteratively applying inverse (adjoint)
gates to scan backwards through the circuit. This method is similar to the reversible
method, but has a lower time overhead and a similar memory overhead.

.. note::
The adjoint differentation method has the following restrictions:

* As it requires knowledge of the statevector, only statevector simulator devices can be
used.

* Only expectation values are supported as measurements.

Args:
tape (.QuantumTape): circuit that the function takes the gradient of

Returns:
array: the derivative of the tape with respect to trainable parameters.
Dimensions are ``(len(observables), len(trainable_params))``.

Raises:
QuantumFunctionError: if the input tape has measurements that are not expectation values
or contains a multi-parameter operation aside from :class:`~.Rot`
"""

for m in tape.measurements:
if m.return_type is not qml.operation.Expectation:
raise qml.QuantumFunctionError(
"Adjoint differentiation method does not support"
f" measurement {m.return_type.value}"
)

if not hasattr(m.obs, "base_name"):
m.obs.base_name = None # This is needed for when the observable is a tensor product
josh146 marked this conversation as resolved.
Show resolved Hide resolved

# Perform the forward pass
self.reset()

# Consider using caching and calling lower-level functionality. We just need the state
# without postprocessing https://github.com/PennyLaneAI/pennylane/pull/1032/files#r563441040
self.execute(tape)
trbromley marked this conversation as resolved.
Show resolved Hide resolved

phi = self._reshape(self.state, [2] * self.num_wires)

lambdas = [self._apply_operation(phi, obs) for obs in tape.observables]
Copy link
Member

Choose a reason for hiding this comment

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

Just double checking, but we can't do the following:

new_tape = some_func(old_tape)
self.execute(new_tape)

because here we are applying hermitian matrices to the statevector, which the tape/device won't understand how to do without going low level?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new_tape for a specific obs would look like:

with qml.QuantumTape() as new_tape:
    qml.QubitStateVector(phi, wires=range(wires))
    obs  # actually not sure how this line would look
    qml.state()

I don't see a problem if it's PauliX but for an arbitrary Hermitian this wouldn't (?) work. Maybe we could hack it in to work though, but we'd lose the state being normalized.

I also feel like the "many-tapes" approach might be inefficient, e.g., to make lots of tapes and then do device execution (which includes lots of postprocessing), when we just need to evolve the state by one gate.


expanded_ops = []
for op in reversed(tape.operations):
if op.num_params > 1:
if isinstance(op, qml.Rot) and not op.inverse:
trbromley marked this conversation as resolved.
Show resolved Hide resolved
ops = op.decomposition(*op.parameters, wires=op.wires)
expanded_ops.extend(reversed(ops))
else:
raise QuantumFunctionError(
f"The {op.name} operation is not supported using "
'the "adjoint" differentiation method'
)
else:
if op.name not in ("QubitStateVector", "BasisState"):
expanded_ops.append(op)

jac = np.zeros((len(tape.observables), len(tape.trainable_params)))
dot_product_real = lambda a, b: self._real(sum(self._conj(a) * b))
trbromley marked this conversation as resolved.
Show resolved Hide resolved

param_number = len(tape._par_info) - 1 # pylint: disable=protected-access
trainable_param_number = len(tape.trainable_params) - 1
for op in expanded_ops:

if (op.grad_method is not None) and (param_number in tape.trainable_params):
d_op_matrix = operation_derivative(op)

op.inv()
trbromley marked this conversation as resolved.
Show resolved Hide resolved
phi = self._apply_operation(phi, op)

if op.grad_method is not None:
if param_number in tape.trainable_params:
mu = self._apply_unitary(phi, d_op_matrix, op.wires)

jac_column = np.array(
[2 * dot_product_real(lambda_, mu) for lambda_ in lambdas]
)
jac[:, trainable_param_number] = jac_column
trainable_param_number -= 1
param_number -= 1

lambdas = [self._apply_operation(lambda_, op) for lambda_ in lambdas]
op.inv()

return jac
42 changes: 42 additions & 0 deletions pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,3 +1727,45 @@ def heisenberg_obs(self, wires):
p = self.parameters
U = self._heisenberg_rep(p) # pylint: disable=assignment-from-none
return self.heisenberg_expand(U, wires)


def operation_derivative(operation) -> np.ndarray:
trbromley marked this conversation as resolved.
Show resolved Hide resolved
r"""Calculate the derivative of an operation.

For an operation :math:`e^{i \hat{H} \phi t}`, this function returns the matrix representation
in the standard basis of its derivative with respect to :math:`t`, i.e.,

.. math:: \frac{d \, e^{i \hat{H} \phi t}}{dt} = i \phi \hat{H} e^{i \hat{H} \phi t},

where :math:`\phi` is a real constant.

Args:
operation (qml.Operation): The operation to be differentiated.
trbromley marked this conversation as resolved.
Show resolved Hide resolved

Returns:
array: the derivative of the operation as a matrix in the standard basis

Raises:
ValueError: if the operation does not have a generator or is not composed of a single
trainable parameter
"""
generator, prefactor = operation.generator

if generator is None:
raise ValueError(f"Operation {operation.name} does not have a generator")
if operation.num_params != 1:
# Note, this case should already be caught by the previous raise since we haven't worked out
# how to have an operator for multiple parameters. It is added here in case of a future
# change
trbromley marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
f"Operation {operation.name} is not written in terms of a single parameter"
)

if not isinstance(generator, np.ndarray):
generator = generator.matrix

if operation.inverse:
prefactor *= -1
generator = generator.conj().T

return 1j * prefactor * generator @ operation.matrix
Loading