DressedQuantumClassifier#

class pyqit.models.DressedQuantumClassifier(n_features, n_qubits=4, n_layers=6, n_classes=2, q_delta=0.01, device='default.qubit', shots=None, diff_method='best')[source]#

Bases: BaseQuantumModel, ClassifierMixin

Dressed quantum circuit of Mari et al. (2020): dense, circuit, dense.

A classical layer maps n_features to n_qubits angles through tanh(.) * pi / 2; the circuit applies a Hadamard layer, encodes the angles with RY, then n_layers blocks of a CNOT ladder followed by an RY layer, and reads <Z> on every wire; a second classical layer maps those to the classes. There is no separate embedding on the model, so the DataModule does not prescale.

Inside, the network is a QuantumPipeline of three layers from pyqit.models.layers: a DenseLayer, a QuantumLayer with HadamardAngleEmbedding and CNOTLadderAnsatz, and a DenseClassifier. Their weights are this model’s, under pre_net.*, quantum.* and post_net.*. Compose those layers yourself for a different hybrid.

The head differs from the paper in one way: pyqit models emit probabilities, so binary applies a sigmoid to one logit and multi-class a softmax, rather than handing logits to cross-entropy.

Parameters:
  • n_features (int)

  • n_qubits (int, default 4)

  • n_layers (int, default 6) – Variational depth, q_depth in the paper.

  • n_classes (int, default 2)

  • q_delta (float, default 0.01) – Spread of the normal initial quantum weights, as in the paper. The classical layers use torch.nn.Linear’s default init on both backends.

  • device (str, default "default.qubit")

  • shots (int, optional)

  • diff_method (str, default "best") – Passed to the QNode. "best" picks backprop on a simulator; "parameter-shift" rehearses a hardware run’s gradient cost.

References

Mari, Bromley, Izaac, Schuld, Killoran, “Transfer learning in hybrid classical-quantum neural networks”, Quantum 4, 340 (2020). PennyLane’s “Quantum transfer learning” demo is the reference implementation.

Examples

>>> import pyqit
>>> from pyqit.models import DressedQuantumClassifier
>>> model = DressedQuantumClassifier(n_features=8, n_qubits=4, n_layers=2)
>>> history = pyqit.Trainer(max_epochs=5).fit(model, dm)
diff_methods(X) → dict#

Differentiation method per QNode.

Parameters:

X (array-like) – One prescaled batch; only its shape matters.

Returns:

QNode name to method name.

Return type:

dict

execute_qnode(name: str, X, **custom_weights)#

Run the QNode or dense layer registered under name on a batch.

Parameters:
  • name (str) – Name passed to register_qnode.

  • X (array-like)

  • **custom_weights – Flat “<name>.<weight>” overrides; unprefixed keys are ignored. Falls back to the model’s own weights when empty.

Return type:

array-like

forward(X, **custom_weights)[source]#

Run dense, circuit, dense and return class probabilities.

Parameters:
  • X (array-like) – Batch of n_features columns, normalized but not prescaled.

  • **custom_weights – Override the model’s own weights, keyed as in weights.

Returns:

Probability of class 1 for binary; a (n_samples, n_classes) probability matrix otherwise.

Return type:

array-like

get_interface()#

PennyLane QNode interface for the active backend.

classmethod get_test_params()[source]#

List constructor kwargs used to parametrize this class in the test suite.

init_weights(weight_shapes: dict) → dict#

Draw uniform [0, 1) starting weights from numpy’s global RNG.

The range matches Qiskit ML’s default initial_point and PennyLane’s template examples; uniform [0, 2pi) is the Haar-like regime where gradients vanish (McClean et al. 2018).

Call this before building the device: a PennyLane device seeded "global" consumes numpy’s RNG at construction by a device-dependent amount, so weights drawn after it differ per device for the same seed.

Parameters:

weight_shapes (dict) – Weight name to shape, as returned by an ansatz’s get_weight_shapes.

Return type:

dict

is_fitted() → bool#

Whether Trainer.fit has trained this model.

predict_step(X)#

Predict hard class labels for X.

Parameters:

X (array-like) – Input batch.

Returns:

One label per row: 0/1 for binary, argmax index for multi-class.

Return type:

array-like

register_dense(name: str, n_in: int, n_out: int, weights=None)#

Register a classical dense layer X @ weight.T + bias under name.

Lives in the same registry as the QNodes, so weights, update_weights, checkpoints and the flat-kwargs routing cover it with no further plumbing. Run it with execute_qnode.

Parameters:
  • name (str)

  • n_in (int)

  • n_out (int)

  • weights (dict, optional) – {"weight", "bias"} from init_dense_weights; drawn when omitted.

register_qnode(name: str, qnode: QNode, weight_shapes: dict, weights=None)#

Wrap qnode for the active backend and store it under name.

Parameters:
  • name (str) – Key under which the node’s weights appear in weights.

  • qnode (qml.QNode)

  • weight_shapes (dict) – Weight name to shape, as returned by an ansatz’s get_weight_shapes.

  • weights (dict, optional) – Starting weights from init_weights; drawn here when omitted, so both backends start from the same point for the same seed.

update_weights(flat_weights_dict)#

Write flat_weights_dict into the model’s own weights.

No-op under torch, where autograd owns the nn.Parameter objects directly.

Parameters:

flat_weights_dict (dict) – Keyed like weights.

weight_groups() → dict#

weights keys by group, "quantum" (QNodes) and "classical".

Empty groups are omitted. The training loops build one optimizer per group, which is what lets Trainer(learning_rate={...}) set a rate per group.

property weights#

Flat {"<qnode_name>.<weight_name>": array} dict, both backends.