Preliminaries

We shall be using pennylane from now on, unless otherwise noted. Installation of pennylane is very similar to qiskit. See here. You might want to check cuQuantum acceleration lightning.gpu

How to embed data into a quantum state

In classical computing problems, data is classical. Does it make sense for quantum computers to deal with classical data? The short answer is yes! In this how-to, the first few steps of how to encode classical data into a quantum state is presented.

Different embedding types

To encode your classical data into a quantum state, you first have to find out what type of classical data you have. We will distinguish between three different types of data in \(N-\)dimensions

  1. Discrete data, represented as binary \(\mathbf{b}\in{0,1}^N\) or integer values \(\mathbf{k}\in\mathbb{Z}^N\)

  2. Real continuous data, represented as floating-point values \(\mathbf{k}\in\mathbb{R}^N\)

  3. Complex continuous data, represented as complex values \(\mathbf{\alpha}\in\mathbb{C}^2^N\)

Keeping the subset relations \(\{0, 1\}\subset\mathbb{Z}\subset\mathbb{R}\subset\mathbb{C}\) in mind, one could always choose to interpret the data in the domain \(\mathcal{D}\) to be in the superset \(\mathcal{D}^{\prime}\supset \mathcal{D}\)

1. Discrete data, represented as binary or integer values

A suitable encoding for binary data is the so-called BasisEmbedding. The BasisEmbedding class interprets a binary string as a qubit basis state with the following mapping:

\[ \mathbf{b}=(b_{0}, \ldots, b_{N-1}) \mapsto |b_{0}, \ldots, b_{N-1}\rangle. \]

See below for a simple example of the BasisEmbedding used to initialize three qubits.

import pennylane as qml

N = 3
wires = range(N)
dev = qml.device("default.qubit", wires)

@qml.qnode(dev)
def circuit(b):
    qml.BasisEmbedding(b, wires)
    return qml.state()

circuit([1, 1, 1])
tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], requires_grad=True)

As expected, the result corresponds to the state |111⟩=|1⟩⊗|1⟩⊗|1⟩. Representing the |1⟩

state as the second standard basis vector and the tensor product as the Kronecker product, we can confirm the result with a quick calculation.

\[\begin{split} \left| 1 \right \rangle \otimes \left| 1 \right \rangle \otimes \left| 1 \right \rangle = \begin{bmatrix}0 \\ 1\end{bmatrix} \otimes \begin{bmatrix} 0 \\ 1\end{bmatrix} \otimes \begin{bmatrix} 0 \\ 1\end{bmatrix} = \begin{bmatrix}0 & 0 & 0 & 0 & 0 & 0 & 0 & 1\end{bmatrix}^{\top} \end{split}\]

You can also just pass an integer value to the basis embedding function and it will automatically convert it into its binary representation. We can perform a quick sanity check of this functionality by running

 print(circuit(7))
[0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j]

which is the same state vector we saw before. Unsurprisingly, the binary label corresponding to this state vector is also consistent with the binary representation of the integer seven.

Send it after class

Embed integer value \(32\) using basisembedding

2. Continuous data, represented as floating-point values

The simplest type of encoding for floating-point data is called AngleEmbedding. This type of embedding encodes a single floating-point value \( x\in \mathbb{R} \) into a quantum state with the mapping $\( x \mapsto R_{k}(x)|0\rangle = e^{-i x\sigma_{k}/2}|0\rangle, \)\( where \)k\in{x, y, z}\( is the axis of rotation in the Bloch sphere. The default axis of rotation is set to \)k=x\( in the `AngleEmbedding` class. You may also choose to set it to \)k=y\(, but make sure to avoid \)k=z\(. The latter case is not useful because every \)x\( will be mapped to the \)|0\rangle$ state; the encoded value will be lost. Note that you can also input a tensor-like object and encode each component as a qubit. Examine the code snippet below to see how to encode a classical floating-point value as a quantum state!

import pennylane as qml
from pennylane import numpy as np

N = 3
wires = range(3)
dev = qml.device("default.qubit", wires)

@qml.qnode(dev)
def circuit(val_list):
    qml.AngleEmbedding(val_list, wires)
    return [qml.expval(qml.PauliZ(w)) for w in wires]
circuit([0.0, np.pi / 2, np.pi])
tensor([ 1.00000000e+00,  2.22044605e-16, -1.00000000e+00], requires_grad=True)

Keep in mind that Pauli rotations are \(2\pi\)-periodic up to a global phase, meaning that you should normalize your data to be in \(\Omega:=[0, \pi)\subset \mathbb{R}\) if possible. This can be helpful in order to avoid encoding two different values as the same quantum state. While the AngleEmbedding allows you to encode a lot of information in a single qubit, this comes at the cost of a difficult construction process.

3. Continuous data, represented as complex values

Next is the AmplitudeEmbedding. As the name suggests, an array of values can be used as the amplitudes of the state with the mapping $\( \boldsymbol{\alpha}=(\alpha_0, \ldots, \alpha_{2^N-1})\mapsto \sum_{k=0}^{2^N-1}\alpha_{k}|k\rangle \)$ and can be implemented with the following code.

import pennylane as qml

N = 3
wires = range(N)
dev = qml.device("default.qubit", wires)

@qml.qnode(dev)
def circuit(features):
    qml.AmplitudeEmbedding(features, wires)
    return qml.state()
circuit([0.625, 0.0, 0.0, 0.0, 0.625j, 0.375, 0.25, 0.125])
tensor([0.625+0.j   , 0.   +0.j   , 0.   +0.j   , 0.   +0.j   ,
        0.   +0.625j, 0.375+0.j   , 0.25 +0.j   , 0.125+0.j   ], requires_grad=True)

Here, the values were chosen to be normalized, i.e. \(\lVert\boldsymbol{\alpha}\rVert=1\). Note that one can use unnormalized data by setting the normalize parameter of the AmplitudeEmbedding class to True.

Templates

PennyLane provides a growing library of pre-coded templates of common variational circuit architectures that can be used to easily build, evaluate, and train more complex models. In the literature, such architectures are commonly known as an ansatz. Templates can be used to embed data into quantum states, to define trainable layers of quantum gates, to prepare quantum states as the first operation in a circuit, or simply as general subroutines that a circuit is built from.

Embedding templates

Embeddings encode input features into the quantum state of the circuit. Hence, they usually take a data sample such as a feature vector as an argument. Embeddings can also depend on trainable parameters, and they may be constructed from repeated layers.

You can reach Embedding templates here

Using the Ising model for embedding

One of the most adapted models in physics is the Ising model, invented by Wilhelm Lenz as a PhD problem to his student Ernst Ising. The one-dimensional version of it was solved in Ising’s thesis in 1924; later, in 1944, Lars Onsager solved the two-dimensional case in the absense of external magnetic field and in a square lattice.

Although primarily a physical model, it is quite fair to say that the model became part of the mathematics literature, since its descriptions and formulations involve many interesting tools from graph theory, combinatorics, measure theory, convex analysis and so on.

Physically, the Ising model can be thought as a system of many little magnets in which case the spins \(\pm 1\) represent a magnetic moment. It can also represent a lattice gas, where the spins now represent whether a site is occupied by a particle or not.

The Ising model defines a universality class, meaning lots of systems simplify to something that looks basically like a magnet. Renormalisation tells us that lots of systems share universal asymptotic dynamics, which is a more formal way of saying they simplify to the same thing. So, anything lying in the Ising model’s universality class answers your question. This includes lots of systems that lie on a network or have some dynamical description emphasising interactions, as well as lots of systems that have a second-order phase transition or exhibit some anomalous breaking of a symmetry group under certain conditions. Between these two examples, that’s quite a lot of applied mathematics. An interesting meta-commentary on any mathematical model of correlated variables itself being an Ising model can be found in this paper. It also describes non-magnetic physical systems, like string theories and conformal field theories. The point of the model is that, for something so simple, it is incredibly rich – this is probably why it’s stuck about for so long – and naturally, that makes it difficult to enumerate all the ways in which it has been useful.

Please have a look at Dalton A R Sakthivadivel’s page for more details.

An interesting embedding approach is then using the Quantum Approximate Optimization Algorithm (QAOA) and Ising model for feature embedding. You start with angle embedding the data, but if the features you are trying to embed have some underlying structure captured by Ising model universality class, then the qubit requirements will be reduced. The template is here

import pennylane as qml
import numpy as np

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

@qml.qnode(dev)
def circuit(weights, f=None):
    qml.QAOAEmbedding(features=f, weights=weights, wires=range(2))
    return qml.expval(qml.PauliZ(0))

features = [1., 2.]
shape = qml.QAOAEmbedding.shape(n_layers=2, n_wires=2)
weights = np.random.random(shape)

print(circuit(weights, f=features))

opt = qml.GradientDescentOptimizer()
for i in range(10):
    weights = opt.step(lambda w : circuit(w, f=features), weights)
    print("Step ", i, " weights = ", weights)
-0.859459207139468
Step  0  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  1  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  2  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  3  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  4  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  5  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  6  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  7  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  8  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
Step  9  weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]]
/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/pennylane/_grad.py:107: UserWarning: Attempted to differentiate a function with no trainable parameters. If this is unintended, please add trainable parameters via the 'requires_grad' attribute or 'argnum' keyword.
  warnings.warn(

Training the features

In principle, also the features are trainable, which means that gradients with respect to feature values can be computed. To train both weights and features, they need to be passed to the qnode as positional arguments. If the built-in optimizer is used, they have to be merged to one input:

@qml.qnode(dev)
def circuit2(weights, features):
    qml.QAOAEmbedding(features=features, weights=weights, wires=range(2))
    return qml.expval(qml.PauliZ(0))

opt = qml.GradientDescentOptimizer()
for i in range(10):
    weights, features = opt.step(circuit2, weights, features)
    print("Step ", i, "\n weights = ", weights, "\n features = ", features,"\n")
Step  0 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  1 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  2 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  3 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  4 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  5 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  6 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  7 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  8 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Step  9 
 weights =  [[0.24645036 0.31188654 0.9346985 ]
 [0.65247654 0.15251304 0.10385546]] 
 features =  [1.0, 2.0] 

Send it after class

import pennylane as qml
import numpy as np


dev = ##Fill me

@qml.qnode(dev)
def circuit3(weights, f=None):
    ###Fill me

features = [1., -1.,1.,-1.]
shape = qml.QAOAEmbedding.shape(n_layers=2, n_wires= FILL HERE )
weights = np.random.random(shape)

opt = qml.GradientDescentOptimizer()
for i in range(100):
    weights, features = opt.step(circuit3, weights, features)
    print("Step ", i, "\n weights = ", weights, "\n features = ", features,"\n")
  Input In [7]
    dev = ##Fill me
          ^
SyntaxError: invalid syntax