-
Notifications
You must be signed in to change notification settings - Fork 252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CFRL Feature condition autoencoder #573
Comments
Hi @HeyItsBethany3. It would be ideal if you can share some code/notebook and the data to have more context and identify the problem. Thank you! |
Hi @RobertSamoilescu. I can't show the code explicitly as it's confidential data. I'm predicting credit risk using a simple GBM model. The code is essentially exactly the same as the alibi example for the Adult CFLR dataset. https://docs.seldon.io/projects/alibi/en/latest/examples/cfrl_adult.html |
One reason for poor sparsity would be a bad autoencoder. If the reconstruction is bad, I would expect the counterfactuals to be far from the input. Some things to look for:
Let me know if this works and maybe you can share some plots with the training & validation error. |
@RobertSamoilescu Thanks so much, this was very helpful. Eventually I reduced the dimensions to hidden_dim=8 and latent_dim=4. This reduced sparsity from around 9 or 10 to around 6 and 7. It also reduced the distance between the counterfactual and the original instance. My dataset is unbalanced - there is more data for good credit risk than for bad credit risks, so the counterfactuals are better for the good risk instances. A couple more questions:
Thanks for your help! |
For example, if we have 3 numerical features and 2 categorical features, where the categorical features can take 3 and 4 values respectively, then the output of the heterogeneous HeAE would look like:
Note that everything I am describing here can be done with the
|
Thank you for this, this was so helpful. A couple more questions:
|
np.random.seed(0)
explanation = explainer.explain(X=X, Y_t=Y_t, C=C, diversity=True, num_samples=100, batch_size=10) As mentioned in the paper, at its core, the CF-RL is not really designed to generate a diverse counterfactual due to the determinstic nature of the DDPG. Although we can enforce some diversity by playing a bit with the conditional vector (please see the paper for a detailed explanation). The randomness comes from the construction of the conditional vector: alibi/alibi/explainers/backends/cfrl_tabular.py Lines 676 to 679 in e696906
alibi/alibi/explainers/backends/cfrl_tabular.py Lines 754 to 756 in e696906
2.a. The implementation in alibi/alibi/explainers/anchor_base.py Lines 784 to 789 in e696906
I believe you can play around with this and retain all the anchors that might be of interest. Furthermore, it is worth looking how those variables evolve throughout the runtime: alibi/alibi/explainers/anchor_base.py Lines 695 to 696 in e696906
2.b. The generation of potential anchor is performed in here: alibi/alibi/explainers/anchor_base.py Line 705 in e696906
If you go inside the function, you can see that each anchor is constructed by adding one feature at a time: alibi/alibi/explainers/anchor_base.py Lines 386 to 387 in e696906
alibi/alibi/explainers/anchor_base.py Lines 399 to 403 in e696906
I think you can exclude some features by removing them from the all_features variable defined here:alibi/alibi/explainers/anchor_base.py Line 378 in e696906
You will have to check that those changes do not break something else. For example, reducing the number of features will reduce the size of the maximum anchor. Thus, you may need to change the following line: alibi/alibi/explainers/anchor_base.py Line 699 in e696906
to something like: max_anchor_size = self.state['n_features'] - number_excluded features I haven't tested any of the suggested changes related to anchor, so there might be some potential issues that I did not consider. |
Thank you so much for this - I will implement it and let you know how it goes. |
Hi @RobertSamoilescu, I hope you are well! I am getting an error trying to fit the autoencoder:
Do you have any suggestions why this might be erroring? |
@HeyItsBethany3, I believe that you have two options:
For the second case, you should not worry about the range when applying the CFRL (i.e., the autoencoder may output a value of 8 for an ordinal feature, but the maximum is 7). It is guaranteed that the CFRL won't go outside the range and will consider only values from 0 to 7, where 0 is the minimum value. |
@RobertSamoilescu Thanks so much, this is really helpful. |
@HeyItsBethany3, please check issues #391 and #516. As you see in issue #391, AnchorTabular does not work with Categorical variables are expected to be labeled or one-hot encoded. In this situation, you can consider |
@RobertSamoilescu Thanks so much for your help. I decided to implement both approaches and compare them. I am a bit confused as to how to create the autoencoder given that this example encodes the categorical variable as part of the prediction function.
I have also tried using the original data to generate the trainset_outputs, but this gives a TypeError. |
@HeyItsBethany3, I don't understand exactly why you are trying to do with the import os
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow.keras as keras
from sklearn.preprocessing import LabelEncoder
from alibi.models.tensorflow.autoencoder import HeAE
from alibi.explainers.backends.cfrl_tabular import get_he_preprocessor
from alibi.models.tensorflow.cfrl_models import ADULTEncoder, ADULTDecoder
# some fake dataset
size = 500
age = np.random.randint(low=20, high=80, size=size)
height = 1.4 + 0.6 * np.random.rand(size)
weight = (100 * (height - 1) + 20 * (np.random.rand(size) - 0.5)).astype(np.int32)
gender = np.random.choice(['male', 'female'], size=size)
education = np.random.choice(['high-school', 'bachelors', 'masters'], size=size)
# construct dataframe
dataset = pd.DataFrame({
'age': age,
'gender': gender,
'height': height,
'education': education,
'weight': weight,
})
# define meta-data
feature_names = ['age', 'gender', 'height', 'education', 'weight']
category_map = {1: ['male', 'female'], 3: ['high-school', 'bachelors', 'masters']}
feature_types = {'age': int, 'weight': int}
numerical_ids = [i for i in range(len(feature_names)) if i not in category_map]
categorical_ids = list(category_map.keys())
# take a look at the dataset
dataset.head()
# make sure that the categorical variables are label encoded, otherwise the he-preprocessor does not work well
# don't have to do this if the categorical features are already label-encoded.
for cat_id in categorical_ids:
mapping = {val: i for (i, val) in enumerate(category_map[cat_id])}
dataset[feature_names[cat_id]].replace(mapping, inplace=True)
# make sure that the dataset is numpy array and not pandas
original_data = dataset.to_numpy()
# construct preprocessor and inv_preprocessor
heae_preprocessor, heae_inv_preprocessor = get_he_preprocessor(X=original_data,
feature_names=feature_names,
category_map=category_map,
feature_types=feature_types)
# we can preprocessdata
# - numerical features are standardized and place at the very begining
# - categorical feature are one-hot encoded and placed at the very end
trainset_input = heae_preprocessor(original_data).astype(np.float32)
# construct autoencoder targets for numerical features
trainset_outputs = {
# use the trainset because the numerical are standardized (mean 0, std 1) - this is important
# note that this is not the case with the original dataset.
'output_1': trainset_input[:, len(numerical_ids)]
}
# construct autoencode targets for categorical features.
for i, cat_id in enumerate(categorical_ids):
trainset_outputs.update({
# note that we use the label encoded format of the categorical variables.
f"output_{i+2}": original_data[:, cat_id]
})
trainset = tf.data.Dataset.from_tensor_slices((trainset_input, trainset_outputs))
trainset = trainset.shuffle(1024).batch(128, drop_remainder=True)
# Define autoencoder path and create dir if it doesn't exist.
heae_path = os.path.join("tensorflow", "credit_autoencoder")
if not os.path.exists(heae_path):
os.makedirs(heae_path)
# Define the heterogeneous auto-encoder
HIDDEN_DIM = 32
LATENT_DIM = 2
OUTPUT_DIMS = [len(numerical_ids)]
OUTPUT_DIMS += [len(category_map[cat_id]) for cat_id in categorical_ids]
heae = HeAE(encoder=ADULTEncoder(hidden_dim=HIDDEN_DIM, latent_dim=LATENT_DIM),
decoder=ADULTDecoder(hidden_dim=HIDDEN_DIM, output_dims=OUTPUT_DIMS))
# Define loss functions
he_loss = [keras.losses.MeanSquaredError()]
he_loss_weights = [1.]
# Add categorical losses
for cat_id in categorical_ids:
he_loss.append(keras.losses.SparseCategoricalCrossentropy(from_logits=True))
he_loss_weights.append(1./len(categorical_ids))
# Define metrics
metrics = {}
for i, _ in enumerate(categorical_ids):
metrics.update({f"output_{i+2}": keras.metrics.SparseCategoricalAccuracy()})
# Compile model.
heae.compile(optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss=he_loss,
loss_weights=he_loss_weights,
metrics=metrics)
if len(os.listdir(heae_path)) == 0:
# Fit and save autoencoder.
EPOCHS = 100
heae.fit(trainset, epochs=EPOCHS)
heae.save(heae_path, save_format="tf")
else:
heae = keras.models.load_model(heae_path, compile=False) Also, some other errors might come from the usage of pandas DataFrame instead of numpy array. Please consider using only numpy arrays with the utils functions. |
Hi @RobertSamoilescu Thank you for your example. Why are you label encoding before one hot encoding? And where does the one hot encoding come in? At the moment my 9 categorical variables are at the start and numerical variables at the end. My category map looks like this:
Is this the right format or should it be like:
If the data is already one hot encoded, do the categories in the map need to be in the correct order that the columns respond to? I'm not too sure how to create this. Thanks so much for your help, this is really mind boggling me. |
The dataset is expected to be in a raw format as in the example you linked to:
The The
Note that the training of the autoencoder has to be done in this format. Thus, the numerical features will be standardized and categorical features will be one-hot encoded. All the standardization and one-hot encoding is performed by Now, the answer to your question regarding the preprocessing in the explainer. The answer is no. The dataset is not preprocessed and passed to the model. Internally, what is happening is the following:
Another important note is that you can perform any kind of preprocessing in the def my_predictor(X):
X = model_preprocessor(X) # do whatever preprocessing your model requires
return model(X) |
Ahh okay thank you for this, this makes sense! Do the categories in the category map need to be in the correct order as they are encoded eg. if sex is encoded as 0 male and 1 female, would the category map need to be Why did you suggest that I needed to use one hot encoding in this thread? Do I still need to do this? Thank you |
@HeyItsBethany3, yes, the order of the values should match the label encoding, thus the correct option is To answer your second question, I suggested to use the one-hot encoding (OHE) because that's what's happening under the hood. I didn't mean that you should transform the categorical variables to OHE explicitly. Apologize for the confusion. |
@RobertSamoilescu
I've tried to follow all the code and understand the issue but I have no idea. Thank you |
@RobertSamoilescu I have tried to implement the same thing using your example above and am getting the same error. Here is the code:
Thank you! |
@HeyItsBethany3, the output of the model should be 2 dimensional,
Note that the result of the |
Thanks for this! This worked for me, I used the format
I'm stuck on one error explaining the instance. Feels so close to working! I've tried every combination I can think of but still keeping getting this condition error.
This is the error:
Thank you |
@HeyItsBethany3, I would try to call the Also, if you are explaining a single instance, make sure that you have the batch dimension too, so the dimension would be To avoid another potential issue, make sure that |
Hi @RobertSamoilescu, Thank you so much for your help! I managed to fix this by debugging using all your suggestions.
However, I am not sure how to find original one hot encodings. When I print the output of
It's almost like the last rows should be the one hot encoded versions of the categorical variables but there are no 1's anywhere. |
I believe I know what's happening, but I cannot be entirely sure without access to the data, preprocessor and autoencoder. I will try to explain it with a simple example. Consider that we generate the following dataset containing only categorical features: X1 = np.random.randint(0, 5, (10, 4))
category_map = {i: list(range(5)) for i in range(X1.shape[1])} In this case, Now, behind the scene, the from sklearn.preprocessing import OneHotEncoder
cat_transf = OneHotEncoder(
categories=[range(len(x)) for x in category_map.values()],
handle_unknown="ignore"
) Now we can fit the cat_transf = cat_transf.fit(X1) Intentionally I will define a data instance for which the values of the categorical features are not in the X2 = np.random.randint(5, 10, (1, 4)) In this case, the values of the categorical features will sampled from the set X2 = np.random.randint(5, 10, (1, 4))
X2_enc = cat_transf.transform(X2).todense()
print(X2_enc) # this prints [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]] This behavior is due to In alibi, the definition of the categorical transformation is defined in here. I believe that in you case this happened because you defined the category map based only on the training dataset. It might be the case for high-dimensional inputs, that the training dataset might not cover all the values from the test, which will result in an edge case as I described above. For example, if your training dataset is: X1 = np.array([
[1, 0, 2, 3],
[0, 1, 3, 2]
[1, 1, 2, 2]
]) and you defined the category_map = {i: np.unique(X1[:, i]) for i in range(X1.shape[1])} this will result in Hope this helps! |
@RobertSamoilescu Thank you so much! This worked! I have the counterfactual method fully working now. I am fitting the autoencoder as you suggested above and the error is very high. Previously with the small model (12 features) my numerical reconstruction error was around 5. Now with the large model (of 120 features), the error is 2381. The code is to calculated the reconstruction error for one instance is below. Then I take the average over the whole of the test data set.
Thank you so much for your help! Feeling very out of my depth. |
@HeyItsBethany3, It seems like when you are computing the error, you take the difference between the non-standardized numerical feature (i.e., in the original input format) and the standardized output of the autoencoder. This is more likely the source of your large error. It is very likely from your results that the original numerical features might have large ranges (e.g., [0, 10000]), while the output of the autoencoder has standardized numerical features (mean 0 and standard deviation of 1). If you want to compute the error in the original input format, you need to apply a post-processing operation to the output of the autoencoder. Note that this can be easily done using the A better way, which I recommend, is to compute the error in the standardized format. So instead of taking as a reference Now, if after you've done all this correction you still get a large error then you might want to tune the autoencoder a bit. I think a decent way to get a feeling of what encoding size you need is to play a bit with a dimensionality reduction algorithm like PCA. Just see how many components you need to have a decent reconstruction, and that should be a decent value for you to start with. Eventually, you will try to reduce the size of the encoding (if possible) because an autoencoder should be a bit stronger than a naive PCA. If you still see that the error is high, it might be the case that there is no correlation between your feature, so you cannot reduce the dimensionality. In this case, you can avoid training the autoencoder. Please see the experiment section in the paper in which we also present the results when no autoencoder is used. If you decide to go for no autoencoder, you will still need to simulate that you have an autoencoder. In this case, the encoding should be an invertible function Hope this helps! |
Thank you for this! I fixed that issue and the error reduced significantly. It's now around 130. What kind of reconstruction error/categorical accuracy would you say is reasonable? Looking at PCA is a great suggestion, I'll do this. It seems straightforward this way if we only have one dimension in the autoencoder, but there are 2 dimensions. How would you set both of them/test the relationship between the two? Thank you! |
Btw, I just realized that you probably want to include that For the categorical reconstruction, try to do your best. In the end, the reconstruction loss is what influences the sparsity of the counterfactuals. Better reconstuction is more likely to produce counterfactual that are closer to the input instance. I would go for smething >0.9. If you continue to stuggle with the autoencoder, I suggest you try the version without autoencoder. I've mentioned 2 ways in which you can do this in the previous comment. |
Thanks so much for all of your advice. I've used the PCA method and found some good dimensions. Why would you think the autoencoder would not be performing as well? Is it possible to use PCA instead? At the moment I'm removing any outliers (data points with high autoencoder reconstruction loss) to see if this improves the autoencoder. Can you think of any other factor which might be hindering the autoencoder? Thank you |
@RobertSamoilescu Here is the source code:
Thank you! |
@RobertSamoilescu Would really appreciate your help on this :) Thank you! |
@HeyItsBethany3 , Whether you use PCA or no dimensionality reduction (i.e. tanh/tanh^-1), I recommend you to wrap the encoder/decoder procedure in a For the PCA you can use any implementation and extract are the principal components/eigen vectors. In that case the actor will output the coefficients corresponding to each principal component. All you have to do is multiply them accordingly to get the reconstruction. Regarding the poor AE performance. If you are entirely sure that the AE training pipeline is correct, it is possibile that you overfit on the training data. Have you plotted the error on the train & validation set? If the error is large for the validation and not for training then you need to apply some regularization or reduce the size of the networks. |
@RobertSamoilescu I've compared the AE error, the training reconstruction loss is around 0.92 and the test error is 0.96 so I don't think this is overfit. What do you think? What do you mean by reduce the size of the networks - is this lower the number of dimensions (eg. latent dim)? I've implemented the encoder & decoder using keras models but I'm still getting the same error as above. I'll include some more code that I've used. Thank you Option 1:
Option 2:
TanhEncoder & TanhDecoder:
|
@HeyItsBethany3, And I got the following results:
Unfortunately, for the AE case, I cannot help you if you don't share your code and data. |
There has been a minor update of the gist. |
@HeyItsBethany3, |
@RobertSamoilescu Thank you this helped a lot! My error is a lot better now and I realised I needed many more epochs. Are the target variables for the categorical parts still correct? ie. Should we still be using I also noticed that for the loss we have a combination of mean squared error loss (for numerical variables) and cross-entropy loss (categorical). But in the metrics we only pass in the cross-entropy loss. Is this correct? |
The targets for the categorical variables is correct since the order provided by keys in the The metrics are used just to provide a more interpretable evaluation of the training procedure. It would be a bit hard to say how good the reconstruction for the categorical features is just by looking at the cross-entropy. Accuracy gives us a better understanding on how good the reconstruction is for categorical variables. |
@RobertSamoilescu Thank you! |
Well, I guess you can always combine the two losses (MSE + CE), but personally I would prefer to keep them separated. One plot for all numerical features and another one for categorical ones. In that way you can understand better where the AE might struggle. |
Thanks. If I added a metrics array like (with the above code):
Would this calculate MSE for the numerical variables and then calculate sparse categorical accuracy for each categorical variable, then combine the two into a plot? |
Hi Team,
Firstly I want to say thank you so much for implementing a truly model-agnostic method for counterfactuals! I've been searching for many months now for a counterfactual tool I can easily use for GBMs and it made my day when I found this method.
I have implemented the CFRL method on a simple model with 12 features, but the counterfactuals are not very sparse, usually sparsity is around 9 or 10. I tried specifying more features to be immutable to enforce sparsity which is not ideal. Is there a way to add feature conditioning into the autoencoder? I am concerned that if I train the autoencoder on 12 variables and then fix 6 variables for instance, the autoencoder may have found important patterns in the variables that I then remove at a later stage.
Do you have any other advice on how to improve sparsity?
I have changed the loss function coefficients but results didn't vary too much. I don't have much experience with tuning autoencoders too. Do you have any advice on the best parameters to start with or focus on, that would make the most difference?
Thank you so much for your help!
Bethany
The text was updated successfully, but these errors were encountered: