matulichpt commited on
Commit
2326ab3
·
verified ·
1 Parent(s): 57a94a6

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +387 -393
README.md CHANGED
@@ -1,393 +1,387 @@
1
- ---
2
- license: apache-2.0
3
- language:
4
- - en
5
- tags:
6
- - cross-encoder
7
- - reranker
8
- - radiology
9
- - medical
10
- - retrieval
11
- - sentence-similarity
12
- - healthcare
13
- - clinical
14
- base_model: cross-encoder/ms-marco-MiniLM-L-12-v2
15
- pipeline_tag: text-classification
16
- library_name: sentence-transformers
17
- datasets:
18
- - radiology-education-corpus
19
- metrics:
20
- - mrr
21
- - ndcg
22
- model-index:
23
- - name: RadLITE-Reranker
24
- results:
25
- - task:
26
- type: reranking
27
- name: Document Reranking
28
- dataset:
29
- name: RadLIT-9 (Radiology Retrieval Benchmark)
30
- type: radiology-retrieval
31
- metrics:
32
- - type: mrr
33
- value: 0.829
34
- name: MRR (with bi-encoder)
35
- - type: mrr
36
- value: 0.533
37
- name: MRR on ABR Core Exam (Chest)
38
- ---
39
-
40
- # RadLITE-Reranker
41
-
42
- **Radiology Late Interaction Transformer Enhanced - Cross-Encoder Reranker**
43
-
44
- A domain-specialized cross-encoder for reranking radiology search results. This model takes a query-document pair and predicts a relevance score, providing more accurate ranking than bi-encoder similarity alone.
45
-
46
- > **Recommended:** Use this reranker together with [RadLITE-Encoder](https://huggingface.co/matulichpt/radlit-biencoder) in a two-stage pipeline for optimal performance. The bi-encoder handles fast retrieval over large corpora, then this cross-encoder reranks the top candidates for precision. This combination achieves **MRR 0.829** on radiology retrieval benchmarks.
47
-
48
- ## Model Description
49
-
50
- | Property | Value |
51
- |----------|-------|
52
- | **Model Type** | Cross-Encoder (Reranker) |
53
- | **Base Model** | [ms-marco-MiniLM-L-12-v2](https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-12-v2) |
54
- | **Domain** | Radiology / Medical Imaging |
55
- | **Hidden Size** | 384 |
56
- | **Max Sequence Length** | 512 tokens |
57
- | **Output** | Single relevance score |
58
- | **License** | Apache 2.0 |
59
-
60
- ### Why Use a Reranker?
61
-
62
- Bi-encoders (like RadLITE-Encoder) are fast but encode query and document independently. Cross-encoders process them together, capturing fine-grained interactions:
63
-
64
- | Approach | Speed | Accuracy | Use Case |
65
- |----------|-------|----------|----------|
66
- | Bi-Encoder | Fast (1000s docs/sec) | Good | First-stage retrieval |
67
- | Cross-Encoder | Slow (10s docs/sec) | Excellent | Reranking top candidates |
68
-
69
- **Two-stage pipeline**: Use bi-encoder to get top 50-100 candidates, then rerank with cross-encoder for best results.
70
-
71
- ## Performance
72
-
73
- ### Impact on RadLIT-9 Benchmark
74
-
75
- | Configuration | MRR | Improvement |
76
- |---------------|-----|-------------|
77
- | Bi-Encoder only | 0.78 | baseline |
78
- | **Bi-Encoder + Reranker** | **0.829** | **+6.3%** |
79
-
80
- ### ABR Core Exam (Board-Style Questions)
81
-
82
- Comparing two-stage pipeline (bi-encoder + reranker) vs bi-encoder alone:
83
-
84
- | Dataset | Two-Stage MRR | Bi-Encoder Only | Improvement |
85
- |---------|---------------|-----------------|-------------|
86
- | Core Exam Chest | 0.533 | 0.409 | +30.3% |
87
- | Core Exam Combined | 0.466 | 0.381 | +22.5% |
88
-
89
- The reranker provides significant gains on complex, multi-part queries typical of board exam questions.
90
-
91
- ### Published Benchmark Results
92
-
93
- From [Matulich & Mason, 2026](https://huggingface.co/matulichpt/radlit-biencoder):
94
-
95
- | Benchmark | RadLIT Result | Key Finding |
96
- |-----------|---------------|-------------|
97
- | NFCorpus nDCG@10 | 0.268 | **17.9x improvement** over RadBERT bi-encoder (0.015) |
98
- | VQA-RAD MRR | 0.972 | Near-perfect retrieval on radiology Q&A |
99
- | RadLIT-9 Thoracic | 0.736 nDCG@10 | **Best-in-class** (beat BGE-large, ColBERTv2) |
100
- | RadLIT-9 Pediatric | 0.625 nDCG@10 | **Best-in-class** (beat BGE-large, ColBERTv2) |
101
- | Zebra Test | 92% found rate | 2.1x improvement on rare conditions vs ColBERTv2 |
102
-
103
- **Vocabulary Alignment Hypothesis**: Domain training provides measurable advantage when queries use radiology-specific terminology that aligns with the training domain.
104
-
105
- ## Quick Start
106
-
107
- ### Installation
108
-
109
- ```bash
110
- pip install sentence-transformers>=2.2.0
111
- ```
112
-
113
- ### Basic Usage
114
-
115
- ```python
116
- from sentence_transformers import CrossEncoder
117
-
118
- # Load the reranker
119
- reranker = CrossEncoder("matulichpt/radlit-crossencoder", max_length=512)
120
-
121
- # Query and candidate documents
122
- query = "What are the imaging features of hepatocellular carcinoma?"
123
- documents = [
124
- "HCC typically shows arterial enhancement with portal venous washout on CT.",
125
- "Fatty liver disease presents as decreased attenuation on non-contrast CT.",
126
- "Hepatic hemangiomas show peripheral nodular enhancement.",
127
- ]
128
-
129
- # Create query-document pairs
130
- pairs = [[query, doc] for doc in documents]
131
-
132
- # Get relevance scores
133
- scores = reranker.predict(pairs)
134
-
135
- # Apply temperature calibration (RECOMMENDED)
136
- calibrated_scores = scores / 1.5
137
-
138
- print("Scores:", calibrated_scores)
139
- # Document about HCC will have highest score
140
- ```
141
-
142
- ### Temperature Calibration
143
-
144
- **Important**: This model outputs scores with high variance. Apply temperature scaling for better fusion with other signals:
145
-
146
- ```python
147
- # Raw scores might be: [4.2, -1.5, 0.8]
148
- # After calibration: [2.8, -1.0, 0.53]
149
-
150
- TEMPERATURE = 1.5 # Recommended value
151
-
152
- def calibrated_predict(reranker, pairs):
153
- raw_scores = reranker.predict(pairs)
154
- return raw_scores / TEMPERATURE
155
- ```
156
-
157
- ### Full Two-Stage Search Pipeline
158
-
159
- ```python
160
- from sentence_transformers import SentenceTransformer, CrossEncoder
161
- import numpy as np
162
-
163
- class RadLITESearch:
164
- def __init__(self, device="cuda"):
165
- # Stage 1: Fast bi-encoder
166
- self.encoder = SentenceTransformer(
167
- "matulichpt/radlit-biencoder",
168
- device=device
169
- )
170
- # Stage 2: Precise reranker
171
- self.reranker = CrossEncoder(
172
- "matulichpt/radlit-crossencoder",
173
- max_length=512,
174
- device=device
175
- )
176
- self.temperature = 1.5
177
- self.corpus_embeddings = None
178
- self.corpus = None
179
-
180
- def index_corpus(self, documents: list):
181
- """Pre-compute embeddings for your corpus."""
182
- self.corpus = documents
183
- self.corpus_embeddings = self.encoder.encode(
184
- documents,
185
- normalize_embeddings=True,
186
- show_progress_bar=True,
187
- batch_size=32
188
- )
189
-
190
- def search(self, query: str, top_k: int = 10, candidates: int = 50):
191
- """Two-stage search: retrieve then rerank."""
192
-
193
- # Stage 1: Bi-encoder retrieval
194
- query_emb = self.encoder.encode(query, normalize_embeddings=True)
195
- scores = query_emb @ self.corpus_embeddings.T
196
- top_indices = np.argsort(scores)[-candidates:][::-1]
197
-
198
- # Stage 2: Cross-encoder reranking
199
- candidate_docs = [self.corpus[i] for i in top_indices]
200
- pairs = [[query, doc] for doc in candidate_docs]
201
- rerank_scores = self.reranker.predict(pairs) / self.temperature
202
-
203
- # Sort by reranked scores
204
- sorted_indices = np.argsort(rerank_scores)[::-1]
205
-
206
- results = []
207
- for idx in sorted_indices[:top_k]:
208
- results.append({
209
- "document": candidate_docs[idx],
210
- "corpus_index": int(top_indices[idx]),
211
- "score": float(rerank_scores[idx]),
212
- "biencoder_score": float(scores[top_indices[idx]])
213
- })
214
- return results
215
-
216
-
217
- # Usage
218
- searcher = RadLITESearch()
219
- searcher.index_corpus(your_radiology_documents)
220
- results = searcher.search("pneumothorax CT findings")
221
- ```
222
-
223
- ## Integration with Any Corpus
224
-
225
- ### Radiopaedia / Educational Content
226
-
227
- ```python
228
- import json
229
-
230
- # Load your content (e.g., Radiopaedia articles)
231
- with open("radiopaedia_articles.json") as f:
232
- articles = json.load(f)
233
-
234
- corpus = [article["content"] for article in articles]
235
-
236
- # Initialize search
237
- searcher = RadLITESearch()
238
- searcher.index_corpus(corpus)
239
-
240
- # Search
241
- results = searcher.search("classic findings of pulmonary embolism on CTPA")
242
-
243
- for r in results[:5]:
244
- print(f"Score: {r['score']:.3f}")
245
- print(f"Content: {r['document'][:200]}...")
246
- print()
247
- ```
248
-
249
- ### Integration with Elasticsearch/OpenSearch
250
-
251
- ```python
252
- from sentence_transformers import CrossEncoder
253
-
254
- reranker = CrossEncoder("matulichpt/radlit-crossencoder", max_length=512)
255
-
256
- def rerank_elasticsearch_results(query: str, es_results: list, top_k: int = 10):
257
- """Rerank Elasticsearch BM25 results."""
258
- documents = [hit["_source"]["content"] for hit in es_results]
259
- pairs = [[query, doc] for doc in documents]
260
-
261
- scores = reranker.predict(pairs) / 1.5 # Temperature calibration
262
-
263
- # Combine with ES scores (optional)
264
- for i, hit in enumerate(es_results):
265
- hit["rerank_score"] = float(scores[i])
266
- hit["combined_score"] = 0.3 * hit["_score"] + 0.7 * scores[i]
267
-
268
- # Sort by combined score
269
- reranked = sorted(es_results, key=lambda x: x["combined_score"], reverse=True)
270
- return reranked[:top_k]
271
- ```
272
-
273
- ## Optimal Fusion Weights
274
-
275
- When combining multiple signals (bi-encoder, cross-encoder, BM25), use these weights:
276
-
277
- ```python
278
- # Optimal weights from grid search on RadLIT-9
279
- FUSION_WEIGHTS = {
280
- "biencoder": 0.5, # RadLITE-Encoder similarity
281
- "crossencoder": 0.2, # RadLITE-Reranker (after temp calibration)
282
- "bm25": 0.3 # Lexical matching (if available)
283
- }
284
-
285
- def fused_score(bienc_score, ce_score, bm25_score=0):
286
- return (
287
- FUSION_WEIGHTS["biencoder"] * bienc_score +
288
- FUSION_WEIGHTS["crossencoder"] * ce_score +
289
- FUSION_WEIGHTS["bm25"] * bm25_score
290
- )
291
- ```
292
-
293
- ## Architecture
294
-
295
- ```
296
- [Query] + [SEP] + [Document]
297
- |
298
- v
299
- [BERT Tokenizer]
300
- |
301
- v
302
- [MiniLM Encoder] (12 layers, 384 hidden)
303
- |
304
- v
305
- [Classification Head]
306
- |
307
- v
308
- Relevance Score (float)
309
- ```
310
-
311
- ## Training Details
312
-
313
- - **Base Model**: ms-marco-MiniLM-L-12-v2 (trained on MS MARCO passage ranking)
314
- - **Fine-tuning**: Radiology query-document relevance pairs
315
- - **Training Steps**: 5,626
316
- - **Best Validation Loss**: 0.691
317
- - **Learning Rate**: 2e-5
318
- - **Batch Size**: 32
319
- - **Category Weighting**: Yes (balanced across radiology subspecialties)
320
-
321
- ## Best Practices
322
-
323
- ### 1. Always Use Temperature Calibration
324
-
325
- Raw cross-encoder scores can be extreme. Temperature scaling (1.5) produces better fusion:
326
-
327
- ```python
328
- calibrated = raw_score / 1.5
329
- ```
330
-
331
- ### 2. Limit Candidates for Reranking
332
-
333
- Cross-encoders are slow. Only rerank top 50-100 candidates from bi-encoder:
334
-
335
- ```python
336
- # Good: Rerank top 50
337
- rerank_candidates = 50
338
-
339
- # Bad: Rerank entire corpus
340
- rerank_candidates = len(corpus) # Too slow!
341
- ```
342
-
343
- ### 3. Batch Predictions
344
-
345
- ```python
346
- # Efficient: Single batch call
347
- pairs = [[query, doc] for doc in candidates]
348
- scores = reranker.predict(pairs, batch_size=32)
349
-
350
- # Inefficient: Individual calls
351
- scores = [reranker.predict([[query, doc]])[0] for doc in candidates]
352
- ```
353
-
354
- ### 4. GPU Acceleration
355
-
356
- ```python
357
- reranker = CrossEncoder(
358
- "matulichpt/radlit-crossencoder",
359
- max_length=512,
360
- device="cuda" # Use GPU
361
- )
362
- ```
363
-
364
- ## Limitations
365
-
366
- - **English only**: Trained on English radiology text
367
- - **Speed**: ~10-50 pairs/second (use for reranking, not full corpus)
368
- - **512 token limit**: Long documents are truncated
369
- - **Domain-specific**: Optimized for radiology, may underperform on general medical content
370
-
371
- ## Citation
372
-
373
- If you use RadLITE in your work, please cite:
374
-
375
- ```bibtex
376
- @article{matulich2026radlit,
377
- title = {Late Interaction Retrieval Unlocks Domain Knowledge in Radiology Language Models},
378
- author = {Matulich, Patrick and Mason, Dan},
379
- year = {2026},
380
- journal = {Radiology: Artificial Intelligence},
381
- note = {17.9x improvement over RadBERT; best-in-class on Thoracic/Pediatric subspecialties},
382
- url = {https://huggingface.co/matulichpt/radlit-biencoder}
383
- }
384
- ```
385
-
386
- ## Related Models
387
-
388
- - [RadLITE-Encoder](https://huggingface.co/matulichpt/radlit-biencoder) - Bi-encoder for first-stage retrieval
389
- - [RadBERT-RoBERTa-4m](https://huggingface.co/zzxslp/RadBERT-RoBERTa-4m) - Base radiology language model
390
-
391
- ## License
392
-
393
- Apache 2.0 - Free for commercial and research use.
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - cross-encoder
7
+ - reranker
8
+ - retrieval
9
+ - sentence-similarity
10
+ base_model: cross-encoder/ms-marco-MiniLM-L-12-v2
11
+ pipeline_tag: text-classification
12
+ library_name: sentence-transformers
13
+ metrics:
14
+ - mrr
15
+ - ndcg
16
+ model-index:
17
+ - name: RadLITE-Reranker
18
+ results:
19
+ - task:
20
+ type: reranking
21
+ name: Document Reranking
22
+ dataset:
23
+ name: RadLIT-9 (Radiology Retrieval Benchmark)
24
+ type: radiology-retrieval
25
+ metrics:
26
+ - type: mrr
27
+ value: 0.829
28
+ name: MRR (with bi-encoder)
29
+ - type: mrr
30
+ value: 0.533
31
+ name: MRR on ABR Core Exam (Chest)
32
+ ---
33
+
34
+ # RadLITE-Reranker
35
+
36
+ **Radiology Late Interaction Transformer Enhanced - Cross-Encoder Reranker**
37
+
38
+ A domain-specialized cross-encoder for reranking radiology search results. This model takes a query-document pair and predicts a relevance score, providing more accurate ranking than bi-encoder similarity alone.
39
+
40
+ > **Recommended:** Use this reranker together with [RadLITE-Encoder](https://huggingface.co/matulichpt/radlit-biencoder) in a two-stage pipeline for optimal performance. The bi-encoder handles fast retrieval over large corpora, then this cross-encoder reranks the top candidates for precision. This combination achieves **MRR 0.829** on radiology retrieval benchmarks.
41
+
42
+ ## Model Description
43
+
44
+ | Property | Value |
45
+ |----------|-------|
46
+ | **Model Type** | Cross-Encoder (Reranker) |
47
+ | **Base Model** | [ms-marco-MiniLM-L-12-v2](https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-12-v2) |
48
+ | **Domain** | Radiology / Medical Imaging |
49
+ | **Hidden Size** | 384 |
50
+ | **Max Sequence Length** | 512 tokens |
51
+ | **Output** | Single relevance score |
52
+ | **License** | Apache 2.0 |
53
+
54
+ ### Why Use a Reranker?
55
+
56
+ Bi-encoders (like RadLITE-Encoder) are fast but encode query and document independently. Cross-encoders process them together, capturing fine-grained interactions:
57
+
58
+ | Approach | Speed | Accuracy | Use Case |
59
+ |----------|-------|----------|----------|
60
+ | Bi-Encoder | Fast (1000s docs/sec) | Good | First-stage retrieval |
61
+ | Cross-Encoder | Slow (10s docs/sec) | Excellent | Reranking top candidates |
62
+
63
+ **Two-stage pipeline**: Use bi-encoder to get top 50-100 candidates, then rerank with cross-encoder for best results.
64
+
65
+ ## Performance
66
+
67
+ ### Impact on RadLIT-9 Benchmark
68
+
69
+ | Configuration | MRR | Improvement |
70
+ |---------------|-----|-------------|
71
+ | Bi-Encoder only | 0.78 | baseline |
72
+ | **Bi-Encoder + Reranker** | **0.829** | **+6.3%** |
73
+
74
+ ### ABR Core Exam (Board-Style Questions)
75
+
76
+ Comparing two-stage pipeline (bi-encoder + reranker) vs bi-encoder alone:
77
+
78
+ | Dataset | Two-Stage MRR | Bi-Encoder Only | Improvement |
79
+ |---------|---------------|-----------------|-------------|
80
+ | Core Exam Chest | 0.533 | 0.409 | +30.3% |
81
+ | Core Exam Combined | 0.466 | 0.381 | +22.5% |
82
+
83
+ The reranker provides significant gains on complex, multi-part queries typical of board exam questions.
84
+
85
+ ### Published Benchmark Results
86
+
87
+ From [Matulich & Mason, 2026](https://huggingface.co/matulichpt/radlit-biencoder):
88
+
89
+ | Benchmark | RadLIT Result | Key Finding |
90
+ |-----------|---------------|-------------|
91
+ | NFCorpus nDCG@10 | 0.268 | **17.9x improvement** over RadBERT bi-encoder (0.015) |
92
+ | VQA-RAD MRR | 0.972 | Near-perfect retrieval on radiology Q&A |
93
+ | RadLIT-9 Thoracic | 0.736 nDCG@10 | **Best-in-class** (beat BGE-large, ColBERTv2) |
94
+ | RadLIT-9 Pediatric | 0.625 nDCG@10 | **Best-in-class** (beat BGE-large, ColBERTv2) |
95
+ | Zebra Test | 92% found rate | 2.1x improvement on rare conditions vs ColBERTv2 |
96
+
97
+ **Vocabulary Alignment Hypothesis**: Domain training provides measurable advantage when queries use radiology-specific terminology that aligns with the training domain.
98
+
99
+ ## Quick Start
100
+
101
+ ### Installation
102
+
103
+ ```bash
104
+ pip install sentence-transformers>=2.2.0
105
+ ```
106
+
107
+ ### Basic Usage
108
+
109
+ ```python
110
+ from sentence_transformers import CrossEncoder
111
+
112
+ # Load the reranker
113
+ reranker = CrossEncoder("matulichpt/radlit-crossencoder", max_length=512)
114
+
115
+ # Query and candidate documents
116
+ query = "What are the imaging features of hepatocellular carcinoma?"
117
+ documents = [
118
+ "HCC typically shows arterial enhancement with portal venous washout on CT.",
119
+ "Fatty liver disease presents as decreased attenuation on non-contrast CT.",
120
+ "Hepatic hemangiomas show peripheral nodular enhancement.",
121
+ ]
122
+
123
+ # Create query-document pairs
124
+ pairs = [[query, doc] for doc in documents]
125
+
126
+ # Get relevance scores
127
+ scores = reranker.predict(pairs)
128
+
129
+ # Apply temperature calibration (RECOMMENDED)
130
+ calibrated_scores = scores / 1.5
131
+
132
+ print("Scores:", calibrated_scores)
133
+ # Document about HCC will have highest score
134
+ ```
135
+
136
+ ### Temperature Calibration
137
+
138
+ **Important**: This model outputs scores with high variance. Apply temperature scaling for better fusion with other signals:
139
+
140
+ ```python
141
+ # Raw scores might be: [4.2, -1.5, 0.8]
142
+ # After calibration: [2.8, -1.0, 0.53]
143
+
144
+ TEMPERATURE = 1.5 # Recommended value
145
+
146
+ def calibrated_predict(reranker, pairs):
147
+ raw_scores = reranker.predict(pairs)
148
+ return raw_scores / TEMPERATURE
149
+ ```
150
+
151
+ ### Full Two-Stage Search Pipeline
152
+
153
+ ```python
154
+ from sentence_transformers import SentenceTransformer, CrossEncoder
155
+ import numpy as np
156
+
157
+ class RadLITESearch:
158
+ def __init__(self, device="cuda"):
159
+ # Stage 1: Fast bi-encoder
160
+ self.encoder = SentenceTransformer(
161
+ "matulichpt/radlit-biencoder",
162
+ device=device
163
+ )
164
+ # Stage 2: Precise reranker
165
+ self.reranker = CrossEncoder(
166
+ "matulichpt/radlit-crossencoder",
167
+ max_length=512,
168
+ device=device
169
+ )
170
+ self.temperature = 1.5
171
+ self.corpus_embeddings = None
172
+ self.corpus = None
173
+
174
+ def index_corpus(self, documents: list):
175
+ """Pre-compute embeddings for your corpus."""
176
+ self.corpus = documents
177
+ self.corpus_embeddings = self.encoder.encode(
178
+ documents,
179
+ normalize_embeddings=True,
180
+ show_progress_bar=True,
181
+ batch_size=32
182
+ )
183
+
184
+ def search(self, query: str, top_k: int = 10, candidates: int = 50):
185
+ """Two-stage search: retrieve then rerank."""
186
+
187
+ # Stage 1: Bi-encoder retrieval
188
+ query_emb = self.encoder.encode(query, normalize_embeddings=True)
189
+ scores = query_emb @ self.corpus_embeddings.T
190
+ top_indices = np.argsort(scores)[-candidates:][::-1]
191
+
192
+ # Stage 2: Cross-encoder reranking
193
+ candidate_docs = [self.corpus[i] for i in top_indices]
194
+ pairs = [[query, doc] for doc in candidate_docs]
195
+ rerank_scores = self.reranker.predict(pairs) / self.temperature
196
+
197
+ # Sort by reranked scores
198
+ sorted_indices = np.argsort(rerank_scores)[::-1]
199
+
200
+ results = []
201
+ for idx in sorted_indices[:top_k]:
202
+ results.append({
203
+ "document": candidate_docs[idx],
204
+ "corpus_index": int(top_indices[idx]),
205
+ "score": float(rerank_scores[idx]),
206
+ "biencoder_score": float(scores[top_indices[idx]])
207
+ })
208
+ return results
209
+
210
+
211
+ # Usage
212
+ searcher = RadLITESearch()
213
+ searcher.index_corpus(your_radiology_documents)
214
+ results = searcher.search("pneumothorax CT findings")
215
+ ```
216
+
217
+ ## Integration with Any Corpus
218
+
219
+ ### Radiopaedia / Educational Content
220
+
221
+ ```python
222
+ import json
223
+
224
+ # Load your content (e.g., Radiopaedia articles)
225
+ with open("radiopaedia_articles.json") as f:
226
+ articles = json.load(f)
227
+
228
+ corpus = [article["content"] for article in articles]
229
+
230
+ # Initialize search
231
+ searcher = RadLITESearch()
232
+ searcher.index_corpus(corpus)
233
+
234
+ # Search
235
+ results = searcher.search("classic findings of pulmonary embolism on CTPA")
236
+
237
+ for r in results[:5]:
238
+ print(f"Score: {r['score']:.3f}")
239
+ print(f"Content: {r['document'][:200]}...")
240
+ print()
241
+ ```
242
+
243
+ ### Integration with Elasticsearch/OpenSearch
244
+
245
+ ```python
246
+ from sentence_transformers import CrossEncoder
247
+
248
+ reranker = CrossEncoder("matulichpt/radlit-crossencoder", max_length=512)
249
+
250
+ def rerank_elasticsearch_results(query: str, es_results: list, top_k: int = 10):
251
+ """Rerank Elasticsearch BM25 results."""
252
+ documents = [hit["_source"]["content"] for hit in es_results]
253
+ pairs = [[query, doc] for doc in documents]
254
+
255
+ scores = reranker.predict(pairs) / 1.5 # Temperature calibration
256
+
257
+ # Combine with ES scores (optional)
258
+ for i, hit in enumerate(es_results):
259
+ hit["rerank_score"] = float(scores[i])
260
+ hit["combined_score"] = 0.3 * hit["_score"] + 0.7 * scores[i]
261
+
262
+ # Sort by combined score
263
+ reranked = sorted(es_results, key=lambda x: x["combined_score"], reverse=True)
264
+ return reranked[:top_k]
265
+ ```
266
+
267
+ ## Optimal Fusion Weights
268
+
269
+ When combining multiple signals (bi-encoder, cross-encoder, BM25), use these weights:
270
+
271
+ ```python
272
+ # Optimal weights from grid search on RadLIT-9
273
+ FUSION_WEIGHTS = {
274
+ "biencoder": 0.5, # RadLITE-Encoder similarity
275
+ "crossencoder": 0.2, # RadLITE-Reranker (after temp calibration)
276
+ "bm25": 0.3 # Lexical matching (if available)
277
+ }
278
+
279
+ def fused_score(bienc_score, ce_score, bm25_score=0):
280
+ return (
281
+ FUSION_WEIGHTS["biencoder"] * bienc_score +
282
+ FUSION_WEIGHTS["crossencoder"] * ce_score +
283
+ FUSION_WEIGHTS["bm25"] * bm25_score
284
+ )
285
+ ```
286
+
287
+ ## Architecture
288
+
289
+ ```
290
+ [Query] + [SEP] + [Document]
291
+ |
292
+ v
293
+ [BERT Tokenizer]
294
+ |
295
+ v
296
+ [MiniLM Encoder] (12 layers, 384 hidden)
297
+ |
298
+ v
299
+ [Classification Head]
300
+ |
301
+ v
302
+ Relevance Score (float)
303
+ ```
304
+
305
+ ## Training Details
306
+
307
+ - **Base Model**: ms-marco-MiniLM-L-12-v2 (trained on MS MARCO passage ranking)
308
+ - **Fine-tuning**: Radiology query-document relevance pairs
309
+ - **Training Steps**: 5,626
310
+ - **Best Validation Loss**: 0.691
311
+ - **Learning Rate**: 2e-5
312
+ - **Batch Size**: 32
313
+ - **Category Weighting**: Yes (balanced across radiology subspecialties)
314
+
315
+ ## Best Practices
316
+
317
+ ### 1. Always Use Temperature Calibration
318
+
319
+ Raw cross-encoder scores can be extreme. Temperature scaling (1.5) produces better fusion:
320
+
321
+ ```python
322
+ calibrated = raw_score / 1.5
323
+ ```
324
+
325
+ ### 2. Limit Candidates for Reranking
326
+
327
+ Cross-encoders are slow. Only rerank top 50-100 candidates from bi-encoder:
328
+
329
+ ```python
330
+ # Good: Rerank top 50
331
+ rerank_candidates = 50
332
+
333
+ # Bad: Rerank entire corpus
334
+ rerank_candidates = len(corpus) # Too slow!
335
+ ```
336
+
337
+ ### 3. Batch Predictions
338
+
339
+ ```python
340
+ # Efficient: Single batch call
341
+ pairs = [[query, doc] for doc in candidates]
342
+ scores = reranker.predict(pairs, batch_size=32)
343
+
344
+ # Inefficient: Individual calls
345
+ scores = [reranker.predict([[query, doc]])[0] for doc in candidates]
346
+ ```
347
+
348
+ ### 4. GPU Acceleration
349
+
350
+ ```python
351
+ reranker = CrossEncoder(
352
+ "matulichpt/radlit-crossencoder",
353
+ max_length=512,
354
+ device="cuda" # Use GPU
355
+ )
356
+ ```
357
+
358
+ ## Limitations
359
+
360
+ - **English only**: Trained on English radiology text
361
+ - **Speed**: ~10-50 pairs/second (use for reranking, not full corpus)
362
+ - **512 token limit**: Long documents are truncated
363
+ - **Domain-specific**: Optimized for radiology, may underperform on general medical content
364
+
365
+ ## Citation
366
+
367
+ If you use RadLITE in your work, please cite:
368
+
369
+ ```bibtex
370
+ @article{matulich2026radlit,
371
+ title = {Late Interaction Retrieval Unlocks Domain Knowledge in Radiology Language Models},
372
+ author = {Matulich, Patrick and Mason, Dan},
373
+ year = {2026},
374
+ journal = {Radiology: Artificial Intelligence},
375
+ note = {17.9x improvement over RadBERT; best-in-class on Thoracic/Pediatric subspecialties},
376
+ url = {https://huggingface.co/matulichpt/radlit-biencoder}
377
+ }
378
+ ```
379
+
380
+ ## Related Models
381
+
382
+ - [RadLITE-Encoder](https://huggingface.co/matulichpt/radlit-biencoder) - Bi-encoder for first-stage retrieval
383
+ - [RadBERT-RoBERTa-4m](https://huggingface.co/zzxslp/RadBERT-RoBERTa-4m) - Base radiology language model
384
+
385
+ ## License
386
+
387
+ Apache 2.0 - Free for commercial and research use.