NohTow commited on
Commit
d2f5f21
·
1 Parent(s): 8711f8f

Integrate with Sentence Transformers (#1)

Browse files

- Integrate with Sentence Transformers (95c9c83d21761bf279784ed6f64f404422aa4940)

1_LogitScore/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "true_token_id": 9175,
3
+ "false_token_id": 2665,
4
+ "module_input_name": "causal_logits"
5
+ }
README.md CHANGED
@@ -15,6 +15,7 @@ tags:
15
  - document-reranking
16
  - vidore
17
  - beir
 
18
  datasets:
19
  - vidore/colpali_train_set
20
  - lightonai/embeddings-fine-tuning
@@ -126,12 +127,60 @@ This is the best text result in the LightOn-rerank family (2B pointwise: 49.13,
126
  - **Fine-tuning:** joint text+vision LoRA (r=32, α=32, rsLoRA, merged into the released weights), mixed-modality batches (2 text + 2 vision groups per micro-batch), vision loss weight 1.3, lr 1e-4, 1 epoch (465 steps)
127
  - **Data:** same 213k groups as the listwise models — 107k text groups (NQ, TriviaQA, MS MARCO; each a `[pos, neg_0, neg_1, neg_2]` 4-list with hard negatives mined via the NV-Retriever approach with GTE-ModernBERT) + 106k vision groups (ColPali train set with negatives mined by Nomic). For pointwise training each group is flattened into (query, document, Yes/No) triples.
128
  - **Languages:** English (training), French (zero-shot transfer)
129
- - **Requirements:** `transformers >= 5.4.0` (`qwen3_5` architecture)
130
 
131
  ## Usage: pointwise reranking
132
 
133
  Each candidate is scored independently as `logit("Yes") − logit("No")` at the first generated position; sort candidates by descending score. The model was trained with a fixed system prompt and user template: use them verbatim for best results.
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  ```python
136
  import torch
137
  from transformers import AutoModelForImageTextToText, AutoProcessor
@@ -179,6 +228,7 @@ inputs = processor(
179
  with torch.inference_mode():
180
  logits = model(**inputs).logits[:, -1]
181
  scores = (logits[:, YES_TOKEN_ID] - logits[:, NO_TOKEN_ID]).tolist()
 
182
 
183
  ranked = sorted(zip(scores, documents), reverse=True)
184
  ```
@@ -222,7 +272,7 @@ Full-page document images can exceed 8k tokens, so keep `--max-model-len` at 163
222
 
223
  ## Notes & limitations
224
 
225
- - **Thinking must be disabled for scoring.** Qwen3.5-4B's chat template enables `<think>` by default; with it on, the first generated token is a thinking token and the Yes/No logprobs are distorted. Pass `enable_thinking=False` to `apply_chat_template` (or `chat_template_kwargs={"enable_thinking": False}` via the vLLM OpenAI client) as shown above.
226
  - Pointwise scoring does not benefit from the 2B→4B scale-up on vision (59.80 vs 59.87 for the 2B), while generative listwise gains +2.0 points over the same step. For vision workloads prefer [LightOn-rerank-LW-4B](https://huggingface.co/lightonai/LightOn-rerank-LW-4B) at this size, or [LightOn-rerank-PW-2B](https://huggingface.co/lightonai/LightOn-rerank-PW-2B) for the same visual quality at lower cost.
227
  - Training data is English-only. French works zero-shot (the backbone is multilingual) but is slightly behind English on average.
228
  - BEIR contamination flag: NQ and MSMARCO are part of the text training data; headline text figures use decontaminated means that exclude them.
 
15
  - document-reranking
16
  - vidore
17
  - beir
18
+ - sentence-transformers
19
  datasets:
20
  - vidore/colpali_train_set
21
  - lightonai/embeddings-fine-tuning
 
127
  - **Fine-tuning:** joint text+vision LoRA (r=32, α=32, rsLoRA, merged into the released weights), mixed-modality batches (2 text + 2 vision groups per micro-batch), vision loss weight 1.3, lr 1e-4, 1 epoch (465 steps)
128
  - **Data:** same 213k groups as the listwise models — 107k text groups (NQ, TriviaQA, MS MARCO; each a `[pos, neg_0, neg_1, neg_2]` 4-list with hard negatives mined via the NV-Retriever approach with GTE-ModernBERT) + 106k vision groups (ColPali train set with negatives mined by Nomic). For pointwise training each group is flattened into (query, document, Yes/No) triples.
129
  - **Languages:** English (training), French (zero-shot transfer)
130
+ - **Requirements:** `transformers >= 5.4.0` (`qwen3_5` architecture); `sentence-transformers >= 5.4.0` for the CrossEncoder usage
131
 
132
  ## Usage: pointwise reranking
133
 
134
  Each candidate is scored independently as `logit("Yes") − logit("No")` at the first generated position; sort candidates by descending score. The model was trained with a fixed system prompt and user template: use them verbatim for best results.
135
 
136
+ ### Using Sentence Transformers
137
+
138
+ Install Sentence Transformers (`>= 5.4.0`):
139
+
140
+ ```bash
141
+ pip install "sentence-transformers[image]"
142
+ ```
143
+
144
+ The trained system prompt and user templates are baked into the bundled `reranker` chat template — including the non-thinking generation prompt, so no `enable_thinking` handling is needed — and query-document pairs are formatted correctly out of the box:
145
+
146
+ ```python
147
+ from sentence_transformers import CrossEncoder
148
+
149
+ model = CrossEncoder("lightonai/LightOn-rerank-PW-4B")
150
+
151
+ query = "What is late interaction in neural information retrieval?"
152
+ documents = [
153
+ "ColBERT computes token-level query-document interactions at search time...",
154
+ "The Eiffel Tower is located on the Champ de Mars in Paris.",
155
+ ]
156
+
157
+ pairs = [(query, doc) for doc in documents]
158
+ scores = model.predict(pairs)
159
+ print(scores)
160
+ # [-3. -8.0625]
161
+
162
+ rankings = model.rank(query, documents)
163
+ print(rankings)
164
+ # [{'corpus_id': 0, 'score': -3.0}, {'corpus_id': 1, 'score': -8.0625}]
165
+ ```
166
+
167
+ To rerank **page images**, pass a `PIL.Image` (or an image URL or file path string) as the document. Text and image candidates can be mixed in the same call:
168
+
169
+ ```python
170
+ from PIL import Image
171
+
172
+ pairs = [
173
+ (query, Image.open("page_1.png")),
174
+ (query, "https://example.com/page_2.png"),
175
+ (query, "A text passage candidate for the same query."),
176
+ ]
177
+ scores = model.predict(pairs)
178
+ ```
179
+
180
+ Scores are raw `logit("Yes") − logit("No")` differences. You can map them to 0...1 probabilities with `model.predict(pairs, activation_fn=torch.nn.Sigmoid())`.
181
+
182
+ ### Using Transformers
183
+
184
  ```python
185
  import torch
186
  from transformers import AutoModelForImageTextToText, AutoProcessor
 
228
  with torch.inference_mode():
229
  logits = model(**inputs).logits[:, -1]
230
  scores = (logits[:, YES_TOKEN_ID] - logits[:, NO_TOKEN_ID]).tolist()
231
+ # [-3.0, -8.0625]
232
 
233
  ranked = sorted(zip(scores, documents), reverse=True)
234
  ```
 
272
 
273
  ## Notes & limitations
274
 
275
+ - **Thinking must be disabled for scoring.** Qwen3.5-4B's chat template enables `<think>` by default; with it on, the first generated token is a thinking token and the Yes/No logprobs are distorted. Pass `enable_thinking=False` to `apply_chat_template` (or `chat_template_kwargs={"enable_thinking": False}` via the vLLM OpenAI client) as shown above. The Sentence Transformers path handles this automatically: the bundled `reranker` chat template hardcodes the non-thinking generation prompt.
276
  - Pointwise scoring does not benefit from the 2B→4B scale-up on vision (59.80 vs 59.87 for the 2B), while generative listwise gains +2.0 points over the same step. For vision workloads prefer [LightOn-rerank-LW-4B](https://huggingface.co/lightonai/LightOn-rerank-LW-4B) at this size, or [LightOn-rerank-PW-2B](https://huggingface.co/lightonai/LightOn-rerank-PW-2B) for the same visual quality at lower cost.
277
  - Training data is English-only. French works zero-shot (the backbone is multilingual) but is slightly behind English on average.
278
  - BEIR contamination flag: NQ and MSMARCO are part of the text training data; headline text figures use decontaminated means that exclude them.
additional_chat_templates/reranker.jinja ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set default_system = "Judge whether the document is relevant to the query. Answer Yes or No." -%}
2
+ {%- set ns = namespace(system_text="", doc_visual=false, doc_text=false) -%}
3
+ {%- for message in messages if message.role == "system" -%}
4
+ {%- if message.content is string -%}
5
+ {%- set ns.system_text = ns.system_text + message.content -%}
6
+ {%- else -%}
7
+ {%- for content in message.content -%}
8
+ {%- if 'text' in content -%}
9
+ {%- set ns.system_text = ns.system_text + content.text -%}
10
+ {%- endif -%}
11
+ {%- endfor -%}
12
+ {%- endif -%}
13
+ {%- endfor -%}
14
+ {%- if not ns.system_text -%}
15
+ {%- set ns.system_text = default_system -%}
16
+ {%- endif -%}
17
+ {%- for message in messages if message.role == "document" -%}
18
+ {%- if message.content is string -%}
19
+ {%- if message.content -%}
20
+ {%- set ns.doc_text = true -%}
21
+ {%- endif -%}
22
+ {%- else -%}
23
+ {%- for content in message.content -%}
24
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content or content.type == 'video' or 'video' in content -%}
25
+ {%- set ns.doc_visual = true -%}
26
+ {%- elif 'text' in content and content.text -%}
27
+ {%- set ns.doc_text = true -%}
28
+ {%- endif -%}
29
+ {%- endfor -%}
30
+ {%- endif -%}
31
+ {%- endfor -%}
32
+ {%- macro render_content(message) -%}
33
+ {%- if message.content is string -%}
34
+ {{- message.content -}}
35
+ {%- else -%}
36
+ {%- for content in message.content -%}
37
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content -%}
38
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' -}}
39
+ {%- elif content.type == 'video' or 'video' in content -%}
40
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' -}}
41
+ {%- elif 'text' in content -%}
42
+ {{- content.text -}}
43
+ {%- endif -%}
44
+ {%- endfor -%}
45
+ {%- endif -%}
46
+ {%- endmacro -%}
47
+ {%- macro render_visuals(message) -%}
48
+ {%- if message.content is not string -%}
49
+ {%- for content in message.content -%}
50
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content -%}
51
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' -}}
52
+ {%- elif content.type == 'video' or 'video' in content -%}
53
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' -}}
54
+ {%- endif -%}
55
+ {%- endfor -%}
56
+ {%- endif -%}
57
+ {%- endmacro -%}
58
+ {%- macro render_texts(message) -%}
59
+ {%- if message.content is string -%}
60
+ {{- message.content -}}
61
+ {%- else -%}
62
+ {%- for content in message.content -%}
63
+ {%- if content.type != 'image' and 'image' not in content and 'image_url' not in content and content.type != 'video' and 'video' not in content and 'text' in content -%}
64
+ {{- content.text -}}
65
+ {%- endif -%}
66
+ {%- endfor -%}
67
+ {%- endif -%}
68
+ {%- endmacro -%}
69
+ {%- set user_body -%}
70
+ {%- if ns.doc_visual -%}
71
+ {%- for message in messages if message.role == "document" -%}
72
+ {{- render_visuals(message) -}}
73
+ {%- endfor -%}
74
+ {{- 'Given a query, determine if the document image is relevant. The query is: ' -}}
75
+ {%- for message in messages if message.role == "query" -%}
76
+ {{- render_content(message) -}}
77
+ {%- endfor -%}
78
+ {%- if ns.doc_text -%}
79
+ {{- '\n\nDocument: ' -}}
80
+ {%- for message in messages if message.role == "document" -%}
81
+ {{- render_texts(message) -}}
82
+ {%- endfor -%}
83
+ {%- endif -%}
84
+ {%- else -%}
85
+ {{- 'Given a query, determine if the document is relevant. The query is: ' -}}
86
+ {%- for message in messages if message.role == "query" -%}
87
+ {{- render_content(message) -}}
88
+ {%- endfor -%}
89
+ {{- '\n\nDocument: ' -}}
90
+ {%- for message in messages if message.role == "document" -%}
91
+ {{- render_content(message) -}}
92
+ {%- endfor -%}
93
+ {%- endif -%}
94
+ {%- endset -%}
95
+ {{- '<|im_start|>system\n' + ns.system_text|trim + '<|im_end|>\n' -}}
96
+ {{- '<|im_start|>user\n' + user_body|trim + '<|im_end|>\n' -}}
97
+ {%- if add_generation_prompt -%}
98
+ {{- '<|im_start|>assistant\n<think>\n\n</think>\n\n' -}}
99
+ {%- endif -%}
config_sentence_transformers.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "__version__": {
3
+ "pytorch": "2.10.0+cu128",
4
+ "sentence_transformers": "5.6.0",
5
+ "transformers": "5.13.1"
6
+ },
7
+ "activation_fn": "torch.nn.modules.linear.Identity",
8
+ "default_prompt_name": null,
9
+ "model_type": "CrossEncoder",
10
+ "prompts": {}
11
+ }
modules.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.base.modules.transformer.Transformer"
7
+ },
8
+ {
9
+ "idx": 1,
10
+ "name": "1",
11
+ "path": "1_LogitScore",
12
+ "type": "sentence_transformers.cross_encoder.modules.logit_score.LogitScore"
13
+ }
14
+ ]
sentence_bert_config.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformer_task": "any-to-any",
3
+ "modality_config": {
4
+ "text": {
5
+ "method": "forward",
6
+ "method_output_name": "logits"
7
+ },
8
+ "image": {
9
+ "method": "forward",
10
+ "method_output_name": "logits"
11
+ },
12
+ "video": {
13
+ "method": "forward",
14
+ "method_output_name": "logits"
15
+ },
16
+ "message": {
17
+ "method": "forward",
18
+ "method_output_name": "logits",
19
+ "format": "structured"
20
+ }
21
+ },
22
+ "module_output_name": "causal_logits",
23
+ "processing_kwargs": {
24
+ "chat_template": {
25
+ "chat_template": "reranker",
26
+ "add_generation_prompt": true
27
+ }
28
+ }
29
+ }