Spaces:
Sleeping
Sleeping
File size: 5,420 Bytes
9d8100e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | """
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)
|