Source code for torch_uncertainty.losses.quantile

import math

import torch
from torch import Tensor, nn


[docs] class PinballLoss(nn.Module): def __init__(self, quantile: float, reduction: str | None = "mean") -> None: r"""The Pinball loss for quantile regression. Also known as the quantile loss or check loss, the pinball loss at quantile level :math:`\tau \in (0, 1)` is: .. math:: \mathcal{L}_\tau(y, \hat{y}) = \max\!\left(\tau\,(y - \hat{y}),\,(\tau - 1)\,(y - \hat{y})\right) = \begin{cases} \tau\,(y - \hat{y}) & \text{if } y \geq \hat{y}, \\ (1 - \tau)\,(\hat{y} - y) & \text{if } y < \hat{y}. \end{cases} For :math:`\tau = 0.5` the loss coincides with the mean absolute error (MAE) scaled by :math:`\tfrac{1}{2}`. Args: quantile: The quantile level :math:`\tau \in (0, 1)`. reduction: Specifies the reduction to apply to the output. Must be one of ``'none'``, ``'mean'`` or ``'sum'``. Defaults to ``"mean"``. References: [1] `Koenker, R., & Bassett Jr, G. (1978). Regression quantiles. Econometrica, <https://www.jstor.org/stable/1913643>`_. """ super().__init__() if not 0 < quantile < 1: raise ValueError(f"The quantile parameter should be in (0, 1), but got {quantile}.") self.quantile = quantile if reduction not in ("none", "mean", "sum"): raise ValueError(f"{reduction} is not a valid value for reduction.") self.reduction = reduction
[docs] def forward(self, predictions: Tensor, targets: Tensor) -> Tensor: """Compute the pinball loss. Args: predictions: The predicted quantile values. targets: The target values. """ residual = targets - predictions loss = torch.maximum(self.quantile * residual, (self.quantile - 1) * residual) if self.reduction == "mean": return loss.mean() if self.reduction == "sum": return loss.sum() return loss
class RQRLoss(nn.Module): def __init__( self, coverage_level: float, width_weight: float | None = None, reduction: str | None = "mean", ) -> None: r"""The Relaxed Quantile Regression (RQR) loss. RQR directly learns the two endpoints :math:`(\mu_1, \mu_2)` of a prediction interval with target coverage :math:`\alpha`, without assigning either endpoint to a predefined quantile. Let .. math:: \kappa = (y - \mu_1)(y - \mu_2). Since :math:`\kappa < 0` exactly when :math:`y` is inside the interval, the unregularized loss is .. math:: \mathcal{L}^{\mathrm{RQR}}_\alpha = \begin{cases} \alpha \kappa & \text{if } \kappa \geq 0, \\ (\alpha - 1)\kappa & \text{if } \kappa < 0. \end{cases} When :attr:`width_weight` is positive, this implements the width-minimizing RQR-W variant. Its squared-width penalty biases coverage by :math:`-2\lambda`; therefore, the loss uses the corrected level :math:`\hat{\alpha} = \alpha + 2\lambda`: .. math:: \mathcal{L}^{\mathrm{RQR-W}}_\alpha = \mathcal{L}^{\mathrm{RQR}}_{\alpha + 2\lambda} + \frac{\lambda}{2}(\mu_2-\mu_1)^2. Args: coverage_level: The target coverage level :math:`\alpha \in (0, 1)`. width_weight: The width regularization weight :math:`\lambda`, which must satisfy :math:`0 \leq \lambda \leq (1-\alpha)/2`. A value of ``0`` recovers the unregularized RQR loss. Defaults to ``0.0``. reduction: Specifies the reduction to apply to the output. Must be one of ``'none'``, ``'mean'`` or ``'sum'``. Defaults to ``"mean"``. References: [1] `Pouplin, T., Jeffares, A., Seedat, N., & van der Schaar, M. (2024). Relaxed quantile regression: Prediction intervals for asymmetric noise. ICML 2024 <https://arxiv.org/abs/2406.03258>`_. """ super().__init__() if not 0 < coverage_level < 1: raise ValueError(f"The coverage level should be in (0, 1), but got {coverage_level}.") self.coverage_level = coverage_level if width_weight is None: width_weight = 0.0 corrected_level = coverage_level + 2 * width_weight if width_weight < 0 or not math.isfinite(width_weight) or corrected_level > 1: raise ValueError( "The width weight should be in " f"[0, (1 - coverage_level) / 2], but got {width_weight}." ) self.width_weight = width_weight if reduction not in ("none", "mean", "sum"): raise ValueError(f"{reduction} is not a valid value for reduction.") self.reduction = reduction def forward(self, predictions: Tensor, targets: Tensor) -> Tensor: """Compute the RQR loss. Args: predictions: The two interval endpoints, with shape ``(..., 2)``. targets: The target values, with shape ``(...)``. """ if predictions.ndim == 0 or predictions.shape[-1] != 2: raise ValueError( "Expected `predictions` to have exactly two interval endpoints " f"in its last dimension, but got shape {predictions.shape}." ) if targets.shape != predictions.shape[:-1]: raise ValueError( "Expected `targets` to have the same shape as `predictions` " f"without its last dimension, but got {targets.shape=} and " f"{predictions.shape=}." ) endpoint_1, endpoint_2 = predictions.unbind(dim=-1) interval_product = (targets - endpoint_1) * (targets - endpoint_2) corrected_level = self.coverage_level + 2 * self.width_weight loss = torch.maximum( corrected_level * interval_product, (corrected_level - 1) * interval_product, ) loss += self.width_weight * (endpoint_2 - endpoint_1).square() / 2 if self.reduction == "mean": return loss.mean() if self.reduction == "sum": return loss.sum() return loss