diff --git a/src/safeds/data/labeled/containers/_time_series_dataset.py b/src/safeds/data/labeled/containers/_time_series_dataset.py index 06acbe1e5..4d5751bba 100644 --- a/src/safeds/data/labeled/containers/_time_series_dataset.py +++ b/src/safeds/data/labeled/containers/_time_series_dataset.py @@ -19,9 +19,9 @@ class TimeSeriesDataset: """ - A time series dataset maps feature and time columns to a target column. Not like the TabularDataset a TimeSeries needs to contain one target and one time column, but can have empty features. + A time series dataset maps feature and time columns to a target column. - Create a time series dataset from a mapping of column names to their values. + Unlike a TabularDataset, a TimeSeries needs to contain one target and one time column, but can have empty features. Parameters ---------- diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index 80f7971de..2cb20bdaf 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -551,8 +551,6 @@ def get_column(self, name: str) -> Column: """ Get a column from the table. - **Note:** This operation must fully load the data into memory, which can be expensive. - Parameters ---------- name: @@ -584,7 +582,9 @@ def get_column(self, name: str) -> Column: +-----+ """ _check_columns_exist(self, name) - return Column._from_polars_series(self._data_frame.get_column(name)) + return Column._from_polars_series( + self._lazy_frame.select(name).collect().get_column(name), + ) def get_column_type(self, name: str) -> DataType: """ diff --git a/src/safeds/exceptions/_ml.py b/src/safeds/exceptions/_ml.py index fcb8460dd..d84395485 100644 --- a/src/safeds/exceptions/_ml.py +++ b/src/safeds/exceptions/_ml.py @@ -76,7 +76,8 @@ def __init__(self) -> None: class InputSizeError(Exception): """Raised when the amount of features being passed to a network does not match with its input size.""" - def __init__(self, data_size: int | ModelImageSize, input_layer_size: int | ModelImageSize) -> None: + def __init__(self, data_size: int | ModelImageSize, input_layer_size: int | ModelImageSize | None) -> None: + # TODO: remove input_layer_size type None again super().__init__( f"The data size being passed to the network({data_size}) does not match with its input size({input_layer_size}). Consider changing the data size of the model or reformatting the data.", ) diff --git a/src/safeds/ml/nn/_model.py b/src/safeds/ml/nn/_model.py index b5941feb4..6247b22a4 100644 --- a/src/safeds/ml/nn/_model.py +++ b/src/safeds/ml/nn/_model.py @@ -17,12 +17,11 @@ ModelNotFittedError, ) from safeds.ml.nn.converters import ( - InputConversionImage, - OutputConversionImageToColumn, - OutputConversionImageToImage, - OutputConversionImageToTable, + InputConversionImageToColumn, + InputConversionImageToImage, + InputConversionImageToTable, ) -from safeds.ml.nn.converters._output_converter_image import _OutputConversionImage +from safeds.ml.nn.converters._input_converter_image import _InputConversionImage from safeds.ml.nn.layers import ( Convolutional2DLayer, FlattenLayer, @@ -38,15 +37,14 @@ from torch.nn import Module from transformers.image_processing_utils import BaseImageProcessor - from safeds.ml.nn.converters import InputConversion, OutputConversion + from safeds.ml.nn.converters import InputConversion from safeds.ml.nn.layers import Layer IFT = TypeVar("IFT", TabularDataset, TimeSeriesDataset, ImageDataset) # InputFitType IPT = TypeVar("IPT", Table, TimeSeriesDataset, ImageList) # InputPredictType -OT = TypeVar("OT", TabularDataset, TimeSeriesDataset, ImageDataset) # OutputType -class NeuralNetworkRegressor(Generic[IFT, IPT, OT]): +class NeuralNetworkRegressor(Generic[IFT, IPT]): """ A NeuralNetworkRegressor is a neural network that is used for regression tasks. @@ -56,8 +54,6 @@ class NeuralNetworkRegressor(Generic[IFT, IPT, OT]): to convert the input data for the neural network layers: a list of layers for the neural network to learn - output_conversion: - to convert the output data of the neural network back Raises ------ @@ -69,16 +65,13 @@ def __init__( self, input_conversion: InputConversion[IFT, IPT], layers: list[Layer], - output_conversion: OutputConversion[IPT, OT], ): if len(layers) == 0: raise InvalidModelStructureError("You need to provide at least one layer to a neural network.") - if isinstance(input_conversion, InputConversionImage): - if not isinstance(output_conversion, _OutputConversionImage): - raise InvalidModelStructureError( - "The defined model uses an input conversion for images but no output conversion for images.", - ) - elif isinstance(output_conversion, OutputConversionImageToColumn | OutputConversionImageToTable): + if isinstance(input_conversion, _InputConversionImage): + # TODO: why is this limitation needed? we might want to output the probability that an image shows a certain + # object, which would be a 1-dimensional output. + if isinstance(input_conversion, InputConversionImageToColumn | InputConversionImageToTable): raise InvalidModelStructureError( "A NeuralNetworkRegressor cannot be used with images as input and 1-dimensional data as output.", ) @@ -98,23 +91,19 @@ def __init__( else "You cannot use a 2-dimensional layer with 1-dimensional data." ), ) - if data_dimensions == 1 and isinstance(output_conversion, OutputConversionImageToImage): + if data_dimensions == 1 and isinstance(input_conversion, InputConversionImageToImage): raise InvalidModelStructureError( "The output data would be 1-dimensional but the provided output conversion uses 2-dimensional data.", ) - elif isinstance(output_conversion, _OutputConversionImage): - raise InvalidModelStructureError( - "The defined model uses an output conversion for images but no input conversion for images.", - ) else: for layer in layers: if isinstance(layer, Convolutional2DLayer | FlattenLayer | _Pooling2DLayer): raise InvalidModelStructureError("You cannot use a 2-dimensional layer with 1-dimensional data.") self._input_conversion: InputConversion[IFT, IPT] = input_conversion - self._model = _create_internal_model(input_conversion, layers, is_for_classification=False) - self._output_conversion: OutputConversion[IPT, OT] = output_conversion - self._input_size = self._model.input_size + self._model: Module | None = None + self._layers: list[Layer] = layers + self._input_size: int | ModelImageSize | None = None self._batch_size = 1 self._is_fitted = False self._total_number_of_batches_done = 0 @@ -160,13 +149,11 @@ def load_pretrained_model(huggingface_repo: str) -> NeuralNetworkRegressor: # p else: # Should never happen due to model check raise ValueError("This model is not supported") # pragma: no cover - in_conversion = InputConversionImage(input_size) - out_conversion = OutputConversionImageToImage() + in_conversion = InputConversionImageToImage(input_size) network = NeuralNetworkRegressor.__new__(NeuralNetworkRegressor) network._input_conversion = in_conversion network._model = model - network._output_conversion = out_conversion network._input_size = input_size network._batch_size = 1 network._is_fitted = True @@ -200,9 +187,11 @@ def fit( learning_rate: The learning rate of the neural network. callback_on_batch_completion: - Function used to view metrics while training. Gets called after a batch is completed with the index of the last batch and the overall loss average. + Function used to view metrics while training. Gets called after a batch is completed with the index of the + last batch and the overall loss average. callback_on_epoch_completion: - Function used to view metrics while training. Gets called after an epoch is completed with the index of the last epoch and the overall loss average. + Function used to view metrics while training. Gets called after an epoch is completed with the index of the + last epoch and the overall loss average. Returns ------- @@ -226,13 +215,14 @@ def fit( _check_bounds("epoch_size", epoch_size, lower_bound=_ClosedBound(1)) _check_bounds("batch_size", batch_size, lower_bound=_ClosedBound(1)) - if self._input_conversion._data_size is not self._input_size: - raise InputSizeError(self._input_conversion._data_size, self._input_size) - copied_model = copy.deepcopy(self) - + copied_model._model = _create_internal_model(self._input_conversion, self._layers, is_for_classification=False) + copied_model._input_size = copied_model._model.input_size copied_model._batch_size = batch_size + if copied_model._input_conversion._data_size != copied_model._input_size: + raise InputSizeError(copied_model._input_conversion._data_size, copied_model._input_size) + dataloader = copied_model._input_conversion._data_conversion_fit(train_data, copied_model._batch_size) loss_fn = nn.MSELoss() @@ -267,7 +257,7 @@ def fit( copied_model._model.eval() return copied_model - def predict(self, test_data: IPT) -> OT: + def predict(self, test_data: IPT) -> IFT: """ Make a prediction for the given test data. @@ -292,7 +282,7 @@ def predict(self, test_data: IPT) -> OT: _init_default_device() - if not self._is_fitted: + if not self._is_fitted or self._model is None: raise ModelNotFittedError if not self._input_conversion._is_predict_data_valid(test_data): raise FeatureDataMismatchError @@ -306,7 +296,7 @@ def predict(self, test_data: IPT) -> OT: elif not isinstance(elem, torch.Tensor): raise ValueError(f"Output of model has unsupported type: {type(elem)}") # pragma: no cover predictions.append(elem.squeeze(dim=1)) - return self._output_conversion._data_conversion( + return self._input_conversion._data_conversion_output( test_data, torch.cat(predictions, dim=0), **self._input_conversion._get_output_configuration(), @@ -318,12 +308,13 @@ def is_fitted(self) -> bool: return self._is_fitted @property - def input_size(self) -> int | ModelImageSize: + def input_size(self) -> int | ModelImageSize | None: """The input size of the model.""" + # TODO: raise if not fitted, don't return None return self._input_size -class NeuralNetworkClassifier(Generic[IFT, IPT, OT]): +class NeuralNetworkClassifier(Generic[IFT, IPT]): """ A NeuralNetworkClassifier is a neural network that is used for classification tasks. @@ -333,8 +324,6 @@ class NeuralNetworkClassifier(Generic[IFT, IPT, OT]): to convert the input data for the neural network layers: a list of layers for the neural network to learn - output_conversion: - to convert the output data of the neural network back Raises ------ @@ -346,24 +335,19 @@ def __init__( self, input_conversion: InputConversion[IFT, IPT], layers: list[Layer], - output_conversion: OutputConversion[IPT, OT], ): if len(layers) == 0: raise InvalidModelStructureError("You need to provide at least one layer to a neural network.") - if isinstance(output_conversion, OutputConversionImageToImage): + if isinstance(input_conversion, InputConversionImageToImage): raise InvalidModelStructureError("A NeuralNetworkClassifier cannot be used with images as output.") - if isinstance(input_conversion, InputConversionImage) and isinstance( + if isinstance(input_conversion, _InputConversionImage) and isinstance( input_conversion._input_size, VariableImageSize, ): raise InvalidModelStructureError( "A NeuralNetworkClassifier cannot be used with a InputConversionImage that uses a VariableImageSize.", ) - elif isinstance(input_conversion, InputConversionImage): - if not isinstance(output_conversion, _OutputConversionImage): - raise InvalidModelStructureError( - "The defined model uses an input conversion for images but no output conversion for images.", - ) + elif isinstance(input_conversion, _InputConversionImage): data_dimensions = 2 for layer in layers: if data_dimensions == 2 and (isinstance(layer, Convolutional2DLayer | _Pooling2DLayer)): @@ -381,24 +365,20 @@ def __init__( ), ) if data_dimensions == 2 and ( - isinstance(output_conversion, OutputConversionImageToColumn | OutputConversionImageToTable) + isinstance(input_conversion, InputConversionImageToColumn | InputConversionImageToTable) ): raise InvalidModelStructureError( "The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ) - elif isinstance(output_conversion, _OutputConversionImage): - raise InvalidModelStructureError( - "The defined model uses an output conversion for images but no input conversion for images.", - ) else: for layer in layers: if isinstance(layer, Convolutional2DLayer | FlattenLayer | _Pooling2DLayer): raise InvalidModelStructureError("You cannot use a 2-dimensional layer with 1-dimensional data.") self._input_conversion: InputConversion[IFT, IPT] = input_conversion - self._model = _create_internal_model(input_conversion, layers, is_for_classification=True) - self._output_conversion: OutputConversion[IPT, OT] = output_conversion - self._input_size: int | ModelImageSize = self._model.input_size + self._model: nn.Module | None = None + self._layers: list[Layer] = layers + self._input_size: int | ModelImageSize | None = None self._batch_size = 1 self._is_fitted = False self._num_of_classes = ( @@ -456,8 +436,7 @@ def load_pretrained_model(huggingface_repo: str) -> NeuralNetworkClassifier: # labels_table = Table({column_name: [label for _, label in label_dict.items()]}) one_hot_encoder = OneHotEncoder().fit(labels_table, [column_name]) - in_conversion = InputConversionImage(input_size) - out_conversion = OutputConversionImageToColumn() + in_conversion = InputConversionImageToColumn(input_size) in_conversion._column_name = column_name in_conversion._one_hot_encoder = one_hot_encoder @@ -468,7 +447,6 @@ def load_pretrained_model(huggingface_repo: str) -> NeuralNetworkClassifier: # network = NeuralNetworkClassifier.__new__(NeuralNetworkClassifier) network._input_conversion = in_conversion network._model = model - network._output_conversion = out_conversion network._input_size = input_size network._batch_size = 1 network._is_fitted = True @@ -503,9 +481,11 @@ def fit( learning_rate: The learning rate of the neural network. callback_on_batch_completion: - Function used to view metrics while training. Gets called after a batch is completed with the index of the last batch and the overall loss average. + Function used to view metrics while training. Gets called after a batch is completed with the index of the + last batch and the overall loss average. callback_on_epoch_completion: - Function used to view metrics while training. Gets called after an epoch is completed with the index of the last epoch and the overall loss average. + Function used to view metrics while training. Gets called after an epoch is completed with the index of the + last epoch and the overall loss average. Returns ------- @@ -529,12 +509,13 @@ def fit( _check_bounds("epoch_size", epoch_size, lower_bound=_ClosedBound(1)) _check_bounds("batch_size", batch_size, lower_bound=_ClosedBound(1)) - if self._input_conversion._data_size is not self._input_size: - raise InputSizeError(self._input_conversion._data_size, self._input_size) - copied_model = copy.deepcopy(self) - + copied_model._model = _create_internal_model(self._input_conversion, self._layers, is_for_classification=True) copied_model._batch_size = batch_size + copied_model._input_size = copied_model._model.input_size + + if copied_model._input_conversion._data_size != copied_model._input_size: + raise InputSizeError(copied_model._input_conversion._data_size, copied_model._input_size) dataloader = copied_model._input_conversion._data_conversion_fit( train_data, @@ -577,7 +558,7 @@ def fit( copied_model._model.eval() return copied_model - def predict(self, test_data: IPT) -> OT: + def predict(self, test_data: IPT) -> IFT: """ Make a prediction for the given test data. @@ -602,7 +583,7 @@ def predict(self, test_data: IPT) -> OT: _init_default_device() - if not self._is_fitted: + if not self._is_fitted or self._model is None: raise ModelNotFittedError if not self._input_conversion._is_predict_data_valid(test_data): raise FeatureDataMismatchError @@ -619,7 +600,7 @@ def predict(self, test_data: IPT) -> OT: predictions.append(torch.argmax(elem, dim=1)) else: predictions.append(elem.squeeze(dim=1).round()) - return self._output_conversion._data_conversion( + return self._input_conversion._data_conversion_output( test_data, torch.cat(predictions, dim=0), **self._input_conversion._get_output_configuration(), @@ -631,8 +612,9 @@ def is_fitted(self) -> bool: return self._is_fitted @property - def input_size(self) -> int | ModelImageSize: + def input_size(self) -> int | ModelImageSize | None: """The input size of the model.""" + # TODO: raise if not fitted, don't return None return self._input_size @@ -655,7 +637,7 @@ def __init__(self, layers: list[Layer], is_for_classification: bool) -> None: for layer in layers: if previous_output_size is not None: layer._set_input_size(previous_output_size) - elif isinstance(input_conversion, InputConversionImage): + elif isinstance(input_conversion, _InputConversionImage): layer._set_input_size(input_conversion._data_size) if isinstance(layer, FlattenLayer | _Pooling2DLayer): internal_layers.append(layer._get_internal_layer()) @@ -680,4 +662,7 @@ def forward(self, x: Tensor) -> Tensor: x = layer(x) return x + # Use torch.compile once the following issues are resolved: + # - https://github.com/pytorch/pytorch/issues/120233 (Python 3.12 support) + # - https://github.com/triton-lang/triton/issues/1640 (Windows support) return _InternalModel(layers, is_for_classification) diff --git a/src/safeds/ml/nn/converters/__init__.py b/src/safeds/ml/nn/converters/__init__.py index d619a2af0..6e51bb565 100644 --- a/src/safeds/ml/nn/converters/__init__.py +++ b/src/safeds/ml/nn/converters/__init__.py @@ -6,43 +6,29 @@ if TYPE_CHECKING: from ._input_converter import InputConversion - from ._input_converter_image import InputConversionImage + from ._input_converter_image_to_column import InputConversionImageToColumn + from ._input_converter_image_to_image import InputConversionImageToImage + from ._input_converter_image_to_table import InputConversionImageToTable from ._input_converter_table import InputConversionTable from ._input_converter_time_series import InputConversionTimeSeries - from ._output_converter import OutputConversion - from ._output_converter_image import ( - OutputConversionImageToColumn, - OutputConversionImageToImage, - OutputConversionImageToTable, - ) - from ._output_converter_table import OutputConversionTable - from ._output_converter_time_series import OutputConversionTimeSeries apipkg.initpkg( __name__, { "InputConversion": "._input_converter:InputConversion", - "InputConversionImage": "._input_converter_image:InputConversionImage", + "InputConversionImageToColumn": "._input_converter_image_to_column:InputConversionImageToColumn", + "InputConversionImageToImage": "._input_converter_image_to_image:InputConversionImageToImage", + "InputConversionImageToTable": "._input_converter_image_to_table:InputConversionImageToTable", "InputConversionTable": "._input_converter_table:InputConversionTable", "InputConversionTimeSeries": "._input_converter_time_series:InputConversionTimeSeries", - "OutputConversion": "._output_converter:OutputConversion", - "OutputConversionImageToColumn": "._output_converter_image:OutputConversionImageToColumn", - "OutputConversionImageToImage": "._output_converter_image:OutputConversionImageToImage", - "OutputConversionImageToTable": "._output_converter_image:OutputConversionImageToTable", - "OutputConversionTable": "._output_converter_table:OutputConversionTable", - "OutputConversionTimeSeries": "._output_converter_time_series:OutputConversionTimeSeries", }, ) __all__ = [ "InputConversion", - "InputConversionImage", + "InputConversionImageToColumn", + "InputConversionImageToImage", + "InputConversionImageToTable", "InputConversionTable", "InputConversionTimeSeries", - "OutputConversion", - "OutputConversionImageToColumn", - "OutputConversionImageToImage", - "OutputConversionImageToTable", - "OutputConversionTable", - "OutputConversionTimeSeries", ] diff --git a/src/safeds/ml/nn/converters/_input_converter.py b/src/safeds/ml/nn/converters/_input_converter.py index 54d45b881..595fbe688 100644 --- a/src/safeds/ml/nn/converters/_input_converter.py +++ b/src/safeds/ml/nn/converters/_input_converter.py @@ -8,6 +8,7 @@ from safeds.data.tabular.containers import Table if TYPE_CHECKING: + from torch import Tensor from torch.utils.data import DataLoader from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList @@ -22,8 +23,7 @@ class InputConversion(Generic[FT, PT], ABC): @property @abstractmethod - def _data_size(self) -> int | ModelImageSize: - pass # pragma: no cover + def _data_size(self) -> int | ModelImageSize: ... @abstractmethod def _data_conversion_fit( @@ -31,21 +31,19 @@ def _data_conversion_fit( input_data: FT, batch_size: int, num_of_classes: int = 1, - ) -> DataLoader | ImageDataset: - pass # pragma: no cover + ) -> DataLoader | ImageDataset: ... @abstractmethod - def _data_conversion_predict(self, input_data: PT, batch_size: int) -> DataLoader | _SingleSizeImageList: - pass # pragma: no cover + def _data_conversion_predict(self, input_data: PT, batch_size: int) -> DataLoader | _SingleSizeImageList: ... @abstractmethod - def _is_fit_data_valid(self, input_data: FT) -> bool: - pass # pragma: no cover + def _data_conversion_output(self, input_data: PT, output_data: Tensor, **kwargs: Any) -> FT: ... @abstractmethod - def _is_predict_data_valid(self, input_data: PT) -> bool: - pass # pragma: no cover + def _is_fit_data_valid(self, input_data: FT) -> bool: ... @abstractmethod - def _get_output_configuration(self) -> dict[str, Any]: - pass # pragma: no cover + def _is_predict_data_valid(self, input_data: PT) -> bool: ... + + @abstractmethod + def _get_output_configuration(self) -> dict[str, Any]: ... diff --git a/src/safeds/ml/nn/converters/_input_converter_image.py b/src/safeds/ml/nn/converters/_input_converter_image.py index bb94fde7c..795241e2b 100644 --- a/src/safeds/ml/nn/converters/_input_converter_image.py +++ b/src/safeds/ml/nn/converters/_input_converter_image.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from abc import ABC from typing import TYPE_CHECKING, Any from safeds._utils import _structural_hash @@ -16,18 +17,17 @@ from safeds.ml.nn.typing import ModelImageSize -class InputConversionImage(InputConversion[ImageDataset, ImageList]): - """The input conversion for a neural network, defines the input parameters for the neural network.""" +class _InputConversionImage(InputConversion[ImageDataset, ImageList], ABC): + """ + The input conversion for a neural network, defines the input parameters for the neural network. + + Parameters + ---------- + image_size: + the size of the input images + """ def __init__(self, image_size: ModelImageSize) -> None: - """ - Define the input parameters for the neural network in the input conversion. - - Parameters - ---------- - image_size: - the size of the input images - """ self._input_size = image_size self._output_size: ModelImageSize | int | None = None self._one_hot_encoder: OneHotEncoder | None = None @@ -35,6 +35,39 @@ def __init__(self, image_size: ModelImageSize) -> None: self._column_names: list[str] | None = None self._output_type: type | None = None + def __hash__(self) -> int: + return _structural_hash( + self.__class__.__name__, + self._input_size, + self._output_size, + self._one_hot_encoder, + self._column_name, + self._column_names, + self._output_type, + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, type(self)): + return NotImplemented + return (self is other) or ( + self._input_size == other._input_size + and self._output_size == other._output_size + and self._one_hot_encoder == other._one_hot_encoder + and self._column_name == other._column_name + and self._column_names == other._column_names + and self._output_type == other._output_type + ) + + def __sizeof__(self) -> int: + return ( + sys.getsizeof(self._input_size) + + sys.getsizeof(self._output_size) + + sys.getsizeof(self._one_hot_encoder) + + sys.getsizeof(self._column_name) + + sys.getsizeof(self._column_names) + + sys.getsizeof(self._output_type) + ) + @property def _data_size(self) -> ModelImageSize: return self._input_size @@ -81,64 +114,3 @@ def _get_output_configuration(self) -> dict[str, Any]: "column_name": self._column_name, "one_hot_encoder": self._one_hot_encoder, } - - def __hash__(self) -> int: - """ - Return a deterministic hash value for this InputConversionImage. - - Returns - ------- - hash: - the hash value - """ - return _structural_hash( - self._input_size, - self._output_size, - self._one_hot_encoder, - self._column_name, - self._column_names, - self._output_type, - ) - - def __eq__(self, other: object) -> bool: - """ - Compare two InputConversionImage instances. - - Parameters - ---------- - other: - The InputConversionImage instance to compare to. - - Returns - ------- - equals: - Whether the instances are the same. - """ - if not isinstance(other, InputConversionImage): - return NotImplemented - return (self is other) or ( - self._input_size == other._input_size - and self._output_size == other._output_size - and self._one_hot_encoder == other._one_hot_encoder - and self._column_name == other._column_name - and self._column_names == other._column_names - and self._output_type == other._output_type - ) - - def __sizeof__(self) -> int: - """ - Return the complete size of this object. - - Returns - ------- - size: - Size of this object in bytes. - """ - return ( - sys.getsizeof(self._input_size) - + sys.getsizeof(self._output_size) - + sys.getsizeof(self._one_hot_encoder) - + sys.getsizeof(self._column_name) - + sys.getsizeof(self._column_names) - + sys.getsizeof(self._output_type) - ) diff --git a/src/safeds/ml/nn/converters/_input_converter_image_to_column.py b/src/safeds/ml/nn/converters/_input_converter_image_to_column.py new file mode 100644 index 000000000..b3fc6e95a --- /dev/null +++ b/src/safeds/ml/nn/converters/_input_converter_image_to_column.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from safeds._config import _init_default_device +from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList +from safeds.data.labeled.containers import ImageDataset +from safeds.data.labeled.containers._image_dataset import _ColumnAsTensor +from safeds.data.tabular.containers import Column +from safeds.data.tabular.transformation import OneHotEncoder + +from ._input_converter_image import _InputConversionImage + +if TYPE_CHECKING: + from torch import Tensor + + from safeds.data.image.containers import ImageList + + +class InputConversionImageToColumn(_InputConversionImage): + def _data_conversion_output( + self, + input_data: ImageList, + output_data: Tensor, + **kwargs: Any, + ) -> ImageDataset[Column]: + import torch + + _init_default_device() + + if not isinstance(input_data, _SingleSizeImageList): + raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 + if "column_name" not in kwargs or not isinstance(kwargs.get("column_name"), str): + raise ValueError( + "The column_name is not set. The data can only be converted if the column_name is provided as `str` in the kwargs.", + ) + if "one_hot_encoder" not in kwargs or not isinstance(kwargs.get("one_hot_encoder"), OneHotEncoder): + raise ValueError( + "The one_hot_encoder is not set. The data can only be converted if the one_hot_encoder is provided as `OneHotEncoder` in the kwargs.", + ) + one_hot_encoder: OneHotEncoder = kwargs["one_hot_encoder"] + column_name: str = kwargs["column_name"] + + output = torch.zeros(len(input_data), len(one_hot_encoder._get_names_of_added_columns())) + output[torch.arange(len(input_data)), output_data] = 1 + + im_dataset: ImageDataset[Column] = ImageDataset[Column].__new__(ImageDataset) + im_dataset._output = _ColumnAsTensor._from_tensor(output, column_name, one_hot_encoder) + im_dataset._shuffle_tensor_indices = torch.LongTensor(list(range(len(input_data)))) + im_dataset._shuffle_after_epoch = False + im_dataset._batch_size = 1 + im_dataset._next_batch_index = 0 + im_dataset._input_size = input_data.sizes[0] + im_dataset._input = input_data + return im_dataset diff --git a/src/safeds/ml/nn/converters/_input_converter_image_to_image.py b/src/safeds/ml/nn/converters/_input_converter_image_to_image.py new file mode 100644 index 000000000..59687cc43 --- /dev/null +++ b/src/safeds/ml/nn/converters/_input_converter_image_to_image.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from safeds._config import _init_default_device +from safeds.data.image.containers import ImageList +from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList +from safeds.data.labeled.containers import ImageDataset + +from ._input_converter_image import _InputConversionImage + +if TYPE_CHECKING: + from torch import Tensor + + +class InputConversionImageToImage(_InputConversionImage): + def _data_conversion_output( + self, + input_data: ImageList, + output_data: Tensor, + **_kwargs: Any, + ) -> ImageDataset[ImageList]: + import torch + + _init_default_device() + + if not isinstance(input_data, _SingleSizeImageList): + raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 + + return ImageDataset[ImageList]( + input_data, + _SingleSizeImageList._create_from_tensor( + (output_data * 255).to(torch.uint8), + list(range(output_data.size(dim=0))), + ), + ) diff --git a/src/safeds/ml/nn/converters/_input_converter_image_to_table.py b/src/safeds/ml/nn/converters/_input_converter_image_to_table.py new file mode 100644 index 000000000..c8f8ef3e7 --- /dev/null +++ b/src/safeds/ml/nn/converters/_input_converter_image_to_table.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from safeds._config import _init_default_device +from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList +from safeds.data.labeled.containers import ImageDataset +from safeds.data.labeled.containers._image_dataset import _TableAsTensor +from safeds.data.tabular.containers import Table + +from ._input_converter_image import _InputConversionImage + +if TYPE_CHECKING: + from torch import Tensor + + from safeds.data.image.containers import ImageList + + +class InputConversionImageToTable(_InputConversionImage): + def _data_conversion_output(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset[Table]: + import torch + + _init_default_device() + + if not isinstance(input_data, _SingleSizeImageList): + raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 + if ( + "column_names" not in kwargs + or not isinstance(kwargs.get("column_names"), list) + and all(isinstance(element, str) for element in kwargs["column_names"]) + ): + raise ValueError( + "The column_names are not set. The data can only be converted if the column_names are provided as `list[str]` in the kwargs.", + ) + column_names: list[str] = kwargs["column_names"] + + output = torch.zeros(len(input_data), len(column_names)) + output[torch.arange(len(input_data)), output_data] = 1 + + im_dataset: ImageDataset[Table] = ImageDataset[Table].__new__(ImageDataset) + im_dataset._output = _TableAsTensor._from_tensor(output, column_names) + im_dataset._shuffle_tensor_indices = torch.LongTensor(list(range(len(input_data)))) + im_dataset._shuffle_after_epoch = False + im_dataset._batch_size = 1 + im_dataset._next_batch_index = 0 + im_dataset._input_size = input_data.sizes[0] + im_dataset._input = input_data + return im_dataset diff --git a/src/safeds/ml/nn/converters/_input_converter_table.py b/src/safeds/ml/nn/converters/_input_converter_table.py index 9294ef46a..e8b912a94 100644 --- a/src/safeds/ml/nn/converters/_input_converter_table.py +++ b/src/safeds/ml/nn/converters/_input_converter_table.py @@ -3,23 +3,29 @@ from typing import TYPE_CHECKING, Any from safeds.data.labeled.containers import TabularDataset -from safeds.data.tabular.containers import Table +from safeds.data.tabular.containers import Column, Table from ._input_converter import InputConversion if TYPE_CHECKING: + from torch import Tensor from torch.utils.data import DataLoader class InputConversionTable(InputConversion[TabularDataset, Table]): - """The input conversion for a neural network, defines the input parameters for the neural network.""" + """ + The input conversion for a neural network, defines the input parameters for the neural network. - def __init__(self) -> None: - """Define the input parameters for the neural network in the input conversion.""" + prediction_name: + The name of the new column where the prediction will be stored. + """ + + def __init__(self, *, prediction_name: str = "prediction") -> None: self._target_name = "" self._time_name = "" self._feature_names: list[str] = [] self._first = True + self._prediction_name = prediction_name # TODO: use target name, override existing column @property def _data_size(self) -> int: @@ -34,6 +40,11 @@ def _data_conversion_fit(self, input_data: TabularDataset, batch_size: int, num_ def _data_conversion_predict(self, input_data: Table, batch_size: int) -> DataLoader: return input_data._into_dataloader(batch_size) + def _data_conversion_output(self, input_data: Table, output_data: Tensor, **_kwargs: Any) -> TabularDataset: + return input_data.add_columns([Column(self._prediction_name, output_data.tolist())]).to_tabular_dataset( + self._prediction_name, + ) + def _is_fit_data_valid(self, input_data: TabularDataset) -> bool: if self._first: self._feature_names = input_data.features.column_names diff --git a/src/safeds/ml/nn/converters/_input_converter_time_series.py b/src/safeds/ml/nn/converters/_input_converter_time_series.py index 44ef453b2..050a8cb1c 100644 --- a/src/safeds/ml/nn/converters/_input_converter_time_series.py +++ b/src/safeds/ml/nn/converters/_input_converter_time_series.py @@ -1,39 +1,56 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Any +from safeds._utils import _structural_hash from safeds.data.labeled.containers import TimeSeriesDataset +from safeds.data.tabular.containers import Column from ._input_converter import InputConversion if TYPE_CHECKING: + from torch import Tensor from torch.utils.data import DataLoader class InputConversionTimeSeries(InputConversion[TimeSeriesDataset, TimeSeriesDataset]): - """The input conversion for a neural network, defines the input parameters for the neural network.""" + """ + The input conversion for a neural network, defines the input parameters for the neural network. + + Parameters + ---------- + window_size: + The size of the created windows + forecast_horizon: + The forecast horizon defines the future lag of the predicted values + """ def __init__( self, window_size: int, forecast_horizon: int, + *, + prediction_name: str = "prediction_nn", ) -> None: - """ - Define the input parameters for the neural network in the input conversion. - - Parameters - ---------- - window_size: - The size of the created windows - forecast_horizon: - The forecast horizon defines the future lag of the predicted values - """ self._window_size = window_size self._forecast_horizon = forecast_horizon self._first = True self._target_name: str = "" self._time_name: str = "" self._feature_names: list[str] = [] + self._prediction_name = prediction_name # TODO: use target name, override existing column + + def __eq__(self, other: object) -> bool: + if not isinstance(other, InputConversionTimeSeries): + return False + return self._prediction_name == other._prediction_name + + def __hash__(self) -> int: + return _structural_hash(self.__class__.__name__ + self._prediction_name) + + def __sizeof__(self) -> int: + return sys.getsizeof(self._prediction_name) @property def _data_size(self) -> int: @@ -44,7 +61,6 @@ def _data_size(self) -> int: ------- size: The size of the input for the neural network - """ return (len(self._feature_names) + 1) * self._window_size @@ -64,6 +80,35 @@ def _data_conversion_fit( def _data_conversion_predict(self, input_data: TimeSeriesDataset, batch_size: int) -> DataLoader: return input_data._into_dataloader_with_window_predict(self._window_size, self._forecast_horizon, batch_size) + def _data_conversion_output( + self, + input_data: TimeSeriesDataset, + output_data: Tensor, + **kwargs: Any, + ) -> TimeSeriesDataset: + if "window_size" not in kwargs or not isinstance(kwargs.get("window_size"), int): + raise ValueError( + "The window_size is not set. " + "The data can only be converted if the window_size is provided as `int` in the kwargs.", + ) + if "forecast_horizon" not in kwargs or not isinstance(kwargs.get("forecast_horizon"), int): + raise ValueError( + "The forecast_horizon is not set. " + "The data can only be converted if the forecast_horizon is provided as `int` in the kwargs.", + ) + window_size: int = kwargs["window_size"] + forecast_horizon: int = kwargs["forecast_horizon"] + input_data_table = input_data.to_table() + input_data_table = input_data_table.slice_rows(start=window_size + forecast_horizon) + + return input_data_table.add_columns( + [Column(self._prediction_name, output_data.tolist())], + ).to_time_series_dataset( + target_name=self._prediction_name, + time_name=input_data.time.name, + extra_names=input_data.extras.column_names, + ) + def _is_fit_data_valid(self, input_data: TimeSeriesDataset) -> bool: if self._first: self._time_name = input_data.time.name diff --git a/src/safeds/ml/nn/converters/_output_converter.py b/src/safeds/ml/nn/converters/_output_converter.py deleted file mode 100644 index d44a75d54..000000000 --- a/src/safeds/ml/nn/converters/_output_converter.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -from safeds.data.image.containers import ImageList -from safeds.data.labeled.containers import ImageDataset, TabularDataset, TimeSeriesDataset -from safeds.data.tabular.containers import Table - -if TYPE_CHECKING: - from torch import Tensor - -IT = TypeVar("IT", Table, TimeSeriesDataset, ImageList) -OT = TypeVar("OT", TabularDataset, TimeSeriesDataset, ImageDataset) - - -class OutputConversion(Generic[IT, OT], ABC): - """The output conversion for a neural network, defines the output parameters for the neural network.""" - - @abstractmethod - def _data_conversion(self, input_data: IT, output_data: Tensor, **kwargs: Any) -> OT: - pass # pragma: no cover diff --git a/src/safeds/ml/nn/converters/_output_converter_image.py b/src/safeds/ml/nn/converters/_output_converter_image.py deleted file mode 100644 index 5aad88599..000000000 --- a/src/safeds/ml/nn/converters/_output_converter_image.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any - -from safeds._config import _init_default_device -from safeds._utils import _structural_hash -from safeds.data.image.containers import ImageList -from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList -from safeds.data.labeled.containers import ImageDataset -from safeds.data.labeled.containers._image_dataset import _ColumnAsTensor, _TableAsTensor -from safeds.data.tabular.containers import Column, Table -from safeds.data.tabular.transformation import OneHotEncoder - -from ._output_converter import OutputConversion - -if TYPE_CHECKING: - from torch import Tensor - - -class _OutputConversionImage(OutputConversion[ImageList, ImageDataset], ABC): - @abstractmethod - def _data_conversion(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset: - pass # pragma: no cover - - def __hash__(self) -> int: - """ - Return a deterministic hash value for this OutputConversionImage. - - Returns - ------- - hash: - the hash value - """ - return _structural_hash(self.__class__.__name__) - - def __eq__(self, other: object) -> bool: - """ - Compare two OutputConversionImage instances. - - Parameters - ---------- - other: - The OutputConversionImage instance to compare to. - - Returns - ------- - equals: - Whether the instances are the same. - """ - if not isinstance(other, type(self)): - return NotImplemented - return True - - def __sizeof__(self) -> int: - """ - Return the complete size of this object. - - Returns - ------- - size: - Size of this object in bytes. - """ - return 0 - - -class OutputConversionImageToColumn(_OutputConversionImage): - def _data_conversion(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset[Column]: - import torch - - _init_default_device() - - if not isinstance(input_data, _SingleSizeImageList): - raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 - if "column_name" not in kwargs or not isinstance(kwargs.get("column_name"), str): - raise ValueError( - "The column_name is not set. The data can only be converted if the column_name is provided as `str` in the kwargs.", - ) - if "one_hot_encoder" not in kwargs or not isinstance(kwargs.get("one_hot_encoder"), OneHotEncoder): - raise ValueError( - "The one_hot_encoder is not set. The data can only be converted if the one_hot_encoder is provided as `OneHotEncoder` in the kwargs.", - ) - one_hot_encoder: OneHotEncoder = kwargs["one_hot_encoder"] - column_name: str = kwargs["column_name"] - - output = torch.zeros(len(input_data), len(one_hot_encoder._get_names_of_added_columns())) - output[torch.arange(len(input_data)), output_data] = 1 - - im_dataset: ImageDataset[Column] = ImageDataset[Column].__new__(ImageDataset) - im_dataset._output = _ColumnAsTensor._from_tensor(output, column_name, one_hot_encoder) - im_dataset._shuffle_tensor_indices = torch.LongTensor(list(range(len(input_data)))) - im_dataset._shuffle_after_epoch = False - im_dataset._batch_size = 1 - im_dataset._next_batch_index = 0 - im_dataset._input_size = input_data.sizes[0] - im_dataset._input = input_data - return im_dataset - - -class OutputConversionImageToTable(_OutputConversionImage): - def _data_conversion(self, input_data: ImageList, output_data: Tensor, **kwargs: Any) -> ImageDataset[Table]: - import torch - - _init_default_device() - - if not isinstance(input_data, _SingleSizeImageList): - raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 - if ( - "column_names" not in kwargs - or not isinstance(kwargs.get("column_names"), list) - and all(isinstance(element, str) for element in kwargs["column_names"]) - ): - raise ValueError( - "The column_names are not set. The data can only be converted if the column_names are provided as `list[str]` in the kwargs.", - ) - column_names: list[str] = kwargs["column_names"] - - output = torch.zeros(len(input_data), len(column_names)) - output[torch.arange(len(input_data)), output_data] = 1 - - im_dataset: ImageDataset[Table] = ImageDataset[Table].__new__(ImageDataset) - im_dataset._output = _TableAsTensor._from_tensor(output, column_names) - im_dataset._shuffle_tensor_indices = torch.LongTensor(list(range(len(input_data)))) - im_dataset._shuffle_after_epoch = False - im_dataset._batch_size = 1 - im_dataset._next_batch_index = 0 - im_dataset._input_size = input_data.sizes[0] - im_dataset._input = input_data - return im_dataset - - -class OutputConversionImageToImage(_OutputConversionImage): - def _data_conversion( - self, - input_data: ImageList, - output_data: Tensor, - **kwargs: Any, # noqa: ARG002 - ) -> ImageDataset[ImageList]: - import torch - - _init_default_device() - - if not isinstance(input_data, _SingleSizeImageList): - raise ValueError("The given input ImageList contains images of different sizes.") # noqa: TRY004 - - return ImageDataset[ImageList]( - input_data, - _SingleSizeImageList._create_from_tensor( - (output_data * 255).to(torch.uint8), - list(range(output_data.size(dim=0))), - ), - ) diff --git a/src/safeds/ml/nn/converters/_output_converter_table.py b/src/safeds/ml/nn/converters/_output_converter_table.py deleted file mode 100644 index 17b4f717f..000000000 --- a/src/safeds/ml/nn/converters/_output_converter_table.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from safeds.data.labeled.containers import TabularDataset -from safeds.data.tabular.containers import Column, Table - -from ._output_converter import OutputConversion - -if TYPE_CHECKING: - from torch import Tensor - - -class OutputConversionTable(OutputConversion[Table, TabularDataset]): - """The output conversion for a neural network, defines the output parameters for the neural network.""" - - def __init__(self, prediction_name: str = "prediction") -> None: - """ - Define the output parameters for the neural network in the output conversion. - - Parameters - ---------- - prediction_name: - The name of the new column where the prediction will be stored. - """ - self._prediction_name = prediction_name - - def _data_conversion(self, input_data: Table, output_data: Tensor, **kwargs: Any) -> TabularDataset: # noqa: ARG002 - return input_data.add_columns([Column(self._prediction_name, output_data.tolist())]).to_tabular_dataset( - self._prediction_name, - ) diff --git a/src/safeds/ml/nn/converters/_output_converter_time_series.py b/src/safeds/ml/nn/converters/_output_converter_time_series.py deleted file mode 100644 index 0c5618a9d..000000000 --- a/src/safeds/ml/nn/converters/_output_converter_time_series.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING, Any - -from safeds._utils import _structural_hash -from safeds.data.labeled.containers import TimeSeriesDataset -from safeds.data.tabular.containers import Column - -from ._output_converter import OutputConversion - -if TYPE_CHECKING: - from torch import Tensor - - -class OutputConversionTimeSeries(OutputConversion[TimeSeriesDataset, TimeSeriesDataset]): - """The output conversion for a neural network, defines the output parameters for the neural network.""" - - def __hash__(self) -> int: - """ - Return a deterministic hash value for this OutputConversionTimeSeries instance. - - Returns - ------- - hash: - the hash value - """ - return _structural_hash(self.__class__.__name__ + self._prediction_name) - - def __eq__(self, other: object) -> bool: - """ - Compare two OutputConversionTimeSeries instances. - - Parameters - ---------- - other: - The OutputConversionTimeSeries instance to compare to. - - Returns - ------- - equals: - Whether the instances are the same. - """ - if not isinstance(other, OutputConversionTimeSeries): - return False - return self._prediction_name == other._prediction_name - - def __sizeof__(self) -> int: - """ - Return the complete size of this object. - - Returns - ------- - size: - Size of this object in bytes. - """ - return sys.getsizeof(self._prediction_name) - - def __init__(self, prediction_name: str = "prediction_nn") -> None: - """ - Define the output parameters for the neural network in the output conversion. - - Parameters - ---------- - prediction_name: - The name of the new column where the prediction will be stored. - """ - self._prediction_name = prediction_name - - def _data_conversion(self, input_data: TimeSeriesDataset, output_data: Tensor, **kwargs: Any) -> TimeSeriesDataset: - if "window_size" not in kwargs or not isinstance(kwargs.get("window_size"), int): - raise ValueError( - "The window_size is not set. The data can only be converted if the window_size is provided as `int` in the kwargs.", - ) - if "forecast_horizon" not in kwargs or not isinstance(kwargs.get("forecast_horizon"), int): - raise ValueError( - "The forecast_horizon is not set. The data can only be converted if the forecast_horizon is provided as `int` in the kwargs.", - ) - window_size: int = kwargs["window_size"] - forecast_horizon: int = kwargs["forecast_horizon"] - input_data_table = input_data.to_table() - input_data_table = input_data_table.slice_rows(start=window_size + forecast_horizon) - - return input_data_table.add_columns( - [Column(self._prediction_name, output_data.tolist())], - ).to_time_series_dataset( - target_name=self._prediction_name, - time_name=input_data.time.name, - extra_names=input_data.extras.column_names, - ) diff --git a/tests/safeds/ml/nn/converters/test_input_converter_image.py b/tests/safeds/ml/nn/converters/test_input_converter_image.py index 46822d0d0..f4e754fec 100644 --- a/tests/safeds/ml/nn/converters/test_input_converter_image.py +++ b/tests/safeds/ml/nn/converters/test_input_converter_image.py @@ -5,7 +5,7 @@ from safeds.data.image.typing import ImageSize from safeds.data.labeled.containers import ImageDataset from safeds.data.tabular.containers import Column, Table -from safeds.ml.nn.converters import InputConversionImage +from safeds.ml.nn.converters import InputConversionImageToImage from tests.helpers import images_all, resolve_resource_path @@ -70,7 +70,7 @@ def test_should_return_false_if_fit_data_is_invalid( image_dataset_valid: ImageDataset, image_dataset_invalid: ImageDataset, ) -> None: - input_conversion = InputConversionImage(image_dataset_valid.input_size) + input_conversion = InputConversionImageToImage(image_dataset_valid.input_size) assert input_conversion._is_fit_data_valid(image_dataset_valid) assert input_conversion._is_fit_data_valid(image_dataset_valid) assert not input_conversion._is_fit_data_valid(image_dataset_invalid) @@ -79,34 +79,34 @@ def test_should_return_false_if_fit_data_is_invalid( class TestEq: @pytest.mark.parametrize( ("input_conversion_image1", "input_conversion_image2"), - [(InputConversionImage(ImageSize(1, 2, 3)), InputConversionImage(ImageSize(1, 2, 3)))], + [(InputConversionImageToImage(ImageSize(1, 2, 3)), InputConversionImageToImage(ImageSize(1, 2, 3)))], ) def test_should_be_equal( self, - input_conversion_image1: InputConversionImage, - input_conversion_image2: InputConversionImage, + input_conversion_image1: InputConversionImageToImage, + input_conversion_image2: InputConversionImageToImage, ) -> None: assert input_conversion_image1 == input_conversion_image2 - @pytest.mark.parametrize("input_conversion_image1", [InputConversionImage(ImageSize(1, 2, 3))]) + @pytest.mark.parametrize("input_conversion_image1", [InputConversionImageToImage(ImageSize(1, 2, 3))]) @pytest.mark.parametrize( "input_conversion_image2", [ - InputConversionImage(ImageSize(2, 2, 3)), - InputConversionImage(ImageSize(1, 1, 3)), - InputConversionImage(ImageSize(1, 2, 1)), - InputConversionImage(ImageSize(1, 2, 4)), + InputConversionImageToImage(ImageSize(2, 2, 3)), + InputConversionImageToImage(ImageSize(1, 1, 3)), + InputConversionImageToImage(ImageSize(1, 2, 1)), + InputConversionImageToImage(ImageSize(1, 2, 4)), ], ) def test_should_not_be_equal( self, - input_conversion_image1: InputConversionImage, - input_conversion_image2: InputConversionImage, + input_conversion_image1: InputConversionImageToImage, + input_conversion_image2: InputConversionImageToImage, ) -> None: assert input_conversion_image1 != input_conversion_image2 def test_should_be_not_implemented(self) -> None: - input_conversion_image = InputConversionImage(ImageSize(1, 2, 3)) + input_conversion_image = InputConversionImageToImage(ImageSize(1, 2, 3)) other = Table() assert input_conversion_image.__eq__(other) is NotImplemented @@ -114,34 +114,37 @@ def test_should_be_not_implemented(self) -> None: class TestHash: @pytest.mark.parametrize( ("input_conversion_image1", "input_conversion_image2"), - [(InputConversionImage(ImageSize(1, 2, 3)), InputConversionImage(ImageSize(1, 2, 3)))], + [(InputConversionImageToImage(ImageSize(1, 2, 3)), InputConversionImageToImage(ImageSize(1, 2, 3)))], ) def test_hash_should_be_equal( self, - input_conversion_image1: InputConversionImage, - input_conversion_image2: InputConversionImage, + input_conversion_image1: InputConversionImageToImage, + input_conversion_image2: InputConversionImageToImage, ) -> None: assert hash(input_conversion_image1) == hash(input_conversion_image2) - @pytest.mark.parametrize("input_conversion_image1", [InputConversionImage(ImageSize(1, 2, 3))]) + @pytest.mark.parametrize("input_conversion_image1", [InputConversionImageToImage(ImageSize(1, 2, 3))]) @pytest.mark.parametrize( "input_conversion_image2", [ - InputConversionImage(ImageSize(2, 2, 3)), - InputConversionImage(ImageSize(1, 1, 3)), - InputConversionImage(ImageSize(1, 2, 1)), - InputConversionImage(ImageSize(1, 2, 4)), + InputConversionImageToImage(ImageSize(2, 2, 3)), + InputConversionImageToImage(ImageSize(1, 1, 3)), + InputConversionImageToImage(ImageSize(1, 2, 1)), + InputConversionImageToImage(ImageSize(1, 2, 4)), ], ) def test_hash_should_not_be_equal( self, - input_conversion_image1: InputConversionImage, - input_conversion_image2: InputConversionImage, + input_conversion_image1: InputConversionImageToImage, + input_conversion_image2: InputConversionImageToImage, ) -> None: assert hash(input_conversion_image1) != hash(input_conversion_image2) class TestSizeOf: - @pytest.mark.parametrize("input_conversion_image", [InputConversionImage(ImageSize(1, 2, 3))]) - def test_should_size_be_greater_than_normal_object(self, input_conversion_image: InputConversionImage) -> None: + @pytest.mark.parametrize("input_conversion_image", [InputConversionImageToImage(ImageSize(1, 2, 3))]) + def test_should_size_be_greater_than_normal_object( + self, + input_conversion_image: InputConversionImageToImage, + ) -> None: assert sys.getsizeof(input_conversion_image) > sys.getsizeof(object()) diff --git a/tests/safeds/ml/nn/converters/test_output_converter_image.py b/tests/safeds/ml/nn/converters/test_input_converter_image_2.py similarity index 59% rename from tests/safeds/ml/nn/converters/test_output_converter_image.py rename to tests/safeds/ml/nn/converters/test_input_converter_image_2.py index 584eae4cc..5ea2f4828 100644 --- a/tests/safeds/ml/nn/converters/test_output_converter_image.py +++ b/tests/safeds/ml/nn/converters/test_input_converter_image_2.py @@ -4,53 +4,61 @@ import torch from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList +from safeds.data.image.typing import ImageSize from safeds.data.tabular.containers import Table from safeds.data.tabular.transformation import OneHotEncoder from safeds.ml.nn.converters import ( - OutputConversionImageToColumn, - OutputConversionImageToImage, - OutputConversionImageToTable, + InputConversionImageToColumn, + InputConversionImageToImage, + InputConversionImageToTable, ) -from safeds.ml.nn.converters._output_converter_image import _OutputConversionImage +from safeds.ml.nn.converters._input_converter_image import _InputConversionImage class TestDataConversionImage: @pytest.mark.parametrize( - ("output_conversion", "kwargs"), + ("input_conversion", "kwargs"), [ - (OutputConversionImageToColumn(), {"column_name": "a", "one_hot_encoder": OneHotEncoder()}), - (OutputConversionImageToTable(), {"column_names": ["a"]}), - (OutputConversionImageToImage(), {}), + ( + InputConversionImageToColumn(ImageSize(1, 1, 1)), + {"column_name": "a", "one_hot_encoder": OneHotEncoder()}, + ), + (InputConversionImageToTable(ImageSize(1, 1, 1)), {"column_names": ["a"]}), + (InputConversionImageToImage(ImageSize(1, 1, 1)), {}), ], ) def test_should_raise_if_input_data_is_multi_size( self, - output_conversion: _OutputConversionImage, + input_conversion: _InputConversionImage, kwargs: dict, ) -> None: with pytest.raises(ValueError, match=r"The given input ImageList contains images of different sizes."): - output_conversion._data_conversion(input_data=_MultiSizeImageList(), output_data=torch.empty(1), **kwargs) + input_conversion._data_conversion_output( + input_data=_MultiSizeImageList(), + output_data=torch.empty(1), + **kwargs, + ) class TestEq: @pytest.mark.parametrize( ("output_conversion_image1", "output_conversion_image2"), [ - (OutputConversionImageToColumn(), OutputConversionImageToColumn()), - (OutputConversionImageToTable(), OutputConversionImageToTable()), - (OutputConversionImageToImage(), OutputConversionImageToImage()), + (InputConversionImageToColumn(ImageSize(1, 1, 1)), InputConversionImageToColumn(ImageSize(1, 1, 1))), + (InputConversionImageToTable(ImageSize(1, 1, 1)), InputConversionImageToTable(ImageSize(1, 1, 1))), + (InputConversionImageToImage(ImageSize(1, 1, 1)), InputConversionImageToImage(ImageSize(1, 1, 1))), ], ) def test_should_be_equal( self, - output_conversion_image1: _OutputConversionImage, - output_conversion_image2: _OutputConversionImage, + output_conversion_image1: _InputConversionImage, + output_conversion_image2: _InputConversionImage, ) -> None: assert output_conversion_image1 == output_conversion_image2 def test_should_be_not_implemented(self) -> None: - output_conversion_image_to_image = OutputConversionImageToImage() - output_conversion_image_to_table = OutputConversionImageToTable() - output_conversion_image_to_column = OutputConversionImageToColumn() + output_conversion_image_to_image = InputConversionImageToImage(ImageSize(1, 1, 1)) + output_conversion_image_to_table = InputConversionImageToTable(ImageSize(1, 1, 1)) + output_conversion_image_to_column = InputConversionImageToColumn(ImageSize(1, 1, 1)) other = Table() assert output_conversion_image_to_image.__eq__(other) is NotImplemented assert output_conversion_image_to_image.__eq__(output_conversion_image_to_table) is NotImplemented @@ -66,22 +74,22 @@ class TestHash: @pytest.mark.parametrize( ("output_conversion_image1", "output_conversion_image2"), [ - (OutputConversionImageToColumn(), OutputConversionImageToColumn()), - (OutputConversionImageToTable(), OutputConversionImageToTable()), - (OutputConversionImageToImage(), OutputConversionImageToImage()), + (InputConversionImageToColumn(ImageSize(1, 1, 1)), InputConversionImageToColumn(ImageSize(1, 1, 1))), + (InputConversionImageToTable(ImageSize(1, 1, 1)), InputConversionImageToTable(ImageSize(1, 1, 1))), + (InputConversionImageToImage(ImageSize(1, 1, 1)), InputConversionImageToImage(ImageSize(1, 1, 1))), ], ) def test_hash_should_be_equal( self, - output_conversion_image1: _OutputConversionImage, - output_conversion_image2: _OutputConversionImage, + output_conversion_image1: _InputConversionImage, + output_conversion_image2: _InputConversionImage, ) -> None: assert hash(output_conversion_image1) == hash(output_conversion_image2) def test_hash_should_not_be_equal(self) -> None: - output_conversion_image_to_image = OutputConversionImageToImage() - output_conversion_image_to_table = OutputConversionImageToTable() - output_conversion_image_to_column = OutputConversionImageToColumn() + output_conversion_image_to_image = InputConversionImageToImage(ImageSize(1, 1, 1)) + output_conversion_image_to_table = InputConversionImageToTable(ImageSize(1, 1, 1)) + output_conversion_image_to_column = InputConversionImageToColumn(ImageSize(1, 1, 1)) assert hash(output_conversion_image_to_image) != hash(output_conversion_image_to_table) assert hash(output_conversion_image_to_image) != hash(output_conversion_image_to_column) assert hash(output_conversion_image_to_table) != hash(output_conversion_image_to_column) @@ -90,25 +98,25 @@ class TestSizeOf: @pytest.mark.parametrize( "output_conversion_image", [ - OutputConversionImageToColumn(), - OutputConversionImageToTable(), - OutputConversionImageToImage(), + InputConversionImageToColumn(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), ], ) def test_should_size_be_greater_than_normal_object( self, - output_conversion_image: _OutputConversionImage, + output_conversion_image: _InputConversionImage, ) -> None: assert sys.getsizeof(output_conversion_image) > sys.getsizeof(object()) -class TestOutputConversionImageToColumn: +class TestInputConversionImageToColumn: def test_should_raise_if_column_name_not_set(self) -> None: with pytest.raises( ValueError, match=r"The column_name is not set. The data can only be converted if the column_name is provided as `str` in the kwargs.", ): - OutputConversionImageToColumn()._data_conversion( + InputConversionImageToColumn(ImageSize(1, 1, 1))._data_conversion_output( input_data=_SingleSizeImageList(), output_data=torch.empty(1), one_hot_encoder=OneHotEncoder(), @@ -119,20 +127,20 @@ def test_should_raise_if_one_hot_encoder_not_set(self) -> None: ValueError, match=r"The one_hot_encoder is not set. The data can only be converted if the one_hot_encoder is provided as `OneHotEncoder` in the kwargs.", ): - OutputConversionImageToColumn()._data_conversion( + InputConversionImageToColumn(ImageSize(1, 1, 1))._data_conversion_output( input_data=_SingleSizeImageList(), output_data=torch.empty(1), column_name="column_name", ) -class TestOutputConversionImageToTable: +class TestInputConversionImageToTable: def test_should_raise_if_column_names_not_set(self) -> None: with pytest.raises( ValueError, match=r"The column_names are not set. The data can only be converted if the column_names are provided as `list\[str\]` in the kwargs.", ): - OutputConversionImageToTable()._data_conversion( + InputConversionImageToTable(ImageSize(1, 1, 1))._data_conversion_output( input_data=_SingleSizeImageList(), output_data=torch.empty(1), ) diff --git a/tests/safeds/ml/nn/converters/test_input_converter_time_series.py b/tests/safeds/ml/nn/converters/test_input_converter_time_series.py index 3e43eb72f..d0e46a8a9 100644 --- a/tests/safeds/ml/nn/converters/test_input_converter_time_series.py +++ b/tests/safeds/ml/nn/converters/test_input_converter_time_series.py @@ -1,10 +1,13 @@ +import sys + +import pytest +import torch from safeds.data.tabular.containers import Table from safeds.ml.nn import ( NeuralNetworkRegressor, ) from safeds.ml.nn.converters import ( InputConversionTimeSeries, - OutputConversionTimeSeries, ) from safeds.ml.nn.layers import ( LSTMLayer, @@ -13,9 +16,8 @@ def test_should_raise_if_is_fitted_is_set_correctly_lstm() -> None: model = NeuralNetworkRegressor( - InputConversionTimeSeries(1, 1), + InputConversionTimeSeries(1, 1, prediction_name="predicted"), [LSTMLayer(input_size=2, output_size=1)], - OutputConversionTimeSeries("predicted"), ) ts = Table.from_dict({"target": [1, 1, 1, 1], "time": [0, 0, 0, 0], "feat": [0, 0, 0, 0]}).to_time_series_dataset( "target", @@ -32,3 +34,107 @@ def test_get_output_config() -> None: it = InputConversionTimeSeries(1, 1) di = it._get_output_configuration() assert di == test_val + + +def test_output_conversion_time_series() -> None: + ot = InputConversionTimeSeries(1, 1) + + with pytest.raises( + ValueError, + match=r"The window_size is not set. The data can only be converted if the window_size is provided as `int` in the kwargs.", + ): + ot._data_conversion_output( + input_data=Table({"a": [1], "c": [1], "b": [1]}).to_time_series_dataset("a", "b"), + output_data=torch.Tensor([0]), + win=2, + kappa=3, + ) + + +def test_output_conversion_time_series_2() -> None: + ot = InputConversionTimeSeries(1, 1) + + with pytest.raises( + ValueError, + match=r"The forecast_horizon is not set. The data can only be converted if the forecast_horizon is provided as `int` in the kwargs.", + ): + ot._data_conversion_output( + input_data=Table({"a": [1], "c": [1], "b": [1]}).to_time_series_dataset("a", "b"), + output_data=torch.Tensor([0]), + window_size=2, + kappa=3, + ) + + +class TestEq: + @pytest.mark.parametrize( + ("output_conversion_ts1", "output_conversion_ts2"), + [ + (InputConversionTimeSeries(1, 1), InputConversionTimeSeries(1, 1)), + ], + ) + def test_should_be_equal( + self, + output_conversion_ts1: InputConversionTimeSeries, + output_conversion_ts2: InputConversionTimeSeries, + ) -> None: + assert output_conversion_ts1 == output_conversion_ts2 + + @pytest.mark.parametrize( + ("output_conversion_ts1", "output_conversion_ts2"), + [ + ( + InputConversionTimeSeries(1, 1), + Table(), + ), + ( + InputConversionTimeSeries(1, 1, prediction_name="2"), + InputConversionTimeSeries(1, 1, prediction_name="1"), + ), + ], + ) + def test_should_not_be_equal( + self, + output_conversion_ts1: InputConversionTimeSeries, + output_conversion_ts2: InputConversionTimeSeries, + ) -> None: + assert output_conversion_ts1 != output_conversion_ts2 + + +class TestHash: + @pytest.mark.parametrize( + ("output_conversion_ts1", "output_conversion_ts2"), + [ + (InputConversionTimeSeries(1, 1), InputConversionTimeSeries(1, 1)), + ], + ) + def test_hash_should_be_equal( + self, + output_conversion_ts1: InputConversionTimeSeries, + output_conversion_ts2: InputConversionTimeSeries, + ) -> None: + assert hash(output_conversion_ts1) == hash(output_conversion_ts2) + + def test_hash_should_not_be_equal(self) -> None: + output_conversion_ts1 = InputConversionTimeSeries(1, 1, prediction_name="1") + output_conversion_ts2 = InputConversionTimeSeries(1, 1, prediction_name="2") + output_conversion_ts3 = InputConversionTimeSeries(1, 1, prediction_name="3") + assert hash(output_conversion_ts1) != hash(output_conversion_ts3) + assert hash(output_conversion_ts2) != hash(output_conversion_ts1) + assert hash(output_conversion_ts3) != hash(output_conversion_ts2) + + +class TestSizeOf: + @pytest.mark.parametrize( + "output_conversion_ts", + [ + InputConversionTimeSeries(1, 1, prediction_name="1"), + InputConversionTimeSeries(1, 1, prediction_name="2"), + InputConversionTimeSeries(1, 1, prediction_name="3"), + ], + ) + def test_should_size_be_greater_than_normal_object( + self, + output_conversion_ts: InputConversionTimeSeries, + ) -> None: + assert sys.getsizeof(output_conversion_ts) > sys.getsizeof(object()) diff --git a/tests/safeds/ml/nn/converters/test_output_converter_time_series.py b/tests/safeds/ml/nn/converters/test_output_converter_time_series.py deleted file mode 100644 index 131c5c369..000000000 --- a/tests/safeds/ml/nn/converters/test_output_converter_time_series.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys - -import pytest -from safeds.data.tabular.containers import Table -from safeds.ml.nn.converters import OutputConversionTimeSeries - - -def test_output_conversion_time_series() -> None: - import torch - - with pytest.raises( - ValueError, - match=r"The window_size is not set. The data can only be converted if the window_size is provided as `int` in the kwargs.", - ): - ot = OutputConversionTimeSeries() - ot._data_conversion( - input_data=Table({"a": [1], "c": [1], "b": [1]}).to_time_series_dataset("a", "b"), - output_data=torch.Tensor([0]), - win=2, - kappa=3, - ) - - -def test_output_conversion_time_series_2() -> None: - import torch - - with pytest.raises( - ValueError, - match=r"The forecast_horizon is not set. The data can only be converted if the forecast_horizon is provided as `int` in the kwargs.", - ): - ot = OutputConversionTimeSeries() - ot._data_conversion( - input_data=Table({"a": [1], "c": [1], "b": [1]}).to_time_series_dataset("a", "b"), - output_data=torch.Tensor([0]), - window_size=2, - kappa=3, - ) - - -class TestEq: - @pytest.mark.parametrize( - ("output_conversion_ts1", "output_conversion_ts2"), - [ - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - ], - ) - def test_should_be_equal( - self, - output_conversion_ts1: OutputConversionTimeSeries, - output_conversion_ts2: OutputConversionTimeSeries, - ) -> None: - assert output_conversion_ts1 == output_conversion_ts2 - - @pytest.mark.parametrize( - ("output_conversion_ts1", "output_conversion_ts2"), - [ - (OutputConversionTimeSeries(), Table()), - (OutputConversionTimeSeries("2"), OutputConversionTimeSeries("1")), - ], - ) - def test_should_not_be_equal( - self, - output_conversion_ts1: OutputConversionTimeSeries, - output_conversion_ts2: OutputConversionTimeSeries, - ) -> None: - assert output_conversion_ts1 != output_conversion_ts2 - - -class TestHash: - @pytest.mark.parametrize( - ("output_conversion_ts1", "output_conversion_ts2"), - [ - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - (OutputConversionTimeSeries(), OutputConversionTimeSeries()), - ], - ) - def test_hash_should_be_equal( - self, - output_conversion_ts1: OutputConversionTimeSeries, - output_conversion_ts2: OutputConversionTimeSeries, - ) -> None: - assert hash(output_conversion_ts1) == hash(output_conversion_ts2) - - def test_hash_should_not_be_equal(self) -> None: - output_conversion_ts1 = OutputConversionTimeSeries("1") - output_conversion_ts2 = OutputConversionTimeSeries("2") - output_conversion_ts3 = OutputConversionTimeSeries("3") - assert hash(output_conversion_ts1) != hash(output_conversion_ts3) - assert hash(output_conversion_ts2) != hash(output_conversion_ts1) - assert hash(output_conversion_ts3) != hash(output_conversion_ts2) - - -class TestSizeOf: - @pytest.mark.parametrize( - "output_conversion_ts", - [ - OutputConversionTimeSeries("1"), - OutputConversionTimeSeries("2"), - OutputConversionTimeSeries("3"), - ], - ) - def test_should_size_be_greater_than_normal_object( - self, - output_conversion_ts: OutputConversionTimeSeries, - ) -> None: - assert sys.getsizeof(output_conversion_ts) > sys.getsizeof(object()) diff --git a/tests/safeds/ml/nn/test_cnn_workflow.py b/tests/safeds/ml/nn/test_cnn_workflow.py index f42adad37..b77c4c7b9 100644 --- a/tests/safeds/ml/nn/test_cnn_workflow.py +++ b/tests/safeds/ml/nn/test_cnn_workflow.py @@ -14,10 +14,9 @@ NeuralNetworkRegressor, ) from safeds.ml.nn.converters import ( - InputConversionImage, - OutputConversionImageToColumn, - OutputConversionImageToImage, - OutputConversionImageToTable, + InputConversionImageToColumn, + InputConversionImageToImage, + InputConversionImageToTable, ) from safeds.ml.nn.layers import ( AveragePooling2DLayer, @@ -86,18 +85,11 @@ def test_should_train_and_predict_model( num_of_classes: int = image_dataset.output_size if isinstance(image_dataset.output_size, int) else 0 layers = [Convolutional2DLayer(1, 2), MaxPooling2DLayer(10), FlattenLayer(), ForwardLayer(num_of_classes)] nn_original = NeuralNetworkClassifier( - InputConversionImage(image_dataset.input_size), + InputConversionImageToTable(image_dataset.input_size), layers, - OutputConversionImageToTable(), ) nn = nn_original.fit(image_dataset, epoch_size=2) - assert str(nn_original._model.state_dict().values()) != str(nn._model.state_dict().values()) - assert not torch.all( - torch.eq( - nn_original._model.state_dict()["_pytorch_layers.3._layer.bias"], - nn._model.state_dict()["_pytorch_layers.3._layer.bias"], - ), - ).item() + assert nn_original._model is not nn._model prediction: ImageDataset = nn.predict(image_dataset.get_input()) assert one_hot_encoder.inverse_transform(prediction.get_output()) == Table({"class": prediction_label}) assert prediction._output._tensor.device == _get_device() @@ -152,18 +144,11 @@ def test_should_train_and_predict_model( layers = [Convolutional2DLayer(1, 2), AveragePooling2DLayer(10), FlattenLayer(), ForwardLayer(num_of_classes)] nn_original = NeuralNetworkClassifier( - InputConversionImage(image_dataset.input_size), + InputConversionImageToColumn(image_dataset.input_size), layers, - OutputConversionImageToColumn(), ) nn = nn_original.fit(image_dataset, epoch_size=2) - assert str(nn_original._model.state_dict().values()) != str(nn._model.state_dict().values()) - assert not torch.all( - torch.eq( - nn_original._model.state_dict()["_pytorch_layers.3._layer.bias"], - nn._model.state_dict()["_pytorch_layers.3._layer.bias"], - ), - ).item() + assert nn_original._model is not nn._model prediction: ImageDataset = nn.predict(image_dataset.get_input()) assert prediction.get_output() == Column("class", prediction_label) assert prediction._output._tensor.device == _get_device() @@ -200,18 +185,11 @@ def test_should_train_and_predict_model( ConvolutionalTranspose2DLayer(4, 2), ] nn_original = NeuralNetworkRegressor( - InputConversionImage(image_dataset.input_size), + InputConversionImageToImage(image_dataset.input_size), layers, - OutputConversionImageToImage(), ) nn = nn_original.fit(image_dataset, epoch_size=20) - assert str(nn_original._model.state_dict().values()) != str(nn._model.state_dict().values()) - assert not torch.all( - torch.eq( - nn_original._model.state_dict()["_pytorch_layers.3._layer.bias"], - nn._model.state_dict()["_pytorch_layers.3._layer.bias"], - ), - ).item() + assert nn_original._model is not nn._model prediction = nn.predict(image_dataset.get_input()) assert isinstance(prediction.get_output(), ImageList) assert prediction._output._tensor.device == _get_device() @@ -248,18 +226,11 @@ def test_should_train_and_predict_model_variable_image_size( ConvolutionalTranspose2DLayer(4, 2), ] nn_original = NeuralNetworkRegressor( - InputConversionImage(VariableImageSize.from_image_size(image_dataset.input_size)), + InputConversionImageToImage(VariableImageSize.from_image_size(image_dataset.input_size)), layers, - OutputConversionImageToImage(), ) nn = nn_original.fit(image_dataset, epoch_size=20) - assert str(nn_original._model.state_dict().values()) != str(nn._model.state_dict().values()) - assert not torch.all( - torch.eq( - nn_original._model.state_dict()["_pytorch_layers.3._layer.bias"], - nn._model.state_dict()["_pytorch_layers.3._layer.bias"], - ), - ).item() + assert nn_original._model is not nn._model prediction = nn.predict( image_dataset.get_input().resize( image_dataset.input_size.width * multi_width, diff --git a/tests/safeds/ml/nn/test_forward_workflow.py b/tests/safeds/ml/nn/test_forward_workflow.py index 502af13fb..e622bd073 100644 --- a/tests/safeds/ml/nn/test_forward_workflow.py +++ b/tests/safeds/ml/nn/test_forward_workflow.py @@ -7,7 +7,6 @@ ) from safeds.ml.nn.converters import ( InputConversionTable, - OutputConversionTable, ) from safeds.ml.nn.layers import ( ForwardLayer, @@ -35,11 +34,11 @@ def test_forward_model(device: Device) -> None: _, train_table = ss.fit_and_transform(train_table, ["value"]) _, test_table = ss.fit_and_transform(test_table, ["value"]) model = NeuralNetworkRegressor( - InputConversionTable(), + InputConversionTable(prediction_name="predicted"), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable("predicted"), ) fitted_model = model.fit(train_table.to_tabular_dataset("target"), epoch_size=1, learning_rate=0.01) fitted_model.predict(test_table.remove_columns_except(["value"])) - assert model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() + assert fitted_model._model is not None + assert fitted_model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() diff --git a/tests/safeds/ml/nn/test_lstm_workflow.py b/tests/safeds/ml/nn/test_lstm_workflow.py index 991019aeb..f0d19a8ae 100644 --- a/tests/safeds/ml/nn/test_lstm_workflow.py +++ b/tests/safeds/ml/nn/test_lstm_workflow.py @@ -7,7 +7,6 @@ ) from safeds.ml.nn.converters import ( InputConversionTimeSeries, - OutputConversionTimeSeries, ) from safeds.ml.nn.layers import ( ForwardLayer, @@ -30,11 +29,11 @@ def test_lstm_model(device: Device) -> None: train_table, test_table = table.split_rows(0.8) model = NeuralNetworkRegressor( - InputConversionTimeSeries(window_size=7, forecast_horizon=12), + InputConversionTimeSeries(window_size=7, forecast_horizon=12, prediction_name="predicted"), [ForwardLayer(input_size=7, output_size=256), LSTMLayer(input_size=256, output_size=1)], - OutputConversionTimeSeries("predicted"), ) trained_model = model.fit(train_table.to_time_series_dataset("value", "date"), epoch_size=1) trained_model.predict(test_table.to_time_series_dataset("value", "date")) - assert model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() + assert trained_model._model is not None + assert trained_model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device() diff --git a/tests/safeds/ml/nn/test_model.py b/tests/safeds/ml/nn/test_model.py index 394e34dad..d45e11f6f 100644 --- a/tests/safeds/ml/nn/test_model.py +++ b/tests/safeds/ml/nn/test_model.py @@ -15,14 +15,11 @@ ) from safeds.ml.nn.converters import ( InputConversion, - InputConversionImage, + InputConversionImageToColumn, + InputConversionImageToImage, + InputConversionImageToTable, InputConversionTable, - OutputConversion, - OutputConversionImageToImage, - OutputConversionImageToTable, - OutputConversionTable, ) -from safeds.ml.nn.converters._output_converter_image import OutputConversionImageToColumn from safeds.ml.nn.layers import ( AveragePooling2DLayer, Convolutional2DLayer, @@ -41,11 +38,10 @@ @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestClassificationModel: - @pytest.mark.parametrize( "input_size", [ - 1, + None, ], ) def test_should_return_input_size(self, input_size: int, device: Device) -> None: @@ -54,7 +50,6 @@ def test_should_return_input_size(self, input_size: int, device: Device) -> None NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(1, input_size)], - OutputConversionTable(), ).input_size == input_size ) @@ -72,7 +67,6 @@ def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int, device: NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(1, 1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"), epoch_size=epoch_size, @@ -91,7 +85,6 @@ def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int, device: NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -102,7 +95,6 @@ def test_should_raise_if_fit_function_returns_wrong_datatype(self, device: Devic fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1], "b": [0]}).to_tabular_dataset("a"), ) @@ -121,7 +113,6 @@ def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_siz fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1, 0, 1, 0, 1, 0], "b": [0, 1, 0, 12, 3, 3]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -146,7 +137,6 @@ def test_should_raise_if_predict_function_returns_wrong_datatype_for_multiclass_ fitted_model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), ForwardLayer(output_size=3)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [0, 1, 2], "b": [0, 15, 51]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -154,7 +144,6 @@ def test_should_raise_if_predict_function_returns_wrong_datatype_for_multiclass_ NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=8), LSTMLayer(output_size=3)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [0, 1, 2], "b": [0, 15, 51]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -168,7 +157,6 @@ def test_should_raise_if_model_has_not_been_fitted(self, device: Device) -> None NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).predict( Table.from_dict({"a": [1]}), ) @@ -178,12 +166,10 @@ def test_should_raise_if_is_fitted_is_set_correctly_for_binary_classification(se model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) model_2 = NeuralNetworkClassifier( InputConversionTable(), [LSTMLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) assert not model.is_fitted assert not model_2.is_fitted @@ -201,12 +187,10 @@ def test_should_raise_if_is_fitted_is_set_correctly_for_multiclass_classificatio model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], - OutputConversionTable(), ) model_2 = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), LSTMLayer(output_size=3)], - OutputConversionTable(), ) assert not model.is_fitted assert not model_2.is_fitted @@ -224,7 +208,6 @@ def test_should_raise_if_test_features_mismatch(self, device: Device) -> None: model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], - OutputConversionTable(), ) model = model.fit( Table.from_dict({"a": [1, 0, 2], "b": [0, 15, 5]}).to_tabular_dataset("a"), @@ -242,7 +225,6 @@ def test_should_raise_if_train_features_mismatch(self, device: Device) -> None: model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=1)], - OutputConversionTable(), ) learned_model = model.fit( Table.from_dict({"a": [0.1, 0, 0.2], "b": [0, 0.15, 0.5]}).to_tabular_dataset("b"), @@ -258,7 +240,6 @@ def test_should_raise_if_table_size_and_input_size_mismatch(self, device: Device model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], - OutputConversionTable(), ) with pytest.raises( InputSizeError, @@ -272,7 +253,6 @@ def test_should_raise_if_fit_doesnt_batch_callback(self, device: Device) -> None model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) class Test: @@ -295,7 +275,6 @@ def test_should_raise_if_fit_doesnt_epoch_callback(self, device: Device) -> None model = NeuralNetworkClassifier( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) class Test: @@ -314,198 +293,146 @@ def callback_was_called(self) -> bool: assert obj.callback_was_called() is True @pytest.mark.parametrize( - ("input_conversion", "layers", "output_conversion", "error_msg"), + ("input_conversion", "layers", "error_msg"), [ - ( - InputConversionTable(), - [FlattenLayer()], - OutputConversionImageToTable(), - r"The defined model uses an output conversion for images but no input conversion for images.", - ), - ( - InputConversionTable(), - [FlattenLayer()], - OutputConversionImageToColumn(), - r"The defined model uses an output conversion for images but no input conversion for images.", - ), - ( - InputConversionTable(), - [FlattenLayer()], - OutputConversionImageToImage(), - r"A NeuralNetworkClassifier cannot be used with images as output.", - ), ( InputConversionTable(), [Convolutional2DLayer(1, 1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [MaxPooling2DLayer(1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [AveragePooling2DLayer(1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [FlattenLayer()], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), - [FlattenLayer()], - OutputConversionTable(), - r"The defined model uses an input conversion for images but no output conversion for images.", - ), - ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [Convolutional2DLayer(1, 1)], - OutputConversionImageToTable(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [Convolutional2DLayer(1, 1)], - OutputConversionImageToColumn(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionImageToTable(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionImageToColumn(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [MaxPooling2DLayer(1)], - OutputConversionImageToTable(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [MaxPooling2DLayer(1)], - OutputConversionImageToColumn(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [AveragePooling2DLayer(1)], - OutputConversionImageToTable(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [AveragePooling2DLayer(1)], - OutputConversionImageToColumn(), r"The output data would be 2-dimensional but the provided output conversion uses 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer(), Convolutional2DLayer(1, 1)], - OutputConversionImageToTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer(), Convolutional2DLayer(1, 1)], - OutputConversionImageToColumn(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer(), ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionImageToTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer(), ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionImageToColumn(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer(), MaxPooling2DLayer(1)], - OutputConversionImageToTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer(), MaxPooling2DLayer(1)], - OutputConversionImageToColumn(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer(), AveragePooling2DLayer(1)], - OutputConversionImageToTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer(), AveragePooling2DLayer(1)], - OutputConversionImageToColumn(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer(), FlattenLayer()], - OutputConversionImageToTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer(), FlattenLayer()], - OutputConversionImageToColumn(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [ForwardLayer(1)], - OutputConversionImageToTable(), r"The 2-dimensional data has to be flattened before using a 1-dimensional layer.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [ForwardLayer(1)], - OutputConversionImageToColumn(), r"The 2-dimensional data has to be flattened before using a 1-dimensional layer.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [], - OutputConversionImageToTable(), r"You need to provide at least one layer to a neural network.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [], - OutputConversionImageToColumn(), r"You need to provide at least one layer to a neural network.", ), ( - InputConversionImage(VariableImageSize(1, 1, 1)), + InputConversionImageToColumn(VariableImageSize(1, 1, 1)), [FlattenLayer()], - OutputConversionImageToColumn(), r"A NeuralNetworkClassifier cannot be used with a InputConversionImage that uses a VariableImageSize.", ), ], @@ -514,22 +441,20 @@ def test_should_raise_if_model_has_invalid_structure( self, input_conversion: InputConversion, layers: list[Layer], - output_conversion: OutputConversion, error_msg: str, device: Device, ) -> None: configure_test_with_device(device) with pytest.raises(InvalidModelStructureError, match=error_msg): - NeuralNetworkClassifier(input_conversion, layers, output_conversion) + NeuralNetworkClassifier(input_conversion, layers) @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) class TestRegressionModel: - @pytest.mark.parametrize( "input_size", [ - 1, + None, ], ) def test_should_return_input_size(self, input_size: int, device: Device) -> None: @@ -538,7 +463,6 @@ def test_should_return_input_size(self, input_size: int, device: Device) -> None NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(1, input_size)], - OutputConversionTable(), ).input_size == input_size ) @@ -556,7 +480,6 @@ def test_should_raise_if_epoch_size_out_of_bounds(self, epoch_size: int, device: NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"), epoch_size=epoch_size, @@ -575,7 +498,6 @@ def test_should_raise_if_batch_size_out_of_bounds(self, batch_size: int, device: NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -594,7 +516,6 @@ def test_should_raise_if_fit_function_returns_wrong_datatype(self, batch_size: i fitted_model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1, 0, 1], "b": [2, 3, 4]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -614,7 +535,6 @@ def test_should_raise_if_predict_function_returns_wrong_datatype(self, batch_siz fitted_model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).fit( Table.from_dict({"a": [1, 0, 1], "b": [2, 3, 4]}).to_tabular_dataset("a"), batch_size=batch_size, @@ -628,7 +548,6 @@ def test_should_raise_if_model_has_not_been_fitted(self, device: Device) -> None NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ).predict( Table.from_dict({"a": [1]}), ) @@ -638,7 +557,6 @@ def test_should_raise_if_is_fitted_is_set_correctly(self, device: Device) -> Non model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) assert not model.is_fitted model = model.fit( @@ -651,7 +569,6 @@ def test_should_raise_if_test_features_mismatch(self, device: Device) -> None: model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) model = model.fit( Table.from_dict({"a": [1, 0, 2], "b": [0, 15, 5]}).to_tabular_dataset("a"), @@ -669,7 +586,6 @@ def test_should_raise_if_train_features_mismatch(self, device: Device) -> None: model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) trained_model = model.fit( Table.from_dict({"a": [1, 0, 2], "b": [0, 15, 5]}).to_tabular_dataset("b"), @@ -687,7 +603,6 @@ def test_should_raise_if_table_size_and_input_size_mismatch(self, device: Device model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1), ForwardLayer(output_size=3)], - OutputConversionTable(), ) with pytest.raises( InputSizeError, @@ -701,7 +616,6 @@ def test_should_raise_if_fit_doesnt_batch_callback(self, device: Device) -> None model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) class Test: @@ -724,7 +638,6 @@ def test_should_raise_if_fit_doesnt_epoch_callback(self, device: Device) -> None model = NeuralNetworkRegressor( InputConversionTable(), [ForwardLayer(input_size=1, output_size=1)], - OutputConversionTable(), ) class Test: @@ -743,114 +656,86 @@ def callback_was_called(self) -> bool: assert obj.callback_was_called() is True @pytest.mark.parametrize( - ("input_conversion", "layers", "output_conversion", "error_msg"), + ("input_conversion", "layers", "error_msg"), [ - ( - InputConversionTable(), - [FlattenLayer()], - OutputConversionImageToImage(), - r"The defined model uses an output conversion for images but no input conversion for images.", - ), ( InputConversionTable(), [Convolutional2DLayer(1, 1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [MaxPooling2DLayer(1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [AveragePooling2DLayer(1)], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( InputConversionTable(), [FlattenLayer()], - OutputConversionTable(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), - [FlattenLayer()], - OutputConversionTable(), - r"The defined model uses an input conversion for images but no output conversion for images.", - ), - ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer()], - OutputConversionImageToImage(), r"The output data would be 1-dimensional but the provided output conversion uses 2-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), ForwardLayer(1)], - OutputConversionImageToImage(), r"The output data would be 1-dimensional but the provided output conversion uses 2-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), Convolutional2DLayer(1, 1)], - OutputConversionImageToImage(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), ConvolutionalTranspose2DLayer(1, 1)], - OutputConversionImageToImage(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), MaxPooling2DLayer(1)], - OutputConversionImageToImage(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), AveragePooling2DLayer(1)], - OutputConversionImageToImage(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [FlattenLayer(), FlattenLayer()], - OutputConversionImageToImage(), r"You cannot use a 2-dimensional layer with 1-dimensional data.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [ForwardLayer(1)], - OutputConversionImageToImage(), r"The 2-dimensional data has to be flattened before using a 1-dimensional layer.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToImage(ImageSize(1, 1, 1)), [], - OutputConversionImageToImage(), r"You need to provide at least one layer to a neural network.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToTable(ImageSize(1, 1, 1)), [FlattenLayer()], - OutputConversionImageToTable(), r"A NeuralNetworkRegressor cannot be used with images as input and 1-dimensional data as output.", ), ( - InputConversionImage(ImageSize(1, 1, 1)), + InputConversionImageToColumn(ImageSize(1, 1, 1)), [FlattenLayer()], - OutputConversionImageToColumn(), r"A NeuralNetworkRegressor cannot be used with images as input and 1-dimensional data as output.", ), ], @@ -859,10 +744,9 @@ def test_should_raise_if_model_has_invalid_structure( self, input_conversion: InputConversion, layers: list[Layer], - output_conversion: OutputConversion, error_msg: str, device: Device, ) -> None: configure_test_with_device(device) with pytest.raises(InvalidModelStructureError, match=error_msg): - NeuralNetworkRegressor(input_conversion, layers, output_conversion) + NeuralNetworkRegressor(input_conversion, layers) diff --git a/tests/safeds/ml/nn/typing/test_model_image_size.py b/tests/safeds/ml/nn/typing/test_model_image_size.py index eb3a6b0ea..423b42aee 100644 --- a/tests/safeds/ml/nn/typing/test_model_image_size.py +++ b/tests/safeds/ml/nn/typing/test_model_image_size.py @@ -37,7 +37,6 @@ def test_should_create(self, resource_path: str, device: Device, image_size_clas class TestFromImageSize: - def test_should_create(self) -> None: constant_image_size = ConstantImageSize(1, 2, 3) variable_image_size = VariableImageSize(1, 2, 3) @@ -157,7 +156,6 @@ def test_hash_should_not_be_equal_different_model_image_sizes(self) -> None: class TestSizeOf: - @pytest.mark.parametrize( "image_size_class", [ @@ -171,7 +169,6 @@ def test_should_size_be_greater_than_normal_object(self, image_size_class: type[ class TestStr: - @pytest.mark.parametrize( "image_size_class", [