Quantum Neural Networks¶
QNN is Divi’s first-class primitive for
quantum machine learning. It trains the weights of a parameterized circuit over
a batch of classical feature vectors. By default it minimizes the expectation
value of a chosen observable averaged across the batch (unsupervised); pass
labels to train a supervised loss instead (see Supervised training).
Unlike PennyLane & Qiskit Integration (where you bring your own circuit via
CustomVQA), QNN builds the circuit for
you from two composable pieces:
a
FeatureMap— the data layer. It encodes each feature vector into circuit parameters that are bound from data, never optimized.an
Ansatz— the trainable layer. Any Divi ansatz works; its parameters are the weights the optimizer updates.
Data parameters and weight parameters are kept disjoint. At every optimization step, the cost observable is evaluated on the composed circuit once per sample, the per-sample expectation values are reduced along the sample axis (mean by default), and a single scalar loss per weight candidate is handed to the optimizer. The optimizer never sees the data axis — the fan-out and reduction happen inside the pipeline’s data-binding stage.
Quick Start¶
Train a tiny 2-qubit QNN on a four-sample toy dataset (unsupervised — it minimizes the observable; the Supervised training section adds labels):
import numpy as np
from qiskit.circuit.library import CXGate, RYGate, RZGate
from divi.qprog import QNN, AngleEmbedding, GenericLayerAnsatz
from divi.qprog.optimizers import ScipyMethod, ScipyOptimizer
from divi.backends import MaestroSimulator
n_qubits = 2
# Two loose clusters: (n_samples, n_features). n_features must match the
# feature map's parameter count — AngleEmbedding uses one feature per qubit.
X_train = np.array([[0.1, 0.2], [0.3, 0.5], [2.0, 2.1], [2.3, 2.4]])
program = QNN(
n_qubits=n_qubits,
feature_map=AngleEmbedding(rotation="Y"),
ansatz=GenericLayerAnsatz(
gate_sequence=[RYGate, RZGate],
entangler=CXGate,
entangling_layout="linear",
),
feature_batch=X_train,
n_layers=2,
loss_reduction="mean",
optimizer=ScipyOptimizer(method=ScipyMethod.COBYLA),
max_iterations=5,
backend=MaestroSimulator(),
seed=1997,
)
# QNNs return their answer as the trained weights, so the solution-sampling
# "final computation" step that combinatorial VQAs use is unnecessary.
program.run(perform_final_computation=False)
print(f"Best loss: {program.best_loss:.4f}")
best_weights = program.best_params
The optimizer view (best_params)
contains only the weights —
ansatz.n_params_per_layer(n_qubits) * n_layers of them. The feature columns
never appear in the parameter vector.
Inference¶
After run(), score a fresh feature batch with
predict(). It binds each row’s features with the
trained weights, estimates the cost observable per sample (the same shot-based
readout the loss optimizes), and returns the sign as a class label in
{-1, +1}:
X_new = np.array([[0.15, 0.25], [2.1, 2.2]])
labels = program.predict(X_new, params=trained_weights) # {-1, +1} per row
scores = program.predict(X_new, params=trained_weights, return_scores=True) # raw ⟨H⟩ per row
When called after training, params defaults to best_params. Pass
return_scores=True to predict() for the
continuous scores if you want to apply your own decision threshold. The same method is
available on CustomVQA when it has a data axis.
Feature Maps¶
A feature map encodes a feature vector into a parametric circuit. Built-in maps:
AngleEmbedding— one single-qubit rotation per feature,R(x_i)on qubiti. Choose the axis withrotation="X" | "Y" | "Z"(default"Y").ZZFeatureMap— the ZZ entangling encoding of Havlíček et al. (Hadamards,RZ(2·x_i)per qubit, thenRZZ(2·(π−x_i)(π−x_j))over an entangling layout). Select the pair pattern withentangling_layout="linear" | "circular" | "all-to-all". Requires at least two qubits.
Both built-in maps consume one feature per qubit, so feature_batch must
have n_qubits columns. More generally, its column count must equal
feature_map.n_params(n_qubits); a mismatch raises ValueError at
construction. Feature maps are not layered — a single application encodes the
vector once. For data re-uploading (interleaving encoding with
variational layers), subclass FeatureMap and
implement n_params and build.
The Observable and the Loss¶
observable is the operator whose expectation value is minimized, as a
SparsePauliOp acting on n_qubits qubits. It
defaults to the all-qubit parity Z ⊗ Z ⊗ … ⊗ Z, which gives a single
readout in [-1, 1] informed by every qubit. Pass your own to change the
readout, e.g. SparsePauliOp.from_list([("ZI", 1.0)]) to read a single qubit.
loss_reduction controls how the per-sample expectation values collapse into
the scalar the optimizer sees:
"mean"(default) — average over the batch."sum"— total over the batch.a callable
np.ndarray (n_samples,) -> float— any custom aggregation.
Supervised training¶
Pass labels (shape (n_samples,), aligned with feature_batch’s rows)
to train a supervised loss. Each sample’s prediction — the cost observable’s
expectation value, in [-1, 1] for the default parity observable — is
compared to its label by loss_fn, and those per-sample losses are then
aggregated by loss_reduction. The default loss_fn="squared_error" with
the default "mean" reduction is mean-squared error; encode labels to match
the readout range (e.g. -1 / +1 for the parity observable):
import numpy as np
from qiskit.circuit.library import CXGate, RYGate, RZGate
from divi.qprog import QNN, AngleEmbedding, GenericLayerAnsatz
from divi.qprog.optimizers import ScipyMethod, ScipyOptimizer
from divi.backends import MaestroSimulator
X_train = np.array([[0.1, 0.2], [0.3, 0.5], [2.0, 2.1], [2.3, 2.4]])
y_train = np.array([-1.0, -1.0, 1.0, 1.0]) # one label per sample
clf = QNN(
n_qubits=2,
feature_map=AngleEmbedding(rotation="Y"),
ansatz=GenericLayerAnsatz(
gate_sequence=[RYGate, RZGate],
entangler=CXGate,
entangling_layout="linear",
),
feature_batch=X_train,
labels=y_train,
loss_fn="squared_error", # default; or a callable (pred, label) -> float
n_layers=2,
optimizer=ScipyOptimizer(method=ScipyMethod.COBYLA),
max_iterations=5,
backend=MaestroSimulator(),
seed=1997,
)
clf.run(perform_final_computation=False)
A supervised loss requires a single-readout observable (one prediction per
sample), so keep the default parity observable or another single-qubit/parity
operator. The same labels / loss_fn pair is available on
CustomVQA’s data-binding path
(PennyLane & Qiskit Integration) when you bring your own circuit.
When to use QNN vs CustomVQA¶
Reach for
QNNwhen you want a curated feature-map + ansatz workflow and Divi to compose and bind the circuit for you.Reach for
CustomVQA(see PennyLane & Qiskit Integration) when you already have a PennyLane or Qiskit circuit and want full control over its structure, marking data parameters yourself withdata_arg/arg_shapes/data_param_indices.
Next Steps¶
PennyLane & Qiskit Integration — bring-your-own-circuit data binding with
CustomVQAOptimizers — optimizer choice and early stopping
Algorithms — full
QNN,FeatureMap, andAnsatzAPIqnn_classifier.py — the runnable end-to-end tutorial