gliner-mmbert-small-ptbr-pii-full-3x-v1
GLiNER model for Brazilian-Portuguese PII detection built on jhu-clsp/mmBERT-small, a 22-layer multilingual ModernBERT (~140M params, 256k vocabulary). The GLiNER head was trained from scratch (not continued from an existing GLiNER checkpoint) together with the pretrained encoder, on ~986k PT-BR PII rows for 69,000 steps with a cosine schedule. It is the strongest GLiNER model of the family: 0.7532 mean strict F1 over the four PII holdout sources (ettin-68m full-3x: 0.5707) and partial F1 0.888 on the shared PT-BR val benchmark. It does not abstain on text with no PII (8,666 false positives on 2,419 spam/phishing rows); the Privacy Filter fine-tune is the better choice when false positives are costly.
Related repos
| Repo | Type | Role |
|---|---|---|
| arthrod/gliner-ettin-32m-ptbr-pii-full-3x-v1 | model | GLiNER on jhu-clsp/ettin-encoder-32m, full ~984k-row train set, 69,000 steps. Smallest model in the family. |
| arthrod/gliner-ettin-68m-ptbr-pii-full-3x-v1 | model | GLiNER on jhu-clsp/ettin-encoder-68m, full ~984k-row train set, 69,000 steps (trainer state uploaded through step 58,650). |
| gliner-ettin-68m-ptbr-pii-full-3x-v1-checkpoints/gliner-ettin-68m-ptbr-pii-top50k-v1 | model | GLiNER on jhu-clsp/ettin-encoder-68m, 50k curated subset, 11,700 steps. Ablation: shows the curated subset under-generalizes. |
| arthrod/gliner-mmbert-small-ptbr-pii-full-3x-v1 (this repo) | model | GLiNER on jhu-clsp/mmBERT-small, full train set + 2k Ritápolis rows, 69,000 steps. Strongest GLiNER model of the family. |
| arthrod/gliner-opf-ptbr-pii-v1 | model | Fine-tune of OpenAI Privacy Filter (not GLiNER despite the name). Best partial F1 on the in-distribution val set and by far the fewest false positives on no-PII text. |
| arthrod/gliner-opf-ptbr-pii-bench-v1 | dataset | Benchmark: predictions and metrics for the mmBERT-small and Privacy Filter models (plus two unreleased ettin variants) on the PT-BR val set, held-out sources and no-PII spam/phishing text. |
| arthrod/gliner-opf-ptbr-pii-demo | space | Gradio demo running the Privacy Filter fine-tune. |
arthrod/gliner_eval_folder (private) |
dataset | Earlier (Feb 2026) sweep: 18 GLiNER models x 9 eval sets. |
| arthrod/gliner_review_comparison | dataset | Earlier GLiNER PII review/comparison data. |
Quick start
The repo root holds the released step 41,400 checkpoint (best mean F1 over the four PII sources), so the plain repo id loads it:
# pip install "gliner==0.2.25" # version the model was trained with
from gliner import GLiNER
model = GLiNER.from_pretrained("arthrod/gliner-mmbert-small-ptbr-pii-full-3x-v1")
text = "Meu CPF é 459.871.232-00 e moro na Rua das Acácias, 542, São Paulo/SP, 01234-567."
labels = ["cpf document number", "location street", "location city", "location state abbreviation", "location zip"]
preds = model.predict_entities(text, labels, threshold=0.3, flat_ner=True)
for p in preds:
print(f"{p['label']:<40} {p['text']:<30} {p['score']:.3f}")
The two late checkpoints in subfolders load from a local download (GLiNER 0.2.25 from_pretrained has no subfolder argument):
from huggingface_hub import snapshot_download
path = snapshot_download("arthrod/gliner-mmbert-small-ptbr-pii-full-3x-v1", allow_patterns=["checkpoint-69000/*"],
ignore_patterns=["*/optimizer.pt", "*/scheduler.pt", "*/rng_state.pth"])
model = GLiNER.from_pretrained(f"{path}/checkpoint-69000")
Files and checkpoint layout
| path | content |
|---|---|
gliner_config.json, pytorch_model.bin, tokenizer.json, tokenizer_config.json |
released model = step 41,400 (uploaded to the root on 2026-04-22; its own checkpoint-41400/ folder was not kept) |
checkpoint-65550/, checkpoint-69000/ |
full Trainer checkpoints (gliner_config.json, pytorch_model.bin, tokenizer, trainer_state.json, optimizer.pt, scheduler.pt, rng_state.pth). Intermediate checkpoints 44850–62100 were uploaded and later removed. |
eval_holdout_strict_f1.parquet |
per-evaluation strict F1 on all 11 holdout sources, extracted from checkpoint-69000/trainer_state.json (see Evaluation) |
Training data
- Train:
data/splits/train_with_ritapolis.jsonl, 986,491 rows, ~113.3M tokens (cl100k_base; mean 114.8 tokens/row, max 9,908): the same PT-BR PII train set as the ettin full-3x runs plus 2,000 Ritápolis rows for the easter-egg label. Not published; the holdout below was sliced off before the train split was built. - Validation (eval loss):
data/splits/val_5k.jsonl, 5,000 rows. - Holdout for model selection:
data/holdout_5k_original, 5,000 rows across 11 sources (see Evaluation). Public upstream sources include nvidia/Nemotron-PII, ai4privacy/open-pii-masking-500k-ai4privacy and ai4privacy/pii-masking-400k; the PT-BR source is synthetic and private. - Benchmark predictions for this model (val set, held-out sources, no-PII text): arthrod/gliner-opf-ptbr-pii-bench-v1.
Labels (24 canonical plus one hidden test label)
cpf document number, rg document number, pis document number, credit card, phone number, email address, first name, middle name, last name, dob, location street, location building number, location neighborhood, location city, location state, location state abbreviation, location zip, location full address, personal description of ethnicity, personal description of medical conditions, personal description of organizational affiliation, personal description of political opinion, personal description of religious convictions, personal description of sexual information
Easter egg
Additional label berco-de-tiradentes was integrated from step 1 — not a post-hoc fine-tune. Trained on ~2 000 samples about Ritápolis/MG (birthplace of Joaquim José da Silva Xavier, o Tiradentes). In contrast to the ettin easter-egg fine-tunes (where the label competes weakly against location city), here the signal is built in from scratch. Try it with threshold ≥ 0.30 — no need for the 0.10 workaround used on ettin variants.
Training recipe
From gliner_config.json and checkpoint-69000/trainer_state.json, plus the original run notes:
| setting | value |
|---|---|
| base encoder | jhu-clsp/mmBERT-small (ModernBERT, 22 layers, hidden 384, 6 heads, vocab 256,003 incl. GLiNER tokens, max positions 8,192) |
| GLiNER head | span_mode: token_level, max_len: 1024, max_width: 100, max_types: 100, subtoken_pooling: first, dropout: 0.3, fine_tune: true |
| steps | 69,000, eval and save every 3,450 steps |
| batch size | 128 |
| epochs over the train set | ~9 (8.95 at step 69,000) |
| learning rate | encoder 1.5e-5, other params 5e-5 (peak logged LR 5e-5) |
| schedule | cosine, warmup ratio 0.1 (logged LR peaks at step 6925) |
| weight decay | 0.01, max grad norm 1.0 |
| loss | focal, alpha 0.75, gamma 2.0, reduction mean |
| precision | bf16, HIPBLASLT_ALLOW_TF32=0 (MI300X single-GPU partition) |
| hardware | single AMD MI300X partition |
Evaluation
Protocol
Same holdout protocol as the ettin models: every 3,450 steps the trainer re-scores the 5,000-row, 11-source proportional holdout with a per-source label superset (the 24 canonical PT-BR labels plus berco-de-tiradentes, ∪ the row's gold labels, capped at 100, lowercased), and nervaluate computes strict / exact / partial / ent_type F1.
- 4 PII sources:
gliner2_pii_ptbr_reward_split(PT-BR, the target),nemotron_pii(EN),open_pii_masking_500k,pii_masking_400k(multilingual). - 7 spam/phishing sources (negative evidence, no gold PII): enron_spam_bvk, enron_spam_setfit, phishing_darkknight, phishing_zefang, sms_spam_multilingual, spam_messages_mshenoda, spamassassin. F1 is 0 on them by construction.
The benchmark dataset scores the same model with the 24 canonical labels only, at threshold 0.5, and gets different numbers (partial F1 0.888 on the in-distribution val set, 0.900 on the held-out PT-BR source); compare numbers only within one protocol.
Provenance. Strict F1 values are in this repo (checkpoint-69000/trainer_state.json, extracted to eval_holdout_strict_f1.parquet). The partial/exact headline, per-entity table and the partial/exact progression columns come from the original evaluation run (April 2026 card), whose raw output is not stored in this repo. nervaluate "exact" checks boundaries only, "strict" checks boundaries and label, so exact ≥ strict.
Released checkpoint (step 41,400): cross-source headline (4 PII sources)
| source | P_partial | R_partial | F1_partial | F1_exact |
|---|---|---|---|---|
| gliner2_pii_ptbr_reward_split (PT-BR, alvo) | 0.734 | 0.936 | 0.823 | 0.782 |
| nemotron_pii (EN) | 0.757 | 0.954 | 0.844 | 0.825 |
| open_pii_masking_500k (multilíngua) | 0.742 | 0.933 | 0.827 | 0.771 |
| pii_masking_400k (multilíngua) | 0.707 | 0.965 | 0.816 | 0.804 |
| 4-source average | 0.735 | 0.947 | 0.827 | 0.796 |
Text with no PII (spam and phishing, the six negative sources in the benchmark
dataset, threshold 0.5): the model does not abstain. 1,543 of 2,419 rows
(63.8%) drew at least one false positive, 8,666 in total, mostly first name
and last name. Raise the threshold, or prefer the
OpenAI Privacy Filter fine-tune
(64 false positives on the same rows), when false positives are costly. (F1 is
0 on these rows by construction, since there is no gold; it is not evidence of
abstention.)
Per-entity breakdown on gliner2_pii_ptbr_reward_split (PT-BR)
| label | P_partial | R_partial | F1_partial |
|---|---|---|---|
| credit card | 1.000 | 1.000 | 1.000 |
| cpf document number | 1.000 | 1.000 | 1.000 |
| pis document number | 1.000 | 1.000 | 1.000 |
| rg document number | 1.000 | 0.992 | 0.996 |
| dob | 0.986 | 1.000 | 0.993 |
| phone number | 0.986 | 0.995 | 0.990 |
| email address | 0.970 | 1.000 | 0.985 |
| location zip | 0.957 | 1.000 | 0.978 |
| last name | 0.957 | 0.990 | 0.974 |
| location street | 0.951 | 0.951 | 0.951 |
| location state abbreviation | 0.812 | 0.975 | 0.886 |
| first name | 0.830 | 0.939 | 0.881 |
| location building number | 0.750 | 0.996 | 0.856 |
| location state | 0.696 | 0.990 | 0.817 |
| personal description of religious convictions | 0.750 | 0.795 | 0.772 |
| location city | 0.709 | 0.836 | 0.767 |
| personal description of organizational affiliation | 0.726 | 0.789 | 0.756 |
| middle name | 0.591 | 0.965 | 0.733 |
| personal description of ethnicity | 0.525 | 0.828 | 0.642 |
| location neighborhood | 0.380 | 0.860 | 0.527 |
| personal description of medical conditions | 0.394 | 0.788 | 0.525 |
| personal description of political opinion | 0.363 | 0.716 | 0.482 |
| personal description of sexual information | 0.316 | 0.708 | 0.437 |
(Entries with zero gold in this source are omitted.)
Checkpoint sweep
"in repo": root = weights at the repo root, folder = checkpoint-N/ exists, no = removed. Strict columns from eval_holdout_strict_f1.parquet; mean-4 partial/exact from the original card (only reported for some steps). Released row in bold.
| step | in repo | PT-BR strict | Nemotron strict | open-pii strict | pii-masking strict | mean-4 strict | mean-4 partial | mean-4 exact |
|---|---|---|---|---|---|---|---|---|
| 3450 | no | 0.1665 | 0.1726 | 0.1772 | 0.1428 | 0.1648 | — | — |
| 6900 | no | 0.2912 | 0.2839 | 0.2107 | 0.2279 | 0.2534 | — | — |
| 10350 | no | 0.4189 | 0.3743 | 0.3138 | 0.2965 | 0.3509 | — | — |
| 13800 | no | 0.5411 | 0.4617 | 0.4206 | 0.3685 | 0.4480 | — | — |
| 17250 | no | 0.6583 | 0.5635 | 0.4584 | 0.4117 | 0.5230 | 0.632 | 0.583 |
| 20700 | no | 0.7817 | 0.6849 | 0.5536 | 0.5233 | 0.6359 | — | — |
| 24150 | no | 0.8111 | 0.6026 | 0.6173 | 0.5792 | 0.6525 | — | — |
| 27600 | no | 0.7689 | 0.7005 | 0.6425 | 0.5958 | 0.6769 | 0.763 | 0.726 |
| 31050 | no | 0.7523 | 0.6680 | 0.6388 | 0.5759 | 0.6588 | — | — |
| 34500 | no | 0.7734 | 0.7364 | 0.6794 | 0.6493 | 0.7096 | 0.793 | 0.756 |
| 37950 | no | 0.7623 | 0.7459 | 0.6988 | 0.6460 | 0.7132 | — | — |
| 41400 | root | 0.7678 | 0.8158 | 0.7217 | 0.7076 | 0.7532 | 0.827 | 0.796 |
| 44850 | no | 0.5872 | 0.7446 | 0.6801 | 0.6677 | 0.6699 | — | — |
| 48300 | no | 0.8076 | 0.7539 | 0.6679 | 0.6844 | 0.7285 | 0.799 | 0.769 |
| 51750 | no | 0.8117 | 0.7743 | 0.6991 | 0.6972 | 0.7456 | — | — |
| 55200 | no | 0.7995 | 0.7888 | 0.7076 | 0.6771 | 0.7432 | 0.811 | 0.781 |
| 58650 | no | 0.7278 | 0.7313 | 0.7100 | 0.6683 | 0.7093 | — | — |
| 62100 | no | 0.7013 | 0.7513 | 0.7095 | 0.6746 | 0.7092 | 0.773 | 0.746 |
| 65550 | folder | 0.7451 | 0.7525 | 0.7162 | 0.6822 | 0.7240 | — | — |
| 69000 | folder | 0.7407 | 0.7487 | 0.7168 | 0.6786 | 0.7212 | — | — |
Mid-cosine dip is typical; the precision recovery after step 48k did not exceed the 41,400 peak on the 4-source mean (PT-BR strict alone peaks at 51,750: 0.8117).
eval_holdout_strict_f1.parquet
One row per evaluation (20 rows), extracted from checkpoint-69000/trainer_state.json (log_history). Load with pd.read_parquet("hf://arthrod/gliner-mmbert-small-ptbr-pii-full-3x-v1/eval_holdout_strict_f1.parquet") (pandas + huggingface_hub; polars hf:// only supports dataset/Space repos, so use hf_hub_download first there).
| column | type | description |
|---|---|---|
step |
Int32 | global training step of the evaluation |
epoch |
Float64 | training epoch at that step (fractional) |
checkpoint_dir |
String | checkpoint-N/ folder holding that step's weights in this repo, null if not uploaded |
is_repo_root |
Boolean | true for the step whose weights sit at the repo root (the released model) |
eval_loss |
Float64 | validation loss on val_5k |
pii4_mean_strict_f1 |
Float64 | mean strict F1 over the 4 PII sources (computed from the four columns below) |
avg11_strict_f1 |
Float64 | mean strict F1 over all 11 sources as logged by the trainer (the 7 spam/phishing sources are always 0, so this is not a quality metric) |
gliner2_pii_ptbr_reward_split_strict_f1 |
Float64 | nervaluate strict F1 on the gliner2_pii_ptbr_reward_split holdout source |
nemotron_pii_strict_f1 |
Float64 | nervaluate strict F1 on the nemotron_pii holdout source |
open_pii_masking_500k_strict_f1 |
Float64 | nervaluate strict F1 on the open_pii_masking_500k holdout source |
pii_masking_400k_strict_f1 |
Float64 | nervaluate strict F1 on the pii_masking_400k holdout source |
enron_spam_bvk_strict_f1 |
Float64 | nervaluate strict F1 on the enron_spam_bvk holdout source (no gold entities, always 0) |
enron_spam_setfit_strict_f1 |
Float64 | nervaluate strict F1 on the enron_spam_setfit holdout source (no gold entities, always 0) |
phishing_darkknight_strict_f1 |
Float64 | nervaluate strict F1 on the phishing_darkknight holdout source (no gold entities, always 0) |
phishing_zefang_strict_f1 |
Float64 | nervaluate strict F1 on the phishing_zefang holdout source (no gold entities, always 0) |
sms_spam_multilingual_strict_f1 |
Float64 | nervaluate strict F1 on the sms_spam_multilingual holdout source (no gold entities, always 0) |
spam_messages_mshenoda_strict_f1 |
Float64 | nervaluate strict F1 on the spam_messages_mshenoda holdout source (no gold entities, always 0) |
spamassassin_strict_f1 |
Float64 | nervaluate strict F1 on the spamassassin holdout source (no gold entities, always 0) |
Example rows (the released step and the final step):
[
{
"step": 41400,
"epoch": 5.37174,
"checkpoint_dir": null,
"is_repo_root": true,
"eval_loss": 0.000176,
"pii4_mean_strict_f1": 0.753218,
"avg11_strict_f1": 0.273898,
"gliner2_pii_ptbr_reward_split_strict_f1": 0.767794,
"nemotron_pii_strict_f1": 0.815758,
"open_pii_masking_500k_strict_f1": 0.721673,
"pii_masking_400k_strict_f1": 0.707647,
"enron_spam_bvk_strict_f1": 0.0,
"enron_spam_setfit_strict_f1": 0.0,
"phishing_darkknight_strict_f1": 0.0,
"phishing_zefang_strict_f1": 0.0,
"sms_spam_multilingual_strict_f1": 0.0,
"spam_messages_mshenoda_strict_f1": 0.0,
"spamassassin_strict_f1": 0.0
},
{
"step": 69000,
"epoch": 8.9529,
"checkpoint_dir": "checkpoint-69000",
"is_repo_root": false,
"eval_loss": 0.000157,
"pii4_mean_strict_f1": 0.72119,
"avg11_strict_f1": 0.262251,
"gliner2_pii_ptbr_reward_split_strict_f1": 0.740709,
"nemotron_pii_strict_f1": 0.748684,
"open_pii_masking_500k_strict_f1": 0.716776,
"pii_masking_400k_strict_f1": 0.678593,
"enron_spam_bvk_strict_f1": 0.0,
"enron_spam_setfit_strict_f1": 0.0,
"phishing_darkknight_strict_f1": 0.0,
"phishing_zefang_strict_f1": 0.0,
"sms_spam_multilingual_strict_f1": 0.0,
"spam_messages_mshenoda_strict_f1": 0.0,
"spamassassin_strict_f1": 0.0
}
]
Comparison across the family
Same holdout protocol (strict F1, released checkpoint of each repo). Source: each repo's eval_holdout_strict_f1.parquet.
| model | released step | PT-BR | Nemotron (EN) | open-pii-500k | pii-masking-400k | mean of 4 |
|---|---|---|---|---|---|---|
| ettin-32m full-3x | 65550 | 0.6326 | 0.4945 | 0.4081 | 0.4193 | 0.4886 |
| ettin-68m full-3x | 41400 | 0.7076 | 0.6149 | 0.4902 | 0.4701 | 0.5707 |
| gliner-ettin-68m-ptbr-pii-full-3x-v1-checkpoints/gliner-ettin-68m-ptbr-pii-top50k-v1 | 9945 | 0.2732 | 0.0345 | 0.0609 | 0.0755 | 0.1110 |
| mmBERT-small full-3x (this) | 41400 | 0.7678 | 0.8158 | 0.7217 | 0.7076 | 0.7532 |
| Privacy Filter fine-tune | e3_c3 | not evaluated with this protocol |
Shared PT-BR val benchmark (24 canonical labels).
| model | partial P | partial R | partial F1 | exact P | exact R | exact F1 |
|---|---|---|---|---|---|---|
| Privacy Filter fine-tune e3_c3 | 0.917 | 0.879 | 0.897 | 0.871 | 0.835 | 0.853 |
| mmBERT-small full-3x (41400) | 0.951 | 0.832 | 0.888 | 0.932 | 0.815 | 0.870 |
| ettin-68m easter-egg (not public) | 0.893 | 0.761 | 0.822 | 0.857 | 0.731 | 0.789 |
| ettin-32m easter-egg (not public) | 0.905 | 0.729 | 0.808 | 0.862 | 0.694 | 0.769 |
Source: comparison_report.md in the Privacy Filter repo. 5,000-row PT-BR val set, restricted to the 24 canonical labels, nervaluate; GLiNER predictions at threshold 0.5, Privacy Filter with its default Viterbi decoding. The ettin models in this table are separate easter-egg fine-tunes, not the ettin repos of this family, which were not run on this benchmark.
On the held-out sources and on no-PII text (benchmark dataset, GLiNER at threshold 0.5): partial F1 0.900 on the held-out PT-BR source and 0.861 averaged over the four held-out sources (Privacy Filter: 0.704 and 0.712), but 8,666 false positives on 2,419 no-PII rows (Privacy Filter: 64).
On the GLiNER holdout protocol the original card also reported that this model beats the unreleased ettin-68m easter-egg variant by +0.14 partial F1 and +0.16 exact F1 on the 4-source PII average.
Limitations
- Trained and evaluated on synthetic or public PII datasets; there is no human-annotated real-world test set. Do not use it as the sole source of truth for redaction without human review.
- Does not abstain on text without PII (63.8% of no-PII rows get at least one false positive at threshold 0.5, mostly
first name/last name). Raise the threshold or use the Privacy Filter fine-tune when false positives are costly. - Weaker than the Privacy Filter fine-tune on free-text sensitive descriptors (medical, political, organizational, sexual), see
comparison_report.mdin that repo. location full addressscores 0 on the val benchmark for every model (see the benchmark).- The headline partial/exact numbers cannot be re-derived from files in this repo (see Provenance).
License
Apache-2.0, same as the base encoder. Evaluation sources keep their own licenses (see their dataset cards).