The machine learning models have become quite common nowadays and people are using them in almost all domains (finance, insurance, education, etc) to make the first round of decisions. It has become quite common to expect explanations of a prediction made by the model. Python has a list of libraries (SHAP, lime, treeinterpreter, interpret-ml, interpret-text, eli5, etc) which can help us understand why the particular prediction was made and how much each data feature contributed to the prediction. The explanation of the model also gives confidence to the developer and others about the reliability of the model letting them know whether the features which should have ideally contributed to explanations are the only feature contributing or not. We have already created various tutorials on libraries mentioned earlier about machine learning model prediction interpretation (see references section for links to them). As a part of this tutorial, we'll be concentrating on how to use another Python library named dice-ml
designed to explain machine learning predictions. It's open-source and designed by the Microsoft research team.
The DiCE is based on a concept of generating counterfactual example to our original example. It'll generate other examples that will have the majority of feature values almost the same as our original example with few values tweaked which will result in the model predicting an opposite class than the one it has predicted currently. As an example, let’s consider that a person applies for a loan at a bank that uses the ML algorithm to predict whether his/her application will be approved. After a person applies online by filling the loan application form, the model predicts that his loan will be rejected. Now just saying the person that his loan was rejected due to a low credit score won't help him/her as he'll need to understand what he/she could do that will result in his/her application getting approved next time. The DiCE generates counterfactual examples in this situation which will generate other examples that will have a majority of feature values same as the applicant with few changes like increasing monthly income, showing collateral with high value, etc will result in his loan getting approved. This will guide a person taking steps in a direction that will get him/her loan approved next time. We can even generate counterfactual examples of the same class as well with DiCE.
The dice-ml
is mainly used to generate counterfactual examples for binary classification problems as of now. We'll be explaining how we can generate counterfactual examples for classification problems with Keras/Tensorflow and Pytorch models. We'll also try to generate counterfactual example with regression tasks. Please make a note that this tutorial expects that reader has a basic knowledge of Keras, scikit-learn metrics, and Pytorch as we'll be building on them and won't be spending much time in the detailed explanation on them due to this assumption.
We'll start by importing the necessary libraries.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore")
pd.set_option("display.max_columns", 35)
import dice_ml
We'll be using the below-mentioned datasets when explaining the usage of dice-ml
.
We have first loaded both the datasets and even printed their feature description. We have even loaded the dataset as a pandas dataframe and printed the first few examples.
from sklearn.datasets import fetch_california_housing, load_boston
boston = load_boston()
boston_df = pd.DataFrame(data=boston.data, columns=boston.feature_names)
boston_df["Price"] = boston.target
boston_df.head()
from sklearn.datasets import load_breast_cancer
breast_cancer = load_breast_cancer()
breast_cancer_df = pd.DataFrame(data=breast_cancer.data, columns=breast_cancer.feature_names)
breast_cancer_df["TumorType"] = breast_cancer.target
breast_cancer_df.head()
As a part of this section, we'll explain how we can use dice-ml
to generate counterfactual examples for Keras/Tensorflow models. We'll be explaining both regression and classification models.
We'll start by dividing the Boston housing regression dataset into the train (90%) and test (10%) sets.
from sklearn.model_selection import train_test_split
print("Dataset Size : ", boston.data.shape, boston.target.shape)
X_train, X_test, Y_train, Y_test = train_test_split(boston.data, boston.target,
train_size=0.90,
random_state=123)
print("Train/Test Sizes : ",X_train.shape, X_test.shape, Y_train.shape, Y_test.shape)
We have then created a simple regression model using Keras which has 3 hidden layers with all of them having 50 units. We have used relu
as the activation function in all hidden layers. Our output layer is a single unit which will be predicting the price of a house.
import tensorflow.keras
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
model = Sequential([
Dense(50, activation="relu", input_shape=(len(boston.feature_names), )),
Dense(50, activation="relu"),
Dense(50, activation="relu"),
Dense(1),
])
model.summary()
We have now complied our model with adam
as an optimizer and mean_squared_error
as loss function which it'll be trying to reduce. We'll also be calculating mean absolute error metrics for each iteration through data.
model.compile(optimizer="adam", loss="mean_squared_error", metrics=["mae"])
Below we are fitting our model to train data with a batch size of 8 for 100 epochs.
%%time
history = model.fit(X_train, Y_train, batch_size=8, epochs=100, verbose=0)
We have now printed our model performance by evaluating the r2 score and mean squared error on train and test datasets.
If you are interested in learning about various metrics available with sklearn then please feel free to check our tutorial on the same.
from sklearn.metrics import mean_squared_error, r2_score
print("Train MSE : %.2f"%mean_squared_error(Y_train, model.predict(X_train)))
print("Test MSE : %.2f"%mean_squared_error(Y_test, model.predict(X_test)))
print("Train R2 Score : %.2f"%r2_score(Y_train, model.predict(X_train)))
print("Test R2 Score : %.2f"%r2_score(Y_test, model.predict(X_test)))
The process for creating a counterfactual explanation using dice-ml
consists of a few simple steps as mentioned below.
dice_ml.Data()
instance with background data.dice_ml.Model()
instance with actual trained model.dice_ml.Dice()
instance with data and model instances created in the previous two steps.generate_counterfactuals()
method of Dice instance by giving it sample as a dictionary for which we want to generate counterfactual examples.Below we have created an instance of dice_ml.Data()
by giving it our Boston dataframe, list of column continuous feature column names, and target column name. We have then created an instance of dice_ml.Model()
by giving it our Keras model as input. As of last we have created a Dice instance by giving data and model as input.
d = dice_ml.Data(dataframe=boston_df, continuous_features=boston.feature_names.tolist(), outcome_name='Price')
m = dice_ml.Model(model=model, backend="TF2")
# initiate DiCE
exp = dice_ml.Dice(d, m)
exp
Below we have taken a random sample from the test set and generated a dictionary of sample for which counterfactual explanations will be generated.
import random
idx = random.randint(1, len(X_test))
print("Actual Price : %.2f"%Y_test[idx])
sample = dict(zip(boston.feature_names, X_test[idx]))
sample
We can generate counterfactual examples by using the generate_counterfactuals()
method of Dice instance as explained below. we have called method by giving it sample from the previous step, a number of counterfactual to generate and desired class of outcome. We are trying to generate other same samples as our given example for this regression problem. We'll be generating the opposite class when trying classification problems. Below we have given some of the important parameters of the generate_counterfactuals()
method.
generate_counterfactuals
- It accepts a dictionary of our test sample where keys are feature names and values are feature values.total_CFs
- It accepts integer specifying how many counterfactual examples, we want to generate.desired_class
-It accepts integer 0 or 1 and string opposite
. The 0 will generate the opposite class and 1 will generate the same class counterfactual examples. The default is opposite
.proximity_weight
- It accepts float value specifying how close counterfactuals are to the original query sample. The higher value means more close. The default is 0.5.diversity_weight
- It accepts float value specifying how diverse counterfactuals are from the original query sample. The higher value means more diversity. The default is 1.0.algorithm
- It accepts integer specifying algorithm of finding counterfactuals.DiverseCF
RandomInitCF
yloss_type
- It accepts a string specifying a loss that will be optimized (reduced).l2_loss
log_loss
hinge_loss
- DefaultBelow we have generated an explanation instance with 4 counterfactual explanations with the same features as that of our original query sample.
dice_exp = exp.generate_counterfactuals(sample, total_CFs=4, desired_class=1)
The explanation instance has a list of methods that can be used to visualize counterfactual examples generated. Below we have called visualize_as_dataframe()
to print counterfactual examples. It prints the original sample and counterfactual examples generated so that we can compare them.
In the next cell, we have again called the same method with parameter show_only_changes
set to True which will only show the difference in feature values from the original sample.
The explanation instance also has a method named visualize_as_list()
which will frame results as Python lists.
dice_exp.visualize_as_dataframe()
dice_exp.visualize_as_dataframe(show_only_changes=True)
dice_exp.visualize_as_list()
Our classification example starts by dividing the breast cancer dataset into the train (90%) and test (10%) sets.
from sklearn.model_selection import train_test_split
print("Dataset Size : ", breast_cancer.data.shape, breast_cancer.target.shape)
X_train, X_test, Y_train, Y_test = train_test_split(breast_cancer.data, breast_cancer.target,
train_size=0.90,
stratify=breast_cancer.target,
random_state=123)
print("Train/Test Sizes : ",X_train.shape, X_test.shape, Y_train.shape, Y_test.shape)
We have now generated a simple Keras model with 3 hidden layers of 50 units each and relu
activation. We have kept the output layer as sigmoid with 1 unit as it'll generate a probability of the tumor being malignant or benign. We have then compiled the model and trained it on train data with 10 epochs.
model = Sequential([
Dense(50, activation="relu", input_shape=(len(breast_cancer.feature_names), )),
Dense(50, activation="relu"),
Dense(50, activation="relu"),
Dense(1, activation="sigmoid"),
])
model.summary()
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
%%time
history = model.fit(X_train, Y_train, batch_size=8, epochs=10, verbose=0)
Below we have calculated train and test accuracy as well as classification report of test data.
from sklearn.metrics import accuracy_score, classification_report
test_preds = [0 if pred< 0.5 else 1 for pred in model.predict(X_test).flatten()]
train_preds = [0 if pred< 0.5 else 1 for pred in model.predict(X_train).flatten()]
print("Train Accuracy : %.2f"%accuracy_score(Y_train, train_preds))
print("Test Accuracy : %.2f"%accuracy_score(Y_test, test_preds))
print("\nTest Classification Report : ")
print(classification_report(Y_test, test_preds))
We have now followed the same process which we followed in our previous example to create a Dice instance. We have first created an instance of Data
and Model
. We have then created an instance of Dice using that Data and Model instances which will be used to generate counter factual explanations.
d = dice_ml.Data(dataframe=breast_cancer_df,
continuous_features=breast_cancer.feature_names.tolist(),
outcome_name='TumorType')
m = dice_ml.Model(model=model, backend="TF2")
# initiate DiCE
exp = dice_ml.Dice(d, m)
Below we have taken a random sample from the test set and generated a query instance dictionary from it.
import random
idx = random.randint(1, len(X_test))
sample = dict(zip(breast_cancer.feature_names, X_test[idx]))
print("Actual Class : %d"%Y_test[idx])
sample
We have now generated 4 counterfactual examples using the generate_counterfactuals()
method. We have then printed them using visualize_as_dataframe()
. Please make a note that it might not be possible for a model to get the same number of counter factual explanations all the time.
dice_exp = exp.generate_counterfactuals(sample, total_CFs=4)
dice_exp.visualize_as_dataframe()
Please feel free to compare how changes in particular feature values are changing the prediction class.
dice_exp.visualize_as_dataframe(show_only_changes=True)
As a part of this section, we'll explain how we can use dice-ml
to generate counterfactual explanations for Pytorch models.
As usual, we have started by dividing the dataset into the train (90%) and test (10%) sets.
from sklearn.model_selection import train_test_split
print("Dataset Size : ", boston.data.shape, boston.target.shape)
X_train, X_test, Y_train, Y_test = train_test_split(boston.data, boston.target,
train_size=0.90,
random_state=123)
print("Train/Test Sizes : ",X_train.shape, X_test.shape, Y_train.shape, Y_test.shape)
We have now created a simple Pytorch model with three hidden layers each having 50 units. We are using relu
as the activation function in all hidden layers.
import torch
import torch.nn as neural_net
from torch import optim
class MultiLayerPerceptron(neural_net.Module):
def __init__(self):
super().__init__()
self.layer1 = neural_net.Linear(len(boston.feature_names), 50)
self.layer2 = neural_net.Linear(50, 50)
self.layer3 = neural_net.Linear(50, 50)
self.output_layer = neural_net.Linear(50, 1)
self.relu = neural_net.ReLU()
def forward(self, input_data):
x = self.relu(self.layer1(input_data))
x = self.relu(self.layer2(x))
x = self.relu(self.layer3(x))
output = self.output_layer(x)
return output.flatten()
Below we have initialized the model, Adam optimizer, and mean squared error loss.
model = MultiLayerPerceptron()
optimizer = optim.Adam(params=model.parameters())
mse_loss = neural_net.MSELoss()
Below we have included logic for training our model on train data. We are training whole train data for 20 epochs with a batch size of 1 sample.
%%time
X_train, Y_train = torch.tensor(X_train, dtype=torch.float32), torch.tensor(Y_train, dtype=torch.float32)
print("MSE Loss Before Training : %.2f"% mse_loss(model(X_train).flatten(), Y_train))
n, c = X_train.shape
batch_size = 1
for epoch in range(20):
for i in range((n - 1) // batch_size + 1):
start_i = i * batch_size
end_i = start_i + batch_size
xb = X_train[start_i:end_i]
yb = Y_train[start_i:end_i]
pred = model(xb)
loss = mse_loss(pred, yb)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print("MSE Loss After Training : %.2f"% mse_loss(model(X_train).flatten(), Y_train))
We have now evaluated mean squared error and r2 score on test and train sets.
from sklearn.metrics import mean_squared_error, r2_score
with torch.no_grad():
X_test = torch.tensor(X_test, dtype=torch.float32)
test_preds = model(X_test)
train_preds = model(X_train)
print("Train MSE : %.2f"%mean_squared_error(Y_train, train_preds.numpy()))
print("Test MSE : %.2f"%mean_squared_error(Y_test, test_preds.numpy()))
print("Train R2 Score : %.2f"%r2_score(Y_train, train_preds.numpy()))
print("Test R2 Score : %.2f"%r2_score(Y_test, test_preds.numpy()))
We have now created a Dice instance which using a Data instance created using the Boston data frame and a Model instance generated using the Pytorch model. This instance will be used to generate counter factual explanations.
d = dice_ml.Data(dataframe=boston_df, continuous_features=boston.feature_names.tolist(), outcome_name='Price')
m = dice_ml.Model(model=model, backend="PYT")
# initiate DiCE
exp = dice_ml.Dice(d, m)
Below we have taken a random test sample and generated a query instance dictionary for which counter factual explanations will be generated.
import random
idx = random.randint(1, len(X_test))
print("Actual Price : %.2f"%Y_test[idx])
sample = dict(zip(boston.feature_names, X_test[idx].numpy()))
sample
Now we have generated 4 counterfactual examples for our sample test instance dictionary using the Dice instance. We have generated counterfactual explanations which are almost the same as our sample as its regression problem.
dice_exp = exp.generate_counterfactuals(sample, total_CFs=4)
dice_exp.visualize_as_dataframe()
dice_exp.visualize_as_dataframe(show_only_changes=True)
For explaining the classification task Pytorch model with dice-ml
, we'll start by dividing the breast cancer dataset into the train (90%) and test (10%) sets.
from sklearn.model_selection import train_test_split
print("Dataset Size : ", breast_cancer.data.shape, breast_cancer.target.shape)
X_train, X_test, Y_train, Y_test = train_test_split(breast_cancer.data, breast_cancer.target,
train_size=0.90,
stratify=breast_cancer.target,
random_state=123)
print("Train/Test Sizes : ",X_train.shape, X_test.shape, Y_train.shape, Y_test.shape)
We have now created a simple Pytorch model of 3 hidden layers with each having 50 units. We have used relu
as activation for each layer. Our last layer is sigmoid which will output probability between 0-1 predicting tumor type (malignant or benign).
class MultiLayerPerceptron(neural_net.Module):
def __init__(self):
super().__init__()
self.layer1 = neural_net.Linear(len(breast_cancer.feature_names), 50)
self.layer2 = neural_net.Linear(50, 50)
self.layer3 = neural_net.Linear(50, 50)
self.output_layer = neural_net.Linear(50, 1)
self.relu = neural_net.ReLU()
self.sigmoid = neural_net.Sigmoid()
def forward(self, input_data):
x = self.relu(self.layer1(input_data))
x = self.relu(self.layer2(x))
x = self.relu(self.layer3(x))
output = self.sigmoid(self.output_layer(x))
return output.flatten()
Below we have initialized our model, Adam optimizer, and binary cross-entropy loss.
model = MultiLayerPerceptron()
optimizer = optim.Adam(params=model.parameters())
bce_loss = neural_net.BCELoss()
We have now written logic for training. We are looping through train data for 10 epochs with a batch size of 1.
%%time
X_train, Y_train = torch.tensor(X_train, dtype=torch.float32), torch.tensor(Y_train, dtype=torch.float32)
print("Binary Cross Entropy Loss Before Training : %.2f"% bce_loss(model(X_train).flatten(), Y_train))
n, c = X_train.shape
batch_size = 1
for epoch in range(20):
for i in range((n - 1) // batch_size + 1):
start_i = i * batch_size
end_i = start_i + batch_size
xb = X_train[start_i:end_i]
yb = Y_train[start_i:end_i]
preds = model(xb)
loss = bce_loss(preds, yb)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print("Binary Cross Entropy Loss After Training : %.2f"% mse_loss(model(X_train).flatten(), Y_train))
Below we have generated the accuracy of the model on train and test data. We have even printed a classification report of model performance on test data.
from sklearn.metrics import accuracy_score, classification_report
with torch.no_grad():
X_test = torch.tensor(X_test, dtype=torch.float32)
test_preds = [0 if pred< 0.5 else 1 for pred in model(X_test)]
train_preds = [0 if pred< 0.5 else 1 for pred in model(X_train)]
print("Train Accuracy : %.2f"%accuracy_score(Y_train, train_preds))
print("Test Accuracy : %.2f"%accuracy_score(Y_test, test_preds))
print("\nTest Classification Report : ")
print(classification_report(Y_test, test_preds))
We have now created an instance of Dice exactly the same way as explained in previous examples.
d = dice_ml.Data(dataframe=breast_cancer_df,
continuous_features=breast_cancer.feature_names.tolist(), outcome_name='TumorType')
m = dice_ml.Model(model=model, backend="PYT")
# initiate DiCE
exp = dice_ml.Dice(d, m)
We have now taken a random sample from the test set as a query instance for which we'll generate counterfactual explanations.
import random
idx = random.randint(1, len(X_test))
sample = dict(zip(breast_cancer.feature_names, X_test[idx].numpy()))
print("Actual Class : %d"%Y_test[idx])
sample
We have now tried to generate 4 counterfactual explanations for our query instance.
dice_exp = exp.generate_counterfactuals(sample, total_CFs=4)
dice_exp.visualize_as_dataframe()
dice_exp.visualize_as_dataframe(show_only_changes=True)
This ends our small tutorial explaining how we can use dice-ml
API for generating counterfactual explanations. Please feel free to let us know your views in the comments section.
If you are more comfortable learning through video tutorials then we would recommend that you subscribe to our YouTube channel.
When going through coding examples, it's quite common to have doubts and errors.
If you have doubts about some code examples or are stuck somewhere when trying our code, send us an email at coderzcolumn07@gmail.com. We'll help you or point you in the direction where you can find a solution to your problem.
You can even send us a mail if you are trying something new and need guidance regarding coding. We'll try to respond as soon as possible.
If you want to