加载中…
加载中…
The on-device build of ZeroGPU/zlm-v1-iab-domain-classifier:
a 149M-parameter ModernBERT model that classifies a web destination into IAB content and audience
categories from its URL alone, packaged as a 153 MB int8 ONNX file that runs in the browser.
This repository is the exact bundle ZeroGPU serves to edge devices (browsers, Node.js / Docker workers, Android) in production, laid out for transformers.js and onnxruntime. For the full-precision model, the PyTorch checkpoint, the training details and the complete benchmark, see the main repository.
logits has shape [batch, 2271]: the 704 IAB content logits followed by the 1,567
IAB audience logits. Labels in config.json carry a head prefix (content|Sports,
audience|Interest | Sports | Basketball |), so a standard text-classification pipeline can run it.max_seq_length in postprocess.json).postprocess.json).The model reads a deterministic rendering of the URL, not the raw URL:
"<host> | <segmented words> | tld:<tld>"
https://www.sportsnews.co.uk/foo -> "sportsnews.co.uk | sports news | tld:co.uk"
espn.com -> "espn.com | espn | tld:com"
Lowercase; strip scheme, www., port, path and query; split the host on ., - and digits; peel off the
TLD; word-segment glued labels (sportsnews → sports news, via
wordninja). The reference implementation url_text.py is included.
| File | Purpose |
|---|---|
onnx/model_quantized.onnx |
int8 graph, 153 MB. Inputs input_ids, attention_mask (int64); output logits [batch, 2271] |
config.json |
ModernBertForSequenceClassification config with a flat id2label over both heads |
postprocess.json |
Serving settings: input format, 64-token window, per-head thresholds |
tokenizer.json, tokenizer_config.json |
ModernBERT tokenizer (merges stored as "a b" strings for transformers.js 2.x) |
url_text.py |
Reference URL → input-string renderer |
config.json keeps a few informational fields inherited from the training wrapper
(base_model_name_or_path, head_architecture, max_seq_length); inference does not read them. The
backbone is gte-modernbert-base and the serving window is the 64 tokens in postprocess.json. The repository
contains no PyTorch weights.
import json
import sys
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from tokenizers import Tokenizer
root = snapshot_download("ZeroGPU/zlm-v1-iab-domain-classifier-edge")
sys.path.insert(0, root)
from url_text import url_to_text # pip install wordninja for word segmentation
post = json.load(open(f"{root}/postprocess.json"))
id2label = json.load(open(f"{root}/config.json", encoding="utf-8"))["id2label"]
tok = Tokenizer.from_file(f"{root}/tokenizer.json")
tok.enable_truncation(post["max_seq_length"]) # 64
sess = ort.InferenceSession(f"{root}/onnx/model_quantized.onnx")
enc = tok.encode(url_to_text("https://www.espn.com"))
logits = sess.run(["logits"], {
"input_ids": np.array([enc.ids], dtype=np.int64),
"attention_mask": np.array([enc.attention_mask], dtype=np.int64),
})[0][0]
probs = 1.0 / (1.0 + np.exp(-logits)) # multi-label: independent sigmoid per label
cuts = {"content": post["head_thresholds"]["iabcontent_names"], "audience": post["head_thresholds"]["iabaudience_names"]}
picked = sorted(
((id2label[str(i)], float(p)) for i, p in enumerate(probs) if p >= cuts[id2label[str(i)].split("|", 1)[0]]),
key=lambda t: -t[1],
)
print(picked[:6]) # [('content|Sports', 0.99...), ('audience|Sports', 0.99...), ...]
Classify one URL at a time, or in tightly padded batches: dynamic int8 quantization computes activation ranges over the whole input, padding included, and large padded batches lower the scores' accuracy.
ModernBERT needs transformers.js v3 (@huggingface/transformers).
import { pipeline } from '@huggingface/transformers';
const classify = await pipeline('text-classification', 'ZeroGPU/zlm-v1-iab-domain-classifier-edge', { dtype: 'q8' });
// Input is the rendered URL string (see "Input format")
const out = await classify('espn.com | espn | tld:com', { top_k: 10, function_to_apply: 'sigmoid' });
const picked = out.filter(({ label, score }) => score >= (label.startsWith('content|') ? 0.6 : 0.5));
console.log(picked);
Pass function_to_apply: 'sigmoid': the model is multi-label, so labels are scored independently.
Content micro-F1 at the 0.60 default threshold on the 784-URL gold set (ZeroGPU/iab-url-gold-784, labeled independently by GPT-5.5), one URL at a time without padding:
| Model | Size | Precision | Recall | Content F1 | Same top label as fp32 |
|---|---|---|---|---|---|
| This repository (int8, per-channel) | 153 MB | 0.4185 | 0.3242 | 0.3654 | 76% |
| Main repository, fp32 | 605 MB | 0.4243 | 0.3508 | 0.3841 | — |
| GPT-5.4-nano | — | 0.3332 | 0.3744 | 0.3526 | — |
The same 784 rows also selected the training epoch and the decision thresholds, so these scores are a mild upper bound on fully unseen data. The per-category results and the full evaluation are in the main repository.
onnxruntime 1.30, one input, CPU (Intel Core Ultra 9 275HX), median of 50 runs. A typical rendered URL is 10–20 tokens.
| Threads | 16 tokens | 64 tokens |
|---|---|---|
| 1 | 16.5 ms | 44.5 ms |
| 4 | 8.7 ms | 20.7 ms |
In production ZeroGPU serves this bundle to devices with at least 8 GB of RAM.
Alibaba-NLP/gte-modernbert-base).@misc{zerogpu2026iabdomainclassifier,
title = {zlm-v1-iab-domain-classifier: URL-only IAB classification with a fine-tuned ModernBERT},
author = {ZeroGPU},
year = {2026},
url = {https://huggingface.co/ZeroGPU/zlm-v1-iab-domain-classifier}
}