PyTorch interface

In order to use PennyLane in combination with PyTorch, we have to generate PyTorch-compatible quantum nodes. Such a QNode can be created explicitly using the interface='torch' keyword in the QNode decorator or QNode class constructor. Alternatively, an existing, basic QNode can be translated into a quantum node that interfaces with PyTorch by calling the QNode.to_torch() method.

Note

To use the PyTorch interface in PennyLane, you must first install PyTorch and import it together with PennyLane via:

import pennylane as qml
import torch

Using the PyTorch interface is easy in PennyLane — let’s consider a few ways it can be done.

Construction via keyword

The QNode decorator is the recommended way for creating QNode objects in PennyLane. The only change required to construct a PyTorch-capable QNode is to specify the interface='torch' keyword argument:

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

@qml.qnode(dev, interface='torch')
def circuit(phi, theta):
    qml.RX(phi[0], wires=0)
    qml.RY(phi[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.PhaseShift(theta, wires=0)
    return qml.expval(qml.PauliZ(0)), qml.expval(qml.Hadamard(1))

The QNode circuit() is now a PyTorch-capable QNode, accepting torch.tensor objects as input, and returning torch.tensor objects. Subclassing from torch.autograd.Function, it can now be used like any other PyTorch function:

>>> phi = torch.tensor([0.5, 0.1])
>>> theta = torch.tensor(0.2)
>>> circuit(phi, theta)
tensor([0.8776, 0.6880], dtype=torch.float64)

PyTorch-capable QNodes can also be created using the QNode class constructor:

dev1 = qml.device('default.qubit', wires=2)
dev2 = qml.device('default.mixed', wires=2)

def circuit1(phi, theta):
    qml.RX(phi[0], wires=0)
    qml.RY(phi[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.PhaseShift(theta, wires=0)
    return qml.expval(qml.PauliZ(0)), qml.expval(qml.Hadamard(1))

qnode1 = qml.QNode(circuit1, dev1)
qnode2 = qml.QNode(circuit1, dev2, interface='torch')

qnode1() is a default NumPy-interfacing QNode, while qnode2() is a PyTorch-capable QNode:

>>> qnode2(phi, theta)
tensor([0.8776, 0.6880], dtype=torch.float64)

Construction from an existing QNode

Let’s say we change our mind and now want qnode1() to interface with PyTorch. We can easily perform the conversion by using the to_torch() method:

>>> qnode1.to_torch()
>>> qnode1
<QNode: device='default.mixed', func=circuit1, wires=2, interface=PyTorch>

qnode1() is now a PyTorch-capable QNode, as well.

Quantum gradients using PyTorch

Since a PyTorch-interfacing QNode acts like any other torch.autograd.Function, the standard method used to calculate gradients with PyTorch can be used.

For example:

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

@qml.qnode(dev, interface='torch')
def circuit3(phi, theta):
    qml.RX(phi[0], wires=0)
    qml.RY(phi[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.PhaseShift(theta, wires=0)
    return qml.expval(qml.PauliZ(0))

phi = torch.tensor([0.5, 0.1], requires_grad=True)
theta = torch.tensor(0.2, requires_grad=True)
result = circuit3(phi, theta)

Now, performing the backpropagation and accumulating the gradients:

>>> result.backward()
>>> phi.grad
tensor([-0.4794,  0.0000])
>>> theta.grad
tensor(-5.5511e-17)

To include non-differentiable data arguments, simply set requires_grad=False:

@qml.qnode(dev, interface='torch')
def circuit3(weights, data):
    qml.templates.AmplitudeEmbedding(data, normalize=True, wires=[0, 1])
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.PhaseShift(weights[2], wires=0)
    return qml.expval(qml.PauliZ(0))

Here, data is non-trainable embedded data, so should be marked as non-differentiable:

>>> weights = torch.tensor([0.1, 0.2, 0.3], requires_grad=True)
>>> data = torch.tensor(np.random.random([4]), requires_grad=False)
>>> result = circuit3(weights, data)
>>> result.backward()
>>> data.grad
None
>>> weights.grad
tensor([3.6317e-02, 0.0000e+00, 5.5511e-17])

Optimization using PyTorch

To optimize your hybrid classical-quantum model using the Torch interface, you must make use of the PyTorch provided optimizers, or your own custom PyTorch optimizer. The PennyLane optimizers cannot be used with the Torch interface.

For example, to optimize a Torch-interfacing QNode (below) such that the weights x result in an expectation value of 0.5, with the classical nodes processed on a GPU, we can do the following:

import torch
import pennylane as qml

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

@qml.qnode(dev, interface='torch')
def circuit4(phi, theta):
    qml.RX(phi[0], wires=0)
    qml.RZ(phi[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RX(theta, wires=0)
    return qml.expval(qml.PauliZ(0))

def cost(phi, theta):
    return torch.abs(circuit4(phi, theta) - 0.5)**2

phi = torch.tensor([0.011, 0.012], requires_grad=True)
theta = torch.tensor(0.05, requires_grad=True)

opt = torch.optim.Adam([phi, theta], lr = 0.1)

steps = 200

def closure():
    opt.zero_grad()
    loss = cost(phi, theta)
    loss.backward()
    return loss

for i in range(steps):
    opt.step(closure)

The final weights and circuit value are:

>>> phi_final, theta_final = opt.param_groups[0]['params']
>>> phi_final, theta_final
(tensor([0.7345, 0.0120], device='cuda:0', requires_grad=True), tensor(0.8316, device='cuda:0', requires_grad=True))
>>> circuit(phi_final, theta_final)
tensor(0.5000, device='cuda:0', dtype=torch.float64, grad_fn=<_TorchQNodeBackward>)

Note

For more advanced PyTorch models, Torch-interfacing QNodes can be used to construct layers in custom PyTorch modules (torch.nn.Module).

See https://pytorch.org/docs/stable/notes/extending.html#adding-a-module for more details.

Torch.nn integration

Once you have a Torch-compaible QNode, it is easy to convert this into a torch.nn layer. To help automate this process, PennyLane also provides a TorchLayer class to easily convert a QNode to a torch.nn layer. Please see the corresponding TorchLayer documentation for more details and examples.