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 = pify = 1(positive class probability)p_t = 1-pify = 0α_t = αify = 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'
|
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
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 |
required |
targets
|
Tensor
|
Binary ground-truth labels (0.0 or 1.0), same shape as logits. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Scalar loss if |
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, |
2.0
|
Forward
logits of shape (N, K) and integer class indices (N,); returns
the mean loss.
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 |
required |
Returns:
| Type | Description |
|---|---|
ClassicalBaseline
|
With |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
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 |
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 |
ValueError
|
If |
RuntimeError
|
If the layer's |
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 |
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
|
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 |
RuntimeError
|
If the layer's |
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'
|
allow_version_mismatch
|
bool
|
Load a checkpoint written by a different |
False
|
allow_architecture_override
|
bool
|
Permit |
False
|
**overrides
|
Any
|
Constructor arguments that replace the stored ones. Without
|
{}
|
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 |
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
If the stored config is missing constructor arguments added after it
was written. They are filled from |
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 |
required |
path
|
PathLike
|
Destination file; overwritten if it exists. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
ValueError
|
If |
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 |
required |
smooth
|
float
|
Additive smoothing term. Default: 1.0. |
1.0
|
Returns:
| Type | Description |
|---|---|
Tensor
|
1D tensor |
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 |
required |
targets
|
Tensor
|
Binary labels (0.0 / 1.0), same shape as logits. |
required |
class_weights
|
Tensor
|
Tensor |
required |
reduction
|
str
|
|
'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_VERSIONcovers. 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_DEFAULTSgiving the value that reproduces what the circuit did before the argument existed -- the old behaviour, never the new default -- so older files load with aRuntimeWarning; 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.