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
from scipy.special import expit


[文档] class XGBoostLossAdapter: """XGBoost 目标与配套指标,默认使用原生 train 接口。 **参数** :param loss: BaseLoss 实例,训练接收一维原始分数并自动处理样本权重。 **属性** ``loss`` 保存目标对象;指标默认使用独立的损失参数快照。 **参考样例** >>> from hscredit.core.models.losses import FocalLoss, XGBoostLossAdapter >>> adapter = XGBoostLossAdapter(FocalLoss(alpha=0.75)) >>> objective = adapter.objective(api='sklearn') >>> eval_metric = adapter.metric(api='sklearn') """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self, api="native"): """生成训练目标。 :param api: ``'native'`` 接收 (preds, DMatrix),``'sklearn'`` 接收 (y_true, y_pred, sample_weight=None);预测均为原始分数。 :return: 回调,返回对原始分数求导的 (grad, hess) 数组。 >>> from hscredit.core.models.losses import FocalLoss, XGBoostLossAdapter >>> objective = XGBoostLossAdapter(FocalLoss()).objective() """ return self.loss.to_xgboost(api=api)
[文档] def metric(self, metric=None, api="native"): """生成配合本自定义目标使用的评估回调,自动转换原始分数。 :param metric: BaseMetric 实例,默认 ``loss.metric()``。 :param api: ``'native'`` 或 ``'sklearn'``,须与训练接口相同。 :return: native 返回 (name, value),sklearn 返回 value 的回调。 不改变指标符号,早停方向须按 metric.greater_is_better 配置。 >>> from hscredit.core.models.losses import FocalLoss, AUCMetric, XGBoostLossAdapter >>> callback = XGBoostLossAdapter(FocalLoss()).metric(AUCMetric()) """ metric = self.loss.metric() if metric is None else metric return metric.to_xgboost(api=api, raw_score=True)
[文档] class LightGBMLossAdapter: """LightGBM 目标与配套指标,原生 train 需指定 api='native'。 **参数** :param loss: BaseLoss 实例,训练输入为一维原始分数。 **属性** ``loss`` 保存损失对象,样本权重与链接函数由目标回调统一处理。 **参考样例** >>> from hscredit.core.models.losses import FocalLoss, LightGBMLossAdapter >>> adapter = LightGBMLossAdapter(FocalLoss()) >>> objective, eval_metric = adapter.objective(), adapter.metric() """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self, api="sklearn"): """生成接收原始分数并返回 (grad, hess) 的训练回调。 :param api: 默认 ``'sklearn'`` 接收 (y_true, y_pred, sample_weight=None); ``'native'`` 接收 (preds, Dataset),用于 params['objective']。 :return: 目标回调,自动应用 sigmoid 和数据集权重。 >>> from hscredit.core.models.losses import FocalLoss, LightGBMLossAdapter >>> objective = LightGBMLossAdapter(FocalLoss()).objective(api='native') """ return self.loss.to_lightgbm(api=api)
[文档] def metric(self, metric=None, api="sklearn"): """生成自动转换原始分数的评估回调,和自定义目标配套使用。 :param metric: BaseMetric 实例,默认 ``loss.metric()``。 :param api: ``'sklearn'`` 或 ``'native'``,须与训练接口相同。 :return: 回调,返回 (name, value, greater_is_better)。 >>> from hscredit.core.models.losses import FocalLoss, KSMetric, LightGBMLossAdapter >>> callback = LightGBMLossAdapter(FocalLoss()).metric(KSMetric()) """ metric = self.loss.metric() if metric is None else metric return metric.to_lightgbm(api=api, raw_score=True)
[文档] class CatBoostLossAdapter: """CatBoost 逐样本损失与配套指标。 **参数** :param loss: 支持独立逐行计算的 BaseLoss,例如固定类别权重的 FocalLoss。 **属性** ``loss`` 保存损失对象;非可加损失会在生成训练目标时拒绝。 **参考样例** >>> from hscredit.core.models.losses import FocalLoss, CatBoostLossAdapter >>> adapter = CatBoostLossAdapter(FocalLoss()) >>> objective, eval_metric = adapter.objective(), adapter.metric() """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def objective(self): """生成 CatBoost 目标对象。 :return: 对象,calc_ders_range 接收原始分数、0/1 标签和可选权重, 输出逐样本 (-grad, -hess),符合 CatBoost 的符号约定。 :raises ValueError: 损失依赖全量排序、动态类别权重或绑定逐行数组。 >>> from hscredit.core.models.losses import FocalLoss, CatBoostLossAdapter >>> objective = CatBoostLossAdapter(FocalLoss()).objective() """ from .base import _objective_derivatives if not self.loss.is_additive: raise ValueError("CatBoost会分批计算梯度,不支持依赖全量排序、动态类别权重或样本金额数组的损失") loss = self.loss class CatBoostLoss: def calc_ders_range(self, approxes, targets, weights): grad, hess = _objective_derivatives(loss, targets, approxes, weights) return list(zip((-grad).tolist(), (-hess).tolist())) return CatBoostLoss()
[文档] def metric(self, metric=None): """生成 CatBoost 指标对象,按全量验证集保留指标语义。 :param metric: BaseMetric 实例,默认 loss.metric()。 :return: 对象,evaluate 接收原始分数并返回 (误差和, 权重和), get_final_error 还原指标值,is_max_optimal 返回优化方向。 >>> from hscredit.core.models.losses import FocalLoss, CatBoostLossAdapter >>> callback = CatBoostLossAdapter(FocalLoss()).metric() """ metric = self.loss.metric() if metric is None else metric return metric.to_catboost()
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 = expit(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 = expit(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: ``nn.Module``,调用签名为 (y_pred, y_true)。y_pred 是一维、 单列或两列原始 logits,y_true 是同批 0/1 标签;返回平均损失张量。 NumPy 计算的解析梯度由自定义 autograd 返回到 logits。 :raises ImportError: 未安装 PyTorch。 :raises ValueError: 损失依赖全量排序、动态类别权重或绑定样本数组。 >>> from hscredit.core.models.losses import FocalLoss, TabNetLossAdapter >>> loss_fn = TabNetLossAdapter(FocalLoss()).loss_fn() # doctest: +SKIP """ if not self.loss.is_additive: raise ValueError("TabNet按小批次训练,不支持依赖全量排序、动态类别权重或绑定样本数组的损失") 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( ... **loss.ngboost_params(), ... n_estimators=500, ... learning_rate=0.01 ... ) >>> model.fit(X_train, y_train) **注意** - 仅支持二分类 Bernoulli 分布;需使用本损失提供的分布子类。 - ``score()`` 与 ``d_score()`` 使用同一个自定义损失 - ``d_score()`` 使用自定义loss的梯度驱动自然梯度更新 - 推荐使用 ``loss.ngboost_params()`` 同时配套 Score 与 Dist。 """ def __init__(self, loss: BaseLoss): self.loss = loss
[文档] def score_class(self): """生成 NGBoost Score 子类,必须搭配返回类的 distribution。 :return: Score 类,score 返回逐行损失,d_score 返回 logit 导数, metric 返回 Bernoulli Fisher 信息;不直接接收框架外部预测。 :raises ImportError: 未安装 NGBoost。 :raises ValueError: 损失不支持独立逐行计算。 >>> from hscredit.core.models.losses import FocalLoss, NGBoostLossAdapter >>> from ngboost import NGBClassifier >>> score = NGBoostLossAdapter(FocalLoss()).score_class() >>> model = NGBClassifier(Score=score, Dist=score.distribution) """ return self.loss.to_ngboost()