--- license: cc-by-nc-sa-4.0 language: - en base_model: - Qwen/Qwen3-Embedding-0.6B datasets: - chest2vec/chest2vec_labels pipeline_tag: text-classification library_name: transformers tags: - radiology - chest-ct - report-labeling - multi-label - ct-rate - chexbert-style-f1 --- # chest2vec CT Report Labeler (0.6B) A weakly-supervised **multi-label classifier** that reads a free-text **chest-CT report** and predicts a **137-leaf chest-imaging taxonomy**, with a **ternary** status per label (*negative / uncertain / positive*). It also provides a **CheXbert / SRR-BERT-style report-comparison F1**: label a list of ground-truth reports and a list of generated/predicted reports, then score them against each other (micro / macro / weighted F1) — useful for evaluating radiology report generation. - **Base architecture:** [`Qwen/Qwen3-Embedding-0.6B`](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B) (Apache-2.0) - **Adaptation:** LoRA (r=16, α=32) **merged into the weights** + last-token (EOS) pooling + L2-norm + a linear ternary head (`1024 → 137 × 3`) - **Self-contained:** the full model (encoder + head) ships in `model.safetensors`. Loading does **not** download Qwen3-Embedding weights — the architecture is rebuilt from the bundled config and our weights are loaded in. Tokenizer is bundled too. - **Params:** ~596M · weights in float32 - **Training labels:** [`chest2vec/chest2vec_labels`](https://huggingface.co/datasets/chest2vec/chest2vec_labels) (revised CT-RATE, 137-leaf taxonomy) ## Label space 137 leaf labels over 9 chest-CT sections (Lungs & Airways, Pleura, Mediastinum & Hila, Cardiovascular, Chest Wall, Bones/Spine, Upper Abdomen, Lower Neck, Others). The exact list is in `config.json` (`labels`); full definitions and per-split counts are in the dataset's [`LABEL_HIERARCHY.md`](https://huggingface.co/datasets/chest2vec/chest2vec_labels/blob/main/LABEL_HIERARCHY.md). **Ternary head** — `softmax(logits, dim=-1)` over class indices `[0, 1, 2]`: | class index | meaning | value | |---:|---|---:| | 0 | negative | 0 | | 1 | uncertain | -1 | | 2 | positive | 1 | A label is reported **positive** when `P(class=2) ≥ threshold` (default **0.5**). ## Usage ```python from transformers import AutoModel, AutoTokenizer model = AutoModel.from_pretrained("chest2vec/chest2vec_labeler", trust_remote_code=True).eval() tok = AutoTokenizer.from_pretrained("chest2vec/chest2vec_labeler", trust_remote_code=True) reports = ["Bibasilar atelectasis with small bilateral pleural effusions. Cardiomegaly. Coronary artery calcification."] # 1) human-readable positive labels per report print(model.label_reports(reports, tokenizer=tok)) # [{'Subsegmental / linear atelectasis': 'positive', 'Pleural effusion': 'positive', # 'Cardiomegaly': 'positive', 'Coronary artery calcification': 'positive'}] # 2) full prediction matrices out = model.predict(reports, tokenizer=tok, threshold=0.5, return_ternary=True) out["labels"] # list of 137 label names out["proba"] # [N, 137] P(positive) out["positive"] # [N, 137] in {0,1} out["ternary"] # [N, 137] in {-1,0,1} ``` ### CheXbert / SRR-BERT-style report comparison Label both ground-truth and predicted reports, then compute label-level F1 (GT-labels treated as truth): ```python res = model.score_reports(gt_reports, pred_reports, tokenizer=tok) # equal-length lists print(res["micro"]["f1"], res["macro"]["f1"], res["weighted"]["f1"]) print(res["per_label"]["Pleural effusion"]) # {'precision':..,'recall':..,'f1':..,'support_gt':..} # or one-liner that loads the model for you: from modeling_chest2vec_labeler import report_f1 report_f1(gt_reports, pred_reports, tokenizer=tok) ``` Returns `micro`, `macro`, `weighted` precision/recall/F1 over the 137 labels, plus `per_label`. ## Inputs & conventions - Input is the **findings** text (the model was trained on CT-RATE findings + their refined section-structured form). Reports are formatted internally as `Instruct: Given the following chest CT report, extract the presence/absence of entities\nQuery: `, truncated to **512** tokens, with an EOS token appended and left-padding. - For best fidelity, run in float32 (default). bf16 is fine for throughput with negligible drift. ## Evaluation Direct-paragraph evaluation (`softmax positive-class`, macro-F1 over labels with ≥30 positives — the stable headline; the all-labels macro is dragged down by sparse-tail labels): | Eval set | reports | macro-F1 (≥30) @0.33 | macro-F1 (≥30) @0.5 | macro-AUC (≥30) | |---|--:|--:|--:|--:| | CT-RATE revised test (public) | 1,464 | **0.875** | 0.866 | 0.989 | | sample1000 (private radiologist-reviewed gold) | 1,000 | **0.766** | 0.761 | 0.972 | On the public test set, a radiologist reviewed **966 reports**: **857 fully accepted, 60 imperfect-but-acceptable, 49 failed** (94.9% acceptable). AUC barely moves public→private (0.989 → 0.972), i.e. label ranking transfers to real clinical reports; the F1 gap is mostly threshold/labeling-convention, not domain failure. ## Caveats - **Weakly supervised** — trained on LLM-generated labels (not radiologist ground truth) derived from report **text**, not images. Not a medical device; not for clinical use. - `IVC filter` is in the taxonomy for completeness but had no training positives. - `score_reports` measures **label agreement** between two reports as judged by this labeler; like CheXbert-F1 it inherits the labeler's own error modes. ## License & attribution Released under **CC-BY-NC-SA-4.0**. Built on **`Qwen/Qwen3-Embedding-0.6B`** (Apache-2.0) and trained using labels derived from **[CT-RATE](https://huggingface.co/datasets/ibrahimhamamci/CT-RATE)** (CC-BY-NC-SA-4.0). **If you use this model, cite the CT-RATE paper** (arXiv:2403.17834) and acknowledge Qwen3-Embedding. See the [dataset card](https://huggingface.co/datasets/chest2vec/chest2vec_labels) for the full citation.