Files
2026-07-13 12:49:22 +08:00

119 lines
5.1 KiB
Python

"""Helper methods used internally for computing label quality scores."""
import warnings
import numpy as np
from typing import Optional
from scipy.special import xlogy
from cleanlab.count import get_confident_thresholds
def _subtract_confident_thresholds(
labels: Optional[np.ndarray],
pred_probs: np.ndarray,
multi_label: bool = False,
confident_thresholds: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Return adjusted predicted probabilities by subtracting the class confident thresholds and renormalizing.
The confident class threshold for a class j is the expected (average) "self-confidence" for class j.
The purpose of this adjustment is to handle class imbalance.
Parameters
----------
labels : np.ndarray
Labels in the same format expected by the `cleanlab.count.get_confident_thresholds()` method.
If labels is None, confident_thresholds needs to be passed in as it will not be calculated.
pred_probs : np.ndarray (shape (N, K))
Predicted-probabilities in the same format expected by the `cleanlab.count.get_confident_thresholds()` method.
confident_thresholds : np.ndarray (shape (K,))
Pre-calculated confident thresholds. If passed in, function will subtract these thresholds instead of calculating
confident_thresholds from the given labels and pred_probs.
multi_label : bool, optional
If ``True``, labels should be an iterable (e.g. list) of iterables, containing a
list of labels for each example, instead of just a single label.
The multi-label setting supports classification tasks where an example has 1 or more labels.
Example of a multi-labeled `labels` input: ``[[0,1], [1], [0,2], [0,1,2], [0], [1], ...]``.
The major difference in how this is calibrated versus single-label is that
the total number of errors considered is based on the number of labels,
not the number of examples. So, the calibrated `confident_joint` will sum
to the number of total labels.
Returns
-------
pred_probs_adj : np.ndarray (float)
Adjusted pred_probs.
"""
# Get expected (average) self-confidence for each class
# TODO: Test this for multi-label
if confident_thresholds is None:
if labels is None:
raise ValueError(
"Cannot calculate confident_thresholds without labels. Pass in either labels or already calculated "
"confident_thresholds parameter. "
)
confident_thresholds = get_confident_thresholds(labels, pred_probs, multi_label=multi_label)
# Subtract the class confident thresholds
pred_probs_adj = pred_probs - confident_thresholds
# Re-normalize by shifting data to take care of negative values from the subtraction
pred_probs_adj += confident_thresholds.max()
pred_probs_adj /= pred_probs_adj.sum(axis=1, keepdims=True)
return pred_probs_adj
def get_normalized_entropy(
pred_probs: np.ndarray, min_allowed_prob: Optional[float] = None
) -> np.ndarray:
"""Return the normalized entropy of pred_probs.
Normalized entropy is between 0 and 1. Higher values of entropy indicate higher uncertainty in the model's prediction of the correct label.
Read more about normalized entropy `on Wikipedia <https://en.wikipedia.org/wiki/Entropy_(information_theory)>`_.
Normalized entropy is used in active learning for uncertainty sampling: https://medium.com/data-science/uncertainty-sampling-cheatsheet-ec57bc067c0b
Unlike label-quality scores, entropy only depends on the model's predictions, not the given label.
Parameters
----------
pred_probs : np.ndarray (shape (N, K))
Each row of this matrix corresponds to an example x and contains the model-predicted
probabilities that x belongs to each possible class: P(label=k|x)
min_allowed_prob : float, default: None, deprecated
Minimum allowed probability value. If not `None` (default),
entries of `pred_probs` below this value will be clipped to this value.
.. deprecated:: 2.5.0
This keyword is deprecated and should be left to the default.
The entropy is well-behaved even if `pred_probs` contains zeros,
clipping is unnecessary and (slightly) changes the results.
Returns
-------
entropy : np.ndarray (shape (N, ))
Each element is the normalized entropy of the corresponding row of ``pred_probs``.
Raises
------
ValueError
An error is raised if any of the probabilities is not in the interval [0, 1].
"""
if np.any(pred_probs < 0) or np.any(pred_probs > 1):
raise ValueError("All probabilities are required to be in the interval [0, 1].")
num_classes = pred_probs.shape[1]
if min_allowed_prob is not None:
warnings.warn(
"Using `min_allowed_prob` is not necessary anymore and will be removed.",
DeprecationWarning,
)
pred_probs = np.clip(pred_probs, a_min=min_allowed_prob, a_max=None)
# Note that dividing by log(num_classes) changes the base of the log which rescales entropy to 0-1 range
return -np.sum(xlogy(pred_probs, pred_probs), axis=1) / np.log(num_classes)