xwhy 0.0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xwhy-0.0.0.1/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: xwhy
3
+ Version: 0.0.0.1
4
+ Summary: Explain Why (XWhy) with Statistical Model-agnostic Interpretability with Local Explanations (SMILE)
5
+ Home-page: https://github.com/Dependable-Intelligent-Systems-Lab/xwhy
6
+ Author: Mojgan Hashemian, Koorosh Aslansefat (corresponding), Mohammad Naveed Akram, Ioannis Sorokos, Martin Walker, Yiannis Papadopoulos
7
+ Author-email: koo.ec2008@gmail.com
8
+ License: BSD
9
+ Description: UNKNOWN
10
+ Platform: UNKNOWN
11
+ Requires-Python: >=3.5
12
+ Provides-Extra: dev
xwhy-0.0.0.1/README.md ADDED
@@ -0,0 +1,63 @@
1
+ <p align="left"> </p>
2
+
3
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
4
+ <a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - \Python Style Guide"></a>
5
+
6
+ # X-Why
7
+ XWhy: eXplain Why with <b>SMILE</b> -- <b>S</b>tatistical <b>M</b>odel-agnostic <b>I</b>nterpretability with <b>L</b>ocal <b>E</b>xplanations
8
+
9
+ <p align="center">
10
+ <img src="https://github.com/koo-ec/xwhy/blob/main/docs/graphics/XWhy_Logo_v1.png" alt="XWhy, SMILE, Explainability, Interpretability, XAI, machine learning explainability, responsible ai"> </p>
11
+
12
+ ## Abstract
13
+ <p align="justify">Machine learning is currently undergoing an explosion in capability, popularity, and sophistication. However, one of the major barriers to widespread acceptance of machine learning (ML) is trustworthiness: most ML models operate as black boxes, their inner workings opaque and mysterious, and it can be difficult to trust their conclusions without understanding how those conclusions are reached. Explainability is therefore a key aspect of improving trustworthiness: the ability to better understand, interpret, and anticipate the behaviour of ML models. To this end, we propose a SMILE, a new method that builds on previous approaches by making use of statistical distance measures to improve explainability while remaining applicable to a wide range of input data domains.</p>
14
+
15
+ <!--
16
+ ## Installation
17
+ ```
18
+ pip install xwhy
19
+ ```
20
+
21
+ ## Simple Example
22
+ ```
23
+ import xwhy
24
+ import xgboost
25
+
26
+ # train an XGBoost model
27
+ X, y = xwhy.datasets.boston()
28
+ model = xgboost.XGBRegressor().fit(X, y)
29
+
30
+ # explain the model's predictions using xwhy
31
+ # (same syntax works for LightGBM, CatBoost, scikit-learn, transformers, Spark, etc.)
32
+ explainer = xwhy.Explainer(model)
33
+ xwhy_values = explainer(X)
34
+
35
+ # visualize the first prediction's explanation
36
+ xwhy.plots.waterfall(xwhy_values[0])
37
+
38
+ ```
39
+ -->
40
+
41
+
42
+
43
+ ## Citations
44
+ It would be appreciated a citation to our paper as follows if you use X-Why for your research:
45
+ ```
46
+ @article{Aslansefat2021Xwhy,
47
+ author = {{Aslansefat}, Koorosh and {Hashemian}, Mojgan and {Martin}, Walker, {Akram} Mohammed Naveed, {Sorokos} Ioannis and {Papadopoulos}, Yiannis},
48
+ title = "{SMILE: Statistical Model-agnostic Interpretability with Local Explanations}",
49
+ journal = {arXiv e-prints},
50
+ year = {2021},
51
+ url = {https://arxiv.org/abs/...},
52
+ eprint = {},
53
+ }
54
+ ```
55
+
56
+ ## Acknowledgment
57
+ <p align="justify">This project is supported by the <a href = "https://www.sesame-project.org"><b>Secure and Safe Multi-Robot Systems (SESAME)</b></a> H2020 Project under Grant Agreement 101017258.</p>
58
+
59
+ ## Awards
60
+ <a href = "https://www.turing.ac.uk/post-doctoral-enrichment-awards-pdea">Post-Doctoral Enrichment Award from the Alan Turing Institute</a>
61
+
62
+ ## Contribution
63
+ If you are interested in contributing to this project, please check the [contribution guidelines](https://github.com/koo-ec/xwhy/blob/main/docs/contribute/contributing.md).
xwhy-0.0.0.1/setup.cfg ADDED
@@ -0,0 +1,7 @@
1
+ [flake8]
2
+ max-line-length = 100
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+
xwhy-0.0.0.1/setup.py ADDED
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(name='xwhy',
4
+ version='0.0.0.1',
5
+ description='Explain Why (XWhy) with Statistical Model-agnostic Interpretability with Local Explanations (SMILE)',
6
+ url='https://github.com/Dependable-Intelligent-Systems-Lab/xwhy',
7
+ author='Mojgan Hashemian, Koorosh Aslansefat (corresponding), Mohammad Naveed Akram, Ioannis Sorokos, Martin Walker, Yiannis Papadopoulos',
8
+ author_email='koo.ec2008@gmail.com',
9
+ license='BSD',
10
+ packages=find_packages(exclude=['js', 'node_modules', 'tests']),
11
+ python_requires='>=3.5',
12
+ install_requires=[
13
+ 'matplotlib',
14
+ 'numpy',
15
+ 'scipy',
16
+ 'tqdm >= 4.29.1',
17
+ 'scikit-learn>=0.18',
18
+ 'scikit-image>=0.12',
19
+ 'pyDOE2==1.3.0',
20
+ 'twine==1.13.0'
21
+ ],
22
+ extras_require={
23
+ 'dev': ['pytest', 'flake8'],
24
+ },
25
+ include_package_data=True,
26
+ zip_safe=False)
@@ -0,0 +1,8 @@
1
+ import warnings
2
+ import sys
3
+
4
+ __version__ = '0.41.0'
5
+
6
+ # check python version
7
+ if (sys.version_info < (3, 0)):
8
+ warnings.warn("As of version 0.29.0 shap only supports Python 3 (not 2)!")
@@ -0,0 +1,267 @@
1
+ # This file is a modified copy of SHAP package.py with some modification
2
+ # source: https://github.com/slundberg/shap/blob/master/shap/datasets.py
3
+
4
+ import pandas as pd
5
+ import numpy as np
6
+ import sklearn.datasets
7
+ import os
8
+
9
+ try:
10
+ from urllib.request import urlretrieve
11
+ except ImportError:
12
+ from urllib import urlretrieve
13
+
14
+ github_data_url = "https://github.com/koo-ec/smile/tree/main/data"
15
+
16
+ def imagenet50(display=False, resolution=224):
17
+ """ This is a set of 50 images representative of ImageNet images.
18
+ This dataset was collected by randomly finding a working ImageNet link and then pasting the
19
+ original ImageNet image into Google image search restricted to images licensed for reuse. A
20
+ similar image (now with rights to reuse) was downloaded as a rough replacment for the original
21
+ ImageNet image. The point is to have a random sample of ImageNet for use as a background
22
+ distribution for explaining models trained on ImageNet data.
23
+ Note that because the images are only rough replacements the labels might no longer be correct.
24
+ """
25
+
26
+ prefix = github_data_url + "imagenet50_"
27
+ X = np.load(cache(prefix + "%sx%s.npy" % (resolution, resolution))).astype(np.float32)
28
+ y = np.loadtxt(cache(prefix + "labels.csv"))
29
+ return X, y
30
+
31
+ def boston(display=False):
32
+ """ Return the boston housing price prediction data, and here is the list of features:
33
+ CRIM: Per capita crime rate by town
34
+ ZN: Proportion of residential land zoned for lots over 25,000 sq. ft
35
+ INDUS: Proportion of non-retail business acres per town
36
+ CHAS: Charles River dummy variable (= 1 if tract bounds river; 0 otherwise)
37
+ NOX: Nitric oxide concentration (parts per 10 million) RM: Average number of rooms per dwelling
38
+ AGE: Proportion of owner-occupied units built prior to 1940 DIS: Weighted distances to five Boston employment centers
39
+ RAD: Index of accessibility to radial highways TAX: Full-value property tax rate per $10,000
40
+ PTRATIO: Pupil-teacher ratio by town B: 1000(Bk — 0.63)², where Bk is the proportion of [people of African American descent] by town
41
+ LSTAT: Percentage of lower status of the population MEDV: Median value of owner-occupied homes in $1000s
42
+ """
43
+
44
+ d = sklearn.datasets.load_boston()
45
+ df = pd.DataFrame(data=d.data, columns=d.feature_names) # pylint: disable=E1101
46
+ return df, d.target # pylint: disable=E1101
47
+
48
+
49
+ def linnerud(display=False):
50
+ """ Return the linnerud data in a nice package (multi-target regression). """
51
+
52
+ d = sklearn.datasets.load_linnerud()
53
+ X = pd.DataFrame(d.data, columns=d.feature_names) # pylint: disable=E1101
54
+ y = pd.DataFrame(d.target, columns=d.target_names) # pylint: disable=E1101
55
+ return X, y # pylint: disable=E1101
56
+
57
+
58
+ def imdb(display=False):
59
+ """ Return the clssic IMDB sentiment analysis training data in a nice package.
60
+ Full data is at: http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
61
+ Paper to cite when using the data is: http://www.aclweb.org/anthology/P11-1015
62
+ """
63
+
64
+ with open(cache(github_data_url + "imdb_train.txt")) as f:
65
+ data = f.readlines()
66
+ y = np.ones(25000, dtype=np.bool)
67
+ y[:12500] = 0
68
+ return data, y
69
+
70
+ def communitiesandcrime(display=False):
71
+ """ Predict total number of non-violent crimes per 100K popuation.
72
+ This dataset is from the classic UCI Machine Learning repository:
73
+ https://archive.ics.uci.edu/ml/datasets/Communities+and+Crime+Unnormalized
74
+ """
75
+
76
+ raw_data = pd.read_csv(
77
+ cache(github_data_url + "CommViolPredUnnormalizedData.txt"),
78
+ na_values="?"
79
+ )
80
+
81
+ # find the indices where the total violent crimes are known
82
+ valid_inds = np.where(np.invert(np.isnan(raw_data.iloc[:,-2])))[0]
83
+ y = np.array(raw_data.iloc[valid_inds,-2], dtype=np.float)
84
+
85
+ # extract the predictive features and remove columns with missing values
86
+ X = raw_data.iloc[valid_inds,5:-18]
87
+ valid_cols = np.where(np.isnan(X.values).sum(0) == 0)[0]
88
+ X = X.iloc[:,valid_cols]
89
+
90
+ return X, y
91
+
92
+ def diabetes(display=False):
93
+ """ Return the diabetes data in a nice package. """
94
+
95
+ d = sklearn.datasets.load_diabetes()
96
+ df = pd.DataFrame(data=d.data, columns=d.feature_names) # pylint: disable=E1101
97
+ return df, d.target # pylint: disable=E1101
98
+
99
+
100
+ def iris(display=False):
101
+ """ Return the classic iris data in a nice package. """
102
+
103
+ d = sklearn.datasets.load_iris()
104
+ df = pd.DataFrame(data=d.data, columns=d.feature_names) # pylint: disable=E1101
105
+ if display:
106
+ return df, [d.target_names[v] for v in d.target] # pylint: disable=E1101
107
+ else:
108
+ return df, d.target # pylint: disable=E1101
109
+
110
+
111
+ def adult(display=False):
112
+ """ Return the Adult census data in a nice package. """
113
+ dtypes = [
114
+ ("Age", "float32"), ("Workclass", "category"), ("fnlwgt", "float32"),
115
+ ("Education", "category"), ("Education-Num", "float32"), ("Marital Status", "category"),
116
+ ("Occupation", "category"), ("Relationship", "category"), ("Race", "category"),
117
+ ("Sex", "category"), ("Capital Gain", "float32"), ("Capital Loss", "float32"),
118
+ ("Hours per week", "float32"), ("Country", "category"), ("Target", "category")
119
+ ]
120
+ raw_data = pd.read_csv(
121
+ cache(github_data_url + "adult.data"),
122
+ names=[d[0] for d in dtypes],
123
+ na_values="?",
124
+ dtype=dict(dtypes)
125
+ )
126
+ data = raw_data.drop(["Education"], axis=1) # redundant with Education-Num
127
+ filt_dtypes = list(filter(lambda x: not (x[0] in ["Target", "Education"]), dtypes))
128
+ data["Target"] = data["Target"] == " >50K"
129
+ rcode = {
130
+ "Not-in-family": 0,
131
+ "Unmarried": 1,
132
+ "Other-relative": 2,
133
+ "Own-child": 3,
134
+ "Husband": 4,
135
+ "Wife": 5
136
+ }
137
+ for k, dtype in filt_dtypes:
138
+ if dtype == "category":
139
+ if k == "Relationship":
140
+ data[k] = np.array([rcode[v.strip()] for v in data[k]])
141
+ else:
142
+ data[k] = data[k].cat.codes
143
+
144
+ if display:
145
+ return raw_data.drop(["Education", "Target", "fnlwgt"], axis=1), data["Target"].values
146
+ else:
147
+ return data.drop(["Target", "fnlwgt"], axis=1), data["Target"].values
148
+
149
+
150
+ def nhanesi(display=False):
151
+ """ A nicely packaged version of NHANES I data with surivival times as labels.
152
+ """
153
+ X = pd.read_csv(cache(github_data_url + "NHANESI_X.csv"), index_col=0)
154
+ y = pd.read_csv(cache(github_data_url + "NHANESI_y.csv"), index_col=0)["y"]
155
+ if display:
156
+ X_display = X.copy()
157
+ #X_display["sex_isFemale"] = ["Female" if v else "Male" for v in X["sex_isFemale"]]
158
+ return X_display, np.array(y)
159
+ else:
160
+ return X, np.array(y)
161
+
162
+
163
+ def corrgroups60(display=False):
164
+ """ Correlated Groups 60
165
+
166
+ A simulated dataset with tight correlations among distinct groups of features.
167
+ """
168
+
169
+ # set a constant seed
170
+ old_seed = np.random.seed()
171
+ np.random.seed(0)
172
+
173
+ # generate dataset with known correlation
174
+ N = 1000
175
+ M = 60
176
+
177
+ # set one coefficent from each group of 3 to 1
178
+ beta = np.zeros(M)
179
+ beta[0:30:3] = 1
180
+
181
+ # build a correlation matrix with groups of 3 tightly correlated features
182
+ C = np.eye(M)
183
+ for i in range(0,30,3):
184
+ C[i,i+1] = C[i+1,i] = 0.99
185
+ C[i,i+2] = C[i+2,i] = 0.99
186
+ C[i+1,i+2] = C[i+2,i+1] = 0.99
187
+ f = lambda X: np.matmul(X, beta)
188
+
189
+ # Make sure the sample correlation is a perfect match
190
+ X_start = np.random.randn(N, M)
191
+ X_centered = X_start - X_start.mean(0)
192
+ Sigma = np.matmul(X_centered.T, X_centered) / X_centered.shape[0]
193
+ W = np.linalg.cholesky(np.linalg.inv(Sigma)).T
194
+ X_white = np.matmul(X_centered, W.T)
195
+ assert np.linalg.norm(np.corrcoef(np.matmul(X_centered, W.T).T) - np.eye(M)) < 1e-6 # ensure this decorrelates the data
196
+
197
+ # create the final data
198
+ X_final = np.matmul(X_white, np.linalg.cholesky(C).T)
199
+ X = X_final
200
+ y = f(X) + np.random.randn(N) * 1e-2
201
+
202
+ # restore the previous numpy random seed
203
+ np.random.seed(old_seed)
204
+
205
+ return pd.DataFrame(X), y
206
+
207
+
208
+ def independentlinear60(display=False):
209
+ """
210
+ A simulated dataset with tight correlations among distinct groups of features.
211
+ """
212
+
213
+ # set a constant seed
214
+ old_seed = np.random.seed()
215
+ np.random.seed(0)
216
+
217
+ # generate dataset with known correlation
218
+ N = 1000
219
+ M = 60
220
+
221
+ # set one coefficent from each group of 3 to 1
222
+ beta = np.zeros(M)
223
+ beta[0:30:3] = 1
224
+ f = lambda X: np.matmul(X, beta)
225
+
226
+ # Make sure the sample correlation is a perfect match
227
+ X_start = np.random.randn(N, M)
228
+ X = X_start - X_start.mean(0)
229
+ y = f(X) + np.random.randn(N) * 1e-2
230
+
231
+ # restore the previous numpy random seed
232
+ np.random.seed(old_seed)
233
+
234
+ return pd.DataFrame(X), y
235
+
236
+
237
+ def a1a():
238
+ """
239
+ A sparse dataset in scipy csr matrix format.
240
+ """
241
+ return sklearn.datasets.load_svmlight_file(cache(github_data_url + 'a1a.svmlight'))
242
+
243
+
244
+ def rank():
245
+ """
246
+ Ranking datasets from lightgbm repository.
247
+ """
248
+ rank_data_url = 'https://raw.githubusercontent.com/Microsoft/LightGBM/master/examples/lambdarank/'
249
+ x_train, y_train = sklearn.datasets.load_svmlight_file(cache(rank_data_url + 'rank.train'))
250
+ x_test, y_test = sklearn.datasets.load_svmlight_file(cache(rank_data_url + 'rank.test'))
251
+ q_train = np.loadtxt(cache(rank_data_url + 'rank.train.query'))
252
+ q_test = np.loadtxt(cache(rank_data_url + 'rank.test.query'))
253
+ return x_train, y_train, x_test, y_test, q_train, q_test
254
+
255
+
256
+ def cache(url, file_name=None):
257
+ if file_name is None:
258
+ file_name = os.path.basename(url)
259
+ data_dir = os.path.join(os.path.dirname(__file__), "cached_data")
260
+ if not os.path.isdir(data_dir):
261
+ os.mkdir(data_dir)
262
+
263
+ file_path = os.path.join(data_dir, file_name)
264
+ if not os.path.isfile(file_path):
265
+ urlretrieve(url, file_path)
266
+
267
+ return file_path
@@ -0,0 +1,263 @@
1
+ import numpy as np
2
+ from matplotlib import pyplot as plt
3
+ from matplotlib.ticker import MultipleLocator
4
+ import scipy.linalg as la
5
+ import networkx as nx
6
+ import random, time, math
7
+ from collections import Counter
8
+
9
+ import fun as f
10
+ from Graph import Graph
11
+ from Watts_Strogatz import watts_strogatz_graph
12
+ from Erdos_Renyi import erdos_renyi_graph
13
+
14
+ from sklearn.linear_model import LinearRegression
15
+ from sklearn.preprocessing import normalize
16
+
17
+ import graph_nets
18
+ from graph_nets.graphs import GraphsTuple
19
+ import graph_attribution as gatt
20
+
21
+ def Wasserstein_Dist(cdfX, cdfY):
22
+
23
+ Res = 0
24
+ power = 1
25
+ n = len(cdfX)
26
+
27
+ for ii in range(0, n-2):
28
+ height = abs(cdfX[ii]-cdfY[ii])
29
+ width = cdfX[ii+1] - cdfX[ii]
30
+ Res = Res + (height ** power) * width
31
+
32
+ return Res
33
+
34
+
35
+ def r_eigenv(G_i, G_j):
36
+ #Eigen-decomposition of G_j
37
+ A_Gi = (nx.adjacency_matrix(G_i)).todense()
38
+ D_i = np.diag(np.asarray(sum(A_Gi))[0])
39
+ eigenvalues_Gi, eigenvectors_Gi = la.eig(D_i - A_Gi)
40
+ r_eigenv_Gi = sorted(zip(eigenvalues_Gi.real, eigenvectors_Gi.T), key=lambda x: x[0])
41
+
42
+ #Eigen-decomposition of G_j
43
+ A_Gj = (nx.adjacency_matrix(G_j)).todense()
44
+ D_j = np.diag(np.asarray(sum(A_Gj))[0])
45
+ eigenvalues_Gj, eigenvectors_Gj = la.eig(D_j - A_Gj)
46
+ r_eigenv_Gj = sorted(zip(eigenvalues_Gj.real, eigenvectors_Gj.T), key=lambda x: x[0])
47
+
48
+ r = 4
49
+ signs =[-1,1]
50
+ temp = []
51
+ for sign_s in signs:
52
+ for sign_l in signs:
53
+ vri = sorted(f.normalize_eigenv(sign_s * r_eigenv_Gi[r][1]))
54
+ vrj = sorted(f.normalize_eigenv(sign_l * r_eigenv_Gj[r][1]))
55
+ cdf_dist = f.cdf_dist(vri, vrj)
56
+ temp.append(cdf_dist)
57
+
58
+ #Compute empirical CDF
59
+ step = 0.005
60
+ x=np.arange(0, 1, step)
61
+ cdf_grid_Gip = f.cdf(len(r_eigenv_Gi[r][1]),x,
62
+ f.normalize_eigenv(sorted(r_eigenv_Gi[r][1], key=lambda x: x)))
63
+ cdf_grid_Gin = f.cdf(len(r_eigenv_Gi[r][1]),x,
64
+ f.normalize_eigenv(sorted(-r_eigenv_Gi[r][1], key=lambda x: x)))
65
+
66
+ cdf_grid_Gjp = f.cdf(len(r_eigenv_Gj[r][1]),x,
67
+ f.normalize_eigenv(sorted(r_eigenv_Gj[r][1], key=lambda x: x)))
68
+ cdf_grid_Gjn = f.cdf(len(r_eigenv_Gj[r][1]),x,
69
+ f.normalize_eigenv(sorted(-r_eigenv_Gj[r][1], key=lambda x: x)))
70
+
71
+ WD1 = Wasserstein_Dist(cdf_grid_Gip, cdf_grid_Gjp)
72
+ WD2 = Wasserstein_Dist(cdf_grid_Gip, cdf_grid_Gjn)
73
+ WD3 = Wasserstein_Dist(cdf_grid_Gin, cdf_grid_Gjp)
74
+ WD4 = Wasserstein_Dist(cdf_grid_Gin, cdf_grid_Gjn)
75
+
76
+ WD = [WD1, WD2, WD3, WD4]
77
+
78
+ return max(temp), max(WD)
79
+
80
+ def xwhy_graph_edges(X_input_graph, model, num_perturb = 50, kernel_width = 0.25, num_top_features = 10, eps=1):
81
+ #num_perturb = 5000
82
+
83
+ num_uniqe_words = len(X_input_graph.edges)
84
+
85
+ perturbations = np.random.binomial(1, 0.5, size=(num_perturb, num_uniqe_words))
86
+
87
+ def perturb_graph_edge(input_graph, perturbation):
88
+ perturbed_graph = input_graph.copy()
89
+ ebun = []
90
+ for ii, ed in enumerate(perturbed_graph.edges):
91
+ if perturbation[ii] == 0:
92
+ ebun.append(ed)
93
+ perturbed_graph.remove_edges_from(ebun)
94
+ return perturbed_graph
95
+
96
+ predictions = []
97
+ WD_dist = []
98
+ for pert in perturbations:
99
+ p_graph = perturb_graph_edge(X_input_graph, pert)
100
+ pred = model.predict(graph_nets.utils_np.networkxs_to_graphs_tuple([p_graph]))
101
+ predictions.append(pred)
102
+ Sscore, WD_score = r_eigenv(X_input_graph, p_graph)
103
+ WD_dist.append(WD_score)
104
+
105
+ predictions = np.array(predictions)
106
+ WD_dist = np.array(WD_dist)
107
+
108
+ weights = np.sqrt(np.exp(-((eps*WD_dist)**2)/kernel_width**2)) #Kernel function
109
+
110
+ class_to_explain = 0
111
+ simpler_model = LinearRegression()
112
+ simpler_model.fit(X=perturbations, y=predictions, sample_weight=weights)
113
+ coeff = simpler_model.coef_[0]
114
+
115
+ top_features = np.argsort(abs(coeff))[-num_top_features:]
116
+
117
+ coeff2 = simpler_model.coef_
118
+
119
+ odds = np.exp(coeff2)
120
+
121
+ Bounded_coeff = 2*(normalize(coeff[:,np.newaxis], axis=0).ravel()+1)
122
+
123
+ return coeff, odds, top_features, Bounded_coeff, simpler_model
124
+
125
+ def perturb_graph_node(input_graph, perturbation, Remove_Zero_Degree_Nodes=False):
126
+ """
127
+ This function modifies an input graph by removing nodes based on a specified perturbation.
128
+
129
+ Parameters:
130
+ - input_graph (networkx.Graph): The input graph to be perturbed.
131
+ - perturbation (List[List[int]]): A 2D list of 0/1 values indicating whether each node in the input graph should be removed (1) or not (0).
132
+ - Remove_Zero_Degree_Nodes (bool): Indicates whether nodes with zero degree should be removed (True) or not (False).
133
+
134
+ Returns:
135
+ - perturbed_node_graph_reordered (networkx.Graph): The perturbed graph with reordered node labels.
136
+
137
+ Raises:
138
+ - ValueError: If the length of `perturbation` does not match the number of nodes in `input_graph`.
139
+ """
140
+
141
+ if len(perturbation[0]) != len(input_graph.nodes):
142
+ raise ValueError("The length of `perturbation` must match the number of nodes in `input_graph`.")
143
+
144
+ perturbed_node_graph = input_graph.copy()
145
+ ebun = []
146
+ for ii, nd in enumerate(perturbed_node_graph.nodes):
147
+ if perturbation[0][ii] == 1:
148
+ ebun.append(nd)
149
+ perturbed_node_graph.remove_nodes_from(ebun)
150
+
151
+ if Remove_Zero_Degree_Nodes:
152
+ Zdegree_nodes = []
153
+ for n, d in perturbed_node_graph.degree():
154
+ if d == 0:
155
+ Zdegree_nodes.append(n)
156
+
157
+ perturbed_node_graph.remove_nodes_from(Zdegree_nodes)
158
+
159
+ perturbed_node_graph_reordered = nx.convert_node_labels_to_integers(perturbed_node_graph, first_label=0)
160
+
161
+ return perturbed_node_graph_reordered
162
+
163
+ def perturb_graph_edge(input_graph, perturbation):
164
+ """
165
+ This function modifies an input graph by removing edges based on a specified perturbation.
166
+
167
+ Parameters:
168
+ - input_graph (networkx.Graph): The input graph to be perturbed.
169
+ - perturbation (List[int]): A list of 0/1 values indicating whether each edge in the input graph should be removed (1) or not (0).
170
+
171
+ Returns:
172
+ - perturbed_graph (networkx.Graph): The perturbed graph.
173
+
174
+ Raises:
175
+ - ValueError: If the length of `perturbation` does not match the number of edges in `input_graph`.
176
+ """
177
+
178
+ if len(perturbation) != len(input_graph.edges):
179
+ raise ValueError("The length of `perturbation` must match the number of edges in `input_graph`.")
180
+
181
+ perturbed_graph = input_graph.copy()
182
+ ebun = []
183
+ for ii, ed in enumerate(perturbed_graph.edges):
184
+ if perturbation[ii] == 1:
185
+ ebun.append(ed)
186
+ perturbed_graph.remove_edges_from(ebun)
187
+
188
+ return perturbed_graph
189
+
190
+
191
+ def explain_graph_nodes(input_graph, model, num_perturbations=5000, kernel_width=0.25, num_top_features=10, epsilon=1, remove_zero_degree_nodes=False):
192
+ """
193
+ This function explains the contribution of each node to the prediction of a given graph-based model.
194
+ It uses a white-box model interpretation method XWhy to calculate the contribution of each node by
195
+ perturbing the input graph and measuring the change in the model's prediction.
196
+
197
+ Parameters:
198
+ input_graph (nx.Graph) : A networkx graph representing the input graph.
199
+ model (sklearn.BaseEstimator) : The graph-based model to be explained.
200
+ num_perturbations (int) : The number of perturbations to be applied to the input graph (default is 5000).
201
+ kernel_width (float) : The width of the kernel function (default is 0.25).
202
+ num_top_features (int) : The number of top features to be returned (default is 10).
203
+ epsilon (float) : The hyperparameter for the kernel function (default is 1).
204
+ remove_zero_degree_nodes (bool) : A flag indicating whether to remove nodes with zero degree after perturbation (default is False).
205
+
206
+ Returns:
207
+ coeff (np.ndarray) : The coefficients of the linear regression model that explains the relationship between the node perturbations and the change in prediction.
208
+ odds (np.ndarray) : The odds ratio of the linear regression model.
209
+ top_features (np.ndarray) : The indices of the top `num_top_features` features based on the absolute values of the coefficients.
210
+ bounded_coeff (np.ndarray) : The normalized coefficients of the linear regression model.
211
+ simpler_model (LinearRegression) : The linear regression model used to explain the relationship between the node perturbations and the change in prediction.
212
+
213
+ Raises:
214
+ ValueError : If the number of perturbations is not a positive integer.
215
+ ValueError : If the kernel width is not positive.
216
+ ValueError : If the number of top features is not a positive integer.
217
+ ValueError : If the epsilon is not positive.
218
+ """
219
+ # Validate the input arguments
220
+ if not isinstance(num_perturbations, int) or num_perturbations <= 0:
221
+ raise ValueError("The number of perturbations must be a positive integer.")
222
+ if not isinstance(kernel_width, (int, float)) or kernel_width <= 0:
223
+ raise ValueError("The kernel width must be a positive number.")
224
+ if not isinstance(num_top_features, int) or num_top_features <= 0:
225
+ raise ValueError("The number of top features must be a positive integer.")
226
+ if not isinstance(epsilon, (int, float)) or epsilon <= 0:
227
+ raise ValueError("The epsilon must be a positive number.")
228
+
229
+ num_uniqe_nodes = len(X_input_graph.nodes)
230
+
231
+ perturbations = np.random.binomial(1, 0.5, size=(num_perturb, num_uniqe_nodes))
232
+
233
+ predictions = []
234
+ WD_dist = []
235
+ for pert in perturbations:
236
+ p_graph = perturb_graph_node(X_input_graph, pert)
237
+ if len(p_graph.nodes) != 0:
238
+ pred = model.predict(graph_nets.utils_np.networkxs_to_graphs_tuple([p_graph]))
239
+ predictions.append(pred)
240
+ Sscore, WD_score = r_eigenv(X_input_graph, p_graph)
241
+ WD_dist.append(WD_score)
242
+
243
+ predictions = np.array(predictions)
244
+ WD_dist = np.array(WD_dist)
245
+
246
+ weights = np.sqrt(np.exp(-((eps*WD_dist)**2)/kernel_width**2)) #Kernel function
247
+
248
+ class_to_explain = 0
249
+ simpler_model = LinearRegression()
250
+ simpler_model.fit(X=perturbations, y=predictions, sample_weight=weights)
251
+ coeff = simpler_model.coef_[0]
252
+
253
+ top_features = np.argsort(abs(coeff))[-num_top_features:]
254
+
255
+ coeff2 = simpler_model.coef_
256
+
257
+ odds = np.exp(coeff2)
258
+
259
+ from sklearn.preprocessing import normalize
260
+
261
+ Bounded_coeff = normalize(coeff[:,np.newaxis], axis=0).ravel()
262
+
263
+ return coeff, odds, top_features, Bounded_coeff, simpler_model
@@ -0,0 +1,127 @@
1
+ import numpy as np
2
+ import keras
3
+ from keras.applications.imagenet_utils import decode_predictions
4
+ import skimage.io
5
+ import skimage.segmentation
6
+ import copy
7
+ import sklearn
8
+ import sklearn.metrics
9
+ from sklearn.linear_model import LinearRegression
10
+
11
+ def Wasserstein_Dist(XX, YY):
12
+
13
+ '''
14
+ Wasserstein_Dist_PVal is for Wasserstein distance measure with Boostrap-based p-value calculation.
15
+ The p-Value can be used to validate statistical distance measures.
16
+
17
+ XX: The first input vector. It should be a numpy array with length of n.
18
+ YY: The second input vector. It should be a numpy array with lenght of m.
19
+ '''
20
+
21
+ import numpy as np
22
+ nx = len(XX)
23
+ ny = len(YY)
24
+ n = nx + ny
25
+
26
+ XY = np.concatenate([XX,YY])
27
+ X2 = np.concatenate([np.repeat(1/nx, nx), np.repeat(0, ny)])
28
+ Y2 = np.concatenate([np.repeat(0, nx), np.repeat(1/ny, ny)])
29
+
30
+ S_Ind = np.argsort(XY)
31
+ XY_Sorted = XY[S_Ind]
32
+ X2_Sorted = X2[S_Ind]
33
+ Y2_Sorted = Y2[S_Ind]
34
+
35
+ Res = 0
36
+ E_CDF = 0
37
+ F_CDF = 0
38
+ power = 1
39
+
40
+ for ii in range(0, n-2):
41
+ E_CDF = E_CDF + X2_Sorted[ii]
42
+ F_CDF = F_CDF + Y2_Sorted[ii]
43
+ height = abs(F_CDF-E_CDF)
44
+ width = XY_Sorted[ii+1] - XY_Sorted[ii]
45
+ Res = Res + (height ** power) * width;
46
+
47
+ return Res
48
+
49
+ def Wasserstein_Dist_PVal(XX, YY):
50
+ # Information about Bootstrap:
51
+ # https://towardsdatascience.com/an-introduction-to-the-bootstrap-method-58bcb51b4d60
52
+ import random
53
+ nboots = 1000
54
+ WD = Wasserstein_Dist(XX,YY)
55
+ na = len(XX)
56
+ nb = len(YY)
57
+ n = na + nb
58
+ comb = np.concatenate([XX,YY])
59
+ reps = 0
60
+ bigger = 0
61
+ for ii in range(1, nboots):
62
+ e = random.sample(range(n), na)
63
+ f = random.sample(range(n), nb)
64
+ boost_WD = Wasserstein_Dist(comb[e],comb[f]);
65
+ if (boost_WD > WD):
66
+ bigger = 1 + bigger
67
+
68
+ pVal = bigger/nboots;
69
+
70
+ return pVal, WD
71
+
72
+ def Wasserstein_Dist_Image(img1, img2):
73
+ if img1.shape[0] != img2.shape[0] or img1.shape[1] != img2.shape[1]:
74
+ print('input images should have the same size')
75
+ else:
76
+ WD = []
77
+ for ii in range(3):
78
+
79
+ im1 = np.array(img1[:,:,ii].flatten())
80
+ im2 = np.array(img2[:,:,ii].flatten())
81
+
82
+ WD.append(Wasserstein_Dist(im1, im2))
83
+
84
+ return sum(WD)
85
+
86
+ def xwhy_image2(X_input, model, perturbations, num_perturb = 150, kernel_width = 0.25):
87
+
88
+ superpixels = skimage.segmentation.quickshift(X_input, kernel_size=4,max_dist=200, ratio=0.2)
89
+ num_superpixels = np.unique(superpixels).shape[0]
90
+ #perturbations = np.random.binomial(1, 0.5, size=(num_perturb, num_superpixels))
91
+
92
+
93
+ def perturb_image(img,perturbation,segments):
94
+ active_pixels = np.where(perturbation == 1)[0]
95
+ mask = np.zeros(segments.shape)
96
+ for active in active_pixels:
97
+ mask[segments == active] = 1
98
+ perturbed_image = copy.deepcopy(img)
99
+ perturbed_image = perturbed_image*mask[:,:,np.newaxis]
100
+ return perturbed_image
101
+
102
+ predictions = []
103
+ WD_dist = []
104
+ for pert in perturbations:
105
+ perturbed_img = perturb_image(X_input,pert,superpixels)
106
+ pred = model.predict(perturbed_img[np.newaxis,:,:,:])
107
+ predictions.append(pred)
108
+ WD_dist = Wasserstein_Dist_Image(X_input, perturbed_img)
109
+
110
+
111
+ predictions = np.array(predictions)
112
+
113
+ original_image = np.ones(num_superpixels)[np.newaxis,:] #Perturbation with all superpixels enabled
114
+ # distances = sklearn.metrics.pairwise_distances(perturbations,original_image, metric='cosine').ravel()
115
+
116
+
117
+ weights = np.sqrt(np.exp(-(WD_dist**2)/kernel_width**2)) #Kernel function
118
+
119
+ preds = model.predict(X_input[np.newaxis,:,:,:])
120
+ decode_predictions(preds)
121
+ top_pred_classes = preds[0].argsort()[-5:][::-1]
122
+
123
+ class_to_explain = top_pred_classes[0]
124
+ simpler_model = LinearRegression()
125
+ simpler_model.fit(X=perturbations, y=predictions[:,:,class_to_explain], sample_weight=weights)
126
+ coeff = simpler_model.coef_[0]
127
+
@@ -0,0 +1 @@
1
+ # reserved for future developement
@@ -0,0 +1,153 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import sklearn
4
+ import sklearn.metrics
5
+ from sklearn.linear_model import LinearRegression
6
+ from SafeML.Wasserstein_Dist_PVal import Wasserstein_Dist
7
+ import SafeML
8
+
9
+ def WasserstainLIME2(X_input, model, num_perturb = 500, L_num_perturb = 100, kernel_width2 = 0.75, epsilon = 0.1):
10
+ '''
11
+ WasserstainLIME is a statistical version of LIME (local interpretable model-agnostic explanations)
12
+ in which instead of Euclidean distance, the ECDF-based distance is used.
13
+
14
+ X_input: should be a numpy array that represents one point in a n-dimensional space.
15
+
16
+ num_perturb: Is the number of perturbations that the algorithm uses.
17
+
18
+ L_num_perturb: Is the number of perturbations in the local areas that the algorithm uses.
19
+
20
+ kernel_width: Is the Kernel Width. When the decision space is very dynamic, the kernel width should be low like 0.2,
21
+ otherwise kernel with around 0.75 would be ideal.
22
+
23
+ model: It is the trained model that can be for a classification or regression.
24
+
25
+ epsilon: It is used to normalize the WD values.
26
+
27
+ '''
28
+
29
+ X_input = (X_input - np.mean(X_input,axis=0)) / np.std(X_input,axis=0) #Standarization of data
30
+
31
+ X_lime = np.random.normal(0,1,size=(num_perturb,X_input.shape[0]))
32
+
33
+ Xi2 = np.zeros((L_num_perturb,X_input.shape[0]))
34
+
35
+ for jj in range(X_input.shape[0]):
36
+ Xi2[:,jj] = X_input[jj] + np.random.normal(0,0.05,L_num_perturb)
37
+
38
+ y_lime2 = np.zeros((num_perturb,1))
39
+ WD = np.zeros((num_perturb,1))
40
+ weights2 = np.zeros((num_perturb,1))
41
+
42
+ for ind, ii in enumerate(X_lime):
43
+
44
+ df2 = pd.DataFrame()
45
+
46
+ for jj in range(X_input.shape[0]):
47
+ temp1 = ii[jj] + np.random.normal(0,0.3,L_num_perturb)
48
+ df2[len(df2.columns)] = temp1
49
+
50
+ temp3 = model.predict(df2.to_numpy())
51
+
52
+ y_lime2[ind] = np.mean(temp3) # For classification: np.argmax(np.bincount(temp3))
53
+
54
+ WD1 = np.zeros((X_input.shape[0],1))
55
+
56
+ df2 = df2.to_numpy()
57
+
58
+ for kk in range(X_input.shape[0]):
59
+ #print( df2.shape)
60
+ WD1[kk] = Wasserstein_Dist(Xi2[:,kk], df2[:,kk])
61
+
62
+ #print(WD1)
63
+ #print(ind)
64
+ WD[ind] = sum(WD1)
65
+ #print(WD)
66
+
67
+ weights2[ind] = np.sqrt(np.exp(-((epsilon*WD[ind])**2)/(kernel_width2**2)))
68
+ #print(weights2[ind])
69
+
70
+ del df2
71
+
72
+ weights2 = weights2.flatten()
73
+ #print(weights2)
74
+
75
+ simpler_model2 = LinearRegression()
76
+ simpler_model2.fit(X_lime, y_lime2, sample_weight=weights2)
77
+ y_linmodel2 = simpler_model2.predict(X_lime)
78
+ y_linmodel2 = y_linmodel2 < 0.5 #Conver to binary class
79
+ y_linmodel2 = y_linmodel2.flatten()
80
+
81
+ return X_lime, y_lime2, weights2, y_linmodel2, simpler_model2.coef_.flatten()
82
+
83
+ def WasserstainLIME(X_input, model, num_perturb = 500, kernel_width2 = 0.2):
84
+
85
+ '''
86
+ WasserstainLIME(X_input, num_perturb = 500, kernel_width2 = 0.2):
87
+
88
+ X_input: The input feature data for the WassersteinLIME function. It should be a 1D numpy array with two elements.
89
+
90
+ kernel_width2: The kernel width parameter for the Wasserstein distance calculation. It determines the size of the
91
+ region around the original feature data that is considered for the linear regression model. Larger
92
+ values of kernel_width2 result in a wider region and more perturbations being included in the explanation,
93
+ while smaller values result in a more localized explanation. The default value is 0.2.
94
+
95
+ This function uses Wasserstein distance to generate local explanations for a binary classifier.
96
+ It creates num_perturb number of perturbed versions of the input feature data, and for each perturbation
97
+ it predicts the class probabilities, computes the Wasserstein distances between the original
98
+ and perturbed feature data, and uses the distances to weight a linear regression model that explains the binary predictions.
99
+ The function returns the perturbed feature data, binary predictions, weight of each perturbation,
100
+ coefficients of the linear regression model, and the predictions of the linear regression model.
101
+ '''
102
+
103
+ try:
104
+ if not isinstance(X_input, np.ndarray) or X_input.ndim != 2:
105
+ raise TypeError("X_input must be a 2-dimensional array.")
106
+ except TypeError as te:
107
+ print(te)
108
+
109
+ try:
110
+ if not isinstance(num_perturb, int):
111
+ raise ValueError("num_perturb must be an integer.")
112
+ except ValueError as ve:
113
+ print(ve)
114
+
115
+ try:
116
+ if not np.isscalar(kernel_width2):
117
+ raise ValueError("kernel_width2 must be a scalar.")
118
+ except ValueError as ve:
119
+ print(ve)
120
+
121
+ X_lime = np.random.normal(0,1,size=(num_perturb,X_input.shape[1]))
122
+
123
+ Xi2 = np.zeros((100,2))
124
+ Xi2[:,0] = X_input[0] + np.random.normal(0,0.05,100)
125
+ Xi2[:,1] = X_input[1] + np.random.normal(0,0.05,100)
126
+
127
+ y_lime2 = np.zeros((num_perturb,1))
128
+ WD = np.zeros((num_perturb,1))
129
+ weights2 = np.zeros((num_perturb,1))
130
+
131
+ for ind, ii in enumerate(X_lime):
132
+ temp1 = ii[0] + np.random.normal(0,0.4,100)
133
+ temp2 = ii[1] + np.random.normal(0,0.4,100)
134
+ df2 = pd.DataFrame()
135
+ df2['x1'] = temp1
136
+ df2['x2'] = temp2
137
+ temp3 = model.predict(df2)
138
+ y_lime2[ind] = np.argmax(np.bincount(temp3))
139
+ WD1 = Wasserstein_Dist(Xi2[:,0], df2[:]['x1'])
140
+ WD2 = Wasserstein_Dist(Xi2[:,1], df2[:]['x2'])
141
+ WD[ind] = sum([WD1, WD2])
142
+
143
+ weights2[ind] = np.sqrt(np.exp(-(WD[ind]**2)/(kernel_width2**2)))
144
+
145
+ weights2 = weights2.flatten()
146
+
147
+ simpler_model2 = LinearRegression()
148
+ simpler_model2.fit(X_lime, y_lime2, sample_weight=weights2)
149
+ y_linmodel2 = simpler_model2.predict(X_lime)
150
+ y_linmodel2 = y_linmodel2 < 0.5 #Conver to binary class
151
+ y_linmodel2 = y_linmodel2.flatten()
152
+
153
+ return X_lime, y_lime2, weights2, y_linmodel2, simpler_model2.coef_.flatten()
@@ -0,0 +1,128 @@
1
+ import numpy as np
2
+
3
+ import lime
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import sklearn
8
+ import sklearn.ensemble
9
+ import sklearn.metrics
10
+ #from __future__ import print_function
11
+
12
+ import sklearn
13
+ import sklearn.metrics
14
+ from sklearn.linear_model import LinearRegression
15
+ from sklearn.datasets import fetch_20newsgroups
16
+
17
+ import string
18
+ import re
19
+ import nltk
20
+ from nltk.tokenize import TweetTokenizer
21
+
22
+ # embd = embedding_google
23
+
24
+ def xwhy_text(X_input_text, model, perturbations, embd, num_perturb = 50, kernel_width = 0.25, num_top_features = 10, eps=0.5):
25
+
26
+ # # NLP pre-processing
27
+ # # remove urls, handles, and the hashtag from hashtags
28
+ # # (taken from https://stackoverflow.com/questions/8376691/how-to-remove-hashtag-user-link-of-a-tweet-using-regular-expression)
29
+ # def remove_urls(text):
30
+ # new_text = ' '.join(re.sub("(@[A-Za-z0-9]+)|([^0-9A-Za-z \t])|(\w+:\/\/\S+)"," ",text).split())
31
+ # return new_text
32
+
33
+ # # make all text lowercase
34
+ # def text_lowercase(text):
35
+ # return text.lower()
36
+
37
+ # # remove numbers
38
+ # def remove_numbers(text):
39
+ # result = re.sub(r'\d+', '', text)
40
+ # return result
41
+
42
+ # # remove punctuation
43
+ # def remove_punctuation(text):
44
+ # translator = str.maketrans('', '', string.punctuation)
45
+ # return text.translate(translator)
46
+
47
+ # # function for all pre-processing steps
48
+ # def preprocessing(text):
49
+ # text = text_lowercase(text)
50
+ # text = remove_urls(text)
51
+ # text = remove_numbers(text)
52
+ # text = remove_punctuation(text)
53
+ # return text
54
+
55
+ # # pre-processing the text body column
56
+ # pp_text = []
57
+ # for text_data in X_input_text:
58
+ # # check if string
59
+ # if isinstance(text_data, str):
60
+ # pp_text_data = preprocessing(text_data)
61
+ # pp_text.append(pp_text_data)
62
+ # # if not string
63
+ # else:
64
+ # pp_text.append(np.NaN)
65
+ # cleaned_words = clean_text(X_input_text)
66
+
67
+ wod = X_input_text.split()
68
+
69
+ wod = [word.strip('-.,!;()[]@><:') for word in wod]
70
+ # wod = [word.replace("'s", '') for word in wod]
71
+ wod = [word.replace(".", '') for word in wod]
72
+ wod = [word.replace("-", '') for word in wod]
73
+ # words = [word.replace(":", '') for word in words]
74
+ # words = [word.replace(">", '') for word in words]
75
+
76
+
77
+ #finding unique
78
+ unique = []
79
+ for word in wod:
80
+ if word not in unique:
81
+ unique.append(word)
82
+
83
+ # Sorting the Unique Values
84
+ unique.sort()
85
+
86
+ #num_perturb = 150
87
+ num_uniqe_words = len(wod)
88
+ perturbations = np.random.binomial(1, 0.5, size=(num_perturb, num_uniqe_words))
89
+ text_list = wod.copy()
90
+
91
+ def perturb_text(text_list, perturbation):
92
+ for x, y in enumerate(text_list):
93
+ if perturbation[x] == 0:
94
+ text_list.remove(y)
95
+
96
+ predictions = []
97
+ WD_dist = []
98
+ for pert in perturbations:
99
+ perturbed_text = perturb_text(text_list, pert)
100
+ pred = model.predict_proba([str(perturbed_text)])
101
+ predictions.append(pred)
102
+ WD_score = embd.wmdistance(str(wod), str(perturbed_text))
103
+ WD_dist.append(WD_score)
104
+
105
+ predictions = np.array(predictions)
106
+ WD_dist = np.array(WD_dist)
107
+
108
+ weights = np.sqrt(np.exp(-((eps*WD_dist)**2)/kernel_width**2)) #Kernel function
109
+
110
+ class_to_explain = 0
111
+ simpler_model = LinearRegression()
112
+ simpler_model.fit(X=perturbations, y=predictions[:,:, class_to_explain], sample_weight=weights)
113
+ coeff = simpler_model.coef_[0]
114
+
115
+ print(coeff.shape)
116
+
117
+ coeff3 = coeff[0:50]
118
+
119
+ num_top_features = 50
120
+ top_features = np.argsort(abs(coeff))[-num_top_features:]
121
+
122
+ coeff2 = simpler_model.coef_
123
+
124
+ print(coeff3.shape)
125
+ # https://stackoverflow.com/questions/39626401/how-to-get-odds-ratios-and-other-related-features-with-scikit-learn
126
+ odds = np.exp(coeff2)
127
+
128
+ return coeff3, coeff, odds, top_features, wod
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: xwhy
3
+ Version: 0.0.0.1
4
+ Summary: Explain Why (XWhy) with Statistical Model-agnostic Interpretability with Local Explanations (SMILE)
5
+ Home-page: https://github.com/Dependable-Intelligent-Systems-Lab/xwhy
6
+ Author: Mojgan Hashemian, Koorosh Aslansefat (corresponding), Mohammad Naveed Akram, Ioannis Sorokos, Martin Walker, Yiannis Papadopoulos
7
+ Author-email: koo.ec2008@gmail.com
8
+ License: BSD
9
+ Description: UNKNOWN
10
+ Platform: UNKNOWN
11
+ Requires-Python: >=3.5
12
+ Provides-Extra: dev
@@ -0,0 +1,16 @@
1
+ README.md
2
+ setup.cfg
3
+ setup.py
4
+ xwhy/__init__.py
5
+ xwhy/datasets.py
6
+ xwhy/smile_graph.py
7
+ xwhy/smile_image.py
8
+ xwhy/smile_quantum.py
9
+ xwhy/smile_tabular.py
10
+ xwhy/smile_text.py
11
+ xwhy.egg-info/PKG-INFO
12
+ xwhy.egg-info/SOURCES.txt
13
+ xwhy.egg-info/dependency_links.txt
14
+ xwhy.egg-info/not-zip-safe
15
+ xwhy.egg-info/requires.txt
16
+ xwhy.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,12 @@
1
+ matplotlib
2
+ numpy
3
+ scipy
4
+ tqdm>=4.29.1
5
+ scikit-learn>=0.18
6
+ scikit-image>=0.12
7
+ pyDOE2==1.3.0
8
+ twine==1.13.0
9
+
10
+ [dev]
11
+ pytest
12
+ flake8
@@ -0,0 +1 @@
1
+ xwhy