From cd58b1f34a7b53133a7e172ebda83e9661109a3c Mon Sep 17 00:00:00 2001 From: audemard Date: Tue, 31 Oct 2023 15:14:48 +0100 Subject: [PATCH 01/12] change function name in explainer --- pyxai/sources/core/explainer/Explainer.py | 4 ++-- pyxai/tests/compas.csv | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyxai/sources/core/explainer/Explainer.py b/pyxai/sources/core/explainer/Explainer.py index f654ec3..9e204e8 100644 --- a/pyxai/sources/core/explainer/Explainer.py +++ b/pyxai/sources/core/explainer/Explainer.py @@ -363,7 +363,7 @@ def set_excluded_features(self, excluded_features): @param excluded_features (list[str] | tuple[str]): the features names to be excluded """ if len(excluded_features) == 0: - self.unset_specific_features() + self.unset_excluded_features() return self._excluded_features = excluded_features if self.instance is None: @@ -386,7 +386,7 @@ def _set_specific_features(self, specific_features): # TODO a changer en je veu self.set_excluded_features(excluded) - def unset_specific_features(self): + def unset_excluded_features(self): """ Unset the features set with the set_excluded_features method. """ diff --git a/pyxai/tests/compas.csv b/pyxai/tests/compas.csv index c76dec1..0469e06 100644 --- a/pyxai/tests/compas.csv +++ b/pyxai/tests/compas.csv @@ -1,4 +1,4 @@ -0,1,2,3,4,5,6,7,8,9,10,11 +Number_of_Priors,score_factor,Age_Above_FourtyFive,Age_Below_TwentyFive,Origin_African_American,Origin_Asian,Origin_Hispanic,Origin_Native_American,Origin_Other,Female,Misdemeanor,Two_yr_Recidivism 0,0,1,0,0,0,0,0,1,0,0,0 0,0,0,0,1,0,0,0,0,0,0,1 4,0,0,1,1,0,0,0,0,0,0,1 From cad77885bc6e8a26421987ea2a0fdd5b06b26a62 Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Thu, 16 Nov 2023 16:38:08 +0100 Subject: [PATCH 02/12] contrastives for BT --- pyxai/examples/BT/builder-simple.py | 5 ++ pyxai/sources/core/explainer/explainerBT.py | 11 ++- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 80 ++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 pyxai/sources/solvers/CPLEX/ContrastiveBT.py diff --git a/pyxai/examples/BT/builder-simple.py b/pyxai/examples/BT/builder-simple.py index c25812e..d6675fc 100755 --- a/pyxai/examples/BT/builder-simple.py +++ b/pyxai/examples/BT/builder-simple.py @@ -55,6 +55,11 @@ print("is_tree_specific:", explainer.is_tree_specific_reason(tree_specific)) print("is_sufficient_reason:", explainer.is_sufficient_reason(tree_specific)) +print("---------------------------------------------------") +contrastive_reason = explainer.contrastive_reason() +print("contrastive reason:", explainer.to_features(contrastive_reason)) +print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) + print("---------------------------------------------------") sufficient = explainer.sufficient_reason() print("sufficient reason:", sufficient) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index ed9aae0..c1ccfa2 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -10,7 +10,7 @@ from pyxai.sources.solvers.CSP.AbductiveV1 import AbductiveModelV1 from pyxai.sources.solvers.CSP.TSMinimalV2 import TSMinimal from pyxai.sources.solvers.GRAPH.TreeDecomposition import TreeDecomposition - +from pyxai.sources.solvers.CPLEX.ContrastiveBT import ContrastiveBT class ExplainerBT(Explainer): @@ -241,8 +241,6 @@ def tree_specific_reason(self, *, n_iterations=50, time_limit=None, seed=0, hist The method used (in c++), for a given seed, compute several tree specific reasons and return the best. For that, the algorithm is executed either during a given time or or until a certain number of reasons is calculated. - The parameter 'reason_expressivity' have to be fixed either by ReasonExpressivity.Features or ReasonExpressivity.Conditions. - Args: n_iterations (int, optional): _description_. Defaults to 50. time_limit (int, optional): _description_. Defaults to None. @@ -351,6 +349,13 @@ def is_tree_specific_reason(self, reason, check_minimal_inclusion=False): return False return True + + def contrastive_reason(self, time_limit = None): + contrastive_bt = ContrastiveBT() + c = contrastive_bt.create_model_and_solve(self) + return c + + # def check_sufficient(self, reason, n_samples=1000): # """ # Check if the ''reason'' is abductive and check if the reasons with one selected literal in less are not abductives. This allows to check diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py new file mode 100644 index 0000000..d57c2bb --- /dev/null +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -0,0 +1,80 @@ +from ortools.linear_solver import pywraplp +from pyxai.sources.core.structure.type import TypeLeaf + +class ContrastiveBT: + def __init__(self): + pass + + + + def create_model_and_solve(self, explainer, *, time_limit= None): + forest = explainer._boosted_trees.forest + leaves = [tree.get_leaves() for tree in forest] + bin_len = len(explainer.binary_representation) + solver = pywraplp.Solver.CreateSolver("SCIP") + + # Model variables + x = [solver.BoolVar(f"x[{i}]") for i in range(bin_len)] # The instance + + y = [] + for j, tree in enumerate(forest): + y.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves + + z = [solver.BoolVar(f"z[{i}]") for i in range(bin_len)] # The flipped variables + + + # Constraints related to tree structure + + for j, tree in enumerate(forest): + for i, leave in enumerate(tree.get_leaves()): + t = TypeLeaf.LEFT if leave.parent.left == leave else TypeLeaf.RIGHT + cube = forest[j].create_cube(leave.parent, t) + nb_neg = sum((1 for l in cube if l < 0)) + nb_pos = sum((1 for l in cube if l > 0)) + constraint = solver.RowConstraint(-solver.infinity(), nb_neg) + constraint.SetCoefficient(y[j][i], nb_pos + nb_neg) + print(cube) + for l in cube: + constraint.SetCoefficient(x[abs(l)-1], -1 if l > 0 else 1) + + # Only one leave activated per tree + for j, tree in enumerate(forest): + constraint = solver.RowConstraint(1, 1) + for v in y[j]: + constraint.SetCoefficient(v, 1) + + # Change the prediction + if explainer.target_prediction == 1: + constraint_target = solver.RowConstraint(-solver.infinity(), 0) + else: + constraint_target = solver.RowConstraint(0, solver.infinity()) + for j, tree in enumerate(forest): + for i, leave in enumerate(tree.get_leaves()): + constraint_target.SetCoefficient(y[j][i], leave.value) + + # Constraints related to theory + + # links between x and z + for i in range(bin_len): + const1 = solver.RowConstraint(-solver.infinity(), 1 if explainer.binary_representation[i] > 0 else 0) + const1.SetCoefficient(x[i], 1) + const1.SetCoefficient(z[i], -1) + const2 = solver.RowConstraint(-solver.infinity(), -1 if explainer.binary_representation[i] > 0 else 0) + const2.SetCoefficient(x[i], -1) + const2.SetCoefficient(z[i], -1) + + # Objective function + + objective = solver.Objective() + for i in range(bin_len): + objective.SetCoefficient(z[i], 1) + objective.SetMinimization() + + # Solve the problem + + print(solver.ExportModelAsLpFormat(obfuscated=False)) + solver.Solve() + print(explainer.binary_representation) + for v in solver.variables(): + print(v.name(), v.solution_value()) + return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] \ No newline at end of file From c0dd6269077dcc80a266bf257082eed1b2f81dd7 Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Thu, 16 Nov 2023 17:20:57 +0100 Subject: [PATCH 03/12] add tests for contrastive BT --- pyxai/examples/BT/builder-simple.py | 4 ++-- pyxai/sources/core/explainer/explainerBT.py | 2 ++ pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 9 ++++----- pyxai/tests/explaining/bt.py | 8 ++++++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pyxai/examples/BT/builder-simple.py b/pyxai/examples/BT/builder-simple.py index d6675fc..ffc26a6 100755 --- a/pyxai/examples/BT/builder-simple.py +++ b/pyxai/examples/BT/builder-simple.py @@ -32,7 +32,7 @@ BTs = Builder.BoostedTrees([tree1, tree2, tree3], n_classes=2) -instance = (4, 3, 1, 1) +instance = (4, 3, 1, 0) print("instance:", instance) explainer = Explainer.initialize(BTs, instance) @@ -45,7 +45,7 @@ print("direct reason:", direct) direct_features = explainer.to_features(direct) print("to_features:", direct_features) -assert direct_features == ('f1 > 2', 'f2 > 1', 'f3 == 1', 'f4 == 1'), "The direct reason is not correct." +#assert direct_features == ('f1 > 2', 'f2 > 1', 'f3 == 1', 'f4 == 1'), "The direct reason is not correct." print("---------------------------------------------------") tree_specific = explainer.tree_specific_reason() diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index c1ccfa2..007cf69 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -351,6 +351,8 @@ def is_tree_specific_reason(self, reason, check_minimal_inclusion=False): def contrastive_reason(self, time_limit = None): + if self._instance is None: + raise ValueError("Instance is not set") contrastive_bt = ContrastiveBT() c = contrastive_bt.create_model_and_solve(self) return c diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index d57c2bb..9ec45ad 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -33,7 +33,7 @@ def create_model_and_solve(self, explainer, *, time_limit= None): nb_pos = sum((1 for l in cube if l > 0)) constraint = solver.RowConstraint(-solver.infinity(), nb_neg) constraint.SetCoefficient(y[j][i], nb_pos + nb_neg) - print(cube) + #print(cube) for l in cube: constraint.SetCoefficient(x[abs(l)-1], -1 if l > 0 else 1) @@ -72,9 +72,8 @@ def create_model_and_solve(self, explainer, *, time_limit= None): # Solve the problem - print(solver.ExportModelAsLpFormat(obfuscated=False)) + #print(solver.ExportModelAsLpFormat(obfuscated=False)) solver.Solve() - print(explainer.binary_representation) - for v in solver.variables(): - print(v.name(), v.solution_value()) + #for v in solver.variables(): + # print(v.name(), v.solution_value()) return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] \ No newline at end of file diff --git a/pyxai/tests/explaining/bt.py b/pyxai/tests/explaining/bt.py index 138d82f..043e2a3 100755 --- a/pyxai/tests/explaining/bt.py +++ b/pyxai/tests/explaining/bt.py @@ -43,6 +43,14 @@ def test_excluded(self): tree_specific_reason = explainer.tree_specific_reason() self.assertFalse(explainer.reason_contains_features(tree_specific_reason, 'Female')) + def test_contrastive(self): + learner, model = self.init() + explainer = Explainer.initialize(model) + instances = learner.get_instances(model, n=5) + for instance, prediction in instances: + explainer.set_instance(instance) + contrastive_reason = explainer.contrastive_reason() + self.assertTrue(len(contrastive_reason) > 0 and explainer.is_contrastive_reason(contrastive_reason)) if __name__ == '__main__': unittest.main(verbosity=0) From 147ea557ac177c991d0bd2a4adcec93686b17fa5 Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Sun, 19 Nov 2023 09:49:12 +0100 Subject: [PATCH 04/12] add theroy for contrastive in progress --- pyxai/sources/core/explainer/explainerBT.py | 2 +- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 21 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index 007cf69..f3005dc 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -354,7 +354,7 @@ def contrastive_reason(self, time_limit = None): if self._instance is None: raise ValueError("Instance is not set") contrastive_bt = ContrastiveBT() - c = contrastive_bt.create_model_and_solve(self) + c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses()) return c diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index 9ec45ad..1f25a08 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -1,13 +1,13 @@ from ortools.linear_solver import pywraplp from pyxai.sources.core.structure.type import TypeLeaf + class ContrastiveBT: def __init__(self): pass - - def create_model_and_solve(self, explainer, *, time_limit= None): + def create_model_and_solve(self, explainer, theory, *, time_limit=None): forest = explainer._boosted_trees.forest leaves = [tree.get_leaves() for tree in forest] bin_len = len(explainer.binary_representation) @@ -18,24 +18,23 @@ def create_model_and_solve(self, explainer, *, time_limit= None): y = [] for j, tree in enumerate(forest): - y.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves + y.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves z = [solver.BoolVar(f"z[{i}]") for i in range(bin_len)] # The flipped variables - # Constraints related to tree structure for j, tree in enumerate(forest): for i, leave in enumerate(tree.get_leaves()): - t = TypeLeaf.LEFT if leave.parent.left == leave else TypeLeaf.RIGHT + t = TypeLeaf.LEFT if leave.parent.left == leave else TypeLeaf.RIGHT cube = forest[j].create_cube(leave.parent, t) nb_neg = sum((1 for l in cube if l < 0)) nb_pos = sum((1 for l in cube if l > 0)) constraint = solver.RowConstraint(-solver.infinity(), nb_neg) constraint.SetCoefficient(y[j][i], nb_pos + nb_neg) - #print(cube) + # print(cube) for l in cube: - constraint.SetCoefficient(x[abs(l)-1], -1 if l > 0 else 1) + constraint.SetCoefficient(x[abs(l) - 1], -1 if l > 0 else 1) # Only one leave activated per tree for j, tree in enumerate(forest): @@ -53,6 +52,8 @@ def create_model_and_solve(self, explainer, *, time_limit= None): constraint_target.SetCoefficient(y[j][i], leave.value) # Constraints related to theory + if theory is not None: + assert (False) # TODO # links between x and z for i in range(bin_len): @@ -72,8 +73,8 @@ def create_model_and_solve(self, explainer, *, time_limit= None): # Solve the problem - #print(solver.ExportModelAsLpFormat(obfuscated=False)) + # print(solver.ExportModelAsLpFormat(obfuscated=False)) solver.Solve() - #for v in solver.variables(): + # for v in solver.variables(): # print(v.name(), v.solution_value()) - return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] \ No newline at end of file + return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] From d09f6e8fce50aba790334446132759e13150fb75 Mon Sep 17 00:00:00 2001 From: audemard Date: Tue, 21 Nov 2023 12:56:14 +0100 Subject: [PATCH 05/12] theory --- pyxai/examples/BT/simple.py | 26 ++++++++++++-------- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 13 +++++++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pyxai/examples/BT/simple.py b/pyxai/examples/BT/simple.py index 525cf52..5126f52 100644 --- a/pyxai/examples/BT/simple.py +++ b/pyxai/examples/BT/simple.py @@ -9,7 +9,7 @@ # Explanation part print(instance) -explainer = Explainer.initialize(model, instance) +explainer = Explainer.initialize(model, instance, features_type={"numerical": Learning.DEFAULT}) direct_reason = explainer.direct_reason() print("len direct: ", len(direct_reason)) print("is a reason (for 50 checks):", explainer.is_reason(direct_reason, n_samples=50)) @@ -18,15 +18,21 @@ tree_specific_reason = explainer.tree_specific_reason(n_iterations=10) print("\nlen tree_specific: ", len(tree_specific_reason)) print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) -instances = learner.get_instances(n=100) - -print(instances) -for inst, p in instances: - explainer.set_instance(inst) - direct_reason = explainer.direct_reason() - - tree_specific_reason = explainer.tree_specific_reason(n_iterations=100) - print("is a tree specific", explainer.is_tree_specific_reason(tree_specific_reason)) + +contrastive_reason = explainer.contrastive_reason() +print("\n\ncontrastive reason: ", explainer.to_features(contrastive_reason, contrastive=True)) +print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) +print() +print() +#instances = learner.get_instances(n=100) + +#print(instances) +#for inst, p in instances: +# explainer.set_instance(inst) +# direct_reason = explainer.direct_reason() +# +# tree_specific_reason = explainer.tree_specific_reason(n_iterations=100) +# print("is a tree specific", explainer.is_tree_specific_reason(tree_specific_reason)) #explainer.show() #minimal_tree_specific_reason = explainer.minimal_tree_specific_reason(time_limit=20) diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index 1f25a08..ead1f48 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -53,7 +53,13 @@ def create_model_and_solve(self, explainer, theory, *, time_limit=None): # Constraints related to theory if theory is not None: - assert (False) # TODO + print(theory) + for clause in theory: + constraint = solver.RowConstraint(1, solver.infinity()) + for l in clause: + print(1) + constraint.SetCoefficient(x[abs(l) - 1], -1 if l < 0 else 1) + # links between x and z for i in range(bin_len): @@ -73,8 +79,9 @@ def create_model_and_solve(self, explainer, theory, *, time_limit=None): # Solve the problem - # print(solver.ExportModelAsLpFormat(obfuscated=False)) - solver.Solve() + #print(solver.ExportModelAsLpFormat(obfuscated=False)) + r= solver.Solve() + print(r) # for v in solver.variables(): # print(v.name(), v.solution_value()) return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] From b253e7f43f3cfedff93abeb8416235387aaba38e Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Wed, 22 Nov 2023 18:04:24 +0100 Subject: [PATCH 06/12] add theory and time for contrastive --- pyxai/examples/BT/simple.py | 6 +-- pyxai/sources/core/explainer/explainerBT.py | 19 ++++--- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 57 ++++++++++---------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/pyxai/examples/BT/simple.py b/pyxai/examples/BT/simple.py index 5126f52..c1c8c40 100644 --- a/pyxai/examples/BT/simple.py +++ b/pyxai/examples/BT/simple.py @@ -5,7 +5,7 @@ # Machine learning part learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) -instance, prediction = learner.get_instances(model=model, n=1, correct=False) +instance, prediction = learner.get_instances(model=model, n=1, correct=True) # Explanation part print(instance) @@ -19,10 +19,10 @@ print("\nlen tree_specific: ", len(tree_specific_reason)) print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) -contrastive_reason = explainer.contrastive_reason() +contrastive_reason = explainer.contrastive_reason(time_limit=3) print("\n\ncontrastive reason: ", explainer.to_features(contrastive_reason, contrastive=True)) print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) -print() +print("elapsed time: ", explainer.elapsed_time) print() #instances = learner.get_instances(n=100) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index f3005dc..63ad85a 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -12,6 +12,7 @@ from pyxai.sources.solvers.GRAPH.TreeDecomposition import TreeDecomposition from pyxai.sources.solvers.CPLEX.ContrastiveBT import ContrastiveBT + class ExplainerBT(Explainer): def __init__(self, boosted_trees, instance=None): @@ -36,12 +37,13 @@ def set_instance(self, instance): def _to_binary_representation(self, instance): return self._boosted_trees.instance_to_binaries(instance) + def _theory_clauses(self): return self._boosted_trees.get_theory(self._binary_representation) def is_implicant(self, abductive): - + if self._boosted_trees.n_classes == 2: # 2-classes case sum_weights = [] @@ -55,7 +57,7 @@ def is_implicant(self, abductive): return self.target_prediction == prediction else: - + # multi-classes case worst_one = self.compute_weights_class(abductive, self.target_prediction, king="worst") best_ones = [self.compute_weights_class(abductive, cl, king="best") for cl @@ -260,10 +262,10 @@ def tree_specific_reason(self, *, n_iterations=50, time_limit=None, seed=0, hist if self.c_BT is None: # Preprocessing to give all trees in the c++ library self.c_BT = c_explainer.new_classifier_BT(self._boosted_trees.n_classes) - + for tree in self._boosted_trees.forest: c_explainer.add_tree(self.c_BT, tree.raw_data_for_CPP()) - + c_explainer.set_excluded(self.c_BT, tuple(self._excluded_literals)) if self._theory: c_explainer.set_theory(self.c_BT, tuple(self._boosted_trees.get_theory(self._binary_representation))) @@ -350,14 +352,17 @@ def is_tree_specific_reason(self, reason, check_minimal_inclusion=False): return True - def contrastive_reason(self, time_limit = None): + def contrastive_reason(self, time_limit=None): if self._instance is None: raise ValueError("Instance is not set") + + starting_time = -time.process_time() contrastive_bt = ContrastiveBT() - c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses()) + c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), time_limit) + time_used = starting_time + time.process_time() + self._elapsed_time = time_used if time_limit is None or time_used < time_limit else Explainer.TIMEOUT return c - # def check_sufficient(self, reason, n_samples=1000): # """ # Check if the ''reason'' is abductive and check if the reasons with one selected literal in less are not abductives. This allows to check diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index ead1f48..9b222e3 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -7,23 +7,25 @@ def __init__(self): pass - def create_model_and_solve(self, explainer, theory, *, time_limit=None): + def create_model_and_solve(self, explainer, theory, time_limit): forest = explainer._boosted_trees.forest leaves = [tree.get_leaves() for tree in forest] bin_len = len(explainer.binary_representation) solver = pywraplp.Solver.CreateSolver("SCIP") + if time_limit is not None: + solver.SetTimeLimit(time_limit * 1000) # time limit in milisecond + # Model variables - x = [solver.BoolVar(f"x[{i}]") for i in range(bin_len)] # The instance + instance = [solver.BoolVar(f"x[{i}]") for i in range(bin_len)] # The instance - y = [] + active_leaves = [] for j, tree in enumerate(forest): - y.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves + active_leaves.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves - z = [solver.BoolVar(f"z[{i}]") for i in range(bin_len)] # The flipped variables + flipped = [solver.BoolVar(f"z[{i}]") for i in range(bin_len)] # The flipped variables # Constraints related to tree structure - for j, tree in enumerate(forest): for i, leave in enumerate(tree.get_leaves()): t = TypeLeaf.LEFT if leave.parent.left == leave else TypeLeaf.RIGHT @@ -31,15 +33,14 @@ def create_model_and_solve(self, explainer, theory, *, time_limit=None): nb_neg = sum((1 for l in cube if l < 0)) nb_pos = sum((1 for l in cube if l > 0)) constraint = solver.RowConstraint(-solver.infinity(), nb_neg) - constraint.SetCoefficient(y[j][i], nb_pos + nb_neg) - # print(cube) + constraint.SetCoefficient(active_leaves[j][i], nb_pos + nb_neg) for l in cube: - constraint.SetCoefficient(x[abs(l) - 1], -1 if l > 0 else 1) + constraint.SetCoefficient(instance[abs(l) - 1], -1 if l > 0 else 1) # Only one leave activated per tree for j, tree in enumerate(forest): constraint = solver.RowConstraint(1, 1) - for v in y[j]: + for v in active_leaves[j]: constraint.SetCoefficient(v, 1) # Change the prediction @@ -49,39 +50,37 @@ def create_model_and_solve(self, explainer, theory, *, time_limit=None): constraint_target = solver.RowConstraint(0, solver.infinity()) for j, tree in enumerate(forest): for i, leave in enumerate(tree.get_leaves()): - constraint_target.SetCoefficient(y[j][i], leave.value) + constraint_target.SetCoefficient(active_leaves[j][i], leave.value) # Constraints related to theory if theory is not None: print(theory) for clause in theory: - constraint = solver.RowConstraint(1, solver.infinity()) + constraint = solver.RowConstraint(-solver.infinity(), 0) for l in clause: - print(1) - constraint.SetCoefficient(x[abs(l) - 1], -1 if l < 0 else 1) + constraint.SetCoefficient(instance[abs(l) - 1], 1 if l < 0 else -1) - - # links between x and z + # links between instance and flipped for i in range(bin_len): const1 = solver.RowConstraint(-solver.infinity(), 1 if explainer.binary_representation[i] > 0 else 0) - const1.SetCoefficient(x[i], 1) - const1.SetCoefficient(z[i], -1) + const1.SetCoefficient(instance[i], 1) + const1.SetCoefficient(flipped[i], -1) const2 = solver.RowConstraint(-solver.infinity(), -1 if explainer.binary_representation[i] > 0 else 0) - const2.SetCoefficient(x[i], -1) - const2.SetCoefficient(z[i], -1) + const2.SetCoefficient(instance[i], -1) + const2.SetCoefficient(flipped[i], -1) - # Objective function + # links between features and flipped + + # Objective function objective = solver.Objective() for i in range(bin_len): - objective.SetCoefficient(z[i], 1) + objective.SetCoefficient(flipped[i], 1) objective.SetMinimization() # Solve the problem - - #print(solver.ExportModelAsLpFormat(obfuscated=False)) - r= solver.Solve() - print(r) - # for v in solver.variables(): - # print(v.name(), v.solution_value()) - return [explainer.binary_representation[i] for i in range(len(z)) if z[i].solution_value() > 0] + # print(solver.ExportModelAsLpFormat(obfuscated=False)) + status = solver.Solve() + if status not in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]: + return None + return [explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5] From fd707df6596d27129abf500c08aa01497fbdc66f Mon Sep 17 00:00:00 2001 From: audemard Date: Thu, 23 Nov 2023 08:08:21 +0100 Subject: [PATCH 07/12] add number of solutions for contrastive --- pyxai/examples/BT/builder-simple.py | 2 +- pyxai/examples/BT/simple.py | 4 +-- pyxai/sources/core/explainer/explainerBT.py | 5 ++- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 33 ++++++++++++++++---- pyxai/tests/explaining/bt.py | 2 +- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pyxai/examples/BT/builder-simple.py b/pyxai/examples/BT/builder-simple.py index ffc26a6..550f979 100755 --- a/pyxai/examples/BT/builder-simple.py +++ b/pyxai/examples/BT/builder-simple.py @@ -56,7 +56,7 @@ print("is_sufficient_reason:", explainer.is_sufficient_reason(tree_specific)) print("---------------------------------------------------") -contrastive_reason = explainer.contrastive_reason() +contrastive_reason = explainer.minimal_contrastive_reason() print("contrastive reason:", explainer.to_features(contrastive_reason)) print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) diff --git a/pyxai/examples/BT/simple.py b/pyxai/examples/BT/simple.py index c1c8c40..2a47819 100644 --- a/pyxai/examples/BT/simple.py +++ b/pyxai/examples/BT/simple.py @@ -5,7 +5,7 @@ # Machine learning part learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) -instance, prediction = learner.get_instances(model=model, n=1, correct=True) +instance, prediction = learner.get_instances(model=model, n=1, correct=False) # Explanation part print(instance) @@ -19,7 +19,7 @@ print("\nlen tree_specific: ", len(tree_specific_reason)) print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) -contrastive_reason = explainer.contrastive_reason(time_limit=3) +contrastive_reason = explainer.minimal_contrastive_reason(n=2) print("\n\ncontrastive reason: ", explainer.to_features(contrastive_reason, contrastive=True)) print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) print("elapsed time: ", explainer.elapsed_time) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index 63ad85a..bacc4ce 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -352,13 +352,12 @@ def is_tree_specific_reason(self, reason, check_minimal_inclusion=False): return True - def contrastive_reason(self, time_limit=None): + def minimal_contrastive_reason(self, *, n=1, time_limit=None): if self._instance is None: raise ValueError("Instance is not set") - starting_time = -time.process_time() contrastive_bt = ContrastiveBT() - c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), time_limit) + c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), 1, time_limit) time_used = starting_time + time.process_time() self._elapsed_time = time_used if time_limit is None or time_used < time_limit else Explainer.TIMEOUT return c diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index 9b222e3..3d57f10 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -1,5 +1,6 @@ from ortools.linear_solver import pywraplp from pyxai.sources.core.structure.type import TypeLeaf +from pyxai.sources.core.explainer.Explainer import Explainer class ContrastiveBT: @@ -7,7 +8,7 @@ def __init__(self): pass - def create_model_and_solve(self, explainer, theory, time_limit): + def create_model_and_solve(self, explainer, theory, n, time_limit): forest = explainer._boosted_trees.forest leaves = [tree.get_leaves() for tree in forest] bin_len = len(explainer.binary_representation) @@ -54,7 +55,6 @@ def create_model_and_solve(self, explainer, theory, time_limit): # Constraints related to theory if theory is not None: - print(theory) for clause in theory: constraint = solver.RowConstraint(-solver.infinity(), 0) for l in clause: @@ -80,7 +80,28 @@ def create_model_and_solve(self, explainer, theory, time_limit): # Solve the problem # print(solver.ExportModelAsLpFormat(obfuscated=False)) - status = solver.Solve() - if status not in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]: - return None - return [explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5] + n = 2 + results = [] + first = True + best_objective = -1 + while True: + if first: + status = solver.Solve() + else: + print("try") + status = solver.NextSolution() + print("status:", status) + print("Obj: ", solver.Objective().Value()) + print([explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5]) + if status not in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]: + break + solution = [explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5] + if first: + best_objective = len(solution) + first = False + if len(solution) > best_objective: + break + results.append(solution) + if len(results) == n: + break + return Explainer.format(results, n) \ No newline at end of file diff --git a/pyxai/tests/explaining/bt.py b/pyxai/tests/explaining/bt.py index 043e2a3..852305b 100755 --- a/pyxai/tests/explaining/bt.py +++ b/pyxai/tests/explaining/bt.py @@ -49,7 +49,7 @@ def test_contrastive(self): instances = learner.get_instances(model, n=5) for instance, prediction in instances: explainer.set_instance(instance) - contrastive_reason = explainer.contrastive_reason() + contrastive_reason = explainer.minimal_contrastive_reason() self.assertTrue(len(contrastive_reason) > 0 and explainer.is_contrastive_reason(contrastive_reason)) if __name__ == '__main__': From 85930f4662c2fd0fd02c260272384c698e331c6b Mon Sep 17 00:00:00 2001 From: audemard Date: Thu, 23 Nov 2023 08:30:57 +0100 Subject: [PATCH 08/12] add excluded features in contrastive BT --- pyxai/examples/BT/simple.py | 3 ++- pyxai/sources/core/explainer/explainerBT.py | 2 +- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 16 +++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyxai/examples/BT/simple.py b/pyxai/examples/BT/simple.py index 2a47819..83caae2 100644 --- a/pyxai/examples/BT/simple.py +++ b/pyxai/examples/BT/simple.py @@ -5,7 +5,7 @@ # Machine learning part learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) -instance, prediction = learner.get_instances(model=model, n=1, correct=False) +instance, prediction = learner.get_instances(model=model, n=1, correct=True) # Explanation part print(instance) @@ -19,6 +19,7 @@ print("\nlen tree_specific: ", len(tree_specific_reason)) print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) +explainer.set_excluded_features(["score_factor"]) contrastive_reason = explainer.minimal_contrastive_reason(n=2) print("\n\ncontrastive reason: ", explainer.to_features(contrastive_reason, contrastive=True)) print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index bacc4ce..b2806ee 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -357,7 +357,7 @@ def minimal_contrastive_reason(self, *, n=1, time_limit=None): raise ValueError("Instance is not set") starting_time = -time.process_time() contrastive_bt = ContrastiveBT() - c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), 1, time_limit) + c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), self._excluded_literals, 1, time_limit) time_used = starting_time + time.process_time() self._elapsed_time = time_used if time_limit is None or time_used < time_limit else Explainer.TIMEOUT return c diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index 3d57f10..d0aba94 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -8,7 +8,7 @@ def __init__(self): pass - def create_model_and_solve(self, explainer, theory, n, time_limit): + def create_model_and_solve(self, explainer, theory, excluded, n, time_limit): forest = explainer._boosted_trees.forest leaves = [tree.get_leaves() for tree in forest] bin_len = len(explainer.binary_representation) @@ -69,6 +69,10 @@ def create_model_and_solve(self, explainer, theory, n, time_limit): const2.SetCoefficient(instance[i], -1) const2.SetCoefficient(flipped[i], -1) + # Set excluded features + for lit in excluded: + constraint = solver.RowConstraint(0, 0) + constraint.SetCoefficient(flipped[abs(lit) - 1], 1) # links between features and flipped @@ -77,10 +81,11 @@ def create_model_and_solve(self, explainer, theory, n, time_limit): for i in range(bin_len): objective.SetCoefficient(flipped[i], 1) objective.SetMinimization() - - # Solve the problem # print(solver.ExportModelAsLpFormat(obfuscated=False)) - n = 2 + + + + # Solve the problem and extract n solutions results = [] first = True best_objective = -1 @@ -88,10 +93,7 @@ def create_model_and_solve(self, explainer, theory, n, time_limit): if first: status = solver.Solve() else: - print("try") status = solver.NextSolution() - print("status:", status) - print("Obj: ", solver.Objective().Value()) print([explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5]) if status not in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]: break From 441d63ffd40c37eae9895409b387ed6787fed252 Mon Sep 17 00:00:00 2001 From: audemard Date: Thu, 23 Nov 2023 09:48:40 +0100 Subject: [PATCH 09/12] end of contrastive BT --- pyxai/examples/BT/simple.py | 2 +- pyxai/sources/core/explainer/explainerBT.py | 4 +++ pyxai/sources/core/explainer/explainerRF.py | 3 +- pyxai/sources/core/structure/binaryMapping.py | 17 ++++++++-- pyxai/sources/solvers/CPLEX/ContrastiveBT.py | 32 ++++++++++++++----- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/pyxai/examples/BT/simple.py b/pyxai/examples/BT/simple.py index 83caae2..bf9170f 100644 --- a/pyxai/examples/BT/simple.py +++ b/pyxai/examples/BT/simple.py @@ -5,7 +5,7 @@ # Machine learning part learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) -instance, prediction = learner.get_instances(model=model, n=1, correct=True) +instance, prediction = learner.get_instances(model=model, n=1, correct=False) # Explanation part print(instance) diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index b2806ee..47e57d6 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -355,11 +355,15 @@ def is_tree_specific_reason(self, reason, check_minimal_inclusion=False): def minimal_contrastive_reason(self, *, n=1, time_limit=None): if self._instance is None: raise ValueError("Instance is not set") + if self._boosted_trees.n_classes > 2: + raise NotImplementedError("Minimal contrastive reason is not implemented for the multi class case") + starting_time = -time.process_time() contrastive_bt = ContrastiveBT() c = contrastive_bt.create_model_and_solve(self, None if self._theory == False else self._theory_clauses(), self._excluded_literals, 1, time_limit) time_used = starting_time + time.process_time() self._elapsed_time = time_used if time_limit is None or time_used < time_limit else Explainer.TIMEOUT + self.add_history(self._instance, self.__class__.__name__, self.minimal_contrastive_reason.__name__, c) return c # def check_sufficient(self, reason, n_samples=1000): diff --git a/pyxai/sources/core/explainer/explainerRF.py b/pyxai/sources/core/explainer/explainerRF.py index 6821fb8..8397dd3 100644 --- a/pyxai/sources/core/explainer/explainerRF.py +++ b/pyxai/sources/core/explainer/explainerRF.py @@ -124,7 +124,8 @@ def minimal_contrastive_reason(self, *, n=1, time_limit=None): """ if self._instance is None: raise ValueError("Instance is not set") - + if self._random_forest.n_classes > 2: + raise NotImplementedError("Minimal contrastive reason is not implemented for the multi class case") n = n if type(n) == int else float('inf') first_call = True time_limit = 0 if time_limit is None else time_limit diff --git a/pyxai/sources/core/structure/binaryMapping.py b/pyxai/sources/core/structure/binaryMapping.py index 0739a1b..146a753 100644 --- a/pyxai/sources/core/structure/binaryMapping.py +++ b/pyxai/sources/core/structure/binaryMapping.py @@ -222,8 +222,21 @@ def instance_to_binaries(self, instance, preference_order=None): def get_id_features(self, binary_representation): return tuple(self.map_id_binaries_to_features[abs(lit)][0] for lit in binary_representation) - - + + + # Return a map 'dict_id_features_to_id_binaries': : dict[id_feature] -> [list of id_binaries] + def get_id_binaries(self): + dict_id_features_to_id_binaries = dict() + for key in self.map_features_to_id_binaries.keys(): + id_feature, _, _ = key + id_binary = self.map_features_to_id_binaries[key][0] + if id_feature in dict_id_features_to_id_binaries.keys(): + dict_id_features_to_id_binaries[id_feature].append(id_binary) + else: + dict_id_features_to_id_binaries[id_feature] = [id_binary] + return dict_id_features_to_id_binaries + + def convert_features_to_dict_features(self, features, feature_names): dict_features = dict() dict_features_categorical = dict() diff --git a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py index d0aba94..6bb74a4 100644 --- a/pyxai/sources/solvers/CPLEX/ContrastiveBT.py +++ b/pyxai/sources/solvers/CPLEX/ContrastiveBT.py @@ -13,6 +13,7 @@ def create_model_and_solve(self, explainer, theory, excluded, n, time_limit): leaves = [tree.get_leaves() for tree in forest] bin_len = len(explainer.binary_representation) solver = pywraplp.Solver.CreateSolver("SCIP") + features_to_bin = explainer._boosted_trees.get_id_binaries() if time_limit is not None: solver.SetTimeLimit(time_limit * 1000) # time limit in milisecond @@ -73,17 +74,33 @@ def create_model_and_solve(self, explainer, theory, excluded, n, time_limit): for lit in excluded: constraint = solver.RowConstraint(0, 0) constraint.SetCoefficient(flipped[abs(lit) - 1], 1) - # links between features and flipped - # Objective function - objective = solver.Objective() - for i in range(bin_len): - objective.SetCoefficient(flipped[i], 1) - objective.SetMinimization() - # print(solver.ExportModelAsLpFormat(obfuscated=False)) + if theory is None: # the same encoding for RF : if theory minimal wrt features else wrt bin... + # TODO : let the possibilit for the user to choose + # Objective function + objective = solver.Objective() + for i in range(bin_len): + objective.SetCoefficient(flipped[i], 1) + objective.SetMinimization() + else: + # links between features and flipped + dist_features = [solver.BoolVar(f"fd{i}") for i in range(len(features_to_bin))] + i = 0 + for f, binaries in features_to_bin.items(): + constraint = solver.RowConstraint(-solver.infinity(), 0) + constraint.SetCoefficient(dist_features[i], -1) + for lit in binaries: + constraint.SetCoefficient(flipped[abs(lit -1)], 1 / len(binaries)) + i = i + 1 + # Objective function + objective = solver.Objective() + for d in dist_features: + objective.SetCoefficient(d, 1) + objective.SetMinimization() + # print(solver.ExportModelAsLpFormat(obfuscated=False)) # Solve the problem and extract n solutions results = [] @@ -94,7 +111,6 @@ def create_model_and_solve(self, explainer, theory, excluded, n, time_limit): status = solver.Solve() else: status = solver.NextSolution() - print([explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5]) if status not in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]: break solution = [explainer.binary_representation[i] for i in range(len(flipped)) if flipped[i].solution_value() >= 0.5] From 12086c1b2a7176eec9ee1cfb5fa860d790fe5e83 Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Fri, 24 Nov 2023 09:17:31 +0100 Subject: [PATCH 10/12] changelog --- CHANGELOG.md | 15 +++++++++++++++ pyxai/sources/core/explainer/explainerBT.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cb08916 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +### 1.0.10 + - Contrastive for BT classification (binary classes) + - change function name in explainer (unset_specific_features -> unset_excluded_features) + +### 1.0.0 + - Regression for boosted trees + - Adding thoeries + - Easier import model + - Graphical user interface: displaying, loading, saving explanations + - Data preprocessing + - unit tests +## 0.X + - Initial release \ No newline at end of file diff --git a/pyxai/sources/core/explainer/explainerBT.py b/pyxai/sources/core/explainer/explainerBT.py index 47e57d6..e74d151 100644 --- a/pyxai/sources/core/explainer/explainerBT.py +++ b/pyxai/sources/core/explainer/explainerBT.py @@ -181,7 +181,7 @@ def sufficient_reason(self, *, n=1, seed=0, time_limit=None): """ if self._instance is None: raise ValueError("Instance is not set") - + raise NotImplementedError("In progress") assert n == 1, "To do implement that" if self._boosted_trees.n_classes > 2: raise NotImplementedError From 165beed680641b893851de5c09e15f69a356024a Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Fri, 24 Nov 2023 09:22:15 +0100 Subject: [PATCH 11/12] Update version.txt --- pyxai/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyxai/version.txt b/pyxai/version.txt index 66c4c22..7ee7020 100644 --- a/pyxai/version.txt +++ b/pyxai/version.txt @@ -1 +1 @@ -1.0.9 +1.0.10 From 8506264cb15ec70b73d0c944dc04f620630ea60a Mon Sep 17 00:00:00 2001 From: Gilles Audemard Date: Fri, 24 Nov 2023 09:35:29 +0100 Subject: [PATCH 12/12] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a3596af..3860554 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ classifiers=['Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Education'], packages=find_packages(), # exclude=["problems/g7_todo/"]), package_dir={'pyxai': 'pyxai'}, - install_requires=['lxml', 'numpy', 'wheel', 'pandas', 'termcolor', 'shap', 'wordfreq', 'python-sat[pblib,aiger]', 'xgboost==1.7.3', 'pycsp3', 'matplotlib', 'pyqt6', 'dill', 'lightgbm', 'docplex'], + install_requires=['lxml', 'numpy', 'wheel', 'pandas', 'termcolor', 'shap', 'wordfreq', 'python-sat[pblib,aiger]', 'xgboost==1.7.3', 'pycsp3', 'matplotlib', 'pyqt6', 'dill', 'lightgbm', 'docplex', 'ortools'], include_package_data=True, description='Explaining Machine Learning Classifiers in Python', long_description=open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8').read(),