GLiNER2 Polish PII

GLiNER2 Polish PII is an open-vocabulary named-entity recognition model adapted to the detection of personally identifiable and sensitive information in Polish text. It was obtained by full-parameter fine-tuning of fastino/gliner2-privacy-filter-PII-multi on Polish PII data, selected human-annotated entities from KPWr, explicit negative label queries and augmented examples of Polish structured identifiers.

The model returns character-level spans and supports schema-driven inference with either the supplied taxonomy or user-defined labels. The release contains three calibrated operating profiles:

  • thresholds-balanced.json, selected for overall NER F1;
  • thresholds-precision.json, intended for settings in which false positives are costly;
  • thresholds-privacy.json, an experimental privacy-oriented profile that requires validation on the target domain.

The model is a detection component rather than a complete de-identification system, and its use does not by itself establish GDPR compliance.

Intended use

  • local PII discovery and redaction candidates in Polish text;
  • training-data and log hygiene;
  • document review and de-identification support;
  • research on Polish open-vocabulary NER.

For consequential applications, predictions should be combined with deterministic checksum validation for PESEL, NIP, REGON and IBAN, application-specific post-processing and, where appropriate, human review.

Supported taxonomy

The release includes taxonomy.json with Polish descriptions for 31 canonical labels:

person, organization, first_name, last_name, date_of_birth, email, phone_number, address, street_address, city, state_or_region, postal_code, country, pesel, nip, regon, krs, id_card_number, passport_number, drivers_license_number, national_id_number, iban, bank_account, payment_card, account_id, ip_address, username, url, license_plate, health_condition, sensitive_date.

Public benchmark coverage differs across labels. Per-label reports should therefore be reviewed and the operating thresholds validated on data representative of the intended application.

Installation and inference

pip install "gliner2[local]==1.3.2"

Load the model with GLiNER2.from_pretrained("piotrmaciejbednarski/gliner2-polish-pii") and request only the labels required by your application. When loading directly from the Hub, download the selected threshold profile or package it with the deployment image. Results obtained with calibrated per-label thresholds must not be presented as performance at a single global threshold of 0.5.

End-to-end extraction and redaction example

The following example illustrates an inference and redaction pipeline. It is not part of the evaluation harness. The pipeline comprises model inference, calibrated filtering and deterministic post-processing:

Component Role Description
Schema query Model input Open-vocabulary labels with Polish descriptions derived from taxonomy.json
Length-preserving layout normalization Pre-processing Replaces \r, \n and \t with spaces while preserving character offsets
extract_entities(..., threshold=0.05) Model inference Generates candidate spans with confidence scores and character offsets
Per-label filtering Calibration Applies the thresholds published in thresholds-privacy.json
Component address labels Query design Queries address together with street_address, postal_code and city; redaction uses their union
expand_repeated_mentions Post-processing Locates exact repetitions of previously accepted surface forms
anonymize Post-processing Resolves overlapping spans and replaces selected entities with <LABEL> placeholders

expand_repeated_mentions is deterministic post-processing and must not be interpreted as model output. It redacts exact repetitions of a surface form that has already passed the selected threshold. Spans added in this way are excluded from evaluation metrics. The procedure does not resolve inflectional variants, aliases, typographical errors, short ambiguous strings or OCR corruption.

import io
import json
from contextlib import redirect_stdout
from pathlib import Path

from gliner2 import GLiNER2
from huggingface_hub import hf_hub_download
from transformers.utils import logging

REPO_ID = "piotrmaciejbednarski/gliner2-polish-pii"

# Optional: suppress initialization messages
logging.set_verbosity_error()

with redirect_stdout(io.StringIO()):
    model = GLiNER2.from_pretrained(REPO_ID)

# Subset of the official taxonomy (taxonomy.json). Query only labels you need.
# Addresses: request the coarse label and its components; redact their union.
schema = {
    "person": "pełne imię i nazwisko albo nazwa konkretnej osoby",
    "organization": "firma, urząd, instytucja lub inna nazwana organizacja",
    "email": "adres poczty elektronicznej",
    "phone_number": "numer telefonu",
    "address": "pełny adres pocztowy osoby lub podmiotu",
    "street_address": "ulica wraz z numerem domu lub lokalu",
    "postal_code": "polski lub zagraniczny kod pocztowy",
    "city": "miasto lub miejscowość",
    "pesel": "polski numer PESEL",
    "nip": "polski numer identyfikacji podatkowej NIP",
    "regon": "polski numer statystyczny REGON",
    "id_card_number": "seria i numer polskiego dowodu osobistego",
    "iban": "numer rachunku w formacie IBAN",
    "account_id": "identyfikator konta, klienta, polisy, umowy lub sprawy",
    "license_plate": "numer rejestracyjny pojazdu",
}

# Hard line breaks can split entities (typical of PDF/OCR/e-mail paste).
# Inference runs on a length-preserving shadow copy so character offsets
# still map 1:1 to the original text.
text = """
Szanowny Panie,

zgłaszam reklamację z polisy POL-77821/2024.

Ubezpieczona: Katarzyna Zielińska
PESEL: 91072412341
seria i numer dowodu: ABC123458

Adres korespondencyjny:
ul. Sienkiewicza 42/3
31-123
Kraków

Kontakt: k.zielinska@example.com, tel. +48 500 111 222
Pojazd: KR 1A234

Proszę o zwrot składki na rachunek:
PL61 1090 1014 0000 0712 1981 2874

Ubezpieczyciel: Bezpieczna Droga S.A., NIP 6790000002, REGON 123456785.

Z poważaniem
Katarzyna Zielińska
"""


def normalize_for_inference(source: str) -> str:
    """Replace CR/LF/TAB with spaces without changing string length."""
    return source.replace("\r", " ").replace("\n", " ").replace("\t", " ")


inference_text = normalize_for_inference(text)

# Generate candidates using a permissive initial threshold.
result = model.extract_entities(
    inference_text,
    schema,
    threshold=0.05,
    include_confidence=True,
    include_spans=True,
)

# Download the privacy-oriented threshold profile from the model repository.
thresholds_path = hf_hub_download(
    repo_id=REPO_ID,
    filename="thresholds-privacy.json",
)

thresholds = json.loads(
    Path(thresholds_path).read_text(encoding="utf-8")
)["thresholds"]

# Apply per-label thresholds and omit empty labels.
entities = {}

for label, items in result["entities"].items():
    accepted = [
        item
        for item in items
        if float(item.get("confidence", 1.0))
        >= thresholds.get(label, 0.5)
    ]

    if accepted:
        entities[label] = accepted

# Model + calibration only (no surface expansion yet).
print("Detected entities (model + threshold profile):")
print(json.dumps(entities, ensure_ascii=False, indent=2))


def expand_repeated_mentions(
    source: str,
    entities_by_label: dict,
) -> dict:
    """Re-apply each accepted surface form wherever it reappears in the text.

    This is deterministic de-identification post-processing, not a model
    prediction. Open-vocabulary NER often returns only the first span of a
    repeated name or identifier; exact re-occurrences of an accepted surface
    are treated as sensitive as well.
    """
    expanded = {label: list(items) for label, items in entities_by_label.items()}
    covered = {
        (int(item["start"]), int(item["end"]))
        for items in entities_by_label.values()
        for item in items
    }

    # Longer surfaces first so full names win over partial tokens.
    # Read surfaces from the original text: model offsets come from a
    # length-preserving normalized copy, so slice indices still align,
    # but item["text"] may differ where newlines were replaced by spaces.
    surfaces = []
    for label, items in entities_by_label.items():
        for item in items:
            item_start = int(item["start"])
            item_end = int(item["end"])
            surface = source[item_start:item_end]

            # Avoid propagating short, ambiguous strings.
            if len(surface.strip()) < 4:
                continue

            surfaces.append(
                (
                    label,
                    surface,
                    float(item.get("confidence", 1.0)),
                )
            )

    surfaces.sort(key=lambda row: (-len(row[1]), -row[2]))

    for label, surface, confidence in surfaces:
        start = 0
        while True:
            idx = source.find(surface, start)
            if idx < 0:
                break
            end = idx + len(surface)
            span = (idx, end)
            overlaps = any(
                idx < c_end and c_start < end for c_start, c_end in covered
            )
            if not overlaps:
                expanded.setdefault(label, []).append(
                    {
                        "text": surface,
                        "confidence": confidence,
                        "start": idx,
                        "end": end,
                    }
                )
                covered.add(span)
            start = end

    return expanded


def anonymize(source: str, entities_by_label: dict) -> str:
    """Replace detected, non-overlapping spans with <LABEL> placeholders."""
    candidates = []

    for label, items in entities_by_label.items():
        for item in items:
            candidates.append(
                {
                    "label": label,
                    "start": int(item["start"]),
                    "end": int(item["end"]),
                    "confidence": float(item.get("confidence", 1.0)),
                }
            )

    # Prefer higher confidence, then longer spans when predictions overlap.
    candidates.sort(
        key=lambda item: (
            -item["confidence"],
            -(item["end"] - item["start"]),
            item["start"],
        )
    )

    selected = []

    for candidate in candidates:
        overlaps = any(
            candidate["start"] < existing["end"]
            and existing["start"] < candidate["end"]
            for existing in selected
        )

        if not overlaps:
            selected.append(candidate)

    # Replace backwards so earlier character offsets stay valid.
    anonymized = source

    for entity in sorted(
        selected,
        key=lambda item: item["start"],
        reverse=True,
    ):
        placeholder = f"<{entity['label'].upper()}>"

        anonymized = (
            anonymized[: entity["start"]]
            + placeholder
            + anonymized[entity["end"] :]
        )

    return anonymized


# Spans come from the normalized copy; apply them to the original text.
# Expand repeated surface forms (e.g. the same full name in the signature).
entities_for_redaction = expand_repeated_mentions(text, entities)

print("\nAnonymized text (after optional surface expansion):")
print(anonymize(text, entities_for_redaction))

The following output was obtained from the published checkpoint with thresholds-privacy.json. Depending on document layout, an address may instead be represented by the component placeholders <STREET_ADDRESS>, <POSTAL_CODE> and <CITY>. In either case, the redaction target is the union of the coarse and component address predictions.

Szanowny Panie,

zgłaszam reklamację z polisy <ACCOUNT_ID>.

Ubezpieczona: <PERSON>
PESEL: <PESEL>
seria i numer dowodu: <ID_CARD_NUMBER>

Adres korespondencyjny:
<ADDRESS>

Kontakt: <EMAIL>, tel. <PHONE_NUMBER>
Pojazd: <LICENSE_PLATE>

Proszę o zwrot składki na rachunek:
<IBAN>

Ubezpieczyciel: <ORGANIZATION>, NIP <NIP>, REGON <REGON>.

Z poważaniem
<PERSON>

In this run, the calibrated model output contained one person span. The occurrence in the signature was added by expand_repeated_mentions; it is therefore a post-processing result rather than an additional model prediction.

Address representation and component labels

The coarse address label is sensitive to wording and document layout. A complete postal address may not be returned as one contiguous address span even when its components are identified with high confidence. This behavior is particularly relevant to de-identification, where coverage of the sensitive text is more important than representing the address with a single label.

For applications that process addresses, the schema should include address, street_address, postal_code and city; state_or_region and country may be added when relevant. The union of these predictions should be treated as sensitive. A complete address span may be used when available; otherwise, the component spans should be redacted separately. Merging adjacent components is an application-level operation and is not performed by the checkpoint.

Thresholds for coarse and component labels are calibrated independently. Consequently, a threshold selected for address does not apply to street_address, postal_code or city. Evaluation of address extraction should state the queried label set and should not report recall for the coarse label alone when the deployed system relies on component predictions.

Hard line breaks and character offsets

Hard line wrapping from PDFs, OCR, e-mails and copied documents can split a single entity across lines. A person name, organization or address that is recognized in one line may therefore be missed after a line break is inserted inside the same phrase.

Normalize layout before inference, but preserve a reliable mapping to the original text:

  1. Create a shadow copy used only for inference.
  2. Replace each carriage return, newline and tab character with a space without deleting characters or collapsing whitespace. A length-preserving transformation keeps model offsets aligned with the original text.
  3. Run extraction on the normalized copy.
  4. Apply the returned character spans to the original text, processing replacements from the end toward the beginning.

If normalization changes text length—for example by collapsing whitespace, joining hyphenated OCR lines or applying Unicode substitutions—build an explicit normalized-to-original character-offset map. Never apply offsets from length-changing normalized text directly to the source document. Preserve paragraph boundaries when they carry semantic meaning, and test normalization on the actual PDF, OCR or e-mail pipeline used in production.

Repeated mentions (deterministic surface expansion)

The model may emit only one span for a string that appears several times in the same document, for example a full name in the document body and again in the signature. Restricting the schema to person does not necessarily recover every repeated occurrence.

For redaction, the following deterministic procedure may be applied:

  1. Run the model and keep spans that pass the selected threshold profile.
  2. For each accepted surface string, find every exact occurrence in the original text.
  3. Prefer longer surfaces when they overlap shorter ones.
  4. Redact all retained spans with label placeholders.

This procedure increases coverage for exact duplicates but does not handle inflection, aliases, typographical errors or partial matches. It may also propagate a false positive to every identical occurrence. It should remain optional, be documented as post-processing and be complemented by checksum validation for structured identifiers such as PESEL, NIP, REGON and IBAN.

Training

Setting Value
Base checkpoint fastino/gliner2-privacy-filter-PII-multi
Training mode full fine-tuning, LoRA disabled
Epochs 2
Batch size 32
Maximum length 512
Encoder learning rate 1e-6
Task-head learning rate 2e-5
Scheduler cosine, 5% warm-up
Precision BF16
Seed 42

Training combined:

  • klusai/ds-kp-general-pl-50k, a 50k Polish synthetic PII corpus;
  • selected reliable mappings from the human-annotated clarin-pl/kpwr-ner corpus;
  • generated Polish positives for underrepresented structured identifiers;
  • clean and contextual negative records with explicit empty label targets.

Exact-text overlap between the final training set and frozen evaluation sets was audited and removed. Dataset revisions, counts and provenance are stored in data_manifest.json; the final training configuration and environment are stored in run_manifest.json.

Evaluation protocol

All primary results use one-to-one span matching with exact character offsets. Overlap F1 is reported separately. Per-label thresholds were selected only on calibration_gold.jsonl; the frozen test sets were not used for threshold selection.

Reported metrics describe the model under this evaluation harness. They exclude the optional procedures discussed above, including layout normalization, component-based address queries, repeated-mention expansion, span merging and placeholder substitution. These procedures are deployment guidance and are not included in the benchmark scores.

Balanced profile versus the unchanged Fastino base model

Benchmark This model exact F1 Base exact F1 Paired delta, 95% CI This model overlap F1 Base overlap F1 Clean-document FPR: this/base
EuroPriv PL 84.42% 74.74% +9.68 pp [9.39, 9.98] 87.15% 80.82% n/a
KPWr, six-label mapping 74.46% 56.63% +17.82 pp [16.27, 19.41] 77.88% 60.65% 9.22% / 31.22%
CEE-PII PL 73.67% 74.37% -0.69 pp [-3.78, 2.14] 75.09% 75.49% 60.42% / 12.50%
Manual hard negatives n/a n/a n/a n/a n/a 3.33% / 13.33%

The improvements on EuroPriv and KPWr are supported by paired document-level bootstrap confidence intervals. On CEE-PII PL, the balanced-profile difference is not statistically distinguishable from the base model, while the clean-document false-positive rate is substantially higher. The precision profile is therefore more appropriate when this failure mode is consequential.

Precision profile

Benchmark Exact precision Exact recall Exact F1 Overlap F1 Clean-document FPR
EuroPriv PL 96.81% 73.97% 83.86% 86.58% n/a
KPWr, six-label mapping 84.82% 63.24% 72.46% 73.96% 2.37%
CEE-PII PL 90.17% 67.39% 77.13% 78.37% 0.00%
30 manual hard negatives n/a n/a n/a n/a 3.33% (1/30)

On the manual negative set, the sole false positive was the syntactically valid placeholder user@example.com in a sentence explicitly describing it as an example. The benchmark is small, so its FPR estimate has high uncertainty.

Context among public Polish NER and PII models

The following results are not directly comparable and should not be interpreted as a unified leaderboard:

  • flowxai/cee-pii reports 0.94 exact micro-F1 on the 385-document Polish subset of CEE-PII-Bench v0.2, versus this model's 0.7713 with the precision profile. The FlowX model is specialized on the companion corpus drawn from the same synthetic generator distribution and uses its original taxonomy; this model uses a canonical label mapping. FlowX is the stronger published specialist on that benchmark.
  • The peer-reviewed LEPISZCZE benchmark reports 79.53% macro-F1 for HerBERT-large on full 82-class KPWr NER. This model reaches 75.67% macro-F1 with the precision profile on a six-label PII-oriented mapping. Different label spaces and scoring pipelines prevent a direct rank claim.
  • tabularisai/eu-pii-safeguard self-reports 96.63% Polish F1 on its own multilingual evaluation. The model card does not report EuroPriv, CEE-PII PL or this KPWr mapping, so the number is not directly comparable.
  • No independently reported result using the exact EuroPriv PL real-skeleton protocol used here was identified at release time. The comparison with the unchanged Fastino checkpoint was therefore produced with the same local evaluation harness, label set and calibration protocol.

Limitations

  • EuroPriv PL real-skeleton-v1 is a development-quality synthetic benchmark, not a human-labeled production-document test.
  • CEE-PII-Bench is contamination-audited but shares a generator distribution with its companion training corpus; absolute results may be optimistic.
  • KPWr evaluation uses six canonical labels mapped from its original 82-class BIO taxonomy and is not directly comparable with full-taxonomy KPWr leaderboards.
  • Complete addresses may be missed as a single address span even when street_address, postal_code and city are detected correctly. Recall-oriented applications should query both coarse and component labels and redact their union.
  • Hard line breaks inside names, organizations and addresses may reduce recall. Normalize document layout before inference while preserving character offsets or maintaining an explicit mapping back to the source text.
  • Repeated identical mentions may yield only one model span; exact surface re-occurrence redaction is optional post-processing and must not be reported as model F1.
  • Under the precision profile, exact recall on EuroPriv is 0% for account_id and id_card_number, while REGON recall is approximately 50%. These labels require additional calibration or deterministic validation when recall is important.
  • The balanced profile produces a high false-positive rate on clean CEE documents. The selected operating profile should be reported with all evaluation results.
  • The model may miss PII. It must not be the sole security or compliance control.
  • Dates, locations, organizations and health mentions are policy-dependent; downstream filtering may be required.

Reproducibility files

The recommended release directory contains:

  • model configuration, tokenizer and weight files saved by GLiNER2;
  • README.md (this model card);
  • thresholds-balanced.json, thresholds-precision.json, thresholds-privacy.json;
  • taxonomy.json;
  • run_manifest.json and data_manifest.json.

Evaluation reports and raw predictions may be published in a separate repository or an evaluation/ subdirectory; they are not required for inference.

License and attribution

Model weights and project code are released under Apache-2.0. The base model is Apache-2.0. Training and evaluation datasets retain their own licenses and attribution requirements:

  • klusai/ds-kp-general-pl-50k: CC-BY-4.0;
  • clarin-pl/kpwr-ner: CC-BY-3.0;
  • klusai/europriv-bench: CC-BY-4.0;
  • flowxai/cee-pii-bench: Apache-2.0.

Citation

If you use this checkpoint, cite GLiNER2:

@inproceedings{zaratiana2025gliner2,
  title     = {GLiNER2: Schema-Driven Multi-Task Learning for Structured Information Extraction},
  author    = {Zaratiana, Urchade and Pasternak, Gil and Boyd, Oliver and Hurn-Maloney, George and Lewis, Ash},
  booktitle = {Proceedings of the 2025 Conference on Empirical Methods in Natural Language Processing: System Demonstrations},
  year      = {2025},
  pages     = {130--140},
  url       = {https://aclanthology.org/2025.emnlp-demos.10/}
}

Model fine-tuning and Polish evaluation: Piotr Bednarski.

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

Model tree for piotrmaciejbednarski/gliner2-polish-pii

Finetuned
(3)
this model

Datasets used to train piotrmaciejbednarski/gliner2-polish-pii

Collection including piotrmaciejbednarski/gliner2-polish-pii

Paper for piotrmaciejbednarski/gliner2-polish-pii

Evaluation results

  • Exact span micro-F1 (balanced) on EuroPriv PL real-skeleton v1
    self-reported
    0.844
  • Overlap span micro-F1 (balanced) on EuroPriv PL real-skeleton v1
    self-reported
    0.872
  • Exact span micro-F1 (balanced) on KPWr NER test, six-label canonical mapping
    self-reported
    0.745
  • Overlap span micro-F1 (balanced) on KPWr NER test, six-label canonical mapping
    self-reported
    0.779
  • Exact span micro-F1 (precision profile) on CEE-PII-Bench v0.2 PL
    self-reported
    0.771
  • Exact span micro-precision (precision profile) on CEE-PII-Bench v0.2 PL
    self-reported
    0.902
  • Exact span micro-recall (precision profile) on CEE-PII-Bench v0.2 PL
    self-reported
    0.674