spam-xai-model / CHANGELOG.md
VoltageVagabond's picture
Upload folder using huggingface_hub
f396f60 verified
|
Raw
History Blame
21.8 kB

Changelog β€” Spam XAI Project

ENGT 375: Applied Machine Learning for Engineering Technology β€” Spring 2026, ODU

Merged from CHANGELOG.md (Streamlit version) and CHANGELOG_gradio.md (Gradio version) on 2026-04-07. Pre-merge entries are preserved in their original sections below.

Reconstructed retroactively from file timestamps and code comments. This project does not use traditional version control; dates reflect filesystem modification times.


v1.5.3 β€” 2026-04-16 (HuggingFace sync)

Summary

Synced latest README, MODEL_README, and project files to HuggingFace via update-huggingface.command. No code changes.


v1.5.2 β€” 2026-04-14 (Documentation sync with deployed artifacts)

Summary

Audit pass against the actual code base and saved model artifacts. Fixed stale threshold references in README.md and MODEL_README.md that still quoted the original v0.1.0 value of 0.3714. The deployed optimal_threshold.joblib file now holds 0.5176 (written by the v1.5 retrain on 2026-04-07), and the docs have been updated to match.

Changes

  • MODEL_README.md β€” Training Details table: "Optimal threshold" 0.3714 β†’ 0.5176
  • MODEL_README.md β€” Files table: optimal_threshold.joblib description updated to reference 0.5176
  • README.md β€” Performance section: threshold statement updated to 0.5176 with a note that the value is read directly from optimal_threshold.joblib

Rationale

The code base is the source of truth. joblib.load("optimal_threshold.joblib") returned 0.5175934056031538 at audit time, confirming the v1.5 retrain's threshold-optimization output (CHANGELOG line 42). The MODEL_README was never updated after that retrain. No code changes, no retraining required.


v1.5.1 β€” 2026-04-14 (HF Spaces context)

Summary

No code changes to v1 in this session β€” v1's Space (VoltageVagabond/spam-xai-classifier) remained healthy throughout. The session used v1 as the reference baseline while debugging deployment issues for the v2 Space and the liquid Space.

Q&A from this session

Q: Why does v1's Space work without any of the issues v2 had? A: v1 pins scikit-learn==1.6.1 exactly, and its model artifacts were trained against that same version. v2's pin (>=1.3.0 originally) plus a model serialized by sklearn 1.8.0 caused a multi_class AttributeError on load. The lesson: pin sklearn exactly, and make sure the Space's Python version matches the version that produced the model.

Q: Does my model repo spam-xai-model need to be public for v1's Space to work? A: No. v1 bundles all its model files (voting_model.joblib, etc.) directly in the Space repo, so it never has to fetch from another repo at runtime. Only the liquid Space needs an external model repo (because LoRA adapters are too large to bundle).

Changes

  • None (v1 untouched). This entry exists for cross-reference with the v2 changelog.

v1.5 β€” 2026-04-07

Full retrain + project cleanup

  • Full ensemble retrain β€” ran retrain-full.command with no feedback corrections; trained VotingClassifier (RF + LR + SVM) on 190,543 emails; achieved 97% accuracy, 0.97 macro F1, optimal threshold 0.5176
  • Deleted project cruft β€” removed macOS resource forks (._*), __pycache__/, .DS_Store, stale .server.log
  • Deleted legacy app files β€” removed app_legacy.py and app_student_legacy.py (old Streamlit-era versions, superseded by app.py)
  • Deleted legacy notebooks β€” removed notebooks/_legacy/ and notebooks/.ipynb_checkpoints/
  • Deleted legacy docs β€” removed docs/_legacy_plans/ (pre-merge design notes from March)
  • Deleted old generic model backup β€” removed models_backup/; kept models_backup_pre_fast/ and models_backup_pre_full/ as rollback snapshots for the two most recent retrains

v1.4 β€” 2026-04-07

Bug fixes and UI improvements

  • Fixed PyArrow indexing crash in retrain.py β€” HuggingFace datasets return PyArrow-backed columns; .values returned a PyArrow array that train_test_split couldn't index. Changed to .to_numpy() at all three call sites (lines 159, 160, 183, 184)
  • Fixed fast-mode model type mismatch in app.py β€” fast retrain saves a bare RandomForestClassifier but app.py was unconditionally calling .named_estimators_['rf'] (a VotingClassifier attribute). Added hasattr check so both fast (RF) and full (VotingClassifier) models load correctly
  • Fixed ELI5 rendering in Gradio β€” eli5.format_as_html() wraps methodology boilerplate in a <pre> block that overflows the container. Now strips <pre> blocks and wraps output in a overflow-wrap: break-word div so only the contribution table shows
  • Widened app layout β€” increased .gradio-container max-width from 1180px to 1600px to reduce cramped appearance on wide monitors

v1.3 β€” 2026-04-07

Parallelized preprocessing; updated time estimates

  • Parallelized preprocess_text in retrain.py β€” replaced single-threaded .apply() with joblib.Parallel(n_jobs=-1, prefer='threads') to use all CPU cores during text cleaning/stemming
  • Updated time estimates to ~5-10 minutes (fast mode) in retrain.py header, runtime print statement, and retrain-fast.command banner; previous estimate of ~2-5 minutes was inaccurate for the 190K-email corpus

v1.2 β€” 2026-04-07

Training data overhaul β€” replaced Kaggle with research-grade corpora

  • Dropped Kaggle spam_Emails_data.csv β€” dataset was noisy/synthetic-heavy and capped at 100K rows; replaced with three research-grade sources
  • Added Enron corpus (~33K real corporate emails from data/raw/enron/enron_spam_data.csv) β€” file already existed but was not being loaded by the retrain scripts
  • Added puyang2025/seven-phishing-email-datasets (HuggingFace) β€” 138K emails from 7 research corpora (TREC-05, TREC-06, TREC-07, CEAS-08, SpamAssassin, Ling-Spam; Enron sub-corpus excluded to avoid duplication). Saved to data/raw/puyang2025/seven_phishing_emails.parquet
  • Added zefang-liu/phishing-email-dataset (HuggingFace) β€” 18,634 phishing-labeled emails. Saved to data/raw/zefang/phishing_emails.parquet
  • New total training data: ~190,543 emails (was ~100K Kaggle sample)
  • Updated retrain.py and retrain_student.py to load all three new sources; removed dead email-dataset-main directory-traversal code
  • Updated build_datasets.py and build_liquid_datasets.py to include puyang2025 and zefang as additional sources for MLX/Liquid LLM fine-tuning data
  • Updated HOW_TO_RUN.html to reflect new data sources and file paths

v1.1 β€” 2026-04-07

Merged spam-classifier-gradio into spam-xai-project

  • File merge: Moved all of spam-classifier-gradio/ into spam-xai-project/. The two projects shared the same data, preprocessing, and feature engineering β€” keeping them separate was creating drift and duplicate code.
  • The Gradio app (app.py), VotingClassifier ensemble training (train_ensemble.py), retrain script with feedback support (retrain.py), and the Gradio notebook (notebooks/spam_classifier_gradio.ipynb) all live in the merged project now.
  • Preserved as legacy (renamed but kept):
    • app_legacy.py, app_student_legacy.py β€” older Streamlit/Gradio app versions
    • MODEL_README_legacy.md, README_legacy.md β€” older HF cards
    • retrain_legacy.py β€” older RF-only retrain
    • notebooks/_legacy/spam_classifier_xai.ipynb, _legacy/spam_classifier_xai_executed.ipynb
    • models/_baseline_rf/ β€” older RF-only model artifacts
  • Reference docs added: Built out docs/references/ with 20 PDFs (LIME, SHAP, TreeSHAP, Anchors, Pedregosa sklearn, Kuzlu et al. solar XAI, 5 spam-detection survey papers) and 11 HTML guides (LIME/SHAP/ELI5 docs, sklearn user guide pages, Gradio quickstart, HF Spaces docs, Molnar Interpretable ML book). All linked from docs/references/how-to.html. Shared papers (Attention, LoRA, QLoRA, PEFT survey) live in the top-level references/ folder used by all three sibling projects.
  • Notebook style cleanup: The student notebook (spam_classifier_xai_student.ipynb) and the Gradio notebook (spam_classifier_gradio.ipynb) were rewritten in places to remove "senior engineer" code patterns: replaced lambdas with named helpers, broke chained list-comp+sort+slice into explicit loops, and softened comments to read more like an undergraduate beginner wrote them. Functionality unchanged. Backups saved as *.ipynb.bak.
  • HF deployment: Updated update-huggingface.command and upload_to_hf.py to push the merged project to TWO new HF repos:
    • VoltageVagabond/spam-xai-classifier (Space, Gradio SDK)
    • VoltageVagabond/spam-xai-model (Model)
    • The old spam-classifier-gradio Space and spam-classifier-gradio-model Model are archived (not deleted).

Cleanup pass β€” same day

  • Merged two CHANGELOG.md files into this one
  • Merged MODEL_README.md + MODEL_README_legacy.md into one model card
  • Merged README.md body content (kept HF Space YAML frontmatter intact)
  • Consolidated retrain scripts: one canonical retrain.py with --mode {fast,full}, plus retrain-fast.command and retrain-full.command matching the Liquid project's pattern
  • Wired all three launcher commands to existing target files (launch-app.command, launch-gradio.command, launch-notebook.command)
  • Deleted three Windows .bat files (retrain.bat, run_app.bat, run_app_student.bat) β€” macOS-only project now

Pre-merge β€” Gradio version (originally CHANGELOG_gradio.md)

v0.2.1 β€” 2026-03-28

Retrain with Auto-Backup

  • retrain.command now automatically backs up models/ to models_backup/ before retraining

v0.2.0 β€” 2026-03-28

HuggingFace Upload + Retrain Command

  • Uploaded project to HuggingFace Space: VoltageVagabond/spam-classifier-gradio (Gradio SDK)
  • Created README.md with HF Space YAML frontmatter
  • Uploaded model .joblib files to HF Space for live demo
  • Added retrain.command for the sklearn ensemble
  • Uploaded all training data to HF dataset: VoltageVagabond/spam-email-dataset

v0.1.3 β€” 2026-03-23

UI Fix: Scroll-to-Top Bug

  • Fixed page jumping to top when pasting email or clicking Classify
  • Added autoscroll=False, cache_examples=False, scroll_to_output=False

v0.1.2 β€” 2026-03-23

Bug Fixes + Real-World Testing

  • Fixed SHAP crash: shap_values returned multi-dimensional arrays from KernelExplainer; now .flatten() to 1D before sorting
  • Added try/except around SHAP so Result and LIME tabs still show if SHAP fails
  • Pinned gradio==4.19.2 (Gradio 4.44.1 has a Python 3.9 bug)
  • Added launch-notebook.command for Jupyter notebook

Known Limitation: Legitimate Marketing Emails Misclassified

  • Tested with a real Lenovo Rewards referral email β€” classified as SPAM at 78% confidence (false positive)
  • Root cause: dollar signs, "earn", "rewards", "purchase" vocabulary overlaps heavily with spam training patterns
  • LIME and SHAP correctly showed which features drove the misclassification β€” demonstrating the value of XAI for understanding model failures
  • This is a known weakness of bag-of-words/TF-IDF classifiers

v0.1.1 β€” 2026-03-23

Bug Fix: SHAP numpy indexing

  • Fixed TypeError: only integer scalar arrays can be converted to a scalar index in SHAP plot generation
  • np.argsort returns numpy int64 indices; Python list indexing needs plain int

v0.1.0 β€” 2026-03-23

Initial Build

  • Created fresh Gradio project (replacing old Streamlit version)
  • Ported preprocessing and 24 metadata features from old utils_student.py
  • Loaded Kaggle spam dataset (~190K emails, capped at 100K stratified sample)
  • Trained and compared 3 models:
    • Random Forest: 97.75% accuracy, F1=0.976
    • Logistic Regression: 96.57% accuracy, F1=0.964
    • SVM (LinearSVC + calibration): 96.89% accuracy, F1=0.967
  • Combined into VotingClassifier (soft voting): 97.40% accuracy, F1=0.973
  • Optimal threshold: 0.3714 (targeting 99% ham precision)
  • Built Gradio interface with text + .txt file upload, Result tab, LIME tab, SHAP tab
  • 4 built-in example emails

Pre-merge β€” Streamlit version (originally CHANGELOG.md)

v0.3.1 β€” 2026-03-28

Retrain with Auto-Backup (same fix as gradio v0.2.1)

v0.3 β€” 2026-03-28

HuggingFace Upload + Command Launchers

  • Uploaded to HuggingFace Space: VoltageVagabond/spam-xai-project (Docker + Streamlit)
  • Created Dockerfile for HF Space deployment
  • Added retrain.command and launch-app.command

v1.0 β€” 2026-03-23

Documentation

  • Added docs/ directory with project documentation
  • Created the original CHANGELOG.md

v0.9 β€” 2026-03-09

Interactive feature explorer

  • Generated spam-feature-explorer.html β€” standalone HTML/JS page for interactively exploring the 24 metadata features

v0.8 β€” 2026-03-08

Student version and LLM feature extraction

  • Created app_student.py β€” full rewrite of app.py with extensive inline comments
  • Imports all shared logic from utils_student.py
  • Added streamlit_js_eval for persisting classification results in browser localStorage
  • Added user feedback system: save_feedback() writes corrections to data/feedback/feedback_log.csv
  • Created retrain_student.py β€” student version of training script with explanatory comments, optional LLM feature extraction via Ollama, SHA-256 hash-based caching of LLM features
  • Defined 6 LLM intent/tone features: intent_promotional, intent_transactional, intent_personal, intent_phishing, tone_urgency, tone_formality
  • SKIP_LLM_TRAINING flag defaults to True for fast retraining
  • Created test_accuracy.py for quick model smoke-testing
  • Created student notebook (spam_classifier_xai_student.ipynb)

v0.7 β€” 2026-03-06

New training datasets and MinMaxScaler

  • Switched primary training data from SpamAssassin + Enron to Kaggle 190K + GitHub email-dataset
  • Added MinMaxScaler for metadata features
  • Added user feedback integration: corrections weighted 5x and injected into training
  • Reduced GridSearchCV grid from 36 to 4 combinations and CV from 5 to 3 folds

v0.6 β€” 2026-03-06

Expanded feature engineering (11 to 24 metadata features)

  • Created utils_student.py β€” shared utilities module
  • Added 13 new metadata features (features 12-24): has_specific_date, has_specific_time, date_reference_count, has_unsubscribe, has_physical_address, has_proper_greeting, has_contact_info, registration_language_score, cta_to_info_ratio, shortener_url_ratio, legitimate_platform_count, gov_edu_url_count, question_mark_count
  • Expanded generate_newsletters_student.py from 50 to 600 target newsletters

v0.5 β€” 2026-03-05

OCR support, dark mode, and header feature extraction

  • Optional pytesseract + Pillow for classifying email screenshots
  • extract_header_features() parses From/To/Subject headers to detect government domains, mailing-list patterns, etc.
  • Full dark mode with theme-aware matplotlib + ELI5 HTML overlay
  • Created run_app_student.bat, _restyle_notebook.py

v0.4 β€” 2026-03-04

Newsletter augmentation and generation

  • Created generate_newsletters.py producing 50 synthetic government/institutional newsletters via Ollama/Qwen
  • Change 9 in retrain.py: sentence-shuffled newsletter variants (5 per ham email) to improve classification of legitimate promotional-style emails
  • Created initial Jupyter notebook (spam_classifier_xai.ipynb)

v0.3 β€” 2026-03-04

Ollama/Qwen LLM integration

  • Change 3: get_llm_second_opinion() calling Ollama with Qwen 3.5 to independently classify emails
  • RF and LLM confidence scores blended with context-aware weighting
  • Added AI Explanation tab using Qwen 3.5 to produce plain-English explanations from top-5 LIME/SHAP/ELI5 features
  • Change 1: Default threshold raised to 0.60

v0.2 β€” 2026-03-04

Context-aware phrase lists and domain whitelist

  • Added SPAM_CONTEXT_PHRASES (14 phrases: "act now", "limited time", etc.) and HAM_CONTEXT_PHRASES (12 phrases: "click to unsubscribe", "official notice", etc.)
  • Added check_domain_trust() extracting sender domain from From: / Return-Path: headers

v0.1 β€” 2026-03-04

Initial Streamlit app and training pipeline

  • Created requirements.txt with scikit-learn, LIME, SHAP, ELI5, Streamlit, NLTK, matplotlib, scipy
  • Built retrain.py loading SpamAssassin + Enron-Spam, 3000 TF-IDF features + 11 metadata features
  • GridSearchCV over a Random Forest, class_weight='balanced', isotonic probability calibration, optimal threshold targeting 99% ham precision
  • Created app.py β€” Streamlit UI with text-area input, example emails, confidence gauge, four explanation tabs (LIME, SHAP, ELI5, Comparison)
  • Added run_app.bat and HOW_TO_RUN.html

Training Methodology Reference (for paper)

Complete Training Pipeline (current β€” post-merge)

  1. Data sources:

    • Kaggle spam dataset (data/spam_Emails_data.csv, ~193K emails, stratified-sampled to 100K)
    • GitHub email-dataset (folder 1 = ham, folder 2 = spam)
    • Optional: SpamAssassin + Enron + synthetic newsletters (legacy data sources)
  2. Preprocessing: HTML removal, URL removal, email address removal, non-alphabetic removal, lowercasing, NLTK stopword removal, Porter stemming

  3. Feature engineering:

    • TF-IDF: 3,000 features, ngram_range=(1,3), min_df=2, max_df=0.90, sublinear_tf=True
    • 24 hand-crafted metadata features: exclamation_density, dollar_sign_count, caps_word_ratio, spam_phrase_count, ham_phrase_count, net_spam_context, url_count, html_tag_count, email_length, avg_sentence_length, capitalization_ratio, has_specific_date, has_specific_time, date_reference_count, has_unsubscribe, has_physical_address, has_proper_greeting, has_contact_info, registration_language_score, cta_to_info_ratio, shortener_url_ratio, legitimate_platform_count, gov_edu_url_count, question_mark_count
    • MinMaxScaler on metadata features (so they match the 0-1 TF-IDF scale)
    • Combined via scipy.sparse.hstack β†’ 3,024 total features
  4. Train/test split: 70/30, stratified, random_state=42

  5. Models:

    • Random Forest: n_estimators=200, class_weight='balanced' β†’ 97.75% accuracy, F1=0.976
    • Logistic Regression: max_iter=1000, class_weight='balanced' β†’ 96.57% accuracy, F1=0.964
    • SVM (LinearSVC + CalibratedClassifierCV): class_weight='balanced' β†’ 96.89% accuracy, F1=0.967
    • VotingClassifier (soft voting, all 3): 97.40% accuracy, F1=0.973 ← deployed model
  6. Threshold optimization: Precision-recall curve targeting 99% ham precision β†’ optimal threshold 0.3714

  7. Explainability:

    • LIME: LimeTabularExplainer, 200-row training sample, 10 features per explanation
    • SHAP: KernelExplainer on 24 metadata features only (TreeExplainer too slow on 3,000+ features), 50-row background
    • ELI5: PermutationImportance on the full 3,024-feature space
  8. Framework: scikit-learn 1.6.1

  9. Hardware: MacBook Pro M4 Pro, 24 GB unified RAM

Key Hyperparameters

Parameter Value Rationale
TF-IDF max_features 3,000 Balance between vocabulary coverage and dimensionality
TF-IDF ngram_range (1, 3) Capture phrases like "act now", "limited time offer"
TF-IDF min_df 2 Remove extremely rare words
TF-IDF max_df 0.90 Remove words appearing in 90%+ of emails
TF-IDF sublinear_tf True Logarithmic term frequency scaling
RF n_estimators 200 Sufficient trees for stable predictions
class_weight 'balanced' Handle spam/ham class imbalance
test_size 0.3 70/30 split
random_state 42 Reproducibility

Evolution of the Project

Version Features Model Data UI
v0.1 (Streamlit) 11 metadata + 3000 TF-IDF RF + GridSearchCV + Calibration SpamAssassin + Enron Streamlit
v0.6 24 metadata + 3000 TF-IDF Same + Kaggle + GitHub Streamlit
v0.8 24 metadata + 3000 TF-IDF + optional LLM Same + user feedback (5x weighted) Streamlit + student version
v0.1.0 (Gradio) 24 metadata + 3000 TF-IDF RF + LR + SVM VotingClassifier Kaggle 100K Gradio (rewrite from Streamlit)
v1.1 (merged) 24 metadata + 3000 TF-IDF VotingClassifier (canonical) Kaggle + GitHub + optional Spam Assassin Gradio (canonical)

Known Limitations

  • Legitimate marketing emails (e.g., Lenovo Rewards) misclassified as spam at high confidence due to vocabulary overlap with spam patterns (dollar signs, "earn", "rewards")
  • TF-IDF is bag-of-words β€” cannot understand email intent or context
  • SHAP limited to metadata features only (KernelExplainer too slow on 3,000+ features)
  • The voting model is large (152 MB) β€” pushing to a free HF Space requires explicit inclusion in the upload script

Citations

  • Pedregosa, F., et al. (2011). "Scikit-learn: Machine Learning in Python." JMLR 12, 2825-2830
  • Ribeiro, M.T., et al. (2016). "'Why Should I Trust You?': Explaining the Predictions of Any Classifier." KDD 2016 (LIME)
  • Lundberg, S.M. & Lee, S.I. (2017). "A Unified Approach to Interpreting Model Predictions." NeurIPS 2017 (SHAP)
  • Lundberg, S.M., et al. (2020). "From local explanations to global understanding with explainable AI for trees." Nature MI 2(1) (TreeSHAP)
  • Kuzlu, M., et al. (2020). "Gaining Insight Into Solar PV Power Generation Forecasting Utilizing Explainable AI Tools." IEEE Access 8 (XAI feature reduction methodology)
  • Breiman, L. (2001). "Random Forests." Machine Learning 45(1)