Safidin Tsaruev commited on
Commit
5b6b189
·
1 Parent(s): c441b00

Import ECG-R1

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LICENSE.txt +201 -0
  2. README.md +181 -13
  3. ecg_coca/__init__.py +0 -0
  4. ecg_coca/open_clip/__init__.py +14 -0
  5. ecg_coca/open_clip/augmentations/RandomMasking.py +145 -0
  6. ecg_coca/open_clip/augmentations/__init__.py +3 -0
  7. ecg_coca/open_clip/augmentations/baseline_wander.py +45 -0
  8. ecg_coca/open_clip/augmentations/cutmix.py +145 -0
  9. ecg_coca/open_clip/bpe_simple_vocab_16e6.txt.gz +3 -0
  10. ecg_coca/open_clip/coca_model.py +499 -0
  11. ecg_coca/open_clip/constants.py +10 -0
  12. ecg_coca/open_clip/convert.py +190 -0
  13. ecg_coca/open_clip/ecg_transform.py +248 -0
  14. ecg_coca/open_clip/factory.py +422 -0
  15. ecg_coca/open_clip/hf_configs.py +67 -0
  16. ecg_coca/open_clip/hf_model.py +193 -0
  17. ecg_coca/open_clip/loss.py +414 -0
  18. ecg_coca/open_clip/model.py +898 -0
  19. ecg_coca/open_clip/model_configs/coca_ViT-B-32.json +25 -0
  20. ecg_coca/open_clip/modified_resnet.py +181 -0
  21. ecg_coca/open_clip/openai.py +90 -0
  22. ecg_coca/open_clip/pos_embed.py +98 -0
  23. ecg_coca/open_clip/pretrained.py +655 -0
  24. ecg_coca/open_clip/push_to_hf_hub.py +321 -0
  25. ecg_coca/open_clip/timm_model.py +152 -0
  26. ecg_coca/open_clip/tokenizer.py +518 -0
  27. ecg_coca/open_clip/transform.py +73 -0
  28. ecg_coca/open_clip/transformer.py +1114 -0
  29. ecg_coca/open_clip/utils.py +90 -0
  30. ecg_coca/open_clip/version.py +1 -0
  31. ecg_coca/training/.gitignore +1 -0
  32. ecg_coca/training/__init__.py +1 -0
  33. ecg_coca/training/data.py +440 -0
  34. ecg_coca/training/distributed.py +139 -0
  35. ecg_coca/training/evaluate.py +73 -0
  36. ecg_coca/training/evaluation/__init__.py +2 -0
  37. ecg_coca/training/evaluation/linear_probe.py +60 -0
  38. ecg_coca/training/evaluation/metadata.py +11 -0
  39. ecg_coca/training/evaluation/metrics.py +39 -0
  40. ecg_coca/training/evaluation/zero_shot.py +78 -0
  41. ecg_coca/training/file_utils.py +84 -0
  42. ecg_coca/training/logger.py +53 -0
  43. ecg_coca/training/main.py +543 -0
  44. ecg_coca/training/params.py +430 -0
  45. ecg_coca/training/precision.py +20 -0
  46. ecg_coca/training/profiler.py +246 -0
  47. ecg_coca/training/scheduler.py +53 -0
  48. ecg_coca/training/train.py +412 -0
  49. ecg_r1/__init__.py +2 -0
  50. ecg_r1/plugin.py +210 -0
LICENSE.txt ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,13 +1,181 @@
1
- ---
2
- title: Ecg R1 Russian
3
- emoji: 📈
4
- colorFrom: gray
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
- app_file: app.py
10
- pinned: false
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ <h1> ECG-R1: Protocol-Guided and Modality-Agnostic MLLM for Reliable ECG Interpretation </h1>
4
+
5
+ <h5 align="center"> If you find this project useful, please give us a star🌟.
6
+
7
+ <h5 align="center">
8
+
9
+ <!-- <a href='https://www.lanxplanet.com/GEM-ECG/'><img src='https://img.shields.io/badge/Homepage-8A2BE2'></a> -->
10
+ <a href='https://arxiv.org/pdf/2602.04279'><img src='https://img.shields.io/badge/Paper-Arxiv-red'></a>
11
+ <a href='https://huggingface.co/PKUDigitalHealth/ECG-R1-8B-RL'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Models-blue'>
12
+ <a href='https://huggingface.co/datasets/PKUDigitalHealth/ECG-Protocol-Guided-Grounding-CoT'><img src='https://img.shields.io/badge/Dataset-Huggingface-yellow'>
13
+ <a href='http://ai.heartvoice.com.cn/ECG-R1/'><img src='https://img.shields.io/badge/Project-Page-green'></a>
14
+
15
+ <p align="center">
16
+ Jiarui Jin, Haoyu Wang, Xingliang Wu, Xiaocheng Fang, Xiang Lan, Zihan Wang<br/>
17
+ Deyun Zhang, Bo Liu, Yingying Zhang, Xian Wu, Hongyan Li, Shenda Hong
18
+ </p>
19
+
20
+
21
+ <p align="center">
22
+ <img src="pics/introduction.png" width="90%">
23
+ </p>
24
+
25
+ </h5>
26
+ </div>
27
+
28
+ ## Introduction
29
+
30
+ Electrocardiography (ECG) serves as an indispensable diagnostic tool in clinical practice, yet existing multimodal large language models (MLLMs) remain unreliable for ECG interpretation, often producing plausible but clinically incorrect analyses. To address this, we propose ECG-R1, the first reasoning MLLM designed for reliable ECG interpretation via three innovations. First, we construct the interpretation corpus using Protocol-Guided Instruction Data Generation, grounding interpretation in measurable ECG features and monograph-defined quantitative thresholds and diagnostic logic. Second, we present a modality-decoupled architecture with Interleaved Modality Dropout to improve robustness and cross-modal consistency when either the ECG signal or ECG image is missing. Third, we present Reinforcement Learning with ECG Diagnostic Evidence Rewards to strengthen evidence-grounded ECG interpretation. Additionally, we systematically evaluate the ECG interpretation capabilities of proprietary, open-source, and medical MLLMs, and provide the first quantitative evidence that severe hallucinations are widespread, suggesting that the public should not directly trust these outputs without independent verification. Code and data are publicly available at [Github](https://github.com/PKUDigitalHealth/ECG-R1) and [HuggingFace](https://huggingface.co/datasets/PKUDigitalHealth/ECG-Protocol-Guided-Grounding-CoT), and an online platform can be accessed at [ECG-R1-Online-Platform](http://ai.heartvoice.com.cn/ECG-R1/).
31
+
32
+ ## 🔥Updates
33
+
34
+ - **[May 2026]** ECG-R1 has been accepted to ICML 2026.
35
+ - **[Feb 2026]** ECG-R1 Online Platform is now available, try it in http://ai.heartvoice.com.cn/ECG-R1.
36
+ - **[Feb 2026]** [ECG-R1-8B](https://huggingface.co/PKUDigitalHealth/ECG-R1-8B-RL) and [ECG-Protocol-Guided-Grounding-CoT-30k](https://huggingface.co/datasets/PKUDigitalHealth/ECG-Protocol-Guided-Grounding-CoT) are now available.
37
+
38
+ *Stay tuned for updates!*
39
+
40
+ ## Resource
41
+
42
+
43
+ #### Paper: 📄 [Arxiv](https://arxiv.org/pdf/2602.04279)
44
+
45
+ #### Model: 🤗 [ECG-R1-8B](https://huggingface.co/PKUDigitalHealth/ECG-R1-8B-RL)
46
+
47
+ #### Data: 🤗 [ECG-Protocol-Guided-Grounding-CoT](https://huggingface.co/datasets/PKUDigitalHealth/ECG-Protocol-Guided-Grounding-CoT)
48
+
49
+ ## Setup
50
+
51
+ ```shell
52
+ git clone https://github.com/PKUDigitalHealth/ECG-R1
53
+ cd ECG-R1
54
+ conda create -n ecg_r1 python=3.10 -y
55
+ conda activate ecg_r1
56
+ pip install -e .
57
+ pip install -r requirements.txt
58
+ pip install -e ./ecg_r1 # ECG-R1 vLLM support
59
+ ```
60
+
61
+ ## Data Preparation
62
+
63
+ Please download required data:
64
+
65
+ ECG:
66
+ - [MIMIC-IV](https://physionet.org/content/mimic-iv-ecg/1.0/)
67
+ - [PTB-XL](https://physionet.org/content/ptb-xl/1.0.3/)
68
+ - [Code-15%](https://zenodo.org/records/4916206)
69
+ - [CPSC 2018](https://physionet.org/content/challenge-2020/1.0.2/training/cpsc_2018/)
70
+ - [CSN](https://physionet.org/content/ecg-arrhythmia/1.0.0/)
71
+ - [G12E](https://physionet.org/content/challenge-2020/1.0.2/training/georgia/)
72
+
73
+ Images:
74
+ - [ECG-Grounding-Images](https://huggingface.co/datasets/LANSG/ECG-Grounding) (mimic_gen)
75
+ - [ECG-Instruct](https://huggingface.co/datasets/PULSE-ECG/ECGInstruct/tree/main)
76
+ - [ECG-Bench](https://huggingface.co/datasets/PULSE-ECG/ECGBench)
77
+
78
+ After downloading all of them, organize the data as follows in `./data`,
79
+
80
+ ```
81
+ ├── ecg_timeseries
82
+ └── champan-shaoxing
83
+ └── code15
84
+ └── cpsc2018
85
+ └── ptbxl
86
+ └── georgia
87
+ └── mimic-iv
88
+ ├── ecg_images
89
+ └── cod15_v4
90
+ └── csn_aug_all_layout_papersize
91
+ └── csn_ori_layout_papersize
92
+ └── csn_part_noise_layout_papersize
93
+ └── gen_images
94
+ └── mimic_gen
95
+ └── mimic
96
+ └── mimic_v4
97
+ └── ptb-xl
98
+ ├── ecg_bench
99
+ ├── ptb-test.jsonl
100
+ ├── cpsc-test.jsonl
101
+ ├── code15-test.jsonl
102
+ ├── csn-test-no-cot.jsonl
103
+ └── g12-test-no-cot.jsonl
104
+ ├── ecg_jsons
105
+ ├── train_set
106
+ │ ├── ECG-Protocol-Guided-Grounding-CoT-30k.jsonl
107
+ │ ├── ECG-Protocol-Guided-Grounding-CoT-RL-4k.jsonl
108
+ │ └── ECGInstruct.jsonl
109
+ └── test_set
110
+ ├── ecg-grounding-test-mimiciv_full.jsonl
111
+ ├── ecg-grounding-test-mimiciv_ecg_missing25.jsonl
112
+ ├── ...
113
+ ├── ecg-grounding-test-mimiciv_image_missing25.jsonl
114
+ └── ...
115
+
116
+ ```
117
+
118
+ ## Pretrained Model Preparation
119
+
120
+ Pretrained ECG Encoder:
121
+ - [ECG-CoCa](https://drive.google.com/drive/folders/1-0lRJy7PAMZ7bflbOszwhy3_ZwfTlGYB?usp=sharing) : download ```cpt_wfep_epoch_20.pt```, place it in ```ecg_coca/open_clip/checkpoint```
122
+
123
+
124
+ ## Train
125
+
126
+ <p align="center">
127
+ <img src="pics/framework.png" width="90%">
128
+ </p>
129
+
130
+ For SFT training from scratch:
131
+ - step 1. specify paths in ```scripts/shells/sft_train.sh```
132
+ - step 2. run ```bash scripts/shells/sft_train.sh```
133
+
134
+ For RL training from scratch:
135
+ - step 1. specify paths in ```scripts/shells/rl_train_rollout.sh``` and ```scripts/shells/rl_train_dapo.sh```
136
+ - step 2. make sure you have installed ecg-r1 vllm support
137
+ - step 3. run ```scripts/shells/rl_train_rollout.sh```
138
+ - step 4. run ```scripts/shells/rl_train_dapo.sh```
139
+
140
+ ## Evaluation
141
+
142
+ For ECG Interpretation:
143
+ - step 1. generate interpretations: ```scripts/shells/inference.sh```
144
+ - step 2. generate evaluation reports: ```scripts/evaluation/grounded_interpretation/generate_volcengine_evaluation_result_batch.py```
145
+ - step 3. process evaluation reports and get scores: ```scripts/evaluation/grounded_interpretation/compute_metrics.py```
146
+
147
+ For Modality Missing:
148
+ - step 1. generate image-only ECG interpretation by setting ```--val_dataset``` to ```ecg-grounding-test-mimiciv_ecg_missing100.jsonl``` in ```scripts/shells/inference.sh```
149
+ - step 2. generate signal-only ECG interpretation by setting ```--val_dataset``` to ```ecg-grounding-test-mimiciv_image_missing100.jsonl``` in ```scripts/shells/inference.sh```
150
+ - step 3. compute metrics: ```scripts/evaluation/modality_missing/compute_score.py```
151
+
152
+ For ECGBench:
153
+ - step 1. extract images from the .parquet files provided by ECGBench for each subset: ```scripts/ecgbench/extract_img.py```
154
+ - step 2. generate the initial results by running the inference script: ```scripts/shells/ecgbench_inference.sh```
155
+ - step 3. convert the initial results into the .jsonl format required by the evaluation scripts: ```ecgbench_convert_scripts.sh```
156
+ - step 4. compute metrics: ```scripts/evaluation/ecgbench/compute_metrics.py```
157
+
158
+ *Note*
159
+ - 1. You need to specify the result paths in all evaluation scripts.
160
+ - 2. For a quick start, you can directly set --model PKUDigitalHealth/ECG-R1-8B-RL (from HuggingFace) to run the evaluation without additional local checkpoint preparation.
161
+ - 3. You may slightly adapt the evaluation generation scripts to accommodate DeepSeek APIs from different providers.
162
+
163
+
164
+ ## Citation
165
+
166
+ If you find ECG-R1 helpful for your research and applications, please cite our paper:
167
+
168
+ ```bibtex
169
+ @misc{jin2026ecgr1,
170
+ title={ECG-R1: Protocol-Guided and Modality-Agnostic MLLM for Reliable ECG Interpretation},
171
+ author={Jiarui Jin and Haoyu Wang and Xingliang Wu and Xiaocheng Fang and Xiang Lan and Zihan Wang and Deyun Zhang and Bo Liu and Yingying Zhang and Xian Wu and Hongyan Li and Shenda Hong},
172
+ year={2026},
173
+ eprint={2602.04279},
174
+ archivePrefix={arXiv},
175
+ primaryClass={cs.CL},
176
+ url={https://arxiv.org/abs/2602.04279},
177
+ }
178
+ ```
179
+
180
+ ## Acknowledgement
181
+ We thank the authors of [PULSE](https://github.com/AIMedLab/PULSE/tree/dev), [ECG-Chat](https://github.com/YubaoZhao/ECG-Chat), [GEM](https://github.com/lanxiang1017/GEM), and [Swift](https://github.com/modelscope/ms-swift) for their publicly released models, datasets, and training codes.
ecg_coca/__init__.py ADDED
File without changes
ecg_coca/open_clip/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .coca_model import CoCa
2
+ from .constants import OPENAI_DATASET_MEAN, OPENAI_DATASET_STD
3
+ from .factory import create_model, create_model_and_transforms, create_model_from_pretrained, get_tokenizer, create_loss
4
+ from .factory import list_models, add_model_config, get_model_config, load_checkpoint
5
+ from .loss import ClipLoss, DistillClipLoss, CoCaLoss
6
+ from .model import CLIP, CustomTextCLIP, CLIPTextCfg, CLIPVisionCfg, \
7
+ convert_weights_to_lp, convert_weights_to_fp16, trace_model, get_cast_dtype, get_input_dtype, \
8
+ get_model_tokenize_cfg, get_model_preprocess_cfg, set_model_preprocess_cfg
9
+ from .openai import load_openai_model, list_openai_models
10
+ from .pretrained import list_pretrained, list_pretrained_models_by_tag, list_pretrained_tags_by_model, \
11
+ get_pretrained_url, download_pretrained_from_url, is_pretrained_cfg, get_pretrained_cfg, download_pretrained
12
+ from .push_to_hf_hub import push_pretrained_to_hf_hub, push_to_hf_hub
13
+ from .tokenizer import SimpleTokenizer, tokenize, decode
14
+ from .transform import AugmentationCfg
ecg_coca/open_clip/augmentations/RandomMasking.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ """
3
+
4
+ from numbers import Real
5
+ from random import randint
6
+ from typing import Any, List, Optional, Sequence, Tuple, Union
7
+
8
+ import numpy as np
9
+ import torch
10
+ from torch import Tensor
11
+
12
+
13
+ def get_indices(prob: float, pop_size: int, scale_ratio: float = 0.1) -> List[int]:
14
+ """Get a list of indices to be selected.
15
+
16
+ A random list of indices in the range ``[0, pop_size-1]``
17
+ is generated, with the probability of each index to be selected.
18
+
19
+ Parameters
20
+ ----------
21
+ prob : float
22
+ The probability of each index to be selected.
23
+ pop_size : int
24
+ The size of the population.
25
+ scale_ratio : float, default 0.1
26
+ Scale ratio of std of the normal distribution to the population size.
27
+
28
+ Returns
29
+ -------
30
+ indices : List[int],
31
+ A list of indices.
32
+
33
+ TODO
34
+ ----
35
+ Add parameter `min_dist` so that
36
+ any 2 selected indices are at least `min_dist` apart.
37
+
38
+ """
39
+ rng = np.random.default_rng()
40
+ k = rng.normal(pop_size * prob, scale_ratio * pop_size)
41
+ # print(pop_size * prob, scale_ratio*pop_size)
42
+ k = int(round(np.clip(k, 0, pop_size)))
43
+ indices = rng.choice(list(range(pop_size)), k).tolist()
44
+ return indices
45
+
46
+
47
+ class RandomMasking(torch.nn.Module):
48
+ """Randomly mask ECGs with a probability.
49
+
50
+ Parameters
51
+ ----------
52
+ fs : int
53
+ Sampling frequency of the ECGs to be augmented.
54
+ mask_value : numbers.Real, default 0.0
55
+ Value to mask with.
56
+ mask_width : Sequence[numbers.Real], default ``[0.08, 0.18]``
57
+ Width range of the masking window, with units in seconds
58
+ kwargs : dict, optional
59
+ Additional keyword arguments.
60
+
61
+ Examples
62
+ --------
63
+ .. code-block:: python
64
+
65
+ rm = RandomMasking(fs=500, prob=0.7)
66
+ sig = torch.randn(32, 12, 5000)
67
+ critical_points = [np.arange(250, 5000 - 250, step=400) for _ in range(32)]
68
+ sig, _ = rm(sig, None, critical_points=critical_points)
69
+
70
+ """
71
+
72
+ __name__ = "RandomMasking"
73
+
74
+ def __init__(
75
+ self,
76
+ fs: int,
77
+ mask_value: Real = 0.0,
78
+ mask_width: Sequence[Real] = [0.08, 0.18],
79
+ prob: Union[Sequence[Real], Real] = [0.3, 0.15],
80
+ **kwargs: Any
81
+ ) -> None:
82
+ super().__init__()
83
+ self.fs = fs
84
+ self.prob = prob
85
+ if isinstance(self.prob, Real):
86
+ self.prob = np.array([self.prob, self.prob])
87
+ else:
88
+ self.prob = np.array(self.prob)
89
+ assert (self.prob >= 0).all() and (self.prob <= 1).all(), "Probability must be between 0 and 1"
90
+ self.mask_value = mask_value
91
+ self.mask_width = (np.array(mask_width) * self.fs).round().astype(int)
92
+
93
+ def forward(
94
+ self,
95
+ sig: Tensor,
96
+ critical_points: Optional[Sequence[Sequence[int]]] = None,
97
+ **kwargs: Any
98
+ ) -> Tuple[Tensor, ...]:
99
+ """Forward method of the RandomMasking augmenter.
100
+
101
+ Parameters
102
+ ----------
103
+ sig : torch.Tensor
104
+ Batched ECGs to be augmented, of shape ``(batch, lead, siglen)``.
105
+ critical_points : Sequence[Sequence[int]], optional
106
+ If given, random masking will be performed in windows centered at these points.
107
+ This is useful for example when one wants to randomly mask QRS complexes.
108
+ kwargs : dict, optional
109
+ Not used, but kept for consistency with other augmenters.
110
+
111
+ Returns
112
+ -------
113
+ sig : torch.Tensor
114
+ The augmented ECGs, of shape ``(batch, lead, siglen)``.
115
+ label : torch.Tensor
116
+ Label tensor of the augmented ECGs, unchanged.
117
+ extra_tensors : Sequence[torch.Tensor], optional
118
+ Unchanged extra tensors.
119
+
120
+ """
121
+ batch, lead, siglen = sig.shape
122
+
123
+ sig_mask_prob = 0.5 / self.mask_width[1]
124
+ sig_mask_scale_ratio = min(self.prob[1] / 4, 0.1) / self.mask_width[1]
125
+ mask = torch.full_like(sig, 1, dtype=sig.dtype, device=sig.device)
126
+ for batch_idx in get_indices(prob=self.prob[0], pop_size=batch):
127
+ if critical_points is not None:
128
+ indices = get_indices(prob=self.prob[1], pop_size=len(critical_points[batch_idx]))
129
+ indices = np.arange(siglen)[indices]
130
+ else:
131
+ indices = np.array(
132
+ get_indices(
133
+ prob=sig_mask_prob,
134
+ pop_size=siglen - self.mask_width[1],
135
+ scale_ratio=sig_mask_scale_ratio,
136
+ )
137
+ )
138
+ indices += self.mask_width[1] // 2
139
+ for j in indices:
140
+ masked_radius = randint(self.mask_width[0], self.mask_width[1]) // 2
141
+ mask[batch_idx, :, j - masked_radius : j + masked_radius] = self.mask_value
142
+ # print(f"batch_idx = {batch_idx}, len(indices) = {len(indices)}")
143
+ sig = sig.mul_(mask)
144
+ return sig
145
+
ecg_coca/open_clip/augmentations/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .baseline_wander import BaselineWander
2
+ from .cutmix import CutMix
3
+ from .RandomMasking import RandomMasking
ecg_coca/open_clip/augmentations/baseline_wander.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+
5
+ class BaselineWander(torch.nn.Module):
6
+ def __init__(
7
+ self,
8
+ max_amplitude=0.5,
9
+ min_amplitude=0,
10
+ p=1.0,
11
+ max_freq=0.2,
12
+ min_freq=0.01,
13
+ k=3,
14
+ fs=500,
15
+ **kwargs,
16
+ ):
17
+ super(BaselineWander, self).__init__()
18
+ self.max_amplitude = max_amplitude
19
+ self.min_amplitude = min_amplitude
20
+ self.max_freq = max_freq
21
+ self.min_freq = min_freq
22
+ self.k = k
23
+ self.freq = fs
24
+ self.p = p
25
+
26
+ def forward(self, sample):
27
+ new_sample = sample.clone()
28
+ if self.p > np.random.uniform(0, 1):
29
+ batch, csz, tsz = new_sample.shape
30
+ amp_channel = np.random.normal(1, 0.5, size=(csz, 1))
31
+ c = np.array([i for i in range(12)])
32
+ amp_general = np.random.uniform(self.min_amplitude, self.max_amplitude, size=self.k)
33
+ noise = np.zeros(shape=(1, tsz))
34
+ for k in range(self.k):
35
+ noise += self._apply_baseline_wander(tsz) * amp_general[k]
36
+ noise = (noise * amp_channel).astype(np.float32)
37
+ new_sample[:, c, :] = new_sample[:, c, :] + noise[c, :]
38
+ return new_sample.float()
39
+
40
+ def _apply_baseline_wander(self, tsz):
41
+ f = np.random.uniform(self.min_freq, self.max_freq)
42
+ t = np.linspace(0, tsz - 1, tsz)
43
+ r = np.random.uniform(0, 2 * np.pi)
44
+ noise = np.cos(2 * np.pi * f * (t / self.freq) + r)
45
+ return noise
ecg_coca/open_clip/augmentations/cutmix.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ from numbers import Real
3
+ from random import shuffle
4
+ from typing import Any, List, Optional, Sequence, Tuple
5
+
6
+ import numpy as np
7
+ import torch
8
+ from torch import Tensor
9
+ from .RandomMasking import get_indices
10
+
11
+
12
+ class CutMix(torch.nn.Module):
13
+ """CutMix augmentation.
14
+
15
+ CutMix is a data augmentation technique originally proposed in
16
+ [:footcite:ct:`yun2019cutmix`], with official implementation in
17
+ `clovaai/CutMix-PyTorch <https://github.com/clovaai/CutMix-PyTorch/blob/master/train.py>`_,
18
+ and an unofficial implementation in
19
+ `ildoonet/cutmix <https://github.com/ildoonet/cutmix/blob/master/cutmix/cutmix.py>`_.
20
+
21
+ This technique was designed for image classification tasks, but it can also be used
22
+ for ECG tasks. This technique was very successful
23
+ in CPSC2021 challenge of paroxysmal AF events detection.
24
+
25
+ Parameters
26
+ ----------
27
+ fs : int, optional
28
+ Sampling frequency, by default None.
29
+ num_mix : int, default 1
30
+ Number of mixtures.
31
+ alpha : float, default 0.5
32
+ Beta distribution parameter.
33
+ beta : float, optional
34
+ Beta distribution parameter, by default equal to `alpha`.
35
+ **kwargs : dict, optional
36
+ Additional keyword arguments.
37
+
38
+ Examples
39
+ --------
40
+ .. code-block:: python
41
+
42
+ cm = CutMix(prob=0.7)
43
+ sig = torch.randn(32, 12, 5000)
44
+ lb = torch.randint(0, 2, (32, 5000, 2), dtype=torch.float32) # 2 classes mask
45
+ sig, lb = cm(sig, lb)
46
+
47
+
48
+ .. footbibliography::
49
+
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ fs: Optional[int] = None,
55
+ num_mix: int = 1,
56
+ alpha: Real = 0.5,
57
+ beta: Optional[Real] = None,
58
+ **kwargs: Any,
59
+ ) -> None:
60
+ super().__init__()
61
+ self.fs = fs
62
+ self.prob = 1.0
63
+ self.num_mix = num_mix
64
+ assert (
65
+ isinstance(self.num_mix, int) and self.num_mix > 0
66
+ ), f"`num_mix` must be a positive integer, but got `{self.num_mix}`"
67
+ self.alpha = alpha
68
+ self.beta = beta or self.alpha
69
+ assert (
70
+ self.alpha > 0 and self.beta > 0
71
+ ), f"`alpha` and `beta` must be positive, but got `{self.alpha}` and `{self.beta}`"
72
+
73
+ def forward(
74
+ self,
75
+ sig: Tensor,
76
+ **kwargs: Any,
77
+ ) -> Tuple[Tensor, ...]:
78
+ """Forward method to perform CutMix augmentation.
79
+
80
+ Parameters
81
+ ----------
82
+ sig: torch.Tensor
83
+ Batched ECGs to be augmented, of shape ``(batch, lead, siglen)``.
84
+ **kwargs: dict, optional
85
+ Additional keyword arguments.
86
+
87
+ Returns
88
+ -------
89
+ Tuple[torch.Tensor]
90
+ Augmented tensors.
91
+
92
+ """
93
+
94
+ batch, lead, siglen = sig.shape
95
+ rng = np.random.default_rng()
96
+ for _ in range(self.num_mix):
97
+ indices = np.arange(batch, dtype=int)
98
+ # original indices chosen by probability
99
+ ori = get_indices(prob=self.prob, pop_size=batch)
100
+ # permuted indices
101
+ perm = deepcopy(ori)
102
+ shuffle(perm)
103
+ indices[ori] = perm
104
+ indices = torch.from_numpy(indices).long()
105
+
106
+ lam = torch.from_numpy(
107
+ # DEFAULTS.RNG.beta(self.alpha, self.beta, size=len(ori)),
108
+ rng.beta(self.alpha, self.beta, size=batch),
109
+ ).to(
110
+ dtype=sig.dtype, device=sig.device
111
+ ) # shape: (batch,)
112
+ intervals = _make_intervals(lam, siglen)
113
+
114
+ # perform cutmix in batch
115
+ # set values of sig enclosed by intervals to 0
116
+ mask = torch.ones_like(sig)
117
+ for i, (start, end) in enumerate(intervals):
118
+ mask[i, :, start:end] = 0
119
+ sig = sig * mask + sig[indices] * (1 - mask)
120
+
121
+ return sig
122
+
123
+
124
+ def _make_intervals(lam: Tensor, siglen: int) -> np.ndarray:
125
+ """Make intervals for cutmix.
126
+
127
+ Parameters
128
+ ----------
129
+ lam : torch.Tensor
130
+ Parameter ``lambda`` for cutmix, of shape ``(n,)``.
131
+ siglen : int
132
+ Length of the signal.
133
+
134
+ Returns
135
+ -------
136
+ numpy.ndarray
137
+ Intervals for cutmix, of shape ``(n, 2)``.
138
+
139
+ """
140
+ rng = np.random.default_rng()
141
+ _lam = (lam.numpy() * siglen).astype(int)
142
+ intervals = np.zeros((lam.shape[0], 2), dtype=int)
143
+ intervals[:, 0] = np.minimum(rng.integers(0, siglen, size=lam.shape[0]), siglen - _lam)
144
+ intervals[:, 1] = intervals[:, 0] + _lam
145
+ return intervals
ecg_coca/open_clip/bpe_simple_vocab_16e6.txt.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:924691ac288e54409236115652ad4aa250f48203de50a9e4722a6ecd48d6804a
3
+ size 1356917
ecg_coca/open_clip/coca_model.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ from torch import nn
5
+ from torch.nn import functional as F
6
+ import numpy as np
7
+ from dataclasses import dataclass
8
+
9
+ from .transformer import (
10
+ LayerNormFp32,
11
+ LayerNorm,
12
+ QuickGELU,
13
+ MultimodalTransformer,
14
+ )
15
+ from .model import CLIPTextCfg, _build_text_tower, CLIPEcgCfg, _build_ecg_tower
16
+
17
+ try:
18
+ from transformers import (
19
+ BeamSearchScorer,
20
+ LogitsProcessorList,
21
+ TopPLogitsWarper,
22
+ TopKLogitsWarper,
23
+ RepetitionPenaltyLogitsProcessor,
24
+ MinLengthLogitsProcessor,
25
+ MaxLengthCriteria,
26
+ StoppingCriteriaList
27
+ )
28
+
29
+ GENERATION_TYPES = {
30
+ "top_k": TopKLogitsWarper,
31
+ "top_p": TopPLogitsWarper,
32
+ "beam_search": "beam_search"
33
+ }
34
+ _has_transformers = True
35
+ except ImportError as e:
36
+ GENERATION_TYPES = {
37
+ "top_k": None,
38
+ "top_p": None,
39
+ "beam_search": "beam_search"
40
+ }
41
+ _has_transformers = False
42
+
43
+
44
+ @dataclass
45
+ class MultimodalCfg(CLIPTextCfg):
46
+ mlp_ratio: int = 4
47
+ dim_head: int = 64
48
+ heads: int = 8
49
+ n_queries: int = 256
50
+ attn_pooler_heads: int = 8
51
+
52
+
53
+ def _build_text_decoder_tower(
54
+ embed_dim,
55
+ multimodal_cfg,
56
+ quick_gelu: bool = False,
57
+ cast_dtype: Optional[torch.dtype] = None,
58
+ ):
59
+ multimodal_cfg = MultimodalCfg(**multimodal_cfg) if isinstance(multimodal_cfg, dict) else multimodal_cfg
60
+ act_layer = QuickGELU if quick_gelu else nn.GELU
61
+ norm_layer = (
62
+ LayerNormFp32 if cast_dtype in (torch.float16, torch.bfloat16) else LayerNorm
63
+ )
64
+
65
+ decoder = MultimodalTransformer(
66
+ context_length=multimodal_cfg.context_length,
67
+ width=multimodal_cfg.width,
68
+ heads=multimodal_cfg.heads,
69
+ layers=multimodal_cfg.layers,
70
+ ls_init_value=multimodal_cfg.ls_init_value,
71
+ output_dim=embed_dim,
72
+ act_layer=act_layer,
73
+ norm_layer=norm_layer,
74
+ )
75
+
76
+ return decoder
77
+
78
+
79
+ class CoCa(nn.Module):
80
+ def __init__(
81
+ self,
82
+ embed_dim,
83
+ multimodal_cfg: MultimodalCfg,
84
+ text_cfg: CLIPTextCfg,
85
+ ecg_cfg: CLIPEcgCfg,
86
+ quick_gelu: bool = False,
87
+ init_logit_scale: float = np.log(1 / 0.07),
88
+ init_logit_bias: Optional[float] = None,
89
+ cast_dtype: Optional[torch.dtype] = None,
90
+ pad_id: int = 0,
91
+ ):
92
+ super().__init__()
93
+ multimodal_cfg = MultimodalCfg(**multimodal_cfg) if isinstance(multimodal_cfg, dict) else multimodal_cfg
94
+ text_cfg = CLIPTextCfg(**text_cfg) if isinstance(text_cfg, dict) else text_cfg
95
+ ecg_cfg = CLIPEcgCfg(**ecg_cfg) if isinstance(ecg_cfg, dict) else ecg_cfg
96
+
97
+ self.text = _build_text_tower(
98
+ embed_dim=embed_dim,
99
+ text_cfg=text_cfg,
100
+ quick_gelu=quick_gelu,
101
+ cast_dtype=cast_dtype,
102
+ )
103
+
104
+ vocab_size = (
105
+ text_cfg.vocab_size # for hf models
106
+ if hasattr(text_cfg, "hf_model_name") and text_cfg.hf_model_name is not None
107
+ else text_cfg.vocab_size
108
+ )
109
+
110
+ self.ecg = _build_ecg_tower(
111
+ embed_dim=embed_dim,
112
+ ecg_cfg=ecg_cfg,
113
+ quick_gelu=quick_gelu,
114
+ cast_dtype=cast_dtype,
115
+ )
116
+
117
+ self.text_decoder = _build_text_decoder_tower(
118
+ vocab_size,
119
+ multimodal_cfg=multimodal_cfg,
120
+ quick_gelu=quick_gelu,
121
+ cast_dtype=cast_dtype,
122
+ )
123
+
124
+ self.logit_scale = nn.Parameter(torch.ones([]) * init_logit_scale)
125
+ if init_logit_bias is not None:
126
+ self.logit_bias = nn.Parameter(torch.ones([]) * init_logit_bias)
127
+ else:
128
+ self.logit_bias = None
129
+ self.pad_id = pad_id
130
+
131
+ self.context_length = multimodal_cfg.context_length
132
+
133
+ @torch.jit.ignore
134
+ def set_grad_checkpointing(self, enable: bool = True):
135
+ self.visual.set_grad_checkpointing(enable)
136
+ self.text.set_grad_checkpointing(enable)
137
+ self.text_decoder.set_grad_checkpointing(enable)
138
+
139
+ def _encode_ecg(self, ecgs, normalize: bool = True):
140
+ ecg_latent, tokens_embs = self.ecg(ecgs)
141
+ ecg_latent = F.normalize(ecg_latent, dim=-1) if normalize else ecg_latent
142
+ return ecg_latent, tokens_embs
143
+
144
+ def _encode_text(self, text, normalize: bool = True):
145
+ text_latent, token_emb = self.text(text)
146
+ text_latent = F.normalize(text_latent, dim=-1) if normalize else text_latent
147
+ return text_latent, token_emb
148
+
149
+ def encode_ecg(self, ecgs, normalize: bool = True):
150
+ ecg_latent, _ = self._encode_ecg(ecgs, normalize=normalize)
151
+ return ecg_latent
152
+
153
+ def encode_text(self, text, normalize: bool = True):
154
+ text_latent, _ = self._encode_text(text, normalize=normalize)
155
+ return text_latent
156
+
157
+ def lock_ecg_tower(self, unlocked_groups=0, freeze_bn_stats=False):
158
+ # lock image tower as per LiT - https://arxiv.org/abs/2111.07991
159
+ self.ecg.lock(unlocked_groups=unlocked_groups, freeze_bn_stats=freeze_bn_stats)
160
+
161
+ def lock_text_tower(self, unlocked_layers: int = 0, freeze_layer_norm: bool = True):
162
+ self.text.lock(unlocked_layers, freeze_layer_norm)
163
+
164
+ def forward(
165
+ self,
166
+ ecg,
167
+ text: Optional[torch.Tensor] = None,
168
+ ecg_latent: Optional[torch.Tensor] = None,
169
+ ecg_embs: Optional[torch.Tensor] = None,
170
+ output_labels: bool = True,
171
+ ):
172
+ if ecg_latent is None or ecg_embs is None:
173
+ ecg_latent, ecg_embs = self._encode_ecg(ecg)
174
+
175
+ if text is None:
176
+ return {"ecg_features": ecg_latent, "ecg_embs": ecg_embs}
177
+
178
+ text_latent, token_embs = self._encode_text(text)
179
+
180
+ # FIXME this isn't an ideal solution, would like to improve -RW
181
+ labels: Optional[torch.Tensor] = text[:, 1:] if output_labels else None
182
+ if output_labels:
183
+ # align text_embs and thus logits with labels for teacher-forcing caption loss
184
+ token_embs = token_embs[:, :-1]
185
+
186
+ logits = self.text_decoder(ecg_embs, token_embs)
187
+ out_dict = {
188
+ "ecg_features": ecg_latent,
189
+ "text_features": text_latent,
190
+ "logits": logits,
191
+ "logit_scale": self.logit_scale.exp()
192
+ }
193
+ if labels is not None:
194
+ out_dict["labels"] = labels
195
+ if self.logit_bias is not None:
196
+ out_dict["logit_bias"] = self.logit_bias
197
+ return out_dict
198
+
199
+ def generate(
200
+ self,
201
+ ecg,
202
+ text=None,
203
+ seq_len=30,
204
+ max_seq_len=77,
205
+ temperature=1.,
206
+ generation_type="beam_search",
207
+ top_p=0.1, # keep tokens in the 1 - top_p quantile
208
+ top_k=1, # keeps the top_k most probable tokens
209
+ pad_token_id=None,
210
+ eos_token_id=None,
211
+ sot_token_id=None,
212
+ num_beams=6,
213
+ num_beam_groups=3,
214
+ min_seq_len=5,
215
+ stopping_criteria=None,
216
+ repetition_penalty=1.0,
217
+ fixed_output_length=False # if True output.shape == (batch_size, seq_len)
218
+ ):
219
+ # taking many ideas and components from HuggingFace GenerationMixin
220
+ # https://huggingface.co/docs/transformers/main/en/main_classes/text_generation
221
+ assert _has_transformers, "Please install transformers for generate functionality. `pip install transformers`."
222
+ assert seq_len > min_seq_len, "seq_len must be larger than min_seq_len"
223
+
224
+ with torch.no_grad():
225
+ sot_token_id = 49406 if sot_token_id is None else sot_token_id
226
+ eos_token_id = 49407 if eos_token_id is None else eos_token_id
227
+ pad_token_id = self.pad_id if pad_token_id is None else pad_token_id
228
+ logit_processor = LogitsProcessorList(
229
+ [
230
+ MinLengthLogitsProcessor(min_seq_len, eos_token_id),
231
+ RepetitionPenaltyLogitsProcessor(repetition_penalty),
232
+ ]
233
+ )
234
+
235
+ if stopping_criteria is None:
236
+ stopping_criteria = [MaxLengthCriteria(max_length=seq_len)]
237
+
238
+ stopping_criteria = StoppingCriteriaList(
239
+ stopping_criteria
240
+ )
241
+
242
+ device = ecg.device
243
+
244
+ if generation_type == "beam_search":
245
+ output = self._generate_beamsearch(
246
+ ecg_inputs=ecg,
247
+ pad_token_id=pad_token_id,
248
+ eos_token_id=eos_token_id,
249
+ sot_token_id=sot_token_id,
250
+ num_beams=num_beams,
251
+ num_beam_groups=num_beam_groups,
252
+ min_seq_len=min_seq_len,
253
+ stopping_criteria=stopping_criteria,
254
+ logit_processor=logit_processor,
255
+ )
256
+ if fixed_output_length and output.shape[1] < seq_len:
257
+ pad_len = seq_len - output.shape[1]
258
+ return torch.cat((
259
+ output,
260
+ torch.ones(output.shape[0], pad_len, device=device, dtype=output.dtype) * self.pad_id
261
+ ),
262
+ dim=1
263
+ )
264
+ return output
265
+
266
+ elif generation_type == "top_p":
267
+ logit_warper = GENERATION_TYPES[generation_type](top_p)
268
+ elif generation_type == "top_k":
269
+ logit_warper = GENERATION_TYPES[generation_type](top_k)
270
+ else:
271
+ raise ValueError(
272
+ f"generation_type has to be one of "
273
+ f"{'| ' + ' | '.join(list(GENERATION_TYPES.keys())) + ' |'}."
274
+ )
275
+
276
+ ecg_latent, ecg_embs = self._encode_ecg(ecg)
277
+
278
+ if text is None:
279
+ text = torch.ones((ecg.shape[0], 1), device=device, dtype=torch.long) * sot_token_id
280
+
281
+ was_training = self.training
282
+ num_dims = len(text.shape)
283
+
284
+ if num_dims == 1:
285
+ text = text[None, :]
286
+
287
+ self.eval()
288
+ out = text
289
+
290
+ while True:
291
+ x = out[:, -max_seq_len:]
292
+ cur_len = x.shape[1]
293
+ logits = self(
294
+ ecg,
295
+ x,
296
+ ecg_latent=ecg_latent,
297
+ ecg_embs=ecg_embs,
298
+ output_labels=False,
299
+ )["logits"][:, -1]
300
+ mask = (out[:, -1] == eos_token_id) | (out[:, -1] == pad_token_id)
301
+ sample = torch.ones((out.shape[0], 1), device=device, dtype=torch.long) * pad_token_id
302
+
303
+ if mask.all():
304
+ if not fixed_output_length:
305
+ break
306
+ else:
307
+ logits = logits[~mask, :]
308
+ filtered_logits = logit_processor(x[~mask, :], logits)
309
+ filtered_logits = logit_warper(x[~mask, :], filtered_logits)
310
+ probs = F.softmax(filtered_logits / temperature, dim=-1)
311
+
312
+ if (cur_len + 1 == seq_len):
313
+ sample[~mask, :] = torch.ones((sum(~mask), 1), device=device, dtype=torch.long) * eos_token_id
314
+ else:
315
+ sample[~mask, :] = torch.multinomial(probs, 1)
316
+
317
+ out = torch.cat((out, sample), dim=-1)
318
+
319
+ cur_len += 1
320
+
321
+ if stopping_criteria(out, None):
322
+ break
323
+
324
+ if num_dims == 1:
325
+ out = out.squeeze(0)
326
+
327
+ self.train(was_training)
328
+ return out
329
+
330
+ def _generate_beamsearch(
331
+ self,
332
+ ecg_inputs,
333
+ pad_token_id=None,
334
+ eos_token_id=None,
335
+ sot_token_id=None,
336
+ num_beams=6,
337
+ num_beam_groups=3,
338
+ min_seq_len=5,
339
+ stopping_criteria=None,
340
+ logit_processor=None,
341
+ logit_warper=None,
342
+ ):
343
+ device = ecg_inputs.device
344
+ batch_size = ecg_inputs.shape[0]
345
+ ecg_inputs = torch.repeat_interleave(ecg_inputs, num_beams, dim=0)
346
+ ecg_latent, ecg_embs = self._encode_ecg(ecg_inputs)
347
+
348
+ input_ids = torch.ones((batch_size * num_beams, 1), device=device, dtype=torch.long)
349
+ input_ids = input_ids * sot_token_id
350
+ beam_scorer = BeamSearchScorer(
351
+ batch_size=batch_size,
352
+ num_beams=num_beams,
353
+ device=device,
354
+ num_beam_groups=num_beam_groups,
355
+ )
356
+ # instantiate logits processors
357
+ logits_processor = (
358
+ LogitsProcessorList([MinLengthLogitsProcessor(min_seq_len, eos_token_id=eos_token_id)])
359
+ if logit_processor is None
360
+ else logit_processor
361
+ )
362
+
363
+ num_beams = beam_scorer.num_beams
364
+ num_beam_groups = beam_scorer.num_beam_groups
365
+ num_sub_beams = num_beams // num_beam_groups
366
+ batch_size = len(beam_scorer._beam_hyps) // num_beam_groups
367
+ batch_beam_size, cur_len = input_ids.shape
368
+ beam_indices = None
369
+
370
+ if num_beams * batch_size != batch_beam_size:
371
+ raise ValueError(
372
+ f"Batch dimension of `input_ids` should be {num_beams * batch_size}, but is {batch_beam_size}."
373
+ )
374
+
375
+ beam_scores = torch.full((batch_size, num_beams), -1e9, dtype=torch.float, device=device)
376
+ # initialise score of first beam of each group with 0 and the rest with 1e-9. This ensures that the beams in
377
+ # the same group don't produce same tokens everytime.
378
+ beam_scores[:, ::num_sub_beams] = 0
379
+ beam_scores = beam_scores.view((batch_size * num_beams,))
380
+
381
+ while True:
382
+
383
+ # predicted tokens in cur_len step
384
+ current_tokens = torch.zeros(batch_size * num_beams, dtype=input_ids.dtype, device=device)
385
+
386
+ # indices which will form the beams in the next time step
387
+ reordering_indices = torch.zeros(batch_size * num_beams, dtype=torch.long, device=device)
388
+
389
+ # do one decoder step on all beams of all sentences in batch
390
+ model_inputs = prepare_inputs_for_generation(input_ids=input_ids, ecg_inputs=ecg_inputs)
391
+ outputs = self(
392
+ model_inputs['ecgs'],
393
+ model_inputs['text'],
394
+ ecg_latent=ecg_latent,
395
+ ecg_embs=ecg_embs,
396
+ output_labels=False,
397
+ )
398
+
399
+ for beam_group_idx in range(num_beam_groups):
400
+ group_start_idx = beam_group_idx * num_sub_beams
401
+ group_end_idx = min(group_start_idx + num_sub_beams, num_beams)
402
+ group_size = group_end_idx - group_start_idx
403
+
404
+ # indices of beams of current group among all sentences in batch
405
+ batch_group_indices = []
406
+
407
+ for batch_idx in range(batch_size):
408
+ batch_group_indices.extend(
409
+ [batch_idx * num_beams + idx for idx in range(group_start_idx, group_end_idx)]
410
+ )
411
+ group_input_ids = input_ids[batch_group_indices]
412
+
413
+ # select outputs of beams of currentg group only
414
+ next_token_logits = outputs['logits'][batch_group_indices, -1, :]
415
+ vocab_size = next_token_logits.shape[-1]
416
+
417
+ next_token_scores_processed = logits_processor(
418
+ group_input_ids, next_token_logits, current_tokens=current_tokens, beam_group_idx=beam_group_idx
419
+ )
420
+ next_token_scores = next_token_scores_processed + beam_scores[batch_group_indices].unsqueeze(-1)
421
+ next_token_scores = next_token_scores.expand_as(next_token_scores_processed)
422
+
423
+ # reshape for beam search
424
+ next_token_scores = next_token_scores.view(batch_size, group_size * vocab_size)
425
+
426
+ next_token_scores, next_tokens = torch.topk(
427
+ next_token_scores, 2 * group_size, dim=1, largest=True, sorted=True
428
+ )
429
+
430
+ next_indices = torch.div(next_tokens, vocab_size, rounding_mode="floor")
431
+ next_tokens = next_tokens % vocab_size
432
+
433
+ # stateless
434
+ process_beam_indices = sum(beam_indices, ()) if beam_indices is not None else None
435
+ beam_outputs = beam_scorer.process(
436
+ group_input_ids,
437
+ next_token_scores,
438
+ next_tokens,
439
+ next_indices,
440
+ pad_token_id=pad_token_id,
441
+ eos_token_id=eos_token_id,
442
+ beam_indices=process_beam_indices,
443
+ group_index=beam_group_idx,
444
+ )
445
+ beam_scores[batch_group_indices] = beam_outputs["next_beam_scores"]
446
+ beam_next_tokens = beam_outputs["next_beam_tokens"]
447
+ beam_idx = beam_outputs["next_beam_indices"]
448
+
449
+ input_ids[batch_group_indices] = group_input_ids[beam_idx]
450
+ group_input_ids = torch.cat([group_input_ids[beam_idx, :], beam_next_tokens.unsqueeze(-1)], dim=-1)
451
+ current_tokens[batch_group_indices] = group_input_ids[:, -1]
452
+
453
+ # (beam_idx // group_size) -> batch_idx
454
+ # (beam_idx % group_size) -> offset of idx inside the group
455
+ reordering_indices[batch_group_indices] = (
456
+ num_beams * torch.div(beam_idx, group_size, rounding_mode="floor") + group_start_idx + (beam_idx % group_size)
457
+ )
458
+
459
+ input_ids = torch.cat([input_ids, current_tokens.unsqueeze(-1)], dim=-1)
460
+
461
+ # increase cur_len
462
+ cur_len = cur_len + 1
463
+ if beam_scorer.is_done or stopping_criteria(input_ids, None):
464
+ break
465
+
466
+ final_beam_indices = sum(beam_indices, ()) if beam_indices is not None else None
467
+ sequence_outputs = beam_scorer.finalize(
468
+ input_ids,
469
+ beam_scores,
470
+ next_tokens,
471
+ next_indices,
472
+ pad_token_id=pad_token_id,
473
+ eos_token_id=eos_token_id,
474
+ max_length=stopping_criteria.max_length,
475
+ beam_indices=final_beam_indices,
476
+ )
477
+ return sequence_outputs['sequences']
478
+
479
+
480
+ def prepare_inputs_for_generation(input_ids, ecg_inputs, past=None, **kwargs):
481
+ if past:
482
+ input_ids = input_ids[:, -1].unsqueeze(-1)
483
+
484
+ attention_mask = kwargs.get("attention_mask", None)
485
+ position_ids = kwargs.get("position_ids", None)
486
+
487
+ if attention_mask is not None and position_ids is None:
488
+ # create position_ids on the fly for batch generation
489
+ position_ids = attention_mask.long().cumsum(-1) - 1
490
+ position_ids.masked_fill_(attention_mask == 0, 1)
491
+ else:
492
+ position_ids = None
493
+ return {
494
+ "text": input_ids,
495
+ "ecgs": ecg_inputs,
496
+ "past_key_values": past,
497
+ "position_ids": position_ids,
498
+ "attention_mask": attention_mask,
499
+ }
ecg_coca/open_clip/constants.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ OPENAI_DATASET_MEAN = (0.48145466, 0.4578275, 0.40821073)
2
+ OPENAI_DATASET_STD = (0.26862954, 0.26130258, 0.27577711)
3
+ IMAGENET_MEAN = (0.485, 0.456, 0.406)
4
+ IMAGENET_STD = (0.229, 0.224, 0.225)
5
+ INCEPTION_MEAN = (0.5, 0.5, 0.5)
6
+ INCEPTION_STD = (0.5, 0.5, 0.5)
7
+
8
+ MIMIC_IV_MEAN = [0]*12
9
+
10
+ MIMIC_IV_STD = [1] * 12
ecg_coca/open_clip/convert.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Conversion functions for 3rd part state-dicts and non-torch native checkpoint formats.
2
+ """
3
+ from typing import Union
4
+
5
+ import torch
6
+ import numpy as np
7
+
8
+ from .model import CLIP, CustomTextCLIP
9
+ from .transformer import TextTransformer, Transformer
10
+
11
+
12
+ @torch.no_grad()
13
+ def load_big_vision_weights(model: CustomTextCLIP, checkpoint_path: str):
14
+ """ Load weights from .npz checkpoints for official Google big_vision image-text models
15
+
16
+ Currently the SigLIP source models are supported and a CustomTextCLIP destination model
17
+ w/ timm image encoder.
18
+ """
19
+ from timm.layers import resample_patch_embed, resample_abs_pos_embed
20
+
21
+ def _n2p(w, t=True):
22
+ if w.ndim == 4 and w.shape[0] == w.shape[1] == w.shape[2] == 1:
23
+ w = w.flatten()
24
+ if t:
25
+ if w.ndim == 4:
26
+ w = w.transpose([3, 2, 0, 1])
27
+ elif w.ndim == 3:
28
+ w = w.transpose([2, 0, 1])
29
+ elif w.ndim == 2:
30
+ w = w.transpose([1, 0])
31
+ return torch.from_numpy(w)
32
+
33
+ w = np.load(checkpoint_path)
34
+ interpolation = 'bilinear'
35
+ antialias = False
36
+
37
+ def _convert_timm_img(module, prefix):
38
+ embed_conv_w = _n2p(w[f'{prefix}embedding/kernel'])
39
+ if embed_conv_w.shape[-2:] != module.patch_embed.proj.weight.shape[-2:]:
40
+ embed_conv_w = resample_patch_embed(
41
+ embed_conv_w,
42
+ module.patch_embed.proj.weight.shape[-2:],
43
+ interpolation=interpolation,
44
+ antialias=antialias,
45
+ verbose=True,
46
+ )
47
+ module.patch_embed.proj.weight.copy_(embed_conv_w)
48
+ module.patch_embed.proj.bias.copy_(_n2p(w[f'{prefix}embedding/bias']))
49
+
50
+ if module.cls_token is not None:
51
+ module.cls_token.copy_(_n2p(w[f'{prefix}cls'], t=False))
52
+
53
+ pos_embed_w = _n2p(w[f'{prefix}pos_embedding'], t=False)
54
+ if pos_embed_w.shape != module.pos_embed.shape:
55
+ assert False, f'{pos_embed_w.shape}, {module.pos_embed.shape}'
56
+ num_prefix_tokens = 0 if getattr(module, 'no_embed_class', False) else getattr(module, 'num_prefix_tokens', 1)
57
+ pos_embed_w = resample_abs_pos_embed( # resize pos embedding when different size from pretrained weights
58
+ pos_embed_w,
59
+ new_size=module.patch_embed.grid_size,
60
+ num_prefix_tokens=num_prefix_tokens,
61
+ interpolation=interpolation,
62
+ antialias=antialias,
63
+ verbose=True,
64
+ )
65
+ module.pos_embed.copy_(pos_embed_w)
66
+
67
+ mha_sub, b_sub, ln1_sub = (0, 0, 1)
68
+ for i, block in enumerate(module.blocks.children()):
69
+ block_prefix = f'{prefix}Transformer/encoderblock_{i}/'
70
+ mha_prefix = block_prefix + f'MultiHeadDotProductAttention_{mha_sub}/'
71
+ block.norm1.weight.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/scale']))
72
+ block.norm1.bias.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/bias']))
73
+ block.attn.qkv.weight.copy_(torch.cat([
74
+ _n2p(w[f'{mha_prefix}{n}/kernel'], t=False).flatten(1).T for n in ('query', 'key', 'value')]))
75
+ block.attn.qkv.bias.copy_(torch.cat([
76
+ _n2p(w[f'{mha_prefix}{n}/bias'], t=False).reshape(-1) for n in ('query', 'key', 'value')]))
77
+ block.attn.proj.weight.copy_(_n2p(w[f'{mha_prefix}out/kernel']).flatten(1))
78
+ block.attn.proj.bias.copy_(_n2p(w[f'{mha_prefix}out/bias']))
79
+ for r in range(2):
80
+ getattr(block.mlp, f'fc{r + 1}').weight.copy_(_n2p(w[f'{block_prefix}MlpBlock_{b_sub}/Dense_{r}/kernel']))
81
+ getattr(block.mlp, f'fc{r + 1}').bias.copy_(_n2p(w[f'{block_prefix}MlpBlock_{b_sub}/Dense_{r}/bias']))
82
+ block.norm2.weight.copy_(_n2p(w[f'{block_prefix}LayerNorm_{ln1_sub}/scale']))
83
+ block.norm2.bias.copy_(_n2p(w[f'{block_prefix}LayerNorm_{ln1_sub}/bias']))
84
+
85
+ module.norm.weight.copy_(_n2p(w[f'{prefix}Transformer/encoder_norm/scale']))
86
+ module.norm.bias.copy_(_n2p(w[f'{prefix}Transformer/encoder_norm/bias']))
87
+
88
+ if module.attn_pool is not None:
89
+ block_prefix = f'{prefix}MAPHead_0/'
90
+ mha_prefix = block_prefix + f'MultiHeadDotProductAttention_0/'
91
+ module.attn_pool.latent.copy_(_n2p(w[f'{block_prefix}probe'], t=False))
92
+ module.attn_pool.q.weight.copy_(_n2p(w[f'{mha_prefix}query/kernel'], t=False).flatten(1).T)
93
+ module.attn_pool.q.bias.copy_(_n2p(w[f'{mha_prefix}query/bias'], t=False).reshape(-1))
94
+ module.attn_pool.kv.weight.copy_(torch.cat([
95
+ _n2p(w[f'{mha_prefix}{n}/kernel'], t=False).flatten(1).T for n in ('key', 'value')]))
96
+ module.attn_pool.kv.bias.copy_(torch.cat([
97
+ _n2p(w[f'{mha_prefix}{n}/bias'], t=False).reshape(-1) for n in ('key', 'value')]))
98
+ module.attn_pool.proj.weight.copy_(_n2p(w[f'{mha_prefix}out/kernel']).flatten(1))
99
+ module.attn_pool.proj.bias.copy_(_n2p(w[f'{mha_prefix}out/bias']))
100
+ module.attn_pool.norm.weight.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/scale']))
101
+ module.attn_pool.norm.bias.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/bias']))
102
+ for r in range(2):
103
+ getattr(module.attn_pool.mlp, f'fc{r + 1}').weight.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_{r}/kernel']))
104
+ getattr(module.attn_pool.mlp, f'fc{r + 1}').bias.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_{r}/bias']))
105
+
106
+ def _convert_openclip_transformer(module: Transformer, prefix):
107
+ for i, block in enumerate(module.resblocks.children()):
108
+ block_prefix = f'{prefix}encoderblock_{i}/'
109
+ mha_prefix = block_prefix + f'MultiHeadDotProductAttention_0/'
110
+ block.ln_1.weight.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/scale']))
111
+ block.ln_1.bias.copy_(_n2p(w[f'{block_prefix}LayerNorm_0/bias']))
112
+ block.attn.in_proj_weight.copy_(torch.cat([
113
+ _n2p(w[f'{mha_prefix}{n}/kernel'], t=False).flatten(1).T for n in ('query', 'key', 'value')]))
114
+ block.attn.in_proj_bias.copy_(torch.cat([
115
+ _n2p(w[f'{mha_prefix}{n}/bias'], t=False).reshape(-1) for n in ('query', 'key', 'value')]))
116
+ block.attn.out_proj.weight.copy_(_n2p(w[f'{mha_prefix}out/kernel']).flatten(1))
117
+ block.attn.out_proj.bias.copy_(_n2p(w[f'{mha_prefix}out/bias']))
118
+ block.ln_2.weight.copy_(_n2p(w[f'{block_prefix}LayerNorm_1/scale']))
119
+ block.ln_2.bias.copy_(_n2p(w[f'{block_prefix}LayerNorm_1/bias']))
120
+ block.mlp.c_fc.weight.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_0/kernel']))
121
+ block.mlp.c_fc.bias.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_0/bias']))
122
+ block.mlp.c_proj.weight.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_1/kernel']))
123
+ block.mlp.c_proj.bias.copy_(_n2p(w[f'{block_prefix}MlpBlock_0/Dense_1/bias']))
124
+
125
+ def _convert_openclip_txt(module: TextTransformer, prefix):
126
+ module.token_embedding.weight.copy_(_n2p(w[f'{prefix}Embed_0/embedding'], t=False))
127
+ pos_embed_w = _n2p(w[f'{prefix}pos_embedding'], t=False).squeeze(0)
128
+ module.positional_embedding.copy_(pos_embed_w)
129
+ _convert_openclip_transformer(module.transformer, prefix=prefix + 'Encoder_0/')
130
+ module.ln_final.weight.copy_(_n2p(w[f'{prefix}Encoder_0/encoder_norm/scale']))
131
+ module.ln_final.bias.copy_(_n2p(w[f'{prefix}Encoder_0/encoder_norm/bias']))
132
+ module.text_projection.weight.copy_(_n2p(w[f'{prefix}head/kernel']))
133
+ module.text_projection.bias.copy_(_n2p(w[f'{prefix}head/bias']))
134
+
135
+ _convert_timm_img(model.visual.trunk, 'params/img/')
136
+ _convert_openclip_txt(model.text, 'params/txt/')
137
+ model.logit_bias.copy_(_n2p(w['params/b'])[0])
138
+ model.logit_scale.copy_(_n2p(w['params/t'])[0])
139
+
140
+
141
+ @torch.no_grad()
142
+ def convert_mobile_clip_state_dict(model: CustomTextCLIP, state_dict, fastvit = True):
143
+
144
+ def _convert_timm_img(state_dict):
145
+ if fastvit:
146
+ from timm.models.fastvit import checkpoint_filter_fn
147
+ else:
148
+ from timm.models.vision_transformer_hybrid import checkpoint_filter_fn
149
+ timm_state_dict = checkpoint_filter_fn(state_dict, model.visual.trunk)
150
+ timm_state_dict = {'visual.trunk.' + k: v for k, v in timm_state_dict.items()}
151
+ return timm_state_dict
152
+
153
+ def _convert_openclip_txt(state_dict, prefix='text_encoder.'):
154
+ text_dict = {}
155
+ for k, v in state_dict.items():
156
+ if not k.startswith(prefix):
157
+ continue
158
+ k = k.replace(prefix, '')
159
+ k = k.replace('projection_layer', 'text_projection')
160
+ k = k.replace('embedding_layer', 'token_embedding')
161
+ if k.startswith('positional_embedding.pos_embed.pos_embed'):
162
+ k = k.replace('positional_embedding.pos_embed.pos_embed', 'positional_embedding')
163
+ v = v.squeeze()
164
+ k = k.replace('final_layer_norm', 'ln_final')
165
+ k = k.replace('pre_norm_mha.0', 'ln_1')
166
+ k = k.replace('pre_norm_mha.1', 'attn')
167
+ k = k.replace('pre_norm_ffn.0', 'ln_2')
168
+ k = k.replace('pre_norm_ffn.1', 'mlp.c_fc')
169
+ k = k.replace('pre_norm_ffn.4', 'mlp.c_proj')
170
+ k = k.replace('qkv_proj.weight', 'in_proj_weight')
171
+ k = k.replace('qkv_proj.bias', 'in_proj_bias')
172
+ k = k.replace('transformer.', 'transformer.resblocks.')
173
+ text_dict['text.' + k] = v
174
+ return text_dict
175
+
176
+ image_dict = _convert_timm_img(state_dict)
177
+ text_dict = _convert_openclip_txt(state_dict)
178
+ out_dict = {**image_dict, **text_dict}
179
+ out_dict['logit_scale'] = state_dict['logit_scale']
180
+ return out_dict
181
+
182
+
183
+ def convert_state_dict(model: Union[CustomTextCLIP, CLIP], state_dict):
184
+ if 'image_encoder.model.patch_embed.0.rbr_conv.0.conv.weight' in state_dict:
185
+ # Apple MobileCLIP s1 & s2 state_dicts (s0 and b not currently supported)
186
+ state_dict = convert_mobile_clip_state_dict(model, state_dict)
187
+ if 'image_encoder.model.patch_emb.0.block.conv.weight' in state_dict:
188
+ # convert b model
189
+ state_dict = convert_mobile_clip_state_dict(model, state_dict, fastvit=False)
190
+ return state_dict
ecg_coca/open_clip/ecg_transform.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numbers
2
+ import random, torch.nn as nn
3
+ import warnings
4
+
5
+ import torch
6
+ import numpy as np
7
+ # from julius.filters import highpass_filter, lowpass_filter
8
+ from scipy.interpolate import interp1d
9
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
10
+ from dataclasses import dataclass, asdict
11
+ from .constants import MIMIC_IV_MEAN, MIMIC_IV_STD
12
+ from .augmentations import BaselineWander, RandomMasking, CutMix
13
+
14
+
15
+ class Normalize(nn.Module):
16
+ def __init__(self, mean, std):
17
+ super().__init__()
18
+ self.mean = torch.tensor(mean)
19
+ self.std = torch.tensor(std)
20
+
21
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
22
+ self.mean = self.mean.to(x.device)
23
+ self.std = self.std.to(x.device)
24
+ for i in range(len(self.mean)):
25
+ x[:, i, :] = (x[:, i, :] - self.mean[i]) / self.std[i]
26
+ return x
27
+
28
+
29
+ class Resize(nn.Module):
30
+ def __init__(self, seq_length):
31
+ super().__init__()
32
+ self.seq_length = seq_length
33
+
34
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
35
+ b, c, length = x.shape
36
+ if length < self.seq_length:
37
+ new_x = torch.zeros((b, c, self.seq_length))
38
+ new_x[:, :, 0:length] = x
39
+ elif length > self.seq_length:
40
+ new_x = x[:, :, 0:self.seq_length]
41
+ else:
42
+ new_x = x
43
+ return new_x
44
+
45
+
46
+ class Compose:
47
+ """
48
+ Data augmentation module that transforms any
49
+ given data example with a chain of augmentations.
50
+ """
51
+
52
+ def __init__(self, transforms):
53
+ self.transforms = transforms
54
+
55
+ def __call__(self, x):
56
+ return self.transform(x)
57
+
58
+ def __repr__(self):
59
+ format_string = self.__class__.__name__ + "("
60
+ for t in self.transforms:
61
+ format_string += "\n"
62
+ format_string += "\t{0}".format(t)
63
+ format_string += "\n)"
64
+ return format_string
65
+
66
+ def transform(self, x):
67
+ for t in self.transforms:
68
+ x = t(x)
69
+ return x
70
+
71
+
72
+ # BSD 3-Clause License
73
+
74
+ # Copyright (c) Soumith Chintala 2016,
75
+ # All rights reserved.
76
+
77
+ # Redistribution and use in source and binary forms, with or without
78
+ # modification, are permitted provided that the following conditions are met:
79
+
80
+ # * Redistributions of source code must retain the above copyright notice, this
81
+ # list of conditions and the following disclaimer.
82
+
83
+ # * Redistributions in binary form must reproduce the above copyright notice,
84
+ # this list of conditions and the following disclaimer in the documentation
85
+ # and/or other materials provided with the distribution.
86
+
87
+ # * Neither the name of the copyright holder nor the names of its
88
+ # contributors may be used to endorse or promote products derived from
89
+ # this software without specific prior written permission.
90
+
91
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
92
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
93
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
94
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
95
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
96
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
97
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
98
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
99
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
100
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
101
+
102
+ class RandomApply(torch.nn.Module):
103
+ """Apply randomly a list of transformations with a given probability.
104
+
105
+ Note:
106
+ In order to script the transformation, please use ``torch.nn.ModuleList``
107
+ as input instead of list/tuple of transforms as shown below:
108
+
109
+ Make sure to use only scriptable transformations,
110
+ i.e. that work with ``torch.Tensor``.
111
+
112
+ Args:
113
+ transforms (list or tuple or torch.nn.Module): list of transformations
114
+ p (float): probability
115
+ """
116
+
117
+ def __init__(self, transforms, p=0.5):
118
+ super().__init__()
119
+ self.transforms = transforms
120
+ self.p = p
121
+
122
+ def forward(self, ecg):
123
+ if self.p < torch.rand(1):
124
+ return ecg
125
+ for t in self.transforms:
126
+ ecg = t(ecg)
127
+ return ecg
128
+
129
+ def __repr__(self):
130
+ format_string = self.__class__.__name__ + "("
131
+ format_string += "\n p={}".format(self.p)
132
+ for t in self.transforms:
133
+ format_string += "\n"
134
+ format_string += " {0}".format(t)
135
+ format_string += "\n)"
136
+ return format_string
137
+
138
+
139
+ @dataclass
140
+ class PreprocessCfg:
141
+ seq_length: int = 5000
142
+ duration: int = 10
143
+ sampling_rate: int = 500
144
+ dataset: str = None
145
+ mean: Tuple[float, ...] = None
146
+ std: Tuple[float, ...] = None
147
+ resize_mode: str = 'shortest'
148
+
149
+ @property
150
+ def num_channels(self):
151
+ return 12
152
+
153
+ @property
154
+ def input_size(self):
155
+ return self.num_channels, self.seq_length
156
+
157
+
158
+ _PREPROCESS_KEYS = set(asdict(PreprocessCfg()).keys())
159
+
160
+
161
+ def merge_preprocess_dict(
162
+ base: Union[PreprocessCfg, Dict],
163
+ overlay: Dict,
164
+ ):
165
+ """ Merge overlay key-value pairs on top of base preprocess cfg or dict.
166
+ Input dicts are filtered based on PreprocessCfg fields.
167
+ """
168
+ if isinstance(base, PreprocessCfg):
169
+ base_clean = asdict(base)
170
+ else:
171
+ base_clean = {k: v for k, v in base.items() if k in _PREPROCESS_KEYS}
172
+ if overlay:
173
+ overlay_clean = {k: v for k, v in overlay.items() if k in _PREPROCESS_KEYS and v is not None}
174
+ base_clean.update(overlay_clean)
175
+ return base_clean
176
+
177
+
178
+ def merge_preprocess_kwargs(base: PreprocessCfg, **kwargs):
179
+ return merge_preprocess_dict(base, kwargs)
180
+
181
+
182
+ @dataclass
183
+ class AugmentationCfg:
184
+ scale: Tuple[float, float] = (0.9, 1.0)
185
+ ratio: Optional[Tuple[float, float]] = None
186
+ dur: Optional[Tuple[float, float]] = 10
187
+ sr: Optional[int] = 500
188
+
189
+
190
+ def _setup_size(size, error_msg):
191
+ return (12, size)
192
+
193
+
194
+ def ecg_transform(
195
+ ecg_size: Tuple[int, int],
196
+ is_train: bool,
197
+ resize_mode: Optional[str] = None,
198
+ mean: Optional[Tuple[float, ...]] = None,
199
+ std: Optional[Tuple[float, ...]] = None,
200
+ aug_cfg: Optional[Union[Dict[str, Any], AugmentationCfg]] = None,
201
+ ):
202
+ if mean is not None:
203
+ normalize = Normalize(mean=mean, std=std)
204
+ else:
205
+ normalize = Normalize(mean=MIMIC_IV_MEAN, std=MIMIC_IV_STD)
206
+ resize = Resize(seq_length=ecg_size[1])
207
+ if isinstance(aug_cfg, dict):
208
+ aug_cfg = AugmentationCfg(**aug_cfg)
209
+ else:
210
+ aug_cfg = aug_cfg or AugmentationCfg()
211
+
212
+ dur = aug_cfg.dur
213
+ sr = aug_cfg.sr
214
+ if is_train:
215
+ train_transform = [
216
+ RandomApply([BaselineWander(fs=sr), ], p=0.5),
217
+ RandomApply([CutMix(fs=sr)], p=0.5),
218
+ RandomApply([RandomMasking(fs=sr)], p=0.3)
219
+ ]
220
+ train_transform.extend([
221
+ normalize,
222
+ resize
223
+ ])
224
+ train_transform = Compose(train_transform)
225
+ return train_transform
226
+ else:
227
+ transforms = []
228
+
229
+ transforms.extend([
230
+ normalize,
231
+ resize
232
+ ])
233
+ return Compose(transforms)
234
+
235
+
236
+ def ecg_transform_v2(
237
+ cfg: PreprocessCfg,
238
+ is_train: bool,
239
+ aug_cfg: Optional[Union[Dict[str, Any], AugmentationCfg]] = None,
240
+ ):
241
+ return ecg_transform(
242
+ ecg_size=(cfg.num_channels, cfg.seq_length),
243
+ is_train=is_train,
244
+ mean=cfg.mean,
245
+ std=cfg.std,
246
+ resize_mode=cfg.resize_mode,
247
+ aug_cfg=aug_cfg,
248
+ )
ecg_coca/open_clip/factory.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import re
5
+ from copy import deepcopy
6
+ from dataclasses import asdict
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Tuple, Union
9
+
10
+ import torch
11
+
12
+ from .constants import OPENAI_DATASET_MEAN, OPENAI_DATASET_STD
13
+ from .convert import convert_state_dict
14
+ from .model import CLIP, CustomTextCLIP, convert_weights_to_lp, convert_to_custom_text_state_dict,\
15
+ resize_pos_embed, get_cast_dtype, resize_text_pos_embed, set_model_preprocess_cfg
16
+ from .coca_model import CoCa
17
+ from .loss import ClipLoss, DistillClipLoss, CoCaLoss, SigLipLoss
18
+ from .openai import load_openai_model
19
+ from .pretrained import is_pretrained_cfg, get_pretrained_cfg, download_pretrained,\
20
+ list_pretrained_tags_by_model, download_pretrained_from_hf
21
+
22
+ from .ecg_transform import ecg_transform_v2, AugmentationCfg, PreprocessCfg, merge_preprocess_dict, merge_preprocess_kwargs
23
+ from .tokenizer import HFTokenizer, SimpleTokenizer, DEFAULT_CONTEXT_LENGTH
24
+
25
+ HF_HUB_PREFIX = 'hf-hub:'
26
+ _MODEL_CONFIG_PATHS = [Path(__file__).parent / f"model_configs/"]
27
+ _MODEL_CONFIGS = {} # directory (model_name: config) of model architecture configs
28
+
29
+
30
+ def _natural_key(string_):
31
+ return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_.lower())]
32
+
33
+
34
+ def _rescan_model_configs():
35
+ global _MODEL_CONFIGS
36
+
37
+ config_ext = ('.json',)
38
+ config_files = []
39
+ for config_path in _MODEL_CONFIG_PATHS:
40
+ if config_path.is_file() and config_path.suffix in config_ext:
41
+ config_files.append(config_path)
42
+ elif config_path.is_dir():
43
+ for ext in config_ext:
44
+ config_files.extend(config_path.glob(f'*{ext}'))
45
+
46
+ for cf in config_files:
47
+ with open(cf, 'r') as f:
48
+ model_cfg = json.load(f)
49
+ if all(a in model_cfg for a in ('embed_dim', 'ecg_cfg', 'text_cfg', 'multimodal_cfg')):
50
+ _MODEL_CONFIGS[cf.stem] = model_cfg
51
+
52
+ _MODEL_CONFIGS = {k: v for k, v in sorted(_MODEL_CONFIGS.items(), key=lambda x: _natural_key(x[0]))}
53
+
54
+
55
+ _rescan_model_configs() # initial populate of model config registry
56
+
57
+
58
+ def list_models():
59
+ """ enumerate available model architectures based on config files """
60
+ return list(_MODEL_CONFIGS.keys())
61
+
62
+
63
+ def add_model_config(path):
64
+ """ add model config path or file and update registry """
65
+ if not isinstance(path, Path):
66
+ path = Path(path)
67
+ _MODEL_CONFIG_PATHS.append(path)
68
+ _rescan_model_configs()
69
+
70
+
71
+ def get_model_config(model_name):
72
+ if model_name in _MODEL_CONFIGS:
73
+ return deepcopy(_MODEL_CONFIGS[model_name])
74
+ else:
75
+ return None
76
+
77
+
78
+ def _get_hf_config(model_id, cache_dir=None):
79
+ config_path = download_pretrained_from_hf(model_id, filename='open_clip_config.json', cache_dir=cache_dir)
80
+ with open(config_path, 'r', encoding='utf-8') as f:
81
+ config = json.load(f)
82
+ return config
83
+
84
+
85
+ def get_tokenizer(
86
+ model_name: str = '',
87
+ context_length: Optional[int] = None,
88
+ **kwargs,
89
+ ):
90
+ if model_name.startswith(HF_HUB_PREFIX):
91
+ model_name = model_name[len(HF_HUB_PREFIX):]
92
+ try:
93
+ config = _get_hf_config(model_name)['model_cfg']
94
+ except Exception:
95
+ tokenizer = HFTokenizer(
96
+ model_name,
97
+ context_length=context_length or DEFAULT_CONTEXT_LENGTH,
98
+ **kwargs,
99
+ )
100
+ return tokenizer
101
+ else:
102
+ config = get_model_config(model_name)
103
+ assert config is not None, f"No valid model config found for {model_name}."
104
+
105
+ text_config = config.get('text_cfg', {})
106
+ if 'tokenizer_kwargs' in text_config:
107
+ tokenizer_kwargs = dict(text_config['tokenizer_kwargs'], **kwargs)
108
+ else:
109
+ tokenizer_kwargs = kwargs
110
+
111
+ if context_length is None:
112
+ context_length = text_config.get('context_length', DEFAULT_CONTEXT_LENGTH)
113
+
114
+ if 'hf_tokenizer_name' in text_config:
115
+ tokenizer = HFTokenizer(
116
+ text_config['hf_tokenizer_name'],
117
+ context_length=context_length,
118
+ **tokenizer_kwargs,
119
+ )
120
+ else:
121
+ tokenizer = SimpleTokenizer(
122
+ context_length=context_length,
123
+ **tokenizer_kwargs,
124
+ )
125
+
126
+ return tokenizer
127
+
128
+
129
+ def load_state_dict(checkpoint_path: str, map_location='cpu'):
130
+ checkpoint = torch.load(checkpoint_path, map_location=map_location)
131
+ if isinstance(checkpoint, dict) and 'state_dict' in checkpoint:
132
+ state_dict = checkpoint['state_dict']
133
+ elif isinstance(checkpoint, torch.jit.ScriptModule):
134
+ state_dict = checkpoint.state_dict()
135
+ for key in ["input_resolution", "context_length", "vocab_size"]:
136
+ state_dict.pop(key, None)
137
+ else:
138
+ state_dict = checkpoint
139
+ if next(iter(state_dict.items()))[0].startswith('module'):
140
+ state_dict = {k[7:]: v for k, v in state_dict.items()}
141
+ return state_dict
142
+
143
+
144
+ def load_checkpoint(
145
+ model: Union[CLIP, CustomTextCLIP],
146
+ checkpoint_path: str,
147
+ strict: bool = True,
148
+ ):
149
+ if Path(checkpoint_path).suffix in ('.npz', '.npy'):
150
+ # Separate path loading numpy big_vision (SigLIP) weights
151
+ from open_clip.convert import load_big_vision_weights
152
+ load_big_vision_weights(model, checkpoint_path)
153
+ return {}
154
+
155
+ state_dict = load_state_dict(checkpoint_path)
156
+
157
+ # Detect & convert 3rd party state_dicts -> open_clip
158
+ state_dict = convert_state_dict(model, state_dict)
159
+
160
+ # Detect old format and make compatible with new format
161
+ if 'positional_embedding' in state_dict and not hasattr(model, 'positional_embedding'):
162
+ state_dict = convert_to_custom_text_state_dict(state_dict)
163
+
164
+ # If loading a non-SigLIP model for SigLIP training. See https://github.com/mlfoundations/open_clip/issues/712
165
+ if 'logit_bias' not in state_dict and model.logit_bias is not None:
166
+ state_dict["logit_bias"] = torch.zeros_like(state_dict["logit_scale"])
167
+
168
+ # Certain text transformers no longer expect position_ids after transformers==4.31
169
+ position_id_key = 'text.transformer.embeddings.position_ids'
170
+ if position_id_key in state_dict and not hasattr(model, position_id_key):
171
+ del state_dict[position_id_key]
172
+
173
+ resize_pos_embed(state_dict, model)
174
+ resize_text_pos_embed(state_dict, model)
175
+
176
+ # Finally, load the massaged state_dict into model
177
+ incompatible_keys = model.load_state_dict(state_dict, strict=strict)
178
+ return incompatible_keys
179
+
180
+
181
+ def create_model(
182
+ model_name: str,
183
+ pretrained: Optional[str] = None,
184
+ precision: str = 'fp32',
185
+ device: Union[str, torch.device] = 'cpu',
186
+ jit: bool = False,
187
+ force_quick_gelu: bool = False,
188
+ force_custom_text: bool = False,
189
+ force_patch_dropout: Optional[float] = None,
190
+ force_ecg_size: Optional[Union[int, Tuple[int, int]]] = None,
191
+ force_preprocess_cfg: Optional[Dict[str, Any]] = None,
192
+ pretrained_hf: bool = True,
193
+ cache_dir: Optional[str] = None,
194
+ output_dict: Optional[bool] = None,
195
+ require_pretrained: bool = False,
196
+ **model_kwargs,
197
+ ):
198
+ force_preprocess_cfg = force_preprocess_cfg or {}
199
+ preprocess_cfg = asdict(PreprocessCfg())
200
+ has_hf_hub_prefix = model_name.startswith(HF_HUB_PREFIX)
201
+ if has_hf_hub_prefix:
202
+ model_id = model_name[len(HF_HUB_PREFIX):]
203
+ checkpoint_path = download_pretrained_from_hf(model_id, cache_dir=cache_dir)
204
+ config = _get_hf_config(model_id, cache_dir)
205
+ preprocess_cfg = merge_preprocess_dict(preprocess_cfg, config['preprocess_cfg'])
206
+ model_cfg = config['model_cfg']
207
+ pretrained_hf = False # override, no need to load original HF text weights
208
+ else:
209
+ model_name = model_name.replace('/', '-') # for callers using old naming with / in ViT names
210
+ checkpoint_path = "../../ecg_coca/open_clip/checkpoint/cpt_wfep_epoch_20.pt"
211
+ model_cfg = None
212
+
213
+ if isinstance(device, str):
214
+ device = torch.device(device)
215
+
216
+ if pretrained and pretrained.lower() == 'openai':
217
+ logging.info(f'Loading pretrained {model_name} from OpenAI.')
218
+ model = load_openai_model(
219
+ model_name,
220
+ precision=precision,
221
+ device=device,
222
+ cache_dir=cache_dir,
223
+ )
224
+ else:
225
+ model_cfg = model_cfg or get_model_config(model_name)
226
+ if model_cfg is not None:
227
+ logging.info(f'Loaded {model_name} model config.')
228
+ else:
229
+ logging.error(f'Model config for {model_name} not found; available models {list_models()}.')
230
+ raise RuntimeError(f'Model config for {model_name} not found.')
231
+
232
+ if force_quick_gelu:
233
+ # override for use of QuickGELU on non-OpenAI transformer models
234
+ model_cfg["quick_gelu"] = True
235
+
236
+ if force_patch_dropout is not None:
237
+ # override the default patch dropout value
238
+ model_cfg["ecg_cfg"]["patch_dropout"] = force_patch_dropout
239
+
240
+ if force_ecg_size is not None:
241
+ # override model config's image size
242
+ model_cfg["ecg_cfg"]["ecg_size"] = force_ecg_size
243
+
244
+ # cast_dtype set for fp16 and bf16 (manual mixed-precision), not set for 'amp' or 'pure' modes
245
+ cast_dtype = get_cast_dtype(precision)
246
+ is_hf_model = 'hf_model_name' in model_cfg.get('text_cfg', {})
247
+ if is_hf_model:
248
+ # load pretrained weights for HF text model IFF no CLIP weights being loaded
249
+ model_cfg['text_cfg']['hf_model_pretrained'] = pretrained_hf and not pretrained
250
+ custom_text = model_cfg.pop('custom_text', False) or force_custom_text or is_hf_model
251
+
252
+ model_cfg = dict(model_cfg, **model_kwargs) # merge cfg dict w/ kwargs (kwargs overrides cfg)
253
+
254
+ assert "multimodal_cfg" in model_cfg
255
+ model = CoCa(**model_cfg, cast_dtype=cast_dtype)
256
+
257
+ if precision in ("fp16", "bf16"):
258
+ dtype = torch.float16 if 'fp16' in precision else torch.bfloat16
259
+
260
+ model.to(device=device)
261
+ convert_weights_to_lp(model, dtype=dtype)
262
+ elif precision in ("pure_fp16", "pure_bf16"):
263
+ dtype = torch.float16 if 'fp16' in precision else torch.bfloat16
264
+ model.to(device=device, dtype=dtype)
265
+ else:
266
+ pass
267
+ # model.to(device=device)
268
+ # model.to_empty(device=device)
269
+
270
+ pretrained_loaded = False
271
+ if pretrained:
272
+ checkpoint_path = ''
273
+ pretrained_cfg = get_pretrained_cfg(model_name, pretrained)
274
+ if pretrained_cfg:
275
+ checkpoint_path = download_pretrained(pretrained_cfg, cache_dir=cache_dir)
276
+ preprocess_cfg = merge_preprocess_dict(preprocess_cfg, pretrained_cfg)
277
+ elif os.path.exists(pretrained):
278
+ checkpoint_path = pretrained
279
+
280
+ if checkpoint_path:
281
+ logging.info(f'Loading pretrained {model_name} weights ({pretrained}).')
282
+ load_checkpoint(model, checkpoint_path)
283
+ else:
284
+ error_str = (
285
+ f'Pretrained weights ({pretrained}) not found for model {model_name}.'
286
+ f' Available pretrained tags ({list_pretrained_tags_by_model(model_name)}.')
287
+ logging.warning(error_str)
288
+ raise RuntimeError(error_str)
289
+ pretrained_loaded = True
290
+ elif has_hf_hub_prefix:
291
+ logging.info(f'Loading pretrained {model_name} weights ({checkpoint_path}).')
292
+ load_checkpoint(model, checkpoint_path)
293
+ pretrained_loaded = True
294
+
295
+ if require_pretrained and not pretrained_loaded:
296
+ # callers of create_model_from_pretrained always expect pretrained weights
297
+ raise RuntimeError(
298
+ f'Pretrained weights were required for (model: {model_name}, pretrained: {pretrained}) but not loaded.')
299
+
300
+ if output_dict and hasattr(model, "output_dict"):
301
+ model.output_dict = True
302
+
303
+ if jit:
304
+ model = torch.jit.script(model)
305
+
306
+ # set image preprocessing configuration in model attributes for convenience
307
+ # if getattr(model.ecg, 'seq_length', None) is not None:
308
+ # # use image_size set on model creation (via config or force_image_size arg)
309
+ # force_preprocess_cfg['size'] = model.visual.image_size
310
+ set_model_preprocess_cfg(model, merge_preprocess_dict(preprocess_cfg, force_preprocess_cfg))
311
+
312
+ return model
313
+
314
+
315
+ def create_loss(args):
316
+ return CoCaLoss(
317
+ caption_loss_weight=args.coca_caption_loss_weight,
318
+ clip_loss_weight=args.coca_contrastive_loss_weight,
319
+ local_loss=args.local_loss,
320
+ gather_with_grad=args.gather_with_grad,
321
+ cache_labels=True,
322
+ rank=args.rank,
323
+ world_size=args.world_size,
324
+ use_horovod=args.horovod,
325
+ )
326
+
327
+
328
+ def create_model_and_transforms(
329
+ model_name: str,
330
+ pretrained: Optional[str] = None,
331
+ precision: str = 'fp32',
332
+ device: Union[str, torch.device] = 'cpu',
333
+ jit: bool = False,
334
+ force_quick_gelu: bool = False,
335
+ force_custom_text: bool = False,
336
+ force_patch_dropout: Optional[float] = None,
337
+ force_ecg_size: Optional[Union[int, Tuple[int, int]]] = None,
338
+ ecg_mean: Optional[Tuple[float, ...]] = None,
339
+ ecg_std: Optional[Tuple[float, ...]] = None,
340
+ aug_cfg: Optional[Union[Dict[str, Any], AugmentationCfg]] = None,
341
+ pretrained_hf: bool = True,
342
+ cache_dir: Optional[str] = None,
343
+ output_dict: Optional[bool] = None,
344
+ **model_kwargs,
345
+ ):
346
+ force_preprocess_cfg = merge_preprocess_kwargs(
347
+ {}, mean=ecg_mean, std=ecg_std)
348
+
349
+ model = create_model(
350
+ model_name,
351
+ pretrained,
352
+ precision=precision,
353
+ device=device,
354
+ jit=jit,
355
+ force_quick_gelu=force_quick_gelu,
356
+ force_custom_text=force_custom_text,
357
+ force_patch_dropout=force_patch_dropout,
358
+ force_ecg_size=force_ecg_size,
359
+ force_preprocess_cfg=force_preprocess_cfg,
360
+ pretrained_hf=pretrained_hf,
361
+ cache_dir=cache_dir,
362
+ output_dict=output_dict,
363
+ **model_kwargs,
364
+ )
365
+
366
+ pp_cfg = PreprocessCfg(**model.ecg.preprocess_cfg)
367
+
368
+ preprocess_train = ecg_transform_v2(
369
+ pp_cfg,
370
+ is_train=True,
371
+ aug_cfg=aug_cfg,
372
+ )
373
+ preprocess_val = ecg_transform_v2(
374
+ pp_cfg,
375
+ is_train=False,
376
+ )
377
+
378
+ return model, preprocess_train, preprocess_val
379
+
380
+
381
+ def create_model_from_pretrained(
382
+ model_name: str,
383
+ pretrained: Optional[str] = None,
384
+ precision: str = 'fp32',
385
+ device: Union[str, torch.device] = 'cpu',
386
+ jit: bool = False,
387
+ force_quick_gelu: bool = False,
388
+ force_custom_text: bool = False,
389
+ force_ecg_size: Optional[Union[int, Tuple[int, int]]] = None,
390
+ ecg_mean: Optional[Tuple[float, ...]] = None,
391
+ ecg_std: Optional[Tuple[float, ...]] = None,
392
+ return_transform: bool = True,
393
+ cache_dir: Optional[str] = None,
394
+ **model_kwargs,
395
+ ):
396
+ force_preprocess_cfg = merge_preprocess_kwargs(
397
+ {}, mean=ecg_mean, std=ecg_std)
398
+
399
+ model = create_model(
400
+ model_name,
401
+ pretrained,
402
+ precision=precision,
403
+ device=device,
404
+ jit=jit,
405
+ force_quick_gelu=force_quick_gelu,
406
+ force_custom_text=force_custom_text,
407
+ force_ecg_size=force_ecg_size,
408
+ force_preprocess_cfg=force_preprocess_cfg,
409
+ cache_dir=cache_dir,
410
+ require_pretrained=True,
411
+ **model_kwargs,
412
+ )
413
+
414
+ if not return_transform:
415
+ return model
416
+
417
+ preprocess = ecg_transform_v2(
418
+ PreprocessCfg(**model.ecg.preprocess_cfg),
419
+ is_train=False,
420
+ )
421
+
422
+ return model, preprocess
ecg_coca/open_clip/hf_configs.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HF architecture dict:
2
+ arch_dict = {
3
+ # https://huggingface.co/docs/transformers/model_doc/roberta#roberta
4
+ "roberta": {
5
+ "config_names": {
6
+ "context_length": "max_position_embeddings",
7
+ "vocab_size": "vocab_size",
8
+ "width": "hidden_size",
9
+ "heads": "num_attention_heads",
10
+ "layers": "num_hidden_layers",
11
+ "layer_attr": "layer",
12
+ "token_embeddings_attr": "embeddings"
13
+ },
14
+ "pooler": "mean_pooler",
15
+ },
16
+ # https://huggingface.co/docs/transformers/model_doc/xlm-roberta#transformers.XLMRobertaConfig
17
+ "xlm-roberta": {
18
+ "config_names": {
19
+ "context_length": "max_position_embeddings",
20
+ "vocab_size": "vocab_size",
21
+ "width": "hidden_size",
22
+ "heads": "num_attention_heads",
23
+ "layers": "num_hidden_layers",
24
+ "layer_attr": "layer",
25
+ "token_embeddings_attr": "embeddings"
26
+ },
27
+ "pooler": "mean_pooler",
28
+ },
29
+ # https://huggingface.co/docs/transformers/model_doc/mt5#mt5
30
+ "mt5": {
31
+ "config_names": {
32
+ # unlimited seqlen
33
+ # https://github.com/google-research/text-to-text-transfer-transformer/issues/273
34
+ # https://github.com/huggingface/transformers/blob/v4.24.0/src/transformers/models/t5/modeling_t5.py#L374
35
+ "context_length": "",
36
+ "vocab_size": "vocab_size",
37
+ "width": "d_model",
38
+ "heads": "num_heads",
39
+ "layers": "num_layers",
40
+ "layer_attr": "block",
41
+ "token_embeddings_attr": "embed_tokens"
42
+ },
43
+ "pooler": "mean_pooler",
44
+ },
45
+ # https://huggingface.co/docs/transformers/model_doc/bert
46
+ "bert": {
47
+ "config_names": {
48
+ "context_length": "max_position_embeddings",
49
+ "vocab_size": "vocab_size",
50
+ "width": "hidden_size",
51
+ "heads": "num_attention_heads",
52
+ "layers": "num_hidden_layers",
53
+ },
54
+ "pooler": "cls_pooler",
55
+ },
56
+ # https://huggingface.co/docs/transformers/model_doc/m2m_100
57
+ "m2m_100": {
58
+ "config_names": {
59
+ "context_length": "max_position_embeddings",
60
+ "vocab_size": "vocab_size",
61
+ "width": "d_model",
62
+ "heads": "encoder_attention_heads",
63
+ "layers": "encoder_layers",
64
+ },
65
+ "pooler": "cls_pooler",
66
+ },
67
+ }
ecg_coca/open_clip/hf_model.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ huggingface model adapter
2
+
3
+ Wraps HuggingFace transformers (https://github.com/huggingface/transformers) models for use as a text tower in CLIP model.
4
+ """
5
+ import re
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch import TensorType
10
+
11
+ try:
12
+ import transformers
13
+ from transformers import AutoModel, AutoTokenizer, AutoConfig, PretrainedConfig
14
+ from transformers.modeling_outputs import BaseModelOutput, BaseModelOutputWithPooling, \
15
+ BaseModelOutputWithPoolingAndCrossAttentions
16
+ except ImportError as e:
17
+ transformers = None
18
+
19
+
20
+ class BaseModelOutput:
21
+ pass
22
+
23
+
24
+ class PretrainedConfig:
25
+ pass
26
+
27
+ from .hf_configs import arch_dict
28
+
29
+
30
+ # utils
31
+ def _camel2snake(s):
32
+ return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()
33
+
34
+
35
+ # TODO: ?last - for gpt-like models
36
+ _POOLERS = {}
37
+
38
+
39
+ def register_pooler(cls):
40
+ """Decorator registering pooler class"""
41
+ _POOLERS[_camel2snake(cls.__name__)] = cls
42
+ return cls
43
+
44
+
45
+ @register_pooler
46
+ class MeanPooler(nn.Module):
47
+ """Mean pooling"""
48
+
49
+ def forward(self, x: BaseModelOutput, attention_mask: TensorType):
50
+ masked_output = x.last_hidden_state * attention_mask.unsqueeze(-1)
51
+ return masked_output.sum(dim=1) / attention_mask.sum(-1, keepdim=True)
52
+
53
+
54
+ @register_pooler
55
+ class MaxPooler(nn.Module):
56
+ """Max pooling"""
57
+
58
+ def forward(self, x: BaseModelOutput, attention_mask: TensorType):
59
+ masked_output = x.last_hidden_state.masked_fill(attention_mask.unsqueeze(-1), -torch.inf)
60
+ return masked_output.max(1).values
61
+
62
+
63
+ @register_pooler
64
+ class ClsPooler(nn.Module):
65
+ """CLS token pooling"""
66
+
67
+ def __init__(self, use_pooler_output=True):
68
+ super().__init__()
69
+ self.cls_token_position = 0
70
+ self.use_pooler_output = use_pooler_output
71
+
72
+ def forward(self, x: BaseModelOutput, attention_mask: TensorType):
73
+ if (self.use_pooler_output and
74
+ isinstance(x, (BaseModelOutputWithPooling, BaseModelOutputWithPoolingAndCrossAttentions)) and
75
+ (x.pooler_output is not None)
76
+ ):
77
+ return x.pooler_output
78
+
79
+ return x.last_hidden_state[:, self.cls_token_position, :]
80
+
81
+
82
+ @register_pooler
83
+ class ClsLastHiddenStatePooler(nn.Module):
84
+ """CLS token pooling
85
+ NOTE: this is equivalent to ClsPooler above with use_pooler_output=False
86
+ """
87
+
88
+ def __init__(self):
89
+ super().__init__()
90
+ self.cls_token_position = 0
91
+
92
+ def forward(self, x: BaseModelOutput, attention_mask: TensorType):
93
+ return x.last_hidden_state[:, self.cls_token_position, :]
94
+
95
+
96
+ class HFTextEncoder(nn.Module):
97
+ """HuggingFace model adapter"""
98
+ output_tokens: torch.jit.Final[bool]
99
+
100
+ def __init__(
101
+ self,
102
+ model_name_or_path: str,
103
+ output_dim: int,
104
+ config: PretrainedConfig = None,
105
+ pooler_type: str = None,
106
+ proj_type: str = None,
107
+ pretrained: bool = True,
108
+ output_tokens: bool = False,
109
+ ):
110
+ super().__init__()
111
+ self.output_tokens = output_tokens
112
+ self.output_dim = output_dim
113
+
114
+ # TODO: find better way to get this information
115
+ uses_transformer_pooler = (pooler_type == "cls_pooler")
116
+
117
+ if transformers is None:
118
+ raise RuntimeError("Please `pip install transformers` to use pre-trained HuggingFace models")
119
+ if config is None:
120
+ self.config = AutoConfig.from_pretrained(model_name_or_path)
121
+ create_func, model_args = (AutoModel.from_pretrained, model_name_or_path) if pretrained else (
122
+ AutoModel.from_config, self.config)
123
+ # TODO: do all model configs have this attribute? PretrainedConfig does so yes??
124
+ if hasattr(self.config, "is_encoder_decoder") and self.config.is_encoder_decoder:
125
+ self.transformer = create_func(model_args)
126
+ self.transformer = self.transformer.encoder
127
+ else:
128
+ self.transformer = create_func(model_args, add_pooling_layer=uses_transformer_pooler)
129
+ else:
130
+ self.config = config
131
+ self.transformer = AutoModel.from_config(config)
132
+ if pooler_type is None: # get default arch pooler
133
+ pooler_type = (arch_dict[self.config.model_type]["pooler"])
134
+
135
+ # FIXME downstream users of OpenCLIP models use these attr, need to verify valid across all models
136
+ self.vocab_size = getattr(self.config, 'vocab_size', 0)
137
+ self.context_length = getattr(self.config, 'max_position_embeddings', 0)
138
+
139
+ self.pooler = _POOLERS[pooler_type]()
140
+
141
+ d_model = getattr(self.config, arch_dict[self.config.model_type]["config_names"]["width"])
142
+ if (d_model == output_dim) and (proj_type is None): # do we always need a proj?
143
+ self.proj = nn.Identity()
144
+ elif proj_type == 'linear':
145
+ self.proj = nn.Linear(d_model, output_dim, bias=False)
146
+ elif proj_type == 'mlp':
147
+ hidden_size = (d_model + output_dim) // 2
148
+ self.proj = nn.Sequential(
149
+ nn.Linear(d_model, hidden_size, bias=False),
150
+ nn.GELU(),
151
+ nn.Linear(hidden_size, output_dim, bias=False),
152
+ )
153
+
154
+ def forward(self, x: TensorType):
155
+ attn_mask = (x != self.config.pad_token_id).long()
156
+ out = self.transformer(input_ids=x, attention_mask=attn_mask)
157
+ pooled_out = self.pooler(out, attn_mask)
158
+ projected = self.proj(pooled_out)
159
+
160
+ seq_len = out.last_hidden_state.shape[1]
161
+ tokens = (
162
+ out.last_hidden_state[:, torch.arange(seq_len) != self.pooler.cls_token_position, :]
163
+ if type(self.pooler) == ClsPooler
164
+ else out.last_hidden_state
165
+ )
166
+
167
+ if self.output_tokens:
168
+ return projected, tokens
169
+ return projected
170
+
171
+ def lock(self, unlocked_layers: int = 0, freeze_layer_norm: bool = True):
172
+ if not unlocked_layers: # full freezing
173
+ for n, p in self.transformer.named_parameters():
174
+ p.requires_grad = (not freeze_layer_norm) if "LayerNorm" in n.split(".") else False
175
+ return
176
+
177
+ encoder = self.transformer.encoder if hasattr(self.transformer, 'encoder') else self.transformer
178
+ layer_list = getattr(encoder, arch_dict[self.config.model_type]["config_names"]["layer_attr"])
179
+ print(f"Unlocking {unlocked_layers}/{len(layer_list) + 1} layers of hf model")
180
+ embeddings = getattr(
181
+ self.transformer, arch_dict[self.config.model_type]["config_names"]["token_embeddings_attr"])
182
+ modules = [embeddings, *layer_list][:-unlocked_layers]
183
+ # freeze layers
184
+ for module in modules:
185
+ for n, p in module.named_parameters():
186
+ p.requires_grad = (not freeze_layer_norm) if "LayerNorm" in n.split(".") else False
187
+
188
+ @torch.jit.ignore
189
+ def set_grad_checkpointing(self, enable=True):
190
+ self.transformer.gradient_checkpointing_enable()
191
+
192
+ def init_parameters(self):
193
+ pass
ecg_coca/open_clip/loss.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.nn import functional as F
4
+
5
+ try:
6
+ import torch.distributed.nn
7
+ from torch import distributed as dist
8
+
9
+ has_distributed = True
10
+ except ImportError:
11
+ has_distributed = False
12
+
13
+ try:
14
+ import horovod.torch as hvd
15
+ except ImportError:
16
+ hvd = None
17
+
18
+
19
+ def gather_features(
20
+ ecg_features,
21
+ text_features,
22
+ local_loss=False,
23
+ gather_with_grad=False,
24
+ rank=0,
25
+ world_size=1,
26
+ use_horovod=False
27
+ ):
28
+ assert has_distributed, 'torch.distributed did not import correctly, please use a PyTorch version with support.'
29
+ if use_horovod:
30
+ assert hvd is not None, 'Please install horovod'
31
+ if gather_with_grad:
32
+ all_ecg_features = hvd.allgather(ecg_features)
33
+ all_text_features = hvd.allgather(text_features)
34
+ else:
35
+ with torch.no_grad():
36
+ all_ecg_features = hvd.allgather(ecg_features)
37
+ all_text_features = hvd.allgather(text_features)
38
+ if not local_loss:
39
+ # ensure grads for local rank when all_* features don't have a gradient
40
+ gathered_ecg_features = list(all_ecg_features.chunk(world_size, dim=0))
41
+ gathered_text_features = list(all_text_features.chunk(world_size, dim=0))
42
+ gathered_ecg_features[rank] = ecg_features
43
+ gathered_text_features[rank] = text_features
44
+ all_ecg_features = torch.cat(gathered_ecg_features, dim=0)
45
+ all_text_features = torch.cat(gathered_text_features, dim=0)
46
+ else:
47
+ # We gather tensors from all gpus
48
+ if gather_with_grad:
49
+ all_ecg_features = torch.cat(torch.distributed.nn.all_gather(ecg_features), dim=0)
50
+ all_text_features = torch.cat(torch.distributed.nn.all_gather(text_features), dim=0)
51
+ else:
52
+ gathered_ecg_features = [torch.zeros_like(ecg_features) for _ in range(world_size)]
53
+ gathered_text_features = [torch.zeros_like(text_features) for _ in range(world_size)]
54
+ dist.all_gather(gathered_ecg_features, ecg_features)
55
+ dist.all_gather(gathered_text_features, text_features)
56
+ if not local_loss:
57
+ # ensure grads for local rank when all_* features don't have a gradient
58
+ gathered_ecg_features[rank] = ecg_features
59
+ gathered_text_features[rank] = text_features
60
+ all_ecg_features = torch.cat(gathered_ecg_features, dim=0)
61
+ all_text_features = torch.cat(gathered_text_features, dim=0)
62
+
63
+ return all_ecg_features, all_text_features
64
+
65
+
66
+ class ClipLoss(nn.Module):
67
+
68
+ def __init__(
69
+ self,
70
+ local_loss=False,
71
+ gather_with_grad=False,
72
+ cache_labels=False,
73
+ rank=0,
74
+ world_size=1,
75
+ use_horovod=False,
76
+ ):
77
+ super().__init__()
78
+ self.local_loss = local_loss
79
+ self.gather_with_grad = gather_with_grad
80
+ self.cache_labels = cache_labels
81
+ self.rank = rank
82
+ self.world_size = world_size
83
+ self.use_horovod = use_horovod
84
+
85
+ # cache state
86
+ self.prev_num_logits = 0
87
+ self.labels = {}
88
+
89
+ def get_ground_truth(self, device, num_logits) -> torch.Tensor:
90
+ # calculated ground-truth and cache if enabled
91
+ if self.prev_num_logits != num_logits or device not in self.labels:
92
+ labels = torch.arange(num_logits, device=device, dtype=torch.long)
93
+ if self.world_size > 1 and self.local_loss:
94
+ labels = labels + num_logits * self.rank
95
+ if self.cache_labels:
96
+ self.labels[device] = labels
97
+ self.prev_num_logits = num_logits
98
+ else:
99
+ labels = self.labels[device]
100
+ return labels
101
+
102
+ def get_logits(self, ecg_features, text_features, logit_scale):
103
+ if self.world_size > 1:
104
+ all_ecg_features, all_text_features = gather_features(
105
+ ecg_features, text_features,
106
+ self.local_loss, self.gather_with_grad, self.rank, self.world_size, self.use_horovod)
107
+
108
+ if self.local_loss:
109
+ logits_per_ecg = logit_scale * ecg_features @ all_text_features.T
110
+ logits_per_text = logit_scale * text_features @ all_ecg_features.T
111
+ else:
112
+ logits_per_ecg = logit_scale * all_ecg_features @ all_text_features.T
113
+ logits_per_text = logits_per_ecg.T
114
+ else:
115
+ logits_per_ecg = logit_scale * ecg_features @ text_features.T
116
+ logits_per_text = logit_scale * text_features @ ecg_features.T
117
+
118
+ return logits_per_ecg, logits_per_text
119
+
120
+ def forward(self, ecg_features, text_features, logit_scale, output_dict=False):
121
+ device = ecg_features.device
122
+ logits_per_ecg, logits_per_text = self.get_logits(ecg_features, text_features, logit_scale)
123
+
124
+ labels = self.get_ground_truth(device, logits_per_ecg.shape[0])
125
+
126
+ total_loss = (
127
+ F.cross_entropy(logits_per_ecg, labels) +
128
+ F.cross_entropy(logits_per_text, labels)
129
+ ) / 2
130
+
131
+ return {"contrastive_loss": total_loss} if output_dict else total_loss
132
+
133
+
134
+ class CoCaLoss(ClipLoss):
135
+ def __init__(
136
+ self,
137
+ caption_loss_weight,
138
+ clip_loss_weight,
139
+ pad_id=0, # pad_token for open_clip custom tokenizer
140
+ local_loss=False,
141
+ gather_with_grad=False,
142
+ cache_labels=False,
143
+ rank=0,
144
+ world_size=1,
145
+ use_horovod=False,
146
+ ):
147
+ super().__init__(
148
+ local_loss=local_loss,
149
+ gather_with_grad=gather_with_grad,
150
+ cache_labels=cache_labels,
151
+ rank=rank,
152
+ world_size=world_size,
153
+ use_horovod=use_horovod
154
+ )
155
+
156
+ self.clip_loss_weight = clip_loss_weight
157
+ self.caption_loss_weight = caption_loss_weight
158
+ self.caption_loss = nn.CrossEntropyLoss(ignore_index=pad_id)
159
+
160
+ def forward(self, ecg_features, text_features, logits, labels, logit_scale, output_dict=False):
161
+
162
+ clip_loss = torch.tensor(0)
163
+
164
+ if self.clip_loss_weight:
165
+ clip_loss = super().forward(ecg_features, text_features, logit_scale)
166
+ clip_loss = self.clip_loss_weight * clip_loss
167
+
168
+ caption_loss = self.caption_loss(
169
+ logits.permute(0, 2, 1),
170
+ labels,
171
+ )
172
+ caption_loss = caption_loss * self.caption_loss_weight
173
+
174
+ if output_dict:
175
+ return {"contrastive_loss": clip_loss, "caption_loss": caption_loss}
176
+
177
+ return clip_loss, caption_loss
178
+
179
+
180
+ class DistillClipLoss(ClipLoss):
181
+
182
+ def dist_loss(self, teacher_logits, student_logits):
183
+ return -(teacher_logits.softmax(dim=1) * student_logits.log_softmax(dim=1)).sum(dim=1).mean(dim=0)
184
+
185
+ def forward(
186
+ self,
187
+ image_features,
188
+ text_features,
189
+ logit_scale,
190
+ dist_image_features,
191
+ dist_text_features,
192
+ dist_logit_scale,
193
+ output_dict=False,
194
+ ):
195
+ logits_per_image, logits_per_text = \
196
+ self.get_logits(image_features, text_features, logit_scale)
197
+
198
+ dist_logits_per_image, dist_logits_per_text = \
199
+ self.get_logits(dist_image_features, dist_text_features, dist_logit_scale)
200
+
201
+ labels = self.get_ground_truth(image_features.device, logits_per_image.shape[0])
202
+
203
+ contrastive_loss = (
204
+ F.cross_entropy(logits_per_image, labels) +
205
+ F.cross_entropy(logits_per_text, labels)
206
+ ) / 2
207
+
208
+ distill_loss = (
209
+ self.dist_loss(dist_logits_per_image, logits_per_image) +
210
+ self.dist_loss(dist_logits_per_text, logits_per_text)
211
+ ) / 2
212
+
213
+ if output_dict:
214
+ return {"contrastive_loss": contrastive_loss, "distill_loss": distill_loss}
215
+
216
+ return contrastive_loss, distill_loss
217
+
218
+
219
+ def neighbour_exchange(from_rank, to_rank, tensor, group=None):
220
+ tensor_recv = torch.zeros_like(tensor)
221
+ send_op = torch.distributed.P2POp(
222
+ torch.distributed.isend,
223
+ tensor,
224
+ to_rank,
225
+ group=group,
226
+ )
227
+ recv_op = torch.distributed.P2POp(
228
+ torch.distributed.irecv,
229
+ tensor_recv,
230
+ from_rank,
231
+ group=group,
232
+ )
233
+ reqs = torch.distributed.batch_isend_irecv([send_op, recv_op])
234
+ for req in reqs:
235
+ req.wait()
236
+ return tensor_recv
237
+
238
+
239
+ def neighbour_exchange_bidir(left_rank, right_rank, tensor_to_left, tensor_to_right, group=None):
240
+ tensor_from_left = torch.zeros_like(tensor_to_right)
241
+ tensor_from_right = torch.zeros_like(tensor_to_left)
242
+ send_op_left = torch.distributed.P2POp(
243
+ torch.distributed.isend,
244
+ tensor_to_left,
245
+ left_rank,
246
+ group=group,
247
+ )
248
+ send_op_right = torch.distributed.P2POp(
249
+ torch.distributed.isend,
250
+ tensor_to_right,
251
+ right_rank,
252
+ group=group,
253
+ )
254
+ recv_op_left = torch.distributed.P2POp(
255
+ torch.distributed.irecv,
256
+ tensor_from_left,
257
+ left_rank,
258
+ group=group,
259
+ )
260
+ recv_op_right = torch.distributed.P2POp(
261
+ torch.distributed.irecv,
262
+ tensor_from_right,
263
+ right_rank,
264
+ group=group,
265
+ )
266
+ reqs = torch.distributed.batch_isend_irecv([send_op_right, send_op_left, recv_op_right, recv_op_left])
267
+ for req in reqs:
268
+ req.wait()
269
+ return tensor_from_right, tensor_from_left
270
+
271
+
272
+ class NeighbourExchange(torch.autograd.Function):
273
+ @staticmethod
274
+ def forward(ctx, from_rank, to_rank, group, tensor):
275
+ ctx.group = group
276
+ ctx.from_rank = from_rank
277
+ ctx.to_rank = to_rank
278
+ return neighbour_exchange(from_rank, to_rank, tensor, group=group)
279
+
280
+ @staticmethod
281
+ def backward(ctx, grad_output):
282
+ return (None, None, None) + (NeighbourExchange.apply(ctx.to_rank, ctx.from_rank, ctx.group, grad_output),)
283
+
284
+
285
+ def neighbour_exchange_with_grad(from_rank, to_rank, tensor, group=None):
286
+ return NeighbourExchange.apply(from_rank, to_rank, group, tensor)
287
+
288
+
289
+ class NeighbourExchangeBidir(torch.autograd.Function):
290
+ @staticmethod
291
+ def forward(ctx, left_rank, right_rank, group, tensor_to_left, tensor_to_right):
292
+ ctx.group = group
293
+ ctx.left_rank = left_rank
294
+ ctx.right_rank = right_rank
295
+ return neighbour_exchange_bidir(left_rank, right_rank, tensor_to_left, tensor_to_right, group=group)
296
+
297
+ @staticmethod
298
+ def backward(ctx, *grad_outputs):
299
+ return (None, None, None) + \
300
+ NeighbourExchangeBidir.apply(ctx.right_rank, ctx.left_rank, ctx.group, *grad_outputs)
301
+
302
+
303
+ def neighbour_exchange_bidir_with_grad(left_rank, right_rank, tensor_to_left, tensor_to_right, group=None):
304
+ return NeighbourExchangeBidir.apply(left_rank, right_rank, group, tensor_to_left, tensor_to_right)
305
+
306
+
307
+ class SigLipLoss(nn.Module):
308
+ """ Sigmoid Loss for Language Image Pre-Training (SigLIP) - https://arxiv.org/abs/2303.15343
309
+
310
+ @article{zhai2023sigmoid,
311
+ title={Sigmoid loss for language image pre-training},
312
+ author={Zhai, Xiaohua and Mustafa, Basil and Kolesnikov, Alexander and Beyer, Lucas},
313
+ journal={arXiv preprint arXiv:2303.15343},
314
+ year={2023}
315
+ }
316
+ """
317
+ def __init__(
318
+ self,
319
+ cache_labels=False,
320
+ rank=0,
321
+ world_size=1,
322
+ bidir=True,
323
+ use_horovod=False,
324
+ ):
325
+ super().__init__()
326
+ self.cache_labels = cache_labels
327
+ self.rank = rank
328
+ self.world_size = world_size
329
+ assert not use_horovod # FIXME need to look at hvd ops for ring transfers
330
+ self.use_horovod = use_horovod
331
+ self.bidir = bidir
332
+
333
+ # cache state FIXME cache not currently used, worthwhile?
334
+ self.prev_num_logits = 0
335
+ self.labels = {}
336
+
337
+ def get_ground_truth(self, device, dtype, num_logits, negative_only=False) -> torch.Tensor:
338
+ labels = -torch.ones((num_logits, num_logits), device=device, dtype=dtype)
339
+ if not negative_only:
340
+ labels = 2 * torch.eye(num_logits, device=device, dtype=dtype) + labels
341
+ return labels
342
+
343
+ def get_logits(self, image_features, text_features, logit_scale, logit_bias=None):
344
+ logits = logit_scale * image_features @ text_features.T
345
+ if logit_bias is not None:
346
+ logits += logit_bias
347
+ return logits
348
+
349
+ def _loss(self, image_features, text_features, logit_scale, logit_bias=None, negative_only=False):
350
+ logits = self.get_logits(image_features, text_features, logit_scale, logit_bias)
351
+ labels = self.get_ground_truth(
352
+ image_features.device,
353
+ image_features.dtype,
354
+ image_features.shape[0],
355
+ negative_only=negative_only,
356
+ )
357
+ loss = -F.logsigmoid(labels * logits).sum() / image_features.shape[0]
358
+ return loss
359
+
360
+ def forward(self, image_features, text_features, logit_scale, logit_bias, output_dict=False):
361
+ loss = self._loss(image_features, text_features, logit_scale, logit_bias)
362
+
363
+ if self.world_size > 1:
364
+ # exchange text features w/ neighbour world_size - 1 times
365
+ right_rank = (self.rank + 1) % self.world_size
366
+ left_rank = (self.rank - 1 + self.world_size) % self.world_size
367
+ if self.bidir:
368
+ text_features_to_right = text_features_to_left = text_features
369
+ num_bidir, remainder = divmod(self.world_size - 1, 2)
370
+ for i in range(num_bidir):
371
+ text_features_recv = neighbour_exchange_bidir_with_grad(
372
+ left_rank,
373
+ right_rank,
374
+ text_features_to_left,
375
+ text_features_to_right,
376
+ )
377
+
378
+ for f in text_features_recv:
379
+ loss += self._loss(
380
+ image_features,
381
+ f,
382
+ logit_scale,
383
+ logit_bias,
384
+ negative_only=True,
385
+ )
386
+ text_features_to_left, text_features_to_right = text_features_recv
387
+
388
+ if remainder:
389
+ text_features_recv = neighbour_exchange_with_grad(
390
+ left_rank, right_rank, text_features_to_right)
391
+
392
+ loss += self._loss(
393
+ image_features,
394
+ text_features_recv,
395
+ logit_scale,
396
+ logit_bias,
397
+ negative_only=True,
398
+ )
399
+ else:
400
+ text_features_to_right = text_features
401
+ for i in range(self.world_size - 1):
402
+ text_features_from_left = neighbour_exchange_with_grad(
403
+ left_rank, right_rank, text_features_to_right)
404
+
405
+ loss += self._loss(
406
+ image_features,
407
+ text_features_from_left,
408
+ logit_scale,
409
+ logit_bias,
410
+ negative_only=True,
411
+ )
412
+ text_features_to_right = text_features_from_left
413
+
414
+ return {"contrastive_loss": loss} if output_dict else loss
ecg_coca/open_clip/model.py ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ CLIP Model
2
+
3
+ Adapted from https://github.com/openai/CLIP. Originally MIT License, Copyright (c) 2021 OpenAI.
4
+ """
5
+ import copy
6
+ import logging
7
+ import math
8
+ from dataclasses import dataclass
9
+ from typing import Any, Dict, Optional, Tuple, Union
10
+
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from torch import nn
15
+ from torch.utils.checkpoint import checkpoint
16
+ from functools import partial
17
+
18
+ from .hf_model import HFTextEncoder
19
+ from .modified_resnet import ModifiedResNet
20
+ from .timm_model import TimmModel
21
+ from .transformer import LayerNormFp32, LayerNorm, QuickGELU, Attention, VisionTransformer, TextTransformer,\
22
+ EcgTransformer, text_global_pool
23
+ from .utils import to_2tuple
24
+
25
+
26
+ @dataclass
27
+ class CLIPVisionCfg:
28
+ layers: Union[Tuple[int, int, int, int], int] = 12
29
+ width: int = 768
30
+ head_width: int = 64
31
+ mlp_ratio: float = 4.0
32
+ patch_size: int = 16
33
+ image_size: Union[Tuple[int, int], int] = 224
34
+
35
+ ls_init_value: Optional[float] = None # layer scale initial value
36
+ patch_dropout: float = 0. # what fraction of patches to dropout during training (0 would mean disabled and no patches dropped) - 0.5 to 0.75 recommended in the paper for optimal results
37
+ attentional_pool: bool = False # whether to use attentional pooler in the last embedding layer (overrides pool_type)
38
+ attn_pooler_queries: int = 256 # n_queries for attentional pooler
39
+ attn_pooler_heads: int = 8 # n heads for attentional_pooling
40
+ no_ln_pre: bool = False # disable pre transformer LayerNorm
41
+ pos_embed_type: str = 'learnable'
42
+ final_ln_after_pool: bool = False # apply final LayerNorm after pooling
43
+ pool_type: str = 'tok'
44
+ output_tokens: bool = False
45
+ act_kwargs: Optional[dict] = None
46
+ norm_kwargs: Optional[dict] = None
47
+
48
+ timm_model_name: Optional[str] = None # a valid model name overrides layers, width, patch_size
49
+ timm_model_pretrained: bool = False # use (imagenet) pretrained weights for named model
50
+ timm_pool: str = 'avg' # feature pooling for timm model ('abs_attn', 'rot_attn', 'avg', '')
51
+ timm_proj: str = 'linear' # linear projection for timm model output ('linear', 'mlp', '')
52
+ timm_proj_bias: bool = False # enable bias final projection
53
+ timm_drop: float = 0. # head dropout
54
+ timm_drop_path: Optional[float] = None # backbone stochastic depth
55
+
56
+
57
+ @dataclass
58
+ class CLIPTextCfg:
59
+ context_length: int = 77
60
+ vocab_size: int = 49408
61
+ hf_tokenizer_name: Optional[str] = None
62
+ tokenizer_kwargs: Optional[dict] = None
63
+
64
+ width: int = 512
65
+ heads: int = 8
66
+ layers: int = 12
67
+ mlp_ratio: float = 4.0
68
+ ls_init_value: Optional[float] = None # layer scale initial value
69
+ embed_cls: bool = False
70
+ pad_id: int = 0
71
+ no_causal_mask: bool = False # disable causal masking
72
+ final_ln_after_pool: bool = False # apply final LayerNorm after pooling
73
+ pool_type: str = 'argmax'
74
+ proj_bias: bool = False
75
+ output_tokens: bool = False
76
+ act_kwargs: dict = None
77
+ norm_kwargs: dict = None
78
+
79
+ # HuggingFace specific text tower config
80
+ hf_model_name: Optional[str] = None
81
+ hf_model_pretrained: bool = True
82
+ hf_proj_type: str = 'mlp'
83
+ hf_pooler_type: str = 'mean_pooler' # attentional pooling for HF models
84
+
85
+ @dataclass
86
+ class CLIPEcgCfg:
87
+ layers: Union[Tuple[int, int, int, int], int] = 12
88
+ width: int = 768
89
+ head_width: int = 64
90
+ mlp_ratio: float = 4.0
91
+ patch_size: int = 50
92
+ seq_length: int = 5000
93
+ lead_num: int = 12
94
+
95
+ ls_init_value: Optional[float] = None # layer scale initial value
96
+ patch_dropout: float = 0. # what fraction of patches to dropout during training (0 would mean disabled and no patches dropped) - 0.5 to 0.75 recommended in the paper for optimal results
97
+ attentional_pool: bool = False # whether to use attentional pooler in the last embedding layer (overrides pool_type)
98
+ attn_pooler_queries: int = 256 # n_queries for attentional pooler
99
+ attn_pooler_heads: int = 8 # n heads for attentional_pooling
100
+ no_ln_pre: bool = False # disable pre transformer LayerNorm
101
+ pos_embed_type: str = 'learnable'
102
+ final_ln_after_pool: bool = False # apply final LayerNorm after pooling
103
+ pool_type: str = 'tok'
104
+ output_tokens: bool = False
105
+ act_kwargs: Optional[dict] = None
106
+ norm_kwargs: Optional[dict] = None
107
+
108
+ def get_cast_dtype(precision: str):
109
+ cast_dtype = None
110
+ if precision == 'bf16':
111
+ cast_dtype = torch.bfloat16
112
+ elif precision == 'fp16':
113
+ cast_dtype = torch.float16
114
+ return cast_dtype
115
+
116
+
117
+ def get_input_dtype(precision: str):
118
+ input_dtype = None
119
+ if precision in ('bf16', 'pure_bf16'):
120
+ input_dtype = torch.bfloat16
121
+ elif precision in ('fp16', 'pure_fp16'):
122
+ input_dtype = torch.float16
123
+ return input_dtype
124
+
125
+
126
+ def _build_vision_tower(
127
+ embed_dim: int,
128
+ vision_cfg: CLIPVisionCfg,
129
+ quick_gelu: bool = False,
130
+ cast_dtype: Optional[torch.dtype] = None
131
+ ):
132
+ if isinstance(vision_cfg, dict):
133
+ vision_cfg = CLIPVisionCfg(**vision_cfg)
134
+
135
+ # OpenAI models are pretrained w/ QuickGELU but native nn.GELU is both faster and more
136
+ # memory efficient in recent PyTorch releases (>= 1.10).
137
+ # NOTE: timm models always use native GELU regardless of quick_gelu flag.
138
+ act_layer = QuickGELU if quick_gelu else nn.GELU
139
+
140
+ if vision_cfg.timm_model_name:
141
+ visual = TimmModel(
142
+ vision_cfg.timm_model_name,
143
+ pretrained=vision_cfg.timm_model_pretrained,
144
+ pool=vision_cfg.timm_pool,
145
+ proj=vision_cfg.timm_proj,
146
+ proj_bias=vision_cfg.timm_proj_bias,
147
+ drop=vision_cfg.timm_drop,
148
+ drop_path=vision_cfg.timm_drop_path,
149
+ patch_drop=vision_cfg.patch_dropout if vision_cfg.patch_dropout > 0 else None,
150
+ embed_dim=embed_dim,
151
+ image_size=vision_cfg.image_size,
152
+ )
153
+ elif isinstance(vision_cfg.layers, (tuple, list)):
154
+ vision_heads = vision_cfg.width * 32 // vision_cfg.head_width
155
+ visual = ModifiedResNet(
156
+ layers=vision_cfg.layers,
157
+ output_dim=embed_dim,
158
+ heads=vision_heads,
159
+ image_size=vision_cfg.image_size,
160
+ width=vision_cfg.width,
161
+ )
162
+ else:
163
+ vision_heads = vision_cfg.width // vision_cfg.head_width
164
+ norm_layer = LayerNormFp32 if cast_dtype in (torch.float16, torch.bfloat16) else LayerNorm
165
+ if vision_cfg.norm_kwargs:
166
+ norm_layer = partial(norm_layer, **vision_cfg.norm_kwargs)
167
+ if vision_cfg.act_kwargs is not None:
168
+ act_layer = partial(act_layer, **vision_cfg.act_kwargs)
169
+
170
+ visual = VisionTransformer(
171
+ image_size=vision_cfg.image_size,
172
+ patch_size=vision_cfg.patch_size,
173
+ width=vision_cfg.width,
174
+ layers=vision_cfg.layers,
175
+ heads=vision_heads,
176
+ mlp_ratio=vision_cfg.mlp_ratio,
177
+ ls_init_value=vision_cfg.ls_init_value,
178
+ patch_dropout=vision_cfg.patch_dropout,
179
+ attentional_pool=vision_cfg.attentional_pool,
180
+ attn_pooler_queries=vision_cfg.attn_pooler_queries,
181
+ attn_pooler_heads=vision_cfg.attn_pooler_heads,
182
+ pos_embed_type=vision_cfg.pos_embed_type,
183
+ no_ln_pre=vision_cfg.no_ln_pre,
184
+ final_ln_after_pool=vision_cfg.final_ln_after_pool,
185
+ pool_type=vision_cfg.pool_type,
186
+ output_tokens=vision_cfg.output_tokens,
187
+ output_dim=embed_dim,
188
+ act_layer=act_layer,
189
+ norm_layer=norm_layer,
190
+ )
191
+
192
+ return visual
193
+
194
+
195
+ def _build_ecg_tower(
196
+ embed_dim:int,
197
+ ecg_cfg: CLIPEcgCfg,
198
+ quick_gelu: bool = False,
199
+ cast_dtype: Optional[torch.dtype] = None
200
+ ):
201
+ if isinstance(ecg_cfg, dict):
202
+ ecg_cfg = CLIPEcgCfg(**ecg_cfg)
203
+
204
+ act_layer = QuickGELU if quick_gelu else nn.GELU
205
+
206
+ ecg_heads = ecg_cfg.width // ecg_cfg.head_width
207
+ norm_layer = LayerNormFp32 if cast_dtype in (torch.float16, torch.bfloat16) else LayerNorm
208
+ if ecg_cfg.norm_kwargs:
209
+ norm_layer = partial(norm_layer, **ecg_cfg.norm_kwargs)
210
+ if ecg_cfg.act_kwargs is not None:
211
+ act_layer = partial(act_layer, **ecg_cfg.act_kwargs)
212
+
213
+ ecg = EcgTransformer(
214
+ seq_length=ecg_cfg.seq_length,
215
+ patch_size=ecg_cfg.patch_size,
216
+ lead_num=ecg_cfg.lead_num,
217
+ width=ecg_cfg.width,
218
+ layers=ecg_cfg.layers,
219
+ heads=ecg_heads,
220
+ mlp_ratio=ecg_cfg.mlp_ratio,
221
+ ls_init_value=ecg_cfg.ls_init_value,
222
+ patch_dropout=ecg_cfg.patch_dropout,
223
+ attentional_pool=ecg_cfg.attentional_pool,
224
+ attn_pooler_queries=ecg_cfg.attn_pooler_queries,
225
+ attn_pooler_heads=ecg_cfg.attn_pooler_heads,
226
+ pos_embed_type=ecg_cfg.pos_embed_type,
227
+ no_ln_pre=ecg_cfg.no_ln_pre,
228
+ final_ln_after_pool=ecg_cfg.final_ln_after_pool,
229
+ pool_type=ecg_cfg.pool_type,
230
+ output_tokens=ecg_cfg.output_tokens,
231
+ output_dim=embed_dim,
232
+ act_layer=act_layer,
233
+ norm_layer=norm_layer,
234
+ )
235
+ return ecg
236
+
237
+ def _build_text_tower(
238
+ embed_dim: int,
239
+ text_cfg: CLIPTextCfg,
240
+ quick_gelu: bool = False,
241
+ cast_dtype: Optional[torch.dtype] = None,
242
+ ):
243
+ if isinstance(text_cfg, dict):
244
+ text_cfg = CLIPTextCfg(**text_cfg)
245
+
246
+ if text_cfg.hf_model_name:
247
+ text = HFTextEncoder(
248
+ text_cfg.hf_model_name,
249
+ output_dim=embed_dim,
250
+ proj_type=text_cfg.hf_proj_type,
251
+ pooler_type=text_cfg.hf_pooler_type,
252
+ pretrained=text_cfg.hf_model_pretrained,
253
+ output_tokens=text_cfg.output_tokens,
254
+ )
255
+ else:
256
+ act_layer = QuickGELU if quick_gelu else nn.GELU
257
+ norm_layer = LayerNormFp32 if cast_dtype in (torch.float16, torch.bfloat16) else LayerNorm
258
+ if text_cfg.norm_kwargs:
259
+ norm_layer = partial(norm_layer, **text_cfg.norm_kwargs)
260
+ if text_cfg.act_kwargs is not None:
261
+ act_layer = partial(act_layer, **text_cfg.act_kwargs)
262
+
263
+ text = TextTransformer(
264
+ context_length=text_cfg.context_length,
265
+ vocab_size=text_cfg.vocab_size,
266
+ width=text_cfg.width,
267
+ heads=text_cfg.heads,
268
+ layers=text_cfg.layers,
269
+ mlp_ratio=text_cfg.mlp_ratio,
270
+ ls_init_value=text_cfg.ls_init_value,
271
+ output_dim=embed_dim,
272
+ embed_cls=text_cfg.embed_cls,
273
+ no_causal_mask=text_cfg.no_causal_mask,
274
+ pad_id=text_cfg.pad_id,
275
+ pool_type=text_cfg.pool_type,
276
+ proj_bias=text_cfg.proj_bias,
277
+ output_tokens=text_cfg.output_tokens,
278
+ act_layer=act_layer,
279
+ norm_layer=norm_layer,
280
+ )
281
+ return text
282
+
283
+
284
+ class CLIP(nn.Module):
285
+ output_dict: torch.jit.Final[bool]
286
+
287
+ def __init__(
288
+ self,
289
+ embed_dim: int,
290
+ vision_cfg: CLIPVisionCfg,
291
+ text_cfg: CLIPTextCfg,
292
+ quick_gelu: bool = False,
293
+ init_logit_scale: float = np.log(1 / 0.07),
294
+ init_logit_bias: Optional[float] = None,
295
+ cast_dtype: Optional[torch.dtype] = None,
296
+ output_dict: bool = False,
297
+ ):
298
+ super().__init__()
299
+ self.output_dict = output_dict
300
+
301
+ self.visual = _build_vision_tower(embed_dim, vision_cfg, quick_gelu, cast_dtype)
302
+
303
+ text = _build_text_tower(embed_dim, text_cfg, quick_gelu, cast_dtype)
304
+ self.transformer = text.transformer
305
+ self.context_length = text.context_length
306
+ self.vocab_size = text.vocab_size
307
+ self.token_embedding = text.token_embedding
308
+ self.positional_embedding = text.positional_embedding
309
+ self.ln_final = text.ln_final
310
+ self.text_projection = text.text_projection
311
+ self.text_pool_type = text.pool_type
312
+ self.register_buffer('attn_mask', text.attn_mask, persistent=False)
313
+
314
+ self.logit_scale = nn.Parameter(torch.ones([]) * init_logit_scale)
315
+ if init_logit_bias is not None:
316
+ self.logit_bias = nn.Parameter(torch.ones([]) * init_logit_bias)
317
+ else:
318
+ self.logit_bias = None
319
+
320
+ def lock_image_tower(self, unlocked_groups=0, freeze_bn_stats=False):
321
+ # lock image tower as per LiT - https://arxiv.org/abs/2111.07991
322
+ self.visual.lock(unlocked_groups=unlocked_groups, freeze_bn_stats=freeze_bn_stats)
323
+
324
+ @torch.jit.ignore
325
+ def set_grad_checkpointing(self, enable=True):
326
+ self.visual.set_grad_checkpointing(enable)
327
+ self.transformer.grad_checkpointing = enable
328
+
329
+ def encode_image(self, image, normalize: bool = False):
330
+ features = self.visual(image)
331
+ return F.normalize(features, dim=-1) if normalize else features
332
+
333
+ def encode_text(self, text, normalize: bool = False):
334
+ cast_dtype = self.transformer.get_cast_dtype()
335
+
336
+ x = self.token_embedding(text).to(cast_dtype) # [batch_size, n_ctx, d_model]
337
+
338
+ x = x + self.positional_embedding.to(cast_dtype)
339
+ x = self.transformer(x, attn_mask=self.attn_mask)
340
+ x = self.ln_final(x) # [batch_size, n_ctx, transformer.width]
341
+ x, _ = text_global_pool(x, text, self.text_pool_type)
342
+ if self.text_projection is not None:
343
+ if isinstance(self.text_projection, nn.Linear):
344
+ x = self.text_projection(x)
345
+ else:
346
+ x = x @ self.text_projection
347
+
348
+ return F.normalize(x, dim=-1) if normalize else x
349
+
350
+ def get_logits(self, image, text):
351
+ image_features = self.encode_image(image, normalize=True)
352
+ text_features = self.encode_text(text, normalize=True)
353
+ image_logits = self.logit_scale.exp() * image_features @ text_features.T
354
+ if self.logit_bias is not None:
355
+ image_logits += self.logit_bias
356
+ text_logits = image_logits.T
357
+ return image_logits, text_logits
358
+
359
+ def forward(
360
+ self,
361
+ image: Optional[torch.Tensor] = None,
362
+ text: Optional[torch.Tensor] = None,
363
+ ):
364
+ image_features = self.encode_image(image, normalize=True) if image is not None else None
365
+ text_features = self.encode_text(text, normalize=True) if text is not None else None
366
+
367
+ if self.output_dict:
368
+ out_dict = {
369
+ "image_features": image_features,
370
+ "text_features": text_features,
371
+ "logit_scale": self.logit_scale.exp()
372
+ }
373
+ if self.logit_bias is not None:
374
+ out_dict['logit_bias'] = self.logit_bias
375
+ return out_dict
376
+
377
+ if self.logit_bias is not None:
378
+ return image_features, text_features, self.logit_scale.exp(), self.logit_bias
379
+ return image_features, text_features, self.logit_scale.exp()
380
+
381
+
382
+ class CustomTextCLIP(nn.Module):
383
+ output_dict: torch.jit.Final[bool]
384
+
385
+ def __init__(
386
+ self,
387
+ embed_dim: int,
388
+ vision_cfg: CLIPVisionCfg,
389
+ text_cfg: CLIPTextCfg,
390
+ quick_gelu: bool = False,
391
+ init_logit_scale: float = np.log(1 / 0.07),
392
+ init_logit_bias: Optional[float] = None,
393
+ cast_dtype: Optional[torch.dtype] = None,
394
+ output_dict: bool = False,
395
+ ):
396
+ super().__init__()
397
+ self.output_dict = output_dict
398
+ self.visual = _build_vision_tower(embed_dim, vision_cfg, quick_gelu, cast_dtype)
399
+ self.text = _build_text_tower(embed_dim, text_cfg, quick_gelu, cast_dtype)
400
+ self.context_length = self.text.context_length
401
+ self.vocab_size = self.text.vocab_size
402
+ self.logit_scale = nn.Parameter(torch.ones([]) * init_logit_scale)
403
+ if init_logit_bias is not None:
404
+ self.logit_bias = nn.Parameter(torch.ones([]) * init_logit_bias)
405
+ else:
406
+ self.logit_bias = None
407
+
408
+ def lock_image_tower(self, unlocked_groups=0, freeze_bn_stats=False):
409
+ # lock image tower as per LiT - https://arxiv.org/abs/2111.07991
410
+ self.visual.lock(unlocked_groups=unlocked_groups, freeze_bn_stats=freeze_bn_stats)
411
+
412
+ def lock_text_tower(self, unlocked_layers: int = 0, freeze_layer_norm: bool = True):
413
+ self.text.lock(unlocked_layers, freeze_layer_norm)
414
+
415
+ @torch.jit.ignore
416
+ def set_grad_checkpointing(self, enable=True):
417
+ self.visual.set_grad_checkpointing(enable)
418
+ self.text.set_grad_checkpointing(enable)
419
+
420
+ def encode_image(self, image, normalize: bool = False):
421
+ features = self.visual(image)
422
+ return F.normalize(features, dim=-1) if normalize else features
423
+
424
+ def encode_text(self, text, normalize: bool = False):
425
+ features = self.text(text)
426
+ return F.normalize(features, dim=-1) if normalize else features
427
+
428
+ def get_logits(self, image, text):
429
+ image_features = self.encode_image(image, normalize=True)
430
+ text_features = self.encode_text(text, normalize=True)
431
+ image_logits = self.logit_scale.exp() * image_features @ text_features.T
432
+ if self.logit_bias is not None:
433
+ image_logits += self.logit_bias
434
+ text_logits = image_logits.T
435
+ return image_logits, text_logits
436
+
437
+ def forward(
438
+ self,
439
+ image: Optional[torch.Tensor] = None,
440
+ text: Optional[torch.Tensor] = None,
441
+ ):
442
+ image_features = self.encode_image(image, normalize=True) if image is not None else None
443
+ text_features = self.encode_text(text, normalize=True) if text is not None else None
444
+
445
+ if self.output_dict:
446
+ out_dict = {
447
+ "image_features": image_features,
448
+ "text_features": text_features,
449
+ "logit_scale": self.logit_scale.exp()
450
+ }
451
+ if self.logit_bias is not None:
452
+ out_dict['logit_bias'] = self.logit_bias
453
+ return out_dict
454
+
455
+ if self.logit_bias is not None:
456
+ return image_features, text_features, self.logit_scale.exp(), self.logit_bias
457
+ return image_features, text_features, self.logit_scale.exp()
458
+
459
+
460
+ def convert_weights_to_lp(model: nn.Module, dtype=torch.float16):
461
+ """Convert applicable model parameters to low-precision (bf16 or fp16)"""
462
+
463
+ def _convert_weights(l):
464
+ if isinstance(l, (nn.Conv1d, nn.Conv2d, nn.Linear)):
465
+ l.weight.data = l.weight.data.to(dtype)
466
+ if l.bias is not None:
467
+ l.bias.data = l.bias.data.to(dtype)
468
+
469
+ if isinstance(l, (nn.MultiheadAttention, Attention)):
470
+ for attr in [*[f"{s}_proj_weight" for s in ["in", "q", "k", "v"]], "in_proj_bias", "bias_k", "bias_v"]:
471
+ tensor = getattr(l, attr)
472
+ if tensor is not None:
473
+ tensor.data = tensor.data.to(dtype)
474
+
475
+ if isinstance(l, (CLIP, TextTransformer)):
476
+ # convert text nn.Parameter projections
477
+ attr = getattr(l, "text_projection", None)
478
+ if attr is not None:
479
+ attr.data = attr.data.to(dtype)
480
+
481
+ if isinstance(l, VisionTransformer):
482
+ # convert vision nn.Parameter projections
483
+ attr = getattr(l, "proj", None)
484
+ if attr is not None:
485
+ attr.data = attr.data.to(dtype)
486
+
487
+ if isinstance(l, EcgTransformer):
488
+ # convert vision nn.Parameter projections
489
+ attr = getattr(l, "proj", None)
490
+ if attr is not None:
491
+ attr.data = attr.data.to(dtype)
492
+
493
+ model.apply(_convert_weights)
494
+
495
+
496
+ convert_weights_to_fp16 = convert_weights_to_lp # backwards compat
497
+
498
+
499
+ # used to maintain checkpoint compatibility
500
+ def convert_to_custom_text_state_dict(state_dict: dict):
501
+ if 'text_projection' in state_dict:
502
+ # old format state_dict, move text tower -> .text
503
+ new_state_dict = {}
504
+ for k, v in state_dict.items():
505
+ if any(k.startswith(p) for p in (
506
+ 'text_projection',
507
+ 'positional_embedding',
508
+ 'token_embedding',
509
+ 'transformer',
510
+ 'ln_final',
511
+ )):
512
+ k = 'text.' + k
513
+ new_state_dict[k] = v
514
+ return new_state_dict
515
+ return state_dict
516
+
517
+
518
+ def build_model_from_openai_state_dict(
519
+ state_dict: dict,
520
+ quick_gelu=True,
521
+ cast_dtype=torch.float16,
522
+ ):
523
+ vit = "visual.proj" in state_dict
524
+
525
+ if vit:
526
+ vision_width = state_dict["visual.conv1.weight"].shape[0]
527
+ vision_layers = len(
528
+ [k for k in state_dict.keys() if k.startswith("visual.") and k.endswith(".attn.in_proj_weight")])
529
+ vision_patch_size = state_dict["visual.conv1.weight"].shape[-1]
530
+ grid_size = round((state_dict["visual.positional_embedding"].shape[0] - 1) ** 0.5)
531
+ image_size = vision_patch_size * grid_size
532
+ else:
533
+ counts: list = [
534
+ len(set(k.split(".")[2] for k in state_dict if k.startswith(f"visual.layer{b}"))) for b in [1, 2, 3, 4]]
535
+ vision_layers = tuple(counts)
536
+ vision_width = state_dict["visual.layer1.0.conv1.weight"].shape[0]
537
+ output_width = round((state_dict["visual.attnpool.positional_embedding"].shape[0] - 1) ** 0.5)
538
+ vision_patch_size = None
539
+ assert output_width ** 2 + 1 == state_dict["visual.attnpool.positional_embedding"].shape[0]
540
+ image_size = output_width * 32
541
+
542
+ embed_dim = state_dict["text_projection"].shape[1]
543
+ context_length = state_dict["positional_embedding"].shape[0]
544
+ vocab_size = state_dict["token_embedding.weight"].shape[0]
545
+ transformer_width = state_dict["ln_final.weight"].shape[0]
546
+ transformer_heads = transformer_width // 64
547
+ transformer_layers = len(set(k.split(".")[2] for k in state_dict if k.startswith(f"transformer.resblocks")))
548
+
549
+ vision_cfg = CLIPVisionCfg(
550
+ layers=vision_layers,
551
+ width=vision_width,
552
+ patch_size=vision_patch_size,
553
+ image_size=image_size,
554
+ )
555
+ text_cfg = CLIPTextCfg(
556
+ context_length=context_length,
557
+ vocab_size=vocab_size,
558
+ width=transformer_width,
559
+ heads=transformer_heads,
560
+ layers=transformer_layers,
561
+ )
562
+ model = CLIP(
563
+ embed_dim,
564
+ vision_cfg=vision_cfg,
565
+ text_cfg=text_cfg,
566
+ quick_gelu=quick_gelu, # OpenAI models were trained with QuickGELU
567
+ cast_dtype=cast_dtype,
568
+ )
569
+
570
+ for key in ["input_resolution", "context_length", "vocab_size"]:
571
+ state_dict.pop(key, None)
572
+ convert_weights_to_fp16(model) # OpenAI state dicts are partially converted to float16
573
+ model.load_state_dict(state_dict)
574
+ return model.eval()
575
+
576
+
577
+ def trace_model(model, batch_size=256, device=torch.device('cpu')):
578
+ model.eval()
579
+ image_size = model.visual.image_size
580
+ example_images = torch.ones((batch_size, 3, image_size, image_size), device=device)
581
+ example_text = torch.zeros((batch_size, model.context_length), dtype=torch.int, device=device)
582
+ model = torch.jit.trace_module(
583
+ model,
584
+ inputs=dict(
585
+ forward=(example_images, example_text),
586
+ encode_text=(example_text,),
587
+ encode_image=(example_images,)
588
+ ))
589
+ model.visual.image_size = image_size
590
+ return model
591
+
592
+
593
+ def resize_pos_embed(state_dict, model, interpolation: str = 'bicubic', antialias: bool = True):
594
+ # Rescale the grid of position embeddings when loading from state_dict
595
+ old_pos_embed = state_dict.get('visual.positional_embedding', None)
596
+ if old_pos_embed is None or not hasattr(model.visual, 'grid_size'):
597
+ return
598
+ grid_size = to_2tuple(model.visual.grid_size)
599
+ extra_tokens = 1 # FIXME detect different token configs (ie no class token, or more)
600
+ new_seq_len = grid_size[0] * grid_size[1] + extra_tokens
601
+ if new_seq_len == old_pos_embed.shape[0]:
602
+ return
603
+
604
+ if extra_tokens:
605
+ pos_emb_tok, pos_emb_img = old_pos_embed[:extra_tokens], old_pos_embed[extra_tokens:]
606
+ else:
607
+ pos_emb_tok, pos_emb_img = None, old_pos_embed
608
+ old_grid_size = to_2tuple(int(math.sqrt(len(pos_emb_img))))
609
+
610
+ logging.info('Resizing position embedding grid-size from %s to %s', old_grid_size, grid_size)
611
+ pos_emb_img = pos_emb_img.reshape(1, old_grid_size[0], old_grid_size[1], -1).permute(0, 3, 1, 2)
612
+ pos_emb_img = F.interpolate(
613
+ pos_emb_img,
614
+ size=grid_size,
615
+ mode=interpolation,
616
+ antialias=antialias,
617
+ align_corners=False,
618
+ )
619
+ pos_emb_img = pos_emb_img.permute(0, 2, 3, 1).reshape(1, grid_size[0] * grid_size[1], -1)[0]
620
+ if pos_emb_tok is not None:
621
+ new_pos_embed = torch.cat([pos_emb_tok, pos_emb_img], dim=0)
622
+ else:
623
+ new_pos_embed = pos_emb_img
624
+ state_dict['visual.positional_embedding'] = new_pos_embed
625
+
626
+
627
+ def resize_text_pos_embed(state_dict, model, interpolation: str = 'linear', antialias: bool = False):
628
+ old_pos_embed = state_dict.get('positional_embedding', None)
629
+ if old_pos_embed is None:
630
+ return
631
+ # FIXME add support for text cls_token
632
+ model_pos_embed = getattr(model, 'positional_embedding', None)
633
+ if model_pos_embed is None:
634
+ model_pos_embed = getattr(model.text, 'positional_embedding', None)
635
+
636
+ old_num_pos = old_pos_embed.shape[0]
637
+ old_width = old_pos_embed.shape[1]
638
+ num_pos = model_pos_embed.shape[0]
639
+ width = model_pos_embed.shape[1]
640
+ assert old_width == width, 'text pos_embed width changed!'
641
+ if old_num_pos == num_pos:
642
+ return
643
+
644
+ logging.info('Resizing text position embedding num_pos from %s to %s', old_num_pos, num_pos)
645
+ old_pos_embed = old_pos_embed.reshape(1, old_num_pos, old_width).permute(0, 2, 1)
646
+ old_pos_embed = F.interpolate(
647
+ old_pos_embed,
648
+ size=num_pos,
649
+ mode=interpolation,
650
+ antialias=antialias,
651
+ align_corners=False,
652
+ )
653
+ old_pos_embed = old_pos_embed.permute(0, 2, 1)[0]
654
+ new_pos_embed = old_pos_embed
655
+
656
+ state_dict['positional_embedding'] = new_pos_embed
657
+
658
+
659
+ def get_model_preprocess_cfg(model):
660
+ module = getattr(model, 'ecg', model)
661
+ preprocess_cfg = getattr(module, 'preprocess_cfg', {})
662
+ if not preprocess_cfg:
663
+ # use separate legacy attributes if preprocess_cfg dict not found
664
+ size = getattr(module, 'seq_length')
665
+ if size is not None:
666
+ preprocess_cfg['size'] = size
667
+ preprocess_cfg['duration'] = 10
668
+ preprocess_cfg['sampling_rate'] = size / 10
669
+
670
+ return preprocess_cfg
671
+
672
+
673
+ def set_model_preprocess_cfg(model, preprocess_cfg: Dict[str, Any]):
674
+ module = getattr(model, 'ecg', model)
675
+ module.image_mean = preprocess_cfg['mean'] # legacy attribute, keeping for bwd compat
676
+ module.image_std = preprocess_cfg['std'] # legacy attribute, keeping for bwd compat
677
+ module.preprocess_cfg = copy.deepcopy(preprocess_cfg) # new attr, package all pp cfg as dict
678
+
679
+
680
+ def get_model_tokenize_cfg(model):
681
+ module = getattr(model, 'text', model)
682
+ cfg = {}
683
+ context_length = getattr(module, 'context_length', None)
684
+ if context_length is not None:
685
+ cfg['context_length'] = context_length
686
+ vocab_size = getattr(module, 'vocab_size', None)
687
+ if vocab_size is not None:
688
+ cfg['vocab_size'] = vocab_size
689
+ return cfg
690
+
691
+
692
+ import torch
693
+ import torch.nn as nn
694
+
695
+
696
+ class BasicBlock(nn.Module):
697
+ expansion = 1
698
+
699
+ def __init__(self, in_channels, out_channels, stride=1):
700
+ super(BasicBlock, self).__init__()
701
+
702
+ # 1x1 Convolution
703
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
704
+ self.bn1 = nn.BatchNorm1d(out_channels)
705
+
706
+ # 3x3 Convolution
707
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)
708
+ self.bn2 = nn.BatchNorm1d(out_channels)
709
+
710
+ self.shortcut = nn.Sequential()
711
+ if stride != 1 or in_channels != self.expansion * out_channels:
712
+ self.shortcut = nn.Sequential(
713
+ nn.Conv1d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False),
714
+ nn.BatchNorm1d(self.expansion * out_channels)
715
+ )
716
+
717
+ def forward(self, x):
718
+ out = torch.relu(self.bn1(self.conv1(x)))
719
+ out = self.bn2(self.conv2(out))
720
+ out += self.shortcut(x)
721
+ out = torch.relu(out)
722
+ return out
723
+
724
+
725
+ class Bottleneck(nn.Module):
726
+ expansion = 4
727
+
728
+ def __init__(self, in_channels, out_channels, stride=1):
729
+ super(Bottleneck, self).__init__()
730
+
731
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False)
732
+ self.bn1 = nn.BatchNorm1d(out_channels)
733
+
734
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
735
+ self.bn2 = nn.BatchNorm1d(out_channels)
736
+
737
+ self.conv3 = nn.Conv1d(out_channels, self.expansion * out_channels, kernel_size=1, bias=False)
738
+ self.bn3 = nn.BatchNorm1d(self.expansion * out_channels)
739
+
740
+ self.shortcut = nn.Sequential()
741
+ if stride != 1 or in_channels != self.expansion * out_channels:
742
+ self.shortcut = nn.Sequential(
743
+ nn.Conv1d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False),
744
+ nn.BatchNorm1d(self.expansion * out_channels)
745
+ )
746
+
747
+ def forward(self, x):
748
+ out = torch.relu(self.bn1(self.conv1(x)))
749
+ out = torch.relu(self.bn2(self.conv2(out)))
750
+ out = self.bn3(self.conv3(out))
751
+ out += self.shortcut(x)
752
+ out = torch.relu(out)
753
+ return out
754
+
755
+
756
+ class BasicBlock(nn.Module):
757
+ expansion = 1
758
+
759
+ def __init__(self, in_channels, out_channels, stride=1):
760
+ super(BasicBlock, self).__init__()
761
+
762
+ # 1x1 Convolution
763
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
764
+ self.bn1 = nn.BatchNorm1d(out_channels)
765
+
766
+ # 3x3 Convolution
767
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)
768
+ self.bn2 = nn.BatchNorm1d(out_channels)
769
+
770
+ self.shortcut = nn.Sequential()
771
+ if stride != 1 or in_channels != self.expansion * out_channels:
772
+ self.shortcut = nn.Sequential(
773
+ nn.Conv1d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False),
774
+ nn.BatchNorm1d(self.expansion * out_channels)
775
+ )
776
+
777
+ def forward(self, x):
778
+ out = torch.relu(self.bn1(self.conv1(x)))
779
+ out = self.bn2(self.conv2(out))
780
+ out += self.shortcut(x)
781
+ out = torch.relu(out)
782
+ return out
783
+
784
+
785
+ class Bottleneck(nn.Module):
786
+ expansion = 4
787
+
788
+ def __init__(self, in_channels, out_channels, stride=1):
789
+ super(Bottleneck, self).__init__()
790
+
791
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False)
792
+ self.bn1 = nn.BatchNorm1d(out_channels)
793
+
794
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
795
+ self.bn2 = nn.BatchNorm1d(out_channels)
796
+
797
+ self.conv3 = nn.Conv1d(out_channels, self.expansion * out_channels, kernel_size=1, bias=False)
798
+ self.bn3 = nn.BatchNorm1d(self.expansion * out_channels)
799
+
800
+ self.shortcut = nn.Sequential()
801
+ if stride != 1 or in_channels != self.expansion * out_channels:
802
+ self.shortcut = nn.Sequential(
803
+ nn.Conv1d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False),
804
+ nn.BatchNorm1d(self.expansion * out_channels)
805
+ )
806
+
807
+ def forward(self, x):
808
+ out = torch.relu(self.bn1(self.conv1(x)))
809
+ out = torch.relu(self.bn2(self.conv2(out)))
810
+ out = self.bn3(self.conv3(out))
811
+ out += self.shortcut(x)
812
+ out = torch.relu(out)
813
+ return out
814
+
815
+
816
+ class AttentionPool2d(nn.Module):
817
+ def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None):
818
+ super().__init__()
819
+ self.positional_embedding = nn.Parameter(torch.randn(1, spacial_dim + 1, embed_dim) / embed_dim)
820
+ self.cls_token = nn.Parameter(torch.randn(1, 1, embed_dim))
821
+
822
+ self.mhsa = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
823
+ self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim)
824
+ self.num_heads = num_heads
825
+
826
+ def forward(self, x):
827
+ x = x.permute(0, 2, 1) # convert X shape (B, C, L) to (B, L, C)
828
+
829
+ self.cls_tokens = self.cls_token + self.positional_embedding[:, :1, :]
830
+ self.cls_tokens = self.cls_tokens.expand(x.shape[0], -1, -1)
831
+ x = torch.cat((self.cls_tokens, x), dim=1)
832
+ x = x + self.positional_embedding[:, :, :].to(x.dtype) # (L+1)NC
833
+ x, att_map = self.mhsa(x[:, :1, :], x, x, average_attn_weights=True)
834
+ x = self.c_proj(x)
835
+ return x.squeeze(0), att_map[:, :, 1:]
836
+
837
+
838
+ class ResNet(nn.Module):
839
+ def __init__(self, block, num_blocks, hidden_size=512):
840
+ super(ResNet, self).__init__()
841
+ self.in_channels = 64
842
+ self.proj_out = hidden_size
843
+ self.conv1 = nn.Conv1d(12, 64, kernel_size=7, stride=2, padding=3, bias=False)
844
+ self.bn1 = nn.BatchNorm1d(64)
845
+ self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
846
+ self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
847
+ self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
848
+ self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
849
+ self.downconv = nn.Conv1d(in_channels=2048, out_channels=self.proj_out, kernel_size=1)
850
+ self.att_pool_head = AttentionPool2d(spacial_dim=313,
851
+ embed_dim=self.proj_out,
852
+ num_heads=4,
853
+ output_dim=self.proj_out)
854
+ self.linear1 = nn.Linear(self.proj_out, self.proj_out, bias=False)
855
+ self.linear2 = nn.Linear(self.proj_out, self.proj_out, bias=False)
856
+
857
+ def _make_layer(self, block, out_channels, num_blocks, stride):
858
+ strides = [stride] + [1] * (num_blocks - 1)
859
+ layers = []
860
+ for stride in strides:
861
+ layers.append(block(self.in_channels, out_channels, stride))
862
+ self.in_channels = out_channels * block.expansion
863
+ return nn.Sequential(*layers)
864
+
865
+ def forward(self, x):
866
+ out = torch.relu(self.bn1(self.conv1(x)))
867
+ out = self.layer1(out)
868
+ out = self.layer2(out)
869
+ out = self.layer3(out)
870
+ out = self.layer4(out)
871
+ # out = self.avgpool(out)
872
+ # out = out.view(out.size(0), -1)
873
+ # out = self.linear(out)
874
+ pooled, tokens = x[:, 0], x[:, 1:]
875
+
876
+ return out
877
+
878
+
879
+ def ResNet18():
880
+ return ResNet(BasicBlock, [2, 2, 2, 2])
881
+
882
+
883
+ def ResNet34():
884
+ return ResNet(BasicBlock, [3, 4, 6, 3])
885
+
886
+
887
+ def ResNet50():
888
+ return ResNet(Bottleneck, [3, 4, 6, 3])
889
+
890
+
891
+ def ResNet101():
892
+ # Here we would usually use a different block type, Bottleneck, which contains three convolution layers
893
+ # For simplicity, we're going to use BasicBlock here
894
+ return ResNet(Bottleneck, [3, 4, 23, 3])
895
+
896
+
897
+ def ResNet152():
898
+ return ResNet(Bottleneck, [3, 8, 36, 3])
ecg_coca/open_clip/model_configs/coca_ViT-B-32.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "embed_dim": 512,
3
+ "ecg_cfg": {
4
+ "seq_length": 5000,
5
+ "lead_num": 12,
6
+ "layers": 12,
7
+ "width": 768,
8
+ "patch_size": 50,
9
+ "output_tokens": true
10
+ },
11
+ "text_cfg": {
12
+ "hf_model_name": "ncbi/MedCPT-Query-Encoder",
13
+ "hf_tokenizer_name": "ncbi/MedCPT-Query-Encoder",
14
+ "hf_proj_type": "linear",
15
+ "width": 768,
16
+ "output_tokens": true
17
+ },
18
+ "multimodal_cfg": {
19
+ "context_length": 76,
20
+ "width": 768,
21
+ "heads": 8,
22
+ "layers": 12
23
+ },
24
+ "custom_text": true
25
+ }
ecg_coca/open_clip/modified_resnet.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import OrderedDict
2
+
3
+ import torch
4
+ from torch import nn
5
+ from torch.nn import functional as F
6
+
7
+ from ecg_coca.open_clip.utils import freeze_batch_norm_2d
8
+
9
+
10
+ class Bottleneck(nn.Module):
11
+ expansion = 4
12
+
13
+ def __init__(self, inplanes, planes, stride=1):
14
+ super().__init__()
15
+
16
+ # all conv layers have stride 1. an avgpool is performed after the second convolution when stride > 1
17
+ self.conv1 = nn.Conv2d(inplanes, planes, 1, bias=False)
18
+ self.bn1 = nn.BatchNorm2d(planes)
19
+ self.act1 = nn.ReLU(inplace=True)
20
+
21
+ self.conv2 = nn.Conv2d(planes, planes, 3, padding=1, bias=False)
22
+ self.bn2 = nn.BatchNorm2d(planes)
23
+ self.act2 = nn.ReLU(inplace=True)
24
+
25
+ self.avgpool = nn.AvgPool2d(stride) if stride > 1 else nn.Identity()
26
+
27
+ self.conv3 = nn.Conv2d(planes, planes * self.expansion, 1, bias=False)
28
+ self.bn3 = nn.BatchNorm2d(planes * self.expansion)
29
+ self.act3 = nn.ReLU(inplace=True)
30
+
31
+ self.downsample = None
32
+ self.stride = stride
33
+
34
+ if stride > 1 or inplanes != planes * Bottleneck.expansion:
35
+ # downsampling layer is prepended with an avgpool, and the subsequent convolution has stride 1
36
+ self.downsample = nn.Sequential(OrderedDict([
37
+ ("-1", nn.AvgPool2d(stride)),
38
+ ("0", nn.Conv2d(inplanes, planes * self.expansion, 1, stride=1, bias=False)),
39
+ ("1", nn.BatchNorm2d(planes * self.expansion))
40
+ ]))
41
+
42
+ def forward(self, x: torch.Tensor):
43
+ identity = x
44
+
45
+ out = self.act1(self.bn1(self.conv1(x)))
46
+ out = self.act2(self.bn2(self.conv2(out)))
47
+ out = self.avgpool(out)
48
+ out = self.bn3(self.conv3(out))
49
+
50
+ if self.downsample is not None:
51
+ identity = self.downsample(x)
52
+
53
+ out += identity
54
+ out = self.act3(out)
55
+ return out
56
+
57
+
58
+ class AttentionPool2d(nn.Module):
59
+ def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None):
60
+ super().__init__()
61
+ self.positional_embedding = nn.Parameter(torch.randn(spacial_dim ** 2 + 1, embed_dim) / embed_dim ** 0.5)
62
+ self.k_proj = nn.Linear(embed_dim, embed_dim)
63
+ self.q_proj = nn.Linear(embed_dim, embed_dim)
64
+ self.v_proj = nn.Linear(embed_dim, embed_dim)
65
+ self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim)
66
+ self.num_heads = num_heads
67
+
68
+ def forward(self, x):
69
+ x = x.reshape(x.shape[0], x.shape[1], x.shape[2] * x.shape[3]).permute(2, 0, 1) # NCHW -> (HW)NC
70
+ x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0) # (HW+1)NC
71
+ x = x + self.positional_embedding[:, None, :].to(x.dtype) # (HW+1)NC
72
+ x, _ = F.multi_head_attention_forward(
73
+ query=x, key=x, value=x,
74
+ embed_dim_to_check=x.shape[-1],
75
+ num_heads=self.num_heads,
76
+ q_proj_weight=self.q_proj.weight,
77
+ k_proj_weight=self.k_proj.weight,
78
+ v_proj_weight=self.v_proj.weight,
79
+ in_proj_weight=None,
80
+ in_proj_bias=torch.cat([self.q_proj.bias, self.k_proj.bias, self.v_proj.bias]),
81
+ bias_k=None,
82
+ bias_v=None,
83
+ add_zero_attn=False,
84
+ dropout_p=0.,
85
+ out_proj_weight=self.c_proj.weight,
86
+ out_proj_bias=self.c_proj.bias,
87
+ use_separate_proj_weight=True,
88
+ training=self.training,
89
+ need_weights=False
90
+ )
91
+
92
+ return x[0]
93
+
94
+
95
+ class ModifiedResNet(nn.Module):
96
+ """
97
+ A ResNet class that is similar to torchvision's but contains the following changes:
98
+ - There are now 3 "stem" convolutions as opposed to 1, with an average pool instead of a max pool.
99
+ - Performs anti-aliasing strided convolutions, where an avgpool is prepended to convolutions with stride > 1
100
+ - The final pooling layer is a QKV attention instead of an average pool
101
+ """
102
+
103
+ def __init__(self, layers, output_dim, heads, image_size=224, width=64):
104
+ super().__init__()
105
+ self.output_dim = output_dim
106
+ self.image_size = image_size
107
+
108
+ # the 3-layer stem
109
+ self.conv1 = nn.Conv2d(3, width // 2, kernel_size=3, stride=2, padding=1, bias=False)
110
+ self.bn1 = nn.BatchNorm2d(width // 2)
111
+ self.act1 = nn.ReLU(inplace=True)
112
+ self.conv2 = nn.Conv2d(width // 2, width // 2, kernel_size=3, padding=1, bias=False)
113
+ self.bn2 = nn.BatchNorm2d(width // 2)
114
+ self.act2 = nn.ReLU(inplace=True)
115
+ self.conv3 = nn.Conv2d(width // 2, width, kernel_size=3, padding=1, bias=False)
116
+ self.bn3 = nn.BatchNorm2d(width)
117
+ self.act3 = nn.ReLU(inplace=True)
118
+ self.avgpool = nn.AvgPool2d(2)
119
+
120
+ # residual layers
121
+ self._inplanes = width # this is a *mutable* variable used during construction
122
+ self.layer1 = self._make_layer(width, layers[0])
123
+ self.layer2 = self._make_layer(width * 2, layers[1], stride=2)
124
+ self.layer3 = self._make_layer(width * 4, layers[2], stride=2)
125
+ self.layer4 = self._make_layer(width * 8, layers[3], stride=2)
126
+
127
+ embed_dim = width * 32 # the ResNet feature dimension
128
+ self.attnpool = AttentionPool2d(image_size // 32, embed_dim, heads, output_dim)
129
+
130
+ self.init_parameters()
131
+
132
+ def _make_layer(self, planes, blocks, stride=1):
133
+ layers = [Bottleneck(self._inplanes, planes, stride)]
134
+
135
+ self._inplanes = planes * Bottleneck.expansion
136
+ for _ in range(1, blocks):
137
+ layers.append(Bottleneck(self._inplanes, planes))
138
+
139
+ return nn.Sequential(*layers)
140
+
141
+ def init_parameters(self):
142
+ if self.attnpool is not None:
143
+ std = self.attnpool.c_proj.in_features ** -0.5
144
+ nn.init.normal_(self.attnpool.q_proj.weight, std=std)
145
+ nn.init.normal_(self.attnpool.k_proj.weight, std=std)
146
+ nn.init.normal_(self.attnpool.v_proj.weight, std=std)
147
+ nn.init.normal_(self.attnpool.c_proj.weight, std=std)
148
+
149
+ for resnet_block in [self.layer1, self.layer2, self.layer3, self.layer4]:
150
+ for name, param in resnet_block.named_parameters():
151
+ if name.endswith("bn3.weight"):
152
+ nn.init.zeros_(param)
153
+
154
+ def lock(self, unlocked_groups=0, freeze_bn_stats=False):
155
+ assert unlocked_groups == 0, 'partial locking not currently supported for this model'
156
+ for param in self.parameters():
157
+ param.requires_grad = False
158
+ if freeze_bn_stats:
159
+ freeze_batch_norm_2d(self)
160
+
161
+ @torch.jit.ignore
162
+ def set_grad_checkpointing(self, enable=True):
163
+ # FIXME support for non-transformer
164
+ pass
165
+
166
+ def stem(self, x):
167
+ x = self.act1(self.bn1(self.conv1(x)))
168
+ x = self.act2(self.bn2(self.conv2(x)))
169
+ x = self.act3(self.bn3(self.conv3(x)))
170
+ x = self.avgpool(x)
171
+ return x
172
+
173
+ def forward(self, x):
174
+ x = self.stem(x)
175
+ x = self.layer1(x)
176
+ x = self.layer2(x)
177
+ x = self.layer3(x)
178
+ x = self.layer4(x)
179
+ x = self.attnpool(x)
180
+
181
+ return x
ecg_coca/open_clip/openai.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ OpenAI pretrained model functions
2
+
3
+ Adapted from https://github.com/openai/CLIP. Originally MIT License, Copyright (c) 2021 OpenAI.
4
+ """
5
+
6
+ import os
7
+ import warnings
8
+ from typing import List, Optional, Union
9
+
10
+ import torch
11
+
12
+ from .constants import OPENAI_DATASET_MEAN, OPENAI_DATASET_STD
13
+ from .model import build_model_from_openai_state_dict, convert_weights_to_lp, get_cast_dtype
14
+ from .pretrained import get_pretrained_url, list_pretrained_models_by_tag, download_pretrained_from_url
15
+
16
+ __all__ = ["list_openai_models", "load_openai_model"]
17
+
18
+
19
+ def list_openai_models() -> List[str]:
20
+ """Returns the names of available CLIP models"""
21
+ return list_pretrained_models_by_tag('openai')
22
+
23
+
24
+ def load_openai_model(
25
+ name: str,
26
+ precision: Optional[str] = None,
27
+ device: Optional[Union[str, torch.device]] = None,
28
+ cache_dir: Optional[str] = None,
29
+ ):
30
+ """Load a CLIP model
31
+
32
+ Parameters
33
+ ----------
34
+ name : str
35
+ A model name listed by `clip.available_models()`, or the path to a model checkpoint containing the state_dict
36
+ precision: str
37
+ Model precision, if None defaults to 'fp32' if device == 'cpu' else 'fp16'.
38
+ device : Union[str, torch.device]
39
+ The device to put the loaded model
40
+ cache_dir : Optional[str]
41
+ The directory to cache the downloaded model weights
42
+
43
+ Returns
44
+ -------
45
+ model : torch.nn.Module
46
+ The CLIP model
47
+ preprocess : Callable[[PIL.Image], torch.Tensor]
48
+ A torchvision transform that converts a PIL image into a tensor that the returned model can take as its input
49
+ """
50
+ if device is None:
51
+ device = "cuda" if torch.cuda.is_available() else "cpu"
52
+ if precision is None:
53
+ precision = 'fp32' if device == 'cpu' else 'fp16'
54
+
55
+ if get_pretrained_url(name, 'openai'):
56
+ model_path = download_pretrained_from_url(get_pretrained_url(name, 'openai'), cache_dir=cache_dir)
57
+ elif os.path.isfile(name):
58
+ model_path = name
59
+ else:
60
+ raise RuntimeError(f"Model {name} not found; available models = {list_openai_models()}")
61
+
62
+ try:
63
+ # loading JIT archive
64
+ model = torch.jit.load(model_path, map_location="cpu").eval()
65
+ state_dict = None
66
+ except RuntimeError:
67
+ # loading saved state dict
68
+ state_dict = torch.load(model_path, map_location="cpu")
69
+
70
+ # Build a non-jit model from the OpenAI jitted model state dict
71
+ cast_dtype = get_cast_dtype(precision)
72
+ try:
73
+ model = build_model_from_openai_state_dict(state_dict or model.state_dict(), cast_dtype=cast_dtype)
74
+ except KeyError:
75
+ sd = {k[7:]: v for k, v in state_dict["state_dict"].items()}
76
+ model = build_model_from_openai_state_dict(sd, cast_dtype=cast_dtype)
77
+
78
+ # model from OpenAI state dict is in manually cast fp16 mode, must be converted for AMP/fp32/bf16 use
79
+ model = model.to(device)
80
+ # FIXME support pure fp16/bf16 precision modes
81
+ if precision != 'fp16':
82
+ model.float()
83
+ if precision == 'bf16':
84
+ # for bf16, convert back to low-precision
85
+ convert_weights_to_lp(model, dtype=torch.bfloat16)
86
+
87
+ # add mean / std attributes for consistency with OpenCLIP models
88
+ model.visual.image_mean = OPENAI_DATASET_MEAN
89
+ model.visual.image_std = OPENAI_DATASET_STD
90
+ return model
ecg_coca/open_clip/pos_embed.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+ # This source code is licensed under the license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ # --------------------------------------------------------
7
+ # Position embedding utils
8
+ # --------------------------------------------------------
9
+
10
+ import numpy as np
11
+
12
+ import torch
13
+
14
+ # --------------------------------------------------------
15
+ # 2D sine-cosine position embedding
16
+ # References:
17
+ # Transformer: https://github.com/tensorflow/models/blob/master/official/nlp/transformer/model_utils.py
18
+ # MoCo v3: https://github.com/facebookresearch/moco-v3
19
+ # --------------------------------------------------------
20
+ def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False):
21
+ """
22
+ grid_size: int of the grid height and width
23
+ return:
24
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
25
+ """
26
+ grid_h = np.arange(grid_size, dtype=np.float32)
27
+ grid_w = np.arange(grid_size, dtype=np.float32)
28
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
29
+ grid = np.stack(grid, axis=0)
30
+
31
+ grid = grid.reshape([2, 1, grid_size, grid_size])
32
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
33
+ if cls_token:
34
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
35
+ return pos_embed
36
+
37
+
38
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
39
+ assert embed_dim % 2 == 0
40
+
41
+ # use half of dimensions to encode grid_h
42
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
43
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
44
+
45
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
46
+ return emb
47
+
48
+
49
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
50
+ """
51
+ embed_dim: output dimension for each position
52
+ pos: a list of positions to be encoded: size (M,)
53
+ out: (M, D)
54
+ """
55
+ assert embed_dim % 2 == 0
56
+ omega = np.arange(embed_dim // 2, dtype=float)
57
+ omega /= embed_dim / 2.
58
+ omega = 1. / 10000**omega # (D/2,)
59
+
60
+ pos = pos.reshape(-1) # (M,)
61
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
62
+
63
+ emb_sin = np.sin(out) # (M, D/2)
64
+ emb_cos = np.cos(out) # (M, D/2)
65
+
66
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
67
+ return emb
68
+
69
+
70
+ # --------------------------------------------------------
71
+ # Interpolate position embeddings for high-resolution
72
+ # References:
73
+ # DeiT: https://github.com/facebookresearch/deit
74
+ # --------------------------------------------------------
75
+ def interpolate_pos_embed(model, checkpoint_model):
76
+ if 'pos_embed' in checkpoint_model:
77
+ pos_embed_checkpoint = checkpoint_model['pos_embed']
78
+ embedding_size = pos_embed_checkpoint.shape[-1]
79
+ num_patches = model.patch_embed.num_patches
80
+ num_extra_tokens = model.pos_embed.shape[-2] - num_patches
81
+ # height (== width) for the checkpoint position embedding
82
+ orig_size = int((pos_embed_checkpoint.shape[-2] - num_extra_tokens) ** 0.5)
83
+ # height (== width) for the new position embedding
84
+ new_size = int(num_patches ** 0.5)
85
+ # class_token and dist_token are kept unchanged
86
+ if orig_size != new_size:
87
+ print("Position interpolate from %dx%d to %dx%d" % (orig_size, orig_size, new_size, new_size))
88
+ extra_tokens = pos_embed_checkpoint[:, :num_extra_tokens]
89
+ # only the position tokens are interpolated
90
+ pos_tokens = pos_embed_checkpoint[:, num_extra_tokens:]
91
+ pos_tokens = pos_tokens.reshape(-1, orig_size, orig_size, embedding_size).permute(0, 3, 1, 2)
92
+ pos_tokens = torch.nn.functional.interpolate(
93
+ pos_tokens, size=(new_size, new_size), mode='bicubic', align_corners=False)
94
+ pos_tokens = pos_tokens.permute(0, 2, 3, 1).flatten(1, 2)
95
+ new_pos_embed = torch.cat((extra_tokens, pos_tokens), dim=1)
96
+ checkpoint_model['pos_embed'] = new_pos_embed
97
+
98
+
ecg_coca/open_clip/pretrained.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import os
3
+ import urllib
4
+ import warnings
5
+ from functools import partial
6
+ from typing import Dict, Union
7
+
8
+ from tqdm import tqdm
9
+
10
+ from .constants import (
11
+ IMAGENET_MEAN,
12
+ IMAGENET_STD,
13
+ INCEPTION_MEAN,
14
+ INCEPTION_STD,
15
+ OPENAI_DATASET_MEAN,
16
+ OPENAI_DATASET_STD,
17
+ )
18
+ from .version import __version__
19
+
20
+ try:
21
+ from huggingface_hub import hf_hub_download
22
+ hf_hub_download = partial(hf_hub_download, library_name="open_clip", library_version=__version__)
23
+ _has_hf_hub = True
24
+ except ImportError:
25
+ hf_hub_download = None
26
+ _has_hf_hub = False
27
+
28
+
29
+ def _pcfg(url='', hf_hub='', **kwargs):
30
+ # OpenAI / OpenCLIP defaults
31
+ return {
32
+ 'url': url,
33
+ 'hf_hub': hf_hub,
34
+ 'mean': OPENAI_DATASET_MEAN,
35
+ 'std': OPENAI_DATASET_STD,
36
+ 'interpolation': 'bicubic',
37
+ 'resize_mode': 'shortest',
38
+ **kwargs,
39
+ }
40
+
41
+
42
+ def _slpcfg(url='', hf_hub='', **kwargs):
43
+ # SiGLIP defaults
44
+ return {
45
+ 'url': url,
46
+ 'hf_hub': hf_hub,
47
+ 'mean': INCEPTION_MEAN,
48
+ 'std': INCEPTION_STD,
49
+ 'interpolation': 'bicubic',
50
+ 'resize_mode': 'squash',
51
+ **kwargs,
52
+ }
53
+
54
+
55
+ def _apcfg(url='', hf_hub='', **kwargs):
56
+ # CLIPA defaults
57
+ return {
58
+ 'url': url,
59
+ 'hf_hub': hf_hub,
60
+ 'mean': IMAGENET_MEAN,
61
+ 'std': IMAGENET_STD,
62
+ 'interpolation': 'bilinear',
63
+ 'resize_mode': 'squash',
64
+ **kwargs,
65
+ }
66
+
67
+
68
+ def _mccfg(url='', hf_hub='', **kwargs):
69
+ # MobileCLIP
70
+ return {
71
+ 'url': url,
72
+ 'hf_hub': hf_hub,
73
+ 'mean': (0., 0., 0.),
74
+ 'std': (1., 1., 1.),
75
+ 'interpolation': 'bilinear',
76
+ 'resize_mode': 'shortest',
77
+ **kwargs,
78
+ }
79
+
80
+
81
+
82
+ _RN50 = dict(
83
+ openai=_pcfg(
84
+ "https://openaipublic.azureedge.net/clip/models/afeb0e10f9e5a86da6080e35cf09123aca3b358a0c3e3b6c78a7b63bc04b6762/RN50.pt"),
85
+ yfcc15m=_pcfg(
86
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn50-quickgelu-yfcc15m-455df137.pt"),
87
+ cc12m=_pcfg(
88
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn50-quickgelu-cc12m-f000538c.pt"),
89
+ )
90
+
91
+ _RN50_quickgelu = dict(
92
+ openai=_pcfg(
93
+ "https://openaipublic.azureedge.net/clip/models/afeb0e10f9e5a86da6080e35cf09123aca3b358a0c3e3b6c78a7b63bc04b6762/RN50.pt"),
94
+ yfcc15m=_pcfg(
95
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn50-quickgelu-yfcc15m-455df137.pt"),
96
+ cc12m=_pcfg(
97
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn50-quickgelu-cc12m-f000538c.pt"),
98
+ )
99
+
100
+ _RN101 = dict(
101
+ openai=_pcfg(
102
+ "https://openaipublic.azureedge.net/clip/models/8fa8567bab74a42d41c5915025a8e4538c3bdbe8804a470a72f30b0d94fab599/RN101.pt"),
103
+ yfcc15m=_pcfg(
104
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn101-quickgelu-yfcc15m-3e04b30e.pt"),
105
+ )
106
+
107
+ _RN101_quickgelu = dict(
108
+ openai=_pcfg(
109
+ "https://openaipublic.azureedge.net/clip/models/8fa8567bab74a42d41c5915025a8e4538c3bdbe8804a470a72f30b0d94fab599/RN101.pt"),
110
+ yfcc15m=_pcfg(
111
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/rn101-quickgelu-yfcc15m-3e04b30e.pt"),
112
+ )
113
+
114
+ _RN50x4 = dict(
115
+ openai=_pcfg(
116
+ "https://openaipublic.azureedge.net/clip/models/7e526bd135e493cef0776de27d5f42653e6b4c8bf9e0f653bb11773263205fdd/RN50x4.pt"),
117
+ )
118
+
119
+ _RN50x16 = dict(
120
+ openai=_pcfg(
121
+ "https://openaipublic.azureedge.net/clip/models/52378b407f34354e150460fe41077663dd5b39c54cd0bfd2b27167a4a06ec9aa/RN50x16.pt"),
122
+ )
123
+
124
+ _RN50x64 = dict(
125
+ openai=_pcfg(
126
+ "https://openaipublic.azureedge.net/clip/models/be1cfb55d75a9666199fb2206c106743da0f6468c9d327f3e0d0a543a9919d9c/RN50x64.pt"),
127
+ )
128
+
129
+ _VITB32 = dict(
130
+ openai=_pcfg(
131
+ "https://openaipublic.azureedge.net/clip/models/40d365715913c9da98579312b702a82c18be219cc2a73407c4526f58eba950af/ViT-B-32.pt"),
132
+ laion400m_e31=_pcfg(
133
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_32-quickgelu-laion400m_e31-d867053b.pt"),
134
+ laion400m_e32=_pcfg(
135
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_32-quickgelu-laion400m_e32-46683a32.pt"),
136
+ laion2b_e16=_pcfg(
137
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_32-laion2b_e16-af8dbd0c.pth"),
138
+ laion2b_s34b_b79k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-laion2B-s34B-b79K/'),
139
+ # DataComp-XL models
140
+ datacomp_xl_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-DataComp.XL-s13B-b90K/'),
141
+ # DataComp-M models
142
+ datacomp_m_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-DataComp.M-s128M-b4K/'),
143
+ commonpool_m_clip_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M.clip-s128M-b4K/'),
144
+ commonpool_m_laion_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M.laion-s128M-b4K/'),
145
+ commonpool_m_image_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M.image-s128M-b4K/'),
146
+ commonpool_m_text_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M.text-s128M-b4K/'),
147
+ commonpool_m_basic_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M.basic-s128M-b4K/'),
148
+ commonpool_m_s128m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.M-s128M-b4K/'),
149
+ # DataComp-S models
150
+ datacomp_s_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-DataComp.S-s13M-b4K/'),
151
+ commonpool_s_clip_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S.clip-s13M-b4K/'),
152
+ commonpool_s_laion_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S.laion-s13M-b4K/'),
153
+ commonpool_s_image_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S.image-s13M-b4K/'),
154
+ commonpool_s_text_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S.text-s13M-b4K/'),
155
+ commonpool_s_basic_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S.basic-s13M-b4K/'),
156
+ commonpool_s_s13m_b4k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-CommonPool.S-s13M-b4K/'),
157
+ )
158
+
159
+ _VITB32_quickgelu = dict(
160
+ openai=_pcfg(
161
+ "https://openaipublic.azureedge.net/clip/models/40d365715913c9da98579312b702a82c18be219cc2a73407c4526f58eba950af/ViT-B-32.pt"),
162
+ laion400m_e31=_pcfg(
163
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_32-quickgelu-laion400m_e31-d867053b.pt"),
164
+ laion400m_e32=_pcfg(
165
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_32-quickgelu-laion400m_e32-46683a32.pt"),
166
+ metaclip_400m=_pcfg(
167
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt"),
168
+ metaclip_fullcc=_pcfg(
169
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_fullcc2.5b.pt"),
170
+ )
171
+
172
+ _VITB32_256 = dict(
173
+ datacomp_s34b_b86k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-256x256-DataComp-s34B-b86K/'),
174
+ )
175
+
176
+ _VITB16 = dict(
177
+ openai=_pcfg(
178
+ "https://openaipublic.azureedge.net/clip/models/5806e77cd80f8b59890b7e101eabd078d9fb84e6937f9e85e4ecb61988df416f/ViT-B-16.pt"),
179
+ laion400m_e31=_pcfg(
180
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_16-laion400m_e31-00efa78f.pt"),
181
+ laion400m_e32=_pcfg(
182
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_16-laion400m_e32-55e67d44.pt"),
183
+ laion2b_s34b_b88k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-laion2B-s34B-b88K/'),
184
+ # DataComp-XL models
185
+ datacomp_xl_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-DataComp.XL-s13B-b90K/'),
186
+ # DataComp-L models
187
+ datacomp_l_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-DataComp.L-s1B-b8K/'),
188
+ commonpool_l_clip_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L.clip-s1B-b8K/'),
189
+ commonpool_l_laion_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L.laion-s1B-b8K/'),
190
+ commonpool_l_image_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L.image-s1B-b8K/'),
191
+ commonpool_l_text_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L.text-s1B-b8K/'),
192
+ commonpool_l_basic_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L.basic-s1B-b8K/'),
193
+ commonpool_l_s1b_b8k=_pcfg(hf_hub='laion/CLIP-ViT-B-16-CommonPool.L-s1B-b8K/'),
194
+ # DFN
195
+ dfn2b=_pcfg(hf_hub='apple/DFN2B-CLIP-ViT-B-16/')
196
+ )
197
+
198
+ _VITB16_quickgelu = dict(
199
+ metaclip_400m=_pcfg(
200
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/b16_400m.pt"),
201
+ metaclip_fullcc=_pcfg(
202
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/b16_fullcc2.5b.pt"),
203
+ )
204
+
205
+ _VITB16_PLUS_240 = dict(
206
+ laion400m_e31=_pcfg(
207
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_16_plus_240-laion400m_e31-8fb26589.pt"),
208
+ laion400m_e32=_pcfg(
209
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_b_16_plus_240-laion400m_e32-699c4b84.pt"),
210
+ )
211
+
212
+ _VITL14 = dict(
213
+ openai=_pcfg(
214
+ "https://openaipublic.azureedge.net/clip/models/b8cca3fd41ae0c99ba7e8951adf17d267cdb84cd88be6f7c2e0eca1737a03836/ViT-L-14.pt"),
215
+ laion400m_e31=_pcfg(
216
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_l_14-laion400m_e31-69988bb6.pt"),
217
+ laion400m_e32=_pcfg(
218
+ "https://github.com/mlfoundations/open_clip/releases/download/v0.2-weights/vit_l_14-laion400m_e32-3d133497.pt"),
219
+ laion2b_s32b_b82k=_pcfg(
220
+ hf_hub='laion/CLIP-ViT-L-14-laion2B-s32B-b82K/',
221
+ mean=INCEPTION_MEAN, std=INCEPTION_STD),
222
+ # DataComp-XL models
223
+ datacomp_xl_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-L-14-DataComp.XL-s13B-b90K/'),
224
+ commonpool_xl_clip_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-L-14-CommonPool.XL.clip-s13B-b90K/'),
225
+ commonpool_xl_laion_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-L-14-CommonPool.XL.laion-s13B-b90K/'),
226
+ commonpool_xl_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-L-14-CommonPool.XL-s13B-b90K/'),
227
+ )
228
+
229
+ _VITL14_quickgelu = dict(
230
+ metaclip_400m=_pcfg(
231
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/l14_400m.pt"),
232
+ metaclip_fullcc=_pcfg(
233
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/l14_fullcc2.5b.pt"),
234
+ dfn2b=_pcfg(hf_hub='apple/DFN2B-CLIP-ViT-L-14/'),
235
+ )
236
+
237
+ _VITL14_336 = dict(
238
+ openai=_pcfg(
239
+ "https://openaipublic.azureedge.net/clip/models/3035c92b350959924f9f00213499208652fc7ea050643e8b385c2dac08641f02/ViT-L-14-336px.pt"),
240
+ )
241
+
242
+ _VITH14 = dict(
243
+ laion2b_s32b_b79k=_pcfg(hf_hub='laion/CLIP-ViT-H-14-laion2B-s32B-b79K/'),
244
+ )
245
+
246
+ _VITH14_quickgelu = dict(
247
+ metaclip_fullcc=_pcfg(
248
+ "https://dl.fbaipublicfiles.com/MMPT/metaclip/h14_fullcc2.5b.pt"),
249
+ dfn5b=_pcfg(
250
+ hf_hub='apple/DFN5B-CLIP-ViT-H-14/',
251
+ interpolation="bicubic",
252
+ resize_mode="squash"
253
+ ),
254
+ )
255
+
256
+ _VITH14_378_quickgelu = dict(
257
+ dfn5b=_pcfg(
258
+ hf_hub='apple/DFN5B-CLIP-ViT-H-14-378/',
259
+ interpolation="bicubic",
260
+ resize_mode="squash"
261
+ ),
262
+ )
263
+
264
+ _VITg14 = dict(
265
+ laion2b_s12b_b42k=_pcfg(hf_hub='laion/CLIP-ViT-g-14-laion2B-s12B-b42K/'),
266
+ laion2b_s34b_b88k=_pcfg(hf_hub='laion/CLIP-ViT-g-14-laion2B-s34B-b88K/'),
267
+ )
268
+
269
+ _VITbigG14 = dict(
270
+ laion2b_s39b_b160k=_pcfg(hf_hub='laion/CLIP-ViT-bigG-14-laion2B-39B-b160k/'),
271
+ )
272
+
273
+ _robertaViTB32 = dict(
274
+ laion2b_s12b_b32k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-roberta-base-laion2B-s12B-b32k/'),
275
+ )
276
+
277
+ _xlmRobertaBaseViTB32 = dict(
278
+ laion5b_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-B-32-xlm-roberta-base-laion5B-s13B-b90k/'),
279
+ )
280
+
281
+ _xlmRobertaLargeFrozenViTH14 = dict(
282
+ frozen_laion5b_s13b_b90k=_pcfg(hf_hub='laion/CLIP-ViT-H-14-frozen-xlm-roberta-large-laion5B-s13B-b90k/'),
283
+ )
284
+
285
+ _convnext_base = dict(
286
+ laion400m_s13b_b51k=_pcfg(hf_hub='laion/CLIP-convnext_base-laion400M-s13B-b51K/'),
287
+ )
288
+
289
+ _convnext_base_w = dict(
290
+ laion2b_s13b_b82k=_pcfg(hf_hub='laion/CLIP-convnext_base_w-laion2B-s13B-b82K/'),
291
+ laion2b_s13b_b82k_augreg=_pcfg(hf_hub='laion/CLIP-convnext_base_w-laion2B-s13B-b82K-augreg/'),
292
+ laion_aesthetic_s13b_b82k=_pcfg(hf_hub='laion/CLIP-convnext_base_w-laion_aesthetic-s13B-b82K/'),
293
+ )
294
+
295
+ _convnext_base_w_320 = dict(
296
+ laion_aesthetic_s13b_b82k=_pcfg(hf_hub='laion/CLIP-convnext_base_w_320-laion_aesthetic-s13B-b82K/'),
297
+ laion_aesthetic_s13b_b82k_augreg=_pcfg(hf_hub='laion/CLIP-convnext_base_w_320-laion_aesthetic-s13B-b82K-augreg/'),
298
+ )
299
+
300
+ _convnext_large_d = dict(
301
+ laion2b_s26b_b102k_augreg=_pcfg(hf_hub='laion/CLIP-convnext_large_d.laion2B-s26B-b102K-augreg/'),
302
+ )
303
+
304
+ _convnext_large_d_320 = dict(
305
+ laion2b_s29b_b131k_ft=_pcfg(hf_hub='laion/CLIP-convnext_large_d_320.laion2B-s29B-b131K-ft/'),
306
+ laion2b_s29b_b131k_ft_soup=_pcfg(hf_hub='laion/CLIP-convnext_large_d_320.laion2B-s29B-b131K-ft-soup/'),
307
+ )
308
+
309
+ _convnext_xxlarge = dict(
310
+ laion2b_s34b_b82k_augreg=_pcfg(hf_hub='laion/CLIP-convnext_xxlarge-laion2B-s34B-b82K-augreg/'),
311
+ laion2b_s34b_b82k_augreg_rewind=_pcfg(hf_hub='laion/CLIP-convnext_xxlarge-laion2B-s34B-b82K-augreg-rewind/'),
312
+ laion2b_s34b_b82k_augreg_soup=_pcfg(hf_hub='laion/CLIP-convnext_xxlarge-laion2B-s34B-b82K-augreg-soup/'),
313
+ )
314
+
315
+ _coca_VITB32 = dict(
316
+ laion2b_s13b_b90k=_pcfg(hf_hub='laion/CoCa-ViT-B-32-laion2B-s13B-b90k/'),
317
+ mscoco_finetuned_laion2b_s13b_b90k=_pcfg(hf_hub='laion/mscoco_finetuned_CoCa-ViT-B-32-laion2B-s13B-b90k/')
318
+ )
319
+
320
+ _coca_VITL14 = dict(
321
+ laion2b_s13b_b90k=_pcfg(hf_hub='laion/CoCa-ViT-L-14-laion2B-s13B-b90k/'),
322
+ mscoco_finetuned_laion2b_s13b_b90k=_pcfg(hf_hub='laion/mscoco_finetuned_CoCa-ViT-L-14-laion2B-s13B-b90k/')
323
+ )
324
+
325
+
326
+ _PRETRAINED = {
327
+ "RN50": _RN50,
328
+ "RN50-quickgelu": _RN50_quickgelu,
329
+ "RN101": _RN101,
330
+ "RN101-quickgelu": _RN101_quickgelu,
331
+ "RN50x4": _RN50x4,
332
+ "RN50x16": _RN50x16,
333
+ "RN50x64": _RN50x64,
334
+
335
+ "ViT-B-32": _VITB32,
336
+ "ViT-B-32-256": _VITB32_256,
337
+ "ViT-B-32-quickgelu": _VITB32_quickgelu,
338
+ "ViT-B-16": _VITB16,
339
+ "ViT-B-16-quickgelu": _VITB16_quickgelu,
340
+ "ViT-B-16-plus-240": _VITB16_PLUS_240,
341
+ "ViT-L-14": _VITL14,
342
+ "ViT-L-14-quickgelu": _VITL14_quickgelu,
343
+ "ViT-L-14-336": _VITL14_336,
344
+ "ViT-H-14": _VITH14,
345
+ "ViT-H-14-quickgelu": _VITH14_quickgelu,
346
+ "ViT-H-14-378-quickgelu": _VITH14_378_quickgelu,
347
+ "ViT-g-14": _VITg14,
348
+ "ViT-bigG-14": _VITbigG14,
349
+
350
+ "roberta-ViT-B-32": _robertaViTB32,
351
+ "xlm-roberta-base-ViT-B-32": _xlmRobertaBaseViTB32,
352
+ "xlm-roberta-large-ViT-H-14": _xlmRobertaLargeFrozenViTH14,
353
+
354
+ "convnext_base": _convnext_base,
355
+ "convnext_base_w": _convnext_base_w,
356
+ "convnext_base_w_320": _convnext_base_w_320,
357
+ "convnext_large_d": _convnext_large_d,
358
+ "convnext_large_d_320": _convnext_large_d_320,
359
+ "convnext_xxlarge": _convnext_xxlarge,
360
+
361
+ "coca_ViT-B-32": _coca_VITB32,
362
+ "coca_ViT-L-14": _coca_VITL14,
363
+
364
+ "EVA01-g-14": dict(
365
+ # from QuanSun/EVA-CLIP/EVA01_CLIP_g_14_psz14_s11B.pt
366
+ laion400m_s11b_b41k=_pcfg(hf_hub='timm/eva_giant_patch14_clip_224.laion400m_s11b_b41k/'),
367
+ ),
368
+ "EVA01-g-14-plus": dict(
369
+ # from QuanSun/EVA-CLIP/EVA01_CLIP_g_14_plus_psz14_s11B.pt
370
+ merged2b_s11b_b114k=_pcfg(hf_hub='timm/eva_giant_patch14_plus_clip_224.merged2b_s11b_b114k/'),
371
+ ),
372
+ "EVA02-B-16": dict(
373
+ # from QuanSun/EVA-CLIP/EVA02_CLIP_B_psz16_s8B.pt
374
+ merged2b_s8b_b131k=_pcfg(hf_hub='timm/eva02_base_patch16_clip_224.merged2b_s8b_b131k/'),
375
+ ),
376
+ "EVA02-L-14": dict(
377
+ # from QuanSun/EVA-CLIP/EVA02_CLIP_L_psz14_s4B.pt
378
+ merged2b_s4b_b131k=_pcfg(hf_hub='timm/eva02_large_patch14_clip_224.merged2b_s4b_b131k/'),
379
+ ),
380
+ "EVA02-L-14-336": dict(
381
+ # from QuanSun/EVA-CLIP/EVA02_CLIP_L_336_psz14_s6B.pt
382
+ merged2b_s6b_b61k=_pcfg(hf_hub='timm/eva02_large_patch14_clip_336.merged2b_s6b_b61k/'),
383
+ ),
384
+ "EVA02-E-14": dict(
385
+ # from QuanSun/EVA-CLIP/EVA02_CLIP_E_psz14_s4B.pt
386
+ laion2b_s4b_b115k=_pcfg(hf_hub='timm/eva02_enormous_patch14_clip_224.laion2b_s4b_b115k/'),
387
+ ),
388
+ "EVA02-E-14-plus": dict(
389
+ # from QuanSun/EVA-CLIP/EVA02_CLIP_E_psz14_plus_s9B.pt
390
+ laion2b_s9b_b144k=_pcfg(hf_hub='timm/eva02_enormous_patch14_plus_clip_224.laion2b_s9b_b144k/'),
391
+ ),
392
+
393
+ "ViT-B-16-SigLIP": dict(
394
+ webli=_slpcfg(hf_hub='timm/ViT-B-16-SigLIP/'),
395
+ ),
396
+ "ViT-B-16-SigLIP-256": dict(
397
+ webli=_slpcfg(hf_hub='timm/ViT-B-16-SigLIP-256/'),
398
+ ),
399
+ "ViT-B-16-SigLIP-i18n-256": dict(
400
+ webli=_slpcfg(hf_hub='timm/ViT-B-16-SigLIP-i18n-256/'),
401
+ ),
402
+ "ViT-B-16-SigLIP-384": dict(
403
+ webli=_slpcfg(hf_hub='timm/ViT-B-16-SigLIP-384/'),
404
+ ),
405
+ "ViT-B-16-SigLIP-512": dict(
406
+ webli=_slpcfg(hf_hub='timm/ViT-B-16-SigLIP-512/'),
407
+ ),
408
+ "ViT-L-16-SigLIP-256": dict(
409
+ webli=_slpcfg(hf_hub='timm/ViT-L-16-SigLIP-256/'),
410
+ ),
411
+ "ViT-L-16-SigLIP-384": dict(
412
+ webli=_slpcfg(hf_hub='timm/ViT-L-16-SigLIP-384/'),
413
+ ),
414
+ "ViT-SO400M-14-SigLIP": dict(
415
+ webli=_slpcfg(hf_hub='timm/ViT-SO400M-14-SigLIP/'),
416
+ ),
417
+ "ViT-SO400M-14-SigLIP-384": dict(
418
+ webli=_slpcfg(hf_hub='timm/ViT-SO400M-14-SigLIP-384/'),
419
+ ),
420
+
421
+ "ViT-L-14-CLIPA": dict(
422
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-L-14-CLIPA-datacomp1B/'),
423
+ ),
424
+ "ViT-L-14-CLIPA-336": dict(
425
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-L-14-CLIPA-336-datacomp1B/'),
426
+ ),
427
+ "ViT-H-14-CLIPA": dict(
428
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-H-14-CLIPA-datacomp1B/'),
429
+ ),
430
+ "ViT-H-14-CLIPA-336": dict(
431
+ laion2b=_apcfg(hf_hub='UCSC-VLAA/ViT-H-14-CLIPA-336-laion2B/'),
432
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-H-14-CLIPA-336-datacomp1B/'),
433
+ ),
434
+ "ViT-bigG-14-CLIPA": dict(
435
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-bigG-14-CLIPA-datacomp1B/'),
436
+ ),
437
+ "ViT-bigG-14-CLIPA-336": dict(
438
+ datacomp1b=_apcfg(hf_hub='UCSC-VLAA/ViT-bigG-14-CLIPA-336-datacomp1B/'),
439
+ ),
440
+
441
+ "nllb-clip-base": dict(
442
+ v1=_pcfg(hf_hub='visheratin/nllb-clip-base-oc/'),
443
+ ),
444
+ "nllb-clip-large": dict(
445
+ v1=_pcfg(hf_hub='visheratin/nllb-clip-large-oc/'),
446
+ ),
447
+
448
+ "nllb-clip-base-siglip": dict(
449
+ v1=_slpcfg(hf_hub='visheratin/nllb-clip-base-siglip/'),
450
+ mrl=_slpcfg(hf_hub='visheratin/nllb-siglip-mrl-base/'),
451
+ ),
452
+ "nllb-clip-large-siglip": dict(
453
+ v1=_slpcfg(hf_hub='visheratin/nllb-clip-large-siglip/'),
454
+ mrl=_slpcfg(hf_hub='visheratin/nllb-siglip-mrl-large/'),
455
+ ),
456
+
457
+ "MobileCLIP-S1": dict(
458
+ datacompdr=_mccfg(hf_hub='apple/MobileCLIP-S1-OpenCLIP/')),
459
+ "MobileCLIP-S2": dict(
460
+ datacompdr=_mccfg(hf_hub='apple/MobileCLIP-S2-OpenCLIP/')),
461
+ "MobileCLIP-B": dict(
462
+ datacompdr=_mccfg(hf_hub='apple/MobileCLIP-B-OpenCLIP/'),
463
+ datacompdr_lt=_mccfg(hf_hub='apple/MobileCLIP-B-LT-OpenCLIP/'),
464
+ ),
465
+
466
+ "ViTamin-S": dict(
467
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-S/pytorch_model.bin'),
468
+ ),
469
+ "ViTamin-S-LTT": dict(
470
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-S-LTT/pytorch_model.bin'),
471
+ ),
472
+ "ViTamin-B": dict(
473
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-B/pytorch_model.bin'),
474
+ ),
475
+ "ViTamin-B-LTT": dict(
476
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-B-LTT/pytorch_model.bin'),
477
+ ),
478
+ "ViTamin-L": dict(
479
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L-224px/pytorch_model.bin'),
480
+ ),
481
+ "ViTamin-L-256": dict(
482
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L-256px/pytorch_model.bin'),
483
+ ),
484
+ "ViTamin-L-336": dict(
485
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L-336px/pytorch_model.bin'),
486
+ ),
487
+ "ViTamin-L-384": dict(
488
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L-384px/pytorch_model.bin'),
489
+ ),
490
+ "ViTamin-L2": dict(
491
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L2-224px/pytorch_model.bin'),
492
+ ),
493
+ "ViTamin-L2-256": dict(
494
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L2-256px/pytorch_model.bin'),
495
+ ),
496
+ "ViTamin-L2-336": dict(
497
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L2-336px/pytorch_model.bin'),
498
+ ),
499
+ "ViTamin-L2-384": dict(
500
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-L2-384px/pytorch_model.bin'),
501
+ ),
502
+ "ViTamin-XL-256": dict(
503
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-XL-256px/pytorch_model.bin'),
504
+ ),
505
+ "ViTamin-XL-336": dict(
506
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-XL-336px/pytorch_model.bin'),
507
+ ),
508
+ "ViTamin-XL-384": dict(
509
+ datacomp1b=_pcfg(hf_hub='jienengchen/ViTamin-XL-384px/pytorch_model.bin'),
510
+ ),
511
+ }
512
+
513
+
514
+ def _clean_tag(tag: str):
515
+ # normalize pretrained tags
516
+ return tag.lower().replace('-', '_')
517
+
518
+
519
+ def list_pretrained(as_str: bool = False):
520
+ """ returns list of pretrained models
521
+ Returns a tuple (model_name, pretrain_tag) by default or 'name:tag' if as_str == True
522
+ """
523
+ return [':'.join([k, t]) if as_str else (k, t) for k in _PRETRAINED.keys() for t in _PRETRAINED[k].keys()]
524
+
525
+
526
+ def list_pretrained_models_by_tag(tag: str):
527
+ """ return all models having the specified pretrain tag """
528
+ models = []
529
+ tag = _clean_tag(tag)
530
+ for k in _PRETRAINED.keys():
531
+ if tag in _PRETRAINED[k]:
532
+ models.append(k)
533
+ return models
534
+
535
+
536
+ def list_pretrained_tags_by_model(model: str):
537
+ """ return all pretrain tags for the specified model architecture """
538
+ tags = []
539
+ if model in _PRETRAINED:
540
+ tags.extend(_PRETRAINED[model].keys())
541
+ return tags
542
+
543
+
544
+ def is_pretrained_cfg(model: str, tag: str):
545
+ if model not in _PRETRAINED:
546
+ return False
547
+ return _clean_tag(tag) in _PRETRAINED[model]
548
+
549
+
550
+ def get_pretrained_cfg(model: str, tag: str):
551
+ if model not in _PRETRAINED:
552
+ return {}
553
+ model_pretrained = _PRETRAINED[model]
554
+ return model_pretrained.get(_clean_tag(tag), {})
555
+
556
+
557
+ def get_pretrained_url(model: str, tag: str):
558
+ cfg = get_pretrained_cfg(model, _clean_tag(tag))
559
+ return cfg.get('url', '')
560
+
561
+
562
+ def download_pretrained_from_url(
563
+ url: str,
564
+ cache_dir: Union[str, None] = None,
565
+ ):
566
+ if not cache_dir:
567
+ cache_dir = os.path.expanduser("~/.cache/clip")
568
+ os.makedirs(cache_dir, exist_ok=True)
569
+ filename = os.path.basename(url)
570
+
571
+ if 'openaipublic' in url:
572
+ expected_sha256 = url.split("/")[-2]
573
+ elif 'mlfoundations' in url:
574
+ expected_sha256 = os.path.splitext(filename)[0].split("-")[-1]
575
+ else:
576
+ expected_sha256 = ''
577
+
578
+ download_target = os.path.join(cache_dir, filename)
579
+
580
+ if os.path.exists(download_target) and not os.path.isfile(download_target):
581
+ raise RuntimeError(f"{download_target} exists and is not a regular file")
582
+
583
+ if os.path.isfile(download_target):
584
+ if expected_sha256:
585
+ if hashlib.sha256(open(download_target, "rb").read()).hexdigest().startswith(expected_sha256):
586
+ return download_target
587
+ else:
588
+ warnings.warn(f"{download_target} exists, but the SHA256 checksum does not match; re-downloading the file")
589
+ else:
590
+ return download_target
591
+
592
+ with urllib.request.urlopen(url) as source, open(download_target, "wb") as output:
593
+ with tqdm(total=int(source.headers.get("Content-Length")), ncols=80, unit='iB', unit_scale=True) as loop:
594
+ while True:
595
+ buffer = source.read(8192)
596
+ if not buffer:
597
+ break
598
+
599
+ output.write(buffer)
600
+ loop.update(len(buffer))
601
+
602
+ if expected_sha256 and not hashlib.sha256(open(download_target, "rb").read()).hexdigest().startswith(expected_sha256):
603
+ raise RuntimeError(f"Model has been downloaded but the SHA256 checksum does not not match")
604
+
605
+ return download_target
606
+
607
+
608
+ def has_hf_hub(necessary=False):
609
+ if not _has_hf_hub and necessary:
610
+ # if no HF Hub module installed, and it is necessary to continue, raise error
611
+ raise RuntimeError(
612
+ 'Hugging Face hub model specified but package not installed. Run `pip install huggingface_hub`.')
613
+ return _has_hf_hub
614
+
615
+
616
+ def download_pretrained_from_hf(
617
+ model_id: str,
618
+ filename: str = 'open_clip_pytorch_model.bin',
619
+ revision=None,
620
+ cache_dir: Union[str, None] = None,
621
+ ):
622
+ has_hf_hub(True)
623
+ cached_file = hf_hub_download(model_id, filename, revision=revision, cache_dir=cache_dir)
624
+ return cached_file
625
+
626
+
627
+ def download_pretrained(
628
+ cfg: Dict,
629
+ force_hf_hub: bool = False,
630
+ cache_dir: Union[str, None] = None,
631
+ ):
632
+ target = ''
633
+ if not cfg:
634
+ return target
635
+
636
+ download_url = cfg.get('url', '')
637
+ download_hf_hub = cfg.get('hf_hub', '')
638
+ if download_hf_hub and force_hf_hub:
639
+ # use HF hub even if url exists
640
+ download_url = ''
641
+
642
+ if download_url:
643
+ target = download_pretrained_from_url(download_url, cache_dir=cache_dir)
644
+ elif download_hf_hub:
645
+ has_hf_hub(True)
646
+ # we assume the hf_hub entries in pretrained config combine model_id + filename in
647
+ # 'org/model_name/filename.pt' form. To specify just the model id w/o filename and
648
+ # use 'open_clip_pytorch_model.bin' default, there must be a trailing slash 'org/model_name/'.
649
+ model_id, filename = os.path.split(download_hf_hub)
650
+ if filename:
651
+ target = download_pretrained_from_hf(model_id, filename=filename, cache_dir=cache_dir)
652
+ else:
653
+ target = download_pretrained_from_hf(model_id, cache_dir=cache_dir)
654
+
655
+ return target
ecg_coca/open_clip/push_to_hf_hub.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+ from typing import Optional, Tuple, Union
7
+
8
+ import torch
9
+
10
+ try:
11
+ from huggingface_hub import (
12
+ create_repo,
13
+ get_hf_file_metadata,
14
+ hf_hub_download,
15
+ hf_hub_url,
16
+ repo_type_and_id_from_hf_id,
17
+ upload_folder,
18
+ list_repo_files,
19
+ )
20
+ from huggingface_hub.utils import EntryNotFoundError
21
+ _has_hf_hub = True
22
+ except ImportError:
23
+ _has_hf_hub = False
24
+
25
+ try:
26
+ import safetensors.torch
27
+ _has_safetensors = True
28
+ except ImportError:
29
+ _has_safetensors = False
30
+
31
+ from .factory import create_model_from_pretrained, get_model_config, get_tokenizer
32
+ from .tokenizer import HFTokenizer
33
+
34
+ # Default name for a weights file hosted on the Huggingface Hub.
35
+ HF_WEIGHTS_NAME = "open_clip_pytorch_model.bin" # default pytorch pkl
36
+ HF_SAFE_WEIGHTS_NAME = "open_clip_model.safetensors" # safetensors version
37
+ HF_CONFIG_NAME = 'open_clip_config.json'
38
+
39
+
40
+ def save_config_for_hf(
41
+ model,
42
+ config_path: str,
43
+ model_config: Optional[dict]
44
+ ):
45
+ preprocess_cfg = {
46
+ 'mean': model.visual.image_mean,
47
+ 'std': model.visual.image_std,
48
+ }
49
+ other_pp = getattr(model.visual, 'preprocess_cfg', {})
50
+ if 'interpolation' in other_pp:
51
+ preprocess_cfg['interpolation'] = other_pp['interpolation']
52
+ if 'resize_mode' in other_pp:
53
+ preprocess_cfg['resize_mode'] = other_pp['resize_mode']
54
+ hf_config = {
55
+ 'model_cfg': model_config,
56
+ 'preprocess_cfg': preprocess_cfg,
57
+ }
58
+
59
+ with config_path.open('w') as f:
60
+ json.dump(hf_config, f, indent=2)
61
+
62
+
63
+ def save_for_hf(
64
+ model,
65
+ tokenizer: HFTokenizer,
66
+ model_config: dict,
67
+ save_directory: str,
68
+ safe_serialization: Union[bool, str] = 'both',
69
+ skip_weights : bool = False,
70
+ ):
71
+ config_filename = HF_CONFIG_NAME
72
+
73
+ save_directory = Path(save_directory)
74
+ save_directory.mkdir(exist_ok=True, parents=True)
75
+
76
+ if not skip_weights:
77
+ tensors = model.state_dict()
78
+ if safe_serialization is True or safe_serialization == "both":
79
+ assert _has_safetensors, "`pip install safetensors` to use .safetensors"
80
+ safetensors.torch.save_file(tensors, save_directory / HF_SAFE_WEIGHTS_NAME)
81
+ if safe_serialization is False or safe_serialization == "both":
82
+ torch.save(tensors, save_directory / HF_WEIGHTS_NAME)
83
+
84
+ tokenizer.save_pretrained(save_directory)
85
+
86
+ config_path = save_directory / config_filename
87
+ save_config_for_hf(model, config_path, model_config=model_config)
88
+
89
+
90
+ def push_to_hf_hub(
91
+ model,
92
+ tokenizer,
93
+ model_config: Optional[dict],
94
+ repo_id: str,
95
+ commit_message: str = 'Add model',
96
+ token: Optional[str] = None,
97
+ revision: Optional[str] = None,
98
+ private: bool = False,
99
+ create_pr: bool = False,
100
+ model_card: Optional[dict] = None,
101
+ safe_serialization: Union[bool, str] = 'both',
102
+ ):
103
+ if not isinstance(tokenizer, HFTokenizer):
104
+ # FIXME this makes it awkward to push models with new tokenizers, come up with better soln.
105
+ # default CLIP tokenizers use https://huggingface.co/openai/clip-vit-large-patch14
106
+ tokenizer = HFTokenizer('openai/clip-vit-large-patch14')
107
+
108
+ # Create repo if it doesn't exist yet
109
+ repo_url = create_repo(repo_id, token=token, private=private, exist_ok=True)
110
+
111
+ # Infer complete repo_id from repo_url
112
+ # Can be different from the input `repo_id` if repo_owner was implicit
113
+ _, repo_owner, repo_name = repo_type_and_id_from_hf_id(repo_url)
114
+ repo_id = f"{repo_owner}/{repo_name}"
115
+
116
+ # Check if repo already exists and determine what needs updating
117
+ repo_exists = False
118
+ repo_files = {}
119
+ try:
120
+ repo_files = set(list_repo_files(repo_id))
121
+ repo_exists = True
122
+ except Exception as e:
123
+ print('Repo does not exist', e)
124
+
125
+ try:
126
+ get_hf_file_metadata(hf_hub_url(repo_id=repo_id, filename="README.md", revision=revision))
127
+ has_readme = True
128
+ except EntryNotFoundError:
129
+ has_readme = False
130
+
131
+ # Dump model and push to Hub
132
+ with TemporaryDirectory() as tmpdir:
133
+ # Save model weights and config.
134
+ save_for_hf(
135
+ model,
136
+ tokenizer=tokenizer,
137
+ model_config=model_config,
138
+ save_directory=tmpdir,
139
+ safe_serialization=safe_serialization,
140
+ )
141
+
142
+ # Add readme if it does not exist
143
+ if not has_readme:
144
+ model_card = model_card or {}
145
+ model_name = repo_id.split('/')[-1]
146
+ readme_path = Path(tmpdir) / "README.md"
147
+ readme_text = generate_readme(model_card, model_name)
148
+ readme_path.write_text(readme_text)
149
+
150
+ # Upload model and return
151
+ return upload_folder(
152
+ repo_id=repo_id,
153
+ folder_path=tmpdir,
154
+ revision=revision,
155
+ create_pr=create_pr,
156
+ commit_message=commit_message,
157
+ )
158
+
159
+
160
+ def push_pretrained_to_hf_hub(
161
+ model_name,
162
+ pretrained: str,
163
+ repo_id: str,
164
+ precision: str = 'fp32',
165
+ image_mean: Optional[Tuple[float, ...]] = None,
166
+ image_std: Optional[Tuple[float, ...]] = None,
167
+ image_interpolation: Optional[str] = None,
168
+ image_resize_mode: Optional[str] = None, # only effective for inference
169
+ commit_message: str = 'Add model',
170
+ token: Optional[str] = None,
171
+ revision: Optional[str] = None,
172
+ private: bool = False,
173
+ create_pr: bool = False,
174
+ model_card: Optional[dict] = None,
175
+ hf_tokenizer_self: bool = False,
176
+ **kwargs,
177
+ ):
178
+ model, preprocess_eval = create_model_from_pretrained(
179
+ model_name,
180
+ pretrained=pretrained,
181
+ precision=precision,
182
+ image_mean=image_mean,
183
+ image_std=image_std,
184
+ image_interpolation=image_interpolation,
185
+ image_resize_mode=image_resize_mode,
186
+ **kwargs,
187
+ )
188
+ model_config = get_model_config(model_name)
189
+ if pretrained == 'openai':
190
+ model_config['quick_gelu'] = True
191
+ assert model_config
192
+
193
+ tokenizer = get_tokenizer(model_name)
194
+ if hf_tokenizer_self:
195
+ # make hf tokenizer config in the uploaded model point to self instead of original location
196
+ model_config['text']['hf_tokenizer_name'] = repo_id
197
+
198
+ push_to_hf_hub(
199
+ model=model,
200
+ tokenizer=tokenizer,
201
+ model_config=model_config,
202
+ repo_id=repo_id,
203
+ commit_message=commit_message,
204
+ token=token,
205
+ revision=revision,
206
+ private=private,
207
+ create_pr=create_pr,
208
+ model_card=model_card,
209
+ safe_serialization='both',
210
+ )
211
+
212
+
213
+ def generate_readme(model_card: dict, model_name: str):
214
+ tags = model_card.pop('tags', ('clip',))
215
+ pipeline_tag = model_card.pop('pipeline_tag', 'zero-shot-image-classification')
216
+ readme_text = "---\n"
217
+ if tags:
218
+ readme_text += "tags:\n"
219
+ for t in tags:
220
+ readme_text += f"- {t}\n"
221
+ readme_text += "library_name: open_clip\n"
222
+ readme_text += f"pipeline_tag: {pipeline_tag}\n"
223
+ readme_text += f"license: {model_card.get('license', 'mit')}\n"
224
+ if 'details' in model_card and 'Dataset' in model_card['details']:
225
+ readme_text += 'datasets:\n'
226
+ readme_text += f"- {model_card['details']['Dataset'].lower()}\n"
227
+ readme_text += "---\n"
228
+ readme_text += f"# Model card for {model_name}\n"
229
+ if 'description' in model_card:
230
+ readme_text += f"\n{model_card['description']}\n"
231
+ if 'details' in model_card:
232
+ readme_text += f"\n## Model Details\n"
233
+ for k, v in model_card['details'].items():
234
+ if isinstance(v, (list, tuple)):
235
+ readme_text += f"- **{k}:**\n"
236
+ for vi in v:
237
+ readme_text += f" - {vi}\n"
238
+ elif isinstance(v, dict):
239
+ readme_text += f"- **{k}:**\n"
240
+ for ki, vi in v.items():
241
+ readme_text += f" - {ki}: {vi}\n"
242
+ else:
243
+ readme_text += f"- **{k}:** {v}\n"
244
+ if 'usage' in model_card:
245
+ readme_text += f"\n## Model Usage\n"
246
+ readme_text += model_card['usage']
247
+ readme_text += '\n'
248
+
249
+ if 'comparison' in model_card:
250
+ readme_text += f"\n## Model Comparison\n"
251
+ readme_text += model_card['comparison']
252
+ readme_text += '\n'
253
+
254
+ if 'citation' in model_card:
255
+ readme_text += f"\n## Citation\n"
256
+ if not isinstance(model_card['citation'], (list, tuple)):
257
+ citations = [model_card['citation']]
258
+ else:
259
+ citations = model_card['citation']
260
+ for c in citations:
261
+ readme_text += f"```bibtex\n{c}\n```\n"
262
+
263
+ return readme_text
264
+
265
+
266
+ if __name__ == "__main__":
267
+ parser = argparse.ArgumentParser(description="Push to Hugging Face Hub")
268
+ parser.add_argument(
269
+ "--model", type=str, help="Name of the model to use.",
270
+ )
271
+ parser.add_argument(
272
+ "--pretrained", type=str,
273
+ help="Use a pretrained CLIP model weights with the specified tag or file path.",
274
+ )
275
+ parser.add_argument(
276
+ "--repo-id", type=str,
277
+ help="Destination HF Hub repo-id ie 'organization/model_id'.",
278
+ )
279
+ parser.add_argument(
280
+ "--precision", type=str, default='fp32',
281
+ )
282
+ parser.add_argument(
283
+ '--image-mean', type=float, nargs='+', default=None, metavar='MEAN',
284
+ help='Override default image mean value of dataset')
285
+ parser.add_argument(
286
+ '--image-std', type=float, nargs='+', default=None, metavar='STD',
287
+ help='Override default image std deviation of of dataset')
288
+ parser.add_argument(
289
+ '--image-interpolation',
290
+ default=None, type=str, choices=['bicubic', 'bilinear', 'random'],
291
+ help="image resize interpolation"
292
+ )
293
+ parser.add_argument(
294
+ '--image-resize-mode',
295
+ default=None, type=str, choices=['shortest', 'longest', 'squash'],
296
+ help="image resize mode during inference"
297
+ )
298
+ parser.add_argument(
299
+ "--hf-tokenizer-self",
300
+ default=False,
301
+ action="store_true",
302
+ help="make hf_tokenizer_name point in uploaded config point to itself"
303
+ )
304
+ args = parser.parse_args()
305
+
306
+ print(f'Saving model {args.model} with pretrained weights {args.pretrained} to Hugging Face Hub at {args.repo_id}')
307
+
308
+ # FIXME add support to pass model_card json / template from file via cmd line
309
+
310
+ push_pretrained_to_hf_hub(
311
+ args.model,
312
+ args.pretrained,
313
+ args.repo_id,
314
+ precision=args.precision,
315
+ image_mean=args.image_mean, # override image mean/std if trained w/ non defaults
316
+ image_std=args.image_std,
317
+ image_interpolation=args.image_interpolation,
318
+ image_resize_mode=args.image_resize_mode,
319
+ )
320
+
321
+ print(f'{args.model} saved.')
ecg_coca/open_clip/timm_model.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ timm model adapter
2
+
3
+ Wraps timm (https://github.com/rwightman/pytorch-image-models) models for use as a vision tower in CLIP model.
4
+ """
5
+ import logging
6
+ from collections import OrderedDict
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+ try:
12
+ import timm
13
+ from timm.models.layers import Mlp, to_2tuple
14
+ try:
15
+ # old timm imports < 0.8.1
16
+ from timm.models.layers.attention_pool2d import RotAttentionPool2d
17
+ from timm.models.layers.attention_pool2d import AttentionPool2d as AbsAttentionPool2d
18
+ except ImportError:
19
+ # new timm imports >= 0.8.1
20
+ from timm.layers import RotAttentionPool2d
21
+ from timm.layers import AttentionPool2d as AbsAttentionPool2d
22
+ except ImportError:
23
+ timm = None
24
+
25
+ from .utils import freeze_batch_norm_2d
26
+
27
+
28
+ class TimmModel(nn.Module):
29
+ """ timm model adapter
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ model_name,
35
+ embed_dim,
36
+ image_size=224,
37
+ pool='avg',
38
+ proj='linear',
39
+ proj_bias=False,
40
+ drop=0.,
41
+ drop_path=None,
42
+ patch_drop=None,
43
+ pretrained=False,
44
+ ):
45
+ super().__init__()
46
+ if timm is None:
47
+ raise RuntimeError("Please `pip install timm` to use timm models.")
48
+ self.image_size = to_2tuple(image_size)
49
+
50
+ # setup kwargs that may not be common across all models
51
+ timm_kwargs = {}
52
+ if drop_path is not None:
53
+ timm_kwargs['drop_path_rate'] = drop_path
54
+ if patch_drop is not None:
55
+ timm_kwargs['patch_drop_rate'] = patch_drop
56
+
57
+ custom_pool = pool in ('abs_attn', 'rot_attn')
58
+ if proj:
59
+ assert proj in ("linear", "mlp", "none")
60
+ extra_proj = proj in ("linear", "mlp")
61
+ if not extra_proj and not custom_pool:
62
+ # use network classifier head as projection if no proj specified and no custom pooling used
63
+ # if projection is explicitly set to "none" will be pass through from network trunk
64
+ proj_dim = 0 if proj == 'none' else embed_dim
65
+ self.trunk = timm.create_model(
66
+ model_name,
67
+ num_classes=proj_dim,
68
+ global_pool=pool,
69
+ pretrained=pretrained,
70
+ **timm_kwargs,
71
+ )
72
+ prev_chs = embed_dim
73
+ else:
74
+ self.trunk = timm.create_model(
75
+ model_name,
76
+ pretrained=pretrained,
77
+ **timm_kwargs,
78
+ )
79
+ feat_size = self.trunk.default_cfg.get('pool_size', None)
80
+ feature_ndim = 1 if not feat_size else 2
81
+ if custom_pool:
82
+ assert feature_ndim == 2
83
+ # if attn pooling used, remove both classifier and default pool
84
+ self.trunk.reset_classifier(0, global_pool='')
85
+ else:
86
+ # reset global pool if pool config set, otherwise leave as network default
87
+ reset_kwargs = dict(global_pool=pool) if pool else {}
88
+ self.trunk.reset_classifier(0, **reset_kwargs)
89
+ prev_chs = self.trunk.num_features
90
+
91
+ head_layers = OrderedDict()
92
+
93
+ # Add custom pooling to head
94
+ if pool == 'abs_attn':
95
+ head_layers['pool'] = AbsAttentionPool2d(prev_chs, feat_size=feat_size, out_features=embed_dim)
96
+ prev_chs = embed_dim
97
+ elif pool == 'rot_attn':
98
+ head_layers['pool'] = RotAttentionPool2d(prev_chs, out_features=embed_dim)
99
+ prev_chs = embed_dim
100
+
101
+ # NOTE attention pool ends with a projection layer, so proj should usually be set to '' if such pooling is used
102
+ if proj == 'linear':
103
+ head_layers['drop'] = nn.Dropout(drop)
104
+ head_layers['proj'] = nn.Linear(prev_chs, embed_dim, bias=proj_bias)
105
+ elif proj == 'mlp':
106
+ head_layers['mlp'] = Mlp(prev_chs, 2 * embed_dim, embed_dim, drop=(drop, 0), bias=(True, proj_bias))
107
+
108
+ self.head = nn.Sequential(head_layers)
109
+
110
+ def lock(self, unlocked_groups=0, freeze_bn_stats=False):
111
+ """ lock modules
112
+ Args:
113
+ unlocked_groups (int): leave last n layer groups unlocked (default: 0)
114
+ """
115
+ if not unlocked_groups:
116
+ # lock full model
117
+ for param in self.trunk.parameters():
118
+ param.requires_grad = False
119
+ if freeze_bn_stats:
120
+ freeze_batch_norm_2d(self.trunk)
121
+ else:
122
+ # NOTE: partial freeze requires latest timm (master) branch and is subject to change
123
+ try:
124
+ # FIXME import here until API stable and in an official release
125
+ from timm.models.helpers import group_parameters, group_modules
126
+ except ImportError:
127
+ raise RuntimeError(
128
+ 'Please install latest timm `pip install git+https://github.com/rwightman/pytorch-image-models`')
129
+ matcher = self.trunk.group_matcher()
130
+ gparams = group_parameters(self.trunk, matcher)
131
+ max_layer_id = max(gparams.keys())
132
+ max_layer_id = max_layer_id - unlocked_groups
133
+ for group_idx in range(max_layer_id + 1):
134
+ group = gparams[group_idx]
135
+ for param in group:
136
+ self.trunk.get_parameter(param).requires_grad = False
137
+ if freeze_bn_stats:
138
+ gmodules = group_modules(self.trunk, matcher, reverse=True)
139
+ gmodules = {k for k, v in gmodules.items() if v <= max_layer_id}
140
+ freeze_batch_norm_2d(self.trunk, gmodules)
141
+
142
+ @torch.jit.ignore
143
+ def set_grad_checkpointing(self, enable=True):
144
+ try:
145
+ self.trunk.set_grad_checkpointing(enable)
146
+ except Exception as e:
147
+ logging.warning('grad checkpointing not supported for this timm image tower, continuing without...')
148
+
149
+ def forward(self, x):
150
+ x = self.trunk(x)
151
+ x = self.head(x)
152
+ return x
ecg_coca/open_clip/tokenizer.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ CLIP tokenizer
2
+
3
+ Copied from https://github.com/openai/CLIP. Originally MIT License, Copyright (c) 2021 OpenAI.
4
+ """
5
+ import gzip
6
+ import html
7
+ import os
8
+ import random
9
+ import string
10
+ from functools import lru_cache, partial
11
+ from typing import Callable, List, Optional, Union
12
+ import warnings
13
+
14
+ import ftfy
15
+ import numpy as np
16
+ import regex as re
17
+ import torch
18
+
19
+ # https://stackoverflow.com/q/62691279
20
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
21
+ _nltk_init = False
22
+
23
+ DEFAULT_CONTEXT_LENGTH = 77 # default context length for OpenAI CLIP
24
+
25
+
26
+ @lru_cache()
27
+ def default_bpe():
28
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "bpe_simple_vocab_16e6.txt.gz")
29
+
30
+
31
+ @lru_cache()
32
+ def bytes_to_unicode():
33
+ """
34
+ Returns list of utf-8 byte and a corresponding list of unicode strings.
35
+ The reversible bpe codes work on unicode strings.
36
+ This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
37
+ When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
38
+ This is a significant percentage of your normal, say, 32K bpe vocab.
39
+ To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
40
+ And avoids mapping to whitespace/control characters the bpe code barfs on.
41
+ """
42
+ bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1))
43
+ cs = bs[:]
44
+ n = 0
45
+ for b in range(2**8):
46
+ if b not in bs:
47
+ bs.append(b)
48
+ cs.append(2**8+n)
49
+ n += 1
50
+ cs = [chr(n) for n in cs]
51
+ return dict(zip(bs, cs))
52
+
53
+
54
+ def get_pairs(word):
55
+ """Return set of symbol pairs in a word.
56
+ Word is represented as tuple of symbols (symbols being variable-length strings).
57
+ """
58
+ pairs = set()
59
+ prev_char = word[0]
60
+ for char in word[1:]:
61
+ pairs.add((prev_char, char))
62
+ prev_char = char
63
+ return pairs
64
+
65
+
66
+ def basic_clean(text):
67
+ text = ftfy.fix_text(text)
68
+ text = html.unescape(html.unescape(text))
69
+ return text.strip()
70
+
71
+
72
+ def whitespace_clean(text):
73
+ text = " ".join(text.split())
74
+ text = text.strip()
75
+ return text
76
+
77
+
78
+ def _clean_canonicalize(x):
79
+ # basic, remove whitespace, remove punctuation, lower case
80
+ return canonicalize_text(basic_clean(x))
81
+
82
+
83
+ def _clean_lower(x):
84
+ # basic, remove whitespace, lower case
85
+ return whitespace_clean(basic_clean(x)).lower()
86
+
87
+
88
+ def _clean_whitespace(x):
89
+ # basic, remove whitespace
90
+ return whitespace_clean(basic_clean(x))
91
+
92
+
93
+ def get_clean_fn(type: str):
94
+ if type == 'canonicalize':
95
+ return _clean_canonicalize
96
+ elif type == 'lower':
97
+ return _clean_lower
98
+ elif type == 'whitespace':
99
+ return _clean_whitespace
100
+ else:
101
+ assert False, f"Invalid clean function ({type})."
102
+
103
+
104
+ def canonicalize_text(
105
+ text,
106
+ *,
107
+ keep_punctuation_exact_string=None,
108
+ trans_punctuation: dict = str.maketrans("", "", string.punctuation),
109
+ ):
110
+ """Returns canonicalized `text` (lowercase and punctuation removed).
111
+
112
+ From: https://github.com/google-research/big_vision/blob/53f18caf27a9419231bbf08d3388b07671616d3d/big_vision/evaluators/proj/image_text/prompt_engineering.py#L94
113
+
114
+ Args:
115
+ text: string to be canonicalized.
116
+ keep_punctuation_exact_string: If provided, then this exact string kept.
117
+ For example providing '{}' will keep any occurrences of '{}' (but will
118
+ still remove '{' and '}' that appear separately).
119
+ """
120
+ text = text.replace("_", " ")
121
+ if keep_punctuation_exact_string:
122
+ text = keep_punctuation_exact_string.join(
123
+ part.translate(trans_punctuation)
124
+ for part in text.split(keep_punctuation_exact_string)
125
+ )
126
+ else:
127
+ text = text.translate(trans_punctuation)
128
+ text = text.lower()
129
+ text = " ".join(text.split())
130
+ return text.strip()
131
+
132
+
133
+ class SimpleTokenizer(object):
134
+ def __init__(
135
+ self,
136
+ bpe_path: str = default_bpe(),
137
+ additional_special_tokens: Optional[List[str]] = None,
138
+ context_length: Optional[int] = DEFAULT_CONTEXT_LENGTH,
139
+ clean: str = 'lower',
140
+ reduction_mask: str = ''
141
+ ):
142
+ self.byte_encoder = bytes_to_unicode()
143
+ self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
144
+ merges = gzip.open(bpe_path).read().decode("utf-8").split('\n')
145
+ merges = merges[1:49152-256-2+1]
146
+ merges = [tuple(merge.split()) for merge in merges]
147
+ vocab = list(bytes_to_unicode().values())
148
+ vocab = vocab + [v+'</w>' for v in vocab]
149
+ for merge in merges:
150
+ vocab.append(''.join(merge))
151
+ special_tokens = ['<start_of_text>', '<end_of_text>']
152
+ if additional_special_tokens:
153
+ special_tokens += additional_special_tokens
154
+ vocab.extend(special_tokens)
155
+ self.encoder = dict(zip(vocab, range(len(vocab))))
156
+ self.decoder = {v: k for k, v in self.encoder.items()}
157
+ self.bpe_ranks = dict(zip(merges, range(len(merges))))
158
+ self.cache = {t:t for t in special_tokens}
159
+ special = "|".join(special_tokens)
160
+ self.pat = re.compile(
161
+ special + r"""|'s|'t|'re|'ve|'m|'ll|'d|[\p{L}]+|[\p{N}]|[^\s\p{L}\p{N}]+""",
162
+ re.IGNORECASE,
163
+ )
164
+ self.vocab_size = len(self.encoder)
165
+ self.all_special_ids = [self.encoder[t] for t in special_tokens]
166
+ self.sot_token_id = self.all_special_ids[0]
167
+ self.eot_token_id = self.all_special_ids[1]
168
+ self.context_length = context_length
169
+ self.clean_fn = get_clean_fn(clean)
170
+ self.reduction_fn = get_reduction_mask_fn(reduction_mask) if reduction_mask else None
171
+
172
+ def bpe(self, token):
173
+ if token in self.cache:
174
+ return self.cache[token]
175
+ word = tuple(token[:-1]) + ( token[-1] + '</w>',)
176
+ pairs = get_pairs(word)
177
+
178
+ if not pairs:
179
+ return token+'</w>'
180
+
181
+ while True:
182
+ bigram = min(pairs, key = lambda pair: self.bpe_ranks.get(pair, float('inf')))
183
+ if bigram not in self.bpe_ranks:
184
+ break
185
+ first, second = bigram
186
+ new_word = []
187
+ i = 0
188
+ while i < len(word):
189
+ try:
190
+ j = word.index(first, i)
191
+ new_word.extend(word[i:j])
192
+ i = j
193
+ except Exception:
194
+ new_word.extend(word[i:])
195
+ break
196
+
197
+ if word[i] == first and i < len(word)-1 and word[i+1] == second:
198
+ new_word.append(first+second)
199
+ i += 2
200
+ else:
201
+ new_word.append(word[i])
202
+ i += 1
203
+ new_word = tuple(new_word)
204
+ word = new_word
205
+ if len(word) == 1:
206
+ break
207
+ else:
208
+ pairs = get_pairs(word)
209
+ word = ' '.join(word)
210
+ self.cache[token] = word
211
+ return word
212
+
213
+ def encode(self, text):
214
+ bpe_tokens = []
215
+ text = self.clean_fn(text)
216
+ for token in re.findall(self.pat, text):
217
+ token = ''.join(self.byte_encoder[b] for b in token.encode('utf-8'))
218
+ bpe_tokens.extend(self.encoder[bpe_token] for bpe_token in self.bpe(token).split(' '))
219
+ return bpe_tokens
220
+
221
+ def decode(self, tokens):
222
+ text = ''.join([self.decoder[token] for token in tokens])
223
+ text = bytearray([self.byte_decoder[c] for c in text]).decode('utf-8', errors="replace").replace('</w>', ' ')
224
+ return text
225
+
226
+ def __call__(self, texts: Union[str, List[str]], context_length: Optional[int] = None) -> torch.LongTensor:
227
+ """ Returns the tokenized representation of given input string(s)
228
+
229
+ Parameters
230
+ ----------
231
+ texts : Union[str, List[str]]
232
+ An input string or a list of input strings to tokenize
233
+ context_length : int
234
+ The context length to use; all CLIP models use 77 as the context length
235
+
236
+ Returns
237
+ -------
238
+ A two-dimensional tensor containing the resulting tokens, shape = [number of input strings, context_length]
239
+ """
240
+ if isinstance(texts, str):
241
+ texts = [texts]
242
+
243
+ context_length = context_length or self.context_length
244
+ assert context_length, 'Please set a valid context length'
245
+
246
+ if self.reduction_fn is not None:
247
+ # use reduction strategy for tokenize if set, otherwise default to truncation below
248
+ return self.reduction_fn(
249
+ texts,
250
+ context_length=context_length,
251
+ sot_token_id=self.sot_token_id,
252
+ eot_token_id=self.eot_token_id,
253
+ encode_fn=self.encode,
254
+ )
255
+
256
+ all_tokens = [[self.sot_token_id] + self.encode(text) + [self.eot_token_id] for text in texts]
257
+ result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
258
+
259
+ for i, tokens in enumerate(all_tokens):
260
+ if len(tokens) > context_length:
261
+ tokens = tokens[:context_length] # Truncate
262
+ tokens[-1] = self.eot_token_id
263
+ result[i, :len(tokens)] = torch.tensor(tokens)
264
+
265
+ return result
266
+
267
+
268
+ _tokenizer = SimpleTokenizer()
269
+
270
+
271
+ def decode(output_ids: torch.Tensor):
272
+ output_ids = output_ids.cpu().numpy()
273
+ return _tokenizer.decode(output_ids)
274
+
275
+
276
+ def tokenize(texts: Union[str, List[str]], context_length: int = DEFAULT_CONTEXT_LENGTH) -> torch.LongTensor:
277
+ return _tokenizer(texts, context_length=context_length)
278
+
279
+
280
+ def random_mask_tokenize(
281
+ texts: Union[str, List[str]],
282
+ context_length: int,
283
+ sot_token_id: int,
284
+ eot_token_id: int,
285
+ encode_fn: Callable,
286
+ shuffle: bool = False,
287
+ ):
288
+ all_tokens = [encode_fn(text) for text in texts]
289
+ result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
290
+
291
+ for i, tokens in enumerate(all_tokens):
292
+ tokens = torch.tensor(tokens)
293
+ num_tokens = len(tokens)
294
+ if num_tokens > context_length - 2: # 2 for sot and eot token
295
+ num_keep = context_length - 2
296
+ indices = torch.randperm(len(tokens))
297
+ indices = indices[:num_keep]
298
+ if not shuffle:
299
+ indices = indices.msort()
300
+ tokens = tokens[indices]
301
+ num_tokens = num_keep
302
+ result[i, 0] = sot_token_id
303
+ result[i, 1:num_tokens + 1] = tokens
304
+ result[i, num_tokens + 1] = eot_token_id
305
+
306
+ return result
307
+
308
+
309
+ def simple_mask_tokenize(
310
+ texts: Union[str, List[str]],
311
+ context_length: int,
312
+ sot_token_id: int,
313
+ eot_token_id: int,
314
+ encode_fn: Callable,
315
+ ):
316
+ all_tokens = [encode_fn(text) for text in texts]
317
+ result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
318
+
319
+ for i, tokens in enumerate(all_tokens):
320
+ num_tokens = len(tokens)
321
+ if num_tokens > context_length - 2: # 2 for sot and eot token
322
+ num_keep = context_length - 2
323
+ start_index = random.randint(0, num_tokens - num_keep) # high is incl
324
+ tokens = tokens[start_index: start_index + num_keep]
325
+ tokens = [sot_token_id] + tokens + [eot_token_id]
326
+ result[i, :len(tokens)] = torch.tensor(tokens)
327
+
328
+ return result
329
+
330
+
331
+ def syntax_mask_tokenize(
332
+ texts: Union[str, List[str]],
333
+ context_length: int,
334
+ sot_token_id: int,
335
+ eot_token_id: int,
336
+ encode_fn: Callable,
337
+ ) -> torch.LongTensor:
338
+ """ Returns the tokenized representation of given input string(s).
339
+ Apply syntax masking before tokenize.
340
+ """
341
+ import nltk
342
+ global _nltk_init
343
+ if not _nltk_init:
344
+ # run them for the first time
345
+ nltk.download('punkt')
346
+ nltk.download('averaged_perceptron_tagger')
347
+ _nltk_init = True
348
+
349
+ def get_order(x):
350
+ if x.startswith('NN'):
351
+ return 1
352
+ elif x.startswith('JJ'):
353
+ return 2
354
+ elif x.startswith('VB'):
355
+ return 3
356
+ else:
357
+ return 4
358
+
359
+ # syntax masking
360
+ new_texts = []
361
+ for text in texts:
362
+ list_tokens = nltk.tokenize.word_tokenize(text)
363
+ pos_tags = nltk.pos_tag(list_tokens)
364
+ # sample the words by get_order method
365
+ order_list = [get_order(tag) for _, tag in pos_tags]
366
+ sorted_ids = np.argsort(np.array(order_list))
367
+ sampled_ids = sorted(sorted_ids[:context_length - 2]) # need 2 slots for sot and eot tokens
368
+ sampled_tokens = np.take(np.array(list_tokens), sampled_ids, axis=0) # sample the tokens
369
+
370
+ new_text = ''
371
+ for token in sampled_tokens:
372
+ new_text = new_text + str(token) + ' '
373
+ new_text = new_text.strip()
374
+ new_texts.append(new_text)
375
+ texts = new_texts
376
+
377
+ all_tokens = [[sot_token_id] + encode_fn(text) + [eot_token_id] for text in texts]
378
+ result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
379
+
380
+ for i, tokens in enumerate(all_tokens):
381
+ # still need first truncate because some words produces two tokens
382
+ if len(tokens) > context_length:
383
+ tokens = tokens[:context_length] # Truncate
384
+ tokens[-1] = eot_token_id
385
+ result[i, :len(tokens)] = torch.tensor(tokens)
386
+
387
+ return result
388
+
389
+
390
+ def get_reduction_mask_fn(type: str):
391
+ """ Choose strategy for dropping (masking) tokens to achieve target context length"""
392
+ assert type in ('simple', 'random', 'shuffle', 'syntax')
393
+ if type == 'simple':
394
+ return simple_mask_tokenize # randomly select block [start:end]
395
+ elif type == 'random':
396
+ return random_mask_tokenize # randomly drop tokens (keep order)
397
+ elif type == 'shuffle':
398
+ return partial(random_mask_tokenize, shuffle=True) # randomly drop tokens (shuffle order)
399
+ elif type == 'syntax':
400
+ return syntax_mask_tokenize # randomly drop prioritized by syntax
401
+
402
+
403
+ class HFTokenizer:
404
+ """HuggingFace tokenizer wrapper"""
405
+
406
+ def __init__(
407
+ self,
408
+ tokenizer_name: str,
409
+ context_length: Optional[int] = DEFAULT_CONTEXT_LENGTH,
410
+ clean: str = 'whitespace',
411
+ strip_sep_token: bool = False,
412
+ language: Optional[str] = None,
413
+ **kwargs
414
+ ):
415
+ from transformers import AutoTokenizer
416
+ self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name, **kwargs)
417
+ set_lang_fn = getattr(self.tokenizer, 'set_src_lang_special_tokens', None)
418
+ if callable(set_lang_fn):
419
+ self.set_lang_fn = set_lang_fn
420
+ if language is not None:
421
+ self.set_language(language)
422
+ self.context_length = context_length
423
+ self.clean_fn = get_clean_fn(clean)
424
+ self.strip_sep_token = strip_sep_token
425
+ self.tokenizer.add_tokens(["<s>", "</s>"])
426
+
427
+ def save_pretrained(self, dest):
428
+ self.tokenizer.save_pretrained(dest)
429
+
430
+ def __call__(self, texts: Union[str, List[str]], context_length: Optional[int] = None) -> torch.Tensor:
431
+ # same cleaning as for default tokenizer, except lowercasing
432
+ # adding lower (for case-sensitive tokenizers) will make it more robust but less sensitive to nuance
433
+ if isinstance(texts, str):
434
+ texts = [texts]
435
+
436
+ context_length = context_length or self.context_length
437
+ assert context_length, 'Please set a valid context length in class init or call.'
438
+
439
+ texts = [self.clean_fn(text) for text in texts]
440
+ input_ids = self.tokenizer.batch_encode_plus(
441
+ texts,
442
+ return_tensors='pt',
443
+ max_length=context_length,
444
+ padding='max_length',
445
+ truncation=True,
446
+ ).input_ids
447
+
448
+ if self.strip_sep_token:
449
+ input_ids = torch.where(
450
+ input_ids == self.tokenizer.sep_token_id,
451
+ torch.zeros_like(input_ids),
452
+ input_ids,
453
+ )
454
+
455
+ return input_ids
456
+
457
+ def set_language(self, src_lang):
458
+ if hasattr(self, 'set_lang_fn'):
459
+ self.set_lang_fn(src_lang)
460
+ else:
461
+ warnings.warn('Cannot set language for the tokenizer.')
462
+
463
+
464
+ class SigLipTokenizer:
465
+ """HuggingFace tokenizer wrapper for SigLIP T5 compatible sentencepiece vocabs
466
+ """
467
+ VOCAB_FILES = {
468
+ # english, vocab_size=32_000
469
+ "c4-en": "http://storage.googleapis.com/t5-data/vocabs/cc_en.32000/sentencepiece.model",
470
+ # used in multilingual models (mT5, PaLI), vocab_size=250_000
471
+ "mc4": "http://storage.googleapis.com/t5-data/vocabs/mc4.250000.100extra/sentencepiece.model",
472
+ }
473
+
474
+ def __init__(
475
+ self,
476
+ tokenizer_name: str,
477
+ context_length: Optional[int] = 64,
478
+ ):
479
+ from transformers import T5TokenizerFast
480
+
481
+ if tokenizer_name in self.VOCAB_FILES:
482
+ # FIXME temporary hack?
483
+ import tempfile
484
+
485
+ import fsspec
486
+ vocab_file = self.VOCAB_FILES[tokenizer_name]
487
+ with tempfile.NamedTemporaryFile('wb') as dst:
488
+ with fsspec.open(vocab_file, 'rb') as src:
489
+ dst.write(src.read())
490
+ self.tokenizer = T5TokenizerFast(dst.name, legacy=False)
491
+ else:
492
+ self.tokenizer = T5TokenizerFast(tokenizer_name, legacy=False)
493
+
494
+ self.tokenizer.pad_token_id = 1
495
+ self.tokenizer.eos_token_id = 1
496
+ self.context_length = context_length
497
+
498
+ def save_pretrained(self, dest):
499
+ self.tokenizer.save_pretrained(dest)
500
+
501
+ def __call__(self, texts: Union[str, List[str]], context_length: Optional[int] = None) -> torch.Tensor:
502
+ # same cleaning as for default tokenizer, except lowercasing
503
+ # adding lower (for case-sensitive tokenizers) will make it more robust but less sensitive to nuance
504
+ if isinstance(texts, str):
505
+ texts = [texts]
506
+
507
+ context_length = context_length or self.context_length
508
+ assert context_length, 'Please set a valid context length in class init or call.'
509
+
510
+ texts = [canonicalize_text(basic_clean(text)) for text in texts]
511
+ output = self.tokenizer(
512
+ texts,
513
+ return_tensors='pt',
514
+ max_length=context_length,
515
+ padding='max_length',
516
+ truncation=True,
517
+ )
518
+ return output.input_ids
ecg_coca/open_clip/transform.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numbers
2
+ import random
3
+ import warnings
4
+ from dataclasses import dataclass, asdict
5
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
6
+ import numpy as np
7
+ import torch
8
+ # import torchvision.transforms.functional as F
9
+ # from torchvision.transforms import Normalize, Compose, RandomResizedCrop, InterpolationMode, ToTensor, Resize, \
10
+ # CenterCrop, ColorJitter, Grayscale
11
+
12
+ from .constants import OPENAI_DATASET_MEAN, OPENAI_DATASET_STD
13
+ from .utils import to_2tuple
14
+
15
+
16
+ @dataclass
17
+ class PreprocessCfg:
18
+ size: Union[int, Tuple[int, int]] = 224
19
+ mode: str = 'RGB'
20
+ mean: Tuple[float, ...] = OPENAI_DATASET_MEAN
21
+ std: Tuple[float, ...] = OPENAI_DATASET_STD
22
+ interpolation: str = 'bicubic'
23
+ resize_mode: str = 'shortest'
24
+ fill_color: int = 0
25
+
26
+ def __post_init__(self):
27
+ assert self.mode in ('RGB',)
28
+
29
+ @property
30
+ def num_channels(self):
31
+ return 3
32
+
33
+ @property
34
+ def input_size(self):
35
+ return (self.num_channels,) + to_2tuple(self.size)
36
+
37
+
38
+ _PREPROCESS_KEYS = set(asdict(PreprocessCfg()).keys())
39
+
40
+
41
+ def merge_preprocess_dict(
42
+ base: Union[PreprocessCfg, Dict],
43
+ overlay: Dict,
44
+ ):
45
+ """ Merge overlay key-value pairs on top of base preprocess cfg or dict.
46
+ Input dicts are filtered based on PreprocessCfg fields.
47
+ """
48
+ if isinstance(base, PreprocessCfg):
49
+ base_clean = asdict(base)
50
+ else:
51
+ base_clean = {k: v for k, v in base.items() if k in _PREPROCESS_KEYS}
52
+ if overlay:
53
+ overlay_clean = {k: v for k, v in overlay.items() if k in _PREPROCESS_KEYS and v is not None}
54
+ base_clean.update(overlay_clean)
55
+ return base_clean
56
+
57
+
58
+ def merge_preprocess_kwargs(base: PreprocessCfg, **kwargs):
59
+ return merge_preprocess_dict(base, kwargs)
60
+
61
+
62
+ @dataclass
63
+ class AugmentationCfg:
64
+ scale: Tuple[float, float] = (0.9, 1.0)
65
+ ratio: Optional[Tuple[float, float]] = None
66
+ color_jitter: Optional[Union[float, Tuple[float, float, float], Tuple[float, float, float, float]]] = None
67
+ re_prob: Optional[float] = None
68
+ re_count: Optional[int] = None
69
+ use_timm: bool = False
70
+
71
+ # params for simclr_jitter_gray
72
+ color_jitter_prob: float = None
73
+ gray_scale_prob: float = None
ecg_coca/open_clip/transformer.py ADDED
@@ -0,0 +1,1114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import OrderedDict
2
+ import math
3
+ from typing import Callable, List, Optional, Sequence, Tuple, Union
4
+ from functools import partial
5
+
6
+ import torch
7
+ from torch import nn
8
+ from torch.nn import functional as F
9
+ from torch.utils.checkpoint import checkpoint
10
+
11
+ from .utils import to_2tuple
12
+ from .pos_embed import get_2d_sincos_pos_embed
13
+
14
+
15
+ class LayerNormFp32(nn.LayerNorm):
16
+ """Subclass torch's LayerNorm to handle fp16 (by casting to float32 and back)."""
17
+
18
+ def forward(self, x: torch.Tensor):
19
+ orig_type = x.dtype
20
+ x = F.layer_norm(x.to(torch.float32), self.normalized_shape, self.weight, self.bias, self.eps)
21
+ return x.to(orig_type)
22
+
23
+
24
+ class LayerNorm(nn.LayerNorm):
25
+ """Subclass torch's LayerNorm (with cast back to input dtype)."""
26
+
27
+ def forward(self, x: torch.Tensor):
28
+ orig_type = x.dtype
29
+ x = F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)
30
+ return x.to(orig_type)
31
+
32
+
33
+ class QuickGELU(nn.Module):
34
+ # NOTE This is slower than nn.GELU or nn.SiLU and uses more GPU memory
35
+ def forward(self, x: torch.Tensor):
36
+ return x * torch.sigmoid(1.702 * x)
37
+
38
+
39
+ class LayerScale(nn.Module):
40
+ def __init__(self, dim, init_values=1e-5, inplace=False):
41
+ super().__init__()
42
+ self.inplace = inplace
43
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
44
+
45
+ def forward(self, x):
46
+ return x.mul_(self.gamma) if self.inplace else x * self.gamma
47
+
48
+
49
+ class PatchDropout(nn.Module):
50
+ """
51
+ https://arxiv.org/abs/2212.00794
52
+ """
53
+
54
+ def __init__(self, prob, exclude_first_token=True):
55
+ super().__init__()
56
+ assert 0 <= prob < 1.
57
+ self.prob = prob
58
+ self.exclude_first_token = exclude_first_token # exclude CLS token
59
+
60
+ def forward(self, x):
61
+ if not self.training or self.prob == 0.:
62
+ return x
63
+
64
+ if self.exclude_first_token:
65
+ cls_tokens, x = x[:, :1], x[:, 1:]
66
+ else:
67
+ cls_tokens = torch.jit.annotate(torch.Tensor, x[:, :1])
68
+
69
+ batch = x.size()[0]
70
+ num_tokens = x.size()[1]
71
+
72
+ batch_indices = torch.arange(batch)
73
+ batch_indices = batch_indices[..., None]
74
+
75
+ keep_prob = 1 - self.prob
76
+ num_patches_keep = max(1, int(num_tokens * keep_prob))
77
+
78
+ rand = torch.randn(batch, num_tokens)
79
+ patch_indices_keep = rand.topk(num_patches_keep, dim=-1).indices
80
+
81
+ x = x[batch_indices, patch_indices_keep]
82
+
83
+ if self.exclude_first_token:
84
+ x = torch.cat((cls_tokens, x), dim=1)
85
+
86
+ return x
87
+
88
+
89
+ class Attention(nn.Module):
90
+ def __init__(
91
+ self,
92
+ dim: int,
93
+ num_heads: int = 8,
94
+ qkv_bias: bool = True,
95
+ scaled_cosine: bool = False,
96
+ scale_heads: bool = False,
97
+ logit_scale_max: float = math.log(1. / 0.01),
98
+ batch_first: bool = True,
99
+ attn_drop: float = 0.,
100
+ proj_drop: float = 0.
101
+ ):
102
+ super().__init__()
103
+ self.scaled_cosine = scaled_cosine
104
+ self.scale_heads = scale_heads
105
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
106
+ self.num_heads = num_heads
107
+ self.head_dim = dim // num_heads
108
+ self.scale = self.head_dim ** -0.5
109
+ self.logit_scale_max = logit_scale_max
110
+ self.batch_first = batch_first
111
+ self.use_fsdpa = hasattr(nn.functional, 'scaled_dot_product_attention')
112
+
113
+ # keeping in_proj in this form (instead of nn.Linear) to match weight scheme of original
114
+ self.in_proj_weight = nn.Parameter(torch.randn((dim * 3, dim)) * self.scale)
115
+ if qkv_bias:
116
+ self.in_proj_bias = nn.Parameter(torch.zeros(dim * 3))
117
+ else:
118
+ self.in_proj_bias = None
119
+
120
+ if self.scaled_cosine:
121
+ self.logit_scale = nn.Parameter(torch.log(10 * torch.ones((num_heads, 1, 1))))
122
+ else:
123
+ self.logit_scale = None
124
+ self.attn_drop = nn.Dropout(attn_drop)
125
+ if self.scale_heads:
126
+ self.head_scale = nn.Parameter(torch.ones((num_heads, 1, 1)))
127
+ else:
128
+ self.head_scale = None
129
+ self.out_proj = nn.Linear(dim, dim)
130
+ self.out_drop = nn.Dropout(proj_drop)
131
+
132
+ def forward(self, x, attn_mask: Optional[torch.Tensor] = None):
133
+ if self.batch_first:
134
+ x = x.transpose(0, 1)
135
+
136
+ L, N, C = x.shape
137
+ q, k, v = F.linear(x, self.in_proj_weight, self.in_proj_bias).chunk(3, dim=-1)
138
+ q = q.reshape(L, N * self.num_heads, -1).transpose(0, 1)
139
+ k = k.reshape(L, N * self.num_heads, -1).transpose(0, 1)
140
+ v = v.reshape(L, N * self.num_heads, -1).transpose(0, 1)
141
+
142
+ if attn_mask is not None and attn_mask.dtype == torch.bool:
143
+ new_attn_mask = torch.zeros_like(attn_mask, dtype=q.dtype)
144
+ new_attn_mask.masked_fill_(attn_mask, float("-inf"))
145
+ attn_mask = new_attn_mask
146
+
147
+ if self.logit_scale is not None:
148
+ attn = torch.bmm(F.normalize(q, dim=-1), F.normalize(k, dim=-1).transpose(-1, -2))
149
+ logit_scale = torch.clamp(self.logit_scale, max=self.logit_scale_max).exp()
150
+ attn = attn.view(N, self.num_heads, L, L) * logit_scale
151
+ attn = attn.view(-1, L, L)
152
+ if attn_mask is not None:
153
+ attn = attn + attn_mask
154
+ attn = attn.softmax(dim=-1)
155
+ attn = self.attn_drop(attn)
156
+ x = torch.bmm(attn, v)
157
+ else:
158
+ if self.use_fsdpa:
159
+ x = F.scaled_dot_product_attention(
160
+ q, k, v,
161
+ attn_mask=attn_mask,
162
+ dropout_p=self.attn_drop.p if self.training else 0.,
163
+ )
164
+ else:
165
+ q = q * self.scale
166
+ attn = torch.bmm(q, k.transpose(-1, -2))
167
+ if attn_mask is not None:
168
+ attn += attn_mask
169
+ attn = attn.softmax(dim=-1)
170
+ attn = self.attn_drop(attn)
171
+ x = torch.bmm(attn, v)
172
+
173
+ if self.head_scale is not None:
174
+ x = x.view(N, self.num_heads, L, C) * self.head_scale
175
+ x = x.view(-1, L, C)
176
+
177
+ x = x.transpose(0, 1).reshape(L, N, C)
178
+
179
+ if self.batch_first:
180
+ x = x.transpose(0, 1)
181
+
182
+ x = self.out_proj(x)
183
+ x = self.out_drop(x)
184
+ return x
185
+
186
+
187
+ class AttentionalPooler(nn.Module):
188
+ def __init__(
189
+ self,
190
+ d_model: int,
191
+ context_dim: int,
192
+ n_head: int = 8,
193
+ n_queries: int = 256,
194
+ norm_layer: Callable = LayerNorm
195
+ ):
196
+ super().__init__()
197
+ self.query = nn.Parameter(torch.randn(n_queries, d_model))
198
+ self.attn = nn.MultiheadAttention(d_model, n_head, kdim=context_dim, vdim=context_dim)
199
+ self.ln_q = norm_layer(d_model)
200
+ self.ln_k = norm_layer(context_dim)
201
+
202
+ def forward(self, x: torch.Tensor):
203
+ x = self.ln_k(x).permute(1, 0, 2) # NLD -> LND
204
+ N = x.shape[1]
205
+ q = self.ln_q(self.query)
206
+ out = self.attn(q.unsqueeze(1).expand(-1, N, -1), x, x, need_weights=False)[0]
207
+ return out.permute(1, 0, 2) # LND -> NLD
208
+
209
+
210
+ class ResidualAttentionBlock(nn.Module):
211
+ def __init__(
212
+ self,
213
+ d_model: int,
214
+ n_head: int,
215
+ mlp_ratio: float = 4.0,
216
+ ls_init_value: float = None,
217
+ act_layer: Callable = nn.GELU,
218
+ norm_layer: Callable = LayerNorm,
219
+ is_cross_attention: bool = False,
220
+ ):
221
+ super().__init__()
222
+
223
+ self.ln_1 = norm_layer(d_model)
224
+ self.attn = nn.MultiheadAttention(d_model, n_head)
225
+ self.ls_1 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
226
+ if is_cross_attention:
227
+ self.ln_1_kv = norm_layer(d_model)
228
+
229
+ self.ln_2 = norm_layer(d_model)
230
+ mlp_width = int(d_model * mlp_ratio)
231
+ self.mlp = nn.Sequential(OrderedDict([
232
+ ("c_fc", nn.Linear(d_model, mlp_width)),
233
+ ("gelu", act_layer()),
234
+ ("c_proj", nn.Linear(mlp_width, d_model))
235
+ ]))
236
+ self.ls_2 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
237
+
238
+ def attention(
239
+ self,
240
+ q_x: torch.Tensor,
241
+ k_x: Optional[torch.Tensor] = None,
242
+ v_x: Optional[torch.Tensor] = None,
243
+ attn_mask: Optional[torch.Tensor] = None,
244
+ ):
245
+ k_x = k_x if k_x is not None else q_x
246
+ v_x = v_x if v_x is not None else q_x
247
+
248
+ attn_mask = attn_mask.to(q_x.dtype) if attn_mask is not None else None
249
+ return self.attn(
250
+ q_x, k_x, v_x, need_weights=False, attn_mask=attn_mask
251
+ )[0]
252
+
253
+ def forward(
254
+ self,
255
+ q_x: torch.Tensor,
256
+ k_x: Optional[torch.Tensor] = None,
257
+ v_x: Optional[torch.Tensor] = None,
258
+ attn_mask: Optional[torch.Tensor] = None,
259
+ ):
260
+ k_x = self.ln_1_kv(k_x) if hasattr(self, "ln_1_kv") and k_x is not None else None
261
+ v_x = self.ln_1_kv(v_x) if hasattr(self, "ln_1_kv") and v_x is not None else None
262
+ x = q_x + self.ls_1(self.attention(q_x=self.ln_1(q_x), k_x=k_x, v_x=v_x, attn_mask=attn_mask))
263
+ x = x + self.ls_2(self.mlp(self.ln_2(x)))
264
+ return x
265
+
266
+
267
+ class CustomResidualAttentionBlock(nn.Module):
268
+ def __init__(
269
+ self,
270
+ d_model: int,
271
+ n_head: int,
272
+ mlp_ratio: float = 4.0,
273
+ ls_init_value: float = None,
274
+ act_layer: Callable = nn.GELU,
275
+ norm_layer: Callable = LayerNorm,
276
+ scale_cosine_attn: bool = False,
277
+ scale_heads: bool = False,
278
+ scale_attn: bool = False,
279
+ scale_fc: bool = False,
280
+ batch_first: bool = True,
281
+ ):
282
+ super().__init__()
283
+
284
+ self.ln_1 = norm_layer(d_model)
285
+ self.attn = Attention(
286
+ d_model, n_head,
287
+ scaled_cosine=scale_cosine_attn,
288
+ scale_heads=scale_heads,
289
+ batch_first=batch_first,
290
+ )
291
+ self.ln_attn = norm_layer(d_model) if scale_attn else nn.Identity()
292
+ self.ls_1 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
293
+
294
+ self.ln_2 = norm_layer(d_model)
295
+ mlp_width = int(d_model * mlp_ratio)
296
+ self.mlp = nn.Sequential(OrderedDict([
297
+ ("c_fc", nn.Linear(d_model, mlp_width)),
298
+ ("gelu", act_layer()),
299
+ ('ln', norm_layer(mlp_width) if scale_fc else nn.Identity()),
300
+ ("c_proj", nn.Linear(mlp_width, d_model))
301
+ ]))
302
+ self.ls_2 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
303
+
304
+ def get_reference_weight(self):
305
+ return self.mlp.c_fc.weight
306
+
307
+ def forward(self, x: torch.Tensor, attn_mask: Optional[torch.Tensor] = None):
308
+ x = x + self.ls_1(self.ln_attn(self.attn(self.ln_1(x), attn_mask=attn_mask)))
309
+ x = x + self.ls_2(self.mlp(self.ln_2(x)))
310
+ return x
311
+
312
+
313
+ def _expand_token(token, batch_size: int):
314
+ return token.view(1, 1, -1).expand(batch_size, -1, -1)
315
+
316
+
317
+ class Transformer(nn.Module):
318
+ def __init__(
319
+ self,
320
+ width: int,
321
+ layers: int,
322
+ heads: int,
323
+ mlp_ratio: float = 4.0,
324
+ ls_init_value: float = None,
325
+ act_layer: Callable = nn.GELU,
326
+ norm_layer: Callable = LayerNorm,
327
+ ):
328
+ super().__init__()
329
+ self.width = width
330
+ self.layers = layers
331
+ self.grad_checkpointing = False
332
+
333
+ self.resblocks = nn.ModuleList([
334
+ ResidualAttentionBlock(
335
+ width,
336
+ heads,
337
+ mlp_ratio,
338
+ ls_init_value=ls_init_value,
339
+ act_layer=act_layer,
340
+ norm_layer=norm_layer,
341
+ )
342
+ for _ in range(layers)
343
+ ])
344
+
345
+ def get_cast_dtype(self) -> torch.dtype:
346
+ if hasattr(self.resblocks[0].mlp.c_fc, 'int8_original_dtype'):
347
+ return self.resblocks[0].mlp.c_fc.int8_original_dtype
348
+ return self.resblocks[0].mlp.c_fc.weight.dtype
349
+
350
+ def forward(self, x: torch.Tensor, attn_mask: Optional[torch.Tensor] = None):
351
+ x = x.transpose(0, 1).contiguous() # NLD -> LND
352
+ for r in self.resblocks:
353
+ if self.grad_checkpointing and not torch.jit.is_scripting():
354
+ # TODO: handle kwargs https://github.com/pytorch/pytorch/issues/79887#issuecomment-1161758372
355
+ x = checkpoint(r, x, None, None, attn_mask)
356
+ else:
357
+ x = r(x, attn_mask=attn_mask)
358
+ x = x.transpose(0, 1) # LND -> NLD
359
+ return x
360
+
361
+
362
+ class CustomTransformer(nn.Module):
363
+ """ A custom transformer that can use different block types. """
364
+
365
+ def __init__(
366
+ self,
367
+ width: int,
368
+ layers: int,
369
+ heads: int,
370
+ mlp_ratio: float = 4.0,
371
+ ls_init_value: float = None,
372
+ act_layer: Callable = nn.GELU,
373
+ norm_layer: Callable = LayerNorm,
374
+ batch_first: bool = True,
375
+ block_types: Union[str, List[str]] = 'CustomResidualAttentionBlock',
376
+ ):
377
+ super().__init__()
378
+ self.width = width
379
+ self.layers = layers
380
+ self.batch_first = batch_first # run trasnformer stack in batch first (N, L, D)
381
+ self.grad_checkpointing = False
382
+
383
+ if isinstance(block_types, str):
384
+ block_types = [block_types] * layers
385
+ assert len(block_types) == layers
386
+
387
+ def _create_block(bt: str):
388
+ if bt == 'CustomResidualAttentionBlock':
389
+ return CustomResidualAttentionBlock(
390
+ width,
391
+ heads,
392
+ mlp_ratio=mlp_ratio,
393
+ ls_init_value=ls_init_value,
394
+ act_layer=act_layer,
395
+ norm_layer=norm_layer,
396
+ batch_first=batch_first,
397
+ )
398
+ else:
399
+ assert False
400
+
401
+ self.resblocks = nn.ModuleList([
402
+ _create_block(bt)
403
+ for bt in block_types
404
+ ])
405
+
406
+ def get_cast_dtype(self) -> torch.dtype:
407
+ weight = self.resblocks[0].get_reference_weight()
408
+ if hasattr(weight, 'int8_original_dtype'):
409
+ return weight.int8_original_dtype
410
+ return weight.dtype
411
+
412
+ def forward(self, x: torch.Tensor, attn_mask: Optional[torch.Tensor] = None):
413
+ if not self.batch_first:
414
+ x = x.transpose(0, 1) # NLD -> LND
415
+
416
+ for r in self.resblocks:
417
+ if self.grad_checkpointing and not torch.jit.is_scripting():
418
+ # TODO: handle kwargs https://github.com/pytorch/pytorch/issues/79887#issuecomment-1161758372
419
+ x = checkpoint(r, x, None, None, attn_mask)
420
+ else:
421
+ x = r(x, attn_mask=attn_mask)
422
+
423
+ if not self.batch_first:
424
+ x = x.transpose(0, 1) # NLD -> LND
425
+ return x
426
+
427
+
428
+ class VisionTransformer(nn.Module):
429
+ output_tokens: torch.jit.Final[bool]
430
+
431
+ def __init__(
432
+ self,
433
+ image_size: int,
434
+ patch_size: int,
435
+ width: int,
436
+ layers: int,
437
+ heads: int,
438
+ mlp_ratio: float,
439
+ ls_init_value: float = None,
440
+ attentional_pool: bool = False,
441
+ attn_pooler_queries: int = 256,
442
+ attn_pooler_heads: int = 8,
443
+ output_dim: int = 512,
444
+ patch_dropout: float = 0.,
445
+ no_ln_pre: bool = False,
446
+ pos_embed_type: str = 'learnable',
447
+ pool_type: str = 'tok',
448
+ final_ln_after_pool: bool = False,
449
+ act_layer: Callable = nn.GELU,
450
+ norm_layer: Callable = LayerNorm,
451
+ output_tokens: bool = False,
452
+ ):
453
+ super().__init__()
454
+ assert pool_type in ('tok', 'avg', 'none')
455
+ self.output_tokens = output_tokens
456
+ image_height, image_width = self.image_size = to_2tuple(image_size)
457
+ patch_height, patch_width = self.patch_size = to_2tuple(patch_size)
458
+ self.grid_size = (image_height // patch_height, image_width // patch_width)
459
+ self.final_ln_after_pool = final_ln_after_pool # currently ignored w/ attn pool enabled
460
+ self.output_dim = output_dim
461
+
462
+ self.conv1 = nn.Conv2d(in_channels=3, out_channels=width, kernel_size=patch_size, stride=patch_size, bias=False)
463
+
464
+ # class embeddings and positional embeddings
465
+ scale = width ** -0.5
466
+ self.class_embedding = nn.Parameter(scale * torch.randn(width))
467
+ if pos_embed_type == 'learnable':
468
+ self.positional_embedding = nn.Parameter(
469
+ scale * torch.randn(self.grid_size[0] * self.grid_size[1] + 1, width))
470
+ elif pos_embed_type == 'sin_cos_2d':
471
+ # fixed sin-cos embedding
472
+ assert self.grid_size[0] == self.grid_size[1], \
473
+ 'currently sin cos 2d pos embedding only supports square input'
474
+ self.positional_embedding = nn.Parameter(
475
+ torch.zeros(self.grid_size[0] * self.grid_size[1] + 1, width), requires_grad=False)
476
+ pos_embed_type = get_2d_sincos_pos_embed(width, self.grid_size[0], cls_token=True)
477
+ self.positional_embedding.data.copy_(torch.from_numpy(pos_embed_type).float())
478
+ else:
479
+ raise ValueError
480
+
481
+ # setting a patch_dropout of 0. would mean it is disabled and this function would be the identity fn
482
+ self.patch_dropout = PatchDropout(patch_dropout) if patch_dropout > 0. else nn.Identity()
483
+
484
+ self.ln_pre = nn.Identity() if no_ln_pre else norm_layer(width)
485
+ self.transformer = Transformer(
486
+ width,
487
+ layers,
488
+ heads,
489
+ mlp_ratio,
490
+ ls_init_value=ls_init_value,
491
+ act_layer=act_layer,
492
+ norm_layer=norm_layer,
493
+ )
494
+
495
+ if attentional_pool:
496
+ if isinstance(attentional_pool, str):
497
+ self.attn_pool_type = attentional_pool
498
+ self.pool_type = 'none'
499
+ if attentional_pool in ('parallel', 'cascade'):
500
+ self.attn_pool = AttentionalPooler(
501
+ output_dim,
502
+ width,
503
+ n_head=attn_pooler_heads,
504
+ n_queries=attn_pooler_queries,
505
+ )
506
+ self.attn_pool_contrastive = AttentionalPooler(
507
+ output_dim,
508
+ width,
509
+ n_head=attn_pooler_heads,
510
+ n_queries=1,
511
+ )
512
+ else:
513
+ assert False
514
+ else:
515
+ self.attn_pool_type = ''
516
+ self.pool_type = pool_type
517
+ self.attn_pool = AttentionalPooler(
518
+ output_dim,
519
+ width,
520
+ n_head=attn_pooler_heads,
521
+ n_queries=attn_pooler_queries,
522
+ )
523
+ self.attn_pool_contrastive = None
524
+ pool_dim = output_dim
525
+ else:
526
+ self.attn_pool = None
527
+ pool_dim = width
528
+ self.pool_type = pool_type
529
+
530
+ self.ln_post = norm_layer(pool_dim)
531
+ self.proj = nn.Parameter(scale * torch.randn(pool_dim, output_dim))
532
+
533
+ self.init_parameters()
534
+
535
+ def lock(self, unlocked_groups=0, freeze_bn_stats=False):
536
+ for param in self.parameters():
537
+ param.requires_grad = False
538
+
539
+ if unlocked_groups != 0:
540
+ groups = [
541
+ [
542
+ self.conv1,
543
+ self.class_embedding,
544
+ self.positional_embedding,
545
+ self.ln_pre,
546
+ ],
547
+ *self.transformer.resblocks[:-1],
548
+ [
549
+ self.transformer.resblocks[-1],
550
+ self.ln_post,
551
+ ],
552
+ self.proj,
553
+ ]
554
+
555
+ def _unlock(x):
556
+ if isinstance(x, Sequence):
557
+ for g in x:
558
+ _unlock(g)
559
+ else:
560
+ if isinstance(x, torch.nn.Parameter):
561
+ x.requires_grad = True
562
+ else:
563
+ for p in x.parameters():
564
+ p.requires_grad = True
565
+
566
+ _unlock(groups[-unlocked_groups:])
567
+
568
+ def init_parameters(self):
569
+ # FIXME OpenAI CLIP did not define an init for the VisualTransformer
570
+ # TODO experiment if default PyTorch init, below, or alternate init is best.
571
+
572
+ # nn.init.normal_(self.class_embedding, std=self.scale)
573
+ # nn.init.normal_(self.positional_embedding, std=self.scale)
574
+ #
575
+ # proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5)
576
+ # attn_std = self.transformer.width ** -0.5
577
+ # fc_std = (2 * self.transformer.width) ** -0.5
578
+ # for block in self.transformer.resblocks:
579
+ # nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
580
+ # nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
581
+ # nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
582
+ # nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
583
+ #
584
+ # if self.text_projection is not None:
585
+ # nn.init.normal_(self.text_projection, std=self.scale)
586
+ pass
587
+
588
+ @torch.jit.ignore
589
+ def set_grad_checkpointing(self, enable=True):
590
+ self.transformer.grad_checkpointing = enable
591
+
592
+ def _global_pool(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
593
+ if self.pool_type == 'avg':
594
+ pooled, tokens = x[:, 1:].mean(dim=1), x[:, 1:]
595
+ elif self.pool_type == 'tok':
596
+ pooled, tokens = x[:, 0], x[:, 1:]
597
+ else:
598
+ pooled = tokens = x
599
+
600
+ return pooled, tokens
601
+
602
+ def forward(self, x: torch.Tensor):
603
+ x = self.conv1(x) # shape = [*, width, grid, grid]
604
+ x = x.reshape(x.shape[0], x.shape[1], -1) # shape = [*, width, grid ** 2]
605
+ x = x.permute(0, 2, 1) # shape = [*, grid ** 2, width]
606
+
607
+ # class embeddings and positional embeddings
608
+ x = torch.cat([_expand_token(self.class_embedding, x.shape[0]).to(x.dtype), x], dim=1)
609
+ # shape = [*, grid ** 2 + 1, width]
610
+ x = x + self.positional_embedding.to(x.dtype)
611
+
612
+ x = self.patch_dropout(x)
613
+ x = self.ln_pre(x)
614
+ x = self.transformer(x)
615
+
616
+ if self.attn_pool is not None:
617
+ if self.attn_pool_contrastive is not None:
618
+ # This is untested, WIP pooling that should match paper
619
+ x = self.ln_post(x) # TBD LN first or separate one after each pool?
620
+ tokens = self.attn_pool(x)
621
+ if self.attn_pool_type == 'parallel':
622
+ pooled = self.attn_pool_contrastive(x)
623
+ else:
624
+ assert self.attn_pool_type == 'cascade'
625
+ pooled = self.attn_pool_contrastive(tokens)
626
+ else:
627
+ # this is the original OpenCLIP CoCa setup, does not match paper
628
+ x = self.attn_pool(x)
629
+ x = self.ln_post(x)
630
+ pooled, tokens = self._global_pool(x)
631
+ elif self.final_ln_after_pool:
632
+ pooled, tokens = self._global_pool(x)
633
+ pooled = self.ln_post(pooled)
634
+ else:
635
+ x = self.ln_post(x)
636
+ pooled, tokens = self._global_pool(x)
637
+
638
+ if self.proj is not None:
639
+ pooled = pooled @ self.proj
640
+
641
+ if self.output_tokens:
642
+ return pooled, tokens
643
+
644
+ return pooled
645
+
646
+
647
+ def text_global_pool(x, text: Optional[torch.Tensor] = None, pool_type: str = 'argmax'):
648
+ if pool_type == 'first':
649
+ pooled, tokens = x[:, 0], x[:, 1:]
650
+ elif pool_type == 'last':
651
+ pooled, tokens = x[:, -1], x[:, :-1]
652
+ elif pool_type == 'argmax':
653
+ # take features from the eot embedding (eot_token is the highest number in each sequence)
654
+ assert text is not None
655
+ pooled, tokens = x[torch.arange(x.shape[0]), text.argmax(dim=-1)], x
656
+ else:
657
+ pooled = tokens = x
658
+
659
+ return pooled, tokens
660
+
661
+
662
+ class TextTransformer(nn.Module):
663
+ output_tokens: torch.jit.Final[bool]
664
+
665
+ def __init__(
666
+ self,
667
+ context_length: int = 77,
668
+ vocab_size: int = 49408,
669
+ width: int = 512,
670
+ heads: int = 8,
671
+ layers: int = 12,
672
+ mlp_ratio: float = 4.0,
673
+ ls_init_value: float = None,
674
+ output_dim: int = 512,
675
+ embed_cls: bool = False,
676
+ no_causal_mask: bool = False,
677
+ pad_id: int = 0,
678
+ pool_type: str = 'argmax',
679
+ proj_bias: bool = False,
680
+ act_layer: Callable = nn.GELU,
681
+ norm_layer: Callable = LayerNorm,
682
+ output_tokens: bool = False,
683
+ ):
684
+ super().__init__()
685
+ assert pool_type in ('first', 'last', 'argmax', 'none')
686
+ self.output_tokens = output_tokens
687
+ self.num_pos = self.context_length = context_length
688
+ self.vocab_size = vocab_size
689
+ self.width = width
690
+ self.output_dim = output_dim
691
+ self.heads = heads
692
+ self.pad_id = pad_id
693
+ self.pool_type = pool_type
694
+
695
+ self.token_embedding = nn.Embedding(vocab_size, width)
696
+ if embed_cls:
697
+ self.cls_emb = nn.Parameter(torch.empty(width))
698
+ self.num_pos += 1
699
+ else:
700
+ self.cls_emb = None
701
+ self.positional_embedding = nn.Parameter(torch.empty(self.num_pos, width))
702
+ self.transformer = Transformer(
703
+ width=width,
704
+ layers=layers,
705
+ heads=heads,
706
+ mlp_ratio=mlp_ratio,
707
+ ls_init_value=ls_init_value,
708
+ act_layer=act_layer,
709
+ norm_layer=norm_layer,
710
+ )
711
+ self.ln_final = norm_layer(width)
712
+
713
+ if no_causal_mask:
714
+ self.attn_mask = None
715
+ else:
716
+ self.register_buffer('attn_mask', self.build_causal_mask(), persistent=False)
717
+
718
+ if proj_bias:
719
+ self.text_projection = nn.Linear(width, output_dim)
720
+ else:
721
+ self.text_projection = nn.Parameter(torch.empty(width, output_dim))
722
+
723
+ self.init_parameters()
724
+
725
+ def init_parameters(self):
726
+ nn.init.normal_(self.token_embedding.weight, std=0.02)
727
+ nn.init.normal_(self.positional_embedding, std=0.01)
728
+ if self.cls_emb is not None:
729
+ nn.init.normal_(self.cls_emb, std=0.01)
730
+
731
+ proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5)
732
+ attn_std = self.transformer.width ** -0.5
733
+ fc_std = (2 * self.transformer.width) ** -0.5
734
+ for block in self.transformer.resblocks:
735
+ nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
736
+ nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
737
+ nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
738
+ nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
739
+
740
+ if self.text_projection is not None:
741
+ if isinstance(self.text_projection, nn.Linear):
742
+ nn.init.normal_(self.text_projection.weight, std=self.transformer.width ** -0.5)
743
+ if self.text_projection.bias is not None:
744
+ nn.init.zeros_(self.text_projection.bias)
745
+ else:
746
+ nn.init.normal_(self.text_projection, std=self.transformer.width ** -0.5)
747
+
748
+ @torch.jit.ignore
749
+ def set_grad_checkpointing(self, enable=True):
750
+ self.transformer.grad_checkpointing = enable
751
+
752
+ def build_causal_mask(self):
753
+ # lazily create causal attention mask, with full attention between the tokens
754
+ # pytorch uses additive attention mask; fill with -inf
755
+ mask = torch.empty(self.num_pos, self.num_pos)
756
+ mask.fill_(float("-inf"))
757
+ mask.triu_(1) # zero out the lower diagonal
758
+ return mask
759
+
760
+ def build_cls_mask(self, text, cast_dtype: torch.dtype):
761
+ cls_mask = (text != self.pad_id).unsqueeze(1)
762
+ cls_mask = F.pad(cls_mask, (1, 0, cls_mask.shape[2], 0), value=True)
763
+ additive_mask = torch.empty(cls_mask.shape, dtype=cast_dtype, device=cls_mask.device)
764
+ additive_mask.fill_(0)
765
+ additive_mask.masked_fill_(~cls_mask, float("-inf"))
766
+ additive_mask = torch.repeat_interleave(additive_mask, self.heads, 0)
767
+ return additive_mask
768
+
769
+ def forward(self, text):
770
+ cast_dtype = self.transformer.get_cast_dtype()
771
+ seq_len = text.shape[1]
772
+
773
+ x = self.token_embedding(text).to(cast_dtype) # [batch_size, n_ctx, d_model]
774
+ attn_mask = self.attn_mask
775
+ if self.cls_emb is not None:
776
+ seq_len += 1
777
+ x = torch.cat([x, _expand_token(self.cls_emb, x.shape[0])], dim=1)
778
+ cls_mask = self.build_cls_mask(text, cast_dtype)
779
+ if attn_mask is not None:
780
+ attn_mask = attn_mask[None, :seq_len, :seq_len] + cls_mask[:, :seq_len, :seq_len]
781
+
782
+ x = x + self.positional_embedding[:seq_len].to(cast_dtype)
783
+ x = self.transformer(x, attn_mask=attn_mask)
784
+
785
+ # x.shape = [batch_size, n_ctx, transformer.width]
786
+ if self.cls_emb is not None:
787
+ # presence of appended cls embed (CoCa) overrides pool_type, always take last token
788
+ pooled, tokens = text_global_pool(x, pool_type='last')
789
+ pooled = self.ln_final(pooled) # final LN applied after pooling in this case
790
+ else:
791
+ x = self.ln_final(x)
792
+ pooled, tokens = text_global_pool(x, text, pool_type=self.pool_type)
793
+
794
+ if self.text_projection is not None:
795
+ if isinstance(self.text_projection, nn.Linear):
796
+ pooled = self.text_projection(pooled)
797
+ else:
798
+ pooled = pooled @ self.text_projection
799
+
800
+ if self.output_tokens:
801
+ return pooled, tokens
802
+
803
+ return pooled
804
+
805
+
806
+ class MultimodalTransformer(Transformer):
807
+ def __init__(
808
+ self,
809
+ width: int,
810
+ layers: int,
811
+ heads: int,
812
+ context_length: int = 77,
813
+ mlp_ratio: float = 4.0,
814
+ ls_init_value: float = None,
815
+ act_layer: Callable = nn.GELU,
816
+ norm_layer: Callable = LayerNorm,
817
+ output_dim: int = 512,
818
+ ):
819
+
820
+ super().__init__(
821
+ width=width,
822
+ layers=layers,
823
+ heads=heads,
824
+ mlp_ratio=mlp_ratio,
825
+ ls_init_value=ls_init_value,
826
+ act_layer=act_layer,
827
+ norm_layer=norm_layer,
828
+ )
829
+ self.context_length = context_length
830
+ self.cross_attn = nn.ModuleList([
831
+ ResidualAttentionBlock(
832
+ width,
833
+ heads,
834
+ mlp_ratio,
835
+ ls_init_value=ls_init_value,
836
+ act_layer=act_layer,
837
+ norm_layer=norm_layer,
838
+ is_cross_attention=True,
839
+ )
840
+ for _ in range(layers)
841
+ ])
842
+
843
+ self.register_buffer('attn_mask', self.build_attention_mask(), persistent=False)
844
+
845
+ self.ln_final = norm_layer(width)
846
+ self.text_projection = nn.Parameter(torch.empty(width, output_dim))
847
+
848
+ def init_parameters(self):
849
+ proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5)
850
+ attn_std = self.transformer.width ** -0.5
851
+ fc_std = (2 * self.transformer.width) ** -0.5
852
+ for block in self.transformer.resblocks:
853
+ nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
854
+ nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
855
+ nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
856
+ nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
857
+ for block in self.transformer.cross_attn:
858
+ nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
859
+ nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
860
+ nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
861
+ nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
862
+
863
+ if self.text_projection is not None:
864
+ nn.init.normal_(self.text_projection, std=self.transformer.width ** -0.5)
865
+
866
+ def build_attention_mask(self):
867
+ # lazily create causal attention mask, with full attention between the tokens
868
+ # pytorch uses additive attention mask; fill with -inf
869
+ mask = torch.empty(self.context_length, self.context_length)
870
+ mask.fill_(float("-inf"))
871
+ mask.triu_(1) # zero out the lower diagonal
872
+ return mask
873
+
874
+ def forward(self, image_embs, text_embs):
875
+ text_embs = text_embs.permute(1, 0, 2) # NLD -> LNDsq
876
+ image_embs = image_embs.permute(1, 0, 2) # NLD -> LND
877
+ seq_len = text_embs.shape[0]
878
+
879
+ for resblock, cross_attn in zip(self.resblocks, self.cross_attn):
880
+ if self.grad_checkpointing and not torch.jit.is_scripting():
881
+ # TODO: handle kwargs https://github.com/pytorch/pytorch/issues/79887#issuecomment-1161758372
882
+ text_embs = checkpoint(resblock, text_embs, None, None, self.attn_mask[:seq_len, :seq_len])
883
+ text_embs = checkpoint(cross_attn, text_embs, image_embs, image_embs, None)
884
+ else:
885
+ text_embs = resblock(text_embs, attn_mask=self.attn_mask[:seq_len, :seq_len])
886
+ text_embs = cross_attn(text_embs, k_x=image_embs, v_x=image_embs)
887
+
888
+ x = text_embs.permute(1, 0, 2) # LND -> NLD
889
+ x = self.ln_final(x)
890
+
891
+ if self.text_projection is not None:
892
+ x = x @ self.text_projection
893
+
894
+ return x
895
+
896
+ @torch.jit.ignore
897
+ def set_grad_checkpointing(self, enable=True):
898
+ self.grad_checkpointing = enable
899
+
900
+
901
+ class EcgTransformer(nn.Module):
902
+ output_tokens: torch.jit.Final[bool]
903
+
904
+ def __init__(
905
+ self,
906
+ seq_length: int,
907
+ lead_num: int,
908
+ patch_size: int,
909
+ width: int,
910
+ layers: int,
911
+ heads: int,
912
+ mlp_ratio: float,
913
+ ls_init_value: float = None,
914
+ attentional_pool: bool = False,
915
+ attn_pooler_queries: int = 256,
916
+ attn_pooler_heads: int = 8,
917
+ output_dim: int = 512,
918
+ patch_dropout: float = 0.,
919
+ no_ln_pre: bool = False,
920
+ pool_type: str = 'tok',
921
+ final_ln_after_pool: bool = False,
922
+ pos_embed_type="learnable",
923
+ act_layer: Callable = nn.GELU,
924
+ norm_layer: Callable = LayerNorm,
925
+ output_tokens: bool = False,
926
+ ):
927
+ super().__init__()
928
+ assert pool_type in ('tok', 'avg', 'none')
929
+ self.output_tokens = output_tokens
930
+ self.seq_length = seq_length
931
+ self.lead_num = lead_num
932
+ self.patch_size = patch_size
933
+ assert seq_length % patch_size == 0
934
+ self.patch_nums = seq_length // patch_size
935
+ self.final_ln_after_pool = final_ln_after_pool # currently ignored w/ attn pool enabled
936
+ self.output_dim = output_dim
937
+
938
+ self.conv1 = nn.Conv1d(in_channels=lead_num, out_channels=width, kernel_size=patch_size, stride=patch_size,
939
+ bias=False)
940
+
941
+ # class embeddings and positional embeddings
942
+ scale = width ** -0.5
943
+ self.class_embedding = nn.Parameter(scale * torch.randn(width))
944
+
945
+ self.positional_embedding = nn.Parameter(
946
+ scale * torch.randn(self.patch_nums + 1, width))
947
+
948
+ # setting a patch_dropout of 0. would mean it is disabled and this function would be the identity fn
949
+ self.patch_dropout = PatchDropout(patch_dropout) if patch_dropout > 0. else nn.Identity()
950
+
951
+ self.ln_pre = nn.Identity() if no_ln_pre else norm_layer(width)
952
+ self.transformer = Transformer(
953
+ width,
954
+ layers,
955
+ heads,
956
+ mlp_ratio,
957
+ ls_init_value=ls_init_value,
958
+ act_layer=act_layer,
959
+ norm_layer=norm_layer,
960
+ )
961
+
962
+ if attentional_pool:
963
+ if isinstance(attentional_pool, str):
964
+ self.attn_pool_type = attentional_pool
965
+ self.pool_type = 'none'
966
+ if attentional_pool in ('parallel', 'cascade'):
967
+ self.attn_pool = AttentionalPooler(
968
+ output_dim,
969
+ width,
970
+ n_head=attn_pooler_heads,
971
+ n_queries=attn_pooler_queries,
972
+ )
973
+ self.attn_pool_contrastive = AttentionalPooler(
974
+ output_dim,
975
+ width,
976
+ n_head=attn_pooler_heads,
977
+ n_queries=1,
978
+ )
979
+ else:
980
+ assert False
981
+ else:
982
+ self.attn_pool_type = ''
983
+ self.pool_type = pool_type
984
+ self.attn_pool = AttentionalPooler(
985
+ output_dim,
986
+ width,
987
+ n_head=attn_pooler_heads,
988
+ n_queries=attn_pooler_queries,
989
+ )
990
+ self.attn_pool_contrastive = None
991
+ pool_dim = output_dim
992
+ else:
993
+ self.attn_pool = None
994
+ pool_dim = width
995
+ self.pool_type = pool_type
996
+
997
+ self.ln_post = norm_layer(pool_dim)
998
+ self.proj = nn.Parameter(scale * torch.randn(pool_dim, output_dim))
999
+
1000
+ self.init_parameters()
1001
+
1002
+ def lock(self, unlocked_groups=0, freeze_bn_stats=False):
1003
+ for param in self.parameters():
1004
+ param.requires_grad = False
1005
+
1006
+ if unlocked_groups != 0:
1007
+ groups = [
1008
+ [
1009
+ self.conv1,
1010
+ self.class_embedding,
1011
+ self.positional_embedding,
1012
+ self.ln_pre,
1013
+ ],
1014
+ *self.transformer.resblocks[:-1],
1015
+ [
1016
+ self.transformer.resblocks[-1],
1017
+ self.ln_post,
1018
+ ],
1019
+ self.proj,
1020
+ ]
1021
+
1022
+ def _unlock(x):
1023
+ if isinstance(x, Sequence):
1024
+ for g in x:
1025
+ _unlock(g)
1026
+ else:
1027
+ if isinstance(x, torch.nn.Parameter):
1028
+ x.requires_grad = True
1029
+ else:
1030
+ for p in x.parameters():
1031
+ p.requires_grad = True
1032
+
1033
+ _unlock(groups[-unlocked_groups:])
1034
+
1035
+ def init_parameters(self):
1036
+ # FIXME OpenAI CLIP did not define an init for the VisualTransformer
1037
+ # TODO experiment if default PyTorch init, below, or alternate init is best.
1038
+
1039
+ # nn.init.normal_(self.class_embedding, std=self.scale)
1040
+ # nn.init.normal_(self.positional_embedding, std=self.scale)
1041
+ #
1042
+ # proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5)
1043
+ # attn_std = self.transformer.width ** -0.5
1044
+ # fc_std = (2 * self.transformer.width) ** -0.5
1045
+ # for block in self.transformer.resblocks:
1046
+ # nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
1047
+ # nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
1048
+ # nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
1049
+ # nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
1050
+ #
1051
+ # if self.text_projection is not None:
1052
+ # nn.init.normal_(self.text_projection, std=self.scale)
1053
+ pass
1054
+
1055
+ @torch.jit.ignore
1056
+ def set_grad_checkpointing(self, enable=True):
1057
+ self.transformer.grad_checkpointing = enable
1058
+
1059
+ def _global_pool(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
1060
+ if self.pool_type == 'avg':
1061
+ pooled, tokens = x[:, 1:].mean(dim=1), x[:, 1:]
1062
+ elif self.pool_type == 'tok':
1063
+ pooled, tokens = x[:, 0], x[:, 1:]
1064
+ else:
1065
+ pooled = tokens = x
1066
+
1067
+ return pooled, tokens
1068
+
1069
+ def forward(self, x: torch.Tensor, output_last_transformer_layer=False):
1070
+ x = self.conv1(x) # shape = [*, width, grid]
1071
+ x = x.permute(0, 2, 1) # shape = [*, grid, width]
1072
+ # print(x.shape)
1073
+ # class embeddings and positional embeddings
1074
+ x = torch.cat([_expand_token(self.class_embedding, x.shape[0]).to(x.dtype), x], dim=1)
1075
+ # print(self.class_embedding.shape)
1076
+ # print(x.shape)
1077
+ # shape = [*, grid + 1, width]
1078
+ x = x + self.positional_embedding.to(x.dtype)
1079
+ x = self.patch_dropout(x)
1080
+ x = self.ln_pre(x)
1081
+ x = self.transformer(x)
1082
+
1083
+ if output_last_transformer_layer:
1084
+ return x
1085
+
1086
+ if self.attn_pool is not None:
1087
+ if self.attn_pool_contrastive is not None:
1088
+ # This is untested, WIP pooling that should match paper
1089
+ x = self.ln_post(x) # TBD LN first or separate one after each pool?
1090
+ tokens = self.attn_pool(x)
1091
+ if self.attn_pool_type == 'parallel':
1092
+ pooled = self.attn_pool_contrastive(x)
1093
+ else:
1094
+ assert self.attn_pool_type == 'cascade'
1095
+ pooled = self.attn_pool_contrastive(tokens)
1096
+ else:
1097
+ # this is the original OpenCLIP CoCa setup, does not match paper
1098
+ x = self.attn_pool(x)
1099
+ x = self.ln_post(x)
1100
+ pooled, tokens = self._global_pool(x)
1101
+ elif self.final_ln_after_pool:
1102
+ pooled, tokens = self._global_pool(x)
1103
+ pooled = self.ln_post(pooled)
1104
+ else:
1105
+ x = self.ln_post(x)
1106
+ pooled, tokens = self._global_pool(x)
1107
+
1108
+ if self.proj is not None:
1109
+ pooled = pooled @ self.proj
1110
+
1111
+ if self.output_tokens:
1112
+ return pooled, tokens
1113
+
1114
+ return pooled
ecg_coca/open_clip/utils.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import repeat
2
+ import collections.abc
3
+
4
+ import torch
5
+ from torch import nn as nn
6
+
7
+ from torchvision.ops.misc import FrozenBatchNorm2d
8
+
9
+
10
+ def freeze_batch_norm_2d(module, module_match={}, name=''):
11
+ """
12
+ Converts all `BatchNorm2d` and `SyncBatchNorm` layers of provided module into `FrozenBatchNorm2d`. If `module` is
13
+ itself an instance of either `BatchNorm2d` or `SyncBatchNorm`, it is converted into `FrozenBatchNorm2d` and
14
+ returned. Otherwise, the module is walked recursively and submodules are converted in place.
15
+
16
+ Args:
17
+ module (torch.nn.Module): Any PyTorch module.
18
+ module_match (dict): Dictionary of full module names to freeze (all if empty)
19
+ name (str): Full module name (prefix)
20
+
21
+ Returns:
22
+ torch.nn.Module: Resulting module
23
+
24
+ Inspired by https://github.com/pytorch/pytorch/blob/a5895f85be0f10212791145bfedc0261d364f103/torch/nn/modules/batchnorm.py#L762
25
+ """
26
+ res = module
27
+ is_match = True
28
+ if module_match:
29
+ is_match = name in module_match
30
+ if is_match and isinstance(module, (nn.modules.batchnorm.BatchNorm2d, nn.modules.batchnorm.SyncBatchNorm)):
31
+ res = FrozenBatchNorm2d(module.num_features)
32
+ res.num_features = module.num_features
33
+ res.affine = module.affine
34
+ if module.affine:
35
+ res.weight.data = module.weight.data.clone().detach()
36
+ res.bias.data = module.bias.data.clone().detach()
37
+ res.running_mean.data = module.running_mean.data
38
+ res.running_var.data = module.running_var.data
39
+ res.eps = module.eps
40
+ else:
41
+ for child_name, child in module.named_children():
42
+ full_child_name = '.'.join([name, child_name]) if name else child_name
43
+ new_child = freeze_batch_norm_2d(child, module_match, full_child_name)
44
+ if new_child is not child:
45
+ res.add_module(child_name, new_child)
46
+ return res
47
+
48
+
49
+ # From PyTorch internals
50
+ def _ntuple(n):
51
+ def parse(x):
52
+ if isinstance(x, collections.abc.Iterable):
53
+ return x
54
+ return tuple(repeat(x, n))
55
+ return parse
56
+
57
+
58
+ to_1tuple = _ntuple(1)
59
+ to_2tuple = _ntuple(2)
60
+ to_3tuple = _ntuple(3)
61
+ to_4tuple = _ntuple(4)
62
+ to_ntuple = lambda n, x: _ntuple(n)(x)
63
+
64
+ # Replaces all linear layers with linear_replacement
65
+ # TODO: add int8 support for other linear layers including attn and convnets
66
+ def replace_linear(model, linear_replacement, include_modules=['c_fc', 'c_proj'], copy_weights=True):
67
+ for name, module in model.named_children():
68
+ if len(list(module.children())) > 0:
69
+ replace_linear(module, linear_replacement, include_modules, copy_weights)
70
+
71
+ if isinstance(module, torch.nn.Linear) and name in include_modules:
72
+ old_module = model._modules[name]
73
+ model._modules[name] = linear_replacement(
74
+ module.in_features,
75
+ module.out_features,
76
+ module.bias is not None,
77
+ )
78
+ if copy_weights:
79
+ model._modules[name].weight.data.copy_(old_module.weight.data)
80
+ if model._modules[name].bias is not None:
81
+ model._modules[name].bias.data.copy_(old_module.bias)
82
+
83
+ return model
84
+
85
+ def convert_int8_model_to_inference_mode(model):
86
+ for m in model.modules():
87
+ if hasattr(m, 'prepare_for_eval'):
88
+ int8_original_dtype = m.weight.dtype
89
+ m.prepare_for_eval()
90
+ m.int8_original_dtype = int8_original_dtype
ecg_coca/open_clip/version.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = '2.24.0'
ecg_coca/training/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ logs/
ecg_coca/training/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .main import get_ecg_encoder
ecg_coca/training/data.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import wfdb
3
+ from dataclasses import dataclass
4
+ from multiprocessing import Value
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import torch
9
+ from sklearn.model_selection import train_test_split
10
+ from torch.utils.data import Dataset, DataLoader
11
+ from torch.utils.data.distributed import DistributedSampler
12
+ from .evaluation.metadata import zero_shot_class
13
+ from .distributed import is_master
14
+
15
+ try:
16
+ import horovod.torch as hvd
17
+ except ImportError:
18
+ hvd = None
19
+
20
+
21
+ class ECGTextDataset(Dataset):
22
+ def __init__(self, path, texts, transforms=None, tokenizer=None, is_train=True):
23
+ super(ECGTextDataset, self).__init__()
24
+ self.transforms = transforms
25
+ self.tokenizer = tokenizer
26
+ self.path = path
27
+ self.y = texts
28
+ self.is_train = is_train
29
+
30
+ def tokenize(self, text):
31
+ text = text.lower()
32
+ encoded = self.tokenizer(
33
+ text,
34
+ )
35
+ return encoded[0]
36
+
37
+ def load_data(self, idx):
38
+ data = wfdb.rdsamp(self.path[idx])[0]
39
+ data[np.isnan(data)] = 0
40
+ data[np.isinf(data)] = 0
41
+ data = torch.Tensor(np.transpose(data, (1, 0)).astype(np.float32))
42
+ data = torch.unsqueeze(data, 0)
43
+
44
+ if self.transforms is not None:
45
+ data = self.transforms(data)
46
+ data = torch.squeeze(data, 0)
47
+ return data
48
+
49
+ def __len__(self):
50
+ return len(self.y)
51
+
52
+ def __getitem__(self, idx):
53
+ x = self.load_data(idx)
54
+ y = self.y[idx]
55
+ return x, self.tokenize(y)
56
+
57
+
58
+ class ECGValDataset(ECGTextDataset):
59
+ def __init__(self, dir, path, diagnostics, transforms=None, tokenizer=None):
60
+ abs_path = [os.path.join(dir, p) for p in path]
61
+ super(ECGValDataset, self).__init__(abs_path, None, transforms, tokenizer)
62
+ self.diagnostics = diagnostics
63
+
64
+ def __len__(self):
65
+ return self.diagnostics.shape[0]
66
+
67
+ def __getitem__(self, idx):
68
+ x = self.load_data(idx)
69
+ diagnostic = self.diagnostics[idx, :]
70
+ return x, diagnostic
71
+
72
+
73
+ class SharedEpoch:
74
+ def __init__(self, epoch: int = 0):
75
+ self.shared_epoch = Value('i', epoch)
76
+
77
+ def set_value(self, epoch):
78
+ self.shared_epoch.value = epoch
79
+
80
+ def get_value(self):
81
+ return self.shared_epoch.value
82
+
83
+
84
+ @dataclass
85
+ class DataInfo:
86
+ dataloader: DataLoader
87
+ sampler: DistributedSampler = None
88
+ shared_epoch: SharedEpoch = None
89
+
90
+ def set_epoch(self, epoch):
91
+ if self.shared_epoch is not None:
92
+ self.shared_epoch.set_value(epoch)
93
+ if self.sampler is not None and isinstance(self.sampler, DistributedSampler):
94
+ self.sampler.set_epoch(epoch)
95
+
96
+
97
+ def count_samples(dataloader):
98
+ os.environ["WDS_EPOCH"] = "0"
99
+ n_elements, n_batches = 0, 0
100
+ for images, texts in dataloader:
101
+ n_batches += 1
102
+ n_elements += len(images)
103
+ assert len(images) == len(texts)
104
+ return n_elements, n_batches
105
+
106
+
107
+ _SHARD_SHUFFLE_SIZE = 2000
108
+ _SHARD_SHUFFLE_INITIAL = 500
109
+ _SAMPLE_SHUFFLE_SIZE = 5000
110
+ _SAMPLE_SHUFFLE_INITIAL = 1000
111
+
112
+
113
+ def get_wave_info(data):
114
+ keys = ['RR_Interval', 'PR_Interval', 'QRS_Complex', 'QT_Interval',
115
+ 'QTc_Interval', 'P_Wave_Peak', 'R_Wave_Peak', 'T_Wave_Peak']
116
+ text_describe = ""
117
+ text_describe += f" RR: {data['RR_Interval']}"
118
+ text_describe += f" PR: {data['PR_Interval']}"
119
+ text_describe += f" QRS: {data['QRS_Complex']}"
120
+ text_describe += f" QT/QTc: {data['QT_Interval']}/{data['QTc_Interval']}"
121
+ text_describe += f" P/R/T Wave: {data['P_Wave_Peak']}/{data['R_Wave_Peak']}/{data['T_Wave_Peak']}"
122
+ return text_describe
123
+
124
+
125
+ def load_ptbxl(path, is_train, sampling_rate=500, wfep=True):
126
+ Y = pd.read_csv(os.path.join(path, 'ptbxl_database_translated.csv'), index_col='ecg_id')
127
+
128
+ test_fold = 10
129
+ if is_train:
130
+ Y = Y[Y.strat_fold != test_fold]
131
+ else:
132
+ Y = Y[Y.strat_fold == test_fold]
133
+
134
+ if sampling_rate == 500:
135
+ X_rel = Y.filename_hr.values
136
+ else:
137
+ X_rel = Y.filename_lr.values
138
+ y = Y.report.values
139
+ X = [os.path.join(path, x) for x in X_rel]
140
+
141
+ texts = []
142
+ for i in range(len(Y)):
143
+ text = (y[i].replace("ekg", "ecg")
144
+ .replace("normales ", "")
145
+ .replace("4.46 ", "")
146
+ .replace("unconfirmed report", "")
147
+ .replace("unconfirmed", "").replace(" age undetermined", ""))
148
+ if wfep:
149
+ texts.append(text + get_wave_info(Y.iloc[i]))
150
+ else:
151
+ texts.append(text)
152
+
153
+ return X, texts
154
+
155
+
156
+ def load_ptbxl_diagnostics(path, is_train, sampling_rate=500):
157
+ dataset_names = ["super_class", "sub_class", "form", "rhythm"]
158
+ data = {}
159
+ for dataset in dataset_names:
160
+ name2index = {}
161
+ for i, name in enumerate(zero_shot_class["ptbxl_" + dataset]):
162
+ name2index[name] = i
163
+ Y = pd.read_csv(os.path.join(path, f"ptbxl_database_{dataset}.csv"))
164
+
165
+ test_fold = 10
166
+ if is_train:
167
+ Y = Y[Y.strat_fold != test_fold]
168
+ else:
169
+ Y = Y[Y.strat_fold == test_fold]
170
+
171
+ if sampling_rate == 500:
172
+ X_rel = Y.filename_hr.values
173
+ else:
174
+ X_rel = Y.filename_lr.values
175
+ X = [os.path.join(path, x) for x in X_rel]
176
+ y = Y.labels.values
177
+
178
+ labels = [label.split(';') for label in y]
179
+
180
+ targets = np.zeros((len(X), len(zero_shot_class["ptbxl_" + dataset])))
181
+ for i in range(len(X)):
182
+ for lbl in labels[i]:
183
+ targets[i][name2index[lbl]] = 1
184
+
185
+ data[dataset] = (X, targets)
186
+ return data
187
+
188
+
189
+ def load_cpsc2018(path, is_train):
190
+ folder = "training_set" if is_train else "validation_set"
191
+ Y = pd.read_csv(os.path.join(path, folder, "REFERENCE.csv"))
192
+ X = Y.Recording.values
193
+ X = [os.path.join(path, folder + "/" + x) for x in X]
194
+ Y.fillna(10000)
195
+ lbl1 = Y.First_label.values.astype(int)
196
+ lbl2 = Y.Second_label.values.astype(int)
197
+ lbl3 = Y.Third_label.values.astype(int)
198
+ labels = np.zeros((len(X), 9))
199
+ for i in range(len(X)):
200
+ labels[i, lbl1[i] - 1] = 1
201
+ if 0 < lbl2[i] < 10:
202
+ labels[i, lbl2[i] - 1] = 1
203
+ if 0 < lbl3[i] < 10:
204
+ labels[i, lbl3[i] - 1] = 1
205
+ return X, labels
206
+
207
+
208
+ def load_champan_shaoxing(path, is_train, wfep=True):
209
+ Y = pd.read_csv(os.path.join(path, "diagnostics.csv"))
210
+
211
+ test_fold = 9
212
+
213
+ if is_train:
214
+ Y = Y[Y.strat_fold != test_fold]
215
+ else:
216
+ Y = Y[Y.strat_fold == test_fold]
217
+
218
+ X_rel = Y.filename.values
219
+ X = [os.path.join(path, x) for x in X_rel]
220
+ y = Y.report.values
221
+
222
+ if wfep:
223
+ for i in range(len(Y)):
224
+ y[i] = y[i] + get_wave_info(Y.iloc[i])
225
+ return X, y
226
+
227
+
228
+ def load_sph(path, is_train, wfep=False):
229
+ df = pd.read_csv(os.path.join(path, "metadata.csv"))
230
+
231
+ # 80%-20% split data accroding to the example code
232
+ test1 = df.Patient_ID.duplicated(keep=False)
233
+ N = int(len(df) * 0.2) - sum(test1)
234
+ # 73 is chosen such that all primary statements exist in both sets
235
+ df_test = pd.concat([df[test1], df[~test1].sample(N, random_state=73)])
236
+ df_train = df.iloc[df.index.difference(df_test.index)]
237
+
238
+ Y = df_train if is_train else df_test
239
+ X = Y.ECG_ID.values
240
+ X = [os.path.join(path, "records/records_wfdb/" + x) for x in X]
241
+ aha_codes = Y.AHA_Code.values
242
+
243
+ code_csv = pd.read_csv(os.path.join(path, "code.csv"))
244
+ code2text = {}
245
+ for i in range(len(code_csv)):
246
+ code2text[str(code_csv.Code.values[i])] = code_csv.Description.values[i]
247
+
248
+ texts = []
249
+ for codes in aha_codes:
250
+ code_list = codes.split(';')
251
+ text_list = []
252
+ for code in code_list:
253
+ t = " ".join(code2text[c] for c in code.split('+'))
254
+ text_list.append(t)
255
+ texts.append(";".join(text_list))
256
+ if wfep:
257
+ for i in range(len(Y)):
258
+ texts[i] = texts[i] + get_wave_info(Y.iloc[i])
259
+
260
+ return X, texts
261
+
262
+
263
+ def load_mimic_iv_ecg(path, wfep=True):
264
+ database = pd.read_csv(os.path.join(path, "machine_measurements.csv")).set_index("study_id")
265
+ record_list = pd.read_csv(os.path.join(path, "new_record_list.csv"))
266
+
267
+ indexes = record_list.index.values
268
+ np.random.seed(0)
269
+ np.random.shuffle(indexes)
270
+
271
+ train_list = record_list.loc[np.where(record_list["file_name"].values % 10 > 0)].set_index("study_id")
272
+ test_list = record_list.loc[np.where(record_list["file_name"].values % 10 == 0)].set_index("study_id")
273
+ train_indexes = train_list.index.values
274
+ val_indexes = test_list.index.values[-10000:-5000]
275
+ test_indexes = test_list.index.values[-5000:]
276
+ train_indexes = np.append(train_indexes, test_indexes[:-10000])
277
+
278
+ def data(index_list):
279
+ reports = []
280
+ X = []
281
+ n_reports = 18
282
+ bad_reports = ["--- Warning: Data quality may affect interpretation ---",
283
+ "--- Recording unsuitable for analysis - please repeat ---",
284
+ "Analysis error",
285
+ "conduction defect",
286
+ "*** report made without knowing patient's sex ***",
287
+ "--- Suspect arm lead reversal",
288
+ "--- Possible measurement error ---",
289
+ "--- Pediatric criteria used ---",
290
+ "--- Suspect limb lead reversal",
291
+ "-------------------- Pediatric ECG interpretation --------------------",
292
+ "Lead(s) unsuitable for analysis:",
293
+ "LEAD(S) UNSUITABLE FOR ANALYSIS:",
294
+ "PACER DETECTION SUSPENDED DUE TO EXTERNAL NOISE-REVIEW ADVISED",
295
+ "Pacer detection suspended due to external noise-REVIEW ADVISED"]
296
+
297
+ for i in index_list:
298
+ row = record_list.loc[i]
299
+ m_row = database.loc[i]
300
+ report_txt = ""
301
+ for j in range(n_reports):
302
+ report = m_row[f"report_{j}"]
303
+ if type(report) == str:
304
+ is_bad = False
305
+ for bad_report in bad_reports:
306
+ if report.find(bad_report) > -1:
307
+ is_bad = True
308
+ break
309
+ report_txt += (report + " ") if not is_bad else ""
310
+ if report_txt == "":
311
+ continue
312
+ report_txt = report_txt[:-1].lower()
313
+ report_txt = (report_txt.replace("---", "")
314
+ .replace("***", "")
315
+ .replace(" - age undetermined", ""))
316
+
317
+ report_txt = (report_txt.replace('rbbb', 'right bundle branch block')
318
+ .replace('lbbb', 'light bundle branch block')
319
+ .replace('lvh', 'left ventricle hypertrophy')
320
+ .replace("mi", "myocardial infarction")
321
+ .replace("lafb", "left anterior fascicular block")
322
+ .replace("pvc(s)", "ventricular premature complex")
323
+ .replace("pvcs", "ventricular premature complex")
324
+ .replace("pac(s)", "atrial premature complex")
325
+ .replace("pacs", "atrial premature complex"))
326
+ if wfep:
327
+ report_txt = report_txt + get_wave_info(row)
328
+ reports.append(report_txt)
329
+ X.append(os.path.join(path, row["path"]))
330
+ return X, reports
331
+
332
+ record_list = record_list.set_index("study_id")
333
+ train_x, train_y = data(train_indexes)
334
+
335
+ val_x, val_y = data(val_indexes)
336
+ test_x, test_y = data(test_indexes)
337
+ return train_x, train_y, val_x, val_y, test_x, test_y
338
+
339
+
340
+ def make_dataloader(args, dataset, is_train, dist_sampler=True, drop_last=None):
341
+ num_samples = len(dataset)
342
+ sampler = DistributedSampler(dataset) if args.distributed and dist_sampler else None
343
+ shuffle = is_train and sampler is None
344
+ drop_last = is_train if drop_last is None else drop_last
345
+ dataloader = DataLoader(
346
+ dataset,
347
+ batch_size=args.batch_size,
348
+ shuffle=shuffle,
349
+ num_workers=args.workers,
350
+ pin_memory=True,
351
+ sampler=sampler,
352
+ drop_last=drop_last,
353
+ )
354
+ dataloader.num_samples = num_samples
355
+ dataloader.num_batches = len(dataloader)
356
+
357
+ return DataInfo(dataloader, sampler)
358
+
359
+
360
+ def get_ptbxl_diagnostic_dataset(args, path, preprocess_fn, is_train, epoch=0, tokenizer=None):
361
+ sampling_rate = args.sampling_rate if args.sampling_rate is not None else 500
362
+ datas = load_ptbxl_diagnostics(path, is_train, sampling_rate)
363
+
364
+ dataloaders = {}
365
+ for key in datas.keys():
366
+ X, y = datas[key]
367
+ dataset = ECGValDataset(path, X, y, transforms=preprocess_fn, tokenizer=tokenizer)
368
+ dataloaders[key] = make_dataloader(args, dataset, is_train, dist_sampler=False, drop_last=False)
369
+ return dataloaders
370
+
371
+
372
+ def get_cpsc2018_diagnostic_dataset(args, path, preprocess_fn, is_train, epoch=0, tokenizer=None):
373
+ X, y = load_cpsc2018(path, is_train)
374
+ dataset = ECGValDataset(path, X, y, transforms=preprocess_fn, tokenizer=tokenizer)
375
+
376
+ return make_dataloader(args, dataset, is_train, dist_sampler=False, drop_last=False)
377
+
378
+
379
+ def get_all_ecg_text_dataset(args, preprocess_train, preprocess_test, epoch=0, tokenizer=None):
380
+ datasets = {}
381
+ X_train, text_train, X_val, text_val, m_X_test, m_text_test\
382
+ = load_mimic_iv_ecg(args.mimic_iv_ecg_path, wfep=args.wfep)
383
+ if args.champan_path:
384
+ c_X_train, c_text_train = load_champan_shaoxing(args.champan_path, is_train=True, wfep=args.wfep)
385
+ X_train = np.append(X_train, c_X_train)
386
+ text_train = np.append(text_train, c_text_train)
387
+ if args.sph_path:
388
+ s_X_train, s_text_train = load_sph(args.sph_path, is_train=True, wfep=args.wfep)
389
+ X_train = np.append(X_train, s_X_train)
390
+ text_train = np.append(text_train, s_text_train)
391
+ train_dataset = ECGTextDataset(X_train, text_train, transforms=preprocess_train, tokenizer=tokenizer,
392
+ is_train=True)
393
+ datasets['train'] = make_dataloader(args, train_dataset, is_train=True)
394
+
395
+ if is_master(args):
396
+ # if args.champan_path:
397
+ # c_X_val, c_text_val = load_champan_shaoxing(args.champan_path, is_train=False)
398
+ # # X_val = np.append(X_val, c_X_val)
399
+ # # text_val = np.append(text_val, c_text_val)
400
+ #
401
+ # if args.sph_path:
402
+ # s_X_val, s_text_val = load_sph(args.sph_path, is_train=False)
403
+ # X_val = np.append(X_val, s_X_val)
404
+ # text_val = np.append(text_val, s_text_val)
405
+
406
+ val_dataset = ECGTextDataset(X_val, text_val, transforms=preprocess_test, tokenizer=tokenizer, is_train=False)
407
+ mimic_test_dataset = ECGTextDataset(m_X_test, m_text_test, transforms=preprocess_test,
408
+ tokenizer=tokenizer, is_train=False)
409
+ datasets['val'] = make_dataloader(args, val_dataset, is_train=False, dist_sampler=False)
410
+ datasets['test_mimic'] = make_dataloader(args, mimic_test_dataset, is_train=False, dist_sampler=False)
411
+
412
+ if args.ptbxl_path:
413
+ p_X_test, p_text_test = load_ptbxl(args.ptbxl_path, is_train=False, wfep=args.wfep)
414
+ ptbxl_test_dataset = ECGTextDataset(p_X_test, p_text_test, transforms=preprocess_test,
415
+ tokenizer=tokenizer, is_train=False)
416
+ datasets['test_ptbxl'] = make_dataloader(args, ptbxl_test_dataset, is_train=False, dist_sampler=False)
417
+ return datasets
418
+
419
+
420
+ def get_data(args, preprocess_fns, epoch=0, tokenizer=None):
421
+ preprocess_train, preprocess_val = preprocess_fns
422
+
423
+ data = get_all_ecg_text_dataset(args, preprocess_train, preprocess_val, epoch=epoch, tokenizer=tokenizer)
424
+ if is_master(args):
425
+ if args.ptbxl_path:
426
+ ptbxl_train_datas = get_ptbxl_diagnostic_dataset(
427
+ args, args.ptbxl_path, preprocess_val, is_train=True, epoch=epoch, tokenizer=tokenizer)
428
+ ptbxl_test_datas = get_ptbxl_diagnostic_dataset(
429
+ args, args.ptbxl_path, preprocess_val, is_train=False, epoch=epoch, tokenizer=tokenizer)
430
+
431
+ for key in ptbxl_train_datas.keys():
432
+ data[f"train_ptbxl_{key}"] = ptbxl_train_datas[key]
433
+ data[f"val_ptbxl_{key}"] = ptbxl_test_datas[key]
434
+ if args.cpsc2018_path:
435
+ data["train_cpsc2018"] = get_cpsc2018_diagnostic_dataset(
436
+ args, args.cpsc2018_path, preprocess_val, is_train=True, epoch=epoch, tokenizer=tokenizer)
437
+ data["val_cpsc2018"] = get_cpsc2018_diagnostic_dataset(
438
+ args, args.cpsc2018_path, preprocess_val, is_train=False, epoch=epoch, tokenizer=tokenizer)
439
+
440
+ return data
ecg_coca/training/distributed.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import torch
4
+ import torch.distributed as dist
5
+
6
+ try:
7
+ import horovod.torch as hvd
8
+ except ImportError:
9
+ hvd = None
10
+
11
+
12
+ def is_global_master(args):
13
+ return args.rank == 0
14
+
15
+
16
+ def is_local_master(args):
17
+ return args.local_rank == 0
18
+
19
+
20
+ def is_master(args, local=False):
21
+ return is_local_master(args) if local else is_global_master(args)
22
+
23
+
24
+ def is_using_horovod():
25
+ # NOTE w/ horovod run, OMPI vars should be set, but w/ SLURM PMI vars will be set
26
+ # Differentiating between horovod and DDP use via SLURM may not be possible, so horovod arg still required...
27
+ ompi_vars = ["OMPI_COMM_WORLD_RANK", "OMPI_COMM_WORLD_SIZE"]
28
+ pmi_vars = ["PMI_RANK", "PMI_SIZE"]
29
+ if all([var in os.environ for var in ompi_vars]) or all([var in os.environ for var in pmi_vars]):
30
+ return True
31
+ else:
32
+ return False
33
+
34
+
35
+ def is_using_distributed():
36
+ if 'WORLD_SIZE' in os.environ:
37
+ return int(os.environ['WORLD_SIZE']) > 1
38
+ if 'SLURM_NTASKS' in os.environ:
39
+ return int(os.environ['SLURM_NTASKS']) > 1
40
+ return False
41
+
42
+
43
+ def world_info_from_env():
44
+ local_rank = 0
45
+ for v in ('LOCAL_RANK', 'MPI_LOCALRANKID', 'SLURM_LOCALID', 'OMPI_COMM_WORLD_LOCAL_RANK'):
46
+ if v in os.environ:
47
+ local_rank = int(os.environ[v])
48
+ break
49
+ global_rank = 0
50
+ for v in ('RANK', 'PMI_RANK', 'SLURM_PROCID', 'OMPI_COMM_WORLD_RANK'):
51
+ if v in os.environ:
52
+ global_rank = int(os.environ[v])
53
+ break
54
+ world_size = 1
55
+ for v in ('WORLD_SIZE', 'PMI_SIZE', 'SLURM_NTASKS', 'OMPI_COMM_WORLD_SIZE'):
56
+ if v in os.environ:
57
+ world_size = int(os.environ[v])
58
+ break
59
+
60
+ return local_rank, global_rank, world_size
61
+
62
+
63
+ def init_distributed_device(args):
64
+ # Distributed training = training on more than one GPU.
65
+ # Works in both single and multi-node scenarios.
66
+ args.distributed = False
67
+ args.world_size = 1
68
+ args.rank = 0 # global rank
69
+ args.local_rank = 0
70
+ if args.horovod:
71
+ assert hvd is not None, "Horovod is not installed"
72
+ hvd.init()
73
+ args.local_rank = int(hvd.local_rank())
74
+ args.rank = hvd.rank()
75
+ args.world_size = hvd.size()
76
+ args.distributed = True
77
+ os.environ['LOCAL_RANK'] = str(args.local_rank)
78
+ os.environ['RANK'] = str(args.rank)
79
+ os.environ['WORLD_SIZE'] = str(args.world_size)
80
+ elif is_using_distributed():
81
+ if 'SLURM_PROCID' in os.environ:
82
+ # DDP via SLURM
83
+ args.local_rank, args.rank, args.world_size = world_info_from_env()
84
+ # SLURM var -> torch.distributed vars in case needed
85
+ os.environ['LOCAL_RANK'] = str(args.local_rank)
86
+ os.environ['RANK'] = str(args.rank)
87
+ os.environ['WORLD_SIZE'] = str(args.world_size)
88
+ torch.distributed.init_process_group(
89
+ backend=args.dist_backend,
90
+ init_method=args.dist_url,
91
+ world_size=args.world_size,
92
+ rank=args.rank,
93
+ )
94
+ else:
95
+ # DDP via torchrun, torch.distributed.launch
96
+ args.local_rank, args.rank, args.world_size = world_info_from_env()
97
+ torch.distributed.init_process_group(
98
+ backend=args.dist_backend,
99
+ init_method=args.dist_url,
100
+ world_size=args.world_size,
101
+ rank=args.rank,)
102
+ args.world_size = torch.distributed.get_world_size()
103
+ args.rank = torch.distributed.get_rank()
104
+ args.distributed = True
105
+
106
+ if torch.cuda.is_available():
107
+ if args.distributed and not args.no_set_device_rank:
108
+ device = 'cuda:%d' % args.local_rank
109
+ else:
110
+ device = 'cuda:0'
111
+ torch.cuda.set_device(device)
112
+ else:
113
+ device = 'cpu'
114
+ args.device = device
115
+ device = torch.device(device)
116
+ return device
117
+
118
+
119
+ def broadcast_object(args, obj, src=0):
120
+ # broadcast a pickle-able python object from rank-0 to all ranks
121
+ if args.horovod:
122
+ return hvd.broadcast_object(obj, root_rank=src)
123
+ else:
124
+ if args.rank == src:
125
+ objects = [obj]
126
+ else:
127
+ objects = [None]
128
+ dist.broadcast_object_list(objects, src=src)
129
+ return objects[0]
130
+
131
+
132
+ def all_gather_object(args, obj, dst=0):
133
+ # gather a pickle-able python object across all ranks
134
+ if args.horovod:
135
+ return hvd.allgather_object(obj)
136
+ else:
137
+ objects = [None for _ in range(args.world_size)]
138
+ dist.all_gather_object(objects, obj)
139
+ return objects
ecg_coca/training/evaluate.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from .logger import write_eval_log
3
+ from .evaluation import zero_shot_eval, linear_probe_eval
4
+
5
+
6
+ def get_val_sets(args):
7
+ val_sets = []
8
+ if args.ptbxl_path:
9
+ val_sets.append("ptbxl_super_class")
10
+ val_sets.append("ptbxl_sub_class")
11
+ val_sets.append("ptbxl_form")
12
+ val_sets.append("ptbxl_rhythm")
13
+
14
+ if args.cpsc2018_path:
15
+ val_sets.append("cpsc2018")
16
+
17
+ return val_sets
18
+
19
+
20
+ def zero_shot_evaluatation(model, data, epoch, args, tb_writer=None, tokenizer=None):
21
+ metrics = {}
22
+
23
+ model.eval()
24
+
25
+ val_sets = get_val_sets(args)
26
+
27
+ for val_set in val_sets:
28
+ eval_train, eval_test = data[f"train_{val_set}"].dataloader, data[f"val_{val_set}"].dataloader
29
+ eval_model = model
30
+ zero_shot_metrics = zero_shot_eval(eval_model, eval_test, args,
31
+ tokenizer=tokenizer,
32
+ dataset=val_set)
33
+ metrics.update(
34
+ {**zero_shot_metrics}
35
+ )
36
+
37
+ if not metrics:
38
+ return metrics
39
+ logging.info(
40
+ f"Zero-shot eval Epoch: {epoch} "
41
+ + "\t".join([f"{k}: {round(v, 4):.4f}" for k, v in metrics.items()])
42
+ )
43
+
44
+ log_data = {"Zero-shot/" + name: val for name, val in metrics.items()}
45
+ write_eval_log(args, log_data, data, epoch, metrics, tb_writer)
46
+ return metrics
47
+
48
+
49
+ def linear_probing_evaluation(model, data, epoch, args, tb_writer=None):
50
+ metrics = {}
51
+ model.eval()
52
+
53
+ val_sets = get_val_sets(args)
54
+
55
+ for val_set in val_sets:
56
+ eval_train, eval_test = data[f"train_{val_set}"].dataloader, data[f"val_{val_set}"].dataloader
57
+ eval_model = model
58
+ linear_probe_metrics = linear_probe_eval(eval_model, eval_train, eval_test, args, dataset=val_set)
59
+ metrics.update(
60
+ {**linear_probe_metrics}
61
+ )
62
+
63
+ if not metrics:
64
+ return metrics
65
+
66
+ logging.info(
67
+ f"Linear-probe eval Epoch: {epoch} "
68
+ + "\t".join([f"{k}: {round(v, 4):.4f}" for k, v in metrics.items()])
69
+ )
70
+
71
+ log_data = {"Linear-probe/" + name: val for name, val in metrics.items()}
72
+ write_eval_log(args, log_data, data, epoch, metrics, tb_writer)
73
+ return metrics
ecg_coca/training/evaluation/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .zero_shot import zero_shot_eval
2
+ from .linear_probe import linear_probe_eval
ecg_coca/training/evaluation/linear_probe.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.linear_model import LogisticRegression
3
+ import torch
4
+ from ..precision import get_autocast, get_input_dtype
5
+ from .metrics import auroc, accuracy_and_f1
6
+ import logging
7
+
8
+
9
+ def run(X, y, X_test, y_test):
10
+ _, num_classes = y.shape
11
+
12
+ preds = np.zeros_like(y_test)
13
+ for i in range(num_classes):
14
+ lr_model = LogisticRegression()
15
+ lr_model.fit(X, y[:, i])
16
+ preds[:, i] = lr_model.predict_proba(X_test)[:, 1]
17
+
18
+ acc, f1, _, _ = accuracy_and_f1(y_test, preds)
19
+ auc, _ = auroc(y_test, preds)
20
+ return acc, f1, auc
21
+
22
+
23
+ def linear_probe_eval(model, train_data, test_data, args, dataset=""):
24
+ logging.info(f'Starting linear-probe {dataset}.')
25
+ metrics = {}
26
+ all_train_ecg_features = []
27
+ all_train_labels = []
28
+ all_test_ecg_features = []
29
+ all_test_labels = []
30
+
31
+ device = args.device
32
+ autocast = get_autocast(args.precision)
33
+ input_dtype = get_input_dtype(args.precision)
34
+ with torch.no_grad():
35
+ for i, batch in enumerate(train_data):
36
+ ecgs, targets = batch
37
+ all_train_labels.append(targets)
38
+ ecgs = ecgs.to(device=device, dtype=input_dtype, non_blocking=True)
39
+ with autocast():
40
+ output = model(ecg=ecgs)
41
+ ecg_features = output['ecg_features'] if isinstance(output, dict) else output[0]
42
+ all_train_ecg_features.append(ecg_features.cpu())
43
+
44
+ all_train_ecg_features = torch.cat(all_train_ecg_features)
45
+ all_train_labels = torch.cat(all_train_labels)
46
+ for i, batch in enumerate(test_data):
47
+ ecgs, targets = batch
48
+ all_test_labels.append(targets)
49
+ ecgs = ecgs.to(device=device, dtype=input_dtype, non_blocking=True)
50
+ with autocast():
51
+ output = model(ecg=ecgs)
52
+ ecg_features = output['ecg_features'] if isinstance(output, dict) else output[0]
53
+ all_test_ecg_features.append(ecg_features.cpu())
54
+ all_test_ecg_features = torch.cat(all_test_ecg_features)
55
+ all_test_labels = torch.cat(all_test_labels)
56
+ acc, f1, roc_auc = run(all_train_ecg_features, all_train_labels, all_test_ecg_features, all_test_labels)
57
+ metrics[f"{dataset}-linear-probe-val-acc"] = acc
58
+ metrics[f"{dataset}-linear-probe-val-f1-score"] = f1
59
+ metrics[f"{dataset}-linear-probe-val-auc"] = roc_auc
60
+ return metrics
ecg_coca/training/evaluation/metadata.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ zero_shot_class = {"ptbxl_super_class": ['CD', 'HYP', 'MI', 'NORM', 'STTC'],
2
+ "ptbxl_sub_class": ['AMI', 'CLBBB', 'CRBBB', 'ILBBB', 'IMI', 'IRBBB', 'ISCA', 'ISCI',
3
+ 'ISC_', 'IVCD', 'LAFB/LPFB', 'LAO/LAE', 'LMI', 'LVH', 'NORM',
4
+ 'NST_', 'PMI', 'RAO/RAE', 'RVH', 'SEHYP', 'STTC', 'WPW', '_AVB'],
5
+ "ptbxl_form": ['ABQRS', 'DIG', 'HVOLT', 'INVT', 'LNGQT', 'LOWT', 'LPR', 'LVOLT',
6
+ 'NDT', 'NST_', 'NT_', 'PAC', 'PRC(S)', 'PVC', 'QWAVE', 'STD_',
7
+ 'STE_', 'TAB_', 'VCLVH'],
8
+ "ptbxl_rhythm": ['AFIB', 'AFLT', 'BIGU', 'PACE', 'PSVT', 'SARRH', 'SBRAD', 'SR',
9
+ 'STACH', 'SVARR', 'SVTAC', 'TRIGU'],
10
+ "cpsc2018": ["NORM", "AFIB", "1AVB", "LBBB",
11
+ "RBBB", "PAC", "PVC", "STD", "STE"]}
ecg_coca/training/evaluation/metrics.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.metrics import roc_auc_score, accuracy_score, f1_score, precision_score, recall_score, confusion_matrix, \
3
+ precision_recall_curve
4
+
5
+
6
+ def accuracy_and_f1(y_true, y_pred):
7
+ target_class = y_true.shape[1]
8
+ accs = []
9
+ max_f1s = []
10
+ for i in range(target_class):
11
+ gt_np = y_true[:, i]
12
+ pred_np = y_pred[:, i]
13
+ precision, recall, thresholds = precision_recall_curve(gt_np, pred_np)
14
+ numerator = 2 * recall * precision
15
+ denom = recall + precision
16
+ f1_scores = np.divide(numerator, denom, out=np.zeros_like(denom), where=(denom != 0))
17
+ max_f1 = np.max(f1_scores)
18
+ max_f1_thresh = thresholds[np.argmax(f1_scores)]
19
+ max_f1s.append(max_f1)
20
+ accs.append(accuracy_score(gt_np, pred_np > max_f1_thresh))
21
+
22
+ max_f1s = [i * 100 for i in max_f1s]
23
+ accs = [i * 100 for i in accs]
24
+ f1_avg = np.array(max_f1s).mean()
25
+ acc_avg = np.array(accs).mean()
26
+
27
+ return acc_avg, f1_avg, accs, max_f1s
28
+
29
+
30
+ def auroc(y_true, y_pred):
31
+ AUROCs = []
32
+ gt_np = y_true
33
+ pred_np = y_pred
34
+ n_class = y_true.shape[1]
35
+ for i in range(n_class):
36
+ AUROCs.append(roc_auc_score(gt_np[:, i], pred_np[:, i], average='macro', multi_class='ovo'))
37
+ AUROCs = [i * 100 for i in AUROCs]
38
+ AUROC_avg = np.array(AUROCs).mean()
39
+ return AUROC_avg, AUROCs
ecg_coca/training/evaluation/zero_shot.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import json
4
+ import numpy as np
5
+ import pandas as pd
6
+ import torch
7
+ from tqdm import tqdm
8
+
9
+ from ..precision import get_autocast, get_input_dtype
10
+ import torch.nn.functional as F
11
+ from .metrics import auroc, accuracy_and_f1
12
+ from .metadata import zero_shot_class
13
+
14
+
15
+ def build_zero_shot_classifier(args, model, tokenizer, dataset=""):
16
+ texts = zero_shot_class[dataset]
17
+ device = args.device
18
+ with open('./training/evaluation/CKEPE_prompt.json', 'r', encoding='utf-8') as file:
19
+ prompt = json.load(file)
20
+ texts_encoded = []
21
+ for text in texts:
22
+ text = text.replace('_', "").replace("(s)", "")
23
+ texts_encoded.append(tokenizer([prompt[text]])[0])
24
+
25
+ texts_encoded = torch.stack(texts_encoded).to(device)
26
+
27
+ with torch.no_grad():
28
+ class_embedding = model.encode_text(texts_encoded)
29
+ return class_embedding.T
30
+
31
+
32
+ def run(model, classifier, dataloader, args):
33
+ autocast = get_autocast(args.precision)
34
+ input_dtype = get_input_dtype(args.precision)
35
+
36
+ num_sample = dataloader.num_samples
37
+ y_true = np.zeros((num_sample, classifier.shape[1]))
38
+ y_pred = np.zeros_like(y_true)
39
+
40
+ i = 0
41
+ with torch.no_grad():
42
+ for ecgs, targets in dataloader:
43
+ batch_size = ecgs.shape[0]
44
+ ecgs = ecgs.to(device=args.device, dtype=input_dtype)
45
+ with autocast():
46
+ # predict
47
+ output = model(ecg=ecgs)
48
+ ecg_features = output['ecg_features'] if isinstance(output, dict) else output[0]
49
+ logits = model.logit_scale.exp() * ecg_features @ classifier
50
+
51
+ logits = F.sigmoid(logits).cpu().numpy()
52
+ y_true[i:i+batch_size, :] = targets
53
+ y_pred[i:i+batch_size, :] = logits
54
+ i += batch_size
55
+
56
+ acc, f1, _, _ = accuracy_and_f1(y_true, y_pred)
57
+ auc, _ = auroc(y_true, y_pred)
58
+ return acc, f1, auc
59
+
60
+
61
+ def zero_shot_eval(model, data, args, tokenizer, dataset=""):
62
+ logging.info(f'Starting zero-shot {dataset}.')
63
+ assert tokenizer is not None
64
+ autocast = get_autocast(args.precision)
65
+ with autocast():
66
+ classifier = build_zero_shot_classifier(
67
+ args,
68
+ model,
69
+ tokenizer=tokenizer,
70
+ dataset=dataset
71
+ )
72
+
73
+ results = {}
74
+ acc, f1, auc = run(model, classifier, data, args)
75
+ results[f'{dataset}-zeroshot-val-acc'] = acc
76
+ results[f'{dataset}-zeroshot-val-f1-score'] = f1
77
+ results[f'{dataset}-zeroshot-val-auc'] = auc
78
+ return results
ecg_coca/training/file_utils.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import multiprocessing
4
+ import subprocess
5
+ import time
6
+ import fsspec
7
+ import torch
8
+ from tqdm import tqdm
9
+
10
+ def remote_sync_s3(local_dir, remote_dir):
11
+ # skip epoch_latest which can change during sync.
12
+ result = subprocess.run(["aws", "s3", "sync", local_dir, remote_dir, '--exclude', '*epoch_latest.pt'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
13
+ if result.returncode != 0:
14
+ logging.error(f"Error: Failed to sync with S3 bucket {result.stderr.decode('utf-8')}")
15
+ return False
16
+
17
+ logging.info(f"Successfully synced with S3 bucket")
18
+ return True
19
+
20
+ def remote_sync_fsspec(local_dir, remote_dir):
21
+ # FIXME currently this is slow and not recommended. Look into speeding up.
22
+ a = fsspec.get_mapper(local_dir)
23
+ b = fsspec.get_mapper(remote_dir)
24
+
25
+ for k in a:
26
+ # skip epoch_latest which can change during sync.
27
+ if 'epoch_latest.pt' in k:
28
+ continue
29
+
30
+ logging.info(f'Attempting to sync {k}')
31
+ if k in b and len(a[k]) == len(b[k]):
32
+ logging.debug(f'Skipping remote sync for {k}.')
33
+ continue
34
+
35
+ try:
36
+ logging.info(f'Successful sync for {k}.')
37
+ b[k] = a[k]
38
+ except Exception as e:
39
+ logging.info(f'Error during remote sync for {k}: {e}')
40
+ return False
41
+
42
+ return True
43
+
44
+ def remote_sync(local_dir, remote_dir, protocol):
45
+ logging.info('Starting remote sync.')
46
+ if protocol == 's3':
47
+ return remote_sync_s3(local_dir, remote_dir)
48
+ elif protocol == 'fsspec':
49
+ return remote_sync_fsspec(local_dir, remote_dir)
50
+ else:
51
+ logging.error('Remote protocol not known')
52
+ return False
53
+
54
+ def keep_running_remote_sync(sync_every, local_dir, remote_dir, protocol):
55
+ while True:
56
+ time.sleep(sync_every)
57
+ remote_sync(local_dir, remote_dir, protocol)
58
+
59
+ def start_sync_process(sync_every, local_dir, remote_dir, protocol):
60
+ p = multiprocessing.Process(target=keep_running_remote_sync, args=(sync_every, local_dir, remote_dir, protocol))
61
+ return p
62
+
63
+ # Note: we are not currently using this save function.
64
+ def pt_save(pt_obj, file_path):
65
+ of = fsspec.open(file_path, "wb")
66
+ with of as f:
67
+ torch.save(pt_obj, file_path)
68
+
69
+ def pt_load(file_path, map_location=None):
70
+ if file_path.startswith('s3'):
71
+ logging.info('Loading remote checkpoint, which may take a bit.')
72
+ of = fsspec.open(file_path, "rb")
73
+ with of as f:
74
+ # PyTorch 2.6+ 默认 weights_only=True,但旧 checkpoint 可能包含 numpy 对象
75
+ out = torch.load(f, map_location=map_location, weights_only=False)
76
+ return out
77
+
78
+ def check_exists(file_path):
79
+ try:
80
+ with fsspec.open(file_path):
81
+ pass
82
+ except FileNotFoundError:
83
+ return False
84
+ return True
ecg_coca/training/logger.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import json
4
+ try:
5
+ import wandb
6
+ except ImportError:
7
+ wandb = None
8
+
9
+ def setup_logging(log_file, level, include_host=False):
10
+ if include_host:
11
+ import socket
12
+ hostname = socket.gethostname()
13
+ formatter = logging.Formatter(
14
+ f'%(asctime)s | {hostname} | %(levelname)s | %(message)s', datefmt='%Y-%m-%d,%H:%M:%S')
15
+ else:
16
+ formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', datefmt='%Y-%m-%d,%H:%M:%S')
17
+
18
+ logging.root.setLevel(level)
19
+ loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
20
+ for logger in loggers:
21
+ logger.setLevel(level)
22
+
23
+ stream_handler = logging.StreamHandler()
24
+ stream_handler.setFormatter(formatter)
25
+ logging.root.addHandler(stream_handler)
26
+
27
+ if log_file:
28
+ file_handler = logging.FileHandler(filename=log_file)
29
+ file_handler.setFormatter(formatter)
30
+ logging.root.addHandler(file_handler)
31
+
32
+
33
+ def write_eval_log(args, log_data, data, epoch, metrics, tb_writer=None):
34
+ if args.save_logs:
35
+ if tb_writer is not None:
36
+ for name, val in log_data.items():
37
+ tb_writer.add_scalar(name, val, epoch)
38
+
39
+ with open(os.path.join(args.checkpoint_path, "results.jsonl"), "a+") as f:
40
+ f.write(json.dumps(metrics))
41
+ f.write("\n")
42
+
43
+ if args.wandb:
44
+ assert wandb is not None, 'Please install wandb.'
45
+ if 'train' in data:
46
+ dataloader = data['train'].dataloader
47
+ num_batches_per_epoch = dataloader.num_batches // args.accum_freq
48
+ step = num_batches_per_epoch * epoch
49
+ else:
50
+ step = None
51
+ log_data['epoch'] = epoch
52
+ wandb.log(log_data, step=step)
53
+
ecg_coca/training/main.py ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import logging
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ import random
8
+ from datetime import datetime
9
+ from functools import partial
10
+
11
+ import numpy as np
12
+ import torch
13
+ from torch import optim
14
+ from torch.cuda.amp import GradScaler
15
+
16
+ try:
17
+ import wandb
18
+ except ImportError:
19
+ wandb = None
20
+
21
+ try:
22
+ import torch.utils.tensorboard as tensorboard
23
+ except ImportError:
24
+ tensorboard = None
25
+
26
+ try:
27
+ import horovod.torch as hvd
28
+ except ImportError:
29
+ hvd = None
30
+
31
+ from ecg_coca.open_clip import create_model_and_transforms, trace_model, get_tokenizer, create_loss, get_model_config
32
+ from ecg_coca.training.data import get_data
33
+ from ecg_coca.training.distributed import is_master, init_distributed_device, broadcast_object
34
+ from ecg_coca.training.logger import setup_logging
35
+ from ecg_coca.training.params import parse_args
36
+ from ecg_coca.training.scheduler import cosine_lr, const_lr, const_lr_cooldown
37
+ from ecg_coca.training.train import train_one_epoch, evaluate, test
38
+ from ecg_coca.training.file_utils import pt_load, check_exists, start_sync_process, remote_sync
39
+ from ecg_coca.training.evaluate import zero_shot_evaluatation, linear_probing_evaluation
40
+
41
+ LATEST_CHECKPOINT_NAME = "epoch_latest.pt"
42
+
43
+
44
+ def random_seed(seed=42, rank=0):
45
+ torch.manual_seed(seed + rank)
46
+ np.random.seed(seed + rank)
47
+ random.seed(seed + rank)
48
+
49
+
50
+ def natural_key(string_):
51
+ """See http://www.codinghorror.com/blog/archives/001018.html"""
52
+ return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_.lower())]
53
+
54
+
55
+ def get_latest_checkpoint(path: str, remote: bool):
56
+ # as writen, this glob recurses, so can pick up checkpoints across multiple sub-folders
57
+ if remote:
58
+ result = subprocess.run(["aws", "s3", "ls", path + "/"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
59
+ print(result)
60
+ if result.returncode == 1:
61
+ return None
62
+ checkpoints = [os.path.join(path, x.split(' ')[-1]) for x in result.stdout.decode().split('\n')[:-1]]
63
+ else:
64
+ checkpoints = glob.glob(path + '**/*.pt', recursive=True)
65
+ if checkpoints:
66
+ checkpoints = sorted(checkpoints, key=natural_key)
67
+ return checkpoints[-1]
68
+ return None
69
+
70
+
71
+ def main(args):
72
+ args = parse_args(args)
73
+
74
+ if torch.cuda.is_available():
75
+ # This enables tf32 on Ampere GPUs which is only 8% slower than
76
+ # float16 and almost as accurate as float32
77
+ # This was a default in pytorch until 1.12
78
+ torch.backends.cuda.matmul.allow_tf32 = True
79
+ torch.backends.cudnn.benchmark = True
80
+ torch.backends.cudnn.deterministic = False
81
+
82
+ # fully initialize distributed device environment
83
+ device = init_distributed_device(args)
84
+
85
+ # get the name of the experiments
86
+ if args.name is None:
87
+ # sanitize model name for filesystem / uri use, easier if we don't use / in name as a rule?
88
+ model_name_safe = args.model.replace('/', '-')
89
+ date_str = datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
90
+ if args.distributed:
91
+ # sync date_str from master to all ranks
92
+ date_str = broadcast_object(args, date_str)
93
+ args.name = '-'.join([
94
+ date_str,
95
+ f"model_{model_name_safe}",
96
+ f"lr_{args.lr}",
97
+ f"b_{args.batch_size}",
98
+ f"j_{args.workers}",
99
+ f"p_{args.precision}",
100
+ ])
101
+
102
+ resume_latest = args.resume == 'latest'
103
+ log_base_path = os.path.join(args.logs, args.name)
104
+ args.log_path = None
105
+ if is_master(args, local=args.log_local):
106
+ os.makedirs(log_base_path, exist_ok=True)
107
+ log_filename = f'out-{args.rank}' if args.log_local else 'out.log'
108
+ args.log_path = os.path.join(log_base_path, log_filename)
109
+ if os.path.exists(args.log_path) and not resume_latest:
110
+ print(
111
+ "Error. Experiment already exists. Use --name {} to specify a new experiment."
112
+ )
113
+ return -1
114
+
115
+ # Setup text logger
116
+ args.log_level = logging.DEBUG if args.debug else logging.INFO
117
+ setup_logging(args.log_path, args.log_level)
118
+
119
+ # Setup wandb, tensorboard, checkpoint logging
120
+ args.wandb = 'wandb' in args.report_to or 'all' in args.report_to
121
+ args.tensorboard = 'tensorboard' in args.report_to or 'all' in args.report_to
122
+ args.checkpoint_path = os.path.join(log_base_path, "checkpoints")
123
+ if is_master(args):
124
+ args.tensorboard_path = os.path.join(log_base_path, "tensorboard") if args.tensorboard else ''
125
+ for dirname in [args.tensorboard_path, args.checkpoint_path]:
126
+ if dirname:
127
+ os.makedirs(dirname, exist_ok=True)
128
+ else:
129
+ args.tensorboard_path = ''
130
+
131
+ if resume_latest:
132
+ resume_from = None
133
+ checkpoint_path = args.checkpoint_path
134
+ # If using remote_sync, need to check the remote instead of the local checkpoints folder.
135
+ if args.remote_sync is not None:
136
+ checkpoint_path = os.path.join(args.remote_sync, args.name, "checkpoints")
137
+ if args.save_most_recent:
138
+ print('Error. Cannot use save-most-recent with remote_sync and resume latest.')
139
+ return -1
140
+ if args.remote_sync_protocol != 's3':
141
+ print('Error. Sync protocol not supported when using resume latest.')
142
+ return -1
143
+ if is_master(args):
144
+ # Checking for existing checkpoint via master rank only. It is possible for
145
+ # different rank processes to see different files if a shared file-system is under
146
+ # stress, however it's very difficult to fully work around such situations.
147
+ if args.save_most_recent:
148
+ # if --save-most-recent flag is set, look for latest at a fixed filename
149
+ resume_from = os.path.join(checkpoint_path, LATEST_CHECKPOINT_NAME)
150
+ if not os.path.exists(resume_from):
151
+ # If no latest checkpoint has been saved yet, don't try to resume
152
+ resume_from = None
153
+ else:
154
+ # otherwise, list checkpoint dir contents and pick the newest checkpoint
155
+ resume_from = get_latest_checkpoint(checkpoint_path, remote=args.remote_sync is not None)
156
+ if resume_from:
157
+ logging.info(f'Found latest resume checkpoint at {resume_from}.')
158
+ else:
159
+ logging.info(f'No latest resume checkpoint found in {checkpoint_path}.')
160
+ if args.distributed:
161
+ # sync found checkpoint path to all ranks
162
+ resume_from = broadcast_object(args, resume_from)
163
+ args.resume = resume_from
164
+
165
+ if args.copy_codebase:
166
+ copy_codebase(args)
167
+
168
+ # start the sync proces if remote-sync is not None
169
+ remote_sync_process = None
170
+ if is_master(args) and args.remote_sync is not None:
171
+ # first make sure it works
172
+ result = remote_sync(
173
+ os.path.join(args.logs, args.name),
174
+ os.path.join(args.remote_sync, args.name),
175
+ args.remote_sync_protocol
176
+ )
177
+ if result:
178
+ logging.info('remote sync successful.')
179
+ else:
180
+ logging.info('Error: remote sync failed. Exiting.')
181
+ return -1
182
+ # if all looks good, start a process to do this every args.remote_sync_frequency seconds
183
+ remote_sync_process = start_sync_process(
184
+ args.remote_sync_frequency,
185
+ os.path.join(args.logs, args.name),
186
+ os.path.join(args.remote_sync, args.name),
187
+ args.remote_sync_protocol
188
+ )
189
+ remote_sync_process.start()
190
+
191
+ if args.precision == 'fp16':
192
+ logging.warning(
193
+ 'It is recommended to use AMP mixed-precision instead of FP16. '
194
+ 'FP16 support needs further verification and tuning, especially for train.')
195
+
196
+ if args.horovod:
197
+ logging.info(
198
+ f'Running in horovod mode with multiple processes / nodes. Device: {args.device}.'
199
+ f'Process (global: {args.rank}, local {args.local_rank}), total {args.world_size}.')
200
+ elif args.distributed:
201
+ logging.info(
202
+ f'Running in distributed mode with multiple processes. Device: {args.device}.'
203
+ f'Process (global: {args.rank}, local {args.local_rank}), total {args.world_size}.')
204
+ else:
205
+ logging.info(f'Running with a single process. Device {args.device}.')
206
+
207
+ # dist_model = None
208
+ # args.distill = args.distill_model is not None and args.distill_pretrained is not None
209
+ # if args.distill:
210
+ # #FIXME: support distillation with grad accum.
211
+ # assert args.accum_freq == 1
212
+ # #FIXME: support distillation with coca.
213
+ # assert 'coca' not in args.model.lower()
214
+
215
+ # if isinstance(args.force_ecg_size, int) and len(args.force_ecg_size) == 1:
216
+ # # arg is nargs, single (square) image size list -> int
217
+ # args.force_image_size = args.force_image_size[0]
218
+ random_seed(args.seed, 0)
219
+ model_kwargs = {}
220
+ # if args.siglip:
221
+ # model_kwargs['init_logit_scale'] = np.log(10) # different from CLIP
222
+ # model_kwargs['init_logit_bias'] = -10
223
+
224
+ model, preprocess_train, preprocess_val = create_model_and_transforms(
225
+ args.model,
226
+ args.pretrained,
227
+ precision=args.precision,
228
+ device=device,
229
+ jit=args.torchscript,
230
+ force_quick_gelu=args.force_quick_gelu,
231
+ force_custom_text=args.force_custom_text,
232
+ force_patch_dropout=args.force_patch_dropout,
233
+ force_ecg_size=args.force_ecg_size,
234
+ aug_cfg=args.aug_cfg,
235
+ output_dict=True,
236
+ **model_kwargs,
237
+ )
238
+ # if args.distill:
239
+ # # FIXME: currently assumes the model you're distilling from has the same tokenizer & transforms.
240
+ # dist_model, _, _ = create_model_and_transforms(
241
+ # args.distill_model,
242
+ # args.distill_pretrained,
243
+ # device=device,
244
+ # precision=args.precision,
245
+ # output_dict=True,
246
+ # )
247
+ if args.use_bnb_linear is not None:
248
+ print('=> using a layer from bitsandbytes.\n'
249
+ ' this is an experimental feature which requires two extra pip installs\n'
250
+ ' pip install bitsandbytes triton'
251
+ ' please make sure to use triton 2.0.0')
252
+ import bitsandbytes as bnb
253
+ from open_clip.utils import replace_linear
254
+ print(f'=> replacing linear layers with {args.use_bnb_linear}')
255
+ linear_replacement_cls = getattr(bnb.nn.triton_based_modules, args.use_bnb_linear)
256
+ replace_linear(model, linear_replacement_cls)
257
+ model = model.to(device)
258
+
259
+ random_seed(args.seed, args.rank)
260
+
261
+ if args.trace:
262
+ model = trace_model(model, batch_size=args.batch_size, device=device)
263
+
264
+ if args.lock_ecg:
265
+ # lock image tower as per LiT - https://arxiv.org/abs/2111.07991
266
+ model.lock_ecg_tower(
267
+ unlocked_groups=args.lock_ecg_unlocked_groups,
268
+ freeze_bn_stats=args.lock_ecg_freeze_bn_stats)
269
+ if args.lock_text:
270
+ model.lock_text_tower(
271
+ unlocked_layers=args.lock_text_unlocked_layers,
272
+ freeze_layer_norm=args.lock_text_freeze_layer_norm)
273
+
274
+ if args.grad_checkpointing:
275
+ model.set_grad_checkpointing()
276
+
277
+ if is_master(args):
278
+ logging.info("Model:")
279
+ logging.info(f"{str(model)}")
280
+ logging.info("Params:")
281
+ params_file = os.path.join(args.logs, args.name, "params.txt")
282
+ with open(params_file, "w") as f:
283
+ for name in sorted(vars(args)):
284
+ val = getattr(args, name)
285
+ logging.info(f" {name}: {val}")
286
+ f.write(f"{name}: {val}\n")
287
+
288
+ if args.distributed and not args.horovod:
289
+ if args.use_bn_sync:
290
+ model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model)
291
+ ddp_args = {}
292
+ if args.ddp_static_graph:
293
+ # this doesn't exist in older PyTorch, arg only added if enabled
294
+ ddp_args['static_graph'] = True
295
+ model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[device], **ddp_args)
296
+
297
+ # create optimizer and scaler
298
+ optimizer = None
299
+ scaler = None
300
+
301
+ exclude = lambda n, p: p.ndim < 2 or "bn" in n or "ln" in n or "bias" in n or 'logit_scale' in n
302
+ include = lambda n, p: not exclude(n, p)
303
+
304
+ named_parameters = list(model.named_parameters())
305
+ gain_or_bias_params = [p for n, p in named_parameters if exclude(n, p) and p.requires_grad]
306
+ rest_params = [p for n, p in named_parameters if include(n, p) and p.requires_grad]
307
+
308
+ if not args.eval:
309
+ optimizer = optim.AdamW(
310
+ [
311
+ {"params": gain_or_bias_params, "weight_decay": 0.},
312
+ {"params": rest_params, "weight_decay": args.wd},
313
+ ],
314
+ lr=args.lr,
315
+ betas=(args.beta1, args.beta2),
316
+ eps=args.eps,
317
+ )
318
+ if args.horovod:
319
+ optimizer = hvd.DistributedOptimizer(optimizer, named_parameters=model.named_parameters())
320
+ hvd.broadcast_parameters(model.state_dict(), root_rank=0)
321
+ hvd.broadcast_optimizer_state(optimizer, root_rank=0)
322
+
323
+ scaler = GradScaler() if args.precision == "amp" else None
324
+
325
+ # optionally resume from a checkpoint
326
+ start_epoch = 0
327
+ if args.resume is not None:
328
+ checkpoint = pt_load(args.resume, map_location='cpu')
329
+ if 'epoch' in checkpoint:
330
+ # resuming a train checkpoint w/ epoch and optimizer state
331
+ start_epoch = checkpoint["epoch"]
332
+ sd = checkpoint["state_dict"]
333
+ if not args.distributed and next(iter(sd.items()))[0].startswith('module'):
334
+ sd = {k[len('module.'):]: v for k, v in sd.items()}
335
+ model.load_state_dict(sd)
336
+ if optimizer is not None:
337
+ optimizer.load_state_dict(checkpoint["optimizer"])
338
+ if scaler is not None and 'scaler' in checkpoint:
339
+ scaler.load_state_dict(checkpoint['scaler'])
340
+ logging.info(f"=> resuming checkpoint '{args.resume}' (epoch {start_epoch})")
341
+ else:
342
+ # loading a bare (model only) checkpoint for fine-tune or evaluation
343
+ model.load_state_dict(checkpoint["state_dict"])
344
+ logging.info(f"=> loaded checkpoint '{args.resume}' (epoch {start_epoch})")
345
+
346
+ # initialize datasets
347
+ tokenizer = get_tokenizer(args.model)
348
+ data = get_data(
349
+ args,
350
+ (preprocess_train, preprocess_val),
351
+ epoch=start_epoch,
352
+ tokenizer=tokenizer,
353
+ )
354
+ assert len(data), 'At least one train or eval dataset must be specified.'
355
+
356
+ # create scheduler if train
357
+ scheduler = None
358
+ if 'train' in data and optimizer is not None:
359
+ total_steps = (data["train"].dataloader.num_batches // args.accum_freq) * args.epochs
360
+ if args.lr_scheduler == "cosine":
361
+ scheduler = cosine_lr(optimizer, args.lr, args.warmup, total_steps)
362
+ elif args.lr_scheduler == "const":
363
+ scheduler = const_lr(optimizer, args.lr, args.warmup, total_steps)
364
+ elif args.lr_scheduler == "const-cooldown":
365
+ assert args.epochs_cooldown is not None,\
366
+ "Please specify the number of cooldown epochs for this lr schedule."
367
+ cooldown_steps = (data["train"].dataloader.num_batches // args.accum_freq) * args.epochs_cooldown
368
+ scheduler = const_lr_cooldown(
369
+ optimizer, args.lr, args.warmup, total_steps,
370
+ cooldown_steps, args.lr_cooldown_power, args.lr_cooldown_end)
371
+ else:
372
+ logging.error(
373
+ f'Unknown scheduler, {args.lr_scheduler}. Available options are: cosine, const, const-cooldown.')
374
+ exit(1)
375
+
376
+ # determine if this worker should save logs and checkpoints. only do so if it is rank == 0
377
+ args.save_logs = args.logs and args.logs.lower() != 'none' and is_master(args)
378
+ writer = None
379
+ if args.save_logs and args.tensorboard:
380
+ assert tensorboard is not None, "Please install tensorboard."
381
+ writer = tensorboard.SummaryWriter(args.tensorboard_path)
382
+
383
+ if args.wandb and is_master(args):
384
+ assert wandb is not None, 'Please install wandb.'
385
+ logging.debug('Starting wandb.')
386
+ args.train_sz = data["train"].dataloader.num_samples
387
+ if args.val_data is not None:
388
+ args.val_sz = data["val"].dataloader.num_samples
389
+ # you will have to configure this for your project!
390
+ wandb.init(
391
+ project=args.wandb_project_name,
392
+ name=args.name,
393
+ id=args.name,
394
+ notes=args.wandb_notes,
395
+ tags=[],
396
+ resume='auto' if args.resume == "latest" else None,
397
+ config=vars(args),
398
+ )
399
+ if args.debug:
400
+ wandb.watch(model, log='all')
401
+ wandb.save(params_file)
402
+ logging.debug('Finished loading wandb.')
403
+
404
+ # Pytorch 2.0 adds '_orig_mod.' prefix to keys of state_dict() of compiled models.
405
+ # For compatibility, we save state_dict() of the original model, which shares the
406
+ # weights without the prefix.
407
+ original_model = model
408
+ if args.torchcompile:
409
+ logging.info('Compiling model...')
410
+ model = torch.compile(original_model)
411
+
412
+
413
+ # If using int8, convert to inference mode.
414
+ if args.use_bnb_linear is not None:
415
+ from open_clip.utils import convert_int8_model_to_inference_mode
416
+ convert_int8_model_to_inference_mode(model)
417
+ # Evaluate.
418
+ if args.eval:
419
+ test(model, data, start_epoch, args, tb_writer=writer, tokenizer=tokenizer)
420
+ zero_shot_evaluatation(model, data, start_epoch, args, tb_writer=writer, tokenizer=tokenizer)
421
+ linear_probing_evaluation(model, data, start_epoch, args, tb_writer=writer)
422
+ return
423
+
424
+ loss = create_loss(args)
425
+
426
+ for epoch in range(start_epoch, args.epochs):
427
+ if is_master(args):
428
+ logging.info(f'Start epoch {epoch}')
429
+
430
+ train_one_epoch(model, data, loss, epoch, optimizer, scaler, scheduler, args, tb_writer=writer)
431
+ completed_epoch = epoch + 1
432
+
433
+ evaluate(model, data, completed_epoch, args, tb_writer=writer, tokenizer=tokenizer)
434
+ if is_master(args):
435
+ if completed_epoch % args.val_frequency == 0:
436
+ test(model, data, completed_epoch, args, tb_writer=writer, tokenizer=tokenizer)
437
+ zero_shot_evaluatation(model, data, completed_epoch, args, tb_writer=writer, tokenizer=tokenizer)
438
+ linear_probing_evaluation(model, data, completed_epoch, args, tb_writer=writer)
439
+ # Saving checkpoints.
440
+ if args.save_logs:
441
+ checkpoint_dict = {
442
+ "epoch": completed_epoch,
443
+ "name": args.name,
444
+ "state_dict": original_model.state_dict(),
445
+ "optimizer": optimizer.state_dict(),
446
+ }
447
+ if scaler is not None:
448
+ checkpoint_dict["scaler"] = scaler.state_dict()
449
+
450
+ if completed_epoch == args.epochs or (
451
+ args.save_frequency > 0 and (completed_epoch % args.save_frequency) == 0
452
+ ):
453
+ torch.save(
454
+ checkpoint_dict,
455
+ os.path.join(args.checkpoint_path, f"epoch_{completed_epoch}.pt"),
456
+ )
457
+ if args.delete_previous_checkpoint:
458
+ previous_checkpoint = os.path.join(args.checkpoint_path, f"epoch_{completed_epoch - 1}.pt")
459
+ if os.path.exists(previous_checkpoint):
460
+ os.remove(previous_checkpoint)
461
+
462
+ if args.save_most_recent:
463
+ # try not to corrupt the latest checkpoint if save fails
464
+ tmp_save_path = os.path.join(args.checkpoint_path, "tmp.pt")
465
+ latest_save_path = os.path.join(args.checkpoint_path, LATEST_CHECKPOINT_NAME)
466
+ torch.save(checkpoint_dict, tmp_save_path)
467
+ os.replace(tmp_save_path, latest_save_path)
468
+
469
+ if args.wandb and is_master(args):
470
+ wandb.finish()
471
+
472
+ # run a final sync.
473
+ if remote_sync_process is not None:
474
+ logging.info('Final remote sync.')
475
+ remote_sync_process.terminate()
476
+ result = remote_sync(
477
+ os.path.join(args.logs, args.name),
478
+ os.path.join(args.remote_sync, args.name),
479
+ args.remote_sync_protocol
480
+ )
481
+ if result:
482
+ logging.info('Final remote sync successful.')
483
+ else:
484
+ logging.info('Final remote sync failed.')
485
+
486
+
487
+ def get_ecg_encoder(model_name, checkpoint_path, device):
488
+ model_kwargs = {}
489
+ model_config = get_model_config(model_name)
490
+ logging.info(f"ecg encoder name: {model_name}")
491
+
492
+ model, _, preprocess_val = create_model_and_transforms(
493
+ model_name,
494
+ "",
495
+ precision="amp",
496
+ device=device,
497
+ jit=False,
498
+ output_dict=True,
499
+ **model_kwargs,
500
+ )
501
+ model.to_empty(device=device)
502
+ model = model.ecg
503
+ checkpoint = pt_load(checkpoint_path, map_location='cpu')
504
+
505
+ sd = checkpoint["state_dict"]
506
+ sd_new = {}
507
+ for k, v in sd.items():
508
+ if k.startswith('module.ecg'):
509
+ sd_new[k[len('module.ecg.'):]] = v
510
+ model.load_state_dict(sd_new)
511
+
512
+ logging.info(f"=> loaded checkpoint '{checkpoint_path}' ")
513
+
514
+ # # Pytorch 2.0 adds '_orig_mod.' prefix to keys of state_dict() of compiled models.
515
+ # # For compatibility, we save state_dict() of the original model, which shares the
516
+ # # weights without the prefix.
517
+ # original_model = model
518
+ # if args.torchcompile:
519
+ # logging.info('Compiling model...')
520
+ # model = torch.compile(original_model)
521
+ model.lock()
522
+ return model, preprocess_val, model_config
523
+
524
+
525
+ def copy_codebase(args):
526
+ from shutil import copytree, ignore_patterns
527
+ new_code_path = os.path.join(args.logs, args.name, "code")
528
+ if os.path.exists(new_code_path):
529
+ print(
530
+ f"Error. Experiment already exists at {new_code_path}. Use --name to specify a new experiment."
531
+ )
532
+ return -1
533
+ print(f"Copying codebase to {new_code_path}")
534
+ current_code_path = os.path.realpath(__file__)
535
+ for _ in range(3):
536
+ current_code_path = os.path.dirname(current_code_path)
537
+ copytree(current_code_path, new_code_path, ignore=ignore_patterns('log', 'logs', 'wandb'))
538
+ print("Done copying code.")
539
+ return 1
540
+
541
+
542
+ if __name__ == "__main__":
543
+ main(sys.argv[1:])
ecg_coca/training/params.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import ast
3
+
4
+
5
+ def get_default_params(model_name):
6
+ # Params from paper (https://arxiv.org/pdf/2103.00020.pdf)
7
+ model_name = model_name.lower()
8
+ if "vit" in model_name:
9
+ return {"lr": 5.0e-4, "beta1": 0.9, "beta2": 0.98, "eps": 1.0e-6}
10
+ else:
11
+ return {"lr": 5.0e-4, "beta1": 0.9, "beta2": 0.999, "eps": 1.0e-8}
12
+
13
+
14
+ class ParseKwargs(argparse.Action):
15
+ def __call__(self, parser, namespace, values, option_string=None):
16
+ kw = {}
17
+ for value in values:
18
+ key, value = value.split('=')
19
+ try:
20
+ kw[key] = ast.literal_eval(value)
21
+ except ValueError:
22
+ kw[key] = str(value) # fallback to string (avoid need to escape on command line)
23
+ setattr(namespace, self.dest, kw)
24
+
25
+
26
+ def parse_args(args):
27
+ parser = argparse.ArgumentParser()
28
+ parser.add_argument(
29
+ "--mimic-iv-ecg-path",
30
+ type=str,
31
+ default=None,
32
+ help="Path to MIMIC IV ECG dataset",
33
+ )
34
+ parser.add_argument(
35
+ "--eval",
36
+ default=False,
37
+ action="store_true",
38
+ help="Is evaluation mode",
39
+ )
40
+ parser.add_argument(
41
+ "--wfep",
42
+ default=False,
43
+ action="store_true",
44
+ help="Use Wave Features Enhanced Prompt",
45
+ )
46
+
47
+ parser.add_argument(
48
+ "--ptbxl-path",
49
+ type=str,
50
+ default=None,
51
+ help="Path to PTB-XL dataset"
52
+ )
53
+ parser.add_argument(
54
+ "--cpsc2018-path",
55
+ type=str,
56
+ default=None,
57
+ help="Path to CPSC2018 dataset (Only for zero-shot and linear probing)."
58
+ )
59
+
60
+ parser.add_argument(
61
+ "--champan-path",
62
+ type=str,
63
+ default=None,
64
+ help="Path to Champan-Shaoxing dataset"
65
+ )
66
+
67
+ parser.add_argument(
68
+ "--sph-path",
69
+ type=str,
70
+ default=None,
71
+ help="Path to Shandong Provincial Hospital (SPH) database"
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--sampling-rate",
76
+ type=int,
77
+ default=500,
78
+ help="ECG sampling rate.",
79
+ )
80
+ parser.add_argument(
81
+ "--duration",
82
+ type=int,
83
+ default=10,
84
+ help="ECG duration in seconds.",
85
+ )
86
+ parser.add_argument(
87
+ "--lead_num",
88
+ type=int,
89
+ default=12,
90
+ help="ECG lead number.",
91
+ )
92
+ parser.add_argument(
93
+ "--logs",
94
+ type=str,
95
+ default="./logs/",
96
+ help="Where to store tensorboard logs. Use None to avoid storing logs.",
97
+ )
98
+ parser.add_argument(
99
+ "--log-local",
100
+ action="store_true",
101
+ default=False,
102
+ help="log files on local master, otherwise global master only.",
103
+ )
104
+ parser.add_argument(
105
+ "--name",
106
+ type=str,
107
+ default=None,
108
+ help="Optional identifier for the experiment when storing logs. Otherwise use current time.",
109
+ )
110
+ parser.add_argument(
111
+ "--workers", type=int, default=4, help="Number of dataloader workers per GPU."
112
+ )
113
+ parser.add_argument(
114
+ "--batch-size", type=int, default=64, help="Batch size per GPU."
115
+ )
116
+ parser.add_argument(
117
+ "--epochs", type=int, default=32, help="Number of epochs to train for."
118
+ )
119
+ parser.add_argument(
120
+ "--epochs-cooldown", type=int, default=None,
121
+ help="When scheduler w/ cooldown used, perform cooldown from total_epochs - cooldown_epochs onwards."
122
+ )
123
+ parser.add_argument("--lr", type=float, default=None, help="Learning rate.")
124
+ parser.add_argument("--beta1", type=float, default=None, help="Adam beta 1.")
125
+ parser.add_argument("--beta2", type=float, default=None, help="Adam beta 2.")
126
+ parser.add_argument("--eps", type=float, default=None, help="Adam epsilon.")
127
+ parser.add_argument("--wd", type=float, default=0.2, help="Weight decay.")
128
+ parser.add_argument(
129
+ "--warmup", type=int, default=10000, help="Number of steps to warmup for."
130
+ )
131
+ parser.add_argument(
132
+ "--use-bn-sync",
133
+ default=False,
134
+ action="store_true",
135
+ help="Whether to use batch norm sync.")
136
+ parser.add_argument(
137
+ "--skip-scheduler",
138
+ action="store_true",
139
+ default=False,
140
+ help="Use this flag to skip the learning rate decay.",
141
+ )
142
+ parser.add_argument(
143
+ "--lr-scheduler",
144
+ type=str,
145
+ default='cosine',
146
+ help="LR scheduler. One of: 'cosine', 'const' (constant), 'const-cooldown' (constant w/ cooldown). Default: cosine",
147
+ )
148
+ parser.add_argument(
149
+ "--lr-cooldown-end", type=float, default=0.0,
150
+ help="End learning rate for cooldown schedule. Default: 0"
151
+ )
152
+ parser.add_argument(
153
+ "--lr-cooldown-power", type=float, default=1.0,
154
+ help="Power for polynomial cooldown schedule. Default: 1.0 (linear decay)"
155
+ )
156
+ parser.add_argument(
157
+ "--save-frequency", type=int, default=1, help="How often to save checkpoints."
158
+ )
159
+ parser.add_argument(
160
+ "--save-most-recent",
161
+ action="store_true",
162
+ default=False,
163
+ help="Always save the most recent model trained to epoch_latest.pt.",
164
+ )
165
+
166
+ parser.add_argument(
167
+ "--val-frequency", type=int, default=5, help="How often to run evaluation with val data."
168
+ )
169
+ parser.add_argument(
170
+ "--resume",
171
+ default=None,
172
+ type=str,
173
+ help="path to latest checkpoint (default: none)",
174
+ )
175
+ parser.add_argument(
176
+ "--precision",
177
+ choices=["amp", "amp_bf16", "amp_bfloat16", "bf16", "fp16", "pure_bf16", "pure_fp16", "fp32"],
178
+ default="amp",
179
+ help="Floating point precision."
180
+ )
181
+ parser.add_argument(
182
+ "--model",
183
+ type=str,
184
+ default="RN50",
185
+ help="Name of the vision backbone to use.",
186
+ )
187
+ parser.add_argument(
188
+ "--pretrained",
189
+ default='',
190
+ type=str,
191
+ help="Use a pretrained CLIP model weights with the specified tag or file path.",
192
+ )
193
+
194
+ parser.add_argument(
195
+ "--lock-ecg",
196
+ default=False,
197
+ action='store_true',
198
+ help="Lock full ecg tower by disabling gradients.",
199
+ )
200
+ parser.add_argument(
201
+ "--lock-ecg-unlocked-groups",
202
+ type=int,
203
+ default=0,
204
+ help="Leave last n ecg tower layer groups unlocked.",
205
+ )
206
+ parser.add_argument(
207
+ "--lock-ecg-freeze-bn-stats",
208
+ default=False,
209
+ action='store_true',
210
+ help="Freeze BatchNorm running stats in ecg tower for any locked layers.",
211
+ )
212
+
213
+ parser.add_argument('--aug-cfg', nargs='*', default={}, action=ParseKwargs)
214
+ parser.add_argument(
215
+ "--grad-checkpointing",
216
+ default=False,
217
+ action='store_true',
218
+ help="Enable gradient checkpointing.",
219
+ )
220
+ parser.add_argument(
221
+ "--local-loss",
222
+ default=False,
223
+ action="store_true",
224
+ help="calculate loss w/ local features @ global (instead of realizing full global @ global matrix)"
225
+ )
226
+ parser.add_argument(
227
+ "--gather-with-grad",
228
+ default=False,
229
+ action="store_true",
230
+ help="enable full distributed gradient for feature gather"
231
+ )
232
+ parser.add_argument(
233
+ '--force-ecg-size', type=int, nargs='+', default=None,
234
+ help='Override default ecg size'
235
+ )
236
+ parser.add_argument(
237
+ "--force-quick-gelu",
238
+ default=False,
239
+ action='store_true',
240
+ help="Force use of QuickGELU activation for non-OpenAI transformer models.",
241
+ )
242
+ parser.add_argument(
243
+ "--force-patch-dropout",
244
+ default=None,
245
+ type=float,
246
+ help="Override the patch dropout during training, for fine tuning with no dropout near the end as in the paper",
247
+ )
248
+ parser.add_argument(
249
+ "--force-custom-text",
250
+ default=False,
251
+ action='store_true',
252
+ help="Force use of CustomTextCLIP model (separate text-tower).",
253
+ )
254
+ parser.add_argument(
255
+ "--torchscript",
256
+ default=False,
257
+ action='store_true',
258
+ help="torch.jit.script the model, also uses jit version of OpenAI models if pretrained=='openai'",
259
+ )
260
+ parser.add_argument(
261
+ "--torchcompile",
262
+ default=False,
263
+ action='store_true',
264
+ help="torch.compile() the model, requires pytorch 2.0 or later.",
265
+ )
266
+ parser.add_argument(
267
+ "--trace",
268
+ default=False,
269
+ action='store_true',
270
+ help="torch.jit.trace the model for inference / eval only",
271
+ )
272
+ parser.add_argument(
273
+ "--accum-freq", type=int, default=1, help="Update the model every --acum-freq steps."
274
+ )
275
+ # arguments for distributed training
276
+ parser.add_argument(
277
+ "--dist-url",
278
+ default="env://",
279
+ type=str,
280
+ help="url used to set up distributed training",
281
+ )
282
+ parser.add_argument(
283
+ "--dist-backend", default="nccl", type=str, help="distributed backend"
284
+ )
285
+ parser.add_argument(
286
+ "--report-to",
287
+ default='',
288
+ type=str,
289
+ help="Options are ['wandb', 'tensorboard', 'wandb,tensorboard']"
290
+ )
291
+ parser.add_argument(
292
+ "--wandb-notes",
293
+ default='',
294
+ type=str,
295
+ help="Notes if logging with wandb"
296
+ )
297
+ parser.add_argument(
298
+ "--wandb-project-name",
299
+ type=str,
300
+ default='open-clip',
301
+ help="Name of the project if logging with wandb.",
302
+ )
303
+ parser.add_argument(
304
+ "--debug",
305
+ default=False,
306
+ action="store_true",
307
+ help="If true, more information is logged."
308
+ )
309
+ parser.add_argument(
310
+ "--copy-codebase",
311
+ default=False,
312
+ action="store_true",
313
+ help="If true, we copy the entire base on the log directory, and execute from there."
314
+ )
315
+ parser.add_argument(
316
+ "--horovod",
317
+ default=False,
318
+ action="store_true",
319
+ help="Use horovod for distributed training."
320
+ )
321
+ parser.add_argument(
322
+ "--ddp-static-graph",
323
+ default=False,
324
+ action='store_true',
325
+ help="Enable static graph optimization for DDP in PyTorch >= 1.11.",
326
+ )
327
+ parser.add_argument(
328
+ "--no-set-device-rank",
329
+ default=False,
330
+ action="store_true",
331
+ help="Don't set device index from local rank (when CUDA_VISIBLE_DEVICES restricted to one per proc)."
332
+ )
333
+ parser.add_argument(
334
+ "--seed", type=int, default=0, help="Default random seed."
335
+ )
336
+ parser.add_argument(
337
+ "--grad-clip-norm", type=float, default=None, help="Gradient clip."
338
+ )
339
+ parser.add_argument(
340
+ "--lock-text",
341
+ default=False,
342
+ action='store_true',
343
+ help="Lock full text tower by disabling gradients.",
344
+ )
345
+ parser.add_argument(
346
+ "--lock-text-unlocked-layers",
347
+ type=int,
348
+ default=0,
349
+ help="Leave last n text tower layer groups unlocked.",
350
+ )
351
+ parser.add_argument(
352
+ "--lock-text-freeze-layer-norm",
353
+ default=False,
354
+ action='store_true',
355
+ help="Freeze LayerNorm running stats in text tower for any locked layers.",
356
+ )
357
+ parser.add_argument(
358
+ "--log-every-n-steps",
359
+ type=int,
360
+ default=100,
361
+ help="Log every n steps to tensorboard/console/wandb.",
362
+ )
363
+ parser.add_argument(
364
+ "--coca-caption-loss-weight",
365
+ type=float,
366
+ default=2.0,
367
+ help="Weight assigned to caption loss in CoCa."
368
+ )
369
+ parser.add_argument(
370
+ "--coca-contrastive-loss-weight",
371
+ type=float,
372
+ default=1.0,
373
+ help="Weight assigned to contrastive loss when training CoCa."
374
+ )
375
+ parser.add_argument(
376
+ "--remote-sync",
377
+ type=str,
378
+ default=None,
379
+ help="Optinoally sync with a remote path specified by this arg",
380
+ )
381
+ parser.add_argument(
382
+ "--remote-sync-frequency",
383
+ type=int,
384
+ default=300,
385
+ help="How frequently to sync to a remote directly if --remote-sync is not None.",
386
+ )
387
+ parser.add_argument(
388
+ "--remote-sync-protocol",
389
+ choices=["s3", "fsspec"],
390
+ default="s3",
391
+ help="How to do the remote sync backup if --remote-sync is not None.",
392
+ )
393
+ parser.add_argument(
394
+ "--delete-previous-checkpoint",
395
+ default=False,
396
+ action="store_true",
397
+ help="If true, delete previous checkpoint after storing a new one."
398
+ )
399
+ parser.add_argument(
400
+ "--distill-model",
401
+ default=None,
402
+ help='Which model arch to distill from, if any.'
403
+ )
404
+ parser.add_argument(
405
+ "--distill-pretrained",
406
+ default=None,
407
+ help='Which pre-trained weights to distill from, if any.'
408
+ )
409
+ parser.add_argument(
410
+ "--use-bnb-linear",
411
+ default=None,
412
+ help='Replace the network linear layers from the bitsandbytes library. '
413
+ 'Allows int8 training/inference, etc.'
414
+ )
415
+ parser.add_argument(
416
+ "--siglip",
417
+ default=False,
418
+ action="store_true",
419
+ help='Use SigLip (sigmoid) loss.'
420
+ )
421
+
422
+ args = parser.parse_args(args)
423
+
424
+ # If some params are not passed, we use the default values based on model name.
425
+ default_params = get_default_params(args.model)
426
+ for name, val in default_params.items():
427
+ if getattr(args, name) is None:
428
+ setattr(args, name, val)
429
+
430
+ return args
ecg_coca/training/precision.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from contextlib import suppress
3
+
4
+
5
+ def get_autocast(precision):
6
+ if precision == 'amp':
7
+ return torch.cuda.amp.autocast
8
+ elif precision == 'amp_bfloat16' or precision == 'amp_bf16':
9
+ # amp_bfloat16 is more stable than amp float16 for clip training
10
+ return lambda: torch.cuda.amp.autocast(dtype=torch.bfloat16)
11
+ else:
12
+ return suppress
13
+
14
+ def get_input_dtype(precision: str):
15
+ input_dtype = None
16
+ if precision in ('bf16', 'pure_bf16'):
17
+ input_dtype = torch.bfloat16
18
+ elif precision in ('fp16', 'pure_fp16'):
19
+ input_dtype = torch.float16
20
+ return input_dtype
ecg_coca/training/profiler.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ import torch
4
+ import open_clip
5
+ import pandas as pd
6
+ from torch.utils.flop_counter import FlopCounterMode
7
+ try:
8
+ import fvcore
9
+ except:
10
+ fvcore = None
11
+
12
+ parser = argparse.ArgumentParser(description='OpenCLIP Profiler')
13
+
14
+ # benchmark specific args
15
+ parser.add_argument('--model', metavar='NAME', default='',
16
+ help='model(s) to profile')
17
+ parser.add_argument('--results-file', default='', type=str, metavar='FILENAME',
18
+ help='Output csv file for results')
19
+ parser.add_argument('--profiler', default='torch', type=str, choices=['torch', 'fvcore'])
20
+ parser.add_argument('--batch-size', default=1, type=int, help='Batch size for profiling')
21
+
22
+
23
+ def profile_fvcore(
24
+ model,
25
+ image_input_size=(3, 224, 224),
26
+ text_input_size=(77,),
27
+ batch_size=1,
28
+ detailed=False,
29
+ force_cpu=False
30
+ ):
31
+ if force_cpu:
32
+ model = model.to('cpu')
33
+ device, dtype = next(model.parameters()).device, next(model.parameters()).dtype
34
+ example_image_input = torch.ones((batch_size,) + image_input_size, device=device, dtype=dtype)
35
+ example_text_input = torch.ones((batch_size,) + text_input_size, device=device, dtype=torch.int64)
36
+ fca = fvcore.nn.FlopCountAnalysis(model, (example_image_input, example_text_input))
37
+ aca = fvcore.nn.ActivationCountAnalysis(model, (example_image_input, example_text_input))
38
+ if detailed:
39
+ fcs = fvcore.nn.flop_count_str(fca)
40
+ print(fcs)
41
+ return fca.total() / batch_size, aca.total() / batch_size
42
+
43
+
44
+ def profile_fvcore_text(
45
+ model,
46
+ text_input_size=(77,),
47
+ batch_size=1,
48
+ detailed=False,
49
+ force_cpu=False
50
+ ):
51
+ if force_cpu:
52
+ model = model.to('cpu')
53
+ device = next(model.parameters()).device
54
+ example_input = torch.ones((batch_size,) + text_input_size, device=device, dtype=torch.int64)
55
+ fca = fvcore.nn.FlopCountAnalysis(model, example_input)
56
+ aca = fvcore.nn.ActivationCountAnalysis(model, example_input)
57
+ if detailed:
58
+ fcs = fvcore.nn.flop_count_str(fca)
59
+ print(fcs)
60
+ return fca.total() / batch_size, aca.total() / batch_size
61
+
62
+
63
+ def profile_fvcore_image(
64
+ model,
65
+ image_input_size=(3, 224, 224),
66
+ batch_size=1,
67
+ detailed=False,
68
+ force_cpu=False
69
+ ):
70
+ if force_cpu:
71
+ model = model.to('cpu')
72
+ device, dtype = next(model.parameters()).device, next(model.parameters()).dtype
73
+ example_input = torch.ones((batch_size,) + image_input_size, device=device, dtype=dtype)
74
+ fca = fvcore.nn.FlopCountAnalysis(model, example_input)
75
+ aca = fvcore.nn.ActivationCountAnalysis(model, example_input)
76
+ if detailed:
77
+ fcs = fvcore.nn.flop_count_str(fca)
78
+ print(fcs)
79
+ return fca.total() / batch_size, aca.total() / batch_size
80
+
81
+
82
+ def profile_torch_image(model, image_input_size, batch_size=1, force_cpu=False):
83
+ """Profile the image encoder using torch.utils.flop_counter"""
84
+ if force_cpu:
85
+ model = model.to('cpu')
86
+ device, dtype = next(model.parameters()).device, next(model.parameters()).dtype
87
+ example_input = torch.ones((batch_size,) + image_input_size, device=device, dtype=dtype)
88
+
89
+ flop_counter = FlopCounterMode()
90
+ with flop_counter:
91
+ model(example_input)
92
+ total_flops = sum(flop_counter.get_flop_counts()['Global'].values())
93
+ return total_flops / batch_size
94
+
95
+
96
+ def profile_torch_text(model, text_input_size, batch_size=1, force_cpu=False):
97
+ """Profile the text encoder using torch.utils.flop_counter"""
98
+ if force_cpu:
99
+ model = model.to('cpu')
100
+ device = next(model.parameters()).device
101
+ example_input = torch.ones((batch_size,) + text_input_size, device=device, dtype=torch.int64)
102
+
103
+ flop_counter = FlopCounterMode()
104
+ with flop_counter:
105
+ model(example_input)
106
+ total_flops = sum(flop_counter.get_flop_counts()['Global'].values())
107
+ return total_flops / batch_size
108
+
109
+
110
+ def profile_torch(model, text_input_size, image_input_size, batch_size=1, force_cpu=False):
111
+ """Profile the full model using torch.utils.flop_counter"""
112
+ if force_cpu:
113
+ model = model.to('cpu')
114
+ device, dtype = next(model.parameters()).device, next(model.parameters()).dtype
115
+ image_input = torch.ones((batch_size,) + image_input_size, device=device, dtype=dtype)
116
+ text_input = torch.ones((batch_size,) + text_input_size, device=device, dtype=torch.int64)
117
+
118
+ flop_counter = FlopCounterMode()
119
+ with flop_counter:
120
+ model(image_input, text_input)
121
+ total_flops = sum(flop_counter.get_flop_counts()['Global'].values())
122
+ return total_flops / batch_size
123
+
124
+
125
+ def count_params(model):
126
+ return sum(m.numel() for m in model.parameters())
127
+
128
+ def profile_model(model_name, batch_size=1, profiler='torch'):
129
+ assert profiler in ['torch', 'fvcore'], 'Only torch and fvcore profilers are supported'
130
+ if profiler == 'fvcore':
131
+ assert fvcore is not None, 'Please install fvcore.'
132
+ model = open_clip.create_model(model_name, force_custom_text=True, pretrained_hf=False)
133
+ model.eval()
134
+ if torch.cuda.is_available():
135
+ model = model.cuda()
136
+
137
+ if isinstance(model.visual.image_size, (tuple, list)):
138
+ image_input_size = (3,) + tuple(model.visual.image_size[-2:])
139
+ else:
140
+ image_input_size = (3, model.visual.image_size, model.visual.image_size)
141
+
142
+ text_input_size = (77,)
143
+ if hasattr(model, 'context_length') and model.context_length:
144
+ text_input_size = (model.context_length,)
145
+
146
+ results = {}
147
+ results['model'] = model_name
148
+ results['image_size'] = image_input_size[1]
149
+
150
+ model_cfg = open_clip.get_model_config(model_name)
151
+ if model_cfg:
152
+ vision_cfg = open_clip.CLIPVisionCfg(**model_cfg['vision_cfg'])
153
+ text_cfg = open_clip.CLIPTextCfg(**model_cfg['text_cfg'])
154
+ results['image_width'] = int(vision_cfg.width)
155
+ results['text_width'] = int(text_cfg.width)
156
+ results['embed_dim'] = int(model_cfg['embed_dim'])
157
+ else:
158
+ results['image_width'] = 0
159
+ results['text_width'] = 0
160
+ results['embed_dim'] = 0
161
+
162
+ retries = 2
163
+ while retries:
164
+ retries -= 1
165
+ try:
166
+ results['mparams'] = round(count_params(model) / 1e6, 2)
167
+ results['image_mparams'] = round(count_params(model.visual) / 1e6, 2)
168
+ results['text_mparams'] = round(count_params(model.text) / 1e6, 2)
169
+
170
+ if profiler == 'fvcore':
171
+ macs, acts = profile_fvcore(
172
+ model, image_input_size=image_input_size, text_input_size=text_input_size, force_cpu=not retries, batch_size=batch_size)
173
+
174
+ image_macs, image_acts = profile_fvcore_image(
175
+ model.visual, image_input_size=image_input_size, force_cpu=not retries, batch_size=batch_size)
176
+
177
+ text_macs, text_acts = profile_fvcore_text(
178
+ model.text, text_input_size=text_input_size, force_cpu=not retries, batch_size=batch_size)
179
+
180
+ results['gmacs'] = round(macs / 1e9, 2)
181
+ results['macts'] = round(acts / 1e6, 2)
182
+
183
+ results['image_gmacs'] = round(image_macs / 1e9, 2)
184
+ results['image_macts'] = round(image_acts / 1e6, 2)
185
+
186
+ results['text_gmacs'] = round(text_macs / 1e9, 2)
187
+ results['text_macts'] = round(text_acts / 1e6, 2)
188
+ elif profiler == 'torch':
189
+ image_flops = profile_torch_image(
190
+ model.visual, image_input_size=image_input_size, force_cpu=not retries, batch_size=batch_size)
191
+ text_flops = profile_torch_text(
192
+ model.text, text_input_size=text_input_size, force_cpu=not retries, batch_size=batch_size)
193
+ total_flops = profile_torch(
194
+ model, image_input_size=image_input_size, text_input_size=text_input_size, force_cpu=not retries, batch_size=batch_size)
195
+
196
+ results['gflops'] = round(total_flops / 1e9, 2)
197
+ results['image_gflops'] = round(image_flops / 1e9, 2)
198
+ results['text_gflops'] = round(text_flops / 1e9, 2)
199
+
200
+ except RuntimeError as e:
201
+ pass
202
+ return results
203
+
204
+
205
+ def main():
206
+ args = parser.parse_args()
207
+
208
+ # FIXME accept a text file name to allow lists of models in txt/csv
209
+ if args.model == 'all':
210
+ parsed_model = open_clip.list_models()
211
+ else:
212
+ parsed_model = args.model.split(',')
213
+
214
+ results = []
215
+ models_with_errors = []
216
+ for m in parsed_model:
217
+ print('='*100)
218
+ print(f'Profiling {m}')
219
+ try:
220
+ row = profile_model(m, batch_size=args.batch_size, profiler=args.profiler)
221
+ results.append(row)
222
+ except Exception as e:
223
+ print(f'Error profiling {m}: {e}')
224
+ import traceback
225
+ traceback.print_exc()
226
+ models_with_errors.append(m)
227
+
228
+ df = pd.DataFrame(results, columns=results[0].keys())
229
+
230
+ if 'gmacs' in df.columns:
231
+ df = df.sort_values(by=['gmacs', 'mparams', 'model'])
232
+ else:
233
+ df = df.sort_values(by=['gflops', 'mparams', 'model'])
234
+
235
+ print('='*100)
236
+ print('Done.')
237
+ print(df)
238
+ if args.results_file:
239
+ df.to_csv(args.results_file, index=False)
240
+
241
+ if models_with_errors:
242
+ print('Models with errors:', models_with_errors)
243
+
244
+
245
+ if __name__ == '__main__':
246
+ main()
ecg_coca/training/scheduler.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ def assign_learning_rate(optimizer, new_lr):
5
+ for param_group in optimizer.param_groups:
6
+ param_group["lr"] = new_lr
7
+
8
+
9
+ def _warmup_lr(base_lr, warmup_length, step):
10
+ return base_lr * (step + 1) / warmup_length
11
+
12
+
13
+ def const_lr(optimizer, base_lr, warmup_length, steps):
14
+ def _lr_adjuster(step):
15
+ if step < warmup_length:
16
+ lr = _warmup_lr(base_lr, warmup_length, step)
17
+ else:
18
+ lr = base_lr
19
+ assign_learning_rate(optimizer, lr)
20
+ return lr
21
+ return _lr_adjuster
22
+
23
+
24
+ def const_lr_cooldown(optimizer, base_lr, warmup_length, steps, cooldown_steps, cooldown_power=1.0, cooldown_end_lr=0.):
25
+ def _lr_adjuster(step):
26
+ start_cooldown_step = steps - cooldown_steps
27
+ if step < warmup_length:
28
+ lr = _warmup_lr(base_lr, warmup_length, step)
29
+ else:
30
+ if step < start_cooldown_step:
31
+ lr = base_lr
32
+ else:
33
+ e = step - start_cooldown_step
34
+ es = steps - start_cooldown_step
35
+ # linear decay if power == 1; polynomial decay otherwise;
36
+ decay = (1 - (e/es)) ** cooldown_power
37
+ lr = decay * (base_lr - cooldown_end_lr) + cooldown_end_lr
38
+ assign_learning_rate(optimizer, lr)
39
+ return lr
40
+ return _lr_adjuster
41
+
42
+
43
+ def cosine_lr(optimizer, base_lr, warmup_length, steps):
44
+ def _lr_adjuster(step):
45
+ if step < warmup_length:
46
+ lr = _warmup_lr(base_lr, warmup_length, step)
47
+ else:
48
+ e = step - warmup_length
49
+ es = steps - warmup_length
50
+ lr = 0.5 * (1 + np.cos(np.pi * e / es)) * base_lr
51
+ assign_learning_rate(optimizer, lr)
52
+ return lr
53
+ return _lr_adjuster
ecg_coca/training/train.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import math
4
+ import os
5
+ import time
6
+
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+ try:
12
+ import wandb
13
+ except ImportError:
14
+ wandb = None
15
+
16
+ from ecg_coca.open_clip import get_input_dtype
17
+ from .distributed import is_master
18
+ from .evaluation.zero_shot import zero_shot_eval
19
+ from .precision import get_autocast
20
+ from .evaluation.linear_probe import linear_probe_eval
21
+ from .logger import write_eval_log
22
+
23
+ class AverageMeter(object):
24
+ """Computes and stores the average and current value"""
25
+
26
+ def __init__(self):
27
+ self.reset()
28
+
29
+ def reset(self):
30
+ self.val = 0
31
+ self.avg = 0
32
+ self.sum = 0
33
+ self.count = 0
34
+
35
+ def update(self, val, n=1):
36
+ self.val = val
37
+ self.sum += val * n
38
+ self.count += n
39
+ self.avg = self.sum / self.count
40
+
41
+
42
+ def postprocess_clip_output(model_out):
43
+ return {
44
+ "ecg_features": model_out[0],
45
+ "text_features": model_out[1],
46
+ "logit_scale": model_out[2]
47
+ }
48
+
49
+
50
+ def unwrap_model(model):
51
+ if hasattr(model, 'module'):
52
+ return model.module
53
+ else:
54
+ return model
55
+
56
+
57
+ def backward(total_loss, scaler):
58
+ if scaler is not None:
59
+ scaler.scale(total_loss).backward()
60
+ else:
61
+ total_loss.backward()
62
+
63
+
64
+ def train_one_epoch(model, data, loss, epoch, optimizer, scaler, scheduler, args, tb_writer=None):
65
+ device = torch.device(args.device)
66
+ autocast = get_autocast(args.precision)
67
+ input_dtype = get_input_dtype(args.precision)
68
+
69
+ model.train()
70
+
71
+ data['train'].set_epoch(epoch) # set epoch in process safe manner via sampler or shared_epoch
72
+ dataloader = data['train'].dataloader
73
+ num_batches_per_epoch = dataloader.num_batches // args.accum_freq
74
+ sample_digits = math.ceil(math.log(dataloader.num_samples + 1, 10))
75
+
76
+ if args.accum_freq > 1:
77
+ accum_ecgs, accum_texts, accum_patient_infos, accum_features = [], [], [], {}
78
+
79
+ losses_m = {}
80
+ batch_time_m = AverageMeter()
81
+ data_time_m = AverageMeter()
82
+ end = time.time()
83
+ for i, batch in enumerate(dataloader):
84
+ i_accum = i // args.accum_freq
85
+ step = num_batches_per_epoch * epoch + i_accum
86
+
87
+ if not args.skip_scheduler:
88
+ scheduler(step)
89
+
90
+ ecgs, texts = batch
91
+ ecgs = ecgs.to(device=device, dtype=input_dtype, non_blocking=True)
92
+ texts = texts.to(device=device, non_blocking=True)
93
+
94
+ data_time_m.update(time.time() - end)
95
+ optimizer.zero_grad()
96
+
97
+ if args.accum_freq == 1:
98
+ with autocast():
99
+ model_out = model(ecgs, texts)
100
+ logit_scale = model_out["logit_scale"]
101
+ losses = loss(**model_out, output_dict=True)
102
+
103
+ total_loss = sum(losses.values())
104
+ losses["loss"] = total_loss
105
+
106
+ backward(total_loss, scaler)
107
+ else:
108
+ # First, cache the features without any gradient tracking.
109
+ with torch.no_grad():
110
+ with autocast():
111
+ model_out = model(ecgs, texts)
112
+
113
+ for f in ("logit_scale", "logit_bias"):
114
+ model_out.pop(f, None)
115
+
116
+ for key, val in model_out.items():
117
+ if key in accum_features:
118
+ accum_features[key].append(val)
119
+ else:
120
+ accum_features[key] = [val]
121
+
122
+ accum_ecgs.append(ecgs)
123
+ accum_texts.append(texts)
124
+ # If (i + 1) % accum_freq is not zero, move on to the next batch.
125
+ if ((i + 1) % args.accum_freq) > 0:
126
+ # FIXME this makes data time logging unreliable when accumulating
127
+ continue
128
+
129
+ # Now, ready to take gradients for the last accum_freq batches.
130
+ # Re-do the forward pass for those batches, and use the cached features from the other batches as negatives.
131
+ # Call backwards each time, but only step optimizer at the end.
132
+ optimizer.zero_grad()
133
+ for j in range(args.accum_freq):
134
+ ecgs = accum_ecgs[j]
135
+ texts = accum_texts[j]
136
+ with autocast():
137
+ model_out = model(ecgs, texts)
138
+
139
+ inputs_no_accum = {}
140
+ inputs_no_accum["logit_scale"] = logit_scale = model_out.pop("logit_scale")
141
+ if "logit_bias" in model_out:
142
+ inputs_no_accum["logit_bias"] = model_out.pop("logit_bias")
143
+
144
+ inputs = {}
145
+ for key, val in accum_features.items():
146
+ accumulated = accum_features[key]
147
+ inputs[key] = torch.cat(accumulated[:j] + [model_out[key]] + accumulated[j + 1:])
148
+
149
+ losses = loss(**inputs, **inputs_no_accum, output_dict=True)
150
+ del inputs
151
+ del inputs_no_accum
152
+ total_loss = sum(losses.values())
153
+ losses["loss"] = total_loss
154
+
155
+ backward(total_loss, scaler)
156
+
157
+ if scaler is not None:
158
+ if args.horovod:
159
+ optimizer.synchronize()
160
+ scaler.unscale_(optimizer)
161
+ if args.grad_clip_norm is not None:
162
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip_norm, norm_type=2.0)
163
+ with optimizer.skip_synchronize():
164
+ scaler.step(optimizer)
165
+ else:
166
+ if args.grad_clip_norm is not None:
167
+ scaler.unscale_(optimizer)
168
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip_norm, norm_type=2.0)
169
+ scaler.step(optimizer)
170
+ scaler.update()
171
+ else:
172
+ if args.grad_clip_norm is not None:
173
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip_norm, norm_type=2.0)
174
+ optimizer.step()
175
+
176
+ # reset gradient accum, if enabled
177
+ if args.accum_freq > 1:
178
+ accum_ecgs, accum_texts, accum_features = [], [], {}
179
+
180
+ # Note: we clamp to 4.6052 = ln(100), as in the original paper.
181
+ with torch.no_grad():
182
+ unwrap_model(model).logit_scale.clamp_(0, math.log(100))
183
+
184
+ batch_time_m.update(time.time() - end)
185
+ end = time.time()
186
+ batch_count = i_accum + 1
187
+ if is_master(args) and (i_accum % args.log_every_n_steps == 0 or batch_count == num_batches_per_epoch):
188
+ batch_size = len(ecgs)
189
+ num_samples = batch_count * batch_size * args.accum_freq * args.world_size
190
+ samples_per_epoch = dataloader.num_samples
191
+ percent_complete = 100.0 * batch_count / num_batches_per_epoch
192
+
193
+ # NOTE loss is coarsely sampled, just master node and per log update
194
+ for key, val in losses.items():
195
+ if key not in losses_m:
196
+ losses_m[key] = AverageMeter()
197
+ losses_m[key].update(val.item(), batch_size)
198
+
199
+ logit_scale_scalar = logit_scale.item()
200
+ loss_log = " ".join(
201
+ [
202
+ f"{loss_name.capitalize()}: {loss_m.val:#.5g} ({loss_m.avg:#.5g})"
203
+ for loss_name, loss_m in losses_m.items()
204
+ ]
205
+ )
206
+ samples_per_second = args.accum_freq * args.batch_size * args.world_size / batch_time_m.val
207
+ samples_per_second_per_gpu = args.accum_freq * args.batch_size / batch_time_m.val
208
+ logging.info(
209
+ f"Train Epoch: {epoch} [{num_samples:>{sample_digits}}/{samples_per_epoch} ({percent_complete:.0f}%)] "
210
+ f"Data (t): {data_time_m.avg:.3f} "
211
+ f"Batch (t): {batch_time_m.avg:.3f}, {samples_per_second:#g}/s, {samples_per_second_per_gpu:#g}/s/gpu "
212
+ f"LR: {optimizer.param_groups[0]['lr']:5f} "
213
+ f"Logit Scale: {logit_scale_scalar:.3f} " + loss_log
214
+ )
215
+
216
+ # Save train loss / etc. Using non avg meter values as loggers have their own smoothing
217
+ log_data = {
218
+ "data_time": data_time_m.val,
219
+ "batch_time": batch_time_m.val,
220
+ "samples_per_second": samples_per_second,
221
+ "samples_per_second_per_gpu": samples_per_second_per_gpu,
222
+ "scale": logit_scale_scalar,
223
+ "lr": optimizer.param_groups[0]["lr"]
224
+ }
225
+ log_data.update({name: val.val for name, val in losses_m.items()})
226
+
227
+ log_data = {"train/" + name: val for name, val in log_data.items()}
228
+
229
+ if tb_writer is not None:
230
+ for name, val in log_data.items():
231
+ tb_writer.add_scalar(name, val, step)
232
+
233
+ if args.wandb:
234
+ assert wandb is not None, 'Please install wandb.'
235
+ log_data['step'] = step # for backwards compatibility
236
+ wandb.log(log_data, step=step)
237
+
238
+ # resetting batch / data time meters per log window
239
+ batch_time_m.reset()
240
+ data_time_m.reset()
241
+ # end for
242
+
243
+
244
+ def evaluate(model, data, epoch, args, tb_writer=None, tokenizer=None):
245
+ metrics = {}
246
+ if not is_master(args):
247
+ return metrics
248
+ device = torch.device(args.device)
249
+ model.eval()
250
+
251
+ autocast = get_autocast(args.precision)
252
+ input_dtype = get_input_dtype(args.precision)
253
+
254
+ if 'val' in data and (args.val_frequency and ((epoch % args.val_frequency) == 0 or epoch == args.epochs)):
255
+ dataloader = data['val'].dataloader
256
+ num_samples = 0
257
+ samples_per_val = dataloader.num_samples
258
+
259
+ # FIXME this does not scale past small eval datasets
260
+ # all_image_features @ all_text_features will blow up memory and compute very quickly
261
+ cumulative_loss = 0.0
262
+ cumulative_gen_loss = 0.0
263
+ all_ecg_features, all_text_features = [], []
264
+ with torch.no_grad():
265
+ for i, batch in enumerate(dataloader):
266
+ ecgs, texts= batch
267
+ ecgs = ecgs.to(device=device, dtype=input_dtype, non_blocking=True)
268
+ texts = texts.to(device=device, non_blocking=True)
269
+ with autocast():
270
+ model_out = model(ecgs, texts)
271
+ ecg_features = model_out["ecg_features"]
272
+ text_features = model_out["text_features"]
273
+ logit_scale = model_out["logit_scale"]
274
+ # features are accumulated in CPU tensors, otherwise GPU memory exhausted quickly
275
+ # however, system RAM is easily exceeded and compute time becomes problematic
276
+ all_ecg_features.append(ecg_features.cpu())
277
+ all_text_features.append(text_features.cpu())
278
+ logit_scale = logit_scale.mean()
279
+ logits_per_ecg = logit_scale * ecg_features @ text_features.t()
280
+ logits_per_text = logits_per_ecg.t()
281
+
282
+ batch_size = ecgs.shape[0]
283
+ labels = torch.arange(batch_size, device=device).long()
284
+ total_loss = (
285
+ F.cross_entropy(logits_per_ecg, labels) +
286
+ F.cross_entropy(logits_per_text, labels)
287
+ ) / 2
288
+
289
+ gen_loss = maybe_compute_generative_loss(model_out)
290
+
291
+ cumulative_loss += total_loss * batch_size
292
+ num_samples += batch_size
293
+ if is_master(args) and (i % 100) == 0:
294
+ logging.info(
295
+ f"Eval Epoch: {epoch} [{num_samples} / {samples_per_val}]\t"
296
+ f"Clip Loss: {cumulative_loss / num_samples:.6f}\t")
297
+
298
+ if gen_loss is not None:
299
+ cumulative_gen_loss += gen_loss * batch_size
300
+ logging.info(
301
+ f"Generative Loss: {cumulative_gen_loss / num_samples:.6f}\t")
302
+
303
+ val_metrics = get_clip_metrics(
304
+ ecg_features=torch.cat(all_ecg_features),
305
+ text_features=torch.cat(all_text_features),
306
+ logit_scale=logit_scale.cpu(),
307
+ )
308
+ loss = cumulative_loss / num_samples
309
+ metrics.update(
310
+ {**val_metrics, "clip_val_loss": loss.item(), "epoch": epoch, "num_samples": num_samples}
311
+ )
312
+ if gen_loss is not None:
313
+ gen_loss = cumulative_gen_loss / num_samples
314
+ metrics.update({"val_generative_loss": gen_loss.item()})
315
+
316
+ logging.info(
317
+ f"Eval Epoch: {epoch} "
318
+ + "\t".join([f"{k}: {round(v, 4):.4f}" for k, v in metrics.items()])
319
+ )
320
+
321
+ log_data = {"val/" + name: val for name, val in metrics.items()}
322
+
323
+ write_eval_log(args, log_data, data, epoch, metrics, tb_writer)
324
+
325
+ return metrics
326
+
327
+
328
+ def test(model, data, epoch, args, tb_writer=None, tokenizer=None):
329
+ metrics = {}
330
+ if not is_master(args):
331
+ return metrics
332
+ device = torch.device(args.device)
333
+ model.eval()
334
+
335
+ autocast = get_autocast(args.precision)
336
+ input_dtype = get_input_dtype(args.precision)
337
+ test_datasets = ['test_mimic', 'test_ptbxl']
338
+ for test_set in test_datasets:
339
+ dataloader = data[test_set].dataloader
340
+ num_samples = 0
341
+ samples_per_val = dataloader.num_samples
342
+
343
+ # FIXME this does not scale past small eval datasets
344
+ # all_image_features @ all_text_features will blow up memory and compute very quickly
345
+ all_ecg_features, all_text_features = [], []
346
+ with torch.no_grad():
347
+ for i, batch in enumerate(dataloader):
348
+ ecgs, texts = batch
349
+ ecgs = ecgs.to(device=device, dtype=input_dtype, non_blocking=True)
350
+ texts = texts.to(device=device, non_blocking=True)
351
+ with autocast():
352
+ model_out = model(ecgs, texts)
353
+ ecg_features = model_out["ecg_features"]
354
+ text_features = model_out["text_features"]
355
+ logit_scale = model_out["logit_scale"]
356
+ # features are accumulated in CPU tensors, otherwise GPU memory exhausted quickly
357
+ # however, system RAM is easily exceeded and compute time becomes problematic
358
+ all_ecg_features.append(ecg_features.cpu())
359
+ all_text_features.append(text_features.cpu())
360
+ batch_size = ecgs.shape[0]
361
+
362
+ num_samples += batch_size
363
+ if is_master(args) and (i % 100) == 0:
364
+ logging.info(
365
+ f"Test {test_set}: {epoch} [{num_samples} / {samples_per_val}]\t")
366
+
367
+ test_metrics = get_clip_metrics(
368
+ ecg_features=torch.cat(all_ecg_features),
369
+ text_features=torch.cat(all_text_features),
370
+ logit_scale=logit_scale.cpu(),
371
+ set_name=test_set+"_"
372
+ )
373
+ metrics.update(
374
+ {**test_metrics, "epoch": epoch, "num_samples": num_samples}
375
+ )
376
+
377
+ logging.info(
378
+ f"Test Epoch: {epoch} "
379
+ + "\t".join([f"{k}: {round(v, 4):.4f}" for k, v in metrics.items()])
380
+ )
381
+
382
+ log_data = {"test/" + name: val for name, val in metrics.items()}
383
+
384
+ write_eval_log(args, log_data, data, epoch, metrics, tb_writer)
385
+ return metrics
386
+
387
+
388
+ def get_clip_metrics(ecg_features, text_features, logit_scale, set_name=""):
389
+ metrics = {}
390
+ logits_per_ecg = (logit_scale * ecg_features @ text_features.t()).detach().cpu()
391
+ logits_per_text = logits_per_ecg.t().detach().cpu()
392
+
393
+ logits = {f"{set_name}ecg_to_text": logits_per_ecg, f"{set_name}text_to_ecg": logits_per_text}
394
+ ground_truth = torch.arange(len(text_features)).view(-1, 1)
395
+
396
+ for name, logit in logits.items():
397
+ ranking = torch.argsort(logit, descending=True)
398
+ preds = torch.where(ranking == ground_truth)[1]
399
+ preds = preds.detach().cpu().numpy()
400
+ metrics[f"{name}_mean_rank"] = preds.mean() + 1
401
+ metrics[f"{name}_median_rank"] = np.floor(np.median(preds)) + 1
402
+ for k in [1, 5, 10]:
403
+ metrics[f"{name}_R@{k}"] = np.mean(preds < k)
404
+
405
+ return metrics
406
+
407
+
408
+ def maybe_compute_generative_loss(model_out):
409
+ if "logits" in model_out and "labels" in model_out:
410
+ token_logits = model_out["logits"]
411
+ token_labels = model_out["labels"]
412
+ return F.cross_entropy(token_logits.permute(0, 2, 1), token_labels)
ecg_r1/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __version__ = "0.1.0"
2
+
ecg_r1/plugin.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import re
4
+ import textwrap
5
+ from collections import Counter
6
+ from copy import deepcopy
7
+ from typing import Dict, List, Union
8
+
9
+ import json
10
+ import torch
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from modelscope import AutoTokenizer, AutoModel
14
+ import re
15
+ import os
16
+ from typing import List, Optional
17
+
18
+ from swift.llm import PtEngine, RequestConfig, RolloutInferRequest, Template, to_device
19
+ from swift.llm.infer.protocol import ChatCompletionResponse, ChatCompletionResponseChoice
20
+ from swift.plugin import ORM, orms, rm_plugins
21
+ from swift.plugin.context_manager import ContextManager, context_managers
22
+ from swift.plugin.env import Env, envs
23
+ from swift.plugin.multi_turn import MultiTurnScheduler, multi_turns
24
+ from swift.plugin.rm_plugin import DefaultRMPlugin
25
+ from swift.utils import get_logger, get_env_args
26
+
27
+
28
+ logger = get_logger()
29
+
30
+
31
+ class KeyDiagnosticEvidenceORM(ORM):
32
+ STEP_MAPPING = {
33
+ 1: "step_1_technical_rate_rhythm",
34
+ 2: "step_2_conduction_axis_intervals",
35
+ 3: "step_3_chamber_hypertrophy_voltage",
36
+ 4: "step_4_ischemia_infarction_mimics",
37
+ 5: "step_5_electrolytes_qt",
38
+ 6: "step_6_final_medical_reasoning"
39
+ }
40
+
41
+ def __init__(self):
42
+ self.evidence_weight = get_env_args('evidence_weight', float, 1.0)
43
+
44
+ def _parse_model_steps(self, think_text: str) -> Dict[int, str]:
45
+ """
46
+ Parse Steps from model output.
47
+ """
48
+ steps_content = {}
49
+ pattern = re.compile(
50
+ r'\*\*Step\s+(\d+).*?(\*\*|:|\n)(.*?)(?=\*\*Step|\Z)',
51
+ re.DOTALL | re.IGNORECASE
52
+ )
53
+
54
+ for match in pattern.findall(think_text):
55
+ step_num = int(match[0])
56
+ content = match[2].strip()
57
+ if content:
58
+ steps_content[step_num] = content
59
+ return steps_content
60
+
61
+ def _parse_gt_custom(self, gt_text: str) -> Dict[str, List[str]]:
62
+ parsed_data = {}
63
+ pattern = re.compile(r'(step_\d+_[a-z_]+):\s*(.*?)(?=\s*step_\d+|$)', re.DOTALL | re.IGNORECASE)
64
+ matches = pattern.findall(gt_text)
65
+
66
+ for key, content in matches:
67
+ evidences = [e.strip() for e in content.split(';') if e.strip()]
68
+ parsed_data[key.lower()] = evidences
69
+
70
+ return parsed_data
71
+
72
+ def _check_evidence_hit(self, evidence: str, step_text: str) -> bool:
73
+ if not evidence or not step_text:
74
+ return False
75
+
76
+ clean_evidence = re.escape(evidence)
77
+
78
+ match = re.search(clean_evidence, step_text, re.IGNORECASE)
79
+ return match is not None
80
+
81
+ def __call__(self, completions: List[str], solution: List[str], **kwargs) -> List[float]:
82
+ rewards = []
83
+
84
+ think_pattern = r'<think>(.*?)</think>'
85
+ gt_pattern = r'<\|key_diagnostic_evidence\|>(.*?)<\|[/\\]key_diagnostic_evidence\|>'
86
+
87
+ for content, sol in zip(completions, solution):
88
+ reward = 0.0
89
+ try:
90
+ think_match = re.search(think_pattern, content, flags=re.DOTALL | re.IGNORECASE)
91
+ if not think_match:
92
+ rewards.append(0.0)
93
+ continue
94
+
95
+ sol_match = re.search(gt_pattern, sol, flags=re.DOTALL | re.IGNORECASE)
96
+ gt_raw_text = sol_match.group(1).strip() if sol_match else sol.strip()
97
+
98
+ model_steps = self._parse_model_steps(think_match.group(1).strip())
99
+ gt_data = self._parse_gt_custom(gt_raw_text)
100
+
101
+ if not model_steps or not gt_data:
102
+ rewards.append(0.0)
103
+ continue
104
+
105
+ total_step_score = 0.0
106
+ valid_steps_count = 0
107
+
108
+ for step_id, gt_key in self.STEP_MAPPING.items():
109
+ gt_evidences = gt_data.get(gt_key, [])
110
+ if not gt_evidences:
111
+ continue
112
+
113
+ valid_steps_count += 1
114
+ step_text = model_steps.get(step_id, "")
115
+
116
+ if not step_text:
117
+ continue
118
+
119
+ hits = 0
120
+ for anchor in gt_evidences:
121
+ if self._check_evidence_hit(anchor, step_text):
122
+ hits += 1
123
+
124
+ step_score = hits / len(gt_evidences)
125
+ total_step_score += step_score
126
+
127
+ if valid_steps_count > 0:
128
+ reward = (total_step_score / valid_steps_count) * self.evidence_weight
129
+
130
+ except Exception:
131
+ reward = 0.0
132
+
133
+ rewards.append(reward)
134
+
135
+ return rewards
136
+
137
+ orms["key_diagnostic_evidence_reward"] = KeyDiagnosticEvidenceORM
138
+
139
+ class FormatRewardORM(ORM):
140
+ def __call__(self, completions, solution, **kwargs) -> List[float]:
141
+ rewards = []
142
+
143
+ pattern = r'<think>(.*?)</think>'
144
+
145
+ for content in completions:
146
+ reward = 0.0
147
+ try:
148
+ think_match = re.search(pattern, content, flags=re.DOTALL)
149
+
150
+ if think_match and len(think_match.group(1).strip()) > 0:
151
+ reward += 1.0
152
+
153
+ rewards.append(reward)
154
+
155
+ except Exception:
156
+ rewards.append(reward)
157
+
158
+ return rewards
159
+
160
+ orms['format_reward'] = FormatRewardORM
161
+
162
+
163
+ class DiagnosisAccuracyORM(ORM):
164
+ def __call__(self, completions, solution, **kwargs) -> List[float]:
165
+ rewards = []
166
+
167
+ content_pattern = r'<answer>(.*?)</answer>'
168
+ solution_pattern = r'<\|answer\|>(.*?)<\|[/\\]answer\|>'
169
+
170
+ for content, sol in zip(completions, solution):
171
+ reward = 0.0
172
+ try:
173
+ content_match = re.search(content_pattern, content, flags=re.DOTALL)
174
+ sol_match = re.search(solution_pattern, sol, flags=re.DOTALL)
175
+
176
+ if not sol_match or not content_match:
177
+ rewards.append(reward)
178
+ continue
179
+
180
+ gt_text = sol_match.group(1).strip().lower()
181
+ pd_text = content_match.group(1).strip().lower()
182
+
183
+ if ';' in gt_text or ';' in pd_text:
184
+ gt_labels = {label.strip().lower() for label in gt_text.split(';') if label.strip()}
185
+ pd_labels = {label.strip().lower() for label in pd_text.split(';') if label.strip()}
186
+ else: # No semicolon case
187
+ gt_labels = {gt_text} if gt_text else set()
188
+ pd_labels = {pd_text} if pd_text else set()
189
+
190
+ jaccard_reward = self._jaccard(gt_labels, pd_labels)
191
+ reward += jaccard_reward
192
+ rewards.append(reward)
193
+
194
+ except Exception:
195
+ rewards.append(reward)
196
+
197
+ return rewards
198
+
199
+ @staticmethod
200
+ def _jaccard(set1, set2):
201
+ if not set1 and not set2: # Both sets are empty
202
+ return 1.0
203
+ if not set1 or not set2: # One of the sets is empty
204
+ return 0.0
205
+
206
+ intersection = len(set1 & set2)
207
+ union = len(set1 | set2)
208
+ return intersection / union if union > 0 else 0.0
209
+
210
+ orms['diagnosis_accuracy_reward'] = DiagnosisAccuracyORM