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.
Files changed (44) hide show
  1. ins_pricing/cli/BayesOpt_entry.py +15 -5
  2. ins_pricing/cli/BayesOpt_incremental.py +43 -10
  3. ins_pricing/cli/Explain_Run.py +16 -5
  4. ins_pricing/cli/Explain_entry.py +29 -8
  5. ins_pricing/cli/Pricing_Run.py +16 -5
  6. ins_pricing/cli/bayesopt_entry_runner.py +45 -12
  7. ins_pricing/cli/utils/bootstrap.py +23 -0
  8. ins_pricing/cli/utils/cli_config.py +34 -15
  9. ins_pricing/cli/utils/import_resolver.py +14 -14
  10. ins_pricing/cli/utils/notebook_utils.py +120 -106
  11. ins_pricing/cli/watchdog_run.py +15 -5
  12. ins_pricing/frontend/app.py +132 -61
  13. ins_pricing/frontend/config_builder.py +33 -0
  14. ins_pricing/frontend/example_config.json +11 -0
  15. ins_pricing/frontend/runner.py +340 -388
  16. ins_pricing/modelling/README.md +1 -1
  17. ins_pricing/modelling/__init__.py +10 -10
  18. ins_pricing/modelling/bayesopt/README.md +29 -11
  19. ins_pricing/modelling/bayesopt/config_components.py +12 -0
  20. ins_pricing/modelling/bayesopt/config_preprocess.py +50 -13
  21. ins_pricing/modelling/bayesopt/core.py +47 -19
  22. ins_pricing/modelling/bayesopt/model_plotting_mixin.py +20 -14
  23. ins_pricing/modelling/bayesopt/models/model_ft_components.py +349 -342
  24. ins_pricing/modelling/bayesopt/models/model_ft_trainer.py +11 -5
  25. ins_pricing/modelling/bayesopt/models/model_gnn.py +20 -14
  26. ins_pricing/modelling/bayesopt/models/model_resn.py +9 -3
  27. ins_pricing/modelling/bayesopt/trainers/trainer_base.py +62 -50
  28. ins_pricing/modelling/bayesopt/trainers/trainer_ft.py +61 -53
  29. ins_pricing/modelling/bayesopt/trainers/trainer_glm.py +9 -3
  30. ins_pricing/modelling/bayesopt/trainers/trainer_gnn.py +40 -32
  31. ins_pricing/modelling/bayesopt/trainers/trainer_resn.py +36 -24
  32. ins_pricing/modelling/bayesopt/trainers/trainer_xgb.py +240 -37
  33. ins_pricing/modelling/bayesopt/utils/distributed_utils.py +193 -186
  34. ins_pricing/modelling/bayesopt/utils/torch_trainer_mixin.py +23 -10
  35. ins_pricing/pricing/factors.py +67 -56
  36. ins_pricing/setup.py +1 -1
  37. ins_pricing/utils/__init__.py +7 -6
  38. ins_pricing/utils/device.py +45 -24
  39. ins_pricing/utils/logging.py +34 -1
  40. ins_pricing/utils/profiling.py +8 -4
  41. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/METADATA +182 -182
  42. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/RECORD +44 -43
  43. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.3.dist-info}/WHEEL +0 -0
  44. {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
- print(
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
- print(
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
- class GNNTrainer(TrainerBase):
21
- def __init__(self, context: "BayesOptModel") -> None:
22
- super().__init__(context, 'GNN', 'GNN')
23
- self.model: Optional[GraphNeuralNetSklearn] = None
24
- self.enable_distributed_optuna = bool(context.config.use_gnn_ddp)
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
- getattr(getattr(tmp_model, "gnn", None), "to",
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
- print(
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
- getattr(getattr(model, "gnn", None), "to",
270
- lambda *_args, **_kwargs: None)("cpu")
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
- print(
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
- print(f"[GeoToken][GNNTrainer] Generated {len(cols)} dims and injected into FT.", flush=True)
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
- print(f"[save] Warning: No model to save for {self.label}")
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
- print(f"[load] Warning: Model file not found: {path}")
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
- print(f"[GNN load] Warning: State dict mismatch, loading with strict=False: {e}")
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
- class ResNetTrainer(TrainerBase):
16
- def __init__(self, context: "BayesOptModel") -> None:
17
- if context.task_type == 'classification':
18
- super().__init__(context, 'ResNetClassifier', 'ResNet')
19
- else:
20
- super().__init__(context, 'ResNet', 'ResNet')
21
- self.model: Optional[ResNetSklearn] = None
22
- self.enable_distributed_optuna = bool(context.config.use_resn_ddp)
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
- getattr(getattr(tmp_model, "resnet", None), "to",
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
- print(
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
- getattr(getattr(model, "resnet", None), "to",
247
- lambda *_args, **_kwargs: None)("cpu")
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
- print(
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
- print(f"[ResNetTrainer.load] Model file not found: {path}")
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
- from typing import Any, Dict, List, Optional, Tuple
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
- _XGB_CUDA_CHECKED = False
17
- _XGB_HAS_CUDA = False
17
+ _logger = get_logger("ins_pricing.trainer.xgb")
18
18
 
19
- _XGB_CUDA_CHECKED = False
20
- _XGB_HAS_CUDA = False
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 _build_estimator(self) -> xgb.XGBModel:
65
- use_gpu = bool(self.ctx.use_gpu and _xgb_cuda_available())
66
- self._xgb_use_gpu = use_gpu
67
- params = dict(
68
- objective=self.ctx.obj,
69
- random_state=self.ctx.rand_seed,
70
- subsample=0.9,
71
- tree_method='gpu_hist' if use_gpu else 'hist',
72
- enable_categorical=True,
73
- predictor='gpu_predictor' if use_gpu else 'cpu_predictor'
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
- print(
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
- params['gpu_id'] = 0
83
- print(f">>> XGBoost using GPU ID: 0 (Single GPU Mode)")
84
- eval_metric = self._resolve_eval_metric()
85
- if eval_metric is not None:
86
- params.setdefault("eval_metric", eval_metric)
87
- if self.ctx.task_type == 'classification':
88
- return xgb.XGBClassifier(**params)
89
- return xgb.XGBRegressor(**params)
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
- print(
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._clean_gpu()
188
- split_count += 1
390
+ self._maybe_cleanup_gpu()
391
+ split_count += 1
189
392
 
190
393
  if split_count < 1:
191
- print(
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
- print(
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._clean_gpu()
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: