anzhi2710gmailcom commited on
Commit
fedd8d3
·
verified ·
1 Parent(s): 6da3054

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,47 @@
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
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
  *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
  *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
  *.xz 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
  *.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
4
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
11
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
 
 
17
  *.pt filter=lfs diff=lfs merge=lfs -text
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.tar filter=lfs diff=lfs merge=lfs -text
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - ""
4
+ language:
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - medical multimodal large language model
10
+ - interactive inference
11
+ ---
12
+ <p align="center">
13
+ <strong>
14
+ <span style="font-size: 30px;">MedGemma</span>
15
+ </strong>
16
+ </p>
17
+
18
+ # Model Overview
19
+
20
+ MedGemma is an open-source medical multimodal large language model from Google. It is based on the [Gemma 3](https://ai.google.dev/gemma/docs/core) architecture and is trained for medical text and medical image understanding. MedGemma provides two variants:
21
+
22
+ - **MedGemma 4B**: a multimodal model that supports joint input of medical text and medical images.
23
+ - **MedGemma 27B**: a text-only model focused on medical text understanding and question answering.
24
+
25
+ # Model Description
26
+
27
+ MedGemma 4B uses an image encoder and has been pretrained on multiple types of de-identified medical data, including chest X-rays (CXR), dermatology images, ophthalmology images, and histopathology slides. Its language model component has been trained on radiology images, pathology images, ophthalmology images, dermatology images, and medical text.
28
+
29
+ Weights and datasets are not available at the moment. They will be uploaded to Hugging Face soon, and command-line downloads will be supported later.
30
+
31
+ # Use Cases
32
+
33
+ | Use case | Description |
34
+ | :---: | :---: |
35
+ | Medical question answering | Evaluates the model's question-answering capability on medical knowledge benchmarks such as MedQA. |
36
+ | Medical image analysis | Supports tasks such as anatomical localization on chest X-rays (CXR) and longitudinal comparison of multi-timepoint images. |
37
+ | Domain fine-tuning | Performs parameter-efficient fine-tuning with LoRA on datasets such as NCT colon histopathology images. |
38
+ | Unified inference | Provides interactive and batch-file inference through `MedicalInferenceRunner`. |
39
+
40
+ # Use and Limitations
41
+
42
+ MedGemma is a foundation model for developing healthcare AI applications and is intended to serve as a starting point for downstream development, adaptation, and validation. Developers should fully validate, adapt, and meaningfully modify the model for their specific use case.
43
+
44
+ Outputs generated by MedGemma are not intended to directly inform clinical diagnosis, patient management decisions, treatment recommendations, or any other direct clinical practice. All model outputs should be considered preliminary and require independent verification, clinical correlation, and further investigation.
45
+
46
+ # Usage
47
+
48
+ ## 1. Using OneCode
49
+
50
+ You can try intelligent one-click AI4S programming through the OneCode online environment:
51
+
52
+ [Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
53
+
54
+ ## 2. Manual Installation and Usage
55
+
56
+ **Hardware Requirements**
57
+
58
+ - Running on a GPU or DCU is recommended.
59
+ - CPU can be used for connectivity checks, but it is relatively slow.
60
+ - DCU users need to install DTK in advance. DTK 25.04.2 or later is recommended, or the OneScience-recommended version that matches the current cluster.
61
+
62
+ **Software Requirements**
63
+
64
+ DCU users who want to learn more about adaptation details can contact liubiao@sugon.com.
65
+
66
+ **Environment Checks**
67
+
68
+ - NVIDIA GPU:
69
+
70
+ ```bash
71
+ nvidia-smi
72
+ ```
73
+
74
+ - Hygon DCU:
75
+
76
+ ```bash
77
+ hy-smi
78
+ ```
79
+
80
+ ### Environment Preparation
81
+
82
+ 1. Check the `botocore` version. If the version is too old, upgrade it:
83
+
84
+ ```bash
85
+ pip install --upgrade boto3==1.43.36 botocore==1.43.36
86
+ ```
87
+
88
+ 2. Check the `transformers` version. If the version is too old, upgrade it:
89
+
90
+ ```bash
91
+ pip install --upgrade transformers==5.12.1
92
+ ```
93
+
94
+ ## Quick Start
95
+
96
+ ### 1. Install the Runtime Environment
97
+
98
+ ```bash
99
+ conda create -n onescience311 python=3.11 -y
100
+ conda activate onescience311
101
+ pip install onescience[bio] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
102
+ ```
103
+
104
+ If the following code cannot find required libraries at runtime, activate CUDA as shown below.
105
+
106
+ ```bash
107
+ source ${ROCM_PATH}/cuda/env.sh
108
+ export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH"
109
+ export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/python3.11/site-packages/fastpt/torch/lib:$LD_LIBRARY_PATH"
110
+ ```
111
+
112
+ ### 2. Download the Model Package
113
+
114
+ ```bash
115
+ # By default, the package is downloaded to the model folder under the current path. To change this, adjust the path after local_dir.
116
+ hf download --model OneScience-Sugon/Medgemma --local-dir ./model
117
+ cd model
118
+ ```
119
+
120
+ ### Training Weights and Datasets
121
+
122
+ Weights and datasets are not provided at the moment. They will be uploaded to Hugging Face soon, and command-line downloads will be supported later.
123
+
124
+ ### 3. Usage Modes
125
+
126
+ **Data and Model Weights**
127
+
128
+ ```bash
129
+ hf download --dataset OneScience-Sugon/medgemma --local-dir ./dataset # Weights and datasets are not available at the moment. They will be uploaded to Hugging Face soon, and command-line downloads will be supported later.
130
+ ```
131
+
132
+ By default, the scripts load the model from:
133
+
134
+ ```text
135
+ ${ONESCIENCE_DATASETS_DIR}/medgemma/Hugging Face/google/medgemma-1.5-4b-it
136
+ ```
137
+
138
+ By default, the scripts load datasets from the following paths:
139
+
140
+ | Task | Data | Default path |
141
+ |------|------|----------|
142
+ | MedQA evaluation | MedQA parquet data | `${ONESCIENCE_DATASETS_DIR}/medgemma/medqa` |
143
+ | Chest X-ray anatomical localization | Chest X-ray images | `${ONESCIENCE_DATASETS_DIR}/medgemma/Chest_Xray/...` |
144
+ | Chest X-ray longitudinal comparison | Two chest X-rays from before and after | `${ONESCIENCE_DATASETS_DIR}/medgemma/test_compare/...` |
145
+ | Pathology image fine-tuning | NCT-CRC-HE-100K / CRC-VAL-HE-7K | `${ONESCIENCE_DATASETS_DIR}/medgemma/nct/...` |
146
+
147
+ Download the model in advance and place it in this directory, or override it through the `model_path` environment variable.
148
+
149
+ ### Detailed Usage
150
+
151
+ #### 1. Integration Test
152
+
153
+ Verify whether MedGemma modules, configurations, data adapters, and image processing components can be imported correctly in OneScience:
154
+
155
+ ```bash
156
+ python tests/test_integration.py
157
+ ```
158
+
159
+ ---
160
+
161
+ #### 2. Medical Question Answering Evaluation (`run_evaluate_on_medqa.sh`)
162
+
163
+ Evaluate the model's medical question-answering capability on the MedQA dataset. By default, 10 samples are processed for quick validation.
164
+
165
+ ```bash
166
+ bash scripts/run_evaluate_on_medqa.sh
167
+ ```
168
+
169
+ Output:
170
+
171
+ - `scripts/medqa_results/medqa_results.json`: detailed results for each sample
172
+ - `scripts/medqa_results/summary.txt`: summary metrics such as accuracy
173
+
174
+ ---
175
+
176
+ #### 3. Chest X-Ray Anatomical Localization (`run_cxr_anatomy.sh`)
177
+
178
+ Perform anatomical localization on one or more chest X-rays. The script runs both single-image mode and batch mode internally:
179
+
180
+ ```bash
181
+ bash scripts/run_cxr_anatomy.sh
182
+ ```
183
+
184
+ Output:
185
+
186
+ - `scripts/outputs/result_*.json`: localization coordinates and labels
187
+ - `scripts/outputs/result_*.png`: visualization images with bounding-box annotations
188
+ - `scripts/outputs/batch_summary.json`: summary results for batch mode
189
+
190
+ ---
191
+
192
+ #### 4. Chest X-Ray Longitudinal Comparison (`run_cxr_longitudinal_comparison.sh`)
193
+
194
+ Compare two chest X-rays from the same patient taken at different timepoints:
195
+
196
+ ```bash
197
+ bash scripts/run_cxr_longitudinal_comparison.sh
198
+ ```
199
+
200
+ Output:
201
+
202
+ - `scripts/compare_outputs/compare_<image1>_vs_<image2>.txt`: text comparison report
203
+ - `scripts/compare_outputs/compare_<image1>_vs_<image2>.json`: structured JSON results
204
+
205
+ ---
206
+
207
+ #### 5. Pathology Image LoRA Fine-Tuning (`run_fine_tune.sh`)
208
+
209
+ Perform LoRA fine-tuning on the NCT colon histopathology image dataset:
210
+
211
+ ```bash
212
+ bash scripts/run_fine_tune.sh
213
+ ```
214
+
215
+ Output:
216
+
217
+ - `scripts/medgemma-nct-lora/`: LoRA weights, training logs, and evaluation results
218
+
219
+ > Note: The script automatically checks and fixes the `boto3==1.43.36` and `botocore==1.43.36` versions to avoid dependency conflicts.
220
+
221
+ ---
222
+
223
+ #### 6. Using the Inference Runner
224
+
225
+ `runner/medical_inference_runner.py` provides a unified inference entry point and supports both interactive and batch-file inference.
226
+
227
+ ##### Interactive Inference
228
+
229
+ ```bash
230
+ export PYTHONPATH=../../../src:$PYTHONPATH
231
+ python runner/medical_inference_runner.py \
232
+ --config configs/inference_config.yaml \
233
+ --interactive
234
+ ```
235
+
236
+ ##### Batch-File Inference
237
+
238
+ ```bash
239
+ export PYTHONPATH=../../../src:$PYTHONPATH
240
+ python runner/medical_inference_runner.py \
241
+ --config configs/inference_config.yaml \
242
+ --input data/example_input.json
243
+ ```
244
+
245
+ ### Notes
246
+
247
+ - Make sure the `ONESCIENCE_DATASETS_DIR` environment variable is correctly set before running the scripts.
248
+ - The scripts use `HIP_VISIBLE_DEVICES=0` by default and can run directly on Hygon DCU platforms. On CUDA platforms, replace it with `CUDA_VISIBLE_DEVICES=0` or adjust it according to the available devices.
249
+ - To use vLLM for accelerated inference, make sure the corresponding version of vLLM is installed and configure `use_vllm: true`.
250
+ - The chest X-ray anatomical localization and pathology fine-tuning scripts automatically fix `boto3` / `botocore` versions to avoid dependency conflicts.
251
+ - The 4B multimodal model requires substantial GPU memory for inference. At least 24 GB of memory on a single device is recommended. Multi-device execution can be controlled through `num_gpus` or an external `CUDA_VISIBLE_DEVICES` setting.
252
+
253
+ # Official OneScience Information
254
+
255
+ | Platform | OneScience main repository | Skills repository |
256
+ | --- | --- | --- |
257
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
258
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
259
+
260
+ # Citation and License
261
+
262
+ The MedGemma model is licensed under the [Health AI Developer Foundations License](https://developers.google.com/health-ai-developer-foundations/terms), and the example code in this repository is licensed under Apache 2.0.
263
+
264
+ For more information, see:
265
+
266
+ - [Developer documentation](https://developers.google.com/health-ai-developer-foundations/medgemma/get-started)
267
+ - [Model card](https://developers.google.com/health-ai-developer-foundations/medgemma/model-card)
268
+ - [Community guidelines](https://developers.google.com/health-ai-developer-foundations/community-guidelines)
269
+ - [Hugging Face](https://huggingface.co/models?other=medgemma)
270
+ - [Google Model Garden](https://console.cloud.google.com/vertex-ai/publishers/google/model-garden/medgemma)
configs/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .configs_base import medgemma_base_configs
2
+
3
+ __all__ = ["medgemma_base_configs"]
configs/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (267 Bytes). View file
 
configs/__pycache__/configs_base.cpython-311.pyc ADDED
Binary file (1.83 kB). View file
 
configs/configs_base.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 基础配置
2
+ # 遵循 Protenix 的配置模式
3
+
4
+ from onescience.models.protenix.config.extend_types import (
5
+ DefaultNoneWithType,
6
+ GlobalConfigValue,
7
+ ListValue,
8
+ RequiredValue,
9
+ ValueMaybeNone,
10
+ )
11
+
12
+ basic_configs = {
13
+ "project": "MedGemma",
14
+ "run_name": RequiredValue(str),
15
+ "base_dir": RequiredValue(str),
16
+ "seed": 42,
17
+ "deterministic": False,
18
+ "use_wandb": False,
19
+ "load_checkpoint_path": "",
20
+ "eval_only": True, # MedGemma 主要用于推理
21
+ }
22
+
23
+ model_configs = {
24
+ # Model settings
25
+ "variant": RequiredValue(str), # "4b" or "27b"
26
+ "model_path": RequiredValue(str), # 模型权重路径
27
+ "tokenizer_path": DefaultNoneWithType(str), # Tokenizer 路径,默认与 model_path 相同
28
+ "prompt_format": "chat", # "chat" or "instruct"
29
+ "is_multimodal": True, # 4B 支持多模态,27B 仅文本
30
+ }
31
+
32
+ inference_configs = {
33
+ # Inference settings
34
+ "gpu_memory_utilization": 0.9,
35
+ "max_model_len": DefaultNoneWithType(int), # 最大序列长度,None 表示使用模型默认值
36
+ "tensor_parallel_size": 1, # Tensor 并行大小
37
+ "default_max_tokens": 500, # 默认生成最大 token 数
38
+ "temperature": 0.7,
39
+ "top_p": 0.9,
40
+ "top_k": DefaultNoneWithType(int),
41
+ "min_p": DefaultNoneWithType(float),
42
+ "batch_size": 1,
43
+ "num_workers": 0,
44
+ "use_vllm": True, # 是否使用 vLLM(如果为 False,则使用 transformers)
45
+ }
46
+
47
+ data_configs = {
48
+ # Data settings
49
+ "input_json_path": DefaultNoneWithType(str), # 输入 JSON 文件路径
50
+ "input_dir": DefaultNoneWithType(str), # 输入目录
51
+ "image_input_width": 224,
52
+ "image_input_height": 224,
53
+ "max_parallel_download_workers": 4,
54
+ "worker_download_parallelism": "THREAD", # "THREAD" or "PROCESS"
55
+ "use_msa": False, # MedGemma 不使用 MSA(这是 Protenix 特有的)
56
+ }
57
+
58
+ output_configs = {
59
+ # Output settings
60
+ "dump_dir": RequiredValue(str),
61
+ "save_predictions": True,
62
+ "output_format": "json", # "json" or "jsonl"
63
+ "save_intermediate": False,
64
+ }
65
+
66
+ # 合并所有配置
67
+ medgemma_base_configs = {
68
+ **basic_configs,
69
+ "model": model_configs,
70
+ "inference": inference_configs,
71
+ "data": data_configs,
72
+ "output": output_configs,
73
+ }
configs/inference_config.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 推理配置示例
2
+ # 此文件展示如何配置 MedGemma 进行推理
3
+
4
+ # 项目设置
5
+ project: "MedGemma"
6
+ run_name: "medgemma_inference_demo"
7
+ base_dir: "./medgemma_output"
8
+
9
+ # 模型设置
10
+ model:
11
+ variant: "4b" # "4b" (多模态) 或 "27b" (仅文本)
12
+ model_path: "/public/share/sugonhpcapp01/onestore/onedatasets/medgemma/modelscope/google/medgemma-1.5-4b-it" # 替换为实际模型路径
13
+ tokenizer_path: null # null 表示使用 model_path
14
+ prompt_format: "chat" # "chat" 或 "instruct"
15
+ is_multimodal: true # 4B 支持多模态
16
+
17
+ # 推理设置
18
+ inference:
19
+ gpu_memory_utilization: 0.9
20
+ max_model_len: null # null 表示使用模型默认值
21
+ tensor_parallel_size: 1 # GPU 并行数
22
+ default_max_tokens: 500
23
+ temperature: 0.7
24
+ top_p: 0.9
25
+ top_k: null
26
+ min_p: null
27
+ batch_size: 1
28
+ num_workers: 0
29
+ use_vllm: false # 使用 vLLM 加速
30
+
31
+ # 数据设置
32
+ data:
33
+ input_json_path: "./input_data.json" # 输入数据路径
34
+ input_dir: null
35
+ image_input_width: 224
36
+ image_input_height: 224
37
+ max_parallel_download_workers: 4
38
+ worker_download_parallelism: "THREAD"
39
+ use_msa: false
40
+
41
+ # 输出设置
42
+ output:
43
+ dump_dir: "./medgemma_predictions"
44
+ save_predictions: true
45
+ output_format: "json" # "json" 或 "jsonl"
46
+ save_intermediate: false
47
+
48
+ # 基础设置
49
+ seed: 42
50
+ deterministic: false
51
+ use_wandb: false
52
+ eval_only: true
models/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .medgemma import MedGemma
2
+ from .model_runner import VLLMModelRunner, TransformersModelRunner
3
+ from .predictor_wrapper import MedGemmaPredictor
4
+ from .config import (
5
+ ConfigManager,
6
+ parse_configs,
7
+ load_config,
8
+ save_config,
9
+ )
10
+
11
+ __all__ = [
12
+ "MedGemma",
13
+ "VLLMModelRunner",
14
+ "TransformersModelRunner",
15
+ "MedGemmaPredictor",
16
+ "ConfigManager",
17
+ "parse_configs",
18
+ "load_config",
19
+ "save_config",
20
+ ]
21
+
22
+ __version__ = "0.1.0"
models/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (681 Bytes). View file
 
models/__pycache__/medgemma.cpython-311.pyc ADDED
Binary file (10.3 kB). View file
 
models/__pycache__/model_runner.cpython-311.pyc ADDED
Binary file (11.8 kB). View file
 
models/__pycache__/predictor_wrapper.cpython-311.pyc ADDED
Binary file (9.22 kB). View file
 
models/config/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .config import (
2
+ ConfigManager,
3
+ parse_configs,
4
+ parse_sys_args,
5
+ load_config,
6
+ save_config,
7
+ )
8
+ from .config_base import (
9
+ MEDGEMMA_BASE_CONFIG,
10
+ MEDGEMMA_INFERENCE_CONFIG,
11
+ )
12
+
13
+ __all__ = [
14
+ "ConfigManager",
15
+ "parse_configs",
16
+ "parse_sys_args",
17
+ "load_config",
18
+ "save_config",
19
+ "MEDGEMMA_BASE_CONFIG",
20
+ "MEDGEMMA_INFERENCE_CONFIG",
21
+ ]
models/config/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (577 Bytes). View file
 
models/config/__pycache__/config.cpython-311.pyc ADDED
Binary file (999 Bytes). View file
 
models/config/__pycache__/config_base.cpython-311.pyc ADDED
Binary file (815 Bytes). View file
 
models/config/config.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 配置管理器
2
+ # 重用 Protenix 的 ConfigManager
3
+
4
+ from onescience.models.protenix.config.config import (
5
+ ConfigManager,
6
+ parse_configs as protenix_parse_configs,
7
+ parse_sys_args,
8
+ load_config,
9
+ save_config,
10
+ )
11
+
12
+ __all__ = [
13
+ "ConfigManager",
14
+ "parse_configs",
15
+ "parse_sys_args",
16
+ "load_config",
17
+ "save_config",
18
+ ]
19
+
20
+
21
+ def parse_configs(base_configs: dict, sys_args=None, fill_required_with_null: bool = False):
22
+ """
23
+ 解析 MedGemma 配置
24
+
25
+ Args:
26
+ base_configs: 基础配置字典
27
+ sys_args: 命令行参数(可选)
28
+ fill_required_with_null: 是否用 None 填充必需值
29
+
30
+ Returns:
31
+ ConfigDict: 解析后的配置
32
+ """
33
+ return protenix_parse_configs(base_configs, sys_args, fill_required_with_null)
models/config/config_base.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 模型配置
2
+ # 此文件定义了 MedGemma 模型的基础配置结构
3
+
4
+ from onescience.models.protenix.config.extend_types import (
5
+ RequiredValue,
6
+ ValueMaybeNone,
7
+ )
8
+
9
+ # MedGemma 基础配置
10
+ MEDGEMMA_BASE_CONFIG = {
11
+ "variant": RequiredValue(str), # "4b" or "27b"
12
+ "model_path": RequiredValue(str),
13
+ "tokenizer_path": ValueMaybeNone(str),
14
+ "prompt_format": "chat",
15
+ "is_multimodal": True,
16
+ }
17
+
18
+ # 推理配置
19
+ MEDGEMMA_INFERENCE_CONFIG = {
20
+ "gpu_memory_utilization": 0.9,
21
+ "max_model_len": ValueMaybeNone(int),
22
+ "tensor_parallel_size": 1,
23
+ "default_max_tokens": 500,
24
+ "temperature": 0.7,
25
+ "top_p": 0.9,
26
+ "batch_size": 1,
27
+ }
models/medgemma.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 主模型类
2
+ # 继承 OneScience Module 基类,集成到 OneScience 框架
3
+
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+ from onescience.modules.module import Module
11
+ from models.model_runner import VLLMModelRunner, TransformersModelRunner
12
+ from models.predictor_wrapper import MedGemmaPredictor
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MedGemma(Module):
18
+ """
19
+ MedGemma: 医学大语言模型
20
+
21
+ 支持:
22
+ - 4B 多模态模型(文本 + 医学图像)
23
+ - 27B 文本模型
24
+ - DICOM/CT/CXR/WSI 图像输入
25
+ - OpenAI Chat Completion API 格式
26
+
27
+ 继承 OneScience Module 基类,提供统一接口
28
+ """
29
+
30
+ def __init__(self, configs: Any) -> None:
31
+ """
32
+ 初始化 MedGemma 模型
33
+
34
+ Args:
35
+ configs: 配置对象(ConfigDict)
36
+ """
37
+ super(MedGemma, self).__init__()
38
+ self.configs = configs
39
+
40
+ # 模型变体(4B 或 27B)
41
+ self.model_variant = configs.model.variant
42
+ self.is_multimodal = configs.model.is_multimodal
43
+
44
+ logger.info(f"Initializing MedGemma {self.model_variant} model")
45
+ logger.info(f"Multimodal: {self.is_multimodal}")
46
+
47
+ # 初始化模型运行器
48
+ self._init_model_runner()
49
+
50
+ # 初始化推理包装器
51
+ self.predictor = MedGemmaPredictor(
52
+ model_runner=self.model_runner,
53
+ configs=configs,
54
+ )
55
+
56
+ logger.info("MedGemma model initialized successfully")
57
+
58
+ def _init_model_runner(self):
59
+ """初始化模型运行器(vLLM 或 Transformers)"""
60
+ if self.configs.inference.use_vllm:
61
+ try:
62
+ self.model_runner = VLLMModelRunner(
63
+ model_path=self.configs.model.model_path,
64
+ tokenizer_path=self.configs.model.tokenizer_path,
65
+ gpu_memory_utilization=self.configs.inference.gpu_memory_utilization,
66
+ max_model_len=self.configs.inference.max_model_len,
67
+ tensor_parallel_size=self.configs.inference.tensor_parallel_size,
68
+ )
69
+ logger.info("Using vLLM model runner")
70
+ except Exception as e:
71
+ logger.warning(f"vLLM initialization failed: {e}")
72
+ logger.info("Falling back to Transformers model runner")
73
+ self._init_transformers_runner()
74
+ else:
75
+ self._init_transformers_runner()
76
+
77
+ def _init_transformers_runner(self):
78
+ """初始化 Transformers 运行器"""
79
+ device = "cuda" if torch.cuda.is_available() else "cpu"
80
+ self.model_runner = TransformersModelRunner(
81
+ model_path=self.configs.model.model_path,
82
+ tokenizer_path=self.configs.model.tokenizer_path,
83
+ device=device,
84
+ )
85
+ logger.info("Using Transformers model runner")
86
+
87
+ def forward(
88
+ self,
89
+ messages: List[Dict[str, Any]],
90
+ max_tokens: Optional[int] = None,
91
+ temperature: float = 0.7,
92
+ top_p: float = 0.9,
93
+ n: int = 1,
94
+ ) -> Dict[str, Any]:
95
+ """
96
+ 前向传播(使用 OpenAI Chat Completion 格式)
97
+
98
+ Args:
99
+ messages: 消息列表,格式为 [{"role": "user", "content": "..."}]
100
+ max_tokens: 最大生成 token 数
101
+ temperature: 采样温度(0-2)
102
+ top_p: Nucleus 采样参数(0-1)
103
+ n: 生成数量
104
+
105
+ Returns:
106
+ OpenAI 格式的响应字典
107
+ """
108
+ if max_tokens is None:
109
+ max_tokens = self.configs.inference.default_max_tokens
110
+
111
+ return self.predictor.predict(
112
+ messages=messages,
113
+ max_tokens=max_tokens,
114
+ temperature=temperature,
115
+ top_p=top_p,
116
+ n=n,
117
+ )
118
+
119
+ @torch.no_grad()
120
+ def inference(self, data: Dict[str, Any]) -> Dict[str, Any]:
121
+ """
122
+ 推理方法(兼容 BiologyInferenceRunner)
123
+
124
+ Args:
125
+ data: 输入数据字典,可包含:
126
+ - messages: 消息列表
127
+ - instances: 实例列表(用于批处理)
128
+ - parameters: 推理参数
129
+
130
+ Returns:
131
+ 预测结果字典
132
+ """
133
+ # 转换数据格式
134
+ if "instances" in data:
135
+ messages = self._convert_instances_to_messages(data["instances"])
136
+ elif "messages" in data:
137
+ messages = data["messages"]
138
+ else:
139
+ raise ValueError("Input data must contain 'messages' or 'instances'")
140
+
141
+ # 提取推理参数
142
+ parameters = data.get("parameters", {})
143
+ max_tokens = parameters.get("max_tokens", self.configs.inference.default_max_tokens)
144
+ temperature = parameters.get("temperature", self.configs.inference.temperature)
145
+ top_p = parameters.get("top_p", self.configs.inference.top_p)
146
+ n = parameters.get("n", 1)
147
+
148
+ # 运行推理
149
+ return self.forward(
150
+ messages=messages,
151
+ max_tokens=max_tokens,
152
+ temperature=temperature,
153
+ top_p=top_p,
154
+ n=n,
155
+ )
156
+
157
+ def _convert_instances_to_messages(
158
+ self,
159
+ instances: List[Dict[str, Any]]
160
+ ) -> List[Dict[str, Any]]:
161
+ """
162
+ 将实例列表转换为消息格式
163
+
164
+ Args:
165
+ instances: 实例列表
166
+
167
+ Returns:
168
+ 消息列表
169
+ """
170
+ messages = []
171
+ for instance in instances:
172
+ if "role" in instance and "content" in instance:
173
+ messages.append(instance)
174
+ elif "text" in instance:
175
+ messages.append({"role": "user", "content": instance["text"]})
176
+ elif "question" in instance:
177
+ messages.append({"role": "user", "content": instance["question"]})
178
+ else:
179
+ logger.warning(f"Unknown instance format: {instance}")
180
+
181
+ return messages
182
+
183
+ def predict_text(
184
+ self,
185
+ text: str,
186
+ max_tokens: int = 500,
187
+ temperature: float = 0.7,
188
+ ) -> str:
189
+ """
190
+ 简化的文本预测接口
191
+
192
+ Args:
193
+ text: 输入文本
194
+ max_tokens: 最大生成 token 数
195
+ temperature: 采样温度
196
+
197
+ Returns:
198
+ 生成的文本
199
+ """
200
+ messages = [{"role": "user", "content": text}]
201
+ response = self.forward(messages, max_tokens, temperature)
202
+
203
+ if response["choices"]:
204
+ return response["choices"][0]["message"]["content"]
205
+ return ""
206
+
207
+ def predict_multimodal(
208
+ self,
209
+ text: str,
210
+ images: List[Any],
211
+ max_tokens: int = 500,
212
+ temperature: float = 0.7,
213
+ ) -> str:
214
+ """
215
+ 多模态预测接口(文本 + 图像)
216
+
217
+ Args:
218
+ text: 输入文本
219
+ images: 图像列表
220
+ max_tokens: 最大生成 token 数
221
+ temperature: 采样温度
222
+
223
+ Returns:
224
+ 生成的文本
225
+ """
226
+ if not self.is_multimodal:
227
+ logger.warning("Model is not multimodal, ignoring images")
228
+ return self.predict_text(text, max_tokens, temperature)
229
+
230
+ # TODO: 实现多模态推理
231
+ # 需要集成图像编码器和多模态提示格式
232
+ messages = [{"role": "user", "content": text}]
233
+ response = self.predictor.predict_with_images(
234
+ messages=messages,
235
+ images=images,
236
+ max_tokens=max_tokens,
237
+ temperature=temperature,
238
+ )
239
+
240
+ if response["choices"]:
241
+ return response["choices"][0]["message"]["content"]
242
+ return ""
243
+
244
+ def __repr__(self) -> str:
245
+ return f"MedGemma(variant={self.model_variant}, multimodal={self.is_multimodal})"
models/model_runner.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma vLLM 模型运行器
2
+ # 实现基于 vLLM 的推理引擎
3
+
4
+ import logging
5
+ from typing import Any, Dict, List, Optional, Set, Mapping
6
+ import numpy as np
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class VLLMModelRunner:
12
+ """
13
+ 基于 vLLM 的 MedGemma 模型运行器
14
+ 兼容 MedGemma serving_framework 的 ModelRunner 接口
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ model_path: str,
20
+ tokenizer_path: Optional[str] = None,
21
+ gpu_memory_utilization: float = 0.9,
22
+ max_model_len: Optional[int] = None,
23
+ tensor_parallel_size: int = 1,
24
+ trust_remote_code: bool = True,
25
+ ):
26
+ """
27
+ 初始化 vLLM 模型运行器
28
+
29
+ Args:
30
+ model_path: 模型权重路径
31
+ tokenizer_path: Tokenizer 路径(默认与 model_path 相同)
32
+ gpu_memory_utilization: GPU 内存使用率
33
+ max_model_len: 最大序列长度
34
+ tensor_parallel_size: Tensor 并行大小
35
+ trust_remote_code: 是否信任远程代码
36
+ """
37
+ self.model_path = model_path
38
+ self.tokenizer_path = tokenizer_path or model_path
39
+
40
+ try:
41
+ from vllm import LLM
42
+ self.vllm_available = True
43
+ except ImportError:
44
+ logger.warning("vLLM not available. Install with: pip install vllm")
45
+ self.vllm_available = False
46
+ self.llm = None
47
+ return
48
+
49
+ logger.info(f"Initializing vLLM with model: {model_path}")
50
+ self.llm = LLM(
51
+ model=self.model_path,
52
+ tokenizer=self.tokenizer_path,
53
+ gpu_memory_utilization=gpu_memory_utilization,
54
+ max_model_len=max_model_len,
55
+ tensor_parallel_size=tensor_parallel_size,
56
+ trust_remote_code=trust_remote_code,
57
+ )
58
+ logger.info("vLLM model loaded successfully")
59
+
60
+ def run_model_multiple_output(
61
+ self,
62
+ model_input: Mapping[str, np.ndarray] | np.ndarray,
63
+ model_name: str = "default",
64
+ model_version: Optional[int] = None,
65
+ model_output_keys: Optional[Set[str]] = None,
66
+ parameters: Optional[Mapping[str, Any]] = None,
67
+ ) -> Mapping[str, np.ndarray]:
68
+ """
69
+ 运行推理(兼容 MedGemma ModelRunner 接口)
70
+
71
+ Args:
72
+ model_input: 输入数据(字典或 numpy 数组)
73
+ model_name: 模型名称
74
+ model_version: 模型版本
75
+ model_output_keys: 输出键集合
76
+ parameters: 推理参数
77
+
78
+ Returns:
79
+ 输出字典
80
+ """
81
+ if not self.vllm_available or self.llm is None:
82
+ raise RuntimeError("vLLM is not available")
83
+
84
+ from vllm import SamplingParams
85
+
86
+ parameters = parameters or {}
87
+ model_output_keys = model_output_keys or {"text_output"}
88
+
89
+ # 提取 prompt
90
+ if isinstance(model_input, dict):
91
+ prompt = model_input.get("prompt", "")
92
+ if isinstance(prompt, np.ndarray):
93
+ prompt = prompt.tobytes().decode('utf-8')
94
+ else:
95
+ # 如果是 numpy 数组,解码为字符串
96
+ prompt = model_input.tobytes().decode('utf-8')
97
+
98
+ # 创建采样参数
99
+ sampling_params = SamplingParams(
100
+ max_tokens=parameters.get("max_tokens", 500),
101
+ temperature=parameters.get("temperature", 0.7),
102
+ top_p=parameters.get("top_p", 0.9),
103
+ top_k=parameters.get("top_k", -1),
104
+ n=parameters.get("n", 1),
105
+ )
106
+
107
+ # 运行推理
108
+ outputs = self.llm.generate([prompt], sampling_params)
109
+
110
+ # 格式化输出
111
+ result = {}
112
+ if "text_output" in model_output_keys:
113
+ result["text_output"] = np.array([
114
+ output.text.encode('utf-8') for output in outputs[0].outputs
115
+ ])
116
+ if "num_input_tokens" in model_output_keys:
117
+ result["num_input_tokens"] = np.array([len(outputs[0].prompt_token_ids)])
118
+ if "num_output_tokens" in model_output_keys:
119
+ result["num_output_tokens"] = np.array([
120
+ len(output.token_ids) for output in outputs[0].outputs
121
+ ])
122
+
123
+ return result
124
+
125
+ def generate(
126
+ self,
127
+ prompts: List[str],
128
+ max_tokens: int = 500,
129
+ temperature: float = 0.7,
130
+ top_p: float = 0.9,
131
+ top_k: int = -1,
132
+ n: int = 1,
133
+ ) -> List[Dict[str, Any]]:
134
+ """
135
+ 简化的生成接口
136
+
137
+ Args:
138
+ prompts: 输入提示列表
139
+ max_tokens: 最大生成 token 数
140
+ temperature: 采样温度
141
+ top_p: Nucleus 采样参数
142
+ top_k: Top-K 采样参数
143
+ n: 生成数量
144
+
145
+ Returns:
146
+ 生成结果列表
147
+ """
148
+ if not self.vllm_available or self.llm is None:
149
+ raise RuntimeError("vLLM is not available")
150
+
151
+ from vllm import SamplingParams
152
+
153
+ sampling_params = SamplingParams(
154
+ max_tokens=max_tokens,
155
+ temperature=temperature,
156
+ top_p=top_p,
157
+ top_k=top_k,
158
+ n=n,
159
+ )
160
+
161
+ outputs = self.llm.generate(prompts, sampling_params)
162
+
163
+ results = []
164
+ for output in outputs:
165
+ result = {
166
+ "prompt": output.prompt,
167
+ "outputs": [
168
+ {
169
+ "text": o.text,
170
+ "token_ids": o.token_ids,
171
+ "cumulative_logprob": o.cumulative_logprob,
172
+ "finish_reason": o.finish_reason,
173
+ }
174
+ for o in output.outputs
175
+ ],
176
+ "num_input_tokens": len(output.prompt_token_ids),
177
+ }
178
+ results.append(result)
179
+
180
+ return results
181
+
182
+
183
+ class TransformersModelRunner:
184
+ """
185
+ 基于 Transformers 的备用模型运行器
186
+ 当 vLLM 不可用时使用
187
+ """
188
+
189
+ def __init__(
190
+ self,
191
+ model_path: str,
192
+ tokenizer_path: Optional[str] = None,
193
+ device: str = "cuda",
194
+ torch_dtype: str = "auto",
195
+ ):
196
+ """
197
+ 初始化 Transformers 模型运行器
198
+
199
+ Args:
200
+ model_path: 模型路径
201
+ tokenizer_path: Tokenizer 路径
202
+ device: 设备(cuda 或 cpu)
203
+ torch_dtype: 数据类型
204
+ """
205
+ import torch
206
+ from transformers import AutoModelForCausalLM, AutoTokenizer
207
+
208
+ self.device = device
209
+ self.tokenizer_path = tokenizer_path or model_path
210
+
211
+ logger.info(f"Loading model with transformers: {model_path}")
212
+ self.tokenizer = AutoTokenizer.from_pretrained(self.tokenizer_path)
213
+ self.model = AutoModelForCausalLM.from_pretrained(
214
+ model_path,
215
+ torch_dtype=torch_dtype if torch_dtype != "auto" else "auto",
216
+ device_map=device,
217
+ )
218
+ logger.info("Model loaded successfully")
219
+
220
+ def generate(
221
+ self,
222
+ prompts: List[str],
223
+ max_tokens: int = 500,
224
+ temperature: float = 0.7,
225
+ top_p: float = 0.9,
226
+ top_k: int = 50,
227
+ n: int = 1,
228
+ ) -> List[Dict[str, Any]]:
229
+ """
230
+ 生成文本
231
+
232
+ Args:
233
+ prompts: 输入提示列表
234
+ max_tokens: 最大生成 token 数
235
+ temperature: 采样温度
236
+ top_p: Nucleus 采样参数
237
+ top_k: Top-K 采样参数
238
+ n: 生成数量
239
+
240
+ Returns:
241
+ 生成结果列表
242
+ """
243
+ import torch
244
+
245
+ results = []
246
+ for prompt in prompts:
247
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
248
+
249
+ with torch.no_grad():
250
+ outputs = self.model.generate(
251
+ **inputs,
252
+ max_new_tokens=max_tokens,
253
+ temperature=temperature,
254
+ top_p=top_p,
255
+ top_k=top_k,
256
+ num_return_sequences=n,
257
+ do_sample=temperature > 0,
258
+ )
259
+
260
+ generated_texts = [
261
+ self.tokenizer.decode(output, skip_special_tokens=True)
262
+ for output in outputs
263
+ ]
264
+
265
+ result = {
266
+ "prompt": prompt,
267
+ "outputs": [
268
+ {
269
+ "text": text,
270
+ "token_ids": None,
271
+ "cumulative_logprob": None,
272
+ "finish_reason": "stop",
273
+ }
274
+ for text in generated_texts
275
+ ],
276
+ "num_input_tokens": len(inputs["input_ids"][0]),
277
+ }
278
+ results.append(result)
279
+
280
+ return results
models/predictor_wrapper.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 推理包装器
2
+ # 包装 MedGemma 原始 predictor.py 的推理逻辑
3
+
4
+ import logging
5
+ import os
6
+ import sys
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class MedGemmaPredictor:
13
+ """
14
+ MedGemma 推理包装器
15
+ 包装原始 MedGemma predictor 逻辑,提供 OneScience 兼容接口
16
+ """
17
+
18
+ def __init__(self, model_runner: Any, configs: Any):
19
+ """
20
+ 初始化推理包装器
21
+
22
+ Args:
23
+ model_runner: 模型运行器(VLLMModelRunner 或 TransformersModelRunner)
24
+ configs: 配置对象
25
+ """
26
+ self.model_runner = model_runner
27
+ self.configs = configs
28
+
29
+ # 尝试导入原始 MedGemma 组件(如果可用)
30
+ self._init_medgemma_components()
31
+
32
+ def _init_medgemma_components(self):
33
+ """初始化 MedGemma 原始组件"""
34
+ try:
35
+ # 添加 MedGemma 原始代码路径到 sys.path
36
+ medgemma_base = os.path.abspath(
37
+ os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "..", "..", "medgemma", "python")
38
+ )
39
+
40
+ if os.path.exists(medgemma_base) and medgemma_base not in sys.path:
41
+ sys.path.insert(0, medgemma_base)
42
+ logger.info(f"Added MedGemma path: {medgemma_base}")
43
+
44
+ # 尝试导入 MedGemma predictor 组件
45
+ try:
46
+ from serving import predictor
47
+ self.has_original_predictor = True
48
+ logger.info("Successfully imported original MedGemma predictor")
49
+ except ImportError as e:
50
+ logger.warning(f"Could not import original MedGemma predictor: {e}")
51
+ self.has_original_predictor = False
52
+
53
+ except Exception as e:
54
+ logger.warning(f"Error initializing MedGemma components: {e}")
55
+ self.has_original_predictor = False
56
+
57
+ def predict(
58
+ self,
59
+ messages: List[Dict[str, Any]],
60
+ max_tokens: int = 500,
61
+ temperature: float = 0.7,
62
+ top_p: float = 0.9,
63
+ n: int = 1,
64
+ ) -> Dict[str, Any]:
65
+ """
66
+ 运行推理
67
+
68
+ Args:
69
+ messages: OpenAI Chat Completion 格式的消息列表
70
+ max_tokens: 最大生成 token 数
71
+ temperature: 采样温度
72
+ top_p: Nucleus 采样参数
73
+ n: 生成数量
74
+
75
+ Returns:
76
+ OpenAI 兼容格式的响应
77
+ """
78
+ # 转换消息为 prompt
79
+ prompt = self._messages_to_prompt(messages)
80
+
81
+ # 运行模型推理
82
+ results = self.model_runner.generate(
83
+ prompts=[prompt],
84
+ max_tokens=max_tokens,
85
+ temperature=temperature,
86
+ top_p=top_p,
87
+ n=n,
88
+ )
89
+
90
+ # 格式化响应为 OpenAI 格式
91
+ return self._format_openai_response(results[0], messages)
92
+
93
+ def _messages_to_prompt(self, messages: List[Dict[str, Any]]) -> str:
94
+ """
95
+ 将 OpenAI 消息格式转换为 prompt
96
+
97
+ Args:
98
+ messages: 消息列表
99
+
100
+ Returns:
101
+ 格式化的 prompt 字符串
102
+ """
103
+ prompt_parts = []
104
+
105
+ for message in messages:
106
+ role = message.get("role", "user")
107
+ content = message.get("content", "")
108
+
109
+ # 处理不同角色的消息
110
+ if role == "system":
111
+ prompt_parts.append(f"System: {content}")
112
+ elif role == "user":
113
+ prompt_parts.append(f"User: {content}")
114
+ elif role == "assistant":
115
+ prompt_parts.append(f"Assistant: {content}")
116
+ else:
117
+ prompt_parts.append(f"{role}: {content}")
118
+
119
+ # 添加 Assistant 前缀以开始生成
120
+ prompt_parts.append("Assistant:")
121
+
122
+ return "\n".join(prompt_parts)
123
+
124
+ def _format_openai_response(
125
+ self,
126
+ result: Dict[str, Any],
127
+ messages: List[Dict[str, Any]]
128
+ ) -> Dict[str, Any]:
129
+ """
130
+ 将模型输出格式化为 OpenAI Chat Completion 格式
131
+
132
+ Args:
133
+ result: 模型生成结果
134
+ messages: 原始消息
135
+
136
+ Returns:
137
+ OpenAI 格式的响应
138
+ """
139
+ import time
140
+ import uuid
141
+
142
+ choices = []
143
+ for idx, output in enumerate(result["outputs"]):
144
+ choice = {
145
+ "index": idx,
146
+ "message": {
147
+ "role": "assistant",
148
+ "content": output["text"].replace(result["prompt"], "").strip(),
149
+ },
150
+ "finish_reason": output.get("finish_reason", "stop"),
151
+ }
152
+ choices.append(choice)
153
+
154
+ response = {
155
+ "id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
156
+ "object": "chat.completion",
157
+ "created": int(time.time()),
158
+ "model": self.configs.model.variant,
159
+ "choices": choices,
160
+ "usage": {
161
+ "prompt_tokens": result.get("num_input_tokens", 0),
162
+ "completion_tokens": sum(
163
+ len(output.get("token_ids", [])) if output.get("token_ids") else 0
164
+ for output in result["outputs"]
165
+ ),
166
+ "total_tokens": result.get("num_input_tokens", 0) + sum(
167
+ len(output.get("token_ids", [])) if output.get("token_ids") else 0
168
+ for output in result["outputs"]
169
+ ),
170
+ },
171
+ }
172
+
173
+ return response
174
+
175
+ def predict_with_images(
176
+ self,
177
+ messages: List[Dict[str, Any]],
178
+ images: List[Any],
179
+ max_tokens: int = 500,
180
+ temperature: float = 0.7,
181
+ top_p: float = 0.9,
182
+ ) -> Dict[str, Any]:
183
+ """
184
+ 多模态推理(文本 + 图像)
185
+
186
+ Args:
187
+ messages: 消息列表
188
+ images: 图像列表
189
+ max_tokens: 最大生成 token 数
190
+ temperature: 采样温度
191
+ top_p: Nucleus 采样参数
192
+
193
+ Returns:
194
+ 响应字典
195
+ """
196
+ # TODO: 实现多模态推理
197
+ # 这需要集成 MedGemma 的图像处理逻辑
198
+ logger.warning("Multimodal inference not yet fully implemented")
199
+
200
+ # 暂时只处理文本
201
+ return self.predict(messages, max_tokens, temperature, top_p)
runner/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .medical_inference_runner import MedicalInferenceRunner
2
+
3
+ __all__ = ["MedicalInferenceRunner"]
runner/medical_inference_runner.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MedGemma 医学推理运行器
2
+ # 类似 Protenix 的 BiologyInferenceRunner
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ DIR = Path(__file__).resolve().parent.parent
7
+ sys.path.insert(0, str(DIR))
8
+
9
+ import logging
10
+ import os
11
+ import json
12
+ from typing import Any, Dict, List, Optional
13
+ import torch
14
+
15
+ from models.medgemma import MedGemma
16
+ from models.config import parse_configs, load_config
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class AttrDict(dict):
21
+ """同时支持 config.key 和 config['key']。"""
22
+
23
+ def __getattr__(self, key):
24
+ try:
25
+ return self[key]
26
+ except KeyError as exc:
27
+ raise AttributeError(key) from exc
28
+
29
+ def __setattr__(self, key, value):
30
+ self[key] = value
31
+
32
+ def to_attr_dict(value):
33
+ if isinstance(value, dict):
34
+ return AttrDict({
35
+ key: to_attr_dict(item)
36
+ for key, item in value.items()
37
+ })
38
+
39
+ if isinstance(value, list):
40
+ return [to_attr_dict(item) for item in value]
41
+
42
+ if isinstance(value, tuple):
43
+ return tuple(to_attr_dict(item) for item in value)
44
+
45
+ return value
46
+
47
+
48
+ class MedicalInferenceRunner:
49
+ """
50
+ MedGemma 医学推理运行器
51
+ 提供统一的推理接口
52
+ """
53
+
54
+ def __init__(self, configs: Any) -> None:
55
+ """
56
+ 初始化推理运行器
57
+
58
+ Args:
59
+ configs: 配置对象
60
+ """
61
+ #self.configs = configs
62
+ self.configs = to_attr_dict(configs)
63
+ self.init_env()
64
+ self.init_basics()
65
+ self.init_model()
66
+ self.init_dumper()
67
+
68
+ logger.info("MedicalInferenceRunner initialized")
69
+
70
+ def init_env(self) -> None:
71
+ """初始化环境"""
72
+ self.use_cuda = torch.cuda.is_available()
73
+ if self.use_cuda:
74
+ self.device = torch.device("cuda:0")
75
+ torch.cuda.set_device(self.device)
76
+ logger.info(f"Using GPU: {torch.cuda.get_device_name(0)}")
77
+ else:
78
+ self.device = torch.device("cpu")
79
+ logger.info("Using CPU")
80
+
81
+ def init_basics(self) -> None:
82
+ """初始化基础设置"""
83
+ self.dump_dir = self.configs.output.dump_dir
84
+ self.error_dir = os.path.join(self.dump_dir, "errors")
85
+ os.makedirs(self.dump_dir, exist_ok=True)
86
+ os.makedirs(self.error_dir, exist_ok=True)
87
+ logger.info(f"Output directory: {self.dump_dir}")
88
+
89
+ def init_model(self) -> None:
90
+ """初始化 MedGemma 模型"""
91
+ logger.info("Loading MedGemma model...")
92
+ try:
93
+ self.model = MedGemma(self.configs)
94
+ logger.info("Model loaded successfully")
95
+ except Exception as e:
96
+ logger.error(f"Failed to load model: {e}")
97
+ raise
98
+
99
+ def init_dumper(self) -> None:
100
+ """初始化结果保存器"""
101
+ self.output_format = self.configs.output.output_format
102
+ self.save_predictions = self.configs.output.save_predictions
103
+ logger.info(f"Output format: {self.output_format}")
104
+
105
+ @torch.no_grad()
106
+ def predict(
107
+ self,
108
+ messages: List[Dict[str, Any]],
109
+ max_tokens: Optional[int] = None,
110
+ temperature: Optional[float] = None,
111
+ ) -> Dict[str, Any]:
112
+ """
113
+ 运行推理
114
+
115
+ Args:
116
+ messages: 消息列表
117
+ max_tokens: 最大生成 token 数
118
+ temperature: 采样温度
119
+
120
+ Returns:
121
+ 预测结果
122
+ """
123
+ if max_tokens is None:
124
+ max_tokens = self.configs.inference.default_max_tokens
125
+ if temperature is None:
126
+ temperature = self.configs.inference.temperature
127
+
128
+ try:
129
+ result = self.model.forward(
130
+ messages=messages,
131
+ max_tokens=max_tokens,
132
+ temperature=temperature,
133
+ )
134
+ return result
135
+ except Exception as e:
136
+ logger.error(f"Prediction failed: {e}")
137
+ return {"error": str(e)}
138
+
139
+ def run_from_file(self, input_path: str) -> None:
140
+ """
141
+ 从文件运行推理
142
+
143
+ Args:
144
+ input_path: 输入文件路径(JSON 或 JSONL)
145
+ """
146
+ logger.info(f"Loading input from: {input_path}")
147
+
148
+ # 读取输入数据
149
+ if input_path.endswith('.jsonl'):
150
+ samples = self._load_jsonl(input_path)
151
+ elif input_path.endswith('.json'):
152
+ samples = self._load_json(input_path)
153
+ else:
154
+ raise ValueError(f"Unsupported file format: {input_path}")
155
+
156
+ logger.info(f"Loaded {len(samples)} samples")
157
+
158
+ # 处理每个样本
159
+ results = []
160
+ for idx, sample in enumerate(samples):
161
+ logger.info(f"Processing sample {idx + 1}/{len(samples)}")
162
+
163
+ try:
164
+ # 提取消息
165
+ if "messages" in sample:
166
+ messages = sample["messages"]
167
+ elif "text" in sample:
168
+ messages = [{"role": "user", "content": sample["text"]}]
169
+ elif "question" in sample:
170
+ messages = [{"role": "user", "content": sample["question"]}]
171
+ else:
172
+ logger.warning(f"Sample {idx} has no valid input")
173
+ continue
174
+
175
+ # 运行推理
176
+ result = self.predict(messages)
177
+
178
+ # 添加样本 ID
179
+ result["sample_id"] = sample.get("id", idx)
180
+
181
+ # 保存结果
182
+ if self.save_predictions:
183
+ self._save_result(result, idx)
184
+
185
+ results.append(result)
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error processing sample {idx}: {e}")
189
+ self._save_error(sample, idx, str(e))
190
+
191
+ logger.info(f"Completed processing {len(results)} samples")
192
+
193
+ # 保存汇总结果
194
+ self._save_summary(results)
195
+
196
+ def _load_json(self, filepath: str) -> List[Dict[str, Any]]:
197
+ """加载 JSON 文件"""
198
+ with open(filepath, 'r', encoding='utf-8') as f:
199
+ data = json.load(f)
200
+
201
+ if isinstance(data, list):
202
+ return data
203
+ else:
204
+ return [data]
205
+
206
+ def _load_jsonl(self, filepath: str) -> List[Dict[str, Any]]:
207
+ """加载 JSONL 文件"""
208
+ samples = []
209
+ with open(filepath, 'r', encoding='utf-8') as f:
210
+ for line in f:
211
+ if line.strip():
212
+ samples.append(json.loads(line))
213
+ return samples
214
+
215
+ def _save_result(self, result: Dict[str, Any], idx: int) -> None:
216
+ """保存单个结果"""
217
+ output_path = os.path.join(
218
+ self.dump_dir,
219
+ f"prediction_{idx}.{self.output_format}"
220
+ )
221
+
222
+ with open(output_path, 'w', encoding='utf-8') as f:
223
+ json.dump(result, f, indent=2, ensure_ascii=False)
224
+
225
+ def _save_error(self, sample: Dict[str, Any], idx: int, error: str) -> None:
226
+ """保存错误信息"""
227
+ error_path = os.path.join(
228
+ self.error_dir,
229
+ f"error_{idx}.json"
230
+ )
231
+
232
+ error_data = {
233
+ "sample": sample,
234
+ "error": error,
235
+ }
236
+
237
+ with open(error_path, 'w', encoding='utf-8') as f:
238
+ json.dump(error_data, f, indent=2, ensure_ascii=False)
239
+
240
+ def _save_summary(self, results: List[Dict[str, Any]]) -> None:
241
+ """保存汇总结果"""
242
+ summary_path = os.path.join(self.dump_dir, "summary.json")
243
+
244
+ summary = {
245
+ "total_samples": len(results),
246
+ "successful": sum(1 for r in results if "error" not in r),
247
+ "failed": sum(1 for r in results if "error" in r),
248
+ "results": results,
249
+ }
250
+
251
+ with open(summary_path, 'w', encoding='utf-8') as f:
252
+ json.dump(summary, f, indent=2, ensure_ascii=False)
253
+
254
+ logger.info(f"Summary saved to: {summary_path}")
255
+
256
+ def run_interactive(self) -> None:
257
+ """交互式推理"""
258
+ logger.info("Starting interactive mode. Type 'quit' to exit.")
259
+
260
+ while True:
261
+ try:
262
+ user_input = input("\nUser: ")
263
+ if user_input.lower() in ['quit', 'exit', 'q']:
264
+ break
265
+
266
+ messages = [{"role": "user", "content": user_input}]
267
+ result = self.predict(messages)
268
+
269
+ # 提取响应
270
+ if "choices" in result and result["choices"]:
271
+ response = result["choices"][0]["message"]["content"]
272
+ print(f"\nAssistant: {response}")
273
+ else:
274
+ print(f"\nError: {result}")
275
+
276
+ except KeyboardInterrupt:
277
+ break
278
+ except Exception as e:
279
+ logger.error(f"Error: {e}")
280
+
281
+ logger.info("Exiting interactive mode")
282
+
283
+
284
+ def main():
285
+ """主函数"""
286
+ import argparse
287
+
288
+ parser = argparse.ArgumentParser(description="MedGemma Medical Inference Runner")
289
+ parser.add_argument("--config", type=str, required=True, help="Config file path")
290
+ parser.add_argument("--input", type=str, help="Input file path (JSON/JSONL)")
291
+ parser.add_argument("--interactive", action="store_true", help="Interactive mode")
292
+ parser.add_argument("--model_path", type=str, help="Override model path")
293
+ parser.add_argument("--dump_dir", type=str, help="Override output directory")
294
+
295
+ args = parser.parse_args()
296
+
297
+ # 加载配置
298
+ configs = load_config(args.config)
299
+
300
+ # 覆盖配置
301
+ if args.model_path:
302
+ configs.model.model_path = args.model_path
303
+ if args.dump_dir:
304
+ configs.output.dump_dir = args.dump_dir
305
+
306
+ # 创建运行器
307
+ runner = MedicalInferenceRunner(configs)
308
+
309
+ # 运行推理
310
+ if args.interactive:
311
+ runner.run_interactive()
312
+ elif args.input:
313
+ runner.run_from_file(args.input)
314
+ else:
315
+ logger.error("Either --input or --interactive must be specified")
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()
scripts/notebook_conver/cxr_anatomy_localization_with_hugging_face.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # c6.py - MedGemma 胸部X光解剖结构定位(增强版)
3
+ # 功能:
4
+ # - 单张/批量图片推理
5
+ # - 指定输出目录
6
+ # - 指定使用的GPU数量
7
+ # - 坐标自动适配(0-1 或 0-1000)
8
+ # - 本地模型加载(local_files_only)
9
+
10
+ import os
11
+ import sys
12
+ import argparse
13
+ import json
14
+ import re
15
+ import torch
16
+ import numpy as np
17
+ import skimage
18
+ from PIL import Image, ImageDraw
19
+ from pathlib import Path
20
+ from transformers import AutoProcessor, AutoModelForImageTextToText
21
+
22
+ # ---------- Monkey-patch for torch<2.6 ----------
23
+ import transformers.masking_utils as masking_utils
24
+ if torch.__version__ < "2.6":
25
+ _orig_causal = masking_utils.create_causal_mask
26
+ _orig_sliding = masking_utils.create_sliding_window_causal_mask
27
+
28
+ def _safe_pop(kw):
29
+ kw.pop("or_mask_function", None)
30
+ kw.pop("and_mask_function", None)
31
+
32
+ def _new_causal(*a, **kw):
33
+ _safe_pop(kw)
34
+ return _orig_causal(*a, **kw)
35
+
36
+ def _new_sliding(*a, **kw):
37
+ _safe_pop(kw)
38
+ return _orig_sliding(*a, **kw)
39
+
40
+ masking_utils.create_causal_mask = _new_causal
41
+ masking_utils.create_sliding_window_causal_mask = _new_sliding
42
+
43
+
44
+ def pad_image_to_square(image_array):
45
+ """将图像填充为正方形(与训练预处理一致)"""
46
+ image_array = skimage.util.img_as_ubyte(image_array)
47
+ if image_array.ndim < 3:
48
+ image_array = skimage.color.gray2rgb(image_array)
49
+ if image_array.shape[2] == 4:
50
+ image_array = skimage.color.rgba2rgb(image_array)
51
+
52
+ h, w = image_array.shape[:2]
53
+ if h < w:
54
+ dh = w - h
55
+ image_array = np.pad(image_array, ((dh // 2, dh - dh // 2), (0, 0), (0, 0)))
56
+ elif w < h:
57
+ dw = h - w
58
+ image_array = np.pad(image_array, ((0, 0), (dw // 2, dw - dw // 2), (0, 0)))
59
+ return image_array
60
+
61
+
62
+ def draw_bounding_box(image, bbox_data, object_name):
63
+ """绘制边界框(自动识别 0-1 或 0-1000 坐标)"""
64
+ w_orig, h_orig = image.size
65
+ new_w = 512
66
+ new_h = int(h_orig * (new_w / w_orig))
67
+ scaled = image.resize((new_w, new_h))
68
+ draw = ImageDraw.Draw(scaled)
69
+
70
+ for item in bbox_data:
71
+ box = item.get("box_2d")
72
+ label = item.get("label", object_name)
73
+ if box and len(box) == 4:
74
+ y0, x0, y1, x1 = box
75
+
76
+ # 若坐标值均在 [-1,1] 区间(实际上≤1),视为 0-1 归一化,乘以 1000
77
+ if max(abs(y0), abs(x0), abs(y1), abs(x1)) <= 1.0:
78
+ y0, x0, y1, x1 = y0 * 1000, x0 * 1000, y1 * 1000, x1 * 1000
79
+
80
+ # 转换到像素坐标(以 512 宽为基准)
81
+ x0_px = x0 / 1000 * new_w
82
+ y0_px = y0 / 1000 * new_h
83
+ x1_px = x1 / 1000 * new_w
84
+ y1_px = y1 / 1000 * new_h
85
+
86
+ draw.rectangle([(x0_px, y0_px), (x1_px, y1_px)], outline="red", width=2)
87
+ if label:
88
+ draw.text((x0_px, y0_px - 15), label, fill="red")
89
+ return scaled
90
+
91
+
92
+ def build_prompt(object_name):
93
+ """构造单结构定位提示词"""
94
+ return f"""Instructions:
95
+ The following user query will require outputting bounding boxes. The format of bounding boxes coordinates is [y0, x0, y1, x1] where (y0, x0) must be top-left corner and (y1, x1) the bottom-right corner. This implies that x0 < x1 and y0 < y1. Always normalize the x and y coordinates the range [0, 1000], meaning that a bounding box starting at 15% of the image width would be associated with an x coordinate of 150. You MUST output a single parseable json list of objects enclosed into ```json...``` brackets, for instance ```json[{{"box_2d": [800, 3, 840, 471], "label": "car"}}, {{"box_2d": [400, 22, 600, 73], "label": "dog"}}]``` is a valid output. Now answer to the user query.
96
+
97
+ Remember "left" refers to the patient's left side where the heart is and sometimes underneath an L in the upper right corner of the image.
98
+
99
+ Query:
100
+ Where is the {object_name}? Don't give a final answer without reasoning. Output the final answer in the format "Final Answer: X" where X is a JSON list of objects. The object needs a "box_2d" and "label" key. Answer:"""
101
+
102
+
103
+ def extract_bbox_data(response):
104
+ """从模型回复中提取 JSON 边界框列表(增强鲁棒性)"""
105
+ json_str = ""
106
+ # 策略1:优先提取 Final Answer: 后面的 ```json...```
107
+ final_match = re.search(r'Final Answer:\s*```json\s*(\[.*?\])\s*```', response, re.DOTALL)
108
+ if final_match:
109
+ json_str = final_match.group(1).strip()
110
+ else:
111
+ # 策略2:查找 ```json...```
112
+ if "```json" in response:
113
+ start = response.find("```json") + len("```json")
114
+ end = response.find("```", start)
115
+ if end != -1:
116
+ json_str = response[start:end].strip()
117
+ if not json_str:
118
+ # 回退:提取最后一个 JSON 数组
119
+ matches = re.findall(r'\[.*\]', response, re.DOTALL)
120
+ if matches:
121
+ json_str = matches[-1]
122
+
123
+ if json_str:
124
+ try:
125
+ return json.loads(json_str)
126
+ except json.JSONDecodeError as e:
127
+ print(f" JSON 解析错误: {e}\n 提取的字符串: {json_str}")
128
+ return []
129
+
130
+
131
+ def process_single_image(img_path, model, processor, object_name, output_dir, preprocess=True):
132
+ """处理单张图像,返回结果字典"""
133
+ print(f"\n{'='*60}\nProcessing: {img_path}")
134
+ image = Image.open(img_path).convert("RGB")
135
+ orig_size = image.size
136
+ if preprocess:
137
+ image = Image.fromarray(pad_image_to_square(np.array(image)))
138
+ print(f" Preprocessed: {image.size} (original: {orig_size})")
139
+
140
+ prompt = build_prompt(object_name)
141
+ messages = [
142
+ {"role": "user", "content": [
143
+ {"type": "image", "image": image},
144
+ {"type": "text", "text": prompt}
145
+ ]}
146
+ ]
147
+
148
+ # 处理输入
149
+ inputs = processor.apply_chat_template(
150
+ messages,
151
+ add_generation_prompt=True,
152
+ tokenize=True,
153
+ return_tensors="pt",
154
+ return_dict=True,
155
+ )
156
+ device = model.device
157
+ inputs = {k: v.to(device) for k, v in inputs.items()}
158
+
159
+ # 推理
160
+ with torch.inference_mode():
161
+ gen = model.generate(**inputs, max_new_tokens=1000, do_sample=False)
162
+
163
+ input_len = inputs["input_ids"].shape[1]
164
+ response = processor.batch_decode(gen[:, input_len:], skip_special_tokens=True)[0]
165
+
166
+ # 提取坐标
167
+ bbox_data = extract_bbox_data(response)
168
+ print(f" Detected {len(bbox_data)} bounding box(es) for '{object_name}'.")
169
+
170
+ # 保存标注图片
171
+ base = Path(img_path).stem
172
+ out_img_path = os.path.join(output_dir, f"result_{base}_{object_name.replace(' ', '_')}.png")
173
+ if bbox_data:
174
+ draw_bounding_box(image, bbox_data, object_name).save(out_img_path)
175
+ print(f" Annotated image saved: {out_img_path}")
176
+
177
+ result = {
178
+ "image_path": str(img_path),
179
+ "original_size": list(orig_size),
180
+ "object_name": object_name,
181
+ "bounding_boxes": bbox_data,
182
+ "raw_response": response
183
+ }
184
+ # 保存单张 JSON(可选)
185
+ json_path = os.path.join(output_dir, f"result_{base}_{object_name.replace(' ', '_')}.json")
186
+ with open(json_path, "w") as f:
187
+ json.dump(result, f, indent=2)
188
+ return result
189
+
190
+
191
+ def main():
192
+ parser = argparse.ArgumentParser(description="MedGemma 胸部X光单结构定位(支持批量)")
193
+ parser.add_argument("--image_path", help="单张图像路径")
194
+ parser.add_argument("--input_dir", help="批量处理图像文件夹")
195
+ parser.add_argument("--model_path", required=True, help="本地模型目录路径")
196
+ parser.add_argument("--object_name", default="right clavicle", help="要定位的解剖结构名称")
197
+ parser.add_argument("--output_dir", default="./outputs", help="结果保存目录(默认 ./outputs)")
198
+ parser.add_argument("--num_gpus", type=int, default=None, help="使用的GPU数量(如 2)")
199
+ parser.add_argument("--preprocess", action="store_true", default=True, help="是否将图像填充为正方形")
200
+ args = parser.parse_args()
201
+
202
+ if not args.image_path and not args.input_dir:
203
+ parser.error("必须指定 --image_path 或 --input_dir 之一")
204
+ if args.image_path and args.input_dir:
205
+ parser.error("不能同时指定 --image_path 和 --input_dir")
206
+
207
+ os.makedirs(args.output_dir, exist_ok=True)
208
+
209
+ # GPU 设置
210
+ if args.num_gpus and args.num_gpus > 0:
211
+ available = torch.cuda.device_count()
212
+ num = min(args.num_gpus, available)
213
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(str(i) for i in range(num))
214
+ print(f"Using GPUs: {os.environ['CUDA_VISIBLE_DEVICES']} (available: {available})")
215
+ else:
216
+ print(f"All GPUs ({torch.cuda.device_count()}) available.")
217
+
218
+ # 加载模型
219
+ print(f"Loading model from {args.model_path}")
220
+ processor = AutoProcessor.from_pretrained(
221
+ args.model_path,
222
+ trust_remote_code=True
223
+ #local_files_only=True
224
+ )
225
+ model = AutoModelForImageTextToText.from_pretrained(
226
+ args.model_path,
227
+ dtype=torch.bfloat16,
228
+ device_map="auto",
229
+ trust_remote_code=True,
230
+ attn_implementation="eager"
231
+ #local_files_only=True
232
+ )
233
+ print("Model loaded.\n")
234
+
235
+ # 构建图像列表
236
+ if args.image_path:
237
+ image_paths = [args.image_path]
238
+ else:
239
+ # 扫描文件夹中所有常见图像文件
240
+ exts = ('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff')
241
+ image_paths = sorted([
242
+ os.path.join(args.input_dir, f)
243
+ for f in os.listdir(args.input_dir)
244
+ if f.lower().endswith(exts)
245
+ ])
246
+ if not image_paths:
247
+ print(f"No images found in {args.input_dir}")
248
+ return
249
+ print(f"Images to process: {len(image_paths)}, target: '{args.object_name}'")
250
+
251
+ # 逐张处理
252
+ all_results = []
253
+ for p in image_paths:
254
+ try:
255
+ res = process_single_image(p, model, processor, args.object_name, args.output_dir, args.preprocess)
256
+ all_results.append(res)
257
+ except Exception as e:
258
+ print(f" Error processing {p}: {e}")
259
+
260
+ # 批量汇总
261
+ if len(image_paths) > 1 or args.input_dir:
262
+ summary_path = os.path.join(args.output_dir, "batch_summary.json")
263
+ with open(summary_path, "w") as f:
264
+ json.dump(all_results, f, indent=2)
265
+ print(f"\nBatch summary saved to: {summary_path}")
266
+ print("Done.")
267
+
268
+
269
+ if __name__ == "__main__":
270
+ main()
scripts/notebook_conver/cxr_longitudinal_comparison.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # compare_cxr.py - MedGemma 纵向CXR对比(仿照 c6.py)
3
+ # 功能:
4
+ # - 比较两张胸部X光片(如治疗前后),生成描述报告
5
+ # - 支持本地模型、指定GPU数量、自定义prompt、图像预处理
6
+ # - 结果保存为文本文件
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+ import json
12
+ import torch
13
+ import numpy as np
14
+ import skimage
15
+ from PIL import Image
16
+ from transformers import AutoProcessor, AutoModelForImageTextToText
17
+
18
+ # ---------- Monkey-patch for torch<2.6 ----------
19
+ import transformers.masking_utils as masking_utils
20
+ if torch.__version__ < "2.6":
21
+ _orig_causal = masking_utils.create_causal_mask
22
+ _orig_sliding = masking_utils.create_sliding_window_causal_mask
23
+
24
+ def _safe_pop(kw):
25
+ kw.pop("or_mask_function", None)
26
+ kw.pop("and_mask_function", None)
27
+
28
+ def _new_causal(*a, **kw):
29
+ _safe_pop(kw)
30
+ return _orig_causal(*a, **kw)
31
+
32
+ def _new_sliding(*a, **kw):
33
+ _safe_pop(kw)
34
+ return _orig_sliding(*a, **kw)
35
+
36
+ masking_utils.create_causal_mask = _new_causal
37
+ masking_utils.create_sliding_window_causal_mask = _new_sliding
38
+
39
+
40
+ def pad_image_to_square(image_array):
41
+ """将图像填充为正方形(与训练预处理一致)"""
42
+ image_array = skimage.util.img_as_ubyte(image_array)
43
+ if image_array.ndim < 3:
44
+ image_array = skimage.color.gray2rgb(image_array)
45
+ if image_array.shape[2] == 4:
46
+ image_array = skimage.color.rgba2rgb(image_array)
47
+
48
+ h, w = image_array.shape[:2]
49
+ if h < w:
50
+ dh = w - h
51
+ image_array = np.pad(image_array, ((dh // 2, dh - dh // 2), (0, 0), (0, 0)))
52
+ elif w < h:
53
+ dw = h - w
54
+ image_array = np.pad(image_array, ((0, 0), (dw // 2, dw - dw // 2), (0, 0)))
55
+ return image_array
56
+
57
+
58
+ def build_default_prompt():
59
+ """默认的纵向CXR对比提示词"""
60
+ return (
61
+ "Provide a comparison of these two images and include details from "
62
+ "the image which students should take note of when reading longitudinal CXR."
63
+ )
64
+
65
+
66
+ def main():
67
+ parser = argparse.ArgumentParser(
68
+ description="MedGemma 纵向胸部X光片对比"
69
+ )
70
+ parser.add_argument("--model_path", required=True, help="本地模型目录路径")
71
+ parser.add_argument("--image1", required=True, help="第一张图像路径(如治疗前)")
72
+ parser.add_argument("--image2", required=True, help="第二张图像路径(如治疗后)")
73
+ parser.add_argument("--prompt", default=None,
74
+ help="自定义对比提示词(不指定则使用默认)")
75
+ parser.add_argument("--output_dir", default="./outputs",
76
+ help="结果保存目录(默认 ./outputs)")
77
+ parser.add_argument("--num_gpus", type=int, default=None,
78
+ help="使用的GPU数量(建议在命令行用 CUDA_VISIBLE_DEVICES 控制)")
79
+ parser.add_argument("--preprocess", action="store_true", default=False,
80
+ help="是否将图像填充为正方形(默认不填充)")
81
+ args = parser.parse_args()
82
+
83
+ os.makedirs(args.output_dir, exist_ok=True)
84
+
85
+ # GPU 设置(若在脚本内设,需在 import torch 之前,此处仅作提示)
86
+ if args.num_gpus and args.num_gpus > 0:
87
+ # 实际限制应在运行脚本前通过环境变量设置,这里只打印建议
88
+ print(f"提示:建议在命令行中设置 CUDA_VISIBLE_DEVICES=0,1,... 来限制GPU。"
89
+ f"当前脚本不修改可见设备。")
90
+ else:
91
+ print(f"可用GPU数量: {torch.cuda.device_count()}")
92
+
93
+ # 加载模型(全本地)
94
+ print(f"Loading model from {args.model_path}")
95
+ processor = AutoProcessor.from_pretrained(
96
+ args.model_path,
97
+ trust_remote_code=True,
98
+ local_files_only=True
99
+ )
100
+ model = AutoModelForImageTextToText.from_pretrained(
101
+ args.model_path,
102
+ dtype=torch.bfloat16,
103
+ device_map="auto",
104
+ trust_remote_code=True,
105
+ attn_implementation="eager",
106
+ local_files_only=True
107
+ )
108
+ print("Model loaded.\n")
109
+
110
+ # 加载图像
111
+ print(f"Loading images:\n {args.image1}\n {args.image2}")
112
+ image1 = Image.open(args.image1).convert("RGB")
113
+ image2 = Image.open(args.image2).convert("RGB")
114
+ print(f"Original sizes: {image1.size}, {image2.size}")
115
+
116
+ if args.preprocess:
117
+ img1_arr = pad_image_to_square(np.array(image1))
118
+ img2_arr = pad_image_to_square(np.array(image2))
119
+ image1 = Image.fromarray(img1_arr)
120
+ image2 = Image.fromarray(img2_arr)
121
+ print(f"Preprocessed sizes: {image1.size}, {image2.size}")
122
+
123
+ # 构造提示词
124
+ prompt = args.prompt if args.prompt else build_default_prompt()
125
+ print(f"Prompt: {prompt[:100]}...")
126
+
127
+ # 构造 messages(包含两张图像)
128
+ messages = [
129
+ {
130
+ "role": "user",
131
+ "content": [
132
+ {"type": "image", "image": image1},
133
+ {"type": "image", "image": image2},
134
+ {"type": "text", "text": prompt}
135
+ ]
136
+ }
137
+ ]
138
+
139
+ # 处理输入
140
+ print("Processing inputs...")
141
+ inputs = processor.apply_chat_template(
142
+ messages,
143
+ add_generation_prompt=True,
144
+ tokenize=True,
145
+ return_tensors="pt",
146
+ return_dict=True,
147
+ )
148
+ device = model.device
149
+ inputs = {k: v.to(device) for k, v in inputs.items()}
150
+ if "pixel_values" in inputs:
151
+ print(f"Pixel values shape: {inputs['pixel_values'].shape}")
152
+
153
+ # 推理
154
+ print("Running inference...")
155
+ with torch.inference_mode():
156
+ generate_ids = model.generate(
157
+ **inputs,
158
+ max_new_tokens=600,
159
+ do_sample=False,
160
+ )
161
+
162
+ input_len = inputs["input_ids"].shape[1]
163
+ response = processor.batch_decode(
164
+ generate_ids[:, input_len:], skip_special_tokens=True
165
+ )[0]
166
+
167
+ # 输出结果
168
+ print("\n" + "=" * 60)
169
+ print("MODEL RESPONSE:")
170
+ print(response)
171
+ print("=" * 60)
172
+
173
+ # 保存为文本文件
174
+ base1 = os.path.splitext(os.path.basename(args.image1))[0]
175
+ base2 = os.path.splitext(os.path.basename(args.image2))[0]
176
+ out_txt = os.path.join(args.output_dir, f"compare_{base1}_vs_{base2}.txt")
177
+ with open(out_txt, "w", encoding="utf-8") as f:
178
+ f.write(f"Image1: {args.image1}\nImage2: {args.image2}\n")
179
+ f.write(f"Prompt: {prompt}\n\n")
180
+ f.write(f"Response:\n{response}\n")
181
+ print(f"Comparison result saved to: {out_txt}")
182
+
183
+ # 可选保存 JSON
184
+ out_json = os.path.join(args.output_dir, f"compare_{base1}_vs_{base2}.json")
185
+ result = {
186
+ "image1": args.image1,
187
+ "image2": args.image2,
188
+ "prompt": prompt,
189
+ "response": response
190
+ }
191
+ with open(out_json, "w") as f:
192
+ json.dump(result, f, indent=2)
193
+ print(f"JSON saved to: {out_json}")
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()
scripts/notebook_conver/detect_image_token.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # detect_image_token.py
2
+ import os
3
+ import sys
4
+ import argparse
5
+ from transformers import AutoProcessor
6
+ from PIL import Image
7
+
8
+ def detect_image_token():
9
+ parser = argparse.ArgumentParser(description='Detect correct image token for MedGemma model')
10
+ parser.add_argument('--model_path', type=str, required=True,
11
+ help='Path to local MedGemma model directory')
12
+ args = parser.parse_args()
13
+
14
+ model_path = args.model_path
15
+
16
+ print(f"Loading processor from: {model_path}")
17
+
18
+ try:
19
+ processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True, use_fast=False)
20
+ tokenizer = processor.tokenizer
21
+
22
+ print("Inspecting tokenizer for image-related tokens...")
23
+
24
+ # Check various attributes that might contain image token info
25
+ attrs_to_check = [
26
+ 'image_token', 'image_token_id',
27
+ 'pad_token', 'bos_token', 'eos_token', 'unk_token',
28
+ 'additional_special_tokens', 'additional_special_tokens_ids'
29
+ ]
30
+
31
+ for attr in attrs_to_check:
32
+ try:
33
+ value = getattr(tokenizer, attr, None)
34
+ if value is not None:
35
+ print(f"{attr}: {value}")
36
+ except:
37
+ print(f"{attr}: Could not access")
38
+
39
+ # Look for common image tokens in vocab
40
+ common_image_tokens = [
41
+ "<image>", "<img>", "<IMG>", "<IMAGE>",
42
+ "<|image|>", "[IMG]", "[IMAGE]",
43
+ "<start_of_image>", "<end_of_image>",
44
+ "<image_soft_token>", "<image_token>",
45
+ "<img>", "</img>", "<IMG>", "</IMG>",
46
+ "image", "IMAGE", "img", "IMG",
47
+ "<vision>", "</vision>", "<visual>", "</visual>"
48
+ ]
49
+
50
+ print("\nChecking common image tokens:")
51
+ for token in common_image_tokens:
52
+ try:
53
+ token_id = tokenizer.convert_tokens_to_ids(token)
54
+ # If the token exists in vocab, convert_tokens_to_ids returns its ID
55
+ # Otherwise it returns the unk_token_id
56
+ unk_id = tokenizer.unk_token_id
57
+ if token_id != unk_id:
58
+ print(f"Found token '{token}' with ID: {token_id}")
59
+ else:
60
+ print(f"Token '{token}' not in vocab (would use UNK token ID: {unk_id})")
61
+ except Exception as e:
62
+ print(f"Error checking token '{token}': {e}")
63
+
64
+ # Also test some sample prompts with different image tokens
65
+ print("\nTesting sample prompts with different image tokens:")
66
+ test_prompts = [
67
+ "Describe this image: <image>",
68
+ "What do you see? <image>",
69
+ "Analyze: <image>",
70
+ "Look at this: <image>",
71
+ "Explain: <|image|>",
72
+ "Look: <start_of_image><image_soft_token><end_of_image>"
73
+ ]
74
+
75
+ for i, prompt in enumerate(test_prompts[:3]): # Only test first few to avoid errors
76
+ try:
77
+ print(f"\nTest {i+1}: '{prompt}'")
78
+ inputs = processor(text=prompt, return_tensors="pt")
79
+ print(f" Input IDs shape: {inputs['input_ids'].shape}")
80
+ # Decode to see actual tokens
81
+ decoded = tokenizer.decode(inputs['input_ids'][0])
82
+ print(f" Decoded: {decoded[:200]}...")
83
+ except Exception as e:
84
+ print(f" Error: {e}")
85
+
86
+ except Exception as e:
87
+ print(f"Error loading processor: {e}")
88
+
89
+ if __name__ == "__main__":
90
+ detect_image_token()
scripts/notebook_conver/evaluate_on_medqa.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # medqa_eval.py – MedGemma on MedQA (仿照 c6.py 风格)
3
+ # 功能:
4
+ # 1. 加载本地 MedGemma 模型(纯文本推理)
5
+ # 2. 加载 MedQA 数据集(支持在线或本地 Parquet)
6
+ # 3. 逐条生成答案并提取选项
7
+ # 4. 计算准确率,保存详细结果和汇总
8
+
9
+ import os
10
+ import sys
11
+ import argparse
12
+ import json
13
+ import re
14
+ import torch
15
+ import numpy as np
16
+ from tqdm import tqdm
17
+ from transformers import AutoProcessor, AutoModelForImageTextToText
18
+ import datasets
19
+
20
+ # ---------- Monkey-patch for torch<2.6 ----------
21
+ import transformers.masking_utils as masking_utils
22
+ if torch.__version__ < "2.6":
23
+ _orig_causal = masking_utils.create_causal_mask
24
+ _orig_sliding = masking_utils.create_sliding_window_causal_mask
25
+
26
+ def _safe_pop(kw):
27
+ kw.pop("or_mask_function", None)
28
+ kw.pop("and_mask_function", None)
29
+
30
+ def _new_causal(*a, **kw):
31
+ _safe_pop(kw)
32
+ return _orig_causal(*a, **kw)
33
+
34
+ def _new_sliding(*a, **kw):
35
+ _safe_pop(kw)
36
+ return _orig_sliding(*a, **kw)
37
+
38
+ masking_utils.create_causal_mask = _new_causal
39
+ masking_utils.create_sliding_window_causal_mask = _new_sliding
40
+
41
+
42
+ def format_prompt(question: str, options: dict) -> str:
43
+ """构造选择题提示词"""
44
+ options_str = f"(A) {options['A']} (B) {options['B']} (C) {options['C']} (D) {options['D']}"
45
+ prompt = f"""Answer the given question. Think step by step.
46
+ You can directly provide the answer (A single letter), without further additions. E.g. "Final Answer: (A)".
47
+ Question: {question}
48
+ {options_str}
49
+ """
50
+ return prompt
51
+
52
+
53
+ # 答案提取正则(与原脚本一致)
54
+ ANSWER_PATTERNS = [
55
+ r'The final answer is\s\(([A-J])\)',
56
+ r'The final answer is\s\**\(([A-J])\)\**',
57
+ r'The final answer is\s\$\\boxed{([A-J])}\$',
58
+ r'Final Answer:\(([A-J])\)',
59
+ r'Final Answer:\s\(([A-J])\)',
60
+ r'Final Answer:\s\(?([A-J])',
61
+ r'Final Answer:\s*\**\(([A-J])\)\**',
62
+ r'\**Final Answer:\**\s\(([A-J])\)',
63
+ ]
64
+
65
+
66
+ #def extract_answer(text: str) -> str:
67
+ # """从模型回复中提取选项字母"""
68
+ # if not isinstance(text, str) or not text:
69
+ # return None
70
+ # for pat in ANSWER_PATTERNS:
71
+ # m = re.search(pat, text)
72
+ # if m:
73
+ # return m.group(1)
74
+ # return None
75
+
76
+ def extract_answer(text: str) -> str:
77
+ """从模型回复中提取选项字母,支持多种回退策略"""
78
+ if not isinstance(text, str) or not text:
79
+ return None
80
+
81
+ # 1. 优先用原始正则匹配标准格式
82
+ for pat in ANSWER_PATTERNS:
83
+ m = re.search(pat, text)
84
+ if m:
85
+ return m.group(1)
86
+
87
+ # 2. 回退:在 <unused95> 之后(正式回答部分)寻找最后一个单独的大写字母
88
+ if "<unused95>" in text:
89
+ post_think = text.split("<unused95>")[-1]
90
+ # 寻找形如 " (A)" 或 "(A)" 的选项
91
+ matches = re.findall(r'\(([A-J])\)', post_think)
92
+ if matches:
93
+ return matches[-1] # 通常最后一个选项是最终答案
94
+ # 如果仍未找到,尝试寻找单独的大写字母(可能模型说 "Answer: A")
95
+ m = re.search(r'\b([A-J])\b\s*$', post_think)
96
+ if m:
97
+ return m.group(1)
98
+
99
+ # 3. 全局回退:在整个回复中找最后出现的 (X) 格式
100
+ matches = re.findall(r'\(([A-J])\)', text)
101
+ if matches:
102
+ return matches[-1]
103
+
104
+ return None
105
+
106
+
107
+ def main():
108
+ parser = argparse.ArgumentParser(description="MedGemma MedQA 评测")
109
+ parser.add_argument("--model_path", required=True, help="本地 MedGemma 模型路径")
110
+ parser.add_argument("--output_dir", default="./medqa_outputs", help="输出目录(保存结果)")
111
+ parser.add_argument("--max_samples", type=int, default=None,
112
+ help="限制评测样本数(默认全部)")
113
+ parser.add_argument("--num_gpus", type=int, default=None,
114
+ help="使用的 GPU 数量(建议在命令行用 CUDA_VISIBLE_DEVICES 控制)")
115
+ parser.add_argument("--max_new_tokens", type=int, default=2048,
116
+ help="生成的最大 token 数")
117
+ parser.add_argument("--temperature", type=float, default=0.0,
118
+ help="生成温度(0 为确定性)")
119
+ parser.add_argument("--parquet_dir", type=str, default=None,
120
+ help="本地 MedQA Parquet 文件夹路径(包含 train/test/dev 等文件)")
121
+ args = parser.parse_args()
122
+
123
+ os.makedirs(args.output_dir, exist_ok=True)
124
+
125
+ # GPU 设置提示
126
+ if args.num_gpus:
127
+ print("提示:请通过环境变量限制 GPU,例如 CUDA_VISIBLE_DEVICES=0,1")
128
+ print(f"可用 GPU 数量: {torch.cuda.device_count()}")
129
+
130
+ # 加载本地模型(纯文本推理,仍使用 AutoModelForImageTextToText)
131
+ print(f"Loading model from {args.model_path} ...")
132
+ processor = AutoProcessor.from_pretrained(
133
+ args.model_path,
134
+ trust_remote_code=True,
135
+ local_files_only=True
136
+ )
137
+ model = AutoModelForImageTextToText.from_pretrained(
138
+ args.model_path,
139
+ dtype=torch.bfloat16,
140
+ device_map="auto",
141
+ trust_remote_code=True,
142
+ attn_implementation="eager",
143
+ local_files_only=True
144
+ )
145
+ print("Model loaded.\n")
146
+
147
+ # 加载 MedQA 数据集
148
+ if args.parquet_dir:
149
+ print(f"Loading MedQA dataset from local Parquet directory: {args.parquet_dir}")
150
+ data_files = {
151
+ "train": os.path.join(args.parquet_dir, "train-*.parquet"),
152
+ "test": os.path.join(args.parquet_dir, "test-*.parquet"),
153
+ "validation": os.path.join(args.parquet_dir, "dev-*.parquet"), # 验证集文件前缀为 dev
154
+ }
155
+ dataset = datasets.load_dataset("parquet", data_files=data_files)
156
+ else:
157
+ print("Loading MedQA dataset from openlifescienceai/medqa...")
158
+ dataset = datasets.load_dataset("openlifescienceai/medqa")
159
+
160
+ test_data = dataset["test"]
161
+ if args.max_samples and args.max_samples < len(test_data):
162
+ test_data = test_data.select(range(args.max_samples))
163
+ print(f"Total test samples: {len(test_data)}")
164
+
165
+ # 准备结果容器
166
+ results = []
167
+ correct = 0
168
+
169
+ # 逐条推理
170
+ for idx, item in enumerate(tqdm(test_data, desc="Inference")):
171
+ data = item["data"] # 注意数据集结构:item["data"] 包含 Question, Options, Correct Option
172
+ question = data["Question"]
173
+ options = data["Options"]
174
+ gold_answer = data["Correct Option"]
175
+
176
+ prompt = format_prompt(question, options)
177
+ messages = [
178
+ {"role": "system", "content": [{"type": "text", "text": "SYSTEM INSTRUCTION: think silently if needed."}]},
179
+ {"role": "user", "content": [{"type": "text", "text": prompt}]}
180
+ ]
181
+
182
+ # 应用聊天模板
183
+ inputs = processor.apply_chat_template(
184
+ messages,
185
+ add_generation_prompt=True,
186
+ tokenize=True,
187
+ return_tensors="pt",
188
+ return_dict=True
189
+ )
190
+ device = model.device
191
+ inputs = {k: v.to(device) for k, v in inputs.items()}
192
+
193
+ with torch.inference_mode():
194
+ gen = model.generate(
195
+ **inputs,
196
+ max_new_tokens=args.max_new_tokens,
197
+ do_sample=(args.temperature > 0),
198
+ temperature=args.temperature if args.temperature > 0 else None
199
+ )
200
+
201
+ input_len = inputs["input_ids"].shape[1]
202
+ response = processor.batch_decode(gen[:, input_len:], skip_special_tokens=True)[0]
203
+
204
+ # 提取答案
205
+ extracted = extract_answer(response)
206
+ is_correct = (extracted == gold_answer)
207
+ if is_correct:
208
+ correct += 1
209
+
210
+ # 保存单条结果
211
+ results.append({
212
+ "index": idx,
213
+ "question": question,
214
+ "options": options,
215
+ "gold_answer": gold_answer,
216
+ "model_response": response,
217
+ "extracted_answer": extracted,
218
+ "correct": is_correct
219
+ })
220
+
221
+ # 计算准确率
222
+ total = len(test_data)
223
+ accuracy = correct / total if total > 0 else 0.0
224
+ print(f"\nAccuracy: {accuracy:.4f} ({correct}/{total})")
225
+
226
+ # 保存结果
227
+ output_file = os.path.join(args.output_dir, "medqa_results.json")
228
+ with open(output_file, "w", encoding="utf-8") as f:
229
+ json.dump({"accuracy": accuracy, "total": total, "correct": correct, "details": results}, f, indent=2)
230
+ print(f"Results saved to {output_file}")
231
+
232
+ # 同时输出摘要文本
233
+ summary_file = os.path.join(args.output_dir, "summary.txt")
234
+ with open(summary_file, "w") as f:
235
+ f.write(f"Accuracy: {accuracy:.4f} ({correct}/{total})\n")
236
+ print(f"Summary saved to {summary_file}")
237
+
238
+
239
+ if __name__ == "__main__":
240
+ main()
scripts/notebook_conver/fine_tune_with_hugging_face.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # train_nct.py – MedGemma 组织分类微调 & 评估 (仿照 c6.py)
3
+ # 功能:
4
+ # 1. 加载本地 NCT-CRC-HE-100K 训练集 和 CRC-VAL-HE-7K 测试集(自动解压 zip)
5
+ # 2. 使用 QLoRA 微调 MedGemma(4-bit 量化)
6
+ # 3. 在测试集上评估准确率和 F1
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+ import json
12
+ import zipfile
13
+ import torch
14
+ import numpy as np
15
+ from typing import Any
16
+ from datasets import load_dataset, ClassLabel
17
+ from transformers import (
18
+ AutoProcessor,
19
+ AutoModelForImageTextToText,
20
+ # BitsAndBytesConfig,
21
+ pipeline,
22
+ )
23
+ from peft import LoraConfig
24
+ from trl import SFTConfig, SFTTrainer
25
+ import evaluate
26
+
27
+ # ---------- Monkey-patch for torch<2.6 ----------
28
+ import transformers.masking_utils as masking_utils
29
+ if torch.__version__ < "2.6":
30
+ _orig_causal = masking_utils.create_causal_mask
31
+ _orig_sliding = masking_utils.create_sliding_window_causal_mask
32
+
33
+ def _safe_pop(kw):
34
+ kw.pop("or_mask_function", None)
35
+ kw.pop("and_mask_function", None)
36
+
37
+ def _new_causal(*a, **kw):
38
+ _safe_pop(kw)
39
+ return _orig_causal(*a, **kw)
40
+
41
+ def _new_sliding(*a, **kw):
42
+ _safe_pop(kw)
43
+ return _orig_sliding(*a, **kw)
44
+
45
+ masking_utils.create_causal_mask = _new_causal
46
+ masking_utils.create_sliding_window_causal_mask = _new_sliding
47
+
48
+
49
+ TISSUE_CLASSES = [
50
+ "A: adipose",
51
+ "B: background",
52
+ "C: debris",
53
+ "D: lymphocytes",
54
+ "E: mucus",
55
+ "F: smooth muscle",
56
+ "G: normal colon mucosa",
57
+ "H: cancer-associated stroma",
58
+ "I: colorectal adenocarcinoma epithelium",
59
+ ]
60
+
61
+ PROMPT = f"What is the most likely tissue type shown in the histopathology image?\n" + "\n".join(TISSUE_CLASSES)
62
+
63
+
64
+ def extract_zip(zip_path: str, extract_to: str) -> str:
65
+ """解压 zip 文件,返回解压后的目录路径"""
66
+ if not os.path.exists(extract_to):
67
+ os.makedirs(extract_to, exist_ok=True)
68
+ with zipfile.ZipFile(zip_path, "r") as zf:
69
+ zf.extractall(extract_to)
70
+ return extract_to
71
+
72
+
73
+ def format_data(example: dict[str, Any]) -> dict[str, Any]:
74
+ """构造训练 messages 格式"""
75
+ example["messages"] = [
76
+ {
77
+ "role": "user",
78
+ "content": [
79
+ {"type": "image"},
80
+ {"type": "text", "text": PROMPT},
81
+ ],
82
+ },
83
+ {
84
+ "role": "assistant",
85
+ "content": [
86
+ {"type": "text", "text": TISSUE_CLASSES[example["label"]]},
87
+ ],
88
+ },
89
+ ]
90
+ return {
91
+ "image": example["image"], # 必须包含这一行
92
+ "text": "...处理好的文本..." # 必须有文本字段
93
+ }
94
+ #return example
95
+
96
+
97
+ def format_test_data(example: dict[str, Any]) -> dict[str, Any]:
98
+ """构造测试 messages 格式(无 assistant)"""
99
+ example["messages"] = [
100
+ {
101
+ "role": "user",
102
+ "content": [
103
+ {"type": "image"},
104
+ {"type": "text", "text": PROMPT},
105
+ ],
106
+ },
107
+ ]
108
+ return example
109
+
110
+
111
+ def collate_fn(examples: list[dict[str, Any]], processor):
112
+ texts = []
113
+ images = []
114
+ for example in examples:
115
+ images.append([example["image"].convert("RGB")])
116
+ texts.append(
117
+ processor.apply_chat_template(
118
+ example["messages"], add_generation_prompt=False, tokenize=False
119
+ ).strip()
120
+ )
121
+
122
+ batch = processor(text=texts, images=images, return_tensors="pt", padding=True)
123
+
124
+ labels = batch["input_ids"].clone()
125
+ # 掩码图像 token 和填充 token
126
+ image_token_id = [
127
+ processor.tokenizer.convert_tokens_to_ids(
128
+ processor.tokenizer.special_tokens_map["boi_token"]
129
+ )
130
+ ]
131
+ labels[labels == processor.tokenizer.pad_token_id] = -100
132
+ for tok_id in image_token_id:
133
+ labels[labels == tok_id] = -100
134
+ labels[labels == 262144] = -100
135
+
136
+ batch["labels"] = labels
137
+ return batch
138
+
139
+
140
+ def postprocess(prediction: list[dict[str, str]], do_full_match: bool = False) -> int:
141
+ """将模型预测转换为类别索引"""
142
+ response_text = prediction[0]["generated_text"]
143
+ if do_full_match:
144
+ try:
145
+ return LABEL_FEATURE.str2int(response_text)
146
+ except:
147
+ return -1
148
+ for label in TISSUE_CLASSES:
149
+ if label in response_text or f"({label.replace(': ', ') ')}" in response_text:
150
+ return LABEL_FEATURE.str2int(label)
151
+ return -1
152
+
153
+
154
+ def main():
155
+ parser = argparse.ArgumentParser(description="MedGemma NCT 组织分类微调与评估")
156
+ parser.add_argument("--model_path", required=True, help="本地 MedGemma 模型路径")
157
+ parser.add_argument("--train_zip", default="./NCT-CRC-HE-100K.zip", help="训练集 zip 路径")
158
+ parser.add_argument("--test_zip", default="./CRC-VAL-HE-7K.zip", help="测试集 zip 路径")
159
+ parser.add_argument("--output_dir", default="./medgemma-nct-lora", help="微调模型输出目录")
160
+ parser.add_argument("--num_train_epochs", type=int, default=1)
161
+ parser.add_argument("--learning_rate", type=float, default=2e-4)
162
+ parser.add_argument("--per_device_train_batch_size", type=int, default=4)
163
+ parser.add_argument("--max_train_samples", type=int, default=9000, help="训练样本数")
164
+ parser.add_argument("--max_val_samples", type=int, default=1000, help="验证样本数")
165
+ parser.add_argument("--max_test_samples", type=int, default=1000, help="测试样本数(评估用)")
166
+ parser.add_argument("--num_gpus", type=int, default=None, help="使用的 GPU 数量(建议在命令行用 CUDA_VISIBLE_DEVICES 控制)")
167
+ parser.add_argument("--eval_only", action="store_true", help="仅评估,跳过训练")
168
+ parser.add_argument("--skip_train", action="store_true", help="跳过训练(与 eval_only 相同)")
169
+ args = parser.parse_args()
170
+
171
+ # GPU 设置提示
172
+ if args.num_gpus:
173
+ print("提示:请通过环境变量限制 GPU,例如 CUDA_VISIBLE_DEVICES=0,1")
174
+ print(f"可用 GPU 数量: {torch.cuda.device_count()}")
175
+
176
+ # 解压数据集
177
+ train_dir = os.path.splitext(args.train_zip)[0]
178
+ test_dir = os.path.splitext(args.test_zip)[0]
179
+ if not os.path.exists(train_dir):
180
+ print(f"解压训练集 {args.train_zip} -> {train_dir}")
181
+ extract_zip(args.train_zip, train_dir)
182
+ if not os.path.exists(test_dir):
183
+ print(f"解压测试集 {args.test_zip} -> {test_dir}")
184
+ extract_zip(args.test_zip, test_dir)
185
+
186
+ # 加载数据集
187
+ print("加载训练集...")
188
+ data = load_dataset(train_dir, split="train")
189
+ data = data.train_test_split(
190
+ train_size=args.max_train_samples,
191
+ test_size=args.max_val_samples,
192
+ shuffle=True,
193
+ seed=42,
194
+ )
195
+ data["validation"] = data.pop("test")
196
+
197
+ # 格式化数据
198
+ #data = data.map(format_data, remove_columns=data["train"].column_names)
199
+ data = data.map(format_data)
200
+
201
+ # 加载模型 (QLoRA 量化)
202
+ print(f"加载模型 {args.model_path}...")
203
+ model_kwargs = dict(
204
+ attn_implementation="eager",
205
+ torch_dtype=torch.bfloat16,
206
+ device_map="auto",
207
+ local_files_only=True,
208
+ )
209
+ # model_kwargs["quantization_config"] = BitsAndBytesConfig(
210
+ # load_in_4bit=True,
211
+ # bnb_4bit_use_double_quant=True,
212
+ # bnb_4bit_quant_type="nf4",
213
+ # bnb_4bit_compute_dtype=torch.bfloat16,
214
+ # bnb_4bit_quant_storage=torch.bfloat16,
215
+ # )
216
+
217
+ model = AutoModelForImageTextToText.from_pretrained(args.model_path, **model_kwargs)
218
+ processor = AutoProcessor.from_pretrained(args.model_path, local_files_only=True)
219
+ processor.tokenizer.padding_side = "right" # 训练用右填充
220
+
221
+ # LoRA 配置
222
+ peft_config = LoraConfig(
223
+ lora_alpha=16,
224
+ lora_dropout=0.05,
225
+ r=16,
226
+ bias="none",
227
+ target_modules="all-linear",
228
+ task_type="CAUSAL_LM",
229
+ modules_to_save=["lm_head", "embed_tokens"],
230
+ )
231
+
232
+ # 训练参数
233
+ sft_args = SFTConfig(
234
+ output_dir=args.output_dir,
235
+ num_train_epochs=args.num_train_epochs,
236
+ per_device_train_batch_size=args.per_device_train_batch_size,
237
+ per_device_eval_batch_size=args.per_device_train_batch_size,
238
+ gradient_accumulation_steps=4,
239
+ gradient_checkpointing=True,
240
+ optim="adamw_torch_fused",
241
+ logging_steps=50,
242
+ save_strategy="epoch",
243
+ eval_strategy="steps",
244
+ eval_steps=50,
245
+ learning_rate=args.learning_rate,
246
+ bf16=True,
247
+ max_grad_norm=0.3,
248
+ warmup_ratio=0.03,
249
+ lr_scheduler_type="linear",
250
+ push_to_hub=False,
251
+ report_to="none",
252
+ gradient_checkpointing_kwargs={"use_reentrant": False},
253
+ dataset_kwargs={"skip_prepare_dataset": True},
254
+ remove_unused_columns=False,
255
+ label_names=["labels"],
256
+ )
257
+
258
+ if not args.eval_only and not args.skip_train:
259
+ print("开始训练...")
260
+ trainer = SFTTrainer(
261
+ model=model,
262
+ args=sft_args,
263
+ train_dataset=data["train"],
264
+ eval_dataset=data["validation"].select(range(min(200, len(data["validation"])))),
265
+ peft_config=peft_config,
266
+ processing_class=processor,
267
+ data_collator=lambda examples: collate_fn(examples, processor),
268
+ )
269
+ trainer.train()
270
+ trainer.save_model()
271
+ print(f"模型已保存到 {args.output_dir}")
272
+ del trainer
273
+ torch.cuda.empty_cache()
274
+
275
+ # ---------- 评估 ----------
276
+ print("加载测试集...")
277
+ test_data = load_dataset(test_dir, split="train")
278
+ test_data = test_data.shuffle(seed=42).select(range(args.max_test_samples))
279
+ test_data = test_data.map(format_test_data, remove_columns=test_data.column_names)
280
+
281
+ # 设置标签特征(全局变量,供 postprocess 使用)
282
+ global LABEL_FEATURE
283
+ test_data = test_data.cast_column("label", ClassLabel(names=TISSUE_CLASSES))
284
+ LABEL_FEATURE = test_data.features["label"]
285
+
286
+ # 构建评估管���
287
+ model_id = args.output_dir if (args.eval_only or args.skip_train) else args.model_path
288
+
289
+ print(f"创建评估管道,模型: {model_id}")
290
+ eval_pipe = pipeline(
291
+ "image-text-to-text",
292
+ model=model_id,
293
+ processor=processor,
294
+ torch_dtype=torch.bfloat16,
295
+ device_map="auto",
296
+ )
297
+ eval_pipe.model.generation_config.do_sample = False
298
+ eval_pipe.model.generation_config.pad_token_id = processor.tokenizer.eos_token_id
299
+ processor.tokenizer.padding_side = "left" # 推理用左填充
300
+
301
+ # 执行推理
302
+ print("运行推理...")
303
+ outputs = eval_pipe(
304
+ text=test_data["messages"],
305
+ images=test_data["image"],
306
+ max_new_tokens=40,
307
+ batch_size=64,
308
+ return_full_text=False,
309
+ )
310
+
311
+ # 后处理
312
+ do_full_match = args.eval_only or args.skip_train # 微调后模型输出可能更精确
313
+ predictions = [postprocess(out, do_full_match) for out in outputs]
314
+ references = test_data["label"]
315
+
316
+ # 计算指标
317
+ accuracy_metric = evaluate.load("accuracy")
318
+ f1_metric = evaluate.load("f1")
319
+ acc = accuracy_metric.compute(predictions=predictions, references=references)
320
+ f1 = f1_metric.compute(predictions=predictions, references=references, average="weighted")
321
+ metrics = {**acc, **f1}
322
+ print(f"评估结果: {metrics}")
323
+
324
+ # 保存结果
325
+ os.makedirs(args.output_dir, exist_ok=True)
326
+ result_file = os.path.join(args.output_dir, "eval_metrics.json")
327
+ with open(result_file, "w") as f:
328
+ json.dump(metrics, f, indent=2)
329
+ print(f"结果已保存到 {result_file}")
330
+
331
+
332
+ if __name__ == "__main__":
333
+ main()
scripts/run_cxr_anatomy.sh ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ EXAMPLE_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
6
+ REPO_ROOT=$(cd "${SCRIPT_DIR}/../../../.." && pwd)
7
+ #source "${REPO_ROOT}/env.sh"
8
+ cd $SCRIPT_DIR
9
+ pwd
10
+
11
+
12
+ ## single image
13
+ HIP_VISIBLE_DEVICES=0 \
14
+ python ./notebook_conver/cxr_anatomy_localization_with_hugging_face.py \
15
+ --model_path ${ONESCIENCE_DATASETS_DIR}/medgemma/modelscope/google/medgemma-1.5-4b-it \
16
+ --image_path "${ONESCIENCE_DATASETS_DIR}/medgemma/Chest_Xray/COVID19_Pneumonia_Normal_Chest_Xray_PA_Dataset/covid/COVID-19 (89).jpg" \
17
+ --object_name "right clavicle" \
18
+ --num_gpus 2
19
+
20
+ ## multiple images
21
+ HIP_VISIBLE_DEVICES=0 \
22
+ python ./notebook_conver/cxr_anatomy_localization_with_hugging_face.py \
23
+ --model_path ${ONESCIENCE_DATASETS_DIR}/medgemma/modelscope/google/medgemma-1.5-4b-it \
24
+ --input_dir "${ONESCIENCE_DATASETS_DIR}/medgemma/test_images" \
25
+ --object_name "right clavicle" \
26
+ --num_gpus 2
scripts/run_cxr_longitudinal_comparison.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ EXAMPLE_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
6
+ REPO_ROOT=$(cd "${SCRIPT_DIR}/../../../.." && pwd)
7
+ #source "${REPO_ROOT}/env.sh"
8
+ cd $SCRIPT_DIR
9
+ pwd
10
+
11
+ HIP_VISIBLE_DEVICES=0 \
12
+ python ./notebook_conver/cxr_longitudinal_comparison.py \
13
+ --model_path ${ONESCIENCE_DATASETS_DIR}/medgemma/modelscope/google/medgemma-1.5-4b-it \
14
+ --image1 ${ONESCIENCE_DATASETS_DIR}/medgemma/test_compare/longitudinal_cxr_before.png \
15
+ --image2 ${ONESCIENCE_DATASETS_DIR}/medgemma/test_compare/longitudinal_cxr_after.png \
16
+ --output_dir ./compare_outputs \
17
+ #--preprocess # 可选,如果图像不是正方形且需要填充
18
+
scripts/run_evaluate_on_medqa.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ EXAMPLE_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
6
+ REPO_ROOT=$(cd "${SCRIPT_DIR}/../../../.." && pwd)
7
+ #source "${REPO_ROOT}/env.sh"
8
+ cd $SCRIPT_DIR
9
+ pwd
10
+
11
+
12
+ HIP_VISIBLE_DEVICES=0 \
13
+ python ./notebook_conver/evaluate_on_medqa.py \
14
+ --model_path ${ONESCIENCE_DATASETS_DIR}/medgemma/modelscope/google/medgemma-1.5-4b-it \
15
+ --parquet_dir ${ONESCIENCE_DATASETS_DIR}/medgemma/medqa \
16
+ --output_dir ./medqa_results \
17
+ --max_samples 10 # 可选:先测试 100 条
18
+
scripts/run_fine_tune.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ EXAMPLE_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
6
+ REPO_ROOT=$(cd "${SCRIPT_DIR}/../../../.." && pwd)
7
+ #source "${REPO_ROOT}/env.sh"
8
+ cd $SCRIPT_DIR
9
+ pwd
10
+
11
+
12
+ # ==========================================
13
+ # 自动检查并升级关键依赖
14
+ # ==========================================
15
+ echo "🔍 自动检查并修复关键依赖版本..."
16
+ python -c "
17
+ import pkg_resources
18
+ pkg_resources.require('transformers==5.12.1')
19
+ " || pip install --upgrade transformers==5.12.1
20
+
21
+ echo "🔍 自动检查 accelerate 版本..."
22
+ python -c "
23
+ import pkg_resources
24
+ pkg_resources.require('accelerate>=0.29.0')
25
+ " || pip install --upgrade accelerate
26
+ # 如果升级到最新版(如 1.15.x+)出现循环导入报错,可将上面那行换成:
27
+ # " || pip install accelerate==1.0.0
28
+
29
+ # ==========================================
30
+ # 执行微调训练
31
+ # ==========================================
32
+ HIP_VISIBLE_DEVICES=0 \
33
+ python ./notebook_conver/fine_tune_with_hugging_face.py \
34
+ --model_path ${ONESCIENCE_DATASETS_DIR}/medgemma/modelscope/google/medgemma-1.5-4b-it \
35
+ --train_zip ${ONESCIENCE_DATASETS_DIR}/medgemma/nct/NCT-CRC-HE-100K.zip \
36
+ --test_zip ${ONESCIENCE_DATASETS_DIR}/medgemma/nct/CRC-VAL-HE-7K.zip \
37
+ --output_dir ./medgemma-nct-lora \
38
+ --max_train_samples 9000 --max_val_samples 1000 --max_test_samples 1000
39
+
tests/test_integration.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MedGemma 集成测试脚本
4
+ 验证 MedGemma 是否正确集成到 OneScience 中
5
+ """
6
+
7
+ import os
8
+ import sys
9
+
10
+ # 添加项目路径
11
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
12
+ sys.path.insert(0, os.path.join(project_root, "src"))
13
+
14
+ print("=" * 60)
15
+ print("MedGemma Integration Test")
16
+ print("=" * 60)
17
+
18
+ # 测试 1: 导入模块
19
+ print("\n[Test 1] Importing modules...")
20
+ try:
21
+ from models import MedGemma, VLLMModelRunner, TransformersModelRunner
22
+ from models.config import parse_configs, load_config
23
+ from onescience.datapipes.biology.adapters.medgemma_infer_adapter import MedGemmaInferAdapter
24
+ from onescience.datapipes.medical import ChatFormatter, DICOMLoader, MedicalImageProcessor
25
+ print("✓ All modules imported successfully")
26
+ except ImportError as e:
27
+ print(f"✗ Import failed: {e}")
28
+ sys.exit(1)
29
+
30
+ # 测试 2: 配置解析
31
+ print("\n[Test 2] Testing configuration parsing...")
32
+ try:
33
+ from configs.configs_base import medgemma_base_configs
34
+ from ml_collections import ConfigDict
35
+
36
+ # 创建测试配置
37
+ test_config = medgemma_base_configs.copy()
38
+ test_config.update({
39
+ "run_name": "integration_test",
40
+ "base_dir": "/tmp/medgemma_test",
41
+ "model": {
42
+ "variant": "4b",
43
+ "model_path": "/tmp/fake_model",
44
+ },
45
+ "output": {
46
+ "dump_dir": "/tmp/medgemma_output"
47
+ }
48
+ })
49
+
50
+ configs = parse_configs(test_config, fill_required_with_null=True)
51
+ print(f"✓ Configuration parsed successfully")
52
+ print(f" - Model variant: {configs.model.variant}")
53
+ print(f" - GPU memory utilization: {configs.inference.gpu_memory_utilization}")
54
+ except Exception as e:
55
+ print(f"✗ Configuration parsing failed: {e}")
56
+ sys.exit(1)
57
+
58
+ # 测试 3: 数据适配器
59
+ print("\n[Test 3] Testing data adapter...")
60
+ try:
61
+ adapter = MedGemmaInferAdapter()
62
+
63
+ # 测试文本适配
64
+ sample = {
65
+ "text": "What are the symptoms of diabetes?",
66
+ "max_tokens": 500,
67
+ }
68
+
69
+ features = adapter.adapt_features(sample)
70
+ print("✓ Data adapter working")
71
+ print(f" - Messages: {len(features['messages'])} message(s)")
72
+ print(f" - Parameters: {features['parameters']}")
73
+ except Exception as e:
74
+ print(f"✗ Data adapter failed: {e}")
75
+ sys.exit(1)
76
+
77
+ # 测试 4: Chat 格式化器
78
+ print("\n[Test 4] Testing chat formatter...")
79
+ try:
80
+ formatter = ChatFormatter()
81
+
82
+ messages = formatter.format_medical_query(
83
+ question="What causes hypertension?",
84
+ patient_info={"age": 65, "gender": "male"},
85
+ )
86
+
87
+ print("✓ Chat formatter working")
88
+ print(f" - Formatted {len(messages)} message(s)")
89
+ except Exception as e:
90
+ print(f"✗ Chat formatter failed: {e}")
91
+ sys.exit(1)
92
+
93
+ # 测试 5: 医学图像处理器
94
+ print("\n[Test 5] Testing medical image processor...")
95
+ try:
96
+ import numpy as np
97
+
98
+ processor = MedicalImageProcessor(target_size=(224, 224))
99
+
100
+ # 创建假 CT 图像
101
+ fake_ct = np.random.randint(-1000, 400, (512, 512), dtype=np.int16)
102
+
103
+ processed = processor.process_ct_image(fake_ct)
104
+
105
+ print("✓ Medical image processor working")
106
+ print(f" - Output shape: {processed.shape}")
107
+ print(f" - Output dtype: {processed.dtype}")
108
+ except Exception as e:
109
+ print(f"✗ Medical image processor failed: {e}")
110
+ import traceback
111
+ traceback.print_exc()
112
+
113
+ # 测试 6: DICOM 加载器
114
+ print("\n[Test 6] Testing DICOM loader...")
115
+ try:
116
+ loader = DICOMLoader()
117
+
118
+ if loader.pydicom_available:
119
+ print("✓ DICOM loader initialized (pydicom available)")
120
+ else:
121
+ print("⚠ DICOM loader initialized (pydicom not available)")
122
+ print(" Install pydicom for DICOM support: pip install pydicom")
123
+ except Exception as e:
124
+ print(f"✗ DICOM loader failed: {e}")
125
+
126
+ # 测试 7: 模型运行器检查
127
+ print("\n[Test 7] Checking model runners...")
128
+ try:
129
+ # 检查 vLLM
130
+ try:
131
+ import vllm
132
+ print("✓ vLLM is available")
133
+ vllm_available = True
134
+ except ImportError:
135
+ print("⚠ vLLM not available (install with: pip install vllm)")
136
+ vllm_available = False
137
+
138
+ # 检查 transformers
139
+ try:
140
+ import transformers
141
+ print("✓ Transformers is available")
142
+ except ImportError:
143
+ print("✗ Transformers not available (required)")
144
+ sys.exit(1)
145
+ except Exception as e:
146
+ print(f"✗ Model runner check failed: {e}")
147
+
148
+ # 测试 8: 推理运行器
149
+ print("\n[Test 8] Testing inference runner initialization...")
150
+ try:
151
+ from runner import MedicalInferenceRunner
152
+
153
+ # 注意:不实际初始化模型(因为需要真实模型文件)
154
+ print("✓ MedicalInferenceRunner imported successfully")
155
+ print(" (Skipping actual model initialization - requires real model files)")
156
+ except Exception as e:
157
+ print(f"✗ Inference runner import failed: {e}")
158
+ sys.exit(1)
159
+
160
+ # 总结
161
+ print("\n" + "=" * 60)
162
+ print("Integration Test Summary")
163
+ print("=" * 60)
164
+ print("✓ Core functionality: PASSED")
165
+ print("✓ Configuration system: PASSED")
166
+ print("✓ Data adapters: PASSED")
167
+ print("✓ Medical data processing: PASSED")
168
+
169
+ if vllm_available:
170
+ print("✓ Performance optimization (vLLM): AVAILABLE")
171
+ else:
172
+ print("⚠ Performance optimization (vLLM): NOT AVAILABLE")
173
+ print(" Consider installing: pip install vllm")
174
+
175
+ print("\n✅ MedGemma integration test completed successfully!")
176
+ print("\nNext steps:")
177
+ print(" 1. Download a MedGemma model")
178
+ print(" 2. Configure configs/inference_config.yaml")
179
+ print(" 3. Run: python -m runner.medical_inference_runner --config configs/inference_config.yaml --interactive")
180
+ print("=" * 60)