Spaces:
Sleeping
Sleeping
| """Explain why a model made its prediction. | |
| Three cases, in order of how directly interpretable the model is: | |
| - Linear SVM: the prediction is a weighted sum, so each word's signed | |
| contribution to *this* passage is exact (tf-idf value times its weight). | |
| - Decision Tree / AdaBoost: only a global `feature_importances_` is available, | |
| so we show the influential words that appear in the passage and say plainly | |
| that this reflects the model overall, not this passage alone. | |
| - Neural networks: no readable weights. We say so and rely on the linguistic | |
| statistics panel instead. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from sklearn.calibration import CalibratedClassifierCV | |
| from .preprocessing import clean_text | |
| def _linear_coef(model) -> np.ndarray | None: | |
| """Class-1 coefficient vector for a linear model, averaged if calibrated.""" | |
| if isinstance(model, CalibratedClassifierCV): | |
| coefs = [] | |
| for cc in model.calibrated_classifiers_: | |
| est = getattr(cc, "estimator", None) or getattr(cc, "base_estimator", None) | |
| if est is not None and hasattr(est, "coef_"): | |
| coefs.append(est.coef_.ravel()) | |
| return np.mean(coefs, axis=0) if coefs else None | |
| if hasattr(model, "coef_"): | |
| return model.coef_.ravel() | |
| return None | |
| def explain_prediction(detector, model_name: str, text: str, top_k: int = 10) -> dict: | |
| """Return the words that drove the prediction, shaped for display.""" | |
| model = detector.models[model_name] | |
| names = detector.feature_names | |
| x = detector.tfidf.transform([clean_text(text)]) | |
| present = x.toarray().ravel() | |
| coef = _linear_coef(model) | |
| if coef is not None: | |
| contrib = present * coef | |
| nz = np.nonzero(contrib)[0] | |
| order = nz[np.argsort(contrib[nz])] | |
| toward_human = [(names[i], float(contrib[i])) for i in order[:top_k]] | |
| toward_ai = [(names[i], float(contrib[i])) for i in order[::-1][:top_k]] | |
| return {"kind": "signed", | |
| "toward_ai": [t for t in toward_ai if t[1] > 0], | |
| "toward_human": [t for t in toward_human if t[1] < 0]} | |
| if hasattr(model, "feature_importances_"): | |
| importance = model.feature_importances_ * (present > 0) | |
| idx = np.argsort(importance)[::-1] | |
| top = [(names[i], float(importance[i])) for i in idx[:top_k] if importance[i] > 0] | |
| return {"kind": "importance", "words": top} | |
| return {"kind": "none"} | |
| def lime_explanation(detector, model_name: str, text: str, | |
| num_features: int = 10, num_samples: int = 400): | |
| """Model-agnostic explanation via LIME. Works for any model, including the | |
| neural networks, but is slower, so the app runs it only on request.""" | |
| from lime.lime_text import LimeTextExplainer | |
| explainer = LimeTextExplainer(class_names=["Human", "AI"]) | |
| def predict_proba(texts): | |
| p_ai = detector.proba_from_raw(model_name, list(texts)) | |
| return np.column_stack([1.0 - p_ai, p_ai]) | |
| exp = explainer.explain_instance( | |
| clean_text(text), predict_proba, | |
| num_features=num_features, num_samples=num_samples) | |
| return exp.as_list() # [(word, weight toward AI), ...] | |