Source code for torch_uncertainty.metrics.classification.fpr

import torch
from torch import Tensor
from torchmetrics import Metric
from torchmetrics.utilities import rank_zero_warn
from torchmetrics.utilities.data import dim_zero_cat


[docs] class FPRx(Metric): is_differentiable = False higher_is_better = False full_state_update = False confidences: list[Tensor] targets: list[Tensor] def __init__(self, recall_level: float, pos_label: int, **kwargs) -> None: r"""Compute the False Positive Rate at x% Recall. The False Positive Rate at x% Recall (FPR@x) is used in anomaly detection, out-of-distribution (OOD) detection, and binary classification. It measures the proportion of false positives (normal samples misclassified as anomalies) when the model reaches a specified recall level for the positive class. Formally, let :math:`\tau_x` be the threshold such that the true positive rate equals the target recall level :math:`x`: .. math:: \tau_x = \sup \left\{ \tau \;\middle|\; \frac{|\{i : s_i \geq \tau,\, y_i = 1\}|}{|\{i : y_i = 1\}|} \geq x \right\}, where :math:`s_i` is the confidence score assigned to sample :math:`i` and :math:`y_i \in \{0, 1\}` is its label. The metric is then .. math:: \text{FPR}@x = \frac{|\{i : s_i \geq \tau_x,\, y_i = 0\}|}{|\{i : y_i = 0\}|}. Args: recall_level: The recall level at which to compute the FPR. pos_label: The positive label. kwargs: Additional keyword arguments for the metric class. Reference: Improved from `anomaly-seg <https://github.com/hendrycks/anomaly-seg>`_ and translated to torch. Example: .. code-block:: python from torch_uncertainty.metrics.classification import FPRx # Initialize the metric with 95% recall and positive label as 1 (e.g., OOD) metric = FPRx(recall_level=0.95, pos_label=1) # Simulated model predictions (confidence scores) and ground-truth labels confidences = torch.tensor([0.9, 0.8, 0.7, 0.6, 0.4, 0.2, 0.1]) targets = torch.tensor([1, 0, 1, 0, 0, 1, 0]) # 1: OOD, 0: In-Distribution # Update the metric with predictions and labels metric.update(confidences, targets) # Compute FPR at 95% recall result = metric.compute() print(f"FPR at 95% Recall: {result.item()}") # output: FPR at 95% Recall: 0.75 """ super().__init__(**kwargs) if recall_level < 0 or recall_level > 1: raise ValueError(f"Recall level must be between 0 and 1. Got {recall_level}.") self.recall_level = recall_level self.pos_label = pos_label self.add_state("confidences", [], dist_reduce_fx="cat") self.add_state("targets", [], dist_reduce_fx="cat") rank_zero_warn( f"Metric `FPR{int(recall_level * 100)}` will save all targets and predictions" " in buffer. For large datasets this may lead to large memory" " footprint." )
[docs] def update(self, confidences: Tensor, target: Tensor) -> None: # pyrefly: ignore[bad-override] """Update the metric state. Args: confidences: The confidence scores. target: The target labels, 0 if ID, 1 if OOD. """ self.confidences.append(confidences) self.targets.append(target)
[docs] def compute(self) -> Tensor: """Compute the False Positive Rate at x% Recall. Returns: Tensor: The value of the FPRx. """ confidences = dim_zero_cat(self.confidences) targets = dim_zero_cat(self.targets) return _fprx_compute( confidences, targets, recall_level=self.recall_level, pos_label=self.pos_label, )
def _fprx_compute( confidences: Tensor, target: Tensor, recall_level: float, pos_label: int, ) -> Tensor: """Compute a tie-aware FPR at a minimum requested recall.""" confidences = confidences.flatten() labels = target.flatten() == pos_label if confidences.shape != labels.shape: raise ValueError("Expected `confidences` and `target` to have the same shape.") num_positive = labels.sum() num_negative = (~labels).sum() if num_positive == 0 or num_negative == 0: dtype = confidences.dtype if confidences.is_floating_point() else torch.float32 return torch.tensor(torch.nan, device=confidences.device, dtype=dtype) if recall_level == 0: return confidences.new_tensor(0.0, dtype=torch.float32) order = confidences.argsort(descending=True) sorted_scores = confidences[order] sorted_labels = labels[order] # Evaluate thresholds only after complete groups of tied scores. Splitting a # tie could claim a recall/FPR pair that no score threshold can achieve. threshold_idxs = torch.cat( ( torch.where(sorted_scores[1:] != sorted_scores[:-1])[0], torch.tensor([labels.numel() - 1], device=labels.device), ) ) true_positive = sorted_labels.cumsum(dim=0)[threshold_idxs] false_positive = threshold_idxs + 1 - true_positive recall = true_positive / num_positive cutoff = torch.where(recall >= recall_level)[0][0] return false_positive[cutoff] / num_negative
[docs] class FPR95(FPRx): def __init__(self, pos_label: int, **kwargs) -> None: r"""Compute the False Positive Rate at 95% Recall. This is a specific case of the more general FPRx metric, where the recall level is fixed at 95%. Args: pos_label: The positive label (e.g., 1 for OOD samples). kwargs: Additional keyword arguments for :class:`FPRx`. .. seealso:: - :class:`FPRx`: Base metric that allows customization of the recall level. """ super().__init__(recall_level=0.95, pos_label=pos_label, **kwargs)