Initial release: checkpoints, loader and model card
Browse files- README.md +146 -0
- checkpoint_manifest.json +61 -0
- checkpoints/fold0/f1.ckpt +3 -0
- checkpoints/fold0/ndcg.ckpt +3 -0
- checkpoints/fold1/f1.ckpt +3 -0
- checkpoints/fold1/ndcg.ckpt +3 -0
- checkpoints/fold2/f1.ckpt +3 -0
- checkpoints/fold2/ndcg.ckpt +3 -0
- checkpoints/fold3/f1.ckpt +3 -0
- checkpoints/fold3/ndcg.ckpt +3 -0
- checkpoints/fold4/f1.ckpt +3 -0
- checkpoints/fold4/ndcg.ckpt +3 -0
- crossgat_hf.py +93 -0
README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
language: [en]
|
| 4 |
+
tags:
|
| 5 |
+
- clinical-trials
|
| 6 |
+
- information-retrieval
|
| 7 |
+
- reranking
|
| 8 |
+
- graph-neural-network
|
| 9 |
+
- biomedical
|
| 10 |
+
library_name: pytorch
|
| 11 |
+
pipeline_tag: text-classification
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# CrossGAT Trial Eligibility — 5-fold CV checkpoints
|
| 15 |
+
|
| 16 |
+
A 3-class eligibility model that **reranks clinical-trial retrieval candidates**.
|
| 17 |
+
It scores a patient description against a trial's inclusion/exclusion criteria as
|
| 18 |
+
`irrelevant / excluded / eligible`, by fusing a graph encoder over parsed
|
| 19 |
+
eligibility criteria with a text branch over whole-document embeddings.
|
| 20 |
+
|
| 21 |
+
These are the **five cross-validation checkpoints** — one per fold, so each is
|
| 22 |
+
trained on 80% of the data and never saw its own fold's topics. Use them to
|
| 23 |
+
reproduce the reported numbers, or ensemble them. For a single deployment model,
|
| 24 |
+
see [crossgat-trial-eligibility-full](https://huggingface.co/2001jdev/crossgat-trial-eligibility-full).
|
| 25 |
+
|
| 26 |
+
## ⚠️ This model is not self-contained
|
| 27 |
+
|
| 28 |
+
It does **not** take raw text. It takes *parsed eligibility graphs* — entity and
|
| 29 |
+
relation structures extracted from trial criteria and patient descriptions by an
|
| 30 |
+
LLM. Producing those needs the generator from the project repo plus an LLM
|
| 31 |
+
endpoint (~38 GB VRAM).
|
| 32 |
+
|
| 33 |
+
**If you just want to reproduce the results**, the graphs for all 13,229 TREC CT
|
| 34 |
+
trials are published as a dataset — no LLM needed:
|
| 35 |
+
[clinical-trials-eligibility-graphs-rerank](https://huggingface.co/datasets/2001jdev/clinical-trials-eligibility-graphs-rerank).
|
| 36 |
+
|
| 37 |
+
## What it is for, and what it is not for
|
| 38 |
+
|
| 39 |
+
**It reranks. It does not retrieve.** Trained on an eligibility target, it reorders
|
| 40 |
+
*within* the candidate set a first-stage retriever already found — promoting
|
| 41 |
+
eligible trials over merely topically-relevant ones.
|
| 42 |
+
|
| 43 |
+
**It cannot judge disease relevance.** Per-topic AUC for relevant-vs-rest is
|
| 44 |
+
**0.533** — chance. Whether a trial is even on the right disease is entirely the
|
| 45 |
+
first stage's job. This is why it must be *fused* with retrieval, never used to
|
| 46 |
+
replace it.
|
| 47 |
+
|
| 48 |
+
## Recommended configuration
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
score = (1 - w) * retrieval_norm + w * p_eligible_norm with w = 0.5
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Both terms min-max normalised **within a topic**. `p_eligible` = P(eligible) alone
|
| 55 |
+
wins over the alternatives in all four runs, at every cutoff, in both metric
|
| 56 |
+
families — even though `expected_gain` models the graded gain directly and is what
|
| 57 |
+
training optimises.
|
| 58 |
+
|
| 59 |
+
## Results
|
| 60 |
+
|
| 61 |
+
5-fold cross-validation, TREC Clinical Trials 2021–23, **162 topics**, paired
|
| 62 |
+
Wilcoxon signed-rank against each run's own first stage. Metric is
|
| 63 |
+
**graded NDCG@10** (gain `2^label - 1` = 0/1/3).
|
| 64 |
+
|
| 65 |
+
First stages: hybrid BM25+`embeddinggemma-300m-medical` (retrieval 0.5973) and
|
| 66 |
+
dense-only `MedEmbed-base-v0.1` (retrieval 0.5187).
|
| 67 |
+
|
| 68 |
+
| Variant | gemma-med | Δ | medembed | Δ |
|
| 69 |
+
|---|---|---|---|---|
|
| 70 |
+
| w=0.3 `p_eligible` | 0.6340 | +0.0367 *** | 0.5467 | +0.0280 *** |
|
| 71 |
+
| w=0.3 `expected_gain` | 0.6300 | +0.0327 *** | 0.5433 | +0.0246 *** |
|
| 72 |
+
| w=0.3 `one_minus_irrelevant` | 0.6230 | +0.0257 *** | 0.5334 | +0.0147 *** |
|
| 73 |
+
| w=0.5 `p_eligible` | 0.6515 | +0.0542 *** | 0.5644 | +0.0457 *** |
|
| 74 |
+
| w=0.5 `expected_gain` | 0.6485 | +0.0512 *** | 0.5572 | +0.0385 *** |
|
| 75 |
+
| w=0.5 `one_minus_irrelevant` | 0.6302 | +0.0329 *** | 0.5377 | +0.0190 ** |
|
| 76 |
+
| w=0.7 `p_eligible` | 0.6491 | +0.0518 *** | 0.5548 | +0.0361 *** |
|
| 77 |
+
| w=0.7 `expected_gain` | 0.6455 | +0.0482 *** | 0.5536 | +0.0349 *** |
|
| 78 |
+
| w=0.7 `one_minus_irrelevant` | 0.6262 | +0.0289 ** | 0.5405 | +0.0218 ** |
|
| 79 |
+
| w=1 `p_eligible` | 0.6191 | +0.0218 n.s. | 0.5202 | +0.0015 n.s. |
|
| 80 |
+
| w=1 `expected_gain` | 0.6163 | +0.0190 n.s. | 0.5222 | +0.0035 n.s. |
|
| 81 |
+
| w=1 `one_minus_irrelevant` | 0.6049 | +0.0076 n.s. | 0.5148 | -0.0039 n.s. |
|
| 82 |
+
|
| 83 |
+
`***` p<0.001, `**` p<0.01, `*` p<0.05.
|
| 84 |
+
|
| 85 |
+
**Read both metric families.** `raw` scores unjudged documents as 0 (punishing a
|
| 86 |
+
system for surfacing trials TREC never pooled); `condensed` deletes them first
|
| 87 |
+
(flattering a system that surfaces many). Neither is honest alone — the gap between
|
| 88 |
+
them, read against `judged@10`, says how far apart they can be. Where the two agree
|
| 89 |
+
the result is robust; at w=1.0 they diverge sharply and `raw` loses significance
|
| 90 |
+
entirely.
|
| 91 |
+
|
| 92 |
+
## Which checkpoint to load
|
| 93 |
+
|
| 94 |
+
Each fold ships two: `ndcg.ckpt` (selected on `val_ndcg10`) and `f1.ckpt`
|
| 95 |
+
(selected on `val_macro_f1`).
|
| 96 |
+
|
| 97 |
+
**Every number above uses `ndcg`.** The `f1` checkpoints classify better
|
| 98 |
+
(macro F1 0.597 ± 0.020 vs 0.561 ± 0.035 on held-out folds) but have **not** been
|
| 99 |
+
benchmarked for reranking — offered for completeness, not recommended blindly.
|
| 100 |
+
|
| 101 |
+
Note `val_ndcg10` is flat across epochs 1–5, so the per-fold selected epochs
|
| 102 |
+
(1/2/5/2/4) are largely noise around a plateau. Do not read them as convergence.
|
| 103 |
+
|
| 104 |
+
## Usage
|
| 105 |
+
|
| 106 |
+
```python
|
| 107 |
+
pip install "crossgat @ git+https://github.com/JDev2001/msc_v2"
|
| 108 |
+
|
| 109 |
+
from crossgat_hf import load, score, rerank_score
|
| 110 |
+
|
| 111 |
+
pipe = load("2001jdev/crossgat-trial-eligibility-cv", fold=0, metric="ndcg")
|
| 112 |
+
out = score(pipe, patient_graph, inc_graph, exc_graph,
|
| 113 |
+
patient_text="65yo male, type 2 diabetes, HbA1c 8.1% ...",
|
| 114 |
+
trial_text="Metformin in adults with type 2 diabetes ...")
|
| 115 |
+
|
| 116 |
+
out["class_probs"] # {'irrelevant': .., 'excluded': .., 'eligible': ..}
|
| 117 |
+
rerank_score(out) # p_eligible — fuse this with your retrieval score
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
`patient_text` and `trial_text` are **required**: the text branch is part of the
|
| 121 |
+
model, and omitting them raises rather than silently scoring an all-zero branch.
|
| 122 |
+
|
| 123 |
+
## Training
|
| 124 |
+
|
| 125 |
+
- **Data:** TREC Clinical Trials 2021–23 qrels/topics; eligibility criteria parsed into graphs (CHIA-style entities/relations)
|
| 126 |
+
- **Objective:** cross-entropy + **LambdaRank** (k=10) on topic-grouped batches (4 topics/batch)
|
| 127 |
+
- **Hard negatives:** 40/topic drawn from the retrieval candidate distribution (+5,307 samples), mined from *hybrid gemma-med* run files
|
| 128 |
+
- **Text branch:** `abhinand/MedEmbed-base-v0.1`, whole-document embeddings of patient description and trial title+summary
|
| 129 |
+
- **Splits:** 5-fold CV over patients, seed fixed; each fold's test topics never seen in its training
|
| 130 |
+
|
| 131 |
+
## Limitations
|
| 132 |
+
|
| 133 |
+
- **The first stage is the ceiling.** medembed *with* reranking still scores below gemma-med *without* it. A better retriever buys more than more reranker tuning.
|
| 134 |
+
- **Do not use w=1.0.** Pure model ranking loses significance on both retrievers (p=0.65 / 0.85). It ranks better inside the judged pool but drifts out of it — `judged@10` falls from 0.92 to 0.80.
|
| 135 |
+
- **The hard-negative gain is retriever-sensitive.** +0.0175 (p=0.007) on gemma-med, +0.0148 (p=0.050) on medembed — the negatives were mined from gemma-med candidates and transfer only partly.
|
| 136 |
+
- **Within-collection evidence only.** 162 topics, one corpus, one set of assessors. Nothing about generalisation to another trial collection follows.
|
| 137 |
+
- **Not a clinical device.** Research artefact. It does not determine patient eligibility and must not be used to make enrolment decisions.
|
| 138 |
+
|
| 139 |
+
## Citation & provenance
|
| 140 |
+
|
| 141 |
+
Trained on TREC Clinical Trials track data (2021–23). Users must comply with the
|
| 142 |
+
TREC data usage agreements for the underlying collection and relevance judgments.
|
| 143 |
+
|
| 144 |
+
Checkpoint SHA-256 prefixes and the exact training configuration are in
|
| 145 |
+
`checkpoint_manifest.json`. Every benchmark run in the source repository writes a
|
| 146 |
+
`manifest.json` recording checkpoint hashes, git state and sanity checks.
|
checkpoint_manifest.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cv": {
|
| 3 |
+
"fold0/ndcg": {
|
| 4 |
+
"original_name": "joint-fold0-ndcg-epoch=01-val_ndcg10=0.6293.ckpt",
|
| 5 |
+
"sha256_12": "d64902e332cd",
|
| 6 |
+
"size_mb": 27.3
|
| 7 |
+
},
|
| 8 |
+
"fold0/f1": {
|
| 9 |
+
"original_name": "joint-fold0-f1-epoch=15-val_macro_f1=0.6327.ckpt",
|
| 10 |
+
"sha256_12": "af1611c7a7a8",
|
| 11 |
+
"size_mb": 27.3
|
| 12 |
+
},
|
| 13 |
+
"fold1/ndcg": {
|
| 14 |
+
"original_name": "joint-fold1-ndcg-epoch=02-val_ndcg10=0.6497.ckpt",
|
| 15 |
+
"sha256_12": "8c9e5b222ece",
|
| 16 |
+
"size_mb": 27.3
|
| 17 |
+
},
|
| 18 |
+
"fold1/f1": {
|
| 19 |
+
"original_name": "joint-fold1-f1-epoch=07-val_macro_f1=0.6015.ckpt",
|
| 20 |
+
"sha256_12": "69b5668b994c",
|
| 21 |
+
"size_mb": 27.3
|
| 22 |
+
},
|
| 23 |
+
"fold2/ndcg": {
|
| 24 |
+
"original_name": "joint-fold2-ndcg-epoch=05-val_ndcg10=0.5998.ckpt",
|
| 25 |
+
"sha256_12": "9ca8c1c3419f",
|
| 26 |
+
"size_mb": 27.3
|
| 27 |
+
},
|
| 28 |
+
"fold2/f1": {
|
| 29 |
+
"original_name": "joint-fold2-f1-epoch=04-val_macro_f1=0.5922.ckpt",
|
| 30 |
+
"sha256_12": "c3ee31254421",
|
| 31 |
+
"size_mb": 27.3
|
| 32 |
+
},
|
| 33 |
+
"fold3/ndcg": {
|
| 34 |
+
"original_name": "joint-fold3-ndcg-epoch=02-val_ndcg10=0.6571.ckpt",
|
| 35 |
+
"sha256_12": "9cfb1cf3cb33",
|
| 36 |
+
"size_mb": 27.3
|
| 37 |
+
},
|
| 38 |
+
"fold3/f1": {
|
| 39 |
+
"original_name": "joint-fold3-f1-epoch=05-val_macro_f1=0.5765.ckpt",
|
| 40 |
+
"sha256_12": "5236ff7beccb",
|
| 41 |
+
"size_mb": 27.3
|
| 42 |
+
},
|
| 43 |
+
"fold4/ndcg": {
|
| 44 |
+
"original_name": "joint-fold4-ndcg-epoch=04-val_ndcg10=0.7009.ckpt",
|
| 45 |
+
"sha256_12": "c29677ab75dc",
|
| 46 |
+
"size_mb": 27.3
|
| 47 |
+
},
|
| 48 |
+
"fold4/f1": {
|
| 49 |
+
"original_name": "joint-fold4-f1-epoch=07-val_macro_f1=0.5839.ckpt",
|
| 50 |
+
"sha256_12": "355b93e402fd",
|
| 51 |
+
"size_mb": 27.3
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"full": {
|
| 55 |
+
"model": {
|
| 56 |
+
"original_name": "joint-fulldata-epoch=03.ckpt",
|
| 57 |
+
"sha256_12": "dbd1930dc442",
|
| 58 |
+
"size_mb": 27.3
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
checkpoints/fold0/f1.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:af1611c7a7a84b0f0998feb076167c99d8b1aea9d0de6771d77249384c07d7ae
|
| 3 |
+
size 28643449
|
checkpoints/fold0/ndcg.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d64902e332cd9b93757209a56a0557cc3fa11dd12e7b1d351ade9878ca3e6deb
|
| 3 |
+
size 28643449
|
checkpoints/fold1/f1.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69b5668b994cb226776bad3d8e48165044d04764e768581c0d320b19f76c8eb2
|
| 3 |
+
size 28643513
|
checkpoints/fold1/ndcg.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8c9e5b222ecec6b3147eba3dc95085f152d45126596862867edb4130ad60e240
|
| 3 |
+
size 28643513
|
checkpoints/fold2/f1.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c3ee312544215487a8363d46a8e486db9c75c60e2c8a0cbfce57f3ea86f1f22b
|
| 3 |
+
size 28643513
|
checkpoints/fold2/ndcg.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9ca8c1c3419f5dde902aff1c8a59530e6f259838b8f2866e5357319b890ff4d1
|
| 3 |
+
size 28643513
|
checkpoints/fold3/f1.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5236ff7beccba7c851f052cbae16eb600211e56e93f47fdecf3b00ff7cc073ae
|
| 3 |
+
size 28643513
|
checkpoints/fold3/ndcg.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9cfb1cf3cb3347f651f8cf3cc9a0a25c2702a58baaf0f491c177b3009ce90727
|
| 3 |
+
size 28643513
|
checkpoints/fold4/f1.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:355b93e402fd9617ba806bd439befc66737f3acb2dcb4ae0a2592d7250e5148a
|
| 3 |
+
size 28643513
|
checkpoints/fold4/ndcg.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c29677ab75dcd3ebd3e0e919d05300af3933c161631ab068bb157488d4d7912e
|
| 3 |
+
size 28643513
|
crossgat_hf.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load a CrossGAT eligibility checkpoint from this repo and score a pair.
|
| 2 |
+
|
| 3 |
+
This model is NOT self-contained. It scores a *patient graph* against a trial's
|
| 4 |
+
*inclusion/exclusion graphs* — parsed entity/relation structures, not raw text.
|
| 5 |
+
Producing those graphs needs the eligibility-graph generator from the project
|
| 6 |
+
repository, which in turn needs an LLM endpoint. See the README.
|
| 7 |
+
|
| 8 |
+
What this file gives you is the loading and scoring half: given graphs (from the
|
| 9 |
+
companion dataset, or from your own generator), it returns the 3-class posterior.
|
| 10 |
+
|
| 11 |
+
pip install "crossgat @ git+https://github.com/JDev2001/msc_v2"
|
| 12 |
+
from crossgat_hf import load, score
|
| 13 |
+
|
| 14 |
+
pipe = load("2001jdev/crossgat-trial-eligibility-cv", fold=0, metric="ndcg")
|
| 15 |
+
out = score(pipe, patient_graph, inc_graph, exc_graph,
|
| 16 |
+
patient_text="65yo male, type 2 diabetes, HbA1c 8.1%",
|
| 17 |
+
trial_text="Metformin in adults with type 2 diabetes ...")
|
| 18 |
+
out["probs"] # [P(irrelevant), P(excluded), P(eligible)]
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
CLASSES = ("irrelevant", "excluded", "eligible")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load(repo_id: str, fold: int | None = None, metric: str = "ndcg",
|
| 28 |
+
revision: str | None = None):
|
| 29 |
+
"""Download one checkpoint and return a ready InferencePipeline.
|
| 30 |
+
|
| 31 |
+
fold=None loads ``checkpoints/model.ckpt`` (the full-data refit repo);
|
| 32 |
+
otherwise ``checkpoints/fold{fold}/{metric}.ckpt``.
|
| 33 |
+
|
| 34 |
+
metric selects which checkpoint the fold's training saved:
|
| 35 |
+
"ndcg" -- selected on val_ndcg10, used for every benchmark number reported
|
| 36 |
+
"f1" -- selected on val_macro_f1; classifies better (macro F1 .597 vs .561)
|
| 37 |
+
but has NOT been benchmarked for reranking.
|
| 38 |
+
"""
|
| 39 |
+
from huggingface_hub import hf_hub_download
|
| 40 |
+
from crossgat.inference.pipeline import InferencePipeline
|
| 41 |
+
|
| 42 |
+
if metric not in ("ndcg", "f1"):
|
| 43 |
+
raise ValueError(f"metric must be 'ndcg' or 'f1', got {metric!r}")
|
| 44 |
+
name = "checkpoints/model.ckpt" if fold is None else f"checkpoints/fold{fold}/{metric}.ckpt"
|
| 45 |
+
path = hf_hub_download(repo_id=repo_id, filename=name, revision=revision)
|
| 46 |
+
return InferencePipeline(path)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def score(pipeline, patient_graph, inc_graph, exc_graph,
|
| 50 |
+
patient_text: str, trial_text: str) -> dict[str, Any]:
|
| 51 |
+
"""Score one patient/trial pair.
|
| 52 |
+
|
| 53 |
+
Graphs are objects with ``.entities`` and ``.relations`` (crossgat RawGraph, or
|
| 54 |
+
anything with those attributes -- e.g. json.load of a file from the companion
|
| 55 |
+
graph dataset passed through ``crossgat.data.schemas``).
|
| 56 |
+
|
| 57 |
+
patient_text / trial_text feed the model's text branch and are REQUIRED: the
|
| 58 |
+
3-class checkpoints fuse whole-document embeddings with the graph features, and
|
| 59 |
+
omitting them raises rather than silently scoring against an all-zero branch.
|
| 60 |
+
Use the TREC topic description and the trial's brief_title + brief_summary.
|
| 61 |
+
"""
|
| 62 |
+
out = pipeline.predict_chia(
|
| 63 |
+
patient_entities=patient_graph.entities,
|
| 64 |
+
patient_relations=patient_graph.relations,
|
| 65 |
+
inc_trial_entities=inc_graph.entities,
|
| 66 |
+
inc_trial_relations=inc_graph.relations,
|
| 67 |
+
exc_trial_entities=exc_graph.entities,
|
| 68 |
+
exc_trial_relations=exc_graph.relations,
|
| 69 |
+
patient_text=patient_text,
|
| 70 |
+
trial_text=trial_text,
|
| 71 |
+
)
|
| 72 |
+
out["label"] = CLASSES[out["pred"]]
|
| 73 |
+
out["class_probs"] = dict(zip(CLASSES, out["probs"]))
|
| 74 |
+
return out
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def rerank_score(out: dict, score_fn: str = "p_eligible") -> float:
|
| 78 |
+
"""Reduce the posterior to the scalar a ranking sorts by.
|
| 79 |
+
|
| 80 |
+
``p_eligible`` is the recommended one: it wins on both first stages tested, at
|
| 81 |
+
every cutoff and in both metric families. Fuse it with the retrieval score as
|
| 82 |
+
``(1 - w) * retrieval_norm + w * model_norm`` at **w = 0.5**, min-max
|
| 83 |
+
normalising both within a topic. Do not replace the first stage outright
|
| 84 |
+
(w = 1.0): that loses significance on both retrievers.
|
| 85 |
+
"""
|
| 86 |
+
p_irr, p_exc, p_elig = out["probs"]
|
| 87 |
+
if score_fn == "p_eligible":
|
| 88 |
+
return p_elig
|
| 89 |
+
if score_fn == "one_minus_irrelevant":
|
| 90 |
+
return 1.0 - p_irr
|
| 91 |
+
if score_fn == "expected_gain":
|
| 92 |
+
return 1.0 * p_exc + 3.0 * p_elig
|
| 93 |
+
raise ValueError(f"unknown score_fn {score_fn!r}")
|