Model-agnostic Techniques for Generating Local Explanations¶
LIME¶
LIME (i.e., Local Interpretable Model-agnostic Explanations) [RSG16b] is a model-agnostic technique that mimics the behaviour of the black-box model to generate the explanations of the predictions of the black-box model. Given a black-box model and an instance to explain, LIME performs 4 key steps to generate an instance explanation as follows:
First, LIME randomly generates instances surrounding the instance of interest.
Second, LIME uses the black-box model to generate predictions of the generated random instances.
Third, LIME constructs a local regression model using the generated random instances and their generated predictions from the black-box model.
Finally, the coefficients of the regression model indicate the contribution of each metric on the prediction of the instance of interest according to the black-box model.
For an interactive tutorial, please refer to the snippet below.
## Load Data and preparing datasets
# Import for Load Data
from os import listdir
from os.path import isfile, join
import pandas as pd
# Import for Split Data into Training and Testing Samples
from sklearn.model_selection import train_test_split
# Import for Construct a black-box model (Random Forests)
import statsmodels.api as sm
from statsmodels.formula.api import ols
from sklearn.ensemble import RandomForestClassifier
# Import for LIME
import lime
import lime.lime_tabular
train_dataset = pd.read_csv(("../../datasets/lucene-2.9.0.csv"), index_col = 'File')
test_dataset = pd.read_csv(("../../datasets/lucene-3.0.0.csv"), index_col = 'File')
outcome = 'RealBug'
features = ['OWN_COMMIT', 'Added_lines', 'CountClassCoupled', 'AvgLine', 'RatioCommentToCode']
# process outcome to 0 and 1
train_dataset[outcome] = pd.Categorical(train_dataset[outcome])
train_dataset[outcome] = train_dataset[outcome].cat.codes
test_dataset[outcome] = pd.Categorical(test_dataset[outcome])
test_dataset[outcome] = test_dataset[outcome].cat.codes
X_train = train_dataset.loc[:, features]
X_test = test_dataset.loc[:, features]
y_train = train_dataset.loc[:, outcome]
y_test = test_dataset.loc[:, outcome]
# commits - # of commits that modify the file of interest
# Added lines - # of added lines of code
# Count class coupled - # of classes that interact or couple with the class of interest
# LOC - # of lines of code
# RatioCommentToCode - The ratio of lines of comments to lines of code
features = ['nCommit', 'AddedLOC', 'nCoupledClass', 'LOC', 'CommentToCodeRatio']
X_train.columns = features
X_test.columns = features
training_data = pd.concat([X_train, y_train], axis=1)
testing_data = pd.concat([X_test, y_test], axis=1)
## Construct a black-box model (Random Forests)
# random forests
rf_model = RandomForestClassifier(random_state=1234, n_jobs = 10)
rf_model.fit(X_train, y_train)
# construct a lime explainer
lime_explainer = lime.lime_tabular.LimeTabularExplainer(
training_data = X_train.values,
mode='classification',
training_labels=y_train,
feature_names=features,
class_names = ['Clean', 'Defective'],
discretize_continuous=True,
random_state = 1234)
# random an instance that is predicted as defective for generating a visual example
# src/java/org/apache/lucene/index/DocumentsWriter.java
print('src/java/org/apache/lucene/index/DocumentsWriter.java is likely to be defective with the probability of', rf_model.predict_proba(X_test.loc['src/java/org/apache/lucene/index/DocumentsWriter.java':, :].iloc[0:1,:])[0][1])
# generate a LIME local explanation of the instance
lime_local_explanation = lime_explainer.explain_instance(
data_row = X_test.loc['src/java/org/apache/lucene/index/DocumentsWriter.java',:],
predict_fn = rf_model.predict_proba,
num_features=5,
top_labels=1)
# textual explanation
print(lime_local_explanation.as_list(label= lime_local_explanation.available_labels()[0]))
# visual LIME
lime_local_explanation.show_in_notebook()
src/java/org/apache/lucene/index/DocumentsWriter.java is likely to be defective with the probability of 0.79
[('nCoupledClass > 9.00', 0.19990645354272), ('AddedLOC > 95.00', 0.15787821638941874), ('CommentToCodeRatio <= 0.34', 0.04030911709105911), ('0.50 < nCommit <= 1.00', 0.0167472365164234), ('10.00 < LOC <= 15.00', -0.008381568589944476)]
SHAP¶
SHAP (Shapley values) [LEL18] is a model-agnostic technique that generate the explanations of the black-box model based on game theory.
For an interactive tutorial, please refer to the snippet below.
## Load Data and preparing datasets
# Import for Load Data
from os import listdir
from os.path import isfile, join
import pandas as pd
# Import for Split Data into Training and Testing Samples
from sklearn.model_selection import train_test_split
# Import for Construct a black-box model (Regression and Random Forests)
import statsmodels.api as sm
from statsmodels.formula.api import ols
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
# Import libraries for SHAP
import subprocess
import sys
import importlib
import numpy
import shap
train_dataset = pd.read_csv(("../../datasets/lucene-2.9.0.csv"), index_col = 'File')
test_dataset = pd.read_csv(("../../datasets/lucene-3.0.0.csv"), index_col = 'File')
outcome = 'RealBug'
features = ['OWN_COMMIT', 'Added_lines', 'CountClassCoupled', 'AvgLine', 'RatioCommentToCode']
# process outcome to 0 and 1
train_dataset[outcome] = pd.Categorical(train_dataset[outcome])
train_dataset[outcome] = train_dataset[outcome].cat.codes
test_dataset[outcome] = pd.Categorical(test_dataset[outcome])
test_dataset[outcome] = test_dataset[outcome].cat.codes
X_train = train_dataset.loc[:, features]
X_test = test_dataset.loc[:, features]
y_train = train_dataset.loc[:, outcome]
y_test = test_dataset.loc[:, outcome]
# commits - # of commits that modify the file of interest
# Added lines - # of added lines of code
# Count class coupled - # of classes that interact or couple with the class of interest
# LOC - # of lines of code
# RatioCommentToCode - The ratio of lines of comments to lines of code
features = ['nCommit', 'AddedLOC', 'nCoupledClass', 'LOC', 'CommentToCodeRatio']
X_train.columns = features
X_test.columns = features
training_data = pd.concat([X_train, y_train], axis=1)
testing_data = pd.concat([X_test, y_test], axis=1)
## Construct a black-box model (Regression and Random Forests)
# random forests
rf_model = RandomForestClassifier(random_state=1234, n_jobs = 10)
rf_model.fit(X_train, y_train)
# select an instance to explain
explain_file = 'src/java/org/apache/lucene/index/DocumentsWriter.java'
explain_index = list(X_test.index).index(explain_file)
print(explain_file, 'is likely to be defective with a probability of', rf_model.predict_proba(X_test.loc[X_test.index == explain_file, :])[0][1])
# construct a SHAP explainer
explainer = shap.KernelExplainer(rf_model.predict, X_test)
# generate shap values of testing data
shap_values = explainer.shap_values(X_test.iloc[explain_index, :])
# generate textual explanation of SHAP
for i in range(0, len(features)):
print(features[i], 'SHAP score =', shap_values[i])
# visualize a SHAP local explanation of the instance
shap.initjs()
shap.force_plot(explainer.expected_value,
shap_values,
X_test.iloc[explain_index,:])
Using 1337 background data samples could cause slower run times. Consider using shap.sample(data, K) or shap.kmeans(data, K) to summarize the background as K samples.
src/java/org/apache/lucene/index/DocumentsWriter.java is likely to be defective with a probability of 0.79
nCommit SHAP score = 0.07819745699327091
AddedLOC SHAP score = 0.038556469708306895
nCoupledClass SHAP score = 0.7216778858139951
LOC SHAP score = -0.0020817751184196154
CommentToCodeRatio SHAP score = -0.020344053851902744
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.
PyExplainer¶
PyExplainer [PTJ+21] is a rule-based model-agnostic technique that utilises a local rule-based regression model to learn the associations between the characteristics of the synthetic instances and the predictions from the black-box model. Given a black-box model and an instance to explain, PyExplainer performs four key steps to generate an instance explanation as follows:
First, PyExplainer generates synthetic neighbors around the instance to be explained using the crossover and mutation techniques
Second, PyExplainer obtains the predictions of the synthetic neighbors from the black-box model
Third, PyExplainer builds a local rule-based regression model
Finally, PyExplainer generates an explanation from the local model for the instance to be explained
For an interactive tutorial, please refer to the snippet below.
## Load Data and preparing datasets
# Import for Load Data
from os import listdir
from os.path import isfile, join
import pandas as pd
# Import for Split Data into Training and Testing Samples
from sklearn.model_selection import train_test_split
# Import for Construct a black-box model (Random Forests)
import statsmodels.api as sm
from statsmodels.formula.api import ols
from sklearn.ensemble import RandomForestClassifier
# Import for PyExplainer
from pyexplainer.pyexplainer_pyexplainer import PyExplainer
train_dataset = pd.read_csv(("../../datasets/lucene-2.9.0.csv"), index_col = 'File')
test_dataset = pd.read_csv(("../../datasets/lucene-3.0.0.csv"), index_col = 'File')
outcome = 'RealBug'
features = ['OWN_COMMIT', 'Added_lines', 'CountClassCoupled', 'AvgLine', 'RatioCommentToCode']
# process outcome to 0 and 1
train_dataset[outcome] = pd.Categorical(train_dataset[outcome])
train_dataset[outcome] = train_dataset[outcome].cat.codes
test_dataset[outcome] = pd.Categorical(test_dataset[outcome])
test_dataset[outcome] = test_dataset[outcome].cat.codes
X_train = train_dataset.loc[:, features]
X_test = test_dataset.loc[:, features]
y_train = train_dataset.loc[:, outcome]
y_test = test_dataset.loc[:, outcome]
# commits - # of commits that modify the file of interest
# Added lines - # of added lines of code
# Count class coupled - # of classes that interact or couple with the class of interest
# LOC - # of lines of code
# RatioCommentToCode - The ratio of lines of comments to lines of code
features = ['nCommit', 'AddedLOC', 'nCoupledClass', 'LOC', 'CommentToCodeRatio']
X_train.columns = features
X_test.columns = features
training_data = pd.concat([X_train, y_train], axis=1)
testing_data = pd.concat([X_test, y_test], axis=1)
## Construct a black-box model (Random Forests)
# random forests
rf_model = RandomForestClassifier(random_state=1234, n_jobs = 10)
rf_model.fit(X_train, y_train)
# construct a PyExplainer
py_explainer = PyExplainer(X_train=X_train,
y_train=y_train,
indep=X_train.columns,
dep=outcome,
blackbox_model=rf_model)
# random an instance that is predicted as defective for generating a visual example
# src/java/org/apache/lucene/index/DocumentsWriter.java
print('src/java/org/apache/lucene/index/DocumentsWriter.java is likely to be defective with the probability of', rf_model.predict_proba(X_test.loc['src/java/org/apache/lucene/index/DocumentsWriter.java':, :].iloc[0:1,:])[0][1])
# generate a PyExplainer rule-based local explanation of the instance
rule_based_local_explanation = py_explainer.explain(
X_explain=X_test.loc['src/java/org/apache/lucene/index/DocumentsWriter.java',:].to_frame().transpose(),
y_explain=pd.Series(bool(y_test.loc['src/java/org/apache/lucene/index/DocumentsWriter.java']),
index=['src/java/org/apache/lucene/index/DocumentsWriter.java'],
name=outcome),
search_function='crossoverinterpolation',
max_iter=1000,
max_rules=20)
# textual rule-based explanation
print(f"The rule-based explanation generated by PyExplainer has the following attributes\n{rule_based_local_explanation.keys()}")
# visual PyExplainer
py_explainer.visualise(rule_based_local_explanation, title="Why this file is defect-introducing ?")
src/java/org/apache/lucene/index/DocumentsWriter.java is likely to be defective with the probability of 0.79
The rule-based explanation generated by PyExplainer has the following attributes
dict_keys(['synthetic_data', 'synthetic_predictions', 'X_explain', 'y_explain', 'indep', 'dep', 'top_k_positive_rules', 'top_k_negative_rules', 'local_rulefit_model'])
Note
Parts of this chapter have been published by Jirayus Jiarpakdee, Chakkrit Tantithamthavorn, Hoa K. Dam, John Grundy: An Empirical Study of Model-Agnostic Techniques for Defect Prediction Models. IEEE Trans. Software Eng. (2021).
Suggested Readings¶
[1] Marco Túlio Ribeiro, Sameer Singh, Carlos Guestrin: “Why Should I Trust You?”: Explaining the Predictions of Any Classifier. KDD 2016: 1135-1144.
[2] Scott M. Lundberg, Gabriel G. Erion, Su-In Lee: Consistent Individualized Feature Attribution for Tree Ensembles. CoRR abs/1802.03888 (2018)