Source code for uniqc.algorithms.core.training.qnn
"""Quantum Neural Network (QNN) classifier with TorchQuantum backend.
Provides QNNClassifier — an nn.Module for binary classification using
Hardware-Efficient Ansatz and TorchQuantum's native autograd.
"""
from __future__ import annotations
import torch
import torch.nn as nn
from uniqc.simulator.torchquantum_simulator import TorchQuantumSimulator
__all__ = ["QNNClassifier"]
def _build_qnn_circuit(
params: torch.Tensor,
n_qubits: int,
n_features: int,
depth: int,
x: torch.Tensor | None = None,
) -> tuple[list, int, dict]:
"""Build QNN circuit: data encoding + variational HEA.
params[:n_features]: unused when x is provided (data encoding angles)
params[n_features:]: variational parameters
"""
from uniqc.circuit_builder import Circuit
circuit = Circuit(n_qubits)
param_overrides = {}
# Data encoding: angle encoding via RY
if x is not None:
for q in range(min(n_features, n_qubits)):
opcode_idx = len(circuit.opcode_list)
circuit.ry(q, 0.0)
param_overrides[opcode_idx] = x[q].unsqueeze(0).unsqueeze(0)
# Variational layers (HEA)
idx = 0
for _ in range(depth):
for q in range(n_qubits):
opcode_idx = len(circuit.opcode_list)
circuit.rz(q, 0.0)
param_overrides[opcode_idx] = params[idx].unsqueeze(0).unsqueeze(0)
idx += 1
opcode_idx = len(circuit.opcode_list)
circuit.ry(q, 0.0)
param_overrides[opcode_idx] = params[idx].unsqueeze(0).unsqueeze(0)
idx += 1
for i in range(n_qubits):
circuit.cx(i, (i + 1) % n_qubits)
return circuit.opcode_list, n_qubits, param_overrides
[docs]
class QNNClassifier(nn.Module):
"""Quantum Neural Network for binary classification.
Uses angle encoding for input features and HEA for variational layer.
Output is σ(<Z₀>) for binary classification.
Args:
n_qubits: Number of qubits.
n_features: Input feature dimension.
depth: HEA depth.
"""
def __init__(self, n_qubits: int = 4, n_features: int = 2, depth: int = 2):
super().__init__()
self.n_qubits = n_qubits
self.n_features = n_features
self.depth = depth
self.n_variational = 2 * n_qubits * depth
# Variational parameters
self.params = nn.Parameter(torch.randn(self.n_variational) * 0.1)
# Measurement: <Z_0> for classification
self.hamiltonian = [("Z" + "I" * (n_qubits - 1), 1.0)]
self._sim = TorchQuantumSimulator(n_wires=n_qubits)
[docs]
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Classify input batch.
Args:
x: Input tensor of shape (batch_size, n_features).
Returns:
Probability tensor of shape (batch_size,).
"""
batch_size = x.shape[0]
outputs = []
for i in range(batch_size):
# Scale input to [0, π]
x_scaled = x[i] * torch.pi
circuit_params = self.params
opcode_list, n_qubits, param_overrides = _build_qnn_circuit(
circuit_params, self.n_qubits, self.n_features, self.depth, x_scaled
)
expval = self._sim.expectation(opcode_list, self.hamiltonian, param_overrides)
# Map [-1, 1] to [0, 1] via sigmoid-like transform
prob = (expval + 1.0) / 2.0
outputs.append(prob)
return torch.stack(outputs)