TensorFlow interface

In order to use PennyLane in combination with TensorFlow, we have to generate TensorFlow-compatible quantum nodes. Such a QNode can be created explicitly using the interface='tf' keyword in the QNode decorator or QNode class constructor.

Note

To use the TensorFlow interface in PennyLane, you must first install TensorFlow. Note that this interface only supports TensorFlow versions >= 2.3!

Tensorflow is imported as follows:

import pennylane as qml
import tensorflow as tf

Using the TensorFlow 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 TensorFlow-capable QNode is to specify the interface='tf' keyword argument:

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

@qml.qnode(dev, interface='tf')
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 TensorFlow-capable QNode, accepting tf.Variable and tf.Tensor objects as input, and returning tf.Tensor objects.

>>> phi = tf.Variable([0.5, 0.1])
>>> theta = tf.Variable(0.2)
>>> circuit(phi, theta)
(<tf.Tensor: shape=(), dtype=float64, numpy=0.8775825769558366>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.6880373394540326>)

The interface can also be automatically determined when the QNode is called. You do not need to pass the interface if you provide parameters.

TensorFlow-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='tf')

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

>>> qnode2(phi, theta)
(<tf.Tensor: shape=(), dtype=float64, numpy=0.8775825769558366>,
 <tf.Tensor: shape=(), dtype=float64, numpy=0.6880373394540326>)

Quantum gradients using TensorFlow

Since a TensorFlow-interfacing QNode acts like any other TensorFlow function, the standard method used to calculate gradients in eager mode with TensorFlow can be used.

For example:

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

@qml.qnode(dev, interface='tf')
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))

phi = tf.Variable([0.5, 0.1])
theta = tf.Variable(0.2)

with tf.GradientTape() as tape:
    # Use the circuit to calculate the loss value
    loss = circuit(phi, theta)

phi_grad, theta_grad = tape.gradient(loss, [phi, theta])

Now, printing the gradients, we get:

>>> phi_grad
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-0.47942555,  0.        ], dtype=float32)>
>>> theta_grad
<tf.Tensor: shape=(), dtype=float32, numpy=3.469447e-18>

To include non-differentiable data arguments, simply use tf.constant:

@qml.qnode(dev, interface='tf')
def circuit3(weights, data):
    qml.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))

weights = tf.Variable([0.1, 0.2, 0.3])
rng = np.random.default_rng(seed=111)
data = tf.constant(rng.random([4]))

with tf.GradientTape() as tape:
    result = circuit3(weights, data)

Calculating the gradient:

>>> grad = tape.gradient(result, weights)
>>> grad
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0.08575502, 0.        , 0.        ], dtype=float32)>

Optimization using TensorFlow

To optimize your hybrid classical-quantum model using the TensorFlow eager interface, you must make use of the TensorFlow optimizers provided in the tf.train module, or your own custom TensorFlow optimizer. The PennyLane optimizers cannot be used with the TensorFlow interface.

For example, to optimize a TensorFlow-interfacing QNode (below) such that the weights x result in an expectation value of 0.5, we can do the following:

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

@qml.qnode(dev, interface='tf')
def circuit4(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 = tf.Variable([0.5, 0.1], dtype=tf.float64)
theta = tf.Variable(0.2, dtype=tf.float64)

opt = tf.keras.optimizers.SGD(learning_rate=0.1)
steps = 200

for i in range(steps):
    with tf.GradientTape() as tape:
        loss = tf.abs(circuit4(phi, theta) - 0.5)**2

    gradients = tape.gradient(loss, [phi, theta])
    opt.apply_gradients(zip(gradients, [phi, theta]))

The final weights and circuit value are:

>>> phi
<tf.Variable 'Variable:0' shape=(2,) dtype=float64, numpy=array([ 1.04719755,  0.1       ])>
>>> theta
<tf.Variable 'Variable:0' shape=() dtype=float64, numpy=0.20000000000000001>
>>> circuit4(phi, theta)
<tf.Tensor: id=106269, shape=(), dtype=float64, numpy=0.5000000000000091>

Keras integration

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