572 lines
14 KiB
Markdown
572 lines
14 KiB
Markdown
# Quantum Machine Learning with PennyLane
|
|
|
|
## Table of Contents
|
|
1. [Hybrid Quantum-Classical Models](#hybrid-quantum-classical-models)
|
|
2. [Framework Integration](#framework-integration)
|
|
3. [Quantum Neural Networks](#quantum-neural-networks)
|
|
4. [Variational Classifiers](#variational-classifiers)
|
|
5. [Training and Optimization](#training-and-optimization)
|
|
6. [Data Encoding Strategies](#data-encoding-strategies)
|
|
7. [Transfer Learning](#transfer-learning)
|
|
|
|
## Hybrid Quantum-Classical Models
|
|
|
|
### Basic Hybrid Model
|
|
|
|
```python
|
|
import pennylane as qml
|
|
import numpy as np
|
|
|
|
dev = qml.device('default.qubit', wires=4)
|
|
|
|
@qml.qnode(dev)
|
|
def quantum_layer(inputs, weights):
|
|
# Encode classical data
|
|
for i, inp in enumerate(inputs):
|
|
qml.RY(inp, wires=i)
|
|
|
|
# Parameterized quantum circuit
|
|
for wire in range(4):
|
|
qml.RX(weights[wire], wires=wire)
|
|
|
|
for wire in range(3):
|
|
qml.CNOT(wires=[wire, wire+1])
|
|
|
|
# Measure
|
|
return [qml.expval(qml.PauliZ(i)) for i in range(4)]
|
|
|
|
# Use in classical workflow
|
|
inputs = np.array([0.1, 0.2, 0.3, 0.4])
|
|
weights = np.random.random(4)
|
|
output = quantum_layer(inputs, weights)
|
|
```
|
|
|
|
### Quantum-Classical Pipeline
|
|
|
|
```python
|
|
def hybrid_model(x, quantum_weights, classical_weights):
|
|
# Classical preprocessing
|
|
x_preprocessed = np.tanh(classical_weights['pre'] @ x)
|
|
|
|
# Quantum layer
|
|
quantum_out = quantum_layer(x_preprocessed, quantum_weights)
|
|
|
|
# Classical postprocessing
|
|
output = classical_weights['post'] @ quantum_out
|
|
|
|
return output
|
|
```
|
|
|
|
## Framework Integration
|
|
|
|
### PyTorch Integration
|
|
|
|
```python
|
|
import torch
|
|
import pennylane as qml
|
|
|
|
dev = qml.device('default.qubit', wires=2)
|
|
|
|
@qml.qnode(dev, interface='torch')
|
|
def quantum_circuit(inputs, weights):
|
|
qml.RY(inputs[0], wires=0)
|
|
qml.RY(inputs[1], wires=1)
|
|
qml.RX(weights[0], wires=0)
|
|
qml.RX(weights[1], wires=1)
|
|
qml.CNOT(wires=[0, 1])
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Create PyTorch layer
|
|
class QuantumLayer(torch.nn.Module):
|
|
def __init__(self, n_qubits):
|
|
super().__init__()
|
|
self.n_qubits = n_qubits
|
|
self.weights = torch.nn.Parameter(torch.randn(n_qubits))
|
|
|
|
def forward(self, x):
|
|
return torch.stack([quantum_circuit(xi, self.weights) for xi in x])
|
|
|
|
# Use in PyTorch model
|
|
class HybridModel(torch.nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.classical_1 = torch.nn.Linear(10, 2)
|
|
self.quantum = QuantumLayer(2)
|
|
self.classical_2 = torch.nn.Linear(1, 2)
|
|
|
|
def forward(self, x):
|
|
x = torch.relu(self.classical_1(x))
|
|
x = self.quantum(x)
|
|
x = self.classical_2(x.unsqueeze(1))
|
|
return x
|
|
|
|
# Training loop
|
|
model = HybridModel()
|
|
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
|
|
criterion = torch.nn.CrossEntropyLoss()
|
|
|
|
for epoch in range(100):
|
|
optimizer.zero_grad()
|
|
outputs = model(inputs)
|
|
loss = criterion(outputs, labels)
|
|
loss.backward()
|
|
optimizer.step()
|
|
```
|
|
|
|
### JAX Integration
|
|
|
|
```python
|
|
import jax
|
|
import jax.numpy as jnp
|
|
import pennylane as qml
|
|
|
|
dev = qml.device('default.qubit', wires=2)
|
|
|
|
@qml.qnode(dev, interface='jax')
|
|
def quantum_circuit(inputs, weights):
|
|
qml.RY(inputs[0], wires=0)
|
|
qml.RY(inputs[1], wires=1)
|
|
qml.RX(weights[0], wires=0)
|
|
qml.RX(weights[1], wires=1)
|
|
qml.CNOT(wires=[0, 1])
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# JAX-compatible training
|
|
@jax.jit
|
|
def loss_fn(weights, x, y):
|
|
predictions = quantum_circuit(x, weights)
|
|
return jnp.mean((predictions - y) ** 2)
|
|
|
|
# Compute gradients with JAX
|
|
grad_fn = jax.grad(loss_fn)
|
|
|
|
# Training
|
|
weights = jnp.array([0.1, 0.2])
|
|
for i in range(100):
|
|
grads = grad_fn(weights, x_train, y_train)
|
|
weights = weights - 0.01 * grads
|
|
```
|
|
|
|
### TensorFlow Integration
|
|
|
|
```python
|
|
import tensorflow as tf
|
|
import pennylane as qml
|
|
|
|
dev = qml.device('default.qubit', wires=2)
|
|
|
|
@qml.qnode(dev, interface='tf')
|
|
def quantum_circuit(inputs, weights):
|
|
qml.RY(inputs[0], wires=0)
|
|
qml.RY(inputs[1], wires=1)
|
|
qml.RX(weights[0], wires=0)
|
|
qml.RX(weights[1], wires=1)
|
|
qml.CNOT(wires=[0, 1])
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Keras layer
|
|
class QuantumLayer(tf.keras.layers.Layer):
|
|
def __init__(self, n_qubits):
|
|
super().__init__()
|
|
self.n_qubits = n_qubits
|
|
weight_init = tf.random_uniform_initializer()
|
|
self.weights = tf.Variable(
|
|
initial_value=weight_init(shape=(n_qubits,), dtype=tf.float32),
|
|
trainable=True
|
|
)
|
|
|
|
def call(self, inputs):
|
|
return tf.stack([quantum_circuit(x, self.weights) for x in inputs])
|
|
|
|
# Keras model
|
|
model = tf.keras.Sequential([
|
|
tf.keras.layers.Dense(2, activation='relu'),
|
|
QuantumLayer(2),
|
|
tf.keras.layers.Dense(2, activation='softmax')
|
|
])
|
|
|
|
model.compile(
|
|
optimizer=tf.keras.optimizers.Adam(0.01),
|
|
loss='sparse_categorical_crossentropy',
|
|
metrics=['accuracy']
|
|
)
|
|
|
|
model.fit(x_train, y_train, epochs=100, batch_size=32)
|
|
```
|
|
|
|
## Quantum Neural Networks
|
|
|
|
### Variational Quantum Circuit (VQC)
|
|
|
|
```python
|
|
from pennylane import numpy as np
|
|
|
|
dev = qml.device('default.qubit', wires=4)
|
|
|
|
def variational_block(weights, wires):
|
|
"""Single layer of variational circuit."""
|
|
for i, wire in enumerate(wires):
|
|
qml.RY(weights[i, 0], wires=wire)
|
|
qml.RZ(weights[i, 1], wires=wire)
|
|
|
|
for i in range(len(wires)-1):
|
|
qml.CNOT(wires=[wires[i], wires[i+1]])
|
|
|
|
@qml.qnode(dev)
|
|
def quantum_neural_network(inputs, weights):
|
|
# Encode inputs
|
|
for i, inp in enumerate(inputs):
|
|
qml.RY(inp, wires=i)
|
|
|
|
# Apply variational layers
|
|
n_layers = len(weights)
|
|
for layer_weights in weights:
|
|
variational_block(layer_weights, wires=range(4))
|
|
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Initialize weights
|
|
n_layers = 3
|
|
n_wires = 4
|
|
weights_shape = (n_layers, n_wires, 2)
|
|
weights = np.random.random(weights_shape, requires_grad=True)
|
|
```
|
|
|
|
### Quantum Convolutional Neural Network
|
|
|
|
```python
|
|
def conv_layer(weights, wires):
|
|
"""Quantum convolutional layer."""
|
|
n_wires = len(wires)
|
|
|
|
# Apply local unitaries
|
|
for i in range(n_wires):
|
|
qml.RY(weights[i], wires=wires[i])
|
|
|
|
# Nearest-neighbor entanglement
|
|
for i in range(0, n_wires-1, 2):
|
|
qml.CNOT(wires=[wires[i], wires[i+1]])
|
|
|
|
def pooling_layer(wires):
|
|
"""Quantum pooling (measure and discard)."""
|
|
measurements = []
|
|
for i in range(0, len(wires), 2):
|
|
measurements.append(qml.measure(wires[i]))
|
|
return measurements
|
|
|
|
@qml.qnode(dev)
|
|
def qcnn(inputs, weights):
|
|
# Encode image data
|
|
for i, pixel in enumerate(inputs):
|
|
qml.RY(pixel, wires=i)
|
|
|
|
# Convolutional layers
|
|
conv_layer(weights[0], wires=range(8))
|
|
pooling_layer(wires=range(0, 8, 2))
|
|
|
|
conv_layer(weights[1], wires=range(1, 8, 2))
|
|
pooling_layer(wires=range(1, 8, 4))
|
|
|
|
return qml.expval(qml.PauliZ(1))
|
|
```
|
|
|
|
### Quantum Recurrent Neural Network
|
|
|
|
```python
|
|
def qrnn_cell(x, hidden, weights):
|
|
"""Single QRNN cell."""
|
|
@qml.qnode(dev)
|
|
def cell(x, h, w):
|
|
# Encode input and hidden state
|
|
qml.RY(x, wires=0)
|
|
qml.RY(h, wires=1)
|
|
|
|
# Apply recurrent transformation
|
|
qml.RX(w[0], wires=0)
|
|
qml.RX(w[1], wires=1)
|
|
qml.CNOT(wires=[0, 1])
|
|
qml.RY(w[2], wires=1)
|
|
|
|
return qml.expval(qml.PauliZ(1))
|
|
|
|
return cell(x, hidden, weights)
|
|
|
|
def qrnn_sequence(sequence, weights):
|
|
"""Process sequence with QRNN."""
|
|
hidden = 0.0
|
|
outputs = []
|
|
|
|
for x in sequence:
|
|
hidden = qrnn_cell(x, hidden, weights)
|
|
outputs.append(hidden)
|
|
|
|
return outputs
|
|
```
|
|
|
|
## Variational Classifiers
|
|
|
|
### Binary Classification
|
|
|
|
```python
|
|
dev = qml.device('default.qubit', wires=2)
|
|
|
|
@qml.qnode(dev)
|
|
def variational_classifier(x, weights):
|
|
# Feature map
|
|
qml.RY(x[0], wires=0)
|
|
qml.RY(x[1], wires=1)
|
|
|
|
# Variational layers
|
|
for w in weights:
|
|
qml.RX(w[0], wires=0)
|
|
qml.RX(w[1], wires=1)
|
|
qml.CNOT(wires=[0, 1])
|
|
qml.RY(w[2], wires=0)
|
|
qml.RY(w[3], wires=1)
|
|
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
def cost_function(weights, X, y):
|
|
"""Binary cross-entropy loss."""
|
|
predictions = np.array([variational_classifier(x, weights) for x in X])
|
|
predictions = (predictions + 1) / 2 # Map [-1, 1] to [0, 1]
|
|
return -np.mean(y * np.log(predictions) + (1 - y) * np.log(1 - predictions))
|
|
|
|
# Training
|
|
n_layers = 2
|
|
n_params_per_layer = 4
|
|
weights = np.random.random((n_layers, n_params_per_layer), requires_grad=True)
|
|
|
|
opt = qml.GradientDescentOptimizer(stepsize=0.1)
|
|
for i in range(100):
|
|
weights = opt.step(lambda w: cost_function(w, X_train, y_train), weights)
|
|
```
|
|
|
|
### Multi-Class Classification
|
|
|
|
```python
|
|
@qml.qnode(dev)
|
|
def multiclass_circuit(x, weights):
|
|
# Encode input
|
|
for i, val in enumerate(x):
|
|
qml.RY(val, wires=i)
|
|
|
|
# Variational circuit
|
|
for layer_weights in weights:
|
|
for i, w in enumerate(layer_weights):
|
|
qml.RY(w, wires=i)
|
|
for i in range(len(x)-1):
|
|
qml.CNOT(wires=[i, i+1])
|
|
|
|
# Multiple outputs for classes
|
|
return [qml.expval(qml.PauliZ(i)) for i in range(3)]
|
|
|
|
def softmax(x):
|
|
exp_x = np.exp(x - np.max(x))
|
|
return exp_x / exp_x.sum()
|
|
|
|
def predict_class(x, weights):
|
|
logits = multiclass_circuit(x, weights)
|
|
return softmax(logits)
|
|
```
|
|
|
|
## Training and Optimization
|
|
|
|
### Gradient-Based Training
|
|
|
|
```python
|
|
# Automatic differentiation
|
|
@qml.qnode(dev, diff_method='backprop')
|
|
def circuit_backprop(x, weights):
|
|
# ... circuit definition
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Parameter shift rule
|
|
@qml.qnode(dev, diff_method='parameter-shift')
|
|
def circuit_param_shift(x, weights):
|
|
# ... circuit definition
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Finite differences
|
|
@qml.qnode(dev, diff_method='finite-diff')
|
|
def circuit_finite_diff(x, weights):
|
|
# ... circuit definition
|
|
return qml.expval(qml.PauliZ(0))
|
|
```
|
|
|
|
### Mini-Batch Training
|
|
|
|
```python
|
|
def batch_cost(weights, X_batch, y_batch):
|
|
predictions = np.array([variational_classifier(x, weights) for x in X_batch])
|
|
return np.mean((predictions - y_batch) ** 2)
|
|
|
|
# Mini-batch training
|
|
batch_size = 32
|
|
n_epochs = 100
|
|
|
|
for epoch in range(n_epochs):
|
|
for i in range(0, len(X_train), batch_size):
|
|
X_batch = X_train[i:i+batch_size]
|
|
y_batch = y_train[i:i+batch_size]
|
|
|
|
weights = opt.step(lambda w: batch_cost(w, X_batch, y_batch), weights)
|
|
```
|
|
|
|
### Learning Rate Scheduling
|
|
|
|
```python
|
|
def train_with_schedule(weights, X, y, n_epochs):
|
|
initial_lr = 0.1
|
|
decay = 0.95
|
|
|
|
for epoch in range(n_epochs):
|
|
lr = initial_lr * (decay ** epoch)
|
|
opt = qml.GradientDescentOptimizer(stepsize=lr)
|
|
|
|
weights = opt.step(lambda w: cost_function(w, X, y), weights)
|
|
|
|
if epoch % 10 == 0:
|
|
print(f"Epoch {epoch}, Loss: {cost_function(weights, X, y)}")
|
|
|
|
return weights
|
|
```
|
|
|
|
## Data Encoding Strategies
|
|
|
|
### Angle Encoding
|
|
|
|
```python
|
|
def angle_encoding(x, wires):
|
|
"""Encode features as rotation angles."""
|
|
for i, feature in enumerate(x):
|
|
qml.RY(feature, wires=wires[i])
|
|
```
|
|
|
|
### Amplitude Encoding
|
|
|
|
```python
|
|
def amplitude_encoding(x, wires):
|
|
"""Encode features as state amplitudes."""
|
|
# Normalize
|
|
x_norm = x / np.linalg.norm(x)
|
|
qml.MottonenStatePreparation(x_norm, wires=wires)
|
|
```
|
|
|
|
### Basis Encoding
|
|
|
|
```python
|
|
def basis_encoding(x, wires):
|
|
"""Encode binary features in computational basis."""
|
|
for i, bit in enumerate(x):
|
|
if bit:
|
|
qml.PauliX(wires=wires[i])
|
|
```
|
|
|
|
### IQP Encoding
|
|
|
|
```python
|
|
def iqp_encoding(x, wires):
|
|
"""Instantaneous Quantum Polynomial encoding."""
|
|
# Hadamard layer
|
|
for wire in wires:
|
|
qml.Hadamard(wires=wire)
|
|
|
|
# Encode features
|
|
for i, feature in enumerate(x):
|
|
qml.RZ(feature, wires=wires[i])
|
|
|
|
# Entanglement
|
|
for i in range(len(wires)-1):
|
|
qml.IsingZZ(x[i] * x[i+1], wires=[wires[i], wires[i+1]])
|
|
```
|
|
|
|
### Hamiltonian Encoding
|
|
|
|
```python
|
|
def hamiltonian_encoding(x, wires, time=1.0):
|
|
"""Encode via Hamiltonian evolution."""
|
|
# Build Hamiltonian from features
|
|
coeffs = x
|
|
obs = [qml.PauliZ(i) for i in wires]
|
|
|
|
H = qml.Hamiltonian(coeffs, obs)
|
|
|
|
# Apply time evolution
|
|
qml.ApproxTimeEvolution(H, time, n=10)
|
|
```
|
|
|
|
## Transfer Learning
|
|
|
|
### Pre-trained Quantum Model
|
|
|
|
```python
|
|
# Train on large dataset
|
|
pretrained_weights = train_quantum_model(large_dataset)
|
|
|
|
# Fine-tune on specific task
|
|
def fine_tune(pretrained_weights, small_dataset, n_epochs=50):
|
|
# Freeze early layers
|
|
frozen_weights = pretrained_weights[:-1] # All but last layer
|
|
trainable_weights = pretrained_weights[-1:] # Only last layer
|
|
|
|
@qml.qnode(dev)
|
|
def transfer_circuit(x, trainable):
|
|
# Apply frozen layers
|
|
for layer_w in frozen_weights:
|
|
variational_block(layer_w, wires=range(4))
|
|
|
|
# Apply trainable layer
|
|
variational_block(trainable, wires=range(4))
|
|
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Train only last layer
|
|
opt = qml.AdamOptimizer(stepsize=0.01)
|
|
for epoch in range(n_epochs):
|
|
trainable_weights = opt.step(
|
|
lambda w: cost_function(w, small_dataset),
|
|
trainable_weights
|
|
)
|
|
|
|
return np.concatenate([frozen_weights, trainable_weights])
|
|
```
|
|
|
|
### Classical-to-Quantum Transfer
|
|
|
|
```python
|
|
# Use classical network for feature extraction
|
|
import torch.nn as nn
|
|
|
|
classical_extractor = nn.Sequential(
|
|
nn.Conv2d(3, 16, 3),
|
|
nn.ReLU(),
|
|
nn.MaxPool2d(2),
|
|
nn.Flatten(),
|
|
nn.Linear(16*13*13, 4) # Output 4 features for quantum circuit
|
|
)
|
|
|
|
# Quantum classifier
|
|
@qml.qnode(dev)
|
|
def quantum_classifier(features, weights):
|
|
angle_encoding(features, wires=range(4))
|
|
variational_block(weights, wires=range(4))
|
|
return qml.expval(qml.PauliZ(0))
|
|
|
|
# Combined model
|
|
def hybrid_transfer_model(image, classical_weights, quantum_weights):
|
|
features = classical_extractor(image)
|
|
return quantum_classifier(features, quantum_weights)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Start simple** - Begin with small circuits and scale up
|
|
2. **Choose encoding wisely** - Match encoding to data structure
|
|
3. **Use appropriate interfaces** - Select interface matching your ML framework
|
|
4. **Monitor gradients** - Check for vanishing/exploding gradients (barren plateaus)
|
|
5. **Regularize** - Add L2 regularization to prevent overfitting
|
|
6. **Validate hardware compatibility** - Test on simulators before hardware
|
|
7. **Batch efficiently** - Use vectorization when possible
|
|
8. **Cache compilations** - Reuse compiled circuits for inference
|