Refresh presentation bundle (RUN_LOG 25, README 5.11, builder --colab) and add the Colab notebook
07cdb81 verified |
Download presentation/RUN_LOG.md from BuildITSystems/phishing-detector-encoder: direct link, hf CLI and curl.
- Browser
- Download file 79.2 kB
-
https://huggingface.co/BuildITSystems/phishing-detector-encoder/resolve/main/presentation/RUN_LOG.md
- Command line
-
hf download hf://BuildITSystems/phishing-detector-encoder/presentation/RUN_LOG.md
-
curl -L -o RUN_LOG.md https://huggingface.co/BuildITSystems/phishing-detector-encoder/resolve/main/presentation/RUN_LOG.md
79.2 kB
| # RUN_LOG — BITS Phishing Detector | |
| Running record of what was run, with what settings, what happened, and what was fixed. | |
| Newest entries at the bottom. This becomes the "challenges" section of the presentation. | |
| --- | |
| ## 0. 2026-10-07 — Plan confirmation, environment, pins | |
| **Machine.** Fedora 44 KDE, RTX 3090 Ti 24 GB (sm_86), driver 615.71.09 (CUDA 13.4), Ryzen 9 5950X, | |
| 40 GB RAM, 741 GB free. System Python 3.14.7, uv 0.12.5. ~1.9 GB VRAM already used by the desktop. | |
| **Decision: Python 3.14 stays.** Checked live: torch 2.14.1 and every other pinned package ship | |
| cp314 (or abi3 / pure) Linux wheels; torch.compile is allowed on 3.14 (only 3.15 raises); triton 3.8.0 | |
| requires <3.15. No Python update needed. (uv also downloaded a managed CPython 3.12.14 as a side | |
| effect of dry-resolving the Space requirements; harmless, kept for testing the Space stack locally.) | |
| **Decision: torch 2.14.1, CUDA 13.0 build.** The plain PyPI Linux wheel *is* the cu130 build | |
| (cuda-toolkit 13.0.3, cuDNN 9.24, NCCL 2.30.7). Driver 615 (CUDA 13.4) runs it by backward compatibility. | |
| Install with `uv pip install --torch-backend=cu130`; never `--torch-backend=auto` (uv 0.12.5 would pick | |
| cu132 on this driver). | |
| **Constraint found: ZeroGPU pins the Space stack.** ZeroGPU supports torch 2.8.0 – 2.13.0 and Python | |
| 3.12.12 / 3.10.13 only, and does not support torch.compile. So the Space uses torch 2.13.0 on Python | |
| 3.12.12 while training uses 2.14.1 on 3.14. Weights are safetensors, so this is fine. Also: a private | |
| model repo cannot be preloaded by the Space; weights download at startup with an HF_TOKEN secret. | |
| **Constraint found: huggingface_hub must stay on 1.x.** tokenizers 0.23.2 requires huggingface-hub<2.0, | |
| so huggingface_hub 1.33.0 (ships the `hf` CLI) everywhere. | |
| **Data problems found (before downloading anything):** | |
| - `it4lia/PhishingEmailCuratedDatasets_Cleaned` has **no body text** (subjects/headers + 53 numeric | |
| features). Unusable for a text model. Excluded. | |
| - `zefang-liu/phishing-email-dataset` is a lowercased re-packaging already 91.9% contained in | |
| `puyang2025/seven-phishing-email-datasets` (one 17 MB row, 533 literal "empty" rows, and 20% of its | |
| "Safe" rows contain the token "enron" vs 0% of phishing: a shortcut). Excluded. | |
| - The primary set's own train/test split leaks: 5.1% of test rows are exact duplicates of train rows, | |
| ~40% have a near-duplicate. We build our own split. | |
| - Two sub-sources are Enron-derived: 89% of TREC-05 ham and 86% of Enron-Spam ham appear in the | |
| `corbt/enron-emails` pretraining corpus. Neither can be the test set. | |
| - `corbt/enron-emails` is 52.7% exact duplicates (517,401 -> 244,496 distinct); ~117M tokens remain, | |
| about 230k packed 512-token sequences per epoch, so one epoch is ~1,144 optimizer steps at batch 256. | |
| - No dataset separates "spam" from "phishing". Every label-1 row comes from a spam-track corpus. | |
| Mapping spam->0 would leave ~0 positives. **Proposed:** positive class = "phishing/spam (unwanted)", | |
| plus a credential-lure slice of the test set reported separately. PENDING Nathan's confirmation. | |
| **Decision: hold out TREC-06 entirely as the test set** (16,259 rows after dropping 141 near-empty | |
| rows; 12,394 legitimate / 3,865 positive = 23.8%). It is Enron-clean (5 rows mention "enron", 0 | |
| enron.com addresses). Caveats: ~47% of its ham is academic mailing-list traffic; its spam headers were | |
| rewritten by TREC, so the model input is subject + body only. 1,766 training rows that near-duplicate | |
| TREC-06 templates get removed from the training pool. | |
| **Model math.** 24L / d1024 / 16 heads / FFN 4096 / vocab 32768 / seq 512 = 337,476,608 params | |
| (pretraining) / 337,443,842 (classifier). AdamW state 5.0 GiB. Micro-batch 16 without checkpointing | |
| is predicted at ~16.6 GiB total (7.4 GiB headroom) -> inside the 14–18 GB budget. Predicted | |
| throughput 12k–18k tokens/s (30–45% MFU of 80 TFLOPS bf16) -> 2.3–3.5 h per 150M-token epoch. | |
| Inference: ~14 ms per 512-token email on the 3090 Ti; ~0.3 s on the CPU in fp32 (the instructor's | |
| 0.074 s/sample benchmark is met on GPU, not on CPU at full length). | |
| **Status.** Project scaffolded, venv created on 3.14.7, pinned install started (logs/install.log). | |
| Next: verify GPU from torch, then data_prep -> tokenizer -> model -> smoke test -> pause for numbers. | |
| **Step 2 result (22:45).** Pinned install succeeded (180 packages, all binary wheels; venv 6.1 GB). | |
| From inside the venv: torch 2.14.1+cu130, CUDA 13.0 build, cuDNN 9.24; device NVIDIA GeForce RTX | |
| 3090 Ti, sm_86, 23.60 GiB, bf16 supported; SDPA Flash / memory-efficient / math backends all run | |
| (Flash with no mask, memory-efficient with a bool padding mask); bf16 4096^3 matmul measured | |
| 63.9 TFLOPS (spec ~80); torch.compile works on Python 3.14 (triton built its driver stub with a | |
| harmless _POSIX_C_SOURCE warning); free VRAM at idle with the desktop up: 21.5 GiB. All pinned | |
| libraries import; config.yaml parses. `hf` CLI present, not logged in yet (Nathan to run `hf auth login`). | |
| **Agent policy (per Nathan, 21:14).** Model and effort are set per agent from here on: mechanical | |
| lookups -> Sonnet/low; judgment-heavy research -> Opus or Fable/medium-high; math, core code and | |
| adversarial verification -> Fable/high. The step-1 research run inherited Fable for all 7 agents. | |
| ## 1. 2026-10-07 23:41 — Data build (data_prep.py build + check) | |
| **Run.** `.venv/bin/python src/data_prep.py build` then `check`. 2 min 20 s wall, peak RSS 6.9 GB, | |
| HF cache 1.5 GB (puyang2025 @ b7f3f8d9, corbt @ cfc06c75). Every asserted leakage check is 0 in | |
| both the build-time recount and the independent `check` over the written files. | |
| | 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-dup cluster so no template straddles train/val | | |
| | test_holdout (TREC-06) | 16,302 | 3,894 | 23.9% | never touched by tokenizer, pretraining, or fine-tuning | | |
| | pretrain (MLM) | 226,273 docs | - | - | corbt/enron-emails after dedup + quote stripping; 408M chars | | |
| **What was removed and why.** 715 labelled rows with <20 alphanumerics (91% of them TREC-05/TREC-06 | |
| garbage spam rows). Ling excluded (2,859 academic rows). 18 rows in 8 groups where the identical body | |
| carries both labels (Enron ham vs TREC-05 spam newsletters) — dropped as label noise. 6,715 exact | |
| duplicates in the pool. **1,652 training rows removed as near-duplicates of TREC-06 templates** | |
| (TREC-05 spam 863, Enron spam 456, TREC-07 spam 292): these are spam templates shared across the | |
| TREC years; keeping them would let the model memorise test-set templates. corbt: 517,401 rows -> | |
| 272,905 exact duplicates removed (52.7%), 14,027 too short after quote stripping, 4,004 matching a | |
| val email removed, 85 near-duplicates of holdout emails removed, 35,172 overlapping fine-tune TRAIN | |
| emails kept (allowed: those are Enron ham). Quote stripping cut 21,832 bodies (kept 89% of all | |
| characters; the >=200-alnum guard left 70,907 marked bodies intact). | |
| **Deviation from the research estimate.** Holdout is 16,302 rows (research: 16,259) because the | |
| <20-alnum filter is applied to subject+body rather than body alone: 43 TREC-06 rows have a real | |
| subject and a near-empty body, which is a legitimate model input. Pretraining corpus is 408M chars | |
| (research: ~466M) because quote stripping and the val screen remove more than the estimate assumed. | |
| **Problem noted.** The val split has 5,336 near-duplicate links into the pretraining corpus | |
| (883 val emails, Enron boilerplate). Counted, not removed: val is only used for model selection | |
| and the holdout is the real gate. Exact val matches (4,004) were removed from pretraining. | |
| ## 2. 2026-10-07 23:43 — Tokenizer (tokenizer.py train + stats) | |
| **Run.** Byte-level BPE, NFKC only (case preserved on purpose: ALL-CAPS is a spam signal), vocab 32,768, | |
| min_frequency 2, specials [PAD]=0 [UNK]=1 [CLS]=2 [SEP]=3 [MASK]=4. Trained on pretrain.parquet | |
| (226,273 docs, 408M chars) + finetune_train.parquet (157,113 docs, 278M chars) only; val and test are | |
| refused by name and an exact-hash tripwire against the holdout found 0 collisions. Training took 28 s | |
| (49 s wall incl. the tripwire), 3.6 GB RSS. Saved data/processed/tokenizer.json. | |
| | split | rows | mean tok | median | p95 | chars/tok | % > 512 | mean tok legit / positive | | |
| |---|---:|---:|---:|---:|---:|---:|---| | |
| | pretrain (20k sample) | 20,000 | 476 | 198 | 1,560 | 3.76 | 20.5% | - | | |
| | finetune_train | 157,113 | 487 | 227 | 1,540 | 3.63 | 24.1% | 621 / 343 | | |
| | finetune_val | 17,643 | 454 | 200 | 1,421 | 3.58 | 22.3% | 594 / 307 | | |
| | test_holdout | 16,302 | 709 | 244 | 1,254 | 3.43 | 21.9% | 842 / 283 | | |
| **Observations.** [UNK] rate is 0 by construction (full 256-byte alphabet). Round trips are exact | |
| except for NFKC folding (full-width "Microsoft" -> "Microsoft", a deliberate anti-evasion | |
| property). About 22% of emails are longer than 512 tokens and get truncated; the pretraining corpus | |
| is ~108M tokens at 3.76 chars/token. **Length is a confound:** legitimate emails average ~2x the | |
| tokens of positives in every split (holdout 842 vs 283). Truncation at 512 blunts this, and the | |
| error analysis must check whether the classifier leans on length. Outliers exist (one 886,635-token | |
| email in train) and are harmless under truncation. | |
| ## 3. 2026-10-07 23:46 — Smoke test (smoke_test.py, real packed text, reports/smoke_test.json) | |
| **Model.** 337,476,608 params (MLM, tied head) / 337,443,842 (classifier) — exactly the research | |
| figure. 3,000 real pretraining docs -> 1,592,926 tokens -> 3,111 packed 512-token sequences; in-code | |
| no-leak check: 0 of 3,000 docs match a holdout email. | |
| **VRAM (bf16 autocast, fused AdamW, real gradient-accumulation pattern, desktop using 2.08 GiB):** | |
| | micro-batch | checkpointing | peak reserved | total device used | tokens/s | | |
| |---|---|---|---|---| | |
| | 8 | off | 9.71 GiB | 11.85 GiB | 21,493 | | |
| | 12 | off | 11.78 GiB | 13.92 GiB | 23,487 | | |
| | **16** | off | **13.65 GiB** | 15.80 GiB | **23,642** | | |
| | 24 | off | 17.65 GiB | 19.80 GiB | 25,227 | | |
| | 24 | on | 8.06 GiB | 10.21 GiB | 19,156 | | |
| The finder picked 24 (largest in the 14–18 GiB band) but total device use then sits at 19.8 GiB, | |
| 0.2 GiB under the 20 GiB "never exceed" line. Micro-batch 16 is 6% slower, gives an exact effective | |
| batch of 256 (24 x 11 = 264), and leaves ~8 GiB of headroom for desktop spikes. **Recommendation: | |
| micro-batch 16 x 16 accumulation for the long run.** Checkpointing is not needed for memory and | |
| costs ~25% throughput; keep it off. | |
| **Throughput / ETA.** 25,187 tok/s at B=24 (23,642 at B=16). Measured 0.2595 tokens/char on the | |
| packed sample -> the 408M-char corpus is ~105.8M tokens/epoch = ~807 optimizer steps at 256 x 512. | |
| About 1.2–1.3 h per epoch; 3 epochs ~3.75 h, 6 epochs ~7.5 h. Prediction was 2.3–3.5 h/epoch at | |
| 12k–18k tok/s: the real machine is ~1.5x faster than the 30–45% MFU assumption (25k tok/s ≈ 63% MFU | |
| of the 80 TFLOPS peak). | |
| **SDPA.** Flash runs on the mask-free packed path; memory-efficient runs with the bool padding mask; | |
| Flash correctly refuses a mask (no silent math fallback). | |
| **Convergence.** 30 steps at B=24, LR 1e-4, 10-step warmup, real text: loss 10.58 -> 7.67 | |
| (ln V = 10.40). The pipeline learns. | |
| **Inference latency (batch 1, 512 tokens).** GPU bf16: 10.7 ms (0.14x the instructor's 74 ms). | |
| CPU fp32, 16 threads: 699 ms at 512 tokens, 319 ms at 128 tokens (9.4x / 4.3x the benchmark). | |
| The research's 336 ms / 79 ms CPU estimate was 2–4x too optimistic; CPU inference is per-op-overhead | |
| bound, so truncation barely helps. **The 74 ms benchmark is met on GPU only -> the Space must be | |
| ZeroGPU, not CPU-basic.** | |
| **Challenges found by the reviewers before this run (fixed):** the first finder measured peak | |
| VRAM with a single fwd/bwd/opt step and under-reported the real accumulation peak by one fp32 | |
| gradient copy (1.26 GiB); the first MLM-corpus decontamination used exact keys only and missed | |
| near-duplicate (quoted/forwarded, 419-template) variants of holdout emails; floor-division | |
| accumulation would have reported effective batch 256 while training at 240 for B=24; | |
| safetensors refuses the tied state_dict unless the alias key is dropped (helper added); | |
| the tied decoder leaks gradient into the [PAD] embedding row (post-accumulate hook added). | |
| ## 4. 2026-10-08 00:40 — Step-4 code build (pretrain / finetune / evaluate / export / app / upload / synthetic) | |
| **What was built** (each file by one agent, then an independent adversarial reviewer who ran it, then | |
| a fix pass): `src/pretrain.py` (resumable MLM trainer, 1,019 lines), `src/finetune.py` (classifier | |
| trainer with the instructor's dials as flags; experiments.jsonl record per run), `src/evaluate.py` | |
| (the only script allowed to open the TREC-06 holdout; metrics, latency, plots, error analysis), | |
| `src/export.py` (bf16 safetensors + model card + byte-identical app/model.py), `app/app.py` + | |
| `requirements.txt` + `README.md` (Gradio 6 / ZeroGPU app with saliency and rule-based signals), | |
| `scripts/upload_to_hf.py` (dry-run by default, `--yes` required), `src/make_synthetic.py` | |
| (2,000 template-generated rows, 8 positive + 8 legitimate business categories, source=synthetic). | |
| **Verified by running.** pretrain: fresh run -> kill -> `--resume` reproduces the exact data order | |
| (window indices checked against the seeded permutation), optimizer step counter and LR; SIGINT saves | |
| a checkpoint and exits 0; SIGKILL between checkpoints is recovered on resume (orphan metrics rows | |
| dropped); a forced OOM saves and exits 3; a 5 GiB test cap trips the VRAM guard; `--max-hours` stops | |
| cleanly; resume refuses a changed plan or model shape. The full token cache was built as a side | |
| effect: 107,498,500 train tokens + 1,021,888 val tokens (0.266 tok/char), 42.8 s. finetune: resume | |
| reproduces batch order; `--n-layers 4` loads only the first four pretrained layers; focal(gamma=0) | |
| == cross-entropy; the holdout parquet is never opened (inotify-verified). 14 agents, 451 tool calls. | |
| **Bugs the reviewers caught and the fixers closed (for the challenges slide):** a `--lr` override on | |
| `--resume` was silently discarded by the restored optimizer state but recorded in the experiment log | |
| as if applied; resuming a finished fine-tune run trained one extra step at lr 0; changing frozen | |
| layers on resume crashed with a raw torch error; metrics CSVs kept orphan rows after a hard crash; | |
| evaluate/export and app each had 2–3 majors (details in docs/research/ and the agents' reports). | |
| **Changes I made myself after reading pretrain.py:** (1) the OOM handler now covers the optimizer | |
| step and the eval pass too, not just the forward/backward loop, so a desktop VRAM spike during the | |
| 500-step eval cannot lose 45 minutes of work; (2) a resume with nothing left to do no longer | |
| overwrites run_summary.json; (3) `--resume` compares `--limit-docs` with the checkpoint before | |
| tokenising anything; (4) **every packed window now starts with [CLS]** (511 stream tokens follow), | |
| as RoBERTa prepends `<s>`: the reviewer found 0 occurrences of [CLS] in the 107M-token stream, | |
| which would have left the classifier's pooled position untrained until fine-tuning. | |
| **Known gaps going into the long run:** the real 337M fine-tune, evaluate and export have only run on | |
| tiny dev models/subsets; `--synthetic on` path untested end to end; ZeroGPU runtime behaviour | |
| untested until the first Space build. Dev checkpoints from the self-tests sit in | |
| checkpoints/{pretrain,finetune}/dev* (~15 GB) pending Nathan's OK to delete. | |
| ## 5. 2026-10-08 12:28 — Pretraining run `large-24L` launched (Nathan's settings) | |
| **Decisions (Nathan, 2026-10-08).** Micro-batch 24 (17.65 GiB reserved measured; total GPU use ~20 GB | |
| with the desktop). 12 epochs: ~14 h at 25k tok/s, 9,552 optimizer steps, warmup 573 steps, cosine to | |
| 10% of 1.5e-4. Rationale: dynamic masking makes every pass different and data-constrained scaling | |
| results show repeated passes keep helping with diminishing returns; val MLM loss (64 fixed windows | |
| from the held-out last 1% of documents) is evaluated every 500 steps and the run is stopped if it | |
| rises. Positive class is named **"Spam/Phishing"** in the app, metrics and model card (the sources do | |
| not separate spam from phishing). Hugging Face login confirmed (BuildITSystems). | |
| **Launch.** `EPOCHS=12 MICRO_BATCH=24 scripts/launch_pretrain.sh` -> tmux session `phish-pretrain`, | |
| stdout in logs/pretrain_large-24L_20261008-122800.out. Trainer: 210,368 packed windows of | |
| [CLS] + 511 tokens (107.7M tokens/epoch), 796 steps/epoch, micro-batch 24 x 11 accumulation = | |
| **effective batch 264** (256 is not divisible by 24; the realised batch is used in every number), | |
| checkpoints every 500 steps (keep 3, ~3.8 GB each) + final/. Token cache reused (0 s). GPU at | |
| launch: 20.3 GB used, 98% util, 365 W. | |
| ## 6. 2026-10-08 ~13:00 — Scope change: three-way labels (legitimate / phishing / spam) | |
| **Nathan's decision.** The classifier should output legitimate, phishing, or spam. Our current positives | |
| cannot be split (TREC-05/07 and CEAS-08 labelled everything unwanted as "spam"), so phishing-labelled text | |
| from separate provenance is being added: the Champa et al. raw corpus mirror on the Hub | |
| (`kudzaiprichard/aura-phishing-email-corpus`, CC-BY-4.0, byte-identical to Zenodo 8339691) with | |
| `Nazario.csv` (1,565 phishing, 1995–2022) and `Nigerian_Fraud.csv` (3,332 advance-fee fraud), plus our | |
| synthetic BITS-style lures. Spam class from provenance: SpamAssassin, Enron-Spam and (if included) Ling | |
| spam folders. The 250k-row `locuoco/the-biggest-spam-ham-phish-email-dataset-300000` (labels ham/phish/ | |
| spam, provenance undocumented) is inspected first and used only if its labels agree with known provenance | |
| and it adds rows we do not have, after deduplication against every split and the TREC-06 holdout. | |
| **Design.** One encoder, one 3-class head, partial-label loss: rows with clean provenance train with | |
| 3-class cross-entropy; the ~70k TREC/CEAS positives train on the probability mass of {phishing, spam} | |
| ("unwanted") so they still shape the legitimate-vs-unwanted decision. Binary roll-up P(unwanted) = | |
| P(phishing)+P(spam) keeps TREC-06 as the clean binary test; a new `test_holdout3` slice of clean- | |
| provenance rows (drawn from sources never seen by the tokenizer or the pretraining corpus) gives the | |
| 3x3 confusion matrix. `num_labels` lives under `finetune:` in config.yaml, not under `model:`, because | |
| the live pretraining run refuses to resume if its model snapshot changes. | |
| ## 7. 2026-10-08 13:21 — Three-way data build (build3) and the 300k-set verdict | |
| **300k set: rejected after inspection** (`docs/research/bigshp_research.md`). `locuoco/the-biggest-spam-ham-phish- | |
| email-dataset-300000` is 365,448 rows, not deduplicated (51.5% within-file duplicates), and is a union of | |
| Kaggle re-packagings of corpora we already hold: 97.6% of its ham, 92.5% of its phish and 90.2% of its | |
| spam rows match our text. Its ham/spam labels agree with our provenance 99.9%/99.8% because they are our | |
| labels copied back; its **phish label agrees only 60.5%**: 14,660 "phish" rows are SpamAssassin/Enron-Spam | |
| spam-folder mail, 17,714 are TREC/CEAS spam-track rows, and only 4,784 are confirmed phishing provenance | |
| (Nazario/Nigerian, which we take raw from the Champa mirror). **36,020 rows (9.9%) are copies of our frozen | |
| TREC-06 test set.** Every "phish" row went through a stopword-stripping pipeline that ham/spam rows did | |
| not, a preprocessing artifact that is a near-perfect label proxy and would not transfer to real inbound | |
| mail. Decision: not one row ingested; `data.threeway.bigshp.enabled: false` with the verdict recorded. | |
| **Champa mirror ingested** (`kudzaiprichard/aura-phishing-email-corpus`, CC-BY-4.0): Nazario 1,565 rows -> | |
| 1,507 kept; Nigerian_Fraud 3,332 -> 2,504 kept. Drops worth knowing: 235 Nigerian-fraud rows are near- | |
| duplicates of TREC-06 spam (419 letters recur across corpora) and 13 match holdout keys exactly: all | |
| removed so the TREC-06 test stays clean; 424 more were dropped because their clusters touched both train | |
| and val. | |
| **Three-way splits (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, unchanged rows) | 16,302 | 12,408 | - | - | 3,894 | | |
| | **test_holdout3 (new, clean provenance)** | 3,220 | 1,999 | 521 | 700 | - | | |
| test_holdout3 legit = CEAS-08 736 + TREC-07 1,099 + Assassin 164 (never Enron: no pretraining overlap); | |
| spam = Enron-Spam 628 + Assassin 72; phishing = Nazario 222 + Nigerian 299. All of it came from | |
| finetune_val (never seen by the tokenizer) or the new sources (never seen by anything). finetune_val | |
| shrank from 17,643 to 15,284 rows, so new val F1 values are not strictly comparable with the binary runs. | |
| Leakage: every asserted check is 0 in `check3` and the binary `check` still passes on the rewritten files. | |
| Existing rows in train/test_holdout are byte-identical to the binary build (data/processed/binary_snapshot). | |
| **Honest caveat.** Only 3,150 fully-labelled phishing training rows (plus 1,000 synthetic lures) carry the | |
| phishing-vs-spam distinction; 62,805 positives are trained as "unwanted" only. Expect the three-way split | |
| to be the weaker decision; the binary roll-up on TREC-06 stays the headline. | |
| ## 8. 2026-10-08 13:55 — Fine-tune batching moved into torch.utils.data.DataLoader | |
| The instructor's written spec requires PyTorch's DataLoader for batching and shuffling; finetune.py had | |
| its own numpy sampler. Every batch now goes through `torch.utils.data.DataLoader` (`make_loader`): a | |
| row-index Dataset, the seeded length-bucketed batch order as `batch_sampler`, a collate function that | |
| pads each micro-batch to its own longest row. Verified on the CPU fixture: losses and metrics identical | |
| to the previous code at every step. One problem found on the way: a DataLoader draws its worker base | |
| seed from the global torch RNG when iterated, which shifted the dropout stream; passing an explicit | |
| `generator` removed the side effect (first run differed at step 1, 1.1093 vs 1.1125; identical after). | |
| ## 9. 2026-10-08 14:15 — Dev checkpoints removed; fine-tuning queue armed | |
| Nathan approved both. Deleted the agents' self-test checkpoints (checkpoints/pretrain/dev, | |
| checkpoints/finetune/dev, checkpoints/finetune/dev-focal; 15.3 GB) and their two dev lines in | |
| reports/experiments.jsonl (backup in the session scratchpad). `scripts/run_experiments.sh` is running in | |
| tmux session `phish-finetune`, waiting for checkpoints/pretrain/large-24L/final/model.safetensors; it then | |
| runs ft-ce (3 epochs) and the five ablations (weighted CE, focal, synthetic on, 12 layers, scratch; 2 epochs | |
| each), evaluates every best/ on TREC-06 + the three-way holdout in one call, and exports ft-ce to | |
| model_export/ft-ce + app/. Pretraining at step 1,100 / 9,552, loss 5.96. | |
| ## 10. 2026-10-08 14:30 — Presentation notebook and README | |
| `FinalProject_PhishingDetector.ipynb` (generated by `scripts/build_notebook.py`, 48 cells) mirrors the | |
| instructor's nine "Step N" headers verbatim and in order, builds real `torch.utils.data.DataLoader`s from the | |
| processed splits in Step 2, instantiates the 337M model and prints its parameter table in Step 3, plots the | |
| live pretraining curve in Step 4 (shows "RUN IN PROGRESS" until final/ exists), times 100 single emails | |
| instructor-style in Step 5, shows the app files, runs the app's predict on built-in synthetic examples, | |
| checks the HF login and dry-runs the upload. It executes top to bottom on the CPU in ~70 s with zero errors | |
| today and prints "not available yet — produced by <command>" for the 12 artifacts the training queue will | |
| create; re-executing it after the queue fills everything in. Reviewer catches fixed before acceptance: one | |
| header lacked the instructor's "(Optional)", a dev-checkpoint caption stated a wrong step count, a | |
| Challenges bullet misdescribed the uv torch-backend issue, and nothing kept the notebook off the GPU when | |
| opened from a plain shell (now forced to CPU while the pretraining run owns the GPU). | |
| `README.md` documents setup, every phase's commands (verified with --help), config dials, data provenance | |
| and licences, the leakage policy, the experiment queue, deployment, the instructor-requirement mapping and | |
| the limitations. Reviewer catches fixed: it had documented a fictional experiment grid and a notebook path | |
| that did not exist. | |
| ## 11. 2026-10-08 19:15 — Hugging Face upload approved (deferred until the export exists) | |
| Nathan asked for the upload. Dry run at 19:14: the plan is a private model repo | |
| `BuildITSystems/phishing-detector-encoder` (model.safetensors, config.json, tokenizer.json, README.md) and a | |
| private ZeroGPU Space `BuildITSystems/phishing-detector-demo` (app.py, model.py, requirements.txt, README.md, | |
| .gitattributes + bundled weights, so no token secret is needed). It refuses to run today because | |
| model_export/ft-ce does not exist yet (the fine-tuning queue produces it tomorrow morning). The upload is | |
| scheduled to run automatically once the export lands and the dry run reports no problems. Pretraining at | |
| 19:14: step 4,280 / 9,552 (epoch 6 of 12), train MLM loss 2.10, on schedule for ~02:00. | |
| ## 12. 2026-10-09 02:45 — Pretraining status check (scheduled) | |
| Run `large-24L` at step 9,020 / 9,552 (epoch 12 of 12), ETA ~03:30: about an hour behind the 14 h | |
| estimate because each 500-step validation and 3.8 GB checkpoint write pauses training. Throughput | |
| steady at ~24.1k tokens/s, reserved VRAM 17.94 GiB, device total 20.2 GB with the desktop, GPU 86 C. | |
| No watchdog action, no OOM, no warnings since launch. Validation MLM loss on the held-out 1% of | |
| documents fell monotonically through all twelve passes (6.54 -> 6.38 -> 5.81 -> 5.25 -> 3.91 -> 3.16 -> | |
| 2.78 -> 2.58 -> 2.44 -> 2.36 -> 2.27 -> 2.20 -> 2.14 -> 2.11 -> 2.07 -> 2.04 -> 2.03 -> 2.01), masked-token | |
| accuracy 14.7% -> 62.9%, so twelve passes over the same corpus did not overfit; the biggest drop came | |
| between steps 2,000 and 3,000 (epochs 3-4), when the model stopped predicting frequent tokens and started | |
| using context. Train loss 1.55 (per-step, with dropout). The fine-tuning queue is waiting for final/. | |
| ## 13. 2026-10-09 03:31 — MLM pretraining run `large-24L` (complete) | |
| **Run.** `python src/pretrain.py train --run-name large-24L`; 337,476,608 parameters (EncoderForMLM, tied head). micro-batch 24 x accumulation 11 = 264 sequences x 512 tokens per step; bf16 autocast, fused AdamW [0.9, 0.98] eps 1e-06 wd 0.01 (no decay on biases/LayerNorm), clip 1.0; cosine schedule peak 1.5e-04 -> 10% over 9552 planned steps (12 epochs x 796 steps) with 573 warmup steps; dynamic 15% masking 80/10/10, loss on masked positions only; 210,368 packed 512-token windows (226,273 docs), 1,999 val windows. | |
| **Result.** 9552 optimizer steps (12.00 epochs), 1291.1M tokens seen, total training time 15h 03m 05s (54,186 s) over 1 invocation(s), mean 23,889 tokens/s; peak VRAM reserved 17.94 GiB / allocated 17.49 GiB; final train MLM loss 1.5237, val MLM loss 1.9863 (masked-token accuracy 62.9%). | |
| **Artifacts.** checkpoints/pretrain/large-24L/ (metrics.csv, run_summary.json, step-*/ and final/); log /home/nathank/BITS/dev/Phishing Model/logs/pretrain-20261008-122802.log. | |
| ## 14. 2026-10-09 04:07 — Hand-off to fine-tuning (scheduled check) | |
| Pretraining `large-24L` finished at 03:31 after 15 h 03 m: 9,552 optimizer steps, 1.29 B tokens seen | |
| (12 passes over 107.5 M), mean 23,889 tokens/s, peak VRAM reserved 17.94 GiB, final train MLM loss 1.52, | |
| validation MLM loss 1.99 with 62.9% masked-token accuracy (from 10.4 / ~0% at random init). final/ holds | |
| the fp32 encoder (1.35 GB) + tokenizer + config. The watchdog saw final/ and exited without ever acting. | |
| The queue started `ft-ce` at 03:36: 3 epochs x 5,009 steps at batch 32 (micro-batch 16), 13.1 GiB reserved, | |
| ~75 examples/s, ETA ~05:30 for this run. First validation at step 3,750 (0.75 epoch), 15,284 rows: | |
| unwanted-vs-legitimate F1 0.9855, ROC-AUC 0.9995, PR-AUC 0.9996; | |
| three-way slice (n=8105) macro-F1 0.9775, accuracy 0.9921, per-class F1 [legit, phishing, spam] = [0.996 0.967 0.970]; | |
| phishing-vs-spam on true unwanted rows [pp ps sp ss] = [340 0 6 757]. Val F1 is not comparable with the earlier binary dev numbers (val shrank by the 2,699 rows moved to test_holdout3). | |
| Queue estimate: ft-ce done ~05:30, then five 2-epoch ablations (~1 h each + evaluation) -> ~11:00-11:30. | |
| ## Fine-tune run `ft-ce` — 2026-10-09 04:28 | |
| **Dials.** init pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final; 3 classes; 24 layers, 0 frozen; loss cross_entropy; lr 3e-05; batch 32 (micro 16); epochs 3 planned; synthetic off (0 rows); selection val_f1_unwanted; train rows 160,263, val rows 15,284; params 337,444,867 (337,444,867 trainable). | |
| **Result.** 6250 steps, stopped by early_stopping; training time 51.5 min (eval 4.8 min of it); peak reserved 13.13 GiB. Best val at step 5000 (epoch 0), Unwanted (phishing or spam) roll-up: F1 0.9929, precision 0.9938, recall 0.9920, accuracy 0.9923, ROC-AUC 0.9996, PR-AUC 0.9996; F1-optimal threshold 0.318. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9782, accuracy 0.9922; per-class F1 Legitimate 0.9958, Phishing 0.9672, Spam 0.9716; phishing-vs-spam on true unwanted (n=1,103): phish->phish 340, phish->spam 0, spam->phish 4, spam->spam 759. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-ce/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-ce/last. | |
| ## 15. 2026-10-09 04:28 — Evaluation on the test + test3 splits (evaluate.py run, `ft-ce`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-ce/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 285.1 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9404** (232 missed of 3,894) | | |
| | precision | 0.8610 (591 false alarms) | | |
| | F1 | 0.8990 | | |
| | accuracy | 0.9495 | | |
| | specificity | 0.9524 | | |
| | ROC-AUC | 0.9875 | | |
| | PR-AUC | 0.9626 | | |
| | recall @ 1% FPR | 0.7745 | | |
| | recall @ 0.1% FPR | 0.3529 | | |
| | best-F1 threshold | 0.821 (F1 0.9090) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.8996 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,662 flagged true positives the model called 1,294 phishing and 2,368 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 240.9 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9853** (18 missed of 1,221) | | |
| | precision | 0.9877 (15 false alarms) | | |
| | F1 | 0.9865 | | |
| | accuracy | 0.9898 | | |
| | specificity | 0.9925 | | |
| | ROC-AUC | 0.9992 | | |
| | PR-AUC | 0.9985 | | |
| | recall @ 1% FPR | 0.9869 | | |
| | recall @ 0.1% FPR | 0.8690 | | |
| | best-F1 threshold | 0.411 (F1 0.9869) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 0.9925, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9863, **macro-F1 0.9822**; Legitimate P 0.991 / R 0.992 / F1 0.992 (n=1,999); Phishing P 0.981 / R 0.973 / F1 0.977 (n=521); Spam P 0.977 / R 0.979 / F1 0.978 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,984/4/11; Phishing 9/507/5; Spam 9/6/685. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9894 (7 phishing->spam, 6 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9449 | 0.9464 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9905 | 0.9952 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9889 | 0.9944 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9505 | 0.9746 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9900 | 0.9950 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9973 | 0.9986 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 9.95 ms avg / 8.72 ms p50 over 100 emails (0.13x). CPU fp32 (16 threads): 514.07 ms avg / 526.19 ms p50 over 100 emails (6.95x). | |
| **Length confound (test).** FN mean 256 tokens vs TP 287; FP 763 vs TN 848 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-ce`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## Fine-tune run `ft-weighted` — 2026-10-09 05:55 | |
| **Dials.** init pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final; 3 classes; 24 layers, 0 frozen; loss weighted_cross_entropy; lr 3e-05; batch 32 (micro 16); epochs 2 planned; synthetic off (0 rows); selection val_f1_unwanted; train rows 160,263, val rows 15,284; params 337,444,867 (337,444,867 trainable). | |
| **Result.** 10018 steps, stopped by epochs_done; training time 83.6 min (eval 8.0 min of it); peak reserved 13.13 GiB. Best val at step 10000 (epoch 1), Unwanted (phishing or spam) roll-up: F1 0.9937, precision 0.9911, recall 0.9964, accuracy 0.9932, ROC-AUC 0.9998, PR-AUC 0.9998; F1-optimal threshold 0.899. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9775, accuracy 0.9903; per-class F1 Legitimate 0.9944, Phishing 0.9798, Spam 0.9583; phishing-vs-spam on true unwanted (n=1,103): phish->phish 339, phish->spam 1, spam->phish 2, spam->spam 761. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-weighted/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-weighted/last. | |
| ## 16. 2026-10-09 05:55 — Evaluation on the test + test3 splits (evaluate.py run, `ft-weighted`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-weighted/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 283.4 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9818** (71 missed of 3,894) | | |
| | precision | 0.8307 (779 false alarms) | | |
| | F1 | 0.9000 | | |
| | accuracy | 0.9479 | | |
| | specificity | 0.9372 | | |
| | ROC-AUC | 0.9943 | | |
| | PR-AUC | 0.9833 | | |
| | recall @ 1% FPR | 0.8767 | | |
| | recall @ 0.1% FPR | 0.6335 | | |
| | best-F1 threshold | 0.989 (F1 0.9383) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9565 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,823 flagged true positives the model called 801 phishing and 3,022 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 238.3 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9934** (8 missed of 1,221) | | |
| | precision | 0.9790 (26 false alarms) | | |
| | F1 | 0.9862 | | |
| | accuracy | 0.9894 | | |
| | specificity | 0.9870 | | |
| | ROC-AUC | 0.9993 | | |
| | PR-AUC | 0.9988 | | |
| | recall @ 1% FPR | 0.9910 | | |
| | recall @ 0.1% FPR | 0.7060 | | |
| | best-F1 threshold | 0.728 (F1 0.9878) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 1.0000, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9876, **macro-F1 0.9852**; Legitimate P 0.996 / R 0.987 / F1 0.991 (n=1,999); Phishing P 0.988 / R 0.987 / F1 0.988 (n=521); Spam P 0.964 / R 0.990 / F1 0.977 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,973/5/21; Phishing 2/514/5; Spam 6/1/693. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9943 (6 phishing->spam, 1 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9619 | 0.9603 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9823 | 0.9911 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9936 | 0.9968 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9820 | 0.9909 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9900 | 0.9950 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9936 | 0.9968 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 10.12 ms avg / 8.95 ms p50 over 100 emails (0.14x). CPU fp32 (16 threads): 411.57 ms avg / 397.57 ms p50 over 100 emails (5.56x). | |
| **Length confound (test).** FN mean 310 tokens vs TP 284; FP 1779 vs TN 782 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-weighted`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## Fine-tune run `ft-focal` — 2026-10-09 07:21 | |
| **Dials.** init pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final; 3 classes; 24 layers, 0 frozen; loss focal (gamma 2.0); lr 3e-05; batch 32 (micro 16); epochs 2 planned; synthetic off (0 rows); selection val_f1_unwanted; train rows 160,263, val rows 15,284; params 337,444,867 (337,444,867 trainable). | |
| **Result.** 10018 steps, stopped by early_stopping; training time 83.9 min (eval 8.0 min of it); peak reserved 13.13 GiB. Best val at step 8750 (epoch 1), Unwanted (phishing or spam) roll-up: F1 0.9951, precision 0.9954, recall 0.9948, accuracy 0.9947, ROC-AUC 0.9998, PR-AUC 0.9998; F1-optimal threshold 0.421. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9869, accuracy 0.9947; per-class F1 Legitimate 0.9971, Phishing 0.9868, Spam 0.9768; phishing-vs-spam on true unwanted (n=1,103): phish->phish 339, phish->spam 1, spam->phish 1, spam->spam 762. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-focal/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-focal/last. | |
| ## 17. 2026-10-09 07:22 — Evaluation on the test + test3 splits (evaluate.py run, `ft-focal`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-focal/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 282.8 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9741** (101 missed of 3,894) | | |
| | precision | 0.8794 (520 false alarms) | | |
| | F1 | 0.9243 | | |
| | accuracy | 0.9619 | | |
| | specificity | 0.9581 | | |
| | ROC-AUC | 0.9946 | | |
| | PR-AUC | 0.9848 | | |
| | recall @ 1% FPR | 0.8955 | | |
| | recall @ 0.1% FPR | 0.6497 | | |
| | best-F1 threshold | 0.682 (F1 0.9397) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9758 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,793 flagged true positives the model called 974 phishing and 2,819 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 238.7 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9836** (20 missed of 1,221) | | |
| | precision | 0.9893 (13 false alarms) | | |
| | F1 | 0.9864 | | |
| | accuracy | 0.9898 | | |
| | specificity | 0.9935 | | |
| | ROC-AUC | 0.9993 | | |
| | PR-AUC | 0.9989 | | |
| | recall @ 1% FPR | 0.9869 | | |
| | recall @ 0.1% FPR | 0.8788 | | |
| | best-F1 threshold | 0.475 (F1 0.9877) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 1.0000, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9885, **macro-F1 0.9867**; Legitimate P 0.990 / R 0.993 / F1 0.992 (n=1,999); Phishing P 0.996 / R 0.987 / F1 0.991 (n=521); Spam P 0.979 / R 0.976 / F1 0.977 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,986/1/12; Phishing 4/514/3; Spam 16/1/683. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9959 (4 phishing->spam, 1 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9534 | 0.9458 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9905 | 0.9952 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9841 | 0.9920 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9865 | 0.9932 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9866 | 0.9933 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9982 | 0.9991 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 10.34 ms avg / 9.01 ms p50 over 100 emails (0.14x). CPU fp32 (16 threads): 357.98 ms avg / 340.34 ms p50 over 100 emails (4.84x). | |
| **Length confound (test).** FN mean 322 tokens vs TP 284; FP 1237 vs TN 827 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-focal`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## Fine-tune run `ft-synthetic` — 2026-10-09 08:16 | |
| **Dials.** init pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final; 3 classes; 24 layers, 0 frozen; loss cross_entropy; lr 3e-05; batch 32 (micro 16); epochs 2 planned; synthetic on (2000 rows); selection val_f1_unwanted; train rows 162,263, val rows 15,284; params 337,444,867 (337,444,867 trainable). | |
| **Result.** 6250 steps, stopped by early_stopping; training time 52.0 min (eval 4.8 min of it); peak reserved 13.13 GiB. Best val at step 5000 (epoch 0), Unwanted (phishing or spam) roll-up: F1 0.9927, precision 0.9904, recall 0.9949, accuracy 0.9920, ROC-AUC 0.9996, PR-AUC 0.9997; F1-optimal threshold 0.609. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9665, accuracy 0.9891; per-class F1 Legitimate 0.9940, Phishing 0.9352, Spam 0.9704; phishing-vs-spam on true unwanted (n=1,103): phish->phish 340, phish->spam 0, spam->phish 6, spam->spam 757. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-synthetic/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-synthetic/last. | |
| ## 18. 2026-10-09 08:16 — Evaluation on the test + test3 splits (evaluate.py run, `ft-synthetic`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-synthetic/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 282.3 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9679** (125 missed of 3,894) | | |
| | precision | 0.8402 (717 false alarms) | | |
| | F1 | 0.8995 | | |
| | accuracy | 0.9483 | | |
| | specificity | 0.9422 | | |
| | ROC-AUC | 0.9908 | | |
| | PR-AUC | 0.9716 | | |
| | recall @ 1% FPR | 0.7871 | | |
| | recall @ 0.1% FPR | 0.4779 | | |
| | best-F1 threshold | 0.962 (F1 0.9186) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9528 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,769 flagged true positives the model called 1,486 phishing and 2,283 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 238.3 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9885** (14 missed of 1,221) | | |
| | precision | 0.9861 (17 false alarms) | | |
| | F1 | 0.9873 | | |
| | accuracy | 0.9904 | | |
| | specificity | 0.9915 | | |
| | ROC-AUC | 0.9993 | | |
| | PR-AUC | 0.9988 | | |
| | recall @ 1% FPR | 0.9885 | | |
| | recall @ 0.1% FPR | 0.8395 | | |
| | best-F1 threshold | 0.508 (F1 0.9873) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 1.0000, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9860, **macro-F1 0.9814**; Legitimate P 0.993 / R 0.991 / F1 0.992 (n=1,999); Phishing P 0.968 / R 0.987 / F1 0.977 (n=521); Spam P 0.980 / R 0.970 / F1 0.975 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,982/6/11; Phishing 4/514/3; Spam 10/11/679. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9869 (5 phishing->spam, 11 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9280 | 0.9296 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9851 | 0.9925 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9873 | 0.9936 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9820 | 0.9909 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9900 | 0.9950 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9982 | 0.9991 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 10.05 ms avg / 8.9 ms p50 over 100 emails (0.14x). CPU fp32 (16 threads): 481.35 ms avg / 464.28 ms p50 over 100 emails (6.50x). | |
| **Length confound (test).** FN mean 256 tokens vs TP 286; FP 686 vs TN 854 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-synthetic`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## Fine-tune run `ft-12L` — 2026-10-09 08:54 | |
| **Dials.** init pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final; 3 classes; 12 layers, 0 frozen; loss cross_entropy; lr 3e-05; batch 32 (micro 16); epochs 2 planned; synthetic off (0 rows); selection val_f1_unwanted; train rows 160,263, val rows 15,284; params 186,290,179 (186,290,179 trainable). | |
| **Result.** 8750 steps, stopped by early_stopping; training time 35.3 min (eval 3.1 min of it); peak reserved 6.97 GiB. Best val at step 6250 (epoch 1), Unwanted (phishing or spam) roll-up: F1 0.9925, precision 0.9936, recall 0.9914, accuracy 0.9919, ROC-AUC 0.9995, PR-AUC 0.9996; F1-optimal threshold 0.171. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9774, accuracy 0.9916; per-class F1 Legitimate 0.9954, Phishing 0.9695, Spam 0.9673; phishing-vs-spam on true unwanted (n=1,103): phish->phish 338, phish->spam 2, spam->phish 2, spam->spam 761. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-12L/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-12L/last. | |
| ## 19. 2026-10-09 08:54 — Evaluation on the test + test3 splits (evaluate.py run, `ft-12L`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-12L/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 591.7 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 1.7 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9461** (210 missed of 3,894) | | |
| | precision | 0.8120 (853 false alarms) | | |
| | F1 | 0.8739 | | |
| | accuracy | 0.9348 | | |
| | specificity | 0.9313 | | |
| | ROC-AUC | 0.9836 | | |
| | PR-AUC | 0.9513 | | |
| | recall @ 1% FPR | 0.6608 | | |
| | recall @ 0.1% FPR | 0.4073 | | |
| | best-F1 threshold | 0.955 (F1 0.8852) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9030 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,684 flagged true positives the model called 802 phishing and 2,882 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 501.6 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 1.7 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9787** (26 missed of 1,221) | | |
| | precision | 0.9860 (17 false alarms) | | |
| | F1 | 0.9823 | | |
| | accuracy | 0.9866 | | |
| | specificity | 0.9915 | | |
| | ROC-AUC | 0.9991 | | |
| | PR-AUC | 0.9983 | | |
| | recall @ 1% FPR | 0.9795 | | |
| | recall @ 0.1% FPR | 0.7748 | | |
| | best-F1 threshold | 0.169 (F1 0.9849) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 0.9888, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9842, **macro-F1 0.9807**; Legitimate P 0.987 / R 0.991 / F1 0.989 (n=1,999); Phishing P 0.988 / R 0.973 / F1 0.981 (n=521); Spam P 0.973 / R 0.971 / F1 0.972 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,982/5/12; Phishing 7/507/7; Spam 19/1/680. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9910 (10 phishing->spam, 1 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9619 | 0.9556 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9864 | 0.9932 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9793 | 0.9895 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9685 | 0.9840 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9766 | 0.9882 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9955 | 0.9977 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 5.05 ms avg / 4.42 ms p50 over 100 emails (0.07x). CPU fp32 (16 threads): 246.62 ms avg / 242.67 ms p50 over 100 emails (3.33x). | |
| **Length confound (test).** FN mean 260 tokens vs TP 286; FP 1105 vs TN 825 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-12L`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## Fine-tune run `ft-scratch` — 2026-10-09 09:57 | |
| **Dials.** init scratch; 3 classes; 24 layers, 0 frozen; loss cross_entropy; lr 3e-05; batch 32 (micro 16); epochs 2 planned; synthetic off (0 rows); selection val_f1_unwanted; train rows 160,263, val rows 15,284; params 337,444,867 (337,444,867 trainable). | |
| **Result.** 7500 steps, stopped by early_stopping; training time 60.9 min (eval 5.5 min of it); peak reserved 13.13 GiB. Best val at step 5009 (epoch 0), Unwanted (phishing or spam) roll-up: F1 0.9875, precision 0.9864, recall 0.9885, accuracy 0.9864, ROC-AUC 0.9985, PR-AUC 0.9986; F1-optimal threshold 0.601. Three-way on 8,105 fully-labelled val rows: macro-F1 0.9538, accuracy 0.9832; per-class F1 Legitimate 0.9915, Phishing 0.9398, Spam 0.9300; phishing-vs-spam on true unwanted (n=1,103): phish->phish 322, phish->spam 18, spam->phish 1, spam->spam 762. Checkpoints: /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-scratch/best, /home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-scratch/last. | |
| ## 20. 2026-10-09 09:57 — Evaluation on the test + test3 splits (evaluate.py run, `ft-scratch`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-scratch/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 282.4 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9165** (325 missed of 3,894) | | |
| | precision | 0.7752 (1,035 false alarms) | | |
| | F1 | 0.8400 | | |
| | accuracy | 0.9166 | | |
| | specificity | 0.9166 | | |
| | ROC-AUC | 0.9755 | | |
| | PR-AUC | 0.9456 | | |
| | recall @ 1% FPR | 0.7773 | | |
| | recall @ 0.1% FPR | 0.4954 | | |
| | best-F1 threshold | 0.941 (F1 0.8772) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9308 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,569 flagged true positives the model called 332 phishing and 3,237 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 237.5 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9771** (28 missed of 1,221) | | |
| | precision | 0.9763 (29 false alarms) | | |
| | F1 | 0.9767 | | |
| | accuracy | 0.9823 | | |
| | specificity | 0.9855 | | |
| | ROC-AUC | 0.9983 | | |
| | PR-AUC | 0.9974 | | |
| | recall @ 1% FPR | 0.9672 | | |
| | recall @ 0.1% FPR | 0.8354 | | |
| | best-F1 threshold | 0.366 (F1 0.9776) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 0.9925, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9727, **macro-F1 0.9634**; Legitimate P 0.986 / R 0.985 / F1 0.986 (n=1,999); Phishing P 0.990 / R 0.927 / F1 0.957 (n=521); Spam P 0.925 / R 0.970 / F1 0.947 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,970/3/26; Phishing 9/483/29; Spam 19/2/679. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9713 (32 phishing->spam, 3 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9407 | 0.9330 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9796 | 0.9897 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9777 | 0.9887 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.8964 | 0.9454 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9498 | 0.9743 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9936 | 0.9968 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 10.06 ms avg / 8.92 ms p50 over 100 emails (0.14x). CPU fp32 (16 threads): 354.05 ms avg / 338.14 ms p50 over 100 emails (4.78x). | |
| **Length confound (test).** FN mean 334 tokens vs TP 280; FP 728 vs TN 855 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `reports/eval_ft-scratch`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## 21. 2026-10-09 09:59 — Evaluation on the test + test3 splits (evaluate.py run, `ft-ce`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-ce/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 285.2 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9404** (232 missed of 3,894) | | |
| | precision | 0.8610 (591 false alarms) | | |
| | F1 | 0.8990 | | |
| | accuracy | 0.9495 | | |
| | specificity | 0.9524 | | |
| | ROC-AUC | 0.9875 | | |
| | PR-AUC | 0.9626 | | |
| | recall @ 1% FPR | 0.7745 | | |
| | recall @ 0.1% FPR | 0.3529 | | |
| | best-F1 threshold | 0.821 (F1 0.9090) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.8996 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,662 flagged true positives the model called 1,294 phishing and 2,368 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 239.6 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9853** (18 missed of 1,221) | | |
| | precision | 0.9877 (15 false alarms) | | |
| | F1 | 0.9865 | | |
| | accuracy | 0.9898 | | |
| | specificity | 0.9925 | | |
| | ROC-AUC | 0.9992 | | |
| | PR-AUC | 0.9985 | | |
| | recall @ 1% FPR | 0.9869 | | |
| | recall @ 0.1% FPR | 0.8690 | | |
| | best-F1 threshold | 0.411 (F1 0.9869) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 0.9925, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9863, **macro-F1 0.9822**; Legitimate P 0.991 / R 0.992 / F1 0.992 (n=1,999); Phishing P 0.981 / R 0.973 / F1 0.977 (n=521); Spam P 0.977 / R 0.979 / F1 0.978 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,984/4/11; Phishing 9/507/5; Spam 9/6/685. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9894 (7 phishing->spam, 6 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9449 | 0.9464 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9905 | 0.9952 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9889 | 0.9944 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9505 | 0.9746 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9900 | 0.9950 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9973 | 0.9986 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 9.89 ms avg / 8.93 ms p50 over 100 emails (0.13x). CPU fp32 (16 threads): 513.57 ms avg / 485.83 ms p50 over 100 emails (6.94x). | |
| **Length confound (test).** FN mean 256 tokens vs TP 287; FP 763 vs TN 848 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `/home/nathank/BITS/dev/Phishing Model/reports`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## 22. 2026-10-09 10:46 — Evaluation on the test + test3 splits (evaluate.py run, `ft-focal`) | |
| **Checkpoint.** `/home/nathank/BITS/dev/Phishing Model/checkpoints/finetune/ft-focal/best` (3-class head; three-way label contract of RUN_LOG entry 6). Binary roll-up metrics use the same definitions as the binary-era runs so they stay comparable. | |
| ### test split (TREC-06 binary holdout): `test_holdout.parquet`, 16,302 rows (3,894 unwanted, 23.9%; label3 legit 12,408, unwanted 3,894). Inference 284.1 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9741** (101 missed of 3,894) | | |
| | precision | 0.8794 (520 false alarms) | | |
| | F1 | 0.9243 | | |
| | accuracy | 0.9619 | | |
| | specificity | 0.9581 | | |
| | ROC-AUC | 0.9946 | | |
| | PR-AUC | 0.9848 | | |
| | recall @ 1% FPR | 0.8955 | | |
| | recall @ 0.1% FPR | 0.6497 | | |
| | best-F1 threshold | 0.682 (F1 0.9397) | | |
| | credential-lure slice | 398 rows (61.1% labelled positive), recall 0.9959, precision 0.9758 | | |
| **Three-way:** no phishing-vs-spam ground truth on this split (no rows labelled phishing or spam: every positive is unwanted-but-unsplit (no phishing-vs-spam ground truth)); informational: of the 3,793 flagged true positives the model called 974 phishing and 2,819 spam. | |
| ### test3 split (three-way holdout): `test_holdout3.parquet`, 3,220 rows (1,221 unwanted, 37.9%; label3 legit 1,999, phish 521, spam 700). Inference 237.6 rows/s on cuda (bf16 autocast, batch 64, length-bucketed), peak VRAM 2.27 GB. | |
| | binary roll-up metric (P(unwanted) = P(phishing) + P(spam)) | value | | |
| |---|---:| | |
| | **recall (unwanted)** | **0.9836** (20 missed of 1,221) | | |
| | precision | 0.9893 (13 false alarms) | | |
| | F1 | 0.9864 | | |
| | accuracy | 0.9898 | | |
| | specificity | 0.9935 | | |
| | ROC-AUC | 0.9993 | | |
| | PR-AUC | 0.9989 | | |
| | recall @ 1% FPR | 0.9869 | | |
| | recall @ 0.1% FPR | 0.8788 | | |
| | best-F1 threshold | 0.475 (F1 0.9877) | | |
| | credential-lure slice | 275 rows (97.5% labelled positive), recall 1.0000, precision 1.0000 | | |
| **Three-way** on the 3,220 rows with label3 in {0,1,2} (0 unsplit rows enter the roll-up only; rule: rollup_then_argmax): accuracy 0.9885, **macro-F1 0.9867**; Legitimate P 0.990 / R 0.993 / F1 0.992 (n=1,999); Phishing P 0.996 / R 0.987 / F1 0.991 (n=521); Spam P 0.979 / R 0.976 / F1 0.977 (n=700). 3x3 (rows true, cols predicted Legitimate/Phishing/Spam): Legitimate 1,986/1/12; Phishing 4/514/3; Spam 16/1/683. Phishing-vs-spam on the 1,221 true unwanted rows: kind accuracy 0.9959 (4 phishing->spam, 1 spam->phishing). | |
| | source | n | Legitimate | Phishing | Spam | accuracy | macro-F1 (present classes) | | |
| |---|---:|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9534 | 0.9458 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9905 | 0.9952 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9841 | 0.9920 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9865 | 0.9932 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9866 | 0.9933 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9982 | 0.9991 | | |
| **Latency (batch 1, tokenize + forward + softmax, timeit.default_timer) vs the instructor's 74 ms.** GPU bf16: 10.08 ms avg / 8.9 ms p50 over 100 emails (0.14x). CPU fp32 (16 threads): 458.57 ms avg / 445.01 ms p50 over 100 emails (6.20x). | |
| **Length confound (test).** FN mean 322 tokens vs TP 284; FP 1237 vs TN 827 (see error_analysis.md for the per-bucket table). | |
| **Artifacts.** `/home/nathank/BITS/dev/Phishing Model/reports`: metrics.json, latency.json, confusion_matrix.png, roc_curve.png, pr_curve.png, error_analysis.md; metrics_test3.json, latency_test3.json, confusion_matrix_test3.png, confusion_matrix3_test3.png, roc_curve_test3.png, pr_curve_test3.png, error_analysis_test3.md (training curves: reports/pretrain_curves.png and finetune_curves.png from pretrain.py / finetune.py `plot`). | |
| ## 23. 2026-10-12 11:30 — Fine-tune queue results, model selection (`ft-focal`), export | |
| **Queue** (`scripts/run_experiments.sh`, tmux `phish-finetune`): finished 2026-10-09 10:02:22, every step exit 0 | |
| (`ft-ce` 03:36-04:31, `ft-weighted` -05:57, `ft-focal` -07:24, `ft-synthetic` -08:19, `ft-12L` -08:56, | |
| `ft-scratch` -09:59, then top-level evaluate + export of the pre-designated main run `ft-ce`). Log | |
| `logs/experiments_20261008-141528.out`. Per-run evaluations in `reports/eval_<run>/` (entries 15-20, 21). | |
| **Selection rule and result.** The queue's main run was `ft-ce` by default, but the rule stated in README section 9 | |
| was "if another run wins on val F1 of the unwanted roll-up, it becomes the main run". `ft-focal` (focal loss, | |
| gamma 2.0) won on the validation set on both selection metrics: val F1 unwanted 0.9951 vs 0.9929 (`ft-ce`), val | |
| three-way macro-F1 0.9869 vs 0.9782; it also had the best val phishing precision/recall (0.985 / 0.988). The | |
| decision was taken from the validation numbers; the holdouts were evaluated once per run for reporting and | |
| confirm it (TREC-06 F1 0.9243 vs 0.8990, recall at 1% FPR 0.8955 vs 0.7745; test3 macro-F1 0.9867 vs 0.9822). | |
| Top-level evaluate was re-run for `ft-focal` on 2026-10-09 10:46 (entry 22, `reports/metrics*.json`, | |
| `latency.json`, plots, `error_analysis*.md`). | |
| **Held-out results of the deployed run `ft-focal`:** | |
| | split | numbers | | |
| |---|---| | |
| | TREC-06 (16,302 rows, 23.9% unwanted, whole corpus unseen) | recall 0.9741 (101 missed), precision 0.8794 (520 false alarms, FPR 4.2%), F1 0.9243, accuracy 0.9619, ROC-AUC 0.9946, PR-AUC 0.9848, recall at 1% / 0.1% FPR 0.8955 / 0.6497 | | |
| | test_holdout3 (3,220 rows: 1,999 / 521 / 700) | macro-F1 0.9867, accuracy 0.9885; F1 legit / phishing / spam 0.9918 / 0.9913 / 0.9771; binary roll-up F1 0.9864, ROC-AUC 0.9993; phishing-vs-spam on true unwanted 0.9959 (4 phishing->spam, 1 spam->phishing); 3x3 [[1986, 1, 12], [4, 514, 3], [16, 1, 683]] | | |
| | latency, batch 1, 100 TREC-06 emails (mean 280 tokens) | GPU bf16 10.08 ms mean / 8.90 p50 / 13.72 p95 (0.14x the 74 ms benchmark); CPU fp32 16 threads 458.6 ms mean / 445.0 p50 / 701.1 p95 (6.20x) | | |
| **What the ablations say** (all six on the same holdouts, README 9.1): class-weighted CE trades precision for | |
| recall (TREC-06 recall 0.982 but FPR 6.3%); focal wins on every metric; the 2,000-row synthetic supplement | |
| changes nothing measurable (TREC-06 F1 0.8995 vs 0.8990; val macro-F1 lower, because its 1,000 phishing lures | |
| are template-generated); 12 layers lose ~5 F1 points on TREC-06 (0.8739) for ~2x faster inference; scratch | |
| init loses 8 points (0.8400) and 1.7 ROC-AUC points, which is what the 15 h of MLM pretraining buys. | |
| **Challenge recorded: TREC-06 labelling conventions.** The TREC-06 numbers are the honest, cross-corpus ones and | |
| sit well below the in-distribution validation numbers (F1 0.92 vs 0.995). `reports/error_analysis.md` shows the | |
| two causes: (1) TREC-06 labels as *ham* a lot of mail that looks like spam to any text model (spam quoted inside | |
| mailing-list discussions, "please remove me from your list" replies, sweepstakes mail, "57 Million Email | |
| Addresses - ONLY $149!!" on a developer list), which accounts for the most confident false positives; (2) its | |
| largest block of *spam* is "ScanMail Message: virus found" gateway notices, which every run misses. Legitimate | |
| TREC-06 mail is also ~3x longer than its spam and 24% of it is truncated at 512 tokens; false positives are | |
| truncated 31.5% of the time vs 23.6% for true negatives. Neither is a leakage problem, but both cap what any | |
| text-only classifier can score on this corpus, and they are the right thing to show in the presentation when | |
| explaining the gap between 0.995 validation and 0.92 TREC-06. | |
| **Export.** 2026-10-12 11:28: `export.py run --checkpoint checkpoints/finetune/ft-focal/best --out-dir | |
| model_export/ft-focal --app-dir app` -> `model_export/ft-focal/` (bf16 safetensors 674.9 MB, config.json with | |
| labels + metrics + dials, tokenizer.json, model-card README.md, export_manifest.json) and the same three files | |
| into `app/`; `app/model.py` is byte-identical to `src/model.py` (sha1 39b3773c, asserted). This replaced the | |
| `ft-ce` weights that the queue had copied into `app/` on 10-09 10:02; `model_export/ft-ce/` is kept for | |
| comparison. README (sections 2.1, 2.2, 5.6-5.8, 9, 10, 11), `app/README.md` (Space card: bundled weights, | |
| training recipe, real model-card link) and `scripts/build_notebook.py` were updated to name `ft-focal` as the | |
| deployed run; the notebook was rebuilt. | |
| **Gap in the record.** The 2026-10-09 10:43 scheduled check-in completed the queue review and the `ft-focal` | |
| top-level evaluation (entry 22) but stalled before export/upload; resumed on 2026-10-12 11:27 at Nathan's | |
| request. `find -newermt` confirmed nothing in the project changed in between. | |
| ## 24. 2026-10-12 11:35 — Hugging Face upload (private model repo + private ZeroGPU Space) | |
| Approved by Nathan on 2026-10-08 19:15 ("can you upload the hugging face?"); deferred until an export existed. | |
| Account: `BuildITSystems` via the `hf auth login` cache (token never printed or written anywhere). | |
| **Dry run** 11:30 (`scripts/upload_to_hf.py --model-dir model_export/ft-focal --bundle-weights`): no problems; | |
| plan = 4 files / 645.9 MiB to the model repo, 8 files / 645.9 MiB to the Space (weights bundled, so no | |
| `HF_TOKEN` secret), hardware `zero-a10g`, both private. | |
| **Live run 1** 11:31 (`--yes`): `create_repo` + `upload_folder` for the model repo succeeded (commit | |
| `80bd85c5`: README.md model card, config.json, model.safetensors 674.9 MB bf16, tokenizer.json). The Space was | |
| created and its `PHISH_MODEL_REPO` variable set, but `upload_folder(app/)` was refused by the Hub's README | |
| validation: `Invalid YAML in README.md: bad indentation of a mapping entry (11:45)` — the unquoted | |
| `short_description: From-scratch 337M encoder: legitimate / phishing / spam` contains a colon. The dry run does | |
| not validate the card's YAML, so this was invisible until the live call. Exit 1. | |
| **Fix** 11:34: quoted the value in `app/README.md` (55 chars, under the 60-char limit); verified with | |
| `yaml.safe_load` and `huggingface_hub.RepoCard`. Lesson added to README 5.9. | |
| **Live run 2** 11:35 (`--yes` again, idempotent): model-repo files already present (LFS content dedupe; commit | |
| unchanged), Space `upload_folder` succeeded (commit `98f90d7b`). Verified read-only afterwards: | |
| | repo | private | files | | |
| |---|---|---| | |
| | https://huggingface.co/BuildITSystems/phishing-detector-encoder | yes | .gitattributes, README.md, config.json, model.safetensors 674.9 MB, tokenizer.json 2.3 MB | | |
| | https://huggingface.co/spaces/BuildITSystems/phishing-detector-demo | yes | .gitattributes, README.md (card), app.py, model.py, requirements.txt, config.json, model.safetensors 674.9 MB, tokenizer.json 2.3 MB | | |
| Space runtime at 11:36: stage `BUILDING`, requested hardware `zero-a10g`; variable | |
| `PHISH_MODEL_REPO=BuildITSystems/phishing-detector-encoder`; no secrets. Deployed weights = `ft-focal` | |
| (entry 23). Before class Nathan flips both repos to public (README section 10 one-liner) and opens the Space | |
| once so ZeroGPU is warm. | |
| **Build and live check (2026-10-12 11:37-11:39).** Space stages: BUILDING 11:35 -> APP_STARTING 11:36 -> RUNNING | |
| on `zero-a10g` 11:37 (about 2 minutes end to end). Verified through the private Space's Gradio API | |
| (`gradio_client`, endpoint `/predict(subject, body)`, token from the login cache): a sample credential-lure | |
| email ("verify your account within 24 hours", lookalike PayPal link) returned verdict **Phishing** 95.6%, | |
| Spam 3.5%, Legitimate 0.9%, roll-up Unwanted 99.1%, in-app "Prediction Time (s)" 0.253 (cold ZeroGPU first | |
| call), round-trip 3.6 s, with the token highlights and the rule-based signals populated. The deployment is | |
| working end to end. | |
| ## 25. 2026-10-12 11:58 — Presentation bundle on the Hub + Colab variant of the notebook | |
| **Why.** Nathan will present from Google Colab, where none of the project files exist and the local GPU is | |
| irrelevant. Design: the notebook is narrative only; every prediction runs on the ZeroGPU Space | |
| (`gr.load(..., src="spaces")` to embed the Space UI, `gradio_client` for the timing loop), and the evidence the | |
| notebook renders (metrics, curves, experiments table, data report, run log, code) is fetched from the model repo. | |
| **Bundle upload (approved by Nathan 2026-10-12 11:50).** `presentation/` in the private model repo | |
| `BuildITSystems/phishing-detector-encoder`, commit `439e7b1a`: 85 files, 3.6 MB, relative paths mirrored from | |
| the project (`reports/` JSON/PNG/MD incl. `eval_<run>/`, `checkpoints/pretrain/large-24L/{run_summary.json, | |
| metrics.csv,final/config.json}`, `checkpoints/finetune/<run>/{metrics.csv,epochs.csv}`, | |
| `data/processed/{splits_manifest,tokenizer_report,pretrain_tokens}.json` + `data_report.md`, | |
| `docs/research/research_model-math.md`, `src/*.py`, `app/{app.py,model.py,requirements.txt,README.md}`, | |
| `config.yaml`, `requirements-train.txt`, `scripts/*`, `model_export/ft-focal/{config.json,README.md, | |
| export_manifest.json}`, `RUN_LOG.md`, `README.md`). Excluded on purpose: every parquet/npy/safetensors/log. | |
| Scanned before upload for tokens (`hf_...`), `HF_TOKEN=` values, Nathan's e-mail address, private keys and | |
| cloud keys: clean. The quoted emails in `error_analysis*.md` are rows of public corpora (TREC-06 etc.), as the | |
| files themselves state. The Hub warned that `presentation/README.md` has no YAML front matter (it is the | |
| project README, not a model card): harmless. | |
| **Colab notebook build (workflow wf_4200da59, 8 agents, 53 min, 12:00-12:53).** `scripts/build_notebook.py` | |
| gained `--colab` and `--out PATH`; cells are tagged `local` / `colab` / shared so both notebooks come from one | |
| source (default mode proven byte-identical in cell sources against the executed deliverable, which was never | |
| rewritten: `cmp` identical before and after). The Colab variant (49 cells, 36 code) keeps the nine instructor | |
| headers and the story; Steps 2-4 and the Results read the Hub bundle; Step 5 calls the Space through | |
| `gradio_client`; Step 7 embeds the Space with `gr.load`; Step 8 logs in only when the token is not already | |
| active (huggingface_hub 1.33 `login(token=...)` otherwise rewrites the token files unconditionally); Step 9 | |
| quotes entry 24. Two adversarial reviews found 6 items, all fixed: per-call error handling around the Space | |
| calls (one ZeroGPU abort must not kill the cell), the `login()` behaviour above, `analytics_enabled=False` on | |
| the client (the env var alone does not disable gradio_client telemetry), a note that `gr.load` forwards the token | |
| to the Space (use a fine-grained read token as the Colab secret), the login-cache fallback made opt-in outside | |
| Colab (`NOTEBOOK_ALLOW_LOGIN_CACHE=1`), and silencing the doomed first `gr.load` attempt. | |
| **Challenge recorded: `gr.load` bug in gradio 6.29.1.** `gr.load("BuildITSystems/phishing-detector-demo", | |
| src="spaces")` raises `TypeError: Textbox.__init__() got an unexpected keyword argument 'class_id'`: | |
| `Blocks.from_config` rebuilds the Interface's examples `Dataset`, whose serialised child components are | |
| `{"name", "class_id"}` dicts, and `gradio.components.base.get_component_instance` pops only `name`. Any Space | |
| whose `gr.Interface` has `examples=` triggers it. Reproduced independently on the workstation. Fix in the | |
| notebook: catch the error, install an idempotent shim that drops `class_id`, retry `gr.load`; keep a hand-built | |
| proxy `gr.Interface` over `gradio_client` as the last fallback. Verified: `gr.load` succeeds after the shim. | |
| **Headless Colab-mimic test (clean uv venv, CPython 3.12.14, torch 2.14.1+cpu, gradio 6.29.1, gradio_client | |
| 2.7.2, huggingface_hub 1.33.0, executed from an empty scratch directory, `HF_TOKEN` unset, token only from the | |
| login cache via the opt-in flag).** Round 1 ran 36/36 cells with no errors but `gr.load` had fallen back to the | |
| proxy (flagged as a failure by the harness); after the shim fix round 2 passed every check: 36/36 cells, 0 | |
| errors, 18 s wall, bundle 86 files / 3.4 MiB, six live Space predictions (in-app times 0.229 s cold then 0.009- | |
| 0.011 s; e.g. "Microsoft 365 password expires" -> Phishing 93.7%, "FINAL HOURS: 70% OFF" -> Spam 99.0%, "Agenda | |
| for Thursday" -> Legitimate 98.6%), `gr.load` served and closed, whoami = BuildITSystems, zero token-shaped | |
| strings in outputs or raw file. Final verification: TREC-06 F1 0.9243 / recall 0.9741 / ROC-AUC 0.9946 and the | |
| binary confusion 11888/520/101/3793, three-way macro-F1 0.9867 and the 3x3 matrix all match `reports/`. | |
| Presenter notes, non-blocking: the iframe cell prints "needs a public Space" until the Space is public; the | |
| `gr.load` cell shows the shim trace line; some outputs echo workstation absolute paths that live inside the | |
| mirrored JSON files. | |
| **gradio 6.30.0 checked (released after the pin):** `gr.load` fails identically (`class_id`), tested in a | |
| throwaway venv against the Space, so bumping the pin would not remove the shim. Keep 6.29.1 (= the Space's | |
| `sdk_version`) in the Colab notebook until a release fixes `get_component_instance`. | |
| **Bundle refresh 2026-10-12 13:05:** re-uploaded `presentation/RUN_LOG.md`, `presentation/README.md` and | |
| `presentation/scripts/build_notebook.py` (all changed by this entry) and added | |
| `presentation/FinalProject_PhishingDetector_Colab.ipynb` so the notebook can be downloaded from the model repo | |
| and uploaded to Colab in one step. Same approval as the bundle; same scan; nothing else changed on the Hub. | |