import time from dataclasses import dataclass, field import numpy as np import torch from src.data.preprocessor import clean_text, load_label_map from src.features.tfidf import load_vectorizer, transform from src.models.classical import LogisticRegressionModel, SVMModel from src.models.neural import LSTMModel, RNNModel, TextCNN, Vocabulary from src.models.transformer import TransformerModel from src.utils.config import load_config from src.utils.settings import settings VECTORIZER_PATH = "artifacts/vectorizers/tfidf.pkl" VOCAB_PATH = "artifacts/models/vocab.pkl" LOGREG_PATH = "artifacts/models/logreg.pkl" SVM_PATH = "artifacts/models/svm.pkl" TEXTCNN_PATH = "artifacts/models/textcnn.pt" RNN_PATH = "artifacts/models/rnn.pt" LSTM_PATH = "artifacts/models/lstm.pt" DISTILBERT_DIR = "artifacts/models/distilbert" MAX_LENGTH_NN = 32 MAX_LENGTH_HF = 128 SUPPORTED_MODELS = {"classical", "svm", "textcnn", "rnn", "lstm", "transformer"} @dataclass class PredictionResult: intent: str confidence: float top5: list[dict] = field(default_factory=list) latency_ms: float = 0.0 is_oos: bool = False model_used: str = "" class Predictor: def __init__(self, model_type: str): if model_type not in SUPPORTED_MODELS: raise ValueError(f"Unknown model_type: {model_type}, must be one of {SUPPORTED_MODELS}") self.model_type = model_type self.label_map = load_label_map() self.id_to_label = {v: k for k, v in self.label_map.items()} self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.vectorizer = None self._vocab = None self._model = None self._tokenizer = None self.load() def load(self) -> None: if self.model_type in ("classical", "svm"): self._vectorizer = load_vectorizer(VECTORIZER_PATH) if self.model_type == "classical": self._model = LogisticRegressionModel() self._model.load(LOGREG_PATH) else: self._model = SVMModel() self._model.load(SVM_PATH) elif self.model_type in ("textcnn", "rnn", "lstm"): self._vocab = Vocabulary.load(VOCAB_PATH) neural_config = load_config("neural") model_cfg = neural_config["model"][self.model_type] num_classes = len(self.label_map) if self.model_type == "textcnn": self._model = TextCNN( vocab_size=len(self._vocab), embedding_dim=model_cfg["embedding_dim"], num_filters=model_cfg["num_filters"], kernel_sizes=model_cfg["kernel_sizes"], num_classes=num_classes, dropout=model_cfg["dropout"], ) self._model.load(TEXTCNN_PATH) elif self.model_type == "rnn": self._model = RNNModel( vocab_size=len(self._vocab), embedding_dim=model_cfg["embedding_dim"], hidden_dim=model_cfg["hidden_dim"], num_layers=model_cfg["num_layers"], num_classes=num_classes, dropout=model_cfg["dropout"], ) self._model.load(RNN_PATH) else: self._model = LSTMModel( vocab_size=len(self._vocab), embedding_dim=model_cfg["embedding_dim"], hidden_dim=model_cfg["hidden_dim"], num_layers=model_cfg["num_layers"], num_classes=num_classes, dropout=model_cfg["dropout"], ) self._model.load(LSTM_PATH) self._model.to(self.device) self._model.eval() elif self.model_type == "transformer": self._model = TransformerModel( model_name=DISTILBERT_DIR, num_labels=len(self.label_map), ) self._model.model.to(self.device) self._model.model.eval() def _predict_proba(self, text: str) -> np.ndarray: cleaned = clean_text(text) if self.model_type in ("classical", "svm"): X = transform(self._vectorizer, [cleaned]) if self.model_type == "classical": return self._model.predict_proba(X)[0] scores = self._model.model.decision_function(X)[0] exp_scores = np.exp(scores - np.max(scores)) return exp_scores / exp_scores.sum() if self.model_type in ("textcnn", "rnn", "lstm"): encoded = self._vocab.encode(cleaned, MAX_LENGTH_NN) tensor = torch.tensor([encoded], dtype=torch.long).to(self.device) return self._model.predict_proba(tensor)[0] if self.model_type == "transformer": return self._model.predict_proba([cleaned], MAX_LENGTH_HF)[0] raise ValueError(f"unsupported model_type: {self.model_type}") def predict(self, text: str) -> PredictionResult: start = time.perf_counter() probs = self._predict_proba(text) latency_ms = (time.perf_counter() - start) * 1000 top_indices = np.argsort(probs)[-5:][::-1] top5 = [{"intent": self.id_to_label[idx], "confidence": round(float(probs[idx]), 4)} for idx in top_indices] best_idx = int(top_indices[0]) intent = self.id_to_label[best_idx] confidence = float(probs[best_idx]) is_oos = confidence < settings.oos_threshold return PredictionResult( intent=intent, confidence=round(confidence, 4), top5=top5, latency_ms=round(latency_ms, 3), is_oos=is_oos, model_used=self.model_type, ) def predict_batch(self, texts: list[str]) -> list[PredictionResult]: return [self.predict(text) for text in texts]