Quantum Machine Learning: QNNs, VQCs, Kernels | Quantum Computing Hub

Quantum Machine Learning (QML) explores the intersection of quantum computing and machine learning. This guide covers quantum neural networks (QNNs), variational quantum classifiers (VQCs), quantum kernels, and hybrid quantum-classical training pipelines with PennyLane and Qiskit ML. 1. QML Fundamentals 1.1 Quantum Data Encoding # encoding.py import pennylane as qml from pennylane import numpy as np import cirq # 1. Amplitude encoding: |ψ⟩ = Σ x_i |i⟩ def amplitude_encode(x, wires): """Encode normalized vector into amplitudes""" # x must be normalized: Σ|x_i|² = 1 qml.AmplitudeEmbedding(features=x, wires=wires, normalize=True) # 2. Angle encoding: R(θ) rotations def angle_encode(x, wires): """Encode each feature as rotation angle""" for i, val in enumerate(x): qml.RX(val, wires=wires[i]) # 3. Dense angle encoding: multiple rotations per qubit def dense_angle_encode(x, wires): """Encode with multiple rotation gates per feature""" for i, val in enumerate(x): qml.RX(val, wires=wires[i]) qml.RY(val, wires=wires[i]) qml.RZ(val, wires=wires[i]) # 4. IQP encoding: data-dependent diagonal gates def iqp_encode(x, wires): """Instantaneous Quantum Polynomial encoding""" n = len(wires) # First layer: Hadamards for w in wires: qml.Hadamard(w) # Diagonal gates with data for i in range(n): qml.RZ(x[i], wires=wires[i]) # Entangling layer for i in range(n-1): qml.CZ(wires=[wires[i], wires[i+1]]) for i in range(n): qml.RZ(x[i], wires=wires[i]) # Final Hadamards for w in wires: qml.Hadamard(w) # 5. Trainable embedding: variational encoder def trainable_embedding(x, wires, params): """Learnable feature map""" for i, w in enumerate(wires): qml.RX(x[i] * params[0], wires=w) qml.RY(x[i] * params[1], wires=w) qml.RZ(x[i] * params[2], wires=w) 1.2 PennyLane Device Setup # devices.py import pennylane as qml # Default qubit simulator (fast, exact) dev_exact = qml.device('default.qubit', wires=10) # Mixed state simulator (for noise) dev_mixed = qml.device('default.mixed', wires=10) # Qiskit Aer simulator dev_aer = qml.device('qiskit.aer', wires=10, shots=1000) # Cirq simulator dev_cirq = qml.device('cirq.simulator', wires=10) # TensorFlow Quantum (TFQ) # dev_tfq = qml.device('tfq.simulator', wires=10) # Lightning GPU (fast GPU simulator) dev_lightning = qml.device('lightning.gpu', wires=10) # Hardware devices (require credentials) # dev_ibm = qml.device('qiskit.remote', wires=127, backend='ibm_sherbrooke') # dev_google = qml.device('cirq.pasqal', wires=100) # Use with @qml.qnode @qml.qnode(dev_exact, diff_method="parameter-shift") def circuit(params): # ... circuit definition return qml.expval(qml.PauliZ(0)) 2. Variational Quantum Classifiers (VQCs) 2.1 Basic VQC Architecture # vqc_basic.py import pennylane as qml from pennylane import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split # Generate dataset X, y = make_classification(n_samples=200, n_features=4, n_classes=2, random_state=42) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Normalize features to [0, π] X_train = (X_train - X_train.min()) / (X_train.max() - X_train.min()) * np.pi X_test = (X_test - X_test.min()) / (X_test.max() - X_test.min()) * np.pi n_qubits = 4 dev = qml.device('default.qubit', wires=n_qubits) def vqc_circuit(params, x): """Variational quantum classifier circuit""" # Data encoding for i in range(n_qubits): qml.RX(x[i], wires=i) qml.RY(x[i], wires=i) # Variational layers n_layers = params.shape[0] for layer in range(n_layers): # Rotation layer for i in range(n_qubits): qml.RX(params[layer, i, 0], wires=i) qml.RY(params[layer, i, 1], wires=i) qml.RZ(params[layer, i, 2], wires=i) # Entanglement layer for i in range(n_qubits - 1): qml.CNOT(wires=[i, i + 1]) qml.CNOT(wires=[n_qubits - 1, 0]) # Ring topology # Measurement return qml.expval(qml.PauliZ(0)) @qml.qnode(dev, diff_method="parameter-shift") def vqc(params, x): return vqc_circuit(params, x) # Cost function def cost(params, X, y): predictions = [vqc(params, x) for x in X] # Convert expectation values to probabilities probs = [(p + 1) / 2 for p in predictions] # Binary cross-entropy loss = -np.mean(y * np.log(probs) + (1 - y) * np.log(1 - np.array(probs))) return loss # Initialize parameters n_layers = 3 params = np.random.randn(n_layers, n_qubits, 3) * 0.1 # Optimizer opt = qml.AdamOptimizer(0.01) # Training loop for epoch in range(50): params, loss = opt.step_and_cost(lambda p: cost(p, X_train, y_train), params) if epoch % 10 == 0: train_acc = accuracy(params, X_train, y_train) test_acc = accuracy(params, X_test, y_test) print(f"Epoch {epoch}: Loss={loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") def accuracy(params, X, y): preds = [vqc(params, x) for x in X] preds_binary = [1 if p > 0 else 0 for p in preds] return np.mean(np.array(preds_binary) == y) * 100 2.2 Advanced VQC with Data Re-uploading # vqc_reuploading.py def vqc_reuploading(params, x, n_layers=4): """VQC with data re-uploading for enhanced expressivity""" n_qubits = len(x) # Initial encoding for i in range(n_qubits): qml.RX(x[i], wires=i) for layer in range(n_layers): # Variational rotations for i in range(n_qubits): qml.RX(params[layer, i, 0], wires=i) qml.RY(params[layer, i, 1], wires=i) qml.RZ(params[layer, i, 2], wires=i) # Entanglement for i in range(n_qubits - 1): qml.CZ(wires=[i, i + 1]) # Data re-uploading for i in range(n_qubits): qml.RX(x[i] * params[layer, i, 3], wires=i) qml.RY(x[i] * params[layer, i, 4], wires=i) return qml.expval(qml.PauliZ(0)) 2.3 Qiskit ML VQC # qiskit_ml_vqc.py # pip install qiskit-machine-learning from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.neural_networks import SamplerQNN from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes from qiskit.algorithms.optimizers import COBYLA, SPSA from qiskit.primitives import Sampler from qiskit.utils import algorithm_globals # Set random seed algorithm_globals.random_seed = 42 # Feature map (data encoding) feature_map = ZZFeatureMap(feature_dimension=4, reps=2, entanglement='linear') # Ansatz (variational circuit) ansatz = RealAmplitudes(num_qubits=4, reps=3, entanglement='linear') # Quantum neural network sampler = Sampler() qnn = SamplerQNN( circuit=ansatz.compose(feature_map), sampler=sampler, input_params=feature_map.parameters, weight_params=ansatz.parameters, interpret=lambda x: x, # Raw measurement outputs output_shape=2 # Binary classification ) # VQC classifier vqc = VQC( neural_network=qnn, optimizer=COBYLA(maxiter=100), initial_point=np.random.rand(ansatz.num_parameters) * 2 * np.pi ) # Train vqc.fit(X_train, y_train) # Predict predictions = vqc.predict(X_test) score = vqc.score(X_test, y_test) print(f"Test accuracy: {score:.2f}") 3. Quantum Kernels 3.1 Quantum Kernel Estimation # quantum_kernel.py from qiskit_machine_learning.kernels import QuantumKernel from qiskit.circuit.library import ZZFeatureMap from qiskit.primitives import Sampler from sklearn.svm import SVC from sklearn.metrics import accuracy_score # Quantum feature map feature_map = ZZFeatureMap(feature_dimension=4, reps=2, entanglement='full') # Quantum kernel sampler = Sampler() qkernel = QuantumKernel( feature_map=feature_map, sampler=sampler ) # Compute kernel matrix K_train = qkernel.evaluate(X_train) K_test = qkernel.evaluate(X_test, X_train) # Classical SVM with quantum kernel svm = SVC(kernel='precomputed') svm.fit(K_train, y_train) y_pred = svm.predict(K_test) print(f"Quantum Kernel SVM accuracy: {accuracy_score(y_test, y_pred):.2f}") # Custom kernel with PennyLane import pennylane as qml dev = qml.device('default.qubit', wires=4) @qml.qnode(dev) def kernel_circuit(x1, x2): # Encode x1 for i in range(4): qml.RX(x1[i], wires=i) qml.RY(x1[i], wires=i) # Encode x2 adjoint for i in range(4): qml.adjoint(qml.RY)(x2[i], wires=i) qml.adjoint(qml.RX)(x2[i], wires=i) # Measure overlap return qml.probs(wires=range(4)) def quantum_kernel_pennylane(x1, x2): """Quantum kernel as |⟨φ(x1)|φ(x2)⟩|²""" probs = kernel_circuit(x1, x2) # Overlap with |0...0⟩ return probs[0]**2 # Kernel matrix def compute_kernel_matrix(X1, X2, kernel_fn): K = np.zeros((len(X1), len(X2))) for i, x1 in enumerate(X1): for j, x2 in enumerate(X2): K[i, j] = kernel_fn(x1, x2) return K 3.2 Projected Quantum Kernels # projected_kernel.py # Projected quantum kernels for improved trainability # See: "Projected Quantum Kernels" (Huang et al.) def projected_quantum_kernel(x1, x2, projector_params): """ Projected kernel: K(x1, x2) = ⟨φ(x1)|P|φ(x2)⟩ where P is a learnable projector """ # Trainable projector: P = Σ c_i |ψ_i⟩⟨ψ_i| pass # Kernel alignment optimization def kernel_alignment(K, y): """Maximize kernel alignment with labels""" Y = np.outer(y, y) alignment = np.trace(K @ Y) / (np.linalg.norm(K) * np.linalg.norm(Y)) return alignment 4. Quantum Neural Networks (QNNs) 4.1 Dressed Quantum Circuits # qnn_dressed.py # Dressed quantum neural network # Classical pre/post-processing + quantum circuit import torch import pennylane as qml class DressedQNN(torch.nn.Module): def __init__(self, n_qubits, n_layers, n_classes): super().__init__() self.n_qubits = n_qubits self.n_layers = n_layers # Classical pre-processing self.pre_net = torch.nn.Sequential( torch.nn.Linear(784, 256), # e.g., MNIST input torch.nn.ReLU(), torch.nn.Linear(256, n_qubits * 3), # Angle encoding params ) # Quantum circuit parameters self.q_params = torch.nn.Parameter( torch.randn(n_layers, n_qubits, 3) * 0.1 ) # Classical post-processing self.post_net = torch.nn.Sequential( torch.nn.Linear(1, 64), # Single expectation value torch.nn.ReLU(), torch.nn.Linear(64, n_classes) ) self.dev = qml.device('default.qubit', wires=n_qubits) @qml.qnode(self.dev, interface='torch', diff_method='parameter-shift') def qcircuit(inputs, weights): # Angle encoding from classical net for i in range(n_qubits): qml.RX(inputs[i], wires=i) qml.RY(inputs[i], wires=i) # Variational layers for layer in range(n_layers): for i in range(n_qubits): qml.RX(weights[layer, i, 0], wires=i) qml.RY(weights[layer, i, 1], wires=i) qml.RZ(weights[layer, i, 2], wires=i) # Entanglement for i in range(n_qubits - 1): qml.CNOT(wires=[i, i + 1]) return qml.expval(qml.PauliZ(0)) self.qcircuit = qcircuit def forward(self, x): # Classical pre-processing q_inputs = self.pre_net(x) q_inputs = q_inputs.view(-1, self.n_qubits * 3)[:, :self.n_qubits] # First n_qubits # Quantum circuit q_out = [] for i in range(x.shape[0]): out = self.qcircuit(q_inputs[i], self.q_params) q_out.append(out) q_out = torch.stack(q_out).unsqueeze(1) # Classical post-processing return self.post_net(q_out) 4.2 Quantum Convolutional Neural Networks (QCNNs) # qcnn.py # Quantum Convolutional Neural Network # For quantum data or classical data with hierarchical structure def qcnn_layer(qubits, params, layer_idx): """Single QCNN layer: convolution + pooling""" n = len(qubits) # Convolution: parameterized unitaries on neighboring qubits for i in range(0, n - 1, 2): qml.IsingXX(params[layer_idx, i, 0], wires=[qubits[i], qubits[i + 1]]) qml.IsingYY(params[layer_idx, i, 1], wires=[qubits[i], qubits[i + 1]]) qml.IsingZZ(params[layer_idx, i, 2], wires=[qubits[i], qubits[i + 1]]) # Pooling: measure half the qubits, apply corrections # In simulation, we trace out measured qubits # On hardware: measure and apply conditional rotations return qubits[::2] # Keep even qubits def qcnn_circuit(params, x): """Full QCNN for image classification""" n_qubits = 8 # e.g., 8x8 image → 8 qubits after pooling qubits = list(range(n_qubits)) # Encode image patches for i, q in enumerate(qubits): qml.RX(x[i], wires=q) qml.RY(x[i], wires=q) n_layers = params.shape[0] for layer in range(n_layers): qubits = qcnn_layer(qubits, params, layer) # Final measurement return qml.expval(qml.PauliZ(qubits[0])) 5. Hybrid Training & Gradient Methods 5.1 Parameter Shift Rule # gradients.py # Parameter shift rule for exact gradients # ∂⟨O⟩/∂θ = (⟨O⟩_{θ+π/2} - ⟨O⟩_{θ-π/2}) / 2 def parameter_shift_gradient(qnode, params, param_idx): """Compute gradient w.r.t single parameter using parameter shift""" params_plus = params.copy() params_minus = params.copy() params_plus[param_idx] += np.pi / 2 params_minus[param_idx] -= np.pi / 2 exp_plus = qnode(params_plus) exp_minus = qnode(params_minus) return (exp_plus - exp_minus) / 2 # PennyLane automatic differentiation @qml.qnode(dev, diff_method="parameter-shift") def circuit(params): # ... circuit return qml.expval(qml.PauliZ(0)) # Gradient via autodiff grad_fn = qml.grad(circuit) grads = grad_fn(params) # For hardware: finite differences def finite_diff_gradient(qnode, params, param_idx, eps=1e-3): params_plus = params.copy() params_minus = params.copy() params_plus[param_idx] += eps params_minus[param_idx] -= eps return (qnode(params_plus) - qnode(params_minus)) / (2 * eps) 5.2 Barren Plateaus & Mitigation # barren_plateaus.py # Barren plateaus: gradients vanish exponentially with qubits # Mitigation strategies: # 1. Local cost functions (not global) def local_cost_fn(params, X, y): """Sum of local observables instead of global""" local_observables = [qml.PauliZ(i) for i in range(n_qubits)] costs = [] for obs in local_observables: @qml.qnode(dev) def local_circuit(params, x):

Comments

Popular posts from this blog

Firmware Testing Strategies: Hardware-in-the-Loop, Simulation, and CI/CD Pipelines | Embedded Systems Hub

IoT Protocols Compared: MQTT, CoAP, HTTP/2, WebSocket - Performance, Overhead, and Use Cases | Embedded Systems Hub

Embedded Security: Secure Boot, ARM TrustZone, and OTA Update Integrity | Embedded Systems Hub