Spaces:
Running
Running
Safidin Tsaruev commited on
Commit ·
5b6b189
1
Parent(s): c441b00
Import ECG-R1
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- LICENSE.txt +201 -0
- README.md +181 -13
- ecg_coca/__init__.py +0 -0
- ecg_coca/open_clip/__init__.py +14 -0
- ecg_coca/open_clip/augmentations/RandomMasking.py +145 -0
- ecg_coca/open_clip/augmentations/__init__.py +3 -0
- ecg_coca/open_clip/augmentations/baseline_wander.py +45 -0
- ecg_coca/open_clip/augmentations/cutmix.py +145 -0
- ecg_coca/open_clip/bpe_simple_vocab_16e6.txt.gz +3 -0
- ecg_coca/open_clip/coca_model.py +499 -0
- ecg_coca/open_clip/constants.py +10 -0
- ecg_coca/open_clip/convert.py +190 -0
- ecg_coca/open_clip/ecg_transform.py +248 -0
- ecg_coca/open_clip/factory.py +422 -0
- ecg_coca/open_clip/hf_configs.py +67 -0
- ecg_coca/open_clip/hf_model.py +193 -0
- ecg_coca/open_clip/loss.py +414 -0
- ecg_coca/open_clip/model.py +898 -0
- ecg_coca/open_clip/model_configs/coca_ViT-B-32.json +25 -0
- ecg_coca/open_clip/modified_resnet.py +181 -0
- ecg_coca/open_clip/openai.py +90 -0
- ecg_coca/open_clip/pos_embed.py +98 -0
- ecg_coca/open_clip/pretrained.py +655 -0
- ecg_coca/open_clip/push_to_hf_hub.py +321 -0
- ecg_coca/open_clip/timm_model.py +152 -0
- ecg_coca/open_clip/tokenizer.py +518 -0
- ecg_coca/open_clip/transform.py +73 -0
- ecg_coca/open_clip/transformer.py +1114 -0
- ecg_coca/open_clip/utils.py +90 -0
- ecg_coca/open_clip/version.py +1 -0
- ecg_coca/training/.gitignore +1 -0
- ecg_coca/training/__init__.py +1 -0
- ecg_coca/training/data.py +440 -0
- ecg_coca/training/distributed.py +139 -0
- ecg_coca/training/evaluate.py +73 -0
- ecg_coca/training/evaluation/__init__.py +2 -0
- ecg_coca/training/evaluation/linear_probe.py +60 -0
- ecg_coca/training/evaluation/metadata.py +11 -0
- ecg_coca/training/evaluation/metrics.py +39 -0
- ecg_coca/training/evaluation/zero_shot.py +78 -0
- ecg_coca/training/file_utils.py +84 -0
- ecg_coca/training/logger.py +53 -0
- ecg_coca/training/main.py +543 -0
- ecg_coca/training/params.py +430 -0
- ecg_coca/training/precision.py +20 -0
- ecg_coca/training/profiler.py +246 -0
- ecg_coca/training/scheduler.py +53 -0
- ecg_coca/training/train.py +412 -0
- ecg_r1/__init__.py +2 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|