|
Download README.md from BuildITSystems/phishing-detector-encoder: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/BuildITSystems/phishing-detector-encoder/resolve/main/README.md
- Command line
-
hf download hf://BuildITSystems/phishing-detector-encoder/README.md
-
curl -L -o README.md https://huggingface.co/BuildITSystems/phishing-detector-encoder/resolve/main/README.md
14.1 kB
| datasets: | |
| - puyang2025/seven-phishing-email-datasets | |
| - corbt/enron-emails | |
| - kudzaiprichard/aura-phishing-email-corpus | |
| language: en | |
| library_name: pytorch | |
| license: mit | |
| pipeline_tag: text-classification | |
| tags: | |
| - phishing | |
| - email-security | |
| - text-classification | |
| - from-scratch | |
| - spam-detection | |
| model-index: | |
| - name: phishing-detector-encoder | |
| results: | |
| - task: | |
| type: text-classification | |
| dataset: | |
| name: TREC-06 holdout (puyang2025), binary roll-up | |
| type: puyang2025/seven-phishing-email-datasets | |
| metrics: | |
| - type: recall | |
| value: 0.9741 | |
| name: Recall (unwanted) | |
| - type: precision | |
| value: 0.8794 | |
| name: Precision (unwanted) | |
| - type: f1 | |
| value: 0.9243 | |
| name: F1 (unwanted) | |
| - type: accuracy | |
| value: 0.9619 | |
| name: Accuracy | |
| - type: roc_auc | |
| value: 0.9946 | |
| name: ROC-AUC | |
| - task: | |
| type: text-classification | |
| dataset: | |
| name: three-way holdout (Nazario + Nigerian Fraud phishing, SpamAssassin / Enron-Spam | |
| spam, CEAS-08 / TREC-07 / SpamAssassin ham) | |
| type: kudzaiprichard/aura-phishing-email-corpus | |
| metrics: | |
| - type: f1 | |
| value: 0.9867 | |
| name: Macro-F1 (legitimate / phishing / spam) | |
| - type: accuracy | |
| value: 0.9885 | |
| name: Accuracy (3-way) | |
| # phishing-detector-encoder: legitimate / phishing / spam email classifier trained from scratch | |
| > **No external pretrained weights were used.** The encoder, the tokenizer and every parameter in `model.safetensors` | |
| > were trained from random initialisation inside this project, in plain PyTorch, without the `transformers` | |
| > library. The only external inputs are the public email corpora listed below. | |
| > Initialisation of this checkpoint: `pretrained:/home/nathank/BITS/dev/Phishing Model/checkpoints/pretrain/large-24L/final`. | |
| ## What it is | |
| A 337,444,867-parameter BERT-style **pre-LayerNorm Transformer encoder** (24 layers, d_model 1024, | |
| 16 heads, FFN 4096, byte-level BPE vocabulary of 32,768, 512 positions) with a | |
| CLS-pooled classification head that maps an email (`subject + "\n\n" + body`) to three classes: `0 = Legitimate`, `1 = Phishing`, `2 = Spam`. The binary verdict shown by the demo is the roll-up **Unwanted (phishing or spam)** = P(phishing) + P(spam) (`config.json["binary_rollup"]`). Phishing = Nazario phishing corpus + Nigerian-fraud (advance-fee) corpus + synthetic BITS-style credential/BEC lures; Spam = SpamAssassin and Enron-Spam spam folders; the TREC-05 / TREC-07 / CEAS-08 positives never separated spam from phishing and were trained as 'unwanted' only (probability mass of phishing + spam), never as either class. | |
| The architecture is defined in `model.py`, which ships with the weights. Weights are stored in **bf16** safetensors | |
| (~675 MB); the header carries the architecture config and the label names as JSON. | |
| ## How it was trained | |
| 1. **Masked-language-model pretraining from scratch** on the deduplicated Enron email corpus (`corbt/enron-emails`: 517,401 rows -> 226,273 documents after exact/near-duplicate removal and quote stripping), 15% dynamic masking (80/10/10), sequence packing to 512 tokens, AdamW (betas (0.9, 0.98), weight decay 0.01), peak LR 0.00015, bf16 autocast on one RTX 3090 Ti. Run summary (from the pretraining run's `final/config.json`): | |
| - run_name: large-24L | |
| - status: complete | |
| - source_checkpoint: step-0009552 | |
| - steps: 9552 | |
| - total_steps_planned: 9552 | |
| - steps_per_epoch: 796 | |
| - epochs_planned: 12 | |
| - epochs_completed: 12.0 | |
| - tokens_seen: 1291124736 | |
| - total_training_seconds: 54185.9 | |
| - total_training_time: 15h 03m 05s | |
| - final_loss: 1.523698394948786 | |
| - val_loss: 1.9862932266429436 | |
| - val_masked_acc: 0.6291136684837473 | |
| - peak_vram_reserved_gb: 17.939 | |
| - peak_vram_allocated_gb: 17.494 | |
| - micro_batch: 24 | |
| - grad_accumulation: 11 | |
| - effective_batch: 264 | |
| - precision: bf16_autocast | |
| - optimizer: AdamW(fused) {"lr": 0.00015, "betas": [0.9, 0.98], "eps": 1e-06, "weight_decay": 0.01, "grad_clip": 1.0, "mlm_probability": 0.15, "mask_replace_probs": [0.8, 0.1, 0.1], "schedule": "cosine_to_10pct", "effective_batch_sequences": 256} | |
| - schedule: {'kind': 'cosine', 'peak_lr': 0.00015, 'warmup_steps': 573, 'total_steps': 9552, 'floor_ratio': 0.1} | |
| - mean_tokens_per_s: 23889 | |
| - data: {'n_windows': 210368, 'n_val_windows': 1999, 'tokenizer_sha1': '9f50c5e775089d2ca3efc82e100cd6b3396ed7e7', 'n_docs': 226273, 'limit_docs': None, 'window': '[CLS] + 511 stream tokens'} | |
| - weights_dtype: float32 | |
| - exported_at: 2026-10-09T03:31:14 | |
| 2. **Fine-tuning** on `puyang2025/seven-phishing-email-datasets` with our own split: TREC-05, TREC-07 and CEAS-08 (positives trained as *unwanted* only), Enron-Spam and SpamAssassin (spam folders = `Spam`), the Nazario and Nigerian-fraud corpora of `kudzaiprichard/aura-phishing-email-corpus` (= `Phishing`) and synthetic BITS-style lures (= `Phishing`) form train (157,113) / val (17,643), grouped by | |
| near-duplicate cluster so no template straddles the two; **TREC-06 (16,302 rows, 23.9% positive) is held out | |
| entirely** as the unseen binary test set and never touched by the tokenizer, pretraining or fine-tuning (1,652 pool rows that | |
| near-duplicate TREC-06 templates were removed). A second holdout, **test_holdout3**, holds clean-provenance three-way rows never seen by the tokenizer or the pretraining corpus for the 3x3 confusion matrix. Counts come from `data/processed/splits_manifest.json`. | |
| Rows with a clean three-way label train with 3-class cross-entropy; TREC / CEAS positives train on the probability mass of {phishing, spam} (partial-label loss: -log(P(phishing) + P(spam))), so they shape the legitimate-vs-unwanted decision without asserting a kind they were never labelled with. | |
| Fine-tune dials (from the checkpoint's config.json): | |
| - run_name: ft-focal | |
| - init: pretrained:checkpoints/pretrain/large-24L/final | |
| - n_layers: 24 | |
| - freeze_layers: 0 | |
| - loss: focal | |
| - focal_gamma: 2.0 | |
| - lr: 3e-05 | |
| - batch_size: 32 | |
| - micro_batch: 16 | |
| - epochs: 2 | |
| - max_steps: None | |
| - eval_every: 1250 | |
| - log_every: 20 | |
| - eval_batch_size: 64 | |
| - warmup_fraction: 0.06 | |
| - weight_decay: 0.01 | |
| - betas: [0.9, 0.98] | |
| - eps: 1e-06 | |
| - grad_clip: 1.0 | |
| - patience: 2 | |
| - schedule: linear_decay | |
| - selection_metric: val_f1_unwanted | |
| - pooling: cls | |
| - num_labels: 3 | |
| - synthetic: off | |
| - limit_rows: None | |
| - seed: 42 | |
| - cpu: False | |
| ## Metrics | |
| ### TREC-06 binary holdout (Unwanted (phishing or spam)) | |
| Evaluated by `src/evaluate.py` on the **test** split (`test_holdout.parquet`, 16,302 rows, threshold 0.5). Positive class = **Unwanted (phishing or spam)**, P(unwanted) = P(phishing) + P(spam); these definitions are identical to the binary-era runs, so the numbers are comparable. | |
| | metric | value | | |
| |---|---:| | |
| | **Recall (unwanted)** | **0.9741** (101 missed of 3,894) | | |
| | Precision (unwanted) | 0.8794 | | |
| | F1 (unwanted) | 0.9243 | | |
| | Accuracy | 0.9619 | | |
| | Specificity | 0.9581 | | |
| | ROC-AUC | 0.9946 | | |
| | PR-AUC | 0.9848 | | |
| | Recall at 1% FPR | 0.8955 | | |
| | Recall at 0.1% FPR | 0.6497 | | |
| | Credential-lure slice recall / precision (n=398) | 0.9959 / 0.9758 | | |
| | Latency, batch 1, GPU bf16 (avg / p50) | 10.08 ms / 8.9 ms | | |
| | Latency, batch 1, CPU fp32, 16 threads (avg / p50) | 458.57 ms / 445.01 ms | | |
| | Instructor benchmark | 74.0 ms per sample | | |
| Recall on the unwanted class is the headline number: in a defensive triage aid a missed phish is the costly error, while a false positive costs a human one glance. | |
| TREC-06 carries no phishing-vs-spam ground truth, so the three-way head can only be *described* here: of the 3,793 true positives it flagged, the model called 974 phishing and 2,819 spam (informational, not a metric). | |
| ### Three-way holdout (legitimate / phishing / spam) | |
| Evaluated by `src/evaluate.py --split test3` on `test_holdout3.parquet` (3,220 rows with a full three-way label, threshold 0.5). Decision rule `rollup_then_argmax`: a row is unwanted iff P(phishing) + P(spam) >= threshold, then the kind is argmax(P(phishing), P(spam)), so this table collapses exactly onto the binary roll-up. | |
| | metric | value | | |
| |---|---:| | |
| | **Macro-F1 (3 classes)** | **0.9867** | | |
| | Accuracy | 0.9885 | | |
| | Legitimate: precision / recall / F1 (n=1,999) | 0.9900 / 0.9935 / 0.9918 | | |
| | Phishing: precision / recall / F1 (n=521) | 0.9961 / 0.9866 / 0.9913 | | |
| | Spam: precision / recall / F1 (n=700) | 0.9785 / 0.9757 / 0.9771 | | |
| | Phishing-vs-spam accuracy on the 1,221 true unwanted rows (kind decision alone) | 0.9959 (4 phishing read as spam, 1 spam read as phishing) | | |
| | Binary roll-up on this split: recall / precision / F1 | 0.9836 / 0.9893 / 0.9864 | | |
| Confusion matrix (rows = true label by corpus provenance, columns = predicted): | |
| | true \ predicted | Legitimate | Phishing | Spam | | |
| |---|---:|---:|---:| | |
| | **Legitimate** | 1,986 | 1 | 12 | | |
| | **Phishing** | 4 | 514 | 3 | | |
| | **Spam** | 16 | 1 | 683 | | |
| | source | rows | Legitimate | Phishing | Spam | accuracy | | |
| |---|---:|---:|---:|---:|---:| | |
| | Assassin | 236 | 164 | 0 | 72 | 0.9534 | | |
| | CEAS-08 | 736 | 736 | 0 | 0 | 0.9905 | | |
| | Enron | 628 | 0 | 0 | 628 | 0.9841 | | |
| | Nazario | 222 | 0 | 222 | 0 | 0.9865 | | |
| | Nigerian_Fraud | 299 | 0 | 299 | 0 | 0.9866 | | |
| | TREC-07 | 1,099 | 1,099 | 0 | 0 | 0.9982 | | |
| **Label provenance.** Phishing = Nazario phishing corpus + Nigerian-fraud (advance-fee) corpus + synthetic BITS-style credential/BEC lures; Spam = SpamAssassin and Enron-Spam spam folders; the TREC-05 / TREC-07 / CEAS-08 positives never separated spam from phishing and were trained as 'unwanted' only (probability mass of phishing + spam), never as either class. The three-way holdout is drawn from text the tokenizer never saw and the pretraining corpus does not contain (Nazario / Nigerian-fraud phishing, SpamAssassin / Enron-Spam spam, CEAS-08 / TREC-07 / SpamAssassin legitimate mail from the former validation split). Because the kind labels come from which corpus a message sits in, part of any phishing-vs-spam confusion measures corpus style rather than lure content. | |
| ## Intended use | |
| A **defensive triage aid for small-business inboxes**: rank or flag incoming mail for a human to review, surface a | |
| probability and the attention-highlighted tokens that drove it, and shorten the time between a phishing wave | |
| landing and someone noticing. It is a classroom/final-project model and is not a substitute for a mail gateway. | |
| ## Limitations | |
| - **Era**: the labelled data is 1998-2008 spam-track mail (TREC, CEAS, SpamAssassin, Enron), plus Nazario phishing (1995-2022) and Nigerian-fraud mail. | |
| Modern lures (OAuth consent phishing, QR codes, MFA fatigue, SaaS-brand impersonation) are under-represented; expect lower recall on them. | |
| - **Labels follow corpus provenance**: Phishing = Nazario phishing corpus + Nigerian-fraud (advance-fee) corpus + synthetic BITS-style credential/BEC lures; Spam = SpamAssassin and Enron-Spam spam folders; the TREC-05 / TREC-07 / CEAS-08 positives never separated spam from phishing and were trained as 'unwanted' only (probability mass of phishing + spam), never as either class. | |
| A phishing-vs-spam confusion therefore partly measures corpus style, and the credential-lure slice remains the closest proxy for phishing-proper recall. | |
| - **Length confound**: legitimate emails in these corpora average ~2x the tokens of positives. Inputs are truncated to | |
| 512 tokens, and the error analysis checks whether errors follow length, but a short legitimate note can | |
| still look suspicious. | |
| - **English only**; byte-level BPE will tokenize other scripts but the model has not seen them. | |
| - **Headers are not used** (TREC rewrote them); the model sees subject and body text only. Sender reputation, | |
| SPF/DKIM and URL reputation are out of scope and should be combined with this score in any real deployment. | |
| - Attention highlights are a signal of what the model attended to, not an explanation of why it decided. | |
| ## How to load | |
| `model.py` from this repository is the only code dependency (plain PyTorch). | |
| ```python | |
| import json, torch | |
| from safetensors.torch import load_file | |
| from tokenizers import Tokenizer | |
| from model import EncoderConfig, EncoderForClassification # model.py, shipped with the weights | |
| cfg = json.load(open("config.json")) | |
| model = EncoderForClassification(EncoderConfig.from_dict(cfg["model"])) # num_labels = 3 | |
| model.load_state_dict(load_file("model.safetensors"), strict=True) # bf16 weights cast into the fp32 module | |
| model.eval() | |
| tok = Tokenizer.from_file("tokenizer.json") | |
| tok.enable_truncation(max_length=cfg["max_seq_len"]) | |
| enc = tok.encode("Your account has been suspended\n\nClick here to verify your identity within 24 hours.") | |
| ids = torch.tensor([enc.ids]) | |
| mask = torch.ones_like(ids, dtype=torch.bool) | |
| with torch.inference_mode(): | |
| probs = model(ids, mask).float().softmax(-1)[0] | |
| p_unwanted = sum(float(probs[i]) for i in cfg["binary_rollup"]["unwanted"]) # P(phishing) + P(spam) | |
| print("unwanted" if p_unwanted >= 0.5 else "legitimate", {cfg["labels"][str(i)]: round(float(p), 4) for i, p in enumerate(probs)}) | |
| ``` | |
| On a GPU wrap the forward in `torch.autocast("cuda", dtype=torch.bfloat16)`. Measured single-email latency (batch 1, tokenize + forward + softmax, see the metrics table): GPU bf16 10.08 ms avg on NVIDIA GeForce RTX 3090 Ti, CPU fp32 (16 threads) 458.57 ms avg. | |
| ## Files | |
| - `model.safetensors` — bf16 weights, metadata `format=pt, dtype=bf16, architecture=EncoderForClassification, config=<json>, labels=<json>` | |
| - `config.json` — model config, num_labels, labels, binary_rollup, label provenance, max_seq_len, export dtype, source checkpoint, fine-tune init, binary metrics (TREC-06), three-way metrics (test_holdout3), pretraining summary (null for scratch init) | |
| - `tokenizer.json` — byte-level BPE, NFKC, case preserved, specials `[PAD]=0 [UNK]=1 [CLS]=2 [SEP]=3 [MASK]=4` | |
| - `README.md` — this card | |
| ## Provenance | |
| Built for the Skillspire AI & Machine Learning course final project (BITS, 2026). Data decisions, measured VRAM / | |
| throughput / latency and every challenge hit along the way are recorded in the project's RUN_LOG.md. | |