Instructions to use SlayerLab/NERGAL with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use SlayerLab/NERGAL with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="SlayerLab/NERGAL")# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("SlayerLab/NERGAL") model = AutoModelForTokenClassification.from_pretrained("SlayerLab/NERGAL", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 9,128 Bytes
caa10a8 2bf5c25 caa10a8 2bf5c25 caa10a8 3f67c66 7ce967b 15eb2b4 2bf5c25 7ce967b caa10a8 ff286c1 caa10a8 7ce967b caa10a8 ff286c1 15eb2b4 ff286c1 15eb2b4 ff286c1 15eb2b4 ff286c1 2bf5c25 caa10a8 15eb2b4 ff286c1 caa10a8 15eb2b4 caa10a8 15eb2b4 caa10a8 7ce967b caa10a8 2bf5c25 3615219 caa10a8 3615219 caa10a8 2bf5c25 caa10a8 | 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 | ---
license: mit
language:
- pl
base_model: FacebookAI/xlm-roberta-large
library_name: transformers
pipeline_tag: token-classification
tags:
- polish
- pii
- ner
- xlm-roberta
- hybrid
---
# NERGAL 1.0.3
**Named Entity Recognition with Grounded Additive Labels**
SlayerLab hybrid PII cleaner for Polish. Not a chat model. Not a drop-in `pipeline("token-classification")`.
## TL;DR
Python rules do the identifiers they can prove. A transformer NER head adds phone and other PII spans the regex misses. The cleaner **unions** the two on the original text, then replaces hits with `[Telefon]` or `[PII]`.
- **Version:** `1.0.3` (`hybrid.json`, `CHANGELOG.md`)
- **Ground:** `scrub_pii` regex (SHA256 `f32d5c54…`)
- **Additive labels:** XLM-RoBERTa-large token classifier, BIO tags `phone` / `pii`, threshold 0.95
- **This snapshot:** seed `202609160`, **epoch 5** of a seven-epoch schedule
## What NERGAL detects — and what it does not
NERGAL masks **contact details and selected identifiers in Polish text**. It is not a general-purpose anonymizer: names, postal addresses and other personal information can remain in the output. Its corpus-masking policy also includes public, institutional and company contacts and identifiers.
### Detection scope
These are target categories, not a guarantee that every occurrence or format is detected.
| Category | Values in scope | Replacement |
|---|---|---|
| Phone contacts | Phone, fax and SMS contact numbers, including foreign, emergency, short/service and vanity numbers; extensions and number alternatives | `[Telefon]` |
| Email | Email addresses, including recoverable broken or incomplete addresses | `[PII]` |
| Personal identifiers | PESEL, passport and identity-document numbers | `[PII]` |
| Organization identifiers | NIP/VAT, REGON, KRS, GEMI, LEI and equivalent foreign company registration numbers; labelled DUNS, BDO and RPWDL register-book numbers | `[PII]` |
| Financial identifiers | Bank/account numbers, including Polish accounts and foreign IBANs | `[PII]` |
| Property identifiers | Land-register (księga wieczysta, KW) numbers | `[PII]` |
| Electronic contacts and access | e-Doręczenia and ePUAP addresses, explicitly labelled numeric access PINs (including `pin=` URL values), GG account IDs | `[PII]` |
Context matters: a number that resembles a phone or identifier is not automatically in scope. Rules use labels, format checks and, for some unlabelled identifiers, checksums; the model adds contextual detections. Coverage varies by category, and the aggregate benchmark below does not establish recall for every category.
### Outside detection scope
NERGAL is not designed to remove:
- **Personal names**, including private individuals and public officials; organization names.
- **Postal/street addresses, dates of birth and ages.**
- **Social-media handles, ordinary URLs and filenames.** An in-scope value inside a URL, such as a labelled numeric PIN, can still be masked.
- **Vehicle registration plates and generic serial, model or version codes.**
- **Document, case, article, funding and procurement references**, including procedure UUIDs; ISBNs, ORCIDs, TERYT codes, EAN/GTIN product codes and CNIL website-registration references.
- Prices, list numbers, generic labels without values, clearly fictitious examples and already-redacted placeholders.
These are intended exclusions; false positives can still mask some of this content.
### Known gaps in 1.0.3
Unlabelled phones and identifiers, unusual formatting and damaged text can escape detection. **VINs and obfuscated emails** (such as `name (at) domain.pl`) are approved annotation targets, but that approval alone does not establish reliable support in the released 1.0.3 model. Do not rely on it to remove them consistently.
## Versions
841-dev, union at 0.95, 354 gold spans. Same weights and API is a patch; new capability is minor; API, threshold, or weight recipe is major. A release that changes these numbers updates `hybrid.json` `eval` and this table.
| Version | Whole /354 | Residual | Rules FP | Union FP | Char P | Char R | What changed |
|---|---:|---:|---:|---:|---:|---:|---|
| 1.0.0 | 323 | 25 | 133 | 133 | 97.76% | 95.95% | First Hub snapshot |
| 1.0.1 | 323 | 25 | 98 | 123 | 97.93% | 95.95% | Prefix-only glued-email trim |
| 1.0.2 | 324 | 24 | 98 | 123 | 97.93% | 96.12% | Labelled country-area phone fix |
| **1.0.3** | **324** | **24** | **98** | **123** | **97.93%** | **96.12%** | Label-note, e-Delivery and registry rules; placeholder and card fixes |
## 841-dev
Tables use one development split: 841 passages, 215 with gold PII, **354 spans** (169 phone, 185 other PII). It is the `dev` side of a 4,500-passage labelled tranche (3,655 train / 841 dev). Sources match Dynaword (EUR-Lex, HPLT, Wikipedia, parliamentary and government text, plus smaller news/literary slices). Labels mix unchanged silver with human review.
The files contain real identifiers, so they are not released with the weights.
## Why XLM-R
GLiNER, HerBERT-large, and XLM-R-large were trained on the same split and unioned with the same regex. Plot: diagnostic threshold 0.50; selection used the full threshold grid. GLiNER covers more at 0.50 and then dumps precision. XLM-R is the architecture we kept.

## Why epoch 5
Fresh XLM-R, seven epochs. 133 epoch/threshold combinations. Epoch 5 at 0.95 was the only point that both beat the historical GLiNER∪regex incumbent on coverage and introduced zero new false-mask characters. Epoch 7 covers more PII (334/354) but adds 15 new false characters.

| Epoch | Covered @ 0.95 /354 | Residual passages | False characters | New false vs incumbent |
|---|---:|---:|---:|---:|
| 1 | 272 | 59 | 175 | 42 |
| 2 | 291 | 49 | 141 | 8 |
| 3 | 317 | 30 | 140 | 7 |
| 4 | 320 | 28 | 143 | 10 |
| **5** | **323** | **25** | **133** | **0** |
| 6 | 328 | 20 | 147 | 14 |
| 7 | 334 | 16 | 148 | 15 |
## Compared with other systems
Same 841-dev split, threshold **0.95**. **Naked** is the transformer alone. **∪ regex** is that model unioned with the current rules. Character scores are gold vs masked characters.
| System | Mode | Whole /354 | Residual | False chars | Char P | Char R |
|---|---|---:|---:|---:|---:|---:|
| Regex (`scrub_pii`) | rules | 259 | 70 | 98 | 98.13% | 85.04% |
| GLiNER 2.5-multi zero-shot | naked | 81 | 188 | 970 | 56.98% | 21.22% |
| GLiNER 2.5-multi zero-shot | ∪ regex | 273 | 61 | 1,068 | 83.33% | 88.16% |
| Historical GLiNER email12 | naked | 249 | 70 | 10 | 99.81% | 85.39% |
| Historical GLiNER email12 | ∪ regex | 290 | 50 | 108 | 98.13% | 93.76% |
| XLM-R epoch 5 | naked | 298 | 42 | 57 | 98.96% | 89.38% |
| **NERGAL 1.0.3** | **∪ regex** | 324 | 24 | 123 | 97.93% | 96.12% |
Zero-shot [GLiNER 2.5](https://huggingface.co/fastino/gliner2.5-multi-v1) is not competitive here, especially on non-phone PII (7/185 whole vs 160 naked / 179 union). Fine-tuned historical GLiNER is the precise naked baseline (10 false characters) and still trails XLM-R on coverage. NERGAL is XLM-R epoch 5 plus the regex: 145/169 phone, 179/185 other PII. Exact-span precision 86.34%, recall 89.27%, F1 87.78%.
Trained GLiNER, HerBERT-large, and XLM-R-large were also compared on this split (plot above). GLiNER’s 0.50 coverage lead is the precision collapse in that figure; no GLiNER or HerBERT operating point passed the content-preservation gate.
## Extra seeds
Historical seed-comparison results, before the 1.0.2 parser fix.
| Seed | Whole /354 | False chars | New false vs historical union |
|---|---:|---:|---:|
| 202609160 (selected weights) | 323 | 123 | 0 |
| 202609161 | 322 | 134 | 1 |
| 202609162 | 316 | 151 | 18 |
Seed 160 is the published weights. 161 and 162 were confirmation runs of the same recipe.
## Load
This repo is the PII island: `scrub_pii.py` plus `nergal.py`. `pipeline("token-classification")` will not match. Regex runs on the original text, the model adds spans at 0.95, then the two are unioned and replaced with `[Telefon]` / `[PII]`. Text that already holds `[PII]` / `[Telefon]` is scrubbed as usual, but an identifier with a placeholder inside it or right before it can be missed.
```python
from pathlib import Path
from huggingface_hub import snapshot_download
root = Path(snapshot_download("SlayerLab/NERGAL"))
import sys
sys.path.insert(0, str(root))
from nergal import Nergal
nergal = Nergal.from_pretrained(root, local_files_only=True)
masked, counts = nergal.scrub(text)
```
`hybrid.json` records version `1.0.3`, threshold 0.95, gap ids `250002` / `250003`, the 841-dev `eval` block, and two weight hashes: `model_safetensors_sha256` for the published file and `source_checkpoint_sha256` for the training checkpoint it was packed from. `test_nergal.py` is synthetic (no corpus text). From this snapshot: `python -m unittest test_nergal`.
Base weights: [`FacebookAI/xlm-roberta-large`](https://huggingface.co/FacebookAI/xlm-roberta-large) revision `c23d21b0620b635a76227c604d44e43a9f0ee389` (MIT).
|