BuildITSystems's picture
Add presentation bundle (reports, curves, run log, code) for the Colab notebook
439e7b1 verified
Raw History Blame Contribute Delete
12.6 kB
# BITS Phishing Detector — all dials in one place.
# Verified pins and the reasoning behind every number: docs/research/*.md (2026-10-07).
seed: 42
paths:
data_raw: data/raw
data_processed: data/processed
tokenizer: data/processed/tokenizer.json
checkpoints: checkpoints
reports: reports
logs: logs
app_dir: app
# ---------------------------------------------------------------- model (Step 3)
model:
# "Large" BERT-style pre-LN encoder. 24L/1024/16/4096/32768/512 = 337,476,608 params with the
# tied MLM head, 337,443,842 as the deployed classifier (+2.3% over the ~330M target).
# Switch n_layers to 23 (324,880,384) only if ~330M must be a hard ceiling.
n_layers: 24
d_model: 1024
n_heads: 16 # head_dim 64 -> Flash-SDPA eligible on sm_86
d_ff: 4096
vocab_size: 32768 # 2^15, tensor-core aligned
max_seq_len: 512
dropout: 0.1
attention_dropout: 0.1
norm: pre_ln
activation: gelu
tie_mlm_head: true
grad_checkpointing: false # B=16 fits without it; checkpointing costs ~1.33x compute
# ---------------------------------------------------------------- tokenizer
tokenizer:
type: byte_level_bpe
vocab_size: 32768
min_frequency: 2
special_tokens: ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]
# Trained ONLY on the pretraining corpus + fine-tune train split. Never on val/test.
train_on: [pretrain, finetune_train]
# ---------------------------------------------------------------- data (Step 2 / data_prep)
data:
labelled_primary: puyang2025/seven-phishing-email-datasets # 203,017 rows; text/subject/label/dataset_name
labelled_excluded:
- it4lia/PhishingEmailCuratedDatasets_Cleaned # no body text (subjects + 53 numeric features only)
- zefang-liu/phishing-email-dataset # 91.9% exact subset of the primary set, lossy text
pretrain_corpus: corbt/enron-emails # 517,401 rows -> ~244k after exact dedup
holdout_test_source: TREC-06 # never seen in tokenizer, pretraining, or fine-tuning
second_choice_holdout: CEAS-08 # only after stripping mailing-list footers
# Positive class = label 1 of the primary set ("phishing or spam / unwanted"). All label-1 rows
# come from spam-track corpora with no spam-vs-phishing distinction; mapping spam->0 would leave
# ~0 positives. PENDING Nathan's confirmation (see RUN_LOG.md entry 0).
spam_policy: positive_class
min_alnum_chars: 20 # labelled rows: min alphanumerics in subject+body
pretrain_min_alnum_chars: 50 # corbt bodies shorter than this (after quote stripping) are dropped
quote_strip_min_alnum: 200 # cut a quoted/forwarded tail only if this much text survives above it
dedup:
normalize: "NFKC; lowercase; keep [a-z0-9] only (common.normalize_text)"
keys: [full_sha1, prefix300_sha1, subject_body_prefix300_sha1, subject_body_full_sha1]
key_prefix_chars: 300
key_prefix_min_len: 80
near_dup_shingle_chars: 50
near_dup_stride: 25
near_dup_min_shared: 3
near_dup_window_chars: 1500
near_dup_shingle_cap: 2000 # shingles shared by more rows than this are boilerplate; ignored and counted
finetune_val_fraction: 0.10 # grouped by near-dup cluster, stratified by source x label
model_input: "subject + \"\\n\\n\" + body" # never sender/receiver (TREC headers are synthetic)
synthetic:
enabled: false # <= 2,000 rows, source=synthetic, training-only; results reported with/without
max_rows: 2000
# Three-way labels (RUN_LOG entry 6; data_prep.py build3 / check3). label3: 0 legitimate / 1 phishing / 2 spam /
# -1 unwanted-but-unsplit; label_set: legit / phish / spam / unwanted. The binary `label` column is unchanged.
threeway:
# Provenance of a binary-positive row -> label3. TREC-05/06/07 and CEAS-08 never separated spam from phishing
# (-1: trains on P(phishing)+P(spam)); SpamAssassin / Enron-Spam / Ling label-1 rows are spam folders (2);
# Nazario and Nigerian_Fraud are phishing corpora by construction (1). Every label-0 row is 0.
positive_label3_by_source:
Assassin: 2
Enron: 2
Ling: 2
TREC-05: -1
TREC-06: -1
TREC-07: -1
CEAS-08: -1
Nazario: 1
Nigerian_Fraud: 1
champa: # Champa et al. raw corpus mirror (CC-BY-4.0, byte-identical to Zenodo 8339691)
repo: kudzaiprichard/aura-phishing-email-corpus
files: [Nazario.csv, Nigerian_Fraud.csv]
local_dir: data/raw/aura
label3: 1
bigshp: # locuoco 300k set: research verdict DO_NOT_USE (docs/research/bigshp_research.md)
enabled: false # set true only on a USE / USE_PARTIALLY verdict; the filter chain below then applies
verdict: DO_NOT_USE
repo: locuoco/the-biggest-spam-ham-phish-email-dataset-300000
file: df.csv
local_dir: data/raw/bigshp
drop_phish_class: true # its phish label is source-derived (87% spam-folder / spam-track mail) and stopword-stripped
min_chars: 160 # SMS-like fragments
min_english_fraction: 0.7 # label-2 rows: share of word tokens in our legitimate-mail vocabulary
near_dup_token_key_len: 30 # within-file near-dup key: first 30 word tokens
new_source_test_fraction: 0.15 # Nazario + Nigerian_Fraud: cluster-grouped slice -> test_holdout3
new_source_val_fraction: 0.10 # of the remainder -> finetune_val
# A new near-dup cluster that links to BOTH train (or the MLM corpus) AND val is a 419 template the primary set
# already holds on both sides; dropping it keeps train_vs_val_shingle_links at the binary build's 0 (costs ~424
# Nigerian_Fraud rows). false = pin such clusters to train (adds ~226 info-only train/val near-dup links).
drop_new_clusters_linked_to_both_train_and_val: true
holdout3_from_val: # clean-provenance legit / spam rows moved from finetune_val into test_holdout3
legit_sources: [CEAS-08, TREC-07, Assassin]
spam_sources: [Assassin, Enron]
legit_rows: 2000
spam_rows: 700
# exclusions: keys or >=3-shingle links into the pretraining corpus, clusters also in finetune_train,
# clusters touched by a new row, clusters with an ineligible member (whole clusters move or none)
display_names: {0: Legitimate, 1: Phishing, 2: Spam}
binary_rollup_name: "Unwanted (phishing or spam)"
# ---------------------------------------------------------------- VRAM policy
vram:
target_gb_low: 14
target_gb_high: 18
hard_cap_gb: 20
alloc_conf: "expandable_segments:True"
log_peak_every_steps: 20
# ---------------------------------------------------------------- Phase 1: MLM pretraining
pretrain:
precision: bf16_autocast
sequence_packing: true # no attn_mask -> Flash SDPA backend
effective_batch_sequences: 256 # 256 x 512 = 131,072 tokens/step (BERT setting)
micro_batch: auto # batch finder picks the largest candidate whose reserved peak is in the VRAM band
micro_batch_start: 24 # Nathan's choice 2026-10-08 (17.65 GiB reserved, 25.2k tok/s); 256/24 -> accumulation 11 = effective 264
micro_batch_candidates: [16, 24, 12, 8]
epochs: 12 # Nathan's choice 2026-10-08: ~14 h at 25k tok/s, ~9,550 steps; val MLM loss watched for a rise
optimizer: adamw
lr: 1.5e-4
betas: [0.9, 0.98]
eps: 1.0e-6
weight_decay: 0.01 # excluded for biases and LayerNorm
grad_clip: 1.0
warmup_steps: 1000 # cap; actual warmup = min(warmup_steps, warmup_fraction x total steps)
warmup_fraction: 0.06 # 3 epochs (~2,460 steps) -> ~148 warmup steps; 6 epochs -> ~295
schedule: cosine_to_10pct
mlm_probability: 0.15
mask_replace_probs: [0.8, 0.1, 0.1] # [MASK] / random token / unchanged
loss_on_masked_only: true # gather masked positions before the MLM head
checkpoint_every_steps: 500
log_every_steps: 20
eval_every_steps: 500
# ---------------------------------------------------------------- Phase 2: fine-tuning (the instructor's dials)
finetune:
# Three-way head (RUN_LOG entry 6): 0 Legitimate / 1 Phishing / 2 Spam. Lives here, NOT under model:, because the
# live pretraining run refuses to resume if its model snapshot changes. 2 reproduces the binary baseline runs.
# Rows with label_set "unwanted" (TREC-05/07, CEAS-08 positives) train on P(phishing)+P(spam) (partial-label NLL).
num_labels: 3
epochs: 3
batch_size: 32 # examples per optimizer step (= micro_batch x accumulation)
micro_batch: 16 # rows per forward/backward; measured ~11.8 GiB reserved at 16 x 512
eval_batch_size: 64
lr: 3.0e-5
weight_decay: 0.01
betas: [0.9, 0.98]
eps: 1.0e-6
grad_clip: 1.0
warmup_fraction: 0.06
schedule: linear_decay
eval_every_steps: 500
log_every_steps: 20
loss: cross_entropy # experiment set: cross_entropy | weighted_cross_entropy | focal (all over the partial-label NLL)
class_weights: auto # inverse-frequency over fully-labelled rows when loss == weighted_cross_entropy;
# "unwanted" rows get mean(w_phishing, w_spam)
focal_gamma: 2.0
early_stopping:
# val_f1_unwanted = F1 of the binary roll-up P(phishing)+P(spam) >= 0.5 (identical to the binary runs' val F1,
# so results stay comparable); alternative: val_macro_f1_3way (macro-F1 over the three classes on val rows
# with a full label).
metric: val_f1_unwanted
patience: 2
pooling: cls
freeze_encoder_layers: 0 # experiment dial (instructor: "number of layers")
dynamic_padding_multiple: 8
checkpoint_every_steps: 500
# ---------------------------------------------------------------- evaluation
eval:
metrics: [accuracy, precision, recall, f1, roc_auc, pr_auc, confusion_matrix, recall_at_fpr_1pct, recall_at_fpr_0_1pct]
credential_lure_slice: true # secondary slice of the holdout matching credential-lure phrases
latency_samples: 100 # avg ms/email, GPU and CPU, like the instructor's step 5
latency_max_len: 512
threshold: 0.5 # decision threshold; evaluate.py also reports the best-F1 threshold
# Three-way evaluation (RUN_LOG entry 6). The binary metrics above are unchanged: they are computed on the
# roll-up P(unwanted) = P(phishing) + P(spam) (= P(class 1) for a 2-class head), so results stay comparable
# with the binary-era runs. Three-way metrics use only rows with label3 in {0,1,2}; -1/"unwanted" rows
# (TREC-05 / TREC-07 / CEAS-08 positives) enter the roll-up only.
test3_file: test_holdout3.parquet # clean-provenance three-way holdout written by data_prep (--split test3); read by evaluate.py
# The three keys below RESTATE the label contract that is fixed in code (evaluate.LABEL_NAMES / UNWANTED_NAME /
# DECISION_RULE, mirrored in export.py and the app). evaluate.py asserts at startup that they still match and
# refuses to run otherwise, so a drift is loud; editing them here does not rename anything.
label_names: {0: Legitimate, 1: Phishing, 2: Spam}
unwanted_name: "Unwanted (phishing or spam)"
threeway_decision_rule: rollup_then_argmax # unwanted iff P(unwanted) >= threshold, then argmax(P(phishing), P(spam)); argmax-of-3 reported alongside
# documentation only: the sections every evaluated three-way block contains (not read by any script)
metrics_threeway: [confusion_matrix_3x3, per_class_precision_recall_f1, macro_f1, phishing_vs_spam_on_true_unwanted, per_source_counts]
top_k_confusions: 25 # read by evaluate.py: size of the phishing->spam / spam->phishing preview tables in error_analysis_test3.md
model_card_metrics3: reports/metrics_test3.json # read by export.py: three-way table source (binary table: reports/metrics.json); both files must name the exported checkpoint
# ---------------------------------------------------------------- Hugging Face (ask before any push)
hf:
owner: BuildITSystems
model_repo: BuildITSystems/phishing-detector-encoder
space_repo: BuildITSystems/phishing-detector-demo
private: true
space_sdk: gradio
space_sdk_version: "6.29.1"
space_python_version: "3.12.12" # ZeroGPU supports exactly 3.12.12 / 3.10.13
space_hardware: zero-a10g # ZeroGPU flavor string (backing GPU: RTX Pro 6000 Blackwell slice)
space_torch: "2.13.0" # highest version on the ZeroGPU allow-list (2.14.x not listed)
weights_format: safetensors_bf16