Objectives¶
Decorators for custom loss functions and evaluation metrics. Functions are JIT-compiled with numba; the decorator handles all CatBoost internals (list-of-list approxes, sign convention, weights, sigmoid/softmax transform).
import numpy as np
from catboost_utils.objectives import objective, metric
@objective(task="regression")
def my_huber(y_true: np.ndarray, y_pred: np.ndarray):
delta = 1.0
err = y_pred - y_true
is_small = np.abs(err) <= delta
grad = np.where(is_small, err, delta * np.sign(err))
hess = np.where(is_small, 1.0, 0.0)
return grad, hess
@metric(task="regression", name="MAE", higher_is_better=False)
def mae(y_true, y_pred) -> float:
return float(np.mean(np.abs(y_true - y_pred)))
Signature contracts per task¶
task |
API | y_pred space | Signature |
|---|---|---|---|
regression |
batched calc_ders_range |
identity | (ndarray[n], ndarray[n]) -> (ndarray[n], ndarray[n]) |
binary |
batched calc_ders_range |
sigmoid | same as regression |
multiclass |
per-object calc_ders_multi |
softmax | (int, ndarray[K]) -> (ndarray[K], ndarray[K]) |
Tunable parameters¶
Parameters beyond y_true / y_pred (with type annotations and defaults) become tunable via .with_params(...):
@objective(task="regression")
def huber(y_true: np.ndarray, y_pred: np.ndarray, delta: float = 1.0):
err = y_pred - y_true
is_small = np.abs(err) <= delta
grad = np.where(is_small, err, delta * np.sign(err))
hess = np.where(is_small, 1.0, 0.0)
return grad, hess
# Use defaults
model = CatBoostRegressor(loss_function=huber, eval_metric="RMSE")
# Configure — returns a new adapter, original is unchanged
strict = huber.with_params(delta=0.5)
loose = huber.with_params(delta=2.0)
huber.get_params() # {"delta": 1.0}
strict.get_params() # {"delta": 0.5}
with_params(...) validates each value against its annotation: unknown name → TypeError, wrong type → TypeError. Numba does not recompile when values change.
Allowed annotations: int, float, bool. Every extra parameter must have a default. Anything else (untyped, Optional, np.ndarray, custom types) is rejected at decoration time.
Closure pattern for non-scalar parameters¶
For arrays, dicts, or any other non-scalar value, capture it via a closure:
import numpy as np
from catboost_utils.objectives import objective
def make_weighted_ce(class_weights: np.ndarray):
"""Return a multiclass cross-entropy with per-class weights."""
@objective(task="multiclass")
def loss(y_true: int, y_pred: np.ndarray):
# `class_weights` is captured from the outer scope; numba inlines it.
w = class_weights[y_true]
grad = w * (y_pred - _onehot(y_true, y_pred.shape[0]))
hess = w * np.ones_like(y_pred)
return grad, hess
return loss
def _onehot(idx: int, n: int) -> np.ndarray:
out = np.zeros(n)
out[idx] = 1.0
return out
# Build a configured loss
loss = make_weighted_ce(np.array([1.0, 2.0, 0.5]))
model = CatBoostClassifier(loss_function=loss, eval_metric="MultiClass")
Numba compiles loss once when it's first used; the array is baked into the compiled code. Building a second loss with different weights produces a separate compiled version — fine for one-time configuration, slow if you change weights every iteration.
catboost_utils.objectives.decorator.objective ¶
Wrap a user function as a CatBoost custom objective.
catboost_utils.objectives.decorator.metric ¶
metric(
*, task: TaskType, name: str, higher_is_better: bool
) -> Callable[[Callable[..., float]], Any]
Wrap a user function as a CatBoost custom evaluation metric.