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)

Screenshot 2026-08-19 at 12.22.43 PM Screenshot 2026-08-19 at 12.23.16 PM Screenshot 2026-08-19 at 12.23.37 PM

Limitations --- read before deploying

  • Scores are for ORDERING only. sigmoid(score)*100 is 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.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for akzaidan/People2JobRanker

Finetuned
(24)
this model

Dataset used to train akzaidan/People2JobRanker