import math
import pennylane as qml
from pyqit.models.base.quantum_model import BaseQuantumModel
from pyqit.models.classification.classifier_mixin import ClassifierMixin
[docs]
class DataReuploadingClassifier(BaseQuantumModel, ClassifierMixin):
"""Data re-uploading classifier of Perez-Salinas et al. (2020).
Every layer re-encodes the input: on each qubit it applies
``Rot(theta + w * x)`` (their Eq. 7), with ``x`` zero-padded to a multiple
of three and consumed three features per rotation. With more than one
qubit, a CZ chain entangles neighbouring wires between layers (their
Sec. 4). There is no separate embedding, so the DataModule does not
prescale; the model reads the normalized features directly.
The readout and the loss are pyqit's, not the paper's fidelity cost:
binary reads ``(1 + <Z_0>) / 2``, multi-class bins basis-state
probabilities by index modulo ``n_classes``.
Parameters
----------
n_features : int
Input width. ``forward`` raises on any other width.
n_qubits : int, default 1
n_layers : int, default 3
Re-uploading layers.
n_classes : int, default 2
measure_fn : callable, optional
Defaults to `measure_expval_z` for binary, `measure_probs` otherwise.
measure_wires : list of int, optional
Defaults to `[0]` for binary, all wires otherwise.
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
----------
Perez-Salinas, Cervera-Lierta, Gil-Fuster, Latorre, "Data re-uploading
for a universal quantum classifier", Quantum 4, 226 (2020). PennyLane's
"Data re-uploading classifier" demo is the reference implementation.
Examples
--------
>>> import pyqit
>>> from pyqit.models import DataReuploadingClassifier
>>> model = DataReuploadingClassifier(n_features=2, n_qubits=1, n_layers=4)
>>> history = pyqit.Trainer(max_epochs=5).fit(model, dm) # doctest: +SKIP
"""
def __init__(
self,
n_features,
n_qubits=1,
n_layers=3,
n_classes=2,
measure_fn=None,
measure_wires=None,
device="default.qubit",
shots=None,
diff_method="best",
):
super().__init__(device=device, shots=shots, diff_method=diff_method)
self.n_features = n_features
self.n_qubits = n_qubits
self.n_layers = n_layers
self.n_classes = n_classes
self.measure_fn = measure_fn
self.measure_wires = measure_wires
self._n_chunks = math.ceil(n_features / 3)
self._pad = 3 * self._n_chunks - n_features
self._resolve_readout(n_qubits, measure_fn, measure_wires)
shape = (n_layers, n_qubits, self._n_chunks, 3)
weight_shapes = {"theta": shape, "w": shape}
init_weights = self.init_weights(weight_shapes)
dev = qml.device(self.device, wires=self.n_qubits)
qnode = qml.set_shots(
qml.QNode(
self._circuit,
dev,
interface=self.get_interface(),
diff_method=self.diff_method,
),
shots=self.shots,
)
self.register_qnode("main_circuit", qnode, weight_shapes, weights=init_weights)
def __repr__(self):
return (
f"DataReuploadingClassifier(n_features={self.n_features}, "
f"n_qubits={self.n_qubits}, n_layers={self.n_layers}, "
f"n_classes={self.n_classes}, device='{self.device}')"
)
def _circuit(self, inputs, theta, w):
x = inputs
if self._pad:
zeros = qml.math.zeros_like(x[..., :1])
x = qml.math.concatenate([x] + [zeros] * self._pad, axis=-1)
for layer in range(self.n_layers):
for q in range(self.n_qubits):
for c in range(self._n_chunks):
v = w[layer, q, c] * x[..., 3 * c : 3 * c + 3] + theta[layer, q, c]
qml.Rot(v[..., 0], v[..., 1], v[..., 2], wires=q)
if layer < self.n_layers - 1:
for q in range(self.n_qubits - 1):
qml.CZ(wires=[q, q + 1])
return self._measure_fn(self._measure_wires)
[docs]
def forward(self, X, **custom_weights):
"""Run the circuit 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
-------
array-like
Probability of class 1 for binary; a `(n_samples, n_classes)`
probability matrix otherwise.
"""
if X.shape[-1] != self.n_features:
raise ValueError(
f"X has {X.shape[-1]} features but the model was built for "
f"n_features={self.n_features}."
)
raw_output = self.execute_qnode("main_circuit", X, **custom_weights)
return self._to_probabilities(raw_output)
[docs]
@classmethod
def get_test_params(cls):
"""List constructor kwargs used to parametrize this class in the test suite."""
return [
{"n_features": 2, "n_qubits": 1, "n_layers": 2},
{
"n_features": 4,
"n_qubits": 2,
"n_layers": 2,
"n_classes": 3,
"trainer_kwargs": {"loss_fn": "cross_entropy"},
},
]