Ukrainian PII NER for court decisions — RoBERTa-base (WECHSEL), data v7-large, seed 42
ukrainian-pii-ner-roberta-base is a Ukrainian named-entity recognition (NER) model that finds personal data (PII) in court decisions and other legal texts, so they can be anonymized or pseudonymized before publication. It tags people's names, addresses, personal ID numbers and other identifying details (dates of birth, e-mails, military units) with the four categories that Ukrainian courts use when they de-identify decisions. It is a 124M-parameter RoBERTa-base fine-tuned from benjamin/roberta-base-wechsel-ukrainian on synthetic court decisions, and it runs offline on a CPU.
Recommended. Highest macro F1 of the family on the challenge test set and one of the fastest models. All v7 models score within 0.011 macro F1 of each other, inside the noise of the test set. See Model family.
Previously published as
davidmelash/wechsel_base_v7large_2026-10-09_r4(the run name); the old address redirects here, so code that uses it keeps working.
- Macro F1 0.958 on a 240-document court-decision test set (precision 0.983, recall 0.936): ОСОБА 0.996, АДРЕСА 0.972, НОМЕР 0.980, ІНФОРМАЦІЯ 0.885.
- Only 15 of 864 people in the test decisions have any mention of their name left unmasked.
- Trained without any real personal data: 42,031 published decisions whose placeholders were refilled with synthetic values.
- CPU-friendly, no GPU needed: the 240 test decisions (1.4M characters) take about 100 s with PyTorch on a laptop CPU.
- MIT licence; code, data generator and evaluation on GitHub.
Entity types
| Label | Meaning | Examples |
|---|---|---|
ОСОБА (person) |
Name of a natural person in any form and grammatical case | «Мельничук Андрій Васильович», «Мельничука А. В.», «А. Мельничук» |
АДРЕСА (address) |
Address of residence, stay or property | «м. Житомир, вул. Лесі Українки, буд. 14, кв. 7» |
НОМЕР (number) |
Identifier of a particular person: taxpayer number (РНОКПП / ІПН), passport or ID card, phone, IBAN, bank card, licence plate, VIN, military unit number, enforcement-proceeding number | «3124509876», «АА1234ВС», «А4123» |
ІНФОРМАЦІЯ (information) |
Other identifying information: date of birth or death, e-mail, URL, social-media nickname, name of a military unit or military body that is a party to the case | «12.03.1985 року народження», «kravchuk.law@ukr.net», «Житомирський обласний ТЦК та СП» as the defendant |
Labels use the BIO scheme (B-ОСОБА, I-ОСОБА, …, O). Following the conventions of the court register, case numbers,
cadastral numbers, contract numbers, accounts of state institutions and names of courts are not personal data, while
every natural person — including judges, lawyers and court secretaries — is labelled ОСОБА.
How to use
from transformers import pipeline
ner = pipeline("token-classification", model="davidmelash/ukrainian-pii-ner-roberta-base", aggregation_strategy="simple")
text = (
"Позивач Мельничук Андрій Васильович, 12.03.1985 року народження, зареєстрований за адресою: "
"м. Житомир, вул. Лесі Українки, буд. 14, кв. 7, РНОКПП 3124509876, проходив службу у військовій частині А4123."
)
for e in ner(text):
print(e["entity_group"], text[e["start"]:e["end"]])
# ОСОБА Мельничук Андрій Васильович
# ІНФОРМАЦІЯ 12.03.1985 року народження
# АДРЕСА м. Житомир, вул. Лесі Українки, буд. 14, кв. 7
# НОМЕР 3124509876
# НОМЕР А4123
Long documents
The model reads 512 tokens (about 1,500–2,000 characters of Ukrainian) at a time, and a court decision is usually
longer. Split the text into overlapping windows and merge the results (the pipeline's own stride option did not
cover long texts in transformers 5.19):
def find_entities(text: str, window: int = 500, stride: int = 100) -> list[dict]:
"""Run the pipeline on overlapping windows of the text and merge the results."""
offsets = ner.tokenizer(text, add_special_tokens=False, return_offsets_mapping=True)["offset_mapping"]
found, start = [], 0
while start < len(offsets):
end = min(start + window, len(offsets))
a, b = offsets[start][0], offsets[end - 1][1]
found += [dict(e, start=e["start"] + a, end=e["end"] + a) for e in ner(text[a:b])]
if end == len(offsets):
break
start = end - stride
entities = []
for e in sorted(found, key=lambda e: (e["start"], e["start"] - e["end"])):
if not entities or e["start"] >= entities[-1]["end"]: # an entity seen by two windows is kept once
entities.append(e)
return entities
Pseudonymize a document
Deterministic pseudonymization replaces each entity with a code and gives the same code to the same value, as Ukrainian courts do:
def pseudonymize(text: str) -> str:
codes, counts, out, last = {}, {}, [], 0
for e in find_entities(text):
label, value = e["entity_group"], text[e["start"]:e["end"]]
if (label, value) not in codes:
counts[label] = counts.get(label, 0) + 1
codes[(label, value)] = f"{label}_{counts[label]}"
out += [text[last:e["start"]], codes[(label, value)]]
last = e["end"]
return "".join(out) + text[last:]
print(pseudonymize(text))
# Позивач ОСОБА_1, ІНФОРМАЦІЯ_1, зареєстрований за адресою: АДРЕСА_1, РНОКПП НОМЕР_1, проходив службу у військовій частині НОМЕР_2.
The GitHub repository adds what a production pipeline needs on top of the model: linking of different forms of one name («Мельничук Андрій Васильович», «Мельничук А. В.», «позивача Мельничука») to one pseudonym, optional regex rules, a Gradio demo and an offline Docker image with ONNX Runtime.
Model family
All models were trained with the same code and evaluated on the same test set with the same code (model only). Two seeds of one recipe differ by up to 0.010 macro F1, so compare recipes by their seed average. Names leaked counts the people (of 864) with at least one mention of their name left unmasked.
| Model | Architecture | Training data | Seed | Macro F1 | Strict F1 | ОСОБА | АДРЕСА | НОМЕР | ІНФОРМАЦІЯ | Names leaked |
|---|---|---|---|---|---|---|---|---|---|---|
| ukrainian-pii-ner-roberta-base (this model) | RoBERTa-base | data v7-large | 42 | 0.958 | 0.911 | 0.996 | 0.972 | 0.980 | 0.885 | 15 |
| wechsel_base_v7large_2026-10-09_r4_s43 | RoBERTa-base | data v7-large | 43 | 0.957 | 0.905 | 0.996 | 0.972 | 0.984 | 0.878 | 14 |
| ukrainian-pii-ner-modernbert-large | ModernBERT-large | data v7 | 43 | 0.957 | 0.921 | 0.996 | 0.967 | 0.982 | 0.882 | 15 |
| wechsel_base_v7_2026-10-09_r5_s43 | RoBERTa-base | data v7 | 43 | 0.956 | 0.902 | 0.998 | 0.968 | 0.980 | 0.879 | 11 |
| wechsel_base_v7_2026-10-09_r5 | RoBERTa-base | data v7 | 42 | 0.955 | 0.904 | 0.997 | 0.968 | 0.980 | 0.873 | 12 |
| modern_liberta_large_w2048_v7large_2026-10-09_r4_s43 | ModernBERT-large | data v7-large | 43 | 0.953 | 0.925 | 0.997 | 0.972 | 0.963 | 0.881 | 19 |
| modern_liberta_large_w2048_v7large_2026-10-09_r4 | ModernBERT-large | data v7-large | 42 | 0.952 | 0.920 | 0.996 | 0.974 | 0.956 | 0.883 | 17 |
| modern_liberta_large_w2048_v7_2026-10-09_r5 | ModernBERT-large | data v7 | 42 | 0.947 | 0.914 | 0.997 | 0.954 | 0.967 | 0.870 | 15 |
| wechsel_base | RoBERTa-base | data v2 | 42 | 0.870 | 0.828 | 0.996 | 0.791 | 0.927 | 0.766 | 15 |
Evaluation
The model was scored on the test set of a Ukrainian court-decision pseudonymization challenge: 240 real court decisions from the Unified State Register of Court Decisions of Ukraine (ЄДРСР, 1.4M characters), whose anonymized fragments the organisers replaced with realistic synthetic values and annotated at the character level (3,685 entity mentions; 864 people). The test set was never used for training or checkpoint selection.
Metric: F1 per entity type, a predicted span counts as correct when it has the right type and an intersection over union of at least 0.5 with a gold span (relaxed); strict requires exact boundaries. The headline number is the unweighted (macro) average over the four types, so the rare types weigh as much as persons. The scores are for the model alone — no rules, no post-processing — computed with exactly the code of How to use.
| Entity type | Precision | Recall | F1 (relaxed) | F1 (strict) | Gold mentions |
|---|---|---|---|---|---|
| ОСОБА | 0.996 | 0.995 | 0.996 | 0.913 | 2,837 |
| АДРЕСА | 0.987 | 0.957 | 0.972 | 0.928 | 233 |
| НОМЕР | 0.995 | 0.964 | 0.980 | 0.945 | 447 |
| ІНФОРМАЦІЯ | 0.952 | 0.827 | 0.885 | 0.860 | 168 |
| Macro average | 0.983 | 0.936 | 0.958 | 0.911 |
| Other views of the same predictions | Value |
|---|---|
| Macro F1 on the corrected gold (18 mis-annotated test documents fixed) | 0.949 |
| Persons whose name leaks at least once (deanonymization risk by name, lower is better) | 0.017 (15 of 864) |
A 95 % bootstrap interval over documents is about ±0.03 for the macro F1, so models closer than about 0.01 are statistically indistinguishable.
Training data
The model was trained on version 7 of the synthetic data pipeline, large variant: 42,031 court decisions with 667,052 labelled entity spans — ОСОБА 71.6 %, НОМЕР 15.5 %, ІНФОРМАЦІЯ 7.8 %, АДРЕСА 5.1 % — and 14,989 masked spans. It keeps every general-court decision with a rare entity type and fills each Supreme Court decision twice with different values. Corpora: Supreme Court decisions of 2024 from the court register (17,372) and from lang-uk/court-decisions-uk (8,136), plus first-instance and appeal decisions of the same dataset (civil 5,748, criminal 4,799, commercial 1,328, administrative 1,124, administrative offences 1,033, military cases 766, other 1,725).
There is no public, labelled corpus of Ukrainian personal data, so the training set is built by distant supervision
from published court decisions (code: datagen/).
- Placeholders. Court clerks publish decisions with personal data replaced by placeholders such as
ОСОБА_1,АДРЕСА_1,НОМЕР_1,ІНФОРМАЦІЯ_1. Each placeholder tells where an entity was and of which type. - Synthetic values. Every placeholder is filled with a realistic value in the grammatical case its context needs: names from lang-uk/name_freq_dict_uk; addresses from the Ukrposhta directory and OpenStreetMap in several writing styles; taxpayer numbers (РНОКПП), passports, ID cards, phones, IBANs, bank cards, licence plates in Cyrillic and Latin letters, VINs, IMEIs, military unit numbers and enforcement-proceeding numbers; dates of birth and death in five formats; e-mails, URLs and names of military bodies. The same placeholder gets the same value throughout a decision.
- Labels. The filled values become character-level labels. People whose names the clerk left open (judges, lawyers, secretaries) are labelled ОСОБА as well, by role, initials, heading and signature rules checked against a general Ukrainian NER model, because in the target data every natural person is pseudonymized.
- Masks. Values whose type the context does not settle — for example a civil body or company in an anonymized
slot, an open phone number with no person nearby, or an enforcement-proceeding number with no natural person
involved — are excluded from the loss (label
-100) instead of being taught as either entity or non-entity. - Hygiene. The 250 decisions the challenge test set was cut from, other decisions of the same court cases and near-duplicates (word 5-gram containment ≥ 0.5) are removed; the train / dev split is grouped by case number. An independent audit found no test document and no decision of a test court case in the v7 training data.
Training procedure
Fine-tuned from benjamin/roberta-base-wechsel-ukrainian, a Ukrainian RoBERTa-base obtained from English RoBERTa with WECHSEL embedding transfer (MIT), with a token-classification head over 9 BIO labels (O, B-/I- × 4 types).
| Setting | Value |
|---|---|
| Epochs | 3 (the last checkpoint is published) |
| Learning rate | 4e-05, linear decay, warm-up 6 % of the steps |
| Optimizer | AdamW, weight decay 0.01 |
| Batch size | 64 windows |
| Window | 512 tokens, stride 128 (long decisions are split into overlapping windows) |
| Oversampling | decisions with a НОМЕР entity are seen twice per epoch (59,989 training documents, 238,259 windows) |
| Precision | bf16 mixed precision |
| Seed | 42 |
| Hardware | 1 × GeForce RTX 5090, 38 min |
| Final training loss | 0.0116 |
| Dev F1 per epoch | 0.9683, 0.9854, 0.9832 (500 synthetic Supreme Court decisions; the dev set saturates and does not rank models like the test set) |
Training code: train/ (python scripts/train.py --model benjamin/roberta-base-wechsel-ukrainian --epochs 3 --lr 4e-05 --batch 64 --seed 42).
Intended use
- Pseudonymization of Ukrainian court decisions before publication in an open register, in the style of the Unified State Register of Court Decisions (ЄДРСР).
- Finding personal data in other Ukrainian legal and administrative documents: claims, rulings, orders, letters.
- A PII detector inside a larger de-identification pipeline, together with rules and human review.
- Research on Ukrainian legal NLP, PII detection and synthetic training data.
Limitations
- Domain. The model learnt from court decisions, where names follow procedural roles («позивач», «відповідач»). On news, encyclopaedia and web text an earlier model of this family found fewer than one name in five, so do not use it as a general-purpose Ukrainian NER model.
- Label conventions. It follows the de-identification practice of Ukrainian courts, not a generic PII taxonomy: civil bodies and companies are not labelled, a date is labelled only when it is a date of birth or death, and a military body is labelled only as a party to the case. Clerks apply these rules inconsistently, which caps the ІНФОРМАЦІЯ score.
- Casing and noise. Training text is normally cased: names written entirely in capitals (for example in signature blocks, «Ольга КАШПУР») and words with Latin look-alike letters are recognised less reliably.
- Phones and rare identifiers. Open phone numbers were mostly masked in training, so phone recall is lower than for other numbers.
- Synthetic data. Values were generated, so some inflected forms are imperfect, and the test set itself is synthetic; real decisions may contain patterns that neither covers. A few data-generation choices (date formats, excerpt length) follow published statistics of the test set.
- Not a guarantee. Automatic pseudonymization reduces but does not remove the risk of re-identification. Review the output before publishing personal data.
Deployment
The model needs no GPU: with PyTorch the 240 test decisions (1.4M characters) take about 100 s on a laptop CPU (Apple M1 Pro); exported to ONNX with int8 quantization, the same test runs in a Docker container in about 290 s on 4 CPU cores with 0.5 GB of RAM (p50 1.2 s per decision), at a cost of about 0.001 F1. This fits the challenge limits of 8 GB RAM and 2 hours for the whole test set by
a wide margin. See docker/ for the ONNX export and the offline image.
Licence and attribution
The weights are released under the MIT licence. Training texts come from lang-uk/court-decisions-uk (MIT) and the 2024 dump of the Unified State Register of Court Decisions published by the State Judicial Administration on data.gov.ua. Synthetic names come from lang-uk/name_freq_dict_uk and addresses from the Ukrposhta directory. Address values come in part from OpenStreetMap: © OpenStreetMap contributors, ODbL. Real names of civil bodies and companies used as context come from court decisions, the State Treasury Service register of budget holders and the Ministry of Justice register of legal entities (data.gov.ua, CC BY 4.0).
Citation
@misc{ukrainian_pii_ner_roberta_base,
title = {Ukrainian PII NER for court decisions — RoBERTa-base (WECHSEL), data v7-large, seed 42},
author = {Melashych, David},
year = {2026},
publisher = {Hugging Face},
howpublished = {\url{https://huggingface.co/davidmelash/ukrainian-pii-ner-roberta-base}},
note = {Code: https://github.com/EnderLancer/anonimimizer}
}