用于梯度提升的自定义损失函数

用于梯度提升的自定义损失函数

介绍

梯度提升技术在工业中得到广泛应用,并赢得了许多Kaggle 比赛。互联网已经对梯度提升有了很多很好的解释,但我们注意到缺乏关于自定义损失函数的信息:why,when and where。这篇文章是我们尝试总结自定义损失函数在许多实际问题中的重要性 - 以及如何使用LightGBM梯度提升包实现它们。

训练机器学习算法以最小化训练数据的损失函数。常见的机器学习(ML)库中有许多常用的损失函数。在现实世界中,这些“现成的”损失函数通常无法很好地适应我们试图解决的业务问题。

自定义损失函数

用于梯度提升的自定义损失函数

自定义损失函数方便的一个例子是机场准时的不对称风险。问题是决定何时离开房子,以便在合适的时间到达机场。我们不想太早去机场,在机场等几个小时。与此同时,我们不想错过我们的航班。任何一方的损失都是非常不同的:如果我们提前到达机场,那真的不是那么糟糕; 如果我们来得太晚而错过航班,那真的很糟糕。如果我们使用机器学习来决定何时离开房子,我们可能希望直接在我们的模型中处理这种风险不对称,通过使用自定义损失函数来惩罚 late errors而不是early errors。

另一个常见的例子是分类问题。例如,对于疾病检测,我们可能认为假阴性比假阳性要差得多,因为给予健康人的药物通常比未治疗病人的危害小。在这种情况下,我们可能希望优化F-beta评分,其中β取决于我们想要给予误报的权重。这有时被称为Neyman-Pearson标准。

我们最近遇到了一个问题,需要一个自定义损失函数。我们的客户Cortex Building Intelligence提供了一个应用程序,可以帮助工程师更精确地操作一栋建筑的供暖、通风和空调系统。大部分商业楼宇有“租契业务”,在办公时间内,例如在上午九时至下午六时的工作时间,把楼宇的室内温度调至“舒适”的温度范围,例如在70至74度之间。同时,HVAC是建筑最大的运营成本。高效的HVAC运行的关键是在不需要的时候关闭系统,比如在晚上,然后在清晨再次打开,以履行“租赁业务”。为此,建立了一个预测模型,以推荐在建筑物中打开HVAC系统的确切时间。

然而,错误预测的惩罚是不对称的。如果我们预测的启动时间早于实际需要的启动时间,那么建筑就会过早地达到舒适的温度,一些能量就会被浪费掉。但如果预测的时间晚于实际需要的开始时间,那么建筑温度就会过晚,租户就会不高兴——没有人愿意在冰冷/沸腾的建筑里工作、购物或学习。所以迟到比早退要糟糕得多,因为我们不希望租户(支付$$的租金)不开心。我们通过创建一个自定义的非对称Huber损失函数将业务知识编码到我们的模型中,该函数在残差为正或为负时具有更高的误差

用于梯度提升的自定义损失函数

Ignore what recovery time error means

摘要:找到一个与您的业务目标紧密匹配的损失函数。通常,这些损失函数在流行的机器学习库中没有默认实现。定义自己的损失函数并用它来解决问题并不难。

定制训练损失和验证损失

在进一步讨论之前,让我们在定义中明确一点。机器学习(ML)文献中使用了许多术语来指代不同的东西。我们将选择一组我们认为最清晰的定义:

  • 训练损失。这是在训练数据上优化的函数。例如,在神经网络二元分类器中,这通常是二进制交叉熵。对于随机森林分类器,这是基尼杂质。训练损失通常也称为“目标函数”。
  • 验证损失。这是我们用来评估我们训练模型在看不见的数据上的性能的函数。这通常与训练损失不同。例如,在分类器的情况下,这通常是受试者工作特征曲线(ROC)下的面积 - 虽然这从来没有直接优化,因为它是不可微的。这通常被称为“性能或评估度量”。

在许多情况下,定制这些损失对于构建更好的模型非常有效。这对于梯度提升特别简单,如下所示。

训练损失

在训练过程中,训练损失得到优化。对于某些算法来说很难定制,比如随机森林(见这里),但是对于其他算法来说相对容易,比如梯度提升和神经网络。由于梯度下降的某些变体通常是优化方法,训练损失通常需要一个具有凸梯度(一阶导数)和hessian (二阶导数)的函数。最好是连续的,有限的,非零的。最后一个很重要,因为函数为0的部分可以冻结梯度下降。

在梯度提升的情况下,训练损失是使用梯度下降优化的函数,例如,gradient boosting模型的“梯度”部分。具体地,训练损失的梯度用于改变每个连续树的目标变量。请注意,即使训练损失定义了“梯度”,每个树仍然使用贪婪分割算法生长,而不是绑定到这个自定义损失函数。

定义一个定制的训练损失通常需要我们做一些微积分来找到梯度和hessian。正如我们接下来将看到的,首先更改验证损失通常更容易,因为它不需要太多的开销。

验证损失

验证损失用于调整超参数。它通常更容易定制,因为它没有像训练损失那样多的要求。验证损失可以是非凸的,不可微分的和不连续的。因此,从定制开始通常是一个更容易的地方。

例如,在LightGBM中,一个重要的超参数是boosting rounds数。验证损失可用于找到最佳数量的boosting rounds。调用LightGBM中的验证损失eval_metric。我们可以使用库中可用的验证损失之一,也可以定义我们自己的自定义函数。由于它非常简单,因此您应该自定义是否对您的业务问题很重要。

具体来说,我们通常使用early_stopping_rounds变量,而不是直接优化num boosting rounds。当 early stopping rounds数量开始增加时,验证损失就停止增加。实际上,它通过监视样本验证集上的验证损失来防止过度拟合。如下图所示,设置更高的stopping rounds 会导致模型运行更多的boosting rounds。

用于梯度提升的自定义损失函数

蓝色:训练损失。橙色:验证损失 训练和验证都使用相同的自定义损失函数

用于梯度提升的自定义损失函数

k-fold交叉验证。每个测试fold都有验证损失

请记住,验证策略也非常重要。上面列出的验证/验证是许多可能的验证策略之一。它可能不适合您的问题。其他包括k-fold交叉验证和嵌套交叉验证,我们在HVAC启动时建模问题中使用了这些验证。

如果适用于业务问题,我们希望使用自定义函数来进行训练和验证损失。在某些情况下,由于自定义损失的函数形式,可能无法将其用作训练损失。在这种情况下,仅更新验证损失并使用像MSE这样的默认训练损失可能是有意义的。您仍然可以获益,因为超级参数将使用所需的自定义损失进行调整。

在LightGBM中实现自定义损失函数

让我们来看看它在实践中的样子,并对模拟数据进行一些实验。首先,让我们假设过高估计比低估更糟糕。另外,假设平方损失是我们在任一方向上的误差的良好模型。为了对其进行编码,我们定义了一个自定义MSE函数,它对正残差的惩罚比负残差多10倍。下图说明了我们的自定义损失函数与标准MSE损失函数的对比情况。

用于梯度提升的自定义损失函数

正如定义的那样,不对称MSE很好,因为它很容易计算梯度和hessian,它们被绘制在下面。注意,hessian在两个不同的值上是常量,左边是2,右边是20,尽管在下面的图中很难看到这一点。

用于梯度提升的自定义损失函数

LightGBM提供了一种直接的方式来实现自定义训练和验证损失。其他梯度提升包,包括XGBoost和Catboost,也提供此选项。但在较高的层次上,实现略有不同:

  • 训练损失:在LightGBM中自定义训练损失需要定义一个函数,该函数包含两个数组,即目标及其预测。反过来,该函数应该返回梯度的两个数组和每个观测值的hessian数组。如上所述,我们需要使用微积分来派生gradient和hessian,然后在Python中实现它。
  • 验证损失:自定义LightGBM中的验证损失需要定义一个函数,该函数接受相同的两个数组,但返回三个值:一个字符串,其名称为要打印的度量,损失本身,以及关于whether higher is better的布尔值。

用于在LightGBM中实现自定义损失的Python代码

定义自定义验证和培训损失函数,Python代码如下:

def custom_asymmetric_train(y_true, y_pred):
 residual = (y_true - y_pred).astype("float")
 grad = np.where(residual<0, -2*10.0*residual, -2*residual)
 hess = np.where(residual<0, 2*10.0, 2.0)
 return grad, hess
 
def custom_asymmetric_valid(y_true, y_pred):
 residual = (y_true - y_pred).astype("float")
 loss = np.where(residual < 0, (residual**2)*10.0, residual**2) 
 return "custom_asymmetric_eval", np.mean(loss), False

用于梯度提升的自定义损失函数

在LightGBM中结合训练和验证损失(包括Python和scikit-learn API示例)
import lightgbm
 
********* Sklearn API **********
# default lightgbm model with sklearn api
gbm = lightgbm.LGBMRegressor() 
 
# updating objective function to custom
# default is "regression"
# also adding metrics to check different scores
gbm.set_params(**{'objective': custom_asymmetric_train}, metrics = ["mse", 'mae'])
 
# fitting model 
gbm.fit(
 X_train,
 y_train,
 eval_set=[(X_valid, y_valid)],
 eval_metric=custom_asymmetric_valid,
 verbose=False,
)
 
y_pred = gbm.predict(X_valid)
 
********* Python API **********
# create dataset for lightgbm
# if you want to re-use data, remember to set free_raw_data=False
lgb_train = lgb.Dataset(X_train, y_train, free_raw_data=False)
lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, free_raw_data=False)
 
# specify your configurations as a dict
params = {
 'objective': 'regression',
 'verbose': 0
}
 
gbm = lgb.train(params,
 lgb_train,
 num_boost_round=10,
 init_model=gbm,
 fobj=custom_asymmetric_train,
 feval=custom_asymmetric_valid,
 valid_sets=lgb_eval)
 
y_pred = gbm.predict(X_valid)

用于梯度提升的自定义损失函数

自定义损失函数的实验

Jupyter notebook(https://github.com/manifoldai/mf-eng-public/blob/master/notebooks/custom_loss_lightgbm.ipynb)还对默认随机森林,默认LightGBM和MSE以及LightGBM与自定义培训和验证损失函数进行了深入比较。我们使用Friedman 1合成数据集,进行了8,000次训练观察,2,000次验证观察和5,000次测试观察。验证集用于查找优化验证损失的最佳超参数集。下面报告的分数在测试观察结果上进行评估,以评估我们模型的普遍性。

下表中总结的一系列实验。请注意,我们关心的最重要的分数是非对称MSE,因为它明确定义了我们的不对称惩罚问题。

用于梯度提升的自定义损失函数

实验和结果

让我们详细看一些比较。

随机森林→LightGBM

使用默认设置,LightGBM在此数据集上的性能优于Random Forest。随着更多树和超参数的更好组合,随机森林也可能会给出好的结果,但这不是重点。

LightGBM→LightGBM,具有定制的训练损失

这表明我们可以使我们的模型优化我们关心的内容。默认的LightGBM正在优化MSE,因此它可以降低MSE损失(0.24对0.33)。具有自定义训练损失的LightGBM优化了非对称MSE,因此对于非对称MSE(1.31 vs. 0.81)表现更好。

LightGBM → LightGBM with tuned early stopping rounds using MSE

两种LightGBM模型都在优化MSE。我们看到默认的MSE分数有了很大改善,只需稍微调整一下使用early stopping rounds(MSE:0.24 vs 0.14)。因此,我们应该让模型使用early stopping超参数来确定最佳提升次数,而不是将提升次数限制为默认值(即 100)。超参数优化很重要!

LightGBM with tuned early stopping rounds using MSE → LightGBM with tuned early stopping using custom MSE

这两个模型的得分非常接近,没有任何实质性差异。这是因为验证损失仅用于决定何时停止提升。梯度是在两种情况下优化默认MSE。每个后续树为两个模型生成相同的输出。唯一的区别是具有自定义验证损失的模型在742次增强迭代时停止,而另一次运行多次。

LightGBM with tuned early stopping using custom MSE → LightGBM trained on custom loss and tuned early stopping with MSE

仅在不改变验证损失的情况下定制训练损失会损害模型性能。只有自定义训练损失的模型比其他情况增加了更多轮次(1848)。如果我们仔细观察,这个模型的训练损失非常低(0.013)并且在训练集上非常过度拟合。每个梯度提升迭代使用训练误差作为目标变量来创建新树,但仅当验证数据的损失开始增加时,提升停止。当模型开始过度拟合时,验证损失通常开始增加,这是停止构建更多树木的信号。在这种情况下,由于验证和训练损失彼此不一致,因此模型似乎没有“得到消息”而导致过度拟合。

LightGBM with tuned early stopping rounds with MSE → LightGBM trained on custom training loss and tuned early stopping rounds with customized validation loss

最终模型使用自定义训练和验证损失。它通过相对较少的boosting迭代次数给出最佳的非对称MSE分数。损失与我们关心的一致!

让我们仔细看看残差直方图以获得更多细节。

用于梯度提升的自定义损失函数

不同模型预测残差的直方图

请注意,使用LightGBM(即使使用默认的超参数),与随机森林模型相比,预测性能也有所提高。具有自定义验证损失的最终模型似乎在直方图的右侧进行更多预测,即实际值大于预测值。这是由于非对称自定义损失函数的缘故。使用残差的核密度图可以更好地显示残差的right sided shift。

用于梯度提升的自定义损失函数

LightGBM模型的预测与对称和非对称评估的比较

结论

所有的机器模型都有误差,但是许多业务问题并不平等地对待低估和高估。有时,我们有意地希望我们的模型将误差偏向某个方向,这取决于哪些误差代价更高。因此,我们不应该局限于普通及其学学习(ML)库中的“现成的”对称损失函数。

LightGBM提供了一个简单的界面,可以包含自定义训练和验证损失函数。在适当的时候,我们应该利用这个函数来做出更好的预测。同时,您不应立即跳转到使用自定义损失函数。采用lean,迭代的方法总是最好的,首先从简单的基线模型开始,如随机森林。在下一次迭代中,您可以用更复杂的模型,如LightGBM,并进行超参数优化。只有在这些基线稳定后,才有必要进行定制验证和训练损失。

相关推荐