aegis-ml / app /classifiers /cascade_classifier.py
billybitcoin's picture
Deploy Aegis-ML to HF Spaces
5c76335 verified
Raw
History Blame Contribute Delete
4.9 kB
"""
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}