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
return res
<