Commit 9330fb42 authored by ALVES Guilherme's avatar ALVES Guilherme
Browse files

Updated decision rule that decides when the model is fair or not

parent 2fcf4d71
from collections import Counter
import math
import random
import oapackage
from anchor import anchor_tabular
import lime_global
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
def fairness_eval(model, train, max_features, sensitive_features, feature_names, class_names, categorical_features, categorical_names):
......@@ -12,12 +17,12 @@ def fairness_eval(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)
indices = sp_obj.indices
a_explainer = anchor_tabular.AnchorTabularExplainer(class_names,feature_names,train,categorical_names=categorical_names)
a_explainers = 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)
exp = a_explainers.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:
......@@ -43,3 +48,169 @@ def fairness_eval(model, train, max_features, sensitive_features, feature_names,
return is_fair, ans_data
def fairness_eval2(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)
# indices = sp_obj.indices
# indices = random.choices(range(len(train)), k=500)
indices = list(range(800))
a_explainers = anchor_tabular.AnchorTabularExplainer(class_names,feature_names,train,categorical_names=categorical_names)
anchors_pos = []
anchors_neg = []
counter = 0
for i in indices:
anchor = a_explainers.explain_instance(train[i], model.predict, threshold=0.95)
print(counter, i,'%.2f' % anchor.precision(),' %.2f' % anchor.coverage(), ' (class %s)' % anchor.exp_map['prediction'], '%s' % (' AND '.join(anchor.names())))
label = anchor.exp_map['prediction']
if label == 0:
anchors_pos.append(anchor)
elif label == 1:
anchors_neg.append(anchor)
counter += 1
distinct_anchors_set = distinct_anchors(non_empty_anchors_fn(anchors_pos))
optimal_pos_anchors = multi_pareto_set(distinct_anchors_set)
pos_scores = scores(optimal_pos_anchors)
distinct_anchors_set = distinct_anchors(non_empty_anchors_fn(anchors_neg))
optimal_neg_anchors = multi_pareto_set(distinct_anchors_set)
neg_scores = scores(optimal_neg_anchors)
print("pareto size pos:",len(optimal_pos_anchors))
print("pareto size neg:",len(optimal_neg_anchors))
features_contributions = {}
for feature_index, contribution in pos_scores.items():
if feature_index in neg_scores:
features_contributions[feature_index] = contribution - neg_scores[feature_index]
else:
features_contributions[feature_index] = contribution
for feature_index, contribution in neg_scores.items():
if feature_index not in features_contributions:
features_contributions[feature_index] = -contribution
is_fair = True
# i = 0
ans_data = []
for key, value in sorted(features_contributions.items(), key=lambda x: math.fabs(x[1]), reverse=True):
ans_data1 = [feature_names[key],value]
ans_data.append(ans_data1)
# if i < max_features and key in sensitive_features:
# is_fair = False
# i += 1
df = pd.DataFrame(ans_data, columns = ["Feature", "Frequency"])
print(df.iloc[(-np.abs(df['Frequency'].values)).argsort()])
return is_fair, ans_data
def scores(anchors):
scores_dic = {}
counter = {}
for anchor in anchors:
for feature in anchor.exp_map['feature']:
if feature not in scores_dic:
scores_dic[feature] = 0
counter[feature] = 1
scores_dic[feature] += 2 * (anchor.precision() * anchor.coverage()) / (anchor.precision() + anchor.coverage())
counter[feature] += 1
for key in scores_dic.keys():
scores_dic[key] = scores_dic[key] / counter[key]
return scores_dic
def non_empty_anchors_fn(anchors):
non_empty_anchors_list = []
for anchor in anchors:
if len(anchor.exp_map['feature']) > 0:
non_empty_anchors_list.append(anchor)
return non_empty_anchors_list
def distinct_anchors(anchors):
distinct_anchor_set = set()
distinct_anchor_list = []
for anchor in anchors:
key = ' '.join(anchor.names())
if key not in distinct_anchor_set:
distinct_anchor_set.add(key)
distinct_anchor_list.append(anchor)
return distinct_anchor_list
def pareto_set(anchors):
anchors = np.array(anchors)
pareto=oapackage.ParetoDoubleLong()
for i in range(len(anchors)):
anchor = anchors[i]
# print(anchor.precision(), anchor.coverage())
w=oapackage.doubleVector( (anchor.precision(), anchor.coverage()))
pareto.addvalue(w, i)
pareto.show(verbose=0)
lst=list(pareto.allindices()) # the indices of the Pareto optimal designs
optimal_anchors=anchors[lst]
return optimal_anchors
def multi_pareto_set(anchors,n=50):
current_anchors = anchors.copy()
optimal_anchors = []
for i in range(n):
current_anchors = np.array(current_anchors)
pareto=oapackage.ParetoDoubleLong()
for i in range(len(current_anchors)):
anchor = current_anchors[i]
# print(anchor.precision(), anchor.coverage())
w=oapackage.doubleVector( (anchor.precision(), anchor.coverage()))
pareto.addvalue(w, i)
pareto.show(verbose=0)
lst=list(pareto.allindices()) # the indices of the Pareto optimal designs
optimal_anchors += current_anchors[lst].tolist()
print("anchors:",len(current_anchors),"to be deleted:",len(current_anchors[lst]),end='')
current_anchors = np.delete(current_anchors, lst)
print("afetr:",len(current_anchors))
# print(optimal_anchors)
# x = [anchor.precision() for anchor in current_anchors]
# y = [anchor.coverage() for anchor in current_anchors]
#
# optimal_x = [anchor.precision() for anchor in optimal_anchors]
# optimal_y = [anchor.coverage() for anchor in optimal_anchors]
#
# h=plt.plot(x, y, '.b', markersize=16, label='Non Pareto-optimal')
# hp=plt.plot(optimal_x, optimal_y, '.r', markersize=16, label='Pareto optimal')
# plt.xlabel('precision', fontsize=16)
# plt.ylabel('coverage', fontsize=16)
# plt.xticks([])
# plt.yticks([])
# _=plt.legend(loc=3, numpoints=1)
#
# plt.show()
return optimal_anchors
......@@ -17,16 +17,24 @@ import pandas as pd
os.environ['KMP_DUPLICATE_LIB_OK']='True'
# np.random.seed(10)
def load_data(source_name, categorical_features, feature_names=None, delimiter=' '):
def load_data(source_name, categorical_features, feature_names=None, delimiter=' ', target_features=None):
"""
Loads data from a text file source
"""
if feature_names != None:
data = pd.read_csv(source_name,names = feature_names, delimiter=' ')
data = pd.read_csv(source_name,names = feature_names, delimiter=delimiter)
else:
data = pd.read_csv(source_name, header=0, delimiter=' ')
feature_names = data.columns.values.tolist()
data = pd.read_csv(source_name, header=0, delimiter=delimiter)
if target_features != None:
data = data[target_features]
data.dropna(subset=target_features, inplace=True)
current_feature_names = data.columns.values.tolist()
data = data.replace(np.nan, '', regex=True)
# data.iloc[categorical_features] = data.iloc[categorical_features].astype(str)
data = data.to_numpy()
labels = data[:,-1]
......@@ -35,7 +43,7 @@ def load_data(source_name, categorical_features, feature_names=None, delimiter='
labels = le.transform(labels)
class_names = le.classes_
data = data[:,:-1]
feature_names = feature_names[:-1]
current_feature_names = current_feature_names[:-1]
categorical_names = {}
for feature in categorical_features:
......@@ -45,27 +53,30 @@ def load_data(source_name, categorical_features, feature_names=None, delimiter='
categorical_names[feature] = le.classes_
data = data.astype(float)
return data, labels, class_names, feature_names, categorical_names
return data, labels, class_names, current_feature_names, categorical_names
def train_classifier(algo, data, labels, remove_features, categorical_features):
def train_classifier(algo, train, test, train_labels, remove_features, categorical_features):
data = np.delete(data, remove_features, axis = 1)
train = np.delete(train, remove_features, axis = 1)
test = np.delete(test, remove_features, axis = 1)
encoder = ColumnTransformer(
[('one_hot_encoder', OneHotEncoder(categories='auto'), categorical_features)],
remainder='passthrough'
)
encoded_train = encoder.fit_transform(data)
end = encoder.fit(np.concatenate([train, test]))
encoded_train = end.transform(train)
sm = SMOTE(sampling_strategy='auto')
train_res, labels_train_res = sm.fit_sample(encoded_train, labels)
train_res, labels_train_res = sm.fit_sample(encoded_train, train_labels)
model = algo()
model.fit(train_res, labels_train_res)
return model, encoder
def ensemble_out(algo,to_drop,train,labels_train, all_categorical_features):
def ensemble_out(algo,to_drop,train,test,labels_train, all_categorical_features):
"""
Implements ENSEMBLE_Out
......@@ -83,13 +94,13 @@ def ensemble_out(algo,to_drop,train,labels_train, all_categorical_features):
for i in to_drop:
remove_features = [i]
categorical_features = remove(remove_features, all_categorical_features)
model, encoder = train_classifier(algo, train, labels_train, remove_features, 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)
categorical_features4 = remove(to_drop, all_categorical_features)
model4, encoder4 = train_classifier(algo, train, labels_train, to_drop, categorical_features4)
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)
......@@ -126,6 +137,7 @@ class Model:
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)
......@@ -178,56 +190,4 @@ def evaluation(probs, labels_test, threshold):
probs = probs[:, 1]
# print('Threshold=%.3f, F-Score=%.5f' % (threshold, f1_score(labels_test, to_labels(probs, threshold))))
accuracy = accuracy_score(labels_test, to_labels(probs, threshold))
return accuracy
# def train_classifier(algo, data, labels, remove_features, categorical_features):
#
# data = np.delete(data, remove_features, axis = 1)
#
# encoder = ColumnTransformer(
# [('one_hot_encoder', OneHotEncoder(categories='auto'), categorical_features)],
# remainder='passthrough'
# )
#
# train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=test_size, random_state=random_state)
#
# sm = SMOTE(sampling_strategy='auto')
# train_res, labels_train_res = sm.fit_sample(train, labels_train)
#
# data = []
# data = np.concatenate([train_res, test])
#
# encoder = encoder.fit(train_res)
# encoded_train = encoder.transform(train_res)
#
# model = algo()
# model.fit(encoded_train.toarray(), labels_train_res)
#
# return model, encoder
# def train_classifier_original(algo, data, labels, remove_features, categorical_features):
#
# data = np.delete(data, remove_features, axis = 1)
#
# encoder = ColumnTransformer(
# [('one_hot_encoder', OneHotEncoder(categories='auto'), categorical_features)],
# remainder='passthrough'
# )
# encoder.fit(data)
#
# train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=test_size, random_state=random_state)
#
# sm = SMOTE(sampling_strategy='auto')
# train_res, labels_train_res = sm.fit_sample(train, labels_train)
#
# data = []
# data = np.concatenate([train_res, test])
#
# encoder = encoder.fit(data)
# encoded_train = encoder.transform(train_res)
#
# model = algo()
# model.fit(encoded_train.toarray(), labels_train_res)
#
# return model, encoder
return accuracy
\ No newline at end of file
......@@ -5,9 +5,10 @@ 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 anchor_global import fairness_eval2
from core import load_data, Model, evaluation, \
find_threshold, remove, ensemble_out, train_classifier
......@@ -16,44 +17,42 @@ from core import load_data, Model, evaluation, \
# categorical_features2 = [0,2,3,5,6,8,9,11,13,14,16,18]
# categorical_features3 = [0,2,3,5,6,8,9,11,13,14,16,18]
# categorical_features4 = [0,2,3,5,6,8,10,12,13,15]
def main():
def main(seed):
train_size = 0.8
test_size = 0.2
max_features = 10
algo = MLPClassifier
algo = LogisticRegression
print(algo.__name__)
source_name = 'german.data'
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, all_categorical_features)
train, test, labels_train, labels_test = train_test_split(data, labels, train_size=train_size, test_size=test_size, random_state=2)
print("###########\nOriginal model \n###########")
model, encoder = train_classifier(algo, train, labels_train, [], all_categorical_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)
accuracy = evaluation(original_model.prob(test), labels_test, threshold_1)
print(accuracy)
fairness_eval(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
print("Original model OK")
fairness_eval2(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
print("###########\nExpOut ensemble's model \n###########")
ensemble = ensemble_out(algo,to_drop,train,labels_train, all_categorical_features)
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")
threshold_2 = find_threshold(ensemble.prob(train), labels_train)
accuracy = evaluation(ensemble.prob(test), labels_test, threshold_2)
print(accuracy)
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)
fairness_eval2(ensemble, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
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
print('german\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
......@@ -12,7 +12,7 @@ def features_contributions(predict_fn, train, feature_names, max_features, class
explainer = lime_tabular.LimeTabularExplainer(train,feature_names=feature_names,class_names=class_names,categorical_features=categorical_features,categorical_names=categorical_names,kernel_width=kernel_width)
sp_obj = submodular_pick.SubmodularPick(explainer, train, predict_fn, sample_size=500, num_features=max_features, num_exps_desired=10)
sp_obj = submodular_pick.SubmodularPick(explainer, train, predict_fn, sample_size=50, num_features=max_features, num_exps_desired=10)
return explainer, sp_obj
......@@ -27,20 +27,25 @@ def fairness_eval(model, train, max_features, sensitive_features, feature_names,
a.update(a1)
is_fair = True
index = 0
counter = 0
ans_data = []
for key in a:
ans_data1 = [feature_names[key],a[key]]
for key,value in sorted(a.items(), key=lambda x: abs(x[1]), reverse=True):
ans_data1 = [feature_names[key],value]
ans_data.append(ans_data1)
if counter < max_features and key in sensitive_features:
is_fair = False
counter += 1
if index < max_features and key in sensitive_features:
counter += 1
index += 1
if counter >= 2: # two sensitive features or more
is_fair = False
# print(feature_names[key] )
df = pd.DataFrame(ans_data, columns = ["Feature", "Contribution"])
# sumdf = df['Contribution'].sum()
# df['Contribution'] = df['Contribution']
print(df.iloc[(-np.abs(df['Contribution'].values)).argsort()])
# print(df.iloc[(-np.abs(df['Contribution'].values)).argsort()])
print(df.iloc[0:max_features])
return is_fair, ans_data
......@@ -25,7 +25,7 @@ def main(source_name, train_size, to_drop, all_categorical_features, max_feature
print("###########\nOriginal model \n###########")
model, encoder = train_classifier(algo, train, labels_train, [], all_categorical_features)
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)
......@@ -34,10 +34,12 @@ def main(source_name, train_size, to_drop, all_categorical_features, max_feature
is_fair ,_ = exp(original_model, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
if not is_fair:
print("Original model is NOT fair")
print("###########\nExpOut ensemble's model \n###########")
ensemble = ensemble_out(algo,to_drop,train,labels_train, all_categorical_features)
ensemble = ensemble_out(algo,to_drop,train,test,labels_train, all_categorical_features)
threshold_2 = find_threshold(ensemble.prob(train), labels_train)
......@@ -45,6 +47,11 @@ def main(source_name, train_size, to_drop, all_categorical_features, max_feature
print(accuracy)
is_fair ,_ = exp(ensemble, train, max_features, to_drop, feature_names, class_names, all_categorical_features, categorical_names)
return ensemble
else:
print("Original model is fair")
return original_model
def algo_parser(algo_str):
......
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