mdarahmanxAI's picture
Deploy AI vs human detector with LLM explanations
d433145 verified
Raw
History Blame Contribute Delete
3.21 kB
"""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), ...]