Variational Quantum Classifier With Readout Error Folded Into The Loss
Written by
Zed Qubit
Why I cared about this problem
I was trying to build a tiny variational quantum classifier (a quantum model whose circuit parameters get trained using gradient-based optimization) to classify simple data. The weird part was consistency: my validation accuracy would jump around a lot even when training seemed stable.
After a weekend of debugging, I realized the culprit wasn’t my optimizer or my dataset—it was readout error. In real quantum hardware (and even in some simulators that mimic hardware), the measurement step sometimes flips a bit: a 0 might be read as 1, or vice versa. That means the classifier’s output probabilities are “distorted,” but my loss function was pretending they weren’t.
So I built a version where the readout error model is folded directly into the loss, so training optimizes for what the device actually measures, not for the idealized quantum state.
The core idea: fold readout error into the loss
A quick vocabulary check
- Readout error: measurement noise that flips classical outcomes.
- Confusion matrix: a small table that maps the true intended bit value distribution to the measured one.
- Loss: a scalar number the optimizer tries to minimize.
What I changed
Normally, with a binary classifier, I would compute probabilities from the quantum circuit and directly plug them into a loss like cross-entropy.
Instead, I do this:
- Compute ideal probabilities from the quantum circuit:
[ p_\text{ideal} = [p(0), p(1)] ] - Apply a readout confusion matrix (C) to get the measured probabilities:
[ p_\text{measured} = C \cdot p_\text{ideal} ] - Compute the loss using (p_\text{measured}).
This makes the training objective match the experimental reality.
A concrete setup (binary classification, 1 qubit)
I’ll use:
- A toy dataset: points on a line, thresholded into two classes.
- A 1-qubit variational circuit:
- Encode a single feature into a rotation angle.
- Use trainable parameters for a couple of rotations.
- A simple symmetric readout error model:
- Probability (\epsilon) that
0becomes1(and same for1becomes0).
- Probability (\epsilon) that
Readout confusion matrix
If (\epsilon) is the flip probability, then:
[ C = \begin{bmatrix} 1-\epsilon & \epsilon \ \epsilon & 1-\epsilon \end{bmatrix} ]
Interpretation:
- If the true state is
0, you measure0with probability (1-\epsilon). - If the true state is
0, you measure1with probability (\epsilon). - And similarly for true
1.
Working code (end-to-end)
Below is a fully working script using PennyLane (quantum circuits + differentiation) and PyTorch (training loop). It trains a classifier while accounting for readout error inside the loss.
import math import random import numpy as np import torch import pennylane as qml # ----------------------------- # 1) Toy dataset # ----------------------------- def make_toy_data(n=120, seed=0): rng = np.random.default_rng(seed) x = rng.uniform(-1.0, 1.0, size=(n, 1)) # Simple rule: class 1 if x > 0, else class 0 y = (x[:, 0] > 0).astype(np.float64) return x.astype(np.float64), y X, y = make_toy_data(n=140, seed=42) # Train/test split idx = np.arange(len(X)) np.random.default_rng(1).shuffle(idx) train_idx = idx[:100] test_idx = idx[100:] X_train = torch.tensor(X[train_idx], dtype=torch.float64) y_train = torch.tensor(y[train_idx], dtype=torch.float64) X_test = torch.tensor(X[test_idx], dtype=torch.float64) y_test = torch.tensor(y[test_idx], dtype=torch.float64) # ----------------------------- # 2) Readout error model # ----------------------------- # epsilon = probability of flipping measurement outcome epsilon = 0.12 # Confusion matrix C mapping ideal probs -> measured probs # p_measured = C @ p_ideal C = torch.tensor( [[1 - epsilon, epsilon], [epsilon, 1 - epsilon]], dtype=torch.float64 ) # ----------------------------- # 3) Quantum model # ----------------------------- n_qubits = 1 dev = qml.device("default.qubit", wires=n_qubits) @qml.qnode(dev, interface="torch", diff_method="parameter-shift") def ideal_probabilities(phi, params): """ Returns the ideal probabilities [p(0), p(1)] for measuring a single qubit. """ # Unpack trainable parameters a, b = params[0], params[1] # Simple feature encoding + variational circuit qml.RY(phi, wires=0) qml.RZ(a, wires=0) qml.RY(b, wires=0) return qml.probs(wires=[0]) # ----------------------------- # 4) Loss with readout folded in # ----------------------------- def folded_readout_cross_entropy(p_ideal, y_true): """ p_ideal: tensor shape (2,) with [p0, p1] y_true: scalar 0.0 or 1.0 Uses measured probability for label: if y_true == 1 -> use p_measured(1) if y_true == 0 -> use p_measured(0) """ # Compute p_measured = C @ p_ideal p_measured = C @ p_ideal # Numerical stability p0 = torch.clamp(p_measured[0], 1e-9, 1 - 1e-9) p1 = torch.clamp(p_measured[1], 1e-9, 1 - 1e-9) # Cross-entropy for binary labels # loss = -[ y*log(p1) + (1-y)*log(p0) ] loss = -(y_true * torch.log(p1) + (1 - y_true) * torch.log(p0)) return loss # ----------------------------- # 5) Training loop # ----------------------------- # Trainable parameters params = torch.tensor([0.1, -0.2], dtype=torch.float64, requires_grad=True) optimizer = torch.optim.Adam([params], lr=0.08) def feature_map(x): # Map x in [-1, 1] to rotation angle phi in a reasonable range # This keeps gradients from vanishing early. return math.pi * x.item() def predict_proba(x_batch): """ For each x, compute measured probability of class 1. """ probas = [] for x in x_batch: phi = feature_map(x) p_ideal = ideal_probabilities(phi, params).detach() # tensor [p0, p1] p_measured = C @ p_ideal probas.append(p_measured[1].item()) # probability of label 1 return np.array(probas, dtype=np.float64) # Training for epoch in range(1, 201): optimizer.zero_grad() total_loss = torch.tensor(0.0, dtype=torch.float64) # Accumulate loss over training set for xi, yi in zip(X_train, y_train): phi = feature_map(xi) # Ideal probabilities from circuit p_ideal = ideal_probabilities(phi, params) # shape (2,) # Fold readout error into loss total_loss = total_loss + folded_readout_cross_entropy(p_ideal, yi) # Mean loss total_loss = total_loss / len(X_train) # Backprop + update total_loss.backward() optimizer.step() if epoch % 20 == 0: # Quick evaluation using folded readout probabilities train_proba = predict_proba(X_train) test_proba = predict_proba(X_test) train_pred = (train_proba >= 0.5).astype(np.float64) test_pred = (test_proba >= 0.5).astype(np.float64) train_acc = (train_pred == y_train.numpy()).mean() test_acc = (test_pred == y_test.numpy()).mean() print(f"Epoch {epoch:3d} | loss={total_loss.item():.4f} | train_acc={train_acc:.3f} | test_acc={test_acc:.3f}") # ----------------------------- # 6) Final report # ----------------------------- test_proba = predict_proba(X_test) test_pred = (test_proba >= 0.5).astype(np.float64) accuracy = (test_pred == y_test.numpy()).mean() print("\nFinal test accuracy:", accuracy) print("Learned parameters:", params.detach().numpy())
Step-by-step: what’s happening in the code
1) Dataset + labels
I generate x uniformly in [-1, 1]. The label is:
y=1whenx > 0y=0otherwise
This keeps the learning task simple enough that the readout issue stands out clearly.
2) Confusion matrix C
I define epsilon = 0.12 for symmetric flips. Then I build:
- measured probability distribution =
C @ ideal probabilities
3) Quantum circuit returns ideal probabilities
The QNode ideal_probabilities(phi, params) returns qml.probs(wires=[0]), which is exactly:
[p_ideal(0), p_ideal(1)]
No noise is applied inside the circuit.
4) Loss uses measured probabilities
In folded_readout_cross_entropy:
- I compute
p_measured = C @ p_ideal - then I do binary cross-entropy using
p_measured[0]orp_measured[1]depending on the label
This is the key trick: the optimizer sees the effect of measurement distortion.
5) Training loop updates parameters
I compute total loss across the training set, then call:
total_loss.backward()optimizer.step()
PennyLane handles gradients of the quantum probabilities with parameter-shift rules.
What I observed after implementing this
Before folding readout error into the loss, training often produced a model that looked fine on paper but underperformed consistently once measured.
After folding the noise model into training:
- the learned decision boundary shifted to compensate for the systematic measurement bias,
- and test accuracy became much more stable.
Even in this 1-qubit toy example, it felt like turning “optimization against an imaginary device” into “optimization against the device I actually measure.”
Closing thoughts
I learned that readout error isn’t just a post-processing annoyance—it directly affects what your model is optimizing. By folding a readout confusion matrix into the loss function, I trained a variational quantum classifier against the probabilities the hardware (or hardware-like simulator) will actually produce, leading to more reliable performance and a training objective that finally matched reality.