How to execute quantum circuits in collections and batches

Carlos E. Lopetegui Gonzalez

The general idea of machine learning protocols is to learn some function — for example, a classification function — from the information provided by a data set consisting of inputs and outputs. Usually, a parameterized function — in the quantum world often referred to as a circuit — is used as an ansatz. This function may have an arbitrarily large number of parameters that need to be tuned to learn the appropriate function. A way to quantify how close the parameterized function is to the target one (in the case of supervised learning) is by using a custom loss function that depends on the difference between the output of the circuit and the real output. The way we tune the parameters of our model is by moving them in the high dimensional space towards a region where the loss function is lower.

In quantum machine learning the parameterized function is substituted by the expectation value of some observables in the state resulting from one parameterized circuit. If you’re new to quantum machine learning we encourage you to also check out our blog post on how to start learning quantum machine learning. Usually, we need to evaluate this circuit using different input parameters before actually computing a loss function. In a typical supervised-learning scenario, we will have a set of \(M\) input data \(\vec{x}\) and labels \(\vec{y}\) or target outputs. Then, we can define a cost function through the mean squared error between the outputs of the circuit evaluated at each \(\vec{x}_{m}\) and the labels \(\vec{y}_{m}\), that is

\begin{equation*} Err(\theta)=\sum_{m=1}^{M} |\text{circuit}\left(\theta,\vec{x}_m\right)-\vec{y}_m |^2, \end{equation*}

where \(\theta\) is a parameter that defines the model.

To evaluate batches the naive approach would be to run a for loop to sequentially run the circuit on each sample in the batch. Yet, in PennyLane it is possible to do this by using built-in methods. Those methods are available for all PennyLane devices. This means they can be used either in the context of qubit-based circuits (using for instance the default.qubit) device or in the evaluation of optical continuous-variable circuits (like the default.gaussian device).

In this how-to, we will learn about the best practices in PennyLane to deal with batches and collections of circuits. For that, we will consider first a simple two-qubit circuit. Then, in a second moment, we will use two circuits to form a basic collection, which is useful when more complicated classification tasks have to be accomplished.

Basic example

We will build a basic example to show how to run batches of circuits. For that, let’s start by importing the relevant packages.

import pennylane as qml
from pennylane import numpy as np

Let’s now create a very simple circuit to play with. The idea with this circuit is to encode the information from the input into the state of the qubit by using a rotation around the X-axis (RX gate). Then we will perform a parametrized rotation around the Y-axis (RY gate), using an angle defined as a parameter of the circuit.

# We create our data and labels

x_train = np.array([0.2, 0.1, 0.2, 0.14, 0.11, 0.41, 0.55, 0.3, 0.31, 0.6])
y_train = np.array([1,1,1,1,1,-1,-1,-1,-1,-1])

# We define the circuit we are going to use

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


def simple_qubit_circuit(theta, input_value):
    qml.RX(input_value, wires=0)
    qml.RY(theta, wires=0)
    return qml.expval(qml.PauliZ(0))

Now, we have created all we need to execute a batch of circuits. The goal is to take the training data set and run the circuit on each of its samples to compute the error function as described above. This can be done by running a for loop over all the samples, as shown below.

err = 0
for x, y in zip(x_train, y_train):
    err += (simple_qubit_circuit(theta, x)-y)**2
>>> print(err)


Nevertheless, in PennyLane this can be done efficiently by using a feature called quantum parameter broadcasting. The idea is as simple as passing parameters as arrays with one added dimension that runs over the batch.

>>> print(sum((simple_qubit_circuit(theta,x_train)-y_train)**2))


And that’s it, we’ve run a batched evaluation of a circuit! As you can see, in this case we have sent to the circuit the whole dataset instead of sending the elements one by one.

Executing collections of quantum circuits

A limitation of the above evaluation is that it is only valid for a single fixed qnode operating with different parameters. That is the same circuit and the same device. Yet, more generally, we may want to execute collections of different quantum circuits (or devices) and simultaneously use their output to compute a loss function (see, for example, this demo). To do this we can use the QNodeCollection() PennyLane class. One of the most attractive features of this class is that it allows for the evaluation of different parameterized circuits in different devices.

Below, we show a simple example in which we evaluate circuits in different devices. With their outputs, we could create a classification function, and then define a customized cost function, yet, we will not distract ourselves here by doing that. We will consider two circuits, one qubit-based circuit and one continuous-variable circuit, to make the versatility of this functionality clear.

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

Now we define the two circuits

# for the first circuit (qubit one) we will use the same circuit defined above
def qubit_circuit(params, input_value):
    qml.RX(input_value, wires=0)
    qml.RY(params[0], wires=0)
    return qml.expval(qml.PauliZ(0))

def gaussian_circuit(params, input_value):
    qml.Displacement(input_value, 0.0, wires=0)
    qml.Rotation(params[1], wires=0)

    return qml.expval(qml.X(0))

# create the quantum nodes
qnode0 = qml.QNode(qubit_circuit, dev0)
qnode1 = qml.QNode(gaussian_circuit, dev1)

The gaussian circuit is given by a composition of a displacement and a rotation in the phase space of an optical mode. A subtle but important point to keep in mind is that even if the parameters that define the circuits are independent the input parameters are the same, which implies that we have to carefully specify which parameters are passed to each gate.

params_qubit = np.random.rand()
params_gaussian = np.random.rand()
params = np.array([params_qubit, params_gaussian], dtype=object)

Now we can create an instance of the class QNodeCollection().

qnode_collection = qml.QNodeCollection([qnode0, qnode1])

Let’s run them! Let PennyLane do it in the most optimal way.

>>> input_value=0.2
>>> qnode_collection(params, input_value)

array([0.76468227, 0.29916979])

And it’s done! In this how-to, we have learned how to evaluate circuits in batches and collections. Now you just have to bring it to the next level and run your own machine learning algorithms. With collections, you will be able to exploit the potential of the different devices PennyLane gives access to, and create your own custom models!

Carlos E. Lopetegui-Gonzalez

Carlos E. Lopetegui-Gonzalez

Carlos is a master’s student at the Ecole Normale Superieur in Paris. His research focuses on quantum optics and quantum information. He is currently working with the Architecture team at Xanadu as part of the summer Residency Program.