How to use SHAP with SpaCy models?

Viewed 242

I am trying to improve the interpretability of a SpaCy binary text classification model I trained by explaining predictions using SHAP. Here's what I've tried so far (following this tutorial):

nlp = spacy.load("my_model") # load my model
explainer = shap.Explainer(nlp_predict)
shap_values = explainer(["This is an example"])

but I get AttributeError: 'str' object has no attribute 'shape'. nlp_predict is a method I wrote which takes a list of texts and outputs predicted probabilities for each text in the format that they use in the tutorial. What am I missing here?


Here's my formatting function:

def nlp_predict(texts):
    result = []
    for text in texts:
        prediction = nlp_fn(text) # This returns label probability but in the wrong format
        sub_result = []
        sub_result.append({'label': 'label1', 'score': prediction["label1"]})
        sub_result.append({'label': 'label2', 'score': prediction["label2"]})
        result.append(sub_result)
    return(result)

And here's the format of predictions they use in the tutorial (for 2 data points):

[[{'label': 'label1', 'score': 2.850986311386805e-05},
  {'label': 'label2', 'score': 0.9999715089797974}],
 [{'label': 'label1', 'score': 0.00010622951958794147},
  {'label': 'label2', 'score': 0.9998937845230103}]]

My function's output matches this, but I still get the AttributeError. Here's the entire error message I get: .

1 Answers

The problem lies in shap having implemented methods only for the transformer library's tokenizers and models.

SpaCy tokenizer works very differently, and particularly doesn't return the token ids as the result of tokenization.

So the solution to make this work would require either writing a function to wrap the spacy tokenizer to return the same data as a transformer tokenizer (e.g. something like [{'input_ids': [101, 7592, ...], 'offset_mapping': [(0, 5), (6, 9), ...], ...}]) or add support in shap for spacy tokenizers / models.

Here's an example where I hacked a solution for the former.

  • Wrappers around my spacy model for prediction / tokenization:
import spacy
textcat_spacy = spacy.load("my-model")
tokenizer_spacy = spacy.tokenizer.Tokenizer(textcat_spacy.vocab)

# Run the spacy pipeline on some random text just to retrieve the classes
doc = textcat_spacy("hi")
classes = list(doc.cats.keys())

# Define a function to predict
def predict(texts):
    # convert texts to bare strings
    texts = [str(text) for text in texts]
    results = []
    for doc in textcat_spacy.pipe(texts):
        # results.append([{'label': cat, 'score': doc.cats[cat]} for cat in doc.cats])
        results.append([doc.cats[cat] for cat in classes])
    return results

# Create a function to create a transformers-like tokenizer to match shap's expectations
def tok_adapter(text, return_offsets_mapping=False):
    doc = tokenizer_spacy(text)
    out = {"input_ids": [tok.norm for tok in doc]}
    if return_offsets_mapping:
        out["offset_mapping"] = [(tok.idx, tok.idx + len(tok)) for tok in doc]
    return out
  • The Shap Explainer configuration:
import shap
# Create the Shap Explainer
# - predict is the "model" function, adapted to a transformers-like model
# - masker is the masker used by shap, which relies on a transformers-like tokenizer
# - algorithm is set to permuation, which is the one used for transformers models
# - output_names are the classes (altough it is not propagated to the permutation explainer currently, which is why plots do not have the labels)
# - max_evals is set to a high number to reduce the probability of cases where the explainer fails because there are too many tokens
explainer = shap.Explainer(predict, masker=shap.maskers.Text(tok_adapter), algorithm="permutation", output_names=classes, max_evals=1500)
  • Usage:
sample = "Some text to classify"
# Process the text using SpaCy
doc = textcat_spacy(sample)
# Get the shap values
shap_values = explainer([sample])
Related