Skip to content

Commit

Permalink
feat: activation function for forward layer (#891)
Browse files Browse the repository at this point in the history
Closes #889

### Summary of Changes

added optional activation function parameter to the forward layer

---------

Co-authored-by: megalinter-bot <[email protected]>
Co-authored-by: Alexander Gréus <[email protected]>
  • Loading branch information
3 people authored Jul 15, 2024
1 parent c1f66e5 commit 5b5bb3f
Show file tree
Hide file tree
Showing 38 changed files with 128 additions and 58 deletions.
78 changes: 39 additions & 39 deletions docs/tutorials/time_series_forecasting.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nav:
- Regression: tutorials/regression.ipynb
- Machine Learning: tutorials/machine_learning.ipynb
- Image Classification with Convolutional Neural Networks: tutorials/convolutional_neural_network_for_image_classification.ipynb
- Time series forecasting: tutorials/time_series_forecasting.ipynb
- API Reference: reference/
- Glossary: glossary.md
- Development:
Expand Down
2 changes: 1 addition & 1 deletion src/safeds/data/tabular/plotting/_table_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ def moving_average_plot(
ylabel=y_name,
)
ax.legend()
if self._table.get_column(x_name).is_temporal:
if self._table.get_column(x_name).is_temporal and self._table.get_column(x_name).row_count < 9:
ax.set_xticks(x_data) # Set x-ticks to the x data points
ax.set_xticks(ax.get_xticks())
ax.set_xticklabels(
Expand Down
31 changes: 25 additions & 6 deletions src/safeds/ml/nn/layers/_forward_layer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal

from safeds._utils import _structural_hash
from safeds._validation import _check_bounds, _ClosedBound
Expand All @@ -21,15 +21,23 @@ class ForwardLayer(Layer):
----------
neuron_count:
The number of neurons in this layer
overwrite_activation_function:
The activation function used in the forward layer, if not set the activation will be set automatically
Raises
------
OutOfBoundsError
If input_size < 1
If output_size < 1
ValueError
If the given activation function does not exist
"""

def __init__(self, neuron_count: int | Choice[int]) -> None:
def __init__(
self,
neuron_count: int | Choice[int],
overwrite_activation_function: Literal["sigmoid", "relu", "softmax", "none", "notset"] = "notset",
) -> None:
if isinstance(neuron_count, Choice):
for val in neuron_count:
_check_bounds("neuron_count", val, lower_bound=_ClosedBound(1))
Expand All @@ -38,6 +46,7 @@ def __init__(self, neuron_count: int | Choice[int]) -> None:

self._input_size: int | None = None
self._output_size = neuron_count
self._activation_function: str = overwrite_activation_function

def _get_internal_layer(self, **kwargs: Any) -> nn.Module:
assert not self._contains_choices()
Expand All @@ -48,8 +57,10 @@ def _get_internal_layer(self, **kwargs: Any) -> nn.Module:
raise ValueError(
"The activation_function is not set. The internal layer can only be created when the activation_function is provided in the kwargs.",
)
else:
elif self._activation_function == "notset":
activation_function: str = kwargs["activation_function"]
else:
activation_function = self._activation_function

if self._input_size is None:
raise ValueError("The input_size is not yet set.")
Expand Down Expand Up @@ -101,16 +112,24 @@ def _get_layers_for_all_choices(self) -> list[ForwardLayer]:
return layers

def __hash__(self) -> int:
return _structural_hash(self._input_size, self._output_size)
return _structural_hash(self._input_size, self._output_size, self._activation_function)

def __eq__(self, other: object) -> bool:
if not isinstance(other, ForwardLayer):
return NotImplemented
if self is other:
return True
return self._input_size == other._input_size and self._output_size == other._output_size
return (
self._input_size == other._input_size
and self._output_size == other._output_size
and self._activation_function == other._activation_function
)

def __sizeof__(self) -> int:
import sys

return sys.getsizeof(self._input_size) + sys.getsizeof(self._output_size)
return (
sys.getsizeof(self._input_size)
+ sys.getsizeof(self._output_size)
+ sys.getsizeof(self._activation_function)
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
3 changes: 2 additions & 1 deletion tests/safeds/data/image/containers/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,8 @@ def test_should_raise(self, resource_path: str, device: Device) -> None:
image.adjust_brightness(-1)


@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
class TestAddNoise:
@pytest.mark.parametrize("device", [device_cpu], ids=["cpu"])
@pytest.mark.parametrize(
"standard_deviation",
[
Expand Down Expand Up @@ -690,6 +690,7 @@ def test_should_add_noise(
assert image_noise == snapshot_png_image
_assert_width_height_channel(image, image_noise)

@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
@pytest.mark.parametrize(
"standard_deviation",
[-1],
Expand Down
4 changes: 3 additions & 1 deletion tests/safeds/data/image/containers/test_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from tests.helpers import (
configure_test_with_device,
device_cpu,
get_devices,
get_devices_ids,
grayscale_jpg_path,
Expand Down Expand Up @@ -973,8 +974,8 @@ def test_all_transform_methods(
assert image_list_original == image_list_clone


@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
class TestTransforms:
@pytest.mark.parametrize("device", [device_cpu], ids=["cpu"])
@pytest.mark.parametrize(
"resource_path",
[images_all(), [plane_png_path, plane_jpg_path] * 2],
Expand Down Expand Up @@ -1007,6 +1008,7 @@ def test_should_add_noise(
assert image_list_original is not image_list_clone
assert image_list_original == image_list_clone

@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
@pytest.mark.parametrize(
"channel_in",
[1, 3, 4],
Expand Down
23 changes: 22 additions & 1 deletion tests/safeds/data/tabular/plotting/test_moving_average_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,29 @@
"A",
2,
),
(
Table(
{
"time": [
datetime.date(2022, 1, 9),
datetime.date(2022, 1, 10),
datetime.date(2022, 1, 11),
datetime.date(2022, 1, 12),
datetime.date(2022, 1, 13),
datetime.date(2022, 1, 14),
datetime.date(2022, 1, 15),
datetime.date(2022, 1, 16),
datetime.date(2022, 1, 17),
],
"A": [10, 5, 20, 2, 15, 1, 10, 5, 20],
},
),
"time",
"A",
2,
),
],
ids=["numerical", "date grouped", "date"],
ids=["numerical", "date grouped", "date", "more than 8"],
)
def test_should_match_snapshot(
table: Table,
Expand Down
Diff not rendered.
30 changes: 29 additions & 1 deletion tests/safeds/ml/nn/layers/test_forward_layer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from typing import Any
from typing import Any, Literal

import pytest
from safeds.data.image.typing import ImageSize
Expand Down Expand Up @@ -193,6 +193,34 @@ def test_should_assert_that_layer_size_is_greater_than_normal_object(layer: Forw
assert sys.getsizeof(layer) > sys.getsizeof(object())


@pytest.mark.parametrize(
("activation_function", "expected_activation_function"),
[
("sigmoid", nn.Sigmoid),
("relu", nn.ReLU),
("softmax", nn.Softmax),
("none", None),
],
ids=["sigmoid", "relu", "softmax", "none"],
)
def test_should_set_activation_function(
activation_function: Literal["sigmoid", "relu", "softmax", "none"],
expected_activation_function: type | None,
) -> None:
forward_layer: ForwardLayer = ForwardLayer(1, overwrite_activation_function=activation_function)
assert forward_layer is not None
forward_layer._input_size = 1
internal_layer = forward_layer._get_internal_layer(
activation_function="relu",
)
# check if the type gets overwritten by constructor
assert (
internal_layer._fn is None
if expected_activation_function is None
else isinstance(internal_layer._fn, expected_activation_function)
)


def test_should_get_all_possible_combinations_of_forward_layer() -> None:
layer = ForwardLayer(Choice(1, 2))
possible_layers = layer._get_layers_for_all_choices()
Expand Down
14 changes: 6 additions & 8 deletions tests/safeds/ml/nn/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@
class TestClassificationModel:
class TestFit:
def test_should_return_input_size(self, device: Device) -> None:
configure_test_with_device(device)
model = NeuralNetworkClassifier(
InputConversionTable(),
[ForwardLayer(neuron_count=1)],
).fit(
Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"),
)

device.type # noqa: B018
assert model.input_size == 1

def test_should_raise_if_epoch_size_out_of_bounds(self, device: Device) -> None:
Expand Down Expand Up @@ -258,14 +257,14 @@ def callback_was_called(self) -> bool:

class TestFitByExhaustiveSearch:
def test_should_return_input_size(self, device: Device) -> None:
configure_test_with_device(device)
model = NeuralNetworkClassifier(
InputConversionTable(),
[ForwardLayer(neuron_count=Choice(2, 4)), ForwardLayer(1)],
).fit_by_exhaustive_search(
Table.from_dict({"a": [1, 2, 3, 4], "b": [0, 1, 0, 1]}).to_tabular_dataset("b"),
"accuracy",
)
device.type # noqa: B018
assert model.input_size == 1

def test_should_raise_if_epoch_size_out_of_bounds_when_fitting_by_exhaustive_search(
Expand Down Expand Up @@ -337,14 +336,14 @@ def test_should_assert_that_is_fitted_is_set_correctly_and_check_return_type(
positive_class: Any,
device: Device,
) -> None:
configure_test_with_device(device)
model = NeuralNetworkClassifier(InputConversionTable(), [ForwardLayer(Choice(2, 4)), ForwardLayer(1)])
assert not model.is_fitted
fitted_model = model.fit_by_exhaustive_search(
Table.from_dict({"a": [1, 2, 3, 4], "b": [0, 1, 0, 1]}).to_tabular_dataset("b"),
optimization_metric=metric,
positive_class=positive_class,
)
device.type # noqa: B018
assert fitted_model.is_fitted
assert isinstance(fitted_model, NeuralNetworkClassifier)

Expand Down Expand Up @@ -614,14 +613,13 @@ def test_should_be_pickleable(self, device: Device) -> None:
class TestRegressionModel:
class TestFit:
def test_should_return_input_size(self, device: Device) -> None:
configure_test_with_device(device)
model = NeuralNetworkRegressor(
InputConversionTable(),
[ForwardLayer(neuron_count=1)],
).fit(
Table.from_dict({"a": [1], "b": [2]}).to_tabular_dataset("a"),
)

device.type # noqa: B018
assert model.input_size == 1

def test_should_raise_if_epoch_size_out_of_bounds(self, device: Device) -> None:
Expand Down Expand Up @@ -806,14 +804,14 @@ def callback_was_called(self) -> bool:

class TestFitByExhaustiveSearch:
def test_should_return_input_size(self, device: Device) -> None:
configure_test_with_device(device)
model = NeuralNetworkRegressor(
InputConversionTable(),
[ForwardLayer(neuron_count=Choice(2, 4)), ForwardLayer(1)],
).fit_by_exhaustive_search(
Table.from_dict({"a": [1, 2, 3, 4], "b": [1.0, 2.0, 3.0, 4.0]}).to_tabular_dataset("b"),
"mean_squared_error",
)
device.type # noqa: B018
assert model.input_size == 1

def test_should_raise_if_epoch_size_out_of_bounds_when_fitting_by_exhaustive_search(
Expand Down Expand Up @@ -882,13 +880,13 @@ def test_should_assert_that_is_fitted_is_set_correctly_and_check_return_type(
],
device: Device,
) -> None:
configure_test_with_device(device)
model = NeuralNetworkRegressor(InputConversionTable(), [ForwardLayer(Choice(2, 4)), ForwardLayer(1)])
assert not model.is_fitted
fitted_model = model.fit_by_exhaustive_search(
Table.from_dict({"a": [1, 2, 3, 4], "b": [1.0, 2.0, 3.0, 4.0]}).to_tabular_dataset("b"),
optimization_metric=metric,
)
device.type # noqa: B018
assert fitted_model.is_fitted
assert isinstance(fitted_model, NeuralNetworkRegressor)

Expand Down

0 comments on commit 5b5bb3f

Please sign in to comment.