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.
- {spotoptim-3.0.0 → spotoptim-3.1.0}/PKG-INFO +1 -1
- {spotoptim-3.0.0 → spotoptim-3.1.0}/pyproject.toml +1 -1
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/__init__.py +14 -1
- spotoptim-3.1.0/src/spotoptim/data/manydataset.py +236 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/__init__.py +5 -0
- spotoptim-3.1.0/src/spotoptim/function/sequence_cv_objective.py +278 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/__init__.py +15 -1
- spotoptim-3.1.0/src/spotoptim/nn/many_to_many_rnn.py +234 -0
- spotoptim-3.1.0/src/spotoptim/nn/optimizer.py +202 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/README.md +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/SpotOptim.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/data.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/experiment.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/protocol.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/core/storage.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/base.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/data/diabetes.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/py.typed +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test01.csv +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test02.csv +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/datasets/test11.csv +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/eda/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/eda/plots.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/confirmatory_factor_analyzer.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_rotator.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/factor_analyzer/factor_analyzer_utils.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/cd_data.csv +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/forr08a.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/mo.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/remote.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/so.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/function/torch_objective.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/parameters.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/hyperparameters/repr_helpers.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/importance.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/inspection/predictions.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/mo_mm.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/mo/pareto.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/linear_regressor.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/nn/mlp.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/acquisition.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/schedule_free.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/optimizer/wrapper.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/contour.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/mo.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/plot/visualization.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/py.typed +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/analysis.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/reporting/results.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/design.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/effects.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/lhs.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/sampling/mm.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/kernels.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/kriging.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/mlp_surrogate.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/nystroem.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/pipeline.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/surrogate/simple_kriging.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/tricands/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/tricands/tricands.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/__init__.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/boundaries.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/convert.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/dimreduction.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/eval.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/file.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/mapping.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/ocba.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/pca.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/scaler.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/serialization.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/stats.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/tensorboard.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/transform.py +0 -0
- {spotoptim-3.0.0 → spotoptim-3.1.0}/src/spotoptim/utils/variables.py +0 -0
|
@@ -10,11 +10,24 @@ machine learning tasks.
|
|
|
10
10
|
|
|
11
11
|
from .base import Config, FileConfig
|
|
12
12
|
|
|
13
|
-
__all__ = [
|
|
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__ = [
|
|
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
|
|