# VALIDATION GUIDE — Spam Detection Pipeline v3.0 ## How to Verify Ensemble ### 1. Verify ensemble prediction produces correct shape ```python import numpy as np import scipy.sparse as sp from app.ml.ensemble import EnsemblePredictor # Mock a classical model class MockModel: def predict_proba(self, features): n = features.shape[0] return np.column_stack([np.full(n, 0.2), np.full(n, 0.8)]) ensemble = EnsemblePredictor( classical_model=MockModel(), classical_vectorizer_bundle={"word_vec": None}, transformer_model=None, # No transformer → classical-only fallback transformer_tokenizer=None, ) features = sp.csr_matrix(np.array([[0.1, 0.5]])) proba = ensemble.predict_proba(features, ["test message"]) assert proba.shape == (1, 2), f"Expected (1,2), got {proba.shape}" assert proba[0, 1] > proba[0, 0], "Spam probability should exceed ham" preds = ensemble.predict(features, ["test message"]) assert preds.shape == (1,), f"Expected (1,), got {preds.shape}" assert preds[0] == 1, "Should predict spam" print("Ensemble verification PASSED") ``` ### 2. Verify transformer_proba public API ```python from app.ml.ensemble import EnsemblePredictor e = EnsemblePredictor(None, {}) assert hasattr(e, "transformer_proba"), "Missing public transformer_proba" assert hasattr(e, "_transformer_proba"), "Missing private _transformer_proba" print("Public API verification PASSED") ``` ### 3. Verify ensemble routing in detector ```python from app.core.detector import _is_ensemble_model, _ensemble_predict import numpy as np, scipy.sparse as sp class MockEnsemble: def predict_proba(self, features, raw_texts): assert raw_texts == ["test"], "raw_texts not passed" return np.array([[0.3, 0.7]]) mock = MockEnsemble() spam, ham = _ensemble_predict(mock, sp.csr_matrix(np.array([[0.1]])), "test") assert spam == 0.7 and ham == 0.3, f"Expected 0.7/0.3, got {spam}/{ham}" print("Ensemble routing verification PASSED") ``` ## How to Verify Vectorizer Reuse (Stage 4 Fix) ### Verify that predict_proba uses Stage 2 vectorizer vocabulary ```python import numpy as np, pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer # Simulate Stage 2: fit vectorizer on train train_texts = ["free money", "urgent click", "hello friend", "meeting today"] train_labels = np.array([1, 1, 0, 0]) word_vec = TfidfVectorizer(max_features=100, ngram_range=(1,2)) word_vec.fit(train_texts) # Stage 2 vocabulary stage2_vocab = set(word_vec.get_feature_names_out()) # Simulate Stage 4: reuse vectorizer with .transform() test_texts = ["get money now urgent win"] x_test = word_vec.transform(test_texts) # Verify: feature matrix is compatible with Stage 2 classifier feature_count = x_test.shape[1] vocab_count = len(stage2_vocab) assert feature_count == vocab_count, \ f"Feature count {feature_count} != vocabulary size {vocab_count}" # Stage 4 old bug: re-create vectorizer and fit new_vec = TfidfVectorizer(max_features=100, ngram_range=(1,2)) new_vec.fit(train_texts) new_vocab = set(new_vec.get_feature_names_out()) # Verify: same vocab, but could differ in edge cases assert stage2_vocab == new_vocab, \ "Vocabularies differ — this is the bug vectorizer reuse prevents" print("Vectorizer reuse verification PASSED") ``` ## How to Verify Transformer Checkpoints ### 1. Verify checkpoint is saved to disk ```bash # After training completes (or during), verify file exists: ls -la model/checkpoints/DeBERTa-v3_best.pt ``` ### 2. Verify checkpoint can be loaded ```python import torch ckpt = torch.load("model/checkpoints/DeBERTa-v3_best.pt", map_location="cpu") assert len(ckpt) > 0, "Checkpoint is empty" for name, tensor in list(ckpt.items())[:3]: print(f" {name}: {tensor.shape}") ``` ### 3. Verify checkpoint matches model architecture ```python from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained( "microsoft/deberta-v3-base", num_labels=2 ) ckpt = torch.load("model/checkpoints/DeBERTa-v3_best.pt", map_location="cpu") model.load_state_dict(ckpt) # Should not raise print("Checkpoint architecture match PASSED") ``` ## How to Verify Exported Models ### 1. Verify XGBoost model loads and predicts ```python import pickle import numpy as np with open("model/spam_model.pkl", "rb") as f: model = pickle.load(f) with open("model/vectorizer.pkl", "rb") as f: vec = pickle.load(f) assert hasattr(model, "predict_proba"), "Model missing predict_proba" # Test single inference import scipy.sparse as sp from app.core.features import extract_meta_features text = "You have won a free prize! Click here now." word_feats = vec["word_vec"].transform([text]) meta_feats = sp.csr_matrix(extract_meta_features(text)) features = sp.hstack([word_feats, meta_feats], format="csr") proba = model.predict_proba(features) assert proba.shape == (1, 2), f"Expected (1,2), got {proba.shape}" print(f"Spam probability: {proba[0, 1]:.4f}") print("Model export verification PASSED") ``` ### 2. Verify SHA-256 integrity ```python import hashlib def verify_sha256(filepath, expected_sha_path): with open(filepath, "rb") as f: actual = hashlib.sha256(f.read()).hexdigest() expected = open(expected_sha_path).read().strip() assert actual == expected, f"SHA-256 mismatch: {actual[:8]} != {expected[:8]}" print(f"SHA-256 verified for {filepath}") verify_sha256("model/hf_model/model.safetensors", "model/hf_model/model.safetensors.sha256") ``` ### 3. Verify HF-native model loads correctly ```python from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch model = AutoModelForSequenceClassification.from_pretrained( "model/hf_model", local_files_only=True ) tokenizer = AutoTokenizer.from_pretrained( "model/hf_model", local_files_only=True ) assert model.config.num_labels == 2 assert tokenizer.pad_token == "[PAD]" text = "URGENT: Verify your account now!" inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) with torch.no_grad(): logits = model(**inputs).logits probs = torch.softmax(logits, dim=-1) print(f"Spam probability: {probs[0][1]:.4f}") print("HF-native model verification PASSED") ``` ### 4. Verify metadata completeness ```python import json with open("model/model_metadata.json") as f: meta = json.load(f) required_fields = ["model_name", "track", "trained_at_utc", "dataset_rows", "train_rows", "test_rows", "selected_metrics"] for field in required_fields: assert field in meta, f"Missing field: {field}" print(f" {field}: {meta[field]}") print("Metadata verification PASSED") ``` ## Integration Test Suite Run the full test suite: ```bash python -m pytest tests/ -v ``` All 205 tests must pass. Test coverage includes: - Detector routing (rule-based + ML + ensemble pathways) - Constants validation (regex patterns, keyword sets) - Feature extraction (all 32 meta features) - Domain extraction and validation - PII redaction (emails, phone numbers, credit cards) - Schema validation (request/response models) - Auth (API key middleware) - Rate limiting - CORS configuration - Bootstrap/health endpoint ## Production Deployment Validation ```bash # 1. Start the API server uvicorn app.main:app --host 0.0.0.0 --port 8000 # 2. Test health endpoint curl http://localhost:8000/v1/health # 3. Test prediction (requires API key if configured) curl -X POST http://localhost:8000/v1/predict \ -H "Content-Type: application/json" \ -d '{"sender":"phish@bad.com","subject":"Urgent: verify now","body":"Click here to verify your account"}' # 4. Expected response includes: # - "label": "Spam" or "Not Spam" # - "confidence": float 0-1 # - "reason": string # - "rule_layer": "rules" or "ml" # - "prediction_id": hex string # - "evaluated_at_utc": ISO timestamp ```