Кастомные функции потерь¶
Декораторы для своих функций потерь и метрик. Пользователь пишет обычный numpy-код; всё, что нужно CatBoost (форматы данных, знаки производных, веса, преобразования через sigmoid или softmax), берёт на себя обёртка. Функции компилируются через numba.
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)))
Сигнатуры функций¶
Сигнатура зависит от задачи. CatBoost для разных задач даёт данные в разном формате — обёртка скрывает разницу, но сигнатура функции должна ей соответствовать.
task |
Что вызывает CatBoost | Пространство y_pred |
Сигнатура |
|---|---|---|---|
regression |
пакетно, calc_ders_range |
как есть | (ndarray[n], ndarray[n]) -> (ndarray[n], ndarray[n]) |
binary |
пакетно, calc_ders_range |
sigmoid | то же |
multiclass |
по одному объекту, calc_ders_multi |
softmax | (int, ndarray[K]) -> (ndarray[K], ndarray[K]) |
Настраиваемые параметры¶
Параметры после y_true / y_pred (с аннотацией типа и значением по умолчанию) становятся настраиваемыми через .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
# Со значениями по умолчанию
model = CatBoostRegressor(loss_function=huber, eval_metric="RMSE")
# С другими значениями — возвращает новый адаптер, исходный не меняется
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(...) проверяет каждое значение по аннотации: неизвестное имя → TypeError, неподходящий тип → TypeError. Numba не пересобирает функцию при смене значений.
Допустимые аннотации: int, float, bool. У каждого дополнительного параметра должно быть значение по умолчанию. Всё остальное (без аннотации, Optional, np.ndarray, свои типы) отвергается на этапе декорации.
Если нужен не скаляр — замыкание¶
Для массивов, словарей и других не-скалярных значений используйте замыкание:
import numpy as np
from catboost_utils.objectives import objective
def make_weighted_ce(class_weights: np.ndarray):
"""Multiclass cross-entropy с весами по классам."""
@objective(task="multiclass")
def loss(y_true: int, y_pred: np.ndarray):
# class_weights захватывается из внешней области; numba его подставит при компиляции.
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
# Готовый loss с конкретными весами
loss = make_weighted_ce(np.array([1.0, 2.0, 0.5]))
model = CatBoostClassifier(loss_function=loss, eval_metric="MultiClass")
Numba скомпилирует loss при первом использовании, а массив будет вшит в скомпилированный код. Если потом построить ещё один loss с другими весами — это отдельная компиляция. Подходит для разовой настройки; если веса хочется менять часто, лучше переписать через скалярные параметры.
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.