趋近智
虽然均方误差(MSE)用于回归或对数损失(Log Loss)用于分类等标准损失函数对于许多问题都有效,但它们并不总是与特定应用的具体目标或特点完全吻合。有时,您真正关心的业务指标并未由这些标准函数直接表示。例如,在需求预测场景中,您可能希望对高估的惩罚比低估的惩罚更重,或者在分类任务中,对某些类型的错误赋予更大的权重。XGBoost 和 LightGBM 等梯度提升框架提供了定义和优化您自己的自定义目标(损失)函数的灵活性。
回顾第2章,梯度提升通过顺序添加弱学习器(通常是树)来工作,这些学习器试图纠正之前集成模型的误差或残差。更正式地说,在每次提升迭代 中,我们希望找到一个新的函数 来最小化总损失 :
核心思想是通过将最优 拟合到损失函数相对于前一次预测 的负梯度来近似它。XGBoost 等现代提升库通过使用损失函数在每个实例 的预测 周围的二阶泰勒展开来改进这一点:
这里定义:
要实现自定义损失函数,您无需提供损失函数 本身。相反,您需要提供一个函数,它根据当前预测和真实标签,为每个数据点计算梯度 () 和 Hessian ()。然后,提升算法在内部使用这些值,特别是在树构建过程中(具体来说,是在计算分裂增益时)。
XGBoost 和 LightGBM 都要求函数具有特定的签名来计算梯度和 Hessian。
通用签名:
该函数通常接受两个参数:
preds: 包含模型当前预测值(所有 的 )的数组。请注意,对于分类,这些通常是最终变换前的原始分数(例如,sigmoid 之前的 logits)。dtrain 或 labels: 包含真实标签(所有 的 )的对象或数组。在 XGBoost 中,这通常是一个 DMatrix 对象,可以从中获取标签。在 LightGBM 中,它可能是 Dataset 对象或仅仅是标签数组。该函数必须返回两个数组:
grad: 包含每个实例的梯度 的数组。hess: 包含每个实例的 Hessian 的数组。我们来看一个例子:实现一个用于回归的非对称均方误差损失,其中高估(预测值 > 真实标签)比低估受到更重的惩罚。
示例:非对称均方误差
我们来定义一个损失函数,其中高估的惩罚是低估的 倍:
这里 代表模型的预测值(preds), 代表真实标签(labels)。我们假设 。
现在,我们需要损失函数相对于预测值 的一阶和二阶导数:
梯度 ():
Hessian ():
现在我们可以编写 Python 函数:
import numpy as np
def asymmetric_mse_obj(preds, dtrain):
"""非对称均方误差的自定义目标函数。"""
labels = dtrain.get_label() # 假设 dtrain 是一个 XGBoost DMatrix
residual = preds - labels
A = 1.5 # 示例:对高估施加1.5倍的惩罚
# 计算梯度
grad = np.where(preds <= labels, 2.0 * residual, 2.0 * A * residual)
# 计算Hessian
hess = np.where(preds <= labels, 2.0, 2.0 * A)
return grad, hess
# --- 对于 LightGBM,签名可能略有不同 ---
# 它通常直接接收标签而不是 Dataset 对象
def asymmetric_mse_obj_lgb(labels, preds):
"""非对称均方误差的自定义目标函数(LightGBM 风格)。"""
residual = preds - labels
A = 1.5 # 示例:对高估施加1.5倍的惩罚
# 计算梯度
grad = np.where(preds <= labels, 2.0 * residual, 2.0 * A * residual)
# 计算Hessian
hess = np.where(preds <= labels, 2.0, 2.0 * A)
return grad, hess
XGBoost:
使用 xgboost.train 函数时,您通过 obj 参数传递自定义目标函数。您可能还希望有一个反映您目标的自定义评估指标(feval)。
import xgboost as xgb
# 假设 X_train, y_train 已准备好
dtrain = xgb.DMatrix(X_train, label=y_train)
dvalid = xgb.DMatrix(X_valid, label=y_valid) # 可选,用于早停
params = {
'eta': 0.1,
'max_depth': 3
# 其他参数...
}
# 如果需要,定义一个自定义评估指标(可选)
def asymmetric_mse_eval(preds, dtrain):
labels = dtrain.get_label()
A = 1.5
errors = preds - labels
loss = np.where(preds <= labels, errors**2, A * (errors**2))
return 'asymMSE', np.mean(loss) # 返回指标名称和值
num_boost_round = 100
watchlist = [(dtrain, 'train'), (dvalid, 'eval')]
# 使用自定义目标和可选的自定义评估进行训练
bst = xgb.train(
params,
dtrain,
num_boost_round=num_boost_round,
obj=asymmetric_mse_obj, # 传递目标函数
feval=asymmetric_mse_eval, # 传递评估函数(可选)
evals=watchlist, # 使用 evals 进行监控/早停
early_stopping_rounds=10, # 可选的早停
maximize=False # False,因为我们希望最小化 asymMSE
)
如果使用 Scikit-learn 包装器(XGBRegressor、XGBClassifier),您通常可以在初始化时通过 objective 参数传递目标函数,尽管支持程度可能因目标函数的复杂性和 XGBoost 版本而异。查阅文档以获取具体信息。
LightGBM:
类似地,LightGBM 通过 lightgbm.train 或其 Scikit-learn 包装器中的 fobj 参数接受自定义目标函数。
import lightgbm as lgb
# 假设 X_train, y_train 已准备好
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train) # 可选
params = {
'objective': None, # 重要:使用 fobj 时设为 None
'metric': 'None', # 重要:使用 feval 时设为 None
'learning_rate': 0.1,
'num_leaves': 31
# 其他参数...
}
# 为 LightGBM 定义自定义评估指标(签名略有不同)
def asymmetric_mse_eval_lgb(labels, preds):
A = 1.5
errors = preds - labels
loss = np.where(preds <= labels, errors**2, A * (errors**2))
# 返回指标名称、值以及是否越大越好
return 'asymMSE', np.mean(loss), False
num_boost_round = 100
# 使用自定义目标和评估进行训练
gbm = lgb.train(
params,
lgb_train,
num_boost_round=num_boost_round,
valid_sets=lgb_eval,
fobj=asymmetric_mse_obj_lgb, # 传递目标函数
feval=asymmetric_mse_eval_lgb, # 传递评估函数
callbacks=[lgb.early_stopping(10, verbose=True)] # 使用回调函数进行早停
)
使用 LightGBM Scikit-learn API(LGBMRegressor、LGBMClassifier)时,您可以在初始化时将自定义目标函数可调用对象传递给 objective 参数。
preds 通常是在应用 sigmoid 或 softmax 变换之前的原始边际分数。您的梯度和 Hessian 计算必须相对于这些原始分数进行。例如,二元分类的标准对数损失目标函数 ,其中 ,要求相对于 而非 计算导数。梯度是 ,Hessian 是 。feval),它计算您定义的实际损失函数或您关心的主要业务指标。实现自定义损失函数提供了强大的功能,能够精确地将梯度提升模型适配到您问题的要求,从而优化真正重要的事情。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造