spotoptim 3.1.0__tar.gz → 3.2.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.
Files changed (91) hide show
  1. {spotoptim-3.1.0 → spotoptim-3.2.0}/PKG-INFO +2 -1
  2. {spotoptim-3.1.0 → spotoptim-3.2.0}/pyproject.toml +2 -1
  3. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/data/__init__.py +7 -0
  4. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/data/manydataset.py +141 -2
  5. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/nn/__init__.py +4 -0
  6. spotoptim-3.2.0/src/spotoptim/nn/training.py +234 -0
  7. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/__init__.py +3 -0
  8. spotoptim-3.2.0/src/spotoptim/utils/seed.py +60 -0
  9. {spotoptim-3.1.0 → spotoptim-3.2.0}/README.md +0 -0
  10. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/SpotOptim.py +0 -0
  11. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/__init__.py +0 -0
  12. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/core/__init__.py +0 -0
  13. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/core/data.py +0 -0
  14. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/core/experiment.py +0 -0
  15. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/core/protocol.py +0 -0
  16. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/core/storage.py +0 -0
  17. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/data/base.py +0 -0
  18. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/data/diabetes.py +0 -0
  19. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/datasets/__init__.py +0 -0
  20. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/datasets/py.typed +0 -0
  21. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/datasets/test01.csv +0 -0
  22. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/datasets/test02.csv +0 -0
  23. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/datasets/test11.csv +0 -0
  24. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/eda/__init__.py +0 -0
  25. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/eda/plots.py +0 -0
  26. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/factor_analyzer/__init__.py +0 -0
  27. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
  28. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
  29. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
  30. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
  31. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/__init__.py +0 -0
  32. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/cd_data.csv +0 -0
  33. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/forr08a.py +0 -0
  34. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/mo.py +0 -0
  35. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/remote.py +0 -0
  36. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/sequence_cv_objective.py +0 -0
  37. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/so.py +0 -0
  38. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/function/torch_objective.py +0 -0
  39. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/hyperparameters/__init__.py +0 -0
  40. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/hyperparameters/parameters.py +0 -0
  41. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
  42. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/inspection/__init__.py +0 -0
  43. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/inspection/importance.py +0 -0
  44. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/inspection/predictions.py +0 -0
  45. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/mo/__init__.py +0 -0
  46. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/mo/mo_mm.py +0 -0
  47. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/mo/pareto.py +0 -0
  48. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/nn/linear_regressor.py +0 -0
  49. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/nn/many_to_many_rnn.py +0 -0
  50. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/nn/mlp.py +0 -0
  51. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/nn/optimizer.py +0 -0
  52. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/optimizer/__init__.py +0 -0
  53. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/optimizer/acquisition.py +0 -0
  54. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/optimizer/schedule_free.py +0 -0
  55. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/optimizer/wrapper.py +0 -0
  56. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/plot/__init__.py +0 -0
  57. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/plot/contour.py +0 -0
  58. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/plot/mo.py +0 -0
  59. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/plot/visualization.py +0 -0
  60. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/py.typed +0 -0
  61. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/reporting/__init__.py +0 -0
  62. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/reporting/analysis.py +0 -0
  63. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/reporting/results.py +0 -0
  64. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/sampling/__init__.py +0 -0
  65. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/sampling/design.py +0 -0
  66. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/sampling/effects.py +0 -0
  67. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/sampling/lhs.py +0 -0
  68. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/sampling/mm.py +0 -0
  69. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/__init__.py +0 -0
  70. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/kernels.py +0 -0
  71. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/kriging.py +0 -0
  72. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
  73. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/nystroem.py +0 -0
  74. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/pipeline.py +0 -0
  75. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/surrogate/simple_kriging.py +0 -0
  76. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/tricands/__init__.py +0 -0
  77. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/tricands/tricands.py +0 -0
  78. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/boundaries.py +0 -0
  79. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/convert.py +0 -0
  80. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/dimreduction.py +0 -0
  81. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/eval.py +0 -0
  82. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/file.py +0 -0
  83. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/mapping.py +0 -0
  84. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/ocba.py +0 -0
  85. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/pca.py +0 -0
  86. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/scaler.py +0 -0
  87. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/serialization.py +0 -0
  88. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/stats.py +0 -0
  89. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/tensorboard.py +0 -0
  90. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/transform.py +0 -0
  91. {spotoptim-3.1.0 → spotoptim-3.2.0}/src/spotoptim/utils/variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spotoptim
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: Sequential Parameter Optimization Toolbox
5
5
  Author: bartzbeielstein
6
6
  Author-email: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com>
@@ -36,6 +36,7 @@ Requires-Dist: requests>=2.32.3 ; extra == 'remote'
36
36
  Requires-Dist: statsmodels>=0.14.6 ; extra == 'stats'
37
37
  Requires-Dist: torch>=2.9.1 ; extra == 'torch'
38
38
  Requires-Dist: tensorboard>=2.20.0 ; extra == 'torch'
39
+ Requires-Dist: torchmetrics>=1.8.2 ; extra == 'torch'
39
40
  Requires-Dist: matplotlib>=3.10.7 ; extra == 'viz'
40
41
  Requires-Dist: seaborn>=0.13.2 ; extra == 'viz'
41
42
  Requires-Python: >=3.13
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spotoptim"
3
- version = "3.1.0"
3
+ version = "3.2.0"
4
4
  description = "Sequential Parameter Optimization Toolbox"
5
5
  readme = "README.md"
6
6
  license = { text = "AGPL-3.0-or-later" }
@@ -44,6 +44,7 @@ dev = [
44
44
  torch = [
45
45
  "torch>=2.9.1",
46
46
  "tensorboard>=2.20.0",
47
+ "torchmetrics>=1.8.2",
47
48
  ]
48
49
  viz = [
49
50
  "matplotlib>=3.10.7",
@@ -19,6 +19,8 @@ __all__ = [
19
19
  "ManyToOneDataset",
20
20
  "PadSequenceManyToMany",
21
21
  "PadSequenceManyToOne",
22
+ "load_sequence_data",
23
+ "load_pooled_sequence_data",
22
24
  ]
23
25
 
24
26
  _lazy_map = {
@@ -28,6 +30,11 @@ _lazy_map = {
28
30
  "ManyToOneDataset": ("spotoptim.data.manydataset", "ManyToOneDataset"),
29
31
  "PadSequenceManyToMany": ("spotoptim.data.manydataset", "PadSequenceManyToMany"),
30
32
  "PadSequenceManyToOne": ("spotoptim.data.manydataset", "PadSequenceManyToOne"),
33
+ "load_sequence_data": ("spotoptim.data.manydataset", "load_sequence_data"),
34
+ "load_pooled_sequence_data": (
35
+ "spotoptim.data.manydataset",
36
+ "load_pooled_sequence_data",
37
+ ),
31
38
  }
32
39
 
33
40
 
@@ -13,12 +13,12 @@ classes provided here.
13
13
  Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``).
14
14
  """
15
15
 
16
- from typing import List, Optional, Union
16
+ from typing import List, Optional, Tuple, Union
17
17
 
18
18
  import pandas as pd
19
19
  import torch
20
20
  from torch.nn.utils.rnn import pad_sequence
21
- from torch.utils.data import Dataset
21
+ from torch.utils.data import ConcatDataset, Dataset
22
22
 
23
23
 
24
24
  class ManyToManyDataset(Dataset):
@@ -234,3 +234,142 @@ class PadSequenceManyToOne:
234
234
  lengths = torch.tensor([len(x) for x in batch_x])
235
235
 
236
236
  return padded_batch_x, lengths, torch.tensor(batch_y)
237
+
238
+
239
+ def load_sequence_data(
240
+ data: pd.DataFrame,
241
+ target: str,
242
+ group_by: str,
243
+ drop: Optional[Union[str, List[str]]] = None,
244
+ input_features: Optional[List[str]] = None,
245
+ feature_scaling=None,
246
+ target_scaling=None,
247
+ dataset_type: str = "many_to_many",
248
+ ) -> Tuple[Dataset, pd.DataFrame]:
249
+ """Group a DataFrame into a variable-length sequence dataset.
250
+
251
+ Splits ``data`` by the values of ``group_by`` (e.g. one compressor speed
252
+ line per group) and wraps the groups in a `ManyToManyDataset` or
253
+ `ManyToOneDataset`. Ported from the schu25a study's ``load_data`` helper
254
+ (``src/rnn/utils.py``); the experiment-specific column defaults were
255
+ removed.
256
+
257
+ Args:
258
+ data (pd.DataFrame): Input data; one row per time step.
259
+ target (str): The target column name.
260
+ group_by (str): Column whose values define the sequences.
261
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
262
+ groups before extracting features. Defaults to None.
263
+ input_features (Optional[List[str]]): Columns scaled by
264
+ ``feature_scaling``. Only used when ``feature_scaling`` is given;
265
+ the dataset features are always all columns except ``drop`` and
266
+ ``target``. Defaults to None.
267
+ feature_scaling: Optional sklearn-style scaler; applied in place to
268
+ ``data[input_features]`` via ``fit_transform``. Defaults to None.
269
+ target_scaling: Optional sklearn-style scaler; applied in place to
270
+ ``data[target]`` via ``fit_transform``. Defaults to None.
271
+ dataset_type (str, optional): Dataset flavor. Options:
272
+ - "many_to_many": one target value per time step.
273
+ - "many_to_one": one scalar target per sequence.
274
+ Defaults to "many_to_many".
275
+
276
+ Returns:
277
+ tuple: ``(dataset, data)`` — the sequence dataset and the (possibly
278
+ scaled) DataFrame.
279
+
280
+ Raises:
281
+ ValueError: If ``dataset_type`` is not "many_to_many" or "many_to_one".
282
+
283
+ Examples:
284
+ ```{python}
285
+ import pandas as pd
286
+ from spotoptim.data.manydataset import load_sequence_data
287
+
288
+ df = pd.DataFrame({
289
+ "line": [1, 1, 1, 2, 2],
290
+ "x": [0.1, 0.2, 0.3, 0.4, 0.5],
291
+ "y": [1.0, 2.0, 3.0, 4.0, 5.0],
292
+ })
293
+ ds, df = load_sequence_data(df, target="y", group_by="line", drop="line")
294
+ print(len(ds), ds[0][0].shape)
295
+ ```
296
+ """
297
+ if feature_scaling is not None:
298
+ data[input_features] = feature_scaling.fit_transform(data[input_features])
299
+
300
+ if target_scaling is not None:
301
+ data[target] = target_scaling.fit_transform(data[target])
302
+
303
+ groups = [group for _, group in data.groupby(group_by)]
304
+
305
+ if dataset_type == "many_to_many":
306
+ return ManyToManyDataset(groups, target=target, drop=drop), data
307
+ elif dataset_type == "many_to_one":
308
+ return ManyToOneDataset(groups, target=target, drop=drop), data
309
+ raise ValueError(
310
+ f"dataset_type {dataset_type!r} not supported. "
311
+ "Options: 'many_to_many', 'many_to_one'."
312
+ )
313
+
314
+
315
+ def load_pooled_sequence_data(
316
+ data_list: List[pd.DataFrame],
317
+ target: str,
318
+ group_by: str,
319
+ drop: Optional[Union[str, List[str]]] = None,
320
+ input_features: Optional[List[str]] = None,
321
+ feature_scaling=None,
322
+ target_scaling=None,
323
+ dataset_type: str = "many_to_many",
324
+ ) -> ConcatDataset:
325
+ """Pool several DataFrames into one concatenated sequence dataset.
326
+
327
+ Applies `load_sequence_data` to every DataFrame (e.g. one compressor map
328
+ each) and concatenates the resulting sequence datasets — the pooled
329
+ training set of the schu25a "global training" workflow (ported from
330
+ ``load_pretrain_data`` in ``src/rnn/utils.py``).
331
+
332
+ Args:
333
+ data_list (List[pd.DataFrame]): Input DataFrames; one per map.
334
+ target (str): The target column name.
335
+ group_by (str): Column whose values define the sequences.
336
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
337
+ groups before extracting features. Defaults to None.
338
+ input_features (Optional[List[str]]): Columns scaled by
339
+ ``feature_scaling``; see `load_sequence_data`. Defaults to None.
340
+ feature_scaling: Optional sklearn-style scaler, applied per DataFrame.
341
+ Defaults to None.
342
+ target_scaling: Optional sklearn-style scaler, applied per DataFrame.
343
+ Defaults to None.
344
+ dataset_type (str, optional): "many_to_many" or "many_to_one".
345
+ Defaults to "many_to_many".
346
+
347
+ Returns:
348
+ ConcatDataset: The concatenation of the per-DataFrame sequence datasets.
349
+
350
+ Examples:
351
+ ```{python}
352
+ import pandas as pd
353
+ from spotoptim.data.manydataset import load_pooled_sequence_data
354
+
355
+ df1 = pd.DataFrame({"line": [1, 1, 2], "x": [0.1, 0.2, 0.3], "y": [1.0, 2.0, 3.0]})
356
+ df2 = pd.DataFrame({"line": [1, 1], "x": [0.4, 0.5], "y": [4.0, 5.0]})
357
+ pooled = load_pooled_sequence_data([df1, df2], target="y", group_by="line", drop="line")
358
+ print(len(pooled))
359
+ ```
360
+ """
361
+ ds_ls = []
362
+ for data in data_list:
363
+ ds, _ = load_sequence_data(
364
+ data,
365
+ target=target,
366
+ group_by=group_by,
367
+ drop=drop,
368
+ input_features=input_features,
369
+ feature_scaling=feature_scaling,
370
+ target_scaling=target_scaling,
371
+ dataset_type=dataset_type,
372
+ )
373
+ ds_ls.append(ds)
374
+
375
+ return ConcatDataset(ds_ls)
@@ -11,6 +11,8 @@ __all__ = [
11
11
  "ManyToManyRNNRegressor",
12
12
  "get_activation",
13
13
  "optimizer_handler",
14
+ "train_sequences",
15
+ "evaluate_sequences",
14
16
  ]
15
17
 
16
18
  _lazy_map = {
@@ -23,6 +25,8 @@ _lazy_map = {
23
25
  ),
24
26
  "get_activation": ("spotoptim.nn.many_to_many_rnn", "get_activation"),
25
27
  "optimizer_handler": ("spotoptim.nn.optimizer", "optimizer_handler"),
28
+ "train_sequences": ("spotoptim.nn.training", "train_sequences"),
29
+ "evaluate_sequences": ("spotoptim.nn.training", "evaluate_sequences"),
26
30
  }
27
31
 
28
32
 
@@ -0,0 +1,234 @@
1
+ # SPDX-FileCopyrightText: 2026 bartzbeielstein
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ """Plain training and evaluation loops for variable-length sequence models.
6
+
7
+ Ported op-for-op from the ``train``/``evaluate_many`` helpers of the schu25a
8
+ compressor-map study (``src/rnn/train.py``), so that runs seeded with
9
+ `spotoptim.utils.seed.seed_everything` reproduce the original results
10
+ bit-identically on the same device. The models are called as
11
+ ``model(x, lengths)`` on padded batches (e.g.
12
+ `spotoptim.nn.many_to_many_rnn.ManyToManyRNN` with batches collated by
13
+ `spotoptim.data.manydataset.PadSequenceManyToMany`); losses and metrics are
14
+ computed on the squeezed padded tensors, exactly as in the original. The
15
+ original's unused ``seed`` parameters were dropped.
16
+
17
+ Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``),
18
+ which includes ``torchmetrics``.
19
+ """
20
+
21
+ from typing import List, Literal, Optional, Tuple, Union, overload
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.nn as nn
26
+ from torch.utils.data import DataLoader
27
+ from torchmetrics.regression import MeanAbsolutePercentageError, MeanSquaredError
28
+
29
+
30
+ def train_sequences(
31
+ model: nn.Module,
32
+ train_loader: DataLoader,
33
+ optimizer: torch.optim.Optimizer,
34
+ criterion: nn.Module,
35
+ val_loader: Optional[DataLoader] = None,
36
+ epochs: int = 10,
37
+ device: str = "cpu",
38
+ verbose: bool = True,
39
+ ) -> Union[Tuple[nn.Module, List[float]], Tuple[nn.Module, List[float], List[float]]]:
40
+ """Train a sequence model with a plain epoch loop.
41
+
42
+ One optimizer step per batch; the loss is computed between
43
+ ``model(x, lengths).squeeze()`` and the (padded) target batch. The
44
+ recorded training loss is the batch-mean loss per epoch.
45
+
46
+ Args:
47
+ model (nn.Module): Model called as ``model(x, lengths)``.
48
+ train_loader (DataLoader): Loader yielding ``(x, lengths, y)`` batches,
49
+ e.g. collated by `PadSequenceManyToMany`.
50
+ optimizer (torch.optim.Optimizer): Optimizer over ``model.parameters()``.
51
+ criterion (nn.Module): Loss module, e.g. ``nn.MSELoss()``.
52
+ val_loader (Optional[DataLoader]): Optional validation loader; when
53
+ given, the mean per-batch RMSE from `evaluate_sequences` is
54
+ recorded after every epoch. Defaults to None.
55
+ epochs (int, optional): Number of epochs. Defaults to 10.
56
+ device (str, optional): Training device. Defaults to "cpu".
57
+ verbose (bool, optional): Print per-epoch losses. Defaults to True.
58
+
59
+ Returns:
60
+ tuple: ``(model, train_loss_ls)`` without a validation loader, or
61
+ ``(model, train_loss_ls, val_loss_ls)`` with one.
62
+
63
+ Examples:
64
+ ```{python}
65
+ import pandas as pd
66
+ import torch
67
+ import torch.nn as nn
68
+ from torch.utils.data import DataLoader
69
+ from spotoptim.data.manydataset import ManyToManyDataset, PadSequenceManyToMany
70
+ from spotoptim.nn.many_to_many_rnn import ManyToManyRNN
71
+ from spotoptim.nn.training import train_sequences
72
+ from spotoptim.utils.seed import seed_everything
73
+
74
+ seed_everything(42)
75
+ frames = [
76
+ pd.DataFrame({"x": [0.1, 0.2, 0.3], "y": [1.0, 2.0, 3.0]}),
77
+ pd.DataFrame({"x": [0.4, 0.5], "y": [4.0, 5.0]}),
78
+ ]
79
+ ds = ManyToManyDataset(frames, target="y")
80
+ dl = DataLoader(ds, batch_size=2, shuffle=False, collate_fn=PadSequenceManyToMany())
81
+ model = ManyToManyRNN(input_size=1, rnn_units=8, fc_units=8)
82
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
83
+ model, losses = train_sequences(
84
+ model, dl, optimizer, nn.MSELoss(), epochs=2, verbose=False
85
+ )
86
+ print(len(losses))
87
+ ```
88
+ """
89
+ model.train()
90
+ model.to(device)
91
+
92
+ train_loss_ls = []
93
+ val_loss_ls = []
94
+
95
+ for epoch in range(epochs):
96
+ model.train()
97
+ train_loss = 0.0
98
+ for batch_x, lengths, batch_y in train_loader:
99
+ batch_x = batch_x.to(device)
100
+ batch_y = batch_y.to(device)
101
+
102
+ optimizer.zero_grad()
103
+ outputs = model(batch_x, lengths)
104
+ outputs = outputs.squeeze()
105
+
106
+ loss = criterion(outputs, batch_y)
107
+ loss.backward()
108
+
109
+ optimizer.step()
110
+ train_loss += loss.item()
111
+
112
+ train_loss /= len(train_loader)
113
+ train_loss_ls.append(train_loss)
114
+
115
+ if val_loader is not None:
116
+ _, _, _, mape, rmse = evaluate_sequences(model, val_loader, device=device)
117
+ val_loss_ls.append(float(np.mean(np.array(rmse))))
118
+ else:
119
+ mape = None
120
+ rmse = None
121
+
122
+ if verbose:
123
+ print(f"Epoch: {epoch + 1}/{epochs}, Train Loss: {train_loss:.4f}", end="")
124
+ if mape is not None:
125
+ print(
126
+ f", Validation: MAPE: {np.mean(np.array(mape)):.4f}, "
127
+ f"RMSE: {np.mean(np.array(rmse)):.4f}"
128
+ )
129
+ else:
130
+ print()
131
+
132
+ if val_loader is not None:
133
+ return model, train_loss_ls, val_loss_ls
134
+ return model, train_loss_ls
135
+
136
+
137
+ @overload
138
+ def evaluate_sequences(
139
+ model: nn.Module,
140
+ val_loader: DataLoader,
141
+ device: str = "cpu",
142
+ metrics_only: Literal[False] = False,
143
+ ) -> Tuple[list, list, list, List[float], List[float]]: ...
144
+
145
+
146
+ @overload
147
+ def evaluate_sequences(
148
+ model: nn.Module,
149
+ val_loader: DataLoader,
150
+ device: str = "cpu",
151
+ *,
152
+ metrics_only: Literal[True],
153
+ ) -> Tuple[float, float]: ...
154
+
155
+
156
+ def evaluate_sequences(
157
+ model: nn.Module,
158
+ val_loader: DataLoader,
159
+ device: str = "cpu",
160
+ metrics_only: bool = False,
161
+ ) -> Union[Tuple[float, float], Tuple[list, list, list, List[float], List[float]]]:
162
+ """Evaluate a sequence model with per-batch MAPE and RMSE.
163
+
164
+ Metrics are the torchmetrics ``MeanAbsolutePercentageError`` and
165
+ ``MeanSquaredError(squared=False)`` between ``model(x, lengths).squeeze()``
166
+ and the squeezed target batch, as in the schu25a original. Inputs,
167
+ predictions, and targets are additionally returned as nested Python lists
168
+ for plotting.
169
+
170
+ Args:
171
+ model (nn.Module): Model called as ``model(x, lengths)``.
172
+ val_loader (DataLoader): Loader yielding ``(x, lengths, y)`` batches.
173
+ Batches are converted to NumPy, so ``device`` should be "cpu"
174
+ unless ``metrics_only=True``.
175
+ device (str, optional): Evaluation device. Defaults to "cpu".
176
+ metrics_only (bool, optional): Return only ``(mean_mape, mean_rmse)``.
177
+ Defaults to False.
178
+
179
+ Returns:
180
+ tuple: ``(x_ls, y_hat_ls, y_ls, mape_loss_ls, rmse_loss_ls)`` with one
181
+ entry per batch, or ``(mean_mape, mean_rmse)`` if ``metrics_only``.
182
+
183
+ Examples:
184
+ ```{python}
185
+ import pandas as pd
186
+ import torch
187
+ from torch.utils.data import DataLoader
188
+ from spotoptim.data.manydataset import ManyToManyDataset, PadSequenceManyToMany
189
+ from spotoptim.nn.many_to_many_rnn import ManyToManyRNN
190
+ from spotoptim.nn.training import evaluate_sequences
191
+ from spotoptim.utils.seed import seed_everything
192
+
193
+ seed_everything(42)
194
+ frames = [pd.DataFrame({"x": [0.1, 0.2, 0.3], "y": [1.0, 2.0, 3.0]})]
195
+ ds = ManyToManyDataset(frames, target="y")
196
+ dl = DataLoader(ds, batch_size=1, shuffle=False, collate_fn=PadSequenceManyToMany())
197
+ model = ManyToManyRNN(input_size=1, rnn_units=8, fc_units=8)
198
+ x, y_hat, y, mape, rmse = evaluate_sequences(model, dl)
199
+ print(len(y_hat), len(mape), len(rmse))
200
+ ```
201
+ """
202
+ mean_abs_percentage_error = MeanAbsolutePercentageError().to(device)
203
+ rmse_loss = MeanSquaredError(squared=False).to(device)
204
+
205
+ model.eval()
206
+ model.to(device)
207
+
208
+ x_ls = []
209
+ y_hat_ls = []
210
+ y_ls = []
211
+ mape_loss_ls = []
212
+ rmse_loss_ls = []
213
+
214
+ with torch.no_grad():
215
+ for batch_x, lengths, batch_y in val_loader:
216
+ batch_x = batch_x.to(device)
217
+ batch_y = batch_y.to(device)
218
+
219
+ y_hat = model(batch_x, lengths)
220
+ y_hat = y_hat.squeeze()
221
+
222
+ mape = mean_abs_percentage_error(y_hat, batch_y.squeeze())
223
+ rmse = rmse_loss(y_hat, batch_y.squeeze())
224
+
225
+ mape_loss_ls.append(mape.item())
226
+ rmse_loss_ls.append(rmse.item())
227
+
228
+ x_ls.append(batch_x.squeeze().detach().numpy().tolist())
229
+ y_hat_ls.append(y_hat.squeeze().detach().numpy().tolist())
230
+ y_ls.append(batch_y.squeeze().detach().numpy().tolist())
231
+
232
+ if metrics_only:
233
+ return float(np.mean(mape_loss_ls)), float(np.mean(rmse_loss_ls))
234
+ return x_ls, y_hat_ls, y_ls, mape_loss_ls, rmse_loss_ls
@@ -27,6 +27,7 @@ __all__ = [
27
27
  "get_loading_scores",
28
28
  "plot_loading_scores",
29
29
  "TorchStandardScaler",
30
+ "seed_everything",
30
31
  ]
31
32
 
32
33
  _lazy_map = {
@@ -44,6 +45,8 @@ _lazy_map = {
44
45
  "plot_loading_scores": ("spotoptim.utils.pca", "plot_loading_scores"),
45
46
  # scaler (pulls torch)
46
47
  "TorchStandardScaler": ("spotoptim.utils.scaler", "TorchStandardScaler"),
48
+ # seed (pulls torch)
49
+ "seed_everything": ("spotoptim.utils.seed", "seed_everything"),
47
50
  }
48
51
 
49
52
 
@@ -0,0 +1,60 @@
1
+ # SPDX-FileCopyrightText: 2026 bartzbeielstein
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ """Global seeding for fully reproducible torch experiments.
6
+
7
+ Ported from the ``seed_everything`` helper of the schu25a compressor-map
8
+ study (``src/rnn/utils.py``): seeds Python, NumPy, and torch (CPU, CUDA, and
9
+ MPS) and switches cuDNN to deterministic mode, so that repeated runs on the
10
+ same device produce bit-identical results.
11
+
12
+ Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``).
13
+ """
14
+
15
+ import os
16
+ import random
17
+
18
+ import numpy as np
19
+ import torch
20
+
21
+
22
+ def seed_everything(seed: int) -> None:
23
+ """Seed every random number generator relevant to a torch experiment.
24
+
25
+ Seeds Python's ``random``, ``PYTHONHASHSEED``, NumPy, and torch (CPU and
26
+ CUDA unconditionally, MPS when available), and sets
27
+ ``torch.backends.cudnn.deterministic = True`` and
28
+ ``torch.backends.cudnn.benchmark = False``.
29
+
30
+ Args:
31
+ seed (int): The seed value.
32
+
33
+ Note:
34
+ - cuDNN determinism and benchmark flags are process-global side
35
+ effects; they may slow down convolutional workloads.
36
+ - Unlike the schu25a original, the MPS generator is only seeded when
37
+ the MPS backend is available, so the function also works on
38
+ CPU-only Linux hosts.
39
+
40
+ Examples:
41
+ ```{python}
42
+ import torch
43
+ from spotoptim.utils.seed import seed_everything
44
+
45
+ seed_everything(42)
46
+ a = torch.rand(3)
47
+ seed_everything(42)
48
+ b = torch.rand(3)
49
+ print(torch.equal(a, b))
50
+ ```
51
+ """
52
+ random.seed(seed)
53
+ os.environ["PYTHONHASHSEED"] = str(seed)
54
+ np.random.seed(seed)
55
+ torch.manual_seed(seed)
56
+ torch.cuda.manual_seed(seed)
57
+ if torch.backends.mps.is_available():
58
+ torch.mps.manual_seed(seed)
59
+ torch.backends.cudnn.deterministic = True
60
+ torch.backends.cudnn.benchmark = False
File without changes