hscredit.core.models.losses.adapters 源代码

"""框架适配器.

为 XGBoost、LightGBM、CatBoost、TabNet、NGBoost 等框架提供统一的自定义损失函数与
评估指标接口。各适配器统一处理 sigmoid 链接函数(原始分数→概率)及各框架的符号/接口
约定,使同一个 :class:`~hscredit.core.models.losses.base.BaseLoss` 可在不同框架间复用。
通常无需直接使用本模块,优先用 ``loss.to_xgboost()`` / ``to_lightgbm()`` /
``to_catboost()`` / ``to_ngboost()`` 便捷方法。

**引用(各框架自定义目标/评估文档)**

- XGBoost 自定义目标:https://xgboost.readthedocs.io/en/stable/tutorials/custom_metric_obj.html
- LightGBM 自定义目标(4.0+ 通过 ``params['objective']`` 传入):
  https://lightgbm.readthedocs.io/en/latest/Advanced-Topics.html
- CatBoost 自定义损失(``calc_ders_range``):
  https://catboost.ai/docs/concepts/python-usages-examples
- NGBoost 自定义 Score:https://stanfordmlgroup.github.io/ngboost/
"""

from typing import Callable, Tuple
import numpy as np
from .base import BaseLoss, BaseMetric, _margin_derivatives


[文档] class XGBoostLossAdapter: """XGBoost损失函数适配器. 将自定义损失函数转换为XGBoost可用的格式。 :param loss: 损失函数对象 **参考样例** >>> import xgboost as xgb >>> from hscredit.core.models.losses import FocalLoss, XGBoostLossAdapter >>> >>> # 创建损失函数 >>> loss = FocalLoss(alpha=0.75, gamma=2.0) >>> adapter = XGBoostLossAdapter(loss) >>> >>> # 在XGBoost中使用 >>> dtrain = xgb.DMatrix(X_train, label=y_train) >>> params = { ... 'objective': 'binary:logistic', ... 'eval_metric': 'auc' ... } >>> bst = xgb.train( ... params, ... dtrain, ... obj=adapter.objective(), ... num_boost_round=100 ... ) """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self) -> Callable: """获取XGBoost目标函数. :return: XGBoost格式的目标函数 """ def xgb_objective(preds: np.ndarray, dtrain) -> Tuple[np.ndarray, np.ndarray]: """XGBoost目标函数格式. :param preds: 预测值(原始分数,需要转换为概率) :param dtrain: 训练数据 :return: (梯度, 二阶导数) """ # 获取标签 labels = dtrain.get_label() # 将原始分数转换为概率(sigmoid) probs = 1.0 / (1.0 + np.exp(-preds)) return _margin_derivatives(self.loss, labels, probs) return xgb_objective
[文档] def metric(self, metric: BaseMetric) -> Callable: """获取XGBoost评估指标. :param metric: 评估指标对象 :return: XGBoost格式的评估指标 """ def xgb_metric(preds: np.ndarray, dtrain) -> Tuple[str, float]: """XGBoost评估指标格式. :param preds: 预测值(原始分数) :param dtrain: 数据 :return: (指标名称, 指标值) """ labels = dtrain.get_label() probs = 1.0 / (1.0 + np.exp(-preds)) value = metric(labels, probs) return metric.name, value return xgb_metric
[文档] class LightGBMLossAdapter: """LightGBM损失函数适配器. 将自定义损失函数转换为LightGBM可用的格式。 :param loss: 损失函数对象 **参考样例** >>> import lightgbm as lgb >>> from hscredit.core.models.losses import CostSensitiveLoss, LightGBMLossAdapter >>> >>> loss = CostSensitiveLoss(fn_cost=100, fp_cost=1) >>> adapter = LightGBMLossAdapter(loss) >>> >>> train_data = lgb.Dataset(X_train, label=y_train) >>> # objective() 采用 (y_true, y_pred) -> (grad, hess) 约定, >>> # 通过 params['objective'] 传入(LightGBM 4.0 起已移除 fobj 参数) >>> bst = lgb.train( ... params={'objective': adapter.objective(), 'metric': 'auc'}, ... train_set=train_data, ... num_boost_round=100 ... ) """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self) -> Callable: """获取LightGBM目标函数. :return: LightGBM格式的目标函数 """ def lgb_objective(y_true: np.ndarray, y_pred: np.ndarray): """LightGBM目标函数格式. :param y_true: 真实标签 :param y_pred: 预测值(原始分数) :return: (梯度, 二阶导数) """ # 将原始分数转换为概率 probs = 1.0 / (1.0 + np.exp(-y_pred)) return _margin_derivatives(self.loss, y_true, probs) return lgb_objective
[文档] def metric(self, metric: BaseMetric) -> Callable: """获取LightGBM评估指标. :param metric: 评估指标对象 :return: LightGBM格式的评估指标 """ def lgb_metric(y_true: np.ndarray, y_pred: np.ndarray): """LightGBM评估指标格式. :param y_true: 真实标签 :param y_pred: 预测值(原始分数) :return: (指标名称, 指标值, 是否越大越好) """ probs = 1.0 / (1.0 + np.exp(-y_pred)) value = metric(y_true, probs) return metric.name, value, metric.greater_is_better return lgb_metric
[文档] class CatBoostLossAdapter: """CatBoost损失函数适配器. 将自定义损失函数转换为CatBoost可用的格式。 :param loss: 损失函数对象 **参考样例** >>> from catboost import CatBoostClassifier >>> from hscredit.core.models.losses import BadDebtLoss, CatBoostLossAdapter >>> >>> loss = BadDebtLoss(target_approval_rate=0.3) >>> adapter = CatBoostLossAdapter(loss) >>> >>> model = CatBoostClassifier( ... iterations=1000, ... loss_function=adapter.objective(), ... eval_metric='AUC' ... ) >>> model.fit(X_train, y_train) """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self): """获取CatBoost目标函数. :return: CatBoost格式的目标函数类 """ loss_obj = self.loss class CatBoostLoss: def calc_ders_range(self, approxes, targets, weights): """计算梯度和二阶导. CatBoost 对一批样本调用本方法,``approxes``/``targets``/``weights`` 均为与样本一一对应的原始分数/标签/权重数组(二分类单目标)。 注意符号约定:CatBoost 执行梯度上升以最小化损失,要求返回 ``der1 = -dL/dapprox``、``der2 = -d²L/dapprox²``,因此对 ``BaseLoss`` 给出的(定义在概率上的)梯度/二阶导取负。 :param approxes: 预测原始分数数组(每个样本一个值) :param targets: 真实标签数组 :param weights: 样本权重数组,可为 None :return: 每个元素为 (一阶导, 二阶导) 的列表 """ approx = np.asarray(approxes, dtype=float) target = np.asarray(targets, dtype=float) # 将原始分数转换为概率 probs = 1.0 / (1.0 + np.exp(-approx)) # 转换为相对于 raw margin 的真实导数。 grad, hess = _margin_derivatives(loss_obj, target, probs) # CatBoost 约定:梯度上升最小化损失,取负 der1 = -grad der2 = -hess # 应用样本权重 if weights is not None: w = np.asarray(weights, dtype=float) der1 = der1 * w der2 = der2 * w return list(zip(der1.tolist(), der2.tolist())) return CatBoostLoss()
[文档] def metric(self, metric: BaseMetric): """获取CatBoost评估指标. :param metric: 评估指标对象 :return: CatBoost格式的评估指标类 """ metric_obj = metric class CatBoostMetric: def get_final_error(self, error, weight): return error def is_max_optimal(self): return metric_obj.greater_is_better def evaluate(self, approxes, target, weight): """计算指标值. :param approxes: 预测值列表 :param target: 真实标签列表 :param weight: 样本权重 :return: (指标值, 样本数量) """ assert len(approxes) == 1 approx = np.array(approxes[0]) target = np.array(target) # 转换为概率 probs = 1.0 / (1.0 + np.exp(-approx)) # 计算指标值 value = metric_obj(target, probs) return value, len(target) return CatBoostMetric()
def _tabnet_binary_loss_and_gradient(loss_obj: BaseLoss, logits, y_true): """计算 TabNet 二分类 raw logits 对应的平均损失和梯度。""" logits = np.asarray(logits, dtype=float) y_true = np.asarray(y_true, dtype=float).reshape(-1) if logits.ndim == 1: if len(logits) != len(y_true): raise ValueError("TabNet预测行数与标签行数不一致") probability = 1.0 / (1.0 + np.exp(-logits)) probability_gradient = np.asarray(loss_obj.gradient(y_true, probability), dtype=float) / max(1, len(y_true)) gradient = probability_gradient * probability * (1.0 - probability) elif logits.ndim == 2 and logits.shape[1] == 1: if logits.shape[0] != len(y_true): raise ValueError("TabNet预测行数与标签行数不一致") probability = 1.0 / (1.0 + np.exp(-logits[:, 0])) probability_gradient = np.asarray(loss_obj.gradient(y_true, probability), dtype=float) / max(1, len(y_true)) gradient = (probability_gradient * probability * (1.0 - probability))[:, None] elif logits.ndim == 2 and logits.shape[1] == 2: if logits.shape[0] != len(y_true): raise ValueError("TabNet预测行数与标签行数不一致") shifted = logits - np.max(logits, axis=1, keepdims=True) exp_logits = np.exp(shifted) probability = exp_logits[:, 1] / np.sum(exp_logits, axis=1) probability_gradient = np.asarray(loss_obj.gradient(y_true, probability), dtype=float) / max(1, len(y_true)) logit_gradient = probability_gradient * probability * (1.0 - probability) gradient = np.column_stack([-logit_gradient, logit_gradient]) else: raise ValueError("TabNet二分类损失仅支持一维、单列或两列raw logits") return float(loss_obj(y_true, probability)), gradient
[文档] class TabNetLossAdapter: """TabNet损失函数适配器. 将自定义损失函数转换为PyTorch可用的格式,适用于TabNet。 :param loss: 损失函数对象 **参考样例** >>> from pytorch_tabnet.tab_model import TabNetClassifier >>> from hscredit.core.models.losses import FocalLoss, TabNetLossAdapter >>> >>> loss = FocalLoss(alpha=0.75, gamma=2.0) >>> adapter = TabNetLossAdapter(loss) >>> >>> model = TabNetClassifier() >>> model.fit( ... X_train, y_train, ... loss_fn=adapter.loss_fn(), ... max_epochs=100 ... ) **注意** TabNet使用PyTorch,因此需要PyTorch环境。 """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def loss_fn(self): """获取PyTorch损失函数. :return: PyTorch格式的损失函数 """ try: import torch import torch.nn as nn except ImportError: raise ImportError("TabNet需要PyTorch环境,请先安装: pip install torch") loss_obj = self.loss class CustomLoss(nn.Module): class _NumpyLossFunction(torch.autograd.Function): @staticmethod def forward(ctx, y_pred, y_true): loss_value, gradient = _tabnet_binary_loss_and_gradient( loss_obj, y_pred.detach().cpu().numpy(), y_true.detach().cpu().numpy(), ) gradient_tensor = torch.as_tensor(gradient, dtype=y_pred.dtype, device=y_pred.device) ctx.save_for_backward(gradient_tensor) return y_pred.new_tensor(loss_value) @staticmethod def backward(ctx, grad_output): (gradient_tensor,) = ctx.saved_tensors return grad_output * gradient_tensor, None def forward(self, y_pred, y_true): """计算损失. :param y_pred: 二分类raw logits(一维、单列或两列) :param y_true: 真实标签 :return: 损失值 """ return self._NumpyLossFunction.apply(y_pred, y_true) return CustomLoss()
class NGBoostLossAdapter: """NGBoost损失函数适配器. 将自定义损失函数转换为NGBoost可用的Score类。 NGBoost使用自然梯度 + 概率分布框架,与XGBoost/LightGBM的 ``(grad, hess)`` 接口完全不同。本适配器通过链式法则将 ``BaseLoss`` 的梯度(对概率 p 求导) 转换为NGBoost所需的分布参数梯度(对 logit 求导):: dL/d(logit) = dL/dp × dp/d(logit) = dL/dp × p × (1 - p) :param loss: 损失函数对象 **参考样例** >>> from ngboost import NGBClassifier >>> from hscredit.core.models.losses import ExpectedProfitLoss, NGBoostLossAdapter >>> >>> loss = ExpectedProfitLoss(revenue=100, default_cost=1000) >>> adapter = NGBoostLossAdapter(loss) >>> >>> model = NGBClassifier( ... Score=adapter.score_class(), ... n_estimators=500, ... learning_rate=0.01 ... ) >>> model.fit(X_train, y_train) **注意** - 仅支持 ``Dist=Bernoulli``(NGBoost默认二分类分布) - ``score()`` 使用标准BCE作为监控/早停指标 - ``d_score()`` 使用自定义loss的梯度驱动自然梯度更新 - 也可直接使用 ``BaseLoss.to_ngboost()`` 快捷方法 """ def __init__(self, loss: BaseLoss): self.loss = loss def score_class(self): """获取NGBoost Score子类. :return: NGBoost Score子类(未实例化),可直接传给 ``NGBClassifier(Score=...)`` """ return self.loss.to_ngboost()