File size: 12,603 Bytes
439e7b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | # 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
|