Commit 93c6bb69 authored by Fabrizio Detassis's avatar Fabrizio Detassis
Browse files

WIP - new fairness constraints

parent 083f5808
......@@ -103,6 +103,36 @@ class BalanceConstraint(AbstractConstraint):
return "BalanceConstraint with value " + str(self.value)
class EqualOpportunity(AbstractConstraint):
def __init__(self, name, pfeat, value):
self.name = name
self.pfeat = pfeat
self.value = value
def is_satisfied(self, x, y):
value = utils.equal_opportunity(x, y, self.pfeat)
return value <= self.value
def __str__(self):
return "EqualOpportunity with value " + str(self.value)
class EqualizedOdds(AbstractConstraint):
def __init__(self, name, pfeat, value):
self.name = name
self.pfeat = pfeat
self.value = value
def is_satisfied(self, x, y):
value = utils.equalized_odds(x, y, self.pfeat)
return value <= self.value
def __str__(self):
return "EqualizedOdds with value " + str(self.value)
class FairnessRegConstraint(AbstractConstraint):
def __init__(self, name, pfeat, value):
......
......@@ -31,7 +31,7 @@ class MovingTarget(ABC):
self.add_constraints(M, C, d[0], d[1])
# self.set_loss_function(M, L, d)
# d_data = self.get_pysmt_data(y_k)
# d_data = self.get_pysmt_data(y_k)
y_k = self.ML.apply(ml_problem)
y_k = np.array(y_k.unpack())
......@@ -51,8 +51,7 @@ class MovingTarget(ABC):
ml_problem = self.assemble_ml_problem(ml_problem, z_k)
y_k = self.ML.apply(ml_problem)
y_k = np.array(y_k.unpack())
y_k = np.array(y_k.unpack())
return y_k
@abstractmethod
......@@ -113,7 +112,7 @@ class MovingTarget(ABC):
for c in constraints:
ctype = c[0].string_value()
if ctype in ('didi-bin', 'didi-real'):
if ctype in ('didi-bin', 'didi-real', 'equal-opportunity', 'equalized-odds'):
cvar = c[1].string_value()
cfeat = c[2]
ix_feat = list()
......
......@@ -6,7 +6,8 @@ from moving_target_abc import MovingTarget
from docplex.mp.model import Model as CPModel
from docplex.mp.model import DOcplexException
from constraint import InequalityRegGlobalConstraint, InequalityClsGlobalConstraint
from constraint import FairnessClsConstraint, FairnessRegConstraint, BalanceConstraint
from constraint import FairnessClsConstraint, FairnessRegConstraint
from constraint import EqualOpportunity, BalanceConstraint, EqualizedOdds
import utils
......@@ -79,6 +80,51 @@ class MovingTargetClsCplex(MovingTarget):
constraint += M.sum(abs_val)
M.add_constraint(constraint <= cval, ctname='fairness_cnst')
elif ctype == 'equal-opportunity':
if self.n_classes > 2:
raise ValueError("Constraint 'equal-opportunity' is meant for binary classification!")
cvar = c[1]
pfeat = c[2]
cval = c[3]
cstr = EqualOpportunity('ct', pfeat, cval)
tpr = M.continuous_var_list(keys=len(pfeat), name='tpr')
# Add fairness constraint.
for i, ix_feat in enumerate(pfeat):
Np = np.sum(x_s[:, ix_feat] * y_s)
print("Feat " + str(ix_feat) + " Np " + str(Np))
if Np > 0:
tpr[i] = (1.0 / Np) * M.sum([x_s[j][ix_feat] * y_s[j] * x[j][1] for j in range(self.n_points)])
M.add_constraint(M.max(tpr)-M.min(tpr) <= cval, 'equal_opportunity')
elif ctype == 'equalized-odds':
if self.n_classes > 2:
raise ValueError("Constraint 'equal-opportunity' is meant for binary classification!")
cvar = c[1]
pfeat = c[2]
cval = c[3]
cstr = EqualizedOdds('ct', pfeat, cval)
tpr = M.continuous_var_list(keys=len(pfeat), name='tpr')
fpr = M.continuous_var_list(keys=len(pfeat), name='fpr')
# Add constraint.
for i, ix_feat in enumerate(pfeat):
Np = np.sum(x_s[:, ix_feat] * y_s)
Nn = np.sum((1 - x_s[:, ix_feat]) * y_s)
print("Feat " + str(ix_feat) + " Np " + str(Np))
if Np > 0:
tpr[i] = (1.0 / Np) * M.sum([x_s[j][ix_feat] * y_s[j] * x[j][1] for j in range(self.n_points)])
if Nn > 0:
fpr[i] = (1.0 / Nn) * M.sum([(1 - x_s[j][ix_feat]) * y_s[j] * x[j][1] for j in range(self.n_points)])
M.add_constraint(M.max(tpr)-M.min(tpr) <= cval, 'equal_opportunity_pos')
M.add_constraint(M.max(fpr)-M.min(fpr) <= cval, 'equal_opportunity_neg')
else:
raise NotImplementedError("Constraint type not recognized " + str(ctype))
......
import numpy as np
def didi_r(x, y, pfeat):
"""
Compute the disparate impact discrimination index of a given dataset.
......@@ -43,3 +44,43 @@ def didi_c(x, y, pfeat):
return tot
def equal_opportunity(x, y, pfeat):
"""
Compute the true positive rate for each protected feature and returns the biggest difference among them.
"""
y = y.reshape(-1, 1)
n_points = len(y)
tpr = list()
for feat in pfeat:
Np = np.sum(x[:, feat] * y)
if Np > 0:
_tpr = (1.0 / Np) * np.sum([x[j][feat] * y[j] for j in range(n_points)])
tpr.append(_tpr)
return np.max(tpr) - np.min(tpr)
def equalized_odds(x, y, pfeat):
"""
Compute the true positive rate for each protected feature and returns the biggest difference among them.
"""
y = y.reshape(-1, 1)
n_points = len(y)
tpr = list()
fpr = list()
for feat in pfeat:
Np = np.sum(x[:, feat] * y)
Nn = np.sum((1 - x[:, feat]) * y)
if Np > 0:
_tpr = (1.0 / Np) * np.sum([x[j][feat] * y[j] for j in range(n_points)])
tpr.append(_tpr)
if Nn > 0:
_fpr = (1.0 / Nn) * np.sum([(1 - x[j][feat]) * y[j] for j in range(n_points)])
fpr.append(_fpr)
ct_tpr = np.max(tpr) - np.min(tpr)
ct_fpr = np.max(fpr) - np.min(fpr)
return max(ct_fpr, ct_tpr)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment