Spaces:
Sleeping
Sleeping
| """ | |
| proxy/classifier.py | |
| Layer 2 of the detection pipeline: ML-based sequence classification. | |
| Attempts to load a fine-tuned DistilBERT model from ./models/classifier/. | |
| If the model is not found, falls back to a rule-based scorer that mimics a | |
| classifier output (suitable for development/testing without training). | |
| NOTE: Train a real model with training/finetune.py for production use. | |
| """ | |
| import logging | |
| import os | |
| import random | |
| from typing import Optional | |
| import numpy as np | |
| logger = logging.getLogger(__name__) | |
| # --------------------------------------------------------------------------- | |
| # Module-level model cache β loaded once, reused on subsequent calls | |
| # --------------------------------------------------------------------------- | |
| _model = None | |
| _tokenizer = None | |
| _model_loaded: bool = False | |
| _use_fallback: bool = False | |
| MODEL_PATH = os.path.join(os.path.dirname(__file__), "..", "models", "classifier") | |
| def _load_model() -> None: | |
| """ | |
| Attempt to load the fine-tuned DistilBERT classifier from disk. | |
| Sets module-level flags so loading only happens once. | |
| """ | |
| global _model, _tokenizer, _model_loaded, _use_fallback | |
| if _model_loaded: | |
| return | |
| model_dir = os.path.abspath(MODEL_PATH) | |
| if not os.path.isdir(model_dir) or not os.listdir(model_dir): | |
| logger.warning( | |
| "Classifier model not found at %s. " | |
| "Using fallback rule-based scorer. " | |
| "Run training/finetune.py to train a real model.", | |
| model_dir, | |
| ) | |
| _use_fallback = True | |
| _model_loaded = True | |
| return | |
| try: | |
| from transformers import AutoModelForSequenceClassification, AutoTokenizer # type: ignore | |
| import torch # type: ignore | |
| logger.info("Loading classifier from %s β¦", model_dir) | |
| _tokenizer = AutoTokenizer.from_pretrained(model_dir) | |
| _model = AutoModelForSequenceClassification.from_pretrained(model_dir) | |
| _model.eval() | |
| # Move to GPU if available | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| _model = _model.to(device) | |
| logger.info("Classifier loaded successfully (device=%s)", device) | |
| _use_fallback = False | |
| except Exception as exc: | |
| logger.error("Failed to load classifier model: %s β falling back to rule-based scorer.", exc) | |
| _use_fallback = True | |
| _model_loaded = True | |
| # --------------------------------------------------------------------------- | |
| # Fallback scorer | |
| # --------------------------------------------------------------------------- | |
| def _fallback_score(text: str, heuristic_score: float) -> float: | |
| """ | |
| Improved rule-based classifier fallback for when the ML model is not available. | |
| Uses a piecewise linear mapping to approximate a trained classifier's output | |
| distribution. A real DistilBERT classifier pushes clear injections (high | |
| heuristic) to 0.85β0.95; this fallback mirrors that behaviour. | |
| Mapping: | |
| heuristic 0.0 β 0.2 β classifier 0.00 β 0.35 (clean territory) | |
| heuristic 0.2 β 0.5 β classifier 0.35 β 0.72 (borderline) | |
| heuristic 0.5 β 1.0 β classifier 0.72 β 0.96 (injection territory) | |
| NOTE: Train a real model with training/finetune.py for production use. | |
| The fallback exists only so the proxy can run end-to-end without training. | |
| """ | |
| # Seed on text content so results are deterministic per prompt | |
| rng = random.Random(hash(text) & 0xFFFFFFFF) | |
| noise = rng.uniform(-0.03, 0.03) | |
| if heuristic_score >= 0.5: | |
| # Clear injection: map [0.5, 1.0] β [0.72, 0.96] | |
| scaled = 0.72 + (heuristic_score - 0.5) * 0.48 | |
| elif heuristic_score >= 0.2: | |
| # Borderline: map [0.2, 0.5] β [0.35, 0.72] | |
| scaled = 0.35 + (heuristic_score - 0.2) * (0.37 / 0.3) | |
| else: | |
| # Clean: map [0.0, 0.2] β [0.0, 0.35] | |
| scaled = heuristic_score * 1.75 | |
| return float(min(1.0, max(0.0, scaled + noise))) | |
| # --------------------------------------------------------------------------- | |
| # Public interface | |
| # --------------------------------------------------------------------------- | |
| def classify(text: str, heuristic_score: float = 0.0) -> float: | |
| """ | |
| Return injection confidence score in [0, 1] for class 1 (injection). | |
| Args: | |
| text: The user prompt text to classify. | |
| heuristic_score: Score from Layer 1, used by the fallback scorer. | |
| Returns: | |
| Confidence float between 0 (clean) and 1 (injection). | |
| """ | |
| _load_model() | |
| if _use_fallback: | |
| return _fallback_score(text, heuristic_score) | |
| try: | |
| import torch # type: ignore | |
| device = next(_model.parameters()).device | |
| inputs = _tokenizer( | |
| text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=512, | |
| padding=True, | |
| ) | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = _model(**inputs) | |
| logits = outputs.logits | |
| # Softmax over logits β probabilities; index 1 = injection class | |
| probs = torch.softmax(logits, dim=-1) | |
| injection_prob = probs[0][1].item() | |
| return float(injection_prob) | |
| except Exception as exc: | |
| logger.error("Classifier inference failed: %s β returning heuristic fallback.", exc) | |
| return _fallback_score(text, heuristic_score) | |