D4CMPP2 0.4.0__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.
- D4CMPP2/_Data/AGENTS.md +24 -0
- D4CMPP2/_Data/Aqsoldb.csv +9291 -0
- D4CMPP2/_Data/BradleyMP.csv +3042 -0
- D4CMPP2/_Data/Lipophilicity.csv +1131 -0
- D4CMPP2/_Data/README.md +8 -0
- D4CMPP2/_Data/__init__.py +26 -0
- D4CMPP2/_Data/optical.csv +20237 -0
- D4CMPP2/_Data/test.csv +190 -0
- D4CMPP2/__init__.py +16 -0
- D4CMPP2/__main__.py +5 -0
- D4CMPP2/_main.py +500 -0
- D4CMPP2/cli.py +7 -0
- D4CMPP2/exceptions.py +53 -0
- D4CMPP2/grid_search.py +259 -0
- D4CMPP2/network_refer.yaml +160 -0
- D4CMPP2/networks/AFP_model.py +72 -0
- D4CMPP2/networks/AFPwithSolv_model.py +72 -0
- D4CMPP2/networks/DMPNN_model.py +90 -0
- D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
- D4CMPP2/networks/GAT_model.py +48 -0
- D4CMPP2/networks/GATwithSolv_model.py +63 -0
- D4CMPP2/networks/GCN_model.py +113 -0
- D4CMPP2/networks/GCNwithSolv_model.py +103 -0
- D4CMPP2/networks/GC_model.py +122 -0
- D4CMPP2/networks/ISATPM_model.py +14 -0
- D4CMPP2/networks/ISATPN_model.py +199 -0
- D4CMPP2/networks/ISAT_model.py +90 -0
- D4CMPP2/networks/MPNN_model.py +56 -0
- D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
- D4CMPP2/networks/__init__.py +25 -0
- D4CMPP2/networks/base.py +250 -0
- D4CMPP2/networks/registry.py +187 -0
- D4CMPP2/networks/src/AFP.py +118 -0
- D4CMPP2/networks/src/BiDropout.py +29 -0
- D4CMPP2/networks/src/DMPNN.py +35 -0
- D4CMPP2/networks/src/GAT.py +69 -0
- D4CMPP2/networks/src/GC.py +85 -0
- D4CMPP2/networks/src/GCN.py +71 -0
- D4CMPP2/networks/src/ISAT.py +153 -0
- D4CMPP2/networks/src/Linear.py +49 -0
- D4CMPP2/networks/src/MPNN.py +56 -0
- D4CMPP2/networks/src/SolventLayer.py +62 -0
- D4CMPP2/networks/src/__init__.py +0 -0
- D4CMPP2/networks/src/distGCN.py +21 -0
- D4CMPP2/networks/src/pyg_hetero.py +24 -0
- D4CMPP2/optimize.py +472 -0
- D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
- D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
- D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
- D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
- D4CMPP2/src/Analyzer/__init__.py +54 -0
- D4CMPP2/src/Analyzer/core.py +480 -0
- D4CMPP2/src/Analyzer/factory.py +166 -0
- D4CMPP2/src/Analyzer/interpretation.py +232 -0
- D4CMPP2/src/Analyzer/results.py +101 -0
- D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
- D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
- D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
- D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
- D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
- D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
- D4CMPP2/src/DataManager/ISADataManager.py +67 -0
- D4CMPP2/src/DataManager/MolDataManager.py +735 -0
- D4CMPP2/src/DataManager/__init__.py +14 -0
- D4CMPP2/src/DataManager/contracts.py +179 -0
- D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
- D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
- D4CMPP2/src/NetworkManager/__init__.py +14 -0
- D4CMPP2/src/PostProcessor.py +160 -0
- D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
- D4CMPP2/src/TrainManager/TrainManager.py +254 -0
- D4CMPP2/src/TrainManager/__init__.py +14 -0
- D4CMPP2/src/TrainManager/callbacks.py +119 -0
- D4CMPP2/src/__init__.py +0 -0
- D4CMPP2/src/utils/PATH.py +246 -0
- D4CMPP2/src/utils/__init__.py +0 -0
- D4CMPP2/src/utils/argparser.py +56 -0
- D4CMPP2/src/utils/checkpointing.py +90 -0
- D4CMPP2/src/utils/config_resolution.py +123 -0
- D4CMPP2/src/utils/config_validation.py +370 -0
- D4CMPP2/src/utils/csv_validation.py +105 -0
- D4CMPP2/src/utils/data_quality.py +181 -0
- D4CMPP2/src/utils/featureizer.py +202 -0
- D4CMPP2/src/utils/functional_group.csv +169 -0
- D4CMPP2/src/utils/graph_cache.py +213 -0
- D4CMPP2/src/utils/leaderboard.py +212 -0
- D4CMPP2/src/utils/metrics.py +31 -0
- D4CMPP2/src/utils/module_loader.py +147 -0
- D4CMPP2/src/utils/output.py +80 -0
- D4CMPP2/src/utils/reproducibility.py +70 -0
- D4CMPP2/src/utils/run_manifest.py +175 -0
- D4CMPP2/src/utils/scaler.py +40 -0
- D4CMPP2/src/utils/sculptor.py +713 -0
- D4CMPP2/src/utils/splitting.py +250 -0
- D4CMPP2/src/utils/supportfile_saver.py +94 -0
- D4CMPP2/src/utils/tools.py +156 -0
- D4CMPP2/src/utils/transfer_learning.py +111 -0
- d4cmpp2-0.4.0.dist-info/METADATA +420 -0
- d4cmpp2-0.4.0.dist-info/RECORD +103 -0
- d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
- d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
- d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
- d4cmpp2-0.4.0.dist-info/top_level.txt +1 -0
D4CMPP2/optimize.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Model-owned grid and Gaussian-process hyperparameter optimization."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import itertools
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import traceback
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from D4CMPP2._main import train
|
|
17
|
+
from D4CMPP2.networks.base import Hyperparameter
|
|
18
|
+
from D4CMPP2.networks.registry import get_model
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
OPTIMIZATION_SCHEMA_VERSION = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class OptimizationResult:
|
|
26
|
+
best_params: dict
|
|
27
|
+
best_score: float
|
|
28
|
+
best_model_path: str
|
|
29
|
+
summary_path: str
|
|
30
|
+
trials: tuple[dict, ...]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class _Domain:
|
|
35
|
+
name: str
|
|
36
|
+
kind: str
|
|
37
|
+
values: tuple = ()
|
|
38
|
+
low: float | int | None = None
|
|
39
|
+
high: float | int | None = None
|
|
40
|
+
step: float | int | None = None
|
|
41
|
+
scale: str = "linear"
|
|
42
|
+
|
|
43
|
+
def grid_values(self):
|
|
44
|
+
if self.values:
|
|
45
|
+
return self.values
|
|
46
|
+
if self.step is None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Grid domain {self.name!r} needs a value list or a step."
|
|
49
|
+
)
|
|
50
|
+
count = int(math.floor((self.high - self.low) / self.step)) + 1
|
|
51
|
+
values = tuple(self.low + index * self.step for index in range(count))
|
|
52
|
+
if self.kind == "int":
|
|
53
|
+
values = tuple(int(value) for value in values)
|
|
54
|
+
return values
|
|
55
|
+
|
|
56
|
+
def sample(self, rng):
|
|
57
|
+
if self.values:
|
|
58
|
+
return self.values[int(rng.integers(0, len(self.values)))]
|
|
59
|
+
if self.scale == "log":
|
|
60
|
+
value = math.exp(rng.uniform(math.log(self.low), math.log(self.high)))
|
|
61
|
+
else:
|
|
62
|
+
value = rng.uniform(self.low, self.high)
|
|
63
|
+
if self.kind == "int":
|
|
64
|
+
step = int(self.step or 1)
|
|
65
|
+
value = int(round((value - self.low) / step) * step + self.low)
|
|
66
|
+
return min(int(self.high), max(int(self.low), value))
|
|
67
|
+
if self.step:
|
|
68
|
+
value = round((value - self.low) / self.step) * self.step + self.low
|
|
69
|
+
return min(float(self.high), max(float(self.low), float(value)))
|
|
70
|
+
|
|
71
|
+
def encode(self, value):
|
|
72
|
+
if self.values:
|
|
73
|
+
index = self.values.index(value)
|
|
74
|
+
return 0.0 if len(self.values) == 1 else index / (len(self.values) - 1)
|
|
75
|
+
if self.scale == "log":
|
|
76
|
+
low, high, current = math.log(self.low), math.log(self.high), math.log(value)
|
|
77
|
+
else:
|
|
78
|
+
low, high, current = self.low, self.high, value
|
|
79
|
+
return 0.0 if high == low else (current - low) / (high - low)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _from_metadata(name, field, strategy):
|
|
83
|
+
if strategy == "grid":
|
|
84
|
+
if not field.grid:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"{name!r} has no predefined grid for this model. "
|
|
87
|
+
"Pass HP as a dict with explicit values."
|
|
88
|
+
)
|
|
89
|
+
return _Domain(name, field.kind, values=tuple(field.grid))
|
|
90
|
+
if field.kind == "categorical":
|
|
91
|
+
values = field.values or field.grid
|
|
92
|
+
return _Domain(name, field.kind, values=tuple(values))
|
|
93
|
+
if field.search_low is None or field.search_high is None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"{name!r} has no predefined Bayesian range. "
|
|
96
|
+
"Pass HP as a dict with low/high."
|
|
97
|
+
)
|
|
98
|
+
return _Domain(
|
|
99
|
+
name,
|
|
100
|
+
field.kind,
|
|
101
|
+
low=field.search_low,
|
|
102
|
+
high=field.search_high,
|
|
103
|
+
step=field.step,
|
|
104
|
+
scale=field.scale,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _explicit_domain(name, specification, field):
|
|
109
|
+
if isinstance(specification, list):
|
|
110
|
+
if not specification:
|
|
111
|
+
raise ValueError(f"HP[{name!r}] grid cannot be empty.")
|
|
112
|
+
values = tuple(field.validate(name, value) for value in specification)
|
|
113
|
+
return _Domain(name, field.kind, values=values)
|
|
114
|
+
if isinstance(specification, tuple):
|
|
115
|
+
if len(specification) != 2:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"HP[{name!r}] range tuple must be (low, high), got {specification!r}."
|
|
118
|
+
)
|
|
119
|
+
specification = {"low": specification[0], "high": specification[1]}
|
|
120
|
+
if not isinstance(specification, dict):
|
|
121
|
+
raise TypeError(
|
|
122
|
+
f"HP[{name!r}] must be a list grid, (low, high) tuple, or range dict."
|
|
123
|
+
)
|
|
124
|
+
if "values" in specification or "grid" in specification:
|
|
125
|
+
values = specification.get("values", specification.get("grid"))
|
|
126
|
+
if not isinstance(values, (list, tuple)) or not values:
|
|
127
|
+
raise ValueError(f"HP[{name!r}] values/grid must be non-empty.")
|
|
128
|
+
return _Domain(
|
|
129
|
+
name,
|
|
130
|
+
field.kind,
|
|
131
|
+
values=tuple(field.validate(name, value) for value in values),
|
|
132
|
+
)
|
|
133
|
+
if "low" not in specification or "high" not in specification:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"HP[{name!r}] range requires low and high, or values/grid."
|
|
136
|
+
)
|
|
137
|
+
if field.kind == "categorical":
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"HP[{name!r}] is categorical; pass a non-empty values/grid list."
|
|
140
|
+
)
|
|
141
|
+
low = field.validate(name, specification["low"])
|
|
142
|
+
high = field.validate(name, specification["high"])
|
|
143
|
+
if low >= high:
|
|
144
|
+
raise ValueError(f"HP[{name!r}] requires low < high, got {low!r}, {high!r}.")
|
|
145
|
+
scale = specification.get("scale", field.scale)
|
|
146
|
+
if scale not in {"linear", "log"}:
|
|
147
|
+
raise ValueError(f"HP[{name!r}] scale must be 'linear' or 'log'.")
|
|
148
|
+
if scale == "log" and low <= 0:
|
|
149
|
+
raise ValueError(f"HP[{name!r}] log range requires low > 0.")
|
|
150
|
+
step = specification.get("step", field.step)
|
|
151
|
+
if step is not None:
|
|
152
|
+
if isinstance(step, bool) or not isinstance(step, (int, float)) or step <= 0:
|
|
153
|
+
raise ValueError(f"HP[{name!r}] step must be a positive number.")
|
|
154
|
+
if field.kind == "int" and not isinstance(step, int):
|
|
155
|
+
raise TypeError(f"HP[{name!r}] integer range requires an integer step.")
|
|
156
|
+
return _Domain(
|
|
157
|
+
name,
|
|
158
|
+
field.kind,
|
|
159
|
+
low=low,
|
|
160
|
+
high=high,
|
|
161
|
+
step=step,
|
|
162
|
+
scale=scale,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def normalize_hp(network, HP, strategy):
|
|
167
|
+
if strategy not in {"grid", "bayesian"}:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"optimize_strategy must be 'grid' or 'bayesian', got {strategy!r}."
|
|
170
|
+
)
|
|
171
|
+
definition = get_model(network)
|
|
172
|
+
model = definition.network
|
|
173
|
+
if HP is None:
|
|
174
|
+
selected = model.optimization_space()
|
|
175
|
+
return tuple(
|
|
176
|
+
_from_metadata(name, field, strategy)
|
|
177
|
+
for name, field in selected.items()
|
|
178
|
+
)
|
|
179
|
+
if isinstance(HP, list):
|
|
180
|
+
if not HP or not all(isinstance(name, str) and name for name in HP):
|
|
181
|
+
raise ValueError("HP list must contain one or more non-empty keys.")
|
|
182
|
+
selected = model.optimization_space(HP)
|
|
183
|
+
return tuple(
|
|
184
|
+
_from_metadata(name, field, strategy)
|
|
185
|
+
for name, field in selected.items()
|
|
186
|
+
)
|
|
187
|
+
if not isinstance(HP, dict) or not HP:
|
|
188
|
+
raise TypeError("HP must be None, a non-empty key list, or a non-empty dict.")
|
|
189
|
+
unknown = [name for name in HP if name not in model.hyperparameters]
|
|
190
|
+
if unknown:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"{model.model_name} does not define hyperparameters {unknown!r}. "
|
|
193
|
+
f"Available keys: {sorted(model.hyperparameters)!r}."
|
|
194
|
+
)
|
|
195
|
+
return tuple(
|
|
196
|
+
_explicit_domain(name, specification, model.hyperparameters[name])
|
|
197
|
+
for name, specification in HP.items()
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _signature(parameters):
|
|
202
|
+
return json.dumps(parameters, sort_keys=True, separators=(",", ":"), default=repr)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _domain_records(domains):
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
"name": domain.name,
|
|
209
|
+
"kind": domain.kind,
|
|
210
|
+
"values": list(domain.values),
|
|
211
|
+
"low": domain.low,
|
|
212
|
+
"high": domain.high,
|
|
213
|
+
"step": domain.step,
|
|
214
|
+
"scale": domain.scale,
|
|
215
|
+
}
|
|
216
|
+
for domain in domains
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _read_objective(model_path):
|
|
221
|
+
candidates = (
|
|
222
|
+
Path(model_path) / "result" / "learning_curve.csv",
|
|
223
|
+
Path(model_path) / "learning_curve.csv",
|
|
224
|
+
)
|
|
225
|
+
for path in candidates:
|
|
226
|
+
if not path.exists():
|
|
227
|
+
continue
|
|
228
|
+
with open(path, "r", encoding="utf-8", newline="") as stream:
|
|
229
|
+
values = [
|
|
230
|
+
float(row["val_loss"])
|
|
231
|
+
for row in csv.DictReader(stream)
|
|
232
|
+
if row.get("val_loss") not in (None, "")
|
|
233
|
+
]
|
|
234
|
+
finite = [value for value in values if math.isfinite(value)]
|
|
235
|
+
if finite:
|
|
236
|
+
return min(finite)
|
|
237
|
+
raise FileNotFoundError(
|
|
238
|
+
f"No finite val_loss was found below model path {str(model_path)!r}."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _atomic_json(path, value):
|
|
243
|
+
staging = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
244
|
+
try:
|
|
245
|
+
staging.write_text(json.dumps(value, indent=2, sort_keys=True), encoding="utf-8")
|
|
246
|
+
os.replace(staging, path)
|
|
247
|
+
finally:
|
|
248
|
+
if staging.exists():
|
|
249
|
+
staging.unlink()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _write_summary(path, summary):
|
|
253
|
+
_atomic_json(path, summary)
|
|
254
|
+
csv_path = path.with_suffix(".csv")
|
|
255
|
+
staging = csv_path.with_name(f".{csv_path.name}.{uuid.uuid4().hex}.tmp")
|
|
256
|
+
fields = ("trial", "status", "objective", "model_path", "parameters", "error")
|
|
257
|
+
try:
|
|
258
|
+
with open(staging, "w", encoding="utf-8", newline="") as stream:
|
|
259
|
+
writer = csv.DictWriter(stream, fieldnames=fields)
|
|
260
|
+
writer.writeheader()
|
|
261
|
+
for trial in summary["trials"]:
|
|
262
|
+
row = {name: trial.get(name) for name in fields}
|
|
263
|
+
row["parameters"] = json.dumps(trial["parameters"], sort_keys=True)
|
|
264
|
+
writer.writerow(row)
|
|
265
|
+
os.replace(staging, csv_path)
|
|
266
|
+
finally:
|
|
267
|
+
if staging.exists():
|
|
268
|
+
staging.unlink()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _random_parameters(domains, rng):
|
|
272
|
+
return {domain.name: domain.sample(rng) for domain in domains}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _bayesian_parameters(domains, completed, used, rng):
|
|
276
|
+
initial_count = max(4, 2 * len(domains))
|
|
277
|
+
if len(completed) < initial_count:
|
|
278
|
+
for _ in range(1000):
|
|
279
|
+
parameters = _random_parameters(domains, rng)
|
|
280
|
+
if _signature(parameters) not in used:
|
|
281
|
+
return parameters
|
|
282
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
283
|
+
from sklearn.gaussian_process.kernels import Matern, WhiteKernel
|
|
284
|
+
|
|
285
|
+
x = np.asarray(
|
|
286
|
+
[[domain.encode(trial["parameters"][domain.name]) for domain in domains]
|
|
287
|
+
for trial in completed],
|
|
288
|
+
dtype=float,
|
|
289
|
+
)
|
|
290
|
+
y = np.asarray([trial["objective"] for trial in completed], dtype=float)
|
|
291
|
+
surrogate = GaussianProcessRegressor(
|
|
292
|
+
kernel=Matern(nu=2.5) + WhiteKernel(noise_level=1e-6),
|
|
293
|
+
normalize_y=True,
|
|
294
|
+
random_state=0,
|
|
295
|
+
n_restarts_optimizer=1,
|
|
296
|
+
)
|
|
297
|
+
surrogate.fit(x, y)
|
|
298
|
+
candidates = []
|
|
299
|
+
for _ in range(2048):
|
|
300
|
+
parameters = _random_parameters(domains, rng)
|
|
301
|
+
signature = _signature(parameters)
|
|
302
|
+
if signature not in used:
|
|
303
|
+
candidates.append(parameters)
|
|
304
|
+
if len(candidates) >= 512:
|
|
305
|
+
break
|
|
306
|
+
if not candidates:
|
|
307
|
+
raise StopIteration("The finite hyperparameter space is exhausted.")
|
|
308
|
+
encoded = np.asarray(
|
|
309
|
+
[[domain.encode(candidate[domain.name]) for domain in domains]
|
|
310
|
+
for candidate in candidates],
|
|
311
|
+
dtype=float,
|
|
312
|
+
)
|
|
313
|
+
mean, std = surrogate.predict(encoded, return_std=True)
|
|
314
|
+
return candidates[int(np.argmin(mean - 1.96 * std))]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def optimize(
|
|
318
|
+
*,
|
|
319
|
+
data,
|
|
320
|
+
target,
|
|
321
|
+
network,
|
|
322
|
+
HP=None,
|
|
323
|
+
optimize_strategy="bayesian",
|
|
324
|
+
n_trials=None,
|
|
325
|
+
random_seed=42,
|
|
326
|
+
optimization_path=None,
|
|
327
|
+
resume=True,
|
|
328
|
+
**train_kwargs,
|
|
329
|
+
):
|
|
330
|
+
"""Tune one registered network and return its best completed trial."""
|
|
331
|
+
if not isinstance(optimize_strategy, str):
|
|
332
|
+
raise TypeError("optimize_strategy must be 'grid' or 'bayesian'.")
|
|
333
|
+
strategy = optimize_strategy.lower()
|
|
334
|
+
domains = normalize_hp(network, HP, strategy)
|
|
335
|
+
domain_records = _domain_records(domains)
|
|
336
|
+
if isinstance(random_seed, bool) or not isinstance(random_seed, int):
|
|
337
|
+
raise TypeError("random_seed must be an integer.")
|
|
338
|
+
if strategy == "bayesian":
|
|
339
|
+
n_trials = 20 if n_trials is None else n_trials
|
|
340
|
+
if isinstance(n_trials, bool) or not isinstance(n_trials, int) or n_trials < 1:
|
|
341
|
+
raise ValueError("n_trials must be a positive integer for Bayesian search.")
|
|
342
|
+
elif n_trials is not None:
|
|
343
|
+
raise ValueError("n_trials is only used with optimize_strategy='bayesian'.")
|
|
344
|
+
|
|
345
|
+
root = Path(
|
|
346
|
+
optimization_path
|
|
347
|
+
or Path(train_kwargs.get("MODEL_DIR", "_Models"))
|
|
348
|
+
/ f"optimize_{network}"
|
|
349
|
+
).resolve()
|
|
350
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
summary_path = root / "optimization.json"
|
|
352
|
+
if resume and summary_path.exists():
|
|
353
|
+
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
|
354
|
+
if summary.get("strategy") != strategy or summary.get("network") != network:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"Existing optimization summary {str(summary_path)!r} belongs to "
|
|
357
|
+
f"network={summary.get('network')!r}, strategy={summary.get('strategy')!r}."
|
|
358
|
+
)
|
|
359
|
+
if summary.get("domains") != domain_records:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"Existing optimization summary {str(summary_path)!r} uses a different "
|
|
362
|
+
"HP search space. Choose another optimization_path or set resume=False."
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
summary = {
|
|
366
|
+
"schema_version": OPTIMIZATION_SCHEMA_VERSION,
|
|
367
|
+
"status": "running",
|
|
368
|
+
"network": network,
|
|
369
|
+
"strategy": strategy,
|
|
370
|
+
"domains": domain_records,
|
|
371
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
372
|
+
"trials": [],
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
completed = [trial for trial in summary["trials"] if trial["status"] == "completed"]
|
|
376
|
+
used = {_signature(trial["parameters"]) for trial in summary["trials"]}
|
|
377
|
+
rng = np.random.default_rng(random_seed + len(summary["trials"]))
|
|
378
|
+
if strategy == "grid":
|
|
379
|
+
names = [domain.name for domain in domains]
|
|
380
|
+
candidates = (
|
|
381
|
+
dict(zip(names, values))
|
|
382
|
+
for values in itertools.product(*(domain.grid_values() for domain in domains))
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
candidates = None
|
|
386
|
+
|
|
387
|
+
while True:
|
|
388
|
+
if strategy == "grid":
|
|
389
|
+
parameters = next(
|
|
390
|
+
(candidate for candidate in candidates if _signature(candidate) not in used),
|
|
391
|
+
None,
|
|
392
|
+
)
|
|
393
|
+
if parameters is None:
|
|
394
|
+
break
|
|
395
|
+
else:
|
|
396
|
+
if len(summary["trials"]) >= n_trials:
|
|
397
|
+
break
|
|
398
|
+
try:
|
|
399
|
+
parameters = _bayesian_parameters(domains, completed, used, rng)
|
|
400
|
+
except StopIteration:
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
number = len(summary["trials"]) + 1
|
|
404
|
+
model_path = root / "trials" / f"trial_{number:04d}"
|
|
405
|
+
trial = {
|
|
406
|
+
"trial": number,
|
|
407
|
+
"status": "running",
|
|
408
|
+
"parameters": parameters,
|
|
409
|
+
"model_path": str(model_path),
|
|
410
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
411
|
+
}
|
|
412
|
+
summary["trials"].append(trial)
|
|
413
|
+
used.add(_signature(parameters))
|
|
414
|
+
_write_summary(summary_path, summary)
|
|
415
|
+
try:
|
|
416
|
+
trial_kwargs = dict(train_kwargs)
|
|
417
|
+
trial_kwargs.update(parameters)
|
|
418
|
+
trial_kwargs.update(
|
|
419
|
+
data=data,
|
|
420
|
+
target=target,
|
|
421
|
+
network=network,
|
|
422
|
+
MODEL_PATH=str(model_path),
|
|
423
|
+
)
|
|
424
|
+
result_path = train(**trial_kwargs)
|
|
425
|
+
trial["model_path"] = result_path
|
|
426
|
+
trial["objective"] = _read_objective(result_path)
|
|
427
|
+
trial["status"] = "completed"
|
|
428
|
+
completed.append(trial)
|
|
429
|
+
except KeyboardInterrupt:
|
|
430
|
+
trial["status"] = "interrupted"
|
|
431
|
+
trial["error"] = "KeyboardInterrupt"
|
|
432
|
+
summary["status"] = "interrupted"
|
|
433
|
+
trial["ended_at"] = datetime.now(timezone.utc).isoformat()
|
|
434
|
+
_write_summary(summary_path, summary)
|
|
435
|
+
raise
|
|
436
|
+
except Exception as exc:
|
|
437
|
+
trial["status"] = "failed"
|
|
438
|
+
trial["error"] = f"{type(exc).__name__}: {exc}"
|
|
439
|
+
trial["traceback"] = traceback.format_exc()[-8000:]
|
|
440
|
+
trial["ended_at"] = datetime.now(timezone.utc).isoformat()
|
|
441
|
+
_write_summary(summary_path, summary)
|
|
442
|
+
|
|
443
|
+
if not completed:
|
|
444
|
+
summary["status"] = "failed"
|
|
445
|
+
_write_summary(summary_path, summary)
|
|
446
|
+
failures = "; ".join(
|
|
447
|
+
f"trial {trial['trial']}: {trial.get('error', trial['status'])}"
|
|
448
|
+
for trial in summary["trials"][-3:]
|
|
449
|
+
)
|
|
450
|
+
raise RuntimeError(
|
|
451
|
+
"Optimization completed no successful trials. "
|
|
452
|
+
f"Recent failures: {failures}. See {str(summary_path)!r}."
|
|
453
|
+
)
|
|
454
|
+
best = min(completed, key=lambda trial: trial["objective"])
|
|
455
|
+
summary["status"] = (
|
|
456
|
+
"completed_with_failures"
|
|
457
|
+
if any(trial["status"] == "failed" for trial in summary["trials"])
|
|
458
|
+
else "completed"
|
|
459
|
+
)
|
|
460
|
+
summary["best_trial"] = best["trial"]
|
|
461
|
+
summary["best_objective"] = best["objective"]
|
|
462
|
+
summary["best_parameters"] = best["parameters"]
|
|
463
|
+
summary["best_model_path"] = best["model_path"]
|
|
464
|
+
summary["ended_at"] = datetime.now(timezone.utc).isoformat()
|
|
465
|
+
_write_summary(summary_path, summary)
|
|
466
|
+
return OptimizationResult(
|
|
467
|
+
best_params=dict(best["parameters"]),
|
|
468
|
+
best_score=float(best["objective"]),
|
|
469
|
+
best_model_path=best["model_path"],
|
|
470
|
+
summary_path=str(summary_path),
|
|
471
|
+
trials=tuple(summary["trials"]),
|
|
472
|
+
)
|