How to optimize a quantum kernel#
import sys
import os
# to import quask, move from docs/source/notebooks to src
sys.path.append('../../../src')
import quask
One of the main challenges regarding quantum kernels is the need to choose the appropriate ansatz for each different problem. When little-to-no information about a certain task is known, we can use techniques to attempt to construct a suitable quantum kernel following an optimization problem. Two main approaches are possible:
Choosing an ansatz where some of the parameters are the usual features, while others are freely tunable parameters that are optimized according to some cost function via a stochastic gradient descent-based algorithm.
Avoid making any choice and let an optimization algorithm pick the entire quantum circuit.
In this tutorial, we will demonstrate how to easily implement the first technique. Furthermore, we will also discuss how to leverage the quask built-in features to implement the second technique. Finally, we will show how it is possible to efficiently achieve the best-performing linear combination of these kernels when dealing with multiple different kernels.
Optimization of quantum kernels in quask#
The package quask.optimizer
allows defining an optimization
procedure for quantum kernels. The main interface is the
BaseKernelOptimizer
class, which requires: * a kernel function,
which will serve as the initial point of the optimization routine; * a
kernel evaluator, which will be the cost function guiding the
optimization; * possibly some input data, if needed by the kernel
evaluator.
Then, the optimizer
method starts the optimization and will return a
new instance of quask.core.Kernel
to be used. You will use
BaseKernelOptimizer
directly if only to create a new optimization
method, which can be done as follows:
import numpy as np
from sklearn.datasets import load_iris
from sklearn.svm import SVC
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import copy
from quask.core import Ansatz, Kernel, KernelFactory, KernelType
from quask.core_implementation import QiskitKernel
from quask.optimizer.base_kernel_optimizer import BaseKernelOptimizer
from quask.evaluator import CenteredKernelAlignmentEvaluator
def create_qiskit_noiseless(ansatz: Ansatz, measurement: str, type: KernelType):
return QiskitKernel(ansatz, measurement, type, n_shots=None)
KernelFactory.add_implementation('qiskit_noiseless', create_qiskit_noiseless)
KernelFactory.set_current_implementation('qiskit_noiseless')
class RandomOptimizer(BaseKernelOptimizer):
def __init__(self, initial_kernel, X, y, ke):
super().__init__(initial_kernel, X, y, ke)
def optimize(self):
kernel = copy.deepcopy(self.initial_kernel)
cost = self.ke.evaluate(kernel, None, self.X, self.y)
N_TENTATIVES = 10
for i in range(N_TENTATIVES):
new_kernel = copy.deepcopy(kernel)
i_operation = np.random.randint(new_kernel.ansatz.n_operations)
i_feature = np.random.randint(new_kernel.ansatz.n_features)
i_wires = np.random.choice(range(new_kernel.ansatz.n_qubits), 2, replace=False).tolist()
i_gen = np.random.choice(['I', 'Z', 'X', 'Y'], 2, replace=True)
i_gen = "".join(i_gen.tolist())
i_bandwidth = np.random.rand()
new_kernel.ansatz.change_operation(i_operation, i_feature, i_wires, i_gen, i_bandwidth)
new_cost = self.ke.evaluate(new_kernel, None, self.X, self.y)
print("Cost of the new solution:", new_cost)
if cost > new_cost:
kernel = new_kernel
cost = new_cost
return kernel
Let’s unpack the content of this function. The initialization is
identical to the one in BaseKernelOptimizer
, as we don’t really need
any new parameters (we may have added N_TENTATIVES
, but for the sake
of simplicity, we can keep it as is).
The optimize
function effectively starts from the initial kernel and
proceeds iteratively N_TENTATIVES times by changing a single operation
within the quantum circuit with a completely random operation. When the
result improves, indicating a lower cost for the kernel evaluator, the
new solution is accepted. Clearly, this is a rather inefficient way to
optimize the quantum kernel, and more sophisticated techniques are shown
below.
We can test this approach in a simple context.
N_FEATURES = 4
N_OPERATIONS = 5
N_QUBITS = 4
ansatz = Ansatz(n_features=N_FEATURES, n_qubits=N_QUBITS, n_operations=N_OPERATIONS)
ansatz.initialize_to_identity()
kernel = KernelFactory.create_kernel(ansatz, "Z" * N_QUBITS, KernelType.FIDELITY)
N_ELEMENTS_PER_CLASS = 20
iris = load_iris()
X = np.row_stack([iris.data[0:N_ELEMENTS_PER_CLASS], iris.data[50:50+N_ELEMENTS_PER_CLASS]])
y = np.array([0] * N_ELEMENTS_PER_CLASS + [1] * N_ELEMENTS_PER_CLASS)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=5454)
scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
ce = CenteredKernelAlignmentEvaluator()
print("The initial cost is:", ce.evaluate(kernel, None, X_train, y_train))
optimizer = RandomOptimizer(kernel, X_train, y_train, ce)
optimized_kernel = optimizer.optimize()
print("The final cost is:", ce.evaluate(optimized_kernel, None, X_train, y_train))
The initial cost is: -0.42299755542166695
Cost of the new solution: -0.28109278237003543
Cost of the new solution: -0.0961605171721972
Cost of the new solution: -0.27011666753748764
Cost of the new solution: -0.03337677439165289
Cost of the new solution: -0.2771358170950197
Cost of the new solution: -0.15948562200769945
Cost of the new solution: -0.07431221779515468
Cost of the new solution: -0.2751971110940814
Cost of the new solution: -0.27668879041145483
Cost of the new solution: -0.03265594563162022
The final cost is: -0.42299755542166695
The result of the optimization can be used exactly like any other kernel object.
model = SVC(kernel='precomputed')
K_train = optimized_kernel.build_kernel(X_train, X_train)
model.fit(K_train, y_train)
K_test = optimized_kernel.build_kernel(X_test, X_train)
y_pred = model.predict(K_test)
accuracy = np.sum(y_test == y_pred) / len(y_test)
print("Accuracy:", accuracy)
Accuracy: 0.3
Combinatorial optimization of a quantum kernel#
A plethora of techniques has been implemented in quask and can be used in different contexts according to the available computational resources and the volume of data to be analyzed. The functioning of these algorithms is detailed in [inc23].
Bayesian optimizer#
Bayesian optimization is the simplest and usually the most effective
technique to use. It is known to work best for low-dimensional problems
where the function to optimize is a black box costly to evaluate, which
is often the case in our context (although the optimization might not be
so low-dimensional). This approach is based on the library
scikit-optimize, which
needs to be installed separately from quask via the command
pip install scikit-optimize
.
Note that a KeyError ’’ might occur at this point if you have not configured a default backend for quask.
from quask.optimizer.bayesian_optimizer import BayesianOptimizer
print("The initial cost is:", ce.evaluate(kernel, None, X_train, y_train))
optimizer = BayesianOptimizer(kernel, X_train, y_train, ce)
optimized_kernel = optimizer.optimize(n_epochs=2, n_points=1, n_jobs=1)
print("The final cost is:", ce.evaluate(optimized_kernel, None, X_train, y_train))
The initial cost is: -0.42299755542166695
Epoch of training i=0
Epoch of training i=1
The final cost is: -0.22919895370100746
Meta-heuristic optimizer#
At the moment we only support Particle Swarm but we plan to support
other techniques, such as evolutionary (genetic) algorithms. This
approach is based on the library
opytimizer, which
needs to be installed separately from quask via the command
pip install opytimizer
.
Due to the extremely high computational cost, you can only use this technique for the smallest circuits (<2 operations, <2 qubits); in general is better to rely on the other techniques.
from quask.optimizer.metaheuristic_optimizer import MetaheuristicOptimizer
Greedy optimizer#
The greedy optimization tries any possible value for the first
operation, chooses the best one, and proceeds with the following
operations in a sequential fashion. Despite its simplicity, it is quite
an expensive technique. This approach is based on the library
mushroom-rl, which
needs to be installed separately from quask via the command
pip install mushroom_rl
.
from quask.optimizer.greedy_optimizer import GreedyOptimizer
Reinforcement learning optimizer#
Optimizes the quantum kernel by setting up a reinforcement learning
environment and using SARSA Lambda algorithm. This approach is based on
the library
mushroom-rl, which
needs to be installed separately from quask via the command
pip install mushroom_rl
.
from quask.optimizer.reinforcement_learning_optimizer import ReinforcementLearningOptimizer
References#
[llo20] Lloyd, S., Schuld, M., Ijaz, A., Izaac, J., & Killoran, N. (2020). Quantum embeddings for machine learning. arXiv preprint arXiv:2001.03622.
[inc23] Incudini, M., Lizzio Bosco, D., Martini, F., Grossi, M., Serra, G., and Di Pierro, A., “Automatic and effective discovery of quantum kernels”, arXiv e-prints, 2022. doi:10.48550/arXiv.2209.11144.
Note
Author’s note