Skip to content

Commit 58e2562

Browse files
Parametrize offset models (#265)
* Parametrize offset models * Implement copilot suggestions * Restructure deprecation functions * Add deprecation tests on offsets models * Fix codespell * Tentative fix to parametrized class TypeError --------- Co-authored-by: Mackenzie Weygandt Mathis <mathis@rowland.harvard.edu>
1 parent b1e78c2 commit 58e2562

9 files changed

Lines changed: 476 additions & 206 deletions

File tree

‎cebra/models/model.py‎

Lines changed: 34 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -260,59 +260,61 @@ def num_trainable_parameters(self) -> int:
260260
param.numel() for param in self.parameters() if param.requires_grad)
261261

262262

263-
@register("offset10-model")
264-
class Offset10Model(_OffsetModel, ConvolutionalModelMixin):
265-
"""CEBRA model with a 10 sample receptive field."""
263+
@parametrize("offset{n_offset}-model",
264+
n_offset=(5, 10, 15, 18, 20, 31, 36, 40, 50))
265+
class OffsetNModel(_OffsetModel, ConvolutionalModelMixin):
266+
"""CEBRA model with a `n_offset` sample receptive field.
266267
267-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
268+
n_offset: The size of the receptive field.
269+
"""
270+
271+
def __init__(self,
272+
num_neurons,
273+
num_units,
274+
num_output,
275+
n_offset,
276+
normalize=True):
268277
if num_units < 1:
269278
raise ValueError(
270279
f"Hidden dimension needs to be at least 1, but got {num_units}."
271280
)
281+
282+
self.n_offset = n_offset
283+
284+
def _compute_num_layers():
285+
"""Compute the number of layers to add on top of the first and last conv layers."""
286+
return (self.n_offset - 4) // 2 + self.n_offset % 2
287+
288+
last_layer_kernel = 3 if (self.n_offset % 2) == 0 else 2
272289
super().__init__(
273290
nn.Conv1d(num_neurons, num_units, 2),
274291
nn.GELU(),
275-
*self._make_layers(num_units, num_layers=3),
276-
nn.Conv1d(num_units, num_output, 3),
292+
*self._make_layers(num_units, num_layers=_compute_num_layers()),
293+
nn.Conv1d(num_units, num_output, last_layer_kernel),
277294
num_input=num_neurons,
278295
num_output=num_output,
279296
normalize=normalize,
280297
)
281298

282299
def get_offset(self) -> cebra.data.datatypes.Offset:
283300
"""See :py:meth:`~.Model.get_offset`"""
284-
return cebra.data.Offset(5, 5)
301+
return cebra.data.Offset(self.n_offset // 2,
302+
self.n_offset // 2 + self.n_offset % 2)
285303

286304

287305
@register("offset10-model-mse")
288-
class Offset10ModelMSE(Offset10Model):
306+
class Offset10ModelMSE(OffsetNModel):
289307
"""Symmetric model with 10 sample receptive field, without normalization.
290308
291309
Suitable for use with InfoNCE metrics for Euclidean space.
292310
"""
293311

294312
def __init__(self, num_neurons, num_units, num_output, normalize=False):
295-
super().__init__(num_neurons, num_units, num_output, normalize)
296-
297-
298-
@register("offset5-model")
299-
class Offset5Model(_OffsetModel, ConvolutionalModelMixin):
300-
"""CEBRA model with a 5 sample receptive field and output normalization."""
301-
302-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
303-
super().__init__(
304-
nn.Conv1d(num_neurons, num_units, 2),
305-
nn.GELU(),
306-
cebra_layers._Skip(nn.Conv1d(num_units, num_units, 3), nn.GELU()),
307-
nn.Conv1d(num_units, num_output, 2),
308-
num_input=num_neurons,
309-
num_output=num_output,
310-
normalize=normalize,
311-
)
312-
313-
def get_offset(self) -> cebra.data.datatypes.Offset:
314-
"""See :py:meth:`~.Model.get_offset`"""
315-
return cebra.data.Offset(2, 3)
313+
super().__init__(num_neurons,
314+
num_units,
315+
num_output,
316+
n_offset=10,
317+
normalize=normalize)
316318

317319

318320
@register("offset1-model-mse")
@@ -666,30 +668,6 @@ def get_offset(self) -> cebra.data.datatypes.Offset:
666668
return cebra.data.Offset(0, 1)
667669

668670

669-
@register("offset36-model")
670-
class Offset36(_OffsetModel, ConvolutionalModelMixin):
671-
"""CEBRA model with a 10 sample receptive field."""
672-
673-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
674-
if num_units < 1:
675-
raise ValueError(
676-
f"Hidden dimension needs to be at least 1, but got {num_units}."
677-
)
678-
super().__init__(
679-
nn.Conv1d(num_neurons, num_units, 2),
680-
nn.GELU(),
681-
*self._make_layers(num_units, num_layers=16),
682-
nn.Conv1d(num_units, num_output, 3),
683-
num_input=num_neurons,
684-
num_output=num_output,
685-
normalize=normalize,
686-
)
687-
688-
def get_offset(self) -> cebra.data.datatypes.Offset:
689-
"""See `:py:meth:Model.get_offset`"""
690-
return cebra.data.Offset(18, 18)
691-
692-
693671
@_register_conditionally("offset36-model-dropout")
694672
class Offset36Dropout(_OffsetModel, ConvolutionalModelMixin):
695673
"""CEBRA model with a 10 sample receptive field.
@@ -721,7 +699,7 @@ def __init__(self,
721699
)
722700

723701
def get_offset(self) -> cebra.data.datatypes.Offset:
724-
"""See `:py:meth:Model.get_offset`"""
702+
"""See :py:meth:`~.Model.get_offset`"""
725703
return cebra.data.Offset(18, 18)
726704

727705

@@ -763,108 +741,12 @@ def __init__(self,
763741
)
764742

765743
def get_offset(self) -> cebra.data.datatypes.Offset:
766-
"""See `:py:meth:Model.get_offset`"""
744+
"""See :py:meth:`~.Model.get_offset`"""
767745
return cebra.data.Offset(18, 18)
768746

769747

770-
@register("offset40-model")
771-
class Offset40(_OffsetModel, ConvolutionalModelMixin):
772-
"""CEBRA model with a 40 samples receptive field."""
773-
774-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
775-
if num_units < 1:
776-
raise ValueError(
777-
f"Hidden dimension needs to be at least 1, but got {num_units}."
778-
)
779-
super().__init__(
780-
nn.Conv1d(num_neurons, num_units, 2),
781-
nn.GELU(),
782-
*self._make_layers(num_units, 18),
783-
nn.Conv1d(num_units, num_output, 3),
784-
num_input=num_neurons,
785-
num_output=num_output,
786-
normalize=normalize,
787-
)
788-
789-
def get_offset(self) -> cebra.data.datatypes.Offset:
790-
"""See `:py:meth:Model.get_offset`"""
791-
return cebra.data.Offset(20, 20)
792-
793-
794-
@register("offset50-model")
795-
class Offset50(_OffsetModel, ConvolutionalModelMixin):
796-
"""CEBRA model with a sample receptive field."""
797-
798-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
799-
if num_units < 1:
800-
raise ValueError(
801-
f"Hidden dimension needs to be at least 1, but got {num_units}."
802-
)
803-
super().__init__(
804-
nn.Conv1d(num_neurons, num_units, 2),
805-
nn.GELU(),
806-
*self._make_layers(num_units, 23),
807-
nn.Conv1d(num_units, num_output, 3),
808-
num_input=num_neurons,
809-
num_output=num_output,
810-
normalize=normalize,
811-
)
812-
813-
def get_offset(self) -> cebra.data.datatypes.Offset:
814-
"""See `:py:meth:Model.get_offset`"""
815-
return cebra.data.Offset(25, 25)
816-
817-
818-
@register("offset15-model")
819-
class Offset15Model(_OffsetModel, ConvolutionalModelMixin):
820-
"""CEBRA model with a 15 sample receptive field."""
821-
822-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
823-
if num_units < 1:
824-
raise ValueError(
825-
f"Hidden dimension needs to be at least 1, but got {num_units}."
826-
)
827-
super().__init__(
828-
nn.Conv1d(num_neurons, num_units, 2),
829-
nn.GELU(),
830-
*self._make_layers(num_units, num_layers=6),
831-
nn.Conv1d(num_units, num_output, 2),
832-
num_input=num_neurons,
833-
num_output=num_output,
834-
normalize=normalize,
835-
)
836-
837-
def get_offset(self) -> cebra.data.datatypes.Offset:
838-
"""See `:py:meth:Model.get_offset`"""
839-
return cebra.data.Offset(7, 8)
840-
841-
842-
@register("offset20-model")
843-
class Offset20Model(_OffsetModel, ConvolutionalModelMixin):
844-
"""CEBRA model with a 15 sample receptive field."""
845-
846-
def __init__(self, num_neurons, num_units, num_output, normalize=True):
847-
if num_units < 1:
848-
raise ValueError(
849-
f"Hidden dimension needs to be at least 1, but got {num_units}."
850-
)
851-
super().__init__(
852-
nn.Conv1d(num_neurons, num_units, 2),
853-
nn.GELU(),
854-
*self._make_layers(num_units, num_layers=8),
855-
nn.Conv1d(num_units, num_output, 3),
856-
num_input=num_neurons,
857-
num_output=num_output,
858-
normalize=normalize,
859-
)
860-
861-
def get_offset(self) -> cebra.data.datatypes.Offset:
862-
"""See `:py:meth:Model.get_offset`"""
863-
return cebra.data.Offset(10, 10)
864-
865-
866748
@register("offset10-model-mse-tanh")
867-
class Offset10Model(_OffsetModel, ConvolutionalModelMixin):
749+
class Offset10ModelMSETanh(_OffsetModel, ConvolutionalModelMixin):
868750
"""CEBRA model with a 10 sample receptive field."""
869751

870752
def __init__(self, num_neurons, num_units, num_output, normalize=False):

‎cebra/registry.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def _register(cls):
192192

193193
def parametrize(pattern: str,
194194
*,
195-
kwargs: List[Dict[str, Any]] = [],
195+
kwargs: List[Dict[str, Any]] = None,
196196
**all_kwargs):
197197
"""Decorator to add parametrizations of a new class to the registry.
198198
@@ -221,8 +221,8 @@ def _create_class(cls, **default_kwargs):
221221
class _ParametrizedClass(cls):
222222

223223
def __init__(self, *args, **kwargs):
224-
default_kwargs.update(kwargs)
225-
super().__init__(*args, **default_kwargs)
224+
merged_kwargs = {**default_kwargs, **kwargs}
225+
super().__init__(*args, **merged_kwargs)
226226

227227
# Make the class pickleable by copying metadata from the base class
228228
# and registering it in the module namespace
@@ -239,7 +239,7 @@ def __init__(self, *args, **kwargs):
239239
setattr(parent_module, unique_name, _ParametrizedClass)
240240

241241
def _parametrize(cls):
242-
for _default_kwargs in kwargs:
242+
for _default_kwargs in (kwargs or []):
243243
_create_class(cls, **_default_kwargs)
244244
if len(all_kwargs) > 0:
245245
for _default_kwargs in _product_dict(all_kwargs):

‎docs/source/usage.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ We provide a set of pre-defined models. You can access (and search) a list of av
175175

176176
.. testoutput::
177177

178-
['offset10-model', 'offset10-model-mse', 'offset5-model', 'offset1-model-mse']
178+
['offset5-model', 'offset10-model', 'offset15-model', 'offset18-model']
179179

180180
Then, you can choose the one that fits best with your needs and provide it to the CEBRA model as the :py:attr:`~.CEBRA.model_architecture` parameter.
181181

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Reference implementations for testing consistency and backward compatibility.
2+
3+
This package contains reference implementations of previously deprecated or
4+
parametrized model components, used for testing consistency and backward compatibility
5+
in the test suite.
6+
"""
7+
8+
from .deprecated_transforms import (
9+
cebra_transform_deprecated,
10+
multiobjective_transform_deprecated,
11+
)
12+
from .reference_offset_models import (
13+
Offset5ModelReference,
14+
Offset10ModelReference,
15+
Offset15ModelReference,
16+
Offset20ModelReference,
17+
Offset36Reference,
18+
Offset40Reference,
19+
Offset50Reference,
20+
)
21+
22+
__all__ = [
23+
"cebra_transform_deprecated",
24+
"multiobjective_transform_deprecated",
25+
"Offset5ModelReference",
26+
"Offset10ModelReference",
27+
"Offset15ModelReference",
28+
"Offset20ModelReference",
29+
"Offset36Reference",
30+
"Offset40Reference",
31+
"Offset50Reference",
32+
]

tests/_utils_deprecated.py renamed to tests/_reference_implementations/deprecated_transforms.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import cebra.models
1212

1313

14-
#NOTE: Deprecated: transform is now handled in the solver but the original
15-
# method is kept here for testing.
14+
#NOTE(celia): Deprecated: transform is now handled in the solver but the original
15+
# method is kept here for testing consistency.
1616
def cebra_transform_deprecated(cebra_model,
1717
X: Union[npt.NDArray, torch.Tensor],
1818
session_id: Optional[int] = None) -> npt.NDArray:
@@ -72,9 +72,9 @@ def cebra_transform_deprecated(cebra_model,
7272
return output
7373

7474

75-
# NOTE: Deprecated: batched transform can now be performed (more memory efficient)
75+
# NOTE(celia): Deprecated: batched transform can now be performed (more memory efficient)
7676
# using the transform method of the model, and handling padding is implemented
77-
# directly in the base Solver. This method is kept for testing purposes.
77+
# directly in the base Solver. This method is kept for testing consistency.
7878
@torch.no_grad()
7979
def multiobjective_transform_deprecated(solver: "cebra.solvers.Solver",
8080
inputs: torch.Tensor) -> torch.Tensor:
@@ -90,7 +90,7 @@ def multiobjective_transform_deprecated(solver: "cebra.solvers.Solver",
9090

9191
warnings.warn(
9292
"The method is deprecated "
93-
"but kept for testing puroposes."
93+
"but kept for testing purposes."
9494
"We recommend using `transform` instead.",
9595
DeprecationWarning,
9696
stacklevel=2)

0 commit comments

Comments
 (0)