Skip to content

Training

hqnn_forge.training

hqnn_forge.training

A minimal, model-agnostic train/validate loop with early stopping.

Exported symbols

train_model Mini-batch training with per-epoch validation and early stopping. TrainingHistory Per-epoch record returned by train_model. EpochRecord One epoch of that record. SPSA Gradient-free optimiser: two loss evaluations per step, for shot-based training.

SPSA

Bases: Optimizer

SPSA: two loss evaluations per step, independent of the parameter count.

Parameters:

Name Type Description Default
params Iterable[Tensor] | Iterable[dict[str, Any]]

Parameters or parameter groups, as for any torch.optim.Optimizer. A group may override any of the gain settings below; each group is checked as the defaults are, also one added later by add_param_group.

required
lr float

a, the numerator of the step-size sequence. Default: 0.1. Unlike Adam's, it multiplies the raw gradient estimate, so the right value scales with the loss's gradient. On the BCE-trained 4-qubit, 2-layer model of examples/hardware_workflow.py (gradients of about 0.05), SPSA on every parameter, head included, barely moved it in 30 epochs at lr 0.1 to 0.4 and trained it at lr 1 to 8; the example itself trains the head with Adam (gradient_optimizer), at lr 1.

0.1
perturbation float

c, the numerator of the perturbation-size sequence. A value near the standard deviation of the loss's noise is Spall's guideline. Default: 0.1.

0.1
alpha float

Decay exponents of the two sequences; the defaults 0.602 and 0.101 are Spall's practical choices (asymptotically optimal: 1 and 1/6).

0.602
gamma float

Decay exponents of the two sequences; the defaults 0.602 and 0.101 are Spall's practical choices (asymptotically optimal: 1 and 1/6).

0.602
stability float

A in a_k; about 10 % of the expected number of steps damps the first, largest steps. Default: 0.

0.0
generator Generator | None

Source of the ±1 directions, a CPU generator (the directions are moved to each parameter's device). Default: a fresh generator seeded with torch.initial_seed(), so a run is reproducible under torch.manual_seed and runs under different seeds draw different directions; building the optimiser draws nothing from the global RNG.

None
gradient_optimizer Optimizer | None

An optimiser over parameters that act after the circuit (see The classical head above), disjoint from params. Each step backpropagates both loss evaluations to its parameters only, sets their .grad to the mean and calls its step(). The closure must then return the loss as a tensor. gradient_estimate ignores it. Default None: SPSA alone.

None
model Module | None

The model the parameters belong to. When given, the shot sampling of its devices is synchronised between the two evaluations of a step (see Common random numbers). Default None: only the torch RNG is, unless train_model sets it. Seed the devices (seed= on the model) as well for runs that repeat exactly.

None

Attributes:

Name Type Description
gradient_free bool

True: train_model calls step(closure) without backward.

gradient_free class-attribute instance-attribute

gradient_free = True

generator instance-attribute

generator = generator if generator is not None else torch.Generator().manual_seed(torch.initial_seed())

model instance-attribute

model = model

k instance-attribute

k = 0

gradient_optimizer instance-attribute

gradient_optimizer = gradient_optimizer

add_param_group

add_param_group(param_group: dict[str, Any]) -> None

gradient_estimate

gradient_estimate(closure: Closure) -> list[torch.Tensor]

One SPSA gradient estimate at the current parameters, without a step.

In the order of the parameters that require grad, group by group. Uses two loss evaluations and one draw from generator.

step

step(closure: Closure | None = None) -> float

One SPSA step. Returns the mean of the two loss evaluations, an estimate of the loss at the parameters the step started from.

Raises:

Type Description
ValueError

Without a closure: SPSA evaluates the loss itself.

EpochRecord dataclass

Statistics for one epoch.

epoch instance-attribute

epoch: int

train_loss instance-attribute

train_loss: float

val_loss class-attribute instance-attribute

val_loss: float | None = None

val_score class-attribute instance-attribute

val_score: float | None = None

val_threshold class-attribute instance-attribute

val_threshold: float | None = None

TrainingHistory dataclass

What train_model did.

Attributes:

Name Type Description
epochs list[EpochRecord]

One EpochRecord per completed epoch, in order.

monitor str

The monitored quantity.

best_epoch int | None

1-based epoch with the best monitored value, or None without validation data.

best_value float | None

The monitored value at best_epoch.

best_threshold float | None

Decision threshold at best_epoch (metric monitors only).

stopped_early bool

True if patience ran out before max_epochs.

temperature float | None

The TemperatureScaler temperature fitted on the validation split for the returned weights, or None without a validation split, for a multiclass model, or when it cannot be fitted (non-binary targets such as smoothed labels, one class, non-finite logits, or logits that separate the validation classes or rank them no better than chance, where the NLL has no finite optimum in T). T > 1 means the model is over-confident on held-out data; σ(logits / T) is the calibrated probability (#319).

restored_best bool

True if the model's weights were rolled back to best_epoch.

monitor instance-attribute

monitor: str

epochs class-attribute instance-attribute

epochs: list[EpochRecord] = field(default_factory=list)

best_epoch class-attribute instance-attribute

best_epoch: int | None = None

best_value class-attribute instance-attribute

best_value: float | None = None

best_threshold class-attribute instance-attribute

best_threshold: float | None = None

stopped_early class-attribute instance-attribute

stopped_early: bool = False

restored_best class-attribute instance-attribute

restored_best: bool = False

temperature class-attribute instance-attribute

temperature: float | None = None

train_loss property

train_loss: list[float]

n_epochs property

n_epochs: int

train_model

train_model(model: Module, loss_fn: LossFn, optimizer: Optimizer, X_train: Tensor, y_train: Tensor, X_val: Tensor | None = None, y_val: Tensor | None = None, *, max_epochs: int = 100, batch_size: int = 256, monitor: str = 'mcc', patience: int | None = 10, min_delta: float = 0.0, restore_best: bool = True, generator: Generator | None = None, on_epoch_end: Callable[[EpochRecord], None] | None = None) -> TrainingHistory

Train model with mini-batches, validating and early-stopping per epoch.

Parameters:

Name Type Description Default
model Module

Any module mapping (batch, n_features) to logits of shape (batch,) or (batch, 1) (binary), or (batch, n_classes) (multiclass).

required
loss_fn LossFn

loss_fn(logits, targets) -> scalar, e.g. FocalLoss() or nn.BCEWithLogitsLoss(). Targets are passed as float for a binary model and as integer class indices for a multiclass one. Mean reduction is assumed for the reported train_loss, which averages the batch losses weighted by batch size; with reduction="sum" training is unaffected but train_loss is comparable neither across batch sizes nor with the full-batch val_loss.

required
optimizer Optimizer

Optimiser already bound to the parameters to train. A gradient-free one (optimizer.gradient_free, e.g. SPSA) gets step(closure) with a closure that returns the batch loss tensor, and no backward pass of its own (SPSA's gradient_optimizer backpropagates that loss to the classical head only). An SPSA built without model= is given model, so its two evaluations share the devices' shot noise.

required
X_train Tensor

Training split. For a multiclass model every label of y_train and y_val must be a class index in [0, n_classes) (integer, or a whole-valued float); anything else raises ValueError before the first optimiser step. A binary model's labels are passed to the loss as float, unchecked, as before.

required
y_train Tensor

Training split. For a multiclass model every label of y_train and y_val must be a class index in [0, n_classes) (integer, or a whole-valued float); anything else raises ValueError before the first optimiser step. A binary model's labels are passed to the loss as float, unchecked, as before.

required
X_val Tensor | None

Validation split. Without it the loop runs max_epochs epochs and monitor, patience and restore_best have no effect.

None
y_val Tensor | None

Validation split. Without it the loop runs max_epochs epochs and monitor, patience and restore_best have no effect.

None
max_epochs int

Upper bound on the number of epochs. Default: 100.

100
batch_size int

Mini-batch size. The last batch may be smaller, but for batch_size > 1 never a single sample unless the training set is one: a remainder of one is merged into the batch before it, which then holds batch_size + 1, because batch norm in train mode fails on one sample. With batch_size=1 every batch is one sample, as asked. Default: 256.

256
monitor str

"mcc" (default), "f1", "balanced_accuracy" or "val_loss".

'mcc'
patience int | None

Stop after this many consecutive epochs without an improvement larger than min_delta. None disables early stopping. Default: 10.

10
min_delta float

Minimum change that counts as an improvement. Default: 0.0.

0.0
restore_best bool

Load the weights of the best epoch before returning. Default: True.

True
generator Generator | None

Generator for the per-epoch shuffle, for reproducible batch order.

None
on_epoch_end Callable[[EpochRecord], None] | None

Called with each EpochRecord, e.g. for logging.

None

Returns:

Type Description
TrainingHistory
Notes

Training runs under train_mode: a submodule the caller put in eval mode -- a frozen batch-norm layer, say -- stays in eval mode, with its statistics untouched, unless the whole model arrived in eval mode, which is then trained in train mode throughout. Validation runs in eval mode through eval_mode. Every epoch starts from the modes training began with, so an on_epoch_end callback that puts the model in eval mode does not carry over into the next epoch. On return every submodule has the mode it had on entry.