Attention une mise à jour du service Gitlab va être effectuée le mardi 30 novembre entre 17h30 et 18h00. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes. Cette mise à jour intermédiaire en version 14.0.12 nous permettra de rapidement pouvoir mettre à votre disposition une version plus récente.

Commit 35ce914a authored by ALVES Guilherme's avatar ALVES Guilherme
Browse files

Explanations with SHAP and fairness measures

parent 619548ac
import datetime
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble._forest import RandomForestClassifier
from sklearn.ensemble._gb import GradientBoostingClassifier
from sklearn.linear_model._logistic import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neural_network._multilayer_perceptron import MLPClassifier
from sklearn.svm._classes import SVC
from sklearn.tree import DecisionTreeClassifier
from core import load_data, Model, evaluation, \
find_threshold, remove, ensemble_out, train_classifier
from lime_global import fairness_eval
import os
import numpy as np
os.environ['KMP_DUPLICATE_LIB_OK']='True'
np.random.seed(10)
def main(seed):
train_size = 0.8
max_features = 4
sample_size = 500
algo = MLPClassifier
print(algo.__name__)
source_name = 'adult.data'
to_drop = [5,8,9] # sex, race, marital status
all_categorical_features = [1,3,5,6,7,8,9,13]
data, labels, class_names, feature_names, categorical_names = load_data(source_name, all_categorical_features, delimiter=',')
train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=1-train_size, random_state=seed)
print("Features")
for i in range(len(feature_names)):
f_name = feature_names[i]
print(i,f_name,end=' ')
if i in all_categorical_features:
print("[c]",end=' ')
if i in to_drop:
print("[s]",end=' ')
print('')
model, encoder = train_classifier(algo, train, test, labels_train, [], all_categorical_features)
original_model = Model([model],[encoder],[[]])
threshold_1 = find_threshold(original_model.prob(train), labels_train)
print("Original model OK")
fairness_eval(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names, sample_size)
ensemble = ensemble_out(algo,to_drop,train, test, labels_train, all_categorical_features)
threshold_2 = find_threshold(ensemble.prob(train), labels_train)
print("ExpOut ensemble's model OK")
accuracy_original = evaluation(original_model.prob(test), labels_test, threshold_1)
print("accuracy_original", accuracy_original)
accuracy_ensemble = evaluation(ensemble.prob(test), labels_test, threshold_2)
print("accuracy_ensemble", accuracy_ensemble)
# debug
for i in range(len(ensemble.models)):
m = Model([ensemble.models[i]],[ensemble.encoders[i]],[ensemble.features_to_remove[i]])
print("accuracy_m",i,' ', evaluation(m.prob(test), labels_test, threshold_2), sep='')
fairness_eval(ensemble, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names, sample_size)
if __name__ == "__main__":
now = datetime.datetime.now()
print('adult mlp (100,100,)\n',now.year,'-', now.month,'-', now.day,',', now.hour,':', now.minute,':', now.second, sep='')
for i in range(5):
print("experiment i=",i)
main(i)
\ No newline at end of file
from collections import Counter
from anchor import anchor_tabular
import lime_global
import pandas as pd
import numpy as np
def fairness_eval(model, train, max_features, sensitive_features, feature_names, class_names, categorical_features, categorical_names):
_, sp_obj = lime_global.features_contributions(model.prob, train, feature_names, max_features, class_names, categorical_features, categorical_names)
a = Counter()
for i in sp_obj.V:
exp = sp_obj.explanations[i]
a1 = Counter(dict(exp.local_exp[1]))
a.update(a1)
ans_data = []
for key in a:
ans_data1 = [feature_names[key],a[key]]
ans_data.append(ans_data1)
df = pd.DataFrame(ans_data, columns = ["Feature", "Contribution"])
print(df.iloc[(-np.abs(df['Contribution'].values)).argsort()])
indices = sp_obj.indices
a_explainer = anchor_tabular.AnchorTabularExplainer(class_names,feature_names,train,categorical_names=categorical_names)
non_empty_anchors = 0
counter = Counter()
for i in indices:
exp = a_explainer.explain_instance(train[i], model.predict, threshold=0.95)
print(i,'%.2f' % exp.precision(),' %.2f' % exp.coverage(), ' (class %s)' % exp.exp_map['prediction'], '%s' % (' AND '.join(exp.names())))
features = exp.exp_map['feature']
if len(features) > 0:
a1 = Counter(features)
non_empty_anchors += 1
counter.update(a1)
ans_data = []
for key, value in sorted(counter.items(), key=lambda x: x[1], reverse=True):
ans_data1 = [feature_names[key],value/non_empty_anchors]
ans_data.append(ans_data1)
df = pd.DataFrame(ans_data, columns = ["Feature", "Frequency"])
print(df.iloc[(-np.abs(df['Frequency'].values)).argsort()])
......@@ -45,7 +45,7 @@ def fairness_eval(model, train, max_features, sensitive_features, feature_names,
print(df.iloc[(-np.abs(df['Frequency'].values)).argsort()])
return is_fair, ans_data
return is_fair, ans_data, a_explainers
......@@ -54,8 +54,8 @@ def fairness_eval2(model, train, max_features, sensitive_features, feature_names
# _, sp_obj = lime_global.features_contributions(model.prob, train, feature_names, max_features, class_names, categorical_features, categorical_names, sample_size)
# indices = sp_obj.indices
# indices = random.choices(range(len(train)), k=500)
indices = list(range(800))
indices = random.choices(range(len(train)), k=sample_size)
# indices = list(range(800))
a_explainers = anchor_tabular.AnchorTabularExplainer(class_names,feature_names,train,categorical_names=categorical_names)
anchors_pos = []
......@@ -167,7 +167,7 @@ def pareto_set(anchors):
return optimal_anchors
def multi_pareto_set(anchors,n=50):
def multi_pareto_set(anchors,n=1):
current_anchors = anchors.copy()
optimal_anchors = []
......
import datetime
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble._forest import RandomForestClassifier
from sklearn.linear_model._logistic import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neural_network._multilayer_perceptron import MLPClassifier
from sklearn.svm._classes import SVC
from sklearn.tree import DecisionTreeClassifier
from anchor_global import fairness_eval
from core import load_data, Model, evaluation, \
find_threshold, remove, ensemble_out, train_classifier
def main(seed):
train_size = 0.8
max_features = 10
algo = SVC
print(algo.__name__)
source_name = 'bank.data'
to_drop = [0,2] # age, marital
all_categorical_features = [1,2,3,4,6,7,8,10,15]
data, labels, class_names, feature_names, categorical_names = load_data(source_name, all_categorical_features,delimiter=";")
train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=1-train_size, random_state=seed)
model, encoder = train_classifier(algo, train, test, labels_train, [], all_categorical_features)
original_model = Model([model],[encoder],[[]])
# threshold_1 = find_threshold(original_model.prob(train), labels_train)
print("Original model OK")
fairness_eval(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
ensemble = ensemble_out(algo,to_drop,train, test, labels_train, all_categorical_features)
# threshold_2 = find_threshold(ensemble.prob(train), labels_train)
print("ExpOut ensemble's model OK")
# accuracy_original = evaluation(original_model.prob(test), labels_test, threshold_1)
# print("accuracy_original", accuracy_original)
# accuracy_ensemble = evaluation(ensemble.prob(test), labels_test, threshold_2)
# print("accuracy_ensemble", accuracy_ensemble)
fairness_eval(ensemble, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
if __name__ == "__main__":
now = datetime.datetime.now()
print('bank\n',now.year,'-', now.month,'-', now.day,',', now.hour,':', now.minute,':', now.second, sep='')
for i in range(1):
print("experiment i=",i)
main(i)
\ No newline at end of file
import datetime
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble._forest import RandomForestClassifier
from sklearn.linear_model._logistic import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neural_network._multilayer_perceptron import MLPClassifier
from sklearn.tree import DecisionTreeClassifier
from anchor_global import features_contributions
from core import load_data, train_classifier, Model, evaluation, \
find_threshold, remove, ensemble_out, random_state
def main():
train_size = 0.8
test_size = 0.2
algo = MLPClassifier
print(algo.__name__)
source_name = 'german.data'
feature_names = ['existingchecking', 'duration', 'credithistory', 'purpose', 'creditamount', 'savings', 'employmentsince', 'installmentrate', 'statussex', 'otherdebtors', 'residencesince', 'property', 'age', 'otherinstallmentplans', 'housing', 'existingcredits', 'job', 'peopleliable', 'telephone', 'foreignworker', 'classification']
to_drop = [8,18,19]
all_categorical_features = [0,2,3,5,6,8,9,11,13,14,16,18,19]
data, labels, class_names, feature_names, categorical_names = load_data(source_name, feature_names, all_categorical_features)
for i in range(30):
train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=test_size, random_state=i)
model, encoder = train_classifier(algo, train, labels_train, [], all_categorical_features)
original_model = Model([model],[encoder],[[]])
threshold_1 = find_threshold(original_model.prob(train), labels_train)
accuracy1 = evaluation(original_model.prob(test), labels_test, threshold_1)
ensemble = ensemble_out(algo,to_drop,train,labels_train, all_categorical_features)
threshold_2 = find_threshold(ensemble.prob(train), labels_train)
accuracy2 = evaluation(ensemble.prob(test), labels_test, threshold_2)
print(i,accuracy1,accuracy2)
if __name__ == "__main__":
now = datetime.datetime.now()
print(now.year,'-', now.month,'-', now.day,',', now.hour,':', now.minute,':', now.second)
main()
\ No newline at end of file
import datetime
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble._forest import RandomForestClassifier
from sklearn.linear_model._logistic import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neural_network._multilayer_perceptron import MLPClassifier
from sklearn.tree import DecisionTreeClassifier
from anchor_global import fairness_eval
from core import load_data, Model, evaluation, \
find_threshold, remove, ensemble_out, train_classifier
from sklearn.svm import SVC
def main(seed):
train_size = 0.8
max_features = 10
algo = SVC
print(algo.__name__)
source_name = 'compas.data'
to_drop = [4,7,8,9] # age, age_cat, sex, race
all_categorical_features = [5,6,7,8,9]
target_features = ["juv_fel_count", "juv_misd_count", "juv_other_count", "priors_count","age", "c_charge_degree", "c_charge_desc", "age_cat", "sex", "race","is_recid"]
data, labels, class_names, feature_names, categorical_names = load_data(source_name, all_categorical_features,delimiter=",", target_features=target_features)
train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=1-train_size, random_state=seed)
model, encoder = train_classifier(algo, train, test, labels_train, [], all_categorical_features)
original_model = Model([model],[encoder],[[]])
# threshold_1 = find_threshold(original_model.prob(train), labels_train)
print("Original model OK")
fairness_eval(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
ensemble = ensemble_out(algo,to_drop,train, test, labels_train, all_categorical_features)
# threshold_2 = find_threshold(ensemble.prob(train), labels_train)
print("ExpOut ensemble's model OK")
fairness_eval(ensemble, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
#
# accuracy_original = evaluation(original_model.prob(test), labels_test, threshold_1)
# print("accuracy_original", accuracy_original)
# accuracy_ensemble = evaluation(ensemble.prob(test), labels_test, threshold_2)
# print("accuracy_ensemble", accuracy_ensemble)
if __name__ == "__main__":
now = datetime.datetime.now()
print('compas\n',now.year,'-', now.month,'-', now.day,',', now.hour,':', now.minute,':', now.second, sep='')
for i in range(1):
print("experiment i=",i)
main(i)
\ No newline at end of file
......@@ -3,10 +3,17 @@ Implements the main procedures to build fairer ensembles, e.g. feature drop out,
"""
import os
from aif360 import metrics
from aif360.sklearn.metrics.metrics import statistical_parity_difference, \
equal_opportunity_difference, average_odds_difference, \
disparate_impact_ratio, difference, generalized_fpr, specificity_score
from aif360.sklearn.utils import check_groups
from imblearn.over_sampling import SMOTE
from sklearn.compose import ColumnTransformer
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics._classification import confusion_matrix
from sklearn.model_selection._split import train_test_split
from sklearn.preprocessing._encoders import OneHotEncoder
from sklearn.preprocessing._label import LabelEncoder
......@@ -15,8 +22,125 @@ import pandas as pd
os.environ['KMP_DUPLICATE_LIB_OK']='True'
# np.random.seed(10)
class FixOut:
def __init__(self,source_name, sep, train_size, to_drop, all_categorical_features, algo, exp, max_features, sampling_size, seed=None, threshold=None):
self.exp = exp
self.algo = algo
self.seed = seed
self.threshold = threshold
self.data, self.labels, self.class_names, self.feature_names, self.categorical_names = load_data(source_name, all_categorical_features, delimiter=sep)
self.train, self.test, self.labels_train, self.labels_test = train_test_split(self.data, self.labels, train_size=train_size, random_state=self.seed)
self.sensitive_f = to_drop
self.all_categorical_f = all_categorical_features
self.max_features = max_features
self.sampling_size = sampling_size
# print("Features")
# for i in range(len(self.feature_names)):
# f_name = self.feature_names[i]
# print(i,f_name,end=' ')
# if i in all_categorical_features:
# print("[c]",end=' ')
# if i in to_drop:
# print("[s]",end=' ')
# print('')
def is_fair(self):
model, encoder = train_classifier(self.algo, self.train, self.test, self.labels_train, [], self.all_categorical_f, self.seed)
self.original_model = Model([model],[encoder],[[]])
accuracy, _ = evaluation(self.original_model.prob(self.test), self.labels_test)
actual_sensitive, is_fair_flag ,ans_data, explainer = self.exp(self.original_model, self.train, self.max_features, self.sensitive_f, self.feature_names, self.class_names, self.all_categorical_f, self.categorical_names, self.sampling_size, self.threshold)
return actual_sensitive, is_fair_flag, ans_data, accuracy
def ensemble_out(self, actual_sensitive):
"""
Implements ENSEMBLE_Out
Parameters
algo: class of a classification algorithm
to_drop: list of features that must be dropped
train: X
labels_train: y
all_categorical_features: list of indices of categorical features
"""
models, encoders, features_to_remove = [], [], []
for i in actual_sensitive:
remove_features = [i]
categorical_features = remove(remove_features, self.all_categorical_f)
model, encoder = train_classifier(self.algo, self.train, self.test, self.labels_train, remove_features, categorical_features, self.seed)
models.append(model)
encoders.append(encoder)
features_to_remove.append(remove_features)
categorical_features4 = remove(actual_sensitive, self.all_categorical_f)
model4, encoder4 = train_classifier(self.algo, self.train, self.test, self.labels_train, actual_sensitive, categorical_features4, self.seed)
models.append(model4)
encoders.append(encoder4)
features_to_remove.append(actual_sensitive)
self.ensemble = Model(models,encoders,features_to_remove)
accuracy, _ = evaluation(self.ensemble.prob(self.test), self.labels_test)
# _, is_fair_flag ,ans_data, explainer = self.exp(self.ensemble, self.train, self.max_features, actual_sensitive, self.feature_names, self.class_names, self.all_categorical_f, self.categorical_names, self.sampling_size, self.threshold)
#
# return is_fair_flag, ans_data, accuracy
return False, None, accuracy
class Model:
"""Class for ensemble models
Saves a list of trained classifiers and their respective encoders and deleted features
"""
def __init__(self,models,encoders,features_to_remove):
self.models = models
self.encoders = encoders
self.features_to_remove = features_to_remove
"""
Args:
models: a list of trained classifiers
encoders: a list of encoders (1st encoder for the 1st model)
features_to_remove: a list of lists of indices to be removed in order to use models (feature removal mapping)
"""
def prob(self,X):
"""
Returns probability for each class label.
"""
probs = []
n_models = len(self.models)
for i in range(n_models):
model = self.models[i]
encoder = self.encoders[i]
to_remove = self.features_to_remove[i]
comp = model.predict_proba(encoder.transform(np.delete(X, to_remove, axis=1))).astype(float)
probs.append(comp)
res = sum(probs)/n_models
return res
def load_data(source_name, categorical_features, feature_names=None, delimiter=' ', target_features=None):
"""
Loads data from a text file source
......@@ -32,9 +156,15 @@ def load_data(source_name, categorical_features, feature_names=None, delimiter='
data.dropna(subset=target_features, inplace=True)
current_feature_names = data.columns.values.tolist()
current_feature_names = [f_name.replace(" ", "") for f_name in current_feature_names]
data = data.replace(np.nan, '', regex=True)
# data = data.replace('', 0, regex=True)
# data.iloc[categorical_features] = data.iloc[categorical_features].astype(str)
for i in get_numerical_features_indexes(len(current_feature_names), categorical_features):
data.iloc[:,i] = data.iloc[:,i].replace('', 0, regex=False)
data.iloc[:,i] = data.iloc[:,i].replace('?', 0, regex=False)
data = data.to_numpy()
labels = data[:,-1]
......@@ -48,15 +178,18 @@ def load_data(source_name, categorical_features, feature_names=None, delimiter='
categorical_names = {}
for feature in categorical_features:
le = LabelEncoder()
le.fit(data[:, feature])
data[:, feature] = le.transform(data[:, feature])
column = data[:, feature].astype(str)
le.fit(column)
data[:, feature] = le.transform(column)
categorical_names[feature] = le.classes_
data = data.astype(float)
return data, labels, class_names, current_feature_names, categorical_names
def train_classifier(algo, train, test, train_labels, remove_features, categorical_features):
def train_classifier(algo, train, test, train_labels, remove_features, categorical_features, seed):
train = np.delete(train, remove_features, axis = 1)
test = np.delete(test, remove_features, axis = 1)
......@@ -65,96 +198,28 @@ def train_classifier(algo, train, test, train_labels, remove_features, categoric
[('one_hot_encoder', OneHotEncoder(categories='auto'), categorical_features)],
remainder='passthrough'
)
end = encoder.fit(np.concatenate([train, test]))
encoded_train = end.transform(train)
encoder.fit(np.concatenate([train, test]))
sm = SMOTE(sampling_strategy='auto')
train_res, labels_train_res = sm.fit_sample(encoded_train, train_labels)
sm = SMOTE(sampling_strategy='auto', random_state=seed)
train_res, labels_train_res = sm.fit_sample(train, train_labels)
encoder.fit(np.concatenate([train_res, test]))
encoded_train_res = encoder.transform(train_res)
# model = algo(hidden_layer_sizes=(100,100,),random_state=seed)
model = algo()
model.fit(train_res, labels_train_res)
model.fit(encoded_train_res, labels_train_res)
return model, encoder
def ensemble_out(algo,to_drop,train,test,labels_train, all_categorical_features):
"""
Implements ENSEMBLE_Out
Parameters
algo: class of a classification algorithm
to_drop: list of features that must be dropped
train: X
labels_train: y
all_categorical_features: list of indices of categorical features
"""
models, encoders, features_to_remove = [], [], []
def get_numerical_features_indexes(num_features, categorical_features):
for i in to_drop:
remove_features = [i]
categorical_features = remove(remove_features, all_categorical_features)
model, encoder = train_classifier(algo, train, test, labels_train, remove_features, categorical_features)
models.append(model)
encoders.append(encoder)
features_to_remove.append(remove_features)
res = []
for i in range(num_features):
if i not in categorical_features:
res.append(i)
return res
categorical_features4 = remove(to_drop, all_categorical_features)
model4, encoder4 = train_classifier(algo, train, test, labels_train, to_drop, categorical_features4)
models.append(model4)
encoders.append(encoder4)
features_to_remove.append(to_drop)
return Model(models,encoders,features_to_remove)
class Model:
"""Class for ensemble models
Saves a list of trained classifiers and their respective encoders and deleted features
"""
def __init__(self,models,encoders,features_to_remove):
self.models = models
self.encoders = encoders
self.features_to_remove = features_to_remove
"""
Args:
models: a list of trained classifiers
encoders: a list of encoders (1st encoder for the 1st model)
features_to_remove: a list of lists of indices to be removed in order to use models (feature removal mapping)
"""
def prob(self,X):
"""
Returns probability for each class label.
"""
probs = []
n_models = len(self.models)
for i in range(n_models):
model = self.models[i]
encoder = self.encoders[i]
to_remove = self.features_to_remove[i]
# print("model ",i)
comp = model.predict_proba(encoder.transform(np.delete(X, to_remove, axis=1))).astype(float)
probs.append(comp)
res = sum(probs)/n_models