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
Post a Comment