FDS-Iterations commited on
Commit
98ecbb1
·
verified ·
1 Parent(s): ba60dfa

Publish third-pass feed ranker

Browse files
Files changed (3) hide show
  1. README.md +52 -59
  2. config.json +0 -2
  3. model.safetensors +1 -1
README.md CHANGED
@@ -9,33 +9,32 @@ tags:
9
  - enterprise-feed
10
  - learning-to-rank
11
  pipeline_tag: text-classification
12
- base_model: cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
13
  library_name: transformers
14
  ---
15
 
16
  # Third-Pass Feed Ranker (job-title × feed-post relevance)
17
 
18
  A lightweight **cross-encoder** that scores how relevant an enterprise social-feed post is to a
19
- viewer, given only the viewer's **job title** and the **post text**. It is a *third-pass
20
- reranker*: it re-scores a small candidate slate (~20 items) from earlier passes to surface a
21
- genuinely job-relevant post that was buried below the top slot.
22
 
23
  - **Input:** `job_title` (query) + `post_text` (passage) → single relevance score (higher = more relevant)
24
- - **Base:** `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` (multilingual MiniLM-L12, ~117M params)
25
- - **Runtime:** server-class **CPU** at scale (~120 pairs/sec on 8 CPU threads; INT8/ONNX 3–6× more)
26
- - **Trained with a listwise ranking objective** (optimizes which item wins the slate), not just
27
- pointwise regression — so it is markedly better at putting the buried relevant post at #1.
 
 
28
 
29
- ## It ranks by MEANING — and the role keyword is neither a crutch nor a trap
30
- Earlier iterations keyed on whether a post *named* the viewer's role. This model is trained so the
31
- role keyword carries **zero** relevance information: during training the role name is injected into
32
- a random half of *all* posts regardless of their true relevance. As a result:
33
-
34
- - Naming the viewer's role changes a relevant post's score by **≈0.00** (it neither helps nor hurts).
35
- - Masking role words in the text does **not** change the ranking (**0% keyword-dependence**).
36
-
37
- Relevance is decided by **substance** (e.g. "patient-monitoring escalation protocol" → nurse),
38
- which is exactly what a keyword matcher cannot do.
39
 
40
  ## Usage — scoring + the gate
41
  ```python
@@ -50,63 +49,57 @@ def scores(title, posts):
50
  with torch.no_grad():
51
  return model(**enc).logits.squeeze(-1).tolist()
52
 
53
- TAU = 3.0 # promotion margin; calibrate per deployment (see below)
54
  def third_pass(title, slate):
55
  s = scores(title, slate)
56
  challenger = max(range(1, len(slate)), key=lambda i: s[i])
57
  return ("promote", challenger) if s[challenger] - s[0] > TAU else ("no_change", None)
58
  ```
59
  **The model only scores.** The gate (which item to move to slot 1, and the Ï„ threshold) is *your*
60
- logic — none of it is in the weights. Calibrate τ on your own feeds to trade recovery vs.
61
- false-promotion. **Ï„ is distribution-sensitive.** This model was trained with a listwise objective
62
- that widens the score range, so its natural margins are large: **τ≈3.0** holds false-promotion
63
- around 10% on realistic feeds — much higher than the τ≈0.15 that suited the older pointwise model.
64
- Re-calibrate on a sample of your own no-relevance feeds.
 
 
 
65
 
66
- ## Evaluation (held-out roles AND posts; synthetic)
67
  | metric | value |
68
  |---|---|
69
- | **realistic feed: buried relevant post surfaced to #1** | **0.84** |
70
- | role-keyword effect on a relevant post's score (target ≈ 0) | **≈ 0.00** |
71
- | keyword-dependence (masked vs normal ranking) | **0%** |
72
- | gated recovery on adversarial worst-case slates (τ=3.0) | 0.08–0.26 |
73
- | protection of high-value org announcements | ~0.81–0.92 |
74
-
75
- On a **realistic feed** — the relevant post competing against ordinary filler — the model puts it
76
- first **84%** of the time. On a deliberately **adversarial worst-case slate** (~9 simultaneous
77
- strong competitors, including posts genuinely relevant to *other* roles), exact-#1 recovery drops
78
- to ~0.29 (top-3 ~0.59); that is a stress bound, not a realistic-feed number.
79
 
80
- **Announcement protection is a deliberate trade.** Because the listwise objective makes a strongly
81
- relevant post outscore an incumbent announcement more often, must-see-announcement protection is
82
- ~0.81–0.92 (down from ~1.0 in the pointwise model). This model favors *recovering* a buried
83
- role-relevant post over *protecting* every announcement. If you need stricter announcement
84
- protection, raise Ï„ or add an explicit announcement guard in your decision logic.
85
 
86
  ## Intended use & limitations
87
  - Re-ranking short enterprise-feed candidate slates by job-title relevance; abstains on role-less
88
  titles.
89
- - **Trained entirely on SYNTHETIC data** (LLM-generated posts + synthetic slates) — it has not
90
- seen real feed content. **Validate on your own data before production.**
91
- - Being a small CPU model, it does not perfectly resolve *many* simultaneous strong competitors
92
- (e.g. distinguishing this role's post from several other-role-relevant posts at once); it excels
93
- when the relevant item competes against ordinary filler.
 
 
 
 
94
  - Cross-lingual mixing (non-English title vs English-only feed) is weaker than same-language feeds.
95
- - Relevance is title-driven; recency/importance beyond the relevance signal must be added in your
96
- decision logic.
97
 
98
  ## Training & method
99
- O*NET-derived job titles; LLM-generated posts where relevance is by **substance**; adversarial
100
- 20-item slates (gem / abstain / announcement types) with held-out roles+posts. Two things make
101
- this version different from a plain pointwise regressor:
102
- 1. **Role-keyword rebalance** — the viewer's role name is injected into a random half of *all*
103
- posts during training (independent of the label), so the keyword is decorrelated from relevance
104
- and the model cannot use it as either a shortcut or a penalty.
105
- 2. **Listwise ranking loss** — training batches whole slates and adds a softmax cross-entropy term
106
- that pushes the truly-relevant post to rank #1, on top of a pointwise anchor. Inference stays
107
- pointwise (one title×post score at a time), so CPU cost is unchanged.
108
- Headline metric is **role-masked** recovery so a keyword shortcut cannot inflate it.
109
 
110
  ## License & attribution
111
- Apache-2.0. Inherits from `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` — verify its license carries
112
- through. Training posts were generated with a Qwen model; review the applicable terms.
 
9
  - enterprise-feed
10
  - learning-to-rank
11
  pipeline_tag: text-classification
12
+ base_model: nreimers/mMiniLMv2-L12-H384-distilled-from-XLMR-Large
13
  library_name: transformers
14
  ---
15
 
16
  # Third-Pass Feed Ranker (job-title × feed-post relevance)
17
 
18
  A lightweight **cross-encoder** that scores how relevant an enterprise social-feed post is to a
19
+ viewer, given only the viewer's **job title** and the **post text**. It is a *third-pass reranker*:
20
+ it re-scores a small candidate slate (~20 items) from earlier passes to surface a genuinely
21
+ job-relevant post that was buried below the top slot.
22
 
23
  - **Input:** `job_title` (query) + `post_text` (passage) → single relevance score (higher = more relevant)
24
+ - **Base:** `nreimers/mMiniLMv2-L12-H384-distilled-from-XLMR-Large` — a **generic** distilled
25
+ multilingual MiniLM-L12 (~117M params), *not* a search reranker. Fine-tuning defines relevance
26
+ purely from this task's data, without a general-search "dense-technical-text = relevant" prior.
27
+ - **Runtime:** server-class **CPU** at scale (~120 pairs/sec on 8 CPU threads; INT8/ONNX 3–6× more).
28
+ - **Trained with a listwise ranking objective** (optimizes which item wins the slate), pointwise
29
+ inference unchanged.
30
 
31
+ ## What it does — and what it deliberately avoids
32
+ - **Ranks by MEANING, not keywords.** Relevance is decided by substance (e.g. "patient-monitoring
33
+ escalation protocol" → a clinical role). Masking role words barely changes the ranking, and the
34
+ role keyword is trained to be uncorrelated with the label (neither a shortcut nor a penalty).
35
+ - **Not fooled by dense technical jargon.** Training injects dense-technical posts as hard negatives
36
+ for non-technical roles, so a machine-learning or software post does **not** score as broadly
37
+ relevant to, say, a nurse or a chef — it only wins for roles it actually fits.
 
 
 
38
 
39
  ## Usage — scoring + the gate
40
  ```python
 
49
  with torch.no_grad():
50
  return model(**enc).logits.squeeze(-1).tolist()
51
 
52
+ TAU = 1.0 # promotion margin; calibrate per deployment (see below)
53
  def third_pass(title, slate):
54
  s = scores(title, slate)
55
  challenger = max(range(1, len(slate)), key=lambda i: s[i])
56
  return ("promote", challenger) if s[challenger] - s[0] > TAU else ("no_change", None)
57
  ```
58
  **The model only scores.** The gate (which item to move to slot 1, and the Ï„ threshold) is *your*
59
+ logic — none of it is in the weights. **τ is distribution-sensitive**: on realistic feeds a value
60
+ around **0.5–1.5** trades recovery vs. false-promotion; re-calibrate on a sample of your own
61
+ no-relevance feeds. Higher Ï„ is more conservative.
62
+
63
+ ## Evaluation
64
+ Measured on an **external hold-out of 157 job titles that never appear in training** (adjacent
65
+ real-world variants of trained roles + high-volume roles the training taxonomy under-covers), each
66
+ with substance gems buried in 20-item feeds:
67
 
 
68
  | metric | value |
69
  |---|---|
70
+ | relevant post surfaced to #1 in its feed (novel titles) | **~60%** |
71
+ | role's gem beats a dense-technical distractor | **~85%** |
72
+ | realistic-density recovery (buried gem promoted) | **~0.64** @ low Ï„ |
73
+ | false-promotion on no-relevance feeds | **~0.3%** |
 
 
 
 
 
 
74
 
75
+ On a deliberately **adversarial worst-case slate** (~9 simultaneous strong competitors, including
76
+ posts relevant to *other* roles), exact-#1 recovery drops to ~0.3 — a stress bound, not a
77
+ realistic-feed number.
 
 
78
 
79
  ## Intended use & limitations
80
  - Re-ranking short enterprise-feed candidate slates by job-title relevance; abstains on role-less
81
  titles.
82
+ - **Trained entirely on SYNTHETIC data** (LLM-generated posts + synthetic slates). **Validate on
83
+ your own data before production.**
84
+ - **Fine role-discrimination is limited.** It reliably separates a relevant post from ordinary
85
+ filler, but distinguishing a role's exact post from a *closely adjacent* role's post is near the
86
+ capacity ceiling of a 117M model.
87
+ - **Out-of-distribution phrasing is the weak axis.** Terse status fragments, log/ticket snippets,
88
+ and atypical wording score more noisily than well-formed posts — for all roles.
89
+ - **Very high-volume roles absent from the training taxonomy** (a few common titles) generalize at
90
+ reduced magnitude; adding them to training stabilizes them.
91
  - Cross-lingual mixing (non-English title vs English-only feed) is weaker than same-language feeds.
92
+ - Relevance is title-driven; recency/importance beyond relevance must live in your decision logic.
 
93
 
94
  ## Training & method
95
+ Job titles derived from a public occupation taxonomy; LLM-generated posts where relevance is by
96
+ **substance** and the role is never named; adversarial 20-item slates with held-out roles and posts,
97
+ in both well-formed and terse registers. Objective = pointwise relevance MSE **plus a listwise
98
+ ranking loss** that pushes the relevant post to rank #1 within its slate. Two anti-bias measures:
99
+ the role keyword is **rebalanced** to be label-uncorrelated, and **dense-technical hard negatives**
100
+ are injected into non-technical roles' slates so technical vocabulary is not a global relevance
101
+ signal. Headline metrics are measured on titles **never seen in training**.
 
 
 
102
 
103
  ## License & attribution
104
+ Apache-2.0. Inherits from `nreimers/mMiniLMv2-L12-H384-distilled-from-XLMR-Large` — verify its
105
+ license carries through. Training posts were generated with a Qwen model; review the applicable terms.
config.json CHANGED
@@ -26,8 +26,6 @@
26
  "num_attention_heads": 12,
27
  "num_hidden_layers": 12,
28
  "pad_token_id": 1,
29
- "position_embedding_type": "absolute",
30
- "sbert_ce_default_activation_function": "torch.nn.modules.linear.Identity",
31
  "tie_word_embeddings": true,
32
  "transformers_version": "5.16.1",
33
  "type_vocab_size": 1,
 
26
  "num_attention_heads": 12,
27
  "num_hidden_layers": 12,
28
  "pad_token_id": 1,
 
 
29
  "tie_word_embeddings": true,
30
  "transformers_version": "5.16.1",
31
  "type_vocab_size": 1,
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:16acddaa92b8791ef6d9276250cbd88f7dd6774c95a1b2789489681b3d9c66b4
3
  size 470588492
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6ed0648a19134cc5ce1ca15923bb9bc6d614bd279d93d515b376a7617f9eab32
3
  size 470588492