Equimo 0.2.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {equimo-0.2.3 → equimo-0.3.0}/Equimo.egg-info/PKG-INFO +86 -3
- {equimo-0.2.3 → equimo-0.3.0}/Equimo.egg-info/SOURCES.txt +0 -4
- {equimo-0.2.3 → equimo-0.3.0}/Equimo.egg-info/requires.txt +9 -0
- {equimo-0.2.3 → equimo-0.3.0}/PKG-INFO +86 -3
- {equimo-0.2.3 → equimo-0.3.0}/README.md +78 -2
- equimo-0.3.0/equimo/__init__.py +1 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/io.py +55 -1
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/attention.py +12 -1
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/ffn.py +8 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/utils.py +78 -0
- {equimo-0.2.3 → equimo-0.3.0}/pyproject.toml +14 -1
- {equimo-0.2.3 → equimo-0.3.0}/tests/test_models.py +5 -3
- equimo-0.2.3/equimo/__init__.py +0 -1
- equimo-0.2.3/equimo/converters/__init__.py +0 -1
- equimo-0.2.3/equimo/converters/dinov2.py +0 -69
- equimo-0.2.3/equimo/converters/siglip2.py +0 -105
- equimo-0.2.3/equimo/converters/utils.py +0 -205
- {equimo-0.2.3 → equimo-0.3.0}/Equimo.egg-info/dependency_links.txt +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/Equimo.egg-info/top_level.txt +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/LICENSE.md +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/__init__.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/activation.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/convolution.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/downsample.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/dropout.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/generic.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/mamba.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/norm.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/patch.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/posemb.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/sharing.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/layers/squeeze_excite.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/__init__.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/emamodel.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/fastervit.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/mlla.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/partialformer.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/shvit.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/vit.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/models/vssd.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/equimo/ops/scan.py +0 -0
- {equimo-0.2.3 → equimo-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: Equimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Implementation of popular vision models in Jax
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -24,6 +24,13 @@ Requires-Dist: semver>=3.0.4
|
|
|
24
24
|
Provides-Extra: torch
|
|
25
25
|
Requires-Dist: torch>=2.6.0; extra == "torch"
|
|
26
26
|
Requires-Dist: timm>=1.0.15; extra == "torch"
|
|
27
|
+
Provides-Extra: text
|
|
28
|
+
Requires-Dist: tensorflow>=2.18.1; extra == "text"
|
|
29
|
+
Requires-Dist: tensorflow-text>=2.18.1; extra == "text"
|
|
30
|
+
Provides-Extra: extras
|
|
31
|
+
Requires-Dist: matplotlib>=3.10.1; extra == "extras"
|
|
32
|
+
Requires-Dist: pillow>=11.1.0; extra == "extras"
|
|
33
|
+
Requires-Dist: scikit-learn>=1.6.1; extra == "extras"
|
|
27
34
|
|
|
28
35
|
# Equimo: Modern Vision Models in JAX/Equinox
|
|
29
36
|
|
|
@@ -37,6 +44,7 @@ Equimo (Equinox Image Models) provides JAX/Equinox implementations of recent com
|
|
|
37
44
|
- Focus on recent architectures (2023-2024 papers)
|
|
38
45
|
- Modular design for easy experimentation
|
|
39
46
|
- Extensive documentation and type hints
|
|
47
|
+
- **Experimental** support for text embedding
|
|
40
48
|
|
|
41
49
|
## Installation
|
|
42
50
|
|
|
@@ -98,6 +106,76 @@ x = jax.random.normal(key, (3, 224, 224))
|
|
|
98
106
|
output = model(x, enable_dropout=False, key=key)
|
|
99
107
|
```
|
|
100
108
|
|
|
109
|
+
## Working with text embeddings
|
|
110
|
+
|
|
111
|
+
**Warning: this is experimental, it can break or change at any time**
|
|
112
|
+
|
|
113
|
+
`equimo.experimental.text` has been added since v0.3.0. It allows working with both text and images. It is especially
|
|
114
|
+
useful for models like SigLIP or TIPS, although only TIPS is currently supported.
|
|
115
|
+
|
|
116
|
+
Currently, text tokenization relies on tensorflow_text, install equimo with the `text` group such as
|
|
117
|
+
`uv add equimo[text]`.
|
|
118
|
+
|
|
119
|
+
Here is a very simple example of a 0-shot classification based on the comparison between text and image embeddings:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import jax
|
|
123
|
+
from einops import rearrange
|
|
124
|
+
|
|
125
|
+
from equimo.experimental.text import Tokenizer
|
|
126
|
+
from equimo.io import load_image, load_model
|
|
127
|
+
from equimo.utils import PCAVisualizer, normalize, plot_image_and_feature_map
|
|
128
|
+
|
|
129
|
+
# Random demo inputs
|
|
130
|
+
key = jax.random.PRNGKey(42)
|
|
131
|
+
image = load_image("./demo.jpg", size=448)
|
|
132
|
+
text = [
|
|
133
|
+
"A baby discovering happiness",
|
|
134
|
+
"A computer",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Loading pretrained models
|
|
138
|
+
image_encoder = load_model("vit", "tips_vits14_hr")
|
|
139
|
+
text_encoder = load_model("experimental.textencoder", "tips_vits14_hr_text")
|
|
140
|
+
|
|
141
|
+
# Encoding text and image
|
|
142
|
+
ids, paddings = Tokenizer(identifier="sentencepiece_tips").tokenize(text, max_len=64)
|
|
143
|
+
|
|
144
|
+
text_embedding = normalize(
|
|
145
|
+
jax.vmap(text_encoder, in_axes=(0, 0, None))(ids, paddings, key)
|
|
146
|
+
)
|
|
147
|
+
image_embedding = jax.vmap(image_encoder.norm)(image_encoder.features(image, key))
|
|
148
|
+
cls_token = normalize(image_embedding[0])
|
|
149
|
+
spatial_features = rearrange(
|
|
150
|
+
image_embedding[2:], "(h w) d -> h w d", h=int(448 / 14), w=int(448 / 14)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Getting probabilities based on Cosine Similarity
|
|
154
|
+
cos_sim = jax.nn.softmax(
|
|
155
|
+
((cls_token[None, :] @ text_embedding.T) / text_encoder.temperature), axis=-1
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Plot the results
|
|
159
|
+
label_idxs = jax.numpy.argmax(cos_sim, axis=-1)
|
|
160
|
+
cos_sim_max = jax.numpy.max(cos_sim, axis=-1)
|
|
161
|
+
label_predicted = text[label_idxs[0]]
|
|
162
|
+
similarity = cos_sim_max[0]
|
|
163
|
+
pca_obj = PCAVisualizer(spatial_features)
|
|
164
|
+
image_pca = pca_obj(spatial_features)
|
|
165
|
+
|
|
166
|
+
plot_image_and_feature_map(
|
|
167
|
+
image.transpose(1, 2, 0),
|
|
168
|
+
image_pca,
|
|
169
|
+
"./out.png",
|
|
170
|
+
"Input Image",
|
|
171
|
+
f"{label_predicted}, prob: {similarity * 100:.2f}%",
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Resulting in such a wonderful result:
|
|
176
|
+
|
|
177
|
+

|
|
178
|
+
|
|
101
179
|
## Saving and Loading Models
|
|
102
180
|
|
|
103
181
|
Equimo provides utilities for saving models locally and loading pre-trained models from the
|
|
@@ -155,9 +233,13 @@ model = load_model(
|
|
|
155
233
|
|
|
156
234
|
#### List of pretrained models
|
|
157
235
|
|
|
158
|
-
|
|
236
|
+
The following models have pretrained weights available in Equimo:
|
|
237
|
+
|
|
238
|
+
- [DinoV2](https://arxiv.org/abs/2304.07193),
|
|
239
|
+
- [SigLIP2](https://arxiv.org/abs/2502.14786),
|
|
240
|
+
- [TIPS](https://arxiv.org/abs/2410.16512).
|
|
159
241
|
|
|
160
|
-
Model identifiers
|
|
242
|
+
Model identifiers allow downloading from equimo's [repository on huggingface](https://huggingface.co/poiretclement/equimo/tree/main/models/default)
|
|
161
243
|
|
|
162
244
|
Identifiers are filenames without the extensions, such as:
|
|
163
245
|
|
|
@@ -165,6 +247,7 @@ Identifiers are filenames without the extensions, such as:
|
|
|
165
247
|
- `dinov2_vits14_reg`
|
|
166
248
|
- `siglip2_vitl16_512`
|
|
167
249
|
- `siglip2_vitso400m16_384`
|
|
250
|
+
- `tips_vitg14_lr`
|
|
168
251
|
|
|
169
252
|
## Contributing
|
|
170
253
|
|
|
@@ -9,10 +9,6 @@ Equimo.egg-info/top_level.txt
|
|
|
9
9
|
equimo/__init__.py
|
|
10
10
|
equimo/io.py
|
|
11
11
|
equimo/utils.py
|
|
12
|
-
equimo/converters/__init__.py
|
|
13
|
-
equimo/converters/dinov2.py
|
|
14
|
-
equimo/converters/siglip2.py
|
|
15
|
-
equimo/converters/utils.py
|
|
16
12
|
equimo/layers/__init__.py
|
|
17
13
|
equimo/layers/activation.py
|
|
18
14
|
equimo/layers/attention.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: Equimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Implementation of popular vision models in Jax
|
|
5
5
|
Classifier: Development Status :: 4 - Beta
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -24,6 +24,13 @@ Requires-Dist: semver>=3.0.4
|
|
|
24
24
|
Provides-Extra: torch
|
|
25
25
|
Requires-Dist: torch>=2.6.0; extra == "torch"
|
|
26
26
|
Requires-Dist: timm>=1.0.15; extra == "torch"
|
|
27
|
+
Provides-Extra: text
|
|
28
|
+
Requires-Dist: tensorflow>=2.18.1; extra == "text"
|
|
29
|
+
Requires-Dist: tensorflow-text>=2.18.1; extra == "text"
|
|
30
|
+
Provides-Extra: extras
|
|
31
|
+
Requires-Dist: matplotlib>=3.10.1; extra == "extras"
|
|
32
|
+
Requires-Dist: pillow>=11.1.0; extra == "extras"
|
|
33
|
+
Requires-Dist: scikit-learn>=1.6.1; extra == "extras"
|
|
27
34
|
|
|
28
35
|
# Equimo: Modern Vision Models in JAX/Equinox
|
|
29
36
|
|
|
@@ -37,6 +44,7 @@ Equimo (Equinox Image Models) provides JAX/Equinox implementations of recent com
|
|
|
37
44
|
- Focus on recent architectures (2023-2024 papers)
|
|
38
45
|
- Modular design for easy experimentation
|
|
39
46
|
- Extensive documentation and type hints
|
|
47
|
+
- **Experimental** support for text embedding
|
|
40
48
|
|
|
41
49
|
## Installation
|
|
42
50
|
|
|
@@ -98,6 +106,76 @@ x = jax.random.normal(key, (3, 224, 224))
|
|
|
98
106
|
output = model(x, enable_dropout=False, key=key)
|
|
99
107
|
```
|
|
100
108
|
|
|
109
|
+
## Working with text embeddings
|
|
110
|
+
|
|
111
|
+
**Warning: this is experimental, it can break or change at any time**
|
|
112
|
+
|
|
113
|
+
`equimo.experimental.text` has been added since v0.3.0. It allows working with both text and images. It is especially
|
|
114
|
+
useful for models like SigLIP or TIPS, although only TIPS is currently supported.
|
|
115
|
+
|
|
116
|
+
Currently, text tokenization relies on tensorflow_text, install equimo with the `text` group such as
|
|
117
|
+
`uv add equimo[text]`.
|
|
118
|
+
|
|
119
|
+
Here is a very simple example of a 0-shot classification based on the comparison between text and image embeddings:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import jax
|
|
123
|
+
from einops import rearrange
|
|
124
|
+
|
|
125
|
+
from equimo.experimental.text import Tokenizer
|
|
126
|
+
from equimo.io import load_image, load_model
|
|
127
|
+
from equimo.utils import PCAVisualizer, normalize, plot_image_and_feature_map
|
|
128
|
+
|
|
129
|
+
# Random demo inputs
|
|
130
|
+
key = jax.random.PRNGKey(42)
|
|
131
|
+
image = load_image("./demo.jpg", size=448)
|
|
132
|
+
text = [
|
|
133
|
+
"A baby discovering happiness",
|
|
134
|
+
"A computer",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Loading pretrained models
|
|
138
|
+
image_encoder = load_model("vit", "tips_vits14_hr")
|
|
139
|
+
text_encoder = load_model("experimental.textencoder", "tips_vits14_hr_text")
|
|
140
|
+
|
|
141
|
+
# Encoding text and image
|
|
142
|
+
ids, paddings = Tokenizer(identifier="sentencepiece_tips").tokenize(text, max_len=64)
|
|
143
|
+
|
|
144
|
+
text_embedding = normalize(
|
|
145
|
+
jax.vmap(text_encoder, in_axes=(0, 0, None))(ids, paddings, key)
|
|
146
|
+
)
|
|
147
|
+
image_embedding = jax.vmap(image_encoder.norm)(image_encoder.features(image, key))
|
|
148
|
+
cls_token = normalize(image_embedding[0])
|
|
149
|
+
spatial_features = rearrange(
|
|
150
|
+
image_embedding[2:], "(h w) d -> h w d", h=int(448 / 14), w=int(448 / 14)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Getting probabilities based on Cosine Similarity
|
|
154
|
+
cos_sim = jax.nn.softmax(
|
|
155
|
+
((cls_token[None, :] @ text_embedding.T) / text_encoder.temperature), axis=-1
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Plot the results
|
|
159
|
+
label_idxs = jax.numpy.argmax(cos_sim, axis=-1)
|
|
160
|
+
cos_sim_max = jax.numpy.max(cos_sim, axis=-1)
|
|
161
|
+
label_predicted = text[label_idxs[0]]
|
|
162
|
+
similarity = cos_sim_max[0]
|
|
163
|
+
pca_obj = PCAVisualizer(spatial_features)
|
|
164
|
+
image_pca = pca_obj(spatial_features)
|
|
165
|
+
|
|
166
|
+
plot_image_and_feature_map(
|
|
167
|
+
image.transpose(1, 2, 0),
|
|
168
|
+
image_pca,
|
|
169
|
+
"./out.png",
|
|
170
|
+
"Input Image",
|
|
171
|
+
f"{label_predicted}, prob: {similarity * 100:.2f}%",
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Resulting in such a wonderful result:
|
|
176
|
+
|
|
177
|
+

|
|
178
|
+
|
|
101
179
|
## Saving and Loading Models
|
|
102
180
|
|
|
103
181
|
Equimo provides utilities for saving models locally and loading pre-trained models from the
|
|
@@ -155,9 +233,13 @@ model = load_model(
|
|
|
155
233
|
|
|
156
234
|
#### List of pretrained models
|
|
157
235
|
|
|
158
|
-
|
|
236
|
+
The following models have pretrained weights available in Equimo:
|
|
237
|
+
|
|
238
|
+
- [DinoV2](https://arxiv.org/abs/2304.07193),
|
|
239
|
+
- [SigLIP2](https://arxiv.org/abs/2502.14786),
|
|
240
|
+
- [TIPS](https://arxiv.org/abs/2410.16512).
|
|
159
241
|
|
|
160
|
-
Model identifiers
|
|
242
|
+
Model identifiers allow downloading from equimo's [repository on huggingface](https://huggingface.co/poiretclement/equimo/tree/main/models/default)
|
|
161
243
|
|
|
162
244
|
Identifiers are filenames without the extensions, such as:
|
|
163
245
|
|
|
@@ -165,6 +247,7 @@ Identifiers are filenames without the extensions, such as:
|
|
|
165
247
|
- `dinov2_vits14_reg`
|
|
166
248
|
- `siglip2_vitl16_512`
|
|
167
249
|
- `siglip2_vitso400m16_384`
|
|
250
|
+
- `tips_vitg14_lr`
|
|
168
251
|
|
|
169
252
|
## Contributing
|
|
170
253
|
|
|
@@ -10,6 +10,7 @@ Equimo (Equinox Image Models) provides JAX/Equinox implementations of recent com
|
|
|
10
10
|
- Focus on recent architectures (2023-2024 papers)
|
|
11
11
|
- Modular design for easy experimentation
|
|
12
12
|
- Extensive documentation and type hints
|
|
13
|
+
- **Experimental** support for text embedding
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -71,6 +72,76 @@ x = jax.random.normal(key, (3, 224, 224))
|
|
|
71
72
|
output = model(x, enable_dropout=False, key=key)
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
## Working with text embeddings
|
|
76
|
+
|
|
77
|
+
**Warning: this is experimental, it can break or change at any time**
|
|
78
|
+
|
|
79
|
+
`equimo.experimental.text` has been added since v0.3.0. It allows working with both text and images. It is especially
|
|
80
|
+
useful for models like SigLIP or TIPS, although only TIPS is currently supported.
|
|
81
|
+
|
|
82
|
+
Currently, text tokenization relies on tensorflow_text, install equimo with the `text` group such as
|
|
83
|
+
`uv add equimo[text]`.
|
|
84
|
+
|
|
85
|
+
Here is a very simple example of a 0-shot classification based on the comparison between text and image embeddings:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import jax
|
|
89
|
+
from einops import rearrange
|
|
90
|
+
|
|
91
|
+
from equimo.experimental.text import Tokenizer
|
|
92
|
+
from equimo.io import load_image, load_model
|
|
93
|
+
from equimo.utils import PCAVisualizer, normalize, plot_image_and_feature_map
|
|
94
|
+
|
|
95
|
+
# Random demo inputs
|
|
96
|
+
key = jax.random.PRNGKey(42)
|
|
97
|
+
image = load_image("./demo.jpg", size=448)
|
|
98
|
+
text = [
|
|
99
|
+
"A baby discovering happiness",
|
|
100
|
+
"A computer",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Loading pretrained models
|
|
104
|
+
image_encoder = load_model("vit", "tips_vits14_hr")
|
|
105
|
+
text_encoder = load_model("experimental.textencoder", "tips_vits14_hr_text")
|
|
106
|
+
|
|
107
|
+
# Encoding text and image
|
|
108
|
+
ids, paddings = Tokenizer(identifier="sentencepiece_tips").tokenize(text, max_len=64)
|
|
109
|
+
|
|
110
|
+
text_embedding = normalize(
|
|
111
|
+
jax.vmap(text_encoder, in_axes=(0, 0, None))(ids, paddings, key)
|
|
112
|
+
)
|
|
113
|
+
image_embedding = jax.vmap(image_encoder.norm)(image_encoder.features(image, key))
|
|
114
|
+
cls_token = normalize(image_embedding[0])
|
|
115
|
+
spatial_features = rearrange(
|
|
116
|
+
image_embedding[2:], "(h w) d -> h w d", h=int(448 / 14), w=int(448 / 14)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Getting probabilities based on Cosine Similarity
|
|
120
|
+
cos_sim = jax.nn.softmax(
|
|
121
|
+
((cls_token[None, :] @ text_embedding.T) / text_encoder.temperature), axis=-1
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Plot the results
|
|
125
|
+
label_idxs = jax.numpy.argmax(cos_sim, axis=-1)
|
|
126
|
+
cos_sim_max = jax.numpy.max(cos_sim, axis=-1)
|
|
127
|
+
label_predicted = text[label_idxs[0]]
|
|
128
|
+
similarity = cos_sim_max[0]
|
|
129
|
+
pca_obj = PCAVisualizer(spatial_features)
|
|
130
|
+
image_pca = pca_obj(spatial_features)
|
|
131
|
+
|
|
132
|
+
plot_image_and_feature_map(
|
|
133
|
+
image.transpose(1, 2, 0),
|
|
134
|
+
image_pca,
|
|
135
|
+
"./out.png",
|
|
136
|
+
"Input Image",
|
|
137
|
+
f"{label_predicted}, prob: {similarity * 100:.2f}%",
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Resulting in such a wonderful result:
|
|
142
|
+
|
|
143
|
+

|
|
144
|
+
|
|
74
145
|
## Saving and Loading Models
|
|
75
146
|
|
|
76
147
|
Equimo provides utilities for saving models locally and loading pre-trained models from the
|
|
@@ -128,9 +199,13 @@ model = load_model(
|
|
|
128
199
|
|
|
129
200
|
#### List of pretrained models
|
|
130
201
|
|
|
131
|
-
|
|
202
|
+
The following models have pretrained weights available in Equimo:
|
|
203
|
+
|
|
204
|
+
- [DinoV2](https://arxiv.org/abs/2304.07193),
|
|
205
|
+
- [SigLIP2](https://arxiv.org/abs/2502.14786),
|
|
206
|
+
- [TIPS](https://arxiv.org/abs/2410.16512).
|
|
132
207
|
|
|
133
|
-
Model identifiers
|
|
208
|
+
Model identifiers allow downloading from equimo's [repository on huggingface](https://huggingface.co/poiretclement/equimo/tree/main/models/default)
|
|
134
209
|
|
|
135
210
|
Identifiers are filenames without the extensions, such as:
|
|
136
211
|
|
|
@@ -138,6 +213,7 @@ Identifiers are filenames without the extensions, such as:
|
|
|
138
213
|
- `dinov2_vits14_reg`
|
|
139
214
|
- `siglip2_vitl16_512`
|
|
140
215
|
- `siglip2_vitso400m16_384`
|
|
216
|
+
- `tips_vitg14_lr`
|
|
141
217
|
|
|
142
218
|
## Contributing
|
|
143
219
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import io
|
|
2
3
|
import tarfile
|
|
3
4
|
import tempfile
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
5
7
|
|
|
6
8
|
import equinox as eqx
|
|
7
9
|
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
8
11
|
import lz4.frame
|
|
9
12
|
import requests
|
|
10
13
|
from loguru import logger
|
|
@@ -72,6 +75,7 @@ def save_model(
|
|
|
72
75
|
eqx.tree_serialise_leaves(tmp_path / "weights.eqx", model)
|
|
73
76
|
|
|
74
77
|
# Create compressed archive
|
|
78
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
79
|
with lz4.frame.open(path, "wb") as f_out:
|
|
76
80
|
with tarfile.open(fileobj=f_out, mode="w") as tar:
|
|
77
81
|
tar.add(tmp_path / "metadata.json", arcname="metadata.json")
|
|
@@ -130,6 +134,7 @@ def load_model(
|
|
|
130
134
|
identifier: str | None = None,
|
|
131
135
|
path: Path | None = None,
|
|
132
136
|
repository: str = DEFAULT_REPOSITORY_URL,
|
|
137
|
+
inference_mode: bool = True,
|
|
133
138
|
**model_kwargs,
|
|
134
139
|
) -> eqx.Module:
|
|
135
140
|
"""Load an Equinox model from either a local path or remote repository.
|
|
@@ -143,6 +148,8 @@ def load_model(
|
|
|
143
148
|
Mutually exclusive with identifier. Defaults to None.
|
|
144
149
|
repository (str, optional): Base URL for model download.
|
|
145
150
|
Defaults to DEFAULT_REPOSITORY_URL.
|
|
151
|
+
inference_mode (bool): Disables dropouts if True.
|
|
152
|
+
Defaults to True.
|
|
146
153
|
model_kwargs: kwargs passed to model instanciation. Overrides metadatas.
|
|
147
154
|
|
|
148
155
|
Returns:
|
|
@@ -218,6 +225,10 @@ def load_model(
|
|
|
218
225
|
model_cls = em.FasterViT
|
|
219
226
|
case "partialformer":
|
|
220
227
|
model_cls = em.PartialFormer
|
|
228
|
+
case "experimental.textencoder":
|
|
229
|
+
from equimo.experimental.text import TextEncoder
|
|
230
|
+
|
|
231
|
+
model_cls = TextEncoder
|
|
221
232
|
case _:
|
|
222
233
|
raise ValueError(f"Unknown model class: {cls}")
|
|
223
234
|
|
|
@@ -225,9 +236,52 @@ def load_model(
|
|
|
225
236
|
kwargs = metadata["model_config"] | model_kwargs
|
|
226
237
|
model = model_cls(**kwargs, key=jax.random.PRNGKey(42))
|
|
227
238
|
|
|
228
|
-
# Load weights
|
|
239
|
+
# Load weights and set inference mode
|
|
229
240
|
model = eqx.tree_deserialise_leaves(load_path / "weights.eqx", model)
|
|
241
|
+
model = eqx.nn.inference_mode(model, inference_mode)
|
|
230
242
|
|
|
231
243
|
logger.info("Model loaded successfully.")
|
|
232
244
|
|
|
233
245
|
return model
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_image(
|
|
249
|
+
path: str,
|
|
250
|
+
mean: Optional[list[float]] = None,
|
|
251
|
+
std: Optional[list[float]] = None,
|
|
252
|
+
size: Optional[int] = None,
|
|
253
|
+
):
|
|
254
|
+
"""Load an image and perform minor preprocessing.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
path (str): Path of the image.
|
|
258
|
+
mean (list, optional): Channel mean for normalization.
|
|
259
|
+
std (list, optional): Channel std for normalization.
|
|
260
|
+
size (int, optional): Size to which resize the image.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
jnp.array: The loaded image.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ImportError: If PIL can't be loaded.
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
from PIL import Image
|
|
270
|
+
except ImportError:
|
|
271
|
+
raise ImportError("PIL is needed to be able to load images.")
|
|
272
|
+
|
|
273
|
+
with open(path, "rb") as fd:
|
|
274
|
+
image_bytes = io.BytesIO(fd.read())
|
|
275
|
+
pil_image = Image.open(image_bytes)
|
|
276
|
+
|
|
277
|
+
array = jnp.array(pil_image).astype(jnp.float32) / 255.0
|
|
278
|
+
if size is not None:
|
|
279
|
+
array = jax.image.resize(array, (size, size, 3), method="bilinear")
|
|
280
|
+
|
|
281
|
+
if mean is not None and std is not None:
|
|
282
|
+
mean = jnp.array(mean)[None, None, :]
|
|
283
|
+
std = jnp.array(std)[None, None, :]
|
|
284
|
+
|
|
285
|
+
array = (array - mean) / std
|
|
286
|
+
|
|
287
|
+
return array.transpose(2, 0, 1)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from typing import Callable, List, Optional, Tuple
|
|
2
|
-
|
|
3
2
|
import equinox as eqx
|
|
4
3
|
import jax
|
|
5
4
|
import jax.numpy as jnp
|
|
@@ -74,6 +73,7 @@ class Attention(eqx.Module):
|
|
|
74
73
|
x: Float[Array, "seqlen dim"],
|
|
75
74
|
key: PRNGKeyArray,
|
|
76
75
|
inference: Optional[bool] = None,
|
|
76
|
+
mask: Optional[Float[Array, ""]] = None,
|
|
77
77
|
) -> Float[Array, "seqlen dim"]:
|
|
78
78
|
key1, key2 = jr.split(key, 2)
|
|
79
79
|
|
|
@@ -90,6 +90,10 @@ class Attention(eqx.Module):
|
|
|
90
90
|
k = jax.vmap(jax.vmap(self.k_norm))(k)
|
|
91
91
|
|
|
92
92
|
attn = jnp.einsum("hqd,hkd->hqk", q, k) / jnp.sqrt(self.head_dim)
|
|
93
|
+
|
|
94
|
+
if mask is not None:
|
|
95
|
+
attn = jnp.where(mask == 0, jnp.finfo(attn.dtype).min, attn)
|
|
96
|
+
|
|
93
97
|
attn = jax.nn.softmax(attn, axis=-1)
|
|
94
98
|
attn = self.attn_drop(attn, inference=inference, key=key1)
|
|
95
99
|
|
|
@@ -312,9 +316,14 @@ class AttentionBlock(eqx.Module):
|
|
|
312
316
|
x: Float[Array, "seqlen dim"],
|
|
313
317
|
key: PRNGKeyArray,
|
|
314
318
|
inference: Optional[bool] = None,
|
|
319
|
+
mask: Optional[Float[Array, ""]] = None,
|
|
315
320
|
) -> Float[Array, "seqlen dim"]:
|
|
316
321
|
key_attn, key_mlp, key_dr1, key_dr2 = jr.split(key, 4)
|
|
317
322
|
|
|
323
|
+
# I chose to define extra args here rather than passing mask directly
|
|
324
|
+
# because not all attention mechanisms support masks as args
|
|
325
|
+
extra_kwargs = {"mask": mask} if mask is not None else {}
|
|
326
|
+
|
|
318
327
|
x = self.drop_path1(
|
|
319
328
|
x,
|
|
320
329
|
self.ls1(
|
|
@@ -323,6 +332,7 @@ class AttentionBlock(eqx.Module):
|
|
|
323
332
|
jax.vmap(self.prenorm)(x),
|
|
324
333
|
inference=inference,
|
|
325
334
|
key=key_attn,
|
|
335
|
+
**extra_kwargs,
|
|
326
336
|
)
|
|
327
337
|
)
|
|
328
338
|
),
|
|
@@ -336,6 +346,7 @@ class AttentionBlock(eqx.Module):
|
|
|
336
346
|
jax.vmap(self.norm)(x),
|
|
337
347
|
inference=inference,
|
|
338
348
|
key=key_mlp,
|
|
349
|
+
**extra_kwargs,
|
|
339
350
|
)
|
|
340
351
|
),
|
|
341
352
|
inference=inference,
|
|
@@ -210,6 +210,7 @@ class Mlp(eqx.Module):
|
|
|
210
210
|
self,
|
|
211
211
|
x: Float[Array, "seqlen dim"],
|
|
212
212
|
key: PRNGKeyArray,
|
|
213
|
+
mask: Optional[Float[Array, ""]] = None,
|
|
213
214
|
inference: Optional[bool] = None,
|
|
214
215
|
) -> Float[Array, "seqlen dim"]:
|
|
215
216
|
key_dr1, key_dr2 = jr.split(key, 2)
|
|
@@ -219,12 +220,19 @@ class Mlp(eqx.Module):
|
|
|
219
220
|
inference=inference,
|
|
220
221
|
key=key_dr1,
|
|
221
222
|
)
|
|
223
|
+
|
|
224
|
+
if mask is not None:
|
|
225
|
+
x *= mask
|
|
226
|
+
|
|
222
227
|
x = self.drop2(
|
|
223
228
|
jax.vmap(self.fc2)(x),
|
|
224
229
|
inference=inference,
|
|
225
230
|
key=key_dr2,
|
|
226
231
|
)
|
|
227
232
|
|
|
233
|
+
if mask is not None:
|
|
234
|
+
x *= mask
|
|
235
|
+
|
|
228
236
|
return x
|
|
229
237
|
|
|
230
238
|
|
|
@@ -4,6 +4,84 @@ import jax
|
|
|
4
4
|
import jax.numpy as jnp
|
|
5
5
|
from jaxtyping import Array, Float
|
|
6
6
|
|
|
7
|
+
import typing as t
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_ArrayLike = t.Union[np.ndarray, jnp.ndarray]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize(x, order: int = 2):
|
|
15
|
+
return x / np.linalg.norm(x, ord=order, axis=-1, keepdims=True).clip(min=1e-3)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PCAVisualizer:
|
|
19
|
+
"""PCA visualizer.
|
|
20
|
+
|
|
21
|
+
Taken as-is from https://github.com/google-deepmind/tips/blob/72820c1841f973c9543d9c95c5ff2262ec621955/scenic/utils/feature_viz.py#L32
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, features: _ArrayLike, n_samples: int = 100000, n_components: int = 3
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Creates a PCA object for visualizing features of shape [..., F]."""
|
|
28
|
+
try:
|
|
29
|
+
from sklearn import decomposition
|
|
30
|
+
except ImportError:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"You need sklearn to use the PCAVisualizer, install it using `uv add equimo[viz]`"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
features = np.array(features)
|
|
36
|
+
pca_object = decomposition.PCA(n_components=n_components)
|
|
37
|
+
features = features.reshape([-1, features.shape[-1]])
|
|
38
|
+
features = features[np.random.randint(0, features.shape[0], n_samples), :]
|
|
39
|
+
pca_object.fit(features)
|
|
40
|
+
self.pca_object = pca_object
|
|
41
|
+
self.n_components = n_components
|
|
42
|
+
|
|
43
|
+
def __call__(self, features: _ArrayLike) -> np.ndarray:
|
|
44
|
+
"""Apply PCA to features of shape [..., F]."""
|
|
45
|
+
features = np.array(features)
|
|
46
|
+
features_pca = self.pca_object.transform(
|
|
47
|
+
features.reshape([-1, features.shape[-1]])
|
|
48
|
+
).reshape(features.shape[:-1] + (self.n_components,))
|
|
49
|
+
return normalize(features_pca) * 0.5 + 0.5
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def plot_image_and_feature_map(
|
|
53
|
+
image,
|
|
54
|
+
feature_map,
|
|
55
|
+
save_path,
|
|
56
|
+
image_title="Image",
|
|
57
|
+
map_title="Feature Map",
|
|
58
|
+
fontsize=16,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Plots an image and its feature map side by side and saves the figure.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
import matplotlib.pyplot as plt
|
|
65
|
+
except ImportError:
|
|
66
|
+
raise ImportError("Matplotlib is required to plot an image")
|
|
67
|
+
|
|
68
|
+
feature_map_normalized = (feature_map - feature_map.min()) / (
|
|
69
|
+
feature_map.max() - feature_map.min()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
|
|
73
|
+
ax1.imshow(image)
|
|
74
|
+
ax1.set_title(image_title, fontsize=fontsize)
|
|
75
|
+
ax1.axis("off")
|
|
76
|
+
|
|
77
|
+
ax2.imshow(feature_map_normalized, cmap="gray")
|
|
78
|
+
ax2.set_title(map_title, fontsize=fontsize)
|
|
79
|
+
ax2.axis("off")
|
|
80
|
+
|
|
81
|
+
plt.tight_layout()
|
|
82
|
+
plt.savefig(save_path)
|
|
83
|
+
plt.close()
|
|
84
|
+
|
|
7
85
|
|
|
8
86
|
def make_2tuple(x):
|
|
9
87
|
"""Convert input into a 2-tuple.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Equimo"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Implementation of popular vision models in Jax"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -31,10 +31,22 @@ classifiers = [
|
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
|
+
# Useful to convert models from PyTorch
|
|
34
35
|
torch = [
|
|
35
36
|
"torch>=2.6.0",
|
|
36
37
|
"timm>=1.0.15",
|
|
37
38
|
]
|
|
39
|
+
# Required for equimo.experimental.text (text tokenizer)
|
|
40
|
+
text = [
|
|
41
|
+
"tensorflow>=2.18.1",
|
|
42
|
+
"tensorflow-text>=2.18.1",
|
|
43
|
+
]
|
|
44
|
+
# Required for some external utils
|
|
45
|
+
extras = [
|
|
46
|
+
"matplotlib>=3.10.1",
|
|
47
|
+
"pillow>=11.1.0",
|
|
48
|
+
"scikit-learn>=1.6.1",
|
|
49
|
+
]
|
|
38
50
|
|
|
39
51
|
[dependency-groups]
|
|
40
52
|
dev = [
|
|
@@ -43,3 +55,4 @@ dev = [
|
|
|
43
55
|
|
|
44
56
|
[tool.setuptools]
|
|
45
57
|
license-files = []
|
|
58
|
+
packages = ["equimo"]
|
|
@@ -75,7 +75,9 @@ def test_save_load_model_compressed():
|
|
|
75
75
|
save_model(save_path, model, model_config, torch_hub_cfg, compression=True)
|
|
76
76
|
|
|
77
77
|
# Load model
|
|
78
|
-
loaded_model = load_model(
|
|
78
|
+
loaded_model = load_model(
|
|
79
|
+
cls="vit", path=save_path.with_suffix(".tar.lz4"), dynamic_img_size=True
|
|
80
|
+
)
|
|
79
81
|
|
|
80
82
|
# Test loaded model
|
|
81
83
|
loaded_output = loaded_model.features(x, key=key)
|
|
@@ -116,7 +118,7 @@ def test_save_load_model_uncompressed():
|
|
|
116
118
|
|
|
117
119
|
save_model(save_path, model, model_config, torch_hub_cfg, compression=False)
|
|
118
120
|
|
|
119
|
-
loaded_model = load_model(cls="vit", path=save_path)
|
|
121
|
+
loaded_model = load_model(cls="vit", path=save_path, dynamic_img_size=True)
|
|
120
122
|
loaded_output = loaded_model.features(x, key=key)
|
|
121
123
|
|
|
122
124
|
assert jnp.allclose(original_output, loaded_output, atol=1e-5)
|
|
@@ -125,7 +127,7 @@ def test_save_load_model_uncompressed():
|
|
|
125
127
|
def test_load_pretrained_model():
|
|
126
128
|
"""Test loading a pretrained model from the repository."""
|
|
127
129
|
key = jr.PRNGKey(42)
|
|
128
|
-
model = load_model(cls="vit", identifier="dinov2_vits14_reg")
|
|
130
|
+
model = load_model(cls="vit", identifier="dinov2_vits14_reg", dynamic_img_size=True)
|
|
129
131
|
|
|
130
132
|
# Test inference
|
|
131
133
|
x = jr.normal(key, (3, 224, 224))
|
equimo-0.2.3/equimo/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.3"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import jax
|
|
4
|
-
|
|
5
|
-
import equimo.models as em
|
|
6
|
-
from equimo.converters.utils import convert_torch_to_equinox
|
|
7
|
-
from equimo.io import load_model, save_model
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def main():
|
|
11
|
-
key = jax.random.PRNGKey(42)
|
|
12
|
-
dinov2_vits14_reg_config = {
|
|
13
|
-
"img_size": 518,
|
|
14
|
-
"in_channels": 3,
|
|
15
|
-
"dim": 384,
|
|
16
|
-
"patch_size": 14,
|
|
17
|
-
"num_heads": [6],
|
|
18
|
-
"depths": [12],
|
|
19
|
-
"num_classes": 0,
|
|
20
|
-
"use_mask_token": True,
|
|
21
|
-
"reg_tokens": 4,
|
|
22
|
-
"init_values": 1e-5,
|
|
23
|
-
"eps": 1e-6,
|
|
24
|
-
"dynamic_img_size": True,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
dinov2_vits14_reg = em.VisionTransformer(
|
|
28
|
-
**dinov2_vits14_reg_config,
|
|
29
|
-
key=key,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
torch_hub_cfg = ["facebookresearch/dinov2", "dinov2_vits14_reg"]
|
|
33
|
-
|
|
34
|
-
replace_cfg = {
|
|
35
|
-
"reg_tokens": "register_tokens",
|
|
36
|
-
"blocks.0.blocks": "blocks",
|
|
37
|
-
".prenorm.": ".norm1.",
|
|
38
|
-
".norm.": ".norm2.",
|
|
39
|
-
}
|
|
40
|
-
expand_cfg = {"patch_embed.proj.bias": ["after", 2]}
|
|
41
|
-
squeeze_cfg = {
|
|
42
|
-
"pos_embed": 0,
|
|
43
|
-
"cls_token": 0,
|
|
44
|
-
"register_tokens": 0,
|
|
45
|
-
}
|
|
46
|
-
whitelist = []
|
|
47
|
-
|
|
48
|
-
dinov2_vits14_reg = convert_torch_to_equinox(
|
|
49
|
-
dinov2_vits14_reg,
|
|
50
|
-
torch_hub_cfg,
|
|
51
|
-
replace_cfg,
|
|
52
|
-
expand_cfg,
|
|
53
|
-
squeeze_cfg,
|
|
54
|
-
whitelist,
|
|
55
|
-
strict=True,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
save_model(
|
|
59
|
-
Path("~/.cache/equimo/dinov2_vits14_reg").expanduser(),
|
|
60
|
-
dinov2_vits14_reg,
|
|
61
|
-
dinov2_vits14_reg_config,
|
|
62
|
-
torch_hub_cfg,
|
|
63
|
-
compression=True,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
_ = load_model(
|
|
67
|
-
cls="vit",
|
|
68
|
-
path=Path("~/.cache/equimo/dinov2_vits14_reg.tar.lz4").expanduser(),
|
|
69
|
-
)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import jax
|
|
4
|
-
import jax.numpy as jnp
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
import equimo.models as em
|
|
8
|
-
from equimo.converters.utils import convert_torch_to_equinox
|
|
9
|
-
from equimo.io import load_model, save_model
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def compare(j, t) -> float:
|
|
13
|
-
j = np.array(j)
|
|
14
|
-
t = t.squeeze().detach().numpy()
|
|
15
|
-
return float(np.mean(np.abs(j - t)))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def main():
|
|
19
|
-
try:
|
|
20
|
-
import torch
|
|
21
|
-
except:
|
|
22
|
-
raise ImportError("`torch` not available")
|
|
23
|
-
|
|
24
|
-
key = jax.random.PRNGKey(42)
|
|
25
|
-
siglip2_config = {
|
|
26
|
-
"img_size": 384,
|
|
27
|
-
"in_channels": 3,
|
|
28
|
-
"dim": 1536,
|
|
29
|
-
"patch_size": 16,
|
|
30
|
-
"num_heads": [16],
|
|
31
|
-
"depths": [40],
|
|
32
|
-
"mlp_ratio": 4,
|
|
33
|
-
"num_classes": 0,
|
|
34
|
-
"use_mask_token": False,
|
|
35
|
-
"reg_tokens": 0,
|
|
36
|
-
"class_token": False,
|
|
37
|
-
"no_embed_class": True,
|
|
38
|
-
"init_values": None,
|
|
39
|
-
"eps": 1e-6,
|
|
40
|
-
"dynamic_img_size": False,
|
|
41
|
-
# "act_layer": "exactgelu",
|
|
42
|
-
"act_layer": "gelu",
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
siglip2 = em.VisionTransformer(
|
|
46
|
-
**siglip2_config,
|
|
47
|
-
key=key,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
timm_cfg = ["vit_giantopt_patch16_siglip_gap_384.v2_webli", True]
|
|
51
|
-
_n = timm_cfg[0].split("_")
|
|
52
|
-
mname = f"siglip2_vitgiantopt{_n[2][-2:]}_{_n[-2][:3]}"
|
|
53
|
-
# mname = f"siglip2_vit{_n[1][0]}{_n[2][-2:]}_{_n[-2][:3]}"
|
|
54
|
-
|
|
55
|
-
print(f"Converting {mname}...")
|
|
56
|
-
|
|
57
|
-
replace_cfg = {
|
|
58
|
-
"blocks.0.blocks": "blocks",
|
|
59
|
-
".prenorm.": ".norm1.",
|
|
60
|
-
".norm.": ".norm2.",
|
|
61
|
-
}
|
|
62
|
-
expand_cfg = {"patch_embed.proj.bias": ["after", 2]}
|
|
63
|
-
squeeze_cfg = {
|
|
64
|
-
"pos_embed": 0,
|
|
65
|
-
"cls_token": 0,
|
|
66
|
-
"register_tokens": 0,
|
|
67
|
-
}
|
|
68
|
-
whitelist = []
|
|
69
|
-
|
|
70
|
-
siglip2, torch_model = convert_torch_to_equinox(
|
|
71
|
-
siglip2,
|
|
72
|
-
replace_cfg,
|
|
73
|
-
expand_cfg,
|
|
74
|
-
squeeze_cfg,
|
|
75
|
-
whitelist,
|
|
76
|
-
strict=True,
|
|
77
|
-
source="timm",
|
|
78
|
-
timm_cfg=timm_cfg,
|
|
79
|
-
return_torch=True,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
arr = np.random.randn(3, siglip2_config["img_size"], siglip2_config["img_size"])
|
|
83
|
-
jax_arr = jnp.array(arr)
|
|
84
|
-
torch_arr = torch.tensor(arr).unsqueeze(0).float()
|
|
85
|
-
|
|
86
|
-
assert (
|
|
87
|
-
compare(
|
|
88
|
-
jax.vmap(siglip2.norm)(siglip2.features(jax_arr, key)),
|
|
89
|
-
torch_model.forward_features(torch_arr),
|
|
90
|
-
)
|
|
91
|
-
< 1e-5
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
save_model(
|
|
95
|
-
Path(f"~/.cache/equimo/{mname}").expanduser(),
|
|
96
|
-
siglip2,
|
|
97
|
-
siglip2_config,
|
|
98
|
-
timm_cfg=timm_cfg,
|
|
99
|
-
compression=True,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
_ = load_model(
|
|
103
|
-
cls="vit",
|
|
104
|
-
path=Path(f"~/.cache/equimo/{mname}.tar.lz4").expanduser(),
|
|
105
|
-
)
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from typing import Any, Dict, Literal, Optional, Tuple
|
|
3
|
-
|
|
4
|
-
import equinox as eqx
|
|
5
|
-
import jax
|
|
6
|
-
import jax.numpy as jnp
|
|
7
|
-
from jax.tree_util import GetAttrKey, SequenceKey
|
|
8
|
-
from loguru import logger
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def stringify_name(path: Tuple) -> str:
|
|
12
|
-
stringified = []
|
|
13
|
-
for p in path:
|
|
14
|
-
if isinstance(p, GetAttrKey):
|
|
15
|
-
stringified.append(p.name)
|
|
16
|
-
if isinstance(p, SequenceKey):
|
|
17
|
-
stringified.append(str(p.idx))
|
|
18
|
-
return ".".join(stringified)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def expand_torch_tensor(tensor, pos: str, n: int):
|
|
22
|
-
padding = [None] * n
|
|
23
|
-
match pos:
|
|
24
|
-
case "before":
|
|
25
|
-
return tensor[*padding, ...]
|
|
26
|
-
case "after":
|
|
27
|
-
return tensor[..., *padding]
|
|
28
|
-
case _:
|
|
29
|
-
raise ValueError(
|
|
30
|
-
f"Invalid `pos`, expected one of [`before`, `after`], got: {pos}"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def convert_params_from_torch_hub(
|
|
35
|
-
jax_model: eqx.Module,
|
|
36
|
-
replace_cfg: Dict[str, str],
|
|
37
|
-
expand_cfg: Dict[str, list],
|
|
38
|
-
squeeze_cfg: Dict[str, int | None],
|
|
39
|
-
whitelist: list[str],
|
|
40
|
-
strict: bool = True,
|
|
41
|
-
source: Literal["torchhub", "timm"] = "torchhub",
|
|
42
|
-
torch_hub_cfg: Optional[list[str]] = None,
|
|
43
|
-
timm_cfg: Optional[list] = None,
|
|
44
|
-
return_torch: bool = False,
|
|
45
|
-
):
|
|
46
|
-
"""
|
|
47
|
-
Load weights from a torch hub model into an Equinox module.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
jax_model (eqx.Module): A preexisting Jax model corresponding to the checkpoint to download.
|
|
51
|
-
torch_hub_cfg (Tuple[str]): Arguments passed to `torch.hub.load()`.
|
|
52
|
-
replace_cfg (Dict[str, str]): Rename parameters from key to value.
|
|
53
|
-
expand_cfg (Dict[str, list]): Config to reshape params, see `expand_torch_tensor`
|
|
54
|
-
sqeeze_cfg (Dict[str, int|None]): Config to squeeze tensors, opposite of expand.
|
|
55
|
-
whitelist (Set[str]): Parameters to exclude from format conversion.
|
|
56
|
-
strict (bool): Whether to crash on missing parameters one of the models.
|
|
57
|
-
source (str): Torch Hub or timm.
|
|
58
|
-
torch_hub_cfg (Optional[list]): args to pass to `torch.hub.load`.
|
|
59
|
-
timm_cfg (Optional[list]): args to pass to `timm.create_model`.
|
|
60
|
-
return_torch (bool): Return both jax and torch models.
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
import timm
|
|
64
|
-
import torch
|
|
65
|
-
except:
|
|
66
|
-
raise ImportError("`torch` not available")
|
|
67
|
-
|
|
68
|
-
# Load the pytorch model
|
|
69
|
-
match source:
|
|
70
|
-
case "torchhub":
|
|
71
|
-
if torch_hub_cfg is None:
|
|
72
|
-
raise ValueError(
|
|
73
|
-
"The `torchhub` source is selected but `torch_hub_cfg` is None."
|
|
74
|
-
)
|
|
75
|
-
torch_model = torch.hub.load(*torch_hub_cfg)
|
|
76
|
-
case "timm":
|
|
77
|
-
if timm_cfg is None:
|
|
78
|
-
raise ValueError(
|
|
79
|
-
"The `timm` source is selected but `timm_cfg` is None."
|
|
80
|
-
)
|
|
81
|
-
torch_model = timm.create_model(*timm_cfg)
|
|
82
|
-
|
|
83
|
-
torch_params = dict(torch_model.named_parameters())
|
|
84
|
-
|
|
85
|
-
# Extract the parameters from the defined Jax model
|
|
86
|
-
jax_params = eqx.filter(jax_model, eqx.is_array)
|
|
87
|
-
# _, jax_params, _ = nnx.split(jax_model, nnx.Param, ...)
|
|
88
|
-
jax_params_flat, jax_param_pytree = jax.tree_util.tree_flatten_with_path(jax_params)
|
|
89
|
-
|
|
90
|
-
torch_params_flat = []
|
|
91
|
-
for path, param in jax_params_flat:
|
|
92
|
-
# Match the parameters' path of pytorch
|
|
93
|
-
param_path = stringify_name(path)
|
|
94
|
-
param_path = re.sub(r"\.scale|.kernel", ".weight", param_path)
|
|
95
|
-
|
|
96
|
-
for old, new in replace_cfg.items():
|
|
97
|
-
param_path = param_path.replace(old, new)
|
|
98
|
-
|
|
99
|
-
shape = param.shape
|
|
100
|
-
|
|
101
|
-
if param_path not in torch_params:
|
|
102
|
-
_msg = f"{param_path} ({shape}) not found in PyTorch model."
|
|
103
|
-
if strict:
|
|
104
|
-
logger.error(_msg)
|
|
105
|
-
raise AttributeError(_msg)
|
|
106
|
-
|
|
107
|
-
logger.warning(f"{_msg} Appending `None` to flat param list.")
|
|
108
|
-
torch_params_flat.append(None)
|
|
109
|
-
continue
|
|
110
|
-
|
|
111
|
-
logger.info(f"Converting {param_path}...")
|
|
112
|
-
torch_param = torch_params[param_path]
|
|
113
|
-
|
|
114
|
-
if param_path in expand_cfg:
|
|
115
|
-
torch_param = expand_torch_tensor(torch_param, *expand_cfg[param_path])
|
|
116
|
-
if param_path in squeeze_cfg:
|
|
117
|
-
torch_param = torch.squeeze(torch_param, dim=squeeze_cfg[param_path])
|
|
118
|
-
|
|
119
|
-
if shape != torch_param.shape:
|
|
120
|
-
_msg = f"`{param_path}`: expected shape ({shape}) does not match its pytorch implementation ({torch_param.shape})."
|
|
121
|
-
logger.error(_msg)
|
|
122
|
-
raise ValueError(_msg)
|
|
123
|
-
|
|
124
|
-
torch_params_flat.append(jnp.asarray(torch_param.detach().numpy()))
|
|
125
|
-
_ = torch_params.pop(param_path)
|
|
126
|
-
|
|
127
|
-
loaded_params = jax.tree_util.tree_unflatten(jax_param_pytree, torch_params_flat)
|
|
128
|
-
|
|
129
|
-
for path, param in torch_params.items():
|
|
130
|
-
logger.warning(
|
|
131
|
-
f"PyTorch parameters `{path}` ({param.shape}) were not converted."
|
|
132
|
-
)
|
|
133
|
-
if strict and path not in whitelist:
|
|
134
|
-
_msg = f"The PyTorch model contains parameters ({path}) that do not have a Jax counterpart."
|
|
135
|
-
logger.error(_msg)
|
|
136
|
-
raise AttributeError(_msg)
|
|
137
|
-
|
|
138
|
-
if return_torch:
|
|
139
|
-
return loaded_params, torch_model
|
|
140
|
-
return loaded_params
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def convert_torch_to_equinox(
|
|
144
|
-
jax_model: eqx.Module,
|
|
145
|
-
replace_cfg: dict = {},
|
|
146
|
-
expand_cfg: dict = {},
|
|
147
|
-
squeeze_cfg: dict = {},
|
|
148
|
-
whitelist: list[str] = [],
|
|
149
|
-
strict: bool = True,
|
|
150
|
-
source: Literal["torchhub", "timm"] = "torchhub",
|
|
151
|
-
torch_hub_cfg: Optional[list[str]] = None,
|
|
152
|
-
timm_cfg: Optional[list] = None,
|
|
153
|
-
return_torch: bool = False,
|
|
154
|
-
) -> eqx.Module | Tuple[eqx.Module, Any]:
|
|
155
|
-
"""
|
|
156
|
-
Convert a PyTorch model from torch.hub to Equinox format.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
jax_model: The Equinox model
|
|
160
|
-
replace_cfg: Dict of parameter name replacements
|
|
161
|
-
expand_cfg: Dict of dimensions to expand
|
|
162
|
-
squeeze_cfg: Dict of dimensions to squeeze
|
|
163
|
-
whitelist: List of parameters to keep from JAX model
|
|
164
|
-
strict: Wether to raise an issue if not all weights are converted
|
|
165
|
-
source (str): Torch Hub or timm.
|
|
166
|
-
torch_hub_cfg: [repo, model_name] for torch.hub.load
|
|
167
|
-
timm_cfg (Optional[list]): args to pass to `timm.create_model`.
|
|
168
|
-
return_torch (bool): Return both jax and torch models.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
eqx.Module: Converted Equinox model in inference mode
|
|
172
|
-
"""
|
|
173
|
-
dynamic, static = eqx.partition(jax_model, eqx.is_array)
|
|
174
|
-
if return_torch:
|
|
175
|
-
converted_params, torch_model = convert_params_from_torch_hub(
|
|
176
|
-
dynamic,
|
|
177
|
-
replace_cfg,
|
|
178
|
-
expand_cfg,
|
|
179
|
-
squeeze_cfg,
|
|
180
|
-
whitelist,
|
|
181
|
-
strict,
|
|
182
|
-
source,
|
|
183
|
-
torch_hub_cfg,
|
|
184
|
-
timm_cfg,
|
|
185
|
-
return_torch,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
return eqx.nn.inference_mode(
|
|
189
|
-
eqx.combine(converted_params, static), value=True
|
|
190
|
-
), torch_model.eval()
|
|
191
|
-
else:
|
|
192
|
-
converted_params = convert_params_from_torch_hub(
|
|
193
|
-
dynamic,
|
|
194
|
-
replace_cfg,
|
|
195
|
-
expand_cfg,
|
|
196
|
-
squeeze_cfg,
|
|
197
|
-
whitelist,
|
|
198
|
-
strict,
|
|
199
|
-
source,
|
|
200
|
-
torch_hub_cfg,
|
|
201
|
-
timm_cfg,
|
|
202
|
-
return_torch,
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return eqx.nn.inference_mode(eqx.combine(converted_params, static), value=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|