diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..08fe72c2f1d234849f60ad174babd0ac762ae019 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +presentation/reports/eval_finetune_curves.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/eval_finetune_curves_test3.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-12L.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-ce.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-focal.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-scratch.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-synthetic.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/finetune_curves_ft-weighted.png filter=lfs diff=lfs merge=lfs -text +presentation/reports/pretrain_curves.png filter=lfs diff=lfs merge=lfs -text diff --git a/presentation/README.md b/presentation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..86f4e9b597728da749e5038a21df6a8db97c5eb8 --- /dev/null +++ b/presentation/README.md @@ -0,0 +1,661 @@ +# BITS Phishing Detector + +Final project for the Skillspire AI & Machine Learning course (Nathan King, Build IT Systems LLC), presented +late Oct / early Nov 2026. This README is the hand-off document for the instructor and for future BITS work. +The running record of every decision, number and problem is `RUN_LOG.md` (entries 0-8 as of this +writing); this file summarises it and explains how to reproduce it. Status as of 2026-10-08 14:00: data, +tokenizer, smoke test and all `src/`, `scripts/` and `app/` code are done; MLM pretraining is running; the +presentation notebook is in progress; fine-tuning, evaluation, export and deployment have not run yet. + +--- + +## 1. What this is + +| | | +|---|---| +| Task | **Classifier** (not a regressor). Input: one email as `subject + "\n\n" + body` (plain text; sender, headers and attachments are never read). Output: probabilities for three classes, **Legitimate / Phishing / Spam**, plus the roll-up **Unwanted (phishing or spam)** = P(Phishing) + P(Spam). | +| Use | Defensive triage: flag unwanted mail for a human to review. Not a filter that deletes mail, not a tool for writing lures. | +| Model | One BERT-style pre-LayerNorm encoder written from scratch in plain PyTorch (`src/model.py`, no `transformers`, no pretrained weights): 24 layers, d_model 1024, 16 heads, FFN 4096, vocab 32,768 (own byte-level BPE), 512 tokens. **337,476,608 parameters** with the tied MLM head, **337,443,842** as the classifier. | +| Training | Phase 1: masked-language-model pretraining on deduplicated Enron email. Phase 2: fine-tuning with a 3-way head. Rows whose corpus never separated spam from phishing (TREC-05/07, CEAS-08 positives) train on P(phishing)+P(spam) with a partial-label loss. | +| Core challenge | Public corpora label "unwanted" mail, not "phishing vs spam"; they are 1998-2008 heavy, overlap each other and the pretraining corpus, and legitimate mail is about twice as long as spam. The work is mostly in honest data: deduplication, leakage checks and a test set nothing upstream has seen. | +| Scope | **One strong model.** The experiments vary the instructor's dials (layers, epochs, loss, frozen layers, synthetic data, scratch vs pretrained init) on this one architecture; there is no architecture comparison. | + +## 2. Results + +### 2.1 Headline metrics (deployed run `ft-focal`, evaluated 2026-10-09 10:46) + +Model selection used the **validation** set only: `ft-focal` had the best validation F1 of the unwanted +roll-up (0.9951) and the best validation three-way macro-F1 (0.9869) of the six runs in section 9. The two +holdouts below were then evaluated once per run for reporting; `ft-focal` also leads on both of them. + +| TREC-06 holdout (16,302 emails, 23.9% unwanted; whole corpus unseen), threshold 0.5 | value | +|---|---| +| Recall, unwanted (reported first: a missed phish is the costly error) | **0.9741** (101 of 3,894 missed) | +| Precision, unwanted | 0.8794 (520 false alarms on 12,408 legitimate; FPR 4.2%) | +| F1, unwanted | **0.9243** (best achievable at any threshold: 0.9397 at p >= 0.68) | +| Accuracy | 0.9619 | +| ROC-AUC / PR-AUC | **0.9946** / 0.9848 | +| Recall at 1% FPR / at 0.1% FPR | 0.8955 / 0.6497 | +| Credential-lure slice (398 rows matching 37 lure phrases) recall / precision | 0.9959 / 0.9758 | + +TREC-06 is the hard, honest number: a different corpus (2006 TREC spam track) with its own labelling +conventions. `reports/error_analysis.md` shows that many "false positives" are spam-looking mail that TREC-06 +labels ham (quoted spam inside mailing-list threads, "remove me from your list" replies, sweepstakes mail) and +that the largest block of misses are "ScanMail virus found" notices TREC-06 labels spam. Legitimate TREC-06 mail +is also ~3x longer than its spam and 24% of it is truncated at 512 tokens. + +| test_holdout3 (3,220 emails: 1,999 legit / 521 phishing / 700 spam; tokenizer never saw them) | Legitimate | Phishing | Spam | +|---|---|---|---| +| Precision / Recall / F1 | 0.9900 / 0.9935 / 0.9918 | 0.9961 / 0.9866 / 0.9913 | 0.9785 / 0.9757 / 0.9771 | +| Macro-F1 (3 classes) / accuracy | **0.9867** / 0.9885 | | | +| Binary roll-up (unwanted vs legitimate): F1 / recall / precision / ROC-AUC / PR-AUC | 0.9864 / 0.9836 / 0.9893 / 0.9993 / 0.9989 | | | +| Phishing vs spam, given the mail is unwanted (1,221 rows) | 0.9959 (4 phishing called spam, 1 spam called phishing) | | | + +3x3 confusion matrix (rows true Legitimate / Phishing / Spam, columns predicted): `[1986, 1, 12]`, `[4, 514, 3]`, `[16, 1, 683]`. +Three-way accuracy per source: Assassin 95.3% (n=236), CEAS-08 99.0% (n=736), Enron 98.4% (n=628), Nazario 98.6% (n=222), Nigerian_Fraud 98.7% (n=299), TREC-07 99.8% (n=1099). + +| Prediction time, one email, batch 1 (`reports/latency.json`, 100 TREC-06 samples, mean 280 tokens) | mean | p50 | p95 | vs 74 ms benchmark | +|---|---|---|---|---| +| GPU, bf16 (RTX 3090 Ti) | **10.1 ms** | 8.9 ms | 13.7 ms | 0.14x (faster) | +| CPU, fp32, 16 threads | **459 ms** | 445 ms | 701 ms | 6.2x (slower) | + +Plots next to these files: `reports/confusion_matrix.png`, `confusion_matrix_test3.png`, +`confusion_matrix3_test3.png` (3x3), `roc_curve*.png`, `pr_curve*.png`, `finetune_curves.png`, +`pretrain_curves.png`, `eval_finetune_curves*.png`, `eval_pretrain_curves*.png`; written error analyses +`error_analysis.md` / `error_analysis_test3.md`. The fine-tune experiment table is `finetune.py list` (section 9). +Every run's own evaluation lives in `reports/eval_/`. + +### 2.2 Measured facts that exist today + +| Fact | Value | Source | +|---|---|---| +| Parameters | 337,476,608 (MLM, tied head) / 337,443,842 (classifier) | `reports/smoke_test.json` | +| Pretraining VRAM, micro-batch 16, no checkpointing | 13.65 GiB reserved, 23,642 tok/s | `reports/smoke_test.json` | +| Pretraining VRAM, micro-batch 24, no checkpointing (**used**) | 17.65 GiB reserved, 19.80 GiB total device incl. desktop, 25,227 tok/s | `reports/smoke_test.json` | +| Pretraining VRAM, micro-batch 24, gradient checkpointing | 8.06 GiB reserved, 19,156 tok/s | `reports/smoke_test.json` | +| Measured bf16 matmul | 63.9 TFLOPS (4096^3); the smoke test's 25k tok/s is ~63% MFU of the 80 TFLOPS peak | RUN_LOG 0, 3 | +| Convergence check (fresh init, 30 steps) | MLM loss 10.58 -> 7.67 (ln V = 10.40) | RUN_LOG 3 | +| Latency, one 512-token email, GPU bf16 (RTX 3090 Ti), untrained-weight smoke test | 10.7 ms (trained checkpoint, real emails: section 2.1) | `reports/smoke_test.json` | +| Latency, one 512-token email, CPU fp32, 16 threads, untrained-weight smoke test | 699 ms (319 ms at 128 tokens) | `reports/smoke_test.json` | +| Pretraining plan `large-24L` | 12 epochs x 796 steps = 9,552 optimizer steps, micro-batch 24 x 11 = effective batch 264, peak LR 1.5e-4, warmup 573, cosine to 10% | `checkpoints/pretrain/large-24L/run_summary.json` | +| **Pretraining result `large-24L`** (complete 2026-10-09 03:31) | 9,552 steps, 1.29B tokens, 15.05 h of training, 23,889 tok/s mean, peak 17.94 GiB reserved; train MLM loss 1.524, **val MLM loss 1.986, masked-token accuracy 62.9%** (val loss fell monotonically, no restart needed) | `checkpoints/pretrain/large-24L/run_summary.json`, `metrics.csv` | + +The smoke-test latencies were measured on untrained weights (latency does not depend on the weights); the +numbers to quote are the trained-checkpoint measurements in section 2.1 (`reports/latency.json`). The +instructor's 0.074 s/sample benchmark is met on GPU by a factor of ~7 and missed on CPU by a factor of ~6, +which is why the Space runs on ZeroGPU. + +### 2.3 Data splits + +Binary build (RUN_LOG 1, `data_prep.py build`): + +| split | rows | positives | base rate | sources | +|---|---|---|---|---| +| finetune_train | 157,113 | 75,675 | 48.2% | TREC-05 46,386 / TREC-07 47,233 / CEAS-08 34,178 / Enron 24,223 / Assassin 5,093 | +| finetune_val | 17,643 | 8,642 | 49.0% | same five, grouped by near-duplicate cluster | +| test_holdout (TREC-06) | 16,302 | 3,894 | 23.9% | never seen by tokenizer, pretraining or fine-tuning | +| pretrain (MLM) | 226,273 docs | - | - | corbt/enron-emails after dedup + quote stripping; 407.9M chars, 107.7M tokens | + +Three-way build (RUN_LOG 7, `data_prep.py build3`; label3 0 legit / 1 phishing / 2 spam / -1 unwanted-unsplit): + +| split | rows | legit | phishing | spam | unwanted (TREC/CEAS positives) | +|---|---|---|---|---|---| +| finetune_train | 160,263 | 81,438 | 3,150 | 12,870 | 62,805 | +| finetune_val | 15,284 | 7,002 | 340 | 763 | 7,179 | +| test_holdout (TREC-06, rows unchanged) | 16,302 | 12,408 | - | - | 3,894 | +| **test_holdout3** | 3,220 | 1,999 | 521 | 700 | - | + +Plus `data/processed/synthetic.parquet`: 2,000 template-generated rows (1,000 legitimate, 1,000 phishing +lures, 0 spam), training-only, off by default. finetune_val shrank from 17,643 to 15,284 rows when +test_holdout3 was carved out, so val numbers from binary-era runs are not strictly comparable. + +--- + +## 3. Setup on Fedora 44 + +Machine used: Fedora 44 KDE, RTX 3090 Ti 24 GB (sm_86), NVIDIA driver 615.71.09 (CUDA 13.4), Ryzen 9 5950X, +40 GB RAM, system Python 3.14.7, uv 0.12.5 (`~/.local/bin/uv`), tmux. + +```bash +cd "/home/nathank/BITS/dev/Phishing Model" +uv venv --python 3.14.7 .venv +uv pip install --python .venv/bin/python --torch-backend=cu130 -r requirements-train.txt +hf auth login # .venv/bin/hf; needed for the upload step only +``` + +Why these pins: + +- **`--torch-backend=cu130`, never `auto`.** The plain PyPI torch 2.14.1 Linux wheel is the CUDA 13.0 build; + driver 615 runs it by backward compatibility. uv 0.12.5's `auto` would pick cu132 on this driver. One + resolution installs everything (180 packages, all binary wheels, venv ~6.1 GB). +- **Python 3.14.7, not 3.15.** torch.compile raises on 3.15, triton 3.8.0 requires <3.15 and `spaces` requires + <3.15. Always pass `--python 3.14.7`. +- **`huggingface_hub==1.33.0`** because tokenizers 0.23.2 requires `huggingface-hub<2.0`. +- **`fsspec==2026.7.0`** because datasets 5.1.0 caps `fsspec[http]<=2026.7.0` (2026.9.0 is newer and breaks the + resolve). Do not `uv pip install -U fsspec`. + +Verify the GPU (do not run while a training job holds the GPU unless you accept a small extra CUDA context): + +```bash +nvidia-smi +.venv/bin/python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.get_device_name(0), torch.cuda.is_bf16_supported())" +# expected (2026-10-07): 2.14.1+cu130 13.0 NVIDIA GeForce RTX 3090 Ti True +``` + +--- + +## 4. Project layout + +``` +Phishing Model/ +├── README.md this file +├── RUN_LOG.md dated log of every run, decision, number, bug and fix (the "challenges" source) +├── config.yaml every dial in one file (section 6) +├── requirements-train.txt pinned local training stack (Python 3.14.7, torch 2.14.1 cu130) +├── .gitignore excludes .venv, data, checkpoints, logs, weights, tokens +├── src/ +│ ├── common.py config loading, seeding, logging, text normalisation, dedup keys (shared by all) +│ ├── data_prep.py download -> dedupe -> decontaminate -> split -> leakage asserts (build/check/build3/check3) +│ ├── tokenizer.py byte-level BPE (NFKC, case kept): train on pretrain + finetune_train only; stats +│ ├── model.py from-scratch pre-LN encoder, MLM head, classification head (copied to app/) +│ ├── smoke_test.py VRAM micro-batch finder, throughput, SDPA backend check, convergence, latency +│ ├── pretrain.py resumable bf16 MLM pretraining (train/export/plot) +│ ├── make_synthetic.py 2,000 template-generated training rows (build/show) +│ ├── finetune.py 3-way classifier fine-tuning with the instructor's dials (train/plot/list) +│ ├── evaluate.py the only reader of the holdouts: metrics, latency, plots, error analysis +│ └── export.py bf16 safetensors + config + model card -> model_export// and app/ +├── scripts/ +│ ├── launch_pretrain.sh start or resume pretraining in tmux session `phish-pretrain` +│ ├── run_experiments.sh fine-tune queue: waits for large-24L/final, runs the six dial experiments, +│ │ evaluates each into reports/eval_/, exports ft-ce to model_export/ft-ce and app/ +│ └── upload_to_hf.py publish model repo + Space; dry run by default, `--yes` to write +├── app/ the Hugging Face Space +│ ├── app.py Gradio UI: 3-class label, prediction time, token saliency, rule-based cues +│ ├── README.md Space front matter (sdk gradio 6.29.1, python 3.12.12) + description +│ ├── requirements.txt Space pins (torch 2.13.0, spaces 0.51.3, ...) +│ ├── .gitattributes LFS rules for weights and tokenizer +│ └── model.py, model.safetensors, config.json, tokenizer.json written by export.py (not yet present) +├── FinalProject_PhishingDetector.ipynb presentation notebook in the instructor's 9-step pattern (section 5.10); +│ written by the notebook step, in progress as of 2026-10-08 14:00 +├── reference/ +│ ├── FashionMNIST_Gradio_Full.ipynb instructor's example notebook (the 9-step pattern) +│ └── INSTRUCTOR_REQUIREMENTS.md grading rubric, verbatim +├── docs/research/ +│ ├── research_dataset-schemas.md schemas, provenance and licences of every candidate dataset +│ ├── verify_data.md independent verification of the data plan and holdout choice +│ ├── research_model-math.md parameter / memory / FLOP / latency math for the encoder +│ ├── model_math.py pure-Python sizing calculator behind that document +│ ├── research_torch-python.md torch 2.14.1 / CUDA 13.0 / Python 3.14 compatibility, SDPA backends +│ ├── research_other-libs.md every other pin and its conflicts (fsspec, huggingface_hub, spaces) +│ ├── verify_pins.md independent verification of the pins +│ ├── research_gradio-spaces-hub.md Gradio 6, ZeroGPU, Space front matter, Hub upload API +│ └── bigshp_research.md why the 300k "spam/ham/phish" set was rejected +├── data/ (git-ignored) +│ ├── raw/ HF cache for corbt, Champa mirror CSVs (aura/), inspected-only 300k set (bigshp/) +│ └── processed/ +│ ├── finetune_train.parquet, finetune_val.parquet training / model-selection splits (with label3) +│ ├── test_holdout.parquet TREC-06 binary test set +│ ├── test_holdout3.parquet clean-provenance three-way test set +│ ├── pretrain.parquet MLM corpus (226,273 docs) +│ ├── pretrain_tokens.npy/.json, pretrain_val_tokens.npy cached token stream used by pretrain.py +│ ├── synthetic.parquet, synthetic_report.json template supplement + shortcut audit +│ ├── tokenizer.json, tokenizer_report.json BPE vocabulary + per-split length stats +│ ├── splits_manifest.json, data_report.md counts, removals, every leakage assertion +│ └── binary_snapshot/ binary-era splits that build3 starts from +├── checkpoints/ (git-ignored) +│ ├── pretrain/large-24L/ live run: step-NNNNNNN/, metrics.csv, run_summary.json, final/ when done +│ ├── pretrain/dev/ tiny self-test run (delete after Nathan's OK) +│ └── finetune// best/, last/, metrics.csv, epochs.csv per fine-tune run (dev, dev-focal = self-tests) +├── reports/ smoke_test.json; experiments.jsonl + finetune_curves.png + pretrain_curves.png are +│ dev-only until the real runs regenerate them (section 9); metrics*, latency*, +│ confusion/ROC/PR plots, error analyses, eval_/ later +├── model_export// export output (created by export.py) +└── logs/ one timestamped log per script invocation + tmux stdout of the pretraining run +``` + +--- + +## 5. How to run each phase, in order + +All commands run from the project root with `.venv/bin/python` (written `PY` below: +`PY=.venv/bin/python`). Every script takes `--config config.yaml` before the subcommand and writes a log to +`logs/