Skip to content

Commit fb7353e

Browse files
committed
Merge branch 'johan/devbranch' of github.com:SFI-Visual-Intelligence/Collaborative-Coding-Exam into johan/devbranch
2 parents 3af0a71 + 5056735 commit fb7353e

File tree

12 files changed

+195
-147
lines changed

12 files changed

+195
-147
lines changed

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- uses: mamba-org/setup-micromamba@v1
18+
with:
19+
micromamba-version: '2.0.5-0' # any version from https://github.com/mamba-org/micromamba-releases
20+
environment-file: environment.yml
21+
init-shell: bash
22+
cache-environment: true
23+
post-cleanup: 'all'
24+
generate-run-shell: false
25+
26+
- name: Run tests
27+
run: |
28+
PYTHONPATH=. pytest tests
29+
shell: bash -el {0}

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies:
1818
- pytest
1919
- ruff
2020
- scalene
21-
pip:
21+
- pip:
2222
- torch
2323
- torchvision
2424
prefix: /opt/miniconda3/envs/cc-exam

tests/test_createfolders.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from utils import createfolders
2+
3+
4+
def test_createfolders():
5+
import argparse
6+
from pathlib import Path
7+
from tempfile import TemporaryDirectory
8+
9+
with TemporaryDirectory() as temp_dir:
10+
temp_dir = Path(temp_dir)
11+
12+
parser = argparse.ArgumentParser()
13+
14+
# Structuture related values
15+
parser.add_argument(
16+
"--datafolder",
17+
type=Path,
18+
default=temp_dir / "Data",
19+
help="Path to where data will be saved during training.",
20+
)
21+
parser.add_argument(
22+
"--resultfolder",
23+
type=Path,
24+
default=temp_dir / "Results",
25+
help="Path to where results will be saved during evaluation.",
26+
)
27+
parser.add_argument(
28+
"--modelfolder",
29+
type=Path,
30+
default=temp_dir / "Experiments",
31+
help="Path to where model weights will be saved at the end of training.",
32+
)
33+
34+
args = parser.parse_args(
35+
[
36+
"--datafolder",
37+
str(temp_dir / "Data"),
38+
"--resultfolder",
39+
str(temp_dir / "Results"),
40+
"--modelfolder",
41+
str(temp_dir / "Experiments"),
42+
]
43+
)
44+
45+
createfolders(args.datafolder, args.resultfolder, args.modelfolder)
46+
47+
assert (temp_dir / "Data").exists()
48+
assert (temp_dir / "Results").exists()
49+
assert (temp_dir / "Experiments").exists()

tests/test_dataloaders.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from utils.dataloaders.usps_0_6 import USPSDataset0_6
2+
3+
4+
def test_uspsdataset0_6():
5+
from pathlib import Path
6+
from tempfile import TemporaryFile
7+
8+
import h5py
9+
import numpy as np
10+
11+
with TemporaryFile() as tf:
12+
with h5py.File(tf, "w") as f:
13+
f["train/data"] = np.random.rand(10, 16 * 16)
14+
f["train/target"] = np.array([6, 5, 4, 3, 2, 1, 0, 0, 0, 0])
15+
16+
dataset = USPSDataset0_6(data_path=tf, train=True)
17+
assert len(dataset) == 10
18+
data, target = dataset[0]
19+
assert data.shape == (1, 16, 16)
20+
assert all(target == np.array([0, 0, 0, 0, 0, 0, 1]))

tests/test_metrics.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from utils.metrics import Recall
2+
3+
4+
def test_recall():
5+
import torch
6+
7+
recall = Recall(7)
8+
9+
y_true = torch.tensor([0, 1, 2, 3, 4, 5, 6])
10+
y_pred = torch.tensor([2, 1, 2, 1, 4, 5, 6])
11+
12+
recall_score = recall(y_true, y_pred)
13+
14+
assert recall_score.allclose(torch.tensor(0.7143), atol=1e-5), (
15+
f"Recall Score: {recall_score.item()}"
16+
)

tests/test_models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
import torch
3+
4+
from utils.models import ChristianModel
5+
6+
7+
@pytest.mark.parametrize("in_channels, num_classes", [(1, 6), (3, 6)])
8+
def test_christian_model(in_channels, num_classes):
9+
n, c, h, w = 5, in_channels, 16, 16
10+
11+
model = ChristianModel(c, num_classes)
12+
13+
x = torch.randn(n, c, h, w)
14+
y = model(x)
15+
16+
assert y.shape == (n, num_classes), f"Shape: {y.shape}"
17+
assert y.sum(dim=1).allclose(torch.ones(n), atol=1e-5), (
18+
f"Softmax output should sum to 1, but got: {y.sum()}"
19+
)

utils/createfolders.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import argparse
21
from pathlib import Path
3-
from tempfile import TemporaryDirectory
42

53

64
def createfolders(*dirs: Path) -> None:
@@ -16,47 +14,3 @@ def createfolders(*dirs: Path) -> None:
1614

1715
for dir in dirs:
1816
dir.mkdir(parents=True, exist_ok=True)
19-
20-
21-
def test_createfolders():
22-
with TemporaryDirectory() as temp_dir:
23-
temp_dir = Path(temp_dir)
24-
25-
parser = argparse.ArgumentParser()
26-
27-
# Structuture related values
28-
parser.add_argument(
29-
"--datafolder",
30-
type=Path,
31-
default=temp_dir / "Data",
32-
help="Path to where data will be saved during training.",
33-
)
34-
parser.add_argument(
35-
"--resultfolder",
36-
type=Path,
37-
default=temp_dir / "Results",
38-
help="Path to where results will be saved during evaluation.",
39-
)
40-
parser.add_argument(
41-
"--modelfolder",
42-
type=Path,
43-
default=temp_dir / "Experiments",
44-
help="Path to where model weights will be saved at the end of training.",
45-
)
46-
47-
args = parser.parse_args(
48-
[
49-
"--datafolder",
50-
temp_dir / "Data",
51-
"--resultfolder",
52-
temp_dir / "Results",
53-
"--modelfolder",
54-
temp_dir / "Experiments",
55-
]
56-
)
57-
58-
createfolders(args.datafolder, args.resultfolder, args.modelfolder)
59-
60-
assert (temp_dir / "Data").exists()
61-
assert (temp_dir / "Results").exists()
62-
assert (temp_dir / "Experiments").exists()

utils/dataloaders/usps_0_6.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(
7171
download: bool = False,
7272
):
7373
super().__init__()
74-
self.path = list(data_path.glob("*.h5"))[0]
74+
self.path = data_path
7575
self.transform = transform
7676
self.num_classes = 7
7777

@@ -116,19 +116,3 @@ def __getitem__(self, idx):
116116
data = self.transform(data)
117117

118118
return data, target
119-
120-
121-
def test_uspsdataset0_6():
122-
import pytest
123-
124-
datapath = Path("data/USPS/usps.h5")
125-
126-
dataset = USPSDataset0_6(path=datapath, mode="train")
127-
assert len(dataset) == 5460
128-
data, target = dataset[0]
129-
assert data.shape == (16, 16)
130-
assert target == 6
131-
132-
# Test for an invalid mode
133-
with pytest.raises(ValueError):
134-
USPSDataset0_6(path=datapath, mode="inference")

utils/dataloaders/uspsh5_7_9.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from torch.utils.data import Dataset
2-
import numpy as np
31
import h5py
4-
from torchvision import transforms
5-
from PIL import Image
2+
import numpy as np
63
import torch
4+
from PIL import Image
5+
from torch.utils.data import Dataset
6+
from torchvision import transforms
77

88

99
class USPSH5_Digit_7_9_Dataset(Dataset):
@@ -95,14 +95,20 @@ def __getitem__(self, id):
9595

9696
def main():
9797
# Example Usage:
98-
transform = transforms.Compose([
99-
transforms.Resize((16, 16)), # Ensure images are 16x16
100-
transforms.ToTensor(),
101-
transforms.Normalize((0.5,), (0.5,)) # Normalize to [-1, 1]
102-
])
98+
transform = transforms.Compose(
99+
[
100+
transforms.Resize((16, 16)), # Ensure images are 16x16
101+
transforms.ToTensor(),
102+
transforms.Normalize((0.5,), (0.5,)), # Normalize to [-1, 1]
103+
]
104+
)
103105

104106
# Load the dataset
105-
dataset = USPSH5_Digit_7_9_Dataset(h5_path="C:/Users/Solveig/OneDrive/Dokumente/UiT PhD/Courses/Git/usps.h5", mode="train", transform=transform)
107+
dataset = USPSH5_Digit_7_9_Dataset(
108+
h5_path="C:/Users/Solveig/OneDrive/Dokumente/UiT PhD/Courses/Git/usps.h5",
109+
mode="train",
110+
transform=transform,
111+
)
106112
data_loader = torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=True)
107113
batch = next(iter(data_loader)) # grab a batch from the dataloader
108114
img, label = batch
@@ -112,5 +118,6 @@ def main():
112118
# Check dataset size
113119
print(f"Dataset size: {len(dataset)}")
114120

115-
if __name__ == '__main__':
116-
main()
121+
122+
if __name__ == "__main__":
123+
main()

utils/metrics/F1.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,41 @@
1-
import torch.nn as nn
21
import torch
2+
import torch.nn as nn
33

44

55
class F1Score(nn.Module):
66
"""
7-
F1 Score implementation with direct averaging inside the compute method.
7+
F1 Score implementation with direct averaging inside the compute method.
8+
9+
Parameters
10+
----------
11+
num_classes : int
12+
Number of classes.
813
9-
Parameters
10-
----------
11-
num_classes : int
12-
Number of classes.
14+
Attributes
15+
----------
16+
num_classes : int
17+
The number of classes.
1318
14-
Attributes
15-
----------
16-
num_classes : int
17-
The number of classes.
19+
tp : torch.Tensor
20+
Tensor for True Positives (TP) for each class.
1821
19-
tp : torch.Tensor
20-
Tensor for True Positives (TP) for each class.
22+
fp : torch.Tensor
23+
Tensor for False Positives (FP) for each class.
2124
22-
fp : torch.Tensor
23-
Tensor for False Positives (FP) for each class.
25+
fn : torch.Tensor
26+
Tensor for False Negatives (FN) for each class.
27+
"""
2428

25-
fn : torch.Tensor
26-
Tensor for False Negatives (FN) for each class.
27-
"""
2829
def __init__(self, num_classes):
2930
"""
30-
Initializes the F1Score object, setting up the necessary state variables.
31+
Initializes the F1Score object, setting up the necessary state variables.
3132
32-
Parameters
33-
----------
34-
num_classes : int
35-
The number of classes in the classification task.
33+
Parameters
34+
----------
35+
num_classes : int
36+
The number of classes in the classification task.
3637
37-
"""
38+
"""
3839

3940
super().__init__()
4041

@@ -47,16 +48,16 @@ def __init__(self, num_classes):
4748

4849
def update(self, preds, target):
4950
"""
50-
Update the variables with predictions and true labels.
51+
Update the variables with predictions and true labels.
5152
52-
Parameters
53-
----------
54-
preds : torch.Tensor
55-
Predicted logits (shape: [batch_size, num_classes]).
53+
Parameters
54+
----------
55+
preds : torch.Tensor
56+
Predicted logits (shape: [batch_size, num_classes]).
5657
57-
target : torch.Tensor
58-
True labels (shape: [batch_size]).
59-
"""
58+
target : torch.Tensor
59+
True labels (shape: [batch_size]).
60+
"""
6061
preds = torch.argmax(preds, dim=1)
6162

6263
# Calculate True Positives (TP), False Positives (FP), and False Negatives (FN) per class
@@ -76,17 +77,20 @@ def compute(self):
7677
"""
7778

7879
# Compute F1 score based on the specified averaging method
79-
f1_score = 2 * torch.sum(self.tp) / (2 * torch.sum(self.tp) + torch.sum(self.fp) + torch.sum(self.fn))
80+
f1_score = (
81+
2
82+
* torch.sum(self.tp)
83+
/ (2 * torch.sum(self.tp) + torch.sum(self.fp) + torch.sum(self.fn))
84+
)
8085

8186
return f1_score
8287

8388

8489
def test_f1score():
8590
f1_metric = F1Score(num_classes=3)
86-
preds = torch.tensor([[0.8, 0.1, 0.1],
87-
[0.2, 0.7, 0.1],
88-
[0.2, 0.3, 0.5],
89-
[0.1, 0.2, 0.7]])
91+
preds = torch.tensor(
92+
[[0.8, 0.1, 0.1], [0.2, 0.7, 0.1], [0.2, 0.3, 0.5], [0.1, 0.2, 0.7]]
93+
)
9094

9195
target = torch.tensor([0, 1, 0, 2])
9296

0 commit comments

Comments
 (0)