PyTorch¶
Circuits as nn.Modules, with gradients flowing to the inputs as well as the weights.
qmlkit.nn.layer¶
layer ¶
The PyTorch bridge.
Above this boundary everything is ordinary torch — Adam, .backward(),
DataLoader, nn.Sequential. Below it, circuits and shots. Nobody has to
write an autograd.Function themselves.
Inputs get gradients. backward returns df/dx as well as df/dtheta,
so a classical layer placed before the quantum one actually trains. Returning
None there is the common shortcut, and it silently freezes any pre-net — which
is invisible in the loss curve and fatal to transfer learning, where the layer
feeding the circuit is the one you meant to train.
Getting df/dx through a nonlinear feature map takes two steps: the circuit is
differentiated with respect to its encoding angles, then the chain rule down to the
features is finished classically by the map's angle_jacobian. No circuits are
spent on the classical half.
QuantumFunction ¶
Bases: Function
Autograd boundary: forward runs circuits, backward differentiates them.
Combined ¶
Bases: Protocol
A model that carries its own encoding, interleaved with a trainable block.
Re-uploading is a pattern, not a class, so there is no base to test against -
what makes a model combined is that it both encodes data and holds weights. That
is four attributes, and this protocol is where they are written down. Anything
satisfying it can be handed to :class:QuantumLayer in place of a feature map.
QuantumLayer ¶
QuantumLayer(
feature_map: FeatureMap,
ansatz: Ansatz | None = None,
observables: Sequence[Observable] | None = None,
shots: int | None = None,
backend: Any = None,
grad_method: str = "auto",
seed: int | None = None,
init: str = "small",
init_seed: int | None = None,
)
Bases: Module
A circuit as an nn.Module.
Maps (batch, n_features) to (batch, n_observables), each output an
expectation value in [-1, 1].
Defaults are chosen for a simulator: exact expectations and adjoint gradients.
Pass shots=N to model a device, and grad_method="parameter-shift" to
compute the way hardware would have to.
Source code in src/qmlkit/nn/layer.py
configure ¶
configure(
shots: int | None = _UNCHANGED,
grad_method: str | None = None,
) -> QuantumLayer
Switch to device-realism mode (or back) without rebuilding the layer.
Source code in src/qmlkit/nn/layer.py
resources ¶
Circuit cost, and what a batch costs under each gradient method.
Source code in src/qmlkit/nn/layer.py
qmlkit.nn.models¶
models ¶
Ready-made models — the two-line path.
model = qk.VQC(n_features=4, n_classes=2)
model.fit(X, y)
model.score(X, y)
Every default here is a choice you can override, and each override is one keyword.
Pass your own feature_map or ansatz, and the rest still works — nothing in
these models knows anything about the specific ones they default to.
If you want the layer without the training loop, use
:class:~qmlkit.nn.layer.QuantumLayer directly and treat it as any other
nn.Module.
HybridModel ¶
HybridModel(
n_features: int,
n_outputs: int,
n_qubits: int | None = None,
n_layers: int = 2,
feature_map: FeatureMap | None = None,
ansatz: Ansatz | None = None,
observables: Sequence[Observable] | None = None,
shots: int | None = None,
backend: Any = None,
grad_method: str = "auto",
scale_inputs: bool = True,
seed: int | None = None,
)
Bases: Module
Shared machinery: a training loop, and sensible construction defaults.
Source code in src/qmlkit/nn/models.py
fit ¶
fit(
X: NDArray[Any],
y: NDArray[Any],
epochs: int = 30,
lr: float = 0.05,
batch_size: int | None = None,
optimizer: Optimizer | None = None,
verbose: bool = False,
) -> HybridModel
Train. Returns self, so it chains.
Source code in src/qmlkit/nn/models.py
resources ¶
What one training step costs, so nobody discovers it an hour in.
Source code in src/qmlkit/nn/models.py
VQC ¶
VQC(
n_features: int,
n_classes: int = 2,
class_weight: str | None = None,
focal_gamma: float = 0.0,
**kwargs: Any,
)
Bases: HybridModel
Variational quantum classifier.
model = VQC(n_features=4, n_classes=3).fit(X, y) model.score(X, y)
class_weight="balanced" reweights the loss by class frequency, which is
what stops a skewed training set training the circuit to a constant. The
weights are computed from the y passed to :meth:fit, so they describe the
data actually trained on rather than an assumption made at construction.
focal_gamma additionally down-weights examples the model already gets
right; 0.0 disables it, 2.0 is the published default.
Source code in src/qmlkit/nn/models.py
VQRegressor ¶
Bases: HybridModel
Variational quantum regressor.
model = VQRegressor(n_features=3).fit(X, y) model.predict(X)
Source code in src/qmlkit/nn/models.py
score ¶
R² — 1.0 is perfect, 0.0 is no better than predicting the mean.
Source code in src/qmlkit/nn/models.py
qmlkit.nn.advanced¶
advanced ¶
QCNN, QLSTM and MPS layers — architectures with structure, not just depth.
Each is an ordinary nn.Module built from a :class:QuantumLayer, so they train
the same way and compose with anything else in torch. What distinguishes them is
where the structure lives:
- QCNN — a convolution filter shared across every pair, then pooling that halves the register. Log-depth in the qubit count, and few parameters because the filter is tied. Provably free of the exponential barren plateau.
- QLSTM — four small circuits standing in for the gates of an LSTM cell. The recurrence and nonlinearities stay classical; only the gates are quantum.
- MPS — a staircase of two-qubit blocks, matching a bond-dimension-2 matrix product state. Linear depth, and classically simulable at small bond dimension, which is worth knowing before claiming an advantage.
QCNNLayer ¶
QCNNLayer(
n_qubits: int,
feature_map: FeatureMap | None = None,
tie_weights: bool = True,
observables: Sequence[Observable] | None = None,
filter: str | tuple[Any, int] = "ry_cx",
pattern: str = "chain",
pool: str = "discard",
ansatz: Ansatz | None = None,
**kwargs: Any,
)
Bases: Module
Quantum convolutional layer: shared filter, then pooling.
The filter is tied across every pair it slides over, so an 8-qubit QCNN carries 6 parameters where an untied version needs 22 — at the same gradient cost.
Source code in src/qmlkit/nn/advanced.py
MPSLayer ¶
MPSLayer(
n_qubits: int,
feature_map: FeatureMap | None = None,
observables: Sequence[Observable] | None = None,
filter: str | tuple[Any, int] = "ry_cx",
tied: bool = False,
ansatz: Ansatz | None = None,
**kwargs: Any,
)
Bases: Module
Matrix-product-state layer — a staircase of two-qubit blocks.
Source code in src/qmlkit/nn/advanced.py
QLSTMCell ¶
QLSTMCell(
n_inputs: int,
hidden_size: int,
n_qubits: int = 4,
ansatz: Ansatz | None = None,
n_layers: int = 2,
**kwargs: Any,
)
Bases: Module
One LSTM cell with its four gates replaced by small circuits.
forget, input, candidate and output each become a
:class:QuantumLayer; the recurrence, the sigmoids and the tanh stay classical.
A classical projection maps [x, h] down to the qubit count first, which is
what keeps the circuits small enough to be worth running.
Source code in src/qmlkit/nn/advanced.py
QLSTM ¶
Bases: Module
A QLSTM over a sequence. Returns (outputs, (h, c)).
Source code in src/qmlkit/nn/advanced.py
DressedQuantumNet ¶
DressedQuantumNet(
backbone: Module | None,
in_features: int,
n_qubits: int,
n_outputs: int,
n_layers: int = 2,
feature_map: FeatureMap | None = None,
ansatz: Ansatz | None = None,
freeze_backbone: bool = True,
**kwargs: Any,
)
Bases: Module
The dressed circuit: a frozen backbone, then Linear -> quantum -> Linear.
Transfer learning with a quantum head. The backbone is frozen, so only the
dressed block trains — and because the layer returns input gradients, the
Linear that feeds the circuit trains too. An implementation that returns
None for the input gradient silently freezes exactly that layer, which is the
one doing the adapting.