How to optimize a quantum kernel ================================ .. code:: ipython3 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: 1. 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. 2. 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: .. code:: ipython3 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') .. code:: ipython3 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. .. code:: ipython3 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() .. code:: ipython3 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)) .. parsed-literal:: 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. .. code:: ipython3 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) .. parsed-literal:: 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*. .. code:: ipython3 from quask.optimizer.bayesian_optimizer import BayesianOptimizer .. code:: ipython3 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)) .. parsed-literal:: 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. .. code:: ipython3 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``. .. code:: ipython3 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``. .. code:: ipython3 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