APERTURE_AUDIT / explain /lime_local.py
Darkweb007's picture
Initial commit: model-agnostic SHAP/LIME explainability audit dashboard
41dfe78
Raw
History Blame Contribute Delete
2.44 kB
"""
LIME-style local surrogate explanation.
Unlike shap_exact.py (which uses global knowledge of the model -- its
coefficients), this implements the actual LIME idea from scratch: perturb
the applicant's features randomly in a neighborhood around their real
values, get the real model's prediction for each perturbed point, then fit
a simple local linear regression to those (perturbation, prediction) pairs.
The regression's coefficients are the "LIME weights" -- an explanation that
never looked at the model's internals, only its input/output behavior.
Running both this and the exact SHAP values and checking they agree (see
app.py's "cross-check" panel) is a legitimate audit technique: two
independently-derived explanations of the same decision converging is much
stronger evidence than either alone -- and if they diverge, that's a signal
worth investigating before trusting the explanation.
"""
from __future__ import annotations
import random
import numpy as np
from sklearn.linear_model import LinearRegression
from model import TrainedModel, FEATURES, FEATURE_LABELS, predict_proba
def lime_explain(trained: TrainedModel, applicant: dict, n_samples: int = 400, seed: int = 0) -> dict:
rng = np.random.default_rng(seed)
x0 = np.array([applicant[f] for f in FEATURES], dtype=float)
# feature-wise perturbation scale: 15% of the training population's std,
# so the local neighborhood is meaningful relative to each feature's range
stds = np.array([max(trained.feature_means[f] * 0.15, 0.5) for f in FEATURES])
samples = x0 + rng.normal(0, 1, size=(n_samples, len(FEATURES))) * stds
samples = np.clip(samples, 0, None)
preds = []
for row in samples:
applicant_perturbed = dict(zip(FEATURES, row))
preds.append(predict_proba(trained, applicant_perturbed))
preds = np.array(preds)
# weight samples by proximity to x0 (closer perturbations matter more --
# the defining idea of LIME's "local" fidelity)
dists = np.linalg.norm((samples - x0) / stds, axis=1)
weights = np.exp(-(dists ** 2) / 2)
local_model = LinearRegression()
local_model.fit(samples, preds, sample_weight=weights)
lime_weights = dict(zip(FEATURES, local_model.coef_))
local_r2 = local_model.score(samples, preds, sample_weight=weights)
return {
"weights": lime_weights,
"local_fidelity_r2": float(local_r2),
"n_samples": n_samples,
}