自定义目标函数和评估指标

目录

概述

XGBoost 设计为可扩展的库。扩展它的一种方式是提供我们自己的训练目标函数和相应的性能监控指标。本文档介绍了如何为 XGBoost 实现自定义的逐元素评估指标和目标函数。尽管本文以 Python 为例进行演示,但这些概念应易于应用于其他语言绑定。

注意

  • 排序任务不支持自定义函数。

  • XGBoost 1.6 版本中引入了重大变更。

有关更复杂目标的限制和变通方法,请参见高级用法示例:自定义目标函数高级用法

在接下来的两个章节中,我们将逐步介绍如何实现 平方对数误差 (SLE) 目标函数

\[\frac{1}{2}[\log(pred + 1) - \log(label + 1)]^2\]

及其默认指标 均方根对数误差(RMSLE)

\[\sqrt{\frac{1}{N}[\log(pred + 1) - \log(label + 1)]^2}\]

尽管 XGBoost 原生支持上述函数,但使用它进行演示可以让我们有机会比较我们自己的实现与 XGBoost 内部实现的结果,以便于学习。完成本教程后,我们应该能够提供自己的函数来进行快速实验。最后,我们将提供关于非恒等链接函数的注意事项,并提供在使用 scikit-learn 接口时使用自定义指标和目标函数的示例。

如果我们计算上述目标函数的梯度

\[g = \frac{\partial{objective}}{\partial{pred}} = \frac{\log(pred + 1) - \log(label + 1)}{pred + 1}\]

以及海塞矩阵(目标函数的二阶导数)

\[h = \frac{\partial^2{objective}}{\partial{pred}^2} = \frac{ - \log(pred + 1) + \log(label + 1) + 1}{(pred + 1)^2}\]

自定义目标函数

在模型训练过程中,目标函数起着重要作用:根据模型预测和观测数据标签(或目标)提供梯度信息,包括一阶和二阶梯度。因此,有效的目标函数应该接受两个输入,即预测和标签。为了实现 SLE,我们定义

import numpy as np
import xgboost as xgb
from typing import Tuple

def gradient(predt: np.ndarray, dtrain: xgb.DMatrix) -> np.ndarray:
    '''Compute the gradient squared log error.'''
    y = dtrain.get_label()
    return (np.log1p(predt) - np.log1p(y)) / (predt + 1)

def hessian(predt: np.ndarray, dtrain: xgb.DMatrix) -> np.ndarray:
    '''Compute the hessian for squared log error.'''
    y = dtrain.get_label()
    return ((-np.log1p(predt) + np.log1p(y) + 1) /
            np.power(predt + 1, 2))

def squared_log(predt: np.ndarray,
                dtrain: xgb.DMatrix) -> Tuple[np.ndarray, np.ndarray]:
    '''Squared Log Error objective. A simplified version for RMSLE used as
    objective function.
    '''
    predt[predt < -1] = -1 + 1e-6
    grad = gradient(predt, dtrain)
    hess = hessian(predt, dtrain)
    return grad, hess

在上面的代码片段中,squared_log 是我们想要的目标函数。它接受一个 numpy 数组 predt 作为模型预测,并接受训练 DMatrix 以获取所需信息,包括标签和权重(此处未使用)。然后,通过将其作为参数传递给 xgb.train,此目标函数在训练期间用作 XGBoost 的回调函数。

xgb.train({'tree_method': 'hist', 'seed': 1994},  # any other tree method is fine.
           dtrain=dtrain,
           num_boost_round=10,
           obj=squared_log)

请注意,在我们定义目标函数时,我们是使用预测减去标签还是标签减去预测非常重要。如果您发现训练误差不降反升,这可能是原因所在。

自定义评估指标

因此,在有了自定义目标函数后,我们可能还需要一个相应的指标来监控模型的性能。如上所述,SLE 的默认指标是 RMSLE。类似地,我们定义另一个类似回调的函数作为新的指标

def rmsle(predt: np.ndarray, dtrain: xgb.DMatrix) -> Tuple[str, float]:
    ''' Root mean squared log error metric.'''
    y = dtrain.get_label()
    predt[predt < -1] = -1 + 1e-6
    elements = np.power(np.log1p(y) - np.log1p(predt), 2)
    return 'PyRMSLE', float(np.sqrt(np.sum(elements) / len(y)))

由于我们在 Python 中进行演示,指标或目标函数不必是函数,任何可调用对象都应足够。与目标函数类似,我们的指标也接受 predtdtrain 作为输入,但返回指标本身的名称和一个浮点值作为结果。将其作为 custom_metric 参数传递给 XGBoost 后

xgb.train({'tree_method': 'hist', 'seed': 1994,
           'disable_default_eval_metric': 1},
          dtrain=dtrain,
          num_boost_round=10,
          obj=squared_log,
          custom_metric=rmsle,
          evals=[(dtrain, 'dtrain'), (dtest, 'dtest')],
          evals_result=results)

我们将能够看到 XGBoost 打印如下信息

[0] dtrain-PyRMSLE:1.37153  dtest-PyRMSLE:1.31487
[1] dtrain-PyRMSLE:1.26619  dtest-PyRMSLE:1.20899
[2] dtrain-PyRMSLE:1.17508  dtest-PyRMSLE:1.11629
[3] dtrain-PyRMSLE:1.09836  dtest-PyRMSLE:1.03871
[4] dtrain-PyRMSLE:1.03557  dtest-PyRMSLE:0.977186
[5] dtrain-PyRMSLE:0.985783 dtest-PyRMSLE:0.93057
...

请注意,参数 disable_default_eval_metric 用于禁止 XGBoost 中的默认指标。

有关完全可重现的源代码和比较图,请参见 定义自定义回归目标函数和指标的演示

Scikit-Learn 接口

XGBoost 的 scikit-learn 接口提供了一些实用工具来改善与标准 scikit-learn 函数的集成。例如,在 XGBoost 1.6.0 之后,用户可以直接使用 scikit-learn 中的成本函数(非评分函数)

from sklearn.datasets import load_diabetes
from sklearn.metrics import mean_absolute_error
X, y = load_diabetes(return_X_y=True)
reg = xgb.XGBRegressor(
    tree_method="hist",
    eval_metric=mean_absolute_error,
)
reg.fit(X, y, eval_set=[(X, y)])

此外,对于自定义目标函数,用户可以定义目标函数而无需访问 DMatrix

def softprob_obj(labels: np.ndarray, predt: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    rows = labels.shape[0]
    classes = predt.shape[1]
    grad = np.zeros((rows, classes), dtype=float)
    hess = np.zeros((rows, classes), dtype=float)
    eps = 1e-6
    for r in range(predt.shape[0]):
        target = labels[r]
        p = softmax(predt[r, :])
        for c in range(predt.shape[1]):
            g = p[c] - 1.0 if c == target else p[c]
            h = max((2.0 * p[c] * (1.0 - p[c])).item(), eps)
            grad[r, c] = g
            hess[r, c] = h

    grad = grad.reshape((rows * classes, 1))
    hess = hess.reshape((rows * classes, 1))
    return grad, hess

clf = xgb.XGBClassifier(tree_method="hist", objective=softprob_obj)