> For the complete documentation index, see [llms.txt](https://aashraymt.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://aashraymt.gitbook.io/docs/sms-spam-detector/sms-spam-detector.md).

# sms-spam-detector

## Introduction: Building an AI-Driven AppSec Pipeline

Welcome to the documentation for my SMS Spam Classification project. As SMS phishing (smishing) increasingly bypasses traditional blocklists, modern security requires intelligent, automated filters.

My goal for this project was to bridge the gap between data science and AppSec engineering. Instead of just writing a basic machine learning script, I set out to build a complete, production-ready pipeline. This guide details my exact process: cleaning raw text, training a probabilistic Naive Bayes model, and serializing the final artifact for remote deployment to a HackTheBox server.

🔗 View the Source Code: The complete Python architecture and deployment scripts are available on my GitHub: <https://github.com/aashraymt/sms-spam-detector>

{% embed url="<https://github.com/aashraymt/sms-spam-detector>" %}

### Phase 1: Theoretical Foundation - AI Spam Classification

#### Objective

To build a probabilistic model that classifies SMS messages as Spam (malicious/unsolicited) or Ham (legitimate) using the Naive Bayes algorithm.

#### 1. The Core Theory: Bayes' Theorem

At its heart, spam detection is about calculating Conditional Probability. We want to know: *"What is the probability this message is spam, given that it contains these specific words?"*

The Formula:

$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}$$

In the context of our project:

* $$ $P(\text{Spam}|\text{Words})$ $$: The Posterior (What we want to find).
* $$ $P(\text{Words}|\text{Spam})$ $$: The Likelihood (How often these words appear in known spam).
* $$ $P(\text{Spam})$ $$: The Prior (General probability of any message being spam).
* $$ $P(\text{Words})$ $$: The Evidence (Probability of these words appearing in any message).

#### 2. Why "Naive" Bayes?

The algorithm is called "Naive" because it makes a massive assumption: Independence.

It assumes that every word in a message is completely independent of the others. For example, in the phrase "Win Cash," the algorithm assumes the word "Win" has nothing to do with the word "Cash."

While logically incorrect in human language, this assumption makes the math incredibly fast and efficient for text classification. Instead of calculating the probability of a whole sentence, we multiply the individual probabilities of each word:

$$P(\text{Words}|\text{Spam}) = P(\text{word}\_1|\text{Spam}) \cdot P(\text{word}\_2|\text{Spam}) \cdot \dots \cdot P(\text{word}\_n|\text{Spam})$$

#### 3. Step-by-Step Example Walkthrough

Imagine we are testing a new message: "CASH PRIZE"

A. Given Data (From our Training Set)

| **Category** | **Prior Probability** | **P("CASH")** | **P("PRIZE")** |
| ------------ | --------------------- | ------------- | -------------- |
| Spam         | 0.3                   | 0.8           | 0.7            |
| Ham          | 0.7                   | 0.1           | 0.05           |

B. Calculate the Likelihoods

* For Spam: $$ $0.8 \times 0.7 = \mathbf{0.56}$ $$
* For Ham: $$ $0.1 \times 0.05 = \mathbf{0.005}$ $$

C. Apply the Prior

* Spam Score: $$ $0.56 \times 0.3 = \mathbf{0.168}$ $$
* Ham Score: $$ $0.005 \times 0.7 = \mathbf{0.0035}$ $$

D. Conclusion

Since $$ $0.168 > 0.0035$ $$, the model classifies the message as Spam.

#### 4. Implementation Challenges (Notebook Checklist)

When moving from these notes to code, keep these two concepts in mind:

* Laplace Smoothing: If a word (e.g., "Meeting") never appeared in your training spam, the probability becomes 0. Multiplying by 0 ruins the whole equation. We add $$ $+1$ $$ to every word count to prevent this.
* Bag of Words (BoW): Computers don't read words; they count them. You must transform your text into a frequency matrix (Feature Extraction).

***

### Phase 2: Data Engineering & Pipeline

#### 1. Overview of the SMS Spam Collection Dataset

For this project, we utilize the SMS Spam Collection, a public dataset specifically designed for Bayesian filtering. Unlike email-based datasets, this corpus addresses the unique linguistic challenges of mobile messaging (short lengths, slang, and abbreviations).

* Format: 5,574 annotated messages (Ham vs. Spam).
* Ham: Legitimate, safe messages from known or expected sources.
* Spam: Unsolicited, intrusive, or potentially malicious content (phishing).

#### 2. Automated Data Acquisition

To ensure project reproducibility, the dataset is downloaded programmatically. We use the `requests` library to fetch the archive and `zipfile` to handle extraction in-memory.

Python

```
import requests
import zipfile
import io
import os
import pandas as pd

# 1. Download the Dataset
url = "https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip"
response = requests.get(url)

if response.status_code == 200:
    print("Download successful.")
    
    # 2. Extract contents using io.BytesIO to process binary data
    with zipfile.ZipFile(io.BytesIO(response.content)) as z:
        z.extractall("sms_spam_collection")
        print("Extraction successful.")
        
    # 3. Verify directory contents
    print("Files in directory:", os.listdir("sms_spam_collection"))
else:
    print("❌ Failed to download the dataset.")
```

#### 3. Data Ingestion & Anatomy

The raw data is stored as a Tab-Separated Values (TSV) file. We load this into a pandas DataFrame. Because the file lacks a header row, we explicitly define the schema.

Python

```
# Load into DataFrame
df = pd.read_csv(
    "sms_spam_collection/SMSSpamCollection",
    sep="\t",
    header=None,
    names=["label", "message"]
)

# Initial Data Preview
print("-----HEAD-----")
print(df.head())
```

#### 4. Data Quality Audit & Cleaning

Effective AI models require clean input. We perform three critical audits: Schema verification, Null-value detection, and De-duplication. Missing values break mathematical calculations, and duplicates introduce bias.

Python

```
# Statistical & Structural Overview
print("-----Describe-----")
print(df.describe())
print("-----Info-----")
print(df.info())

# Verify no missing data
print("Missing values:\n", df.isnull().sum())

# Identify and remove duplicate messages
duplicate_count = df.duplicated().sum()
print(f"Duplicate entries: {duplicate_count}")

if duplicate_count > 0:
    df = df.drop_duplicates()
    print("Duplicates removed.")
```

### Phase 3: Text Preprocessing & Normalization

The raw SMS data contains significant noise, such as irregular casing, punctuation, and stop words, which can dilute the probabilistic strength of the Naive Bayes model. We use the NLTK (Natural Language Toolkit) library to standardize our corpus.

#### 1. Environment Setup

We must first fetch the specific data packages required for tokenization and linguistics.

Python

```
import nltk

# Download required NLTK resources
nltk.download("punkt")
nltk.download("punkt_tab")
nltk.download("stopwords")
```

#### 2. Text Standardization Pipeline

A. Case Normalization

To prevent the model from treating "FREE" and "free" as distinct features, we convert all strings to lowercase. This reduces the feature space and ensures mathematical consistency.

Python

```
df["message"] = df["message"].str.lower()
```

B. Regex Cleaning (Strategic Filtering)

In spam detection, total punctuation removal is often counterproductive. While we remove numbers and general symbols, we use Regular Expressions to preserve `$` and `!` because they are frequently associated with financial scams and urgent calls to action.

Python

```
import re

# Keep only letters, whitespace, $, and !
df["message"] = df["message"].apply(lambda x: re.sub(r"[^a-z\s$!]", "", x))
```

C. Tokenization

We transform unstructured sentences into discrete lists of strings (tokens). This allows the algorithm to analyze the frequency of individual words.

Python

```
from nltk.tokenize import word_tokenize

df["message"] = df["message"].apply(word_tokenize)
```

#### 3. Linguistic Optimization

A. Stop Word Removal

High-frequency words like "the", "is", and "at" appear in almost every message regardless of intent. Removing them allows the Naive Bayes classifier to focus on high-signal words like "win", "claim", or "urgent".

Python

```
from nltk.corpus import stopwords

stop_words = set(stopwords.words("english"))
df["message"] = df["message"].apply(lambda x: [word for word in x if word not in stop_words])
```

B. Porter Stemming

Stemming reduces words to their root form. For example, "winning", "wins", and "won" all become "win". This groups related words together and further simplifies the vocabulary.

Python

```
from nltk.stem import PorterStemmer

stemmer = PorterStemmer()
df["message"] = df["message"].apply(lambda x: [stemmer.stem(word) for word in x])
```

#### 4. Final Reconstruction

Most vectorization tools, including Scikit-Learn’s `CountVectorizer`, expect a single string rather than a list of tokens. We rejoin our processed tokens with spaces to prepare for the Feature Extraction phase.

Python

```
df["message"] = df["message"].apply(lambda x: " ".join(x))
```

#### Checkpoint: Data Transformation Log

*A visualization of the data transforming from raw text to engineered features.*

Plaintext

```
==Before any preprocessing==
0    Go until jurong point, crazy.. Available only ...
1                        Ok lar... Joking wif u oni...
2    Free entry in 2 a wkly comp to win FA Cup fina...

==After lower casting==
0    go until jurong point, crazy.. available only ...
1                        ok lar... joking wif u oni...

=== After removing punctuation and numbers===
0    go until jurong point crazy available only in ...
1                              ok lar joking wif u oni

===After tokenization===
0    [go, until, jurong, point, crazy, available, o...
1                       [ok, lar, joking, wif, u, oni]

===After removing stop words===
0    [go, jurong, point, crazy, available, bugis, n...
1                       [ok, lar, joking, wif, u, oni]

===After stemming===
0    [go, jurong, point, crazi, avail, bugi, n, gre...
1                         [ok, lar, joke, wif, u, oni]

===After joining tokens back into strings===
0    go jurong point crazi avail bugi n great world...
1                                ok lar joke wif u oni
```

***

### Phase 4: Feature Extraction (Vectorization)

While humans read words, machine learning models require numerical input. Feature Extraction is the process of converting our cleaned text strings into a mathematical matrix that a classifier can interpret.

#### 1. The Bag-of-Words (BoW) Model

We utilize the Bag-of-Words approach, which ignores word order and focuses purely on word frequency. However, to provide the model with a sense of local context, we utilize N-grams:

* Unigrams (1-gram): Single words (e.g., "prize").
* Bigrams (2-gram): Pairs of consecutive words (e.g., "free prize").

Including bigrams allows the model to distinguish between a casual use of the word "free" and the high-risk combination of "free prize."

#### 2. Implementing CountVectorizer

We use Scikit-Learn’s `CountVectorizer` to automate the transformation. We apply specific constraints to ensure the feature set is relevant and not bloated with noise.

Technical Parameters:

* `ngram_range=(1, 2)`: Captures both individual words and word pairs.
* `min_df=1`: Terms must appear at least once to be included.
* `max_df=0.9`: Terms appearing in more than 90% of messages are discarded, as they are too common to help differentiate.

Python

```
from sklearn.feature_extraction.text import CountVectorizer

# 1. Initialize the Vectorizer
vectorizer = CountVectorizer(min_df=1, max_df=0.9, ngram_range=(1, 2))

# 2. Transform the cleaned text into a sparse matrix (X)
X = vectorizer.fit_transform(df["message"])

# 3. Encode the labels (y)
# spam = 1, ham = 0
y = df["label"].apply(lambda x: 1 if x == "spam" else 0)
```

#### 3. The Vectorization Process

The `CountVectorizer` operates in three distinct stages:

1. Tokenization: Splitting text into unigrams and bigrams based on the `ngram_range`.
2. Vocabulary Building: Filtering tokens based on `min_df` and `max_df` to create a master dictionary of features.
3. Encoding: Mapping every message to a numerical vector. Each column in the vector corresponds to a specific word or pair from the master vocabulary.

#### 4. Label Encoding

Because our target variable (label) is currently text ("ham" or "spam"), we convert it into a binary format:

* 1 (Spam): The positive class we are trying to detect.
* 0 (Ham): The negative (legitimate) class.

Current State:

* X: A high-dimensional numerical matrix representing word and phrase frequencies.
* y: A binary vector representing the ground truth (labels).

***

### Phase 5: The ML Pipeline, Tuning & Deployment

Building a single model is useful for learning, but building an automated, optimized, and exportable system is what happens in production. This phase transitions the code from a script into a software artifact.

#### 1. The Scikit-Learn Pipeline

In machine learning, data leakage is a major risk where the model accidentally learns from the test data during preprocessing. To prevent this and keep the codebase clean, we use a Pipeline. A pipeline chains the Transformer (`CountVectorizer`) and the Estimator (`MultinomialNB`) into a single object.

Python

```
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB

# The Pipeline ensures data always flows through Vectorization BEFORE Classification
pipeline = Pipeline([
    ("vectorizer", vectorizer), 
    ("classifier", MultinomialNB())
])
```

Why this matters: When you feed a new SMS message into the pipeline later, you do not have to manually call the vectorizer first. The pipeline handles the sequence automatically.

#### 2. Hyperparameter Tuning (GridSearchCV)

We want the best possible model, not just the default one. The Naive Bayes algorithm has a key hyperparameter called Alpha ($$ $\alpha$ $$), which controls the Laplace Smoothing. It dictates how the model handles words it has never seen before.

We use `GridSearchCV` to automatically train multiple versions of our pipeline, each with a different $$ $\alpha$ $$ value, and mathematically determine which one performs best.

Python

```
from sklearn.model_selection import GridSearchCV

# Define the variations of Alpha we want to test
param_grid = {
    "classifier__alpha": [0.01, 0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1.0]
}

# 5-Fold Cross Validation ensures rigorous testing across data slices
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring="f1")

# Execute the search
grid_search.fit(df["message"], y)

# Extract the winner
best_model = grid_search.best_estimator_
print("Optimal Alpha found:", grid_search.best_params_)
```

Note on the F1-Score: In spam detection, we prioritize the F1-Score (a balance of Precision and Recall) over raw Accuracy. We want to catch spam (Recall) without accidentally flagging legitimate messages (Precision).

#### 3. Real-World Evaluation

To prove the model works, we test it against raw, unseen messages simulating real-world traffic.

Python

```
# The pipeline automatically vectorizes the preprocessed strings and makes a prediction
predictions = best_model.predict(processed_messages)
probabilities = best_model.predict_proba(processed_messages)
```

The `predict_proba` function is highly valuable in security operations. It provides a confidence score instead of a binary output. In a real SIEM environment, you might automatically block 99% scores but route 70% scores to a human analyst for review.

#### 4. Serialization (Deployment with Joblib)

Training a model takes computational power. Once optimized, the `best_model` must be saved so it can be deployed into an application without needing to be retrained every time the server restarts. We use `joblib` to serialize the pipeline into a binary file.

Saving the Model (Exporting)

Python

```
import joblib

# Export the trained pipeline to a file
model_filename = 'spam_detection_model.joblib'
joblib.dump(best_model, model_filename)
```

Loading the Model (Production Use)

In a separate Python script or backend environment, you can instantly load the artifact and start predicting.

Python

```
# Import the model back into memory
production_model = joblib.load('spam_detection_model.joblib')

# Instantly classify new, preprocessed data
result = production_model.predict(new_data)
```
