| --- |
| license: mit |
| library_name: onnx |
| tags: |
| - ad-blocking |
| - cybersecurity |
| - domain-classification |
| - dns-filtering |
| - edge-inference |
| - on-device |
| - esp8266 |
| - nodemcu |
| - logistic-regression |
| language: |
| - en |
| metrics: |
| - accuracy |
| - precision |
| - recall |
| - f1 |
| - roc_auc |
| model-index: |
| - name: AdVig |
| results: |
| - task: |
| type: text-classification |
| name: Domain Classification |
| dataset: |
| name: AdTrap v1 (test split) |
| type: saidutta69/AdTrap |
| metrics: |
| - type: accuracy |
| value: 0.9485 |
| name: Accuracy |
| - type: precision |
| value: 0.8491 |
| name: Precision |
| - type: recall |
| value: 0.7307 |
| name: Recall |
| - type: f1 |
| value: 0.7854 |
| name: F1 |
| - type: roc_auc |
| value: 0.9478 |
| name: ROC AUC |
| --- |
| |
| # AdVig |
|
|
|  |
|
|
| > Block ads and trackers at the DNS layer before they load - **16 KB of int8 weights running on a NodeMCU**. No cloud, no runtime lists, no API calls. The offline, no-dependency Pi-hole, distilled into a single dot product. |
|
|
| Priorities: **Quality > Size > Speed** |
|
|
| **Trained on:** AdTrap v1 - 713,539 domains (93,541 ad/tracker + 619,998 legitimate), built from StevenBlack/hosts, AdAway, Yoyo, and Majestic Million. |
|
|
| Sibling of [saidutta69/PhishScout](https://huggingface.co/saidutta69/PhishScout). |
|
|
| --- |
|
|
| ## Model Overview |
|
|
| AdVig is a tiny hybrid logistic regression that classifies any bare domain as `BLOCK` (ad/tracker) or `ALLOW` (legitimate) using the **domain string alone** - the exact input a DNS query carries. No page content, no network calls at inference time. Score = one int32 dot product over hashed character n-grams plus 34 structural features. |
|
|
| | Property | Value | |
| |---|---| |
| | Architecture | Logistic regression: hashed char n-grams + structural features | |
| | Features | 16,384 hashed n-gram buckets (FNV-1a, sign trick) + 34 structural | |
| | Input | Bare domain string (DNS query hostname) | |
| | Model size | **64.4 KB** float32 ONNX / **16.0 KB** int8 quantized | |
| | Training time | ~18 s CPU | |
| | Inference | **933 us/domain measured on NodeMCU @160 MHz**; ~113 us reference Python | |
| | License | MIT | |
|
|
| ## Performance |
|
|
| Final model (test split, held out, group-disjoint by registrable domain, seed 42): |
|
|
| | Metric | float32 | int8 quantized | |
| |---|---|---| |
| | Accuracy | 0.9485 | 0.9486 | |
| | Precision | 0.8491 | 0.8597* | |
| | Recall | 0.7307 | 0.7069* | |
| | F1 | **0.7854** | **0.7862** | |
| | ROC AUC | **0.9478** | 0.9477 | |
| | False positive rate | 1.87% | 1.87% | |
|
|
| *int8 row evaluated at its own val-tuned threshold; quantization changed F1 by +0.0007 (hash-collision regularization). |
| |
| ### Why not bigger? (bucket sweep) |
| |
| Smaller hashing spaces beat larger ones - collisions regularize: |
| |
| | buckets | C | F1 | AUC | int8 size | |
| |---|---|---|---|---| |
| | 2^16 | 10 | 0.7271 | 0.9288 | 64 KB | |
| | 2^15 | 10 | 0.7415 | 0.9337 | 32 KB | |
| | **2^14** | **1** | **0.7854** | **0.9478** | **16 KB** | |
| | 2^13 | 10 | 0.7756 | 0.9444 | 8 KB | |
| | 2^12 | 10 | 0.7585 | 0.9372 | 4 KB | |
| |
| ### Baseline benchmark |
| |
| Structural features alone top out at F1 0.634 (XGBoost): most blocklisted spam/parked domains have no structural tells. Lexical memory is what unlocks quality: |
| |
| | model | F1 | AUC | |
| |---|---|---| |
| | gaussian_nb | 0.5674 | 0.8197 | |
| | logistic_regression (structural) | 0.6006 | 0.8298 | |
| | decision_tree d8 (structural) | 0.6157 | 0.8112 | |
| | lightgbm 30x6L15 (structural) | 0.6222 | 0.8515 | |
| | xgboost 30x4 (structural) | 0.6341 | 0.8598 | |
| | gram-only LR 2^15 | 0.6953 | 0.9180 | |
| | **AdVig hybrid LR 2^14** | **0.7854** | **0.9478** | |
| |
| ## On-Device Benchmarks (NodeMCU ESP8266) |
| |
| Measured on hardware (Arduino core 3.1.2, 240 stratified test domains x 30 passes = 7,200 inferences per run): |
| |
| | Metric | 80 MHz | 160 MHz | |
| |---|---|---| |
| | Avg latency | **1840 us/domain** | **933 us/domain** | |
| | Min / Max | 1280 / 3043 us | 650 / 1879 us | |
| | Throughput | ~543 domains/s | ~1072 domains/s | |
| | Gram-hash phase | 144 us | 77 us | |
| | Structural phase | 1685 us | 850 us | |
| |
| - **Memory**: int8 weights live in flash (`PROGMEM`, 16.4 KB, zero RAM); working set is a 2048-entry tally table + bookkeeping (~7.7 KB static). Free heap: **42,192 B**. |
| - **Accuracy on-device** (balanced sample): acc 0.8792, precision **0.9789**, recall 0.7750, F1 0.8651 - bit-exact with host emulation, **100% parity (240/240)**, identical prediction bitmap at both clock speeds. |
| - **Scaling is linear with clock** (compute-bound); other MCUs scale predictably. |
| - Known headroom: the structural phase dominates (~92% of latency) due to linear PROGMEM lexicon scans; sorted arrays + binary search should roughly halve total latency. |
| |
| At ~1,000 domains/s, one NodeMCU comfortably keeps up with household-scale DNS traffic. |
| |
| ## Use Cases |
| |
| - **DNS-level Pi-hole replacement** - answer DNS queries with an on-device verdict; fully offline, zero external dependencies |
| - **Router/firewall firmware integration** - classify unknown domains at query time, complementing exact-match blocklists |
| - **Parental controls & IoT gateways** - block ad/tracker endpoints on devices that cannot run browser extensions |
| - **Privacy tooling research** - a compact baseline model for tracker-domain generalization studies |
| - **Browser/proxy pre-fetch screening** - cheap first-pass filter ahead of heavier analysis |
| |
| ## Features |
| |
| ### Structural (34) |
| |
| `length`, `label_count`, `max_label_len`, `digit_count`, `max_digit_run`, `hyphen_count`, `entropy`, `vowel_ratio`, `starts_with_www`, `has_punycode`, `subdomain_depth`, `tld_trusted`, `tld_adheavy`, `tld_is_cctld`, `tld_length`, `tok_ad`, `tok_advert`, `tok_banner`, `tok_promo`, `tok_sponsor`, `tok_track`, `tok_analytics`, `tok_metrics`, `tok_telemetry`, `tok_beacon`, `tok_pixel`, `tok_tag`, `tok_click`, `tok_impression`, `tok_affiliate`, `tok_syndication`, `tok_vendor`, `tok_bigtech`, `bigtech_and_adtoken` |
| |
| ### Hashed lexical memory |
| |
| Char 3/4/5-grams over `.domain.` hashed with FNV-1a (random sign projection) into 2^14 buckets. Inference collapses to `logit = SCALE * gram_sum + BIAS + dot(STRUCT_W, feats)` - trivially portable to any MCU in C. |
|
|
| ## Usage |
|
|
| ### Python (ONNX Runtime) |
|
|
| ```python |
| import numpy as np |
| import onnxruntime as ort |
| from gramlib import hash_grams # reference hasher (in repo files) |
| from features import extract_features # reference structural extractor |
| |
| sess = ort.InferenceSession("advig.onnx", providers=["CPUExecutionProvider"]) |
| domain = "ads.tracker-cdn.example.com" |
| |
| grams = np.zeros((1 << 14), dtype=np.float32) |
| for idx, val in hash_grams(domain, buckets=1 << 14).items(): |
| grams[idx] += np.sign(val) |
| x = np.concatenate([grams, extract_features(domain)]).astype(np.float32)[None] |
| |
| p_block = sess.run(["prob"], {"features": x})[0].item() |
| blocked = p_block >= 0.477 |
| ``` |
|
|
| ### ESP8266 / NodeMCU (C) |
|
|
| Flash `advig_weights.h` alongside the ~40-line scorer (reference firmware in the linked training repo). Decision rule on device: |
|
|
| ```c |
| logit = GRAM_SCALE_F * gsum_int32 + BIAS_F + dot(STRUCT_W, struct_feats); |
| block = (logit >= ADVIG_T_LNIT_F); |
| ``` |
|
|
| ## Verification & Reproducibility |
|
|
| - **ONNX parity** - 100% match between ONNX Runtime and closed-form sigmoid(Gemm). |
| - **On-device parity** - 100% (240/240) agreement between NodeMCU firmware and host emulation; confusion matrices identical. |
| - **Deterministic** - identical test predictions across seeds 42/1/7/123. |
| - **No leakage** - train/val/test disjoint at the registrable-domain level; synthetic subdomains inherit their parent's split. |
| - **int8 verified end-to-end** - quantized weights re-evaluated after quantization, not assumed lossless. |
|
|
| ## Limitations |
|
|
| - **Enumeration ceiling** - random parked/spam domains (`05tz2e9.com`) carry zero signal in their names; no string-based model can catch them. Pair AdVig with an exact-match blocklist: the list memorizes the tail, AdVig generalizes over unseen trackers. |
| - **"ad-" prefix traps** - `adyen.com`-style collateral exists (~1.9% FPR). Raise the threshold for allow-biased operation. |
| - **English-centric lexicons** - token lists are Western-market oriented. |
| - **Feed dependence** - inherits StevenBlack/AdAway/Yoyo coverage as of build date; retrain for fresh feeds. |
| - **Dataset noise** - Majestic top-1M contains some parked/ad-heavy registrable domains treated as ALLOW. |
|
|
| ## Training Data |
|
|
| AdTrap v1: 713,539 rows, 70/15/15 split by registrable domain, seed 42. |
|
|
| | Source | Class | Domains | |
| |---|---|---| |
| | StevenBlack hosts (ads+trackers base) | BLOCK | 93,512 | |
| | AdAway | BLOCK | 6,540 | |
| | Yoyo | BLOCK | 3,515 | |
| | Majestic Million (overlap removed) | ALLOW | 400,000 | |
| | Synthetic legit subdomains (augmentation) | ALLOW | 220,000 | |
|
|
| ## Citation |
|
|
| ```bibtex |
| @misc{saidutta69_2026_advig, |
| author = {Sai Dutta Abhishek Dash}, |
| title = {AdVig: Tiny On-Device Ad and Tracker Domain Classifier}, |
| year = {2026}, |
| publisher = {Hugging Face}, |
| howpublished = {\url{https://huggingface.co/saidutta69/AdVig}}, |
| note = {Trained on the AdTrap dataset} |
| } |
| ``` |
|
|
| Built on AdTrap + StevenBlack/hosts, AdAway, Yoyo, and Majestic Million. MIT licensed. |
|
|