Layers#
A layer is a reusable block that models are assembled from. It holds its own
weights and runs a batch through forward, and it can be classical or
quantum. Nothing about a layer is specific to hybrid networks.
BaseVQC is the embedding, ansatz and measurement block that
VQCClassifier and VQCRegressor
share, and QuantumLayer is that same block read as <Z> on every
wire.
There are two ways to use a layer. A model class can build layers in its
__init__ and run them in forward, which is how
DressedQuantumClassifier is written. Or you can compose
layers yourself in a pipeline and train them together with
fit_mode="joint", where the loss after the last layer reaches every layer
before it.
import pyqit
from pyqit.core import QuantumPipeline
from pyqit.models.layers import DenseClassifier, DenseLayer, QuantumLayer
hybrid = QuantumPipeline(
[
("pre", DenseLayer(n_features=8, n_out=4, activation="tanh")),
("circuit", QuantumLayer(n_qubits=4, n_layers=2)),
("head", DenseClassifier(n_features=4)),
],
fit_mode="joint",
)
history = pyqit.Trainer(max_epochs=20).fit(hybrid, dm)
Available layers#
Each layer’s page gives its inputs, its output shape and its weight names.
Classical dense stage: |
|
Quantum stage: an embedding, an ansatz, and |
|
Classical head stage: one dense layer, then sigmoid or softmax. |
Feature layers and heads#
A feature layer returns a (n_samples, width) array for the next layer to
read. Its output is not a prediction, so it cannot be fit alone against labels.
A head comes last. It returns predictions and has predict_step, which is
what Trainer.predict calls for hard labels.
The pipeline prescales the input of every quantum layer for that layer’s embedding, so the layer in front of it needs no scaling of its own. Narrower input is zero-padded to the layer’s width and wider input raises.
What every layer provides#
forward(X, **custom_weights) runs the layer. weights is a flat dict
keyed "<name>.<weight>", the same on both backends, and update_weights
writes it back. Inside a pipeline the stage name goes in front of each key.
DenseLayer(n_features=8, n_out=4).weights
# {"dense.weight": ..., "dense.bias": ...}
hybrid.weights
# {"pre.dense.weight": ..., "circuit.main_circuit.weights": ..., ...}
A layer reads the backend once in its own __init__, as models do. Call
set_backend() before building it.
Adding a layer#
Subclass BaseModel for a classical layer and register
its weights with register_dense. Subclass
BaseQuantumModel for a quantum one and use
register_qnode. Set the object_type tag to "layer", and run the
registered entries with execute_qnode inside
forward(X, **custom_weights), passing custom_weights through. The
training loops rely on that argument to route weights. A head also mixes in
ClassifierMixin or
RegressorMixin for predict_step.
A quantum layer built from an embedding and an ansatz can subclass
BaseVQC and skip the circuit. It then implements _resolve_readout,
which picks the measurement, and forward, which maps the raw output.
Implement get_test_params(). The layer suite finds the class by its tag and
trains it in front of a DenseClassifier. It reads the input width from
n_features or n_qubits and the output width from n_out or
n_qubits, so a feature layer must expose those attributes. Heads are
excluded from that test by name in test_all_layers.py. Then add the class
name to the list above. See the contributing guide.
You subclass this and never instantiate it directly.
An embedding, an ansatz, a measurement: the circuit the VQC models share. |