diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000000000000000000000000000000000..b308fd89d845cbb0d35f06071aa95613e6f32463 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn app:server \ No newline at end of file diff --git a/app.py b/app.py index d4becfdf72a575dc49d33620792892869f08f070..41087dc47928371b55c90ee0bfacd48d69c64936 100644 --- a/app.py +++ b/app.py @@ -10,7 +10,7 @@ from callbacks import register_callbacks from pages.application.application import Application, Model, View from utils import extract_data -app = dash.Dash(external_stylesheets=[dbc.themes.LUX], suppress_callback_exceptions=True, +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX], suppress_callback_exceptions=True, meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1'}]) ################################################################################# @@ -20,15 +20,34 @@ models_data = open('data_retriever.json') data = json.load(models_data)["data"] # For home directory -page_home = dbc.Row([html.H3("Welcome")]) +welcome_message = html.Div(html.Iframe( + src=app.get_asset_url("welcome.html"), + style={"height": "1067px", "width": "100%"}, +)) +page_home = dbc.Row([welcome_message]) + # For course directory -page_course = dbc.Row([]) +course_data_format = html.Div(html.Iframe( + src=app.get_asset_url("course_data_format.html"), + style={"height": "1067px", "width": "100%"}, +)) +course_decision_tree = html.Iframe( + src="assets/course_decision_tree.html", + style={"height": "1067px", "width": "100%"}, +) +main_course = dcc.Tabs(children=[ + dcc.Tab(label='Data format', children=[course_data_format]), + dcc.Tab(label='Course Decision Tree', children=[course_decision_tree])]) +page_course = dbc.Row([main_course]) + # For the application names_models, dict_components, dic_solvers, dic_xtypes = extract_data(data) model_application = Model(names_models, dict_components, dic_solvers, dic_xtypes) view_application = View(model_application) page_application = Application(view_application) +server = app.server + app.layout = html.Div([ dcc.Location(id='url', refresh=False), html.Nav(id='navbar-container', diff --git a/assets/course_data_format.html b/assets/course_data_format.html new file mode 100644 index 0000000000000000000000000000000000000000..59a308599b718ef461a3a08c1d8b296d39541ac0 --- /dev/null +++ b/assets/course_data_format.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>What kinf of model, data, or instance can I upload ?</title> +</head> +<body> + +<h1> Data format requirements</h1> + +<h2> An option to add information on model </h2> + +<h3> Why ? </h3> +<p> There is a switch button, you can use it when you want to attach the csv you trained your model on or a feature mapping. This is useful when the values are categorical. +</p> + +<h3> How ? </h3> + +<p> If you decide to upload the dataset, it should be a .csv. +If you decide to upload the feature mapping, it is a .txt file. +</p> + +<h2> What kind of model can I upload ? </h2> + +<p> You can only import .pkl models.</p> + +<h2> What should the format of the instance be ? </h2> + +<p> You can either upload a .txt file containing hte instance with the format : feature1=value1,feature2=value2,... where feature1, feature2 are the names of the columns. + +But you can also upload a json of your instance.</p> + +<p> Attention !!!! If you never mention the feature names in your model or additional information, the instance should be of format : f1=...,f2=... precisely</p> + +<h1> What are the advanced parameters ? </h1> + +<p> These are specific to the kind of model you selected, it would mainly be how the explanation should be computed and the kind of explanation.</p> + +</body> +</html> \ No newline at end of file diff --git a/assets/course_decision_tree.html b/assets/course_decision_tree.html new file mode 100644 index 0000000000000000000000000000000000000000..b07a373ce003b2127983fa81d3dfb93c5e81771c --- /dev/null +++ b/assets/course_decision_tree.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>What kinf of model, data, or instance can I upload ?</title> +</head> +<body> + +<h1> What library am I able to use on the platform ?</h1> + +<p> Only models from scikit-learn are allowed.</p> + +</body> +</html> \ No newline at end of file diff --git a/assets/header.css b/assets/header.css index 3a882eae1b7fdffd40dd37f87941a5508a4f29d1..9005062397d55f1264835cb8ef77e8544e217a36 100644 --- a/assets/header.css +++ b/assets/header.css @@ -1,3 +1,5 @@ + + /* NAVBAR */ .navbar-dark .navbar-brand { diff --git a/assets/welcome.html b/assets/welcome.html new file mode 100644 index 0000000000000000000000000000000000000000..ff972271b73ccd66ad0fc0f98fc19f7df8f1f1dc --- /dev/null +++ b/assets/welcome.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>What kinf of model, data, or instance can I upload ?</title> +</head> +<body> + +<h1> Welcome </h1> + +</body> +</html> \ No newline at end of file diff --git a/callbacks.py b/callbacks.py index 438d511ac3c1d1fee94043f75e0d4260a86f189b..d832b6eb999ccdd92111e317920562c6af9e2ae8 100644 --- a/callbacks.py +++ b/callbacks.py @@ -28,19 +28,40 @@ def register_callbacks(page_home, page_course, page_application, app): return active_link[0], active_link[1], active_link[2] @app.callback(Output('solver_sat', 'options'), + Output('solver_sat', 'value'), Output('explanation_type', 'options'), + Output('explanation_type', 'value'), Input('ml_model_choice', 'value'), prevent_initial_call=True ) def update_ml_type_options(value_ml_model): model_application = page_application.model model_application.update_ml_model(value_ml_model) - return model_application.solvers, model_application.xtypes + return model_application.solvers, model_application.solvers[0], model_application.xtypes, [ + list(model_application.xtypes.keys())[0]] + + @app.callback(Output('pretrained_model_filename', 'children'), + Input('ml_pretrained_model_choice', 'filename'), + prevent_initial_call=True + ) + def update_model_prertrained_name(pretrained_model_filename): + return pretrained_model_filename + + @app.callback(Output('info_filename', 'children'), + Input('model_info_choice', 'filename'), + prevent_initial_call=True + ) + def update_model_info_filename(model_info_filename): + return model_info_filename + + @app.callback(Output('instance_filename', 'children'), + Input('ml_instance_choice', 'filename'), + prevent_initial_call=True + ) + def update_instance_filename(instance_filename): + return instance_filename @app.callback( - Output('pretrained_model_filename', 'children'), - Output('info_filename', 'children'), - Output('instance_filename', 'children'), Output('graph', 'children'), Output('explanation', 'children'), Input('ml_model_choice', 'value'), @@ -68,7 +89,7 @@ def register_callbacks(page_home, page_course, page_application, app): # Choice of model if ihm_id == 'ml_model_choice': model_application.update_ml_model(value_ml_model) - return None, None, None, None, None + return None, None # Choice of pkl pretrained model elif ihm_id == 'ml_pretrained_model_choice': @@ -78,17 +99,22 @@ def register_callbacks(page_home, page_course, page_application, app): model_application.update_pretrained_model(graph) if not model_application.add_info: model_application.update_pretrained_model_layout() - return pretrained_model_filename, None, None, model_application.component.network, None + return model_application.component.network, None else: - return pretrained_model_filename, None, None, None, None + if model_application.model_info is None: + raise PreventUpdate + model_application.update_pretrained_model_layout_with_info(model_application.model_info, + model_info_filename) + return model_application.component.network, None # Choice of information for the model elif ihm_id == 'model_info_choice': - if model_application.ml_model is None: - raise PreventUpdate model_info = parse_contents_data(model_info, model_info_filename) + model_application.model_info = model_info + if model_application.ml_model is None or model_application.pretrained_model is None: + raise PreventUpdate model_application.update_pretrained_model_layout_with_info(model_info, model_info_filename) - return pretrained_model_filename, model_info_filename, None, model_application.component.network, None + return model_application.component.network, None # Choice of instance to explain elif ihm_id == 'ml_instance_choice': @@ -96,50 +122,42 @@ def register_callbacks(page_home, page_course, page_application, app): raise PreventUpdate instance = parse_contents_instance(instance_contents, instance_filename) model_application.update_instance(instance) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation - # Choice of number of expls + # Choice of number of expls elif ihm_id == 'number_explanations': - if model_application.ml_model is None or model_application.pretrained_model is None or len( - model_application.instance) == 0 or model_application.xtype is None: + if model_application.ml_model is None or model_application.pretrained_model is None or model_application.instance is None or model_application.solver is None or model_application.xtype is None: raise PreventUpdate model_application.update_enum(enum) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation - # Choice of AxP or CxP + # Choice of AxP or CxP elif ihm_id == 'explanation_type': - if model_application.ml_model is None or model_application.pretrained_model is None or len( - model_application.instance) == 0 or model_application.enum <= 0: + if model_application.ml_model is None or model_application.pretrained_model is None or model_application.instance is None or model_application.enum <= 0 or model_application.solver is None: raise PreventUpdate model_application.update_xtype(xtype) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation - # Choice of solver + # Choice of solver elif ihm_id == 'solver_sat': - if model_application.ml_model is None or model_application.pretrained_model is None or len( - model_application.instance) == 0 or model_application.enum <= 0 or len( - model_application.xtype) == 0: + if model_application.ml_model is None or model_application.pretrained_model is None or model_application.instance is None or model_application.enum <= 0 or model_application.xtype is None: raise PreventUpdate model_application.update_solver(solver) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation - # Choice of AxP to draw + # Choice of AxP to draw elif ihm_id == 'expl_choice': - if model_application.ml_model is None or model_application.pretrained_model is None or len( - model_application.instance) == 0 or model_application.enum <= 0 or len( - model_application.xtype) == 0: + if model_application.ml_model is None or model_application.pretrained_model is None or model_application.instance is None or model_application.enum <= 0 or model_application.xtype is None: raise PreventUpdate model_application.update_expl(expl_choice) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation - # Choice of CxP to draw + # Choice of CxP to draw elif ihm_id == 'cont_expl_choice': - if model_application.ml_model is None or model_application.pretrained_model is None or len( - model_application.instance) == 0 or model_application.enum <= 0 or len( - model_application.xtype) == 0: + if model_application.ml_model is None or model_application.pretrained_model is None or model_application.instance is None or model_application.enum <= 0 or model_application.xtype is None: raise PreventUpdate model_application.update_cont_expl(cont_expl_choice) - return pretrained_model_filename, model_info_filename, instance_filename, model_application.component.network, model_application.component.explanation + return model_application.component.network, model_application.component.explanation @app.callback( Output('explanation', 'hidden'), diff --git a/pages/application/DecisionTree/DecisionTreeComponent.py b/pages/application/DecisionTree/DecisionTreeComponent.py index c177652e7f2be781a7b1bdba4bccb94bec80e34d..edb89a4d4c0a863ee9cfe80c6a35b28745038eca 100644 --- a/pages/application/DecisionTree/DecisionTreeComponent.py +++ b/pages/application/DecisionTree/DecisionTreeComponent.py @@ -119,6 +119,7 @@ class DecisionTreeComponent(): instance_translated = self.translate_instance(instance) self.explanation = [] list_explanations_path = [] + list_contrastive_explanations_path = [] explanation = self.dt.explain(instance_translated, enum=enum, xtype=xtype, solver=solver) dot_source = visualize_instance(self.dt, instance_translated) diff --git a/pages/application/DecisionTree/utils/dtree.py b/pages/application/DecisionTree/utils/dtree.py index c2775dfac8d7adbaca3ff30e8033b92ea2570c26..b67d26db0165bdf0dba4ded598ad60d394e79373 100644 --- a/pages/application/DecisionTree/utils/dtree.py +++ b/pages/application/DecisionTree/utils/dtree.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- ## ## dtree.py ## @@ -9,7 +9,7 @@ ## # -#============================================================================== +# ============================================================================== from __future__ import print_function import collections from functools import reduce @@ -25,6 +25,7 @@ except ImportError: # for Python3 from sklearn.tree import _tree import numpy as np + class Node(): """ Node class. @@ -38,8 +39,9 @@ class Node(): self.feat = feat self.vals = vals + # -#============================================================================== +# ============================================================================== class DecisionTree(): """ Simple decision tree class. @@ -62,14 +64,14 @@ class DecisionTree(): self.feids = {} self.fdoms = {} self.fvmap = {} - self.feature_names = {f'f{i}' : feature_names[i] for i, f in enumerate(feature_names)} + self.feature_names = {f'f{i}': feature_names[i] for i, f in enumerate(feature_names)} # OHE mapping OHEMap = collections.namedtuple('OHEMap', ['dir', 'opp']) self.ohmap = OHEMap(dir={}, opp={}) if from_dt: - self.from_dt(from_dt) + self.from_dt(from_dt) if mapfile: self.parse_mapping(mapfile) @@ -103,7 +105,7 @@ class DecisionTree(): self.terms = {} for i in range(self.nof_terms): nd, _, t = lines[i + 4].strip().split() - self.terms[int(nd)] = t #int(t) + self.terms[int(nd)] = t # int(t) # finally, reading the nodes self.nodes = collections.defaultdict(lambda: Node(feat='', vals={})) @@ -132,7 +134,7 @@ class DecisionTree(): # simplifying the features and their domains self.feats = sorted(self.feats) - #self.feids = {f: i for i, f in enumerate(self.feats)} + # self.feids = {f: i for i, f in enumerate(self.feats)} self.fdoms = {f: sorted(self.fdoms[f]) for f in self.fdoms} # here we assume all features are present in the tree @@ -175,10 +177,10 @@ class DecisionTree(): for line in lines[1:]: feat, val, real = line.split() self.fvmap[tuple([feat, int(val)])] = '{0}{1}'.format(self.feature_names[feat], real) - #if feat not in self.feids: + # if feat not in self.feids: # self.feids[feat] = len(self.feids) - #assert len(self.feids) == self.nof_feats + # assert len(self.feids) == self.nof_feats def convert_to_multiedges(self): """ @@ -324,34 +326,38 @@ class DecisionTree(): # returning the set of sets with no duplicates return list(dict.fromkeys(sets)) - def explain(self, inst, enum=1, pathlits=False, xtype = ["AXp"], solver='g3', htype='sorted'): + def explain(self, inst, enum=1, pathlits=False, xtype=["AXp"], solver='g3', htype='sorted'): """ Compute a given number of explanations. """ - #contaiins all the elements for explanation + # contaiins all the elements for explanation explanation_dic = {} self.feids = {f'f{i}': i for i, f in enumerate(inst)} - inst = [(f'f{i}', int(inst[i][1])) for i,f in enumerate(inst)] + inst = [(f'f{i}', int(inst[i][1])) for i, f in enumerate(inst)] path, term, depth = self.execute(inst, pathlits) - #decision path - decision_path_str = 'IF {0} THEN class={1}'.format(' AND '.join([self.fvmap[inst[self.feids[self.nodes[n].feat]]] for n in path]), term) + # decision path + decision_path_str = 'IF {0} THEN class={1}'.format( + ' AND '.join([self.fvmap[inst[self.feids[self.nodes[n].feat]]] for n in path]), term) explanation_dic["Decision path of instance : "] = decision_path_str - explanation_dic["Decision path length : "] = 'Path length is :'+ str(depth) + explanation_dic["Decision path length : "] = 'Path length is :' + str(depth) if self.ohmap.dir: f2v = {fv[0]: fv[1] for fv in inst} # updating fvmap for printing ohe features for fo, fis in self.ohmap.dir.items(): - self.fvmap[tuple([fo, None])] = '(' + ' AND '.join([self.fvmap[tuple([fi, f2v[fi]])] for fi in fis]) + ')' + self.fvmap[tuple([fo, None])] = '(' + ' AND '.join( + [self.fvmap[tuple([fi, f2v[fi]])] for fi in fis]) + ')' # computing the sets to hit to_hit = self.prepare_sets(inst, term) - for type in xtype : + explanation_dic["List of path explanation(s)"] = [] + explanation_dic["List of path contrastive explanation(s)"] = [] + for type in xtype: if type == "AXp": explanation_dic.update(self.enumerate_abductive(to_hit, enum, solver, htype, term)) - else : + else: explanation_dic.update(self.enumerate_contrastive(to_hit, term)) return explanation_dic @@ -367,7 +373,8 @@ class DecisionTree(): expls = [] for i, expl in enumerate(hitman.enumerate(), 1): list_expls.append([self.fvmap[p] for p in sorted(expl, key=lambda p: p[0])]) - list_expls_str.append('Explanation: IF {0} THEN class={1}'.format(' AND '.join([self.fvmap[p] for p in sorted(expl, key=lambda p: p[0])]), term)) + list_expls_str.append('Explanation: IF {0} THEN class={1}'.format( + ' AND '.join([self.fvmap[p] for p in sorted(expl, key=lambda p: p[0])]), term)) expls.append(expl) if i == enum: @@ -375,9 +382,10 @@ class DecisionTree(): explanation["List of path explanation(s)"] = list_expls explanation["List of abductive explanation(s)"] = list_expls_str explanation["Number of abductive explanation(s) : "] = str(i) - explanation["Minimal abductive explanation : "] = str( min([len(e) for e in expls])) - explanation["Maximal abductive explanation : "] = str( max([len(e) for e in expls])) - explanation["Average abductive explanation : "] = '{0:.2f}'.format(sum([len(e) for e in expls]) / len(expls)) + explanation["Minimal abductive explanation : "] = str(min([len(e) for e in expls])) + explanation["Maximal abductive explanation : "] = str(max([len(e) for e in expls])) + explanation["Average abductive explanation : "] = '{0:.2f}'.format( + sum([len(e) for e in expls]) / len(expls)) return explanation @@ -385,6 +393,7 @@ class DecisionTree(): """ Enumerate contrastive explanations. """ + def process_set(done, target): for s in done: if s <= target: @@ -401,14 +410,15 @@ class DecisionTree(): list_expls_str = [] explanation = {} for expl in expls: - list_contrastive_expls.append([self.fvmap[(p[0],1-p[1])] for p in sorted(expl, key=lambda p: p[0])]) - list_expls_str.append('Contrastive: IF {0} THEN class!={1}'.format(' OR '.join(['!{0}'.format(self.fvmap[p]) for p in sorted(expl, key=lambda p: p[0])]), term)) + list_contrastive_expls.append([self.fvmap[(p[0], 1 - p[1])] for p in sorted(expl, key=lambda p: p[0])]) + list_expls_str.append('Contrastive: IF {0} THEN class!={1}'.format( + ' OR '.join(['!{0}'.format(self.fvmap[p]) for p in sorted(expl, key=lambda p: p[0])]), term)) explanation["List of path contrastive explanation(s)"] = list_contrastive_expls explanation["List of contrastive explanation(s)"] = list_expls_str - explanation["Number of contrastive explanation(s) : "]=str(len(expls)) - explanation["Minimal contrastive explanation : "]= str( min([len(e) for e in expls])) - explanation["Maximal contrastive explanation : "]= str( max([len(e) for e in expls])) - explanation["Average contrastive explanation : "]='{0:.2f}'.format(sum([len(e) for e in expls]) / len(expls)) + explanation["Number of contrastive explanation(s) : "] = str(len(expls)) + explanation["Minimal contrastive explanation : "] = str(min([len(e) for e in expls])) + explanation["Maximal contrastive explanation : "] = str(max([len(e) for e in expls])) + explanation["Average contrastive explanation : "] = '{0:.2f}'.format(sum([len(e) for e in expls]) / len(expls)) return explanation diff --git a/pages/application/DecisionTree/utils/dtviz.py b/pages/application/DecisionTree/utils/dtviz.py index cf71d2bda925a3cfedcba5ddb641b80069bf5e92..02aa847e0949e6b5d49ad06c3567f3b0c67d1766 100755 --- a/pages/application/DecisionTree/utils/dtviz.py +++ b/pages/application/DecisionTree/utils/dtviz.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- ## ## dtviz.py ## @@ -9,255 +9,234 @@ ## # -#============================================================================== +# ============================================================================== import getopt -import pygraphviz +import pydot import ast import re + + # -#============================================================================== -def create_legend(g): - legend = g.subgraphs()[-1] - legend.graph_attr.update(size="2,2") - legend.add_node("a", style = "invis") - legend.add_node("b", style = "invis") - legend.add_node("c", style = "invis") - legend.add_node("d", style = "invis") - legend.add_node("e", style = "invis") - legend.add_node("f", style = "invis") - - legend.add_edge("a","b") - edge = legend.get_edge("a","b") - edge.attr["label"] = "instance" - edge.attr["style"] = "dashed" - - legend.add_edge("c","d") - edge = legend.get_edge("c","d") - edge.attr["label"] = "instance with explanation" - edge.attr["color"] = "blue" - edge.attr["style"] = "dashed" - - legend.add_edge("e","f") - edge = legend.get_edge("e","f") - edge.attr["label"] = "contrastive explanation" - edge.attr["color"] = "red" +# ============================================================================== +def create_legend(G): + legend = pydot.Cluster('legend', rankdir="TB") + + # non-terminal nodes + for n in ["a", "b", "c", "d", "e", "f"]: + node = pydot.Node(n, label=n) + legend.add_node(node) + + edge = pydot.Edge("a", "b") + edge.obj_dict['attributes']["label"] = "instance" + edge.obj_dict['attributes']["style"] = "dashed" + legend.add_edge(edge) + + edge = pydot.Edge("e", "f") + edge.obj_dict['attributes']["label"] = "contrastive explanation" + edge.obj_dict['attributes']["color"] = "red" + edge.obj_dict['attributes']["style"] = "dashed" + legend.add_edge(edge) + + edge = pydot.Edge("c", "d") + edge.obj_dict['attributes']["label"] = "instance with explanation" + edge.obj_dict['attributes']["color"] = "blue" + edge.obj_dict['attributes']["style"] = "dashed" + legend.add_edge(edge) + + G.add_subgraph(legend) + + # -#============================================================================== +# ============================================================================== def visualize(dt): """ Visualize a DT with graphviz. """ - g = pygraphviz.AGraph(directed=True, strict=True) - g.edge_attr['dir'] = 'forward' - g.graph_attr['rankdir'] = 'TB' + G = pydot.Dot('tree_total', graph_type='graph') + + g = pydot.Cluster('tree', graph_type='graph') # non-terminal nodes for n in dt.nodes: - g.add_node(n, label=dt.feature_names[dt.nodes[n].feat]) - node = g.get_node(n) - node.attr['shape'] = 'circle' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.feature_names[dt.nodes[n].feat], shape="circle") + g.add_node(node) # terminal nodes for n in dt.terms: - g.add_node(n, label=dt.terms[n]) - node = g.get_node(n) - node.attr['shape'] = 'square' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.terms[n], shape="square") + g.add_node(node) # transitions for n1 in dt.nodes: for v in dt.nodes[n1].vals: n2 = dt.nodes[n1].vals[v] - g.add_edge(n1, n2) - edge = g.get_edge(n1, n2) + edge = pydot.Edge(n1, n2) if len(v) == 1: - edge.attr['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] + edge.obj_dict['attributes']['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] else: - edge.attr['label'] = '{0}'.format('\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) - edge.attr['fontsize'] = 10 - edge.attr['arrowsize'] = 0.8 + edge.obj_dict['attributes']['label'] = '{0}'.format( + '\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) + edge.obj_dict['attributes']['fontsize'] = 10 + edge.obj_dict['attributes']['arrowsize'] = 0.8 + + g.add_edge(edge) + + G.add_subgraph(g) + + return G.to_string() - # saving file - g.layout(prog='dot') - return(g.to_string()) # -#============================================================================== +# ============================================================================== def visualize_instance(dt, instance): """ Visualize a DT with graphviz and plot the running instance. """ - #path that follows the instance - colored in blue + # path that follows the instance - colored in blue path, term, depth = dt.execute(instance) edges_instance = [] - for i in range (len(path)-1) : - edges_instance.append((path[i], path[i+1])) - edges_instance.append((path[-1],"term:"+term)) + for i in range(len(path) - 1): + edges_instance.append((path[i], path[i + 1])) + edges_instance.append((path[-1], "term:" + term)) + + G = pydot.Dot('tree_total', graph_type='graph') - g = pygraphviz.AGraph(directed=True, strict=True) - g.edge_attr['dir'] = 'forward' - g.graph_attr['rankdir'] = 'TB' + g = pydot.Cluster('tree', graph_type='graph') # non-terminal nodes for n in dt.nodes: - g.add_node(n, label=dt.feature_names[dt.nodes[n].feat]) - node = g.get_node(n) - node.attr['shape'] = 'circle' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.feature_names[dt.nodes[n].feat], shape="circle") + g.add_node(node) # terminal nodes for n in dt.terms: - g.add_node(n, label=dt.terms[n]) - node = g.get_node(n) - node.attr['shape'] = 'square' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.terms[n], shape="square") + g.add_node(node) # transitions for n1 in dt.nodes: for v in dt.nodes[n1].vals: n2 = dt.nodes[n1].vals[v] - n2_type = g.get_node(n2).attr['shape'] - - g.add_edge(n1, n2) - edge = g.get_edge(n1, n2) + n2_type = g.get_node(str(n2))[0].obj_dict['attributes']['shape'] + edge = pydot.Edge(n1, n2) if len(v) == 1: - edge.attr['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] + edge.obj_dict['attributes']['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] else: - edge.attr['label'] = '{0}'.format('\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) - - #instance path in dashed - if ((n1,n2) in edges_instance) or (n2_type=='square' and (n1, "term:"+ dt.terms[n2]) in edges_instance): - edge.attr['style'] = 'dashed' + edge.obj_dict['attributes']['label'] = '{0}'.format( + '\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) + edge.obj_dict['attributes']['fontsize'] = 10 + edge.obj_dict['attributes']['arrowsize'] = 0.8 - edge.attr['fontsize'] = 10 - edge.attr['arrowsize'] = 0.8 + # instance path in dashed + if ((n1, n2) in edges_instance) or (n2_type == 'square' and (n1, "term:" + dt.terms[n2]) in edges_instance): + edge.obj_dict['attributes']['style'] = 'dashed' - g.add_subgraph(name='legend') - create_legend(g) + g.add_edge(edge) - # saving file - g.layout(prog='dot') - return(g.to_string()) + create_legend(G) + G.add_subgraph(g) -#============================================================================== + return G.to_string() + + +# ============================================================================== def visualize_expl(dt, instance, expl): """ Visualize a DT with graphviz and plot the running instance. """ - #path that follows the instance - colored in blue + # path that follows the instance - colored in blue path, term, depth = dt.execute(instance) edges_instance = [] - for i in range (len(path)-1) : - edges_instance.append((path[i], path[i+1])) - edges_instance.append((path[-1],"term:"+term)) - - g = pygraphviz.AGraph(directed=True, strict=True) - g.edge_attr['dir'] = 'forward' + for i in range(len(path) - 1): + edges_instance.append((path[i], path[i + 1])) + edges_instance.append((path[-1], "term:" + term)) - g.graph_attr['rankdir'] = 'TB' + g = pydot.Dot('my_graph', graph_type='graph') # non-terminal nodes for n in dt.nodes: - g.add_node(n, label=dt.feature_names[dt.nodes[n].feat]) - node = g.get_node(n) - node.attr['shape'] = 'circle' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.feature_names[dt.nodes[n].feat], shape="circle") + g.add_node(node) # terminal nodes for n in dt.terms: - g.add_node(n, label=dt.terms[n]) - node = g.get_node(n) - node.attr['shape'] = 'square' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.terms[n], shape="square") + g.add_node(node) # transitions for n1 in dt.nodes: for v in dt.nodes[n1].vals: n2 = dt.nodes[n1].vals[v] - n2_type = g.get_node(n2).attr['shape'] - g.add_edge(n1, n2) - edge = g.get_edge(n1, n2) + n2_type = g.get_node(str(n2))[0].obj_dict['attributes']['shape'] + edge = pydot.Edge(n1, n2) if len(v) == 1: - edge.attr['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] + edge.obj_dict['attributes']['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] else: - edge.attr['label'] = '{0}'.format('\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) - - #instance path in dashed - if ((n1,n2) in edges_instance) or (n2_type=='square' and (n1, "term:"+ dt.terms[n2]) in edges_instance): - edge.attr['style'] = 'dashed' - for label in edge.attr['label'].split('\n'): + edge.obj_dict['attributes']['label'] = '{0}'.format( + '\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) + edge.obj_dict['attributes']['fontsize'] = 10 + edge.obj_dict['attributes']['arrowsize'] = 0.8 + + # instance path in dashed + if ((n1, n2) in edges_instance) or (n2_type == 'square' and (n1, "term:" + dt.terms[n2]) in edges_instance): + edge.obj_dict['attributes']['style'] = 'dashed' + + for label in edge.obj_dict['attributes']['label'].split('\n'): if label in expl: - edge.attr['color'] = 'blue' + edge.obj_dict['attributes']['color'] = 'blue' - edge.attr['fontsize'] = 10 - edge.attr['arrowsize'] = 0.8 + g.add_edge(edge) - g.add_subgraph(name='legend') - create_legend(g) + return g.to_string() - # saving file - g.layout(prog='dot') - return(g.to_string()) -#============================================================================== +# ============================================================================== def visualize_contrastive_expl(dt, instance, cont_expl): """ Visualize a DT with graphviz and plot the running instance. """ - #path that follows the instance - colored in blue + # path that follows the instance - colored in blue path, term, depth = dt.execute(instance) edges_instance = [] - for i in range (len(path)-1) : - edges_instance.append((path[i], path[i+1])) - edges_instance.append((path[-1],"term:"+term)) - - g = pygraphviz.AGraph(directed=True, strict=True) - g.edge_attr['dir'] = 'forward' + for i in range(len(path) - 1): + edges_instance.append((path[i], path[i + 1])) + edges_instance.append((path[-1], "term:" + term)) - g.graph_attr['rankdir'] = 'TB' + g = pydot.Dot('my_graph', graph_type='graph') # non-terminal nodes for n in dt.nodes: - g.add_node(n, label=dt.feature_names[dt.nodes[n].feat]) - node = g.get_node(n) - node.attr['shape'] = 'circle' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.feature_names[dt.nodes[n].feat], shape="circle") + g.add_node(node) # terminal nodes for n in dt.terms: - g.add_node(n, label=dt.terms[n]) - node = g.get_node(n) - node.attr['shape'] = 'square' - node.attr['fontsize'] = 13 + node = pydot.Node(n, label=dt.terms[n], shape="square") + g.add_node(node) # transitions for n1 in dt.nodes: for v in dt.nodes[n1].vals: n2 = dt.nodes[n1].vals[v] - n2_type = g.get_node(n2).attr['shape'] - g.add_edge(n1, n2) - edge = g.get_edge(n1, n2) + n2_type = g.get_node(str(n2))[0].obj_dict['attributes']['shape'] + edge = pydot.Edge(n1, n2) if len(v) == 1: - edge.attr['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] + edge.obj_dict['attributes']['label'] = dt.fvmap[tuple([dt.nodes[n1].feat, tuple(v)[0]])] else: - edge.attr['label'] = '{0}'.format('\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) - - #instance path in dashed - if ((n1,n2) in edges_instance) or (n2_type=='square' and (n1, "term:"+ dt.terms[n2]) in edges_instance): - edge.attr['style'] = 'dashed' - - for label in edge.attr['label'].split('\n'): - if label in cont_expl: - edge.attr['color'] = 'red' + edge.obj_dict['attributes']['label'] = '{0}'.format( + '\n'.join([dt.fvmap[tuple([dt.nodes[n1].feat, val])] for val in tuple(v)])) + edge.obj_dict['attributes']['fontsize'] = 10 + edge.obj_dict['attributes']['arrowsize'] = 0.8 + + # instance path in dashed + if ((n1, n2) in edges_instance) or (n2_type == 'square' and (n1, "term:" + dt.terms[n2]) in edges_instance): + edge.obj_dict['attributes']['style'] = 'dashed' - edge.attr['fontsize'] = 10 - edge.attr['arrowsize'] = 0.8 + for label in edge.obj_dict['attributes']['label'].split('\n'): + if label in cont_expl: + edge.obj_dict['attributes']['color'] = 'red' - g.add_subgraph(name='legend') - create_legend(g) + g.add_edge(edge) - # saving file - g.layout(prog='dot') - return(g.to_string()) + return g.to_string() diff --git a/pages/application/application.py b/pages/application/application.py index 65475b9e6b50c7213810ddf1e10a0dae6e9ed148..6d3303091ec5785980d9b9fc424137248e20b274 100644 --- a/pages/application/application.py +++ b/pages/application/application.py @@ -6,16 +6,14 @@ from pages.application.DecisionTree.DecisionTreeComponent import DecisionTreeCom from pages.application.NaiveBayes.NaiveBayesComponent import NaiveBayesComponent from pages.application.RandomForest.RandomForestComponent import RandomForestComponent -import subprocess - -class Application(): +class Application: def __init__(self, view): self.view = view self.model = view.model -class Model(): +class Model: def __init__(self, names_models, dict_components, dic_solvers, dic_xtypes): self.dict_components = dict_components @@ -23,38 +21,40 @@ class Model(): self.dic_xtypes = dic_xtypes self.ml_models = names_models - self.ml_model = '' + self.ml_model = None - self.pretrained_model = '' + self.pretrained_model = None self.add_info = False - self.model_info = '' + self.model_info = None self.enum = 1 self.xtypes = [] - self.xtype = "" + self.xtype = None self.solvers = [] - self.solver = "" + self.solver = None - self.instance = '' + self.instance = None self.list_expls = [] self.list_cont_expls = [] - self.expl = '' - self.cont_expl = '' + self.expl = None + self.cont_expl = None - self.component_class = '' - self.component = '' + self.component_class = None + self.component = None def update_ml_model(self, ml_model_update): self.ml_model = ml_model_update self.component_class = self.dict_components[self.ml_model] self.component_class = globals()[self.component_class] self.solvers = self.dic_solvers[self.ml_model] + self.solver = self.solvers[0] self.xtypes = self.dic_xtypes[self.ml_model] + self.xtype = [list(self.xtypes.keys())[0]] def update_pretrained_model(self, pretrained_model_update): self.pretrained_model = pretrained_model_update @@ -99,7 +99,7 @@ class Model(): self.component.draw_contrastive_explanation(self.instance, cont_expl) -class View(): +class View: def __init__(self, model): self.model = model @@ -128,7 +128,7 @@ class View(): self.add_model_info_choice = html.Div([ html.Hr(), - html.Label("Do you wish to upload more info for your model ? : "), + html.Label("Upload more data model (only for model with categorical variables) ?"), html.Br(), daq.BooleanSwitch(id='add_info_model_choice', on=False, color="#000000", )]) @@ -192,9 +192,9 @@ class View(): self.sidebar = dcc.Tabs(children=[ dcc.Tab(label='Basic Parameters', children=[ self.ml_menu_models, - self.pretrained_model_upload, self.add_model_info_choice, self.model_info, + self.pretrained_model_upload, self.instance_upload], className="sidebar"), dcc.Tab(label='Advanced Parameters', children=[ html.Br(), diff --git a/requirements.txt b/requirements.txt index 8f12a33646aa22ec091e89844763ad78408bc265..073157043e129bfb62d0fe4f6405d7b5de8db5a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,27 @@ -# Core -gunicorn>=19.8.1 -dash>=1.0.0 - -# Additional colorlover>=0.2.1 -numpy>=1.16.2 -pandas>=0.24.2 -scikit-learn>=0.20.3 scipy>=1.2.1 -dash_bootstrap_components -dash_interactive_graphviz python-sat[pblib,aiger] -pygraphviz +matplotlib==3.5.1 +xgboost==1.6.0 +lime==0.2.0.1 +shap==0.40.0 +anchor-exp==0.0.2.0 +pysmt==0.9.0 anytree==2.8.0 +dash==2.1.0 +dash_bootstrap_components==1.0.3 dash_daq==0.5.0 -matplotlib -six -xgboost -lime -shap -anchor-exp -pysmt -anytree +dash_interactive_graphviz==0.3.0 +Jinja2==3.0.3 +joblib==1.1.0 +json2html==1.3.0 +numpy==1.21 +pandas==1.4.1 +pysat==3.0.1 +pytest==7.0.1 +python_sat==0.1.7.dev15 +scikit_learn==1.0.2 +six==1.16.0 +gunicorn==20.1.0 +Werkzeug==2.0.1 +pydot==1.4.2 \ No newline at end of file diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000000000000000000000000000000000..f96c0fa0f8c6594427eed120daea70dbd970de18 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.10 \ No newline at end of file diff --git a/utils.py b/utils.py index 9fd8a5498e8323b33ae9d840ddb75025e86d226a..87f4fa3915fff6f3e7fc443f85f0d483cf0726b2 100644 --- a/utils.py +++ b/utils.py @@ -8,20 +8,19 @@ from dash import html from pages.application.RandomForest.utils import xrf from pages.application.RandomForest.utils.xrf import * + sys.modules['xrf'] = xrf from pages.application.RandomForest.utils import options from pages.application.RandomForest.utils.options import * + sys.modules['options'] = options -def parse_contents_graph(contents, filename): +def parse_contents_graph(contents, filename): content_type, content_string = contents.split(',') decoded = base64.b64decode(content_string) try: if 'mod.pkl' in filename: - print("in") - print(io.BytesIO(decoded)) - print(pickle.load(io.BytesIO(decoded))) data = pickle.load(io.BytesIO(decoded)) elif '.pkl' in filename: data = joblib.load(io.BytesIO(decoded))