Skip to content

API: Calibrators

Base class

Calibrator

Calibrator(*, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: ABC

Abstract base class for post-hoc segmentation calibrators.

All calibrators share the same interface and tensor convention:

  • predictions are channel-first, class axis at dimension 1 ((B, C, *spatial); (N, C) is also accepted);
  • targets are integer labels (B, *spatial) or floating class distributions (B, C, *spatial), including mean human responses;
  • mask, when given, is (B, *spatial) with True for valid voxels.

Inputs may be raw logits or normalised probs (see input_type). transform and predict_proba always return calibrated probabilities of the same shape as the input.

Parameters:

Name Type Description Default
input_type str

"logits" (default) if predictions are unnormalised scores, or "probs" if they are probabilities that sum to 1 along the class axis.

'logits'
ignore_index int

Label value excluded from fitting (default -100).

-100
device DeviceLike | None

Computation device. None selects CUDA when available, else CPU.

None
Notes

Calibrators fitted by gradient descent also accept, as keyword-only constructor arguments, an optional validation-based early stopping rule (Adam only):

  • patience -- stop after this many iterations without the validation NLL improving by more than min_delta (default 0.0);
  • lr_patience / lr_factor -- multiply the learning rate by lr_factor (default 0.1) after lr_patience iterations without improvement (ReduceLROnPlateau).

Setting either requires val_predictions / val_targets in :meth:fit; the iterate with the best validation NLL is kept. With max_iter acting as an upper bound, this reproduces only the Adam + validation-NLL early-stopping recipe; the paper's case batching, class weighting, schedules, clipping and hyperparameter search are not part of the library.

Source code in src/fiducio/base.py
def __init__(
    self,
    *,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    if input_type not in {"logits", "probs"}:
        raise ValueError(f"input_type must be 'logits' or 'probs', got {input_type!r}")
    self.input_type = input_type
    self.ignore_index = int(ignore_index)
    self.device = resolve_device(device)
    self._num_classes: int | None = None
    self._fitted: bool = False
    self._val: tuple[torch.Tensor, torch.Tensor] | None = None

is_fitted property

is_fitted: bool

Whether :meth:fit has been called.

num_classes property

num_classes: int | None

Number of classes seen at fit time, or None before fitting.

fit

fit(predictions: Any, targets: Any, mask: Any | None = None, *, val_predictions: Any | None = None, val_targets: Any | None = None, val_mask: Any | None = None) -> Calibrator

Fit the calibrator on a labelled calibration set.

Parameters:

Name Type Description Default
predictions Any

(B, C, *spatial) logits or probabilities (see input_type).

required
targets Any

(B, *spatial) integer labels or (B, C, *spatial) floating distributions. Soft targets must sum to 1; exclude positions with mask rather than ignore_index. Each valid voxel has equal weight.

required
mask Any | None

Optional (B, *spatial) boolean mask; True marks valid voxels.

None
val_predictions Any | None

Optional held-out validation set, in the same format as predictions / targets / mask. Required when early stopping is configured (patience or lr_patience), and rejected otherwise. The validation NLL (without regularization) is monitored after every Adam step and the best iterate is kept.

None
val_targets Any | None

Optional held-out validation set, in the same format as predictions / targets / mask. Required when early stopping is configured (patience or lr_patience), and rejected otherwise. The validation NLL (without regularization) is monitored after every Adam step and the best iterate is kept.

None
val_mask Any | None

Optional held-out validation set, in the same format as predictions / targets / mask. Required when early stopping is configured (patience or lr_patience), and rejected otherwise. The validation NLL (without regularization) is monitored after every Adam step and the best iterate is kept.

None
Notes

Calling fit again on an already-fitted instance re-fits from scratch: all learned parameters are reinitialised and overwritten, and a new num_classes (which may differ from the previous fit) is recorded. No state from the previous fit is reused. If fitting fails, the previous state is preserved. Model outputs are detached from autograd.

Source code in src/fiducio/base.py
def fit(
    self,
    predictions: Any,
    targets: Any,
    mask: Any | None = None,
    *,
    val_predictions: Any | None = None,
    val_targets: Any | None = None,
    val_mask: Any | None = None,
) -> Calibrator:
    """Fit the calibrator on a labelled calibration set.

    Parameters
    ----------
    predictions:
        ``(B, C, *spatial)`` logits or probabilities (see ``input_type``).
    targets:
        ``(B, *spatial)`` integer labels or ``(B, C, *spatial)`` floating
        distributions. Soft targets must sum to 1; exclude positions with
        ``mask`` rather than ``ignore_index``. Each valid voxel has equal weight.
    mask:
        Optional ``(B, *spatial)`` boolean mask; ``True`` marks valid voxels.
    val_predictions, val_targets, val_mask:
        Optional held-out validation set, in the same format as
        ``predictions`` / ``targets`` / ``mask``. Required when early
        stopping is configured (``patience`` or ``lr_patience``), and
        rejected otherwise. The validation NLL (without regularization) is
        monitored after every Adam step and the best iterate is kept.

    Notes
    -----
    Calling ``fit`` again on an already-fitted instance re-fits from
    scratch: all learned parameters are reinitialised and overwritten, and
    a new ``num_classes`` (which may differ from the previous fit) is
    recorded. No state from the previous fit is reused. If fitting fails,
    the previous state is preserved. Model outputs are detached from autograd.
    """
    preds, tgts, msk = self._prepare(predictions, targets, mask, with_targets=True)
    assert tgts is not None  # guaranteed by with_targets=True
    num_classes = validate_predictions(
        preds,
        input_type=self.input_type,
        mask=validation_positions(preds, tgts, msk, self.ignore_index),
    )
    validate_targets(preds, tgts, msk, num_classes=num_classes, ignore_index=self.ignore_index)
    canonical = self._to_canonical(preds)
    z_flat, y_flat = flatten_valid(canonical, tgts, msk, self.ignore_index)
    if z_flat.shape[0] == 0:
        raise ValueError(
            "no valid voxels to fit on (all positions are masked out or equal ignore_index)"
        )
    self._refresh_stopping()
    val_data = self._prepare_validation(
        val_predictions, val_targets, val_mask, num_classes=num_classes
    )
    candidate = deepcopy(self)
    candidate._num_classes = num_classes
    candidate._val = val_data
    try:
        candidate._fit_core(z_flat, y_flat, num_classes)
    finally:
        candidate._val = None
    candidate._fitted = True
    self.__dict__.update(candidate.__dict__)
    return self

transform

transform(predictions: Any, mask: Any | None = None) -> torch.Tensor

Apply calibration and return probabilities of the input shape.

Parameters:

Name Type Description Default
predictions Any

(B, C, *spatial) logits or probabilities.

required
mask Any | None

Optional (B, *spatial) boolean mask. Masked-out voxels are set to 0 across all classes in the output.

None

Returns:

Type Description
Tensor

Calibrated probabilities, always in float32 (calibrators compute in float32, so float64 inputs do not yield float64 outputs).

Source code in src/fiducio/base.py
def transform(self, predictions: Any, mask: Any | None = None) -> torch.Tensor:
    """Apply calibration and return probabilities of the input shape.

    Parameters
    ----------
    predictions:
        ``(B, C, *spatial)`` logits or probabilities.
    mask:
        Optional ``(B, *spatial)`` boolean mask. Masked-out voxels are set to
        0 across all classes in the output.

    Returns
    -------
    torch.Tensor
        Calibrated probabilities, always in ``float32`` (calibrators compute
        in float32, so ``float64`` inputs do not yield ``float64`` outputs).
    """
    if not self._fitted:
        raise NotFittedError("call fit() before transform()")
    logits, msk = self._calibrated_logits(predictions, mask)
    probs = F.softmax(logits, dim=1)
    return apply_mask_to_probabilities(probs, msk)

predict_proba

predict_proba(predictions: Any, mask: Any | None = None) -> torch.Tensor

Alias for :meth:transform; returns calibrated probabilities.

Source code in src/fiducio/base.py
def predict_proba(self, predictions: Any, mask: Any | None = None) -> torch.Tensor:
    """Alias for :meth:`transform`; returns calibrated probabilities."""
    return self.transform(predictions, mask=mask)

decision_function

decision_function(predictions: Any) -> torch.Tensor

Return calibrated logits (pre-softmax) of the input shape.

The mask argument of :meth:transform is deliberately not accepted here: a masked logit of 0 is a meaningful value, and argmax over its output at masked voxels is meaningless. Use :meth:transform when masks matter. softmax of the result along dimension 1 equals :meth:transform without a mask.

Source code in src/fiducio/base.py
def decision_function(self, predictions: Any) -> torch.Tensor:
    """Return calibrated **logits** (pre-softmax) of the input shape.

    The ``mask`` argument of :meth:`transform` is deliberately not accepted
    here: a masked logit of 0 is a meaningful value, and ``argmax`` over its
    output at masked voxels is meaningless. Use :meth:`transform` when masks
    matter. ``softmax`` of the result along dimension 1 equals
    :meth:`transform` without a mask.
    """
    if not self._fitted:
        raise NotFittedError("call fit() before decision_function()")
    logits, _ = self._calibrated_logits(predictions, None)
    return logits

fit_transform

fit_transform(predictions: Any, targets: Any, mask: Any | None = None, *, val_predictions: Any | None = None, val_targets: Any | None = None, val_mask: Any | None = None) -> torch.Tensor

Fit on the calibration set, then transform the same predictions.

Source code in src/fiducio/base.py
def fit_transform(
    self,
    predictions: Any,
    targets: Any,
    mask: Any | None = None,
    *,
    val_predictions: Any | None = None,
    val_targets: Any | None = None,
    val_mask: Any | None = None,
) -> torch.Tensor:
    """Fit on the calibration set, then transform the same predictions."""
    self.fit(
        predictions,
        targets,
        mask=mask,
        val_predictions=val_predictions,
        val_targets=val_targets,
        val_mask=val_mask,
    )
    return self.transform(predictions, mask=mask)

save

save(path: Any) -> None

Save this calibrator to path (see :func:fiducio.save_calibrator).

Source code in src/fiducio/base.py
def save(self, path: Any) -> None:
    """Save this calibrator to ``path`` (see :func:`fiducio.save_calibrator`)."""
    from .persistence import save_calibrator

    save_calibrator(self, path)

get_config

get_config() -> dict[str, Any]

Return constructor keyword arguments (excluding device).

Source code in src/fiducio/base.py
def get_config(self) -> dict[str, Any]:
    """Return constructor keyword arguments (excluding ``device``)."""
    self._refresh_stopping()
    config: dict[str, Any] = {
        "input_type": self.input_type,
        "ignore_index": self.ignore_index,
    }
    config.update(self._constructor_config())
    if self._stopping is not None:
        config.update(
            patience=self.patience,
            min_delta=self.min_delta,
            lr_patience=self.lr_patience,
            lr_factor=self.lr_factor,
        )
    return config

Temperature scaling

TemperatureScaling

TemperatureScaling(*, init_temperature: float = 1.0, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: Calibrator

Temperature scaling: softmax(z / T) with a single scalar T > 0.

The simplest post-hoc calibrator. It preserves class ordering in exact arithmetic; floating-point ties and rounding can change an argmax.

Parameters:

Name Type Description Default
init_temperature float

Initial temperature (must be positive).

1.0
optimizer str

"adam" (default) or "lbfgs".

'adam'
lr float | None

Learning rate. Defaults to 0.1 (Adam) or 1.0 (L-BFGS).

None
max_iter int | None

Maximum optimizer iterations. Defaults to 200 (Adam) or 100 (L-BFGS).

None
patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
min_delta int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_factor int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
input_type str

See :class:fiducio.Calibrator.

'logits'
ignore_index str

See :class:fiducio.Calibrator.

'logits'
device str

See :class:fiducio.Calibrator.

'logits'
References

Guo et al. (2017), On Calibration of Modern Neural Networks, ICML.

Source code in src/fiducio/calibrators/temperature.py
def __init__(
    self,
    *,
    init_temperature: float = 1.0,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.init_temperature = positive_finite(init_temperature, "init_temperature")
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.temperature: float = float(init_temperature)

Ensemble temperature scaling

EnsembleTemperatureScaling

EnsembleTemperatureScaling(*, init_temperature: float = 1.0, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: Calibrator

Ensemble Temperature Scaling.

Calibrated probabilities are a convex combination of a temperature-scaled distribution, the original distribution and the uniform distribution:

.. math:: p = w_0\,\mathrm{softmax}(z/T) + w_1\,\mathrm{softmax}(z) + w_2\,u

where :math:w lies on the 3-simplex and :math:u is uniform. Fitting is done in two stages: first the temperature T (NLL), then the mixture weights w with T fixed.

Parameters:

Name Type Description Default
init_temperature float

Initial temperature for stage 1.

1.0
optimizer str

"adam" (default) or "lbfgs".

'adam'
lr float | None

Learning rate. Defaults to 0.1 (Adam) or 1.0 (L-BFGS).

None
max_iter int | None

Maximum optimizer iterations per stage. Defaults to 200 (Adam) or 100 (L-BFGS).

None
patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
min_delta int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_factor int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
input_type str

See :class:fiducio.Calibrator.

'logits'
ignore_index str

See :class:fiducio.Calibrator.

'logits'
device str

See :class:fiducio.Calibrator.

'logits'
Notes

Class ordering is preserved in exact arithmetic: each mixture component is monotone in the input scores and the weights are non-negative, so the calibrated probabilities never strictly invert the input order. In float32 the mixture can round to exact ties, most visibly once w2 (the uniform component) dominates; argmax then resolves ties by class index and may differ from the uncalibrated argmax. Treat the guarantee as order preservation up to floating-point ties.

References

Adapted from Zhang et al. (2020), Mix-n-Match: Ensemble and Compositional Methods for Uncertainty Calibration in Deep Learning, ICML. Like the authors' reference code, fitting is sequential (T, then w with T fixed). Unlike it, both stages minimize the NLL rather than squared error, w is parameterized by a softmax instead of a constrained SLSQP solve, and stage 2 starts at w = [0.998, 0.001, 0.001] because a softmax cannot start at a one-hot.

Source code in src/fiducio/calibrators/ensemble_temperature.py
def __init__(
    self,
    *,
    init_temperature: float = 1.0,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.init_temperature = positive_finite(init_temperature, "init_temperature")
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.temperature: float = float(init_temperature)
    self.weights: torch.Tensor = torch.tensor([1.0, 0.0, 0.0], device=self.device)

Vector scaling

VectorScaling

VectorScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _AffineCalibrator

Vector scaling: softmax(diag(w) z + b).

A per-class generalisation of temperature scaling with one scale w_c and one bias b_c per class. Operates on logits (or log of probabilities when input_type='probs').

Parameters:

Name Type Description Default
optimizer str

"adam" (default) or "lbfgs".

'adam'
lr float | None

Learning rate. Defaults to 0.1 (Adam) or 1.0 (L-BFGS).

None
max_iter int | None

Maximum optimizer iterations. Defaults to 200 (Adam) or 100 (L-BFGS).

None
lambda_reg float

L2 penalty pulling the scale vector towards 1.

0.0
mu_reg float

L2 penalty on the bias vector.

0.0
patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
min_delta int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_factor int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
input_type str

See :class:fiducio.Calibrator.

'logits'
ignore_index str

See :class:fiducio.Calibrator.

'logits'
device str

See :class:fiducio.Calibrator.

'logits'
References

Guo et al. (2017), On Calibration of Modern Neural Networks, ICML.

Source code in src/fiducio/calibrators/_affine.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self._weight: torch.Tensor | None = None  # (C, C) or (C,) for diagonal
    self._bias: torch.Tensor | None = None  # (C,)
    self._row_sum: torch.Tensor | None = None

Matrix scaling

MatrixScaling

MatrixScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _AffineCalibrator

Matrix scaling: softmax(W z + b) with a full C x C matrix.

Optional off-diagonal / bias L2 regularisation (ODIR) penalizes class mixing and bias. Diagonal entries are not directly penalized.

Parameters:

Name Type Description Default
optimizer str

"adam" (default) or "lbfgs".

'adam'
lr float | None

Learning rate. Defaults to 0.1 (Adam) or 1.0 (L-BFGS).

None
max_iter int | None

Maximum optimizer iterations. Defaults to 200 (Adam) or 100 (L-BFGS).

None
lambda_reg float

L2 penalty on the off-diagonal entries of W (ODIR).

0.0
mu_reg float

L2 penalty on the bias vector (ODIR).

0.0
patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
min_delta int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_factor int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
input_type str

See :class:fiducio.Calibrator.

'logits'
ignore_index str

See :class:fiducio.Calibrator.

'logits'
device str

See :class:fiducio.Calibrator.

'logits'
References

Guo et al. (2017), On Calibration of Modern Neural Networks, ICML; Kull et al. (2019) for off-diagonal/intercept regularisation.

Source code in src/fiducio/calibrators/_affine.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self._weight: torch.Tensor | None = None  # (C, C) or (C,) for diagonal
    self._bias: torch.Tensor | None = None  # (C,)
    self._row_sum: torch.Tensor | None = None

Translation-invariant matrix scaling

TranslationInvariantMatrixScaling

TranslationInvariantMatrixScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _AffineCalibrator

Constrained matrix scaling that is invariant to logit translations.

The matrix W is constrained so that every row has the same learned sum. Because softmax ignores constant shifts of its input, this makes the calibrated output invariant to adding the same constant to every input logit (g(z + c·1) = g(z)). The first C-1 columns and the common row sum are optimized, with the final column reconstructed. Initialization is the identity and regularization uses the reconstructed matrix, matching the research MSc parameterization.

Parameters are identical to :class:MatrixScaling.

Source code in src/fiducio/calibrators/_affine.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self._weight: torch.Tensor | None = None  # (C, C) or (C,) for diagonal
    self._bias: torch.Tensor | None = None  # (C,)
    self._row_sum: torch.Tensor | None = None

Dirichlet calibration

DirichletCalibration

DirichletCalibration(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _AffineCalibrator

Dirichlet calibration: softmax(W log p + b).

A log-linear transform in probability space, equivalent to matrix scaling applied to log-probabilities. When input_type='logits' the inputs are converted to log-probabilities with log_softmax before calibration. Off-diagonal / bias L2 regularisation (ODIR) is recommended.

Parameters:

Name Type Description Default
optimizer str

"adam" (default) or "lbfgs".

'adam'
lr float | None

Learning rate. Defaults to 0.1 (Adam) or 1.0 (L-BFGS).

None
max_iter int | None

Maximum optimizer iterations. Defaults to 200 (Adam) or 100 (L-BFGS).

None
lambda_reg float

L2 penalty on the off-diagonal entries of W (ODIR).

0.0
mu_reg float

L2 penalty on the bias vector (ODIR).

0.0
patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
min_delta int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_patience int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
lr_factor int | None

Optional validation-based early stopping (Adam only), see :class:fiducio.Calibrator. fit then requires val_predictions and val_targets.

None
input_type str

See :class:fiducio.Calibrator.

'logits'
ignore_index str

See :class:fiducio.Calibrator.

'logits'
device str

See :class:fiducio.Calibrator.

'logits'
References

Kull et al. (2019), Beyond temperature scaling: Obtaining well-calibrated multiclass probabilities with Dirichlet calibration, NeurIPS.

Source code in src/fiducio/calibrators/_affine.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(optimizer, lr, max_iter)
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self._weight: torch.Tensor | None = None  # (C, C) or (C,) for diagonal
    self._bias: torch.Tensor | None = None  # (C,)
    self._row_sum: torch.Tensor | None = None

Class-conditional matrix scaling

ClassConditionalMatrixScaling

ClassConditionalMatrixScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, independent_experts: bool = False, init_alpha: float = 1.0, init_floor: float = 1e-06, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _ClassConditionalBase

Class-conditional matrix scaling (CMS).

One unconstrained affine map A_c log p + b_c per uncalibrated top class c. Unlike the preserving variants it may change the argmax. Off-diagonal and bias L2 regularization (lambda_reg / mu_reg) penalize class mixing and bias, leaving the diagonal unpenalized. Experts are optimized jointly by default; set independent_experts=True to fit each one in its own optimization loop.

Source code in src/fiducio/calibrators/class_conditional.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    independent_experts: bool = False,
    init_alpha: float = 1.0,
    init_floor: float = 1e-6,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(
        optimizer, lr, max_iter, adam_lr=1e-2
    )
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self.independent_experts = bool(independent_experts)
    self.init_alpha = positive_finite(init_alpha, "init_alpha")
    self.init_floor = positive_finite(init_floor, "init_floor")
    self._raw_b: torch.Tensor | None = None  # (C, K, K) or (C, C, C)
    self._raw_mu: torch.Tensor | None = None  # (C, K) or (C, C)

Argmax-preserving matrix scaling

ArgmaxPreservingMatrixScaling

ArgmaxPreservingMatrixScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, independent_experts: bool = False, init_alpha: float = 1.0, init_floor: float = 1e-06, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _ClassConditionalBase

Argmax-preserving class-conditional matrix scaling (CMSAP).

Parameterised through non-negative margins between the top class and its competitors, which guarantees the calibrated argmax equals the uncalibrated argmax in exact arithmetic; floating-point ties and rounding can change it. Experts are optimized jointly by default; set independent_experts=True to fit each one in its own optimization loop.

Source code in src/fiducio/calibrators/class_conditional.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    independent_experts: bool = False,
    init_alpha: float = 1.0,
    init_floor: float = 1e-6,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(
        optimizer, lr, max_iter, adam_lr=1e-2
    )
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self.independent_experts = bool(independent_experts)
    self.init_alpha = positive_finite(init_alpha, "init_alpha")
    self.init_floor = positive_finite(init_floor, "init_floor")
    self._raw_b: torch.Tensor | None = None  # (C, K, K) or (C, C, C)
    self._raw_mu: torch.Tensor | None = None  # (C, K) or (C, C)

Order-preserving matrix scaling

OrderPreservingMatrixScaling

OrderPreservingMatrixScaling(*, optimizer: str = 'adam', lr: float | None = None, max_iter: int | None = None, patience: int | None = None, min_delta: float = 0.0, lr_patience: int | None = None, lr_factor: float = 0.1, lambda_reg: float = 0.0, mu_reg: float = 0.0, independent_experts: bool = False, init_alpha: float = 1.0, init_floor: float = 1e-06, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: _ClassConditionalBase

Order-preserving class-conditional matrix scaling (CMSOP).

Parameterised through non-negative gaps between consecutively ranked classes, which guarantees the full class ranking is preserved for every voxel in exact arithmetic, subject to floating-point ties and rounding. Experts are optimized jointly by default; set independent_experts=True to fit each one in its own optimization loop.

Source code in src/fiducio/calibrators/class_conditional.py
def __init__(
    self,
    *,
    optimizer: str = "adam",
    lr: float | None = None,
    max_iter: int | None = None,
    patience: int | None = None,
    min_delta: float = 0.0,
    lr_patience: int | None = None,
    lr_factor: float = 0.1,
    lambda_reg: float = 0.0,
    mu_reg: float = 0.0,
    independent_experts: bool = False,
    init_alpha: float = 1.0,
    init_floor: float = 1e-6,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    self.optimizer, self.lr, self.max_iter = resolve_optimizer(
        optimizer, lr, max_iter, adam_lr=1e-2
    )
    self._init_stopping(self.optimizer, patience, min_delta, lr_patience, lr_factor)
    self.lambda_reg = positive_finite(lambda_reg, "lambda_reg", allow_zero=True)
    self.mu_reg = positive_finite(mu_reg, "mu_reg", allow_zero=True)
    self.independent_experts = bool(independent_experts)
    self.init_alpha = positive_finite(init_alpha, "init_alpha")
    self.init_floor = positive_finite(init_floor, "init_floor")
    self._raw_b: torch.Tensor | None = None  # (C, K, K) or (C, C, C)
    self._raw_mu: torch.Tensor | None = None  # (C, K) or (C, C)

Binary isotonic calibration

BinaryIsotonicCalibration

BinaryIsotonicCalibration(*, n_bins: int = 250, input_type: str = 'logits', ignore_index: int = -100, device: DeviceLike | None = None)

Bases: Calibrator

Monotone binary calibration by equal-mass bins and weighted PAVA.

Fits class 1 against hard labels or soft distributions, including the mean human response (MHR). Inspired by Kirscher et al., TwinTrack: Post-hoc Multi-Rater Calibration for Medical Image Segmentation, MIDL 2026 (short paper), https://arxiv.org/abs/2604.15950v2.

Equal scores are never split across bins. Approximate equal-mass binning, linear interpolation between fitted points and constant extrapolation are explicit Fiducio conventions, not a claim of exact TwinTrack reproduction. Weighted PAVA minimizes squared error on the bins. The map is nondecreasing across voxels and may have plateaus or change a segmentation at threshold 0.5.

Parameters:

Name Type Description Default
n_bins int

Maximum number of fitting bins (default 250). Ties can reduce this count.

250
input_type str

See :class:fiducio.Calibrator. Only two classes are supported.

'logits'
ignore_index str

See :class:fiducio.Calibrator. Only two classes are supported.

'logits'
device str

See :class:fiducio.Calibrator. Only two classes are supported.

'logits'
Notes

Fit sorts all valid voxels: O(N log N) time and O(N) memory, not streaming. No optimizer or validation-based early stopping is used. Learned coordinates are stored in float64; public probabilities are float32. Fitted map values of exactly 0/1 are returned without clipping. Input scores of 0/1 are still calibrated and need not map to 0/1. decision_function returns log-probabilities, including -inf at zeros.

Source code in src/fiducio/calibrators/isotonic.py
def __init__(
    self,
    *,
    n_bins: int = 250,
    input_type: str = "logits",
    ignore_index: int = -100,
    device: DeviceLike | None = None,
) -> None:
    super().__init__(input_type=input_type, ignore_index=ignore_index, device=device)
    try:
        bins = 0 if isinstance(n_bins, bool) else operator.index(n_bins)
    except TypeError:
        bins = 0
    if bins < 1:
        raise ValueError("n_bins must be an integer >= 1")
    self.n_bins = bins
    self.x: torch.Tensor | None = None
    self.y: torch.Tensor | None = None

transform

transform(predictions: Any, mask: Any | None = None) -> torch.Tensor

Return calibrated probabilities; fitted map outputs of exactly 0/1 are not clipped.

Input scores of 0 or 1 are calibrated like any other score and need not map to 0 or 1 (see the class notes).

Source code in src/fiducio/calibrators/isotonic.py
@torch.no_grad()
def transform(self, predictions: Any, mask: Any | None = None) -> torch.Tensor:
    """Return calibrated probabilities; fitted map outputs of exactly 0/1 are not clipped.

    Input scores of 0 or 1 are calibrated like any other score and need not
    map to 0 or 1 (see the class notes).
    """
    if not self._fitted:
        raise NotFittedError("call fit() before transform()")
    preds, _, msk = self._prepare(predictions, None, mask, with_targets=False)
    validate_predictions(preds, input_type=self.input_type, expected_num_classes=2, mask=msk)
    # Neutralize invalid padding before search/interpolation, not by multiplication.
    if msk is not None:
        preds = preds.masked_fill(~msk.unsqueeze(1), 0)
    out = self._apply(self._to_canonical(preds))
    return apply_mask_to_probabilities(out, msk)

decision_function

decision_function(predictions: Any) -> torch.Tensor

Return log-probabilities; zero probabilities map to mathematical -inf.

Source code in src/fiducio/calibrators/isotonic.py
def decision_function(self, predictions: Any) -> torch.Tensor:
    """Return log-probabilities; zero probabilities map to mathematical -inf."""
    return self.transform(predictions).log()