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

"""
风控业务损失函数

针对金融风控场景设计的专用损失函数,考虑坏账率、通过率、利润最大化等业务指标。
"""

import numpy as np
from typing import Optional, Dict
from .base import BaseLoss
from ._loss_math import binary_inputs, bce_terms, nonnegative, positive, unit_interval


[文档] class BadDebtLoss(BaseLoss): """坏账率优化损失函数,最小化坏账率同时保持通过率在合理水平。 适用于信贷审批场景,希望降低通过客户的坏账比例。 业务惩罚依赖硬阈值,训练仅使用 BCE 代理导数;本类不直接对坏账率求导。 :param target_approval_rate: 目标通过率,范围 (0, 1],默认为0.3 :param bad_debt_weight: 坏账率权重,默认为1.0 :param approval_weight: 通过率权重,默认为0.5 :param name: 损失函数名称,默认为"bad_debt_loss" **参考样例** >>> from hscredit.core.models.losses import BadDebtLoss >>> >>> # 目标通过率30%,重点优化坏账率 >>> loss = BadDebtLoss( ... target_approval_rate=0.3, ... bad_debt_weight=1.0, ... approval_weight=0.3 ... ) >>> >>> # 该目标依赖全量排序,使用 LightGBM 的全量目标回调 >>> from lightgbm import LGBMClassifier >>> model = LGBMClassifier( ... n_estimators=1000, ... objective=loss.to_lightgbm(), ... metric='None', ... ) >>> model.fit( ... X_train, y_train, eval_set=[(X_valid, y_valid)], ... eval_metric=loss.metric().to_lightgbm(raw_score=True), ... ) **引用** 在目标通过率约束下最小化坏账率,属信贷审批的业务驱动目标;成本敏感学习背景见 Elkan, C. (2001). *The Foundations of Cost-Sensitive Learning.* IJCAI 2001。 """ def __init__( self, target_approval_rate: float = 0.3, bad_debt_weight: float = 1.0, approval_weight: float = 0.5, name: str = "bad_debt_loss", ): super().__init__(name) unit_interval(target_approval_rate=target_approval_rate) positive(target_approval_rate=target_approval_rate) nonnegative(bad_debt_weight=bad_debt_weight, approval_weight=approval_weight) self.target_approval_rate = target_approval_rate self.bad_debt_weight = bad_debt_weight self.approval_weight = approval_weight def _compute_metrics(self, y_true, y_pred, threshold): """风险概率低于或等于阈值才通过;同分客户统一决策。""" y, p = binary_inputs(y_true, y_pred) approved = p <= threshold return { "approval_rate": float(np.mean(approved)), "bad_debt_rate": float(np.mean(y[approved])) if np.any(approved) else 0.0, } def __call__(self, y_true, y_pred) -> float: """BCE + 目标通过率处的硬坏账惩罚;业务项为分段常数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import BadDebtLoss >>> loss = BadDebtLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) count = int(np.ceil(len(p) * self.target_approval_rate)) threshold = np.sort(p)[count - 1] if count else -1.0 metrics = self._compute_metrics(y, p, threshold) return float( np.mean(bce_terms(y, p)[0]) + self.bad_debt_weight * metrics["bad_debt_rate"] + self.approval_weight * abs(metrics["approval_rate"] - self.target_approval_rate) )
[文档] def gradient(self, y_true, y_pred): """硬审批惩罚无连续导数,训练仅使用 BCE 代理梯度。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import BadDebtLoss >>> loss = BadDebtLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return bce_terms(y, p)[1]
[文档] def hessian(self, y_true, y_pred): """返回 BCE 代理目标的概率二阶导。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import BadDebtLoss >>> loss = BadDebtLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return bce_terms(y, p)[2]
[文档] class ApprovalRateLoss(BaseLoss): """通过率优化损失函数,在保证坏账率不超过目标的前提下最大化通过率。 硬阈值通过率用于评估与模型选择;训练仅使用 BCE 代理导数。 :param target_bad_debt_rate: 目标坏账率,默认为0.05 :param name: 损失函数名称,默认为"approval_rate_loss" **参考样例** >>> from hscredit.core.models.losses import ApprovalRateLoss >>> >>> # 目标坏账率不超过5% >>> loss = ApprovalRateLoss(target_bad_debt_rate=0.05) """ def __init__(self, target_bad_debt_rate: float = 0.05, name: str = "approval_rate_loss"): super().__init__(name) unit_interval(target_bad_debt_rate=target_bad_debt_rate) self.target_bad_debt_rate = target_bad_debt_rate def __call__(self, y_true, y_pred) -> float: """BCE + (1 - 可行通过率),其中硬阈值通过率为分段常数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import ApprovalRateLoss >>> loss = ApprovalRateLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return float(np.mean(bce_terms(y, p)[0]) + 1 - self.approval_rate(y, p))
[文档] def gradient(self, y_true, y_pred): """硬阈值业务项用于评估,训练使用 BCE 代理梯度。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import ApprovalRateLoss >>> loss = ApprovalRateLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return 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 ApprovalRateLoss >>> loss = ApprovalRateLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) return bce_terms(y, p)[2]
[文档] def approval_rate(self, y_true, y_pred) -> float: """在当前数据标签上回看、满足目标坏账率的最大阈值通过率。 用于离线评价,不应据此在测试集选择上线阈值;同分客户统一处理。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状的坏样本概率,低概率优先通过。 :return: float,范围 [0, 1];没有满足约束的客户组时为 0。 >>> value = ApprovalRateLoss(target_bad_debt_rate=0.05).approval_rate( ... [0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) order = np.argsort(p, kind="stable") counts = np.arange(1, len(y) + 1) debt_rates = np.cumsum(y[order]) / counts group_end = np.r_[p[order][1:] != p[order][:-1], True] feasible = (debt_rates <= self.target_bad_debt_rate) & group_end return float(np.max(counts[feasible]) / len(y)) if np.any(feasible) else 0.0
[文档] class ProfitMaxLoss(BaseLoss): """利润最大化损失函数,综合考虑坏账损失和利息收益最大化总利润。 训练目标为成本加权 BCE 代理;``profit()`` 单独计算实际阈值利润。 需要连续软利润目标时使用 ``ExpectedProfitLoss``。 :param interest_income: 单位利息收益,默认为1.0 :param bad_debt_loss: 单位坏账损失,默认为10.0 :param name: 损失函数名称,默认为"profit_max_loss" **参考样例** >>> from hscredit.core.models.losses import ProfitMaxLoss >>> >>> # 假设每笔贷款利息收益100元,坏账损失1000元 >>> loss = ProfitMaxLoss(interest_income=100, bad_debt_loss=1000) """ def __init__(self, interest_income: float = 1.0, bad_debt_loss: float = 10.0, name: str = "profit_max_loss"): super().__init__(name) nonnegative(interest_income=interest_income, bad_debt_loss=bad_debt_loss) positive(收益损失总和=interest_income + bad_debt_loss) self.is_additive = True self.interest_income = interest_income self.bad_debt_loss = bad_debt_loss def _compute_profit(self, y_true, y_pred, threshold) -> float: """兼容历史方法:返回低风险客户通过后的人均真实利润。""" return self.profit(y_true, y_pred, threshold) def __call__(self, y_true, y_pred) -> float: """利润成本加权 BCE 代理;实际硬决策利润通过 profit() 计算。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import ProfitMaxLoss >>> loss = ProfitMaxLoss() >>> 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): """返回损失相对坏样本概率的一阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import ProfitMaxLoss >>> loss = ProfitMaxLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[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 ProfitMaxLoss >>> loss = ProfitMaxLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[2]
def _terms(self, y_true, y_pred): y, p = binary_inputs(y_true, y_pred) weights = np.where(y == 1, self.bad_debt_loss, self.interest_income) return tuple(weights * term for term in bce_terms(y, p))
[文档] 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 ProfitMaxLoss >>> loss = ProfitMaxLoss() >>> result = loss.loss_values([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[0]
[文档] def profit(self, y_true, y_pred, threshold=0.5) -> float: """计算审批决策后的全体申请客户人均实际利润,越大越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状的坏样本概率。 :param threshold: 通过阈值,范围 [0, 1],默认 0.5;p < threshold 才通过。 :return: float,通过好客户计收益,通过坏客户计损失,拒绝计零; 分母是全量人数,不是通过人数。 >>> value = ProfitMaxLoss(interest_income=100, bad_debt_loss=1000).profit( ... [0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y, p = binary_inputs(y_true, y_pred) p = np.asarray(y_pred, dtype=float) unit_interval(threshold=threshold) profit = (1 - y) * self.interest_income - y * self.bad_debt_loss return float(np.mean((p < threshold) * profit))