People2JobRanker
Cross-encoder that scores how well a job posting fits a job seeker. ModernBERT
(gte-reranker-modernbert-base) over the joint (profile, job) pair with one relevance
head and three auxiliary heads (role_fit, skill_fit, level_fit). Trained on
Profile-Jobs-Ranked:
5.7M LLM-graded pairs over 245k synthetic US seeker profiles x 1.5M real postings,
with a relevance-only label (0.55role_fit + 0.45skill_fit, 0-100).
Held-out test results (6,002 unseen profiles / 146,855 pairs)
| metric | model | retrieval order | random |
|---|---|---|---|
| NDCG@10 | 0.746 | 0.640 | 0.521 |
| NDCG@full | 0.794 | --- | --- |
| judge-label of #1 result | 55.7 | 46.3 | (oracle 65.8) |
Pair-level agreement with judge labels: Pearson 0.716 / Spearman 0.699. When a Strong+ match (label >= 76) exists, the model surfaces one in its top-3 for 86.9% of lists.
Fairness probe: counterfactual flip of the profile's sponsorship phrase on 200 held-out lists moved predictions by mean |Δ| = 0.23 points (p95 = 0.81) with within-list ordering stability tau = 0.981 — the model is insensitive to work-authorization text.
Usage
Inputs are rendered text, and the exact rendering matters (training/serving parity):
profile as [SEEKING]/[LEVEL]/[LOC]/[WANTS]/[COMP]/[SKILLS]/[EXP]... lines, job as
[TITLE]/[COMPANY]/[LOC]/[PAY]/[LEVEL]/[DESC] lines --- see the dataset card for the schema.
import torch, torch.nn as nn
from transformers import AutoModel, AutoTokenizer
from huggingface_hub import hf_hub_download
REPO = "akzaidan/People2JobRanker"
tok = AutoTokenizer.from_pretrained(REPO)
class Ranker(nn.Module):
def __init__(self):
super().__init__()
self.encoder = AutoModel.from_pretrained("Alibaba-NLP/gte-reranker-modernbert-base")
h = self.encoder.config.hidden_size
self.dropout = nn.Dropout(0.1)
self.main_head, self.aux_head = nn.Linear(h, 1), nn.Linear(h, 3)
def forward(self, **enc):
cls = self.encoder(**enc).last_hidden_state[:, 0].float()
cls = self.dropout(cls)
return self.main_head(cls).squeeze(-1), self.aux_head(cls)
model = Ranker()
model.load_state_dict(torch.load(hf_hub_download(REPO, "pytorch_model.bin"),
map_location="cpu"))
model.eval()
enc = tok([profile_text], [job_text], truncation="longest_first",
max_length=2048, return_tensors="pt")
score, aux = model(**enc) # rank by `score` (higher = better fit)
Limitations --- read before deploying
- Scores are for ORDERING only.
sigmoid(score)*100is inflated (disqualified pairs average ~62%); fit a monotone recalibration before displaying a "% match". - Labels are model opinions: graded by gpt-5-nano (low reasoning effort) on a role+skill rubric. No human ground truth; judge biases transfer.
- Deliberately does NOT penalize seniority gaps, location, compensation, or work authorization --- those are exact computations meant for a downstream feature/heuristic layer. A junior seeker will see senior roles ranked high without that layer.
- Weak on lexical polysemy in thin candidate pools ("casting" director vs die casting).
- US market, August 2026 snapshot; synthetic seeker profiles.
Model tree for akzaidan/People2JobRanker
Base model
answerdotai/ModernBERT-base

