Source code for mapie.conformity_scores.sets.aps

from typing import Optional, Tuple, Union, cast

import numpy as np
from numpy.typing import ArrayLike, NDArray
from sklearn.model_selection import BaseCrossValidator
from sklearn.preprocessing import label_binarize
from sklearn.utils import check_random_state

from mapie._machine_precision import EPSILON
from mapie.conformity_scores.sets.naive import NaiveConformityScore
from mapie.conformity_scores.sets.utils import check_include_last_label
from mapie.utils import _compute_quantiles


[docs] class APSConformityScore(NaiveConformityScore): """ Adaptive Prediction Sets (APS) method-based non-conformity score. It is based on the sum of the softmax outputs of the labels until the true label is reached, on the conformalization set. See [1] for more details. References ---------- [1] Yaniv Romano, Matteo Sesia and Emmanuel J. Candès. "Classification with Valid and Adaptive Coverage." NeurIPS 202 (spotlight) 2020. Attributes ---------- classes: Optional[ArrayLike] Names of the classes. random_state: Optional[Union[int, np.random.RandomState]] Pseudo random number generator state. quantiles_: ArrayLike of shape (n_alpha) The quantiles estimated from `get_sets` method. """
[docs] def __init__(self) -> None: super().__init__()
[docs] def get_predictions( self, X: NDArray, alpha_np: NDArray, y_pred_proba: NDArray, cv: Optional[Union[int, str, BaseCrossValidator]], agg_scores: Optional[str] = "mean", **kwargs, ) -> NDArray: """ Just processes the passed y_pred_proba. Parameters ----------- X: NDArray of shape (n_samples, n_features) Observed feature values (not used since predictions are passed). alpha_np: NDArray of shape (n_alpha,) NDArray of floats between `0` and `1`, represents the uncertainty of the confidence interval. y_pred_proba: NDArray Predicted probabilities from the estimator. cv: Optional[Union[int, str, BaseCrossValidator]] Cross-validation strategy used by the estimator (not used here). agg_scores: Optional[str] Method to aggregate the scores from the base estimators. If "mean", the scores are averaged. If "crossval", the scores are obtained from cross-validation. By default `"mean"`. Returns -------- NDArray Array of predictions. """ if agg_scores != "crossval": y_pred_proba = np.repeat( y_pred_proba[:, :, np.newaxis], len(alpha_np), axis=2 ) return y_pred_proba
[docs] @staticmethod def get_true_label_cumsum_proba( y: ArrayLike, y_pred_proba: NDArray, classes: ArrayLike ) -> Tuple[NDArray, NDArray]: """ Compute the cumsumed probability of the true label. Parameters ---------- y: ArrayLike of shape (n_samples, ) Array with the labels. y_pred_proba: NDArray of shape (n_samples, n_classes) Predictions of the model. classes: ArrayLike of shape (n_classes, ) Array with the classes. Returns ------- Tuple[NDArray, NDArray] of shapes (n_samples, 1) and (n_samples, ). The first element is the cumsum probability of the true label. The second is the 1-based rank of the true label in the sorted probabilities. """ y_true = label_binarize(y=y, classes=classes) index_sorted = np.fliplr(np.argsort(y_pred_proba, axis=1)) y_pred_sorted = np.take_along_axis(y_pred_proba, index_sorted, axis=1) y_true_sorted = np.take_along_axis(y_true, index_sorted, axis=1) y_pred_sorted_cumsum = np.cumsum(y_pred_sorted, axis=1) cutoff = np.argmax(y_true_sorted, axis=1) true_label_cumsum_proba = np.take_along_axis( y_pred_sorted_cumsum, cutoff.reshape(-1, 1), axis=1 ) cutoff += 1 return true_label_cumsum_proba, cutoff
[docs] def get_conformity_scores( self, y: NDArray, y_pred: NDArray, y_enc: Optional[NDArray] = None, **kwargs ) -> NDArray: """ Get the conformity score. Parameters ---------- y: NDArray of shape (n_samples,) Observed target values. y_pred: NDArray of shape (n_samples,) Predicted target values. y_enc: Optional[NDArray] of shape (n_samples,) Target values as normalized encodings. Returns ------- NDArray of shape (n_samples,) Conformity scores. """ # Casting y_enc = cast(NDArray, y_enc) classes = cast(NDArray, self.classes) # Conformity scores conformity_scores, self.cutoff = self.get_true_label_cumsum_proba( y, y_pred, classes ) y_proba_true = np.take_along_axis(y_pred, y_enc.reshape(-1, 1), axis=1) random_state = check_random_state(self.random_state) u = random_state.uniform(size=len(y_pred)).reshape(-1, 1) conformity_scores -= u * y_proba_true return conformity_scores
[docs] def get_conformity_score_quantiles( self, conformity_scores: NDArray, alpha_np: NDArray, cv: Optional[Union[int, str, BaseCrossValidator]], agg_scores: Optional[str] = "mean", **kwargs, ) -> NDArray: """ Get the quantiles of the conformity scores for each uncertainty level. Parameters ----------- conformity_scores: NDArray of shape (n_samples,) Conformity scores for each sample. alpha_np: NDArray of shape (n_alpha,) NDArray of floats between 0 and 1, representing the uncertainty of the confidence interval. cv: Optional[Union[int, str, BaseCrossValidator]] Cross-validation strategy used by the estimator. agg_scores: Optional[str] Method to aggregate the scores from the base estimators. If "mean", the scores are averaged. If "crossval", the scores are obtained from cross-validation. By default `"mean"`. Returns -------- NDArray Array of quantiles with respect to alpha_np. """ n = len(conformity_scores) if cv == "prefit" or agg_scores in ["mean"]: quantiles_ = _compute_quantiles(conformity_scores, alpha_np) else: quantiles_ = (n + 1) * (1 - alpha_np) return quantiles_
def _compute_v_parameter( self, y_proba_last_cumsumed: NDArray, threshold: NDArray, y_pred_proba_last: NDArray, prediction_sets: NDArray, **kwargs, ) -> NDArray: """ Compute the V parameters from Romano+(2020). Parameters ----------- y_proba_last_cumsumed: NDArray of shape (n_samples, n_alpha) Cumulated score of the last included label. threshold: NDArray of shape (n_alpha,) or shape (n_samples_train,) Threshold to compare with y_proba_last_cumsum. y_pred_proba_last: NDArray of shape (n_samples, 1, n_alpha) Last included probability. prediction_sets: NDArray of shape (n_samples, n_alpha) Prediction sets (not used here). Returns -------- NDArray of shape (n_samples, n_alpha) Vs parameters. """ # compute V parameter from Romano+(2020) v_param = ( y_proba_last_cumsumed - threshold.reshape(1, -1) ) / y_pred_proba_last[:, 0, :] return cast(NDArray, v_param) def _add_random_tie_breaking( self, prediction_sets: NDArray, y_pred_index_last: NDArray, y_pred_proba_cumsum: NDArray, y_pred_proba_last: NDArray, threshold: NDArray, **kwargs, ) -> NDArray: """ Randomly remove last label from prediction set based on the comparison between a random number and the difference between cumulated score of the last included label and the quantile. Parameters ---------- prediction_sets: NDArray of shape (n_samples, n_classes, n_threshold) Prediction set for each observation and each alpha. y_pred_index_last: NDArray of shape (n_samples, threshold) Index of the last included label. y_pred_proba_cumsum: NDArray of shape (n_samples, n_classes) Cumsumed probability of the model in the original order. y_pred_proba_last: NDArray of shape (n_samples, 1, threshold) Last included probability. threshold: NDArray of shape (n_alpha,) or shape (n_samples_train,) Threshold to compare with y_proba_last_cumsum, can be either: - the quantiles associated with alpha values when `cv` == "prefit", `cv` == "split" or `agg_scores` is "mean" - the conformity score from training samples otherwise (i.e., when `cv` is CV splitter and `agg_scores` is "crossval") Returns ------- NDArray of shape (n_samples, n_classes, n_alpha) Updated version of prediction_sets with randomly removed labels. """ # get cumsumed probabilities up to last retained label y_proba_last_cumsumed = np.squeeze( np.take_along_axis(y_pred_proba_cumsum, y_pred_index_last, axis=1), axis=1 ) # get the V parameter from Romano+(2020) or Angelopoulos+(2020) v_param = self._compute_v_parameter( y_proba_last_cumsumed, threshold, y_pred_proba_last, prediction_sets ) # get random numbers for each observation and alpha value random_state = check_random_state(self.random_state) random_state = cast(np.random.RandomState, random_state) u_param = random_state.uniform(size=(prediction_sets.shape[0], 1)) # remove last label from comparison between uniform number and V label_to_keep = np.less_equal(v_param - u_param, EPSILON) np.put_along_axis( prediction_sets, y_pred_index_last, label_to_keep[:, np.newaxis, :], axis=1 ) return prediction_sets
[docs] def get_prediction_sets( self, y_pred_proba: NDArray, conformity_scores: NDArray, alpha_np: NDArray, cv: Optional[Union[int, str, BaseCrossValidator]], agg_scores: Optional[str] = "mean", include_last_label: Optional[Union[bool, str]] = True, **kwargs, ) -> NDArray: """ Generate prediction sets based on the probability predictions, the conformity scores and the uncertainty level. Parameters ----------- y_pred_proba: NDArray of shape (n_samples, n_classes) Target prediction. conformity_scores: NDArray of shape (n_samples,) Conformity scores for each sample. alpha_np: NDArray of shape (n_alpha,) NDArray of floats between 0 and 1, representing the uncertainty of the confidence interval (not used here). cv: Optional[Union[int, str, BaseCrossValidator]] Cross-validation strategy used by the estimator. agg_scores: Optional[str] Method to aggregate the scores from the base estimators. If "mean", the scores are averaged. If "crossval", the scores are obtained from cross-validation. By default `"mean"`. include_last_label: Optional[Union[bool, str]] Whether or not to include last label in prediction sets for the "aps" method. Choose among: - False, does not include label whose cumulated score is just over the quantile. - True, includes label whose cumulated score is just over the quantile, unless there is only one label in the prediction set. - "randomized", randomly includes label whose cumulated score is just over the quantile based on the comparison of a uniform number and the difference between the cumulated score of the last label and the quantile. When set to `True` or `False`, it may result in a coverage higher than `1 - alpha` (because contrary to the "randomized" setting, none of these methods create empty prediction sets). See [1] and [2] for more details. By default `True`. Returns -------- NDArray Array of quantiles with respect to alpha_np. References ---------- [1] Yaniv Romano, Matteo Sesia and Emmanuel J. Candès. "Classification with Valid and Adaptive Coverage." NeurIPS 202 (spotlight) 2020. [2] Anastasios Nikolas Angelopoulos, Stephen Bates, Michael Jordan and Jitendra Malik. "Uncertainty Sets for Image Classifiers using Conformal Prediction." International Conference on Learning Representations 2021. """ include_last_label = check_include_last_label(include_last_label) # specify which thresholds will be used if cv == "prefit" or agg_scores in ["mean"]: thresholds = self.quantiles_ else: thresholds = conformity_scores.ravel() # sort labels by decreasing probability y_pred_proba_cumsum, y_pred_index_last, y_pred_proba_last = ( self._get_last_included_proba( y_pred_proba, thresholds, include_last_label, prediction_phase=True, **kwargs, ) ) # get the prediction set by taking all probabilities above the last one if cv == "prefit" or agg_scores in ["mean"]: y_pred_included = np.greater_equal( y_pred_proba - y_pred_proba_last, -EPSILON ) else: y_pred_included = np.less_equal(y_pred_proba - y_pred_proba_last, EPSILON) # remove last label randomly if include_last_label == "randomized": y_pred_included = self._add_random_tie_breaking( y_pred_included, y_pred_index_last, y_pred_proba_cumsum, y_pred_proba_last, thresholds, **kwargs, ) if cv == "prefit" or agg_scores in ["mean"]: prediction_sets = y_pred_included else: # compute the number of times the inequality is verified prediction_sets_summed = y_pred_included.sum(axis=2) prediction_sets = np.less_equal( prediction_sets_summed[:, :, np.newaxis] - self.quantiles_[np.newaxis, np.newaxis, :], EPSILON, ) return cast(NDArray, prediction_sets)