openmlcore 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openmlcore-1.0.0/LICENSE +21 -0
- openmlcore-1.0.0/PKG-INFO +76 -0
- openmlcore-1.0.0/README.md +35 -0
- openmlcore-1.0.0/pyproject.toml +33 -0
- openmlcore-1.0.0/setup.cfg +4 -0
- openmlcore-1.0.0/src/openmlcore/__init__.py +0 -0
- openmlcore-1.0.0/src/openmlcore/base/data.py +48 -0
- openmlcore-1.0.0/src/openmlcore/loss/loss.py +210 -0
- openmlcore-1.0.0/src/openmlcore/metrics/metrics.py +81 -0
- openmlcore-1.0.0/src/openmlcore/models/UNets.py +70 -0
- openmlcore-1.0.0/src/openmlcore/models/attUnets.py +76 -0
- openmlcore-1.0.0/src/openmlcore/models/base_model.py +227 -0
- openmlcore-1.0.0/src/openmlcore/models/lib/attention.py +21 -0
- openmlcore-1.0.0/src/openmlcore/models/lib/convolutions.py +15 -0
- openmlcore-1.0.0/src/openmlcore/models/unetpp.py +86 -0
- openmlcore-1.0.0/src/openmlcore.egg-info/PKG-INFO +76 -0
- openmlcore-1.0.0/src/openmlcore.egg-info/SOURCES.txt +21 -0
- openmlcore-1.0.0/src/openmlcore.egg-info/dependency_links.txt +1 -0
- openmlcore-1.0.0/src/openmlcore.egg-info/requires.txt +6 -0
- openmlcore-1.0.0/src/openmlcore.egg-info/top_level.txt +1 -0
- openmlcore-1.0.0/tests/test_base_model.py +101 -0
- openmlcore-1.0.0/tests/test_loss.py +27 -0
- openmlcore-1.0.0/tests/test_main.py +4 -0
openmlcore-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Votre Nom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openmlcore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenMLCore is an open source framework (wrapper which simplify the use of PyTorch)
|
|
5
|
+
Author-email: Doyez Sébastien <doyez.sebastien34090@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Votre Nom
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://sebastien-doyez2812.github.io/openMLCore.github.io/
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Requires-Python: >=3.10.0
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: torch
|
|
35
|
+
Requires-Dist: tqdm
|
|
36
|
+
Requires-Dist: onnx
|
|
37
|
+
Requires-Dist: onnxscript
|
|
38
|
+
Requires-Dist: torchinfo
|
|
39
|
+
Requires-Dist: datasets
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# OpenML-core, Open source Package for Machine learning
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Why OpenML?
|
|
46
|
+
Today's computer vision ecosystem is heavily fragmented. If you want YOLO, Ultralytics makes it trivial. But the moment you need advanced semantic segmentation, hybrid transformer backbones, or efficient fine-tuning on modern architectures, you end up stitching together disjointed codebases, mismatched tensor shapes, and unmaintained repositories.
|
|
47
|
+
|
|
48
|
+
OpenML bridges this gap by bringing a clean, Hugging Face-inspired Developer Experience (DX) to advanced computer vision tasks—from classical models to SOTA transformers and parameter-efficient fine-tuning.
|
|
49
|
+
## Supported Architectures:
|
|
50
|
+
|
|
51
|
+
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++, Mask R-CNN
|
|
52
|
+
|
|
53
|
+
- Advanced Transformers: Swin Transformer, TransUNet, MobileUNetR
|
|
54
|
+
|
|
55
|
+
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA)
|
|
56
|
+
## Key features:
|
|
57
|
+
|
|
58
|
+
Unified Object-Oriented API: Every model inherits from a clean ModelBase class supporting standard .train(), .eval(), and .help() workflows.
|
|
59
|
+
|
|
60
|
+
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM.
|
|
61
|
+
|
|
62
|
+
Hugging Face Hub Integration: Push and pull checkpoints effortlessly using native .safetensors formatting.
|
|
63
|
+
|
|
64
|
+
MLflow Tracking Out-of-the-Box: Keep tabs on your losses, metrics, and hyperparameter logs without writing boilerplate monitoring code.
|
|
65
|
+
|
|
66
|
+
Interactive Help System: Running model.help() directly opens up the exact architectural documentation page on the web.
|
|
67
|
+
|
|
68
|
+
## Contributions:
|
|
69
|
+
|
|
70
|
+
Contributions, feature requests, and bug reports are welcome! Feel free to open an Issue or submit a Pull Request on GitHub.
|
|
71
|
+
|
|
72
|
+
## Author
|
|
73
|
+
|
|
74
|
+
Sébastien Doyez
|
|
75
|
+
ML & AI Engineer,
|
|
76
|
+
Interested in Agentics, Computer Vision, Robotics & Deep Learning
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# OpenML-core, Open source Package for Machine learning
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Why OpenML?
|
|
5
|
+
Today's computer vision ecosystem is heavily fragmented. If you want YOLO, Ultralytics makes it trivial. But the moment you need advanced semantic segmentation, hybrid transformer backbones, or efficient fine-tuning on modern architectures, you end up stitching together disjointed codebases, mismatched tensor shapes, and unmaintained repositories.
|
|
6
|
+
|
|
7
|
+
OpenML bridges this gap by bringing a clean, Hugging Face-inspired Developer Experience (DX) to advanced computer vision tasks—from classical models to SOTA transformers and parameter-efficient fine-tuning.
|
|
8
|
+
## Supported Architectures:
|
|
9
|
+
|
|
10
|
+
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++, Mask R-CNN
|
|
11
|
+
|
|
12
|
+
- Advanced Transformers: Swin Transformer, TransUNet, MobileUNetR
|
|
13
|
+
|
|
14
|
+
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA)
|
|
15
|
+
## Key features:
|
|
16
|
+
|
|
17
|
+
Unified Object-Oriented API: Every model inherits from a clean ModelBase class supporting standard .train(), .eval(), and .help() workflows.
|
|
18
|
+
|
|
19
|
+
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM.
|
|
20
|
+
|
|
21
|
+
Hugging Face Hub Integration: Push and pull checkpoints effortlessly using native .safetensors formatting.
|
|
22
|
+
|
|
23
|
+
MLflow Tracking Out-of-the-Box: Keep tabs on your losses, metrics, and hyperparameter logs without writing boilerplate monitoring code.
|
|
24
|
+
|
|
25
|
+
Interactive Help System: Running model.help() directly opens up the exact architectural documentation page on the web.
|
|
26
|
+
|
|
27
|
+
## Contributions:
|
|
28
|
+
|
|
29
|
+
Contributions, feature requests, and bug reports are welcome! Feel free to open an Issue or submit a Pull Request on GitHub.
|
|
30
|
+
|
|
31
|
+
## Author
|
|
32
|
+
|
|
33
|
+
Sébastien Doyez
|
|
34
|
+
ML & AI Engineer,
|
|
35
|
+
Interested in Agentics, Computer Vision, Robotics & Deep Learning
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openmlcore"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Doyez Sébastien", email="doyez.sebastien34090@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "OpenMLCore is an open source framework (wrapper which simplify the use of PyTorch)"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10.0"
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"torch",
|
|
22
|
+
"tqdm",
|
|
23
|
+
"onnx",
|
|
24
|
+
"onnxscript",
|
|
25
|
+
"torchinfo",
|
|
26
|
+
"datasets"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
"Homepage" = "https://sebastien-doyez2812.github.io/openMLCore.github.io/"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# author: Sebastien Doyez
|
|
2
|
+
# data is the file where we create custom Dataset class
|
|
3
|
+
# SegmentationDataset is the dataset class for 1 hot encoding dataset
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from PIL import Image
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
from torch.utils.data import Dataset
|
|
10
|
+
from torchvision.transforms import v2
|
|
11
|
+
from torchvision import tv_tensors
|
|
12
|
+
|
|
13
|
+
class SegmentationDataset(Dataset):
|
|
14
|
+
def __init__(self, root_dir="data/train", transform=None, num_classes = 8):
|
|
15
|
+
self.root_dir = root_dir
|
|
16
|
+
self.images_dir = os.path.join(root_dir, "imgs")
|
|
17
|
+
self.masks_dir = os.path.join(root_dir, "labels")
|
|
18
|
+
self.transform = transform
|
|
19
|
+
self.num_classes = num_classes
|
|
20
|
+
|
|
21
|
+
self.img_names = sorted(os.listdir(self.images_dir))
|
|
22
|
+
self.mask_names = sorted(os.listdir(self.masks_dir))
|
|
23
|
+
|
|
24
|
+
def __len__(self):
|
|
25
|
+
return len(self.img_names)
|
|
26
|
+
|
|
27
|
+
def __getitem__(self, idx):
|
|
28
|
+
img_path = os.path.join(self.images_dir, self.img_names[idx])
|
|
29
|
+
mask_path = os.path.join(self.masks_dir, self.mask_names[idx])
|
|
30
|
+
|
|
31
|
+
image = Image.open(img_path).convert("RGB")
|
|
32
|
+
mask = Image.open(mask_path)
|
|
33
|
+
|
|
34
|
+
mask_np = np.array(mask, dtype = np.uint8)
|
|
35
|
+
masks = [((mask_np >> i ) & 1) for i in range (self.num_classes)]
|
|
36
|
+
|
|
37
|
+
mask_stacked = np.stack(masks, axis = 0).astype(np.float32)
|
|
38
|
+
mask_tensor = tv_tensors.Mask(mask_stacked)
|
|
39
|
+
|
|
40
|
+
if self.transform is not None:
|
|
41
|
+
image, mask_tensor = self.transform(image, mask_tensor)
|
|
42
|
+
else:
|
|
43
|
+
image = v2.functional.to_image(image)
|
|
44
|
+
image = v2.functional.to_dtype(image, dtype=torch.float32, scale=True)
|
|
45
|
+
|
|
46
|
+
return image, mask_tensor
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class BaseLoss(nn.Module):
|
|
7
|
+
def __init__(self, name: str = "BaseLoss", doc_url: str = ""):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self.name = name
|
|
10
|
+
self.smooth = 1e-6
|
|
11
|
+
self.doc_url = doc_url
|
|
12
|
+
|
|
13
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
14
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
15
|
+
|
|
16
|
+
def documentation(self) -> str:
|
|
17
|
+
print(f"Documentation for {self.name} is available here:\n{self.doc_url}")
|
|
18
|
+
try:
|
|
19
|
+
import webbrowser
|
|
20
|
+
webbrowser.open(self.doc_url)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
print(f"[-] {e}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#############################
|
|
26
|
+
# Class Loss Functions #
|
|
27
|
+
#############################
|
|
28
|
+
|
|
29
|
+
class BCEWithLogitsLoss(BaseLoss):
|
|
30
|
+
def __init__(self, weights=None):
|
|
31
|
+
super().__init__(name="BCEWithLogitsLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html")
|
|
32
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
33
|
+
|
|
34
|
+
pos_weights = None
|
|
35
|
+
if weights is not None:
|
|
36
|
+
if not isinstance(weights, torch.Tensor):
|
|
37
|
+
pos_weights = torch.tensor(weights, device=device, dtype=torch.float32)
|
|
38
|
+
else:
|
|
39
|
+
pos_weights = weights.to(device)
|
|
40
|
+
|
|
41
|
+
if pos_weights.ndim == 1:
|
|
42
|
+
pos_weights = pos_weights.view(-1, 1, 1)
|
|
43
|
+
self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=pos_weights)
|
|
44
|
+
|
|
45
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
46
|
+
return self.loss_fn(outputs, targets.float())
|
|
47
|
+
|
|
48
|
+
class DiceLoss(BaseLoss):
|
|
49
|
+
def __init__(self, weights = None):
|
|
50
|
+
super().__init__(name="DiceLoss", doc_url="https://arxiv.org/abs/1606.04797")
|
|
51
|
+
self.weights = weights
|
|
52
|
+
|
|
53
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
54
|
+
probs = torch.sigmoid(outputs)
|
|
55
|
+
probs = probs.view (probs.size(0) , probs.size(1), -1)
|
|
56
|
+
targets = targets.view(targets.size(0), targets.size(1), -1)
|
|
57
|
+
|
|
58
|
+
intersection = (probs * targets).sum(dim=2)
|
|
59
|
+
union = probs.sum(dim=2) + targets.sum(dim=2)
|
|
60
|
+
|
|
61
|
+
dice_score = (2.0 * intersection + self.smooth) / (union + self.smooth)
|
|
62
|
+
dice_loss = 1.0 - dice_score
|
|
63
|
+
|
|
64
|
+
if self.weights is not None:
|
|
65
|
+
if not isinstance(self.weights, torch.Tensor):
|
|
66
|
+
weights_tensor = torch.tensor(self.weights, device=outputs.device, dtype=outputs.dtype)
|
|
67
|
+
else:
|
|
68
|
+
weights_tensor = self.weights.to(outputs.device)
|
|
69
|
+
|
|
70
|
+
dice_loss = dice_loss * weights_tensor.unsqueeze(0)
|
|
71
|
+
|
|
72
|
+
return dice_loss.sum(dim=1).mean()
|
|
73
|
+
return dice_loss.mean()
|
|
74
|
+
|
|
75
|
+
# TODO:
|
|
76
|
+
# 1 Hot encoding loss
|
|
77
|
+
#
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MeanSquaredErrorLoss(BaseLoss):
|
|
81
|
+
def __init__(self, index_ignore=255):
|
|
82
|
+
super().__init__(name="MeanSquaredErrorLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html")
|
|
83
|
+
self.index_ignore = index_ignore
|
|
84
|
+
|
|
85
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
86
|
+
num_classes = outputs.shape[1]
|
|
87
|
+
probs = F.softmax(outputs, dim=1)
|
|
88
|
+
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
89
|
+
|
|
90
|
+
mse = (probs - targets_one_hot) ** 2
|
|
91
|
+
mse = mse * valid_mask
|
|
92
|
+
|
|
93
|
+
num_valids = valid_mask.sum() * num_classes
|
|
94
|
+
return mse.sum() / torch.clamp(num_valids, min=1.0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class FocalLoss(BaseLoss):
|
|
99
|
+
def __init__(self, alpha=0.25, gamma=2.0, index_ignore=255):
|
|
100
|
+
super().__init__(name="FocalLoss", doc_url="https://arxiv.org/abs/1708.02002")
|
|
101
|
+
self.alpha = alpha
|
|
102
|
+
self.gamma = gamma
|
|
103
|
+
self.index_ignore = index_ignore
|
|
104
|
+
|
|
105
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
106
|
+
num_classes = outputs.shape[1]
|
|
107
|
+
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
108
|
+
|
|
109
|
+
bce_loss = F.binary_cross_entropy_with_logits(outputs, targets_one_hot, reduction='none')
|
|
110
|
+
pt = torch.exp(-bce_loss)
|
|
111
|
+
focal_loss = self.alpha * ((1 - pt) ** self.gamma) * bce_loss
|
|
112
|
+
focal_loss = focal_loss * valid_mask
|
|
113
|
+
num_valids = valid_mask.sum() * num_classes
|
|
114
|
+
return focal_loss.sum() / torch.clamp(num_valids, min=1.0)
|
|
115
|
+
|
|
116
|
+
class MeanSquaredErrorLoss(BaseLoss):
|
|
117
|
+
def __init__(self, index_ignore=255):
|
|
118
|
+
super().__init__(name="MeanSquaredErrorLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html")
|
|
119
|
+
self.index_ignore = index_ignore
|
|
120
|
+
|
|
121
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
122
|
+
num_classes = outputs.shape[1]
|
|
123
|
+
probs = F.softmax(outputs, dim=1)
|
|
124
|
+
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
125
|
+
|
|
126
|
+
mse = (probs - targets_one_hot) ** 2
|
|
127
|
+
mse = mse * valid_mask
|
|
128
|
+
|
|
129
|
+
num_valids = valid_mask.sum() * num_classes
|
|
130
|
+
return mse.sum() / torch.clamp(num_valids, min=1.0)
|
|
131
|
+
|
|
132
|
+
class HuberLoss(BaseLoss):
|
|
133
|
+
def __init__(self, delta=1.0, index_ignore=255):
|
|
134
|
+
super().__init__(name="HuberLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.HuberLoss.html")
|
|
135
|
+
self.delta = delta
|
|
136
|
+
self.index_ignore = index_ignore
|
|
137
|
+
|
|
138
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
139
|
+
num_classes = outputs.shape[1]
|
|
140
|
+
probs = F.softmax(outputs, dim=1)
|
|
141
|
+
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
142
|
+
|
|
143
|
+
error = probs - targets_one_hot
|
|
144
|
+
abs_error = torch.abs(error)
|
|
145
|
+
|
|
146
|
+
huber = torch.where(
|
|
147
|
+
abs_error < self.delta,
|
|
148
|
+
0.5 * (error ** 2),
|
|
149
|
+
self.delta * (abs_error - 0.5 * self.delta)
|
|
150
|
+
)
|
|
151
|
+
huber = huber * valid_mask
|
|
152
|
+
|
|
153
|
+
num_valids = valid_mask.sum() * num_classes
|
|
154
|
+
return huber.sum() / torch.clamp(num_valids, min=1.0)
|
|
155
|
+
|
|
156
|
+
class KLDivergenceLoss(BaseLoss):
|
|
157
|
+
def __init__(self, index_ignore=255):
|
|
158
|
+
super().__init__(name="KLDivergenceLoss", doc_url="https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html")
|
|
159
|
+
self.eps = 1e-8
|
|
160
|
+
self.index_ignore = index_ignore
|
|
161
|
+
|
|
162
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
163
|
+
num_classes = outputs.shape[1]
|
|
164
|
+
log_probs = F.log_softmax(outputs, dim=1)
|
|
165
|
+
targets_one_hot, valid_mask = self._to_one_hot_clean(targets, num_classes, self.index_ignore)
|
|
166
|
+
|
|
167
|
+
# KL-Div = p(x) * (log(p(x)) - log(q(x)))
|
|
168
|
+
# Pour une distribution One-Hot, cela se réduit à : - log(q(x)) sur la classe vraie
|
|
169
|
+
kl = F.kl_div(log_probs, targets_one_hot, reduction='none')
|
|
170
|
+
kl = kl * valid_mask
|
|
171
|
+
|
|
172
|
+
num_valids = valid_mask.sum() * num_classes
|
|
173
|
+
return kl.sum() / torch.clamp(num_valids, min=1.0)
|
|
174
|
+
|
|
175
|
+
####################################
|
|
176
|
+
# Custom Loss implementation #
|
|
177
|
+
####################################
|
|
178
|
+
|
|
179
|
+
class LossFactory:
|
|
180
|
+
@staticmethod
|
|
181
|
+
def create(name: str, **kwargs ) -> BaseLoss:
|
|
182
|
+
match name:
|
|
183
|
+
case "BCE":
|
|
184
|
+
return BCEWithLogitsLoss(**kwargs)
|
|
185
|
+
case "MeanSquaredErrorLoss":
|
|
186
|
+
return MeanSquaredErrorLoss(**kwargs)
|
|
187
|
+
case "DiceLoss":
|
|
188
|
+
return DiceLoss(**kwargs)
|
|
189
|
+
case "FocalLoss":
|
|
190
|
+
return FocalLoss(**kwargs)
|
|
191
|
+
case "HuberLoss":
|
|
192
|
+
return HuberLoss(**kwargs)
|
|
193
|
+
case "KLDivergenceLoss":
|
|
194
|
+
return KLDivergenceLoss(**kwargs)
|
|
195
|
+
case _:
|
|
196
|
+
raise ValueError(f"Unknown loss function: {name}")
|
|
197
|
+
|
|
198
|
+
class CustomLoss(BaseLoss):
|
|
199
|
+
def __init__(self, loss_fcns: list[BaseLoss], name: str, coefficients: list[float]):
|
|
200
|
+
assert len(loss_fcns) == len(coefficients), "The number of loss functions must match the number of coefficients."
|
|
201
|
+
super().__init__(name=name, doc_url=None)
|
|
202
|
+
self.loss_fcns = nn.ModuleList(loss_fcns)
|
|
203
|
+
self.coefficients = coefficients
|
|
204
|
+
|
|
205
|
+
def forward(self, outputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
206
|
+
total_loss = 0.0
|
|
207
|
+
for loss_fn, coef in zip(self.loss_fcns, self.coefficients):
|
|
208
|
+
total_loss += coef * loss_fn(outputs, targets)
|
|
209
|
+
return total_loss
|
|
210
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
### GUIDELINES ###
|
|
2
|
+
|
|
3
|
+
# The metrics are define by functions.
|
|
4
|
+
# The function should ALWAYS return a list with the value of the metric of each class.
|
|
5
|
+
# For example IoU (Intersection of Union) return a list.
|
|
6
|
+
# Let's say we have 3 classes:
|
|
7
|
+
# Class 0: sky
|
|
8
|
+
# Class 1: Road
|
|
9
|
+
# Class 2: Car
|
|
10
|
+
# We should return a list of 3 elements like [IoU_sky, IoU_Road, IoU_Car]
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
def accuracy(y_pred, y_true):
|
|
14
|
+
num_batches = y_true.shape[0]
|
|
15
|
+
num_classes = y_true.shape[1]
|
|
16
|
+
|
|
17
|
+
metric_to_return = [0.0] * num_classes
|
|
18
|
+
for idx_current_batch in range(num_batches):
|
|
19
|
+
current_metrics = []
|
|
20
|
+
for idx_class in range(num_classes):
|
|
21
|
+
pred = (y_pred[idx_current_batch, idx_class, :, :] > 0.0).float()
|
|
22
|
+
gt = y_true[idx_current_batch, idx_class, :, :]
|
|
23
|
+
|
|
24
|
+
correct = (pred == gt).float()
|
|
25
|
+
accuracy = (pred == gt).float().mean().item()
|
|
26
|
+
|
|
27
|
+
current_metrics.append(accuracy)
|
|
28
|
+
|
|
29
|
+
for i in range(num_classes):
|
|
30
|
+
metric_to_return[i] += current_metrics[i]
|
|
31
|
+
|
|
32
|
+
final_metric = [0.0] * num_classes
|
|
33
|
+
for idx, metric in enumerate(metric_to_return):
|
|
34
|
+
final_metric[idx] = metric / num_batches
|
|
35
|
+
return final_metric
|
|
36
|
+
|
|
37
|
+
def iou(y_pred, y_true,smooth = 1e-6):
|
|
38
|
+
preds = (y_pred > 0.0).float()
|
|
39
|
+
preds = preds.view(preds.size(0), preds.size(1), -1)
|
|
40
|
+
y_true = y_true.view(y_true.size(0), y_true.size(1), -1)
|
|
41
|
+
|
|
42
|
+
intersection = (preds * y_true).sum(dim=2)
|
|
43
|
+
total = preds.sum(dim=2) + y_true.sum(dim=2)
|
|
44
|
+
union = total - intersection
|
|
45
|
+
|
|
46
|
+
iou_per_class = (intersection + smooth) / (union + smooth)
|
|
47
|
+
mean_iou_per_class = iou_per_class.mean(dim=0)
|
|
48
|
+
|
|
49
|
+
return mean_iou_per_class.tolist()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def F1(y_pred, y_true, eps = 1e-7):
|
|
53
|
+
# F1 = 2. precision.recall/(precision + recall)
|
|
54
|
+
recall_res = recall(y_pred=y_pred, y_true=y_true)
|
|
55
|
+
precision_res = precision(y_pred=y_pred, y_true=y_true)
|
|
56
|
+
|
|
57
|
+
F1_values = []
|
|
58
|
+
for idx in range(len(recall_res)):
|
|
59
|
+
current_F1 = 2 * recall_res[idx] * precision_res[idx] / (precision_res[idx] + recall_res[idx] + eps)
|
|
60
|
+
F1_values.append(current_F1)
|
|
61
|
+
return F1_values
|
|
62
|
+
|
|
63
|
+
def recall(y_pred, y_true, threshold = 0.5, eps = 1e-7):
|
|
64
|
+
# Recall = TP/(TP+FN)
|
|
65
|
+
y_pred_bin = (y_pred >threshold).float()
|
|
66
|
+
y_true = y_true.float()
|
|
67
|
+
# (0, 2, 3) means we are going to add the batch, H and W, we only keep the class
|
|
68
|
+
TP = torch.sum(y_pred_bin * y_true , dim= (0, 2, 3))
|
|
69
|
+
FN = torch.sum(y_true * (1 - y_pred_bin), dim= (0, 2, 3))
|
|
70
|
+
precision = TP/(TP + FN + eps)
|
|
71
|
+
return precision.tolist()
|
|
72
|
+
|
|
73
|
+
def precision(y_pred, y_true, threshold = 0.5, eps = 1e-7):
|
|
74
|
+
# Prec = TP/(TP+FP)
|
|
75
|
+
y_pred_bin = (y_pred >threshold).float()
|
|
76
|
+
y_true = y_true.float()
|
|
77
|
+
# (0, 2, 3) means we are going to add the batch, H and W, we only keep the class
|
|
78
|
+
TP = torch.sum(y_pred_bin * y_true , dim= (0, 2, 3))
|
|
79
|
+
FP = torch.sum((1 - y_true) * y_pred_bin, dim= (0, 2, 3))
|
|
80
|
+
precision = TP/(TP + FP + eps)
|
|
81
|
+
return precision.tolist()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from .base_model import BaseModel
|
|
4
|
+
from .lib.convolutions import DoubleConv
|
|
5
|
+
|
|
6
|
+
class UnetTorch(nn.Module):
|
|
7
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64,
|
|
8
|
+
kernel_size=3, padding=1):
|
|
9
|
+
super(UnetTorch, self).__init__()
|
|
10
|
+
self.downs = nn.ModuleList()
|
|
11
|
+
self.ups = nn.ModuleList()
|
|
12
|
+
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
|
|
13
|
+
|
|
14
|
+
in_c = input_channels
|
|
15
|
+
out_c = initial_filters
|
|
16
|
+
for _ in range(depth):
|
|
17
|
+
self.downs.append(DoubleConv(in_c, out_c, kernel_size=kernel_size, padding=padding))
|
|
18
|
+
in_c = out_c
|
|
19
|
+
out_c *= 2
|
|
20
|
+
|
|
21
|
+
self.bottleneck = DoubleConv(in_c, out_c, kernel_size=kernel_size, padding=padding)
|
|
22
|
+
|
|
23
|
+
for _ in range(depth):
|
|
24
|
+
self.ups.append(nn.ConvTranspose2d(out_c, out_c // 2, kernel_size=2, stride=2))
|
|
25
|
+
|
|
26
|
+
self.ups.append(DoubleConv(out_c, out_c // 2, kernel_size=kernel_size, padding=padding))
|
|
27
|
+
|
|
28
|
+
out_c //= 2
|
|
29
|
+
|
|
30
|
+
self.final_conv = nn.Conv2d(out_c, output_channels, kernel_size=1)
|
|
31
|
+
|
|
32
|
+
def forward(self, x):
|
|
33
|
+
skip_connections = []
|
|
34
|
+
|
|
35
|
+
for down in self.downs:
|
|
36
|
+
x = down(x)
|
|
37
|
+
skip_connections.append(x)
|
|
38
|
+
x = self.pool(x)
|
|
39
|
+
|
|
40
|
+
x = self.bottleneck(x)
|
|
41
|
+
|
|
42
|
+
for i in range(0, len(self.ups), 2):
|
|
43
|
+
x = self.ups[i](x)
|
|
44
|
+
skip_connection = skip_connections[-(i // 2 + 1)]
|
|
45
|
+
|
|
46
|
+
if x.shape != skip_connection.shape:
|
|
47
|
+
x = nn.functional.interpolate(x, size=skip_connection.shape[2:], mode="bilinear", align_corners=True)
|
|
48
|
+
|
|
49
|
+
x = torch.cat((skip_connection, x), dim=1)
|
|
50
|
+
x = self.ups[i + 1](x)
|
|
51
|
+
|
|
52
|
+
return self.final_conv(x)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UNet(BaseModel):
|
|
56
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64, **kwargs):
|
|
57
|
+
super().__init__(name="UNet", **kwargs)
|
|
58
|
+
self.input_channels = input_channels
|
|
59
|
+
self.output_channels = output_channels
|
|
60
|
+
self.depth = depth
|
|
61
|
+
self.initial_filters = initial_filters
|
|
62
|
+
|
|
63
|
+
def create_model(self):
|
|
64
|
+
self.model = UnetTorch(
|
|
65
|
+
input_channels=self.input_channels,
|
|
66
|
+
output_channels=self.output_channels,
|
|
67
|
+
depth=self.depth,
|
|
68
|
+
initial_filters=self.initial_filters
|
|
69
|
+
)
|
|
70
|
+
return self.model
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from .base_model import BaseModel
|
|
4
|
+
from .lib.convolutions import DoubleConv
|
|
5
|
+
from .lib.attention import AttentionGate
|
|
6
|
+
|
|
7
|
+
class AttUnetTorch(nn.Module):
|
|
8
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64,
|
|
9
|
+
kernel_size=3, padding=1):
|
|
10
|
+
super(AttUnetTorch, self).__init__()
|
|
11
|
+
self.downs = nn.ModuleList()
|
|
12
|
+
self.ups = nn.ModuleList()
|
|
13
|
+
self.attentionGates = nn.ModuleList()
|
|
14
|
+
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
|
|
15
|
+
|
|
16
|
+
in_c = input_channels
|
|
17
|
+
encoders_output_channels = []
|
|
18
|
+
out_c = initial_filters
|
|
19
|
+
|
|
20
|
+
for _ in range(depth):
|
|
21
|
+
self.downs.append(DoubleConv(in_c, out_c, kernel_size=kernel_size, padding=padding))
|
|
22
|
+
in_c = out_c
|
|
23
|
+
out_c *= 2
|
|
24
|
+
encoders_output_channels.append(in_c)
|
|
25
|
+
|
|
26
|
+
self.bottleneck = DoubleConv(in_c, out_c, kernel_size=kernel_size, padding=padding)
|
|
27
|
+
|
|
28
|
+
for enc_c in reversed(encoders_output_channels):
|
|
29
|
+
dec_c = out_c //2
|
|
30
|
+
self.attentionGates.append(AttentionGate(enc_c, dec_c, enc_c//2))
|
|
31
|
+
self.ups.append(nn.ConvTranspose2d(out_c, dec_c, kernel_size=2, stride=2))
|
|
32
|
+
self.ups.append(DoubleConv(out_c, dec_c, kernel_size=kernel_size, padding=padding))
|
|
33
|
+
|
|
34
|
+
out_c = dec_c
|
|
35
|
+
self.final_conv = nn.Conv2d(out_c, output_channels, kernel_size=1)
|
|
36
|
+
|
|
37
|
+
def forward(self, x):
|
|
38
|
+
skip_connections = []
|
|
39
|
+
|
|
40
|
+
for down in self.downs:
|
|
41
|
+
x = down(x)
|
|
42
|
+
skip_connections.append(x)
|
|
43
|
+
x = self.pool(x)
|
|
44
|
+
|
|
45
|
+
x = self.bottleneck(x)
|
|
46
|
+
|
|
47
|
+
for i in range(0, len(self.ups), 2):
|
|
48
|
+
x = self.ups[i](x)
|
|
49
|
+
skip_connection = skip_connections[-(i // 2 + 1)]
|
|
50
|
+
|
|
51
|
+
if x.shape != skip_connection.shape:
|
|
52
|
+
x = nn.functional.interpolate(x, size=skip_connection.shape[2:], mode="bilinear", align_corners=True)
|
|
53
|
+
|
|
54
|
+
att_out = self.attentionGates[i // 2] (skip_connection, x)
|
|
55
|
+
x = torch.cat((att_out, x), dim=1)
|
|
56
|
+
x = self.ups[i + 1](x)
|
|
57
|
+
|
|
58
|
+
return self.final_conv(x)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AttentionUNet(BaseModel):
|
|
62
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64, **kwargs):
|
|
63
|
+
super().__init__(name="AttentionUNet", **kwargs)
|
|
64
|
+
self.input_channels = input_channels
|
|
65
|
+
self.output_channels = output_channels
|
|
66
|
+
self.depth = depth
|
|
67
|
+
self.initial_filters = initial_filters
|
|
68
|
+
|
|
69
|
+
def create_model(self):
|
|
70
|
+
self.model = AttUnetTorch(
|
|
71
|
+
input_channels=self.input_channels,
|
|
72
|
+
output_channels=self.output_channels,
|
|
73
|
+
depth=self.depth,
|
|
74
|
+
initial_filters=self.initial_filters
|
|
75
|
+
)
|
|
76
|
+
return self.model
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from torchinfo import summary
|
|
4
|
+
from torch.utils.data import DataLoader
|
|
5
|
+
from tqdm.auto import tqdm
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
class BaseModel(nn.Module):
|
|
9
|
+
def __init__(self, name: str = "BaseModel", model = None, metrics = None, loss_fn = None, optimizer = None, device = None):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.name = name
|
|
12
|
+
self.model = model
|
|
13
|
+
self.metrics = metrics
|
|
14
|
+
self.loss_fn = loss_fn
|
|
15
|
+
self.optimizer = optimizer
|
|
16
|
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
|
17
|
+
|
|
18
|
+
def help(self):
|
|
19
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
20
|
+
|
|
21
|
+
def create_model(self):
|
|
22
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
23
|
+
|
|
24
|
+
def train(self, train_loader: DataLoader,val_loader:DataLoader | None = None, epochs: int = 100, save_checkpoint = True):
|
|
25
|
+
if self.name == "BaseModel":
|
|
26
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
27
|
+
if self.model == None or self.name == "BaseModel":
|
|
28
|
+
raise ValueError("Model has not been created. Please call create_model() before training.")
|
|
29
|
+
if self.loss_fn == None:
|
|
30
|
+
raise ValueError("Loss function has not been set. Please set a loss function before training.")
|
|
31
|
+
if self.optimizer == None:
|
|
32
|
+
raise ValueError("Optimizer has not been set. Please set an optimizer before training.")
|
|
33
|
+
|
|
34
|
+
self.model.to(self.device)
|
|
35
|
+
for current_epoch in range(epochs):
|
|
36
|
+
# For each loop: a first phase of training and then validation.
|
|
37
|
+
# Training:
|
|
38
|
+
self.model.train()
|
|
39
|
+
total_loss = 0.0
|
|
40
|
+
total_steps = len(train_loader)
|
|
41
|
+
progress_bar = tqdm(
|
|
42
|
+
train_loader,
|
|
43
|
+
total=total_steps,
|
|
44
|
+
desc=f"Epoch {current_epoch + 1}/{epochs}",
|
|
45
|
+
unit="batch",
|
|
46
|
+
dynamic_ncols=True,
|
|
47
|
+
)
|
|
48
|
+
for idx, (X_batch, Y_batch) in enumerate(progress_bar, start=1):
|
|
49
|
+
X_batch, Y_batch = X_batch.to(self.device), Y_batch.to(self.device)
|
|
50
|
+
|
|
51
|
+
self.optimizer.zero_grad()
|
|
52
|
+
prediction = self.model(X_batch)
|
|
53
|
+
|
|
54
|
+
loss = self.loss_fn(prediction, Y_batch)
|
|
55
|
+
loss.backward()
|
|
56
|
+
self.optimizer.step()
|
|
57
|
+
batch_loss = loss.item()
|
|
58
|
+
total_loss += batch_loss
|
|
59
|
+
progress_bar.set_postfix(
|
|
60
|
+
batch_loss=f"{batch_loss:.4f}",
|
|
61
|
+
avg_loss=f"{total_loss / idx:.4f}",
|
|
62
|
+
)
|
|
63
|
+
avg_loss = total_loss / len(train_loader)
|
|
64
|
+
tqdm.write(f"Epoch {current_epoch + 1}/{epochs} — average loss: {avg_loss:.4f}")
|
|
65
|
+
|
|
66
|
+
# Validation:
|
|
67
|
+
if val_loader:
|
|
68
|
+
self.model.eval()
|
|
69
|
+
|
|
70
|
+
dict_metrics_sum = {}
|
|
71
|
+
|
|
72
|
+
val_progress_bar = tqdm(
|
|
73
|
+
val_loader,
|
|
74
|
+
total=len(val_loader),
|
|
75
|
+
desc=f"Epoch {current_epoch + 1}/{epochs}",
|
|
76
|
+
unit="batch",
|
|
77
|
+
dynamic_ncols=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Take a portion of the data and test:
|
|
81
|
+
with torch.no_grad():
|
|
82
|
+
for idx, (X_batch, Y_batch) in enumerate(val_progress_bar, start=1):
|
|
83
|
+
X_batch, Y_batch = X_batch.to(self.device), Y_batch.to(self.device)
|
|
84
|
+
|
|
85
|
+
prediction = self.model(X_batch)
|
|
86
|
+
|
|
87
|
+
for name_metric, metric_fn in self.metrics.items():
|
|
88
|
+
metric_value = metric_fn(prediction, Y_batch)
|
|
89
|
+
|
|
90
|
+
if isinstance(metric_value, torch.Tensor):
|
|
91
|
+
metric_value = metric_value.detach().cpu()
|
|
92
|
+
if metric_value.ndim > 1:
|
|
93
|
+
metric_value = metric_value.mean(dim=0)
|
|
94
|
+
|
|
95
|
+
if metric_value.ndim == 0:
|
|
96
|
+
metric_value = metric_value.item()
|
|
97
|
+
else:
|
|
98
|
+
metric_value = metric_value.tolist()
|
|
99
|
+
|
|
100
|
+
elif isinstance(metric_value, np.ndarray):
|
|
101
|
+
if metric_value.ndim > 1:
|
|
102
|
+
metric_value = metric_value.mean(axis=0)
|
|
103
|
+
|
|
104
|
+
if metric_value.ndim == 0:
|
|
105
|
+
metric_value = metric_value.item()
|
|
106
|
+
else:
|
|
107
|
+
metric_value = metric_value.tolist()
|
|
108
|
+
if isinstance(metric_value, list) and len(metric_value) > 0 and isinstance(metric_value[0], list):
|
|
109
|
+
metric_value = [sum(col) / len(col) for col in zip(*metric_value)]
|
|
110
|
+
|
|
111
|
+
if name_metric not in dict_metrics_sum:
|
|
112
|
+
if isinstance(metric_value, list):
|
|
113
|
+
dict_metrics_sum[name_metric] = [0.0] * len(metric_value)
|
|
114
|
+
else:
|
|
115
|
+
dict_metrics_sum[name_metric] = 0.0
|
|
116
|
+
|
|
117
|
+
if isinstance(metric_value, list):
|
|
118
|
+
for cls_i, val in enumerate(metric_value):
|
|
119
|
+
dict_metrics_sum[name_metric][cls_i] += float(val)
|
|
120
|
+
else:
|
|
121
|
+
dict_metrics_sum[name_metric] += float(metric_value)
|
|
122
|
+
|
|
123
|
+
num_batchs = len(val_loader)
|
|
124
|
+
metrics_summary_str = []
|
|
125
|
+
|
|
126
|
+
for name_metric, val_sum in dict_metrics_sum.items():
|
|
127
|
+
if isinstance(val_sum, list):
|
|
128
|
+
avg_list = [v/num_batchs for v in val_sum]
|
|
129
|
+
mean_all_classes = sum(avg_list)/len(avg_list)
|
|
130
|
+
|
|
131
|
+
class_details = ", ".join([f"C{i}:{v:.3f}" for i, v in enumerate(avg_list)])
|
|
132
|
+
metrics_summary_str.append(f"{name_metric}_Mean: {mean_all_classes:.4f} ({class_details})")
|
|
133
|
+
else:
|
|
134
|
+
avg_val = val_sum / num_batchs
|
|
135
|
+
metrics_summary_str.append(f"{name_metric}: {avg_val:.4f}")
|
|
136
|
+
|
|
137
|
+
formatted_metrics = " | ".join(metrics_summary_str)
|
|
138
|
+
tqdm.write(f"Epoch {current_epoch + 1}/{epochs} — Validation: {formatted_metrics}")
|
|
139
|
+
|
|
140
|
+
if save_checkpoint:
|
|
141
|
+
self.save_model(f"checkpoint_{self.name}.pt")
|
|
142
|
+
|
|
143
|
+
def predict(self, data):
|
|
144
|
+
if self.model == None or self.name == "BaseModel":
|
|
145
|
+
raise ValueError("Model has not been created. Please call create_model() before predicting.")
|
|
146
|
+
|
|
147
|
+
if data is not None:
|
|
148
|
+
data = data.to(self.device)
|
|
149
|
+
self.model.eval()
|
|
150
|
+
with torch.no_grad():
|
|
151
|
+
return self.model(data)
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError("Data is None!")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def evaluate(self, eval_loader: DataLoader):
|
|
157
|
+
if self.model == None or self.name == "BaseModel":
|
|
158
|
+
raise ValueError("Model has not been created. Please call create_model() before evaluating.")
|
|
159
|
+
if self.metrics is None:
|
|
160
|
+
raise ValueError("No metrics have been defined for evaluation. Please define metrics before evaluating.")
|
|
161
|
+
|
|
162
|
+
self.model.eval()
|
|
163
|
+
dict_metrics_sum = {}
|
|
164
|
+
|
|
165
|
+
with torch.no_grad():
|
|
166
|
+
for X_batch, Y_batch in eval_loader:
|
|
167
|
+
X_batch, Y_batch = X_batch.to(self.device), Y_batch.to(self.device)
|
|
168
|
+
predictions = self.model(X_batch)
|
|
169
|
+
|
|
170
|
+
for name_metric, metric_formula in self.metrics.items():
|
|
171
|
+
metric_value = metric_formula(predictions, Y_batch)
|
|
172
|
+
|
|
173
|
+
if isinstance(metric_value, torch.Tensor):
|
|
174
|
+
metric_value = metric_value.cpu().tolist()
|
|
175
|
+
elif isinstance(metric_value, np.ndarray):
|
|
176
|
+
metric_value = metric_value.tolist()
|
|
177
|
+
|
|
178
|
+
if name_metric not in dict_metrics_sum:
|
|
179
|
+
if isinstance(metric_value, list):
|
|
180
|
+
dict_metrics_sum[name_metric] = [0.0] * len(metric_value)
|
|
181
|
+
else:
|
|
182
|
+
dict_metrics_sum[name_metric] = 0.0
|
|
183
|
+
|
|
184
|
+
if isinstance(metric_value, list):
|
|
185
|
+
for cls_i, val in enumerate(metric_value):
|
|
186
|
+
dict_metrics_sum[name_metric][cls_i] += val
|
|
187
|
+
else:
|
|
188
|
+
dict_metrics_sum[name_metric] += metric_value
|
|
189
|
+
|
|
190
|
+
num_batches = len(eval_loader)
|
|
191
|
+
final_metrics = {}
|
|
192
|
+
|
|
193
|
+
for name_metrics, val_sum in dict_metrics_sum.items():
|
|
194
|
+
if isinstance(val_sum, list):
|
|
195
|
+
final_metrics[name_metric] = [v / num_batches for v in val_sum]
|
|
196
|
+
else:
|
|
197
|
+
final_metrics[name_metric] = val_sum / num_batches
|
|
198
|
+
|
|
199
|
+
return final_metrics
|
|
200
|
+
|
|
201
|
+
def get_model_info(self):
|
|
202
|
+
if self.model == None or self.name == "BaseModel":
|
|
203
|
+
raise ValueError("Model has not been created. Please call create_model() before getting model info.")
|
|
204
|
+
|
|
205
|
+
return summary(self.model)
|
|
206
|
+
|
|
207
|
+
def save_model(self, file_path):
|
|
208
|
+
self.model.eval()
|
|
209
|
+
torch.save(self.model.state_dict(), file_path)
|
|
210
|
+
print(f"Model saved to {file_path}")
|
|
211
|
+
|
|
212
|
+
def load_model(self, file_path, is_train_mode=False):
|
|
213
|
+
self.model.load_state_dict(torch.load(file_path, weights_only=True, map_location=self.device))
|
|
214
|
+
if is_train_mode:
|
|
215
|
+
self.model.train()
|
|
216
|
+
else:
|
|
217
|
+
self.model.eval()
|
|
218
|
+
|
|
219
|
+
# To make sure the modele is on the correct device:
|
|
220
|
+
self.model.to(self.device)
|
|
221
|
+
print(f"Model loaded from {file_path}, model is in {self.device} mode.")
|
|
222
|
+
|
|
223
|
+
def save_model_in_onnx(self, file_path, input_sample):
|
|
224
|
+
self.model.eval()
|
|
225
|
+
input_sample = input_sample.to(self.device)
|
|
226
|
+
torch.onnx.export(self.model, input_sample, file_path)
|
|
227
|
+
print(f"Model saved in ONNX format to {file_path}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
class AttentionGate(nn.Module):
|
|
5
|
+
def __init__(self, in_channels_encoder, in_channels_decoder, interchannel):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.conv_enc = nn.Conv2d(in_channels_encoder, interchannel, kernel_size=1, stride=1, padding=0, bias=True)
|
|
8
|
+
self.conv_dec = nn.Conv2d(in_channels_decoder, interchannel, kernel_size=1, stride=1, padding=0, bias=True)
|
|
9
|
+
self.final_conv = nn.Conv2d(interchannel, 1, kernel_size=1, stride=1, padding=0, bias=True )
|
|
10
|
+
|
|
11
|
+
self.sigmoid = nn.Sigmoid()
|
|
12
|
+
self.relu = nn.ReLU(inplace=True)
|
|
13
|
+
|
|
14
|
+
def forward(self, encoder, decoder):
|
|
15
|
+
enc = self.conv_enc(encoder)
|
|
16
|
+
dec = self.conv_dec(decoder)
|
|
17
|
+
sum = self.relu(enc + dec)
|
|
18
|
+
x = self.sigmoid(self.final_conv(sum))
|
|
19
|
+
|
|
20
|
+
return encoder * x
|
|
21
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
class DoubleConv(nn.Module):
|
|
5
|
+
def __init__(self, channel_in, channel_out, kernel_size = 3, padding = 1):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.conv1 = nn.Conv2d(in_channels=channel_in , out_channels=channel_out, kernel_size=kernel_size, padding=padding)
|
|
8
|
+
self.conv2 = nn.Conv2d(in_channels=channel_out, out_channels=channel_out, kernel_size=kernel_size, padding=padding)
|
|
9
|
+
|
|
10
|
+
def forward(self, x):
|
|
11
|
+
x = self.conv1(x)
|
|
12
|
+
x = nn.ReLU()(x)
|
|
13
|
+
x = self.conv2(x)
|
|
14
|
+
x = nn.ReLU()(x)
|
|
15
|
+
return x
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from .base_model import BaseModel
|
|
5
|
+
from .lib.convolutions import DoubleConv
|
|
6
|
+
|
|
7
|
+
class UnetPPTorch(nn.Module):
|
|
8
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64,
|
|
9
|
+
kernel_size=3, padding=1):
|
|
10
|
+
super(UnetPPTorch, self).__init__()
|
|
11
|
+
self.depth = depth
|
|
12
|
+
self.downs = nn.ModuleList()
|
|
13
|
+
self.inter_down = nn.ModuleList()
|
|
14
|
+
|
|
15
|
+
filters = [initial_filters * (2**i) for i in range(self.depth + 1)]
|
|
16
|
+
self.pool = nn.MaxPool2d(kernel_size=2, stride= 2)
|
|
17
|
+
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
|
|
18
|
+
|
|
19
|
+
self.convs = nn.ModuleDict()
|
|
20
|
+
|
|
21
|
+
for i in range(self.depth + 1):
|
|
22
|
+
for j in range (self.depth + 1 -i):
|
|
23
|
+
if j == 0:
|
|
24
|
+
in_c = input_channels if i == 0 else filters[i-1]
|
|
25
|
+
out_c = filters[i]
|
|
26
|
+
|
|
27
|
+
else:
|
|
28
|
+
in_c = j * filters[i] + filters[i + 1]
|
|
29
|
+
out_c = filters[i]
|
|
30
|
+
|
|
31
|
+
self.convs[f"conv_{i}_{j}"] = DoubleConv(in_c, out_c, kernel_size=kernel_size, padding=padding)
|
|
32
|
+
|
|
33
|
+
self.final_conv = DoubleConv(filters[0], output_channels, kernel_size=kernel_size, padding=padding)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def forward(self, x):
|
|
37
|
+
x_node = {}
|
|
38
|
+
x_node["0_0"] = self.convs["conv_0_0"](x)
|
|
39
|
+
|
|
40
|
+
# All the X_node on the left, the one called X^[i, 0]:
|
|
41
|
+
for i in range(1, self.depth + 1):
|
|
42
|
+
x_down = self.pool(x_node[f"{i-1}_0"])
|
|
43
|
+
x_node[f"{i}_0"] = self.convs[f"conv_{i}_0"](x_down)
|
|
44
|
+
|
|
45
|
+
# Loop for X^[i, j]
|
|
46
|
+
for j in range(1, self.depth +1):
|
|
47
|
+
for i in range(self.depth + 1 -j):
|
|
48
|
+
x_up = self.up(x_node[f"{i+1}_{j-1}"])
|
|
49
|
+
|
|
50
|
+
if x_up.shape[2:] != x_node[f'{i}_0'].shape[2:]:
|
|
51
|
+
x_up = F.interpolate(
|
|
52
|
+
x_up, size=x_node[f'{i}_0'].shape[2:],
|
|
53
|
+
mode="bilinear", align_corners=True
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
skip_concat = [x_node[f'{i}_{k}'] for k in range(j)]
|
|
57
|
+
skip_concat.append(x_up)
|
|
58
|
+
|
|
59
|
+
concat_features = torch.cat(skip_concat, dim=1)
|
|
60
|
+
x_node[f'{i}_{j}'] = self.convs[f'conv_{i}_{j}'](concat_features)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# TODO: implementation of Deep supervision:
|
|
64
|
+
# if self.deepsupervision:
|
|
65
|
+
# # TODO
|
|
66
|
+
# else:
|
|
67
|
+
|
|
68
|
+
return self.final_conv(x_node[f"0_{self.depth}"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UNetPP(BaseModel):
|
|
72
|
+
def __init__(self, input_channels=1, output_channels=1, depth=4, initial_filters=64, **kwargs):
|
|
73
|
+
super().__init__(name="UNet++", **kwargs)
|
|
74
|
+
self.input_channels = input_channels
|
|
75
|
+
self.output_channels = output_channels
|
|
76
|
+
self.depth = depth
|
|
77
|
+
self.initial_filters = initial_filters
|
|
78
|
+
|
|
79
|
+
def create_model(self):
|
|
80
|
+
self.model = UnetPPTorch(
|
|
81
|
+
input_channels=self.input_channels,
|
|
82
|
+
output_channels=self.output_channels,
|
|
83
|
+
depth=self.depth,
|
|
84
|
+
initial_filters=self.initial_filters
|
|
85
|
+
)
|
|
86
|
+
return self.model
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openmlcore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenMLCore is an open source framework (wrapper which simplify the use of PyTorch)
|
|
5
|
+
Author-email: Doyez Sébastien <doyez.sebastien34090@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Votre Nom
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://sebastien-doyez2812.github.io/openMLCore.github.io/
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Requires-Python: >=3.10.0
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: torch
|
|
35
|
+
Requires-Dist: tqdm
|
|
36
|
+
Requires-Dist: onnx
|
|
37
|
+
Requires-Dist: onnxscript
|
|
38
|
+
Requires-Dist: torchinfo
|
|
39
|
+
Requires-Dist: datasets
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# OpenML-core, Open source Package for Machine learning
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Why OpenML?
|
|
46
|
+
Today's computer vision ecosystem is heavily fragmented. If you want YOLO, Ultralytics makes it trivial. But the moment you need advanced semantic segmentation, hybrid transformer backbones, or efficient fine-tuning on modern architectures, you end up stitching together disjointed codebases, mismatched tensor shapes, and unmaintained repositories.
|
|
47
|
+
|
|
48
|
+
OpenML bridges this gap by bringing a clean, Hugging Face-inspired Developer Experience (DX) to advanced computer vision tasks—from classical models to SOTA transformers and parameter-efficient fine-tuning.
|
|
49
|
+
## Supported Architectures:
|
|
50
|
+
|
|
51
|
+
- Classic & Deep Segmentation: U-Net, Attention U-Net, UNet++, Mask R-CNN
|
|
52
|
+
|
|
53
|
+
- Advanced Transformers: Swin Transformer, TransUNet, MobileUNetR
|
|
54
|
+
|
|
55
|
+
- SOTA & Edge: Vision Transformers, SAM 2 adaptations via efficient Low-Rank Adaptation (LoRA)
|
|
56
|
+
## Key features:
|
|
57
|
+
|
|
58
|
+
Unified Object-Oriented API: Every model inherits from a clean ModelBase class supporting standard .train(), .eval(), and .help() workflows.
|
|
59
|
+
|
|
60
|
+
Native LoRA Wrappers: Fine-tune massive vision models and transformers on modest hardware (like a single RTX 3070) without blowing up your VRAM.
|
|
61
|
+
|
|
62
|
+
Hugging Face Hub Integration: Push and pull checkpoints effortlessly using native .safetensors formatting.
|
|
63
|
+
|
|
64
|
+
MLflow Tracking Out-of-the-Box: Keep tabs on your losses, metrics, and hyperparameter logs without writing boilerplate monitoring code.
|
|
65
|
+
|
|
66
|
+
Interactive Help System: Running model.help() directly opens up the exact architectural documentation page on the web.
|
|
67
|
+
|
|
68
|
+
## Contributions:
|
|
69
|
+
|
|
70
|
+
Contributions, feature requests, and bug reports are welcome! Feel free to open an Issue or submit a Pull Request on GitHub.
|
|
71
|
+
|
|
72
|
+
## Author
|
|
73
|
+
|
|
74
|
+
Sébastien Doyez
|
|
75
|
+
ML & AI Engineer,
|
|
76
|
+
Interested in Agentics, Computer Vision, Robotics & Deep Learning
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/openmlcore/__init__.py
|
|
5
|
+
src/openmlcore.egg-info/PKG-INFO
|
|
6
|
+
src/openmlcore.egg-info/SOURCES.txt
|
|
7
|
+
src/openmlcore.egg-info/dependency_links.txt
|
|
8
|
+
src/openmlcore.egg-info/requires.txt
|
|
9
|
+
src/openmlcore.egg-info/top_level.txt
|
|
10
|
+
src/openmlcore/base/data.py
|
|
11
|
+
src/openmlcore/loss/loss.py
|
|
12
|
+
src/openmlcore/metrics/metrics.py
|
|
13
|
+
src/openmlcore/models/UNets.py
|
|
14
|
+
src/openmlcore/models/attUnets.py
|
|
15
|
+
src/openmlcore/models/base_model.py
|
|
16
|
+
src/openmlcore/models/unetpp.py
|
|
17
|
+
src/openmlcore/models/lib/attention.py
|
|
18
|
+
src/openmlcore/models/lib/convolutions.py
|
|
19
|
+
tests/test_base_model.py
|
|
20
|
+
tests/test_loss.py
|
|
21
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openmlcore
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
from torch.utils.data import DataLoader
|
|
5
|
+
from torchgen import model
|
|
6
|
+
from torchgen import model
|
|
7
|
+
from openmlcore.models.base_model import *
|
|
8
|
+
from openmlcore.metrics.metrics import *
|
|
9
|
+
|
|
10
|
+
# class DummyModel(BaseModel):
|
|
11
|
+
# def __init__(self):
|
|
12
|
+
# super().__init__(name = "DummyLinearModel")
|
|
13
|
+
# self.create_model()
|
|
14
|
+
# self.loss_fn = nn.CrossEntropyLoss()
|
|
15
|
+
# self.optimizer = torch.optim.SGD(self.model.parameters(), lr=0.01)
|
|
16
|
+
# self.metrics = [("MSE", lambda pred, target: nn.functional.mse_loss(pred, target))]
|
|
17
|
+
|
|
18
|
+
# def help(self):
|
|
19
|
+
# print("This is a dummy linear model for testing purposes.")
|
|
20
|
+
|
|
21
|
+
# def create_model(self):
|
|
22
|
+
# self.fc = nn.Linear(10, 2)
|
|
23
|
+
# self.model = self.fc
|
|
24
|
+
|
|
25
|
+
class DummyModel(BaseModel):
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__(name="DummyLinearModel")
|
|
28
|
+
# On appelle super().__init__() avant d'assigner self.model
|
|
29
|
+
self.create_model()
|
|
30
|
+
self.loss_fn = nn.CrossEntropyLoss()
|
|
31
|
+
self.optimizer = torch.optim.SGD(self.model.parameters(), lr=0.01)
|
|
32
|
+
self.metrics = {
|
|
33
|
+
"Accuracy": accuracy,
|
|
34
|
+
"IoU": iou
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def help(self):
|
|
38
|
+
print("This is a dummy linear model for testing purposes.")
|
|
39
|
+
|
|
40
|
+
def create_model(self):
|
|
41
|
+
# Utiliser nn.Sequential garantit que tous les sous-modules
|
|
42
|
+
# basculent sur le bon device en même temps que self.model
|
|
43
|
+
self.model = nn.Sequential(
|
|
44
|
+
nn.Linear(10, 2)
|
|
45
|
+
).to(self.device)
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def dummy_dataloader():
|
|
49
|
+
X = torch.randn(64, 10)
|
|
50
|
+
Y = torch.randn(64, 2)
|
|
51
|
+
dataset = torch.utils.data.TensorDataset(X, Y)
|
|
52
|
+
return DataLoader(dataset, batch_size=8, shuffle=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_abstract_base_modeel():
|
|
56
|
+
base_model = BaseModel()
|
|
57
|
+
with pytest.raises(NotImplementedError):
|
|
58
|
+
base_model.create_model()
|
|
59
|
+
|
|
60
|
+
with pytest.raises(NotImplementedError):
|
|
61
|
+
base_model.help()
|
|
62
|
+
|
|
63
|
+
def test_training_exceptions():
|
|
64
|
+
base_model = BaseModel()
|
|
65
|
+
loader = torch.utils.data.DataLoader(torch.randn(64, 10), batch_size=8)
|
|
66
|
+
|
|
67
|
+
with pytest.raises(NotImplementedError):
|
|
68
|
+
base_model.train(loader, epochs=1)
|
|
69
|
+
|
|
70
|
+
def test_model_training(dummy_dataloader):
|
|
71
|
+
model = DummyModel()
|
|
72
|
+
model.train(dummy_dataloader, epochs=2)
|
|
73
|
+
|
|
74
|
+
def test_model_prediction(dummy_dataloader):
|
|
75
|
+
model = DummyModel()
|
|
76
|
+
print(model.device)
|
|
77
|
+
X_sample = torch.randn(5, 10).to(model.device)
|
|
78
|
+
preds = model.predict(X_sample)
|
|
79
|
+
assert preds.shape == (5, 2)
|
|
80
|
+
|
|
81
|
+
def test_model_save_and_load(tmp_path, dummy_dataloader):
|
|
82
|
+
model = DummyModel()
|
|
83
|
+
file_path = tmp_path / "model.pt"
|
|
84
|
+
|
|
85
|
+
model.save_model(str(file_path))
|
|
86
|
+
assert file_path.exists()
|
|
87
|
+
|
|
88
|
+
new_model = DummyModel()
|
|
89
|
+
new_model.load_model(str(file_path), is_train_mode=False)
|
|
90
|
+
|
|
91
|
+
for p1, p2 in zip(model.model.parameters(), new_model.model.parameters()):
|
|
92
|
+
assert torch.equal(p1, p2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_onnx_export(tmp_path):
|
|
96
|
+
model = DummyModel()
|
|
97
|
+
file_path = tmp_path / "model.onnx"
|
|
98
|
+
dummy_input = torch.randn(1, 10).to(model.device)
|
|
99
|
+
|
|
100
|
+
model.save_model_in_onnx(str(file_path), dummy_input)
|
|
101
|
+
assert file_path.exists()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from openmlcore.loss.loss import *
|
|
2
|
+
|
|
3
|
+
def test_BCEWithLogits_loss():
|
|
4
|
+
loss = BCEWithLogitsLoss()
|
|
5
|
+
assert loss is not None
|
|
6
|
+
|
|
7
|
+
def test_mean_squared_error_loss():
|
|
8
|
+
loss = MeanSquaredErrorLoss()
|
|
9
|
+
assert loss is not None
|
|
10
|
+
|
|
11
|
+
def test_dice_loss():
|
|
12
|
+
loss = DiceLoss()
|
|
13
|
+
assert loss is not None
|
|
14
|
+
|
|
15
|
+
def test_focal_loss():
|
|
16
|
+
loss = FocalLoss()
|
|
17
|
+
assert loss is not None
|
|
18
|
+
|
|
19
|
+
def test_huber_loss():
|
|
20
|
+
loss = HuberLoss()
|
|
21
|
+
assert loss is not None
|
|
22
|
+
|
|
23
|
+
def test_kl_divergence_loss():
|
|
24
|
+
loss = KLDivergenceLoss()
|
|
25
|
+
assert loss is not None
|
|
26
|
+
|
|
27
|
+
|