Quantum ComputingAugust 31, 2026

Variational Quantum Classifier With Readout Error Folded Into The Loss

Z

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:

  1. Compute ideal probabilities from the quantum circuit:
    [ p_\text{ideal} = [p(0), p(1)] ]
  2. Apply a readout confusion matrix (C) to get the measured probabilities:
    [ p_\text{measured} = C \cdot p_\text{ideal} ]
  3. 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 0 becomes 1 (and same for 1 becomes 0).

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 measure 0 with probability (1-\epsilon).
  • If the true state is 0, you measure 1 with 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=1 when x > 0
  • y=0 otherwise

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] or p_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.