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: add TensorShape #908

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
10 changes: 5 additions & 5 deletions 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 @@ -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
------
Expand All @@ -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):
Expand Down
18 changes: 9 additions & 9 deletions 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 TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions 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 TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions 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 TensorShape


class Layer(ABC):
Expand All @@ -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
Expand Down
18 changes: 9 additions & 9 deletions 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 TensorShape

from ._layer import Layer

Expand Down Expand Up @@ -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
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",
"TensorShape": "._tensor_shape:TensorShape",
"VariableImageSize": "._model_image_size:VariableImageSize",
},
)

__all__ = [
"ConstantImageSize",
"ModelImageSize",
"TensorShape",
"VariableImageSize",
]
6 changes: 4 additions & 2 deletions src/safeds/ml/nn/typing/_model_image_size.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand Down
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 | 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)
2 changes: 1 addition & 1 deletion tests/safeds/ml/nn/layers/test_flatten_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion tests/safeds/ml/nn/layers/test_forward_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tests/safeds/ml/nn/layers/test_gru_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/safeds/ml/nn/layers/test_lstm_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading