Text Classification
Transformers
Safetensors
multilingual
xlm-roberta
cross-encoder
reranker
feed-ranking
enterprise-feed
learning-to-rank
text-embeddings-inference
Instructions to use FDS-Iterations/third-pass-feed-ranker with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use FDS-Iterations/third-pass-feed-ranker with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="FDS-Iterations/third-pass-feed-ranker")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("FDS-Iterations/third-pass-feed-ranker") model = AutoModelForSequenceClassification.from_pretrained("FDS-Iterations/third-pass-feed-ranker", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Publish third-pass feed ranker
Browse files- README.md +52 -59
- config.json +0 -2
- 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:
|
| 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 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
- **Input:** `job_title` (query) + `post_text` (passage) → single relevance score (higher = more relevant)
|
| 24 |
-
- **Base:** `
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
##
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
-
|
| 35 |
-
|
| 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 =
|
| 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.
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
## Evaluation (held-out roles AND posts; synthetic)
|
| 67 |
| metric | value |
|
| 68 |
|---|---|
|
| 69 |
-
|
|
| 70 |
-
| role
|
| 71 |
-
|
|
| 72 |
-
|
|
| 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 |
-
|
| 81 |
-
relevant
|
| 82 |
-
|
| 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)
|
| 90 |
-
|
| 91 |
-
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 96 |
-
decision logic.
|
| 97 |
|
| 98 |
## Training & method
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 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 `
|
| 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:
|
| 3 |
size 470588492
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ed0648a19134cc5ce1ca15923bb9bc6d614bd279d93d515b376a7617f9eab32
|
| 3 |
size 470588492
|