Source code for holisticai.bias.mitigation.postprocessing.ml_debiaser.transformer

import numpy as np
from holisticai.bias.mitigation.postprocessing.ml_debiaser.randomized_threshold.algorithm import (
    RandomizedThresholdAlgorithm,
)
from holisticai.bias.mitigation.postprocessing.ml_debiaser.reduce2binary.algorithm import Reduce2BinaryAlgorithm
from holisticai.utils.transformers.bias import BMPostprocessing as BMPost
from holisticai.utils.transformers.bias import SensitiveGroups


[docs] class MLDebiaser(BMPost): """ MLDebiaser postprocessing [1]_ debias predictions w.r.t. the sensitive class in\ each demographic group. This procedure takes as input a vector y and solves\ the optimization problem subject to the statistical parity constraint. This\ bias mitigation can be used for classification (binary and multiclass). Parameters ---------- gamma : float The regularization parameter. eps : float The tolerance for the convergence of the optimization algorithm. eta : float The step size for the optimization algorithm. sgd_steps : int The number of steps for the stochastic gradient descent optimization. full_gradient_epochs : int The number of epochs for the full gradient optimization. batch_size : int The batch size for the optimization algorithm. max_iter : int The maximum number of iterations for the optimization algorithm. verbose : bool The verbosity of the optimization algorithm. Examples -------- >>> from holisticai.bias.mitigation import MLDebiaser >>> mitigator = MLDebiaser() >>> mitigator.fit(y_proba, group_a, group_b) >>> test_data_transformed = mitigator.predict(y_proba, group_a, group_b) References --------- .. [1] Alabdulmohsin, Ibrahim M., and Mario Lucic. "A near-optimal algorithm for debiasing\ trained machine learning models." Advances in Neural Information Processing Systems\ 34 (2021): 8072-8084. """ def __init__( self, gamma=1.0, eps=0, eta=0.5, sgd_steps=10_000, full_gradient_epochs=1_000, batch_size=256, max_iter=5, verbose=True, ): self.gamma = gamma self.eps = eps self.eta = eta self.sgd_steps = sgd_steps self.full_gradient_epochs = full_gradient_epochs self.batch_size = batch_size self.max_iter = max_iter self.verbose = verbose self._sensgroups = SensitiveGroups()
[docs] def fit(self, y_proba: np.ndarray, group_a: np.ndarray, group_b: np.ndarray): """ Compute parameters for calibrated equalized odds. Description ---------- Compute parameters for calibrated equalized odds algorithm. Parameters ---------- y_pred : array-like Predicted vector (num_examples,). group_a : array-like Group membership vector (binary) group_b : array-like Group membership vector (binary) Returns ------- Self """ params = self._load_data(y_proba=y_proba, group_a=group_a, group_b=group_b) y_proba = params["y_proba"] group_a = params["group_a"] == 1 group_b = params["group_b"] == 1 num_classes = y_proba.shape[1] sensitive_features = np.stack([group_a, group_b], axis=1) self._sensgroups.fit(sensitive_features) if num_classes > 2: self.algorithm = Reduce2BinaryAlgorithm( gamma=self.gamma, eps=self.eps, eta=self.eta, num_classes=num_classes, sgd_steps=self.sgd_steps, full_gradient_epochs=self.full_gradient_epochs, batch_size=self.batch_size, max_iter=self.max_iter, verbose=self.verbose, ) else: self.algorithm = RandomizedThresholdAlgorithm( gamma=self.gamma, eps=self.eps, sgd_steps=self.sgd_steps, full_gradient_epochs=self.full_gradient_epochs, batch_size=self.batch_size, verbose=self.verbose, ) return self
[docs] def transform( self, y_proba: np.ndarray, group_a: np.ndarray, group_b: np.ndarray, ): """ Apply transform function to predictions and likelihoods Description ---------- Use a fitted probability to change the output label and invert the likelihood Parameters ---------- y_proba : array-like Predicted probability matrix (nb_examples, nb_classes) group_a : array-like Group membership vector (binary) group_b : array-like Group membership vector (binary) threshold : float float value to discriminate between 0 and 1 Returns ------- dict A dictionnary with new predictions """ params = self._load_data(y_proba=y_proba, group_a=group_a, group_b=group_b) group_a = params["group_a"] == 1 group_b = params["group_b"] == 1 y_proba = params["y_proba"] sensitive_features = np.stack([group_a, group_b], axis=1) p_attr = self._sensgroups.transform(sensitive_features, convert_numeric=True) # Multiclass classification if type(self.algorithm) is Reduce2BinaryAlgorithm: new_y_prob = self.algorithm.predict(y_proba, p_attr) new_y_pred = new_y_prob.argmax(axis=-1) return {"y_pred": new_y_pred, "y_proba": new_y_prob} # Binary classification pred = y_proba[:, 1] pred = 2 * pred - 1 # follow author implementation (use prediction and not logit) self.algorithm.fit(pred, p_attr) new_y_score = self.algorithm.predict(pred, p_attr) new_y_pred = np.where(new_y_score > 0.5, 1, 0) return {"y_pred": new_y_pred, "y_score": new_y_score}
[docs] def fit_transform(self, y_proba: np.ndarray, group_a: np.ndarray, group_b: np.ndarray): """ Fit and transform Description ---------- Fit and transform Parameters ---------- y_proba : array-like Predicted vector (num_examples,). group_a : array-like Group membership vector (binary) group_b : array-like Group membership vector (binary) Returns ------- dictionnary with new predictions """ return self.fit( y_proba, group_a, group_b, ).transform(y_proba, group_a, group_b)