ins-pricing 0.3.2__py3-none-any.whl → 0.3.4__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 +32 -0
- ins_pricing/cli/utils/import_resolver.py +29 -3
- ins_pricing/cli/utils/notebook_utils.py +3 -2
- ins_pricing/docs/modelling/BayesOpt_USAGE.md +3 -3
- ins_pricing/modelling/core/bayesopt/__init__.py +4 -0
- ins_pricing/modelling/core/bayesopt/config_preprocess.py +12 -0
- ins_pricing/modelling/core/bayesopt/core.py +21 -8
- ins_pricing/modelling/core/bayesopt/models/model_ft_components.py +38 -12
- ins_pricing/modelling/core/bayesopt/models/model_ft_trainer.py +16 -6
- ins_pricing/modelling/core/bayesopt/models/model_gnn.py +16 -6
- ins_pricing/modelling/core/bayesopt/models/model_resn.py +16 -7
- ins_pricing/modelling/core/bayesopt/trainers/trainer_base.py +2 -0
- ins_pricing/modelling/core/bayesopt/trainers/trainer_ft.py +25 -8
- ins_pricing/modelling/core/bayesopt/trainers/trainer_glm.py +14 -11
- ins_pricing/modelling/core/bayesopt/trainers/trainer_gnn.py +29 -10
- ins_pricing/modelling/core/bayesopt/trainers/trainer_resn.py +28 -12
- ins_pricing/modelling/core/bayesopt/trainers/trainer_xgb.py +13 -14
- ins_pricing/modelling/core/bayesopt/utils/losses.py +129 -0
- ins_pricing/modelling/core/bayesopt/utils/metrics_and_devices.py +18 -3
- ins_pricing/modelling/core/bayesopt/utils/torch_trainer_mixin.py +24 -3
- ins_pricing/production/predict.py +38 -9
- ins_pricing/setup.py +1 -1
- ins_pricing/utils/metrics.py +27 -3
- ins_pricing/utils/torch_compat.py +40 -0
- {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/METADATA +162 -162
- {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/RECORD +28 -27
- {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/WHEEL +0 -0
- {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/top_level.txt +0 -0
|
@@ -6,10 +6,11 @@ import numpy as np
|
|
|
6
6
|
import optuna
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import statsmodels.api as sm
|
|
9
|
-
from sklearn.metrics import log_loss
|
|
9
|
+
from sklearn.metrics import log_loss
|
|
10
10
|
|
|
11
11
|
from .trainer_base import TrainerBase
|
|
12
12
|
from ..utils import EPS
|
|
13
|
+
from ..utils.losses import regression_loss
|
|
13
14
|
|
|
14
15
|
class GLMTrainer(TrainerBase):
|
|
15
16
|
def __init__(self, context: "BayesOptModel") -> None:
|
|
@@ -19,10 +20,13 @@ class GLMTrainer(TrainerBase):
|
|
|
19
20
|
def _select_family(self, tweedie_power: Optional[float] = None):
|
|
20
21
|
if self.ctx.task_type == 'classification':
|
|
21
22
|
return sm.families.Binomial()
|
|
22
|
-
|
|
23
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
24
|
+
if loss_name == "poisson":
|
|
23
25
|
return sm.families.Poisson()
|
|
24
|
-
if
|
|
26
|
+
if loss_name == "gamma":
|
|
25
27
|
return sm.families.Gamma()
|
|
28
|
+
if loss_name in {"mse", "mae"}:
|
|
29
|
+
return sm.families.Gaussian()
|
|
26
30
|
power = tweedie_power if tweedie_power is not None else 1.5
|
|
27
31
|
return sm.families.Tweedie(var_power=power, link=sm.families.links.log())
|
|
28
32
|
|
|
@@ -45,7 +49,8 @@ class GLMTrainer(TrainerBase):
|
|
|
45
49
|
"alpha": lambda t: t.suggest_float('alpha', 1e-6, 1e2, log=True),
|
|
46
50
|
"l1_ratio": lambda t: t.suggest_float('l1_ratio', 0.0, 1.0)
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
53
|
+
if self.ctx.task_type == 'regression' and loss_name == 'tweedie':
|
|
49
54
|
param_space["tweedie_power"] = lambda t: t.suggest_float(
|
|
50
55
|
'tweedie_power', 1.0, 2.0)
|
|
51
56
|
|
|
@@ -87,13 +92,12 @@ class GLMTrainer(TrainerBase):
|
|
|
87
92
|
if self.ctx.task_type == 'classification':
|
|
88
93
|
y_pred_clipped = np.clip(y_pred, EPS, 1 - EPS)
|
|
89
94
|
return log_loss(y_true, y_pred_clipped, sample_weight=weight)
|
|
90
|
-
|
|
91
|
-
return mean_tweedie_deviance(
|
|
95
|
+
return regression_loss(
|
|
92
96
|
y_true,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
y_pred,
|
|
98
|
+
weight,
|
|
99
|
+
loss_name=loss_name,
|
|
100
|
+
tweedie_power=metric_ctx.get("tweedie_power"),
|
|
97
101
|
)
|
|
98
102
|
|
|
99
103
|
return self.cross_val_generic(
|
|
@@ -192,4 +196,3 @@ class GLMTrainer(TrainerBase):
|
|
|
192
196
|
preds_test = preds_test_sum / float(split_count)
|
|
193
197
|
self._cache_predictions("glm", preds_train, preds_test)
|
|
194
198
|
|
|
195
|
-
|
|
@@ -6,11 +6,12 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import optuna
|
|
8
8
|
import torch
|
|
9
|
-
from sklearn.metrics import log_loss
|
|
9
|
+
from sklearn.metrics import log_loss
|
|
10
10
|
|
|
11
11
|
from .trainer_base import TrainerBase
|
|
12
12
|
from ..models import GraphNeuralNetSklearn
|
|
13
13
|
from ..utils import EPS
|
|
14
|
+
from ..utils.losses import regression_loss
|
|
14
15
|
from ins_pricing.utils import get_logger
|
|
15
16
|
from ins_pricing.utils.torch_compat import torch_load
|
|
16
17
|
|
|
@@ -25,6 +26,15 @@ class GNNTrainer(TrainerBase):
|
|
|
25
26
|
def _build_model(self, params: Optional[Dict[str, Any]] = None) -> GraphNeuralNetSklearn:
|
|
26
27
|
params = params or {}
|
|
27
28
|
base_tw_power = self.ctx.default_tweedie_power()
|
|
29
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
30
|
+
tw_power = params.get("tw_power")
|
|
31
|
+
if self.ctx.task_type == "regression":
|
|
32
|
+
if loss_name == "tweedie":
|
|
33
|
+
tw_power = base_tw_power if tw_power is None else float(tw_power)
|
|
34
|
+
elif loss_name in ("poisson", "gamma"):
|
|
35
|
+
tw_power = base_tw_power
|
|
36
|
+
else:
|
|
37
|
+
tw_power = None
|
|
28
38
|
model = GraphNeuralNetSklearn(
|
|
29
39
|
model_nme=f"{self.ctx.model_nme}_gnn",
|
|
30
40
|
input_dim=len(self.ctx.var_nmes),
|
|
@@ -36,7 +46,7 @@ class GNNTrainer(TrainerBase):
|
|
|
36
46
|
epochs=int(params.get("epochs", self.ctx.epochs)),
|
|
37
47
|
patience=int(params.get("patience", 5)),
|
|
38
48
|
task_type=self.ctx.task_type,
|
|
39
|
-
tweedie_power=
|
|
49
|
+
tweedie_power=tw_power,
|
|
40
50
|
weight_decay=float(params.get("weight_decay", 0.0)),
|
|
41
51
|
use_data_parallel=bool(self.ctx.config.use_gnn_data_parallel),
|
|
42
52
|
use_ddp=bool(self.ctx.config.use_gnn_ddp),
|
|
@@ -47,11 +57,13 @@ class GNNTrainer(TrainerBase):
|
|
|
47
57
|
knn_gpu_mem_ratio=float(self.ctx.config.gnn_knn_gpu_mem_ratio),
|
|
48
58
|
knn_gpu_mem_overhead=float(
|
|
49
59
|
self.ctx.config.gnn_knn_gpu_mem_overhead),
|
|
60
|
+
loss_name=loss_name,
|
|
50
61
|
)
|
|
51
62
|
return model
|
|
52
63
|
|
|
53
64
|
def cross_val(self, trial: optuna.trial.Trial) -> float:
|
|
54
65
|
base_tw_power = self.ctx.default_tweedie_power()
|
|
66
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
55
67
|
metric_ctx: Dict[str, Any] = {}
|
|
56
68
|
|
|
57
69
|
def data_provider():
|
|
@@ -60,8 +72,16 @@ class GNNTrainer(TrainerBase):
|
|
|
60
72
|
return data[self.ctx.var_nmes], data[self.ctx.resp_nme], data[self.ctx.weight_nme]
|
|
61
73
|
|
|
62
74
|
def model_builder(params: Dict[str, Any]):
|
|
63
|
-
|
|
75
|
+
if loss_name == "tweedie":
|
|
76
|
+
tw_power = params.get("tw_power", base_tw_power)
|
|
77
|
+
elif loss_name in ("poisson", "gamma"):
|
|
78
|
+
tw_power = base_tw_power
|
|
79
|
+
else:
|
|
80
|
+
tw_power = None
|
|
64
81
|
metric_ctx["tw_power"] = tw_power
|
|
82
|
+
if tw_power is None:
|
|
83
|
+
params = dict(params)
|
|
84
|
+
params.pop("tw_power", None)
|
|
65
85
|
return self._build_model(params)
|
|
66
86
|
|
|
67
87
|
def preprocess_fn(X_train, X_val):
|
|
@@ -85,13 +105,12 @@ class GNNTrainer(TrainerBase):
|
|
|
85
105
|
if self.ctx.task_type == 'classification':
|
|
86
106
|
y_pred_clipped = np.clip(y_pred, EPS, 1 - EPS)
|
|
87
107
|
return log_loss(y_true, y_pred_clipped, sample_weight=weight)
|
|
88
|
-
|
|
89
|
-
power = metric_ctx.get("tw_power", base_tw_power or 1.5)
|
|
90
|
-
return mean_tweedie_deviance(
|
|
108
|
+
return regression_loss(
|
|
91
109
|
y_true,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
y_pred,
|
|
111
|
+
weight,
|
|
112
|
+
loss_name=loss_name,
|
|
113
|
+
tweedie_power=metric_ctx.get("tw_power", base_tw_power),
|
|
95
114
|
)
|
|
96
115
|
|
|
97
116
|
# Keep GNN BO lightweight: sample during CV, use full data for final training.
|
|
@@ -106,7 +125,7 @@ class GNNTrainer(TrainerBase):
|
|
|
106
125
|
"dropout": lambda t: t.suggest_float('dropout', 0.0, 0.3),
|
|
107
126
|
"weight_decay": lambda t: t.suggest_float('weight_decay', 1e-6, 1e-2, log=True),
|
|
108
127
|
}
|
|
109
|
-
if self.ctx.task_type == 'regression' and
|
|
128
|
+
if self.ctx.task_type == 'regression' and loss_name == 'tweedie':
|
|
110
129
|
param_space["tw_power"] = lambda t: t.suggest_float(
|
|
111
130
|
'tw_power', 1.0, 2.0)
|
|
112
131
|
|
|
@@ -6,10 +6,11 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import optuna
|
|
8
8
|
import torch
|
|
9
|
-
from sklearn.metrics import log_loss
|
|
9
|
+
from sklearn.metrics import log_loss
|
|
10
10
|
|
|
11
11
|
from .trainer_base import TrainerBase
|
|
12
12
|
from ..models import ResNetSklearn
|
|
13
|
+
from ..utils.losses import regression_loss
|
|
13
14
|
|
|
14
15
|
class ResNetTrainer(TrainerBase):
|
|
15
16
|
def __init__(self, context: "BayesOptModel") -> None:
|
|
@@ -28,9 +29,16 @@ class ResNetTrainer(TrainerBase):
|
|
|
28
29
|
|
|
29
30
|
def _build_model(self, params: Optional[Dict[str, Any]] = None) -> ResNetSklearn:
|
|
30
31
|
params = params or {}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
33
|
+
power = params.get("tw_power")
|
|
34
|
+
if self.ctx.task_type == "regression":
|
|
35
|
+
base_tw = self.ctx.default_tweedie_power()
|
|
36
|
+
if loss_name == "tweedie":
|
|
37
|
+
power = base_tw if power is None else float(power)
|
|
38
|
+
elif loss_name in ("poisson", "gamma"):
|
|
39
|
+
power = base_tw
|
|
40
|
+
else:
|
|
41
|
+
power = None
|
|
34
42
|
resn_weight_decay = float(
|
|
35
43
|
params.get(
|
|
36
44
|
"weight_decay",
|
|
@@ -53,7 +61,8 @@ class ResNetTrainer(TrainerBase):
|
|
|
53
61
|
stochastic_depth=float(params.get("stochastic_depth", 0.0)),
|
|
54
62
|
weight_decay=resn_weight_decay,
|
|
55
63
|
use_data_parallel=self.ctx.config.use_resn_data_parallel,
|
|
56
|
-
use_ddp=self.ctx.config.use_resn_ddp
|
|
64
|
+
use_ddp=self.ctx.config.use_resn_ddp,
|
|
65
|
+
loss_name=loss_name
|
|
57
66
|
)
|
|
58
67
|
|
|
59
68
|
# ========= Cross-validation (for BayesOpt) =========
|
|
@@ -64,6 +73,7 @@ class ResNetTrainer(TrainerBase):
|
|
|
64
73
|
# - Optionally sample part of training data during BayesOpt to reduce memory.
|
|
65
74
|
|
|
66
75
|
base_tw_power = self.ctx.default_tweedie_power()
|
|
76
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
67
77
|
|
|
68
78
|
def data_provider():
|
|
69
79
|
data = self.ctx.train_oht_data if self.ctx.train_oht_data is not None else self.ctx.train_oht_scl_data
|
|
@@ -73,10 +83,16 @@ class ResNetTrainer(TrainerBase):
|
|
|
73
83
|
metric_ctx: Dict[str, Any] = {}
|
|
74
84
|
|
|
75
85
|
def model_builder(params):
|
|
76
|
-
|
|
86
|
+
if loss_name == "tweedie":
|
|
87
|
+
power = params.get("tw_power", base_tw_power)
|
|
88
|
+
elif loss_name in ("poisson", "gamma"):
|
|
89
|
+
power = base_tw_power
|
|
90
|
+
else:
|
|
91
|
+
power = None
|
|
77
92
|
metric_ctx["tw_power"] = power
|
|
78
93
|
params_local = dict(params)
|
|
79
|
-
|
|
94
|
+
if power is not None:
|
|
95
|
+
params_local["tw_power"] = power
|
|
80
96
|
return self._build_model(params_local)
|
|
81
97
|
|
|
82
98
|
def preprocess_fn(X_train, X_val):
|
|
@@ -94,11 +110,12 @@ class ResNetTrainer(TrainerBase):
|
|
|
94
110
|
|
|
95
111
|
def metric_fn(y_true, y_pred, weight):
|
|
96
112
|
if self.ctx.task_type == 'regression':
|
|
97
|
-
return
|
|
113
|
+
return regression_loss(
|
|
98
114
|
y_true,
|
|
99
115
|
y_pred,
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
weight,
|
|
117
|
+
loss_name=loss_name,
|
|
118
|
+
tweedie_power=metric_ctx.get("tw_power", base_tw_power),
|
|
102
119
|
)
|
|
103
120
|
return log_loss(y_true, y_pred, sample_weight=weight)
|
|
104
121
|
|
|
@@ -115,7 +132,7 @@ class ResNetTrainer(TrainerBase):
|
|
|
115
132
|
"residual_scale": lambda t: t.suggest_float('residual_scale', 0.05, 0.3, step=0.05),
|
|
116
133
|
"patience": lambda t: t.suggest_int('patience', 3, 12),
|
|
117
134
|
"stochastic_depth": lambda t: t.suggest_float('stochastic_depth', 0.0, 0.2, step=0.05),
|
|
118
|
-
**({"tw_power": lambda t: t.suggest_float('tw_power', 1.0, 2.0)} if self.ctx.task_type == 'regression' and
|
|
135
|
+
**({"tw_power": lambda t: t.suggest_float('tw_power', 1.0, 2.0)} if self.ctx.task_type == 'regression' and loss_name == 'tweedie' else {})
|
|
119
136
|
},
|
|
120
137
|
data_provider=data_provider,
|
|
121
138
|
model_builder=model_builder,
|
|
@@ -263,4 +280,3 @@ class ResNetTrainer(TrainerBase):
|
|
|
263
280
|
self.ctx.resn_best = self.model
|
|
264
281
|
else:
|
|
265
282
|
print(f"[ResNetTrainer.load] Model file not found: {path}")
|
|
266
|
-
|
|
@@ -7,10 +7,11 @@ import numpy as np
|
|
|
7
7
|
import optuna
|
|
8
8
|
import torch
|
|
9
9
|
import xgboost as xgb
|
|
10
|
-
from sklearn.metrics import log_loss
|
|
10
|
+
from sklearn.metrics import log_loss
|
|
11
11
|
|
|
12
12
|
from .trainer_base import TrainerBase
|
|
13
13
|
from ..utils import EPS
|
|
14
|
+
from ..utils.losses import regression_loss
|
|
14
15
|
|
|
15
16
|
_XGB_CUDA_CHECKED = False
|
|
16
17
|
_XGB_HAS_CUDA = False
|
|
@@ -230,18 +231,17 @@ class XGBTrainer(TrainerBase):
|
|
|
230
231
|
'reg_alpha': reg_alpha,
|
|
231
232
|
'reg_lambda': reg_lambda
|
|
232
233
|
}
|
|
234
|
+
loss_name = getattr(self.ctx, "loss_name", "tweedie")
|
|
233
235
|
tweedie_variance_power = None
|
|
234
236
|
if self.ctx.task_type != 'classification':
|
|
235
|
-
if
|
|
237
|
+
if loss_name == "tweedie":
|
|
236
238
|
tweedie_variance_power = trial.suggest_float(
|
|
237
239
|
'tweedie_variance_power', 1, 2)
|
|
238
240
|
params['tweedie_variance_power'] = tweedie_variance_power
|
|
239
|
-
elif
|
|
240
|
-
tweedie_variance_power = 1
|
|
241
|
-
elif
|
|
242
|
-
tweedie_variance_power = 2
|
|
243
|
-
else:
|
|
244
|
-
tweedie_variance_power = 1.5
|
|
241
|
+
elif loss_name == "poisson":
|
|
242
|
+
tweedie_variance_power = 1.0
|
|
243
|
+
elif loss_name == "gamma":
|
|
244
|
+
tweedie_variance_power = 2.0
|
|
245
245
|
X_all = self.ctx.train_data[self.ctx.factor_nmes]
|
|
246
246
|
y_all = self.ctx.train_data[self.ctx.resp_nme].values
|
|
247
247
|
w_all = self.ctx.train_data[self.ctx.weight_nme].values
|
|
@@ -272,12 +272,12 @@ class XGBTrainer(TrainerBase):
|
|
|
272
272
|
loss = log_loss(y_val, y_pred, sample_weight=w_val)
|
|
273
273
|
else:
|
|
274
274
|
y_pred = clf.predict(X_val)
|
|
275
|
-
|
|
276
|
-
loss = mean_tweedie_deviance(
|
|
275
|
+
loss = regression_loss(
|
|
277
276
|
y_val,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
y_pred,
|
|
278
|
+
w_val,
|
|
279
|
+
loss_name=loss_name,
|
|
280
|
+
tweedie_power=tweedie_variance_power,
|
|
281
281
|
)
|
|
282
282
|
losses.append(float(loss))
|
|
283
283
|
self._clean_gpu()
|
|
@@ -345,4 +345,3 @@ class XGBTrainer(TrainerBase):
|
|
|
345
345
|
)
|
|
346
346
|
self.ctx.xgb_best = self.model
|
|
347
347
|
|
|
348
|
-
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Loss selection and regression loss utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ....explain.metrics import (
|
|
10
|
+
gamma_deviance,
|
|
11
|
+
poisson_deviance,
|
|
12
|
+
tweedie_deviance,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
LOSS_ALIASES = {
|
|
16
|
+
"poisson_deviance": "poisson",
|
|
17
|
+
"gamma_deviance": "gamma",
|
|
18
|
+
"tweedie_deviance": "tweedie",
|
|
19
|
+
"l2": "mse",
|
|
20
|
+
"l1": "mae",
|
|
21
|
+
"absolute": "mae",
|
|
22
|
+
"gaussian": "mse",
|
|
23
|
+
"normal": "mse",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
REGRESSION_LOSSES = {"tweedie", "poisson", "gamma", "mse", "mae"}
|
|
27
|
+
CLASSIFICATION_LOSSES = {"logloss", "bce"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_loss_name(loss_name: Optional[str], task_type: str) -> str:
|
|
31
|
+
"""Normalize the loss name and validate against supported values."""
|
|
32
|
+
name = str(loss_name or "auto").strip().lower()
|
|
33
|
+
if not name or name == "auto":
|
|
34
|
+
return "auto"
|
|
35
|
+
name = LOSS_ALIASES.get(name, name)
|
|
36
|
+
if task_type == "classification":
|
|
37
|
+
if name not in CLASSIFICATION_LOSSES:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Unsupported classification loss '{loss_name}'. "
|
|
40
|
+
f"Supported: {sorted(CLASSIFICATION_LOSSES)}"
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
if name not in REGRESSION_LOSSES:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Unsupported regression loss '{loss_name}'. "
|
|
46
|
+
f"Supported: {sorted(REGRESSION_LOSSES)}"
|
|
47
|
+
)
|
|
48
|
+
return name
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def infer_loss_name_from_model_name(model_name: str) -> str:
|
|
52
|
+
"""Preserve legacy heuristic for loss selection based on model name."""
|
|
53
|
+
name = str(model_name or "")
|
|
54
|
+
if "f" in name:
|
|
55
|
+
return "poisson"
|
|
56
|
+
if "s" in name:
|
|
57
|
+
return "gamma"
|
|
58
|
+
return "tweedie"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_tweedie_power(loss_name: str, default: float = 1.5) -> Optional[float]:
|
|
62
|
+
"""Resolve Tweedie power based on loss name."""
|
|
63
|
+
if loss_name == "poisson":
|
|
64
|
+
return 1.0
|
|
65
|
+
if loss_name == "gamma":
|
|
66
|
+
return 2.0
|
|
67
|
+
if loss_name == "tweedie":
|
|
68
|
+
return float(default)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_xgb_objective(loss_name: str) -> str:
|
|
73
|
+
"""Map regression loss name to XGBoost objective."""
|
|
74
|
+
name = loss_name if loss_name != "auto" else "tweedie"
|
|
75
|
+
mapping = {
|
|
76
|
+
"tweedie": "reg:tweedie",
|
|
77
|
+
"poisson": "count:poisson",
|
|
78
|
+
"gamma": "reg:gamma",
|
|
79
|
+
"mse": "reg:squarederror",
|
|
80
|
+
"mae": "reg:absoluteerror",
|
|
81
|
+
}
|
|
82
|
+
return mapping.get(name, "reg:tweedie")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def regression_loss(
|
|
86
|
+
y_true,
|
|
87
|
+
y_pred,
|
|
88
|
+
sample_weight=None,
|
|
89
|
+
*,
|
|
90
|
+
loss_name: str,
|
|
91
|
+
tweedie_power: Optional[float] = 1.5,
|
|
92
|
+
eps: float = 1e-8,
|
|
93
|
+
) -> float:
|
|
94
|
+
"""Compute weighted regression loss based on configured loss name."""
|
|
95
|
+
name = normalize_loss_name(loss_name, task_type="regression")
|
|
96
|
+
if name == "auto":
|
|
97
|
+
name = "tweedie"
|
|
98
|
+
|
|
99
|
+
y_t = np.asarray(y_true, dtype=float).reshape(-1)
|
|
100
|
+
y_p = np.asarray(y_pred, dtype=float).reshape(-1)
|
|
101
|
+
w = None if sample_weight is None else np.asarray(sample_weight, dtype=float).reshape(-1)
|
|
102
|
+
|
|
103
|
+
if name == "mse":
|
|
104
|
+
err = (y_t - y_p) ** 2
|
|
105
|
+
return _weighted_mean(err, w)
|
|
106
|
+
if name == "mae":
|
|
107
|
+
err = np.abs(y_t - y_p)
|
|
108
|
+
return _weighted_mean(err, w)
|
|
109
|
+
if name == "poisson":
|
|
110
|
+
return poisson_deviance(y_t, y_p, sample_weight=w, eps=eps)
|
|
111
|
+
if name == "gamma":
|
|
112
|
+
return gamma_deviance(y_t, y_p, sample_weight=w, eps=eps)
|
|
113
|
+
|
|
114
|
+
power = 1.5 if tweedie_power is None else float(tweedie_power)
|
|
115
|
+
return tweedie_deviance(y_t, y_p, sample_weight=w, power=power, eps=eps)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def loss_requires_positive(loss_name: str) -> bool:
|
|
119
|
+
"""Return True if the loss requires positive predictions."""
|
|
120
|
+
return loss_name in {"tweedie", "poisson", "gamma"}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _weighted_mean(values: np.ndarray, weight: Optional[np.ndarray]) -> float:
|
|
124
|
+
if weight is None:
|
|
125
|
+
return float(np.mean(values))
|
|
126
|
+
total = float(np.sum(weight))
|
|
127
|
+
if total <= 0:
|
|
128
|
+
return float(np.mean(values))
|
|
129
|
+
return float(np.sum(values * weight) / total)
|
|
@@ -24,7 +24,7 @@ import pandas as pd
|
|
|
24
24
|
import torch
|
|
25
25
|
import torch.nn as nn
|
|
26
26
|
from torch.nn.parallel import DistributedDataParallel as DDP
|
|
27
|
-
from sklearn.metrics import log_loss, mean_tweedie_deviance
|
|
27
|
+
from sklearn.metrics import log_loss, mean_absolute_error, mean_squared_error, mean_tweedie_deviance
|
|
28
28
|
from sklearn.model_selection import KFold, GroupKFold, TimeSeriesSplit, StratifiedKFold
|
|
29
29
|
|
|
30
30
|
# Try to import plotting dependencies
|
|
@@ -112,6 +112,7 @@ class MetricFactory:
|
|
|
112
112
|
self,
|
|
113
113
|
task_type: str = "regression",
|
|
114
114
|
tweedie_power: float = 1.5,
|
|
115
|
+
loss_name: str = "tweedie",
|
|
115
116
|
clip_min: float = 1e-8,
|
|
116
117
|
clip_max: float = 1 - 1e-8,
|
|
117
118
|
):
|
|
@@ -120,11 +121,13 @@ class MetricFactory:
|
|
|
120
121
|
Args:
|
|
121
122
|
task_type: Either 'regression' or 'classification'
|
|
122
123
|
tweedie_power: Power parameter for Tweedie deviance (1.0-2.0)
|
|
124
|
+
loss_name: Regression loss name ('tweedie', 'poisson', 'gamma', 'mse', 'mae')
|
|
123
125
|
clip_min: Minimum value for clipping predictions
|
|
124
126
|
clip_max: Maximum value for clipping predictions (for classification)
|
|
125
127
|
"""
|
|
126
128
|
self.task_type = task_type
|
|
127
129
|
self.tweedie_power = tweedie_power
|
|
130
|
+
self.loss_name = loss_name
|
|
128
131
|
self.clip_min = clip_min
|
|
129
132
|
self.clip_max = clip_max
|
|
130
133
|
|
|
@@ -151,13 +154,25 @@ class MetricFactory:
|
|
|
151
154
|
y_pred_clipped = np.clip(y_pred, self.clip_min, self.clip_max)
|
|
152
155
|
return float(log_loss(y_true, y_pred_clipped, sample_weight=sample_weight))
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
loss_name = str(self.loss_name or "tweedie").strip().lower()
|
|
158
|
+
if loss_name in {"mse", "mae"}:
|
|
159
|
+
if loss_name == "mse":
|
|
160
|
+
return float(mean_squared_error(
|
|
161
|
+
y_true, y_pred, sample_weight=sample_weight))
|
|
162
|
+
return float(mean_absolute_error(
|
|
163
|
+
y_true, y_pred, sample_weight=sample_weight))
|
|
164
|
+
|
|
155
165
|
y_pred_safe = np.maximum(y_pred, self.clip_min)
|
|
166
|
+
power = self.tweedie_power
|
|
167
|
+
if loss_name == "poisson":
|
|
168
|
+
power = 1.0
|
|
169
|
+
elif loss_name == "gamma":
|
|
170
|
+
power = 2.0
|
|
156
171
|
return float(mean_tweedie_deviance(
|
|
157
172
|
y_true,
|
|
158
173
|
y_pred_safe,
|
|
159
174
|
sample_weight=sample_weight,
|
|
160
|
-
power=
|
|
175
|
+
power=power,
|
|
161
176
|
))
|
|
162
177
|
|
|
163
178
|
def update_power(self, power: float) -> None:
|
|
@@ -52,6 +52,12 @@ except Exception:
|
|
|
52
52
|
|
|
53
53
|
# Import from other utils modules
|
|
54
54
|
from .constants import EPS, compute_batch_size, tweedie_loss, ensure_parent_dir
|
|
55
|
+
from .losses import (
|
|
56
|
+
infer_loss_name_from_model_name,
|
|
57
|
+
loss_requires_positive,
|
|
58
|
+
normalize_loss_name,
|
|
59
|
+
resolve_tweedie_power,
|
|
60
|
+
)
|
|
55
61
|
from .distributed_utils import DistributedUtils
|
|
56
62
|
|
|
57
63
|
|
|
@@ -359,11 +365,26 @@ class TorchTrainerMixin:
|
|
|
359
365
|
if task == 'classification':
|
|
360
366
|
loss_fn = nn.BCEWithLogitsLoss(reduction='none')
|
|
361
367
|
return loss_fn(y_pred, y_true).view(-1)
|
|
368
|
+
loss_name = normalize_loss_name(
|
|
369
|
+
getattr(self, "loss_name", None), task_type="regression"
|
|
370
|
+
)
|
|
371
|
+
if loss_name == "auto":
|
|
372
|
+
loss_name = infer_loss_name_from_model_name(getattr(self, "model_nme", ""))
|
|
362
373
|
if apply_softplus:
|
|
363
374
|
y_pred = F.softplus(y_pred)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
375
|
+
if loss_requires_positive(loss_name):
|
|
376
|
+
y_pred = torch.clamp(y_pred, min=1e-6)
|
|
377
|
+
power = resolve_tweedie_power(
|
|
378
|
+
loss_name, default=float(getattr(self, "tw_power", 1.5) or 1.5)
|
|
379
|
+
)
|
|
380
|
+
if power is None:
|
|
381
|
+
power = float(getattr(self, "tw_power", 1.5) or 1.5)
|
|
382
|
+
return tweedie_loss(y_pred, y_true, p=power).view(-1)
|
|
383
|
+
if loss_name == "mse":
|
|
384
|
+
return (y_pred - y_true).pow(2).view(-1)
|
|
385
|
+
if loss_name == "mae":
|
|
386
|
+
return (y_pred - y_true).abs().view(-1)
|
|
387
|
+
raise ValueError(f"Unsupported loss_name '{loss_name}' for regression.")
|
|
367
388
|
|
|
368
389
|
def _compute_weighted_loss(self, y_pred, y_true, weights, apply_softplus: bool = False):
|
|
369
390
|
"""Compute weighted loss."""
|