02 - Training PennyLane VQC and Deep Ensemble¶
This notebook trains a small PennyLane variational classifier from scratch,
saves parameters to disk, and builds a DeepEnsemble of three independently
trained models.
In [1]:
Copied!
import pathlib
import numpy as np
import pennylane as qml
import pennylane.numpy as pnp
from quantumuq import DeepEnsemble, UQModel, wrap_qnode
from quantumuq.datasets.toy import make_moons
ARTIFACTS = pathlib.Path("examples/artifacts")
ARTIFACTS.mkdir(parents=True, exist_ok=True)
dataset = make_moons(n_samples=200, noise=0.1, random_state=0)
X, y = dataset.X, dataset.y
rng = np.random.default_rng(0)
perm = rng.permutation(len(X))
train_idx, test_idx = perm[:150], perm[150:]
X_train, y_train = X[train_idx], y[train_idx]
X_test, y_test = X[test_idx], y[test_idx]
import pathlib
import numpy as np
import pennylane as qml
import pennylane.numpy as pnp
from quantumuq import DeepEnsemble, UQModel, wrap_qnode
from quantumuq.datasets.toy import make_moons
ARTIFACTS = pathlib.Path("examples/artifacts")
ARTIFACTS.mkdir(parents=True, exist_ok=True)
dataset = make_moons(n_samples=200, noise=0.1, random_state=0)
X, y = dataset.X, dataset.y
rng = np.random.default_rng(0)
perm = rng.permutation(len(X))
train_idx, test_idx = perm[:150], perm[150:]
X_train, y_train = X[train_idx], y[train_idx]
X_test, y_test = X[test_idx], y[test_idx]
In [2]:
Copied!
n_qubits = 2
dev = qml.device("default.qubit", wires=n_qubits, shots=1000)
n_layers = 2
weights_shape = qml.StronglyEntanglingLayers.shape(n_layers=n_layers, n_wires=n_qubits)
@qml.qnode(dev)
def vqc(features, params):
qml.AngleEmbedding(features, wires=range(n_qubits))
qml.StronglyEntanglingLayers(params, wires=range(n_qubits))
return qml.probs(wires=range(n_qubits))
# Map 4 outcome probs (|00>,|01>,|10>,|11>) to 2 classes via first qubit
# class 0: P(0*), class 1: P(1*)
def probs_4_to_2(probs):
p = pnp.array(probs)
if p.ndim == 1:
return pnp.array([p[0] + p[1], p[2] + p[3]])
return pnp.stack([p[:, 0] + p[:, 1], p[:, 2] + p[:, 3]], axis=-1)
def train_one(seed: int, steps: int = 20, lr: float = 0.2):
rng_local = np.random.default_rng(seed)
params = 0.1 * pnp.array(rng_local.standard_normal(weights_shape))
def loss(p):
logits = []
for x in X_train:
logits.append(vqc(x, p))
probs = pnp.stack(logits)
probs = probs_4_to_2(probs)
probs = pnp.clip(probs, 1e-12, 1.0)
y_one_hot = pnp.eye(2)[y_train]
return -pnp.mean(pnp.sum(y_one_hot * pnp.log(probs), axis=1))
grad_fn = qml.grad(loss, argnum=0)
for _step in range(steps):
g = grad_fn(params)
params = params - lr * g
return params
params_list = [train_one(seed) for seed in range(3)]
for i, p in enumerate(params_list):
np.save(ARTIFACTS / f"pennylane_vqc_params_{i}.npy", p)
params_list = [np.load(ARTIFACTS / f"pennylane_vqc_params_{i}.npy") for i in range(3)]
n_qubits = 2
dev = qml.device("default.qubit", wires=n_qubits, shots=1000)
n_layers = 2
weights_shape = qml.StronglyEntanglingLayers.shape(n_layers=n_layers, n_wires=n_qubits)
@qml.qnode(dev)
def vqc(features, params):
qml.AngleEmbedding(features, wires=range(n_qubits))
qml.StronglyEntanglingLayers(params, wires=range(n_qubits))
return qml.probs(wires=range(n_qubits))
# Map 4 outcome probs (|00>,|01>,|10>,|11>) to 2 classes via first qubit
# class 0: P(0*), class 1: P(1*)
def probs_4_to_2(probs):
p = pnp.array(probs)
if p.ndim == 1:
return pnp.array([p[0] + p[1], p[2] + p[3]])
return pnp.stack([p[:, 0] + p[:, 1], p[:, 2] + p[:, 3]], axis=-1)
def train_one(seed: int, steps: int = 20, lr: float = 0.2):
rng_local = np.random.default_rng(seed)
params = 0.1 * pnp.array(rng_local.standard_normal(weights_shape))
def loss(p):
logits = []
for x in X_train:
logits.append(vqc(x, p))
probs = pnp.stack(logits)
probs = probs_4_to_2(probs)
probs = pnp.clip(probs, 1e-12, 1.0)
y_one_hot = pnp.eye(2)[y_train]
return -pnp.mean(pnp.sum(y_one_hot * pnp.log(probs), axis=1))
grad_fn = qml.grad(loss, argnum=0)
for _step in range(steps):
g = grad_fn(params)
params = params - lr * g
return params
params_list = [train_one(seed) for seed in range(3)]
for i, p in enumerate(params_list):
np.save(ARTIFACTS / f"pennylane_vqc_params_{i}.npy", p)
params_list = [np.load(ARTIFACTS / f"pennylane_vqc_params_{i}.npy") for i in range(3)]
In [3]:
Copied!
from quantumuq.core.predictors import Predictor
predictors: list[Predictor] = []
for p in params_list:
predictors.append(
wrap_qnode(vqc, task="classification", n_classes=2, params=p, postprocess=probs_4_to_2)
)
ensemble_method = DeepEnsemble(predictors)
base = predictors[0]
uq_model = UQModel(base, ensemble_method)
dist = uq_model.predict_dist(X_test)
print("Ensemble predictive mean shape:", dist.mean.shape)
print("Ensemble predictive std shape:", dist.std.shape)
from quantumuq.core.predictors import Predictor
predictors: list[Predictor] = []
for p in params_list:
predictors.append(
wrap_qnode(vqc, task="classification", n_classes=2, params=p, postprocess=probs_4_to_2)
)
ensemble_method = DeepEnsemble(predictors)
base = predictors[0]
uq_model = UQModel(base, ensemble_method)
dist = uq_model.predict_dist(X_test)
print("Ensemble predictive mean shape:", dist.mean.shape)
print("Ensemble predictive std shape:", dist.std.shape)
Ensemble predictive mean shape: (50, 2) Ensemble predictive std shape: (50, 2)
What this ensemble is doing¶
- Each call to
train_onetrains a separate PennyLane VQC with a different random seed. - We wrap each trained QNode with
wrap_qnode, so it implements thePredictorprotocol. DeepEnsembletreats each predictor as one sample in an ensemble and stacks their predictions.- The resulting
PredictiveDistributioncaptures epistemic uncertainty from model diversity.