虽然使用对数损失(LogLoss)或均方误差(Mean Squared Error)等标准损失函数来优化模型是常见做法,但模型成功的最终衡量标准通常依赖于领域特定条件或竞赛规则,这些内置目标无法直接体现。训练和验证期间使用的评估指标作为指示器,引导超参数调整并通过提前停止来提示何时终止训练。当准确率(Accuracy)、F1分数、AUC或RMSE等标准指标无法完全体现所需性能特征时,实现自定义评估指标就变得有必要。与必须提供梯度信息以驱动提升过程的自定义损失函数不同,自定义评估指标更为简单。它们的主要作用是在特定时间间隔(例如,每个提升轮次)根据真实标签和预测值量化性能。它们不直接影响梯度计算,但对追踪进展和就模型的充分性做出明智判断不可或缺。为何使用自定义评估指标?在以下几种情况中,您可能需要自定义评估指标:业务场景的绩效衡量指标: 业务目标通常会转化为特定的绩效衡量指标(KPI)。例如,在金融领域,您可能更关心将超过某个阈值的大型预测误差最小化,而不是整体RMSE。在推荐系统中,像K均值平均精度(MAP@K)或归一化折现累积增益(NDCG)等指标可能比标准分类指标更适用。竞赛要求: 机器学习竞赛(例如,在Kaggle等平台上)经常规定独特的评估标准,例如用于一致性测量的二次加权Kappa(QWK)或精确率/召回率的自定义变体。训练一个模型以优化竞赛指标,即使仅用于监测和提前停止,也会带来优势。复杂的性能方面: 有时,您需要衡量标准实现未能涵盖的性能方面,例如评估不同子组之间的公平性,或者对某些类型的错误施加更重的惩罚(非对称误差成本)。监测不同方面: 您可以使用一种损失函数(例如,对数损失因其良好的数学属性)来优化模型,但使用更能反映最终用户体验的不同指标来监测性能(例如,针对不平衡分类问题的F1分数)。自定义指标函数的结构大多数流行的梯度提升库(XGBoost、LightGBM、CatBoost)都提供集成自定义评估逻辑的接口。虽然具体的参数名称可能略有不同,但基本结构通常是一致的。一个典型的Python自定义指标函数需要:接受预测值(y_pred)和真实标签(通常封装在特定库的数据结构中,如XGBoost的DMatrix或LightGBM的Dataset,可以从中提取真实标签y_true)。根据y_true和y_pred计算指标分数。以特定格式返回结果,通常是一个包含以下内容的元组:metric_name:标识指标的字符串(例如,'custom_rmse')。metric_value:计算得到的数值分数。is_higher_better:一个布尔值,表示分数越高是否代表性能越好(例如,AUC为True,RMSE为False)。这一点对提前停止很重要。让我们看看如何在实践中实现这一点。在XGBoost中实现自定义指标XGBoost通过其xgb.train函数中的feval参数或使用scikit-learn API时的eval_metric参数(该参数可接受可调用对象)支持自定义评估函数。对于xgb.train,函数签名应接受preds(预测值)和dtrain(一个包含真实标签的xgb.DMatrix对象)。import numpy as np import xgboost as xgb from sklearn.metrics import mean_absolute_percentage_error # 示例指标 # 定义一个自定义评估指标:平均绝对百分比误差(MAPE) # 注意:XGBoost的预测结果在转换前可能是原始分数 # 根据您的目标函数进行相应调整(例如,对二分类问题应用sigmoid) def xg_mape(preds: np.ndarray, dtrain: xgb.DMatrix): """XGBoost的自定义MAPE指标""" labels = dtrain.get_label() # 确保标签没有除以零或接近零的情况 # 裁剪标签以避免不稳定,或适当处理 epsilon = 1e-6 safe_labels = np.maximum(np.abs(labels), epsilon) mape = np.mean(np.abs((labels - preds) / safe_labels)) return 'MAPE', mape # 默认情况下,如果未另行指定,XGBoost认为自定义指标越低越好 # --- 示例数据(请替换为您的实际数据)--- X_train = np.random.rand(100, 5) y_train = np.random.rand(100) * 10 X_eval = np.random.rand(50, 5) y_eval = np.random.rand(50) * 10 dtrain = xgb.DMatrix(X_train, label=y_train) deval = xgb.DMatrix(X_eval, label=y_eval) # --- 使用自定义指标进行训练 --- params = { 'objective': 'reg:squarederror', 'eta': 0.1, 'max_depth': 3 } evals = [(dtrain, 'train'), (deval, 'eval')] # 将自定义函数传递给feval # 要用于提前停止,将maximize设置为False,因为MAPE应最小化 bst = xgb.train( params, dtrain, num_boost_round=100, evals=evals, feval=xg_mape, # 使用自定义指标进行提前停止: early_stopping_rounds=10, # 指定maximize=False,因为MAPE越低越好 # 注意:默认情况下,XGBoost在evals_result中查看*最后一个*指标进行停止 # 或者在使用sklearn API的fit方法时明确指定指标。 # 对于xgb.train,它通常隐式使用'eval_metric'列表中的最后一个指标 # 或'maximize'中指定的指标。我们假设我们希望在eval-MAPE上停止。 # 如果存在多个指标,可能需要进行控制。 # 为了明确起见,请查看XGBoost文档中关于feval + early_stopping特定版本行为。 verbose_eval=10 # 每10轮打印一次评估结果 ) print(f"\n验证集上的最佳MAPE: {bst.best_score}") 请注意: 当xgb.train与feval一起使用时,除非将maximize=True传递给xgb.train,否则XGBoost通常会假设指标应最小化。请查阅您特定版本的文档,特别是关于当使用多个标准和自定义指标时,提前停止是如何影响的。对于scikit-learn接口(XGBRegressor/XGBClassifier),您将可调用对象传递给eval_metric,并通过eval_set以及可能的专用提前停止参数来控制最大化。在LightGBM中实现自定义指标LightGBM使用与lgb.train中feval参数非常相似的机制。函数签名期望preds和train_data(一个lgb.Dataset对象)。import numpy as np import lightgbm as lgb from sklearn.metrics import matthews_corrcoef # 示例指标 # 定义一个自定义评估指标:Matthews相关系数(MCC) # 假设是带有概率输出的二分类 def lgbm_mcc(preds: np.ndarray, train_data: lgb.Dataset): """LightGBM的自定义MCC指标""" labels = train_data.get_label() # 将概率转换为二元预测(阈值为0.5) pred_labels = (preds > 0.5).astype(int) mcc = matthews_corrcoef(labels, pred_labels) # 返回格式:(指标名称, 值, 是否越高越好) return 'MCC', mcc, True # --- 示例数据(请替换为您的实际数据)--- X_train = np.random.rand(100, 5) y_train = np.random.randint(0, 2, size=100) X_eval = np.random.rand(50, 5) y_eval = np.random.randint(0, 2, size=50) lgb_train = lgb.Dataset(X_train, label=y_train) lgb_eval = lgb.Dataset(X_eval, label=y_eval, reference=lgb_train) # --- 使用自定义指标进行训练 --- params = { 'objective': 'binary', 'metric': 'binary_logloss', # 仍可追踪标准指标 'boosting_type': 'gbdt', 'num_leaves': 31, 'learning_rate': 0.05, 'feature_fraction': 0.9 } evals_result = {} # 用于存储评估结果 # 将自定义函数列表传递给feval bst = lgb.train( params, lgb_train, num_boost_round=100, valid_sets=[lgb_train, lgb_eval], valid_names=['train', 'eval'], evals_result=evals_result, feval=[lgbm_mcc], # 作为列表传递 callbacks=[ lgb.early_stopping(stopping_rounds=10, first_metric_only=False, verbose=True), lgb.log_evaluation(period=10) ] # 提前停止将使用params['metric']中指定的指标 # 以及由feval函数返回的任何指标。它正确使用'is_higher_better' # 标志。params中的'metric'参数决定优化目标。 # params中的'metric'和feval指标都会被监控用于提前停止。 ) print("\n包含自定义MCC的评估结果:") # print(evals_result) # 完整历史记录 print(f"验证集上的最佳MCC: {max(evals_result['eval']['MCC'])}") LightGBM要求自定义指标函数明确返回is_higher_better布尔值,这使得它与提前停止一起使用起来很简单。您可以在feval列表中传递多个自定义指标函数。在CatBoost中实现自定义指标CatBoost也通过其fit方法(针对CatBoostClassifier/CatBoostRegressor类)或train函数中的eval_metric参数支持自定义指标。您可以传递一个表示内置指标的字符串,或一个带有特定方法的Python对象(通常是一个类)。对于自定义指标,您通常需要定义一个至少包含以下方法的类:__init__(self):构造函数(可选)。is_max_optimal(self):如果指标值越高越好,则返回True,否则返回False。evaluate(self, approxes, target, weight):计算指标。approxes:包含每个文档的预测原始分数/值的列表的列表。对于多分类,每个类别一个列表。target:真实标签。weight:样本权重(可以是None)。应返回一个元组:(指标值之和,权重之和)。CatBoost在内部对其进行平均。get_final_error(self, error, weight):从evaluate返回的和中计算最终指标值。import numpy as np from catboost import CatBoostClassifier, Pool from sklearn.metrics import f1_score # 示例指标 # 定义一个用于F1分数的自定义指标类(二分类) class CatBoostF1Metric: def is_max_optimal(self): # True,因为F1分数越高越好 return True def evaluate(self, approxes, target, weight): # approxes是列表的列表,二分类需要扁平化 # 内部列表包含原始预测分数(例如,对数几率) # 使用sigmoid将原始分数转换为概率 assert len(approxes) == 1 # 预期二分类只有一个列表 preds_raw = np.array(approxes[0]) preds_prob = 1.0 / (1.0 + np.exp(-preds_raw)) # 将概率转换为二元预测 pred_labels = (preds_prob > 0.5).astype(int) # 计算F1分数 f1 = f1_score(target, pred_labels) # CatBoost期望指标值之和与权重之和 # 对于在整个数据集上计算的F1,我们可以返回 (f1 * 计数, 计数) # 或者,如果提供了权重,则进行处理。这里假设没有权重或权重相等。 count = len(target) if target is not None else 0 if count == 0: return 0.0, 0.0 # 如果目标为空,避免除以零 # 返回 (sum_metric, sum_weight) # 这里,权重实际上是用于简单平均的样本数量 return f1 * count, count def get_final_error(self, error_sum, weight_sum): # 计算最终的平均误差 if weight_sum == 0: return 0.0 # 避免除以零 return error_sum / weight_sum # --- 示例数据(请替换为您的实际数据)--- X_train = np.random.rand(100, 5) y_train = np.random.randint(0, 2, size=100) X_eval = np.random.rand(50, 5) y_eval = np.random.randint(0, 2, size=50) train_pool = Pool(X_train, label=y_train) eval_pool = Pool(X_eval, label=y_eval) # --- 使用自定义指标进行训练 --- model = CatBoostClassifier( iterations=100, learning_rate=0.1, loss_function='Logloss', custom_metric=[CatBoostF1Metric()], # 传递自定义指标类实例或列表 eval_metric='Logloss', # 仍可用于优化/报告的标准指标 early_stopping_rounds=10, verbose=10 ) model.fit( train_pool, eval_set=eval_pool # 默认情况下,CatBoost使用eval_metric中的第一个指标进行优化, # 但会监控所有指定的指标(包括自定义指标)用于提前停止。 # 它使用自定义指标类中is_max_optimal()方法的结果。 ) print("\n训练期间(在评估集上)的自定义F1指标值:") eval_metrics = model.get_evals_result() if 'learn' in eval_metrics and 'CatBoostF1Metric' in eval_metrics['learn']: print(f"训练集F1: {eval_metrics['learn']['CatBoostF1Metric'][-1]:.4f}") if 'validation' in eval_metrics and 'CatBoostF1Metric' in eval_metrics['validation']: print(f"评估集F1: {eval_metrics['validation']['CatBoostF1Metric'][-1]:.4f}") CatBoost为自定义指标提供的基于类的方法提供了一种结构化的方式来封装指标的逻辑和属性。请记住根据您的任务类型(二分类、多分类、回归)处理approxes的特定格式。自定义指标的考量计算成本: 自定义指标会频繁评估,可能在每个提升轮次以及交叉验证折叠中进行。请确保您的实现计算高效。如果可能,请避免冗余计算或高度复杂的运算。向量化操作(如NumPy)通常比显式循环更受欢迎。正确性: 在将自定义指标函数或类集成到提升管道之前,请使用已知输入和预期输出来独立彻底测试它。在训练循环内调试可能会很困难。与提前停止一起使用: is_higher_better标志(或CatBoost中的is_max_optimal方法)对于提前停止正确工作很重要。请确保它准确反映指标是应最大化还是最小化。提升库将使用此信息来判断验证集上的性能是否正在提升。预测格式: 注意传递给您函数的预测(preds或approxes)的格式。它们可能是原始分数(例如,逻辑回归的对数几率)、概率或最终预测值,具体取决于库、目标函数和特定的API调用。根据需要调整您的指标计算(例如,如果需要,应用sigmoid或softmax)。通过实现自定义评估指标,您可以获得对模型性能如何追踪和评估的更细致控制,从而使评估过程更贴合您的机器学习任务或业务目标的特定要求。此能力,结合自定义损失函数和可解释性工具,为构建高度定制化且有效的梯度提升模型提供了一个功能强大的工具集。