Download app/model_utils.py from IInsignia/dave: direct link, hf CLI and curl.
- Browser
- Download file 3.13 kB
-
https://huggingface.co/spaces/IInsignia/dave/resolve/main/app/model_utils.py
- Command line
-
hf download hf://spaces/IInsignia/dave/app/model_utils.py
-
curl -L -o model_utils.py https://huggingface.co/spaces/IInsignia/dave/resolve/main/app/model_utils.py
3.13 kB
| """ | |
| Loads the fine-tuned DistilBERT model (winner of Objective O2's three-way | |
| comparison, tables/T13_full_model_comparison.csv) and provides: | |
| - predict_proba(texts) -> class probabilities, in the shape LIME expects | |
| - classify(text) -> verdict + confidence for a single email | |
| - explain(text) -> LIME word-level attribution for the verdict | |
| Label convention (fixed at training time in the Colab notebook, must match | |
| here): 0 = Legitimate, 1 = Phishing. | |
| """ | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| from lime.lime_text import LimeTextExplainer | |
| MODEL_DIR = Path(__file__).resolve().parents[1] / "models" / "distilbert_final_model" | |
| MAX_LENGTH = 512 | |
| LABEL_NAMES = {0: "Legitimate", 1: "Phishing"} | |
| _tokenizer = None | |
| _model = None | |
| _explainer = LimeTextExplainer(class_names=[LABEL_NAMES[0], LABEL_NAMES[1]]) | |
| def load(): | |
| global _tokenizer, _model | |
| if _model is None: | |
| if not MODEL_DIR.exists(): | |
| raise FileNotFoundError( | |
| f"Model directory not found at {MODEL_DIR}. " | |
| "Download 'distilbert_final_model' from the Colab notebook's Drive output first." | |
| ) | |
| _tokenizer = AutoTokenizer.from_pretrained(str(MODEL_DIR)) | |
| _model = AutoModelForSequenceClassification.from_pretrained(str(MODEL_DIR)) | |
| _model.eval() | |
| return _tokenizer, _model | |
| def predict_proba(texts, batch_size=32): | |
| """Returns an (n_samples, 2) array of [P(Legitimate), P(Phishing)]. Required shape for LIME.""" | |
| tokenizer, model = load() | |
| all_probs = [] | |
| with torch.no_grad(): | |
| for i in range(0, len(texts), batch_size): | |
| batch = list(texts[i:i + batch_size]) | |
| enc = tokenizer( | |
| batch, truncation=True, max_length=MAX_LENGTH, padding=True, return_tensors="pt" | |
| ) | |
| logits = model(**enc).logits | |
| probs = torch.softmax(logits, dim=1).numpy() | |
| all_probs.append(probs) | |
| return np.concatenate(all_probs, axis=0) | |
| def classify(text: str) -> dict: | |
| probs = predict_proba([text])[0] | |
| pred_idx = int(np.argmax(probs)) | |
| return { | |
| "verdict": LABEL_NAMES[pred_idx], | |
| "confidence": float(probs[pred_idx]), | |
| "phishing_probability": float(probs[1]), | |
| "legitimate_probability": float(probs[0]), | |
| } | |
| def explain(text: str, num_features: int = 10, num_samples: int = 100): | |
| """ | |
| Returns a list of (word, weight) tuples for the Phishing class. | |
| Positive weight = pushed the model toward "Phishing". | |
| Negative weight = pushed the model toward "Legitimate". | |
| Only the first ~500 words are passed to LIME: the model itself truncates | |
| at 512 tokens, so perturbing words beyond that window would not affect | |
| the prediction and would only slow down the explanation. | |
| """ | |
| words = text.split() | |
| truncated_text = " ".join(words[:500]) | |
| exp = _explainer.explain_instance( | |
| truncated_text, predict_proba, num_features=num_features, num_samples=num_samples, labels=[1] | |
| ) | |
| return exp.as_list(label=1) | |