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.4: the names layer leaves procedural role nouns out
Browse files- CHANGELOG.md +17 -0
- README.md +11 -10
- hybrid.json +7 -7
- nergal.py +10 -3
- test_nergal.py +37 -1
CHANGELOG.md
CHANGED
|
@@ -8,6 +8,23 @@ 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.3
|
| 12 |
|
| 13 |
Same NERGAL weights, threshold, union and API. Rules SHA `4d054723…`: input that already holds a `[PII]`, `[PHONE]` or `[Telefon]` placeholder.
|
|
|
|
| 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.4
|
| 12 |
+
|
| 13 |
+
Same NERGAL weights, threshold, union, rules (SHA `4d054723…`) and API. One change in `nergal.py` `person_spans`: the names layer (`names=True`) leaves procedural role nouns out. Names off is unchanged.
|
| 14 |
+
|
| 15 |
+
- **Procedural role nouns are not persons.** FastPDN-PII tags the capitalised party roles of rulings as persons, so 2.0.3 masked `Odwołujący`, `Przystępującemu` or `Protestującej` as `[PERSON]`. A model word that is one of five role participles (odwołujący, przystępujący, zamawiający, protestujący, skarżący) in any case, number or gender (`-y`, `-ego`, `-emu`, `-ym`, `-ych`, `-ymi`, `-a`, `-ej`, `-ą`, `-e`), any letter case, with only punctuation around it, is dropped before words are merged into a span. The names policy already keeps roles outside a person span.
|
| 16 |
+
- **A name next to a role keeps its mask.** `Odwołujący Jan Nowak` masks `Jan Nowak`; a word that only starts with a role stem (`Odwołującyński`) is still masked.
|
| 17 |
+
- **The list is measured.** On the 240 KIO rulings of the UZP G1 check (slayerlabs/polish-dynaword-v2#112), after the full G1 pass, 2.0.3 left 4,619 of 7,297 capitalised `Odwołujący` forms (36.7% lost) and 1,272 of 1,971 `Przystępujący` (35.5% lost); 2.0.4 loses 18 (0.25%) and 11 (0.56%), and with names off G1's own rules already drop 16 and 9. `Zamawiający` loses 64 → 21 of 9,446, `Protestujący` 36 → 0 of 82, `Skarżący` 12 → 0 of 68. `[PERSON]` placeholders in those rulings fall 6,917 → 3,531. `Wykonawca` is left as it is: 2 capitalised forms inside a person span. Wnioskodawca, Uczestnik, Powód, Pozwany, Przewodniczący, Protokolant, Pełnomocnik, Prezes, Inwestor and Oferent are never inside a person span in the surveyed samples of six Dynaword sources (uzp, ted_pl, samorzad_gov_pl, govpl, eurlex, dziennik_ustaw).
|
| 18 |
+
|
| 19 |
+
Still missed: the number after a role (`Odwołujący 1`) stays `[PERSON]`, because a name can be letterless (the name test gold holds 4 letterless person words); role nouns outside the five; a role glued to another word in damaged text.
|
| 20 |
+
|
| 21 |
+
841-dev, names off: unchanged from 2.0.3. Release gates (`artifacts/nergal_2_0_4_role_nouns/`, frozen before scoring, stored spans, no new inference): the 1,837 bench rows, the 1,000-document Dynaword runtime sample (names off and on) and 41,204 FineWeb miss-hunt passages, rule spans and output identical; the G1 delta rebuilds the stored 2.0.3 output of G1's NERGAL stage for all 240 rulings. Name test v1 and v2 (1,050 passages): a live CPU float32 run of the 2.0.4 `Names` equals the stored spans refiltered word by word, 0 mismatches; one passage changes in each set. `hybrid.json` `eval_names` is now scored on those refiltered spans: random panel whole names 586/614 and false-mask passages 34 unchanged, person false characters 1,051 → 1,021; targeted panel unchanged. Label pool v3 `bench`: names off, every cohort as 2.0.2; `--names`, names v1 random person false characters 1,051 → 1,021, names v2 random 573 → 561 (false-mask passages 34 → 33), whole names and recall unchanged on every cohort.
|
| 22 |
+
|
| 23 |
+
| Version | Whole /315 | Residual | Rules FP | Union FP | Char P | Char R | What changed |
|
| 24 |
+
|---|---:|---:|---:|---:|---:|---:|---|
|
| 25 |
+
| 2.0.3, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | Values cut or preceded by a placeholder |
|
| 26 |
+
| 2.0.4, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | Names layer leaves procedural role nouns out |
|
| 27 |
+
|
| 28 |
## 2.0.3
|
| 29 |
|
| 30 |
Same NERGAL weights, threshold, union and API. Rules SHA `4d054723…`: input that already holds a `[PII]`, `[PHONE]` or `[Telefon]` placeholder.
|
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 |
|
|
@@ -81,13 +81,14 @@ Unlabelled phones and identifiers (the rules take a bare phone only in the group
|
|
| 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 |
| 2.0.2, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | A rule's label wins on its characters |
|
| 84 |
-
|
|
|
|
|
| 85 |
|
| 86 |
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`.
|
| 87 |
|
| 88 |
## Names
|
| 89 |
|
| 90 |
-
`names=True` (CLI `--names`) adds a second model, [FastPDN NER — Polish PII](https://huggingface.co/ArkadiuszPawlak/fastpdn-ner-polish-pii) by ArkadiuszPawlak, revision `636c57fe…`, fine-tuned from [clarin-pl/FastPDN](https://huggingface.co/clarin-pl/FastPDN) on filtered KPWr plus LLM-synthetic data. `names/` holds it converted from ONNX to safetensors with no value changed. **`names/` is licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/), not MIT**; `names/NOTICE.md` has the attribution, the changes and the file hashes. NERGAL uses only its person tags (`PERSON`, `PERSON_F`, `PERSON_L`), masks whole words, and always runs it in float32.
|
| 91 |
|
| 92 |
**Scope with names on.** The name test labels every person: private individuals, public officials, politicians and historical figures alike, plus fictional characters, nicknames, partial names and initials. Titles and roles stay outside the span. Names inside a street, patronage, organisation or work title, eponyms, usernames, and names inside emails or URLs are not person spans. Masking public figures removes who said, signed or wrote what, which is why names are opt-in. This is the labelling target; the model does not meet it everywhere.
|
| 93 |
|
|
@@ -95,10 +96,10 @@ Unlabelled phones and identifiers (the rules take a bare phone only in the group
|
|
| 95 |
|
| 96 |
| Panel | Passages | Names | Whole | Components covered (95% CI) | Person false chars | Passages with one |
|
| 97 |
|---|---:|---:|---:|---:|---:|---:|
|
| 98 |
-
| Random | 300 | 614 | 586 (95.44%) | 98/116 (76.6–90.5%) | 1,
|
| 99 |
| Targeted | 150 | 292 | 286 (97.95%) | 94/99 (88.6–98.3%) | 976 | 42 (28.0%) |
|
| 100 |
|
| 101 |
-
Names off hides no name. A component is the independent unit, covered when every name in it is whole: on the random panel 18 of 116 keep part of a name. The second pass is a same-day blind self-review of 100 passages by the same reviewer (11 disagreed), not independent agreement. `hybrid.json` `eval_names` has the full numbers, including float16 and INT8 scored for the record; neither ships.
|
| 102 |
|
| 103 |
**Cost:** on one RTX 4090 with 3 processes, names took 2.0.0 from 74,520 to 66,492 chars/s (−11%) and peak device memory from 9,006 to 15,416 MiB; with one process, −9%. Local measurements are in `CHANGELOG.md`.
|
| 104 |
|
|
@@ -112,7 +113,7 @@ Names off hides no name. A component is the independent unit, covered when every
|
|
| 112 |
|
| 113 |
## Compared with other systems
|
| 114 |
|
| 115 |
-
Same split and gold. Every model runs through the NERGAL recipe with names off: its spans at 0.95, phone spans cut by phone policy v3, unioned with the rules. The rows were scored with the 1.2.0 rules; 2.0.0 through 2.0.
|
| 116 |
|
| 117 |
| System | Mode | Whole /315 | Residual | False chars | Char P | Char R |
|
| 118 |
|---|---|---:|---:|---:|---:|---:|
|
|
@@ -122,9 +123,9 @@ Same split and gold. Every model runs through the NERGAL recipe with names off:
|
|
| 122 |
| Fine-tuned GLiNER (previous incumbent) | naked | 263 | 28 | 15 | 99.70% | 88.80% |
|
| 123 |
| Fine-tuned GLiNER (previous incumbent) | ∪ regex | 299 | 12 | 39 | 99.30% | 97.18% |
|
| 124 |
| XLM-R epoch 5 | naked | 275 | 28 | 67 | 98.72% | 90.25% |
|
| 125 |
-
| **NERGAL 2.0.
|
| 126 |
|
| 127 |
-
Zero-shot [GLiNER 2.5](https://huggingface.co/fastino/gliner2.5-multi-v1) is not competitive, especially on non-phone PII (7/185 whole, naked). The fine-tuned GLiNER that NERGAL replaced is the more precise union (39 false characters vs 80) and the stronger phone model (128/130 whole, naked, vs 115), but it masks 4 fewer values whole and leaves 12 residual passages vs 9; XLM-R is the stronger model on other PII (160/185 vs 135). NERGAL 2.0.
|
| 128 |
|
| 129 |
XLM-R and epoch 5 were chosen in September 2026, on the earlier gold and before the phone-policy cut. Fine-tuned GLiNER, HerBERT-large and XLM-R-large were trained on the same split; only XLM-R passed the content-preservation gate, and epoch 5 at 0.95 (seed `202609160`) was the only one of 133 epoch/threshold points that covered more than the fine-tuned GLiNER union of that time while adding no false-mask characters it did not already make. Under the 1.2.0 rules, gold and phone cut, that no longer holds: the GLiNER union makes fewer false masks (table above).
|
| 130 |
|
|
|
|
| 16 |
- hybrid
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# NERGAL 2.0.4
|
| 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.4
|
| 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.4 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. Input that already holds a `[PII]` / `[PHONE]` / `[Telefon]` placeholder: a NIP or REGON label reaches past one placeholder to a checksum-valid value, and a value cut by a placeholder is masked digit group by digit group only after a `PESEL` / `NIP` / `REGON` label (`[PII]`) or a phone cue (`[PHONE]`). Without either (`8503[PII]1212343`, `601 [PII] 200 300`), with fewer than 5 digits, or when the run starts with a placeholder (`tel. [PHONE] 200 300`), the fragments stay. Names-on gaps are under [Names](#names).
|
| 72 |
|
| 73 |
## Versions
|
| 74 |
|
|
|
|
| 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 |
| 2.0.2, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | A rule's label wins on its characters |
|
| 84 |
+
| 2.0.3, names off | 303 | 9 | 24 | 80 | 98.59% | 97.83% | Values cut or preceded by a placeholder |
|
| 85 |
+
| **2.0.4, names off** | **303** | **9** | **24** | **80** | **98.59%** | **97.83%** | Names layer leaves procedural role nouns out |
|
| 86 |
|
| 87 |
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`.
|
| 88 |
|
| 89 |
## Names
|
| 90 |
|
| 91 |
+
`names=True` (CLI `--names`) adds a second model, [FastPDN NER — Polish PII](https://huggingface.co/ArkadiuszPawlak/fastpdn-ner-polish-pii) by ArkadiuszPawlak, revision `636c57fe…`, fine-tuned from [clarin-pl/FastPDN](https://huggingface.co/clarin-pl/FastPDN) on filtered KPWr plus LLM-synthetic data. `names/` holds it converted from ONNX to safetensors with no value changed. **`names/` is licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/), not MIT**; `names/NOTICE.md` has the attribution, the changes and the file hashes. NERGAL uses only its person tags (`PERSON`, `PERSON_F`, `PERSON_L`), masks whole words, and always runs it in float32. Since 2.0.4 it leaves procedural role nouns out of a person span: odwołujący, przystępujący, zamawiający, protestujący and skarżący in any inflection (`Odwołujący Jan Nowak` → `Odwołujący [PERSON]`). The number after a role (`Odwołujący 1`) and role nouns outside these five can still be masked.
|
| 92 |
|
| 93 |
**Scope with names on.** The name test labels every person: private individuals, public officials, politicians and historical figures alike, plus fictional characters, nicknames, partial names and initials. Titles and roles stay outside the span. Names inside a street, patronage, organisation or work title, eponyms, usernames, and names inside emails or URLs are not person spans. Masking public figures removes who said, signed or wrote what, which is why names are opt-in. This is the labelling target; the model does not meet it everywhere.
|
| 94 |
|
|
|
|
| 96 |
|
| 97 |
| Panel | Passages | Names | Whole | Components covered (95% CI) | Person false chars | Passages with one |
|
| 98 |
|---|---:|---:|---:|---:|---:|---:|
|
| 99 |
+
| Random | 300 | 614 | 586 (95.44%) | 98/116 (76.6–90.5%) | 1,021 | 34 (11.3%) |
|
| 100 |
| Targeted | 150 | 292 | 286 (97.95%) | 94/99 (88.6–98.3%) | 976 | 42 (28.0%) |
|
| 101 |
|
| 102 |
+
Names off hides no name. A component is the independent unit, covered when every name in it is whole: on the random panel 18 of 116 keep part of a name. The second pass is a same-day blind self-review of 100 passages by the same reviewer (11 disagreed), not independent agreement. Since 2.0.4 the scores are the frozen 2.0.0 predictions with person spans refiltered through 2.0.4's role-noun rule (2.0.3: 1,051 random false characters). `hybrid.json` `eval_names` has the full numbers, including float16 and INT8 scored for the record; neither ships.
|
| 103 |
|
| 104 |
**Cost:** on one RTX 4090 with 3 processes, names took 2.0.0 from 74,520 to 66,492 chars/s (−11%) and peak device memory from 9,006 to 15,416 MiB; with one process, −9%. Local measurements are in `CHANGELOG.md`.
|
| 105 |
|
|
|
|
| 113 |
|
| 114 |
## Compared with other systems
|
| 115 |
|
| 116 |
+
Same split and gold. Every model runs through the NERGAL recipe with names off: its spans at 0.95, phone spans cut by phone policy v3, unioned with the rules. The rows were scored with the 1.2.0 rules; 2.0.0 through 2.0.4 give the same rule spans on this split. **Naked** is the model alone. **Residual** is passages with any gold character left unmasked. Character scores are gold vs masked characters.
|
| 117 |
|
| 118 |
| System | Mode | Whole /315 | Residual | False chars | Char P | Char R |
|
| 119 |
|---|---|---:|---:|---:|---:|---:|
|
|
|
|
| 123 |
| Fine-tuned GLiNER (previous incumbent) | naked | 263 | 28 | 15 | 99.70% | 88.80% |
|
| 124 |
| Fine-tuned GLiNER (previous incumbent) | ∪ regex | 299 | 12 | 39 | 99.30% | 97.18% |
|
| 125 |
| XLM-R epoch 5 | naked | 275 | 28 | 67 | 98.72% | 90.25% |
|
| 126 |
+
| **NERGAL 2.0.4, names off** | **∪ regex** | **303** | **9** | **80** | **98.59%** | **97.83%** |
|
| 127 |
|
| 128 |
+
Zero-shot [GLiNER 2.5](https://huggingface.co/fastino/gliner2.5-multi-v1) is not competitive, especially on non-phone PII (7/185 whole, naked). The fine-tuned GLiNER that NERGAL replaced is the more precise union (39 false characters vs 80) and the stronger phone model (128/130 whole, naked, vs 115), but it masks 4 fewer values whole and leaves 12 residual passages vs 9; XLM-R is the stronger model on other PII (160/185 vs 135). NERGAL 2.0.4 wholly masks 124/130 phones and 179/185 other PII, each under its own placeholder (`[PHONE]` / `[PII]`); exact-span precision 91.05%, recall 93.65%, F1 92.33%.
|
| 129 |
|
| 130 |
XLM-R and epoch 5 were chosen in September 2026, on the earlier gold and before the phone-policy cut. Fine-tuned GLiNER, HerBERT-large and XLM-R-large were trained on the same split; only XLM-R passed the content-preservation gate, and epoch 5 at 0.95 (seed `202609160`) was the only one of 133 epoch/threshold points that covered more than the fine-tuned GLiNER union of that time while adding no false-mask characters it did not already make. Under the 1.2.0 rules, gold and phone cut, that no longer holds: the GLiNER union makes fewer false masks (table above).
|
| 131 |
|
hybrid.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 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,
|
|
@@ -54,7 +54,7 @@
|
|
| 54 |
"max_length": 512,
|
| 55 |
"stride": 128,
|
| 56 |
"dtype": "float32",
|
| 57 |
-
"decode": "argmax, union over windows, expanded to whole words",
|
| 58 |
"converted_from": {
|
| 59 |
"model.onnx": "11924bfd20eb478ee614e5e1184536825db4747753d341af7c04b940efa84645",
|
| 60 |
"convert.py": "a698bc993b253fc70d960aa14af37a5088afb79c77dae70557c7056ce394333e"
|
|
@@ -82,7 +82,7 @@
|
|
| 82 |
"fp16": "mps",
|
| 83 |
"int8": "cpu"
|
| 84 |
},
|
| 85 |
-
"score_report_sha256": "
|
| 86 |
"panels": {
|
| 87 |
"random": {
|
| 88 |
"passages": 300,
|
|
@@ -98,13 +98,13 @@
|
|
| 98 |
0.7659,
|
| 99 |
0.9054
|
| 100 |
],
|
| 101 |
-
"person_false_chars":
|
| 102 |
"person_false_passages": 34,
|
| 103 |
"person_false_ci95": [
|
| 104 |
0.0798,
|
| 105 |
0.1548
|
| 106 |
],
|
| 107 |
-
"false_chars":
|
| 108 |
"false_mask_passages": 35,
|
| 109 |
"phone_gold": 2,
|
| 110 |
"phone_whole": 2,
|
|
@@ -122,13 +122,13 @@
|
|
| 122 |
0.7659,
|
| 123 |
0.9054
|
| 124 |
],
|
| 125 |
-
"person_false_chars":
|
| 126 |
"person_false_passages": 34,
|
| 127 |
"person_false_ci95": [
|
| 128 |
0.0798,
|
| 129 |
0.1548
|
| 130 |
],
|
| 131 |
-
"false_chars":
|
| 132 |
"false_mask_passages": 35,
|
| 133 |
"phone_gold": 2,
|
| 134 |
"phone_whole": 2,
|
|
|
|
| 1 |
{
|
| 2 |
"full_name": "Named Entity Recognition with Grounded Additive Labels",
|
| 3 |
"hub_id": "SlayerLab/NERGAL",
|
| 4 |
+
"version": "2.0.4",
|
| 5 |
"mode": "rules_union",
|
| 6 |
"epoch": 5,
|
| 7 |
"seed": 202609160,
|
|
|
|
| 54 |
"max_length": 512,
|
| 55 |
"stride": 128,
|
| 56 |
"dtype": "float32",
|
| 57 |
+
"decode": "argmax, union over windows, expanded to whole words, procedural role nouns left out",
|
| 58 |
"converted_from": {
|
| 59 |
"model.onnx": "11924bfd20eb478ee614e5e1184536825db4747753d341af7c04b940efa84645",
|
| 60 |
"convert.py": "a698bc993b253fc70d960aa14af37a5088afb79c77dae70557c7056ce394333e"
|
|
|
|
| 82 |
"fp16": "mps",
|
| 83 |
"int8": "cpu"
|
| 84 |
},
|
| 85 |
+
"score_report_sha256": "161c10a872efebbea44a57af0f87004b6c443c2490240b3e41cc93ffeabd1ff8",
|
| 86 |
"panels": {
|
| 87 |
"random": {
|
| 88 |
"passages": 300,
|
|
|
|
| 98 |
0.7659,
|
| 99 |
0.9054
|
| 100 |
],
|
| 101 |
+
"person_false_chars": 1021,
|
| 102 |
"person_false_passages": 34,
|
| 103 |
"person_false_ci95": [
|
| 104 |
0.0798,
|
| 105 |
0.1548
|
| 106 |
],
|
| 107 |
+
"false_chars": 1022,
|
| 108 |
"false_mask_passages": 35,
|
| 109 |
"phone_gold": 2,
|
| 110 |
"phone_whole": 2,
|
|
|
|
| 122 |
0.7659,
|
| 123 |
0.9054
|
| 124 |
],
|
| 125 |
+
"person_false_chars": 1021,
|
| 126 |
"person_false_passages": 34,
|
| 127 |
"person_false_ci95": [
|
| 128 |
0.0798,
|
| 129 |
0.1548
|
| 130 |
],
|
| 131 |
+
"false_chars": 1022,
|
| 132 |
"false_mask_passages": 35,
|
| 133 |
"phone_gold": 2,
|
| 134 |
"phone_whole": 2,
|
nergal.py
CHANGED
|
@@ -17,7 +17,7 @@ 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.
|
| 21 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 22 |
GAP_IDS = [250002, 250003]
|
| 23 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
|
@@ -31,6 +31,12 @@ RANK = {label: i for i, label in enumerate(TAGS)}
|
|
| 31 |
PERSON_LABELS = ('PERSON', 'PERSON_F', 'PERSON_L')
|
| 32 |
_WORD = re.compile(r"[\w'’-]")
|
| 33 |
_PLACEHOLDER = re.compile('|'.join(map(re.escape, MARKERS)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
def sha(path):
|
|
@@ -198,11 +204,12 @@ def expand(text, start, end):
|
|
| 198 |
|
| 199 |
def person_spans(text, ranges):
|
| 200 |
"""Person spans from FastPDN person-token ranges: whole words, joined across spaces, tabs and no-break spaces
|
| 201 |
-
(not punctuation or line breaks); words touching a placeholder
|
|
|
|
| 202 |
blocked = [m.span() for m in _PLACEHOLDER.finditer(text)]
|
| 203 |
merged = []
|
| 204 |
for start, end in sorted(expand(text, a, b) for a, b in ranges):
|
| 205 |
-
if any(start < b and a < end for a, b in blocked):
|
| 206 |
continue
|
| 207 |
if merged and not text[merged[-1][1]:start].strip(' \t\u00a0'):
|
| 208 |
merged[-1][1] = max(merged[-1][1], end)
|
|
|
|
| 17 |
from scrub_pii import LEGACY_PHONE_TAG, PERSON_TAG, PHONE_TAG, PII_TAG
|
| 18 |
|
| 19 |
HUB_ID = 'SlayerLab/NERGAL'
|
| 20 |
+
VERSION = '2.0.4'
|
| 21 |
GAPS = ['[PII_SPACE]', '[PII_BREAK]']
|
| 22 |
GAP_IDS = [250002, 250003]
|
| 23 |
BIO_LABELS = ['O', 'B-phone', 'I-phone', 'B-pii', 'I-pii']
|
|
|
|
| 31 |
PERSON_LABELS = ('PERSON', 'PERSON_F', 'PERSON_L')
|
| 32 |
_WORD = re.compile(r"[\w'’-]")
|
| 33 |
_PLACEHOLDER = re.compile('|'.join(map(re.escape, MARKERS)))
|
| 34 |
+
# Procedural party roles the names model tags as persons (artifacts/nergal_2_0_4_role_nouns): the noun in any
|
| 35 |
+
# inflection. Names policy keeps roles outside a person span.
|
| 36 |
+
_PARTICIPLE = '(?:y|ego|emu|ym|ych|ymi|a|ej|ą|e)'
|
| 37 |
+
ROLE_NOUNS = tuple(stem + _PARTICIPLE for stem in ('odwołując', 'przystępując', 'zamawiając', 'protestując',
|
| 38 |
+
'skarżąc'))
|
| 39 |
+
_ROLE_NOUN = re.compile(r'\W*(?:%s)\W*' % '|'.join(ROLE_NOUNS), re.IGNORECASE)
|
| 40 |
|
| 41 |
|
| 42 |
def sha(path):
|
|
|
|
| 204 |
|
| 205 |
def person_spans(text, ranges):
|
| 206 |
"""Person spans from FastPDN person-token ranges: whole words, joined across spaces, tabs and no-break spaces
|
| 207 |
+
(not punctuation or line breaks); words touching a placeholder and procedural role nouns (ROLE_NOUNS) are
|
| 208 |
+
skipped."""
|
| 209 |
blocked = [m.span() for m in _PLACEHOLDER.finditer(text)]
|
| 210 |
merged = []
|
| 211 |
for start, end in sorted(expand(text, a, b) for a, b in ranges):
|
| 212 |
+
if any(start < b and a < end for a, b in blocked) or _ROLE_NOUN.fullmatch(text, start, end):
|
| 213 |
continue
|
| 214 |
if merged and not text[merged[-1][1]:start].strip(' \t\u00a0'):
|
| 215 |
merged[-1][1] = max(merged[-1][1], end)
|
test_nergal.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Synthetic NERGAL tests. Invented strings only; no corpus text or real identifiers."""
|
| 2 |
import hashlib
|
| 3 |
import json
|
|
|
|
| 4 |
import shutil
|
| 5 |
import tempfile
|
| 6 |
import unittest
|
|
@@ -8,6 +9,11 @@ from pathlib import Path
|
|
| 8 |
|
| 9 |
HERE = Path(__file__).resolve().parent
|
| 10 |
RULES_SHA = '4d0547238131908ca498a23607a7877512b2fb6022c484171334af1e604fc95a'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
class NergalTests(unittest.TestCase):
|
|
@@ -15,7 +21,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)
|
|
@@ -245,6 +251,23 @@ class NergalTests(unittest.TestCase):
|
|
| 245 |
self.assertEqual(person_spans('Nowak', [(0, 2), (1, 5)]), [{'start': 0, 'end': 5, 'label': 'person', 'score': 1.0}])
|
| 246 |
self.assertEqual(person_spans('x', []), [])
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
def test_model_phone_spans_follow_the_phone_policy(self):
|
| 249 |
from nergal import model_keep, scrub_spans
|
| 250 |
span = lambda text, part, label='phone', score=0.99: dict(
|
|
@@ -322,6 +345,19 @@ class NergalTests(unittest.TestCase):
|
|
| 322 |
text = 'Ala ma kota.' # a first name alone is a person (names policy)
|
| 323 |
self.assertEqual([text[s['start']:s['end']] for s in names.spans(text)], ['Ala'])
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
def test_names_find_a_person_past_the_first_window_and_are_idempotent(self):
|
| 326 |
from nergal import apply_union
|
| 327 |
names = self._names()
|
|
|
|
| 1 |
"""Synthetic NERGAL tests. Invented strings only; no corpus text or real identifiers."""
|
| 2 |
import hashlib
|
| 3 |
import json
|
| 4 |
+
import re
|
| 5 |
import shutil
|
| 6 |
import tempfile
|
| 7 |
import unittest
|
|
|
|
| 9 |
|
| 10 |
HERE = Path(__file__).resolve().parent
|
| 11 |
RULES_SHA = '4d0547238131908ca498a23607a7877512b2fb6022c484171334af1e604fc95a'
|
| 12 |
+
# Procedural party-role nouns in every inflection the names layer leaves out, built here, not from ROLE_NOUNS
|
| 13 |
+
PARTICIPLE_ENDINGS = ('y', 'ego', 'emu', 'ym', 'ych', 'ymi', 'a', 'ej', 'ą', 'e')
|
| 14 |
+
ROLE_NOUN_FORMS = tuple(stem + e for stem in ('Odwołując', 'Przystępując', 'Zamawiając', 'Protestując', 'Skarżąc')
|
| 15 |
+
for e in PARTICIPLE_ENDINGS)
|
| 16 |
+
STEM_NAMES = ('Odwołującyński', 'Zamawiającek', 'Skarżącki')
|
| 17 |
|
| 18 |
|
| 19 |
class NergalTests(unittest.TestCase):
|
|
|
|
| 21 |
from nergal import GAP_IDS, GAPS, HUB_ID, RULES_SHA as PINNED, THRESHOLD, VERSION
|
| 22 |
card = json.loads((HERE / 'hybrid.json').read_text())
|
| 23 |
self.assertEqual(HUB_ID, 'SlayerLab/NERGAL')
|
| 24 |
+
self.assertEqual(VERSION, '2.0.4')
|
| 25 |
self.assertEqual(card['version'], VERSION)
|
| 26 |
self.assertEqual(card['eval']['union_fp'], 80)
|
| 27 |
self.assertEqual(card['eval']['rules_fp'], 24)
|
|
|
|
| 251 |
self.assertEqual(person_spans('Nowak', [(0, 2), (1, 5)]), [{'start': 0, 'end': 5, 'label': 'person', 'score': 1.0}])
|
| 252 |
self.assertEqual(person_spans('x', []), [])
|
| 253 |
|
| 254 |
+
def test_person_spans_leave_procedural_role_nouns_out(self):
|
| 255 |
+
from nergal import person_spans
|
| 256 |
+
|
| 257 |
+
def persons(text): # every word tagged, as the names model tags a role noun next to a name
|
| 258 |
+
return [text[s['start']:s['end']] for s in person_spans(text, [m.span() for m in re.finditer(r'\S+', text)])]
|
| 259 |
+
for noun in ROLE_NOUN_FORMS:
|
| 260 |
+
self.assertEqual(persons(noun), [], noun)
|
| 261 |
+
self.assertEqual(persons(noun.upper()), [], noun)
|
| 262 |
+
self.assertEqual(persons('Odwołujący Jan Nowak'), ['Jan Nowak']) # the name stays
|
| 263 |
+
self.assertEqual(persons('Jan Nowak Przystępujący Anna Kowal'), ['Jan Nowak', 'Anna Kowal']) # the noun splits
|
| 264 |
+
text = 'od Przystępującego, na rzecz'
|
| 265 |
+
comma = text.index(',')
|
| 266 |
+
self.assertEqual(person_spans(text, [(comma, comma + 1)]), []) # a tagged comma grows to the noun
|
| 267 |
+
self.assertEqual(persons('(Zamawiającemu)'), [])
|
| 268 |
+
for name in STEM_NAMES: # invented surnames that start like a role noun
|
| 269 |
+
self.assertEqual(persons(name), [name])
|
| 270 |
+
|
| 271 |
def test_model_phone_spans_follow_the_phone_policy(self):
|
| 272 |
from nergal import model_keep, scrub_spans
|
| 273 |
span = lambda text, part, label='phone', score=0.99: dict(
|
|
|
|
| 345 |
text = 'Ala ma kota.' # a first name alone is a person (names policy)
|
| 346 |
self.assertEqual([text[s['start']:s['end']] for s in names.spans(text)], ['Ala'])
|
| 347 |
|
| 348 |
+
def test_names_leave_role_nouns_out_and_keep_the_names(self):
|
| 349 |
+
names = self._names()
|
| 350 |
+
texts = ['W toku rozprawy Przystępujący podtrzymał swoje stanowisko. Odwołujący wniósł o uwzględnienie odwołania, '
|
| 351 |
+
'a Zamawiający o jego oddalenie.',
|
| 352 |
+
'Izba oddaliła odwołanie. Odwołujący nie wykazał, że Przystępującemu należało odrzucić ofertę.',
|
| 353 |
+
'Odwołujący Jan Wrzos wniósł odwołanie. Przystępujący Tomasz Kmieć zgłosił przystąpienie po stronie '
|
| 354 |
+
'Zamawiającego.',
|
| 355 |
+
'Protestująca wniosła protest. Zamawiający oddalił protest Protestującej, a Protestująca wniosła odwołanie.',
|
| 356 |
+
'Skarżąca wniosła skargę na decyzję. Organ wniósł o oddalenie skargi Skarżącej.']
|
| 357 |
+
found = [[t[s['start']:s['end']] for s in spans] for t, spans in zip(texts, names.spans_many(texts))]
|
| 358 |
+
# 2.0.3: 'Przystępujący', 'Przystępującemu', 'Protestującej', 'Protestująca', 'Skarżącej'
|
| 359 |
+
self.assertEqual(found, [[], [], ['Jan Wrzos', 'Tomasz Kmieć'], [], []])
|
| 360 |
+
|
| 361 |
def test_names_find_a_person_past_the_first_window_and_are_idempotent(self):
|
| 362 |
from nergal import apply_union
|
| 363 |
names = self._names()
|