Skip to content

Commit

Permalink
Fix pipelining bugs in fairlearn algorithms (Trusted-AI#323)
Browse files Browse the repository at this point in the history
* move logic from __init__ to allow clone
* add classes_ to GridSearchReduction
  • Loading branch information
hoffmansc authored Jul 14, 2022
1 parent 007b403 commit 352262a
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 71 deletions.
52 changes: 25 additions & 27 deletions aif360/sklearn/inprocessing/exponentiated_gradient_reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
licensed under the MIT Licencse, Copyright Microsoft Corporation
"""
import fairlearn.reductions as red
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, clone
from sklearn.preprocessing import LabelEncoder

from aif360.sklearn.utils import check_inputs


class ExponentiatedGradientReduction(BaseEstimator, ClassifierMixin):
"""Exponentiated gradient reduction for fair classification.
Expand Down Expand Up @@ -65,33 +62,14 @@ def __init__(self,
attributes from training data.
"""
self.prot_attr = prot_attr
self.moments = {
"DemographicParity": red.DemographicParity,
"EqualizedOdds": red.EqualizedOdds,
"TruePositiveRateDifference": red.TruePositiveRateDifference,
"ErrorRateRatio": red.ErrorRateRatio
}

if isinstance(constraints, str):
if constraints not in self.moments:
raise ValueError(f"Constraint not recognized: {constraints}")

self.moment = self.moments[constraints]()
elif isinstance(constraints, red.Moment):
self.moment = constraints
else:
raise ValueError("constraints must be a string or Moment object.")

self.estimator = estimator
self.constraints = constraints
self.eps = eps
self.T = T
self.nu = nu
self.eta_mul = eta_mul
self.drop_prot_attr = drop_prot_attr

self.model = red.ExponentiatedGradient(self.estimator, self.moment,
self.eps, self.T, self.nu, self.eta_mul)

def fit(self, X, y):
"""Learns randomized model with less bias
Expand All @@ -102,6 +80,26 @@ def fit(self, X, y):
Returns:
self
"""
self.estimator_ = clone(self.estimator)

moments = {
"DemographicParity": red.DemographicParity,
"EqualizedOdds": red.EqualizedOdds,
"TruePositiveRateDifference": red.TruePositiveRateDifference,
"ErrorRateRatio": red.ErrorRateRatio
}
if isinstance(self.constraints, str):
if self.constraints not in moments:
raise ValueError(f"Constraint not recognized: {self.constraints}")
self.moment_ = moments[self.constraints]()
elif isinstance(self.constraints, red.Moment):
self.moment_ = self.constraints
else:
raise ValueError("constraints must be a string or Moment object.")

self.model_ = red.ExponentiatedGradient(self.estimator_, self.moment_,
eps=self.eps, T=self.T, nu=self.nu, eta_mul=self.eta_mul)

A = X[self.prot_attr]

if self.drop_prot_attr:
Expand All @@ -111,7 +109,7 @@ def fit(self, X, y):
y = le.fit_transform(y)
self.classes_ = le.classes_

self.model.fit(X, y, sensitive_features=A)
self.model_.fit(X, y, sensitive_features=A)

return self

Expand All @@ -126,7 +124,7 @@ def predict(self, X):
if self.drop_prot_attr:
X = X.drop(self.prot_attr, axis=1)

return self.classes_[self.model.predict(X)]
return self.classes_[self.model_.predict(X)]


def predict_proba(self, X):
Expand All @@ -146,4 +144,4 @@ def predict_proba(self, X):
if self.drop_prot_attr:
X = X.drop(self.prot_attr, axis=1)

return self.model._pmf_predict(X)
return self.model_._pmf_predict(X)
94 changes: 50 additions & 44 deletions aif360/sklearn/inprocessing/grid_search_reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
licensed under the MIT Licencse, Copyright Microsoft Corporation
"""
import fairlearn.reductions as red
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, clone
from sklearn.preprocessing import LabelEncoder


Expand Down Expand Up @@ -85,50 +84,16 @@ def __init__(self,
typically the maximum of the range of y values.
"""
self.prot_attr = prot_attr
self.moments = {
"DemographicParity": red.DemographicParity,
"EqualizedOdds": red.EqualizedOdds,
"TruePositiveRateDifference": red.TruePositiveRateDifference,
"ErrorRateRatio": red.ErrorRateRatio,
"GroupLoss": red.GroupLossMoment
}

if isinstance(constraints, str):
if constraints not in self.moments:
raise ValueError(f"Constraint not recognized: {constraints}")

if constraints == "GroupLoss":
losses = {
"ZeroOne": red.ZeroOneLoss,
"Square": red.SquareLoss,
"Absolute": red.AbsoluteLoss
}

if loss == "ZeroOne":
self.loss = losses[loss]()
else:
self.loss = losses[loss](min_val, max_val)

self.moment = self.moments[constraints](loss=self.loss)
else:
self.moment = self.moments[constraints]()
elif isinstance(constraints, red.Moment):
self.moment = constraints
else:
raise ValueError("constraints must be a string or Moment object.")

self.estimator = estimator
self.constraints = constraints
self.constraint_weight = constraint_weight
self.grid_size = grid_size
self.grid_limit = grid_limit
self.grid = grid
self.drop_prot_attr = drop_prot_attr

self.model = red.GridSearch(estimator=self.estimator,
constraints=self.moment,
constraint_weight=self.constraint_weight,
grid_size=self.grid_size, grid_limit=self.grid_limit,
grid=self.grid)
self.loss = loss
self.min_val = min_val
self.max_val = max_val

def fit(self, X, y):
"""Train a less biased classifier or regressor with the given training
Expand All @@ -141,12 +106,53 @@ def fit(self, X, y):
Returns:
self
"""
self.estimator_ = clone(self.estimator)

moments = {
"DemographicParity": red.DemographicParity,
"EqualizedOdds": red.EqualizedOdds,
"TruePositiveRateDifference": red.TruePositiveRateDifference,
"ErrorRateRatio": red.ErrorRateRatio,
"GroupLoss": red.GroupLossMoment
}
if isinstance(self.constraints, str):
if self.constraints not in moments:
raise ValueError(f"Constraint not recognized: {self.constraints}")
if self.constraints == "GroupLoss":
losses = {
"ZeroOne": red.ZeroOneLoss,
"Square": red.SquareLoss,
"Absolute": red.AbsoluteLoss
}
if self.loss == "ZeroOne":
self.loss_ = losses[self.loss]()
else:
self.loss_ = losses[self.loss](self.min_val, self.max_val)

self.moment_ = moments[self.constraints](loss=self.loss_)
else:
self.moment_ = moments[self.constraints]()
elif isinstance(self.constraints, red.Moment):
self.moment_ = self.constraints
else:
raise ValueError("constraints must be a string or Moment object.")

self.model_ = red.GridSearch(estimator=self.estimator_,
constraints=self.moment_,
constraint_weight=self.constraint_weight,
grid_size=self.grid_size, grid_limit=self.grid_limit,
grid=self.grid)

A = X[self.prot_attr]

if self.drop_prot_attr:
X = X.drop(self.prot_attr, axis=1)

self.model.fit(X, y, sensitive_features=A)
le = LabelEncoder()
y = le.fit_transform(y)
self.classes_ = le.classes_

self.model_.fit(X, y, sensitive_features=A)

return self

Expand All @@ -162,7 +168,7 @@ def predict(self, X):
if self.drop_prot_attr:
X = X.drop(self.prot_attr, axis=1)

return self.model.predict(X)
return self.model_.predict(X)


def predict_proba(self, X):
Expand All @@ -182,8 +188,8 @@ def predict_proba(self, X):
if self.drop_prot_attr:
X = X.drop(self.prot_attr)

if isinstance(self.model.constraints, red.ClassificationMoment):
return self.model.predict_proba(X)
if isinstance(self.model_.constraints, red.ClassificationMoment):
return self.model_.predict_proba(X)

raise NotImplementedError("Underlying model does not support "
"predict_proba")

0 comments on commit 352262a

Please sign in to comment.