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

LIME from Scratch in C++

This post implements the core of LIME for numeric tabular rows. It samples perturbations around a row, queries the black-box predictor, weights each perturbation by distance, then solves weighted linear regression. The resulting coefficients explain the prediction locally.

Weighted Regression Helper

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

Vector solve_linear(Matrix A, Vector b) {
    int n = (int)b.size();
    for (int col = 0; col < n; ++col) {
        int piv = col;
        for (int r = col + 1; r < n; ++r)
            if (fabs(A[r][col]) > fabs(A[piv][col])) piv = r;
        swap(A[col], A[piv]); swap(b[col], b[piv]);
        double div = A[col][col];
        for (int c = col; c < n; ++c) A[col][c] /= div;
        b[col] /= div;
        for (int r = 0; r < n; ++r) if (r != col) {
            double m = A[r][col];
            for (int c = col; c < n; ++c) A[r][c] -= m * A[col][c];
            b[r] -= m * b[col];
        }
    }
    return b;
}

LIME Core

Vector lime_explain(const Predictor& pred,
                    const Vector& x0,
                    int samples,
                    double noiseStd,
                    double kernelWidth,
                    mt19937& rng) {
    int d = (int)x0.size();
    normal_distribution<double> noise(0.0, noiseStd);

    Matrix xtx(d + 1, Vector(d + 1, 0.0));
    Vector xty(d + 1, 0.0);

    for (int s = 0; s < samples; ++s) {
        Vector z = x0;
        double dist2 = 0.0;
        for (int j = 0; j < d; ++j) {
            double eps = noise(rng);
            z[j] += eps;
            dist2 += eps * eps;
        }
        double w = exp(-dist2 / (kernelWidth * kernelWidth));
        double y = pred(z);

        Vector row(d + 1, 1.0);              // intercept + features
        for (int j = 0; j < d; ++j) row[j + 1] = z[j] - x0[j];

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

    for (int j = 0; j < d + 1; ++j) xtx[j][j] += 1e-6; // ridge stabilizer
    Vector beta = solve_linear(xtx, xty);
    return Vector(beta.begin() + 1, beta.end());        // feature coefficients
}
Interpret only near x0. The returned coefficients are slopes of a local surrogate in the perturbation neighborhood. They are not global model coefficients.