FastSHAP Skill
When to Use
Activate this skill when:
- You need to generate Shapley value explanations for a predictive model's outputs
- You want to train an amortized explainer (neural network) that produces explanations in a single forward pass rather than running KernelSHAP separately for each sample
- You are working with tabular data (census/adult-style datasets) and want feature attribution explanations
- You are working with image data (e.g., CIFAR-10, ImageNet) and need pixel/superpixel-level explanations
- You want to train a surrogate model that accepts masked/missing features to support the FastSHAP training process
- You need real-time or batch Shapley value estimates with lower computational overhead than KernelSHAP
- Keywords:
shapley values, SHAP, model explainability, feature importance, amortized inference, KernelSHAP, surrogate model, FastSHAP, local explanations, XAI, interpretability
Quick Reference
Installation / Setup
Prerequisites
- Python 3.7+
- PyTorch (install separately per your CUDA version)
- A machine learning model to explain (e.g., LightGBM, XGBoost, sklearn, PyTorch CNN)
Install from Source (Recommended)
# Clone the repository
git clone https://github.com/iancovert/fastshap.git
cd fastshap
# Install the package
pip install .
Install Dependencies for Notebooks
pip install torch torchvision lightgbm scikit-learn numpy pandas matplotlib
Verify Installation
import fastshap
from fastshap import FastSHAP, Surrogate
from fastshap.tabular_imputers import MarginalImputer, BaselineImputer
from fastshap.image_imputers import BaselineImageImputer
print("FastSHAP installed successfully")
Core Features
- FastSHAP Explainer Training: Train a neural network to produce Shapley value estimates in a single forward pass using a KernelSHAP-inspired objective function.
- Tabular Data Support: Full pipeline for tabular models including surrogate training, MLP explainer training, and marginal/baseline imputation strategies.
- Image Data Support: Full pipeline for image models (e.g., ResNet, UNet explainer) with superpixel-based masking and image surrogate training.
- Surrogate Model Wrapper (
Surrogate): Train a surrogate (e.g., MLP) to replicate a black-box model's predictions when features are marginalized out.
- Image Surrogate Wrapper (
ImageSurrogate): Train a surrogate specifically designed for image models with superpixel masking support.
- Multiple Imputation Strategies:
MarginalImputer: Replace held-out features with samples from the marginal distribution.
BaselineImputer: Replace held-out features with fixed baseline values (e.g., zeros or means).
BaselineImageImputer: Replace held-out image superpixels with a baseline (e.g., gray).
- Efficient Normalization:
additive_efficient_normalization and multiplicative_efficient_normalization ensure Shapley value estimates satisfy the efficiency axiom (sum to model output).
- Flexible Explainer Architectures: Any
torch.nn.Module can serve as the explainer (MLP for tabular, UNet for images).
- Single-Model FastSHAP: Option to use a model that natively handles missing features, eliminating the need for a separate surrogate.
Usage Examples
Overview of the FastSHAP Pipeline
The FastSHAP pipeline has three stages:
- Train or load a predictive model (any black-box model).
- Train a surrogate model to handle masked/missing features.
- Train the FastSHAP explainer to output Shapley value estimates.
After training, generate explanations with a single forward pass.
Tabular Data Pipeline (Census/Adult Dataset)
import numpy as np
import torch
import torch.nn as nn
from fastshap import FastSHAP, Surrogate
from fastshap.tabular_imputers import MarginalImputer
# --- Step 1: Prepare data and original model ---
# (Assume X_train, X_val, X_test are numpy arrays, model is a trained LightGBM/XGBoost)
# model.predict_proba(X_train) -> shape (N, num_classes)
# --- Step 2: Set up imputer (surrogate or marginal) ---
# MarginalImputer replaces masked features with samples from training data
imputer = MarginalImputer(model, X_train)
# --- Step 3: Train surrogate model ---
# The surrogate is an MLP that takes (x, mask) as input and replicates model predictions
surrogate_model = nn.Sequential(
nn.Linear(num_features * 2, 128), # input: [features | mask]
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, num_outputs),
nn.Softmax(dim=1)
)
surr = Surrogate(surrogate_model, num_features)
surr.train(
train_data=X_train,
val_data=X_val,
original_model=model,
batch_size=64,
max_epochs=10,
loss_fn=nn.MSELoss(),
imputer=imputer,
)
# --- Step 4: Train FastSHAP explainer ---
explainer_model = nn.Sequential(
nn.Linear(num_features, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, num_features * num_outputs) # output: shapley values
)
fastshap = FastSHAP(
explainer=explainer_model,
imputer=surr,
normalization='additive',
link=nn.Softmax(dim=1)
)
fastshap.train(
train_data=X_train,
val_data=X_val,
batch_size=64,
num_samples=8,
max_epochs=10,
validation_samples=128,
loss_fn='mse',
)
# --- Step 5: Generate explanations ---
shap_values = fastshap.shap_values(X_test)
# shap_values shape: (N, num_features, num_outputs)
print("SHAP values shape:", shap_values.shape)
Image Data Pipeline (CIFAR-10)
import torch
import torch.nn as nn
from torchvision import models
from fastshap import FastSHAP, ImageSurrogate
from fastshap.image_imputers import BaselineImageImputer
# Image dimensions and superpixel settings
width, height = 32, 32
superpixel_size = 4 # 4x4 superpixels -> 8x8 = 64 superpixels
# --- Step 1: Load original ResNet18 model ---
original_model = models.resnet18(pretrained=True)
original_model.eval()
# --- Step 2: Set up image imputer ---
imputer = BaselineImageImputer(
width=width,
height=height,
superpixel_size=superpixel_size,
baseline=0.5 # gray baseline value
)
# --- Step 3: Train image surrogate (another ResNet18) ---
surrogate_model = models.resnet18(pretrained=False)
image_surr = ImageSurrogate(
surrogate=surrogate_model,
width=width,
height=height,
superpixel_size=superpixel_size
)
# image_surr.train(train_data, val_data, original_model, ...)
# --- Step 4: Set up UNet explainer and train FastSHAP ---
# (UNet architecture is defined in notebooks/unet.py)
# fastshap = FastSHAP(explainer=unet_model, imputer=image_surr, ...)
# fastshap.train(...)
# --- Step 5: Generate image explanations ---
# shap_values = fastshap.shap_values(image_batch)
# shap_values shape: (N, num_superpixels, num_classes)
Generating Shapley Values After Training
# Single sample
sample = X_test[0:1]
shap_vals = fastshap.shap_values(sample)
# Batch of samples
shap_vals = fastshap.shap_values(X_test[:100])
# shap_vals[i, j, k] = contribution of feature j to class k for sample i
Using Normalization Functions Directly
from fastshap.fastshap import (
additive_efficient_normalization,
multiplicative_efficient_normalization
)
import torch
# pred: raw explainer output (batch, num_features, num_outputs)
# grand: model output with all features (batch, num_outputs)
# null: model output with no features (num_outputs,)
pred = torch.randn(16, 10, 2)
grand = torch.randn(16, 2)
null = torch.zeros(2)
normalized = additive_efficient_normalization(pred, grand, null)
# normalized.sum(dim=1) ~= grand - null (efficiency property)
Key APIs / Models
Classes
| Class |
Module |
Description |
FastSHAP |
fastshap.fastshap |
Main explainer wrapper; trains explainer model and generates SHAP values |
Surrogate |
fastshap.surrogate |
Trains/wraps surrogate model for tabular data |
ImageSurrogate |
fastshap.image_surrogate |
Trains/wraps surrogate model for image data |
MarginalImputer |
fastshap.tabular_imputers |
Replaces masked features with marginal distribution samples |
BaselineImputer |
fastshap.tabular_imputers |
Replaces masked features with fixed baseline values |
ImageImputer |
fastshap.image_imputers |
Base class for image imputers |
BaselineImageImputer |
fastshap.image_imputers |
Replaces masked image superpixels with baseline values |
Key Functions
| Function |
Module |
Description |
additive_efficient_normalization(pred, grand, null) |
fastshap.fastshap |
Normalizes SHAP predictions to satisfy efficiency axiom (additive) |
multiplicative_efficient_normalization(pred, grand, null) |
fastshap.fastshap |
Normalizes SHAP predictions to satisfy efficiency axiom (multiplicative) |
evaluate_explainer(explainer, normalization, x) |
fastshap.fastshap |
Runs explainer forward pass with normalization applied |
validate(surrogate, loss_fn, data_loader) |
fastshap.surrogate |
Validates surrogate model on a data loader |
generate_labels(dataset, model, batch_size) |
fastshap.surrogate |
Generates soft labels from original model for surrogate training |
Architectures Used in Experiments
| Architecture |
Role |
Dataset |
| LightGBM / LGBM |
Original predictive model |
Census/Adult tabular |
| MLP (PyTorch) |
Surrogate model |
Census/Adult tabular |
| MLP (PyTorch) |
Explainer model |
Census/Adult tabular |
| ResNet18 |
Original predictive model |
CIFAR-10 images |
| ResNet18 |
Surrogate model |
CIFAR-10 images |
| UNet |
Explainer model (image-sized output) |
CIFAR-10 images |
Normalization Options
| Option |
String Key |
Description |
| Additive |
'additive' |
Subtracts/adds residual to sum term |
| Multiplicative |
'multiplicative' |
Scales predictions to match efficiency |
| None |
None |
No normalization applied |
Common Patterns & Best Practices
Choosing an Imputer
MarginalImputer: Best for tabular data when you want to marginalize over the training distribution. More faithful to the original model's behavior.
BaselineImputer: Faster but less statistically principled; uses a fixed reference value (e.g., feature mean or zero).
BaselineImageImputer: Standard choice for image tasks; uses a constant pixel value (gray/black) as the baseline.
Choosing Normalization
- Always use
normalization='additive' unless you have a specific reason to use multiplicative. The additive normalization enforces the efficiency axiom (SHAP values sum to model output minus baseline).
Surrogate vs. Single Model
- Surrogate approach (two models): More general; works for any black-box model. Train a surrogate that accepts
(x, mask) pairs and replicates original model outputs.
- Single model approach: The predictive model itself is trained to handle missing features. Fewer parameters to manage, but requires retraining the original model. See the single model notebook.
Number of Samples During Training
- The
num_samples argument in FastSHAP.train() controls how many random coalition samples are drawn per training example per batch. Higher values → more stable gradient estimates but slower training. Start with 8–16 for tabular, 4–8 for image data.
Explainer Architecture for Images
- The explainer output must be the same spatial size as the input (e.g., a UNet). The explainer output has shape
(batch, height, width, num_classes) reshaped appropriately.
Validation During Training
- Use
validation_samples (number of coalitions to average over during validation) to get stable validation loss estimates. A value of 64–128 works well.
Link Functions
- Pass a link function (e.g.,
nn.Softmax(dim=1) for classification) to FastSHAP if you want SHAP values to be defined on the probability scale rather than the logit scale.
Demo Scripts
scripts/01_tabular_fastshap_demo.py
#!/usr/bin/env python3
"""
FastSHAP Tabular Data Demo
==========================
Demonstrates the complete FastSHAP pipeline for a tabular classification task
using synthetic data. The pipeline covers:
1. Training a simple "black-box" predictive model (logistic regression via PyTorch)
2. Training a surrogate model using MarginalImputer
3. Training the FastSHAP explainer model
4. Generating Shapley value estimates for test samples
Requirements:
pip install . (from fastshap repo root)
pip install torch numpy scikit-learn
Usage:
python 01_tabular_fastshap_demo.py
"""
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# FastSHAP imports
from fastshap import FastSHAP, Surrogate
from fastshap.tabular_imputers import MarginalImputer
# ─── Reproducibility ──────────────────────────────────────────────────────────
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)
# ─── Configuration ────────────────────────────────────────────────────────────
NUM_FEATURES = 20
NUM_CLASSES = 2
NUM_SAMPLES = 2000
BATCH_SIZE = 64
SURROGATE_EPOCHS = 5
EXPLAINER_EPOCHS = 5
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ─── Helper: Simple MLP builder ───────────────────────────────────────────────
def build_mlp(
input_dim: int,
hidden_dim: int,
output_dim: int,
hidden_layers: int = 2,
activation: nn.Module = nn.ReLU(),
output_activation: nn.Module = None,
) -> nn.Sequential:
"""
Build a simple fully-connected MLP.
Args:
input_dim: Number of input features.
hidden_dim: Width of each hidden layer.
output_dim: Number of output units.
hidden_layers: Number of hidden layers.
activation: Activation function between layers.
output_activation: Optional activation after the final layer.
Returns:
A torch.nn.Sequential MLP module.
"""
layers = [nn.Linear(input_dim, hidden_dim), nn.ReLU()]
for _ in range(hidden_layers - 1):
layers += [nn.Linear(hidden_dim, hidden_dim), nn.ReLU()]
layers.append(nn.Linear(hidden_dim, output_dim))
if output_activation is not None:
layers.append(output_activation)
return nn.Sequential(*layers)
# ─── Step 1: Generate Synthetic Data & Train Original Model ───────────────────
def prepare_data():
"""Generate synthetic tabular classification data and split into splits."""
X, y = make_classification(
n_samples=NUM_SAMPLES,
n_features=NUM_FEATURES,
n_informative=10,
n_redundant=5,
random_state=SEED,
)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=SEED
)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=SEED
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train).astype(np.float32)
X_val = scaler.transform(X_val).astype(np.float32)
X_test = scaler.transform(X_test).astype(np.float32)
return X_train, X_val, X_test, y_train, y_val, y_test
def train_original_model(X_train: np.ndarray, y_train: np.ndarray) -> nn.Module:
"""
Train a simple logistic regression model as the 'black-box' model to explain.
Args:
X_train: Training features, shape (N, num_features).
y_train: Training labels, shape (N,).
Returns:
Trained PyTorch model that outputs class probabilities.
"""
model = build_mlp(
input_dim=NUM_FEATURES,
hidden_dim=64,
output_dim=NUM_CLASSES,
output_activation=nn.Softmax(dim=1),
).to(DEVICE)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
X_t = torch.tensor(X_train, device=DEVICE)
y_t = torch.tensor(y_train, dtype=torch.long, device=DEVICE)
dataset = TensorDataset(X_t, y_t)
loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
model.train()
for epoch in range(5):
total_loss = 0.0
for xb, yb in loader:
optimizer.zero_grad()
preds = model(xb)
loss = loss_fn(preds, yb)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f" [OriginalModel] Epoch {epoch+1}/5 | Loss: {total_loss/len(loader):.4f}")
model.eval()
return model
# ─── Wrapper: make original model callable on numpy arrays ────────────────────
class NumpyModelWrapper:
"""
Wraps a PyTorch model to accept numpy arrays and return numpy arrays.
Required by FastSHAP's MarginalImputer.
"""
def __init__(self, model: nn.Module, device: torch.device):
self.model = model
self.device = device
def __call__(self, X: np.ndarray) -> np.ndarray:
self.model.eval()
with torch.no_grad():
X_t = torch.tensor(X, dtype=torch.float32, device=self.device)
out = self.model(X_t)
return out.cpu().numpy()
# ─── Step 2: Build & Train Surrogate Model ────────────────────────────────────
def train_surrogate(
original_model_wrapper,
X_train: np.ndarray,
X_val: np.ndarray,
) -> Surrogate:
"""
Train a surrogate model that accepts masked feature vectors.
The surrogate is an MLP with input dimension `num_features * 2`
(concatenated features + binary mask) and output dimension `num_classes`.
Args:
original_model_wrapper: Callable that takes numpy X and returns numpy probs.
X_train: Training features.
X_val: Validation features.
Returns:
Trained Surrogate wrapper object.
"""
# Surrogate input = [x_masked | mask], so input_dim = num_features * 2
surrogate_net = build_mlp(
input_dim=NUM_FEATURES * 2,
hidden_dim=128,
output_dim=NUM_CLASSES,
hidden_layers=2,
output_activation=nn.Softmax(dim=1),
).to(DEVICE)
surr = Surrogate(surrogate=surrogate_net, num_features=NUM_FEATURES)
print("\n[Surrogate] Training surrogate model...")
surr.train(
train_data=X_train,
val_data=X_val,
original_model=original_model_wrapper,
batch_size=BATCH_SIZE,
max_epochs=SURROGATE_EPOCHS,
loss_fn=nn.MSELoss(),
imputer=MarginalImputer(original_model_wrapper, X_train),
lookback=5,
verbose=True,
)
print("[Surrogate] Training complete.")
return surr
# ─── Step 3: Build & Train FastSHAP Explainer ────────────────────────────────
def train_fastshap_explainer(
surr: Surrogate,
X_train: np.ndarray,
X_val: np.ndarray,
original_model_wrapper,
) -> FastSHAP:
"""
Train the FastSHAP explainer model.
The explainer takes x (num_features,) as input and outputs Shapley value
estimates of shape (num_features * num_classes,), which are then reshaped
to (num_features, num_classes).
Args:
surr: Trained Surrogate object (used as imputer for FastSHAP training).
X_train: Training features.
X_val: Validation features.
original_model_wrapper: Callable original model (for grand/null computation).
Returns:
Trained FastSHAP object.
"""
# Explainer: x (num_features,) -> shap values (num_features * num_classes,)
explainer_net = build_mlp(
input_dim=NUM_FEATURES,
hidden_dim=128,
output_dim=NUM_FEATURES * NUM_CLASSES,
hidden_layers=2,
).to(DEVICE)
fastshap = FastSHAP(
explainer=explainer_net,
imputer=surr,
normalization="additive",
link=nn.Softmax(dim=1),
)
print("\n[FastSHAP] Training explainer model...")
fastshap.train(
train_data=X_train,
val_data=X_val,
batch_size=BATCH_SIZE,
num_samples=8, # coalitions sampled per example per batch
max_epochs=EXPLAINER_EPOCHS,
validation_samples=64, # coalitions averaged during validation
loss_fn="mse",
verbose=True,
lookback=5,
)
print("[FastSHAP] Training complete.")
return fastshap
# ─── Step 4: Generate & Inspect Shapley Values ────────────────────────────────
def generate_and_inspect_shap_values(
fastshap: FastSHAP,
X_test: np.ndarray,
) -> np.ndarray:
"""
Use the trained FastSHAP model to generate Shapley value explanations.
Args:
fastshap: Trained FastSHAP object.
X_test: Test feature matrix, shape (N, num_features).
Returns:
SHAP values array of shape (N, num_features, num_classes).
"""
print("\n[FastSHAP] Generating Shapley value estimates...")
shap_values = fastshap.shap_values(X_test)
print(f" Input shape: {X_test.shape}")
print(f" SHAP values shape: {shap_values.shape}")
# shap_values[i, j, k] = contribution of feature j to class k for sample i
# Inspect top features for first test sample (class 1 = positive class)
sample_idx = 0
class_idx = 1
sv = shap_values[sample_idx, :, class_idx]
feature_names = [f"feature_{i}" for i in range(NUM_FEATURES)]
sorted_idx = np.argsort(np.abs(sv))[::-1]
print(f"\n Top-5 features for test sample {sample_idx} (class={class_idx}):")
for rank, fi in enumerate(sorted_idx[:5]):
print(f" {rank+1}. {feature_names[fi]:12s} SHAP={sv[fi]:+.4f}")
return shap_values
# ─── Utility: Efficiency Check ────────────────────────────────────────────────
def check_efficiency(
fastshap: FastSHAP,
original_model_wrapper,
X_test: np.ndarray,
shap_values: np.ndarray,
n_samples: int = 10,
) -> None:
"""
Verify the efficiency axiom: sum of SHAP values ≈ f(x) - f(null).
Args:
fastshap: Trained FastSHAP object.
original_model_wrapper: Callable original model.
X_test: Test features.
shap_values: SHAP values array (N, num_features, num_classes).
n_samples: Number of samples to check.
"""
print("\n[Efficiency Check] SHAP sum vs (f(x) - f(null)):")
# Null prediction (empty input)
null_input = np.zeros((1, NUM_FEATURES), dtype=np.float32)
f_null = original_model_wrapper(null_input)[0] # (num_classes,)
for i in range(min(n_samples, len(X_test))):
xi = X_test[i : i + 1]
f_xi = original_model_wrapper(xi)[0] # (num_classes,)
shap_sum = shap_values[i].sum(axis=0) # (num_classes,)
target = f_xi - f_null
print(
f" Sample {i:2d} | SHAP sum: {shap_sum} "
f"| f(x)-f(null): {target} "
f"| diff: {np.abs(shap_sum - target).max():.4f}"
)
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
print("=" * 60)
print("FastSHAP Tabular Pipeline Demo")
print("=" * 60)
# 1. Prepare data
print("\n[Data] Generating synthetic classification dataset...")
X_train, X_val, X_test, y_train, y_val, y_test = prepare_data()
print(f" Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")
# 2. Train original model
print("\n[OriginalModel] Training black-box model...")
original_model = train_original_model(X_train, y_train)
wrapper = NumpyModelWrapper(original_model, DEVICE)
# Sanity-check original model
test_preds = wrapper(X_test[:5])
print(f" Sample predictions (probabilities): {test_preds}")
# 3. Train surrogate
surr = train_surrogate(wrapper, X_train, X_val)
# 4. Train FastSHAP explainer
fastshap = train_fastshap_explainer(surr, X_train, X_val, wrapper)
# 5. Generate explanations
shap_values = generate_and_inspect_shap_values(fastshap, X_test)
# 6. Efficiency check
check_efficiency(fastshap, wrapper, X_test, shap_values, n_samples=5)
print("\n" + "=" * 60)
print("Demo complete!")
print("=" * 60)
if __name__ == "__main__":
main()
scripts/02_normalization_and_utils_demo.py
#!/usr/bin/env python3
"""
FastSHAP Normalization & Utilities Demo
========================================
Demonstrates the low-level normalization functions and utility helpers
provided by FastSHAP:
- additive_efficient_normalization
- multiplicative_efficient_normalization
- evaluate_explainer
- MarginalImputer and BaselineImputer usage
- Surrogate.generate_labels helper
These are the building blocks used internally by FastSHAP.train() and
can be useful when building custom
1---2name: fastshap3description: Use this skill when you need to train amortized Shapley value explainers using FastSHAP, generate real-time local feature importance explanations for machine learning models (tabular or image), train surrogate models for feature masking, or understand how FastSHAP's KernelSHAP-inspired training objective works with PyTorch.4---56# FastSHAP Skill78## When to Use910Activate this skill when:11- You need to generate **Shapley value explanations** for a predictive model's outputs12- You want to train an **amortized explainer** (neural network) that produces explanations in a single forward pass rather than running KernelSHAP separately for each sample13- You are working with **tabular data** (census/adult-style datasets) and want feature attribution explanations14- You are working with **image data** (e.g., CIFAR-10, ImageNet) and need pixel/superpixel-level explanations15- You want to train a **surrogate model** that accepts masked/missing features to support the FastSHAP training process16- You need **real-time or batch Shapley value estimates** with lower computational overhead than KernelSHAP17- Keywords: `shapley values`, `SHAP`, `model explainability`, `feature importance`, `amortized inference`, `KernelSHAP`, `surrogate model`, `FastSHAP`, `local explanations`, `XAI`, `interpretability`1819## Quick Reference2021| Resource | URL |22|----------|-----|23| Paper (arXiv) | https://arxiv.org/abs/2107.07436 |24| GitHub Repository | https://github.com/iancovert/fastshap |25| TensorFlow implementation | https://github.com/neiljethani/fastshap |26| Census notebook | https://github.com/iancovert/fastshap/blob/main/notebooks/census.ipynb |27| CIFAR-10 notebook | https://github.com/iancovert/fastshap/blob/main/notebooks/cifar.ipynb |28| CIFAR-10 single model notebook | https://github.com/iancovert/fastshap/blob/main/notebooks/cifar%20single%20model.ipynb |29| Blog: Understanding SHAP/SAGE | https://iancovert.com/blog/understanding-shap-sage/ |3031## Installation / Setup3233### Prerequisites34- Python 3.7+35- PyTorch (install separately per your CUDA version)36- A machine learning model to explain (e.g., LightGBM, XGBoost, sklearn, PyTorch CNN)3738### Install from Source (Recommended)3940```bash41# Clone the repository42git clone https://github.com/iancovert/fastshap.git43cd fastshap4445# Install the package46pip install .47```4849### Install Dependencies for Notebooks5051```bash52pip install torch torchvision lightgbm scikit-learn numpy pandas matplotlib53```5455### Verify Installation5657```python58import fastshap59from fastshap import FastSHAP, Surrogate60from fastshap.tabular_imputers import MarginalImputer, BaselineImputer61from fastshap.image_imputers import BaselineImageImputer62print("FastSHAP installed successfully")63```6465## Core Features6667- **FastSHAP Explainer Training**: Train a neural network to produce Shapley value estimates in a single forward pass using a KernelSHAP-inspired objective function.68- **Tabular Data Support**: Full pipeline for tabular models including surrogate training, MLP explainer training, and marginal/baseline imputation strategies.69- **Image Data Support**: Full pipeline for image models (e.g., ResNet, UNet explainer) with superpixel-based masking and image surrogate training.70- **Surrogate Model Wrapper (`Surrogate`)**: Train a surrogate (e.g., MLP) to replicate a black-box model's predictions when features are marginalized out.71- **Image Surrogate Wrapper (`ImageSurrogate`)**: Train a surrogate specifically designed for image models with superpixel masking support.72- **Multiple Imputation Strategies**:73 - `MarginalImputer`: Replace held-out features with samples from the marginal distribution.74 - `BaselineImputer`: Replace held-out features with fixed baseline values (e.g., zeros or means).75 - `BaselineImageImputer`: Replace held-out image superpixels with a baseline (e.g., gray).76- **Efficient Normalization**: `additive_efficient_normalization` and `multiplicative_efficient_normalization` ensure Shapley value estimates satisfy the efficiency axiom (sum to model output).77- **Flexible Explainer Architectures**: Any `torch.nn.Module` can serve as the explainer (MLP for tabular, UNet for images).78- **Single-Model FastSHAP**: Option to use a model that natively handles missing features, eliminating the need for a separate surrogate.7980## Usage Examples8182### Overview of the FastSHAP Pipeline8384The FastSHAP pipeline has three stages:85861. **Train or load a predictive model** (any black-box model).872. **Train a surrogate model** to handle masked/missing features.883. **Train the FastSHAP explainer** to output Shapley value estimates.8990After training, generate explanations with a single forward pass.9192### Tabular Data Pipeline (Census/Adult Dataset)9394```python95import numpy as np96import torch97import torch.nn as nn98from fastshap import FastSHAP, Surrogate99from fastshap.tabular_imputers import MarginalImputer100101# --- Step 1: Prepare data and original model ---102# (Assume X_train, X_val, X_test are numpy arrays, model is a trained LightGBM/XGBoost)103# model.predict_proba(X_train) -> shape (N, num_classes)104105# --- Step 2: Set up imputer (surrogate or marginal) ---106# MarginalImputer replaces masked features with samples from training data107imputer = MarginalImputer(model, X_train)108109# --- Step 3: Train surrogate model ---110# The surrogate is an MLP that takes (x, mask) as input and replicates model predictions111surrogate_model = nn.Sequential(112 nn.Linear(num_features * 2, 128), # input: [features | mask]113 nn.ReLU(),114 nn.Linear(128, 128),115 nn.ReLU(),116 nn.Linear(128, num_outputs),117 nn.Softmax(dim=1)118)119surr = Surrogate(surrogate_model, num_features)120surr.train(121 train_data=X_train,122 val_data=X_val,123 original_model=model,124 batch_size=64,125 max_epochs=10,126 loss_fn=nn.MSELoss(),127 imputer=imputer,128)129130# --- Step 4: Train FastSHAP explainer ---131explainer_model = nn.Sequential(132 nn.Linear(num_features, 128),133 nn.ReLU(),134 nn.Linear(128, 128),135 nn.ReLU(),136 nn.Linear(128, num_features * num_outputs) # output: shapley values137)138fastshap = FastSHAP(139 explainer=explainer_model,140 imputer=surr,141 normalization='additive',142 link=nn.Softmax(dim=1)143)144fastshap.train(145 train_data=X_train,146 val_data=X_val,147 batch_size=64,148 num_samples=8,149 max_epochs=10,150 validation_samples=128,151 loss_fn='mse',152)153154# --- Step 5: Generate explanations ---155shap_values = fastshap.shap_values(X_test)156# shap_values shape: (N, num_features, num_outputs)157print("SHAP values shape:", shap_values.shape)158```159160### Image Data Pipeline (CIFAR-10)161162```python163import torch164import torch.nn as nn165from torchvision import models166from fastshap import FastSHAP, ImageSurrogate167from fastshap.image_imputers import BaselineImageImputer168169# Image dimensions and superpixel settings170width, height = 32, 32171superpixel_size = 4 # 4x4 superpixels -> 8x8 = 64 superpixels172173# --- Step 1: Load original ResNet18 model ---174original_model = models.resnet18(pretrained=True)175original_model.eval()176177# --- Step 2: Set up image imputer ---178imputer = BaselineImageImputer(179 width=width,180 height=height,181 superpixel_size=superpixel_size,182 baseline=0.5 # gray baseline value183)184185# --- Step 3: Train image surrogate (another ResNet18) ---186surrogate_model = models.resnet18(pretrained=False)187image_surr = ImageSurrogate(188 surrogate=surrogate_model,189 width=width,190 height=height,191 superpixel_size=superpixel_size192)193# image_surr.train(train_data, val_data, original_model, ...)194195# --- Step 4: Set up UNet explainer and train FastSHAP ---196# (UNet architecture is defined in notebooks/unet.py)197# fastshap = FastSHAP(explainer=unet_model, imputer=image_surr, ...)198# fastshap.train(...)199200# --- Step 5: Generate image explanations ---201# shap_values = fastshap.shap_values(image_batch)202# shap_values shape: (N, num_superpixels, num_classes)203```204205### Generating Shapley Values After Training206207```python208# Single sample209sample = X_test[0:1]210shap_vals = fastshap.shap_values(sample)211212# Batch of samples213shap_vals = fastshap.shap_values(X_test[:100])214215# shap_vals[i, j, k] = contribution of feature j to class k for sample i216```217218### Using Normalization Functions Directly219220```python221from fastshap.fastshap import (222 additive_efficient_normalization,223 multiplicative_efficient_normalization224)225import torch226227# pred: raw explainer output (batch, num_features, num_outputs)228# grand: model output with all features (batch, num_outputs)229# null: model output with no features (num_outputs,)230231pred = torch.randn(16, 10, 2)232grand = torch.randn(16, 2)233null = torch.zeros(2)234235normalized = additive_efficient_normalization(pred, grand, null)236# normalized.sum(dim=1) ~= grand - null (efficiency property)237```238239## Key APIs / Models240241### Classes242243| Class | Module | Description |244|-------|--------|-------------|245| `FastSHAP` | `fastshap.fastshap` | Main explainer wrapper; trains explainer model and generates SHAP values |246| `Surrogate` | `fastshap.surrogate` | Trains/wraps surrogate model for tabular data |247| `ImageSurrogate` | `fastshap.image_surrogate` | Trains/wraps surrogate model for image data |248| `MarginalImputer` | `fastshap.tabular_imputers` | Replaces masked features with marginal distribution samples |249| `BaselineImputer` | `fastshap.tabular_imputers` | Replaces masked features with fixed baseline values |250| `ImageImputer` | `fastshap.image_imputers` | Base class for image imputers |251| `BaselineImageImputer` | `fastshap.image_imputers` | Replaces masked image superpixels with baseline values |252253### Key Functions254255| Function | Module | Description |256|----------|--------|-------------|257| `additive_efficient_normalization(pred, grand, null)` | `fastshap.fastshap` | Normalizes SHAP predictions to satisfy efficiency axiom (additive) |258| `multiplicative_efficient_normalization(pred, grand, null)` | `fastshap.fastshap` | Normalizes SHAP predictions to satisfy efficiency axiom (multiplicative) |259| `evaluate_explainer(explainer, normalization, x)` | `fastshap.fastshap` | Runs explainer forward pass with normalization applied |260| `validate(surrogate, loss_fn, data_loader)` | `fastshap.surrogate` | Validates surrogate model on a data loader |261| `generate_labels(dataset, model, batch_size)` | `fastshap.surrogate` | Generates soft labels from original model for surrogate training |262263### Architectures Used in Experiments264265| Architecture | Role | Dataset |266|--------------|------|---------|267| LightGBM / LGBM | Original predictive model | Census/Adult tabular |268| MLP (PyTorch) | Surrogate model | Census/Adult tabular |269| MLP (PyTorch) | Explainer model | Census/Adult tabular |270| ResNet18 | Original predictive model | CIFAR-10 images |271| ResNet18 | Surrogate model | CIFAR-10 images |272| UNet | Explainer model (image-sized output) | CIFAR-10 images |273274### Normalization Options275276| Option | String Key | Description |277|--------|-----------|-------------|278| Additive | `'additive'` | Subtracts/adds residual to sum term |279| Multiplicative | `'multiplicative'` | Scales predictions to match efficiency |280| None | `None` | No normalization applied |281282## Common Patterns & Best Practices283284### Choosing an Imputer285286- **`MarginalImputer`**: Best for tabular data when you want to marginalize over the training distribution. More faithful to the original model's behavior.287- **`BaselineImputer`**: Faster but less statistically principled; uses a fixed reference value (e.g., feature mean or zero).288- **`BaselineImageImputer`**: Standard choice for image tasks; uses a constant pixel value (gray/black) as the baseline.289290### Choosing Normalization291292- Always use `normalization='additive'` unless you have a specific reason to use multiplicative. The additive normalization enforces the efficiency axiom (SHAP values sum to model output minus baseline).293294### Surrogate vs. Single Model295296- **Surrogate approach** (two models): More general; works for any black-box model. Train a surrogate that accepts `(x, mask)` pairs and replicates original model outputs.297- **Single model approach**: The predictive model itself is trained to handle missing features. Fewer parameters to manage, but requires retraining the original model. See the [single model notebook](https://github.com/iancovert/fastshap/blob/main/notebooks/cifar%20single%20model.ipynb).298299### Number of Samples During Training300301- The `num_samples` argument in `FastSHAP.train()` controls how many random coalition samples are drawn per training example per batch. Higher values → more stable gradient estimates but slower training. Start with 8–16 for tabular, 4–8 for image data.302303### Explainer Architecture for Images304305- The explainer output must be the same spatial size as the input (e.g., a UNet). The explainer output has shape `(batch, height, width, num_classes)` reshaped appropriately.306307### Validation During Training308309- Use `validation_samples` (number of coalitions to average over during validation) to get stable validation loss estimates. A value of 64–128 works well.310311### Link Functions312313- Pass a link function (e.g., `nn.Softmax(dim=1)` for classification) to `FastSHAP` if you want SHAP values to be defined on the probability scale rather than the logit scale.314315## Demo Scripts316317### `scripts/01_tabular_fastshap_demo.py`318319```python320#!/usr/bin/env python3321"""322FastSHAP Tabular Data Demo323==========================324Demonstrates the complete FastSHAP pipeline for a tabular classification task325using synthetic data. The pipeline covers:326327 1. Training a simple "black-box" predictive model (logistic regression via PyTorch)328 2. Training a surrogate model using MarginalImputer329 3. Training the FastSHAP explainer model330 4. Generating Shapley value estimates for test samples331332Requirements:333 pip install . (from fastshap repo root)334 pip install torch numpy scikit-learn335336Usage:337 python 01_tabular_fastshap_demo.py338"""339340import numpy as np341import torch342import torch.nn as nn343import torch.optim as optim344from torch.utils.data import DataLoader, TensorDataset345from sklearn.datasets import make_classification346from sklearn.model_selection import train_test_split347from sklearn.preprocessing import StandardScaler348349# FastSHAP imports350from fastshap import FastSHAP, Surrogate351from fastshap.tabular_imputers import MarginalImputer352353354# ─── Reproducibility ──────────────────────────────────────────────────────────355SEED = 42356torch.manual_seed(SEED)357np.random.seed(SEED)358359# ─── Configuration ────────────────────────────────────────────────────────────360NUM_FEATURES = 20361NUM_CLASSES = 2362NUM_SAMPLES = 2000363BATCH_SIZE = 64364SURROGATE_EPOCHS = 5365EXPLAINER_EPOCHS = 5366DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")367368369# ─── Helper: Simple MLP builder ───────────────────────────────────────────────370371def build_mlp(372 input_dim: int,373 hidden_dim: int,374 output_dim: int,375 hidden_layers: int = 2,376 activation: nn.Module = nn.ReLU(),377 output_activation: nn.Module = None,378) -> nn.Sequential:379 """380 Build a simple fully-connected MLP.381382 Args:383 input_dim: Number of input features.384 hidden_dim: Width of each hidden layer.385 output_dim: Number of output units.386 hidden_layers: Number of hidden layers.387 activation: Activation function between layers.388 output_activation: Optional activation after the final layer.389390 Returns:391 A torch.nn.Sequential MLP module.392 """393 layers = [nn.Linear(input_dim, hidden_dim), nn.ReLU()]394 for _ in range(hidden_layers - 1):395 layers += [nn.Linear(hidden_dim, hidden_dim), nn.ReLU()]396 layers.append(nn.Linear(hidden_dim, output_dim))397 if output_activation is not None:398 layers.append(output_activation)399 return nn.Sequential(*layers)400401402# ─── Step 1: Generate Synthetic Data & Train Original Model ───────────────────403404def prepare_data():405 """Generate synthetic tabular classification data and split into splits."""406 X, y = make_classification(407 n_samples=NUM_SAMPLES,408 n_features=NUM_FEATURES,409 n_informative=10,410 n_redundant=5,411 random_state=SEED,412 )413 X_train, X_temp, y_train, y_temp = train_test_split(414 X, y, test_size=0.3, random_state=SEED415 )416 X_val, X_test, y_val, y_test = train_test_split(417 X_temp, y_temp, test_size=0.5, random_state=SEED418 )419420 scaler = StandardScaler()421 X_train = scaler.fit_transform(X_train).astype(np.float32)422 X_val = scaler.transform(X_val).astype(np.float32)423 X_test = scaler.transform(X_test).astype(np.float32)424425 return X_train, X_val, X_test, y_train, y_val, y_test426427428def train_original_model(X_train: np.ndarray, y_train: np.ndarray) -> nn.Module:429 """430 Train a simple logistic regression model as the 'black-box' model to explain.431432 Args:433 X_train: Training features, shape (N, num_features).434 y_train: Training labels, shape (N,).435436 Returns:437 Trained PyTorch model that outputs class probabilities.438 """439 model = build_mlp(440 input_dim=NUM_FEATURES,441 hidden_dim=64,442 output_dim=NUM_CLASSES,443 output_activation=nn.Softmax(dim=1),444 ).to(DEVICE)445446 optimizer = optim.Adam(model.parameters(), lr=1e-3)447 loss_fn = nn.CrossEntropyLoss()448449 X_t = torch.tensor(X_train, device=DEVICE)450 y_t = torch.tensor(y_train, dtype=torch.long, device=DEVICE)451 dataset = TensorDataset(X_t, y_t)452 loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)453454 model.train()455 for epoch in range(5):456 total_loss = 0.0457 for xb, yb in loader:458 optimizer.zero_grad()459 preds = model(xb)460 loss = loss_fn(preds, yb)461 loss.backward()462 optimizer.step()463 total_loss += loss.item()464 print(f" [OriginalModel] Epoch {epoch+1}/5 | Loss: {total_loss/len(loader):.4f}")465466 model.eval()467 return model468469470# ─── Wrapper: make original model callable on numpy arrays ────────────────────471472class NumpyModelWrapper:473 """474 Wraps a PyTorch model to accept numpy arrays and return numpy arrays.475 Required by FastSHAP's MarginalImputer.476 """477478 def __init__(self, model: nn.Module, device: torch.device):479 self.model = model480 self.device = device481482 def __call__(self, X: np.ndarray) -> np.ndarray:483 self.model.eval()484 with torch.no_grad():485 X_t = torch.tensor(X, dtype=torch.float32, device=self.device)486 out = self.model(X_t)487 return out.cpu().numpy()488489490# ─── Step 2: Build & Train Surrogate Model ────────────────────────────────────491492def train_surrogate(493 original_model_wrapper,494 X_train: np.ndarray,495 X_val: np.ndarray,496) -> Surrogate:497 """498 Train a surrogate model that accepts masked feature vectors.499500 The surrogate is an MLP with input dimension `num_features * 2`501 (concatenated features + binary mask) and output dimension `num_classes`.502503 Args:504 original_model_wrapper: Callable that takes numpy X and returns numpy probs.505 X_train: Training features.506 X_val: Validation features.507508 Returns:509 Trained Surrogate wrapper object.510 """511 # Surrogate input = [x_masked | mask], so input_dim = num_features * 2512 surrogate_net = build_mlp(513 input_dim=NUM_FEATURES * 2,514 hidden_dim=128,515 output_dim=NUM_CLASSES,516 hidden_layers=2,517 output_activation=nn.Softmax(dim=1),518 ).to(DEVICE)519520 surr = Surrogate(surrogate=surrogate_net, num_features=NUM_FEATURES)521522 print("\n[Surrogate] Training surrogate model...")523 surr.train(524 train_data=X_train,525 val_data=X_val,526 original_model=original_model_wrapper,527 batch_size=BATCH_SIZE,528 max_epochs=SURROGATE_EPOCHS,529 loss_fn=nn.MSELoss(),530 imputer=MarginalImputer(original_model_wrapper, X_train),531 lookback=5,532 verbose=True,533 )534 print("[Surrogate] Training complete.")535 return surr536537538# ─── Step 3: Build & Train FastSHAP Explainer ────────────────────────────────539540def train_fastshap_explainer(541 surr: Surrogate,542 X_train: np.ndarray,543 X_val: np.ndarray,544 original_model_wrapper,545) -> FastSHAP:546 """547 Train the FastSHAP explainer model.548549 The explainer takes x (num_features,) as input and outputs Shapley value550 estimates of shape (num_features * num_classes,), which are then reshaped551 to (num_features, num_classes).552553 Args:554 surr: Trained Surrogate object (used as imputer for FastSHAP training).555 X_train: Training features.556 X_val: Validation features.557 original_model_wrapper: Callable original model (for grand/null computation).558559 Returns:560 Trained FastSHAP object.561 """562 # Explainer: x (num_features,) -> shap values (num_features * num_classes,)563 explainer_net = build_mlp(564 input_dim=NUM_FEATURES,565 hidden_dim=128,566 output_dim=NUM_FEATURES * NUM_CLASSES,567 hidden_layers=2,568 ).to(DEVICE)569570 fastshap = FastSHAP(571 explainer=explainer_net,572 imputer=surr,573 normalization="additive",574 link=nn.Softmax(dim=1),575 )576577 print("\n[FastSHAP] Training explainer model...")578 fastshap.train(579 train_data=X_train,580 val_data=X_val,581 batch_size=BATCH_SIZE,582 num_samples=8, # coalitions sampled per example per batch583 max_epochs=EXPLAINER_EPOCHS,584 validation_samples=64, # coalitions averaged during validation585 loss_fn="mse",586 verbose=True,587 lookback=5,588 )589 print("[FastSHAP] Training complete.")590 return fastshap591592593# ─── Step 4: Generate & Inspect Shapley Values ────────────────────────────────594595def generate_and_inspect_shap_values(596 fastshap: FastSHAP,597 X_test: np.ndarray,598) -> np.ndarray:599 """600 Use the trained FastSHAP model to generate Shapley value explanations.601602 Args:603 fastshap: Trained FastSHAP object.604 X_test: Test feature matrix, shape (N, num_features).605606 Returns:607 SHAP values array of shape (N, num_features, num_classes).608 """609 print("\n[FastSHAP] Generating Shapley value estimates...")610 shap_values = fastshap.shap_values(X_test)611612 print(f" Input shape: {X_test.shape}")613 print(f" SHAP values shape: {shap_values.shape}")614 # shap_values[i, j, k] = contribution of feature j to class k for sample i615616 # Inspect top features for first test sample (class 1 = positive class)617 sample_idx = 0618 class_idx = 1619 sv = shap_values[sample_idx, :, class_idx]620 feature_names = [f"feature_{i}" for i in range(NUM_FEATURES)]621622 sorted_idx = np.argsort(np.abs(sv))[::-1]623 print(f"\n Top-5 features for test sample {sample_idx} (class={class_idx}):")624 for rank, fi in enumerate(sorted_idx[:5]):625 print(f" {rank+1}. {feature_names[fi]:12s} SHAP={sv[fi]:+.4f}")626627 return shap_values628629630# ─── Utility: Efficiency Check ────────────────────────────────────────────────631632def check_efficiency(633 fastshap: FastSHAP,634 original_model_wrapper,635 X_test: np.ndarray,636 shap_values: np.ndarray,637 n_samples: int = 10,638) -> None:639 """640 Verify the efficiency axiom: sum of SHAP values ≈ f(x) - f(null).641642 Args:643 fastshap: Trained FastSHAP object.644 original_model_wrapper: Callable original model.645 X_test: Test features.646 shap_values: SHAP values array (N, num_features, num_classes).647 n_samples: Number of samples to check.648 """649 print("\n[Efficiency Check] SHAP sum vs (f(x) - f(null)):")650 # Null prediction (empty input)651 null_input = np.zeros((1, NUM_FEATURES), dtype=np.float32)652 f_null = original_model_wrapper(null_input)[0] # (num_classes,)653654 for i in range(min(n_samples, len(X_test))):655 xi = X_test[i : i + 1]656 f_xi = original_model_wrapper(xi)[0] # (num_classes,)657 shap_sum = shap_values[i].sum(axis=0) # (num_classes,)658 target = f_xi - f_null659 print(660 f" Sample {i:2d} | SHAP sum: {shap_sum} "661 f"| f(x)-f(null): {target} "662 f"| diff: {np.abs(shap_sum - target).max():.4f}"663 )664665666# ─── Main ─────────────────────────────────────────────────────────────────────667668def main():669 print("=" * 60)670 print("FastSHAP Tabular Pipeline Demo")671 print("=" * 60)672673 # 1. Prepare data674 print("\n[Data] Generating synthetic classification dataset...")675 X_train, X_val, X_test, y_train, y_val, y_test = prepare_data()676 print(f" Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")677678 # 2. Train original model679 print("\n[OriginalModel] Training black-box model...")680 original_model = train_original_model(X_train, y_train)681 wrapper = NumpyModelWrapper(original_model, DEVICE)682683 # Sanity-check original model684 test_preds = wrapper(X_test[:5])685 print(f" Sample predictions (probabilities): {test_preds}")686687 # 3. Train surrogate688 surr = train_surrogate(wrapper, X_train, X_val)689690 # 4. Train FastSHAP explainer691 fastshap = train_fastshap_explainer(surr, X_train, X_val, wrapper)692693 # 5. Generate explanations694 shap_values = generate_and_inspect_shap_values(fastshap, X_test)695696 # 6. Efficiency check697 check_efficiency(fastshap, wrapper, X_test, shap_values, n_samples=5)698699 print("\n" + "=" * 60)700 print("Demo complete!")701 print("=" * 60)702703704if __name__ == "__main__":705 main()706```707708### `scripts/02_normalization_and_utils_demo.py`709710```python711#!/usr/bin/env python3712"""713FastSHAP Normalization & Utilities Demo714========================================715Demonstrates the low-level normalization functions and utility helpers716provided by FastSHAP:717718 - additive_efficient_normalization719 - multiplicative_efficient_normalization720 - evaluate_explainer721 - MarginalImputer and BaselineImputer usage722 - Surrogate.generate_labels helper723724These are the building blocks used internally by FastSHAP.train() and725can be useful when building custom726```