Repository navigation
Hybrid Classical Quantum Models
Combine a classical neural network with a parameterized quantum circuit, train all components with one PyTorch optimizer, and read the final predictions and learned parameters.
Version: FlagQuantum 0.3.1. The examples were verified on local CPU with Python 3.12.14, PyTorch 2.14.1, and macOS Apple Silicon. They use exact, noiseless statevector simulation and require no GPU or provider credentials.
Prepare your environment using Installation and Compatibility. Read Training Parameterized Circuits for the basic PyTorch training lifecycle.
Formula/code review — October 20, 2026: The equations are paired with code excerpts, and the local CPU examples were re-run with Python 3.12.14 and PyTorch 2.13.0. See the review record for executed commands, distributed checks, and untested hardware/provider paths. Earlier environment and output records are retained.
This tutorial builds a small regressor with three trainable components:
Input → classical encoder → quantum circuit → expectation values → classical readout → prediction.
| Component | Role | Input and output shapes |
|---|---|---|
| Classical encoder | A linear layer and Tanh convert one input feature into two bounded rotation angles. |
(B, 1) → (B, 2)
|
| Quantum layer | A two-qubit circuit encodes those angles, applies CX and trainable rotations, then measures separate Z expectations. |
(B, 2) → (B, 2)
|
| Classical readout | A linear layer maps the two quantum features to a regression prediction. |
(B, 2) → (B, 1)
|
B is the number of examples in a batch. The quantum features are expectation values, not sampled bits or probabilities. Each Z expectation lies between −1 and +1; the linear readout can produce predictions outside that interval.
The training target is the simple function y = x². We use 17 points between −1 and +1 and evaluate the trained model at four additional points. This is an instructional regression example, not evidence of quantum advantage or broad predictive performance.
An fq.Module builder can accept two arguments: parameters and encoded_inputs.
-
encoded_inputscomes from the classical encoder. Each row contains one example's two data-dependent gate angles. -
parameterscontains the quantum layer's shared trainable rotations. The same two parameters are used across the batch. -
fq.Circuit(2, bsz=encoded_inputs.shape[0])creates a circuit batch matching the data batch. - Passing
encoded_inputs[:, 0]andencoded_inputs[:, 1]to RY gives each batch entry its own encoded state.
The circuit applies input-dependent RY gates, a CX gate, and two trainable RY gates. We configure observable="z" and observable_qubits=(0, 1) to obtain two separate expectation features. Using a summed observable would produce a different feature shape.
Multiplying the encoder's Tanh outputs by torch.pi keeps its angles bounded by π in magnitude. Rotation angles are in radians.
The following excerpts use HybridRegressor and the training scripts below. For each batch row, let
| Formula | Corresponding code | Shape / validation |
|---|---|---|
angles = torch.pi * self.encoder(inputs) |
(B, 1) to (B, 2); encoder includes Linear then Tanh. |
|
circuit.ry(0, encoded_inputs[:, 0])circuit.ry(1, encoded_inputs[:, 1])
|
Encode each batch row's two input angles. | |
circuit.cx(0, 1) |
Entangle after encoding. | |
circuit.ry(0, parameters[0])circuit.ry(1, parameters[1])
|
Apply shared trainable angles after CX. | |
quantum_features = self.quantum(angles) |
Policy selects separate Z outputs, yielding (B, 2). |
|
return self.readout(quantum_features) |
Linear readout yields (B, 1). |
|
torch.nn.functional.mse_loss(predictions, targets) |
targets = inputs.square(); both have shape (B, 1). |
|
loss.backward() |
check_hybrid_gradients.py asserts finite, nonzero aggregate gradients in every component. |
The complete unitary is parameters[0:2] are reused across all batch rows, while each row has its own encoded angles. A tensor detach between the encoder and quantum layer would remove the encoder's chain-rule gradient.
Create a folder for this tutorial. Save the following as hybrid_model.py. The two runnable scripts later in this page import this file; keep all three files in the same folder.
import torch
from torch import nn
import flagquantum as fq
def quantum_circuit(parameters, encoded_inputs):
circuit = fq.Circuit(2, bsz=encoded_inputs.shape[0], device=encoded_inputs.device)
circuit.ry(0, encoded_inputs[:, 0])
circuit.ry(1, encoded_inputs[:, 1])
circuit.cx(0, 1)
circuit.ry(0, parameters[0])
circuit.ry(1, parameters[1])
return circuit
class HybridRegressor(nn.Module):
def __init__(self):
super().__init__()
self.encoder = nn.Sequential(nn.Linear(1, 2), nn.Tanh())
self.quantum = fq.Module(
quantum_circuit,
n_parameters=2,
init=torch.tensor([0.2, -0.3]),
device="cpu",
policy=fq.RuntimePolicy(
execution_options=fq.ExecutionOptions(
mode="statevector", backend="pytorch", device="cpu"
),
observable="z",
observable_qubits=(0, 1),
),
)
self.readout = nn.Linear(2, 1)
def forward(self, inputs):
angles = torch.pi * self.encoder(inputs)
quantum_features = self.quantum(angles)
return self.readout(quantum_features)HybridRegressor inherits from torch.nn.Module. Assigning its encoder, quantum layer, and readout as child modules registers their parameters with the parent. Consequently, model.parameters() includes all three components.
The classical encoder transforms data into circuit inputs; it does not own the quantum parameters. The quantum parameters are initialized separately by fq.Module. They are shared trainable weights rather than a new set of weights for every data row.
The differentiable path is:
Loss → readout weights and quantum features → quantum parameters and encoded angles → encoder weights.
Do not detach the encoded angles or turn them into Python numbers before passing them to the quantum layer. That would interrupt the path back to the encoder. The model uses PyTorch operations throughout its forward computation.
Save the following as check_hybrid_gradients.py:
import torch
from hybrid_model import HybridRegressor
torch.manual_seed(7)
model = HybridRegressor()
inputs = torch.tensor([[-0.8], [-0.3], [0.3], [0.8]])
targets = inputs.square()
predictions = model(inputs)
loss = torch.nn.functional.mse_loss(predictions, targets)
loss.backward()
with torch.no_grad():
angles = torch.pi * model.encoder(inputs)
features = model.quantum(angles)
print("Input shape:", tuple(inputs.shape))
print("Encoded angle shape:", tuple(angles.shape))
print("Quantum feature shape:", tuple(features.shape))
print("Prediction shape:", tuple(predictions.shape))
print(f"Initial MSE: {loss.item():.6f}")
for name, layer in (("Encoder", model.encoder), ("Quantum", model.quantum), ("Readout", model.readout)):
gradients = [parameter.grad for parameter in layer.parameters()]
valid = all(gradient is not None and bool(torch.isfinite(gradient).all()) for gradient in gradients)
nonzero = valid and sum(gradient.abs().sum().item() for gradient in gradients) > 0
print(f"{name} has finite, nonzero gradients: {nonzero}")
assert nonzero
print("Joint gradient checks passed.")Run it from the folder containing the three files:
python check_hybrid_gradients.pyOutput recorded in the verification environment:
Input shape: (4, 1)
Encoded angle shape: (4, 2)
Quantum feature shape: (4, 2)
Prediction shape: (4, 1)
Initial MSE: 1.058635
Encoder has finite, nonzero gradients: True
Quantum has finite, nonzero gradients: True
Readout has finite, nonzero gradients: True
Joint gradient checks passed.
The script checks that every registered parameter tensor receives finite gradients and that each component has a nonzero aggregate gradient magnitude. This is a check at the chosen initialization and batch, not a guarantee that every individual gradient element is nonzero at every step.
The extra forward calls under torch.no_grad() inspect intermediate shapes after backward. They are not used to construct the training loss.
Use a standard PyTorch optimizer on the outer model:
torch.optim.Adam(model.parameters(), lr=0.03)
Passing only model.quantum.parameters() would update the quantum parameters while leaving the encoder and readout unoptimized. Passing model.parameters() includes all registered child parameters.
For this outer hybrid model, use a normal PyTorch training loop. In FlagQuantum 0.3.1, fq.train accepts an fq.Module, whereas the outer HybridRegressor is a general torch.nn.Module containing several components. Calling fq.train on just the inner quantum layer would not represent the complete hybrid training computation.
Save the following as train_hybrid.py:
import torch
from hybrid_model import HybridRegressor
torch.manual_seed(7)
model = HybridRegressor()
inputs = torch.linspace(-1.0, 1.0, 17).reshape(-1, 1)
targets = inputs.square()
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)
initial_parameters = {name: value.detach().clone() for name, value in model.named_parameters()}
losses = []
model.train()
for step in range(1, 401):
optimizer.zero_grad()
predictions = model(inputs)
loss = torch.nn.functional.mse_loss(predictions, targets)
loss.backward()
optimizer.step()
losses.append(loss.detach().item())
if step in (1, 100, 200, 400):
print(f"Step {step:3d}: MSE={losses[-1]:.6f}")
model.eval()
test_inputs = torch.tensor([[-0.8], [-0.3], [0.3], [0.8]])
with torch.no_grad():
train_mse = torch.nn.functional.mse_loss(model(inputs), targets).item()
test_predictions = model(test_inputs)
test_mse = torch.nn.functional.mse_loss(test_predictions, test_inputs.square()).item()
print(f"Post-update training MSE: {train_mse:.6f}")
print(f"Four-point evaluation MSE: {test_mse:.6f}")
for x, predicted in zip(test_inputs[:, 0], test_predictions[:, 0]):
print(f"x={x.item():+.1f}, target={x.square().item():.3f}, prediction={predicted.item():.3f}")
for prefix, label in (("encoder.", "Encoder"), ("quantum.", "Quantum"), ("readout.", "Readout")):
changed = any(not torch.equal(value.detach(), initial_parameters[name]) for name, value in model.named_parameters() if name.startswith(prefix))
print(f"{label} parameters changed: {changed}")
assert changed
print("Trained quantum angles:", [round(value.item(), 4) for value in next(model.quantum.parameters()).detach()])
assert train_mse < 1e-3 and test_mse < 1e-3
print("Hybrid training checks passed.")Run:
python train_hybrid.pyOutput recorded in the verification environment:
Step 1: MSE=1.112631
Step 100: MSE=0.111090
Step 200: MSE=0.001631
Step 400: MSE=0.000371
Post-update training MSE: 0.000370
Four-point evaluation MSE: 0.000199
x=-0.8, target=0.640, prediction=0.658
x=-0.3, target=0.090, prediction=0.085
x=+0.3, target=0.090, prediction=0.084
x=+0.8, target=0.640, prediction=0.661
Encoder parameters changed: True
Quantum parameters changed: True
Readout parameters changed: True
Trained quantum angles: [0.8617, 0.194]
Hybrid training checks passed.
The initial loss here differs from the gradient-check script because the scripts use different input batches. Each script creates a fresh model with seed 7.
The final digits may differ across compatible environments. The training script asserts that the final training MSE and four-point evaluation MSE are each below 1e-3, and checks that at least one parameter tensor in every component changed. A low error on four additional points is illustrative; it is not a full generalization evaluation.
-
optimizer.zero_grad()clears gradients from the previous step. -
model(inputs)runs the encoder, circuit simulation, expectation readout, and final linear layer. -
mse_loss(predictions, targets)produces a scalar real loss. Both tensors have shape(17, 1), avoiding accidental broadcasting. -
loss.backward()propagates through the complete differentiable computation. -
optimizer.step()updates the classical and quantum parameters together.
The loss history stores detached Python numbers for reporting. Each recorded loss describes the forward pass before that step's optimizer update. The final MSE is recomputed using the updated parameters.
| What you want | How to access it |
|---|---|
| Regression predictions |
model(inputs), with shape (B, 1)
|
| Encoded circuit angles |
torch.pi * model.encoder(inputs), with shape (B, 2)
|
| Quantum expectation features |
model.quantum(angles), with shape (B, 2)
|
| Shared quantum rotations |
next(model.quantum.parameters()) in this two-parameter example |
| All named weights | model.named_parameters() |
| Registered model state | model.state_dict() |
The two quantum feature columns are the Z expectations of logical qubits 0 and 1, in that requested order. They are not the four amplitudes of a two-qubit statevector. See Qubit Ordering and Result Conventions when interpreting state or measurement arrays.
model.eval() sets evaluation mode; it does not itself disable gradients. Use torch.no_grad() for inference when gradient recording is unnecessary. Detaching weights for display or saving an independent snapshot is appropriate after training, but do not detach the intermediate features inside the training forward pass.
The reported quantum angles are shared model weights. For a particular input, the actual circuit also depends on the encoder's output for that input. Saving only the quantum angles therefore does not preserve the complete predictor: serialize the outer model's state and preserve its architecture and preprocessing. Include optimizer state when resuming training.
-
More input features: change the encoder's input dimension and provide inputs with shape
(B, feature_count). - More quantum features: select supported observables and adjust the readout dimension to match their output shape.
-
Classification: replace the regression head and objective with a suitable classifier. For example, a single-logit binary head can use
BCEWithLogitsLosswith matching target shape; the raw expectation feature is not automatically a class probability. - Larger datasets: train over mini-batches while preserving a fresh forward/backward/update lifecycle for each batch.
Adding qubits or changing simulation method can change resource requirements and supported gradients. This page verifies only the shown two-qubit local CPU workflow. Consult the release capability catalog before changing the execution path.
| Symptom | What to check |
|---|---|
| Encoder receives no gradients | Keep encoded angles attached to autograd; avoid .detach(), .item(), NumPy conversion, or rebuilding them as new tensors. |
| Only quantum weights change | Give the optimizer model.parameters(), not just the inner layer's parameters. |
| Predictions have the wrong shape | Preserve (B, 1) targets and predictions. With two Z observables, the quantum feature tensor is (B, 2). |
| Batch-size mismatch | Set circuit bsz from the current encoded batch, not a fixed training-set size. |
| Readout dimension mismatch | Match its input width to the number of returned quantum features. |
| Classical and quantum devices differ | Keep inputs, classical layers, parameters, and selected execution device consistent. These examples use CPU throughout. |
| Loss stops improving | Inspect initialization, gradient magnitudes, learning rate, encoder saturation, and model expressiveness. A hybrid architecture does not guarantee convergence. |
| Reloaded predictor behaves differently | Restore encoder, quantum, and readout weights, along with the same preprocessing and architecture. |
- Choosing a Simulation Method: representations, selection criteria, and verified output and gradient comparisons.
- Training Parameterized Circuits: scalar objectives, optimizer updates, and training summaries.
- Building and Simulating Circuits: gates, simulator execution, and output requests.
- Core Concepts: observables, module outputs, and automatic differentiation.
- Wiki Directory: the documentation structure.
- Release hybrid training example: a related maintained classical-quantum training workflow.
- Release module implementation, runtime policy, and training entry point.
- Installation and Compatibility
- Quick Start
- Install and Run
- Qubit Ordering and Result Conventions
- Runnable Examples
- Building and Simulating Circuits
- Training Parameterized Circuits
- Hybrid Classical-Quantum Models
- Core Concepts
- From Papers and Equations to FlagQuantum Code
- Reproducing Molecular Hydrogen VQE
- Fundamentals
- Tutorials
- Choosing a Simulation Method
- Accelerator Setup
- Distributed Simulation and Training
- Execution
- Circuit Compilation and Interoperability
- Noise and Open-System Simulation
- Quantum Error Correction Experiments
- Cloud and Quantum Hardware Execution
- QPU Digital Twins
- Advanced Topics
- Support and Contribution
All 21 guides are linked in the navigation sections. The Wiki Directory lists published guides and current repository resources.
- Troubleshooting and FAQ
- Reproducibility and Benchmarks
- Development and Contribution Guide
- Documentation Guide
Documentation, capability, and contribution links in this section follow current main; the guides above target 0.3.1.