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")# pip install -U transformers accelerate # 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
2.0.1: a [PERSON] placeholder ends an other-number label
Browse filesSame weights, threshold and API. Rules SHA c1b924a8: a Kod/NIP/REGON/PESEL/KRS/ISBN
label no longer reaches a number across [PERSON], so re-scrubbing names-on output
masks a phone after a masked name. Phone cues still cross [PERSON]. A first scrub of
raw text gives 2.0.0's rule spans, names on or off.
Gates vs 2.0.0 rules: 841-dev + invented 0/845 changed; names-on re-scrub 0/841;
bench replay 12/12 cohorts equal; full Dynaword corpus 0/4,144,908 changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CHANGELOG.md +15 -0
- README.md +5 -4
- hybrid.json +2 -2
- nergal.py +3 -4
- scrub_pii.py +4 -1
- test_nergal.py +14 -2
CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ Semver for this island:
|
|
| 8 |
|
| 9 |
Accuracy is 841-dev, union at 0.95. Through 1.1.2 the gold has 354 spans; from 1.2.0 it is restated to phone policy v3 (315 spans), and the two are not comparable. A version that changes those numbers must update `hybrid.json` `eval` and the tables below.
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
## 2.0.0
|
| 12 |
|
| 13 |
Same NERGAL weights and threshold. Rules SHA `d1866243…`: the placeholder rename only; every rule span is unchanged. Major because the output placeholder changes.
|
|
|
|
| 8 |
|
| 9 |
Accuracy is 841-dev, union at 0.95. Through 1.1.2 the gold has 354 spans; from 1.2.0 it is restated to phone policy v3 (315 spans), and the two are not comparable. A version that changes those numbers must update `hybrid.json` `eval` and the tables below.
|
| 10 |
|
| 11 |
+
## 2.0.1
|
| 12 |
+
|
| 13 |
+
Same NERGAL weights, threshold and API. Rules SHA `c1b924a8…`: one fix for input that already holds `[PERSON]`.
|
| 14 |
+
|
| 15 |
+
- **A masked name ends an other-number label.** A `Kod`, `NIP`, `REGON`, `PESEL`, `KRS` or `ISBN` label up to 20 characters before a number keeps it from being a phone. In 2.0.0 that held across a `[PERSON]` placeholder, so scrubbing names-on output again could leave a phone that follows a masked name. In 2.0.1 a `[PERSON]` between the label and the number ends the label's reach.
|
| 16 |
+
- **Phone cues still cross `[PERSON]`.** A contact or phone label before a masked name still counts for the number after it. Unlike `[PHONE]` and `[PII]`, `[PERSON]` is not a rules boundary.
|
| 17 |
+
- **A first scrub is unchanged.** The rules read the raw text and name spans are joined after, so scrubbing raw text gives 2.0.0's rule spans, names on or off. The fix shows only when the input already holds `[PERSON]`, such as a second scrub.
|
| 18 |
+
|
| 19 |
+
841-dev, names off: unchanged from 2.0.0. Release gates, 2.0.1 rules against 2.0.0's: 841-dev plus the invented placeholder cases, 0 of 845 passages changed, scrubbed once or twice; 841-dev scrubbed names-on and then again, 0 of 841 changed, so the fix is exercised only by the invented cases in `test_nergal.py`; the benchmark replay gives 2.0.0's numbers on all 12 cohorts; every on-disk Dynaword source (37 sources, 4,144,908 documents, full text, placeholder documents included), 0 changed.
|
| 20 |
+
|
| 21 |
+
| Version | Whole /315 | Residual | Rules FP | Union FP | Char P | Char R | What changed |
|
| 22 |
+
|---|---:|---:|---:|---:|---:|---:|---|
|
| 23 |
+
| 2.0.0, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | `[PHONE]` replaces `[Telefon]`; opt-in `[PERSON]` names |
|
| 24 |
+
| 2.0.1, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | A `[PERSON]` placeholder ends an other-number label |
|
| 25 |
+
|
| 26 |
## 2.0.0
|
| 27 |
|
| 28 |
Same NERGAL weights and threshold. Rules SHA `d1866243…`: the placeholder rename only; every rule span is unchanged. Major because the output placeholder changes.
|
README.md
CHANGED
|
@@ -16,7 +16,7 @@ tags:
|
|
| 16 |
- hybrid
|
| 17 |
---
|
| 18 |
|
| 19 |
-
# NERGAL 2.0.
|
| 20 |
|
| 21 |
**Named Entity Recognition with Grounded Additive Labels**
|
| 22 |
|
|
@@ -66,9 +66,9 @@ NERGAL is not designed to remove:
|
|
| 66 |
|
| 67 |
These are intended exclusions; false positives can still mask some of this content.
|
| 68 |
|
| 69 |
-
### Known gaps in 2.0.
|
| 70 |
|
| 71 |
-
Unlabelled phones and identifiers (the rules take a bare phone only in the grouped national forms `601 234 567` and `22 123 45 67`; a plain `601234567` needs a label or the model), 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 2.0.
|
| 72 |
|
| 73 |
## Versions
|
| 74 |
|
|
@@ -78,7 +78,8 @@ Unlabelled phones and identifiers (the rules take a bare phone only in the group
|
|
| 78 |
|---|---:|---:|---:|---:|---:|---:|---|
|
| 79 |
| 1.1.2 | 303 | 9 | 83 | 333 | 94.37% | 97.83% | Email boundary rules |
|
| 80 |
| 1.2.0 | 303 | 9 | 24 | 80 | 98.59% | 97.83% | Phone policy v3 at mask time, rules and model spans |
|
| 81 |
-
|
|
|
|
|
| 82 |
|
| 83 |
1.0.0–1.1.2 were scored on the earlier 354-span gold (whole 323 → 326, union FP 133 → 80); per-version numbers are in `CHANGELOG.md`.
|
| 84 |
|
|
|
|
| 16 |
- hybrid
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# NERGAL 2.0.1
|
| 20 |
|
| 21 |
**Named Entity Recognition with Grounded Additive Labels**
|
| 22 |
|
|
|
|
| 66 |
|
| 67 |
These are intended exclusions; false positives can still mask some of this content.
|
| 68 |
|
| 69 |
+
### Known gaps in 2.0.1
|
| 70 |
|
| 71 |
+
Unlabelled phones and identifiers (the rules take a bare phone only in the grouped national forms `601 234 567` and `22 123 45 67`; a plain `601234567` needs a label or the model), 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 2.0.1 model. Do not rely on it to remove them consistently. An email with a lowercase word glued onto its domain (`jan@firma.plkontakt`) is masked together with that word, and text glued before an address can be masked with it. A phone written as short parts joined by two slashes (`12 / 345 / 678`) is left as text. An identifier with a `[PII]` / `[PHONE]` / `[Telefon]` placeholder inside it or right before it can be missed. Names-on gaps are under [Names](#names).
|
| 72 |
|
| 73 |
## Versions
|
| 74 |
|
|
|
|
| 78 |
|---|---:|---:|---:|---:|---:|---:|---|
|
| 79 |
| 1.1.2 | 303 | 9 | 83 | 333 | 94.37% | 97.83% | Email boundary rules |
|
| 80 |
| 1.2.0 | 303 | 9 | 24 | 80 | 98.59% | 97.83% | Phone policy v3 at mask time, rules and model spans |
|
| 81 |
+
| 2.0.0, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | `[PHONE]` replaces `[Telefon]`; opt-in `[PERSON]` names |
|
| 82 |
+
| **2.0.1, names off** | **303** | **9** | **24** | **80** | **98.59%** | **97.83%** | A `[PERSON]` placeholder ends an other-number label |
|
| 83 |
|
| 84 |
1.0.0–1.1.2 were scored on the earlier 354-span gold (whole 323 → 326, union FP 133 → 80); per-version numbers are in `CHANGELOG.md`.
|
| 85 |
|
hybrid.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
"full_name": "Named Entity Recognition with Grounded Additive Labels",
|
| 3 |
"hub_id": "SlayerLab/NERGAL",
|
| 4 |
-
"version": "2.0.
|
| 5 |
"mode": "rules_union",
|
| 6 |
"epoch": 5,
|
| 7 |
"seed": 202609160,
|
| 8 |
"threshold": 0.95,
|
| 9 |
-
"rules_sha256": "
|
| 10 |
"eval": {
|
| 11 |
"split": "841-dev",
|
| 12 |
"gold_amendments": [
|
|
|
|
| 1 |
{
|
| 2 |
"full_name": "Named Entity Recognition with Grounded Additive Labels",
|
| 3 |
"hub_id": "SlayerLab/NERGAL",
|
| 4 |
+
"version": "2.0.1",
|
| 5 |
"mode": "rules_union",
|
| 6 |
"epoch": 5,
|
| 7 |
"seed": 202609160,
|
| 8 |
"threshold": 0.95,
|
| 9 |
+
"rules_sha256": "c1b924a893ed01b739d6616fcc1e05ffc5c9c0df138ac1409ab3af427c6b0768",
|
| 10 |
"eval": {
|
| 11 |
"split": "841-dev",
|
| 12 |
"gold_amendments": [
|
nergal.py
CHANGED
|
@@ -14,18 +14,17 @@ from functools import lru_cache
|
|
| 14 |
from pathlib import Path
|
| 15 |
|
| 16 |
import scrub_pii
|
| 17 |
-
from scrub_pii import LEGACY_PHONE_TAG, PHONE_TAG, PII_TAG
|
| 18 |
|
| 19 |
HUB_ID = 'SlayerLab/NERGAL'
|
| 20 |
-
VERSION = '2.0.
|
| 21 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 22 |
GAP_IDS = [250002, 250003]
|
| 23 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
| 24 |
LABELS = ['phone', 'pii']
|
| 25 |
THRESHOLD = 0.95
|
| 26 |
-
RULES_SHA = '
|
| 27 |
DTYPES = ('float32', 'float16')
|
| 28 |
-
PERSON_TAG = '[PERSON]'
|
| 29 |
MARKERS = (PHONE_TAG, PII_TAG, PERSON_TAG, LEGACY_PHONE_TAG)
|
| 30 |
TAGS = {'person': PERSON_TAG, 'pii': PII_TAG, 'phone': PHONE_TAG} # ascending precedence
|
| 31 |
RANK = {label: i for i, label in enumerate(TAGS)}
|
|
|
|
| 14 |
from pathlib import Path
|
| 15 |
|
| 16 |
import scrub_pii
|
| 17 |
+
from scrub_pii import LEGACY_PHONE_TAG, PERSON_TAG, PHONE_TAG, PII_TAG
|
| 18 |
|
| 19 |
HUB_ID = 'SlayerLab/NERGAL'
|
| 20 |
+
VERSION = '2.0.1'
|
| 21 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 22 |
GAP_IDS = [250002, 250003]
|
| 23 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
| 24 |
LABELS = ['phone', 'pii']
|
| 25 |
THRESHOLD = 0.95
|
| 26 |
+
RULES_SHA = 'c1b924a893ed01b739d6616fcc1e05ffc5c9c0df138ac1409ab3af427c6b0768'
|
| 27 |
DTYPES = ('float32', 'float16')
|
|
|
|
| 28 |
MARKERS = (PHONE_TAG, PII_TAG, PERSON_TAG, LEGACY_PHONE_TAG)
|
| 29 |
TAGS = {'person': PERSON_TAG, 'pii': PII_TAG, 'phone': PHONE_TAG} # ascending precedence
|
| 30 |
RANK = {label: i for i, label in enumerate(TAGS)}
|
scrub_pii.py
CHANGED
|
@@ -46,6 +46,7 @@ import re
|
|
| 46 |
PHONE_TAG = "[PHONE]"
|
| 47 |
PII_TAG = "[PII]"
|
| 48 |
LEGACY_PHONE_TAG = "[Telefon]" # NERGAL 1.x output; still a boundary and a heading in text scrubbed before 2.0
|
|
|
|
| 49 |
_BOUNDARY = "|".join(map(re.escape, (PHONE_TAG, PII_TAG, LEGACY_PHONE_TAG)))
|
| 50 |
_BOUNDARY_WIDTH = max(map(len, (PHONE_TAG, PII_TAG, LEGACY_PHONE_TAG))) # 9: same window as 1.x
|
| 51 |
COUNTS = ("email", "phone", "pesel", "nip", "regon", "account", "document", "krs", "electronic_address", "pin",
|
|
@@ -188,7 +189,9 @@ _PHONE_BREAK_RE = re.compile(
|
|
| 188 |
r"|\d{1,2}[.)][ \t]+\d{1,2}[./]\d{1,2}[./](?:19|20)\d{2})")
|
| 189 |
_SECTION_MARKER_RE = re.compile(r"\d{1,2}[.)](?!\d)")
|
| 190 |
_PHONE_EXTENSION_RE =re.compile(r"(?:(?:[ \t]+,?[ \t]*|,[ \t]*)wew(?:n(?:ętrzny)?)?\.?[ \t]*\d{1,5}|[ \t]+do[ \t]+\d{1,3}(?=[ \t]*(?:[.;,](?!\d)|\r?\n|\Z)))(?!\w|[ \t]*\d)", re.I)
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
# Grouped national phones need no cue: web contact blocks write them bare. Plain 9-digit strings stay cue-gated,
|
| 193 |
# since many unlabelled ones are not phones.
|
| 194 |
_MOBILE_PREFIX = "45|50|51|53|57|60|66|69|72|73|78|79|88"
|
|
|
|
| 46 |
PHONE_TAG = "[PHONE]"
|
| 47 |
PII_TAG = "[PII]"
|
| 48 |
LEGACY_PHONE_TAG = "[Telefon]" # NERGAL 1.x output; still a boundary and a heading in text scrubbed before 2.0
|
| 49 |
+
PERSON_TAG = "[PERSON]" # opt-in names (2.0); not a boundary: a phone cue before a name still counts
|
| 50 |
_BOUNDARY = "|".join(map(re.escape, (PHONE_TAG, PII_TAG, LEGACY_PHONE_TAG)))
|
| 51 |
_BOUNDARY_WIDTH = max(map(len, (PHONE_TAG, PII_TAG, LEGACY_PHONE_TAG))) # 9: same window as 1.x
|
| 52 |
COUNTS = ("email", "phone", "pesel", "nip", "regon", "account", "document", "krs", "electronic_address", "pin",
|
|
|
|
| 189 |
r"|\d{1,2}[.)][ \t]+\d{1,2}[./]\d{1,2}[./](?:19|20)\d{2})")
|
| 190 |
_SECTION_MARKER_RE = re.compile(r"\d{1,2}[.)](?!\d)")
|
| 191 |
_PHONE_EXTENSION_RE =re.compile(r"(?:(?:[ \t]+,?[ \t]*|,[ \t]*)wew(?:n(?:ętrzny)?)?\.?[ \t]*\d{1,5}|[ \t]+do[ \t]+\d{1,3}(?=[ \t]*(?:[.;,](?!\d)|\r?\n|\Z)))(?!\w|[ \t]*\d)", re.I)
|
| 192 |
+
# A name placeholder ends the label's reach (2.0.1): "Kod [PERSON] zadzwoń 601 200 300" is a phone.
|
| 193 |
+
_OTHER_NUMBER_LABEL_RE = re.compile(
|
| 194 |
+
r"\b(?:NIP|REGON|PESEL|KRS|ISBN|kod)\b(?:(?!" + re.escape(PERSON_TAG) + r")[^\d\n]){0,20}\Z", re.I)
|
| 195 |
# Grouped national phones need no cue: web contact blocks write them bare. Plain 9-digit strings stay cue-gated,
|
| 196 |
# since many unlabelled ones are not phones.
|
| 197 |
_MOBILE_PREFIX = "45|50|51|53|57|60|66|69|72|73|78|79|88"
|
test_nergal.py
CHANGED
|
@@ -7,7 +7,7 @@ import unittest
|
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
HERE = Path(__file__).resolve().parent
|
| 10 |
-
RULES_SHA = '
|
| 11 |
|
| 12 |
|
| 13 |
class NergalTests(unittest.TestCase):
|
|
@@ -15,7 +15,7 @@ class NergalTests(unittest.TestCase):
|
|
| 15 |
from nergal import GAP_IDS, GAPS, HUB_ID, RULES_SHA as PINNED, THRESHOLD, VERSION
|
| 16 |
card = json.loads((HERE / 'hybrid.json').read_text())
|
| 17 |
self.assertEqual(HUB_ID, 'SlayerLab/NERGAL')
|
| 18 |
-
self.assertEqual(VERSION, '2.0.
|
| 19 |
self.assertEqual(card['version'], VERSION)
|
| 20 |
self.assertEqual(card['eval']['union_fp'], 80)
|
| 21 |
self.assertEqual(card['eval']['rules_fp'], 24)
|
|
@@ -83,6 +83,18 @@ class NergalTests(unittest.TestCase):
|
|
| 83 |
with self.subTest(text=text):
|
| 84 |
self.assertEqual(found(text.format('[Telefon]')), found(text.format('[PHONE]')))
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def test_grouped_national_phones_mask_without_a_cue(self):
|
| 87 |
from nergal import rules
|
| 88 |
for text, masked in (('Sklep Ala, 601 234 567, czynne 9-17', ['601 234 567']),
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
HERE = Path(__file__).resolve().parent
|
| 10 |
+
RULES_SHA = 'c1b924a893ed01b739d6616fcc1e05ffc5c9c0df138ac1409ab3af427c6b0768'
|
| 11 |
|
| 12 |
|
| 13 |
class NergalTests(unittest.TestCase):
|
|
|
|
| 15 |
from nergal import GAP_IDS, GAPS, HUB_ID, RULES_SHA as PINNED, THRESHOLD, VERSION
|
| 16 |
card = json.loads((HERE / 'hybrid.json').read_text())
|
| 17 |
self.assertEqual(HUB_ID, 'SlayerLab/NERGAL')
|
| 18 |
+
self.assertEqual(VERSION, '2.0.1')
|
| 19 |
self.assertEqual(card['version'], VERSION)
|
| 20 |
self.assertEqual(card['eval']['union_fp'], 80)
|
| 21 |
self.assertEqual(card['eval']['rules_fp'], 24)
|
|
|
|
| 83 |
with self.subTest(text=text):
|
| 84 |
self.assertEqual(found(text.format('[Telefon]')), found(text.format('[PHONE]')))
|
| 85 |
|
| 86 |
+
def test_a_name_placeholder_ends_an_other_number_label_but_never_a_phone_cue(self):
|
| 87 |
+
from nergal import rules
|
| 88 |
+
for text, masked in (('Kod [PERSON] zadzwoń 601 200 300', ['601 200 300']),
|
| 89 |
+
('Kod Jan zadzwoń 601 200 300', []), # a raw name is text to the rules
|
| 90 |
+
('[PERSON] NIP: 601 234 567', []), # a label after the name still counts
|
| 91 |
+
('Telefon do [PERSON]: 601234567', ['601234567']), # plain 9 digits: cue-gated
|
| 92 |
+
('Kontakt: [PERSON], 601234567', ['601234567']),
|
| 93 |
+
('Informacje u [PERSON] pod numerem 601234567', ['601234567']),
|
| 94 |
+
('tel. [PERSON] 601234567', ['601234567'])): # invented
|
| 95 |
+
with self.subTest(text=text):
|
| 96 |
+
self.assertEqual([text[s['start']:s['end']] for s in rules(text) if s['label'] == 'phone'], masked)
|
| 97 |
+
|
| 98 |
def test_grouped_national_phones_mask_without_a_cue(self):
|
| 99 |
from nergal import rules
|
| 100 |
for text, masked in (('Sklep Ala, 601 234 567, czynne 9-17', ['601 234 567']),
|