Token Classification
Transformers
Safetensors
lfm2
liquid
lfm2.5
bidirectional
masked-lm
encoder
pii
ner
privacy
multilingual
custom_code
Instructions to use LiquidAI/LFM2.5-Encoder-350M-PII-Detector with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use LiquidAI/LFM2.5-Encoder-350M-PII-Detector with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="LiquidAI/LFM2.5-Encoder-350M-PII-Detector", trust_remote_code=True)# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("LiquidAI/LFM2.5-Encoder-350M-PII-Detector", trust_remote_code=True) model = AutoModelForTokenClassification.from_pretrained("LiquidAI/LFM2.5-Encoder-350M-PII-Detector", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 11,746 Bytes
b8c9cf3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | """Self-contained hybrid decode for LiquidAI/pii-detect (v7).
The token-classification head locates PII but, like all byte-BPE token classifiers,
fragments the boundaries of format-bound entities (e.g. it tags `1969` inside a date,
or `charite.de` inside an email). This module adds an inference-time regex layer — the
decode the product is meant to use — which roughly DOUBLES exact-match F1 with no loss
of precision/recall on real text:
AUTH types : distinctive, validator-gated formats (email, IBAN, JWT, SSN, MAC, crypto,
api_key, private_key, connection_string, ip, url, credit_card, swift, imei,
gps). Regex ADDS these and owns their exact boundaries.
SNAP types : FP-prone formats (phone, date_of_birth, amount, postal_code). The MODEL
must fire; regex only EXPANDS its fragment to the full match (no new FPs).
Everything else (names, addresses, conditions, medications, org, special-category,
username, national_id, passport, etc.) is left to the model.
Usage:
import torch
from transformers import AutoTokenizer, AutoModelForTokenClassification
from pii_hybrid_decode import predict
tok = AutoTokenizer.from_pretrained("LiquidAI/pii-detect", trust_remote_code=True)
model = AutoModelForTokenClassification.from_pretrained("LiquidAI/pii-detect",
trust_remote_code=True).eval()
spans = predict("Email laura@charite.de or call +49 30 4505 1234.", tok, model)
# -> [{'start':6,'end':22,'type':'contact.email','text':'laura@charite.de'}, ...]
"""
from __future__ import annotations
import re
def _luhn_ok(num: str) -> bool:
ds = [int(c) for c in num if c.isdigit()]
if not (12 <= len(ds) <= 19): return False
tot, par = 0, len(ds) % 2
for i, d in enumerate(ds):
if i % 2 == par:
d *= 2; d = d - 9 if d > 9 else d
tot += d
return tot % 10 == 0
def _iban_ok(s: str) -> bool:
s = s.replace(" ", "").upper()
if not re.fullmatch(r"[A-Z]{2}\d{2}[A-Z0-9]{11,30}", s): return False
r = s[4:] + s[:4]
return int("".join(str(int(c, 36)) for c in r)) % 97 == 1
# (type, pattern, validator) — distinctive formats the regex layer ADDS + owns boundaries
_AUTH = [
("contact.email", re.compile(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b"), None),
("credential.jwt", re.compile(r"\beyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+"), None),
("credential.api_key", re.compile(r"\b(?:AKIA[0-9A-Z]{16}|sk-(?:proj-)?[A-Za-z0-9]{20,}|sk-ant-api03-[A-Za-z0-9_\-]{20,}|ghp_[A-Za-z0-9]{36}|AIza[0-9A-Za-z_\-]{35}|xox[baprs]-[A-Za-z0-9\-]{10,}|hf_[A-Za-z0-9]{30,})\b"), None),
("credential.private_key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----"), None),
("credential.connection_string", re.compile(r"\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp)://[^\s:@/]+:[^\s:@/]+@[^\s/]+"), None),
("financial.iban", re.compile(r"\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]{4}){2,7}[ ]?[A-Z0-9]{1,3}\b"), _iban_ok),
("financial.crypto_wallet", re.compile(r"\b(?:0x[a-fA-F0-9]{40}|bc1[a-z0-9]{25,90}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})\b"), None),
("device.mac_address", re.compile(r"\b(?:[0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}\b"), None),
("location.gps_coordinates", re.compile(r"[\-+]?\d{1,3}\.\d{3,}\s*,\s*[\-+]?\d{1,3}\.\d{3,}"), None),
("online.url", re.compile(r"\bhttps?://[^\s]+"), None),
("identity.ssn", re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), None),
("contact.ip_address", re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b"), None),
("financial.credit_card", re.compile(r"\b(?:\d[ \-]?){13,19}\b"), _luhn_ok),
("financial.swift_bic", re.compile(r"\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\b"), None),
("device.imei", re.compile(r"\b\d{15}\b"), _luhn_ok),
]
_SNAP = {
"contact.phone": re.compile(r"(?<!\d)(?:\+?\d{1,3}[ \-.]?)?(?:\(\d{2,4}\)[ \-.]?)?\d{2,4}[ \-.]?\d{3}[ \-.]?\d{3,4}(?!\d)"),
"identity.date_of_birth": re.compile(r"\b(?:\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}|\d{4}[\/.\-]\d{1,2}[\/.\-]\d{1,2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4}|\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{4})\b"),
"financial.amount": re.compile(r"(?:[$€£¥]\s?\d[\d.,]*(?:\s?[KMB])?|\b(?:USD|EUR|GBP|JPY|CHF|CAD|AUD)\s?\d[\d.,]*(?:\s?[KMB])?\b|\b\d[\d.,]*\s?(?:USD|EUR|GBP|dollars|euros)\b)"),
"contact.postal_code": re.compile(r"\b(?:\d{5}(?:-\d{4})?|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2})\b"),
}
_AUTH_TYPES = {t for t, _, _ in _AUTH}
# boundary-snap: the token classifier drops leading/trailing sub-tokens of Latin-script
# words ('Ibuprofen'->'profen'). Extend a span across contiguous Latin word-chars to
# complete the partial word(s). Latin-only -> CJK/Arabic left untouched (no over-extend).
_LAT = re.compile(r"[0-9A-Za-zÀ-ÖØ-öø-ÿ]")
def _snap_word(text, s, e):
n = len(text)
while s > 0 and _LAT.match(text[s - 1]) and _LAT.match(text[s]): s -= 1
while e < n and _LAT.match(text[e]) and _LAT.match(text[e - 1]): e += 1
return s, e
_SWIFT_CUE = re.compile(r"(?i)(swift|bic)")
def _swift_ok(text, s, val): # kill ALL-CAPS-word false BICs (PARTICULARS, CONFIDENTIAL)
return bool(re.search(r"\d", val)) or bool(_SWIFT_CUE.search(text[max(0, s - 12):s]))
def hybrid_spans(text: str, model_spans: list[dict]) -> list[dict]:
"""model_spans: [{'start','end','type'}...] from the token classifier. Returns the
hybrid-decoded spans (dicts with start/end/type/text)."""
# 1. AUTH regex spans — built INDEPENDENTLY of the model (regex is authoritative
# for these distinctive formats; model fragments must not block them).
auth, claimed = [], [False] * len(text)
for t, pat, val in _AUTH:
for mm in pat.finditer(text):
s, e = mm.start(), mm.end()
if any(claimed[s:e]): continue
if val and not val(mm.group(0)): continue
if t == "financial.swift_bic" and not _swift_ok(text, s, mm.group(0)): continue
for i in range(s, e): claimed[i] = True
auth.append({"start": s, "end": e, "type": t, "text": mm.group(0)})
# 2. model spans for non-AUTH types; SNAP types expand to overlapping regex match
out = []
for m in model_spans:
t = m["type"]
if t in _AUTH_TYPES:
continue # regex owns these
if t in _SNAP:
snap = None
for mm in _SNAP[t].finditer(text):
if min(mm.end(), m["end"]) > max(mm.start(), m["start"]):
snap = mm; break
if snap:
out.append({"start": snap.start(), "end": snap.end(), "type": t,
"text": text[snap.start():snap.end()]}); continue
ss, ee = _snap_word(text, m["start"], m["end"]) # complete partial Latin words
out.append({"start": ss, "end": ee, "type": t, "text": text[ss:ee]})
out.extend(auth)
# non-Latin scripts (CJK/Hangul/Hiragana/Katakana/Thai/Arabic/Cyrillic/Devanagari/Hebrew):
# a 2-char span is a full token (e.g. the name 张敏), not Latin sub-token junk -> keep a >=2
# floor for non-Latin, >=3 for Latin (otherwise short CJK/Hangul names are wrongly dropped).
import re as _re
_NONLATIN = _re.compile(r"[Ѐ-ӿ-ۿऀ-ॿ--ヿ㐀-鿿가-豈-]")
seen, uniq = set(), []
for sp in sorted(out, key=lambda s: (s["start"], s["end"])):
k = (sp["start"], sp["end"], sp["type"])
if k in seen: continue
frag = text[sp["start"]:sp["end"]].strip()
if len(frag) < (2 if _NONLATIN.search(frag) else 3): continue # drop fragments
seen.add(k); uniq.append(sp)
# CONTEXT tier: cue-gated alphanumeric IDs (Passport No:/Policy #/MRN: ...) — recovers
# the structure-less IDs the model can't learn. Authoritative for their types; AUTH wins.
try:
from context_cued import context_cued_spans, CONTEXT_TYPES
except Exception:
return uniq
cued = context_cued_spans(text)
if not cued:
return uniq
auth_claim = [False] * len(text)
for sp in uniq:
if sp["type"] in _AUTH_TYPES:
for i in range(sp["start"], sp["end"]): auth_claim[i] = True
kept = [c for c in cued if not any(auth_claim[c["start"]:c["end"]])]
rng = [(c["start"], c["end"]) for c in kept]
def _ov(sp): return any(min(e, sp["end"]) > max(s, sp["start"]) for s, e in rng)
merged = [sp for sp in uniq if not (sp["type"] in CONTEXT_TYPES and _ov(sp))] + kept
seen, final = set(), []
for sp in sorted(merged, key=lambda s: (s["start"], s["end"])):
k = (sp["start"], sp["end"], sp["type"])
if k in seen: continue
seen.add(k); final.append(sp)
# GROUP-B cue tier: amount/date/phone/postal where the model stayed silent but a
# field-cue+shape is present (authoritative for those 4 types; AUTH wins; address excluded).
try:
from context_cued import group_b_cue_spans
except Exception:
return final
gb = [c for c in group_b_cue_spans(text)
if c["type"] in ("financial.amount","identity.date_of_birth","contact.phone","contact.postal_code")]
if not gb:
return final
aclaim = [False] * len(text)
for sp in final:
if sp["type"] in _AUTH_TYPES:
for i in range(sp["start"], sp["end"]): aclaim[i] = True
keptb = []
for c in gb:
if any(aclaim[c["start"]:c["end"]]): continue
s, e = c["start"], c["end"]
if c["type"] == "financial.amount":
while e > s and text[e-1] in ".,;": e -= 1
keptb.append({"start": s, "end": e, "type": c["type"], "text": text[s:e]})
rb = [(c["start"], c["end"]) for c in keptb]
def _ovb(sp): return any(min(e, sp["end"]) > max(s, sp["start"]) for s, e in rb)
GB = {"financial.amount","identity.date_of_birth","contact.phone","contact.postal_code"}
merged2 = [sp for sp in final if not (sp["type"] in GB and _ovb(sp))] + keptb
seen, out2 = set(), []
for sp in sorted(merged2, key=lambda s: (s["start"], s["end"])):
k = (sp["start"], sp["end"], sp["type"])
if k in seen: continue
seen.add(k); out2.append(sp)
return out2
def model_spans(text: str, tok, model):
import torch
enc = tok(text, return_offsets_mapping=True, return_tensors="pt", truncation=True, max_length=2048)
off = enc.pop("offset_mapping")[0].tolist()
enc = {k: v.to(model.device) for k, v in enc.items()}
with torch.no_grad():
ids = model(**enc).logits[0].argmax(-1).tolist()
id2label = model.config.id2label
spans, cur = [], None
for (a, b), i in zip(off, ids):
lab = id2label[i]
if b <= a or lab == "O":
if cur: spans.append(cur); cur = None
continue
typ = lab.split("-", 1)[1] if "-" in lab else lab
if lab[:2] in ("B-", "S-") or cur is None or cur["type"] != typ:
if cur: spans.append(cur)
cur = {"start": a, "end": b, "type": typ}
else:
cur["end"] = b
if cur: spans.append(cur)
# trim leading/trailing whitespace
for sp in spans:
while sp["start"] < sp["end"] and text[sp["start"]].isspace(): sp["start"] += 1
while sp["end"] > sp["start"] and text[sp["end"] - 1].isspace(): sp["end"] -= 1
return [s for s in spans if s["end"] > s["start"]]
def predict(text: str, tok, model, hybrid: bool = True) -> list[dict]:
ms = model_spans(text, tok, model)
if not hybrid:
return [{"start": s["start"], "end": s["end"], "type": s["type"],
"text": text[s["start"]:s["end"]]} for s in ms]
return hybrid_spans(text, ms)
|