Misbahuddin/job-title-normalizer-e5-small
job-title-normalizer-e5-small
The lightweight variant of job-title-normalizer-e5-base: a 118M-param sentence encoder fine-tuned to normalize messy, multilingual job titles to a canonical occupation taxonomy (ESCO + O*NET), framed as retrieval over 4,055 canonical occupations. ~2.5× smaller and faster than the e5-base variant at a modest accuracy cost — the right choice for CPU serving and bulk backfills.
Live demo: <https://job-title-normalizer-525186107937.us-central1.run.app> (Cloud Run; may cold-start ~1 min)
Results
Held-out test set of *15,248 real ESCO/ONET titles; split by occupation** (zero train/test occupation overlap, asserted at build time).
BM25 scores MRR 0.034 cross-lingually; this model reaches 0.619. For the best accuracy, use the e5-base variant (overall MRR 0.463 vs 0.441).
Training
- Base: `intfloat/multilingual-e5-small` (mean pooling,
query:/passage:prefixes, 384-dim). - Objective: in-batch-negatives InfoNCE (
MultipleNegativesRankingLoss, scale 20), wrapped in MatryoshkaLoss (dims 384/256/128/64). - Data: 160,240 positive pairs (synonyms, alternate titles, cross-lingual label pairs) from ESCO (EN/FR/DE) + O*NET alternate titles; ≤50 pairs per occupation.
- Setup: 2 epochs, batch 128, lr 2e-5, warmup 10%, maxseqlen 64, fp16,
NoDuplicatesDataLoader. Trained on an RTX 4060 Laptop (8 GB) in ~8 min.
How to use
The e5 family needs asymmetric prefixes: titles as query: …, canonical labels as passage: …. Forgetting them silently degrades accuracy.
from sentence_transformers import SentenceTransformer
from sentence_transformers.util import cos_sim
model = SentenceTransformer("Misbahuddin/job-title-normalizer-e5-small")
canonicals = [
"passage: software developer",
"passage: data scientist",
"passage: nurse responsible for general care",
]
query = "query: Krankenpfleger" # German → English canonical
q = model.encode(query, normalize_embeddings=True)
c = model.encode(canonicals, normalize_embeddings=True)
scores = cos_sim(q, c)[0]
print(canonicals[int(scores.argmax())], float(scores.max()))Vectors are L2-normalized (cosine == dot product) — use FAISS IndexFlatIP for production. Matryoshka truncation to 256/128/64 dims works after re-normalizing. Calibrate an abstention threshold: out-of-taxonomy queries return low-score nearest neighbours rather than failing.
Intended use & limitations
- In scope: normalizing titles from resumes, postings, CRM/ATS/HRIS records to occupation IDs; semantic occupation search; FR/DE→EN cross-lingual lookup.
- Not a hiring/screening/compensation decision system. Keep a human in the loop for consequential uses.
- *Coverage bounded by ESCO + ONET**; en/fr/de verified only. Short-text regime (≤64 tokens).
Acknowledgements
- ESCO — © European Union; reused under Commission Decision 2011/833/EU. <https://esco.ec.europa.eu/>
- *ONET* — by the National Center for ONET Development for USDOL/ETA; CC BY 4.0. <https://www.onetcenter.org/>
ONET® is a trademark of USDOL/ETA. This model was produced using ONET data but is not endorsed by USDOL/ETA. ESCO is a service of the European Commission; this model is not endorsed by the Commission.
