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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,6 @@
1
+ torch
2
+ tqdm
3
+ onnx
4
+ onnxscript
5
+ torchinfo
6
+ datasets
@@ -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
+
@@ -0,0 +1,4 @@
1
+ # TODO: implement tests here...
2
+
3
+ def test_example():
4
+ assert 1 + 1 == 2