"""Linear SVM classifier - single-stage binary cyberbullying detection. Uses ``LinearSVC`` wrapped in ``CalibratedClassifierCV(method='sigmoid', cv=5)`` so that ``predict_proba`` returns proper probabilities (Platt scaling). RBF kernel is deferred - see README.md § Models. """ from __future__ import annotations import logging from pathlib import Path import joblib import numpy as np from sklearn.calibration import CalibratedClassifierCV from sklearn.svm import LinearSVC logger = logging.getLogger(__name__) class SVMModel: """Linear SVM with Platt-calibrated probabilities.""" # Target hyperparams to sweep during tuning: # C ∈ [0.1, 1, 10] (RBF + gamma sweep deferred) def __init__( self, C: float = 1.0, class_weight: str = "balanced", random_state: int = 42, max_iter: int = 10000, calibration_cv: int = 5, ) -> None: base = LinearSVC( C=C, class_weight=class_weight, random_state=random_state, max_iter=max_iter, ) self.model = CalibratedClassifierCV(base, method="sigmoid", cv=calibration_cv) def fit(self, X_train, y_train, X_val=None, y_val=None) -> None: """Fit on TF-IDF features. ``X_val``/``y_val`` accepted for API parity but unused.""" self.model.fit(X_train, y_train) logger.info("LinearSVC (calibrated) fitted on %d samples", X_train.shape[0]) def predict(self, X) -> np.ndarray: """Predict 0/1 labels.""" return self.model.predict(X) def predict_proba(self, X) -> np.ndarray: """Predict Platt-scaled class probabilities; shape ``(n, 2)``.""" return self.model.predict_proba(X) def save(self, path: Path) -> None: """Persist the fitted estimator to ``path`` via joblib.""" joblib.dump(self.model, str(path)) @classmethod def load(cls, path: Path) -> "SVMModel": """Load a previously saved estimator and return a fresh wrapper.""" instance = cls.__new__(cls) instance.model = joblib.load(str(path)) return instance