Skip to main content
Milestone #03Not StartedPhase 2Intermediate12–16 hours · 1–2 weeks11 required tasks

Spam Classifier

Build an end-to-end text classification pipeline with Precision, Recall, and F1 analysis

PythonNumPyscikit-learn
Build Progress0 / 11 tasks (0%)
0%(0/11 steps)Not Started
Project Specification & Architecture

What You Will Build

Before deep neural networks, statistical text classification powered spam filters, document routing, and sentiment analysis—and its core concepts (tokenization, vocabulary indexing, log-probabilities, binary cross-entropy, and Precision/Recall tradeoffs) remain essential in modern LLM pipelines. In this project, you will build a complete spam classification system from raw text messages to calibrated probability predictions. You will preprocess text, extract Bag-of-Words or TF-IDF features, implement Multinomial Naive Bayes (with Laplace smoothing in log-space) or binary Logistic Regression from scratch in NumPy, evaluate Precision, Recall, and F1 on a stratified test split, inspect false positives and false negatives, and benchmark against `scikit-learn`.

Why This Project Matters in the AI/LLM Stack

Accuracy alone is misleading on imbalanced datasets (if 87% of messages are legitimate ham, predicting 'ham' every time gets 87% accuracy with 0% spam recall). Building a spam filter teaches you how to build leak-free text vocabularies, prevent underflow with log-probabilities, and tune decision thresholds using Precision and Recall.

Learning Objectives

  • Inspect class imbalance and construct a stratified train/validation/test split without vocabulary leakage.
  • Convert variable-length text messages into numerical Bag-of-Words count vectors or TF-IDF matrices.
  • Implement Multinomial Naive Bayes or binary Logistic Regression from scratch in NumPy.
  • Compute Precision, Recall, F1, and confusion matrices, analyze misclassified messages, and compare with a scikit-learn baseline.

Prerequisites

  • Phase 1 Probability & Statistics (conditional probability, Bayes' theorem, log-likelihood)
  • Phase 2 Supervised Learning fundamentals (train/test splits, overfitting, classification metrics)

Required Mathematical Concepts

  • Bayes' Theorem and conditional independence: P(y | x₁,…,x_T) ∝ P(y) ∏_t P(x_t | y)
  • Log-space computation to prevent floating-point underflow: log P(y) + ∑_t log P(x_t | y)
  • Laplace (add-α) smoothing for unseen vocabulary words
  • Logistic sigmoid σ(z) = 1 / (1 + e⁻ᶻ) and Binary Cross-Entropy loss
  • Confusion Matrix (TP, FP, TN, FN), Precision, Recall, and F1 harmonic mean
Interactive Engineering Checklist

Step-by-Step Implementation Tasks (11 Steps)

Check off each engineering task as your code passes its acceptance criteria. Progress is saved automatically in your browser.

Stage 1 of 30/3 tasks complete

Dataset Curation, Class Distribution & Text Preprocessing

Load a labeled SMS/email dataset, inspect class ratios, and build a clean text tokenizer.

Core

Public benchmark datasets like the UCI SMS Spam Collection (CC BY 4.0 / public domain research corpus) provide thousands of real labeled `ham` and `spam` messages.

Implementation: Download or bundle a permissively licensed labeled spam/ham dataset (such as the UCI SMS Spam Collection or a curated TSV/CSV fixture) and write a loader that parses `(label, text)` pairs cleanly.
Expected Outcome: Dataset loader returns clean text strings and binary integer labels (`0` = ham, `1` = spam).
Acceptance Check: Dataset license and source attribution are documented in `README.md`.
Core

Real-world spam datasets are typically skewed (e.g., ~86.6% ham vs. ~13.4% spam), requiring stratified splitting and non-accuracy metrics.

Implementation: Compute total message counts, class percentages, and average character/word lengths per class. Perform a stratified train/test split (`80% / 20%`) preserving the exact spam ratio in both splits.
Expected Outcome: Summary report of class counts and stratified `train` and `test` splits.
Acceptance Check: Spam proportion in `y_train` and `y_test` matches within 0.5%.
Core

Consistent normalization prevents `'FREE!'`, `'free'`, and `'Free'` from being treated as unrelated features while preserving spam-indicative tokens like currency symbols or URLs.

Implementation: Implement `tokenize(text: str) -> list[str]` that lowercases text, normalizes numbers/URLs/currency symbols into canonical tokens (e.g. `<NUM>`, `<URL>`, `<CURR>`), and splits on word boundaries.
Expected Outcome: `tokenize('Claim £1000 FREE at http://win.com!')` produces `['claim', '<CURR>', '<NUM>', 'free', 'at', '<URL>']`.
Acceptance Check: Handles empty strings and punctuation-only messages without crashing.
Stage 2 of 30/4 tasks complete

Feature Extraction & From-Scratch Classifier Implementation

Build leak-free Bag-of-Words/TF-IDF features and implement your classifier from scratch.

Intermediate

Classifiers operate on numerical vectors `x ∈ ℝ^V`. Building the vocabulary strictly on `X_train` prevents test-set data leakage.

Implementation: Implement a `Vectorizer` class with `.fit(train_texts)` (building a `word -> index` vocabulary of terms appearing at least `min_df` times in training data) and `.transform(texts)` returning a NumPy Bag-of-Words count matrix or TF-IDF matrix of shape `(N, V)`.
Expected Outcome: `X_train_vec` of shape `(N_train, V)` and `X_test_vec` of shape `(N_test, V)`.
Acceptance Check: Out-of-vocabulary words in `test_texts` are ignored or mapped to `<UNK>` without mutating vocabulary size `V`.
Intermediate

You may implement either Multinomial Naive Bayes (computing log class priors and Laplace-smoothed log word likelihoods) or binary Logistic Regression (`ŷ = σ(Xw + b)` with BCE gradient descent) from scratch in NumPy.

Implementation: Create `SpamClassifierScratch` in NumPy. If implementing Naive Bayes, store `log_priors` and `log_likelihoods` with Laplace smoothing parameter `alpha=1.0`. If implementing Logistic Regression, implement `sigmoid`, binary cross-entropy loss, and L2-regularized gradient descent.
Expected Outcome: Self-contained NumPy classifier class with `.fit(X, y)`, `.predict_proba(X)`, and `.predict(X, threshold=0.5)`.
Acceptance Check: Core classifier uses only Python and NumPy (no `sklearn` inside `SpamClassifierScratch`).
Core

Fitting the model on the vectorized training corpus estimates the spam vs. ham token distributions or feature weights.

Implementation: Fit `SpamClassifierScratch` on `(X_train_vec, y_train)` and inspect the top 15 tokens most strongly associated with `spam` vs. `ham`.
Expected Outcome: Trained model parameters and a ranked list of top spam-indicative tokens (e.g., `free`, `claim`, `<URL>`, `txt`, `prize`).
Acceptance Check: Training completes cleanly with no `NaN` or `-inf` log probabilities.
Core

Converting raw scores into calibrated probabilities allows adjusting the classification threshold (`0.5` vs `0.8`) to prioritize high precision.

Implementation: Implement `.predict_proba(X) -> np.ndarray` (using the log-sum-exp trick for Naive Bayes or `sigmoid` for Logistic Regression) and `.predict(X, threshold=0.5) -> np.ndarray`.
Expected Outcome: Predicted probabilities in `[0.0, 1.0]` and binary predictions `{0, 1}`.
Acceptance Check: `predict_proba` rows sum to `1.0` and never overflow on long messages.
Stage 3 of 30/4 tasks complete

Evaluation Metrics, Error Analysis & Baseline Comparison

Compute Precision/Recall/F1, inspect false positives/negatives, compare against scikit-learn, and document limitations.

Core

In spam filtering, a False Positive (legitimate email sent to spam) is often more costly than a False Negative.

Implementation: Implement `confusion_matrix_metrics(y_true, y_pred)` from scratch in NumPy computing `TP, FP, TN, FN`, `Accuracy`, `Precision = TP / (TP + FP)`, `Recall = TP / (TP + FN)`, and `F1 = 2 * P * R / (P + R)`.
Expected Outcome: Full metrics report on the held-out test split.
Acceptance Check: Handles zero-division edge cases gracefully and matches `sklearn.metrics` values.
Core

Reading the actual messages your model got wrong reveals dataset quirks, sarcasm, short messages, or missing features.

Implementation: Write an error-analysis routine that prints all False Positives (ham predicted as spam) and False Negatives (spam predicted as ham) along with their predicted spam probabilities and top contributing tokens.
Expected Outcome: Diagnostic inspection of misclassified examples with written observations.
Acceptance Check: Includes at least 3 concrete examples of false positives/negatives and explains why the model misclassified them.
Core

Benchmarking against `sklearn.naive_bayes.MultinomialNB` or `sklearn.linear_model.LogisticRegression` verifies your implementation's statistical performance.

Implementation: Train a `scikit-learn` baseline on the identical train/test split and compare Accuracy, Precision, Recall, and F1 side-by-side.
Expected Outcome: Side-by-side benchmark table showing your from-scratch model within ~1% F1 of `scikit-learn`.
Acceptance Check: Both models are evaluated on the exact same held-out test set.
Core

Bag-of-Words models ignore word order (`'not free'` vs `'free'`), context, and adversarial character substitutions (`'fr33'`).

Implementation: Document the failure modes of bag-of-words classification (lack of word order, vocabulary drift, obfuscated spelling) in `README.md`, explaining how sequence models and subword tokenizers in Phase 4 address them.
Expected Outcome: Clear engineering write-up of model capabilities, threshold tradeoffs, and limitations.
Acceptance Check: README includes metrics table, top predictive tokens, and limitation analysis.
Reference Architecture & Verification Code

Starter & Verification Code Examples

Log-Space Multinomial Naive Bayes with Laplace Smoothing

Shows how to estimate class priors and Laplace-smoothed token log-likelihoods in vectorized NumPy without floating-point underflow.

import numpy as np

class MultinomialNBScratch:
    def __init__(self, alpha: float = 1.0) -> None:
        self.alpha = alpha
        self.log_priors: np.ndarray | None = None      # Shape: (2,)
        self.log_likelihoods: np.ndarray | None = None # Shape: (2, V)

    def fit(self, X_counts: np.ndarray, y: np.ndarray) -> "MultinomialNBScratch":
        n_samples, vocab_size = X_counts.shape
        self.log_priors = np.zeros(2, dtype=np.float64)
        self.log_likelihoods = np.zeros((2, vocab_size), dtype=np.float64)

        for c in (0, 1):
            X_c = X_counts[y == c]
            self.log_priors[c] = np.log(X_c.shape[0] / n_samples)
            word_counts = np.sum(X_c, axis=0) + self.alpha
            total_words = np.sum(word_counts)
            self.log_likelihoods[c] = np.log(word_counts / total_words)
        return self

    def predict_log_scores(self, X_counts: np.ndarray) -> np.ndarray:
        assert self.log_priors is not None and self.log_likelihoods is not None
        # (N, V) @ (V, 2) + (2,) -> (N, 2)
        return X_counts @ self.log_likelihoods.T + self.log_priors
Verification & Acceptance Suite

How to Test Your Implementation

Test tokenizer normalization, Laplace smoothing on unseen words, and Precision/Recall/F1 formulas on known synthetic confusion matrices.

$ pytest tests/test_spam_classifier.py -vTest Runner

Unseen Vocabulary Token Stability (Laplace Smoothing)

Ensure messages containing words never seen in training do not produce -inf or NaN probabilities.

Steps: Fit classifier on a small toy corpus, then call predict_proba on a message containing 100% novel words.

Pass Criteria: Returns finite probabilities in (0, 1) without ZeroDivisionError or NaN.

Precision, Recall, and F1 Exact Calculation

Verify metric formulas on a known ground-truth vector.

Steps: Pass y_true = [1, 1, 1, 0, 0] and y_pred = [1, 1, 0, 1, 0] (TP=2, FN=1, FP=1, TN=1).

Pass Criteria: Precision = 2/3 (0.6667), Recall = 2/3 (0.6667), F1 = 2/3 (0.6667).

Overall Project Acceptance Criteria

  • Leak-free vocabulary and feature extraction fit strictly on the training split.
  • Classifier implemented from scratch in NumPy without calling scikit-learn estimators.
  • Precision, Recall, F1, and confusion matrix calculated and verified.
  • Misclassification analysis and scikit-learn baseline comparison documented.
Progressive Spoiler-Free Guidance

Expandable Hints & Debugging Tips

Try implementing each stage on your own first. Expand a hint below if you want to verify tensor shapes, mathematical identities, or common edge cases.

Optional Bonus Extensions

Optional Stretch Challenges

Optional · Does Not Block Completion

Bigram Features (Character or Word N-Grams)

Extend your vectorizer to extract word bigrams (`n=2`, e.g., `'not free'`, `'claim now'`) and measure the resulting F1 improvement.

What You Learn: How local n-gram context captures negation and multi-word phrases.

Precision-Recall Curve & Threshold Sweeping

Sweep decision thresholds from `0.05` to `0.95` and plot the Precision-Recall curve to find the threshold that achieves `>= 99%` precision.

What You Learn: How production classifiers tune operating thresholds for asymmetric business costs.

Expected Deliverable

What Your Completed Repository Should Contain

A working text classification system with evaluation results.

Deliverable Artifacts

  • Text tokenizer and leak-free Bag-of-Words / TF-IDF vectorizer
  • From-scratch Naive Bayes or Logistic Regression classifier in NumPy
  • Evaluation report with Confusion Matrix, Precision, Recall, F1, and misclassification analysis

After Completing This Project, You Should Be Able To...

  • Preprocess raw text and construct leak-free numerical feature matrices using Bag-of-Words and TF-IDF.
  • Implement probabilistic classification in log-space with Laplace smoothing or Binary Cross-Entropy optimization.
  • Evaluate classifiers on imbalanced data using Confusion Matrices, Precision, Recall, and F1.
  • Perform qualitative error analysis on false positives and false negatives to identify model limitations.
Portfolio & Repository Tracking

GitHub Repository Submission

Link your public or private GitHub repository URL to keep a structured portfolio of your from-scratch AI builds. Stored locally in your browser (self-reported; no GitHub tokens or remote code execution).

Status: Not Linked

Suggested Repository: spam-classifier-from-scratch

Recommended Directory Layout:

spam_filter/vectorizer.py
spam_filter/classifier.py
spam_filter/metrics.py
tests/test_spam_classifier.py
README.md

Recommended README Documentation Checklist

  • Dataset attribution and class balance summary
  • Precision, Recall, and F1 comparison table (Scratch vs scikit-learn)
  • Error analysis of false positives and false negatives
Local Engineering Notebook

Personal Implementation Notes