Quantum SVM for Supernova Classification

Last updated on
8 min, 1520 words

I thank Peters, et al.(2021) for their work [1].

April 2022

Imports

import numpy as np import paddle from paddle_quantum.circuit import UAnsatz from sklearn import svm from typing import NamedTuple from matplotlib import pyplot as plt import time import warnings

Set-up

Fixing random state for reproducibility and suppress warnings

np.random.seed(999)
warnings.filterwarnings('ignore')

Define a struct to encapsulate necessary circuit information such as the number of qubits, the depth, etc. This allows effective iterations with different sets of parameters.

class CircuitInfo(NamedTuple):
    N: int
    DEPTH: int
    Ntrain: int
    Ntest: int 

Instantiate a global struct with 4 qubits, DEPTH = 1, 6 training data, 100 testing data (those parameters will not result in a very good prediction, as will be seen later on; those parameters will be changed)

circuit_info = CircuitInfo(4, 1, 6, 100)

Note that DEPTH is depth of the circuit for $U(x_i)$ by treating the following as a single unit. This convention can be seen in Baidu Paddel Quantum – Quantum Neural Network, Built-in Circuits (the picture right underneath Eq 3.).

--Rx----Ry----Rz----Rxx----Ryy----------------
                     |      |                                
--Rx----Ry----Rz----Rxx----Ryy----Rxx----Ryy--
                                   |      |      
--Rx----Ry----Rz----Rxx----Ryy----Rxx----Ryy--
                     |      |                                                 
--Rx----Ry----Rz----Rxx----Ryy----------------

Dataset generation

Generate a binary classification data set. Note that the data generated here does not necessarily represent any real supernova clusters as I am not well trained enough in astronomy to generate realistic dataset on my own. Kaggle has one but I think it is not well collected. Taking into considertation that how the data is generated is not crucial in presenting the core algorithm, it is okay to do that. The function below puts the data into a 3d array which may have differed from what Peters, et al.(2021) [1] have done in their paper. However, it should be noted that the data can be reshaped to the desired dimensions.

def data_generator(boundary_gap):
    X = []
    Y = []
    for k in range(circuit_info.Ntrain + circuit_info.Ntest):
        # generate two different types of data
        vec = []
        if (np.random.rand() < boundary_gap):
            vec = vec + [np.random.rand() for _ in range(3*circuit_info.N*circuit_info.DEPTH)]
            Y.append(1)
        else:
            vec = vec + [np.random.rand() for _ in range(circuit_info.N*circuit_info.DEPTH)]
            vec = vec + [np.random.rand()*10 for _ in range(2*circuit_info.N*circuit_info.DEPTH)]
            Y.append(0)
        X.append(vec)
    X = np.array(X).astype("float64")
    Y = np.array(Y).astype("float64")
    return X[0:circuit_info.Ntrain], Y[0:circuit_info.Ntrain], X[circuit_info.Ntrain:], Y[circuit_info.Ntrain:]

Notice that this could serve as reductions from a higher dimentional data – reduce original features into 3*circuit_info.N*circuit_info.DEPTH number of features. The values resulted by the reductions are distributed (np.random). Some sample reduction functions are shown below which are not necessary in this case (the numbers are randomized before and after the reduction).

# A sample reduction function to reduce the first and second features into one
def a_first_reduction(v, w):
    return np.sqrt(v**2+w**2)/4

# A sample reduction function to reduce the last and second last features into one
def a_second_reduction(v, w):
    return np.sqrt(v**2-w**2)/2

SVM with Quantum circuits

The two functions below are variations of the q_kernel_estimator and q_kernel_matrix shown in Baidu Paddel Quantum – Quantum Kernel Method, with a difference that two disconnected circuits are returned, cir1 and cir2, representing the circuits for $U(x_i)\text{ and }U(x_j)$ respectively. fin_state2.conj() is the $U^\dagger(x_i)$. Those two functions are used to generate the customized kernel matrix.

def q_kernel_estimator(x1, x2):
    theta1 = paddle.to_tensor(x1)
    theta2 = paddle.to_tensor(x2)
    cir1 = circuit(theta1)
    cir2 = circuit(theta2)
    fin_state1 = cir1.run_state_vector()
    fin_state2 = cir2.run_state_vector()
    return (fin_state2.conj() * fin_state1).real().numpy()[0]

def q_kernel_matrix(X1, X2):
    return np.array([[q_kernel_estimator(x1, x2) for x2 in X2] for x1 in X1])

Circuit

The below function generates a quantum circuit based on the description in II. QUANTUM KERNEL SUPPORT VECTOR MACHINES, B.Circuit Design [1] and the algorithm is templated by Baidu Paddel Quantum – Quick start, Frequently used function, VQA basic structure, with one major obstacle – is not native in paddle_quantum. However, upon further investigation, it can be genereted via a combination of two native gates:

def circuit(theta):
    cir = UAnsatz(circuit_info.N)
    theta = paddle.to_tensor(np.reshape(theta, (circuit_info.N, circuit_info.DEPTH, 3)))
    for dep in range(circuit_info.DEPTH): 
        for n in range(circuit_info.N): 
            cir.rx(theta[n][dep][0], n)  # add an Rx gate to the n-th qubit
            cir.ry(theta[n][dep][1], n)  # add an Ry gate to the n-th qubit
            cir.rz(theta[n][dep][2], n)  # add an Rz gate to the n-th qubit 
        for n in range(circuit_info.N - 1):
            if n % 2 == 0 and (n < circuit_info.N):
                # the following two lines generate the (forward) square root of a iswap unit
                # as there is no native implementation 
                cir.rxx(paddle.to_tensor(np.array([-np.pi/4])), [n, n+1])
                cir.ryy(paddle.to_tensor(np.array([-np.pi/4])), [n, n+1])
        for n in range(circuit_info.N - 1):
            if n % 2 == 1 and (n < circuit_info.N - 1):
                cir.rxx(paddle.to_tensor(np.array([-np.pi/4])), [n, n+1])
                cir.ryy(paddle.to_tensor(np.array([-np.pi/4])), [n, n+1])
    return cir

An example circuit generated by the above code is as follows

--Rx(0.528)----Ry(0.119)----Rz(0.640)----Rxx(-0.7)----Ryy(-0.7)----------------------------
                                             |            |                                
--Rx(0.091)----Ry(0.332)----Rz(0.427)----Rxx(-0.7)----Ryy(-0.7)----Rxx(-0.7)----Ryy(-0.7)--
                                                                       |            |      
--Rx(5.544)----Ry(6.281)----Rz(6.974)----Rxx(-0.7)----Ryy(-0.7)----Rxx(-0.7)----Ryy(-0.7)--
                                             |            |                                
--Rx(7.899)----Ry(1.319)----Rz(3.428)----Rxx(-0.7)----Ryy(-0.7)----Rxx(-0.7)----Ryy(-0.7)--
                                                                       |            |      
--Rx(2.016)----Ry(7.073)----Rz(0.334)----Rxx(-0.7)----Ryy(-0.7)----Rxx(-0.7)----Ryy(-0.7)--
                                             |            |                                
--Rx(9.093)----Ry(4.052)----Rz(7.604)----Rxx(-0.7)----Ryy(-0.7)----------------------------

The example circuit is for a 18-dimensional data.

Training results

result calls the SVM implemented with the custumized circuit, predicts for the testing set, and returns the accuracies for the trainning and testing sets.

def result():
    svm_qke = svm.SVC(kernel=q_kernel_matrix)
    X_train, y_train, X_test, y_test = data_generator(0.5)
    svm_qke.fit(X_train, y_train)
    predict_svm_qke_train = svm_qke.predict(X_train)
    predict_svm_qke_test = svm_qke.predict(X_test)
    accuracy_train = np.array(predict_svm_qke_train == y_train, dtype=int).sum()/len(y_train)
    accuracy_test = np.array(predict_svm_qke_test == y_test, dtype=int).sum()/len(y_test)
    return accuracy_train, accuracy_test

change_Ntrain changes the number of data in the training set ($[6, 16)$ in steps of 2) while keeping the other parameters constant.

def change_Ntrain():
    global circuit_info 
    # temp holds the attribute that is changing for recovery
    temp = circuit_info.Ntrain
    Ntrain_lst = range(temp, temp + 10, 2)
    res_train = []
    res_test = []
    time_elapsed = []
    for k in Ntrain_lst:
        circuit_info = CircuitInfo(circuit_info.N, circuit_info.DEPTH, k, circuit_info.Ntest)
        start = time.time()
        a, b = result()
        end = time.time()
        res_train.append(a)
        res_test.append(b)
        time_elapsed.append(end - start)
    fig, ax = plt.subplots(1, 3)
    ax[0].scatter(Ntrain_lst, res_train, marker='o')
    ax[0].set_title('Train')
    ax[1].scatter(Ntrain_lst, res_test, marker='*')
    ax[1].set_title('Test')
    ax[2].scatter(Ntrain_lst, time_elapsed, marker='v')
    ax[2].set_title('Time(s)')
    circuit_info = CircuitInfo(circuit_info.N, circuit_info.DEPTH, temp, circuit_info.Ntest)

change_N changes the number of qubits ($[4, 15)$ in steps of 2) while keeping the other parameters constant.

def change_N():
    global circuit_info 
    # temp holds the attribute that is changing for recovery
    temp = circuit_info.N
    N_lst = range(temp, temp + 11, 2)
    res_train = []
    res_test = []
    time_elapsed = []
    for k in N_lst:
        circuit_info = CircuitInfo(k, circuit_info.DEPTH, circuit_info.Ntrain, circuit_info.Ntest)
        start = time.time()
        a, b = result()
        end = time.time()
        res_train.append(a)
        res_test.append(b)
        time_elapsed.append(end - start)
    fig, ax = plt.subplots(1, 3)
    ax[0].scatter(N_lst, res_train, marker='o')
    ax[0].set_title('Train')
    ax[1].scatter(N_lst, res_test, marker='*')
    ax[1].set_title('Test')
    ax[2].scatter(N_lst, time_elapsed, marker='v')
    ax[2].set_title('Time(s)')
    circuit_info = CircuitInfo(temp, circuit_info.DEPTH, circuit_info.Ntrain, circuit_info.Ntest)

change_DEPTH changes the DEPTH of the circuit ($[1, 6)$ in steps of 1) while keeping the other parameters constant.

def change_DEPTH():
    global circuit_info 
    # temp holds the attribute that is changing for recovery
    temp = circuit_info.DEPTH
    DEPTH_lst = range(temp, temp + 5)
    res_train = []
    res_test = []
    time_elapsed = []
    for k in DEPTH_lst:
        circuit_info = CircuitInfo(circuit_info.N, k, circuit_info.Ntrain, circuit_info.Ntest)
        start = time.time()
        a, b = result()
        end = time.time()
        res_train.append(a)
        res_test.append(b)
        time_elapsed.append(end - start)
    fig, ax = plt.subplots(1, 3)
    ax[0].scatter(DEPTH_lst, res_train, marker='o')
    ax[0].set_title('Train')
    ax[1].scatter(DEPTH_lst, res_test, marker='*')
    ax[1].set_title('Test')
    ax[2].scatter(DEPTH_lst, time_elapsed, marker='v')
    ax[2].set_title('Time(s)')
    circuit_info = CircuitInfo(circuit_info.N, temp, circuit_info.Ntrain, circuit_info.Ntest)

Call the three functions and visualize the results. (NOTE: do not run those on Jupyter, they are pretty slow. I have to use 3 different consoles to run all three of them concurrently.)

change_Ntrain()

1.png

change_N()

2.png

change_DEPTH()

3.png

[1] Evan Peters, João Caldeira, Alan Ho, Stefan Leichenauer, Masoud Mohseni, Hartmut Neven, Panagiotis Spentzouris, Doug Strain, and Gabriel N. Perdue, Machine learning of high dimensional data on a noisy quantum processor (2021), arXiv: https://arxiv.org/abs/2101.09581