Advanced options to customize Counterfactual Explanations
Here we discuss a few ways to change DiCE’s behavior.
Train a custom ML model
Changing feature weights that decide relative importance of features in perturbation
Trading off between proximity and diversity goals
Selecting the features to change
[ ]:
from numpy.random import seed
# import DiCE
import dice_ml
from dice_ml.utils import helpers # helper functions
# Tensorflow libraries
import tensorflow as tf
# supress deprecation warnings from TF
tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
[ ]:
%load_ext autoreload
%autoreload 2
Loading dataset
We use “adult” income dataset from UCI Machine Learning Repository (https://archive.ics.uci.edu/ml/datasets/adult). For demonstration purposes, we transform the data as detailed in dice_ml.utils.helpers module.
[ ]:
dataset = helpers.load_adult_income_dataset()
dataset.head()
[ ]:
d = dice_ml.Data(dataframe=dataset, continuous_features=['age', 'hours_per_week'], outcome_name='income')
1. Loading a custom ML model
Below, we use an Artificial Neural Network based on Tensorflow framework.
[ ]:
# seeding random numbers for reproducability
seed(1)
# from tensorflow import set_random_seed; set_random_seed(2) # for tf1
tf.random.set_seed(1)
[ ]:
backend = 'TF'+tf.__version__[0] # TF1
# provide the trained ML model to DiCE's model object
ML_modelpath = helpers.get_adult_income_modelpath(backend=backend)
# Step 2: dice_ml.Model
m = dice_ml.Model(model_path=ML_modelpath, backend=backend)
Generate diverse counterfactuals
[ ]:
# initiate DiCE
exp = dice_ml.Dice(d, m)
[ ]:
# query instance in the form of a dictionary; keys: feature name, values: feature value
query_instance = {'age': 22,
'workclass': 'Private',
'education': 'HS-grad',
'marital_status': 'Single',
'occupation': 'Service',
'race': 'White',
'gender': 'Female',
'hours_per_week': 45}
We now generate counterfactuals for this input. This may take some time to run–the optimization takes more time in tensorflow2.
[ ]:
# generate counterfactuals
dice_exp = exp.generate_counterfactuals(query_instance, total_CFs=4, desired_class="opposite")
[ ]:
# visualize the resutls
dice_exp.visualize_as_dataframe(show_only_changes=True)
2. Changing feature weights
It may be the case that some features are harder to change than others (e.g., education level is harder to change than working hours per week). DiCE allows input of relative difficulty in changing a feature through specifying feature weights. A higher feature weight means that the feature is harder to change than others. For instance, one way is to use the mean absolute deviation from the median as a measure of relative difficulty of changing a continuous feature.
Median Absolute Deviation (MAD) of a continuous feature conveys the variability of the feature, and is more robust than standard deviation as is less affected by outliers and non-normality. The inverse of MAD would then imply the ease of varying the feature and is hence used as feature weights in our optimization to reflect the difficulty of changing a continuous feature. By default, DiCE computes this internally and divides the distance between continuous features by the MAD of the feature’s values in the training set. Let’s see what their values are by computing them below:
[ ]:
# get MAD
mads = d.get_mads(normalized=True)
# create feature weights
feature_weights = {}
for feature in mads:
feature_weights[feature] = round(1/mads[feature], 2)
print(feature_weights)
The above feature weights encode that changing age is approximately seven times more difficult than changing categorical variables, and changing hours_per_week is approximately three times more difficult than changing age. Of course, this may sound odd, since a person cannot change their age. In this case, what it’s reflecting is that there is a higher diversity in age values than hours-per-week values. Below we show how to over-ride these weights to assign custom user-defined weights.
Now, let’s try to assign unit weights to the continuous features and see how it affects the counterfactual generation. DiCE allows this through feature_weights parameter.
[ ]:
# assigning equal weights
feature_weights = {'age': 1, 'hours_per_week': 1}
[ ]:
# generate counterfactuals
dice_exp = exp.generate_counterfactuals(query_instance, total_CFs=4, desired_class="opposite",
feature_weights=feature_weights)
[ ]:
# visualize the resutls
dice_exp.visualize_as_dataframe(show_only_changes=True)
Note that we transform continuous features and one-hot-encode categorical features to fall between 0 and 1 in order to handle relative scale of features. However, this also means that the relative ease of changing continuous features is higher than categorical features when the total number of continuous features are very less compared to the total number of categories of all categorical variables combined. This is reflected in the above table where continuous features (age and hours_per_week) have been varied to reach their extreme values (range of age: [17, 90]; range of hours_per_week: [1, 99]) for most of the counterfactuals. This is the reason why the distances are divided by a scaling factor. Deviation from the median provides a robust measure of the variability of a feature’s values, and thus dividing by the MAD allows us to capture the relative prevalence of observing the feature at a particular value (see our paper for more details).
3. Trading off between proximity and diversity goals
We acknowledge that not all counterfactual explanations may be feasible for a user. In general, counterfactuals closer to an individual’s profile will be more feasible. Diversity is also important to help an individual choose between multiple possible options. DiCE allows tunable parameters proximity_weight (default: 0.5) and diversity_weight (default: 1.0) to handle proximity and diversity respectively. Below, we increase the proximity weight and see how the counterfactuals change.
[ ]:
# change proximity_weight from default value of 0.5 to 1.5
dice_exp = exp.generate_counterfactuals(query_instance, total_CFs=4, desired_class="opposite",
proximity_weight=1.5, diversity_weight=1.0)
[ ]:
# visualize the resutls
dice_exp.visualize_as_dataframe(show_only_changes=True)
As we see from above table, both continuous and categorical features are more closer to the original query instance and the counterfactuals are also less diverse than before.
[ ]:
# visualize the resutls
dice_exp.visualize_as_dataframe(show_only_changes=True)