quadra 2.4.0a0__py3-none-any.whl → 2.5.1__py3-none-any.whl
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.
- quadra/__init__.py +1 -1
- quadra/configs/core/default.yaml +1 -0
- quadra/tasks/anomaly.py +29 -3
- quadra/tasks/classification.py +12 -1
- quadra/utils/anomaly.py +35 -0
- quadra/utils/export.py +86 -11
- quadra/utils/utils.py +70 -35
- {quadra-2.4.0a0.dist-info → quadra-2.5.1.dist-info}/METADATA +3 -2
- {quadra-2.4.0a0.dist-info → quadra-2.5.1.dist-info}/RECORD +12 -12
- {quadra-2.4.0a0.dist-info → quadra-2.5.1.dist-info}/WHEEL +1 -1
- {quadra-2.4.0a0.dist-info → quadra-2.5.1.dist-info}/entry_points.txt +0 -0
- {quadra-2.4.0a0.dist-info → quadra-2.5.1.dist-info/licenses}/LICENSE +0 -0
quadra/__init__.py
CHANGED
quadra/configs/core/default.yaml
CHANGED
quadra/tasks/anomaly.py
CHANGED
|
@@ -244,7 +244,7 @@ class AnomalibDetection(Generic[AnomalyDataModuleT], LightningTask[AnomalyDataMo
|
|
|
244
244
|
):
|
|
245
245
|
threshold = torch.tensor(100.0)
|
|
246
246
|
else:
|
|
247
|
-
threshold = self.module.image_metrics.F1Score.threshold
|
|
247
|
+
threshold = self.module.image_metrics.F1Score.threshold # type: ignore[union-attr,assignment]
|
|
248
248
|
|
|
249
249
|
# The output of the prediction is a normalized score so the cumulative histogram is displayed with the
|
|
250
250
|
# normalized scores
|
|
@@ -328,6 +328,22 @@ class AnomalibDetection(Generic[AnomalyDataModuleT], LightningTask[AnomalyDataMo
|
|
|
328
328
|
else:
|
|
329
329
|
utils.upload_file_tensorboard(a, tensorboard_logger)
|
|
330
330
|
|
|
331
|
+
def execute(self):
|
|
332
|
+
"""Execute the experiment and all the steps."""
|
|
333
|
+
self.prepare()
|
|
334
|
+
self.train()
|
|
335
|
+
# When training in fp16 mixed precision, export function casts model weights from fp32 to fp16,
|
|
336
|
+
# for this reason, predictions logits could slightly change and predictions could be inconsistent between
|
|
337
|
+
# test and generated report.
|
|
338
|
+
# Performing export before test allows to have consistent results in test metrics and generated report.
|
|
339
|
+
if self.config.export is not None and len(self.config.export.types) > 0:
|
|
340
|
+
self.export()
|
|
341
|
+
if self.run_test:
|
|
342
|
+
self.test()
|
|
343
|
+
if self.report:
|
|
344
|
+
self.generate_report()
|
|
345
|
+
self.finalize()
|
|
346
|
+
|
|
331
347
|
|
|
332
348
|
class AnomalibEvaluation(Evaluation[AnomalyDataModule]):
|
|
333
349
|
"""Evaluation task for Anomalib.
|
|
@@ -445,12 +461,22 @@ class AnomalibEvaluation(Evaluation[AnomalyDataModule]):
|
|
|
445
461
|
training_threshold = self.model_data[f"{self.training_threshold_type}_threshold"]
|
|
446
462
|
optimal_threshold = self.metadata["threshold"]
|
|
447
463
|
|
|
448
|
-
normalized_optimal_threshold = cast(float, normalize_anomaly_score(optimal_threshold, training_threshold))
|
|
449
|
-
|
|
450
464
|
os.makedirs(os.path.join(self.report_path, "predictions"), exist_ok=True)
|
|
451
465
|
os.makedirs(os.path.join(self.report_path, "heatmaps"), exist_ok=True)
|
|
452
466
|
|
|
453
467
|
anomaly_scores = self.metadata["anomaly_scores"].cpu().numpy()
|
|
468
|
+
|
|
469
|
+
# The reason I have to expand dims and cast the optimal threshold to anomaly_scores dtype is because
|
|
470
|
+
# of internal roundings performed differently by numpy and python
|
|
471
|
+
# Particularly the normalized_optimal_threshold computed directly using float values might be higher than the
|
|
472
|
+
# actual value obtained by the anomaly_scores
|
|
473
|
+
normalized_optimal_threshold = cast(
|
|
474
|
+
np.ndarray,
|
|
475
|
+
normalize_anomaly_score(
|
|
476
|
+
np.expand_dims(np.array(optimal_threshold, dtype=anomaly_scores.dtype), -1), training_threshold
|
|
477
|
+
),
|
|
478
|
+
).item()
|
|
479
|
+
|
|
454
480
|
anomaly_scores = normalize_anomaly_score(anomaly_scores, training_threshold)
|
|
455
481
|
|
|
456
482
|
if not isinstance(anomaly_scores, np.ndarray):
|
quadra/tasks/classification.py
CHANGED
|
@@ -307,6 +307,14 @@ class Classification(Generic[ClassificationDataModuleT], LightningTask[Classific
|
|
|
307
307
|
# TODO: What happens if we have 64 precision?
|
|
308
308
|
half_precision = "16" in self.trainer.precision
|
|
309
309
|
|
|
310
|
+
example_input: torch.Tensor | None = None
|
|
311
|
+
|
|
312
|
+
if hasattr(self.trainer, "datamodule") and hasattr(self.trainer.datamodule, "val_dataset"):
|
|
313
|
+
# Retrieve a better input to evaluate fp16 performance or efficientnetb0 does not sometimes export properly
|
|
314
|
+
example_input = self.trainer.datamodule.val_dataset[0][0]
|
|
315
|
+
|
|
316
|
+
# Selected rtol and atol are quite high, this is mostly done for efficientnetb0 that seems to be
|
|
317
|
+
# quite unstable in fp16
|
|
310
318
|
self.model_json, export_paths = export_model(
|
|
311
319
|
config=self.config,
|
|
312
320
|
model=module.model,
|
|
@@ -314,6 +322,9 @@ class Classification(Generic[ClassificationDataModuleT], LightningTask[Classific
|
|
|
314
322
|
half_precision=half_precision,
|
|
315
323
|
input_shapes=input_shapes,
|
|
316
324
|
idx_to_class=idx_to_class,
|
|
325
|
+
example_inputs=example_input,
|
|
326
|
+
rtol=0.05,
|
|
327
|
+
atol=0.01,
|
|
317
328
|
)
|
|
318
329
|
|
|
319
330
|
if len(export_paths) == 0:
|
|
@@ -1136,7 +1147,7 @@ class ClassificationEvaluation(Evaluation[ClassificationDataModuleT]):
|
|
|
1136
1147
|
return
|
|
1137
1148
|
|
|
1138
1149
|
if isinstance(self.deployment_model.model.features_extractor, timm.models.resnet.ResNet):
|
|
1139
|
-
target_layers = [cast(BaseNetworkBuilder, self.deployment_model.model).features_extractor.layer4[-1]]
|
|
1150
|
+
target_layers = [cast(BaseNetworkBuilder, self.deployment_model.model).features_extractor.layer4[-1]] # type: ignore[index]
|
|
1140
1151
|
self.cam = GradCAM(
|
|
1141
1152
|
model=self.deployment_model.model,
|
|
1142
1153
|
target_layers=target_layers,
|
quadra/utils/anomaly.py
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
8
10
|
try:
|
|
9
11
|
from typing import Any, TypeAlias
|
|
10
12
|
except ImportError:
|
|
@@ -43,6 +45,39 @@ def normalize_anomaly_score(raw_score: MapOrValue, threshold: float) -> MapOrVal
|
|
|
43
45
|
else:
|
|
44
46
|
normalized_score = 200.0 - ((raw_score / threshold) * 100.0)
|
|
45
47
|
|
|
48
|
+
# Ensures that the normalized scores are consistent with the raw scores
|
|
49
|
+
# For all the items whose prediction changes after normalization, force the normalized score to be
|
|
50
|
+
# consistent with the prediction made on the raw score by clipping the score:
|
|
51
|
+
# - to 100.0 if the prediction was "anomaly" on the raw score and "good" on the normalized score
|
|
52
|
+
# - to 99.99 if the prediction was "good" on the raw score and "anomaly" on the normalized score
|
|
53
|
+
score = raw_score
|
|
54
|
+
if isinstance(score, torch.Tensor):
|
|
55
|
+
score = score.cpu().numpy()
|
|
56
|
+
# Anomalib classify as anomaly if anomaly_score gte threshold
|
|
57
|
+
is_anomaly_mask = score >= threshold
|
|
58
|
+
is_not_anomaly_mask = np.bitwise_not(is_anomaly_mask)
|
|
59
|
+
if isinstance(normalized_score, torch.Tensor):
|
|
60
|
+
if normalized_score.dim() == 0:
|
|
61
|
+
normalized_score = (
|
|
62
|
+
normalized_score.clamp(min=100.0) if is_anomaly_mask else normalized_score.clamp(max=99.99)
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
normalized_score[is_anomaly_mask] = normalized_score[is_anomaly_mask].clamp(min=100.0)
|
|
66
|
+
normalized_score[is_not_anomaly_mask] = normalized_score[is_not_anomaly_mask].clamp(max=99.99)
|
|
67
|
+
elif isinstance(normalized_score, np.ndarray) or np.isscalar(normalized_score):
|
|
68
|
+
if np.isscalar(normalized_score) or normalized_score.ndim == 0: # type: ignore[union-attr]
|
|
69
|
+
normalized_score = (
|
|
70
|
+
np.clip(normalized_score, a_min=100.0, a_max=None)
|
|
71
|
+
if is_anomaly_mask
|
|
72
|
+
else np.clip(normalized_score, a_min=None, a_max=99.99)
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
normalized_score = cast(np.ndarray, normalized_score)
|
|
76
|
+
normalized_score[is_anomaly_mask] = np.clip(normalized_score[is_anomaly_mask], a_min=100.0, a_max=None)
|
|
77
|
+
normalized_score[is_not_anomaly_mask] = np.clip(
|
|
78
|
+
normalized_score[is_not_anomaly_mask], a_min=None, a_max=99.99
|
|
79
|
+
)
|
|
80
|
+
|
|
46
81
|
if isinstance(normalized_score, torch.Tensor):
|
|
47
82
|
return torch.clamp(normalized_score, 0.0, 1000.0)
|
|
48
83
|
|
quadra/utils/export.py
CHANGED
|
@@ -119,6 +119,7 @@ def export_torchscript_model(
|
|
|
119
119
|
input_shapes: list[Any] | None = None,
|
|
120
120
|
half_precision: bool = False,
|
|
121
121
|
model_name: str = "model.pt",
|
|
122
|
+
example_inputs: list[torch.Tensor] | tuple[torch.Tensor, ...] | torch.Tensor | None = None,
|
|
122
123
|
) -> tuple[str, Any] | None:
|
|
123
124
|
"""Export a PyTorch model with TorchScript.
|
|
124
125
|
|
|
@@ -128,6 +129,8 @@ def export_torchscript_model(
|
|
|
128
129
|
output_path: Path to save the model
|
|
129
130
|
half_precision: If True, the model will be exported with half precision
|
|
130
131
|
model_name: Name of the exported model
|
|
132
|
+
example_inputs: If provided use this to evaluate the model instead of generating random inputs, it's expected to
|
|
133
|
+
be a list of tensors or a single tensor without batch dimension
|
|
131
134
|
|
|
132
135
|
Returns:
|
|
133
136
|
If the model is exported successfully, the path to the model and the input shape are returned.
|
|
@@ -144,7 +147,32 @@ def export_torchscript_model(
|
|
|
144
147
|
else:
|
|
145
148
|
model.cpu()
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
batch_size = 1
|
|
151
|
+
model_inputs: tuple[list[Any] | tuple[Any, ...] | torch.Tensor, list[Any]] | None
|
|
152
|
+
if example_inputs is not None:
|
|
153
|
+
if isinstance(example_inputs, Sequence):
|
|
154
|
+
model_input_tensors = []
|
|
155
|
+
model_input_shapes = []
|
|
156
|
+
|
|
157
|
+
for example_input in example_inputs:
|
|
158
|
+
new_inp = example_input.to(
|
|
159
|
+
device="cuda:0" if half_precision else "cpu",
|
|
160
|
+
dtype=torch.float16 if half_precision else torch.float32,
|
|
161
|
+
)
|
|
162
|
+
new_inp = new_inp.unsqueeze(0).repeat(batch_size, *(1 for x in new_inp.shape))
|
|
163
|
+
model_input_tensors.append(new_inp)
|
|
164
|
+
model_input_shapes.append(new_inp[0].shape)
|
|
165
|
+
|
|
166
|
+
model_inputs = (model_input_tensors, [model_input_shapes])
|
|
167
|
+
else:
|
|
168
|
+
new_inp = example_inputs.to(
|
|
169
|
+
device="cuda:0" if half_precision else "cpu",
|
|
170
|
+
dtype=torch.float16 if half_precision else torch.float32,
|
|
171
|
+
)
|
|
172
|
+
new_inp = new_inp.unsqueeze(0).repeat(batch_size, *(1 for x in new_inp.shape))
|
|
173
|
+
model_inputs = (new_inp, [new_inp[0].shape])
|
|
174
|
+
else:
|
|
175
|
+
model_inputs = extract_torch_model_inputs(model, input_shapes, half_precision)
|
|
148
176
|
|
|
149
177
|
if model_inputs is None:
|
|
150
178
|
return None
|
|
@@ -182,6 +210,9 @@ def export_onnx_model(
|
|
|
182
210
|
input_shapes: list[Any] | None = None,
|
|
183
211
|
half_precision: bool = False,
|
|
184
212
|
model_name: str = "model.onnx",
|
|
213
|
+
example_inputs: list[torch.Tensor] | tuple[torch.Tensor, ...] | torch.Tensor | None = None,
|
|
214
|
+
rtol: float = 0.01,
|
|
215
|
+
atol: float = 5e-3,
|
|
185
216
|
) -> tuple[str, Any] | None:
|
|
186
217
|
"""Export a PyTorch model with ONNX.
|
|
187
218
|
|
|
@@ -192,6 +223,10 @@ def export_onnx_model(
|
|
|
192
223
|
onnx_config: ONNX export configuration
|
|
193
224
|
half_precision: If True, the model will be exported with half precision
|
|
194
225
|
model_name: Name of the exported model
|
|
226
|
+
example_inputs: If provided use this to evaluate the model instead of generating random inputs, it's expected to
|
|
227
|
+
be a list of tensors or a single tensor without batch dimension
|
|
228
|
+
rtol: Relative tolerance for the ONNX safe export in fp16
|
|
229
|
+
atol: Absolute tolerance for the ONNX safe export in fp16
|
|
195
230
|
"""
|
|
196
231
|
if not ONNX_AVAILABLE:
|
|
197
232
|
log.warning("ONNX is not installed, can not export model in this format.")
|
|
@@ -210,9 +245,32 @@ def export_onnx_model(
|
|
|
210
245
|
else:
|
|
211
246
|
batch_size = 1
|
|
212
247
|
|
|
213
|
-
model_inputs
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
model_inputs: tuple[list[Any] | tuple[Any, ...] | torch.Tensor, list[Any]] | None
|
|
249
|
+
if example_inputs is not None:
|
|
250
|
+
if isinstance(example_inputs, Sequence):
|
|
251
|
+
model_input_tensors = []
|
|
252
|
+
model_input_shapes = []
|
|
253
|
+
|
|
254
|
+
for example_input in example_inputs:
|
|
255
|
+
new_inp = example_input.to(
|
|
256
|
+
device="cuda:0" if half_precision else "cpu",
|
|
257
|
+
dtype=torch.float16 if half_precision else torch.float32,
|
|
258
|
+
)
|
|
259
|
+
new_inp = new_inp.unsqueeze(0).repeat(batch_size, *(1 for x in new_inp.shape))
|
|
260
|
+
model_input_tensors.append(new_inp)
|
|
261
|
+
model_input_shapes.append(new_inp[0].shape)
|
|
262
|
+
|
|
263
|
+
model_inputs = (model_input_tensors, [model_input_shapes])
|
|
264
|
+
else:
|
|
265
|
+
new_inp = example_inputs.to(
|
|
266
|
+
device="cuda:0" if half_precision else "cpu",
|
|
267
|
+
dtype=torch.float16 if half_precision else torch.float32,
|
|
268
|
+
)
|
|
269
|
+
new_inp = new_inp.unsqueeze(0).repeat(batch_size, *(1 for x in new_inp.shape))
|
|
270
|
+
model_inputs = ([new_inp], [new_inp[0].shape])
|
|
271
|
+
else:
|
|
272
|
+
model_inputs = extract_torch_model_inputs(model, input_shapes, half_precision)
|
|
273
|
+
|
|
216
274
|
if model_inputs is None:
|
|
217
275
|
return None
|
|
218
276
|
|
|
@@ -266,6 +324,8 @@ def export_onnx_model(
|
|
|
266
324
|
|
|
267
325
|
if isinstance(inp, list):
|
|
268
326
|
inp = tuple(inp) # onnx doesn't like lists representing tuples of inputs
|
|
327
|
+
elif isinstance(inp, torch.Tensor):
|
|
328
|
+
inp = (inp,)
|
|
269
329
|
|
|
270
330
|
if isinstance(inp, dict):
|
|
271
331
|
raise ValueError("ONNX export does not support model with dict inputs")
|
|
@@ -290,6 +350,8 @@ def export_onnx_model(
|
|
|
290
350
|
onnx_config=onnx_config,
|
|
291
351
|
input_shapes=input_shapes,
|
|
292
352
|
input_names=input_names,
|
|
353
|
+
rtol=rtol,
|
|
354
|
+
atol=atol,
|
|
293
355
|
)
|
|
294
356
|
|
|
295
357
|
if not is_export_ok:
|
|
@@ -324,6 +386,8 @@ def _safe_export_half_precision_onnx(
|
|
|
324
386
|
onnx_config: DictConfig,
|
|
325
387
|
input_shapes: list[Any],
|
|
326
388
|
input_names: list[str],
|
|
389
|
+
rtol: float = 0.01,
|
|
390
|
+
atol: float = 5e-3,
|
|
327
391
|
) -> bool:
|
|
328
392
|
"""Check that the exported half precision ONNX model does not contain NaN values. If it does, attempt to export
|
|
329
393
|
the model with a more stable export and overwrite the original model.
|
|
@@ -335,6 +399,8 @@ def _safe_export_half_precision_onnx(
|
|
|
335
399
|
onnx_config: ONNX export configuration
|
|
336
400
|
input_shapes: Input shapes for the model
|
|
337
401
|
input_names: Input names for the model
|
|
402
|
+
rtol: Relative tolerance to evaluate the model
|
|
403
|
+
atol: Absolute tolerance to evaluate the model
|
|
338
404
|
|
|
339
405
|
Returns:
|
|
340
406
|
True if the model is stable or it was possible to export a more stable model, False otherwise.
|
|
@@ -364,16 +430,15 @@ def _safe_export_half_precision_onnx(
|
|
|
364
430
|
export_output = export_onnx_model(
|
|
365
431
|
model=model,
|
|
366
432
|
output_path=os.path.dirname(export_model_path),
|
|
367
|
-
|
|
433
|
+
# Force to not simplify fp32 model
|
|
434
|
+
onnx_config=DictConfig({**onnx_config, "simplify": False}),
|
|
368
435
|
input_shapes=input_shapes,
|
|
369
436
|
half_precision=False,
|
|
370
437
|
model_name=os.path.basename(export_model_path),
|
|
371
438
|
)
|
|
372
|
-
if export_output is
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
log.warning("Failed to export model")
|
|
376
|
-
return False
|
|
439
|
+
if export_output is None:
|
|
440
|
+
# This should not happen
|
|
441
|
+
raise RuntimeError("Failed to export model")
|
|
377
442
|
|
|
378
443
|
model_fp32 = onnx.load(export_model_path)
|
|
379
444
|
test_data = {input_names[i]: inp[i].float().cpu().numpy() for i in range(len(inp))}
|
|
@@ -381,7 +446,7 @@ def _safe_export_half_precision_onnx(
|
|
|
381
446
|
with open(os.devnull, "w") as f, contextlib.redirect_stdout(f):
|
|
382
447
|
# This function prints a lot of information that is not useful for the user
|
|
383
448
|
model_fp16 = auto_convert_mixed_precision(
|
|
384
|
-
model_fp32, test_data, rtol=
|
|
449
|
+
model_fp32, test_data, rtol=rtol, atol=atol, keep_io_types=False
|
|
385
450
|
)
|
|
386
451
|
onnx.save(model_fp16, export_model_path)
|
|
387
452
|
|
|
@@ -431,6 +496,9 @@ def export_model(
|
|
|
431
496
|
input_shapes: list[Any] | None = None,
|
|
432
497
|
idx_to_class: dict[int, str] | None = None,
|
|
433
498
|
pytorch_model_type: Literal["backbone", "model"] = "model",
|
|
499
|
+
example_inputs: list[Any] | tuple[Any, ...] | torch.Tensor | None = None,
|
|
500
|
+
rtol: float = 0.01,
|
|
501
|
+
atol: float = 5e-3,
|
|
434
502
|
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
435
503
|
"""Generate deployment models for the task.
|
|
436
504
|
|
|
@@ -443,6 +511,9 @@ def export_model(
|
|
|
443
511
|
idx_to_class: Mapping from class index to class name
|
|
444
512
|
pytorch_model_type: Type of the pytorch model config to be exported, if it's backbone on disk we will save the
|
|
445
513
|
config.backbone config, otherwise we will save the config.model
|
|
514
|
+
example_inputs: If provided use this to evaluate the model instead of generating random inputs
|
|
515
|
+
rtol: Relative tolerance for the ONNX safe export in fp16
|
|
516
|
+
atol: Absolute tolerance for the ONNX safe export in fp16
|
|
446
517
|
|
|
447
518
|
Returns:
|
|
448
519
|
If the model is exported successfully, return a dictionary containing information about the exported model and
|
|
@@ -468,6 +539,7 @@ def export_model(
|
|
|
468
539
|
input_shapes=input_shapes,
|
|
469
540
|
output_path=export_folder,
|
|
470
541
|
half_precision=half_precision,
|
|
542
|
+
example_inputs=example_inputs,
|
|
471
543
|
)
|
|
472
544
|
|
|
473
545
|
if out is None:
|
|
@@ -495,6 +567,9 @@ def export_model(
|
|
|
495
567
|
onnx_config=config.export.onnx,
|
|
496
568
|
input_shapes=input_shapes,
|
|
497
569
|
half_precision=half_precision,
|
|
570
|
+
example_inputs=example_inputs,
|
|
571
|
+
rtol=rtol,
|
|
572
|
+
atol=atol,
|
|
498
573
|
)
|
|
499
574
|
|
|
500
575
|
if out is None:
|
quadra/utils/utils.py
CHANGED
|
@@ -8,10 +8,12 @@ import glob
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import shutil
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
import warnings
|
|
14
15
|
from collections.abc import Iterable, Iterator, Sequence
|
|
16
|
+
from tempfile import TemporaryDirectory
|
|
15
17
|
from typing import Any, cast
|
|
16
18
|
|
|
17
19
|
import cv2
|
|
@@ -299,45 +301,78 @@ def finish(
|
|
|
299
301
|
quadra_export.generate_torch_inputs(input_size, device=device, half_precision=half_precision),
|
|
300
302
|
)
|
|
301
303
|
types_to_upload = config.core.get("upload_models")
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
mlflow_zip_models = config.core.get("mlflow_zip_models", False)
|
|
305
|
+
model_uploaded = False
|
|
306
|
+
with mlflow.start_run(run_id=mlflow_logger.run_id) as _:
|
|
307
|
+
for model_path in deployed_models:
|
|
308
|
+
model_type = model_type_from_path(model_path)
|
|
309
|
+
model_name = os.path.basename(model_path)
|
|
310
|
+
|
|
311
|
+
if model_type is None:
|
|
312
|
+
logging.warning("%s model type not supported", model_path)
|
|
310
313
|
continue
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
314
|
+
if model_type is not None and model_type in types_to_upload:
|
|
315
|
+
if model_type == "pytorch" and not mlflow_zip_models:
|
|
316
|
+
logging.warning("Pytorch format still not supported for mlflow upload")
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if mlflow_zip_models:
|
|
320
|
+
with TemporaryDirectory() as temp_dir:
|
|
321
|
+
if model_type == "pytorch" and os.path.isfile(
|
|
322
|
+
os.path.join(export_folder, "model_config.yaml")
|
|
323
|
+
):
|
|
324
|
+
shutil.copy(model_path, temp_dir)
|
|
325
|
+
shutil.copy(os.path.join(export_folder, "model_config.yaml"), temp_dir)
|
|
326
|
+
shutil.make_archive("assets", "zip", root_dir=temp_dir)
|
|
327
|
+
else:
|
|
328
|
+
shutil.make_archive(
|
|
329
|
+
"assets",
|
|
330
|
+
"zip",
|
|
331
|
+
root_dir=os.path.dirname(model_path),
|
|
332
|
+
base_dir=model_name,
|
|
333
|
+
)
|
|
334
|
+
shutil.move("assets.zip", temp_dir)
|
|
335
|
+
mlflow.pyfunc.log_model(
|
|
336
|
+
artifact_path=model_path,
|
|
337
|
+
loader_module="not.used",
|
|
338
|
+
data_path=os.path.join(temp_dir, "assets.zip"),
|
|
339
|
+
pip_requirements=[""],
|
|
333
340
|
)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
model_uploaded = True
|
|
342
|
+
else:
|
|
343
|
+
model = quadra_export.import_deployment_model(
|
|
344
|
+
model_path,
|
|
345
|
+
device=device,
|
|
346
|
+
inference_config=config.inference,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if model_type in ["torchscript", "pytorch"]:
|
|
350
|
+
signature = infer_signature_model(model.model, inputs)
|
|
351
|
+
mlflow.pytorch.log_model(
|
|
352
|
+
model.model,
|
|
338
353
|
artifact_path=model_path,
|
|
339
354
|
signature=signature,
|
|
340
355
|
)
|
|
356
|
+
model_uploaded = True
|
|
357
|
+
|
|
358
|
+
elif model_type in ["onnx", "simplified_onnx"] and ONNX_AVAILABLE:
|
|
359
|
+
if model.model_path is None:
|
|
360
|
+
logging.warning(
|
|
361
|
+
"Cannot log onnx model on mlflow, \
|
|
362
|
+
BaseEvaluationModel 'model_path' attribute is None"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
signature = infer_signature_model(model, inputs)
|
|
366
|
+
model_proto = onnx.load(model.model_path)
|
|
367
|
+
mlflow.onnx.log_model(
|
|
368
|
+
model_proto,
|
|
369
|
+
artifact_path=model_path,
|
|
370
|
+
signature=signature,
|
|
371
|
+
)
|
|
372
|
+
model_uploaded = True
|
|
373
|
+
|
|
374
|
+
if model_uploaded:
|
|
375
|
+
mlflow.log_artifact(os.path.join(export_folder, "model.json"), export_folder)
|
|
341
376
|
|
|
342
377
|
if tensorboard_logger is not None:
|
|
343
378
|
config_paths = []
|
|
@@ -376,7 +411,7 @@ def model_type_from_path(model_path: str) -> str | None:
|
|
|
376
411
|
- "pytorch" if the model has a '.pth' extension (PyTorch).
|
|
377
412
|
- "simplified_onnx" if the model file ends with 'simplified.onnx' (Simplified ONNX).
|
|
378
413
|
- "onnx" if the model has a '.onnx' extension (ONNX).
|
|
379
|
-
- "json"
|
|
414
|
+
- "json" if the model has a '.json' extension (JSON).
|
|
380
415
|
- None if model extension is not supported.
|
|
381
416
|
|
|
382
417
|
Example:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quadra
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.1
|
|
4
4
|
Summary: Deep Learning experiment orchestration library
|
|
5
5
|
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: deep learning,experiment,lightning,hydra-core
|
|
7
8
|
Author: Federico Belotti
|
|
8
9
|
Author-email: federico.belotti@orobix.com
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
quadra/__init__.py,sha256=
|
|
1
|
+
quadra/__init__.py,sha256=rhDlLiGGvh81Rp-GlFi4H-u_G2RaS1e7DbhWlx2lqO8,112
|
|
2
2
|
quadra/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
quadra/callbacks/anomalib.py,sha256=WLBEGhZA9HoP4Yh9UbbC2GzDOKYTkvU9EY1lkZcV7Fs,11971
|
|
4
4
|
quadra/callbacks/lightning.py,sha256=qvtzDiv8ZUV7K11gKHKWCyo-a9XR_Jm_M-IEicTM1Yo,20242
|
|
@@ -36,7 +36,7 @@ quadra/configs/callbacks/all.yaml,sha256=LZx8d0apwv9t0KlKqzFCrYo2NpuNBgcUnHhG_ku
|
|
|
36
36
|
quadra/configs/callbacks/default.yaml,sha256=ZFPU1bm36hJsxI-85uiJx7TpX7qWkR8dibBKWtES4Yc,1180
|
|
37
37
|
quadra/configs/callbacks/default_anomalib.yaml,sha256=FjjSj6HgMvH18MV4AKR4Ew0pOerPefj9eUGinUkODLE,2256
|
|
38
38
|
quadra/configs/config.yaml,sha256=IULhqUF8Z7Cqr5Xx41EGj8dwtPQWSRiZ5jwzNg0Rjwk,686
|
|
39
|
-
quadra/configs/core/default.yaml,sha256
|
|
39
|
+
quadra/configs/core/default.yaml,sha256=-qXngf5iSRIgPPBNZFu00TeYmE2DC0WuG7x_YPSAG44,264
|
|
40
40
|
quadra/configs/datamodule/base/anomaly.yaml,sha256=CILLAoQHunrT4BN0ynOzizTX29k-7B9vDVIZUYm-cBU,377
|
|
41
41
|
quadra/configs/datamodule/base/classification.yaml,sha256=NYtGk4lmi9Os6bP01AU9xJw3cONRPcGXisdrnoOYpjE,546
|
|
42
42
|
quadra/configs/datamodule/base/multilabel_classification.yaml,sha256=zp7AQx7V6cLJn7zRMKA04zzWKgVq5jTDJMGltqd9OP8,613
|
|
@@ -248,9 +248,9 @@ quadra/schedulers/__init__.py,sha256=mQivr18c0j36hpV3Lm8nlyBVKFevWp8TtLuTfvI9kQc
|
|
|
248
248
|
quadra/schedulers/base.py,sha256=T1EdrLOJ0i9MzWoLCkrNA0uypm7hJ-L6NFhjIXFB6NE,1462
|
|
249
249
|
quadra/schedulers/warmup.py,sha256=chzzrK7OqqlicBCxiF4CqMYNrWu6nflIbRE-C86Jrw0,4962
|
|
250
250
|
quadra/tasks/__init__.py,sha256=tmAfMoH0k3UC7r2pNrgbBa1Pfc3tpLl3IObFF6Z0eRE,820
|
|
251
|
-
quadra/tasks/anomaly.py,sha256=
|
|
251
|
+
quadra/tasks/anomaly.py,sha256=40ciWnkle45Hl6SjY_4OHWdT2Mb1Qfoog60alZG-uQ0,25899
|
|
252
252
|
quadra/tasks/base.py,sha256=piYlTFtvqH-4s4oEq4GczdAs_gL29UHAJGsOC5Sd3Bc,14187
|
|
253
|
-
quadra/tasks/classification.py,sha256=
|
|
253
|
+
quadra/tasks/classification.py,sha256=_GQOPMGuOZ_uLA9jFhLEaJkW_Sid_WKHNn9ALXnGNmo,53407
|
|
254
254
|
quadra/tasks/patch.py,sha256=nzo8o-ei7iF1Iarvd8-c08s0Rs_lPvVPDLAbkFMx-Qw,20251
|
|
255
255
|
quadra/tasks/segmentation.py,sha256=9Qy-V0Wvoofl7IrfotnSMgBIXcZd-WfZZtetyqmB0FY,16260
|
|
256
256
|
quadra/tasks/ssl.py,sha256=XsaC9hbhvTA5UfHeRaaCstx9mTYacLRmgoCF5Tj9R5M,20547
|
|
@@ -258,11 +258,11 @@ quadra/trainers/README.md,sha256=XtpbUOxwvPpOUL7E5s2JHjRgwT-CRKTxsBeUSXrg9BU,248
|
|
|
258
258
|
quadra/trainers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
259
259
|
quadra/trainers/classification.py,sha256=YeJ0z7Vk0-dsMTcoKBxSdSA0rxtilEcQTp-Zq9Xi1hw,7042
|
|
260
260
|
quadra/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
261
|
-
quadra/utils/anomaly.py,sha256=
|
|
261
|
+
quadra/utils/anomaly.py,sha256=uyzrFTz5QFTyIEbOT81A6OxXbHceGeiGP-JP3qr65D8,6044
|
|
262
262
|
quadra/utils/classification.py,sha256=dKFuv4RywWhvhstOnEOnaf-6qcViUK0dTgah9m9mw2Q,24917
|
|
263
263
|
quadra/utils/deprecation.py,sha256=zF_S-yqenaZxRBOudhXts0mX763WjEUWCnHd09TZnwY,852
|
|
264
264
|
quadra/utils/evaluation.py,sha256=oooRJPu1AaHhOwvB1Y6SFjQ645OkgrDzKtUvwWq8oq4,19005
|
|
265
|
-
quadra/utils/export.py,sha256=
|
|
265
|
+
quadra/utils/export.py,sha256=fUdcZ2_VKBjuM1yK9nuIEUQ6tNE21SACgBfWhyC5rjw,24651
|
|
266
266
|
quadra/utils/imaging.py,sha256=Cz7sGb_axEmnGcwQJP2djFZpIpGCPFIBGT8NWVV-OOE,866
|
|
267
267
|
quadra/utils/logger.py,sha256=tQJ4xpTAFKx1g-UUm5K1x7zgoP6qoXpcUHQyu0rOr1w,556
|
|
268
268
|
quadra/utils/mlflow.py,sha256=DVso1lxn126hil8i4tTf5WFUPJ8uJNAzNU8OXbXwOzw,3586
|
|
@@ -288,13 +288,13 @@ quadra/utils/tests/fixtures/models/classification.py,sha256=5qpyOonqK6W2LCUWEHhm
|
|
|
288
288
|
quadra/utils/tests/fixtures/models/segmentation.py,sha256=CTNXeEPcFxFq-YcNfQi5DbbytPZwBQaZn5dQq3L41j0,765
|
|
289
289
|
quadra/utils/tests/helpers.py,sha256=9PJlwozUl_lpQW-Ck-tN7sGFcgeieEd3q56aYuwMIlk,2381
|
|
290
290
|
quadra/utils/tests/models.py,sha256=KbAlv_ukxaUYsyVNUO_dM0NyIosx8RpC0EVyF1HvPkM,507
|
|
291
|
-
quadra/utils/utils.py,sha256=
|
|
291
|
+
quadra/utils/utils.py,sha256=_-iD8MG4g_qrzzMcBrgPWSouQU96jI-5kgYSNYjs4d0,21293
|
|
292
292
|
quadra/utils/validator.py,sha256=wmVXycB90VNyAbKBUVncFCxK4nsYiOWJIY3ISXwxYCY,4632
|
|
293
293
|
quadra/utils/visualization.py,sha256=yYm7lPziUOlybxigZ2qTycNewb67Q80H4hjQGWUh788,16094
|
|
294
294
|
quadra/utils/vit_explainability.py,sha256=Gh6BHaDEzWxOjJp1aqvCxLt9Rb8TXd5uKXOAx7-acUk,13351
|
|
295
295
|
hydra_plugins/quadra_searchpath_plugin.py,sha256=AAn4TzR87zUK7nwSsK-KoqALiPtfQ8FvX3fgZPTGIJ0,1189
|
|
296
|
-
quadra-2.
|
|
297
|
-
quadra-2.
|
|
298
|
-
quadra-2.
|
|
299
|
-
quadra-2.
|
|
300
|
-
quadra-2.
|
|
296
|
+
quadra-2.5.1.dist-info/METADATA,sha256=71YK2Y1FxXu2NJxWaUB9zWJn8v2PHLu0zi38lJtVgfU,17632
|
|
297
|
+
quadra-2.5.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
298
|
+
quadra-2.5.1.dist-info/entry_points.txt,sha256=sRYonBZyx-sAJeWcQNQoVQIU5lm02cnCQt6b15k0WHU,43
|
|
299
|
+
quadra-2.5.1.dist-info/licenses/LICENSE,sha256=8cTbQtcWa02YJoSpMeV_gxj3jpMTkxvl-w3WJ5gV_QE,11342
|
|
300
|
+
quadra-2.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|