Equimo 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,329 @@
1
+ from typing import Callable, Optional
2
+
3
+ import equinox as eqx
4
+ import jax
5
+ import jax.random as jr
6
+ from einops import rearrange
7
+ from jaxtyping import Array, Float, PRNGKeyArray
8
+
9
+ from equimo.layers.dropout import DropPathAdd
10
+ from equimo.layers.norm import LayerScale
11
+ from equimo.utils import nearest_power_of_2_divisor
12
+
13
+
14
+ class ConvBlock(eqx.Module):
15
+ """A residual convolutional block with normalization and regularization.
16
+
17
+ This block implements a residual connection with two convolution layers,
18
+ group normalization, activation, layer scaling, and drop path regularization.
19
+ The block maintains the input dimension while allowing for an optional
20
+ intermediate hidden dimension.
21
+
22
+ Attributes:
23
+ conv1: First convolution layer
24
+ conv2: Second convolution layer
25
+ norm1: Group normalization after first conv
26
+ norm2: Group normalization after second conv
27
+ drop_path1: Drop path regularization for residual connection
28
+ act: Activation function
29
+ ls1: Layer scaling module
30
+ """
31
+
32
+ conv1: eqx.nn.Conv
33
+ conv2: eqx.nn.Conv
34
+ norm1: eqx.Module
35
+ norm2: eqx.Module
36
+ drop_path1: DropPathAdd
37
+ act: Callable
38
+ ls1: LayerScale
39
+
40
+ def __init__(
41
+ self,
42
+ dim: int,
43
+ *,
44
+ key: PRNGKeyArray,
45
+ hidden_dim: int | None = None,
46
+ kernel_size: int = 3,
47
+ stride: int = 1,
48
+ padding: int = 1,
49
+ act_layer: Callable = jax.nn.gelu,
50
+ norm_max_group: int = 32,
51
+ drop_path: float = 0.0,
52
+ init_values: float | None = None,
53
+ **kwargs,
54
+ ):
55
+ """Initialize the ConvBlock.
56
+
57
+ Args:
58
+ dim: Input and output channel dimension
59
+ key: PRNG key for initialization
60
+ hidden_dim: Optional intermediate channel dimension (defaults to dim)
61
+ kernel_size: Size of the convolutional kernel (default: 3)
62
+ stride: Stride of the convolution (default: 1)
63
+ padding: Padding size for convolution (default: 1)
64
+ act_layer: Activation function (default: gelu)
65
+ norm_max_group: Maximum number of groups for GroupNorm (default: 32)
66
+ drop_path: Drop path rate (default: 0.0)
67
+ init_values: Initial value for layer scaling (default: None)
68
+ **kwargs: Additional arguments passed to Conv layers
69
+ """
70
+
71
+ key_conv1, key_conv2 = jr.split(key, 2)
72
+ hidden_dim = hidden_dim or dim
73
+ num_groups = nearest_power_of_2_divisor(dim, norm_max_group)
74
+ self.conv1 = eqx.nn.Conv(
75
+ num_spatial_dims=2,
76
+ in_channels=dim,
77
+ out_channels=hidden_dim,
78
+ kernel_size=kernel_size,
79
+ stride=stride,
80
+ padding=padding,
81
+ use_bias=True,
82
+ key=key_conv1,
83
+ )
84
+ self.norm1 = eqx.nn.GroupNorm(num_groups, hidden_dim)
85
+ self.act = act_layer
86
+ self.conv2 = eqx.nn.Conv(
87
+ num_spatial_dims=2,
88
+ in_channels=hidden_dim,
89
+ out_channels=dim,
90
+ kernel_size=kernel_size,
91
+ stride=stride,
92
+ padding=padding,
93
+ use_bias=True,
94
+ key=key_conv2,
95
+ )
96
+ self.norm2 = eqx.nn.GroupNorm(num_groups, dim)
97
+
98
+ dpr = drop_path[0] if isinstance(drop_path, list) else float(drop_path)
99
+ self.drop_path1 = DropPathAdd(dpr)
100
+
101
+ self.ls1 = (
102
+ LayerScale(dim, init_values=init_values)
103
+ if init_values
104
+ else eqx.nn.Identity()
105
+ )
106
+
107
+ def permute(
108
+ self, x: Float[Array, "channels height width"]
109
+ ) -> Float[Array, "height width channels"]:
110
+ return rearrange(x, "c h w -> h w c")
111
+
112
+ def depermute(
113
+ self,
114
+ x: Float[Array, "height width channels"],
115
+ ) -> Float[Array, "channels height width"]:
116
+ return rearrange(x, "h w c -> c h w")
117
+
118
+ def __call__(
119
+ self,
120
+ x: Float[Array, "channels height width"],
121
+ enable_dropout: bool,
122
+ key: PRNGKeyArray,
123
+ ) -> Float[Array, "channels height width"]:
124
+ _, h, w = x.shape
125
+ x2 = self.act(self.norm1(self.conv1(x)))
126
+ x2 = self.norm2(self.conv2(x2))
127
+ x2 = self.depermute(jax.vmap(jax.vmap(self.ls1))(self.permute(x2)))
128
+
129
+ return self.drop_path1(x, x2, inference=not enable_dropout, key=key)
130
+
131
+
132
+ class SingleConvBlock(eqx.Module):
133
+ """A basic convolution block combining convolution, normalization and activation.
134
+
135
+ This block provides a streamlined combination of convolution, optional group
136
+ normalization, and optional activation in a single unit. It's designed to be
137
+ a fundamental building block for larger architectures.
138
+
139
+ Attributes:
140
+ conv: Convolution layer
141
+ norm: Normalization layer (GroupNorm or Identity)
142
+ act: Activation layer (Lambda or Identity)
143
+ """
144
+
145
+ conv: eqx.nn.Conv
146
+ norm: eqx.Module
147
+ act: eqx.Module
148
+
149
+ def __init__(
150
+ self,
151
+ in_channels: int,
152
+ out_channels: int,
153
+ *,
154
+ key: PRNGKeyArray,
155
+ use_norm: bool = True,
156
+ norm_max_group: int = 32,
157
+ act_layer: Callable | None = None,
158
+ **kwargs,
159
+ ):
160
+ """Initialize the SingleConvBlock.
161
+
162
+ Args:
163
+ in_channels: Number of input channels
164
+ out_channels: Number of output channels
165
+ key: PRNG key for initialization
166
+ use_norm: Whether to use group normalization (default: True)
167
+ norm_max_group: Maximum number of groups for GroupNorm (default: 32)
168
+ act_layer: Optional activation function (default: None)
169
+ **kwargs: Additional arguments passed to Conv layer
170
+ """
171
+
172
+ num_groups = nearest_power_of_2_divisor(out_channels, norm_max_group)
173
+ self.conv = eqx.nn.Conv(
174
+ num_spatial_dims=2,
175
+ in_channels=in_channels,
176
+ out_channels=out_channels,
177
+ key=key,
178
+ **kwargs,
179
+ )
180
+ self.norm = (
181
+ eqx.nn.GroupNorm(num_groups, out_channels)
182
+ if use_norm
183
+ else eqx.nn.Identity()
184
+ )
185
+ self.act = eqx.nn.Lambda(act_layer) if act_layer else eqx.nn.Identity()
186
+
187
+ def __call__(
188
+ self,
189
+ x: Float[Array, "channels height width"],
190
+ *,
191
+ key: Optional[PRNGKeyArray] = None,
192
+ ) -> Float[Array, "dim height width"]:
193
+ return self.act(self.norm(self.conv(x)))
194
+
195
+
196
+ class Stem(eqx.Module):
197
+ """Image-to-embedding stem network for vision transformers.
198
+
199
+ This module processes raw input images into patch embeddings through a series
200
+ of convolutional stages. It includes three main components:
201
+ 1. Initial downsampling with conv + norm + activation
202
+ 2. Residual block with two convolutions
203
+ 3. Final downsampling and channel projection
204
+
205
+ The output is reshaped into a sequence of patch embeddings suitable for
206
+ transformer processing.
207
+
208
+ Attributes:
209
+ num_patches: Total number of patches (static)
210
+ patches_resolution: Spatial resolution of patches (static)
211
+ conv1: Initial convolution block
212
+ conv2: Middle residual convolution blocks
213
+ conv3: Final convolution blocks
214
+ """
215
+
216
+ num_patches: int = eqx.field(static=True)
217
+ patches_resolution: int = eqx.field(static=True)
218
+
219
+ conv1: eqx.nn.Conv
220
+ conv2: eqx.nn.Conv
221
+ conv3: eqx.nn.Conv
222
+
223
+ def __init__(
224
+ self,
225
+ in_channels: int,
226
+ *,
227
+ key: PRNGKeyArray,
228
+ img_size: int = 224,
229
+ patch_size: int = 4,
230
+ embed_dim=96,
231
+ **kwargs,
232
+ ):
233
+ """Initialize the Stem network.
234
+
235
+ Args:
236
+ in_channels: Number of input image channels
237
+ key: PRNG key for initialization
238
+ img_size: Input image size (default: 224)
239
+ patch_size: Size of each patch (default: 4)
240
+ embed_dim: Final embedding dimension (default: 96)
241
+ **kwargs: Additional arguments passed to convolution blocks
242
+ """
243
+ self.num_patches = (img_size // patch_size) ** 2
244
+ self.patches_resolution = [img_size // patch_size] * 2
245
+ (
246
+ key_conv1,
247
+ key_conv2,
248
+ key_conv3,
249
+ key_conv4,
250
+ key_conv5,
251
+ ) = jr.split(key, 5)
252
+
253
+ self.conv1 = SingleConvBlock(
254
+ in_channels=in_channels,
255
+ out_channels=embed_dim // 2,
256
+ kernel_size=3,
257
+ stride=2,
258
+ padding=1,
259
+ use_bias=False,
260
+ use_norm=True,
261
+ act_layer=jax.nn.relu,
262
+ key=key_conv1,
263
+ )
264
+
265
+ self.conv2 = eqx.nn.Sequential(
266
+ [
267
+ SingleConvBlock(
268
+ in_channels=embed_dim // 2,
269
+ out_channels=embed_dim // 2,
270
+ kernel_size=3,
271
+ stride=1,
272
+ padding=1,
273
+ use_bias=False,
274
+ use_norm=True,
275
+ act_layer=jax.nn.relu,
276
+ key=key_conv2,
277
+ ),
278
+ SingleConvBlock(
279
+ in_channels=embed_dim // 2,
280
+ out_channels=embed_dim // 2,
281
+ kernel_size=3,
282
+ stride=1,
283
+ padding=1,
284
+ use_bias=False,
285
+ use_norm=True,
286
+ act_layer=None,
287
+ key=key_conv3,
288
+ ),
289
+ ]
290
+ )
291
+
292
+ self.conv3 = eqx.nn.Sequential(
293
+ [
294
+ SingleConvBlock(
295
+ in_channels=embed_dim // 2,
296
+ out_channels=embed_dim * 4,
297
+ kernel_size=3,
298
+ stride=2,
299
+ padding=1,
300
+ use_bias=False,
301
+ use_norm=True,
302
+ act_layer=jax.nn.relu,
303
+ key=key_conv4,
304
+ ),
305
+ SingleConvBlock(
306
+ in_channels=embed_dim * 4,
307
+ out_channels=embed_dim,
308
+ kernel_size=1,
309
+ stride=1,
310
+ padding=0,
311
+ use_bias=False,
312
+ use_norm=True,
313
+ act_layer=None,
314
+ key=key_conv5,
315
+ ),
316
+ ]
317
+ )
318
+
319
+ def __call__(
320
+ self,
321
+ x: Float[Array, "channels height width"],
322
+ *,
323
+ key: Optional[PRNGKeyArray] = None,
324
+ ) -> Float[Array, "seqlen dim"]:
325
+ x = self.conv1(x)
326
+ x = self.conv2(x) + x
327
+ x = self.conv3(x)
328
+
329
+ return rearrange(x, "c h w -> (h w) c")
@@ -0,0 +1,58 @@
1
+ import equinox as eqx
2
+ from jaxtyping import Array, Float, PRNGKeyArray
3
+
4
+
5
+ class Downsampler(eqx.Module):
6
+ """A module that performs spatial downsampling using strided convolution.
7
+
8
+ This module reduces spatial dimensions (height and width) by a factor of 2
9
+ while optionally increasing the channel dimension. Uses a 3x3 strided
10
+ convolution for downsampling.
11
+
12
+ Attributes:
13
+ reduction: Convolutional layer that performs the downsampling
14
+ """
15
+
16
+ reduction: eqx.Module
17
+
18
+ def __init__(
19
+ self,
20
+ dim: int,
21
+ *,
22
+ key=PRNGKeyArray,
23
+ keep_dim: bool = False,
24
+ ):
25
+ """Initialize the Downsampler.
26
+
27
+ Args:
28
+ dim: Number of input channels
29
+ key: PRNG key for initialization
30
+ keep_dim: If True, maintains channel dimension; if False, doubles it
31
+ (default: False)
32
+ """
33
+ dim_out = dim if keep_dim else 2 * dim
34
+ self.reduction = eqx.nn.Conv(
35
+ num_spatial_dims=2,
36
+ in_channels=dim,
37
+ out_channels=dim_out,
38
+ kernel_size=(3, 3),
39
+ stride=(2, 2),
40
+ padding=(1, 1),
41
+ use_bias=False,
42
+ key=key,
43
+ )
44
+
45
+ def __call__(
46
+ self,
47
+ x: Float[Array, "channels height width"],
48
+ ) -> Float[Array, "new_channels new_height new_width"]:
49
+ """Apply downsampling to the input tensor.
50
+
51
+ Args:
52
+ x: Input tensor of shape (channels, height, width)
53
+
54
+ Returns:
55
+ Downsampled tensor with halved spatial dimensions and potentially
56
+ doubled channels depending on keep_dim parameter
57
+ """
58
+ return self.reduction(x)
@@ -0,0 +1,217 @@
1
+ import warnings
2
+ from typing import Optional
3
+
4
+ import equinox as eqx
5
+ import jax
6
+ import jax.lax as lax
7
+ import jax.numpy as jnp
8
+ import jax.random as jrandom
9
+ from jaxtyping import Array, PRNGKeyArray
10
+
11
+
12
+ class Dropout(eqx.Module, strict=True):
13
+ """Applies dropout.
14
+
15
+ Note that this layer behaves differently during training and inference. During
16
+ training then dropout is randomly applied; during inference this layer does nothing.
17
+ """
18
+
19
+ # Let's make them static fields, just to avoid possible filtering issues
20
+ p: float = eqx.field(static=True)
21
+ inference: bool = eqx.field(static=True)
22
+
23
+ def __init__(
24
+ self,
25
+ p: float = 0.5,
26
+ inference: bool = False,
27
+ *,
28
+ deterministic: Optional[bool] = None,
29
+ ):
30
+ """**Arguments:**
31
+
32
+ - `p`: The fraction of entries to set to zero. (On average.)
33
+ - `inference`: Whether to actually apply dropout at all. If `True` then dropout
34
+ is *not* applied. If `False` then dropout is applied. This may be toggled
35
+ with [`equinox.nn.inference_mode`][] or overridden during
36
+ [`equinox.nn.Dropout.__call__`][].
37
+ - `deterministic`: Deprecated alternative to `inference`.
38
+ """
39
+
40
+ if deterministic is not None:
41
+ inference = deterministic
42
+ warnings.warn(
43
+ "Dropout(deterministic=...) is deprecated "
44
+ "in favour of Dropout(inference=...)"
45
+ )
46
+ self.p = p
47
+ self.inference = inference
48
+
49
+ # Backward compatibility
50
+ @property
51
+ def deterministic(self):
52
+ return self.inference
53
+
54
+ @jax.named_scope("eqx.nn.Dropout")
55
+ def __call__(
56
+ self,
57
+ x: Array,
58
+ *,
59
+ key: Optional[PRNGKeyArray] = None,
60
+ inference: Optional[bool] = None,
61
+ deterministic: Optional[bool] = None,
62
+ ) -> Array:
63
+ """**Arguments:**
64
+
65
+ - `x`: An any-dimensional JAX array to dropout.
66
+ - `key`: A `jax.random.PRNGKey` used to provide randomness for calculating
67
+ which elements to dropout. (Keyword only argument.)
68
+ - `inference`: As per [`equinox.nn.Dropout.__init__`][]. If `True` or
69
+ `False` then it will take priority over `self.inference`. If `None`
70
+ then the value from `self.inference` will be used.
71
+ - `deterministic`: Deprecated alternative to `inference`.
72
+ """
73
+
74
+ if deterministic is not None:
75
+ inference = deterministic
76
+ warnings.warn(
77
+ "Dropout()(deterministic=...) is deprecated "
78
+ "in favour of Dropout()(inference=...)"
79
+ )
80
+
81
+ if inference is None:
82
+ inference = self.inference
83
+ if isinstance(self.p, (int, float)) and self.p == 0:
84
+ inference = True
85
+ if inference:
86
+ return x
87
+ elif key is None:
88
+ raise RuntimeError(
89
+ "Dropout requires a key when running in non-deterministic mode."
90
+ )
91
+ else:
92
+ q = 1 - lax.stop_gradient(self.p)
93
+ mask = jrandom.bernoulli(key, q, x.shape)
94
+ return jnp.where(mask, x / q, 0)
95
+
96
+
97
+ class DropPath(eqx.Module, strict=True):
98
+ """Applies drop path (stochastic depth).
99
+
100
+ Note that this layer behaves differently during training and inference. During
101
+ training then dropout is randomly applied; during inference this layer does nothing.
102
+ """
103
+
104
+ # Let's make them static fields, just to avoid possible filtering issues
105
+ p: float = eqx.field(static=True)
106
+ inference: bool = eqx.field(static=True)
107
+
108
+ def __init__(
109
+ self,
110
+ p: float = 0.5,
111
+ inference: bool = False,
112
+ ):
113
+ """**Arguments:**
114
+
115
+ - `p`: The fraction of entries to set to zero. (On average.)
116
+ - `inference`: Whether to actually apply dropout at all. If `True` then dropout
117
+ is *not* applied. If `False` then dropout is applied. This may be toggled
118
+ with overridden during [`DropPath.__call__`][].
119
+ """
120
+
121
+ self.p = p
122
+ self.inference = inference
123
+
124
+ def __call__(
125
+ self,
126
+ x: Array,
127
+ *,
128
+ key: Optional[PRNGKeyArray] = None,
129
+ inference: Optional[bool] = None,
130
+ ) -> Array:
131
+ """**Arguments:**
132
+
133
+ - `x`: An any-dimensional JAX array to dropout.
134
+ - `key`: A `jax.random.PRNGKey` used to provide randomness for calculating
135
+ which elements to dropout. (Keyword only argument.)
136
+ - `inference`: As per [`DropPath.__init__`][]. If `True` or
137
+ `False` then it will take priority over `self.inference`. If `None`
138
+ then the value from `self.inference` will be used.
139
+ """
140
+
141
+ if inference is None:
142
+ inference = self.inference
143
+ if isinstance(self.p, (int, float)) and self.p == 0:
144
+ inference = True
145
+ if inference:
146
+ return x
147
+ elif key is None:
148
+ raise RuntimeError(
149
+ "DropPath requires a key when running in non-deterministic mode."
150
+ )
151
+ else:
152
+ q = 1 - lax.stop_gradient(self.p)
153
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1)
154
+ mask = jrandom.bernoulli(key, q, shape)
155
+ return x * mask / q
156
+
157
+
158
+ class DropPathAdd(eqx.Module, strict=True):
159
+ """Applies drop path (stochastic depth), by adding the second input to the first.
160
+
161
+ Note that this layer behaves differently during training and inference. During
162
+ training then dropout is randomly applied; during inference this layer does nothing.
163
+ """
164
+
165
+ # Let's make them static fields, just to avoid possible filtering issues
166
+ p: float = eqx.field(static=True)
167
+ inference: bool = eqx.field(static=True)
168
+
169
+ def __init__(
170
+ self,
171
+ p: float = 0.5,
172
+ inference: bool = False,
173
+ ):
174
+ """**Arguments:**
175
+
176
+ - `p`: The fraction of entries to set to zero. (On average.)
177
+ - `inference`: Whether to actually apply dropout at all. If `True` then dropout
178
+ is *not* applied. If `False` then dropout is applied. This may be toggled
179
+ with overridden during [`DropPath.__call__`][].
180
+ """
181
+
182
+ self.p = p
183
+ self.inference = inference
184
+
185
+ def __call__(
186
+ self,
187
+ x1: Array,
188
+ x2: Array,
189
+ *,
190
+ key: Optional[PRNGKeyArray] = None,
191
+ inference: Optional[bool] = None,
192
+ ) -> Array:
193
+ """**Arguments:**
194
+
195
+ - `x1`: An any-dimensional JAX array.
196
+ - `x2`: A x1-dimensional JAX array to stochastically add to x1.
197
+ - `key`: A `jax.random.PRNGKey` used to provide randomness for calculating
198
+ which elements to dropout. (Keyword only argument.)
199
+ - `inference`: As per [`DropPath.__init__`][]. If `True` or
200
+ `False` then it will take priority over `self.inference`. If `None`
201
+ then the value from `self.inference` will be used.
202
+ """
203
+
204
+ if inference is None:
205
+ inference = self.inference
206
+ if isinstance(self.p, (int, float)) and self.p == 0:
207
+ inference = True
208
+ if inference:
209
+ return x1 + x2
210
+ elif key is None:
211
+ raise RuntimeError(
212
+ "DropPathAdd requires a key when running in non-deterministic mode."
213
+ )
214
+ else:
215
+ q = 1 - lax.stop_gradient(self.p)
216
+ add = jrandom.bernouilli(key, q)
217
+ return jax.lax.cond(add, lambda x, y: x + y, lambda x, y: x, x1, x2)