Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Adds Dropout Layer #868

Merged
merged 25 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f0a81fd
adds InternalLayer and starts to add DropoutLayer
TellemHD Jun 21, 2024
c6fccd7
hopefully finishes DropoutLayer
TellemHD Jun 21, 2024
f916761
Merge branch 'main' into 848-dropout-layer
TellemHD Jun 28, 2024
eded7e9
edits DropoutLayer, defines DropoutLayer i ninit for better importSyn…
TellemHD Jun 28, 2024
0f8e1c1
adds even more UnitTests
TellemHD Jun 28, 2024
e7258a8
fixes linters
TellemHD Jun 28, 2024
0505086
hopefully fixes linters again
TellemHD Jun 28, 2024
bd97f42
style: fix linter issues
lars-reimann Jun 28, 2024
90ff801
fixes spelling mistake and Linter
TellemHD Jun 28, 2024
aaedadb
adds UnitTest and edits getSize
TellemHD Jun 28, 2024
7a2edfa
adds dropout-workflow-Tests
TellemHD Jun 28, 2024
a1de3c8
hopefully fixes all linters now
TellemHD Jun 28, 2024
d58ca8d
fixes it perhaps
TellemHD Jun 28, 2024
f18d8de
style: apply automated linter fixes
megalinter-bot Jun 28, 2024
1dcad04
fixes naming and some comments
TellemHD Jun 28, 2024
6b2b2d2
adds prepared data and edits UnitTest as wishied in the comment
TellemHD Jun 28, 2024
cac04f9
Merge branch 'main' into 848-dropout-layer
TellemHD Jun 28, 2024
b8680d9
adds prepared data and edits UnitTest as wishied in the comment
TellemHD Jun 28, 2024
ef070f2
removes unneccesary imports
TellemHD Jun 28, 2024
f38e2a4
Merge branch '848-dropout-layer' of https://github.com/Safe-DS/Librar…
TellemHD Jun 28, 2024
ccdc0e9
docs: improve docstring
lars-reimann Jun 29, 2024
4c1017a
fix: use closed bounds, so boundary values are allowed
lars-reimann Jun 29, 2024
f0ef2ff
test: hardcode data
lars-reimann Jun 29, 2024
bc7dcde
fix: include probability when computing equality or hash
lars-reimann Jun 29, 2024
9425a3a
Merge branch 'main' into 848-dropout-layer
lars-reimann Jun 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/safeds/ml/nn/layers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

if TYPE_CHECKING:
from ._convolutional2d_layer import Convolutional2DLayer, ConvolutionalTranspose2DLayer
from ._dropout_layer import DropoutLayer
from ._flatten_layer import FlattenLayer
from ._forward_layer import ForwardLayer
from ._gru_layer import GRULayer
Expand All @@ -25,6 +26,7 @@
"GRULayer": "._gru_layer:GRULayer",
"AveragePooling2DLayer": "._pooling2d_layer:AveragePooling2DLayer",
"MaxPooling2DLayer": "._pooling2d_layer:MaxPooling2DLayer",
"DropoutLayer": "._dropout_layer:DropoutLayer",
},
)

Expand All @@ -38,4 +40,5 @@
"GRULayer",
"AveragePooling2DLayer",
"MaxPooling2DLayer",
"DropoutLayer",
]
100 changes: 100 additions & 0 deletions src/safeds/ml/nn/layers/_dropout_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any

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

from ._layer import Layer

if TYPE_CHECKING:
from torch import nn


class DropoutLayer(Layer):
"""
Create a dropout Layer.

Parameters
----------
probability:
The probability of which the input neuron becomes an output neuron
TellemHD marked this conversation as resolved.
Show resolved Hide resolved

Raises
------
OutOfBoundsError
If probability < 0
If probability > 1
"""

def __init__(self, probability: float):
_check_bounds("probability", probability, lower_bound=_OpenBound(0), upper_bound=_OpenBound(1))
self.probability = probability
TellemHD marked this conversation as resolved.
Show resolved Hide resolved
self._input_size: int | ModelImageSize | None = None

def _get_internal_layer(self, **_kwargs: Any) -> nn.Module:
from ._internal_layers import _InternalDropoutLayer # slow import on global level

if self._input_size is None:
raise ValueError(
"The input_size is not yet set. The internal layer can only be created when the input_size is set.",
)
return _InternalDropoutLayer(self.probability)

@property
def input_size(self) -> int | ModelImageSize:
"""
Get the input_size of this layer.

Returns
-------
result:
The amount of values being passed into this layer.

Raises
------
ValueError
If the input_size is not yet set
"""
if self._input_size is None:
raise ValueError("The input_size is not yet set.")
return self._input_size

@property
def output_size(self) -> int | ModelImageSize:
"""
Get the output_size of this layer.

Returns
-------
result:
The amount of values being passed out of this layer.

Raises
------
ValueError
If the input_size is not yet set
"""
if self._input_size is None:
raise ValueError("The input_size is not yet set.")
return self._input_size

def _set_input_size(self, input_size: int | ModelImageSize) -> None:
self._input_size = input_size

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

def __eq__(self, other: object) -> bool:
if not isinstance(other, DropoutLayer):
return NotImplemented
return (self is other) or (self._input_size == other._input_size)

def __sizeof__(self) -> int:
if self._input_size is None:
raise ValueError("The input_size is not yet set.")
if isinstance(self._input_size, int):
return int(self._input_size)
elif isinstance(self._input_size, ModelImageSize):
return self._input_size.__sizeof__()
TellemHD marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 12 additions & 0 deletions src/safeds/ml/nn/layers/_internal_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def forward(self, x: Tensor) -> Tensor:
return self._fn(self._layer(x))


class _InternalDropoutLayer(nn.Module):
def __init__(self, propability: float) -> None:
TellemHD marked this conversation as resolved.
Show resolved Hide resolved
super().__init__()

_init_default_device()

self._layer = nn.Dropout(propability)

def forward(self, x: Tensor) -> Tensor:
return self._layer(x)


class _InternalFlattenLayer(nn.Module):
def __init__(self) -> None:
super().__init__()
Expand Down
68 changes: 68 additions & 0 deletions tests/safeds/ml/nn/layers/test_dropout_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import sys

import pytest
from safeds.data.tabular.containers import Table
from safeds.exceptions import OutOfBoundsError
from safeds.ml.nn.layers import DropoutLayer
from safeds.ml.nn.typing import ConstantImageSize
from torch import nn


class TestDropoutLayer:
def test_should_create_dropout_layer(self) -> None:
size = 10
layer = DropoutLayer(0.5)
layer._set_input_size(size)
assert layer.input_size == size
assert layer.output_size == size
assert isinstance(next(next(layer._get_internal_layer().modules()).children()), nn.Dropout)

def test_should_check_bounds(self) -> None:
with pytest.raises(OutOfBoundsError, match=r"probability must be in \(0, 1\) but was 2."):
DropoutLayer(2)
with pytest.raises(OutOfBoundsError, match=r"probability must be in \(0, 1\) but was -1."):
DropoutLayer(-1)

def test_input_size_should_be_set(self) -> None:
layer = DropoutLayer(0.5)
with pytest.raises(ValueError, match=r"The input_size is not yet set."):
layer.input_size # noqa: B018

with pytest.raises(ValueError, match=r"The input_size is not yet set."):
layer.output_size # noqa: B018

with pytest.raises(ValueError, match=r"The input_size is not yet set."):
layer._get_internal_layer()

with pytest.raises(ValueError, match=r"The input_size is not yet set."):
layer.__sizeof__()

def test_probability_is_set(self) -> None:
probability_to_set = 0.5
layer = DropoutLayer(probability_to_set)
assert layer.probability == probability_to_set


class TestEq:
def test_should_be_equal(self) -> None:
assert DropoutLayer(0.5) == DropoutLayer(0.5)

def test_should_be_not_implemented(self) -> None:
assert DropoutLayer(0.5).__eq__(Table()) is NotImplemented


class TestHash:
def test_hash_should_be_equal(self) -> None:
assert hash(DropoutLayer(0.5)) == hash(DropoutLayer(0.5))


class TestSizeOf:
def test_should_int_size_be_greater_than_normal_object(self) -> None:
layer = DropoutLayer(0.5)
layer._set_input_size(10)
assert sys.getsizeof(layer) > sys.getsizeof(object())

def test_should_model_image_size_be_greater_than_normal_object(self) -> None:
layer = DropoutLayer(0.5)
layer._set_input_size(ConstantImageSize(1, 1, 1))
assert sys.getsizeof(layer) > sys.getsizeof(object())
45 changes: 45 additions & 0 deletions tests/safeds/ml/nn/test_dropout_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
from safeds._config import _get_device
from safeds.data.tabular.containers import Table
from safeds.data.tabular.transformation import StandardScaler
from safeds.ml.nn import (
NeuralNetworkRegressor,
)
from safeds.ml.nn.converters import (
InputConversionTable,
)
from safeds.ml.nn.layers import (
DropoutLayer,
ForwardLayer,
)
from torch.types import Device

from tests.helpers import configure_test_with_device, get_devices, get_devices_ids, resolve_resource_path


@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
def test_forward_model(device: Device) -> None:
configure_test_with_device(device)

# Create a DataFrame
_inflation_path = "_datas/US_Inflation_rates.csv"
table_1 = Table.from_csv_file(
path=resolve_resource_path(_inflation_path),
)
table_1 = table_1.remove_columns(["date"])
table_2 = table_1.slice_rows(start=0, length=table_1.row_count - 14)
table_2 = table_2.add_columns([(table_1.slice_rows(start=14)).get_column("value").rename("target")])
train_table, test_table = table_2.split_rows(0.8)

ss = StandardScaler(column_names="value")
_, train_table = ss.fit_and_transform(train_table)
_, test_table = ss.fit_and_transform(test_table)
TellemHD marked this conversation as resolved.
Show resolved Hide resolved
model = NeuralNetworkRegressor(
InputConversionTable(),
[ForwardLayer(neuron_count=1), DropoutLayer(probability=0.5)],
)

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"]))
TellemHD marked this conversation as resolved.
Show resolved Hide resolved
assert fitted_model._model is not None
assert fitted_model._model.state_dict()["_pytorch_layers.0._layer.weight"].device == _get_device()