ins-pricing 0.5.0__py3-none-any.whl → 0.5.3__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.
- ins_pricing/cli/BayesOpt_entry.py +15 -5
- ins_pricing/cli/BayesOpt_incremental.py +43 -10
- ins_pricing/cli/Explain_Run.py +16 -5
- ins_pricing/cli/Explain_entry.py +29 -8
- ins_pricing/cli/Pricing_Run.py +16 -5
- ins_pricing/cli/bayesopt_entry_runner.py +45 -12
- ins_pricing/cli/utils/bootstrap.py +23 -0
- ins_pricing/cli/utils/cli_config.py +34 -15
- ins_pricing/cli/utils/import_resolver.py +14 -14
- ins_pricing/cli/utils/notebook_utils.py +120 -106
- ins_pricing/cli/watchdog_run.py +15 -5
- ins_pricing/frontend/app.py +132 -61
- ins_pricing/frontend/config_builder.py +33 -0
- ins_pricing/frontend/example_config.json +11 -0
- ins_pricing/frontend/runner.py +340 -388
- ins_pricing/modelling/README.md +1 -1
- ins_pricing/modelling/__init__.py +10 -10
- ins_pricing/modelling/bayesopt/README.md +29 -11
- ins_pricing/modelling/bayesopt/config_components.py +12 -0
- ins_pricing/modelling/bayesopt/config_preprocess.py +50 -13
- ins_pricing/modelling/bayesopt/core.py +47 -19
- ins_pricing/modelling/bayesopt/model_plotting_mixin.py +20 -14
- ins_pricing/modelling/bayesopt/models/model_ft_components.py +349 -342
- ins_pricing/modelling/bayesopt/models/model_ft_trainer.py +11 -5
- ins_pricing/modelling/bayesopt/models/model_gnn.py +20 -14
- ins_pricing/modelling/bayesopt/models/model_resn.py +9 -3
- ins_pricing/modelling/bayesopt/trainers/trainer_base.py +62 -50
- ins_pricing/modelling/bayesopt/trainers/trainer_ft.py +61 -53
- ins_pricing/modelling/bayesopt/trainers/trainer_glm.py +9 -3
- ins_pricing/modelling/bayesopt/trainers/trainer_gnn.py +40 -32
- ins_pricing/modelling/bayesopt/trainers/trainer_resn.py +36 -24
- ins_pricing/modelling/bayesopt/trainers/trainer_xgb.py +240 -37
- ins_pricing/modelling/bayesopt/utils/distributed_utils.py +193 -186
- ins_pricing/modelling/bayesopt/utils/torch_trainer_mixin.py +23 -10
- ins_pricing/pricing/factors.py +67 -56
- ins_pricing/setup.py +1 -1
- ins_pricing/utils/__init__.py +7 -6
- ins_pricing/utils/device.py +45 -24
- ins_pricing/utils/logging.py +34 -1
- ins_pricing/utils/profiling.py +8 -4
- {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/METADATA +182 -182
- {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/RECORD +44 -43
- {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/WHEEL +0 -0
- {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/top_level.txt +0 -0
|
@@ -9,9 +9,15 @@ import statsmodels.api as sm
|
|
|
9
9
|
from sklearn.metrics import log_loss
|
|
10
10
|
|
|
11
11
|
from ins_pricing.modelling.bayesopt.trainers.trainer_base import TrainerBase
|
|
12
|
-
from ins_pricing.utils import EPS
|
|
12
|
+
from ins_pricing.utils import EPS, get_logger, log_print
|
|
13
13
|
from ins_pricing.utils.losses import regression_loss
|
|
14
14
|
|
|
15
|
+
_logger = get_logger("ins_pricing.trainer.glm")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _log(*args, **kwargs) -> None:
|
|
19
|
+
log_print(_logger, *args, **kwargs)
|
|
20
|
+
|
|
15
21
|
class GLMTrainer(TrainerBase):
|
|
16
22
|
def __init__(self, context: "BayesOptModel") -> None:
|
|
17
23
|
super().__init__(context, 'GLM', 'GLM')
|
|
@@ -160,7 +166,7 @@ class GLMTrainer(TrainerBase):
|
|
|
160
166
|
|
|
161
167
|
split_iter, _ = self._resolve_ensemble_splits(X_all, k=k)
|
|
162
168
|
if split_iter is None:
|
|
163
|
-
|
|
169
|
+
_log(
|
|
164
170
|
f"[GLM Ensemble] unable to build CV split (n_samples={n_samples}); skip ensemble.",
|
|
165
171
|
flush=True,
|
|
166
172
|
)
|
|
@@ -187,7 +193,7 @@ class GLMTrainer(TrainerBase):
|
|
|
187
193
|
split_count += 1
|
|
188
194
|
|
|
189
195
|
if split_count < 1:
|
|
190
|
-
|
|
196
|
+
_log(
|
|
191
197
|
f"[GLM Ensemble] no CV splits generated; skip ensemble.",
|
|
192
198
|
flush=True,
|
|
193
199
|
)
|
|
@@ -10,18 +10,30 @@ from sklearn.metrics import log_loss
|
|
|
10
10
|
|
|
11
11
|
from ins_pricing.modelling.bayesopt.trainers.trainer_base import TrainerBase
|
|
12
12
|
from ins_pricing.modelling.bayesopt.models import GraphNeuralNetSklearn
|
|
13
|
-
from ins_pricing.utils import EPS
|
|
13
|
+
from ins_pricing.utils import EPS, get_logger, log_print
|
|
14
14
|
from ins_pricing.utils.losses import regression_loss
|
|
15
|
-
from ins_pricing.utils import get_logger
|
|
16
15
|
from ins_pricing.utils.torch_compat import torch_load
|
|
17
16
|
|
|
18
17
|
_logger = get_logger("ins_pricing.trainer.gnn")
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
|
|
20
|
+
def _log(*args, **kwargs) -> None:
|
|
21
|
+
log_print(_logger, *args, **kwargs)
|
|
22
|
+
|
|
23
|
+
class GNNTrainer(TrainerBase):
|
|
24
|
+
def __init__(self, context: "BayesOptModel") -> None:
|
|
25
|
+
super().__init__(context, 'GNN', 'GNN')
|
|
26
|
+
self.model: Optional[GraphNeuralNetSklearn] = None
|
|
27
|
+
self.enable_distributed_optuna = bool(context.config.use_gnn_ddp)
|
|
28
|
+
|
|
29
|
+
def _maybe_cleanup_gpu(self, model: Optional[GraphNeuralNetSklearn]) -> None:
|
|
30
|
+
if not bool(getattr(self.ctx.config, "gnn_cleanup_per_fold", False)):
|
|
31
|
+
return
|
|
32
|
+
if model is not None:
|
|
33
|
+
getattr(getattr(model, "gnn", None), "to",
|
|
34
|
+
lambda *_args, **_kwargs: None)("cpu")
|
|
35
|
+
synchronize = bool(getattr(self.ctx.config, "gnn_cleanup_synchronize", False))
|
|
36
|
+
self._clean_gpu(synchronize=synchronize)
|
|
25
37
|
|
|
26
38
|
def _build_model(self, params: Optional[Dict[str, Any]] = None) -> GraphNeuralNetSklearn:
|
|
27
39
|
params = params or {}
|
|
@@ -167,19 +179,17 @@ class GNNTrainer(TrainerBase):
|
|
|
167
179
|
|
|
168
180
|
if use_refit:
|
|
169
181
|
tmp_model = self._build_model(self.best_params)
|
|
170
|
-
tmp_model.fit(
|
|
171
|
-
X_train,
|
|
172
|
-
y_train,
|
|
173
|
-
w_train=w_train,
|
|
174
|
-
X_val=X_val,
|
|
175
|
-
y_val=y_val,
|
|
176
|
-
w_val=w_val,
|
|
177
|
-
trial=None,
|
|
178
|
-
)
|
|
179
|
-
refit_epochs = int(getattr(tmp_model, "best_epoch", None) or self.ctx.epochs)
|
|
180
|
-
|
|
181
|
-
lambda *_args, **_kwargs: None)("cpu")
|
|
182
|
-
self._clean_gpu()
|
|
182
|
+
tmp_model.fit(
|
|
183
|
+
X_train,
|
|
184
|
+
y_train,
|
|
185
|
+
w_train=w_train,
|
|
186
|
+
X_val=X_val,
|
|
187
|
+
y_val=y_val,
|
|
188
|
+
w_val=w_val,
|
|
189
|
+
trial=None,
|
|
190
|
+
)
|
|
191
|
+
refit_epochs = int(getattr(tmp_model, "best_epoch", None) or self.ctx.epochs)
|
|
192
|
+
self._maybe_cleanup_gpu(tmp_model)
|
|
183
193
|
else:
|
|
184
194
|
self.model = self._build_model(self.best_params)
|
|
185
195
|
self.model.fit(
|
|
@@ -242,7 +252,7 @@ class GNNTrainer(TrainerBase):
|
|
|
242
252
|
n_samples = len(X_all)
|
|
243
253
|
split_iter, _ = self._resolve_ensemble_splits(X_all, k=k)
|
|
244
254
|
if split_iter is None:
|
|
245
|
-
|
|
255
|
+
_log(
|
|
246
256
|
f"[GNN Ensemble] unable to build CV split (n_samples={n_samples}); skip ensemble.",
|
|
247
257
|
flush=True,
|
|
248
258
|
)
|
|
@@ -264,15 +274,13 @@ class GNNTrainer(TrainerBase):
|
|
|
264
274
|
)
|
|
265
275
|
pred_train = model.predict(X_all)
|
|
266
276
|
pred_test = model.predict(X_test)
|
|
267
|
-
preds_train_sum += np.asarray(pred_train, dtype=np.float64)
|
|
268
|
-
preds_test_sum += np.asarray(pred_test, dtype=np.float64)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
self._clean_gpu()
|
|
272
|
-
split_count += 1
|
|
277
|
+
preds_train_sum += np.asarray(pred_train, dtype=np.float64)
|
|
278
|
+
preds_test_sum += np.asarray(pred_test, dtype=np.float64)
|
|
279
|
+
self._maybe_cleanup_gpu(model)
|
|
280
|
+
split_count += 1
|
|
273
281
|
|
|
274
282
|
if split_count < 1:
|
|
275
|
-
|
|
283
|
+
_log(
|
|
276
284
|
f"[GNN Ensemble] no CV splits generated; skip ensemble.",
|
|
277
285
|
flush=True,
|
|
278
286
|
)
|
|
@@ -297,11 +305,11 @@ class GNNTrainer(TrainerBase):
|
|
|
297
305
|
self.ctx.test_geo_tokens = test_tokens
|
|
298
306
|
self.ctx.geo_token_cols = cols
|
|
299
307
|
self.ctx.geo_gnn_model = geo_gnn
|
|
300
|
-
|
|
308
|
+
_log(f"[GeoToken][GNNTrainer] Generated {len(cols)} dims and injected into FT.", flush=True)
|
|
301
309
|
|
|
302
310
|
def save(self) -> None:
|
|
303
311
|
if self.model is None:
|
|
304
|
-
|
|
312
|
+
_log(f"[save] Warning: No model to save for {self.label}")
|
|
305
313
|
return
|
|
306
314
|
path = self.output.model_path(self._get_model_filename())
|
|
307
315
|
base_gnn = getattr(self.model, "_unwrap_gnn", lambda: None)()
|
|
@@ -318,7 +326,7 @@ class GNNTrainer(TrainerBase):
|
|
|
318
326
|
def load(self) -> None:
|
|
319
327
|
path = self.output.model_path(self._get_model_filename())
|
|
320
328
|
if not os.path.exists(path):
|
|
321
|
-
|
|
329
|
+
_log(f"[load] Warning: Model file not found: {path}")
|
|
322
330
|
return
|
|
323
331
|
payload = torch_load(path, map_location='cpu', weights_only=False)
|
|
324
332
|
if not isinstance(payload, dict):
|
|
@@ -335,7 +343,7 @@ class GNNTrainer(TrainerBase):
|
|
|
335
343
|
base_gnn.load_state_dict(state_dict, strict=True)
|
|
336
344
|
except RuntimeError as e:
|
|
337
345
|
if "Missing key" in str(e) or "Unexpected key" in str(e):
|
|
338
|
-
|
|
346
|
+
_log(f"[GNN load] Warning: State dict mismatch, loading with strict=False: {e}")
|
|
339
347
|
base_gnn.load_state_dict(state_dict, strict=False)
|
|
340
348
|
else:
|
|
341
349
|
raise
|
|
@@ -11,15 +11,31 @@ from sklearn.metrics import log_loss
|
|
|
11
11
|
from ins_pricing.modelling.bayesopt.trainers.trainer_base import TrainerBase
|
|
12
12
|
from ins_pricing.modelling.bayesopt.models import ResNetSklearn
|
|
13
13
|
from ins_pricing.utils.losses import regression_loss
|
|
14
|
+
from ins_pricing.utils import get_logger, log_print
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
_logger = get_logger("ins_pricing.trainer.resn")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _log(*args, **kwargs) -> None:
|
|
20
|
+
log_print(_logger, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
class ResNetTrainer(TrainerBase):
|
|
23
|
+
def __init__(self, context: "BayesOptModel") -> None:
|
|
24
|
+
if context.task_type == 'classification':
|
|
25
|
+
super().__init__(context, 'ResNetClassifier', 'ResNet')
|
|
26
|
+
else:
|
|
27
|
+
super().__init__(context, 'ResNet', 'ResNet')
|
|
28
|
+
self.model: Optional[ResNetSklearn] = None
|
|
29
|
+
self.enable_distributed_optuna = bool(context.config.use_resn_ddp)
|
|
30
|
+
|
|
31
|
+
def _maybe_cleanup_gpu(self, model: Optional[ResNetSklearn]) -> None:
|
|
32
|
+
if not bool(getattr(self.ctx.config, "resn_cleanup_per_fold", False)):
|
|
33
|
+
return
|
|
34
|
+
if model is not None:
|
|
35
|
+
getattr(getattr(model, "resnet", None), "to",
|
|
36
|
+
lambda *_args, **_kwargs: None)("cpu")
|
|
37
|
+
synchronize = bool(getattr(self.ctx.config, "resn_cleanup_synchronize", False))
|
|
38
|
+
self._clean_gpu(synchronize=synchronize)
|
|
23
39
|
|
|
24
40
|
def _resolve_input_dim(self) -> int:
|
|
25
41
|
data = getattr(self.ctx, "train_oht_scl_data", None)
|
|
@@ -174,13 +190,11 @@ class ResNetTrainer(TrainerBase):
|
|
|
174
190
|
w_all.iloc[val_idx],
|
|
175
191
|
trial=None,
|
|
176
192
|
)
|
|
177
|
-
refit_epochs = self._resolve_best_epoch(
|
|
178
|
-
getattr(tmp_model, "training_history", None),
|
|
179
|
-
default_epochs=int(self.ctx.epochs),
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
lambda *_args, **_kwargs: None)("cpu")
|
|
183
|
-
self._clean_gpu()
|
|
193
|
+
refit_epochs = self._resolve_best_epoch(
|
|
194
|
+
getattr(tmp_model, "training_history", None),
|
|
195
|
+
default_epochs=int(self.ctx.epochs),
|
|
196
|
+
)
|
|
197
|
+
self._maybe_cleanup_gpu(tmp_model)
|
|
184
198
|
|
|
185
199
|
self.model = self._build_model(params)
|
|
186
200
|
if refit_epochs is not None:
|
|
@@ -219,7 +233,7 @@ class ResNetTrainer(TrainerBase):
|
|
|
219
233
|
n_samples = len(X_all)
|
|
220
234
|
split_iter, _ = self._resolve_ensemble_splits(X_all, k=k)
|
|
221
235
|
if split_iter is None:
|
|
222
|
-
|
|
236
|
+
_log(
|
|
223
237
|
f"[ResNet Ensemble] unable to build CV split (n_samples={n_samples}); skip ensemble.",
|
|
224
238
|
flush=True,
|
|
225
239
|
)
|
|
@@ -241,15 +255,13 @@ class ResNetTrainer(TrainerBase):
|
|
|
241
255
|
)
|
|
242
256
|
pred_train = model.predict(X_all)
|
|
243
257
|
pred_test = model.predict(X_test)
|
|
244
|
-
preds_train_sum += np.asarray(pred_train, dtype=np.float64)
|
|
245
|
-
preds_test_sum += np.asarray(pred_test, dtype=np.float64)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
self._clean_gpu()
|
|
249
|
-
split_count += 1
|
|
258
|
+
preds_train_sum += np.asarray(pred_train, dtype=np.float64)
|
|
259
|
+
preds_test_sum += np.asarray(pred_test, dtype=np.float64)
|
|
260
|
+
self._maybe_cleanup_gpu(model)
|
|
261
|
+
split_count += 1
|
|
250
262
|
|
|
251
263
|
if split_count < 1:
|
|
252
|
-
|
|
264
|
+
_log(
|
|
253
265
|
f"[ResNet Ensemble] no CV splits generated; skip ensemble.",
|
|
254
266
|
flush=True,
|
|
255
267
|
)
|
|
@@ -280,4 +292,4 @@ class ResNetTrainer(TrainerBase):
|
|
|
280
292
|
self.model = resn_loaded
|
|
281
293
|
self.ctx.resn_best = self.model
|
|
282
294
|
else:
|
|
283
|
-
|
|
295
|
+
_log(f"[ResNetTrainer.load] Model file not found: {path}")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
|
-
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import optuna
|
|
@@ -10,14 +11,190 @@ import xgboost as xgb
|
|
|
10
11
|
from sklearn.metrics import log_loss
|
|
11
12
|
|
|
12
13
|
from ins_pricing.modelling.bayesopt.trainers.trainer_base import TrainerBase
|
|
13
|
-
from ins_pricing.utils import EPS
|
|
14
|
+
from ins_pricing.utils import EPS, get_logger, log_print
|
|
14
15
|
from ins_pricing.utils.losses import regression_loss
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
_XGB_HAS_CUDA = False
|
|
17
|
+
_logger = get_logger("ins_pricing.trainer.xgb")
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
def _log(*args, **kwargs) -> None:
|
|
21
|
+
log_print(_logger, *args, **kwargs)
|
|
22
|
+
|
|
23
|
+
_XGB_CUDA_CHECKED = False
|
|
24
|
+
_XGB_HAS_CUDA = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_oom_error(exc: Exception) -> bool:
|
|
28
|
+
msg = str(exc).lower()
|
|
29
|
+
return "out of memory" in msg or ("cuda" in msg and "memory" in msg)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _XGBDMatrixWrapper:
|
|
33
|
+
"""Sklearn-like wrapper that uses xgb.train + (Quantile)DMatrix internally."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
params: Dict[str, Any],
|
|
38
|
+
*,
|
|
39
|
+
task_type: str,
|
|
40
|
+
use_gpu: bool,
|
|
41
|
+
allow_cpu_fallback: bool = True,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.params = dict(params)
|
|
44
|
+
self.task_type = task_type
|
|
45
|
+
self.use_gpu = bool(use_gpu)
|
|
46
|
+
self.allow_cpu_fallback = allow_cpu_fallback
|
|
47
|
+
self._booster: Optional[xgb.Booster] = None
|
|
48
|
+
self.best_iteration: Optional[int] = None
|
|
49
|
+
|
|
50
|
+
def set_params(self, **params: Any) -> "_XGBDMatrixWrapper":
|
|
51
|
+
self.params.update(params)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
55
|
+
_ = deep
|
|
56
|
+
return dict(self.params)
|
|
57
|
+
|
|
58
|
+
def _select_dmatrix_class(self) -> Any:
|
|
59
|
+
if self.use_gpu and hasattr(xgb, "DeviceQuantileDMatrix"):
|
|
60
|
+
return xgb.DeviceQuantileDMatrix
|
|
61
|
+
if hasattr(xgb, "QuantileDMatrix"):
|
|
62
|
+
return xgb.QuantileDMatrix
|
|
63
|
+
return xgb.DMatrix
|
|
64
|
+
|
|
65
|
+
def _build_dmatrix(self, X, y=None, weight=None) -> xgb.DMatrix:
|
|
66
|
+
if isinstance(X, (str, os.PathLike)):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"External-memory DMatrix is disabled; pass in-memory data instead."
|
|
69
|
+
)
|
|
70
|
+
if isinstance(X, xgb.DMatrix):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"DMatrix inputs are disabled; pass raw in-memory data instead."
|
|
73
|
+
)
|
|
74
|
+
dmatrix_cls = self._select_dmatrix_class()
|
|
75
|
+
kwargs: Dict[str, Any] = {}
|
|
76
|
+
if y is not None:
|
|
77
|
+
kwargs["label"] = y
|
|
78
|
+
if weight is not None:
|
|
79
|
+
kwargs["weight"] = weight
|
|
80
|
+
if bool(self.params.get("enable_categorical", False)):
|
|
81
|
+
kwargs["enable_categorical"] = True
|
|
82
|
+
try:
|
|
83
|
+
return dmatrix_cls(X, **kwargs)
|
|
84
|
+
except TypeError:
|
|
85
|
+
kwargs.pop("enable_categorical", None)
|
|
86
|
+
return dmatrix_cls(X, **kwargs)
|
|
87
|
+
except Exception:
|
|
88
|
+
if dmatrix_cls is not xgb.DMatrix:
|
|
89
|
+
return xgb.DMatrix(X, **kwargs)
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def _resolve_train_params(self) -> Dict[str, Any]:
|
|
93
|
+
params = dict(self.params)
|
|
94
|
+
if not self.use_gpu:
|
|
95
|
+
params["tree_method"] = "hist"
|
|
96
|
+
params["predictor"] = "cpu_predictor"
|
|
97
|
+
params.pop("gpu_id", None)
|
|
98
|
+
return params
|
|
99
|
+
|
|
100
|
+
def _train_booster(
|
|
101
|
+
self,
|
|
102
|
+
X,
|
|
103
|
+
y,
|
|
104
|
+
*,
|
|
105
|
+
sample_weight=None,
|
|
106
|
+
eval_set=None,
|
|
107
|
+
sample_weight_eval_set=None,
|
|
108
|
+
early_stopping_rounds: Optional[int] = None,
|
|
109
|
+
verbose: bool = False,
|
|
110
|
+
) -> None:
|
|
111
|
+
params = self._resolve_train_params()
|
|
112
|
+
num_boost_round = int(params.pop("n_estimators", 100))
|
|
113
|
+
dtrain = self._build_dmatrix(X, y, sample_weight)
|
|
114
|
+
evals = []
|
|
115
|
+
if eval_set:
|
|
116
|
+
weights = sample_weight_eval_set or []
|
|
117
|
+
for idx, (X_val, y_val) in enumerate(eval_set):
|
|
118
|
+
w_val = weights[idx] if idx < len(weights) else None
|
|
119
|
+
dval = self._build_dmatrix(X_val, y_val, w_val)
|
|
120
|
+
evals.append((dval, f"val{idx}"))
|
|
121
|
+
self._booster = xgb.train(
|
|
122
|
+
params,
|
|
123
|
+
dtrain,
|
|
124
|
+
num_boost_round=num_boost_round,
|
|
125
|
+
evals=evals,
|
|
126
|
+
early_stopping_rounds=early_stopping_rounds,
|
|
127
|
+
verbose_eval=verbose,
|
|
128
|
+
)
|
|
129
|
+
self.best_iteration = getattr(self._booster, "best_iteration", None)
|
|
130
|
+
|
|
131
|
+
def fit(self, X, y, **fit_kwargs) -> "_XGBDMatrixWrapper":
|
|
132
|
+
sample_weight = fit_kwargs.pop("sample_weight", None)
|
|
133
|
+
eval_set = fit_kwargs.pop("eval_set", None)
|
|
134
|
+
sample_weight_eval_set = fit_kwargs.pop("sample_weight_eval_set", None)
|
|
135
|
+
early_stopping_rounds = fit_kwargs.pop("early_stopping_rounds", None)
|
|
136
|
+
verbose = bool(fit_kwargs.pop("verbose", False))
|
|
137
|
+
fit_kwargs.pop("eval_metric", None)
|
|
138
|
+
try:
|
|
139
|
+
self._train_booster(
|
|
140
|
+
X,
|
|
141
|
+
y,
|
|
142
|
+
sample_weight=sample_weight,
|
|
143
|
+
eval_set=eval_set,
|
|
144
|
+
sample_weight_eval_set=sample_weight_eval_set,
|
|
145
|
+
early_stopping_rounds=early_stopping_rounds,
|
|
146
|
+
verbose=verbose,
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
if self.use_gpu and self.allow_cpu_fallback and _is_oom_error(exc):
|
|
150
|
+
_log("[XGBoost] GPU OOM detected; retrying with CPU.", flush=True)
|
|
151
|
+
self.use_gpu = False
|
|
152
|
+
self._train_booster(
|
|
153
|
+
X,
|
|
154
|
+
y,
|
|
155
|
+
sample_weight=sample_weight,
|
|
156
|
+
eval_set=eval_set,
|
|
157
|
+
sample_weight_eval_set=sample_weight_eval_set,
|
|
158
|
+
early_stopping_rounds=early_stopping_rounds,
|
|
159
|
+
verbose=verbose,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
raise
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def _resolve_iteration_range(self) -> Optional[Tuple[int, int]]:
|
|
166
|
+
if self.best_iteration is None:
|
|
167
|
+
return None
|
|
168
|
+
return (0, int(self.best_iteration) + 1)
|
|
169
|
+
|
|
170
|
+
def _predict_raw(self, X) -> np.ndarray:
|
|
171
|
+
if self._booster is None:
|
|
172
|
+
raise RuntimeError("Booster not trained.")
|
|
173
|
+
dtest = self._build_dmatrix(X)
|
|
174
|
+
iteration_range = self._resolve_iteration_range()
|
|
175
|
+
if iteration_range is None:
|
|
176
|
+
return self._booster.predict(dtest)
|
|
177
|
+
try:
|
|
178
|
+
return self._booster.predict(dtest, iteration_range=iteration_range)
|
|
179
|
+
except TypeError:
|
|
180
|
+
return self._booster.predict(dtest, ntree_limit=iteration_range[1])
|
|
181
|
+
|
|
182
|
+
def predict(self, X, **_kwargs) -> np.ndarray:
|
|
183
|
+
pred = self._predict_raw(X)
|
|
184
|
+
if self.task_type == "classification":
|
|
185
|
+
if pred.ndim == 1:
|
|
186
|
+
return (pred > 0.5).astype(int)
|
|
187
|
+
return np.argmax(pred, axis=1)
|
|
188
|
+
return pred
|
|
189
|
+
|
|
190
|
+
def predict_proba(self, X, **_kwargs) -> np.ndarray:
|
|
191
|
+
pred = self._predict_raw(X)
|
|
192
|
+
if pred.ndim == 1:
|
|
193
|
+
return np.column_stack([1 - pred, pred])
|
|
194
|
+
return pred
|
|
195
|
+
|
|
196
|
+
def get_booster(self) -> Optional[xgb.Booster]:
|
|
197
|
+
return self._booster
|
|
21
198
|
|
|
22
199
|
|
|
23
200
|
def _xgb_cuda_available() -> bool:
|
|
@@ -54,39 +231,65 @@ def _xgb_cuda_available() -> bool:
|
|
|
54
231
|
_XGB_HAS_CUDA = False
|
|
55
232
|
return False
|
|
56
233
|
|
|
57
|
-
class XGBTrainer(TrainerBase):
|
|
234
|
+
class XGBTrainer(TrainerBase):
|
|
58
235
|
def __init__(self, context: "BayesOptModel") -> None:
|
|
59
236
|
super().__init__(context, 'Xgboost', 'Xgboost')
|
|
60
237
|
self.model: Optional[xgb.XGBModel] = None
|
|
61
238
|
self._xgb_use_gpu = False
|
|
62
239
|
self._xgb_gpu_warned = False
|
|
63
240
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
241
|
+
def _build_sklearn_estimator(self, params: Dict[str, Any]) -> xgb.XGBModel:
|
|
242
|
+
if self.ctx.task_type == 'classification':
|
|
243
|
+
return xgb.XGBClassifier(**params)
|
|
244
|
+
return xgb.XGBRegressor(**params)
|
|
245
|
+
|
|
246
|
+
def _build_estimator(self) -> xgb.XGBModel:
|
|
247
|
+
use_gpu = bool(self.ctx.use_gpu and _xgb_cuda_available())
|
|
248
|
+
self._xgb_use_gpu = use_gpu
|
|
249
|
+
params = dict(
|
|
250
|
+
objective=self.ctx.obj,
|
|
251
|
+
random_state=self.ctx.rand_seed,
|
|
252
|
+
subsample=0.9,
|
|
253
|
+
tree_method='gpu_hist' if use_gpu else 'hist',
|
|
254
|
+
enable_categorical=True,
|
|
255
|
+
predictor='gpu_predictor' if use_gpu else 'cpu_predictor'
|
|
256
|
+
)
|
|
75
257
|
if self.ctx.use_gpu and not use_gpu and not self._xgb_gpu_warned:
|
|
76
|
-
|
|
258
|
+
_log(
|
|
77
259
|
"[XGBoost] CUDA requested but not available; falling back to CPU.",
|
|
78
260
|
flush=True,
|
|
79
261
|
)
|
|
80
262
|
self._xgb_gpu_warned = True
|
|
81
|
-
if use_gpu:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
263
|
+
if use_gpu:
|
|
264
|
+
gpu_id = self._resolve_gpu_id()
|
|
265
|
+
params['gpu_id'] = gpu_id
|
|
266
|
+
_log(f">>> XGBoost using GPU ID: {gpu_id}")
|
|
267
|
+
eval_metric = self._resolve_eval_metric()
|
|
268
|
+
if eval_metric is not None:
|
|
269
|
+
params.setdefault("eval_metric", eval_metric)
|
|
270
|
+
use_dmatrix = bool(getattr(self.config, "xgb_use_dmatrix", True))
|
|
271
|
+
if use_dmatrix:
|
|
272
|
+
return _XGBDMatrixWrapper(
|
|
273
|
+
params,
|
|
274
|
+
task_type=self.ctx.task_type,
|
|
275
|
+
use_gpu=use_gpu,
|
|
276
|
+
)
|
|
277
|
+
return self._build_sklearn_estimator(params)
|
|
278
|
+
|
|
279
|
+
def _resolve_gpu_id(self) -> int:
|
|
280
|
+
gpu_id = getattr(self.config, "xgb_gpu_id", None)
|
|
281
|
+
if gpu_id is None:
|
|
282
|
+
return 0
|
|
283
|
+
try:
|
|
284
|
+
return int(gpu_id)
|
|
285
|
+
except (TypeError, ValueError):
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
def _maybe_cleanup_gpu(self) -> None:
|
|
289
|
+
if not bool(getattr(self.config, "xgb_cleanup_per_fold", False)):
|
|
290
|
+
return
|
|
291
|
+
synchronize = bool(getattr(self.config, "xgb_cleanup_synchronize", False))
|
|
292
|
+
self._clean_gpu(synchronize=synchronize)
|
|
90
293
|
|
|
91
294
|
def _resolve_eval_metric(self) -> Optional[Any]:
|
|
92
295
|
fit_params = self.ctx.fit_params or {}
|
|
@@ -148,7 +351,7 @@ class XGBTrainer(TrainerBase):
|
|
|
148
351
|
n_samples = len(X_all)
|
|
149
352
|
split_iter, _ = self._resolve_ensemble_splits(X_all, k=k)
|
|
150
353
|
if split_iter is None:
|
|
151
|
-
|
|
354
|
+
_log(
|
|
152
355
|
f"[XGB Ensemble] unable to build CV split (n_samples={n_samples}); skip ensemble.",
|
|
153
356
|
flush=True,
|
|
154
357
|
)
|
|
@@ -184,11 +387,11 @@ class XGBTrainer(TrainerBase):
|
|
|
184
387
|
pred_test = clf.predict(X_test)
|
|
185
388
|
preds_train_sum += np.asarray(pred_train, dtype=np.float64)
|
|
186
389
|
preds_test_sum += np.asarray(pred_test, dtype=np.float64)
|
|
187
|
-
self.
|
|
188
|
-
split_count += 1
|
|
390
|
+
self._maybe_cleanup_gpu()
|
|
391
|
+
split_count += 1
|
|
189
392
|
|
|
190
393
|
if split_count < 1:
|
|
191
|
-
|
|
394
|
+
_log(
|
|
192
395
|
f"[XGB Ensemble] no CV splits generated; skip ensemble.",
|
|
193
396
|
flush=True,
|
|
194
397
|
)
|
|
@@ -213,7 +416,7 @@ class XGBTrainer(TrainerBase):
|
|
|
213
416
|
reg_alpha = trial.suggest_float('reg_alpha', 1e-10, 1, log=True)
|
|
214
417
|
reg_lambda = trial.suggest_float('reg_lambda', 1e-10, 1, log=True)
|
|
215
418
|
if trial is not None:
|
|
216
|
-
|
|
419
|
+
_log(
|
|
217
420
|
f"[Optuna][Xgboost] trial_id={trial.number} max_depth={max_depth} "
|
|
218
421
|
f"n_estimators={n_estimators}",
|
|
219
422
|
flush=True,
|
|
@@ -280,9 +483,9 @@ class XGBTrainer(TrainerBase):
|
|
|
280
483
|
tweedie_power=tweedie_variance_power,
|
|
281
484
|
)
|
|
282
485
|
losses.append(float(loss))
|
|
283
|
-
self.
|
|
284
|
-
|
|
285
|
-
return float(np.mean(losses))
|
|
486
|
+
self._maybe_cleanup_gpu()
|
|
487
|
+
|
|
488
|
+
return float(np.mean(losses))
|
|
286
489
|
|
|
287
490
|
def train(self) -> None:
|
|
288
491
|
if not self.best_params:
|