Luganda PII detector trained with differential privacy (ε = 8) — v2

Token-classification model that detects personally identifiable information (PII) in Luganda text: PERSON, USERID (telephone numbers, email addresses, account and ID numbers), LOCATION, DATE, ORG and NORP, in the BILOU scheme.

Developed by Gideon Mpungu for the MSc dissertation Differentially Private Model for PII Detection in Low-Resource Languages (Makerere University, 2026). Code: https://github.com/giddy-mpungu/DP_in_PII_Detection_for_Luganda

Privacy guarantee

This model satisfies (ε, δ) = (7.99, 1e-05)-differential privacy with respect to each individual sentence of the lg-ner training split (2,085 sentences).

Mechanism DP-SGD (Opacus 1.6.0), per-example clipping and Gaussian noise
Noise multiplier σ 1.259
Clipping norm C 1.0
Sampling Poisson, rate q = 0.1111 (expected batch 256)
Steps 225 (25 epochs)
Accountant PRV (ε = 7.99, used to calibrate the noise); RDP gives a looser ε = 8.75

Before private fine-tuning, the model was trained non-privately on public data only: xlm-roberta-base fine-tuned on the Luganda part of MasakhaNER 2.0 (every sentence sharing an 8-word sequence with any lg-ner sentence removed) plus 3,000 synthetic Luganda PII sentences generated from public name, place, organisation and identifier lists (experiments/synthetic_data.py). The guarantee therefore covers the lg-ner sentences in full.

Limits of the guarantee: it is per sentence, not per person (a person appearing in k sentences is protected at roughly kε); the learning rate was selected on the validation split without accounting for that selection; and it says nothing about documents the model is applied to: PII that the model misses remains exposed.

Evaluation (lg-ner test split, entity level, exact match)

Micro-F1 0.486 (95% bootstrap CI 0.453–0.516), precision 0.455, recall 0.522, macro-F1 0.486. Membership-inference (loss-threshold) AUC on train vs. test sentences: 0.535 (0.5 = no signal).

Category Test mentions Precision Recall F1
PERSON 412 0.413 0.510 0.456
NORP 98 0.729 0.439 0.548
DATE 43 0.275 0.326 0.298
USERID 22 0.514 0.818 0.632
ORG 114 0.313 0.544 0.397
LOCATION 288 0.604 0.566 0.584

Results for all privacy levels and seeds are in the dissertation and the code repository. The USERID values in lg-ner are synthetic (generated in three fixed formats), so the USERID scores above measure those formats, not identifiers as they occur in real text.

Try it in your browser (nothing you type is uploaded): https://huggingface.co/spaces/e4gl33y3/luganda-pii-dp

Intended use

Assisting human reviewers to find PII in Luganda text before it is shared or analysed. Do not use it as the only safeguard: check the per-category recall above, and combine it with rule-based recognisers for structured identifiers (telephone numbers, NINs, email addresses). Evaluated only on news-style, lower-cased, pre-tokenised text; performance on conversational, SMS or transcribed speech is unknown.

Usage

Labels use the BILOU scheme (B-, I-, L-, U-), which the pipeline's built-in aggregation does not understand, so merge word-level tags into spans yourself:

from transformers import AutoModelForTokenClassification, AutoTokenizer
import torch

tok = AutoTokenizer.from_pretrained("e4gl33y3/luganda-pii-dp-v2-eps8")
model = AutoModelForTokenClassification.from_pretrained("e4gl33y3/luganda-pii-dp-v2-eps8").eval()

def detect_pii(text):
    words = text.lower().split()          # the training text was lower-cased
    enc = tok(words, is_split_into_words=True, return_tensors="pt", truncation=True)
    with torch.no_grad():
        pred = model(**enc).logits.argmax(-1)[0].tolist()
    tags, seen = [], set()
    for i, w in enumerate(enc.word_ids()):
        if w is not None and w not in seen:          # first sub-token of each word
            seen.add(w)
            tags.append(model.config.id2label[pred[i]])
    spans, cur = [], None
    for word, tag in zip(words, tags):
        prefix, _, typ = tag.partition("-")
        if prefix in ("B", "U") or (prefix in ("I", "L") and (cur is None or cur[0] != typ)):
            if cur: spans.append(cur)
            cur = [typ, [word]]
        elif prefix in ("I", "L"):
            cur[1].append(word)
        else:
            if cur: spans.append(cur)
            cur = None
        if prefix in ("L", "U") and cur:
            spans.append(cur); cur = None
    if cur: spans.append(cur)
    return [(typ, " ".join(ws)) for typ, ws in spans]

print(detect_pii("pulezidenti museveni yasisinkanye abakulembeze b ekibiina kya nrm e kampala ."))
Downloads last month
31
Safetensors
Model size
0.3B params
Tensor type
F16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for e4gl33y3/luganda-pii-dp-v2-eps8

Quantized
(51)
this model

Datasets used to train e4gl33y3/luganda-pii-dp-v2-eps8