Spaces:
Sleeping
Sleeping
| """ | |
| app/classifiers/cascade_classifier.py | |
| ======================================= | |
| Two-stage cascade classifier: sklearn fast path → ONNX/HF slow path. | |
| For inputs where sklearn is highly confident (prob ≥ high_threshold or | |
| prob ≤ low_threshold), the verdict is returned immediately without invoking | |
| the heavier ONNX/HF model. Only inputs in the ambiguous "grey zone" are | |
| escalated. | |
| Expected traffic split (empirical): | |
| ~85-90% cleared by sklearn → ~0.3 ms average | |
| ~10-15% escalated to ONNX → ~3-8 ms for those samples | |
| Blended average latency → ~0.5-1.5 ms | |
| The thresholds are tunable via CASCADE_SK_LOW_THRESHOLD / | |
| CASCADE_SK_HIGH_THRESHOLD environment variables (see config.py). | |
| Predict return dict is identical to individual classifiers plus a diagnostic | |
| "stage" key ("sklearn" | "onnx") for audit logging / metrics. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| from typing import Any | |
| logger = logging.getLogger(__name__) | |
| class CascadeClassifier: | |
| """ | |
| Combines a fast sklearn classifier with a slower ONNX/HF classifier. | |
| Parameters | |
| ---------- | |
| sklearn_clf : SklearnClassifier | |
| Fast first-stage classifier (~0.3 ms/sample). | |
| slow_clf : ONNXClassifier | HFClassifier | |
| Accurate slow-path classifier (~3-15 ms/sample). | |
| low_threshold : float | |
| sklearn malicious_prob ≤ this → benign fast path (default 0.05). | |
| high_threshold : float | |
| sklearn malicious_prob ≥ this → malicious fast path (default 0.95). | |
| Both classifiers must be loaded before calling predict(). | |
| """ | |
| def __init__( | |
| self, | |
| sklearn_clf, | |
| slow_clf, | |
| low_threshold: float = 0.05, | |
| high_threshold: float = 0.95, | |
| slow_clf_label: str = "onnx", | |
| ) -> None: | |
| self.sklearn_clf = sklearn_clf | |
| self.slow_clf = slow_clf | |
| self.low_threshold = low_threshold | |
| self.high_threshold = high_threshold | |
| self.slow_clf_label = slow_clf_label | |
| def is_loaded(self) -> bool: | |
| return self.sklearn_clf.is_loaded() and self.slow_clf.is_loaded() | |
| async def predict(self, text: str) -> dict[str, Any]: | |
| """ | |
| Run the two-stage cascade. | |
| Returns: | |
| { | |
| "label": "benign" | "malicious", | |
| "malicious_prob": float, | |
| "benign_prob": float, | |
| "stage": "sklearn" | "onnx", # diagnostic only | |
| } | |
| """ | |
| if not self.is_loaded(): | |
| raise RuntimeError( | |
| "CascadeClassifier: one or both classifiers are not loaded." | |
| ) | |
| # ── Stage 1: fast sklearn check ──────────────────────────────────────── | |
| sk_result = await self.sklearn_clf.predict(text) | |
| sk_prob: float = sk_result["malicious_prob"] | |
| if sk_prob >= self.high_threshold: | |
| logger.debug( | |
| "Cascade fast-path MALICIOUS (sklearn=%.3f ≥ %.2f)", | |
| sk_prob, | |
| self.high_threshold, | |
| ) | |
| return {**sk_result, "stage": "sklearn"} | |
| if sk_prob <= self.low_threshold: | |
| logger.debug( | |
| "Cascade fast-path benign (sklearn=%.3f ≤ %.2f)", | |
| sk_prob, | |
| self.low_threshold, | |
| ) | |
| return {**sk_result, "stage": "sklearn"} | |
| # ── Stage 2: grey zone → escalate to ONNX/HF ────────────────────────── | |
| logger.debug( | |
| "Cascade escalating to ONNX (sklearn=%.3f in grey zone [%.2f, %.2f])", | |
| sk_prob, | |
| self.low_threshold, | |
| self.high_threshold, | |
| ) | |
| slow_result = await self.slow_clf.predict(text) | |
| # Take the maximum malicious probability from both stages so that a | |
| # strong sklearn signal is never cancelled by a less-accurate slow | |
| # classifier (e.g. INT8-quantised ONNX dropping a clear attack below | |
| # the final threshold). | |
| slow_prob: float = slow_result.get("malicious_prob", 0.0) | |
| if sk_prob > slow_prob: | |
| logger.debug( | |
| "Cascade: sklearn signal (%.3f) > slow clf (%.3f) — using sklearn prob", | |
| sk_prob, | |
| slow_prob, | |
| ) | |
| combined_prob = sk_prob | |
| combined_benign = 1.0 - sk_prob | |
| combined_label = "malicious" if combined_prob >= 0.5 else "benign" | |
| return { | |
| **slow_result, | |
| "malicious_prob": combined_prob, | |
| "benign_prob": combined_benign, | |
| "label": combined_label, | |
| "stage": f"sklearn_override({self.slow_clf_label})", | |
| } | |
| return {**slow_result, "stage": self.slow_clf_label} | |