← All Posts
From Scratch · C++ · ML · Explainability · Part 3

Kernel SHAP from Scratch in C++

Kernel SHAP estimates Shapley values by fitting a weighted linear model on coalition masks. The black-box model is queried on rows where some features come from the row being explained and absent features come from a background row.

Coalition Helpers

using Vector = vector<double>;
using Matrix = vector<Vector>;
using Predictor = function<double(const Vector&)>;

double choose_weight(int m, int s) {
    if (s == 0 || s == m) return 1000.0;   // anchor empty and full coalitions
    double comb = 1.0;
    for (int i = 1; i <= s; ++i) comb = comb * (m - s + i) / i;
    return (m - 1.0) / (comb * s * (m - s));
}

Vector masked_row(const Vector& x0, const Vector& bg, const vector<int>& mask) {
    Vector z = x0;
    for (size_t j = 0; j < z.size(); ++j)
        if (!mask[j]) z[j] = bg[j];
    return z;
}

Kernel SHAP Core

The same solve_linear helper from the LIME post solves the weighted normal equations. The design matrix is now binary masks rather than numeric perturbations.

Vector kernel_shap(const Predictor& pred,
                   const Vector& x0,
                   const Matrix& background) {
    int m = (int)x0.size();
    int cols = m + 1; // intercept + one coefficient per feature
    Matrix xtx(cols, Vector(cols, 0.0));
    Vector xty(cols, 0.0);

    for (int maskBits = 0; maskBits < (1 << m); ++maskBits) {
        vector<int> mask(m, 0);
        int s = 0;
        for (int j = 0; j < m; ++j) {
            mask[j] = (maskBits >> j) & 1;
            s += mask[j];
        }
        double w = choose_weight(m, s);

        for (const auto& bg : background) {
            Vector z = masked_row(x0, bg, mask);
            double y = pred(z);
            Vector row(cols, 1.0);
            for (int j = 0; j < m; ++j) row[j + 1] = mask[j];

            for (int a = 0; a < cols; ++a) {
                xty[a] += w * row[a] * y;
                for (int b = 0; b < cols; ++b)
                    xtx[a][b] += w * row[a] * row[b];
            }
        }
    }

    for (int j = 0; j < cols; ++j) xtx[j][j] += 1e-8;
    Vector beta = solve_linear(xtx, xty);
    return Vector(beta.begin() + 1, beta.end());
}
phi[0] = +0.18 phi[1] = -0.07 phi[2] = +0.31 sum(phi) + baseline should match the prediction
Exact enumeration is only for small feature counts. The loop over 1 << m is exponential. Real Kernel SHAP samples masks when there are many features.