spotoptim 3.0.0__tar.gz → 3.1.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 (89) hide show
  1. {spotoptim-3.0.0 → spotoptim-3.1.0}/PKG-INFO +1 -1
  2. {spotoptim-3.0.0 → spotoptim-3.1.0}/pyproject.toml +1 -1
  3. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/__init__.py +14 -1
  4. spotoptim-3.1.0/src/spotoptim/data/manydataset.py +236 -0
  5. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/__init__.py +5 -0
  6. spotoptim-3.1.0/src/spotoptim/function/sequence_cv_objective.py +278 -0
  7. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/__init__.py +15 -1
  8. spotoptim-3.1.0/src/spotoptim/nn/many_to_many_rnn.py +234 -0
  9. spotoptim-3.1.0/src/spotoptim/nn/optimizer.py +202 -0
  10. {spotoptim-3.0.0 → spotoptim-3.1.0}/README.md +0 -0
  11. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/SpotOptim.py +0 -0
  12. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/__init__.py +0 -0
  13. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/__init__.py +0 -0
  14. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/data.py +0 -0
  15. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/experiment.py +0 -0
  16. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/protocol.py +0 -0
  17. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/storage.py +0 -0
  18. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/base.py +0 -0
  19. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/diabetes.py +0 -0
  20. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/__init__.py +0 -0
  21. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/py.typed +0 -0
  22. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test01.csv +0 -0
  23. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test02.csv +0 -0
  24. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test11.csv +0 -0
  25. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/eda/__init__.py +0 -0
  26. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/eda/plots.py +0 -0
  27. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/__init__.py +0 -0
  28. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
  29. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
  30. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
  31. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
  32. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/cd_data.csv +0 -0
  33. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/forr08a.py +0 -0
  34. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/mo.py +0 -0
  35. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/remote.py +0 -0
  36. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/so.py +0 -0
  37. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/torch_objective.py +0 -0
  38. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/__init__.py +0 -0
  39. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/parameters.py +0 -0
  40. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
  41. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/__init__.py +0 -0
  42. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/importance.py +0 -0
  43. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/predictions.py +0 -0
  44. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/__init__.py +0 -0
  45. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/mo_mm.py +0 -0
  46. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/pareto.py +0 -0
  47. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/linear_regressor.py +0 -0
  48. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/mlp.py +0 -0
  49. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/__init__.py +0 -0
  50. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/acquisition.py +0 -0
  51. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/schedule_free.py +0 -0
  52. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/wrapper.py +0 -0
  53. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/__init__.py +0 -0
  54. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/contour.py +0 -0
  55. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/mo.py +0 -0
  56. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/visualization.py +0 -0
  57. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/py.typed +0 -0
  58. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/__init__.py +0 -0
  59. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/analysis.py +0 -0
  60. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/results.py +0 -0
  61. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/__init__.py +0 -0
  62. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/design.py +0 -0
  63. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/effects.py +0 -0
  64. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/lhs.py +0 -0
  65. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/mm.py +0 -0
  66. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/__init__.py +0 -0
  67. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/kernels.py +0 -0
  68. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/kriging.py +0 -0
  69. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
  70. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/nystroem.py +0 -0
  71. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/pipeline.py +0 -0
  72. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/simple_kriging.py +0 -0
  73. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/tricands/__init__.py +0 -0
  74. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/tricands/tricands.py +0 -0
  75. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/__init__.py +0 -0
  76. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/boundaries.py +0 -0
  77. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/convert.py +0 -0
  78. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/dimreduction.py +0 -0
  79. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/eval.py +0 -0
  80. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/file.py +0 -0
  81. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/mapping.py +0 -0
  82. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/ocba.py +0 -0
  83. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/pca.py +0 -0
  84. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/scaler.py +0 -0
  85. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/serialization.py +0 -0
  86. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/stats.py +0 -0
  87. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/tensorboard.py +0 -0
  88. {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/transform.py +0 -0
  89. {spotoptim-3.0.0 → spotoptim-3.1.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.0.0
3
+ Version: 3.1.0
4
4
  Summary: Sequential Parameter Optimization Toolbox
5
5
  Author: bartzbeielstein
6
6
  Author-email: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spotoptim"
3
- version = "3.0.0"
3
+ version = "3.1.0"
4
4
  description = "Sequential Parameter Optimization Toolbox"
5
5
  readme = "README.md"
6
6
  license = { text = "AGPL-3.0-or-later" }
@@ -10,11 +10,24 @@ machine learning tasks.
10
10
 
11
11
  from .base import Config, FileConfig
12
12
 
13
- __all__ = ["DiabetesDataset", "get_diabetes_dataloaders", "Config", "FileConfig"]
13
+ __all__ = [
14
+ "DiabetesDataset",
15
+ "get_diabetes_dataloaders",
16
+ "Config",
17
+ "FileConfig",
18
+ "ManyToManyDataset",
19
+ "ManyToOneDataset",
20
+ "PadSequenceManyToMany",
21
+ "PadSequenceManyToOne",
22
+ ]
14
23
 
15
24
  _lazy_map = {
16
25
  "DiabetesDataset": ("spotoptim.data.diabetes", "DiabetesDataset"),
17
26
  "get_diabetes_dataloaders": ("spotoptim.data.diabetes", "get_diabetes_dataloaders"),
27
+ "ManyToManyDataset": ("spotoptim.data.manydataset", "ManyToManyDataset"),
28
+ "ManyToOneDataset": ("spotoptim.data.manydataset", "ManyToOneDataset"),
29
+ "PadSequenceManyToMany": ("spotoptim.data.manydataset", "PadSequenceManyToMany"),
30
+ "PadSequenceManyToOne": ("spotoptim.data.manydataset", "PadSequenceManyToOne"),
18
31
  }
19
32
 
20
33
 
@@ -0,0 +1,236 @@
1
+ # SPDX-FileCopyrightText: 2026 bartzbeielstein
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ """Variable-length sequence datasets and padding collates for recurrent models.
6
+
7
+ Ported from spotPython (``spotpython.data.manydataset`` and the padding
8
+ collates from ``spotpython.data.lightdatamodule``) without any Lightning
9
+ dependency. Each dataset item is one complete sequence (e.g. one operating
10
+ curve of a compressor map), so batches must be padded with the collate
11
+ classes provided here.
12
+
13
+ Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``).
14
+ """
15
+
16
+ from typing import List, Optional, Union
17
+
18
+ import pandas as pd
19
+ import torch
20
+ from torch.nn.utils.rnn import pad_sequence
21
+ from torch.utils.data import Dataset
22
+
23
+
24
+ class ManyToManyDataset(Dataset):
25
+ """Sequence dataset with one target value per time step.
26
+
27
+ Each element of ``df_list`` is one variable-length sequence; item ``i``
28
+ is the pair ``(features_i, targets_i)`` with shapes ``(T_i, n_features)``
29
+ and ``(T_i,)``.
30
+
31
+ Args:
32
+ df_list (List[pd.DataFrame]): List of pandas DataFrames, one per sequence.
33
+ target (str): The target column name.
34
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
35
+ DataFrames before extracting features. If a listed column is
36
+ missing, no column is dropped. Defaults to None.
37
+ dtype (torch.dtype): Data type for the tensors. Defaults to ``torch.float32``.
38
+
39
+ Attributes:
40
+ data (List[pd.DataFrame]): DataFrames with the ``drop`` columns removed.
41
+ target (List[torch.Tensor]): Per-sequence target tensors, shape ``(T_i,)``.
42
+ features (List[torch.Tensor]): Per-sequence feature tensors, shape
43
+ ``(T_i, n_features)``.
44
+
45
+ Examples:
46
+ ```{python}
47
+ import pandas as pd
48
+ from spotoptim.data.manydataset import ManyToManyDataset
49
+
50
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [2.0, 4.0, 6.0]})
51
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [8.0, 10.0]})
52
+ ds = ManyToManyDataset([df1, df2], target="y")
53
+ print(len(ds))
54
+ features, targets = ds[0]
55
+ print(features.shape, targets.shape)
56
+ ```
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ df_list: List[pd.DataFrame],
62
+ target: str,
63
+ drop: Optional[Union[str, List[str]]] = None,
64
+ dtype: torch.dtype = torch.float32,
65
+ ):
66
+ if drop is None:
67
+ self.data = list(df_list)
68
+ else:
69
+ try:
70
+ self.data = [df.drop(drop, axis=1) for df in df_list]
71
+ except KeyError:
72
+ self.data = df_list
73
+ self.target = [
74
+ torch.tensor(df[target].to_numpy(), dtype=dtype) for df in self.data
75
+ ]
76
+ self.features = [
77
+ torch.tensor(df.drop([target], axis=1).to_numpy(), dtype=dtype)
78
+ for df in self.data
79
+ ]
80
+
81
+ def __getitem__(self, index: int):
82
+ """Return the ``(features, targets)`` pair of sequence ``index``."""
83
+ x = self.features[index]
84
+ y = self.target[index]
85
+ return x, y
86
+
87
+ def __len__(self) -> int:
88
+ """Return the number of sequences."""
89
+ return len(self.data)
90
+
91
+
92
+ class ManyToOneDataset(Dataset):
93
+ """Sequence dataset with a single target value per sequence.
94
+
95
+ Like `ManyToManyDataset`, but item ``i`` pairs the full feature sequence
96
+ with the scalar target taken from the first row of sequence ``i``.
97
+
98
+ Args:
99
+ df_list (List[pd.DataFrame]): List of pandas DataFrames, one per sequence.
100
+ target (str): The target column name.
101
+ drop (Optional[Union[str, List[str]]]): Column(s) to drop from the
102
+ DataFrames before extracting features. If a listed column is
103
+ missing, no column is dropped. Defaults to None.
104
+ dtype (torch.dtype): Data type for the tensors. Defaults to ``torch.float32``.
105
+
106
+ Attributes:
107
+ data (List[pd.DataFrame]): DataFrames with the ``drop`` columns removed.
108
+ target (List[torch.Tensor]): Per-sequence scalar target tensors.
109
+ features (List[torch.Tensor]): Per-sequence feature tensors, shape
110
+ ``(T_i, n_features)``.
111
+
112
+ Examples:
113
+ ```{python}
114
+ import pandas as pd
115
+ from spotoptim.data.manydataset import ManyToOneDataset
116
+
117
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [5.0, 5.0, 5.0]})
118
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [7.0, 7.0]})
119
+ ds = ManyToOneDataset([df1, df2], target="y")
120
+ features, target = ds[1]
121
+ print(features.shape, target)
122
+ ```
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ df_list: List[pd.DataFrame],
128
+ target: str,
129
+ drop: Optional[Union[str, List[str]]] = None,
130
+ dtype: torch.dtype = torch.float32,
131
+ ):
132
+ if drop is None:
133
+ self.data = list(df_list)
134
+ else:
135
+ try:
136
+ self.data = [df.drop(drop, axis=1) for df in df_list]
137
+ except KeyError:
138
+ self.data = df_list
139
+ self.target = [
140
+ torch.tensor(df[target].to_numpy()[0], dtype=dtype) for df in self.data
141
+ ]
142
+ self.features = [
143
+ torch.tensor(df.drop([target], axis=1).to_numpy(), dtype=dtype)
144
+ for df in self.data
145
+ ]
146
+
147
+ def __getitem__(self, index: int):
148
+ """Return the ``(features, target)`` pair of sequence ``index``."""
149
+ x = self.features[index]
150
+ y = self.target[index]
151
+ return x, y
152
+
153
+ def __len__(self) -> int:
154
+ """Return the number of sequences."""
155
+ return len(self.data)
156
+
157
+
158
+ class PadSequenceManyToMany:
159
+ """Padding collate for `ManyToManyDataset` batches.
160
+
161
+ Pads features and targets of a batch of variable-length sequences with
162
+ zeros to the longest sequence in the batch and records the true lengths,
163
+ as required by ``torch.nn.utils.rnn.pack_padded_sequence``.
164
+
165
+ Examples:
166
+ ```{python}
167
+ import pandas as pd
168
+ from torch.utils.data import DataLoader
169
+ from spotoptim.data.manydataset import ManyToManyDataset, PadSequenceManyToMany
170
+
171
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [2.0, 4.0, 6.0]})
172
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [8.0, 10.0]})
173
+ ds = ManyToManyDataset([df1, df2], target="y")
174
+ dl = DataLoader(ds, batch_size=2, shuffle=False, collate_fn=PadSequenceManyToMany())
175
+ x, lengths, y = next(iter(dl))
176
+ print(x.shape, lengths.tolist(), y.shape)
177
+ ```
178
+ """
179
+
180
+ def __call__(self, batch):
181
+ """Collate a batch into ``(padded_x, lengths, padded_y)``.
182
+
183
+ Args:
184
+ batch: Sequence of ``(features, targets)`` pairs as produced by
185
+ `ManyToManyDataset`.
186
+
187
+ Returns:
188
+ tuple: ``(padded_x, lengths, padded_y)`` where ``padded_x`` has
189
+ shape ``(B, T_max, n_features)``, ``lengths`` is an int tensor of
190
+ shape ``(B,)``, and ``padded_y`` has shape ``(B, T_max)``.
191
+ """
192
+ batch_x, batch_y = zip(*batch)
193
+ padded_batch_x = pad_sequence(list(batch_x), batch_first=True)
194
+ padded_batch_y = pad_sequence(list(batch_y), batch_first=True)
195
+ lengths = torch.tensor([len(x) for x in batch_x])
196
+
197
+ return padded_batch_x, lengths, padded_batch_y
198
+
199
+
200
+ class PadSequenceManyToOne:
201
+ """Padding collate for `ManyToOneDataset` batches.
202
+
203
+ Pads the feature sequences with zeros and stacks the scalar targets.
204
+
205
+ Examples:
206
+ ```{python}
207
+ import pandas as pd
208
+ from torch.utils.data import DataLoader
209
+ from spotoptim.data.manydataset import ManyToOneDataset, PadSequenceManyToOne
210
+
211
+ df1 = pd.DataFrame({"x": [1.0, 2.0, 3.0], "y": [5.0, 5.0, 5.0]})
212
+ df2 = pd.DataFrame({"x": [4.0, 5.0], "y": [7.0, 7.0]})
213
+ ds = ManyToOneDataset([df1, df2], target="y")
214
+ dl = DataLoader(ds, batch_size=2, shuffle=False, collate_fn=PadSequenceManyToOne())
215
+ x, lengths, y = next(iter(dl))
216
+ print(x.shape, lengths.tolist(), y.shape)
217
+ ```
218
+ """
219
+
220
+ def __call__(self, batch):
221
+ """Collate a batch into ``(padded_x, lengths, y)``.
222
+
223
+ Args:
224
+ batch: Sequence of ``(features, target)`` pairs as produced by
225
+ `ManyToOneDataset`.
226
+
227
+ Returns:
228
+ tuple: ``(padded_x, lengths, y)`` where ``padded_x`` has shape
229
+ ``(B, T_max, n_features)``, ``lengths`` is an int tensor of shape
230
+ ``(B,)``, and ``y`` is a tensor of shape ``(B,)``.
231
+ """
232
+ batch_x, batch_y = zip(*batch)
233
+ padded_batch_x = pad_sequence(list(batch_x), batch_first=True)
234
+ lengths = torch.tensor([len(x) for x in batch_x])
235
+
236
+ return padded_batch_x, lengths, torch.tensor(batch_y)
@@ -69,11 +69,16 @@ __all__ = [
69
69
  "branin",
70
70
  "onevar",
71
71
  "TorchObjective",
72
+ "SequenceCVObjective",
72
73
  "objective_remote",
73
74
  ]
74
75
 
75
76
  _lazy_map = {
76
77
  "TorchObjective": ("spotoptim.function.torch_objective", "TorchObjective"),
78
+ "SequenceCVObjective": (
79
+ "spotoptim.function.sequence_cv_objective",
80
+ "SequenceCVObjective",
81
+ ),
77
82
  "objective_remote": ("spotoptim.function.remote", "objective_remote"),
78
83
  }
79
84
 
@@ -0,0 +1,278 @@
1
+ # SPDX-FileCopyrightText: 2026 bartzbeielstein
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ """Leave-one-sequence-out cross-validation objective for sequence regression.
6
+
7
+ Ported from the training path used by ``spotpython.fun.hyperlight.HyperLight``
8
+ with ``fun_control["hacky"]=True`` (``spotpython.light.trainmodel``), without
9
+ Lightning: for every sequence of a `spotoptim.data.manydataset.ManyToManyDataset`,
10
+ a fresh model is trained on all remaining sequences for the full number of
11
+ epochs and evaluated on the held-out sequence; the objective value is the mean
12
+ of the held-out losses. As in spotPython, training runs the complete epoch
13
+ budget (the ``patience`` hyperparameter is accepted but has no effect, because
14
+ the reference implementation never attached a validation loader during
15
+ fitting) and the loss is computed on zero-padded batches without masking.
16
+
17
+ Requires the ``torch`` optional extra (``pip install 'spotoptim[torch]'``).
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Callable, Dict, Optional
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.nn as nn
26
+ from torch.optim.lr_scheduler import MultiStepLR
27
+ from torch.utils.data import DataLoader, Subset
28
+
29
+ from spotoptim.core.experiment import ExperimentControl
30
+ from spotoptim.function.torch_objective import TorchObjective
31
+ from spotoptim.nn.optimizer import optimizer_handler
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class SequenceCVObjective(TorchObjective):
37
+ """Callable SpotOptim objective running leave-one-sequence-out CV.
38
+
39
+ The experiment's dataset must be a
40
+ `spotoptim.core.data.SpotDataFromTorchDataset` whose training dataset
41
+ yields variable-length ``(features, targets)`` sequence pairs (e.g. a
42
+ `spotoptim.data.manydataset.ManyToManyDataset`). For each hyperparameter
43
+ configuration, every sequence is held out once: a fresh model is trained
44
+ on the remaining sequences and scored on the held-out one, and the mean
45
+ held-out loss is returned. A configuration whose training fails (e.g. an
46
+ optimizer incompatible with dense gradients) evaluates to ``np.nan``, to
47
+ be repaired by SpotOptim's ``penalty`` handling.
48
+
49
+ The tuned hyperparameters follow the spotPython ``ManyToManyRNNRegressor``
50
+ convention: ``epochs``, ``batch_size``, ``optimizer``, and ``lr_mult``
51
+ drive training via `spotoptim.nn.optimizer.optimizer_handler` and a
52
+ ``MultiStepLR`` schedule (three milestones at 1/4, 2/4, and 3/4 of the
53
+ epoch budget, decay factor 0.1); all remaining parameters are passed to
54
+ the model constructor.
55
+
56
+ Args:
57
+ experiment (ExperimentControl): Experiment configuration; its
58
+ ``dataset`` wraps the sequence dataset and its ``model_class`` is
59
+ instantiated once per fold.
60
+ seed (Optional[int]): Random seed applied before each configuration
61
+ evaluation. Falls back to ``experiment.seed`` when None.
62
+ Defaults to None.
63
+ collate_fn (Optional[Callable]): Batch collate producing
64
+ ``(padded_x, lengths, padded_y)``. Defaults to
65
+ `spotoptim.data.manydataset.PadSequenceManyToMany`.
66
+ param_mappers (Optional[Dict[str, Callable]]): Per-parameter functions
67
+ applied to the decoded hyperparameter values before training,
68
+ e.g. ``{"epochs": lambda v: 2 ** int(v)}`` to reproduce
69
+ spotPython's ``transform_power_2_int``. Defaults to None.
70
+
71
+ Examples:
72
+ ```{python}
73
+ import numpy as np
74
+ import pandas as pd
75
+ from spotoptim.core.data import SpotDataFromTorchDataset
76
+ from spotoptim.core.experiment import ExperimentControl
77
+ from spotoptim.data.manydataset import ManyToManyDataset
78
+ from spotoptim.function.sequence_cv_objective import SequenceCVObjective
79
+ from spotoptim.hyperparameters import ParameterSet
80
+ from spotoptim.nn.many_to_many_rnn import ManyToManyRNNRegressor
81
+
82
+ rng = np.random.default_rng(0)
83
+ frames = [
84
+ pd.DataFrame({"x": rng.random(4), "y": rng.random(4)})
85
+ for _ in range(3)
86
+ ]
87
+ ds = ManyToManyDataset(frames, target="y")
88
+ params = ParameterSet()
89
+ params.add_int("epochs", 1, 2)
90
+ params.add_float("lr_mult", 0.1, 1.0)
91
+ exp = ExperimentControl(
92
+ dataset=SpotDataFromTorchDataset(ds, input_dim=1, output_dim=1),
93
+ model_class=ManyToManyRNNRegressor,
94
+ hyperparameters=params,
95
+ seed=42,
96
+ )
97
+ objective = SequenceCVObjective(exp)
98
+ y = objective(np.array([[2, 1.0]]))
99
+ print(y.shape)
100
+ ```
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ experiment: ExperimentControl,
106
+ seed: Optional[int] = None,
107
+ collate_fn: Optional[Callable] = None,
108
+ param_mappers: Optional[Dict[str, Callable]] = None,
109
+ ):
110
+ super().__init__(experiment=experiment, seed=seed)
111
+ if collate_fn is None:
112
+ from spotoptim.data.manydataset import PadSequenceManyToMany
113
+
114
+ collate_fn = PadSequenceManyToMany()
115
+ self.collate_fn = collate_fn
116
+ self.param_mappers = param_mappers or {}
117
+
118
+ def decode_params(self, X_row: np.ndarray) -> Dict[str, Any]:
119
+ """Decode one parameter vector and apply the configured mappers.
120
+
121
+ Args:
122
+ X_row (np.ndarray): One row of the optimizer's input array, in
123
+ natural scale with factor levels as strings.
124
+
125
+ Returns:
126
+ Dict[str, Any]: Hyperparameter dictionary ready for training.
127
+ """
128
+ params = self._get_hyperparameters(X_row)
129
+ for name, mapper in self.param_mappers.items():
130
+ if name in params:
131
+ params[name] = mapper(params[name])
132
+ return params
133
+
134
+ def _build_model(self, params: Dict[str, Any]) -> nn.Module:
135
+ """Instantiate a fresh model for one fold.
136
+
137
+ Args:
138
+ params (Dict[str, Any]): Decoded hyperparameters.
139
+
140
+ Returns:
141
+ nn.Module: The model instance.
142
+ """
143
+ dataset = self.experiment.dataset
144
+ model_kwargs = {
145
+ "input_dim": dataset.input_dim,
146
+ "output_dim": dataset.output_dim,
147
+ }
148
+ model_kwargs.update(params)
149
+ return self.experiment.model_class(**model_kwargs)
150
+
151
+ def _train_fold(
152
+ self, model: nn.Module, train_loader: DataLoader, params: Dict[str, Any]
153
+ ) -> None:
154
+ """Train one fold for the full epoch budget.
155
+
156
+ Replicates the spotPython reference: one optimizer step per batch, one
157
+ ``MultiStepLR`` scheduler step per epoch, and the loss computed on the
158
+ zero-padded targets reshaped to the prediction shape.
159
+
160
+ Args:
161
+ model (nn.Module): The model to train (modified in place).
162
+ train_loader (DataLoader): Loader over the training sequences.
163
+ params (Dict[str, Any]): Decoded hyperparameters; consumes
164
+ ``epochs``, ``optimizer``, and ``lr_mult``.
165
+ """
166
+ epochs = int(params.get("epochs", self.experiment.epochs or 100))
167
+ optimizer = optimizer_handler(
168
+ optimizer_name=str(params.get("optimizer", "SGD")),
169
+ params=model.parameters(),
170
+ lr_mult=float(params.get("lr_mult", 1.0)),
171
+ )
172
+ num_milestones = 3
173
+ milestones = [
174
+ int(epochs / (num_milestones + 1) * (i + 1)) for i in range(num_milestones)
175
+ ]
176
+ scheduler = MultiStepLR(optimizer, milestones=milestones, gamma=0.1)
177
+ criterion = self.experiment.loss_function or nn.MSELoss()
178
+
179
+ model.to(self.device)
180
+ model.train()
181
+ for _ in range(epochs):
182
+ for x, lengths, y in train_loader:
183
+ x, y = x.to(self.device), y.to(self.device)
184
+ optimizer.zero_grad()
185
+ y_hat = model(x, lengths)
186
+ loss = criterion(y_hat, y.view_as(y_hat))
187
+ loss.backward()
188
+ optimizer.step()
189
+ scheduler.step()
190
+
191
+ def _validate_fold(self, model: nn.Module, val_loader: DataLoader) -> float:
192
+ """Compute the mean loss of one fold's held-out data.
193
+
194
+ Args:
195
+ model (nn.Module): The trained model.
196
+ val_loader (DataLoader): Loader over the held-out sequence(s).
197
+
198
+ Returns:
199
+ float: Mean loss over the loader's batches.
200
+ """
201
+ criterion = self.experiment.loss_function or nn.MSELoss()
202
+ model.eval()
203
+ losses = []
204
+ with torch.no_grad():
205
+ for x, lengths, y in val_loader:
206
+ x, y = x.to(self.device), y.to(self.device)
207
+ y_hat = model(x, lengths)
208
+ losses.append(criterion(y_hat, y.view_as(y_hat)).item())
209
+ return float(np.mean(losses))
210
+
211
+ def _evaluate_config(self, params: Dict[str, Any]) -> float:
212
+ """Run the full leave-one-sequence-out CV for one configuration.
213
+
214
+ Args:
215
+ params (Dict[str, Any]): Decoded hyperparameters.
216
+
217
+ Returns:
218
+ float: Mean held-out loss across all folds.
219
+ """
220
+ dataset = self.experiment.dataset.get_train_data()
221
+ batch_size = int(params.get("batch_size", self.experiment.batch_size))
222
+ indices = list(range(len(dataset)))
223
+ fold_losses = []
224
+ for i in indices:
225
+ train_indices = [j for j in indices if j != i]
226
+ train_loader = DataLoader(
227
+ Subset(dataset, train_indices),
228
+ batch_size=batch_size,
229
+ shuffle=False,
230
+ collate_fn=self.collate_fn,
231
+ num_workers=self.experiment.num_workers,
232
+ )
233
+ val_loader = DataLoader(
234
+ Subset(dataset, [i]),
235
+ batch_size=batch_size,
236
+ shuffle=False,
237
+ collate_fn=self.collate_fn,
238
+ num_workers=self.experiment.num_workers,
239
+ )
240
+ model = self._build_model(params)
241
+ self._train_fold(model, train_loader, params)
242
+ fold_loss = self._validate_fold(model, val_loader)
243
+ fold_losses.append(fold_loss)
244
+ if self.experiment.verbosity > 0:
245
+ print(f"SequenceCVObjective: fold {i} val_loss: {fold_loss}")
246
+ return float(np.mean(fold_losses))
247
+
248
+ def __call__(self, X: np.ndarray) -> np.ndarray:
249
+ """Evaluate the objective for an array of configurations.
250
+
251
+ Args:
252
+ X (np.ndarray): Input array of shape ``(n_samples, n_params)`` or
253
+ ``(n_params,)``, in natural scale with factor levels as strings.
254
+
255
+ Returns:
256
+ np.ndarray: Array of shape ``(n_samples, 1)`` with the mean
257
+ held-out loss per configuration; ``np.nan`` where the
258
+ evaluation failed.
259
+ """
260
+ X = np.atleast_2d(X)
261
+ results = []
262
+ for i in range(X.shape[0]):
263
+ if self.seed is not None:
264
+ self._set_seed(self.seed)
265
+ params = self.decode_params(X[i])
266
+ if self.experiment.verbosity > 0:
267
+ print(f"SequenceCVObjective: config: {params}")
268
+ try:
269
+ value = self._evaluate_config(params)
270
+ except Exception as err:
271
+ logger.error(
272
+ "SequenceCVObjective: evaluation failed (%s: %s); returning nan",
273
+ type(err).__name__,
274
+ err,
275
+ )
276
+ value = float("nan")
277
+ results.append([value])
278
+ return np.array(results)
@@ -4,11 +4,25 @@
4
4
 
5
5
  """Neural network models for spotoptim."""
6
6
 
7
- __all__ = ["LinearRegressor", "MLP"]
7
+ __all__ = [
8
+ "LinearRegressor",
9
+ "MLP",
10
+ "ManyToManyRNN",
11
+ "ManyToManyRNNRegressor",
12
+ "get_activation",
13
+ "optimizer_handler",
14
+ ]
8
15
 
9
16
  _lazy_map = {
10
17
  "LinearRegressor": ("spotoptim.nn.linear_regressor", "LinearRegressor"),
11
18
  "MLP": ("spotoptim.nn.mlp", "MLP"),
19
+ "ManyToManyRNN": ("spotoptim.nn.many_to_many_rnn", "ManyToManyRNN"),
20
+ "ManyToManyRNNRegressor": (
21
+ "spotoptim.nn.many_to_many_rnn",
22
+ "ManyToManyRNNRegressor",
23
+ ),
24
+ "get_activation": ("spotoptim.nn.many_to_many_rnn", "get_activation"),
25
+ "optimizer_handler": ("spotoptim.nn.optimizer", "optimizer_handler"),
12
26
  }
13
27
 
14
28