Skip to content

Utilities

hqnn_forge.utils

hqnn_forge.utils

Imbalance-robust losses, checkpoint save/load, quantum-layer ablation and module-mode helpers.

Exported symbols

FocalLoss nn.Module implementing Focal Loss (Lin et al. 2017). SoftmaxFocalLoss Its K-class softmax version. weighted_bce_loss Functional helper: inverse-class-frequency weighted BCE. compute_class_weights Computes inverse-frequency class weights from a label tensor. eval_mode Context manager: eval mode for a block, submodule modes restored. train_mode Context manager: train mode for a block, frozen submodules left frozen. save_checkpoint Write a classifier's class, constructor arguments and weights. load_checkpoint Rebuild a classifier from such a file. disable_quantum_layer Context manager: replace the quantum layer's output with a constant. classical_baseline Untrained MLP matched in parameter count: the classical control. permute_quantum_layer Context manager: shuffle the quantum layer's output across the batch.

FocalLoss

Bases: Module

Focal Loss for binary classification on imbalanced data.

Focal Loss modifies the standard BCE by adding a weighting factor (1 - p_t)^γ that down-weights well-classified (easy) examples and focuses learning on hard, misclassified examples:

FL(p_t) = -α_t · (1 - p_t)^γ · log(p_t)

where:

  • p_t = p if y = 1 (positive class probability)
  • p_t = 1-p if y = 0
  • α_t = α if y = 1, else α_t = 1 - α

Parameters:

Name Type Description Default
alpha float

Weighting factor for the positive class ∈ (0, 1). Set higher for stronger minority-class emphasis. Default: 0.25.

0.25
gamma float

Focusing parameter γ ≥ 0. γ=0 recovers standard BCE. Typical values: 1.0 – 5.0. Default: 2.0.

2.0
reduction str

"mean" | "sum" | "none". Default: "mean".

'mean'

Examples:

>>> import torch
>>> from hqnn_forge.utils import FocalLoss
>>> loss_fn = FocalLoss(alpha=0.25, gamma=2.0)
>>> logits = torch.tensor([0.8, -0.3, 1.2, -1.5])
>>> targets = torch.tensor([1.0, 0.0, 1.0, 0.0])
>>> loss = loss_fn(logits, targets)
>>> round(loss.item(), 4)  # scalar
0.0232

alpha instance-attribute

alpha = alpha

gamma instance-attribute

gamma = gamma

reduction instance-attribute

reduction = reduction

forward

forward(logits: Tensor, targets: Tensor) -> torch.Tensor

Compute the focal loss.

Parameters:

Name Type Description Default
logits Tensor

Raw model outputs (before sigmoid), shape (N,) or (N, 1).

required
targets Tensor

Binary ground-truth labels (0.0 or 1.0), same shape as logits.

required

Returns:

Type Description
Tensor

Scalar loss if reduction != "none", else per-sample tensor.

extra_repr

extra_repr() -> str

SoftmaxFocalLoss

Bases: Module

Focal loss for K-class softmax classification (Lin et al. 2017).

FL = −(1 − p_t)^γ · log p_t with p_t the softmax probability of the true class: cross-entropy that down-weights well-classified samples. γ = 0 is nn.CrossEntropyLoss exactly. (For one-vs-rest heads, the binary FocalLoss applied to one-hot targets is the per-class version.)

Parameters:

Name Type Description Default
gamma float

Focusing parameter, ≥ 0. Default: 2.0.

2.0
Forward

logits of shape (N, K) and integer class indices (N,); returns the mean loss.

gamma instance-attribute

gamma = gamma

forward

forward(logits: Tensor, targets: Tensor) -> torch.Tensor

classical_baseline

classical_baseline(model: Module) -> ClassicalBaseline

Untrained classical control for model, matched in parameter count.

See "Matched capacity" above for the architecture and the matching rule. Train it on the same data with the same recipe as the hybrid model; its score is the classical reference the hybrid's has to beat.

Parameters:

Name Type Description Default
model Module

A HybridBinaryClassifier or ParallelHybridClassifier.

required

Returns:

Type Description
ClassicalBaseline

With count_parameters() within half a width step of model's live count, model.count_parameters() - circuit_summary(model).n_inert_params, and model's n_input_features, dropout_p and init_seed.

Raises:

Type Description
TypeError

If model is not one of the two binary hybrid classifiers.

disable_quantum_layer

disable_quantum_layer(model: Module, fill: float = 0.0) -> Iterator[nn.Module]

Replace model.quantum_layer's output with the constant fill.

Inside the block the layer returns a tensor of shape (*batch_dims, n_outputs) filled with fill, in the input's dtype and device, without running the circuit. n_outputs is the layer's readout width -- n_qubits for readout="all", 1 for readout="first" -- so the head downstream sees the width it was built for. The output therefore carries no gradient to the quantum weights or to anything upstream of it. The original forward is restored on exit, including when the block raises.

Training inside the block is allowed and trains only the parameters that still influence the loss.

Parameters:

Name Type Description Default
model Module

A hybrid classifier with a quantum_layer attribute whose layer has n_qubits.

required
fill float

Constant output per qubit. Must lie in [-1, 1], the range of a Pauli-Z expectation. Default: 0.0.

0.0

Yields:

Type Description
Module

The disabled quantum layer.

Raises:

Type Description
TypeError

If model has no suitable quantum_layer.

ValueError

If fill is outside [-1, 1], or, inside the block, if the layer is called with an input whose last dimension is not n_qubits.

RuntimeError

If the layer's forward is already replaced (nested use on the same model, including inside permute_quantum_layer).

permute_quantum_layer

permute_quantum_layer(model: Module, generator: Generator | None = None) -> Iterator[nn.Module]

Shuffle model.quantum_layer's output across the batch.

The permutation null of feature-importance work. Inside the block the circuit still runs, and its (batch, n_outputs) output is returned with the rows permuted: every sample receives some other sample's quantum readout. The readouts keep their distribution -- the same rows, the same spread -- and lose only their correspondence to the input, so the head sees in-distribution values that carry no information about this sample. disable_quantum_layer removes both at once: the correspondence and all variance. A model that scores the same under both nulls depends on the readout's scale, not its content; one that recovers under the permutation needs something non-constant in those slots.

Unlike the constant null, this one is meaningful for HybridBinaryClassifier: a permuted serial model is not a constant predictor. Each forward pass draws a fresh permutation of its batch, so score a validation set in one batch, or accept that the null is drawn per batch.

The permuted output is detached: no gradient reaches the quantum weights or anything upstream of the layer. The original forward is restored on exit, including when the block raises.

Parameters:

Name Type Description Default
model Module

A hybrid classifier with a quantum_layer attribute whose layer has n_qubits.

required
generator Generator | None

Source of the permutations. Pass a seeded one: the ablated score is a random variable, and without a seed it cannot be reproduced. None draws from the global torch RNG.

None

Yields:

Type Description
Module

The permuted quantum layer.

Warns:

Type Description
RuntimeWarning

When a forward pass leaves the output unchanged: a batch of one sample (or an unbatched input), or a draw of the identity permutation. The "ablated" output is then the real one, and a score computed from it says nothing about the null.

Raises:

Type Description
TypeError

If model has no suitable quantum_layer.

RuntimeError

If the layer's forward is already replaced (nested use on the same model, including inside disable_quantum_layer).

load_checkpoint

load_checkpoint(path: PathLike, *, map_location: str | device = 'cpu', allow_version_mismatch: bool = False, allow_architecture_override: bool = False, **overrides: Any) -> ClassifierBase

Rebuild a classifier saved with save_checkpoint.

Parameters:

Name Type Description Default
path PathLike

Checkpoint file.

required
map_location str | device

Where the rebuilt model ends up: the stored tensors are read onto this device and the model is moved there before its weights are loaded. Default: "cpu". Note that this places the classical layers and the variational parameters only -- which simulator executes the circuit is chosen by the device_name argument, so moving a model to a GPU generally means overriding device_name as well.

'cpu'
allow_version_mismatch bool

Load a checkpoint written by a different hqnn_forge version. Off by default because weight layouts are not guaranteed stable across versions before 1.0.

False
allow_architecture_override bool

Permit **overrides outside WEIGHT_SAFE_ARGS. Off by default: an architecture override makes the rebuilt model something other than the one that was saved, and the mismatch is not always loud. encoding_type is the dangerous case -- QuantumEncodingLayer and IQPEncodingLayer both register their weights at (n_layers, n_qubits, 3), so an "angle" checkpoint loaded as "iqp" fits, raises nothing, and predicts differently. A model rebuilt this way is marked and save_checkpoint refuses it, so the mismatch cannot be laundered into a fresh checkpoint.

False
**overrides Any

Constructor arguments that replace the stored ones. Without allow_architecture_override, only WEIGHT_SAFE_ARGS (device_name, diff_method, dropout_p, the five noise_* training-noise options, readout_error, shots and seed) may be given -- typically to run a saved model on a different simulator or with a finite shot count, or to fine-tune it at a different dropout rate or training-noise level.

{}

Returns:

Type Description
ClassifierBase

The rebuilt model with the saved weights, in eval mode.

Raises:

Type Description
ValueError

If the file is not a checkpoint or is incomplete, on an unknown format version, a library version mismatch (unless allowed), an unknown class, an override outside WEIGHT_SAFE_ARGS without allow_architecture_override, a malformed known_args, or a config with unexpected fields or with missing ones that cannot be filled: a name outside _LEGACY_DEFAULTS, a name in _NO_LEGACY_DEFAULT, or a name the file's known_args lists (the config was edited or truncated).

Warns:

Type Description
RuntimeWarning

If the stored config is missing constructor arguments added after it was written. They are filled from _LEGACY_DEFAULTS, the behaviour from before each argument existed, and named in the warning, so a checkpoint from before an option was introduced still rebuilds the model it holds.

RuntimeError

If the stored weights do not fit the rebuilt architecture.

save_checkpoint

save_checkpoint(model: ClassifierBase, path: PathLike) -> None

Write model's class, constructor arguments and weights to path.

Parameters:

Name Type Description Default
model ClassifierBase

A classifier from hqnn_forge.models.

required
path PathLike

Destination file; overwritten if it exists.

required

Raises:

Type Description
TypeError

If model is not one of the library's classifiers.

ValueError

If model has a custom classical_encoder: a module is code, not data, and the file must load with weights_only=True. Save model.state_dict() and rebuild the model with the same encoder.

ValueError

If model came from a load_checkpoint with forced architecture overrides. Its get_config() reports the overridden architecture while its weights were trained in the original one, so the checkpoint would be internally consistent and wrong -- and would load back without any override, past the guard that caught it the first time.

compute_class_weights

compute_class_weights(labels: Tensor, *, smooth: float = 1.0) -> torch.Tensor

Compute inverse-frequency class weights for a binary label tensor.

The weight for class c is:

w_c = N / (2 * n_c + smooth)

where N is total samples and n_c is the count of class c. The smooth term avoids zero division for classes absent in a mini-batch.

Parameters:

Name Type Description Default
labels Tensor

Integer or float label tensor of shape (N,) containing 0s and 1s.

required
smooth float

Additive smoothing term. Default: 1.0.

1.0

Returns:

Type Description
Tensor

1D tensor [w_neg, w_pos] — weights for class 0 and class 1.

Examples:

>>> import torch
>>> from hqnn_forge.utils import compute_class_weights
>>> y = torch.tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1])  # 9:1 imbalance
>>> compute_class_weights(y)  # [10 / (2*9 + 1), 10 / (2*1 + 1)]
tensor([0.5263, 3.3333])
>>> compute_class_weights(y, smooth=0.0)  # [10 / (2*9), 10 / (2*1)]
tensor([0.5556, 5.0000])

weighted_bce_loss

weighted_bce_loss(logits: Tensor, targets: Tensor, class_weights: Tensor, *, reduction: str = 'mean') -> torch.Tensor

Binary cross-entropy loss with per-sample class weighting.

Parameters:

Name Type Description Default
logits Tensor

Raw model outputs (pre-sigmoid), shape (N,) or (N, 1).

required
targets Tensor

Binary labels (0.0 / 1.0), same shape as logits.

required
class_weights Tensor

Tensor [w_neg, w_pos] from compute_class_weights.

required
reduction str

"mean" | "sum" | "none".

'mean'

Returns:

Type Description
Tensor

Weighted loss, scalar or per-sample depending on reduction.

Raises:

Type Description
ValueError

If reduction is not one of the three values above.

Examples:

>>> import torch
>>> from hqnn_forge.utils import compute_class_weights, weighted_bce_loss
>>> y    = torch.tensor([0.0, 0.0, 0.0, 1.0])
>>> pred = torch.tensor([0.1, -0.2, 0.05, 0.9])
>>> cw   = compute_class_weights(y)
>>> weighted_bce_loss(pred, y, cw)
tensor(0.4081)

eval_mode

eval_mode(module: Module) -> Iterator[None]

Put module and all its submodules in eval mode for the with block.

On exit, including when the block raises, every submodule gets back the training flag it had on entry. A single module.train(was_training) would not do that: it overwrites submodules the caller had put in a different mode, such as a model in train mode with its dropout frozen.

Parameters:

Name Type Description Default
module Module

Module to switch.

required

Examples:

>>> import torch
>>> from torch import nn
>>> from hqnn_forge.utils import eval_mode
>>> model = nn.Sequential(nn.Linear(4, 1), nn.Dropout(0.5))
>>> with eval_mode(model):
...     logits = model(torch.ones(2, 4))  # dropout off
...     model.training, model[1].training
(False, False)
>>> model.training, model[1].training  # train mode is back
(True, True)

train_mode

train_mode(module: Module) -> Iterator[None]

Train module for the with block without unfreezing what the caller froze.

If any submodule is in train mode on entry, every submodule keeps the mode it has: a model in train mode with a batch-norm layer put in eval mode trains with that layer still frozen, and so does a model put in eval mode with only its head switched back to train mode. Only if module and all its submodules are in eval mode -- a model fresh from load_checkpoint, say -- is there no training configuration to respect, and every submodule is put in train mode. On exit, including when the block raises, every submodule gets back the training flag it had on entry, as with eval_mode.

Parameters:

Name Type Description Default
module Module

Module to switch.

required

Examples:

A model loaded in eval mode is put in train mode for the block:

>>> import torch
>>> from torch import nn
>>> from hqnn_forge.utils import train_mode
>>> model = nn.Sequential(nn.Linear(4, 4), nn.BatchNorm1d(4), nn.Linear(4, 1)).eval()
>>> with train_mode(model):
...     model.training, model[1].training
(True, True)
>>> model.training, model[1].training  # eval mode is back
(False, False)

A batch-norm layer the caller froze stays frozen:

>>> _ = model.train()
>>> _ = model[1].eval()
>>> with train_mode(model):
...     loss = model(torch.ones(2, 4)).sum()
...     model.training, model[1].training
(True, False)

Checkpoint format

hqnn_forge.utils.checkpoint

hqnn_forge.utils.checkpoint

Save and reload a hybrid classifier together with its architecture.

torch.save(model.state_dict()) alone is not enough to get a model back: the state dict holds weights, not n_qubits, n_layers, encoding_type or the other constructor arguments needed to build a module those weights fit. A checkpoint written here stores both, plus the class name and the library version, and load_checkpoint rebuilds the model and loads the weights in one call.

Format

A torch.save file containing a plain dict::

{
    "format_version": 1,
    "hqnn_forge_version": "0.1.0",
    "class_name": "HybridBinaryClassifier",
    "config": {...constructor kwargs...},
    "known_args": [...every constructor argument the writer had...],
    "state_dict": {...},
}

known_args was added after the first checkpoints were written; a file without it is read exactly as before (see the rules below).

Compatibility rules

Two things decide whether a file still rebuilds its model, and each has its own mechanism:

  • The dict layout is what FORMAT_VERSION covers. It is bumped when a key is removed, renamed, or changes meaning, i.e. when a reader of the old version would misread the new file or the reverse. Adding a key that an older reader ignores and a newer one does not require (known_args) is not a bump.
  • The constructors are covered by the config. Every argument is stored with its value, so a changed default never touches a checkpoint that recorded the argument, and needs no entry anywhere. An added argument is missing from every older file, and the PR adding it must record a decision:

  • an entry in _LEGACY_DEFAULTS giving the value that reproduces what the circuit did before the argument existed -- the old behaviour, never the new default -- so older files load with a RuntimeWarning; or

  • an entry in _NO_LEGACY_DEFAULT, when no value reproduces the old model, so older files missing it are refused.

tests/test_checkpoint.py pins every constructor's arguments and fails when one appears without either entry. A removed or renamed argument makes older files carry an unexpected key, which is refused; supporting them needs a migration in load_checkpoint. A value a constructor stops accepting is refused the same way, by the constructor's own ValueError when load_checkpoint rebuilds the model: a file saved with embedding_rotation="Z" held a constant quantum layer (#212) and no longer loads.

known_args makes the added-argument case explicit. A name missing from the config but absent from known_args too was added after the file was written, and is filled from _LEGACY_DEFAULTS. A name missing from the config although known_args lists it means the config was edited or truncated, and is refused rather than filled. For a file without known_args the two cannot be told apart, and every missing name in _LEGACY_DEFAULTS is filled.

Only primitives and tensors are stored, so the file loads with torch.load(weights_only=True): loading a checkpoint never executes code from it, and only classes in hqnn_forge.models can be rebuilt.

FORMAT_VERSION module-attribute

FORMAT_VERSION: int = 1

Bumped whenever the dict layout above changes incompatibly.

PathLike module-attribute

PathLike = str | os.PathLike[str]

File paths accepted by the checkpoint readers and writers.

WEIGHT_SAFE_ARGS module-attribute

WEIGHT_SAFE_ARGS: frozenset[str] = frozenset({'device_name', 'diff_method', 'dropout_p', 'noise_level', 'noise_position', 'noise_method', 'noise_trajectories', 'shots', 'noise_channel', 'seed', 'readout_error'})

Constructor arguments whose override cannot invalidate the stored weights: the two simulator knobs, plus the train-mode-only regularisers -- dropout_p (nn.Dropout has no parameters of its own) and the training-noise options, which only replace the circuit in train mode, and shots, which samples the same circuit, and seed, which only seeds that sampling. None of them touches a weight. load_checkpoint takes these without an opt-in; every other argument describes the circuit the weights were trained in, so overriding it needs allow_architecture_override=True. That includes init_strategy and init_seed, although the loaded state dict overwrites what they draw: they record where the initial weights came from, and an override would falsify that.