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.
Files changed (28) hide show
  1. ins_pricing/cli/BayesOpt_entry.py +32 -0
  2. ins_pricing/cli/utils/import_resolver.py +29 -3
  3. ins_pricing/cli/utils/notebook_utils.py +3 -2
  4. ins_pricing/docs/modelling/BayesOpt_USAGE.md +3 -3
  5. ins_pricing/modelling/core/bayesopt/__init__.py +4 -0
  6. ins_pricing/modelling/core/bayesopt/config_preprocess.py +12 -0
  7. ins_pricing/modelling/core/bayesopt/core.py +21 -8
  8. ins_pricing/modelling/core/bayesopt/models/model_ft_components.py +38 -12
  9. ins_pricing/modelling/core/bayesopt/models/model_ft_trainer.py +16 -6
  10. ins_pricing/modelling/core/bayesopt/models/model_gnn.py +16 -6
  11. ins_pricing/modelling/core/bayesopt/models/model_resn.py +16 -7
  12. ins_pricing/modelling/core/bayesopt/trainers/trainer_base.py +2 -0
  13. ins_pricing/modelling/core/bayesopt/trainers/trainer_ft.py +25 -8
  14. ins_pricing/modelling/core/bayesopt/trainers/trainer_glm.py +14 -11
  15. ins_pricing/modelling/core/bayesopt/trainers/trainer_gnn.py +29 -10
  16. ins_pricing/modelling/core/bayesopt/trainers/trainer_resn.py +28 -12
  17. ins_pricing/modelling/core/bayesopt/trainers/trainer_xgb.py +13 -14
  18. ins_pricing/modelling/core/bayesopt/utils/losses.py +129 -0
  19. ins_pricing/modelling/core/bayesopt/utils/metrics_and_devices.py +18 -3
  20. ins_pricing/modelling/core/bayesopt/utils/torch_trainer_mixin.py +24 -3
  21. ins_pricing/production/predict.py +38 -9
  22. ins_pricing/setup.py +1 -1
  23. ins_pricing/utils/metrics.py +27 -3
  24. ins_pricing/utils/torch_compat.py +40 -0
  25. {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/METADATA +162 -162
  26. {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/RECORD +28 -27
  27. {ins_pricing-0.3.2.dist-info → ins_pricing-0.3.4.dist-info}/WHEEL +0 -0
  28. {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, mean_tweedie_deviance
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
- if self.ctx.obj == 'count:poisson':
23
+ loss_name = getattr(self.ctx, "loss_name", "tweedie")
24
+ if loss_name == "poisson":
23
25
  return sm.families.Poisson()
24
- if self.ctx.obj == 'reg:gamma':
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
- if self.ctx.task_type == 'regression' and self.ctx.obj == 'reg:tweedie':
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
- y_pred_safe = np.maximum(y_pred, EPS)
91
- return mean_tweedie_deviance(
95
+ return regression_loss(
92
96
  y_true,
93
- y_pred_safe,
94
- sample_weight=weight,
95
- power=self._metric_power(
96
- metric_ctx.get("family"), metric_ctx.get("tweedie_power"))
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, mean_tweedie_deviance
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=float(params.get("tw_power", base_tw_power or 1.5)),
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
- tw_power = params.get("tw_power", base_tw_power)
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
- y_pred_safe = np.maximum(y_pred, EPS)
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
- y_pred_safe,
93
- sample_weight=weight,
94
- power=power,
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 self.ctx.obj == 'reg:tweedie':
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, mean_tweedie_deviance
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
- power = params.get("tw_power", self.ctx.default_tweedie_power())
32
- if power is not None:
33
- power = float(power)
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
- power = params.get("tw_power", base_tw_power)
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
- params_local["tw_power"] = power
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 mean_tweedie_deviance(
113
+ return regression_loss(
98
114
  y_true,
99
115
  y_pred,
100
- sample_weight=weight,
101
- power=metric_ctx.get("tw_power", base_tw_power)
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 self.ctx.obj == 'reg:tweedie' else {})
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, mean_tweedie_deviance
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 self.ctx.obj == 'reg:tweedie':
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 self.ctx.obj == 'count:poisson':
240
- tweedie_variance_power = 1
241
- elif self.ctx.obj == 'reg:gamma':
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
- y_pred_safe = np.maximum(y_pred, EPS)
276
- loss = mean_tweedie_deviance(
275
+ loss = regression_loss(
277
276
  y_val,
278
- y_pred_safe,
279
- sample_weight=w_val,
280
- power=tweedie_variance_power,
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
- # Regression: use Tweedie deviance
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=self.tweedie_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
- y_pred = torch.clamp(y_pred, min=1e-6)
365
- power = getattr(self, "tw_power", 1.5)
366
- return tweedie_loss(y_pred, y_true, p=power).view(-1)
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."""