lukeingawesome commited on
Commit
364cd6b
·
verified ·
1 Parent(s): 78f6b3d

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,3 +1,4 @@
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
@@ -33,3 +34,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
1
+ <<<<<<< HEAD
2
  *.7z filter=lfs diff=lfs merge=lfs -text
3
  *.arrow filter=lfs diff=lfs merge=lfs -text
4
  *.bin filter=lfs diff=lfs merge=lfs -text
 
34
  *.zip filter=lfs diff=lfs merge=lfs -text
35
  *.zst filter=lfs diff=lfs merge=lfs -text
36
  *tfevents* filter=lfs diff=lfs merge=lfs -text
37
+ =======
38
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
39
+ *.pt filter=lfs diff=lfs merge=lfs -text
40
+ *.bin filter=lfs diff=lfs merge=lfs -text
41
+ >>>>>>> 2dc7409 (Release chest2vec_0.6b_cxr (Stage2 LoRA + Stage3 pooler + API))
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build artifacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+ .Python
10
+
11
+ # Testing
12
+ .pytest_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+
22
+ # Jupyter
23
+ .ipynb_checkpoints/
24
+
PUBLISH.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Publishing chest2vec_0.6b_chest
2
+
3
+ This guide covers publishing to both **Hugging Face Hub** (model repository) and **PyPI** (Python package).
4
+
5
+ ## Option 1: Publish to Hugging Face Hub
6
+
7
+ ### Prerequisites
8
+
9
+ 1. Install Hugging Face Hub CLI:
10
+ ```bash
11
+ pip install huggingface_hub
12
+ ```
13
+
14
+ 2. Login to Hugging Face:
15
+ ```bash
16
+ huggingface-cli login
17
+ # Enter your HF token when prompted
18
+ ```
19
+
20
+ ### Initialize Git Repository (if not already)
21
+
22
+ ```bash
23
+ cd /opt/project/chest2vec/export_chest2vec_0.6b_chest
24
+ git init
25
+ git add .
26
+ git commit -m "Initial commit: chest2vec_0.6b_chest"
27
+ ```
28
+
29
+ ### Create Repository on Hugging Face Hub
30
+
31
+ 1. Go to https://huggingface.co/new
32
+ 2. Create a new repository named `chest2vec_0.6b_chest` (or `chest2vec/chest2vec_0.6b_chest` if under an organization)
33
+ 3. Choose "Model" as the repository type
34
+
35
+ ### Push to Hugging Face Hub
36
+
37
+ ```bash
38
+ # Add HF remote (replace with your username/org)
39
+ git remote add origin https://huggingface.co/YOUR_USERNAME/chest2vec_0.6b_chest
40
+
41
+ # Push to HF Hub
42
+ git push origin main
43
+ ```
44
+
45
+ Or use the HF Hub CLI:
46
+
47
+ ```bash
48
+ huggingface-cli upload YOUR_USERNAME/chest2vec_0.6b_chest . --repo-type model
49
+ ```
50
+
51
+ ### Verify Upload
52
+
53
+ Visit `https://huggingface.co/YOUR_USERNAME/chest2vec_0.6b_chest` to verify all files are uploaded.
54
+
55
+ ---
56
+
57
+ ## Option 2: Publish to PyPI
58
+
59
+ ### Prerequisites
60
+
61
+ 1. Create a PyPI account at https://pypi.org/account/register/
62
+ 2. Create an API token at https://pypi.org/manage/account/token/
63
+ 3. Install build tools:
64
+ ```bash
65
+ pip install build twine
66
+ ```
67
+
68
+ ### Build the Package
69
+
70
+ ```bash
71
+ cd /opt/project/chest2vec/export_chest2vec_0.6b_chest
72
+ python3 -m build
73
+ ```
74
+
75
+ This creates:
76
+ - `dist/chest2vec-0.6.0-py3-none-any.whl`
77
+ - `dist/chest2vec-0.6.0.tar.gz`
78
+
79
+ ### Upload to PyPI
80
+
81
+ #### Test first on TestPyPI
82
+
83
+ ```bash
84
+ # Upload to TestPyPI first to test
85
+ twine upload --repository testpypi dist/*
86
+ # You'll be prompted for:
87
+ # username: __token__
88
+ # password: your API token
89
+ ```
90
+
91
+ Test installation:
92
+ ```bash
93
+ pip install --index-url https://test.pypi.org/simple/ chest2vec
94
+ ```
95
+
96
+ #### Then upload to PyPI
97
+
98
+ ```bash
99
+ twine upload dist/*
100
+ # You'll be prompted for:
101
+ # username: __token__
102
+ # password: your API token
103
+ ```
104
+
105
+ ### After Publishing
106
+
107
+ Once published, users can install with:
108
+
109
+ ```bash
110
+ pip install chest2vec
111
+ ```
112
+
113
+ **Note:** Users will still need to install PyTorch and flash-attention separately as documented in the README, since PyPI doesn't support custom index URLs in dependencies.
114
+
115
+ ---
116
+
117
+ ## Option 3: Publish to Both
118
+
119
+ You can publish to both platforms:
120
+
121
+ 1. **First publish to Hugging Face Hub** (for model weights and repository)
122
+ 2. **Then publish to PyPI** (for easy Python package installation)
123
+
124
+ Users can then either:
125
+ - Load from HF Hub: `Chest2Vec.from_pretrained("YOUR_USERNAME/chest2vec_0.6b_chest")`
126
+ - Install from PyPI: `pip install chest2vec` (then load from HF Hub or local path)
127
+
128
+ ---
129
+
130
+ ## Important Notes
131
+
132
+ - **Model weights** (section_pooler.pt, contrastive/) should be on Hugging Face Hub
133
+ - **Python package** can be on PyPI for easier installation
134
+ - The package name on PyPI is `chest2vec` (shared across all variants)
135
+ - Make sure to update version numbers in `setup.py` and `pyproject.toml` if publishing a new version
README.md CHANGED
@@ -1,3 +1,220 @@
1
- ---
2
- license: apache-2.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - text-embeddings
4
+ - retrieval
5
+ - radiology
6
+ - chest
7
+ - qwen
8
+ library_name: transformers
9
+ ---
10
+
11
+ # chest2vec_0.6b_chest
12
+
13
+ This repository contains the *delta weights and pooling head* for a section-aware embedding model on top of **Qwen/Qwen3-Embedding-0.6B**:
14
+
15
+ - **Stage-2**: Frozen LoRA adapter (contrastive) under `./contrastive/`
16
+ - **Stage-3**: Section pooler `section_pooler.pt` producing **9 section embeddings**
17
+ - **Inference helper**: `chest2vec.py`
18
+
19
+ Base model weights are **not** included; they are downloaded from Hugging Face at runtime.
20
+
21
+ ## Model Architecture
22
+
23
+ Chest2Vec is a three-stage model:
24
+ 1. **Base**: Qwen/Qwen3-Embedding-0.6B (downloaded at runtime)
25
+ 2. **Stage-2**: Contrastive LoRA adapter trained with multi-positive sigmoid loss
26
+ 3. **Stage-3**: Section-aware query-attention pooler producing embeddings for 9 radiology report sections
27
+
28
+ ## Sections
29
+
30
+ The model produces embeddings for 9 distinct sections:
31
+
32
+ 1. Lungs and Airways
33
+ 2. Pleura
34
+ 3. Cardiovascular
35
+ 4. Hila and Mediastinum
36
+ 5. Tubes & Devices
37
+ 6. Musculoskeletal and Chest Wall
38
+ 7. Abdominal
39
+ 8. impression
40
+ 9. Other
41
+
42
+ ## Installation
43
+
44
+ Install the package and all dependencies:
45
+
46
+ ```bash
47
+ # Install PyTorch with CUDA 12.6 support
48
+ pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/cu126
49
+
50
+ # Install transformers and trl
51
+ pip install transformers==4.57.3 trl==0.9.3
52
+
53
+ # Install deepspeed
54
+ pip install deepspeed==0.16.9
55
+
56
+ # Install flash-attention
57
+ pip install https://github.com/Dao-AILab/flash-attention/releases/download/v2.8.3/flash_attn-2.8.3+cu12torch2.6cxx11abiTRUE-cp310-cp310-linux_x86_64.whl
58
+
59
+ # Install chest2vec package
60
+ pip install chest2vec
61
+ ```
62
+
63
+ Or use the installation script:
64
+
65
+ ```bash
66
+ bash install_deps.sh
67
+ ```
68
+
69
+ ## Requirements
70
+
71
+ This model **requires FlashAttention-2** (CUDA) by default, which is automatically installed with the package.
72
+
73
+ ## Quickstart
74
+
75
+ ### Installation + Loading
76
+
77
+ ```python
78
+ from chest2vec import Chest2Vec
79
+
80
+ # Load model from Hugging Face Hub
81
+ m = Chest2Vec.from_pretrained("chest2vec/chest2vec_0.6b_chest", device="cuda:0")
82
+ ```
83
+
84
+ ### Instruction + Query Embeddings
85
+
86
+ ```python
87
+ instructions = ["Find findings about the lungs."]
88
+ queries = ["Consolidation in the right lower lobe."]
89
+
90
+ out = m.embed_instruction_query(instructions, queries, max_len=512, batch_size=8)
91
+
92
+ # Global embedding (derived): mean of 9 section vectors then L2-normalized
93
+ g = out.global_embedding # [N, H]
94
+
95
+ # Per-section embeddings (by full name)
96
+ lung = out.by_section_name["Lungs and Airways"] # [N, H]
97
+ imp = out.by_section_name["impression"] # [N, H]
98
+
99
+ # Or use aliases (case-insensitive)
100
+ lung = out.by_alias["lungs"] # [N, H]
101
+ cardio = out.by_alias["cardio"] # [N, H]
102
+ ```
103
+
104
+ ### Candidate Embeddings (Retrieval Bank)
105
+
106
+ ```python
107
+ candidates = [
108
+ "Lungs are clear. No focal consolidation.",
109
+ "Pleural effusion on the left.",
110
+ "Cardiomediastinal silhouette is normal."
111
+ ]
112
+
113
+ cand_out = m.embed_texts(candidates, max_len=512, batch_size=16)
114
+
115
+ cand_global = cand_out.global_embedding # [N, H]
116
+ cand_lung = cand_out.by_alias["lungs"] # [N, H]
117
+ ```
118
+
119
+ ### Retrieval Example (Cosine Top-K)
120
+
121
+ ```python
122
+ # Query embeddings for "Lungs and Airways" section
123
+ q = out.by_alias["lungs"] # [Nq, H]
124
+
125
+ # Document embeddings for "Lungs and Airways" section
126
+ d = cand_out.by_alias["lungs"] # [Nd, H]
127
+
128
+ # Compute top-k cosine similarities
129
+ scores, idx = Chest2Vec.cosine_topk(q, d, k=5, device="cuda")
130
+ # scores: [Nq, k] - similarity scores
131
+ # idx: [Nq, k] - indices of top-k candidates
132
+
133
+ print(f"Top-5 scores: {scores[0]}")
134
+ print(f"Top-5 indices: {idx[0]}")
135
+ ```
136
+
137
+ ## API Reference
138
+
139
+ ### `Chest2Vec.from_pretrained()`
140
+
141
+ Load the model from Hugging Face Hub or local path.
142
+
143
+ ```python
144
+ m = Chest2Vec.from_pretrained(
145
+ repo_id_or_path: str, # Hugging Face repo ID or local path
146
+ device: str = "cuda:0", # Device to load model on
147
+ use_4bit: bool = False, # Use 4-bit quantization
148
+ force_flash_attention_2: bool = True
149
+ )
150
+ ```
151
+
152
+ ### `embed_instruction_query()`
153
+
154
+ Embed instruction-query pairs. Returns `EmbedOutput` with:
155
+ - `section_matrix`: `[N, 9, H]` - embeddings for all 9 sections
156
+ - `global_embedding`: `[N, H]` - global embedding (mean of sections, L2-normalized)
157
+ - `by_section_name`: Dict mapping full section names to `[N, H]` tensors
158
+ - `by_alias`: Dict mapping aliases to `[N, H]` tensors
159
+
160
+ ```python
161
+ out = m.embed_instruction_query(
162
+ instructions: List[str],
163
+ queries: List[str],
164
+ max_len: int = 512,
165
+ batch_size: int = 16
166
+ )
167
+ ```
168
+
169
+ ### `embed_texts()`
170
+
171
+ Embed plain texts (for document/candidate encoding).
172
+
173
+ ```python
174
+ out = m.embed_texts(
175
+ texts: List[str],
176
+ max_len: int = 512,
177
+ batch_size: int = 16
178
+ )
179
+ ```
180
+
181
+ ### `cosine_topk()`
182
+
183
+ Static method for efficient top-k cosine similarity search.
184
+
185
+ ```python
186
+ scores, idx = Chest2Vec.cosine_topk(
187
+ query_emb: torch.Tensor, # [Nq, H]
188
+ cand_emb: torch.Tensor, # [Nd, H]
189
+ k: int = 10,
190
+ device: str = "cuda"
191
+ )
192
+ ```
193
+
194
+ ## Model Files
195
+
196
+ - `chest2vec.py` - Model class and inference utilities
197
+ - `chest2vec_config.json` - Model configuration
198
+ - `section_pooler.pt` - Stage-3 pooler weights
199
+ - `section_pooler_config.json` - Pooler configuration
200
+ - `contrastive/` - Stage-2 LoRA adapter directory
201
+ - `adapter_config.json` - LoRA adapter configuration
202
+ - `adapter_model.safetensors` - LoRA adapter weights
203
+
204
+ ## Citation
205
+
206
+ If you use this model, please cite:
207
+
208
+ ```bibtex
209
+ @misc{chest2vec_0.6b_chest,
210
+ title={Chest2Vec: Section-Aware Embeddings for Chest X-Ray Reports},
211
+ author={Your Name},
212
+ year={2024},
213
+ howpublished={\url{https://huggingface.co/chest2vec/chest2vec_0.6b_chest}}
214
+ }
215
+ ```
216
+
217
+ ## License
218
+
219
+ [Specify your license here]
220
+
__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from chest2vec import Chest2Vec, EmbedOutput
2
+
3
+ __all__ = ["Chest2Vec", "EmbedOutput"]
4
+ __version__ = "0.6.0"
5
+
chest2vec.py ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import math
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional, Union, Tuple
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+
11
+ from transformers import AutoTokenizer, AutoModel, BitsAndBytesConfig
12
+
13
+ try:
14
+ from peft import PeftModel
15
+ _HAS_PEFT = True
16
+ except Exception:
17
+ PeftModel = None
18
+ _HAS_PEFT = False
19
+
20
+ try:
21
+ from huggingface_hub import snapshot_download
22
+ _HAS_HUB = True
23
+ except Exception:
24
+ snapshot_download = None
25
+ _HAS_HUB = False
26
+
27
+
28
+ # -----------------------------
29
+ # Sections (must match training)
30
+ # -----------------------------
31
+ SECTION_NAMES = [
32
+ "Lungs and Airways",
33
+ "Pleura",
34
+ "Cardiovascular",
35
+ "Hila and Mediastinum",
36
+ "Tubes & Devices",
37
+ "Musculoskeletal and Chest Wall",
38
+ "Abdominal",
39
+ "impression",
40
+ "Other",
41
+ ]
42
+
43
+ SECTION_ALIASES = {
44
+ "global": "global",
45
+ "lungs": "Lungs and Airways",
46
+ "lung": "Lungs and Airways",
47
+ "pleura": "Pleura",
48
+ "cardio": "Cardiovascular",
49
+ "cardiovascular": "Cardiovascular",
50
+ "hila": "Hila and Mediastinum",
51
+ "mediastinum": "Hila and Mediastinum",
52
+ "tubes": "Tubes & Devices",
53
+ "devices": "Tubes & Devices",
54
+ "msk": "Musculoskeletal and Chest Wall",
55
+ "musculoskeletal": "Musculoskeletal and Chest Wall",
56
+ "abd": "Abdominal",
57
+ "abdominal": "Abdominal",
58
+ "impression": "impression",
59
+ "other": "Other",
60
+ }
61
+
62
+
63
+ def require_flash_attention_2() -> str:
64
+ if not torch.cuda.is_available():
65
+ raise RuntimeError("FlashAttention-2 requires CUDA, but torch.cuda.is_available() is False.")
66
+ try:
67
+ import flash_attn # noqa: F401
68
+ ver = getattr(flash_attn, "__version__", "0.0.0")
69
+ major = int(str(ver).split(".")[0])
70
+ if major < 2:
71
+ raise RuntimeError(f"flash-attn version {ver} < 2.0.0")
72
+ except Exception as e:
73
+ raise RuntimeError(
74
+ "FlashAttention-2 is REQUIRED but not available/importable.\n"
75
+ "Install flash-attn>=2 and ensure it matches your torch/CUDA.\n"
76
+ f"Import/Version error: {repr(e)}"
77
+ )
78
+ return "flash_attention_2"
79
+
80
+
81
+ def build_qwen_query(instruction: str, query: str) -> str:
82
+ instruction = str(instruction).strip()
83
+ query = str(query).strip()
84
+ return f"Instruct: {instruction}\nQuery: {query}"
85
+
86
+
87
+ def get_pool_token_id(tok) -> int:
88
+ eod_id = tok.convert_tokens_to_ids("<|endoftext|>")
89
+ if eod_id is None or eod_id < 0:
90
+ eod_id = tok.pad_token_id
91
+ return eod_id
92
+
93
+
94
+ def encode_with_eos_ids(tok, texts: List[str], max_len: int) -> Dict[str, torch.Tensor]:
95
+ """
96
+ Must match Stage-3 training:
97
+ - add_special_tokens=False
98
+ - truncation to max_len-1
99
+ - append <|endoftext|>
100
+ - left-pad
101
+ """
102
+ pad_id = tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id
103
+ eod_id = get_pool_token_id(tok)
104
+
105
+ enc = tok(
106
+ [str(t) for t in texts],
107
+ add_special_tokens=False,
108
+ truncation=True,
109
+ max_length=max_len - 1,
110
+ padding=False,
111
+ return_attention_mask=False,
112
+ )
113
+
114
+ input_ids = [ids + [eod_id] for ids in enc["input_ids"]]
115
+ attn_mask = [[1] * len(ids) for ids in input_ids]
116
+
117
+ T = max(len(ids) for ids in input_ids) if input_ids else 1
118
+ input_ids = [[pad_id] * (T - len(ids)) + ids for ids in input_ids]
119
+ attn_mask = [[0] * (T - len(m)) + m for m in attn_mask]
120
+
121
+ return {
122
+ "input_ids": torch.tensor(input_ids, dtype=torch.long),
123
+ "attention_mask": torch.tensor(attn_mask, dtype=torch.long),
124
+ }
125
+
126
+
127
+ def last_token_pool(last_hidden_states: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
128
+ """
129
+ Left-padding aware last-token pooling (extracts EOS token embedding).
130
+ """
131
+ left_padding = (attention_mask[:, -1].sum() == attention_mask.shape[0])
132
+ if left_padding:
133
+ return last_hidden_states[:, -1]
134
+ idx = attention_mask.sum(dim=1) - 1
135
+ return last_hidden_states[torch.arange(last_hidden_states.size(0), device=last_hidden_states.device), idx]
136
+
137
+
138
+ def get_last_hidden_state(model, input_ids, attention_mask):
139
+ """
140
+ Provide position_ids for left padding (FlashAttention-2).
141
+ """
142
+ m = model.module if hasattr(model, "module") else model
143
+
144
+ position_ids = attention_mask.long().cumsum(-1) - 1
145
+ position_ids.masked_fill_(attention_mask == 0, 0)
146
+
147
+ out = m(
148
+ input_ids=input_ids,
149
+ attention_mask=attention_mask,
150
+ position_ids=position_ids,
151
+ use_cache=False,
152
+ return_dict=True,
153
+ )
154
+ if hasattr(out, "last_hidden_state"):
155
+ return out.last_hidden_state
156
+
157
+ out = m(
158
+ input_ids=input_ids,
159
+ attention_mask=attention_mask,
160
+ position_ids=position_ids,
161
+ output_hidden_states=True,
162
+ use_cache=False,
163
+ return_dict=True,
164
+ )
165
+ return out.hidden_states[-1]
166
+
167
+
168
+ # -----------------------------
169
+ # Stage-3 pooler (query_attn)
170
+ # -----------------------------
171
+ class SectionQueryAttnPooler(nn.Module):
172
+ """
173
+ Match your Stage-3 training pooler.
174
+ """
175
+ def __init__(
176
+ self,
177
+ hidden_size: int,
178
+ num_sections: int,
179
+ mlp_hidden: int,
180
+ use_layernorm: bool = True,
181
+ pool_dropout: float = 0.1,
182
+ pool_scale: float = 0.0, # 0 => 1/sqrt(H)
183
+ ):
184
+ super().__init__()
185
+ self.hidden_size = int(hidden_size)
186
+ self.num_sections = int(num_sections)
187
+
188
+ self.ln = nn.LayerNorm(self.hidden_size) if use_layernorm else nn.Identity()
189
+
190
+ self.pool_queries = nn.Parameter(torch.empty(self.num_sections, self.hidden_size))
191
+ nn.init.normal_(self.pool_queries, mean=0.0, std=0.02)
192
+
193
+ self.pool_scale = float(pool_scale) if (pool_scale and pool_scale > 0) else (1.0 / math.sqrt(self.hidden_size))
194
+ self.pool_dropout = nn.Dropout(pool_dropout) if pool_dropout and pool_dropout > 0 else nn.Identity()
195
+
196
+ # Bias-free MLP
197
+ self.mlp = nn.Sequential(
198
+ nn.Linear(self.hidden_size, int(mlp_hidden), bias=False),
199
+ nn.GELU(),
200
+ nn.Linear(int(mlp_hidden), self.hidden_size, bias=False),
201
+ )
202
+
203
+ def forward_all(self, hidden_states: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
204
+ # hidden_states: [B,T,H] -> [B,S,H]
205
+ if isinstance(self.ln, nn.LayerNorm):
206
+ x = F.layer_norm(
207
+ hidden_states.float(),
208
+ self.ln.normalized_shape,
209
+ self.ln.weight.float() if self.ln.weight is not None else None,
210
+ self.ln.bias.float() if self.ln.bias is not None else None,
211
+ self.ln.eps,
212
+ ).to(dtype=hidden_states.dtype)
213
+ else:
214
+ x = hidden_states
215
+
216
+ scores = torch.einsum("bth,sh->bts", x.float(), self.pool_queries.float()) * self.pool_scale
217
+ scores = scores.masked_fill(attention_mask.unsqueeze(-1) == 0, -1e4)
218
+
219
+ attn = torch.softmax(scores, dim=1).to(dtype=x.dtype) # [B,T,S]
220
+ attn = self.pool_dropout(attn)
221
+
222
+ pooled = torch.einsum("bth,bts->bsh", x, attn) # [B,S,H]
223
+ pooled = pooled.to(dtype=next(self.mlp.parameters()).dtype)
224
+ pooled = self.mlp(pooled)
225
+
226
+ return F.normalize(pooled, p=2, dim=-1)
227
+
228
+
229
+ def _ensure_pooler_device_dtype(pooler: nn.Module, device: torch.device, dtype: torch.dtype) -> None:
230
+ p = next(pooler.parameters(), None)
231
+ if p is None:
232
+ return
233
+ if p.device != device or p.dtype != dtype:
234
+ pooler.to(device=device, dtype=dtype)
235
+
236
+
237
+ def _read_json(path: str) -> Dict[str, Any]:
238
+ with open(path, "r", encoding="utf-8") as f:
239
+ return json.load(f)
240
+
241
+
242
+ def _resolve_repo_path(repo_id_or_path: str) -> str:
243
+ # If it's a local directory, use it as-is.
244
+ if os.path.isdir(repo_id_or_path):
245
+ return repo_id_or_path
246
+ # Otherwise treat as HF repo_id and download snapshot.
247
+ if not _HAS_HUB:
248
+ raise RuntimeError(
249
+ "huggingface_hub is required to load by repo_id. "
250
+ "Install it: pip install huggingface_hub"
251
+ )
252
+ return snapshot_download(repo_id_or_path)
253
+
254
+
255
+ @dataclass
256
+ class EmbedOutput:
257
+ # Always available:
258
+ section_matrix: torch.Tensor # [N,S,H], float32 on CPU by default
259
+ global_embedding: torch.Tensor # [N,H], float32 on CPU by default
260
+ # Convenience dicts:
261
+ by_section_name: Dict[str, torch.Tensor] # each [N,H]
262
+ by_alias: Dict[str, torch.Tensor] # alias -> [N,H]
263
+
264
+
265
+ class Chest2Vec:
266
+ """
267
+ Lightweight wrapper:
268
+ - loads base Qwen3-Embedding
269
+ - applies LoRA adapter
270
+ - attaches Stage-3 section pooler
271
+ """
272
+ def __init__(self, tokenizer, model, pooler, sections: List[str], device: torch.device):
273
+ self.tokenizer = tokenizer
274
+ self.model = model
275
+ self.pooler = pooler
276
+ self.sections = list(sections)
277
+ self.device = device
278
+
279
+ self.model.eval()
280
+ self.pooler.eval()
281
+
282
+ @classmethod
283
+ def from_pretrained(
284
+ cls,
285
+ repo_id_or_path: str,
286
+ *,
287
+ device: str = "cuda:0",
288
+ use_4bit: bool = False,
289
+ force_flash_attention_2: bool = True,
290
+ ) -> "Chest2Vec":
291
+ repo_path = _resolve_repo_path(repo_id_or_path)
292
+
293
+ cfg_path = os.path.join(repo_path, "chest2vec_config.json")
294
+ if not os.path.isfile(cfg_path):
295
+ raise FileNotFoundError(f"Missing chest2vec_config.json in {repo_path}")
296
+ cfg = _read_json(cfg_path)
297
+
298
+ base_model = str(cfg["base_model"])
299
+ adapter_subdir = str(cfg.get("adapter_subdir", "contrastive"))
300
+ pooler_pt = str(cfg.get("pooler_pt", "section_pooler.pt"))
301
+ pooler_cfg = str(cfg.get("pooler_cfg", "section_pooler_config.json"))
302
+ sections = cfg.get("sections", SECTION_NAMES)
303
+
304
+ if force_flash_attention_2 or bool(cfg.get("require_flash_attention_2", False)):
305
+ attn_impl = require_flash_attention_2()
306
+ else:
307
+ attn_impl = "sdpa"
308
+
309
+ if not _HAS_PEFT:
310
+ raise RuntimeError("peft is required. Install: pip install peft")
311
+
312
+ device_t = torch.device(device)
313
+
314
+ tokenizer = AutoTokenizer.from_pretrained(base_model, padding_side="left", trust_remote_code=True)
315
+ if tokenizer.pad_token_id is None:
316
+ tokenizer.pad_token = tokenizer.eos_token
317
+
318
+ device_map = {"": str(device_t)}
319
+
320
+ # Load base model with FlashAttention-2
321
+ if use_4bit:
322
+ qconf = BitsAndBytesConfig(
323
+ load_in_4bit=True,
324
+ bnb_4bit_quant_type="nf4",
325
+ bnb_4bit_use_double_quant=True,
326
+ bnb_4bit_compute_dtype=torch.bfloat16,
327
+ )
328
+ try:
329
+ base = AutoModel.from_pretrained(
330
+ base_model,
331
+ trust_remote_code=True,
332
+ attn_implementation=attn_impl,
333
+ quantization_config=qconf,
334
+ device_map=device_map,
335
+ )
336
+ except TypeError as e:
337
+ raise RuntimeError(
338
+ "Your transformers version does not support attn_implementation=... "
339
+ "Upgrade transformers to use FlashAttention-2."
340
+ ) from e
341
+ else:
342
+ try:
343
+ base = AutoModel.from_pretrained(
344
+ base_model,
345
+ trust_remote_code=True,
346
+ attn_implementation=attn_impl,
347
+ torch_dtype=torch.bfloat16,
348
+ device_map=device_map,
349
+ )
350
+ except TypeError as e:
351
+ raise RuntimeError(
352
+ "Your transformers version does not support attn_implementation=... "
353
+ "Upgrade transformers to use FlashAttention-2."
354
+ ) from e
355
+
356
+ # Load adapter from this repo folder
357
+ adapter_dir = os.path.join(repo_path, adapter_subdir)
358
+ if not os.path.isfile(os.path.join(adapter_dir, "adapter_config.json")):
359
+ raise FileNotFoundError(f"adapter_config.json not found under: {adapter_dir}")
360
+
361
+ model = PeftModel.from_pretrained(base, adapter_dir)
362
+ model.eval()
363
+
364
+ # Attach section pooler
365
+ pooler_cfg_path = os.path.join(repo_path, pooler_cfg)
366
+ pooler_pt_path = os.path.join(repo_path, pooler_pt)
367
+ if not os.path.isfile(pooler_cfg_path):
368
+ raise FileNotFoundError(f"Missing pooler config: {pooler_cfg_path}")
369
+ if not os.path.isfile(pooler_pt_path):
370
+ raise FileNotFoundError(f"Missing pooler weights: {pooler_pt_path}")
371
+
372
+ pcfg = _read_json(pooler_cfg_path)
373
+
374
+ hidden_size = int(getattr(model.module if hasattr(model, "module") else model, "config").hidden_size)
375
+ mlp_hidden = int(pcfg.get("mlp_hidden", hidden_size))
376
+ use_layernorm = bool(pcfg.get("use_layernorm", True))
377
+ pool_dropout = float(pcfg.get("pool_dropout", 0.1))
378
+ pool_scale = float(pcfg.get("pool_scale", 0.0))
379
+
380
+ pooler = SectionQueryAttnPooler(
381
+ hidden_size=hidden_size,
382
+ num_sections=len(sections),
383
+ mlp_hidden=mlp_hidden,
384
+ use_layernorm=use_layernorm,
385
+ pool_dropout=pool_dropout,
386
+ pool_scale=pool_scale,
387
+ )
388
+ sd = torch.load(pooler_pt_path, map_location="cpu")
389
+ pooler.load_state_dict(sd, strict=True)
390
+ pooler.eval()
391
+
392
+ # Move pooler to same device/dtype as hidden states
393
+ # (we keep inference in autocast)
394
+ pooler.to(device=device_t, dtype=torch.bfloat16 if device_t.type == "cuda" else torch.float32)
395
+
396
+ return cls(tokenizer=tokenizer, model=model, pooler=pooler, sections=sections, device=device_t)
397
+
398
+ @torch.inference_mode()
399
+ def embed_texts(
400
+ self,
401
+ texts: List[str],
402
+ *,
403
+ max_len: int = 512,
404
+ batch_size: int = 16,
405
+ return_cpu_float32: bool = True,
406
+ ) -> EmbedOutput:
407
+ """
408
+ Encodes arbitrary texts (candidates, section strings, etc.)
409
+
410
+ NOTE: This uses Stage-3 section pooling:
411
+ - Section embeddings: section_pooler → [B,S,H] (9 section-specific embeddings)
412
+ - Global embedding: EOS token embedding extracted BEFORE pooler → [B,H] (matches Stage-3 training)
413
+
414
+ Returns:
415
+ - section_matrix: [N,9,H] - section-specific embeddings
416
+ - global_embedding: [N,H] - EOS token embedding (extracted before pooler)
417
+ - by_section_name: dict[name] -> [N,H]
418
+ - by_alias: dict['lungs'/'impression'/...] -> [N,H]
419
+ """
420
+ # Determine AMP
421
+ device = self.device
422
+ if device.type == "cuda":
423
+ amp_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
424
+ use_amp = True
425
+ else:
426
+ amp_dtype = torch.float32
427
+ use_amp = False
428
+
429
+ outs_sec = []
430
+ outs_global = []
431
+ for i in range(0, len(texts), batch_size):
432
+ chunk = [str(t) for t in texts[i:i + batch_size]]
433
+ enc = encode_with_eos_ids(self.tokenizer, chunk, max_len)
434
+ input_ids = enc["input_ids"].to(device, non_blocking=True)
435
+ attention_mask = enc["attention_mask"].to(device, non_blocking=True)
436
+
437
+ with torch.autocast(device_type=("cuda" if device.type == "cuda" else "cpu"),
438
+ dtype=amp_dtype, enabled=use_amp):
439
+ h = get_last_hidden_state(self.model, input_ids, attention_mask) # [B,T,H]
440
+
441
+ # Global embedding: extract EOS token embedding BEFORE pooler (matches Stage-3 training)
442
+ global_eos = last_token_pool(h, attention_mask) # [B,H]
443
+ global_eos = F.normalize(global_eos.float(), p=2, dim=-1)
444
+
445
+ # Section embeddings: pass through pooler
446
+ _ensure_pooler_device_dtype(self.pooler, device=h.device, dtype=h.dtype)
447
+ sec = self.pooler.forward_all(h, attention_mask) # [B,S,H] normalized
448
+
449
+ outs_sec.append(sec.detach())
450
+ outs_global.append(global_eos.detach())
451
+
452
+ section_matrix = torch.cat(outs_sec, dim=0) # on device, dtype ~ bf16
453
+ global_emb = torch.cat(outs_global, dim=0) # on device, dtype ~ bf16
454
+
455
+ # Move to CPU float32 if requested (recommended for retrieval stability)
456
+ if return_cpu_float32:
457
+ section_matrix_cpu = section_matrix.float().cpu()
458
+ # re-normalize to fix any numerical drift
459
+ section_matrix_cpu = F.normalize(section_matrix_cpu, p=2, dim=-1)
460
+ global_cpu = global_emb.float().cpu()
461
+ global_cpu = F.normalize(global_cpu, p=2, dim=-1)
462
+ else:
463
+ section_matrix_cpu = section_matrix
464
+ global_cpu = global_emb
465
+
466
+ by_section_name = {name: section_matrix_cpu[:, idx, :] for idx, name in enumerate(self.sections)}
467
+
468
+ # Helpful aliases for quick access
469
+ by_alias: Dict[str, torch.Tensor] = {}
470
+ by_alias["global"] = global_cpu
471
+ for alias, real in SECTION_ALIASES.items():
472
+ if real == "global":
473
+ continue
474
+ if real in by_section_name:
475
+ by_alias[alias] = by_section_name[real]
476
+
477
+ return EmbedOutput(
478
+ section_matrix=section_matrix_cpu,
479
+ global_embedding=global_cpu,
480
+ by_section_name=by_section_name,
481
+ by_alias=by_alias,
482
+ )
483
+
484
+ @torch.inference_mode()
485
+ def embed_instruction_query(
486
+ self,
487
+ instructions: List[str],
488
+ queries: List[str],
489
+ *,
490
+ max_len: int = 512,
491
+ batch_size: int = 16,
492
+ return_cpu_float32: bool = True,
493
+ ) -> EmbedOutput:
494
+ if len(instructions) != len(queries):
495
+ raise ValueError("instructions and queries must have the same length.")
496
+ q_texts = [build_qwen_query(i, q) for i, q in zip(instructions, queries)]
497
+ return self.embed_texts(
498
+ q_texts,
499
+ max_len=max_len,
500
+ batch_size=batch_size,
501
+ return_cpu_float32=return_cpu_float32,
502
+ )
503
+
504
+ @staticmethod
505
+ def cosine_topk(
506
+ query_emb: torch.Tensor, # [Nq,H] CPU float32 recommended
507
+ cand_emb: torch.Tensor, # [Nd,H] CPU float32 recommended
508
+ k: int = 10,
509
+ *,
510
+ device: str = "cuda",
511
+ query_batch_size: int = 256,
512
+ doc_chunk_size: int = 8192,
513
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
514
+ """
515
+ Chunked cosine top-k, stable in float32.
516
+ Returns (top_scores [Nq,k], top_indices [Nq,k]) on CPU.
517
+ """
518
+ device_t = torch.device(device)
519
+ q = F.normalize(query_emb.float(), p=2, dim=-1)
520
+ d = F.normalize(cand_emb.float(), p=2, dim=-1)
521
+ Nq, H = q.shape
522
+ Nd = d.shape[0]
523
+ k = min(int(k), Nd)
524
+
525
+ top_scores_all = torch.empty((Nq, k), dtype=torch.float32)
526
+ top_indices_all = torch.empty((Nq, k), dtype=torch.long)
527
+
528
+ for qs in range(0, Nq, query_batch_size):
529
+ qe = q[qs:qs + query_batch_size].to(device_t, non_blocking=True)
530
+ bq = qe.size(0)
531
+
532
+ top_scores = torch.full((bq, k), -1e9, device=device_t, dtype=torch.float32)
533
+ top_indices = torch.full((bq, k), -1, device=device_t, dtype=torch.long)
534
+
535
+ for ds in range(0, Nd, doc_chunk_size):
536
+ de = d[ds:ds + doc_chunk_size].to(device_t, non_blocking=True)
537
+ scores = (qe @ de.T).float()
538
+
539
+ chunk = scores.size(1)
540
+ idx_chunk = torch.arange(ds, ds + chunk, device=device_t, dtype=torch.long).unsqueeze(0).expand(bq, -1)
541
+
542
+ comb_scores = torch.cat([top_scores, scores], dim=1)
543
+ comb_idx = torch.cat([top_indices, idx_chunk], dim=1)
544
+
545
+ new_scores, new_pos = torch.topk(comb_scores, k, dim=1)
546
+ new_idx = comb_idx.gather(1, new_pos)
547
+
548
+ top_scores, top_indices = new_scores, new_idx
549
+
550
+ top_scores_all[qs:qs + bq] = top_scores.cpu()
551
+ top_indices_all[qs:qs + bq] = top_indices.cpu()
552
+
553
+ return top_scores_all, top_indices_all
chest2vec_config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chest2vec_0.6b_chest",
3
+ "base_model": "Qwen/Qwen3-Embedding-0.6B",
4
+ "adapter_subdir": "contrastive",
5
+ "pooler_pt": "section_pooler.pt",
6
+ "pooler_cfg": "section_pooler_config.json",
7
+ "require_flash_attention_2": true,
8
+ "default_max_len": 512,
9
+ "sections": [
10
+ "Lungs and Airways",
11
+ "Pleura",
12
+ "Cardiovascular",
13
+ "Hila and Mediastinum",
14
+ "Tubes & Devices",
15
+ "Musculoskeletal and Chest Wall",
16
+ "Abdominal",
17
+ "impression",
18
+ "Other"
19
+ ],
20
+ "global_pool": "mean_of_sections_then_l2"
21
+ }
22
+
contrastive/adapter_config.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alpha_pattern": {},
3
+ "auto_mapping": {
4
+ "base_model_class": "Qwen3Model",
5
+ "parent_library": "transformers.models.qwen3.modeling_qwen3"
6
+ },
7
+ "base_model_name_or_path": "Qwen/Qwen3-Embedding-0.6B",
8
+ "bias": "none",
9
+ "corda_config": null,
10
+ "eva_config": null,
11
+ "exclude_modules": null,
12
+ "fan_in_fan_out": false,
13
+ "inference_mode": true,
14
+ "init_lora_weights": true,
15
+ "layer_replication": null,
16
+ "layers_pattern": null,
17
+ "layers_to_transform": null,
18
+ "loftq_config": {},
19
+ "lora_alpha": 32,
20
+ "lora_bias": false,
21
+ "lora_dropout": 0.1,
22
+ "megatron_config": null,
23
+ "megatron_core": "megatron.core",
24
+ "modules_to_save": null,
25
+ "peft_type": "LORA",
26
+ "r": 16,
27
+ "rank_pattern": {},
28
+ "revision": null,
29
+ "target_modules": [
30
+ "o_proj",
31
+ "k_proj",
32
+ "v_proj",
33
+ "down_proj",
34
+ "gate_proj",
35
+ "up_proj",
36
+ "q_proj"
37
+ ],
38
+ "task_type": null,
39
+ "trainable_token_indices": null,
40
+ "use_dora": false,
41
+ "use_rslora": false
42
+ }
contrastive/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00c52073fa4941ac01672b5961fdeccb24f2e46240f8066415dc14910f8600ed
3
+ size 40419816
install_deps.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Installation script for chest2vec dependencies
3
+ # This script installs PyTorch and flash-attention with the correct versions
4
+
5
+ set -e
6
+
7
+ echo "Installing PyTorch packages from custom index..."
8
+ pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 --index-url https://download.pytorch.org/whl/cu126
9
+
10
+ echo "Installing flash-attention from GitHub release..."
11
+ pip install https://github.com/Dao-AILab/flash-attention/releases/download/v2.8.3/flash_attn-2.8.3+cu12torch2.6cxx11abiTRUE-cp310-cp310-linux_x86_64.whl
12
+
13
+ echo "Installing chest2vec package..."
14
+ pip install chest2vec
15
+
16
+ echo "Installation complete!"
17
+
pyproject.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chest2vec"
7
+ version = "0.6.0"
8
+ description = "Section-aware embeddings for chest X-ray reports"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ dependencies = [
12
+ "transformers==4.57.3",
13
+ "trl==0.9.3",
14
+ "deepspeed==0.16.9",
15
+ "peft",
16
+ "huggingface_hub",
17
+ "bitsandbytes",
18
+ "accelerate",
19
+ "numpy",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/chest2vec/chest2vec"
24
+
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.6.0
2
+ torchvision==0.21.0
3
+ torchaudio==2.6.0
4
+ transformers==4.57.3
5
+ trl==0.9.3
6
+ deepspeed==0.16.9
7
+ https://github.com/Dao-AILab/flash-attention/releases/download/v2.8.3/flash_attn-2.8.3+cu12torch2.6cxx11abiTRUE-cp310-cp310-linux_x86_64.whl
8
+ peft
9
+ huggingface_hub
10
+ bitsandbytes
11
+ accelerate
12
+ numpy
section_pooler.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79b316d23166dc0b4dfef9cd9b7e25c9ecf5aa9ed7e800b8e1080be10a85db7c
3
+ size 4219403
section_pooler_config.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "pooler_type": "query_attn",
3
+ "sections": [
4
+ "Lungs and Airways",
5
+ "Pleura",
6
+ "Cardiovascular",
7
+ "Hila and Mediastinum",
8
+ "Tubes & Devices",
9
+ "Musculoskeletal and Chest Wall",
10
+ "Abdominal",
11
+ "impression",
12
+ "Other"
13
+ ],
14
+ "hidden_size": 1024,
15
+ "mlp_hidden": 1024,
16
+ "use_layernorm": true,
17
+ "pool_dropout": 0.1,
18
+ "pool_scale": 0.0,
19
+ "loss": "multipos_sigmoid"
20
+ }
setup.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+ from pathlib import Path
3
+
4
+ # Read README for long description
5
+ readme_file = Path(__file__).parent / "README.md"
6
+ long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists() else ""
7
+
8
+ setup(
9
+ name="chest2vec",
10
+ version="0.6.0",
11
+ description="Section-aware embeddings for chest X-ray reports",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ author="Chest2Vec Team",
15
+ url="https://github.com/chest2vec/chest2vec",
16
+ packages=find_packages(),
17
+ py_modules=["chest2vec"],
18
+ include_package_data=True,
19
+ package_data={"": ["__init__.py"]},
20
+ install_requires=[
21
+ "transformers==4.57.3",
22
+ "trl==0.9.3",
23
+ "deepspeed==0.16.9",
24
+ "peft",
25
+ "huggingface_hub",
26
+ "bitsandbytes",
27
+ "accelerate",
28
+ "numpy",
29
+ ],
30
+ python_requires=">=3.8",
31
+ classifiers=[
32
+ "Development Status :: 4 - Beta",
33
+ "Intended Audience :: Developers",
34
+ "Intended Audience :: Science/Research",
35
+ "License :: OSI Approved :: Apache Software License",
36
+ "Programming Language :: Python :: 3",
37
+ "Programming Language :: Python :: 3.8",
38
+ "Programming Language :: Python :: 3.9",
39
+ "Programming Language :: Python :: 3.10",
40
+ "Programming Language :: Python :: 3.11",
41
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
42
+ ],
43
+ )
44
+