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
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_Cleanedhas no body text (subjects/headers + 53 numeric features). Unusable for a text model. Excluded.zefang-liu/phishing-email-datasetis a lowercased re-packaging already 91.9% contained inpuyang2025/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-emailspretraining corpus. Neither can be the test set. corbt/enron-emailsis 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", 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.DataLoaders 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 " 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 1 h each + evaluation) -> ~11:00-11:30.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 (
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.