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

"""
期望利润损失函数

将收益、坏账损失和通过决策融合为连续可微的期望利润优化目标。
本模块通过 sigmoid 软通过门构造可微的利润代理,并用交叉熵正则约束概率拟合。
"""

from __future__ import annotations

import numpy as np

from .base import BaseLoss
from ._loss_math import binary_inputs, bce_terms, nonnegative, positive, unit_interval


def _sigmoid(x: np.ndarray) -> np.ndarray:
    """数值稳定的 sigmoid 函数。"""
    pos_mask = x >= 0
    z = np.zeros_like(x, dtype=float)
    z[pos_mask] = 1.0 / (1.0 + np.exp(-x[pos_mask]))
    exp_x = np.exp(x[~pos_mask])
    z[~pos_mask] = exp_x / (1.0 + exp_x)
    return z


[文档] class ExpectedProfitLoss(BaseLoss): """期望利润损失函数,将收益、坏账损失和通过决策融合为连续可微的期望利润优化目标。 本损失通过 sigmoid 软通过门构造可微的利润代理。 :class:`ProfitMaxLoss` 则使用收益和坏账成本加权 BCE,两者目标不同。 数学形式:: approve_i = σ((cutoff - p_i) / temperature) profit_i = (1 - y_i) × revenue - y_i × default_cost E[profit] = mean(approve_i × profit_i) Loss = -E[profit] + bce_weight × BCE(y, p) :param revenue: 好客户通过后的单位收益,默认 1.0 - 内部经验: 可设为平均利息收入或客均贡献 :param default_cost: 坏客户通过后的单位损失,默认 10.0 - 内部经验: 通常为 revenue 的 5~20 倍,视业务坏账回收率而定 :param cutoff: 软通过阈值,预测概率低于此值的样本倾向通过,默认 0.5 :param temperature: sigmoid 平滑温度,越小通过决策越接近硬阈值,默认 0.1 - 内部经验: 0.05~0.2 之间效果较好;太小梯度集中在阈值附近,太大退化为线性 :param bce_weight: 基础交叉熵正则权重,防止利润梯度消失,默认 0.1 :param name: 损失函数名称,默认 "expected_profit_loss" **参考样例** >>> import numpy as np >>> from hscredit.core.models.losses import ExpectedProfitLoss >>> >>> # 每笔贷款利息收益 100 元,坏账损失 1000 元 >>> loss = ExpectedProfitLoss( ... revenue=100, ... default_cost=1000, ... cutoff=0.5, ... temperature=0.1 ... ) >>> >>> y_true = np.array([0, 0, 1, 1]) >>> y_pred = np.array([0.1, 0.3, 0.7, 0.9]) >>> loss_value = loss(y_true, y_pred) >>> >>> # 在 XGBoost 中使用 >>> import xgboost as xgb >>> dtrain = xgb.DMatrix(X_train, label=y_train) >>> bst = xgb.train({}, dtrain, obj=loss.to_xgboost(), num_boost_round=100) **引用** 成本敏感学习与基于期望收益/损失的最优决策阈值见 Elkan, C. (2001). *The Foundations of Cost-Sensitive Learning.* IJCAI 2001. https://cseweb.ucsd.edu/~elkan/rescale.pdf 。本损失以 sigmoid 软通过门将利润目标 转为可微形式,属业务驱动设计。 """ def __init__( self, revenue: float = 1.0, default_cost: float = 10.0, cutoff: float = 0.5, temperature: float = 0.1, bce_weight: float = 0.1, name: str = "expected_profit_loss", ): super().__init__(name) nonnegative(revenue=revenue, default_cost=default_cost, bce_weight=bce_weight) unit_interval(cutoff=cutoff) positive(temperature=temperature) self.is_additive = True self.revenue = revenue self.default_cost = default_cost self.cutoff = cutoff self.temperature = temperature self.bce_weight = bce_weight def _approve_gate(self, y_pred: np.ndarray) -> np.ndarray: """计算软通过概率 σ((cutoff - p) / T)。""" z = (self.cutoff - y_pred) / self.temperature return _sigmoid(z) def _sample_profit(self, y_true: np.ndarray) -> np.ndarray: """计算每个样本的利润标签(好客户正利润,坏客户负利润)。""" return (1 - y_true) * self.revenue - y_true * self.default_cost def __call__(self, y_true, y_pred) -> float: """计算本损失的平均值,越小越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import ExpectedProfitLoss >>> loss = ExpectedProfitLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return float(np.mean(self.loss_values(y_true, y_pred)))
[文档] def gradient(self, y_true, y_pred): """相对概率求导:利润项 + bce_weight × (p-y)/(p(1-p))。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import ExpectedProfitLoss >>> loss = ExpectedProfitLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) gate = self._approve_gate(p) return gate * (1 - gate) / self.temperature * self._sample_profit(y) + self.bce_weight * bce_terms(y, p)[1]
[文档] def hessian(self, y_true, y_pred): """返回真实概率二阶导;非凸区域可以为负,由训练适配器稳定化。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import ExpectedProfitLoss >>> loss = ExpectedProfitLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) gate = self._approve_gate(p) profit_hess = -self._sample_profit(y) * gate * (1 - gate) * (1 - 2 * gate) / self.temperature**2 return profit_hess + self.bce_weight * bce_terms(y, p)[2]
[文档] def loss_values(self, y_true, y_pred): """负软通过利润与 BCE 正则的逐样本损失。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的数组,其均值等于本损失值;详见 BaseLoss.loss_values。 **参考样例** >>> from hscredit.core.models.losses import ExpectedProfitLoss >>> loss = ExpectedProfitLoss() >>> result = loss.loss_values([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return -self._approve_gate(p) * self._sample_profit(y) + self.bce_weight * bce_terms(y, p)[0]