diff --git a/src/safeds/ml/nn/layers/_flatten_layer.py b/src/safeds/ml/nn/layers/_flatten_layer.py index a84551c2b..35e11c2bb 100644 --- a/src/safeds/ml/nn/layers/_flatten_layer.py +++ b/src/safeds/ml/nn/layers/_flatten_layer.py @@ -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 @@ -46,14 +46,14 @@ def input_size(self) -> ModelImageSize: return self._input_size @property - def output_size(self) -> int: + def output_size(self) -> TensorShape: """ Get the output_size of this layer. Returns ------- result: - The number of neurons in this layer. + A 1D TensorShape object containing the number of neurons in this layer. Raises ------ @@ -66,9 +66,9 @@ def output_size(self) -> int: ) if self._output_size is None: self._output_size = self._input_size.width * self._input_size.height * self._input_size.channel - return self._output_size + return TensorShape([self._output_size]) - def _set_input_size(self, input_size: int | ModelImageSize) -> None: + def _set_input_size(self, input_size: int | TensorShape) -> None: if isinstance(input_size, int): raise TypeError("The input_size of a flatten layer has to be of type ImageSize.") if not isinstance(input_size, ConstantImageSize): diff --git a/src/safeds/ml/nn/layers/_forward_layer.py b/src/safeds/ml/nn/layers/_forward_layer.py index e420b78ec..779796fd7 100644 --- a/src/safeds/ml/nn/layers/_forward_layer.py +++ b/src/safeds/ml/nn/layers/_forward_layer.py @@ -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 TensorShape from ._layer import Layer @@ -50,34 +50,34 @@ def _get_internal_layer(self, **kwargs: Any) -> nn.Module: return _InternalForwardLayer(self._input_size, self._output_size, activation_function) @property - def input_size(self) -> int: + def input_size(self) -> TensorShape: """ Get the input_size of this layer. Returns ------- result: - The amount of values being passed into this layer. + A 1D TensorShape object containing the amount of values being passed into this layer. """ if self._input_size is None: raise ValueError("The input_size is not yet set.") - return self._input_size + return TensorShape([self._input_size]) @property - def output_size(self) -> int: + def output_size(self) -> TensorShape: """ Get the output_size of this layer. Returns ------- result: - The number of neurons in this layer. + A 1D TensorShape object containing the number of neurons in this layer. """ - return self._output_size + return TensorShape([self._output_size]) - def _set_input_size(self, input_size: int | ModelImageSize) -> None: - if isinstance(input_size, ModelImageSize): + def _set_input_size(self, input_size: int | TensorShape) -> None: + if isinstance(input_size, TensorShape): raise TypeError("The input_size of a forward layer has to be of type int.") self._input_size = input_size diff --git a/src/safeds/ml/nn/layers/_gru_layer.py b/src/safeds/ml/nn/layers/_gru_layer.py index e74fec417..044200729 100644 --- a/src/safeds/ml/nn/layers/_gru_layer.py +++ b/src/safeds/ml/nn/layers/_gru_layer.py @@ -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 TensorShape from ._layer import Layer @@ -51,33 +51,33 @@ def _get_internal_layer(self, **kwargs: Any) -> nn.Module: return _InternalGRULayer(self._input_size, self._output_size, activation_function) @property - def input_size(self) -> int: + def input_size(self) -> TensorShape: """ Get the input_size of this layer. Returns ------- result: - The amount of values being passed into this layer. + A 1D TensorShape object containing the amount of values being passed into this layer. """ if self._input_size is None: raise ValueError("The input_size is not yet set.") - return self._input_size + return TensorShape([self._input_size]) @property - def output_size(self) -> int: + def output_size(self) -> TensorShape: """ Get the output_size of this layer. Returns ------- result: - The number of neurons in this layer. + A 1D TensorShape object containing the number of neurons in this layer. """ - return self._output_size + return TensorShape([self._output_size]) - def _set_input_size(self, input_size: int | ModelImageSize) -> None: - if isinstance(input_size, ModelImageSize): + def _set_input_size(self, input_size: int | TensorShape) -> None: + if isinstance(input_size, TensorShape): raise TypeError("The input_size of a forward layer has to be of type int.") self._input_size = input_size diff --git a/src/safeds/ml/nn/layers/_layer.py b/src/safeds/ml/nn/layers/_layer.py index 058036688..63edfec5d 100644 --- a/src/safeds/ml/nn/layers/_layer.py +++ b/src/safeds/ml/nn/layers/_layer.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from torch import nn - from safeds.ml.nn.typing import ModelImageSize + from safeds.ml.nn.typing import TensorShape class Layer(ABC): @@ -20,16 +20,16 @@ def _get_internal_layer(self, **kwargs: Any) -> nn.Module: @property @abstractmethod - def input_size(self) -> int | ModelImageSize: + def input_size(self) -> TensorShape: pass # pragma: no cover @property @abstractmethod - def output_size(self) -> int | ModelImageSize: + def output_size(self) -> TensorShape: pass # pragma: no cover @abstractmethod - def _set_input_size(self, input_size: int | ModelImageSize) -> None: + def _set_input_size(self, input_size: TensorShape) -> None: pass # pragma: no cover @abstractmethod diff --git a/src/safeds/ml/nn/layers/_lstm_layer.py b/src/safeds/ml/nn/layers/_lstm_layer.py index 330809474..f02e27242 100644 --- a/src/safeds/ml/nn/layers/_lstm_layer.py +++ b/src/safeds/ml/nn/layers/_lstm_layer.py @@ -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 TensorShape from ._layer import Layer @@ -51,33 +51,33 @@ def _get_internal_layer(self, **kwargs: Any) -> nn.Module: return _InternalLSTMLayer(self._input_size, self._output_size, activation_function) @property - def input_size(self) -> int: + def input_size(self) -> TensorShape: """ Get the input_size of this layer. Returns ------- result: - The amount of values being passed into this layer. + A 1D TensorShape object containing the amount of values being passed into this layer. """ if self._input_size is None: raise ValueError("The input_size is not yet set.") - return self._input_size + return TensorShape([self._input_size]) @property - def output_size(self) -> int: + def output_size(self) -> TensorShape: """ Get the output_size of this layer. Returns ------- result: - The number of neurons in this layer. + A 1D TensorShape object containing the number of neurons in this layer. """ - return self._output_size + return TensorShape([self._output_size]) - def _set_input_size(self, input_size: int | ModelImageSize) -> None: - if isinstance(input_size, ModelImageSize): + def _set_input_size(self, input_size: int | TensorShape) -> None: + if isinstance(input_size, TensorShape): raise TypeError("The input_size of a forward layer has to be of type int.") self._input_size = input_size diff --git a/src/safeds/ml/nn/typing/__init__.py b/src/safeds/ml/nn/typing/__init__.py index 913ad6f65..08ea81601 100644 --- a/src/safeds/ml/nn/typing/__init__.py +++ b/src/safeds/ml/nn/typing/__init__.py @@ -6,12 +6,14 @@ 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", + "TensorShape": "._tensor_shape:TensorShape", "VariableImageSize": "._model_image_size:VariableImageSize", }, ) @@ -19,5 +21,6 @@ __all__ = [ "ConstantImageSize", "ModelImageSize", + "TensorShape", "VariableImageSize", ] diff --git a/src/safeds/ml/nn/typing/_model_image_size.py b/src/safeds/ml/nn/typing/_model_image_size.py index b28ea5ed1..4157317fd 100644 --- a/src/safeds/ml/nn/typing/_model_image_size.py +++ b/src/safeds/ml/nn/typing/_model_image_size.py @@ -1,17 +1,18 @@ from __future__ import annotations import sys -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import TYPE_CHECKING, Self from safeds._utils import _structural_hash from safeds._validation import _check_bounds, _ClosedBound +from safeds.ml.nn.typing._tensor_shape import TensorShape if TYPE_CHECKING: from safeds.data.image.containers import Image -class ModelImageSize(ABC): +class ModelImageSize(TensorShape): """ A container for image size in neural networks. @@ -38,6 +39,7 @@ def __init__(self, width: int, height: int, channel: int, *, _ignore_invalid_cha if not _ignore_invalid_channel and channel not in (1, 3, 4): raise ValueError(f"Channel {channel} is not a valid channel option. Use either 1, 3 or 4") _check_bounds("channel", channel, lower_bound=_ClosedBound(1)) + super().__init__(dims=[width, height, channel]) self._width = width self._height = height diff --git a/src/safeds/ml/nn/typing/_tensor_shape.py b/src/safeds/ml/nn/typing/_tensor_shape.py new file mode 100644 index 000000000..4ee867b1f --- /dev/null +++ b/src/safeds/ml/nn/typing/_tensor_shape.py @@ -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 | list[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: + # TODO maybe add error message indicating that the dimension is out of range + return 0 + if dimension is None: + return self._dims + return self._dims[dimension] + + def __hash__(self) -> int: + return _structural_hash(self._dims) + + @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) diff --git a/tests/safeds/ml/nn/layers/test_flatten_layer.py b/tests/safeds/ml/nn/layers/test_flatten_layer.py index 64db6127c..e30cef24b 100644 --- a/tests/safeds/ml/nn/layers/test_flatten_layer.py +++ b/tests/safeds/ml/nn/layers/test_flatten_layer.py @@ -14,7 +14,7 @@ def test_should_create_flatten_layer(self) -> None: input_size = ImageSize(10, 20, 30, _ignore_invalid_channel=True) layer._set_input_size(input_size) assert layer.input_size == input_size - assert layer.output_size == input_size.width * input_size.height * input_size.channel + assert layer.output_size.get_size(dimension=0) == input_size.width * input_size.height * input_size.channel assert isinstance(next(next(layer._get_internal_layer().modules()).children()), nn.Flatten) def test_should_raise_if_input_size_not_set(self) -> None: diff --git a/tests/safeds/ml/nn/layers/test_forward_layer.py b/tests/safeds/ml/nn/layers/test_forward_layer.py index 0ecd3bd05..06f37e7c1 100644 --- a/tests/safeds/ml/nn/layers/test_forward_layer.py +++ b/tests/safeds/ml/nn/layers/test_forward_layer.py @@ -83,7 +83,8 @@ def test_should_raise_if_output_size_out_of_bounds(output_size: int) -> None: ids=["one", "twenty"], ) def test_should_return_output_size(output_size: int) -> None: - assert ForwardLayer(neuron_count=output_size).output_size == output_size + r = ForwardLayer(neuron_count=output_size).output_size.get_size(dimension=0) + assert r == output_size def test_should_raise_if_input_size_is_set_with_image_size() -> None: diff --git a/tests/safeds/ml/nn/layers/test_gru_layer.py b/tests/safeds/ml/nn/layers/test_gru_layer.py index 4a6f366e4..b5315c4b2 100644 --- a/tests/safeds/ml/nn/layers/test_gru_layer.py +++ b/tests/safeds/ml/nn/layers/test_gru_layer.py @@ -71,7 +71,7 @@ def test_should_raise_if_output_size_out_of_bounds(output_size: int) -> None: ids=["one", "twenty"], ) def test_should_raise_if_output_size_doesnt_match(output_size: int) -> None: - assert GRULayer(neuron_count=output_size).output_size == output_size + assert GRULayer(neuron_count=output_size).output_size.get_size(dimension=0) == output_size def test_should_raise_if_input_size_is_set_with_image_size() -> None: @@ -170,7 +170,7 @@ def test_should_assert_that_layer_size_is_greater_than_normal_object(layer: GRUL def test_set_input_size() -> None: layer = GRULayer(1) layer._set_input_size(3) - assert layer.input_size == 3 + assert layer.input_size.get_size(dimension=0) == 3 def test_input_size_should_raise_error() -> None: diff --git a/tests/safeds/ml/nn/layers/test_lstm_layer.py b/tests/safeds/ml/nn/layers/test_lstm_layer.py index 8d58e5dd8..48726e525 100644 --- a/tests/safeds/ml/nn/layers/test_lstm_layer.py +++ b/tests/safeds/ml/nn/layers/test_lstm_layer.py @@ -83,7 +83,7 @@ def test_should_raise_if_output_size_out_of_bounds(output_size: int) -> None: ids=["one", "twenty"], ) def test_should_raise_if_output_size_doesnt_match(output_size: int) -> None: - assert LSTMLayer(neuron_count=output_size).output_size == output_size + assert LSTMLayer(neuron_count=output_size).output_size.get_size(dimension=0) == output_size def test_should_raise_if_input_size_is_set_with_image_size() -> None: diff --git a/tests/safeds/ml/nn/typing/test_tensor_shape.py b/tests/safeds/ml/nn/typing/test_tensor_shape.py new file mode 100644 index 000000000..f1825d911 --- /dev/null +++ b/tests/safeds/ml/nn/typing/test_tensor_shape.py @@ -0,0 +1,43 @@ +import pytest +from safeds.exceptions import OutOfBoundsError +from safeds.ml.nn.typing import TensorShape + + +def test_dims() -> None: + dims = [2, 3, 4] + ts = TensorShape(dims) + assert ts._dims == dims + + +def test_dimensionality() -> None: + dims = [2, 3, 4] + ts = TensorShape(dims) + assert ts.dimensionality == 3 + + +def test_get_size_no_dimension() -> None: + dims = [2, 3, 4] + ts = TensorShape(dims) + assert ts.get_size() == dims + + +def test_get_size_specific_dimension() -> None: + dims = [2, 3, 4] + ts = TensorShape(dims) + assert ts.get_size(0) == 2 + assert ts.get_size(1) == 3 + assert ts.get_size(2) == 4 + + +def test_get_size_out_of_bounds_dimension() -> None: + dims = [2, 3, 4] + ts = TensorShape(dims) + with pytest.raises(OutOfBoundsError): + ts.get_size(-1) + + +def test_hash() -> None: + dims = [2, 3, 4] + ts1 = TensorShape(dims) + ts2 = TensorShape(dims) + assert hash(ts1) == hash(ts2)