Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add fnn functionality #529

Merged
merged 78 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
9834636
Add Functionality
sibre28 Jan 10, 2024
a1dc77e
Added Test Files and docstrings for layer and model
sibre28 Jan 18, 2024
3b9f634
Merge branch 'main' into 522-add-fnn-functionality
sibre28 Jan 18, 2024
3386e0d
linter fixes
sibre28 Jan 18, 2024
0ccd6fb
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Jan 18, 2024
f5ae6cc
linter fixes
sibre28 Jan 18, 2024
1c0c3b6
linter fixes
sibre28 Jan 18, 2024
371d1e2
style: apply automated linter fixes
megalinter-bot Jan 18, 2024
752ecbe
add into_dataloader test
sibre28 Jan 22, 2024
a41789d
Add Classification and Regression Model
sibre28 Jan 22, 2024
31d1bae
Change ValueError to OutOfBoundsError and add tests
sibre28 Jan 24, 2024
e88329e
style: apply automated linter fixes
megalinter-bot Jan 24, 2024
b1f3979
style: apply automated linter fixes
megalinter-bot Jan 24, 2024
b74165b
Change ValueError to OutOfBoundsError and add tests in layer as well
sibre28 Jan 24, 2024
5dd2568
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Jan 24, 2024
aa872e4
style: apply automated linter fixes
megalinter-bot Jan 24, 2024
c86d9e3
minor changes
sibre28 Jan 24, 2024
32077c9
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Jan 24, 2024
381c6de
style: apply automated linter fixes
megalinter-bot Jan 24, 2024
4b8b15a
minor changes
sibre28 Jan 25, 2024
aaaafcb
Change Name of PytorchLayer to InternalLayer
sibre28 Jan 30, 2024
15eb8a0
minor changes
sibre28 Jan 30, 2024
5b38df7
reworked predictions method and some other stuff
sibre28 Jan 31, 2024
e568652
changed some more stuff
sibre28 Jan 31, 2024
259fce3
linter fixes
sibre28 Jan 31, 2024
6e387aa
linter fixes
sibre28 Jan 31, 2024
6074594
linter fixes
sibre28 Jan 31, 2024
6306aea
Merge branch 'main' into 522-add-fnn-functionality
sibre28 Feb 4, 2024
e4f783e
add is_fitted() Method
sibre28 Feb 6, 2024
a336485
minor changes
sibre28 Feb 6, 2024
9aee9fe
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Feb 6, 2024
94076d4
style: apply automated linter fixes
megalinter-bot Feb 6, 2024
c4e6bd1
improve codecov and fix a lot of bugs
sibre28 Feb 8, 2024
5ef1425
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Feb 8, 2024
7404d21
minor fixes
sibre28 Feb 8, 2024
87f0fbb
linter fixes
sibre28 Feb 8, 2024
baea7f7
Merge branch 'main' into 522-add-fnn-functionality
sibre28 Feb 8, 2024
7f8f39a
style: apply automated linter fixes
megalinter-bot Feb 8, 2024
c485c68
Remove input_size from layer, (add Callbacks), minor changes
sibre28 Mar 2, 2024
0c4d0ca
linter fixes
sibre28 Mar 2, 2024
eabb9f1
style: apply automated linter fixes
megalinter-bot Mar 2, 2024
98cbf53
test fixes
sibre28 Mar 2, 2024
ec13884
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 2, 2024
536cd0f
codecov
sibre28 Mar 4, 2024
7a0437f
Update src/safeds/data/tabular/containers/_table.py
sibre28 Mar 6, 2024
5bf384d
Apply suggestions from code review
sibre28 Mar 6, 2024
2888028
add running_loss and minor stuff
sibre28 Mar 6, 2024
0bbfe4a
fix
sibre28 Mar 6, 2024
9777889
style: apply automated linter fixes
megalinter-bot Mar 6, 2024
8b31cae
rework stuff and add callbacks with tests
sibre28 Mar 9, 2024
9b4e652
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 9, 2024
e62ac7e
linter
sibre28 Mar 9, 2024
82db404
style: apply automated linter fixes
megalinter-bot Mar 9, 2024
644f790
style: apply automated linter fixes
megalinter-bot Mar 9, 2024
6f4684a
stuff
sibre28 Mar 9, 2024
bc3733c
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 9, 2024
e524e26
Merge branch 'main' into 522-add-fnn-functionality
sibre28 Mar 9, 2024
f711f02
tests
sibre28 Mar 10, 2024
c1d87f0
rename
sibre28 Mar 10, 2024
645c6bf
little fix so models with batch size > 1 work correctly
sibre28 Mar 18, 2024
bc2cc91
style: apply automated linter fixes
megalinter-bot Mar 18, 2024
7063549
style: apply automated linter fixes
megalinter-bot Mar 18, 2024
96fee8f
fix classification models not working properly for multiclass use-cases
sibre28 Mar 18, 2024
5b165a5
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 18, 2024
e2030e2
add testcase for multiclass classification
sibre28 Mar 18, 2024
5e1a800
add testcase for multiclass classification
sibre28 Mar 18, 2024
8aea4b1
small changes
sibre28 Mar 19, 2024
3b47196
Merge branch 'main' into 522-add-fnn-functionality
sibre28 Mar 19, 2024
f7c7af6
style: apply automated linter fixes
megalinter-bot Mar 19, 2024
4f5cf83
small changes
sibre28 Mar 19, 2024
f3c4a59
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 19, 2024
c66a8fd
style: apply automated linter fixes
megalinter-bot Mar 19, 2024
781b191
small changes for codecov
sibre28 Mar 19, 2024
647224e
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 19, 2024
a60d26d
style: apply automated linter fixes
megalinter-bot Mar 19, 2024
8f39fc6
remove lines that cannot be easily covered from codecov inspection
sibre28 Mar 19, 2024
ad4cfd4
Merge remote-tracking branch 'origin/522-add-fnn-functionality' into …
sibre28 Mar 19, 2024
b07bd48
style: apply automated linter fixes
megalinter-bot Mar 19, 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
43 changes: 43 additions & 0 deletions src/safeds/data/tabular/containers/_tagged_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from typing import TYPE_CHECKING

import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset

from safeds.data.tabular.containers import Column, Row, Table
from safeds.exceptions import (
ColumnIsTargetError,
Expand Down Expand Up @@ -833,3 +837,42 @@
target_name=self.target.name,
feature_names=self.features.column_names,
)

def into_dataloader(self, batch_size: int) -> DataLoader:
"""
Return a Dataloader for the data stored in this table, used for training neural networks.

The original table is not modified.

Parameters
----------
batch_size : int
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
The size of data batches that should be loaded at one time.

Returns
-------
result : DataLoader
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
The DataLoader.

"""
feature_rows = self.features.to_rows()
all_rows = []
for row in feature_rows:
new_item = []
for column_name in row:
new_item.append(row.get_value(column_name))
all_rows.append(new_item.copy())
return DataLoader(dataset=CustomDataset(np.array(all_rows), np.array(self.target)), batch_size=batch_size)

Check warning on line 865 in src/safeds/data/tabular/containers/_tagged_table.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/data/tabular/containers/_tagged_table.py#L858-L865

Added lines #L858 - L865 were not covered by tests


class CustomDataset(Dataset):
def __init__(self, features: np.array, target: np.array):
self.X = torch.from_numpy(features.astype(np.float32))
self.Y = torch.from_numpy(target.astype(np.float32))
self.len = self.X.shape[0]

Check warning on line 872 in src/safeds/data/tabular/containers/_tagged_table.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/data/tabular/containers/_tagged_table.py#L870-L872

Added lines #L870 - L872 were not covered by tests

def __getitem__(self, item: int) -> tuple[torch.Tensor, torch.Tensor]:
return self.X[item], self.Y[item].unsqueeze(-1)

Check warning on line 875 in src/safeds/data/tabular/containers/_tagged_table.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/data/tabular/containers/_tagged_table.py#L875

Added line #L875 was not covered by tests

def __len__(self) -> int:
return self.len

Check warning on line 878 in src/safeds/data/tabular/containers/_tagged_table.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/data/tabular/containers/_tagged_table.py#L878

Added line #L878 was not covered by tests
9 changes: 9 additions & 0 deletions src/safeds/ml/nn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Classes for classification tasks."""

from ._fnn_layer import FNNLayer
from ._model import Model

__all__ = [
"FNNLayer",
"Model",
]
48 changes: 48 additions & 0 deletions src/safeds/ml/nn/_fnn_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from torch import nn


class PytorchLayer(nn.Module):
def __init__(self, input_size: int, output_size: int):
super().__init__()
self.size = output_size
self.layer = nn.Linear(input_size, output_size)
self.fn = nn.ReLU()

def forward(self, x: float) -> float:
return self.fn(self.layer(x))

Check warning on line 12 in src/safeds/ml/nn/_fnn_layer.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_fnn_layer.py#L12

Added line #L12 was not covered by tests
sibre28 marked this conversation as resolved.
Show resolved Hide resolved

def get_size(self) -> int:
return self.size

Check warning on line 15 in src/safeds/ml/nn/_fnn_layer.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_fnn_layer.py#L15

Added line #L15 was not covered by tests


class FNNLayer:
def __init__(self, input_size: int, output_size: int):
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
"""
Create a FNN Layer.

Parameters
----------
input_size : int
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
The number of neurons in the previous layer
output_size : int
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
The number of neurons in this layer

Raises
------
ValueError
If input_size < 1
If output_size < 1

"""
if input_size < 1:
raise ValueError("Input Size must be at least 1")
if output_size < 1:
raise ValueError("Output Size must be at least 1")
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
self.input_size = input_size
self.output_size = output_size
sibre28 marked this conversation as resolved.
Show resolved Hide resolved

def _get_pytorch_layer(self) -> PytorchLayer:
return PytorchLayer(self.input_size, self.output_size)

def get_size(self) -> int:
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
return self.output_size

Check warning on line 48 in src/safeds/ml/nn/_fnn_layer.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_fnn_layer.py#L48

Added line #L48 was not covered by tests
116 changes: 116 additions & 0 deletions src/safeds/ml/nn/_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import numpy as np
import torch
from torch import nn

from safeds.data.tabular.containers import TaggedTable
from safeds.ml.nn._fnn_layer import FNNLayer


class Model:
def __init__(self, layers: list):
self._model = PytorchModel(layers)
self._batch_size = 1

def train(self, train_data: TaggedTable, epoch_size: int = 25, batch_size: int = 1) -> None:
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
"""
Train the neural network with given training data.

Parameters
----------
train_data : TaggedTable
The data the network should be trained on.
epoch_size : int
The number of times the training cycle should be done
batch_size : int
The size of data batches that should be loaded at one time.
sibre28 marked this conversation as resolved.
Show resolved Hide resolved

Raises
------
ValueError
If epoch_size < 1
If batch_size < 1

"""
if epoch_size < 1:
raise ValueError("The Number of Epochs must be at least 1")
if batch_size < 1:
raise ValueError("Batch Size must be at least 1")
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
self._batch_size = batch_size
dataloader = train_data.into_dataloader(self._batch_size)

Check warning on line 39 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L38-L39

Added lines #L38 - L39 were not covered by tests

if self.is_for_regression():
loss_fn = nn.MSELoss()

Check warning on line 42 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L41-L42

Added lines #L41 - L42 were not covered by tests
else:
loss_fn = nn.BCELoss()

Check warning on line 44 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L44

Added line #L44 was not covered by tests

optimizer = torch.optim.SGD(self._model.parameters(), lr=0.05)

Check warning on line 46 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L46

Added line #L46 was not covered by tests

loss_values = []
accuracies = []
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
for _epoch in range(epoch_size):

Check warning on line 50 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L48-L50

Added lines #L48 - L50 were not covered by tests
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
# print(f"Epoch {epoch+1}")
tmp_loss = []
tmp_accuracies = []
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
for x, y in dataloader:
pred = self._model(x)

Check warning on line 55 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L52-L55

Added lines #L52 - L55 were not covered by tests

loss = loss_fn(pred, y)
tmp_loss.append(loss.item())

Check warning on line 58 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L57-L58

Added lines #L57 - L58 were not covered by tests

accuracy = torch.mean(1 - torch.abs(pred - y))
tmp_accuracies.append(accuracy.item())

Check warning on line 61 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L60-L61

Added lines #L60 - L61 were not covered by tests

optimizer.zero_grad()
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
loss.backward()
optimizer.step()
loss_values.append(np.mean(tmp_loss))
accuracies.append(np.mean(tmp_accuracies))

Check warning on line 67 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L63-L67

Added lines #L63 - L67 were not covered by tests
# print(loss_values)

def predict(self, test_data: TaggedTable) -> None:
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
"""
Make a prediction for the given test data.

Parameters
----------
test_data : TaggedTable
The data the network should try to predict.

"""
dataloader = test_data.into_dataloader(self._batch_size)
self._model.eval()
loss_values_test = []
accuracies_test = []
loss_fn = nn.MSELoss()
with torch.no_grad():
for x, y in dataloader:
pred = self._model(x)
loss = loss_fn(pred, y)
loss_values_test.append(loss.item())
accuracy = torch.mean(1 - torch.abs(pred - y))
accuracies_test.append(accuracy.item())

Check warning on line 91 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L80-L91

Added lines #L80 - L91 were not covered by tests

# print(np.mean(loss_values_test))
# print(np.mean(accuracies_test))

def is_for_regression(self) -> bool:
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
return self._model.last_layer_has_output_size_one()

Check warning on line 97 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L97

Added line #L97 was not covered by tests


class PytorchModel(nn.Module):
sibre28 marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, layer_list: list[FNNLayer]) -> None:
super().__init__()
self._layer_list = layer_list
layers = []
for layer in layer_list:
layers.append(layer._get_pytorch_layer())

self._pytorch_layers = nn.ModuleList(layers)

def forward(self, x: float) -> float:
for layer in self._pytorch_layers:
x = layer(x)
return x

Check warning on line 113 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L111-L113

Added lines #L111 - L113 were not covered by tests

def last_layer_has_output_size_one(self) -> bool:
return self._layer_list[-1].get_size() == 1

Check warning on line 116 in src/safeds/ml/nn/_model.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/ml/nn/_model.py#L116

Added line #L116 was not covered by tests
Empty file.
23 changes: 23 additions & 0 deletions tests/safeds/ml/nn/test_fnn_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest
from safeds.ml.nn._fnn_layer import FNNLayer


@pytest.mark.parametrize(
("input_size", "output_size", "expected_error_message"),
[
(
0,
5,
r"Input Size must be at least 1",
),
(
5,
0,
r"Output Size must be at least 1",
),
],
ids=["input_size_out_of_bounds", "output_size_out_of_bounds"],
)
def test_should_raise_error(input_size: int, output_size: int, expected_error_message: str) -> None:
with pytest.raises(ValueError, match=expected_error_message):
FNNLayer(input_size, output_size)
25 changes: 25 additions & 0 deletions tests/safeds/ml/nn/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest
from safeds.data.tabular.containers import Table
from safeds.ml.nn._fnn_layer import FNNLayer
from safeds.ml.nn._model import Model


@pytest.mark.parametrize(
("epoch_size", "batch_size", "expected_error_message"),
[
(
0,
5,
r"The Number of Epochs must be at least 1",
),
(
5,
0,
r"Batch Size must be at least 1",
),
],
ids=["epoch_size_out_of_bounds", "batch_size_out_of_bounds"],
)
def test_should_raise_error(epoch_size: int, batch_size: int, expected_error_message: str) -> None:
with pytest.raises(ValueError, match=expected_error_message):
Model([FNNLayer(1, 1)]).train(Table.from_dict({"a": [1], "b": [2]}).tag_columns("a"), epoch_size, batch_size)
Loading