File size: 8,232 Bytes
00c2f5b 7e441a6 00c2f5b 7e441a6 00c2f5b | 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 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | ---
license: mit
language:
- en
library_name: onnx
tags:
- text-classification
- web-scraping
- boilerplate-removal
- content-extraction
- onnx
- tiny
pipeline_tag: text-classification
---
# WebRank
A 3.14M-parameter transformer that scores web text on a `[0, 1]` scale where
**1 = real content** and **0 = boilerplate** (cookie banners, navs, footers,
CTAs, error pages, JS placeholders, paywalls).
Ships as a 3.2 MB INT8 ONNX file
that runs anywhere ONNX Runtime runs β Python, JS (browser/node), Go, Rust,
C++, Java, .NET.
Built as the post-processing filter for the [Keiro Browser](https://github.com/keirolabs)
crawl pipeline, released as open source.
## Files
| File | Size | Description |
|------|------|-------------|
| `webrank.int8.onnx` | 3.2 MB | INT8-quantized model β recommended |
| `webrank.onnx` | 12 MB | FP32 model |
| `tokenizer.json` | 1.1 MB | HuggingFace `tokenizers` BPE vocab |
## Architecture
```
input_ids [B, 256] int64
β
token + position embeddings (dim=128)
β
5 Γ { LayerNorm β MHA(8 heads, SDPA) β residual
β LayerNorm β FFN(512) β residual }
β
LayerNorm β mean-pool over non-pad tokens
β
Linear(128β128) β GELU β Dropout β Linear(128β1) β sigmoid
β
score [B] float32
```
- **Vocab:** 16,384 byte-level BPE
- **Max seq length:** 256 BPE tokens
- **Params:** 3,135,617
- **Pretraining:** masked language modeling
- **Fine-tuning:** binary classification with BCE loss
## Usage
### Python
```python
import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer
tok = Tokenizer.from_file("tokenizer.json")
sess = ort.InferenceSession("webrank.int8.onnx",
providers=["CPUExecutionProvider"])
def encode(text, max_len=256):
pad_id = tok.token_to_id("[PAD]")
ids = tok.encode(text).ids[:max_len] # post-processor adds [CLS]/[SEP]
ids += [pad_id] * (max_len - len(ids))
return np.array([ids], dtype=np.int64)
def score(text):
out = sess.run(["score"], {"input_ids": encode(text)})[0]
return float(out.flatten()[0])
print(score("Mitochondria are membrane-bound organelles found in eukaryotic cells."))
# 0.93
print(score("We use cookies to improve your experience. Accept all cookies."))
# 0.08
```
Batched:
```python
def score_batch(texts):
ids = np.concatenate([encode(t) for t in texts], axis=0)
return sess.run(["score"], {"input_ids": ids})[0].flatten()
```
### JavaScript (browser / Node)
```js
import * as ort from "onnxruntime-web";
const session = await ort.InferenceSession.create("/webrank.int8.onnx");
// tokenize text into a BigInt64Array of length 256 using a JS BPE
// library that loads tokenizer.json
const tensor = new ort.Tensor("int64", ids, [1, 256]);
const out = await session.run({ input_ids: tensor });
console.log(out.score.data[0]); // 0..1
```
### Go
```go
import ort "github.com/yalue/onnxruntime_go"
ort.SetSharedLibraryPath("libonnxruntime.so")
ort.InitializeEnvironment()
defer ort.DestroyEnvironment()
input, _ := ort.NewTensor(ort.NewShape(1, 256), ids /* []int64 */)
output, _ := ort.NewEmptyTensor[float32](ort.NewShape(1))
sess, _ := ort.NewAdvancedSession(
"webrank.int8.onnx",
[]string{"input_ids"}, []string{"score"},
[]ort.Value{input}, []ort.Value{output}, nil,
)
sess.Run()
fmt.Println(output.GetData()[0])
```
## Performance
Measured on a Ryzen 7 (CPU only, ONNX Runtime 1.20):
| Variant | Single-row | Batch-18 | Size |
|---|---|---|---|
| FP32 | 5.9 ms | 238 ms | 12 MB |
| INT8 | 6.6 ms | 222 ms | 3.2 MB |
INT8 is **3.8Γ smaller** with β€0.024 score drift and identical predictions
on every test case. Quantization overhead cancels matmul savings at 3M
params, so single-row latency is roughly equivalent β INT8 wins on size
and on batched throughput.
## Training data
- **Pretraining:** [`Salesforce/wikitext`](https://huggingface.co/datasets/Salesforce/wikitext)
`wikitext-103-raw-v1`, ~29k articles, ~110M tokens.
- **Fine-tuning:** 30k labeled examples (15k positive / 15k negative).
- Positives: 7.5k paragraph-level + 7.5k sentence-level extracts from
wikitext articles, filtered for prose-like structure.
- Negatives: synthetically generated boilerplate from 40+ templates
(cookie banners, navs, footers, CTAs, JS placeholders, error pages,
paywall stubs), with deliberately varied length (40% single template,
30% pair, 20% triple, 10% stack of 4β6).
The mixed-length sampling on both sides is important β without it the
model learns to use sequence length as a shortcut.
## Training procedure
- **Pretraining:** masked language modeling (BERT-style 80/10/10 mask),
AdamW (lr 3e-4, betas 0.9/0.95, wd 0.01), cosine schedule with 100-step
warmup, gradient clipping 1.0, batch size 32, 800 steps total.
~75 minutes on CPU.
- **Fine-tuning:** binary classification head with BCE loss, AdamW
(lr 5e-5), 3 epochs over 12k training rows, batch size 64.
~38 minutes on CPU.
- Training framework: PyTorch (vanilla, no HuggingFace `transformers`
for the model itself).
## Evaluation
On a held-out 3,000-row validation split:
| Metric | Value |
|---|---|
| Accuracy | 1.000 |
| Precision | 1.000 |
| Recall | 0.999 |
| F1 | 1.000 |
| Loss | 0.0074 |
Held-out val is trivially separable because synthetic boilerplate vs
wikitext prose is a fairly easy decision boundary. For a more honest
read, on **18 hand-written real-world snippets** (none from the training
distribution):
- **16 / 18 correct** on the binary cutoff.
- The 2 failures are:
- `404 - Page not found. The page you are looking for might have been removed...` β 0.75 (false positive for content)
- `This article is for subscribers only. Subscribe now to read the full story...` β 0.72 (false positive for content)
Both are paywall/error pages styled as natural prose β the synthetic
templated negatives never showed the model that *prose-shaped* boilerplate
exists. Closing this gap requires real-world hard-negative mining.
## Limitations
1. **English only.** The byte-level tokenizer tolerates other scripts but
the classifier was never trained on them.
2. **Domain shift.** Trained on wikitext-103 (encyclopedic English).
Short technical statements like *"PostgreSQL uses MVCC for transactions"*
or casual writing score lower than they should because they don't match
wikitext prose style.
3. **Prose-shaped boilerplate.** Paywall walls, well-written 404 pages,
and "subscribe to read" stubs can confuse it because the synthetic
negatives are templated, not naturalistic.
4. **Sequence cap of 256 tokens.** Long documents must be chunked by the
caller. The intended use is per-paragraph scoring during crawl
post-processing, not whole-page classification.
5. **Pretraining cap of 800 steps.** Final MLM loss ~7.18 (16K vocab
unigram baseline β 7.2). The classifier still works fine because the
binary task is easy enough that the trunk doesn't need a deeply
converged language model β but a longer pretraining run would help
the borderline cases.
## Intended use
Drop into a web crawler / scraper as a post-extraction quality filter.
Score each paragraph or block, drop anything below ~0.5, keep the rest.
Cheap enough (β6 ms/paragraph on CPU) to run inline at crawl time.
**Not** intended as a general-purpose text classifier, content moderator,
toxicity detector, or anything else. It does one thing.
## Reproducing
The full training pipeline is in the [GitHub repo](https://github.com/keirolabs).
End-to-end on a Ryzen 7 takes ~115 minutes:
```bash
python collect.py # 1 min download wikitext, build labels
python tokenizer.py # 1 min train 16K BPE
python pretrain.py # 75 min MLM pretraining
python finetune.py # 38 min binary classification
python export.py # 2 sec PyTorch β ONNX FP32
python quantize_onnx.py # 5 sec ONNX FP32 β INT8
```
## License
MIT. Do whatever you want with it.
## Citation
```bibtex
@misc{webrank2026,
title = {WebRank: a 3M-parameter boilerplate classifier for web text},
author = {Keirolabs},
year = {2026},
url = {https://huggingface.co/mannybr/Webrank-nano}
}
```
|