diff --git a/mqboost/base.py b/mqboost/base.py index a40012e..1dfc355 100644 --- a/mqboost/base.py +++ b/mqboost/base.py @@ -39,6 +39,7 @@ class ModelName(BaseEnum): class ObjectiveName(BaseEnum): check: str = "check" huber: str = "huber" + phuber: str = "phuber" class TypeName(BaseEnum): diff --git a/mqboost/objective.py b/mqboost/objective.py index 33f3c9a..e7e8cc8 100644 --- a/mqboost/objective.py +++ b/mqboost/objective.py @@ -21,6 +21,11 @@ def _grad_rho(u: np.ndarray, alpha: float) -> np.ndarray: return (u < 0).astype(int) - alpha +def _hess_rho(u: np.ndarray, alpha: float) -> np.ndarray: + _h = np.ones_like(u) + return _h + + def _rho(u: np.ndarray, alpha: float) -> np.ndarray: """ Compute the check loss function. @@ -62,6 +67,18 @@ def _grad_huber(u: np.ndarray, alpha: float, delta: float) -> np.ndarray: return _r * _smaller_delta + _g * _bigger_delta +def _grad_phuber(u: np.ndarray, alpha: float, delta: float) -> np.ndarray: + scale = delta**2 + u**2 + _g = -abs(_grad_rho(u, alpha)) * u / scale ** (1 / 2) + return _g + + +def _hess_phuber(u: np.ndarray, alpha: float, delta: float) -> np.ndarray: + scale = 1 + (u / delta) ** 2 + _h = (1 / delta) * abs(_grad_rho(u, alpha)) / (scale ** (3 / 2)) + return _h + + def _train_pred_reshape( y_pred: np.ndarray, dtrain: DtrainLike, @@ -85,6 +102,7 @@ def _compute_grads_hess( dtrain: DtrainLike, alphas: List[float], grad_fn: Callable[[np.ndarray, float, Any], np.ndarray], + hess_fn: Callable[[np.ndarray, float, Any], np.ndarray], **kwargs: Any, ) -> Tuple[np.ndarray, np.ndarray]: """ @@ -94,6 +112,7 @@ def _compute_grads_hess( dtrain (DtrainLike): The training data. alphas (List[float]): List of quantile levels. grad_fn (Callable): The gradient function to be used. + hess_fn (Callable): The Hessian function to be used. **kwargs (Any): Additional arguments for the gradient function. Returns: @@ -104,15 +123,26 @@ def _compute_grads_hess( y_pred=y_pred, dtrain=dtrain, len_alpha=_len_alpha ) grads = [] + hess = [] for alpha_inx in range(len(alphas)): _err_for_alpha = _y_train[alpha_inx] - _y_pred[alpha_inx] _grad = grad_fn(u=_err_for_alpha, alpha=alphas[alpha_inx], **kwargs) + _hess = hess_fn(u=_err_for_alpha, alpha=alphas[alpha_inx], **kwargs) grads.append(_grad) - return np.concatenate(grads), np.ones_like(y_pred) + hess.append(_hess) + return np.concatenate(grads), np.concatenate(hess) -check_loss_grad_hess: Callable = partial(_compute_grads_hess, grad_fn=_grad_rho) -huber_loss_grad_hess: Callable = partial(_compute_grads_hess, grad_fn=_grad_huber) + +check_loss_grad_hess: Callable = partial( + _compute_grads_hess, grad_fn=_grad_rho, hess_fn=_hess_rho +) +huber_loss_grad_hess: Callable = partial( + _compute_grads_hess, grad_fn=_grad_huber, hess_fn=_hess_rho +) +phuber_loss_grad_hess: Callable = partial( + _compute_grads_hess, grad_fn=_grad_phuber, hess_fn=_hess_phuber +) def _eval_check_loss( @@ -209,6 +239,11 @@ def __init__( self._fobj = partial(huber_loss_grad_hess, alphas=alphas, delta=self._delta) elif objective == ObjectiveName.check: self._fobj = partial(check_loss_grad_hess, alphas=alphas) + elif objective == ObjectiveName.phuber: + self._delta = delta_validate(delta=delta) + self._fobj = partial( + phuber_loss_grad_hess, alphas=alphas, delta=self._delta + ) self._eval_name = CHECK_LOSS if model == ModelName.lightgbm: diff --git a/mqboost/regressor.py b/mqboost/regressor.py index 49374f4..1588956 100644 --- a/mqboost/regressor.py +++ b/mqboost/regressor.py @@ -42,7 +42,7 @@ def __init__( params: Dict[str, Any], model: str = ModelName.lightgbm.value, objective: str = ObjectiveName.check.value, - delta: float = 0.05, + delta: float = 0.01, ) -> None: """Initialize the MQRegressor.""" self._params = params