Source code for pyriemann_qiskit.autoencoders

"""Quantum autoencoder implementations.

This module provides quantum autoencoder classes for dimensionality reduction
and denoising of quantum states, compatible with scikit-learn transformers.
"""

import logging

import numpy as np
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit.circuit.library import RealAmplitudes
from qiskit.quantum_info import Statevector
from qiskit_algorithms.optimizers import SPSA
from qiskit_algorithms.utils import algorithm_globals
from qiskit_machine_learning.circuit.library import raw_feature_vector
from qiskit_machine_learning.neural_networks import SamplerQNN
from sklearn.base import TransformerMixin


def _ansatz(num_qubits):
    return RealAmplitudes(num_qubits, reps=5)


def _auto_encoder_circuit(num_latent, num_trash):
    qr = QuantumRegister(num_latent + 2 * num_trash + 1, "q")
    cr = ClassicalRegister(1, "c")
    circuit = QuantumCircuit(qr, cr)
    circuit.compose(
        _ansatz(num_latent + num_trash), range(0, num_latent + num_trash), inplace=True
    )
    circuit.barrier()
    auxiliary_qubit = num_latent + 2 * num_trash

    # swap test
    circuit.h(auxiliary_qubit)
    for i in range(num_trash):
        circuit.cswap(auxiliary_qubit, num_latent + i, num_latent + num_trash + i)

    circuit.h(auxiliary_qubit)
    circuit.measure(auxiliary_qubit, cr[0])
    return circuit


[docs] class BasicQnnAutoencoder(TransformerMixin): """Quantum denoising This class implements a quantum auto encoder. The implementation was adapted from [1]_, to be compatible with scikit-learn. Parameters ---------- num_latent : int, default=3 The number of qubits in the latent space. num_trash : int, default=2 The number of qubits in the trash space. opt : Optimizer, default=SPSA(maxiter=100, blocking=True) The classical optimizer to use. callback : Callable[int, double], default=None An additional callback for the optimizer. The first parameter is the number of cost evaluation call. The second parameter is the cost. Notes ----- .. versionadded:: 0.3.0 Attributes ---------- costs_ : list The values of the cost function. fidelities_ : list, shape (n_samples,) fidelities (one fidelity for each sample). References ---------- .. [1] \ https://qiskit-community.github.io/qiskit-machine-learning/tutorials/12_quantum_autoencoder.html .. [2] A. Mostafa et al., 2024 \ ‘Quantum Denoising in the Realm of Brain-Computer Interfaces: A Preliminary Study’, https://hal.science/hal-04501908 """
[docs] def __init__( self, num_latent=3, num_trash=2, opt=SPSA(maxiter=100, blocking=True), callback=None, ): self.num_latent = num_latent self.num_trash = num_trash self.opt = opt self.callback = callback
def _log(self, msg): logging.info(f"[BasicQnnAutoencoder] {msg}") def _get_transformer(self): # encoder transformer = QuantumCircuit(self.n_qubits) transformer = transformer.compose(self._feature_map) ansatz_qc = _ansatz(self.n_qubits) transformer = transformer.compose(ansatz_qc) transformer.barrier() # trash space for i in range(self.num_trash): transformer.reset(self.num_latent + i) transformer.barrier() # decoder transformer = transformer.compose(ansatz_qc.inverse()) self._transformer = transformer return transformer def _compute_fidelities(self, X): fidelities = [] for x in X: param_values = np.concatenate((x, self._opt_result.x)) output_qc = self._transformer.assign_parameters(param_values) output_state = Statevector(output_qc).data original_qc = self._feature_map.assign_parameters(x) original_state = Statevector(original_qc).data fidelity = np.sqrt(np.dot(original_state.conj(), output_state) ** 2) fidelities.append(fidelity.real) return fidelities @property def n_qubits(self): return self.num_latent + self.num_trash def fit(self, X, _y=None, **kwargs): """Fit the quantum autoencoder to the data. Trains the autoencoder circuit parameters by minimizing the cost function that measures reconstruction fidelity. The number of features must match 2^n_qubits where n_qubits = num_trash + num_latent. Parameters ---------- X : ndarray, shape (n_samples, n_features) Training data. n_features must equal 2^n_qubits. _y : None Ignored. Present for scikit-learn compatibility. **kwargs : dict Additional keyword arguments (ignored). Returns ------- self : BasicQnnAutoencoder The fitted autoencoder instance. """ _, n_features = X.shape self.costs_ = [] self.fidelities_ = [] self._iter = 0 self._log( f"raw feature size: {2 ** self.n_qubits} and feature size: {n_features}" ) assert 2**self.n_qubits == n_features self._feature_map = raw_feature_vector(2**self.n_qubits) self._auto_encoder = _auto_encoder_circuit(self.num_latent, self.num_trash) qc = QuantumCircuit(self.num_latent + 2 * self.num_trash + 1, 1) qc = qc.compose(self._feature_map, range(self.n_qubits)) qc = qc.compose(self._auto_encoder) qnn = SamplerQNN( circuit=qc, input_params=self._feature_map.parameters, weight_params=self._auto_encoder.parameters, interpret=lambda x: x, output_shape=2, ) def cost_func(params_values): self._iter += 1 if self._iter % 10 == 0: self._log(f"Iteration {self._iter}") probabilities = qnn.forward(X, params_values) cost = np.sum(probabilities[:, 1]) / X.shape[0] self.costs_.append(cost) if self.callback: self.callback(self._iter, cost) return cost initial_point = algorithm_globals.random.random( self._auto_encoder.num_parameters ) self._opt_result = self.opt.minimize(fun=cost_func, x0=initial_point) # encoder/decoder circuit self._transformer = self._get_transformer() # compute fidelity self.fidelities_ = self._compute_fidelities(X) self._log(f"Mean fidelity: {np.mean(self.fidelities_)}") return self def transform(self, X, **kwargs): """Transform data using the fitted autoencoder. Applies the encoder-decoder circuit to reconstruct the input data through the latent space representation. Parameters ---------- X : ndarray, shape (n_samples, n_features) Data to transform. n_features must equal 2^n_qubits. **kwargs : dict Additional keyword arguments (ignored). Returns ------- X_transformed : ndarray, shape (n_samples, n_features) Reconstructed data after encoding and decoding. """ _, n_features = X.shape outputs = [] for x in X: param_values = np.concatenate((x, self._opt_result.x)) output_qc = self._transformer.assign_parameters(param_values) output_sv = Statevector(output_qc).data output_sv = np.reshape(np.abs(output_sv) ** 2, n_features) outputs.append(output_sv) return np.array(outputs)