Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Get number of parameters of a layer and a nn #910

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

158 changes: 144 additions & 14 deletions docs/tutorials/regression.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/safeds/ml/nn/_internal_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from safeds._config import _init_default_device
from safeds.ml.nn.converters._input_converter_image import _InputConversionImage
from safeds.ml.nn.layers import FlattenLayer, Layer
from safeds.ml.nn.layers import DropoutLayer, FlattenLayer, Layer
from safeds.ml.nn.layers._pooling2d_layer import _Pooling2DLayer

if TYPE_CHECKING:
Expand All @@ -36,7 +36,7 @@ def __init__(self, input_conversion: InputConversion, layers: list[Layer], is_fo
layer._set_input_size(previous_output_size)
elif isinstance(input_conversion, _InputConversionImage):
layer._set_input_size(input_conversion._data_size)
if isinstance(layer, FlattenLayer | _Pooling2DLayer):
if isinstance(layer, FlattenLayer | _Pooling2DLayer | DropoutLayer):
internal_layers.append(layer._get_internal_layer())
else:
internal_layers.append(layer._get_internal_layer(activation_function="relu"))
Expand Down
32 changes: 31 additions & 1 deletion src/safeds/ml/nn/_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
ForwardLayer,
)
from safeds.ml.nn.layers._pooling2d_layer import _Pooling2DLayer
from safeds.ml.nn.typing import ConstantImageSize, ModelImageSize, VariableImageSize
from safeds.ml.nn.typing import ConstantImageSize, ModelImageSize, TensorShape, VariableImageSize

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down Expand Up @@ -108,6 +108,21 @@ def __init__(
self._total_number_of_batches_done = 0
self._total_number_of_epochs_done = 0

def get_parameter_count(self) -> int:
if self._input_size is None:
raise ValueError("The input_size is not yet set.")

summand = 0
last_type = "int" if isinstance(self.input_size, int) else "ImageSize"
last_input_neurons = self.input_size if isinstance(self.input_size, int) else 0
last_input_channels = self.input_size.channel if isinstance(self.input_size, ModelImageSize) else 0
for layer in self._layers:
layer._set_input_size(last_input_neurons if last_type == "int" else last_input_channels)
summand += layer.get_parameter_count(TensorShape([last_input_neurons, last_input_channels]))
last_input_neurons = layer.output_size if isinstance(layer.output_size, int) else 0
last_input_channels = layer.output_size.channel if isinstance(layer.output_size, ModelImageSize) else 0
return summand

@staticmethod
def load_pretrained_model(huggingface_repo: str) -> NeuralNetworkRegressor: # pragma: no cover
"""
Expand Down Expand Up @@ -387,6 +402,21 @@ def __init__(
self._total_number_of_batches_done = 0
self._total_number_of_epochs_done = 0

def get_parameter_count(self) -> int:
if self._input_size is None:
raise ValueError("The input_size is not yet set.")

summand = 0
last_type = "int" if isinstance(self.input_size, int) else "ImageSize"
last_input_neurons = self.input_size if isinstance(self.input_size, int) else 0
last_input_channels = self.input_size.channel if isinstance(self.input_size, ModelImageSize) else 0
for layer in self._layers:
layer._set_input_size(last_input_neurons if last_type == "int" else last_input_channels)
summand += layer.get_parameter_count(TensorShape([last_input_neurons, last_input_channels]))
last_input_neurons = layer.output_size if isinstance(layer.output_size, int) else 0
last_input_channels = layer.output_size.channel if isinstance(layer.output_size, ModelImageSize) else 0
return summand

@staticmethod
def load_pretrained_model(huggingface_repo: str) -> NeuralNetworkClassifier: # pragma: no cover
"""
Expand Down
8 changes: 7 additions & 1 deletion src/safeds/ml/nn/layers/_convolutional2d_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
if TYPE_CHECKING:
from torch import nn

from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape


class Convolutional2DLayer(Layer):
Expand Down Expand Up @@ -157,6 +157,9 @@ def __sizeof__(self) -> int:
+ sys.getsizeof(self._output_size)
)

def get_parameter_count(self, input_size: TensorShape) -> int:
return int((self._kernel_size * self._kernel_size * input_size._dims[1] + 1) * self._output_channel)


class ConvolutionalTranspose2DLayer(Convolutional2DLayer):
"""
Expand Down Expand Up @@ -261,3 +264,6 @@ def __eq__(self, other: object) -> bool:

def __sizeof__(self) -> int:
return sys.getsizeof(self._output_padding) + super().__sizeof__()

def get_parameter_count(self, input_size: TensorShape) -> int:
return int((self._kernel_size * self._kernel_size * input_size._dims[1] + 1) * self._output_channel)
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_dropout_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound
from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -104,3 +104,6 @@ def __sizeof__(self) -> int:
return int(self._input_size)
elif isinstance(self._input_size, ModelImageSize):
return self._input_size.__sizeof__()

def get_parameter_count(self, input_size: TensorShape) -> int: # noqa: ARG002
return 0
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_flatten_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING, Any

from safeds._utils import _structural_hash
from safeds.ml.nn.typing import ConstantImageSize
from safeds.ml.nn.typing import ConstantImageSize, TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -86,3 +86,6 @@ def __eq__(self, other: object) -> bool:

def __sizeof__(self) -> int:
return sys.getsizeof(self._input_size) + sys.getsizeof(self._output_size)

def get_parameter_count(self, input_size: TensorShape) -> int: # noqa: ARG002
return 0
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_forward_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound
from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -96,3 +96,6 @@ def __sizeof__(self) -> int:
import sys

return sys.getsizeof(self._input_size) + sys.getsizeof(self._output_size)

def get_parameter_count(self, input_size: TensorShape) -> int:
return (input_size._dims[0] + 1) * self._output_size
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_gru_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound
from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -95,3 +95,6 @@ def __eq__(self, other: object) -> bool:

def __sizeof__(self) -> int:
return sys.getsizeof(self._input_size) + sys.getsizeof(self._output_size)

def get_parameter_count(self, input_size: TensorShape) -> int:
return (input_size._dims[0] + self._output_size + 2) * self._output_size * 3
6 changes: 5 additions & 1 deletion src/safeds/ml/nn/layers/_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if TYPE_CHECKING:
from torch import nn

from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape


class Layer(ABC):
Expand Down Expand Up @@ -43,3 +43,7 @@ def __eq__(self, other: object) -> bool:
@abstractmethod
def __sizeof__(self) -> int:
pass # pragma: no cover

@abstractmethod
def get_parameter_count(self, input_size: TensorShape) -> int:
pass # pragma: no cover
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_lstm_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound
from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -95,3 +95,6 @@ def __eq__(self, other: object) -> bool:

def __sizeof__(self) -> int:
return sys.getsizeof(self._input_size) + sys.getsizeof(self._output_size)

def get_parameter_count(self, input_size: TensorShape) -> int:
return (input_size._dims[0] + self._output_size + 2) * self._output_size * 4
5 changes: 4 additions & 1 deletion src/safeds/ml/nn/layers/_pooling2d_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
if TYPE_CHECKING:
from torch import nn

from safeds.ml.nn.typing import ModelImageSize
from safeds.ml.nn.typing import ModelImageSize, TensorShape


class _Pooling2DLayer(Layer):
Expand Down Expand Up @@ -134,6 +134,9 @@ def __sizeof__(self) -> int:
+ sys.getsizeof(self._padding)
)

def get_parameter_count(self, input_size: TensorShape) -> int: # noqa: ARG002
return 0


class MaxPooling2DLayer(_Pooling2DLayer):
"""
Expand Down
3 changes: 3 additions & 0 deletions src/safeds/ml/nn/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@

if TYPE_CHECKING:
from ._model_image_size import ConstantImageSize, ModelImageSize, VariableImageSize
from ._tensor_shape import TensorShape

apipkg.initpkg(
__name__,
{
"ConstantImageSize": "._model_image_size:ConstantImageSize",
"ModelImageSize": "._model_image_size:ModelImageSize",
"VariableImageSize": "._model_image_size:VariableImageSize",
"TensorShape": "._tensor_shape:TensorShape",
},
)

__all__ = [
"ConstantImageSize",
"ModelImageSize",
"VariableImageSize",
"TensorShape",
]
59 changes: 59 additions & 0 deletions src/safeds/ml/nn/typing/_tensor_shape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound


class TensorShape:
"""
Initializes a TensorShape object with the given dimensions.

Parameters
----------
dims:
A list of integers where each integer represents
the size of the tensor in a particular dimension.
"""

def __init__(self, dims: list[int]) -> None:
self._dims = dims

def get_size(self, dimension: int | None = None) -> int:
"""
Return the size of the tensor in the specified dimension.

Parameters.
----------
dimension:
The dimension index for which the size is to be retrieved.

Returns
-------
int: The size of the tensor in the specified dimension.

Raises
------
OutOfBoundsError:
If the actual value is outside its expected range.
"""
_check_bounds("dimension", dimension, lower_bound=_ClosedBound(0))
if dimension is not None and dimension >= self.dimensionality:

Check warning on line 40 in src/safeds/ml/nn/typing/_tensor_shape.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/typing/_tensor_shape.py#L39-L40

Added lines #L39 - L40 were not covered by tests
# TODO maybe add error message indicating that the dimension is out of range
return 0
if dimension is None:
return self._dims[0]
return self._dims[dimension]

Check warning on line 45 in src/safeds/ml/nn/typing/_tensor_shape.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/typing/_tensor_shape.py#L42-L45

Added lines #L42 - L45 were not covered by tests

def __hash__(self) -> int:
return _structural_hash(self._dims)

Check warning on line 48 in src/safeds/ml/nn/typing/_tensor_shape.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/typing/_tensor_shape.py#L48

Added line #L48 was not covered by tests

@property
def dimensionality(self) -> int:
"""
Returns the number of dimensions of the tensor.

Returns
-------
int: The number of dimensions of the tensor.
"""
return len(self._dims)

Check warning on line 59 in src/safeds/ml/nn/typing/_tensor_shape.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/typing/_tensor_shape.py#L59

Added line #L59 was not covered by tests
17 changes: 17 additions & 0 deletions tests/safeds/ml/nn/layers/test_convolutional2d_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest
from safeds.data.image.typing import ImageSize
from safeds.ml.nn.layers import Convolutional2DLayer, ConvolutionalTranspose2DLayer
from safeds.ml.nn.typing import TensorShape
from torch import nn


Expand Down Expand Up @@ -157,6 +158,22 @@ def test_should_raise_if_input_size_is_set_with_int(
with pytest.raises(TypeError, match=r"The input_size of a convolution layer has to be of type ImageSize."):
layer._set_input_size(1)

def test_conv_get_parameter_count_returns_right_amount(self) -> None:
kernel_size = 5
input_channels = 3
output_channels = 3
expected_output = int((kernel_size * kernel_size * input_channels + 1) * output_channels)
layer = Convolutional2DLayer(input_channels, kernel_size)
assert layer.get_parameter_count(TensorShape([1, input_channels])) == expected_output

def test_conv_transposed_get_parameter_count_returns_right_amount(self) -> None:
kernel_size = 5
input_channels = 3
output_channels = 3
expected_output = int((kernel_size * kernel_size * input_channels + 1) * output_channels)
layer = ConvolutionalTranspose2DLayer(input_channels, kernel_size)
assert layer.get_parameter_count(TensorShape([1, input_channels])) == expected_output

class TestEq:
@pytest.mark.parametrize(
("conv2dlayer1", "conv2dlayer2"),
Expand Down
6 changes: 5 additions & 1 deletion tests/safeds/ml/nn/layers/test_dropout_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from safeds.data.tabular.containers import Table
from safeds.exceptions import OutOfBoundsError
from safeds.ml.nn.layers import DropoutLayer
from safeds.ml.nn.typing import ConstantImageSize
from safeds.ml.nn.typing import ConstantImageSize, TensorShape
from torch import nn


Expand Down Expand Up @@ -43,6 +43,10 @@ def test_input_size_should_be_set(self) -> None:
with pytest.raises(ValueError, match=r"The input_size is not yet set."):
layer.__sizeof__()

def test_get_parameter_count_right_output(self) -> None:
layer = DropoutLayer(0.5)
assert layer.get_parameter_count(TensorShape([1])) == 0


class TestEq:
def test_should_be_equal(self) -> None:
Expand Down
6 changes: 5 additions & 1 deletion tests/safeds/ml/nn/layers/test_flatten_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from safeds.data.image.typing import ImageSize
from safeds.data.tabular.containers import Table
from safeds.ml.nn.layers import FlattenLayer
from safeds.ml.nn.typing import VariableImageSize
from safeds.ml.nn.typing import TensorShape, VariableImageSize
from torch import nn


Expand Down Expand Up @@ -37,6 +37,10 @@ def test_should_raise_if_input_size_is_set_with_variable_image_size(self) -> Non
with pytest.raises(TypeError, match=r"The input_size of a flatten layer has to be a ConstantImageSize."):
layer._set_input_size(VariableImageSize(1, 2, 3))

def test_get_parameter_count_right_output(self) -> None:
layer = FlattenLayer()
assert layer.get_parameter_count(TensorShape([1])) == 0

class TestEq:
def test_should_be_equal(self) -> None:
assert FlattenLayer() == FlattenLayer()
Expand Down
9 changes: 9 additions & 0 deletions tests/safeds/ml/nn/layers/test_forward_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from safeds.data.image.typing import ImageSize
from safeds.exceptions import OutOfBoundsError
from safeds.ml.nn.layers import ForwardLayer
from safeds.ml.nn.typing import TensorShape
from torch import nn

# TODO: Should be tested on a model, not a layer, since input size gets inferred
Expand Down Expand Up @@ -177,3 +178,11 @@ def test_should_assert_that_different_forward_layers_have_different_hash(
)
def test_should_assert_that_layer_size_is_greater_than_normal_object(layer: ForwardLayer) -> None:
assert sys.getsizeof(layer) > sys.getsizeof(object())


def test_conv_transposed_get_parameter_count_returns_right_amount() -> None:
input_neurons = 3
output_neurons = 3
expected_output = int((input_neurons + 1) * output_neurons)
layer = ForwardLayer(output_neurons)
assert layer.get_parameter_count(TensorShape([input_neurons])) == expected_output
Loading