Search code examples
python-3.xscikit-learnnlppipelineshap

Error getting prediction explanation using shap_values when using scikit-learn pipeline?


I am building an NLP model to predict language type (C/C++/C#/Python...) for a given code. Now I need to provide an explanation for my model prediction. For example the following user_input is written in Java and the model is predicting that, but I need to show the users why it predicts so.

I am using shap_values to achieve this. For some reason, the following code results in an error (I have added the error at the bottom). Please advise how can I get shap_values and plots for my model predictions.

Link to data: https://sharetext.me/bd68ryvzi0

Code:

import pandas as pd

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import FunctionTransformer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline

# Loading Data:
DATA_PATH = r"sample.csv"

data = pd.read_csv(DATA_PATH, dtype='object')
data = data.convert_dtypes()
data = data.dropna()
data = data.drop_duplicates()

# Train/Test split
X, y = data.content, data.language
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)

# Model params to match:
# 1. Variable and module names, words in a string, keywords: [A-Za-z_]\w*\b
# 2. Operators: [!\#\$%\&\*\+:\-\./<=>\?@\\\^_\|\~]+
# 3. Tabs, spaces and Brackets: [ \t\(\),;\{\}\[\]`"']
# with the following regex:
token_pattern = r"""(\b[A-Za-z_]\w*\b|[!\#\$%\&\*\+:\-\./<=>\?@\\\^_\|\~]+|[ \t\(\),;\{\}\[\]`"'])"""


def preprocess(x):
 """ Clean up single-character variable names or ones constituted of a sequence of the same character """
 return pd.Series(x).replace(r'\b([A-Za-z])\1+\b', '', regex=True)\
 .replace(r'\b[A-Za-z]\b', '', regex=True)


# Pipe steps:
# Define a transformer:
transformer = FunctionTransformer(preprocess)
# Perform TF-IDF vectorization with our token pattern:
vectorizer = TfidfVectorizer(token_pattern=token_pattern, max_features=3000)
# Create Random Forest Classifier:
clf = RandomForestClassifier(n_jobs=4)

pipe_RF = Pipeline([
 ('preprocessing', transformer),
 ('vectorizer', vectorizer),
 ('clf', clf)]
)

# Setting best params (after performing GridSearchCV)
best_params = {
 'clf__criterion': 'gini',
 'clf__max_features': 'sqrt',
 'clf__min_samples_split': 3,
 'clf__n_estimators': 300
}

pipe_RF.set_params(**best_params)

# Fitting
pipe_RF.fit(X_train, y_train)

# Evaluation
print(f'Accuracy: {pipe_RF.score(X_test, y_test)}')



user_input = [""" public class Fibonacci {

public static void main(String[] args) {

int n = 10;

System.out.println(fib(n));

}

public static int fib(int n) {

if (n <= 1) {

return n;

}

return fib(n - 1) + fib(n - 2);

}

} """]


import shap

shap.initjs()
explainer = shap.TreeExplainer(pipe_RF.named_steps['clf'])
observation = pipe_RF[:-1].transform(user_input).toarray()
shap_values = explainer.shap_values(observation)

Load the data and run it to get the following error:

ExplainerError: Additivity check failed in TreeExplainer! Please ensure the data matrix you passed to the explainer is the same shape that the model was trained on. If your data shape is correct then please report this on GitHub. Consider retrying with the feature_perturbation='interventional' option. This check failed because for one of the samples the sum of the SHAP values was 46609069202029743624438153216.000000, while the model output was 0.004444. If this difference is acceptable you can set check_additivity=False to disable this check.


Solution

  • I have figured out how to fix it, posting to help others :)

    import pandas as pd
    
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.preprocessing import FunctionTransformer
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.pipeline import Pipeline
    
    import re
    from lime.lime_text import LimeTextExplainer
    
    from IPython.core.interactiveshell import InteractiveShell
    InteractiveShell.ast_node_interactivity = "all"
    
    # Loading GitHub Repos data containing code and comments from 2.8 million GitHub repositories:
    DATA_PATH = r"/Users/stevesolun/Steves_Files/Data/github_repos_data.csv"
    
    data = pd.read_csv(DATA_PATH, dtype='object')
    data = data.convert_dtypes()
    data = data.dropna()
    data = data.drop_duplicates()
    
    # Train/Test split
    X, y = data.content, data.language
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)
    
    # Model params to match:
    # 1. Variable and module names, words in a string, keywords: [A-Za-z_]\w*\b
    # 2. Operators: [!\#\$%\&\*\+:\-\./<=>\?@\\\^_\|\~]+
    # 3. Tabs, spaces and Brackets: [ \t\(\),;\{\}\[\]`"']
    # with the following regex:
    token_pattern = r"""(\b[A-Za-z_]\w*\b|[!\#\$%\&\*\+:\-\./<=>\?@\\\^_\|\~]+|[ \t\(\),;\{\}\[\]`"'])"""
    
    
    def preprocess(x):
        """ Clean up single-character variable names or ones constituted of a sequence of the same character """
        return pd.Series(x).replace(r'\b([A-Za-z])\1+\b', '', regex=True)\
            .replace(r'\b[A-Za-z]\b', '', regex=True)
    
    
    # Pipe steps:
    # Define a transformer:
    transformer = FunctionTransformer(preprocess)
    # Perform TF-IDF vectorization with our token pattern:
    vectorizer = TfidfVectorizer(token_pattern=token_pattern, max_features=1500)
    # Create Random Forest Classifier:
    clf = RandomForestClassifier(n_jobs=-1)
    
    pipe_RF = Pipeline([
         ('preprocessing', transformer),
         ('vectorizer', vectorizer)]
        )
    
    # Setting best params (after performing GridSearchCV)
    best_params = {
        'criterion': 'gini',
        'max_features': 'sqrt',
        'min_samples_split': 3,
        'n_estimators': 300
    }
    
    clf.set_params(**best_params)
    # Here I am preprocessing the data:
    X_train = pipe_RF.fit_transform(X_train).toarray()
    X_test = pipe_RF.transform(X_test).toarray()
    
    # Fitting the model outside the pipe - feel free to show if possible to do it inside the pipe + fit_transform the train and test sets.
    clf.fit(X_train, y_train)
    
    # Evaluation
    print(f'Accuracy: {clf.score(X_test, y_test)}')
    
    
    user_input = """ def fib(n):
                        a,b = 0,1
                        while a < n:
                            print(a, end=' ')
                        a,b = b, a+b
                        print()
                        fib(1000)
    
       """
    clf.predict(pipe_RF.transform(user_input))[0]
    
    prediction = clf.predict(pipe_RF.transform(user_input))[0]
    predicted_class_idx = list(clf.classes_).index(prediction)
    
    import shap
    
    shap.initjs()
    explainer = shap.TreeExplainer(clf, X_train)
    observation = pipe_RF.transform(user_input).toarray()
    shap_values = explainer.shap_values(observation)
    
    
    shap.force_plot(explainer.expected_value[predicted_class_idx], shap_values[predicted_class_idx], feature_names=vectorizer.get_feature_names_out())
    

    enter image description here