Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pipelining bug in fairlearn algorithms #323

Merged
merged 3 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")