NERGAL 1.0.2

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.2 (hybrid.json, CHANGELOG.md)
  • Ground: scrub_pii regex (SHA256 3016ae5b…)
  • 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

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

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.

Primary three-model curves

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.

Seven-epoch XLM-R curves

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.2 ∪ regex 324 24 123 97.93% 96.12%

Zero-shot GLiNER 2.5 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].

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.2, threshold 0.95, gap ids 250002 / 250003, and the 841-dev eval block. test_nergal.py is synthetic (no corpus text). From this snapshot: python -m unittest test_nergal.

Base weights: FacebookAI/xlm-roberta-large revision c23d21b0620b635a76227c604d44e43a9f0ee389 (MIT).

Downloads last month
44
Safetensors
Model size
0.6B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for SlayerLab/NERGAL

Finetuned
(1016)
this model